aw-spec-cli 0.1.18 → 0.1.20

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 (77) hide show
  1. package/assets/.appweaver/DESIGN-FORMAT.md +63 -31
  2. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +62 -14
  3. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +10 -6
  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/delta-design-template.skeleton.md +1 -1
  8. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +22 -13
  9. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +1 -1
  10. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +6 -4
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +399 -131
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +1 -1
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +199 -38
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +1 -1
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +159 -25
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +20 -9
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +1 -1
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +183 -40
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +1 -1
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +311 -36
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +221 -40
  22. package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +217 -174
  23. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +494 -109
  24. package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +1 -1
  25. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +83 -18
  26. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +319 -0
  27. package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +1 -1
  28. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +29 -23
  29. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +143 -34
  30. package/assets/.appweaver/scripts/bash/aw-inventory.sh +15 -0
  31. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +878 -51
  32. package/assets/.appweaver/scripts/bash/aw-xref.sh +3 -0
  33. package/assets/.appweaver/scripts/bash/common.sh +28 -1
  34. package/assets/.appweaver/scripts/powershell/aw-inventory.ps1 +15 -0
  35. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +868 -51
  36. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +3 -0
  37. package/assets/.appweaver/scripts/python/aw-inventory.py +1832 -65
  38. package/assets/.appweaver/scripts/python/aw-xref.py +500 -81
  39. package/assets/.appweaver/templates/delta-design-template.md +1 -1
  40. package/assets/.appweaver/templates/delta-domain-model-template.md +1 -1
  41. package/assets/.appweaver/templates/delta-spec-template.md +4 -3
  42. package/assets/.appweaver/templates/design-template.md +56 -40
  43. package/assets/.appweaver/templates/okf-concept-template.md +11 -1
  44. package/assets/.appweaver/templates/proposed-inventory-template.md +127 -0
  45. package/assets/.appweaver/templates/source-map-template.md +5 -3
  46. package/assets/.appweaver/templates/spec-template.md +24 -21
  47. package/assets/.appweaver/templates/ui-delta-design-template.md +1 -1
  48. package/assets/.appweaver/templates/ui-design-template.md +11 -7
  49. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +4489 -361
  50. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +2274 -125
  51. package/assets/guardrails/domain/healthcare/auditing.domain.md +3 -3
  52. package/assets/guardrails/domain/pharma/auditing.domain.md +3 -3
  53. package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +1 -1
  54. package/assets/guardrails/technology/dotnet-webapi/auditing.md +1 -1
  55. package/assets/guardrails/technology/dotnet-webapi/authorization.md +1 -1
  56. package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +1 -1
  57. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +1 -1
  58. package/assets/guardrails/technology/dotnet-webapi/data-classification.md +1 -1
  59. package/assets/guardrails/technology/dotnet-webapi/eventing.md +1 -1
  60. package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +1 -1
  61. package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +1 -1
  62. package/assets/guardrails/technology/dotnet-webapi/persistence.md +1 -1
  63. package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +1 -1
  64. package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +1 -1
  65. package/assets/guardrails/technology/nextjs/accessibility-implementation.md +1 -1
  66. package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +1 -1
  67. package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +1 -1
  68. package/assets/guardrails/technology/nextjs/auth-session.md +1 -1
  69. package/assets/guardrails/technology/nextjs/component-architecture.md +1 -1
  70. package/assets/guardrails/technology/nextjs/design-system.md +1 -1
  71. package/assets/guardrails/technology/nextjs/error-handling.md +1 -1
  72. package/assets/guardrails/technology/nextjs/forms-and-validation.md +1 -1
  73. package/assets/guardrails/technology/nextjs/i18n.md +1 -1
  74. package/assets/guardrails/technology/nextjs/performance-budgets.md +1 -1
  75. package/assets/guardrails/technology/nextjs/routing-navigation.md +1 -1
  76. package/assets/guardrails/technology/nextjs/state-management.md +1 -1
  77. package/package.json +1 -1
@@ -95,7 +95,7 @@ workflow:
95
95
  action: verify_context
96
96
  require_non_empty: feature_dir
97
97
  on_empty: stop
98
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run parity."
98
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run parity."
99
99
 
100
100
  - id: verify_spec
