aw-spec-cli 0.1.28 → 0.1.30

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 (67) hide show
  1. package/assets/.amp/agent.json +22 -0
  2. package/assets/.amp/settings-edit.json +5 -0
  3. package/assets/.amp/settings-read.json +12 -0
  4. package/assets/.appweaver/DESIGN-FORMAT.md +9 -0
  5. package/assets/.appweaver/STACK-PROFILES.md +102 -15
  6. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +40 -18
  7. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +56 -4
  8. package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +17 -1
  9. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +12 -3
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +289 -24
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +370 -85
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +349 -65
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +39 -7
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +63 -4
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +5 -4
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +430 -138
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +392 -76
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-lane.orch.yaml +104 -0
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +20 -3
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +26 -3
  21. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +314 -44
  22. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +249 -20
  23. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +17 -2
  24. package/assets/.appweaver/scripts/python/aw-plangate.py +109 -19
  25. package/assets/.appweaver/scripts/python/aw-stack.py +1295 -9
  26. package/assets/.appweaver/templates/delta-design-template.md +7 -5
  27. package/assets/.appweaver/templates/delta-spec-template.md +2 -0
  28. package/assets/.appweaver/templates/design-template.md +17 -0
  29. package/assets/.appweaver/templates/ui-design-template.md +19 -4
  30. package/assets/.augment/agent.json +22 -0
  31. package/assets/.augment/commands/appweaver.md +12 -0
  32. package/assets/.cline/agent.json +22 -0
  33. package/assets/.clinerules/workflows/appweaver.md +7 -0
  34. package/assets/.codex/agent.json +22 -0
  35. package/assets/.continue/agent.json +22 -0
  36. package/assets/.continue/prompts/appweaver.md +13 -0
  37. package/assets/.cursor/agent.json +22 -0
  38. package/assets/.cursor/commands/appweaver.md +7 -0
  39. package/assets/.devin/agent.json +22 -0
  40. package/assets/.devin/workflows/appweaver.md +11 -0
  41. package/assets/.factory/agent.json +22 -0
  42. package/assets/.factory/commands/appweaver.md +12 -0
  43. package/assets/.gemini/agent.json +22 -0
  44. package/assets/.github/prompts/appweaver.prompt.md +13 -0
  45. package/assets/.kilo/agent.json +28 -0
  46. package/assets/.kilo/agents/appweaver-edit.md +12 -0
  47. package/assets/.kilo/agents/appweaver-read.md +12 -0
  48. package/assets/.kilo/commands/appweaver.md +11 -0
  49. package/assets/.kiro/agent.json +22 -0
  50. package/assets/.kiro/skills/appweaver/SKILL.md +12 -0
  51. package/assets/.opencode/agent.json +22 -0
  52. package/assets/.opencode/agents/appweaver-edit.md +12 -0
  53. package/assets/.opencode/agents/appweaver-read.md +13 -0
  54. package/assets/.opencode/commands/appweaver.md +11 -0
  55. package/assets/.roo/agent.json +22 -0
  56. package/assets/.roo/commands/appweaver.md +12 -0
  57. package/dist/commands/init.d.ts.map +1 -1
  58. package/dist/commands/init.js +14 -1
  59. package/dist/commands/init.js.map +1 -1
  60. package/dist/utils/config.d.ts.map +1 -1
  61. package/dist/utils/config.js +14 -0
  62. package/dist/utils/config.js.map +1 -1
  63. package/dist/utils/copy-assets.d.ts +16 -3
  64. package/dist/utils/copy-assets.d.ts.map +1 -1
  65. package/dist/utils/copy-assets.js +29 -14
  66. package/dist/utils/copy-assets.js.map +1 -1
  67. package/package.json +1 -1
@@ -37,22 +37,39 @@
37
37
  # (SOLE WRITER of .appweaver/stacks.json; callers = select-stack, onboard)
38
38
  # lane-resolve [--kind <k>] [--id <lane>] [--feature-dir <path>]
39
39
  # -> {declared, lanes[], found, lane, root, source_path, build, test, run,
40
- # backend_*/frontend_* (declared, root, source_path, root_exists, build, test, run,
41
- # feature_excluded), feature_lanes[], prototype}
40
+ # backend_*/frontend_* (declared, working, root, source_path, root_exists,
41
+ # build, test, run, feature_excluded), feature_lanes[], prototype,
42
+ # working_lane, working_kind, working_kinds[], working_root, working_test_path}
42
43
  # (absence is a valid state: no stacks.json -> declared:false, exit 0;
43
- # --feature-dir reports a lane the feature records no surface in as not declared)
44
- # lane-verify [--require backend|frontend]… [--allow-missing-root] [--command <phase>]
45
- # -> {ok, declared, issues[], issue_count, message, backend_*/frontend_*}
44
+ # --feature-dir reports a lane the feature records no surface in as not declared;
45
+ # *_working = declared AND in this developer's .appweaver/lane.json)
46
+ # lane-verify [--require backend|frontend]… [--allow-missing-root] [--working-only] [--command <phase>]
47
+ # -> {ok, declared, issues[], issue_count, message, backend_*/frontend_*, working_*}
46
48
  # (the GATE: is the declaration usable for the phase about to run.
47
- # Always exits 0 — the workflow's verify_context owns the stop)
49
+ # Always exits 0 — the workflow's verify_context owns the stop;
50
+ # --working-only checks on disk only the working lanes' root folders)
51
+ # lane-select [--lane backend|frontend|both] [--default] [--preselect <text>]
52
+ # -> {lane, kinds[], source, note, default, select_options[], preselect, written}
53
+ # (the developer's WORKING lane in .appweaver/lane.json, not committed;
54
+ # --default = both when both lanes are declared, else the one that is)
48
55
  # lane-probe [--kind <k>] [--hint <text>] -> {declared, declared_profile, hint_profile, hint_terms[], signals[], any, evidence}
49
56
  # (the FILE signals for "does this lane exist"; resolve-tech carries the rule signal)
50
57
  # lane-set (--id <lane>|--kind <k>) [--set KEY=VALUE]… [--unset KEY]… -> {ok, lane_id, set, removed, written}
51
58
  # (KEY: contract_run, contract, client; an empty VALUE changes nothing)
52
59
  # contract-export [--timeout <s>] -> {ok, ran, cmd, url, doc, doc_exists, operations, unnamed, already_running, reason, log_tail}
