aw-spec-cli 0.1.29 → 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
@@ -654,8 +654,8 @@ function Invoke-LaneSet {
654
654
  Invoke-AwStackCore -Op 'set' -Rest $fwd
655
655
  }
656
656
 
657
- # ─────────────── contract-export / contract-client / contract-check ───────────────
658
- # Twins of the bash cmd_contract_export / cmd_contract_client / cmd_contract_check.
657
+ # ─────────────── contract-export / -draft / -combine / -requests / -client / -check ───────────────
658
+ # Twins of the bash cmd_contract_export / _draft / _combine / _requests / _client / _check.
659
659
  function Invoke-ContractExport {
660
660
  param([string[]]$Rest)
661
661
  $fwd = @()
@@ -668,10 +668,48 @@ function Invoke-ContractExport {
668
668
  Invoke-AwStackCore -Op 'contract-export' -Rest $fwd
669
669
  }
670
670
 
671
+ function Invoke-ContractDraft {
672
+ param([string[]]$Rest)
673
+ $fwd = @()
674
+ for ($i = 0; $i -lt $Rest.Count; $i++) {
675
+ switch ($Rest[$i]) {
676
+ '--feature-dir' { $v = $Rest[++$i]; if ($v) { $fwd += '--feature-dir'; $fwd += $v } }
677
+ default { Write-Error "contract-draft: unknown arg '$($Rest[$i])'"; exit 1 }
678
+ }
679
+ }
680
+ Invoke-AwStackCore -Op 'contract-draft' -Rest $fwd
681
+ }
682
+
683
+ function Invoke-ContractCombine {
684
+ param([string[]]$Rest)
685
+ if ($Rest.Count -gt 0) { Write-Error "contract-combine: unknown arg '$($Rest[0])'"; exit 1 }
686
+ Invoke-AwStackCore -Op 'contract-combine' -Rest @()
687
+ }
688
+
689
+ function Invoke-ContractRequests {
690
+ param([string[]]$Rest)
691
+ $fwd = @()
692
+ for ($i = 0; $i -lt $Rest.Count; $i++) {
693
+ switch ($Rest[$i]) {
694
+ '--feature-dir' { $v = $Rest[++$i]; if ($v) { $fwd += '--feature-dir'; $fwd += $v } }
695
+ default { Write-Error "contract-requests: unknown arg '$($Rest[$i])'"; exit 1 }
696
+ }
697
+ }
698
+ Invoke-AwStackCore -Op 'contract-requests' -Rest $fwd
699
+ }
700
+
701
+ # --doc names the document `{{contract}}` stands for in the lane's `client` command; an
702
+ # empty one is dropped. Twin of bash cmd_contract_client.
671
703
  function Invoke-ContractClient {
672
704
  param([string[]]$Rest)
673
- if ($Rest.Count -gt 0) { Write-Error "contract-client: unknown arg '$($Rest[0])'"; exit 1 }
674
- Invoke-AwStackCore -Op 'run-phase' -Rest @('--phase', 'client', '--kind', 'frontend')
705
+ $fwd = @('--phase', 'client', '--kind', 'frontend')
706
+ for ($i = 0; $i -lt $Rest.Count; $i++) {
707
+ switch ($Rest[$i]) {
708
+ '--doc' { $v = $Rest[++$i]; if ($v) { $fwd += '--contract'; $fwd += $v } }
709
+ default { Write-Error "contract-client: unknown arg '$($Rest[$i])'"; exit 1 }
710
+ }
711
+ }
712
+ Invoke-AwStackCore -Op 'run-phase' -Rest $fwd
675
713
  }
676
714
 
677
715
  function Invoke-ContractCheck {
@@ -686,6 +724,26 @@ function Invoke-ContractCheck {
686
724
  Invoke-AwStackCore -Op 'contract-check' -Rest $fwd
687
725
  }
688
726
 
727
+ # cmd_contract_impact: what a change's service delta changes or removes that the frontend
728
+ # master plan binds to. Each path is forwarded only when it is not empty — PowerShell 5.1
729
+ # drops an empty value, and the flag would then take the next one.
730
+ function Invoke-ContractImpact {
731
+ param([string[]]$Rest)
732
+ $fwd = @()
733
+ for ($i = 0; $i -lt $Rest.Count; $i++) {
734
+ switch ($Rest[$i]) {
735
+ { $_ -in '--delta-backend','--frontend-plan','--delta-frontend' } {
736
+ $flag = $Rest[$i]
737
+ if ($i + 1 -lt $Rest.Count -and -not "$($Rest[$i + 1])".StartsWith('--')) {
738
+ $v = $Rest[++$i]; if ($v) { $fwd += $flag; $fwd += $v }
739
+ }
740
+ }
741
+ default { Write-Error "contract-impact: unknown arg '$($Rest[$i])'"; exit 1 }
742
+ }
743
+ }
744
+ Invoke-AwStackCore -Op 'contract-impact' -Rest $fwd
745
+ }
746
+
689
747
  # The gate: is the declaration USABLE for the phase about to run. Twin of bash
690
748
  # cmd_lane_verify. --require names a lane kind the phase cannot run without;
691
749
  # --allow-missing-root is the greenfield state, where a lane root folder is declared
@@ -705,12 +763,31 @@ function Invoke-LaneVerify {
705
763
  # --flag=value: --command carries a phase name that starts with '/'.
706
764
  '--command' { $fwd += ('--command=' + $Rest[++$i]) }
707
765
  '--allow-missing-root' { $fwd += '--allow-missing-root' }
766
+ '--working-only' { $fwd += '--working-only' }
708
767
  default { Write-Error "lane-verify: unknown arg '$($Rest[$i])'"; exit 1 }
709
768
  }
710
769
  }
711
770
  Invoke-AwStackCore -Op 'verify' -Rest $fwd
712
771
  }
