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
@@ -124,6 +124,15 @@ class Stop(Exception):
124
124
  pass
125
125
 
126
126
 
127
+ class Return(Exception):
128
+ """A gate's `return` outcome: end the module being included and carry on with the
129
+ step after the `include`. A Stop ends the whole skill, which is wrong for a caller
130
+ that runs one module per lane — the frontend lane must still run when the backend's
131
+ scaffold was declined. At the top level of a workflow it ends the run normally, with
132
+ the outputs resolved, rather than as a stop."""
133
+ pass
134
+
135
+
127
136
  class Suspend(Exception):
128
137
  """Raised in checkpoint mode when a generation is needed: the interpreter
129
138
  persists state and exits so the INVOKING agent can fulfill the request
@@ -231,9 +240,12 @@ class Orchestrator:
231
240
  # paths an agent_generate reply named as written that were not on disk —
232
241
  # run-cumulative, reported at the end beside missing_required.
233
242
  self.reported_missing = []
243
+ # agent_author targets a --dry-run left unwritten; required_files gates
244
+ # count them as present, so a dry run reaches the steps after them.
245
+ self.dry_run_unwritten = []
234
246
  self.last_result = {}
235
247
  # agent-specific transport profile (None in mock mode). This engine is
236
- # general — the profile (loaded from a .claude/.agents/.kilocode file)
248
+ # general — the profile (loaded from a .claude/.agents/.kilo file)
237
249
  # describes HOW to spawn that agent's fresh generation CLI.
238
250
  self.profile = getattr(opts, "profile", None)
239
251
  # checkpoint transport (run-skill.py method): replay + response cache
@@ -545,7 +557,7 @@ class Orchestrator:
545
557
  Returns (structured, raw).
546
558
 
547
559
  The engine is agent-agnostic: the profile (agent-specific, from a
548
- .claude/.agents/.kilocode file) declares the CLI to spawn. A SEPARATE
560
+ .claude/.agents/.kilo file) declares the CLI to spawn. A SEPARATE
549
561
  process → a brand-new conversation that does NOT inherit this
550
562
  orchestrator's transcript. The profile maps our neutral knobs
551
563
  (system / user / tools / permission_mode / isolation / model / budget)
@@ -553,6 +565,34 @@ class Orchestrator:
553
565
  if self.opts.transport == "mock":
554
566
  # No model call. The banner (already printed) is the demonstration.
555
567
  return None, None
568
+ argv, stdin_data = self._fresh_command(system, user, tools, permission_mode, model)
569
+
570
+ if self.opts.dry_run:
571
+ # Safety net: every model step prints its command through _mock_step and
572
+ # takes its mock result, so a dry run never reaches here — and must never
573
+ # execute if a new caller does.
574
+ self._emit_fresh_command(argv, stdin_data)
575
+ raise Stop("dry-run: command shown above (nothing executed)")
576
+
577
+ proc = subprocess.run(argv, input=stdin_data, capture_output=True, text=True)
578
+ if proc.returncode != 0:
579
+ raise Stop(f"fresh agent failed ({proc.returncode}): {proc.stderr.strip()[:500]}")
580
+ try:
581
+ obj = json.loads(proc.stdout)
582
+ except Exception:
583
+ return _extract_json(proc.stdout), proc.stdout # text-output fallback
584
+ raw = ""
585
+ for k in (self.profile or {}).get("result_keys", ["result"]):
586
+ if isinstance(obj, dict) and obj.get(k):
587
+ raw = obj[k]
588
+ break
589
+ if not raw:
590
+ raw = proc.stdout
591
+ return _extract_json(raw), raw
592
+
593
+ def _fresh_command(self, system, user, tools, permission_mode, model=None):
594
+ """The argv (and stdin, or None) for one fresh generation, built from the
595
+ PROFILE's neutral knobs — shared by the real run and the dry-run print."""
556
596
  prof = self.profile or {}
557
597
  argv = [prof["bin"]] + list(prof.get("args", []))
558
598
  model = model or self.opts.model # per-step `model:` beats --model
@@ -584,29 +624,32 @@ class Orchestrator:
584
624
  argv += [umode.get("flag", "-p"), prompt_user]
585
625
  else:
586
626
  stdin_data = prompt_user
