@opengsd/gsd-core 1.3.1 → 1.4.0-rc.2

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 (136) hide show
  1. package/.claude-plugin/plugin.json +23 -0
  2. package/GEMINI.md +53 -0
  3. package/agents/gsd-advisor-researcher.md +1 -20
  4. package/agents/gsd-ai-researcher.md +2 -21
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-code-reviewer.md +1 -1
  7. package/agents/gsd-domain-researcher.md +2 -21
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-eval-planner.md +1 -1
  10. package/agents/gsd-executor.md +1 -1
  11. package/agents/gsd-framework-selector.md +1 -1
  12. package/agents/gsd-nyquist-auditor.md +1 -1
  13. package/agents/gsd-pattern-mapper.md +1 -1
  14. package/agents/gsd-phase-researcher.md +92 -166
  15. package/agents/gsd-planner.md +9 -36
  16. package/agents/gsd-project-researcher.md +62 -141
  17. package/agents/gsd-security-auditor.md +1 -1
  18. package/agents/gsd-ui-auditor.md +1 -1
  19. package/agents/gsd-ui-checker.md +1 -1
  20. package/agents/gsd-ui-researcher.md +3 -22
  21. package/agents/gsd-user-profiler.md +1 -1
  22. package/agents/gsd-verifier.md +8 -2
  23. package/bin/install.js +1977 -339
  24. package/commands/gsd/autonomous.md +2 -0
  25. package/commands/gsd/execute-phase.md +2 -0
  26. package/commands/gsd/graphify.md +11 -6
  27. package/commands/gsd/import.md +6 -2
  28. package/commands/gsd/plan-phase.md +4 -2
  29. package/commands/gsd/progress.md +1 -0
  30. package/commands/gsd/stats.md +1 -0
  31. package/commands/gsd/update.md +3 -2
  32. package/gemini-extension.json +6 -0
  33. package/gsd-core/bin/check-latest-version.cjs +61 -6
  34. package/gsd-core/bin/gsd-tools.cjs +238 -32
  35. package/gsd-core/bin/lib/check-command-router.cjs +1 -0
  36. package/gsd-core/bin/lib/cli-exit.cjs +42 -0
  37. package/gsd-core/bin/lib/command-routing-hub.cjs +1 -1
  38. package/gsd-core/bin/lib/commands.cjs +5 -4
  39. package/gsd-core/bin/lib/config.cjs +28 -4
  40. package/gsd-core/bin/lib/core.cjs +72 -28
  41. package/gsd-core/bin/lib/graphify.cjs +2 -2
  42. package/gsd-core/bin/lib/init-command-router.cjs +2 -2
  43. package/gsd-core/bin/lib/init.cjs +19 -3
  44. package/gsd-core/bin/lib/install-profiles.cjs +58 -0
  45. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  46. package/gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs +1 -1
  47. package/gsd-core/bin/lib/intel.cjs +3 -20
  48. package/gsd-core/bin/lib/package-legitimacy.cjs +368 -0
  49. package/gsd-core/bin/lib/phase.cjs +3 -3
  50. package/gsd-core/bin/lib/research-provider.cjs +137 -0
  51. package/gsd-core/bin/lib/research-store.cjs +167 -0
  52. package/gsd-core/bin/lib/roadmap-upgrade.cjs +4 -19
  53. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +67 -9
  54. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +56 -0
  55. package/gsd-core/bin/lib/runtime-homes.cjs +32 -11
  56. package/gsd-core/bin/lib/security.cjs +73 -0
  57. package/gsd-core/bin/lib/shell-command-projection.cjs +9 -0
  58. package/gsd-core/bin/lib/surface.cjs +54 -11
  59. package/gsd-core/bin/lib/validate.cjs +2 -2
  60. package/gsd-core/bin/lib/verification-command-router.cjs +31 -0
  61. package/gsd-core/bin/lib/verification.cjs +193 -0
  62. package/gsd-core/bin/lib/verify.cjs +2 -2
  63. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -1
  64. package/gsd-core/bin/lib/worktree-base-ref.cjs +325 -0
  65. package/gsd-core/bin/lib/worktree-safety.cjs +31 -0
  66. package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
  67. package/gsd-core/bin/verify-reapply-patches.cjs +8 -11
  68. package/gsd-core/references/planner-load-graph-context.md +36 -0
  69. package/gsd-core/references/planning-config.md +3 -1
  70. package/gsd-core/references/research-documentation-lookup.md +29 -0
  71. package/gsd-core/references/research-philosophy.md +29 -0
  72. package/gsd-core/references/research-verification-protocol.md +27 -0
  73. package/gsd-core/workflows/execute-phase.md +19 -8
  74. package/gsd-core/workflows/help/modes/full.md +4 -3
  75. package/gsd-core/workflows/ingest-docs.md +3 -2
  76. package/gsd-core/workflows/plan-phase.md +14 -10
  77. package/gsd-core/workflows/plan-review-convergence.md +3 -3
  78. package/gsd-core/workflows/review.md +24 -7
  79. package/gsd-core/workflows/ship.md +5 -8
  80. package/gsd-core/workflows/spec-phase.md +2 -1
  81. package/gsd-core/workflows/update.md +34 -6
  82. package/hooks/dist/gsd-config-reload.js +133 -0
  83. package/hooks/dist/gsd-context-monitor.js +1 -1
  84. package/hooks/dist/gsd-cursor-post-tool.js +75 -0
  85. package/hooks/dist/gsd-cursor-session-start.js +52 -0
  86. package/hooks/dist/gsd-workflow-guard.js +1 -0
  87. package/hooks/dist/gsd-worktree-path-guard.js +1 -1
  88. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  89. package/hooks/gsd-config-reload.js +133 -0
  90. package/hooks/gsd-context-monitor.js +1 -1
  91. package/hooks/gsd-cursor-post-tool.js +75 -0
  92. package/hooks/gsd-cursor-session-start.js +52 -0
  93. package/hooks/gsd-workflow-guard.js +1 -0
  94. package/hooks/gsd-worktree-path-guard.js +1 -1
  95. package/hooks/hooks.json +69 -0
  96. package/hooks/managed-hooks-registry.cjs +3 -0
  97. package/package.json +8 -1
  98. package/scripts/affected-tests-lib.cjs +3 -2
  99. package/scripts/build-hooks.js +7 -0
  100. package/scripts/changeset/cli.cjs +226 -28
  101. package/scripts/changeset/lint.cjs +5 -4
  102. package/scripts/changeset/new.cjs +4 -4
  103. package/scripts/check-alias-drift.cjs +77 -71
  104. package/scripts/check-env.cjs +185 -179
  105. package/scripts/check-npm-integrity.cjs +115 -109
  106. package/scripts/ci-guard-runner.cjs +11 -5
  107. package/scripts/ci-prepare-test-scope.cjs +27 -22
  108. package/scripts/ci-rebase-check.cjs +46 -45
  109. package/scripts/ci-test-scope.cjs +126 -22
  110. package/scripts/diff-touches-shipped-paths.cjs +52 -44
  111. package/scripts/gen-inventory-manifest.cjs +38 -32
  112. package/scripts/gen-research-agents.cjs +276 -0
  113. package/scripts/issue-dedupe.cjs +278 -0
  114. package/scripts/lib/cli-exit.cjs +56 -0
  115. package/scripts/lint-command-contract.cjs +28 -22
  116. package/scripts/lint-descriptions.cjs +32 -28
  117. package/scripts/lint-docs-required.cjs +4 -4
  118. package/scripts/lint-legacy-dir-name.cjs +56 -52
  119. package/scripts/lint-pr-check-project-dir.cjs +3 -1
  120. package/scripts/lint-shell-command-projection-drift.cjs +27 -22
  121. package/scripts/lint-skill-deps.cjs +31 -26
  122. package/scripts/lint-test-file-count.allowlist.json +2 -0
  123. package/scripts/lint-test-file-count.cjs +5 -4
  124. package/scripts/mutation-matrix.cjs +6 -3
  125. package/scripts/prompt-injection-scan.sh +1 -1
  126. package/scripts/release-notes/discord-release-summary.cjs +373 -0
  127. package/scripts/release-notes/format-github-release-notes.cjs +8 -3
  128. package/scripts/release-tarball-smoke.cjs +6 -4
  129. package/scripts/research-profiles.cjs +149 -0
  130. package/scripts/run-affected-tests.cjs +2 -1
  131. package/scripts/run-cross-platform-tests.cjs +11 -7
  132. package/scripts/run-tests.cjs +8 -7
  133. package/scripts/strip-prose-atrefs.cjs +1 -1
  134. package/scripts/sync-manifest-versions.cjs +119 -0
  135. package/scripts/sync-runtime-launcher.cjs +0 -3
  136. package/scripts/verify-npm-publish.cjs +14 -26
