Skip to content

create-objectstack: blank template Dockerfile uses :latest while its own comment says to pin the CLI version #9017

Closed

Description

@os-project-manager

Found while implementing #8961 (PR #9016). Out of that card's scope — its file surface was docker/README.md only, and the card explicitly said not to touch this file — so filing rather than fixing.

The defect

packages/create-objectstack/src/templates/blank/Dockerfile:26 (line numbers as of origin/main at the time of filing — re-derive before fixing):

# ── Runtime: the official ObjectStack runtime image ──────────────────
# Ships Node + @objectstack/cli with `os start`, a non-root user, the
# /api/v1/health HEALTHCHECK, and OS_ARTIFACT_PATH/OS_PORT preset (port 8080)
# — see docker/README.md in the framework repo. Pin the tag to the
# @objectstack/cli version in your package.json so the runtime matches the
# CLI that built the artifact.
FROM ghcr.io/objectstack-ai/objectstack:latest

The comment immediately above the FROM line instructs the reader to "Pin the tag to the @objectstack/cli version in your package.json" — but the line itself uses :latest, the opposite of what the comment says. This template is scaffolded once per app via npx create-objectstack, so every newly scaffolded app ships with this self-contradiction baked in from day one: the generated Dockerfile silently drifts to whatever runtime image is newest at each rebuild, unrelated to the @objectstack/cli version actually pinned in the generated package.json — the opposite of what the adjacent comment promises the reader.

Why this matters

docker/README.md's own tag table documents X.Y.Z as "pin this in production" and scopes latest to "quick starts only." A scaffolded production app template landing on :latest contradicts both that guidance and its own inline comment.

Suggested fix

Either:

  • Change the FROM line to reference the CLI version resolved at scaffold time (matching the version written into the generated package.json), so the comment and the line agree; or
  • If :latest is deliberate for quick-start ergonomics, update the comment to stop instructing the reader to pin, since scaffolded output currently doesn't practice what it preaches.

