hub-launch 1.16.0 → 1.18.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 (101) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +102 -2
  3. package/dist/commands/init.d.ts +61 -0
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +125 -114
  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 +22 -1
  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 +206 -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 +91 -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-help/SKILL.md +117 -0
  78. package/dist/templates/skills/hula-launch/SKILL.md +14 -8
  79. package/dist/templates/skills/hula-merge/SKILL.md +2 -2
  80. package/dist/templates/skills/hula-plan/SKILL.md +5 -5
  81. package/dist/templates/skills/hula-schedule/SKILL.md +16 -16
  82. package/dist/templates/skills/hula-verify/SKILL.md +32 -33
  83. package/dist/types/config.schema.d.ts +235 -0
  84. package/dist/types/config.schema.d.ts.map +1 -1
  85. package/dist/types/config.schema.js +66 -0
  86. package/dist/types/config.schema.js.map +1 -1
  87. package/dist/utils/client-session.d.ts +2 -2
  88. package/dist/utils/client-session.js +2 -2
  89. package/package.json +6 -5
  90. package/scripts/postinstall.mjs +102 -0
  91. package/dist/templates/scripts/hula-fix-commit.sh +0 -114
  92. package/dist/templates/scripts/hula-fix-setup.sh +0 -152
  93. package/dist/templates/scripts/hula-launch-run.sh +0 -140
  94. package/dist/templates/scripts/hula-merge-local.sh +0 -138
  95. package/dist/templates/scripts/hula-merge-remote.sh +0 -94
  96. package/dist/templates/scripts/hula-read-config.sh +0 -38
  97. package/dist/templates/scripts/hula-schedule-manage.sh +0 -159
  98. package/dist/templates/scripts/hula-schedule-run.sh +0 -148
  99. package/dist/templates/scripts/hula-session-hook.sh +0 -68
  100. package/dist/templates/scripts/hula-verify-gather.sh +0 -135
  101. package/dist/templates/scripts/hula-verify-post.sh +0 -84
package/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.18.0] - 2026-07-28
11
+
12
+ ### Added
13
+
14
+ - Verify reports now auto-post to the PR by default; use `--no-post` to restore the old confirm-before-posting behavior.
15
+ - Added a `hula-help` Agent Skill for interactive onboarding and reference guidance.
16
+ - Moved `hula-verify`'s tmp writes from OS `/tmp` to a repo-local, gitignored `.hublaunch/tmp/` directory (#325).
17
+
18
+ ## [1.17.0] - 2026-07-23
19
+
20
+ ### Added
21
+
22
+ - `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.
23
+
10
24
  ## [1.16.0] - 2026-07-17
11
25
 
12
26
  ### Fixed
package/README.md CHANGED
@@ -40,6 +40,7 @@ That's the whole fundamental workflow.
40
40
  - 🖥️ **Client Agnostic** — based on skills that can be adapted to most agent harnesses
41
41
  - 🔔 **Notifications** - you can configure an endpoint for notifications about progress. For instance, a slack channel.
42
42
  - 📋 **Logging** - run `hula-info` for progress in the container. Also pushes several documents within each PR showing logging.
43
+ - 🧭 **Guided Help** — run `/hula-help` anytime for an interactive walkthrough of setup, the workflow, and any command or skill
43
44
  - 🖥️ **Dashboard** — track all your active plans at [https://www.hublaunch.site/dashboard](https://www.hublaunch.site/dashboard)
44
45
  - 🔒 **Safe** - tokens are maintained by you locally in your config file, and on our server securely protected and removed when no longer needed.
45
46
 
@@ -82,8 +83,9 @@ After upgrading the CLI:
82
83
  npm install -g hub-launch@latest # or: pnpm add -g hub-launch
83
84
  ```
84
85
 
85
- re-run `hula init` inside each project to refresh the bundled Agent Skills,
86
- instruction templates, and config scaffolding:
86
+ re-run `hula init` inside each project to refresh the bundled Agent Skills and
87
+ config scaffolding (workflow scripts and instruction docs now ship with the
88
+ package, so they update automatically with the CLI):
87
89
 
