lattice-cli 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.
- lattice/__init__.py +5 -0
- lattice/__main__.py +5 -0
- lattice/agent.py +150 -0
- lattice/cli.py +488 -0
- lattice/constraints.py +391 -0
- lattice/formatters.py +131 -0
- lattice/ipog.py +308 -0
- lattice/model.py +116 -0
- lattice/parser.py +404 -0
- lattice/resources/claude/lattice-workflow/SKILL.md +109 -0
- lattice/resources/claude/lattice-workflow/references/modeling-rules.md +52 -0
- lattice/resources/claude/lattice-workflow/references/worked-examples.md +59 -0
- lattice/resources/codex/lattice-workflow/SKILL.md +118 -0
- lattice/resources/codex/lattice-workflow/agents/openai.yaml +7 -0
- lattice/resources/codex/lattice-workflow/references/modeling-rules.md +52 -0
- lattice/resources/codex/lattice-workflow/references/worked-examples.md +59 -0
- lattice/resources/generic/lattice-workflow/SKILL.md +124 -0
- lattice/resources/generic/lattice-workflow/references/modeling-rules.md +52 -0
- lattice/resources/generic/lattice-workflow/references/worked-examples.md +59 -0
- lattice_cli-0.1.0.dist-info/METADATA +229 -0
- lattice_cli-0.1.0.dist-info/RECORD +25 -0
- lattice_cli-0.1.0.dist-info/WHEEL +5 -0
- lattice_cli-0.1.0.dist-info/entry_points.txt +2 -0
- lattice_cli-0.1.0.dist-info/licenses/LICENSE +176 -0
- lattice_cli-0.1.0.dist-info/top_level.txt +1 -0
lattice/__init__.py
ADDED
lattice/__main__.py
ADDED
lattice/agent.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
SKILL_NAME = "lattice-workflow"
|
|
10
|
+
|
|
11
|
+
MEMORY_SNIPPET = (
|
|
12
|
+
"Reach for Lattice when planning, testing, or reviewing a finite constrained surface "
|
|
13
|
+
"with interacting states such as roles, permissions, feature flags, modes, providers, "
|
|
14
|
+
"optional fields, component props, rendering branches, template variants, config "
|
|
15
|
+
"matrices, lifecycle states, or cross-product edge cases. Extract a schema with "
|
|
16
|
+
"parameters, values, and true constraints; run `lattice validate`; then run "
|
|
17
|
+
"`lattice generate`; use the generated rows as the source of truth for plan review, "
|
|
18
|
+
"rendered variants, fixtures, or missing tests instead of hand-enumerating pairwise "
|
|
19
|
+
"combinations."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
DEFAULT_PROMPT = (
|
|
23
|
+
"Use $lattice-workflow. Inspect this feature, test surface, component variant space, "
|
|
24
|
+
"template, or config matrix; extract a Lattice schema; validate it; generate pairwise "
|
|
25
|
+
"rows; and use the rows to strengthen the plan, tests, fixtures, or visual review."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
GENERIC_AGENT_INSTRUCTIONS = f"""# Lattice Agent Instructions
|
|
29
|
+
|
|
30
|
+
Lattice is harness-agnostic. Use these instructions in OpenClaw, Hermes, Codex, xAI-backed agents, Claude Code, or any agent that can run shell commands.
|
|
31
|
+
|
|
32
|
+
Memory:
|
|
33
|
+
{MEMORY_SNIPPET}
|
|
34
|
+
|
|
35
|
+
Default prompt:
|
|
36
|
+
{DEFAULT_PROMPT}
|
|
37
|
+
|
|
38
|
+
Workflow:
|
|
39
|
+
1. Check whether `lattice --help` works.
|
|
40
|
+
2. Extract one coherent schema with parameters, values, and true constraints.
|
|
41
|
+
3. Encode nonsensical or impossible combinations as constraints. Do not strip constraints and mark invalid rows after generation. Lattice supports exclusion-style constraints such as `invalid_pair` and `higher_order`, not only `conditional` parameters.
|
|
42
|
+
4. Run `lattice validate` before generation.
|
|
43
|
+
5. Run `lattice generate` and treat the generated rows as the source of truth.
|
|
44
|
+
6. Map rows to the active evaluator: plan review, tests, fixtures, rendered variants, screenshots, visual diffs, sandbox calls, or another harness-specific action.
|
|
45
|
+
|
|
46
|
+
Prefer JSON or YAML on stdin:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cat schema.json | lattice validate
|
|
50
|
+
cat schema.json | lattice generate
|
|
51
|
+
```
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class AgentSetup:
|
|
57
|
+
surface: str
|
|
58
|
+
skill_name: str
|
|
59
|
+
skill_path: Path
|
|
60
|
+
memory: str
|
|
61
|
+
default_prompt: str
|
|
62
|
+
|
|
63
|
+
def to_json(self) -> dict[str, Any]:
|
|
64
|
+
return {
|
|
65
|
+
"surface": self.surface,
|
|
66
|
+
"skill_name": self.skill_name,
|
|
67
|
+
"skill_path": str(self.skill_path),
|
|
68
|
+
"memory": self.memory,
|
|
69
|
+
"default_prompt": self.default_prompt,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def default_codex_skill_path() -> Path:
|
|
74
|
+
codex_home = os.environ.get("CODEX_HOME")
|
|
75
|
+
base = Path(codex_home).expanduser() if codex_home else Path.home() / ".codex"
|
|
76
|
+
return base / "skills" / SKILL_NAME
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def default_claude_skill_path() -> Path:
|
|
80
|
+
return Path.home() / ".claude" / "skills" / SKILL_NAME
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def codex_available() -> bool:
|
|
84
|
+
return (
|
|
85
|
+
shutil.which("codex") is not None
|
|
86
|
+
or (Path.home() / ".codex").exists()
|
|
87
|
+
or bool(os.environ.get("CODEX_HOME"))
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def claude_code_available() -> bool:
|
|
92
|
+
return shutil.which("claude") is not None or (Path.home() / ".claude").exists()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def install_codex_skill(target: str | None = None) -> AgentSetup:
|
|
96
|
+
destination = Path(target).expanduser() if target else default_codex_skill_path()
|
|
97
|
+
_copy_bundled_skill("codex", destination)
|
|
98
|
+
return AgentSetup(
|
|
99
|
+
surface="codex",
|
|
100
|
+
skill_name=SKILL_NAME,
|
|
101
|
+
skill_path=destination,
|
|
102
|
+
memory=MEMORY_SNIPPET,
|
|
103
|
+
default_prompt=DEFAULT_PROMPT,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def install_generic_skill(target: str) -> AgentSetup:
|
|
108
|
+
destination = Path(target).expanduser()
|
|
109
|
+
_copy_bundled_skill("generic", destination)
|
|
110
|
+
return AgentSetup(
|
|
111
|
+
surface="generic",
|
|
112
|
+
skill_name=SKILL_NAME,
|
|
113
|
+
skill_path=destination,
|
|
114
|
+
memory=MEMORY_SNIPPET,
|
|
115
|
+
default_prompt=DEFAULT_PROMPT,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def install_claude_skill(target: str | None = None, *, force: bool = False) -> AgentSetup:
|
|
120
|
+
if not force and not claude_code_available():
|
|
121
|
+
raise RuntimeError("Claude Code was not detected. Re-run with --force to install anyway.")
|
|
122
|
+
|
|
123
|
+
destination = Path(target).expanduser() if target else default_claude_skill_path()
|
|
124
|
+
_copy_bundled_skill("claude", destination)
|
|
125
|
+
return AgentSetup(
|
|
126
|
+
surface="claude-code",
|
|
127
|
+
skill_name=SKILL_NAME,
|
|
128
|
+
skill_path=destination,
|
|
129
|
+
memory=MEMORY_SNIPPET,
|
|
130
|
+
default_prompt=DEFAULT_PROMPT,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def codex_skill_installed(target: str | None = None) -> bool:
|
|
135
|
+
destination = Path(target).expanduser() if target else default_codex_skill_path()
|
|
136
|
+
return (destination / "SKILL.md").is_file()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def claude_skill_installed(target: str | None = None) -> bool:
|
|
140
|
+
destination = Path(target).expanduser() if target else default_claude_skill_path()
|
|
141
|
+
return (destination / "SKILL.md").is_file()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _copy_bundled_skill(resource_name: str, destination: Path) -> None:
|
|
145
|
+
source = Path(__file__).with_name("resources") / resource_name / SKILL_NAME
|
|
146
|
+
if not source.is_dir():
|
|
147
|
+
raise FileNotFoundError(f"Bundled {resource_name} skill `{SKILL_NAME}` was not found.")
|
|
148
|
+
|
|
149
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
shutil.copytree(source, destination, dirs_exist_ok=True)
|
lattice/cli.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Callable, Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from lattice.agent import (
|
|
12
|
+
AgentSetup,
|
|
13
|
+
DEFAULT_PROMPT,
|
|
14
|
+
GENERIC_AGENT_INSTRUCTIONS,
|
|
15
|
+
MEMORY_SNIPPET,
|
|
16
|
+
claude_code_available,
|
|
17
|
+
claude_skill_installed,
|
|
18
|
+
codex_available,
|
|
19
|
+
codex_skill_installed,
|
|
20
|
+
default_claude_skill_path,
|
|
21
|
+
default_codex_skill_path,
|
|
22
|
+
install_claude_skill,
|
|
23
|
+
install_codex_skill,
|
|
24
|
+
install_generic_skill,
|
|
25
|
+
)
|
|
26
|
+
from lattice.constraints import ConstraintEngine, ConstraintError
|
|
27
|
+
from lattice.formatters import render_output
|
|
28
|
+
from lattice.ipog import GenerationProgress, generate_covering_array
|
|
29
|
+
from lattice.parser import ModelIOError, ValidationError, load_model
|
|
30
|
+
|
|
31
|
+
DEFAULT_AGENT_FORMAT = "text"
|
|
32
|
+
SCHEMA_HELP = (
|
|
33
|
+
"Schema features: constraints include invalid_pair, higher_order, "
|
|
34
|
+
"forward_dep, bidirectional, conditional, and forced; parameters may use "
|
|
35
|
+
"weights. Run `lattice agent instructions` for the full schema workflow."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
40
|
+
parser = _build_parser()
|
|
41
|
+
args = parser.parse_args(argv)
|
|
42
|
+
|
|
43
|
+
if args.command == "agent":
|
|
44
|
+
return _run_agent_command(args)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
model = load_model(args.model, strength_override=getattr(args, "strength", None))
|
|
48
|
+
engine = ConstraintEngine(model)
|
|
49
|
+
if not engine.can_complete({}):
|
|
50
|
+
raise ValidationError(["Model has no valid complete assignments."])
|
|
51
|
+
except ValidationError as exc:
|
|
52
|
+
return _emit_error(args, "validation_error", exc.errors, exit_code=1)
|
|
53
|
+
except ConstraintError as exc:
|
|
54
|
+
return _emit_error(args, "constraint_error", [str(exc)], exit_code=1)
|
|
55
|
+
except ModelIOError as exc:
|
|
56
|
+
return _emit_error(args, "io_error", [str(exc)], exit_code=2)
|
|
57
|
+
|
|
58
|
+
if args.command == "validate":
|
|
59
|
+
_print_validation_success(args, model)
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
progress_callback = (
|
|
64
|
+
_build_progress_callback(args.progress_every)
|
|
65
|
+
if getattr(args, "progress", False)
|
|
66
|
+
else None
|
|
67
|
+
)
|
|
68
|
+
result = generate_covering_array(
|
|
69
|
+
model,
|
|
70
|
+
strength=model.strength,
|
|
71
|
+
seed=args.seed,
|
|
72
|
+
pool_size=args.pool_size,
|
|
73
|
+
progress=progress_callback,
|
|
74
|
+
max_rows=args.max_rows,
|
|
75
|
+
stop_after_coverage=args.stop_after_coverage,
|
|
76
|
+
)
|
|
77
|
+
except ValueError as exc:
|
|
78
|
+
return _emit_error(args, "generation_error", [str(exc)], exit_code=1)
|
|
79
|
+
|
|
80
|
+
print(render_output(model, result, output_format=args.format, seed=args.seed))
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
85
|
+
parser = argparse.ArgumentParser(prog="lattice")
|
|
86
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
87
|
+
|
|
88
|
+
generate = subparsers.add_parser(
|
|
89
|
+
"generate",
|
|
90
|
+
help="Generate deterministic scenarios from a schema.",
|
|
91
|
+
epilog=SCHEMA_HELP,
|
|
92
|
+
)
|
|
93
|
+
generate.add_argument("model", nargs="?", help="Path to a JSON or YAML schema. Reads stdin when omitted.")
|
|
94
|
+
generate.add_argument("--strength", type=int, help="Override the schema strength.")
|
|
95
|
+
generate.add_argument("--format", default="json", choices=("table", "json", "csv", "summary"))
|
|
96
|
+
generate.add_argument("--seed", type=int, default=42)
|
|
97
|
+
generate.add_argument("--pool-size", type=int, default=50)
|
|
98
|
+
generate.add_argument(
|
|
99
|
+
"--max-rows",
|
|
100
|
+
type=int,
|
|
101
|
+
help="Generate at most the first N scenarios.",
|
|
102
|
+
)
|
|
103
|
+
generate.add_argument(
|
|
104
|
+
"--stop-after-coverage",
|
|
105
|
+
type=float,
|
|
106
|
+
help="Stop after the first generated scenario reaching this cumulative coverage percent.",
|
|
107
|
+
)
|
|
108
|
+
generate.add_argument("--progress", action="store_true", help="Print generation progress to stderr.")
|
|
109
|
+
generate.add_argument(
|
|
110
|
+
"--progress-every",
|
|
111
|
+
type=int,
|
|
112
|
+
default=10,
|
|
113
|
+
help="With --progress, print every N generated scenarios plus the final scenario.",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
validate = subparsers.add_parser(
|
|
117
|
+
"validate",
|
|
118
|
+
help="Validate a schema without generating scenarios.",
|
|
119
|
+
epilog=SCHEMA_HELP,
|
|
120
|
+
)
|
|
121
|
+
validate.add_argument("model", nargs="?", help="Path to a JSON or YAML schema. Reads stdin when omitted.")
|
|
122
|
+
validate.add_argument("--strength", type=int, help="Override the schema strength.")
|
|
123
|
+
validate.add_argument("--format", default="json", choices=("json", "text"))
|
|
124
|
+
|
|
125
|
+
agent = subparsers.add_parser("agent", help="Install and describe coding-agent integrations.")
|
|
126
|
+
agent_subparsers = agent.add_subparsers(dest="agent_command", required=True)
|
|
127
|
+
|
|
128
|
+
bootstrap = agent_subparsers.add_parser(
|
|
129
|
+
"bootstrap",
|
|
130
|
+
help="Install skills for detected known harnesses and print universal agent guidance.",
|
|
131
|
+
)
|
|
132
|
+
bootstrap.add_argument("--target", help="Codex skill directory. Defaults to $CODEX_HOME or ~/.codex.")
|
|
133
|
+
bootstrap.add_argument("--claude-target", help="Claude Code skill directory. Defaults to ~/.claude/skills.")
|
|
134
|
+
bootstrap.add_argument("--force-codex", action="store_true", help="Install the Codex skill even if Codex is not detected.")
|
|
135
|
+
bootstrap.add_argument("--skip-codex", action="store_true", help="Do not install the Codex skill.")
|
|
136
|
+
bootstrap.add_argument("--skip-claude", action="store_true", help="Do not install the Claude Code skill.")
|
|
137
|
+
bootstrap.add_argument("--force-claude", action="store_true", help="Install the Claude Code skill even if Claude Code is not detected.")
|
|
138
|
+
bootstrap.add_argument("--format", default=DEFAULT_AGENT_FORMAT, choices=("text", "json"))
|
|
139
|
+
|
|
140
|
+
install_skill = agent_subparsers.add_parser("install-codex-skill", help="Install the bundled Codex skill.")
|
|
141
|
+
install_skill.add_argument("--target", help="Destination skill directory. Defaults to $CODEX_HOME or ~/.codex.")
|
|
142
|
+
install_skill.add_argument("--format", default=DEFAULT_AGENT_FORMAT, choices=("text", "json"))
|
|
143
|
+
|
|
144
|
+
install_generic = agent_subparsers.add_parser(
|
|
145
|
+
"install-skill",
|
|
146
|
+
help="Install the bundled generic skill into any harness-specific skill directory.",
|
|
147
|
+
)
|
|
148
|
+
install_generic.add_argument("target", help="Destination skill directory for the target harness.")
|
|
149
|
+
install_generic.add_argument("--format", default=DEFAULT_AGENT_FORMAT, choices=("text", "json"))
|
|
150
|
+
|
|
151
|
+
install_claude = agent_subparsers.add_parser("install-claude-skill", help="Install the bundled Claude Code skill.")
|
|
152
|
+
install_claude.add_argument("--target", help="Destination skill directory. Defaults to ~/.claude/skills.")
|
|
153
|
+
install_claude.add_argument("--force", action="store_true", help="Install even if Claude Code is not detected.")
|
|
154
|
+
install_claude.add_argument("--format", default=DEFAULT_AGENT_FORMAT, choices=("text", "json"))
|
|
155
|
+
|
|
156
|
+
memory = agent_subparsers.add_parser("memory", help="Print the persistent memory snippet for agents.")
|
|
157
|
+
memory.add_argument("--format", default=DEFAULT_AGENT_FORMAT, choices=("text", "json"))
|
|
158
|
+
|
|
159
|
+
instructions = agent_subparsers.add_parser(
|
|
160
|
+
"instructions",
|
|
161
|
+
help="Print harness-agnostic instructions for any coding agent.",
|
|
162
|
+
)
|
|
163
|
+
instructions.add_argument("--format", default=DEFAULT_AGENT_FORMAT, choices=("text", "json"))
|
|
164
|
+
|
|
165
|
+
doctor = agent_subparsers.add_parser("doctor", help="Check whether agent skills are installed.")
|
|
166
|
+
doctor.add_argument("--target", help="Codex skill directory to check. Defaults to $CODEX_HOME or ~/.codex.")
|
|
167
|
+
doctor.add_argument("--claude-target", help="Claude Code skill directory to check. Defaults to ~/.claude/skills.")
|
|
168
|
+
doctor.add_argument("--format", default=DEFAULT_AGENT_FORMAT, choices=("text", "json"))
|
|
169
|
+
return parser
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _build_progress_callback(every: int) -> Callable[[GenerationProgress], None]:
|
|
173
|
+
if every < 1:
|
|
174
|
+
raise ValueError("--progress-every must be at least 1")
|
|
175
|
+
|
|
176
|
+
started = time.monotonic()
|
|
177
|
+
|
|
178
|
+
def report(progress: GenerationProgress) -> None:
|
|
179
|
+
elapsed = time.monotonic() - started
|
|
180
|
+
if progress.event == "enumerating":
|
|
181
|
+
eta = _format_eta(elapsed, progress.covered, progress.total)
|
|
182
|
+
print(
|
|
183
|
+
"lattice progress: "
|
|
184
|
+
f"enumerating interactions "
|
|
185
|
+
f"checked={progress.covered}/{progress.total} "
|
|
186
|
+
f"progress={progress.cumulative_pct:.1f}% "
|
|
187
|
+
f"elapsed={_format_seconds(elapsed)} "
|
|
188
|
+
f"eta={eta}",
|
|
189
|
+
file=sys.stderr,
|
|
190
|
+
flush=True,
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
if progress.event == "enumerated":
|
|
195
|
+
print(
|
|
196
|
+
"lattice progress: "
|
|
197
|
+
f"enumerated {progress.total} valid interactions; starting generation",
|
|
198
|
+
file=sys.stderr,
|
|
199
|
+
flush=True,
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
if progress.event == "scenario" and progress.scenario % every != 0 and progress.uncovered != 0:
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
eta = _format_eta(elapsed, progress.covered, progress.total)
|
|
207
|
+
label = "stopped " if progress.event == "stopped" else ""
|
|
208
|
+
print(
|
|
209
|
+
"lattice progress: "
|
|
210
|
+
f"{label}scenario={progress.scenario} "
|
|
211
|
+
f"covered={progress.covered}/{progress.total} "
|
|
212
|
+
f"coverage={progress.cumulative_pct:.1f}% "
|
|
213
|
+
f"uncovered={progress.uncovered} "
|
|
214
|
+
f"elapsed={_format_seconds(elapsed)} "
|
|
215
|
+
f"eta={eta}",
|
|
216
|
+
file=sys.stderr,
|
|
217
|
+
flush=True,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return report
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _format_eta(elapsed: float, covered: int, total: int) -> str:
|
|
224
|
+
if total <= 0 or covered <= 0:
|
|
225
|
+
return "unknown"
|
|
226
|
+
if covered >= total:
|
|
227
|
+
return "0s"
|
|
228
|
+
remaining = elapsed * ((total - covered) / covered)
|
|
229
|
+
return _format_seconds(remaining)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _format_seconds(seconds: float) -> str:
|
|
233
|
+
seconds = max(0, int(round(seconds)))
|
|
234
|
+
minutes, sec = divmod(seconds, 60)
|
|
235
|
+
hours, minutes = divmod(minutes, 60)
|
|
236
|
+
if hours:
|
|
237
|
+
return f"{hours}h{minutes:02d}m{sec:02d}s"
|
|
238
|
+
if minutes:
|
|
239
|
+
return f"{minutes}m{sec:02d}s"
|
|
240
|
+
return f"{sec}s"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _print_errors(errors: list[str]) -> None:
|
|
244
|
+
for error in errors:
|
|
245
|
+
print(error, file=sys.stderr)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _emit_error(args: argparse.Namespace, code: str, errors: list[str], *, exit_code: int) -> int:
|
|
249
|
+
if getattr(args, "format", None) == "json":
|
|
250
|
+
print(json.dumps({"status": "error", "code": code, "errors": errors}, indent=2))
|
|
251
|
+
else:
|
|
252
|
+
_print_errors(errors)
|
|
253
|
+
return exit_code
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _print_validation_success(args: argparse.Namespace, model) -> None:
|
|
257
|
+
if args.format == "json":
|
|
258
|
+
print(
|
|
259
|
+
json.dumps(
|
|
260
|
+
{
|
|
261
|
+
"status": "ok",
|
|
262
|
+
"valid": True,
|
|
263
|
+
"schema": {
|
|
264
|
+
"name": model.model_name,
|
|
265
|
+
"parameter_count": len(model.parameters),
|
|
266
|
+
"constraint_count": len(model.constraints),
|
|
267
|
+
"strength": model.strength,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
indent=2,
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
print(
|
|
276
|
+
"\n".join(
|
|
277
|
+
[
|
|
278
|
+
"Schema is valid.",
|
|
279
|
+
f"name: {model.model_name}",
|
|
280
|
+
f"parameters: {len(model.parameters)}",
|
|
281
|
+
f"constraints: {len(model.constraints)}",
|
|
282
|
+
f"strength: {model.strength}",
|
|
283
|
+
]
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _run_agent_command(args: argparse.Namespace) -> int:
|
|
289
|
+
if args.agent_command == "bootstrap":
|
|
290
|
+
codex_detected = codex_available()
|
|
291
|
+
claude_available = claude_code_available()
|
|
292
|
+
codex_skill_path = Path(args.target).expanduser() if args.target else default_codex_skill_path()
|
|
293
|
+
claude_skill_path = Path(args.claude_target).expanduser() if args.claude_target else default_claude_skill_path()
|
|
294
|
+
codex_setup, codex_skipped_reason = _maybe_install_agent_skill(
|
|
295
|
+
detected=codex_detected,
|
|
296
|
+
force=args.force_codex,
|
|
297
|
+
skip=args.skip_codex,
|
|
298
|
+
skip_flag="--skip-codex",
|
|
299
|
+
unavailable_reason="Codex was not detected",
|
|
300
|
+
install=lambda: install_codex_skill(args.target),
|
|
301
|
+
)
|
|
302
|
+
claude_setup, claude_skipped_reason = _maybe_install_agent_skill(
|
|
303
|
+
detected=claude_available,
|
|
304
|
+
force=args.force_claude,
|
|
305
|
+
skip=args.skip_claude,
|
|
306
|
+
skip_flag="--skip-claude",
|
|
307
|
+
unavailable_reason="Claude Code was not detected",
|
|
308
|
+
install=lambda: install_claude_skill(args.claude_target, force=args.force_claude),
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if args.format == "json":
|
|
312
|
+
payload = {
|
|
313
|
+
"status": "ok",
|
|
314
|
+
"codex": _agent_surface_payload(
|
|
315
|
+
detected=codex_detected,
|
|
316
|
+
setup=codex_setup,
|
|
317
|
+
skill_path=codex_skill_path,
|
|
318
|
+
skipped_reason=codex_skipped_reason,
|
|
319
|
+
),
|
|
320
|
+
"claude": _agent_surface_payload(
|
|
321
|
+
detected=claude_available,
|
|
322
|
+
setup=claude_setup,
|
|
323
|
+
skill_path=claude_skill_path,
|
|
324
|
+
skipped_reason=claude_skipped_reason,
|
|
325
|
+
),
|
|
326
|
+
"memory": MEMORY_SNIPPET,
|
|
327
|
+
"default_prompt": DEFAULT_PROMPT,
|
|
328
|
+
"instructions_command": "lattice agent instructions",
|
|
329
|
+
"generic_skill_command": "lattice agent install-skill <target>",
|
|
330
|
+
}
|
|
331
|
+
print(json.dumps(payload, indent=2))
|
|
332
|
+
return 0
|
|
333
|
+
|
|
334
|
+
_print_agent_install_status("Codex", codex_setup, codex_skipped_reason)
|
|
335
|
+
_print_agent_install_status("Claude Code", claude_setup, claude_skipped_reason)
|
|
336
|
+
print("")
|
|
337
|
+
print("Agent memory:")
|
|
338
|
+
print(MEMORY_SNIPPET)
|
|
339
|
+
print("")
|
|
340
|
+
print("Default prompt:")
|
|
341
|
+
print(DEFAULT_PROMPT)
|
|
342
|
+
print("")
|
|
343
|
+
print("Any other harness:")
|
|
344
|
+
print("Run `lattice agent instructions`, or `lattice agent install-skill <target>` if the harness accepts a SKILL.md directory.")
|
|
345
|
+
return 0
|
|
346
|
+
|
|
347
|
+
if args.agent_command == "install-codex-skill":
|
|
348
|
+
setup = install_codex_skill(args.target)
|
|
349
|
+
return _emit_agent_setup(setup, "Codex", args.format)
|
|
350
|
+
|
|
351
|
+
if args.agent_command == "install-skill":
|
|
352
|
+
setup = install_generic_skill(args.target)
|
|
353
|
+
return _emit_agent_setup(setup, "generic", args.format)
|
|
354
|
+
|
|
355
|
+
if args.agent_command == "install-claude-skill":
|
|
356
|
+
try:
|
|
357
|
+
setup = install_claude_skill(args.target, force=args.force)
|
|
358
|
+
except RuntimeError as exc:
|
|
359
|
+
if args.format == "json":
|
|
360
|
+
print(json.dumps({"status": "unavailable", "error": str(exc)}, indent=2))
|
|
361
|
+
else:
|
|
362
|
+
print(str(exc), file=sys.stderr)
|
|
363
|
+
return 1
|
|
364
|
+
|
|
365
|
+
return _emit_agent_setup(
|
|
366
|
+
setup,
|
|
367
|
+
"Claude Code",
|
|
368
|
+
args.format,
|
|
369
|
+
extra={"available": claude_code_available()},
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if args.agent_command == "memory":
|
|
373
|
+
if args.format == "json":
|
|
374
|
+
print(json.dumps({"memory": MEMORY_SNIPPET}, indent=2))
|
|
375
|
+
else:
|
|
376
|
+
print(MEMORY_SNIPPET)
|
|
377
|
+
return 0
|
|
378
|
+
|
|
379
|
+
if args.agent_command == "instructions":
|
|
380
|
+
if args.format == "json":
|
|
381
|
+
print(
|
|
382
|
+
json.dumps(
|
|
383
|
+
{
|
|
384
|
+
"instructions": GENERIC_AGENT_INSTRUCTIONS,
|
|
385
|
+
"memory": MEMORY_SNIPPET,
|
|
386
|
+
"default_prompt": DEFAULT_PROMPT,
|
|
387
|
+
},
|
|
388
|
+
indent=2,
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
else:
|
|
392
|
+
print(GENERIC_AGENT_INSTRUCTIONS)
|
|
393
|
+
return 0
|
|
394
|
+
|
|
395
|
+
if args.agent_command == "doctor":
|
|
396
|
+
codex_path = default_codex_skill_path() if args.target is None else args.target
|
|
397
|
+
claude_path = default_claude_skill_path() if args.claude_target is None else args.claude_target
|
|
398
|
+
codex_detected = codex_available()
|
|
399
|
+
codex_installed = codex_skill_installed(args.target)
|
|
400
|
+
claude_available = claude_code_available()
|
|
401
|
+
claude_installed = claude_skill_installed(args.claude_target)
|
|
402
|
+
known_harness_detected = codex_detected or claude_available
|
|
403
|
+
known_skills_ok = (not codex_detected or codex_installed) and (not claude_available or claude_installed)
|
|
404
|
+
ok = known_skills_ok
|
|
405
|
+
payload = {
|
|
406
|
+
"status": "ok" if ok else "missing",
|
|
407
|
+
"generic_instructions_available": True,
|
|
408
|
+
"known_harness_detected": known_harness_detected,
|
|
409
|
+
"codex_available": codex_detected,
|
|
410
|
+
"codex_skill_installed": codex_installed,
|
|
411
|
+
"codex_skill_path": str(codex_path),
|
|
412
|
+
"claude_code_available": claude_available,
|
|
413
|
+
"claude_skill_installed": claude_installed,
|
|
414
|
+
"claude_skill_path": str(claude_path),
|
|
415
|
+
}
|
|
416
|
+
if args.format == "json":
|
|
417
|
+
print(json.dumps(payload, indent=2))
|
|
418
|
+
else:
|
|
419
|
+
print("Lattice agent setup")
|
|
420
|
+
print("cli: ok")
|
|
421
|
+
print("generic instructions: available (`lattice agent instructions`)")
|
|
422
|
+
print(f"codex: {'available' if codex_detected else 'not detected'}")
|
|
423
|
+
print(f"codex skill: {'installed' if codex_installed else 'missing'}")
|
|
424
|
+
print(f"codex path: {codex_path}")
|
|
425
|
+
print(f"claude code: {'available' if claude_available else 'not detected'}")
|
|
426
|
+
print(f"claude skill: {'installed' if claude_installed else 'missing'}")
|
|
427
|
+
print(f"claude path: {claude_path}")
|
|
428
|
+
return 0 if ok else 1
|
|
429
|
+
|
|
430
|
+
print(f"Unknown agent command `{args.agent_command}`.", file=sys.stderr)
|
|
431
|
+
return 2
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _maybe_install_agent_skill(
|
|
435
|
+
*,
|
|
436
|
+
detected: bool,
|
|
437
|
+
force: bool,
|
|
438
|
+
skip: bool,
|
|
439
|
+
skip_flag: str,
|
|
440
|
+
unavailable_reason: str,
|
|
441
|
+
install: Callable[[], AgentSetup],
|
|
442
|
+
) -> tuple[AgentSetup | None, str | None]:
|
|
443
|
+
if skip:
|
|
444
|
+
return None, f"skipped by {skip_flag}"
|
|
445
|
+
if detected or force:
|
|
446
|
+
return install(), None
|
|
447
|
+
return None, unavailable_reason
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _agent_surface_payload(
|
|
451
|
+
*,
|
|
452
|
+
detected: bool,
|
|
453
|
+
setup: AgentSetup | None,
|
|
454
|
+
skill_path: Path,
|
|
455
|
+
skipped_reason: str | None,
|
|
456
|
+
) -> dict[str, Any]:
|
|
457
|
+
return {
|
|
458
|
+
"available": detected,
|
|
459
|
+
"installed": setup is not None,
|
|
460
|
+
"skill_path": str(setup.skill_path if setup else skill_path),
|
|
461
|
+
"skipped_reason": skipped_reason,
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _emit_agent_setup(
|
|
466
|
+
setup: AgentSetup,
|
|
467
|
+
label: str,
|
|
468
|
+
output_format: str,
|
|
469
|
+
*,
|
|
470
|
+
extra: dict[str, Any] | None = None,
|
|
471
|
+
) -> int:
|
|
472
|
+
if output_format == "json":
|
|
473
|
+
print(json.dumps({"status": "ok", **(extra or {}), **setup.to_json()}, indent=2))
|
|
474
|
+
return 0
|
|
475
|
+
|
|
476
|
+
print(f"Installed `{setup.skill_name}` {label} skill at {setup.skill_path}")
|
|
477
|
+
return 0
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _print_agent_install_status(
|
|
481
|
+
label: str,
|
|
482
|
+
setup: AgentSetup | None,
|
|
483
|
+
skipped_reason: str | None,
|
|
484
|
+
) -> None:
|
|
485
|
+
if setup is not None:
|
|
486
|
+
print(f"Installed `{setup.skill_name}` {label} skill at {setup.skill_path}")
|
|
487
|
+
elif skipped_reason:
|
|
488
|
+
print(f"{label} skill: not installed ({skipped_reason})")
|