aw-spec-cli 0.1.14 → 0.1.16

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 (32) hide show
  1. package/assets/.appweaver/STACK-PROFILES.md +1 -1
  2. package/assets/.appweaver/orchestrator/modules/spec-state-gate.orch.yaml +87 -0
  3. package/assets/.appweaver/orchestrator/orchestrator.py +5 -0
  4. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +10 -3
  5. package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +38 -11
  6. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +2 -1
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +672 -124
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +700 -72
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement-ui.orch.yaml +531 -135
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +573 -145
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +78 -14
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan-ui.orch.yaml +600 -0
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +721 -0
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +3 -3
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +119 -32
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-story-plan.orch.yaml +1147 -0
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-sync-stories.orch.yaml +1121 -0
  18. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +2564 -38
  19. package/assets/.appweaver/scripts/bash/aw-xref.sh +2 -1
  20. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +2458 -22
  21. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +2 -1
  22. package/assets/.appweaver/scripts/python/aw-payload-parity.py +158 -0
  23. package/assets/.appweaver/scripts/python/aw-xref.py +431 -48
  24. package/assets/.appweaver/templates/design-template.md +88 -54
  25. package/assets/.appweaver/templates/spec-template.md +12 -10
  26. package/assets/.appweaver/templates/story-template.md +83 -12
  27. package/assets/.appweaver/templates/ui-design-template.md +71 -39
  28. package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +19 -7
  29. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +35 -26
  30. package/package.json +1 -1
  31. package/assets/.appweaver/orchestrator/workflows/aw-spec-design-ui.orch.yaml +0 -386
  32. package/assets/.appweaver/orchestrator/workflows/aw-spec-design.orch.yaml +0 -382
@@ -1,33 +1,57 @@
1
1
  # =============================================================================
2
- # aw-spec-implement (orchestrator) — model-driven code generation from the DESIGN
2
+ # aw-spec-implement (orchestrator) — code generation from ONE STORY'S DESIGN
3
3
  #
4
- # The spec-anchored Implement phase: GENERATE the source files + paired tests for
5
- # the aggregate STRICTLY from its single high-level design.md — the mediator between
6
- # spec and code. Implement does NOT read spec.md: `analyze` already guaranteed the
7
- # design conforms to the spec (spec is the anchor), so the design is the complete,
8
- # authoritative source for WHAT to build. The target STACK is bound here from the
9
- # constitution + the active stack profile (the design is technology-agnostic).
10
- # Prereqs: run /appweaver design then analyze-design first (the gate conforms the
11
- # design to the spec + closes any open change), and scaffold the project once —
12
- # implement GENERATES into that project tree, so with no project there is nowhere
13
- # to place the files. /appweaver foundation unpacks one where a bundle ships for the
14
- # stack; a stack with no bundle scaffolds with its own tooling instead, and the gate
15
- # below accepts either, since it tests for a project rather than for a bundle.
4
+ # The executable end of the story loop: GENERATE the service lane's source files and
5
+ # paired tests for ONE STORY, strictly from the LATEST design pass in that story's own
6
+ # service design — specs/<F>/stories/<ID>/story-backend-plan.md.
16
7
  #
17
- # Next: Build (compile + fix; reconcile only high-level variations into design.md).
8
+ # IT BUILDS A SLICE, NOT A FEATURE. The old contract generated the whole aggregate from
9
+ # the feature design every run, which is only correct once: the second run rewrites the
10
+ # first story's code from a document that has since moved. A story design names the
11
+ # files it changes, so this phase edits an existing tree — creating only what the pass
12
+ # says is new — and its OKF write is a MERGE, never a replace.
18
13
  #
19
- # Launch: /appweaver implement [strategy] [guidance]
14
+ # DESIGN-ONLY, AND THE DESIGN IS THE STORY'S. spec.md is deliberately NOT a generation
15
+ # input: generating from the spec routes around every gate between them and makes the
16
+ # design advisory without anyone deciding that it should. The spec appears once, AFTER
17
+ # the code exists, in a read-only acceptance check that reports and never repairs.
18
+ #
19
+ # THE LATEST PASS WINS, AND A HAND EDIT TO IT IS HONOURED. Passes append; earlier ones
20
+ # are superseded history. The pass this run builds from is named in the prompt and in
21
+ # the outputs, so "which design produced this code" has one answer.
22
+ #
23
+ # INPUT CHANNELS, each with one job:
24
+ # story-backend-plan.md the LATEST pass = the complete WHAT for this slice
25
+ # stories/<ID>.md the story's acceptance criteria — what "done" means
26
+ # current code (OKF) what EXISTS, per lane, so this run edits rather than recreates
27
+ # interfaces.md the PUBLISHED surface the generated code must match
28
+ # adr.md what was DECIDED — binding unless a pass supersedes it
29
+ # constitution + rules HOW to build it (stack, layout, standards)
30
+ # backend-plan.md is NOT a generation input. The pass was designed inside it and is the
31
+ # concrete form of it; re-reading the plan here would let code drift from the pass back
32
+ # toward the architecture, with no gate between the two. It is read for two DERIVED
33
+ # facts only — the aggregate's cross-slice edges and its Overview prose — both of which
34
+ # are feature-level properties no single story can state.
35
+ #
36
+ # GATES (all loud, all before any write): the service lane declared with its source path;
37
+ # a scaffolded project under that path; an active feature; an OPEN story; and that story
38
+ # having a service design at all. A design behind the current spec, or a story the spec
39
+ # has moved past, is ANNOUNCED — never refused: the story is legitimately in flight, and
40
+ # the spec may have moved on ids it does not carry. The route out of both is named.
41
+ #
42
+ # Launch: /appweaver implement [<story-id>] [guidance] (asks when ambiguous)
43
+ # Prereq: /appweaver story-plan (the story's design) · Next: /appweaver build
20
44
  # =============================================================================
21
45
 
22
46
  name: aw-spec-implement
23
- description: Generate the aggregate's source files and paired tests STRICTLY from its design.md, bound to the project's stack. Model-driven, design-only (does not read the spec — analyze already conformed the design to it).
24
- version: 2.1.0
47
+ description: "Generate the service lane's source files and paired tests for ONE STORY, strictly from the LATEST design pass in stories/<ID>/story-backend-plan.md, bound to the declared backend stack and written only under that lane's source path. Model-driven and design-only — the spec is never a generation input; it appears once afterwards in a read-only acceptance check that reports and never repairs. Incremental by contract: it edits the feature's existing tree, creating only what the pass names as new, and MERGES the files it wrote into the feature's OKF concept rather than replacing the list."
48
+ version: 3.0.0
25
49
  agent_agnostic: true
26
50
  dialect: aw-spec.orchestrator/v1
27
51
 
28
52
  inputs:
29
53
  user_input:
30
- description: Optional implementation guidance (stack hints, focus area).
54
+ description: Optional story id and/or implementation guidance (e.g. "Order-001 keep the mapper hand-written").
31
55
  required: false
32
56
  bind: USER_INPUT
