aw-spec-cli 0.1.26 → 0.1.28

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 (137) 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 +11 -12
  4. package/assets/.appweaver/STACK-PROFILES.md +260 -108
  5. package/assets/.appweaver/domain.json +1 -1
  6. package/assets/.appweaver/interpreter/run-skill.py +28 -7
  7. package/assets/.appweaver/orchestrator/AGENT-PROFILE.md +38 -10
  8. package/assets/.appweaver/orchestrator/LAUNCHER.md +88 -0
  9. package/assets/.appweaver/orchestrator/README.md +120 -53
  10. package/assets/.appweaver/orchestrator/adapters/claude_agent_sdk.py +1 -1
  11. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +17 -7
  12. package/assets/.appweaver/orchestrator/modules/domain-select.orch.yaml +6 -4
  13. package/assets/.appweaver/orchestrator/modules/foundation-lane.orch.yaml +237 -0
  14. package/assets/.appweaver/orchestrator/modules/foundation-pack.orch.yaml +392 -0
  15. package/assets/.appweaver/orchestrator/modules/foundation-spec.orch.yaml +548 -0
  16. package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +4 -4
  17. package/assets/.appweaver/orchestrator/modules/tech-select.orch.yaml +3 -3
  18. package/assets/.appweaver/orchestrator/orchestrator.py +162 -63
  19. package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +5 -4
  20. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +3 -1
  21. package/assets/.appweaver/orchestrator/rev-templates/domain-constitution.skeleton.md +2 -2
  22. package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +2 -2
  23. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +21 -13
  24. package/assets/.appweaver/orchestrator/rev-templates/frontend-rule-file.skeleton.md +1 -1
  25. package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +28 -31
  26. package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +2 -2
  27. package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +1 -1
  28. package/assets/.appweaver/orchestrator/rev-templates/ui-constitution.skeleton.md +3 -3
  29. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +4 -1
  30. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +6 -4
  31. package/assets/.appweaver/orchestrator/workflows/_smoke.orch.yaml +41 -3
  32. package/assets/.appweaver/orchestrator/workflows/aw-spec-add-component.orch.yaml +1 -1
  33. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +17 -16
  34. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +7 -1
  35. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +7 -1
  36. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +3 -3
  37. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +3 -3
  38. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +4 -4
  39. package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +2 -2
  40. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +61 -12
  41. package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +1 -1
  42. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +172 -293
  43. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +253 -44
  44. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +72 -71
  45. package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +2 -2
  46. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +38 -14
  47. package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +4 -4
  48. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-bounded-context.orch.yaml +26 -2
  49. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +21 -15
  50. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +14 -14
  51. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +3 -3
  52. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +3 -3
  53. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +6 -6
  54. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +371 -131
  55. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +293 -76
  56. package/assets/.appweaver/scripts/python/aw-foundation.py +885 -20
  57. package/assets/.appweaver/scripts/python/aw-inventory.py +28 -21
  58. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +2 -2
  59. package/assets/.appweaver/scripts/python/aw-rev-constitution.py +6 -6
  60. package/assets/.appweaver/scripts/python/aw-stack.py +676 -99
  61. package/assets/.appweaver/stack-profiles/angular.json +2 -1
  62. package/assets/.appweaver/stack-profiles/blazor-wasm.json +2 -1
  63. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +18 -3
  64. package/assets/.appweaver/stack-profiles/go-chi.json +6 -1
  65. package/assets/.appweaver/stack-profiles/java-spring.json +14 -5
  66. package/assets/.appweaver/stack-profiles/nextjs.json +2 -1
  67. package/assets/.appweaver/stack-profiles/node-express.json +8 -2
  68. package/assets/.appweaver/stack-profiles/python-fastapi.json +9 -2
  69. package/assets/.appweaver/stack-profiles/react-vite.json +2 -1
  70. package/assets/.appweaver/stack-profiles/sveltekit.json +2 -1
  71. package/assets/.appweaver/stack-profiles/vue-vite.json +2 -1
  72. package/assets/.appweaver/stacks.json +40 -1
  73. package/assets/.appweaver/templates/constitution-ui-template.md +2 -2
  74. package/assets/.appweaver/templates/delta-design-template.md +7 -5
  75. package/assets/.appweaver/templates/design-template.md +3 -0
  76. package/assets/.appweaver/templates/ui-delta-design-template.md +1 -1
  77. package/assets/.appweaver/templates/ui-design-template.md +6 -4
  78. package/assets/.claude/skills/appweaver/SKILL.md +7 -46
  79. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.Reference.rules.json +31 -0
  80. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.lane.json +6 -0
  81. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.Reference.rules.json +16 -0
  82. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.lane.json +6 -0
  83. package/assets/foundations/foundation.spec.template.txt +98 -0
  84. package/assets/foundations/java/Hexagonal/Hexagonal.lane.json +6 -0
  85. package/assets/foundations/java/Hexagonal/Hexagonal.pack.txt +8 -1
  86. package/assets/foundations/java/Hexagonal/unpack-foundation.sh +4 -5
  87. package/assets/foundations/node/NextAppRouter/NextAppRouter.spec.txt +68 -0
  88. package/assets/guardrails/README.md +31 -17
  89. package/assets/guardrails/{projects/dotnet-default-project → project-specific}/domain-rule-bindings.md +25 -13
  90. package/assets/guardrails/project-specific/tech-rule-bindings.md +296 -0
  91. package/assets/guardrails/technology/dotnet-webapi/PACKAGING.md +58 -4
  92. package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +1 -1
  93. package/assets/guardrails/technology/dotnet-webapi/auditing.md +1 -1
  94. package/assets/guardrails/technology/dotnet-webapi/authorization.md +1 -1
  95. package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +1 -1
  96. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +1 -1
  97. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +1 -1
  98. package/assets/guardrails/technology/dotnet-webapi/data-classification.md +1 -1
  99. package/assets/guardrails/technology/dotnet-webapi/dependency-policy.md +1 -1
  100. package/assets/guardrails/technology/dotnet-webapi/eventing.md +1 -1
  101. package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +1 -1
  102. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +1 -1
  103. package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +1 -1
  104. package/assets/guardrails/technology/dotnet-webapi/persistence.md +1 -1
  105. package/assets/guardrails/technology/dotnet-webapi/testing.md +1 -1
  106. package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +1 -1
  107. package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +1 -1
  108. package/assets/guardrails/technology/nextjs/accessibility-implementation.md +1 -1
  109. package/assets/guardrails/technology/nextjs/api-client.md +1 -1
  110. package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +1 -1
  111. package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +1 -1
  112. package/assets/guardrails/technology/nextjs/auth-session.md +1 -1
  113. package/assets/guardrails/technology/nextjs/client-security.md +1 -1
  114. package/assets/guardrails/technology/nextjs/component-architecture.md +1 -1
  115. package/assets/guardrails/technology/nextjs/component-conventions.md +2 -2
  116. package/assets/guardrails/technology/nextjs/design-system.md +1 -1
  117. package/assets/guardrails/technology/nextjs/error-handling.md +1 -1
  118. package/assets/guardrails/technology/nextjs/forms-and-validation.md +1 -1
  119. package/assets/guardrails/technology/nextjs/frontend-testing.md +1 -1
  120. package/assets/guardrails/technology/nextjs/i18n.md +1 -1
  121. package/assets/guardrails/technology/nextjs/observability.md +1 -1
  122. package/assets/guardrails/technology/nextjs/performance-budgets.md +1 -1
  123. package/assets/guardrails/technology/nextjs/routing-navigation.md +1 -1
  124. package/assets/guardrails/technology/nextjs/state-management.md +1 -1
  125. package/assets/guardrails/technology/nextjs/ui-constitution.md +3 -3
  126. package/package.json +1 -1
  127. package/assets/.agents/skills/appweaver-demo/SKILL.md +0 -59
  128. package/assets/.appweaver/orchestrator/.smoke-out/smoke.md +0 -6
  129. package/assets/.appweaver/orchestrator/workflows/aw-spec-demo.orch.yaml +0 -100
  130. package/assets/.claude/skills/appweaver-demo/SKILL.md +0 -59
  131. package/assets/.kilocode/agent.json +0 -28
  132. package/assets/.kilocode/workflows/appweaver-demo.md +0 -59
  133. package/assets/.kilocode/workflows/appweaver.md +0 -55
  134. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +0 -271
  135. /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/domain/domain-requirements.md +0 -0
  136. /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/frontend/ui-requirements.md +0 -0
  137. /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")
