Skip to content

Commit 32cbdb7

Browse files
fix(skills): journal-source harvest read-path to survive the task-store drain
Re-diagnose-first verification spike: harvest was already drain-survivable (the session journal is session_id-scoped under pact-sessions/, the drained task store is team_name-scoped under tasks/). Re-point Step 10's variety read to the journal (filtered by feature task_id + latest-ts, KeyError-safe via resolve_variety_total), and retire the born-dead revision_number>1 harvest read-branch that routed revision-harvest to the GC-vulnerable metadata.handoff instead of the drain-proof journal. Correct the agent-teams revision-visibility instruction to the accurate lead-only-completion model; keep the revision_number field (rejection-record / META-BLOCK trail). Adds a non-vacuous drain-survival regression suite. PATCH 4.4.38. Completes ACs of #995 sub-1 (journal-sourced harvest survives the drain). Parent #995 stays open (sub-2 is an accepted lead-process-only teammate-frame boundary). Follow-up observation tracked in #1017.
1 parent 1e991f7 commit 32cbdb7

8 files changed

Lines changed: 502 additions & 20 deletions

File tree

.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.4.37",
15+
"version": "4.4.38",
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.4.37/ # Plugin version
608+
│ └── 4.4.38/ # Plugin version
609609
│ ├── agents/
610610
│ ├── commands/
611611
│ ├── skills/

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "PACT",
3-
"version": "4.4.37",
3+
"version": "4.4.38",
44
"description": "Orchestration harness that turns Claude Code into a coordinated team of specialist AI agents",
55
"author": {
66
"name": "Synaptic-Labs-AI",

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.4.37
3+
> **Version**: 4.4.38
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/skills/pact-agent-teams/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,12 @@ If the team-lead rejects your teachback or HANDOFF, you wake on the inbound Send
194194
3. **Revise**. For teachback rejection: rewrite `metadata.teachback_submit` per the corrections. For HANDOFF rejection: revise the deliverable (re-edit files, re-run tests, etc.) and rewrite `metadata.handoff`.
195195

196196
4. **Re-submit on the SAME task** (do NOT create a new task):
197-
- Increment `metadata.revision_number`. The team-lead writes `revision_number=1` in the rejection record. On your first revision, increment to `2`. On each subsequent revision, increment again. The harvest path reads `metadata.handoff` directly when `revision_number > 1` to surface revised content; setting `revision_number=1` would route harvest to the rejected journal event and silently lose the revised content.
197+
- Increment `metadata.revision_number`. The team-lead writes `revision_number=1` in the rejection record. On your first revision, increment to `2`. On each subsequent revision, increment again. This count is the rejection-cycle audit trail — it feeds the imPACT META-BLOCK 3-cycle signal, not harvest routing. It does NOT gate whether your revised content is preserved: the team-lead's acceptance (the single completion) emits whatever `metadata.handoff` holds at that moment, so the revised content reaches the journal regardless of the count.
198198
- SendMessage the team-lead: `"[{sender}→team-lead] Revised teachback/HANDOFF on Task #{id}. See metadata.{teachback_submit|handoff} (revision {N})."`
199199
- Re-SET `intentional_wait{reason=awaiting_lead_completion, since=<fresh canonical_since() output>}`.
200200
- Idle.
201201

202-
> **Revision visibility**: on revision (`revision_number > 1`), the journal `agent_handoff` event from your *first* completion is preserved (one event per task lifetime). The secretary's harvest path reads `metadata.handoff` directly when `revision_number > 1`, so your revised content reaches institutional memory. The metadata write is sufficient.
202+
> **Revision visibility**: your revised content reaches institutional memory because of *when* the journal event is emitted, not because of `revision_number`. A rejection keeps your task `in_progress` and emits nothing; the only `agent_handoff` journal event fires at the team-lead's single completion — their acceptance — and captures whatever `metadata.handoff` holds at that moment, i.e. your revised content. So the journal carries the accepted, revised HANDOFF, and harvest reads it from there (drain-proof). Your job on revision is simply to rewrite `metadata.handoff` before the lead accepts.
203203
204204
### HANDOFF Format
205205

pact-plugin/skills/pact-handoff-harvest/SKILL.md

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,26 +41,22 @@ Read your processed task list from your team's section in agent memory (`~/.clau
4141

4242
### Step 3: Read All HANDOFFs
4343

44-
For each discovered task, read the HANDOFF using this revision-aware fallback:
44+
For each discovered task, read the HANDOFF using this journal-first fallback:
4545

46-
1. **Revision-aware metadata read**: Read the task's `metadata` (raw JSON; `TaskGet` is metadata-blind for `handoff` content). If `metadata.revision_number` is set and `> 1`, prefer `metadata.handoff` over the journal event. The journal `agent_handoff` event captured the FIRST (rejected) submission only — `agent_handoff_emitter.py` writes one journal event per task lifetime via an O_EXCL marker. On revision, the team-lead-completion of the revised task does NOT emit a second journal event, so the revised content lives in `metadata.handoff` only.
47-
2. **Session journal** (preferred for revision_number == 1 or unset, GC-proof): If the task was discovered via `agent_handoff` journal events and `revision_number` is unset or 1, the journal event's `handoff` field contains the full HANDOFF content inline — use it directly. This is the most reliable source for first-pass acceptance flows.
48-
3. **`TaskGet` fallback**: If both above fail (no journal event AND no `metadata.handoff`), fall back to `TaskGet(taskId).metadata.handoff`. May fail for garbage-collected tasks.
49-
4. **Report gap**: If all sources fail, report the gap to lead — note the task_id, agent name, and timestamp so the team-lead has context.
46+
1. **Session journal** (preferred, GC-proof): If the task was discovered via `agent_handoff` journal events, the journal event's `handoff` field contains the full HANDOFF content inline — use it directly. The journal carries the **accepted** HANDOFF for every completed task: the team-lead's single completion (acceptance) emits whatever `metadata.handoff` holds at that moment, so on a reject→revise→accept flow the journal event holds the revised content the lead accepted. This is the most reliable source for all completed tasks, first-pass and revised alike.
47+
2. **`TaskGet` fallback**: If there is no journal event for the task, fall back to `TaskGet(taskId).metadata.handoff` (read the raw `metadata` JSON; `TaskGet` is metadata-blind for `handoff` content). May fail for garbage-collected tasks.
48+
3. **Report gap**: If all sources fail, report the gap to lead — note the task_id, agent name, and timestamp so the team-lead has context.
5049

51-
Pseudocode for the revision-aware branch:
50+
Pseudocode for the journal-first read:
5251

5352
```python
5453
for task_id in unprocessed:
5554
journal_event = next((e for e in journal_events if e.task_id == task_id), None)
56-
task_meta = read_task_metadata(task_id) or {} # raw JSON read; TaskGet is metadata-blind
57-
revision_n = task_meta.get("revision_number", 1)
58-
if revision_n > 1:
59-
# Revised HANDOFF; journal event captured only the first (rejected) submission.
60-
handoff = task_meta.get("handoff")
61-
elif journal_event:
62-
handoff = journal_event.handoff
55+
if journal_event:
56+
handoff = journal_event.handoff # accepted content, GC-proof
6357
else:
58+
# No journal event — last-resort metadata read (may be GC'd).
59+
task_meta = read_task_metadata(task_id) or {} # raw JSON; TaskGet is metadata-blind
6460
handoff = task_meta.get("handoff")
6561
# ...process handoff...
6662
```
@@ -160,7 +156,7 @@ Gaps: {any HANDOFFs that were thin or missing}",
160156
### Step 10: Gather Calibration Data
161157

162158
After processing HANDOFFs, gather calibration metrics for the orchestrator's variety scoring feedback loop:
163-
- Read the feature task metadata for `initial_variety_score` (stored during variety assessment). If `TaskGet` fails (garbage-collected), ask the team-lead for the variety score instead.
159+
- Read `initial_variety_score` from the journal's `variety_assessed` event (GC-proof, survives the task-store drain): `python3 -c "import sys; sys.path.insert(0, '{hooks_dir}'); from shared.session_journal import read_events; import json; [print(json.dumps(e)) for e in read_events('variety_assessed')]"`. **Select the event for THIS feature** — `variety_assessed` events carry a `task_id`, and a resumed/multi-feature session holds one per feature (plus, because the platform reuses task_ids across arcs, the current feature's id can match a PRIOR arc too). So do NOT take the first event: filter to events whose `task_id` matches the feature task being harvested and take the **latest-`ts`** match — the `resolve_arc_start(events, feature_task_id)` semantics the wrap-up retrospective uses (`shared/variety_divergence.resolve_arc_start` is the canonical implementation). Then resolve the scalar total from that event's `variety` dict via the pure `resolve_variety_total(variety)` helper (`shared/teachback_schema.py`) rather than indexing `variety['total']` directly — it prefers the canonical `total` key, falls through a documented fallback chain, and returns `None` instead of raising `KeyError` if the dict is malformed or `total` is missing. If no `variety_assessed` event matches this feature (e.g., a feature dispatched without a variety emit), or `resolve_variety_total` returns `None`, ask the team-lead for the variety score instead.
164160
- Scan `TaskList` for blocker count (tasks with "BLOCKER:" in subject). Note: `TaskList` may be incomplete in long sessions due to garbage collection — report what's available.
165161
- Scan `TaskList` for phase rerun count (retry/redo phase tasks)
166162
- Note domain from feature task description

0 commit comments

Comments
 (0)