boxdown 1.0.0 → 1.2.1

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 (76) hide show
  1. package/README.md +135 -12
  2. package/assets/devcontainer/README.md +65 -21
  3. package/assets/devcontainer/devcontainer.json +27 -15
  4. package/assets/devcontainer/hooks/initialize.sh +76 -22
  5. package/assets/devcontainer/hooks/post-create.sh +70 -12
  6. package/assets/devcontainer/hooks/post-start.sh +20 -13
  7. package/assets/devcontainer/ssh-config-install.sh +12 -3
  8. package/assets/devcontainer/start.sh +721 -44
  9. package/assets/devcontainer/utils/coding-agent-cli-update.sh +267 -3
  10. package/assets/devcontainer/utils/deps-install.sh +68 -0
  11. package/assets/devcontainer/utils/git-config-bootstrap.sh +128 -0
  12. package/assets/devcontainer/utils/git-signing-bootstrap.sh +109 -0
  13. package/assets/devcontainer/utils/python-bootstrap.sh +69 -0
  14. package/assets/devcontainer/utils/secret-env-bootstrap.sh +22 -0
  15. package/assets/devcontainer/utils/ssh-agent-proxy-bootstrap.sh +27 -0
  16. package/assets/devcontainer/utils/ssh-agent-proxy.mjs +39 -0
  17. package/assets/devcontainer/utils/ssh-bootstrap.sh +9 -0
  18. package/dist/bin/cli.cjs +1 -1
  19. package/dist/bin/cli.mjs +1 -1
  20. package/dist/main-BDgyf2t5.cjs +5758 -0
  21. package/dist/main-J4_2Up3o.mjs +5718 -0
  22. package/dist/main-J4_2Up3o.mjs.map +1 -0
  23. package/dist/main.cjs +5 -2
  24. package/dist/main.d.cts +501 -4
  25. package/dist/main.d.cts.map +1 -1
  26. package/dist/main.d.mts +501 -4
  27. package/dist/main.d.mts.map +1 -1
  28. package/dist/main.mjs +2 -2
  29. package/docs/README.md +1 -0
  30. package/docs/architecture.md +32 -0
  31. package/docs/development.md +13 -6
  32. package/docs/features/README.md +2 -0
  33. package/docs/features/commit-signing.md +94 -0
  34. package/docs/features/generated-config-and-state.md +73 -5
  35. package/docs/features/github-auth-refresh.md +15 -2
  36. package/docs/features/lifecycle.md +103 -11
  37. package/docs/features/setup.md +66 -0
  38. package/docs/features/ssh-config-and-proxy.md +228 -7
  39. package/docs/features/start-and-shell.md +45 -5
  40. package/docs/superpowers/plans/2026-07-11-default-commit-signing.md +110 -0
  41. package/docs/superpowers/plans/2026-07-11-progress-ci-regression.md +125 -0
  42. package/docs/superpowers/plans/2026-07-17-node-ssh-agent-proxy-signing.md +132 -0
  43. package/docs/superpowers/plans/2026-07-17-runtime-secret-environment.md +174 -0
  44. package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +416 -0
  45. package/docs/superpowers/specs/2026-07-11-progress-ci-regression-design.md +43 -0
  46. package/docs/superpowers/specs/2026-07-17-node-ssh-agent-proxy-design.md +63 -0
  47. package/docs/superpowers/specs/2026-07-17-runtime-secret-environment-design.md +194 -0
  48. package/docs/testing.md +35 -2
  49. package/docs/todo.md +2 -2
  50. package/package.json +1 -1
  51. package/src/claude-app-config.ts +304 -0
  52. package/src/cli-style.ts +43 -0
  53. package/src/codex-app-config.ts +656 -0
  54. package/src/config.ts +80 -10
  55. package/src/constants.ts +12 -0
  56. package/src/devcontainer.ts +511 -64
  57. package/src/doctor.ts +292 -30
  58. package/src/git-signing.ts +267 -0
  59. package/src/interactive-prompts.ts +692 -0
  60. package/src/list.ts +16 -2
  61. package/src/logging.ts +164 -0
  62. package/src/main.ts +1214 -63
  63. package/src/metadata.ts +52 -3
  64. package/src/package-info.ts +18 -0
  65. package/src/paths.ts +71 -11
  66. package/src/process.ts +80 -2
  67. package/src/progress.ts +593 -0
  68. package/src/purge.ts +216 -0
  69. package/src/shell.ts +25 -0
  70. package/src/ssh-config.ts +134 -16
  71. package/src/ssh-install-targets.ts +111 -0
  72. package/src/ssh-key.ts +53 -13
  73. package/src/status.ts +5 -0
  74. package/dist/main-BuEptwlL.cjs +0 -1707
  75. package/dist/main-ZFTrSVgt.mjs +0 -1685
  76. package/dist/main-ZFTrSVgt.mjs.map +0 -1
