aw-spec-cli 0.1.21 → 0.1.23

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 (51) hide show
  1. package/assets/.appweaver/STACK-PROFILES.md +87 -8
  2. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +30 -0
  3. package/assets/.appweaver/orchestrator/orchestrator.py +47 -3
  4. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +41 -8
  5. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +62 -2
  6. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +18 -5
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +1 -1
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +4 -1
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +115 -27
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +5 -1
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +225 -27
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +4 -15
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +1 -1
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +39 -0
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +28 -7
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +3 -1
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +104 -1
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +159 -0
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +386 -0
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +153 -0
  21. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +203 -212
  22. package/assets/.appweaver/scripts/bash/aw-xref.sh +4 -1
  23. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +191 -224
  24. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +4 -1
  25. package/assets/.appweaver/scripts/python/aw-crstate.py +127 -2
  26. package/assets/.appweaver/scripts/python/aw-gaps.py +119 -0
  27. package/assets/.appweaver/scripts/python/aw-payload-parity.py +17 -1
  28. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +84 -4
  29. package/assets/.appweaver/scripts/python/aw-plangate.py +216 -0
  30. package/assets/.appweaver/scripts/python/aw-principles.py +108 -0
  31. package/assets/.appweaver/scripts/python/aw-stack.py +6 -0
  32. package/assets/.appweaver/scripts/python/aw-xref.py +141 -4
  33. package/assets/.appweaver/stack-profiles/angular.json +26 -0
  34. package/assets/.appweaver/stack-profiles/blazor-wasm.json +38 -0
  35. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +38 -0
  36. package/assets/.appweaver/stack-profiles/java-spring.json +19 -0
  37. package/assets/.appweaver/stack-profiles/nextjs.json +26 -0
  38. package/assets/.appweaver/stack-profiles/node-express.json +26 -0
  39. package/assets/.appweaver/stack-profiles/python-fastapi.json +15 -0
  40. package/assets/.appweaver/stack-profiles/react-vite.json +26 -0
  41. package/assets/.appweaver/stack-profiles/sveltekit.json +26 -0
  42. package/assets/.appweaver/stack-profiles/vue-vite.json +26 -0
  43. package/assets/.appweaver/templates/ui-model-template.md +67 -0
  44. package/assets/.appweaver/templates/workflow-model-template.md +62 -0
  45. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +3 -3
  46. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +5 -123
  47. package/assets/foundations/java/Hexagonal/Hexagonal.pack.txt +784 -0
  48. package/assets/foundations/java/Hexagonal/pack-foundation.sh +357 -0
  49. package/assets/foundations/java/Hexagonal/unpack-foundation.sh +399 -0
  50. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +44 -0
  51. package/package.json +1 -1
@@ -49,7 +49,7 @@
49
49
  # version-read [--feature-dir <path>] -> {feature, version, active_change, exists, bootstrap_pending}
50
50
  # version-init [--feature-dir <path>] [--bootstrap] -> {…, created, bootstrap_pending} (create active_version.json v1.0; --bootstrap marks a FIRST draft in progress)
51
51
  # version-bootstrap-done [--feature-dir <path>] -> {…, cleared} (the ONLY clearer of the --bootstrap marker; call it when the first draft is finished)
52
- # feature-paths -> {feature, feature_dir, spec_file, backend_plan, frontend_plan, *_exists,
52
+ # feature-paths -> {feature, feature_slug, feature_dir, spec_file, backend_plan, frontend_plan, *_exists,
53
53
  # design_file/ui_design_file (aliases of the two plans), spec_exists, bootstrap_pending, needs_bootstrap}
54
54
  # checklist-read --file <path> -> {exists, total, checked, items[]}
55
55
  # checklist-apply --file <path> --results <json>-> {exists, before_checked, after_checked, total, newly_passing[], regressions[]}
@@ -1100,9 +1100,12 @@ _cr_id_token() { # $1 = raw --cr-id value
1100
1100
  }
1101
1101
 
1102
1102
  # One change's wave, read from the documents that carry it: the delta-spec first, then
1103
- # the model delta. Both are stamped together, so either answers; 1 is the floor for a
1104
- # change opened before waves existed.
1105
- _wave_of() { # $1 = abs feature dir, $2 = cr id, $3 = repo_root
1103
+ # the model delta. Both are stamped together, so either answers. EMPTY when neither carries
1104
+ # a readable one — `**Wave**: [N]` is the unfilled template line, so "no wave" is a state
1105
+ # that happens, and a caller who WRITES the answer somewhere must not write a number nobody
1106
+ # derived. A phantom wave is worse than none: the merge and recall fences can see that a
1107
+ # wave is missing and say so, but a 1 that was invented downstream reads to them as fact.
1108
+ _wave_of_readable() { # $1 = abs feature dir, $2 = cr id, $3 = repo_root
1106
1109
  local fd="$1" cr="$2" repo_root="$3" w=""
1107
1110
  local dspec="$fd/changes/$cr/delta-spec.md"
1108
1111
  local ctx_dir; ctx_dir=$(_context_dir_for_feature "$fd" "$repo_root")
@@ -1112,11 +1115,20 @@ _wave_of() { # $1 = abs feature dir, $2 = cr id, $3 = repo_root
1112
1115
  w=$(sed -n -E 's/^\*\*Wave\*\*:[[:space:]]*([0-9]+).*$/\1/p' "$f" | head -1)
1113
1116
  [ -n "$w" ] && break
1114
1117
  done
1115
- case "$w" in ''|*[!0-9]*) w=1 ;; esac
1116
- [ "$w" -ge 1 ] 2>/dev/null || w=1
1118
+ case "$w" in ''|*[!0-9]*) w="" ;; esac
1119
+ if [ -n "$w" ]; then
1120
+ [ "$w" -ge 1 ] 2>/dev/null || w=""
1121
+ fi
1117
1122
  printf '%s' "$w"
1118
1123
  }
1119
1124
 