@@ -129,11 +129,12 @@ migration is needed (yes/no); any contract change other systems couple to.]
129
129
  <!-- What this change adds to or alters in the service's PUBLISHED surface — the
130
130
  only part of this document the frontend lane may bind against. `None` when
131
131
  the surface is unchanged. Omit the section entirely where the code has no
132
- frontend lane. -->
132
+ frontend lane. The operation id is the endpoint's name in the published API
133
+ document; an endpoint that already has one keeps it. -->
133
134
 
134
- | Contract | Change | Shape | Serves |
135
- |---|---|---|---|
136
- | `[contract]` | [added / changed] | [request → response field shapes] | IC-[nnn] |
135
+ | Operation id | Contract | Change | Shape | Serves |
136
+ |---|---|---|---|---|
137
+ | `[OperationId]` | `[contract]` | [added / changed] | [request → response field shapes] | IC-[nnn] |
137
138
 
138
139
  ## Other open deltas
139
140
 
@@ -169,7 +169,9 @@
169
169
  the titles are the code's answer — the slots below are illustrative
170
170
  positions, not a required set. In each: the components that live there,
171
171
  their behaviour in signature form, and the contracts they expose or
172
- require. -->
172
+ require. A component that PUBLISHES an endpoint names it with its route, verb
173
+ and operation id — the endpoint's name in the published API document, which
174
+ the frontend plan cites and implement looks the endpoint up by. -->
173
175
 
