whitesmith 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +286 -88
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +90 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/comment.d.ts.map +1 -1
  6. package/dist/comment.js +18 -11
  7. package/dist/comment.js.map +1 -1
  8. package/dist/git.d.ts +5 -3
  9. package/dist/git.d.ts.map +1 -1
  10. package/dist/git.js +20 -29
  11. package/dist/git.js.map +1 -1
  12. package/dist/harnesses/pi.d.ts.map +1 -1
  13. package/dist/harnesses/pi.js +22 -6
  14. package/dist/harnesses/pi.js.map +1 -1
  15. package/dist/index.d.ts +3 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/orchestrator.d.ts +31 -3
  20. package/dist/orchestrator.d.ts.map +1 -1
  21. package/dist/orchestrator.js +214 -10
  22. package/dist/orchestrator.js.map +1 -1
  23. package/dist/prompts.d.ts +52 -0
  24. package/dist/prompts.d.ts.map +1 -1
  25. package/dist/prompts.js +197 -0
  26. package/dist/prompts.js.map +1 -1
  27. package/dist/providers/github-ci.d.ts +40 -0
  28. package/dist/providers/github-ci.d.ts.map +1 -1
  29. package/dist/providers/github-ci.js +463 -213
  30. package/dist/providers/github-ci.js.map +1 -1
  31. package/dist/providers/index.d.ts +1 -1
  32. package/dist/providers/index.d.ts.map +1 -1
  33. package/dist/review.d.ts +48 -0
  34. package/dist/review.d.ts.map +1 -0
  35. package/dist/review.js +221 -0
  36. package/dist/review.js.map +1 -0
  37. package/dist/types.d.ts +4 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/cli.ts +116 -3
  41. package/src/comment.ts +20 -14
  42. package/src/git.ts +23 -30
  43. package/src/harnesses/pi.ts +27 -6
  44. package/src/index.ts +9 -1
  45. package/src/orchestrator.ts +253 -14
  46. package/src/prompts.ts +239 -0
  47. package/src/providers/github-ci.ts +513 -217
  48. package/src/providers/index.ts +1 -1
  49. package/src/review.ts +290 -0
  50. package/src/types.ts +4 -0
@@ -127,19 +127,23 @@ async function promptDefaults(providers) {
127
127
  return { provider, model };
128
128
  }
129
129
  /**
130
- * Prompt for API key values and set them as GitHub secrets.
131
- * Returns the list of secrets that were set.
130
+ * Set API key secrets on GitHub. If `knownSecrets` contains a value for an
131
+ * env var, it is used directly; otherwise the user is prompted interactively.
132
+ * Returns the list of secret names that were successfully set.
132
133
  */
