aw-spec-cli 0.1.18 → 0.1.19

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 (53) hide show
  1. package/assets/.appweaver/DESIGN-FORMAT.md +45 -21
  2. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +62 -14
  3. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +6 -4
  4. package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +9 -1
  5. package/assets/.appweaver/orchestrator/modules/tech-select.orch.yaml +21 -0
  6. package/assets/.appweaver/orchestrator/orchestrator.py +23 -1
  7. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +18 -11
  8. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +5 -3
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +298 -109
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +161 -34
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +125 -24
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +18 -7
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +182 -39
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +300 -35
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +190 -31
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +93 -8
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +34 -7
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +319 -0
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +125 -24
  20. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +846 -35
  21. package/assets/.appweaver/scripts/bash/common.sh +28 -1
  22. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +843 -46
  23. package/assets/.appweaver/scripts/python/aw-inventory.py +3 -2
  24. package/assets/.appweaver/templates/design-template.md +41 -30
  25. package/assets/.appweaver/templates/spec-template.md +7 -4
  26. package/assets/.appweaver/templates/ui-design-template.md +7 -3
  27. package/assets/guardrails/domain/healthcare/auditing.domain.md +3 -3
  28. package/assets/guardrails/domain/pharma/auditing.domain.md +3 -3
  29. package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +1 -1
  30. package/assets/guardrails/technology/dotnet-webapi/auditing.md +1 -1
  31. package/assets/guardrails/technology/dotnet-webapi/authorization.md +1 -1
  32. package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +1 -1
  33. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +1 -1
  34. package/assets/guardrails/technology/dotnet-webapi/data-classification.md +1 -1
  35. package/assets/guardrails/technology/dotnet-webapi/eventing.md +1 -1
  36. package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +1 -1
  37. package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +1 -1
  38. package/assets/guardrails/technology/dotnet-webapi/persistence.md +1 -1
  39. package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +1 -1
  40. package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +1 -1
  41. package/assets/guardrails/technology/nextjs/accessibility-implementation.md +1 -1
  42. package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +1 -1
  43. package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +1 -1
  44. package/assets/guardrails/technology/nextjs/auth-session.md +1 -1
  45. package/assets/guardrails/technology/nextjs/component-architecture.md +1 -1
  46. package/assets/guardrails/technology/nextjs/design-system.md +1 -1
  47. package/assets/guardrails/technology/nextjs/error-handling.md +1 -1
  48. package/assets/guardrails/technology/nextjs/forms-and-validation.md +1 -1
  49. package/assets/guardrails/technology/nextjs/i18n.md +1 -1
  50. package/assets/guardrails/technology/nextjs/performance-budgets.md +1 -1
  51. package/assets/guardrails/technology/nextjs/routing-navigation.md +1 -1
  52. package/assets/guardrails/technology/nextjs/state-management.md +1 -1
  53. package/package.json +1 -1
