aw-spec-cli 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/assets/.appweaver/STACK-PROFILES.md +1 -1
  2. package/assets/.appweaver/orchestrator/orchestrator.py +5 -0
  3. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +10 -3
  4. package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +38 -11
  5. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +2 -1
  6. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +607 -123
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +625 -66
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement-ui.orch.yaml +407 -134
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +452 -143
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +78 -14
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan-ui.orch.yaml +597 -0
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +688 -0
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +3 -3
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +23 -23
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-story-plan.orch.yaml +945 -0
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-sync-stories.orch.yaml +1040 -0
  17. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +2069 -28
  18. package/assets/.appweaver/scripts/bash/aw-xref.sh +2 -1
  19. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +1973 -13
  20. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +2 -1
  21. package/assets/.appweaver/scripts/python/aw-xref.py +246 -20
  22. package/assets/.appweaver/templates/design-template.md +88 -54
  23. package/assets/.appweaver/templates/spec-template.md +12 -10
  24. package/assets/.appweaver/templates/story-template.md +48 -8
  25. package/assets/.appweaver/templates/ui-design-template.md +71 -39
  26. package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +19 -7
  27. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +35 -26
  28. package/package.json +1 -1
  29. package/assets/.appweaver/orchestrator/workflows/aw-spec-design-ui.orch.yaml +0 -386
  30. package/assets/.appweaver/orchestrator/workflows/aw-spec-design.orch.yaml +0 -382
