arkaos 4.0.1 → 4.1.0

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 (200) hide show
  1. package/README.md +2 -2
  2. package/VERSION +1 -1
  3. package/arka/SKILL.md +16 -22
  4. package/arka/skills/flow/SKILL.md +108 -234
  5. package/config/claude-agents/eduardo-copy.md +53 -0
  6. package/config/claude-agents/francisca-tech.md +52 -0
  7. package/config/claude-agents/marta-cqo.md +52 -0
  8. package/config/claude-agents/paulo-tech-lead.md +50 -0
  9. package/config/cognition/schedules.yaml +15 -0
  10. package/config/constitution.yaml +17 -21
  11. package/config/hooks/_lib/workflow-classifier.sh +1 -1
  12. package/config/hooks/post-tool-use.sh +40 -539
  13. package/config/hooks/pre-tool-use.sh +32 -281
  14. package/config/hooks/session-start.sh +8 -10
  15. package/config/hooks/stop.sh +30 -425
  16. package/config/hooks/user-prompt-submit.sh +42 -459
  17. package/core/__pycache__/__init__.cpython-312.pyc +0 -0
  18. package/core/cognition/__pycache__/__init__.cpython-312.pyc +0 -0
  19. package/core/cognition/__pycache__/dreaming.cpython-313.pyc +0 -0
  20. package/core/cognition/__pycache__/retrieval.cpython-312.pyc +0 -0
  21. package/core/cognition/capture/__pycache__/__init__.cpython-312.pyc +0 -0
  22. package/core/cognition/capture/__pycache__/store.cpython-312.pyc +0 -0
  23. package/core/cognition/dreaming.py +5 -0
  24. package/core/cognition/insights/__pycache__/__init__.cpython-312.pyc +0 -0
  25. package/core/cognition/insights/__pycache__/store.cpython-312.pyc +0 -0
  26. package/core/cognition/memory/__pycache__/__init__.cpython-312.pyc +0 -0
  27. package/core/cognition/memory/__pycache__/obsidian.cpython-312.pyc +0 -0
  28. package/core/cognition/memory/__pycache__/schemas.cpython-312.pyc +0 -0
  29. package/core/cognition/memory/__pycache__/vector.cpython-312.pyc +0 -0
  30. package/core/cognition/memory/__pycache__/writer.cpython-312.pyc +0 -0
  31. package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
  32. package/core/cognition/scheduler/daemon.py +10 -0
  33. package/core/forge/__init__.py +2 -0
  34. package/core/forge/__pycache__/__init__.cpython-313.pyc +0 -0
  35. package/core/forge/__pycache__/orchestrator.cpython-313.pyc +0 -0
  36. package/core/forge/__pycache__/persistence.cpython-313.pyc +0 -0
  37. package/core/forge/__pycache__/renderer.cpython-313.pyc +0 -0
  38. package/core/forge/__pycache__/runtime_dispatcher.cpython-313.pyc +0 -0
  39. package/core/forge/__pycache__/schema.cpython-313.pyc +0 -0
  40. package/core/forge/orchestrator.py +50 -27
  41. package/core/forge/persistence.py +16 -0
  42. package/core/forge/renderer.py +16 -0
  43. package/core/forge/runtime_dispatcher.py +34 -19
  44. package/core/forge/schema.py +12 -0
  45. package/core/governance/__pycache__/__init__.cpython-312.pyc +0 -0
  46. package/core/governance/__pycache__/closing_marker_check.cpython-313.pyc +0 -0
  47. package/core/governance/__pycache__/compliance_telemetry.cpython-313.pyc +0 -0
  48. package/core/governance/__pycache__/constitution.cpython-312.pyc +0 -0
  49. package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
  50. package/core/governance/__pycache__/meta_tag_check.cpython-313.pyc +0 -0
  51. package/core/governance/__pycache__/qg_verdict.cpython-313.pyc +0 -0
  52. package/core/governance/__pycache__/review_workflow.cpython-313.pyc +0 -0
  53. package/core/governance/__pycache__/skill_proposer.cpython-313.pyc +0 -0
  54. package/core/governance/closing_marker_check.py +9 -5
  55. package/core/governance/compliance_telemetry.py +1 -1
  56. package/core/governance/compliance_telemetry_cli.py +1 -1
  57. package/core/governance/evidence_checks.py +421 -0
  58. package/core/governance/meta_tag_check.py +36 -0
  59. package/core/governance/qg_verdict.py +78 -0
  60. package/core/governance/review_workflow.py +34 -1
  61. package/core/governance/skill_proposer.py +2 -1
  62. package/core/hooks/__init__.py +12 -0
  63. package/core/hooks/__pycache__/__init__.cpython-312.pyc +0 -0
  64. package/core/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  65. package/core/hooks/__pycache__/_shared.cpython-312.pyc +0 -0
  66. package/core/hooks/__pycache__/_shared.cpython-313.pyc +0 -0
  67. package/core/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
  68. package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
  69. package/core/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
  70. package/core/hooks/__pycache__/pre_tool_use.cpython-313.pyc +0 -0
  71. package/core/hooks/__pycache__/stop.cpython-312.pyc +0 -0
  72. package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
  73. package/core/hooks/__pycache__/user_prompt_submit.cpython-312.pyc +0 -0
  74. package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  75. package/core/hooks/_shared.py +110 -0
  76. package/core/hooks/post_tool_use.py +689 -0
  77. package/core/hooks/pre_tool_use.py +267 -0
  78. package/core/hooks/stop.py +395 -0
  79. package/core/hooks/user_prompt_submit.py +600 -0
  80. package/core/jobs/__pycache__/__init__.cpython-312.pyc +0 -0
  81. package/core/jobs/__pycache__/auto_doc_worker.cpython-312.pyc +0 -0
  82. package/core/jobs/__pycache__/manager.cpython-312.pyc +0 -0
  83. package/core/knowledge/__pycache__/__init__.cpython-312.pyc +0 -0
  84. package/core/knowledge/__pycache__/chunker.cpython-312.pyc +0 -0
  85. package/core/knowledge/__pycache__/embedder.cpython-312.pyc +0 -0
  86. package/core/knowledge/__pycache__/embedder.cpython-313.pyc +0 -0
  87. package/core/knowledge/__pycache__/vector_store.cpython-312.pyc +0 -0
  88. package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
  89. package/core/knowledge/embedder.py +118 -11
  90. package/core/knowledge/vector_store.py +111 -14
  91. package/core/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  92. package/core/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
  93. package/core/runtime/__pycache__/base.cpython-312.pyc +0 -0
  94. package/core/runtime/__pycache__/base.cpython-313.pyc +0 -0
  95. package/core/runtime/__pycache__/capabilities_cli.cpython-312.pyc +0 -0
  96. package/core/runtime/__pycache__/capabilities_cli.cpython-313.pyc +0 -0
  97. package/core/runtime/__pycache__/claude_code.cpython-312.pyc +0 -0
  98. package/core/runtime/__pycache__/claude_code.cpython-313.pyc +0 -0
  99. package/core/runtime/__pycache__/codex_cli.cpython-312.pyc +0 -0
  100. package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
  101. package/core/runtime/__pycache__/cost_governor.cpython-312.pyc +0 -0
  102. package/core/runtime/__pycache__/cost_governor.cpython-313.pyc +0 -0
  103. package/core/runtime/__pycache__/cursor.cpython-312.pyc +0 -0
  104. package/core/runtime/__pycache__/cursor.cpython-313.pyc +0 -0
  105. package/core/runtime/__pycache__/gemini_cli.cpython-312.pyc +0 -0
  106. package/core/runtime/__pycache__/gemini_cli.cpython-313.pyc +0 -0
  107. package/core/runtime/__pycache__/llm_cost_telemetry.cpython-312.pyc +0 -0
  108. package/core/runtime/__pycache__/llm_provider.cpython-312.pyc +0 -0
  109. package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
  110. package/core/runtime/__pycache__/llm_retry.cpython-312.pyc +0 -0
  111. package/core/runtime/__pycache__/llm_retry.cpython-313.pyc +0 -0
  112. package/core/runtime/__pycache__/native_usage.cpython-312.pyc +0 -0
  113. package/core/runtime/__pycache__/native_usage.cpython-313.pyc +0 -0
  114. package/core/runtime/__pycache__/ollama_provider.cpython-312.pyc +0 -0
  115. package/core/runtime/__pycache__/path_resolver.cpython-312.pyc +0 -0
  116. package/core/runtime/__pycache__/pricing.cpython-312.pyc +0 -0
  117. package/core/runtime/__pycache__/pricing.cpython-313.pyc +0 -0
  118. package/core/runtime/__pycache__/registry.cpython-312.pyc +0 -0
  119. package/core/runtime/__pycache__/registry.cpython-313.pyc +0 -0
  120. package/core/runtime/__pycache__/squad_orchestrator.cpython-312.pyc +0 -0
  121. package/core/runtime/__pycache__/user_paths.cpython-312.pyc +0 -0
  122. package/core/runtime/base.py +29 -0
  123. package/core/runtime/capabilities_cli.py +74 -0
  124. package/core/runtime/claude_code.py +8 -0
  125. package/core/runtime/codex_cli.py +205 -37
  126. package/core/runtime/cost_governor.py +184 -0
  127. package/core/runtime/cursor.py +8 -0
  128. package/core/runtime/gemini_cli.py +8 -0
  129. package/core/runtime/llm_provider.py +32 -13
  130. package/core/runtime/llm_retry.py +233 -0
  131. package/core/runtime/native_usage.py +186 -0
  132. package/core/runtime/pricing.py +9 -0
  133. package/core/runtime/registry.py +3 -0
  134. package/core/shared/__pycache__/__init__.cpython-312.pyc +0 -0
  135. package/core/shared/__pycache__/safe_session_id.cpython-312.pyc +0 -0
  136. package/core/shared/__pycache__/test_evidence.cpython-313-pytest-9.1.1.pyc +0 -0
  137. package/core/shared/__pycache__/test_evidence.cpython-313.pyc +0 -0
  138. package/core/shared/test_evidence.py +34 -0
  139. package/core/synapse/__pycache__/__init__.cpython-312.pyc +0 -0
  140. package/core/synapse/__pycache__/agent_experiences_layer.cpython-312.pyc +0 -0
  141. package/core/synapse/__pycache__/cache.cpython-312.pyc +0 -0
  142. package/core/synapse/__pycache__/engine.cpython-312.pyc +0 -0
  143. package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
  144. package/core/synapse/__pycache__/graph_context_layer.cpython-313.pyc +0 -0
  145. package/core/synapse/__pycache__/kb_cache.cpython-312.pyc +0 -0
  146. package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
  147. package/core/synapse/__pycache__/layers.cpython-312.pyc +0 -0
  148. package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
  149. package/core/synapse/engine.py +7 -0
  150. package/core/synapse/graph_context_layer.py +212 -0
  151. package/core/synapse/kb_cache.py +3 -0
  152. package/core/synapse/layers.py +104 -20
  153. package/core/workflow/__pycache__/__init__.cpython-312.pyc +0 -0
  154. package/core/workflow/__pycache__/enforcer.cpython-313.pyc +0 -0
  155. package/core/workflow/__pycache__/engine.cpython-312.pyc +0 -0
  156. package/core/workflow/__pycache__/flow_enforcer.cpython-312.pyc +0 -0
  157. package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
  158. package/core/workflow/__pycache__/gate_checkpoint.cpython-312.pyc +0 -0
  159. package/core/workflow/__pycache__/gate_checkpoint.cpython-313.pyc +0 -0
  160. package/core/workflow/__pycache__/loader.cpython-312.pyc +0 -0
  161. package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
  162. package/core/workflow/__pycache__/rules_registry.cpython-313.pyc +0 -0
  163. package/core/workflow/__pycache__/schema.cpython-312.pyc +0 -0
  164. package/core/workflow/__pycache__/specialist_enforcer.cpython-312.pyc +0 -0
  165. package/core/workflow/__pycache__/specialist_enforcer.cpython-313.pyc +0 -0
  166. package/core/workflow/enforcer.py +5 -16
  167. package/core/workflow/flow_enforcer.py +36 -11
  168. package/core/workflow/gate_checkpoint.py +149 -0
  169. package/core/workflow/research_gate.py +15 -3
  170. package/core/workflow/rules_registry.py +137 -389
  171. package/core/workflow/specialist_enforcer.py +21 -4
  172. package/dashboard/app/pages/knowledge/index.vue +4 -1
  173. package/departments/brand/agents/ux-designer.yaml +1 -0
  174. package/departments/brand/agents/visual-designer.yaml +2 -0
  175. package/departments/dev/agents/frontend-dev.yaml +2 -0
  176. package/departments/ops/skills/n8n-flow/SKILL.md +13 -0
  177. package/departments/ops/skills/workflow-automate/SKILL.md +16 -0
  178. package/departments/quality/SKILL.md +45 -5
  179. package/departments/quality/agents/copy-director.yaml +3 -1
  180. package/departments/quality/agents/tech-director.yaml +3 -1
  181. package/installer/adapters/claude-code.js +46 -2
  182. package/installer/config-seed.js +39 -14
  183. package/installer/doctor.js +8 -0
  184. package/installer/graphify.js +153 -0
  185. package/installer/index.js +24 -0
  186. package/installer/init.js +11 -0
  187. package/installer/update.js +23 -0
  188. package/package.json +1 -1
  189. package/pyproject.toml +1 -1
  190. package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
  191. package/scripts/__pycache__/knowledge-index.cpython-313.pyc +0 -0
  192. package/scripts/__pycache__/synapse-bridge.cpython-312.pyc +0 -0
  193. package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
  194. package/scripts/bench/__pycache__/__init__.cpython-313.pyc +0 -0
  195. package/scripts/bench/__pycache__/harness.cpython-313.pyc +0 -0
  196. package/scripts/bench/__pycache__/run.cpython-313.pyc +0 -0
  197. package/scripts/knowledge-index.py +8 -2
  198. package/scripts/synapse-bridge.py +102 -26
  199. package/scripts/tools/__pycache__/sync_wiki.cpython-313.pyc +0 -0
  200. package/scripts/tools/sync_wiki.py +163 -0
