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,1040 @@
1
+ # =============================================================================
2
+ # aw-spec-sync-stories (orchestrator) — raise and refresh the BACKLOG from the spec
3
+ #
4
+ # Spec-anchored pipeline, spec phase. The spec is the CONTRACT and a story is an
5
+ # execution SLICE over it. The two directions between them each have their own
6
+ # workflow and neither is shared: sync-stories carries the SPEC forward into the
7
+ # backlog; /appweaver update-spec folds a hand-edited STORY back into the contract.
8
+ # Keeping them apart is what stops the spec->story rule having two implementations.
9
+ #
10
+ # WHAT THIS READS. The spec's `## Change History` — the version ledger. Each row names
11
+ # the requirement ids that version touched; each story records the version it last
12
+ # synced at. "What has moved since the backlog was last in step" is therefore a read of
13
+ # rows and one integer comparison, with no diffing, no hashing and no model involved.
14
+ #
15
+ # THE WORK SET is the union of the ids named by every row ABOVE THE LOWEST spec_version
16
+ # across the OPEN stories — not the newest row alone. Those are the same set whenever the
17
+ # backlog is one version behind, which is the ordinary case; they diverge only when a sync
18
+ # was skipped, and that is the case worth getting right. Two specify runs with no sync
19
+ # between them leave the middle version named by no row anyone reads, and its requirements
20
+ # would get no story with nothing to say so. Silent, in the one mechanism built to make
21
+ # staleness loud. `ledger-read` computes it and names the versions it covered.
22
+ #
23
+ # AT v1.0 there is no "since": nothing has moved because nothing has synced. So when NOTHING
24
+ # has synced — an empty backlog, or stories predating the field — the work set is every
25
+ # FR-/BR- the document declares, and the per-group rules below then create only what has no
26
+ # story. A second run over the same spec creates nothing (E42).
27
+ #
28
+ # A BACKLOG THAT IS ENTIRELY CLOSED IS NOT THAT CASE. A closed story carries the version its
29
+ # work was measured against, so it sets the floor exactly as an open one does, and a
30
+ # requirement whose closed story is stamped at or above the version that last moved it is
31
+ # DELIVERED rather than unsliced. Reading the floor from open stories alone made the last
32
+ # story closing look identical to a spec nobody had ever sliced, and one idle re-run then
33
+ # raised a fresh story for every requirement in the feature (E72). Cancelled stories are
34
+ # excluded throughout: abandoned work delivered nothing and settles nothing.
35
+ #
36
+ # FR-/BR- ONLY, throughout. CN-/DC-/IC-/UX- move the contract and belong in the ledger
37
+ # row, but they are constraints a story CARRIES, not slices of work it delivers — they
38
+ # reach the story through its `## Constraints` section and never through `exercises:`.
39
+ #
40
+ # THE SHAPE OF A RUN.
41
+ # 1. the tripwire spec.md hand-edited with no version row -> REFUSE. Every story
42
+ # citing an edited requirement is already silently stale, and
43
+ # raising a backlog on top would bury the edit rather than name it.
44
+ # 2. the work set ledger-read. An empty ledger stops the run with a warning: there
45
+ # is no version to sync from, and inventing one raises a backlog
46
+ # against a document nobody has committed to.
47
+ # 3. grouping the one judgment in the workflow. Requirements that belong to one
48
+ # slice become one story; a group may hold one id or several. How
49
+ # coarsely is steered by the launch line's strategy word.
50
+ # 4. the ask where a group already has open stories in front of it. Asked, not
51
+ # decided — see below.
52
+ # 5. the writes one file per plan entry, then a deterministic frontmatter stamp.
53
+ # 6. coverage the spec's Story Coverage block, regenerated from stories/.
54
+ #
55
+ # ONE STORY, IF THAT IS WHAT YOU ASKED FOR. `/appweaver sync-stories single-run` puts the
56
+ # WHOLE work set into a single story — at v1.0 that is the whole spec, on a later run it is
57
+ # the whole update. It is the right shape for a small feature, for a spike, and for the
58
+ # change that is one thing however many requirement ids it happened to move.
59
+ #
60
+ # AND IT IS A REQUEST, NOT A GUARANTEE. Where the work set is too heavy to be picked up as
61
+ # one piece, it is split along its DEPENDENCIES rather than written as a story nobody can
62
+ # start — and the run says both that it split and what came out. A silent split leaves
63
+ # someone believing they have one story when they have six; a silently oversized story is
64
+ # worse, because it is the one that never gets picked up at all.
65
+ #
66
+ # THE ORDER, AND WHY IT IS CALLED A WAVE. Stories leave a sync with no inherent order — an
67
+ # id is minting order, which is not delivery order. So the grouping declares which slices
68
+ # stand behind which, and the arithmetic lays the result out in WAVES: wave N holds the
69
+ # stories whose every dependency sits in a wave below it.
70
+ #
71
+ # The word is doing work. "Order" and "priority" both promise a queue — a first, a second, a
72
+ # third, and a wrong answer whenever two things tie. What is true here is the opposite: TWO
73
+ # STORIES IN THE SAME WAVE ARE UNORDERED WITH RESPECT TO EACH OTHER, which is exactly why
74
+ # they can be picked up in parallel, and a scheme that cannot say "these two are equal"
75
+ # would have to invent a ranking to hide that it cannot. A wave is an ELIGIBILITY LEVEL:
76
+ # wave 1 means nothing has to land first, not "do this first". Nothing waits for a whole
77
+ # wave to finish, and a story becomes startable the moment its own dependencies are done.
78
+ # `wave` is also already the kit's word for this — /appweaver dependency-tree sorts
79
+ # migration units into waves the same way — so the backlog and the migration tree read alike.
80
+ #
81
+ # The edges are a judgment and the levels are not, which is the same split as everywhere
82
+ # else here: the model declares what stands behind what, and backlog-plan computes the
83
+ # levels, breaks any cycle deterministically, and names an edge that resolves to nothing
84
+ # rather than dropping it in silence. `follows:` is NOT an edge — a follow-on carries the
85
+ # REMAINDER of a story in flight, and a remainder is often buildable alongside it.
86
+ #
87
+ # WHY THE ASK. A requirement whose story is already open is the one case with no right
88
+ # default. Rewriting the story keeps one slice of work whole; leaving it alone keeps
89
+ # somebody's context intact and puts the change in a new story beside it. Both are
90
+ # defensible and the machine cannot weigh them, so the author picks — per story, not per
91
+ # run. Declining does not abandon the group: the declined story's requirements withdraw
92
+ # from it and whatever remains still becomes a story.
93
+ #
94
+ # AND WHY IN-FLIGHT IS DIFFERENT. Once `stories/<ID>/` exists somebody has run story-plan
95
+ # and is designing or building against the requirements as they read then. That story is
96
+ # never rewritten. Picking it mints a FOLLOW-ON carrying the change, linked by `follows:`;
97
+ # the original stays open and closes having delivered what it was written to deliver
98
+ # (§5.5). There is no `superseded` status, so completeness stays computable one way: a
99
+ # requirement is delivered when it has NO OPEN STORIES.
100
+ #
101
+ # WHAT THIS DOES NOT WRITE. The Change History table, including its `Stories` cell. That
102
+ # table is inside the region the ledger hashes, so writing it here would be an unledgered
103
+ # edit of the contract by the workflow whose whole job is to react to those. The
104
+ # spec->story relation is held ONCE, in each story's `exercises:`, and the Story Coverage
105
+ # block — which IS fenced out of the hash — is that relation read the other way round.
106
+ #
107
+ # Launch: /appweaver sync-stories [single-run] [optional guidance on how to slice]
108
+ # Prereq: /appweaver specify (the spec and its version row must exist)
109
+ # Next: /appweaver story-plan <story-id> (the execution design for one slice)
110
+ # =============================================================================
111
+
112
+ name: aw-spec-sync-stories
113
+ description: "Raise and refresh the story backlog of the ACTIVE feature from its spec. Reads the spec's Change History ledger to find the requirements that have moved since the backlog was last in step, groups them into execution slices, asks before disturbing a story someone already has open, and writes one story per slice — creating, updating in place, or minting a follow-on where a story plan already exists. Slicing is steered by an opening strategy word: `single-run` puts the whole work set into ONE story, splitting it along its dependencies only where that story could not be picked up as one piece, and reporting the split either way; omitted, the number of slices is decided from the requirements. The grouping declares which slices stand behind which, and the kit computes DELIVERY WAVES from those edges — wave N holds the stories whose every dependency sits below it, so stories sharing a wave have no ordering between them and may be picked up in parallel. Writes `depends_on:` and `wave:` into each story's frontmatter, breaks any declared cycle deterministically, and regenerates the spec's Story Coverage block. Writes no requirements — /appweaver specify and /appweaver update-spec own the contract."
114
+ version: 1.1.0
115
+ agent_agnostic: true
116
+ dialect: aw-spec.orchestrator/v1
117
+
118
+ inputs:
119
+ user_input:
120
+ description: "Optional. An opening STRATEGY word, then optional guidance on how to slice. `single-run` (also `single-story`, `single`) puts the whole work set into ONE story — split along its dependencies only where that story would be too heavy to pick up, and the split is reported. Omit it, or say `auto`, and the number of slices is decided from the requirements alone. Guidance is free prose (\"keep the reporting requirements in one story\", \"split anything touching payments\"); only the FIRST word is read as a strategy, so guidance may use the word \"single\" without changing the mode."
121
+ required: false
122
+ bind: USER_INPUT
123
+
124
+ scripts:
125
+ actions:
126
+ sh: .appweaver/scripts/bash/aw-spec-actions.sh
127
+ ps1: .appweaver/scripts/powershell/aw-spec-actions.ps1
128
+
129
+ # The universal operating rules only. A story carries the spec's constraints by id and
130
+ # invents none of its own, so the domain and technical rule corpora have nothing to add
131
+ # here — specify already lifted everything they impose into the document this reads.
132
+ standards:
133
+ - { path: .appweaver/ai-rules.md }
134
+
135
+ vars:
136
+ # A GATE THAT ONLY EXISTS TO STOP. The engine renders a JSON boolean two different ways:
137
+ # `{{var}}` json-dumps it (`true`/`false`, which is what `condition:` compares against)
138
+ # while verify_context's require_value stringifies it (`True`/`False`). So a boolean
139
+ # coming out of a script cannot be gated with require_value/equals — the comparison is
140
+ # against a spelling the interpolation never produces, and the gate passes whatever the
141
+ # value is. The test therefore lives in `condition:`, and the step it guards fails
142
+ # unconditionally: reaching it IS the failure. Used by bootstrap_gate and
143
+ # ledger_empty_gate below.
144
+ never: "false"
145
+
146
+ # THE LAUNCH LINE, TAKEN APART. slice-mode reads the strategy word off the front of the
147
+ # input and hands back what is left as guidance, so the prompt below never sees the word
148
+ # it is being steered by — and guidance that happens to contain it is still guidance.
149
+ strategy: "auto" # auto | single
150
+ strategy_note: "" # that choice, in words, for the run report
151
+ guidance: "" # the input with the strategy word taken off the front
152
+ slice_directive: "" # the grouping instruction the strategy selects
153
+ # A SIGNAL, NOT A LIMIT. Read to the model as the point at which a single story is worth
154
+ # stopping over, and reported by backlog-plan against the groups that came back. Nothing
155
+ # refuses a group for exceeding it: how big a slice can be is a fact about the work and
156
+ # the team, and a hard cap here would split a coherent story to satisfy a number.
157
+ single_ceiling: "8"
158
+ split_note: "" # single-run was asked for — did the work set still split, and why
159
+ split_reason: "" # the grouping's own sentence on what made one story undeliverable
160
+ split_count: "0"
161
+ oversized_csv: "" # groups holding more ids than the ceiling
162
+
163
+ version: "" # the spec version the backlog is being brought up to
164
+ ledger_ok: "true" # entry tripwire result
165
+ ledger_state: "" # clean | unstamped | ledgered_edit | unledgered_edit | rollback | delta_open
166
+ ledger_error: ""
167
+ needs_bootstrap: "false" # an unfinished first draft is not a contract to slice
168
+ ledger_empty: "true" # no version row at all -> warn and stop
169
+ row_count: "0"
170
+ work_csv: "" # the requirements this run has to account for
171
+ work_count: "0"
172
+ work_any: "false"
173
+ work_source: "" # ledger (rows above the floor) | spec (the v1.0 case)
174
+ work_note: "" # how the work set was arrived at, in words
175
+ span_versions_csv: "" # the versions it spans
176
+ gap: "false" # true = more than one version behind; a sync was skipped
177
+ removed_csv: "" # ids a row named that the document no longer declares (E1)
178
+ synced_floor: "" # the oldest version a story is still sitting at
179
+ floor_source: "" # open | closed | none — which stories that floor was read from
180
+ latest_version: ""
181
+ backlog_digest: "" # one line per open story, for the grouping prompt
182
+ story_count: "0"
183
+ open_count: "0"
184
+ groups: [] # the model's slices
185
+ groups_digest: ""
186
+ group_count: "0"
187
+ unplaced_csv: "" # work-set ids the grouping left out
188
+ repeated_csv: "" # ids it put in two groups
189
+ any_candidates: "false" # does any group already have an open story in front of it
190
+ candidate_ids: [] # the options of the ask
191
+ candidate_csv: ""
192
+ ask_digest: "" # those candidates, worded
193
+ approved_stories: "" # the author's answer
194
+ any_plan: "false"
195
+ plan: [] # one entry per story file to write
196
+ plan_count: "0"
197
+ plan_digest: ""
198
+ create_count: "0"
199
+ update_count: "0"
200
+ followon_count: "0"
201
+ skipped_csv: "" # stories the author declined to disturb
202
+ plan_notes: "" # what the arithmetic decided that the counts do not show
203
+ overflow_csv: "" # stories the write loop's ceiling will not reach
204
+ nothing_note: "" # why a run that wrote nothing wrote nothing — see plan_gate
205
+ stories_csv: "" # every story this run wrote
206
+ wave_digest: "" # the stories this run wrote, laid out by dependency level
207
+ wave_count: "0"
208
+ max_wave: "0"
209
+ cycle_csv: "" # dependency edges dropped to break a declared cycle
210
+ unresolved_deps_csv: "" # declared dependencies naming no slice and no open story
211
+ uncovered_csv: "" # requirements still with no story at all
212
+ delivered_csv: "" # requirements whose every story is closed
213
+
214
+ workflow:
215
+
216
+ # 0. THE LAUNCH LINE. Read before anything else because it steers the one judgment step,
217
+ # and it depends on nothing — no feature, no spec, no backlog.
218
+ #
219
+ # NOT A `condition:`. The engine compares a whole rendered value, so
220
+ # `{{USER_INPUT}} == single-run` matches only an input carrying the strategy and no
221
+ # guidance at all — and `/appweaver sync-stories single-run keep the payment rules
222
+ # together`, the form the launch line documents, would slice by default while looking
223
+ # as though it had been told not to. Silent, and in the one place the author was
224
+ # explicit.
225
+ - id: slice_mode
226
+ action: execute_script
227
+ script: { ref: actions }
228
+ subcommand: slice-mode
229
+ args: ["--input", "{{USER_INPUT}}"]
230
+ outputs:
231
+ - { name: strategy, from: "$.strategy" }
232
+ - { name: guidance, from: "$.guidance" }
233
+ - { name: strategy_note, from: "$.note" }
234
+
235
+ # 0a. THE GROUPING INSTRUCTION THE STRATEGY SELECTS. Two `set` steps rather than two
236
+ # prompts: everything else the grouping is told is the same in both modes, and a
237
+ # second copy of those rules is a second place for them to drift.
238
+ - id: directive_auto
239
+ condition: "{{strategy}} == auto"
240
+ action: set
241
+ vars:
242
+ slice_directive: |
243
+ SLICING. How many slices there are is yours to decide, from the work itself.
244
+
245
+ - A group is what one person can pick up and deliver as a coherent piece of work.
246
+ - SIZE IS A REASON TO SPLIT, NEVER A REASON TO MERGE. Where one requirement is
247
+ large enough to be a story on its own, it is one — a group of one is the normal
248
+ case, not a failure to group. Do not pad a group to make it look substantial.
249
+
250
+ - id: directive_single
251
+ condition: "{{strategy}} == single"
252
+ action: set
253
+ vars:
254
+ slice_directive: |
255
+ SLICING. ONE STORY was asked for. Put EVERY requirement listed above into a SINGLE
256
+ group covering the whole work set, and title it for what the feature delivers as a
257
+ whole rather than for any one part of it.
258
+
259
+ - SPLIT ONLY IF THAT STORY COULD NOT BE PICKED UP. The test is whether one person
260
+ could take it and deliver it as one coherent piece of work — not whether it is
261
+ tidy, and not the id count: more than {{single_ceiling}} ids in one story is a
262
+ signal worth stopping on, never a verdict on its own.
263
+ - IF YOU DO SPLIT, SPLIT ALONG THE DEPENDENCIES. The first group is what everything
264
+ else is built on and what needs nothing itself; each group after it holds what
265
+ becomes possible once the ones before it exist. Do not split by theme, by section
266
+ of the spec, or by id range — a split that does not follow the dependencies
267
+ produces stories that cannot be started in any order at all, which is a worse
268
+ answer than the single heavy story it was meant to fix.
269
+ - SAY WHY, in `split_reason`: one sentence naming what made the single story
270
+ undeliverable. Somebody asked for one story and is getting several, and they are
271
+ owed the reason rather than left to infer it from the count.
272
+
273
+ # 1. CONSUME the active feature (set by select-feature). No derive-names.
274
+ - id: feature_paths
275
+ action: execute_script
276
+ on_error: continue
277
+ script: { ref: actions }
278
+ subcommand: feature-paths
279
+ outputs:
280
+ - { name: feature_dir, from: "$.feature_dir" }
281
+ - { name: spec_file, from: "$.spec_file" }
282
+ - { name: spec_exists, from: "$.spec_exists" }
283
+ - { name: needs_bootstrap, from: "$.needs_bootstrap" }
284
+
285
+ - id: require_feature
286
+ action: verify_context
287
+ require_non_empty: feature_dir
288
+ on_empty: stop
289
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then /appweaver specify, then re-run sync-stories."
290
+
291
+ # 2. AN UNFINISHED FIRST DRAFT IS NOT A CONTRACT. needs_bootstrap covers both "no spec.md"
292
+ # and "a specify run that died before writing its v1.0 row" — and the second is the one
293
+ # that matters here, because that document IS on disk and does carry requirements. Its
294
+ # ledger row is still the template's placeholder, whose "Requirements changed" cell
295
+ # literally reads FR-001…FR-00n; syncing off it would raise a backlog against ids
296
+ # nobody chose, at a version nobody committed to.
297
+ - id: bootstrap_gate
298
+ condition: "{{needs_bootstrap}} == true"
299
+ action: verify_context
300
+ require_true: never
301
+ on_false: stop
302
+ on_false_message: >
303
+ {{spec_file}} is an unfinished first draft — specify has not written its v1.0 Change
304
+ History row, so the document is not yet a contract anything can be sliced against.
305
+ Re-run /appweaver specify with the same input (it re-authors the draft rather than
306
+ extending it), then re-run sync-stories.
307
+
308
+ # 3. THE ENTRY TRIPWIRE. The ledger is hand-writable, which is the point — a spec can be
309
+ # edited by hand — and also its one silent failure: a body edited with no version row
310
+ # leaves every story citing an edited requirement stale with nothing to detect it.
311
+ # That is precisely the state this workflow exists to resolve, and precisely the one it
312
+ # cannot resolve, because the row is what says WHICH requirements moved. Refusing here
313
+ # turns the silent case into a message naming the fix (E13).
314
+ #
315
+ # `unstamped` and `ledgered_edit` both pass: the first is a spec that predates the
316
+ # ledger, the second is a hand edit that did write its row — exactly the Direction A
317
+ # case this workflow is for.
318
+ #
319
+ # No `on_error: continue`: spec-check reports every ledger STATE through its JSON with
320
+ # a zero exit, so a non-zero exit is the script itself failing, and continuing past
321
+ # that would bind the gate's vars to nothing and produce the refusal with its reason
322
+ # blanked out.
323
+ - id: ledger_check
324
+ action: execute_script
325
+ script: { ref: actions }
326
+ subcommand: spec-check
327
+ args: ["--feature-dir", "{{feature_dir}}"]
328
+ outputs:
329
+ - { name: ledger_ok, from: "$.ok" }
330
+ - { name: ledger_state, from: "$.state" }
331
+ - { name: ledger_error, from: "$.error" }
332
+ - { name: version, from: "$.version" }
333
+
334
+ - id: ledger_gate
335
+ action: verify_context
336
+ require_true: ledger_ok
337
+ on_false: stop
338
+ on_false_message: >
339
+ /appweaver sync-stories cannot read the backlog's anchor: {{ledger_error}}
340
+ A version row is what tells a story that a requirement it cites has moved, so a spec
341
+ whose body has changed without one cannot be synced — this run would raise stories
342
+ for what the LAST row named and leave the hand edit unnamed and unreconciled.
343
+ Resolve the state above, then re-run. (State: {{ledger_state}}.)
344
+
345
+ # 4. THE WORK SET. See the header for why it spans versions rather than reading the
346
+ # newest row alone, and for the v1.0 case.
347
+ - id: ledger_read
348
+ action: execute_script
349
+ script: { ref: actions }
350
+ subcommand: ledger-read
351
+ args: ["--feature-dir", "{{feature_dir}}"]
352
+ outputs:
353
+ - { name: row_count, from: "$.row_count" }
354
+ - { name: ledger_empty, from: "$.ledger_empty" }
355
+ - { name: work_csv, from: "$.work_csv" }
356
+ - { name: work_count, from: "$.work_count" }
357
+ - { name: work_any, from: "$.work_any" }
358
+ - { name: work_source, from: "$.work_source" }
359
+ - { name: work_note, from: "$.work_note" }
360
+ - { name: span_versions_csv, from: "$.span_versions_csv" }
361
+ - { name: gap, from: "$.gap" }
362
+ - { name: removed_csv, from: "$.removed_csv" }
363
+ - { name: synced_floor, from: "$.synced_floor" }
364
+ - { name: floor_source, from: "$.floor_source" }
365
+ - { name: latest_version, from: "$.latest_version" }
366
+
367
+ # 4a. AN EMPTY LEDGER STOPS THE RUN. The table is there and carries no version row, which
368
+ # means the spec has never been versioned. There is no "since" to sync from, and
369
+ # choosing one — the whole document, say — would raise a backlog against requirements
370
+ # nobody has committed to a version of. A warning, not a failure: nothing is wrong
371
+ # with the document, it simply is not ready to be sliced.
372
+ - id: ledger_empty_gate
373
+ condition: "{{ledger_empty}} == true"
374
+ action: verify_context
375
+ require_true: never
376
+ on_false: stop
377
+ on_false_message: >
378
+ The Change History table in {{spec_file}} carries no version row, so there is no
379
+ version for a story to sync at and nothing recording what this spec has changed.
380
+ Nothing was written. Run /appweaver specify to author or extend the spec — it computes
381
+ the row from what it actually changed and stamps the document — then re-run
382
+ /appweaver sync-stories. (If the spec was written by hand, add a row naming the
383
+ requirement ids it introduced and re-run spec-stamp.)
384
+
385
+ # 4b. NOTHING TO DO IS NOT AN ERROR. Every version row is at or below the oldest open
386
+ # story's stamp: the backlog is already in step. Stopping here rather than running an
387
+ # empty grouping keeps the no-op cheap and the message honest.
388
+ - id: work_gate
389
+ action: verify_context
390
+ require_true: work_any
391
+ on_false: stop
392
+ on_false_message: >
393
+ The backlog is already in step with {{spec_file}} at {{latest_version}} — every
394
+ requirement a version row names has either been delivered by a closed story or is
395
+ already carried by an open one synced past it. Nothing was written. ({{work_note}}.)
396
+
397
+ # 4c. A REMOVED REQUIREMENT IS A DECISION, NOT A DEFAULT (E1). A row names what a version
398
+ # TOUCHED, and that includes ids it deleted. Those leave the work set — there is no
399
+ # wording left to write a story from — but every open story citing one is now
400
+ # orphaned, delivering a requirement the contract no longer makes. Reported and
401
+ # continued: this run can still raise the stories it came to raise, and the orphan
402
+ # needs a person, not a rule.
403
+ - id: removed_notice
404
+ condition: "{{removed_csv}} != ''"
405
+ action: verify_context
406
+ require_value: removed_csv
407
+ equals: ""
408
+ on_mismatch: continue
409
+ on_mismatch_message: >
410
+ A version row names requirements the spec no longer declares: {{removed_csv}}.
411
+ They are excluded from this run — a removed requirement has no wording to write a
412
+ story from — but any OPEN story citing one is now orphaned, and closing it would
413
+ report a requirement delivered that the contract no longer makes. Decide those by
414
+ hand: cancel the story, or re-state the requirement via /appweaver specify.
415
+
416
+ # 4d. MORE THAN ONE VERSION BEHIND. The span exists precisely so a skipped sync does not
417
+ # bury a version, and it does that silently — the middle version's requirements are
418
+ # picked up with nothing saying they nearly were not. This says so, because a gap is
419
+ # also a fact about how the two documents are being worked: a spec that moved twice
420
+ # with no sync between is one whose backlog is not being kept.
421
+ - id: gap_notice
422
+ condition: "{{gap}} == true"
423
+ action: verify_context
424
+ require_value: span_versions_csv
425
+ equals: ""
426
+ on_mismatch: continue
427
+ on_mismatch_message: >
428
+ The backlog is more than one version behind: this run spans {{span_versions_csv}},
429
+ because the oldest {{floor_source}} story is still at {{synced_floor}}. Every requirement those
430
+ rows name is in the work set, so nothing is lost — but a version in the middle
431
+ reached no story until now.
432
+
433
+ # 5. THE BACKLOG AS IT STANDS. Read once, whole, and only from frontmatter — which is why
434
+ # §6.1 puts everything the gates need there. Fed to the grouping step so slices are
435
+ # proposed against the work that exists rather than into a vacuum.
436
+ # No `on_error: continue`. story-refs exits zero on an empty backlog and on a stories
437
+ # directory that does not exist yet, which are the ordinary cases — so a non-zero exit
438
+ # is the op itself failing to read the backlog, and grouping against a digest that says
439
+ # "no open stories" for that reason proposes slices over work that already exists.
440
+ - id: backlog_scan
441
+ action: execute_script
442
+ script: { ref: actions }
443
+ subcommand: story-refs
444
+ args: ["--feature-dir", "{{feature_dir}}"]
445
+ outputs:
446
+ - { name: backlog_digest, from: "$.digest" }
447
+ - { name: story_count, from: "$.story_count" }
448
+ - { name: open_count, from: "$.open_count" }
449
+
450
+ # 6. THE ONE JUDGMENT IN THE WORKFLOW. Everything before this is a read and everything
451
+ # after it is arithmetic; how coarsely a spec slices into stories is decided here,
452
+ # once, in one place (§5.15) — which is why specify writes no stories at all.
453
+ #
454
+ # The spec is in context because grouping is a judgment about the REQUIREMENTS, not
455
+ # about their ids: two that share an aggregate invariant belong together and two that
456
+ # merely sort next to each other do not.
457
+ # No build_context envelope: a sticky context is carried in front of EVERY later model
458
+ # step, and every step below already declares the spec itself — so the envelope would
459
+ # send it twice per call and once again for each story written. Each step names what it
460
+ # reads, which is also the only place a reader has to look.
461
+ - id: group_query
462
+ action: agent_query
463
+ context:
464
+ - "{{spec_file}}"
465
+ include_standards: false
466
+ task: |
467
+ The specification in context is the contract for this aggregate. These requirements
468
+ have moved since its story backlog was last in step, and each one needs to reach a
469
+ story:
470
+
471
+ {{work_csv}}
472
+
473
+ ({{work_note}}.)
474
+
475
+ The backlog as it stands today:
476
+
477
+ {{backlog_digest}}
478
+
479
+ {{guidance}}
480
+
481
+ Group those requirements into EXECUTION SLICES. One group becomes one story.
482
+
483
+ {{slice_directive}}
484
+
485
+ WHAT BELONGS IN ONE SLICE.
486
+
487
+ - A group is what one person can pick up and deliver as a coherent piece of work.
488
+ Judge that on the requirements themselves, as the spec words them — not on their
489
+ id numbers, which say nothing about what belongs together.
490
+ - PUT TOGETHER what cannot honestly be delivered apart: a behaviour and the business
491
+ rule that constrains it; two requirements that share an aggregate invariant, an
492
+ entity's lifecycle, or one user-visible outcome. A requirement whose acceptance
493
+ cannot be demonstrated without another one is in that other one's group.
494
+ - THAT TEST IGNORES WHERE THE SPEC PUTS THEM. Two requirements that read as one piece
495
+ of work belong in one group however far apart they are stated, and one that reads
496
+ as a separate concern stays separate however adjacent it is. The document's section
497
+ order is how it was written down, not how it gets built.
498
+ - KEEP APART what merely resembles: two requirements over the same entity that a
499
+ reviewer could accept independently are two stories. Coupling them buys nothing and
500
+ makes both wait for the slower half.
501
+
502
+ WHAT STANDS BEHIND WHAT.
503
+
504
+ - DECLARE THE ORDER. In each group's `depends_on`, name the groups in THIS list that
505
+ have to land before it — where this slice would be built against something another
506
+ slice has not delivered yet. An OPEN STORY from the backlog above may be named the
507
+ same way, by its story id. Empty where nothing has to come first.
508
+ - THIS IS ABOUT CODE, NOT IMPORTANCE. A group everything else calls into comes first
509
+ because of what it builds, not because it matters more. Do not name a group merely
510
+ because it is related, or thematically prior, or bigger: an edge that is not real
511
+ makes work wait for nothing, and it will not be questioned by anything downstream.
512
+ - GROUPS WITH NO EDGE BETWEEN THEM END UP AT THE SAME LEVEL and are picked up in
513
+ PARALLEL. That is the outcome to aim for — order only what genuinely has to be
514
+ ordered, and leave the rest free to run alongside each other.
515
+ - NO CYCLES. If two groups each need the other before they can be built, they are not
516
+ two slices — they are one, and should be one group.
517
+
518
+ THE RULES THAT DO NOT BEND.
519
+
520
+ - Every id in the list above must appear in EXACTLY ONE group. Not zero: an id left
521
+ out gets no story and nothing downstream will notice. Not two: two stories both
522
+ claiming one requirement makes "is this requirement delivered" unanswerable.
523
+ - Use ONLY the ids listed above. Do not pull in a requirement that has not moved, and
524
+ do not invent one.
525
+ - Give each group a title that names the OUTCOME it delivers, in the spec's own
526
+ vocabulary — what a reviewer would see working, not the ids it covers.
527
+ - `complexity` is your read of the slice's size: low | medium | high.
528
+ - `rationale` is one sentence on why these belong together, or why this one stands
529
+ alone. It is read by a person deciding whether to accept the slicing.
530
+ schema:
531
+ split_reason: "<empty unless ONE story was asked for and you returned more than one; then one sentence on what made the single story undeliverable>"
532
+ groups:
533
+ - id: g1
534
+ title: "<the outcome this slice delivers>"
535
+ requirements: ["FR-001", "BR-002"]
536
+ depends_on: ["<a group id from this list, or an open story id, that must land first — empty if nothing must>"]
537
+ complexity: "low|medium|high"
538
+ rationale: "<why these belong in one story>"
539
+ # No slicing without a model. Any fixed grouping here would name requirement ids that
540
+ # exist in one repo and nowhere else, and backlog-plan drops ids outside the work set — so
541
+ # a "realistic" mock degenerates to this one anyway, having first suggested otherwise. A
542
+ # mock run therefore exercises the ledger read, the work set and every gate, then stops
543
+ # at plan_gate saying the grouping produced nothing. That is the honest outcome.
544
+ mock:
545
+ groups: []
546
+ split_reason: ""
547
+ outputs:
548
+ - { name: groups, from: "$.groups" }
549
+ - { name: split_reason, from: "$.split_reason" }
550
+
551
+ # 7. WHICH GROUPS ALREADY HAVE STORIES IN FRONT OF THEM. Deterministic: the model
552
+ # proposed the slices, and the arithmetic over them — what each group covers, which
553
+ # open stories intersect it, and what the question about each one should say — is
554
+ # computed, so the same grouping always produces the same question.
555
+ - id: plan_scan
556
+ action: execute_script
557
+ script: { ref: actions }
558
+ subcommand: backlog-plan
559
+ args: ["--feature-dir", "{{feature_dir}}", "--groups", "{{groups}}", "--work", "{{work_csv}}", "--strategy", "{{strategy}}", "--ceiling", "{{single_ceiling}}"]
560
+ outputs:
561
+ - { name: group_count, from: "$.group_count" }
562
+ - { name: groups_digest, from: "$.groups_digest" }
563
+ - { name: unplaced_csv, from: "$.unplaced_csv" }
564
+ - { name: repeated_csv, from: "$.repeated_csv" }
565
+ - { name: any_candidates, from: "$.any_candidates" }
566
+ - { name: candidate_ids, from: "$.candidate_ids" }
567
+ - { name: candidate_csv, from: "$.candidate_csv" }
568
+ - { name: ask_digest, from: "$.ask_digest" }
569
+ - { name: split_note, from: "$.split_note" }
570
+ - { name: split_count, from: "$.split_count" }
571
+ - { name: oversized_csv, from: "$.oversized_csv" }
572
+
573
+ # 7aa. ONE STORY WAS ASKED FOR AND MORE THAN ONE CAME BACK. A legitimate answer — a slice
574
+ # nobody can pick up is not a story, and the split is what the option promises where
575
+ # the work set is too heavy. Reported BEFORE the ask, because it changes what the
576
+ # author is agreeing to: they asked for one story and are about to approve several.
577
+ # Continuing rather than stopping is the point; refusing would leave the only route
578
+ # to a large single-story feature being not to use the option at all.
579
+ - id: split_notice
580
+ condition: "{{split_count}} != 0"
581
+ action: verify_context
582
+ require_value: split_count
583
+ equals: "0"
584
+ on_mismatch: continue
585
+ on_mismatch_message: >
586
+ {{split_note}}. The stories are written in dependency order and each one names what
587
+ has to land before it, so the work is still one sequence — it is spread over more
588
+ stories than the launch line asked for. Reason given: {{split_reason}}
589
+
590
+ # 7ab. A SLICE NOBODY CAN PICK UP. The ceiling is a signal and not a limit, so this
591
+ # neither refuses nor rewrites: how big a story can be is a fact about the work and
592
+ # the team, and a hard cap here would split a coherent story to satisfy a number.
593
+ # Said out loud because an oversized story is the one that stays open — it is easier
594
+ # to re-slice now than to discover it three weeks in.
595
+ - id: oversized_notice
596
+ condition: "{{oversized_csv}} != ''"
597
+ action: verify_context
598
+ require_value: oversized_csv
599
+ equals: ""
600
+ on_mismatch: continue
601
+ on_mismatch_message: >
602
+ These slices carry more than {{single_ceiling}} requirements each: {{oversized_csv}}.
603
+ Nothing is refused — the count is a signal, not a limit — but a story that large is
604
+ the one that never gets picked up. Re-run /appweaver sync-stories with guidance on
605
+ where to divide it if it is genuinely more than one piece of work.
606
+
607
+ # 7a. A REQUIREMENT THE GROUPING DROPPED GETS NO STORY, AND THAT IS THE SILENT ONE. The
608
+ # prompt above forbids it; this is the check that the model obeyed. Reported rather
609
+ # than stopping, because the groups that ARE well formed are still worth writing —
610
+ # but the run says which ids it is about to leave uncovered, and spec-coverage at the
611
+ # end says so again from the document's own point of view.
612
+ - id: unplaced_notice
613
+ condition: "{{unplaced_csv}} != ''"
614
+ action: verify_context
615
+ require_value: unplaced_csv
616
+ equals: ""
617
+ on_mismatch: continue
618
+ on_mismatch_message: >
619
+ The grouping left these requirements out of every slice: {{unplaced_csv}}. No story
620
+ will be written for them by this run. Re-run /appweaver sync-stories to place them,
621
+ or raise them by hand with /appweaver user-story.
622
+
623
+ - id: repeated_notice
624
+ condition: "{{repeated_csv}} != ''"
625
+ action: verify_context
626
+ require_value: repeated_csv
627
+ equals: ""
628
+ on_mismatch: continue
629
+ on_mismatch_message: >
630
+ The grouping put these requirements in more than one slice: {{repeated_csv}}. Each
631
+ has been kept in the FIRST group that claimed it and removed from the others, so no
632
+ requirement gets two stories both claiming it — which would make "does this
633
+ requirement still have open stories", the completeness rule, answer for work that was
634
+ never really duplicated. No coverage is lost; the slicing may not be the one the
635
+ grouping intended, so it is worth a look at the stories this run wrote.
636
+
637
+ # 8. THE ASK. See the header for why this is asked rather than decided. Multi-select,
638
+ # per story, because the decision is per story: a group can hold one story the author
639
+ # is happy to move and another they will not touch, and a single yes/no would force
640
+ # the same answer onto both.
641
+ #
642
+ # A non-interactive run answers with ALL options, which is the settled default in both
643
+ # directions and not a shortcut: a not-started story updated in place is §5.4's case 2,
644
+ # and an in-flight story picked here mints a follow-on and is never rewritten (§5.5).
645
+ # Leaving one out is the choice that needs a person.
646
+ - id: ask_update
647
+ condition: "{{any_candidates}} == true"
648
+ action: ask
649
+ multi: true
650
+ prompt: |
651
+ Some of these requirements already have OPEN stories in front of them.
652
+
653
+ {{ask_digest}}
654
+
655
+ Pick the stories this sync MAY change.
656
+
657
+ PICKED, not started the story is rewritten to cover its whole group and
658
+ restamped at {{latest_version}}.
659
+ PICKED, in flight the story is left exactly as it is and a FOLLOW-ON is minted
660
+ to carry the change, linked back to it.
661
+ LEFT OUT the story is not touched at all, AND the requirements it
662
+ already covers withdraw from their group. Whatever is left
663
+ of that group still becomes a new story, so a group where
664
+ one id has a story you will not move still delivers the rest.
665
+
666
+ Which may be changed?
667
+ options_from: "{{candidate_ids}}"
668
+ answer_key: approved_stories
669
+ answer_var: approved_stories
670
+
671
+ # 9. THE PLAN: one entry per story FILE, each already carrying its id, its path, and the
672
+ # ids it must end up citing. Same op, same groups, same backlog as the scan above, so
673
+ # the question the author answered and the work that follows from it cannot disagree
674
+ # about what a group contains.
675
+ - id: plan_build
676
+ action: execute_script
677
+ script: { ref: actions }
678
+ subcommand: backlog-plan
679
+ args: ["--plan", "--feature-dir", "{{feature_dir}}", "--groups", "{{groups}}", "--work", "{{work_csv}}", "--approved", "{{approved_stories}}", "--version", "{{latest_version}}", "--cap", "200", "--strategy", "{{strategy}}", "--ceiling", "{{single_ceiling}}"]
680
+ outputs:
681
+ - { name: plan, from: "$.plan" }
682
+ - { name: plan_count, from: "$.plan_count" }
683
+ - { name: any_plan, from: "$.any_plan" }
684
+ - { name: plan_digest, from: "$.plan_digest" }
685
+ - { name: create_count, from: "$.create_count" }
686
+ - { name: update_count, from: "$.update_count" }
687
+ - { name: followon_count, from: "$.followon_count" }
688
+ - { name: skipped_csv, from: "$.skipped_csv" }
689
+ - { name: stories_csv, from: "$.stories_csv" }
690
+ - { name: plan_notes, from: "$.notes_text" }
691
+ - { name: overflow_csv, from: "$.overflow_csv" }
692
+ - { name: wave_digest, from: "$.wave_digest" }
693
+ - { name: wave_count, from: "$.wave_count" }
694
+ - { name: max_wave, from: "$.max_wave" }
695
+ - { name: cycle_csv, from: "$.cycle_csv" }
696
+ - { name: unresolved_deps_csv, from: "$.unresolved_deps_csv" }
697
+
698
+ # 9a. NOTHING TO WRITE HAS TWO CAUSES, AND THEY NEED DIFFERENT ANSWERS. Either the author
699
+ # declined every story that stood in the way — a decision, not a failure — or the
700
+ # grouping produced no slice at all, which means the one judgment step returned
701
+ # nothing usable and the run should be repeated. A single message asserting the first
702
+ # cause would send someone looking for a decision they never made. Two ordered sets
703
+ # rather than a compound condition, the same idiom specify uses for covered_note.
704
+ - id: nothing_grouping
705
+ action: set
706
+ vars:
707
+ nothing_note: >-
708
+ the grouping produced no slice for any of them ({{work_count}} requirement(s) went
709
+ in, {{group_count}} group(s) came out). Nothing was declined and nothing was
710
+ written — re-run /appweaver sync-stories, and if it comes back empty again, raise
711
+ the stories by hand with /appweaver user-story
712
+
713
+ - id: nothing_declined
714
+ condition: "{{skipped_csv}} != ''"
715
+ action: set
716
+ vars:
717
+ nothing_note: >-
718
+ every one of them is already covered by a story you chose not to disturb
719
+ ({{skipped_csv}}). Those stories keep their existing spec versions, so this sync
720
+ will be offered again on the next run
721
+
722
+ - id: plan_gate
723
+ action: verify_context
724
+ require_true: any_plan
725
+ on_false: stop
726
+ on_false_message: >
727
+ Nothing to write for {{work_csv}}: {{nothing_note}}. The backlog is unchanged.
728
+
729
+ # 9b. MORE STORIES THAN THE WRITE LOOP WILL WRITE. `over:` stops at max_iterations and
730
+ # says nothing — the entries past it are never written and the run still reports
731
+ # success, which is the silent failure this whole workflow exists to remove. The cap
732
+ # passed to backlog-plan above MUST match write_loop's max_iterations; this is what
733
+ # names the stories that fall past it.
734
+ - id: overflow_notice
735
+ condition: "{{overflow_csv}} != ''"
736
+ action: verify_context
737
+ require_value: overflow_csv
738
+ equals: ""
739
+ on_mismatch: continue
740
+ on_mismatch_message: >
741
+ The plan holds {{plan_count}} stories and this run writes at most 200 of them. These
742
+ are not written: {{overflow_csv}}. Their requirements stay in the work set, so a
743
+ second /appweaver sync-stories run picks them up — or raise write_loop's
744
+ max_iterations (and the --cap it is passed) if a spec of this size is normal here.
745
+
746
+ # 9c. A CYCLE WAS DECLARED AND HAD TO BE BROKEN. Two slices each needing the other before
747
+ # it can be built is a statement that they are one slice, and the arithmetic cannot
748
+ # merge them — the grouping is the judgment. So the edge closing each cycle is
749
+ # dropped, deterministically, and named: a backlog with no order at all is worse than
750
+ # one missing an edge somebody can re-add, and an edge dropped in silence is worse
751
+ # than both.
752
+ - id: cycle_notice
753
+ condition: "{{cycle_csv}} != ''"
754
+ action: verify_context
755
+ require_value: cycle_csv
756
+ equals: ""
757
+ on_mismatch: continue
758
+ on_mismatch_message: >
759
+ The slicing declared stories that depend on each other in a cycle. These edges were
760
+ dropped so the backlog has an order at all: {{cycle_csv}}. Two slices that each need
761
+ the other are one slice — check whether those stories should have been written as
762
+ one, and add the ordering back by hand in `depends_on:` if the cycle was a mistake in
763
+ the naming rather than in the slicing.
764
+
765
+ # 9d. AN EDGE POINTING AT NOTHING. Named rather than dropped in silence, because a
766
+ # dependency that resolves to no slice and no open story is usually a real ordering
767
+ # claim aimed at the wrong name — and the story it should have pointed at now looks
768
+ # free to start.
769
+ - id: unresolved_deps_notice
770
+ condition: "{{unresolved_deps_csv}} != ''"
771
+ action: verify_context
772
+ require_value: unresolved_deps_csv
773
+ equals: ""
774
+ on_mismatch: continue
775
+ on_mismatch_message: >
776
+ The slicing named dependencies that match no slice in this run and no open story:
777
+ {{unresolved_deps_csv}}. They set no order, so any story that was meant to wait for
778
+ them reads as free to start. Check `depends_on:` on the stories this run wrote.
779
+
780
+ # 10. THE WRITES. One pass per plan entry, and each pass is an author step followed by a
781
+ # stamp. The three author steps are mutually exclusive on `action`; whichever runs
782
+ # writes the whole file.
783
+ #
784
+ # ENTRIES ARRIVE IN WAVE ORDER, so the stories are written in the order they can be
785
+ # picked up in — and a run that meets the ceiling below loses the LAST wave rather
786
+ # than an arbitrary slice of the backlog.
787
+ #
788
+ # THE STAMP IS NOT A FORMALITY. The frontmatter is the gate surface — spec-coverage,
789
+ # check-stories, analyze-slice and close-story all read `exercises:`, `spec_version:`
790
+ # and `status:` and none of them opens the body, and `depends_on:` / `wave:` are what
791
+ # say which stories can start now. So those fields are written by the
792
+ # kit from the plan, not taken on trust from the model that wrote the prose, for the
793
+ # same reason the Change History row is computed rather than declared: a model that
794
+ # drops one id from `exercises:` produces a story that reads perfectly and is
795
+ # invisible to every gate built to protect it.
796
+ - id: write_loop
797
+ action: loop
798
+ over: "{{plan}}"
799
+ as: p
800
+ # MUST MATCH the --cap passed to plan_build above, which is what reports the entries
801
+ # that fall past it. The engine's foreach stops here in silence.
802
+ max_iterations: 200
803
+ steps:
804
+
805
+ # 10a. CREATE — a group nothing is building yet.
806
+ - id: create_story
807
+ condition: "{{p.action}} == create"
808
+ action: agent_author
809
+ context:
810
+ - .appweaver/templates/story-template.md
811
+ - "{{spec_file}}"
812
+ - { path: .appweaver/memory/constitution.ui.md, optional: true }
813
+ output_target: { type: file, path: "{{p.path}}" }
814
+ task: |
815
+ Write ONE user story into the template in context, for this slice of the
816
+ specification:
817
+
818
+ story id: {{p.story}}
819
+ slice: {{p.group_title}}
820
+ requirements: {{p.requirements_csv}}
821
+ why one slice: {{p.rationale}}
822
+ size: {{p.complexity}}
823
+ lands after: {{p.depends_on_csv}}
824
+ wave: {{p.wave}}
825
+
826
+ Return the COMPLETE story document. Rules:
827
+
828
+ - THE SPEC IS THE CONTRACT AND THIS IS A SLICE OF WORK OVER IT. Nothing here
829
+ changes it. If writing this story exposes something the spec gets wrong or
830
+ leaves out, say so in Notes and leave the requirement alone — the contract
831
+ moves through /appweaver update-spec, never through a story.
832
+ - Frontmatter: set `id:` to {{p.story}}, `exercises:` to exactly
833
+ [{{p.requirements_csv}}], `status: open`, and `spec_version:` to
834
+ {{latest_version}}. Give it a `title:` that names the OUTCOME a reviewer would
835
+ see working, in the spec's vocabulary — not the ids it covers.
836
+ - `depends_on:` AND `wave:` ARE THE KIT'S, written from the slicing above. Do not
837
+ set them and do not write anything that contradicts them. What they cannot say
838
+ belongs in "## Notes": WHY the stories named above have to land first, and what
839
+ of this slice can be started before they do. A wave is an eligibility level and
840
+ not a priority — stories sharing one have no ordering between them and are
841
+ picked up in parallel.
842
+ - "## Story" is one "As a … I want … so that …", written from the actor the spec
843
+ names. If the requirements serve different actors, that is a signal the slice
844
+ is wrong — say so in Notes rather than inventing a role that covers both.
845
+ - "## Requirements delivered": one line per id, saying what THIS story delivers
846
+ OF that requirement. A story rarely delivers a whole requirement; name the
847
+ part. Quote no requirement in full — the spec is the wording.
848
+ - "## Constraints" IS NOT OPTIONAL AND IS THE HALF MOST OFTEN LOST. Read the
849
+ spec's Constraints & NFRs (CN-), Data Classification (DC-), Integration
850
+ Contracts (IC-) and Experience Standards (UX-) sections, and its Experience
851
+ Scope, and carry BY ID every entry that binds this slice — with one line each
852
+ saying what this slice has to do about it. Where the slice has a user-facing
853
+ surface, the UX- standards and the device / browser / assistive-technology
854
+ floors apply and MUST be listed: prototype, design-ui and implement-ui author
855
+ from the spec and the story, so a standard not carried here is a standard this
856
+ slice will be built without. Do NOT add these ids to `exercises:` — that field
857
+ is FR-/BR- only, because it is the delivery relation, and a constraint is not
858
+ delivered, it is met. Write "None" only for a class the spec genuinely leaves
859
+ empty or that genuinely does not touch this slice.
860
+ - "## Acceptance": checkable statements in the spec's vocabulary, covering every
861
+ requirement in `exercises:` and every constraint listed above that this slice
862
+ can be observed to meet. These are what analyze-slice pairs tests against, so
863
+ each one must be decidable by looking at the running system — never "works
864
+ correctly", never a restatement of the requirement.
865
+ - THIS STORY IS THE INPUT TO story-plan, which designs its execution against
866
+ the current spec and code. Write it so that design pass needs nothing this
867
+ document does not carry:
868
+ the actor, the requirement ids, the constraints by id, and acceptance that
869
+ settles what "done" means. Detail that pins the outcome down, not detail for
870
+ its own sake.
871
+ - NO IMPLEMENTATION. No types, endpoints, schemas, frameworks, file layouts or
872
+ library choices — those are the design phase's, and a story that names them
873
+ has decided something it has no standing to decide.
874
+ - Use "## Notes" for sequencing, risks, and what is deliberately OUT of this
875
+ slice. Leave the template's guidance comments in place.
876
+
877
+ # 10b. FOLLOW-ON — the group's story is already being implemented (§5.5). The original
878
+ # is in context to be read, never to be rewritten: this file is a NEW story, and
879
+ # what makes the pair coherent is that it carries only the remainder.
880
+ - id: followon_story
881
+ condition: "{{p.action}} == followon"
882
+ action: agent_author
883
+ context:
884
+ - .appweaver/templates/story-template.md
885
+ - "{{spec_file}}"
886
+ - "{{p.prior_path}}"
887
+ - { path: .appweaver/memory/constitution.ui.md, optional: true }
888
+ output_target: { type: file, path: "{{p.path}}" }
889
+ task: |
890
+ Write a FOLLOW-ON story. {{p.follows}} is open and already being implemented — a
891
+ story plan exists and someone is building against the requirements as they read
892
+ when it was written. That story is NOT being changed. This one carries what the
893
+ spec has moved since.
894
+
895
+ new story id: {{p.story}}
896
+ follows: {{p.follows}} ("{{p.prior_title}}", which cites {{p.prior_csv}})
897
+ slice: {{p.group_title}}
898
+ requirements: {{p.requirements_csv}}
899
+ why one slice: {{p.rationale}}
900
+
901
+ {{p.follows}} is in context. Read it to see what is ALREADY being delivered, and
902
+ write only the remainder.
903
+
904
+ Return the COMPLETE story document. Rules:
905
+
906
+ - Frontmatter: `id: {{p.story}}`, `follows: {{p.follows}}`, `exercises:` exactly
907
+ [{{p.requirements_csv}}], `status: open`, `spec_version: {{latest_version}}`.
908
+ - THE REMAINDER, NOT THE WHOLE. {{p.follows}} closes having delivered what it was
909
+ written to deliver, and this story delivers what the spec now asks for beyond
910
+ that. Say in "## Requirements delivered", per id, what has moved and what this
911
+ story adds — a reader holding both should see two halves of one requirement,
912
+ never two stories racing to deliver the same thing.
913
+ - Both stories stay open and both are legitimate. There is no "superseded": a
914
+ requirement is delivered when it has NO OPEN STORIES, so the pair is exactly
915
+ how a mid-flight change is recorded.
916
+ - Name the dependency honestly in "## Notes": what of {{p.follows}} has to land
917
+ first, and what can proceed in parallel. `depends_on:` and `wave:` are the
918
+ kit's — this story is in wave {{p.wave}}, after {{p.depends_on_csv}} — and
919
+ `follows:` is deliberately NOT an ordering link, because the remainder is often
920
+ buildable alongside the story it carries the remainder of. Saying which half is
921
+ which is exactly what "## Notes" is for.
922
+ - Everything the ordinary story rules require still holds: one "As a … I want …
923
+ so that …"; "## Constraints" carrying the spec's CN-/DC-/IC-/UX- ids and the
924
+ Experience Scope floors that bind this slice, by id, never in `exercises:`;
925
+ "## Acceptance" decidable by looking at the running system; no implementation
926
+ detail; enough that story-plan can scope its design pass from it directly.
927
+
928
+ # 10c. UPDATE — the story is open, no story plan exists, and the author approved it.
929
+ # It is rewritten against the spec as it now reads, and it may GAIN requirements:
930
+ # the group's unclaimed ids move into it, which is the point of approving it.
931
+ - id: update_story
932
+ condition: "{{p.action}} == update"
933
+ action: agent_author
934
+ context:
935
+ - "{{p.path}}"
936
+ - "{{spec_file}}"
937
+ - { path: .appweaver/memory/constitution.ui.md, optional: true }
938
+ output_target: { type: file, path: "{{p.path}}" }
939
+ task: |
940
+ The story {{p.story}} is in context, and the specification it slices has moved.
941
+ Bring it back into step.
942
+
943
+ story: {{p.story}} ("{{p.prior_title}}")
944
+ it cites today: {{p.prior_csv}}
945
+ it must now cite: {{p.requirements_csv}}
946
+ its slice: {{p.group_title}}
947
+ why one slice: {{p.rationale}}
948
+
949
+ Return the COMPLETE updated story. Rules:
950
+
951
+ - EDIT IN PLACE. Keep everything the spec's change did not affect exactly as it
952
+ reads — wording, ordering, the Notes someone wrote. This story may already be
953
+ planned around, and an unrelated rewrite costs its reader a re-read for nothing.
954
+ - Frontmatter: `exercises:` becomes exactly [{{p.requirements_csv}}] and
955
+ `spec_version:` becomes {{latest_version}}. Leave `id:`, `status:`,
956
+ `depends_on:`, `wave:`, `follows:`, `work_item:` and `created:` alone.
957
+ `depends_on:` and `wave:` are rewritten from the slicing by the kit, which KEEPS
958
+ the dependencies this story already carries rather than replacing them — an edge
959
+ somebody added by hand may name work outside this feature that the sync has no
960
+ view of. This story is in wave {{p.wave}}.
961
+ - WHERE IT GAINED IDS, the story is genuinely bigger: add a "## Requirements
962
+ delivered" line for each new id, extend "## Acceptance" to cover it, and revisit
963
+ the "## Story" sentence and the title so they still name what this slice
964
+ actually delivers. A story whose title stopped describing it is worse than one
965
+ that was never updated.
966
+ - WHERE AN ID IT ALREADY CITED HAS CHANGED WORDING, restate what this story
967
+ delivers of it and re-check every acceptance line that depended on the old
968
+ reading. That is the whole reason this story is being touched.
969
+ - "## Constraints" is re-derived, not preserved: read the spec's CN-/DC-/IC-/UX-
970
+ sections and its Experience Scope again and carry by id what binds the slice as
971
+ it now stands. A constraint that has left the spec leaves this section; one that
972
+ has arrived is added, with the acceptance line that shows it is met. Never add
973
+ these ids to `exercises:`.
974
+ - NO IMPLEMENTATION DETAIL, and nothing here changes the spec. If the change
975
+ reveals a problem with the contract, note it and leave the requirement alone.
976
+
977
+ # 10d. THE FIELDS THE GATES READ, written from the plan. See the block above 10a.
978
+ - id: stamp_story
979
+ action: execute_script
980
+ script: { ref: actions }
981
+ subcommand: story-stamp
982
+ # --title is a FALLBACK, not an override: story-stamp applies it only where the
983
+ # title is missing or still the template's placeholder, so the one the author wrote
984
+ # always wins. It exists because a story with no title reads as "(untitled)" in
985
+ # every later prompt and listing, and losing the name of the work is a needless
986
+ # way to make the backlog harder to read.
987
+ # `--depends-on` is a UNION with what the file already carries, not a replacement:
988
+ # the sync sees one feature's spec, and an edge added by hand may name work outside
989
+ # it. `--wave` is set outright — it is derived from the union, so re-deriving it is
990
+ # the only way it stays true.
991
+ args: ["--feature-dir", "{{feature_dir}}", "--story", "{{p.story}}", "--ids", "{{p.requirements_csv}}", "--version", "{{latest_version}}", "--follows", "{{p.follows}}", "--title", "{{p.group_title}}", "--depends-on", "{{p.depends_on_csv}}", "--wave", "{{p.wave}}"]
992
+
993
+ # 11. THE RELATION, READ THE OTHER WAY ROUND. Every story declares what it delivers in
994
+ # its own `exercises:`; the spec's Story Coverage block is that relation viewed from
995
+ # the requirements, regenerated wholesale from stories/ rather than patched. It sits
996
+ # between the AW:COVERAGE markers, which the ledger hash cuts out — so writing it is
997
+ # not an unledgered edit of the contract, and this workflow never has to re-stamp a
998
+ # document it did not change.
999
+ - id: coverage
1000
+ action: execute_script
1001
+ on_error: continue
1002
+ script: { ref: actions }
1003
+ subcommand: spec-coverage
1004
+ args: ["--feature-dir", "{{feature_dir}}"]
1005
+ outputs:
1006
+ - { name: uncovered_csv, from: "$.uncovered_csv" }
1007
+ - { name: delivered_csv, from: "$.delivered_csv" }
1008
+
1009
+ - id: resolve_post_hooks
1010
+ action: execute_script
1011
+ script: { ref: actions }
1012
+ subcommand: resolve-hooks
1013
+ args: ["--phase", "after_sync_stories"]
1014
+ outputs:
1015
+ - { name: after_hooks, from: "$.hooks" }
1016
+ - id: dispatch_post_hooks
1017
+ action: dispatch_hooks
1018
+ hooks_from_variable: after_hooks
1019
+
1020
+ outputs:
1021
+ feature_directory: "{{feature_dir}}"
1022
+ spec_file: "{{spec_file}}"
1023
+ spec_version: "{{latest_version}}"
1024
+ requirements_synced: "{{work_csv}}"
1025
+ synced_from: "{{work_note}}"
1026
+ versions_covered: "{{span_versions_csv}}"
1027
+ requirements_removed: "{{removed_csv}}" # named by a row, no longer in the spec (E1)
1028
+ requirements_unplaced: "{{unplaced_csv}}" # the grouping left these out — no story
1029
+ requirements_uncovered: "{{uncovered_csv}}" # no story at all, from the spec's own view
1030
+ requirements_delivered: "{{delivered_csv}}" # every story citing them is closed
1031
+ stories_written: "{{stories_csv}}"
1032
+ stories_untouched: "{{skipped_csv}}" # you declined these
1033
+ plan_summary: "{{create_count}} created, {{update_count}} updated, {{followon_count}} follow-on"
1034
+ plan_notes: "{{plan_notes}}"
1035
+ slicing_strategy: "{{strategy}} — {{strategy_note}}"
1036
+ slicing_split: "{{split_note}}" # one story asked for; what actually came out
1037
+ delivery_waves: "{{wave_digest}}" # wave 1 can be started now; the rest wait
1038
+ dependency_cycles: "{{cycle_csv}}" # edges dropped to break a declared cycle
1039
+ dependencies_unresolved: "{{unresolved_deps_csv}}" # edges naming no slice and no story
1040
+ next: "The backlog is in step with spec.md at {{latest_version}}, laid out over {{wave_count}} delivery wave(s). START WITH WAVE 1 — those stories have nothing in front of them, and stories sharing a wave have no ordering between them, so they can be picked up in parallel; a later wave becomes startable as its own dependencies land, not when the whole wave before it finishes. Make sure /appweaver plan (and /appweaver plan-ui where a frontend lane is declared) has been run for this spec version first — a story design reads the feature plan and stops against one the spec has moved past. Then run /appweaver story-plan <story-id> to design one story's execution, then implement. READ THE FIELDS ABOVE FIRST: dependency_cycles and dependencies_unresolved are ordering the run could not honour, and a story that reads as free to start may not be; requirements_removed leaves open stories orphaned and needs deciding by hand; requirements_unplaced and requirements_uncovered are requirements with no story, which no later gate will raise for you; stories_untouched are the ones you declined, and they stay at their old spec version, so this sync will offer them again next run."