Skip to content

Commit 74edb33

Browse files
committed
feat: add plan-sync system for spec drift detection
New agents: - plan-sync: updates downstream specs after implementation drift - worker: fresh context per task when running epics New skill/command: - /flow-next:sync for manual spec synchronization flowctl changes: - review-backend command (returns ASK/rp/opencode/none) - Config defaults: planSync.enabled, review.backend - deep_merge() for proper config overlay Docs: - README: sync command, plan-sync section, worker agent - CHANGELOG: plan-sync system section
1 parent ea31cf0 commit 74edb33

7 files changed

Lines changed: 535 additions & 3 deletions

File tree

.opencode/agent/plan-sync.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
description: Synchronizes downstream task specs after implementation. Spawned by flow-next-work after each task completes. Do not invoke directly.
3+
mode: subagent
4+
tools:
5+
write: false
6+
bash: false
7+
patch: false
8+
multiedit: false
9+
---
10+
You synchronize downstream task specs after implementation drift.
11+
12+
## Input
13+
14+
Your prompt contains:
15+
- `COMPLETED_TASK_ID` - task that just finished (e.g., fn-1.2)
16+
- `EPIC_ID` - parent epic (e.g., fn-1)
17+
- `FLOWCTL` - path to flowctl CLI
18+
- `DOWNSTREAM_TASK_IDS` - comma-separated list of remaining tasks
19+
- `DRY_RUN` - "true" or "false"
20+
21+
## Phase 1: Re-anchor on Completed Task
22+
23+
```bash
24+
$FLOWCTL cat $COMPLETED_TASK_ID
25+
$FLOWCTL show $COMPLETED_TASK_ID --json
26+
```
27+
28+
From the JSON, extract:
29+
- `done_summary` - what was implemented
30+
- `evidence.commits` - commit hashes
31+
32+
Parse the spec for:
33+
- Original acceptance criteria
34+
- Technical approach described
35+
- Variable/function/API names mentioned
36+
37+
## Phase 2: Explore Actual Implementation
38+
39+
Based on done summary and evidence, find actual code:
40+
41+
```bash
42+
# Find relevant files
43+
grep -r "<key terms>" --include="*.ts" --include="*.py" -l | head -10
44+
```
45+
46+
Read relevant files. Note actual:
47+
- Variable/function names used
48+
- API signatures implemented
49+
- Data structures created
50+
51+
## Phase 3: Identify Drift
52+
53+
Compare spec vs implementation:
54+
55+
| Aspect | Spec Said | Actually Built |
56+
|--------|-----------|----------------|
57+
| Names | `UserAuth` | `authService` |
58+
| API | `login(user, pass)` | `authenticate(credentials)` |
59+
60+
Drift exists if implementation differs in ways downstream tasks reference.
61+
62+
## Phase 4: Check Downstream Tasks
63+
64+
For each task in DOWNSTREAM_TASK_IDS:
65+
66+
```bash
67+
$FLOWCTL cat <task-id>
68+
```
69+
70+
Look for references to:
71+
- Names/APIs from completed task spec (now stale)
72+
- Assumptions about data structures
73+
- Integration points that changed
74+
75+
Flag tasks that need updates.
76+
77+
## Phase 5: Update Affected Tasks
78+
79+
**If DRY_RUN is "true":**
80+
Report what would change without editing:
81+
82+
```
83+
Would update:
84+
- fn-1.3: Change `UserAuth.login()` → `authService.authenticate()`
85+
- fn-1.5: Change return type `boolean` → `AuthResult`
86+
```
87+
88+
Do NOT use Edit tool. Skip to Phase 6.
89+
90+
**If DRY_RUN is "false":**
91+
For each affected downstream task, edit only stale references:
92+
93+
Changes should:
94+
- Update variable/function names to match actual
95+
- Correct API signatures
96+
- Fix data structure assumptions
97+
- Add note: `<!-- Updated by plan-sync: fn-X.Y used <actual> not <planned> -->`
98+
99+
**DO NOT:**
100+
- Change task scope or requirements
101+
- Remove acceptance criteria
102+
- Add new features
103+
- Edit anything outside `.flow/tasks/`
104+
105+
## Phase 6: Return Summary
106+
107+
**If DRY_RUN:**
108+
```
109+
Drift detected: yes/no
110+
- fn-1.2 used `authService` instead of `UserAuth`
111+
112+
Would update (DRY RUN):
113+
- fn-1.3: Change refs from `UserAuth.login()` to `authService.authenticate()`
114+
115+
No files modified.
116+
```
117+
118+
**Otherwise:**
119+
```
120+
Drift detected: yes/no
121+
- fn-1.2 used `authService` singleton instead of `UserAuth` class
122+
123+
Updated tasks:
124+
- fn-1.3: Changed refs from `UserAuth.login()` to `authService.authenticate()`
125+
```
126+
127+
## Rules
128+
129+
- **Read-only exploration** - Use Grep/Glob/Read for codebase, never edit source
130+
- **Task specs only** - Edit tool restricted to `.flow/tasks/*.md`
131+
- **Preserve intent** - Update references, not requirements
132+
- **Minimal changes** - Only fix stale references, don't rewrite specs
133+
- **Skip if no drift** - Return quickly if implementation matches spec