713
772
 
773
+ # The developer's WORKING lane, in .appweaver/lane.json (not committed). Twin of bash
774
+ # cmd_lane_select: no argument reads it and lists the answers the declared stacks offer;
775
+ # --lane writes one of them; --default writes the default for the declared stacks.
776
+ function Invoke-LaneSelect {
777
+ param([string[]]$Rest)
778
+ $fwd = @()
779
+ for ($i = 0; $i -lt $Rest.Count; $i++) {
780
+ switch ($Rest[$i]) {
781
+ # --flag=value: --preselect carries raw user prose, and a value that starts
782
+ # with '-' reads as another flag when passed separately.
783
+ { $_ -in '--lane','--preselect' } { $fwd += ($Rest[$i] + '=' + $Rest[++$i]) }
784
+ '--default' { $fwd += '--default' }
785
+ default { Write-Error "lane-select: unknown arg '$($Rest[$i])'"; exit 1 }
786
+ }
787
+ }
788
+ Invoke-AwStackCore -Op 'lane-select' -Rest $fwd
789
+ }
790
+
714
791
  # Evidence that a lane of this kind exists, from the FILE signals only: a lane in
715
792
  # stacks.json, and a stack name appearing in free prose. The authoritative signal
716
793
  # — whether the active project's technical map carries `## /aw-spec-<command>` for the
