aw-spec-cli 0.1.26 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/assets/.agents/skills/appweaver/SKILL.md +7 -47
  2. package/assets/.appweaver/ORCH-DIALECT.md +13 -9
  3. package/assets/.appweaver/RULE-PARTITION.md +3 -3
  4. package/assets/.appweaver/STACK-PROFILES.md +188 -101
  5. package/assets/.appweaver/domain.json +1 -1
  6. package/assets/.appweaver/orchestrator/AGENT-PROFILE.md +38 -10
  7. package/assets/.appweaver/orchestrator/LAUNCHER.md +88 -0
  8. package/assets/.appweaver/orchestrator/README.md +120 -53
  9. package/assets/.appweaver/orchestrator/adapters/claude_agent_sdk.py +1 -1
  10. package/assets/.appweaver/orchestrator/modules/domain-select.orch.yaml +6 -4
  11. package/assets/.appweaver/orchestrator/modules/foundation-lane.orch.yaml +237 -0
  12. package/assets/.appweaver/orchestrator/modules/foundation-pack.orch.yaml +349 -0
  13. package/assets/.appweaver/orchestrator/modules/foundation-spec.orch.yaml +381 -0
  14. package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +4 -4
  15. package/assets/.appweaver/orchestrator/modules/tech-select.orch.yaml +3 -3
  16. package/assets/.appweaver/orchestrator/orchestrator.py +162 -63
  17. package/assets/.appweaver/orchestrator/rev-templates/domain-constitution.skeleton.md +2 -2
  18. package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +2 -2
  19. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +5 -5
  20. package/assets/.appweaver/orchestrator/rev-templates/frontend-rule-file.skeleton.md +1 -1
  21. package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +14 -14
  22. package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +2 -2
  23. package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +1 -1
  24. package/assets/.appweaver/orchestrator/rev-templates/ui-constitution.skeleton.md +3 -3
  25. package/assets/.appweaver/orchestrator/workflows/_smoke.orch.yaml +41 -3
  26. package/assets/.appweaver/orchestrator/workflows/aw-spec-add-component.orch.yaml +1 -1
  27. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +13 -13
  28. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +3 -3
  29. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +3 -3
  30. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +4 -4
  31. package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +2 -2
  32. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +61 -12
  33. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +172 -293
  34. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +29 -28
  35. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +53 -53
  36. package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +2 -2
  37. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +3 -3
  38. package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +4 -4
  39. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-bounded-context.orch.yaml +26 -2
  40. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +21 -15
  41. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +14 -14
  42. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +2 -2
  43. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +3 -3
  44. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +6 -6
  45. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +284 -128
  46. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +224 -70
  47. package/assets/.appweaver/scripts/python/aw-foundation.py +809 -20
  48. package/assets/.appweaver/scripts/python/aw-inventory.py +28 -21
  49. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +2 -2
  50. package/assets/.appweaver/scripts/python/aw-rev-constitution.py +6 -6
  51. package/assets/.appweaver/scripts/python/aw-stack.py +195 -89
  52. package/assets/.appweaver/stack-profiles/angular.json +2 -1
  53. package/assets/.appweaver/stack-profiles/blazor-wasm.json +2 -1
  54. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +16 -3
  55. package/assets/.appweaver/stack-profiles/go-chi.json +6 -1
  56. package/assets/.appweaver/stack-profiles/java-spring.json +12 -5
  57. package/assets/.appweaver/stack-profiles/nextjs.json +2 -1
  58. package/assets/.appweaver/stack-profiles/node-express.json +8 -2
  59. package/assets/.appweaver/stack-profiles/python-fastapi.json +9 -2
  60. package/assets/.appweaver/stack-profiles/react-vite.json +2 -1
  61. package/assets/.appweaver/stack-profiles/sveltekit.json +2 -1
  62. package/assets/.appweaver/stack-profiles/vue-vite.json +2 -1
  63. package/assets/.appweaver/stacks.json +37 -1
  64. package/assets/.appweaver/templates/constitution-ui-template.md +2 -2
  65. package/assets/.claude/skills/appweaver/SKILL.md +7 -46
  66. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.Reference.rules.json +31 -0
  67. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.Reference.rules.json +16 -0
  68. package/assets/foundations/foundation.spec.template.txt +73 -0
  69. package/assets/foundations/java/Hexagonal/Hexagonal.pack.txt +1 -1
  70. package/assets/foundations/java/Hexagonal/unpack-foundation.sh +4 -5
  71. package/assets/foundations/node/NextAppRouter/NextAppRouter.spec.txt +68 -0
  72. package/assets/guardrails/README.md +31 -17
  73. package/assets/guardrails/{projects/dotnet-default-project → project-specific}/domain-rule-bindings.md +5 -5
  74. package/assets/guardrails/{projects/dotnet-default-project → project-specific}/tech-rule-bindings.md +151 -114
  75. package/assets/guardrails/technology/dotnet-webapi/PACKAGING.md +58 -4
  76. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +1 -1
  77. package/assets/guardrails/technology/dotnet-webapi/dependency-policy.md +1 -1
  78. package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +1 -1
  79. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +1 -1
  80. package/assets/guardrails/technology/dotnet-webapi/testing.md +1 -1
  81. package/assets/guardrails/technology/nextjs/api-client.md +1 -1
  82. package/assets/guardrails/technology/nextjs/component-architecture.md +1 -1
  83. package/assets/guardrails/technology/nextjs/component-conventions.md +2 -2
  84. package/assets/guardrails/technology/nextjs/design-system.md +1 -1
  85. package/assets/guardrails/technology/nextjs/frontend-testing.md +1 -1
  86. package/assets/guardrails/technology/nextjs/routing-navigation.md +1 -1
  87. package/assets/guardrails/technology/nextjs/ui-constitution.md +3 -3
  88. package/package.json +1 -1
  89. package/assets/.agents/skills/appweaver-demo/SKILL.md +0 -59
  90. package/assets/.appweaver/orchestrator/.smoke-out/smoke.md +0 -6
  91. package/assets/.appweaver/orchestrator/workflows/aw-spec-demo.orch.yaml +0 -100
  92. package/assets/.claude/skills/appweaver-demo/SKILL.md +0 -59
  93. package/assets/.kilocode/agent.json +0 -28
  94. package/assets/.kilocode/workflows/appweaver-demo.md +0 -59
  95. package/assets/.kilocode/workflows/appweaver.md +0 -55
  96. /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/domain/domain-requirements.md +0 -0
  97. /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/frontend/ui-requirements.md +0 -0
  98. /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/service/technical-requirements.md +0 -0