@@ -3,12 +3,20 @@
3
3
  ## Commands
4
4
 
5
5
  ```sh
6
- boxdown ssh-config install
6
+ boxdown setup --target codex
7
+ boxdown setup --target claude
8
+ boxdown ssh install
9
+ boxdown ssh install --target codex
10
+ boxdown ssh install --target claude
11
+ boxdown ssh uninstall
7
12
  boxdown ssh-proxy
13
+ boxdown tunnel --port 3030
8
14
  ```
9
15
 
10
- `boxdown ssh-config` is accepted as a convenience shortcut for
11
- `boxdown ssh-config install`, but docs use the explicit install form.
16
+ `boxdown ssh` is accepted as a convenience shortcut for
17
+ `boxdown ssh install`, but docs use the explicit install form.
18
+ `boxdown setup` is the higher-level workflow when you also want to start or
19
+ recreate the devcontainer before installing the SSH alias.
12
20
 
13
21
  `ssh-proxy` is primarily an internal command launched by OpenSSH as a
14
22
  `ProxyCommand`.
@@ -31,11 +39,103 @@ The generated SSH config block includes:
31
39
  The block is wrapped in Boxdown markers so repeated installs replace the managed
32
40
  block instead of duplicating it.
33
41
 
42
+ When `boxdown ssh install` runs without `--target` in an interactive terminal,
43
+ Boxdown asks which optional install targets to update in addition to the SSH
44
+ alias. The menu supports selecting multiple targets or skipping all optional
45
+ targets. In non-interactive contexts, Boxdown skips optional targets instead of
46
+ blocking and prints the explicit `--target` form for scripts.
47
+
48
+ `boxdown ssh uninstall` removes the Boxdown-managed marker block for the
49
+ selected alias, the matching Codex app remote project entry, and the matching
50
+ Codex persisted sidebar state for the workspace. It leaves unrelated OpenSSH
51
+ config entries, unrelated Codex remote projects, generated state, and SSH key
52
+ files in place.
53
+
34
54
  `boxdown status` reports whether that Boxdown-managed block is `installed`,
35
55
  `missing`, or `outdated`. It only recognizes blocks wrapped in Boxdown's marker
36
56
  comments; an unrelated OpenSSH `Host` entry with the same alias is not treated
37
57
  as an installed Boxdown alias.
38
58
 
59
+ ## Codex App Target
60
+
61
+ Selecting Codex from the interactive prompt or running
62
+ `boxdown ssh install --target codex` keeps the normal SSH install flow and also
63
+ writes a Codex app remote project entry for the same alias. `--target` is
64
+ repeatable so future optional SSH install targets can be combined in one
65
+ install command.
66
+
67
+ The Codex app config is written to:
68
+
69
+ ```text
70
+ ~/.codex/codex-app/config.json
71
+ ```
72
+
73
+ `BOXDOWN_CODEX_APP_CONFIG` overrides this path for tests and local development.
74
+
75
+ The generated Codex entry points at the canonical container workspace path:
76
+
77
+ ```json
78
+ {
79
+ "sshAlias": "<repo-name>-devcontainer",
80
+ "projects": [
81
+ {
82
+ "remotePath": "/workspaces/<repo-name>",
83
+ "label": "<repo-name>"
84
+ }
85
+ ]
86
+ }
87
+ ```
88
+
89
+ Boxdown merges by SSH alias and normalized remote path, so repeated installs
90
+ update the existing Codex project instead of duplicating it. If an older
91
+ Boxdown install registered `/home/node/<repo-name>`, the next Codex target
92
+ install migrates that entry to `/workspaces/<repo-name>`. Existing known Codex
93
+ config keys are preserved, but unknown keys are not written back because
94
+ Codex's app config parser is strict.
95
+
96
+ Boxdown normalizes matching Codex sidebar state for the same SSH alias when it
97
+ installs the Codex target. Restart Codex after installing the target so Codex
98
+ applies the app config, discovers the SSH alias from normal OpenSSH config, and
99
+ creates or updates its sidebar project entry.
100
+
101
+ ## Claude App Target
102
+
103
+ Selecting Claude from the interactive prompt or running
104
+ `boxdown ssh install --target claude` keeps the normal SSH install flow and also
105
+ writes a Claude app SSH remote entry for the same alias. `--target` is
106
+ repeatable, so `codex` and `claude` can be installed in one command.
107
+
108
+ The Claude SSH config is written to:
109
+
110
+ ```text
111
+ ~/Library/Application Support/Claude/ssh_configs.json
112
+ ```
113
+
114
+ `BOXDOWN_CLAUDE_SSH_CONFIGS` overrides this path for tests and local
115
+ development.
116
+
117
+ The generated Claude entry mirrors the desktop app's SSH remote shape:
118
+
119
+ ```json
120
+ {
121
+ "configs": [
122
+ {
123
+ "name": "<repo-name>",
124
+ "sshHost": "<repo-name>-devcontainer",
125
+ "id": "<uuid>",
126
+ "source": "desktop"
127
+ }
128
+ ],
129
+ "trustedHosts": [
130
+ "<repo-name>-devcontainer"
131
+ ]
132
+ }
133
+ ```
134
+
135
+ Boxdown merges by `sshHost`, preserves an existing Claude remote ID, and adds
136
+ the alias to `trustedHosts`. Restart Claude after installing the target so the
137
+ app applies the SSH remote entry.
138
+
39
139
  ## Proxy Flow
