# Skill Planner Hard

> Create hard-mode implementation plans with phase sizing, postmortem constraints, and preserved-assets accounting. Invoke for --hard planning tasks.

- Skill: `benbrastmckie/skill-planner-hard` (Agent Skill)
- Install (CLI): `npx skillmds@latest add benbrastmckie/skill-planner-hard`
- Raw SKILL.md: https://api.skillmd.com/api/skills/benbrastmckie/skill-planner-hard/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: benbrastmckie (https://skillmd.com/u/benbrastmckie)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/benbrastmckie/skill-planner-hard

---


# Planner Hard Skill

Hard-mode wrapper that delegates plan creation to `planner-hard-agent` subagent.
Extends `skill-planner` with H8 plan requirements passed in delegation context.

**Relationship to base skill**: Structurally identical to `skill-planner` except it
dispatches to `planner-hard-agent` and passes H8 plan requirements.
Maintenance note: changes to `skill-planner` postflight should be mirrored here.

## Context References

Reference (do not load eagerly):
- Path: `.claude/context/formats/return-metadata-file.md` - Metadata file schema
- Path: `.claude/context/contracts/reference-grounding.md` - H3 contract (loaded by agent)
- Path: `.claude/context/patterns/postflight-control.md` - Marker file protocol
- Path: `.claude/context/patterns/jq-escaping-workarounds.md` - jq escaping patterns

## Trigger Conditions

This skill activates when:
- `/plan N --hard` is invoked and no extension hard variant exists
- Routed here by `command-route-skill.sh` with `effort_flag="hard"`

---

## Execution Flow

### Stage 1: Input Validation

```bash
task_data=$(jq -r --argjson num "$task_number" \
  '.active_projects[] | select(.project_number == $num)' \
  specs/state.json)

if [ -z "$task_data" ]; then
  return error "Task $task_number not found"
fi

task_type=$(echo "$task_data" | jq -r '.task_type // "general"')
status=$(echo "$task_data" | jq -r '.status')
project_name=$(echo "$task_data" | jq -r '.project_name')
description=$(echo "$task_data" | jq -r '.description // ""')

if [ "$status" = "completed" ] || [ "$status" = "abandoned" ] || [ "$status" = "expanded" ]; then
  return error "Task is in terminal state [$status]"
fi
```

---

### Stage 1.5: Hard-Mode Cost Note

```bash
session_flag_file="/tmp/.hard-mode-notified-${SESSION_ID:-$$}"
if [ ! -f "$session_flag_file" ]; then
  echo "[hard-mode] Hard mode active. Cost: ~3-5x standard. Use --hard for deflection-prone or formally complex tasks." >&2
  touch "$session_flag_file"
fi
```

---

### Stage 2 + Stage 3: Preflight Status Update and Postflight Marker

Source `skill-base.sh` once, then follow `@.claude/context/patterns/skill-preflight-flow.md` in
full for Stage 2 (preflight status update) and Stage 3 (marker creation):

```bash
source .claude/scripts/skill-base.sh
padded_num=$(printf "%03d" "$task_number")
skill_name="skill-planner-hard"
operation="plan"
```

**Marker unification note**: this skill's marker previously dropped `created` and
`stop_hook_active` (Shape C) — a drift, not a hard-mode design decision. Routing through
`skill_create_postflight_marker` restores both fields as part of this conversion, matching every
other importer's Shape A schema.

---

### Stage 3a: Read Artifact Number

```bash
artifact_number=$(jq -r --argjson num "$task_number" \
  '.active_projects[] | select(.project_number == $num) | .next_artifact_number // 1' \
  specs/state.json)

if [ "$artifact_number" = "null" ] || [ -z "$artifact_number" ]; then
  artifact_number=1
fi
# Plans use (current - 1) to stay in the same round as research
plan_artifact_number=$(( artifact_number - 1 ))
[ "$plan_artifact_number" -lt 1 ] && plan_artifact_number=1
plan_padded=$(printf "%02d" "$plan_artifact_number")
```

---

### Stage 3b: Find Research Report and Prior Plan

```bash
padded_num=$(printf "%03d" "$task_number")
task_dir="specs/${padded_num}_${project_name}"

# Find latest research report
research_path=$(ls "${task_dir}/reports/"*.md 2>/dev/null | sort | tail -1)

# Find latest prior plan
prior_plan_path=$(ls "${task_dir}/plans/"*.md 2>/dev/null | sort | tail -1)
```

---

### Stage 4a: Memory Retrieval (Auto)

```bash
if [ "$clean_flag" != "true" ]; then
  memory_context=$(bash .claude/scripts/memory-retrieve.sh "$description" "$task_type" "" 2>/dev/null) || memory_context=""
fi
```