587
-
588
- if self.opts.dry_run:
589
- shown = [a if len(a) < 120 else f"<{len(a)} chars>" for a in argv]
590
- self.emit(f"[dry-run] {prof.get('name', '?')} would run:\n " + " ".join(shown))
591
- if stdin_data is not None:
592
- self.emit(f"[dry-run] + stdin: {len(stdin_data)} chars")
593
- raise Stop("dry-run: command shown above (nothing executed)")
594
-
595
- proc = subprocess.run(argv, input=stdin_data, capture_output=True, text=True)
596
- if proc.returncode != 0:
597
- raise Stop(f"fresh agent failed ({proc.returncode}): {proc.stderr.strip()[:500]}")
598
- try:
599
- obj = json.loads(proc.stdout)
600
- except Exception:
601
- return _extract_json(proc.stdout), proc.stdout # text-output fallback
602
- raw = ""
603
- for k in prof.get("result_keys", ["result"]):
604
- if isinstance(obj, dict) and obj.get(k):
605
- raw = obj[k]
606
- break
607
- if not raw:
608
- raw = proc.stdout
609
- return _extract_json(raw), raw
627
+ return argv, stdin_data
628
+
629
+ def _emit_fresh_command(self, argv, stdin_data):
630
+ """Print the command a dry run would have executed (long args elided)."""
631
+ shown = [a if len(a) < 120 else f"<{len(a)} chars>" for a in argv]
632
+ self.emit(f"[dry-run] {(self.profile or {}).get('name', '?')} would run:\n "
633
+ + " ".join(shown))
634
+ if stdin_data is not None:
635
+ self.emit(f"[dry-run] + stdin: {len(stdin_data)} chars")
636
+
637
+ def _mock_step(self, strat, system, user, tools, permission_mode, model=None):
638
+ """True when a model step takes its mock result instead of running: the
639
+ mock transport, or --dry-run under spawn/sdk/sdk-apply. A dry run first
640
+ prints the request the profile would send, then lets the step carry on
641
+ with its mock result, so one dry run prints every model step's command.
642
+ The agent strategy is left alone: the invoking agent fulfils it."""
643
+ if self.opts.transport == "mock":
644
+ return True
645
+ if not self.opts.dry_run or strat == "agent":
646
+ return False
647
+ if strat in ("sdk", "sdk-apply"):
648
+ self._emit_sdk_request(system, user, tools, permission_mode)
649
+ else:
650
+ self._emit_fresh_command(
651
+ *self._fresh_command(system, user, tools, permission_mode, model))
652
+ return True
610
653
 
611
654
  def _ensure_sdk_module(self, module_name, package_name):
612
655
  """Import the profile-declared sdk module; if missing, optionally pip
@@ -663,6 +706,35 @@ class Orchestrator:
663
706
  raise Stop(f"sdk adapter {path} must export generate(request)")
664
707
  return fn
665
708
 
709
+ def _sdk_config(self):
710
+ """(module, package, adapter) from the profile's sdk block — a dry run
711
+ checks them too, so it catches a profile that cannot run sdk/sdk-apply."""
712
+ if not self.profile:
713
+ raise Stop("strategy 'sdk'/'sdk-apply' needs --agent-profile — the "
714
+ "profile's sdk block declares the SDK to use (the engine "
715
+ "has no default agent)")
716
+ sdk_cfg = self.profile.get("sdk") or {}
717
+ module_name = sdk_cfg.get("module")
718
+ package_name = sdk_cfg.get("package")
719
+ adapter_rel = sdk_cfg.get("adapter")
720
+ missing = [k for k, v in (("module", module_name), ("package", package_name),
721
+ ("adapter", adapter_rel)) if not v]
722
+ if missing:
723
+ raise Stop(f"agent profile '{self.profile.get('name', '?')}': sdk block "
724
+ f"is missing {', '.join(missing)} — strategy 'sdk'/'sdk-apply' "
725
+ "is fully profile-driven (no built-in default); declare the "
726
+ "sdk module/package to import and the adapter file that "
727
+ "drives it")
728
+ return module_name, package_name, adapter_rel
729
+
730
+ def _emit_sdk_request(self, system, user, tools, permission_mode):
731
+ """Print the SDK request a dry run would have sent."""
732
+ module_name, package_name, adapter_rel = self._sdk_config()
733
+ self.emit(f"[dry-run] sdk would run: module={module_name} "
734
+ f"package={package_name} adapter={adapter_rel} "
735
+ f"system={len(system)} chars user={len(user)} chars "
736
+ f"tools={tools or '(none)'} permission_mode={permission_mode}")
737
+
666
738
  def _run_sdk(self, system, user, tools, permission_mode, model=None):
667
739
  """Strategy 1/2 executor: an isolated generation via the agent's SDK.
668
740
  Each call is a NEW session — the only context is the payload we pass
@@ -680,28 +752,12 @@ class Orchestrator:
680
752
  SDK's calling convention (sync or async)
681
753
  Everything is imported lazily so the engine stays stdlib-only