33
57
 
@@ -39,91 +63,361 @@ scripts:
39
63
  sh: .appweaver/scripts/bash/aw-xref.sh
40
64
  ps1: .appweaver/scripts/powershell/aw-xref.ps1
41
65
 
42
- # Sticky standards seed = the UNIVERSAL always-on rule only (ai-rules). The
43
- # FRAMEWORK / code-generation rule files are no longer hardcoded here — they are now
44
- # CONFIGURATION, loaded per-command from guardrails/projects/<project>/tech-rule-bindings.md
45
- # by the tech-select module (see the workflow's tech_select step), exactly as design's
46
- # technical rules are. Per the rule-partition, implement still generates from design.md +
47
- # these HOW-to-build standards and loads NO domain rule file — the tech map lists only
48
- # technical files. The architecture principles come via constitution.technical.md
49
- # (referred to directly, as-is — NOT mapped); the structure via design.md. Component
50
- # rule docs are NOT loaded here — implement codes against a component's PORT; adding the
51
- # component itself is a separate step, /appweaver add-component. (missing files skipped)
66
+ # Sticky standards seed = the UNIVERSAL always-on rule only (ai-rules). The FRAMEWORK /
67
+ # code-generation rule files are CONFIGURATION, loaded per-command from
68
+ # guardrails/projects/<project>/tech-rule-bindings.md under "## /aw-spec-implement" by
69
+ # the tech-select module. Per the rule-partition this phase loads NO domain rule file —
70
+ # the domain reached it through spec → feature plan → the story's design pass. The
71
+ # architecture principles arrive via constitution.technical.md (referred to directly,
72
+ # as-is — NOT mapped). Component rule docs are NOT loaded here: implement codes against
73
+ # a component's PORT, and installing the component is /appweaver add-component.
52
74
  standards:
53
75
  - { path: .appweaver/ai-rules.md } # kit-resident: the universal operating rules, every flow
54
76
 
77
+ vars:
78
+ feature_dir: ""
79
+ spec_file: ""
80
+ be_plan: ""
81
+ be_plan_exists: "false"
82
+ # story resolution
83
+ any_open: "false"
84
+ open_ids: "[]"
85
+ digest: ""
86
+ resolved: "false"
87
+ needs_pick: "false"
88
+ pick_reason: ""
89
+ picked_story: ""
90
+ story: ""
91
+ story_path: ""
92
+ story_open: "false"
93
+ status_msg: ""
94
+ story_title: ""
95
+ exercises_csv: ""
96
+ constraints_csv: ""
97
+ # UNASKED IS NOT UNMATCHED. detect-project answers true whenever no --ecosystem was
98
+ # given, and the gate below only reads it on the repo-scope fallback — so the default
99
+ # has to be permissive or a probe that could not run would refuse a scaffolded lane.
100
+ ecosystem_match: "true"
101
+ project_scope: ""
102
+ project_markers: ""
103
+ current_version: ""
104
+ stale_csv: ""
105
+ stale_note: ""
106
+ story_dir: ""
107
+ adr_path: ""
108
+ if_path: ""
109
+ # the story's SERVICE design
110
+ design_doc: ""
111
+ design_exists: "false"
112
+ design_pass: "0"
113
+ design_spec_version: ""
114
+ design_stale: "false"
115
+ # the analyze-design verdict — law 40's evidence. certified defaults FALSE so a probe that
116
+ # could not run fails CLOSED, and the message default carries a route for the same reason.
117
+ pass_plan_hash: ""
118
+ gate_status: ""
119
+ gate_certified: "false"
120
+ gate_aborted: "false"
121
+ gate_gap_refs: ""
122
+ gate_report: ""
123
+ gate_lane_sha: ""
124
+ gate_msg: "The analyze-design verdict could not be read, so this run cannot tell whether the feature plans were ever certified. Run /appweaver analyze-design, then re-run."
125
+ # the feature's current code, per lane (OKF)
126
+ okf_concept: ""
127
+ coverage_digest: ""
128
+ be_source_csv: ""
129
+ be_source_count: "0"
130
+ fe_source_count: "0"
131
+ # generation + enrichment
132
+ files: ""
133
+ xref_wrote: ""
134
+ xref_edge_count: "0"
135
+ okf_overview: ""
136
+ accept_ok: "true"
137
+ accept_gaps: ""
138
+ accept_note: ""
139
+
55
140
  workflow:
56
141
 
57
142
  # 0. TECHNICAL rule files → sticky standards (configuration, not hardcoded).
58
- # tech-select reads guardrails/projects/<project>/tech-rule-bindings.md under
59
- # "## /aw-spec-implement" and loads that set into the sticky standards the
60
- # agent_generate step ALWAYS prepends. No domain rule file is loaded here — the
61
- # tech map lists only technical files (rule-partition intact). No blocking gate:
62
- # an unconfigured project soft-continues (constitution.technical.md still carries
63
- # the tech base, referred to directly below).
143
+ # No blocking gate: an unconfigured project soft-continues, because
144
+ # constitution.technical.md still carries the technology base.
64
145
  - id: tech_select
65
146
  action: include
66
147
  module: .appweaver/orchestrator/modules/tech-select.orch.yaml
67
148
  with: { command: aw-spec-implement }
68
149
 
69
- # 1. Resolve the current feature + its design; require the DESIGN (implement's
70
- # only input). No spec — analyze already conformed the design to the spec.
150
+ # 1. The active feature. spec.md and backend-plan.md are resolved here but neither is
151
+ # a generation input — see the header for what each is actually used for.
71
152
  - id: feature_paths
72
153
  action: execute_script
73
154
  on_error: continue # never crash raw on 'no feature set' — the gate below explains it
74
155
  script: { ref: actions }
75
156
  subcommand: feature-paths
76
157
  outputs:
77
- - { name: feature_dir, from: "$.feature_dir" }
78
- - { name: design_doc, from: "$.design_file" }
79
- - { name: design_exists, from: "$.design_exists" }
80
-
158
+ - { name: feature_dir, from: "$.feature_dir" }
159
+ - { name: spec_file, from: "$.spec_file" }
160
+ - { name: be_plan, from: "$.backend_plan_file" }
161
+ - { name: be_plan_exists, from: "$.backend_plan_exists" }
81
162
  - id: require_feature
82
163
  action: verify_context
83
164
  require_non_empty: feature_dir
84
165
  on_empty: stop
85
166
  on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run implement."
86
167
 
