forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +18 -7
- package/CHANGELOG.md +79 -1
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +28 -375
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +8 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +114 -35
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +359 -144
- package/lib/audit-evidence.js +71 -110
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +50 -20
- package/lib/commands/clean.js +252 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +276 -30
- package/lib/commands/insights.js +8 -3
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +1265 -58
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +3 -1
- package/lib/commands/preflight.js +21 -4
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +146 -54
- package/lib/commands/recall.js +127 -49
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +39 -3
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +323 -98
- package/lib/commands/shepherd.js +591 -73
- package/lib/commands/ship.js +36 -91
- package/lib/commands/skill.js +127 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +187 -38
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +359 -45
- package/lib/core/runtime-graph.js +1 -1
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +134 -32
- package/lib/gate-events.js +98 -10
- package/lib/git-defaults.js +56 -0
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +122 -5
- package/lib/insights.js +96 -80
- package/lib/issue-render.js +19 -0
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +739 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3435 -172
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +110 -28
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +106 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +143 -21
- package/lib/npm-publish-workflow.js +465 -0
- package/lib/orientation.js +68 -43
- package/lib/package-root.js +2 -0
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +18 -39
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1129 -470
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +293 -0
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +38 -20
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +145 -27
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +51 -11
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +178 -4
- package/lib/protected-state-authority.js +1100 -0
- package/lib/protected-state-surfaces.js +243 -45
- package/lib/release-readiness.js +53 -7
- package/lib/review-adapter.js +65 -0
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +71 -35
- package/lib/smart-merge.js +28 -4
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +39 -0
- package/lib/using-forge.js +19 -6
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +12 -9
- package/scripts/commitlint.js +13 -15
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +457 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +773 -0
- package/scripts/protected-state-check.js +479 -31
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +935 -37
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +271 -57
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +6 -11
- package/skills/review/evals/scorecard.json +4 -4
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +39 -16
- package/skills/shepherd/evals/scorecard.json +4 -4
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
- package/skills/worktree/SKILL.md +6 -1
- package/skills/worktree/evals/scorecard.json +2 -2
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -206
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
package/lib/commands/test.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Wraps test execution with smart defaults:
|
|
7
7
|
* - Auto-detects package manager from lockfiles
|
|
8
|
-
* - Checks Beads (Dolt) connectivity, sets BEADS_SKIP_TESTS if unavailable
|
|
9
8
|
* - Supports --affected flag to run only tests for changed files
|
|
10
9
|
*
|
|
11
10
|
* Security: Uses execFileSync for subprocess calls (OWASP A03)
|
|
@@ -17,6 +16,8 @@ const { execFileSync: defaultExecFileSync, spawnSync: defaultSpawnSync } = requi
|
|
|
17
16
|
const defaultFs = require('node:fs');
|
|
18
17
|
const path = require('node:path');
|
|
19
18
|
|
|
19
|
+
const { selectDocAssertingTests } = require('../doc-assertions');
|
|
20
|
+
|
|
20
21
|
/** @type {Array<[string, string]>} Lockfile → package manager mapping (order matters) */
|
|
21
22
|
const LOCKFILE_MAP = [
|
|
22
23
|
['bun.lockb', 'bun'],
|
|
@@ -26,13 +27,14 @@ const LOCKFILE_MAP = [
|
|
|
26
27
|
];
|
|
27
28
|
|
|
28
29
|
const DEFAULT_TIMEOUT = 120000;
|
|
29
|
-
const BEADS_CHECK_TIMEOUT = 3000;
|
|
30
30
|
const DIRECT_TEST_CANDIDATES = Object.freeze({
|
|
31
31
|
'bin/forge.js': [
|
|
32
32
|
'test/cli-flags.test.js',
|
|
33
33
|
'test/forge-cli-registry.test.js',
|
|
34
|
-
'test/setup-github-sync.test.js',
|
|
35
34
|
'test/setup-runtime-flags.test.js',
|
|
35
|
+
'test/github-launcher.test.js',
|
|
36
|
+
'test/integration/github-account-context.test.js',
|
|
37
|
+
'test/structural/github-account-public-surface.test.js',
|
|
36
38
|
],
|
|
37
39
|
// The second CLI surface (command descriptions + shepherd wiring). Without an
|
|
38
40
|
// entry here it resolves to zero tests and pushes fall back to the full suite.
|
|
@@ -41,15 +43,150 @@ const DIRECT_TEST_CANDIDATES = Object.freeze({
|
|
|
41
43
|
'test/forge-cmd-shepherd.test.js',
|
|
42
44
|
],
|
|
43
45
|
'bin/forge-preflight.js': ['test/bin/forge-preflight.test.js'],
|
|
46
|
+
// The CI entry point is covered by the doc-assertion regression suite; without
|
|
47
|
+
// this entry it resolves to zero tests and forces the full suite.
|
|
48
|
+
// (lib/doc-assertions.js resolves via the lib/ -> test/ convention below.)
|
|
49
|
+
'scripts/doc-asserting-tests.js': [
|
|
50
|
+
'test/doc-assertions.test.js',
|
|
51
|
+
'test/scripts/doc-asserting-tests.test.js',
|
|
52
|
+
],
|
|
53
|
+
// The suites that assert the repo .gitignore's actual content. The path is
|
|
54
|
+
// already known to the classifier, so without candidates here it resolves
|
|
55
|
+
// zero tests and a one-line ignore addition pays the full-suite lane.
|
|
56
|
+
'.gitignore': [
|
|
57
|
+
'test/check-forge-token.test.js',
|
|
58
|
+
'test/cleanup/dropped-agent-config.test.js',
|
|
59
|
+
'test/coverage-config.test.js',
|
|
60
|
+
'test/eval/eval-history.test.js',
|
|
61
|
+
'test/freshness-token.test.js',
|
|
62
|
+
'test/mutation-config.test.js',
|
|
63
|
+
'test/test-dashboard.test.js',
|
|
64
|
+
],
|
|
44
65
|
'lib/lefthook-check.js': ['test/lefthook-check.test.js', 'test/runtime-health.test.js'],
|
|
45
66
|
'lib/runtime-health.js': ['test/runtime-health.test.js'],
|
|
46
67
|
// skill-eval hosts the accuracy-lint detectors (auditCommandDocumentation /
|
|
47
68
|
// auditRouterPrecision), so edits there must also run their detector suite.
|
|
48
69
|
'lib/skill-eval.js': ['test/skill-eval.test.js', 'test/skill-accuracy.test.js'],
|
|
70
|
+
// Schema is shared by every kernel authority; keep it on the full-suite lane.
|
|
71
|
+
'lib/kernel/schema.js': [],
|
|
72
|
+
// Migrations have consumers across the kernel; an empty mapping deliberately
|
|
73
|
+
// selects the full-suite lane, matching the shared SQLite driver.
|
|
74
|
+
'lib/kernel/migrations.js': [],
|
|
75
|
+
'lib/commands/shepherd.js': [
|
|
76
|
+
'test/commands/shepherd.test.js',
|
|
77
|
+
'test/commands/github-route-matrix.test.js',
|
|
78
|
+
'test/pr-monitor/arm-on-push.test.js',
|
|
79
|
+
'test/pr-monitor/shepherd-watch.test.js',
|
|
80
|
+
'test/pr-monitor/watch-owner-cutover.test.js',
|
|
81
|
+
'test/pr-monitor/watch-owner-launch-contention.test.js',
|
|
82
|
+
'test/pr-monitor/watch-owner-launch-gate.test.js',
|
|
83
|
+
],
|
|
84
|
+
'lib/commands/github.js': [
|
|
85
|
+
'test/commands/github.test.js',
|
|
86
|
+
'test/github-launcher.test.js',
|
|
87
|
+
'test/integration/github-account-context.test.js',
|
|
88
|
+
],
|
|
89
|
+
'lib/github-context.js': ['test/github-context.test.js', 'test/integration/github-account-context.test.js'],
|
|
90
|
+
'lib/commands/_registry.js': [
|
|
91
|
+
'test/commands/_registry.test.js',
|
|
92
|
+
'test/commands/_registry-github-context.test.js',
|
|
93
|
+
],
|
|
94
|
+
'lib/commands/pr.js': ['test/commands/pr.test.js', 'test/commands/github-route-matrix.test.js'],
|
|
95
|
+
'lib/commands/ship.js': ['test/commands/ship.test.js', 'test/commands/github-route-matrix.test.js'],
|
|
96
|
+
'lib/commands/merge.js': ['test/commands/merge.test.js', 'test/commands/github-route-matrix.test.js'],
|
|
97
|
+
'lib/commands/team.js': ['test/commands/team.test.js', 'test/commands/github-route-matrix.test.js'],
|
|
98
|
+
'lib/commands/clean.js': ['test/commands/clean.test.js', 'test/commands/github-route-matrix.test.js'],
|
|
99
|
+
'scripts/github-context-bridge.sh': ['test/commands/github-route-matrix.test.js'],
|
|
100
|
+
'lib/commands/push.js': ['test/commands/push.test.js', 'test/commands/github-indirect-routes.test.js'],
|
|
101
|
+
'lib/commands/hooks.js': ['test/commands/hooks.test.js', 'test/hooks-session-start.test.js', 'test/commands/github-indirect-routes.test.js'],
|
|
102
|
+
'lib/commands/serve.js': ['test/commands/serve.test.js', 'test/commands/github-indirect-routes.test.js'],
|
|
103
|
+
'lib/commands/skill.js': ['test/commands/skill.test.js', 'test/commands/github-indirect-routes.test.js'],
|
|
104
|
+
'scripts/lib/behavioral-eval-runtime.js': ['test/eval/behavioral-eval-runtime.test.js', 'test/commands/github-indirect-routes.test.js'],
|
|
105
|
+
'web/dashboard/generate-snapshot.mjs': ['test/commands/github-indirect-routes.test.js'],
|
|
106
|
+
// The shared SQLite driver has more consumers than a bounded direct list can
|
|
107
|
+
// safely represent; its empty mapping deliberately selects the full-suite lane.
|
|
108
|
+
'lib/kernel/sqlite-driver.js': [],
|
|
109
|
+
'lib/pr-monitor/watch-owner.js': [
|
|
110
|
+
'test/pr-monitor/watch-owner.test.js',
|
|
111
|
+
'test/kernel/watch-owner-transaction.test.js',
|
|
112
|
+
],
|
|
113
|
+
'lib/pr-monitor/reconcile.js': [
|
|
114
|
+
'test/pr-monitor/reconcile.test.js',
|
|
115
|
+
'test/pr-monitor/reconcile-owner-authority.test.js',
|
|
116
|
+
],
|
|
117
|
+
'lib/pr-monitor/reconcile-executor.js': [
|
|
118
|
+
'test/commands/github-indirect-routes.test.js',
|
|
119
|
+
'test/pr-monitor/auto-trigger-containment.test.js',
|
|
120
|
+
'test/pr-monitor/reconcile-executor-owner-authority.test.js',
|
|
121
|
+
'test/pr-monitor/reconcile-executor.test.js',
|
|
122
|
+
'test/pr-monitor/reconcile-daemon-owner-authority.test.js',
|
|
123
|
+
'test/pr-monitor/reconcile-migration-gate.test.js',
|
|
124
|
+
'test/pr-monitor/watch-owner-launch-contention.test.js',
|
|
125
|
+
'test/windows-hide-background-spawns.test.js',
|
|
126
|
+
],
|
|
127
|
+
'lib/pr-monitor/watch.js': [
|
|
128
|
+
'test/pr-monitor/watch-owner-cutover.test.js',
|
|
129
|
+
'test/pr-monitor/watch.test.js',
|
|
130
|
+
],
|
|
131
|
+
'lib/pr-monitor/watch-lifecycle.js': [
|
|
132
|
+
'test/commands/github-indirect-routes.test.js',
|
|
133
|
+
'test/pr-monitor/watch-lifecycle.test.js',
|
|
134
|
+
'test/pr-monitor/watch-owner-launch-contention.test.js',
|
|
135
|
+
'test/pr-monitor/watch-owner-launch-gate.test.js',
|
|
136
|
+
],
|
|
137
|
+
'lib/pr-monitor/shepherd-lease.js': [
|
|
138
|
+
'test/pr-monitor/shepherd-lease.test.js',
|
|
139
|
+
'test/pr-monitor/shepherd-lease-election-only.test.js',
|
|
140
|
+
],
|
|
141
|
+
'lib/pr-monitor/monitor.js': [
|
|
142
|
+
'test/pr-monitor/monitor.test.js',
|
|
143
|
+
'test/pr-monitor/monitor-owner-signal.test.js',
|
|
144
|
+
],
|
|
49
145
|
'lib/upgrade-safety.js': ['test/commands/upgrade.test.js'],
|
|
50
146
|
'scripts/test.js': ['test/scripts/test-runner.test.js'],
|
|
147
|
+
// `bun.lock` is deliberately absent: a dependency change can break anything,
|
|
148
|
+
// so it stays on the full-suite lane.
|
|
149
|
+
// lefthook.yml is the repository's hook wiring, so this must also cover the
|
|
150
|
+
// suites that assert the individual pre-commit/pre-push commands exist.
|
|
151
|
+
'lefthook.yml': [
|
|
152
|
+
'test/lefthook-check.test.js',
|
|
153
|
+
'test/lefthook-wiring.test.js',
|
|
154
|
+
'test/commands/lefthook-user-config.test.js',
|
|
155
|
+
'test/branch-protection.test.js',
|
|
156
|
+
'test/commitlint.test.js',
|
|
157
|
+
'test/cross-platform-install.test.js',
|
|
158
|
+
'test/sync-d20-audit-script.test.js',
|
|
159
|
+
],
|
|
160
|
+
'eslint.config.js': ['test/lint-script.test.js'],
|
|
161
|
+
'.coderabbit.yaml': ['test/coderabbit-config.test.js'],
|
|
162
|
+
'.claude-plugin/marketplace.json': [
|
|
163
|
+
'test/activation/plugin-scaffold.test.js',
|
|
164
|
+
'test/plugin-catalog.test.js',
|
|
165
|
+
],
|
|
51
166
|
});
|
|
52
167
|
|
|
168
|
+
// Shared authority modules must run the full suite even when another changed file
|
|
169
|
+
// resolves a targeted test. Keep this signal separate from test candidates: an
|
|
170
|
+
// empty candidate list is not enough once a mapped consumer is also changed.
|
|
171
|
+
const FULL_SUITE_REQUIRED_FILES = new Set([
|
|
172
|
+
'lib/kernel/schema.js',
|
|
173
|
+
'lib/kernel/migrations.js',
|
|
174
|
+
'lib/kernel/sqlite-driver.js',
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
// Directory-level mappings for source trees that own a dedicated suite. Without
|
|
178
|
+
// these a change under any of them resolves to zero tests and forces the full
|
|
179
|
+
// suite (see hasZeroResolvedTests in scripts/test.js).
|
|
180
|
+
const PREFIX_TEST_TARGETS = Object.freeze([
|
|
181
|
+
['validation/', ['test/validation/risk-manifest.test.js', 'test/validation/risk-manifest-generator.test.js']],
|
|
182
|
+
// `eval/corpus/` must come first: the corpus hash and manifest checks live in
|
|
183
|
+
// immutable-corpus, and eval-sets only reads `eval/commands/`.
|
|
184
|
+
['eval/corpus/', ['test/eval/immutable-corpus.test.js']],
|
|
185
|
+
['eval/', ['test/eval/eval-sets.test.js']],
|
|
186
|
+
['rules/', ['test/rules-sync.test.js', 'test/skill-dispatch-parity.test.js']],
|
|
187
|
+
['plugin/', ['test/activation/plugin-scaffold.test.js', 'test/plugin-catalog.test.js', 'test/release-readiness.test.js']],
|
|
188
|
+
]);
|
|
189
|
+
|
|
53
190
|
// Skill sources (canonical `skills/**` and the committed `.agents/skills/**` mirror)
|
|
54
191
|
// map to the fast skill suite. This is what keeps a skills-only PR on the targeted
|
|
55
192
|
// lane instead of the full ~1500-test suite. `skills-sync-drift` guards the mirror.
|
|
@@ -84,22 +221,6 @@ function detectPackageManager(projectRoot, fs) {
|
|
|
84
221
|
return 'npm';
|
|
85
222
|
}
|
|
86
223
|
|
|
87
|
-
/**
|
|
88
|
-
* Check if Beads (bd CLI) is reachable.
|
|
89
|
-
*
|
|
90
|
-
* @param {Function} execFileSync - Injected execFileSync
|
|
91
|
-
* @returns {boolean} true if bd is available
|
|
92
|
-
*/
|
|
93
|
-
function checkBeadsConnectivity(execFileSync) {
|
|
94
|
-
try {
|
|
95
|
-
execFileSync('bd', ['list', '--limit=1'], { timeout: BEADS_CHECK_TIMEOUT });
|
|
96
|
-
return true;
|
|
97
|
-
} catch (_e) { // NOSONAR S2486
|
|
98
|
-
/* intentional: bd not installed or unreachable */
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
224
|
function resolveBaseBranch(execFileSync) {
|
|
104
225
|
let baseBranch = 'main';
|
|
105
226
|
try {
|
|
@@ -186,7 +307,9 @@ function getChangedFiles(execFileSync, options = {}) {
|
|
|
186
307
|
* @returns {string[]} Array of test file paths (e.g. ['test/foo.test.js'])
|
|
187
308
|
*/
|
|
188
309
|
function getAffectedTestFiles(projectRoot, execFileSync, fs = defaultFs, options = {}) {
|
|
189
|
-
const changedFiles =
|
|
310
|
+
const changedFiles = Array.isArray(options.changedFiles)
|
|
311
|
+
? options.changedFiles
|
|
312
|
+
: getChangedFiles(execFileSync, options);
|
|
190
313
|
const testFiles = new Set();
|
|
191
314
|
for (const file of changedFiles) {
|
|
192
315
|
for (const candidate of getTestCandidatesForChangedFile(file)) {
|
|
@@ -196,6 +319,16 @@ function getAffectedTestFiles(projectRoot, execFileSync, fs = defaultFs, options
|
|
|
196
319
|
}
|
|
197
320
|
}
|
|
198
321
|
|
|
322
|
+
// Markdown changes must also run the suites that assert on that markdown's
|
|
323
|
+
// CONTENT (README size badge, AGENTS.md convention, docs consistency). The
|
|
324
|
+
// static mapping above only knows the docs-consistency suite, so a docs PR
|
|
325
|
+
// used to merge green and break master for the next code PR (issue 63556816).
|
|
326
|
+
// The set is derived from the test sources, so new markdown-reading suites are
|
|
327
|
+
// picked up without editing a list here.
|
|
328
|
+
for (const candidate of selectDocAssertingTests(changedFiles, projectRoot, fs)) {
|
|
329
|
+
testFiles.add(candidate);
|
|
330
|
+
}
|
|
331
|
+
|
|
199
332
|
return Array.from(testFiles).sort((left, right) => left.localeCompare(right));
|
|
200
333
|
}
|
|
201
334
|
|
|
@@ -215,10 +348,24 @@ function getTestCandidatesForChangedFile(file) {
|
|
|
215
348
|
return [...SKILL_TEST_TARGETS];
|
|
216
349
|
}
|
|
217
350
|
|
|
351
|
+
// Maintainer-only contributor skills. They document the AGENTS.md product /
|
|
352
|
+
// maintainer split, so the docs-bleed gate is the suite that can catch a
|
|
353
|
+
// contradiction between them.
|
|
354
|
+
if (file.startsWith('.forge/contributor-skills/')) {
|
|
355
|
+
return ['test/structural/agents-docs-bleed.test.js'];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const [prefix, targets] of PREFIX_TEST_TARGETS) {
|
|
359
|
+
if (file.startsWith(prefix)) {
|
|
360
|
+
return [...targets];
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
218
364
|
if (file === 'README.md'
|
|
219
365
|
|| file === 'QUICKSTART.md'
|
|
220
366
|
|| file === 'CHANGELOG.md'
|
|
221
367
|
|| file === 'AGENTS.md'
|
|
368
|
+
|| file === 'CODING_STANDARDS.md'
|
|
222
369
|
|| file === 'DEVELOPMENT.md'
|
|
223
370
|
|| file === 'docs/INDEX.md'
|
|
224
371
|
|| file === 'docs/PROJECT_DESIGN.md'
|
|
@@ -226,7 +373,9 @@ function getTestCandidatesForChangedFile(file) {
|
|
|
226
373
|
|| file.startsWith('docs/guides/')
|
|
227
374
|
|| file.startsWith('docs/reference/')
|
|
228
375
|
|| file.startsWith('docs/work/')) {
|
|
229
|
-
return
|
|
376
|
+
return file === 'AGENTS.md'
|
|
377
|
+
? ['test/docs-consistency.test.js', 'test/structural/agents-docs-bleed.test.js']
|
|
378
|
+
: ['test/docs-consistency.test.js'];
|
|
230
379
|
}
|
|
231
380
|
|
|
232
381
|
if (file.startsWith('lib/') && file.endsWith('.js')) {
|
|
@@ -270,10 +419,15 @@ function getTestCandidatesForChangedFile(file) {
|
|
|
270
419
|
return [];
|
|
271
420
|
}
|
|
272
421
|
|
|
422
|
+
function isFullSuiteRequiredFile(file) {
|
|
423
|
+
return FULL_SUITE_REQUIRED_FILES.has(file);
|
|
424
|
+
}
|
|
425
|
+
|
|
273
426
|
module.exports = {
|
|
274
427
|
getChangedFiles,
|
|
275
428
|
getAffectedTestFiles,
|
|
276
429
|
getTestCandidatesForChangedFile,
|
|
430
|
+
isFullSuiteRequiredFile,
|
|
277
431
|
name: 'test',
|
|
278
432
|
description: 'Run tests with smart defaults (timeout, affected-only)',
|
|
279
433
|
usage: 'forge test [--affected]',
|
|
@@ -291,7 +445,7 @@ module.exports = {
|
|
|
291
445
|
* @param {Object} [deps.fs] - fs module
|
|
292
446
|
* @param {Function} [deps.execFileSync] - child_process.execFileSync
|
|
293
447
|
* @param {Function} [deps.spawnSync] - child_process.spawnSync
|
|
294
|
-
* @returns {Promise<{ success: boolean, exitCode: number
|
|
448
|
+
* @returns {Promise<{ success: boolean, exitCode: number }>}
|
|
295
449
|
*/
|
|
296
450
|
async handler(_args, flags, projectRoot, deps = {}) {
|
|
297
451
|
const fs = deps.fs || defaultFs;
|
|
@@ -313,32 +467,28 @@ module.exports = {
|
|
|
313
467
|
}
|
|
314
468
|
} catch (_e) { /* intentional: package.json missing or unreadable, use default timeout */ } // NOSONAR S2486
|
|
315
469
|
|
|
316
|
-
// 3.
|
|
317
|
-
const beadsAvailable = checkBeadsConnectivity(execFileSync);
|
|
318
|
-
const beadsSkipped = !beadsAvailable;
|
|
319
|
-
|
|
320
|
-
const extraEnv = {};
|
|
321
|
-
if (beadsSkipped) {
|
|
322
|
-
extraEnv.BEADS_SKIP_TESTS = '1';
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// 4. Build test command args
|
|
470
|
+
// 3. Build test command args
|
|
326
471
|
let testArgs = ['run', 'test'];
|
|
327
472
|
|
|
328
|
-
//
|
|
473
|
+
// 4. --affected flag: find changed test files
|
|
329
474
|
if (flags['--affected'] || flags.affected) {
|
|
330
|
-
const
|
|
475
|
+
const diffOptions = {
|
|
331
476
|
sinceUpstream: flags.sinceUpstream || flags['--since-upstream'],
|
|
477
|
+
};
|
|
478
|
+
const changedFiles = getChangedFiles(execFileSync, diffOptions);
|
|
479
|
+
const affectedTests = getAffectedTestFiles(projectRoot, execFileSync, fs, {
|
|
480
|
+
...diffOptions,
|
|
481
|
+
changedFiles,
|
|
332
482
|
});
|
|
333
|
-
if (affectedTests.length > 0) {
|
|
483
|
+
if (!changedFiles.some(isFullSuiteRequiredFile) && affectedTests.length > 0) {
|
|
334
484
|
testArgs = ['run', 'test', ...affectedTests];
|
|
335
485
|
}
|
|
336
486
|
// If no affected tests found, fall back to running all tests
|
|
337
487
|
}
|
|
338
488
|
|
|
339
|
-
//
|
|
489
|
+
// 5. Run tests
|
|
340
490
|
const result = spawnSync(pkgManager, testArgs, {
|
|
341
|
-
env: { ...process.env
|
|
491
|
+
env: { ...process.env },
|
|
342
492
|
timeout,
|
|
343
493
|
stdio: 'inherit',
|
|
344
494
|
shell: process.platform === 'win32',
|
|
@@ -349,7 +499,6 @@ module.exports = {
|
|
|
349
499
|
return {
|
|
350
500
|
success: exitCode === 0,
|
|
351
501
|
exitCode,
|
|
352
|
-
beadsSkipped,
|
|
353
502
|
};
|
|
354
503
|
},
|
|
355
504
|
};
|
package/lib/commands/validate.js
CHANGED
|
@@ -44,10 +44,12 @@ const CONFLICT_DIVIDER_PATTERN = /^=======$/;
|
|
|
44
44
|
const CONFLICT_END_PATTERN = /^>>>>>>>.*$/;
|
|
45
45
|
const GIT_CONFLICT_SCAN_MAX_BUFFER = 16 * 1024 * 1024;
|
|
46
46
|
const VALIDATION_COMMAND_TIMEOUT_MS = 600000;
|
|
47
|
-
|
|
47
|
+
// Match the full-suite runner's measured headroom; its verbose output exceeds 1 MiB.
|
|
48
|
+
const FULL_SUITE_TIMEOUT_MS = 25 * 60 * 1000;
|
|
49
|
+
const FULL_SUITE_MAX_BUFFER = 16 * 1024 * 1024;
|
|
48
50
|
|
|
49
|
-
function validationTimeoutMessage(label) {
|
|
50
|
-
return `${label} timed out after ${
|
|
51
|
+
function validationTimeoutMessage(label, timeoutMs = VALIDATION_COMMAND_TIMEOUT_MS) {
|
|
52
|
+
return `${label} timed out after ${Math.round(timeoutMs / 60000)} minutes`;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
function listConflictScanFiles(rootDir) {
|
|
@@ -80,8 +82,15 @@ function shouldSkipConflictScanPath(relativePath) {
|
|
|
80
82
|
));
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
/** Build bounded process options, with extra headroom only for Forge's full suite. */
|
|
86
|
+
function getExecOptions(rootDir = process.cwd(), fullSuite = false) {
|
|
87
|
+
const options = {
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
cwd: rootDir,
|
|
90
|
+
timeout: fullSuite ? FULL_SUITE_TIMEOUT_MS : VALIDATION_COMMAND_TIMEOUT_MS,
|
|
91
|
+
};
|
|
92
|
+
if (fullSuite) options.maxBuffer = FULL_SUITE_MAX_BUFFER;
|
|
93
|
+
return options;
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
const ERROR_PATTERNS = {
|
|
@@ -130,7 +139,7 @@ function parseNumber(match, index = 1, defaultValue = 0) {
|
|
|
130
139
|
*/
|
|
131
140
|
function getCheckStatus(check) {
|
|
132
141
|
if (!check) return null;
|
|
133
|
-
if (check.skipped) return 'SKIPPED';
|
|
142
|
+
if (check.skipped === true) return 'SKIPPED';
|
|
134
143
|
return check.success ? 'PASS' : 'FAIL';
|
|
135
144
|
}
|
|
136
145
|
|
|
@@ -343,17 +352,18 @@ async function runTypeCheck() {
|
|
|
343
352
|
* Run ESLint
|
|
344
353
|
* Executes eslint . to check code quality
|
|
345
354
|
*
|
|
355
|
+
* @param {typeof execFileSync} exec - Synchronous process runner
|
|
346
356
|
* @returns {Promise<{success: boolean, duration: number, warnings?: number, errors?: number, message?: string}>} Lint result
|
|
347
357
|
* @example
|
|
348
358
|
* const result = await runLint();
|
|
349
359
|
* console.log(`Warnings: ${result.warnings}, Errors: ${result.errors}`);
|
|
350
360
|
*/
|
|
351
|
-
async function runLint() {
|
|
361
|
+
async function runLint(exec = execFileSync) {
|
|
352
362
|
const startTime = Date.now();
|
|
353
363
|
|
|
354
364
|
try {
|
|
355
365
|
// Run eslint with no output (exit code determines success)
|
|
356
|
-
|
|
366
|
+
exec('eslint', ['.'], getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
357
367
|
|
|
358
368
|
return {
|
|
359
369
|
success: true,
|
|
@@ -556,7 +566,7 @@ async function runSecurityScan() { // NOSONAR S3776
|
|
|
556
566
|
|
|
557
567
|
/**
|
|
558
568
|
* Run all tests
|
|
559
|
-
*
|
|
569
|
+
* Uses Forge's full-suite runner when present, with raw Bun as an external-repository fallback
|
|
560
570
|
*
|
|
561
571
|
* @returns {Promise<{success: boolean, duration: number, passed: number, failed: number, total: number, message?: string}>} Test execution result
|
|
562
572
|
* @example
|
|
@@ -565,6 +575,22 @@ async function runSecurityScan() { // NOSONAR S3776
|
|
|
565
575
|
*/
|
|
566
576
|
function parseTestCounts(output) {
|
|
567
577
|
const text = String(output || '');
|
|
578
|
+
const aggregateMatches = [...text.matchAll(
|
|
579
|
+
/Full suite aggregate: status=(PASS|FAIL|INCOMPLETE) tests=(\d+) assertions=\d+ passed=(\d+) failed=(\d+) errors=(\d+) skipped=(\d+)/g,
|
|
580
|
+
)]; // NOSONAR S5852 - fixed tokens and bounded numeric fields
|
|
581
|
+
const aggregate = aggregateMatches.at(-1);
|
|
582
|
+
if (aggregate) {
|
|
583
|
+
const total = parseNumber(aggregate, 2);
|
|
584
|
+
return {
|
|
585
|
+
status: aggregate[1],
|
|
586
|
+
passed: parseNumber(aggregate, 3),
|
|
587
|
+
failed: parseNumber(aggregate, 4),
|
|
588
|
+
errors: parseNumber(aggregate, 5),
|
|
589
|
+
skipped: parseNumber(aggregate, 6),
|
|
590
|
+
total,
|
|
591
|
+
testsFound: total > 0,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
568
594
|
const passed = parseNumber(/(\d+) pass/.exec(text)); // NOSONAR S5852 - bounded \d+ pattern
|
|
569
595
|
const failed = parseNumber(/(\d+) fail/.exec(text)); // NOSONAR S5852 - bounded \d+ pattern
|
|
570
596
|
const skipped = parseNumber(/(\d+) skip/.exec(text)); // NOSONAR S5852 - bounded \d+ pattern
|
|
@@ -573,13 +599,31 @@ function parseTestCounts(output) {
|
|
|
573
599
|
return { passed, failed, skipped, total, testsFound: total > 0 };
|
|
574
600
|
}
|
|
575
601
|
|
|
576
|
-
|
|
602
|
+
function isForgeCheckout(rootDir) {
|
|
603
|
+
try {
|
|
604
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'));
|
|
605
|
+
return manifest.name === 'forge-workflow'
|
|
606
|
+
&& manifest.bin?.forge === 'bin/forge.js'
|
|
607
|
+
&& manifest.scripts?.['test:full:parallel'] === 'node scripts/test-full-suite.js'
|
|
608
|
+
&& fs.existsSync(path.join(rootDir, 'scripts', 'test-full-suite.js'));
|
|
609
|
+
} catch (error) {
|
|
610
|
+
if (error instanceof SyntaxError || error.code === 'ENOENT' || error.code === 'ENOTDIR') return false;
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function runAllTests(exec = execFileSync, rootDir = process.cwd()) {
|
|
577
616
|
const startTime = Date.now();
|
|
617
|
+
const useFullSuite = isForgeCheckout(rootDir);
|
|
618
|
+
const command = useFullSuite ? 'node' : 'bun';
|
|
619
|
+
const args = useFullSuite ? ['scripts/test-full-suite.js'] : ['test', '--timeout', '30000'];
|
|
620
|
+
const execOptions = getExecOptions(rootDir, useFullSuite);
|
|
578
621
|
|
|
579
622
|
try {
|
|
580
|
-
const result = exec(
|
|
623
|
+
const result = exec(command, args, execOptions); // NOSONAR S4036 - fixed CLI commands, no user input, developer tool context
|
|
581
624
|
|
|
582
|
-
const { passed, failed, skipped, total, testsFound } = parseTestCounts(result);
|
|
625
|
+
const { status, passed, failed, errors = 0, skipped, total, testsFound } = parseTestCounts(result);
|
|
626
|
+
const success = failed === 0 && errors === 0 && (status === undefined || status === 'PASS');
|
|
583
627
|
|
|
584
628
|
// B2 (N1): a bun run that executes 0 tests must NEVER report PASS — that
|
|
585
629
|
// manufactures false confidence. Surface it as an explicit SKIP instead.
|
|
@@ -597,27 +641,27 @@ async function runAllTests(exec = execFileSync) {
|
|
|
597
641
|
}
|
|
598
642
|
|
|
599
643
|
return {
|
|
600
|
-
success
|
|
644
|
+
success,
|
|
601
645
|
testsFound: true,
|
|
602
646
|
duration: Date.now() - startTime,
|
|
603
647
|
passed,
|
|
604
648
|
failed,
|
|
605
649
|
skipped,
|
|
606
650
|
total,
|
|
607
|
-
message:
|
|
651
|
+
message: success
|
|
608
652
|
? `All ${total} tests passed`
|
|
609
|
-
: `${failed}/${total} tests failed`,
|
|
653
|
+
: `${failed + errors}/${total} tests failed`,
|
|
610
654
|
};
|
|
611
655
|
} catch (error) {
|
|
612
656
|
// Check for timeout
|
|
613
|
-
if (error.killed && error.signal === 'SIGTERM') {
|
|
657
|
+
if (error.code === 'ETIMEDOUT' || (error.killed && error.signal === 'SIGTERM')) {
|
|
614
658
|
return {
|
|
615
659
|
success: false,
|
|
616
660
|
duration: Date.now() - startTime,
|
|
617
661
|
passed: 0,
|
|
618
662
|
failed: 0,
|
|
619
663
|
total: 0,
|
|
620
|
-
message: validationTimeoutMessage('Test execution'),
|
|
664
|
+
message: validationTimeoutMessage('Test execution', execOptions.timeout),
|
|
621
665
|
};
|
|
622
666
|
}
|
|
623
667
|
|
|
@@ -631,7 +675,7 @@ async function runAllTests(exec = execFileSync) {
|
|
|
631
675
|
passed: 0,
|
|
632
676
|
failed: 0,
|
|
633
677
|
total: 0,
|
|
634
|
-
message: 'Tests skipped:
|
|
678
|
+
message: 'Tests skipped: test runner not found. Install Node.js and Bun to run tests.',
|
|
635
679
|
};
|
|
636
680
|
}
|
|
637
681
|
|
|
@@ -641,7 +685,7 @@ async function runAllTests(exec = execFileSync) {
|
|
|
641
685
|
const passed = counts.passed;
|
|
642
686
|
// bun exited non-zero: if the output shows no explicit failure count, treat
|
|
643
687
|
// it as at least one failure rather than swallowing the non-zero exit.
|
|
644
|
-
const failed = counts.failed > 0 ? counts.failed : Math.max(1, counts.
|
|
688
|
+
const failed = counts.failed > 0 ? counts.failed : Math.max(1, counts.errors || 0);
|
|
645
689
|
const total = counts.total > 0 ? counts.total : passed + failed + counts.skipped;
|
|
646
690
|
|
|
647
691
|
return {
|
|
@@ -753,8 +797,8 @@ async function executeValidate(options = {}) { // NOSONAR S3776
|
|
|
753
797
|
// 4. Tests
|
|
754
798
|
if (!skip.includes(CHECK_TYPES.TESTS)) {
|
|
755
799
|
try {
|
|
756
|
-
checks.tests = await runAllTests();
|
|
757
|
-
if (!checks.tests.success &&
|
|
800
|
+
checks.tests = await runAllTests(execFileSync, rootDir);
|
|
801
|
+
if (!checks.tests.success && checks.tests.skipped !== true) {
|
|
758
802
|
failedChecks.push(CHECK_TYPES.TESTS);
|
|
759
803
|
if (!continueOnError) {
|
|
760
804
|
return buildResult(checks, failedChecks, errors, startTime);
|