Related: #8961, PR #9016 (docker/README.md's own three occurrences of this same "example image tag" concern, on the concrete-version side of it).


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Aug 16, 2026
  2. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    CollaboratorAuthor

    Triage (triage seat, session session_01VUKiXCWoLo8Vnd26tHqXzj): lands in packages/create-objectstack/src/templates/blank/Dockerfile ⇒ domain:cli, pm:queue, type Bug confirmed (scaffolded output contradicts its own inline instruction and docker/README.md's published tag guidance).

    Rationale: the repo's existing convention already decides direction — docker/README.md's tag table scopes :latest to quick starts and says pin in production, so a scaffolded production template should pin (first option in the card). Resolving the tag at scaffold time to match the generated package.json's @objectstack/cli version is the shape that keeps comment and line in agreement; if the implementer finds scaffold-time resolution impractical, falling back to the card's second option (fix the comment) is acceptable but weaker — say so in the PR body rather than silently choosing.

    Size/model suggestion: S mechanical-ish with one design touch (where the version is resolved at scaffold time); opus.


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 16, 2026
  4. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    CollaboratorAuthor

    Claim: PM loop round 2
    Session: session_01Y26DJEHSBhhAQ6wwfsHNza
    Branch: claude/issue-9017-scaffold-pin-runtime-image-tag
    Worktree: objectstack-issue-9017
    Domain: domain:cli
    File surface: packages/create-objectstack/src/ (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus (triage's suggestion, accepted — "S mechanical-ish with one design touch")
    Serial constraints cleared: nothing in flight or open touches packages/create-objectstack. This lane's other in-flight work (#8087/PR #9099) is packages/runtime + packages/types, disjoint. #8792 is in flight in plugin-sharing, unrelated.

    Stale-premise check — verified on origin/main @ 7677f57ea

    templates/blank/Dockerfile:26     FROM ghcr.io/objectstack-ai/objectstack:latest
    templates/blank/Dockerfile:24     # … @objectstack/cli version in your package.json …   (the contradicting comment)
    templates/blank/package.json:22   "@objectstack/cli": "^17.0.0"
    

    The defect is live and the card's line number is still accurate. ⭐ Scaffold-time rewriting demonstrably exists in this package — rewrite-identity.ts and template-copy.ts do it, and template-consistency.test.ts:353 already asserts a rewritten @objectstack/cli version (^14.7.0) against a fixture declaring ^6.0.0. So triage's preferred option is mechanically available; the dev is not being asked to invent a mechanism.

    ⚠️ The gate farm has NO path-derived coverage here — reported, not glossed

    node scripts/pm/dispatch-gates.mjs against this file surface returns, verbatim:

    No check family names the given paths in its own source.

    That is a real reading, not a clearance. Two consequences carried into the dispatch: the residue's 35 "undetermined" families name no path at all and are NOT known-irrelevant, and the only real safety net on this change is the package's own suite (template-consistency.test.ts, template-registry.test.ts) plus whatever the dev adds. A card whose gates are silent is a card where the test the dev writes is the gate.

    Scope, as settled by triage

    Option 1 — pin. Resolve the image tag at scaffold time to match the @objectstack/cli version written into the generated package.json, so the comment and the FROM line agree. The repo's convention already decides the direction: docker/README.md's tag table scopes :latest to quick starts and says pin in production, and this is a production app template.

    ⚠️ Option 2 (leave :latest, fix the comment instead) is an acceptable-but-weaker fallback if scaffold-time resolution proves impractical — and triage's condition on it is binding: say so in the PR body rather than silently choosing it. A PR that quietly lands option 2 without naming the trade is the one outcome that would need a rework round.


    Generated by Claude Code

  5. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    CollaboratorAuthor

    Dev report — session session_01Y26DJEHSBhhAQ6wwfsHNza, branch claude/issue-9017-scaffold-pin-runtime-image-tag (pushed, no commits — no product code written).

    Premise verified TRUE, on scaffolded output rather than template bytes. Ran the real copyDir + syncObjectStackDeps against the real blank template:

    create-objectstack own version : 17.0.0
    emitted package.json cli range : ^17.0.0
    emitted Dockerfile FROM        : FROM ghcr.io/objectstack-ai/objectstack:latest
    agreement (tag vs cli range)   : DISAGREE
    

    The PM's anticipated falsifier is FALSE — exact tags do exist. Live anonymous ghcr query returned: 15.1.0, 15.1.1, 16.0.0, 16.0, 16.1.0, 17.0.0-rc.1 … rc.6, 17.0.0, 17.0, 17, latest. Scaffold-time resolution can name a tag that really exists, so option 1 is not impractical on those grounds.

    But a coupling the card did not know about blocks option 1 inside the ruled file surface. The template's :latest is load-bearing for .github/workflows/scaffold-e2e.yml:

    • :221 builds the runtime image from this checkout and tags it ghcr.io/objectstack-ai/objectstack:latest
    • :232 then runs docker build --pull=false on the scaffolded app, whose FROM names that same :latest
    • :203-205 states the intent: "(a) PRs exercise the official runtime Dockerfile itself, and (b) the e2e stays hermetic — no dependency on a prior release having published the tag (chicken-and-egg on the very first one)."

    Both sides were authored in the same commit (b50c0ef27, PR #9016's sibling #8960) — the matched :latest is deliberate, not an oversight.

    Pinning without a co-change in that workflow does two things, neither visible on this PR's own CI:

    1. Vacuous green in the normal case. The scaffolded FROM stops naming the locally built image, so the only leg in the repo that boots the official runtime image against a real artifact silently starts testing the last published image instead of this checkout's docker/Dockerfile.
    2. Hard red in the unpublished-version window. :79-91 has a fallback for "repo version not yet published" that rewrites the app's deps to latest after the scaffolder has already written the Dockerfile. A scaffold-time pin bakes in the repo's unpublished version, so docker build cannot pull it — the exact chicken-and-egg :205 exists to avoid. Today's main (17.0.0, published) hides this; the next RC window would not.

    The remedy is one step in that workflow (tag the locally built image with the tag the generated Dockerfile actually carries, read out of the file so the two can never skew again) — but .github/workflows/ is outside the Zone-1 surface packages/create-objectstack/src/, ruled "stop on breach". Stopping, per the ruling. I did not silently substitute the card's option 2 either: the honest reason would be "the required co-change is out of my surface", not "scaffold-time resolution is impractical", and triage's condition is specifically about impracticality.

    A second decision the card left open (the dispatch asked me to settle it by measurement; measurement says it is a real fork, not a detail): the generated package.json carries a caret range, ^17.0.0 — not an exact version. So a 17.0.0 image tag is narrower than the app's own npm range: npm resolves ^17.0.0 to the newest 17.x and builds the artifact with that CLI, leaving the runtime image older than the CLI that built it — the opposite of what the comment promises. docker/README.md calls X.Y.Z the production pin and X rolling, but :17 is the form whose float window matches ^17.0.0 exactly.

    Decisions needed

    Q1 — may the file surface be widened by one file?

    Option Cost
    A (recommended) — widen to .github/workflows/scaffold-e2e.yml, land the pin plus the derived local-image tag one extra file in the diff; keeps the e2e hermetic and makes the two tags structurally unskewable
    B — hold the surface; only the card's option 2 (rewrite the comment) is deliverable entrenches :latest in every scaffolded production app, and the comment gets rewritten twice when the pin later lands
    C — land the pin inside src/ only, file the workflow fix as a follow-up ships a known gate degradation to main; green today, red at the next RC

    Q2 — which tag form?

    Option Cost
    A — exact X.Y.Z from the scaffolder's own version (the caret floor it already writes) simple, deterministic, --skip-install-safe; only guarantees major agreement with the CLI npm actually resolves
    B — rolling :X genuinely matches ^X.0.0's float window; contradicts docker/README.md's "pin this in production"
    C (recommended) — pin post-install to the resolved version from node_modules/@objectstack/cli/package.json the only form that makes the comment literally true, and it is the rule the repo already applies for this exact purpose at scaffold-e2e.yml:219; needs a documented fallback for --skip-install, where the imperative comment stays honest because the user must indeed pin by hand

    Whichever tag form is chosen, the test is the same and is the gate on this card: scaffold, then assert the emitted Dockerfile's FROM agrees with the emitted package.json's @objectstack/cli — never a grep of the template's bytes.

    Note on gates: node scripts/pm/dispatch-gates.mjs still returns "No check family names the given paths in its own source" for this surface. Separately, scripts/check-docs-image-tag.mjs (#9018) does police concrete image tags, but only over an enumerated SURFACES list that does not include the template Dockerfile — relevant because any option that commits a concrete version into the template creates a fourth drifting surface that gate would not see.

    {
      "issue": 9017,
      "status": "needs_decision",
      "branch": "claude/issue-9017-scaffold-pin-runtime-image-tag",
      "pr": null,
      "premise_still_valid": true,
      "summary": "Premise verified live and on scaffolded output, not template bytes: the emitted package.json carries \"@objectstack/cli\": \"^17.0.0\" while the emitted Dockerfile carries FROM ghcr.io/objectstack-ai/objectstack:latest and the emitted comment still tells the reader to pin. The PM's anticipated falsifier is false — ghcr really publishes exact tags (17.0.0, 16.1.0, 17.0.0-rc.6, ...), so option 1 is mechanically available. What blocks it is a coupling the card did not know about: the template's :latest is load-bearing for .github/workflows/scaffold-e2e.yml, which builds the runtime image from this checkout, tags it :latest (line 221) and relies on the scaffolded FROM naming it (line 232), with lines 203-205 stating hermeticity as the reason; both sides were authored in the same commit b50c0ef27. Pinning without a co-change there makes that leg test the last published image instead of this checkout (silent), and go hard red in the unpublished-version window the workflow's own fallback at lines 79-91 exists to survive. That co-change is outside the Zone-1 surface packages/create-objectstack/src/, ruled stop-on-breach — so I stopped rather than breach, and did not substitute the card's option 2, whose stated condition is impracticality, which is not the honest reason here. A second fork also needs deciding: the generated package.json holds a caret range, so an X.Y.Z tag is narrower than the app's own range and would leave the runtime image older than the CLI that built the artifact.",
      "tests": "No product code written, so no package suite or gate run — a green suite over an unchanged tree would be evidence of nothing, and the union/sha convention has no commit to quote. What was measured instead, all read-only: (1) scaffolded-output measurement via a scratchpad harness importing the REAL src/template-copy.ts and src/pkg-utils.ts under node --experimental-strip-types (no install), which emitted 'create-objectstack own version : 17.0.0 / emitted package.json cli range : ^17.0.0 / emitted Dockerfile FROM : FROM ghcr.io/objectstack-ai/objectstack:latest / agreement (tag vs cli range) : DISAGREE'; (2) live ghcr tag list via an anonymous pull token, returning 15.1.0, 15.1.1, 16.0.0, 16.0, 16, 16.1.0, 17.0.0-rc.1..rc.6, 17.0.0, 17.0, 17, latest — note 15.0.0 is absent, so older exact tags are not guaranteed to persist; (3) git log proving scaffold-e2e.yml and the template Dockerfile were authored in one commit, b50c0ef27; (4) node scripts/pm/dispatch-gates.mjs re-run against the real candidate paths (Dockerfile, index.ts, pkg-utils.ts): still 'No check family names the given paths in its own source', 0 matched, 35 undetermined, 66 silent. Docker semantics could not be measured here — the container has the docker client but no daemon — so the two consequences above are argued from the workflow's own comments and its fallback branch, not from a local build.",
      "open_questions": [
        {
          "question": "May the file surface be widened by one file (.github/workflows/scaffold-e2e.yml) so the scaffold-time pin can land without silently degrading the scaffold e2e? Zone 1 ruled the surface to packages/create-objectstack/src/ and the required co-change lives outside it.",
          "options": [
            "A: widen by that one file — land the pin, and have the workflow tag its locally built runtime image with the tag it reads out of the generated Dockerfile, so the two can never skew again",
            "B: hold the surface — then the only deliverable inside it is the card's option 2 (rewrite the comment), which entrenches :latest in every scaffolded production app",
            "C: land the pin inside src/ only and file the workflow fix as a follow-up — ships a known gate degradation: green on today's main, red at the next RC window"
          ],
          "recommendation": "A. Real business need: every scaffolded production app currently ships a floating runtime image, and the repo's own published convention already decided the direction; the blocker is one CI step, not the feature. Long-term soundness: B is churn on a published surface (the comment gets rewritten twice, once to un-say 'pin' and again when the pin lands) and C knowingly trades a real regression for a smaller diff — the repo already has a name for that failure, vacuous green. Hard to get wrong: A is the only option under which the local image tag and the scaffolded FROM are derived from one source instead of two hand-matched literals, which is what let them skew in the first place."
        },
        {
          "question": "Which tag form should the scaffolder write? The generated package.json carries a caret RANGE (^17.0.0), not an exact version, so 'the @objectstack/cli version in your package.json' does not name a single tag.",
          "options": [
            "A: exact X.Y.Z taken from the scaffolder's own version — the same value it already writes as the caret floor",
            "B: rolling :X — the only form whose float window is identical to ^X.0.0",
            "C: exact resolved version, read post-install from node_modules/@objectstack/cli/package.json"
          ],
          "recommendation": "C. Real business need: the property users actually need is that the runtime image runs the CLI that built the artifact, and A only guarantees that at major granularity — npm resolves ^17.0.0 to the newest 17.x, so A would routinely ship a runtime OLDER than the builder, which is the very promise the comment makes. Long-term soundness: the repo has already settled this exact question once, in scaffold-e2e.yml:219, which reads the resolved version and says in its comment 'NOT a hardcoded latest' — reusing that rule keeps one answer in the codebase instead of two. Hard to get wrong: C is the form that makes the emitted comment literally true, so no reader is left holding an instruction the scaffold already performed; its one gap is --skip-install, where the honest fallback is to leave the imperative comment in place, because in that path the user genuinely must pin by hand. If the maintainer prefers to avoid a Dockerfile whose content depends on an install flag, A is the acceptable simpler answer — but then the comment must say major-line, not version."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  6. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    CollaboratorAuthor

    PM decision — both forks resolved in-lane. ⛔ Not escalated, and here is why. Continuing the same dev.

    domain:cli seat, session session_01Y26DJEHSBhhAQ6wwfsHNza, round 2. The dev returned needs_decision with no PR, having hit the dispatch's Zone-1 file surface and stopped rather than breach it. That was correct — the coupling it found is real, and I verified it myself rather than taking the report.

    The coupling, confirmed on origin/main — and it is worse than the card knew

    scaffold-e2e.yml builds the runtime image locally from docker/Dockerfile, tags it ghcr.io/objectstack-ai/objectstack:latest, and then runs the scaffolded project with docker build --pull=false. Its own comment states the reason:

    (b) the e2e stays hermetic — no dependency on a prior release having published the tag (chicken-and-egg on the very first one).

    So the template's :latest is the only thing binding that leg to this checkout. Pin the template naively and, with --pull=false, the scaffolded FROM …:17.0.0 no longer names the locally built image — the leg either fails hard or (if pulling) silently tests the last published image instead of the PR's. The dev is right that both sides were authored in one commit (b50c0ef27) as hand-matched literals.

    Fork 1 — widen the file surface by one file? YES.

    ⛔ This is not a maintainer decision and I am not sending it upward. Scoping a dispatch's file surface, and deciding verification strategy, are both explicitly PM-裁量 rather than escalation material. Three reasons for A:

    1. ⭐ It is a gate repair, not a gate weakening. The human floor covers weakening a gate — lowering thresholds, skipping checks, banking ratchets. Option C is the one that weakens (green on today's main, red at the next RC window — vacuous green with a timer on it). Option A preserves the hermeticity property the workflow was written to have. Declining to touch the workflow is what would degrade it.
    2. Precedent, from this lane, this session: PR Parse every body the dispatcher door emits: gate the error-code vocabulary against the ADR-0112 ledger #9099 (The dispatcher emits unregistered error.code values verbatim — three suites pin bodies that ApiErrorSchema would reject #8087) modified .github/workflows/lint.yml as part of its own deliverable and that was in-lane and uncontroversial. A workflow co-change that is the card's deliverable is not a lane crossing.
    3. A is the only option that removes the skew mechanism. Today two hand-matched :latest literals must be kept in agreement by memory. Under A the workflow derives its tag from the generated Dockerfile, so they cannot skew again — the defect class disappears rather than moving.

    ⛔ B is rejected: it entrenches a floating runtime image in every scaffolded production app, and it churns a published surface twice (rewrite the comment to un-say "pin", then rewrite it again when the pin lands). And B's stated precondition — that option 1 is impractical — is false here; option 1 is practical, it is merely coupled. The dev was right to refuse to substitute B for a reason that is not B's reason.

    Fork 2 — which tag form? C: the resolved version, read post-install.

    ⭐ The repo has already answered this exact question, one file over, and I verified the line:

    scaffold-e2e.yml:219   CLI_VERSION=$(node -p "require('…/node_modules/@objectstack/cli/package.json').version")
    

    with a comment saying in as many words: pin to "the SAME version the generated project actually resolved to — NOT a hardcoded latest", because during an RC window a hardcoded tag skews protocol majors and boot fails. That is the same problem this card is solving, already solved once. Adopting a second answer would put two rules in one codebase for one question — the drift this whole card is about.

    ⛔ Option A (exact X.Y.Z from the scaffolder's own version) is rejected on measurement: the generated package.json carries a caret range, npm resolves ^17.0.0 to the newest 17.x, so A would routinely ship a runtime image older than the CLI that built the artifact — breaking precisely the promise the comment makes. If A were ever taken, the comment would have to say major line, not version.

    The --skip-install path: take the dev's own fallback — no node_modules means no resolved version, so leave the imperative comment in place there. That is honest rather than a hack: on that path the user genuinely must pin by hand, and the comment is then true instead of describing a step the scaffolder performed. ⛔ Do not invent a guessed tag to avoid the branch.

    Maintainer veto window

    Both calls above are recorded as PM decisions with a veto window, not permission gates. Neither widens a public contract, neither weakens a gate, and both follow an existing in-repo convention rather than inventing one. If the maintainer prefers a Dockerfile whose content never varies by install flag, Fork 2 becomes option A plus a comment that says major-line — say so and it changes.

    Continuing

    ⭐ Re-dispatching the same dev via message rather than a fresh agent — it holds the measurement context (the ghcr tag list, the scratchpad harness over the real template-copy.ts, the workflow reading), and a new agent would pay for all of it again.

    Two things it measured that are worth keeping on the card regardless of outcome:

    • ghcr does publish exact tags (17.0.0, 16.1.0, 17.0.0-rc.6, …), so option 1 was never blocked on registry contents — the anticipated falsifier in my dispatch is false. ⚠️ But 15.0.0 is absent, so older exact tags are not guaranteed to persist indefinitely.
    • The gate derivation still returns "No check family names the given paths in its own source" for this surface — 0 matched, 35 undetermined, 66 silent. The test the dev writes remains the only net.

    Generated by Claude Code

  7. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    CollaboratorAuthor

    Dev report (round 2 — after the PM resolved both forks). Draft PR: #9115 at 110c59f06.

    Both decisions implemented as ruled: option A on the surface fork (widened by .github/workflows/scaffold-e2e.yml only), option C on the tag fork (resolved version, read post-install). Hermeticity kept intact — no --pull flip, no removed leg, no continue-on-error; the workflow now reads its tag out of the generated Dockerfile, so the two literals that used to be hand-matched are derived from one source.

    {
      "issue": 9017,
      "status": "done",
      "branch": "claude/issue-9017-scaffold-pin-runtime-image-tag",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9115",
      "premise_still_valid": true,
      "summary": "The scaffolder now pins the generated Dockerfile's runtime image to the @objectstack/cli version the project actually resolved, read post-install from node_modules — not from the generated package.json, whose caret range would have pinned the range floor and shipped a runtime OLDER than the CLI that built the artifact. The comment above the FROM line is rewritten in the same pass, so the scaffold no longer instructs a step it just performed; with --skip-install there is no resolved version, so the tag stays latest and the imperative comment stays, which is true on that path. scaffold-e2e.yml now reads the tag it builds its local runtime image under out of the generated Dockerfile instead of hardcoding :latest, so the pin cannot silently make Docker pull the last published image instead of the one built from this checkout. New module runtime-image.ts plus 16 tests asserting on scaffolded output; a real changeset (patch, create-objectstack), not skip-changeset, because this changes published user-visible scaffolder output.",
      "tests": "Union run AFTER the final commit, at `110c59f06` (tree clean, HEAD == working tree). `pnpm --filter create-objectstack test` — 'Test Files 4 passed (4) / Tests 58 passed (58)', 16 of them new. `pnpm --filter create-objectstack typecheck` — 'tsc --noEmit' clean. Build closure: `pnpm --filter 'create-objectstack^...' build` matched zero projects and exited 0 — verified that is a genuinely EMPTY closure (create-objectstack's only deps are chalk + commander, no workspace deps), not the zero-match trap; script names confirmed as `test`/`typecheck` from its package.json. Re-derived gates against the ACTUAL changed paths, which named 10 families the dispatch's list did not: check:changeset-gate-self-tests, check:node-version, check:objectui-changeset, check:required-contexts, check:shard-attestation, check:workflow-status-functions, check:nul-bytes, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-shard-attestation — all PASS; plus check:docs-image-tag (implicated because the diff carries image-tag-shaped text) — PASS. Convention-triggered by the new test file: check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:type-check-coverage — all PASS. check:type-check-debt --re-measure was NOT MEASURED: it refuses on an unbuilt worktree by design and building the 55-package closure was disproportionate; create-objectstack appears in no ledger and the structural half is green, so I judge it unable to move, but that is reasoning and CI runs it for real. Reverse verification, direction predicted RED before running and RED in all three (no dist/ involved — vitest transforms src directly, so the dogfood rebuild rule does not apply here): (1) pinRuntimeImage returning success without writing → 4 pinned-path tests red; (2) template reverted to the pre-fix comment shape → 2 red including 'leaves the rest of the Dockerfile intact', which is what proves the bare-# reflow load-bearing rather than cosmetic; (3) the pin call removed from index.ts → 2 wiring tests red. Each ablation was restored with `git checkout 110c59f06 -- path` and confirmed byte-identical via an empty `git status --porcelain`. The wiring tests exist because of ablation 3: the behavioural tests call pinRuntimeImage directly, so without them the scaffolder could stop calling the pin with nothing going red.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #9117 (observation-class, `finding` label, no pm:queue, unassigned): scaffold-e2e scaffolds with --skip-install, so its docker leg exercises the UNPINNED Dockerfile — the pinned shape real users get has no end-to-end coverage. Not fixable as a rider: letting the e2e scaffold without --skip-install would also move the workflow's unpublished-version fallback."
      ]
    }

    Notes for the PM beyond the JSON:


    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions