Skip to content

Commit 1a5bd3e

Browse files
authored
feat(workspace-planning): support module_files split layout (#10)
- load_schedule resolves module_files references, merging modules from separate YAML files with _source_file tracking - update/link commands are now validate-only (no yaml.dump writes) and output source_file so the LLM applies edits with Edit tool - document module_files field in schema and SKILL.md
1 parent 588b8b8 commit 1a5bd3e

3 files changed

Lines changed: 80 additions & 32 deletions

File tree

skills/workspace/workspace-planning/SKILL.md

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,21 @@ python3 <skill-dir>/scripts/planning.py week W3 # Show week
7575
```
7676

7777
All commands output JSON for the LLM to format. Use `--file` to specify a schedule YAML
78-
if multiple exist.
78+
if multiple exist. Always point `--file` at the **main** schedule file (e.g. `sylsmart.yaml`),
79+
not the month files — the script resolves `module_files` references automatically.
7980

8081
Requires: `pip install pyyaml`
8182

83+
### Split vs Inline Modules
84+
85+
Schedules support two layouts:
86+
87+
- **Inline**: `modules:` list directly in the main YAML (simple projects)
88+
- **Split**: `module_files:` list of relative paths, each containing a `modules:` list (large projects)
89+
90+
When using split layout, all read commands (`review`, `week`) merge modules from all
91+
referenced files. Write commands (`update`, `link`) save back to the correct source file.
92+
8293
## Commands
8394

8495
### `planning init <project-name>`
@@ -135,22 +146,33 @@ Legend: V done, * in_progress, o planned, - deferred
135146

136147
Update a module's status.
137148

149+
**Step 1 — Validate** with the script (does NOT write to files):
150+
138151
```bash
139152
python3 <skill-dir>/scripts/planning.py update <module-id> --status <status>
140153
```
141154

142-
The script validates the state machine transition and returns JSON with the result.
143-
If invalid, it shows the error with allowed target states.
155+
The script validates the state machine transition and returns JSON including `source_file`
156+
(the YAML file containing the module). If invalid, it exits with an error.
157+
158+
**Step 2 — Apply** with the Edit tool: use the `source_file` from the JSON output to
159+
locate the module and change its `status:` field. This preserves YAML comments and formatting.
144160

145161
### `planning link <module-id> --change <change-name>`
146162

147163
Associate an OpenSpec change with a module.
148164

165+
**Step 1 — Validate** with the script (does NOT write to files):
166+
149167
```bash
150168
python3 <skill-dir>/scripts/planning.py link <module-id> --change <change-name>
151169
```
152170

153-
The script verifies the change exists, appends it, and auto-transitions `planned` to `in_progress`.
171+
The script verifies the change exists and returns JSON with the updated `changes` list,
172+
`source_file`, and whether an auto-transition from `planned` to `in_progress` should apply.
173+
174+
**Step 2 — Apply** with the Edit tool: add the change name to the module's `changes:` list
175+
(or create the field). If `auto_transition` is true, also update `status: in_progress`.
154176

155177
### `planning sync-yunxiao`
156178

skills/workspace/workspace-planning/references/yaml-schema.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ phases: # optional
3131
end: 2026-04-03
3232
weeks: [W1, W2, W3, W4]
3333

34+
# Option A: inline modules
3435
modules:
3536
# Infrastructure module (backend-only, no UI frames)
3637
- id: core-extraction
@@ -62,6 +63,15 @@ modules:
6263
notes: "optional notes"
6364
yunxiao_id: "WI-12345"
6465
changes: ["add-auth-api"]
66+
67+
# Option B: split modules into separate files
68+
# Use module_files instead of modules. Paths are relative to
69+
# the main YAML file. Each referenced file has a top-level
70+
# "modules:" list. The CLI merges them at load time and writes
71+
# back to the correct file on update/link.
72+
module_files:
73+
- my-project-month-1.yaml
74+
- my-project-month-2.yaml
6575
```
6676
6777
## Field Reference
@@ -75,14 +85,15 @@ modules:
7585
| `timeline.start` | date | Project start date (ISO) |
7686
| `timeline.end` | date | Project end date (ISO) |
7787
| `milestones` | list | Milestone definitions |
78-
| `modules` | list | Module definitions |
88+
| `modules` | list | Module definitions (use this OR `module_files`, not both) |
7989

8090
### Optional Top-Level Fields
8191

8292
| Field | Type | Description |
8393
|-------|------|-------------|
8494
| `capacity` | object | Team capacity config |
8595
| `phases` | list | Phase definitions |
96+
| `module_files` | list | Paths to YAML files containing modules (relative to main file; alternative to inline `modules`) |
8697

8798
### Module Required Fields
8899

skills/workspace/workspace-planning/scripts/planning.py

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,34 @@ def find_schedule(path: str | None) -> Path:
4848

4949

5050
def load_schedule(path: Path) -> dict:
51-
"""Load and return the schedule YAML."""
52-
with open(path) as f:
53-
return yaml.safe_load(f)
54-
51+
"""Load schedule YAML, resolving module_files if present.
5552
56-
def save_schedule(path: Path, data: dict) -> None:
57-
"""Write schedule data back to YAML, preserving readability."""
58-
with open(path, "w") as f:
59-
yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
53+
When the main YAML contains a ``module_files`` list, modules are loaded
54+
from each referenced file (paths relative to the main file's directory).
55+
Each module gets a ``_source_file`` tag so callers know which file it
56+
came from.
57+
"""
58+
with open(path) as f:
59+
data = yaml.safe_load(f)
60+
61+
if "module_files" in data:
62+
all_modules: list[dict] = []
63+
for ref in data["module_files"]:
64+
ref_path = path.parent / ref
65+
if not ref_path.exists():
66+
print(
67+
f"Error: referenced module file '{ref}' not found at {ref_path}",
68+
file=sys.stderr,
69+
)
70+
sys.exit(1)
71+
with open(ref_path) as f:
72+
ref_data = yaml.safe_load(f)
73+
for m in ref_data.get("modules", []):
74+
m["_source_file"] = str(ref_path)
75+
all_modules.append(m)
76+
data["modules"] = all_modules
77+
78+
return data
6079

6180

6281
def find_module(data: dict, module_id: str) -> dict | None:
@@ -147,7 +166,7 @@ def cmd_review(args: argparse.Namespace) -> None:
147166

148167

149168
def cmd_update(args: argparse.Namespace) -> None:
150-
"""Update a module's status."""
169+
"""Validate a status transition and output what to change (does not write)."""
151170
path = find_schedule(args.file)
152171
data = load_schedule(path)
153172

@@ -172,16 +191,18 @@ def cmd_update(args: argparse.Namespace) -> None:
172191
)
173192
sys.exit(1)
174193

