From aa929ac39f16bfcb31237e963c3c23cb7a362d63 Mon Sep 17 00:00:00 2001 From: YZJF <195568136+YZJF@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:31:25 +0800 Subject: [PATCH] docs(catalog): add IP-032 completed-work archive with durable decision retention `loopx todo archive-completed` encodes three interaction rules that no catalog entry explains to humans: 1. a done todo carrying a durable standing decision is not a move candidate, and the decision must still resolve as active standing authority after the move (`retained_standing_decision_count`, `standing_decision_authority_v0`); 2. the archive only touches the section for the requested role, so `--role user` must leave `Agent Todo` alone while the role defaults to `agent`; 3. without `--execute` the command is a preview that must not change the state file. Today `examples/control_plane/todo-archive-completed-smoke.py` and `examples/control_plane/todo-standing-decision-authority-smoke.py` encode this behavior but nothing in the catalog names it, which is exactly the "a smoke encodes a behavior that is not yet explained to humans" case in the catalog maintenance rules. The entry explicitly separates itself from IP-020 (claim / supersede / successor lifecycle) and IP-014 (how a decision is written): neither owns what happens to a durable decision once the todo carrying it leaves the active window. Pattern-To-Canary matrix: IP-032 joins State And Boundary, and "completed-work archive" joins that family's trigger surfaces. The catalog smoke also gains a data-driven structural check: pattern ids own exactly one table row, every row has its detail heading (and vice versa), and no id is listed under more than one family in the Pattern-To-Canary matrix, so an id collision like the one this entry initially shipped cannot pass silently again. Rebased onto main f4ed58de9; the entry is numbered IP-032 because IP-030 (Machine Configuration Preview And Revision-Guarded Apply) and IP-031 (Manager Context Is Not Turn Authority) are already taken on main. Validation: - python3 examples/interaction-pattern-catalog-smoke.py -> ok (now also enforces id uniqueness / family membership / detail-heading pairing) - python3 examples/canary/catalog-planner-smoke.py -> ok - python3 examples/docs-governance-smoke.py -> ok - loopx check --scan-path docs/concepts/interaction-pattern-catalog.md -> ok, errors=0, public boundary scan clean - mutation check: duplicating an id or listing it under two families now fails the new structural assertions Role isolation is documented as a future CLI-level smoke because it is still helper-level only; the gap is stated rather than hidden. Docs-only; no runtime, validation, or benchmark behavior changes. Signed-off-by: YZJF <195568136+YZJF@users.noreply.github.com> --- docs/concepts/interaction-pattern-catalog.md | 87 ++++++++++++++++++- examples/interaction-pattern-catalog-smoke.py | 41 +++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/docs/concepts/interaction-pattern-catalog.md b/docs/concepts/interaction-pattern-catalog.md index a81a0bc65c..249dd44c29 100644 --- a/docs/concepts/interaction-pattern-catalog.md +++ b/docs/concepts/interaction-pattern-catalog.md @@ -88,7 +88,7 @@ Map P0/P1 catalog rows to canary archetypes before picking commands: | --- | --- | --- | --- | --- | --- | | Work Routing | IP-001, IP-002, IP-003, IP-007, IP-008, IP-021, IP-029 | Hot-path route canary; Planning governance canary when cadence or repair is involved | `quota should-run`, `interaction_contract`, `work_lane_contract`, scheduler hint, handoff todo state | one eligible delivery fixture, one blocked/fallback fixture, one quiet or monitor fixture | agent turn routing is unsafe: it may spend, wait, notify, or choose fallback incorrectly | | Human Decision | IP-004, IP-014, IP-017, IP-027, IP-030 | Scoped decision canary; Product/readiness canary when first-screen human copy changes | user todos, decision scope, operator-gate/reward preview, deferred resume candidates | one concrete user ask, one scoped non-blocking gate, one preview-or-append dry run | humans may be asked the wrong question, or an agent may continue without the needed decision | -| State And Boundary | IP-005, IP-006, IP-011, IP-016, IP-019, IP-020, IP-022, IP-023, IP-025, IP-026, IP-028, IP-031 | Projection and boundary canary; Hot-path route canary when the projection feeds quota/status | active state, todo metadata, task graph, authority source, claim lease, connector runtime policy, public/private scan | fixture state plus structured projection check; boundary scan for touched public files | compact state and executable truth diverge, so dashboards and agents may trust stale or unsafe authority | +| State And Boundary | IP-005, IP-006, IP-011, IP-016, IP-019, IP-020, IP-022, IP-023, IP-025, IP-026, IP-028, IP-031, IP-032 | Projection and boundary canary; Hot-path route canary when the projection feeds quota/status | active state, todo metadata, task graph, authority source, claim lease, completed-work archive, connector runtime policy, public/private scan | fixture state plus structured projection check; boundary scan for touched public files | compact state and executable truth diverge, so dashboards and agents may trust stale or unsafe authority | | Evidence Lifecycle | IP-012, IP-015 | Evidence lifecycle canary; Product/readiness canary when evidence is rendered | external handle observation, benchmark lifecycle reducer, compact result projection | compact public-safe evidence fixture with raw-material exclusion assertions | progress evidence may be missing, double-counted, or represented with unsafe raw material | | Planning Governance | IP-010, IP-013, IP-018, IP-024 | Planning governance canary; Hot-path route canary when cadence changes affect execution | stalled run history, autonomous replan obligation, repair delta, cadence hint, plan-to-todo writeback | two-turn stalled fixture plus repair/writeback delta assertion | the agent may keep planning in prose while the machine-visible frontier stays unchanged | @@ -341,6 +341,7 @@ Projection, authority, write scope, and lease integrity. | P1 | IP-025 | Experimental Diagnostic Sidecar Boundary | Runtime/protocol owners | no interruption unless an opt-in proof asks for user action | keep proof/debug verdicts as sidecar diagnostics until a product-general schema is validated | | P1 | IP-028 | Connector Runtime Boundary | Connector/runtime owners | notify only if the required owner decision is missing | enforce runtime allow/deny policy before browser or API connector reads can autoload raw material | | P1 | IP-031 | Manager Context Is Not Turn Authority | Manager connection owner | no interruption; retention is silent | retain group context only and act only on a provider-native mention, verified reply, or existing typed authority | +| P1 | IP-032 | Completed Work Archive With Durable Decision Retention | Archive selector plus controller | no interruption; preview-then-execute readback | treat archived done work as history, keep durable decisions authoritative, and never move another role's lane | ### Evidence Lifecycle @@ -2255,6 +2256,90 @@ request database. - `tests/extensions/test_lark_goal_topic_runtime.py`; - `tests/extensions/test_lark_goal_topic_connections.py`. +#### IP-032 Completed Work Archive With Durable Decision Retention + +**Trigger** + +- a role-scoped todo lane holds more done todos than the active window allows, + so `loopx todo archive-completed --max-active-done ` has something to move; +- a done todo carries a durable decision receipt, so later turns still depend on + it even though the todo itself is finished; and +- the caller picks a lane with `--role user` or `--role agent`, and omits + `--execute` for a preview. + +**Expected behavior** + +Archive is a storage move, not a decision loss. The command moves finished work +out of the active lane into the `Completed Work Archive` section, and three +rules bound what that move may do. + +1. **Retention.** A done todo carrying a durable standing decision is not a move + candidate. The payload reports `retained_standing_decision_count`, and after + the move the decision still resolves as active standing authority under + `standing_decision_authority_v0`. Archiving completed work must never be the + reason a settled policy has to be re-decided. +2. **Role scope.** The archive only touches the section for the requested role. + `--role user` moves out of `User Todo / Owner Review Reading Queue` and must + leave `Agent Todo` untouched. The role defaults to `agent`, so a caller that + means the user lane has to say so. A todo whose role contradicts its active + section is rejected rather than silently relocated. +3. **Preview.** Without `--execute` the command is a dry run. The preview must + not change the state file, and the preview payload must describe exactly what + the execute run would move. + +Moved blocks keep their role identity with a `` +marker inside the mixed archive section, so the archive stays readable by lane +instead of collapsing ownership into one undifferentiated list. + +IP-020 owns claim, supersede, and successor lifecycle, and IP-014 owns how a +decision is written. Neither owns what happens to a durable decision when the +todo carrying it leaves the active window, which is the gap this pattern fills. + +**Visual Model** + +```mermaid +flowchart TD + A["done todos exceed --max-active-done"] --> P{"--execute?"} + P -->|"no"| V["preview payload, state file unchanged"] + P -->|"yes"| R{"requested --role"} + R -->|"user"| U["scan User Todo section only"] + R -->|"agent"| G["scan Agent Todo section only"] + U --> S{"todo carries durable standing decision?"} + G --> S + S -->|"yes"| K["retain in active lane
retained_standing_decision_count += 1"] + S -->|"no"| M["move to Completed Work Archive
preserve role marker"] + K --> Z["authority still resolves as active"] + M --> Z +``` + +**Bad smell** + +An agent tidies the active lane, the durable policy decision is compressed away +with the todo that carried it, and two turns later the agent re-asks a question +the user already answered or re-litigates an approved policy. The archive +"cleaned up" the only durable record of the decision. + +The opposite bad smell is ownership bleed: an operator runs `--role user` +expecting to tidy the user lane and the agent lane moves too, so the archive +section mixes decisions nobody can attribute later. A third bad smell is +treating a dry-run preview as applied, after which status and projection +quietly disagree with what the operator believes happened. + +**Validation** + +- `examples/control_plane/todo-archive-completed-smoke.py` owns the CLI-level + archive move, preview, and payload metadata. +- `examples/control_plane/todo-standing-decision-authority-smoke.py` owns + standing-decision retention and authority, including + `assert_archive_retains_standing_receipt`. +- `loopx/control_plane/todos/completed_archive.py` and + `loopx/control_plane/coordination/todo_archive_selection.ts` own the typed + selector behind the command. +- `examples/interaction-pattern-catalog-smoke.py` protects this entry. +- Future smoke: role isolation is currently proven at helper level; a CLI-level + assertion that `--role user` leaves `Agent Todo` byte-identical is proposed + and not yet landed. + ### Evidence Lifecycle #### IP-012 External Evidence Observation diff --git a/examples/interaction-pattern-catalog-smoke.py b/examples/interaction-pattern-catalog-smoke.py index 266f62d1cc..d267513bc3 100644 --- a/examples/interaction-pattern-catalog-smoke.py +++ b/examples/interaction-pattern-catalog-smoke.py @@ -3,6 +3,7 @@ from __future__ import annotations +import re import sys from pathlib import Path @@ -30,6 +31,39 @@ def require(text: str, snippets: list[str], *, source: Path) -> None: assert not missing, f"{source}: missing {missing}" +def require_catalog_structure(text: str, *, source: Path) -> None: + table_ids = re.findall(r"^\| P\d \| (IP-\d{3}) \| ", text, re.MULTILINE) + duplicated = sorted({pid for pid in table_ids if table_ids.count(pid) > 1}) + assert not duplicated, f"{source}: pattern ids own more than one table row: {duplicated}" + + detail_ids = re.findall(r"^#### (IP-\d{3}) ", text, re.MULTILINE) + missing_detail = sorted(set(table_ids) - set(detail_ids)) + assert not missing_detail, f"{source}: pattern rows without a detail heading: {missing_detail}" + orphan_detail = sorted(set(detail_ids) - set(table_ids)) + assert not orphan_detail, f"{source}: detail headings without a pattern row: {orphan_detail}" + + matrix_block = re.search( + r"^\| Family \| P0/P1 Pattern Coverage \|[^\n]*\n\|[^\n]*\|\n(.*?)\n\n", + text, + re.MULTILINE | re.DOTALL, + ) + assert matrix_block, f"{source}: Pattern-To-Canary matrix block not found" + family_cells = re.findall( + r"^\| [^|]+ \| ((?:IP-\d{3}, )*IP-\d{3}) \|", + matrix_block.group(1), + re.MULTILINE, + ) + family_ids = [pid for cell in family_cells for pid in cell.split(", ")] + split_families = sorted({pid for pid in family_ids if family_ids.count(pid) > 1}) + assert not split_families, ( + f"{source}: pattern ids listed under more than one family: {split_families}" + ) + unknown_matrix_ids = sorted(set(family_ids) - set(table_ids)) + assert not unknown_matrix_ids, ( + f"{source}: family matrix lists ids without a pattern row: {unknown_matrix_ids}" + ) + + def main() -> int: catalog = CATALOG.read_text(encoding="utf-8") state_model = STATE_MODEL.read_text(encoding="utf-8") @@ -104,6 +138,11 @@ def main() -> int: "browser_open_allowed_before_gate: false", "message-list or\nmessage-detail APIs", "UI display limit must not become the control-plane reasoning window", + "IP-032 | Completed Work Archive With Durable Decision Retention", + "Archive is a storage move, not a decision loss.", + "retained_standing_decision_count", + "The role defaults to `agent`", + "examples/control_plane/todo-archive-completed-smoke.py", "## Catalog Maintenance And Validation Design", "Do not add\na new IP merely because a maintainer needs a validation technique", "Those are uses of the\ncatalog, not catalog patterns by themselves.", @@ -164,6 +203,8 @@ def main() -> int: source=SELF_REPAIR_PATTERNS, ) + require_catalog_structure(catalog, source=CATALOG) + # Every registered built-in machine-configuration namespace must be # discoverable from the catalog, so a new capability cannot land as a # silent omission in the IP-030 inventory.