Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ outside this repository.

## Unreleased

- **What:** re-pin the six drifted ingredient pins in `kit.lock.json` from their live remote tags (fixes #11). The ingredient repositories' published histories were rewritten and their release tags re-pointed after the pins were taken, so every fresh install failed with an integrity error (exit 4) while warmed machines kept passing. New peeled commit shas, read from the remote rather than from a local cache: `protean-doctrine` v1.1.0 -> `838f71ea00139bc79643ee5625523733bdf0652d`, `protean-ops` v1.1.0 -> `f278db48e8e58f3107df8fd1391c4a145e5ffea0`, `protean-drafts` v1.0.0 -> `c091210c296bad9366cb6e4bb7ebf531d6bc40e3`, `protean-github-flow` v1.0.0 -> `4995e4dfad21744440f485554c5db52c1c9cac14`, `protean-sym2p` v1.0.0 -> `43334b2fa756b11481a7c021931870864f95ea71`, `protean-control-plane` v1.1.0 -> `6c7e8f8e7b13c882a37e2cd0847ecdc17dc489cb`. The rewrite was content-preserving: `treeSha256`, `fileCount`, contract line, and `installTargets` are unchanged for all six, so only the `sha` fields move. `protean-handoff` v1.0.0 and `protean-context` v0.1.0 were already current and are untouched. `generatedAt` moves to the re-pin date.
- **Why:** a cold-cache `bash install.sh --all` could not install the kit at all; the lock named commits that no published ref reaches.
- **Also in this change:** `build/check-lock.py --verify` now reports each pin's verification provenance (`remote`, `cache`, or `ingredients-dir`) and re-resolves every tag against the remote even when a warm cache entry sits at the pinned sha, failing closed when the published tag has moved. `--verify --offline` keeps passing on warm caches but says plainly that the remote tag pairing was not re-resolved, so a local cache can never present itself as a published-release verification.
- **Evidence class:** [VERIFIED — internal operating record]; the new shas were read from the remotes and their `protean-tree-v1` hashes recomputed against freshly fetched trees.
- **Verification:** `python3 build/check-lock.py kit.lock.json --verify` passes (all eight pins at provenance `remote`); a fixture reproducing the issue's warm-cache scenario now fails closed where the pre-fix code passed; `python3 build/verify-all.py`, `tests.test_kit_lock`, and `tests.test_contribution_mode` all pass.
- **Open item:** the cold-cache acceptance install now clears every integrity check but stops at exit 5 inside the `protean-ops` ingredient's own `op-contrib-state` gate: its example `CONTRIB-STATE.md` carries a grant that expired 2026-09-24. That is the ingredient's content, not the composer's; it needs a `protean-ops` release and a follow-up re-pin, outside this change. The installer's closing README line (noted in #11) is likewise untouched as adjacent, out-of-scope observation.

- **What:** add an install-time contribution-mode toggle to the composer. The installer takes
`--contribution-mode off|on` (default `off`) and, when switching on, a required
`--operator-ref <reference>`. After a successful install it writes the contribution-state
Expand Down
47 changes: 44 additions & 3 deletions build/check-lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@
plus parity against ingredient trees that are already
present in the cache or an ingredients directory.
--verify requires every pin's tree to be present (fetching it from
the pinned tag when needed) and recomputes the sha pairing
and treeSha256 for each one; a mismatch is exit 4.
the pinned tag when needed), re-resolves every tag against
the remote, and recomputes the sha pairing and treeSha256
for each one. Each pin's verification provenance (remote
tag pairing or local tree only) is reported; a moved tag
or a hash mismatch is a violation.

Usage:
check-lock.py [LOCK] [--cache DIR] [--ingredients-dir DIR] [--verify] [--offline]
Expand Down Expand Up @@ -107,6 +110,22 @@ def descriptor_of(pin, cache, ingredients_dir):
return None, None


def remote_tag_sha(repo, ref):
"""Resolve a tag ref against the remote. Returns the peeled commit sha,
or None when the ref cannot be resolved."""
result = run(["git", "ls-remote", repo, ref, ref + "^{}"])
if result.returncode != 0:
return None
tag_sha = None
for line in result.stdout.splitlines():
sha, _, name = line.partition("\t")
if name == ref + "^{}":
return sha
if name == ref:
tag_sha = sha
return tag_sha


