boxdown 1.2.1 → 1.3.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 (32) hide show
  1. package/README.md +10 -0
  2. package/assets/devcontainer/README.md +12 -1
  3. package/assets/devcontainer/devcontainer.json +1 -1
  4. package/assets/devcontainer/hooks/post-create.sh +12 -1
  5. package/dist/bin/cli.cjs +1 -1
  6. package/dist/bin/cli.mjs +1 -1
  7. package/dist/{main-BDgyf2t5.cjs → main-CT2n9qcb.cjs} +448 -67
  8. package/dist/{main-J4_2Up3o.mjs → main-O__JaiXe.mjs} +431 -68
  9. package/dist/main-O__JaiXe.mjs.map +1 -0
  10. package/dist/main.cjs +4 -1
  11. package/dist/main.d.cts +92 -26
  12. package/dist/main.d.cts.map +1 -1
  13. package/dist/main.d.mts +92 -26
  14. package/dist/main.d.mts.map +1 -1
  15. package/dist/main.mjs +2 -2
  16. package/docs/features/lifecycle.md +13 -0
  17. package/docs/features/setup.md +5 -0
  18. package/docs/features/start-and-shell.md +5 -0
  19. package/docs/superpowers/plans/2026-07-18-architecture-aware-1password-installer.md +163 -0
  20. package/docs/superpowers/plans/2026-07-18-devcontainer-node-image-digest.md +341 -0
  21. package/docs/superpowers/plans/2026-07-19-container-runtime-readiness.md +1536 -0
  22. package/docs/superpowers/specs/2026-07-18-architecture-aware-1password-installer-design.md +63 -0
  23. package/docs/superpowers/specs/2026-07-18-devcontainer-node-image-digest-design.md +108 -0
  24. package/docs/superpowers/specs/2026-07-19-container-runtime-readiness-design.md +353 -0
  25. package/package.json +1 -1
  26. package/src/container-runtime.ts +295 -0
  27. package/src/devcontainer.ts +20 -4
  28. package/src/doctor.ts +48 -22
  29. package/src/main.ts +95 -11
  30. package/src/process.ts +50 -10
  31. package/src/progress.ts +63 -8
  32. package/dist/main-J4_2Up3o.mjs.map +0 -1
