aw-spec-cli 0.1.26 → 0.1.27

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 (98) hide show
  1. package/assets/.agents/skills/appweaver/SKILL.md +7 -47
  2. package/assets/.appweaver/ORCH-DIALECT.md +13 -9
  3. package/assets/.appweaver/RULE-PARTITION.md +3 -3
  4. package/assets/.appweaver/STACK-PROFILES.md +188 -101
  5. package/assets/.appweaver/domain.json +1 -1
  6. package/assets/.appweaver/orchestrator/AGENT-PROFILE.md +38 -10
  7. package/assets/.appweaver/orchestrator/LAUNCHER.md +88 -0
  8. package/assets/.appweaver/orchestrator/README.md +120 -53
  9. package/assets/.appweaver/orchestrator/adapters/claude_agent_sdk.py +1 -1
  10. package/assets/.appweaver/orchestrator/modules/domain-select.orch.yaml +6 -4
  11. package/assets/.appweaver/orchestrator/modules/foundation-lane.orch.yaml +237 -0
  12. package/assets/.appweaver/orchestrator/modules/foundation-pack.orch.yaml +349 -0
  13. package/assets/.appweaver/orchestrator/modules/foundation-spec.orch.yaml +381 -0
  14. package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +4 -4
  15. package/assets/.appweaver/orchestrator/modules/tech-select.orch.yaml +3 -3
  16. package/assets/.appweaver/orchestrator/orchestrator.py +162 -63
  17. package/assets/.appweaver/orchestrator/rev-templates/domain-constitution.skeleton.md +2 -2
  18. package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +2 -2
  19. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +5 -5
  20. package/assets/.appweaver/orchestrator/rev-templates/frontend-rule-file.skeleton.md +1 -1
  21. package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +14 -14
  22. package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +2 -2
  23. package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +1 -1
  24. package/assets/.appweaver/orchestrator/rev-templates/ui-constitution.skeleton.md +3 -3
  25. package/assets/.appweaver/orchestrator/workflows/_smoke.orch.yaml +41 -3
  26. package/assets/.appweaver/orchestrator/workflows/aw-spec-add-component.orch.yaml +1 -1
  27. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +13 -13
  28. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +3 -3
  29. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +3 -3
  30. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +4 -4
  31. package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +2 -2
  32. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +61 -12
  33. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +172 -293
  34. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +29 -28
  35. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +53 -53
  36. package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +2 -2
  37. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +3 -3
  38. package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +4 -4
  39. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-bounded-context.orch.yaml +26 -2
  40. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +21 -15
  41. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +14 -14
  42. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +2 -2
  43. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +3 -3
  44. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +6 -6
  45. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +284 -128
  46. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +224 -70
  47. package/assets/.appweaver/scripts/python/aw-foundation.py +809 -20
  48. package/assets/.appweaver/scripts/python/aw-inventory.py +28 -21
  49. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +2 -2
  50. package/assets/.appweaver/scripts/python/aw-rev-constitution.py +6 -6
  51. package/assets/.appweaver/scripts/python/aw-stack.py +195 -89
  52. package/assets/.appweaver/stack-profiles/angular.json +2 -1
  53. package/assets/.appweaver/stack-profiles/blazor-wasm.json +2 -1
  54. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +16 -3
  55. package/assets/.appweaver/stack-profiles/go-chi.json +6 -1
  56. package/assets/.appweaver/stack-profiles/java-spring.json +12 -5
  57. package/assets/.appweaver/stack-profiles/nextjs.json +2 -1
  58. package/assets/.appweaver/stack-profiles/node-express.json +8 -2
  59. package/assets/.appweaver/stack-profiles/python-fastapi.json +9 -2
  60. package/assets/.appweaver/stack-profiles/react-vite.json +2 -1
  61. package/assets/.appweaver/stack-profiles/sveltekit.json +2 -1
  62. package/assets/.appweaver/stack-profiles/vue-vite.json +2 -1
  63. package/assets/.appweaver/stacks.json +37 -1
  64. package/assets/.appweaver/templates/constitution-ui-template.md +2 -2
  65. package/assets/.claude/skills/appweaver/SKILL.md +7 -46
  66. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.Reference.rules.json +31 -0
  67. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.Reference.rules.json +16 -0
  68. package/assets/foundations/foundation.spec.template.txt +73 -0
  69. package/assets/foundations/java/Hexagonal/Hexagonal.pack.txt +1 -1
  70. package/assets/foundations/java/Hexagonal/unpack-foundation.sh +4 -5
  71. package/assets/foundations/node/NextAppRouter/NextAppRouter.spec.txt +68 -0
  72. package/assets/guardrails/README.md +31 -17
  73. package/assets/guardrails/{projects/dotnet-default-project → project-specific}/domain-rule-bindings.md +5 -5
  74. package/assets/guardrails/{projects/dotnet-default-project → project-specific}/tech-rule-bindings.md +151 -114
  75. package/assets/guardrails/technology/dotnet-webapi/PACKAGING.md +58 -4
  76. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +1 -1
  77. package/assets/guardrails/technology/dotnet-webapi/dependency-policy.md +1 -1
  78. package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +1 -1
  79. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +1 -1
  80. package/assets/guardrails/technology/dotnet-webapi/testing.md +1 -1
  81. package/assets/guardrails/technology/nextjs/api-client.md +1 -1
  82. package/assets/guardrails/technology/nextjs/component-architecture.md +1 -1
  83. package/assets/guardrails/technology/nextjs/component-conventions.md +2 -2
  84. package/assets/guardrails/technology/nextjs/design-system.md +1 -1
  85. package/assets/guardrails/technology/nextjs/frontend-testing.md +1 -1
  86. package/assets/guardrails/technology/nextjs/routing-navigation.md +1 -1
  87. package/assets/guardrails/technology/nextjs/ui-constitution.md +3 -3
  88. package/package.json +1 -1
  89. package/assets/.agents/skills/appweaver-demo/SKILL.md +0 -59
  90. package/assets/.appweaver/orchestrator/.smoke-out/smoke.md +0 -6
  91. package/assets/.appweaver/orchestrator/workflows/aw-spec-demo.orch.yaml +0 -100
  92. package/assets/.claude/skills/appweaver-demo/SKILL.md +0 -59
  93. package/assets/.kilocode/agent.json +0 -28
  94. package/assets/.kilocode/workflows/appweaver-demo.md +0 -59
  95. package/assets/.kilocode/workflows/appweaver.md +0 -55
  96. /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/domain/domain-requirements.md +0 -0
  97. /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/frontend/ui-requirements.md +0 -0
  98. /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/service/technical-requirements.md +0 -0
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # aw-spec-actions.sh — common action dispatcher for the aw-spec orchestrator.
4
4
  #