87
- - id: verify_design
168
+ # 1b. THE LEDGER FENCE. Generation is design-only by contract, but this run reads the
169
+ # spec twice all the same: stale_csv below is computed against the ledger, and the
170
+ # acceptance pass at the end reads spec.md as the wording the code is judged
171
+ # against. Both are worthless if the version does not name the contents.
172
+ - id: spec_state_guard
173
+ action: include
174
+ module: .appweaver/orchestrator/modules/spec-state-gate.orch.yaml
175
+ with: { command: "/appweaver implement" }
176
+
177
+ # 2. WHICH STORY. Same resolution story-plan uses: an explicit id in the input wins,
178
+ # anything else (nothing named, an unknown id, several) becomes a pick from the open
179
+ # backlog. One scan returns every fact the gates below read.
180
+ - id: story_scan
181
+ action: execute_script
182
+ script: { ref: actions }
183
+ subcommand: story-plan-check
184
+ args: ["--feature-dir", "{{feature_dir}}", "--input", "{{USER_INPUT}}"]
185
+ outputs:
186
+ - { name: any_open, from: "$.any_open" }
187
+ - { name: open_ids, from: "$.open_ids" }
188
+ - { name: digest, from: "$.digest" }
189
+ - { name: resolved, from: "$.resolved" }
190
+ - { name: needs_pick, from: "$.needs_pick" }
191
+ - { name: pick_reason, from: "$.pick_reason" }
192
+ - { name: story, from: "$.story" }
193
+ - { name: story_path, from: "$.story_path" }
194
+ - { name: story_open, from: "$.story_open" }
195
+ - { name: status_msg, from: "$.status_msg" }
196
+ - { name: story_title, from: "$.story_title" }
197
+ - { name: exercises_csv, from: "$.exercises_csv" }
198
+ - { name: constraints_csv, from: "$.constraints_csv" }
199
+ - { name: current_version, from: "$.current_version" }
200
+ - { name: stale_csv, from: "$.stale_csv" }
201
+ - { name: stale_note, from: "$.stale_note" }
202
+ - { name: story_dir, from: "$.story_dir" }
203
+ - { name: adr_path, from: "$.adr_path" }
204
+ - { name: if_path, from: "$.if_path" }
205
+ - { name: design_doc, from: "$.be_doc_path" }
206
+ - { name: design_exists, from: "$.be_doc_exists" }
207
+ - { name: design_pass, from: "$.be_last_pass" }
208
+ - { name: design_spec_version, from: "$.be_doc_spec_version" }
209
+ - { name: design_stale, from: "$.be_doc_stale" }
210
+ - { name: pass_plan_hash, from: "$.be_pass_plan_hash" }
211
+ - id: require_open_backlog
212
+ action: verify_context
213
+ require_true: any_open
214
+ on_false: stop
215
+ on_false_message: "No open stories for this feature — there is nothing to implement. Stories are minted by /appweaver specify (bootstrap) and /appweaver sync-stories (when the spec moves)."
216
+ - id: ask_story
217
+ condition: "{{needs_pick}} == true"
218
+ action: ask
219
+ prompt: |
220
+ Which story is this implementation for? ({{pick_reason}})
221
+
222
+ {{digest}}
223
+ options_from: "{{open_ids}}"
224
+ answer_key: picked_story
225
+ answer_var: picked_story
226
+ - id: story_pick_check
227
+ condition: "{{needs_pick}} == true"
228
+ action: execute_script
229
+ script: { ref: actions }
230
+ subcommand: story-plan-check
231
+ args: ["--feature-dir", "{{feature_dir}}", "--story", "{{picked_story}}"]
232
+ outputs:
233
+ - { name: resolved, from: "$.resolved" }
234
+ - { name: story, from: "$.story" }
235
+ - { name: story_path, from: "$.story_path" }
236
+ - { name: story_open, from: "$.story_open" }
237
+ - { name: status_msg, from: "$.status_msg" }
238
+ - { name: story_title, from: "$.story_title" }
239
+ - { name: exercises_csv, from: "$.exercises_csv" }
240
+ - { name: constraints_csv, from: "$.constraints_csv" }
241
+ - { name: stale_csv, from: "$.stale_csv" }
242
+ - { name: stale_note, from: "$.stale_note" }
243
+ - { name: story_dir, from: "$.story_dir" }
244
+ - { name: design_doc, from: "$.be_doc_path" }
245
+ - { name: design_exists, from: "$.be_doc_exists" }
246
+ - { name: design_pass, from: "$.be_last_pass" }
247
+ - { name: design_spec_version, from: "$.be_doc_spec_version" }
248
+ - { name: design_stale, from: "$.be_doc_stale" }
249
+ - { name: pass_plan_hash, from: "$.be_pass_plan_hash" }
250
+ - id: require_story
251
+ action: verify_context
252
+ require_true: resolved
253
+ on_false: stop
254
+ on_false_message: |
255
+ Could not resolve a story from '{{picked_story}}'. Open stories:
256
+ {{digest}}
257
+ Re-run implement naming one of them.
258
+ - id: require_open_story
259
+ action: verify_context
260
+ require_true: story_open
261
+ on_false: stop
262
+ on_false_message: "{{status_msg}}"
263
+
264
+ # 3. THE STORY MUST HAVE A SERVICE DESIGN. This phase generates from a design pass and
265
+ # from nothing else, so an undesigned story has no input — and the fix is one
266
+ # command, not a judgement call.
267
+ - id: require_design
88
268
  action: verify_context
89
269
  require_true: design_exists
90
270
  on_false: stop
