arkaos 2.0.1 → 2.0.3

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 (55) hide show
  1. package/VERSION +1 -1
  2. package/config/constitution.yaml +6 -0
  3. package/config/hooks/user-prompt-submit-v2.sh +33 -40
  4. package/core/budget/__init__.py +6 -0
  5. package/core/budget/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/core/budget/__pycache__/manager.cpython-313.pyc +0 -0
  7. package/core/budget/__pycache__/schema.cpython-313.pyc +0 -0
  8. package/core/budget/manager.py +193 -0
  9. package/core/budget/schema.py +82 -0
  10. package/core/knowledge/__init__.py +6 -0
  11. package/core/knowledge/__pycache__/__init__.cpython-313.pyc +0 -0
  12. package/core/knowledge/__pycache__/chunker.cpython-313.pyc +0 -0
  13. package/core/knowledge/__pycache__/embedder.cpython-313.pyc +0 -0
  14. package/core/knowledge/__pycache__/indexer.cpython-313.pyc +0 -0
  15. package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
  16. package/core/knowledge/chunker.py +121 -0
  17. package/core/knowledge/embedder.py +52 -0
  18. package/core/knowledge/indexer.py +97 -0
  19. package/core/knowledge/vector_store.py +213 -0
  20. package/core/obsidian/__init__.py +6 -0
  21. package/core/obsidian/__pycache__/__init__.cpython-313.pyc +0 -0
  22. package/core/obsidian/__pycache__/templates.cpython-313.pyc +0 -0
  23. package/core/obsidian/__pycache__/writer.cpython-313.pyc +0 -0
  24. package/core/obsidian/templates.py +76 -0
  25. package/core/obsidian/writer.py +148 -0
  26. package/core/orchestration/__init__.py +6 -0
  27. package/core/orchestration/__pycache__/__init__.cpython-313.pyc +0 -0
  28. package/core/orchestration/__pycache__/patterns.cpython-313.pyc +0 -0
  29. package/core/orchestration/__pycache__/protocol.cpython-313.pyc +0 -0
  30. package/core/orchestration/patterns.py +136 -0
  31. package/core/orchestration/protocol.py +96 -0
  32. package/core/runtime/__pycache__/subagent.cpython-313.pyc +0 -0
  33. package/core/runtime/subagent.py +5 -0
  34. package/core/squads/__pycache__/schema.cpython-313.pyc +0 -0
  35. package/core/squads/schema.py +3 -0
  36. package/core/squads/templates/project-squad.yaml +28 -0
  37. package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
  38. package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
  39. package/core/synapse/engine.py +5 -1
  40. package/core/synapse/layers.py +95 -9
  41. package/core/tasks/__pycache__/schema.cpython-313.pyc +0 -0
  42. package/core/tasks/schema.py +7 -0
  43. package/core/workflow/__pycache__/engine.cpython-313.pyc +0 -0
  44. package/core/workflow/__pycache__/schema.cpython-313.pyc +0 -0
  45. package/core/workflow/engine.py +44 -0
  46. package/core/workflow/schema.py +1 -0
  47. package/departments/dev/agents/research-assistant.yaml +51 -0
  48. package/departments/kb/agents/data-collector.yaml +51 -0
  49. package/departments/ops/agents/doc-writer.yaml +51 -0
  50. package/departments/pm/agents/pm-director.yaml +1 -1
  51. package/installer/cli.js +36 -0
  52. package/installer/init.js +105 -0
  53. package/installer/migrate.js +4 -1
  54. package/package.json +1 -1
  55. package/pyproject.toml +5 -1