40
140
 
41
141
  When OpenSSH launches `boxdown ssh-proxy`, Boxdown:
@@ -44,15 +144,136 @@ When OpenSSH launches `boxdown ssh-proxy`, Boxdown:
44
144
  2. Ensures the per-workspace host key exists.
45
145
  3. Reuses a running devcontainer when possible.
46
146
  4. Starts the devcontainer when needed.
47
- 5. Runs a throttled coding-agent CLI update preflight inside the container.
147
+ 5. Runs a throttled default coding-agent CLI update preflight inside the
148
+ container.
48
149
  6. Runs the container SSH bootstrap runtime.
49
150
  7. Bridges OpenSSH to `/usr/sbin/sshd -i` through `docker exec -i`.
50
151
 
51
152
  This does not publish an SSH port. The SSH stream travels through Docker exec.
52
153
 
53
- The coding-agent CLI update preflight covers already-running containers, where
54
- `postStartCommand` does not necessarily run before a new SSH session. Its output
55
- is routed to stderr so stdout remains reserved for SSH traffic.
154
+ Boxdown augments child-process `PATH` with common macOS GUI-missing tool
155
+ locations, including `/usr/local/bin`, Homebrew paths, `~/.docker/bin`, and
156
+ Docker Desktop's bundled CLI directory. This lets GUI apps such as Claude launch
157
+ the SSH proxy through OpenSSH even when their launchd environment cannot
158
+ resolve `docker` by default.
159
+
160
+ The default coding-agent CLI update preflight covers Codex and Claude Code in
161
+ already-running containers, where `postStartCommand` does not necessarily run
162
+ before a new SSH session. OpenCode and Antigravity remain lazy installs through
163
+ their direct `boxdown` commands. The preflight output is routed to stderr so
164
+ stdout remains reserved for SSH traffic.
165
+
166
+ SSH proxy startup uses the same concise progress protocol as `setup` and
167
+ `start`, but routes progress and failure summaries to stderr because stdout is
168
+ reserved for the SSH stream. Use `boxdown ssh-proxy --verbose` only for manual
169
+ debugging.
170
+
171
+ ## Local Web Tunnels
172
+
173
+ The SSH proxy supports TCP forwarding, so Boxdown can expose a web server that is
174
+ listening only inside the devcontainer.
175
+
176
+ From the target project directory:
177
+
178
+ ```sh
179
+ boxdown tunnel --port 3030
180
+ ```
181
+
182
+ When `--port` is omitted in an interactive terminal, Boxdown prompts for one or
183
+ more port mappings. Pressing Enter accepts the generated devcontainer published
184
+ port when Boxdown can read one from the config, currently `3000`. Non-TTY and
185
+ CI runs keep requiring an explicit `--port`.
186
+
187
+ From another directory:
188
+
189
+ ```sh
190
+ boxdown tunnel --workspace /path/to/project --port 3030
191
+ ```
192
+
193
+ This starts or reuses the devcontainer, ensures the SSH alias exists, and then
194
+ keeps a foreground SSH tunnel open:
195
+
196
+ ```text
197
+ 127.0.0.1:3030 -> localhost:3030
198
+ ```
199
+
200
+ While the tunnel is running, host browsers and the Codex in-app browser can open
201
+ `http://localhost:3030/`.
202
+
203
+ Tunnel setup also hides raw devcontainer startup logs by default; pass
204
+ `--verbose` before the foreground tunnel opens when debugging startup.
205
+
206
+ Repeat `--port` for multiple forwards. Use `<local:remote>` when the host port
207
+ should differ from the container port:
208
+
209
+ ```sh
210
+ boxdown tunnel --port 3030 --port 8080:3031
211
+ ```
212
+
213
+ The tunnel targets remote `localhost` rather than `127.0.0.1` so servers that
214
+ bind to the container's IPv6 loopback, such as some Slidev dev sessions, still
215
+ work.
216
+
217
+ Devcontainer `forwardPorts` is an editor hint, not a host listener in the Codex
218
+ desktop app. Docker `runArgs` / `appPort` publishing can also expose services,
219
+ but it requires the server to bind to an externally reachable container address
220
+ such as `0.0.0.0` and may require recreating the container.
221
+
222
+ Codex's own detected-port forwarding may create a random host URL such as
223
+ `http://localhost:52548/`. That is expected, but the generated SSH forward may
224
+ target remote `127.0.0.1:<port>`. If the dev server is only listening on remote
225
+ IPv6 loopback (`[::1]:<port>`), the random forwarded URL can reset even though a
226
+ listener exists. In that case, either use `boxdown tunnel --port <port>`, or
227
+ start the dev server with a host flag such as `--host 0.0.0.0`.
228
+
229
+ ## IPv4 and IPv6 Loopback Options
230
+
231
+ The Codex detected-port issue is not an `/etc/hosts` problem. `/etc/hosts` maps
232
+ names such as `localhost` to addresses, but a generated SSH forward that targets
233
+ remote `127.0.0.1:<port>` uses a numeric IP literal. No name lookup happens, so
234
+ changing the container's `localhost` entry would not make `127.0.0.1` reach a
235
+ server that is listening only on `[::1]`.
236
+
237
+ Potential Boxdown-side approaches:
238
+
239
+ 1. Inject a Node.js IPv4 preference.
240
+
241
+ Boxdown could add this to the generated devcontainer environment:
242
+
243
+ ```sh
244
+ NODE_OPTIONS=--dns-result-order=ipv4first
245
+ ```
246
+
247
+ This would likely make Node-based dev servers such as Vite and Slidev resolve
248
+ `localhost` to `127.0.0.1` first, causing Codex's generated
249
+ `127.0.0.1:<port>` forward to work without app-specific flags.
250
+
251
+ This is the smallest built-in fix to evaluate, but it mainly helps Node.js
252
+ tools. It would not normalize loopback behavior for dev servers written in
253
+ other runtimes.
254
+
255
+ 2. Run a Boxdown-managed loopback bridge inside the container.
256
+
257
+ A background helper could detect a listener on `[::1]:<port>` where
258
+ `127.0.0.1:<port>` is not listening, then bind `127.0.0.1:<port>` and proxy
259
+ traffic to `[::1]:<port>`.
260
+
261
+ This would be runtime-agnostic and would let Codex's generated
262
+ `127.0.0.1:<port>` forwards work for more tools. The tradeoff is a more
263
+ complex container-side service: it would need port discovery, race handling,
264
+ conflict handling, lifecycle cleanup, and clear diagnostics.
265
+
266
+ 3. Keep `boxdown tunnel` as the explicit escape hatch.
267
+
268
+ The current `boxdown tunnel --port <port>` command creates Boxdown's own SSH
269
+ local forward and targets remote `localhost:<port>` rather than remote
270
+ `127.0.0.1:<port>`. That covers dev servers bound to IPv6 loopback,
271
+ IPv4 loopback, or all interfaces, as long as the remote `localhost` name
272
+ resolves to a reachable address for that server.
273
+
274
+ This is reliable and fully under Boxdown's control, but it requires an extra
275
+ foreground command. It is useful as a debugging and fallback path, not a
276
+ fully automatic browser-forwarding experience.
56
277
 