@@ -157,6 +157,8 @@ class ForgeOrchestrator:
157
157
  self._dispatcher = dispatcher or ClaudeCodeForgeDispatcher()
158
158
  self._current_plan: Optional[ForgePlan] = None
159
159
  self._current_step: Optional[ForgeStep] = None
160
+ self._critic_verdict: Optional[CriticVerdict] = None
161
+ self._dispatch_errors: list[str] = []
160
162
 
161
163
  # -------------------------------------------------------------------------
162
164
  # Main Commands
@@ -554,12 +556,15 @@ class ForgeOrchestrator:
554
556
  for lens in lenses
555
557
  ]
556
558
 
559
+ self._dispatch_errors = []
557
560
  for req in requests:
558
561
  try:
559
562
  approach = self._dispatcher.dispatch_explorer_and_parse(req)
560
563
  self._approaches.append(approach)
561
564
  except Exception as e:
562
- pass
565
+ self._dispatch_errors.append(
566
+ f"explorer[{req.lens.value}]: {type(e).__name__}: {e}"
567
+ )
563
568
 
564
569
  def _step6_critic_synthesis(self, revision_request: Optional[str] = None) -> None:
565
570
  """Step 6: Launch critic subagent for synthesis.
@@ -576,16 +581,13 @@ class ForgeOrchestrator:
576
581
 
577
582
  try:
578
583
  self._critic_verdict = self._dispatcher.dispatch_critic_and_parse(critic_req)
579
- except Exception:
580
- self._critic_verdict = CriticVerdict(
581
- synthesis={"approach_1": [a.summary for a in self._approaches]},
582
- rejected_elements=[],
583
- risks=[],
584
- confidence=0.5,
585
- estimated_phases=len(self._approaches[0].phases) if self._approaches else 3,
586
- )
584
+ except Exception as e:
585
+ # Honest failure: NEVER fabricate a synthetic verdict. A missing
586
+ # critic means the plan is degraded and must say so.
587
+ self._critic_verdict = None
588
+ self._dispatch_errors.append(f"critic: {type(e).__name__}: {e}")
587
589
 
588
- if revision_request:
590
+ if revision_request and self._critic_verdict is not None:
589
591
  self._critic_verdict.synthesis["revision_request"] = [revision_request]
590
592
 
591
593
  def _step7_enforce_constitution(self) -> None:
@@ -596,7 +598,7 @@ class ForgeOrchestrator:
596
598
  has_branch = any("branch" in n or "feature" in n for n in phase_names)
597
599
  has_spec = any("spec" in n or "specification" in n for n in phase_names)
598
600
  has_qg = any("quality" in n or "gate" in n or "qg" in n for n in phase_names)
599
- has_obsidian = any("obsidian" in n or "persist" in n or "persist" in n for n in phase_names)
601
+ has_obsidian = any("obsidian" in n or "persist" in n or "document" in n for n in phase_names)
600
602
 
601
603
  enforced = []
602
604
  if not has_branch:
@@ -643,9 +645,16 @@ class ForgeOrchestrator:
643
645
  self._enforced_phases = enforced
644
646
 
645
647
  def _step8_build_plan(self) -> None:
646
- """Step 8: Build the ForgePlan object."""
648
+ """Step 8: Build the ForgePlan object.
649
+
650
+ When explorer/critic dispatch failed, the plan is marked
651
+ ``degraded`` with confidence 0.0 and carries the dispatch errors —
652
+ never a fabricated verdict.
653
+ """
647
654
  plan_id = self._generate_plan_id(self._forge_context.prompt)
648
- execution_path = select_execution_path(self._final_phases_from_critic())
655
+ phases = self._final_phases_from_critic()
656
+ execution_path = select_execution_path(phases)
657
+ degraded = self._critic_verdict is None
649
658
 
650
659
  self._current_plan = ForgePlan(
651
660
  id=plan_id,
@@ -656,8 +665,8 @@ class ForgeOrchestrator:
656
665
  context=self._forge_context,
657
666
  complexity=self._complexity,
658
667
  approaches=self._approaches,
659
- critic=self._critic_verdict,
660
- plan_phases=self._final_phases_from_critic(),
668
+ critic=self._critic_verdict or CriticVerdict(confidence=0.0),
669
+ plan_phases=phases,
661
670
  goal=self._forge_context.prompt,
662
671
  execution_path=execution_path,
663
672
  governance=ForgeGovernance(
@@ -665,6 +674,8 @@ class ForgeOrchestrator:
665
674
  quality_gate_required=True,
666
675
  branch_strategy=f"feature/{plan_id}",
667
676
  ),
677
+ degraded=degraded,
678
+ dispatch_errors=list(self._dispatch_errors),
668
679
  status=ForgeStatus.REVIEWING,
669
680
  )
670
681
 
@@ -700,21 +711,33 @@ class ForgeOrchestrator:
700
711
  pass
701
712
 
702
713
  def _final_phases_from_critic(self) -> list[PlanPhase]:
703
- """Build final phases list from critic verdict + constitution enforcement."""
704
- phases = []
705
- for i, p in enumerate(range(self._critic_verdict.estimated_phases or 3)):
706
- phases.append(
707
- PlanPhase(
708
- name=f"Phase {i + 1}",
709
- department="dev",
710
- )
711
- )
712
-
713
- for ep in self._enforced_phases:
714
- phases.append(ep)
714
+ """Build final phases: critic-derived base + constitution enforcement.
715
715
 