682
754
  otherwise."""
683
- if not self.profile:
684
- raise Stop("strategy 'sdk'/'sdk-apply' needs --agent-profile — the "
685
- "profile's sdk block declares the SDK to use (the engine "
686
- "has no default agent)")
687
- sdk_cfg = self.profile.get("sdk") or {}
688
- module_name = sdk_cfg.get("module")
689
- package_name = sdk_cfg.get("package")
690
- adapter_rel = sdk_cfg.get("adapter")
691
- missing = [k for k, v in (("module", module_name), ("package", package_name),
692
- ("adapter", adapter_rel)) if not v]
693
- if missing:
694
- raise Stop(f"agent profile '{self.profile.get('name', '?')}': sdk block "
695
- f"is missing {', '.join(missing)} — strategy 'sdk'/'sdk-apply' "
696
- "is fully profile-driven (no built-in default); declare the "
697
- "sdk module/package to import and the adapter file that "
698
- "drives it")
755
+ module_name, package_name, adapter_rel = self._sdk_config()
756
+ sdk_cfg = self.profile.get("sdk") or {} # handed to the adapter whole
699
757
 
700
758
  if self.opts.dry_run:
701
- self.emit(f"[dry-run] sdk would run: module={module_name} "
702
- f"package={package_name} adapter={adapter_rel} "
703
- f"system={len(system)} chars user={len(user)} chars "
704
- f"tools={tools or '(none)'} permission_mode={permission_mode}")
759
+ # Safety net, as in _run_fresh: _mock_step prints the request first.
760
+ self._emit_sdk_request(system, user, tools, permission_mode)
705
761
  raise Stop("dry-run: sdk request shown above (nothing executed)")
706
762
 
707
763
  self._ensure_sdk_module(module_name, package_name)
@@ -839,7 +895,10 @@ class Orchestrator:
839
895
  user, ctx_files = self._assemble_user(instr, extra=extra)
840
896
  self._print_envelope("agent_generate", system, sys_files, user, ctx_files, tools, strat)
841
897
 
842
- if self.opts.transport == "mock":
898
+ # sdk-apply's generation is the read-only one; the dry run shows that request
899
+ sent_tools, sent_mode = (("Read", "readonly") if strat == "sdk-apply"
900
+ else (tools, permission_mode))
901
+ if self._mock_step(strat, system, user, sent_tools, sent_mode, step.get("model")):
843
902
  structured = self._mock_generate(task)
844
903
  elif strat == "agent":
845
904
  self._suspend("agent_generate", ck, system, user, tools, permission_mode)
@@ -853,7 +912,9 @@ class Orchestrator:
853
912
  else: # spawn
854
913
  structured, _ = self._run_fresh(system, user, tools, permission_mode,
855
914
  model=step.get("model"))
856
- if self.checkpoint:
915
+ # A dry run's result is a mock: caching it would let a later run without
916
+ # --dry-run replay it as if a model had produced it.
917
+ if self.checkpoint and not self.opts.dry_run:
857
918
  self.cache[ck] = structured
858
919
  structured = self._written_only(step, structured)
859
920
  self.last_result = structured
@@ -893,7 +954,7 @@ class Orchestrator:
893
954
  # No file tools by default: the files under review are already in the payload.
894
955
  self._print_envelope("agent_review", system, sys_files, user, ctx_files, tools, strat)
895
956
 
896
- if self.opts.transport == "mock":
957
+ if self._mock_step(strat, system, user, tools, "readonly", step.get("model")):
897
958
  # mock: pass if every reviewed file exists
898
959
  ok = all(os.path.exists(r) for r in review_of)
899
960
  structured = {"pass": ok, "issues": ([] if ok else ["missing output file(s)"])}
@@ -907,7 +968,7 @@ class Orchestrator:
907
968
  structured, _ = self._run_fresh(system, user, tools, "readonly",
908
969
  model=step.get("model"))
909
970
  self.last_result = structured
910
- if self.checkpoint:
971
+ if self.checkpoint and not self.opts.dry_run: # never cache a dry run's mock
911
972
  self.cache[ck] = structured
912
973
  self._bind(step, structured)
913
974
  self.emit(f"[agent_review] strategy={strat or 'mock'} "
@@ -988,10 +1049,23 @@ class Orchestrator:
988
1049
  self.emit(f"[execute_script] -> {json.dumps(data)[:200]}")
989
1050
 
990
1051
  # -- F2: gates ---------------------------------------------------------------
1052
+ def _gate_failed(self, p, outcome, msg):
1053
+ """Apply a failed gate's outcome: `stop` ends the skill, `return` ends the module
1054
+ being included (see Return), anything else continues. `message_var` binds the
1055
+ resolved message first, so a caller that continues or returns can still report
1056
+ why — a message that only reaches the run log is not seen by the operator."""
1057
+ if p.get("message_var"):
1058
+ self.vars[p["message_var"]] = msg
1059
+ if outcome == "stop":
1060
+ raise Stop(msg)
1061
+ if outcome == "return":
1062
+ raise Return(msg)
1063
+
991
1064
  def act_verify_context(self, step):
992
1065
  """F2 — the four decl gate forms: required_files (+allow_missing),
993
1066
  require_non_empty (+on_empty), require_true (+on_false),
