Files
coreci da15bda6d2 docs(milestone): complete cli-packaging
---ci---
phase: 3
milestone: v0.2
status: complete
requirements:
  covered: [REQ-21, REQ-22, REQ-23]
  partial: []
---/ci---
2026-10-11 00:32:26 +00:00

20 KiB
Raw Permalink Blame History

CIAgent Grill Report

Run: 2026-10-10 (mode: interactive, focus: v0.2 plan red-team — cli-packaging)

Supersedes the v0.1 grill in this file (v0.1 run of 2026-10-09; its full text and verdicts G-001..G-008 live in git history at 7f7868f and remain binding where the plan depends on them: G-005's owner-run post-ship OWUI smoke checklist and G-008's 90-day adoption criterion carry forward in ROADMAP.md §Post-Milestone Notes; G-007's forge-repo-creation test was executed at v0.1's P0 ship and is closed). New verdicts number G-009 onward.

Verdict (summary): GO-WITH-CONDITIONS (confidence: 0.82)

The plan rests on genuinely strong research — live-verified forge/registry wire shapes, a working zipapp prototype booted against the real broker sources, zero-broker-lines verified against the actual file (1390/1400). Red-team findings are concrete and fixable by task/ship-step edits: one unexecutable mechanism (the v0.2.1 attachment bridge cites a harness path RESEARCH itself proved absent from this environment), one ship-sequence inversion (P3-05 attaches the release before the tag exists — gitea would mint v0.2.3 at the wrong commit), one false-green determinism gap (the normalize pass "preserves" umask-derived zip attributes, so must-have 4 passes while the "clean checkout" exit criterion silently fails cross-machine), and a set of test-realism holes (subprocess harness with no timeout discipline, missing creds env in the packaged secret-read, no live read-only validation before the one live publish). Three forks were resolved interactively with the owner this session (G-011, G-013, G-014); everything else is bound on evidence below.


Surface 1 — Determinism claims (must-have 4; P1-01)

Forcing question: Is two-build sha256 equality actually achievable and testable, and what does the normalize pass fix vs. miss?

  • Evidence (stdlib source, read this session): /usr/lib/python3.11/zipapp.py builds via for child in source.rglob('*'): z.write(child, arcname.as_posix()) — entry order = scandir order, entry mtime = source mtime (ZipInfo.from_file), and external_attr = the staging file's stat mode << 16. PLAN P1-01's normalize pass fixes order (sorted), date_time=(1980,1,1,0,0,0), ZIP_DEFLATED — but specifies "preserved external_attr". Preserved from what zipapp wrote, i.e. derived from staging file modes — which are 0666 & ~umask if staged via plain copy, or checkout-mode if via copy2. Umask 0022 vs 0002 across machines/clones → different external_attr → different sha256, while must-have 4 (two consecutive builds, same machine) still passes. A silent false green against the phase exit criterion ("builds reproducibly from a clean checkout").
  • gzip is not in the pyz path (zipfile DEFLATE only). Image-layer gzip is deterministic on-platform: gzip.compress(..., mtime=0) routes to zlib.compress(wbits=31) on 3.11 (verified in gzip.py source) — fixed header, fixed OS byte, stable per zlib build. Scope claim: byte-identity is same-environment (same machine, same zlib); cross-zlib-version identity is neither claimed nor required by must-have 4.
  • Answer: achievable and testable as scoped — provided the normalize pass also fixes what it currently preserves. → G-009.

Surface 2 — Boot parity via subprocess (P1-02)