88
90
  ```bash
89
91
  cd <your-project>
@@ -149,9 +151,41 @@ Top-level commands:
149
151
  | `hula merge` | — | Merge PR and clean up |
150
152
  | `hula launch` | — | Trigger AI coding session on hula-project server |
151
153
  | `hula schedule` | — | Trigger or schedule an execute-action (e.g. `--built-in harden`) |
154
+ | `hula script <name>` | — | Run a bundled cross-platform workflow script (used by the Agent Skills) |
155
+ | `hula instructions <name>` | — | Print a bundled instruction doc (`planning`, `proceed`, `skill-creation`) |
156
+ | `hula session-hook` | — | Claude Code PreToolUse hook that captures launch-session provenance |
152
157
 
153
158
  Run `hula <command> --help` for details, or see the full [Commands Reference](./docs/commands.md).
154
159
 
160
+ ### Cross-platform, low-footprint tooling
161
+
162
+ The workflow scripts and instruction documents that power the Agent Skills ship
163
+ **inside the `hula` package** as cross-platform Node — they are no longer copied
164
+ into your repo, and they need **no `bash` and no `jq`**, so they run identically
165
+ on macOS, Linux, and Windows. `hula init` therefore does not write
166
+ `.github/scripts/*.sh` or `.hublaunch/*-instructions.md`.
167
+
168
+ Skills invoke them via the global bin:
169
+
170
+ ```bash
171
+ # Run a bundled workflow script (cross-platform, no bash/jq)
172
+ hula script merge-local -- 42 ".hula-worktrees/issue-42" "fix(#42): message"
173
+
174
+ # Print a bundled instruction document
175
+ hula instructions planning
176
+ ```
177
+
178
+ **Migration:** upgrading from an older version? Existing
179
+ `.github/scripts/hula-*.sh` and `.hublaunch/*-instructions.md` keep working, but
180
+ are now obsolete and safe to delete — `hula init` prints a reminder listing them
181
+ (it never deletes anything).
182
+
183
+ **Claude Code commands:** the `/hula-*` commands are available as a Claude Code
184
+ plugin so they load globally, so `hula init` no longer writes `.claude/commands/*`
185
+ symlinks by default. Prefer repo-local command files instead? Run
186
+ `hula init --with-claude-commands`. The committed `.agents/skills/` directory
187
+ (read by GitHub Copilot, Cursor, Codex, and 30+ tools) is unchanged.
188
+
155
189
  ## `hula launch` and Resume
156
190
 
157
191
  `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 +315,72 @@ variables, and includes them in the request to the server. Notes:
281
315
  - Only forward variables your tests actually need; treat anything you list as
282
316
  leaving your machine.
283
317
 
318
+ ### Configuring per-step model & iteration overrides
319
+
320
+ `hula launch` runs your plan through a fixed 9-step server-side pipeline. By
321
+ default every step uses the server's built-in model, loop-iteration cap, and
322
+ skip behavior. To override those per step for your project, add a `steps` block
323
+ to `.hublaunch/hublaunch.config.js` (config-file only — this is a persistent
324
+ project setting, not a per-launch flag):
325
+
326
+ ```js
327
+ export const config = {
328
+ // ...
329
+ steps: {
330
+ implementation: { model: "opus" },
331
+ lintfix: { model: "haiku", maxIterations: 2 },
332
+ regression: { skip: true },
333
+ },
334
+ };
335
+ ```
336
+
337
+ Each of the 9 keys maps to a pipeline step. All fields are optional; omit a step
338
+ (or the whole `steps` block) to keep the server default:
339
+
340
+ | Step key | `model` | `maxIterations` | `skip` | Other |
341
+ | ---------------- | :-----: | :-------------: | :----: | ----- |
342
+ | `implementation` | ✅ | | | |
343
+ | `findBugs` | ✅ | ✅ (1–20) | | `diffMaxLines` (≥1), `excludeRegex` (string) |
344
+ | `bugfix` | ✅ | | | |
345
+ | `lintfix` | ✅ | ✅ (1–20) | | |
346
+ | `build` | ✅ | ✅ (1–20) | | |
347
+ | `regression` | | | ✅ | |
348
+ | `mergeConflict` | ✅ | | | |
349
+ | `summary` | ✅ | ✅ (1–20) | | |
350
+ | `verify` | ✅ | ✅ (1–20) | ✅ | |
351
+
352
+ `model` values are free-form strings passed straight through to the server
353
+ (`claude --model <value>`); no allowlist is enforced. Values are validated at
354
+ config-load time — an out-of-range `maxIterations`, a non-boolean `skip`, or an
355
+ empty `model` string fails immediately with a clear error.
356
+
357
+ #### `--skip-regression`
358
+
359
+ As a per-launch counterpart to the config `steps.regression.skip`, `hula launch`
360
+ accepts a `--skip-regression` flag that force-skips the regression-tests step for
361
+ that one invocation (it wins over whatever `steps.regression.skip` is set to in
362
+ config):
363
+
364
+ ```bash
365
+ # Skip regression tests for a single launch
366
+ hula launch feature-auth .hublaunch/plans/my-plan.md --skip-regression
367
+ ```
368
+
369
+ #### Conflict rules (validated locally before any network call)
370
+
371
+ Two combinations are rejected client-side — `hula launch` exits `1` immediately
372
+ with the same message the server would return, so misconfigurations fail fast:
373
+
374
+ - **`bugfix.model` ≠ `mergeConflict.model`** — both map to the same
375
+ `RALPH_BUGFIX_MODEL` env var on the server, so they cannot be set to different
376
+ values:
377
+ `bugfix.model and mergeConflict.model both map to RALPH_BUGFIX_MODEL and cannot conflict`
378
+ - **`regression.skip: true` combined with the legacy `--regression` flag** — one
379
+ forces the step off, the other forces it on. This also covers passing both
380
+ `--skip-regression` and `--regression`, and a config `steps.regression.skip:
381
+ true` combined with a one-off `--regression`:
382
+ `regression.skip and the legacy regression flag are contradictory`
383
+
284
384
  ## `hula schedule`
285
385
 
286
386
  `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.
@@ -1,5 +1,11 @@
1
1
  import type { Command } from 'commander';
2
2
  import type { Config } from '../types/index.js';
3
+ /**
4
+ * Idempotently append a `.gitignore` entry with its own comment header, if
5
+ * `marker` isn't already present in `content`. Pure/sync so the append logic is
6
+ * unit-testable without touching the filesystem.
7
+ */
8
+ export declare function appendGitignoreEntry(content: string, marker: string, comment: string, entry: string): string;
3
9
  /**
4
10
  * Keys whose **actual runtime value** is written by the config template below.
5
11
  * A key belongs here if and only if emitting it a second time would duplicate
@@ -40,7 +46,62 @@ export declare function initCommand(program: Command, _config: Config): void;
40
46
  * Generate hook templates based on authentication provider
41
47
  */
