@opengsd/gsd-core 1.5.0-rc.2 → 1.5.0-rc.4

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 (131) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-advisor-researcher.md +1 -1
  3. package/agents/gsd-assumptions-analyzer.md +1 -1
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-code-reviewer.md +1 -1
  6. package/agents/gsd-codebase-mapper.md +1 -1
  7. package/agents/gsd-debugger.md +1 -1
  8. package/agents/gsd-doc-writer.md +1 -1
  9. package/agents/gsd-eval-auditor.md +1 -1
  10. package/agents/gsd-executor.md +1 -1
  11. package/agents/gsd-integration-checker.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +47 -0
  13. package/agents/gsd-nyquist-auditor.md +1 -0
  14. package/agents/gsd-phase-researcher.md +1 -1
  15. package/agents/gsd-plan-checker.md +1 -1
  16. package/agents/gsd-planner.md +1 -1
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +1 -1
  19. package/agents/gsd-roadmapper.md +55 -2
  20. package/agents/gsd-security-auditor.md +1 -0
  21. package/agents/gsd-ui-auditor.md +1 -1
  22. package/agents/gsd-ui-checker.md +1 -1
  23. package/agents/gsd-ui-researcher.md +1 -1
  24. package/agents/gsd-verifier.md +13 -2
  25. package/bin/install.js +61 -64
  26. package/commands/gsd/mempalace-capture.md +71 -0
  27. package/commands/gsd/mempalace-recall.md +102 -0
  28. package/commands/gsd/ns-context.md +4 -2
  29. package/commands/gsd/progress.md +2 -1
  30. package/gemini-extension.json +1 -1
  31. package/gsd-core/bin/gsd-tools.cjs +277 -95
  32. package/gsd-core/bin/lib/active-workstream-store.cjs +6 -0
  33. package/gsd-core/bin/lib/capability-activation.cjs +86 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
  35. package/gsd-core/bin/lib/capability-state.cjs +128 -21
  36. package/gsd-core/bin/lib/capability-writer.cjs +354 -0
  37. package/gsd-core/bin/lib/check-command-router.cjs +328 -1
  38. package/gsd-core/bin/lib/clusters.cjs +2 -0
  39. package/gsd-core/bin/lib/command-roster.cjs +19 -0
  40. package/gsd-core/bin/lib/commands.cjs +33 -10
  41. package/gsd-core/bin/lib/config-loader.cjs +7 -8
  42. package/gsd-core/bin/lib/config-schema.cjs +32 -3
  43. package/gsd-core/bin/lib/config.cjs +81 -26
  44. package/gsd-core/bin/lib/core.cjs +5 -2
  45. package/gsd-core/bin/lib/edge-probe.cjs +25 -2
  46. package/gsd-core/bin/lib/frontmatter.cjs +53 -1
  47. package/gsd-core/bin/lib/git-base-branch.cjs +194 -0
  48. package/gsd-core/bin/lib/init.cjs +36 -11
  49. package/gsd-core/bin/lib/install-profiles.cjs +57 -1
  50. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  51. package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
  52. package/gsd-core/bin/lib/model-resolver.cjs +47 -5
  53. package/gsd-core/bin/lib/phase.cjs +99 -23
  54. package/gsd-core/bin/lib/plan-drift-guard.cjs +117 -0
  55. package/gsd-core/bin/lib/probe-core.cjs +117 -1
  56. package/gsd-core/bin/lib/profile-output.cjs +45 -4
  57. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
  58. package/gsd-core/bin/lib/roadmap-parser.cjs +13 -3
  59. package/gsd-core/bin/lib/roadmap.cjs +97 -7
  60. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1946 -0
  61. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +54 -30
  62. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
  63. package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
  64. package/gsd-core/bin/lib/state-command-router.cjs +15 -3
  65. package/gsd-core/bin/lib/state-document.cjs +46 -1
  66. package/gsd-core/bin/lib/state.cjs +461 -94
  67. package/gsd-core/bin/lib/verify.cjs +92 -8
  68. package/gsd-core/bin/lib/worktree-safety.cjs +2 -1
  69. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
  70. package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
  71. package/gsd-core/bin/shared/model-catalog.json +1 -0
  72. package/gsd-core/references/edge-probe.md +11 -0
  73. package/gsd-core/references/loop-hook-dispatch.md +61 -0
  74. package/gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json +14 -0
  75. package/gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json +4 -0
  76. package/gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json +32 -0
  77. package/gsd-core/references/prohibition-probe.md +248 -0
  78. package/gsd-core/templates/config.json +1 -1
  79. package/gsd-core/templates/spec.md +14 -0
  80. package/gsd-core/workflows/audit-milestone.md +5 -3
  81. package/gsd-core/workflows/autonomous.md +10 -5
  82. package/gsd-core/workflows/code-review-fix.md +9 -7
  83. package/gsd-core/workflows/code-review.md +8 -6
  84. package/gsd-core/workflows/complete-milestone.md +1 -5
  85. package/gsd-core/workflows/discuss-phase.md +14 -0
  86. package/gsd-core/workflows/execute-phase.md +86 -146
  87. package/gsd-core/workflows/execute-plan.md +21 -6
  88. package/gsd-core/workflows/help/modes/full.md +7 -1
  89. package/gsd-core/workflows/new-project.md +3 -3
  90. package/gsd-core/workflows/next.md +50 -2
  91. package/gsd-core/workflows/pause-work.md +7 -1
  92. package/gsd-core/workflows/plan-phase.md +91 -221
  93. package/gsd-core/workflows/plan-review-convergence.md +14 -4
  94. package/gsd-core/workflows/pr-branch.md +4 -2
  95. package/gsd-core/workflows/profile-user.md +3 -1
  96. package/gsd-core/workflows/progress.md +58 -1
  97. package/gsd-core/workflows/quick.md +12 -8
  98. package/gsd-core/workflows/resume-project.md +17 -1
  99. package/gsd-core/workflows/review.md +19 -2
  100. package/gsd-core/workflows/secure-phase.md +4 -2
  101. package/gsd-core/workflows/settings-advanced.md +2 -0
  102. package/gsd-core/workflows/settings.md +27 -1
  103. package/gsd-core/workflows/ship.md +58 -5
  104. package/gsd-core/workflows/spec-phase.md +75 -0
  105. package/gsd-core/workflows/validate-phase.md +4 -2
  106. package/gsd-core/workflows/verify-phase.md +14 -4
  107. package/gsd-core/workflows/verify-work.md +27 -11
  108. package/hooks/dist/gsd-ensure-canonical-path.js +305 -0
  109. package/hooks/dist/gsd-statusline.js +1 -1
  110. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  111. package/hooks/gsd-ensure-canonical-path.js +305 -0
  112. package/hooks/gsd-statusline.js +1 -1
  113. package/hooks/hooks.json +1 -0
  114. package/hooks/managed-hooks-registry.cjs +1 -0
  115. package/package.json +5 -4
  116. package/scripts/affected-tests-lib.cjs +16 -4
  117. package/scripts/build-hooks.js +7 -0
  118. package/scripts/changeset/new.cjs +17 -3
  119. package/scripts/fix-slash-commands.cjs +15 -3
  120. package/scripts/gen-capability-registry.cjs +373 -49
  121. package/scripts/gen-inventory-manifest.cjs +1 -4
  122. package/scripts/gen-loop-host-contract.cjs +55 -0
  123. package/scripts/issue-version-gate.cjs +140 -0
  124. package/scripts/lint-allow-test-rule-refs.allowlist.json +327 -0
  125. package/scripts/lint-allow-test-rule-refs.cjs +162 -0
  126. package/scripts/lint-test-file-count.allowlist.json +14 -0
  127. package/scripts/mutation-matrix.cjs +108 -7
  128. package/scripts/pr-target-policy.cjs +63 -0
  129. package/scripts/release-tarball-smoke.cjs +7 -1
  130. package/scripts/research-profiles.cjs +5 -5
  131. package/scripts/run-tests.cjs +178 -17