5
- # ONE shared script for every integration (.claude / .agents / .kilocode). The
5
+ # ONE shared script for every integration (.claude / .agents / .kilo). The
6
6
  # orchestrator workflows reach these subcommands through `execute_script` steps.
7
7
  # Each subcommand performs deterministic, governance-safe work and prints a single
8
8
  # JSON object on stdout. NONE of these actions author spec/code content — content
@@ -49,6 +49,8 @@
49
49
  # (the FILE signals for "does this lane exist"; resolve-tech carries the rule signal)
50
50
  # list-features [--preselect <name>] [--bundle <dir>] -> {features[], names[], count, select_options[], sentinel} (specs/ ∪ features the OKF concepts serve)
51
51
  # set-feature --feature <name> -> {feature, feature_dir, spec_file, existed, created, spec_exists}
52
+ # default-scope --context <name> --feature <name> -> {created, skipped_reason, context_dir, feature_dir}
53
+ # (creates and activates both only on a project with no specs yet)
52
54
  # version-read [--feature-dir <path>] -> {feature, version, active_change, exists, bootstrap_pending}
53
55
  # version-init [--feature-dir <path>] [--bootstrap] -> {…, created, bootstrap_pending} (create active_version.json v1.0; --bootstrap marks a FIRST draft in progress)
54
56
  # version-bootstrap-done [--feature-dir <path>] -> {…, cleared} (the ONLY clearer of the --bootstrap marker; call it when the first draft is finished)
@@ -63,11 +65,26 @@
63
65
  # -> {ok, total, names[], names_csv, elements[], gaps[], gap_count, gaps_text}
64
66
  # (which plan elements the lane's code names and the model reported done)
65
67
  # detect-project -> {scaffolded, markers[]}
66
- # foundation-list [--args '<F> <A> [out]'] -> {foundations[], count, preselect_name, preselect_app, out_dir}
68
+ # foundation-list [--kind <k>] [--profile <id>] [--ecosystem <e>] [--lane-id <id>] [--args '<F> <A>']
69
+ # -> {options[], entries[], count, foundations[], specs[], preselect_option, preselect_app, rejected_out}
70
+ # (one lane's pick list: its packs + specs, then [manual spec] and [skip])
71
+ # foundation-choice --selection <option> [same filters as foundation-list]
72
+ # -> {ok, route (pack|spec|manual|skip), name, file, reason}
73
+ # foundation-spec-capture --lane <id> [--kind <k>] (--file <path> | --input <path|text>)
74
+ # -> {ok, spec_path, source, chars, title, reason}
75
+ # foundation-lane-files --source <p> [--test <p>] [--workdir <p>] [--profile <id>]
76
+ # -> {has_files, count, sample[], generated[], root_files[], folders[], workdir, summary}
77
+ # foundation-snapshot --out <repo-relative file> -> {ok, snapshot, entries}
78
+ # foundation-outside --snapshot <file> --lane <lane root folder> [--undo]
79
+ # -> {clean, created[], changed[], deleted[], removed[], summary}
67
80
  # foundation-locate --name <F> --app <A> [--out <d>]
68
- # -> {ok, reason, name, app, out_dir, pack_file, unpacker, runner, sentinel, file_count, available[]}
69
- # foundation-unpack --name <F> --app <A> --out <d> [--dry-run] [--force <yes|no>] [--reference]
70
- # -> {ok, mode, written, has_collisions, stdout}
81
+ # -> {ok, reason, name, app, out_dir, pack_file, unpacker, runner, sentinel, file_count, has_reference, available[]}
82
+ # foundation-unpack --name <F> --app <A> --out <d> [--test-path <d>] [--dry-run] [--force <yes|no>] [--reference]
83
+ # -> {ok, mode, written, has_collisions, tests_moved, tests_message, stdout}
84
+ # foundation-rules --name <F> --app <A> --out <lane root folder> [--reference] [--state <folder>]
85
+ # -> {ok, reason, rules_file, rules, merges[], merge_count, stale[], note}
86
+ # (what the pack's <pack name>.rules.json asks of this unpack; puts the files to merge aside)
87
+ # foundation-rules-done [--state <folder>] -> {ok, removed}
71
88
  # foundation-verify --out <d> --sentinel <s> -> {clean, residue[]}
72
89
  # list-components -> {components[]} (foundations/dotnet/components/* with integration.md)
73
90
  # find-host-project -> {found, program_cs, project_dir, csproj}
@@ -86,7 +103,9 @@
86
103
  # (design-extract / design-reconcile / design-paths are RETIRED — the path-tagged
87
104
  # four-document model is gone. "Which files is this slice made of" is now an OKF
88
105
  # query: aw-xref concept-paths / concept-upsert --merge.)
89
- # build-compile [--feature-dir <path>] -> {ok, tool, ran, cmd, log_tail}
106
+ # build-compile [--feature-dir <path>] [--kind backend|frontend]
107
+ # -> {ok, tool, ran, cmd, log_tail}
108
+ # (--kind builds that one declared lane; ignored when no lane is declared)
90
109
  # test-run [--feature-dir <path>] -> {ok, tool, ran, cmd, log_tail,
91
110
  # failing_excerpt, failing_lanes[],
92
111
  # no_setup, setup_reason, setup_missing[]}