42
48
  export declare function generateHookTemplates(projectRoot: string, authProvider: string): Promise<void>;
49
+ /**
50
+ * Ensure the `.hublaunch/skills/` action-file directory exists.
51
+ *
52
+ * The planning/proceed/skill-creation instruction documents are no longer
53
+ * copied into the consumer repo — they ship inside the `hula` package and are
54
+ * read at runtime via `hula instructions <planning|proceed|skill-creation>`.
55
+ * This keeps the project footprint to genuinely project-specific files.
56
+ *
57
+ * Only the `.hublaunch/skills/` directory (user-authored /hula-schedule action
58
+ * files) is created here; files committed there persist (it is not gitignored).
59
+ */
60
+ export declare function initializeSkillsActionDir(projectRoot: string): Promise<void>;
61
+ /**
62
+ * Initialize Agent Skills for AI-assisted hula workflows.
63
+ *
64
+ * Single source of truth: .agents/skills/<name>/SKILL.md (Agent Skills spec).
65
+ *
66
+ * Each harness gets a reference to that canonical file:
67
+ * - GitHub Copilot, Cursor, Codex, and other Agent Skills-compatible tools
68
+ * read directly from .agents/skills/<name>/SKILL.md.
69
+ * - Claude Code reads from .claude/commands/<name>.md, which is created as
70
+ * a symlink pointing into .agents/skills/ (relative path so it works after
71
+ * cloning on any machine). No content is duplicated.
72
+ *
73
+ * Because the symlinks are named <name>.md (not SKILL.md) and live under
74
+ * .claude/commands/ (not .agents/skills/), GitHub Copilot's Agent Skills
75
+ * scanner does not pick them up, so no duplicate commands appear.
76
+ *
77
+ * Any existing .claude/skills/ directory is removed on init to clean up
78
+ * legacy installations that previously wrote full files there.
79
+ *
80
+ * Agent Skills standard: https://agentskills.io/specification
81
+ */
82
+ export declare function initializeAgentSkills(projectRoot: string, withClaudeCommands: boolean): Promise<void>;
83
+ /**
84
+ * Detect tool-owned files from earlier hula versions that are now obsolete and
85
+ * print a non-destructive notice. `hula init` no longer generates
86
+ * `.github/scripts/hula-*.sh` or `.hublaunch/*-instructions.md` — those scripts
87
+ * and instruction docs ship inside the `hula` package now (run via
88
+ * `hula script <name>` / `hula instructions <name>`). Pre-existing copies are
89
+ * left untouched (they still work) but can be safely deleted.
90
+ */
91
+ export declare function noticeObsoleteFiles(projectRoot: string): Promise<void>;
92
+ /**
93
+ * Register the hula session hook in the consumer repo's .claude/settings.json.
94
+ *
95
+ * Adds a PreToolUse/Bash hook entry that captures the Claude Code session id
96
+ * of the chat session running /hula-launch (clientSessionId provenance —
97
+ * hula-server PR #419). Unlike initVsCodeAutoApproval's single-key overwrite,
98
+ * hooks live in a NESTED ARRAY (`hooks.PreToolUse[]`) that users may already
99
+ * populate, so this merges: it preserves every existing hook and appends ours
100
+ * only when absent (idempotent by the command string).
101
+ */
102
+ export declare function initClaudeSessionHook(projectRoot: string): Promise<void>;
43
103
  export declare function executeInit(options?: {
44
104
  force?: boolean;
105
+ withClaudeCommands?: boolean;
45
106
  }): Promise<void>;