91
- on_false_message: "No design.md for the current feature. Run /appweaver design then /appweaver analyze-design first — implement generates STRICTLY from the conformed design."
271
+ on_false_message: "Story {{story}} has no service design — {{story_dir}}/story-backend-plan.md does not exist. Run /appweaver story-plan {{story}} first: it designs the slice against the feature's backend plan, the current code and the published interfaces. implement generates from that pass and has no other input."
272
+
273
+ # 3a. A design behind the spec is ANNOUNCED, not refused. The story is legitimately in
274
+ # flight and the spec may have moved on ids it does not carry; stale_csv below is
275
+ # the sharper signal, because it names only the ids THIS story actually carries.
276
+ - id: design_stale_notice
277
+ condition: "{{design_stale}} == true"
278
+ action: verify_context
279
+ require_value: design_stale
280
+ equals: "false"
281
+ on_mismatch: continue
282
+ on_mismatch_message: "Design pass {{design_pass}} was written against spec {{design_spec_version}} and the spec is now at {{current_version}}. Generating from it is not wrong — the move may not touch this story — but if it does, the code inherits a contract that no longer holds. Re-run /appweaver story-plan {{story}} to design against the current spec, or check the ids named below."
283
+ # stale_csv is a STRING, so require_value/equals compares cleanly and needs no
284
+ # condition. design_stale above is a JSON boolean, which require_value stringifies as
285
+ # 'True' — it can never equal 'false', so that one is gated by `condition:` and the
286
+ # value gate is only the vehicle that prints (law 43).
287
+ - id: story_stale_notice
288
+ action: verify_context
289
+ require_value: stale_csv
290
+ equals: ""
291
+ on_mismatch: continue
292
+ on_mismatch_message: "The spec has moved on ids THIS story carries: {{stale_csv}}. Whatever this run generates for them realises superseded wording. Consider /appweaver sync-stories to bring the backlog back in step, then /appweaver story-plan {{story}}, before building."
293
+
294
+ # ═══════ 3a-bis. LAW 40, THE LAST HOP — the plan this pass was designed against ═══════
295
+ # THREE HASHES, not two. story-plan proved the feature plan was certified in the shape it
296
+ # was in when this pass was designed, and story-plan-doc stamped that certified hash into
297
+ # the pass header. But a pass is written once and read forever: comparing only the CURRENT
298
+ # plan to the CURRENT verdict would let a pass designed against plan H1 still generate
299
+ # after the plan was edited to H2 and re-gated — which is precisely the case law 40 names,
300
+ # one level below where it was being checked.
301
+ # pass hash what this pass was designed against (the story pass header)
302
+ # verdict hash what analyze-design last certified (the gate verdict)
303
+ # A MISMATCH IS A STOP, at the same severity as never-gated: continuing ships code built
304
+ # to an architecture nobody approved. Per lane, and only for the lane this command builds.
305
+ - id: gate_verdict
306
+ action: execute_script
307
+ on_error: continue
308
+ script: { ref: actions }
309
+ subcommand: gate-read
310
+ args: ["--feature-dir", "{{feature_dir}}", "--gate", "analyze-design"]
311
+ outputs:
312
+ - { name: gate_status, from: "$.status" }
313
+ - { name: gate_certified, from: "$.certified" }
314
+ - { name: gate_aborted, from: "$.aborted" }
315
+ - { name: gate_gap_refs, from: "$.gap_refs_csv" }
316
+ - { name: gate_report, from: "$.report" }
317
+ - { name: gate_lane_sha, from: "$.backend_sha256" }
318
+
319
+ # Three reasons, three routes; ordered sets, last match wins. Aborted is last because it
320
+ # also carries status "fail" — the red gate_result_open writes BEFORE any analysis — and
321
+ # sending someone to read gaps that were never found is the one wrong answer here.
322
+ - id: gate_msg_never
323
+ action: set
324
+ vars: { gate_msg: "The feature plans for this feature have never been certified: there is no analyze-design verdict beside them. Generation builds from a story pass that sits under an unapproved architecture. Run /appweaver analyze-design, then /appweaver story-plan {{story}}, then re-run /appweaver implement." }
325
+ - id: gate_msg_red
326
+ condition: "{{gate_status}} == fail"
327
+ action: set
328
+ vars: { gate_msg: "The feature plans do NOT conform to the spec: analyze-design's last verdict is FAIL on {{gate_gap_refs}} (report: {{gate_report}}). Close those gaps before generating code beneath them, then re-run /appweaver implement." }
329
+ - id: gate_msg_aborted
330
+ condition: "{{gate_aborted}} == true"
331
+ action: set
332
+ vars: { gate_msg: "The last analyze-design run STARTED and never reached a verdict — the red on disk is the one it writes before any analysis, so nothing was found and there are no gaps to read. The plans are UNCERTIFIED, not rejected. Run /appweaver analyze-design to completion, then re-run /appweaver implement." }
333
+
334
+ - id: gate_certified_guard
335
+ action: verify_context
336
+ require_true: gate_certified
337
+ on_false: stop
338
+ on_false_message: "{{gate_msg}}"
339
+
340
+ # An EMPTY pass hash fails this, and that is correct: a pass written before the hash was
341
+ # stamped cannot say which plan shape it was designed against, so nothing can prove it was
342
+ # designed against the approved one. The route is the same either way — re-design.
343
+ - id: pass_plan_shape_guard
344
+ action: verify_context
345
+ require_value: pass_plan_hash
346
+ equals: "{{gate_lane_sha}}"
347
+ on_mismatch: stop
348
+ on_mismatch_message: "Design pass {{design_pass}} for story {{story}} was designed against a backend plan that is NOT the one analyze-design currently certifies — the plan has been edited and re-gated since, or this pass predates plan-hash stamping and cannot say what it was designed against. Generating from it would build to an architecture nobody approved for this shape of the plan. Re-run /appweaver story-plan {{story}} to design against the currently certified plan, then re-run /appweaver implement."
92
349
 
93
- # 1a. Require the SERVICE LANE and its source path. Implement writes code, and
94
- # the lane's root is the subtree it is allowed to write into — so this runs
95
- # before the foundation gate, which searches that subtree for a project.
350
+ # 4. THE SERVICE LANE and its source path. This phase writes code, and the lane's root
351
+ # is the only subtree it may write into — so this runs before the project gate,
352
+ # which searches that subtree.
96
353
  - id: lane_gate
97
354
  action: include
98
355
  module: .appweaver/orchestrator/modules/lane-gate.orch.yaml
99
356
  with: { require: backend, command: "/appweaver implement" }
100
357
 
101
- # 1b. Require the PROJECT — a scaffolded, compiling tree — to be present. Implement
102
- # GENERATES the aggregate's code INTO it; with no project there is nowhere to place
103
- # the files. Deterministic structure gate: searches the LANE'S OWN subtree for an
104
- # ecosystem manifest (.sln/.csproj, pom.xml, build.gradle, package.json, go.mod,
105
- # Cargo.toml, pyproject.toml), falling back to the whole repo for the layout that
106
- # keeps its manifest beside the lane root rather than inside it, and ignoring the
107
- # framework/spec/guardrails/foundations dirs — never trusts the model for this.
108
- # How the project got there is not asked: `foundation` unpacked it, or the team
109
- # scaffolded it with the stack's own tooling.
358
+ # 5. Require the PROJECT — a scaffolded, compiling tree. Implement GENERATES into it;
359
+ # with no project there is nowhere to place the files. Deterministic structure gate:
360
+ # searches the LANE'S OWN subtree for an ecosystem manifest, falling back to the
361
+ # whole repo for the layout that keeps its manifest beside the lane root rather than
362
+ # inside it. How the project got there is not asked.
363
+ # THE FALLBACK IS DELIBERATE AND IT NEEDED A DISCRIMINATOR. Searching the whole repo
364
+ # when the lane's own subtree has no manifest is right — a .NET .sln legitimately sits
365
+ # ABOVE the src/ it builds — but it answered with whatever manifest it found first, so
366
+ # in a two-lane repo where only the frontend is scaffolded, the SERVICE lane's gate
367
+ # passed on the frontend's package.json. Which is exactly the failure implement-ui's
368
+ # `project_scope == lane` rule was written to prevent, one lane over.
369
+ # `--ecosystem` is the fix, and NOT implement-ui's rule: this passes the value
370
+ # lane-gate has already bound from stacks.json (that include runs above), so the repo
371
+ # -scope hit is accepted only when the manifest is one this lane's ecosystem knows.
372
+ # Nothing here re-derives it — a second guess is a second thing to keep in step.
110
373
  - id: detect_foundation
111
374
  action: execute_script
112
375
  script: { ref: actions }
113
376
  subcommand: detect-project
114
- args: ["--root", "{{lane_source_path}}"]
377
+ args: ["--root", "{{lane_source_path}}", "--ecosystem", "{{lane_ecosystem}}"]
115
378
  outputs:
116
379
  - { name: foundation_present, from: "$.scaffolded" }