@@ -0,0 +1,319 @@
1
+ # =============================================================================
2
+ # aw-spec-recall-delta (orchestrator) — WITHDRAW a change that never shipped
3
+ #
4
+ # The escape hatch of the CR loop, and the counterpart of close-delta:
5
+ # close-delta the change SHIPPED — archive it and bump the version
6
+ # recall-delta the change is WITHDRAWN — archive it and bump NOTHING
7
+ #
8
+ # specify → plan → analyze-plan-delta → implement → build → analyze-slice-delta
9
+ # → finalize-delta → analyze-slice → close-delta
10
+ # └─ recall-delta may end it anywhere BEFORE finalize-delta ────────────┘
11
+ #
12
+ # WHY IT EXISTS. Changes get abandoned: the requirement was dropped, the approach was
13
+ # wrong, a wider refactor swallowed it. Until this stage there was no way to say so. The
14
+ # only mechanism that took a change out of the open set was `cr-archive`, which archives
15
+ # AND bumps the version in one act, so withdrawing one meant either burning a version
16
+ # number on work that shipped nothing or deleting the directory by hand and editing
17
+ # active_version.json — an operation with no record, no gate, and nothing to stop it being
18
+ # done to the wrong change. The archive and the bump are now separable (`cr-archive
19
+ # --recall`), and this stage is the supported way to reach that half.
20
+ #
21
+ # A RECALL IS NOT A ROLLBACK. It withdraws the change's DOCUMENTS — its delta-spec, its
22
+ # delta plans and its model delta, all archived intact under changes/archive/<id>/ beside a
23
+ # RECALLED.md that records the date and the reason. It does NOT revert code: a change that
24
+ # reached /appweaver implement has already generated source files, and reverting those is
25
+ # git's job, not this pipeline's. The output and the marker both say so, because a stage
26
+ # that half-undoes something while sounding like it undid all of it is worse than no stage.
27
+ #
28
+ # TWO REFUSALS, BOTH MECHANICAL AND BOTH READ OFF DISK.
29
+ #
30
+ # MERGED — finalize-delta has already folded this change into spec.md and the context's
31
+ # domain-model.md, and appended a Change History row carrying its id. The masters now
32
+ # describe behaviour that includes it, so withdrawing the change would leave them
33
+ # describing work no open change accounts for. The remedy for a merged change that should
34
+ # not have landed is a NEW change that reverses it. Detected from the id's presence in
35
+ # spec.md, which is that Change History row.
36
+ #
37
+ # ⏭️ THE UN-MERGE IS DELIBERATELY NOT BUILT (decision, 2026-09-11 — not a missing piece).
38
+ # Both prerequisites now exist: finalize-delta snapshots both masters to
39
+ # changes/<id>/pre-merge/, and cr-merge-check can prove no other delta landed in between,
40
+ # which is exactly the window a restore would be safe in. It is still not built, because a
41
+ # restore is a whole-file overwrite of two long-lived hand-curated documents, and what it
42
+ # would NOT roll back is the rest of what finalize-delta did — the OKF cross-reference
43
+ # edges and the re-authored concept Overview. A command that restores the masters while
44
+ # leaving the knowledge layer describing the withdrawn change is the kind of half-undo
45
+ # this stage's own header refuses to be. The snapshot is kept regardless: it is the record
46
+ # of what the masters said, and an operator who needs it can read it.
47
+ #
48
+ # DEPENDANTS — a wave is a dependency level. Same wave means independent; a HIGHER wave
49
+ # means planned and built on top of the lower ones. Withdrawing a lower-wave change
50
+ # therefore strands every open change above it, so this stage refuses and names them.
51
+ # `depends_on` is stamped nowhere yet (⌛ with the rest of the parallel-delta merge
52
+ # fence), so the wave is the only dependency signal that actually exists — and it is the
53
+ # conservative one, since same-wave siblings are never counted.
54
+ #
55
+ # THE VERSION DOES NOT MOVE. That is the entire point of the stage and the one invariant
56
+ # worth stating twice: a version records what a change SHIPPED as, and a recalled change
57
+ # shipped nothing. Nothing here stamps a header, appends a Change History row, or touches
58
+ # active_version.json's `version` field.
59
+ #
60
+ # Launch: /appweaver recall-delta [CR-id] (a bare id skips the prompt)
61
+ # Prereq: an OPEN change on the active feature that finalize-delta has NOT merged.
62
+ # =============================================================================
63
+
64
+ name: aw-spec-recall-delta
65
+ description: "Withdraw an open change that will not ship — archive changes/<id>/ and its model delta into changes/archive/<id>/ beside a RECALLED.md recording the date and reason, and drop it from active_version.json's open set WITHOUT bumping the version. Refuses a change finalize-delta already merged into the masters (the remedy there is a new change that reverses it) and one that a higher-wave open change is built on top of. Withdraws documents only — code already generated by implement is not reverted. Invokes no model and generates no code."
66
+ version: 1.0.0
67
+ agent_agnostic: true
68
+ dialect: aw-spec.orchestrator/v1
69
+
70
+ inputs:
71
+ user_input:
72
+ description: "Optional CR id of the change to withdraw. A bare id selects it and skips the prompt; anything else is ignored and the menu is shown."
73
+ required: false
74
+ bind: USER_INPUT
75
+
76
+ scripts:
77
+ actions:
78
+ sh: .appweaver/scripts/bash/aw-spec-actions.sh
79
+ ps1: .appweaver/scripts/powershell/aw-spec-actions.ps1
80
+ xref:
81
+ sh: .appweaver/scripts/bash/aw-xref.sh
82
+ ps1: .appweaver/scripts/powershell/aw-xref.ps1
83
+
84
+ # No standards seed and no domain/tech rule selection: this stage authors nothing, so it
85
+ # has no prompt to govern. It is bookkeeping over changes/ + active_version.json, plus the
86
+ # two refusals.
87
+ standards: []
88
+
89
+ vars:
90
+ has_changes: "false"
91
+ cr_choice: ""
92
+ cr_id: ""
93
+ merged: "false"
94
+ merge_evidence: ""
95
+ has_dependants: "false"
96
+ dependant_ids: ""
97
+ dependant_count: "0"
98
+ # The two refusal flags, both DOWN by default and raised only by the good state — see
99
+ # the gates below for why fail-closed is the right default here.
100
+ not_merged: "false"
101
+ no_dependants: "false"
102
+ recall_confirmed: "false"
103
+ recalled: "false"
104
+ archived_dir: ""
105
+ recall_marker: ""
106
+ open_count: "0"
107
+
108
+ workflow:
109
+
110
+ # 1. Resolve the active feature.
111
+ - id: feature_paths
112
+ action: execute_script
113
+ on_error: continue # never crash raw on 'no feature set' — the gate below explains it
114
+ script: { ref: actions }
115
+ subcommand: feature-paths
116
+ outputs:
117
+ - { name: feature, from: "$.feature" }
118
+ - { name: feature_dir, from: "$.feature_dir" }
119
+
120
+ - id: require_feature
121
+ action: verify_context
122
+ require_non_empty: feature_dir
123
+ on_empty: stop
124
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run recall-delta."
125
+
126
+ # 2. WHICH CHANGES ARE OPEN — the evidence is changes/<CR-id>/ on disk. No synthetic
127
+ # rows: `main` is not a change and there is nothing to open here, so the menu is the
128
+ # real changes and nothing else. `matched_label` resolves a CR id passed as the
129
+ # argument to the whole option string, which is how a bare `recall-delta CR-…` skips
130
+ # the prompt entirely.
131
+ - id: cr_list
132
+ action: execute_script
133
+ script: { ref: actions }
134
+ subcommand: cr-list
135
+ args: ["--feature-dir", "{{feature_dir}}", "--preselect", "{{USER_INPUT}}"]
136
+ outputs:
137
+ - { name: change_options, from: "$.select_options" }
138
+ - { name: matched_label, from: "$.matched_label" }
139
+ - { name: has_changes, from: "$.has_changes" }
140
+ - { name: change_count, from: "$.count" }
141
+
142
+ - id: require_changes
143
+ action: verify_context
144
+ require_true: has_changes
145
+ on_false: stop
146
+ on_false_message: "No open change on this feature — nothing to withdraw. A change is opened by /appweaver specify; one that already shipped was archived by /appweaver close-delta and is under changes/archive/, where a recall does not reach it."
147
+
148
+ # 3. WHICH ONE. single_option_confirm so a feature with exactly one open change still
149
+ # confirms the id out loud — this stage archives documents, and picking silently is
150
+ # how the wrong change gets withdrawn on a feature the operator has two open on.
151
+ - id: pick_change
152
+ action: user_select
153
+ prompt: "Which open change is being WITHDRAWN? Its delta-spec, delta plans and model delta are archived under changes/archive/ and it leaves the open set — with no version bump, because a recalled change ships nothing. Code that /appweaver implement already generated for it is NOT reverted; that is a git operation."
154
+ options_from_variable: change_options
155
+ preselect_from: matched_label
156
+ single_option_confirm: true
157
+ outputs:
158
+ - { name: cr_choice, from: selection }
159
+
160
+ # 3b. That change's documents, and the clean id back out of the label the pick returned.
161
+ - id: cr_paths
162
+ action: execute_script
163
+ script: { ref: actions }
164
+ subcommand: cr-paths
165
+ args: ["--cr-id", "{{cr_choice}}", "--feature-dir", "{{feature_dir}}"]
166
+ outputs:
167
+ - { name: cr_id, from: "$.cr_id" }
168
+ - { name: cr_dir, from: "$.cr_dir" }
169
+ - { name: delta_spec, from: "$.delta_spec" }
170
+ - { name: delta_model, from: "$.delta_model" }
171
+ - { name: wave, from: "$.wave" }
172
+
173
+ # 4. THE TWO REFUSALS, both answered from disk in one call — see the header for why each
174
+ # one is a stop rather than a warning.
175
+ - id: recall_check
176
+ action: execute_script
177
+ script: { ref: actions }
178
+ subcommand: cr-recall-check
179
+ args: ["--cr-id", "{{cr_choice}}", "--feature-dir", "{{feature_dir}}"]
180
+ outputs:
181
+ - { name: merged, from: "$.merged" }
182
+ - { name: merge_evidence, from: "$.merge_evidence" }
183
+ - { name: has_dependants, from: "$.has_dependants" }
184
+ - { name: dependant_ids, from: "$.dependant_ids" }
185
+ - { name: dependant_count, from: "$.dependant_count" }
186
+
187
+ # Each refusal is a RAISE then a gate, not a conditional gate: the raise fires only on
188
+ # the good state, so a value that never arrived (the script errored, the field was
189
+ # renamed) leaves the flag down and the stage refuses. Fail-closed is the only safe
190
+ # default for a gate in front of an archive.
191
+ - id: not_merged_raise
192
+ condition: "{{merged}} == false"
193
+ action: set
194
+ vars: { not_merged: "true" }
195
+
196
+ - id: refuse_merged
197
+ action: verify_context
198
+ require_true: not_merged
199
+ on_false: stop
200
+ on_false_message: "Change {{cr_id}} has already been MERGED — /appweaver finalize-delta folded it into spec.md and the context's domain-model.md, and the spec's Change History carries it: {{merge_evidence}}. The masters now describe behaviour that includes this change, so withdrawing it would leave them describing work no open change accounts for. A recall cannot un-merge, and this stage will not pretend otherwise. If the change should not have landed, finish it (/appweaver analyze-slice → close-delta) and then open a NEW change that reverses it with /appweaver specify — that keeps both the landing and the reversal in the ledger, which is what a reader of the spec needs to see."
201
+
202
+ - id: no_dependants_raise
203
+ condition: "{{has_dependants}} == false"
204
+ action: set
205
+ vars: { no_dependants: "true" }
206
+
207
+ - id: refuse_dependants
208
+ action: verify_context
209
+ require_true: no_dependants
210
+ on_false: stop
211
+ on_false_message: "Change {{cr_id}} is at wave {{wave}} and {{dependant_count}} open change(s) sit at a HIGHER wave: {{dependant_ids}}. A higher wave means planned and built ON TOP of the lower ones, so withdrawing this one strands them — their delta plans were authored against a predecessor that would no longer exist. Withdraw the higher-wave change(s) first (/appweaver recall-delta <CR-id>), or re-plan them against the lower waves that remain (/appweaver plan <CR-id>) before recalling this one. Same-wave changes are independent and are not counted here."
212
+
213
+ # 5. CONFIRM. Ordered no-first against the kit's usual yes-first convention, and
214
+ # deliberately: act_ask takes options[0] non-interactively, so an unattended run
215
+ # DECLINES rather than archiving a change nobody was watching. The reason is asked
216
+ # for in the same breath because it is what the marker records — a withdrawn change
217
+ # with no stated reason is a directory in an archive that nobody can account for later.
218
+ - id: confirm_recall
219
+ action: ask
220
+ prompt: "Withdraw {{cr_id}} (wave {{wave}})? Its documents — {{delta_spec}}, its delta plans and {{delta_model}} — move to changes/archive/{{cr_id}}/ and it leaves the open set. The version does NOT move: this change ships nothing. Any code implement already generated for it stays on disk; revert that with git if it should not remain. Answer no to stop with everything untouched."
221
+ options: ["no", "yes"]
222
+ answer_var: recall_answer
223
+
224
+ - id: recall_go
225
+ condition: "{{recall_answer}} == yes"
226
+ action: set
227
+ vars: { recall_confirmed: "true" }
228
+
229
+ - id: require_confirm
230
+ action: verify_context
231
+ require_true: recall_confirmed
232
+ on_false: stop
233
+ on_false_message: "Stopped — {{cr_id}} is untouched and still open. Nothing was archived and no version moved. Re-run and answer yes to withdraw it, or carry on with it: /appweaver plan {{cr_id}} to (re)author its delta plans, or /appweaver implement {{cr_id}} to build it."
234
+
235
+ - id: ask_reason
236
+ action: ask
237
+ prompt: "Why is {{cr_id}} being withdrawn? One line — it is written into changes/archive/{{cr_id}}/RECALLED.md, which is the only record of why this work stopped."
238
+ options: ["superseded by another change", "requirement dropped", "approach was wrong"]
239
+ answer_var: recall_reason
240
+
241
+ # 6. Pre-execution hooks.
242
+ - id: resolve_pre_hooks
243
+ action: execute_script
244
+ script: { ref: actions }
245
+ subcommand: resolve-hooks
246
+ args: ["--phase", "before_recall_cr"]
247
+ outputs:
248
+ - { name: before_hooks, from: "$.hooks" }
249
+ - id: dispatch_pre_hooks
250
+ action: dispatch_hooks
251
+ hooks_from_variable: before_hooks
252
+
253
+ # 7. THE RECALL. --recall is what separates this from close-delta: the same archive of
254
+ # the directory and of the model delta that lives beside the master, plus the
255
+ # RECALLED.md marker, and NO version bump. The script refuses an id the ledger does
256
+ # not carry and leaves everything in place when it does, so a refusal here is a no-op
257
+ # rather than a half-archived change.
258
+ - id: recall
259
+ action: execute_script
260
+ script: { ref: actions }
261
+ subcommand: cr-archive
262
+ args: ["--feature-dir", "{{feature_dir}}", "--cr-id", "{{cr_choice}}", "--recall", "--reason", "{{recall_reason}}"]
263
+ outputs:
264
+ - { name: recall_ok, from: "$.ok" }
265
+ - { name: recalled, from: "$.recalled" }
266
+ - { name: archived_dir, from: "$.archived_dir" }
267
+ - { name: archived_model, from: "$.archived_model" }
268
+ - { name: recall_marker, from: "$.recall_marker" }
269
+ - { name: version, from: "$.version" }
270
+ - { name: active_change, from: "$.active_change" }
271
+ - { name: open_count, from: "$.open_count" }
272
+
273
+ - id: verify_recalled
274
+ action: verify_context
275
+ require_true: recall_ok
276
+ on_false: stop
277
+ on_false_message: "The withdrawal of {{cr_id}} did not complete and nothing was moved — active_version.json does not carry it as an open change. That happens when the change was already closed or recalled, or when its directory exists on disk but was never registered by /appweaver specify. Check {{feature_dir}}/active_version.json against {{feature_dir}}/changes/."
278
+
279
+ # 8. Anchor the WITHDRAWAL in the OKF bundle's freshness journal (log.md, OKF §7).
280
+ # close-delta logs the ship; this logs the stop, which is exactly as much a part of
281
+ # the feature's history. Best-effort + git-aware: a no-op when there is no bundle.
282
+ - id: xref_log
283
+ action: execute_script
284
+ on_error: continue
285
+ script: { ref: xref }
286
+ subcommand: log
287
+ args: ["--note", "CR {{cr_id}} RECALLED — withdrawn before merge ({{recall_reason}}), archived to {{archived_dir}}, version unchanged at {{version}}"]
288
+ outputs:
289
+ - { name: xref_log, from: "$.log" }
290
+
291
+ # 9. Post-execution hooks.
292
+ - id: resolve_post_hooks
293
+ action: execute_script
294
+ script: { ref: actions }
295
+ subcommand: resolve-hooks
296
+ args: ["--phase", "after_recall_cr"]
297
+ outputs:
298
+ - { name: after_hooks, from: "$.hooks" }
299
+ - id: dispatch_post_hooks
300
+ action: dispatch_hooks
301
+ hooks_from_variable: after_hooks
302
+
303
+ outputs:
304
+ feature: "{{feature}}"
305
+ feature_dir: "{{feature_dir}}"
306
+ cr_id: "{{cr_id}}"
307
+ wave: "{{wave}}"
308
+ reason: "{{recall_reason}}"
309
+ archived_dir: "{{archived_dir}}"
310
+ archived_model: "{{archived_model}}"
311
+ recall_marker: "{{recall_marker}}"
312
+ # The number BEFORE and after are the same number — stated as an output because "nothing
313
+ # moved" is the result this stage exists to produce, and an absent field would read as
314
+ # an unanswered question rather than an answered one.
315
+ version: "{{version}}"
316
+ open_changes_remaining: "{{open_count}}"
317
+ active_change: "{{active_change}}"
318
+ note: "Change {{cr_id}} WITHDRAWN — its delta-spec, delta plans and model delta are archived intact at {{archived_dir}}, with the reason recorded in {{recall_marker}}. The version did NOT move and stands at {{version}}: this change shipped nothing, so no number was spent on it. spec.md and the context's domain-model.md never carried it and were not touched. Code that /appweaver implement generated for this change is still on disk — a recall withdraws documents, not source; revert it with git if it should not stay."
319
+ next: "{{open_count}} change(s) still open on this feature. Carry on with one of them (/appweaver plan <CR-id> → analyze-plan-delta → implement), or with none left the feature is back on its masters: /appweaver plan main re-derives the lane plans from spec.md and the domain model, and /appweaver specify opens a fresh change."
@@ -15,7 +15,14 @@
15
15
  # assert different rules for it and nothing arbitrates. Authoring both here is also what
