forge-workflow 0.0.8 โ†’ 0.0.9

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 (57) hide show
  1. package/.claude/commands/premerge.md +2 -2
  2. package/.claude/commands/review.md +5 -2
  3. package/.claude/commands/ship.md +4 -3
  4. package/.claude/rules/greptile-review-process.md +4 -4
  5. package/.cline/workflows/premerge.md +2 -2
  6. package/.cline/workflows/review.md +5 -2
  7. package/.cline/workflows/ship.md +4 -3
  8. package/.codex/skills/premerge/SKILL.md +2 -2
  9. package/.codex/skills/review/SKILL.md +5 -2
  10. package/.codex/skills/ship/SKILL.md +4 -3
  11. package/.cursor/commands/premerge.md +2 -2
  12. package/.cursor/commands/review.md +5 -2
  13. package/.cursor/commands/ship.md +4 -3
  14. package/.github/prompts/premerge.prompt.md +2 -2
  15. package/.github/prompts/review.prompt.md +5 -2
  16. package/.github/prompts/ship.prompt.md +4 -3
  17. package/.github/workflows/beads-to-github.yml +1 -1
  18. package/.github/workflows/github-to-beads.yml +1 -1
  19. package/.kilocode/workflows/premerge.md +2 -2
  20. package/.kilocode/workflows/review.md +5 -2
  21. package/.kilocode/workflows/ship.md +4 -3
  22. package/.opencode/commands/premerge.md +2 -2
  23. package/.opencode/commands/review.md +5 -2
  24. package/.opencode/commands/ship.md +4 -3
  25. package/.roo/commands/premerge.md +2 -2
  26. package/.roo/commands/review.md +5 -2
  27. package/.roo/commands/ship.md +4 -3
  28. package/AGENTS.md +9 -9
  29. package/README.md +12 -6
  30. package/bin/forge.js +14 -3
  31. package/docs/BEADS_GITHUB_SYNC.md +6 -2
  32. package/docs/EXAMPLES.md +22 -22
  33. package/docs/ROADMAP.md +3 -3
  34. package/docs/TOOLCHAIN.md +60 -52
  35. package/lib/agents/codex.plugin.json +3 -0
  36. package/lib/agents-config.js +18 -12
  37. package/lib/codex-skills.js +54 -1
  38. package/lib/commands/plan.js +5 -2
  39. package/lib/commands/setup.js +231 -17
  40. package/lib/commands/ship.js +188 -5
  41. package/lib/commands/status.js +20 -33
  42. package/lib/commands/test.js +90 -25
  43. package/lib/commands/validate.js +218 -1
  44. package/lib/setup-action-log.js +2 -0
  45. package/lib/setup-summary-renderer.js +15 -11
  46. package/lib/workflow/enforce-stage.js +12 -8
  47. package/lib/workflow/state-manager.js +193 -0
  48. package/package.json +1 -1
  49. package/scripts/dep-guard.sh +11 -1
  50. package/scripts/forge-team/lib/hooks.sh +1 -1
  51. package/scripts/forge-team/lib/verify.sh +1 -1
  52. package/scripts/forge-team/lib/workload.sh +56 -27
  53. package/scripts/forge-team/tests/workload.test.sh +35 -4
  54. package/scripts/github-beads-sync/run-bd.mjs +4 -2
  55. package/scripts/smart-status.sh +10 -1
  56. package/scripts/sync-utils.sh +39 -0
  57. package/scripts/test.js +144 -38
package/scripts/test.js CHANGED
@@ -1,17 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Cross-platform test runner for lefthook pre-push hook.
4
- * Replaces bash-only: bun/pnpm/yarn/npm test detection with if/elif.
5
- * Works on Windows CMD, PowerShell, macOS, Linux.
3
+ * Cross-platform test runner for the lefthook pre-push hook.
4
+ *
5
+ * Runs only the tests affected by the changes being pushed when possible.
6
+ * Falls back to the full suite only for package-level changes.
6
7
  */
7
8
 
8
- const { spawnSync } = require('node:child_process');
9
+ const { execFileSync: defaultExecFileSync, spawnSync: defaultSpawnSync } = require('node:child_process');
9
10
  const fs = require('node:fs');
10
11
 