@@ -163,7 +182,7 @@ cmd_resolve_domain() {
163
182
  fi
164
183
  fi
165
184
 
166
- local rulemap="$repo_root/guardrails/projects/$domain/domain-rule-bindings.md"
185
+ local rulemap="$repo_root/guardrails/$domain/domain-rule-bindings.md"
167
186
  local rulemap_exists=false
168
187
  [ -n "$domain" ] && [ -f "$rulemap" ] && rulemap_exists=true
169
188
 
@@ -188,7 +207,7 @@ cmd_resolve_domain() {
188
207
 
189
208
  # ──────────────────────────────── resolve-tech ─────────────────────────────
190
209
  # Technical rule-file mapping — parallel to resolve-domain, but reads
191
- # guardrails/projects/<project>/tech-rule-bindings.md. The TECHNICAL rule files live in the
210
+ # guardrails/<project>/tech-rule-bindings.md. The TECHNICAL rule files live in the
192
211
  # shared per-stack corpora at guardrails/technology/<stack>/ (rules) and
193
212
  # guardrails/technology/<stack>/examples/ (the worked sample for a rule file), and are
194
213
  # configured per-command in this map, so they can be tuned per project WITHOUT editing the
@@ -239,7 +258,7 @@ repo = os.environ["AW_REPO"]
239
258
  domain = os.environ.get("AW_DOMAIN", "")
240
259
  command = os.environ.get("AW_COMMAND", "")
241
260
 
242
- techmap = os.path.join(repo, "guardrails", "projects", domain, "tech-rule-bindings.md")
261
+ techmap = os.path.join(repo, "guardrails", domain, "tech-rule-bindings.md")
243
262
  exists = bool(domain) and os.path.isfile(techmap)
244
263
 
245
264
  # WHAT A PLACEHOLDER IS FOR. A technical rule corpus is stack-partitioned —
@@ -638,11 +657,12 @@ EOF
638
657
  }
639
658
 
640
659
  # ─────────────────────────────── list-domains ──────────────────────────────
641
- # The selectable projects: immediate subdirs of guardrails/projects/ that carry a rule
642
- # map. The map is what makes a directory a project, exactly as a *.pack.txt is what makes
643
- # a foundations/ directory a foundation — without that test any stray sibling would be
644
- # offered as a governance target. EITHER map qualifies, because onboard writes the domain
645
- # map several steps before the technical one and a half-built project must stay visible.
660
+ # The projects: immediate subdirs of guardrails/ that carry a rule map. The map is what
661
+ # makes a directory a project, exactly as a *.pack.txt is what makes a foundations/
662
+ # directory a foundation — the shared corpora technology/ and domain/ sit beside the
663
+ # projects and carry no map, so they are never listed. EITHER map qualifies, because
664
+ # onboard writes the domain map several steps before the technical one and a half-built
665
+ # project must stay visible.
646
666
  cmd_list_domains() {
647
667
  while [ $# -gt 0 ]; do
648
668
  case "$1" in
@@ -651,7 +671,7 @@ cmd_list_domains() {
651
671
  esac
652
672
  done
653
673
  local repo_root; repo_root=$(get_repo_root)
654
- local base="$repo_root/guardrails/projects"
674
+ local base="$repo_root/guardrails"
655
675
  local -a domains=()
656
676
  if [ -d "$base" ]; then
657
677
  local d
@@ -677,7 +697,7 @@ cmd_set_domain() {
677
697
  [ -n "$domain" ] || { echo "set-domain: --domain is required" >&2; return 1; }
678
698
 
679
699
  local repo_root; repo_root=$(get_repo_root)
680
- local map_rel="guardrails/projects/$domain/domain-rule-bindings.md"
700
+ local map_rel="guardrails/$domain/domain-rule-bindings.md"
681
701
  local map_exists=false
682
702
  [ -f "$repo_root/$map_rel" ] && map_exists=true
683
703
 
@@ -728,7 +748,7 @@ except Exception:
728
748
  ' 2>/dev/null || echo ""
729
749
  }
730
750
 
731
- # The declared SOURCE PATH of a lane kind — the field asked for most often.
751
+ # The declared LANE ROOT FOLDER of a lane kind — the field asked for most often.
732
752
  _aw_lane_root() { _aw_lane_field "$1" root; }
733
753
 
734
754
  # Every ecosystem the declared lanes belong to, one per line, deduped and empty when
@@ -815,10 +835,10 @@ cmd_lane_resolve() {
815
835
 
816
836
  # The gate: is the declaration USABLE for the phase about to run. --require names a
817
837
  # lane kind the phase cannot run without and is repeatable; every DECLARED lane is
818
- # checked for a sound source path whether or not it was required, because a stack
838
+ # checked for a sound lane root folder whether or not it was required, because a stack
819
839
  # named against a directory that is not there is a mistake in every phase.
820
- # --allow-missing-root is the greenfield state, where select-stack declares a source
821
- # path before implement creates it.
840
+ # --allow-missing-root is the greenfield state, where select-stack declares a lane root
841
+ # folder before implement creates it.
822
842
  cmd_lane_verify() {
823
843
  local -a fwd=()
824
844
  while [ $# -gt 0 ]; do
@@ -949,6 +969,13 @@ PY
949
969
  # file; a new one is created. The context directory is created but domain-model.md is
950
970
  # NOT — that document is authored by specify, which is the only writer of the model,
951
971
  # and an empty placeholder would read as "the model exists" to every later gate.
972
+ #
973
+ # The active feature belongs to the context it was created in. When the selected
974
+ # context does not contain it, .appweaver/feature.json is cleared, so the next
975
+ # feature-scoped command stops with "No active feature" instead of writing into the
976
+ # context the developer just left (and adopt with no argument does not recreate that
977
+ # feature's name in the new context). A flat specs/<Feature> is outside every context
978
+ # and is cleared the same way.
952
979
  cmd_set_context() {
953
980
  local raw=""
954
981
  while [ $# -gt 0 ]; do
@@ -991,11 +1018,20 @@ cmd_set_context() {
991
1018
  "$(json_escape "$context")" "$(json_escape "$context_dir_rel")" > "$cj"
992
1019
  fi
993
1020
 
1021
+ local active_fd; active_fd=$(read_feature_json_feature_directory "$repo_root")
1022
+ active_fd="${active_fd#"$repo_root/"}"; active_fd="${active_fd#./}"; active_fd="${active_fd%/}"
1023
+ local feature_cleared=false cleared_feature=""
1024
+ if [ -n "$active_fd" ] && [[ "$active_fd" != "$context_dir_rel/"* ]]; then
1025
+ _persist_feature_json "$repo_root" ""
1026
+ feature_cleared=true; cleared_feature="$active_fd"
1027
+ fi
1028
+
994
1029
  local created=true; [ "$existed" = true ] && created=false
995
1030
  local dm_exists=false; [ -f "$context_dir/domain-model.md" ] && dm_exists=true
996
- printf '{"context":"%s","context_dir":"%s","domain_model":"%s","existed":%s,"created":%s,"domain_model_exists":%s,"written":true}\n' \
1031
+ printf '{"context":"%s","context_dir":"%s","domain_model":"%s","existed":%s,"created":%s,"domain_model_exists":%s,"feature_cleared":%s,"cleared_feature":"%s","written":true}\n' \
997
1032
  "$(json_escape "$context")" "$(json_escape "$context_dir_rel")" \
998
- "$(json_escape "$context_dir_rel/domain-model.md")" "$existed" "$created" "$dm_exists"
1033
+ "$(json_escape "$context_dir_rel/domain-model.md")" "$existed" "$created" "$dm_exists" \
1034
+ "$feature_cleared" "$(json_escape "$cleared_feature")"
999
1035
  }
1000
1036
 
1001
1037
  # ══════════════════════ domain model — context-level helpers ════════════════
@@ -1460,6 +1496,47 @@ PY
1460
1496
  "$(json_escape "$context")"
1461
1497
  }
1462
1498
 
1499
+ # ─────────────────────────────── default-scope ─────────────────────────────
1500
+ # The bounded context and feature a new project starts in, so a simple project goes
1501
+ # from /appweaver constitution straight to specify. Acts only on a project with no specs
1502
+ # yet: no directory under specs/ (no context, no feature) and no active feature. Every
1503
+ # other state is reported as skipped and left untouched, so a re-run of constitution
1504
+ # never moves the active feature and an onboarded brownfield tree is never added to.
1505
+ # Creation goes through set-context and set-feature, so the folders and both pointer
1506
+ # files are written exactly as select-bounded-context and select-feature write them.
1507
+ # context_dir / feature_dir report the active pair after the call, created or not.
1508
+ cmd_default_scope() {
1509
+ local ctx="" feat=""
1510
+ while [ $# -gt 0 ]; do
1511
+ case "$1" in
1512
+ --context) ctx="$2"; shift 2 ;;
1513
+ --feature) feat="$2"; shift 2 ;;
1514
+ --help|-h) echo "default-scope --context <name> --feature <name> (create and activate both on a project with no specs yet)"; return 0 ;;
1515
+ *) echo "default-scope: unknown arg '$1'" >&2; return 1 ;;
1516
+ esac
1517
+ done
1518
+ { [ -n "$ctx" ] && [ -n "$feat" ]; } || { echo "default-scope: --context and --feature are required" >&2; return 1; }
1519
+
1520
+ local repo_root; repo_root=$(get_repo_root)
1521
+ local reason=""
1522
+ if [ -d "$repo_root/specs" ] && [ -n "$(find "$repo_root/specs" -mindepth 1 -maxdepth 1 -type d -print -quit 2>/dev/null)" ]; then
1523
+ reason="specs/ already holds a bounded context or a feature"
1524
+ elif [ -n "$(read_feature_json_feature_directory "$repo_root")" ]; then
1525
+ reason="a feature is already active in .appweaver/feature.json"
1526
+ fi
1527
+
1528
+ local created=false
1529
+ if [ -z "$reason" ]; then
1530
+ cmd_set_context --context "$ctx" >/dev/null || return 1
1531
+ cmd_set_feature --feature "$feat" >/dev/null || return 1
1532
+ created=true
1533
+ fi
1534
+ printf '{"created":%s,"skipped_reason":"%s","context_dir":"%s","feature_dir":"%s"}\n' \
1535
+ "$created" "$(json_escape "$reason")" \
1536
+ "$(json_escape "$(read_context_json_context_directory "$repo_root")")" \
1537
+ "$(json_escape "$(read_feature_json_feature_directory "$repo_root")")"
1538
+ }
1539
+
1463
1540
  # ═══════════════════════════ active_version.json ════════════════════════════
1464
1541
  # PER-FEATURE state at <feature_dir>/active_version.json:
1465
1542
  # {feature, version, active_change, updated}
@@ -1938,7 +2015,7 @@ cmd_detect_project() {
1938
2015
  local repo_root; repo_root=$(get_repo_root)
1939
2016
  local -a markers=()
1940
2017
  local scope="repo" start="$repo_root" rel
1941
- # A lane's source path arrives as the value declare stored, so it may carry a
2018
+ # A lane root folder arrives as the value declare stored, so it may carry a
1942
2019
  # trailing slash; an option label ("path — why") never reaches a script argument.
1943
2020
  rel="${scope_root#./}"; rel="${rel%/}"
1944
2021
  if [ -n "$rel" ] && [ -d "$repo_root/$rel" ]; then
@@ -1971,10 +2048,12 @@ cmd_detect_project() {
1971
2048
  # they scaffold. Names are unique across stacks, so callers pass a bare <Name> and
1972
2049
  # the lookup searches every stack.
1973
2050
 
1974
- # Echo "<stack>/<Name>" for each foundation directory, one per line. A stack also
1975
- # holds siblings that are not foundations (components/), so carrying a *.pack.txt is
1976
- # what makes a directory one — without that test they surface as bundle-less
1977
- # foundations in every lookup.
2051
+ # Echo "<stack>/<Name>" for each PACK foundation directory, one per line — the scan
2052
+ # foundation-locate and foundation-unpack resolve a pack name through. A stack also holds
2053
+ # siblings that are not foundations (components/), so carrying a *.pack.txt is what makes
2054
+ # a directory one here — without that test they surface as bundle-less foundations in
2055
+ # every lookup. A directory carrying only *.spec.txt is listed by foundation-list
2056
+ # (aw-foundation.py) and never unpacked, so it is deliberately absent from this scan.
1978
2057
  _foundation_dirs() {
1979
2058
  local repo_root="$1" s d
1980
2059
  [ -d "$repo_root/foundations" ] || return 0
@@ -1997,98 +2076,145 @@ _foundation_stack() {
1997
2076
  }
1998
2077
 
1999
2078
  # ──────────────────────────── foundation-list ──────────────────────────────
2000
- # Enumerate available foundations (foundations/<stack>/<Name>/ that carry a real
2001
- # <Name>.pack.txt) so the workflow can let the user PICK one — deterministic, no LLM. An optional
2002
- # --args "<Foundation> <App>" is parsed as a direct-argument fast path:
2003
- # preselect_name is echoed back only when it names a real foundation; preselect_app
2004
- # carries any 2nd token through (empty otherwise).
2079
+ # What ONE lane can be scaffolded from, as a pick list — deterministic, no LLM. Every
2080
+ # pack (<Name>.pack.txt) and every spec (*.spec.txt) under foundations/<ecosystem>/<Name>/
2081
+ # that fits the lane, then "[manual spec]" and "[skip]". The scan, the header filters
2082
+ # (# kind / # profiles) and the option labels live in aw-foundation.py so the two shells
2083
+ # answer from one implementation.
2084
+ #
2085
+ # WHICH entries a lane may be offered. A pack is a whole compiling project in one
2086
+ # ecosystem's build system, and an ecosystem is shared by stacks that cannot use each
2087
+ # other's scaffold, so three filters apply: --ecosystem (the lane's), --kind (backend |
2088
+ # frontend; an entry with no `# kind:` is backend) and --profile (only when the entry
2089
+ # names profiles). Without --ecosystem the filter falls back to every declared lane's
2090
+ # ecosystem, and to nothing at all on a bare repo; an EMPTY --ecosystem filters to none.
2005
2091
  #
2006
- # A THIRD token is no longer an output directory. A scaffold lands in the source path of
2007
- # the lane it is scaffolding, which is the only thing that keeps generated code inside a
2008
- # declared lane, so there is no caller-chosen destination left to accept. It comes back as
2009
- # `rejected_out` for the workflow to refuse by name rather than be silently ignored —
2010
- # a path argument that is quietly dropped is how someone believes they scaffolded a
2011
- # sandbox and finds they overwrote their service.
2092
+ # --args "<Foundation> <App>" is the direct-argument fast path: preselect_option is the
2093
+ # option for that name when this lane offers it; preselect_app carries the 2nd token. A
2094
+ # THIRD token comes back as `rejected_out` — a scaffold lands in its lane root folder,
2095
+ # so there is no caller-chosen destination to accept, and the workflow refuses it by name.
2012
2096
  cmd_foundation_list() {
2013
- local raw="" eco_filter=""
2097
+ local -a fwd=()
2098
+ local eco_given=false
2014
2099
  while [ $# -gt 0 ]; do
2015
2100
  case "$1" in
2016
- --args) raw="$2"; shift 2 ;;
2017
- --ecosystem) eco_filter="$2"; shift 2 ;;
2018
- --help|-h) echo "foundation-list [--args '<Foundation> <App>'] [--ecosystem <eco>] -> {foundations[], count, preselect_name, preselect_app, rejected_out}"; return 0 ;;
2101
+ --args|--kind|--profile|--lane-id) fwd+=("$1" "$2"); shift 2 ;;
2102
+ --ecosystem) fwd+=(--ecosystem "$2"); eco_given=true; shift 2 ;;
2103
+ --help|-h) echo "foundation-list [--kind backend|frontend] [--profile <id>] [--ecosystem <eco>] [--lane-id <id>] [--args '<Foundation> <App>'] -> {options[], entries[], count, foundations[], specs[], all_foundations[], preselect_option, preselect_name, preselect_app, rejected_out}"; return 0 ;;
2019
2104
  *) echo "foundation-list: unknown arg '$1'" >&2; return 1 ;;
2020
2105
  esac
2021
2106
  done
2022
- local repo_root; repo_root=$(get_repo_root)
2023
-
2024
- # WHICH foundations this repo may be offered. A foundation bundle is a whole
2025
- # compiling project in one ecosystem's languages and build system, so unpacking
2026
- # a .NET one into a Spring repo does not produce a scaffold — it produces a
2027
- # second, foreign project beside the real one.
2028
- #
2029
- # --ecosystem narrows to the ONE lane being scaffolded, which is stricter than the
2030
- # union of every declared lane: a repo with a .NET service and a Node UI must not be
2031
- # offered its .NET bundle while the UI lane is the target. Without it the filter falls
2032
- # back to every declared lane's ecosystem, and to nothing at all on a bare repo.
2033
- local -a lane_ecos=()
2034
- local e
2035
- if [ -n "$eco_filter" ]; then
2036
- lane_ecos=("$eco_filter")
2037
- else
2038
- while IFS= read -r e; do [ -n "$e" ] && lane_ecos+=("$e"); done < <(_aw_ecosystems)
2107
+ if [ "$eco_given" = false ]; then
2108
+ local e
2109
+ while IFS= read -r e; do [ -n "$e" ] && fwd+=(--ecosystem "$e"); done < <(_aw_ecosystems)
2039
2110
  fi
2111
+ _aw_foundation_py list "${fwd[@]}"
2112
+ }
2040
2113
 
2041
- local -a foundations=() all=() eco_of=()
2042
- local rel name eco keep
2043
- while IFS= read -r rel; do
2044
- name="${rel##*/}"; eco="${rel%%/*}"
2045
- [ -f "$repo_root/foundations/$rel/$name.pack.txt" ] || continue # the primary pack (excludes Reference-only dirs)
2046
- all+=("$name"); eco_of+=("$eco")
2047
- keep=true
2048
- if [ ${#lane_ecos[@]} -gt 0 ]; then
2049
- keep=false
2050
- for e in "${lane_ecos[@]}"; do [ "$e" = "$eco" ] && keep=true; done
2051
- fi
2052
- [ "$keep" = true ] && foundations+=("$name")
2053
- done < <(_foundation_dirs "$repo_root")
2054
- # Optional direct-argument fast path: "<FoundationName> <AppName>".
2055
- local pre_name="" pre_app="" rejected_out=""
2056
- if [ -n "$raw" ]; then
2057
- # shellcheck disable=SC2086
2058
- set -- $raw
2059
- [ -n "${1:-}" ] && pre_name="$1"; [ -n "${2:-}" ] && pre_app="$2"; [ -n "${3:-}" ] && rejected_out="$3"
2114
+ # ─────────────────────────── foundation-choice ─────────────────────────────
2115
+ # Resolve the option picked from foundation-list back to its entry: route (pack | spec |
2116
+ # manual | skip), name and file. Takes the same filters as the list, so a numbered answer
2117
+ # counts the same rows the operator saw.
2118
+ cmd_foundation_choice() {
2119
+ local -a fwd=()
2120
+ while [ $# -gt 0 ]; do
2121
+ case "$1" in
2122
+ --selection|--kind|--profile|--ecosystem) fwd+=("$1" "$2"); shift 2 ;;
2123
+ --help|-h) echo "foundation-choice --selection <option> [--kind <k>] [--profile <id>] [--ecosystem <eco>] -> {ok, route, name, file, ecosystem, title, reason}"; return 0 ;;
2124
+ *) echo "foundation-choice: unknown arg '$1'" >&2; return 1 ;;
2125
+ esac
2126
+ done
2127
+ _aw_foundation_py choice "${fwd[@]}"
2128
+ }
2129
+
2130
+ # ────────────────────────── foundation-spec-capture ────────────────────────
2131
+ # Copy the spec a lane is about to be built from to .appweaver/foundation/<lane>.spec.md
2132
+ # — from a bundle's *.spec.txt (--file), or from what the operator gave (--input: a path
2133
+ # when it names a file, the spec text otherwise). Always exits 0; `ok` carries the verdict.
2134
+ cmd_foundation_spec_capture() {
2135
+ local -a fwd=()
2136
+ while [ $# -gt 0 ]; do
2137
+ case "$1" in
2138
+ --lane|--kind|--file|--input) fwd+=("$1" "$2"); shift 2 ;;
2139
+ --help|-h) echo "foundation-spec-capture --lane <id> [--kind <k>] (--file <path> | --input <path|text>) -> {ok, spec_path, source, chars, title, reason}"; return 0 ;;
2140
+ *) echo "foundation-spec-capture: unknown arg '$1'" >&2; return 1 ;;
2141
+ esac
2142
+ done
2143
+ _aw_foundation_py spec-capture "${fwd[@]}"
2144
+ }
2145
+
2146
+ # ─────────────────────────── foundation-lane-files ─────────────────────────
2147
+ # What a lane already holds: its lane root folder, its test path when that is another
2148
+ # subtree, and its build files where its commands run when that is outside the lane (the
2149
+ # profile's `detect` entries with no `/`). Build output and dependency directories are
2150
+ # named, not walked. Asked before anything is scaffolded into the lane.
2151
+ cmd_foundation_lane_files() {
2152
+ local -a fwd=()
2153
+ while [ $# -gt 0 ]; do
2154
+ case "$1" in
2155
+ --source|--test|--workdir|--profile) fwd+=("$1" "$2"); shift 2 ;;
2156
+ --help|-h) echo "foundation-lane-files --source <p> [--test <p>] [--workdir <p>] [--profile <id>] -> {has_files, count, sample[], generated[], root_files[], folders[], workdir, summary}"; return 0 ;;
2157
+ *) echo "foundation-lane-files: unknown arg '$1'" >&2; return 1 ;;
2158
+ esac
2159
+ done
2160
+ _aw_foundation_py lane-files "${fwd[@]}"
2161
+ }
2162
+
2163
+ # ─────────────────────── foundation-snapshot / -outside ────────────────────
2164
+ # A spec build is told to write only inside its lane root folder; these check that it did.
2165
+ # `snapshot` records every file in the repo before the build; `outside` lists what was
2166
+ # created, changed or deleted anywhere else afterwards and, with --undo, removes what was
2167
+ # created. Build output and dependency folders are one entry each, not walked.
2168
+ cmd_foundation_snapshot() {
2169
+ local -a fwd=()
2170
+ while [ $# -gt 0 ]; do
2171
+ case "$1" in
2172
+ --out) fwd+=("$1" "$2"); shift 2 ;;
2173
+ --help|-h) echo "foundation-snapshot --out <repo-relative file> -> {ok, snapshot, entries}"; return 0 ;;
2174
+ *) echo "foundation-snapshot: unknown arg '$1'" >&2; return 1 ;;
2175
+ esac
2176
+ done
2177
+ _aw_foundation_py snapshot "${fwd[@]}"
2178
+ }
2179
+
2180
+ cmd_foundation_outside() {
2181
+ local -a fwd=()
2182
+ while [ $# -gt 0 ]; do
2183
+ case "$1" in
2184
+ --snapshot|--lane) fwd+=("$1" "$2"); shift 2 ;;
2185
+ --undo) fwd+=("$1"); shift ;;
2186
+ --help|-h) echo "foundation-outside --snapshot <file> --lane <lane root folder> [--undo] -> {clean, created[], changed[], deleted[], removed[], summary}"; return 0 ;;
2187
+ *) echo "foundation-outside: unknown arg '$1'" >&2; return 1 ;;
2188
+ esac
2189
+ done
2190
+ _aw_foundation_py outside "${fwd[@]}"
2191
+ }
2192
+
2193
+ _aw_foundation_py() {
2194
+ local repo_root; repo_root=$(get_repo_root)
2195
+ local core="$SCRIPT_DIR/../python/aw-foundation.py"
2196
+ if [ ! -f "$core" ]; then
2197
+ echo "aw-foundation: core not found at $core" >&2; return 1
2198
+ fi
2199
+ if ! command -v python3 >/dev/null 2>&1; then
2200
+ echo "aw-foundation: python3 is required to list foundations" >&2; return 1
2060
2201
  fi
2061
- # Only preselect a name that actually names an available foundation.
2062
- local match="" f
2063
- for f in "${foundations[@]}"; do [ "$f" = "$pre_name" ] && match="$pre_name"; done
2064
- printf '{"foundations":%s,"count":%s,"all_foundations":%s,"all_count":%s,"ecosystems":%s,"lane_ecosystems":%s,"filtered":%s,"preselect_name":"%s","preselect_app":"%s","rejected_out":"%s"}\n' \
2065
- "$(json_str_array "${foundations[@]}")" "${#foundations[@]}" \
2066
- "$(json_str_array "${all[@]}")" "${#all[@]}" \
2067
- "$(json_str_array "${eco_of[@]}")" "$(json_str_array "${lane_ecos[@]}")" \
2068
- "$([ ${#lane_ecos[@]} -gt 0 ] && echo true || echo false)" \
2069
- "$(json_escape "$match")" "$(json_escape "$pre_app")" "$(json_escape "$rejected_out")"
2202
+ python3 "$core" --repo "$repo_root" "$@"
2070
2203
  }