53
60
  # (start the service lane with contract_run, save the document at contract, stop it)
54
- # contract-client -> run-phase result for the frontend lane's `client` command
55
- # contract-check --plan <p> [--doc <d>] -> {ok, cited, ids[], missing[], uncited, doc_exists, reason}
61
+ # contract-draft --feature-dir <p> -> {ok, written, removed, path, plan, operations, ids[], ids_csv, unread_count, reason}
62
+ # (the feature's api-draft.json, drafted by script from its backend plan's API section)
63
+ # contract-combine -> {ok, doc, export, export_exists, drafts[], added, added_csv, served_csv, clashes_csv, status, reason}
64
+ # (.appweaver/contracts/openapi.combined.json: the export plus every drafted operation it does not serve)
65
+ # contract-requests --feature-dir <p> -> {count, open_count, open_csv, declined_csv, rebind_csv, backend_text, frontend_text, summary, requests[]}
66
+ # (the frontend plan's RQ-n requests and the backend plan's answers)
67
+ # contract-client [--doc <d>] -> run-phase result for the frontend lane's `client` command ({{contract}} = --doc)
68
+ # contract-check --plan <p> [--doc <d>] -> {ok, cited, ids[], missing[], drafted[], uncited, doc_exists, reason}
69
+ # contract-impact --delta-backend <p> --frontend-plan <p> [--delta-frontend <p>]
70
+ # -> {changes[], affected[], addressed[], unaddressed[], unaddressed_csv, warn}
71
+ # (operations a change's service delta changes or removes that the frontend binds to,
72
+ # with no matching change in its frontend delta plan)
56
73
  # list-features [--preselect <name>] [--bundle <dir>] -> {features[], names[], count, select_options[], sentinel} (specs/ ∪ features the OKF concepts serve)
57
74
  # set-feature --feature <name> -> {feature, feature_dir, spec_file, existed, created, spec_exists}
58
75
  # default-scope --context <name> --feature <name> -> {created, skipped_reason, context_dir, feature_dir}
@@ -114,7 +131,8 @@
114
131
  # build-compile [--feature-dir <path>] [--kind backend|frontend]
115
132
  # -> {ok, tool, ran, cmd, log_tail}
116
133
  # (--kind builds that one declared lane; ignored when no lane is declared)
117
- # test-run [--feature-dir <path>] -> {ok, tool, ran, cmd, log_tail,
134
+ # test-run [--feature-dir <path>] [--scope slice|repo] [--also <csv>]… [--kind backend|frontend]
135
+ # -> {ok, tool, ran, cmd, log_tail,
118
136
  # failing_excerpt, failing_lanes[],
119
137
  # no_setup, setup_reason, setup_missing[]}
120
138
  # (failing_excerpt = the lines that NAME the failure, per failing lane;
@@ -125,17 +143,32 @@
125
143
  # -> {cr_id, cr_dir, delta_spec, delta_model, wave, open_count, created,
126
144
  # created_on, base_version, base_context_version}
127
145
  # cr-list [--feature-dir <path>] [--preselect <cr-id|main|new>] [--include-main] [--include-new]
128
- # -> {changes[], ids[], count, select_options[], matched_label, main_label,
129
- # new_label, only_id, active_change}
146
+ # [--lane backend|frontend|both]
147
+ # -> {changes[] (each with lanes[], lanes_recorded), ids[], count,
148
+ # select_options[], matched_label, main_label, new_label, only_id,
149
+ # active_change, lane_filter[], hidden_ids[], hidden_count,
150
+ # hidden_ids_csv, preselect_hidden}
151
+ # (--lane lists only the changes touching one of those lanes; none = every open change)
130
152
  # cr-paths --cr-id <id> [--feature-dir <path>]
131
153
  # -> {cr_dir, delta_spec, delta_model, delta_backend_plan, delta_frontend_plan,
132
- # wave, predecessor_files[], predecessor_specs[], predecessor_docs[],
133
- # sibling_files[], domain_model}
154
+ # wave, lanes[], lanes_csv, lanes_recorded, touches_backend,
155
+ # touches_frontend, predecessor_files[], predecessor_specs[],
156
+ # predecessor_docs[], sibling_files[], domain_model}
134
157
  # cr-overlap --cr-id <id> [--feature-dir <path>]
135
158
  # -> {aggregates[], overlaps[], overlap_count, predecessor_overlaps,
136
159
  # sibling_overlaps, successor_overlaps, overlap_report}
137
160
  # cr-activate --cr-id <id> [--feature-dir <path>] -> {active_change, activated, open_changes[]}
138
161
  # cr-wave --cr-id <id> --wave <n> [--feature-dir <path>] -> {cr_id, wave, stamped}
162
+ # cr-lanes [--cr-id <id> [--feature-dir <path>] [--stamp] [--set both|backend|frontend]]
163
+ # -> {select_options[], has_options, default, recorded, recorded_kinds[],
164
+ # lanes[], lanes_csv, backend, frontend, source, stamped}
165
+ # (the lanes a change touches, on its delta-spec's **Lanes** line; no line = every declared lane)
166
+ # slice-gate-stamp --cr-id <id> --lane backend|frontend [--feature-dir <path>] [--tests-green true|false]
167
+ # [--tests-ran true|false] [--requirements-ok true|false] [--findings <json list>]
168
+ # -> {file, lane, cr_id, verdict, tests_green, tests_ran,
169
+ # requirements_ok, findings, high_findings, gated}
170
+ # (one lane's analyze-slice-delta result, as changes/<id>/.aw-slice-gate.<lane>.json;
171
+ # pass = no test failed and the requirements met, tests_ran = a suite actually ran)
139
172
  # cr-archive --cr-id <id> [--feature-dir <path>] [--recall] [--reason <text>]
140
173
  # -> {ok, archived_dir, archived_model, recalled,
141
174
  # recall_marker, version, previous,
@@ -858,10 +891,11 @@ cmd_lane_set() {
858
891
  _aw_stack_py set "${fwd[@]}"
859
892
  }
860
893
 
861
- # ─────────────── contract-export / contract-client / contract-check ───────────────
894
+ # ─────────────── contract-export / -draft / -combine / -requests / -client / -check ───────────────
862
895
  # The API contract between the lanes: export the service lane's API document from the