994
- require_value/equals (+on_mismatch). Messages interpolate {{vars}}."""
1067
+ require_value/equals (+on_mismatch). Messages interpolate {{vars}}.
1068
+ Outcomes: stop | return | continue; `message_var` binds a failed gate's message."""
995
1069
  p = step.get("parameters") or step
996
1070
  if p.get("require_non_empty"):
997
1071
  var = p["require_non_empty"]
@@ -999,8 +1073,7 @@ class Orchestrator:
999
1073
  if not val:
1000
1074
  msg = self.resolve(p.get("on_empty_message", f"{var} is empty"))
1001
1075
  self.emit(f"[verify_context] {var} is empty: {msg}")
1002
- if p.get("on_empty") == "stop":
1003
- raise Stop(msg)
1076
+ self._gate_failed(p, p.get("on_empty"), msg)
1004
1077
  else:
1005
1078
  self.emit(f"[verify_context] {var} non-empty")
1006
1079
  if p.get("require_true"):
@@ -1011,8 +1084,7 @@ class Orchestrator:
1011
1084
  if not truthy:
1012
1085
  msg = self.resolve(p.get("on_false_message", f"{var} is not satisfied"))
1013
1086
  self.emit(f"[verify_context] gate failed ({var}): {msg}")
1014
- if p.get("on_false", "stop") == "stop":
1015
- raise Stop(msg)
1087
+ self._gate_failed(p, p.get("on_false", "stop"), msg)
1016
1088
  else:
1017
1089
  self.emit(f"[verify_context] gate ok ({var})")
1018
1090
  if p.get("require_value"):
@@ -1022,14 +1094,15 @@ class Orchestrator:
1022
1094
  if actual != expected:
1023
1095
  msg = self.resolve(p.get("on_mismatch_message", f"{var} != {expected}"))
1024
1096
  self.emit(f"[verify_context] value gate failed ({var}={actual!r}): {msg}")
1025
- if p.get("on_mismatch", "stop") == "stop":
1026
- raise Stop(msg)
1097
+ self._gate_failed(p, p.get("on_mismatch", "stop"), msg)
1027
1098
  else:
1028
1099
  self.emit(f"[verify_context] value gate ok ({var}=={expected})")
1029
1100
  for rf in p.get("required_files", []) or []:
1030
1101
  rf = self.resolve(rf)
1031
1102
  if rf in self.loaded or os.path.exists(rf):
1032
1103
  self.emit(f"[verify_context] present {rf}")
1104
+ elif rf in self.dry_run_unwritten:
1105
+ self.emit(f"[verify_context] present {rf} (dry-run: its author step did not write it)")
1033
1106
  elif p.get("allow_missing"):
1034
1107
  self.emit(f"[verify_context] missing (allowed) {rf}")
1035
1108
  else:
@@ -1124,7 +1197,7 @@ class Orchestrator:
1124
1197
  system, sys_files = self._assemble_system(p.get("include_standards", True))
1125
1198
  user, ctx_files = self._assemble_user(instr, extra=extra)
1126
1199
  self._print_envelope("agent_author", system, sys_files, user, ctx_files, tools, strat)
1127
- if self.opts.transport == "mock":
1200
+ if self._mock_step(strat, system, user, tools, "readonly", step.get("model")):
1128
1201
  content = self._mock_author(target, task)
1129
1202
  elif strat == "agent":
