phase-doc-sync
Applies sprint implementation results to docs/phases/ phase plan files. Used by phase-builder as a sub-agent after each implementation wave — updates task Status columns from structured SPRINT RESULT
SKILL.mdplugin/skills/phase-doc-sync/SKILL.md
GitHubPhase Doc Sync
Updates docs/phases/Phase-*.md task tables from structured sprint results. Read-only for implementation code — only touches phase plan markdown, plus the run log when the payload has run_log.
The phase-builder orchestrator spawns you as a sub-agent after each implementation wave. The orchestrator parses SPRINT RESULT blocks and passes a structured payload; you apply edits using phase-planner rules.
Orchestrator contract (phase-builder)
The orchestrator MUST:
- Parse each implementation sub-agent's
SPRINT RESULT:block into structured data - Spawn one doc-sync call per wave (never edit
docs/phases/*.mditself) - Wait for your
DOC SYNC RESULT:block before the next wave's verify (the next wave's implementation may already be running alongside you) - Log only a one-line summary per sprint (no direct edits to phase files in the orchestrator thread)
The orchestrator MUST NOT edit docs/phases/*.md during an active phase run.
Model (optional): this role is mechanical — flip Status cells from a structured payload — a good candidate for a cheaper/faster model. If runtime-adapter.md resolved a gate_model, spawn this call with it. Otherwise use the session default; this is a cost optimization, never a blocker.
Input payload (from orchestrator)
The orchestrator includes this JSON in your prompt:
{
"phase_file": "docs/phases/Phase-11-Example-Feature.md",
"project_root": "/path/to/workspace",
"app_root": "/path/to/app-root",
"sprints": [
{
"id": "11.1",
"title": "Example sprint title",
"completed": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
"blocked": [],
"blocked_reasons": {},
"manual": [],
"deferred": [],
"cut": [],
"notes": "optional string from SPRINT RESULT NOTES"
}
],
"user_overrides": [],
"run_log": {
"path": "/path/to/workspace/docs/phases/.runs/phase-11.jsonl", "phase": 11, "wave": 1, "attempt": 1, "max": 1,
"diff_base": "git tree sha the wave started from, or null",
"wave_end": "git tree sha of the code that passed the wave's last verify, or null"
}
}
run_log is optional. When present, append doc_sync events after your edits (see Run log); when absent, write nothing.
You may be running alongside the next wave's implementation agents. They edit app code, never the phase file, so there is nothing to coordinate — but don't read or diff the live working tree for this wave's changes. Use the two shas above.
Field rules:
| Field | Maps to Status |
|---|---|
completed |
x |
blocked |
BLOCKED |
manual |
MANUAL |
deferred |
DEFERRED |
cut |
CUT |
user_overrides |
After user says skip/defer/cut, or that a task is theirs to do — e.g. [{ "id": "11.3", "task": 3, "status": "DEFERRED" }]. status is DEFERRED, CUT or MANUAL |
Process sprints in ascending ID order (X.Y numeric) even if the payload order differs.
If a task number appears in multiple lists, precedence: blocked > manual > deferred > cut > completed. A user_overrides entry beats all of them.
Execution steps
- Load the phase-planner skill (per runtime-adapter.md in the phase-builder skill) only if a table needs migrating or its format is unclear. A table that already has a Status column doesn't need it.
- Don't read the whole phase file. Grep it for the sprint headers (
^# Sprint) and the task rows (^\| .* \| \d+ \|) with line numbers, then read only the### Taskstable of each sprint in the payload (offset/limit). - For each sprint in the payload (ascending
id):- Locate section
# Sprint {id} — {title} - Find the
### Taskstable - Migrate table (add Status column) if missing — per phase-planner
- Update Status cells for listed task numbers only
- Leave task text, Module, Reference, acceptance criteria, dependencies unchanged
- Locate section
- Write discipline:
- One edit per sprint section when possible (replace the entire
### Taskstable block) - If tables are too large for one match, at most one edit per sprint by matching from
### Tasksthrough the row before### Acceptance - Never one edit per task row
- One write pass per phase file for the whole payload (batch all sprint edits before writing, or sequential edits on the same file in one session — still no per-row spam)
- One edit per sprint section when possible (replace the entire
- Verify with the same grep: every listed task shows the expected status. Don't re-read the file.
- Log the sync — once STATUS is decided, if
run_logis present, append onedoc_syncline per sprint in the payload, with the wave'sfiles(see Run log) - End with
DOC SYNC RESULT:(required)
Status column values
| Value | Meaning |
|---|---|
x |
Completed |
BLOCKED |
Blocked |
MANUAL |
Only the user can do it; they mark it x themselves |
DEFERRED |
Deferred |
CUT |
Cut from scope |
— |
Not started (do not change unless explicitly in payload as reset) |
~ |
In progress (only set if orchestrator requests mark_active) |
Do not change tasks not mentioned in the payload unless the orchestrator sends mark_sprint_done: true for a sprint with empty lists — then set all — and ~ rows in that sprint to x (whole sprint complete). Leave MANUAL rows as they are.
Output block (required)
DOC SYNC RESULT:
STATUS: SUCCESS | PARTIAL | FAILED
FILE: {path relative to project root}
SPRINTS_SYNCED: {comma-separated sprint ids, or NONE}
TASKS_UPDATED: {total count}
FAILURES: [sprint id + reason per line, or NONE]
NOTES: [formatting issues, ambiguous rows, anything orchestrator should know]
- SUCCESS — every requested status applied and verified
- PARTIAL — some sprints updated; list in FAILURES
- FAILED — file not found, no matching sprint section, or could not apply without guessing
Run log
Only when the payload has run_log. The spec is phase-builder's run-log.md; this is the part you need. Never write the log with a file-edit tool.
1. The wave's changed files. Skip this line when run_log.diff_base or run_log.wave_end is null; the events then carry no files. Otherwise run it first, in the same command as the appends, with {app_root} = APP ROOT, {diff_base} = run_log.diff_base and {snapshot} = run_log.wave_end:
RL_FILES=$(cd '{app_root}' && git -c core.quotePath=false diff --relative --name-only {diff_base} {snapshot} -- ':(exclude)docs/phases') && RL_FILES=',"files":['"$(printf '%s' "$RL_FILES" | tr '\042\134' "'/" | sed 's/.*/"&"/' | paste -sd, -)"']' || RL_FILES=''
2. One append per sprint, with this exact line, replacing only the {…} placeholders and keeping every other character, including the summary's tr pipe and "${RL_FILES:-}". {path}, {phase}, {wave}, {attempt} and {max} come from run_log. Every sprint in the wave gets the same files.
mkdir -p "$(dirname '{path}')" && printf '{"v":1,"ts":"%s","phase":%d,"wave":%d,"sprint":"%s","gate":"%s","result":"%s","attempt":%d,"max":%d,"summary":"%s"%s}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '{phase}' '{wave}' '{sprint}' '{gate}' '{result}' '{attempt}' '{max}' "$(printf '%s' '{summary}' | tr '\042\134\011\012\015' "'/ " | tr -d '\000-\037')" "${RL_FILES:-}" >> '{path}'
Run it as one call: RL_FILES=…; {append for 11.1} && {append for 11.2}.
gate |
One line per | result |
summary |
|---|---|---|---|
doc_sync |
sprint in the payload, ascending id |
pass if its statuses were applied; fail if it's in FAILURES |
{k} tasks updated, or the reason from FAILURES |
- Summaries are one line, at most 200 characters. Write a straight apostrophe as
'\''; type everything else as-is, including"and\. Never backslash-escape a character: inside single quotes\"stays two characters and lands in the log as/'. - Use a POSIX shell (Git Bash on Windows).
- A git error in step 1 still writes the events, without
files; say so inNOTES. An append that exits non-zero gets one line inNOTES(run log: append failed — {reason}). Neither changes STATUS.
Prompt template (orchestrator fills braces)
You are the phase doc-sync agent. Update phase plan task statuses only.
Read and follow: the phase-doc-sync skill (loaded per runtime-adapter.md)
Read and follow: the phase-planner skill (loaded per runtime-adapter.md)
PROJECT ROOT: {project_root}
APP ROOT: {app_root}
PHASE FILE: {phase_file}
PAYLOAD:
{json payload}
RULES:
- Edit ONLY the phase file task Status columns for sprints in the payload
- Batch edits — never one edit per task row
- Do not modify acceptance criteria, dependencies, or overview prose
- Don't run git commands that change the working tree, index, stash or refs
- If the payload has run_log, append the doc_sync lines last (Execution step 6)
- End with DOC SYNC RESULT block
If SPRINT RESULT was missing for a sprint but orchestrator verified acceptance and sent mark_sprint_done, mark all incomplete tasks in that sprint as x.
Error handling
| Situation | DOC SYNC STATUS | Action |
|---|---|---|
| Phase file not found | FAILED | Report path; orchestrator confirms location |
| Sprint header not found | PARTIAL or FAILED | List in FAILURES; do not edit wrong section |
| Task # not in table | PARTIAL | Note in FAILURES; continue other tasks |
| Table has no Status column | SUCCESS after migrate | Migrate atomically per phase-planner |
| Ambiguous duplicate task text | FAILED | Do not guess; list row in NOTES |
Example
Input: Sprint 11.2, completed [1,2,3,4,5,6,7], blocked []
Action: Single edit on the ### Tasks table under # Sprint 11.2, flipping | — | to | x | for tasks 1–7 only.
Output:
DOC SYNC RESULT:
STATUS: SUCCESS
FILE: docs/phases/Phase-11-Example-Feature.md
SPRINTS_SYNCED: 11.2
TASKS_UPDATED: 7
FAILURES: NONE
NOTES: none