@@ -2702,11 +2779,14 @@ function Invoke-TestRun {
2702
2779
  # feature's aggregate name (feature-dir basename) plus any --also referrer
2703
2780
  # names — the per-feature integrity scope. .NET only; npm/none ignore it
2704
2781
  # and run the full suite. (Kept byte-for-byte in step with the bash cmd_test_run.)
2705
- $featureDir = ''; $scope = 'repo'; $also = ''
2782
+ # --kind runs ONE declared lane's suite — the developer's working lane. Empty means
2783
+ # every declared lane, as before. (Twin of the bash cmd_test_run.)
2784
+ $featureDir = ''; $scope = 'repo'; $also = ''; $kind = ''
2706
2785
  for ($i = 0; $i -lt $Rest.Count; $i++) {
2707
2786
  switch ($Rest[$i]) {
2708
2787
  { $_ -in '--feature-dir','-FeatureDir' } { $featureDir = $Rest[++$i] }
2709
2788
  { $_ -in '--scope','-Scope' } { $scope = $Rest[++$i] }
2789
+ { $_ -in '--kind','-Kind' } { $kind = $Rest[++$i] }
2710
2790
  # Repeatable, lists UNION — see the bash twin for why the caller passes two.
2711
2791
  { $_ -in '--also','-Also' } { $v = $Rest[++$i]; $also = if ($also) { "$also,$v" } else { $v } }
2712
2792
  }
@@ -2747,16 +2827,20 @@ function Invoke-TestRun {
2747
2827
  # VSTest's --filter is the only per-test selector any shipped profile has. Elsewhere a
2748
2828
  # slice run delegates and widens to the lane's whole suite, which the fence tolerates
2749
2829
  # by design, and reports scoped=false so fence_green comes from `ok`.
2830
+ # A FRONTEND-only run always delegates: the .NET path below runs the service's suite,
2831
+ # which is not the lane being asked about.
2750
2832
  # (Kept in step with the bash cmd_test_run.)
2751
2833
  $laneCount = Get-LaneField -Field 'lane_count'
2752
2834
  $useLanes = $false
2753
2835
  if ($laneCount -and $laneCount -ne '0') {
2754
- if ($scope -ne 'slice' -or -not $dotnetPresent) { $useLanes = $true }
2836
+ if ($scope -ne 'slice' -or -not $dotnetPresent -or $kind -eq 'frontend') { $useLanes = $true }
2755
2837
  }
2756
2838
  if ($useLanes) {
2757
2839
  $py = Get-Py
2758
2840
  $core = Join-Path $PSScriptRoot '..' 'python' 'aw-stack.py'
2759
- $line = (& $py $core '--repo' $repo 'run-phase' '--phase' 'test' | Select-Object -First 1)
2841
+ $kindArgs = @()
2842
+ if ($kind) { $kindArgs = @('--kind', $kind) }
2843
+ $line = (& $py $core '--repo' $repo 'run-phase' '--phase' 'test' @kindArgs | Select-Object -First 1)
2760
2844
  $d = if ($line) { $line | ConvertFrom-Json } else { $null }
2761
2845
  $dLog = if ($d -and $d.log_tail) { [string]$d.log_tail } else { '' }
2762
2846
  $dOk = [bool]($d -and $d.ok)
@@ -3137,9 +3221,35 @@ function Invoke-ModelScaffold {
3137
3221
  # new` resolves to it.
3138
3222
  # `count` and `has_changes` keep counting REAL open changes, so a caller that gates on
3139
3223
  # them is unaffected by either flag.
3224
+ #
3225
+ # EACH CHANGE'S LANES, from its delta-spec's `**Lanes**:` line (`lanes[]`, and
3226
+ # `lanes_recorded` false when there is no line, which means every lane). `--lane backend |
3227
+ # frontend | both` lists only the changes that touch one of those lanes; every count, id,
3228
+ # option and preselect above then covers that list alone, and `hidden_ids[]` names the open
3229
+ # changes it left out. No `--lane`, or an empty one, lists every open change. A change that
3230
+ # touches one lane only says so in its option label; the label of one touching both is
3231
+ # unchanged. Twin of bash cmd_cr_list.
3232
+ #
3233
+ # The lanes a change touches, from its delta-spec's **Lanes** line: the kinds it names, or
3234
+ # nothing when the line is missing or still the template placeholder — a change touching
3235
+ # every lane. Callers wrap the call in @() so one kind stays an array. Twin of
3236
+ # read_change_lanes in python/aw-stack.py, which bash cmd_cr_list / cmd_cr_paths load.
3237
+ function Get-ChangeLanes {
3238
+ param([string]$Path)
3239
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return }
3240
+ $t = Get-Content -LiteralPath $Path -Raw -ErrorAction SilentlyContinue
3241
+ if (-not $t) { return }
3242
+ $m = [regex]::Match($t, '(?m)^\*\*Lanes\*\*:[ \t]*(.*?)[ \t]*(?:<!--.*)?$')
3243
+ $v = ''
3244
+ if ($m.Success) { $v = $m.Groups[1].Value.Trim().ToLower() }
3245
+ if (-not $v -or $v.StartsWith('[')) { return }
3246
+ if ($v -match '\bboth\b') { return @('backend', 'frontend') }
3247
+ foreach ($k in @('backend', 'frontend')) { if ($v -match "\b$k\b") { $k } }
3248
+ }
3249
+
3140
3250
  function Invoke-CrList {
3141
3251
  param([string[]]$Rest)
3142
- $fdArg = ''; $pre = ''; $incMain = $false; $incNew = $false
3252
+ $fdArg = ''; $pre = ''; $incMain = $false; $incNew = $false; $laneArg = ''
3143
3253
  $mainLabel = "main · the feature's own spec — re-plan the master (not a change)"
3144
3254
  $newLabel = 'new · open a plan-only change — a refactor or architectural decision, no spec delta'
3145
3255
  for ($i = 0; $i -lt $Rest.Count; $i++) {
@@ -3148,8 +3258,17 @@ function Invoke-CrList {
3148
3258
  { $_ -in '--preselect','-Preselect' } { $pre = $Rest[++$i] }
3149
3259
  { $_ -in '--include-main','-IncludeMain' } { $incMain = $true }
3150
3260
  { $_ -in '--include-new','-IncludeNew' } { $incNew = $true }
3261
+ # PowerShell 5.1 drops an empty value, so the next flag is never taken as one.
3262
+ { $_ -in '--lane','-Lane' } { if ($i + 1 -lt $Rest.Count -and -not "$($Rest[$i + 1])".StartsWith('-')) { $laneArg = $Rest[++$i] } }
3151
3263
  }
3152
3264
  }
3265
+ $want = @()
3266
+ foreach ($tok in @([regex]::Split("$laneArg".Trim().ToLower(), '[\s,]+') | Where-Object { $_ })) {
3267
+ if ($tok -eq 'both') { $want += 'backend'; $want += 'frontend' }
3268
+ elseif ($tok -in @('backend', 'frontend')) { $want += $tok }
3269
+ else { Write-Error "cr-list: --lane takes backend, frontend or both, not '$tok'"; exit 1 }
3270
+ }
3271
+ $want = @(@('backend', 'frontend') | Where-Object { $want -contains $_ })
3153
3272
  # The WHOLE argument, kept alongside the token. A cr id is matched on the token
3154
3273
  # because a selection label starts with one; the synthetic words are matched on the
3155
3274
  # whole string so that guidance prose like "new caching layer" — which plan passes
@@ -3188,6 +3307,10 @@ function Invoke-CrList {
3188
3307
  $first = (Get-Content $dspec -TotalCount 1 -ErrorAction SilentlyContinue)
3189
3308
  if ("$first" -match '^#\s+.*?:\s*(.+)$') { $t = $Matches[1].Trim(); if ($t -and -not $t.StartsWith('[')) { $title = $t } }
3190
3309
  }
3310
+ $recorded = @(Get-ChangeLanes -Path $dspec)
3311
+ # Assigned, not $( )-wrapped: a subexpression unrolls a one-kind array to a string.
3312
+ $rowLanes = @('backend', 'frontend')
3313
+ if ($recorded.Count) { $rowLanes = @($recorded) }
3191
3314
  $rows += [ordered]@{
3192
3315
  cr_id = $d.Name; cr_dir = (ConvertTo-RepoRelative -Path $d.FullName -RepoRoot $repo); wave = $wave
3193
3316
  title = $title
@@ -3198,13 +3321,19 @@ function Invoke-CrList {
3198
3321
  legacy_delta_design = (Test-Path $legacy)
3199
3322
  tracked = ($tracked -contains $d.Name)
3200
3323
  active = ($d.Name -eq $active)
3324
+ lanes = $rowLanes
3325
+ lanes_recorded = ($recorded.Count -gt 0)
3201
3326
  }
3202
3327
  }
3203
3328
  }
3204
3329
  $rows = @($rows | Sort-Object @{ Expression = { $_.wave } }, @{ Expression = { $_.cr_id } })
3330
+ $hidden = @($rows | Where-Object { $want.Count -and -not @($_.lanes | Where-Object { $want -contains $_ }).Count })
3331
+ $hiddenIds = @($hidden | ForEach-Object { $_.cr_id })
3332
+ $rows = @($rows | Where-Object { $hiddenIds -notcontains $_.cr_id })
3205
3333
  $crOpts = @(); foreach ($r in $rows) {
3206
3334
  $bits = @($r.cr_id, ("wave " + $r.wave))
3207
3335
  if ($r.title) { $bits += $r.title } else { $bits += '(delta-spec not authored yet)' }
3336
+ if (@($r.lanes).Count -eq 1) { $bits += ('{0} only' -f @($r.lanes)[0]) }
3208
3337
  $planned = @(); if ($r.delta_backend_plan_exists) { $planned += 'backend' }; if ($r.delta_frontend_plan_exists) { $planned += 'frontend' }
3209
3338
  if ($planned.Count) { $bits += ('planned: ' + ($planned -join '+')) }
3210
3339
  $crOpts += ($bits -join ' · ')
@@ -3241,6 +3370,12 @@ function Invoke-CrList {
3241
3370
  active_change = $active; tracked = @($tracked)
3242
3371
  has_changes = ($rows.Count -gt 0)
3243
3372
  only_id = $(if ($rows.Count -eq 1) { $rows[0].cr_id } else { '' })
3373
+ # What --lane left out: open changes that touch none of the lanes asked for.
3374
+ lane_filter = @($want)
3375
+ hidden_ids = @($hiddenIds); hidden_count = $hidden.Count
3376
+ hidden_ids_csv = (@($hidden | ForEach-Object { '{0} ({1} only)' -f $_.cr_id, (@($_.lanes) -join '+') }) -join ', ')
3377
+ # A --preselect naming one of those, so a caller can say why it was not selected.
3378
+ preselect_hidden = $(if ($pre -and -not $matched -and $hiddenIds -contains $pre) { $pre } else { '' })
3244
3379
  })
3245
3380
  }
3246
3381
 
@@ -3271,6 +3406,9 @@ function Invoke-CrPaths {
3271
3406
  $fe = Join-Path $crDir 'delta-frontend-plan.md'
3272
3407
  $master = Join-Path $ctx 'domain-model.md'
3273
3408
  $wave = [int](Get-WaveOf -FeatureDir $fd -CrId $crId -Repo $repo)
3409
+ $recorded = @(Get-ChangeLanes -Path $dspec)
3410
+ $lanes = @('backend', 'frontend')
3411
+ if ($recorded.Count) { $lanes = @($recorded) }
3274
3412
  $pred = @(); $sib = @(); $predRows = @(); $sibRows = @(); $predSpecs = @()
3275
3413
  foreach ($f in @(Get-ChildItem -LiteralPath $ctx -Filter 'domain-model.*.delta.md' -File -ErrorAction SilentlyContinue | Sort-Object Name)) {
3276
3414
  $other = $f.Name.Substring('domain-model.'.Length)
@@ -3308,6 +3446,10 @@ function Invoke-CrPaths {
3308
3446
  delta_backend_plan = (ConvertTo-RepoRelative -Path $be -RepoRoot $repo); delta_backend_plan_exists = (Test-Path $be)
3309
3447
  delta_frontend_plan = (ConvertTo-RepoRelative -Path $fe -RepoRoot $repo); delta_frontend_plan_exists = (Test-Path $fe)
3310
3448
  wave = $wave
3449
+ # The lanes this change touches, from its delta-spec's **Lanes** line — every lane
3450
+ # when it records none (lanes_recorded false).
3451
+ lanes = @($lanes); lanes_csv = (@($lanes) -join ','); lanes_recorded = ($recorded.Count -gt 0)
3452
+ touches_backend = (@($lanes) -contains 'backend'); touches_frontend = (@($lanes) -contains 'frontend')
3311
3453
  context_dir = (ConvertTo-RepoRelative -Path $ctx -RepoRoot $repo)
3312
3454
  domain_model = (ConvertTo-RepoRelative -Path $master -RepoRoot $repo); domain_model_exists = (Test-Path $master)
3313
3455
  # Lower wave = committed to landing first, so this delta may plan on top of it.
@@ -3497,6 +3639,62 @@ function Invoke-CrWave {
3497
3639
  Write-Json ([ordered]@{ cr_id = $crId; wave = [int]$wave; stamped = $stamped; delta_spec = $dspec; delta_model = $dmodel })
3498
3640
  }
3499
3641
 
3642
+ # cmd_cr_lanes: the lanes a change touches, on its delta-spec's **Lanes** line. No --cr-id
3643
+ # lists the answers; with it, reads the line, and --set / --stamp write it. An empty value
3644
+ # is dropped by PowerShell 5.1 before it gets here, so --set goes last in a caller's args
3645
+ # and an answer is forwarded only when there is one.
3646
+ function Invoke-CrLanes {
3647
+ param([string[]]$Rest)
3648
+ $crId = ''; $fdArg = ''; $answer = ''; $stamp = $false
3649
+ for ($i = 0; $i -lt $Rest.Count; $i++) {
3650
+ switch ($Rest[$i]) {
3651
+ '--cr-id' { $crId = $Rest[++$i] }
3652
+ '--feature-dir' { $fdArg = $Rest[++$i] }
3653
+ '--set' { $answer = $Rest[++$i] }
3654
+ '--stamp' { $stamp = $true }
3655
+ default { Write-Error "cr-lanes: unknown arg '$($Rest[$i])'"; exit 1 }
3656
+ }
3657
+ }
3658
+ $crId = Get-CrIdToken -Raw $crId
3659
+ $fwd = @()
3660
+ if ($crId) {
3661
+ $repo = Get-RepoRoot; $fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
3662
+ if (-not $fd) { Write-Error 'cr-lanes: no feature set and no --feature-dir'; exit 1 }
3663
+ $fwd += '--delta-spec'; $fwd += (Join-Path $fd "changes/$crId/delta-spec.md")
3664
+ } elseif ($stamp -or $answer) {
3665
+ Write-Error 'cr-lanes: --set and --stamp need --cr-id'; exit 1
3666
+ }
3667
+ if ($stamp) { $fwd += '--stamp' }
3668
+ if ($answer) { $fwd += ('--set=' + $answer) }
3669
+ Invoke-AwStackCore -Op 'cr-lanes' -Rest $fwd
3670
+ }
3671
+
3672
+ # analyze-slice-delta's result for ONE lane, written into the change's directory as
3673
+ # .aw-slice-gate.<lane>.json. Twin of bash cmd_slice_gate_stamp; the rule is in aw-stack.py.
3674
+ function Invoke-SliceGateStamp {
3675
+ param([string[]]$Rest)
3676
+ $crId = ''; $fdArg = ''; $fwd = @()
3677
+ for ($i = 0; $i -lt $Rest.Count; $i++) {
3678
+ switch ($Rest[$i]) {
3679
+ '--cr-id' { $crId = $Rest[++$i] }
3680
+ '--feature-dir' { $fdArg = $Rest[++$i] }
3681
+ # --flag=value: the findings are a JSON list. PowerShell 5.1 drops an empty
3682
+ # argument, so a value that would be the next flag is read as empty instead.
3683
+ { $_ -in '--lane','--tests-green','--tests-ran','--requirements-ok','--findings' } {
3684
+ $flag = $Rest[$i]; $v = ''
3685
+ if ($i + 1 -lt $Rest.Count -and -not "$($Rest[$i + 1])".StartsWith('--')) { $v = $Rest[++$i] }
3686
+ $fwd += ($flag + '=' + $v)
3687
+ }
3688
+ default { Write-Error "slice-gate-stamp: unknown arg '$($Rest[$i])'"; exit 1 }
3689
+ }
3690
+ }
3691
+ $crId = Get-CrIdToken -Raw $crId
3692
+ if (-not $crId) { Write-Error 'slice-gate-stamp: --cr-id is required'; exit 1 }
3693
+ $repo = Get-RepoRoot; $fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
3694
+ if (-not $fd) { Write-Error 'slice-gate-stamp: no feature set and no --feature-dir'; exit 1 }
3695
+ Invoke-AwStackCore -Op 'slice-gate-stamp' -Rest (@('--cr-dir', (Join-Path $fd "changes/$crId"), '--cr-id', $crId) + $fwd)
3696
+ }
3697
+
3500
3698
  $PyCrWaveDerive = @'
3501
3699
  import glob, json, os, re
3502
3700
 
@@ -4443,11 +4641,16 @@ function Invoke-CrRecallCheck {
4443
4641
  # an untagged gap included — is a plan gap. Unreadable input is one plan gap: a verdict that
4444
4642
  # could not be read must not pass.
4445
4643
  function Invoke-PlanGaps {
4644
+ # --lanes is the developer's WORKING lanes (csv) — see the bash twin cmd_plan_gaps.
4446
4645
  param([string[]]$Rest)
4447
- $raw = ''
4646
+ $raw = ''; $lanesArg = ''
4448
4647
  for ($i = 0; $i -lt $Rest.Count; $i++) {
4449
- switch ($Rest[$i]) { { $_ -in '--gaps','-Gaps' } { $raw = $Rest[++$i] } }
4648
+ switch ($Rest[$i]) {
4649
+ { $_ -in '--gaps','-Gaps' } { $raw = $Rest[++$i] }
4650
+ { $_ -in '--lanes','-Lanes' } { $lanesArg = $Rest[++$i] }
4651
+ }
4450
4652
  }
4653
+ $lanes = @("$lanesArg".Split(',') | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ })
4451
4654
  $gaps = @(); $readable = $true
4452
4655
  if ("$raw".Trim()) {
4453
4656
  try {
@@ -4455,25 +4658,39 @@ function Invoke-PlanGaps {
4455
4658
  if ($parsed -is [array]) { $gaps = @($parsed) } else { $readable = $false }
4456
4659
  } catch { $readable = $false }
4457
4660
  }
4458
- $plan = 0; $routed = @()
4661
+ $plan = 0; $routed = @(); $other = @()
4662
+ $perPlan = @{ backend = 0; frontend = 0 }
4663
+ $perRouted = @{ backend = @(); frontend = @() }
4459
4664
  foreach ($g in $gaps) {
4460
4665
  if ($g -is [System.Management.Automation.PSCustomObject]) {
4461
4666
  $text = "$($g.gap)".Trim(); $route = "$($g.route)".Trim().ToLower(); $ref = "$($g.spec_ref)".Trim()
4667
+ $lane = "$($g.lane)".Trim().ToLower()
4462
4668
  } else {
4463
- $text = "$g".Trim(); $route = ''; $ref = ''
4669
+ $text = "$g".Trim(); $route = ''; $ref = ''; $lane = ''
4464
4670
  }
4671
+ $line = $(if ($ref -and $ref -ne '—') { '{0}: {1}' -f $ref, $text } else { $text })
4672
+ $targets = $(if ($lane -in 'backend','frontend') { @($lane) } else { @('backend','frontend') })
4465
4673
  if ($text.ToUpper().StartsWith('LIFT-FAILURE') -or $route -eq 'specify') {
4466
- $routed += $(if ($ref -and $ref -ne '—') { '{0}: {1}' -f $ref, $text } else { $text })
4674
+ $routed += $line
4675
+ foreach ($t in $targets) { $perRouted[$t] += $line }
4676
+ } elseif ($lanes.Count -gt 0 -and $lane -in 'backend','frontend' -and $lanes -notcontains $lane) {
4677
+ $other += ('{0}: {1}' -f $lane, $line)
4467
4678
  } else {
4468
4679
  $plan++
4680
+ foreach ($t in $targets) { $perPlan[$t]++ }
4469
4681
  }
4470
4682
  }
4471
- if (-not $readable) { $plan++ }
4472
- $verdict = if ($plan -gt 0) { 'gaps' } elseif ($routed.Count -gt 0) { 'routed-gaps' } else { 'conformant' }
4683
+ if (-not $readable) { $plan++; $perPlan['backend']++; $perPlan['frontend']++ }
4684
+ function Get-GapVerdict([int]$p, [int]$r) { if ($p -gt 0) { 'gaps' } elseif ($r -gt 0) { 'routed-gaps' } else { 'conformant' } }
4473
4685
  Write-Json ([ordered]@{
4474
- ok = $true; readable = $readable; verdict = $verdict; plan_ok = ($plan -eq 0)
4686
+ ok = $true; readable = $readable; verdict = (Get-GapVerdict $plan $routed.Count); plan_ok = ($plan -eq 0)
4475
4687
  plan_gaps = $plan; routed_gaps = $routed.Count; routed = @($routed)
4476
- routed_csv = ($routed -join '; ')
4688
+ routed_csv = ($routed -join '; '); lanes = @($lanes)
4689
+ other_gaps = $other.Count; other = @($other); other_csv = ($other -join '; ')
4690
+ backend_verdict = (Get-GapVerdict $perPlan['backend'] $perRouted['backend'].Count)
4691
+ backend_plan_gaps = $perPlan['backend']; backend_routed = @($perRouted['backend'])
4692
+ frontend_verdict = (Get-GapVerdict $perPlan['frontend'] $perRouted['frontend'].Count)
4693
+ frontend_plan_gaps = $perPlan['frontend']; frontend_routed = @($perRouted['frontend'])
4477
4694
  })
4478
4695
  }
4479
4696
 
@@ -4509,9 +4726,12 @@ function Invoke-PlanGateStamp {
4509
4726
 
4510
4727
  function Invoke-PlanGateCheck {
4511
4728
  param([string[]]$Rest)
4512
- $fdArg = ''
4729
+ $fdArg = ''; $lane = ''
4513
4730
  for ($i = 0; $i -lt $Rest.Count; $i++) {
4514
- switch ($Rest[$i]) { { $_ -in '--feature-dir','-FeatureDir' } { $fdArg = $Rest[++$i] } }
4731
+ switch ($Rest[$i]) {
4732
+ { $_ -in '--feature-dir','-FeatureDir' } { $fdArg = $Rest[++$i] }
4733
+ { $_ -in '--lane','-Lane' } { $lane = $Rest[++$i] }
4734
+ }
4515
4735
  }
4516
4736
  $repo = Get-RepoRoot; $fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
4517
4737
  if (-not $fd) { Write-Error 'plan-gate-check: no feature set and no --feature-dir'; exit 1 }
@@ -4520,7 +4740,9 @@ function Invoke-PlanGateCheck {
4520
4740
  if (-not $py) { Write-Error 'plan-gate-check: python3 is required'; exit 1 }
4521
4741
  $core = Join-Path $PSScriptRoot '..' 'python' 'aw-plangate.py'
4522
4742
  if (-not (Test-Path $core)) { Write-Error "plan-gate-check: core not found at $core"; exit 1 }
4523
- & $py $core check --feature-dir $fd --context-dir $ctx --root $repo
4743
+ $laneArgs = @()
4744
+ if ($lane) { $laneArgs = @('--lane', $lane) }
4745
+ & $py $core check --feature-dir $fd --context-dir $ctx --root $repo @laneArgs
4524
4746
  if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
4525
4747
  }
4526
4748
 
@@ -4657,9 +4879,14 @@ switch ($sub) {
4657
4879
  'lane-verify' { Invoke-LaneVerify -Rest $rest }
4658
4880
  'lane-probe' { Invoke-LaneProbe -Rest $rest }
4659
4881
  'lane-set' { Invoke-LaneSet -Rest $rest }
4882
+ 'lane-select' { Invoke-LaneSelect -Rest $rest }
4660
4883
  'contract-export' { Invoke-ContractExport -Rest $rest }
4884
+ 'contract-draft' { Invoke-ContractDraft -Rest $rest }
4885
+ 'contract-combine' { Invoke-ContractCombine -Rest $rest }
4886
+ 'contract-requests' { Invoke-ContractRequests -Rest $rest }
4661
4887
  'contract-client' { Invoke-ContractClient -Rest $rest }
4662
4888
  'contract-check' { Invoke-ContractCheck -Rest $rest }
4889
+ 'contract-impact' { Invoke-ContractImpact -Rest $rest }
4663
4890
  'list-contexts' { Invoke-ListContexts -Rest $rest }
4664
4891
  'set-context' { Invoke-SetContext -Rest $rest }
4665
4892
  'list-features' { Invoke-ListFeatures -Rest $rest }
@@ -4685,6 +4912,8 @@ switch ($sub) {
4685
4912
  'cr-overlap' { Invoke-CrOverlap -Rest $rest }
4686
4913
  'cr-activate' { Invoke-CrActivate -Rest $rest }
4687
4914
  'cr-wave' { Invoke-CrWave -Rest $rest }
4915
+ 'cr-lanes' { Invoke-CrLanes -Rest $rest }
4916
+ 'slice-gate-stamp' { Invoke-SliceGateStamp -Rest $rest }
4688
4917
  'cr-wave-derive' { Invoke-CrWaveDerive -Rest $rest }
4689
4918
  'cr-rebase' { Invoke-CrRebase -Rest $rest }
4690
4919
  'cr-archive' { Invoke-CrArchive -Rest $rest }
@@ -2,7 +2,8 @@
2
2
  #
3
3
  # Cross-reference (OKF dependency) substrate. Logic lives ONCE in
4
4
  # ..\python\aw-xref.py; this wrapper exists only so the orchestrator's
5
- # execute_script (sh:/ps1: dispatch) can invoke it. Args pass through.
5
+ # execute_script (sh:/ps1: dispatch) can invoke it. Args pass through, each
6
+ # flag joined to its value (see below).
6
7
  #
7
8
  # aw-xref.ps1 record --using X --design <design.md> [--origin ...] [--date YYYY-MM-DD] [--root DIR]
8
9
  # aw-xref.ps1 list --aggregate Y [--root DIR] [--bundle knowledge]
@@ -17,5 +18,19 @@
17
18
  # then the boundary map, then the feature name itself)
18
19
  $ErrorActionPreference = "Stop"
19
20
  $core = Join-Path $PSScriptRoot "..\python\aw-xref.py"
20
- & python3 $core @args
21
+ # Each value travels to python as ONE `--flag=value` argument. Windows PowerShell 5.1, and
22
+ # pwsh before 7.3, drop an empty argument when calling a program, which would leave a flag
23
+ # such as --under with no value or with the next flag as its value. `--under=` keeps the
24
+ # empty value, and the core ignores an empty --under. --merge and --prune take no value.
25
+ $switches = @('--merge', '--prune')
26
+ $fwd = @()
27
+ for ($i = 0; $i -lt $args.Count; $i++) {
28
+ $a = "$($args[$i])"
29
+ if ($a.StartsWith('--') -and -not $a.Contains('=') -and $switches -notcontains $a -and $i + 1 -lt $args.Count) {
30
+ $fwd += ($a + '=' + $args[++$i])
31
+ } else {
32
+ $fwd += $a
33
+ }
34
+ }
35
+ & python3 $core @fwd
21
36
  exit $LASTEXITCODE