hub-launch 1.16.0 → 1.17.0

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 (100) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +101 -2
  3. package/dist/commands/init.d.ts +55 -0
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +93 -103
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/instructions.d.ts +17 -0
  8. package/dist/commands/instructions.d.ts.map +1 -0
  9. package/dist/commands/instructions.js +51 -0
  10. package/dist/commands/instructions.js.map +1 -0
  11. package/dist/commands/launch.d.ts +21 -0
  12. package/dist/commands/launch.d.ts.map +1 -1
  13. package/dist/commands/launch.js +61 -0
  14. package/dist/commands/launch.js.map +1 -1
  15. package/dist/commands/script.d.ts +28 -0
  16. package/dist/commands/script.d.ts.map +1 -0
  17. package/dist/commands/script.js +69 -0
  18. package/dist/commands/script.js.map +1 -0
  19. package/dist/commands/session-hook.d.ts +26 -0
  20. package/dist/commands/session-hook.d.ts.map +1 -0
  21. package/dist/commands/session-hook.js +146 -0
  22. package/dist/commands/session-hook.js.map +1 -0
  23. package/dist/index.js +21 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/scripts/fix-commit.d.ts +2 -0
  26. package/dist/scripts/fix-commit.d.ts.map +1 -0
  27. package/dist/scripts/fix-commit.js +125 -0
  28. package/dist/scripts/fix-commit.js.map +1 -0
  29. package/dist/scripts/fix-setup.d.ts +2 -0
  30. package/dist/scripts/fix-setup.d.ts.map +1 -0
  31. package/dist/scripts/fix-setup.js +161 -0
  32. package/dist/scripts/fix-setup.js.map +1 -0
  33. package/dist/scripts/launch-run.d.ts +14 -0
  34. package/dist/scripts/launch-run.d.ts.map +1 -0
  35. package/dist/scripts/launch-run.js +117 -0
  36. package/dist/scripts/launch-run.js.map +1 -0
  37. package/dist/scripts/lib/exec.d.ts +48 -0
  38. package/dist/scripts/lib/exec.d.ts.map +1 -0
  39. package/dist/scripts/lib/exec.js +61 -0
  40. package/dist/scripts/lib/exec.js.map +1 -0
  41. package/dist/scripts/lib/io.d.ts +48 -0
  42. package/dist/scripts/lib/io.d.ts.map +1 -0
  43. package/dist/scripts/lib/io.js +72 -0
  44. package/dist/scripts/lib/io.js.map +1 -0
  45. package/dist/scripts/lib/read-config.d.ts +26 -0
  46. package/dist/scripts/lib/read-config.d.ts.map +1 -0
  47. package/dist/scripts/lib/read-config.js +65 -0
  48. package/dist/scripts/lib/read-config.js.map +1 -0
  49. package/dist/scripts/merge-local.d.ts +2 -0
  50. package/dist/scripts/merge-local.d.ts.map +1 -0
  51. package/dist/scripts/merge-local.js +146 -0
  52. package/dist/scripts/merge-local.js.map +1 -0
  53. package/dist/scripts/merge-remote.d.ts +12 -0
  54. package/dist/scripts/merge-remote.d.ts.map +1 -0
  55. package/dist/scripts/merge-remote.js +71 -0
  56. package/dist/scripts/merge-remote.js.map +1 -0
  57. package/dist/scripts/schedule-manage.d.ts +2 -0
  58. package/dist/scripts/schedule-manage.d.ts.map +1 -0
  59. package/dist/scripts/schedule-manage.js +151 -0
  60. package/dist/scripts/schedule-manage.js.map +1 -0
  61. package/dist/scripts/schedule-run.d.ts +2 -0
  62. package/dist/scripts/schedule-run.d.ts.map +1 -0
  63. package/dist/scripts/schedule-run.js +146 -0
  64. package/dist/scripts/schedule-run.js.map +1 -0
  65. package/dist/scripts/verify-gather.d.ts +2 -0
  66. package/dist/scripts/verify-gather.d.ts.map +1 -0
  67. package/dist/scripts/verify-gather.js +210 -0
  68. package/dist/scripts/verify-gather.js.map +1 -0
  69. package/dist/scripts/verify-post.d.ts +12 -0
  70. package/dist/scripts/verify-post.d.ts.map +1 -0
  71. package/dist/scripts/verify-post.js +72 -0
  72. package/dist/scripts/verify-post.js.map +1 -0
  73. package/dist/templates/planning-instructions.md +7 -7
  74. package/dist/templates/skill-creation-instructions.md +2 -2
  75. package/dist/templates/skills/hula-confirm/SKILL.md +3 -3
  76. package/dist/templates/skills/hula-fix/SKILL.md +3 -3
  77. package/dist/templates/skills/hula-launch/SKILL.md +14 -8
  78. package/dist/templates/skills/hula-merge/SKILL.md +2 -2
  79. package/dist/templates/skills/hula-plan/SKILL.md +5 -5
  80. package/dist/templates/skills/hula-schedule/SKILL.md +16 -16
  81. package/dist/templates/skills/hula-verify/SKILL.md +2 -2
  82. package/dist/types/config.schema.d.ts +235 -0
  83. package/dist/types/config.schema.d.ts.map +1 -1
  84. package/dist/types/config.schema.js +66 -0
  85. package/dist/types/config.schema.js.map +1 -1
  86. package/dist/utils/client-session.d.ts +2 -2
  87. package/dist/utils/client-session.js +2 -2
  88. package/package.json +6 -5
  89. package/scripts/postinstall.mjs +101 -0
  90. package/dist/templates/scripts/hula-fix-commit.sh +0 -114
  91. package/dist/templates/scripts/hula-fix-setup.sh +0 -152
  92. package/dist/templates/scripts/hula-launch-run.sh +0 -140
  93. package/dist/templates/scripts/hula-merge-local.sh +0 -138
  94. package/dist/templates/scripts/hula-merge-remote.sh +0 -94
  95. package/dist/templates/scripts/hula-read-config.sh +0 -38
  96. package/dist/templates/scripts/hula-schedule-manage.sh +0 -159
  97. package/dist/templates/scripts/hula-schedule-run.sh +0 -148
  98. package/dist/templates/scripts/hula-session-hook.sh +0 -68
  99. package/dist/templates/scripts/hula-verify-gather.sh +0 -135
  100. package/dist/templates/scripts/hula-verify-post.sh +0 -84
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.17.0] - 2026-07-23
11
+
12
+ ### Added
13
+
14
+ - `hula launch` now supports per-pipeline-step model and iteration-cap overrides via a `steps` block in `.hublaunch/hublaunch.config.js` (hula-server PR #442), plus a `--skip-regression` flag to force-skip regression tests for a single launch. Conflicting `bugfix.model`/`mergeConflict.model` values, or `regression.skip` combined with the legacy `--regression` flag, are rejected locally before any network call.
15
+
10
16
  ## [1.16.0] - 2026-07-17
11
17
 
12
18
  ### Fixed
package/README.md CHANGED
@@ -82,8 +82,9 @@ After upgrading the CLI:
82
82
  npm install -g hub-launch@latest # or: pnpm add -g hub-launch
83
83
  ```
84
84
 
85
- re-run `hula init` inside each project to refresh the bundled Agent Skills,
86
- instruction templates, and config scaffolding:
85
+ re-run `hula init` inside each project to refresh the bundled Agent Skills and
86
+ config scaffolding (workflow scripts and instruction docs now ship with the
87
+ package, so they update automatically with the CLI):
87
88
 
88
89
  ```bash
89
90
  cd <your-project>
@@ -149,9 +150,41 @@ Top-level commands:
149
150
  | `hula merge` | — | Merge PR and clean up |
150
151
  | `hula launch` | — | Trigger AI coding session on hula-project server |
151
152
  | `hula schedule` | — | Trigger or schedule an execute-action (e.g. `--built-in harden`) |
153
+ | `hula script <name>` | — | Run a bundled cross-platform workflow script (used by the Agent Skills) |
154
+ | `hula instructions <name>` | — | Print a bundled instruction doc (`planning`, `proceed`, `skill-creation`) |
155
+ | `hula session-hook` | — | Claude Code PreToolUse hook that captures launch-session provenance |
152
156
 
153
157
  Run `hula <command> --help` for details, or see the full [Commands Reference](./docs/commands.md).
154
158
 
159
+ ### Cross-platform, low-footprint tooling
160
+
161
+ The workflow scripts and instruction documents that power the Agent Skills ship
162
+ **inside the `hula` package** as cross-platform Node — they are no longer copied
163
+ into your repo, and they need **no `bash` and no `jq`**, so they run identically
164
+ on macOS, Linux, and Windows. `hula init` therefore does not write
165
+ `.github/scripts/*.sh` or `.hublaunch/*-instructions.md`.
166
+
167
+ Skills invoke them via the global bin:
168
+
169
+ ```bash
170
+ # Run a bundled workflow script (cross-platform, no bash/jq)
171
+ hula script merge-local -- 42 ".hula-worktrees/issue-42" "fix(#42): message"
172
+
173
+ # Print a bundled instruction document
174
+ hula instructions planning
175
+ ```
176
+
177
+ **Migration:** upgrading from an older version? Existing
178
+ `.github/scripts/hula-*.sh` and `.hublaunch/*-instructions.md` keep working, but
179
+ are now obsolete and safe to delete — `hula init` prints a reminder listing them
180
+ (it never deletes anything).
181
+
182
+ **Claude Code commands:** the `/hula-*` commands are available as a Claude Code
183
+ plugin so they load globally, so `hula init` no longer writes `.claude/commands/*`
184
+ symlinks by default. Prefer repo-local command files instead? Run
185
+ `hula init --with-claude-commands`. The committed `.agents/skills/` directory
186
+ (read by GitHub Copilot, Cursor, Codex, and 30+ tools) is unchanged.
187
+
155
188
  ## `hula launch` and Resume
156
189
 
157
190
  `hula launch` submits a job to the hula-project server, which runs an AI coding agent (Claude Code) through a fixed 9-step pipeline:
@@ -281,6 +314,72 @@ variables, and includes them in the request to the server. Notes:
281
314
  - Only forward variables your tests actually need; treat anything you list as
282
315
  leaving your machine.
283
316
 
317
+ ### Configuring per-step model & iteration overrides
318
+
319
+ `hula launch` runs your plan through a fixed 9-step server-side pipeline. By
320
+ default every step uses the server's built-in model, loop-iteration cap, and
321
+ skip behavior. To override those per step for your project, add a `steps` block
322
+ to `.hublaunch/hublaunch.config.js` (config-file only — this is a persistent
323
+ project setting, not a per-launch flag):
324
+
325
+ ```js
326
+ export const config = {
327
+ // ...
328
+ steps: {
329
+ implementation: { model: "opus" },
330
+ lintfix: { model: "haiku", maxIterations: 2 },
331
+ regression: { skip: true },
332
+ },
333
+ };
334
+ ```
335
+
336
+ Each of the 9 keys maps to a pipeline step. All fields are optional; omit a step
337
+ (or the whole `steps` block) to keep the server default:
338
+
339
+ | Step key | `model` | `maxIterations` | `skip` | Other |
340
+ | ---------------- | :-----: | :-------------: | :----: | ----- |
341
+ | `implementation` | ✅ | | | |
342
+ | `findBugs` | ✅ | ✅ (1–20) | | `diffMaxLines` (≥1), `excludeRegex` (string) |
343
+ | `bugfix` | ✅ | | | |
344
+ | `lintfix` | ✅ | ✅ (1–20) | | |
345
+ | `build` | ✅ | ✅ (1–20) | | |
346
+ | `regression` | | | ✅ | |
347
+ | `mergeConflict` | ✅ | | | |
348
+ | `summary` | ✅ | ✅ (1–20) | | |
349
+ | `verify` | ✅ | ✅ (1–20) | ✅ | |
350
+
351
+ `model` values are free-form strings passed straight through to the server
352
+ (`claude --model <value>`); no allowlist is enforced. Values are validated at
353
+ config-load time — an out-of-range `maxIterations`, a non-boolean `skip`, or an
354
+ empty `model` string fails immediately with a clear error.
355
+
356
+ #### `--skip-regression`
357
+
358
+ As a per-launch counterpart to the config `steps.regression.skip`, `hula launch`
359
+ accepts a `--skip-regression` flag that force-skips the regression-tests step for
360
+ that one invocation (it wins over whatever `steps.regression.skip` is set to in
361
+ config):
362
+
363
+ ```bash
364
+ # Skip regression tests for a single launch
365
+ hula launch feature-auth .hublaunch/plans/my-plan.md --skip-regression
366
+ ```
367
+
368
+ #### Conflict rules (validated locally before any network call)
369
+
370
+ Two combinations are rejected client-side — `hula launch` exits `1` immediately
371
+ with the same message the server would return, so misconfigurations fail fast:
372
+
373
+ - **`bugfix.model` ≠ `mergeConflict.model`** — both map to the same
374
+ `RALPH_BUGFIX_MODEL` env var on the server, so they cannot be set to different
375
+ values:
376
+ `bugfix.model and mergeConflict.model both map to RALPH_BUGFIX_MODEL and cannot conflict`
377
+ - **`regression.skip: true` combined with the legacy `--regression` flag** — one
378
+ forces the step off, the other forces it on. This also covers passing both
379
+ `--skip-regression` and `--regression`, and a config `steps.regression.skip:
380
+ true` combined with a one-off `--regression`:
381
+ `regression.skip and the legacy regression flag are contradictory`
382
+
284
383
  ## `hula schedule`
285
384
 
286
385
  `hula schedule` triggers a built-in or custom execute-action (e.g. the `harden` security audit) on the hula-project server, which provisions a sandbox to run it and opens a PR / plan / feedback as the outcome.
@@ -40,7 +40,62 @@ export declare function initCommand(program: Command, _config: Config): void;
40
40
  * Generate hook templates based on authentication provider
41
41
  */
42
42
  export declare function generateHookTemplates(projectRoot: string, authProvider: string): Promise<void>;
43
+ /**
44
+ * Ensure the `.hublaunch/skills/` action-file directory exists.
45
+ *
46
+ * The planning/proceed/skill-creation instruction documents are no longer
47
+ * copied into the consumer repo — they ship inside the `hula` package and are
48
+ * read at runtime via `hula instructions <planning|proceed|skill-creation>`.
49
+ * This keeps the project footprint to genuinely project-specific files.
50
+ *
51
+ * Only the `.hublaunch/skills/` directory (user-authored /hula-schedule action
52
+ * files) is created here; files committed there persist (it is not gitignored).
53
+ */
54
+ export declare function initializeSkillsActionDir(projectRoot: string): Promise<void>;
55
+ /**
56
+ * Initialize Agent Skills for AI-assisted hula workflows.
57
+ *
58
+ * Single source of truth: .agents/skills/<name>/SKILL.md (Agent Skills spec).
59
+ *
60
+ * Each harness gets a reference to that canonical file:
61
+ * - GitHub Copilot, Cursor, Codex, and other Agent Skills-compatible tools
62
+ * read directly from .agents/skills/<name>/SKILL.md.
63
+ * - Claude Code reads from .claude/commands/<name>.md, which is created as
64
+ * a symlink pointing into .agents/skills/ (relative path so it works after
65
+ * cloning on any machine). No content is duplicated.
66
+ *
67
+ * Because the symlinks are named <name>.md (not SKILL.md) and live under
68
+ * .claude/commands/ (not .agents/skills/), GitHub Copilot's Agent Skills
69
+ * scanner does not pick them up, so no duplicate commands appear.
70
+ *
71
+ * Any existing .claude/skills/ directory is removed on init to clean up
72
+ * legacy installations that previously wrote full files there.
73
+ *
74
+ * Agent Skills standard: https://agentskills.io/specification
75
+ */
76
+ export declare function initializeAgentSkills(projectRoot: string, withClaudeCommands: boolean): Promise<void>;
77
+ /**
78
+ * Detect tool-owned files from earlier hula versions that are now obsolete and
79
+ * print a non-destructive notice. `hula init` no longer generates
80
+ * `.github/scripts/hula-*.sh` or `.hublaunch/*-instructions.md` — those scripts
81
+ * and instruction docs ship inside the `hula` package now (run via
82
+ * `hula script <name>` / `hula instructions <name>`). Pre-existing copies are
83
+ * left untouched (they still work) but can be safely deleted.
84
+ */
85
+ export declare function noticeObsoleteFiles(projectRoot: string): Promise<void>;
86
+ /**
87
+ * Register the hula session hook in the consumer repo's .claude/settings.json.
88
+ *
89
+ * Adds a PreToolUse/Bash hook entry that captures the Claude Code session id
90
+ * of the chat session running /hula-launch (clientSessionId provenance —
91
+ * hula-server PR #419). Unlike initVsCodeAutoApproval's single-key overwrite,
92
+ * hooks live in a NESTED ARRAY (`hooks.PreToolUse[]`) that users may already
93
+ * populate, so this merges: it preserves every existing hook and appends ours
94
+ * only when absent (idempotent by the command string).
95
+ */
96
+ export declare function initClaudeSessionHook(projectRoot: string): Promise<void>;
43
97
  export declare function executeInit(options?: {
44
98
  force?: boolean;
99
+ withClaudeCommands?: boolean;
45
100
  }): Promise<void>;
46
101
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAczC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAahD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,qBAAqB,aAkBhC,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAe9E;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAmBnE;AA0bD;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AA4bD,wBAAsB,WAAW,CAAC,OAAO,CAAC,EAAE;IAC1C,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2gBhB"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAczC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAahD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,qBAAqB,aAmBhC,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAe9E;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAuBnE;AA0bD;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAwED;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQlF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,MAAM,EACnB,kBAAkB,EAAE,OAAO,GAC1B,OAAO,CAAC,IAAI,CAAC,CAmEf;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmC5E;AAwED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkD9E;AAsCD,wBAAsB,WAAW,CAAC,OAAO,CAAC,EAAE;IAC1C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CAohBhB"}
@@ -1,4 +1,4 @@
1
- import { writeFile, readFile, mkdir, copyFile, chmod, rm, symlink, } from 'fs/promises';
1
+ import { writeFile, readFile, readdir, mkdir, copyFile, rm, symlink, } from 'fs/promises';
2
2
  import { existsSync } from 'fs';