117
380
  - { name: project_markers, from: "$.markers" }
118
381
  - { name: project_scope, from: "$.scope" }
119
-
382
+ - { name: ecosystem_match, from: "$.ecosystem_match" }
120
383
  - id: foundation_gate
121
384
  action: verify_context
122
385
  require_true: foundation_present
123
386
  on_false: stop
124
- on_false_message: "No project found under the {{lane_id}} lane's source path `{{lane_source_path}}`, nor anywhere else in this repository. Implement GENERATES the aggregate's code into an existing project tree, so one has to be scaffolded first: run /appweaver foundation <FoundationName> <AppName> where a foundation bundle ships for this stack, or create the project with the stack's own tooling under that path. Any ecosystem manifest marks it — .sln/.csproj, pom.xml, build.gradle(.kts), package.json, go.mod, Cargo.toml or pyproject.toml."
387
+ on_false_message: "No project found under the {{lane_id}} lane's source path `{{lane_source_path}}`, nor anywhere else in this repository. Implement GENERATES this story's code into an existing project tree, so one has to be scaffolded first: run /appweaver foundation <FoundationName> <AppName> where a foundation bundle ships for this stack, or create the project with the stack's own tooling under that path. Any ecosystem manifest marks it — .sln/.csproj, pom.xml, build.gradle(.kts), package.json, go.mod, Cargo.toml or pyproject.toml."
388
+
389
+ # 5b. THE GATE PASSED ON SOMEBODY ELSE'S PROJECT. A repo-scope hit whose manifests are
390
+ # none of this lane's ecosystem means the search fell back past the lane root and
391
+ # found the OTHER lane's project. STOPS rather than warns: the next thing this run
392
+ # does is generate {{lane_language}} code, and it has just been told there is nowhere
393
+ # of that kind to put it — carrying on writes a service tree inside a frontend, or
394
+ # into the repo root. Scoped to the fallback, so the deliberate manifest-above-the-lane
395
+ # layout (a .sln over src/) still passes: that hit IS this lane's ecosystem.
396
+ - id: foundation_ecosystem_gate
397
+ condition: "{{project_scope}} == repo"
398
+ action: verify_context
399
+ require_true: ecosystem_match
400
+ on_false: stop
401
+ on_false_message: "The {{lane_id}} lane's source path `{{lane_source_path}}` holds no project, and the repo-wide fallback found only manifests that are not {{lane_ecosystem}}: {{project_markers}}. That is the OTHER lane's project — this gate passing on it would send {{lane_language}} code into a tree built for something else. Scaffold this lane: /appweaver foundation <FoundationName> <AppName> where a bundle ships for {{lane_profile}}, or create the project with the stack's own tooling under `{{lane_source_path}}`. (A manifest deliberately kept ABOVE the lane root — a .sln over src/ — is accepted, because it is this lane's ecosystem.)"
402
+
403
+ # 6. WHAT ALREADY EXISTS, from the feature's OKF concept, split by lane. This is what
404
+ # makes the run incremental: the pass names the files it changes, and this names the
405
+ # files that are there to change. Advisory — the first story of a greenfield feature
406
+ # has no concept and generates into a clean tree.
407
+ - id: coverage
408
+ action: execute_script
409
+ on_error: continue
410
+ script: { ref: xref }
411
+ subcommand: concept-coverage
412
+ args: ["--feature-dir", "{{feature_dir}}", "--ids", "{{exercises_csv}}"]
413
+ outputs:
414
+ - { name: okf_concept, from: "$.concept" }
415
+ - { name: coverage_digest, from: "$.digest" }
416
+ - { name: be_source_csv, from: "$.backend_csv" }
417
+ - { name: be_source_count, from: "$.backend_count" }
418
+ - { name: fe_source_count, from: "$.frontend_count" }
125
419
 
126
- # 2. Pre-execution hooks.
420
+ # 7. Pre-execution hooks.
127
421
  - id: resolve_pre_hooks
128
422
  action: execute_script
129
423
  script: { ref: actions }
@@ -135,74 +429,115 @@ workflow:
135
429
  action: dispatch_hooks
136
430
  hooks_from_variable: before_hooks
137
431
 
138
- # 3. Model-driven generation: design.md → source files + paired tests.
139
- # The design constrains WHAT to build; the constitution/stack profile fixes HOW.
140
- # The spec is deliberately NOT in context — the design is the single source here.
432
+ # ═══════════════════ GENERATION — one story's slice, from one pass ═══════════
433
+ # The pass constrains WHAT to build; the constitution, the lane and the loaded
434
+ # standards fix HOW. spec.md is deliberately absent: the design is the single source
435
+ # here, and the acceptance check below is where the spec gets its say.
141
436
  - id: ctx_generate
142
437
  action: build_context
438
+ vars:
439
+ story_id: "{{story}}"
440
+ story_title: "{{story_title}}"
441
+ exercises_csv: "{{exercises_csv}}"
442
+ constraints_csv: "{{constraints_csv}}"
443
+ pass_number: "{{design_pass}}"
444
+ coverage_digest: "{{coverage_digest}}"
445
+ source_files_csv: "{{be_source_csv}}"
446
+ source_file_count: "{{be_source_count}}"
447
+ stale_note: "{{stale_note}}"
143
448
  context:
144
- - { path: "{{design_doc}}" }
449
+ - { path: "{{design_doc}}" } # the story's SERVICE design — the LATEST pass is the input
450
+ - { path: "{{story_path}}" } # the story's acceptance criteria — what "done" means
451
+ - { path: "{{if_path}}", optional: true } # the PUBLISHED surface this code must match
452
+ - { path: "{{adr_path}}", optional: true } # ratified decisions — binding unless a pass supersedes them
145
453
  - { path: .appweaver/memory/constitution.technical.md, optional: true } # technology base ONLY — no domain
146
454
  - id: generate
147
455
  action: agent_generate
148
- tools: "Read,Write,Edit"
456
+ tools: "Read,Write,Edit" # the existing code arrives as {{source_files_csv}} paths, not inlined
149
457
  permission_mode: acceptEdits
150
458
  task: |