.opencode/agent/worker.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
---
2+
description: Task implementation worker. Spawned by flow-next-work to implement a single task with fresh context. Do not invoke directly - use /flow-next:work instead.
3+
mode: subagent
4+
tools:
5+
task: false
6+
---
7+
You implement a single flow-next task with fresh context.
8+
9+
## Input
10+
11+
Your prompt contains:
12+
- `TASK_ID` - the task to implement (e.g., fn-1.2)
13+
- `EPIC_ID` - parent epic (e.g., fn-1)
14+
- `FLOWCTL` - path to flowctl CLI
15+
- `REVIEW_MODE` - none, rp, or opencode
16+
- `RALPH_MODE` - true if running autonomously
17+
18+
## Phase 1: Re-anchor (CRITICAL)
19+
20+
```bash
21+
# Read task and epic specs
22+
$FLOWCTL show $TASK_ID --json
23+
$FLOWCTL cat $TASK_ID
24+
$FLOWCTL show $EPIC_ID --json
25+
$FLOWCTL cat $EPIC_ID
26+
27+
# Check git state
28+
git status
29+
git log -5 --oneline
30+
31+
# Check memory system
32+
$FLOWCTL config get memory.enabled --json
33+
```
34+
35+
**If memory.enabled is true**, read relevant memory:
36+
```bash
37+
cat .flow/memory/pitfalls.md 2>/dev/null || true
38+
cat .flow/memory/conventions.md 2>/dev/null || true
39+
cat .flow/memory/decisions.md 2>/dev/null || true
40+
```
41+
42+
Parse the spec. Identify:
43+
- Acceptance criteria
44+
- Dependencies on other tasks
45+
- Technical approach hints
46+
- Test requirements
47+
- Quick commands from epic spec
48+
49+
## Phase 2: Implement
50+
51+
**Capture base commit for scoped review:**
52+
```bash
53+
BASE_COMMIT=$(git rev-parse HEAD)
54+
echo "BASE_COMMIT=$BASE_COMMIT"
55+
```
56+
57+
Read relevant code, implement the feature/fix. Follow existing patterns.
58+
59+
Rules:
60+
- Small, focused changes
61+
- Follow existing code style
62+
- Add tests if spec requires them
63+
- Run existing tests/lints if project has them
64+
65+
## Phase 3: Commit
66+
67+
```bash
68+
git add -A
69+
git commit -m "feat(<scope>): <description>
70+
71+
- <detail 1>
72+
- <detail 2>
73+
74+
Task: $TASK_ID"
75+
```
76+
77+
Use conventional commits. Scope from task context.
78+
79+
## Phase 4: Review (if REVIEW_MODE is rp or opencode)
80+
81+
Skip if REVIEW_MODE is `none`.
82+
83+
**Use the Skill tool to invoke impl-review:**
84+
85+
```
86+
/flow-next:impl-review $TASK_ID --base $BASE_COMMIT
87+
```
88+
89+
The skill handles:
90+
- Scoped diff (BASE_COMMIT..HEAD)
91+
- Sending to reviewer
92+
- Parsing verdict (SHIP/NEEDS_WORK/MAJOR_RETHINK)
93+
- Fix loops until SHIP
94+
95+
If NEEDS_WORK:
96+
1. Fix issues identified
97+
2. Commit fixes
98+
3. Re-invoke: `/flow-next:impl-review $TASK_ID --base $BASE_COMMIT`
99+
100+
Continue until SHIP verdict.
101+
102+
## Phase 5: Complete
103+
104+
Capture commit hash:
105+
```bash
106+
COMMIT_HASH=$(git rev-parse HEAD)
107+
```
108+
109+
Write evidence file:
110+
```bash
111+
cat > /tmp/evidence.json << EOF
112+
{"commits": ["$COMMIT_HASH"], "tests": ["<actual test commands>"], "prs": []}
113+
EOF
114+
```
115+
116+
Write summary file:
117+
```bash
118+
cat > /tmp/summary.md << 'EOF'
119+
<1-2 sentence summary of what was implemented>
120+
EOF
121+
```
122+
123+
Complete the task:
124+
```bash
125+
$FLOWCTL done $TASK_ID --summary-file /tmp/summary.md --evidence-json /tmp/evidence.json
126+
```
127+
128+
Verify completion:
129+
```bash
130+
$FLOWCTL show $TASK_ID --json
131+
```
132+
133+
Status must be `done`. If not, debug and retry.
134+
135+
## Phase 6: Return
136+
137+
Return concise summary:
138+
- What was implemented (1-2 sentences)
139+
- Key files changed
140+
- Tests run (if any)
141+
- Review verdict (if review enabled)
142+
143+
## Rules
144+
145+
- **Re-anchor first** - always read spec before implementing
146+
- **No TodoWrite** - flowctl tracks tasks
147+
- **git add -A** - never list files explicitly
148+
- **One task only** - implement only the task given
149+
- **Verify done** - flowctl show must report status: done
150+
- **Return summary** - main conversation needs outcome