101
101
  action: verify_context
@@ -85,7 +85,7 @@
85
85
 
86
86
  name: aw-spec-plan
87
87
  description: "Author the feature's lane plans from the spec and the bounded context's domain model — backend-plan.md when a backend lane is declared, frontend-plan.md when a frontend lane is, both when both are. Lanes come from .appweaver/stacks.json, so one command covers backend-only, frontend-only and full-stack projects; the former separate design-ui command is folded in. It ASKS what it is planning, the way implement asks what it is building: the master (`main`), one of the OPEN CHANGES, or a new plan-only change for a refactor with no spec delta — the argument doubles as the answer, and the menu appears only once a master exists, so a first run bootstraps with no prompt. `main` on a feature that already has masters RE-DERIVES them from the merged spec.md + domain-model.md — the operation finalize-delta's contract assumes when it merges a change's spec and model halves and no lane plan — and is confirmed before it rewrites anything. A change instead gets delta-backend-plan.md / delta-frontend-plan.md inside changes/<CR-id>/ while the masters stay untouched, written against the effective model (the master model, the change's own model delta, and the pending model deltas of LOWER waves, which are its predecessors) and against the feature's real source files from its OKF concept. Master authoring is template-driven: the kit's templates by default, or the code-derived as-built structure when onboard installed an override. Authoring only — a master gates at analyze-plan; a change ships through the CR chain."
88
- version: 5.0.0
88
+ version: 5.1.0
89
89
  agent_agnostic: true
90
90
  dialect: aw-spec.orchestrator/v1
91
91
 
@@ -164,6 +164,10 @@ vars:
164
164
  plan_mode: "bootstrap" # bootstrap | rederive | delta — reported, not branched on
165
165
  domain_model: ""
166
166
  domain_model_exists: "false"
167
+ owned_sections: "" # the model section(s) this feature answers for — feature-aggregates
168
+ shared_with_csv: "" # the other features answering for the same aggregate(s)
169
+ one_to_one: "true"
170
+ model_scope_plan: "" # which part of those sections the domain layer realises
167
171
  source_files: [] # the feature's real source files, from its OKF concept
168
172
  source_count: "0"
169
173
 
@@ -200,18 +204,21 @@ workflow:
200
204
  # project's standards are exactly what they were before the two commands merged.
201
205
  # No blocking gate: an unconfigured project soft-continues (the constitutions still
202
206
  # carry each lane's ratified base).
207
+ #
208
+ # ONLY THE BACKEND'S RULES LOAD HERE. The frontend's used to load alongside them,
209
+ # before either lane authored anything, and sticky standards only accumulate — so the
210
+ # service plan was authored with `## /aw-spec-plan-ui` in its envelope and the
211
+ # experience plan with the service set. Each prompt names its lane, but it said so
212
+ # inside an envelope that contradicted it. The UI set now loads at the frontend
213
+ # boundary instead, with `replace`, in both places a frontend document is authored:
214
+ # tech_select_ui_master before the master frontend sections, and the delta-plan
215
+ # module's own swap before the frontend delta plan.
203
216
  - id: tech_select
204
217
  condition: "{{be_declared}} == true"
205
218
  action: include
206
219
  module: .appweaver/orchestrator/modules/tech-select.orch.yaml
207
220
  with: { command: aw-spec-plan }
208
221
 
209
- - id: tech_select_ui
210
- condition: "{{fe_declared}} == true"
211
- action: include
212
- module: .appweaver/orchestrator/modules/tech-select.orch.yaml
213
- with: { command: aw-spec-plan-ui }
214
-
215
222
  # 2. Resolve the current feature and require its spec.
216
223
  - id: feature_paths
217
224
  action: execute_script
@@ -238,11 +245,42 @@ workflow:
238
245
  - { name: domain_model, from: "$.domain_model" }
239
246
  - { name: domain_model_exists, from: "$.domain_model_exists" }
240
247
 