16
16
  # keeps the model in step with the spec version: it never waits for plan or implement.
17
17
  #
18
- # Auto-detects first-vs-subsequent from spec.md:
18
+ # Auto-detects first-vs-subsequent from feature-paths' needs_bootstrap — NOT from spec.md's
19
+ # presence. The two differ on one state and it is the one that corrupts: a first run that
20
+ # wrote spec.md and then died leaves the file on disk with the unfinished-bootstrap marker
21
+ # still up, and the file alone reads as a finished spec to EXTEND. version-init raises that
22
+ # marker (--bootstrap, which adopt must never pass — a finished reversal is not a crashed
23
+ # draft) and version-bootstrap-done lowers it AFTER the converge block, so the whole
24
+ # authoring window is recoverable. An interrupted draft ASKS before it is overwritten,
25
+ # because a deliberate stop and a crash look identical from here:
19
26
  # • BOOTSTRAP (no spec.md) → author the MASTER spec (US-1) + the context's MASTER
20
27
  # domain model + init active_version.json. On the context's second feature the model
21
28
  # already exists and gains a section rather than being replaced.
@@ -33,11 +40,12 @@
33
40
  # defaults a new delta to the wave already in flight (parallel by default); `cr-wave`
34
41
  # raises one that turns out to build on another.
