@opengsd/gsd-core 1.7.0 → 1.9.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 (261) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +45 -1
  4. package/README.md +2 -0
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-codebase-mapper.md +1 -1
  7. package/agents/gsd-debug-session-manager.md +78 -4
  8. package/agents/gsd-debugger.md +87 -29
  9. package/agents/gsd-executor.md +49 -9
  10. package/agents/gsd-intel-updater.md +3 -3
  11. package/agents/gsd-phase-researcher.md +4 -2
  12. package/agents/gsd-plan-checker.md +20 -0
  13. package/agents/gsd-planner.md +44 -59
  14. package/agents/gsd-project-researcher.md +2 -2
  15. package/agents/gsd-ui-auditor.md +0 -40
  16. package/agents/gsd-verifier.md +2 -2
  17. package/bin/install.js +1338 -135
  18. package/commands/gsd/ai-integration-phase.md +1 -1
  19. package/commands/gsd/mempalace-capture.md +9 -5
  20. package/commands/gsd/new-milestone.md +1 -1
  21. package/commands/gsd/plan-phase.md +5 -3
  22. package/commands/gsd/plan-review-convergence.md +7 -2
  23. package/gsd-core/bin/gsd-tools.cjs +2690 -2472
  24. package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
  25. package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
  26. package/gsd-core/bin/lib/api-coverage.cjs +360 -53
  27. package/gsd-core/bin/lib/audit.cjs +8 -8
  28. package/gsd-core/bin/lib/broken-windows.cjs +716 -0
  29. package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
  30. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  31. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  32. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  33. package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
  34. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  35. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  36. package/gsd-core/bin/lib/capability-writer.cjs +6 -1
  37. package/gsd-core/bin/lib/check-command-router.cjs +140 -27
  38. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  39. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
  41. package/gsd-core/bin/lib/command-aliases.cjs +14 -0
  42. package/gsd-core/bin/lib/commands.cjs +326 -21
  43. package/gsd-core/bin/lib/config-loader.cjs +214 -30
  44. package/gsd-core/bin/lib/config.cjs +158 -22
  45. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  46. package/gsd-core/bin/lib/decisions.cjs +32 -8
  47. package/gsd-core/bin/lib/docs.cjs +6 -0
  48. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  49. package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
  50. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  51. package/gsd-core/bin/lib/gap-checker.cjs +17 -2
  52. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  53. package/gsd-core/bin/lib/init.cjs +155 -66
  54. package/gsd-core/bin/lib/install-engine.cjs +299 -23
  55. package/gsd-core/bin/lib/install-profiles.cjs +239 -1
  56. package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
  57. package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
  58. package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
  59. package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
  60. package/gsd-core/bin/lib/milestone.cjs +248 -14
  61. package/gsd-core/bin/lib/model-catalog.cjs +69 -4
  62. package/gsd-core/bin/lib/model-resolver.cjs +189 -7
  63. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  64. package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
  65. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  66. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  67. package/gsd-core/bin/lib/phase-id.cjs +304 -9
  68. package/gsd-core/bin/lib/phase.cjs +258 -17
  69. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  70. package/gsd-core/bin/lib/plan-scan.cjs +70 -2
  71. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  72. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  73. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  74. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  75. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  76. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  77. package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
  78. package/gsd-core/bin/lib/roadmap.cjs +23 -7
  79. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
  80. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
  81. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
  82. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  83. package/gsd-core/bin/lib/smart-entry.cjs +70 -5
  84. package/gsd-core/bin/lib/state-document.cjs +171 -24
  85. package/gsd-core/bin/lib/state-transition.cjs +50 -11
  86. package/gsd-core/bin/lib/state.cjs +206 -32
  87. package/gsd-core/bin/lib/surface.cjs +51 -9
  88. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  89. package/gsd-core/bin/lib/uat.cjs +428 -11
  90. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  91. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  92. package/gsd-core/bin/lib/validate.cjs +44 -8
  93. package/gsd-core/bin/lib/verification.cjs +163 -31
  94. package/gsd-core/bin/lib/verify.cjs +348 -42
  95. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  96. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  97. package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
  98. package/gsd-core/bin/shared/model-catalog.json +5 -0
  99. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  100. package/gsd-core/references/api-coverage.md +37 -7
  101. package/gsd-core/references/checkpoints.md +1 -1
  102. package/gsd-core/references/common-bug-patterns.md +13 -0
  103. package/gsd-core/references/context-budget.md +40 -0
  104. package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
  105. package/gsd-core/references/debugger-fix-acceptance.md +157 -0
  106. package/gsd-core/references/debugger-philosophy.md +1 -0
  107. package/gsd-core/references/debugger-prevention.md +98 -0
  108. package/gsd-core/references/debugger-rca-branching.md +98 -0
  109. package/gsd-core/references/debugger-repro-hardening.md +130 -0
  110. package/gsd-core/references/debugger-sbfl.md +110 -0
  111. package/gsd-core/references/debugger-semantic-recall.md +81 -0
  112. package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
  113. package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
  114. package/gsd-core/references/execute-phase-response-language.md +7 -0
  115. package/gsd-core/references/gate-prompts.md +6 -3
  116. package/gsd-core/references/model-profile-resolution.md +64 -13
  117. package/gsd-core/references/offer-next.md +88 -0
  118. package/gsd-core/references/planner-antipatterns.md +6 -0
  119. package/gsd-core/references/planner-mvp-mode.md +12 -13
  120. package/gsd-core/references/planner-preconditions.md +156 -0
  121. package/gsd-core/references/planner-reversibility.md +132 -0
  122. package/gsd-core/references/planning-config.md +2 -1
  123. package/gsd-core/references/reviewer-instances.md +28 -19
  124. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  125. package/gsd-core/references/skeleton-template.md +1 -1
  126. package/gsd-core/references/thinking-models-planning.md +3 -1
  127. package/gsd-core/references/ui-consideration-probe.md +2 -2
  128. package/gsd-core/references/worktree-branch-check.md +4 -4
  129. package/gsd-core/templates/DEBUG.md +5 -3
  130. package/gsd-core/templates/summary-minimal.md +4 -0
  131. package/gsd-core/templates/summary-standard.md +4 -0
  132. package/gsd-core/templates/summary.md +7 -0
  133. package/gsd-core/workflows/add-phase.md +2 -0
  134. package/gsd-core/workflows/add-tests.md +3 -1
  135. package/gsd-core/workflows/add-todo.md +32 -1
  136. package/gsd-core/workflows/ai-integration-phase.md +8 -6
  137. package/gsd-core/workflows/audit-fix.md +6 -2
  138. package/gsd-core/workflows/audit-milestone.md +8 -0
  139. package/gsd-core/workflows/autonomous.md +19 -15
  140. package/gsd-core/workflows/check-todos.md +5 -3
  141. package/gsd-core/workflows/cleanup.md +7 -1
  142. package/gsd-core/workflows/code-review-fix.md +14 -6
  143. package/gsd-core/workflows/code-review.md +93 -24
  144. package/gsd-core/workflows/complete-milestone.md +3 -0
  145. package/gsd-core/workflows/debug.md +35 -7
  146. package/gsd-core/workflows/diagnose-issues.md +5 -1
  147. package/gsd-core/workflows/discovery-phase.md +7 -0
  148. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  149. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  150. package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
  151. package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
  152. package/gsd-core/workflows/discuss-phase.md +2 -2
  153. package/gsd-core/workflows/do.md +7 -1
  154. package/gsd-core/workflows/docs-update.md +9 -0
  155. package/gsd-core/workflows/eval-review.md +4 -1
  156. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  157. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  158. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
  159. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
  160. package/gsd-core/workflows/execute-phase.md +110 -149
  161. package/gsd-core/workflows/execute-plan.md +20 -8
  162. package/gsd-core/workflows/explore.md +4 -0
  163. package/gsd-core/workflows/extract-learnings.md +21 -0
  164. package/gsd-core/workflows/graduation.md +3 -0
  165. package/gsd-core/workflows/health.md +7 -1
  166. package/gsd-core/workflows/help/modes/full.md +9 -5
  167. package/gsd-core/workflows/import.md +11 -2
  168. package/gsd-core/workflows/inbox.md +7 -0
  169. package/gsd-core/workflows/ingest-docs.md +19 -10
  170. package/gsd-core/workflows/manager.md +3 -1
  171. package/gsd-core/workflows/map-codebase.md +17 -10
  172. package/gsd-core/workflows/mvp-phase.md +3 -0
  173. package/gsd-core/workflows/new-milestone.md +79 -23
  174. package/gsd-core/workflows/new-project.md +28 -19
  175. package/gsd-core/workflows/new-workspace.md +3 -1
  176. package/gsd-core/workflows/next.md +5 -2
  177. package/gsd-core/workflows/onboard.md +3 -0
  178. package/gsd-core/workflows/plan-phase.md +56 -51
  179. package/gsd-core/workflows/plan-review-convergence.md +61 -12
  180. package/gsd-core/workflows/plant-seed.md +3 -0
  181. package/gsd-core/workflows/profile-user.md +7 -1
  182. package/gsd-core/workflows/progress.md +31 -3
  183. package/gsd-core/workflows/quick.md +33 -10
  184. package/gsd-core/workflows/remove-workspace.md +3 -0
  185. package/gsd-core/workflows/review.md +172 -585
  186. package/gsd-core/workflows/scan.md +10 -2
  187. package/gsd-core/workflows/secure-phase.md +13 -2
  188. package/gsd-core/workflows/settings-integrations.md +3 -0
  189. package/gsd-core/workflows/settings.md +3 -0
  190. package/gsd-core/workflows/ship.md +88 -11
  191. package/gsd-core/workflows/sketch.md +3 -0
  192. package/gsd-core/workflows/smart-entry.md +4 -1
  193. package/gsd-core/workflows/spike.md +7 -1
  194. package/gsd-core/workflows/ui-phase.md +11 -2
  195. package/gsd-core/workflows/ui-review.md +11 -1
  196. package/gsd-core/workflows/undo.md +7 -0
  197. package/gsd-core/workflows/update.md +106 -5
  198. package/gsd-core/workflows/validate-phase.md +13 -2
  199. package/gsd-core/workflows/verify-phase.md +2 -2
  200. package/gsd-core/workflows/verify-work.md +15 -4
  201. package/hooks/dist/gsd-context-monitor.js +27 -9
  202. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  203. package/hooks/dist/gsd-cursor-stop.js +6 -2
  204. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  205. package/hooks/dist/gsd-graphify-update.sh +9 -0
  206. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  207. package/hooks/dist/gsd-prompt-guard.js +101 -2
  208. package/hooks/dist/gsd-read-guard.js +100 -2
  209. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  210. package/hooks/dist/gsd-statusline.js +97 -9
  211. package/hooks/dist/gsd-workflow-guard.js +110 -6
  212. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  213. package/hooks/dist/lib/cursor-workspace.js +74 -0
  214. package/hooks/gsd-context-monitor.js +27 -9
  215. package/hooks/gsd-cursor-session-start.js +6 -2
  216. package/hooks/gsd-cursor-stop.js +6 -2
  217. package/hooks/gsd-cursor-subagent-start.js +6 -2
  218. package/hooks/gsd-graphify-update.sh +9 -0
  219. package/hooks/gsd-phase-boundary.sh +14 -2
  220. package/hooks/gsd-prompt-guard.js +101 -2
  221. package/hooks/gsd-read-guard.js +100 -2
  222. package/hooks/gsd-read-injection-scanner.js +109 -2
  223. package/hooks/gsd-statusline.js +97 -9
  224. package/hooks/gsd-workflow-guard.js +110 -6
  225. package/hooks/gsd-worktree-path-guard.js +132 -8
  226. package/hooks/lib/cursor-workspace.js +74 -0
  227. package/package.json +10 -8
  228. package/pi/gsd.cjs +34 -3
  229. package/scripts/changeset/lint.cjs +1 -0
  230. package/scripts/changeset/parse.cjs +26 -0
  231. package/scripts/check-coverage-gate.cjs +51 -0
  232. package/scripts/check-glossary-refs.cjs +244 -0
  233. package/scripts/ci-rebase-check.cjs +48 -4
  234. package/scripts/ci-test-scope.cjs +67 -17
  235. package/scripts/gen-adr-index.cjs +528 -0
  236. package/scripts/gen-capability-matrix.cjs +26 -2
  237. package/scripts/gen-capability-registry.cjs +132 -34
  238. package/scripts/gen-emitted-baseline.cjs +145 -0
  239. package/scripts/gen-test-timings.cjs +201 -0
  240. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  241. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  242. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  243. package/scripts/lint-portable-timeout.cjs +140 -0
  244. package/scripts/lint-resolution-provenance.cjs +9 -0
  245. package/scripts/lint-test-file-count.allowlist.json +1 -0
  246. package/scripts/mutation-matrix.cjs +4 -0
  247. package/scripts/prompt-injection-scan.sh +6 -0
  248. package/scripts/registry-schema.cjs +57 -8
  249. package/scripts/release-notes/conventional-title.cjs +19 -1
  250. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  251. package/scripts/release-tarball-smoke.cjs +18 -11
  252. package/scripts/run-tests.cjs +420 -58
  253. package/scripts/workflow-size.cjs +16 -8
  254. package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
  255. package/skills/gsd-mempalace-capture/SKILL.md +9 -5
  256. package/skills/gsd-new-milestone/SKILL.md +1 -1
  257. package/skills/gsd-plan-phase/SKILL.md +5 -3
  258. package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
  259. package/vscode/package.json +1 -1
  260. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  261. package/scripts/update-size-baseline.cjs +0 -68