1130
1203
  self._suspend("author", ck, system, user, tools, "readonly",
@@ -1148,7 +1221,16 @@ class Orchestrator:
1148
1221
  if not content.strip():
1149
1222
  raise Stop(f"step {step.get('id')}: agent_author returned empty content")
1150
1223
  if self.opts.dry_run:
1224
+ # The target can be a real project file, so a dry run never writes it.
1225
+ # A later required_files gate counts it as present, and the step's
1226
+ # outputs still bind, so the run reaches the steps after it.
1151
1227
  self.emit(f"[agent_author] dry-run: not writing {target}")
1228
+ if target not in self.dry_run_unwritten:
1229
+ self.dry_run_unwritten.append(target)
1230
+ structured = {"status": "ok", "files": [target],
1231
+ "summary": f"dry-run: {target} not written"}
1232
+ self.last_result = structured
1233
+ self._bind(step, structured)
1152
1234
  return
1153
1235
  self._write_target(target, content)
1154
1236
  structured = {"status": "ok", "files": [target], "summary": f"authored {target}"}
@@ -1205,14 +1287,14 @@ class Orchestrator:
1205
1287
  system, sys_files = self._assemble_system(p.get("include_standards", True))
1206
1288
  user, ctx_files = self._assemble_user(instr, extra=extra)
1207
1289
  self._print_envelope("agent_query", system, sys_files, user, ctx_files, tools, strat)
1208
- if self.opts.transport == "mock":
1290
+ if self._mock_step(strat, system, user, tools, "readonly", step.get("model")):
1209
1291
  structured = self.resolve_value(p.get("mock")) or {}
1210
1292
  elif strat == "agent":
1211
1293
  self._suspend("agent_query", ck, system, user, tools, "readonly")
1212
1294
  else:
1213
1295
  structured = self._structured_fresh(step, strat, system, user, tools)
1214
1296
  self.last_result = structured
1215
- if self.checkpoint:
1297
+ if self.checkpoint and not self.opts.dry_run: # never cache a dry run's mock
1216
1298
  self.cache[ck] = structured
1217
1299
  self._bind(step, structured)
1218
1300
  self.emit(f"[agent_query] strategy={strat or 'mock'} "
@@ -1381,7 +1463,8 @@ class Orchestrator:
1381
1463
  """F9 — run a module .orch.yaml inline: shares vars, envelope, sticky
1382
1464
  standards, the caller's scripts map and the replay cache. `with:` seeds
1383
1465
  module inputs; cache keys are namespaced under the include id. Stop
1384
- propagates to the caller (module gates stop the whole skill)."""
1466
+ propagates to the caller (module gates stop the whole skill); a gate's
1467
+ `return` outcome ends only this module and the caller carries on."""
1385
1468
  module_path = self.resolve(step.get("module", ""))
1386
1469
  for k, v in (step.get("with", {}) or {}).items():
1387
1470
  self.vars[k] = self.resolve_value(v)
@@ -1398,6 +1481,8 @@ class Orchestrator:
1398
1481
  self.key_stack.append("inc:" + step.get("id", "include"))
1399
1482
  try:
1400
1483
  self._run_steps(mod.get("workflow", []) or [])
1484
+ except Return as r:
1485
+ self.emit(f"[include] {module_path} returned early: {str(r)[:200]}")
1401
1486
  finally:
1402
1487
  self.key_stack.pop()
1403
1488
 
@@ -1494,7 +1579,10 @@ class Orchestrator:
1494
1579
 
1495
1580
  def run(self):
1496
1581
  self._bind_inputs()
1497
- self._run_steps(self.wf.get("workflow", []) or [])
1582
+ try:
1583
+ self._run_steps(self.wf.get("workflow", []) or [])
1584
+ except Return as r:
1585
+ self.emit(f"[return] workflow ended early: {str(r)[:200]}")
1498
1586
  return {"status": "ok", "outputs": self.resolve(self.wf.get("outputs", {}) or {}),
1499
1587
  "vars": self.vars, "missing_required": list(self.missing_required),
1500
1588
  "reported_not_written": list(self.reported_missing)}
@@ -1510,6 +1598,7 @@ KNOWN_ACTIONS = {
1510
1598
  "dispatch_hooks", "user_select", "ask", "loop", "break", "include",
1511
1599
  }
1512
1600
  STRATEGIES = {"sdk", "sdk-apply", "spawn", "agent"}
1601
+ GATE_OUTCOMES = {"stop", "return", "continue"}
1513
1602
  # engine-produced vars a workflow may reference without declaring: the loop/ask
1514
1603
  # bindings, plus the context-assembly miss report (_collect_files, item H).
1515
1604
  _SPECIAL_VARS = {"selection", "answer", "iteration",
@@ -1606,6 +1695,8 @@ def validate_workflow(wf, base_dir, path=""):
1606
1695
  produced.update((s.get("vars") or s.get("parameters") or {}).keys())
1607
1696
  elif act == "build_context":
1608
1697
  produced.update((s.get("vars") or {}).keys())
1698
+ elif act == "verify_context" and p.get("message_var"):
1699
+ produced.add(p["message_var"])
1609
1700
  elif act == "loop":
1610
1701
  produced.add(s.get("as", "item"))
1611
1702
  produced.add(s.get("as", "item") + "_index")
@@ -1650,6 +1741,13 @@ def validate_workflow(wf, base_dir, path=""):
1650
1741
  warnings.append(f"{loc}: ask has no options source (free-text input)")
1651
1742
  if action == "user_select" and not p.get("options_from_variable"):
1652
1743
  errors.append(f"{loc}: user_select needs options_from_variable")
1744
+ if action == "verify_context":
1745
+ # Any value other than stop/return continues, so a misspelt outcome is a gate
1746
+ # that silently never fires.
1747
+ for k in ("on_empty", "on_false", "on_mismatch"):
1748
+ if p.get(k) is not None and p[k] not in GATE_OUTCOMES:
1749
+ errors.append(f"{loc}: {k} '{p[k]}' is not a gate outcome "
1750
+ f"(known: {', '.join(sorted(GATE_OUTCOMES))})")
1653
1751
  if action == "include":
1654
1752
  m = step.get("module")
1655
1753
  if not m:
@@ -1766,13 +1864,14 @@ def main(argv=None):
1766
1864
  "real = spawn the agent profile's CLI per generation")
1767
1865
  ap.add_argument("--agent-profile", default="", dest="agent_profile",
1768
1866
  help="path to an agent transport profile JSON "
1769
- "(.claude/.agents/.kilocode/...); implies --transport real")
1867
+ "(.claude/.agents/.kilo/...); implies --transport real")
1770
1868
  ap.add_argument("--isolation", choices=["bare", "fresh"], default="bare",
1771
1869
  help="selects the profile's isolation.<mode> args (bare = strongest)")
1772
1870
  ap.add_argument("--model", default="", help="model alias/name for the fresh agent")
1773
1871
  ap.add_argument("--budget", default="", help="value for the profile's budget_flag, if it declares one")
1774
1872
  ap.add_argument("--dry-run", action="store_true", dest="dry_run",
1775
- help="print the exact command the profile would run, without executing")
1873
+ help="print the exact command the profile would run for every model "
1874
+ "step, without executing any; each step then takes its mock result")
1776
1875
  ap.add_argument("--sdk-install", action="store_true", dest="sdk_install",
1777
1876
  help="if the profile's sdk module isn't importable, pip install "
1778
1877
  "the package its sdk block declares, then continue")
@@ -1,6 +1,6 @@
1
1
  <!--
2
2
  rev-templates/domain-constitution.skeleton.md — SHAPE-ONLY reference for the
3
- domain-constitution SOURCE (guardrails/projects/[project]/domain/domain-constitution.md),
3
+ domain-constitution SOURCE (guardrails/[project]/domain/domain-constitution.md),
4
4
  the human-readable
5
5
  charter the domain half is authored from (so the normal /appweaver
6
6
  constitution can re-derive it later). It shows the Project-Identity + Domain-
@@ -60,4 +60,4 @@ Extends technology [principle ref]. Evidence: [ids]. → [rule file(s)]
60
60
 
61
61
  ## Domain Requirements Pointer
62
62
 
63
- → guardrails/projects/[project]/domain/domain-requirements.md
63
+ → guardrails/[project]/domain/domain-requirements.md
@@ -27,8 +27,8 @@ generated_by: aw-spec-onboard/[VERSION]
27
27
  =====================================================================
28
28
  [DOMAIN: subsection pointers]
29
29
  =====================================================================
30
- [DOMAIN: <subsection>] -> guardrails/projects/[project]/domain/[generated-file].md
31
- [DOMAIN: <subsection>] -> guardrails/projects/[project]/domain/[generated-file].md
30
+ [DOMAIN: <subsection>] -> guardrails/[project]/domain/[generated-file].md
31
+ [DOMAIN: <subsection>] -> guardrails/[project]/domain/[generated-file].md
32
32
  [DOMAIN: Platform Topology] -> below
33
33
 
34
34
  =====================================================================
@@ -19,7 +19,7 @@ domain principles), and the **gates** (conformance checklist). **plan** and **im
19
19
  **NO** domain file — the domain reaches them solely through the spec's ids and the context's
20
20
  domain-model.md. This is the rule partition; preserve it exactly.
21
21
 
22
- - All paths are workspace-relative and live in `guardrails/projects/[project]/domain/`.
22
+ - All paths are workspace-relative and live in `guardrails/[project]/domain/`.
23
23
  - `resolve-domain --command <name>` collects the `- ` bullets under `## /<name>`.
24
24
 
25
25
  ---
@@ -27,13 +27,13 @@ domain-model.md. This is the rule partition; preserve it exactly.
27
27
  ## /aw-spec-constitution
28
28
  <!-- Domain identity / regulatory / interop sources that become domain principles in
29
29
  constitution.domain.md. -->
30
- - guardrails/projects/[project]/domain/[identity-or-requirements-file].md
31
- - guardrails/projects/[project]/domain/[each generated domain-principle source].md
30
+ - guardrails/[project]/domain/[identity-or-requirements-file].md
31
+ - guardrails/[project]/domain/[each generated domain-principle source].md
32
32
 
33
33
  ## /aw-spec-specify
34
34
  <!-- The FULL generated domain set. Specify models the domain and LIFTS the overlay constraints
35
35
  into the spec's CN-/DC-/IC- (plan/implement never see these files). -->
36
- - guardrails/projects/[project]/domain/[each generated domain rule file].md
36
+ - guardrails/[project]/domain/[each generated domain rule file].md
37
37
 
38
38
  ## /aw-spec-plan
39
39
  _None — plan loads no domain file. Domain reaches plan through the spec and the context's domain-model.md._
@@ -44,7 +44,7 @@ _None — implement loads no domain file. Domain reaches implement through the l
44
44
  ## /aw-spec-gate
45
45
  <!-- Conformance checklist for the gates (analyze-plan · analyze-slice): the domain-derived
46
46
  constraints plan/implement were NOT given as authoring input, VALIDATED against output. -->
47
- - guardrails/projects/[project]/domain/[each *.domain.md overlay to validate against].md
47
+ - guardrails/[project]/domain/[each *.domain.md overlay to validate against].md
48
48
 
49
49
  ---
50
50
 
@@ -6,7 +6,7 @@ concern: "[concern-slug]"
6
6
  <!--
7
7
  rev-templates/frontend-rule-file.skeleton.md — SHAPE-ONLY reference for ONE
8
8
  generated FRONTEND LANE rule file in the project's frontend/ folder
9
- (guardrails/projects/[project]/frontend/<concern>.md). It is the lane's twin of
9
+ (guardrails/[project]/frontend/<concern>.md). It is the lane's twin of
10
10
  technical-rule-file.skeleton.md, and it exists separately because the lane is a
11
11
  different tier: these files are ratified by constitution.ui.md's UX-* series,
12
12
  not by the technical half's numerals, and they govern only the code under the
@@ -7,9 +7,9 @@
7
7
  placeholder for a service/ file the author actually generated from THIS
8
8
  project's CODE.
9
9
 
10
- Wire ONLY files you generated (all under guardrails/projects/[project]/service/).
10
+ Wire ONLY files you generated (all under guardrails/[project]/service/).
11
11
  Never list a file you did not write. Never list a path belonging to another
12
- project (guardrails/projects/<other>/**) — that one is another project's
12
+ project (any other project folder under guardrails/, e.g. guardrails/project-specific/**) — that one is another project's
13
13
  rules, not this one's.
14
14
  -->
15
15
  # [PROJECT_NAME] — Command → Technical-Rule-File Map (as-built)
@@ -21,11 +21,11 @@ files), and the CODE-DERIVED replacement for the greenfield technical map.
21
21
 
22
22
  **The model:** the technical rule files (this project's own architecture / persistence /
23
23
  concurrency / testing / security-posture set, reverse-engineered from the code into
24
- `guardrails/projects/[project]/service/`) are CONFIGURATION — tune this map, not the workflows. The
24
+ `guardrails/[project]/service/`) are CONFIGURATION — tune this map, not the workflows. The
25
25
  technology base itself (`constitution.technical.md`) is NOT listed here — every consumer still
26
26
  refers to it directly, as-is.
27
27
 
28
- - All paths are workspace-relative and live in `guardrails/projects/[project]/service/`.
28
+ - All paths are workspace-relative and live in `guardrails/[project]/service/`.
29
29
  - `resolve-tech --command <name>` collects the `- ` bullets under `## /<name>`.
30
30
  - **`## /aw-spec-specify` is the MODELLING-METHODOLOGY slot** — exactly one file,
31
31
  `service/domain-modelling.md`: HOW this code expresses a domain model. Never an architecture /
@@ -41,39 +41,39 @@ refers to it directly, as-is.
41
41
  (what marks an aggregate root, entity vs value object, where invariants live, the context
42
42
  boundary), NOT what the model is (that is the domain set's domain-model.domain.md, wired
43
43
  via domain-rule-bindings). No technology rule may appear under this heading. -->
44
- - guardrails/projects/[project]/service/domain-modelling.md
44
+ - guardrails/[project]/service/domain-modelling.md
45
45
 
46
46
  ## /aw-spec-plan
47
47
  <!-- The as-built architecture / cross-cutting technical rules the SERVICE lane's plan author
48
48
  mediates the spec against. NO domain file here — domain reaches plan only as spec ids and
49
49
  the context's domain-model.md. -->
50
- - guardrails/projects/[project]/service/[as-built-architecture].md
51
- - guardrails/projects/[project]/service/[each generated architecture/cross-cutting rule].md
50
+ - guardrails/[project]/service/[as-built-architecture].md
51
+ - guardrails/[project]/service/[each generated architecture/cross-cutting rule].md
52
52
 
53
53
  ## /aw-spec-implement
54
54
  <!-- The framework / code-generation rule set the SERVICE lane's generator builds against
55
55
  (from backend-plan.md). -->
56
- - guardrails/projects/[project]/service/[each generated codegen/convention rule].md
56
+ - guardrails/[project]/service/[each generated codegen/convention rule].md
57
57
 
58
58
  ## /aw-spec-implement-delta
59
59
  <!-- The same set for the service lane on the DELTA path. `implement` is one command: it keys
60
60
  this heading when it is generating a change's delta plan and the one above when it is
61
61
  generating the master, so repeat the bullets here to keep both paths identical. -->
62
- - guardrails/projects/[project]/service/[each generated codegen/convention rule].md
62
+ - guardrails/[project]/service/[each generated codegen/convention rule].md
63
63
 
64
64
  ## /aw-spec-build
65
65
  <!-- The layering + convention rules the compile-fixer respects. -->
66
- - guardrails/projects/[project]/service/[as-built-architecture].md
66
+ - guardrails/[project]/service/[as-built-architecture].md
67
67
 
68
68
  ## /aw-spec-analyze-slice
69
69
  <!-- The plan↔code conformance gate's TECHNICAL checklist (validate, don't author). -->
70
- - guardrails/projects/[project]/service/[testing/convention rule to validate against].md
70
+ - guardrails/[project]/service/[testing/convention rule to validate against].md
71
71
 
72
72
  ## /aw-spec-adopt
73
73
  <!-- Brownfield reverse-engineering: the technical REFERENCE set the reversed BACKEND plan
74
74
  mediates the reversed spec ↔ existing code against — the CODE wins, these are reference
75
75
  only. A slice's reversed frontend-plan.md is authored under `## /aw-spec-plan-ui`. -->
76
- - guardrails/projects/[project]/service/[architecture/cross-cutting reference rule].md
76
+ - guardrails/[project]/service/[architecture/cross-cutting reference rule].md
77
77
 
78
78
  ## /aw-spec-constitution
79
79
  <!-- The set the TECHNICAL half of the split constitution is RE-ratified from when /appweaver
@@ -84,12 +84,12 @@ refers to it directly, as-is.
84
84
  service/ files that carry a ratifiable standard, NOT every generated file, and NO worked examples/
85
85
  sample (the ratified half is one declarative line per principle). NO domain file — the domain
86
86
  half's sources are the DOMAIN map's ## /aw-spec-constitution. -->
87
- - guardrails/projects/[project]/service/[ratifiable technical standard].md
87
+ - guardrails/[project]/service/[ratifiable technical standard].md
88
88
 
89
89
  ## /aw-spec-run
90
90
  <!-- The runtime / execution conventions the app-run step honors — add ONLY if the code evidences
91
91
  a runtime/hosting concern file. NOT data-migration (migration is the deferred later phase). -->
92
- - guardrails/projects/[project]/service/[runtime/hosting rule].md
92
+ - guardrails/[project]/service/[runtime/hosting rule].md
93
93
 
94
94
  <!-- THE FRONTEND LANE'S THREE HEADINGS. They are NOT written by the step that authors this
95
95
  map; onboard's frontend rule-set step APPENDS them here, after the technical headings and
@@ -46,8 +46,8 @@ Sync Impact Report
46
46
  - …
47
47
  - Dependent artifacts:
48
48
  - constitution.domain.md — ⚠ authored next; continues at [N+1]
49
- - guardrails/projects/[project]/domain/domain-constitution.md — [✅/⚠] domain charter source
50
- - guardrails/projects/[project]/service/ — [✅/⚠] technical rule set
49
+ - guardrails/[project]/domain/domain-constitution.md — [✅/⚠] domain charter source
50
+ - guardrails/[project]/service/ — [✅/⚠] technical rule set
51
51
  - TODOs: [none mechanical | …]
52
52
  -->
53
53
  # [PROJECT_NAME] Constitution — Technical Base
@@ -6,7 +6,7 @@ concern: "[concern-slug]"
6
6
  <!--
7
7
  rev-templates/technical-rule-file.skeleton.md — SHAPE-ONLY reference for ONE
8
8
  generated technical rule file in the project's service/ folder
9
- (guardrails/projects/[project]/service/<concern>.md). It is the TECHNICAL twin of
9
+ (guardrails/[project]/service/<concern>.md). It is the TECHNICAL twin of
10
10
  domain-rule-file.skeleton.md. Write one such file per TECHNOLOGY concern the
11
11
  CODE ACTUALLY exhibits (evidenced-only, lean) — e.g. as-built-architecture,
12
12
  persistence, concurrency, testing, security-posture, api-surface, eventing,
@@ -59,8 +59,8 @@ Sync Impact Report
59
59
  - Experience concerns DELIBERATELY OMITTED (lack of as-built evidence):
60
60
  - [CONCERN] — [what the code shows instead, or that it shows nothing]
61
61
  - Dependent artifacts:
62
- - guardrails/projects/[project]/frontend/ — [✅/⚠] generated lane rule set
63
- - guardrails/projects/[project]/tech-rule-bindings.md — [✅/⚠] `## /aw-spec-ui-constitution`
62
+ - guardrails/[project]/frontend/ — [✅/⚠] generated lane rule set
63
+ - guardrails/[project]/tech-rule-bindings.md — [✅/⚠] `## /aw-spec-ui-constitution`
64
64
  - TODOs: [none mechanical | …]
65
65
  -->
66
66
  # [PROJECT_NAME] Constitution — User Experience Base
@@ -103,7 +103,7 @@ Sync Impact Report
103
103
  <!-- Form: the lane's AS-BUILT layout under its own root — where components, screens/pages,
104
104
  routing, client state, styling and assets actually live, named as THIS code names them.
105
105
  Do not impose a folder taxonomy the code does not use. This layout sits entirely under
106
- the lane's root and never adds to the service lane's structure. -->
106
+ the lane root folder and never adds to the service lane's structure. -->
107
107
 
108
108
  ## [UI_CONVENTIONS_SECTION_NAME]
109
109