1125
+ # The same reading with the floor applied, for the readers that must show a number — a
1126
+ # listing has a column to fill and 1 is the floor for a change opened before waves existed.
1127
+ _wave_of() { # $1 = abs feature dir, $2 = cr id, $3 = repo_root
1128
+ local w; w=$(_wave_of_readable "$1" "$2" "$3")
1129
+ printf '%s' "${w:-1}"
1130
+ }
1131
+
1120
1132
  # ─────────────────────────────── context-paths ─────────────────────────────
1121
1133
  # Everything specify needs to know about the domain model of the context it is working
1122
1134
  # in: where the master is, whether it exists yet, and EVERY open delta model beside it.
@@ -1664,6 +1676,16 @@ cmd_feature_paths() {
1664
1676
  fi
1665
1677
  [[ "$fd" != /* ]] && fd="$repo_root/$fd"
1666
1678
  local feature; feature=$(basename "$fd") # the aggregate name (feature = specs/<Feature>)
1679
+ # feature_slug is the URL-safe form of that name, and it is what the UI stages use
1680
+ # as a route segment and a directory name (app/<slug>/page.tsx). It is derived here
1681
+ # rather than per workflow so every reader gets the same segment for one feature.
1682
+ # CamelCase splits on the case boundary before the non-alphanumeric squeeze, so
1683
+ # OrderManagement and "Order Management" both reach order-management.
1684
+ local feature_slug
1685
+ feature_slug=$(printf '%s' "$feature" \
1686
+ | sed -E 's/([a-z0-9])([A-Z])/\1-\2/g; s/([A-Z]+)([A-Z][a-z])/\1-\2/g' \
1687
+ | tr '[:upper:]' '[:lower:]' \
1688
+ | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')
1667
1689
  # The master documents of a feature: spec.md (WHAT/WHY) + one LANE PLAN per declared
1668
1690
  # lane (each lane's HOW) — backend-plan.md for the service lane, frontend-plan.md for
1669
1691
  # the frontend lane. `plan` writes them; nothing else does.
@@ -1696,8 +1718,8 @@ cmd_feature_paths() {
1696
1718
  local needs_bootstrap=false
1697
1719
  [ "$spec_exists" = false ] && needs_bootstrap=true
1698
1720
  [ "$bootstrap_pending" = true ] && needs_bootstrap=true
1699
- printf '{"feature":"%s","feature_dir":"%s","spec_file":"%s","backend_plan":"%s","frontend_plan":"%s","backend_plan_exists":%s,"frontend_plan_exists":%s,"design_file":"%s","ui_design_file":"%s","spec_exists":%s,"design_exists":%s,"ui_design_exists":%s,"bootstrap_pending":%s,"needs_bootstrap":%s}\n' \
1700
- "$(json_escape "$feature")" "$(json_escape "$fd")" "$(json_escape "$spec")" \
1721
+ printf '{"feature":"%s","feature_slug":"%s","feature_dir":"%s","spec_file":"%s","backend_plan":"%s","frontend_plan":"%s","backend_plan_exists":%s,"frontend_plan_exists":%s,"design_file":"%s","ui_design_file":"%s","spec_exists":%s,"design_exists":%s,"ui_design_exists":%s,"bootstrap_pending":%s,"needs_bootstrap":%s}\n' \
1722
+ "$(json_escape "$feature")" "$(json_escape "$feature_slug")" "$(json_escape "$fd")" "$(json_escape "$spec")" \
1701
1723
  "$(json_escape "$design")" "$(json_escape "$ui_design")" "$design_exists" "$ui_design_exists" \
1702
1724
  "$(json_escape "$design")" "$(json_escape "$ui_design")" "$spec_exists" "$design_exists" "$ui_design_exists" \
1703
1725
  "$bootstrap_pending" "$needs_bootstrap"
@@ -2564,6 +2586,25 @@ print(json.dumps({"ok": ok, "exists": True, "issues": issues,
2564
2586
  PY
2565
2587
  }
2566
2588
 
2589
+ # ─────────────────────────────── principles-list ───────────────────────────
2590
+ # The ratified NON-NEGOTIABLE principles. A thin wrapper: the scan lives in
2591
+ # python/aw-principles.py, which BOTH shells invoke, so the list a gate cites cannot
2592
+ # differ between them. Defaults to the two halves specify loads; --file overrides.
2593
+ cmd_principles_list() {
2594
+ local rest=()
2595
+ while [ $# -gt 0 ]; do
2596
+ case "$1" in
2597
+ --help|-h) echo "principles-list [--file <path>]... -> {files[], missing[], principles[], count, titles_csv}"; return 0 ;;
2598
+ *) rest+=("$1"); shift ;;
2599
+ esac
2600
+ done
2601
+ local repo_root; repo_root=$(get_repo_root)
2602
+ local core="$SCRIPT_DIR/../python/aw-principles.py"
2603
+ if [ ! -f "$core" ]; then echo "principles-list: core not found at $core" >&2; return 1; fi
2604
+ if ! command -v python3 >/dev/null 2>&1; then echo "principles-list: python3 is required" >&2; return 1; fi
2605
+ python3 "$core" list --root "$repo_root" "${rest[@]+"${rest[@]}"}"
2606
+ }
2607
+
2567
2608
  # ────────────────────────── agent-context-update ───────────────────────────
2568
2609
  # Run the agent-context extension's updater (refreshes the managed AW-Spec block).
2569
2610
  cmd_agent_context_update() {
@@ -2924,7 +2965,14 @@ cmd_delta_plan_scaffold() {
2924
2965
  if [ -n "$tpl" ] && [ -f "$tpl" ]; then cp "$tpl" "$target"; else touch "$target"; fi
2925
2966
  created="true"
2926
2967
  fi
2927
- _stamp_header "$target" "Wave" "$(_wave_of "$fd" "$cr_id" "$repo_root")"
2968
+ # The lane plan carries the change's wave so a reader of the plan alone can see where it
2969
+ # sits. It is COPIED from the delta-spec, never decided here — so when the spec has no
2970
+ # readable wave the template's `[N]` is left standing, which is what the merge and recall
2971
+ # fences read as "not ordered yet" and refuse on, with the command that stamps it.
2972
+ local scaffold_wave; scaffold_wave=$(_wave_of_readable "$fd" "$cr_id" "$repo_root")
2973
+ if [ -n "$scaffold_wave" ]; then
2974
+ _stamp_header "$target" "Wave" "$scaffold_wave"
2975
+ fi
2928
2976
  local override_active="false"
2929
2977
  [ -f "$repo_root/.appweaver/templates/overrides/${template_arg}.md" ] && override_active="true"
2930
2978
  local template_source; template_source=$(resolve_template "$template_arg" "$repo_root") || true
@@ -2997,13 +3045,17 @@ cmd_test_run() {
2997
3045
  # feature's aggregate name (feature-dir basename) plus any --also
2998
3046
  # referrer names — the per-feature integrity scope. .NET only; npm/none
2999
3047
  # ignore it and run the full suite.
3048
+ # --also is REPEATABLE and the lists UNION. A merge's blast radius has two halves that
3049
+ # are computed separately — the referrer closure and the features that co-own the
3050
+ # aggregates the change touches — and the dialect cannot concatenate two variables into
3051
+ # one argument, so the caller passes each list as its own --also.
3000
3052
  local feature_dir="" scope="repo" also=""
3001
3053
  while [ $# -gt 0 ]; do
3002
3054
  case "$1" in
3003
3055
  --feature-dir) feature_dir="$2"; shift 2 ;;
3004
3056
  --scope) scope="$2"; shift 2 ;;
3005
- --also) also="$2"; shift 2 ;;
3006
- --help|-h) echo "test-run [--feature-dir <path>] [--scope slice|repo] [--also <csv>]"; return 0 ;;
3057
+ --also) also="${also:+$also,}$2"; shift 2 ;;
3058
+ --help|-h) echo "test-run [--feature-dir <path>] [--scope slice|repo] [--also <csv>]... (--also repeatable; lists union)"; return 0 ;;
3007
3059
  *) echo "test-run: unknown arg '$1'" >&2; return 1 ;;
3008
3060
  esac
3009
3061
  done
@@ -3493,8 +3545,12 @@ required = max([d["wave"] for d in known], default=0) + 1
3493
3545
  current = wave_of(cr)
3494
3546
  ok = current is not None and current >= required
3495
3547
 
3548
+ # A change with NO readable wave is stamped too, not only one sitting too low. The merge
3549
+ # and recall fences refuse on an unordered change and send the operator here, so the one
3550
+ # command they are given has to be able to write the first wave as well as correct a wrong
3551
+ # one — a remedy that declines the case it was named for is no remedy.
3496
3552
  stamped = []
3497
- if stamp and current is not None and current < required:
3553
+ if stamp and (current is None or current < required):
3498
3554
  line = "**Wave**: %d" % required
3499
3555
  pat = re.compile(r"^\*\*Wave\*\*:.*$", re.M)
3500
3556
  for p in present:
@@ -3505,13 +3561,16 @@ if stamp and current is not None and current < required:
3505
3561
  if stamped:
3506
3562
  current, ok = required, True
3507
3563
 
3508
- if not deps:
3509
- msg = "%s declares no predecessor, so wave %d is the floor; it carries %s" % (
3510
- cr, required, current if current is not None else "no wave")
3511
- elif stamped:
3564
+ if stamped and deps:
3512
3565
  msg = "%s depends on %s, so it was raised to wave %d in %s" % (
3513
3566
  cr, ", ".join("%s (wave %s)" % (d["cr_id"], d["wave"]) for d in known),
3514
3567
  required, ", ".join(stamped))
3568
+ elif stamped:
3569
+ msg = "%s declares no predecessor, so it was stamped at wave %d — the floor — in %s" % (
3570
+ cr, required, ", ".join(stamped))
3571
+ elif not deps:
3572
+ msg = "%s declares no predecessor, so wave %d is the floor; it carries %s" % (
3573
+ cr, required, current if current is not None else "no wave")
3515
3574
  elif ok:
3516
3575
  msg = "%s is at wave %s, at or above the %d its `## Depends on` requires" % (
3517
3576
  cr, current, required)
@@ -4434,7 +4493,7 @@ cmd_cr_merge_check() {
4434
4493
  case "$1" in
4435
4494
  --cr-id) cr_id="$2"; shift 2 ;;
4436
4495
  --feature-dir) feature_dir_arg="$2"; shift 2 ;;
4437
- --help|-h) echo "cr-merge-check --cr-id <id> [--feature-dir <path>] -> {rebase_needed, base_version, current_version, blockers[], blocker_count, wave, …}"; return 0 ;;
4496
+ --help|-h) echo "cr-merge-check --cr-id <id> [--feature-dir <path>] -> {rebase_needed, base_version, current_version, blockers[], blocker_count, wave, wave_ordering_ok, unordered[], …}"; return 0 ;;
4438
4497
  *) echo "cr-merge-check: unknown arg '$1'" >&2; return 1 ;;
4439
4498
  esac
4440
4499
  done
@@ -4462,14 +4521,8 @@ def norm(v):
4462
4521
  if not v: return ""
4463
4522
  return v if v.startswith("v") else "v" + v
4464
4523
 
4465
- # THIS change's own spec half and wave.
4524
+ # THIS change's own spec half.
4466
4525
  my_spec = os.path.join(fd, "changes", cr, "delta-spec.md")
4467
- def wave_of(p, default=1):
4468
- try: t = open(p, encoding="utf-8").read()
4469
- except Exception: return default
4470
- m = re.search(r"^\*\*Wave\*\*:[ \t]*(\d+)", t, re.M)
4471
- return int(m.group(1)) if m else default
4472
- wave = wave_of(my_spec)
4473
4526
  base_version = norm(header(my_spec, "Base version"))
4474
4527
 
4475
4528
  # The master's CURRENT version, from the ledger — the same authority version-read reports,
@@ -4486,52 +4539,65 @@ if not current:
4486
4539
  # absence of evidence and is not treated as a move.
4487
4540
  rebase_needed = bool(base_version and current and base_version != current)
4488
4541
 
4489
- # EVERY open change in the context, both halves. changes/<id>/ under each feature is the
4490
- # authority for "open"; the pending model deltas are unioned in so a model-only artifact of
4491
- # a half-built change is still seen.
4492
- opens = {}
4493
- for feat in sorted(os.listdir(ctx)) if os.path.isdir(ctx) else []:
4494
- ch = os.path.join(ctx, feat, "changes")
4495
- if not os.path.isdir(ch): continue
4496
- for name in sorted(os.listdir(ch)):
4497
- d = os.path.join(ch, name)
4498
- if name == "archive" or not os.path.isdir(d): continue
4499
- opens.setdefault(name, {"cr_id": name, "wave": wave_of(os.path.join(d, "delta-spec.md")),
4500
- "feature": feat})
4501
- for f in sorted(os.listdir(ctx)) if os.path.isdir(ctx) else []:
4502
- m = re.match(r"^domain-model\.(.+)\.delta\.md$", f)
4503
- if not m: continue
4504
- other = m.group(1)
4505
- opens.setdefault(other, {"cr_id": other, "wave": wave_of(os.path.join(ctx, f)), "feature": ""})
4506
-
4507
- blockers = [v for k, v in sorted(opens.items()) if k != cr and v["wave"] < wave]
4508
-
4509
- # THE MASTERS CAN MOVE WITHOUT THE VERSION MOVING. finalize-delta merges and close-delta
4510
- # bumps, so between the two the masters carry a change the number does not yet reflect — and
4511
- # a second delta sharing the same base_version would compare equal and merge onto masters
4512
- # that had already shifted under it. The proof is the Change History row finalize-delta
4513
- # appends, carrying the id in its CR cell.
4542
+ # BOTH CONTEXT-WIDE QUESTIONS COME FROM aw-crstate, for every reader of them and for both
4543
+ # shells: which open changes this one is ordered against, and which of them have already
4544
+ # merged. Each was once a copy of the scan living in this payload and another in its
4545
+ # PowerShell twin, which is how the two came to answer the same question differently.
4546
+ #
4547
+ # PREDECESSORS. Every open change in the context, both halves — changes/<id>/ under each
4548
+ # feature is the authority for "open", and the pending model deltas are unioned in so a
4549
+ # model-only artifact of a half-built change is still seen. A change whose `**Wave**:` is
4550
+ # unreadable is reported as unordered rather than assumed to be wave 1, which is what let an
4551
+ # unstamped delta pose as a predecessor and let an unstamped SUBJECT skip this fence
4552
+ # entirely.
4514
4553
  #
4515
- # aw-crstate owns that test, for every reader of it. It replaces `cr in spec.md` — which
4516
- # fired on an id quoted in prose and matched a prefix of a longer id — and it looks where a
4517
- # merge can actually show: this feature's spec AND the context model every feature shares,
4518
- # so a SIBLING feature's merged-unclosed change is no longer invisible. A sibling blocks
4519
- # only where it touched an aggregate this delta also names (`blocking`); a merge in this
4520
- # feature blocks outright, because it moved this delta's own master.
4521
- merged_open = []
4554
+ # MERGED-OPEN. The masters can move without the version moving: finalize-delta merges and
4555
+ # close-delta bumps, so between the two the masters carry a change the number does not yet
4556
+ # reflect, and a second delta sharing the same base_version would compare equal and merge
4557
+ # onto masters that had already shifted under it. The proof is the Change History row
4558
+ # finalize-delta appends, carrying the id in its CR cell — which replaced `cr in spec.md`,
4559
+ # a test that fired on an id quoted in prose and matched a prefix of a longer id. It looks
4560
+ # where a merge can actually show: this feature's spec AND the context model every feature
4561
+ # shares, so a SIBLING feature's merged-unclosed change is no longer invisible. A sibling
4562
+ # blocks only where it touched an aggregate this delta also names (`blocking`); a merge in
4563
+ # this feature blocks outright, because it moved this delta's own master.
4564
+ _m, _why = None, ""
4522
4565
  try:
4523
4566
  import importlib.util
4524
4567
  _mod_path = os.path.join(root, ".appweaver", "scripts", "python", "aw-crstate.py")
4525
4568
  _spec = importlib.util.spec_from_file_location("aw_crstate", _mod_path)
4526
- _m = importlib.util.module_from_spec(_spec)
4527
- _spec.loader.exec_module(_m)
4528
- merged_open = [r for r in _m.merged_open(ctx, fd, cr) if r["blocking"]]
4569
+ _loaded = importlib.util.module_from_spec(_spec)
4570
+ _spec.loader.exec_module(_loaded)
4571
+ _m = _loaded
4529
4572
  except Exception as _e:
4530
- # Fail CLOSED on an unreadable helper: report the state as blocking rather than clear.
4531
- merged_open = [{"cr_id": "(merged-open check failed: %s)" % _e, "blocking": True}]
4573
+ _why = str(_e)
4574
+
4575
+ order, merged_open, merged_text = None, None, ""
4576
+ if _m is not None:
4577
+ try:
4578
+ order = _m.wave_order(ctx, fd, cr, "lower", "context")
4579
+ merged_open = [r for r in _m.merged_open(ctx, fd, cr) if r["blocking"]]
4580
+ merged_text = _m.merged_open_text(merged_open)
4581
+ except Exception as _e:
4582
+ order, merged_open, _why = None, None, str(_e)
4583
+
4584
+ # Fail CLOSED on an unreadable helper: report the state as blocking rather than clear, and
4585
+ # say which check could not run rather than which condition was found.
4586
+ if order is None:
4587
+ order = {"wave": None, "wave_known": False, "related": [], "related_count": 0,
4588
+ "related_ids": "", "unknown": [], "unknown_count": 0, "unknown_ids": "",
4589
+ "unknown_blocks": True, "wave_ordering_ok": False,
4590
+ "wave_problem": "the wave reader could not be run (%s), so no open change in "
4591
+ "this context is ordered against %s at all" % (_why, cr)}
4592
+ if merged_open is None:
4593
+ merged_open = [{"cr_id": "(merged-open check failed: %s)" % _why, "blocking": True}]
4594
+ merged_text = merged_open[0]["cr_id"]
4595
+ blockers = order["related"]
4532
4596
 
4533
4597
  print(json.dumps({
4534
- "ok": True, "cr_id": cr, "wave": wave,
4598
+ "ok": True, "cr_id": cr,
4599
+ "wave": order["wave"] if order["wave"] is not None else "",
4600
+ "wave_known": order["wave_known"],
4535
4601
  "exists": os.path.isdir(os.path.join(fd, "changes", cr)),
4536
4602
  # REBASE half — the masters moved because a change CLOSED and bumped the number.
4537
4603
  "base_version": base_version, "current_version": current,
@@ -4540,14 +4606,20 @@ print(json.dumps({
4540
4606
  # PREDECESSOR half. Lower wave = committed to land first.
4541
4607
  "blockers": blockers, "blocker_count": len(blockers),
4542
4608
  "has_blockers": len(blockers) > 0,
4543
- "blocker_ids": ", ".join("%s (wave %d)" % (b["cr_id"], b["wave"]) for b in blockers),
4609
+ "blocker_ids": order["related_ids"],
4610
+ # The changes this one cannot be ordered against at all, in either direction.
4611
+ "unordered": order["unknown"], "unordered_count": order["unknown_count"],
4612
+ "unordered_ids": order["unknown_ids"],
4613
+ "wave_ordering_ok": order["wave_ordering_ok"],
4614
+ "wave_problem": order["wave_problem"],
4544
4615
  # The other way the masters move: a change MERGED and has not closed yet.
4545
4616
  "merged_open": merged_open, "merged_open_count": len(merged_open),
4546
4617
  "has_merged_open": len(merged_open) > 0,
4547
- "merged_open_ids": ", ".join(m["cr_id"] for m in merged_open),
4548
- # All three refusals as ONE positive flag, because the engine takes no compound condition
4549
- # and a gate that must read three booleans would need three passes over one question.
4550
- "merge_ok": (not rebase_needed) and len(blockers) == 0 and len(merged_open) == 0,
4618
+ "merged_open_ids": merged_text,
4619
+ # All four refusals as ONE positive flag, because the engine takes no compound condition
4620
+ # and a gate that must read four booleans would need four passes over one question.
4621
+ "merge_ok": (not rebase_needed) and order["wave_ordering_ok"] and len(blockers) == 0
4622
+ and len(merged_open) == 0,
4551
4623
  }))
4552
4624
  PY
4553
4625
  }
@@ -4572,7 +4644,7 @@ cmd_cr_recall_check() {
4572
4644
  case "$1" in
4573
4645
  --cr-id) cr_id="$2"; shift 2 ;;
4574
4646
  --feature-dir) feature_dir_arg="$2"; shift 2 ;;
4575
- --help|-h) echo "cr-recall-check --cr-id <id> [--feature-dir <path>] -> {merged, dependants[], dependant_count, wave, …}"; return 0 ;;
4647
+ --help|-h) echo "cr-recall-check --cr-id <id> [--feature-dir <path>] -> {merged, dependants[], dependant_count, wave, wave_ordering_ok, unordered[], …}"; return 0 ;;
4576
4648
  *) echo "cr-recall-check: unknown arg '$1'" >&2; return 1 ;;
4577
4649
  esac
4578
4650
  done
@@ -4588,21 +4660,13 @@ root = os.environ.get("AW_ROOT", ""); ctx = os.environ.get("AW_CTX", "")
4588
4660
  changes = os.path.join(fd, "changes")
4589
4661
  cr_dir = os.path.join(changes, cr)
4590
4662
 
4591
- def wave_of(d):
4592
- for name in ("delta-spec.md",):
4593
- p = os.path.join(d, name)
4594
- if os.path.isfile(p):
4595
- m = re.search(r'^\*\*Wave\*\*:[ \t]*(\d+)', open(p, encoding="utf-8").read(), re.M)
4596
- if m:
4597
- return int(m.group(1))
4598
- return 1
4599
-
4600
4663
  # MERGED = a Change History row whose CR cell IS this change, in this feature's spec or in
4601
4664
  # the context model every feature shares — aw-crstate owns that test for all three readers
4602
4665
  # of it. The old test was `cr in line`, which fired on an id quoted in prose and matched a
4603
4666
  # prefix of a longer id.
4604
4667
  spec = os.path.join(fd, "spec.md")
4605
4668
  merged, evidence, merged_doc = False, "", ""
4669
+ order, _why = None, ""
4606
4670
  try:
4607
4671
  import importlib.util
4608
4672
  _mod_path = os.path.join(root, ".appweaver", "scripts", "python", "aw-crstate.py")
@@ -4611,29 +4675,39 @@ try:
4611
4675
  _spec.loader.exec_module(_m)
4612
4676
  evidence, merged_doc = _m.merged_in([spec, os.path.join(ctx, "domain-model.md")], cr)
4613
4677
  merged = bool(evidence)
4614
- except Exception:
4678
+ # DEPENDANTS, from the same reader the merge fence uses in the opposite direction. An
4679
+ # unreadable `**Wave**:` is not wave 1 here either: it used to mean `w > wave` was false
4680
+ # and the change was not counted, so recall withdrew the ground from under a dependant
4681
+ # it could not see. Waves have no ceiling, so any unordered change could sit above this
4682
+ # one and every one of them holds recall.
4683
+ order = _m.wave_order(ctx, fd, cr, "higher", "feature")
4684
+ except Exception as _e:
4615
4685
  # Fail CLOSED: an unreadable helper must not report "never merged", which is the
4616
4686
  # answer that lets close-delta archive a change nobody merged.
4617
- merged, evidence = True, "(merged check unavailable — treated as merged)"
4618
-
4619
- wave = wave_of(cr_dir) if os.path.isdir(cr_dir) else 1
4620
- dependants = []
4621
- if os.path.isdir(changes):
4622
- for name in sorted(os.listdir(changes)):
4623
- d = os.path.join(changes, name)
4624
- if name == cr or name == "archive" or not os.path.isdir(d):
4625
- continue
4626
- w = wave_of(d)
4627
- if w > wave:
4628
- dependants.append({"cr_id": name, "wave": w})
4687
+ merged, evidence, _why = True, "(merged check unavailable — treated as merged)", str(_e)
4688
+
4689
+ if order is None:
4690
+ order = {"wave": None, "wave_known": False, "related": [], "related_count": 0,
4691
+ "related_ids": "", "unknown": [], "unknown_count": 0, "unknown_ids": "",
4692
+ "unknown_blocks": True, "wave_ordering_ok": False,
4693
+ "wave_problem": "the wave reader could not be run (%s), so no open change in "
4694
+ "this feature is ordered against %s at all" % (_why, cr)}
4695
+ dependants = order["related"]
4629
4696
 
4630
4697
  print(json.dumps({
4631
- "ok": True, "cr_id": cr, "exists": os.path.isdir(cr_dir), "wave": wave,
4698
+ "ok": True, "cr_id": cr, "exists": os.path.isdir(cr_dir),
4699
+ "wave": order["wave"] if order["wave"] is not None else "",
4700
+ "wave_known": order["wave_known"],
4632
4701
  "merged": merged, "merge_evidence": evidence,
4633
4702
  "dependants": dependants,
4634
- "dependant_ids": ", ".join("%s (wave %d)" % (x["cr_id"], x["wave"]) for x in dependants),
4703
+ "dependant_ids": order["related_ids"],
4635
4704
  "dependant_count": len(dependants),
4636
4705
  "has_dependants": len(dependants) > 0,
4706
+ # The changes this one cannot be ordered against at all, in either direction.
4707
+ "unordered": order["unknown"], "unordered_count": order["unknown_count"],
4708
+ "unordered_ids": order["unknown_ids"],
4709
+ "wave_ordering_ok": order["wave_ordering_ok"],
4710
+ "wave_problem": order["wave_problem"],
4637
4711
  }))
4638
4712
  PY
4639
4713
  }
@@ -4641,6 +4715,12 @@ PY
4641
4715
  # ──────────────────── plan-gate-stamp / plan-gate-check ────────────────────
4642
4716
  # THE RECEIPT THAT analyze-plan PASSED, and over WHICH BYTES.
4643
4717
  #
4718
+ # THE RULE ITSELF LIVES IN python/aw-plangate.py, which both shells invoke — these two are
4719
+ # thin wrappers that resolve the feature and hand over. It is a set of sha comparisons with
4720
+ # three deliberate asymmetries in it, and while it was embedded twice (a heredoc here, a
4721
+ # native function there) every change to it had to be re-derived by hand on the other side
4722
+ # and kept in step by eye. It changed twice that way. Now there is nothing to keep in step.
4723
+ #
4644
4724
  # implement generates from the lane plans and deliberately does NOT read the spec — its
4645
4725
  # own header says "analyze-plan already conformed the plans to it". That sentence was an
4646
4726
  # assumption about the order the operator ran things in, not a fact anything checked: the
@@ -4719,75 +4799,23 @@ PY
4719
4799
  }
4720
4800
 
4721
4801
  cmd_plan_gate_stamp() {
4722
- local feature_dir_arg="" spec_version="" mode="master" verdict="conformant" plan_gaps="0" routed="[]"
4802
+ case "${1:-}" in
4803
+ --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 ;;
4804
+ esac
4805
+ local feature_dir_arg="" rest=()
4723
4806
  while [ $# -gt 0 ]; do
4724
4807
  case "$1" in
4725
4808
  --feature-dir) feature_dir_arg="$2"; shift 2 ;;
4726
- --spec-version) spec_version="$2"; shift 2 ;;
4727
- --mode) mode="$2"; shift 2 ;;
4728
- --verdict) verdict="$2"; shift 2 ;;
4729
- --plan-gaps) plan_gaps="$2"; shift 2 ;;
4730
- --routed) routed="$2"; shift 2 ;;
4731
- --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 ;;
4732
- *) echo "plan-gate-stamp: unknown arg '$1'" >&2; return 1 ;;
4809
+ *) rest+=("$1"); shift ;;
4733
4810
  esac
4734
4811
  done
4735
4812
  local repo_root; repo_root=$(get_repo_root)
4736
4813
  local fd; fd=$(_resolve_feature_dir "$feature_dir_arg" "$repo_root") || { echo "plan-gate-stamp: no feature set and no --feature-dir" >&2; return 1; }
4737
4814
  local ctx_dir; ctx_dir=$(_context_dir_for_feature "$fd" "$repo_root")
4738
- AW_FD="$fd" AW_CTX="$ctx_dir" AW_ROOT="$repo_root" AW_VER="$spec_version" AW_MODE="$mode" \
4739
- AW_VERDICT="$verdict" AW_PLAN_GAPS="$plan_gaps" AW_ROUTED="$routed" python3 - <<'PY'
4740
- import datetime, hashlib, json, os
4741
- fd = os.environ["AW_FD"]; ctx = os.environ["AW_CTX"]; root = os.environ["AW_ROOT"]
4742
- def sha(p):
4743
- try:
4744
- with open(p, "rb") as f: return hashlib.sha256(f.read()).hexdigest()
4745
- except Exception: return ""
4746
- def rel(p): return os.path.relpath(p, root)
4747
- # Every document the gate READ. A lane that is not declared has no plan on disk and is
4748
- # simply absent from the map — the check treats a name it does not carry as not gated,
4749
- # never as changed, so declaring a second lane later does not read as tampering.
4750
- #
4751
- # THE LEGACY NAMES ARE HERE FOR THE SAME REASON version-stamp CARRIES THEM. feature-paths
4752
- # resolves design.md / design.ui.md under the backend_plan / frontend_plan keys, so a tree
4753
- # that has not been through `plan` since the rename — every brownfield feature `adopt`
4754
- # produces — is GATED on those documents. Hashing only the new names would leave such a
4755
- # feature with an empty plan map: a receipt that looks tracked and fresh while the document
4756
- # implement actually generates from is unwatched.
4757
- plans = {}
4758
- for name in ("backend-plan.md", "frontend-plan.md", "design.md", "design.ui.md"):
4759
- h = sha(os.path.join(fd, name))
4760
- if h: plans[name] = h
4761
- # An unrecognised verdict is recorded as `gaps`, the one that refuses.
4762
- verdict = os.environ.get("AW_VERDICT", "").strip()
4763
- if verdict not in ("conformant", "routed-gaps", "gaps"):
4764
- verdict = "gaps"
4765
- try: plan_gaps = int(os.environ.get("AW_PLAN_GAPS", "0") or 0)
4766
- except ValueError: plan_gaps = 0
4767
- try: routed = [str(x) for x in json.loads(os.environ.get("AW_ROUTED") or "[]")]
4768
- except Exception: routed = []
4769
- rec = {
4770
- "gated": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
4771
- "gate_mode": os.environ.get("AW_MODE", "master"),
4772
- "spec_version": os.environ.get("AW_VER", "").strip(),
4773
- "verdict": verdict,
4774
- "plan_gaps": plan_gaps,
4775
- "routed_gaps": routed,
4776
- "spec_sha": sha(os.path.join(fd, "spec.md")),
4777
- "model_sha": sha(os.path.join(ctx, "domain-model.md")),
4778
- "plans": plans,
4779
- "note": ("Written by every master-mode analyze-plan run. It records the bytes that were "
4780
- "checked against each other and the verdict on them, not a version — the plans' "
4781
- "**Version**: headers are stamped in lockstep by close-delta and cannot answer "
4782
- "this. implement reads it through plan-gate-check: moved bytes or verdict `gaps` "
4783
- "refuse; `routed-gaps` lists what /appweaver specify still has to lift."),
4784
- }
4785
- p = os.path.join(fd, ".aw-plan-gate.json")
4786
- open(p, "w", encoding="utf-8").write(json.dumps(rec, indent=2) + "\n")
4787
- print(json.dumps({"file": rel(p), "gated": rec["gated"], "spec_version": rec["spec_version"],
4788
- "verdict": verdict, "plan_gaps": plan_gaps, "routed_gaps": len(routed),
4789
- "hashed": sorted(plans.keys()), "count": len(plans), "ok": True}))
4790
- PY
4815
+ local core="$SCRIPT_DIR/../python/aw-plangate.py"
4816
+ if [ ! -f "$core" ]; then echo "plan-gate-stamp: core not found at $core" >&2; return 1; fi
4817
+ if ! command -v python3 >/dev/null 2>&1; then echo "plan-gate-stamp: python3 is required" >&2; return 1; fi
4818
+ python3 "$core" stamp --feature-dir "$fd" --context-dir "$ctx_dir" --root "$repo_root" "${rest[@]+"${rest[@]}"}"
4791
4819
  }
4792
4820
 
4793
4821
  cmd_plan_gate_check() {
@@ -4802,64 +4830,25 @@ cmd_plan_gate_check() {
4802
4830
  local repo_root; repo_root=$(get_repo_root)
4803
4831
  local fd; fd=$(_resolve_feature_dir "$feature_dir_arg" "$repo_root") || { echo "plan-gate-check: no feature set and no --feature-dir" >&2; return 1; }
4804
4832
  local ctx_dir; ctx_dir=$(_context_dir_for_feature "$fd" "$repo_root")
4805
- AW_FD="$fd" AW_CTX="$ctx_dir" AW_ROOT="$repo_root" python3 - <<'PY'
4806
- import hashlib, json, os
4807
- fd = os.environ["AW_FD"]; ctx = os.environ["AW_CTX"]; root = os.environ["AW_ROOT"]
4808
- def sha(p):
4809
- try:
4810
- with open(p, "rb") as f: return hashlib.sha256(f.read()).hexdigest()
4811
- except Exception: return ""
4812
- p = os.path.join(fd, ".aw-plan-gate.json")
4813
- rec = None
4814
- try: rec = json.load(open(p, encoding="utf-8"))
4815
- except Exception: rec = None
4816
- out = {"file": os.path.relpath(p, root), "tracked": bool(rec), "gate_fresh": False,
4817
- "verdict": "", "may_generate": True, "plan_gaps": 0, "routed_gaps": [], "routed_csv": "",
4818
- "spec_moved": False, "model_moved": False, "plans_changed": [],
4819
- "plans_changed_csv": "", "gated": "", "spec_version": "", "reason": "", "ok": True}
4820
- if not rec:
4821
- # Never gated. Not a failure and not a stale plan — an unknown, and the only honest
4822
- # thing to report about a tree that has not run analyze-plan since this op existed.
4823
- out["reason"] = ("no .aw-plan-gate.json — this feature has not been through analyze-plan "
4824
- "in master mode since the gate began leaving a receipt")
4825
- else:
4826
- out["gated"] = rec.get("gated", "")
4827
- out["spec_version"] = rec.get("spec_version", "")
4828
- # A receipt from before the verdict existed was only ever written on a clean pass.
4829
- out["verdict"] = rec.get("verdict") or "conformant"
4830
- out["plan_gaps"] = rec.get("plan_gaps") or 0
4831
- out["routed_gaps"] = [str(x) for x in (rec.get("routed_gaps") or [])]
4832
- out["routed_csv"] = "; ".join(out["routed_gaps"])
4833
- out["spec_moved"] = bool(rec.get("spec_sha")) and rec["spec_sha"] != sha(os.path.join(fd, "spec.md"))
4834
- # A model recorded as absent that is still absent has not moved. A context that
4835
- # GAINED a domain model since the gate has moved: check 4 never ran against it.
4836
- out["model_moved"] = rec.get("model_sha", "") != sha(os.path.join(ctx, "domain-model.md"))
4837
- changed = []
4838
- for name, was in sorted((rec.get("plans") or {}).items()):
4839
- if sha(os.path.join(fd, name)) != was:
4840
- changed.append(name)
4841
- out["plans_changed"] = changed
4842
- out["plans_changed_csv"] = ", ".join(changed)
4843
- out["gate_fresh"] = not (out["spec_moved"] or out["model_moved"] or changed)
4844
- # Moved bytes refuse whatever the verdict was; unmoved bytes refuse only on `gaps`.
4845
- out["may_generate"] = out["gate_fresh"] and out["verdict"] != "gaps"
4846
- if not out["gate_fresh"]:
4847
- bits = []
4848
- if out["spec_moved"]: bits.append("spec.md has changed")
4849
- if out["model_moved"]: bits.append("domain-model.md has changed")
4850
- if changed: bits.append("%s changed (a re-derive by `plan main`, or a hand edit)" % ", ".join(changed))
4851
- out["reason"] = "since analyze-plan checked them on %s: %s" % (out["gated"], "; ".join(bits))
4852
- elif out["verdict"] == "gaps":
4853
- out["reason"] = ("analyze-plan on %s left %s gap(s) a plan rewrite closes, unrepaired"
4854
- % (out["gated"], out["plan_gaps"] or "some"))
4855
- elif out["verdict"] == "routed-gaps":
4856
- out["reason"] = ("the spec, the model and every gated plan are byte-identical to what "
4857
- "analyze-plan checked on %s; it left %d gap(s) only /appweaver specify "
4858
- "can close: %s" % (out["gated"], len(out["routed_gaps"]), out["routed_csv"]))
4859
- else:
4860
- out["reason"] = "the spec, the model and every gated plan are byte-identical to what analyze-plan passed on %s" % out["gated"]
4861
- print(json.dumps(out))
4862
- PY
4833
+ local core="$SCRIPT_DIR/../python/aw-plangate.py"
4834
+ if [ ! -f "$core" ]; then echo "plan-gate-check: core not found at $core" >&2; return 1; fi
4835
+ if ! command -v python3 >/dev/null 2>&1; then echo "plan-gate-check: python3 is required" >&2; return 1; fi
4836
+ python3 "$core" check --feature-dir "$fd" --context-dir "$ctx_dir" --root "$repo_root"
4837
+ }
4838
+
4839
+ # ─────────────────────────────── gap-route ─────────────────────────────────
4840
+ # Split a gate's findings by the command that CLOSES each one. The checks already tag
4841
+ # every finding with `fix`; this turns that tag into the routing a gate message prints,
4842
+ # so a plan defect is never sent to `implement` — which generates FROM the plan and would
4843
+ # reproduce it. Thin wrapper: the logic is in aw-gaps.py, called identically by both shells.
4844
+ cmd_gap_route() {
4845
+ case "${1:-}" in
4846
+ --help|-h) echo "gap-route --gaps '<json>'... [--cr <id>] -> {total, high_count, <route>_count, <route>_refs, <route>_text, summary, routes}"; return 0 ;;
4847
+ esac
4848
+ local core="$SCRIPT_DIR/../python/aw-gaps.py"
4849
+ if [ ! -f "$core" ]; then echo "gap-route: core not found at $core" >&2; return 1; fi
4850
+ if ! command -v python3 >/dev/null 2>&1; then echo "gap-route: python3 is required" >&2; return 1; fi
4851
+ python3 "$core" route "$@"
4863
4852
  }
4864
4853
 
4865
4854
  # ──────────────────────────────── plan-coverage ─────────────────────────────
@@ -4870,7 +4859,7 @@ PY
4870
4859
  # (python/aw-plan-coverage.py), called identically by both shells.
4871
4860
  cmd_plan_coverage() {
4872
4861
  case "${1:-}" in
4873
- --help|-h) echo "plan-coverage --plan <path> --kind backend|frontend [--mode bootstrap|delta] [--source-path <p>] [--test-path <p>] [--reported <json list>]... [--list-only] -> {ok, total, names[], names_csv, elements[], gaps[], gap_names[], gap_count, gaps_text, message}"; return 0 ;;
4862
+ --help|-h) echo "plan-coverage --plan <path> --kind backend|frontend [--mode bootstrap|delta] [--source-path <p>] [--test-path <p>] [--reported <json list>]... [--generated <json list>] [--list-only] -> {ok, outcome, total, names[], names_csv, elements[], gaps[], gap_names[], gap_count, gaps_text, message}"; return 0 ;;
4874
4863
  esac
4875
4864
  local repo_root; repo_root=$(get_repo_root)
4876
4865
  local core="$SCRIPT_DIR/../python/aw-plan-coverage.py"
@@ -5039,6 +5028,7 @@ case "$sub" in
5039
5028
  plan-gate-stamp) cmd_plan_gate_stamp "$@" ;;
5040
5029
  plan-gate-check) cmd_plan_gate_check "$@" ;;
5041
5030
  plan-coverage) cmd_plan_coverage "$@" ;;
5031
+ gap-route) cmd_gap_route "$@" ;;
5042
5032
  checklist-read) cmd_checklist_read "$@" ;;
5043
5033
  checklist-apply) cmd_checklist_apply "$@" ;;
5044
5034
  detect-project) cmd_detect_project "$@" ;;
@@ -5054,6 +5044,7 @@ case "$sub" in
5054
5044
  setup-constitution) cmd_setup_constitution "$@" ;;
5055
5045
  compose-constitution) cmd_compose_constitution "$@" ;;
5056
5046
  constitution-verify) cmd_constitution_verify "$@" ;;
5047
+ principles-list) cmd_principles_list "$@" ;;
5057
5048
  agent-context-update) cmd_agent_context_update "$@" ;;
5058
5049
  ""|--help|-h)
5059
5050
  sed -n '2,37p' "$0" | sed 's/^#\{0,1\} \{0,1\}//' ;;