3
3
  import { join, relative, isAbsolute } from 'path';
4
4
  import { logger } from '../utils/logger.js';
@@ -45,6 +45,7 @@ export const TEMPLATE_EMITTED_KEYS = new Set([
45
45
  'hooks',
46
46
  'worktreeBasePath',
47
47
  'envVars',
48
+ 'steps',
48
49
  ]);
49
50
  /**
50
51
  * Build the `// Preserved custom settings` block for any keys present in the
@@ -75,6 +76,7 @@ export function initCommand(program, _config) {
75
76
  .command('init')
76
77
  .description('Initialize HubLaunch configuration (interactive)')
77
78
  .option('--force', 'Overwrite existing configuration')
79
+ .option('--with-claude-commands', 'Also write repo-local .claude/commands/* symlinks (default: use the global hula Claude Code plugin instead)')
78
80
  .action(async (options) => {
79
81
  try {
80
82
  await executeInit(options);
@@ -607,41 +609,17 @@ const AGENT_SKILLS = [
607
609
  { templateDir: 'hula-info', skillName: 'hula-info' },
608
610
  ];
609
611
  /**
610
- * Copy planning instruction templates to .hublaunch/.
611
- * These are used by the hula-plan and hula-confirm skills at runtime.
612
+ * Ensure the `.hublaunch/skills/` action-file directory exists.
613
+ *
614
+ * The planning/proceed/skill-creation instruction documents are no longer
615
+ * copied into the consumer repo — they ship inside the `hula` package and are
616
+ * read at runtime via `hula instructions <planning|proceed|skill-creation>`.
617
+ * This keeps the project footprint to genuinely project-specific files.
618
+ *
619
+ * Only the `.hublaunch/skills/` directory (user-authored /hula-schedule action
620
+ * files) is created here; files committed there persist (it is not gitignored).
612
621
  */