def main():
parser = argparse.ArgumentParser(
prog="check-lock.py",
Expand Down Expand Up @@ -216,15 +235,18 @@ def main():
base = xdg if xdg else os.path.join(os.path.expanduser("~"), ".cache")
cache = os.path.join(base, "protean-kit")

provenance = {}
for slug in sorted(seen):
pin = seen[slug]
tree = None
if args.ingredients_dir:
candidate = os.path.join(args.ingredients_dir, slug)
if os.path.isdir(os.path.join(candidate, ".git")):
tree = candidate
provenance[slug] = "ingredients-dir"
if tree is None and os.path.isdir(os.path.join(cache, slug, pin["sha"], ".git")):
tree = os.path.join(cache, slug, pin["sha"])
provenance[slug] = "cache"
if tree is None:
if args.verify:
if args.offline:
Expand All @@ -243,11 +265,24 @@ def main():
continue
run(["git", "checkout", "--quiet", "--detach", pin["sha"]], cwd=work)
tree = work
unchecked.append(slug)
provenance[slug] = "remote"
else:
unchecked.append(slug)
if tree is None:
continue
if args.verify and not args.offline and provenance.get(slug) != "remote":
# A warm cache or an ingredients-dir checkout can sit at the pinned
# sha while the published tag has moved on. --verify must never
# present a local cache entry as a published-release verification,
# so the tag pairing is re-resolved against the remote.
remote = remote_tag_sha(pin["repo"], pin["ref"])
if remote is None:
violations.append("%s: cannot resolve %s against the remote" % (slug, pin["ref"]))
elif remote != pin["sha"]:
violations.append("%s: %s resolves on the remote to %s, not the pinned %s"
% (slug, pin["ref"], remote, pin["sha"]))
else:
provenance[slug] = "remote"
head = run(["git", "rev-parse", "HEAD"], cwd=tree)
if head.returncode == 0 and head.stdout.strip() not in (pin["sha"], ""):
violations.append("%s: tree is at %s, not the pinned %s"
Expand All @@ -272,6 +307,12 @@ def main():
print("note: " + note)
if unchecked:
print("unverified (no tree available; run --verify): %s" % ", ".join(sorted(set(unchecked))))
if args.verify:
for slug in sorted(seen):
print("verify: %s [provenance: %s]" % (slug, provenance.get(slug, "no tree")))
if args.offline:
print("note: --verify --offline checked pins against local trees only; "
"the remote tag pairing was not re-resolved")
if violations:
print("LOCK VIOLATION: %d" % len(violations))
for v in violations:
Expand Down
14 changes: 7 additions & 7 deletions kit.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"version": "2.0.0",
"tag": "v2.0.0"
},
"generatedAt": "2026-09-17T00:00:00Z",
"generatedAt": "2026-10-08T14:15:00Z",
"allowedHosts": [
"github.com"
],
Expand All @@ -27,7 +27,7 @@
"slug": "protean-doctrine",
"repo": "https://github.com/aska-digital/protean-doctrine",
"ref": "refs/tags/v1.1.0",
"sha": "f72d6da9be8bdf11dfa300ace7cbd47e2453fa79",
"sha": "838f71ea00139bc79643ee5625523733bdf0652d",
"version": "1.1.0",
"contract": "The operating doctrine: the default pipeline, role delegation map, handoff protocol, QA gates, and external-writing discipline.",
"kind": "skills",
Expand Down Expand Up @@ -63,7 +63,7 @@
"slug": "protean-ops",
"repo": "https://github.com/aska-digital/protean-ops",
"ref": "refs/tags/v1.1.0",
"sha": "0b78ff77a2ba32c1b0d1c6464a18ad36053ed303",
"sha": "f278db48e8e58f3107df8fd1391c4a145e5ffea0",
"version": "1.1.0",
"contract": "The dispatch/rotation/inflight ops kit: append-only record schemas and the deterministic gates that check them.",
"kind": "records",
Expand Down Expand Up @@ -129,7 +129,7 @@
"slug": "protean-drafts",
"repo": "https://github.com/aska-digital/protean-drafts",
"ref": "refs/tags/v1.0.0",
"sha": "f09703b05a5491074f870fe6fdc15f81baa71d0e",
"sha": "c091210c296bad9366cb6e4bb7ebf531d6bc40e3",
"version": "1.0.0",
"contract": "The draft-review pipeline: render a draft as a self-contained dark HTML review page through one command that runs the prose, identifier, quote-integrity, and render-fidelity gates.",
"kind": "skills",
Expand Down Expand Up @@ -174,7 +174,7 @@
"slug": "protean-github-flow",
"repo": "https://github.com/aska-digital/protean-github-flow",
"ref": "refs/tags/v1.0.0",
"sha": "e79f1685b0d429676fd5e4a6273da35d334a9b60",
"sha": "4995e4dfad21744440f485554c5db52c1c9cac14",
"version": "1.0.0",
"contract": "The GitHub workflow skill pack: issue triage, issue-to-PR, PR lifecycle, PR audit, and upstream-contribution procedures.",
"kind": "skills",
Expand Down Expand Up @@ -216,7 +216,7 @@
"slug": "protean-sym2p",
"repo": "https://github.com/aska-digital/protean-sym2p",
"ref": "refs/tags/v1.0.0",
"sha": "efeec42a77c04bf4d3b40dbc780e07885f16f02f",
"sha": "43334b2fa756b11481a7c021931870864f95ea71",
"version": "1.0.0",
"contract": "SYM-2P: a compact versioned agentic message language - one canonical JSON packet per line over durable text artifacts - with validator, enums, and fixtures.",
"kind": "skills",
Expand Down Expand Up @@ -268,7 +268,7 @@
"slug": "protean-control-plane",
"repo": "https://github.com/aska-digital/protean-control-plane",
"ref": "refs/tags/v1.1.0",
"sha": "ca71a6686710e713a3dc8f2fad666b892c300d37",
"sha": "6c7e8f8e7b13c882a37e2cd0847ecdc17dc489cb",
"version": "1.1.0",
"contract": "The control-plane skill pack: one canonical procedure home that routes a request to the minimum skill bundle, with the gate table and failure policy.",
"kind": "skills",
Expand Down
Loading
Loading