Forcing question: What makes the subprocess acceptance flaky, and what's missing?

  • Evidence: zero v0.1 tests use subprocess/Popen (grep over tests/: no hits) — P1-02 introduces the repo's first subprocess-boot harness. The plan's env enumeration (BROKER_BIND, BROKER_ENV_PATH/DB/AUDIT) omits BROKER_CREDS_FILE/ BROKER_CREDS_SOURCE while requiring "secret-read reuse the v0.1 acceptance request patterns" — the v0.1 precedent (tests/test_acceptance_full.py:76-77) sets both; FileCredsSource on a missing file raises _SecretNotFound → 404 (coreci_broker.py:440-441), so the packaged secret-read would fail. Popen(["python3", ...], env={minimal}) leaves the child without PATH (works today via execvp's confstr fallback — by accident). No timeout discipline on "read stdout lines until coreci-broker listening on" (RESEARCH §4.3) — a failed boot never prints the line and hangs the suite. "Pass printed exactly once" is asserted against a still-running server; shutdown-path output is unread unless drained after terminate. And every planned boot pins absolute state paths — the owner's literal happy path (download → python3 *.pyz in some directory → .env/db/audit materialize in the CWD) is never tested.
  • Answer: harden the harness — the plan's claims are all testable once the harness is honest about them. → G-010.

Surface 3 — One-shot lead upload bridge (P1-03)

Forcing question: Is the v0.2.1 ad-hoc attachment a discipline violation, and is it even executable as specified?

  • Evidence: PLAN P1-03 attaches via "the same harness mechanism v0.1 used for its broker-vX.Y.Z.tar.gz assets" — but RESEARCH §2.2 (the plan's own research base) found that mechanism is the ship harness's coreci build/package/release upload CLI, "not installed in this dev environment"; v0.1's archives were produced container-side, ad-hoc, with no repo-owned script. The bridge as written is therefore unexecutable via the cited mechanism, and its realistic substitute is an untested hand-rolled live-forge write (multipart encoding, token handling, idempotency) at first live use — performed by lead-developer, the one persona barred from code writes, using token discipline that scripts/_env_secrets.py (which does not exist until P2) is supposed to enforce. This is precisely the practice REQ-22 formalizes away.
  • Owner decision (this session): restructure — release automation moves into P1; the bridge is deleted. → G-011. The hard gate question dissolves: v0.2.1 ships script-driven; there is no bridge to become permanent.

Surface 4 — Daemonless OCI publish (P2-02, P3-05)

Forcing question: Does the milestone ship sequence hold up when publish fails after the tag exists — and does it even run in the right order?

  • Evidence — sequence inversion: P3-05 as written runs release.py attach → publish image → squash-merge → main → tag v0.2.3 → push. But POST /api/v1/repos/ {o}/{r}/releases with a missing tag creates the tag at target_commitish (default-branch head) — gitea's documented CreateReleaseOption semantics (schema in RESEARCH §2.1). A create-first flow pre-merge would mint v0.2.3 pinned to pre-merge main, then collide with (or silently diverge from) the harness's own tag push. The plan's own P2-04 sequences correctly (tag → merge → push → release) — P3-05 alone inverts it.
  • Evidence — wire discipline: bounded retry(3) is specified but no per-request timeout anywhere; a hung 29.8 MB monolithic POST through a throttled gitea turns "bounded retry" into an unbounded hang. The 307/308 stub test proves only Location-following, and gitea's Location may be relative (RESEARCH §3.3) — the harder case is untested. Upstream 3.11-slim drift is handled (resolve-at-publish + --base-manifest + provenance digests; RESEARCH §3.7) and the anonymous-200 token trap fails loudly per spec.
  • Evidence — failure story: "publish never blocks merge" is honored, but when publish fails after v0.2.3 exists, the plan records only release_pending — no named post-milestone re-run/closure step. The ship contract makes the escalation auto-resolution; somebody must own the retry.
  • Answer: reorder P3-05, add timeouts + relative-Location coverage, and make the escalation carry its own closure runbook. → G-012, G-019, G-020.

Surface 5 — Scope discipline (P2-03, REQ-22/23)

Forcing question: Is anything decorative, and is anything REQ-22/23 implies missing?

  • Evidence — decorative risk: P2-03's verify is presence-only ("Dockerfile + README sections present") and the dev environment cannot build the Dockerfile (no runtime) — a syntactically or semantically broken Dockerfile passes the gate. The "equivalence mapping" header is documentation with no enforcement.
  • Evidence — missing: no .dockerignore anywhere in the plan — docker build . ships the whole repo (.git/, any local .env*, data/, *.db, dist/, __pycache__/, .ciagent/) as build context, adjacent to a security review that asserts "no secrets in any image layer". REQ-22's notes model is covered (§2.3, stub asserts sections present); the sha256's source is unspecified (hand-pasted vs. computed); latest semantics are coherent (once per milestone at final ship — v0.2.3 is the first image ever, D-021); the state-dir volume contract is documented (WORKDIR /state, README docker run).
  • Answer: make the Dockerfile contract testable, add .dockerignore, pin digest provenance. → G-015, G-016, G-017.

Surface 6 — Frozen broker / shim dependency risk (P1-01)

Forcing question: Does the shim need broker cooperation, and what is the coupling risk?

  • Evidence: RESEARCH §1.2's prototype booted from the real sources with zero broker cooperation — re-verified against the file this session: run_standalone (coreci_broker.py:1369), _parse_env_file (:154), boot (:598), load_config (:240) are module-level; load_config precedence (process env → .env → defaults, :246-257) makes the shim's pre-set os.environ approach contract-identical. The broker's .env resolution is CWD-relative by default (ENV_PATH_DEFAULT=".env", ENV_DEFAULTS :53-66) — packaged boots with unset BROKER_ENV_PATH anchor state to the CWD, which is exactly the documented happy path and is currently untested (folded into G-010). Residual risk: the shim calls _parse_env_file — an underscore-private symbol. Within v0.2 the broker is frozen so the risk is nil; across future milestones a rename breaks the shim with a loud ImportError (acceptable), but only if somebody knows to look.
  • Answer: no cooperation needed (verified twice); the private-API dependency must be documented and review-tracked, and the default-anchoring boot must be tested. → G-018 (anchor case bound under G-010).

Surface 7 — Test realism: stub fidelity (P2-01/P2-02 tests)

Forcing question: What would a stub MISS that the real forge does — and is the as-built client ever validated against the real forge before its one live write?

  • Evidence: the stub coverage is unusually good (anonymous-200 trap, index→amd64 resolution with Accept check, HEAD-skip, monolithic+two-step uploads, 307-following, digest and diff_id assertions, both tags; multipart field/?name= byte assertions, idempotent re-attach, 500×3 marker, token discipline). Gaps: fixtures are frozen at research-time shapes (2026-10-10) while upstream index and forge auth can move mid-milestone; the 307 test doesn't pin the relative-Location case (bound in G-019); gitea's 4 MB attachment cap is out-of-band (pyz ≈30 KB — immaterial, noted); and no step anywhere validates the as-built scripts against the real git.coreci.dev in read-only mode — the first live run of publish_image.py is the P3-05 milestone publish itself, the one shot per milestone.
  • Owner decision (this session): add an env-gated, read-only live smoke as a P3-05 pre-ship verify step. → G-013.

Surface 8 — Wave ordering & effort (P2 packing)

Forcing question: Is P2 overloaded, and are there hidden dependencies that break the wave order?

  • Evidence: original P2 packs _env_secrets + release.py + publish_image.py + Dockerfile + README + 2 test files into 3 waves — the overload is real, and the bridge finding (Surface 3) is the reason it dissolves: release automation belongs in P1 (G-011), leaving P2 = publish client → Dockerfile/docs → ship. Hidden dependencies check out: publish_image.py imports _env_secrets (lands earlier in P1 now — order preserved transitively); the Dockerfile task consumes P1-01's staging allowlist (G-016 formalizes the contract as a test). publish_image.py remains the plan's biggest single commit even after the restructure.
  • Owner decision (this session): keep it one vertical task — the pull/push seam is artificial, the stub fixtures are shared end-to-end, RESEARCH §3.4 is near-pseudocode, and a split would not isolate the actual risk (first live write — covered by G-013). → G-014.

Binding Decisions

ID Surface Decision Binds Confidence
G-009 1 Normalize pass must SET constant external_attr (files 0o100644<<16, dir entries 0o40755<<16), not "preserve" staging-derived modes; staging is an explicit allowlist (coreci_broker.py, coreci_broker_client.py, coreci_broker_ui/**, shim staged as root __main__.py, generated _cli_version.py) — never a repo rglob, __pycache__/dist/ excluded by construction. Byte-identity scope = same environment (same zlib/machine); cross-zlib identity is not claimed. P1-01 task edit 0.85
G-010 2 P1-02 harness hardening: subprocess boots use sys.executable (absolute); child env adds BROKER_CREDS_SOURCE=file + BROKER_CREDS_FILE=<tmp fixture> (v0.1 precedent test_acceptance_full.py:76-77); deadline-bounded wait for the listening line (~30s → fail with captured stdout+stderr, terminate child); pass-once/silent-second-boot assertions completed by draining stdout after terminate; ADD a default-anchoring boot case (cwd=<tmp>, no state overrides beyond BROKER_BIND) asserting .env/broker.db/broker.audit.log in the CWD — the owner's literal happy path. P1-02 task edit 0.85
G-011 3 Restructure (owner, this session): move scripts/_env_secrets.py + scripts/release.py + tests/test_release_upload.py into Phase 1 as new wave 3 (P1-03 · release-asset automation); old P1-03 ship renumbers P1-04 and ships v0.2.1 script-driven (build_cli.py v0.2.1 → release.py v0.2.1 <pyz>); the one-shot lead-upload note is DELETED (its cited mechanism is absent from this environment — RESEARCH §2.2). P2 renumbers: publish_image.py → P2-01 (wave 1), Dockerfile+docs → P2-02 (wave 2), ship → P2-03 (wave 3). Four-test-file count unchanged. PLAN edit (before P1-01 executes) 0.90
G-012 4 P3-05 sequence corrected: squash-merge → main → tag v0.2.3 → push → THEN release.py attach (GET-by-tag finds the real tag; no pre-merge release create) → publish image → closeout. Mirrors P2's own correct ship order; eliminates the gitea create-tag-at-target_commitish hazard. P3-05 ship-step edit 0.80
G-013 7 Env-gated live read-only smoke (owner, this session): pre-publish P3-05 verify step — gitea GET /v2/ 401 challenge shape, GET .../releases/tags/{tag}, GET /v2/token Basic → non-empty JWT, Docker Hub anonymous token + 3.11-slim index→amd64 resolve. Implemented as a --check/verify mode on the scripts or scripts/verify_endpoints.py; GET-only, never in the default unittest run; tokens only via _env_secrets. P3-05 verify + task edit 0.85
G-014 8 publish_image.py stays one vertical task (owner, this session): shared stub fixtures, RESEARCH §3.4 near-pseudocode; a split would not isolate the real risk (first live write — G-013 covers it). PLAN shape (no edit) 0.75
G-015 5 .dockerignore added to the Dockerfile task: excludes .git, .env*, data/, *.db, *.log, dist/, __pycache__, tests/, .ciagent/. Build-context hygiene; P3-02 scope gains "no secrets in the build context" alongside "none in any layer". P2-02 task edit 0.85
G-016 5 Dockerfile equivalence made testable: the Dockerfile task adds a consistency test (into tests/test_build_cli.py) asserting COPY source set == build_cli.py staging allowlist, identical zipapp flags (--python "/usr/bin/env python3" --compress), identical _cli_version.py generation format, ENTRYPOINT/WORKDIR == publish_image's config.Entrypoint/config.WorkingDir. Presence-only verification no longer gates a possibly-broken Dockerfile. P2-02 task edit 0.80
G-017 5 Digest discipline: the sha256 in release notes is computed by release.py from the artifact file itself — never a CLI argument, never hand-pasted. Digest/asset mismatch becomes structurally impossible. P1-03 task edit 0.85
G-018 6 Shim private-API dependency tracked: cli/__main__.py header documents the coupling to coreci_broker._parse_env_file + run_standalone ("private-API dependency — any future broker refactor must re-run the v0.2 packaged acceptance"); P3-02 review scope lists it. Loud-ImportError breakage accepted. P1-01 task edit + P3-02 scope 0.75
G-019 4 Publish-client wire hardening: explicit per-request timeouts on every registry call (generous on blob uploads, ~120s); blob-level bounded retry with HEAD-skip between attempts (a timed-out 29.8 MB POST resumes cheap); the 307 stub test uses a RELATIVE Location at least once (gitea may answer relative — RESEARCH §3.3; absolute is the easy case). P2-01 task edit 0.80
G-020 4 Publish-failure closure is a named step, not an open escalation: the release_pending entry records the exact re-run command (python3 scripts/publish_image.py on main @ v0.2.3; --base-manifest optional pin) and its close condition (both tags visible in gitea packages). "Never blocks merge" unchanged. P3-05 ship-step edit 0.80
G-021 4 The v0.2.0 (phase-0) release carries NO .pyz and its notes must not promise one — D-020 starts at v0.2.1 (build tooling first exists in P1). Prevents a D-020 misread at the phase-0 ship immediately following this grill. Phase-0 ship note 0.85

Escalations

  • (none open — the three material forks were resolved interactively with the owner this session: G-011 restructure, G-013 live smoke, G-014 task packing. G-001..G-008 from v0.1 remain binding per git history; G-005/G-008 carry forward in ROADMAP post-milestone notes unchanged.)

Promotion note (explicit, not performed by the grill)

G-011 shifts REQ-22's attach automation into Phase 1; ROADMAP.md's Phase 2 description ("release-asset automation" wording) and Phase 1's requirements line need a one-line touch-up when the PLAN restructure is promoted — by the orchestrator/owner via the normal plan-revision path, not by this file. Nothing else in PROJECT/REQUIREMENTS/ROADMAP is affected by these verdicts.

Overall Verdict

GO-WITH-CONDITIONS — confidence 0.82.

The plan is research-grounded to an unusual degree (live wire shapes, working prototype, zero-broker-lines verified against the file), and every finding is actionable by task or ship-step edit. It is GO only with all of the following applied:

  1. G-011 restructure applied to PLAN.md before P1-01 executes — the bridge is unexecutable as written; v0.2.1 must ship script-driven.
  2. G-009 + G-010 applied to P1-01/P1-02 — determinism must fix (not preserve) entry attributes; the subprocess harness needs interpreter-path, creds-env, timeout, drain, and default-anchoring discipline.
  3. G-012 + G-020 applied before P3-05 — attach-after-tag ordering and the publish-failure closure runbook are ship-contract corrections, not optional polish.
  4. G-013 live smoke wired as the P3-05 pre-ship verify step — the one live publish must not also be the first live wire contact of the as-built client.
  5. G-015/016/017/018/019 applied with their tasks — context hygiene, testable Dockerfile contract, digest provenance, tracked private-API coupling, wire timeouts.
  6. G-021 noted at the phase-0 ship — v0.2.0 carries no .pyz.