2071
2204
 
2072
2205
  # ─────────────────────────── foundation-locate ─────────────────────────────
2073
2206
  # Parse/validate args, locate the pack + unpacker, read the pack header (bounded).
2074
2207
  cmd_foundation_locate() {
2075
- local name="" app="" out="." raw=""
2208
+ local name="" app="" out="."
2076
2209
  while [ $# -gt 0 ]; do
2077
2210
  case "$1" in
2078
2211
  --name) name="$2"; shift 2 ;;
2079
2212
  --app) app="$2"; shift 2 ;;
2080
2213
  --out) out="$2"; shift 2 ;;
2081
- --args) raw="$2"; shift 2 ;;
2082
- --help|-h) echo "foundation-locate (--args '<F> <A> [out]' | --name <F> --app <A> [--out <dir>])"; return 0 ;;
2214
+ --help|-h) echo "foundation-locate --name <F> --app <A> [--out <dir>]"; return 0 ;;
2083
2215
  *) echo "foundation-locate: unknown arg '$1'" >&2; return 1 ;;
2084
2216
  esac
2085
2217
  done
2086
- # positional parse: "<FoundationName> <AppName> [OutputDir]"
2087
- if [ -n "$raw" ]; then
2088
- # shellcheck disable=SC2086
2089
- set -- $raw
2090
- [ -n "$1" ] && name="$1"; [ -n "$2" ] && app="$2"; [ -n "$3" ] && out="$3"
2091
- fi
2092
2218
  local repo_root; repo_root=$(get_repo_root)