@@ -0,0 +1,945 @@
1
+ # =============================================================================
2
+ # aw-spec-story-plan (orchestrator) — the per-story execution design, BOTH LANES
3
+ #
4
+ # Authors ONE story's ephemeral design as ONE DOCUMENT PER LANE:
5
+ # stories/<ID>/story-backend-plan.md the service design → /appweaver implement
6
+ # stories/<ID>/story-frontend-plan.md the UI design → /appweaver implement-ui
7
+ # Neither is a maintained design: each is designed FROM the current truth every
8
+ # time, left behind (archived with the story) at close, and never merged anywhere.
9
+ # The durable knowledge routes to append-only logs instead:
10
+ # • adr.md — each pass PROPOSES ADRs; they are ratified at story close
11
+ # • interfaces.md — the SERVICE pass appends published-surface changes here
12
+ # (status: proposed; story close verifies them against the
13
+ # as-built and flips them to live). The UI pass CONSUMES that
14
+ # log — it never publishes into it.
15
+ #
16
+ # ONE COMMAND, TWO DOCUMENTS. One command because everything before the authoring
17
+ # is shared and must not be answered twice: which story, whether its spec is
18
+ # ledgered, whether it has gone stale, and what code already exists. Two documents
19
+ # because the two designs are authored under different rule sets, from different
20
+ # code, and are read by different implement commands — folded into one file, every
21
+ # one of those becomes a section boundary the next reader has to trust. There is no
22
+ # /appweaver story-plan-ui, and there will not be one.
23
+ #
24
+ # LANE SCOPE IS WHAT IS ACTUALLY THERE. A lane is designed when it is DECLARED in
25
+ # stacks.json and has a USABLE feature plan. A declared lane whose plan is missing,
26
+ # or which the spec has moved past, is dropped from THIS RUN and said out loud with
27
+ # its route — it is not a reason to refuse the lane that is ready. The run stops
28
+ # only when NO lane survives, and then the message carries both lanes' reasons.
29
+ #
30
+ # INPUT CHANNELS, each with one job and no overlap:
31
+ # spec.md what MUST be (the contract — the only WHAT authority)
32
+ # backend-plan.md the FEATURE ARCHITECTURE the service slice sits inside
33
+ # frontend-plan.md the FEATURE ARCHITECTURE the UI slice sits inside
34
+ # — both binding, and both read ONE WAY: no story pass ever
35
+ # writes back, so each feature plan keeps exactly one writer
36
+ # (/appweaver plan · /appweaver plan-ui)
37
+ # OKF code coverage what IS — the feature's existing code SPLIT BY LANE, its
38
+ # drift, its dependencies and its blast radius, plus per-id
39
+ # code anchors where a source map exists (concept-coverage)
40
+ # constitution + rules what must ALWAYS hold (standards, via tech-select)
41
+ # adr.md what was DECIDED and why (binding unless superseded)
42
+ # interfaces.md what is PUBLISHED (superseded only by new proposed entries)
43
+ # The feature blueprint is deliberately NOT an input — it is a read-only render for
44
+ # humans; coherence flows through the logs and the code, never through it.
45
+ #
46
+ # THE DESIGN IS YOURS TO EDIT BEFORE YOU BUILD. Each document is a plain markdown
47
+ # file, and a hand edit to its latest pass is what implement then builds. Re-running
48
+ # story-plan does not overwrite that edit: every run APPENDS a
49
+ # "## Design pass N — <date> (spec vX)" section under a SCRIPT-written header, so
50
+ # edit to adjust and re-run to redesign. THE LATEST PASS WINS; earlier passes are
51
+ # the iteration history. Pass numbers are per lane, so designing one lane leaves the
52
+ # other's history untouched. Writing into stories/<ID>/ is what flips the story
53
+ # IN-FLIGHT (sync-stories' isdir test) and drives follow-on minting — the documents
54
+ # must live nowhere else.
55
+ #
56
+ # RULE SETS ARE LOADED PER LANE, IN LANE ORDER — the service rules
57
+ # (## /aw-spec-plan) before the service pass, the UI rules (## /aw-spec-plan-ui)
58
+ # before the UI pass. The engine's standards set is sticky and cumulative, so the UI
59
+ # pass also carries the service rules where both lanes run. That direction is the
60
+ # deliberate one: a UI author reading service layering rules is noise, while a
61
+ # service author reading component and styling rules is misdirection.
62
+ #
63
+ # GATES (all loud, all before any write): at least one lane declared in stacks.json;
64
+ # active feature + spec.md present; the spec-check ledger tripwire (no design from
65
+ # an unledgered spec); per-lane plan freshness (fresh/behind/edited_behind/untracked
66
+ # drop that lane and name the fix; a hand edit still in step is announced in the
67
+ # pass, not refused); the story exists and is OPEN (closed/cancelled/draft each get
68
+ # their own message). A stale story (ledger rows above its stamp naming ids it
69
+ # carries — exercises AND its Constraints section) is announced in the pass, not
70
+ # refused: a declined sync-stories rewrite is a legitimate author decision.
71
+ #
72
+ # Launch: /appweaver story-plan [<story-id>] [guidance] (asks when ambiguous)
73
+ # Prereq: /appweaver plan and/or /appweaver plan-ui (the feature plans this slices)
74
+ # + a story (specify/sync-stories mint them) · Next: implement · implement-ui
75
+ # =============================================================================
76
+
77
+ name: aw-spec-story-plan
78
+ description: "Author or extend ONE story's ephemeral execution design, one document per lane — stories/<ID>/story-backend-plan.md and stories/<ID>/story-frontend-plan.md. Each run APPENDS a dated design pass per lane in scope (script-numbered; the latest pass wins, and a manual edit to the latest pass is what implement builds) designed from the current spec, that lane's binding feature plan (read one way — a story pass never writes back), the feature's lane-split OKF code coverage, the technical standards, and the feature's append-only ADR + interface logs. A lane is designed when it is declared AND has a usable plan; a lane whose plan is missing or behind the spec is dropped from the run and said out loud, and the run stops only when no lane survives. The service pass appends published-surface changes to interfaces.md as proposed entries; the UI pass consumes that log. Refuses non-open stories; announces (never hides) a spec that moved above the story's stamp."
79
+ version: 2.0.0
80
+ agent_agnostic: true
81
+ dialect: aw-spec.orchestrator/v1
82
+
83
+ inputs:
84
+ user_input:
85
+ description: Optional story id and/or design guidance (e.g. "Payment-003 keep the dedupe table in-process").
86
+ required: false
87
+ bind: USER_INPUT
88
+
89
+ scripts:
90
+ actions:
91
+ sh: .appweaver/scripts/bash/aw-spec-actions.sh
92
+ ps1: .appweaver/scripts/powershell/aw-spec-actions.ps1
93
+ xref:
94
+ sh: .appweaver/scripts/bash/aw-xref.sh
95
+ ps1: .appweaver/scripts/powershell/aw-xref.ps1
96
+
97
+ # Sticky standards seed = the universal always-on rule only. The TECHNICAL rule
98
+ # files are CONFIGURATION, loaded per lane from tech-rule-bindings.md — the service
99
+ # pass under "## /aw-spec-plan" and the UI pass under "## /aw-spec-plan-ui". Each
100
+ # story design applies the SAME HOW rules as the feature plan it slices, so both
101
+ # reuse their lane's existing slot rather than minting a new one.
102
+ standards:
103
+ - { path: .appweaver/ai-rules.md }
104
+
105
+ vars:
106
+ # lane declaration + lane scope (declared AND usably planned)
107
+ lanes_declared: "false"
108
+ be_declared: "false"
109
+ fe_declared: "false"
110
+ be_in_scope: "false"
111
+ fe_in_scope: "false"
112
+ have_lane: "false"
113
+ have_design_lane: "false"
114
+ lane_gate_msg: "No lane is declared in .appweaver/stacks.json, so there is nothing to design this story against. Run /appweaver select-stack and declare the stack(s) plus their source paths, then re-run story-plan."
115
+ # feature + spec
116
+ feature_dir: ""
117
+ spec_file: ""
118
+ spec_exists: "false"
119
+ be_plan: ""
120
+ fe_plan: ""
121
+ spec_ok: "false"
122
+ spec_state: ""
123
+ # per-lane feature-plan freshness (the one-way coupling: a story pass READS a plan)
124
+ be_plan_state: ""
125
+ be_plan_derived: ""
126
+ be_plan_changed: ""
127
+ be_plan_note: ""
128
+ be_drop_msg: ""
129
+ be_dropped: "false"
130
+ drops_note: "none — every declared lane was designed this run"
131
+ fe_plan_state: ""
132
+ fe_plan_derived: ""
133
+ fe_plan_changed: ""
134
+ fe_plan_note: ""
135
+ fe_drop_msg: ""
136
+ fe_dropped: "false"
137
+ # story-plan-check bindings
138
+ any_open: "false"
139
+ open_ids: "[]"
140
+ open_ids_csv: ""
141
+ digest: ""
142
+ resolved: "false"
143
+ needs_pick: "false"
144
+ pick_reason: ""
145
+ story: ""
146
+ story_path: ""
147
+ story_open: "false"
148
+ status_msg: ""
149
+ story_title: ""
150
+ exercises_csv: ""
151
+ constraints_csv: ""
152
+ story_spec_version: ""
153
+ current_version: ""
154
+ stale_csv: ""
155
+ stale_note: ""
156
+ story_dir: ""
157
+ legacy_doc: ""
158
+ legacy_doc_exists: "false"
159
+ be_doc_path: ""
160
+ be_doc_exists: "false"
161
+ be_next_pass: "1"
162
+ be_pass_tmp: ""
163
+ fe_doc_path: ""
164
+ fe_doc_exists: "false"
165
+ fe_next_pass: "1"
166
+ fe_pass_tmp: ""
167
+ if_tmp: ""
168
+ adr_path: ""
169
+ if_path: ""
170
+ picked_story: ""
171
+ # OKF code coverage (the feature's current code, per lane)
172
+ okf_concept: ""
173
+ coverage_digest: ""
174
+ be_source_csv: ""
175
+ be_source_count: "0"
176
+ fe_source_csv: ""
177
+ fe_source_count: "0"
178
+ coverage_drift: "0"
179
+ used_by_csv: ""
180
+ unanchored_csv: ""
181
+ source_map: ""
182
+ # write results
183
+ be_pass_n: "0"
184
+ be_created: "false"
185
+ fe_pass_n: "0"
186
+ fe_created: "false"
187
+ if_appended: "0"
188
+ if_ids_csv: ""
189
+ if_unknown_csv: ""
190
+ if_badsup_csv: ""
191
+ if_ok: "true"
192
+ if_error: ""
193
+
194
+ workflow:
195
+
196
+ # 1. WHICH LANES EXIST — the DECLARATION in stacks.json, not a directory on disk
197
+ # (greenfield designs before the code exists). At least one is required; which
198
+ # ones are actually designed is settled by the plan gates in step 4.
199
+ - id: lane_resolve
200
+ action: execute_script
201
+ on_error: continue # never crash raw on an undeclared lane — the gate below explains it
202
+ script: { ref: actions }
203
+ subcommand: lane-resolve
204
+ outputs:
205
+ - { name: lanes_declared, from: "$.declared" }
206
+ - { name: be_declared, from: "$.backend_declared" }
207
+ - { name: fe_declared, from: "$.frontend_declared" }
208
+ - id: be_scope
209
+ condition: "{{be_declared}} == true"
210
+ action: set
211
+ vars: { be_in_scope: "true", have_lane: "true" }
212
+ - id: fe_scope
213
+ condition: "{{fe_declared}} == true"
214
+ action: set
215
+ vars: { fe_in_scope: "true", have_lane: "true" }
216
+ - id: require_lane
217
+ action: verify_context
218
+ require_true: have_lane
219
+ on_false: stop
220
+ on_false_message: "{{lane_gate_msg}}"
221
+
222
+ # 2. Active feature + its spec. feature-paths reports both plan paths whether or
223
+ # not they exist; step 4 decides what an absence means for that lane.
224
+ - id: feature_paths
225
+ action: execute_script
226
+ on_error: continue # never crash raw on 'no feature set' — the gate below explains it
227
+ script: { ref: actions }
228
+ subcommand: feature-paths
229
+ outputs:
230
+ - { name: feature_dir, from: "$.feature_dir" }
231
+ - { name: spec_file, from: "$.spec_file" }
232
+ - { name: spec_exists, from: "$.spec_exists" }
233
+ - { name: be_plan, from: "$.backend_plan_file" }
234
+ - { name: fe_plan, from: "$.frontend_plan_file" }
235
+ - id: require_feature
236
+ action: verify_context
237
+ require_non_empty: feature_dir
238
+ on_empty: stop
239
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run story-plan."
240
+ - id: verify_spec
241
+ action: verify_context
242
+ require_true: spec_exists
243
+ on_false: stop
244
+ on_false_message: "No spec.md for the current feature. Run /appweaver specify first — a story design designs against the contract, and there is none."
245
+
246
+ # 3. The spec's ledger tripwire: no design pass from an unledgered spec — every
247
+ # pass records the spec version it designed against, and an unledgered edit has
248
+ # no version to record.
249
+ - id: spec_guard_read
250
+ action: execute_script
251
+ script: { ref: actions }
252
+ subcommand: spec-check
253
+ args: ["--feature-dir", "{{feature_dir}}"]
254
+ outputs:
255
+ - { name: spec_ok, from: "$.ok" }
256
+ - { name: spec_state, from: "$.state" }
257
+ - id: spec_guard
258
+ action: verify_context
259
+ require_true: spec_ok
260
+ on_false: stop
261
+ on_false_message: "spec.md has moved outside the ledger (state: {{spec_state}}). Record what changed in a Change History row (fold it back through the spec workflow), re-run spec-stamp to re-baseline, then re-run story-plan — a design pass derived from an unledgered spec records a baseline the ledger cannot name."
262
+
263
+ # 4. THE FEATURE-PLAN GATES, one per declared lane. A story design is a SLICE of
264
+ # the feature's architecture, so it READS that lane's plan and NEVER writes
265
+ # back. The freshness check stops a slice inheriting an architecture the spec
266
+ # has already moved past while the script-written pass header stamps the CURRENT
267
+ # spec version over it — the pass would read as designed against a spec the plan
268
+ # never saw.
269
+ #
270
+ # A FAILING GATE DROPS ITS LANE, IT DOES NOT STOP THE RUN. Missing, behind,
271
+ # edited_behind and untracked all mean the same thing — this lane has no usable
272
+ # architecture right now — and none of them make the OTHER lane's design wrong.
273
+ # Each drop is announced with its own route, and the run stops only if both
274
+ # lanes are gone. `untracked` drops for the reason it always did: until one
275
+ # /appweaver plan run baselines the document this gate cannot tell clean from
276
+ # behind, and a gate that cannot tell is worse than one that asks. `edited` is
277
+ # admitted and announced INTO the pass — the plan is the architect's document.
278
+ - id: be_plan_read
279
+ condition: "{{be_in_scope}} == true"
280
+ action: execute_script
281
+ on_error: continue # a probe that fails binds nothing; the lane stays out of scope ⇒ fails CLOSED
282
+ script: { ref: actions }
283
+ subcommand: plan-check
284
+ args: ["--doc", "{{be_plan}}", "--feature-dir", "{{feature_dir}}"]
285
+ outputs:
286
+ - { name: be_plan_state, from: "$.state" }
287
+ - { name: be_plan_derived, from: "$.derived_version" }
288
+ - { name: be_plan_changed, from: "$.changed_csv" }
289
+ - id: fe_plan_read
290
+ condition: "{{fe_in_scope}} == true"
291
+ action: execute_script
292
+ on_error: continue # same fail-closed contract as the service probe
293
+ script: { ref: actions }
294
+ subcommand: plan-check
295
+ args: ["--doc", "{{fe_plan}}", "--feature-dir", "{{feature_dir}}"]
296
+ outputs:
297
+ - { name: fe_plan_state, from: "$.state" }
298
+ - { name: fe_plan_derived, from: "$.derived_version" }
299
+ - { name: fe_plan_changed, from: "$.changed_csv" }
300
+
301
+ # 4a. SERVICE lane verdict. Only `clean` and `edited` keep the lane; every other
302
+ # state (including the empty string a crashed probe leaves) drops it.
303
+ - id: be_drop_default
304
+ condition: "{{be_in_scope}} == true"
305
+ action: set
306
+ vars:
307
+ be_in_scope: "false"
308
+ be_dropped: "true"
309
+ be_drop_msg: "SERVICE lane not designed: backend-plan.md could not be checked against the spec (state: '{{be_plan_state}}'). Run /appweaver plan for this feature, then re-run story-plan."
310
+ - id: be_keep_clean
311
+ condition: "{{be_plan_state}} == clean"
312
+ action: set
313
+ vars: { be_in_scope: "true", be_dropped: "false", be_drop_msg: "" }
314
+ - id: be_keep_edited
315
+ condition: "{{be_plan_state}} == edited"
316
+ action: set
317
+ vars:
318
+ be_in_scope: "true"
319
+ be_dropped: "false"
320
+ be_drop_msg: ""
321
+ be_plan_note: "NOTE — backend-plan.md carries hand edits that /appweaver plan has not baselined. It is still in step with the spec, so it is authoritative here; design to it as it stands."
322
+ - id: be_msg_fresh
323
+ condition: "{{be_plan_state}} == fresh"
324
+ action: set
325
+ vars: { be_drop_msg: "SERVICE lane not designed: a backend lane is declared but this feature has no backend-plan.md, so there is no feature architecture to slice. Run /appweaver plan — it authors the plan from spec.md and publishes this feature's interfaces, both of which the service design pass reads — then re-run story-plan." }
326
+ - id: be_msg_behind
327
+ condition: "{{be_plan_state}} == behind"
328
+ action: set
329
+ vars: { be_drop_msg: "SERVICE lane not designed: backend-plan.md derives from spec {{be_plan_derived}} and the spec has moved past it (ids since: {{be_plan_changed}}). Designing a slice against it would inherit an architecture the spec has outrun. Run /appweaver plan to conform it, then re-run story-plan." }
330
+ - id: be_msg_edited_behind
331
+ condition: "{{be_plan_state}} == edited_behind"
332
+ action: set
333
+ vars: { be_drop_msg: "SERVICE lane not designed: backend-plan.md has been hand-edited AND the spec has moved past it (derived from {{be_plan_derived}}; ids since: {{be_plan_changed}}). Run /appweaver plan with accept_manual: true — it reconciles the edit and the spec change together — then re-run story-plan." }
334
+ - id: be_msg_untracked
335
+ condition: "{{be_plan_state}} == untracked"
336
+ action: set
337
+ vars: { be_drop_msg: "SERVICE lane not designed: backend-plan.md has no derivation record — a reversed or hand-placed plan that no baseline names, so nothing can say which spec version it matches and this gate cannot tell fresh from stale. Run /appweaver plan once: it reconciles the WHOLE plan to the current spec and baselines it. Then re-run story-plan." }
338
+
339
+ # 4b. UI lane verdict — the same contract, routed to the frontend lane's author.
340
+ - id: fe_drop_default
341
+ condition: "{{fe_in_scope}} == true"
342
+ action: set
343
+ vars:
344
+ fe_in_scope: "false"
345
+ fe_dropped: "true"
346
+ fe_drop_msg: "UI lane not designed: frontend-plan.md could not be checked against the spec (state: '{{fe_plan_state}}'). Run /appweaver plan-ui for this feature, then re-run story-plan."
347
+ - id: fe_keep_clean
348
+ condition: "{{fe_plan_state}} == clean"
349
+ action: set
350
+ vars: { fe_in_scope: "true", fe_dropped: "false", fe_drop_msg: "" }
351
+ - id: fe_keep_edited
352
+ condition: "{{fe_plan_state}} == edited"
353
+ action: set
354
+ vars:
355
+ fe_in_scope: "true"
356
+ fe_dropped: "false"
357
+ fe_drop_msg: ""
358
+ fe_plan_note: "NOTE — frontend-plan.md carries hand edits that /appweaver plan-ui has not baselined. It is still in step with the spec, so it is authoritative here; design to it as it stands."
359
+ - id: fe_msg_fresh
360
+ condition: "{{fe_plan_state}} == fresh"
361
+ action: set
362
+ vars: { fe_drop_msg: "UI lane not designed: a frontend lane is declared but this feature has no frontend-plan.md, so there is no UI architecture to slice. Run /appweaver plan-ui — it authors the plan from spec.md and the published interfaces, which the UI design pass reads — then re-run story-plan." }
363
+ - id: fe_msg_behind
364
+ condition: "{{fe_plan_state}} == behind"
365
+ action: set
366
+ vars: { fe_drop_msg: "UI lane not designed: frontend-plan.md derives from spec {{fe_plan_derived}} and the spec has moved past it (ids since: {{fe_plan_changed}}). Run /appweaver plan-ui to conform it, then re-run story-plan." }
367
+ - id: fe_msg_edited_behind
368
+ condition: "{{fe_plan_state}} == edited_behind"
369
+ action: set
370
+ vars: { fe_drop_msg: "UI lane not designed: frontend-plan.md has been hand-edited AND the spec has moved past it (derived from {{fe_plan_derived}}; ids since: {{fe_plan_changed}}). Run /appweaver plan-ui with accept_manual: true, then re-run story-plan." }
371
+ - id: fe_msg_untracked
372
+ condition: "{{fe_plan_state}} == untracked"
373
+ action: set
374
+ vars: { fe_drop_msg: "UI lane not designed: frontend-plan.md has no derivation record — a reversed or hand-placed plan that no baseline names, so this gate cannot tell fresh from stale. Run /appweaver plan-ui once to reconcile and baseline it, then re-run story-plan." }
375
+
376
+ # 4c. At least one lane must survive. When none does, BOTH reasons travel in the
377
+ # stop message — a run that reported only the first would send the author back
378
+ # for a second refusal.
379
+ - id: have_be
380
+ condition: "{{be_in_scope}} == true"
381
+ action: set
382
+ vars: { have_design_lane: "true" }
383
+ - id: have_fe
384
+ condition: "{{fe_in_scope}} == true"
385
+ action: set
386
+ vars: { have_design_lane: "true" }
387
+ - id: require_design_lane
388
+ action: verify_context
389
+ require_true: have_design_lane
390
+ on_false: stop
391
+ on_false_message: |
392
+ No lane has a usable feature plan, so there is nothing to design this story against.
393
+
394
+ {{be_drop_msg}}
395
+
396
+ {{fe_drop_msg}}
397
+
398
+ # 4d. What the run did NOT design, in one line for the outputs. Both branches
399
+ # compose the same expression: either message is empty when its lane is fine,
400
+ # so one set covers a drop on either lane or on both.
401
+ - id: drops_note_be
402
+ condition: "{{be_dropped}} == true"
403
+ action: set
404
+ vars: { drops_note: "{{be_drop_msg}} {{fe_drop_msg}}" }
405
+ - id: drops_note_fe
406
+ condition: "{{fe_dropped}} == true"
407
+ action: set
408
+ vars: { drops_note: "{{be_drop_msg}} {{fe_drop_msg}}" }
409
+
410
+ # 5. WHICH STORY. The check resolves an explicit id in the input; anything else
411
+ # (nothing named, an unknown id, several ids) becomes a pick from the open
412
+ # backlog. All state the gates below read comes back in one scan, including
413
+ # both lanes' document and pass state.
414
+ - id: story_scan
415
+ action: execute_script
416
+ script: { ref: actions }
417
+ subcommand: story-plan-check
418
+ args: ["--feature-dir", "{{feature_dir}}", "--input", "{{USER_INPUT}}"]
419
+ outputs:
420
+ - { name: any_open, from: "$.any_open" }
421
+ - { name: open_ids, from: "$.open_ids" }
422
+ - { name: open_ids_csv, from: "$.open_ids_csv" }
423
+ - { name: digest, from: "$.digest" }
424
+ - { name: resolved, from: "$.resolved" }
425
+ - { name: needs_pick, from: "$.needs_pick" }
426
+ - { name: pick_reason, from: "$.pick_reason" }
427
+ - { name: story, from: "$.story" }
428
+ - { name: story_path, from: "$.story_path" }
429
+ - { name: story_open, from: "$.story_open" }
430
+ - { name: status_msg, from: "$.status_msg" }
431
+ - { name: story_title, from: "$.story_title" }
432
+ - { name: exercises_csv, from: "$.exercises_csv" }
433
+ - { name: constraints_csv, from: "$.constraints_csv" }
434
+ - { name: story_spec_version, from: "$.story_spec_version" }
435
+ - { name: current_version, from: "$.current_version" }
436
+ - { name: stale_csv, from: "$.stale_csv" }
437
+ - { name: stale_note, from: "$.stale_note" }
438
+ - { name: story_dir, from: "$.story_dir" }
439
+ - { name: legacy_doc, from: "$.legacy_doc" }
440
+ - { name: legacy_doc_exists, from: "$.legacy_doc_exists" }
441
+ - { name: be_doc_path, from: "$.be_doc_path" }
442
+ - { name: be_doc_exists, from: "$.be_doc_exists" }
443
+ - { name: be_next_pass, from: "$.be_next_pass" }
444
+ - { name: be_pass_tmp, from: "$.be_pass_tmp" }
445
+ - { name: fe_doc_path, from: "$.fe_doc_path" }
446
+ - { name: fe_doc_exists, from: "$.fe_doc_exists" }
447
+ - { name: fe_next_pass, from: "$.fe_next_pass" }
448
+ - { name: fe_pass_tmp, from: "$.fe_pass_tmp" }
449
+ - { name: if_tmp, from: "$.if_tmp" }
450
+ - { name: adr_path, from: "$.adr_path" }
451
+ - { name: if_path, from: "$.if_path" }
452
+ - id: require_open_backlog
453
+ action: verify_context
454
+ require_true: any_open
455
+ on_false: stop
456
+ on_false_message: "No open stories for this feature — there is nothing to design. Stories are minted by /appweaver specify (bootstrap) and /appweaver sync-stories (when the spec moves)."
457
+ - id: ask_story
458
+ condition: "{{needs_pick}} == true"
459
+ action: ask
460
+ prompt: |
461
+ Which story is this design pass for? ({{pick_reason}})
462
+
463
+ {{digest}}
464
+ options_from: "{{open_ids}}"
465
+ answer_key: picked_story
466
+ answer_var: picked_story
467
+ - id: story_pick_check
468
+ condition: "{{needs_pick}} == true"
469
+ action: execute_script
470
+ script: { ref: actions }
471
+ subcommand: story-plan-check
472
+ args: ["--feature-dir", "{{feature_dir}}", "--story", "{{picked_story}}"]
473
+ outputs:
474
+ - { name: resolved, from: "$.resolved" }
475
+ - { name: story, from: "$.story" }
476
+ - { name: story_path, from: "$.story_path" }
477
+ - { name: story_open, from: "$.story_open" }
478
+ - { name: status_msg, from: "$.status_msg" }
479
+ - { name: story_title, from: "$.story_title" }
480
+ - { name: exercises_csv, from: "$.exercises_csv" }
481
+ - { name: constraints_csv, from: "$.constraints_csv" }
482
+ - { name: story_spec_version, from: "$.story_spec_version" }
483
+ - { name: stale_csv, from: "$.stale_csv" }
484
+ - { name: stale_note, from: "$.stale_note" }
485
+ - { name: story_dir, from: "$.story_dir" }
486
+ - { name: legacy_doc, from: "$.legacy_doc" }
487
+ - { name: legacy_doc_exists, from: "$.legacy_doc_exists" }
488
+ - { name: be_doc_path, from: "$.be_doc_path" }
489
+ - { name: be_doc_exists, from: "$.be_doc_exists" }
490
+ - { name: be_next_pass, from: "$.be_next_pass" }
491
+ - { name: be_pass_tmp, from: "$.be_pass_tmp" }
492
+ - { name: fe_doc_path, from: "$.fe_doc_path" }
493
+ - { name: fe_doc_exists, from: "$.fe_doc_exists" }
494
+ - { name: fe_next_pass, from: "$.fe_next_pass" }
495
+ - { name: fe_pass_tmp, from: "$.fe_pass_tmp" }
496
+ - { name: if_tmp, from: "$.if_tmp" }
497
+ - id: require_story
498
+ action: verify_context
499
+ require_true: resolved
500
+ on_false: stop
501
+ on_false_message: |
502
+ Could not resolve a story from '{{picked_story}}'. Open stories:
503
+ {{digest}}
504
+ Re-run story-plan naming one of them.
505
+ - id: require_open_story
506
+ action: verify_context
507
+ require_true: story_open
508
+ on_false: stop
509
+ on_false_message: "{{status_msg}}"
510
+
511
+ # 5a. A dropped lane is announced HERE — after the story is known, so the notice
512
+ # names the story it applies to, and before any write, so the author can stop
513
+ # and fix the plan rather than discovering the gap after a pass has landed.
514
+ - id: be_drop_notice
515
+ condition: "{{be_dropped}} == true"
516
+ action: verify_context
517
+ require_value: be_dropped
518
+ equals: "false"
519
+ on_mismatch: continue
520
+ on_mismatch_message: "{{be_drop_msg}}"
521
+ - id: fe_drop_notice
522
+ condition: "{{fe_dropped}} == true"
523
+ action: verify_context
524
+ require_value: fe_dropped
525
+ equals: "false"
526
+ on_mismatch: continue
527
+ on_mismatch_message: "{{fe_drop_msg}}"
528
+
529
+ # 5b. A pre-split story-plan.md beside the story is REPORTED, never read and never
530
+ # written. Silently leaving it there would leave a design document nobody
531
+ # consumes sitting beside two that are read — and the next person to open the
532
+ # story directory has no way to tell which of the three implement builds from.
533
+ - id: legacy_doc_notice
534
+ condition: "{{legacy_doc_exists}} == true"
535
+ action: verify_context
536
+ require_value: legacy_doc_exists
537
+ equals: "false"
538
+ on_mismatch: continue
539
+ on_mismatch_message: "{{legacy_doc}} is a story design from before the lane split. Nothing reads it any more — the service design now lives in story-backend-plan.md and the UI design in story-frontend-plan.md. Fold anything still worth keeping into this run's pass and delete it, or leave it as history; it will not reach implement either way."
540
+
541
+ # 6. THE FEATURE'S CODE, from its OKF concept — what IS, split by lane, with the
542
+ # drift, the dependencies and the blast radius a bare path list drops. Advisory:
543
+ # a greenfield feature has no concept yet and designs from an empty current
544
+ # state, so a missing bundle must never block the design. --ids reports which of
545
+ # the story's requirements already have a code anchor where the feature carries
546
+ # a source map; where it does not, every id comes back unanchored and the digest
547
+ # says so rather than implying the code is absent.
548
+ - id: coverage
549
+ action: execute_script
550
+ on_error: continue
551
+ script: { ref: xref }
552
+ subcommand: concept-coverage
553
+ args: ["--feature-dir", "{{feature_dir}}", "--ids", "{{exercises_csv}}"]
554
+ outputs:
555
+ - { name: okf_concept, from: "$.concept" }
556
+ - { name: coverage_digest, from: "$.digest" }
557
+ - { name: be_source_csv, from: "$.backend_csv" }
558
+ - { name: be_source_count, from: "$.backend_count" }
559
+ - { name: fe_source_csv, from: "$.frontend_csv" }
560
+ - { name: fe_source_count, from: "$.frontend_count" }
561
+ - { name: coverage_drift, from: "$.missing_count" }
562
+ - { name: used_by_csv, from: "$.used_by_csv" }
563
+ - { name: unanchored_csv, from: "$.unanchored_csv" }
564
+ - { name: source_map, from: "$.source_map" }
565
+ - id: drift_notice
566
+ condition: "{{coverage_drift}} != 0"
567
+ action: verify_context
568
+ require_value: coverage_drift
569
+ equals: "0"
570
+ on_mismatch: continue
571
+ on_mismatch_message: "The OKF concept for this feature declares {{coverage_drift}} source file(s) that are not on disk — a file moved or deleted without the owning slice being re-run. The design pass reads only what exists, so the index, not the design, is what is wrong. The drifted paths are named in the coverage block; /appweaver analyze-slice or the next implement on that slice re-writes the list."
572
+
573
+ # 7. Pre-execution hooks.
574
+ - id: resolve_pre_hooks
575
+ action: execute_script
576
+ script: { ref: actions }
577
+ subcommand: resolve-hooks
578
+ args: ["--phase", "before_story_plan"]
579
+ outputs:
580
+ - { name: before_hooks, from: "$.hooks" }
581
+ - id: dispatch_pre_hooks
582
+ action: dispatch_hooks
583
+ hooks_from_variable: before_hooks
584
+
585
+ # ════════════════════ THE SERVICE DESIGN (payload-only + Read) ═══════════════
586
+ # Lane rules first, then the pass. The doc itself is in context on a re-run
587
+ # (optional — absent before pass 1), so a later pass SEES earlier passes and any
588
+ # manual edits instead of contradicting them blind. Code arrives as a PATH LIST;
589
+ # the step declares Read to open it.
590
+ - id: tech_select_be
591
+ condition: "{{be_in_scope}} == true"
592
+ action: include
593
+ module: .appweaver/orchestrator/modules/tech-select.orch.yaml
594
+ with: { command: aw-spec-plan }
595
+
596
+ - id: ctx_be
597
+ condition: "{{be_in_scope}} == true"
598
+ action: build_context
599
+ vars:
600
+ story_id: "{{story}}"
601
+ story_title: "{{story_title}}"
602
+ exercises_csv: "{{exercises_csv}}"
603
+ constraints_csv: "{{constraints_csv}}"
604
+ spec_current_version: "{{current_version}}"
605
+ stale_note: "{{stale_note}}"
606
+ pass_number: "{{be_next_pass}}"
607
+ coverage_digest: "{{coverage_digest}}"
608
+ source_files_csv: "{{be_source_csv}}"
609
+ source_file_count: "{{be_source_count}}"
610
+ feature_plan_note: "{{be_plan_note}}"
611
+ ui_lane_note: "{{fe_drop_msg}}"
612
+ guidance: "{{USER_INPUT}}"
613
+ context:
614
+ - { path: "{{story_path}}" } # the slice: exercises, Constraints, Acceptance
615
+ - { path: "{{spec_file}}" } # the contract — the only WHAT authority
616
+ - { path: "{{feature_dir}}/backend-plan.md" } # the feature architecture this slice sits inside (READ-ONLY)
617
+ - { path: "{{be_doc_path}}", optional: true } # earlier service passes + manual edits (pass 2+)
618
+ - { path: "{{adr_path}}", optional: true } # ratified decisions — binding unless superseded
619
+ - { path: "{{if_path}}", optional: true } # the published surface (latest entry per id wins)
620
+ - { path: "{{okf_concept}}", optional: true } # the feature's OKF concept (use-case log, source map)
621
+ - { path: .appweaver/memory/constitution.technical.md, optional: true }
622
+ - id: gen_be
623
+ condition: "{{be_in_scope}} == true"
624
+ action: agent_author
625
+ tools: Read # the current code arrives as {{source_files_csv}} paths, not inlined
626
+ output_target: { type: file, path: "{{be_pass_tmp}}" }
627
+ task: |
628
+ Write SERVICE design pass {{pass_number}} for story {{story_id}} — {{story_title}}.
629
+ Output the PASS BODY ONLY: no "## Design pass" header (the script writes and
630
+ numbers it), no version numbers, no dates. Guidance: {{guidance}}
631
+
632
+ SCOPE — the story's ids and NOTHING else: it delivers {{exercises_csv}} and
633
+ must satisfy {{constraints_csv}}. Resolve every id's wording from spec.md in
634
+ context. Design work serving any other requirement is overreach — if touching
635
+ shared structure is genuinely unavoidable, name it under "### Risks & out of
636
+ scope" with the reason; do not design it here.
637
+
638
+ THIS IS THE SERVICE LANE ONLY. Screens, components, client state and styling
639
+ are the UI design's subject and are written to a separate document. Where the
640
+ slice has a user-facing surface, design the CONTRACT that serves it — the
641
+ endpoint or event and its shape — and stop there.
642
+ {{ui_lane_note}}
643
+
644
+ WHAT ALREADY EXISTS — the feature's code coverage, read from its OKF concept.
645
+ The BACKEND list is yours; the FRONTEND list is there so you do not design a
646
+ contract that ignores the client that already calls one.
647
+
648
+ {{coverage_digest}}
649
+
650
+ AUTHORITY, per input channel — on a conflict, stop at the defect, never pick
651
+ silently:
652
+ - spec.md is the ONLY authority for WHAT. A contradiction between spec and
653
+ story wording is a defect: flag it in the pass and design to the spec.
654
+ - The CURRENT CODE is the authority for what exists. Read the service files
655
+ listed here ({{source_file_count}} on disk); an empty list means greenfield —
656
+ say so in "### Current state" and design from a clean slate.
657
+ {{source_files_csv}}
658
+ - backend-plan.md is the FEATURE ARCHITECTURE this slice sits inside, and it
659
+ is BINDING: design within its layering, its ports and use-cases (§2), its
660
+ published surface (§3) and its cross-cutting decisions (§5). This pass reads
661
+ it ONE WAY — it NEVER writes back, and nothing here merges into it. Where the
662
+ slice genuinely cannot fit the plan, say so under "### Risks & out of scope"
663
+ and stop at the defect; the route is to change the spec and re-run
664
+ /appweaver plan, never to design around the plan here.
665
+ {{feature_plan_note}}
666
+ - The constitution and loaded technical rules are STANDARDS — non-negotiable.
667
+ - adr.md entries are DECIDED — follow them, or propose a superseding ADR under
668
+ "### Proposed ADRs" with the trade-off; never silently deviate.
669
+ - interfaces.md is the PUBLISHED surface — the latest entry per id is the
670
+ contract. Changing one means a superseding proposed entry, described under
671
+ "### Interfaces".
672
+ {{stale_note}}
673
+
674
+ STRUCTURE — exactly these subsections, in this order:
675
+ ### Scope — the ids and the story's acceptance, one line each, resolved
676
+ from the spec (cite ids; never restate spec wording at length)
677
+ ### Current state — what the service code has today that this slice touches
678
+ (file-level, from the coverage above; "greenfield — no code
679
+ yet" when the backend list is empty)
680
+ ### Approach — the implementation strategy and WHY it fits the standards
681
+ and ADRs; concrete, this is the bridge to real code
682
+ ### Changes — file-by-file: each file to create or modify and what
683
+ changes in it (signatures, wiring, migrations)
684
+ ### Interfaces — published-surface changes in prose (endpoint/event, shape,
685
+ what it serves, what it supersedes) — or "None."
686
+ ### Tests — what proves the acceptance criteria, named per criterion
687
+ ### Proposed ADRs — ONLY a deliberate choice among rule-compliant alternatives,
688
+ a rejected alternative with the reason, or a trade-off with
689
+ its revisit trigger. Never restate what code shows or a rule
690
+ mandates. "None." when there is nothing durable to record.
691
+ ### Risks & out of scope — divergences flagged above, unavoidable shared-structure
692
+ touches, what this slice deliberately leaves out
693
+
694
+ This pass is EPHEMERAL scaffolding — it is archived with the story, never
695
+ merged anywhere. Be concrete and name real paths, classes and signatures; a
696
+ later pass supersedes this one wholesale, so write nothing here that must
697
+ outlive the story (that is what the ADR and interface logs are for). Do not
698
+ write the story id into the body — the document's directory is its identity.
699
+
700
+ # 8. APPEND — story-plan-doc is the only writer of either lane's design.
701
+ # Script-owned header ("## Design pass N — <date> (spec vX)"), frontmatter
702
+ # stamps, temp file removed. Creating the first document in stories/<ID>/ is
703
+ # what flips the story in-flight.
704
+ - id: append_be
705
+ condition: "{{be_in_scope}} == true"
706
+ action: execute_script
707
+ script: { ref: actions }
708
+ subcommand: story-plan-doc
709
+ args: ["--feature-dir", "{{feature_dir}}", "--story", "{{story}}", "--lane", "backend", "--pass-file", "{{be_pass_tmp}}", "--spec-version", "{{current_version}}"]
710
+ outputs:
711
+ - { name: be_pass_n, from: "$.pass" }
712
+ - { name: be_created, from: "$.created" }
713
+
714
+ # ═══════════════ THE INTERFACE ENTRIES (payload-only, from the doc) ══════════
715
+ # Formalise the SERVICE pass's "### Interfaces" into append-only log entries.
716
+ # Separate step so the script stays the only id authority: entries arrive as
717
+ # IF-NEW blocks and log-append assigns IF-nnn, the date and status: proposed.
718
+ # The UI lane never runs this — it consumes the log, it does not publish into it.
719
+ - id: ctx_if
720
+ condition: "{{be_in_scope}} == true"
721
+ action: build_context
722
+ vars:
723
+ pass_number: "{{be_pass_n}}"
724
+ exercises_csv: "{{exercises_csv}}"
725
+ context:
726
+ - { path: "{{be_doc_path}}" } # the service design, latest pass included
727
+ - { path: "{{if_path}}", optional: true } # existing entries — what a change supersedes
728
+ - id: gen_if
729
+ condition: "{{be_in_scope}} == true"
730
+ action: agent_author
731
+ output_target: { type: file, path: "{{if_tmp}}" }
732
+ task: |
733
+ From "## Design pass {{pass_number}}" (the LAST pass) of the service story
734
+ design in context, extract its "### Interfaces" section into formal
735
+ interface-log entries.
736
+
737
+ THE FEATURE PLAN PUBLISHED THE SURFACE ALREADY. /appweaver plan writes this
738
+ feature's endpoints, events and external contracts into the log when it
739
+ authors or conforms backend-plan.md, so most passes have nothing to add here.
740
+ Output an entry ONLY where this slice CHANGES the published surface — a new
741
+ interface the log does not carry, or one it carries that this pass designs
742
+ differently (a SUPERSEDING entry naming the id it replaces). An interface the
743
+ log already carries unchanged is NOT re-published: a second entry saying the
744
+ same thing splits one contract in two.
745
+
746
+ If that section says None, describes no published-surface change (an endpoint,
747
+ event, or contract consumed outside this service), or describes only what the
748
+ log already carries — output exactly:
749
+ none
750
+
751
+ Otherwise output ONE block per changed interface, nothing else:
752
+
753
+ ## IF-NEW — <METHOD /route, or the event name>
754
+
755
+ serves: <the FR-/BR- ids from {{exercises_csv}} this interface exists for>
756
+ supersedes: <the IF-nnn id it replaces from the interface log in context, or none>
757
+
758
+ <a concise request/response or payload sketch — shapes, not implementations>
759
+
760
+ Every block starts with "## IF-NEW — " exactly: the script assigns real ids,
761
+ the date and status: proposed. Entries are proposals against the as-built
762
+ check at story close — describe the surface the pass actually designed, and
763
+ invent nothing the pass does not contain.
764
+ - id: append_if
765
+ condition: "{{be_in_scope}} == true"
766
+ action: execute_script
767
+ on_error: continue # a malformed extraction is refused LOUDLY but must not burn
768
+ # the pass that already landed: the temp file survives for a
769
+ # hand-run of log-append; if_error carries the refusal below
770
+ script: { ref: actions }
771
+ subcommand: log-append
772
+ args: ["--feature-dir", "{{feature_dir}}", "--log", "interfaces", "--entries-file", "{{if_tmp}}"]
773
+ outputs:
774
+ - { name: if_ok, from: "$.ok" }
775
+ - { name: if_error, from: "$.error" }
776
+ - { name: if_appended, from: "$.appended" }
777
+ - { name: if_ids_csv, from: "$.ids_csv" }
778
+ - { name: if_unknown_csv, from: "$.unknown_ids_csv" }
779
+ - { name: if_badsup_csv, from: "$.bad_supersedes_csv" }
780
+
781
+ # ══════════════════════ THE UI DESIGN (payload-only + Read) ══════════════════
782
+ # The UI lane's rules load here, AFTER the service pass has been written, so a
783
+ # service author is never handed component and styling rules. The pass reads the
784
+ # service story design of the SAME story where there is one: that document, not a
785
+ # guess, is what this slice's screens bind to.
786
+ - id: tech_select_fe
787
+ condition: "{{fe_in_scope}} == true"
788
+ action: include
789
+ module: .appweaver/orchestrator/modules/tech-select.orch.yaml
790
+ with: { command: aw-spec-plan-ui }
791
+
792
+ - id: ctx_fe
793
+ condition: "{{fe_in_scope}} == true"
794
+ action: build_context
795
+ vars:
796
+ story_id: "{{story}}"
797
+ story_title: "{{story_title}}"
798
+ exercises_csv: "{{exercises_csv}}"
799
+ constraints_csv: "{{constraints_csv}}"
800
+ spec_current_version: "{{current_version}}"
801
+ stale_note: "{{stale_note}}"
802
+ pass_number: "{{fe_next_pass}}"
803
+ coverage_digest: "{{coverage_digest}}"
804
+ ui_source_files_csv: "{{fe_source_csv}}"
805
+ ui_source_file_count: "{{fe_source_count}}"
806
+ feature_plan_note: "{{fe_plan_note}}"
807
+ service_lane_note: "{{be_drop_msg}}"
808
+ guidance: "{{USER_INPUT}}"
809
+ context:
810
+ - { path: "{{story_path}}" } # the slice: exercises, Constraints, Acceptance
811
+ - { path: "{{spec_file}}" } # the contract — the only WHAT authority
812
+ - { path: "{{feature_dir}}/frontend-plan.md" } # the UI architecture this slice sits inside (READ-ONLY)
813
+ - { path: "{{fe_doc_path}}", optional: true } # earlier UI passes + manual edits (pass 2+)
814
+ - { path: "{{be_doc_path}}", optional: true } # this story's service design — what these screens bind to
815
+ - { path: "{{adr_path}}", optional: true } # ratified decisions — binding unless superseded
816
+ - { path: "{{if_path}}", optional: true } # the published surface this slice CONSUMES
817
+ - { path: "{{okf_concept}}", optional: true } # the feature's OKF concept (use-case log, source map)
818
+ - { path: .appweaver/memory/constitution.ui.md, optional: true }
819
+ - id: gen_fe
820
+ condition: "{{fe_in_scope}} == true"
821
+ action: agent_author
822
+ tools: Read # the current UI code arrives as {{ui_source_files_csv}} paths, not inlined
823
+ output_target: { type: file, path: "{{fe_pass_tmp}}" }
824
+ task: |
825
+ Write UI design pass {{pass_number}} for story {{story_id}} — {{story_title}}.
826
+ Output the PASS BODY ONLY: no "## Design pass" header (the script writes and
827
+ numbers it), no version numbers, no dates. Guidance: {{guidance}}
828
+
829
+ SCOPE — the story's ids and NOTHING else: it delivers {{exercises_csv}} and
830
+ must satisfy {{constraints_csv}}, of which the UX- constraints are yours to
831
+ realise. Resolve every id's wording from spec.md in context. Design work
832
+ serving any other requirement is overreach — if touching a shared shell,
833
+ layout or design-system component is genuinely unavoidable, name it under
834
+ "### Risks & out of scope" with the reason; do not design it here.
835
+
836
+ THIS IS THE UI LANE ONLY. Persistence, domain rules, service layering and
837
+ server-side validation belong to the service design and are written to a
838
+ separate document. Where this slice needs behaviour from the service, bind to
839
+ the PUBLISHED CONTRACT and stop there.
840
+ {{service_lane_note}}
841
+
842
+ WHAT ALREADY EXISTS — the feature's code coverage, read from its OKF concept.
843
+ The FRONTEND list is yours; the BACKEND list is there so you bind to code that
844
+ exists rather than to an endpoint nobody wrote.
845
+
846
+ {{coverage_digest}}
847
+
848
+ AUTHORITY, per input channel — on a conflict, stop at the defect, never pick
849
+ silently:
850
+ - spec.md is the ONLY authority for WHAT. A contradiction between spec and
851
+ story wording is a defect: flag it in the pass and design to the spec.
852
+ - The CURRENT CODE is the authority for what exists. Read the UI files listed
853
+ here ({{ui_source_file_count}} on disk); an empty list means this slice has
854
+ no UI yet — say so in "### Current state" and design from a clean slate.
855
+ {{ui_source_files_csv}}
856
+ - frontend-plan.md is the FEATURE'S UI ARCHITECTURE this slice sits inside, and
857
+ it is BINDING: design within its navigation and screen structure (§1), its
858
+ client-state placement (§2), its component composition (§3) and its
859
+ cross-cutting decisions (§6). This pass reads it ONE WAY — it NEVER writes
860
+ back, and nothing here merges into it. Where the slice genuinely cannot fit
861
+ the plan, say so under "### Risks & out of scope" and stop at the defect; the
862
+ route is to change the spec and re-run /appweaver plan-ui, never to design
863
+ around the plan here.
864
+ {{feature_plan_note}}
865
+ - The constitution and loaded technical rules are STANDARDS — non-negotiable.
866
+ - adr.md entries are DECIDED — follow them, or propose a superseding ADR under
867
+ "### Proposed ADRs" with the trade-off; never silently deviate.
868
+ - interfaces.md is the PUBLISHED surface and this lane CONSUMES it: bind to the
869
+ latest entry per id and INVENT NOTHING. An interface this slice needs that
870
+ the log does not carry is a DEFECT, not a gap for you to fill — name it under
871
+ "### Risks & out of scope" with the shape you needed; the route is
872
+ /appweaver story-plan on the service lane, which publishes it. Where this
873
+ story's service design is in context, it is the fresher word on what this
874
+ slice's surface will be — bind to it and say which entry it supersedes.
875
+ {{stale_note}}
876
+
877
+ STRUCTURE — exactly these subsections, in this order:
878
+ ### Scope — the ids and the story's acceptance, one line each,
879
+ resolved from the spec (cite ids; never restate spec
880
+ wording at length)
881
+ ### Current state — what the UI code has today that this slice touches
882
+ (file-level, from the coverage above; "no UI for this
883
+ slice yet" when the frontend list is empty)
884
+ ### Approach — routes and screens touched, where the state for this
885
+ slice lives and why that placement fits the plan, and
886
+ how the slice fits the existing composition
887
+ ### Screens & components — file-by-file: each component, route, store or style
888
+ file to create or modify and what changes in it (props,
889
+ state, events)
890
+ ### Contract binding — every published interface this slice calls, BY ITS
891
+ IF-nnn id, with the request it sends and the response
892
+ it renders; and for each one what the user sees while
893
+ it is in flight, when it is empty, and when it fails
894
+ ### Tests — what proves the acceptance criteria, named per
895
+ criterion (component, interaction and accessibility)
896
+ ### Proposed ADRs — ONLY a deliberate choice among rule-compliant
897
+ alternatives, a rejected alternative with the reason, or
898
+ a trade-off with its revisit trigger. Never restate what
899
+ code shows or a rule mandates. "None." when there is
900
+ nothing durable to record.
901
+ ### Risks & out of scope — divergences flagged above, contracts this slice needs
902
+ that are not published, unavoidable shared-shell
903
+ touches, what this slice deliberately leaves out
904
+
905
+ This pass is EPHEMERAL scaffolding — it is archived with the story, never
906
+ merged anywhere. Be concrete and name real paths, components and props; a later
907
+ pass supersedes this one wholesale, so write nothing here that must outlive the
908
+ story (that is what the ADR and interface logs are for). Do not write the story
909
+ id into the body — the document's directory is its identity.
910
+
911
+ - id: append_fe
912
+ condition: "{{fe_in_scope}} == true"
913
+ action: execute_script
914
+ script: { ref: actions }
915
+ subcommand: story-plan-doc
916
+ args: ["--feature-dir", "{{feature_dir}}", "--story", "{{story}}", "--lane", "frontend", "--pass-file", "{{fe_pass_tmp}}", "--spec-version", "{{current_version}}"]
917
+ outputs:
918
+ - { name: fe_pass_n, from: "$.pass" }
919
+ - { name: fe_created, from: "$.created" }
920
+
921
+ # 9. Post-execution hooks.
922
+ - id: resolve_post_hooks
923
+ action: execute_script
924
+ script: { ref: actions }
925
+ subcommand: resolve-hooks
926
+ args: ["--phase", "after_story_plan"]
927
+ outputs:
928
+ - { name: after_hooks, from: "$.hooks" }
929
+ - id: dispatch_post_hooks
930
+ action: dispatch_hooks
931
+ hooks_from_variable: after_hooks
932
+
933
+ outputs:
934
+ story: "{{story}}"
935
+ service_design: "{{be_doc_path}} (in scope: {{be_in_scope}}, pass {{be_pass_n}})"
936
+ ui_design: "{{fe_doc_path}} (in scope: {{fe_in_scope}}, pass {{fe_pass_n}})"
937
+ lanes_not_designed: "{{drops_note}}"
938
+ designed_against_spec: "{{current_version}}"
939
+ designed_within_plans: "backend-plan.md ({{be_plan_state}}, derived from spec {{be_plan_derived}}) · frontend-plan.md ({{fe_plan_state}}, derived from spec {{fe_plan_derived}})"
940
+ code_coverage: "OKF concept {{okf_concept}} — service {{be_source_count}} file(s), UI {{fe_source_count}} file(s), {{coverage_drift}} declared-but-missing; downstream of this aggregate: [{{used_by_csv}}]"
941
+ requirements_without_code_anchor: "{{unanchored_csv}} — anchors read from [{{source_map}}]; an EMPTY bracket means this feature carries no requirement-to-code index, so the ids above are unproven either way, not uncovered"
942
+ stale_ids: "{{stale_csv}}"
943
+ interfaces_appended: "{{if_ids_csv}}"
944
+ interface_warnings: "serves ids not in spec: [{{if_unknown_csv}}] · supersedes targets not in log: [{{if_badsup_csv}}] · append refusal: [{{if_error}}] (empty brackets = clean; a refusal keeps the temp entries file beside the doc — fix it and run the log-append action by hand rather than burning a new pass)"
945
+ next: "THE LATEST PASS WINS, per lane — read '## Design pass {{be_pass_n}}' in the service design and '## Design pass {{fe_pass_n}}' in the UI design; earlier passes are history and manual edits there survive. EDIT EITHER DOCUMENT BEFORE YOU BUILD: implement reads the latest pass as it stands, so a hand correction is honoured; re-run story-plan instead to redesign. Interface entries appended as status:proposed ({{if_ids_csv}}) — story close verifies them against the as-built and flips them live. Proposed ADRs (if any) sit in the passes for ratification into adr.md at story close. stale_ids non-empty means the spec moved above this story's stamp — consider /appweaver sync-stories. Then: /appweaver implement (service) · /appweaver implement-ui (UI)."