863
- # running service, generate the frontend lane's client from it, and check the operation
864
- # ids a frontend plan cites against it. All three delegate to aw-stack.py.
896
+ # running service, draft a feature's document from its backend plan, combine the two for
897
+ # the frontend, read the request ledger between the plans, generate the frontend lane's
898
+ # client, and check the operation ids a frontend plan cites. All delegate to aw-stack.py.
865
899
  cmd_contract_export() {
866
900
  local -a fwd=()
867
901
  while [ $# -gt 0 ]; do
@@ -874,14 +908,53 @@ cmd_contract_export() {
874
908
  _aw_stack_py contract-export "${fwd[@]}"
875
909
  }
876
910
 
911
+ cmd_contract_draft() {
912
+ local -a fwd=()
913
+ while [ $# -gt 0 ]; do
914
+ case "$1" in
915
+ --feature-dir) if [ -n "$2" ]; then fwd+=("$1" "$2"); fi; shift 2 ;;
916
+ --help|-h) echo "contract-draft --feature-dir <path> -> {ok, written, removed, path, plan, operations, ids, ids_csv, unread_count, reason}"; return 0 ;;
917
+ *) echo "contract-draft: unknown arg '$1'" >&2; return 1 ;;
918
+ esac
919
+ done
920
+ _aw_stack_py contract-draft "${fwd[@]+"${fwd[@]}"}"
921
+ }
922
+
923
+ cmd_contract_combine() {
924
+ while [ $# -gt 0 ]; do
925
+ case "$1" in
926
+ --help|-h) echo "contract-combine -> {ok, doc, export, export_exists, drafts, added, added_csv, served_csv, clashes_csv, status, reason}"; return 0 ;;
927
+ *) echo "contract-combine: unknown arg '$1'" >&2; return 1 ;;
928
+ esac
929
+ done
930
+ _aw_stack_py contract-combine
931
+ }
932
+
933
+ cmd_contract_requests() {
934
+ local -a fwd=()
935
+ while [ $# -gt 0 ]; do
936
+ case "$1" in
937
+ --feature-dir) if [ -n "$2" ]; then fwd+=("$1" "$2"); fi; shift 2 ;;
938
+ --help|-h) echo "contract-requests --feature-dir <path> -> {count, open_count, open_csv, declined_csv, rebind_csv, backend_text, frontend_text, summary, requests}"; return 0 ;;
939
+ *) echo "contract-requests: unknown arg '$1'" >&2; return 1 ;;
940
+ esac
941
+ done
942
+ _aw_stack_py contract-requests "${fwd[@]+"${fwd[@]}"}"
943
+ }
944
+
945
+ # --doc names the document `{{contract}}` stands for in the lane's `client` command — the
946
+ # combined document, from implement. Without it, the exported one. An empty --doc is
947
+ # dropped, so a workflow can render the argument whole.
877
948
  cmd_contract_client() {
949
+ local -a fwd=()
878
950
  while [ $# -gt 0 ]; do
879
951
  case "$1" in
880
- --help|-h) echo "contract-client -> run-phase --phase client for the frontend lane"; return 0 ;;
952
+ --doc) if [ -n "$2" ]; then fwd+=("--contract" "$2"); fi; shift 2 ;;
953
+ --help|-h) echo "contract-client [--doc <api document>] -> run-phase --phase client for the frontend lane"; return 0 ;;
881
954
  *) echo "contract-client: unknown arg '$1'" >&2; return 1 ;;
882
955
  esac
883
956
  done
884
- _aw_stack_py run-phase --phase client --kind frontend
957
+ _aw_stack_py run-phase --phase client --kind frontend "${fwd[@]+"${fwd[@]}"}"
885
958
  }
886
959
 
887
960
  cmd_contract_check() {
@@ -896,6 +969,21 @@ cmd_contract_check() {
896
969
  _aw_stack_py contract-check "${fwd[@]}"
897
970
  }
898
971
 
972
+ # What a change's service delta changes or removes that the frontend master plan binds to,
973
+ # and whether the change's frontend delta plan carries it. Each path is forwarded only when
974
+ # it is not empty.
975
+ cmd_contract_impact() {
976
+ local -a fwd=()
977
+ while [ $# -gt 0 ]; do
978
+ case "$1" in
979
+ --delta-backend|--frontend-plan|--delta-frontend) if [ -n "$2" ]; then fwd+=("$1" "$2"); fi; shift 2 ;;
980
+ --help|-h) echo "contract-impact --delta-backend <p> --frontend-plan <p> [--delta-frontend <p>] -> {changes[], changed_csv, affected[], addressed[], unaddressed[], unaddressed_count, unaddressed_csv, warn}"; return 0 ;;
981
+ *) echo "contract-impact: unknown arg '$1'" >&2; return 1 ;;
982
+ esac
983
+ done
984
+ _aw_stack_py contract-impact "${fwd[@]+"${fwd[@]}"}"
985
+ }
986
+
899
987
  # The gate: is the declaration USABLE for the phase about to run. --require names a
900
988
  # lane kind the phase cannot run without and is repeatable; every DECLARED lane is
901
989
  # checked for a sound lane root folder whether or not it was required, because a stack
@@ -916,13 +1004,32 @@ cmd_lane_verify() {
916
1004
  # --flag=value: --command carries a phase name that starts with '/'.
917
1005
  --command) fwd+=("--command=$2"); shift 2 ;;
918
1006
  --allow-missing-root) fwd+=("--allow-missing-root"); shift ;;
919
- --help|-h) echo "lane-verify [--require backend|frontend]… [--allow-missing-root] [--command <phase>]"; return 0 ;;
1007
+ --working-only) fwd+=("--working-only"); shift ;;
1008
+ --help|-h) echo "lane-verify [--require backend|frontend]… [--allow-missing-root] [--working-only] [--command <phase>]"; return 0 ;;
920
1009
  *) echo "lane-verify: unknown arg '$1'" >&2; return 1 ;;
921
1010
  esac
922
1011
  done
923
1012
  _aw_stack_py verify "${fwd[@]}"
924
1013
  }
925
1014
 