35
42
  #
36
- # ⚠️ finalize-delta and close-delta still resolve "the" change through
37
- # active_version.json.active_change — the most recently opened. plan, analyze-plan-delta
38
- # and implement ASK which change and re-activate the answer, so the choice reaches those
39
- # two; threading an explicit --cr-id through the whole chain is the next step and is not
40
- # done yet.
43
+ # EVERY DELTA STAGE NOW ASKS WHICH CHANGE. plan, analyze-plan-delta, implement,
44
+ # recall-delta, finalize-delta and close-delta all list what is open, ask, and activate the
45
+ # answer, with a bare CR id as the argument doubling as the answer. None of them takes
46
+ # active_version.json's `active_change` on trust — which matters most here, because THIS
47
+ # phase overwrites that field on every run: a bare finalize used to merge whichever change
48
+ # was opened last, and `cr-setup` scaffolds a delta-spec stub the moment it opens one.
41
49
  #
42
50
  # Both paths then run ONE converge block — checklist + clarify + validate — over the
43
51
  # document THIS invocation authored, never the other one:
@@ -66,7 +74,7 @@
66
74
 
67
75
  name: aw-spec-specify
68
76
  description: Author the ACTIVE feature's specification AND its bounded context's domain model, together. The aggregate root, its entities, events and invariants (BR-n) live in specs/<Context>/domain-model.md — the one canonical definition every spec in the context cites by id and never restates. First call writes the master spec (US-1) plus the master model; a later call opens its OWN change (many may be open at once) and writes that change's delta-spec and delta-domain-model, leaving both masters untouched for finalize-delta to merge. Each change carries a WAVE — deltas sharing one are independent and developable in parallel, a higher wave is implemented after the lower ones land — and its model delta sits beside the master so every other open delta can see it while being authored. Spec phase only — plan/design is separate.