613
- async function initializePlanningInstructions(projectRoot, overridePlanningInstructions, overrideProceedInstructions) {
614
- const __filename = fileURLToPath(import.meta.url);
615
- const __dirname = dirname(__filename);
616
- const templatesSourceDir = join(__dirname, '..', 'templates');
617
- const instructionsTemplatePath = join(templatesSourceDir, 'planning-instructions.md');
618
- const instructionsDestPath = join(projectRoot, '.hublaunch', 'planning-instructions.md');
619
- if (!existsSync(instructionsDestPath) || overridePlanningInstructions) {
620
- await copyFile(instructionsTemplatePath, instructionsDestPath);
621
- logger.success('Updated: .hublaunch/planning-instructions.md');
622
- }
623
- else {
624
- logger.info('Keeping existing planning instructions');
625
- }
626
- const proceedInstructionsTemplatePath = join(templatesSourceDir, 'proceed-instructions.md');
627
- const proceedInstructionsDestPath = join(projectRoot, '.hublaunch', 'proceed-instructions.md');
628
- if (!existsSync(proceedInstructionsDestPath) || overrideProceedInstructions) {
629
- await copyFile(proceedInstructionsTemplatePath, proceedInstructionsDestPath);
630
- logger.success('Updated: .hublaunch/proceed-instructions.md');
631
- }
632
- else {
633
- logger.info('Keeping existing proceed instructions');
634
- }
635
- // Skill-creation instructions are read by the /hula-schedule skill at runtime
636
- // (parallel to how /hula-plan reads planning-instructions.md). They are
637
- // deterministic tooling, so always refresh them with the latest template.
638
- const skillCreationTemplatePath = join(templatesSourceDir, 'skill-creation-instructions.md');
639
- const skillCreationDestPath = join(projectRoot, '.hublaunch', 'skill-creation-instructions.md');
640
- await copyFile(skillCreationTemplatePath, skillCreationDestPath);
641
- logger.success('Updated: .hublaunch/skill-creation-instructions.md');
642
- // Ensure the action-file directory used by the /hula-schedule skill exists.
643
- // Files committed here persist (it is not gitignored). Seed a .gitkeep so the
644
- // empty directory survives in git until the first action file is created.
622
+ export async function initializeSkillsActionDir(projectRoot) {
645
623
  const skillsDir = join(projectRoot, '.hublaunch', 'skills');
646
624
  await mkdir(skillsDir, { recursive: true });
647
625
  const gitkeepPath = join(skillsDir, '.gitkeep');
@@ -671,16 +649,20 @@ async function initializePlanningInstructions(projectRoot, overridePlanningInstr
671
649
  *
672
650
  * Agent Skills standard: https://agentskills.io/specification
673
651
  */
674
- async function initializeAgentSkills(projectRoot) {
652
+ export async function initializeAgentSkills(projectRoot, withClaudeCommands) {
675
653
  const __filename = fileURLToPath(import.meta.url);
676
654
  const __dirname = dirname(__filename);
677
655
  const skillsSourceDir = join(__dirname, '..', 'templates', 'skills');
678
656
  // Canonical location — single source of truth.
679
657
  const agentsSkillsDir = join(projectRoot, '.agents', 'skills');
680
658
  await mkdir(agentsSkillsDir, { recursive: true });
681
- // Claude Code reads from .claude/commands/.
659
+ // Claude Code reads from .claude/commands/ — only when the opt-in flag asks
660
+ // for repo-local commands. By default the /hula-* commands come from the hula
661
+ // Claude Code plugin (installed globally), so no symlinks are written.
682
662
  const claudeCommandsDir = join(projectRoot, '.claude', 'commands');
683
- await mkdir(claudeCommandsDir, { recursive: true });
663
+ if (withClaudeCommands) {
664
+ await mkdir(claudeCommandsDir, { recursive: true });
665
+ }
684
666
  // Remove .claude/skills/ if it exists — legacy path that caused duplicates.
685
667
  const claudeSkillsDir = join(projectRoot, '.claude', 'skills');
686
668
  try {
@@ -695,63 +677,66 @@ async function initializeAgentSkills(projectRoot) {
695
677
  const agentsDestDir = join(agentsSkillsDir, skillName);
696
678
  await mkdir(agentsDestDir, { recursive: true });
697
679
  await copyFile(srcSkillMd, join(agentsDestDir, 'SKILL.md'));
698
- // Create a symlink in .claude/commands/ pointing back to the canonical file.
699
- // Relative path so the symlink works after cloning on any machine.
700
- const symlinkPath = join(claudeCommandsDir, `${skillName}.md`);
701
- const symlinkTarget = join('..', '..', '.agents', 'skills', skillName, 'SKILL.md');
702
- // Remove any existing file/symlink before (re-)creating.
703
- await rm(symlinkPath, { force: true });
704
- await symlink(symlinkTarget, symlinkPath);
680
+ // Optionally create a symlink in .claude/commands/ pointing back to the
681
+ // canonical file. Relative path so it works after cloning on any machine.
682
+ if (withClaudeCommands) {
683
+ const symlinkPath = join(claudeCommandsDir, `${skillName}.md`);
684
+ const symlinkTarget = join('..', '..', '.agents', 'skills', skillName, 'SKILL.md');
685
+ // Remove any existing file/symlink before (re-)creating.
686
+ await rm(symlinkPath, { force: true });
687
+ await symlink(symlinkTarget, symlinkPath);
688
+ }
705
689
  logger.success(`Updated skill: ${skillName}`);
706
690
  }
707
691
  logger.blank();
708
692
  logger.info('🎉 Agent Skills installed — works with Claude Code, GitHub Copilot, Codex, and 30+ AI tools');
709
693
  logger.info('💡 Invoke with /hula-plan, /hula-fix, /hula-create, etc. (note: hyphens, not dots)');
694
+ if (withClaudeCommands) {
695
+ logger.info('🔗 Repo-local Claude Code commands written to .claude/commands/ (--with-claude-commands).');
696
+ }
697
+ else {
698
+ logger.info('📦 For Claude Code, /hula-* commands are installed globally to ~/.claude/commands/ when hula is installed/built — no per-project files needed.');
699
+ logger.info(' If they are missing, reinstall hula (or run its postinstall), or use `hula init --with-claude-commands` for repo-local command files.');
700
+ }
710
701
  }
711
702
  /**
712
- * List of bundled shell scripts that support the chat participant workflows.
713
- * These scripts are copied from src/templates/scripts/ to .github/scripts/
714
- * during `hula init` to reduce terminal approval clicks in VS Code.
703
+ * Detect tool-owned files from earlier hula versions that are now obsolete and
704
+ * print a non-destructive notice. `hula init` no longer generates
705
+ * `.github/scripts/hula-*.sh` or `.hublaunch/*-instructions.md` those scripts
706
+ * and instruction docs ship inside the `hula` package now (run via
707
+ * `hula script <name>` / `hula instructions <name>`). Pre-existing copies are
708
+ * left untouched (they still work) but can be safely deleted.
715
709
  */
716
- const BUNDLED_SCRIPTS = [
717
- 'hula-read-config.sh',
718
- 'hula-fix-setup.sh',
719
- 'hula-fix-commit.sh',
720
- 'hula-verify-gather.sh',
721
- 'hula-verify-post.sh',
722
- 'hula-merge-local.sh',
723
- 'hula-merge-remote.sh',
724
- 'hula-launch-run.sh',
725
- // No new auto-approve rule needed: the VSCODE_AUTO_APPROVE_SETTINGS pattern
726
- // `^bash \.github/scripts/hula-` already matches hula-schedule-run.sh and
727
- // hula-schedule-manage.sh.
728
- 'hula-schedule-run.sh',
729
- 'hula-schedule-manage.sh',
730
- // Claude Code PreToolUse hook that captures the launching chat session id
731
- // (clientSessionId provenance — hula-server PR #419). Registered in the
732
- // consumer's .claude/settings.json by initClaudeSessionHook().
733
- 'hula-session-hook.sh',
734
- ];
735
- /**
736
- * Initialize bundled shell scripts for chat participant workflows.
737
- * Copies scripts from src/templates/scripts/ to .github/scripts/ in the target project.
738
- * These scripts bundle multiple terminal commands into single invocations,
739
- * reducing VS Code terminal approval popups from ~28 to ~7 per workflow cycle.
740
- */
741
- async function initializeScriptFiles(projectRoot) {
742
- const __filename = fileURLToPath(import.meta.url);
743
- const __dirname = dirname(__filename);
744
- const scriptsSourceDir = join(__dirname, '..', 'templates', 'scripts');
745
- const scriptsDestDir = join(projectRoot, '.github', 'scripts');
746
- // Create .github/scripts directory
747
- await mkdir(scriptsDestDir, { recursive: true });
748
- for (const scriptName of BUNDLED_SCRIPTS) {
749
- const srcPath = join(scriptsSourceDir, scriptName);
750
- const destPath = join(scriptsDestDir, scriptName);
751
- // Always overwrite scripts with latest version (they are deterministic tooling, not user-customized)
752
- await copyFile(srcPath, destPath);
753
- await chmod(destPath, 0o755);
754
- logger.success(`Updated script: .github/scripts/${scriptName}`);
710
+ export async function noticeObsoleteFiles(projectRoot) {
711
+ const obsolete = [];
712
+ const scriptsDir = join(projectRoot, '.github', 'scripts');
713
+ if (existsSync(scriptsDir)) {
714
+ try {
715
+ const entries = await readdir(scriptsDir);
716
+ if (entries.some((f) => f.startsWith('hula-') && f.endsWith('.sh'))) {
717
+ obsolete.push('.github/scripts/hula-*.sh');
718
+ }
719
+ }
720
+ catch {
721
+ // Unreadable dir — skip the notice rather than fail init.
722
+ }
723
+ }
724
+ for (const name of [
725
+ 'planning-instructions.md',
726
+ 'proceed-instructions.md',
727
+ 'skill-creation-instructions.md',
728
+ ]) {
729
+ if (existsSync(join(projectRoot, '.hublaunch', name))) {
730
+ obsolete.push(`.hublaunch/${name}`);
731
+ break;
732
+ }
733
+ }
734
+ if (obsolete.length > 0) {
735
+ logger.blank();
736
+ logger.info('ℹ️ These hula files are now obsolete and safe to delete (scripts & instructions ship with the hula package):');
737
+ for (const f of obsolete) {
738
+ logger.listItem(f, 1);
739
+ }
755
740
  }
756
741
  }
757
742
  /**
@@ -770,8 +755,6 @@ const VSCODE_AUTO_APPROVE_SETTINGS = {
770
755
  '/^gh (issue view|pr list|pr view|pr diff|repo view)\\b/': true,
771
756
  // GitHub CLI write commands used by hula workflows
772
757
  '/^gh pr (comment|merge)\\b/': true,
773
- // Hula bash scripts deployed to .github/scripts/
774
- '/^bash \\.github\\/scripts\\/hula-/': true,
775
758
  // Common safe commands
776
759
  '/^(ls|cat|head|tail|wc|echo|mkdir|rm -f|cd|pwd)\\b/': true,
777
760
  };
@@ -807,10 +790,10 @@ async function initVsCodeAutoApproval(projectRoot) {
807
790
  await writeFile(settingsPath, content, 'utf-8');
808
791
  logger.success('VS Code auto-approval configured: .vscode/settings.json');
809
792
  }
810
- /** The hook command registered in .claude/settings.json (relative path
811
- * Claude Code runs hook commands from the project root, so this is
812
- * machine-independent and safe to commit in the consumer repo). */
813
- const CLAUDE_SESSION_HOOK_COMMAND = 'bash .github/scripts/hula-session-hook.sh';
793
+ /** The hook command registered in .claude/settings.json. Runs the global
794
+ * `hula` bin (cross-platform, no bash/jq) instead of a project-local shell
795
+ * script, so no file is written into the consumer repo. */
796
+ const CLAUDE_SESSION_HOOK_COMMAND = 'hula session-hook';
814
797
  /**
815
798
  * Register the hula session hook in the consumer repo's .claude/settings.json.
816
799
  *
@@ -821,7 +804,7 @@ const CLAUDE_SESSION_HOOK_COMMAND = 'bash .github/scripts/hula-session-hook.sh';
821
804
  * populate, so this merges: it preserves every existing hook and appends ours
822
805
  * only when absent (idempotent by the command string).
823
806
  */
824
- async function initClaudeSessionHook(projectRoot) {
807
+ export async function initClaudeSessionHook(projectRoot) {
825
808
  const claudeDir = join(projectRoot, '.claude');
826
809
  const settingsPath = join(claudeDir, 'settings.json');
827
810
  await mkdir(claudeDir, { recursive: true });
@@ -1118,6 +1101,12 @@ export async function executeInit(options) {
1118
1101
  envVarsLine =
1119
1102
  '\n // envVars: ["TEST_USER_EMAIL", "API_KEY"], // Environment variables to forward to container (read from .env)';
1120
1103
  }
1104
+ // Per-pipeline-step model/iteration overrides (hula-server PR #442). Rendered
1105
+ // as a real value when preserved across re-init, otherwise a commented example.
1106
+ const stepsDocsUrl = 'https://github.com/NoStackApp/hub-launch#configuring-per-step-model--iteration-overrides';
1107
+ const stepsLine = finalConfig.steps
1108
+ ? `\n steps: ${JSON.stringify(finalConfig.steps)}, // Per-pipeline-step model/iteration overrides. Keys/fields/ranges: ${stepsDocsUrl}`
1109
+ : `\n // steps: { implementation: { model: "opus" }, regression: { skip: true } }, // Per-pipeline-step model/iteration overrides. Keys/fields/ranges: ${stepsDocsUrl}`;
1121
1110
  const configContent = `export const config = {${versionLine}
1122
1111
  // API Configuration${apiKeyLine}${anthropicKeyLine}${daytonaKeyLine}${daytonaTierLine}${containerResourcesLine}${updateNotificationUrlLine}${updateNotificationNameTagLine}${usageTierLine}
1123
1112
 
@@ -1132,7 +1121,7 @@ export async function executeInit(options) {
1132
1121
 
1133
1122
  // Worktree base path (used by /hula-fix, /hula-merge, /hula-upload)
1134
1123
  worktreeBasePath: "${(finalConfig.worktreeBasePath || '.hula-worktrees').replace(/\\/g, '\\\\').replace(/"/g, '\\"')}",
1135
- ${envVarsLine}${buildPreservedSection(finalConfig)}
1124
+ ${envVarsLine}${stepsLine}${buildPreservedSection(finalConfig)}
1136
1125
  };
1137
1126
  `;
1138
1127
  // Create directory if it doesn't exist
@@ -1232,18 +1221,16 @@ ${envVarsLine}${buildPreservedSection(finalConfig)}
1232
1221
  logger.blank();
1233
1222
  logger.info('Generating templates directory structure...');
1234
1223
  await generateTemplatesStructure(repoRoot);
1235
- // Copy planning instruction templates to .hublaunch/ for skills to reference at runtime
1224
+ // Ensure the .hublaunch/skills/ action-file directory exists. Instruction
1225
+ // documents are no longer copied here — skills read them from the package via
1226
+ // `hula instructions <name>`.
1236
1227
  logger.blank();
1237
- logger.info('Updating planning instruction templates...');
1238
- await initializePlanningInstructions(repoRoot, true, true);
1228
+ logger.info('Ensuring .hublaunch/skills/ action directory...');
1229
+ await initializeSkillsActionDir(repoRoot);
1239
1230
  // Initialize Agent Skills for AI-assisted planning (works with Copilot, Claude Code, and others)
1240
1231
  logger.blank();
1241
1232
  logger.info('Setting up Agent Skills for AI-assisted planning...');
1242
- await initializeAgentSkills(repoRoot);
1243
- // Initialize bundled shell scripts for chat participant workflows
1244
- logger.blank();
1245
- logger.info('Setting up bundled shell scripts for chat workflows...');
1246
- await initializeScriptFiles(repoRoot);
1233
+ await initializeAgentSkills(repoRoot, options?.withClaudeCommands ?? false);
1247
1234
  // Register the Claude Code session hook (clientSessionId provenance)
1248
1235
  logger.blank();
1249
1236
  logger.info('Registering Claude Code session hook...');
@@ -1254,6 +1241,8 @@ ${envVarsLine}${buildPreservedSection(finalConfig)}
1254
1241
  logger.info('Configuring VS Code auto-approval for hula agent commands...');
1255
1242
  await initVsCodeAutoApproval(repoRoot);
1256
1243
  }
1244
+ // Non-destructive migration notice for repos initialized by an older hula.
1245
+ await noticeObsoleteFiles(repoRoot);
1257
1246
  logger.blank();
1258
1247
  logger.blank();
1259
1248
  logger.info('Next steps:');
@@ -1263,6 +1252,7 @@ ${envVarsLine}${buildPreservedSection(finalConfig)}
1263
1252
  logger.listItem('📝 Edit .hublaunch/hublaunch.config.js to customize (each field is documented inline):', 1);
1264
1253
  logger.listItem('daytonaTier, containerResources — sandbox sizing', 2);
1265
1254
  logger.listItem('teamMembers, envVars — project specifics', 2);
1255
+ logger.listItem('steps — per-pipeline-step model & iteration overrides', 2);
1266
1256
  logger.listItem('updateNotificationUrl, updateNotificationNameTag — Slack notifications', 2);
1267
1257
  // Show credential setup instructions based on selected services
1268
1258
  if (authProvider !== 'none') {