1015
+ # The developer's WORKING lane, in .appweaver/lane.json (not committed). No argument
1016
+ # reads it and lists the answers the declared stacks offer; --lane writes one of them;
1017
+ # --default writes the default for the declared stacks (select-stack's call).
1018
+ cmd_lane_select() {
1019
+ local -a fwd=()
1020
+ while [ $# -gt 0 ]; do
1021
+ case "$1" in
1022
+ # --flag=value: --preselect carries raw user prose, and a value that starts
1023
+ # with '-' reads as another flag when passed separately.
1024
+ --lane|--preselect) fwd+=("$1=$2"); shift 2 ;;
1025
+ --default) fwd+=("--default"); shift ;;
1026
+ --help|-h) echo "lane-select [--lane backend|frontend|both] [--default] [--preselect <text>] -> {lane, kinds[], source, note, default, select_options[], preselect, written}"; return 0 ;;
1027
+ *) echo "lane-select: unknown arg '$1'" >&2; return 1 ;;
1028
+ esac
1029
+ done
1030
+ _aw_stack_py lane-select "${fwd[@]}"
1031
+ }
1032
+
926
1033
  # Evidence that a lane of this kind exists, from the FILE signals only: a lane in
927
1034
  # stacks.json, and a stack name appearing in free prose. The authoritative signal
928
1035
  # — whether the active project's technical map carries `## /aw-spec-<command>` for the
@@ -3338,13 +3445,17 @@ cmd_test_run() {
3338
3445
  # are computed separately — the referrer closure and the features that co-own the
3339
3446
  # aggregates the change touches — and the dialect cannot concatenate two variables into
3340
3447
  # one argument, so the caller passes each list as its own --also.
3341
- local feature_dir="" scope="repo" also=""
3448
+ # --kind runs ONE declared lane's suite — the developer's working lane, so a backend
3449
+ # developer's gate is not failed by the frontend's tests, nor its fix loop sent into
3450
+ # the frontend's code. Empty means every declared lane, as before.
3451
+ local feature_dir="" scope="repo" also="" kind=""
3342
3452
  while [ $# -gt 0 ]; do
3343
3453
  case "$1" in
3344
3454
  --feature-dir) feature_dir="$2"; shift 2 ;;
3345
3455
  --scope) scope="$2"; shift 2 ;;
3346
3456
  --also) also="${also:+$also,}$2"; shift 2 ;;
3347
- --help|-h) echo "test-run [--feature-dir <path>] [--scope slice|repo] [--also <csv>]... (--also repeatable; lists union)"; return 0 ;;
3457
+ --kind) kind="$2"; shift 2 ;;
3458
+ --help|-h) echo "test-run [--feature-dir <path>] [--scope slice|repo] [--also <csv>]... [--kind backend|frontend] (--also repeatable; lists union)"; return 0 ;;
3348
3459
  *) echo "test-run: unknown arg '$1'" >&2; return 1 ;;
3349
3460
  esac
3350
3461
  done
@@ -3387,13 +3498,17 @@ cmd_test_run() {
3387
3498
  # slice run delegates and widens to the lane's whole suite, which the fence tolerates
3388
3499
  # by design — it over-includes rather than risking an under-scoped miss — and reports
3389
3500
  # scoped=false so fence_green comes from `ok` rather than from the zero-match guard.
3501
+ # A FRONTEND-only run always delegates: the .NET path below runs the service's suite,
3502
+ # which is not the lane being asked about.
3390
3503
  local use_lanes=false
3504
+ local -a kind_args=()
3505
+ if [ -n "$kind" ]; then kind_args=("--kind" "$kind"); fi
3391
3506
  if [ -n "$(_aw_lanes_declared)" ]; then
3392
- if [ "$scope" != "slice" ] || [ "$dotnet_present" = false ]; then use_lanes=true; fi
3507
+ if [ "$scope" != "slice" ] || [ "$dotnet_present" = false ] || [ "$kind" = "frontend" ]; then use_lanes=true; fi
3393
3508
  fi
3394
3509
  if [ "$use_lanes" = true ]; then
3395
3510
  rm -f "$log"
3396
- _aw_stack_py run-phase --phase test | AW_SCOPE="$scope" AW_SLICES="$slices" python3 -c '
3511
+ _aw_stack_py run-phase --phase test "${kind_args[@]}" | AW_SCOPE="$scope" AW_SLICES="$slices" python3 -c '
3397
3512
  import json, os, re, sys
3398
3513
  d = json.loads(sys.stdin.readline() or "{}")
3399
3514
  log, ok = d.get("log_tail", ""), bool(d.get("ok"))
@@ -3738,6 +3853,63 @@ cmd_cr_wave() {
3738
3853
  "$(json_escape "$dspec")" "$(json_escape "$dmodel")"
3739
3854
  }
3740
3855
 
3856
+ # ─────────────────────────────── cr-lanes ───────────────────────────────────
3857
+ # The lanes a change touches, on its delta-spec's `**Lanes**:` line. No --cr-id lists the
3858
+ # answers the declared stacks offer (both first); with --cr-id it reads that change's line,
3859
+ # and --set / --stamp write it — the way specify and plan record their answer, and the way
3860
+ # to widen or narrow an open change afterwards. The lane logic is aw-stack.py's; this
3861
+ # resolves the change to its delta-spec.
3862
+ cmd_cr_lanes() {
3863
+ local cr_id="" fd_arg="" answer="" stamp=""
3864
+ while [ $# -gt 0 ]; do
3865
+ case "$1" in
3866
+ --cr-id) cr_id="$2"; shift 2 ;;
3867
+ --feature-dir) fd_arg="$2"; shift 2 ;;
3868
+ --set) answer="$2"; shift 2 ;;
3869
+ --stamp) stamp=1; shift ;;
3870
+ --help|-h) echo "cr-lanes [--cr-id <id> [--feature-dir <path>] [--stamp] [--set both|backend|frontend]] -> {select_options[], has_options, default, recorded, lanes[], lanes_csv, backend, frontend, source, stamped}"; return 0 ;;
3871
+ *) echo "cr-lanes: unknown arg '$1'" >&2; return 1 ;;
3872
+ esac
3873
+ done
3874
+ cr_id=$(_cr_id_token "$cr_id")
3875
+ local -a fwd=()
3876
+ if [ -n "$cr_id" ]; then
3877
+ local repo_root; repo_root=$(get_repo_root)
3878
+ local fd; fd=$(_resolve_feature_dir "$fd_arg" "$repo_root") || { echo "cr-lanes: no feature set and no --feature-dir" >&2; return 1; }
3879
+ fwd+=("--delta-spec" "$fd/changes/$cr_id/delta-spec.md")
3880
+ elif [ -n "$stamp$answer" ]; then
3881
+ echo "cr-lanes: --set and --stamp need --cr-id" >&2; return 1
3882
+ fi
3883
+ [ -n "$stamp" ] && fwd+=("--stamp")
3884
+ # --flag=value: the answer may arrive as a whole option line, spaces and all.
3885
+ [ -n "$answer" ] && fwd+=("--set=$answer")
3886
+ _aw_stack_py cr-lanes "${fwd[@]+"${fwd[@]}"}"
3887
+ }
3888
+
3889
+ # ─────────────────────────── slice-gate-stamp ──────────────────────────────
3890
+ # analyze-slice-delta's result for ONE lane, written into the change's directory as
3891
+ # .aw-slice-gate.<lane>.json and committed with it. The rule lives in python/aw-stack.py;
3892
+ # this resolves the change's directory and hands over.
3893
+ cmd_slice_gate_stamp() {
3894
+ local cr_id="" fd_arg=""
3895
+ local -a fwd=()
3896
+ while [ $# -gt 0 ]; do
3897
+ case "$1" in
3898
+ --cr-id) cr_id="$2"; shift 2 ;;
3899
+ --feature-dir) fd_arg="$2"; shift 2 ;;
3900
+ # --flag=value: the findings are a JSON list, and any value may be empty.
3901
+ --lane|--tests-green|--tests-ran|--requirements-ok|--findings) fwd+=("$1=$2"); shift 2 ;;
3902
+ --help|-h) echo "slice-gate-stamp --cr-id <id> --lane backend|frontend [--feature-dir <path>] [--tests-green true|false] [--tests-ran true|false] [--requirements-ok true|false] [--findings <json list>] -> {file, lane, cr_id, verdict, tests_green, tests_ran, requirements_ok, findings, high_findings, gated}"; return 0 ;;
3903
+ *) echo "slice-gate-stamp: unknown arg '$1'" >&2; return 1 ;;
3904
+ esac
3905
+ done
3906
+ cr_id=$(_cr_id_token "$cr_id")
3907
+ [ -n "$cr_id" ] || { echo "slice-gate-stamp: --cr-id is required" >&2; return 1; }
3908
+ local repo_root; repo_root=$(get_repo_root)
3909
+ local fd; fd=$(_resolve_feature_dir "$fd_arg" "$repo_root") || { echo "slice-gate-stamp: no feature set and no --feature-dir" >&2; return 1; }
3910
+ _aw_stack_py slice-gate-stamp --cr-dir "$fd/changes/$cr_id" --cr-id "$cr_id" "${fwd[@]+"${fwd[@]}"}"
3911
+ }
3912
+
3741
3913
  # ──────────────────────────── cr-wave-derive ───────────────────────────────