@@ -0,0 +1,163 @@
1
+ # Architecture-Aware 1Password Installer 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:** Install the pinned prebuilt 1Password CLI archive for the current Linux architecture and exercise amd64, arm64, and unsupported paths in CI.
6
+
7
+ **Architecture:** Keep architecture selection inside the existing `install_1password_cli` shell function. Behavioral Node tests source the hook and replace only external commands, so CI runs the real selection logic without network access, privileged writes, Buildx, QEMU, or source builds.
8
+
9
+ **Tech Stack:** Bash, Node.js test runner, TypeScript, `node:assert`, pnpm
10
+
11
+ ## Global Constraints
12
+
13
+ - Keep 1Password CLI pinned to version `2.32.1`.
14
+ - Fetch the prebuilt Linux zip; do not build 1Password from source.
15
+ - Support `x86_64|amd64` with the `amd64` archive and `aarch64|arm64` with the `arm64` archive.
16
+ - Warn and return successfully without downloading on unsupported architectures.
17
+ - Do not change the CI runner matrix, Snyk installer, devcontainer image, or unrelated lifecycle behavior.
18
+
19
+ ---
20
+
21
+ ### Task 1: Select and Test the 1Password Archive Architecture
22
+
23
+ **Files:**
24
+
25
+ - Modify: `assets/devcontainer/hooks/post-create.sh:112-117`
26
+ - Test: `__tests__/app.test.ts` near the existing post-create hook tests
27
+
28
+ **Interfaces:**
29
+
30
+ - Consumes: `install_1password_cli()` and the machine name returned by `uname -m`.
31
+ - Produces: A download request for `op_linux_<amd64|arm64>_v2.32.1.zip`, or a successful warning-only skip for an unsupported machine name.
32
+
33
+ - [ ] **Step 1: Add a shell smoke-test helper and failing architecture tests**
34
+
35
+ Add this helper near the existing `tempDir` test helper:
36
+
37
+ ```ts
38
+ function runOnePasswordInstallForArchitecture (arch: string): { status: number | null, stderr: string, downloads: string } {
39
+ const postCreatePath = join(assetsDevcontainerDir, 'hooks', 'post-create.sh')
40
+ const curlLogPath = join(tempDir('onepassword-curl-log'), 'calls.log')
41
+ const script = [
42
+ 'source "$1"',
43
+ 'uname() { printf "%s\\n" "$BOXDOWN_TEST_ARCH"; }',
44
+ 'curl() { printf "%s\\n" "$*" >> "$BOXDOWN_TEST_CURL_LOG"; }',
45
+ 'python3() { :; }',
46
+ 'sudo() { :; }',
47
+ 'chmod() { :; }',
48
+ 'rm() { :; }',
49
+ 'install_1password_cli'
50
+ ].join('\n')
51
+ const result = spawnSync('bash', ['-c', script, 'bash', postCreatePath], {
52
+ encoding: 'utf8',
53
+ env: {
54
+ ...process.env,
55
+ BOXDOWN_TEST_ARCH: arch,
56
+ BOXDOWN_TEST_CURL_LOG: curlLogPath
57
+ }
58
+ })
59
+
60
+ return {
61
+ status: result.status,
62
+ stderr: result.stderr,
63
+ downloads: existsSync(curlLogPath) ? readFileSync(curlLogPath, 'utf8') : ''
64
+ }
65
+ }
66
+ ```
67
+
68
+ Add these focused tests near `post-create local git config is idempotent with multiple GitHub helpers`:
69
+
70
+ ```ts
71
+ test('1Password installer selects the amd64 archive on x86_64', () => {
72
+ const result = runOnePasswordInstallForArchitecture('x86_64')
73
+
74
+ assert.strictEqual(result.status, 0)
75
+ assert.match(result.downloads, /op_linux_amd64_v2\.32\.1\.zip/)
76
+ assert.doesNotMatch(result.downloads, /op_linux_arm64/)
77
+ })
78
+
79
+ test('1Password installer selects the arm64 archive on aarch64', () => {
80
+ const result = runOnePasswordInstallForArchitecture('aarch64')
81
+
82
+ assert.strictEqual(result.status, 0)
83
+ assert.match(result.downloads, /op_linux_arm64_v2\.32\.1\.zip/)
84
+ assert.doesNotMatch(result.downloads, /op_linux_amd64/)
85
+ })
86
+
87
+ test('1Password installer skips unsupported architectures', () => {
88
+ const result = runOnePasswordInstallForArchitecture('riscv64')
89
+
90
+ assert.strictEqual(result.status, 0)
91
+ assert.strictEqual(result.downloads, '')
92
+ assert.match(result.stderr, /skipping 1Password CLI \(unsupported arch: riscv64\)/)
93
+ })
94
+ ```
95
+
96
+ - [ ] **Step 2: Run the focused tests and verify the red phase**
97
+
98
+ Run:
99
+
100
+ ```bash
101
+ pnpm exec c8 --clean=false node --import tsx --test \
102
+ --test-name-pattern='1Password installer' __tests__/app.test.ts
103
+ ```
104
+
105
+ Expected: FAIL. The amd64 test records the hard-coded arm64 URL, and the unsupported-architecture test records a download instead of a warning-only skip. The existing arm64 behavior may already pass.
106
+
107
+ - [ ] **Step 3: Implement architecture-aware archive selection**
108
+
109
+ Replace `install_1password_cli` with:
110
+
111
+ ```bash
112
+ install_1password_cli() {
113
+ local op_version="2.32.1"
114
+ local op_arch
115
+
116
+ case "$(uname -m)" in
117
+ aarch64 | arm64) op_arch="arm64" ;;
118
+ x86_64 | amd64) op_arch="amd64" ;;
119
+ *)
120
+ echo "post-create: skipping 1Password CLI (unsupported arch: $(uname -m))" >&2
121
+ return 0
122
+ ;;
123
+ esac
124
+
125
+ curl -fsSL "https://cache.agilebits.com/dist/1P/op2/pkg/v${op_version}/op_linux_${op_arch}_v${op_version}.zip" -o /tmp/op.zip
126
+ python3 -c "import zipfile; zipfile.ZipFile('/tmp/op.zip').extract('op', '/tmp')"
127
+ sudo mv /tmp/op /usr/local/bin/op && chmod +x /usr/local/bin/op && rm /tmp/op.zip
128
+ }
129
+ ```
130
+
131
+ - [ ] **Step 4: Rerun the focused tests and shell syntax check**
132
+
133
+ Run:
134
+
135
+ ```bash
136
+ pnpm exec c8 --clean=false node --import tsx --test \
137
+ --test-name-pattern='1Password installer' __tests__/app.test.ts
138
+ bash -n assets/devcontainer/hooks/post-create.sh
139
+ ```
140
+
141
+ Expected: All three focused tests pass and Bash reports no syntax errors.
142
+
143
+ - [ ] **Step 5: Run full repository verification**
144
+
145
+ Run:
146
+
147
+ ```bash
148
+ pnpm run test
149
+ pnpm run lint
150
+ pnpm run build
151
+ git diff --check
152
+ ```
153
+
154
+ Expected: The full test suite, lint, and build pass; the diff check produces no output.
155
+
156
+ - [ ] **Step 6: Commit the implementation**
157
+
158
+ ```bash
159
+ git add assets/devcontainer/hooks/post-create.sh __tests__/app.test.ts
160
+ git commit -m "fix: select 1password installer architecture"
161
+ ```
162
+
163
+ Expected: One implementation commit containing the architecture selector and its behavioral smoke tests.
@@ -0,0 +1,341 @@
1
+ # Devcontainer Node Image Digest 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:** Pin Boxdown's packaged Node 24 devcontainer image to its verified multi-platform index digest and configure Renovate to refresh that digest through monthly pull requests.
6
+
7
+ **Architecture:** Keep the existing `node:24-trixie-slim` update track but append the immutable OCI index digest resolved from Docker Hub. Add a narrowly scoped Renovate configuration that enables only the devcontainer manager, covers Boxdown's nonstandard packaged-template path, disables Feature updates, and permits only the Node image digest rule.
8
+
9
+ **Tech Stack:** Dev Container JSONC, Renovate JSON, Node.js built-in test runner, TypeScript, Docker Buildx registry inspection, Markdown.
10
+
11
+ ## Global Constraints
12
+
13
+ - The image tag remains exactly `node:24-trixie-slim`.
14
+ - The pinned digest is the multi-platform OCI index digest `sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573` resolved on 2026-07-18.
15
+ - Renovate manages only the packaged devcontainer and does not overlap with Dependabot's npm or GitHub Actions responsibilities.
16
+ - Dev Container Feature updates remain disabled in Renovate.
17
+ - Renovate uses `exact` versioning for the Node image so only its digest can change; the `24-trixie-slim` tag cannot be upgraded.
18
+ - Node image digest updates are eligible only on the first day of each month before 04:00 UTC.
19
+ - The Renovate GitHub app is an external operational prerequisite; no repository token or scheduled workflow is added.
20
+
21
+ ---
22
+
23
+ ### Task 1: Enforce and Pin the Multi-Platform Node Image
24
+
25
+ **Files:**
26
+
27
+ - Create: `__tests__/devcontainer-image-policy.test.ts`
28
+ - Modify: `assets/devcontainer/devcontainer.json:3`
29
+
30
+ **Interfaces:**
31
+
32
+ - Consumes: `parseJsonc<T>(input: string): T` from `src/jsonc.ts`.
33
+ - Produces: a static repository invariant requiring the exact Node 24 trixie slim tag plus a 64-hex-character SHA-256 digest.
34
+
35
+ - [ ] **Step 1: Write the failing image-policy test**
36
+
37
+ ```ts
38
+ import assert from 'node:assert'
39
+ import { readFileSync } from 'node:fs'
40
+ import { fileURLToPath } from 'node:url'
41
+ import { test } from 'node:test'
42
+
43
+ import { parseJsonc } from '../src/jsonc.ts'
44
+
45
+ const devcontainerPath = fileURLToPath(new URL('../assets/devcontainer/devcontainer.json', import.meta.url))
46
+
47
+ test('pins the packaged Node 24 devcontainer image to a SHA-256 digest', () => {
48
+ const devcontainer = parseJsonc<{ image: string }>(readFileSync(devcontainerPath, 'utf8'))
49
+
50
+ assert.match(devcontainer.image, /^node:24-trixie-slim@sha256:[a-f0-9]{64}$/)
51
+ })
52
+ ```
53
+
54
+ - [ ] **Step 2: Run the focused test and verify RED**
55
+
56
+ Run:
57
+
58
+ ```bash
59
+ node --test __tests__/devcontainer-image-policy.test.ts
60
+ ```
61
+
62
+ Expected: FAIL because `node:24-trixie-slim` does not include `@sha256:` and a digest.
63
+
64
+ - [ ] **Step 3: Pin the verified OCI index digest**
65
+
66
+ Change the image property in `assets/devcontainer/devcontainer.json` to:
67
+
68
+ ```jsonc
69
+ "image": "node:24-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573",
70
+ ```
71
+
72
+ - [ ] **Step 4: Run the focused test and verify GREEN**
73
+
74
+ Run:
75
+
76
+ ```bash
77
+ node --test __tests__/devcontainer-image-policy.test.ts
78
+ ```
79
+
80
+ Expected: PASS with 1 test and 0 failures.
81
+
82
+ - [ ] **Step 5: Reinspect the registry reference**
83
+
84
+ Run:
85
+
86
+ ```bash
87
+ docker buildx imagetools inspect node:24-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573
88
+ ```
89
+
90
+ Expected: `MediaType: application/vnd.oci.image.index.v1+json`, the same top-level digest, and platform manifests including `linux/amd64` and `linux/arm64/v8`.
91
+
92
+ - [ ] **Step 6: Commit the image pin**
93
+
94
+ ```bash
95
+ git add __tests__/devcontainer-image-policy.test.ts assets/devcontainer/devcontainer.json
96
+ git commit -m "chore: pin devcontainer Node image digest"
97
+ ```
98
+
99
+ ### Task 2: Scope Renovate to Monthly Node Image Digest Updates
100
+
101
+ **Files:**
102
+
103
+ - Modify: `__tests__/devcontainer-image-policy.test.ts`
104
+ - Create: `renovate.json`
105
+
106
+ **Interfaces:**
107
+
108
+ - Consumes: the `devcontainer` manager's `image` and `feature` dependency types and the packaged template path from Task 1.
109
+ - Produces: Renovate configuration restricted to the Node image digest rule, with Feature management disabled and a monthly UTC schedule.
110
+
111
+ - [ ] **Step 1: Add the failing Renovate-policy test**
112
+
113
+ Append the following declarations and test to `__tests__/devcontainer-image-policy.test.ts`, adding `existsSync` to the existing `node:fs` import:
114
+
115
+ ```ts
116
+ const renovatePath = fileURLToPath(new URL('../renovate.json', import.meta.url))
117
+
118
+ interface RenovatePackageRule {
119
+ description?: string
120
+ matchManagers?: string[]
121
+ matchDepTypes?: string[]
122
+ matchPackageNames?: string[]
123
+ matchFileNames?: string[]
124
+ versioning?: string
125
+ pinDigests?: boolean
126
+ schedule?: string[]
127
+ enabled?: boolean
128
+ }
129
+
130
+ interface RenovateConfig {
131
+ enabledManagers?: string[]
132
+ devcontainer?: {
133
+ managerFilePatterns?: string[]
134
+ }
135
+ packageRules?: RenovatePackageRule[]
136
+ }
137
+
138
+ test('scopes Renovate to monthly packaged Node image digest updates', () => {
139
+ assert.equal(existsSync(renovatePath), true, 'renovate.json must exist')
140
+
141
+ const renovate = JSON.parse(readFileSync(renovatePath, 'utf8')) as RenovateConfig
142
+ assert.deepEqual(renovate.enabledManagers, ['devcontainer'])
143
+ assert.deepEqual(renovate.devcontainer?.managerFilePatterns, [
144
+ '/^assets\\/devcontainer\\/devcontainer\\.json$/'
145
+ ])
146
+
147
+ const featureRule = renovate.packageRules?.find(rule => rule.matchDepTypes?.includes('feature'))
148
+ assert.deepEqual(featureRule?.matchManagers, ['devcontainer'])
149
+ assert.equal(featureRule?.enabled, false)
150
+
151
+ const imageRule = renovate.packageRules?.find(rule => rule.matchDepTypes?.includes('image'))
152
+ assert.deepEqual(imageRule?.matchManagers, ['devcontainer'])
153
+ assert.deepEqual(imageRule?.matchPackageNames, ['node'])
154
+ assert.deepEqual(imageRule?.matchFileNames, ['assets/devcontainer/devcontainer.json'])
155
+ assert.equal(imageRule?.versioning, 'exact')
156
+ assert.equal(imageRule?.pinDigests, true)
157
+ assert.deepEqual(imageRule?.schedule, ['* 0-3 1 * *'])
158
+ })
159
+ ```
160
+
161
+ - [ ] **Step 2: Run the focused test and verify RED**
162
+
163
+ Run:
164
+
165
+ ```bash
166
+ node --test __tests__/devcontainer-image-policy.test.ts
167
+ ```
168
+
169
+ Expected: the image-policy test passes and the Renovate-policy test fails with `renovate.json must exist`.
170
+
171
+ - [ ] **Step 3: Add the scoped Renovate configuration**
172
+
173
+ Create `renovate.json` with:
174
+
175
+ ```json
176
+ {
177
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
178
+ "enabledManagers": [
179
+ "devcontainer"
180
+ ],
181
+ "devcontainer": {
182
+ "managerFilePatterns": [
183
+ "/^assets\\/devcontainer\\/devcontainer\\.json$/"
184
+ ]
185
+ },
186
+ "packageRules": [
187
+ {
188
+ "description": "Keep digest-pinned Dev Container Features outside Renovate",
189
+ "matchManagers": [
190
+ "devcontainer"
191
+ ],
192
+ "matchDepTypes": [
193
+ "feature"
194
+ ],
195
+ "enabled": false
196
+ },
197
+ {
198
+ "description": "Refresh the Boxdown Node 24 devcontainer image digest monthly",
199
+ "matchManagers": [
200
+ "devcontainer"
201
+ ],
202
+ "matchDepTypes": [
203
+ "image"
204
+ ],
205
+ "matchPackageNames": [
206
+ "node"
207
+ ],
208
+ "matchFileNames": [
209
+ "assets/devcontainer/devcontainer.json"
210
+ ],
211
+ "versioning": "exact",
212
+ "pinDigests": true,
213
+ "schedule": [
214
+ "* 0-3 1 * *"
215
+ ]
216
+ }
217
+ ]
218
+ }
219
+ ```
220
+
221
+ - [ ] **Step 4: Run the focused test and verify GREEN**
222
+
223
+ Run:
224
+
225
+ ```bash
226
+ node --test __tests__/devcontainer-image-policy.test.ts
227
+ ```
228
+
229
+ Expected: PASS with 2 tests and 0 failures.
230
+
231
+ - [ ] **Step 5: Validate the Renovate JSON and schema URL**
232
+
233
+ Run:
234
+
235
+ ```bash
236
+ node -e "const fs=require('node:fs'); const config=JSON.parse(fs.readFileSync('renovate.json','utf8')); if (config['$schema'] !== 'https://docs.renovatebot.com/renovate-schema.json') process.exit(1)"
237
+ ```
238
+
239
+ Expected: exit 0 with no output.
240
+
241
+ - [ ] **Step 6: Commit the automation policy**
242
+
243
+ ```bash
244
+ git add __tests__/devcontainer-image-policy.test.ts renovate.json
245
+ git commit -m "chore: automate devcontainer image digest updates"
246
+ ```
247
+
248
+ ### Task 3: Document Reproducibility and Verify the Complete Change
249
+
250
+ **Files:**
251
+
252
+ - Modify: `assets/devcontainer/README.md:25-45`
253
+
254
+ **Interfaces:**
255
+
256
+ - Consumes: the pinned index digest and Renovate scope delivered by Tasks 1 and 2.
257
+ - Produces: contributor-facing explanation of the immutable cross-platform image reference and automated PR update path.
258
+
259
+ - [ ] **Step 1: Update the Base image documentation**
260
+
261
+ Replace the first paragraph under `## Base image` with:
262
+
263
+ ```markdown
264
+ Boxdown uses `node:24-trixie-slim` as the base-image update track to keep the
265
+ shared image smaller than the full Dev Containers TypeScript/Node image. The
266
+ template appends the upstream multi-platform OCI index digest, making rebuilds
267
+ immutable while allowing AMD64 and ARM64 hosts to select the matching platform
268
+ manifest from the same pinned release set. Renovate checks the packaged
269
+ template monthly and opens a pull request when that tag's index digest changes,
270
+ so base-image updates remain explicit and auditable.
271
+
272
+ The devcontainer then installs the required operating-system tools through
273
+ pinned Dev Container features. `common-utils` and `git` run first so later
274
+ features and lifecycle hooks can rely on shell basics, `sudo`, package
275
+ metadata, Git, and related utilities.
276
+ ```
277
+
278
+ - [ ] **Step 2: Run focused policy and documentation checks**
279
+
280
+ Run:
281
+
282
+ ```bash
283
+ node --test __tests__/devcontainer-image-policy.test.ts
284
+ ./node_modules/.bin/markdownlint -c .github/.markdownlint.yml assets/devcontainer/README.md docs/superpowers/plans/2026-07-18-devcontainer-node-image-digest.md
285
+ git diff --check
286
+ ```
287
+
288
+ Expected: 2 tests pass, Markdown lint exits 0, and `git diff --check` exits 0.
289
+
290
+ - [ ] **Step 3: Run the full test suite outside the socket-restricted sandbox**
291
+
292
+ Run:
293
+
294
+ ```bash
295
+ node --test __tests__/**/*.test.ts
296
+ ```
297
+
298
+ Expected: 203 tests pass with 0 failures. This uses Node's built-in TypeScript support because the host Node 26 runtime is incompatible with the installed `c8`/`tsx` execution path; the test contents are unchanged.
299
+
300
+ - [ ] **Step 4: Run lint and build using installed binaries**
301
+
302
+ Run:
303
+
304
+ ```bash
305
+ ./node_modules/.bin/eslint .
306
+ ./node_modules/.bin/markdownlint -c .github/.markdownlint.yml -i 'apm_modules/**' -i '.git' -i '__tests__' -i '.github' -i '.changeset' -i 'CODE_OF_CONDUCT.md' -i 'CHANGELOG.md' -i 'node_modules' -i 'dist' '**/**.md'
307
+ ./node_modules/.bin/tsc
308
+ ./node_modules/.bin/tsdown
309
+ ```
310
+
311
+ Expected: every command exits 0. `tsdown` reports successful ESM and CJS builds.
312
+
313
+ - [ ] **Step 5: Reverify the pinned registry object and final diff**
314
+
315
+ Run:
316
+
317
+ ```bash
318
+ docker buildx imagetools inspect node:24-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573
319
+ git diff --check
320
+ git status --short
321
+ ```
322
+
323
+ Expected: the registry object is an OCI image index with AMD64 and ARM64 manifests, whitespace validation exits 0, and status lists only the planned documentation change before its commit.
324
+
325
+ - [ ] **Step 6: Commit the documentation**
326
+
327
+ ```bash
328
+ git add assets/devcontainer/README.md
329
+ git commit -m "docs: explain devcontainer image digest updates"
330
+ ```
331
+
332
+ - [ ] **Step 7: Confirm branch state**
333
+
334
+ Run:
335
+
336
+ ```bash
337
+ git status --short --branch
338
+ git log --oneline --decorate -4
339
+ ```
340
+
341
+ Expected: a clean `work/devcontainer-node-digest` branch with the implementation-plan commit followed by the three implementation commits.