@@ -13,6 +13,15 @@
13
13
  // node scripts/run-tests.cjs --suite slow # *.slow.test.cjs
14
14
  // node scripts/run-tests.cjs --files "a.test.cjs b.test.cjs"
15
15
  // node scripts/run-tests.cjs --files-from /tmp/selected-tests.txt
16
+ // node scripts/run-tests.cjs --suite unit --shard 1/3 # shard 1 of 3 (#1212)
17
+ //
18
+ // Sharding (issue #1212): --shard <i>/<n> runs a deterministic, balanced
19
+ // round-robin slice of the SORTED selected file list (file index k → shard
20
+ // k % n). i is 1-based (1..n); n >= 1; n=1 is a pure no-op (all files). The
21
+ // CI windows full-test lane shards across N parallel runners so per-job
22
+ // wall-clock scales as O(total/N) and stops hitting the job time cap. Sharding
23
+ // composes with --suite (it slices the post-filter selection) and preserves
24
+ // the existing 28K argv chunking WITHIN each shard.
16
25
  //
17
26
  // Suite grouping convention: filename suffix marker before `.test.cjs`.
18
27
  // A file named `foo.security.test.cjs` belongs to the `security` suite.
@@ -21,7 +30,7 @@
21
30
  'use strict';
22
31
 
