feat: capability ladder Rung 0/1/2 — temporal-correlation spine + COP internals#1
Conversation
…MO proposal adopted Capture the Warpline capability ladder (Rung 0-4) as the roadmap backbone, the temporal-correlation spine and its consumer surface (the temporal common operating picture), and the PMO proposal that took the spine to the foundation. - roadmap.md: capability ladder; correlation spine RATIFIED (hub PDR-0025); the COP read surface; PM conditions inlined (squash-merge demo, episode ~= work-session, sequencing fence behind launch cutover + base impl). - PDR-0002: adopt the capability ladder as roadmap backbone. - PDR-0003: relay the hub ruling (PDR-0025) sponsoring warpline's temporal-episode axis into warpline's decision log, with all three PM conditions. - PMO proposal (adopted): §7 corrected — SHA-rewrite (squash/rebase) is distinct from rename (PDR-0021); the rename feed carries no rewrite reconciliation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Execution plan for the capability-ladder work, produced by a multi-agent design workflow (map -> specialist design -> reality/architecture/doctrine review -> synthesis) and hardened by a second adversarial build-readiness review. - Folds PDR-0025 conditions: squash-merge reconstruction demo as the load-bearing acceptance criterion, episode ~= work-session, sequencing fence behind launch. - Owner-session enhancements E1-E6 (demo deliverable, fence, full verification gate incl. wardline, dirty-tree honest fallback, co-change kill-switch, 1.1.0). - Round-2 resolutions (authoritative): fix a Rung 0 NameError blocker (_EDGES_FOR_COMPLETENESS), DEFER Track B (no wardline resolved-finding surface), pin migration v2<v3 ordering, Rung 1d always-on (no inputSchema field), plus ~20 reality/sequencing fixes. Build-ready for base-impl tracks; COP public surface remains interface-pending. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Behaviour-preserving decomposition of commands.py: characterization tests first, then extract the pure staleness/completeness helpers into warpline._enrichment and the blast-pipeline prep helpers into warpline._blast. - _enrichment.py: EDGES_FOR_COMPLETENESS + is_stale/edges_enrichment/ staleness_warnings/completeness_warnings. Import-free except typing.Any (structurally incapable of gating). is_stale uses int(behind) (R2). - _blast.py: rev_range_commits/resolve_changed_inputs/enrich_blast with a private strict-assert _as_int. WarplineStore is always a parameter. - commands.py 959 -> 771 LOC (<800, M1). capture_snapshot keeps using EDGES_FOR_COMPLETENESS (B1); _as_int and the 2 rev_range_commits sites stay (M2). Zero change to the 7 tool signatures, 6 SCHEMA_* constants, cli.py, mcp.py behaviour. - Lift _init_repo/_commit into tests/conftest.py (conftest-helper minor). - New characterization suites: test_enrichment_helpers.py (incl. B1 dict access) + test_blast_helpers.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Establish a real ordered, forward-only migration runner in store.py as the prerequisite gate for all schema work (Rung 1b v2 anchors, Rung 2 v3 co-change). The runner is established here with an empty MIGRATIONS list (highest known version stays 1); v2/v3 land in later tracks. - Connection hardening: foreign_keys=ON, busy_timeout=5000, synchronous=NORMAL (WAL retained via SCHEMA executescript fresh-DB note). - SQLite >= 3.35 floor, justified by the RETURNING clause in create_edge_snapshot (no migration drops a column). - Per-step BEGIN IMMEDIATE / COMMIT with conn.execute only (never executescript) so user_version + meta update atomically (R3). - Concurrent open() re-reads user_version under the RESERVED lock and skips already-applied steps (idempotent, no double-apply). - M9 reconcile: user_version==0 with meta.schema_version=='1' adopts 1; with a divergent meta value, warn + adopt it before later steps. - user_version > highest-known: warn to health_log, reads stay safe, no fail. - SCHEMA DDL is FROZEN after Rung 1a (all change via MIGRATIONS). - test_store.py BOTH schema_version()==1 assertions intentionally left at 1 (they flip to ==2 when v2 lands in Rung 1b, not here). Add tests/test_store_migrations.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1b, schema v2) Migration v2 adds detected_branch, detected_head_sha, detected_at, and detected_context (clean|working_tree_dirty|detached_head, M8/E4) to change_events — the working-context anchor for the DETECTION act, orthogonal to the SEI and never on entity_keys. git.py ingest_commit computes the anchor once per call and threads it onto every change_event it writes; backfill records all anchor columns NULL (B3: reconstruction is not detection). store.append_change_event takes optional anchor kwargs; list_change_events and timeline surface the new columns. Updates both test_store.py schema_version assertions to ==2 and reconciles the Rung 1a migration-runner tests to a non-empty MIGRATIONS list (highest known version 2). Adds tests/test_anchor_capture.py covering branch/dirty/detached detection, the B3 backfill all-NULL rule, the store read surface, the v1->v2 migration, and the M10 additive-column non-regression for change_list / entity_timeline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the idempotent UPDATE-or-merge core that heals entity keys minted while loomweave was unavailable (sei IS NULL). store.null_sei_entity_keys pages the worklist; store.reresolve_entity_key_sei repoints in place, or on a resolved-sei twin collision repoints change_events, DELETEs null-keyed duplicate events (resolved-keyed row canonical, M5/R11), deletes the orphan null key, and carries min(first)/max(last) seen onto the survivor. reresolve.sweep_reresolve_sei orchestrates per-locator resolution and reports the loomweave posture explicitly (present|absent|unavailable); client=None is a pure no-op that never marks a key resolved-to-null. The reresolve-sei CLI verb drives it and is NON-FROZEN/internal (not one of the six frozen v1 tools). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ung 1d) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e reconstruction demo (Rung 2 Track D) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d5edbc36f3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| repo, | ||
| client=client, | ||
| source_version=source_version, | ||
| scope_locators=scope_locators or None, |
There was a problem hiding this comment.
Avoid marking scoped lazy snapshots as full
When the first impact_radius/reverify call on a cold store has resolved seed IDs, this passes only those seed locators into capture_edge_snapshot; that helper still records the snapshot as FULL unless there are failures or caps. As a result, a depth>1 traversal can miss edges beyond the seed neighborhoods, and later queries for other entities skip recapture while reporting edges: present/completeness: FULL over a partial graph. Either capture the full graph here or make scoped captures surface as partial and not suppress future captures.
Useful? React with 👍 / 👎.
| ON CONFLICT(repo_id, entity_key_id_a, entity_key_id_b) DO UPDATE SET | ||
| co_change_count = co_change_count + 1, | ||
| last_co_change = excluded.last_co_change, |
There was a problem hiding this comment.
Keep co-change counts idempotent per commit
If the same commit is ingested/backfilled more than once, append_change_event is idempotent but this conflict handler still increments co_change_count again for the existing pair. That makes warpline backfill reruns or repeated ingest-commit HEAD calls inflate coupling counts and confidence without any new co-change commit; the upsert needs to distinguish an already-counted last_commit_sha (or track commit-pair membership) instead of always adding one.
Useful? React with 👍 / 👎.
| # Delete the now-orphaned null key (its events were repointed/merged). | ||
| self.conn.execute("DELETE FROM entity_keys WHERE id = ?", (null_key_id,)) |
There was a problem hiding this comment.
Repoint co-change pairs when merging SEI twins
When a null-SEI key collides with an already-resolved twin, this deletes the null key after repointing change_events, but any co_change_pairs rows that were written for the null key remain keyed to the deleted id. In the normal hook order (ingest-commit writes pairs, then reresolve-sei heals), those orphaned pairs disappear from co_change_partners for the surviving entity and can also make the other partner skip the missing key, so the coupling graph loses historical co-change evidence unless the pair rows are merged/rebuilt in the same transaction.
Useful? React with 👍 / 👎.
| store.log_health(args.repo, "HOOK_INGEST_FAILED", str(exc)) | ||
| return 0 | ||
| if args.command == "loomweave-probe": | ||
| if args.command == "reresolve-sei": |
There was a problem hiding this comment.
Restore the loomweave-probe CLI branch
This new reresolve-sei branch replaced the old if args.command == "loomweave-probe" guard but left the probe payload after a return, so invoking warpline loomweave-probe --json now falls through to the top-level help with exit 0 instead of probing loomweave. The parser still advertises the subcommand, so callers and doctor-style diagnostics get no probe result until the loomweave-probe handler is restored as its own branch.
Useful? React with 👍 / 👎.
| # Managed by Warpline. Fail-soft by design: Warpline must never block commits. | ||
| {executable} ingest-commit HEAD >/dev/null 2>&1 || true | ||
| {executable} reresolve-sei --limit 25 >/dev/null 2>&1 || true | ||
| {executable} capture-snapshot --commit HEAD >/dev/null 2>&1 || true |
There was a problem hiding this comment.
Keep full captures out of the commit hook
In repos where loomweave is installed and indexed, this hook now runs capture-snapshot synchronously on every git commit; the CLI defaults to a full capture, which probes loomweave and can query every stored entity neighborhood before the commit command returns. That violates the hook's own fail-soft/non-blocking contract for nontrivial repos even though the exit code is swallowed, so the capture should be made bounded/backgrounded or left to the lazy read path instead of running full in post-commit.
Useful? React with 👍 / 👎.
| # added the reresolve-sei + capture-snapshot lines (install.py:hook_body); an | ||
| # older installed hook lacks them, so `doctor` flags it stale and `--fix` | ||
| # reinstalls (R5 — editing hook_body alone never rewrites installed hooks). | ||
| HOOK_CURRENCY_SENTINEL = "reresolve-sei" |
There was a problem hiding this comment.
Check every current hook line in doctor
The doctor currency check only looks for reresolve-sei, so a managed hook that has that line but is missing the new capture-snapshot line is reported as current and doctor --fix will not repair it. In that stale-hook state commits keep ingesting/re-resolving but never capture snapshots, which is exactly the newly added hook behavior doctor is meant to enforce; make the sentinel require all current managed commands or compare against the generated block.
Useful? React with 👍 / 👎.
| if since is not None: | ||
| clauses += " AND ce.changed_at >= ?" | ||
| params.append(since) | ||
| if until is not None: | ||
| clauses += " AND ce.changed_at <= ?" | ||
| params.append(until) |
There was a problem hiding this comment.
Normalize timestamps for time-window filters
The new COP time_window path filters changed_at with string comparisons, but the stored values come from git %aI and can include arbitrary timezone offsets. For events authored outside UTC, lexicographic ordering can include changes before since or exclude changes before/after until incorrectly (for example 2026-01-01T00:30:00+02:00 sorts after a UTC since even though it is earlier in absolute time), so these bounds need to compare normalized instants rather than raw text.
Useful? React with 👍 / 👎.
| # Delete the now-orphaned null key (its events were repointed/merged). | ||
| self.conn.execute("DELETE FROM entity_keys WHERE id = ?", (null_key_id,)) |
There was a problem hiding this comment.
Repoint snapshot edges when merging SEI twins
The same null-key merge also leaves snapshot_edges rows pointing at the deleted entity id. If a snapshot was captured before the SEI later resolves to an existing twin, impact_radius seeded with the survivor cannot traverse edges whose source/target is still the orphaned id, so affected-set results lose previously captured graph evidence until a full recapture happens; merge or rewrite those snapshot edges in this transaction before deleting the null key.
Useful? React with 👍 / 👎.
| """ | ||
|
|
||
| repo_id = self.ensure_repo(repo) | ||
| self.clear_co_change_pairs(repo) |
There was a problem hiding this comment.
Honor the co-change kill switch during rebuild
rebuild_co_change_pairs clears the existing graph before replaying commits, but the replay path now honors WARPLINE_COCHANGE=0 by skipping every write. Running warpline rebuild-coupling in an environment that disabled live co-change writes therefore deletes all existing coupling data and replaces it with an empty graph; check the kill switch before clearing, or make rebuild a no-op when writes are disabled.
Useful? React with 👍 / 👎.
…0006 Records the accept+ship decision (14-agent adversarially-verified review, verdict ship, 0 blockers/majors; verified-minor findings deferred to 4 tracked follow-ups). Roadmap: hardening out of Now (shipped); verification-freshness sole active Now bet. Metrics: 2026-06-24 reading for the 1.2.0 ship. Escalation #1 (merge+1.2.0) resolved; #2 (hub handover) carries forward. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the reviewed plan at
docs/plans/2026-06-16-rung-0-1-2-implementation-plan.md— the base-impl stabilization plus the temporal-correlation spine internals. Built on the ratified contract (hub PDR-0025) sponsoring warpline's temporal-episode axis.How this was produced
NameError(_EDGES_FOR_COMPLETENESS) and that Track B was unbuildable (no wardline resolved-finding surface). Verdict resolutions are authoritative in the plan's round-2 section.What's in (8 commits)
commands.py(959→771 LOC) into_enrichment.py+_blast.py.user_versiongate, atomicBEGIN IMMEDIATEper step).detected_branch/head_sha/at+detected_context(clean|working_tree_dirty|detached_head). The change-episode axis, orthogonal to SEI.reresolve.py+reresolve-seiCLI; idempotent merge of null-keyed rows).blast_radiuspreserved; hook + doctor checks). No new inputSchema field.risk/governancereverify enrichment (honest present|absent|unavailable).WARPLINE_COCHANGEkill-switch).cop.pyCOP internals (resolve_frameover rev_range/sei/time_window/edit/branch_sha +compose_temporal_copwith coverage/dark-sectors) + a non-frozen internalwarpline copdemo CLI.PDR-0025 acceptance
tests/integration/test_reconstruction_demo.pyexercises a real squash-merge fixture (N commits collapsed to one new mainline SHA, branch deleted) — reconstructs via SEI frame, degrades honestly withweft_reason_classon rev-range, branch_sha fallback stays useful. 3/3 pass.Verification (independent, on the branch)
ruffclean ·mypy --strictclean (33 files) · pytest 263 passed (was 166) ·wardline scan --fail-on ERRORexit 0 (0 active findings).Explicitly NOT in scope
cop.pyinternals + the non-frozen demo CLI are ready for it to drop onto.Doctrine
Enrich-only, never-gating; SEI never minted/parsed; no sibling data mirrored (read-time compose); honesty invariant throughout; no frozen v1 contract mutated (all additive). Per PDR-0025 cond 3 the spine sits behind the launch cutover in priority — this branch is ready but not a launch-cutover dependency.
🤖 Generated with Claude Code