151
- Generate the production code AND paired tests for this aggregate STRICTLY from
152
- its design.md (in context). The design is the complete, authoritative source of
153
- WHAT to build — do NOT consult or infer from the spec; `analyze` already
154
- conformed the design to the spec. This phase is the HOME of the
155
- technology-specific, solution-wide code-generation rules. Guidance: {{USER_INPUT}}
459
+ Implement story {{story_id}} — {{story_title}} — in the service lane, from
460
+ "## Design pass {{pass_number}}" of the story design in context. Guidance: {{USER_INPUT}}
461
+
462
+ THAT PASS IS THE ONLY SOURCE OF WHAT TO BUILD. It is the LAST pass in the
463
+ document; every earlier pass is superseded history, and any of them may carry
464
+ hand edits that were deliberate. Read the last one and build exactly it. Do NOT
465
+ consult or infer from spec.md — it is not in context on purpose: the design
466
+ already went through the gates that conform it, and generating from the spec
467
+ routes around all of them.
468
+
469
+ THIS IS ONE SLICE OF AN EXISTING FEATURE, NOT THE WHOLE FEATURE.
470
+ - The pass's "### Changes" section names, file by file, what to create and what
471
+ to modify. Create ONLY the files it names as new; modify the named existing
472
+ files IN PLACE. Every other file in this feature belongs to work that is
473
+ already done — do not regenerate, reformat or "tidy" it.
474
+ - The story delivers {{exercises_csv}} and must satisfy {{constraints_csv}} — the
475
+ complete set of bounds the spec declares for this aggregate, stamped into the
476
+ story's frontmatter by the kit rather than transcribed by anyone. Satisfy the
477
+ CN-, DC- and IC- among them. IGNORE the UX-: those are what the UI lane realises,
478
+ and a service-side obligation is stated as a CN- by design. Anything outside those
479
+ ids is another story's work, even if you can see it is
480
+ missing. If the slice genuinely cannot be built without touching shared
481
+ structure, make the smallest change that works and SAY SO in your summary.
482
+ - The code that already exists for this feature, per lane, is below. The service
483
+ files ({{source_file_count}} on disk) are yours to read before you edit; the UI
484
+ list is there so you do not change a surface that already has a client.
485
+
486
+ {{coverage_digest}}
487
+
488
+ {{source_files_csv}}
489
+
490
+ BIND THE STACK: this lane is `{{lane_id}}` — {{lane_title}}, language
491
+ {{lane_language}} ({{lane_ecosystem}}), declared in .appweaver/stacks.json and
492
+ compiled with `{{lane_build}}`, tested with `{{lane_test}}`. Generate in THAT
493
+ language and framework and no other, using its idioms, its project layout and its
494
+ test framework. Take persistence, messaging and the remaining technology choices
495
+ from the constitution's technology base + the loaded standards; the design is
496
+ technology-agnostic, so do not re-decide anything already fixed there. If the
497
+ loaded standards describe a DIFFERENT stack than the one above, the lane wins on
498
+ language and layout — follow the standards' INTENT and STOP to surface the
499
+ mismatch in your summary.
500
+
501
+ WRITE ONLY UNDER `{{lane_source_path}}` — the source path the `{{lane_id}}` lane
502
+ declares. That subtree is this lane's; a file outside it belongs to another lane
503
+ or to no lane at all. Do not create a parallel source tree because the layout
504
+ inside it is unfamiliar; if the pass needs a place this subtree has no home for,
505
+ STOP and surface it.
506
+
156
507
  Rules:
157
- - THE DESIGN IS THE ONLY DOMAIN INPUT. You have design.md + framework/code
158
- standards, and NO domain rule files. Introduce NO domain rule, identifier,
159
- terminology, or behaviour that the design does not state — everything domain
160
- already passed through spec → design. If a domain specific seems missing, STOP
161
- and surface it; never invent it from memory.
162
- - BIND THE STACK: this lane is `{{lane_id}}` — {{lane_title}}, language
163
- {{lane_language}} ({{lane_ecosystem}}), declared in .appweaver/stacks.json and
164
- compiled with `{{lane_build}}`, tested with `{{lane_test}}`. Generate in THAT
165
- language and framework and no other, using its idioms, its project layout and
166
- its test framework. Take persistence, messaging and the remaining technology
167
- choices from the constitution's technology base + the loaded standards; the
168
- design is technology-agnostic, so do not re-decide anything already fixed there.
169
- If the loaded standards describe a DIFFERENT stack than the one above, the lane
170
- wins on language and layout — the standards' stack-specific mechanics do not
171
- apply, so follow their INTENT and STOP to surface the mismatch in your summary.
172
- - WRITE ONLY UNDER `{{lane_source_path}}` — the source path the `{{lane_id}}`
173
- lane declares in .appweaver/stacks.json. That subtree is this lane's, and a file
174
- outside it belongs to another lane or to no lane at all. Do not create a
175
- parallel source tree because the layout inside it is unfamiliar; if the design
176
- needs a place this subtree has no home for, STOP and surface it.
508
+ - THE PASS IS THE ONLY DOMAIN INPUT. You have it, the story, and framework/code
509
+ standards — and NO domain rule files. Introduce NO domain rule, identifier,
510
+ terminology or behaviour the pass does not state. If a domain specific seems
511
+ missing, STOP and surface it; never invent it from memory.
177
512
  - Follow the project's folder/layer convention + the loaded coding-standards,
178
- dependency-policy and testing standards for HOW to structure, name, and place
179
- files WITHIN that subtree — the design says WHAT to build; these standards
180
- (identical for every feature) say HOW.
181
- - COMPONENTS: when the design's External Integrations table depends on a framework
182
- component (e.g. `IEmailService`, `IJobScheduler`), code against that component's
183
- ABSTRACTION / port as the design declares — do NOT hand-roll it. Installing the
184
- component into the project (packages, provider wiring, config) is a SEPARATE step:
185
- /appweaver add-component <Component>. If the port isn't present yet, code to
186
- it and note add-component as the prerequisite; do not load component rule docs here.
187
- - Realise EVERY design element as code, changing nothing about WHAT it specifies:
188
- one file per Domain type / Application use-case / API endpoint / Infrastructure
189
- adapter, placed by layer. Preserve the ubiquitous-language names verbatim.
190
- - Enforce EXACTLY as the design states: every Domain invariant, Application
191
- handler orchestration, and API error contract; the cross-cutting constraints in
192
- the Cross-Cutting Concerns table (audit / retention / data-exposure — the
193
- CN-/DC- realisations); and each integration contract (IC-) in the External
194
- Integrations table. Implement every port the design declares.
195
- - Author a paired test for every design behaviour / use-case / endpoint; derive
196
- the cases from the design's pre/post/invariant contracts and cite the spec id
197
- (FR / BR / CN / DC / IC / US) from the design's Traceability section. Include tests
198
- for the constraints — e.g. an audit record is emitted, a DC-restricted field is
199
- NOT exposed on the public surface, an IC- projection round-trips.
200
- - Do NOT add behaviour the design does not state, and do NOT omit any it does.
201
- If the design looks wrong or incomplete, STOP and surface it (it is fixed in
202
- design/analyze) — never compensate from the spec or from memory.
203
- - This aggregate OWNS its files; never regenerate a file another feature owns.
204
- - Where generated code depends on a design Assumption (DA-n) or Workaround
205
- (WA-n), mark it with a comment citing that id so it is easy to revisit later.
513
+ dependency-policy and testing standards for HOW to structure, name and place
514
+ files WITHIN that subtree — the pass says WHAT to build; these say HOW.
515
+ - Realise the pass's "### Approach" and "### Changes" exactly, changing nothing
516
+ about WHAT they specify, and preserve every ubiquitous-language name verbatim.
517
+ - INTERFACES: where the pass's "### Interfaces" describes a published surface,
518
+ the entry in interfaces.md (in context) is the contract — match its route,
519
+ shape and semantics. Where the log and the pass disagree, the pass is the
520
+ newer word and the log entry it supersedes is named in it; if neither says
521
+ which, STOP and surface it rather than picking.
522
+ - COMPONENTS: where the pass depends on a framework component (e.g.
523
+ `IEmailService`, `IJobScheduler`), code against that component's ABSTRACTION /
524
+ port as the pass declares — do NOT hand-roll it. Installing the component into
525
+ the project (packages, provider wiring, config) is a SEPARATE step:
526
+ /appweaver add-component <Component>. If the port is not present yet, code to
527
+ it and note add-component as the prerequisite.
528
+ - ADRs are DECIDED — follow them. The pass may propose a superseding one; a
529
+ proposal is not yet ratified, so follow the pass and change nothing else.
530
+ - Author a paired test for every acceptance criterion in the story and every
531
+ behaviour the pass's "### Tests" section names, and cite the requirement id
532
+ ({{exercises_csv}}) each test exists for. Include the constraint tests the
533
+ pass names ({{constraints_csv}}) — e.g. an audit record is emitted, a
534
+ restricted field is NOT exposed on the public surface.
535
+ - Do NOT add behaviour the pass does not state, and do NOT omit any it does. If
536
+ the pass looks wrong or incomplete, STOP and surface it — it is fixed by
537
+ re-running /appweaver story-plan, never compensated for here from the spec or
538
+ from memory.
539
+ - This feature OWNS its files; never regenerate a file another feature owns.
540
+ {{stale_note}}
206
541
  outputs:
