Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
585bd20
fix(dispatch): correct 4c286c1f rename + harden bootstrap_gate (#662)
michael-wojcik May 6, 2026
cff3697
feat(dispatch_gate): PreToolUse Agent gate (#662)
michael-wojcik May 6, 2026
bfd8009
feat(task_lifecycle_gate): PostToolUse TaskCreate|TaskUpdate gate (#662)
michael-wojcik May 6, 2026
13e4662
docs+chore: F22 runbook + shadow-mode env-var + version 4.2.0 (#662)
michael-wojcik May 6, 2026
c6f95d6
test: comprehensive coverage for #662 dispatch + lifecycle gates
michael-wojcik May 6, 2026
6358ceb
fix(dispatch_gate): correct has_task_assigned task path (#662)
michael-wojcik May 6, 2026
255f253
fix(bootstrap_gate): correct marker-provenance docstring overstatement
michael-wojcik May 6, 2026
aadb0b9
chore: bump plugin version to 4.1.3 (patch)
michael-wojcik May 6, 2026
1b4f5f1
chore: rename inline-mission env-var to behavioral name
michael-wojcik May 6, 2026
64dad75
refactor: behavioral identifiers for gate decisions and journal events
michael-wojcik May 6, 2026
e8f09de
refactor: rewrite source comments, docstrings, test names, and runbook
michael-wojcik May 7, 2026
2df4b98
refactor: behavioral names for path-alignment regression tests
michael-wojcik May 7, 2026
e2d24a7
refactor: drop internal alias and clean up provenance phrases in tests
michael-wojcik May 7, 2026
6a5ec16
refactor: rename marker schema constant to behavioral identifier
michael-wojcik May 7, 2026
5a5aac9
fix(dispatch_gate): reserve self-completion-exempt agent names
michael-wojcik May 7, 2026
8b2ec7a
refactor: tighten name validation, normalize team_name, broaden
michael-wojcik May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"name": "PACT",
"source": "./pact-plugin",
"description": "Orchestration harness that turns Claude Code into a coordinated team of specialist AI agents",
"version": "4.1.2",
"version": "4.1.3",
"author": {
"name": "Synaptic-Labs-AI"
},
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ When installed as a plugin, PACT lives in your plugin cache:
│ └── cache/
│ └── pact-plugin/
│ └── PACT/
│ └── 4.1.2/ # Plugin version
│ └── 4.1.3/ # Plugin version
│ ├── agents/
│ ├── commands/
│ ├── skills/
Expand Down
2 changes: 1 addition & 1 deletion pact-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "PACT",
"version": "4.1.2",
"version": "4.1.3",
"description": "Orchestration harness that turns Claude Code into a coordinated team of specialist AI agents",
"author": {
"name": "Synaptic-Labs-AI",
Expand Down
10 changes: 9 additions & 1 deletion pact-plugin/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PACT — Orchestration Harness for Claude Code

> **Version**: 4.1.2
> **Version**: 4.1.3

Turn a single Claude Code session into a managed team of specialist AI agents that prepare, design, build, and test your code systematically.

Expand Down Expand Up @@ -68,6 +68,14 @@ Then restart Claude Code. Requires [Agent Teams enabled](https://github.com/Syna
- **Restored session-start ritual** (v4.1): Scaled-down `/PACT:bootstrap` command + `bootstrap_gate.py` injection re-establish the first-turn ritual under the new delivery model
- **Communication Charter**: Async-at-idle-boundary delivery model formalized for inter-agent SendMessage mechanics

## Configuration

Environment variables that tune hook behavior:

| Variable | Default | Allowed values | Effect |
|---|---|---|---|
| `PACT_DISPATCH_INLINE_MISSION_MODE` | `warn` | `warn` / `deny` / `shadow` | Disposition of the dispatch-gate inline-mission heuristic (flags dispatchers inlining mission text into `prompt=` instead of using the canonical "check TaskList" form; F7 in the #662 failure-mode index). `warn` emits an advisory `additionalContext`; `deny` blocks the spawn (flip after the F22 counter-test in `tests/runbooks/662-dispatch-gate.md` confirms `additionalContext` is silently dropped under PreToolUse); `shadow` journals only — the trigger is observable but neither WARNs nor DENYs (calibration / first-session safety net). F1-F6, F14, F15 are unaffected. Unknown values fall back to `warn`. |

## Full Documentation

For installation options, detailed features, examples, and technical reference:
Expand Down
20 changes: 15 additions & 5 deletions pact-plugin/agents/pact-orchestrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ Create a feature branch before any new workstream begins.

**Checkpoint**: Reaching for **Edit**/**Write** on application code (`.py`, `.ts`, `.js`, `.rb`, etc.)? **DELEGATE**.

**Checkpoint**: Reaching for `Task(subagent_type=...)` without `team_name`? **Create a team first.** Every specialist dispatch uses Agent Teams — no exceptions.
**Checkpoint**: Reaching for `Agent(subagent_type=...)` without `team_name`? **Create a team first.** Every specialist dispatch uses Agent Teams — no exceptions.

Explicit user override ("you code this, don't delegate") should be honored; casual requests ("just fix this") are NOT implicit overrides — delegate anyway.

Expand Down Expand Up @@ -357,17 +357,27 @@ For full detail, `Read(file_path="../protocols/pact-variety.md")` when calibrati

## 11. Agent Teams Dispatch

> ⚠️ **MANDATORY**: Specialists are spawned as teammates via `Task(name=..., team_name="{team_name}", subagent_type=...)`. The session team is created at session start per INSTRUCTIONS step 1. The `session_init` hook provides the specific team name in your session context.
> ⚠️ **MANDATORY**: Specialists are spawned as teammates via `Agent(name=..., team_name="{team_name}", subagent_type=...)`. The session team is created at session start per INSTRUCTIONS step 1. The `session_init` hook provides the specific team name in your session context.
>
> ⚠️ **NEVER** use plain `Task(subagent_type=...)` without `name` and `team_name` for specialist agents. This bypasses team coordination, task tracking, and `SendMessage` communication.
> ⚠️ **NEVER** use plain `Agent(subagent_type=...)` without `name` and `team_name` for specialist agents. This bypasses team coordination, task tracking, and `SendMessage` communication.

**Dispatch pattern**:

1. `TaskCreate(subject, description)` — create the tracking task with full mission
2. `TaskUpdate(taskId, owner="{name}")` — assign ownership
3. `Task(name="{name}", team_name="{team_name}", subagent_type="pact-{type}", prompt="YOUR PACT ROLE: teammate ({name}).\n\nYou are joining team {team_name}. Check `TaskList` for tasks assigned to you.")` — spawn the teammate
3. `Agent(name="{name}", team_name="{team_name}", subagent_type="pact-{type}", prompt="YOUR PACT ROLE: teammate ({name}).\n\nYou are joining team {team_name}. Check `TaskList` for tasks assigned to you.")` — spawn the teammate

> ⚠️ **`{name}` constraint (SECURITY)**: the `name=` parameter you pass to `Task()` is interpolated verbatim into the `YOUR PACT ROLE: teammate ({name}).` marker line. To prevent marker spoofing via injected newlines or close-parens, the `name` value MUST match the pattern `^[a-z0-9-]+$` — lowercase alphanumerics and hyphens only, no spaces, no newlines, no parentheses. Examples of valid names: `backend-coder-1`, `review-test-engineer-7`, `secretary`. Examples of invalid names: `backend coder 1` (spaces), `backend-coder)evil` (close-paren), any name containing newlines.
> ⚠️ **`{name}` constraint (SECURITY)**: the `name=` parameter you pass to `Agent()` is interpolated verbatim into the `YOUR PACT ROLE: teammate ({name}).` marker line. To prevent marker spoofing via injected newlines or close-parens, the `name` value MUST match the pattern `^[a-z0-9-]+$` — lowercase alphanumerics and hyphens only, no spaces, no newlines, no parentheses. Examples of valid names: `backend-coder-1`, `review-test-engineer-7`, `secretary`. Examples of invalid names: `backend coder 1` (spaces), `backend-coder)evil` (close-paren), any name containing newlines.

#### First-spawn verification (HARD-RULE)

After your first specialist spawn in a session — and after any subsequent spawn where you suspect dispatch tooling may be misconfigured — verify the teammate received the full PACT protocol surface. The teammate's first message MUST demonstrate access to `TaskList`, `TaskUpdate`, and `SendMessage`. If the teammate reports any of those tools "not available", "not loaded", or otherwise missing:

> ⚠️ **HARD STOP — DISPATCH PROTOCOL VIOLATION**. This is **NOT** degraded mode. **NOT** something to "work around". The dispatch was malformed (almost always: spawn shape used `Task(...)` instead of `Agent(...)`, or omitted `name=` / `team_name=`). Stop the teammate, correct the dispatch shape, and re-spawn with the canonical `Agent(name=..., team_name=..., subagent_type=...)` form documented above. Do **not** instruct the teammate to "make do" — they cannot self-recover from a malformed spawn.

#### Hook WARN signals are STOP signals

When a PreToolUse hook (`bootstrap_gate`, `dispatch_gate`, `team_guard`, etc.) emits a WARN-shaped advisory or a `permissionDecision: deny` rationale, treat it as a HARD STOP. **WARN means STOP and re-dispatch correctly** — not "note the warning and proceed". Rationalizing past a WARN ("the gate is overly cautious", "this case doesn't apply") is the failure mode the WARN exists to prevent. If a gate fires unexpectedly on a dispatch you believe is correct, the dispatch is likely subtly wrong; investigate before retrying.

### Reuse vs. Spawn Decision

Expand Down
27 changes: 24 additions & 3 deletions pact-plugin/commands/bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,36 @@ Command files use `{team_name}`, `{session_dir}`, and `{plugin_root}` as literal

This step unlocks code-editing tools (`Edit`, `Write`) and agent spawning (`Agent`, `NotebookEdit`), which are blocked by the `bootstrap_gate` PreToolUse hook until the bootstrap-complete marker exists.

Find the `PACT_SESSION_DIR=<path>` line in your context (injected by `bootstrap_prompt_gate` at every prompt while the marker is absent). Run:
Find the `PACT_SESSION_DIR=<path>` line in your context (injected by `bootstrap_prompt_gate` at every prompt while the marker is absent). Run the marker-write command below, substituting `<path>` with the `PACT_SESSION_DIR=` value and `<plugin_root>` with the `{plugin_root}` value (from the Current Session block):

```
mkdir -p "<path>" && touch "<path>/bootstrap-complete"
mkdir -p "<path>" && python3 - "<path>" "<plugin_root>" <<'PY'
import hashlib, json, os, sys
session_dir = sys.argv[1].rstrip("/")
plugin_root = sys.argv[2].rstrip("/")
sid = os.path.basename(session_dir)
plugin_version = json.loads(
open(os.path.join(plugin_root, ".claude-plugin", "plugin.json"),
encoding="utf-8").read()
)["version"]
v = 1
sig = hashlib.sha256(
f"{sid}|{plugin_root}|{plugin_version}|{v}".encode("utf-8")
).hexdigest()
marker = os.path.join(session_dir, "bootstrap-complete")
with open(marker, "w", encoding="utf-8") as f:
f.write(json.dumps({"v": v, "sid": sid, "sig": sig}))
PY
```

Substitute `<path>` with the value from `PACT_SESSION_DIR=`. The marker name `bootstrap-complete` is the load-bearing literal that `bootstrap_gate.is_marker_set` checks; do not rename it.
The marker is a JSON sentinel `{"v": 1, "sid": <session_id>, "sig": SHA256("<sid>|<plugin_root>|<plugin_version>|<v>")}` (#662). The marker name `bootstrap-complete` is the load-bearing literal that `bootstrap_gate.is_marker_set` checks; do not rename it. The signature is a marker-content fingerprint that closes the trivial `Bash("touch <path>/bootstrap-complete")` bypass. It is NOT cryptographic provenance: all four signature inputs are readable from the same-user filesystem, so a same-user attacker with Python execution can recompute the digest. The fingerprint raises attacker effort and creates a detection surface; it does not make the marker unforgeable.

<!-- Coupling: marker name "bootstrap-complete" must match shared.BOOTSTRAP_MARKER_NAME
in pact-plugin/hooks/shared/__init__.py.
Marker schema (v=1, keys {v,sid,sig}, signature input order
"sid|plugin_root|plugin_version|v") must match the verifier in
pact-plugin/hooks/bootstrap_gate.py::is_marker_set + the
MARKER_SCHEMA_VERSION constant. Bumping the schema version requires
updating BOTH this producer AND the verifier in lockstep.
Pattern: convention-must-be-enforced-not-just-documented (test_three_surface_split_enforcement.py
pins the persona §2 / bootstrap.md split; this comment pins the marker-name SSOT). -->
12 changes: 6 additions & 6 deletions pact-plugin/commands/comPACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ Every specialist dispatch creates **two tasks**, not one:
- **Task A** — TEACHBACK gate. `subject = "{specialist}: TEACHBACK for {sub-task}"`, owner = teammate. Description: teachback expectations + dispatch context.
- **Task B** — primary work. `subject = "{specialist}: {sub-task}"`, owner = teammate, `blockedBy = [<Task A id>]`.

Both are created BEFORE the `Task(...)` spawn call so the teammate sees them on first `TaskList`. The teammate claims A, submits teachback metadata, idles on `awaiting_lead_completion`. You review and accept via the two-call atomic pair (`TaskUpdate(A, status="completed")` + paired wake-signal SendMessage — see [Teachback Review](../protocols/pact-completion-authority.md#teachback-review)). On accept, the teammate wakes to claim B.
Both are created BEFORE the `Agent(...)` spawn call so the teammate sees them on first `TaskList`. The teammate claims A, submits teachback metadata, idles on `awaiting_lead_completion`. You review and accept via the two-call atomic pair (`TaskUpdate(A, status="completed")` + paired wake-signal SendMessage — see [Teachback Review](../protocols/pact-completion-authority.md#teachback-review)). On accept, the teammate wakes to claim B.

**Dispatch sequence (replaces single-task dispatch)**:

Expand All @@ -190,10 +190,10 @@ B_id = TaskCreate(subject="{specialist}: {sub-task}", description="<full mission
TaskUpdate(B_id, owner="{specialist-name}", addBlockedBy=[A_id])
TaskUpdate(A_id, addBlocks=[B_id])

# 3. Spawn the teammate via the canonical Task() form (shown in §Invocation below).
# 3. Spawn the teammate via the canonical Agent() form (shown in §Invocation below).
```

The `Task()` `prompt` does NOT change shape — the two-task dispatch is encoded in the surrounding TaskCreate sequence, not in the `Task()` call.
The `Agent()` `prompt` does NOT change shape — the two-task dispatch is encoded in the surrounding TaskCreate sequence, not in the `Agent()` call.

**Carve-outs** — single-task dispatch still applies for:

Expand Down Expand Up @@ -225,15 +225,15 @@ JSON
4. Spawn the specialist with the canonical dispatch form. The `prompt` MUST lead with the `YOUR PACT ROLE: teammate ({specialist-name})` marker on its own line (team protocol + teachback content arrive via spawn-time skills frontmatter):

```
Task(
Agent(
name="{specialist-name}",
team_name="{team_name}",
subagent_type="pact-{specialist-type}",
prompt="YOUR PACT ROLE: teammate ({specialist-name}).\n\nYou are joining team {team_name}. Check `TaskList` for tasks assigned to you."
)
```

Spawn all specialists in parallel (multiple `Task` calls in one response).
Spawn all specialists in parallel (multiple `Agent` calls in one response).

**Progress monitoring**: For parallel dispatch or novel domains, include "Send progress signals per the agent-teams skill Progress Signals section" in each specialist's dispatch prompt.

Expand Down Expand Up @@ -263,7 +263,7 @@ JSON
4. Spawn the specialist with the canonical dispatch form. The `prompt` MUST lead with the `YOUR PACT ROLE: teammate ({specialist-name})` marker on its own line (team protocol + teachback content arrive via spawn-time skills frontmatter):

```
Task(
Agent(
name="{specialist-name}",
team_name="{team_name}",
subagent_type="pact-{specialist-type}",
Expand Down
24 changes: 12 additions & 12 deletions pact-plugin/commands/orchestrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ h. Monitor via `SendMessage` (completion summaries) and `TaskList` until agents
i. `TaskUpdate`: phase status = "completed" (agents self-manage their task status)
```

The canonical `Task()` dispatch form, referenced by every phase below:
The canonical `Agent()` dispatch form, referenced by every phase below:

```
Task(
Agent(
name="{teammate-name}",
team_name="{team_name}",
subagent_type="pact-{teammate-type}",
Expand All @@ -66,7 +66,7 @@ Every specialist dispatch creates **two tasks**, not one:
- **Task A** — TEACHBACK gate. `subject = "{role}: TEACHBACK for {feature}"`, owner = teammate. Description: teachback expectations + dispatch context.
- **Task B** — primary work. `subject = "{role}: {mission}"`, owner = teammate, `blockedBy = [<Task A id>]`.

Both are created BEFORE the `Task(...)` spawn call so the teammate sees them on first `TaskList`. The teammate claims A, submits teachback metadata, idles on `awaiting_lead_completion`. You review the teachback, accept via the two-call atomic pair (`TaskUpdate(A, status="completed")` + paired wake-signal SendMessage — see [Teachback Review](../protocols/pact-completion-authority.md#teachback-review)), and the teammate wakes to claim B.
Both are created BEFORE the `Agent(...)` spawn call so the teammate sees them on first `TaskList`. The teammate claims A, submits teachback metadata, idles on `awaiting_lead_completion`. You review the teachback, accept via the two-call atomic pair (`TaskUpdate(A, status="completed")` + paired wake-signal SendMessage — see [Teachback Review](../protocols/pact-completion-authority.md#teachback-review)), and the teammate wakes to claim B.

**Dispatch sequence (replaces single-task dispatch)**:

Expand Down Expand Up @@ -98,10 +98,10 @@ B_id = TaskCreate(
TaskUpdate(B_id, owner="{teammate-name}", addBlockedBy=[A_id])
TaskUpdate(A_id, addBlocks=[B_id])

# 3. Spawn the teammate via the canonical Task() form above.
# 3. Spawn the teammate via the canonical Agent() form above.
```

The `Task()` `prompt` does NOT change shape — the two-task dispatch is encoded in the surrounding TaskCreate sequence, not in the `Task()` call. The teammate discovers Task A + Task B via `TaskList` and follows pact-agent-teams §On Start.
The `Agent()` `prompt` does NOT change shape — the two-task dispatch is encoded in the surrounding TaskCreate sequence, not in the `Agent()` call. The teammate discovers Task A + Task B via `TaskList` and follows pact-agent-teams §On Start.

**Carve-outs** — single-task dispatch still applies for:

Expand Down Expand Up @@ -463,7 +463,7 @@ JSON
4. Spawn the preparer with the canonical dispatch form:

```
Task(
Agent(
name="preparer",
team_name="{team_name}",
subagent_type="pact-preparer",
Expand Down Expand Up @@ -555,7 +555,7 @@ JSON
4. Spawn the architect with the canonical dispatch form:

```
Task(
Agent(
name="architect",
team_name="{team_name}",
subagent_type="pact-architect",
Expand Down Expand Up @@ -678,7 +678,7 @@ JSON
4. Spawn each coder with the canonical dispatch form:

```
Task(
Agent(
name="{coder-name}",
team_name="{team_name}",
subagent_type="pact-{coder-type}",
Expand Down Expand Up @@ -706,7 +706,7 @@ Valid skip reasons: single coder on familiar pattern, variety reassessed below 7
3. Spawn the auditor with the canonical dispatch form:

```
Task(
Agent(
name="auditor",
team_name="{team_name}",
subagent_type="pact-auditor",
Expand Down Expand Up @@ -805,7 +805,7 @@ JSON
4. Spawn the test engineer with the canonical dispatch form:

```
Task(
Agent(
name="test-engineer",
team_name="{team_name}",
subagent_type="pact-test-engineer",
Expand Down Expand Up @@ -868,9 +868,9 @@ When a blocker is resolved, prefer resuming the original agent over spawning fre

**Resume pattern**:
1. Read agent ID from task metadata: `TaskGet(taskId).metadata.agent_id`
2. Resume with blocker context: `Task(resume="{agent_id}", prompt="Blocker resolved: {details}. Continue your task.")`
2. Resume with blocker context: `Agent(resume="{agent_id}", prompt="Blocker resolved: {details}. Continue your task.")`

**Fresh spawn pattern** (when resume is inappropriate): Follow the standard dispatch pattern (`TaskCreate` + `TaskUpdate` + Task with name/team_name/subagent_type).
**Fresh spawn pattern** (when resume is inappropriate): Follow the standard dispatch pattern (`TaskCreate` + `TaskUpdate` + Agent with name/team_name/subagent_type).

---

Expand Down
Loading