46
107
  //# 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;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,MAAM,CAIR;AAED;;;;;;;;;;;;;;;;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;AAyED;;;;;;;;;;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,CA4iBhB"}
@@ -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';
@@ -10,6 +10,17 @@ import { DEFAULT_CONFIG } from '../types/config.schema.js';
10
10
  import { checkGhVersion } from '../utils/github-cli.js';
11
11
  import { parseExistingConfig, mergeConfigs, CONFIG_PARSE_FAILED_PREFIX, } from '../utils/config-parser.js';
12
12
  import { getCliVersion } from '../utils/version.js';
13
+ /**
14
+ * Idempotently append a `.gitignore` entry with its own comment header, if
15
+ * `marker` isn't already present in `content`. Pure/sync so the append logic is
16
+ * unit-testable without touching the filesystem.
17
+ */
18
+ export function appendGitignoreEntry(content, marker, comment, entry) {
19
+ if (content.includes(marker))
20
+ return content;
21
+ const prefix = content.length === 0 || content.endsWith('\n') ? '' : '\n';
22
+ return `${content}${prefix}\n${comment}\n${entry}\n`;
23
+ }
13
24
  /**
14
25
  * Keys whose **actual runtime value** is written by the config template below.
15
26
  * A key belongs here if and only if emitting it a second time would duplicate
@@ -45,6 +56,7 @@ export const TEMPLATE_EMITTED_KEYS = new Set([
45
56
  'hooks',
46
57
  'worktreeBasePath',
47
58
  'envVars',
59
+ 'steps',
48
60
  ]);
49
61
  /**
50
62
  * Build the `// Preserved custom settings` block for any keys present in the
@@ -75,6 +87,7 @@ export function initCommand(program, _config) {
75
87
  .command('init')
76
88
  .description('Initialize HubLaunch configuration (interactive)')
77
89
  .option('--force', 'Overwrite existing configuration')
90
+ .option('--with-claude-commands', 'Also write repo-local .claude/commands/* symlinks (default: use the global hula Claude Code plugin instead)')
78
91
  .action(async (options) => {
79
92
  try {
80
93
  await executeInit(options);
@@ -605,43 +618,20 @@ const AGENT_SKILLS = [
605
618
  { templateDir: 'hula-schedule', skillName: 'hula-schedule' },
606
619
  { templateDir: 'hula-upload', skillName: 'hula-upload' },
607
620
  { templateDir: 'hula-info', skillName: 'hula-info' },
621
+ { templateDir: 'hula-help', skillName: 'hula-help' },
608
622
  ];
609
623
  /**
610
- * Copy planning instruction templates to .hublaunch/.
611
- * These are used by the hula-plan and hula-confirm skills at runtime.
624
+ * Ensure the `.hublaunch/skills/` action-file directory exists.
625
+ *
626
+ * The planning/proceed/skill-creation instruction documents are no longer
627
+ * copied into the consumer repo — they ship inside the `hula` package and are
628
+ * read at runtime via `hula instructions <planning|proceed|skill-creation>`.
629
+ * This keeps the project footprint to genuinely project-specific files.
630
+ *
631
+ * Only the `.hublaunch/skills/` directory (user-authored /hula-schedule action
632
+ * files) is created here; files committed there persist (it is not gitignored).
612
633
  */
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.
634
+ export async function initializeSkillsActionDir(projectRoot) {
645
635
  const skillsDir = join(projectRoot, '.hublaunch', 'skills');
646
636
  await mkdir(skillsDir, { recursive: true });
647
637
  const gitkeepPath = join(skillsDir, '.gitkeep');
@@ -671,16 +661,20 @@ async function initializePlanningInstructions(projectRoot, overridePlanningInstr
671
661
  *
672
662
  * Agent Skills standard: https://agentskills.io/specification
673
663
  */
674
- async function initializeAgentSkills(projectRoot) {
664
+ export async function initializeAgentSkills(projectRoot, withClaudeCommands) {
675
665
  const __filename = fileURLToPath(import.meta.url);
676
666
  const __dirname = dirname(__filename);
677
667
  const skillsSourceDir = join(__dirname, '..', 'templates', 'skills');
678
668
  // Canonical location — single source of truth.
679
669
  const agentsSkillsDir = join(projectRoot, '.agents', 'skills');
680
670
  await mkdir(agentsSkillsDir, { recursive: true });
681
- // Claude Code reads from .claude/commands/.
671
+ // Claude Code reads from .claude/commands/ — only when the opt-in flag asks
672
+ // for repo-local commands. By default the /hula-* commands come from the hula
673
+ // Claude Code plugin (installed globally), so no symlinks are written.
682
674
  const claudeCommandsDir = join(projectRoot, '.claude', 'commands');
683
- await mkdir(claudeCommandsDir, { recursive: true });
675
+ if (withClaudeCommands) {
676
+ await mkdir(claudeCommandsDir, { recursive: true });
677
+ }
684
678
  // Remove .claude/skills/ if it exists — legacy path that caused duplicates.
685
679
  const claudeSkillsDir = join(projectRoot, '.claude', 'skills');
686
680
  try {
@@ -695,63 +689,66 @@ async function initializeAgentSkills(projectRoot) {
695
689
  const agentsDestDir = join(agentsSkillsDir, skillName);
696
690
  await mkdir(agentsDestDir, { recursive: true });
697
691
  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);
692
+ // Optionally create a symlink in .claude/commands/ pointing back to the
693
+ // canonical file. Relative path so it works after cloning on any machine.
694
+ if (withClaudeCommands) {
695
+ const symlinkPath = join(claudeCommandsDir, `${skillName}.md`);
696
+ const symlinkTarget = join('..', '..', '.agents', 'skills', skillName, 'SKILL.md');
697
+ // Remove any existing file/symlink before (re-)creating.
698
+ await rm(symlinkPath, { force: true });
699
+ await symlink(symlinkTarget, symlinkPath);
700
+ }
705
701
  logger.success(`Updated skill: ${skillName}`);
706
702
  }
707
703
  logger.blank();
708
704
  logger.info('🎉 Agent Skills installed — works with Claude Code, GitHub Copilot, Codex, and 30+ AI tools');
709
705
  logger.info('💡 Invoke with /hula-plan, /hula-fix, /hula-create, etc. (note: hyphens, not dots)');
706
+ if (withClaudeCommands) {
707
+ logger.info('🔗 Repo-local Claude Code commands written to .claude/commands/ (--with-claude-commands).');
708
+ }
709
+ else {
710
+ logger.info('📦 For Claude Code, /hula-* commands are installed globally to ~/.claude/commands/ when hula is installed/built — no per-project files needed.');
711
+ logger.info(' If they are missing, reinstall hula (or run its postinstall), or use `hula init --with-claude-commands` for repo-local command files.');
712
+ }
710
713
  }
711
714
  /**
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.
715
- */
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.
715
+ * Detect tool-owned files from earlier hula versions that are now obsolete and
716
+ * print a non-destructive notice. `hula init` no longer generates
717
+ * `.github/scripts/hula-*.sh` or `.hublaunch/*-instructions.md` those scripts
718
+ * and instruction docs ship inside the `hula` package now (run via
719
+ * `hula script <name>` / `hula instructions <name>`). Pre-existing copies are
720
+ * left untouched (they still work) but can be safely deleted.
740
721
  */
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}`);
722
+ export async function noticeObsoleteFiles(projectRoot) {
723
+ const obsolete = [];
724
+ const scriptsDir = join(projectRoot, '.github', 'scripts');
725
+ if (existsSync(scriptsDir)) {
726
+ try {
727
+ const entries = await readdir(scriptsDir);
728
+ if (entries.some((f) => f.startsWith('hula-') && f.endsWith('.sh'))) {
729
+ obsolete.push('.github/scripts/hula-*.sh');
730
+ }
731
+ }
732
+ catch {
733
+ // Unreadable dir — skip the notice rather than fail init.
734
+ }
735
+ }
736
+ for (const name of [
737
+ 'planning-instructions.md',
738
+ 'proceed-instructions.md',
739
+ 'skill-creation-instructions.md',
740
+ ]) {
741
+ if (existsSync(join(projectRoot, '.hublaunch', name))) {
742
+ obsolete.push(`.hublaunch/${name}`);
743
+ break;
744
+ }
745
+ }
746
+ if (obsolete.length > 0) {
747
+ logger.blank();
748
+ logger.info('ℹ️ These hula files are now obsolete and safe to delete (scripts & instructions ship with the hula package):');
749
+ for (const f of obsolete) {
750
+ logger.listItem(f, 1);
751
+ }
755
752
  }
756
753
  }
757
754
  /**
@@ -770,8 +767,6 @@ const VSCODE_AUTO_APPROVE_SETTINGS = {
770
767
  '/^gh (issue view|pr list|pr view|pr diff|repo view)\\b/': true,
771
768
  // GitHub CLI write commands used by hula workflows
772
769
  '/^gh pr (comment|merge)\\b/': true,
773
- // Hula bash scripts deployed to .github/scripts/
774
- '/^bash \\.github\\/scripts\\/hula-/': true,
775
770
  // Common safe commands
776
771
  '/^(ls|cat|head|tail|wc|echo|mkdir|rm -f|cd|pwd)\\b/': true,
777
772
  };
@@ -807,10 +802,10 @@ async function initVsCodeAutoApproval(projectRoot) {
807
802
  await writeFile(settingsPath, content, 'utf-8');
808
803
  logger.success('VS Code auto-approval configured: .vscode/settings.json');
809
804
  }
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';
805
+ /** The hook command registered in .claude/settings.json. Runs the global
806
+ * `hula` bin (cross-platform, no bash/jq) instead of a project-local shell
807
+ * script, so no file is written into the consumer repo. */
808
+ const CLAUDE_SESSION_HOOK_COMMAND = 'hula session-hook';
814
809
  /**
815
810
  * Register the hula session hook in the consumer repo's .claude/settings.json.
816
811
  *
@@ -821,7 +816,7 @@ const CLAUDE_SESSION_HOOK_COMMAND = 'bash .github/scripts/hula-session-hook.sh';
821
816
  * populate, so this merges: it preserves every existing hook and appends ours
822
817
  * only when absent (idempotent by the command string).
823
818
  */
824
- async function initClaudeSessionHook(projectRoot) {
819
+ export async function initClaudeSessionHook(projectRoot) {
825
820
  const claudeDir = join(projectRoot, '.claude');
826
821
  const settingsPath = join(claudeDir, 'settings.json');
827
822
  await mkdir(claudeDir, { recursive: true });
@@ -1118,6 +1113,12 @@ export async function executeInit(options) {
1118
1113
  envVarsLine =
1119
1114
  '\n // envVars: ["TEST_USER_EMAIL", "API_KEY"], // Environment variables to forward to container (read from .env)';
1120
1115
  }
1116
+ // Per-pipeline-step model/iteration overrides (hula-server PR #442). Rendered
1117
+ // as a real value when preserved across re-init, otherwise a commented example.
1118
+ const stepsDocsUrl = 'https://github.com/NoStackApp/hub-launch#configuring-per-step-model--iteration-overrides';
1119
+ const stepsLine = finalConfig.steps
1120
+ ? `\n steps: ${JSON.stringify(finalConfig.steps)}, // Per-pipeline-step model/iteration overrides. Keys/fields/ranges: ${stepsDocsUrl}`
1121
+ : `\n // steps: { implementation: { model: "opus" }, regression: { skip: true } }, // Per-pipeline-step model/iteration overrides. Keys/fields/ranges: ${stepsDocsUrl}`;
1121
1122
  const configContent = `export const config = {${versionLine}
1122
1123
  // API Configuration${apiKeyLine}${anthropicKeyLine}${daytonaKeyLine}${daytonaTierLine}${containerResourcesLine}${updateNotificationUrlLine}${updateNotificationNameTagLine}${usageTierLine}
1123
1124
 
@@ -1132,7 +1133,7 @@ export async function executeInit(options) {
1132
1133
 
1133
1134
  // Worktree base path (used by /hula-fix, /hula-merge, /hula-upload)
1134
1135
  worktreeBasePath: "${(finalConfig.worktreeBasePath || '.hula-worktrees').replace(/\\/g, '\\\\').replace(/"/g, '\\"')}",
1135
- ${envVarsLine}${buildPreservedSection(finalConfig)}
1136
+ ${envVarsLine}${stepsLine}${buildPreservedSection(finalConfig)}
1136
1137
  };
1137
1138
  `;
1138
1139
  // Create directory if it doesn't exist
@@ -1168,22 +1169,30 @@ ${envVarsLine}${buildPreservedSection(finalConfig)}
1168
1169
  if (existsSync(gitignorePath)) {
1169
1170
  gitignoreContent = await import('fs/promises').then((fs) => fs.readFile(gitignorePath, 'utf-8'));
1170
1171
  }
1172
+ const originalGitignoreContent = gitignoreContent;
1171
1173
  // Check if hublaunch.config.js is already in .gitignore
1172
- if (!gitignoreContent.includes('hublaunch.config.js')) {
1173
- // Add entry with comment
1174
- const newEntry = gitignoreContent.endsWith('\n')
1175
- ? '# HubLaunch configuration\nhublaunch.config.js\n'
1176
- : '\n# HubLaunch configuration\nhublaunch.config.js\n';
1177
- gitignoreContent += newEntry;
1178
- await import('fs/promises').then((fs) => fs.writeFile(gitignorePath, gitignoreContent, 'utf-8'));
1174
+ const beforeConfig = gitignoreContent;
1175
+ gitignoreContent = appendGitignoreEntry(gitignoreContent, 'hublaunch.config.js', '# HubLaunch configuration', 'hublaunch.config.js');
1176
+ if (gitignoreContent !== beforeConfig) {
1179
1177
  logger.success('✨ Added hublaunch.config.js to .gitignore');
1180
1178
  }
1181
1179
  // Check if worktree base path is already in .gitignore (skip absolute paths — they're outside the repo)
1182
- if (!isAbsolute(worktreeBasePath) &&
1183
- !gitignoreContent.includes(worktreeBasePath)) {
1184
- gitignoreContent += `\n# HubLaunch worktrees\n${worktreeBasePath}/\n`;
1180
+ if (!isAbsolute(worktreeBasePath)) {
1181
+ const beforeWorktree = gitignoreContent;
1182
+ gitignoreContent = appendGitignoreEntry(gitignoreContent, worktreeBasePath, '# HubLaunch worktrees', `${worktreeBasePath}/`);
1183
+ if (gitignoreContent !== beforeWorktree) {
1184
+ logger.success(`✨ Added ${worktreeBasePath}/ to .gitignore`);
1185
+ }
1186
+ }
1187
+ // Ignore the repo-local scratch dir hula-verify writes its report/diff/
1188
+ // comment files into.
1189
+ const beforeTmp = gitignoreContent;
1190
+ gitignoreContent = appendGitignoreEntry(gitignoreContent, '.hublaunch/tmp', '# HubLaunch verify report/diff scratch files', '.hublaunch/tmp/');
1191
+ if (gitignoreContent !== beforeTmp) {
1192
+ logger.success('✨ Added .hublaunch/tmp/ to .gitignore');
1193
+ }
1194
+ if (gitignoreContent !== originalGitignoreContent) {
1185
1195
  await import('fs/promises').then((fs) => fs.writeFile(gitignorePath, gitignoreContent, 'utf-8'));
1186
- logger.success(`✨ Added ${worktreeBasePath}/ to .gitignore`);
1187
1196
  }
1188
1197
  // Warn if .agents/skills is in .gitignore — it must be committed so all
1189
1198
  // team members get GitHub Copilot and Claude Code support.
@@ -1232,18 +1241,16 @@ ${envVarsLine}${buildPreservedSection(finalConfig)}
1232
1241
  logger.blank();
1233
1242
  logger.info('Generating templates directory structure...');
1234
1243
  await generateTemplatesStructure(repoRoot);
1235
- // Copy planning instruction templates to .hublaunch/ for skills to reference at runtime
1244
+ // Ensure the .hublaunch/skills/ action-file directory exists. Instruction
1245
+ // documents are no longer copied here — skills read them from the package via
1246
+ // `hula instructions <name>`.
1236
1247
  logger.blank();
1237
- logger.info('Updating planning instruction templates...');
1238
- await initializePlanningInstructions(repoRoot, true, true);
1248
+ logger.info('Ensuring .hublaunch/skills/ action directory...');
1249
+ await initializeSkillsActionDir(repoRoot);
1239
1250
  // Initialize Agent Skills for AI-assisted planning (works with Copilot, Claude Code, and others)
1240
1251
  logger.blank();
1241
1252
  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);
1253
+ await initializeAgentSkills(repoRoot, options?.withClaudeCommands ?? false);
1247
1254
  // Register the Claude Code session hook (clientSessionId provenance)
1248
1255
  logger.blank();
1249
1256
  logger.info('Registering Claude Code session hook...');
@@ -1254,6 +1261,8 @@ ${envVarsLine}${buildPreservedSection(finalConfig)}
1254
1261
  logger.info('Configuring VS Code auto-approval for hula agent commands...');
1255
1262
  await initVsCodeAutoApproval(repoRoot);
1256
1263
  }
1264
+ // Non-destructive migration notice for repos initialized by an older hula.
1265
+ await noticeObsoleteFiles(repoRoot);
1257
1266
  logger.blank();
1258
1267
  logger.blank();
1259
1268
  logger.info('Next steps:');
@@ -1263,6 +1272,7 @@ ${envVarsLine}${buildPreservedSection(finalConfig)}
1263
1272
  logger.listItem('📝 Edit .hublaunch/hublaunch.config.js to customize (each field is documented inline):', 1);
1264
1273
  logger.listItem('daytonaTier, containerResources — sandbox sizing', 2);
1265
1274
  logger.listItem('teamMembers, envVars — project specifics', 2);
1275
+ logger.listItem('steps — per-pipeline-step model & iteration overrides', 2);
1266
1276
  logger.listItem('updateNotificationUrl, updateNotificationNameTag — Slack notifications', 2);
1267
1277
  // Show credential setup instructions based on selected services
1268
1278
  if (authProvider !== 'none') {
@@ -1278,6 +1288,7 @@ ${envVarsLine}${buildPreservedSection(finalConfig)}
1278
1288
  logger.listItem("Run 'hublaunch create' to create your first issue", 1);
1279
1289
  logger.blank();
1280
1290
  logger.info('💡 Or use the Agent Skills slash commands in any AI chat:');
1291
+ logger.info(' - /hula-help [topic] - Interactive guide: getting started, workflow, commands, config');
1281
1292
  logger.info(' - /hula-plan <description> - Generate implementation plan');
1282
1293
  logger.info(' - /hula-confirm - Validate and refine the plan');
1283
1294
  logger.info(' - /hula-upload - Upload plan to origin/main');