@@ -16,7 +16,7 @@ into a single cross-platform python core, with two brownfield additions:
16
16
  are SIGNALS, not decisions: the workflow's agent draws the real aggregate
17
17
  boundaries. This is the "inventory following aggregate root" the pipeline
18
18
  needs — one aggregate ≈ one candidate specs/<Feature>/ vertical slice.
19
- * frontend_signals — candidate FRONTEND LANE roots derived from component /
19
+ * frontend_signals — candidate FRONTEND lane root folders derived from component /
20
20
  view / stylesheet clustering around frontend build configs. Like the
21
21
  aggregate signals these are SIGNALS, not decisions, and they are what lets
22
22
  /appweaver onboard answer "does this repo have a UI, and where" from the
@@ -24,7 +24,7 @@ into a single cross-platform python core, with two brownfield additions:
24
24
  * ui_attribution — which frontend files serve which aggregate, so a slice's OKF
25
25
  concept carries its UI surface and not just its backend files. Needed because
26
26
  the lane scoping below builds aggregate_signals from backend files ALONE.
27
- Searches the FRONTEND SOURCE PATH only (declared root, else the roots the scan
27
+ Searches the FRONTEND LANE ROOT FOLDER only (declared root, else the roots the scan
28
28
  found) and does not run at all where the repo has no frontend — a backend repo's
29
29
  email templates and docs stylesheets are not its UI. Signals again: files
30
30
  matching no aggregate are reported unattributed rather than forced into one.
@@ -153,7 +153,7 @@ UI_COMPONENT_RE = re.compile(
153
153
  UI_MARKUP_RE = re.compile(r"\.(html?|xhtml)$", re.IGNORECASE)
154
154
  UI_STYLE_RE = re.compile(r"\.(css|scss|sass|less|styl|pcss)$", re.IGNORECASE)
155
155
  # A build/tooling config only a frontend carries. The strongest separable-lane
156
- # signal there is: the directory holding one IS the lane root.
156
+ # signal there is: the directory holding one IS the lane root folder.
157
157
  UI_BUILD_CFG_RE = re.compile(
158
158
  r"(^|/)(angular\.json|vite\.config\.[cm]?[jt]s|next\.config\.[cm]?[jt]s|"
159
159
  r"nuxt\.config\.[cm]?[jt]s|svelte\.config\.[cm]?[jt]s|vue\.config\.[cm]?[jt]s|"
@@ -223,7 +223,7 @@ UI_STRONG_MIN = 5
223
223
 
224
224
 
225
225
  def frontend_signals(files, churn_map):
226
- """Derive CANDIDATE frontend lane roots — the deterministic answer to "does
226
+ """Derive CANDIDATE frontend lane root folders — the deterministic answer to "does
227
227
  this repo have a UI, and where". Each root reports its evidence, what KIND of
228
228
  UI it is, and whether it is SEPARABLE (owns a subtree) or server-rendered
229
229
  inside the service tree.
@@ -471,9 +471,16 @@ def is_excluded(path, config_excludes):
471
471
 
472
472
  # The agent integrations the kit installs beside a project, and the agent-instruction
473
473
  # files at the repository root. Configuration for an assistant, never evidence of how
474
- # the project's code is built.
475
- KIT_AGENT_DIRS = (".claude", ".agents", ".kilocode")
476
- KIT_AGENT_FILES = ("CLAUDE.md", "AGENTS.md")
474
+ # the project's code is built. Each agent folder is excluded whole, the project's own
475
+ # assistant setup in it included. `.kilocode` is where older kit installs put Kilo.
476
+ # `.github/` also holds the project's CI, so only the kit's launcher there is excluded,
477
+ # by its exact path.
478
+ KIT_AGENT_DIRS = (
479
+ ".claude", ".agents", ".kilo", ".kilocode", ".cursor", ".copilot", ".devin",
480
+ ".cline", ".clinerules", ".roo", ".opencode", ".factory", ".augment",
481
+ ".continue", ".kiro", ".codex", ".gemini", ".amp",
482
+ )
483
+ KIT_AGENT_FILES = ("CLAUDE.md", "AGENTS.md", ".github/prompts/appweaver.prompt.md")
477
484
 
478
485
 
479
486
  def kit_excludes(root):
@@ -485,11 +492,11 @@ def kit_excludes(root):
485
492
  `guardrails/` and `foundations/` are ordinary names a project may use for its own
486
493
  code, so each is excluded only when it has the kit's shape: `guardrails/` holding
487
494
  one of the kit's rule tiers, `foundations/` holding a scaffold pack (a directory with
488
- a `*.pack.txt`). The agent directories and root instruction files are excluded
495
+ a `*.pack.txt`). The agent directories and agent instruction files are excluded
489
496
  whenever present."""
490
497
  names = [d for d in KIT_AGENT_DIRS if os.path.isdir(os.path.join(root, d))]
491
498
  g = os.path.join(root, "guardrails")
492
- if any(os.path.isdir(os.path.join(g, t)) for t in ("projects", "technology", "domain")):
499
+ if any(os.path.isdir(os.path.join(g, t)) for t in ("project-specific", "technology", "domain")):
493
500
  names.append("guardrails")
494
501
  f = os.path.join(root, "foundations")
495
502
  if os.path.isdir(f):
@@ -518,7 +525,7 @@ def _ignore_rules(dirpath, base):
518
525
  """The patterns one .gitignore contributes, as (dir_rel, pattern, dir_only, anchored).
519
526
 
520
527
  Best-effort by design — negation, character classes and escaped spaces are not
521
- honoured. It exists for the one case git cannot answer: a lane root the HOST
528
+ honoured. It exists for the one case git cannot answer: a lane root folder the HOST
522
529
  repository ignores wholesale, whose own nested .gitignore files still describe
523
530
  what inside it is build output."""
524
531
  rules = []
@@ -589,11 +596,11 @@ def list_files(root, config_excludes, lane_roots=()):
589
596
  same ignore rules git itself would, including nested .gitignore files, so build
590
597
  output (obj/, bin/, node_modules/) stays out exactly as before.
591
598
 
592
- A DECLARED LANE ROOT IS READ EVEN WHEN THE HOST REPO IGNORES IT. A codebase under
599
+ A DECLARED LANE ROOT FOLDER IS READ EVEN WHEN THE HOST REPO IGNORES IT. A codebase under
593
600
  assessment is often nested inside a repository that deliberately keeps it out of
594
601
  version control — a test bed, a vendored drop, a legacy tree copied in for analysis.
595
602
  git then answers "nothing here" for the one subtree select-stack said IS the code.
596
- The declaration outranks the host's ignore rule: an ignored lane root is walked from
603
+ The declaration outranks the host's ignore rule: an ignored lane root folder is walked from
597
604
  disk, with build output pruned and the lane's OWN nested .gitignore files applied.
598
605
  """
599
606
  if git(root, "rev-parse", "--git-dir"):
@@ -2941,7 +2948,7 @@ def attribute_ui_files(signals, fe_files, churn_map, ui_roots, feature_aggs=None
2941
2948
  the architecture note instead of being forced into a slice.
2942
2949
 
2943
2950
  SCOPED TO THE FRONTEND TREE, AND SKIPPED ENTIRELY WITHOUT ONE. `ui_roots` is the
2944
- lane's source path — the declared one, or the roots the scan actually found. Two
2951
+ lane root folder — the declared one, or the roots the scan actually found. Two
2945
2952
  guards, because neither is sufficient alone: with no UI root declared scope_to_lanes
2946
2953
  hands back the WHOLE REPO as the frontend list, and the file-shape regexes match
2947
2954
  plenty of things a backend repo legitimately owns — a server-side email template, a
@@ -3091,7 +3098,7 @@ def bundle_state(bundle_dir):
3091
3098
 
3092
3099
 
3093
3100
  def _lane_prefix(p):
3094
- """A declared source path as a '/'-terminated repo-relative prefix, or ''."""
3101
+ """A declared lane root folder as a '/'-terminated repo-relative prefix, or ''."""
3095
3102
  s = (p or "").strip().replace("\\", "/").lstrip("./").lstrip("/")
3096
3103
  if s and not s.endswith("/"):
3097
3104
  s += "/"
@@ -3177,7 +3184,7 @@ def cmd_scan(args):
3177
3184
 
3178
3185
  # A DECLARED UI lane is a lane. The strength test below UI_STRONG_MIN exists to
3179
3186
  # stop a scan INVENTING one from a wwwroot/ of loose scripts; it has no business
3180
- # overruling a person who declared the lane and named its source path. What the
3187
+ # overruling a person who declared the lane and named its lane root folder. What the
3181
3188
  # evidence still decides is what goes IN the governance — and a declared root
3182
3189
  # holding no recognisable UI files is reported as exactly that, so onboard can
3183
3190
  # say so rather than quietly authoring a UI half out of nothing.
@@ -3193,7 +3200,7 @@ def cmd_scan(args):
3193
3200
  "build_configs": [], "route_files": [], "store_files": []}]