@@ -16,62 +16,7 @@
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
18
 
19
- // ---- Argument parsing -------------------------------------------------------
20
-
21
- let ignoreExtraneous = false;
22
-
23
- for (const arg of process.argv.slice(2)) {
24
- if (arg === '--ignore-extraneous') {
25
- ignoreExtraneous = true;
26
- } else if (arg === '--help' || arg === '-h') {
27
- process.stdout.write(
28
- 'Usage: node scripts/check-npm-integrity.cjs [--ignore-extraneous]\n'
29
- );
30
- process.exit(0);
31
- } else {
32
- process.stderr.write(`ERROR: Unknown argument: ${arg}\n`);
33
- process.exit(2);
34
- }
35
- }
36
-
37
- // ---- Locate lockfile --------------------------------------------------------
38
-
39
- const lockfilePath = path.join(process.cwd(), 'package-lock.json');
40
-
41
- if (!fs.existsSync(lockfilePath)) {
42
- process.stderr.write(`ERROR: package-lock.json not found in ${process.cwd()}\n`);
43
- process.exit(2);
44
- }
45
-
46
- // ---- Parse lockfile ---------------------------------------------------------
47
-
48
- let lock;
49
- try {
50
- lock = JSON.parse(fs.readFileSync(lockfilePath, 'utf-8'));
51
- } catch (e) {
52
- process.stderr.write(`ERROR: Failed to parse package-lock.json: ${e.message}\n`);
53
- process.exit(2);
54
- }
55
-
56
- const lockVersion = lock.lockfileVersion || 1;
57
- if (lockVersion < 2) {
58
- process.stderr.write(
59
- `ERROR: package-lock.json lockfileVersion ${lockVersion} is not supported. ` +
60
- 'Run `npm install` to upgrade to v3.\n'
61
- );
62
- process.exit(2);
63
- }
64
-
65
- const packages = lock.packages || {};
66
- const rootEntry = packages[''] || {};
67
-
68
- // Collect declared dependency ranges from root entry.
69
- const declaredRanges = {};
70
- for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
71
- for (const [name, range] of Object.entries(rootEntry[field] || {})) {
72
- if (!declaredRanges[name]) declaredRanges[name] = range;
73
- }
74
- }
19
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
75
20
 