@@ -0,0 +1,136 @@
1
+ """Built-in orchestration patterns.
2
+
3
+ Four patterns adapted from claude-skills orchestration protocol,
4
+ mapped to ArkaOS department/agent/skill system.
5
+ """
6
+
7
+ from core.orchestration.protocol import OrchestrationPattern, PatternType
8
+
9
+
10
+ SOLO_SPRINT = OrchestrationPattern(
11
+ type=PatternType.SOLO_SPRINT,
12
+ name="Solo Sprint",
13
+ description="One department lead drives a multi-phase sprint, pulling skills from their squad. Fast, focused execution.",
14
+ when_to_use=[
15
+ "Single-domain task with clear scope",
16
+ "Time-constrained delivery (< 1 week)",
17
+ "One department has all required expertise",
18
+ ],
19
+ structure="Lead → Phase 1 (skills A,B) → Phase 2 (skills C,D) → Quality Gate → Deliver",
20
+ example=(
21
+ "Objective: Launch MVP landing page\n"
22
+ "Lead: Ines (Landing)\n"
23
+ "Phase 1: /landing copy-framework + /landing funnel-design\n"
24
+ "Phase 2: /landing page-build + /landing seo-optimize\n"
25
+ "Phase 3: Quality Gate → Ship"
26
+ ),
27
+ anti_patterns=[
28
+ "Using for cross-department work (use Multi-Agent Handoff instead)",
29
+ "Skipping Quality Gate because 'it's just one department'",
30
+ ],
31
+ )
32
+
33
+ DOMAIN_DEEP_DIVE = OrchestrationPattern(
34
+ type=PatternType.DOMAIN_DEEP_DIVE,
35
+ name="Domain Deep-Dive",
36
+ description="One agent, multiple stacked skills for thorough analysis. Deep expertise over breadth.",
37
+ when_to_use=[
38
+ "Complex technical audit or review",
39
+ "Due diligence or compliance assessment",
40
+ "Research requiring depth in one area",
41
+ ],
42
+ structure="Agent → Skill 1 (foundation) → Skill 2 (analysis) → Skill 3 (recommendations) → Report",
43
+ example=(
44
+ "Objective: Full security assessment\n"
45
+ "Agent: Bruno (Security Engineer)\n"
46
+ "Skills stacked:\n"
47
+ " 1. /dev security-audit (OWASP scan)\n"
48
+ " 2. /dev dependency-audit (CVE check)\n"
49
+ " 3. /dev red-team (attack simulation)\n"
50
+ " 4. /dev ai-security (LLM-specific risks)\n"
51
+ "Output: Consolidated security report with severity rankings"
52
+ ),
53
+ anti_patterns=[
54
+ "Stacking unrelated skills (each skill should build on previous)",
55
+ "Skipping the foundational skill and going straight to advanced",
56
+ "No consolidated output (each skill reports independently)",
57
+ ],
58
+ )
59
+
60
+ MULTI_AGENT_HANDOFF = OrchestrationPattern(
61
+ type=PatternType.MULTI_AGENT_HANDOFF,
62
+ name="Multi-Agent Handoff",
63
+ description="Work flows between departments with structured handoffs. Each department adds its expertise.",
64
+ when_to_use=[
65
+ "Cross-department projects (product launch, brand + marketing)",
66
+ "Sequential expertise needed (strategy → dev → marketing)",
67
+ "Each phase requires different domain knowledge",
68
+ ],
69
+ structure="Dept A → Handoff → Dept B → Handoff → Dept C → Quality Gate → Deliver",
70
+ example=(
71
+ "Objective: Launch new SaaS product\n"
72
+ "Phase 1: Tomas (Strategy) → /strat bmc + /strat five-forces\n"
73
+ " Handoff: Market validation, pricing strategy, competitive position\n"
74
+ "Phase 2: Paulo (Dev) → /dev spec + /dev feature\n"
75
+ " Handoff: MVP built, deployed to staging\n"
76
+ "Phase 3: Luna (Marketing) → /mkt growth-plan + /mkt email-sequence\n"
77
+ " Handoff: Landing page, email funnel, launch plan\n"
78
+ "Phase 4: Quality Gate → Launch"
79
+ ),
80
+ anti_patterns=[
81
+ "No handoff context (next department starts from zero)",
82
+ "Too many departments (>4 phases becomes unwieldy)",
83
+ "Skipping departments that should review (e.g., security for a public launch)",
84
+ ],
85
+ )
86
+
87
+ SKILL_CHAIN = OrchestrationPattern(
88
+ type=PatternType.SKILL_CHAIN,
89
+ name="Skill Chain",
90
+ description="Sequential procedural skills with no specific agent identity. Pure execution pipeline.",
91
+ when_to_use=[
92
+ "Procedural work with well-defined inputs/outputs",
93
+ "Automated pipelines (generate → validate → publish)",
94
+ "No judgment calls needed, just execution",
95
+ ],
96
+ structure="Skill A (input) → Skill B (transform) → Skill C (output) → Done",
97
+ example=(
98
+ "Objective: Content production pipeline\n"
99
+ "Chain:\n"
100
+ " 1. /content hook-write → 10 headline options\n"
101
+ " 2. python scripts/tools/headline_scorer.py → scored + ranked\n"
102
+ " 3. /content viral-design → full post with winning hook\n"
103
+ " 4. /mkt social-strategy → distribution plan\n"
104
+ "No persona needed — pure skill execution"
105
+ ),
106
+ anti_patterns=[
107
+ "Using for work that needs judgment (use Solo Sprint or Handoff)",
108
+ "Long chains with no validation checkpoints",
109
+ "Mixing judgment skills with procedural skills",
110
+ ],
111
+ )
112
+
113
+
114
+ PATTERNS = {
115
+ PatternType.SOLO_SPRINT: SOLO_SPRINT,
116
+ PatternType.DOMAIN_DEEP_DIVE: DOMAIN_DEEP_DIVE,
117
+ PatternType.MULTI_AGENT_HANDOFF: MULTI_AGENT_HANDOFF,
118
+ PatternType.SKILL_CHAIN: SKILL_CHAIN,
119
+ }
120
+
121
+
122
+ def select_pattern(
123
+ departments_involved: int,
124
+ needs_judgment: bool,
125
+ is_sequential: bool,
126
+ ) -> PatternType:
127
+ """Recommend an orchestration pattern based on task characteristics."""
128
+ if departments_involved == 1 and needs_judgment:
129
+ return PatternType.SOLO_SPRINT
130
+ if departments_involved == 1 and not needs_judgment:
131
+ return PatternType.SKILL_CHAIN
132
+ if departments_involved > 1 and is_sequential:
133
+ return PatternType.MULTI_AGENT_HANDOFF
134
+ if departments_involved == 1:
135
+ return PatternType.DOMAIN_DEEP_DIVE
136
+ return PatternType.MULTI_AGENT_HANDOFF
@@ -0,0 +1,96 @@
1
+ """Orchestration protocol schemas.
2
+
3
+ Defines the structure for multi-agent coordination across departments.
4
+ Four patterns: Solo Sprint, Domain Deep-Dive, Multi-Agent Handoff, Skill Chain.
5
+ """
6
+
7
+ from enum import Enum
8
+ from typing import Optional
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class PatternType(str, Enum):
14
+ SOLO_SPRINT = "solo_sprint"
15
+ DOMAIN_DEEP_DIVE = "domain_deep_dive"
16
+ MULTI_AGENT_HANDOFF = "multi_agent_handoff"
17
+ SKILL_CHAIN = "skill_chain"
18
+
19
+
20
+ class PhaseHandoff(BaseModel):
21
+ """Context passed between orchestration phases."""
22
+ phase_number: int
23
+ phase_name: str
24
+ decisions: list[str] = Field(default_factory=list)
25
+ artifacts: list[str] = Field(default_factory=list)
26
+ open_questions: list[str] = Field(default_factory=list)
27
+ next_department: str = ""
28
+ next_agent: str = ""
29
+ next_skills: list[str] = Field(default_factory=list)
30
+
31
+ def to_context(self) -> str:
32
+ """Render handoff as context string for next phase."""
33
+ lines = [f"Phase {self.phase_number} ({self.phase_name}) complete."]
34
+ if self.decisions:
35
+ lines.append(f"Decisions: {', '.join(self.decisions)}")
36
+ if self.artifacts:
37
+ lines.append(f"Artifacts: {', '.join(self.artifacts)}")
38
+ if self.open_questions:
39
+ lines.append(f"Open questions: {', '.join(self.open_questions)}")
40
+ if self.next_department:
41
+ lines.append(f"Next: {self.next_department}/{self.next_agent}")
42
+ return "\n".join(lines)
43
+
44
+
45
+ class OrchestrationPhase(BaseModel):
46
+ """A single phase in an orchestration plan."""
47
+ number: int
48
+ name: str
49
+ department: str
50
+ agent_id: str
51
+ skills: list[str] = Field(default_factory=list)
52
+ objective: str = ""
53
+ outputs: list[str] = Field(default_factory=list)
54
+ gate: str = "user_approval" # user_approval, auto, quality_gate
55
+
56
+
57
+ class OrchestrationPlan(BaseModel):
58
+ """A complete orchestration plan spanning multiple departments."""
59
+ objective: str
60
+ pattern: PatternType
61
+ constraints: list[str] = Field(default_factory=list)
62
+ success_criteria: list[str] = Field(default_factory=list)
63
+ phases: list[OrchestrationPhase] = Field(default_factory=list)
64
+ current_phase: int = 0
65
+
66
+ def next_phase(self) -> Optional[OrchestrationPhase]:
67
+ """Get the next phase to execute."""
68
+ if self.current_phase < len(self.phases):
69
+ return self.phases[self.current_phase]
70
+ return None
71
+
72
+ def advance(self, handoff: PhaseHandoff) -> Optional[OrchestrationPhase]:
73
+ """Record handoff and advance to next phase."""
74
+ self.current_phase += 1
75
+ return self.next_phase()
76
+
77
+ @property
78
+ def is_complete(self) -> bool:
79
+ return self.current_phase >= len(self.phases)
80
+
81
+ @property
82
+ def progress_percent(self) -> int:
83
+ if not self.phases:
84
+ return 0
85
+ return int((self.current_phase / len(self.phases)) * 100)
86
+
87
+
88
+ class OrchestrationPattern(BaseModel):
89
+ """Definition of a coordination pattern."""
90
+ type: PatternType
91
+ name: str
92
+ description: str
93
+ when_to_use: list[str] = Field(default_factory=list)
94
+ structure: str = ""
95
+ example: str = ""
96
+ anti_patterns: list[str] = Field(default_factory=list)
@@ -102,6 +102,11 @@ class SubagentDispatcher:
102
102
  The dispatcher creates HandoffArtifacts from agent definitions
