maestro-flow 0.4.19 → 0.4.21
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/.agents/agents/workflow-collab-planner.md +4 -1
- package/.agents/agents/workflow-plan-checker.md +11 -1
- package/.agents/agents/workflow-planner.md +4 -1
- package/.agents/skills/maestro/SKILL.md +8 -5
- package/.agents/skills/maestro-analyze/SKILL.md +1 -1
- package/.agents/skills/maestro-brainstorm/SKILL.md +2 -1
- package/.agents/skills/maestro-companion/SKILL.md +533 -0
- package/.agents/skills/maestro-grill/SKILL.md +116 -0
- package/.agents/skills/maestro-plan/SKILL.md +4 -0
- package/.agents/skills/maestro-ralph/SKILL.md +11 -7
- package/.agents/skills/maestro-ralph-execute/SKILL.md +2 -1
- package/.agents/skills/maestro-swarm-workflow/SKILL.md +266 -0
- package/.agents/skills/maestro-universal-workflow/SKILL.md +563 -0
- package/.agents/skills/manage-codebase-rebuild/SKILL.md +13 -1
- package/.agents/skills/manage-codebase-refresh/SKILL.md +3 -0
- package/.agents/skills/spec-setup/SKILL.md +9 -5
- package/.agents/skills/team-adversarial-swarm/SKILL.md +235 -0
- package/.agents/skills/team-adversarial-swarm/scripts/aco.py +473 -0
- package/.agents/skills/team-adversarial-swarm/scripts/pheromone.py +144 -0
- package/.agents/skills/team-adversarial-swarm/scripts/scoring.py +92 -0
- package/.agents/skills/team-adversarial-swarm/scripts/test_aco.py +475 -0
- package/.agents/skills/team-adversarial-swarm/specs/ant-output-schema.md +115 -0
- package/.agents/skills/team-adversarial-swarm/specs/convergence-criteria.md +75 -0
- package/.agents/skills/team-adversarial-swarm/specs/pheromone-schema.md +90 -0
- package/.agents/skills/team-adversarial-swarm/specs/swarm-config-template.json +66 -0
- package/.agents/skills/team-adversarial-swarm/specs/swarm-protocol.md +105 -0
- package/.agents/skills/team-adversarial-swarm/workflows/wf-swarm-converge.js +197 -0
- package/.agents/skills/team-adversarial-swarm/workflows/wf-swarm-explore.js +194 -0
- package/.agents/skills/team-adversarial-swarm/workflows/wf-swarm-score.js +188 -0
- package/.agents/skills/team-adversarial-swarm/workflows/wf-swarm-synthesize.js +248 -0
- package/.agy/agents/workflow-collab-planner.md +4 -1
- package/.agy/agents/workflow-plan-checker.md +11 -1
- package/.agy/agents/workflow-planner.md +4 -1
- package/.agy/skills/maestro/SKILL.md +8 -5
- package/.agy/skills/maestro-analyze/SKILL.md +1 -1
- package/.agy/skills/maestro-brainstorm/SKILL.md +2 -1
- package/.agy/skills/maestro-companion/SKILL.md +529 -0
- package/.agy/skills/maestro-grill/SKILL.md +116 -0
- package/.agy/skills/maestro-plan/SKILL.md +4 -0
- package/.agy/skills/maestro-ralph/SKILL.md +11 -7
- package/.agy/skills/maestro-ralph-execute/SKILL.md +2 -1
- package/.agy/skills/maestro-swarm-workflow/SKILL.md +263 -0
- package/.agy/skills/maestro-universal-workflow/SKILL.md +560 -0
- package/.agy/skills/manage-codebase-rebuild/SKILL.md +13 -1
- package/.agy/skills/manage-codebase-refresh/SKILL.md +3 -0
- package/.agy/skills/spec-setup/SKILL.md +9 -5
- package/.agy/skills/team-adversarial-swarm/SKILL.md +244 -0
- package/.agy/skills/team-adversarial-swarm/scripts/aco.py +473 -0
- package/.agy/skills/team-adversarial-swarm/scripts/pheromone.py +144 -0
- package/.agy/skills/team-adversarial-swarm/scripts/scoring.py +92 -0
- package/.agy/skills/team-adversarial-swarm/scripts/test_aco.py +475 -0
- package/.agy/skills/team-adversarial-swarm/specs/ant-output-schema.md +115 -0
- package/.agy/skills/team-adversarial-swarm/specs/convergence-criteria.md +75 -0
- package/.agy/skills/team-adversarial-swarm/specs/pheromone-schema.md +90 -0
- package/.agy/skills/team-adversarial-swarm/specs/swarm-config-template.json +66 -0
- package/.agy/skills/team-adversarial-swarm/specs/swarm-protocol.md +105 -0
- package/.agy/skills/team-adversarial-swarm/workflows/wf-swarm-converge.js +197 -0
- package/.agy/skills/team-adversarial-swarm/workflows/wf-swarm-explore.js +194 -0
- package/.agy/skills/team-adversarial-swarm/workflows/wf-swarm-score.js +188 -0
- package/.agy/skills/team-adversarial-swarm/workflows/wf-swarm-synthesize.js +248 -0
- package/.claude/agents/workflow-collab-planner.md +4 -1
- package/.claude/agents/workflow-plan-checker.md +11 -1
- package/.claude/agents/workflow-planner.md +4 -1
- package/.claude/commands/maestro-analyze.md +1 -1
- package/.claude/commands/maestro-brainstorm.md +2 -1
- package/.claude/commands/maestro-companion.md +531 -0
- package/.claude/commands/maestro-grill.md +114 -0
- package/.claude/commands/maestro-plan.md +4 -0
- package/.claude/commands/maestro-ralph-execute.md +2 -1
- package/.claude/commands/maestro-ralph.md +11 -7
- package/.claude/commands/maestro-swarm-workflow.md +264 -0
- package/.claude/commands/maestro-universal-workflow.md +561 -0
- package/.claude/commands/maestro.md +8 -5
- package/.claude/commands/manage-codebase-rebuild.md +13 -1
- package/.claude/commands/manage-codebase-refresh.md +3 -0
- package/.claude/commands/spec-setup.md +9 -5
- package/.claude/skills/team-adversarial-swarm/SKILL.md +233 -0
- package/.claude/skills/team-adversarial-swarm/scripts/aco.py +473 -0
- package/.claude/skills/team-adversarial-swarm/scripts/pheromone.py +144 -0
- package/.claude/skills/team-adversarial-swarm/scripts/scoring.py +92 -0
- package/.claude/skills/team-adversarial-swarm/scripts/test_aco.py +475 -0
- package/.claude/skills/team-adversarial-swarm/specs/ant-output-schema.md +115 -0
- package/.claude/skills/team-adversarial-swarm/specs/convergence-criteria.md +75 -0
- package/.claude/skills/team-adversarial-swarm/specs/pheromone-schema.md +90 -0
- package/.claude/skills/team-adversarial-swarm/specs/swarm-config-template.json +66 -0
- package/.claude/skills/team-adversarial-swarm/specs/swarm-protocol.md +105 -0
- package/.claude/skills/team-adversarial-swarm/workflows/wf-swarm-converge.js +197 -0
- package/.claude/skills/team-adversarial-swarm/workflows/wf-swarm-explore.js +194 -0
- package/.claude/skills/team-adversarial-swarm/workflows/wf-swarm-score.js +188 -0
- package/.claude/skills/team-adversarial-swarm/workflows/wf-swarm-synthesize.js +248 -0
- package/.codex/skills/maestro/SKILL.md +7 -2
- package/.codex/skills/maestro-companion/SKILL.md +485 -0
- package/.codex/skills/maestro-grill/SKILL.md +111 -0
- package/.codex/skills/maestro-ralph/SKILL.md +11 -7
- package/.codex/skills/manage-codebase-rebuild/SKILL.md +6 -0
- package/.codex/skills/manage-codebase-refresh/SKILL.md +6 -0
- package/dashboard/dist-server/dashboard/src/server/wiki/graph-analysis.d.ts +36 -0
- package/dashboard/dist-server/dashboard/src/server/wiki/graph-analysis.js +138 -2
- package/dashboard/dist-server/dashboard/src/server/wiki/graph-analysis.js.map +1 -1
- package/dashboard/dist-server/dashboard/src/server/wiki/search.js +13 -0
- package/dashboard/dist-server/dashboard/src/server/wiki/search.js.map +1 -1
- package/dashboard/dist-server/dashboard/src/server/wiki/virtual-wiki-adapters.d.ts +11 -0
- package/dashboard/dist-server/dashboard/src/server/wiki/virtual-wiki-adapters.js +178 -0
- package/dashboard/dist-server/dashboard/src/server/wiki/virtual-wiki-adapters.js.map +1 -1
- package/dashboard/dist-server/dashboard/src/server/wiki/wiki-indexer.d.ts +1 -0
- package/dashboard/dist-server/dashboard/src/server/wiki/wiki-indexer.js +39 -23
- package/dashboard/dist-server/dashboard/src/server/wiki/wiki-indexer.js.map +1 -1
- package/dashboard/dist-server/src/graph/types.d.ts +111 -0
- package/dashboard/dist-server/src/graph/types.js +2 -0
- package/dashboard/dist-server/src/graph/types.js.map +1 -0
- package/dist/src/cli.js +1 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/kg.d.ts +11 -0
- package/dist/src/commands/kg.d.ts.map +1 -0
- package/dist/src/commands/kg.js +486 -0
- package/dist/src/commands/kg.js.map +1 -0
- package/dist/src/graph/analyzers/fs-analyzer.d.ts +10 -0
- package/dist/src/graph/analyzers/fs-analyzer.d.ts.map +1 -0
- package/dist/src/graph/analyzers/fs-analyzer.js +959 -0
- package/dist/src/graph/analyzers/fs-analyzer.js.map +1 -0
- package/dist/src/graph/index.d.ts +6 -0
- package/dist/src/graph/index.d.ts.map +1 -0
- package/dist/src/graph/index.js +6 -0
- package/dist/src/graph/index.js.map +1 -0
- package/dist/src/graph/loader.d.ts +3 -0
- package/dist/src/graph/loader.d.ts.map +1 -0
- package/dist/src/graph/loader.js +12 -0
- package/dist/src/graph/loader.js.map +1 -0
- package/dist/src/graph/merger.d.ts +56 -0
- package/dist/src/graph/merger.d.ts.map +1 -0
- package/dist/src/graph/merger.js +896 -0
- package/dist/src/graph/merger.js.map +1 -0
- package/dist/src/graph/query.d.ts +7 -0
- package/dist/src/graph/query.d.ts.map +1 -0
- package/dist/src/graph/query.js +126 -0
- package/dist/src/graph/query.js.map +1 -0
- package/dist/src/graph/types.d.ts +112 -0
- package/dist/src/graph/types.d.ts.map +1 -0
- package/dist/src/graph/types.js +2 -0
- package/dist/src/graph/types.js.map +1 -0
- package/dist/src/tui/install-ui/KgVendorConfig.d.ts +7 -0
- package/dist/src/tui/install-ui/KgVendorConfig.d.ts.map +1 -0
- package/dist/src/tui/install-ui/KgVendorConfig.js +9 -0
- package/dist/src/tui/install-ui/KgVendorConfig.js.map +1 -0
- package/dist/src/utils/update-notices.js +23 -0
- package/dist/src/utils/update-notices.js.map +1 -1
- package/package.json +1 -1
- package/workflows/analyze.md +2 -1
- package/workflows/brainstorm.md +24 -1
- package/workflows/codebase-rebuild.md +141 -1
- package/workflows/codebase-refresh.md +20 -0
- package/workflows/finish-work.md +7 -2
- package/workflows/grill.md +513 -0
- package/workflows/plan.md +7 -4
- package/workflows/specs-setup.md +99 -3
- package/workflows/swarm/wf-analyze.js +347 -0
- package/workflows/swarm/wf-brainstorm.js +456 -0
- package/workflows/swarm/wf-execute.js +379 -0
- package/workflows/swarm/wf-grill.js +359 -0
- package/workflows/swarm/wf-milestone-audit.js +385 -0
- package/workflows/swarm/wf-plan.js +468 -0
- package/workflows/swarm/wf-review.js +341 -0
- package/workflows/swarm/wf-verify.js +395 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""ACO controller CLI - the script side of team-swarm.
|
|
2
|
+
|
|
3
|
+
Subcommands: init | select | update | converged | report
|
|
4
|
+
Spec: ../specs/swarm-protocol.md (script <-> coordinator contract)
|
|
5
|
+
|
|
6
|
+
All commands:
|
|
7
|
+
- read state from --session <path>
|
|
8
|
+
- emit JSON to stdout
|
|
9
|
+
- exit 0 success, 1 runtime error, 2 config invalid
|
|
10
|
+
|
|
11
|
+
Invoked by team-swarm coordinator via Bash. No prose output, no interactive prompts.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import glob
|
|
17
|
+
import json
|
|
18
|
+
import random
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import List, Optional
|
|
23
|
+
|
|
24
|
+
# Local imports (script directory)
|
|
25
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
26
|
+
from pheromone import PheromoneState # noqa: E402
|
|
27
|
+
from scoring import ( # noqa: E402
|
|
28
|
+
FallbackScorer,
|
|
29
|
+
ScriptScorer,
|
|
30
|
+
hallucination_check,
|
|
31
|
+
load_verified_scores,
|
|
32
|
+
resolve_score,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
VERSION = "1.0"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Session paths
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
class SessionPaths:
|
|
43
|
+
def __init__(self, session: Path):
|
|
44
|
+
self.session = session
|
|
45
|
+
self.config = session / "swarm-config.json"
|
|
46
|
+
self.pheromone_dir = session / "pheromone"
|
|
47
|
+
self.pheromone_current = self.pheromone_dir / "current.json"
|
|
48
|
+
self.pheromone_init = self.pheromone_dir / "init.json"
|
|
49
|
+
self.pheromone_history = self.pheromone_dir / "history"
|
|
50
|
+
self.task_space = session / "task-space.json"
|
|
51
|
+
self.trails = session / "trails"
|
|
52
|
+
self.artifacts = session / "artifacts"
|
|
53
|
+
self.scores = session / "scores"
|
|
54
|
+
self.best = session / "best.json"
|
|
55
|
+
|
|
56
|
+
def ensure_dirs(self) -> None:
|
|
57
|
+
for d in [self.pheromone_dir, self.pheromone_history, self.trails, self.artifacts, self.scores]:
|
|
58
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _emit(payload: dict) -> None:
|
|
62
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _fail(code: int, msg: str):
|
|
66
|
+
_emit({"status": "error", "message": msg})
|
|
67
|
+
sys.exit(code)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# init
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def _discover_nodes(spec: dict) -> List[str]:
|
|
75
|
+
"""Resolve task_space.nodes — supports explicit list or auto_discover_from glob."""
|
|
76
|
+
if isinstance(spec.get("nodes"), list):
|
|
77
|
+
return [str(n) for n in spec["nodes"]]
|
|
78
|
+
if "auto_discover_from" in spec:
|
|
79
|
+
pattern = spec["auto_discover_from"]
|
|
80
|
+
# Glob relative to cwd (workspace root), not session
|
|
81
|
+
matches = sorted(glob.glob(pattern, recursive=True))
|
|
82
|
+
if not matches:
|
|
83
|
+
raise ValueError(f"auto_discover_from '{pattern}' matched no files")
|
|
84
|
+
return matches
|
|
85
|
+
raise ValueError("task_space requires either 'nodes' list or 'auto_discover_from' glob")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cmd_init(args: argparse.Namespace) -> None:
|
|
89
|
+
paths = SessionPaths(Path(args.session))
|
|
90
|
+
if not paths.config.exists():
|
|
91
|
+
_fail(2, f"config not found: {paths.config}")
|
|
92
|
+
config = json.loads(paths.config.read_text())
|
|
93
|
+
|
|
94
|
+
paths.ensure_dirs()
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
nodes = _discover_nodes(config.get("task_space", {}))
|
|
98
|
+
except ValueError as e:
|
|
99
|
+
_fail(2, str(e))
|
|
100
|
+
return # unreachable, satisfies type checker
|
|
101
|
+
|
|
102
|
+
# Write task-space.json
|
|
103
|
+
task_space = {
|
|
104
|
+
"nodes": nodes,
|
|
105
|
+
"n_nodes": len(nodes),
|
|
106
|
+
"max_path_length": config.get("task_space", {}).get("max_path_length", 5),
|
|
107
|
+
"start_nodes": config.get("task_space", {}).get("start_nodes", "any"),
|
|
108
|
+
"edges": config.get("task_space", {}).get("edges", "complete"),
|
|
109
|
+
}
|
|
110
|
+
paths.task_space.write_text(json.dumps(task_space, indent=2, ensure_ascii=False))
|
|
111
|
+
|
|
112
|
+
# Initialize pheromone
|
|
113
|
+
aco_cfg = config.get("aco", {})
|
|
114
|
+
state = PheromoneState.initialize(nodes, aco_cfg)
|
|
115
|
+
state.save(paths.pheromone_current)
|
|
116
|
+
state.save(paths.pheromone_init)
|
|
117
|
+
|
|
118
|
+
_emit({
|
|
119
|
+
"status": "ok",
|
|
120
|
+
"command": "init",
|
|
121
|
+
"session": str(paths.session),
|
|
122
|
+
"pheromone_path": str(paths.pheromone_current),
|
|
123
|
+
"task_space_path": str(paths.task_space),
|
|
124
|
+
"n_nodes": len(nodes),
|
|
125
|
+
"n_edges": len(state.tau),
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# select
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
def _pick_start_node(nodes: List[str], state: PheromoneState, mode: str) -> str:
|
|
134
|
+
if mode == "weighted":
|
|
135
|
+
weights = [state.node_tau.get(n, 1.0) for n in nodes]
|
|
136
|
+
total = sum(weights)
|
|
137
|
+
if total == 0:
|
|
138
|
+
return random.choice(nodes)
|
|
139
|
+
r = random.uniform(0, total)
|
|
140
|
+
acc = 0.0
|
|
141
|
+
for n, w in zip(nodes, weights):
|
|
142
|
+
acc += w
|
|
143
|
+
if acc >= r:
|
|
144
|
+
return n
|
|
145
|
+
return nodes[-1]
|
|
146
|
+
return random.choice(nodes)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def cmd_select(args: argparse.Namespace) -> None:
|
|
150
|
+
paths = SessionPaths(Path(args.session))
|
|
151
|
+
config = json.loads(paths.config.read_text())
|
|
152
|
+
state = PheromoneState.load(paths.pheromone_current)
|
|
153
|
+
task_space = json.loads(paths.task_space.read_text())
|
|
154
|
+
|
|
155
|
+
n_ants = config.get("swarm", {}).get("n_ants", 5)
|
|
156
|
+
nodes = task_space["nodes"]
|
|
157
|
+
max_len = task_space.get("max_path_length", 5)
|
|
158
|
+
start_mode = task_space.get("start_nodes", "any")
|
|
159
|
+
|
|
160
|
+
assignments = []
|
|
161
|
+
for i in range(1, n_ants + 1):
|
|
162
|
+
start = _pick_start_node(nodes, state, "weighted" if start_mode == "weighted" else "uniform")
|
|
163
|
+
edge_prefs = state.select_neighbors(start, nodes)
|
|
164
|
+
# Keep top-k edges to reduce noise
|
|
165
|
+
top_k = sorted(edge_prefs.items(), key=lambda x: -x[1])[:8]
|
|
166
|
+
assignments.append({
|
|
167
|
+
"ant_id": f"ANT-{args.iter}-{i}",
|
|
168
|
+
"start_node": start,
|
|
169
|
+
"edge_preferences": {f"{start}::{b}": w for b, w in top_k},
|
|
170
|
+
"max_path_length": max_len,
|
|
171
|
+
"iteration": args.iter,
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
_emit({
|
|
175
|
+
"status": "ok",
|
|
176
|
+
"command": "select",
|
|
177
|
+
"iteration": args.iter,
|
|
178
|
+
"n_assignments": len(assignments),
|
|
179
|
+
"assignments": assignments,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
# update
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def _load_iteration_artifacts(paths: SessionPaths, iteration: int) -> List[dict]:
|
|
188
|
+
pattern = str(paths.artifacts / f"ant-{iteration}-*.json")
|
|
189
|
+
files = sorted(glob.glob(pattern))
|
|
190
|
+
artifacts = []
|
|
191
|
+
for f in files:
|
|
192
|
+
try:
|
|
193
|
+
artifacts.append(json.loads(Path(f).read_text()))
|
|
194
|
+
except json.JSONDecodeError as e:
|
|
195
|
+
print(f"warning: skipped malformed artifact {f}: {e}", file=sys.stderr)
|
|
196
|
+
return artifacts
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _validate_artifact(art: dict, valid_nodes: set) -> Optional[str]:
|
|
200
|
+
required = ["schema_version", "ant_id", "iteration", "path", "self_score", "self_confidence"]
|
|
201
|
+
for f in required:
|
|
202
|
+
if f not in art:
|
|
203
|
+
return f"missing field: {f}"
|
|
204
|
+
if not isinstance(art["path"], list) or not art["path"]:
|
|
205
|
+
return "path must be non-empty list"
|
|
206
|
+
for n in art["path"]:
|
|
207
|
+
if n not in valid_nodes:
|
|
208
|
+
return f"path node '{n}' not in task_space"
|
|
209
|
+
if not 0.0 <= art["self_score"] <= 1.0:
|
|
210
|
+
return "self_score out of [0,1]"
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def cmd_update(args: argparse.Namespace) -> None:
|
|
215
|
+
paths = SessionPaths(Path(args.session))
|
|
216
|
+
config = json.loads(paths.config.read_text())
|
|
217
|
+
state = PheromoneState.load(paths.pheromone_current)
|
|
218
|
+
task_space = json.loads(paths.task_space.read_text())
|
|
219
|
+
valid_nodes = set(task_space["nodes"])
|
|
220
|
+
|
|
221
|
+
artifacts = _load_iteration_artifacts(paths, args.iter)
|
|
222
|
+
if not artifacts:
|
|
223
|
+
_fail(1, f"no artifacts found for iteration {args.iter}")
|
|
224
|
+
|
|
225
|
+
# Resolve scorers
|
|
226
|
+
scoring_cfg = config.get("scoring", {})
|
|
227
|
+
script_scorer = None
|
|
228
|
+
if scoring_cfg.get("mode") in ("script", "hybrid"):
|
|
229
|
+
rule_path = scoring_cfg.get("script_path")
|
|
230
|
+
if rule_path:
|
|
231
|
+
full = Path(args.session).parent.parent / rule_path if not Path(rule_path).is_absolute() else Path(rule_path)
|
|
232
|
+
if full.exists():
|
|
233
|
+
script_scorer = ScriptScorer(full)
|
|
234
|
+
fallback = FallbackScorer(scoring_cfg.get("self_score_discount", 0.5))
|
|
235
|
+
verified_scores = load_verified_scores(paths.scores / f"iter-{args.iter}-scores.json")
|
|
236
|
+
|
|
237
|
+
# Evaporate first
|
|
238
|
+
state.evaporate()
|
|
239
|
+
|
|
240
|
+
# Process each ant
|
|
241
|
+
trail_log = []
|
|
242
|
+
hallucinations = []
|
|
243
|
+
scored = []
|
|
244
|
+
for art in artifacts:
|
|
245
|
+
err = _validate_artifact(art, valid_nodes)
|
|
246
|
+
if err:
|
|
247
|
+
print(f"warning: invalid artifact {art.get('ant_id', '?')}: {err}", file=sys.stderr)
|
|
248
|
+
continue
|
|
249
|
+
score, src = resolve_score(art, verified_scores, script_scorer, fallback)
|
|
250
|
+
scored.append({"ant_id": art["ant_id"], "score": score, "source": src})
|
|
251
|
+
|
|
252
|
+
if src == "verified_llm":
|
|
253
|
+
if hallucination_check(art["self_score"], score):
|
|
254
|
+
hallucinations.append(art["ant_id"])
|
|
255
|
+
score *= 0.5
|
|
256
|
+
|
|
257
|
+
state.deposit(art["path"], score)
|
|
258
|
+
trail_log.append({
|
|
259
|
+
"ant_id": art["ant_id"],
|
|
260
|
+
"path": art["path"],
|
|
261
|
+
"self_score": art["self_score"],
|
|
262
|
+
"verified_score": score,
|
|
263
|
+
"source": src,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
# Elitist: re-load best history, deposit extra on best path
|
|
267
|
+
best_data = None
|
|
268
|
+
if paths.best.exists():
|
|
269
|
+
best_data = json.loads(paths.best.read_text())
|
|
270
|
+
current_best = max(scored, key=lambda x: x["score"]) if scored else None
|
|
271
|
+
if current_best:
|
|
272
|
+
best_art = next(a for a in artifacts if a["ant_id"] == current_best["ant_id"])
|
|
273
|
+
if best_data is None or current_best["score"] > best_data.get("score", -1):
|
|
274
|
+
best_data = {
|
|
275
|
+
"ant_id": current_best["ant_id"],
|
|
276
|
+
"iteration": args.iter,
|
|
277
|
+
"path": best_art["path"],
|
|
278
|
+
"score": current_best["score"],
|
|
279
|
+
"self_score": best_art["self_score"],
|
|
280
|
+
"candidate_solution": best_art.get("candidate_solution"),
|
|
281
|
+
"evidence": best_art.get("evidence", []),
|
|
282
|
+
"updated_at": time.time(),
|
|
283
|
+
}
|
|
284
|
+
paths.best.write_text(json.dumps(best_data, indent=2, ensure_ascii=False))
|
|
285
|
+
# Elite deposit
|
|
286
|
+
state.deposit(best_data["path"], best_data["score"])
|
|
287
|
+
|
|
288
|
+
state.clip()
|
|
289
|
+
state.iteration = args.iter
|
|
290
|
+
state.save(paths.pheromone_current)
|
|
291
|
+
state.save(paths.pheromone_history / f"{args.iter}.json")
|
|
292
|
+
|
|
293
|
+
# Persist trails
|
|
294
|
+
trails_file = paths.trails / f"{args.iter}.jsonl"
|
|
295
|
+
trails_file.write_text("\n".join(json.dumps(t, ensure_ascii=False) for t in trail_log))
|
|
296
|
+
|
|
297
|
+
mean_score = sum(s["score"] for s in scored) / len(scored) if scored else 0.0
|
|
298
|
+
best_score = best_data["score"] if best_data else 0.0
|
|
299
|
+
prev_best = 0.0
|
|
300
|
+
history_files = sorted(paths.pheromone_history.glob("*.json"))
|
|
301
|
+
if len(history_files) >= 2:
|
|
302
|
+
prev = json.loads(history_files[-2].read_text())
|
|
303
|
+
prev_best = prev.get("stats", {}).get("best_known", best_score)
|
|
304
|
+
delta = best_score - prev_best
|
|
305
|
+
|
|
306
|
+
_emit({
|
|
307
|
+
"status": "ok",
|
|
308
|
+
"command": "update",
|
|
309
|
+
"iteration": args.iter,
|
|
310
|
+
"n_ants_processed": len(scored),
|
|
311
|
+
"mean_score": round(mean_score, 4),
|
|
312
|
+
"best_score": round(best_score, 4),
|
|
313
|
+
"delta": round(delta, 4),
|
|
314
|
+
"elite_updated": current_best is not None and (best_data is None or current_best["ant_id"] == best_data["ant_id"]),
|
|
315
|
+
"hallucinations_flagged": hallucinations,
|
|
316
|
+
"stats": state.stats(),
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# converged
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
def cmd_converged(args: argparse.Namespace) -> None:
|
|
325
|
+
paths = SessionPaths(Path(args.session))
|
|
326
|
+
config = json.loads(paths.config.read_text())
|
|
327
|
+
cv = config.get("convergence", {})
|
|
328
|
+
|
|
329
|
+
state = PheromoneState.load(paths.pheromone_current)
|
|
330
|
+
iteration = state.iteration
|
|
331
|
+
|
|
332
|
+
triggered = []
|
|
333
|
+
metrics = {
|
|
334
|
+
"iteration": iteration,
|
|
335
|
+
"entropy": state.stats()["entropy"],
|
|
336
|
+
"best_score": 0.0,
|
|
337
|
+
"mean_score": 0.0,
|
|
338
|
+
"iterations_since_best_change": 0,
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if paths.best.exists():
|
|
342
|
+
metrics["best_score"] = json.loads(paths.best.read_text()).get("score", 0.0)
|
|
343
|
+
|
|
344
|
+
# max_iterations
|
|
345
|
+
max_iter = cv.get("max_iterations", 5)
|
|
346
|
+
if iteration >= max_iter:
|
|
347
|
+
triggered.append("max_iterations")
|
|
348
|
+
|
|
349
|
+
# entropy_floor
|
|
350
|
+
ef = cv.get("entropy_floor", {})
|
|
351
|
+
if ef.get("enabled", True) and metrics["entropy"] < ef.get("threshold", 0.5):
|
|
352
|
+
triggered.append("entropy_floor")
|
|
353
|
+
|
|
354
|
+
# target_score
|
|
355
|
+
ts = cv.get("target_score", {})
|
|
356
|
+
if ts.get("enabled", True) and metrics["best_score"] >= ts.get("value", 0.95):
|
|
357
|
+
triggered.append("target_score")
|
|
358
|
+
|
|
359
|
+
# stagnation
|
|
360
|
+
st = cv.get("stagnation", {})
|
|
361
|
+
if st.get("enabled", True):
|
|
362
|
+
patience = st.get("patience", 2)
|
|
363
|
+
min_delta = st.get("min_delta", 0.01)
|
|
364
|
+
# Use trails to compute per-iter best
|
|
365
|
+
trail_files = sorted(paths.trails.glob("*.jsonl"))
|
|
366
|
+
per_iter_best = []
|
|
367
|
+
for tf in trail_files:
|
|
368
|
+
lines = [json.loads(l) for l in tf.read_text().splitlines() if l.strip()]
|
|
369
|
+
if lines:
|
|
370
|
+
per_iter_best.append(max(l.get("verified_score", 0) for l in lines))
|
|
371
|
+
if len(per_iter_best) > patience:
|
|
372
|
+
recent = per_iter_best[-patience - 1:]
|
|
373
|
+
deltas = [abs(recent[i] - recent[i - 1]) for i in range(1, len(recent))]
|
|
374
|
+
metrics["iterations_since_best_change"] = sum(1 for d in deltas if d < min_delta)
|
|
375
|
+
if all(d < min_delta for d in deltas):
|
|
376
|
+
triggered.append("stagnation")
|
|
377
|
+
|
|
378
|
+
_emit({
|
|
379
|
+
"status": "ok",
|
|
380
|
+
"command": "converged",
|
|
381
|
+
"converged": len(triggered) > 0,
|
|
382
|
+
"iteration": iteration,
|
|
383
|
+
"triggered_by": triggered,
|
|
384
|
+
"reason": triggered[0] if triggered else "in_progress",
|
|
385
|
+
"metrics": metrics,
|
|
386
|
+
"recommendation": "ready for report" if triggered else f"continue to iteration {iteration + 1}",
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
# report
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
def cmd_report(args: argparse.Namespace) -> None:
|
|
395
|
+
paths = SessionPaths(Path(args.session))
|
|
396
|
+
state = PheromoneState.load(paths.pheromone_current)
|
|
397
|
+
|
|
398
|
+
best = None
|
|
399
|
+
if paths.best.exists():
|
|
400
|
+
best = json.loads(paths.best.read_text())
|
|
401
|
+
|
|
402
|
+
# Top-K trails across all iterations
|
|
403
|
+
all_trails = []
|
|
404
|
+
for tf in sorted(paths.trails.glob("*.jsonl")):
|
|
405
|
+
for line in tf.read_text().splitlines():
|
|
406
|
+
if line.strip():
|
|
407
|
+
all_trails.append(json.loads(line))
|
|
408
|
+
top_k = sorted(all_trails, key=lambda x: -x.get("verified_score", 0))[:5]
|
|
409
|
+
|
|
410
|
+
# Convergence curve
|
|
411
|
+
curve = []
|
|
412
|
+
for hf in sorted(paths.pheromone_history.glob("*.json"), key=lambda p: int(p.stem)):
|
|
413
|
+
snap = json.loads(hf.read_text())
|
|
414
|
+
curve.append({
|
|
415
|
+
"iteration": snap["iteration"],
|
|
416
|
+
"entropy": snap["stats"]["entropy"],
|
|
417
|
+
"tau_max": snap["stats"]["max"],
|
|
418
|
+
"tau_mean": snap["stats"]["mean"],
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
_emit({
|
|
422
|
+
"status": "ok",
|
|
423
|
+
"command": "report",
|
|
424
|
+
"best": best,
|
|
425
|
+
"top_k": top_k,
|
|
426
|
+
"convergence_curve": curve,
|
|
427
|
+
"final_pheromone_stats": state.stats(),
|
|
428
|
+
"iterations_completed": state.iteration,
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# ---------------------------------------------------------------------------
|
|
433
|
+
# CLI
|
|
434
|
+
# ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
437
|
+
p = argparse.ArgumentParser(prog="aco", description=f"ACO controller v{VERSION}")
|
|
438
|
+
p.add_argument("--session", required=True, help="path to session folder")
|
|
439
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
440
|
+
|
|
441
|
+
sub.add_parser("init", help="initialize pheromone + task-space from config")
|
|
442
|
+
|
|
443
|
+
s_select = sub.add_parser("select", help="produce N ant assignments for iteration k")
|
|
444
|
+
s_select.add_argument("--iter", type=int, required=True)
|
|
445
|
+
|
|
446
|
+
s_update = sub.add_parser("update", help="update pheromone from iteration artifacts")
|
|
447
|
+
s_update.add_argument("--iter", type=int, required=True)
|
|
448
|
+
|
|
449
|
+
sub.add_parser("converged", help="check convergence criteria")
|
|
450
|
+
sub.add_parser("report", help="emit full result report")
|
|
451
|
+
|
|
452
|
+
return p
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def main(argv: Optional[List[str]] = None) -> None:
|
|
456
|
+
args = build_parser().parse_args(argv)
|
|
457
|
+
handlers = {
|
|
458
|
+
"init": cmd_init,
|
|
459
|
+
"select": cmd_select,
|
|
460
|
+
"update": cmd_update,
|
|
461
|
+
"converged": cmd_converged,
|
|
462
|
+
"report": cmd_report,
|
|
463
|
+
}
|
|
464
|
+
try:
|
|
465
|
+
handlers[args.command](args)
|
|
466
|
+
except SystemExit:
|
|
467
|
+
raise
|
|
468
|
+
except Exception as e:
|
|
469
|
+
_fail(1, f"{type(e).__name__}: {e}")
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
if __name__ == "__main__":
|
|
473
|
+
main()
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Pheromone matrix module - state representation, update, evaporation.
|
|
2
|
+
|
|
3
|
+
Format spec: ../specs/pheromone-schema.md
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def edge_key(a: str, b: str) -> str:
|
|
15
|
+
"""Lexically-ordered edge key (undirected)."""
|
|
16
|
+
return f"{a}::{b}" if a <= b else f"{b}::{a}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PheromoneState:
|
|
21
|
+
iteration: int = 0
|
|
22
|
+
n_nodes: int = 0
|
|
23
|
+
matrix_type: str = "edge_weighted_sparse"
|
|
24
|
+
tau: Dict[str, float] = field(default_factory=dict)
|
|
25
|
+
node_tau: Dict[str, float] = field(default_factory=dict)
|
|
26
|
+
metadata: Dict[str, float] = field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def initialize(cls, nodes: List[str], aco_config: dict) -> "PheromoneState":
|
|
30
|
+
tau_init = aco_config.get("tau_init", 1.0)
|
|
31
|
+
tau = {}
|
|
32
|
+
for i, a in enumerate(nodes):
|
|
33
|
+
for b in nodes[i + 1:]:
|
|
34
|
+
tau[edge_key(a, b)] = tau_init
|
|
35
|
+
return cls(
|
|
36
|
+
iteration=0,
|
|
37
|
+
n_nodes=len(nodes),
|
|
38
|
+
tau=tau,
|
|
39
|
+
node_tau={n: tau_init for n in nodes},
|
|
40
|
+
metadata={
|
|
41
|
+
"alpha": aco_config.get("alpha", 1.0),
|
|
42
|
+
"beta": aco_config.get("beta", 2.0),
|
|
43
|
+
"rho": aco_config.get("rho", 0.2),
|
|
44
|
+
"q": aco_config.get("q", 1.0),
|
|
45
|
+
"tau_init": tau_init,
|
|
46
|
+
"tau_min": aco_config.get("tau_min", 0.01),
|
|
47
|
+
"tau_max": aco_config.get("tau_max", 10.0),
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def evaporate(self) -> None:
|
|
52
|
+
rho = self.metadata["rho"]
|
|
53
|
+
for k in list(self.tau.keys()):
|
|
54
|
+
self.tau[k] = max(self.metadata["tau_min"], (1 - rho) * self.tau[k])
|
|
55
|
+
|
|
56
|
+
def deposit(self, path: List[str], score: float) -> None:
|
|
57
|
+
q = self.metadata["q"]
|
|
58
|
+
delta = q * score
|
|
59
|
+
for a, b in zip(path[:-1], path[1:]):
|
|
60
|
+
k = edge_key(a, b)
|
|
61
|
+
self.tau[k] = self.tau.get(k, self.metadata["tau_init"]) + delta
|
|
62
|
+
for node in path:
|
|
63
|
+
self.node_tau[node] = self.node_tau.get(node, self.metadata["tau_init"]) + delta * 0.5
|
|
64
|
+
|
|
65
|
+
def clip(self) -> None:
|
|
66
|
+
lo, hi = self.metadata["tau_min"], self.metadata["tau_max"]
|
|
67
|
+
for k in self.tau:
|
|
68
|
+
self.tau[k] = max(lo, min(hi, self.tau[k]))
|
|
69
|
+
for n in self.node_tau:
|
|
70
|
+
self.node_tau[n] = max(lo, min(hi, self.node_tau[n]))
|
|
71
|
+
|
|
72
|
+
def stats(self) -> Dict[str, float]:
|
|
73
|
+
if not self.tau:
|
|
74
|
+
return {"mean": 0.0, "max": 0.0, "min": 0.0, "entropy": 0.0, "n_edges_active": 0}
|
|
75
|
+
vals = list(self.tau.values())
|
|
76
|
+
total = sum(vals)
|
|
77
|
+
active = [v for v in vals if v > self.metadata["tau_min"] * 1.01]
|
|
78
|
+
entropy = 0.0
|
|
79
|
+
if total > 0:
|
|
80
|
+
for v in vals:
|
|
81
|
+
p = v / total
|
|
82
|
+
if p > 0:
|
|
83
|
+
entropy -= p * math.log2(p)
|
|
84
|
+
return {
|
|
85
|
+
"mean": sum(vals) / len(vals),
|
|
86
|
+
"max": max(vals),
|
|
87
|
+
"min": min(vals),
|
|
88
|
+
"entropy": entropy,
|
|
89
|
+
"n_edges_active": len(active),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> dict:
|
|
93
|
+
return {
|
|
94
|
+
"version": "1.0",
|
|
95
|
+
"iteration": self.iteration,
|
|
96
|
+
"n_nodes": self.n_nodes,
|
|
97
|
+
"matrix_type": self.matrix_type,
|
|
98
|
+
"tau": self.tau,
|
|
99
|
+
"node_tau": self.node_tau,
|
|
100
|
+
"metadata": self.metadata,
|
|
101
|
+
"stats": self.stats(),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_dict(cls, d: dict) -> "PheromoneState":
|
|
106
|
+
return cls(
|
|
107
|
+
iteration=d["iteration"],
|
|
108
|
+
n_nodes=d["n_nodes"],
|
|
109
|
+
matrix_type=d.get("matrix_type", "edge_weighted_sparse"),
|
|
110
|
+
tau=d.get("tau", {}),
|
|
111
|
+
node_tau=d.get("node_tau", {}),
|
|
112
|
+
metadata=d["metadata"],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def save(self, path: Path) -> None:
|
|
116
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
path.write_text(json.dumps(self.to_dict(), indent=2, ensure_ascii=False))
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def load(cls, path: Path) -> "PheromoneState":
|
|
121
|
+
return cls.from_dict(json.loads(path.read_text()))
|
|
122
|
+
|
|
123
|
+
def select_neighbors(
|
|
124
|
+
self,
|
|
125
|
+
current: str,
|
|
126
|
+
candidates: List[str],
|
|
127
|
+
heuristic: Optional[Dict[str, float]] = None,
|
|
128
|
+
) -> Dict[str, float]:
|
|
129
|
+
"""Return probability distribution over candidates given current node."""
|
|
130
|
+
alpha = self.metadata["alpha"]
|
|
131
|
+
beta = self.metadata["beta"]
|
|
132
|
+
heuristic = heuristic or {}
|
|
133
|
+
weights = {}
|
|
134
|
+
for c in candidates:
|
|
135
|
+
if c == current:
|
|
136
|
+
continue
|
|
137
|
+
tau_val = self.tau.get(edge_key(current, c), self.metadata["tau_init"])
|
|
138
|
+
eta = heuristic.get(c, 1.0)
|
|
139
|
+
weights[c] = (tau_val ** alpha) * (eta ** beta)
|
|
140
|
+
total = sum(weights.values())
|
|
141
|
+
if total == 0:
|
|
142
|
+
n = len(weights)
|
|
143
|
+
return {c: 1.0 / n for c in weights} if n else {}
|
|
144
|
+
return {c: w / total for c, w in weights.items()}
|