76
21
  // ---- Minimal semver satisfies -----------------------------------------------
77
22
 
@@ -132,78 +77,139 @@ function satisfies(installed, range) {
132
77
  return installed === range;
133
78
  }
134
79
 
135
- // ---- Walk packages map ------------------------------------------------------
80
+ function main() {
81
+ // ---- Argument parsing -------------------------------------------------------
82
+
83
+ let ignoreExtraneous = false;
84
+
85
+ for (const arg of process.argv.slice(2)) {
86
+ if (arg === '--ignore-extraneous') {
87
+ ignoreExtraneous = true;
88
+ } else if (arg === '--help' || arg === '-h') {
89
+ process.stdout.write(
90
+ 'Usage: node scripts/check-npm-integrity.cjs [--ignore-extraneous]\n'
91
+ );
92
+ return 0;
93
+ } else {
94
+ process.stderr.write(`ERROR: Unknown argument: ${arg}\n`);
95
+ throw new ExitError(2);
96
+ }
97
+ }
136
98
 
137
- const invalids = [];
138
- const missings = [];
139
- const extraneousFound = [];
99
+ // ---- Locate lockfile --------------------------------------------------------
140
100
 
141
- for (const [key, entry] of Object.entries(packages)) {
142
- if (!key.startsWith('node_modules/')) continue;
143
- const rest = key.slice('node_modules/'.length);
144
- const isScoped = rest[0] === '@';
145
- const slashCount = (rest.match(/\//g) || []).length;
146
- if (isScoped && slashCount > 1) continue;
147
- if (!isScoped && slashCount > 0) continue;
101
+ const lockfilePath = path.join(process.cwd(), 'package-lock.json');
148
102
 
149
- const pkgName = rest;
150
- const installedVersion = entry.version || '';
103
+ if (!fs.existsSync(lockfilePath)) {
104
+ process.stderr.write(`ERROR: package-lock.json not found in ${process.cwd()}\n`);
105
+ throw new ExitError(2);
106
+ }
151
107
 
152
- if (entry.extraneous) {
153
- extraneousFound.push({ name: pkgName, version: installedVersion });
154
- continue;
108
+ // ---- Parse lockfile ---------------------------------------------------------
109
+
110
+ let lock;
111
+ try {
112
+ lock = JSON.parse(fs.readFileSync(lockfilePath, 'utf-8'));
113
+ } catch (e) {
114
+ process.stderr.write(`ERROR: Failed to parse package-lock.json: ${e.message}\n`);
115
+ throw new ExitError(2);
155
116
  }
156
117
 
157
- if (!declaredRanges[pkgName]) continue;
118
+ const lockVersion = lock.lockfileVersion || 1;
119
+ if (lockVersion < 2) {
120
+ process.stderr.write(
121
+ `ERROR: package-lock.json lockfileVersion ${lockVersion} is not supported. ` +
122
+ 'Run `npm install` to upgrade to v3.\n'
123
+ );
124
+ throw new ExitError(2);
125
+ }
158
126
 
159
- if (!satisfies(installedVersion, declaredRanges[pkgName])) {
160
- invalids.push({ name: pkgName, version: installedVersion, declared: declaredRanges[pkgName] });
127
+ const packages = lock.packages || {};
128
+ const rootEntry = packages[''] || {};
129
+
130
+ // Collect declared dependency ranges from root entry.
131
+ const declaredRanges = {};
132
+ for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
133
+ for (const [name, range] of Object.entries(rootEntry[field] || {})) {
134
+ if (!declaredRanges[name]) declaredRanges[name] = range;
135
+ }
161
136
  }
162
- }
163
137
 
164
- for (const name of Object.keys(declaredRanges)) {
165
- if (!packages[`node_modules/${name}`]) {
166
- missings.push({ name, required: declaredRanges[name] });
138
+ // ---- Walk packages map ------------------------------------------------------
139
+
140
+ const invalids = [];
141
+ const missings = [];
142
+ const extraneousFound = [];
143
+
144
+ for (const [key, entry] of Object.entries(packages)) {
145
+ if (!key.startsWith('node_modules/')) continue;
146
+ const rest = key.slice('node_modules/'.length);
147
+ const isScoped = rest[0] === '@';
148
+ const slashCount = (rest.match(/\//g) || []).length;
149
+ if (isScoped && slashCount > 1) continue;
150
+ if (!isScoped && slashCount > 0) continue;
151
+
152
+ const pkgName = rest;
153
+ const installedVersion = entry.version || '';
154
+
155
+ if (entry.extraneous) {
156
+ extraneousFound.push({ name: pkgName, version: installedVersion });
157
+ continue;
158
+ }
159
+
160
+ if (!declaredRanges[pkgName]) continue;
161
+
162
+ if (!satisfies(installedVersion, declaredRanges[pkgName])) {
163
+ invalids.push({ name: pkgName, version: installedVersion, declared: declaredRanges[pkgName] });
164
+ }
167
165
  }
168
- }
169
166
 
170
- // ---- Verdict ----------------------------------------------------------------
167
+ for (const name of Object.keys(declaredRanges)) {
168
+ if (!packages[`node_modules/${name}`]) {
169
+ missings.push({ name, required: declaredRanges[name] });
170
+ }
171
+ }
171
172
 
172
- const failInvalid = invalids.length > 0;
173
- const failMissing = missings.length > 0;
174
- const failExtra = !ignoreExtraneous && extraneousFound.length > 0;
173
+ // ---- Verdict ----------------------------------------------------------------
175
174
 
176
- if (!failInvalid && !failMissing && !failExtra) {
177
- process.stderr.write('check-npm-integrity.cjs: clean\n');
178
- process.exit(0);
179
- }
175
+ const failInvalid = invalids.length > 0;
176
+ const failMissing = missings.length > 0;
177
+ const failExtra = !ignoreExtraneous && extraneousFound.length > 0;
180
178
 
181
- const lines = ['FAIL: dependency integrity drift detected', ''];
179
+ if (!failInvalid && !failMissing && !failExtra) {
180
+ process.stderr.write('check-npm-integrity.cjs: clean\n');
181
+ return 0;
182
+ }
182
183
 
183
- if (failInvalid) {
184
- lines.push(' INVALID (installed version does not satisfy declared range):');
185
- for (const { name, declared, version } of invalids) {
186
- lines.push(` ${name}: declared=${declared} installed=${version}`);
184
+ const lines = ['FAIL: dependency integrity drift detected', ''];
185
+
186
+ if (failInvalid) {
187
+ lines.push(' INVALID (installed version does not satisfy declared range):');
188
+ for (const { name, declared, version } of invalids) {
189
+ lines.push(` ${name}: declared=${declared} installed=${version}`);
190
+ }
191
+ lines.push('');
187
192
  }
188
- lines.push('');
189
- }
190
193
 
191
- if (failMissing) {
192
- lines.push(' MISSING (declared but absent from lockfile packages map):');
193
- for (const { name, required } of missings) {
194
- lines.push(` ${name}@${required}`);
194
+ if (failMissing) {
195
+ lines.push(' MISSING (declared but absent from lockfile packages map):');
196
+ for (const { name, required } of missings) {
197
+ lines.push(` ${name}@${required}`);
198
+ }
199
+ lines.push('');
195
200
  }
196
- lines.push('');
197
- }
198
201
 
199
- if (failExtra) {
200
- lines.push(' EXTRANEOUS (in lockfile but not declared as a dependency):');
201
- for (const { name, version } of extraneousFound) {
202
- lines.push(` ${name}@${version}`);
202
+ if (failExtra) {
203
+ lines.push(' EXTRANEOUS (in lockfile but not declared as a dependency):');
204
+ for (const { name, version } of extraneousFound) {
205
+ lines.push(` ${name}@${version}`);
206
+ }
207
+ lines.push('');
203
208
  }
204
- lines.push('');
209
+
210
+ lines.push('Remediation: rm -rf node_modules && npm ci');
211
+ process.stderr.write(lines.join('\n') + '\n');
212
+ throw new ExitError(1);
205
213
  }
206
214
 
207
- lines.push('Remediation: rm -rf node_modules && npm ci');
208
- process.stderr.write(lines.join('\n') + '\n');
209
- process.exit(1);
215
+ runMain(main);
@@ -6,11 +6,17 @@
6
6
  // Exit 0 = github-hosted runner confirmed.
7
7
  // Exit 1 = not a github-hosted runner (emits GitHub Actions error annotation).
8
8
 
9
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
10
+
9
11
  const env = process.env.RUNNER_ENVIRONMENT || '';
10
12
 
11
- if (env !== 'github-hosted') {
12
- process.stderr.write(
13
- `::error::Expected github-hosted runner. RUNNER_ENVIRONMENT=${env || 'unset'}\n`
14
- );
15
- process.exit(1);
13
+ function main() {
14
+ if (env !== 'github-hosted') {
15
+ throw new ExitError(
16
+ 1,
17
+ `::error::Expected github-hosted runner. RUNNER_ENVIRONMENT=${env || 'unset'}`
18
+ );
19
+ }
16
20
  }
21
+
22
+ runMain(main);
@@ -14,33 +14,38 @@
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
16
 
17
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
18
+
17
19
  const scope = process.env.TEST_SCOPE || '';
18
20
  const targeted = process.env.TARGETED_TESTS || '';
19
21
  const windows = process.env.WINDOWS_TESTS || '';
20
22
 
21
23
  const FALLBACK = 'tests/command-contract.test.cjs tests/commands.test.cjs tests/core.test.cjs tests/package-manifest.test.cjs';
22
24
 
23
- let selected;
24
- if (scope === 'windows') {
25
- selected = windows;
26
- } else if (scope === 'targeted') {
27
- selected = targeted;
28
- } else {
29
- process.stderr.write(`::error::Unknown test scope: ${scope}\n`);
30
- process.exit(1);
31
- }
32
-
33
- // Trim and fall back to default set if empty.
34
- if (!selected.trim()) {
35
- selected = FALLBACK;
25
+ function main() {
26
+ let selected;
27
+ if (scope === 'windows') {
28
+ selected = windows;
29
+ } else if (scope === 'targeted') {
30
+ selected = targeted;
31
+ } else {
32
+ throw new ExitError(1, `::error::Unknown test scope: ${scope}`);
33
+ }
34
+
35
+ // Trim and fall back to default set if empty.
36
+ if (!selected.trim()) {
37
+ selected = FALLBACK;
38
+ }
39
+
40
+ // Split on whitespace, filter blanks, join with newlines.
41
+ const lines = selected.split(/\s+/).filter(Boolean);
42
+ const content = lines.join('\n') + '\n';
43
+
44
+ const outPath = path.join(process.cwd(), '.ci-selected-tests.txt');
45
+ fs.writeFileSync(outPath, content, 'utf-8');
46
+
47
+ process.stdout.write('Scoped tests:\n');
48
+ process.stdout.write(content);
36
49
  }
37
50
 
38
- // Split on whitespace, filter blanks, join with newlines.
39
- const lines = selected.split(/\s+/).filter(Boolean);
40
- const content = lines.join('\n') + '\n';
41
-
42
- const outPath = path.join(process.cwd(), '.ci-selected-tests.txt');
43
- fs.writeFileSync(outPath, content, 'utf-8');
44
-
45
- process.stdout.write('Scoped tests:\n');
46
- process.stdout.write(content);
51
+ runMain(main);
@@ -11,8 +11,9 @@
11
11
  // Exit 0 = merged cleanly (or merge was a no-op).
12
12
  // Exit 1 = merge conflict or fetch failure.
13
13
 
14
- const { execFileSync, execSync } = require('child_process');
15
- const path = require('path');
14
+ const { execFileSync } = require('child_process');
15
+
16
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
16
17
 
17
18
  function run(cmd, args, opts) {
18
19
  try {
@@ -27,8 +28,7 @@ function runOrThrow(cmd, args, label) {
27
28
  try {
28
29
  execFileSync(cmd, args, { stdio: 'inherit' });
29
30
  } catch (e) {
30
- process.stderr.write(`::error::${label} failed\n`);
31
- process.exit(1);
31
+ throw new ExitError(1, `::error::${label} failed`);
32
32
  }
33
33
  }
34
34
 
@@ -36,50 +36,51 @@ const token = process.env.GITHUB_TOKEN || '';
36
36
  const baseBranch = process.env.GITHUB_BASE_REF || 'main';
37
37
  const repo = process.env.GITHUB_REPOSITORY || '';
38
38
 
39
- // Configure git identity (needed for merge commit).
40
- runOrThrow('git', ['config', 'user.email', 'ci@gsd-redux'], 'git config user.email');
41
- runOrThrow('git', ['config', 'user.name', 'CI Rebase Check'], 'git config user.name');
42
-
43
- // Set authenticated remote URL.
44
- if (token && repo) {
45
- runOrThrow(
46
- 'git',
47
- ['remote', 'set-url', 'origin', `https://x-access-token:${token}@github.com/${repo}.git`],
48
- 'git remote set-url'
49
- );
50
- }
39
+ function main() {
40
+ // Configure git identity (needed for merge commit).
41
+ runOrThrow('git', ['config', 'user.email', 'ci@gsd-redux'], 'git config user.email');
42
+ runOrThrow('git', ['config', 'user.name', 'CI Rebase Check'], 'git config user.name');
51
43
 
52
- // Fetch base branch with retry.
53
- let fetched = false;
54
- for (let attempt = 1; attempt <= 3; attempt++) {
55
- const result = run('git', ['fetch', 'origin', baseBranch]);
56
- if (result) {
57
- fetched = true;
58
- break;
44
+ // Set authenticated remote URL.
45
+ if (token && repo) {
46
+ runOrThrow(
47
+ 'git',
48
+ ['remote', 'set-url', 'origin', `https://x-access-token:${token}@github.com/${repo}.git`],
49
+ 'git remote set-url'
50
+ );
59
51
  }
60
- if (attempt === 3) {
61
- process.stderr.write(`::error::git fetch origin ${baseBranch} failed after 3 attempts.\n`);
62
- process.exit(1);
52
+
53
+ // Fetch base branch with retry.
54
+ for (let attempt = 1; attempt <= 3; attempt++) {
55
+ const result = run('git', ['fetch', 'origin', baseBranch]);
56
+ if (result) {
57
+ break;
58
+ }
59
+ if (attempt === 3) {
60
+ throw new ExitError(1, `::error::git fetch origin ${baseBranch} failed after 3 attempts.`);
61
+ }
62
+ // Wait before retry: attempt * 4 seconds.
63
+ const waitMs = attempt * 4000;
64
+ const deadline = Date.now() + waitMs;
65
+ while (Date.now() < deadline) { /* busy wait, acceptable in CI */ }
63
66
  }
64
- // Wait before retry: attempt * 4 seconds.
65
- const waitMs = attempt * 4000;
66
- const deadline = Date.now() + waitMs;
67
- while (Date.now() < deadline) { /* busy wait, acceptable in CI */ }
68
- }
69
67
 
70
- // Attempt merge.
71
- try {
72
- execFileSync('git', ['merge', '--no-edit', '--no-ff', `origin/${baseBranch}`], { stdio: 'inherit' });
73
- } catch (e) {
74
- process.stderr.write(
75
- `::error::This PR cannot cleanly merge origin/${baseBranch}. Rebase your branch onto current ${baseBranch} and push again.\n`
76
- );
77
- process.stderr.write('::error::Conflicting files:\n');
68
+ // Attempt merge.
78
69
  try {
79
- execFileSync('git', ['diff', '--name-only', '--diff-filter=U'], { stdio: 'inherit' });
80
- } catch (_) { /* ignore */ }
81
- try {
82
- execFileSync('git', ['merge', '--abort'], { stdio: 'inherit' });
83
- } catch (_) { /* ignore */ }
84
- process.exit(1);
70
+ execFileSync('git', ['merge', '--no-edit', '--no-ff', `origin/${baseBranch}`], { stdio: 'inherit' });
71
+ } catch (e) {
72
+ process.stderr.write(
73
+ `::error::This PR cannot cleanly merge origin/${baseBranch}. Rebase your branch onto current ${baseBranch} and push again.\n`
74
+ );
75
+ process.stderr.write('::error::Conflicting files:\n');
76
+ try {
77
+ execFileSync('git', ['diff', '--name-only', '--diff-filter=U'], { stdio: 'inherit' });
78
+ } catch (_) { /* ignore */ }
79
+ try {
80
+ execFileSync('git', ['merge', '--abort'], { stdio: 'inherit' });
81
+ } catch (_) { /* ignore */ }
82
+ throw new ExitError(1);
83
+ }
85
84
  }
85
+
86
+ runMain(main);