248
+ # WHICH PART OF THAT MODEL THIS PLAN REALISES. The model is filed by aggregate, and a
249
+ # feature is a capability scope the developer chose: it may act on several aggregates,
250
+ # and several features may act on one. Resolved from the spec's Acts-on line, then the
251
+ # boundary map, then the feature name. on_error: continue — without an answer the
252
+ # prompt falls back to the model as a whole, which is what it read before.
253
+ - id: feature_aggregates
254
+ action: execute_script
255
+ on_error: continue
256
+ script: { ref: xref }
257
+ subcommand: feature-aggregates
258
+ args: ["--feature-dir", "{{feature_dir}}"]
259
+ outputs:
260
+ - { name: owned_sections, from: "$.owned_sections" }
261
+ - { name: shared_with_csv, from: "$.shared_with_csv" }
262
+ - { name: one_to_one, from: "$.one_to_one" }
263
+
264
+ # HOW MUCH OF THOSE SECTIONS THE DOMAIN LAYER CARRIES. A feature that alone answers for
265
+ # an aggregate realises its whole section. A feature that shares it realises only what
266
+ # its own requirements act on: the rest is a sibling feature's, and planning it here
267
+ # would give two plans the same element to realise. The engine has no conditional
268
+ # prompt text, so the rule is an ordered pair of sets handed to the prompt as one line.
269
+ - id: model_scope_shared
270
+ action: set
271
+ vars:
272
+ model_scope_plan: "{{owned_sections}}, which this feature SHARES with other features. Realise only the entities, value objects, states and BR-nnn this feature's requirements act on, under the model's names and boundary; an element of that section this feature does not use belongs to another feature and is left out — never redefined, renamed or narrowed"
273
+ - id: model_scope_sole
274
+ condition: "{{one_to_one}} == true"
275
+ action: set
276
+ vars:
277
+ model_scope_plan: "{{owned_sections}}. This feature is the only one that answers for it, so the domain layer realises that whole section"
278
+
241
279
  - id: require_feature
242
280
  action: verify_context
243
281
  require_non_empty: feature_dir
244
282
  on_empty: stop
245
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run plan."
283
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run plan."
246
284
 
247
285
  - id: verify_spec
248
286
  action: verify_context
@@ -528,9 +566,10 @@ workflow:
528
566
  - THE SPEC AND THE DOMAIN MODEL ARE THE ONLY DOMAIN INPUTS. You have them plus the
529
567
  technical standards, and NO domain rule files. Realise what they state, and invent
530
568
  no domain rule they omit (an unavoidable new concept → §7 DA-n).
531
- - THE DOMAIN LAYER (§1) MUST FOLLOW THE CONTEXT'S DOMAIN MODEL EXACTLY: entities &
532
- value objects are its `## Aggregate:` section's entities (SAME names), invariants
533
- are its BR-nnn (cite each, never restate), the aggregate boundary is its Boundary
569
+ - THE DOMAIN LAYER (§1) MUST FOLLOW THE CONTEXT'S DOMAIN MODEL EXACTLY. The
570
+ section(s) this feature answers for: {{model_scope_plan}}. Within that scope,
571
+ entities & value objects are the section's entities (SAME names), invariants are
572
+ its BR-nnn (cite each, never restate), the aggregate boundary is its Boundary
534
573
  block, and states are the lifecycle it describes. The model — not the spec — is
535
574
  where those facts live; the spec cites them by id. This document records WHERE
536
575
  each invariant is ENFORCED, never what it says. Elaborate with domain events /
@@ -628,10 +667,11 @@ workflow:
628
667
  textbook layer gets its own section; a layer the feature does not need does NOT
629
668
  appear.
630
669
  - KEEP THE NEUTRAL ANCHORS (the template pins them): §1.2 is the domain/data MODEL
631
- and MUST follow the CONTEXT'S DOMAIN MODEL EXACTLY — entities & value objects are
632
- its aggregate section's entities (SAME names), invariants are its BR-nnn (cite
633
- each and record WHERE each is enforced), the boundary is its Boundary block,
634
- states are its lifecycle; cross-slice references-by-id live here. One §2
670
+ and MUST follow the CONTEXT'S DOMAIN MODEL EXACTLY for the section(s) this feature
671
+ answers for — {{model_scope_plan}}. Entities & value objects are the section's
672
+ entities (SAME names), invariants are its BR-nnn (cite each and record WHERE each
673
+ is enforced), the boundary is its Boundary block, states are its lifecycle;
674
+ cross-slice references-by-id live here. One §2
635
675
  subsection is the EXTERNAL INTEGRATIONS table (its heading contains "Integration")