2093
2219
  # available = every foundation dir (_foundation_dirs already requires a *.pack.txt)
2094
2220
  local -a available=()
@@ -2098,7 +2224,7 @@ cmd_foundation_locate() {
2098
2224
  done < <(_foundation_dirs "$repo_root")
2099
2225
  _fnd_fail() { printf '{"ok":false,"reason":"%s","available":%s}\n' "$(json_escape "$1")" "$(json_str_array "${available[@]}")"; }
2100
2226
  if [ -z "$name" ] || [ -z "$app" ]; then
2101
- _fnd_fail "Usage: /aw-spec-foundation <FoundationName> <AppName> [OutputDirectory]"; return 0
2227
+ _fnd_fail "foundation-locate needs --name <FoundationName> and --app <AppName>"; return 0
2102
2228
  fi
2103
2229
  local stack; stack=$(_foundation_stack "$repo_root" "$name")
2104
2230
  if [ -z "$stack" ]; then _fnd_fail "Foundation '$name' not found (expected foundations/<stack>/$name/)"; return 0; fi
@@ -2113,7 +2239,7 @@ cmd_foundation_locate() {
2113
2239
  elif [ -f "$fdir/unpack-foundation.sh" ]; then
2114
2240
  runner="sh"; unpacker="$rel/unpack-foundation.sh"
2115
2241
  else
2116
- emit_fail "No unpacker found in $rel/ (unpack-foundation.ps1 or .sh)"; return 0
2242
+ _fnd_fail "No unpacker found in $rel/ (unpack-foundation.ps1 or .sh)"; return 0
2117
2243
  fi
2118
2244
  # bounded header read (never load the pack body)
2119
2245
  local header sentinel file_count
@@ -2122,17 +2248,21 @@ cmd_foundation_locate() {
2122
2248
  file_count=$(printf '%s\n' "$header" | grep -iE 'file' | grep -oE '[0-9]+' | head -1)
2123
2249
  [ -n "$file_count" ] || file_count="0"
2124
2250
 
2125
- printf '{"ok":true,"reason":"","name":"%s","stack":"%s","app":"%s","out_dir":"%s","pack_file":"%s","unpacker":"%s","runner":"%s","sentinel":"%s","file_count":%s,"available":%s}\n' \
2251
+ # A reference sample is offered only when its pack is there to unpack.
2252
+ local has_reference=false
2253
+ [ -f "$fdir/$name.Reference.pack.txt" ] && has_reference=true
2254
+
2255
+ printf '{"ok":true,"reason":"","name":"%s","stack":"%s","app":"%s","out_dir":"%s","pack_file":"%s","unpacker":"%s","runner":"%s","sentinel":"%s","file_count":%s,"has_reference":%s,"available":%s}\n' \
2126
2256
  "$(json_escape "$name")" "$(json_escape "$stack")" "$(json_escape "$app")" "$(json_escape "$out")" "$(json_escape "$rel/$name.pack.txt")" \
2127
- "$(json_escape "$unpacker")" "$runner" "$(json_escape "$sentinel")" "$file_count" "$(json_str_array "${available[@]}")"
2257
+ "$(json_escape "$unpacker")" "$runner" "$(json_escape "$sentinel")" "$file_count" "$has_reference" "$(json_str_array "${available[@]}")"
2128
2258
  }
2129
2259
 
2130
2260
  # ─────────────────────────── foundation-unpack ─────────────────────────────
2131
2261
  # Run the OS-selected unpacker for the foundation (or reference) pack.
2132
2262
  #
2133
- # --out is the LANE'S SOURCE PATH, so every file a pack carries lands inside the subtree
2263
+ # --out is the LANE ROOT FOLDER, so every file a pack carries lands inside the subtree
2134
2264
  # its lane owns. --test-path is the lane's other subtree: a pack that carries its own test
2135
- # project puts it under the source path like everything else, and aw-foundation.py lifts it
2265
+ # project puts it under the lane root folder like everything else, and aw-foundation.py lifts it
2136
2266
  # onto the path the lane actually keeps tests in. See that module for when it is a no-op.
2137
2267
  cmd_foundation_unpack() {
2138
2268
  local name="" app="" out="." dry=false force="no" reference=false test_path=""
@@ -2206,27 +2336,35 @@ cmd_foundation_unpack() {
2206
2336
  "$(json_escape "$(printf '%s' "$outp" | tail -c 800)")"
2207
2337
  }
2208
2338
 
2209
- # ─────────────────────── foundation-stash-dbcontext ────────────────────────
2210
- # Copy the current ApplicationDbContext.cs to a .foundation.bak sibling before a
2211
- # reference unpack overwrites it (so the two can be merged afterward).
2212
- cmd_foundation_stash_dbcontext() {
2213
- local out="."
2339
+ # ───────────────────── foundation-rules / -rules-done ──────────────────────
2340
+ # A pack's rule file (<pack name>.rules.json beside it) names the files and folders that
2341
+ # are merged with, or replaced over, what the lane already holds; every other pack file is
2342
+ # written as packed. `rules` runs before an unpack: it lists the merges and puts the files
2343
+ # they merge with aside under --state; `rules-done` removes them once merged. Both
2344
+ # delegate to aw-foundation.py.
2345
+ cmd_foundation_rules() {
2346
+ local -a fwd=()
2214
2347
  while [ $# -gt 0 ]; do
2215
2348
  case "$1" in
2216
- --out) out="$2"; shift 2 ;;
2217
- --help|-h) echo "foundation-stash-dbcontext --out <dir>"; return 0 ;;
2218
- *) echo "foundation-stash-dbcontext: unknown arg '$1'" >&2; return 1 ;;
2349
+ --name|--app|--out|--state) fwd+=("$1" "$2"); shift 2 ;;
2350
+ --reference) fwd+=("$1"); shift ;;
2351
+ --help|-h) echo "foundation-rules --name <F> --app <A> --out <lane root folder> [--reference] [--state <folder>] -> {ok, reason, rules_file, rules, merges[], merge_count, stale[], note}"; return 0 ;;
2352
+ *) echo "foundation-rules: unknown arg '$1'" >&2; return 1 ;;
2219
2353
  esac