**Literature Detection and Injection (Stage 4a, shared block)**

Follow `@.claude/context/patterns/lit-stage4a-flow.md` in full to resolve `--lit` and set
`lit_context`: call `literature-lit-flag-resolve.sh`, branch on all six directives
(`LIT_DISABLED`, `SUBINDEX_PRESENT`, `GLOBAL_MISSING`, `PROMPT_NEEDED`, `AUTONOMOUS_GLOBAL`,
`SPARSE_PROMPT_NEEDED`), issue the real four-option `AskUserQuestion` for the two interactive
directives (including the "Search online to ingest" option wired to the STABLE-CONTRACT
`literature-ingest-online.sh` bridge), apply the two-checkpoint sparse re-prompt after "Use
global corpus now", and take the deterministic `[lit:auto]` autonomous fallback when
`orchestrator_mode == "true"` (never calling `AskUserQuestion` in that case). This skill
supplies the shared block's preconditions: `lit_flag`, `description`, and `orchestrator_mode`
(read from the delegation context; default `"false"` when unset).

**Note**: `lit_flag` is independent of `clean_flag`. Using `--clean --lit` suppresses memory retrieval but still injects literature briefing. Literature briefing is gated solely on `lit_flag == "true"`.

---

### Stage 4: Prepare Delegation Context

Pass H8 plan requirements explicitly in the delegation context:

```json
{
  "session_id": "{session_id}",
  "delegation_depth": 1,
  "delegation_path": ["orchestrator", "plan", "skill-planner-hard"],
  "timeout": 3600,
  "task_context": {
    "task_number": N,
    "task_name": "{project_name}",
    "description": "{description}",
    "task_type": "{task_type}"
  },
  "artifact_number": "{plan_artifact_number}",
  "research_path": "{research_path or null}",
  "prior_plan_path": "{prior_plan_path or null}",
  "effort_flag": "hard",
  "model_flag": "{model_flag from command, null if not set}",
  "roadmap_path": "specs/ROADMAP.md",
  "roadmap_flag": "{roadmap_flag from command}",
  "metadata_file_path": "specs/{NNN}_{SLUG}/.return-meta.json",
  "hard_mode_requirements": {
    "phase_sizing_constraint": "Each phase must be completable in one agent run (~100-500 lines output)",
    "postmortem_constraints_required": true,
    "preserved_assets_accounting": "Required when prior plan exists",
    "source_to_implementation_mapping": "Required for Tier 1/2 reference tasks",
    "wave_map_required": true
  }
}
```

---

### Stage 4b: Read Format Specification

```bash
format_content=$(cat .claude/context/formats/plan-format.md)
```

---

### Stage 5: Invoke Subagent

```
Tool: Agent
Parameters:
  - subagent_type: "planner-hard-agent"
  - prompt: [task_context, delegation_context, format specification, memory_context, lit_context]
  - description: "Create hard-mode implementation plan for task {N}"
```

If `lit_context` is non-empty, inject it as a `<literature-briefing>` block after the memory context and before the task-specific instructions.

---

### Stage 5b: Self-Execution Fallback

Follow `@.claude/context/patterns/skill-self-execution-fallback.md` in full. This skill's success
status value for that block's write obligation is `"planned"`.

---

## Postflight (ALWAYS EXECUTE)

### Stage 6: Parse Subagent Return

```bash
metadata_file="specs/${padded_num}_${project_name}/.return-meta.json"

if [ -f "$metadata_file" ] && jq empty "$metadata_file" 2>/dev/null; then
    status=$(jq -r '.status' "$metadata_file")
    artifact_path=$(jq -r '.artifacts[0].path // ""' "$metadata_file")
    artifact_type=$(jq -r '.artifacts[0].type // ""' "$metadata_file")
    artifact_summary=$(jq -r '.artifacts[0].summary // ""' "$metadata_file")
    memory_candidates=$(jq -c '.memory_candidates // []' "$metadata_file")
    postmortem_rules_count=$(jq -r '.postmortem_rules_count // 0' "$metadata_file")
    echo "[hard-mode] Postmortem rules added to plan: $postmortem_rules_count" >&2
else
    status="failed"
fi
```

---

### Stage 6a: Validate Artifact Content (non-blocking)

```bash
if [ "$status" = "planned" ] && [ -n "$artifact_path" ] && [ -f "$artifact_path" ]; then
    bash .claude/scripts/validate-artifact.sh "$artifact_path" plan --fix || true
fi
```

---

### Stage 6b: Skeleton Task Allocation (H8 escape valve)

Duplicate-with-modification of `skill-spawn/SKILL.md` Stages 7-11 (do NOT extract a shared
helper — this is a deliberate lower-risk, smaller-diff first cut; revisit only if a second
consumer appears). Structurally identical, EXCEPT the dependency direction is REVERSED (see
below) and the artifact consumed is `.skeleton-return.json` rather than `.spawn-return.json`.