716
+ When the critic verdict is missing (dispatch failed) or carries no
717
+ real content, the base list is EMPTY — the plan then contains only
718
+ the constitution-enforced phases and is marked degraded downstream.
719
+ """
720
+ phases = self._critic_base_phases()
721
+ phases.extend(self._enforced_phases)
716
722
  return phases
717
723
 
724
+ def _critic_base_phases(self) -> list[PlanPhase]:
725
+ """Phases derived from the critic verdict; [] when absent/empty."""
726
+ verdict = self._critic_verdict
727
+ if verdict is None or not self._verdict_has_content(verdict):
728
+ return []
729
+ return [
730
+ PlanPhase(name=f"Phase {i + 1}", department="dev")
731
+ for i in range(verdict.estimated_phases)
732
+ ]
733
+
734
+ @staticmethod
735
+ def _verdict_has_content(verdict: CriticVerdict) -> bool:
736
+ """A verdict counts as real only when it estimated phases AND synthesized something."""
737
+ return verdict.estimated_phases > 0 and bool(
738
+ verdict.synthesis or verdict.rejected_elements or verdict.risks
739
+ )
740
+
718
741
  def _generate_plan_id(self, prompt: str) -> str:
719
742
  """Generate a forge plan ID."""
720
743
  date = datetime.now(timezone.utc).strftime("%Y%m%d")
@@ -123,9 +123,25 @@ def _render_obsidian_frontmatter(plan: ForgePlan) -> list[str]:
123
123
  f"complexity: {plan.complexity.score}",
124
124
  f"created: {plan.created_at or ''}",
125
125
  ]
126
+ if plan.degraded:
127
+ lines.append("degraded: true")
126
128
  if plan.executed_at:
127
129
  lines.append(f"executed: {plan.executed_at}")
128
130
  lines += ["---", "", f"# {plan.name}", ""]
131
+ if plan.degraded:
132
+ lines += _render_obsidian_degraded(plan)
133
+ return lines
134
+
135
+
136
+ def _render_obsidian_degraded(plan: ForgePlan) -> list[str]:
137
+ """Render the degraded-plan warning block."""
138
+ lines = [
139
+ "> [!warning] DEGRADED: explorer/critic dispatch unavailable — "
140
+ "plan contains only constitution-enforced phases",
141
+ ]
142
+ for err in plan.dispatch_errors:
143
+ lines.append(f"> - {err}")
144
+ lines.append("")
129
145
  return lines
130
146
 
131
147
 
@@ -60,6 +60,12 @@ def render_plan_overview(phases: list[PlanPhase], execution_path: ExecutionPath)
60
60
  return "\n".join(lines)
61
61
 
62
62
 
63
+ DEGRADED_NOTICE = (
64
+ "DEGRADED: explorer/critic dispatch unavailable — "
65
+ "plan contains only constitution-enforced phases"
66
+ )
67
+
68
+
63
69
  def render_terminal(plan: ForgePlan) -> str:
64
70
  """Render a complete forge plan for terminal display."""
65
71
  lines = [
@@ -70,6 +76,10 @@ def render_terminal(plan: ForgePlan) -> str:
70
76
  "▸ Complexity Analysis",
71
77
  render_complexity(plan.complexity), "",
72
78
  ]
79
+ if plan.degraded:
80
+ lines.extend([f"⚠ {DEGRADED_NOTICE}"])
81
+ lines.extend(f" - {err}" for err in plan.dispatch_errors)
82
+ lines.append("")
73
83
  if plan.critic.confidence > 0:
74
84
  lines.extend(["▸ Critic Verdict", render_critic_summary(plan.critic), ""])
75
85
  if plan.plan_phases:
@@ -115,6 +125,11 @@ def render_html(plan: ForgePlan) -> str:
115
125
  phases_html = _render_phases_html(plan.plan_phases)
116
126
  critic_html = _render_critic_html(plan.critic)
117
127
  approaches_html = _render_approaches_html(plan.approaches)
128
+ degraded_html = (
129
+ f'<p><span class="badge badge-red">⚠ {_esc(DEGRADED_NOTICE)}</span></p>'
130
+ if plan.degraded
131
+ else ""
132
+ )
118
133
  ctx = plan.context
119
134
  meta = (
120
135
  f"{_esc(ctx.repo)} @ {_esc(ctx.commit_at_forge)} | Branch: {_esc(ctx.branch)} "
@@ -128,6 +143,7 @@ def render_html(plan: ForgePlan) -> str:
128
143
  f"<style>\n{_COMPANION_CSS}\n</style></head><body>"
129
144
  f"<h1>\u2692 {_esc(plan.name)}</h1>"
130
145
  f'<div class="meta">{meta}</div>'
146
+ f"{degraded_html}"
131
147
  f'<div class="grid">'
132
148
  f'<div class="card"><h2>Complexity Radar</h2>{radar_svg}</div>'
133
149
  f'<div class="card"><h2>Critic Verdict</h2>{critic_html}</div>'
@@ -84,6 +84,29 @@ Be adversarial but constructive. Your job is to stress-test assumptions, not to
84
84
  }
85
85
 
86
86
 
87
+ class ForgeDispatchUnavailableError(RuntimeError):
88
+ """Raised when Python-side subagent dispatch is attempted.
89
+
90
+ Python-side agent dispatch does not exist: there is no in-process
91
+ bridge to the runtime's Agent/Task tool. The runtime executes the
92
+ explorer and critic prompts (built by ``_build_explorer_prompt`` /
93
+ ``_build_critic_prompt``) via the arka-forge SKILL, not via this
94
+ module. Callers that invoke the dispatcher directly MUST handle
95
+ degraded planning: no explorer approaches, no critic verdict, and
96
+ a plan that only carries constitution-enforced phases.
97
+ """
98
+
99
+ def __init__(self, agent_type: str, model: str) -> None:
100
+ super().__init__(
101
+ f"Cannot dispatch '{agent_type}' subagent (model={model}): "
102
+ "Python-side agent dispatch does not exist. The runtime "
103
+ "executes explorer/critic prompts via the arka-forge SKILL. "
104
+ "Callers must handle degraded planning."
105
+ )
106
+ self.agent_type = agent_type
107
+ self.model = model
108
+
109
+
87
110
  @dataclass
88
111
  class DispatchResult:
89
112
  """Result from a dispatcher call."""
@@ -384,22 +407,23 @@ class ForgeTaskDispatcher(ABC):
384
407
 
385
408
 
386
409
  class ClaudeCodeForgeDispatcher(ForgeTaskDispatcher):
387
- """Claude Code implementation using the Task tool with model parameter.
410
+ """Claude Code dispatcher prompt builder only, no in-process dispatch.
388
411
 
389
- Claude Code supports the Agent tool with model routing:
390
- - haiku: fast, cost-optimized
391
- - sonnet: balanced
392
- - opus: highest capability
412
+ This class builds the full explorer/critic prompts. Actual subagent
413
+ execution happens in the runtime via the arka-forge SKILL, never in
414
+ Python. ``_call_agent`` therefore raises
415
+ ``ForgeDispatchUnavailableError`` callers must catch it and degrade
416
+ honestly instead of fabricating results.
393
417
  """
