forge-workflow 0.1.0-beta.6 → 0.1.0-beta.8

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 (179) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/forge-gh-proxy.js +15 -0
  3. package/bin/forge-github-credential.js +21 -0
  4. package/bin/forge.js +16 -1
  5. package/docs/guides/MIGRATION.md +4 -3
  6. package/docs/reference/RELEASE.md +5 -3
  7. package/docs/reference/VALIDATION.md +14 -0
  8. package/docs/reference/github-accounts.md +114 -38
  9. package/lib/bun-workflow-pins.js +129 -3
  10. package/lib/commands/github.js +234 -49
  11. package/lib/commands/merge.js +2 -2
  12. package/lib/commands/push.js +308 -47
  13. package/lib/commands/release.js +18 -1
  14. package/lib/commands/shepherd.js +1 -1
  15. package/lib/commands/test.js +8 -0
  16. package/lib/commands/validate.js +219 -67
  17. package/lib/gh-proxy.js +309 -0
  18. package/lib/github-context.js +160 -25
  19. package/lib/github-credential.js +47 -0
  20. package/lib/github-router.js +774 -0
  21. package/lib/kernel/migrations.js +1 -1
  22. package/lib/kernel/sqlite-driver.js +1 -1
  23. package/lib/memory/router.js +1 -1
  24. package/lib/memory/usage-evidence.js +1 -1
  25. package/lib/native-gh.js +177 -0
  26. package/lib/npm-publish-workflow.js +5 -5
  27. package/lib/pr-monitor/flow-monitor.js +127 -28
  28. package/lib/pr-monitor/reconcile-executor.js +1 -1
  29. package/lib/project-memory.js +1 -1
  30. package/lib/protected-state-authority.js +214 -13
  31. package/lib/release-readiness.js +57 -41
  32. package/lib/shell-utils.js +41 -15
  33. package/lib/test-workflow.js +389 -0
  34. package/lib/validation-receipt.js +305 -0
  35. package/lib/workflow-templates/test.yml +654 -0
  36. package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  37. package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  38. package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  39. package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  40. package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  41. package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  42. package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  43. package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  44. package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  45. package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  46. package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  47. package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  48. package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  49. package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  50. package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  51. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  52. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  53. package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  54. package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  55. package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  56. package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  57. package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  58. package/node_modules/@forge/contracts/index.js +32 -0
  59. package/node_modules/@forge/contracts/package.json +35 -0
  60. package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  61. package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  62. package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  63. package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  64. package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  65. package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  66. package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  67. package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  68. package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  69. package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  70. package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  71. package/node_modules/@forge/contracts/src/baseline.js +23 -0
  72. package/node_modules/@forge/contracts/src/canonical.js +151 -0
  73. package/node_modules/@forge/contracts/src/definitions.js +176 -0
  74. package/node_modules/@forge/contracts/src/identity.js +42 -0
  75. package/node_modules/@forge/contracts/src/schema.js +72 -0
  76. package/node_modules/@forge/contracts/src/validate.js +305 -0
  77. package/node_modules/@forge/flow/index.js +114 -0
  78. package/node_modules/@forge/flow/package.json +34 -0
  79. package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
  80. package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
  81. package/node_modules/@forge/flow/src/executor.js +279 -0
  82. package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
  83. package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
  84. package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
  85. package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
  86. package/node_modules/@forge/memory/index.js +331 -0
  87. package/node_modules/@forge/memory/package.json +34 -0
  88. package/node_modules/@forge/memory/src/authority-provider.js +67 -0
  89. package/node_modules/@forge/memory/src/backend-registry.js +166 -0
  90. package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
  91. package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
  92. package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
  93. package/package.json +12 -2
  94. package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  95. package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  96. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  97. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  98. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  99. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  100. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  101. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  102. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  103. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  104. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  105. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  106. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  107. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  108. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  109. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  110. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  111. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  112. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  113. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  114. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  115. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  116. package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
  117. package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
  118. package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  119. package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  120. package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  121. package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  122. package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  123. package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  124. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  125. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  126. package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  127. package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  128. package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  129. package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
  130. package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
  131. package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
  132. package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
  133. package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
  134. package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
  135. package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  136. package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  137. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  138. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  139. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  140. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  141. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  142. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  143. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  144. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  145. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  146. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  147. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  148. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  149. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  150. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  151. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  152. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  153. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  154. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  155. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  156. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  157. package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
  158. package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
  159. package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  160. package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  161. package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  162. package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  163. package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  164. package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  165. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  166. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  167. package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  168. package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  169. package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  170. package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
  171. package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
  172. package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
  173. package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
  174. package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
  175. package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
  176. package/scripts/check-forge-token.js +85 -67
  177. package/scripts/gen-embedded-assets.mjs +31 -5
  178. package/scripts/test-full-suite.js +69 -15
  179. package/scripts/test.js +11 -1
