← the whole session plugin/skills/pipeline-scope-guardian/SKILL.md
Scope compliance enforcer for the pipeline orchestrator. Registers the full promised scope at session start, tracks coverage across dispatch cycles, and blocks 'done' declarations when any list or requirement has been silently dropped. Designed to run every 10 minutes during orchestrated pipeline sessions. Prevents the v1 failure mode: 4 lists received, only 1 processed, 3 silently dropped.
Pipeline Scope Guardian
Why This Skill Exists
This skill exists because of a real, named failure (example from real history — the actual list names were that product's own internal categories; these are relabelled generically): a person handed an orchestrator 4 explicit lists in one message —
- 31 pages needing an update
- 18 draft documents needing review
- 1 diagnostic report to run
- 16 pending fix items
The orchestrator processed list 1 (the pages) and declared progress. Lists 2, 3, and 4 were silently dropped — no flag, no warning, no request for permission. The person did not authorize the reduction. This skill makes that failure mode architecturally impossible.
Core guarantee: The orchestrator cannot say "done" when items from the registered scope have not been started or completed, unless the person explicitly approves the reduction in writing.
Phase 1 — Session Start: Register Full Scope
When to invoke: Immediately after the person provides the full list of work. Before any dispatch begins.
Step 1: Parse Every List
Read the person's message and extract EVERY discrete item from EVERY list. Do not summarize, combine, or drop any item.
Structure the scope registry as:
{
"scope_id": "{session_id}-scope-{timestamp}",
"registered_at": "{ISO timestamp}",
"lists": [
{
"list_id": "L1",
"list_name": "{human name from the person's message}",
"item_count": 31,
"items": [
{ "item_id": "L1-001", "label": "{exact text from person}", "status": "not_started", "registered_at": "{ts}", "started_at": null, "completed_at": null }
]
},
{
"list_id": "L2",
"list_name": "{human name}",
"item_count": 18,
"items": [...]
}
],
"requirements": [
{ "req_id": "R1", "description": "{a per-item quality check that applies to everything — e.g. '/jonathan-check2 on every item' is one example; use your own review step, or your own reaction-prediction oracle if you've set one up}", "applies_to": "all", "checked_count": 0, "total_items": 49 }
],
"total_items": 66,
"approved_reductions": []
}
Step 2: Persist the Registry
Write the scope registry to a persistent location accessible across dispatch cycles:
SESSION_ID=$(alignment-harness session-id 2>/dev/null || echo "unknown-session")
SCOPE_FILE="<project>/.claude/pipeline-scope/${SESSION_ID}.json" # one file per session — a shared fixed path lets two pipelines corrupt or block each other
# Write registry
cat > "$SCOPE_FILE" << 'EOF'
{scope_registry_json}
EOF
echo "✅ Scope registered: {total_items} items across {list_count} lists"
echo " L1: {count} {name}"
echo " L2: {count} {name}"
echo " (continue for all lists)"
echo " Requirements: {requirement descriptions}"
Step 3: Print the Registered Scope to Chat
ALWAYS print the full scope to the person before any dispatch begins. This is the alignment checkpoint.
## SCOPE REGISTERED — {total_items} ITEMS ACROSS {list_count} LISTS
L1 — {name}: {count} items
L2 — {name}: {count} items
L3 — {name}: {count} items
L4 — {name}: {count} items
Requirements applied to all items:
- {requirement 1}
- {requirement 2}
TOTAL SCOPE: {total} items + {req_count} requirements
This scope is now locked. The orchestrator will not declare done until all items are accounted for — or until you explicitly authorize a reduction.
Wait for the person to confirm the scope is complete before dispatching anything.
Step 4: Post Scope to Compaction
Use declareScope() from /declare-scope with the full item list encoded as decomposed statements:
declareScope \
"Process {total_items} items across {list_count} lists with {req_count} requirements" \
'["When all L1 items are dispatched → each page gets its per-item check", "When all L2 items are dispatched → each draft gets its per-item check", "When all L3 items are dispatched → the diagnostic report is complete", "When all L4 items are dispatched → all pending items are resolved"]' \
"Not reducing scope without explicit person approval" \
"cat <project>/.claude/pipeline-scope-registry.json | jq '.total_items, (.lists[] | {list: .list_name, started: [.items[] | select(.status != \"not_started\")] | length, total: .item_count})'" \
95
Phase 2 — Dispatch Cycle: Mark Items as Started
When to invoke: Each time the orchestrator dispatches work for a specific item.
SESSION_ID=$(alignment-harness session-id 2>/dev/null || echo "unknown-session")
SCOPE_FILE="<project>/.claude/pipeline-scope/${SESSION_ID}.json" # one file per session — a shared fixed path lets two pipelines corrupt or block each other
# Mark item started
mark_started() {
local LIST_ID="$1" # e.g., "L1"
local ITEM_ID="$2" # e.g., "L1-001"
local NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
local UPDATED=$(jq \
--arg lid "$LIST_ID" \
--arg iid "$ITEM_ID" \
--arg ts "$NOW" \
'(.lists[] | select(.list_id == $lid) | .items[] | select(.item_id == $iid) | .status) = "in_progress" |
(.lists[] | select(.list_id == $lid) | .items[] | select(.item_id == $iid) | .started_at) = $ts' \
"$SCOPE_FILE")
echo "$UPDATED" > "$SCOPE_FILE"
echo "▶ Marked $ITEM_ID as in_progress"
}
# Mark item complete
mark_complete() {
local LIST_ID="$1"
local ITEM_ID="$2"
local NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
local UPDATED=$(jq \
--arg lid "$LIST_ID" \
--arg iid "$ITEM_ID" \
--arg ts "$NOW" \
'(.lists[] | select(.list_id == $lid) | .items[] | select(.item_id == $iid) | .status) = "complete" |
(.lists[] | select(.list_id == $lid) | .items[] | select(.item_id == $iid) | .completed_at) = $ts' \
"$SCOPE_FILE")
echo "$UPDATED" > "$SCOPE_FILE"
echo "✅ Marked $ITEM_ID as complete"
}
# Mark requirement checked for an item
mark_req_checked() {
local REQ_ID="$1" # e.g., "R1"
local UPDATED=$(jq \
--arg rid "$REQ_ID" \
'(.requirements[] | select(.req_id == $rid) | .checked_count) += 1' \
"$SCOPE_FILE")
echo "$UPDATED" > "$SCOPE_FILE"
}
Phase 3 — Coverage Report (runs every 10 minutes)
When to invoke: On a 10-minute timer during active pipeline sessions, AND any time the orchestrator considers saying "done" or "complete."
Calculate Coverage
SESSION_ID=$(alignment-harness session-id 2>/dev/null || echo "unknown-session")
SCOPE_FILE="<project>/.claude/pipeline-scope/${SESSION_ID}.json" # one file per session — a shared fixed path lets two pipelines corrupt or block each other
generate_coverage_report() {
if [ ! -f "$SCOPE_FILE" ]; then
echo "❌ SCOPE GUARDIAN: No scope registry found at $SCOPE_FILE"
echo " The orchestrator received work but never called pipeline-scope-guardian to register it."
echo " This is the v1 failure mode. Register scope before dispatching any work."
return 1
fi
local TOTAL=$(jq '.total_items' "$SCOPE_FILE")
local NOT_STARTED=$(jq '[.lists[].items[] | select(.status == "not_started")] | length' "$SCOPE_FILE")
local IN_PROGRESS=$(jq '[.lists[].items[] | select(.status == "in_progress")] | length' "$SCOPE_FILE")
local COMPLETE=$(jq '[.lists[].items[] | select(.status == "complete")] | length' "$SCOPE_FILE")
local STARTED=$((IN_PROGRESS + COMPLETE))
local STARTED_PCT=$(echo "scale=1; $STARTED * 100 / $TOTAL" | bc)
local COMPLETE_PCT=$(echo "scale=1; $COMPLETE * 100 / $TOTAL" | bc)
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📋 SCOPE COMPLIANCE REPORT — $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "COVERAGE:"
echo " Started: $STARTED / $TOTAL ($STARTED_PCT%)"
echo " Complete: $COMPLETE / $TOTAL ($COMPLETE_PCT%)"
echo " Remaining: $NOT_STARTED items not yet started"
echo ""
# Per-list breakdown
echo "PER LIST:"
jq -r '.lists[] | " \(.list_id) — \(.list_name): \([.items[] | select(.status == "complete")] | length)/\(.item_count) complete, \([.items[] | select(.status == "not_started")] | length) not started"' "$SCOPE_FILE"
echo ""
# Requirements compliance
echo "REQUIREMENTS:"
jq -r '.requirements[] | " \(.req_id) — \(.description): \(.checked_count)/\(.total_items) checked"' "$SCOPE_FILE"
echo ""
# What's missing
if [ "$NOT_STARTED" -gt 0 ]; then
echo "NOT YET STARTED ($NOT_STARTED items):"
jq -r '.lists[] | .list_id as $lid | .list_name as $lname | .items[] | select(.status == "not_started") | " [\($lid)] \(.item_id) — \(.label)"' "$SCOPE_FILE" | head -50
if [ "$NOT_STARTED" -gt 50 ]; then
echo " ... and $((NOT_STARTED - 50)) more"
fi
echo ""
fi
# Time-since-registration for unstarted items
local SCOPE_AGE_MINS=0
local REG_TIME=$(jq -r '.registered_at' "$SCOPE_FILE")
if [ -n "$REG_TIME" ] && [ "$REG_TIME" != "null" ]; then
local NOW_EPOCH=$(date -u +%s)
# Try BSD date (macOS) first, then GNU date (Linux), then give up honestly
local REG_EPOCH
REG_EPOCH=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$REG_TIME" +%s 2>/dev/null) \
|| REG_EPOCH=$(date -u -d "$REG_TIME" +%s 2>/dev/null) \
|| REG_EPOCH="$NOW_EPOCH"
SCOPE_AGE_MINS=$(( (NOW_EPOCH - REG_EPOCH) / 60 ))
echo " Scope registered $SCOPE_AGE_MINS minutes ago"
echo ""
fi
# Compliance verdict
if [ "$COMPLETE" -eq "$TOTAL" ]; then
echo "✅ SCOPE COMPLETE — All $TOTAL items processed."
echo " Orchestrator may declare done."
elif [ "$NOT_STARTED" -gt 0 ]; then
echo "🔴 SCOPE INCOMPLETE — $NOT_STARTED items have never been dispatched."
echo ""
echo " The orchestrator MUST NOT declare done."
echo " If scope reduction is intended, the person must explicitly authorize it."
echo " Authorized reductions so far: $(jq '.approved_reductions | length' "$SCOPE_FILE")"
echo ""
echo " NEXT REQUIRED ACTION: Dispatch the $NOT_STARTED unstarted items, or"
echo " ask the person: 'Do you want me to skip [list name]? It has $NOT_STARTED items not yet started.'"
else
echo "⏳ SCOPE IN PROGRESS — All items started, $((TOTAL - COMPLETE)) not yet complete."
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Return non-zero if incomplete (used by done-gate)
if [ "$COMPLETE" -lt "$TOTAL" ]; then
return 1
fi
return 0
}
generate_coverage_report
Phase 4 — Done Gate (blocks "done" declarations)
When to invoke: ANY TIME the orchestrator considers outputting the words "done", "complete", "finished", "all processed", "wrapped up", or any functional equivalent.
This is a hard gate. It must run BEFORE the orchestrator types those words.
SESSION_ID=$(alignment-harness session-id 2>/dev/null || echo "unknown-session")
SCOPE_FILE="<project>/.claude/pipeline-scope/${SESSION_ID}.json" # one file per session — a shared fixed path lets two pipelines corrupt or block each other
done_gate() {
if [ ! -f "$SCOPE_FILE" ]; then
echo "🔴 DONE GATE BLOCKED: No scope registry exists."
echo " Scope was never registered. Cannot verify completion."
echo " If this session had a scope, register it now via pipeline-scope-guardian Phase 1."
return 1
fi
local TOTAL=$(jq '.total_items' "$SCOPE_FILE")
local COMPLETE=$(jq '[.lists[].items[] | select(.status == "complete")] | length' "$SCOPE_FILE")
local NOT_STARTED=$(jq '[.lists[].items[] | select(.status == "not_started")] | length' "$SCOPE_FILE")
local APPROVED_REDUCTIONS=$(jq '.approved_reductions | length' "$SCOPE_FILE")
if [ "$COMPLETE" -eq "$TOTAL" ]; then
echo "✅ DONE GATE PASSED: $COMPLETE/$TOTAL items complete."
return 0
fi
# Check if a reduction was approved that covers remaining items
if [ "$APPROVED_REDUCTIONS" -gt 0 ]; then
local APPROVED_COUNT=$(jq '[.approved_reductions[].item_ids[]] | length' "$SCOPE_FILE")
local EFFECTIVE_TOTAL=$((TOTAL - APPROVED_COUNT))
if [ "$COMPLETE" -ge "$EFFECTIVE_TOTAL" ]; then
echo "✅ DONE GATE PASSED (with approved reductions): $COMPLETE/$EFFECTIVE_TOTAL effective items complete."
echo " $APPROVED_COUNT items were explicitly removed from scope by person."
return 0
fi
fi
echo ""
echo "🔴 DONE GATE BLOCKED"
echo ""
echo " The orchestrator attempted to declare done."
echo " Scope compliance check failed."
echo ""
echo " Promised: $TOTAL items"
echo " Complete: $COMPLETE items"
echo " Not started: $NOT_STARTED items"
echo " Missing: $((TOTAL - COMPLETE)) items"
echo ""
echo " This is the v1 failure mode. The orchestrator received scope and"
echo " did not process all of it. Declaring done without authorization"
echo " is a scope reduction without consent."
echo ""
echo " REQUIRED: Either dispatch the missing $NOT_STARTED items, or ask"
echo " the person for explicit authorization to skip them."
echo ""
echo " To authorize a reduction, the person says:"
echo " 'Skip [list/items] — remove them from scope'"
echo " Then call: register_approved_reduction 'L2' 'all' 'person said skip the drafts'"
echo ""
return 1
}
Phase 5 — Person-Authorized Reduction
When to invoke: Only when the person has explicitly said to skip or remove items from scope. Never infer this from context.
register_approved_reduction() {
local LIST_ID="$1" # e.g., "L2" or "all"
local ITEM_SCOPE="$2" # e.g., "all", "L2-001,L2-002", specific item IDs
local REASON="$3" # verbatim person quote or clear description
local NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
if [ -z "$REASON" ]; then
echo "❌ Cannot register reduction without a reason."
echo " Include the person's verbatim authorization."
return 1
fi
# Collect affected item IDs
local AFFECTED_IDS="[]"
if [ "$ITEM_SCOPE" = "all" ]; then
if [ "$LIST_ID" = "all" ]; then
AFFECTED_IDS=$(jq '[.lists[].items[].item_id]' "$SCOPE_FILE")
else
AFFECTED_IDS=$(jq --arg lid "$LIST_ID" '[.lists[] | select(.list_id == $lid) | .items[].item_id]' "$SCOPE_FILE")
fi
else
# ITEM_SCOPE is a specific comma-separated list of item IDs, e.g. "L2-001,L2-002" —
# this branch was missing before, which meant an explicit partial-skip authorization
# silently recorded zero items and the done-gate kept blocking anyway.
AFFECTED_IDS=$(echo "$ITEM_SCOPE" | tr ',' '\n' | sed '/^$/d' | jq -R . | jq -s .)
fi
local REDUCTION=$(jq -nc \
--arg lid "$LIST_ID" \
--arg scope "$ITEM_SCOPE" \
--arg reason "$REASON" \
--arg ts "$NOW" \
--argjson ids "$AFFECTED_IDS" \
'{list_id: $lid, item_scope: $scope, reason: $reason, authorized_at: $ts, item_ids: $ids}')
local UPDATED=$(jq \
--argjson reduction "$REDUCTION" \
'.approved_reductions += [$reduction]' \
"$SCOPE_FILE")
echo "$UPDATED" > "$SCOPE_FILE"
local COUNT=$(echo "$AFFECTED_IDS" | jq 'length')
echo "✅ Reduction registered: $COUNT items removed from scope for list $LIST_ID"
echo " Reason: $REASON"
echo " This authorization will be preserved in the scope registry."
}
Timer Protocol — 10-Minute Cycle
The orchestrator is responsible for invoking the coverage report every 10 minutes during an active pipeline session. This is not optional — it is the mechanism that prevents silent drift.
Invoke pattern (orchestrator includes in its dispatch loop):
Every 10 minutes during a pipeline session:
1. Run Phase 3 coverage report
2. If coverage < 100% and scope is not complete:
- Do NOT proceed to "done"
- Surface the missing items to person
- Ask: "I still have [N] items in [list name] not started. Continue dispatching them?"
3. If person says yes → continue dispatching
4. If person says skip → call register_approved_reduction with their words
5. If person says done → run done_gate, which will block unless coverage is 100%
The orchestrator MUST print the coverage report as its regular status update. It is not internal tooling — it is the person's real-time view of what was promised vs what was done.
Scope Compliance Report Format
Every report must answer these 4 questions in order:
WHAT WAS PROMISED
{list all lists with item counts}
WHAT WAS DELIVERED
{list all complete items, grouped by list}
WHAT IS MISSING
{list all not-started items, with time-since-registration}
REQUIREMENTS STATUS
{for each requirement, checked_count / total_items}
If the answer to "what is missing" is "nothing" — coverage is 100%. If the answer to "what is missing" is anything else — the session is not done.
Integration With /declare-scope
This skill calls /declare-scope at registration time to persist the scope in the compaction system. After the full session is complete, the compaction record will show:
- The full scope that was registered
- The coverage at each checkpoint
- Any authorized reductions with person quotes
- Final completion status
This makes scope decisions auditable — the person can review the compaction and see exactly what was promised, what was delivered, and who authorized any gaps.
Failure Mode Reference
The specific failure this skill prevents:
| v1 Behavior | Guardian Behavior |
|---|---|
| Received 4 lists, processed 1 | Registers all 4 lists before any dispatch |
| Lists 2-4 silently dropped | Timer surfaces L2-L4 as 0% started after 10 minutes |
| Orchestrator declared progress | Done gate blocks any "complete" claim |
| Person unaware scope was reduced | Coverage report shows L2-L4 at 0 of N |
| No record of reduction decision | Reduction registry is empty — no authorization existed |
File Location
<project>/.claude/pipeline-scope/{session-id}.json
One file per session — not a single shared path — so two pipeline sessions running at once, or a leftover file from a session that crashed, can't corrupt or block each other. This file is the single source of truth for the current pipeline session's scope. It persists across dispatch cycles. If it does not exist when the orchestrator begins dispatching, Phase 1 was skipped — that is a compliance failure.
Honest limits of this gate
Everything above — registering scope, the coverage report, the done gate — is real, working bash the orchestrator runs. But as shipped, it is enforced by the orchestrator choosing to run it, not by anything that stops the orchestrator on its own if it just skips straight to saying "done." If your setup has a way to run a check automatically at the end of a turn (a Stop hook, or a dispatched watcher agent scoped to this skill), wire done_gate into it — that's what turns this from "the orchestrator polices itself" into a check with teeth. Until then, treat the gate as a strong discipline the orchestrator is instructed to follow, and say so plainly if asked whether it's truly unskippable.