174
176
  ### 2.1 [PROJECT_STRUCTURAL_AXIS_NAME]
175
177
 
@@ -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
  =====================================================================
@@ -1,8 +1,8 @@
1
1
  <!--
2
2
  rev-templates/domain-rule-bindings.skeleton.md — SHAPE-ONLY reference for the project's
3
3
  domain-rule-bindings.md (resolve-domain reads it by that exact name — the symmetric
4
- twin of tech-rule-bindings.md, which resolve-tech reads). It shows the five per-command headings, the RULE PARTITION (plan +
5
- implement get NO domain file), the bullet shape, and the quick-reference
4
+ twin of tech-rule-bindings.md, which resolve-tech reads). It shows the seven headings (plan and
5
+ implement keyed per lane), the RULE PARTITION (neither lane of plan or implement gets a domain file), the bullet shape, and the quick-reference
6
6
  table. It lists NO real rule files — every bullet is a placeholder for a file
7
7
  the author actually generated from THIS project's evidence.
8
8
 
@@ -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,24 +27,30 @@ 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
- ## /aw-spec-plan
39
- _None — plan loads no domain file. Domain reaches plan through the spec and the context's domain-model.md._
38
+ ## /aw-spec-plan-backend
39
+ _None — the service lane of plan loads no domain file. Domain reaches it through the spec and the context's domain-model.md._
40
40
 
41
- ## /aw-spec-implement
42
- _None — implement loads no domain file. Domain reaches implement through the lane plans and the context's domain-model.md._
41
+ ## /aw-spec-plan-frontend
42
+ _None — the frontend lane of plan loads no domain file. Domain reaches it through the spec and the context's domain-model.md._
43
+
44
+ ## /aw-spec-implement-backend
45
+ _None — the service lane of implement loads no domain file. Domain reaches it through backend-plan.md and the context's domain-model.md._
46
+
47
+ ## /aw-spec-implement-frontend
48
+ _None — the frontend lane of implement loads no domain file. Domain reaches it through frontend-plan.md and the context's domain-model.md._
43
49
 
44
50
  ## /aw-spec-gate
45
51
  <!-- Conformance checklist for the gates (analyze-plan · analyze-slice): the domain-derived
46
52
  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
53
+ - guardrails/[project]/domain/[each *.domain.md overlay to validate against].md
48
54
 
49
55
  ---
50
56
 
@@ -54,6 +60,8 @@ _None — implement loads no domain file. Domain reaches implement through the l
54
60
  |---|---|---|
55
61
  | /aw-spec-constitution | [n] | domain principles |
56
62
  | /aw-spec-specify | [n] | author into spec (lift → CN-/DC-/IC-) |
57
- | /aw-spec-plan | 0 | none — the spec and the domain model carry the domain |
58
- | /aw-spec-implement | 0 | none — the lane plans and the domain model carry the domain |
63
+ | /aw-spec-plan-backend | 0 | none — the spec and the domain model carry the domain |
64
+ | /aw-spec-plan-frontend | 0 | none — the spec and the domain model carry the domain |
65
+ | /aw-spec-implement-backend | 0 | none — backend-plan.md and the domain model carry the domain |
66
+ | /aw-spec-implement-frontend | 0 | none — frontend-plan.md and the domain model carry the domain |
59
67
  | /aw-spec-gate | [n] | conformance checklist (validate, don't author) |
@@ -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