207
542
  - { name: files, from: "$.files" }
208
543
 
@@ -210,63 +545,153 @@ workflow:
210
545
  action: verify_context
211
546
  require_non_empty: files
212
547
  on_empty: stop
213
- on_empty_message: "No files generated. Ensure /appweaver design produced a filled design.md (not the empty template) and passed analyze, then re-run."
214
-
215
- # 3b. OKF knowledge + cross-references (deterministic, design-derived). Greenfield's
216
- # entry into the shared OKF bundle: author THIS aggregate's concept (its source_files
217
- # = the files just generated), then record its cross-slice edges — write a
218
- # Cross-References row into every SIBLING aggregate it uses (from design.md §1
219
- # References-by-id + its External integrations table), reconcile-on-write so a dropped
220
- # dependency self-heals and a used sibling with no concept yet is stubbed. Finally
221
- # author the concept's Overview prose (3c). This is enrichment downstream of a
222
- # successful generate — it never re-decides WHAT was built.
548
+ on_empty_message: "No files generated for story {{story}}. Check that design pass {{design_pass}} in {{design_doc}} actually describes changes (a pass whose '### Changes' section is empty has nothing to build), then re-run — or re-run /appweaver story-plan {{story}} to design the slice properly first."
549
+
550
+ # 8. OKF — MERGE, never replace. This run wrote ONE STORY's files; the feature's
551
+ # other stories wrote the rest, and a replace would erase them the moment a second
552
+ # story implements. --lane backend puts them in `source_files`, the service lane's
553
+ # key, leaving `ui_source_files` for implement-ui.
554
+ # Known limit: a file the slice DELETED stays in the list; concept-coverage reports
555
+ # it as declared-but-missing drift rather than silently carrying it as real.
223
556
  - id: xref_concept
224
557
  action: execute_script
225
558
  script: { ref: xref }
226
559
  subcommand: concept-upsert
227
- args: ["--feature-dir", "{{feature_dir}}", "--source-files", "{{files}}", "--origin", "greenfield"]
560
+ # --ids + --slice record WHICH REQUIREMENT this run's files realise, as Requirement
561
+ # Anchors rows in the concept. Both halves were already here and thrown away: the ids
562
+ # the story exercises, and the files just generated. Only `adopt` writes a
563
+ # source-map.md, so without this every id on a greenfield feature came back unanchored
564
+ # at analyze-slice and the trace pass had to read the whole tree to find out what
565
+ # implements what. Reconcile-on-write, keyed by slice + lane.
566
+ args: ["--feature-dir", "{{feature_dir}}", "--source-files", "{{files}}", "--merge", "--lane", "backend", "--origin", "greenfield", "--ids", "{{exercises_csv}}", "--slice", "{{story}}"]
228
567
  outputs:
229
- - { name: okf_concept, from: "$.concept" }
568
+ - { name: okf_concept, from: "$.concept" }
569
+ - { name: okf_anchors, from: "$.anchors_written" }
230
570
 
571
+ # 8a. Cross-slice edges are a FEATURE property, not a story's: which sibling
572
+ # aggregates this one references is stated by the feature plan's domain model,
573
+ # and no single slice can answer it. So this reads backend-plan.md — the one
574
+ # derived fact it takes from the plan — and reconciles on write, so a dropped
575
+ # dependency self-heals out of the used aggregate's Cross-References. Skipped
576
+ # with no plan on disk: there is then nothing to derive edges from.
231
577
  - id: xref_record
578
+ condition: "{{be_plan_exists}} == true"
232
579
  action: execute_script
580
+ on_error: continue
233
581
  script: { ref: xref }
234
582
  subcommand: record
235
- args: ["--feature-dir", "{{feature_dir}}", "--design", "{{design_doc}}", "--origin", "greenfield"]
583
+ args: ["--feature-dir", "{{feature_dir}}", "--design", "{{be_plan}}", "--origin", "greenfield"]
236
584
  outputs:
237
585
  - { name: xref_wrote, from: "$.wrote" }
238
586
  - { name: xref_edge_count, from: "$.edge_count" }
239
587
 
240
- # 3c. The concept's Overview — the ONE part of the OKF concept a deterministic script
241
- # cannot produce (prose distilled from design.md §1). The model authors the text
242
- # only; concept-overview splices it into the `## Overview` section alone, so the
243
- # Cross-References rows other slices own are structurally out of reach.
588
+ # 8b. The concept's Overview — the one part a deterministic script cannot produce.
589
+ # It describes the AGGREGATE, which is the feature plan's subject: distilling it
590
+ # from one story's slice would name whatever that slice happened to touch. The
591
+ # model authors prose only; concept-overview splices it into `## Overview` alone,
592
+ # so the Cross-References rows other slices own are structurally out of reach.
593
+ # build_context, not a step-level `context:`: the engine REPLACES the envelope on
594
+ # build_context and only ADDS to it on a step-level list, so naming the plan alone
595
+ # there would have left generation's whole payload — this story's slice included —
596
+ # underneath a prompt that says "describe the whole aggregate, not one story".
597
+ - id: ctx_overview
598
+ condition: "{{be_plan_exists}} == true"
599
+ action: build_context
600
+ context:
601
+ - { path: "{{be_plan}}" }
244
602
  - id: okf_overview
603
+ condition: "{{be_plan_exists}} == true"
245
604
  action: agent_query
246
- context: ["{{design_doc}}"]
247
605
  include_standards: false
248
606
  schema: { overview: "<2-4 sentences of prose>" }