69
- version: 3.0.0
77
+ version: 3.5.0
70
78
  agent_agnostic: true
71
79
  dialect: aw-spec.orchestrator/v1
72
80
 
@@ -115,6 +123,19 @@ vars:
115
123
  # the checklist. NEVER hand-set to spec_file on the delta
116
124
  # path — that is what silently drifts an adopted spec.
117
125
  version: "" # current active_version.json version (for the report)
126
+ bootstrap_cleared: "false" # whether THIS run lowered the unfinished-first-draft marker
127
+ # THE BOOTSTRAP BRANCH AND ITS CRASH-RESUME. is_bootstrap is what every branch below
128
+ # reads; it starts from feature-paths' needs_bootstrap, which is `no spec.md` OR `a first
129
+ # draft is still marked unfinished`. The second half is the crash case, and it is the one
130
+ # that used to become a delta against a v1.0 nobody finished.
131
+ spec_exists: "false"
132
+ needs_bootstrap: "true" # defaulted to BOOTSTRAP: with no answer, authoring a first
133
+ # draft is recoverable, while extending a spec that may not
134
+ # exist is not
135
+ bootstrap_pending: "false"
136
+ is_bootstrap: "false"
137
+ resume_bootstrap: "false" # bootstrap_pending AND spec.md on disk ⇒ a draft was interrupted
138
+ resume_choice: ""
118
139
 
