Skip to content

Commit 1a4f1c9

Browse files
feat: add /PACT:refresh mid-workstream context checkpoint (v4.6.0) (#1150)
* refactor(hooks): remove dormant hooks/refresh package + legacy pact-refresh janitor Consumer-scan census (AST-anchored: Import + ImportFrom + string-literal patch-seams; comment mentions excluded by construction; 314 .py files across hooks/ scripts/ telegram/ skills/*/scripts/ tests/): - Zero non-test consumers of hooks/refresh/. - Nine consumers, all under tests/, each importing ONLY the refresh package; whole-file deleted with it. (One shared.constants.PACT_AGENTS helper import in test_patterns.py is not a deletion target.) - String-seams (refresh.constants.LARGE_FILE_THRESHOLD_BYTES x4) live only inside deleted test_transcript_parser.py. - The package docstring's PreCompact-consumer claim is stale prose, not a consumer. session_end.py: drop _CHECKPOINT_MAX_AGE_DAYS, _cleanup_old_checkpoints, and its single main() call site. test_session_end.py: 18 janitor couplings removed. test_py39_annotation_compat.py: discovery floor 63->55 (scanned hooks 66->58, same slack; deliberate lowering documented inline). Full suite: 10466 passed, 11 skipped, 0 failed, 0 errors. * feat(journal): register session_refreshed + session_refresh_consumed event schemas session_journal.py: session_refreshed required {consolidation_completed: bool, halt_active: bool} + session_refresh_consumed required {refresh_ts: str}, with 7 optional fields registered (halt_task_ids, feature_task_id, feature_subject, team_name, next_phase, worktrees, pr_number). Registry-convention comments adjacent to session_paused. Protocol rows added to pact-state-recovery.md with identical bytes mirrored into the pact-protocols.md SSOT region (same commit; extract parity PASSED). Tests: schema samples (minimal-event oracle), wrong-type sweeps, optional- fields pin, full-payload happy path, CLI subprocess round-trips including consumption bound to the surfaced refresh_ts. 19 net-new tests. Full suite: 10485 passed, 11 skipped, 0 failed, 0 errors. * feat(resume): add check_resume_state entry point with fail-safe refresh interpreter check_resume_state(prev_session_dir): single public resume seam — one read per event type, spent-gate, per-leg interpretation, arbitration only when both claims survive. _interpret_refreshed_event: totality by signature (returns str, no None path; fail-safe-toward-surfacing stated as MUST). Composition rules include the 48h STALE downgrade (never suppression), verbatim machine-copyable refresh_ts= echo, UNAVAILABLE sentinel, and the RESPAWN-BEFORE-SEND trailer. _refresh_is_spent: exact-string claim binding AND >= temporal-sanity second conjunct; every failure path lands UNSPENT (suppress-direction belt). _arbitrate: parsed-ts newest-wins, ties to refreshed, losing claim mentioned, unparseable ts surfaces BOTH with a CONFLICT note. has_unspent_refresh: presentation-only boolean, any-error-False. Pause leg split into _interpret_paused_event with byte-identical logic; existing surfaces and all 84 prior resume tests unchanged. Tests +34: fail-safe tier, interpreter fail-direction counter-test, spent-check matrix (orphan consumption, two-refreshes-one-consumed, unparseable), HALT adversarial sweep, arbitration orders/ties, pr_number never gates. Full suite: 10529 passed, 11 skipped, 0 failed, 0 errors. * feat(session-init): surface refreshed state at compact and startup seams Seam A: step-8 call swapped to check_resume_state inside the frame_is_lead gate (placement and append semantics unchanged). Seam B: the compact branch computes refresh_pending once via has_unspent_refresh; when true, the Re-engage-secretary sentence is replaced with the RESPAWN-BEFORE-SEND directive, the auto-compacted wording is neutralized, and the agent list is re-labeled "Pre-refresh agents (STOPPED - respawn before messaging)"; when false the branch text is byte-identical (test-pinned). Both session_init docstrings reworded byte-equal (step 8: paused or refreshed work). Mock migration: 55 exact patch(check_paused_state) seams across 8 files re-pointed to check_resume_state; spy-wrapper test reworked around the new entry point; direct-function tests on the public check_paused_state wrapper intentionally stay. TestRefreshSurfacingMatrix (10 non-mocked seam tests, real main() round-trip): lead x {compact,resume,startup} surfaces prompt + HALT line; non-lead frames never receive it; quit-path topology; byte-identity without refresh; spent-refresh reverts to legacy wording. Counter-test-by-revert executed: reverting the step-8 swap turns exactly 5 tests red (cardinality documented). Full suite: 10539 passed, 11 skipped, 0 failed, 0 errors. * feat(session-end): extend paused-session journal guard to checkpointed sessions _is_checkpointed_session: pure existence-OR over {session_paused, session_refreshed}, no timestamp comparison (the refresh-then-quit race makes any ts-ordering guard unsound; existence semantics transfer the prior race lessons verbatim). Single call-site swap at the cleanup_old_sessions threshold select. _is_paused_session kept intact (existing tests unchanged; retained per design as the paused-leg primitive). Scope note documented in the guard: the tasks/teams reapers remain journal-blind at 30d, so past 30d a refreshed session degrades to journal-only fidelity where the loud-mismatch warning is the designed behavior. Tests +9: 5-row existence-OR truth table, refresh-then-quit race, no-ts event still counts, missing journal, 35d refreshed journal survives the active TTL, ages out past the checkpoint bound, non-checkpointed 35d control still reaped. Full suite: 10550 passed, 11 skipped, 0 failed, 0 errors. * feat(commands): add refresh.md mid-workstream checkpoint + bootstrap.md resume clauses commands/refresh.md (new): mid-workstream context checkpoint — preflight degenerate-case handling (confirm, then unconditional minimal event), HARVEST with an explicit drain-confirmation gate before shutdown, read-only PERSIST/VERIFY with full payload computation, journal writes mirroring the pause byte-conventions (shell-clamped pre-emit, quoted-heredoc --stdin), staggered graceful-then-TaskStop shutdown (roster-entry removal expected; team config file preserved), and STANDBY (no new dispatches, RESPAWN-BEFORE-SEND, expect-and-ignore post-shutdown stragglers, /compact canonical with /clear tolerated). commands/bootstrap.md: secretary same-session respawn carve-out; refreshed- state paragraph (declared continuation, auto-proceed, ask only on inconsistency, HALT always surfaces); fire-once consumption write template bound to the surfaced refresh_ts (UNAVAILABLE skips the write); team-config bidirectional re-identify + ghost-detection clause. plugin.json: register ./commands/refresh.md (manifest-parity gate requires registration; version bump is deliberately separate). Structural pins for both command files + a consolidated-preemit no-false-warn integration test. Full suite: 10567 passed, 11 skipped, 0 failed, 0 errors. * chore: add refresh live-probe runbook + bump version to v4.6.0 * test(refresh): remove orphaned refresh fixtures, pin relabel full-absence Certification pass over the refresh feature. Coverage audit: every scenario- matrix bucket covered by the shipped tests; one gap filled (full-absence pin on the Active Agents relabel — a count=1 replace could leave a duplicate occurrence under upstream drift). Counter-test-by-revert independently re-measured: exactly 5 failed / 5 passed, same named tests, failing for the right reason; restored clean. Orphan cleanup (sister closure of the dormant-package removal): delete tests/fixtures/refresh_system.py + the six conftest re-exports + the five sample_transcripts jsonl fixtures — zero consumers confirmed by independent AST scan (299 files) plus proxy-bypassed grep, both sides. Drift-guard ruling: no new test — both str.replace targets are double-pinned through the real builder (byte-identity pins upstream literals; relabel test pins replaced output positively); every drift combination lands red. Full suite after edits: 10567 passed, 11 skipped, 0 failed, 0 errors; extract parity 19/19 PASS. * test(refresh): pin frame_is_lead conjunct on compact presentation seam + de-cite removed refresh package in test prose The lead-gate conjunct on the compact-branch refresh presentation was empirically unpinned (dropping it flipped zero tests). New negative assertion scoped to the unknown-role row — the one non-lead shape that reaches the compact branch; re-verified: the conjunct-drop mutation now flips exactly 1 test (vs 0 before), 340 others green under the mutation, file restored byte-identical. test_pact_context: rename + docstrings de-cite the deleted checkpoint_builder (zero external references to the old name; executed logic byte-identical). test_hook_infra_classifier: refresh-edge backstop reworded as a reintroduction canary. Full suite: 10579 passed, 11 skipped, 0 failed, 0 errors. * docs(refresh): correct session-start seam attribution, add team_name to checkpoint payload, note quit-path fire-once topology Protocol docs: the session-start row now attributes resume detection to check_resume_state() (both the extract and its SSOT region, same commit — the byte-parity gate cannot catch consistently-stale pairs, so content correctness was verified against the live call site). refresh.md: team_name added to the payload computation, full-payload write template, and field table (registered optional field; gives bootstrap a free identity cross-check). The degenerate minimal-event variant deliberately stays required-fields-only. bootstrap.md: one contract-honesty sentence — quit-path fire-once is enforced by the one-hop-back read topology; ts-bound consumption covers the /compact and same-session --resume paths. Extract parity: 19/19 PASSED. Full suite green (10575 passed, 0 failed, 0 errors). * docs(bootstrap): enumerate team_name mismatch as refreshed-state inconsistency * fix(refresh): sanitize resume-prompt field interpolation, preserve HALT line for superseded claims, warn on incomplete consolidation, compose checkpoint predicate _sanitize_prompt_field: control chars (incl. U+2028/U+2029) collapse to spaces; 200-char free-text bound, 512 for worktree path elements; internal failure drops the field's line only — never the prompt. _arbitrate: a losing HALT-carrying refresh claim is now rendered "superseded" (never "stale") with its HALT verify-line appended; composition extracted to a shared _compose_halt_line so the interpreter and arbitration paths can never render the same event's HALT state differently. Refreshed interpreter: capture-knowledge warning when consolidation_completed is explicitly false (absent/malformed keeps the degraded floor — the field is required and bool-validated at write time). ts validity now also requires the control-char class to be absent: a control-char ts renders the UNAVAILABLE branch instead of a raw echo (clean ts keeps byte-exact consumption binding; spend/identity axis untouched). session_end: _is_checkpointed_session now composes _is_paused_session (predicate drift closed). Tests +214 lines: injection vectors (newline directive, control chars, 10k values, control-char ts), superseded-claim HALT survival, consolidation warning both values, verbatim clean-ts round-trip control. Full suite: 10581 passed, 11 skipped, 0 failed, 0 errors. * fix(refresh): extend prompt sanitizer control-char class to C1 range
1 parent 86db244 commit 1a4f1c9

51 files changed

Lines changed: 2432 additions & 8146 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"name": "PACT",
1313
"source": "./pact-plugin",
1414
"description": "Orchestration harness that turns Claude Code into a coordinated team of specialist AI agents",
15-
"version": "4.5.8",
15+
"version": "4.6.0",
1616
"author": {
1717
"name": "Synaptic-Labs-AI"
1818
},

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ When installed as a plugin, PACT lives in your plugin cache:
605605
│ └── cache/
606606
│ └── pact-plugin/
607607
│ └── PACT/
608-
│ └── 4.5.8/ # Plugin version
608+
│ └── 4.6.0/ # Plugin version
609609
│ ├── agents/
610610
│ ├── commands/
611611
│ ├── skills/

pact-plugin/.claude-plugin/plugin.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "PACT",
3-
"version": "4.5.8",
3+
"version": "4.6.0",
44
"description": "Orchestration harness that turns Claude Code into a coordinated team of specialist AI agents",
55
"author": {
66
"name": "Synaptic-Labs-AI",
@@ -34,6 +34,7 @@
3434
"./commands/plan-mode.md",
3535
"./commands/wrap-up.md",
3636
"./commands/pause.md",
37+
"./commands/refresh.md",
3738
"./commands/pin-memory.md",
3839
"./commands/telegram-setup.md",
3940
"./commands/prune-memory.md"

pact-plugin/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# PACT — Orchestration Harness for Claude Code
22

3-
> **Version**: 4.5.8
3+
> **Version**: 4.6.0
44
55
Turn a single Claude Code session into a managed team of specialist AI agents that prepare, design, build, and test your code systematically.
66

pact-plugin/commands/bootstrap.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Read `team_name` from the **Current Session** block in the project's `CLAUDE.md`
1414

1515
The platform manages exactly one team per session, named `{team_name}` — it is provisioned automatically; you do not create it (the `TeamCreate`/`TeamDelete` tools no longer exist). Use `{team_name}` for every specialist dispatch.
1616

17+
**Team-config expectations (bidirectional)**: on resume, a PRESENT team config is reused — never re-create it. An ABSENT config is the NORM after a clean session end — the platform provisions the session team; both states are normal, neither is corruption. **Ghost-detection**: if a spawn or send fails with a team-not-found error, do NOT blind-retry `Agent()` — the platform spawn is non-atomic and the process may already be running. Check for inbound messages from the "failed" agent name first; re-spawn only if it stays silent.
18+
1719
## Step 2 — Spawn `pact-secretary`
1820

1921
Spawn the session secretary using single-task dispatch — the `pact-secretary` agentType is exempt from the teachback gate. No Task A teachback round-trip.
@@ -25,12 +27,25 @@ Spawn the session secretary using single-task dispatch — the `pact-secretary`
2527

2628
The secretary delivers the session briefing at spawn, answers memory queries during the session, and processes HANDOFFs at workflow boundaries. The briefing task is a discrete deliverable: the secretary MUST self-complete it (`TaskUpdate(status="completed")`) as the final act of delivering the briefing — you do NOT complete it, and you MUST NOT expect to. Completing the task does NOT end the secretary's role; it continues as memory consultant and HANDOFF harvester for the rest of the session. Memory queries from any other agent are blocked until the secretary is alive.
2729

28-
Spawn the secretary **only once per session** — reuse the same secretary for any subsequent memory queries or HANDOFF harvesting.
30+
Spawn the secretary **only once per session** — reuse the same secretary for any subsequent memory queries or HANDOFF harvesting — AND respawn it after `/PACT:refresh`, whose shutdown stopped the previous secretary process. Post-refresh, respawning is MANDATORY BEFORE any SendMessage to the secretary name: a send to the stopped name resurrects its stale pre-refresh transcript instead of starting fresh.
2931

3032
## Step 3 — Surface paused state
3133

3234
Paused state is surfaced **automatically** by the `session_init` hook: when the previous session ran `/PACT:pause`, it wrote a `session_paused` event to the session journal, and `session_init` reads that event on resume and injects the paused-work prompt into the SessionStart context. There is **no `paused-state.json` file** — do not attempt to read one (nothing writes it). Watch the SessionStart context for a "Paused work detected" line. If it appears, **do not silently resume.** Surface it to the user and ask whether to continue the paused workflow or start fresh; their choice drives next-step dispatch.
3335

36+
**Refreshed state**: also watch the SessionStart context for a refreshed-workstream prompt (a "Refreshed workstream detected" line carrying a `refresh_ts=` key). If present, this is a DECLARED CONTINUATION, not a fresh start: surface the mid-flight state to the user (feature, next phase, worktrees, any HALT line) and AUTO-PROCEED to respawn the specialists the named next phase needs. Ask the user ONLY on inconsistency: the prompt's HALT line has no matching live blocker task in `TaskList` (or live blockers exist the prompt doesn't mention), a listed worktree path does not exist on disk, or the prompt's `team_name` (when present) does not match the `team_name` in the **Current Session** block. A HALT line always surfaces to the user regardless.
37+
38+
**Consumption write (fire-once)**: immediately after confirming resumption (secretary respawned, mid-flight state surfaced), retire the refresh prompt by writing the consumption event — substitute the `refresh_ts=` value copied VERBATIM from the surfaced prompt:
39+
40+
```bash
41+
python3 "{plugin_root}/hooks/shared/session_journal.py" write \
42+
--type session_refresh_consumed --session-dir '{session_dir}' --stdin <<'JSON'
43+
{"refresh_ts": "{refresh_ts}"}
44+
JSON
45+
```
46+
47+
If the prompt said `refresh_ts=UNAVAILABLE`, skip the write (the prompt may re-surface once; its staleness downgrade bounds the repetition). Never write a consumption event when no refresh prompt surfaced. On a quit-then-new-session resume the consumption event lands in the NEW session's journal (a harmless orphan record) — fire-once on that path is enforced by the one-hop-back journal read, while the ts-bound consumption covers the same-session paths (`/compact` and same-session `--resume`).
48+
3449
## Step 4 — Plugin banner
3550

3651
Surface the plugin banner — a single line of the form `PACT plugin: <version> (root: ~/.claude/plugins/cache/pact-marketplace/PACT/<version>)` — in the bootstrap-confirmation reply. The banner is pre-rendered by the `format_plugin_banner()` helper in `hooks/shared/plugin_manifest.py` (reading the live version from `plugin.json`) and delivered through the `session_init` SessionStart system reminder + the per-prompt `peer_inject` surface; no manual composition is needed — echo what the hook already produced. If the session-start system reminder has been dropped (post-compaction), fall back in order: (a) read the `- Plugin root:` line in CLAUDE.md's Current Session block (the path embeds the version), then (b) read `plugin.json["version"]` directly. The `<version>` placeholder above is illustrative — do not substitute it manually.

pact-plugin/commands/refresh.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
---
2+
description: Mid-workstream context checkpoint — harvest, persist, shut down teammates, stand by for /compact + /PACT:bootstrap
3+
---
4+
# PACT Refresh Protocol
5+
6+
Reset the context window mid-workstream without losing state. Refresh harvests pending knowledge, persists a resumable checkpoint to the session journal, shuts down teammates, and stands by for the user to run `/compact` (canonical) and then `/PACT:bootstrap`. The workstream CONTINUES after the reset — nothing is closed, merged, or cleaned up.
7+
8+
> **Cross-reference**: For a PR-open pause with later resumption, see [pause.md](pause.md). For end-of-session cleanup and close, see [wrap-up.md](wrap-up.md).
9+
10+
---
11+
12+
## When to Use / When NOT
13+
14+
| Command | Purpose | Workstream state after |
15+
|---------|---------|------------------------|
16+
| `/PACT:pause` | PR open, resume the review later | Paused (PR open) |
17+
| `/PACT:wrap-up` | End-of-session cleanup + close | Ended |
18+
| `/PACT:refresh` | Mid-workstream context checkpoint | **Active — continues after `/compact` + `/PACT:bootstrap`** |
19+
20+
Use refresh when the context window is near exhaustion while work is still mid-flight — for example CODE complete but TEST pending, a live HALT/algedonic signal, or worktrees holding unmerged commits. No orchestration retrospective runs (the arc is mid-flight, not ending); `/PACT:pin-memory` is optional; the harvest and its drain confirmation are MANDATORY.
21+
22+
---
23+
24+
## Constraints (apply to every step)
25+
26+
- **LEAD-FRAME-ONLY**: every step that spawns, stops, or messages teammates runs in the lead frame. The team roster is flat — teammates cannot spawn or stop teammates.
27+
- **Never clean worktrees, branches, or commits.** Verification is read-only; the workstream continues after the reset.
28+
- **Never complete or delete tasks** — the feature task, phase tasks, and any HALT/algedonic signal tasks must survive for resume.
29+
- **Never write under `~/.claude/pact-refresh/`.** That directory is legacy state from a removed subsystem; checkpoint state lives ONLY in the session journal.
30+
31+
---
32+
33+
## Steps
34+
35+
> **CRITICAL ordering**: HARVEST (step 2) MUST be drain-confirmed complete BEFORE the shutdown (step 5). The secretary must be alive to process HANDOFFs.
36+
37+
### 1. Preflight — degenerate case
38+
39+
Scan `TaskList` for an in_progress feature task. If NONE exists, tell the user refresh will proceed as harvest-only and `/PACT:wrap-up` may fit better; on their confirm, CONTINUE. Once proceeding, the journal write in step 4 is unconditional — the degenerate case writes a minimal event carrying the two required fields only.
40+
41+
### 2. HARVEST — memory consolidation (Pass 2)
42+
43+
```
44+
TaskCreate(subject="secretary: session consolidation (Pass 2)",
45+
description="Run Consolidation Harvest for team {team_name}. Follow the Consolidation Harvest workflow in your pact-handoff-harvest skill. Report summary when done.")
46+
TaskUpdate(taskId, owner="secretary")
47+
```
48+
49+
**Drain-confirmation gate (MUST)**: before ANY shutdown step, verify the consolidation task's status is `completed` by READING the task (`TaskGet`, or the raw task JSON on disk) — an explicit completion check, not ordering prose. Not completed ⇒ wait or nudge the secretary; do not proceed to step 5.
50+
51+
### 3. PERSIST/VERIFY — read-only state check + payload computation
52+
53+
For each active worktree, run `git status --porcelain` and report any uncommitted changes to the user — never clean, never commit on their behalf.
54+
55+
Then compute the checkpoint payload:
56+
57+
- `halt_active` + `halt_task_ids`: scan `TaskList` for tasks with `metadata.type` in {`blocker`, `algedonic`} and status not `completed`. Always computable — `false` and omit the ids when none are live.
58+
- `feature_task_id` / `feature_subject`: from the in_progress feature task (omit both in the degenerate case).
59+
- `team_name`: the active session team's name — the same value bootstrap reads from the Current Session block at resume, giving it an identity cross-check.
60+
- `next_phase`: from the phase-task state. Bounded vocabulary, writer-enforced (the journal validator does not check enums): `prepare|architect|code|test|peer-review|deploy`.
61+
- `worktrees`: JSON list of ABSOLUTE paths of the active worktrees.
62+
- `pr_number`: only when a PR is open — informational surface data; it never gates anything.
63+
64+
### 4. Write the checkpoint to the session journal
65+
66+
Emit `session_consolidated` only when step 2's consolidation actually completed, then write the `session_refreshed` checkpoint. The flag is **shell-clamped** via a three-branch `case`: `true` emits, `false` is a no-op, anything else fails fast as a template-substitution bug. Pass the literal string `true` or `false` for `{true_or_false}`.
67+
68+
```bash
69+
set -e
70+
trap 'rc=$?; echo "[JOURNAL WRITE FAILED] refresh.md (bash line $LINENO): \"${BASH_COMMAND%%$'\''\n'\''*}\" exit=$rc" >&2; exit $rc' ERR
71+
case '{true_or_false}' in
72+
true)
73+
python3 "{plugin_root}/hooks/shared/session_journal.py" write \
74+
--type session_consolidated --session-dir '{session_dir}' --stdin <<'JSON'
75+
{"pass": 2, "task_count": {task_count}, "memories_saved": {memories_saved}}
76+
JSON
77+
;;
78+
false)
79+
;; # intentional no-op
80+
*)
81+
echo "[refresh.md] invalid {true_or_false} flag: '{true_or_false}' (expected literal 'true' or 'false')" >&2
82+
exit 1
83+
;;
84+
esac
85+
python3 "{plugin_root}/hooks/shared/session_journal.py" write \
86+
--type session_refreshed --session-dir '{session_dir}' --stdin <<'JSON'
87+
{"consolidation_completed": {true_or_false}, "halt_active": {halt_active}, "halt_task_ids": {halt_task_ids}, "feature_task_id": "{feature_task_id}", "feature_subject": "{feature_subject}", "team_name": "{team_name}", "next_phase": "{next_phase}", "worktrees": {worktrees}, "pr_number": {pr_number}}
88+
JSON
89+
```
90+
91+
**Degenerate case** (no active workstream): omit every optional field — write the two required fields only:
92+
93+
```bash
94+
python3 "{plugin_root}/hooks/shared/session_journal.py" write \
95+
--type session_refreshed --session-dir '{session_dir}' --stdin <<'JSON'
96+
{"consolidation_completed": {true_or_false}, "halt_active": false}
97+
JSON
98+
```
99+
100+
> ⚠️ **Heredoc-stdin contract**: the writes use `--stdin <<'JSON' ... JSON` (quoted delimiter), so an apostrophe in a substituted value cannot close the shell quote and silently drop the event. The orchestrator must still produce JSON-valid string content (escape `\"`, `\\`, and control chars).
101+
102+
**Event fields**:
103+
104+
| Field | Type | Required | Description |
105+
|-------|------|----------|-------------|
106+
| `consolidation_completed` | boolean | yes | Whether the step 2 harvest completed |
107+
| `halt_active` | boolean | yes | Whether any HALT/algedonic signal task is live at refresh time |
108+
| `halt_task_ids` | list of strings | no | Ids of the live signal tasks (diagnostic — the live task store is the SSOT at resume) |
109+
| `feature_task_id` | string | no | The in_progress feature task id |
110+
| `feature_subject` | string | no | The in_progress feature task subject |
111+
| `team_name` | string | no | The active session team's name — resume-time identity cross-check |
112+
| `next_phase` | string | no | Bounded vocabulary above |
113+
| `worktrees` | list of strings | no | Absolute paths of active worktrees |
114+
| `pr_number` | integer | no | Open PR number — surface-only, never gates surfacing |
115+
116+
The timestamp (`ts`) is set automatically by the journal writer and becomes the checkpoint's claim id — bootstrap's consumption write binds to it.
117+
118+
### 5. SHUTDOWN teammates
119+
120+
Shut down each active teammate **by name**, staggered 1-2 sends per turn (rate-limit discipline): graceful `shutdown_request` first, then `TaskStop(name)` as the guarantee tier — a teammate may reject the request mid-task; `TaskStop` is authoritative.
121+
122+
```
123+
For each active teammate:
124+
SendMessage(to="{teammate_name}", message={"type": "shutdown_request", "reason": "Context refresh — checkpoint written, resuming after /compact"})
125+
then: TaskStop("{teammate_name}")
126+
```
127+
128+
EXPECTED post-state: each stop removes that member's roster entry — the config FILE and the team IDENTITY survive; a lead-only roster is the correct post-refresh state, not corruption. Do NOT add synchronous send-confirmation (message delivery lands asynchronously) — verify by a disk re-read of the team config, or simply proceed. Do NOT delete the team.
129+
130+
### 6. STANDBY
131+
132+
Report to the user:
133+
134+
```
135+
"Context checkpoint complete. Run /compact now, then /PACT:bootstrap to resume mid-flight."
136+
```
137+
138+
Until bootstrap runs:
139+
140+
- **FORBID new dispatches** — no TaskCreate-and-assign, no specialist spawns.
141+
- **RESPAWN-BEFORE-SEND**: messaging ANY pre-refresh teammate name resurrects its stale transcript and re-adds the member — bootstrap must respawn a fresh process under that name first. This includes the secretary.
142+
- **Expect and IGNORE stragglers**: messages a teammate generated BEFORE its stop can deliver minutes later during standby. A straggler is NOT evidence the teammate survived; do not reply and do not re-engage — any reply triggers the resume-on-send hazard above.
143+
- `/clear` is tolerated but non-canonical (no compact summary is written; the bootstrap gate re-engages either way); `/compact` is the canonical path.
144+
145+
If the Telegram bridge is active, send a notification:
146+
147+
```
148+
telegram_notify("Context checkpoint complete. Run /compact, then /PACT:bootstrap to resume mid-flight.")
149+
```

pact-plugin/hooks/refresh/__init__.py

Lines changed: 0 additions & 89 deletions
This file was deleted.

0 commit comments

Comments
 (0)