57
278
  ## Key Boundary
58
279
 
@@ -6,7 +6,6 @@
6
6
  boxdown start
7
7
  boxdown codex
8
8
  boxdown claude
9
- boxdown cc
10
9
  boxdown opencode
11
10
  boxdown antigravity
12
11
  ```
@@ -16,18 +15,30 @@ boxdown antigravity
16
15
  ```sh
17
16
  --workspace <path>
18
17
  --recreate
18
+ --verbose
19
19
  ```
20
20
 
21
+ Use `boxdown setup` when you want to prepare the devcontainer and SSH/app
22
+ integration without opening an interactive shell.
23
+
21
24
  `boxdown shell` remains supported as an alias for `boxdown start`, but `start`
22
25
  is the canonical command used in help and documentation.
26
+ `boxdown cc` remains supported as an alias for `boxdown claude`, but `claude`
27
+ is the canonical command used in help and documentation.
23
28
 
24
29
  The coding-agent aliases start or reuse the same workspace devcontainer and
25
30
  launch the selected CLI directly:
26
31
 
27
32
  - `boxdown codex` launches `codex`.
28
- - `boxdown claude` and `boxdown cc` launch `claude`.
29
- - `boxdown opencode` launches `opencode`.
30
- - `boxdown antigravity` launches `agy`.
33
+ - `boxdown claude` launches `claude`.
34
+ - `boxdown opencode` installs/updates OpenCode when needed, then launches
35
+ `opencode`.
36
+ - `boxdown antigravity` installs/updates Antigravity when needed, then launches
37
+ `agy`.
38
+
39
+ Container bring-up eagerly installs only Codex and Claude Code. OpenCode and
40
+ Antigravity are lazy installs so projects that do not use them avoid the extra
41
+ disk usage.
31
42
 
32
43
  Pass agent-specific arguments after `--` so Boxdown options stay unambiguous:
33
44
 
@@ -42,7 +53,7 @@ boxdown claude -- --continue
42
53
  3. Generate a Boxdown-owned devcontainer config.
43
54
  4. Install or reuse the pinned Dev Containers CLI.
44
55
  5. Run `devcontainer up --workspace-folder <repo> --override-config <config>`.
45
- 6. Run container lifecycle hooks, including a best-effort coding-agent CLI refresh.
56
+ 6. Run container lifecycle hooks, including a best-effort Codex/Claude CLI refresh.
46
57
  7. Print a dynamic port hint when the configured published port is mapped.
47
58
  8. Run `devcontainer exec ... bash` to open an interactive shell.
48
59
 
@@ -50,6 +61,11 @@ Coding-agent aliases use the same startup flow but skip the port hint, run a
50
61
  best-effort refresh for the selected agent, and exec the agent binary instead of
51
62
  opening `bash`.
52
63
 
64
+ Startup progress is concise by default. Raw Docker, Dev Containers CLI,
65
+ lifecycle hook, and coding-agent install/update logs are captured; on failure,
66
+ Boxdown prints the failed step and a short output tail. Pass `--verbose` to
67
+ stream the full startup output before the interactive shell or agent takes over.
68
+
53
69
  ## Terminal Width
54
70
 
55
71
  Some terminal UIs behave poorly when a host terminal reports an extremely wide
@@ -57,6 +73,12 @@ PTY, especially from embedded terminals. Before opening the shell, Boxdown
57
73
  clamps interactive terminal width to 120 columns when the reported width is
58
74
  larger.
59
75
 
76
+ Before starting the interactive shell or a coding-agent command, Boxdown also
77
+ checks whether the container recognizes the forwarded `TERM` value. If the
78
+ container does not have terminfo for a host-specific terminal such as Ghostty,
79
+ Boxdown falls back to `xterm-256color` while preserving truecolor support with
80
+ `COLORTERM=truecolor`.
81
+
60
82
  Override the default width when you want a wider layout:
61
83
 
62
84
  ```sh