119
140
  workflow:
120
141
 
@@ -129,6 +150,14 @@ workflow:
129
150
  - { name: feature_dir, from: "$.feature_dir" }
130
151
  - { name: spec_file, from: "$.spec_file" }
131
152
  - { name: spec_exists, from: "$.spec_exists" }
153
+ # WHICH BRANCH THIS RUN TAKES. needs_bootstrap, not spec_exists — the two differ on
154
+ # exactly one state, and it is the one that corrupts: a FIRST run that wrote spec.md
155
+ # and then died leaves the file on disk with the unfinished-bootstrap marker still up,
156
+ # and branching on the file alone reads that as a finished spec to EXTEND. The next
157
+ # call then opens a change against a v1.0 nobody finished. feature-paths already
158
+ # computed this in both shells; nothing read it.
159
+ - { name: needs_bootstrap, from: "$.needs_bootstrap" }
160
+ - { name: bootstrap_pending, from: "$.bootstrap_pending" }
132
161
 
133
162
  - id: require_feature
134
163
  action: verify_context
@@ -185,27 +214,78 @@ workflow:
185
214
  action: dispatch_hooks
186
215
  hooks_from_variable: before_hooks
187
216
 
188
- # ═══════════════════════ BOOTSTRAP — no spec.md yet ═══════════════════════
217
+ # ═══════════ WHICH BRANCH — and the one case where it is ASKED ═══════════
218
+ # is_bootstrap takes needs_bootstrap's answer. Two states reach it:
219
+ # no spec.md → an ordinary first run. Nothing is asked.
220
+ # spec.md + the marker still up → a first draft was INTERRUPTED. Re-authoring is
221
+ # the designed recovery, but it OVERWRITES what is
222
+ # on disk, and a deliberate stop is indistinguishable
223
+ # from a crash. So that case, and only that case,
224
+ # asks before it writes.
225
+ - id: branch_from_bootstrap
226
+ action: set
227
+ vars: { is_bootstrap: "{{needs_bootstrap}}" }
228
+
229
+ # The AND, as an ordered assignment (the engine has no compound conditions): under a
230
+ # raised marker, take spec.md's presence as the answer. A marker with no spec.md is a
231
+ # first run that died before writing anything — there is nothing to overwrite and
232
+ # nothing to ask.
233
+ - id: flag_resume
234
+ condition: "{{bootstrap_pending}} == true"
235
+ action: set
236
+ vars: { resume_bootstrap: "{{spec_exists}}" }
237
+
238
+ - id: ask_resume
239
+ condition: "{{resume_bootstrap}} == true"
240
+ action: ask
241
+ prompt: |
242
+ A FIRST DRAFT of this feature's spec.md was started and never finished — the
243
+ unfinished-bootstrap marker in active_version.json is still up, which means the last
244
+ run wrote the file and then stopped before completing it.
245
+
246
+ Re-author it (the draft is replaced from your input, and the domain model with it), or
247
+ treat what is on disk as the finished v1.0 and open a CHANGE against it instead?
248
+
249
+ Re-authoring OVERWRITES specs/…/spec.md. Treat-as-finished keeps it exactly as it is
250
+ and clears the marker, so this is asked once either way.
251
+ options: ["Re-author the first draft", "Treat it as finished"]
252
+ answer_var: resume_choice
253
+
254
+ # Taking it as finished is a decision about the DOCUMENT, so it clears the marker and
255
+ # hands the run to the delta path — the same path an ordinary later specify takes.
256
+ - id: accept_draft
257
+ condition: "{{resume_choice}} == Treat it as finished"
258
+ action: set
259
+ vars: { is_bootstrap: "false" }
260
+
261
+ - id: accept_draft_clear
262
+ condition: "{{resume_choice}} == Treat it as finished"
263
+ action: execute_script
264
+ script: { ref: actions }
265
+ subcommand: version-bootstrap-done
266
+ args: ["--feature-dir", "{{feature_dir}}"]
267
+
268
+ # ═══════════════════════ BOOTSTRAP — no finished spec.md yet ══════════════
189
269
  # Copy the spec template into the (already-existing) feature dir + init version.