Runs only when `planner-hard-agent` (Stage 4a of `planner-hard-agent.md`) declared a skeleton
plan. No-ops cleanly otherwise.

```bash
skeleton_file="specs/${padded_num}_${project_name}/.skeleton-return.json"

if [ -f "$skeleton_file" ] && jq empty "$skeleton_file" 2>/dev/null; then
  new_tasks=$(jq -r '.new_tasks' "$skeleton_file")
  task_count=$(jq '.new_tasks | length' "$skeleton_file")

  if [ "$task_count" -gt 0 ]; then
    dependency_order=$(jq -r '.dependency_order' "$skeleton_file")

    # Stage 6b-i: Get next task numbers (mirrors skill-spawn Stage 8)
    next_num=$(jq -r '.next_project_number' specs/state.json)

    # Stage 6b-ii: Apply topological sort (mirrors skill-spawn Stage 9)
    # dependency_order is already topologically sorted (foundational first).
    declare -A task_num_map
    order_idx=0
    for idx in $(echo "$dependency_order" | jq -r '.[]'); do
        task_num_map[$idx]=$((next_num + order_idx))
        order_idx=$((order_idx + 1))
    done

    # Stage 6b-iii: Create new task directories (mirrors skill-spawn Stage 10)
    for idx in $(echo "$dependency_order" | jq -r '.[]'); do
        new_task_num=${task_num_map[$idx]}
        new_padded=$(printf "%03d" "$new_task_num")
        task_title=$(jq -r --argjson i "$idx" '.new_tasks[$i].title' "$skeleton_file")
        task_slug=$(echo "$task_title" | tr '[:upper:]' '[:lower:]' | tr ' ' '_' | sed 's/[^a-z0-9_]//g')
        mkdir -p "specs/${new_padded}_${task_slug}/reports"
    done

    # Stage 6b-iv: Update state.json with new tasks (mirrors skill-spawn Stage 11), BUT with the
    # SETTLED REVERSED dependency direction: each follow-up task's `dependencies` includes the
    # SKELETON (current) task number, NOT sibling new_tasks that gate it the spawn-agent way.
    # This is the inverse of skill-spawn Stage 13 -- copying that step verbatim here would wire
    # dependencies backwards and leave the skeleton task perpetually [BLOCKED].
    for idx in $(echo "$dependency_order" | jq -r '.[]'); do
        new_task_num=${task_num_map[$idx]}
        task_title=$(jq -r --argjson i "$idx" '.new_tasks[$i].title' "$skeleton_file")
        task_desc=$(jq -r --argjson i "$idx" '.new_tasks[$i].description' "$skeleton_file")
        task_effort=$(jq -r --argjson i "$idx" '.new_tasks[$i].effort' "$skeleton_file")
        task_type_new=$(jq -r --argjson i "$idx" '.new_tasks[$i].task_type' "$skeleton_file")
        task_slug=$(echo "$task_title" | tr '[:upper:]' '[:lower:]' | tr ' ' '_' | sed 's/[^a-z0-9_]//g')

        # REVERSED: dependencies = [skeleton_task_number] (the current task), not the spawn-agent
        # sibling-index resolution. The skeleton task's own `dependencies` field is untouched --
        # there is no Stage 13-equivalent "update parent task dependencies" step here.
        resolved_deps="[$task_number]"

        bash .claude/scripts/state-write.sh \
          '.active_projects += [{
            "project_number": $num,
            "project_name": $name,
            "status": "not_started",
            "task_type": $lang,
            "description": $desc,
            "effort": $effort,
            "parent_task": $parent,
            "dependencies": $deps,
            "created": $ts,
            "last_updated": $ts,
            "artifacts": []
          }]' \
          --session-id "$session_id" \
          --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
          --argjson num "$new_task_num" \
          --arg name "$task_slug" \
          --arg desc "$task_desc" \
          --arg effort "$task_effort" \
          --arg lang "$task_type_new" \
          --argjson deps "$resolved_deps" \
          --argjson parent "$task_number"
    done

    # Update next_project_number
    bash .claude/scripts/state-write.sh \
      '.next_project_number = $next' \
      --session-id "$session_id" \
      --argjson next "$((next_num + task_count))"

    # Stage 6b-v: Record follow-up task numbers on the skeleton (current) task's plan_metadata
    # (skeleton: true, follow_up_tasks: [...]) per plan-format.md's schema (Phase 3).
    follow_up_task_nums="[]"
    for idx in $(echo "$dependency_order" | jq -r '.[]'); do
        follow_up_task_nums=$(echo "$follow_up_task_nums" | jq --argjson n "${task_num_map[$idx]}" '. + [$n]')
    done
    # Stage 6b-vi: fold --regen-todo -- this write is immediately followed by nothing but the
    # TODO.md regen, matching the review.md `.active_goal` precedent.
    bash .claude/scripts/state-write.sh \
       '(.active_projects[] | select(.project_number == $num) | .plan_metadata) =
        ((.active_projects[] | select(.project_number == $num) | .plan_metadata) // {} +
         {"skeleton": true, "follow_up_tasks": $follow_ups})' \
      --session-id "$session_id" \
      --argjson num "$task_number" --argjson follow_ups "$follow_up_task_nums" \
      --regen-todo
  fi
fi
```