3194
3201
 
3195
3202
  # Give every aggregate candidate the UI that serves it — searching the FRONTEND
3196
- # SOURCE PATH and nothing else. The declared root is the authority; absent a
3203
+ # LANE ROOT FOLDER and nothing else. The declared root is the authority; absent a
3197
3204
  # declaration the roots the scan actually found stand in, which keeps a repo whose
3198
3205
  # UI is server-rendered inside the service tree covered. With neither, this repo
3199
3206
  # has no frontend and the pass does not run at all.
@@ -3295,7 +3302,7 @@ def cmd_scan(args):
3295
3302
  "frontend_present": "true" if fsig["present"] else "false",
3296
3303
  "frontend_separable": "true" if fsig["separable"] else "false",
3297
3304
  "frontend_root_count": len(fsig["roots"]),
3298
- # The PRIMARY lane root — the strongest candidate, and the single value a
3305
+ # The PRIMARY lane root folder — the strongest candidate, and the single value a
3299
3306
  # lane declaration needs. Empty when no root reached lane strength.
3300
3307
  "frontend_root": next((r["root"] for r in fsig["roots"]
3301
3308
  if r["confidence"] == "strong"), ""),
@@ -4526,10 +4533,10 @@ def main():
4526
4533
  ps.add_argument("--bundle", help="OKF bundle dir to check for existing curation (default: <root>/knowledge).")