190
270
  - id: boot_scaffold
191
- condition: "{{spec_exists}} == false"
271
+ condition: "{{is_bootstrap}} == true"
192
272
  action: execute_script
193
273
  script: { ref: actions }
194
274
  subcommand: scaffold
195
275
  args: ["--feature-dir", "{{feature_dir}}", "--allow-existing"]
196
276
 
197
277
  - id: boot_version_init
198
- condition: "{{spec_exists}} == false"
278
+ condition: "{{is_bootstrap}} == true"
199
279
  action: execute_script
200
280
  script: { ref: actions }
201
281
  subcommand: version-init
202
- args: ["--feature-dir", "{{feature_dir}}"]
282
+ args: ["--feature-dir", "{{feature_dir}}", "--bootstrap"]
203
283
 
204
284
  # Copy the domain-model template into the CONTEXT (not the feature) when the context
205
285
  # has no model yet. Idempotent and non-destructive: the second feature in a context
206
286
  # finds the model already there and adds its aggregate to it rather than replacing it.
207
287
  - id: boot_model_scaffold
208
- condition: "{{spec_exists}} == false"
288
+ condition: "{{is_bootstrap}} == true"
209
289
  action: execute_script
210
290
  script: { ref: actions }
211
291
  subcommand: model-scaffold
@@ -215,7 +295,7 @@ workflow:
215
295
  - { name: model_was_created, from: "$.created" }
216
296
 
217
297
  - id: boot_ctx
218
- condition: "{{spec_exists}} == false"
298
+ condition: "{{is_bootstrap}} == true"
219
299
  action: build_context
220
300
  context:
221
301
  - "{{spec_file}}" # the copied template = section structure
@@ -241,7 +321,7 @@ workflow:
241
321
  # no file and no ux/ rules mapped, the section resolves to
242
322
  # "None" and the spec is unchanged from a backend-only one.
243
323
  - id: boot_generate
244
- condition: "{{spec_exists}} == false"
324
+ condition: "{{is_bootstrap}} == true"
245
325
  action: agent_author
246
326
  output_target: { type: file, path: "{{spec_file}}" }
247
327
  task: |
@@ -334,7 +414,7 @@ workflow:
334
414
  # which a frontend-only project has as much as any other; where an invariant is
335
415
  # ENFORCED is a plan concern and is recorded in backend-plan.md.