3742
3914
  # THE WAVE A CHANGE'S OWN DOCUMENTS IMPLY, from their `## Depends on` sections.
3743
3915
  #
@@ -4114,15 +4286,24 @@ PY
4114
4286
  # new` resolves to it.
4115
4287
  # `count` and `has_changes` keep counting REAL open changes, so a caller that gates on
4116
4288
  # them is unaffected by either flag.
4289
+ #
4290
+ # EACH CHANGE'S LANES, from its delta-spec's `**Lanes**:` line (`lanes[]`, and
4291
+ # `lanes_recorded` false when there is no line, which means every lane). `--lane backend |
4292
+ # frontend | both` lists only the changes that touch one of those lanes; every count, id,
4293
+ # option and preselect above then covers that list alone, and `hidden_ids[]` names the open
4294
+ # changes it left out. No `--lane`, or an empty one, lists every open change. A change that
4295
+ # touches one lane only says so in its option label; the label of one touching both is
4296
+ # unchanged.
4117
4297
  cmd_cr_list() {
4118
- local feature_dir_arg="" preselect="" inc_main="" inc_new=""
4298
+ local feature_dir_arg="" preselect="" inc_main="" inc_new="" lane=""
4119
4299
  while [ $# -gt 0 ]; do
4120
4300
  case "$1" in
4121
4301
  --feature-dir) feature_dir_arg="$2"; shift 2 ;;
4122
4302
  --preselect) preselect="$2"; shift 2 ;;
4123
4303
  --include-main) inc_main="1"; shift ;;
4124
4304
  --include-new) inc_new="1"; shift ;;
4125
- --help|-h) echo "cr-list [--feature-dir <path>] [--preselect <cr-id|main|new>] [--include-main] [--include-new] -> {changes[], ids[], count, select_options[], matched_label, main_label, new_label, active_change}"; return 0 ;;
4305
+ --lane) lane="$2"; shift 2 ;;
4306
+ --help|-h) echo "cr-list [--feature-dir <path>] [--preselect <cr-id|main|new>] [--include-main] [--include-new] [--lane backend|frontend|both] -> {changes[], ids[], count, select_options[], matched_label, main_label, new_label, active_change, lane_filter[], hidden_ids[], hidden_count}"; return 0 ;;
4126
4307
  *) echo "cr-list: unknown arg '$1'" >&2; return 1 ;;
4127
4308
  esac
4128
4309
  done