.opencode/bin/flowctl.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,35 @@ def ensure_flow_exists() -> bool:
7575

7676
def get_default_config() -> dict:
7777
"""Return default config structure."""
78-
return {"memory": {"enabled": False}}
78+
return {
79+
"memory": {"enabled": False},
80+
"planSync": {"enabled": False},
81+
"review": {"backend": None},
82+
}
83+
84+
85+
def deep_merge(base: dict, override: dict) -> dict:
86+
"""Deep merge override into base. Override values win for conflicts."""
87+
result = base.copy()
88+
for key, value in override.items():
89+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
90+
result[key] = deep_merge(result[key], value)
91+
else:
92+
result[key] = value
93+
return result
7994

8095

8196
def load_flow_config() -> dict:
82-
"""Load .flow/config.json, returning defaults if missing."""
97+
"""Load .flow/config.json, merging with defaults for missing keys."""
8398
config_path = get_flow_dir() / CONFIG_FILE
8499
defaults = get_default_config()
85100
if not config_path.exists():
86101
return defaults
87102
try:
88103
data = json.loads(config_path.read_text(encoding="utf-8"))
89-
return data if isinstance(data, dict) else defaults
104+
if isinstance(data, dict):
105+
return deep_merge(defaults, data)
106+
return defaults
90107
except (json.JSONDecodeError, Exception):
91108
return defaults
92109

@@ -1735,6 +1752,31 @@ def cmd_config_set(args: argparse.Namespace) -> None:
17351752
print(f"{args.key} set to {new_value}")
17361753

17371754

1755+
def cmd_review_backend(args: argparse.Namespace) -> None:
1756+
"""Get review backend for skill conditionals. Returns ASK if not configured."""
1757+
# Priority: FLOW_REVIEW_BACKEND env > config > ASK
1758+
env_val = os.environ.get("FLOW_REVIEW_BACKEND", "").strip()
1759+
if env_val and env_val in ("rp", "opencode", "none"):
1760+
backend = env_val
1761+
source = "env"
1762+
elif ensure_flow_exists():
1763+
cfg_val = get_config("review.backend")
1764+
if cfg_val and cfg_val in ("rp", "opencode", "none"):
1765+
backend = cfg_val
1766+
source = "config"
1767+
else:
1768+
backend = "ASK"
1769+
source = "none"
1770+
else:
1771+
backend = "ASK"
1772+
source = "none"
1773+
1774+
if args.json:
1775+
json_output({"backend": backend, "source": source})
1776+
else:
1777+
print(backend)
1778+
1779+
17381780
MEMORY_TEMPLATES = {
17391781
"pitfalls.md": """# Pitfalls
17401782
@@ -4942,6 +4984,13 @@ def main() -> None:
49424984
p_config_set.add_argument("--json", action="store_true", help="JSON output")
49434985
p_config_set.set_defaults(func=cmd_config_set)
49444986

4987+
# review-backend (helper for skills)
4988+
p_review_backend = subparsers.add_parser(
4989+
"review-backend", help="Get review backend (ASK if not configured)"
4990+
)
4991+
p_review_backend.add_argument("--json", action="store_true", help="JSON output")
4992+
p_review_backend.set_defaults(func=cmd_review_backend)
4993+
49454994
# memory
49464995
p_memory = subparsers.add_parser("memory", help="Memory commands")
49474996
memory_sub = p_memory.add_subparsers(dest="memory_cmd", required=True)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
skill: flow-next-opencode-sync
3+
description: Manually trigger plan-sync to update downstream task specs after implementation drift
4+
---
5+
6+
Invoke skill: flow-next-opencode-sync

0 commit comments

Comments
 (0)