175-
module["status"] = target
176-
save_schedule(path, data)
177-
178-
result = {"module": args.module_id, "from": current, "to": target}
194+
result = {
195+
"module": args.module_id,
196+
"from": current,
197+
"to": target,
198+
"source_file": module.get("_source_file", str(path)),
199+
}
179200
json.dump(result, sys.stdout, ensure_ascii=False, indent=2)
180201
print()
181202

182203

183204
def cmd_link(args: argparse.Namespace) -> None:
184-
"""Link an OpenSpec change to a module."""
205+
"""Validate a change link and output what to change (does not write)."""
185206
path = find_schedule(args.file)
186207
data = load_schedule(path)
187208

@@ -201,25 +222,19 @@ def cmd_link(args: argparse.Namespace) -> None:
201222
changes = module.get("changes", [])
202223
if changes is None:
203224
changes = []
204-
if change_name in changes:
205-
print(f"Warning: '{change_name}' already linked to '{args.module_id}'", file=sys.stderr)
206-
else:
207-
changes.append(change_name)
208-
module["changes"] = changes
209-
210-
# Auto-transition planned → in_progress
211-
auto_transitioned = False
212-
if module["status"] == "planned":
213-
module["status"] = "in_progress"
214-
auto_transitioned = True
225+
already_linked = change_name in changes
226+
if not already_linked:
227+
changes = [*changes, change_name]
215228

216-
save_schedule(path, data)
229+
auto_transition = module["status"] == "planned"
217230

218231
result = {
219232
"module": args.module_id,
220233
"change": change_name,
221234
"changes": changes,
222-
"auto_transitioned": auto_transitioned,
235+
"already_linked": already_linked,
236+
"auto_transition": auto_transition,
237+
"source_file": module.get("_source_file", str(path)),
223238
}
224239
json.dump(result, sys.stdout, ensure_ascii=False, indent=2)
225240
print()

0 commit comments

Comments
 (0)