2220
2354
  done
2221
- local repo_root; repo_root=$(get_repo_root)
2222
- [[ "$out" != /* ]] && out="$repo_root/$out"
2223
- # Searched from the unpack root itself, which IS the lane's source path — a `src/`
2224
- # segment underneath it is the old repo-root layout and is not there any more.
2225
- local ctx; ctx=$(find "$out" -name "ApplicationDbContext.cs" 2>/dev/null | head -1)
2226
- if [ -z "$ctx" ]; then printf '{"found":false,"dbcontext":"","backup":""}\n'; return 0; fi
2227
- local bak="$ctx.foundation.bak"
2228
- cp "$ctx" "$bak"
2229
- printf '{"found":true,"dbcontext":"%s","backup":"%s"}\n' "$(json_escape "$ctx")" "$(json_escape "$bak")"
2355
+ _aw_foundation_py rules "${fwd[@]}"
2356
+ }
2357
+
2358
+ cmd_foundation_rules_done() {
2359
+ local -a fwd=()
2360
+ while [ $# -gt 0 ]; do
2361
+ case "$1" in
2362
+ --state) fwd+=("$1" "$2"); shift 2 ;;
2363
+ --help|-h) echo "foundation-rules-done [--state <folder>] -> {ok, removed}"; return 0 ;;
2364
+ *) echo "foundation-rules-done: unknown arg '$1'" >&2; return 1 ;;
2365
+ esac
2366
+ done
2367
+ _aw_foundation_py rules-done "${fwd[@]}"
2230
2368
  }
2231
2369
 
2232
2370
  # ─────────────────────────── foundation-verify ─────────────────────────────
@@ -2246,9 +2384,11 @@ cmd_foundation_verify() {
2246
2384
  local -a residue=()
2247
2385
  if [ -n "$sentinel" ] && [ -d "$out" ]; then
2248
2386
  # scan the generated tree only — never the bundle/framework dirs (the
2249
- # .pack.txt legitimately contains the sentinel).
2387
+ # .pack.txt legitimately contains the sentinel). -F: the sentinel is literal
2388
+ # text, and as a pattern its dots match any character ("AppWeaver Foo" would
2389
+ # count as AppWeaver.Foo) — the PowerShell twin uses -SimpleMatch for the same.
2250
2390
  while IFS= read -r m; do [ -n "$m" ] && residue+=("$m"); done < <(
2251
- grep -rIl --exclude-dir=.git --exclude-dir=guardrails --exclude-dir=foundations --exclude-dir=.appweaver \
2391
+ grep -rIlF --exclude-dir=.git --exclude-dir=guardrails --exclude-dir=foundations --exclude-dir=.appweaver \
2252
2392
  --exclude-dir=specs --exclude-dir=node_modules -- "$sentinel" "$out" 2>/dev/null \
2253
2393
  | sed "s#^$repo_root/##" | head -50 )
2254
2394
  fi
@@ -2743,7 +2883,7 @@ cmd_find_host_project() {
2743
2883
  done
2744
2884
  local repo_root; repo_root=$(get_repo_root)
2745
2885
  local prog
2746
- # WHERE the host lives. The service lane's declared source path when there is one,
2886
+ # WHERE the host lives. The declared lane root folder of the service lane when there is one,
2747
2887
  # `src/` when there is not — the same question run-migrate asks, and the same answer.
2748
2888
  # A repo whose service sits in `backend/` or `Legacy/App/` is searched where its code
2749
2889
  # actually is instead of being reported as having no host project at all.
@@ -3061,10 +3201,15 @@ except Exception:
3061
3201
  }
3062
3202
 
3063
3203
  cmd_build_compile() {
3204
+ # --kind builds ONE declared lane — what `foundation` uses to check the lane it just
3205
+ # created without compiling a lane it did not touch. Ignored on the undeclared ladder
3206
+ # below, which has no lanes to choose between.
3207
+ local kind=""
3064
3208
  while [ $# -gt 0 ]; do
3065
3209
  case "$1" in
3066
3210
  --feature-dir) shift 2 ;;
3067
- --help|-h) echo "build-compile [--feature-dir <path>]"; return 0 ;;
3211
+ --kind) kind="$2"; shift 2 ;;
3212
+ --help|-h) echo "build-compile [--feature-dir <path>] [--kind backend|frontend]"; return 0 ;;
3068
3213
  *) echo "build-compile: unknown arg '$1'" >&2; return 1 ;;
3069
3214
  esac
3070
3215
  done
@@ -3074,7 +3219,11 @@ cmd_build_compile() {
3074
3219
  # sniffer takes the first toolchain it finds and silently never builds the
3075
3220
  # second, which reports green for a frontend that was never compiled.
3076
3221
  if [ -n "$(_aw_lanes_declared)" ]; then
3077
- _aw_stack_py run-phase --phase build
3222
+ if [ -n "$kind" ]; then
3223
+ _aw_stack_py run-phase --phase build --kind "$kind"
3224
+ else
3225
+ _aw_stack_py run-phase --phase build
3226
+ fi
3078
3227
  return $?
3079
3228
  fi
3080
3229
 
@@ -3283,8 +3432,8 @@ print(json.dumps({"ok": bool(d.get("ok")), "tool": "lanes", "ran": bool(d.get("r
3283
3432
  return 0
3284
3433
  fi
3285
3434
 
3286
- # WHERE to look for the migration project. The service lane's declared source
3287
- # path when there is one, `src/` when there is not. Migrations belong to the
3435
+ # WHERE to look for the migration project. The declared lane root folder of the
3436
+ # service lane when there is one, `src/` when there is not. Migrations belong to the
3288
3437
  # backend lane by definition, so a repo whose service code sits in `Legacy/App/`
3289
3438
  # or `backend/` gets searched where its code actually is instead of being told
3290
3439
  # it has no .NET project at all.
@@ -3297,7 +3446,7 @@ print(json.dumps({"ok": bool(d.get("ok")), "tool": "lanes", "ran": bool(d.get("r
3297
3446
  if ls "$repo_root"/*.sln >/dev/null 2>&1 || find "$svc_dir" -maxdepth 4 -name "*.csproj" 2>/dev/null | grep -q .; then has_dotnet=true; fi
3298
3447
  if [ "$has_dotnet" = true ] && command -v dotnet >/dev/null 2>&1 && ( cd "$repo_root" && dotnet ef --version >/dev/null 2>&1 ); then
3299
3448
  # Prefer *Service.csproj (API host with ApplicationDbContext); fall back to first
3300
- # non-AppHost/Web/ServiceDefaults/test project under the service lane's root.
3449
+ # non-AppHost/Web/ServiceDefaults/test project under the service lane root folder.
3301
3450
  local project=""
3302
3451
  project=$(find "$svc_dir" -maxdepth 3 -name '*.Service.csproj' 2>/dev/null | head -1)
3303
3452
  if [ -z "$project" ]; then
@@ -5055,6 +5204,7 @@ case "$sub" in
5055
5204
  set-context) cmd_set_context "$@" ;;
5056
5205
  list-features) cmd_list_features "$@" ;;
5057
5206
  set-feature) cmd_set_feature "$@" ;;
5207
+ default-scope) cmd_default_scope "$@" ;;
5058
5208
  version-read) cmd_version_read "$@" ;;
5059
5209
  version-init) cmd_version_init "$@" ;;
5060
5210
  version-bootstrap-done) cmd_version_bootstrap_done "$@" ;;
@@ -5095,10 +5245,16 @@ case "$sub" in
5095
5245
  checklist-apply) cmd_checklist_apply "$@" ;;
5096
5246
  detect-project) cmd_detect_project "$@" ;;
5097
5247
  foundation-list) cmd_foundation_list "$@" ;;
5248
+ foundation-choice) cmd_foundation_choice "$@" ;;
5249
+ foundation-spec-capture) cmd_foundation_spec_capture "$@" ;;
5250
+ foundation-lane-files) cmd_foundation_lane_files "$@" ;;
5251
+ foundation-snapshot) cmd_foundation_snapshot "$@" ;;
5252
+ foundation-outside) cmd_foundation_outside "$@" ;;
5098
5253
  foundation-locate) cmd_foundation_locate "$@" ;;
5099
5254
  foundation-unpack) cmd_foundation_unpack "$@" ;;
5100
5255
  foundation-verify) cmd_foundation_verify "$@" ;;
5101
- foundation-stash-dbcontext) cmd_foundation_stash_dbcontext "$@" ;;
5256
+ foundation-rules) cmd_foundation_rules "$@" ;;
5257
+ foundation-rules-done) cmd_foundation_rules_done "$@" ;;
5102
5258
  list-components) cmd_list_components "$@" ;;
5103
5259
  component-catalog) cmd_component_catalog "$@" ;;
5104
5260
  component-verify) cmd_component_verify "$@" ;;