arkaos 2.17.1 → 2.17.4
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.
- package/VERSION +1 -1
- package/arka/SKILL.md +9 -67
- package/arka/skills/comfyui/SKILL.md +50 -12
- package/arka/skills/conclave/SKILL.md +43 -141
- package/arka/skills/conclave/references/advisors.md +36 -0
- package/arka/skills/human-writing/SKILL.md +15 -100
- package/arka/skills/human-writing/references/forbidden-patterns.md +32 -0
- package/config/hooks/post-tool-use.sh +72 -0
- package/config/hooks/session-start.sh +16 -0
- package/config/hooks/user-prompt-submit.ps1 +2 -2
- package/config/hooks/user-prompt-submit.sh +102 -26
- package/core/agents/__pycache__/behavior_enforcer.cpython-313.pyc +0 -0
- package/core/agents/__pycache__/dna_registry.cpython-313.pyc +0 -0
- package/core/agents/adapters/__pycache__/disc_adapter.cpython-313.pyc +0 -0
- package/core/agents/adapters/disc_adapter.py +149 -0
- package/core/agents/behavior_enforcer.py +255 -0
- package/core/agents/dna_registry.py +235 -0
- package/core/forge/__init__.py +36 -0
- package/core/forge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/runtime_dispatcher.cpython-313.pyc +0 -0
- package/core/forge/orchestrator.py +770 -0
- package/core/forge/runtime_dispatcher.py +465 -0
- package/core/governance/__pycache__/quality_api.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/quality_router.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/review_workflow.cpython-313.pyc +0 -0
- package/core/governance/quality_api.py +280 -0
- package/core/governance/quality_router.py +304 -0
- package/core/governance/review_workflow.py +386 -0
- package/core/memory/__pycache__/compressor.cpython-313.pyc +0 -0
- package/core/memory/__pycache__/rehydrator.cpython-313.pyc +0 -0
- package/core/memory/__pycache__/session_store.cpython-313.pyc +0 -0
- package/core/memory/compressor.py +269 -0
- package/core/memory/rehydrator.py +204 -0
- package/core/memory/session_store.py +256 -0
- package/core/runtime/__pycache__/context_compactor.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/subagent.cpython-313.pyc +0 -0
- package/core/synapse/__init__.py +10 -3
- package/core/synapse/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/synapse/engine.py +27 -16
- package/core/synapse/kb_cache.py +382 -0
- package/core/synapse/layers.py +253 -50
- package/core/sync/__pycache__/self_healing.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/announcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/dashboard.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/rules_registry.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/state.cpython-313.pyc +0 -0
- package/core/workflow/announcer.py +246 -0
- package/core/workflow/dashboard.py +194 -0
- package/core/workflow/enforcer.py +234 -0
- package/core/workflow/recovery.py +196 -0
- package/core/workflow/rules_registry.py +484 -0
- package/core/workflow/session_summary.py +204 -0
- package/core/workflow/state.py +12 -2
- package/departments/dev/SKILL.md +10 -42
- package/departments/dev/skills/agent-design/SKILL.md +6 -26
- package/departments/dev/skills/ci-cd-pipeline/SKILL.md +6 -29
- package/departments/dev/skills/db-schema/SKILL.md +6 -24
- package/departments/dev/skills/dependency-audit/SKILL.md +1 -3
- package/departments/dev/skills/incident/SKILL.md +6 -24
- package/departments/dev/skills/mcp-builder/SKILL.md +4 -17
- package/departments/dev/skills/observability/SKILL.md +6 -29
- package/departments/dev/skills/performance-profiler/SKILL.md +5 -26
- package/departments/dev/skills/rag-architect/SKILL.md +5 -23
- package/departments/dev/skills/release/SKILL.md +4 -17
- package/departments/dev/skills/spec/SKILL.md +47 -148
- package/departments/landing/skills/landing-gen/SKILL.md +3 -15
- package/departments/marketing/skills/cold-email/SKILL.md +5 -17
- package/departments/marketing/skills/programmatic-seo/SKILL.md +6 -21
- package/departments/strategy/skills/board-advisor/SKILL.md +7 -21
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/synapse-bridge.py +27 -3
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
"""Forge Orchestrator — 10-step planning flow implementation.
|
|
2
|
+
|
|
3
|
+
Coordinates the full Forge planning flow:
|
|
4
|
+
1. Context snapshot (git info)
|
|
5
|
+
2. Obsidian knowledge check (patterns/plans)
|
|
6
|
+
3. Complexity analysis
|
|
7
|
+
4. Explorer dispatch (1-3 based on tier)
|
|
8
|
+
5. Critic synthesis
|
|
9
|
+
6. Constitution enforcement
|
|
10
|
+
7. Render plan
|
|
11
|
+
8. Handoff preparation
|
|
12
|
+
9. Persistence
|
|
13
|
+
|
|
14
|
+
User interaction (Approve/Revise/Companion/Detail/Quit) is handled
|
|
15
|
+
by the caller (Claude Code session) via method calls on this orchestrator.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
import subprocess
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from core.forge.schema import (
|
|
27
|
+
ForgeContext,
|
|
28
|
+
ForgePlan,
|
|
29
|
+
ForgeStatus,
|
|
30
|
+
ForgeTier,
|
|
31
|
+
ComplexityScore,
|
|
32
|
+
ComplexityDimensions,
|
|
33
|
+
ExplorerLens,
|
|
34
|
+
ExplorerApproach,
|
|
35
|
+
CriticVerdict,
|
|
36
|
+
PlanPhase,
|
|
37
|
+
ExecutionPath,
|
|
38
|
+
ForgeGovernance,
|
|
39
|
+
)
|
|
40
|
+
from core.forge.complexity import analyze_complexity
|
|
41
|
+
from core.forge.persistence import (
|
|
42
|
+
save_plan,
|
|
43
|
+
load_plan,
|
|
44
|
+
list_plans,
|
|
45
|
+
get_active_plan,
|
|
46
|
+
set_active_plan,
|
|
47
|
+
clear_active_plan,
|
|
48
|
+
export_to_obsidian,
|
|
49
|
+
extract_patterns,
|
|
50
|
+
load_patterns,
|
|
51
|
+
)
|
|
52
|
+
from core.forge.handoff import (
|
|
53
|
+
select_execution_path,
|
|
54
|
+
check_repo_drift,
|
|
55
|
+
generate_workflow_yaml,
|
|
56
|
+
)
|
|
57
|
+
from core.forge.renderer import (
|
|
58
|
+
render_terminal,
|
|
59
|
+
render_complexity,
|
|
60
|
+
render_critic_summary,
|
|
61
|
+
render_plan_overview,
|
|
62
|
+
render_html,
|
|
63
|
+
should_suggest_companion,
|
|
64
|
+
)
|
|
65
|
+
from core.forge.runtime_dispatcher import (
|
|
66
|
+
ForgeTaskDispatcher,
|
|
67
|
+
ClaudeCodeForgeDispatcher,
|
|
68
|
+
ExplorerDispatchRequest,
|
|
69
|
+
CriticDispatchRequest,
|
|
70
|
+
_tier_to_model,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
CONSTITUTION_PHASES = [
|
|
75
|
+
"Create feature branch",
|
|
76
|
+
"Write specification",
|
|
77
|
+
"Quality Gate",
|
|
78
|
+
"Persist to Obsidian",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class ForgeStep:
|
|
84
|
+
"""Current step in the forge flow."""
|
|
85
|
+
|
|
86
|
+
step_number: int
|
|
87
|
+
step_name: str
|
|
88
|
+
description: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ForgeDecision(str, Enum):
|
|
92
|
+
APPROVE = "approve"
|
|
93
|
+
REVISE = "revise"
|
|
94
|
+
COMPANION = "companion"
|
|
95
|
+
DETAIL = "detail"
|
|
96
|
+
QUIT = "quit"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class ForgeStatusOutput:
|
|
101
|
+
"""Output from a forge status check."""
|
|
102
|
+
|
|
103
|
+
plan_id: str
|
|
104
|
+
name: str
|
|
105
|
+
status: ForgeStatus
|
|
106
|
+
tier: ForgeTier
|
|
107
|
+
score: int
|
|
108
|
+
confidence: float
|
|
109
|
+
n_phases: int
|
|
110
|
+
departments: list[str]
|
|
111
|
+
created_at: str
|
|
112
|
+
revision_count: int
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class ForgeHistoryEntry:
|
|
117
|
+
"""Entry in forge history listing."""
|
|
118
|
+
|
|
119
|
+
plan_id: str
|
|
120
|
+
name: str
|
|
121
|
+
status: ForgeStatus
|
|
122
|
+
tier: ForgeTier
|
|
123
|
+
confidence: float
|
|
124
|
+
created_date: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class ForgeCompareOutput:
|
|
129
|
+
"""Output from comparing two plans."""
|
|
130
|
+
|
|
131
|
+
left: ForgeStatusOutput
|
|
132
|
+
right: ForgeStatusOutput
|
|
133
|
+
left_phases: list[PlanPhase]
|
|
134
|
+
right_phases: list[PlanPhase]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ForgeOrchestrator:
|
|
138
|
+
"""Main Forge orchestrator class.
|
|
139
|
+
|
|
140
|
+
Implements the 10-step planning flow. Stateful — maintains
|
|
141
|
+
the current plan being worked on.
|
|
142
|
+
|
|
143
|
+
Usage:
|
|
144
|
+
orch = ForgeOrchestrator()
|
|
145
|
+
plan = orch.forge("build user auth module")
|
|
146
|
+
print(orch.render())
|
|
147
|
+
# User decides: orch.approve() or orch.revise("add tests")
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(self, dispatcher: Optional[ForgeTaskDispatcher] = None):
|
|
151
|
+
"""Initialize orchestrator.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
dispatcher: Task dispatcher for subagent dispatch.
|
|
155
|
+
Defaults to ClaudeCodeForgeDispatcher.
|
|
156
|
+
"""
|
|
157
|
+
self._dispatcher = dispatcher or ClaudeCodeForgeDispatcher()
|
|
158
|
+
self._current_plan: Optional[ForgePlan] = None
|
|
159
|
+
self._current_step: Optional[ForgeStep] = None
|
|
160
|
+
|
|
161
|
+
# -------------------------------------------------------------------------
|
|
162
|
+
# Main Commands
|
|
163
|
+
# -------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def forge(self, prompt: str) -> ForgePlan:
|
|
166
|
+
"""Execute the full forge flow for a prompt.
|
|
167
|
+
|
|
168
|
+
Runs steps 1-7 (through rendering). User decision (approve/revise/etc)
|
|
169
|
+
is handled by separate method calls.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
prompt: The user's request to plan
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
ForgePlan ready for user decision
|
|
176
|
+
"""
|
|
177
|
+
self._step1_snapshot_context(prompt)
|
|
178
|
+
self._step2_obsidian_check()
|
|
179
|
+
self._step3_complexity()
|
|
180
|
+
self._step4_confirm_tier()
|
|
181
|
+
self._step5_launch_explorers()
|
|
182
|
+
self._step6_critic_synthesis()
|
|
183
|
+
self._step7_enforce_constitution()
|
|
184
|
+
self._step8_build_plan()
|
|
185
|
+
self._step9_render()
|
|
186
|
+
return self._current_plan
|
|
187
|
+
|
|
188
|
+
def resume(self) -> Optional[ForgePlan]:
|
|
189
|
+
"""Resume the active forge plan.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Active plan if exists, None otherwise
|
|
193
|
+
"""
|
|
194
|
+
active = get_active_plan()
|
|
195
|
+
if active is None:
|
|
196
|
+
return None
|
|
197
|
+
self._current_plan = active
|
|
198
|
+
return active
|
|
199
|
+
|
|
200
|
+
def status(self) -> Optional[ForgeStatusOutput]:
|
|
201
|
+
"""Get status of the active plan.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
ForgeStatusOutput if active plan exists, None otherwise
|
|
205
|
+
"""
|
|
206
|
+
if self._current_plan is None:
|
|
207
|
+
self._current_plan = get_active_plan()
|
|
208
|
+
if self._current_plan is None:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
plan = self._current_plan
|
|
212
|
+
depts = list({p.department for p in plan.plan_phases})
|
|
213
|
+
return ForgeStatusOutput(
|
|
214
|
+
plan_id=plan.id,
|
|
215
|
+
name=plan.name,
|
|
216
|
+
status=plan.status,
|
|
217
|
+
tier=plan.complexity.tier,
|
|
218
|
+
score=plan.complexity.score,
|
|
219
|
+
confidence=plan.critic.confidence,
|
|
220
|
+
n_phases=len(plan.plan_phases),
|
|
221
|
+
departments=depts,
|
|
222
|
+
created_at=plan.created_at,
|
|
223
|
+
revision_count=plan.version - 1,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def history(self, limit: int = 20) -> list[ForgeHistoryEntry]:
|
|
227
|
+
"""List past forge plans.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
limit: Maximum number to return
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of ForgeHistoryEntry
|
|
234
|
+
"""
|
|
235
|
+
plans = list_plans()
|
|
236
|
+
entries = []
|
|
237
|
+
for p in plans[:limit]:
|
|
238
|
+
entries.append(
|
|
239
|
+
ForgeHistoryEntry(
|
|
240
|
+
plan_id=p["id"],
|
|
241
|
+
name=p["name"],
|
|
242
|
+
status=ForgeStatus(p.get("status", "draft")),
|
|
243
|
+
tier=ForgeTier(p.get("complexity", {}).get("tier", "shallow")),
|
|
244
|
+
confidence=p.get("critic", {}).get("confidence", 0.0),
|
|
245
|
+
created_date=p.get("created_at", "")[:10],
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
return entries
|
|
249
|
+
|
|
250
|
+
def show(self, plan_id: str) -> Optional[ForgePlan]:
|
|
251
|
+
"""Load and return a specific plan by ID.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
plan_id: Plan ID to load
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
ForgePlan if found, None otherwise
|
|
258
|
+
"""
|
|
259
|
+
plan = load_plan(plan_id)
|
|
260
|
+
if plan:
|
|
261
|
+
self._current_plan = plan
|
|
262
|
+
return plan
|
|
263
|
+
|
|
264
|
+
def compare(self, id1: str, id2: str) -> Optional[ForgeCompareOutput]:
|
|
265
|
+
"""Compare two plans side by side.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
id1: Left plan ID
|
|
269
|
+
id2: Right plan ID
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
ForgeCompareOutput if both found, None otherwise
|
|
273
|
+
"""
|
|
274
|
+
left = load_plan(id1)
|
|
275
|
+
right = load_plan(id2)
|
|
276
|
+
if not left or not right:
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
return ForgeCompareOutput(
|
|
280
|
+
left=self._plan_to_status(left),
|
|
281
|
+
right=self._plan_to_status(right),
|
|
282
|
+
left_phases=left.plan_phases,
|
|
283
|
+
right_phases=right.plan_phases,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def patterns(self) -> list[dict]:
|
|
287
|
+
"""List reusable patterns from past plans.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
List of pattern dicts
|
|
291
|
+
"""
|
|
292
|
+
return load_patterns()
|
|
293
|
+
|
|
294
|
+
def cancel(self) -> bool:
|
|
295
|
+
"""Cancel the active plan.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
True if cancelled, False if no active plan
|
|
299
|
+
"""
|
|
300
|
+
if self._current_plan is None:
|
|
301
|
+
self._current_plan = get_active_plan()
|
|
302
|
+
if self._current_plan is None:
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
self._current_plan.status = ForgeStatus.CANCELLED
|
|
306
|
+
save_plan(self._current_plan)
|
|
307
|
+
clear_active_plan()
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
# -------------------------------------------------------------------------
|
|
311
|
+
# User Decision Handlers
|
|
312
|
+
# -------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
def approve(self) -> ForgePlan:
|
|
315
|
+
"""Handle user Approve decision.
|
|
316
|
+
|
|
317
|
+
Runs steps 9-10: handoff and persist.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Approved ForgePlan
|
|
321
|
+
"""
|
|
322
|
+
if self._current_plan is None:
|
|
323
|
+
raise RuntimeError("No active forge plan. Run forge() first.")
|
|
324
|
+
|
|
325
|
+
self._step9_handoff()
|
|
326
|
+
self._step10_persist()
|
|
327
|
+
return self._current_plan
|
|
328
|
+
|
|
329
|
+
def revise(self, revision_text: str) -> ForgePlan:
|
|
330
|
+
"""Handle user Revise decision.
|
|
331
|
+
|
|
332
|
+
Re-runs critic only (not explorers), capped at 5 revisions.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
revision_text: User's revision request
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Updated ForgePlan
|
|
339
|
+
"""
|
|
340
|
+
if self._current_plan is None:
|
|
341
|
+
raise RuntimeError("No active forge plan. Run forge() first.")
|
|
342
|
+
|
|
343
|
+
plan = self._current_plan
|
|
344
|
+
if plan.version > 5:
|
|
345
|
+
raise RuntimeError("Maximum 5 revisions exceeded.")
|
|
346
|
+
|
|
347
|
+
self._step6_critic_synthesis(revision_request=revision_text)
|
|
348
|
+
self._step7_enforce_constitution()
|
|
349
|
+
self._step8_build_plan()
|
|
350
|
+
return self._current_plan
|
|
351
|
+
|
|
352
|
+
def companion(self) -> str:
|
|
353
|
+
"""Generate HTML visual companion.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Path to HTML file
|
|
357
|
+
"""
|
|
358
|
+
if self._current_plan is None:
|
|
359
|
+
raise RuntimeError("No active forge plan.")
|
|
360
|
+
|
|
361
|
+
html = render_html(self._current_plan)
|
|
362
|
+
path = f"/tmp/forge-{self._current_plan.id}.html"
|
|
363
|
+
Path(path).write_text(html, encoding="utf-8")
|
|
364
|
+
return path
|
|
365
|
+
|
|
366
|
+
def detail(self, phase_index: int) -> Optional[str]:
|
|
367
|
+
"""Get detail for a specific phase.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
phase_index: 1-based phase number
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Detailed string for the phase, or None if not found
|
|
374
|
+
"""
|
|
375
|
+
if self._current_plan is None:
|
|
376
|
+
return None
|
|
377
|
+
if phase_index < 1 or phase_index > len(self._current_plan.plan_phases):
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
phase = self._current_plan.plan_phases[phase_index - 1]
|
|
381
|
+
lines = [
|
|
382
|
+
f"Phase {phase_index}: {phase.name}",
|
|
383
|
+
f"Department: {phase.department}",
|
|
384
|
+
f"Agents: {', '.join(phase.agents) if phase.agents else 'none'}",
|
|
385
|
+
"",
|
|
386
|
+
"Deliverables:",
|
|
387
|
+
]
|
|
388
|
+
for d in phase.deliverables or []:
|
|
389
|
+
lines.append(f" - {d}")
|
|
390
|
+
lines.extend(["", "Acceptance Criteria:"])
|
|
391
|
+
for c in phase.acceptance_criteria or []:
|
|
392
|
+
lines.append(f" - {c}")
|
|
393
|
+
if phase.depends_on:
|
|
394
|
+
lines.extend(["", f"Depends on: {', '.join(phase.depends_on)}"])
|
|
395
|
+
return "\n".join(lines)
|
|
396
|
+
|
|
397
|
+
def quit(self) -> ForgePlan:
|
|
398
|
+
"""Handle user Quit decision.
|
|
399
|
+
|
|
400
|
+
Saves as draft and clears active.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Draft ForgePlan
|
|
404
|
+
"""
|
|
405
|
+
if self._current_plan is None:
|
|
406
|
+
raise RuntimeError("No active forge plan.")
|
|
407
|
+
|
|
408
|
+
self._current_plan.status = ForgeStatus.DRAFT
|
|
409
|
+
save_plan(self._current_plan)
|
|
410
|
+
clear_active_plan()
|
|
411
|
+
return self._current_plan
|
|
412
|
+
|
|
413
|
+
# -------------------------------------------------------------------------
|
|
414
|
+
# Render Output
|
|
415
|
+
# -------------------------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
def render(self) -> str:
|
|
418
|
+
"""Render current plan for terminal display.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
Terminal-formatted string
|
|
422
|
+
"""
|
|
423
|
+
if self._current_plan is None:
|
|
424
|
+
return "No active forge plan."
|
|
425
|
+
return render_terminal(self._current_plan)
|
|
426
|
+
|
|
427
|
+
def render_complexity(self) -> str:
|
|
428
|
+
"""Render complexity analysis.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Complexity analysis string
|
|
432
|
+
"""
|
|
433
|
+
if self._current_plan is None:
|
|
434
|
+
return ""
|
|
435
|
+
return render_complexity(self._current_plan.complexity)
|
|
436
|
+
|
|
437
|
+
def _step9_render(self) -> None:
|
|
438
|
+
"""Step 9: Render the plan for user review."""
|
|
439
|
+
if self._current_plan is None:
|
|
440
|
+
return
|
|
441
|
+
self._rendered_output = render_terminal(self._current_plan)
|
|
442
|
+
|
|
443
|
+
# -------------------------------------------------------------------------
|
|
444
|
+
# Internal Steps
|
|
445
|
+
# -------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
def _step1_snapshot_context(self, prompt: str) -> None:
|
|
448
|
+
"""Step 1: Collect git context."""
|
|
449
|
+
try:
|
|
450
|
+
git_rev = subprocess.run(
|
|
451
|
+
["git", "rev-parse", "HEAD"], capture_output=True, text=True, check=True
|
|
452
|
+
)
|
|
453
|
+
commit = git_rev.stdout.strip()
|
|
454
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
455
|
+
commit = "unknown"
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
git_branch = subprocess.run(
|
|
459
|
+
["git", "branch", "--show-current"], capture_output=True, text=True, check=True
|
|
460
|
+
)
|
|
461
|
+
branch = git_branch.stdout.strip()
|
|
462
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
463
|
+
branch = "unknown"
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
git_remote = subprocess.run(
|
|
467
|
+
["git", "remote", "get-url", "origin"], capture_output=True, text=True
|
|
468
|
+
)
|
|
469
|
+
repo = git_remote.stdout.strip()
|
|
470
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
471
|
+
repo = Path.cwd().name
|
|
472
|
+
|
|
473
|
+
version_file = Path(__file__).parent.parent.parent / "VERSION"
|
|
474
|
+
version = version_file.read_text().strip() if version_file.exists() else "unknown"
|
|
475
|
+
|
|
476
|
+
self._forge_context = ForgeContext(
|
|
477
|
+
repo=repo,
|
|
478
|
+
branch=branch,
|
|
479
|
+
commit_at_forge=commit,
|
|
480
|
+
arkaos_version=version,
|
|
481
|
+
prompt=prompt,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
def _step2_obsidian_check(self) -> None:
|
|
485
|
+
"""Step 2: Check Obsidian for similar plans and patterns."""
|
|
486
|
+
self._similar_plans: list[str] = []
|
|
487
|
+
self._reused_patterns: list[str] = []
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
patterns = load_patterns()
|
|
491
|
+
for p in patterns:
|
|
492
|
+
if any(kw in p.get("tags", []) for kw in ["dev", "forge"]):
|
|
493
|
+
self._reused_patterns.append(p.get("name", ""))
|
|
494
|
+
except Exception:
|
|
495
|
+
pass
|
|
496
|
+
|
|
497
|
+
def _step3_complexity(self) -> None:
|
|
498
|
+
"""Step 3: Analyze complexity."""
|
|
499
|
+
prompt = self._forge_context.prompt
|
|
500
|
+
affected_files = self._estimate_affected_files(prompt)
|
|
501
|
+
departments = self._estimate_departments(prompt)
|
|
502
|
+
|
|
503
|
+
result = analyze_complexity(
|
|
504
|
+
prompt=prompt,
|
|
505
|
+
affected_files=affected_files,
|
|
506
|
+
departments=departments,
|
|
507
|
+
similar_plans=self._similar_plans,
|
|
508
|
+
reused_patterns=self._reused_patterns,
|
|
509
|
+
)
|
|
510
|
+
self._complexity = result
|
|
511
|
+
|
|
512
|
+
def _step4_confirm_tier(self) -> ForgeTier:
|
|
513
|
+
"""Step 4: Confirm tier (returns current tier, caller handles user input).
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
Confirmed ForgeTier
|
|
517
|
+
"""
|
|
518
|
+
self._confirmed_tier = self._complexity.tier
|
|
519
|
+
return self._confirmed_tier
|
|
520
|
+
|
|
521
|
+
def set_tier(self, tier: ForgeTier) -> None:
|
|
522
|
+
"""Override the confirmed tier (after user override request).
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
tier: New tier to use
|
|
526
|
+
"""
|
|
527
|
+
self._confirmed_tier = tier
|
|
528
|
+
|
|
529
|
+
def _step5_launch_explorers(self) -> None:
|
|
530
|
+
"""Step 5: Launch explorer subagents in parallel."""
|
|
531
|
+
tier = self._confirmed_tier
|
|
532
|
+
lens_map = {
|
|
533
|
+
ForgeTier.SHALLOW: [ExplorerLens.PRAGMATIC],
|
|
534
|
+
ForgeTier.STANDARD: [ExplorerLens.PRAGMATIC, ExplorerLens.ARCHITECTURAL],
|
|
535
|
+
ForgeTier.DEEP: [
|
|
536
|
+
ExplorerLens.PRAGMATIC,
|
|
537
|
+
ExplorerLens.ARCHITECTURAL,
|
|
538
|
+
ExplorerLens.CONTRARIAN,
|
|
539
|
+
],
|
|
540
|
+
}
|
|
541
|
+
lenses = lens_map.get(tier, [ExplorerLens.PRAGMATIC])
|
|
542
|
+
model = _tier_to_model(tier)
|
|
543
|
+
|
|
544
|
+
self._approaches: list[ExplorerApproach] = []
|
|
545
|
+
requests = [
|
|
546
|
+
ExplorerDispatchRequest(
|
|
547
|
+
lens=lens,
|
|
548
|
+
prompt=self._forge_context.prompt,
|
|
549
|
+
context=self._forge_context,
|
|
550
|
+
similar_plans=self._similar_plans,
|
|
551
|
+
reused_patterns=self._reused_patterns,
|
|
552
|
+
model=model,
|
|
553
|
+
)
|
|
554
|
+
for lens in lenses
|
|
555
|
+
]
|
|
556
|
+
|
|
557
|
+
for req in requests:
|
|
558
|
+
try:
|
|
559
|
+
approach = self._dispatcher.dispatch_explorer_and_parse(req)
|
|
560
|
+
self._approaches.append(approach)
|
|
561
|
+
except Exception as e:
|
|
562
|
+
pass
|
|
563
|
+
|
|
564
|
+
def _step6_critic_synthesis(self, revision_request: Optional[str] = None) -> None:
|
|
565
|
+
"""Step 6: Launch critic subagent for synthesis.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
revision_request: Optional revision text from user
|
|
569
|
+
"""
|
|
570
|
+
model = _tier_to_model(self._confirmed_tier)
|
|
571
|
+
critic_req = CriticDispatchRequest(
|
|
572
|
+
original_prompt=self._forge_context.prompt,
|
|
573
|
+
approaches=self._approaches,
|
|
574
|
+
model=model,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
try:
|
|
578
|
+
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
|
+
)
|
|
587
|
+
|
|
588
|
+
if revision_request:
|
|
589
|
+
self._critic_verdict.synthesis["revision_request"] = [revision_request]
|
|
590
|
+
|
|
591
|
+
def _step7_enforce_constitution(self) -> None:
|
|
592
|
+
"""Step 7: Enforce Constitution rules - add missing phases."""
|
|
593
|
+
self._enforced_phases: list[PlanPhase] = []
|
|
594
|
+
phase_names = [p.name.lower() for p in self._final_phases_from_critic()]
|
|
595
|
+
|
|
596
|
+
has_branch = any("branch" in n or "feature" in n for n in phase_names)
|
|
597
|
+
has_spec = any("spec" in n or "specification" in n for n in phase_names)
|
|
598
|
+
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)
|
|
600
|
+
|
|
601
|
+
enforced = []
|
|
602
|
+
if not has_branch:
|
|
603
|
+
enforced.append(
|
|
604
|
+
PlanPhase(
|
|
605
|
+
name="Create feature branch",
|
|
606
|
+
department="dev",
|
|
607
|
+
agents=["developer"],
|
|
608
|
+
deliverables=["feature/forge-xyz branch created"],
|
|
609
|
+
acceptance_criteria=["git branch created from main"],
|
|
610
|
+
)
|
|
611
|
+
)
|
|
612
|
+
if not has_spec and self._confirmed_tier != ForgeTier.SHALLOW:
|
|
613
|
+
enforced.append(
|
|
614
|
+
PlanPhase(
|
|
615
|
+
name="Write specification",
|
|
616
|
+
department="dev",
|
|
617
|
+
agents=["architect"],
|
|
618
|
+
deliverables=["SPEC.md created"],
|
|
619
|
+
acceptance_criteria=["spec approved"],
|
|
620
|
+
)
|
|
621
|
+
)
|
|
622
|
+
if not has_qg:
|
|
623
|
+
enforced.append(
|
|
624
|
+
PlanPhase(
|
|
625
|
+
name="Quality Gate",
|
|
626
|
+
department="quality",
|
|
627
|
+
agents=["cqo"],
|
|
628
|
+
deliverables=["QA review completed"],
|
|
629
|
+
acceptance_criteria=["Marta APPROVED"],
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
if not has_obsidian:
|
|
633
|
+
enforced.append(
|
|
634
|
+
PlanPhase(
|
|
635
|
+
name="Persist to Obsidian",
|
|
636
|
+
department="kb",
|
|
637
|
+
agents=["knowledge-curator"],
|
|
638
|
+
deliverables=["plan archived in Obsidian"],
|
|
639
|
+
acceptance_criteria=["Obsidian note created"],
|
|
640
|
+
)
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
self._enforced_phases = enforced
|
|
644
|
+
|
|
645
|
+
def _step8_build_plan(self) -> None:
|
|
646
|
+
"""Step 8: Build the ForgePlan object."""
|
|
647
|
+
plan_id = self._generate_plan_id(self._forge_context.prompt)
|
|
648
|
+
execution_path = select_execution_path(self._final_phases_from_critic())
|
|
649
|
+
|
|
650
|
+
self._current_plan = ForgePlan(
|
|
651
|
+
id=plan_id,
|
|
652
|
+
name=self._forge_context.prompt[:60],
|
|
653
|
+
created_at=datetime.now(timezone.utc).isoformat(),
|
|
654
|
+
forged_by="forge",
|
|
655
|
+
version=1,
|
|
656
|
+
context=self._forge_context,
|
|
657
|
+
complexity=self._complexity,
|
|
658
|
+
approaches=self._approaches,
|
|
659
|
+
critic=self._critic_verdict,
|
|
660
|
+
plan_phases=self._final_phases_from_critic(),
|
|
661
|
+
goal=self._forge_context.prompt,
|
|
662
|
+
execution_path=execution_path,
|
|
663
|
+
governance=ForgeGovernance(
|
|
664
|
+
constitution_check="passed",
|
|
665
|
+
quality_gate_required=True,
|
|
666
|
+
branch_strategy=f"feature/{plan_id}",
|
|
667
|
+
),
|
|
668
|
+
status=ForgeStatus.REVIEWING,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
def _step9_handoff(self) -> None:
|
|
672
|
+
"""Step 9: Handoff - check drift, select execution path."""
|
|
673
|
+
drift = check_repo_drift(self._current_plan.context.commit_at_forge)
|
|
674
|
+
self._drift = drift
|
|
675
|
+
|
|
676
|
+
if drift["changed"]:
|
|
677
|
+
pass
|
|
678
|
+
|
|
679
|
+
self._current_plan.status = ForgeStatus.APPROVED
|
|
680
|
+
self._current_plan.approved_at = datetime.now(timezone.utc).isoformat()
|
|
681
|
+
|
|
682
|
+
exec_path = select_execution_path(self._current_plan.plan_phases)
|
|
683
|
+
self._current_plan.execution_path = exec_path
|
|
684
|
+
|
|
685
|
+
def _step10_persist(self) -> None:
|
|
686
|
+
"""Step 10: Persist plan to YAML and Obsidian."""
|
|
687
|
+
save_plan(self._current_plan)
|
|
688
|
+
set_active_plan(self._current_plan.id)
|
|
689
|
+
|
|
690
|
+
try:
|
|
691
|
+
export_to_obsidian(self._current_plan)
|
|
692
|
+
except Exception:
|
|
693
|
+
pass
|
|
694
|
+
|
|
695
|
+
try:
|
|
696
|
+
patterns = extract_patterns(self._current_plan)
|
|
697
|
+
if patterns:
|
|
698
|
+
pass
|
|
699
|
+
except Exception:
|
|
700
|
+
pass
|
|
701
|
+
|
|
702
|
+
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)
|
|
715
|
+
|
|
716
|
+
return phases
|
|
717
|
+
|
|
718
|
+
def _generate_plan_id(self, prompt: str) -> str:
|
|
719
|
+
"""Generate a forge plan ID."""
|
|
720
|
+
date = datetime.now(timezone.utc).strftime("%Y%m%d")
|
|
721
|
+
suffix = hashlib.md5(prompt.encode()).hexdigest()[:4]
|
|
722
|
+
return f"forge-{date}-{suffix}"
|
|
723
|
+
|
|
724
|
+
def _estimate_affected_files(self, prompt: str) -> list[str]:
|
|
725
|
+
"""Estimate affected files from prompt keywords."""
|
|
726
|
+
keywords = {
|
|
727
|
+
"auth": ["auth/", "middleware/auth.py"],
|
|
728
|
+
"db": ["core/db/", "models/"],
|
|
729
|
+
"api": ["routes/", "controllers/"],
|
|
730
|
+
"test": ["tests/"],
|
|
731
|
+
"config": ["config/", ".env"],
|
|
732
|
+
}
|
|
733
|
+
files = []
|
|
734
|
+
lower = prompt.lower()
|
|
735
|
+
for kw, paths in keywords.items():
|
|
736
|
+
if kw in lower:
|
|
737
|
+
files.extend(paths)
|
|
738
|
+
return files
|
|
739
|
+
|
|
740
|
+
def _estimate_departments(self, prompt: str) -> list[str]:
|
|
741
|
+
"""Estimate departments from prompt keywords."""
|
|
742
|
+
keywords = {
|
|
743
|
+
"dev": ["dev"],
|
|
744
|
+
"marketing": ["mkt", "marketing", "content"],
|
|
745
|
+
"brand": ["brand", "design"],
|
|
746
|
+
"sales": ["sales"],
|
|
747
|
+
"ops": ["ops", "operations"],
|
|
748
|
+
}
|
|
749
|
+
depts = []
|
|
750
|
+
lower = prompt.lower()
|
|
751
|
+
for kw, depts_list in keywords.items():
|
|
752
|
+
if kw in lower:
|
|
753
|
+
depts.extend(depts_list)
|
|
754
|
+
return list(set(depts)) or ["dev"]
|
|
755
|
+
|
|
756
|
+
def _plan_to_status(self, plan: ForgePlan) -> ForgeStatusOutput:
|
|
757
|
+
"""Convert ForgePlan to ForgeStatusOutput."""
|
|
758
|
+
depts = list({p.department for p in plan.plan_phases})
|
|
759
|
+
return ForgeStatusOutput(
|
|
760
|
+
plan_id=plan.id,
|
|
761
|
+
name=plan.name,
|
|
762
|
+
status=plan.status,
|
|
763
|
+
tier=plan.complexity.tier,
|
|
764
|
+
score=plan.complexity.score,
|
|
765
|
+
confidence=plan.critic.confidence,
|
|
766
|
+
n_phases=len(plan.plan_phases),
|
|
767
|
+
departments=depts,
|
|
768
|
+
created_at=plan.created_at,
|
|
769
|
+
revision_count=plan.version - 1,
|
|
770
|
+
)
|