23
32
  const { readdirSync } = require('fs');
24
- const { join } = require('path');
33
+ const { join, basename } = require('path');
25
34
  const { execFileSync } = require('child_process');
26
35
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
27
36
 
@@ -112,14 +121,90 @@ function ensureBuiltArtifacts(overrides = {}) {
112
121
  }
113
122
  const MARKED_SUITES = ['integration', 'install', 'security', 'slow'];
114
123
 
124
+ // Recursively collect *.test.cjs files under dir, returning paths relative to dir.
125
+ // Skips node_modules to avoid accidentally picking up decoy files.
126
+ function walkTestFiles(dir, relBase) {
127
+ const results = [];
128
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
129
+ if (entry.isDirectory()) {
130
+ if (entry.name === 'node_modules') continue;
131
+ results.push(...walkTestFiles(join(dir, entry.name), relBase ? `${relBase}/${entry.name}` : entry.name));
132
+ } else if (entry.name.endsWith('.test.cjs')) {
133
+ results.push(relBase ? `${relBase}/${entry.name}` : entry.name);
134
+ }
135
+ }
136
+ return results;
137
+ }
138
+
139
+ // Parse a `--shard i/n` value into { index, total } or { error }.
140
+ // i is 1-based and must satisfy 1 <= i <= n; n must be >= 1. Both parts must be
141
+ // plain non-negative integers (no decimals, signs, or surrounding whitespace).
142
+ // `n=1` is the pure no-op (every file). This is the strict-input boundary
143
+ // (Postel's Law: be strict in what a CLI flag accepts so a typo fails loudly
144
+ // rather than silently running the wrong slice of the suite).
145
+ function parseShardArg(value) {
146
+ if (typeof value !== 'string') {
147
+ return { error: `--shard requires a value of the form i/n` };
148
+ }
149
+ const m = /^(\d+)\/(\d+)$/.exec(value);
150
+ if (!m) {
151
+ return { error: `--shard value "${value}" must be of the form i/n (e.g. 1/3)` };
152
+ }
153
+ const index = Number(m[1]);
154
+ const total = Number(m[2]);
155
+ if (!Number.isInteger(total) || total < 1) {
156
+ return { error: `--shard total n must be an integer >= 1, got "${m[2]}"` };
157
+ }
158
+ if (!Number.isInteger(index) || index < 1 || index > total) {
159
+ return { error: `--shard index i must be an integer in 1..${total}, got "${m[1]}"` };
160
+ }
161
+ return { index, total };
162
+ }
163
+
164
+ // Deterministic, balanced round-robin partition of an ALREADY-SORTED file list.
165
+ // Shard `index` (1-based) receives every file whose position k in the sorted
166
+ // list satisfies k % total === index - 1. Round-robin (not contiguous blocks)
167
+ // spreads duration variance across shards and guarantees shard sizes differ by
168
+ // at most 1. Selection keys off array INDEX, never off the path string, so the
169
+ // partition is byte-identical across Windows/macOS/Linux as long as the caller
170
+ // sorts the list with the same (locale-independent) comparator. `total=1`
171
+ // returns the input unchanged (pure no-op). A shard with no files (total >
172
+ // file count) returns [] and is a legitimate result, not an error.
173
+ function selectShard(sortedFiles, { index, total }) {
174
+ if (total === 1) return sortedFiles;
175
+ return sortedFiles.filter((_, k) => k % total === index - 1);
176
+ }
177
+
115
178
  function parseArgs(argv) {
116
179
  let suite = null;
117
180
  let seen = false;
118
181
  let files = null;
119
182
  let filesFrom = null;
183
+ let shard = null;
184
+ let shardSeen = false;
120
185
  for (let i = 0; i < argv.length; i++) {
121
186
  const a = argv[i];
122
- if (a === '--suite') {
187
+ if (a === '--shard' || a.startsWith('--shard=')) {
188
+ if (shardSeen) {
189
+ return { error: 'duplicate --shard flag' };
190
+ }
191
+ shardSeen = true;
192
+ let v;
193
+ if (a === '--shard') {
194
+ v = argv[i + 1];
195
+ if (v === undefined || (typeof v === 'string' && v.startsWith('--'))) {
196
+ return { error: '--shard requires a value of the form i/n' };
197
+ }
198
+ i++;
199
+ } else {
200
+ v = a.slice('--shard='.length);
201
+ }
202
+ const parsed = parseShardArg(v);
203
+ if (parsed.error) {
204
+ return { error: parsed.error };
205
+ }
206
+ shard = parsed;
207
+ } else if (a === '--suite') {
123
208
  if (seen) {
124
209
  return { error: 'duplicate --suite flag' };
125
210
  }
@@ -182,15 +267,18 @@ function parseArgs(argv) {
182
267
  if (files !== null && filesFrom !== null) {
183
268
  return { error: '--files and --files-from cannot be combined' };
184
269
  }
185
- return { suite, files, filesFrom };
270
+ return { suite, files, filesFrom, shard };
186
271
  }
187
272
 
188
273
  // Return the marked suite name embedded in a filename, or null if it's unmarked.
189
274
  // foo.security.test.cjs -> "security"
190
275
  // foo.test.cjs -> null (unit)
276
+ // Accepts either a bare filename or a relative subdir path; classification is
277
+ // based on the basename only so subdir paths classify identically to root files.
191
278
  function suiteOf(filename) {
192
- if (!filename.endsWith('.test.cjs')) return null;
193
- const base = filename.slice(0, -'.test.cjs'.length);
279
+ const name = basename(filename);
280
+ if (!name.endsWith('.test.cjs')) return null;
281
+ const base = name.slice(0, -'.test.cjs'.length);
194
282
  const lastDot = base.lastIndexOf('.');
195
283
  if (lastDot === -1) return null;
196
284
  const marker = base.slice(lastDot + 1);
@@ -213,7 +301,8 @@ function splitFileList(value) {
213
301
  .split(/[,\s]+/)
214
302
  .map(v => v.trim())
215
303
  .filter(Boolean)
216
- .map(v => v.replace(/^tests[\\/]/, ''));
304
+ .map(v => v.replace(/\\/g, '/')) // normalize Windows backslashes
305
+ .map(v => v.replace(/^tests\//, ''));
217
306
  }
218
307
 
219
308
  function selectExplicitFiles(allFiles, filesValue, filesFrom) {
@@ -222,8 +311,19 @@ function selectExplicitFiles(allFiles, filesValue, filesFrom) {
222
311
  ? splitFileList(fs.readFileSync(filesFrom, 'utf8'))
223
312
  : splitFileList(filesValue);
224
313
  const available = new Set(allFiles);
314
+
315
+ // Build a basename -> [relpath, ...] index for bare-basename resolution.
316
+ // A bare basename (no directory separator) may match exactly one subdir file.
317
+ const basenameIndex = new Map();
318
+ for (const f of allFiles) {
319
+ const b = basename(f);
320
+ if (!basenameIndex.has(b)) basenameIndex.set(b, []);
321
+ basenameIndex.get(b).push(f);
322
+ }
323
+
225
324
  const selected = [];
226
325
  const missing = [];
326
+ const errors = [];
227
327
  for (const file of requested) {
228
328
  // If the token is a bare suite name (e.g. "unit" written by ci-test-scope
229
329
  // as the #408 fallback sentinel), delegate to the existing suite resolver
@@ -234,11 +334,27 @@ function selectExplicitFiles(allFiles, filesValue, filesFrom) {
234
334
  selected.push(f);
235
335
  }
236
336
  } else if (available.has(file)) {
337
+ // Exact relpath match (e.g. "installer-migrations/001-legacy-orphan-files.test.cjs").
237
338
  selected.push(file);
339
+ } else if (!file.includes('/')) {
340
+ // Bare basename (no directory separator): resolve via index.
341
+ const candidates = basenameIndex.get(file);
342
+ if (!candidates || candidates.length === 0) {
343
+ missing.push(file);
344
+ } else if (candidates.length > 1) {
345
+ errors.push(
346
+ `ambiguous basename "${file}" matches multiple files: ${candidates.join(', ')} — pass the subdir path instead`,
347
+ );
348
+ } else {
349
+ selected.push(candidates[0]);
350
+ }
238
351
  } else {
239
352
  missing.push(file);
240
353
  }
241
354
  }
355
+ if (errors.length > 0) {
356
+ return { error: errors.join('; ') };
357
+ }
242
358
  if (missing.length > 0) {
243
359
  return {
244
360
  error: `requested test file(s) not found: ${missing.join(', ')}`,
@@ -266,17 +382,16 @@ function main() {
266
382
  ? process.env.GSD_TEST_DIR
267
383
  : join(__dirname, '..', 'tests');
268
384
 
269
- const allFiles = readdirSync(testDir)
270
- .filter(f => f.endsWith('.test.cjs'))
271
- .sort();
385
+ const allFiles = walkTestFiles(testDir, '').sort();
272
386
 
273
387
  if (allFiles.length === 0) {
274
388
  console.error(`No test files found in ${testDir}`);
275
389
  throw new ExitError(1);
276
390
  }
277
391
 
392
+ const usingExplicitFiles = parsed.files !== null || parsed.filesFrom !== null;
278
393
  let selectedNames;
279
- if (parsed.files !== null || parsed.filesFrom !== null) {
394
+ if (usingExplicitFiles) {
280
395
  const explicit = selectExplicitFiles(allFiles, parsed.files, parsed.filesFrom);
281
396
  if (explicit.error) {
282
397
  console.error(`run-tests: ${explicit.error}`);
@@ -286,14 +401,54 @@ function main() {
286
401
  } else {
287
402
  selectedNames = selectFiles(allFiles, suite);
288
403
  }
404
+
405
+ // Shard partitioning (#1212): when --shard i/n is given, keep only this
406
+ // shard's deterministic round-robin slice of the selected list. Applied
407
+ // AFTER suite/explicit selection so it composes with --suite (each shard
408
+ // runs i/n of the post-filter selection).
409
+ //
410
+ // The partition keys off array index, so the slice is only reproducible if
411
+ // the input is in a stable order. --suite/default selections are already
412
+ // sorted (allFiles came from walkTestFiles(...).sort() and selectFiles
413
+ // preserves that order), but --files/--files-from preserve REQUEST order.
414
+ // Sort here so --shard is deterministic regardless of how the selection was
415
+ // produced — the runner's documented contract is a sorted partition.
416
+ //
417
+ // emptyBeforeShard distinguishes "this shard legitimately got zero files
418
+ // from a non-empty list" (total > file count — a valid no-op) from "the
419
+ // selection was already empty before sharding" (a genuinely empty suite,
420
+ // which must still hit the discovery hard-error below — Codex #1212 review).
421
+ const usingShard = parsed.shard !== null;
422
+ let emptyBeforeShard = false;
423
+ if (usingShard) {
424
+ emptyBeforeShard = selectedNames.length === 0;
425
+ selectedNames = selectShard([...selectedNames].sort(), parsed.shard);
426
+ }
427
+
289
428
  const selected = selectedNames.map(f => join(testDir, f));
290
429
 
291
430
  if (selected.length === 0) {
292
- // Empty suite: report and exit 0 so empty lanes (e.g. `security` before
293
- // adversarial tests land) don't gate CI. CI consumers wanting strictness
294
- // can grep stderr for "no tests in suite".
295
- console.error(`run-tests: no tests in suite "${suite || 'all'}"`);
296
- return 0;
431
+ // A legitimately-empty shard: --shard was given, the pre-shard selection
432
+ // had files, but this shard index drew zero (total > file count). Exit 0.
433
+ const legitimatelyEmptyShard = usingShard && !emptyBeforeShard;
434
+ if (usingExplicitFiles || legitimatelyEmptyShard) {
435
+ // Empty file list from --files/--files-from (e.g. CI passes an empty
436
+ // .ci-selected-tests.txt on docs-only/inert PRs) OR a legitimately-empty
437
+ // shard: both are expected. Exit 0 silently rather than taking the
438
+ // "discovery broken" hard-error path below. An EMPTY suite that was
439
+ // empty BEFORE sharding falls through to the hard error so a broken
440
+ // --suite filter is still caught even with --shard present.
441
+ console.error(`run-tests: no tests in suite "${suite || 'all'}"`);
442
+ return 0;
443
+ }
444
+ // Empty suite/default run: this means discovery or the suite filter is broken.
445
+ // Allow GSD_ALLOW_EMPTY_SUITE=1 as an escape hatch (downgrades to a warning).
446
+ if (process.env.GSD_ALLOW_EMPTY_SUITE === '1') {
447
+ console.error(`run-tests: WARNING: 0 test files selected for suite "${suite || 'all'}" — discovery or suite filter may be broken (GSD_ALLOW_EMPTY_SUITE=1 suppressed the error)`);
448
+ return 0;
449
+ }
450
+ console.error(`run-tests: ERROR: 0 test files selected for suite "${suite || 'all'}" — discovery or suite filter is broken`);
451
+ throw new ExitError(1);
297
452
  }
298
453
 
299
454
  // Build the gitignored bin/lib artifact if absent, before any test requires it.
@@ -342,6 +497,9 @@ function main() {
342
497
  const MAX_CMDLINE_CHARS = process.env.RUN_TESTS_MAX_CMDLINE_CHARS
343
498
  ? Number(process.env.RUN_TESTS_MAX_CMDLINE_CHARS)
344
499
  : 28000; // headroom below the 32,767 Windows ceiling
500
+ const MAX_FILES_PER_CHUNK = process.env.RUN_TESTS_MAX_FILES_PER_CHUNK
501
+ ? Number(process.env.RUN_TESTS_MAX_FILES_PER_CHUNK)
502
+ : 180;
345
503
 
346
504
  // node:test does not exit until the event loop drains. A unit test that leaks
347
505
  // an open handle (un-terminated Worker, un-killed child_process, ref'd timer)
@@ -362,7 +520,10 @@ function main() {
362
520
  let currentLen = FIXED_OVERHEAD;
363
521
  for (const file of selected) {
364
522
  const add = file.length + 1; // +1 for the inter-arg separator
365
- if (current.length > 0 && currentLen + add > MAX_CMDLINE_CHARS) {
523
+ if (
524
+ current.length > 0 &&
525
+ (currentLen + add > MAX_CMDLINE_CHARS || current.length >= MAX_FILES_PER_CHUNK)
526
+ ) {
366
527
  chunks.push(current);
367
528
  current = [];
368
529
  currentLen = FIXED_OVERHEAD;
@@ -424,4 +585,4 @@ if (require.main === module) {
424
585
  runMain(main);
425
586
  }
426
587
 
427
- module.exports = { suiteOf, ensureBuiltArtifacts };
588
+ module.exports = { suiteOf, ensureBuiltArtifacts, parseShardArg, selectShard };