336
416
  - id: boot_model_ctx
337
- condition: "{{spec_exists}} == false"
417
+ condition: "{{is_bootstrap}} == true"
338
418
  action: build_context
339
419
  context:
340
420
  - "{{domain_model}}" # the model (template on a first feature, populated on a later one)
@@ -343,7 +423,7 @@ workflow:
343
423
  - { path: .appweaver/memory/constitution.ui.md, optional: true }
344
424
 
345
425
  - id: boot_model_generate
346
- condition: "{{spec_exists}} == false"
426
+ condition: "{{is_bootstrap}} == true"
347
427
  action: agent_author
348
428
  output_target: { type: file, path: "{{domain_model}}" }
349
429
  task: |
@@ -379,15 +459,18 @@ workflow:
379
459
 
380
460
  # ═════════════════════════ DELTA — spec.md exists ═════════════════════════
381
461
  # A later requirement = a new user story on the SAME aggregate. Delegate to the
382
- # delta-spec module: it opens the change (cr-setup, single-active-change guarded) and
383
- # authors the delta-spec ONLY — the master spec.md is left UNTOUCHED. The change
384
- # stays OPEN; finalize-delta merges the delta into spec.md later in the chain. Runs in
385
- # this context (shares vars + the DDD + domain sticky standards). The former standalone
386
- # `cr` capture's spec half now lives here. Downstream: plan (the delta plans) →
462
+ # delta-spec module: it opens the change (cr-setup) and authors BOTH of the change's
463
+ # halves — the delta-spec under changes/<cr-id>/ and the model delta beside the context's
464
+ # master model — leaving the master spec.md and domain-model.md UNTOUCHED. MANY changes
465
+ # may be open at once: every call on an established feature opens its own, the wave is
466
+ # what orders them, and the single-active-change guard that used to refuse a second one
467
+ # is gone. The change stays OPEN; finalize-delta merges both halves later in the chain.
468
+ # Runs in this context (shares vars + the DDD + domain sticky standards). The former
469
+ # standalone `cr` capture's spec half now lives here. Downstream: plan (the delta plans) →
387
470
  # analyze-plan-delta → implement → build → analyze-slice-delta → finalize-delta → analyze-slice →
388
471
  # close-delta (close-delta is what finally archives the change and bumps the version).
389
472
  - id: delta_spec_module
390
- condition: "{{spec_exists}} == true"
473
+ condition: "{{is_bootstrap}} == false"
391
474
  action: include
392
475
  module: .appweaver/orchestrator/modules/delta-spec.orch.yaml
393
476
 
@@ -404,7 +487,7 @@ workflow:
404
487
  # content; note it rewrites the file, so it is deliberately scoped to the path that
405
488
  # authored the document (a delta run no longer resets the master's scored boxes).
406
489
  - id: create_checklist
407
- condition: "{{spec_exists}} == false"
490
+ condition: "{{is_bootstrap}} == true"
408
491
  action: execute_script
409
492
  script: { ref: actions }
410
493
  subcommand: create-checklist
@@ -418,7 +501,7 @@ workflow:
418
501
  # checklist's items assume a full spec and would mis-fire here, which is exactly why
419
502
  # this is a separate checklist rather than the same one re-pointed.
420
503
  - id: create_delta_checklist
421
- condition: "{{spec_exists}} == true"
504
+ condition: "{{is_bootstrap}} == false"
422
505
  action: execute_script
423
506
  script: { ref: actions }
424
507
  subcommand: create-checklist
@@ -526,6 +609,24 @@ workflow:
526
609
  outputs:
527
610
  - { name: checklist_passed, from: "$.after_checked" }
528
611
 
612
+ # 7b. THE FIRST DRAFT IS FINISHED — lower the marker. Placed HERE, after the converge
613
+ # block, because that is the last thing the bootstrap path does to the document: a
614
+ # run that died inside the checklist or the clarify loop left a spec.md that is
615
+ # present but not yet what it will be, and clearing earlier would certify exactly the
616
+ # half-written state the marker exists to catch. Everything between version-init
617
+ # --bootstrap and this step is the window a crash is recoverable in.
618
+ #
619
+ # Idempotent: clearing a marker that is already down is the ordinary outcome of a
620
+ # re-run and is not an error, so this needs no guard beyond the branch.
621
+ - id: boot_done
622
+ condition: "{{is_bootstrap}} == true"
623
+ action: execute_script
624
+ script: { ref: actions }
625
+ subcommand: version-bootstrap-done
626
+ args: ["--feature-dir", "{{feature_dir}}"]
627
+ outputs:
628
+ - { name: bootstrap_cleared, from: "$.cleared" }
629
+
529
630
  # 8. Read the current version for the report (both paths).
530
631
  - id: version_read
531
632
  action: execute_script