behavior-os 0.1.0__py3-none-any.whl
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.
- agentic_mindset/__init__.py +5 -0
- agentic_mindset/cli.py +504 -0
- agentic_mindset/context.py +264 -0
- agentic_mindset/fusion.py +103 -0
- agentic_mindset/ir/__init__.py +0 -0
- agentic_mindset/ir/conditions.py +31 -0
- agentic_mindset/ir/models.py +79 -0
- agentic_mindset/pack.py +77 -0
- agentic_mindset/registry.py +48 -0
- agentic_mindset/renderer/__init__.py +0 -0
- agentic_mindset/renderer/inject.py +146 -0
- agentic_mindset/resolver/__init__.py +0 -0
- agentic_mindset/resolver/policies.py +58 -0
- agentic_mindset/resolver/resolver.py +275 -0
- agentic_mindset/schema/__init__.py +20 -0
- agentic_mindset/schema/behavior.py +19 -0
- agentic_mindset/schema/meta.py +43 -0
- agentic_mindset/schema/mindset.py +37 -0
- agentic_mindset/schema/personality.py +110 -0
- agentic_mindset/schema/sources.py +26 -0
- agentic_mindset/schema/version.py +16 -0
- agentic_mindset/schema/voice.py +23 -0
- behavior_os-0.1.0.dist-info/METADATA +12 -0
- behavior_os-0.1.0.dist-info/RECORD +26 -0
- behavior_os-0.1.0.dist-info/WHEEL +4 -0
- behavior_os-0.1.0.dist-info/entry_points.txt +2 -0
agentic_mindset/cli.py
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import typer
|
|
10
|
+
import yaml
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
|
|
14
|
+
from agentic_mindset.pack import CharacterPack, PackLoadError
|
|
15
|
+
from agentic_mindset.registry import CharacterRegistry
|
|
16
|
+
from agentic_mindset.fusion import FusionEngine, FusionStrategy, FusionReport
|
|
17
|
+
from agentic_mindset.context import ContextBlock
|
|
18
|
+
from agentic_mindset.resolver.resolver import ConflictResolver
|
|
19
|
+
from agentic_mindset.renderer.inject import render_for_runtime as _render_inject
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(name="mindset", help="Agentic Mindset CLI")
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
_GENERATE_SCHEMA_VERSION = "1.0"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _format_output(text: str, fmt: str, meta: dict | None = None) -> str:
|
|
28
|
+
if fmt == "text":
|
|
29
|
+
return text
|
|
30
|
+
if fmt == "anthropic-json":
|
|
31
|
+
return json.dumps({"type": "text", "text": text})
|
|
32
|
+
if fmt == "debug-json":
|
|
33
|
+
return json.dumps({"meta": meta, "type": "text", "text": text}, indent=2)
|
|
34
|
+
raise ValueError(f"Unknown output format: {fmt!r}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _explain_decision_policy(report: "FusionReport") -> str:
|
|
38
|
+
"""Build the decision_policy string for --explain YAML output."""
|
|
39
|
+
if len(report.personas) == 1:
|
|
40
|
+
return f"{report.personas[0][0]}-only"
|
|
41
|
+
if report.dominant_character is not None:
|
|
42
|
+
return f"{report.dominant_character}-dominant"
|
|
43
|
+
return "equal-blend"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_TEMPLATE_META = {
|
|
47
|
+
"id": "{id}",
|
|
48
|
+
"name": "{name}",
|
|
49
|
+
"version": "1.0.0",
|
|
50
|
+
"schema_version": "1.0",
|
|
51
|
+
"type": "{type}",
|
|
52
|
+
"description": "TODO: describe this character",
|
|
53
|
+
"tags": [],
|
|
54
|
+
"authors": [{"name": "TODO", "url": ""}],
|
|
55
|
+
"created": "{today}",
|
|
56
|
+
}
|
|
57
|
+
_TEMPLATE_MINDSET = {
|
|
58
|
+
"core_principles": [{"description": "TODO", "detail": "TODO"}],
|
|
59
|
+
"decision_framework": {"risk_tolerance": "medium", "time_horizon": "long-term", "approach": "TODO"},
|
|
60
|
+
"thinking_patterns": ["TODO"],
|
|
61
|
+
"mental_models": [{"name": "TODO", "description": "TODO"}],
|
|
62
|
+
}
|
|
63
|
+
_TEMPLATE_PERSONALITY = {
|
|
64
|
+
"traits": [{"name": "TODO", "description": "TODO", "intensity": 0.5}],
|
|
65
|
+
"emotional_tendencies": {"stress_response": "TODO", "motivation_source": "TODO"},
|
|
66
|
+
"interpersonal_style": {"communication": "TODO", "leadership": "TODO"},
|
|
67
|
+
"drives": ["TODO"],
|
|
68
|
+
}
|
|
69
|
+
_TEMPLATE_BEHAVIOR = {
|
|
70
|
+
"work_patterns": ["TODO"],
|
|
71
|
+
"decision_speed": "deliberate",
|
|
72
|
+
"execution_style": ["TODO"],
|
|
73
|
+
"conflict_style": "TODO",
|
|
74
|
+
}
|
|
75
|
+
_TEMPLATE_VOICE = {
|
|
76
|
+
"tone": "TODO",
|
|
77
|
+
"vocabulary": {"preferred": [], "avoided": []},
|
|
78
|
+
"sentence_style": "TODO",
|
|
79
|
+
"signature_phrases": [],
|
|
80
|
+
}
|
|
81
|
+
_TEMPLATE_SOURCES = {
|
|
82
|
+
"sources": [
|
|
83
|
+
{"title": "TODO source 1", "type": "book", "accessed": "2026-01-01"},
|
|
84
|
+
{"title": "TODO source 2", "type": "book", "accessed": "2026-01-01"},
|
|
85
|
+
{"title": "TODO source 3", "type": "book", "accessed": "2026-01-01"},
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.command()
|
|
91
|
+
def init(
|
|
92
|
+
character_id: str = typer.Argument(..., help="Character ID (kebab-case)"),
|
|
93
|
+
type_: str = typer.Option("historical", "--type", help="historical or fictional"),
|
|
94
|
+
output: Optional[Path] = typer.Option(None, "--output", help="Directory to create pack in"),
|
|
95
|
+
):
|
|
96
|
+
"""Scaffold a new empty character pack."""
|
|
97
|
+
from datetime import date
|
|
98
|
+
out_dir = (output or Path(".")) / character_id
|
|
99
|
+
if type_ not in ("historical", "fictional"):
|
|
100
|
+
console.print(f"[red]--type must be 'historical' or 'fictional', got: {type_!r}[/red]")
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
if out_dir.exists():
|
|
103
|
+
console.print(f"[red]Directory already exists: {out_dir}[/red]")
|
|
104
|
+
raise typer.Exit(1)
|
|
105
|
+
out_dir.mkdir(parents=True)
|
|
106
|
+
|
|
107
|
+
name = character_id.replace("-", " ").title()
|
|
108
|
+
today = date.today().isoformat()
|
|
109
|
+
|
|
110
|
+
def _render(template: dict) -> dict:
|
|
111
|
+
s = json.dumps(template)
|
|
112
|
+
s = s.replace("{id}", character_id).replace("{name}", name)
|
|
113
|
+
s = s.replace("{type}", type_).replace("{today}", today)
|
|
114
|
+
return json.loads(s)
|
|
115
|
+
|
|
116
|
+
files = {
|
|
117
|
+
"meta.yaml": _render(_TEMPLATE_META),
|
|
118
|
+
"mindset.yaml": _TEMPLATE_MINDSET,
|
|
119
|
+
"personality.yaml": _TEMPLATE_PERSONALITY,
|
|
120
|
+
"behavior.yaml": _TEMPLATE_BEHAVIOR,
|
|
121
|
+
"voice.yaml": _TEMPLATE_VOICE,
|
|
122
|
+
"sources.yaml": _TEMPLATE_SOURCES,
|
|
123
|
+
}
|
|
124
|
+
for fname, data in files.items():
|
|
125
|
+
(out_dir / fname).write_text(yaml.dump(data, allow_unicode=True))
|
|
126
|
+
|
|
127
|
+
console.print(f"[green]Created character pack:[/green] {out_dir}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@app.command()
|
|
131
|
+
def validate(
|
|
132
|
+
pack_path: Path = typer.Argument(..., help="Path to character pack directory"),
|
|
133
|
+
):
|
|
134
|
+
"""Validate a character pack against the schema."""
|
|
135
|
+
try:
|
|
136
|
+
CharacterPack.load(pack_path)
|
|
137
|
+
console.print(f"[green]✓ Pack is valid:[/green] {pack_path}")
|
|
138
|
+
except PackLoadError as e:
|
|
139
|
+
console.print(f"[red]✗ Validation failed:[/red]\n{e}")
|
|
140
|
+
raise typer.Exit(1)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@app.command()
|
|
144
|
+
def preview(
|
|
145
|
+
pack_path: Optional[Path] = typer.Argument(None, help="Path to a single character pack"),
|
|
146
|
+
fusion_config: Optional[Path] = typer.Option(None, "--fusion", help="Path to fusion.yaml"),
|
|
147
|
+
output_format: str = typer.Option("plain_text", "--format", help="plain_text or xml_tagged"),
|
|
148
|
+
registry_path: Optional[Path] = typer.Option(None, "--registry", help="Override registry path"),
|
|
149
|
+
):
|
|
150
|
+
"""Preview the Context Block for a character or fusion."""
|
|
151
|
+
if pack_path is None and fusion_config is None:
|
|
152
|
+
console.print("[red]Provide either a pack path or --fusion config.[/red]")
|
|
153
|
+
raise typer.Exit(1)
|
|
154
|
+
|
|
155
|
+
if pack_path:
|
|
156
|
+
pack = CharacterPack.load(pack_path)
|
|
157
|
+
block = ContextBlock.from_packs([(pack, 1.0)])
|
|
158
|
+
else:
|
|
159
|
+
cfg = yaml.safe_load(fusion_config.read_text(encoding="utf-8"))
|
|
160
|
+
search_paths = [registry_path] if registry_path else None
|
|
161
|
+
registry = CharacterRegistry(search_paths=search_paths)
|
|
162
|
+
engine = FusionEngine(registry)
|
|
163
|
+
chars = [(c["id"], c["weight"]) for c in cfg["characters"]]
|
|
164
|
+
strategy = FusionStrategy(cfg.get("fusion_strategy", "blend"))
|
|
165
|
+
block = engine.fuse(chars, strategy=strategy)
|
|
166
|
+
|
|
167
|
+
console.print(Panel(block.to_prompt(output_format=output_format), title="Context Block"))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _parse_weights(weights_str: Optional[str], ids: list[str]) -> Optional[list[float]]:
|
|
171
|
+
"""Parse --weights string into a list of floats. Prints errors to stderr and returns None on failure."""
|
|
172
|
+
if weights_str is None:
|
|
173
|
+
return [1.0] * len(ids)
|
|
174
|
+
|
|
175
|
+
# reject trailing/leading commas or empty segments
|
|
176
|
+
if weights_str.startswith(",") or weights_str.endswith(",") or ",," in weights_str:
|
|
177
|
+
typer.echo(
|
|
178
|
+
"Error: --weights must be comma-separated numbers (e.g. --weights 6,4).",
|
|
179
|
+
err=True,
|
|
180
|
+
)
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
parts = weights_str.split(",")
|
|
184
|
+
|
|
185
|
+
# count must match number of original IDs
|
|
186
|
+
if len(parts) != len(ids):
|
|
187
|
+
typer.echo(
|
|
188
|
+
f"Error: --weights has {len(parts)} values but {len(ids)} character IDs were given.",
|
|
189
|
+
err=True,
|
|
190
|
+
)
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
parsed: list[float] = []
|
|
194
|
+
for part in parts:
|
|
195
|
+
try:
|
|
196
|
+
val = float(part)
|
|
197
|
+
except ValueError:
|
|
198
|
+
typer.echo(
|
|
199
|
+
"Error: --weights must be comma-separated numbers (e.g. --weights 6,4).",
|
|
200
|
+
err=True,
|
|
201
|
+
)
|
|
202
|
+
return None
|
|
203
|
+
if val < 0:
|
|
204
|
+
typer.echo("Error: --weights values must be positive numbers.", err=True)
|
|
205
|
+
return None
|
|
206
|
+
parsed.append(val)
|
|
207
|
+
|
|
208
|
+
if all(w == 0.0 for w in parsed):
|
|
209
|
+
typer.echo("Error: --weights values cannot all be zero.", err=True)
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
return parsed
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _deduplicate(ids: list[str], weights: list[float]) -> tuple[list[str], list[float]]:
|
|
216
|
+
"""Merge duplicate IDs by summing their weights, then normalize to sum=1.0."""
|
|
217
|
+
merged: dict[str, float] = {}
|
|
218
|
+
for cid, w in zip(ids, weights):
|
|
219
|
+
merged[cid] = merged.get(cid, 0.0) + w
|
|
220
|
+
|
|
221
|
+
total = sum(merged.values())
|
|
222
|
+
ids_out = list(merged.keys())
|
|
223
|
+
weights_out = [merged[cid] / total for cid in ids_out]
|
|
224
|
+
return ids_out, weights_out
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@app.command("list")
|
|
228
|
+
def list_characters(
|
|
229
|
+
registry: Optional[Path] = typer.Option(None, "--registry", help="Override registry path"),
|
|
230
|
+
):
|
|
231
|
+
"""List available characters in the registry."""
|
|
232
|
+
search_paths = [registry] if registry else None
|
|
233
|
+
reg = CharacterRegistry(search_paths=search_paths)
|
|
234
|
+
ids = reg.list_ids()
|
|
235
|
+
if not ids:
|
|
236
|
+
console.print("[yellow]No characters found.[/yellow]")
|
|
237
|
+
for cid in ids:
|
|
238
|
+
console.print(f" {cid}")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@app.command()
|
|
242
|
+
def generate(
|
|
243
|
+
ids: list[str] = typer.Argument(..., help="Character IDs to compile"),
|
|
244
|
+
weights: Optional[str] = typer.Option(None, "--weights", help="Comma-separated weights"),
|
|
245
|
+
strategy: str = typer.Option("blend", "--strategy", help="blend | dominant"),
|
|
246
|
+
format_: str = typer.Option("text", "--format", help="text | anthropic-json | debug-json"),
|
|
247
|
+
output: Optional[Path] = typer.Option(None, "--output", help="Write to file instead of stdout"),
|
|
248
|
+
explain: bool = typer.Option(False, "--explain", help="Print compilation summary to stderr"),
|
|
249
|
+
registry: Optional[Path] = typer.Option(None, "--registry", help="Override registry path"),
|
|
250
|
+
):
|
|
251
|
+
"""Compile character mindset(s) into an injectable system prompt block."""
|
|
252
|
+
search_paths = [registry] if registry else None
|
|
253
|
+
reg = CharacterRegistry(search_paths=search_paths)
|
|
254
|
+
|
|
255
|
+
# --- parse and validate weights ---
|
|
256
|
+
parsed_weights = _parse_weights(weights, ids)
|
|
257
|
+
if parsed_weights is None:
|
|
258
|
+
raise typer.Exit(1)
|
|
259
|
+
|
|
260
|
+
# --- deduplicate IDs, summing weights ---
|
|
261
|
+
ids_deduped, weights_deduped = _deduplicate(ids, parsed_weights)
|
|
262
|
+
|
|
263
|
+
# --- load characters (validate existence) ---
|
|
264
|
+
# Note: reg.load_id() is called here for early validation with a clear error message.
|
|
265
|
+
# FusionEngine.fuse() takes (id, weight) pairs and re-loads from registry internally.
|
|
266
|
+
# This double-load is intentional: the validation pass gives a targeted error before
|
|
267
|
+
# any fusion work begins.
|
|
268
|
+
missing_cid = None
|
|
269
|
+
for cid in ids_deduped:
|
|
270
|
+
try:
|
|
271
|
+
reg.load_id(cid)
|
|
272
|
+
except KeyError:
|
|
273
|
+
missing_cid = cid
|
|
274
|
+
break
|
|
275
|
+
if missing_cid is not None:
|
|
276
|
+
typer.echo(
|
|
277
|
+
f"Error: character '{missing_cid}' not found. Run 'mindset list' to see available characters.",
|
|
278
|
+
err=True,
|
|
279
|
+
)
|
|
280
|
+
raise typer.Exit(1)
|
|
281
|
+
|
|
282
|
+
# --- fuse ---
|
|
283
|
+
engine = FusionEngine(reg)
|
|
284
|
+
|
|
285
|
+
if strategy == "sequential":
|
|
286
|
+
typer.echo("Error: --strategy sequential is not supported in v0.", err=True)
|
|
287
|
+
raise typer.Exit(1)
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
strat = FusionStrategy(strategy)
|
|
291
|
+
except ValueError:
|
|
292
|
+
typer.echo(f"Error: unknown strategy '{strategy}'.", err=True)
|
|
293
|
+
raise typer.Exit(1)
|
|
294
|
+
|
|
295
|
+
chars = list(zip(ids_deduped, weights_deduped))
|
|
296
|
+
report = FusionReport() if explain else None
|
|
297
|
+
block = engine.fuse(chars, strategy=strat, report=report)
|
|
298
|
+
text = block.to_prompt(output_format="plain_text")
|
|
299
|
+
|
|
300
|
+
# --- format ---
|
|
301
|
+
meta = {
|
|
302
|
+
"characters": ids_deduped,
|
|
303
|
+
"weights": weights_deduped,
|
|
304
|
+
"strategy": strategy,
|
|
305
|
+
"schema_version": _GENERATE_SCHEMA_VERSION,
|
|
306
|
+
}
|
|
307
|
+
result_str = _format_output(text, format_, meta=meta)
|
|
308
|
+
|
|
309
|
+
# --- explain ---
|
|
310
|
+
if explain:
|
|
311
|
+
weighted_packs_ex = engine.prepare_packs(chars, strat)
|
|
312
|
+
dominant_pack = weighted_packs_ex[0][0]
|
|
313
|
+
explain_data = {
|
|
314
|
+
"personas": [{cid: round(w, 4)} for cid, w in report.personas],
|
|
315
|
+
"merged": {
|
|
316
|
+
"decision_policy": _explain_decision_policy(report),
|
|
317
|
+
"risk_tolerance": dominant_pack.mindset.decision_framework.risk_tolerance,
|
|
318
|
+
"time_horizon": dominant_pack.mindset.decision_framework.time_horizon,
|
|
319
|
+
},
|
|
320
|
+
"removed_conflicts": report.removed_items,
|
|
321
|
+
}
|
|
322
|
+
typer.echo(
|
|
323
|
+
yaml.dump(explain_data, default_flow_style=False, allow_unicode=True),
|
|
324
|
+
err=True,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# --- output ---
|
|
328
|
+
if output:
|
|
329
|
+
try:
|
|
330
|
+
output.write_text(result_str, encoding="utf-8")
|
|
331
|
+
except OSError as e:
|
|
332
|
+
typer.echo(f"Error: cannot write to '{output}': {e}.", err=True)
|
|
333
|
+
raise typer.Exit(1)
|
|
334
|
+
else:
|
|
335
|
+
typer.echo(result_str)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _emit_explain_from_ir(ir: "BehaviorIR") -> None:
|
|
339
|
+
"""Emit structured YAML explain output for the inject path."""
|
|
340
|
+
slots_data = {}
|
|
341
|
+
for slot_name, slot in ir.slots.items():
|
|
342
|
+
slots_data[slot_name] = {
|
|
343
|
+
"primary": {
|
|
344
|
+
"value": slot.primary.value,
|
|
345
|
+
"source": slot.primary.source,
|
|
346
|
+
"weight": round(slot.primary.weight, 4),
|
|
347
|
+
},
|
|
348
|
+
"has_conflict": slot.has_conflict,
|
|
349
|
+
"modifiers": [
|
|
350
|
+
{
|
|
351
|
+
"value": m.value,
|
|
352
|
+
"condition": m.condition,
|
|
353
|
+
"conjunction": m.conjunction,
|
|
354
|
+
"source": m.source,
|
|
355
|
+
"provenance": m.provenance,
|
|
356
|
+
**({"note": m.note} if m.note else {}),
|
|
357
|
+
}
|
|
358
|
+
for m in slot.modifiers
|
|
359
|
+
],
|
|
360
|
+
"dropped": [
|
|
361
|
+
{
|
|
362
|
+
"value": d.value,
|
|
363
|
+
"source": d.source,
|
|
364
|
+
"weight": round(d.weight, 4),
|
|
365
|
+
"reason": d.reason,
|
|
366
|
+
}
|
|
367
|
+
for d in slot.dropped
|
|
368
|
+
],
|
|
369
|
+
}
|
|
370
|
+
data = {
|
|
371
|
+
"personas": [{cid: round(w, 4)} for cid, w in ir.preamble.personas],
|
|
372
|
+
"slots": slots_data,
|
|
373
|
+
}
|
|
374
|
+
typer.echo(yaml.dump(data, default_flow_style=False, allow_unicode=True), err=True)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _emit_explain_from_report(
|
|
378
|
+
report: "FusionReport",
|
|
379
|
+
weighted_packs: list,
|
|
380
|
+
) -> None:
|
|
381
|
+
"""Emit explain YAML for the text path."""
|
|
382
|
+
dominant_pack = weighted_packs[0][0]
|
|
383
|
+
# Populate report.personas from weighted_packs if not already set
|
|
384
|
+
if not report.personas:
|
|
385
|
+
report.personas = [(pack.meta.id, weight) for pack, weight in weighted_packs]
|
|
386
|
+
explain_data = {
|
|
387
|
+
"personas": [{cid: round(w, 4)} for cid, w in report.personas],
|
|
388
|
+
"merged": {
|
|
389
|
+
"decision_policy": _explain_decision_policy(report),
|
|
390
|
+
"risk_tolerance": dominant_pack.mindset.decision_framework.risk_tolerance,
|
|
391
|
+
"time_horizon": dominant_pack.mindset.decision_framework.time_horizon,
|
|
392
|
+
},
|
|
393
|
+
"removed_conflicts": report.removed_items,
|
|
394
|
+
}
|
|
395
|
+
typer.echo(
|
|
396
|
+
yaml.dump(explain_data, default_flow_style=False, allow_unicode=True),
|
|
397
|
+
err=True,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@app.command()
|
|
402
|
+
def run(
|
|
403
|
+
runtime: str = typer.Argument(..., help="Runtime name (v0: claude only)"),
|
|
404
|
+
persona: list[str] = typer.Option(..., "--persona", help="Character ID. Repeat for multi-persona."),
|
|
405
|
+
weights: Optional[str] = typer.Option(None, "--weights", help="Comma-separated weights, auto-normalized"),
|
|
406
|
+
strategy: str = typer.Option("blend", "--strategy", help="blend | dominant"),
|
|
407
|
+
format_: str = typer.Option("inject", "--format", help="text | inject (v0: equivalent)"),
|
|
408
|
+
registry: Optional[Path] = typer.Option(None, "--registry", help="Override registry path"),
|
|
409
|
+
explain: bool = typer.Option(False, "--explain", help="Print compilation summary to stderr"),
|
|
410
|
+
query: Optional[str] = typer.Argument(None, help="One-shot query. Omit for interactive mode."),
|
|
411
|
+
):
|
|
412
|
+
"""Compile mindset(s) and inject into an agent runtime."""
|
|
413
|
+
# --- compile phase ---
|
|
414
|
+
search_paths = [registry] if registry else None
|
|
415
|
+
reg = CharacterRegistry(search_paths=search_paths)
|
|
416
|
+
|
|
417
|
+
parsed_weights = _parse_weights(weights, persona)
|
|
418
|
+
if parsed_weights is None:
|
|
419
|
+
raise typer.Exit(1)
|
|
420
|
+
|
|
421
|
+
ids_deduped, weights_deduped = _deduplicate(persona, parsed_weights)
|
|
422
|
+
|
|
423
|
+
missing_cid = None
|
|
424
|
+
for cid in ids_deduped:
|
|
425
|
+
try:
|
|
426
|
+
reg.load_id(cid)
|
|
427
|
+
except KeyError:
|
|
428
|
+
missing_cid = cid
|
|
429
|
+
break
|
|
430
|
+
if missing_cid is not None:
|
|
431
|
+
typer.echo(
|
|
432
|
+
f"Error: character '{missing_cid}' not found. Run 'mindset list' to see available characters.",
|
|
433
|
+
err=True,
|
|
434
|
+
)
|
|
435
|
+
raise typer.Exit(1)
|
|
436
|
+
|
|
437
|
+
if strategy == "sequential":
|
|
438
|
+
typer.echo("Error: --strategy sequential is not supported by 'run' (v0).", err=True)
|
|
439
|
+
raise typer.Exit(1)
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
strat = FusionStrategy(strategy)
|
|
443
|
+
except ValueError:
|
|
444
|
+
typer.echo(f"Error: unknown strategy '{strategy}'.", err=True)
|
|
445
|
+
raise typer.Exit(1)
|
|
446
|
+
|
|
447
|
+
engine = FusionEngine(reg)
|
|
448
|
+
chars = list(zip(ids_deduped, weights_deduped))
|
|
449
|
+
|
|
450
|
+
# Both paths start from prepare_packs for identical normalization.
|
|
451
|
+
weighted_packs = engine.prepare_packs(chars, strat)
|
|
452
|
+
|
|
453
|
+
if format_ == "inject":
|
|
454
|
+
# New path: ConflictResolver → BehaviorIR → ClaudeRenderer
|
|
455
|
+
ir = ConflictResolver().resolve(weighted_packs)
|
|
456
|
+
injected = _render_inject(ir, fmt="inject")
|
|
457
|
+
|
|
458
|
+
if explain:
|
|
459
|
+
_emit_explain_from_ir(ir)
|
|
460
|
+
|
|
461
|
+
elif format_ == "text":
|
|
462
|
+
# Existing path: ContextBlock → to_prompt
|
|
463
|
+
show_weights = strat != FusionStrategy.sequential
|
|
464
|
+
report = FusionReport() if explain else None
|
|
465
|
+
block = ContextBlock.from_packs(weighted_packs, show_weights=show_weights, report=report)
|
|
466
|
+
injected = block.to_prompt("plain_text")
|
|
467
|
+
|
|
468
|
+
if explain:
|
|
469
|
+
_emit_explain_from_report(report, weighted_packs)
|
|
470
|
+
|
|
471
|
+
else:
|
|
472
|
+
typer.echo(f"Error: unknown format '{format_}'.", err=True)
|
|
473
|
+
raise typer.Exit(1)
|
|
474
|
+
|
|
475
|
+
# --- write temp file ---
|
|
476
|
+
fd, tmppath = tempfile.mkstemp(suffix=".txt", prefix="mindset_run_")
|
|
477
|
+
try:
|
|
478
|
+
try:
|
|
479
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
480
|
+
f.write(injected)
|
|
481
|
+
except OSError as e:
|
|
482
|
+
typer.echo(f"Error: failed to write temporary file: {e}.", err=True)
|
|
483
|
+
raise typer.Exit(1)
|
|
484
|
+
|
|
485
|
+
# --- runtime phase ---
|
|
486
|
+
if shutil.which(runtime) is None:
|
|
487
|
+
typer.echo(
|
|
488
|
+
f"Error: '{runtime}' not found. Install Claude CLI: https://claude.ai/code",
|
|
489
|
+
err=True,
|
|
490
|
+
)
|
|
491
|
+
raise typer.Exit(1)
|
|
492
|
+
|
|
493
|
+
cmd = [runtime, "--append-system-prompt-file", tmppath]
|
|
494
|
+
if query is not None:
|
|
495
|
+
cmd.append(query)
|
|
496
|
+
|
|
497
|
+
proc = subprocess.run(cmd, check=False)
|
|
498
|
+
raise typer.Exit(proc.returncode)
|
|
499
|
+
|
|
500
|
+
finally:
|
|
501
|
+
try:
|
|
502
|
+
os.unlink(tmppath)
|
|
503
|
+
except OSError:
|
|
504
|
+
pass # best-effort cleanup
|