249
607
  mock: { overview: "" }
250
608
  task: |
251
- Write the OKF knowledge-concept Overview for the aggregate that design.md (in
252
- context) describes. 2-4 sentences of PROSE, distilled from §1 Domain Model:
609
+ Write the OKF knowledge-concept Overview for the aggregate that the backend plan
610
+ (in context) describes. 2-4 sentences of PROSE, distilled from §1 Domain Model:
253
611
  - name the aggregate ROOT and what the aggregate IS;
254
- - state the INVARIANTS it guards (from §1 / the design's business rules);
612
+ - state the INVARIANTS it guards;
255
613
  - state its TRANSACTION BOUNDARY — what it OWNS inside the boundary vs what it
256
614
  only REFERENCES by id.
257
615
  Plain prose only: no headings, no bullets, no code fences, no file paths, and no
258
616
  restating of the dependency edges (those live in the Depends-On / Cross-References
259
- tables). Describe only what the design states — invent nothing.
617
+ tables). Describe only what the plan states — invent nothing, and describe the
618
+ whole aggregate rather than the one story that happens to be building now.
260
619
  outputs:
261
620
  - { name: okf_overview, from: "$.overview" }
262
-
263
621
  - id: okf_overview_write
622
+ condition: "{{be_plan_exists}} == true"
264
623
  action: execute_script
265
624
  script: { ref: xref }
266
625
  subcommand: concept-overview
267
626
  args: ["--feature-dir", "{{feature_dir}}", "--text", "{{okf_overview}}"]
268
627
 
269
- # 4. Post-execution hooks.
628
+ # ═══════════════ ACCEPTANCE — the ONE place the spec is read ═════════════════
629
+ # Generation is design-only by contract; this is the check that the design was not
630
+ # the wrong design. READ-ONLY and REPORT-ONLY: it never edits code, never edits the
631
+ # pass, and never stops the run. A gap here means one of two documents is wrong, and
632
+ # which one is a human decision — the message names both routes.
633
+ - id: ctx_accept
634
+ action: build_context
635
+ vars:
636
+ story_id: "{{story}}"
637
+ exercises_csv: "{{exercises_csv}}"
638
+ constraints_csv: "{{constraints_csv}}"
639
+ files_csv: "{{files}}"
640
+ context:
641
+ - { path: "{{story_path}}" } # the acceptance criteria
642
+ - { path: "{{spec_file}}" } # the contract — the ONLY step in this workflow that reads it
643
+ - { path: "{{design_doc}}" } # what was designed
644
+ - id: accept_check
645
+ action: agent_query
646
+ tools: Read # the generated files arrive as paths, not inlined
647
+ include_standards: false
648
+ schema: { ok: "<true|false>", gaps: "<one line per gap, or empty>" }
649
+ mock: { ok: "true", gaps: "" }
650
+ task: |
651
+ Story {{story_id}} was just implemented. Judge ONLY this: does the code that now
652
+ exists satisfy the requirements the story delivers, AS THE SPEC WORDS THEM?
653
+
654
+ In scope: {{exercises_csv}} (delivered) and {{constraints_csv}} (must hold).
655
+ Resolve each id's wording from spec.md in context — that wording, not the design's
656
+ restatement of it, is what the code is measured against.
657
+
658
+ Read the files this run wrote:
659
+ {{files_csv}}
660
+ plus anything they call that you need to follow. Do not modify a single file.
661
+
662
+ Report a gap ONLY where the code fails to deliver an in-scope id as the spec words
663
+ it, or breaks an in-scope constraint. These are NOT gaps, and reporting them is
664
+ noise that hides the real ones:
665
+ - a requirement no story has claimed yet, or one another story delivers;
666
+ - style, naming, structure or test-coverage opinions — other gates own those;
667
+ - anything the design pass deliberately deferred and named under its risks.
668
+
669
+ One line per gap: the id, what the spec requires, and what the code does instead.
670
+ Set ok=false only when you found at least one. If the SPEC and the DESIGN disagree
671
+ about an in-scope id, that IS a gap — say which document you believe is wrong and
672
+ why, because the two routes out of it are different commands.
673
+ outputs:
674
+ - { name: accept_ok, from: "$.ok" }
675
+ - { name: accept_gaps, from: "$.gaps" }
676
+ - id: accept_notice
677
+ condition: "{{accept_ok}} != true"
678
+ action: verify_context
679
+ require_value: accept_ok
680
+ equals: "true"
681
+ on_mismatch: continue
682
+ on_mismatch_message: |
683
+ ACCEPTANCE — story {{story}} does not yet satisfy the spec wording of the ids it delivers:
684
+
685
+ {{accept_gaps}}
686
+
687
+ Nothing was changed to fix this: the code follows design pass {{design_pass}}, so a gap
688
+ here means either the design was wrong or the spec has moved. Route by which it is —
689
+ the design is wrong: /appweaver story-plan {{story}} (a new pass supersedes this one);
690
+ the spec is wrong: /appweaver sync-spec (⌛ not yet built — until it is, fold the change back by hand under .appweaver/SPEC-EDIT-PROTOCOL.md), then sync-stories, then story-plan.
691
+ Run /appweaver build either way — a design that is right and code that does not compile
692
+ is a third thing, and build is where that one surfaces.
693
+
694
+ # 9. Post-execution hooks.
270
695
  - id: resolve_post_hooks
271
696
  action: execute_script
272
697
  script: { ref: actions }
@@ -279,9 +704,12 @@ workflow:
279
704
  hooks_from_variable: after_hooks
280
705
 
281
706
  outputs:
282
- feature_dir: "{{feature_dir}}"
283
- design_doc: "{{design_doc}}"
707
+ story: "{{story}}"
708
+ built_from: "{{design_doc}} — design pass {{design_pass}} (designed against spec {{design_spec_version}}; spec now {{current_version}})"
709
+ requirements: "{{exercises_csv}}"
284
710
  files: "{{files}}"
285
- okf_concept: "{{okf_concept}}"
286
- xref_edge_count: "{{xref_edge_count}}"
287
- next: "/appweaver build (OKF concept authored at {{okf_concept}}; {{xref_edge_count}} cross-slice edge(s) recorded into sibling aggregates)"
711
+ lane: "{{lane_id}} → {{lane_source_path}}"
712
+ okf_concept: "{{okf_concept}} (this run's files MERGED into source_files; {{xref_edge_count}} cross-slice edge(s) recorded into sibling aggregates)"
713
+ acceptance: "ok={{accept_ok}} · gaps: {{accept_gaps}}"
714
+ stale_ids: "{{stale_csv}}"
715
+ next: "/appweaver build — it compiles every declared lane. Then /appweaver analyze-slice (spec ↔ code, through the design), and /appweaver implement-ui if this story has a UI design. The story stays OPEN: /appweaver close-story — which would ratify its ADRs, flip its interface entries live and archive the passes — is ⌛ NOT BUILT YET, so there is no transition out of in-flight and no last gate before merge."