636
676
  — one port/adapter row per IC-, realising the obligation the spec named.
637
677
  - REALISE THE LIFTED CONSTRAINTS (each cites its spec id): CN- → structural ones
@@ -701,9 +741,10 @@ workflow:
701
741
  A row whose id is gone is deleted along with what it justified. A spec id or model
702
742
  invariant with NO plan element is a GAP — realise it, in the shape the rest of the
703
743
  document already uses.
704
- - THE DOMAIN LAYER (§1) MUST FOLLOW THE CONTEXT'S DOMAIN MODEL EXACTLY: entities &
705
- value objects are its `## Aggregate:` section's entities (SAME names), invariants
706
- are its BR-nnn (cite each, never restate), the aggregate boundary is its Boundary
744
+ - THE DOMAIN LAYER (§1) MUST FOLLOW THE CONTEXT'S DOMAIN MODEL EXACTLY. The
745
+ section(s) this feature answers for: {{model_scope_plan}}. Within that scope,
746
+ entities & value objects are the section's entities (SAME names), invariants are
747
+ its BR-nnn (cite each, never restate), the aggregate boundary is its Boundary
707
748
  block, and states are the lifecycle it describes. Where the model has MOVED since
708
749
  this plan was written, the model is right and the plan is stale.
709
750
  - CARRY THE LEDGERS FORWARD. §7 DA-n architectural decisions and the WA-n
@@ -722,6 +763,23 @@ workflow:
722
763
  (the spec ids and BR-nnn that drove it). Do NOT invent a version number — the
723
764
  version spine belongs to close-delta.
724
765
 
766
+ # ══════════════ THE LANE SWAP — the frontend's rules replace the backend's ═══
767
+ # Both tech-select includes used to run up at step 4, before either lane authored
768
+ # anything, and sticky standards only accumulate — so on a full-stack run the backend
769
+ # plan was authored with `## /aw-spec-plan-ui` in its envelope and the frontend plan with
770
+ # the service set. Each prompt names its lane, but it did so inside an envelope that
771
+ # contradicted it, and the envelope is the half the model cannot argue with.
772
+ #
773
+ # `fe_mode != skip` is what makes this the MASTER path's swap and not the delta path's:
774
+ # fe_mode is only set to a real mode by the master branches above, so on a `plan CR-…`
775
+ # run this does not fire and the delta module below does its own swap at its own
776
+ # frontend boundary.
777
+ - id: tech_select_ui_master
778
+ condition: "{{fe_mode}} != skip"
779
+ action: include
780
+ module: .appweaver/orchestrator/modules/tech-select.orch.yaml
781
+ with: { command: aw-spec-plan-ui, replace: "true" }
782
+
725
783
  # ═══════════ FRONTEND LANE — bootstrap, greenfield experience template ═══════
726
784
  # The experience twin of the service plan, and the MEDIATOR between spec.md and the
727
785
  # frontend code. Its sections are the experience, not the four clean-architecture
@@ -993,7 +1051,14 @@ workflow:
993
1051
  - { name: wave, from: "$.wave" }
994
1052
  - { name: predecessor_files, from: "$.predecessor_files" }
995
1053
  - { name: predecessor_count, from: "$.predecessor_count" }
1054
+ - { name: predecessor_ids, from: "$.predecessor_ids" }
996
1055
  - { name: sibling_count, from: "$.sibling_count" }
1056
+ - { name: sibling_ids, from: "$.sibling_ids" }
1057
+ # Predecessors AND siblings in one list: the delta plan is authored against both, and
1058
+ # a build_context takes a single `context_from:` variable. Which entries are which is
1059
+ # carried by predecessor_ids / sibling_ids, because the planner's licence over the two
1060
+ # is opposite — a predecessor may be built on, a sibling may only be designed around.
1061
+ - { name: related_docs, from: "$.related_docs" }
997
1062
 
998
1063
  # Author that change's delta plans — one per declared lane whose MASTER exists, inside
999
1064
  # changes/<CR-id>/ beside its delta-spec. The masters are left UNTOUCHED; the change