4527
4534
  ps.add_argument("--config", help="okf-config.yml whose exclude: list is honored.")
4528
4535
  ps.add_argument("--service-root", dest="service_root", default="",
4529
- help="The service lane's declared source path. Backend signals are "
4536
+ help="The declared lane root folder of the service lane. Backend signals are "
4530
4537
  "derived from this subtree only.")
4531
4538
  ps.add_argument("--ui-root", dest="ui_root", default="",
4532
- help="The UI lane's declared source path. Frontend signals are derived "
4539
+ help="The declared lane root folder of the UI lane. Frontend signals are derived "
4533
4540
  "from this subtree only, and it is excluded from the backend's — "
4534
4541
  "so neither lane's substrate carries the other's files.")
4535
4542
  ps.add_argument("--map", help="Machine map whose features attribute UI feature folders "
@@ -4541,9 +4548,9 @@ def main():
4541
4548
  pp.add_argument("--root", help="Codebase to scan (default: CWD).")
4542
4549
  pp.add_argument("--config", help="okf-config.yml whose exclude: list is honored.")
4543
4550
  pp.add_argument("--service-root", dest="service_root", default="",
4544
- help="The service lane's declared source path.")
4551
+ help="The declared lane root folder of the service lane.")
4545
4552
  pp.add_argument("--ui-root", dest="ui_root", default="",
4546
- help="The UI lane's declared source path.")
4553
+ help="The declared lane root folder of the UI lane.")
4547
4554
  pp.add_argument("--strategy", default="",
4548
4555
  help="Pass A strategy: structure | reference | persistence. "
4549
4556
  "Default: the highest-coverage AVAILABLE strategy, ties "
@@ -15,7 +15,7 @@ lane's code names it and whether the generating model reported it done.
15
15
  TWO KINDS OF ELEMENT.
16
16
  checked an identifier the code must contain — an entity, value object, domain
17
17
  event or use-case (backend); a component (frontend); an identifier in a
18
- delta plan's ADDED table. Searched for in the lane's source path, test
18
+ delta plan's ADDED table. Searched for in the lane root folder, test
19
19
  files excluded. Entities and value objects must appear as a whole word.
20
20
  Every other checked name may carry a suffix, because stacks add one by
21
21
  convention: `MedicationOrderPlaced` is realised as
@@ -246,7 +246,7 @@ def is_test_file(rel):
246
246
 
247
247
 
248
248
  def scan(repo, source_path, test_path, names):
249
- """{name: [relative files naming it]} over the lane's source path, tests excluded.
249
+ """{name: [relative files naming it]} over the lane root folder, tests excluded.
250
250
 
251
251
  `names` holds (name, whole) keys: whole is True when the name must appear as a whole
252
252
  word, False when an identifier that STARTS with it counts.
@@ -15,7 +15,7 @@ amends it additively from its own code. The standalone
15
15
  steps without calling this core.
16
16
 
17
17
  This core is the MODEL-FREE half: it gathers the project framing the OKF bundle holds
18
- and scaffolds the `guardrails/projects/brf-<domain>/` project directory; the calling
18
+ and scaffolds the `guardrails/brf-<domain>/` project directory; the calling
19
19
  workflow's authoring agents fill its rule files + the split constitution. Governance
20
20
  stance is AS-BUILT (the constitution reflects what the code does, not an aspiration).
21
21
 
@@ -36,7 +36,7 @@ STDERR so they never corrupt the JSON contract:
36
36
  works; it answers with this framing and no longer lists features.
37
37
 
38
38
  project-setup --name <domain> [--fallback <name>] [--root <dir>]
39
- -> slugify + `brf-` prefix the domain, create guardrails/projects/<slug>/ with its
39
+ -> slugify + `brf-` prefix the domain, create guardrails/<slug>/ with its
40
40
  service/ frontend/ domain/ subfolders when the project does not exist yet.
41
41
  Without --name, an ACTIVE `brf-` project (.appweaver/domain.json) that already
42
42
  holds rule files wins over the fallback name, so every later context amends the
@@ -196,7 +196,7 @@ def _active_brf_project(root):
196
196
  return ""
197
197
  if not name.startswith("brf-"):
198
198
  return ""
199
- d = os.path.join(root, "guardrails", "projects", name)
199
+ d = os.path.join(root, "guardrails", name)
200
200
  return name if _md_files(d, root) else ""
201
201
 
202
202
 
@@ -214,7 +214,7 @@ def cmd_pack_setup(args):
214
214
  slug = re.sub(r"^brf-+", "", slug)
215
215
  domain_slug = "brf-" + slug
216
216
 
217
- pack_dir = os.path.join(root, "guardrails", "projects", domain_slug)
217
+ pack_dir = os.path.join(root, "guardrails", domain_slug)
218
218
  # A project that already holds .md files was authored by an earlier context: the
219
219
  # calling workflow AMENDS it rather than authoring it again. The walk covers the
220
220
  # whole subtree, because a project's rule files live in its service/ frontend/
@@ -248,7 +248,7 @@ def cmd_pack_setup(args):
248
248
  "root": root,
249
249
  }
250
250
  print(
251
- f"[govern-backward] project: guardrails/projects/{domain_slug}/ "
251
+ f"[govern-backward] project: guardrails/{domain_slug}/ "
252
252
  f"({'EXISTS — ' + ', '.join(f'{k} {len(v)}' for k, v in tiers.items()) if present else 'created'})",
253
253
  file=sys.stderr,
254
254
  )
@@ -273,7 +273,7 @@ def main(argv):
273
273
  # the folder it creates is a PROJECT, so the primary name says so, and a caller
274
274
  # written against either spelling reaches the same code.
275
275
  for name in ("project-setup", "pack-setup"):
276
- ps = sub.add_parser(name, help="create the guardrails/projects/brf-<domain>/ project dir")
276
+ ps = sub.add_parser(name, help="create the guardrails/brf-<domain>/ project dir")
277
277
  ps.add_argument("--name", default="")
278
278
  ps.add_argument("--fallback", default="")
279
279
  ps.add_argument("--root", default="")