feat(warpline): P1 integration — reverse-lookup lifecycle facts + commit anchor + atomic ingest#70
Conversation
…p (warpline b-ii)
GET /api/entity-associations?entity_id=<sei> (and the MCP/db reverse lookup)
now enriches each binding row with the bound issue's claimed_at, closed_at,
status, and status_category, so warpline can correlate "changed since the
issue was claimed/closed" against its own changed-set in one round trip.
closed_at is the proven-good signal ("issue closed at commit X"); Filigree
exposes the resolution timestamp verbatim and stores no commit SHA — warpline
maps timestamp->commit on its side.
Implemented as a separate enriched projection (EntityAssociationByEntityRow)
via a LEFT JOIN to issues — the shared _row_to_entity_association mapper, the
forward list_entity_associations, the add-response, and governance.py are all
untouched and byte-identical. LEFT (not INNER) JOIN keeps an orphaned binding
(issue row absent) in the result with null facts rather than dropping it.
Additive and Loomweave-safe: the Loomweave consumer
(parse_entity_associations_response) has no serde deny_unknown_fields and is
tested to ignore unknown fields, so the v1 consumer still parses v2. Contract
fixture bumped to fixture_version 2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t-UNBOUND) ingest_reverify_worklist created the issue and bound its SEI in two separate @_in_immediate_tx transactions, so a non-retryable storage error on the bind left the issue FILED-but-UNBOUND. Because the loop-closure contract keys on the SEI association warpline reads back, the next ingest then saw the entity as untracked and re-filed a duplicate. Route the bind through create_issue's existing atomic inline-bind path (entity_id / content_hash / entity_kind), so file+bind commit together in one transaction; a bind failure rolls the whole item back. The warpline content sentinel is preserved verbatim. Regression test simulates a bind failure and asserts no orphaned issue remains. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fy ingest Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…schema v29) Warpline correlates "changed since the issue was claimed/closed" on commits, not wall-clock timestamps. New nullable issues.claim_commit / issues.close_commit columns hold an opaque caller-supplied branch@sha the issue was claimed/closed at; Filigree stores it verbatim and never parses it (git/CI is Legis's domain, matching how it treats content_hash on entity associations). - Schema v28->v29: additive nullable columns + idempotent migrate_v28_to_v29. - Capture: optional `commit` arg on close_issue/claim_issue/start_work/reclaim and update_issue (claim_commit/close_commit); plumbed through the MCP (issue_close/work_claim/work_start), CLI (--commit on close/claim/start-work), and HTTP close/claim routes. - Mirror invariant: the anchor is set wherever its timestamp is set and cleared wherever its timestamp is cleared — done-entry/leaving-done, assignee set/clear, claim (COALESCE-preserve), release, reclaim (OVERWRITE so the prior holder's anchor never survives), and the undo/restore paths (NULL — no commit to restore). A stale anchor can never outlive its claim/close. - Exposed on the issue read (classic + weft) and the entity-association reverse-lookup; NULL when no commit supplied -> warpline falls back to the timestamp. - Additive + Loomweave-safe; entity-associations contract fixture -> v3. With no commit supplied every existing flow is byte-identical (warpline-absent parity). 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: 8ad6826396
ℹ️ 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".
| claim_commit TEXT, | ||
| close_commit TEXT, |
There was a problem hiding this comment.
Preserve commit anchors during JSONL imports
With these new issue columns, export_jsonl already writes them because it uses SELECT * FROM issues (db_meta.py:947), but the JSONL import path still inserts issues with an explicit column list that stops at fields (db_meta.py:1205-1208; bulk_insert_issue does the same at db_meta.py:596-599). A backup/restore or project import of issues closed or claimed with commit will silently drop claim_commit/close_commit, causing restored reverse lookups to return null anchors and warpline to fall back to timestamps.
Useful? React with 👍 / 👎.
| claim_expires_at = _claim_expiry(now) | ||
| cursor = self.conn.execute( | ||
| f"UPDATE issues SET assignee = ?, claimed_at = COALESCE(claimed_at, ?), " | ||
| f"claim_commit = COALESCE(claim_commit, ?), " |
There was a problem hiding this comment.
Do not backfill claim_commit on same-holder claims
When an issue is already claimed by the same assignee but has no anchor (for example, it was claimed before v29 or claimed without --commit), a later idempotent claim_issue(..., commit=...) matches the existing-assignee branch and this COALESCE writes the later commit while preserving the old claimed_at. That produces a claim_commit that is not the commit where the claim actually started, so reverse-lookup consumers compare from the wrong point instead of falling back to claimed_at; only populate the anchor when claimed_at is being populated for a fresh claim.
Useful? React with 👍 / 👎.
| # Opaque branch@sha commit anchor (warpline seam, contract B) -> claim_commit. | ||
| commit: NotRequired[str] |
There was a problem hiding this comment.
Thread commit through all claim-producing verbs
This only adds commit to the direct claim args; the same lifecycle anchor is still unreachable from ReclaimIssueArgs, ClaimNextArgs, and StartNextWorkArgs, and their handlers call reclaim_issue / claim_next / start_next_work without a commit. In the normal auto-pick or stale-transfer flows, Filigree sets a new claimed_at but persists claim_commit as null even when the caller knows the current commit, so warpline cannot correlate those claims on commit and must fall back to timestamps.
Useful? React with 👍 / 👎.
| # Opaque branch@sha commit anchor (warpline seam, contract B) -> close_commit. | ||
| commit: NotRequired[str] |
There was a problem hiding this comment.
Preserve close_commit in batch close
The new commit field is wired only for the single-issue close path; I checked the batch close args/handler and FiligreeDB.batch_close, and they still have no commit parameter and call close_issue without one. Any caller using the MCP/HTTP batch-close endpoint at a known commit will close every issue with closed_at set but close_commit left null, forcing warpline back to timestamp correlation for exactly the multi-issue close workflow this anchor is meant to support.
Useful? React with 👍 / 👎.
… atomic ingest Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes warpline's P1 federation integration request (tracker:
filigree-1299255fb5). Single PR, all four commits, targetingmain.Under contract Option B, warpline computes "changed since the issue was claimed/closed" itself; Filigree exposes the facts.
Changes
GET /api/entity-associations?entity_id=<sei>(+ MCP/db) exposes the bound issue'sclaimed_at/closed_at/status/status_categoryvia a separateEntityAssociationByEntityRowprojection over aLEFT JOIN issues. Shared mapper / forward list / governance gate untouched + byte-identical. Loomweave-safe (consumer ignores unknown fields). Fixture → v2.filigree-8340b79615) —warpline_worklist_ingestfiled the issue and bound its SEI in two transactions (FILED-but-UNBOUND on partial failure → duplicate re-filing); now one transaction viacreate_issue's inline-bind.issues.claim_commit/close_commithold a caller-supplied opaquebranch@sha, so warpline correlates on commits not clocks. Filigree stores it verbatim, never parses it (git/CI is Legis's domain). Mirrored at everyclaimed_at/closed_atset and clear site (release/reopen/unassign/reclaim-overwrite/undo). Optionalcommiton close/claim/start-work across CLI/MCP/HTTP. Exposed on the issue read (classic+weft) + reverse-lookup. Fixture → v3.Boundary & acceptance
Filigree stays the work-state authority; nothing calls warpline or git. With no
commitsupplied / warpline absent, every existing flow is byte-identical.Tests
New
test_commit_anchor*.py+ extended entity-association/schema tests; fixtures v2→v3. Local gate green:ruff+ruff format --check+mypy src/filigree/+ fullpytest(exit 0; only the env-gated live-integration skip).Field names (
claim_commit/close_commit) are provisional pending the federation hub-glossary ruling; additive/renamable.🤖 Generated with Claude Code