11
- // On Windows, package manager CLIs are .cmd files โ€” shell: true resolves them
12
+ const {
13
+ getAffectedTestFiles,
14
+ getChangedFiles,
15
+ } = require('../lib/commands/test');
16
+
17
+ const PACKAGE_LEVEL_PATHS = new Set([
18
+ 'package.json',
19
+ 'bun.lockb',
20
+ 'pnpm-lock.yaml',
21
+ 'yarn.lock',
22
+ 'package-lock.json',
23
+ ]);
24
+
12
25
  const isWindows = process.platform === 'win32';
13
26
 
14
- // Detect package manager from lock files (same priority as forge.js)
15
27
  function detectPackageManager() {
16
28
  if (fs.existsSync('bun.lockb') || fs.existsSync('bun.lock')) return 'bun';
17
29
  if (fs.existsSync('pnpm-lock.yaml')) return 'pnpm';
@@ -19,43 +31,137 @@ function detectPackageManager() {
19
31
  return 'npm';
20
32
  }
21
33
 
22
- const pkgManager = detectPackageManager();
23
- console.log(`๐Ÿงช Running test suite (${pkgManager} test)...`);
24
-
25
- // Strip git hook environment variables so child processes (especially tests
26
- // that create temp git repos) never accidentally operate on the real worktree.
27
- // During pre-push hooks, git sets GIT_DIR pointing to the repo โ€” any test that
28
- // runs `git init` / `git commit` in a temp dir inherits this and silently
29
- // commits into the worktree instead, creating rogue "initial commit" that
30
- // deletes the entire codebase.
31
- const env = { ...process.env };
32
- for (const key of Object.keys(env)) {
33
- if (key === 'GIT_DIR' || key === 'GIT_WORK_TREE' || key === 'GIT_INDEX_FILE'
34
- || key === 'GIT_OBJECT_DIRECTORY' || key === 'GIT_ALTERNATE_OBJECT_DIRECTORIES'
35
- || key === 'GIT_QUARANTINE_PATH') {
36
- delete env[key];
34
+ function stripGitHookEnv(sourceEnv = process.env) {
35
+ const env = { ...sourceEnv };
36
+ for (const key of Object.keys(env)) {
37
+ if (key === 'GIT_DIR' || key === 'GIT_WORK_TREE' || key === 'GIT_INDEX_FILE'
38
+ || key === 'GIT_OBJECT_DIRECTORY' || key === 'GIT_ALTERNATE_OBJECT_DIRECTORIES'
39
+ || key === 'GIT_QUARANTINE_PATH') {
40
+ delete env[key];
41
+ }
37
42
  }
43
+ return env;
38
44
  }
39
45
 
40
- // Use 'run test' to invoke the package.json script (which may include --timeout flags)
41
- // 'bun test' is a built-in that ignores package.json scripts
42
- const result = spawnSync(pkgManager, ['run', 'test'], { stdio: 'inherit', shell: isWindows, env });
46
+ function classifyPushTests(projectRoot, execFileSync = defaultExecFileSync) {
47
+ const changedFiles = getChangedFiles(execFileSync, { sinceUpstream: true });
48
+ const testTargets = getAffectedTestFiles(projectRoot, execFileSync, fs, { sinceUpstream: true });
49
+
50
+ let runFullSuite = false;
51
+ let runTestEnv = false;
52
+ let runE2E = false;
53
+ let hasUnmappedFiles = false;
54
+ const hasUnknownChangedFiles = changedFiles.length === 0 && testTargets.length === 0;
55
+
56
+ for (const file of changedFiles) {
57
+ if (PACKAGE_LEVEL_PATHS.has(file) || file.startsWith('packages/')) {
58
+ runFullSuite = true;
59
+ runTestEnv = true;
60
+ runE2E = true;
61
+ break;
62
+ }
63
+
64
+ if (file.startsWith('test-env/')) {
65
+ runTestEnv = true;
66
+ continue;
67
+ }
68
+
69
+ if (file.startsWith('test/e2e/')) {
70
+ runE2E = true;
71
+ continue;
72
+ }
73
+
74
+ if ((file.startsWith('lib/') || file.startsWith('scripts/')) && file.endsWith('.js')) {
75
+ runTestEnv = true;
76
+ continue;
77
+ }
78
+
79
+ if (file.startsWith('test/') || file.startsWith('.github/workflows/') || file.startsWith('.claude/commands/')) {
80
+ continue;
81
+ }
82
+
83
+ hasUnmappedFiles = true;
84
+ }
85
+
86
+ return {
87
+ changedFiles,
88
+ hasUnmappedFiles,
89
+ hasUnknownChangedFiles,
90
+ runE2E,
91
+ runFullSuite: runFullSuite || hasUnmappedFiles || hasUnknownChangedFiles,
92
+ runTestEnv,
93
+ testTargets,
94
+ };
95
+ }
43
96
 
44
- if (result.error) {
45
- console.error('');
46
- console.error(`โŒ Failed to run ${pkgManager} test: ${result.error.message}`);
47
- console.error(` Is '${pkgManager}' installed and on PATH?`);
48
- console.error('');
49
- process.exit(1);
97
+ function runCommand(command, args, options = {}, spawnSync = defaultSpawnSync) {
98
+ const result = spawnSync(command, args, {
99
+ stdio: 'inherit',
100
+ shell: isWindows,
101
+ ...options,
102
+ });
103
+
104
+ if (result.error) {
105
+ throw result.error;
106
+ }
107
+
108
+ return result.status ?? 1;
109
+ }
110
+
111
+ function runPrePushTests(projectRoot = process.cwd(), deps = {}) {
112
+ const spawnSync = deps.spawnSync || defaultSpawnSync;
113
+ const execFileSync = deps.execFileSync || defaultExecFileSync;
114
+ const pkgManager = deps.pkgManager || detectPackageManager();
115
+ const env = deps.env || stripGitHookEnv(process.env);
116
+ const plan = classifyPushTests(projectRoot, execFileSync);
117
+
118
+ console.log(`Running pre-push tests (${pkgManager})...`);
119
+
120
+ try {
121
+ if (plan.runFullSuite) {
122
+ const reason = plan.hasUnmappedFiles
123
+ ? 'unmapped pushed files require full unit coverage'
124
+ : plan.hasUnknownChangedFiles
125
+ ? 'changed files could not be resolved safely'
126
+ : 'package-level changes detected';
127
+ console.log(` Mode: full suite (${reason})`);
128
+ const status = runCommand(pkgManager, ['run', 'test'], { env }, spawnSync);
129
+ if (status !== 0) return status;
130
+ } else if (plan.testTargets.length > 0) {
131
+ console.log(` Mode: targeted (${plan.testTargets.length} test file${plan.testTargets.length === 1 ? '' : 's'})`);
132
+ const status = runCommand(pkgManager, ['run', 'test', ...plan.testTargets], { env }, spawnSync);
133
+ if (status !== 0) return status;
134
+ }
135
+
136
+ if (plan.runE2E) {
137
+ console.log(' Extra: running affected e2e tests');
138
+ const status = runCommand('bun', ['test', 'test/e2e/'], { env }, spawnSync);
139
+ if (status !== 0) return status;
140
+ }
141
+
142
+ if (plan.runTestEnv) {
143
+ console.log(' Extra: running affected edge-case tests');
144
+ const status = runCommand('bun', ['test', 'test-env/'], { env }, spawnSync);
145
+ if (status !== 0) return status;
146
+ }
147
+
148
+ console.log('Relevant tests passed');
149
+ return 0;
150
+ } catch (error) {
151
+ console.error('');
152
+ console.error(`Failed to run tests: ${error.message}`);
153
+ console.error('');
154
+ return 1;
155
+ }
50
156
  }
51
157
 
52
- if (result.status !== 0) {
53
- console.error('');
54
- console.error('โŒ Tests failed. Fix them before pushing.');
55
- console.error('');
56
- console.error('Fix the failing checks. Do not bypass hooks.');
57
- console.error('');
58
- process.exit(1);
158
+ if (require.main === module) {
159
+ process.exit(runPrePushTests());
59
160
  }
60
161
 
61
- console.log('โœ… All tests passed');
162
+ module.exports = {
163
+ classifyPushTests,
164
+ detectPackageManager,
165
+ runPrePushTests,
166
+ stripGitHookEnv,
167
+ };