@@ -71,6 +71,7 @@ const {
71
71
  validateArtifactKindEntry,
72
72
  validateArtifactLayout,
73
73
  validateRuntimeBody,
74
+ collectReviewerWarnings,
74
75
  materializeHookFragments,
75
76
  validateAgainstContract,
76
77
  validateConsumesGlobal,
@@ -128,6 +129,71 @@ function loadCentralConfigKeys(schemaPath = CONFIG_SCHEMA_PATH) {
128
129
  return new Set(Array.isArray(manifest.validKeys) ? manifest.validKeys : []);
129
130
  }
130
131
 
132
+ /**
133
+ * Loads the central config-schema's DYNAMIC key patterns (#2797).
134
+ *
135
+ * `loadCentralConfigKeys` above reads `validKeys` only, so a federated key
136
+ * claimed by a central *pattern* was invisible to the exclusivity check. That is
137
+ * not a cosmetic gap: `isCentralConfigKey` consults these same patterns, and
138
+ * `mergeFederatedConfig` skips every key for which it returns true — so an
139
+ * overlapping slice is inert while the build stays green.
140
+ *
141
+ * The patterns are read from the SAME manifest the runtime reads and compiled
142
+ * with the same `source`, rather than re-implementing a matcher here, so the two
143
+ * cannot drift.
144
+ *
145
+ * Failure contract MATCHES `loadCentralConfigKeys` above deliberately — the two
146
+ * read the same file and must not disagree about what a broken one means:
147
+ * - ENOENT → empty list. Legitimately absent.
148
+ * - Any other read error, or a JSON parse error → prominent stderr + throw.
149
+ *
150
+ * An earlier revision swallowed the parse error and returned []. That is
151
+ * fail-OPEN on the gate this function exists to feed: with zero patterns,
152
+ * `validateCrossCapability`'s pattern-collision check silently passes and an
153
+ * inert federated slice ships green. It was masked in the one production call
154
+ * site only because `loadCentralConfigKeys` runs first against the same path and
155
+ * throws — a coincidence of ordering, not a guarantee, and this function is
156
+ * exported and called standalone.
157
+ *
158
+ * A single unparseable PATTERN is still skipped rather than fatal: that is a
159
+ * per-entry defect the central schema's own tests own, and skipping one pattern
160
+ * degrades to "checked less" rather than blocking every build.
161
+ */
162
+ function loadCentralConfigPatterns(schemaPath = CONFIG_SCHEMA_PATH) {
163
+ let raw;
164
+ try {
165
+ raw = fs.readFileSync(schemaPath, 'utf8');
166
+ } catch (err) {
167
+ if (err && err.code === 'ENOENT') return [];
168
+ process.stderr.write(
169
+ ' ERROR Failed to read config-schema manifest at ' + schemaPath + ': ' + err.message + '\n',
170
+ );
171
+ throw new ExitError(1, 'could not read config-schema manifest');
172
+ }
173
+
174
+ let manifest;
175
+ try {
176
+ manifest = JSON.parse(raw);
177
+ } catch (err) {
178
+ process.stderr.write(
179
+ ' ERROR Config-schema manifest at ' + schemaPath + ' is broken (JSON parse error): ' + err.message + '\n',
180
+ );
181
+ throw new ExitError(1, 'config-schema manifest JSON is malformed');
182
+ }
183
+ const declared = Array.isArray(manifest.dynamicKeyPatterns) ? manifest.dynamicKeyPatterns : [];
184
+ const out = [];
185
+ for (const entry of declared) {
186
+ const src = entry && typeof entry.source === 'string' ? entry.source : null;
187
+ if (!src) continue;
188
+ try {
189
+ out.push(new RegExp(src));
190
+ } catch {
191
+ // Unparseable pattern — the central schema's own tests own that failure.
192
+ }
193
+ }
194
+ return out;
195
+ }
196
+
131
197
  // ─── ADR-857 Phase 4a: Derived views ─────────────────────────────────────────
132
198
 
133
199
  // (Config-slice validation, per-capability validators, contract validators,
@@ -335,14 +401,21 @@ function runConsistencyGate(capabilityClusters, profileMembership, capMap) {
335
401
  * (used during 3a-impl while migration is in-progress).
336
402
  * @param {string} [capabilitiesDir] Override capabilities dir (for testing with fixtures).
337
403
  */
338
- function loadAndValidate(centralKeys, capabilitiesDir) {
404
+ function loadAndValidate(centralKeys, capabilitiesDir, centralPatterns) {
339
405
  const resolvedCentralKeys = centralKeys !== undefined ? centralKeys : loadCentralConfigKeys();
406
+ // #2797: patterns default to the real manifest unless a caller passes its own
407
+ // (tests pass [] to isolate the exact-key path).
408
+ const resolvedCentralPatterns = centralPatterns !== undefined ? centralPatterns : loadCentralConfigPatterns();
340
409
  const resolvedCapDir = capabilitiesDir !== undefined ? capabilitiesDir : CAPABILITIES_DIR;
341
410
  const errors = [];
342
411
  const capMap = new Map();
412
+ // ADR-2782 D4 — non-fatal diagnostics (e.g. an unknown field inside a reviewer
413
+ // body). These NEVER fail the build; they surface on stderr so a forward-built
414
+ // manifest degrades visibly instead of silently.
415
+ const warnings = [];
343
416
 
344
417
  if (!fs.existsSync(resolvedCapDir)) {
345
- return { capMap, errors };
418
+ return { capMap, errors, warnings };
346
419
  }
347
420
 
348
421
  // Compute wired points ONCE before iterating capabilities so the filesystem
@@ -366,6 +439,10 @@ function loadAndValidate(centralKeys, capabilitiesDir) {
366
439
  continue;
367
440
  }
368
441
 
442
+ // Collected BEFORE the error short-circuit below so a manifest that is both
443
+ // forward-built and invalid still reports why it looked unfamiliar.
444
+ for (const w of collectReviewerWarnings(cap)) warnings.push(folderId + '/capability.json: ' + w);
445
+
369
446
  const capErrors = validateCapability(cap, folderId);
370
447
  if (capErrors.length > 0) {
371
448
  for (const e of capErrors) errors.push(folderId + '/capability.json: ' + e);
@@ -397,7 +474,7 @@ function loadAndValidate(centralKeys, capabilitiesDir) {
397
474
  }
398
475
 
399
476
  // Cross-capability invariants — capMap contains only fully-valid capabilities at this point.
400
- const crossErrors = validateCrossCapability(capMap, resolvedCentralKeys);
477
+ const crossErrors = validateCrossCapability(capMap, resolvedCentralKeys, resolvedCentralPatterns);
401
478
  errors.push(...crossErrors);
402
479
 
403
480
  // C2: Global consumes-satisfiability — runs after capMap is fully built so cross-capability
@@ -406,7 +483,7 @@ function loadAndValidate(centralKeys, capabilitiesDir) {
406
483
  const consumesErrors = validateConsumesGlobal(capMap);
407
484
  errors.push(...consumesErrors);
408
485
 
409
- return { capMap, errors };
486
+ return { capMap, errors, warnings };
410
487
  }
411
488
 
412
489
  /**
@@ -446,6 +523,46 @@ function buildRegistry(capMap) {
446
523
  if (capId === '__proto__' || capId === 'constructor' || capId === 'prototype') continue;
447
524
  capabilities[capId] = cap;
448
525
 
526
+ // Federated config slice — harvested from ANY role that declares one.
527
+ //
528
+ // ADR-2782 D1/D9: this loop was nested inside the `role === 'feature'` branch,
529
+ // so a `role: "runtime"` capability's `config` was read by nothing and dropped
530
+ // in silence — the actual reason reviewer config keys are stranded in the
531
+ // central schema. (The often-cited reason, that the runtime body forbids
532
+ // feature-only fields, does not apply: `config` is NOT in
533
+ // FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME.) Owning a config slice is a property of
534
+ // DECLARING one, not of being a feature. Verified inert at introduction — no
535
+ // shipped capability declares `config` on a non-feature role — so this changes
536
+ // no existing key; it stops a latent silent drop and unblocks Phase 4 (#2797).
537
+ for (const key of Object.keys(cap.config || {})) {
538
+ // S2b: inline literal guard at each write site (CodeQL barrier)
539
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
540
+ configKeys[key] = capId;
541
+
542
+ // Build configSchema entry — validate the slice first (throw on violation)
543
+ const slice = (cap.config || {})[key];
544
+ const sliceErrors = validateConfigSliceEntry(capId, key, slice);
545
+ if (sliceErrors.length > 0) {
546
+ throw new Error(
547
+ 'configSchema validation failed during registry build:\n' +
548
+ sliceErrors.map((e) => ' ' + e).join('\n'),
549
+ );
550
+ }
551
+ // S2b: inline literal guard for configSchema write site
552
+ if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
553
+ configSchema[key] = {
554
+ owner: capId,
555
+ type: slice.type,
556
+ default: slice.default,
557
+ description: slice.description,
558
+ };
559
+ // Preserve values array for enum types if present
560
+ if (slice.type === 'enum' && Array.isArray(slice.values)) {
561
+ configSchema[key].values = slice.values;
562
+ }
563
+ }
564
+ }
565
+
449
566
  if (cap.role === 'feature') {
450
567
  for (const skill of (cap.skills || [])) {
451
568
  // S2b: inline literal guard at each write site (CodeQL barrier)
@@ -457,34 +574,6 @@ function buildRegistry(capMap) {
457
574
  if (agent === '__proto__' || agent === 'constructor' || agent === 'prototype') continue;
458
575
  byAgent[agent] = capId;
459
576
  }
460
- for (const key of Object.keys(cap.config || {})) {
461
- // S2b: inline literal guard at each write site (CodeQL barrier)
462
- if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
463
- configKeys[key] = capId;
464
-
465
- // Build configSchema entry — validate the slice first (throw on violation)
466
- const slice = (cap.config || {})[key];
467
- const sliceErrors = validateConfigSliceEntry(capId, key, slice);
468
- if (sliceErrors.length > 0) {
469
- throw new Error(
470
- 'configSchema validation failed during registry build:\n' +
471
- sliceErrors.map((e) => ' ' + e).join('\n'),
472
- );
473
- }
474
- // S2b: inline literal guard for configSchema write site
475
- if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
476
- configSchema[key] = {
477
- owner: capId,
478
- type: slice.type,
479
- default: slice.default,
480
- description: slice.description,
481
- };
482
- // Preserve values array for enum types if present
483
- if (slice.type === 'enum' && Array.isArray(slice.values)) {
484
- configSchema[key].values = slice.values;
485
- }
486
- }
487
- }
488
577
 
489
578
  for (const step of (cap.steps || [])) {
490
579
  if (VALID_LOOP_POINTS.has(step.point)) {
@@ -741,7 +830,11 @@ function main() {
741
830
  if (flag === '--check') {
742
831
  // Fix #3: read the REAL central config keys so collision detection fires and is visible.
743
832
  const centralKeys = loadCentralConfigKeys();
744
- const { capMap, errors } = loadAndValidate(centralKeys);
833
+ const { capMap, errors, warnings } = loadAndValidate(centralKeys);
834
+
835
+ // ADR-2782 D4 — non-fatal manifest diagnostics. Emitted BEFORE the hard-error
836
+ // exit so a forward-built manifest still explains itself on a failing build.
837
+ for (const w of warnings) process.stderr.write(w + '\n');
745
838
 
746
839
  // Separate pending-migration warnings from hard errors
747
840
  const { hardErrors, pendingMigrationWarnings } = classifyCrossErrors(errors);
@@ -778,7 +871,11 @@ function main() {
778
871
  } else if (flag === '--write') {
779
872
  // Fix #3: read the REAL central config keys so collision detection fires and is visible.
780
873
  const centralKeys = loadCentralConfigKeys();
781
- const { capMap, errors } = loadAndValidate(centralKeys);
874
+ const { capMap, errors, warnings } = loadAndValidate(centralKeys);
875
+
876
+ // ADR-2782 D4 — non-fatal manifest diagnostics. Emitted BEFORE the hard-error
877
+ // exit so a forward-built manifest still explains itself on a failing build.
878
+ for (const w of warnings) process.stderr.write(w + '\n');
782
879
 
783
880
  // Separate pending-migration warnings from hard errors
784
881
  const { hardErrors, pendingMigrationWarnings } = classifyCrossErrors(errors);
@@ -828,6 +925,7 @@ module.exports = {
828
925
  validateCrossCapability,
829
926
  classifyCrossErrors,
830
927
  loadCentralConfigKeys,
928
+ loadCentralConfigPatterns,
831
929
  loadAndValidate,
832
930
  buildRegistry,
833
931
  serializeRegistry,
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * gen-emitted-baseline.cjs — publishes the emitted-baseline artifact the
6
+ * differential attribution check reads via `resolveBaseline()` (#2724, ADR-2719 §5).
7
+ *
8
+ * ## Why this exists
9
+ *
10
+ * Phase 3 (#2723) built `resolveBaseline()` (tests/helpers/emitted-baseline.cjs) and its
11
+ * cache-key staleness discipline, but nothing produced the artifact it reads — the
12
+ * dual-run window instead read the baseline straight off the committed golden fixtures
13
+ * at the base ref (`baselineManifestsAtRef`). Phase 4 deletes those fixtures, so
14
+ * something has to build and publish `{version, sha, manifests, sizes}` for real.
15
+ *
16
+ * ## What it does
17
+ *
18
+ * Builds the emitted manifest set (19 real installer spawns) and the workflow/agent size
19
+ * maps for the tree at `--dir <path>` (default: THIS script's own checkout — i.e.
20
+ * whatever commit is currently checked out where `gen-emitted-baseline.cjs` itself
21
+ * lives). Writes the result to `--out <path>` (default `.gsd-cache/emitted-baseline.json`).
22
+ *
23
+ * `--dir` decouples "which copy of this script runs" from "which tree gets measured"
24
+ * (#2767). The MEASUREMENT SCHEMA — this script, and the `currentManifests`/
25
+ * `currentSizes`/`buildParityManifest` functions it calls — always comes from wherever
26
+ * `gen-emitted-baseline.cjs` itself is being run from (relative `require`s resolve
27
+ * there); only the tree being measured (which `bin/install.js` gets spawned, which
28
+ * `hooks/`/`gsd-core/workflows/`/`agents/` get read) moves to `--dir`. That is what lets
29
+ * a differential apply ONE definition of "the emitted manifest" to two different
30
+ * commits and stay comparable even as that definition evolves — see
31
+ * `tests/helpers/emitted-runtime.cjs`'s `buildBaselineAtRef` for the caller that
32
+ * exercises this for real, and why it must NOT run the `--dir` tree's own copy of this
33
+ * script (which may be an older version, or — for any base ref that predates the PR
34
+ * that added this script at all — may not exist there yet).
35
+ *
36
+ * ## Callers
37
+ *
38
+ * 1. CI's push-to-`next` job runs this straight after `next` advances (no `--dir`,
39
+ * measuring its own checkout), then uploads `.gsd-cache/emitted-baseline.json` as a
40
+ * cache entry keyed on the merge sha (.github/workflows/test.yml,
41
+ * `publish-emitted-baseline` job).
42
+ * 2. `tests/emitted-attribution.test.cjs`'s real-tree test passes
43
+ * `buildBaselineAtRef` (tests/helpers/emitted-runtime.cjs) to `resolveBaseline()`
44
+ * as the `buildFallback` for a cache miss: it checks out `base` into a throwaway
45
+ * `git worktree`, symlinks in `node_modules` and runs `npm run build:lib` there
46
+ * (this script and the test helpers are Node-builtins-only per CONTRIBUTING.md's
47
+ * "No external dependencies in core", but `tests/helpers/install-shared.cjs`
48
+ * requires the TSC-compiled, gitignored `gsd-core/bin/lib/*.cjs`, so that one build
49
+ * step is unavoidable), then spawns THIS repo's OWN `gen-emitted-baseline.cjs`
50
+ * with `--dir <worktree> --out <tmp>` and reads the artifact back.
51
+ *
52
+ * Every git subprocess is bounded (CLAUDE.md → KNOWN DEFECTS: unbounded subprocesses
53
+ * hang CI silently).
54
+ */
55
+
56
+ const fs = require('node:fs');
57
+ const path = require('node:path');
58
+ const { execFileSync } = require('node:child_process');
59
+
60
+ const { runMain, ExitError } = require('./lib/cli-exit.cjs');
61
+ const { BUILD_SCRIPT } = require('../tests/helpers/install-shared.cjs');
62
+ const { currentManifests, currentSizes, git } = require('../tests/helpers/emitted-runtime.cjs');
63
+ const { BASELINE_VERSION } = require('../tests/helpers/emitted-baseline.cjs');
64
+
65
+ const REPO_ROOT = path.join(__dirname, '..');
66
+ const DEFAULT_OUT = path.join(REPO_ROOT, '.gsd-cache', 'emitted-baseline.json');
67
+
68
+ // Reuses emitted-runtime.cjs's `git()` (not a local execFileSync) so the
69
+ // `-c safe.directory=<dir>` fix for the remote runner's dubious-ownership
70
+ // mount (#2767) has exactly one source of truth — see that module's
71
+ // `safeDirArgs` doc comment.
72
+ function resolveHeadSha(cwd) {
73
+ return git(['rev-parse', 'HEAD'], { cwd }).trim();
74
+ }
75
+
76
+ function parseArgs(argv) {
77
+ let out = DEFAULT_OUT;
78
+ let dir = REPO_ROOT;
79
+ for (let i = 0; i < argv.length; i++) {
80
+ if (argv[i] === '--out') {
81
+ out = argv[i + 1];
82
+ i++;
83
+ } else if (argv[i] === '--dir') {
84
+ dir = argv[i + 1];
85
+ i++;
86
+ } else {
87
+ throw new ExitError(2, `gen-emitted-baseline: unknown argument "${argv[i]}"`);
88
+ }
89
+ }
90
+ if (!out || typeof out !== 'string') {
91
+ throw new ExitError(2, 'gen-emitted-baseline: --out requires a path');
92
+ }
93
+ if (!dir || typeof dir !== 'string') {
94
+ throw new ExitError(2, 'gen-emitted-baseline: --dir requires a path');
95
+ }
96
+ return {
97
+ out: path.isAbsolute(out) ? out : path.join(process.cwd(), out),
98
+ dir: path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir),
99
+ };
100
+ }
101
+
102
+ async function main() {
103
+ const { out, dir } = parseArgs(process.argv.slice(2));
104
+
105
+ if (!fs.existsSync(dir)) {
106
+ throw new ExitError(1, `gen-emitted-baseline: --dir "${dir}" does not exist`);
107
+ }
108
+
109
+ const sha = resolveHeadSha(dir);
110
+ if (!/^[0-9a-f]{40}$/.test(sha)) {
111
+ throw new ExitError(1, `gen-emitted-baseline: HEAD of "${dir}" did not resolve to a 40-hex sha: ${sha}`);
112
+ }
113
+
114
+ // hooks/dist is gitignored and built; a scoped CI checkout may not have run
115
+ // build:hooks yet. Idempotent, mirrors the real-tree test. Built INSIDE `dir` — the
116
+ // MEASURED tree's own hooks/ source, not this script's — so a `--dir` pointed at a
117
+ // different checkout (e.g. a base-ref worktree) reflects that checkout's hooks, not
118
+ // the caller's. `dir === REPO_ROOT` (the default, no `--dir`) collapses to the
119
+ // pre-#2767 behavior of building this own repo's hooks/dist.
120
+ const buildScript = dir === REPO_ROOT ? BUILD_SCRIPT : path.join(dir, 'scripts', 'build-hooks.js');
121
+ execFileSync(process.execPath, [buildScript], { cwd: dir, encoding: 'utf-8', stdio: 'pipe', timeout: 120_000 });
122
+
123
+ // The measurement SCHEMA (currentManifests/currentSizes/buildParityManifest) is
124
+ // always THIS script's own — only the tree being measured moves with `repoRoot`.
125
+ const manifests = currentManifests({ repoRoot: dir });
126
+ const sizes = currentSizes({ repoRoot: dir });
127
+
128
+ const artifact = { version: BASELINE_VERSION, sha, manifests, sizes };
129
+
130
+ fs.mkdirSync(path.dirname(out), { recursive: true });
131
+ fs.writeFileSync(out, `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
132
+
133
+ const familyCount = Object.keys(manifests).length;
134
+ const sizeCount = Object.keys(sizes).length;
135
+ process.stdout.write(
136
+ `gen-emitted-baseline: wrote ${out} (dir=${dir}, sha=${sha.slice(0, 12)}, ${familyCount} families, ${sizeCount} sized files)\n`,
137
+ );
138
+ return 0;
139
+ }
140
+
141
+ if (require.main === module) {
142
+ runMain(main);
143
+ }
144
+
145
+ module.exports = { parseArgs, resolveHeadSha, DEFAULT_OUT, REPO_ROOT };
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ // Regenerate the per-file test timing table used to weight chunk packing in
3
+ // scripts/run-tests.cjs (issue #2456).
4
+ //
5
+ // The chunk packer needs to know what each test file actually COSTS. Before
6
+ // #2456 it guessed from the filename (`^(?:install|codex-)` scored 12x,
7
+ // everything else 1) and was wrong in both directions — installer-migration-
8
+ // authoring.test.cjs scored 12x but runs ~0.1s, while the two heaviest files in
9
+ // the suite (run-tests-harness.test.cjs and release-tarball-smoke.install
10
+ // .test.cjs) both scored 1. This script replaces the guess with measurement.
11
+ //
12
+ // Input is one or more node:test reporter event streams as emitted by
13
+ // `gsd-test` (`~/.local/state/gsd-test/runs/<run-id>/test-events-<os>-node<v>
14
+ // .jsonl`). Each stream carries one `test:summary` event per test FILE, whose
15
+ // `data.duration_ms` is that file's total wall-clock and whose `data.file` is
16
+ // its absolute in-container path.
17
+ //
18
+ // Usage:
19
+ // node scripts/gen-test-timings.cjs <events.jsonl> [<events.jsonl> ...]
20
+ // node scripts/gen-test-timings.cjs ~/.local/state/gsd-test/runs/*/test-events-*.jsonl
21
+ // node scripts/gen-test-timings.cjs events.jsonl --out tests/test-timings.json
22
+ //
23
+ // When several streams are supplied (multiple lanes, e.g. node22 + node24), a
24
+ // file's recorded time is the MAX across them, not the mean: the packer exists
25
+ // to keep the SLOWEST lane's slowest chunk away from the per-chunk timeout, so
26
+ // the conservative bound is the right one to balance against.
27
+ //
28
+ // The table is ADVISORY and deliberately un-gated — there is no `--check` mode
29
+ // and no CI lint that fails on staleness, because timing data legitimately
30
+ // varies run to run. A file missing from the table falls back to the table's
31
+ // median weight, so a stale table degrades chunk BALANCE gracefully instead of
32
+ // failing the build. Regenerate it when the suite's cost profile has visibly
33
+ // drifted, not on a schedule.
34
+ 'use strict';
35
+
36
+ const fs = require('fs');
37
+ const { basename, dirname, join } = require('path');
38
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
39
+
40
+ const DEFAULT_OUT = join(__dirname, '..', 'tests', 'test-timings.json');
41
+ const SCHEMA_VERSION = 1;
42
+
43
+ function parseArgs(argv) {
44
+ const inputs = [];
45
+ let out = DEFAULT_OUT;
46
+ for (let i = 0; i < argv.length; i++) {
47
+ const arg = argv[i];
48
+ if (arg === '--out') {
49
+ const value = argv[++i];
50
+ if (!value) return { error: '--out requires a path' };
51
+ out = value;
52
+ } else if (arg.startsWith('--out=')) {
53
+ const value = arg.slice('--out='.length);
54
+ if (!value) return { error: '--out requires a path' };
55
+ out = value;
56
+ } else if (arg.startsWith('-')) {
57
+ return { error: `unknown flag "${arg}"` };
58
+ } else {
59
+ inputs.push(arg);
60
+ }
61
+ }
62
+ if (inputs.length === 0) {
63
+ return { error: 'usage: gen-test-timings.cjs <reporter-events.jsonl> [...] [--out <path>]' };
64
+ }
65
+ return { inputs, out };
66
+ }
67
+
68
+ // Fold one reporter event stream into `acc`, keeping the MAX duration seen for
69
+ // each test file. Returns per-stream counters plus any basename collisions
70
+ // found WITHIN this stream.
71
+ //
72
+ // Keying is by BASENAME, matching how run-tests.cjs weights a selected file:
73
+ // the reporter reports absolute in-container paths (/work/tests/foo.test.cjs)
74
+ // while the harness carries paths relative to its test dir, so the basename is
75
+ // the only stable join key between the two. A basename seen in two different
76
+ // directories would make the table ambiguous, so it must fail loudly.
77
+ //
78
+ // Collision detection is scoped to a SINGLE stream deliberately. Every lane
79
+ // writes its own stream under its own container root (`/work/tests` on Linux,
80
+ // `C:/work/tests` on Windows), so comparing directories ACROSS streams reports
81
+ // every shared basename as a collision — which is the script's own documented
82
+ // usage (globbing `test-events-*.jsonl` across lanes). Within one stream the
83
+ // root is constant, so a differing directory is a real collision.
84
+ function foldStream(text, acc) {
85
+ const dirsByBase = new Map();
86
+ let files = 0;
87
+ let malformed = 0;
88
+ for (const line of text.split('\n')) {
89
+ if (line.trim() === '') continue;
90
+ let event;
91
+ try {
92
+ event = JSON.parse(line);
93
+ } catch {
94
+ malformed++;
95
+ continue;
96
+ }
97
+ if (!event || event.type !== 'test:summary') continue;
98
+ const data = event.data;
99
+ if (!data || typeof data.file !== 'string') continue;
100
+ const ms = data.duration_ms;
101
+ if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) continue;
102
+ const path = data.file.replace(/\\/g, '/');
103
+ const base = basename(path);
104
+ if (!dirsByBase.has(base)) dirsByBase.set(base, new Set());
105
+ dirsByBase.get(base).add(dirname(path));
106
+ const prev = acc.get(base);
107
+ if (prev === undefined || ms > prev) acc.set(base, ms);
108
+ files++;
109
+ }
110
+ const collisions = [...dirsByBase.entries()]
111
+ .filter(([, dirs]) => dirs.size > 1)
112
+ .map(([base, dirs]) => `${base} (${[...dirs].sort().join(', ')})`);
113
+ return { files, malformed, collisions };
114
+ }
115
+
116
+ function main() {
117
+ const parsed = parseArgs(process.argv.slice(2));
118
+ if (parsed.error) throw new ExitError(2, `gen-test-timings: ${parsed.error}`);
119
+
120
+ const acc = new Map();
121
+ const allCollisions = new Set();
122
+ const sources = [];
123
+ for (const input of parsed.inputs) {
124
+ let text;
125
+ try {
126
+ text = fs.readFileSync(input, 'utf8');
127
+ } catch (err) {
128
+ throw new ExitError(2, `gen-test-timings: cannot read "${input}": ${err.message}`);
129
+ }
130
+ const { files, malformed, collisions } = foldStream(text, acc);
131
+ for (const c of collisions) allCollisions.add(c);
132
+ sources.push(basename(input));
133
+ console.error(
134
+ `gen-test-timings: ${basename(input)} — ${files} file summaries` +
135
+ (malformed > 0 ? `, ${malformed} unparseable lines skipped` : ''),
136
+ );
137
+ }
138
+
139
+ if (acc.size === 0) {
140
+ throw new ExitError(
141
+ 2,
142
+ 'gen-test-timings: no `test:summary` events with a file and duration_ms were found. ' +
143
+ 'Check that the input is a node:test reporter event stream (test-events-<os>-node<v>.jsonl).',
144
+ );
145
+ }
146
+
147
+ // A basename that resolves to two different directories within one lane makes
148
+ // the table ambiguous: run-tests.cjs joins on basename alone, so one file's
149
+ // measured cost would silently be applied to the other. Fail rather than emit
150
+ // a table that lies.
151
+ if (allCollisions.size > 0) {
152
+ throw new ExitError(
153
+ 2,
154
+ `gen-test-timings: basename collision — the table cannot key on basename alone:\n ${[...allCollisions].sort().join('\n ')}`,
155
+ );
156
+ }
157
+
158
+ // Every key must be a plain test-file basename. This is a data-integrity
159
+ // check on a stream we do not control (the reporter emits whatever path the
160
+ // runner saw), and it structurally excludes a computed key like `__proto__`
161
+ // or `constructor` from being written into the table object below — the
162
+ // `js/prototype-polluting-assignment` shape, even though the value here is
163
+ // always a number and could not actually pollute.
164
+ const SAFE_BASENAME_RE = /^[A-Za-z0-9._-]+\.test\.cjs$/;
165
+ const rejected = [...acc.keys()].filter((base) => !SAFE_BASENAME_RE.test(base));
166
+ if (rejected.length > 0) {
167
+ throw new ExitError(
168
+ 2,
169
+ `gen-test-timings: refusing to emit non-test-file keys: ${rejected.sort().join(', ')}`,
170
+ );
171
+ }
172
+
173
+ // Sorted keys keep the checked-in diff reviewable: a regeneration shows only
174
+ // the files whose cost actually moved, not a reshuffled object.
175
+ const timings = Object.create(null);
176
+ for (const base of [...acc.keys()].sort()) {
177
+ timings[base] = Math.round(acc.get(base));
178
+ }
179
+
180
+ const payload = {
181
+ schema_version: SCHEMA_VERSION,
182
+ generated_by: 'scripts/gen-test-timings.cjs',
183
+ unit: 'ms',
184
+ sources: sources.sort(),
185
+ file_count: acc.size,
186
+ timings,
187
+ };
188
+
189
+ fs.writeFileSync(parsed.out, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
190
+ const totalMs = [...acc.values()].reduce((sum, ms) => sum + ms, 0);
191
+ console.error(
192
+ `gen-test-timings: wrote ${parsed.out} — ${acc.size} files, ${(totalMs / 1000).toFixed(1)}s total`,
193
+ );
194
+ return 0;
195
+ }
196
+
197
+ if (require.main === module) {
198
+ runMain(main);
199
+ }
200
+
201
+ module.exports = { parseArgs, foldStream };