@@ -4130,8 +4311,9 @@ cmd_cr_list() {
4130
4311
  local fd; fd=$(_resolve_feature_dir "$feature_dir_arg" "$repo_root") || { echo "cr-list: no feature set and no --feature-dir" >&2; return 1; }
4131
4312
  local ctx_dir; ctx_dir=$(_context_dir_for_feature "$fd" "$repo_root")
4132
4313
  AW_FD="$fd" AW_CTX="$ctx_dir" AW_ROOT="$repo_root" AW_PRE="$(_cr_id_token "$preselect")" \
4133
- AW_PRE_RAW="$preselect" AW_INC_MAIN="$inc_main" AW_INC_NEW="$inc_new" python3 - <<'PY'
4134
- import json, os, re
4314
+ AW_PRE_RAW="$preselect" AW_INC_MAIN="$inc_main" AW_INC_NEW="$inc_new" AW_LANE="$lane" \
4315
+ AW_STACK_PY="$SCRIPT_DIR/../python/aw-stack.py" python3 - <<'PY'
4316
+ import json, os, re, sys
4135
4317
  fd = os.environ["AW_FD"]; ctx = os.environ["AW_CTX"]; root = os.environ["AW_ROOT"]
4136
4318
  pre = os.environ.get("AW_PRE", "").strip()
4137
4319
  # The WHOLE argument, not its first token. A cr id is matched on the token because a
@@ -4158,6 +4340,27 @@ def title_of(path):
4158
4340
  m = re.match(r"^#\s+.*?:\s*(.+)$", first)
4159
4341
  t = m.group(1).strip() if m else ""
4160
4342
  return "" if (not t or t.startswith("[")) else t
4343
+ # The lanes a change touches: read_change_lanes in python/aw-stack.py, the one reader of a
4344
+ # delta-spec's **Lanes** line. None when the line is missing or still the template
4345
+ # placeholder — a change touching every lane.
4346
+ import importlib.util
4347
+ try:
4348
+ _spec = importlib.util.spec_from_file_location("aw_stack", os.environ["AW_STACK_PY"])
4349
+ _aw_stack = importlib.util.module_from_spec(_spec)
4350
+ _spec.loader.exec_module(_aw_stack)
4351
+ except Exception as _e:
4352
+ sys.stderr.write("cr-list: cannot load %s: %s\n" % (os.environ.get("AW_STACK_PY", ""), _e))
4353
+ sys.exit(1)
4354
+ lanes_of = _aw_stack.read_change_lanes
4355
+ want = []
4356
+ for tok in re.split(r"[\s,]+", os.environ.get("AW_LANE", "").strip().lower()):
4357
+ if not tok: continue
4358
+ if tok == "both": want += ["backend", "frontend"]
4359
+ elif tok in ("backend", "frontend"): want.append(tok)
4360
+ else:
4361
+ sys.stderr.write("cr-list: --lane takes backend, frontend or both, not '%s'\n" % tok)
4362
+ sys.exit(1)
4363
+ want = [k for k in ("backend", "frontend") if k in want]
4161
4364
  tracked = []
4162
4365
  avp = os.path.join(fd, "active_version.json")
4163
4366
  active = ""
@@ -4181,6 +4384,7 @@ if os.path.isdir(changes_dir):
4181
4384
  wave = header(dspec, "Wave") or header(dmodel, "Wave") or "1"
4182
4385
  try: wave = int(re.sub(r"\D", "", wave) or 1)
4183
4386
  except Exception: wave = 1
4387
+ lanes = lanes_of(dspec)
4184
4388
  rows.append({
4185
4389
  "cr_id": name, "cr_dir": rel(d), "wave": wave,
4186
4390
  "title": title_of(dspec),
@@ -4193,11 +4397,17 @@ if os.path.isdir(changes_dir):
4193
4397
  "legacy_delta_design": os.path.isfile(legacy),
4194
4398
  "tracked": name in tracked,
4195
4399
  "active": name == active,
4400
+ "lanes": lanes or ["backend", "frontend"],
4401
+ "lanes_recorded": lanes is not None,
4196
4402
  })
4197
4403
  rows.sort(key=lambda r: (r["wave"], r["cr_id"]))
4404
+ hidden = [r for r in rows if want and not set(r["lanes"]) & set(want)]
4405
+ hidden_set = set(r["cr_id"] for r in hidden)
4406
+ rows = [r for r in rows if r["cr_id"] not in hidden_set]
4198
4407
  def label(r):
4199
4408
  bits = [r["cr_id"], "wave %d" % r["wave"]]
4200
4409
  bits.append(r["title"] or "(delta-spec not authored yet)")
4410
+ if len(r["lanes"]) == 1: bits.append("%s only" % r["lanes"][0])
4201
4411
  planned = [k for k, v in (("backend", r["delta_backend_plan_exists"]), ("frontend", r["delta_frontend_plan_exists"])) if v]
4202
4412
  if planned: bits.append("planned: " + "+".join(planned))
4203
4413
  return " · ".join(bits)
@@ -4229,6 +4439,12 @@ print(json.dumps({
4229
4439
  # The lone open change, so a caller can take it without a prompt when there is
4230
4440
  # nothing to choose between.
4231
4441
  "only_id": rows[0]["cr_id"] if len(rows) == 1 else "",
4442
+ # What --lane left out: open changes that touch none of the lanes asked for.
4443
+ "lane_filter": want,
4444
+ "hidden_ids": [r["cr_id"] for r in hidden], "hidden_count": len(hidden),
4445
+ "hidden_ids_csv": ", ".join("%s (%s only)" % (r["cr_id"], "+".join(r["lanes"])) for r in hidden),
4446
+ # A --preselect naming one of those, so a caller can say why it was not selected.
4447
+ "preselect_hidden": pre if (pre and not matched and pre in hidden_set) else "",
4232
4448
  }))
4233
4449
  PY
4234
4450
  }
@@ -4249,7 +4465,7 @@ cmd_cr_paths() {
4249
4465
  case "$1" in
4250
4466
  --cr-id) cr_id="$2"; shift 2 ;;
4251
4467
  --feature-dir) feature_dir_arg="$2"; shift 2 ;;
4252
- --help|-h) echo "cr-paths --cr-id <id> [--feature-dir <path>] -> {cr_id, cr_dir, delta_spec, delta_model, delta_backend_plan, delta_frontend_plan, wave, predecessor_files[], predecessor_specs[], predecessor_docs[], sibling_files[], domain_model}"; return 0 ;;
4468
+ --help|-h) echo "cr-paths --cr-id <id> [--feature-dir <path>] -> {cr_id, cr_dir, delta_spec, delta_model, delta_backend_plan, delta_frontend_plan, wave, lanes[], lanes_csv, lanes_recorded, touches_backend, touches_frontend, predecessor_files[], predecessor_specs[], predecessor_docs[], sibling_files[], domain_model}"; return 0 ;;
4253
4469
  *) echo "cr-paths: unknown arg '$1'" >&2; return 1 ;;
4254
4470
  esac
4255
4471
  done
