Scaffold Integrity Root-Cause Plan
Scaffold Integrity Root-Cause Plan
Decision
This report supersedes the earlier issue #104 policy recommendation in Scaffold Integrity Edge Cases Research. The accepted contract is explicit: normal hi update applies verified baseline recovery to eligible fixed ScaffoldManaged conflicts by default; --write and --yes remain compatibility aliases for that same behavior; --check is read-only; and project-owned, generated, consumer-owned, structured, dependency, and intentionally customizable exceptions remain preserved silently.
The scaffold manifest is a receipt, not authority. Its hashes must describe the files that a trusted operation actually committed. They must never be refreshed merely because observed bytes differ.
The product invariant is:
A successful first-party scaffold, update, or generated projection operation leaves
hi check --jsonclean. Managed-content drift, warning, or degraded state is reserved for an actual out-of-band edit to fixed scaffold-managed content. Runninghi updateresolves that fixed-content drift by default. Unavailable authority or unsafe/incomplete execution is a separate typed operational failure and must never masquerade as drift.
This gives two different answers to “should hashes be refreshed?”
- Issue #105: yes. The projection producer must refresh exactly the projection-receipt manifest entry whenever it successfully publishes a new receipt. It owns both sides of that evidence relationship.
- Issue #104: no blanket refresh. If observed bytes already equal the selected baseline's desired bytes, stale receipt metadata is repaired automatically because no content conflict exists. If observed bytes differ from both prior receipt and desired baseline, refreshing the hash would bless an unproven edit. Every normal update archives and replaces the divergent fixed
ScaffoldManagedfile from verified baseline authority, then records the hash of the final observed replacement.
Do not weaken hi check or broaden semantic hashing to skills/hooks/config. Make ownership truthful instead: anything intentionally project-editable must not be ScaffoldManaged. With that boundary, ordinary update can always overwrite fixed scaffold-managed content safely.
Source-of-truth model
| State | Authority | Required behavior |
|---|---|---|
| Fixed scaffold-managed desired bytes | Verified selected baseline | Compare byte-for-byte except the existing narrow semantic-JSON allowlist. |
| Generated projection receipt | A completed sync-subagents producer run | Producer advances only its own receipt evidence. |
| Current repository bytes | Confined filesystem observation | Never infer them from desired content or a stale receipt. |
| Manifest hashes, types, and modes | Post-operation observation | Commit last as evidence of completed work. |
For each fixed managed file, reconciliation is a three-way classification:
observed == desired: clean content. Refresh stale receipt metadata without warning or archive.observed == prior receiptanddesired != observed: safe baseline update. Apply desired content and record the final observed state.observed != desiredandobserved != prior receipt: real or unprovable out-of-band edit.hi checkreports it.hi updaterecoverably archives it, replaces it from the verified selected baseline, and records the final observed state.
This classifier, rather than receipt freshness alone, is what prevents false local-edited reports. Issue #104's exact stale-hash incident must become a fixture: baseline-identical hook, lint-spec, Oxlint, and skill files with stale manifest hashes must converge without conflict. If that fixture already passes at the pure reconciler, trace the discrepancy through baseline selection, staged desired bytes, type/mode comparison, and presentation instead of adding an override that masks it.
Issue #104 design: baseline-authoritative update by default
Public contract
hi update is an applying command by default. The explicit read-only operation is:
hi update --checkRetain --write and --yes as compatibility aliases during migration, but they no longer select stronger conflict semantics. Interactive and non-interactive hi update both apply verified baseline changes without a confirmation gate; --check previews, writes nothing, and exits nonzero when an update or fixed-content repair is needed.
This is safe only if ownership is exact:
ScaffoldManagedmeans fixed, baseline-authoritative, and not project-editable;ProjectGeneratedmeans another trusted producer owns evolution;- structured/dependency ownership preserves designated project-authored fields; and
- consumer-owned or intentionally customizable files remain unmanaged or receive an explicit project-editable ownership contract.
Audit every currently managed kind before enabling the new default. Reclassify any legitimate customization surface rather than retaining a generic conflict-preservation escape hatch. This makes the update rule permanent and removes path-specific policy.
Automatic archive-and-replace is eligible only for direct whole-file conflicts that are:
- owned as
ScaffoldManaged; - sourced from the verified selected baseline;
- confined regular files or managed symlinks with an exact planned identity; and
- not project-generated evidence, structured fields, dependencies, repository mirrors, consumer-owned files, or overlapping ownership families.
The action also requires desired replacement bytes or a desired link target. Receipt-only removals remain conflicts: there is no selected-baseline replacement whose identity can be proven.
Apply it to all eligible scaffold-managed fixed files, not only /skills. The issue includes skills, hooks, lint specifications, and Oxlint configuration; the ownership/provenance contract is safer than path heuristics.
Recovery transaction
- Plan and preflight every replacement and archive destination before mutation.
- Archive each conflicting file under a deterministic observed-fingerprint directory such as
.devpunks/replaced-scaffold/<observed-fingerprint>/<relative-path>. This makes retry reuse a proven identical archive instead of multiplying copies. - Never overwrite an archive or a concurrently recreated target.
- Archive a symlink as a link and never follow it. Publish desired baseline bytes/link target through a dedicated confined archive-and-replace primitive; composing an ordinary remove and write loses source identity between operations.
- On publication failure, restore the archived original when the target is still absent. If safe restoration is impossible, retain the archive and return its exact recovery path.
- Persist the manifest only from post-application observed bytes, types, and modes.
- Run the normal final check contract; a successful command must not emit conflict, warning, or degraded output.
No archive is created when observed content already equals desired content. That case is metadata repair, not destructive recovery. A repeated successful update must be a no-op.
Baseline version, pack, or pointer movement without materialized fixed-content divergence is also metadata, not drift. It may be exposed as a neutral fact, but must not produce the current baseline warning/degraded status. Warning and nonzero state derive from unresolved content or authority failure, not version inequality alone.
Required tests
- stale manifest hash plus baseline-identical fixed file: automatic metadata convergence, no warning, no archive;
- prior receipt match plus newer desired baseline: normal safe update, clean check;
- true out-of-band edit under
hi update --check: drift reported and bytes preserved; - the same edit under normal
hi update: archived, replaced, receipt reflects final disk bytes, clean check; - intentionally customizable fixture: proven non-
ScaffoldManagedand never overwritten by fixed-content reconciliation; - mixed eligible/ineligible conflicts: ineligible entries remain fail-closed and are never silently blessed;
- archive collision, symlink swap, concurrent target recreation, publication failure, rollback failure, and retry idempotence;
- exact installed CLI flow ending in
hi check --jsonwith no changed, stale, warning, or degraded entries.
Issue #105 design: producer-bound evidence commit
The canonical generated sync-subagents.mjs now owns the evidence commit. It atomically writes .devpunks/harness-projection-receipt.json, rereads the committed receipt, and advances only that receipt's entry in .devpunks/scaffold-manifest.json. Check remains strict and still detects manual receipt corruption.
Producer contract
- Treat the manifest as an optional confined input so direct legacy script use without a scaffold manifest remains supported.
- When present, preflight it before projection side effects or receipt mutation. Require confinement, the complete supported manifest schema, and exactly one
managedFilesentry whose kind and path identify.devpunks/harness-projection-receipt.json. Reject missing, duplicate, ambiguous, unsafe, or contradictory ownership, and capture the validated manifest identity for compare-and-swap. - Generate and atomically publish the receipt using the existing confined writer.
- Reread the committed receipt bytes and calculate the same canonical semantic JSON SHA-256 used by the CLI.
- Immediately before manifest publication, revalidate confinement, the complete schema, unique receipt entry, and the captured manifest identity. Compare-and-swap only if all four still hold. Change only the receipt entry's hash and the manifest's receipt path/status fields; preserve every unrelated field.
- Print success only after manifest publication succeeds. If a commit-time identity conflict occurs after truthful receipt publication, retain the committed receipt and return a typed retry result without overwriting concurrent manifest bytes. Other receipt-published/manifest-failed paths also exit nonzero with an explicit retry instruction. A rerun must converge safely.
The generated script cannot import the CLI's TypeScript helper in consumer repositories. Add the smallest ESM-local canonical JSON/hash implementation and protect it with parity vectors against hashManagedFileContent. Keep the active .agents copy byte-identical to the bundled canonical source through the existing contract test and normal scaffold generation.
Failure receipts are still producer output. If current behavior intentionally publishes a receipt with status: failure, the manifest must record that exact receipt hash/status before the script throws; otherwise the producer itself creates false drift. A malformed pre-existing manifest must fail before projection side effects.
Two independent file renames are not crash-atomic. The contract provides individually atomic publication, commit-time identity compare-and-swap, truthful retained receipt evidence, nonzero partial-failure reporting, and idempotent retry. A durable journal is warranted only if automatic recovery after process death between the two renames becomes a requirement; do not claim cross-file atomicity without one.
Required tests
- scaffold, then force a controlled semantic projection-input change; capture the old manifest receipt hash and, after new receipt publication but before manifest refresh, prove it differs from the new committed receipt's semantic hash; after refresh, prove only receipt hash/path/status changed and
hi check --jsonconverges cleanly; - success and intentional failure receipts both leave coherent evidence before return/throw;
- canonical-hash parity under key reordering and whitespace changes;
- malformed, missing, duplicate, wrong-path, and symlinked manifest evidence fails closed;
- manifest compare-and-swap revalidates identity at commit, rejects a replacement race without overwriting concurrent bytes, retains the truthful committed receipt, and returns retry;
- receipt-written/manifest-failed retry converges;
- public
scaffold -> controlled semantic projection change -> generated sync-subagents -> hi check --jsonflow is clean; a no-op consecutive sync is insufficient; - existing manual receipt-corruption test stays red, proving check did not become permissive.
Implementation conclusion
The local implementation and public lifecycle matrix prove the accepted contract. No-flag update applies; --check remains read-only; --write and --yes are aliases; contradictory combinations fail before baseline, operation, or filesystem effects; baseline-identical stale receipt evidence refreshes without an archive; and a real fixed ScaffoldManaged edit is reported by check, archived under .devpunks/replaced-scaffold/<fingerprint>/<path>, replaced from verified baseline authority, and followed by a clean check. Project-owned, intentionally customizable, and current ProjectGenerated paths remain silent and preserved. Unavailable authority remains a typed operational result rather than drift.
The generated producer proof covers exact-entry manifest refresh, canonical-hash parity, success and intentional failure receipts, compare-and-swap conflict, retry convergence, and the manual-corruption negative control. Receipt and manifest files publish atomically one at a time. The proof does not claim one crash-atomic two-file transaction.
Stable baseline baseline/stable/2026.08.07-scaffold-integrity-convergence is published at c4dcca3134eace76750fa899f3b581411af094d0. @punks/cli 3.1.6 and its installed-consumer proof remain unpublished, so issues #104 and #105 remain subject to the published-consumer closure criteria below. This release state supersedes the earlier prepared-only wording; see Cross-Thread Stack Reconstruction Research.
Delivery graph
Wave 1: RED contracts
- Reconciliation lane: encode the three-way classifier, stale-metadata convergence, ownership audit, default archive-replace actions, and check-only preservation in pure tests.
- Producer lane: encode manifest preflight, canonical-hash parity, compare-and-swap, success/failure evidence, and partial-failure retry in generated-script tests.
These lanes are disjoint until shared lifecycle fixtures.
Wave 2: bounded implementation
- Implement #104 in update default semantics, reconciliation planning/application, confined persistent archive support, operation output, ownership migrations, and scoped tests.
- Implement #105 in the canonical bundled script and its direct tests; regenerate the active scaffolded copy rather than hand-maintaining it.
Wave 3: one integration owner
- Add public lifecycle regressions for scaffold, generated sync, update, and check.
- Regenerate managed-assets/baseline fixtures and prove active/bundled script identity.
- Update
CHANGELOG.md,BASELINE_CHANGELOG.md,docs/README.md, and the CLI runbook. - Run scoped tests, CLI type/lint/format verification, full CLI verification, baseline build/attestation, and installed-package smoke tests.
- Prepare CLI and baseline release inputs; publishing remains a separate release-gated task.
Do not run #104 and #105 as independent end-to-end releases: they meet at manifest truth, integration fixtures, packaging, and closure proof.
Closure criteria
Close #104 only after a published CLI proves all three fixed-file states: baseline-identical stale metadata self-heals silently; safe old-baseline content updates normally; and actual edits are visible under --check but automatically archived and replaced by normal hi update. Also prove that every intentionally customizable asset is outside fixed ScaffoldManaged ownership.
Close #105 only after a published CLI/baseline proves scaffold -> generated sync-subagents -> hi check --json is clean while manual receipt corruption still fails.
For both issues, capture JSON proof that failed is false and changed, stale, warning, degraded, and conflict collections are empty after every successful first-party flow. The manifest must always describe actual committed state; no code path may refresh arbitrary hashes to make check green.
Rejected shortcuts
- Refresh every observed hash during check: launders arbitrary edits and destroys drift detection.
- Make check invoke producers or repair state: violates its read-only integrity role.
- Refresh divergent hashes without replacement: launders actual edits instead of restoring baseline authority.
- Preserve customization by leaving editable files classified
ScaffoldManaged: makes update semantics ambiguous; reclassify their ownership instead. - Stop writing projection receipts: leaves stale or false evidence.
- Remove the receipt from managed integrity: hides producer defects and manual corruption.
- Mark the receipt
ProjectGenerated: repository-mirror ownership does not prove this producer ran. - Claim two sequential atomic writes form one filesystem transaction: false across process death.
Lane coverage
- #104 lane: reconciliation policy, ownership boundary, default update semantics, archive/rollback behavior, and closure tests.
- #105 lane: generated producer ownership, manifest compare-and-swap, hashing parity, failure evidence, and end-to-end regression.
- Cross-cutting lane: 3.0.0 contract lineage, revised ownership semantics, invalid parallelism, packaging, release, and immutable closure proof.