@@ -12,6 +12,7 @@ const { execFileSync } = require('node:child_process');
12
12
  const fs = require('node:fs');
13
13
  const path = require('node:path');
14
14
  const { secureExecFileSync } = require('../shell-utils');
15
+ const validationReceipt = require('../validation-receipt');
15
16
 
16
17
  // Constants
17
18
  const CHECK_TYPES = {
@@ -94,7 +95,6 @@ function getExecOptions(rootDir = process.cwd(), fullSuite = false) {
94
95
  }
95
96
 
96
97
  const ERROR_PATTERNS = {
97
- COMMAND_NOT_FOUND: ['ENOENT', 'not found'],
98
98
  NO_LOCK_FILE: ['requires an existing', 'package-lock'],
99
99
  GIT_LISTING_UNAVAILABLE: [
100
100
  'not a git repository',
@@ -112,9 +112,7 @@ const ERROR_PATTERNS = {
112
112
  * @private
113
113
  */
114
114
  function isCommandNotFound(error) {
115
- return ERROR_PATTERNS.COMMAND_NOT_FOUND.some(pattern =>
116
- error.message.includes(pattern),
117
- );
115
+ return error?.code === 'ENOENT';
118
116
  }
119
117
 
120
118
  function isGitListingUnavailableError(error) {
@@ -299,6 +297,7 @@ async function runTypeCheck() {
299
297
  return {
300
298
  success: true,
301
299
  skipped: true,
300
+ notConfigured: true,
302
301
  duration: Date.now() - startTime,
303
302
  message: 'TypeScript not configured (no tsconfig.json)',
304
303
  };
@@ -358,7 +357,7 @@ async function runTypeCheck() {
358
357
  * const result = await runLint();
359
358
  * console.log(`Warnings: ${result.warnings}, Errors: ${result.errors}`);
360
359
  */
361
- async function runLint(exec = execFileSync) {
360
+ async function runLint(exec = secureExecFileSync) {
362
361
  const startTime = Date.now();
363
362
 
364
363
  try {
@@ -575,6 +574,21 @@ async function runSecurityScan() { // NOSONAR S3776
575
574
  */
576
575
  function parseTestCounts(output) {
577
576
  const text = String(output || '');
577
+ const effectiveBudgetMatch = /Full suite resource budget: requested=(default|\d+) effective=(\d+)/.exec(text);
578
+ const rejectedBudgetMatch = /Full suite resource budget: requested=(\d+) minimum=(\d+) outcome=rejected/.exec(text);
579
+ let resourceBudget;
580
+ if (effectiveBudgetMatch) {
581
+ resourceBudget = {
582
+ requested: effectiveBudgetMatch[1] === 'default' ? null : Number(effectiveBudgetMatch[1]),
583
+ effective: Number(effectiveBudgetMatch[2]),
584
+ };
585
+ } else if (rejectedBudgetMatch) {
586
+ resourceBudget = {
587
+ requested: Number(rejectedBudgetMatch[1]),
588
+ minimum: Number(rejectedBudgetMatch[2]),
589
+ outcome: 'rejected',
590
+ };
591
+ }
578
592
  const aggregateMatches = [...text.matchAll(
579
593
  /Full suite aggregate: status=(PASS|FAIL|INCOMPLETE) tests=(\d+) assertions=\d+ passed=(\d+) failed=(\d+) errors=(\d+) skipped=(\d+)/g,
580
594
  )]; // NOSONAR S5852 - fixed tokens and bounded numeric fields
@@ -583,6 +597,7 @@ function parseTestCounts(output) {
583
597
  const total = parseNumber(aggregate, 2);
584
598
  return {
585
599
  status: aggregate[1],
600
+ resourceBudget,
586
601
  passed: parseNumber(aggregate, 3),
587
602
  failed: parseNumber(aggregate, 4),
588
603
  errors: parseNumber(aggregate, 5),
@@ -596,7 +611,24 @@ function parseTestCounts(output) {
596
611
  const skipped = parseNumber(/(\d+) skip/.exec(text)); // NOSONAR S5852 - bounded \d+ pattern
597
612
  const ranMatch = /Ran (\d+) tests/.exec(text); // NOSONAR S5852 - bounded \d+ pattern
598
613
  const total = ranMatch ? parseNumber(ranMatch, 1) : passed + failed + skipped;
599
- return { passed, failed, skipped, total, testsFound: total > 0 };
614
+ return { passed, failed, skipped, total, testsFound: total > 0, resourceBudget };
615
+ }
616
+
617
+ function parseResourceBudget(args = []) {
618
+ let budget = null;
619
+ for (let index = 0; index < args.length; index += 1) {
620
+ if (args[index] !== '--shards') continue;
621
+ const value = args[index + 1];
622
+ if (!/^[1-9]\d*$/.test(String(value ?? ''))) {
623
+ throw new TypeError('--shards must be a positive integer resource budget');
624
+ }
625
+ budget = Number(value);
626
+ if (!Number.isSafeInteger(budget)) {
627
+ throw new TypeError('--shards must be a positive integer resource budget');
628
+ }
629
+ index += 1;
630
+ }
631
+ return budget;
600
632
  }
601
633
 
602
634
  function isForgeCheckout(rootDir) {
@@ -612,17 +644,142 @@ function isForgeCheckout(rootDir) {
612
644
  }
613
645
  }
614
646
 
615
- async function runAllTests(exec = execFileSync, rootDir = process.cwd()) {
647
+ function isValidationRootAvailable(rootDir) {
648
+ try {
649
+ return fs.statSync(rootDir).isDirectory();
650
+ } catch {
651
+ return false;
652
+ }
653
+ }
654
+
655
+ function unavailableValidationRootResult(startTime) {
656
+ return {
657
+ success: false,
658
+ testsFound: false,
659
+ fullSuite: false,
660
+ duration: Date.now() - startTime,
661
+ passed: 0,
662
+ failed: 0,
663
+ total: 0,
664
+ message: 'Test execution failed: validation root directory is unavailable',
665
+ };
666
+ }
667
+
668
+ function testExecutionErrorResult(error, startTime, rootDir, useFullSuite, timeout) {
669
+ const output = error.stdout || error.message;
670
+ const counts = parseTestCounts(output);
671
+ const budgetEvidence = counts.resourceBudget ? { resourceBudget: counts.resourceBudget } : {};
672
+ if (error.code === 'ETIMEDOUT' || (error.killed && error.signal === 'SIGTERM')) {
673
+ return {
674
+ success: false,
675
+ fullSuite: useFullSuite,
676
+ ...budgetEvidence,
677
+ duration: Date.now() - startTime,
678
+ passed: 0,
679
+ failed: 0,
680
+ total: 0,
681
+ message: validationTimeoutMessage('Test execution', timeout),
682
+ };
683
+ }
684
+
685
+ if (isCommandNotFound(error)) {
686
+ if (!isValidationRootAvailable(rootDir)) {
687
+ return { ...unavailableValidationRootResult(startTime), ...budgetEvidence };
688
+ }
689
+ return {
690
+ success: true,
691
+ skipped: true,
692
+ testsFound: false,
693
+ fullSuite: useFullSuite,
694
+ ...budgetEvidence,
695
+ duration: Date.now() - startTime,
696
+ passed: 0,
697
+ failed: 0,
698
+ total: 0,
699
+ message: 'Tests skipped: test runner not found. Install Node.js and Bun to run tests.',
700
+ };
701
+ }
702
+
703
+ if (counts.resourceBudget?.outcome === 'rejected') {
704
+ return {
705
+ success: false,
706
+ testsFound: false,
707
+ fullSuite: useFullSuite,
708
+ resourceBudget: counts.resourceBudget,
709
+ duration: Date.now() - startTime,
710
+ passed: 0,
711
+ failed: 0,
712
+ total: 0,
713
+ message: `Full suite resource budget rejected: requested=${counts.resourceBudget.requested} minimum=${counts.resourceBudget.minimum}`,
714
+ output,
715
+ };
716
+ }
717
+ const passed = counts.passed;
718
+ const failed = counts.failed > 0 ? counts.failed : Math.max(1, counts.errors || 0);
719
+ const total = counts.total > 0 ? counts.total : passed + failed + counts.skipped;
720
+
721
+ return {
722
+ success: false,
723
+ testsFound: counts.testsFound,
724
+ fullSuite: useFullSuite,
725
+ resourceBudget: counts.resourceBudget,
726
+ duration: Date.now() - startTime,
727
+ passed,
728
+ failed,
729
+ total,
730
+ message: `${failed}/${total} tests failed`,
731
+ output,
732
+ };
733
+ }
734
+
735
+ async function runAllTests(exec = execFileSync, rootDir = process.cwd(), resourceBudget = null) {
616
736
  const startTime = Date.now();
737
+ if (!isValidationRootAvailable(rootDir)) return unavailableValidationRootResult(startTime);
617
738
  const useFullSuite = isForgeCheckout(rootDir);
739
+ if (resourceBudget !== null && !useFullSuite) {
740
+ return {
741
+ success: false,
742
+ testsFound: false,
743
+ fullSuite: false,
744
+ duration: Date.now() - startTime,
745
+ passed: 0,
746
+ failed: 0,
747
+ total: 0,
748
+ message: '--shards requires the canonical Forge full-suite runner',
749
+ };
750
+ }
618
751
  const command = useFullSuite ? 'node' : 'bun';
619
752
  const args = useFullSuite ? ['scripts/test-full-suite.js'] : ['test', '--timeout', '30000'];
753
+ if (useFullSuite && resourceBudget !== null) args.push('--shards', String(resourceBudget));
620
754
  const execOptions = getExecOptions(rootDir, useFullSuite);
621
755
 
622
756
  try {
623
757
  const result = exec(command, args, execOptions); // NOSONAR S4036 - fixed CLI commands, no user input, developer tool context
624
758
 
625
- const { status, passed, failed, errors = 0, skipped, total, testsFound } = parseTestCounts(result);
759
+ const {
760
+ status,
761
+ passed,
762
+ failed,
763
+ errors = 0,
764
+ skipped,
765
+ total,
766
+ testsFound,
767
+ resourceBudget: budgetEvidence,
768
+ } = parseTestCounts(result);
769
+ if (useFullSuite && status !== 'PASS') {
770
+ return {
771
+ success: false,
772
+ testsFound,
773
+ fullSuite: true,
774
+ resourceBudget: budgetEvidence,
775
+ duration: Date.now() - startTime,
776
+ passed,
777
+ failed,
778
+ skipped,
779
+ total,
780
+ message: 'Full suite did not report a passing terminal aggregate',
781
+ };
782
+ }
626
783
  const success = failed === 0 && errors === 0 && (status === undefined || status === 'PASS');
627
784
 
628
785
  // B2 (N1): a bun run that executes 0 tests must NEVER report PASS — that
@@ -632,6 +789,7 @@ async function runAllTests(exec = execFileSync, rootDir = process.cwd()) {
632
789
  success: true,
633
790
  skipped: true,
634
791
  testsFound: false,
792
+ fullSuite: useFullSuite,
635
793
  duration: Date.now() - startTime,
636
794
  passed,
637
795
  failed,
@@ -643,61 +801,17 @@ async function runAllTests(exec = execFileSync, rootDir = process.cwd()) {
643
801
  return {
644
802
  success,
645
803
  testsFound: true,
804
+ fullSuite: useFullSuite,
805
+ resourceBudget: budgetEvidence,
646
806
  duration: Date.now() - startTime,
647
807
  passed,
648
808
  failed,
649
809
  skipped,
650
810
  total,
651
- message: success
652
- ? `All ${total} tests passed`
653
- : `${failed + errors}/${total} tests failed`,
811
+ message: success ? `All ${total} tests passed` : `${failed + errors}/${total} tests failed`,
654
812
  };
655
813
  } catch (error) {
656
- // Check for timeout
657
- if (error.code === 'ETIMEDOUT' || (error.killed && error.signal === 'SIGTERM')) {
658
- return {
659
- success: false,
660
- duration: Date.now() - startTime,
661
- passed: 0,
662
- failed: 0,
663
- total: 0,
664
- message: validationTimeoutMessage('Test execution', execOptions.timeout),
665
- };
666
- }
667
-
668
- // Test runner absent — say so explicitly (do NOT silently pass).
669
- if (isCommandNotFound(error)) {
670
- return {
671
- success: true,
672
- skipped: true,
673
- testsFound: false,
674
- duration: Date.now() - startTime,
675
- passed: 0,
676
- failed: 0,
677
- total: 0,
678
- message: 'Tests skipped: test runner not found. Install Node.js and Bun to run tests.',
679
- };
680
- }
681
-
682
- // Parse test failures from output
683
- const output = error.stdout || error.message;
684
- const counts = parseTestCounts(output);
685
- const passed = counts.passed;
686
- // bun exited non-zero: if the output shows no explicit failure count, treat
687
- // it as at least one failure rather than swallowing the non-zero exit.
688
- const failed = counts.failed > 0 ? counts.failed : Math.max(1, counts.errors || 0);
689
- const total = counts.total > 0 ? counts.total : passed + failed + counts.skipped;
690
-
691
- return {
692
- success: false,
693
- testsFound: counts.testsFound,
694
- duration: Date.now() - startTime,
695
- passed,
696
- failed,
697
- total,
698
- message: `${failed}/${total} tests failed`,
699
- output,
700
- };
814
+ return testExecutionErrorResult(error, startTime, rootDir, useFullSuite, execOptions.timeout);
701
815
  }
702
816
  }
703
817
 
@@ -723,13 +837,27 @@ async function runAllTests(exec = execFileSync, rootDir = process.cwd()) {
723
837
  * console.log(result.summary);
724
838
  */
725
839
  async function executeValidate(options = {}) { // NOSONAR S3776
726
- const { skip = [], continueOnError = true, rootDir = process.cwd() } = options || {};
840
+ const {
841
+ skip = [],
842
+ continueOnError = true,
843
+ rootDir = process.cwd(),
844
+ resourceBudget = null,
845
+ } = options || {};
846
+ const startTime = Date.now();
847
+ if (!isValidationRootAvailable(rootDir)) {
848
+ const checks = { tests: unavailableValidationRootResult(startTime) };
849
+ const result = buildResult(checks, [CHECK_TYPES.TESTS], [], startTime);
850
+ result.validationReceipt = false;
851
+ return result;
852
+ }
853
+ const receiptApi = options?.validationReceipt || validationReceipt;
854
+ const testRunner = options?.runAllTests || runAllTests;
855
+ const validationSnapshot = receiptApi.beginValidation(rootDir);
856
+ const receiptSnapshot = skip.length === 0 ? validationSnapshot : null;
727
857
 
728
858
  const checks = {};
729
859
  const failedChecks = [];
730
860
  const errors = [];
731
- const startTime = Date.now();
732
-
733
861
  // 0. Conflict markers
734
862
  if (!skip.includes(CHECK_TYPES.CONFLICT_MARKERS)) {
735
863
  try {
@@ -797,7 +925,7 @@ async function executeValidate(options = {}) { // NOSONAR S3776
797
925
  // 4. Tests
798
926
  if (!skip.includes(CHECK_TYPES.TESTS)) {
799
927
  try {
800
- checks.tests = await runAllTests(execFileSync, rootDir);
928
+ checks.tests = await testRunner(execFileSync, rootDir, resourceBudget);
801
929
  if (!checks.tests.success && checks.tests.skipped !== true) {
802
930
  failedChecks.push(CHECK_TYPES.TESTS);
803
931
  if (!continueOnError) {
@@ -810,7 +938,9 @@ async function executeValidate(options = {}) { // NOSONAR S3776
810
938
  }
811
939
  }
812
940
 
813
- return buildResult(checks, failedChecks, errors, startTime);
941
+ const result = buildResult(checks, failedChecks, errors, startTime);
942
+ result.validationReceipt = receiptApi.completeValidation(rootDir, receiptSnapshot, result);
943
+ return result;
814
944
  }
815
945
 
816
946
  /**
@@ -885,21 +1015,43 @@ function executeDebugMode({ fixAttempts = 0, claim } = {}) {
885
1015
  module.exports = {
886
1016
  name: 'validate',
887
1017
  description: 'Run validation checks across type, lint, security, and tests',
888
- handler: async () => {
889
- const result = await executeValidate();
890
- if (!result.success) {
891
- return result;
1018
+ usage: 'forge validate [--shards N]',
1019
+ flags: {
1020
+ '--shards N': 'Set the full-suite resource budget',
1021
+ },
1022
+ handler: async (args = [], _flags = {}, projectRoot = process.cwd(), deps = {}) => {
1023
+ let resourceBudget;
1024
+ try {
1025
+ resourceBudget = parseResourceBudget(args);
1026
+ } catch (error) {
1027
+ return { success: false, error: error.message };
892
1028
  }
893
-
1029
+ const result = await (deps.executeValidate || executeValidate)({
1030
+ rootDir: projectRoot,
1031
+ resourceBudget,
1032
+ });
1033
+ const errorParts = Array.isArray(result.errors) ? result.errors.filter(Boolean) : [];
1034
+ for (const check of Object.values(result.checks || {})) {
1035
+ if (check?.success !== false || check.skipped === true || !check.message) continue;
1036
+ if (!errorParts.some(error => error.endsWith(check.message))) errorParts.push(check.message);
1037
+ }
1038
+ const budget = result.checks?.tests?.resourceBudget;
1039
+ const budgetOutput = Number.isInteger(budget?.effective)
1040
+ ? `Full suite resource budget: requested=${budget.requested ?? 'default'} effective=${budget.effective}`
1041
+ : null;
1042
+ const output = [result.success ? result.summary : null, budgetOutput].filter(Boolean).join('\n');
1043
+ const error = result.error || result.message || errorParts.join('; ') || result.summary;
894
1044
  return {
895
1045
  ...result,
896
- output: result.summary,
1046
+ ...(output ? { output } : {}),
1047
+ ...(!result.success && error ? { error } : {}),
897
1048
  };
898
1049
  },
899
1050
  runTypeCheck,
900
1051
  runLint,
901
1052
  runSecurityScan,
902
1053
  runAllTests,
1054
+ parseResourceBudget,
903
1055
  parseTestCounts,
904
1056
  scanForConflictMarkers,
905
1057
  executeValidate,