@@ -4258,8 +4474,9 @@ cmd_cr_paths() {
4258
4474
  local repo_root; repo_root=$(get_repo_root)
4259
4475
  local fd; fd=$(_resolve_feature_dir "$feature_dir_arg" "$repo_root") || { echo "cr-paths: no feature set and no --feature-dir" >&2; return 1; }
4260
4476
  local ctx_dir; ctx_dir=$(_context_dir_for_feature "$fd" "$repo_root")
4261
- AW_FD="$fd" AW_CTX="$ctx_dir" AW_ROOT="$repo_root" AW_CR="$cr_id" python3 - <<'PY'
4262
- import json, os, re, glob
4477
+ AW_FD="$fd" AW_CTX="$ctx_dir" AW_ROOT="$repo_root" AW_CR="$cr_id" \
4478
+ AW_STACK_PY="$SCRIPT_DIR/../python/aw-stack.py" python3 - <<'PY'
4479
+ import json, os, re, glob, sys
4263
4480
  fd = os.environ["AW_FD"]; ctx = os.environ["AW_CTX"]; root = os.environ["AW_ROOT"]; cr = os.environ["AW_CR"]
4264
4481
  def rel(p): return os.path.relpath(p, root)
4265
4482
  def wave_of(path, default=1):
@@ -4274,6 +4491,19 @@ be = os.path.join(cr_dir, "delta-backend-plan.md")
4274
4491
  fe = os.path.join(cr_dir, "delta-frontend-plan.md")
4275
4492
  master = os.path.join(ctx, "domain-model.md")
4276
4493
  wave = wave_of(dspec, wave_of(dmodel, 1))
4494
+ # The lanes a change touches: read_change_lanes in python/aw-stack.py, the one reader of a
4495
+ # delta-spec's **Lanes** line. None when the line is missing or still the template
4496
+ # placeholder — a change touching every lane.
4497
+ import importlib.util
4498
+ try:
4499
+ _spec = importlib.util.spec_from_file_location("aw_stack", os.environ["AW_STACK_PY"])
4500
+ _aw_stack = importlib.util.module_from_spec(_spec)
4501
+ _spec.loader.exec_module(_aw_stack)
4502
+ except Exception as _e:
4503
+ sys.stderr.write("cr-paths: cannot load %s: %s\n" % (os.environ.get("AW_STACK_PY", ""), _e))
4504
+ sys.exit(1)
4505
+ recorded = _aw_stack.read_change_lanes(dspec)
4506
+ lanes = recorded or ["backend", "frontend"]
4277
4507
  # A change's model delta sits at CONTEXT level and its delta-spec at FEATURE level, so the
4278
4508
  # spec of another open change is found by walking the context's features rather than
4279
4509
  # assuming it belongs to this one — two features of one context can both have changes open.
@@ -4302,6 +4532,10 @@ print(json.dumps({
4302
4532
  "delta_backend_plan": rel(be), "delta_backend_plan_exists": os.path.isfile(be),
4303
4533
  "delta_frontend_plan": rel(fe), "delta_frontend_plan_exists": os.path.isfile(fe),
4304
4534
  "wave": wave,
4535
+ # The lanes this change touches, from its delta-spec's **Lanes** line — every lane when
4536
+ # it records none (lanes_recorded false).
4537
+ "lanes": lanes, "lanes_csv": ",".join(lanes), "lanes_recorded": recorded is not None,
4538
+ "touches_backend": "backend" in lanes, "touches_frontend": "frontend" in lanes,
4305
4539
  "context_dir": rel(ctx),
4306
4540
  "domain_model": rel(master), "domain_model_exists": os.path.isfile(master),
4307
4541
  # Lower wave = committed to landing first, so this delta may plan on top of it.
@@ -5051,17 +5285,24 @@ _plan_gate_file() { printf '%s/.aw-plan-gate.json' "$1"; }
5051
5285
  # an untagged gap included — is a plan gap. Unreadable input is one plan gap: a verdict that
5052
5286
  # could not be read must not pass.
5053
5287
  cmd_plan_gaps() {
5054
- local gaps=""
5288
+ # --lanes is the developer's WORKING lanes (csv). A plan-route gap tagged with a lane
5289
+ # outside it belongs to the developer who owns that lane: it is listed under `other`
5290
+ # and neither drives this run's repair nor its stop. A gap routed to specify stays
5291
+ # routed whatever lane it was found in — the spec is shared. With no --lanes every
5292
+ # lane is worked, which is the behaviour from before the option existed.
5293
+ local gaps="" lanes=""
5055
5294
  while [ $# -gt 0 ]; do
5056
5295
  case "$1" in
5057
5296
  --gaps) gaps="$2"; shift 2 ;;
5058
- --help|-h) echo "plan-gaps --gaps <json list> -> {verdict, plan_ok, plan_gaps, routed_gaps, routed[], routed_csv}"; return 0 ;;
5297
+ --lanes) lanes="$2"; shift 2 ;;
5298
+ --help|-h) echo "plan-gaps --gaps <json list> [--lanes backend,frontend] -> {verdict, plan_ok, plan_gaps, routed_gaps, routed[], routed_csv, other_gaps, other[], other_csv, backend_verdict, backend_plan_gaps, backend_routed[], frontend_verdict, frontend_plan_gaps, frontend_routed[]}"; return 0 ;;
5059
5299
  *) echo "plan-gaps: unknown arg '$1'" >&2; return 1 ;;
5060
5300
  esac
5061
5301
  done
5062
- AW_GAPS="$gaps" python3 - <<'PY'
5302
+ AW_GAPS="$gaps" AW_LANES="$lanes" python3 - <<'PY'
5063
5303
  import json, os
5064
5304
  raw = (os.environ.get("AW_GAPS") or "").strip()
5305
+ lanes = [l.strip().lower() for l in (os.environ.get("AW_LANES") or "").split(",") if l.strip()]
5065
5306
  try:
5066
5307
  gaps = json.loads(raw) if raw else []
5067
5308
  if not isinstance(gaps, list):
@@ -5069,27 +5310,46 @@ try:
5069
5310
  readable = True
5070
5311
  except Exception:
5071
5312
  gaps, readable = [], False
5072
- plan, routed = 0, []
5313
+ plan, routed, other = 0, [], []
5314
+ per = {"backend": [0, []], "frontend": [0, []]}
5073
5315
  for g in gaps:
5074
5316
  d = g if isinstance(g, dict) else {"gap": str(g)}
5075
5317
  text = str(d.get("gap", "")).strip()
5318
+ lane = str(d.get("lane", "") or "").strip().lower()
5319
+ ref = str(d.get("spec_ref", "") or "").strip()
5320
+ line = ("%s: %s" % (ref, text)) if ref and ref != "—" else text
5321
+ targets = [lane] if lane in per else list(per)
5076
5322
  if text.upper().startswith("LIFT-FAILURE") or str(d.get("route", "")).strip().lower() == "specify":
5077
- ref = str(d.get("spec_ref", "") or "").strip()
5078
- routed.append(("%s: %s" % (ref, text)) if ref and ref != "—" else text)
5323
+ routed.append(line)
5324
+ for t in targets:
5325
+ per[t][1].append(line)
5326
+ elif lanes and lane in per and lane not in lanes:
5327
+ other.append("%s: %s" % (lane, line))
5079
5328
  else:
5080
5329
  plan += 1
5330
+ for t in targets:
5331
+ per[t][0] += 1
5081
5332
  if not readable:
5082
5333
  plan += 1
5083
- verdict = "gaps" if plan else ("routed-gaps" if routed else "conformant")
5084
- print(json.dumps({"ok": True, "readable": readable, "verdict": verdict, "plan_ok": plan == 0,
5085
- "plan_gaps": plan, "routed_gaps": len(routed), "routed": routed,
5086
- "routed_csv": "; ".join(routed)}))
5334
+ for t in per:
5335
+ per[t][0] += 1
5336
+ def verdict(p, r):
5337
+ return "gaps" if p else ("routed-gaps" if r else "conformant")
5338
+ out = {"ok": True, "readable": readable, "verdict": verdict(plan, routed), "plan_ok": plan == 0,
5339
+ "plan_gaps": plan, "routed_gaps": len(routed), "routed": routed,
5340
+ "routed_csv": "; ".join(routed), "lanes": lanes,
5341
+ "other_gaps": len(other), "other": other, "other_csv": "; ".join(other)}
5342
+ for k in ("backend", "frontend"):
5343
+ out[k + "_verdict"] = verdict(per[k][0], per[k][1])
5344
+ out[k + "_plan_gaps"] = per[k][0]
5345
+ out[k + "_routed"] = per[k][1]
5346
+ print(json.dumps(out))
5087
5347
  PY
5088
5348
  }