103
103
  and task descriptions, then delegates to the runtime adapter
104
104
  for actual execution.
105
+
106
+ Nesting policy: Maximum 1 level of nesting (agent -> subagent).
107
+ Sub-subagent dispatch is not recommended -- creates context fragmentation
108
+ and debugging complexity. If a subagent needs help, it should escalate
109
+ to its squad lead rather than spawning another subagent.
105
110
  """
106
111
 
107
112
  def __init__(self) -> None:
@@ -35,6 +35,9 @@ class SquadMember(BaseModel):
35
35
  borrowed: bool = False # Borrowed from another department?
36
36
  source_department: str = "" # Original department if borrowed
37
37
  availability: float = 1.0 # 0.0-1.0, for shared agents
38
+ # Tier 2 agents can collaborate directly within project squads
39
+ # without requiring Tier 1 approval for each interaction.
40
+ can_collaborate_directly: bool = True
38
41
 
39
42
 
40
43
  class SquadWorkflow(BaseModel):
@@ -0,0 +1,28 @@
1
+ # Project Squad Template
2
+ # Copy and customize for cross-department projects
3
+ id: project-{name}
4
+ name: "{Project Name} Squad"
5
+ description: "Cross-department squad for {project description}"
6
+ department: "" # No single department — cross-cutting
7
+ squad_type: project
8
+ topology: stream-aligned
9
+
10
+ members:
11
+ # Borrow from department squads
12
+ - agent_id: "{lead-agent-id}"
13
+ role: "Project Lead"
14
+ is_lead: true
15
+ borrowed: true # Borrowed from department squad
16
+ availability: 0.5 # 50% allocation
17
+
18
+ - agent_id: "{specialist-id}"
19
+ role: "Technical Implementation"
20
+ borrowed: true
21
+ availability: 0.3
22
+
23
+ # Project squads:
24
+ # - Created by COO (Sofia) or any Squad Lead
25
+ # - Agents are borrowed, not moved
26
+ # - Max 10 members (Two-Pizza Team)
27
+ # - Dissolved when project completes
28
+ # - Quality Gate still mandatory
@@ -10,6 +10,7 @@ Design goals:
10
10
 
11
11
  import time
12
12
  from dataclasses import dataclass, field
13
+ from typing import Any
13
14
 
14
15
  from core.synapse.layers import Layer, LayerResult, PromptContext
15
16
  from core.synapse.cache import LayerCache
@@ -152,6 +153,7 @@ def create_default_engine(
152
153
  constitution_compressed: str = "",
153
154
  commands: list[dict] | None = None,
154
155
  agents_registry: dict[str, dict] | None = None,
156
+ vector_store: Any = None,
155
157
  ) -> SynapseEngine:
156
158
  """Create a SynapseEngine with all 8 default layers.