394
418
 
395
419
  def dispatch_explorer(self, request: ExplorerDispatchRequest) -> DispatchResult:
396
- """Dispatch explorer using Claude Code's Agent tool."""
420
+ """Build the explorer prompt and attempt dispatch (raises)."""
397
421
  prompt = _build_explorer_prompt(request)
398
422
  raw_response = self._call_agent(prompt, model=request.model, agent_type="explorer")
399
423
  return DispatchResult(success=True, raw_response=raw_response, output=raw_response)
400
424
 
401
425
  def dispatch_critic(self, request: CriticDispatchRequest) -> DispatchResult:
402
- """Dispatch critic using Claude Code's Agent tool."""
426
+ """Build the critic prompt and attempt dispatch (raises)."""
403
427
  prompt = _build_critic_prompt(request)
404
428
  raw_response = self._call_agent(prompt, model=request.model, agent_type="critic")
405
429
  return DispatchResult(success=True, raw_response=raw_response, output=raw_response)
@@ -411,20 +435,11 @@ class ClaudeCodeForgeDispatcher(ForgeTaskDispatcher):
411
435
  agent_type: str,
412
436
  timeout: int = 120,
413
437
  ) -> str:
414
- """Call the Claude Code Agent tool.
438
+ """Always raises — Python cannot call the runtime's Agent tool.
415
439
 