133
- async function promptAndSetSecrets(ctx, providers) {
134
+ async function setOrPromptSecrets(ctx, providers, knownSecrets) {
134
135
  const setSecrets = [];
135
136
  const seen = new Set();
136
137
  for (const p of providers) {
137
138
  if (seen.has(p.apiKeyEnvVar))
138
139
  continue;
139
140
  seen.add(p.apiKeyEnvVar);
140
- const apiKey = await password({
141
- message: `Enter API key for ${p.name} (secret: ${p.apiKeyEnvVar}):`,
142
- });
141
+ let apiKey = knownSecrets?.[p.apiKeyEnvVar];
142
+ if (!apiKey) {
143
+ apiKey = await password({
144
+ message: `Enter API key for ${p.name} (secret: ${p.apiKeyEnvVar}):`,
145
+ });
146
+ }
143
147
  if (!apiKey) {
144
148
  console.log(` ⚠ Skipped ${p.apiKeyEnvVar} (empty)`);
145
149
  continue;
@@ -189,86 +193,157 @@ function indent(text, spaces) {
189
193
  .map((line) => (line.trim() === '' ? '' : pad + line))
190
194
  .join('\n');
191
195
  }
192
- function generateModelsJsonStep(config) {
193
- const modelsJson = buildModelsJson(config.providers);
194
- const modelsJsonStr = JSON.stringify(modelsJson, null, 2);
195
- return `\
196
- - name: Configure pi models
197
- run: |
198
- mkdir -p ~/.pi/agent
199
- cat > ~/.pi/agent/models.json << 'MODELS_EOF'
200
- ${indent(modelsJsonStr, 10)}
201
- MODELS_EOF`;
202
- }
203
- function generateAuthJsonSteps() {
204
- return `\
205
- - name: Configure pi auth
206
- env:
207
- PI_AUTH_JSON: \${{ secrets.PI_AUTH_JSON }}
208
- run: |
209
- if [ -z "$PI_AUTH_JSON" ]; then
210
- echo "ERROR: PI_AUTH_JSON secret is not configured."
211
- echo "Set it to the contents of ~/.pi/agent/auth.json"
212
- exit 1
213
- fi
214
- mkdir -p ~/.pi/agent
215
- echo "$PI_AUTH_JSON" > ~/.pi/agent/auth.json
216
- chmod 600 ~/.pi/agent/auth.json
217
-
218
- # Workaround for https://github.com/badlogic/pi-mono/issues/2743
219
- - name: Refresh OAuth token
220
- env:
221
- GH_PAT: \${{ secrets.GH_PAT }}
222
- run: node .github/scripts/refresh-oauth-token.mjs`;
223
- }
224
- function generateAuthSteps(config) {
225
- if (config.authMode === 'auth-json') {
226
- return generateAuthJsonSteps();
227
- }
228
- return generateModelsJsonStep(config);
229
- }
230
- function generateRunEnvBlock(config) {
231
- const envs = {
232
- GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}',
233
- };
196
+ /**
197
+ * Top-level env block shared by whitesmith.yml and whitesmith-comment.yml.
198
+ * Includes defaults, GH_TOKEN, and API key secrets.
199
+ */
200
+ function generateTopLevelEnv(config) {
201
+ const lines = [
202
+ ` WHITESMITH_PROVIDER: ${config.defaultProvider}`,
203
+ ` WHITESMITH_MODEL: ${config.defaultModel}`,
204
+ ` GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}`,
205
+ ];
234
206
  if (config.authMode === 'models-json') {
207
+ const seen = new Set();
235
208
  for (const p of config.providers) {
236
- envs[p.apiKeyEnvVar] = `\${{ secrets.${p.apiKeyEnvVar} }}`;
209
+ if (seen.has(p.apiKeyEnvVar))
210
+ continue;
211
+ seen.add(p.apiKeyEnvVar);
212
+ lines.push(` ${p.apiKeyEnvVar}: \${{ secrets.${p.apiKeyEnvVar} }}`);
237
213
  }
238
214
  }
239
- return Object.entries(envs)
240
- .map(([k, v]) => ` ${k}: ${v}`)
241
- .join('\n');
215
+ return lines.join('\n');
216
+ }
217
+ /**
218
+ * Composite action: node setup, git config, npm cache, install, auth config.
219
+ * This is written to .github/actions/setup-whitesmith/action.yml so workflows
220
+ * can just do `uses: ./.github/actions/setup-whitesmith`.
221
+ */
222
+ function generateSetupAction(config) {
223
+ let authStep;
224
+ if (config.authMode === 'auth-json') {
225
+ authStep = `\
226
+ - name: Configure pi auth
227
+ shell: bash
228
+ run: |
229
+ if [ -z "$PI_AUTH_JSON" ]; then
230
+ echo "ERROR: PI_AUTH_JSON secret is not set" >&2; exit 1
231
+ fi
232
+ mkdir -p ~/.pi/agent
233
+ echo "$PI_AUTH_JSON" > ~/.pi/agent/auth.json
234
+ chmod 600 ~/.pi/agent/auth.json
235
+
236
+ # Workaround for https://github.com/badlogic/pi-mono/issues/2743
237
+ - name: Refresh OAuth token
238
+ shell: bash
239
+ run: node .github/scripts/refresh-oauth-token.mjs`;
240
+ }
241
+ else {
242
+ const modelsJson = buildModelsJson(config.providers);
243
+ const modelsJsonStr = JSON.stringify(modelsJson, null, 2);
244
+ authStep = `\
245
+ - name: Configure pi models
246
+ shell: bash
247
+ run: |
248
+ mkdir -p ~/.pi/agent
249
+ cat > ~/.pi/agent/models.json << 'MODELS_EOF'
250
+ ${indent(modelsJsonStr, 8)}
251
+ MODELS_EOF`;
252
+ }
253
+ let installSteps;
254
+ if (config.dev) {
255
+ // Dev mode: build whitesmith from source using pnpm.
256
+ // We add pnpm's global bin to $GITHUB_PATH so that `whitesmith` and `pi`
257
+ // are available in all subsequent steps (persists across composite action
258
+ // steps and the calling workflow).
259
+ // We always rebuild (even on cache hit) because source changes per commit.
260
+ installSteps = `\
261
+ - name: Setup pnpm
262
+ uses: pnpm/action-setup@v4
263
+
264
+ - name: Add pnpm global bin to PATH
265
+ shell: bash
266
+ run: |
267
+ pnpm setup
268
+ echo "$HOME/.local/share/pnpm" >> "$GITHUB_PATH"
269
+
270
+ - name: Install dependencies and build whitesmith
271
+ shell: bash
272
+ run: |
273
+ pnpm install
274
+ pnpm run build
275
+ pnpm link --global
276
+
277
+ - name: Install pi
278
+ shell: bash
279
+ run: pnpm add -g @mariozechner/pi-coding-agent`;
280
+ }
281
+ else {
282
+ installSteps = `\
283
+ - name: Get npm global prefix
284
+ id: npm-prefix
285
+ shell: bash
286
+ run: echo "dir=$(npm prefix -g)" >> "$GITHUB_OUTPUT"
287
+
288
+ - name: Cache npm packages
289
+ id: npm-cache
290
+ uses: actions/cache@v4
291
+ with:
292
+ path: \${{ steps.npm-prefix.outputs.dir }}
293
+ key: whitesmith-\${{ runner.os }}-${config.version}
294
+
295
+ - name: Install whitesmith and pi
296
+ if: steps.npm-cache.outputs.cache-hit != 'true'
297
+ shell: bash
298
+ run: npm install -g whitesmith@${config.version} @mariozechner/pi-coding-agent`;
299
+ }
300
+ return `\
301
+ name: Setup whitesmith
302
+ description: Install Node.js, whitesmith, pi, and configure AI provider auth
303
+
304
+ runs:
305
+ using: composite
306
+ steps:
307
+ - name: Setup Node.js
308
+ uses: actions/setup-node@v4
309
+ with:
310
+ node-version: '22'
311
+
312
+ - name: Configure git
313
+ shell: bash
314
+ run: |
315
+ git config user.name "whitesmith[bot]"
316
+ git config user.email "whitesmith[bot]@users.noreply.github.com"
317
+
318
+ ${installSteps}
319
+
320
+ ${authStep}
321
+ `;
242
322
  }
243
323
  function generateMainWorkflow(config) {
244
- const authSteps = generateAuthSteps(config);
245
- const envBlock = generateRunEnvBlock(config);
324
+ const envBlock = generateTopLevelEnv(config);
246
325
  return `\
247
- # NOTE: This workflow requires the repo setting:
248
- # Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests"
249
- # Without this, PR creation will fail with a permissions error.
326
+ # Requires: Settings Actions General → "Allow GitHub Actions to create and approve pull requests"
250
327
  name: whitesmith
251
328
 
252
329
  on:
253
- schedule:
254
- - cron: '*/15 * * * *'
255
330
  workflow_dispatch:
256
331
  inputs:
332
+ issue:
333
+ description: 'Issue number to target (leave empty for global scan)'
257
334
  max_iterations:
258
335
  description: 'Maximum iterations'
259
336
  default: '3'
260
- type: string
261
337
  provider:
262
- description: 'AI provider (e.g. ${config.defaultProvider})'
263
- required: false
264
- type: string
338
+ description: 'AI provider (overrides WHITESMITH_PROVIDER)'
265
339
  model:
266
- description: 'AI model ID (e.g. ${config.defaultModel})'
267
- required: false
268
- type: string
340
+ description: 'AI model (overrides WHITESMITH_MODEL)'
341
+
342
+ env:
343
+ ${envBlock}
269
344
 
270
345
  concurrency:
271
- group: whitesmith-loop
346
+ group: \${{ inputs.issue && format('whitesmith-issue-{0}', inputs.issue) || 'whitesmith-global' }}
272
347
  cancel-in-progress: false
273
348
 
274
349
  permissions:
@@ -283,62 +358,34 @@ jobs:
283
358
  - uses: actions/checkout@v4
284
359
  with:
285
360
  fetch-depth: 0
286
- token: \${{ secrets.GITHUB_TOKEN }}
287
-
288
- - name: Setup Node.js
289
- uses: actions/setup-node@v4
290
- with:
291
- node-version: '22'
292
-
293
- - name: Configure git
294
- run: |
295
- git config user.name "whitesmith[bot]"
296
- git config user.email "whitesmith[bot]@users.noreply.github.com"
297
-
298
- - name: Get npm global prefix
299
- id: npm-prefix
300
- run: echo "dir=$(npm prefix -g)" >> "$GITHUB_OUTPUT"
301
-
302
- - name: Cache global npm packages
303
- id: npm-cache
304
- uses: actions/cache@v4
305
- with:
306
- path: \${{ steps.npm-prefix.outputs.dir }}
307
- key: npm-global-\${{ runner.os }}-pi-v1
308
361
 
309
- - name: Install whitesmith and pi
310
- if: steps.npm-cache.outputs.cache-hit != 'true'
311
- run: |
312
- npm install -g whitesmith
313
- npm install -g @mariozechner/pi-coding-agent
314
-
315
- ${authSteps}
362
+ - uses: ./.github/actions/setup-whitesmith
316
363
 
317
- - name: Run whitesmith
318
- env:
319
- ${envBlock}
320
- run: |
321
- PROVIDER="\${{ inputs.provider || '${config.defaultProvider}' }}"
322
- MODEL="\${{ inputs.model || '${config.defaultModel}' }}"
364
+ - run: |
365
+ ISSUE_FLAG=""
366
+ if [ -n "\${{ inputs.issue }}" ]; then
367
+ ISSUE_FLAG="--issue \${{ inputs.issue }}"
368
+ fi
323
369
  whitesmith run . \\
324
- --agent-cmd "pi" \\
325
- --provider "$PROVIDER" \\
326
- --model "$MODEL" \\
370
+ \$ISSUE_FLAG \\
371
+ --provider "\${{ inputs.provider || env.WHITESMITH_PROVIDER }}" \\
372
+ --model "\${{ inputs.model || env.WHITESMITH_MODEL }}" \\
327
373
  --max-iterations \${{ inputs.max_iterations || '3' }}
328
374
  `;
329
375
  }
330
376
  function generateCommentWorkflow(config) {
331
- const authSteps = generateAuthSteps(config);
332
- const envBlock = generateRunEnvBlock(config);
377
+ const envBlock = generateTopLevelEnv(config);
333
378
  return `\
334
- # NOTE: This workflow requires the repo setting:
335
- # Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests"
379
+ # Requires: Settings Actions General → "Allow GitHub Actions to create and approve pull requests"
336
380
  name: whitesmith-comment
337
381
 
338
382
  on:
339
383
  issue_comment:
340
384
  types: [created]
341
385
 
386
+ env:
387
+ ${envBlock}
388
+
342
389
  concurrency:
343
390
  group: whitesmith-comment-\${{ github.event.issue.number }}
344
391
  cancel-in-progress: false
@@ -354,119 +401,196 @@ jobs:
354
401
  outputs:
355
402
  should_run: \${{ steps.check.outputs.should_run }}
356
403
  steps:
357
- - name: Check if should run
358
- id: check
404
+ - id: check
359
405
  env:
360
- GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
361
406
  COMMENT_BODY: \${{ github.event.comment.body }}
362
407
  run: |
363
- # Always run if comment contains /whitesmith
364
408
  if echo "$COMMENT_BODY" | grep -q '/whitesmith'; then
365
409
  echo "should_run=true" >> "$GITHUB_OUTPUT"
366
- echo "Triggered by /whitesmith keyword"
367
410
  exit 0
368
411
  fi
369
-
370
- # For PR comments, auto-trigger if the PR is on a whitesmith branch
371
412
  if [ -n "\${{ github.event.issue.pull_request.url }}" ]; then
372
413
  BRANCH=$(gh pr view \${{ github.event.issue.number }} \\
373
- --repo \${{ github.repository }} \\
374
- --json headRefName -q .headRefName)
375
- echo "PR branch: $BRANCH"
376
- if echo "$BRANCH" | grep -qE '^(investigate|task)/'; then
414
+ --repo \${{ github.repository }} --json headRefName -q .headRefName)
415
+ if echo "$BRANCH" | grep -qE '^(investigate|issue)/'; then
377
416
  echo "should_run=true" >> "$GITHUB_OUTPUT"
378
- echo "Triggered by comment on whitesmith PR branch"
379
417
  exit 0
380
418
  fi
381
419
  fi
382
-
383
420
  echo "should_run=false" >> "$GITHUB_OUTPUT"
384
- echo "Skipping: not a /whitesmith command and not a whitesmith PR"
385
421
 
386
422
  run:
387
423
  needs: check
388
- runs-on: ubuntu-latest
389
424
  if: needs.check.outputs.should_run == 'true'
425
+ runs-on: ubuntu-latest
390
426
  steps:
391
- - name: React with eyes to acknowledge
392
- env:
393
- GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
394
- run: |
427
+ - run: |
395
428
  gh api repos/\${{ github.repository }}/issues/comments/\${{ github.event.comment.id }}/reactions \\
396
429
  -f content=eyes
397
430
 
398
431
  - uses: actions/checkout@v4
399
432
  with:
400
433
  fetch-depth: 0
401
- token: \${{ secrets.GITHUB_TOKEN }}
402
-
403
- - name: Setup Node.js
404
- uses: actions/setup-node@v4
405
- with:
406
- node-version: '22'
407
-
408
- - name: Configure git
409
- run: |
410
- git config user.name "whitesmith[bot]"
411
- git config user.email "whitesmith[bot]@users.noreply.github.com"
412
434
 
413
- - name: Get npm global prefix
414
- id: npm-prefix
415
- run: echo "dir=$(npm prefix -g)" >> "$GITHUB_OUTPUT"
435
+ - uses: ./.github/actions/setup-whitesmith
416
436
 
417
- - name: Cache global npm packages
418
- id: npm-cache
419
- uses: actions/cache@v4
420
- with:
421
- path: \${{ steps.npm-prefix.outputs.dir }}
422
- key: npm-global-\${{ runner.os }}-pi-v1
423
-
424
- - name: Install whitesmith and pi
425
- if: steps.npm-cache.outputs.cache-hit != 'true'
426
- run: |
427
- npm install -g whitesmith
428
- npm install -g @mariozechner/pi-coding-agent
429
-
430
- ${authSteps}
431
-
432
- - name: Save comment body to file
433
- env:
437
+ - env:
434
438
  COMMENT_BODY: \${{ github.event.comment.body }}
435
439
  run: |
436
440
  printf '%s' "$COMMENT_BODY" > .whitesmith-comment-body.txt
437
-
438
- - name: Run whitesmith comment
439
- env:
440
- ${envBlock}
441
- run: |
442
441
  whitesmith comment . \\
443
442
  --number "\${{ github.event.issue.number }}" \\
444
443
  --body-file .whitesmith-comment-body.txt \\
445
- --provider "${config.defaultProvider}" \\
446
- --model "${config.defaultModel}" \\
444
+ --provider "$WHITESMITH_PROVIDER" \\
445
+ --model "$WHITESMITH_MODEL" \\
447
446
  --post
448
447
 
449
- - name: React with checkmark on success
450
- if: success()
451
- env:
452
- GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
448
+ - if: success()
453
449
  run: |
454
450
  gh api repos/\${{ github.repository }}/issues/comments/\${{ github.event.comment.id }}/reactions \\
455
451
  -f content="+1"
456
452
 
457
- - name: React with X and comment on failure
458
- if: failure()
459
- env:
460
- GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
453
+ - if: failure()
461
454
  run: |
462
455
  gh api repos/\${{ github.repository }}/issues/comments/\${{ github.event.comment.id }}/reactions \\
463
456
  -f content="-1"
464
- gh issue comment \${{ github.event.issue.number }} \\
465
- --repo \${{ github.repository }} \\
466
- --body "❌ Agent run failed for [this comment](\${{ github.event.comment.html_url }}). Check the [workflow run](\${{ github.server_url }}/\${{ github.repository }}/actions/runs/\${{ github.run_id }}) for details."
457
+ gh issue comment \${{ github.event.issue.number }} --repo \${{ github.repository }} \\
458
+ --body "❌ Agent run failed. See [workflow run](\${{ github.server_url }}/\${{ github.repository }}/actions/runs/\${{ github.run_id }})."
467
459
  `;
468
460
  }
469
- function generateReconcileWorkflow() {
461
+ function generateReviewWorkflow(config) {
462
+ const envBlock = generateTopLevelEnv(config);
463
+ // When the review step is enabled in the main loop, whitesmith PRs are
464
+ // already reviewed inline. The workflow should only review non-whitesmith PRs.
465
+ // When the review step is disabled, the workflow reviews ALL PRs.
466
+ const skipWhitesmithCheck = config.reviewStepEnabled
467
+ ? `\
468
+ check:
469
+ if: github.event_name == 'pull_request'
470
+ runs-on: ubuntu-latest
471
+ outputs:
472
+ should_run: \${{ steps.check.outputs.should_run }}
473
+ steps:
474
+ - id: check
475
+ run: |
476
+ BRANCH="\${{ github.event.pull_request.head.ref }}"
477
+ if echo "$BRANCH" | grep -qE '^(investigate|issue)/'; then
478
+ echo "Skipping review for whitesmith-managed branch: $BRANCH"
479
+ echo "should_run=false" >> "$GITHUB_OUTPUT"
480
+ else
481
+ echo "should_run=true" >> "$GITHUB_OUTPUT"
482
+ fi
483
+
484
+ review:
485
+ needs: check
486
+ if: >-
487
+ (github.event_name == 'workflow_dispatch') ||
488
+ (needs.check.outputs.should_run == 'true')`
489
+ : `\
490
+ review:`;
491
+ return `\
492
+ name: whitesmith-review
493
+
494
+ on:
495
+ pull_request:
496
+ types: [opened, synchronize]
497
+ workflow_dispatch:
498
+ inputs:
499
+ number:
500
+ description: 'PR or issue number to review'
501
+ required: true
502
+ type:
503
+ description: 'Review type (auto-detected if empty): pr, issue-tasks, issue-tasks-completed'
504
+ provider:
505
+ description: 'AI provider (overrides WHITESMITH_PROVIDER)'
506
+ model:
507
+ description: 'AI model (overrides WHITESMITH_MODEL)'
508
+
509
+ env:
510
+ ${envBlock}
511
+
512
+ concurrency:
513
+ group: whitesmith-review-\${{ github.event.pull_request.number || inputs.number }}
514
+ cancel-in-progress: true
515
+
516
+ permissions:
517
+ contents: read
518
+ issues: write
519
+ pull-requests: write
520
+
521
+ jobs:
522
+ ${skipWhitesmithCheck}
523
+ runs-on: ubuntu-latest
524
+ steps:
525
+ - uses: actions/checkout@v4
526
+ with:
527
+ fetch-depth: 0
528
+
529
+ - uses: ./.github/actions/setup-whitesmith
530
+
531
+ - if: github.event_name == 'pull_request'
532
+ run: |
533
+ whitesmith review . \\
534
+ --number "\${{ github.event.pull_request.number }}" \\
535
+ --provider "\${{ env.WHITESMITH_PROVIDER }}" \\
536
+ --model "\${{ env.WHITESMITH_MODEL }}" \\
537
+ --post
538
+
539
+ - if: github.event_name == 'workflow_dispatch'
540
+ run: |
541
+ TYPE_FLAG=""
542
+ if [ -n "\${{ inputs.type }}" ]; then
543
+ TYPE_FLAG="--type \${{ inputs.type }}"
544
+ fi
545
+ whitesmith review . \\
546
+ --number "\${{ inputs.number }}" \\
547
+ \$TYPE_FLAG \\
548
+ --provider "\${{ inputs.provider || env.WHITESMITH_PROVIDER }}" \\
549
+ --model "\${{ inputs.model || env.WHITESMITH_MODEL }}" \\
550
+ --post
551
+ `;
552
+ }
553
+ function generateIssueWorkflow(config) {
554
+ const envBlock = generateTopLevelEnv(config);
555
+ return `\
556
+ name: whitesmith-issue
557
+
558
+ on:
559
+ issues:
560
+ types: [opened]
561
+
562
+ env:
563
+ ${envBlock}
564
+
565
+ concurrency:
566
+ group: whitesmith-issue-\${{ github.event.issue.number }}
567
+ cancel-in-progress: false
568
+
569
+ permissions:
570
+ contents: write
571
+ issues: write
572
+ pull-requests: write
573
+
574
+ jobs:
575
+ run:
576
+ runs-on: ubuntu-latest
577
+ steps:
578
+ - uses: actions/checkout@v4
579
+ with:
580
+ fetch-depth: 0
581
+
582
+ - uses: ./.github/actions/setup-whitesmith
583
+
584
+ - run: |
585
+ whitesmith run . \\
586
+ --issue "\${{ github.event.issue.number }}" \\
587
+ --provider "$WHITESMITH_PROVIDER" \\
588
+ --model "$WHITESMITH_MODEL" \\
589
+ --max-iterations 10
590
+ `;
591
+ }
592
+ function generateReconcileWorkflow(config) {
593
+ const envBlock = generateTopLevelEnv(config);
470
594
  return `\
471
595
  name: whitesmith-reconcile
472
596
 
@@ -475,42 +599,71 @@ on:
475
599
  types: [closed]
476
600
  branches: [main]
477
601
 
602
+ env:
603
+ ${envBlock}
604
+
478
605
  permissions:
479
- contents: read
606
+ contents: write
480
607
  issues: write
481
- pull-requests: read
608
+ pull-requests: write
482
609
 
483
610
  jobs:
484
- reconcile:
611
+ parse:
485
612
  if: github.event.pull_request.merged == true
486
613
  runs-on: ubuntu-latest
614
+ outputs:
615
+ issue_number: \${{ steps.parse.outputs.issue_number }}
616
+ branch_type: \${{ steps.parse.outputs.branch_type }}
487
617
  steps:
488
- - uses: actions/checkout@v4
618
+ - id: parse
619
+ run: |
620
+ BRANCH="\${{ github.event.pull_request.head.ref }}"
621
+ INVESTIGATE_NUM=$(echo "$BRANCH" | sed -n 's|^investigate/\\([0-9]*\\)$|\\1|p')
622
+ ISSUE_NUM=$(echo "$BRANCH" | sed -n 's|^issue/\\([0-9]*\\)$|\\1|p')
623
+ if [ -n "$INVESTIGATE_NUM" ]; then
624
+ echo "issue_number=$INVESTIGATE_NUM" >> "$GITHUB_OUTPUT"
625
+ echo "branch_type=investigate" >> "$GITHUB_OUTPUT"
626
+ elif [ -n "$ISSUE_NUM" ]; then
627
+ echo "issue_number=$ISSUE_NUM" >> "$GITHUB_OUTPUT"
628
+ echo "branch_type=issue" >> "$GITHUB_OUTPUT"
629
+ else
630
+ echo "branch_type=other" >> "$GITHUB_OUTPUT"
631
+ fi
489
632
 
490
- - name: Setup Node.js
491
- uses: actions/setup-node@v4
633
+ implement:
634
+ needs: parse
635
+ if: needs.parse.outputs.branch_type == 'investigate'
636
+ runs-on: ubuntu-latest
637
+ concurrency:
638
+ group: whitesmith-issue-\${{ needs.parse.outputs.issue_number }}
639
+ cancel-in-progress: false
640
+ steps:
641
+ - uses: actions/checkout@v4
492
642
  with:
493
- node-version: '22'
643
+ fetch-depth: 0
494
644
 
495
- - name: Get npm global prefix
496
- id: npm-prefix
497
- run: echo "dir=$(npm prefix -g)" >> "$GITHUB_OUTPUT"
645
+ - uses: ./.github/actions/setup-whitesmith
498
646
 
499
- - name: Cache global npm packages
500
- id: npm-cache
501
- uses: actions/cache@v4
502
- with:
503
- path: \${{ steps.npm-prefix.outputs.dir }}
504
- key: npm-global-\${{ runner.os }}-whitesmith-v1
647
+ - run: |
648
+ whitesmith run . \\
649
+ --issue "\${{ needs.parse.outputs.issue_number }}" \\
650
+ --provider "$WHITESMITH_PROVIDER" \\
651
+ --model "$WHITESMITH_MODEL" \\
652
+ --max-iterations 10
653
+
654
+ reconcile:
655
+ needs: parse
656
+ if: needs.parse.outputs.branch_type != 'investigate'
657
+ runs-on: ubuntu-latest
658
+ concurrency:
659
+ group: \${{ (needs.parse.outputs.issue_number && format('whitesmith-issue-{0}', needs.parse.outputs.issue_number)) || 'whitesmith-reconcile-other' }}
660
+ cancel-in-progress: false
661
+ steps:
662
+ - uses: actions/checkout@v4
505
663
 
506
- - name: Install whitesmith
507
- if: steps.npm-cache.outputs.cache-hit != 'true'
508
- run: npm install -g whitesmith
664
+ - uses: ./.github/actions/setup-whitesmith
509
665
 
510
- - name: Reconcile
511
- env:
512
- GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
513
- run: whitesmith reconcile .
666
+ - run: whitesmith reconcile .
514
667
  `;
515
668
  }
516
669
  // ─── Refresh OAuth Script (auth-json mode only) ─────────────────────────────
@@ -608,30 +761,58 @@ if (repo && token) {
608
761
  console.log("Skipping secret update (no GH_PAT or GITHUB_REPOSITORY)");
609
762
  }
610
763
  `;
764
+ /**
765
+ * Load config from a JSON file, skipping interactive prompts.
766
+ */
767
+ function loadConfigFile(filePath) {
768
+ const raw = fs.readFileSync(filePath, 'utf-8');
769
+ const data = JSON.parse(raw);
770
+ if (!data.providers || !Array.isArray(data.providers) || data.providers.length === 0) {
771
+ throw new Error(`Config file must contain a non-empty "providers" array`);
772
+ }
773
+ if (!data.defaultProvider) {
774
+ throw new Error(`Config file must contain "defaultProvider"`);
775
+ }
776
+ if (!data.defaultModel) {
777
+ throw new Error(`Config file must contain "defaultModel"`);
778
+ }
779
+ return data;
780
+ }
611
781
  export async function installGitHubCI(ctx, options) {
612
782
  const { authMode } = options;
783
+ const fake = options.fake ?? false;
784
+ const exportConfig = options.exportConfig ?? undefined;
613
785
  console.log('=== whitesmith install-ci (GitHub) ===\n');
614
786
  console.log(`Auth mode: ${authMode}\n`);
615
787
  let repo = ctx.repo;
616
- if (!repo && authMode === 'models-json' && ctx.ghAvailable) {
788
+ if (!exportConfig && !fake && !repo && authMode === 'models-json' && ctx.ghAvailable) {
617
789
  repo = await input({
618
790
  message: 'GitHub repository (owner/repo) — needed to set secrets:',
619
791
  });
620
792
  ctx.repo = repo;
621
793
  }
622
- let providers = [];
794
+ let providers;
623
795
  let defaultProvider;
624
796
  let defaultModel;
625
- if (authMode === 'models-json') {
626
- // Configure providers interactively
797
+ let loadedSecrets;
798
+ if (options.configFile) {
799
+ // Load from file — no prompts
800
+ const loaded = loadConfigFile(options.configFile);
801
+ providers = loaded.providers;
802
+ defaultProvider = loaded.defaultProvider;
803
+ defaultModel = loaded.defaultModel;
804
+ loadedSecrets = loaded.secrets;
805
+ }
806
+ else if (authMode === 'models-json') {
807
+ // Interactive prompts
627
808
  providers = await promptProviders();
628
- // Pick defaults
629
809
  const defaults = await promptDefaults(providers);
630
810
  defaultProvider = defaults.provider;
631
811
  defaultModel = defaults.model;
632
812
  }
633
813
  else {
634
814
  // auth.json mode — still need provider/model for whitesmith commands
815
+ providers = [];
635
816
  defaultProvider = await input({
636
817
  message: 'Default AI provider:',
637
818
  default: 'anthropic',
@@ -641,22 +822,75 @@ export async function installGitHubCI(ctx, options) {
641
822
  default: 'claude-sonnet-4-20250514',
642
823
  });
643
824
  }
825
+ // ── Export config mode — write JSON to stdout and exit ───────────────
826
+ if (exportConfig) {
827
+ const configFile = { providers, defaultProvider, defaultModel };
828
+ if (options.includeSecrets && providers.length > 0) {
829
+ const secrets = {};
830
+ const seen = new Set();
831
+ for (const p of providers) {
832
+ if (seen.has(p.apiKeyEnvVar))
833
+ continue;
834
+ seen.add(p.apiKeyEnvVar);
835
+ const key = await password({
836
+ message: `Enter API key for ${p.name} (${p.apiKeyEnvVar}):`,
837
+ });
838
+ if (key)
839
+ secrets[p.apiKeyEnvVar] = key;
840
+ }
841
+ if (Object.keys(secrets).length > 0) {
842
+ configFile.secrets = secrets;
843
+ }
844
+ }
845
+ const json = JSON.stringify(configFile, null, 2) + '\n';
846
+ fs.writeFileSync(exportConfig, json, 'utf-8');
847
+ console.log(`\n✅ Config written to ${exportConfig}`);
848
+ return;
849
+ }
850
+ // Auto-detect dev mode: check if we're inside the whitesmith repo itself
851
+ let dev = options.dev ?? false;
852
+ if (!dev) {
853
+ try {
854
+ const pkgPath = path.join(ctx.workDir, 'package.json');
855
+ if (fs.existsSync(pkgPath)) {
856
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
857
+ if (pkg.name === 'whitesmith') {
858
+ dev = true;
859
+ console.log('📦 Detected whitesmith repo — using dev mode (build from source)\n');
860
+ }
861
+ }
862
+ }
863
+ catch {
864
+ // Ignore — not in whitesmith repo
865
+ }
866
+ }
867
+ const reviewWorkflow = options.reviewWorkflow ?? false;
868
+ const reviewStepEnabled = options.reviewStepEnabled ?? true;
644
869
  const config = {
645
870
  authMode,
646
871
  providers,
647
872
  defaultProvider,
648
873
  defaultModel,
874
+ dev,
875
+ reviewWorkflow,
876
+ reviewStepEnabled,
877
+ version: options.version,
649
878
  };
650
879
  // ── Set GitHub secrets via gh CLI ─────────────────────────────────────
651
- const fake = options.fake ?? false;
652
- if (!fake && authMode === 'models-json' && repo) {
880
+ const skipSecrets = options.skipSecrets ?? false;
881
+ if (skipSecrets) {
882
+ console.log('\n🔑 Skipping secret setup (--no-secrets)');
883
+ }
884
+ else if (!fake && authMode === 'models-json' && repo) {
653
885
  if (!ctx.ghAvailable) {
654
886
  console.log('\n⚠ GitHub CLI (gh) is not available or not authenticated.');
655
887
  console.log(' You will need to set the following secrets manually.\n');
656
888
  }
657
889
  else {
890
+ // If config file included secrets, set them directly without prompting
891
+ const configSecrets = options.configFile ? loadedSecrets : undefined;
658
892
  console.log('\n🔑 Setting API key secrets on GitHub...\n');
659
- const setSecrets = await promptAndSetSecrets(ctx, providers);
893
+ const setSecrets = await setOrPromptSecrets(ctx, providers, configSecrets);
660
894
  const allEnvVars = [...new Set(providers.map((p) => p.apiKeyEnvVar))];
661
895
  const missing = allEnvVars.filter((v) => !setSecrets.includes(v));
662
896
  if (missing.length > 0) {
@@ -672,10 +906,16 @@ export async function installGitHubCI(ctx, options) {
672
906
  }
673
907
  // ── Generate and write workflow files ─────────────────────────────────
674
908
  const outputBase = fake ? '.fake' : '.github';
675
- const githubDir = path.join(ctx.workDir, outputBase);
676
- const workflowsDir = path.join(githubDir, 'workflows');
909
+ const baseDir = path.join(ctx.workDir, outputBase);
910
+ const workflowsDir = path.join(baseDir, 'workflows');
911
+ const actionsDir = path.join(baseDir, 'actions', 'setup-whitesmith');
677
912
  fs.mkdirSync(workflowsDir, { recursive: true });
913
+ fs.mkdirSync(actionsDir, { recursive: true });
678
914
  const files = [
915
+ {
916
+ path: path.join(actionsDir, 'action.yml'),
917
+ content: generateSetupAction(config),
918
+ },
679
919
  {
680
920
  path: path.join(workflowsDir, 'whitesmith.yml'),
681
921
  content: generateMainWorkflow(config),
@@ -684,13 +924,23 @@ export async function installGitHubCI(ctx, options) {
684
924
  path: path.join(workflowsDir, 'whitesmith-comment.yml'),
685
925
  content: generateCommentWorkflow(config),
686
926
  },
927
+ {
928
+ path: path.join(workflowsDir, 'whitesmith-issue.yml'),
929
+ content: generateIssueWorkflow(config),
930
+ },
687
931
  {
688
932
  path: path.join(workflowsDir, 'whitesmith-reconcile.yml'),
689
- content: generateReconcileWorkflow(),
933
+ content: generateReconcileWorkflow(config),
690
934
  },
691
935
  ];
936
+ if (config.reviewWorkflow) {
937
+ files.push({
938
+ path: path.join(workflowsDir, 'whitesmith-review.yml'),
939
+ content: generateReviewWorkflow(config),
940
+ });
941
+ }
692
942
  if (authMode === 'auth-json') {
693
- const scriptsDir = path.join(githubDir, 'scripts');
943
+ const scriptsDir = path.join(baseDir, 'scripts');
694
944
  fs.mkdirSync(scriptsDir, { recursive: true });
695
945
  files.push({
696
946
  path: path.join(scriptsDir, 'refresh-oauth-token.mjs'),