@@ -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 feature, e.g. Order or PatientRegistration), 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."
@@ -64,7 +64,7 @@ workflow:
64
64
  action: verify_context
65
65
  require_non_empty: feature_dir
66
66
  on_empty: stop
67
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run run."
67
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run run."
68
68
 
69
69
  # Require the SERVICE LANE and its source path. Migrations belong to the backend
70
70
  # lane by definition, and run-migrate searches that lane's root for the project
@@ -1,17 +1,21 @@
1
1
  # =============================================================================
2
2
  # aw-spec-select-feature (orchestrator) — pick or create the active FEATURE
3
3
  #
4
- # feature = AGGREGATE (e.g. Order). This is the FIRST step of the spec-anchored
5
- # pipeline: it fixes the aggregate boundary BEFORE any spec/design exists, so the
6
- # feature's single spec.md + backend-plan.md and every command (CreateOrder/UpdateOrder =
7
- # user stories) hang off one directory. It replaces guessing the aggregate out of prose in
8
- # `specify` — that is why finding #1 (feature=aggregate) is enforced here, not hoped for.
4
+ # A FEATURE IS A CAPABILITY SCOPE THE DEVELOPER CHOOSES (e.g. Order, or
5
+ # PatientRegistration). It may act on several aggregates, and several features may act on
6
+ # one: the aggregates live in the bounded context's domain model and the OKF bundle, above
7
+ # every feature, and the spec names the ones this feature acts on. This is the FIRST step
8
+ # of the spec-anchored pipeline: it fixes the feature's scope BEFORE any spec/plan exists,
9
+ # so its single spec.md + backend-plan.md and every operation (CreateOrder/UpdateOrder =
10
+ # user stories) hang off one directory.
9
11
  #
10
12
  # The pickable list is TWO merged sources (deduped by name): specs/ sub-folders
11
- # (features already scaffolded/adopted) AND the OKF bundle's Aggregate concepts —
12
- # the brownfield feature map `/appweaver onboard` reverse-engineers. So in a
13
- # brownfield repo you pick a real discovered code feature BEFORE anything is under
14
- # specs/; on greenfield (no bundle) the list is specs-only, exactly as before.
13
+ # (features already scaffolded/adopted) AND the features the OKF bundle's concepts are
14
+ # known to serve — from the boundary map the developer confirmed, else from each
15
+ # concept's `features:` list. So in a brownfield repo you pick a real discovered code
16
+ # feature BEFORE anything is under specs/. An aggregate is never offered as a feature
17
+ # nobody defined: on greenfield every feature already has its specs/ directory, so the
18
+ # list is specs-only.
15
19
  #
16
20
  # SCOPED BY THE ACTIVE BOUNDED CONTEXT. When .appweaver/context.json names one, both
17
21
  # the listing and the creation happen inside specs/<Context>/ — so a context shows
@@ -22,7 +26,7 @@
22
26
  # ZERO LLM: only scripts + one select + (conditionally) one ask. No generation,
23
27
  # no standards, no domain-select. Pure, deterministic orchestration/governance.
24
28
  #
25
- # list-features (specs/ ∪ OKF aggregates) → user_select (existing | "+ create new") → [ask name] → set-feature
29
+ # list-features (specs/ ∪ features the OKF concepts serve) → user_select (existing | "+ create new") → [ask name] → set-feature
26
30
  #
27
31
  # Persists .appweaver/feature.json {"feature_directory":"specs/<Feature>"} so every
28
32
  # later command operates on it: the forward loop (specify/plan/analyze-plan/implement/
@@ -36,14 +40,14 @@
36
40
  # =============================================================================
37
41
 
38
42
  name: aw-spec-select-feature
39
- description: Pick an existing feature (aggregate) or create a new one, and store it in .appweaver/feature.json so every AW-Spec command operates on it. Zero LLM — scripts + one selection only.
43
+ description: Pick an existing feature (a capability scope you choose) or create a new one, and store it in .appweaver/feature.json so every AW-Spec command operates on it. Zero LLM — scripts + one selection only.
40
44
  version: 1.0.0
41
45
  agent_agnostic: true
42
46
  dialect: aw-spec.orchestrator/v1
43
47
 
44
48
  inputs:
45
49
  user_input:
46
- description: Optional feature (aggregate) name to create or select directly, e.g. Order.
50
+ description: Optional feature name to create or select directly, e.g. Order or PatientRegistration.
47
51
  required: false
48
52
  bind: USER_INPUT
49
53
 
@@ -85,11 +89,12 @@ workflow:
85
89
  on_false_message: >
86
90
  {{scope_message}}
87
91
 
88
- # 1. Enumerate selectable aggregates from TWO merged sources (deduped by name):
89
- # specs/ sub-folders (already scaffolded/adopted) AND the OKF bundle's
90
- # Aggregate concepts — the brownfield feature map onboard reverse-engineers,
91
- # so a not-yet-adopted code feature is pickable before its specs/ dir exists.
92
- # On greenfield (no bundle) the OKF source is empty → specs-only, as before.
92
+ # 1. Enumerate selectable FEATURES from TWO merged sources (deduped by name):
93
+ # specs/ sub-folders (already scaffolded/adopted) AND the features the OKF
94
+ # bundle's aggregate concepts serve — the boundary map first, else each concept's
95
+ # `features:` list — so a not-yet-adopted code feature is pickable before its
96
+ # specs/ dir exists. A concept greenfield implement wrote contributes nothing:
97
+ # its features all have specs/ dirs, and its own name is an aggregate.
93
98
  # `select_options` carries the "+ create new" sentinel and (when USER_INPUT
94
99
  # names a not-yet feature) that name injected — a direct arg is one-list select.
95
100
  - id: list_features
@@ -108,7 +113,7 @@ workflow:
108
113
  # no prompt; the lone sentinel at bootstrap (no features yet) auto-confirms.
109
114
  - id: pick
110
115
  action: user_select
111
- prompt: "Select a feature (the aggregate, e.g. Order) in {{feature_base}}/ — or create a new one:"
116
+ prompt: "Select a feature (a capability scope, e.g. Order or PatientRegistration) in {{feature_base}}/ — or create a new one:"
112
117
  options_from_variable: select_options
113
118
  preselect_from: USER_INPUT
114
119
  single_option_confirm: true
@@ -120,9 +125,10 @@ workflow:
120
125
  action: ask
121
126
  condition: "{{chosen}} == {{new_sentinel}}"
122
127
  prompt: >
123
- Name the new feature — this is the AGGREGATE root (e.g. Order, Invoice,
124
- Customer), NOT a single command. Operations like CreateOrder / UpdateOrder
125
- become user stories inside it:
128
+ Name the new feature — the capability scope you want to specify and build as
129
+ one unit (e.g. Order, PatientRegistration). It may act on several aggregates, and
130
+ other features may act on the same ones; its spec names them. Each operation in
131
+ its scope (e.g. CreateOrder / UpdateOrder) becomes a user story inside it:
126
132
  answer_var: typed_name
127
133
 
128
134
  # 4. Coalesce the target name. The engine has no compound conditions, so this
@@ -136,7 +142,7 @@ workflow:
136
142
  condition: "{{chosen}} == {{new_sentinel}}"
137
143
  vars: { target_name: "{{typed_name}}" }
138
144
 
139
- # 5. Create-or-select the aggregate dir + persist it as the active feature.
145
+ # 5. Create-or-select the feature dir + persist it as the active feature.
140
146
  - id: set_feature
141
147
  action: execute_script
142
148
  script: { ref: actions }
@@ -157,5 +163,5 @@ outputs:
157
163
  context: "{{context}}"
158
164
  created: "{{created}}"
159
165
  spec_exists: "{{spec_exists}}"
160
- note: "Active feature set in .appweaver/feature.json — the aggregate boundary for its single spec.md + backend-plan.md. `context` names the bounded context it was created in, and is empty on a repo with no active context (the flat specs/<Feature> layout, which keeps working unchanged)."
166
+ note: "Active feature set in .appweaver/feature.json — the capability scope that owns its single spec.md + backend-plan.md. `context` names the bounded context it was created in, and is empty on a repo with no active context (the flat specs/<Feature> layout, which keeps working unchanged)."
161
167
  next: "/appweaver specify <what this feature does> (authors {{feature_dir}}/spec.md; each operation = a user story)"