416
- This method should be overridden in tests to mock the actual
417
- Task tool call. The real implementation uses the Agent tool
418
- from Claude Code's toolkit.
440
+ Tests may override this method to simulate a working runtime.
419
441
  """
420
- from agentTool import Agent # type: ignore
421
-
422
- task_result = Agent(
423
- prompt=prompt,
424
- model=model,
425
- task_type=agent_type,
426
- )
427
- return task_result.output
442
+ raise ForgeDispatchUnavailableError(agent_type=agent_type, model=model)
428
443
 
429
444
 
430
445
  def create_dispatcher(runtime: str | None = None) -> ForgeTaskDispatcher:
@@ -206,6 +206,18 @@ class ForgePlan(BaseModel):
206
206
 
207
207
  governance: ForgeGovernance = Field(default_factory=ForgeGovernance)
208
208
 
209
+ degraded: bool = Field(
210
+ default=False,
211
+ description=(
212
+ "True when explorer/critic dispatch was unavailable — the plan "
213
+ "contains only constitution-enforced phases and confidence is 0.0."
214
+ ),
215
+ )
216
+ dispatch_errors: List[str] = Field(
217
+ default_factory=list,
218
+ description="Dispatch failures collected during forge (lens: error class: message).",
219
+ )
220
+
209
221
  status: ForgeStatus = ForgeStatus.DRAFT
210
222
  approved_at: Optional[str] = None
211
223
  approved_by: Optional[str] = None
@@ -1,9 +1,10 @@
1
- """[arka:phase:13] / [arka:trivial] closing-marker soft-block (PR59 v2.76.0).
1
+ """Closing-marker soft-block (PR59 v2.76.0; evidence flow v4.1.0).
2
2
 
3
3
  Response-side classifier. Inspects the closing assistant message of a
4
- flow-required turn for the mandatory closure marker — either
5
- ``[arka:phase:13]`` (full flow completed) or ``[arka:trivial]``
6
- (trivial bypass). Mirrors the contract of
4
+ flow-required turn for the mandatory closure marker — ``[arka:gate:4]``
5
+ (evidence flow review gate), the legacy ``[arka:phase:13]`` (accepted
6
+ during the v4.1 deprecation window), or ``[arka:trivial]`` (trivial
7
+ bypass). Mirrors the contract of
7
8
  ``core.governance.meta_tag_check`` (PR30 v2.49.0) and
8
9
  ``core.governance.kb_cite_check`` (PR18 v2.40.0).
9
10
 
@@ -23,12 +24,13 @@ import re
23
24
  from dataclasses import dataclass
24
25
 
25
26
 
27
+ _GATE4_RE: re.Pattern[str] = re.compile(r"\[arka:gate:4\]", re.IGNORECASE)
26
28
  _PHASE13_RE: re.Pattern[str] = re.compile(r"\[arka:phase:13\]", re.IGNORECASE)
27
29
  _TRIVIAL_RE: re.Pattern[str] = re.compile(r"\[arka:trivial\]", re.IGNORECASE)
28
30
  _TRIVIAL_WORD_THRESHOLD: int = 15
29
31
  _SUGGESTION_TEXT: str = (
30
32
  "Closing marker missing — end every flow-required turn with "
31
- "`[arka:phase:13] <label>` (full canonical flow) or "
33
+ "`[arka:gate:4] <label>` (evidence flow review gate) or "
32
34
  "`[arka:trivial] <reason>` (single-file edit < 10 lines, "
33
35
  "imperative verb). Without the marker, telemetry can't confirm "
34
36
  "the turn closed cleanly."
@@ -52,6 +54,8 @@ def check_closing_marker(response_text: str) -> ClosingMarkerResult:
52
54
  marker is found.
53
55
  """
