aw-spec-cli 0.1.19 → 0.1.21
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.
- package/assets/.appweaver/DESIGN-FORMAT.md +17 -9
- package/assets/.appweaver/ORCH-DIALECT.md +8 -3
- package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +26 -3
- package/assets/.appweaver/orchestrator/orchestrator.py +65 -3
- package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +1 -1
- package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +4 -2
- package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +1 -1
- package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +1 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +121 -42
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +7 -7
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +145 -52
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +18 -10
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +306 -28
- package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +1 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +1 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +1 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +4 -4
- package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +1 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +114 -30
- package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +140 -36
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +385 -61
- package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +217 -174
- package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +407 -107
- package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +1 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +49 -11
- package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +1 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +1 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +29 -23
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +47 -23
- package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +19 -11
- package/assets/.appweaver/scripts/bash/aw-inventory.sh +15 -0
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +683 -92
- package/assets/.appweaver/scripts/bash/aw-xref.sh +3 -0
- package/assets/.appweaver/scripts/powershell/aw-inventory.ps1 +15 -0
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +625 -64
- package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +3 -0
- package/assets/.appweaver/scripts/python/aw-crstate.py +194 -0
- package/assets/.appweaver/scripts/python/aw-foundation.py +151 -0
- package/assets/.appweaver/scripts/python/aw-inventory.py +1829 -63
- package/assets/.appweaver/scripts/python/aw-payload-parity.py +3 -0
- package/assets/.appweaver/scripts/python/aw-plan-coverage.py +423 -0
- package/assets/.appweaver/scripts/python/aw-stack.py +251 -28
- package/assets/.appweaver/scripts/python/aw-testlog.py +148 -0
- package/assets/.appweaver/scripts/python/aw-xref.py +500 -81
- package/assets/.appweaver/stack-profiles/dotnet-webapi.json +1 -0
- package/assets/.appweaver/stack-profiles/java-spring.json +1 -0
- package/assets/.appweaver/stack-profiles/node-express.json +1 -0
- package/assets/.appweaver/stack-profiles/python-fastapi.json +1 -0
- package/assets/.appweaver/templates/constitution-domain-template.md +17 -2
- package/assets/.appweaver/templates/delta-design-template.md +1 -1
- package/assets/.appweaver/templates/delta-domain-model-template.md +1 -1
- package/assets/.appweaver/templates/delta-spec-template.md +4 -3
- package/assets/.appweaver/templates/design-template.md +16 -11
- package/assets/.appweaver/templates/okf-concept-template.md +11 -1
- package/assets/.appweaver/templates/proposed-inventory-template.md +127 -0
- package/assets/.appweaver/templates/source-map-template.md +5 -3
- package/assets/.appweaver/templates/spec-template.md +17 -17
- package/assets/.appweaver/templates/ui-delta-design-template.md +1 -1
- package/assets/.appweaver/templates/ui-design-template.md +4 -4
- package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +4489 -361
- package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +2274 -125
- package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +10 -6
- package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +4 -0
- package/assets/guardrails/technology/dotnet-webapi/auditing.md +3 -1
- package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +5 -3
- package/assets/guardrails/technology/dotnet-webapi/cqrs.md +5 -1
- package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +13 -5
- package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +2 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +6 -2
- package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +70 -0
- package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +3 -0
- package/assets/guardrails/technology/dotnet-webapi/testing.md +10 -0
- package/assets/guardrails/technology/nextjs/ui-constitution.md +8 -1
- package/package.json +1 -1
- package/assets/.appweaver/orchestrator/.state/smoke.state.json +0 -32
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
# drift. What this stage must NOT do is hide that by merging a delta plan: the plan would
|
|
41
41
|
# then agree with the spec without any run of `plan` ever having derived it.
|
|
42
42
|
#
|
|
43
|
-
# ✅ THE DETECTION IS BUILT (2026-09-14).
|
|
44
|
-
#
|
|
45
|
-
# and `implement` reads it through plan-gate-check before any
|
|
46
|
-
# master run whose plans are no longer the ones the gate
|
|
43
|
+
# ✅ THE DETECTION IS BUILT (2026-09-14). Every master-mode analyze-plan run leaves
|
|
44
|
+
# `.aw-plan-gate.json` — the sha256 of spec.md, the context's domain-model.md and each lane
|
|
45
|
+
# plan it read, and its verdict — and `implement` reads it through plan-gate-check before any
|
|
46
|
+
# lane generates, refusing a master run whose plans are no longer the ones the gate checked. So after this merge the
|
|
47
47
|
# plans are not merely "visibly behind": the next `implement` on the master path says so and
|
|
48
48
|
# stops, naming `plan main`. A feature with no receipt has never been gated since the receipt
|
|
49
49
|
# existed and is reported, never refused.
|
|
@@ -94,17 +94,33 @@
|
|
|
94
94
|
# REBASE. The delta carries the master version it was authored against (`**Base version**:`,
|
|
95
95
|
# stamped by cr-setup). If the master has moved, the delta's MODIFIED items name a BEFORE
|
|
96
96
|
# spec.md no longer says, and merging them is last-writer-wins over work that landed.
|
|
97
|
+
# cr-rebase runs FIRST and re-stamps that header — and `**Base context version**` — when
|
|
98
|
+
# the move is one this delta survives: the masters moved, and everything that landed in
|
|
99
|
+
# between touched OTHER aggregates than the ones this delta names. Nothing else in the kit
|
|
100
|
+
# could ever write those headers, so before this the only way past the refusal was a hand
|
|
101
|
+
# edit the authoring prompts forbid. Where a landed change DOES share an aggregate, the
|
|
102
|
+
# stamp is left alone and the refusal below names it.
|
|
97
103
|
#
|
|
98
104
|
# MERGED-NOT-CLOSED. The masters can move WITHOUT the version moving, because this stage
|
|
99
|
-
# merges and close-delta bumps. In that window
|
|
100
|
-
# reflect, so a sibling sharing the same base_version compares EQUAL and would merge
|
|
101
|
-
# masters that had already shifted. The version alone is not a proxy for "the masters
|
|
102
|
-
# moved";
|
|
105
|
+
# merges and close-delta bumps. In that window the masters carry a change the number does
|
|
106
|
+
# not reflect, so a sibling sharing the same base_version compares EQUAL and would merge
|
|
107
|
+
# onto masters that had already shifted. The version alone is not a proxy for "the masters
|
|
108
|
+
# moved"; a Change History row naming the change is the other half of the answer. That test
|
|
109
|
+
# lives in aw-crstate — one definition for this fence, cr-recall-check and close-delta —
|
|
110
|
+
# and it reads the CR CELL of a row, not the id anywhere in the file, which used to fire on
|
|
111
|
+
# an id quoted in prose. It looks at the context model too, which every feature shares, so
|
|
112
|
+
# a SIBLING feature's merged-unclosed change is visible; a sibling counts only where it
|
|
113
|
+
# touched an aggregate this delta also names.
|
|
103
114
|
#
|
|
104
115
|
# PREDECESSOR. A lower wave is committed to landing first, and `plan` put exactly those
|
|
105
|
-
# deltas in this one's context as the base it was planned on top of.
|
|
106
|
-
#
|
|
107
|
-
#
|
|
116
|
+
# deltas in this one's context as the base it was planned on top of. Same-wave changes are
|
|
117
|
+
# independent by definition, so the wave is the conservative signal.
|
|
118
|
+
#
|
|
119
|
+
# DECLARED DEPENDENCIES vs THE WAVE. The wave is a number; `## Depends on` in the change's
|
|
120
|
+
# own delta documents is what that number is supposed to encode. specify derives the wave
|
|
121
|
+
# from that section, and this stage re-checks it (cr-wave-derive): a change naming a
|
|
122
|
+
# predecessor at the same or a higher wave contradicts itself, and the predecessor fence
|
|
123
|
+
# above — which sees only numbers — would let the two merge in either order.
|
|
108
124
|
#
|
|
109
125
|
# All three are RAISE-then-VERIFY and fail-closed: each flag is raised only by the good
|
|
110
126
|
# state, so a check that did not run leaves the merge refused rather than waved through.
|
|
@@ -153,6 +169,17 @@ vars:
|
|
|
153
169
|
downstream_csv: "" # the blast-radius names, fed to the scoped fence's --also and shown to the human
|
|
154
170
|
fence_ok: "true" # default green so the gate is a no-op when nothing downstream
|
|
155
171
|
gate_decision: "clean" # audit: 'clean' unless a RED fence was deliberately overridden
|
|
172
|
+
# The change's own `## Depends on` vs its wave — the fence that keeps the number and the
|
|
173
|
+
# prose from drifting apart. Defaults green, so a run that cannot read them is not blocked.
|
|
174
|
+
wave_ok: "true"
|
|
175
|
+
wave_required: ""
|
|
176
|
+
wave_message: ""
|
|
177
|
+
# The automatic rebase (cr-rebase). Defaults say "nothing moved, nothing stamped",
|
|
178
|
+
# so a step that could not run leaves the existing refusal to speak.
|
|
179
|
+
rebase_clean: "false"
|
|
180
|
+
rebase_stamped: ""
|
|
181
|
+
rebase_conflicts: ""
|
|
182
|
+
rebase_message: ""
|
|
156
183
|
domain_model: "" # specs/<Context>/domain-model.md — the second merge target
|
|
157
184
|
domain_model_exists: "false"
|
|
158
185
|
delta_model: "" # specs/<Context>/domain-model.<id>.delta.md — the model half of the change
|
|
@@ -176,11 +203,12 @@ vars:
|
|
|
176
203
|
merge_wave: "1"
|
|
177
204
|
snapshot_dir: "" # changes/<id>/pre-merge/ — the before-image recall-delta restores
|
|
178
205
|
snapshot_count: "0"
|
|
179
|
-
# THE OKF OVERVIEW'S SUBJECT. The
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
# a behaviour-only change, which falls back to the
|
|
206
|
+
# THE OKF OVERVIEW'S SUBJECT. The Overview step selects a `## Aggregate:` section BY
|
|
207
|
+
# NAME, so it needs the aggregate, which is not always the feature: a feature that is
|
|
208
|
+
# one capability over a shared aggregate (PatientRegistration over Patient) has no
|
|
209
|
+
# section of its own name. delta_aggregates is what the change's own model delta
|
|
210
|
+
# declares; it is empty for a behaviour-only change, which falls back to the aggregate
|
|
211
|
+
# the feature answers for (feature-aggregates — the spec's Acts-on line).
|
|
184
212
|
delta_aggregates: ""
|
|
185
213
|
overview_scope: ""
|
|
186
214
|
overview_section_ok: "false"
|
|
@@ -211,7 +239,7 @@ workflow:
|
|
|
211
239
|
action: verify_context
|
|
212
240
|
require_non_empty: feature_dir
|
|
213
241
|
on_empty: stop
|
|
214
|
-
on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the
|
|
242
|
+
on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run finalize-delta."
|
|
215
243
|
|
|
216
244
|
- id: verify_spec
|
|
217
245
|
action: verify_context
|
|
@@ -351,6 +379,28 @@ workflow:
|
|
|
351
379
|
# NO on_error: continue. A gate that cannot run must not be a gate that passes, and the
|
|
352
380
|
# raise-then-verify below is fail-closed for the same reason: each flag is raised ONLY
|
|
353
381
|
# by the good state, so a field that never arrived leaves it down and the merge refuses.
|
|
382
|
+
# AUTO-REBASE FIRST, and only where it is EARNED. `**Base version**` was written once by
|
|
383
|
+
# cr-setup and nothing could ever update it — every other site reads it, and every
|
|
384
|
+
# authoring prompt forbids the model from writing it — so a moved master left the operator
|
|
385
|
+
# hand-editing the delta-spec in the middle of an automated loop. cr-rebase re-stamps it
|
|
386
|
+
# (and `**Base context version**`, which until now nothing read at all) when the masters
|
|
387
|
+
# moved and NOTHING THAT LANDED IN BETWEEN touched the aggregates this delta names: then
|
|
388
|
+
# the delta still describes the document it was written against, and the stamp is
|
|
389
|
+
# bookkeeping. A landed change sharing an aggregate, or one whose model delta cannot be
|
|
390
|
+
# read, leaves the stamp alone — and the refusal below fires with the overlap named.
|
|
391
|
+
# It runs BEFORE cr-merge-check so that check reads the stamp this step just settled.
|
|
392
|
+
- id: auto_rebase
|
|
393
|
+
action: execute_script
|
|
394
|
+
on_error: continue
|
|
395
|
+
script: { ref: actions }
|
|
396
|
+
subcommand: cr-rebase
|
|
397
|
+
args: ["--cr-id", "{{active_change}}", "--feature-dir", "{{feature_dir}}", "--stamp"]
|
|
398
|
+
outputs:
|
|
399
|
+
- { name: rebase_clean, from: "$.clean" }
|
|
400
|
+
- { name: rebase_stamped, from: "$.stamped" }
|
|
401
|
+
- { name: rebase_conflicts, from: "$.conflict_ids" }
|
|
402
|
+
- { name: rebase_message, from: "$.message" }
|
|
403
|
+
|
|
354
404
|
- id: merge_check
|
|
355
405
|
action: execute_script
|
|
356
406
|
script: { ref: actions }
|
|
@@ -375,7 +425,7 @@ workflow:
|
|
|
375
425
|
action: verify_context
|
|
376
426
|
require_true: rebase_ok
|
|
377
427
|
on_false: stop
|
|
378
|
-
on_false_message: "finalize-delta REFUSED — change {{active_change}} needs a REBASE. It was authored against master version {{base_version}}, and the feature now stands at {{current_version}}: another change finalized and closed while this one was open. Its MODIFIED items name a BEFORE that spec.md no longer says, so merging now would overwrite work that already landed. Rebase: run /appweaver analyze-plan-delta {{active_change}} against the current master, fix what it reports
|
|
428
|
+
on_false_message: "finalize-delta REFUSED — change {{active_change}} needs a REBASE. It was authored against master version {{base_version}}, and the feature now stands at {{current_version}}: another change finalized and closed while this one was open. Its MODIFIED items name a BEFORE that spec.md no longer says, so merging now would overwrite work that already landed. The automatic re-stamp did not apply, because what landed in between touches this delta's own aggregates: {{rebase_message}}. Rebase for real: run /appweaver analyze-plan-delta {{active_change}} against the current master, fix what it reports — then `.appweaver/scripts/bash/aw-spec-actions.sh cr-rebase --cr-id {{active_change}} --stamp` re-stamps `**Base version**` and `**Base context version**` once the overlap with {{rebase_conflicts}} is resolved, and /appweaver finalize-delta {{active_change}} can run again."
|
|
379
429
|
|
|
380
430
|
- id: merged_open_ok_raise
|
|
381
431
|
condition: "{{has_merged_open}} == false"
|
|
@@ -385,7 +435,7 @@ workflow:
|
|
|
385
435
|
action: verify_context
|
|
386
436
|
require_true: merged_open_ok
|
|
387
437
|
on_false: stop
|
|
388
|
-
on_false_message: "finalize-delta REFUSED — {{merged_open_ids}} has already been MERGED into
|
|
438
|
+
on_false_message: "finalize-delta REFUSED — {{merged_open_ids}} has already been MERGED into a master but not yet CLOSED, so the masters carry it while the version still reads {{current_version}}. That change may belong to ANOTHER feature in this context: domain-model.md is shared, so a sibling's merge moves a master this delta was authored against, and it is counted here when it touched an aggregate this delta also names. Change {{active_change}} was authored against those pre-merge masters and the version comparison cannot see the difference. Finish the merged change first: /appweaver analyze-slice then /appweaver close-delta {{merged_open_ids}}, which bumps the version — then rebase this one against the new number and re-run /appweaver finalize-delta {{active_change}}."
|
|
389
439
|
|
|
390
440
|
- id: no_blockers_raise
|
|
391
441
|
condition: "{{has_blockers}} == false"
|
|
@@ -395,7 +445,28 @@ workflow:
|
|
|
395
445
|
action: verify_context
|
|
396
446
|
require_true: no_blockers
|
|
397
447
|
on_false: stop
|
|
398
|
-
on_false_message: "finalize-delta REFUSED — change {{active_change}} is at wave {{merge_wave}} and {{blocker_count}} open change(s) sit at a LOWER wave: {{blocker_ids}}. A lower wave is committed to landing FIRST, and this delta was planned with those in its context as the base it builds on — merging it now folds in behaviour that assumes a predecessor the masters do not yet carry. Merge and close the lower-wave change(s) first (/appweaver finalize-delta <CR-id> → analyze-slice → close-delta), then rebase and re-run this one. Same-wave changes are independent and are not counted here; if this delta does NOT in fact build on them, /appweaver cr-wave
|
|
448
|
+
on_false_message: "finalize-delta REFUSED — change {{active_change}} is at wave {{merge_wave}} and {{blocker_count}} open change(s) sit at a LOWER wave: {{blocker_ids}}. A lower wave is committed to landing FIRST, and this delta was planned with those in its context as the base it builds on — merging it now folds in behaviour that assumes a predecessor the masters do not yet carry. Merge and close the lower-wave change(s) first (/appweaver finalize-delta <CR-id> → analyze-slice → close-delta), then rebase and re-run this one. Same-wave changes are independent and are not counted here; if this delta does NOT in fact build on them, correct its `## Depends on` section (naming only the deltas it truly builds on) and re-run /appweaver specify's wave derivation — `.appweaver/scripts/bash/aw-spec-actions.sh cr-wave-derive --cr-id {{active_change}} --stamp` applies it directly."
|
|
449
|
+
|
|
450
|
+
# THE DECLARED DEPENDENCIES AND THE WAVE MUST AGREE. The fence above orders changes by
|
|
451
|
+
# their wave NUMBER; this one checks that number against what the change's own documents
|
|
452
|
+
# SAY under `## Depends on`. Before this, the two could diverge silently: an author named
|
|
453
|
+
# a predecessor in prose, cr-setup left the wave at the predecessor's own level, and the
|
|
454
|
+
# fence above then read the two as independent and let them merge in either order.
|
|
455
|
+
- id: wave_declared_check
|
|
456
|
+
action: execute_script
|
|
457
|
+
on_error: continue
|
|
458
|
+
script: { ref: actions }
|
|
459
|
+
subcommand: cr-wave-derive
|
|
460
|
+
args: ["--cr-id", "{{active_change}}"]
|
|
461
|
+
outputs:
|
|
462
|
+
- { name: wave_ok, from: "$.ok" }
|
|
463
|
+
- { name: wave_required, from: "$.required_wave" }
|
|
464
|
+
- { name: wave_message, from: "$.message" }
|
|
465
|
+
- id: require_wave_matches_depends
|
|
466
|
+
action: verify_context
|
|
467
|
+
require_true: wave_ok
|
|
468
|
+
on_false: stop
|
|
469
|
+
on_false_message: "finalize-delta REFUSED — change {{active_change}} contradicts its own `## Depends on`: {{wave_message}}. The blocker fence above compares wave NUMBERS, so it would let the two merge in either order and this delta could land before the change it says it builds on. Either raise this change to wave {{wave_required}} (`.appweaver/scripts/bash/aw-spec-actions.sh cr-wave-derive --cr-id {{active_change}} --stamp`, which reads the same section), or correct `## Depends on` in its delta documents if it does not in fact build on them."
|
|
399
470
|
|
|
400
471
|
# 1c. DOWNSTREAM GATE (hard, with a REVIEWED override). Who depends on this aggregate —
|
|
401
472
|
# its OKF Cross-References, TRANSITIVELY (C→B→A all count) and placeholder-filtered
|
|
@@ -700,16 +771,15 @@ workflow:
|
|
|
700
771
|
# the context has no model: there would be nothing in context to distil, and a
|
|
701
772
|
# query with no source invents one. Best-effort; an empty answer is a no-op.
|
|
702
773
|
# WHICH AGGREGATE SECTION(S) THIS CHANGE ACTUALLY TOUCHED, read off the model delta's
|
|
703
|
-
# own
|
|
704
|
-
#
|
|
705
|
-
#
|
|
706
|
-
#
|
|
707
|
-
#
|
|
708
|
-
# declared aggregates makes the common case exact and the uncommon case visible.
|
|
774
|
+
# own `**Aggregates touched**:` header rather than assumed from the feature name. The Overview
|
|
775
|
+
# step below selects a section by name, and a feature whose name is not an aggregate
|
|
776
|
+
# name — one capability over a shared aggregate — selects nothing, producing an empty
|
|
777
|
+
# answer that the writer then declines. Driving it off the delta's declared aggregates
|
|
778
|
+
# makes a model change exact.
|
|
709
779
|
#
|
|
710
780
|
# A BEHAVIOUR-ONLY CHANGE HAS NO MODEL DELTA, so this reports none and the step falls
|
|
711
|
-
# back to the feature
|
|
712
|
-
# change to distil and the concept's existing Overview stands.
|
|
781
|
+
# back to the aggregate the feature answers for — which is correct, not a degradation:
|
|
782
|
+
# there is no model change to distil and the concept's existing Overview stands.
|
|
713
783
|
- id: cr_aggregates
|
|
714
784
|
action: execute_script
|
|
715
785
|
on_error: continue # informational — a missing model delta is the ordinary case
|
|
@@ -720,9 +790,19 @@ workflow:
|
|
|
720
790
|
- { name: delta_aggregates, from: "$.aggregates_csv" }
|
|
721
791
|
- { name: delta_aggregate_list, from: "$.aggregates" }
|
|
722
792
|
|
|
793
|
+
# The fallback subject: the aggregate this feature answers for, which is the feature
|
|
794
|
+
# itself on a 1:1 slice and the shared aggregate on a capability over one.
|
|
795
|
+
- id: feature_aggregates
|
|
796
|
+
action: execute_script
|
|
797
|
+
script: { ref: xref }
|
|
798
|
+
subcommand: feature-aggregates
|
|
799
|
+
args: ["--feature-dir", "{{feature_dir}}"]
|
|
800
|
+
outputs:
|
|
801
|
+
- { name: owned_csv, from: "$.owned_csv" }
|
|
802
|
+
|
|
723
803
|
- id: overview_scope_feature
|
|
724
804
|
action: set
|
|
725
|
-
vars: { overview_scope: "{{
|
|
805
|
+
vars: { overview_scope: "{{owned_csv}}" }
|
|
726
806
|
- id: overview_scope_delta
|
|
727
807
|
condition: "{{delta_aggregates}} != "
|
|
728
808
|
action: set
|
|
@@ -820,7 +900,11 @@ outputs:
|
|
|
820
900
|
# the change by close-delta. What an un-merge would restore from.
|
|
821
901
|
pre_merge_snapshot: "{{snapshot_dir}}"
|
|
822
902
|
base_version: "{{base_version}}"
|
|
903
|
+
# What the automatic rebase did before the fence read the stamp — empty when the delta was
|
|
904
|
+
# already current, and the reason it could not when a landed change shared its aggregates.
|
|
905
|
+
rebased: "{{rebase_stamped}}"
|
|
906
|
+
rebase_note: "{{rebase_message}}"
|
|
823
907
|
model_merged: "{{delta_model_exists}}" # true ⇒ domain-model.md was the second target of this merge.
|
|
824
908
|
# false = a behaviour-only change: the spec merge was the
|
|
825
909
|
# whole merge, not a half of one.
|
|
826
|
-
next: "Delta MERGED into spec.md, and into the context's domain-model.md one aggregate section at a time where the change carried a model delta (model_merged={{delta_model_exists}} — false means a behaviour-only change, so the spec merge was the whole merge). The lane plans were NOT touched — they are DERIVED from these two masters, so backend-plan.md / frontend-plan.md now stand BEHIND the spec and model at their last authored content. That drift starts here and lasts until /appweaver plan main re-derives them, carrying the recorded decisions (DA-n) and workarounds (WA-n) forward and rewriting what the spec and model no longer support. analyze-plan is not what closes it and cannot repair it while this change is open: with a change open it runs READ-ONLY, reporting the drift into plan-analyze-report.md without rewriting either plan. The full gate — check, repair, stop — comes back once the last change is closed, so the order is close-delta, then plan main, then analyze-plan. OKF edges reconciled ({{xref_edge_count}} dep(s)), {{downstream_count}} downstream slice(s) fenced (gate: {{gate_decision}}). The concept Overview was distilled for aggregate(s) `{{overview_scope}}` — taken from what this change's model delta DECLARES it touched, not assumed from the feature name (empty on a behaviour-only change, which falls back to the feature). section_found={{overview_section_ok}}, written={{overview_written}}{{overview_reason}} — written=false is normal for a behaviour-only change (nothing to distil, the existing Overview stands), but section_found=false means domain-model.md has no `## Aggregate:` section under that name and the concept's Overview was left as it was: check that the
|
|
910
|
+
next: "Delta MERGED into spec.md, and into the context's domain-model.md one aggregate section at a time where the change carried a model delta (model_merged={{delta_model_exists}} — false means a behaviour-only change, so the spec merge was the whole merge). The lane plans were NOT touched — they are DERIVED from these two masters, so backend-plan.md / frontend-plan.md now stand BEHIND the spec and model at their last authored content. That drift starts here and lasts until /appweaver plan main re-derives them, carrying the recorded decisions (DA-n) and workarounds (WA-n) forward and rewriting what the spec and model no longer support. analyze-plan is not what closes it and cannot repair it while this change is open: with a change open it runs READ-ONLY, reporting the drift into plan-analyze-report.md without rewriting either plan. The full gate — check, repair, stop — comes back once the last change is closed, so the order is close-delta, then plan main, then analyze-plan. OKF edges reconciled ({{xref_edge_count}} dep(s)), {{downstream_count}} downstream slice(s) fenced (gate: {{gate_decision}}). The concept Overview was distilled for aggregate(s) `{{overview_scope}}` — taken from what this change's model delta DECLARES it touched, not assumed from the feature name (empty on a behaviour-only change, which falls back to the aggregate the feature answers for). section_found={{overview_section_ok}}, written={{overview_written}}{{overview_reason}} — written=false is normal for a behaviour-only change (nothing to distil, the existing Overview stands), but section_found=false means domain-model.md has no `## Aggregate:` section under that name and the concept's Overview was left as it was: check that the spec's Acts on aggregate(s) line names an aggregate the model defines. Change {{active_change}} is STILL OPEN and the version is still {{version}}. Next: /appweaver analyze-slice — it now reads masters that carry this change, and a red result is still recoverable because nothing has been archived. Then /appweaver close-delta archives the change and bumps the version."
|
|
@@ -12,9 +12,26 @@
|
|
|
12
12
|
# Interactive confirmations (proceed / overwrite / reference sample) ride the
|
|
13
13
|
# checkpoint channel under EVERY strategy.
|
|
14
14
|
#
|
|
15
|
+
# A SCAFFOLD LANDS IN A LANE. The output directory is the SOURCE PATH of the lane being
|
|
16
|
+
# scaffolded, read from .appweaver/stacks.json — never the repository root and never a
|
|
17
|
+
# directory named on the command line. One bundle is one lane's project, so a repo with a
|
|
18
|
+
# service and a UI is scaffolded by running this twice, once per lane, and each run's files
|
|
19
|
+
# land in the subtree that lane owns. That is what makes every later phase's "write only
|
|
20
|
+
# into your lane" rule true of the scaffold as well as of generated code: implement,
|
|
21
|
+
# build, test and run all resolve the same path this unpacked into.
|
|
22
|
+
#
|
|
23
|
+
# The lane's TEST PATH is its second subtree, and for most backend stacks it is a sibling
|
|
24
|
+
# of the source path rather than a directory inside it. A pack carrying its own test
|
|
25
|
+
# project is unpacked like everything else and then has that tree lifted onto the lane's
|
|
26
|
+
# test path — see .appweaver/scripts/python/aw-foundation.py for when that is a no-op.
|
|
27
|
+
#
|
|
28
|
+
# Lanes must therefore be declared FIRST: /appweaver select-stack is the sole writer of
|
|
29
|
+
# stacks.json and the first command of both sequences.
|
|
30
|
+
#
|
|
15
31
|
# Launch via the parametric skill: /appweaver foundation [strategy]
|
|
16
|
-
# Interactive (no args):
|
|
17
|
-
# for the app (project) name —
|
|
32
|
+
# Interactive (no args): ASKS which lane to scaffold, LISTS the foundations available
|
|
33
|
+
# for that lane's ecosystem, then ASKS for the app (project) name — all deterministic,
|
|
34
|
+
# no LLM. Fast path still works:
|
|
18
35
|
# /appweaver foundation agent VerticalSlice AppWeaver.Health
|
|
19
36
|
#
|
|
20
37
|
# Headless:
|
|
@@ -25,14 +42,14 @@
|
|
|
25
42
|
# =============================================================================
|
|
26
43
|
|
|
27
44
|
name: aw-spec-foundation
|
|
28
|
-
description: Scaffold a complete, compiling project — pick from the
|
|
29
|
-
version: 1.
|
|
45
|
+
description: Scaffold a complete, compiling project INTO A DECLARED LANE — pick the lane, pick from the foundation bundles available for its ecosystem, enter the app name, and the verified bundle is unpacked into that lane's source path with its sentinel renamed. Selection is zero-LLM (list + pick + ask).
|
|
46
|
+
version: 1.2.0
|
|
30
47
|
agent_agnostic: true
|
|
31
48
|
dialect: aw-spec.orchestrator/v1
|
|
32
49
|
|
|
33
50
|
inputs:
|
|
34
51
|
user_input:
|
|
35
|
-
description: "Optional fast path: FoundationName [AppName]
|
|
52
|
+
description: "Optional fast path: FoundationName [AppName]. Omit to pick a foundation from the list and be asked for the app name. There is no output-directory argument — the scaffold goes into the chosen lane's source path."
|
|
36
53
|
required: false
|
|
37
54
|
bind: USER_INPUT
|
|
38
55
|
|
|
@@ -50,21 +67,83 @@ vars:
|
|
|
50
67
|
stash_found: "false" # true only when the reference flow stashed a DbContext
|
|
51
68
|
app: "" # resolved app name: fast-path 2nd token, else the ask below
|
|
52
69
|
app_typed: "" # app name entered interactively (empty on the fast path)
|
|
70
|
+
out_ok: "true" # cleared when a third fast-path token names an output directory
|
|
53
71
|
|
|
54
72
|
workflow:
|
|
55
73
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
# 0a. THE LANE MODEL, which is where this scaffold is going. Read-only: select-stack is
|
|
75
|
+
# the sole writer, and a scaffold has no business declaring the lane it fills.
|
|
76
|
+
- id: lanes
|
|
77
|
+
action: execute_script
|
|
78
|
+
on_error: continue # never crash raw on an absent stacks.json — the gate explains it
|
|
79
|
+
script: { ref: actions }
|
|
80
|
+
subcommand: lane-resolve
|
|
81
|
+
outputs:
|
|
82
|
+
- { name: lane_options, from: "$.lane_options" }
|
|
83
|
+
- { name: lane_count, from: "$.lane_count" }
|
|
84
|
+
|
|
85
|
+
- id: require_lanes
|
|
86
|
+
action: verify_context
|
|
87
|
+
require_non_empty: lane_options
|
|
88
|
+
on_empty: stop
|
|
89
|
+
on_empty_message: >
|
|
90
|
+
/appweaver foundation cannot run: no lane is declared in .appweaver/stacks.json.
|
|
91
|
+
A scaffold is unpacked into the SOURCE PATH of the lane it belongs to — that is what
|
|
92
|
+
keeps a project's files inside the subtree every later phase compiles, tests and
|
|
93
|
+
generates into — so there has to be a lane to scaffold before there is a scaffold.
|
|
94
|
+
Run /appweaver select-stack first: it declares each lane's stack and source path, and
|
|
95
|
+
is the first command of both the greenfield and the brownfield sequence.
|
|
96
|
+
|
|
97
|
+
# 0b. WHICH lane. One bundle is one lane's project, so a fullstack repo runs this once
|
|
98
|
+
# per lane rather than unpacking a tree that spans both. A single declared lane
|
|
99
|
+
# auto-confirms; the option carries its own explanation and resolves below.
|
|
100
|
+
- id: pick_lane
|
|
101
|
+
action: user_select
|
|
102
|
+
prompt: "Which lane is this scaffold for? The bundle is unpacked into that lane's source path, and its tests into the lane's test path — nothing lands outside them. Scaffold the other lane by running /appweaver foundation again."
|
|
103
|
+
options_from_variable: lane_options
|
|
104
|
+
single_option_confirm: true
|
|
105
|
+
outputs:
|
|
106
|
+
- { name: chosen_lane, from: selection }
|
|
107
|
+
|
|
108
|
+
# 0c. Resolve the chosen lane to the two paths this run writes into, plus the ecosystem
|
|
109
|
+
# that narrows the bundle list. `lane-resolve` strips the option's label itself, so
|
|
110
|
+
# the selection goes back in as the id it already carries.
|
|
111
|
+
- id: lane_target
|
|
112
|
+
action: execute_script
|
|
113
|
+
script: { ref: actions }
|
|
114
|
+
subcommand: lane-resolve
|
|
115
|
+
args: ["--id", "{{chosen_lane}}"]
|
|
116
|
+
outputs:
|
|
117
|
+
- { name: lane_found, from: "$.found" }
|
|
118
|
+
- { name: lane_id, from: "$.lane_id" }
|
|
119
|
+
- { name: lane_root, from: "$.source_path" }
|
|
120
|
+
- { name: lane_test_path, from: "$.test_path" }
|
|
121
|
+
- { name: lane_ecosystem, from: "$.ecosystem" }
|
|
122
|
+
- { name: lane_language, from: "$.language" }
|
|
123
|
+
|
|
124
|
+
- id: lane_resolved
|
|
125
|
+
action: verify_context
|
|
126
|
+
require_true: lane_found
|
|
127
|
+
on_false: stop
|
|
128
|
+
on_false_message: "No lane matches the selection `{{chosen_lane}}` in .appweaver/stacks.json. Re-run /appweaver select-stack to re-declare the lanes, then re-run foundation."
|
|
129
|
+
|
|
130
|
+
- id: lane_has_root
|
|
131
|
+
action: verify_context
|
|
132
|
+
require_non_empty: lane_root
|
|
133
|
+
on_empty: stop
|
|
134
|
+
on_empty_message: "Lane `{{lane_id}}` declares no source path in .appweaver/stacks.json, and the source path is where this scaffold is unpacked. Re-run /appweaver select-stack and give the lane the directory it owns."
|
|
135
|
+
|
|
136
|
+
# 1a. LIST the foundations available for THAT LANE's ecosystem (deterministic
|
|
137
|
+
# foundations/<ecosystem>/<Name>/<Name>.pack.txt scan — no LLM). A bundle is a whole
|
|
138
|
+
# compiling project in one ecosystem's build system, so a Node UI lane is not offered
|
|
139
|
+
# a .NET bundle even in a repo that also declares a .NET service; `all_foundations`
|
|
140
|
+
# keeps the unfiltered set for the gate message below. A direct "Foundation App"
|
|
141
|
+
# argument is parsed here too as an optional fast path.
|
|
63
142
|
- id: list_foundations
|
|
64
143
|
action: execute_script
|
|
65
144
|
script: { ref: actions }
|
|
66
145
|
subcommand: foundation-list
|
|
67
|
-
args: ["--args", "{{USER_INPUT}}"]
|
|
146
|
+
args: ["--args", "{{USER_INPUT}}", "--ecosystem", "{{lane_ecosystem}}"]
|
|
68
147
|
outputs:
|
|
69
148
|
- { name: foundations, from: "$.foundations" }
|
|
70
149
|
- { name: foundation_count, from: "$.count" }
|
|
@@ -72,23 +151,41 @@ workflow:
|
|
|
72
151
|
- { name: lane_ecosystems, from: "$.lane_ecosystems" }
|
|
73
152
|
- { name: preselect_name, from: "$.preselect_name" }
|
|
74
153
|
- { name: preselect_app, from: "$.preselect_app" }
|
|
75
|
-
- { name:
|
|
154
|
+
- { name: rejected_out, from: "$.rejected_out" }
|
|
155
|
+
|
|
156
|
+
# 1b. A third fast-path token used to be an output directory. Refused by name rather
|
|
157
|
+
# than ignored: silently dropping a path argument is how someone believes they
|
|
158
|
+
# scaffolded a sandbox and finds they filled their service lane.
|
|
159
|
+
- id: out_dir_given
|
|
160
|
+
action: set
|
|
161
|
+
condition: "{{rejected_out}} != "
|
|
162
|
+
vars: { out_ok: "" }
|
|
163
|
+
|
|
164
|
+
- id: no_out_dir
|
|
165
|
+
action: verify_context
|
|
166
|
+
require_true: out_ok
|
|
167
|
+
on_false: stop
|
|
168
|
+
on_false_message: >
|
|
169
|
+
`{{rejected_out}}` was given as an output directory, and foundation has none to give.
|
|
170
|
+
A scaffold is unpacked into the source path of the lane it is for — here lane
|
|
171
|
+
`{{lane_id}}` at `{{lane_root}}` — so that everything it writes is inside the subtree
|
|
172
|
+
that lane owns. Re-run as `/appweaver foundation {{preselect_name}} {{preselect_app}}`.
|
|
173
|
+
To scaffold somewhere else, declare a lane there with /appweaver select-stack and
|
|
174
|
+
pick that lane.
|
|
76
175
|
|
|
77
176
|
- id: have_foundations
|
|
78
177
|
action: verify_context
|
|
79
178
|
require_non_empty: foundations
|
|
80
179
|
on_empty: stop
|
|
81
180
|
on_empty_message: >
|
|
82
|
-
No foundation bundle
|
|
83
|
-
{{all_foundations}}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
/appweaver select-stack — implement generates INTO a project tree and does not
|
|
91
|
-
require that the kit be the thing that created it.
|
|
181
|
+
No foundation bundle ships for the `{{lane_id}}` lane's ecosystem ({{lane_ecosystem}}).
|
|
182
|
+
Bundles present: {{all_foundations}}. A foundation is a complete project in one
|
|
183
|
+
ecosystem's languages and build system, so one from another ecosystem is not a
|
|
184
|
+
scaffold for this lane — it is a second, foreign project inside it. Either add a
|
|
185
|
+
bundle under foundations/{{lane_ecosystem}}/<Name>/<Name>.pack.txt (see the pack
|
|
186
|
+
scripts in that directory), or scaffold `{{lane_root}}` with the stack's own tooling
|
|
187
|
+
(Spring Initializr, `npm create`, `cargo new`, …) — implement generates INTO a
|
|
188
|
+
project tree and does not require that the kit be the thing that created it.
|
|
92
189
|
|
|
93
190
|
# 1b. PICK the foundation. A matching fast-path arg auto-selects; a lone
|
|
94
191
|
# foundation auto-confirms; otherwise the user chooses from the list.
|
|
@@ -128,14 +225,13 @@ workflow:
|
|
|
128
225
|
action: execute_script
|
|
129
226
|
script: { ref: actions }
|
|
130
227
|
subcommand: foundation-locate
|
|
131
|
-
args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{
|
|
228
|
+
args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{lane_root}}"]
|
|
132
229
|
outputs:
|
|
133
230
|
- { name: locate_ok, from: "$.ok" }
|
|
134
231
|
- { name: locate_msg, from: "$.reason" }
|
|
135
232
|
- { name: available, from: "$.available" }
|
|
136
233
|
- { name: fname, from: "$.name" }
|
|
137
234
|
- { name: app, from: "$.app" }
|
|
138
|
-
- { name: out_dir, from: "$.out_dir" }
|
|
139
235
|
- { name: sentinel, from: "$.sentinel" }
|
|
140
236
|
- { name: file_count, from: "$.file_count" }
|
|
141
237
|
|
|
@@ -151,7 +247,9 @@ workflow:
|
|
|
151
247
|
prompt: |
|
|
152
248
|
Foundation: {{fname}} (sentinel {{sentinel}}, {{file_count}} files)
|
|
153
249
|
App Name: {{app}}
|
|
154
|
-
|
|
250
|
+
Lane: {{lane_id}} — {{lane_language}} ({{lane_ecosystem}})
|
|
251
|
+
Source: {{lane_root}} <- the whole bundle unpacks here
|
|
252
|
+
Tests: {{lane_test_path}} <- a test tree the bundle carries is lifted here
|
|
155
253
|
Rename {{sentinel}} -> {{app}} (dotted/underscored/hyphen/lower forms).
|
|
156
254
|
Unchanged: 'Projects' sample domain + AppWeaver.* framework packages. Proceed?
|
|
157
255
|
options: ["yes", "no"]
|
|
@@ -169,7 +267,7 @@ workflow:
|
|
|
169
267
|
action: execute_script
|
|
170
268
|
script: { ref: actions }
|
|
171
269
|
subcommand: foundation-unpack
|
|
172
|
-
args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{
|
|
270
|
+
args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{lane_root}}", "--dry-run"]
|
|
173
271
|
outputs:
|
|
174
272
|
- { name: has_collisions, from: "$.has_collisions" }
|
|
175
273
|
|
|
@@ -193,9 +291,11 @@ workflow:
|
|
|
193
291
|
action: execute_script
|
|
194
292
|
script: { ref: actions }
|
|
195
293
|
subcommand: foundation-unpack
|
|
196
|
-
args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{
|
|
294
|
+
args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{lane_root}}", "--test-path", "{{lane_test_path}}", "--force", "{{overwrite}}"]
|
|
197
295
|
outputs:
|
|
198
|
-
- { name: written,
|
|
296
|
+
- { name: written, from: "$.written" }
|
|
297
|
+
- { name: tests_moved, from: "$.tests_moved" }
|
|
298
|
+
- { name: tests_message, from: "$.tests_message" }
|
|
199
299
|
|
|
200
300
|
# 5. Optional reference sample.
|
|
201
301
|
- id: ask_reference
|
|
@@ -209,7 +309,7 @@ workflow:
|
|
|
209
309
|
condition: "{{want_reference}} == yes"
|
|
210
310
|
script: { ref: actions }
|
|
211
311
|
subcommand: foundation-stash-dbcontext
|
|
212
|
-
args: ["--out", "{{
|
|
312
|
+
args: ["--out", "{{lane_root}}"]
|
|
213
313
|
outputs:
|
|
214
314
|
- { name: stash_found, from: "$.found" }
|
|
215
315
|
- { name: db_context, from: "$.dbcontext" }
|
|
@@ -220,7 +320,7 @@ workflow:
|
|
|
220
320
|
condition: "{{want_reference}} == yes"
|
|
221
321
|
script: { ref: actions }
|
|
222
322
|
subcommand: foundation-unpack
|
|
223
|
-
args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{
|
|
323
|
+
args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{lane_root}}", "--test-path", "{{lane_test_path}}", "--reference"]
|
|
224
324
|
outputs:
|
|
225
325
|
- { name: ref_written, from: "$.written" }
|
|
226
326
|
|
|
@@ -248,7 +348,7 @@ workflow:
|
|
|
248
348
|
action: execute_script
|
|
249
349
|
script: { ref: actions }
|
|
250
350
|
subcommand: foundation-verify
|
|
251
|
-
args: ["--out", "{{
|
|
351
|
+
args: ["--out", "{{lane_root}}", "--sentinel", "{{sentinel}}"]
|
|
252
352
|
outputs:
|
|
253
353
|
- { name: clean, from: "$.clean" }
|
|
254
354
|
- { name: residue, from: "$.residue" }
|
|
@@ -256,10 +356,14 @@ workflow:
|
|
|
256
356
|
outputs:
|
|
257
357
|
foundation: "{{fname}}"
|
|
258
358
|
app: "{{app}}"
|
|
259
|
-
|
|
359
|
+
lane: "{{lane_id}}"
|
|
360
|
+
source_path: "{{lane_root}}"
|
|
361
|
+
test_path: "{{lane_test_path}}"
|
|
260
362
|
files_written: "{{written}}"
|
|
363
|
+
tests_moved: "{{tests_moved}}"
|
|
364
|
+
tests_note: "{{tests_message}}"
|
|
261
365
|
reference_files: "{{ref_written}}"
|
|
262
366
|
sentinel_clean: "{{clean}}"
|
|
263
367
|
sentinel_residue: "{{residue}}"
|
|
264
|
-
note: "Foundation scaffolded into {{
|
|
265
|
-
next: "/appweaver select-feature
|
|
368
|
+
note: "Foundation scaffolded into the {{lane_id}} lane at {{lane_root}} ({{written}} file(s), sentinel renamed to {{app}}); {{tests_message}}. Everything written is inside the subtree that lane owns, which is where implement generates and build/test/run act. Configure your AppWeaver.* NuGet feed and run the AppHost to confirm the scaffold starts before building on it."
|
|
369
|
+
next: "Another lane still to scaffold? Run /appweaver foundation again and pick it — one bundle fills one lane. Otherwise /appweaver select-feature (pick the feature to build; every stage from specify onward reads that active feature and stops without it. If this workspace has governed projects under guardrails/projects/, run /appweaver select-domain first — it is one-time and optional.)"
|