5089
5349
 
5090
5350
  cmd_plan_gate_stamp() {
5091
5351
  case "${1:-}" in
5092
- --help|-h) echo "plan-gate-stamp [--feature-dir <path>] [--spec-version <v>] [--mode <text>] [--verdict conformant|routed-gaps|gaps] [--plan-gaps <n>] [--routed <json list>] -> {file, gated, verdict, hashed[], count}"; return 0 ;;
5352
+ --help|-h) echo "plan-gate-stamp [--feature-dir <path>] [--lane backend|frontend] [--spec-version <v>] [--mode <text>] [--verdict conformant|routed-gaps|gaps] [--plan-gaps <n>] [--routed <json list>] -> {file, lane, gated, verdict, hashed[], count}"; return 0 ;;
5093
5353
  esac
5094
5354
  local feature_dir_arg="" rest=()
5095
5355
  while [ $# -gt 0 ]; do
@@ -5108,11 +5368,12 @@ cmd_plan_gate_stamp() {
5108
5368
  }
5109
5369
 
5110
5370
  cmd_plan_gate_check() {
5111
- local feature_dir_arg=""
5371
+ local feature_dir_arg="" lane=""
5112
5372
  while [ $# -gt 0 ]; do
5113
5373
  case "$1" in
5114
5374
  --feature-dir) feature_dir_arg="$2"; shift 2 ;;
5115
- --help|-h) echo "plan-gate-check [--feature-dir <path>] -> {tracked, gate_fresh, verdict, may_generate, plan_gaps, routed_gaps[], routed_csv, spec_moved, model_moved, plans_changed[], plans_changed_csv, gated, reason}"; return 0 ;;
5375
+ --lane) lane="$2"; shift 2 ;;
5376
+ --help|-h) echo "plan-gate-check [--feature-dir <path>] [--lane backend|frontend] -> {lane, shared, tracked, gate_fresh, verdict, may_generate, plan_gaps, routed_gaps[], routed_csv, spec_moved, model_moved, plans_changed[], plans_changed_csv, gated, reason}"; return 0 ;;
5116
5377
  *) echo "plan-gate-check: unknown arg '$1'" >&2; return 1 ;;
5117
5378
  esac
5118
5379
  done
@@ -5122,7 +5383,9 @@ cmd_plan_gate_check() {
5122
5383
  local core="$SCRIPT_DIR/../python/aw-plangate.py"
5123
5384
  if [ ! -f "$core" ]; then echo "plan-gate-check: core not found at $core" >&2; return 1; fi
5124
5385
  if ! command -v python3 >/dev/null 2>&1; then echo "plan-gate-check: python3 is required" >&2; return 1; fi
5125
- python3 "$core" check --feature-dir "$fd" --context-dir "$ctx_dir" --root "$repo_root"
5386
+ local -a lane_args=()
5387
+ if [ -n "$lane" ]; then lane_args=("--lane" "$lane"); fi
5388
+ python3 "$core" check --feature-dir "$fd" --context-dir "$ctx_dir" --root "$repo_root" "${lane_args[@]+"${lane_args[@]}"}"
5126
5389
  }
5127
5390
 
5128
5391
  # ─────────────────────────────── gap-route ─────────────────────────────────
@@ -5280,9 +5543,14 @@ case "$sub" in
5280
5543
  lane-verify) cmd_lane_verify "$@" ;;
5281
5544
  lane-probe) cmd_lane_probe "$@" ;;
5282
5545
  lane-set) cmd_lane_set "$@" ;;
5546
+ lane-select) cmd_lane_select "$@" ;;
5283
5547
  contract-export) cmd_contract_export "$@" ;;
5548
+ contract-draft) cmd_contract_draft "$@" ;;
5549
+ contract-combine) cmd_contract_combine "$@" ;;
5550
+ contract-requests) cmd_contract_requests "$@" ;;
5284
5551
  contract-client) cmd_contract_client "$@" ;;
5285
5552
  contract-check) cmd_contract_check "$@" ;;
5553
+ contract-impact) cmd_contract_impact "$@" ;;
5286
5554
  list-contexts) cmd_list_contexts "$@" ;;
5287
5555
  set-context) cmd_set_context "$@" ;;
5288
5556
  list-features) cmd_list_features "$@" ;;
@@ -5309,6 +5577,8 @@ case "$sub" in
5309
5577
  cr-overlap) cmd_cr_overlap "$@" ;;
5310
5578
  cr-activate) cmd_cr_activate "$@" ;;
5311
5579
  cr-wave) cmd_cr_wave "$@" ;;
5580
+ cr-lanes) cmd_cr_lanes "$@" ;;
5581
+ slice-gate-stamp) cmd_slice_gate_stamp "$@" ;;
5312
5582
  cr-wave-derive) cmd_cr_wave_derive "$@" ;;
5313
5583
  cr-rebase) cmd_cr_rebase "$@" ;;
5314
5584
  cr-archive) cmd_cr_archive "$@" ;;