54
56
  text = response_text or ""
57
+ if _GATE4_RE.search(text):
58
+ return ClosingMarkerResult(True, "gate4", None)
55
59
  if _PHASE13_RE.search(text):
56
60
  return ClosingMarkerResult(True, "phase13", None)
57
61
  if _TRIVIAL_RE.search(text):
@@ -4,7 +4,7 @@ Reads the stop-hook entries in ``~/.arkaos/telemetry/enforcement.jsonl``
4
4
  and reports compliance with the four contracts the session-start hook
5
5
  establishes:
6
6
 
7
- - closing_marker_found — [arka:phase:13] or [arka:trivial] present
7
+ - closing_marker_found — [arka:gate:4] (or legacy [arka:phase:13]) or [arka:trivial] present
8
8
  - meta_tag_found — [arka:meta] one-liner present (PR12)
9
9
  - kb_cite_passed — KB citation soft block result (PR18)
10
10
  - sycophancy clean — inverse of sycophancy_is_flagged (PR13)
@@ -27,7 +27,7 @@ def _render(summary: ComplianceSummary) -> str:
27
27
  "",
28
28
  "| Contract | Compliance rate |",
29
29
  "|---|---|",
30
- f"| `[arka:phase:13]` / `[arka:trivial]` closing marker "
30
+ f"| `[arka:gate:4]` / `[arka:trivial]` closing marker "
31
31
  f"| {_fmt_rate(summary.closing_marker_rate)} |",
32
32
  f"| `[arka:meta]` one-liner (PR12 v2.34.0) "
33
33
  f"| {_fmt_rate(summary.meta_tag_rate)} |",