---

### Stage 6c: Placeholder-Token Substitution Pass

Immediately after Stage 6b's allocation. Resolves every `{{FOLLOWUP:i}}` token written by
`planner-hard-agent` in the just-written plan file to the concrete allocated task number from
Stage 6b's `task_num_map`. This step has no `skill-spawn` equivalent (spawn-agent never emits
forward-reference tokens into a sibling artifact).

Guards to a clean no-op when `.skeleton-return.json` is absent (non-skeleton plans) — the plan
file is left completely untouched in that case.

```bash
if [ -f "$skeleton_file" ] && [ "$task_count" -gt 0 ] && [ -n "$artifact_path" ] && [ -f "$artifact_path" ]; then
  # Single text-substitution pass: replace {{FOLLOWUP:i}} with the real allocated task number.
  # Covers both the plan overview prose and the `## Planned Strategic Sorries` table's
  # `Follow-Up Task` column -- both are plain textual occurrences of the same token.
  for idx in $(echo "$dependency_order" | jq -r '.[]'); do
      token="{{FOLLOWUP:${idx}}}"
      real_num="${task_num_map[$idx]}"
      sed -i "s/${token//\//\\/}/${real_num}/g" "$artifact_path"
  done

  # Verify no unresolved tokens remain (defensive check, non-fatal)
  if grep -q '{{FOLLOWUP:' "$artifact_path" 2>/dev/null; then
    echo "[hard-mode] Warning: unresolved {{FOLLOWUP:i}} token(s) remain in $artifact_path after substitution pass" >&2
  fi

  echo "[hard-mode] Skeleton plan: allocated ${task_count} follow-up task(s), substituted placeholder tokens" >&2
fi
```

---

### Stage 7: Update Task Status (Postflight)

Follow `@.claude/context/patterns/skill-postflight-flow.md`'s Stage 7 (postflight status update):

```bash
skill_postflight_update "$task_number" "$operation" "$session_id" "$status"
```

---

### Stage 7a: Propagate Memory Candidates

Was a `Same as skill-planner Stage 7a pattern` prose cross-reference pointing at a target that
did not exist until `skill-planner` gained a real Stage 7a — drifted and functionally broken, not
cosmetic. Now a real import of `@.claude/context/patterns/skill-postflight-flow.md`'s Stage 7a,
resolving to the identical shared block `skill-planner` itself uses:

```bash
skill_propagate_memory_candidates "$task_number" "$memory_candidates" "$session_id"
```

---

### Stage 8: Link Artifacts

Follow `@.claude/context/patterns/skill-postflight-flow.md`'s Stage 8 (artifact linking):

```bash
field_name='**Plan**'
next_field='**Description**'
skill_link_artifacts "$task_number" "$artifact_path" "$artifact_type" "$artifact_summary" \
  "$field_name" "$next_field" "$session_id"
```

Performs the two-step jq pattern internally (Issue #1132-safe) and regenerates TODO.md when
`artifact_path` is non-empty.

---

### Stage 8a: Lifecycle TTS Notification

Follow `@.claude/context/patterns/skill-postflight-flow.md`'s Stage 8a (TTS notify):

```bash
skill_lifecycle_notify "$status"
```

---

### Stage 9: Cleanup

Follow `@.claude/context/patterns/skill-postflight-flow.md`'s Stage 9 (cleanup):

```bash
skill_cleanup "$padded_num" "$project_name"
```

---

## MUST NOT (Postflight Boundary)

After the agent returns, this skill MUST NOT:

1. **Edit source files** - All planning work is done by agent
2. **Run build/test commands** - Verification is done by agent
3. **Use research tools** - Web/codebase search is for agent use only
4. **Analyze task requirements** - Analysis is agent work
5. **Write plan files** - Artifact creation is agent work

The postflight phase is LIMITED TO:
- Reading agent metadata file
- Updating status via `update-task-status.sh` (handles state.json + TODO.md atomically)
- Linking artifacts in state.json
- Git commit
- Cleanup of temp/marker files

Reference: @.claude/context/standards/postflight-tool-restrictions.md