157
159
 
@@ -166,7 +168,7 @@ def create_default_engine(
166
168
  from core.synapse.layers import (
167
169
  ConstitutionLayer, DepartmentLayer, AgentLayer,
168
170
  ProjectLayer, BranchLayer, CommandHintsLayer,
169
- QualityGateLayer, TimeLayer,
171
+ QualityGateLayer, TimeLayer, KnowledgeRetrievalLayer,
170
172
  )
171
173
 
172
174
  engine = SynapseEngine()
@@ -176,6 +178,8 @@ def create_default_engine(
176
178
  engine.register_layer(DepartmentLayer())
177
179
  engine.register_layer(AgentLayer(agents_registry=agents_registry))
178
180
  engine.register_layer(ProjectLayer())
181
+ if vector_store is not None:
182
+ engine.register_layer(KnowledgeRetrievalLayer(vector_store=vector_store))
179
183
  engine.register_layer(BranchLayer())
180
184
  engine.register_layer(CommandHintsLayer(commands=commands))
181
185
  engine.register_layer(QualityGateLayer())
@@ -1,17 +1,18 @@
1
- """Synapse layer definitions — the 8 context layers.
1
+ """Synapse layer definitions — the 9 context layers.
2
2
 
3
3
  Each layer extracts a specific type of context and compresses it
4
4
  for injection into the prompt. Layers are pluggable and ordered.
5
5
 
6
6
  Layer Architecture:
7
- L0: Constitution — Compressed governance rules (TTL: 300s)
8
- L1: Department — Detected department from input (no cache)
9
- L2: Agent — Agent profile + last gotchas (TTL: 30s)
10
- L3: Project — Active project context (TTL: 30s)
11
- L4: Branch Current git branch (no cache)
12
- L5: Command Hints Matching commands from registry (TTL: 30s)
13
- L6: Quality Gate QG status and last verdicts (TTL: 60s)
14
- L7: Time Time-of-day signal (no cache)
7
+ L0: Constitution — Compressed governance rules (TTL: 300s)
8
+ L1: Department — Detected department from input (no cache)
9
+ L2: Agent — Agent profile + last gotchas (TTL: 30s)
10
+ L3: Project — Active project context (TTL: 30s)
11
+ L3.5: KnowledgeRetrieval Semantic search from vector DB (TTL: 30s)
12
+ L4: Branch Current git branch (no cache)
13
+ L5: Command Hints Matching commands from registry (TTL: 30s)
14
+ L6: Quality Gate QG status and last verdicts (TTL: 60s)
15
+ L7: Time — Time-of-day signal (no cache)
15
16
  """
16
17
 
17
18
  import re
@@ -439,3 +440,88 @@ class TimeLayer(Layer):
439
440
  layer_id=self.id, tag=tag, content=period,
440
441
  tokens_est=1, compute_ms=ms, cached=False,
441
442
  )
443
+
444
+
445
+ # --- L3.5: Knowledge Retrieval ---
446
+
447
+ class KnowledgeRetrievalLayer(Layer):
448
+ """L3.5: Semantic knowledge retrieval from vector DB.
449
+
450
+ Searches the local vector store for chunks relevant to the user's
451
+ input and injects them as context. Gracefully skips if vector store
452
+ is unavailable or empty.
453
+ """
454
+
455
+ def __init__(self, vector_store: Any = None, max_chunks: int = 3, max_tokens: int = 400) -> None:
456
+ self._store = vector_store
457
+ self._max_chunks = max_chunks
458
+ self._max_tokens = max_tokens
459
+
460
+ @property
461
+ def id(self) -> str:
462
+ return "L3.5"
463
+
464
+ @property
465
+ def name(self) -> str:
466
+ return "KnowledgeRetrieval"
467
+
468
+ @property
469
+ def cache_ttl(self) -> int:
470
+ return 30
471
+
472
+ @property
473
+ def priority(self) -> int:
474
+ return 35
475
+
476
+ def compute(self, ctx: PromptContext) -> LayerResult:
477
+ start = time.time()
478
+
479
+ if not self._store or not ctx.user_input:
480
+ return LayerResult(
481
+ layer_id=self.id, tag="", content="",
482
+ tokens_est=0, compute_ms=0, cached=False,
483
+ )
484
+
485
+ try:
486
+ results = self._store.search(ctx.user_input, top_k=self._max_chunks)
487
+ except Exception:
488
+ return LayerResult(
489
+ layer_id=self.id, tag="", content="",
490
+ tokens_est=0, compute_ms=0, cached=False,
491
+ )
492
+
493
+ if not results:
494
+ ms = int((time.time() - start) * 1000)
495
+ return LayerResult(
496
+ layer_id=self.id, tag="", content="",
497
+ tokens_est=0, compute_ms=ms, cached=False,
498
+ )
499
+
500
+ # Build compact knowledge context
501
+ snippets = []
502
+ total_tokens = 0
503
+ for r in results:
504
+ text = r["text"][:200].replace("\n", " ").strip()
505
+ tokens = len(text.split())
506
+ if total_tokens + tokens > self._max_tokens:
507
+ break
508
+ source = r.get("source", "").split("/")[-1] if r.get("source") else ""
509
+ snippet = f"{source}: {text}" if source else text
510
+ snippets.append(snippet)
511
+ total_tokens += tokens
512
+
513
+ if not snippets:
514
+ ms = int((time.time() - start) * 1000)
515
+ return LayerResult(
516
+ layer_id=self.id, tag="", content="",
517
+ tokens_est=0, compute_ms=ms, cached=False,
518
+ )
519
+
520
+ content = " | ".join(snippets)
521
+ tag = f"[knowledge:{len(snippets)} chunks]"
522
+ ms = int((time.time() - start) * 1000)
523
+
524
+ return LayerResult(
525
+ layer_id=self.id, tag=tag, content=content,
526
+ tokens_est=total_tokens, compute_ms=ms, cached=False,
527
+ )
@@ -29,6 +29,7 @@ class TaskType(str, Enum):
29
29
  RESEARCH = "research" # Background research
30
30
  GENERATION = "generation" # AI content/image generation
31
31
  EXPORT = "export" # Export to external system
32
+ KB_INDEX = "kb_index" # Index documents into vector store
32
33
  CUSTOM = "custom"
33
34
 
34
35
 
@@ -47,6 +48,12 @@ class Task(BaseModel):
47
48
  output_data: dict[str, Any] = Field(default_factory=dict)
48
49
  output_path: str = "" # File path for output
49
50
 
51
+ # Budget
52
+ tokens_estimated: int = 0 # Pre-execution estimate
53
+ tokens_actual: int = 0 # Post-execution actual
54
+ budget_approved: bool = True # True if within budget or approved
55
+ approved_by: str = "" # Tier 0 agent who approved overrun
56
+
50
57
  # Progress
51
58
  progress_percent: int = 0 # 0-100
52
59
  progress_message: str = ""
@@ -48,6 +48,8 @@ class WorkflowEngine:
48
48
  on_phase_complete: Optional[Callable[[Phase, PhaseResult], None]] = None,
49
49
  on_gate_check: Optional[Callable[[Gate], GateResult]] = None,
50
50
  on_visibility: Optional[Callable[[str], None]] = None,
51
+ budget_manager: Any = None,
52
+ obsidian_writer: Any = None,
51
53
  ):
52
54
  """Initialize the workflow engine.
53
55
 
@@ -61,6 +63,8 @@ class WorkflowEngine:
61
63
  self._on_phase_complete = on_phase_complete
62
64
  self._on_gate_check = on_gate_check
63
65
  self._on_visibility = on_visibility
66
+ self._budget_manager = budget_manager
67
+ self._obsidian_writer = obsidian_writer
64
68
  self._history: list[PhaseResult] = []
65
69
 
66
70
  def announce(self, message: str) -> None:
@@ -144,6 +148,22 @@ class WorkflowEngine:
144
148
  if self._on_phase_complete:
145
149
  self._on_phase_complete(phase, result)
146
150
 
151
+ # Save outputs to Obsidian vault (NON-NEGOTIABLE: obsidian-output)
152
+ if self._obsidian_writer and hasattr(phase, "outputs"):
153
+ for output in getattr(phase, "outputs", []):
154
+ obsidian_path = getattr(output, "obsidian_path", "")
155
+ if obsidian_path and result.output:
156
+ try:
157
+ saved = self._obsidian_writer.save(
158
+ obsidian_path=obsidian_path,
159
+ content=result.output,
160
+ department=workflow.department,
161
+ workflow=workflow.id,
162
+ )
163
+ self.announce(f"Saved to vault: {saved.name}")
164
+ except Exception as e:
165
+ self.announce(f"Vault save failed: {e}")
166
+
147
167
  self.announce(f"Phase {i}: {phase.name} — COMPLETED")
148
168
 
149
169
  # Evaluate gate
@@ -189,12 +209,36 @@ class WorkflowEngine:
189
209
  if gate.type == GateType.AUTO:
190
210
  return GateResult(passed=True, gate_type=GateType.AUTO, message="Auto-pass")
191
211
 
212
+ if gate.type == GateType.BUDGET_CHECK and self._budget_manager:
213
+ return self._evaluate_budget_gate(gate)
214
+
192
215
  if self._on_gate_check:
193
216
  return self._on_gate_check(gate)
194
217
 
195
218
  # Default: pass
196
219
  return GateResult(passed=True, gate_type=gate.type, message="Default pass")
197
220
 
221
+ def _evaluate_budget_gate(self, gate: Gate) -> GateResult:
222
+ """Evaluate a budget gate using the budget manager."""
223
+ # Default tier for budget checks (can be overridden via gate metadata)
224
+ tier = 2
225
+ estimated_tokens = 50_000 # Default estimate per phase
226
+
227
+ if self._budget_manager.check_budget(tier, estimated_tokens):
228
+ needs_approval = self._budget_manager.needs_approval(tier)
229
+ msg = "Budget OK"
230
+ if needs_approval:
231
+ msg = "Budget OK (>80% used — Tier 0 notified)"
232
+ self.announce(f"Budget warning: tier {tier} at >80% monthly usage")
233
+ return GateResult(passed=True, gate_type=GateType.BUDGET_CHECK, message=msg)
234
+
235
+ summary = self._budget_manager.get_summary(tier)
236
+ return GateResult(
237
+ passed=False,
238
+ gate_type=GateType.BUDGET_CHECK,
239
+ message=f"Budget exceeded: {summary.used}/{summary.allocated} tokens used ({summary.percent_used}%). Needs Tier 0 approval.",
240
+ )
241
+
198
242
  def _evaluate_condition(self, condition: str) -> bool:
199
243
  """Evaluate a skip/branch condition.
200
244
 
@@ -27,6 +27,7 @@ class GateType(str, Enum):
27
27
  QUALITY_GATE = "quality_gate" # Marta + Eduardo + Francisca
28
28
  AUTO = "auto" # Passes automatically if phase succeeds
29
29
  CONDITION = "condition" # Passes if condition evaluates true
30
+ BUDGET_CHECK = "budget_check" # Verifies token budget before execution
30
31
 
31
32
 
32
33
  class Gate(BaseModel):
@@ -0,0 +1,51 @@
1
+ id: research-assistant
2
+ name: Maria
3
+ role: Research Assistant
4
+ department: dev
5
+ tier: 3
6
+
7
+ behavioral_dna:
8
+ disc:
9
+ primary: C
10
+ secondary: S
11
+ communication_style: "Thorough, detail-oriented, presents findings systematically"
12
+ under_pressure: "Digs deeper into data before responding"
13
+ motivator: "Understanding the full picture"
14
+ enneagram:
15
+ type: 5
16
+ wing: 6
17
+ core_motivation: "To understand and be competent"
18
+ core_fear: "Being ignorant or uninformed"
19
+ subtype: social
20
+ big_five:
21
+ openness: 90
22
+ conscientiousness: 85
23
+ extraversion: 30
24
+ agreeableness: 70
25
+ neuroticism: 35
26
+ mbti:
27
+ type: INTP
28
+
29
+ authority:
30
+ veto: false
31
+ approve_budget: false
32
+ approve_architecture: false
33
+ approve_quality: false
34
+ block_release: false
35
+ block_delivery: false
36
+ orchestrate: false
37
+ delegates_to: []
38
+ escalates_to: tech-lead-paulo
39
+
40
+ expertise:
41
+ domains: ["research", "documentation", "analysis", "literature-review"]
42
+ frameworks: ["Systematic Review", "PRISMA", "Research Methodology"]
43
+ depth: proficient
44
+ years_equivalent: 5
45
+
46
+ communication:
47
+ language: en
48
+ tone: "Precise and informative"
49
+ vocabulary_level: specialist
50
+ preferred_format: "Structured reports with citations"
51
+ avoid: ["assumptions without evidence", "vague conclusions"]
@@ -0,0 +1,51 @@
1
+ id: data-collector
2
+ name: Tomas Jr
3
+ role: Data Collector
4
+ department: kb
5
+ tier: 3
6
+
7
+ behavioral_dna:
8
+ disc:
9
+ primary: C
10
+ secondary: D
11
+ communication_style: "Data-driven, factual, structured"
12
+ under_pressure: "Relies on systematic data collection"
13
+ motivator: "Complete and accurate data"
14
+ enneagram:
15
+ type: 6
16
+ wing: 5
17
+ core_motivation: "To have reliable information"
18
+ core_fear: "Making decisions on incomplete data"
19
+ subtype: self-preservation
20
+ big_five:
21
+ openness: 70
22
+ conscientiousness: 88
23
+ extraversion: 35
24
+ agreeableness: 65
25
+ neuroticism: 40
26
+ mbti:
27
+ type: ISTJ
28
+
29
+ authority:
30
+ veto: false
31
+ approve_budget: false
32
+ approve_architecture: false
33
+ approve_quality: false
34
+ block_release: false
35
+ block_delivery: false
36
+ orchestrate: false
37
+ delegates_to: []
38
+ escalates_to: kb-lead-clara
39
+
40
+ expertise:
41
+ domains: ["data-collection", "web-scraping", "API-integration", "data-validation"]
42
+ frameworks: ["ETL", "Data Quality Framework"]
43
+ depth: proficient
44
+ years_equivalent: 4
45
+
46
+ communication:
47
+ language: en
48
+ tone: "Factual and precise"
49
+ vocabulary_level: specialist
50
+ preferred_format: "Data tables with quality scores"
51
+ avoid: ["subjective interpretations", "unverified claims"]