@@ -76,8 +98,26 @@ BOXDOWN_TTY_NORMALIZE=0 boxdown start
76
98
  Use it when changing create-time settings such as image, features, mounts, or
77
99
  Docker `runArgs`.
78
100
 
101
+ Existing containers created before Boxdown adopted runtime-mounted secrets
102
+ must be recreated once. After recreation, Boxdown-provided secrets remain
103
+ ordinary Bash-session environment variables but are absent from Docker
104
+ container configuration and `docker inspect`.
105
+
79
106
  ## Port Hint
80
107
 
81
108
  The v1 asset config publishes container port `3000` with a dynamic host port.
82
109
  After `up`, Boxdown asks Docker for the mapped host binding and prints the
83
110
  result as an HTTP URL when available.
111
+
112
+ For dev servers that choose another port after the container is already running,
113
+ use the SSH tunnel command instead. Interactive terminals can omit `--port` to
114
+ be prompted for the mapping:
115
+
116
+ ```sh
117
+ boxdown tunnel
118
+ boxdown tunnel --port 3030
119
+ boxdown tunnel --workspace /path/to/project --port 3030
120
+ ```
121
+
122
+ This is the preferred path for host browsers and the Codex in-app browser when a
123
+ server is listening on container-local `localhost`.
@@ -0,0 +1,110 @@
1
+ # Default Commit Signing Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make new Boxdown environments sign commits through the host SSH agent by default, while retaining an unsigned, warning-only fallback.
6
+
7
+ **Architecture:** A host-side resolver selects one unambiguous public SSH key and snapshots only that key into workspace state. The generated devcontainer config mounts the host agent and the public-key snapshot. A container bootstrap validates a disposable SSH-signed commit and configures global Git for SSH signing or disables it without touching repository-local Git config.
8
+
9
+ **Tech Stack:** Node.js 24, TypeScript, Bash, Git, OpenSSH, Docker/Dev Containers CLI, Node test runner.
10
+
11
+ ## Global Constraints
12
+
13
+ - Never copy private signing-key material into Boxdown state or the container.
14
+ - Multiple ambiguous agent identities disable signing and warn; never select one arbitrarily.
15
+ - Signing diagnostics are `warn`, never a setup-preflight failure.
16
+ - Never write `commit.gpgsign` in the repository-local `.git/config`.
17
+ - Docker integration is optional and must not pull images or push commits.
18
+
19
+ ---
20
+
21
+ ### Task 1: Resolve and persist a signing plan
22
+
23
+ **Files:**
24
+
25
+ - Create: `src/git-signing.ts`
26
+ - Modify: `src/constants.ts`, `src/paths.ts`, `src/config.ts`
27
+ - Test: `__tests__/app.test.ts`
28
+
29
+ **Interfaces:**
30
+
31
+ ```ts
32
+ export type GitSigningReason = 'agent-unavailable' | 'no-identities' | 'ambiguous-identities' | 'configured-key-not-loaded' | 'agent-mount-unavailable'
33
+ export interface GitSigningPlan { enabled: boolean, reason?: GitSigningReason, publicKey?: string, agentSocketSource?: string }
34
+ export function parseSshPublicKey(value: string): string | undefined
35
+ export function selectGitSigningKey(identities: string[], configuredKey?: string, githubKeys?: string[]): { key?: string, reason?: GitSigningReason }
36
+ export function writeGitSigningPublicKey(context: WorkspaceContext, key: string): void
37
+ ```
38
+
39
+ - [ ] Write focused failing tests for parsing keys without comments, configured-key selection, a unique GitHub-key match, single-key fallback, ambiguous fallback, and config mount generation.
40
+ - [ ] Run `node --import tsx --test --test-name-pattern='git signing|devcontainer config generation' __tests__/app.test.ts` and confirm the new assertions fail because the module and mounts do not exist.
41
+ - [ ] Add state paths and container constants, implement the pure resolver and public-key snapshot, and let `buildGeneratedDevcontainerConfig(context, signingPlan)` add read-only key-state and agent mounts only for enabled plans.
42
+ - [ ] Rerun the focused tests and confirm they pass.
43
+
44
+ ### Task 2: Configure and validate container-side Git signing
45
+
46
+ **Files:**
47
+
48
+ - Create: `assets/devcontainer/utils/git-signing-bootstrap.sh`
49
+ - Modify: `assets/devcontainer/utils/git-config-bootstrap.sh`, `assets/devcontainer/hooks/post-create.sh`, `assets/devcontainer/hooks/post-start.sh`
50
+ - Test: `__tests__/app.test.ts`
51
+
52
+ **Interfaces:**
53
+
54
+ ```bash
55
+ bash /opt/boxdown/devcontainer/utils/git-signing-bootstrap.sh
56
+ # Reads BOXDOWN_GIT_SIGNING_ENABLED and BOXDOWN_GIT_SIGNING_KEY_PATH.
57
+ # Exits 0 in both signed and unsigned fallback modes.
58
+ ```
59
+
60
+ - [ ] Write failing shell-hook tests for successful SSH configuration, absent-agent fallback, removal of incompatible `gpg.program`, and absence of repository-local `commit.gpgsign` writes.
61
+ - [ ] Run the matching `devcontainer git config hooks` tests and confirm the new assertions fail.
62
+ - [ ] Implement a temporary-repository signing probe using `ssh-add -L`, a temporary allowed-signers file, and `git commit --allow-empty`; set global SSH signing only after it succeeds, otherwise set global `commit.gpgsign=false` and print a warning.
63
+ - [ ] Remove unconditional global signing disablement and all post-create local `commit.gpgsign` mutations; call the bootstrap after create and start.
64
+ - [ ] Rerun the hook tests and confirm they pass.
65
+
66
+ ### Task 3: Use one signing-aware lifecycle for every command
67
+
68
+ **Files:**
69
+
70
+ - Modify: `src/devcontainer.ts`, `src/main.ts`
71
+ - Test: `__tests__/app.test.ts`
72
+
73
+ **Interfaces:**
74
+
75
+ ```ts
76
+ export async function resolveGitSigningPlan(context: WorkspaceContext): Promise<GitSigningPlan>
77
+ export async function ensureContainerGitSigning(context: WorkspaceContext, plan: GitSigningPlan, options?: ContainerCommandOptions): Promise<void>
78
+ ```
79
+
80
+ - [ ] Write failing command-flow tests proving `startDevcontainer` writes a signing-aware config and invokes the non-fatal container signing refresh for fresh and reused containers.
81
+ - [ ] Run the relevant `CLI execution` and `coding-agent command mapping` tests and confirm they fail on the missing signing lifecycle.
82
+ - [ ] Resolve the plan before writing generated config, pass plan state to the lifecycle script, and call the refresh from shared startup so setup, shell, coding agents, SSH proxy, and tunnels all inherit it.
83
+ - [ ] Ensure refresh failures only warn and do not change the command exit code.
84
+ - [ ] Rerun focused lifecycle tests and confirm they pass.
85
+
86
+ ### Task 4: Add doctor/preflight warnings and user documentation
87
+
88
+ **Files:**
89
+
90
+ - Modify: `src/doctor.ts`, `src/main.ts`, `README.md`, `docs/features/setup.md`, `docs/architecture.md`, `docs/features/README.md`
91
+ - Create: `docs/features/commit-signing.md`, `.changeset/<generated-name>.md`
92
+ - Test: `__tests__/app.test.ts`
93
+
94
+ - [ ] Write failing doctor tests for unavailable agent, ambiguous identities, unusable mount, missing GitHub signing registration, and warning-only setup behavior.
95
+ - [ ] Run the `doctor output` and setup-preflight tests and confirm the new checks are absent.
96
+ - [ ] Add warning-only host signing checks, including an optional GitHub signing-key query; do not add account mutations or required preflight failures.
97
+ - [ ] Document automatic selection order, unsigned fallback, Docker mount limitations, agent security exposure, and GitHub's one-time separate signing-key registration.
98
+ - [ ] Add a changeset for default-on best-effort commit signing.
99
+ - [ ] Rerun focused doctor/setup tests and lint changed Markdown.
100
+
101
+ ### Task 5: Verify the full feature
102
+
103
+ **Files:**
104
+
105
+ - Test: `__tests__/app.test.ts`
106
+
107
+ - [ ] Run `node --import tsx --test __tests__/**/*.test.ts`; record the two known unrelated baseline failures and require no additional failures.
108
+ - [ ] Run `npm run build` and `npm run lint` using local project tooling where available.
109
+ - [ ] With Docker and a loaded agent available, create a disposable container using the generated mount, make an empty signed commit, and verify its SSH fingerprint; do not push it.
110
+ - [ ] Review `git diff --check`, changed docs, and the implementation against `docs/superpowers/specs/2026-07-11-default-commit-signing-design.md`.
@@ -0,0 +1,125 @@
1
+ # Progress CI Regression Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Restore CI by making progress checklist cleanup mode-independent and updating stale spinner-label source assertions.
6
+
7
+ **Architecture:** Preserve the existing `ProgressReporter` API and output behavior. Move only internal cleanup ahead of rendering guards, then loosen two source-presence assertions while leaving runtime checklist-output coverage intact.
8
+
9
+ **Tech Stack:** TypeScript, Node.js 24 test runner, `node:assert`, pnpm, ESLint, tsdown
10
+
11
+ ## Global Constraints
12
+
13
+ - Do not change progress APIs, spinner copy, output formatting, or command flow.
14
+ - Use Node.js 24 for tests because the current Node.js 26 host runtime is incompatible with an installed CommonJS test dependency.
15
+ - Commit documentation separately from the implementation, as explicitly requested.
16
+
17
+ ---
18
+
19
+ ### Task 1: Commit the approved documentation
20
+
21
+ **Files:**
22
+
23
+ - Create: `docs/superpowers/specs/2026-07-11-progress-ci-regression-design.md`
24
+ - Create: `docs/superpowers/plans/2026-07-11-progress-ci-regression.md`
25
+
26
+ **Interfaces:**
27
+
28
+ - Consumes: The user-approved diagnosis and design.
29
+ - Produces: Durable design and execution context for the CI fix.
30
+
31
+ - [ ] **Step 1: Verify documentation formatting**
32
+
33
+ Run:
34
+
35
+ ```bash
36
+ git diff --check
37
+ fnm exec --using v24.15.0 node ../../node_modules/markdownlint-cli/markdownlint.js \
38
+ -c .github/.markdownlint.yml -i 'apm_modules/**' -i '.git' -i '__tests__' \
39
+ -i '.github' -i '.changeset' -i 'CODE_OF_CONDUCT.md' -i 'CHANGELOG.md' \
40
+ -i 'node_modules' -i 'dist' '**/**.md'
41
+ ```
42
+
43
+ Expected: PASS with no whitespace or Markdown lint errors.
44
+
45
+ - [ ] **Step 2: Commit documentation only**
46
+
47
+ ```bash
48
+ git add docs/superpowers/specs/2026-07-11-progress-ci-regression-design.md docs/superpowers/plans/2026-07-11-progress-ci-regression.md
49
+ git commit -m "docs: design progress CI regression fix"
50
+ ```
51
+
52
+ Expected: A commit containing only the design specification and implementation plan.
53
+
54
+ ### Task 2: Fix progress cleanup and stale assertions
55
+
56
+ **Files:**
57
+
58
+ - Modify: `src/progress.ts:147`
59
+ - Test: `__tests__/app.test.ts:3080`
60
+ - Test: `__tests__/app.test.ts:3427`
61
+
62
+ **Interfaces:**
63
+
64
+ - Consumes: `ProgressReporter.end()` and `ProgressReporter.isChecklistActive()`.
65
+ - Produces: Mode-independent reporter cleanup without API changes.
66
+
67
+ - [ ] **Step 1: Verify the existing tests reproduce both CI failures**
68
+
69
+ Run:
70
+
71
+ ```bash
72
+ fnm exec --using v24.15.0 node --import tsx --test \
73
+ --test-name-pattern='reports whether a checklist is active|hidden command helpers use friendly spinner labels' \
74
+ __tests__/app.test.ts
75
+ ```
76
+
77
+ Expected: FAIL because checklist state remains active in `none` mode and the SSH label regular expressions require an outdated direct-property expression.
78
+
79
+ - [ ] **Step 2: Make cleanup independent of rendering mode**
80
+
81
+ In `ProgressReporter.end()`, clear internal state immediately after stopping timers:
82
+
83
+ ```ts
84
+ this.#steps = []
85
+ this.#renderedStepLineCount = 0
86
+ ```
87
+
88
+ Remove the existing duplicate cleanup after the interactive section terminator.
89
+
90
+ - [ ] **Step 3: Make the source-presence assertions expression-shape independent**
91
+
92
+ Replace the two SSH assertions with:
93
+
94
+ ```ts
95
+ assert.match(sshKeySource, /Generating Boxdown SSH identity/)
96
+ assert.match(sshKeySource, /Writing Boxdown SSH public key/)
97
+ ```
98
+
99
+ - [ ] **Step 4: Verify the focused regression tests pass**
100
+
101
+ Run the Step 1 command again.
102
+
103
+ Expected: PASS for both selected tests.
104
+
105
+ - [ ] **Step 5: Run full verification**
106
+
107
+ Run:
108
+
109
+ ```bash
110
+ fnm exec --using v24.15.0 node ../../node_modules/c8/bin/c8.js node --import tsx --test __tests__/**/*.test.ts
111
+ fnm exec --using v24.15.0 node ../../node_modules/eslint/bin/eslint.js .
112
+ fnm exec --using v24.15.0 node ../../node_modules/typescript/bin/tsc
113
+ fnm exec --using v24.15.0 node ../../node_modules/tsdown/dist/run.mjs
114
+ ```
115
+
116
+ Expected: All tests pass, ESLint reports no errors, TypeScript compilation succeeds, and tsdown builds successfully.
117
+
118
+ - [ ] **Step 6: Commit the implementation separately**
119
+
120
+ ```bash
121
+ git add src/progress.ts __tests__/app.test.ts
122
+ git commit -m "fix(progress): clear checklist state in every mode"
123
+ ```
124
+
125
+ Expected: A second commit containing only the production fix and updated assertions.