chipgraph 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.
- chipgraph/__init__.py +7 -0
- chipgraph/adapters/__init__.py +1 -0
- chipgraph/adapters/format/__init__.py +6 -0
- chipgraph/adapters/format/__main__.py +16 -0
- chipgraph/adapters/format/_yaml_lines.py +54 -0
- chipgraph/adapters/format/chip_yaml.py +323 -0
- chipgraph/adapters/format/qsoc_contract.py +246 -0
- chipgraph/adapters/llm/__init__.py +58 -0
- chipgraph/adapters/llm/_budget.py +173 -0
- chipgraph/adapters/llm/_errors.py +50 -0
- chipgraph/adapters/llm/_nda.py +30 -0
- chipgraph/adapters/llm/_retry.py +85 -0
- chipgraph/adapters/llm/anthropic.py +247 -0
- chipgraph/adapters/llm/decide_backend.py +179 -0
- chipgraph/adapters/llm/fake.py +87 -0
- chipgraph/adapters/llm/guard.py +68 -0
- chipgraph/adapters/parser/__init__.py +22 -0
- chipgraph/adapters/parser/cocotb.py +304 -0
- chipgraph/adapters/parser/generic_regex.py +75 -0
- chipgraph/adapters/parser/verible.py +55 -0
- chipgraph/adapters/parser/verilator.py +46 -0
- chipgraph/adapters/review/__init__.py +7 -0
- chipgraph/adapters/review/file.py +123 -0
- chipgraph/adapters/review/github.py +886 -0
- chipgraph/adapters/runner/__init__.py +7 -0
- chipgraph/adapters/runner/local.py +122 -0
- chipgraph/adapters/runtime/__init__.py +5 -0
- chipgraph/adapters/runtime/claude_code/__init__.py +24 -0
- chipgraph/adapters/runtime/claude_code/agents.py +158 -0
- chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
- chipgraph/adapters/runtime/claude_code/guarded.py +310 -0
- chipgraph/adapters/runtime/claude_code/isolation.py +482 -0
- chipgraph/adapters/runtime/claude_code/loop.py +342 -0
- chipgraph/adapters/runtime/claude_code/previous.py +419 -0
- chipgraph/adapters/runtime/claude_code/review.py +492 -0
- chipgraph/adapters/runtime/claude_code/runtime.py +192 -0
- chipgraph/adapters/runtime/claude_code/service.py +2285 -0
- chipgraph/adapters/tool/__init__.py +7 -0
- chipgraph/adapters/tool/cmd.py +229 -0
- chipgraph/adapters/tool/edalize.py +773 -0
- chipgraph/adapters/tool/filelist.py +259 -0
- chipgraph/adapters/tool/pyslang.py +485 -0
- chipgraph/adapters/vcs/__init__.py +1 -0
- chipgraph/adapters/vcs/git.py +521 -0
- chipgraph/app/__init__.py +10 -0
- chipgraph/app/baseline.py +339 -0
- chipgraph/app/build.py +332 -0
- chipgraph/app/checks.py +202 -0
- chipgraph/app/context.py +191 -0
- chipgraph/app/errors.py +11 -0
- chipgraph/app/executors.py +79 -0
- chipgraph/app/findings.py +203 -0
- chipgraph/app/ingest.py +523 -0
- chipgraph/checks/__init__.py +52 -0
- chipgraph/checks/_cdc_analyze.py +272 -0
- chipgraph/checks/_cdc_elaborate.py +455 -0
- chipgraph/checks/_common.py +268 -0
- chipgraph/checks/_cross.py +96 -0
- chipgraph/checks/_model.py +64 -0
- chipgraph/checks/_naming_pyslang.py +235 -0
- chipgraph/checks/_naming_rules.py +127 -0
- chipgraph/checks/_xref_syntax.py +260 -0
- chipgraph/checks/cdc_struct.py +235 -0
- chipgraph/checks/connect.py +353 -0
- chipgraph/checks/cross_chip.py +454 -0
- chipgraph/checks/duplicate.py +153 -0
- chipgraph/checks/filelist.py +323 -0
- chipgraph/checks/format.py +268 -0
- chipgraph/checks/generated.py +151 -0
- chipgraph/checks/hardcode.py +233 -0
- chipgraph/checks/layout.py +155 -0
- chipgraph/checks/naming.py +311 -0
- chipgraph/checks/plan_check.py +901 -0
- chipgraph/checks/ports_diff.py +203 -0
- chipgraph/checks/spec_schema.py +277 -0
- chipgraph/checks/style.py +910 -0
- chipgraph/checks/tb_static.py +599 -0
- chipgraph/checks/trace.py +224 -0
- chipgraph/checks/vendor_guard.py +369 -0
- chipgraph/checks/wrap_check.py +486 -0
- chipgraph/cli/__init__.py +1688 -0
- chipgraph/cli/next_steps.py +137 -0
- chipgraph/core/__init__.py +1 -0
- chipgraph/core/config/__init__.py +93 -0
- chipgraph/core/config/defaults.py +14 -0
- chipgraph/core/config/errors.py +32 -0
- chipgraph/core/config/loader.py +766 -0
- chipgraph/core/config/models.py +845 -0
- chipgraph/core/config/templates.py +340 -0
- chipgraph/core/contracts/__init__.py +75 -0
- chipgraph/core/contracts/_paths.py +26 -0
- chipgraph/core/contracts/agent.py +49 -0
- chipgraph/core/contracts/artifact.py +68 -0
- chipgraph/core/contracts/check.py +76 -0
- chipgraph/core/contracts/event.py +70 -0
- chipgraph/core/contracts/export.py +128 -0
- chipgraph/core/contracts/finding.py +171 -0
- chipgraph/core/contracts/gate.py +50 -0
- chipgraph/core/contracts/rule.py +177 -0
- chipgraph/core/contracts/types.py +45 -0
- chipgraph/core/engine/__init__.py +70 -0
- chipgraph/core/engine/agent_rule.py +1362 -0
- chipgraph/core/engine/baseline.py +283 -0
- chipgraph/core/engine/decide.py +462 -0
- chipgraph/core/engine/fanout.py +1358 -0
- chipgraph/core/engine/gate.py +170 -0
- chipgraph/core/engine/graph.py +647 -0
- chipgraph/core/engine/records.py +60 -0
- chipgraph/core/engine/rules.py +92 -0
- chipgraph/core/engine/scheduler.py +637 -0
- chipgraph/core/engine/select.py +1239 -0
- chipgraph/core/engine/targets.py +342 -0
- chipgraph/core/model/__init__.py +98 -0
- chipgraph/core/model/entities.py +244 -0
- chipgraph/core/model/export.py +16 -0
- chipgraph/core/model/ingest.py +693 -0
- chipgraph/core/model/json_value.py +4 -0
- chipgraph/core/model/keys.py +95 -0
- chipgraph/core/model/model.py +153 -0
- chipgraph/core/model/provenance.py +27 -0
- chipgraph/core/model/query.py +583 -0
- chipgraph/core/model/registry.py +80 -0
- chipgraph/core/model/relations.py +61 -0
- chipgraph/core/model/store.py +279 -0
- chipgraph/core/plugin_api/__init__.py +81 -0
- chipgraph/core/plugin_api/local.py +167 -0
- chipgraph/core/plugin_api/pack.py +159 -0
- chipgraph/core/plugin_api/protocols.py +177 -0
- chipgraph/core/plugin_api/registry.py +111 -0
- chipgraph/core/plugin_api/types.py +94 -0
- chipgraph/core/runtime/__init__.py +41 -0
- chipgraph/core/runtime/executor.py +79 -0
- chipgraph/core/runtime/queue.py +565 -0
- chipgraph/core/runtime/roles/__init__.py +91 -0
- chipgraph/core/runtime/roles/data/author.md +41 -0
- chipgraph/core/runtime/roles/data/critic.md +56 -0
- chipgraph/core/runtime/roles/data/planner.md +38 -0
- chipgraph/core/runtime/roles/data/researcher.md +37 -0
- chipgraph/core/runtime/roles/data/rtl-author.md +64 -0
- chipgraph/core/runtime/roles/data/tb-author.md +48 -0
- chipgraph/core/runtime/roles/data/triage.md +34 -0
- chipgraph/core/runtime/roles/policy.py +299 -0
- chipgraph/core/runtime/roles/registry.py +52 -0
- chipgraph/core/runtime/roles/review.py +275 -0
- chipgraph/core/runtime/roles/schema.py +82 -0
- chipgraph/core/runtime/roles/skills.py +134 -0
- chipgraph/core/runtime/roles/spec.py +232 -0
- chipgraph/core/state/__init__.py +1 -0
- chipgraph/core/state/artifacts.py +156 -0
- chipgraph/core/state/backend.py +117 -0
- chipgraph/core/state/findings.py +151 -0
- chipgraph/core/state/handoff.py +411 -0
- chipgraph/core/state/idempotency.py +59 -0
- chipgraph/core/state/journal.py +238 -0
- chipgraph/core/state/layout.py +82 -0
- chipgraph/core/state/lock.py +195 -0
- chipgraph/core/state/trace.py +225 -0
- chipgraph/learn/__init__.py +96 -0
- chipgraph/learn/draft.py +187 -0
- chipgraph/learn/infer.py +580 -0
- chipgraph/learn/models.py +145 -0
- chipgraph/learn/try_run.py +228 -0
- chipgraph/mcp/__init__.py +12 -0
- chipgraph/mcp/model_tools.py +167 -0
- chipgraph/mcp/server.py +787 -0
- chipgraph/orgs/qnsc/naming-v1.yml +113 -0
- chipgraph/orgs/qnsc/profile.yml +5 -0
- chipgraph/packs/__init__.py +8 -0
- chipgraph/packs/assist/__init__.py +16 -0
- chipgraph/packs/assist/ask/__init__.py +58 -0
- chipgraph/packs/assist/ask/_db.py +57 -0
- chipgraph/packs/assist/ask/_project.py +230 -0
- chipgraph/packs/assist/ask/answer.py +266 -0
- chipgraph/packs/assist/ask/check.py +182 -0
- chipgraph/packs/assist/ask/contract.py +115 -0
- chipgraph/packs/assist/ask/documents.py +157 -0
- chipgraph/packs/assist/ask/fts.py +80 -0
- chipgraph/packs/assist/ask/retrieve.py +331 -0
- chipgraph/packs/assist/audit/__init__.py +383 -0
- chipgraph/packs/assist/pack.yml +13 -0
- chipgraph/packs/assist/triage/__init__.py +64 -0
- chipgraph/packs/assist/triage/contract.py +94 -0
- chipgraph/packs/assist/triage/parse.py +371 -0
- chipgraph/packs/assist/triage/question.py +384 -0
- chipgraph/packs/assist/triage/report.py +160 -0
- chipgraph/packs/assist/triage/rules.py +447 -0
- chipgraph/packs/assist/triage/run.py +224 -0
- chipgraph/packs/digital_rtl/__init__.py +19 -0
- chipgraph/packs/digital_rtl/layout.py +144 -0
- chipgraph/packs/digital_rtl/pack.yml +25 -0
- chipgraph/packs/digital_rtl/plan/__init__.py +34 -0
- chipgraph/packs/digital_rtl/plan/__main__.py +16 -0
- chipgraph/packs/digital_rtl/plan/context.py +82 -0
- chipgraph/packs/digital_rtl/plan/expand.py +94 -0
- chipgraph/packs/digital_rtl/plan/model.py +150 -0
- chipgraph/packs/digital_rtl/plan/resolver.py +400 -0
- chipgraph/packs/digital_rtl/plan/show.py +176 -0
- chipgraph/packs/digital_rtl/pr/__init__.py +11 -0
- chipgraph/packs/digital_rtl/pr/body.py +165 -0
- chipgraph/packs/digital_rtl/pr/gate.py +110 -0
- chipgraph/packs/digital_rtl/pr/open.py +397 -0
- chipgraph/packs/digital_rtl/rtl/__init__.py +9 -0
- chipgraph/packs/digital_rtl/rtl/context.py +272 -0
- chipgraph/packs/digital_rtl/rules/open_pr.yml +30 -0
- chipgraph/packs/digital_rtl/rules/plan.yml +28 -0
- chipgraph/packs/digital_rtl/rules/plan_expand.yml +29 -0
- chipgraph/packs/digital_rtl/rules/pr_gate.yml +27 -0
- chipgraph/packs/digital_rtl/rules/review.yml +31 -0
- chipgraph/packs/digital_rtl/rules/rtl_module.yml +53 -0
- chipgraph/packs/digital_rtl/skills/lang-sv-rtl.md +58 -0
- chipgraph/packs/digital_rtl/skills/org-naming.md +29 -0
- chipgraph/packs/digital_rtl/skills/plan-modules.md +63 -0
- chipgraph/packs/digital_rtl/skills/review-diff.md +41 -0
- chipgraph/packs/digital_rtl/targets/plan.yml +11 -0
- chipgraph/packs/digital_rtl/targets/rtl.yml +20 -0
- chipgraph/packs/digital_rtl/wrapper/__init__.py +24 -0
- chipgraph/packs/digital_rtl/wrapper/common.py +171 -0
- chipgraph/packs/digital_rtl/wrapper/context.py +222 -0
- chipgraph/packs/digital_rtl/wrapper/emacs.py +626 -0
- chipgraph/packs/digital_rtl/wrapper/expansion.py +304 -0
- chipgraph/packs/digital_rtl/wrapper/lisp.py +504 -0
- chipgraph/packs/digital_rtl/wrapper/resolver.py +60 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper.yml +41 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper_gen.yml +35 -0
- chipgraph/packs/digital_rtl/wrapper/skills/wrapper-auto.md +120 -0
- chipgraph/packs/dv/__init__.py +15 -0
- chipgraph/packs/dv/declaration.py +392 -0
- chipgraph/packs/dv/interface.py +596 -0
- chipgraph/packs/dv/pack.yml +14 -0
- chipgraph/packs/dv/rules/tb_module.yml +42 -0
- chipgraph/packs/dv/skills/dv/cocotb.md +72 -0
- chipgraph/packs/spec_core/__init__.py +9 -0
- chipgraph/packs/spec_core/extract/__init__.py +12 -0
- chipgraph/packs/spec_core/extract/_markdown.py +371 -0
- chipgraph/packs/spec_core/extract/_text.py +116 -0
- chipgraph/packs/spec_core/extract/mas.py +975 -0
- chipgraph/packs/spec_core/gen/__init__.py +11 -0
- chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
- chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
- chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
- chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
- chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
- chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
- chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
- chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
- chipgraph/packs/spec_core/pack.yml +6 -0
- chipgraph/py.typed +0 -0
- chipgraph-0.1.0.dist-info/METADATA +53 -0
- chipgraph-0.1.0.dist-info/RECORD +252 -0
- chipgraph-0.1.0.dist-info/WHEEL +4 -0
- chipgraph-0.1.0.dist-info/entry_points.txt +61 -0
- chipgraph-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""`LocalRunner`: runs commands as a direct subprocess on the local machine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextlib
|
|
7
|
+
import os
|
|
8
|
+
import signal
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Mapping, Sequence
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from chipgraph.core.plugin_api.types import RunResult
|
|
15
|
+
|
|
16
|
+
_MISSING_EXECUTABLE_RETURNCODE = 127
|
|
17
|
+
_POSIX = os.name == "posix"
|
|
18
|
+
_KILL_GRACE_S = 5.0
|
|
19
|
+
"""How long, after a timeout's kill, the output pipes may stay open."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _kill_tree(proc: asyncio.subprocess.Process) -> None:
|
|
23
|
+
"""Kill `proc` and, on POSIX, every process of its process group (its children)."""
|
|
24
|
+
if _POSIX:
|
|
25
|
+
try:
|
|
26
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
27
|
+
return
|
|
28
|
+
except (ProcessLookupError, PermissionError):
|
|
29
|
+
pass # the group is gone: kill the child itself, if it is still there
|
|
30
|
+
with contextlib.suppress(ProcessLookupError):
|
|
31
|
+
proc.kill()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LocalRunner:
|
|
35
|
+
"""Runs `cmd` as an argument list via `asyncio.create_subprocess_exec`.
|
|
36
|
+
|
|
37
|
+
Never invokes a shell: `cmd` is always an argument vector. `env` is merged on top
|
|
38
|
+
of the current process environment (`os.environ`). On POSIX the child starts in a
|
|
39
|
+
session (and process group) of its own, so a timeout, or the caller being cancelled,
|
|
40
|
+
kills the child and everything it started: a grandchild that keeps the output pipes
|
|
41
|
+
open cannot block the run after the kill. A timeout returns a `RunResult` with
|
|
42
|
+
`timed_out=True`, keeping whatever output was captured before the kill (if the pipes
|
|
43
|
+
are still open `_KILL_GRACE_S` after it, the output is dropped). A missing executable
|
|
44
|
+
is reported as `returncode=127` rather than raising.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
name = "local"
|
|
48
|
+
|
|
49
|
+
async def run(
|
|
50
|
+
self,
|
|
51
|
+
cmd: Sequence[str],
|
|
52
|
+
*,
|
|
53
|
+
cwd: Path,
|
|
54
|
+
env: Mapping[str, str] | None = None,
|
|
55
|
+
timeout_s: float | None = None,
|
|
56
|
+
) -> RunResult:
|
|
57
|
+
full_env = dict(os.environ)
|
|
58
|
+
if env is not None:
|
|
59
|
+
full_env.update(env)
|
|
60
|
+
|
|
61
|
+
start = time.monotonic()
|
|
62
|
+
extra: dict[str, Any] = {"start_new_session": True} if _POSIX else {}
|
|
63
|
+
try:
|
|
64
|
+
proc = await asyncio.create_subprocess_exec(
|
|
65
|
+
*cmd,
|
|
66
|
+
cwd=str(cwd),
|
|
67
|
+
env=full_env,
|
|
68
|
+
stdout=asyncio.subprocess.PIPE,
|
|
69
|
+
stderr=asyncio.subprocess.PIPE,
|
|
70
|
+
**extra,
|
|
71
|
+
)
|
|
72
|
+
except FileNotFoundError as exc:
|
|
73
|
+
duration_s = time.monotonic() - start
|
|
74
|
+
return RunResult(
|
|
75
|
+
returncode=_MISSING_EXECUTABLE_RETURNCODE,
|
|
76
|
+
stdout="",
|
|
77
|
+
stderr=f"executable not found: {exc}",
|
|
78
|
+
duration_s=duration_s,
|
|
79
|
+
)
|
|
80
|
+
except OSError as exc:
|
|
81
|
+
duration_s = time.monotonic() - start
|
|
82
|
+
return RunResult(
|
|
83
|
+
returncode=_MISSING_EXECUTABLE_RETURNCODE,
|
|
84
|
+
stdout="",
|
|
85
|
+
stderr=f"failed to start command: {exc}",
|
|
86
|
+
duration_s=duration_s,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
timed_out = False
|
|
90
|
+
try:
|
|
91
|
+
if timeout_s is None:
|
|
92
|
+
stdout_bytes, stderr_bytes = await proc.communicate()
|
|
93
|
+
else:
|
|
94
|
+
try:
|
|
95
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
96
|
+
proc.communicate(), timeout=timeout_s
|
|
97
|
+
)
|
|
98
|
+
except TimeoutError:
|
|
99
|
+
timed_out = True
|
|
100
|
+
_kill_tree(proc)
|
|
101
|
+
try:
|
|
102
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
103
|
+
proc.communicate(), timeout=_KILL_GRACE_S
|
|
104
|
+
)
|
|
105
|
+
except TimeoutError:
|
|
106
|
+
await proc.wait()
|
|
107
|
+
stdout_bytes = b""
|
|
108
|
+
stderr_bytes = b"output dropped: its pipes stayed open after the kill"
|
|
109
|
+
except BaseException:
|
|
110
|
+
_kill_tree(proc) # cancelled (e.g. Ctrl-C): leave nothing running
|
|
111
|
+
raise
|
|
112
|
+
finally:
|
|
113
|
+
duration_s = time.monotonic() - start
|
|
114
|
+
|
|
115
|
+
returncode = proc.returncode if proc.returncode is not None else -9
|
|
116
|
+
return RunResult(
|
|
117
|
+
returncode=returncode,
|
|
118
|
+
stdout=stdout_bytes.decode("utf-8", errors="replace"),
|
|
119
|
+
stderr=stderr_bytes.decode("utf-8", errors="replace"),
|
|
120
|
+
duration_s=duration_s,
|
|
121
|
+
timed_out=timed_out,
|
|
122
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Runtime `claude-code` (DESIGN.md 5.5, D35, spike S7).
|
|
2
|
+
|
|
3
|
+
- `runtime.ClaudeCodeRuntime`: the `AgentRuntime` a build uses; it queues agent tasks.
|
|
4
|
+
`runtime.ClaudeCodeExecutor` is the build's executor over it.
|
|
5
|
+
- `loop`: the M2-02a agent rule loop applied to one submitted attempt (label, budget,
|
|
6
|
+
redo instruction, the dispatch bound).
|
|
7
|
+
- `previous`: the outputs of a role without a read tool, moved aside at each dispatch
|
|
8
|
+
(so its `Write` creates a file), shown in its context, and put back if it fails.
|
|
9
|
+
- `service`: what the MCP tools `next_task`, `get_context` and `submit` do.
|
|
10
|
+
- The Claude Code plugin (`plugin/` in the repo) holds the loop command, one subagent
|
|
11
|
+
per role, and the write guard hook.
|
|
12
|
+
|
|
13
|
+
`service` imports `chipgraph.app` (the build, the check runner), so it is not imported
|
|
14
|
+
here: `chipgraph.app.build` imports this package for the runtime.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from chipgraph.adapters.runtime.claude_code.runtime import (
|
|
18
|
+
PLUGIN_NAME,
|
|
19
|
+
RUN_COMMAND,
|
|
20
|
+
ClaudeCodeExecutor,
|
|
21
|
+
ClaudeCodeRuntime,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = ["PLUGIN_NAME", "RUN_COMMAND", "ClaudeCodeExecutor", "ClaudeCodeRuntime"]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""The plugin's role subagents, rendered from the role data (DESIGN.md 5.1, 5.5; D8, D35).
|
|
2
|
+
|
|
3
|
+
Each built-in role's tool table (`chipgraph.core.runtime.roles`) becomes a Claude Code
|
|
4
|
+
subagent file `plugin/agents/<role>.md`: its abstract capabilities mapped to Claude
|
|
5
|
+
Code tools (`CAPABILITY_TOOLS`), its first tier mapped to a model alias, its prompt with
|
|
6
|
+
the tool placeholders filled in. The files are generated; a test fails when a committed
|
|
7
|
+
one differs from the render::
|
|
8
|
+
|
|
9
|
+
python -m chipgraph.adapters.runtime.claude_code.agents --write # regenerate
|
|
10
|
+
python -m chipgraph.adapters.runtime.claude_code.agents --check # exit 1 if stale
|
|
11
|
+
|
|
12
|
+
`asker.md` and `decider.md` stay hand-written: `/chipgraph:ask` is not a role, and
|
|
13
|
+
`decider` serves the `triage` role through `decide()` (its tools and tier are checked
|
|
14
|
+
against that role by a test).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from chipgraph.adapters.runtime.claude_code.runtime import PLUGIN_NAME
|
|
24
|
+
from chipgraph.core.contracts import ModelTier
|
|
25
|
+
from chipgraph.core.runtime.roles import CAPABILITIES, Capability, RoleSpec, get_role
|
|
26
|
+
|
|
27
|
+
MCP_TOOL_PREFIX = f"mcp__plugin_{PLUGIN_NAME}_{PLUGIN_NAME}__"
|
|
28
|
+
"""How Claude Code names the plugin's MCP server tools: `mcp__plugin_<p>_<server>__<t>`."""
|
|
29
|
+
|
|
30
|
+
CAPABILITY_TOOLS: dict[Capability, tuple[str, ...]] = {
|
|
31
|
+
"read_files": ("Read",),
|
|
32
|
+
"search_files": ("Glob", "Grep"),
|
|
33
|
+
"write_outputs": ("Write", "Edit", "MultiEdit"),
|
|
34
|
+
"engine_context": (f"{MCP_TOOL_PREFIX}get_context",),
|
|
35
|
+
"engine_decisions": (f"{MCP_TOOL_PREFIX}pending_decisions",),
|
|
36
|
+
}
|
|
37
|
+
"""The Claude Code tools for each abstract capability. Nothing maps to a shell or to web
|
|
38
|
+
tools: roles get neither."""
|
|
39
|
+
|
|
40
|
+
DEFAULT_TIER_MODELS: dict[ModelTier, str] = {"small": "haiku", "medium": "sonnet", "large": "opus"}
|
|
41
|
+
"""Claude Code model aliases per tier, when the profile's `models.tiers` names none. The
|
|
42
|
+
agent file's `model` is its role's first tier; `next_task` passes the model per attempt."""
|
|
43
|
+
|
|
44
|
+
RENDERED_ROLES = ("author", "tb-author", "rtl-author", "critic", "planner", "researcher")
|
|
45
|
+
"""The roles with a generated subagent file."""
|
|
46
|
+
|
|
47
|
+
HAND_WRITTEN_AGENTS = {"triage": "decider"}
|
|
48
|
+
"""Roles served by a hand-written subagent, and its name."""
|
|
49
|
+
|
|
50
|
+
_GENERATED_NOTE = (
|
|
51
|
+
"# Generated from src/chipgraph/core/runtime/roles/data/{role}.md by\n"
|
|
52
|
+
"# `python -m chipgraph.adapters.runtime.claude_code.agents --write`; do not edit.\n"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def agent_name(role_id: str) -> str:
|
|
57
|
+
"""The subagent file name (without `.md`) for a role: its id, or the hand-written one."""
|
|
58
|
+
short = role_id.rsplit("/", 1)[-1]
|
|
59
|
+
return HAND_WRITTEN_AGENTS.get(short, short)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def agent_type(role_id: str) -> str:
|
|
63
|
+
"""The `subagent_type` for a role: `chipgraph:<agent name>`."""
|
|
64
|
+
return f"{PLUGIN_NAME}:{agent_name(role_id)}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def tools_for(role: RoleSpec) -> tuple[str, ...]:
|
|
68
|
+
"""The Claude Code tools a role's subagent gets, in capability order."""
|
|
69
|
+
return tuple(
|
|
70
|
+
tool
|
|
71
|
+
for capability in CAPABILITIES
|
|
72
|
+
if capability in role.tools
|
|
73
|
+
for tool in CAPABILITY_TOOLS[capability]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _quoted(text: str) -> str:
|
|
78
|
+
"""A single-quoted YAML scalar."""
|
|
79
|
+
return "'" + text.replace("'", "''") + "'"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def render_agent(role: RoleSpec) -> str:
|
|
83
|
+
"""The subagent file for `role`."""
|
|
84
|
+
names: dict[str, tuple[str, ...]] = {c: CAPABILITY_TOOLS[c] for c in role.tools}
|
|
85
|
+
head = (
|
|
86
|
+
"---\n"
|
|
87
|
+
+ _GENERATED_NOTE.format(role=role.id)
|
|
88
|
+
+ f"name: {agent_name(role.id)}\n"
|
|
89
|
+
+ f"description: {_quoted(role.description)}\n"
|
|
90
|
+
+ f"tools: {', '.join(tools_for(role))}\n"
|
|
91
|
+
+ f"model: {DEFAULT_TIER_MODELS[role.default_tier]}\n"
|
|
92
|
+
+ "---\n\n"
|
|
93
|
+
)
|
|
94
|
+
return head + role.render_prompt(names)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def rendered_files() -> dict[str, str]:
|
|
98
|
+
"""File name (`<role>.md`) to content, for every generated subagent file."""
|
|
99
|
+
return {f"{agent_name(r)}.md": render_agent(get_role(r)) for r in RENDERED_ROLES}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _default_agents_dir() -> Path:
|
|
103
|
+
here = Path(__file__).resolve()
|
|
104
|
+
for candidate in here.parents:
|
|
105
|
+
if (candidate / "pyproject.toml").is_file():
|
|
106
|
+
return candidate / "plugin" / "agents"
|
|
107
|
+
raise FileNotFoundError(f"no pyproject.toml found above {here}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def check(agents_dir: Path) -> list[str]:
|
|
111
|
+
"""Generated files in `agents_dir` that are missing or differ from the render."""
|
|
112
|
+
problems = []
|
|
113
|
+
for name, content in rendered_files().items():
|
|
114
|
+
path = agents_dir / name
|
|
115
|
+
if not path.is_file():
|
|
116
|
+
problems.append(f"missing: {path}")
|
|
117
|
+
elif path.read_text(encoding="utf-8") != content:
|
|
118
|
+
problems.append(f"outdated: {path}")
|
|
119
|
+
return problems
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def main(argv: list[str] | None = None) -> int:
|
|
123
|
+
parser = argparse.ArgumentParser(description="Render plugin/agents/ from the role data.")
|
|
124
|
+
mode = parser.add_mutually_exclusive_group(required=True)
|
|
125
|
+
mode.add_argument("--write", action="store_true", help="write the agent files")
|
|
126
|
+
mode.add_argument("--check", action="store_true", help="exit 1 if a file is out of date")
|
|
127
|
+
parser.add_argument("--dir", type=Path, default=None, help="default: plugin/agents/")
|
|
128
|
+
args = parser.parse_args(argv)
|
|
129
|
+
agents_dir: Path = args.dir if args.dir is not None else _default_agents_dir()
|
|
130
|
+
if args.check:
|
|
131
|
+
problems = check(agents_dir)
|
|
132
|
+
for problem in problems:
|
|
133
|
+
print(problem, file=sys.stderr)
|
|
134
|
+
return 1 if problems else 0
|
|
135
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
for name, content in rendered_files().items():
|
|
137
|
+
(agents_dir / name).write_text(content, encoding="utf-8")
|
|
138
|
+
print(f"wrote {len(RENDERED_ROLES)} agent files to {agents_dir}")
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
raise SystemExit(main())
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
__all__ = [
|
|
147
|
+
"CAPABILITY_TOOLS",
|
|
148
|
+
"DEFAULT_TIER_MODELS",
|
|
149
|
+
"HAND_WRITTEN_AGENTS",
|
|
150
|
+
"MCP_TOOL_PREFIX",
|
|
151
|
+
"RENDERED_ROLES",
|
|
152
|
+
"agent_name",
|
|
153
|
+
"agent_type",
|
|
154
|
+
"check",
|
|
155
|
+
"render_agent",
|
|
156
|
+
"rendered_files",
|
|
157
|
+
"tools_for",
|
|
158
|
+
]
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""`decide()` in runtime `claude-code`: questions answered by a small-model subagent.
|
|
2
|
+
|
|
3
|
+
chipgraph never calls a model in this runtime (D35), so `ClaudeCodeDecideBackend.ask`
|
|
4
|
+
does not answer: it records the question under the state backend and returns
|
|
5
|
+
`Deferred`. The plugin then runs the question in the user's Claude Code session::
|
|
6
|
+
|
|
7
|
+
pending_decisions() the MCP tool lists the queued questions, each with the
|
|
8
|
+
tier's model and a prompt, for a `chipgraph:decider`
|
|
9
|
+
subagent started with that model
|
|
10
|
+
answer_decision(id, ...) the MCP tool records the subagent's JSON answer, after
|
|
11
|
+
checking the value is one of the question's choices
|
|
12
|
+
decide(question) again the recorded answer comes back from `ask`; if it is
|
|
13
|
+
under the small threshold, `decide()` asks the large
|
|
14
|
+
tier, which queues the question again with its model
|
|
15
|
+
|
|
16
|
+
State, one file per question (never in the repo tree)::
|
|
17
|
+
|
|
18
|
+
<state>/runtime/decisions/<sha256(question_id)>.json PendingDecision
|
|
19
|
+
|
|
20
|
+
A recorded answer is reused only for the very same question (same fingerprint): a
|
|
21
|
+
question id asked with a new prompt, context or choices starts afresh. A question
|
|
22
|
+
labeled 'nda' is refused (`NdaBlocked`): every model of this runtime is a cloud model.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
from datetime import UTC, datetime
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Literal
|
|
31
|
+
|
|
32
|
+
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field
|
|
33
|
+
|
|
34
|
+
from chipgraph.adapters.llm._errors import NdaBlocked
|
|
35
|
+
from chipgraph.adapters.llm.decide_backend import question_prompt
|
|
36
|
+
from chipgraph.adapters.runtime.claude_code.runtime import PLUGIN_NAME
|
|
37
|
+
from chipgraph.core.config.models import DecideTier, ModelsCfg
|
|
38
|
+
from chipgraph.core.engine.decide import Deferred, ModelAnswer, Question
|
|
39
|
+
from chipgraph.core.runtime import state_key
|
|
40
|
+
from chipgraph.core.state.layout import StateLayout
|
|
41
|
+
|
|
42
|
+
DECIDER_AGENT = f"{PLUGIN_NAME}:decider"
|
|
43
|
+
"""The plugin subagent that answers one question (`plugin/agents/decider.md`)."""
|
|
44
|
+
|
|
45
|
+
DEFAULT_TIER_MODELS: dict[DecideTier, str] = {"small": "haiku", "large": "opus"}
|
|
46
|
+
"""Claude Code model aliases per decide tier, when the profile's `models.tiers` names none
|
|
47
|
+
(the same aliases the task loop uses for these tiers)."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DecisionStateError(Exception):
|
|
51
|
+
"""An unknown question, one not waiting for an answer, or an answer it cannot take."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _now() -> datetime:
|
|
55
|
+
return datetime.now(UTC)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RecordedAnswer(BaseModel):
|
|
59
|
+
"""What the decider subagent answered for one tier."""
|
|
60
|
+
|
|
61
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
62
|
+
|
|
63
|
+
value: str = Field(description="The answer: one of the question's choices.")
|
|
64
|
+
confidence: float = Field(ge=0, le=1, description="The subagent's confidence.")
|
|
65
|
+
reason: str = Field(default="", description="Its short justification.")
|
|
66
|
+
model: str = Field(description="The model the subagent was asked to run on.")
|
|
67
|
+
answered_at: AwareDatetime = Field(default_factory=_now, description="When it answered.")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PendingDecision(BaseModel):
|
|
71
|
+
"""One question `decide()` handed to the Claude Code session, and its answers."""
|
|
72
|
+
|
|
73
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
74
|
+
|
|
75
|
+
schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
|
|
76
|
+
question: Question = Field(description="The question.")
|
|
77
|
+
fingerprint: str = Field(description="`question.fingerprint()` when it was queued.")
|
|
78
|
+
tier: DecideTier = Field(description="The tier last asked: the one pending, if any.")
|
|
79
|
+
model: str = Field(description="The model for `tier`.")
|
|
80
|
+
status: Literal["pending", "answered"] = Field(description="Whether `tier` has answered.")
|
|
81
|
+
answers: dict[DecideTier, RecordedAnswer] = Field(
|
|
82
|
+
default_factory=dict, description="Recorded answers, per tier."
|
|
83
|
+
)
|
|
84
|
+
updated_at: AwareDatetime = Field(default_factory=_now, description="Last change.")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _write_atomic(path: Path, text: str) -> None:
|
|
88
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
90
|
+
tmp.write_text(text, encoding="utf-8")
|
|
91
|
+
os.replace(tmp, path)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DecisionQueue:
|
|
95
|
+
"""Reads and writes the queued questions under a project's state directory."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, layout: StateLayout) -> None:
|
|
98
|
+
self.layout = layout
|
|
99
|
+
self.dir = layout.state_dir / "runtime" / "decisions"
|
|
100
|
+
|
|
101
|
+
def _path(self, question_id: str) -> Path:
|
|
102
|
+
return self.dir / f"{state_key(question_id)}.json"
|
|
103
|
+
|
|
104
|
+
def get(self, question_id: str) -> PendingDecision | None:
|
|
105
|
+
"""The record for `question_id`, or `None` if it was never queued."""
|
|
106
|
+
path = self._path(question_id)
|
|
107
|
+
if not path.is_file():
|
|
108
|
+
return None
|
|
109
|
+
return PendingDecision.model_validate_json(path.read_text(encoding="utf-8"))
|
|
110
|
+
|
|
111
|
+
def all(self) -> list[PendingDecision]:
|
|
112
|
+
"""Every record, sorted by question id."""
|
|
113
|
+
if not self.dir.is_dir():
|
|
114
|
+
return []
|
|
115
|
+
records = [
|
|
116
|
+
PendingDecision.model_validate_json(p.read_text(encoding="utf-8"))
|
|
117
|
+
for p in self.dir.glob("*.json")
|
|
118
|
+
]
|
|
119
|
+
return sorted(records, key=lambda r: r.question.id)
|
|
120
|
+
|
|
121
|
+
def pending(self) -> list[PendingDecision]:
|
|
122
|
+
"""The records waiting for an answer."""
|
|
123
|
+
return [r for r in self.all() if r.status == "pending"]
|
|
124
|
+
|
|
125
|
+
def _put(self, record: PendingDecision) -> PendingDecision:
|
|
126
|
+
record = record.model_copy(update={"updated_at": _now()})
|
|
127
|
+
_write_atomic(self._path(record.question.id), record.model_dump_json(indent=2))
|
|
128
|
+
return record
|
|
129
|
+
|
|
130
|
+
def request(self, question: Question, tier: DecideTier, model: str) -> PendingDecision:
|
|
131
|
+
"""Queue `question` for `tier` (idempotent); a changed question starts afresh."""
|
|
132
|
+
fingerprint = question.fingerprint()
|
|
133
|
+
existing = self.get(question.id)
|
|
134
|
+
answers = existing.answers if existing and existing.fingerprint == fingerprint else {}
|
|
135
|
+
if (
|
|
136
|
+
existing is not None
|
|
137
|
+
and existing.fingerprint == fingerprint
|
|
138
|
+
and existing.status == "pending"
|
|
139
|
+
and existing.tier == tier
|
|
140
|
+
and existing.model == model
|
|
141
|
+
):
|
|
142
|
+
return existing
|
|
143
|
+
return self._put(
|
|
144
|
+
PendingDecision(
|
|
145
|
+
question=question,
|
|
146
|
+
fingerprint=fingerprint,
|
|
147
|
+
tier=tier,
|
|
148
|
+
model=model,
|
|
149
|
+
status="pending",
|
|
150
|
+
answers={t: a for t, a in answers.items() if t != tier},
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def answer(
|
|
155
|
+
self, question_id: str, value: str, confidence: float, reason: str = ""
|
|
156
|
+
) -> PendingDecision:
|
|
157
|
+
"""Record the answer to the pending tier of `question_id`.
|
|
158
|
+
|
|
159
|
+
Raises `DecisionStateError` for an unknown question, one not pending, a value
|
|
160
|
+
that is not one of its choices, or a confidence outside 0..1.
|
|
161
|
+
"""
|
|
162
|
+
record = self.get(question_id)
|
|
163
|
+
if record is None:
|
|
164
|
+
raise DecisionStateError(
|
|
165
|
+
f"no queued question {question_id!r}; pending_decisions lists them"
|
|
166
|
+
)
|
|
167
|
+
if record.status != "pending":
|
|
168
|
+
raise DecisionStateError(
|
|
169
|
+
f"question {question_id!r} is not waiting for an answer (its {record.tier} "
|
|
170
|
+
"tier already answered)"
|
|
171
|
+
)
|
|
172
|
+
choice = record.question.match(value)
|
|
173
|
+
if choice is None:
|
|
174
|
+
raise DecisionStateError(
|
|
175
|
+
f"{value!r} is not a choice of question {question_id!r}; answer one of "
|
|
176
|
+
f"{list(record.question.choices)}"
|
|
177
|
+
)
|
|
178
|
+
if not 0 <= confidence <= 1:
|
|
179
|
+
raise DecisionStateError(f"confidence must be between 0 and 1, got {confidence}")
|
|
180
|
+
answers = dict(record.answers)
|
|
181
|
+
answers[record.tier] = RecordedAnswer(
|
|
182
|
+
value=choice, confidence=confidence, reason=reason, model=record.model
|
|
183
|
+
)
|
|
184
|
+
return self._put(record.model_copy(update={"status": "answered", "answers": answers}))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ClaudeCodeDecideBackend:
|
|
188
|
+
"""A `ModelBackend` for `decide()` that defers each question to the Claude Code session.
|
|
189
|
+
|
|
190
|
+
`models.tiers` (the profile's) may name the model per tier; otherwise `haiku` for
|
|
191
|
+
`small` and `opus` for `large`.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
name = "claude-code"
|
|
195
|
+
|
|
196
|
+
def __init__(self, layout: StateLayout, models: ModelsCfg | None = None) -> None:
|
|
197
|
+
self.queue = DecisionQueue(layout)
|
|
198
|
+
self.models = models if models is not None else ModelsCfg()
|
|
199
|
+
|
|
200
|
+
def model_for(self, tier: DecideTier) -> str:
|
|
201
|
+
"""The subagent model for `tier`."""
|
|
202
|
+
return self.models.tiers.get(tier) or DEFAULT_TIER_MODELS[tier]
|
|
203
|
+
|
|
204
|
+
async def ask(self, question: Question, tier: DecideTier) -> ModelAnswer | Deferred:
|
|
205
|
+
if "nda" in question.labels:
|
|
206
|
+
raise NdaBlocked(
|
|
207
|
+
f"question {question.id!r} is labeled 'nda' and runtime claude-code only "
|
|
208
|
+
"has cloud models; 'nda' data only goes to a self-hosted model"
|
|
209
|
+
)
|
|
210
|
+
record = self.queue.get(question.id)
|
|
211
|
+
if record is not None and record.fingerprint == question.fingerprint():
|
|
212
|
+
answered = record.answers.get(tier)
|
|
213
|
+
if answered is not None:
|
|
214
|
+
return ModelAnswer(
|
|
215
|
+
value=answered.value,
|
|
216
|
+
confidence=answered.confidence,
|
|
217
|
+
model=answered.model,
|
|
218
|
+
reason=answered.reason,
|
|
219
|
+
)
|
|
220
|
+
model = self.model_for(tier)
|
|
221
|
+
self.queue.request(question, tier, model)
|
|
222
|
+
return Deferred(
|
|
223
|
+
question_id=question.id,
|
|
224
|
+
tier=tier,
|
|
225
|
+
model=model,
|
|
226
|
+
message=(
|
|
227
|
+
f"queued for the {DECIDER_AGENT} subagent (model {model}): the plugin "
|
|
228
|
+
"reads it with pending_decisions and records the answer with "
|
|
229
|
+
"answer_decision; then ask again"
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# --- what the MCP tools do ---------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _entry(record: PendingDecision) -> dict[str, Any]:
|
|
238
|
+
question = record.question
|
|
239
|
+
return {
|
|
240
|
+
"question_id": question.id,
|
|
241
|
+
"tier": record.tier,
|
|
242
|
+
"model": record.model,
|
|
243
|
+
"agent": DECIDER_AGENT,
|
|
244
|
+
"description": f"chipgraph decide {question.id}",
|
|
245
|
+
"choices": list(question.choices),
|
|
246
|
+
"prompt": question_prompt(question),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def pending_decisions(layout: StateLayout) -> dict[str, Any]:
|
|
251
|
+
"""The `pending_decisions` MCP tool: every question waiting for the decider."""
|
|
252
|
+
return {"decisions": [_entry(r) for r in DecisionQueue(layout).pending()]}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def answer_decision(
|
|
256
|
+
layout: StateLayout, question_id: str, value: str, confidence: float, reason: str = ""
|
|
257
|
+
) -> dict[str, Any]:
|
|
258
|
+
"""The `answer_decision` MCP tool: record the decider's answer for a pending question."""
|
|
259
|
+
record = DecisionQueue(layout).answer(question_id, value, confidence, reason)
|
|
260
|
+
answer = record.answers[record.tier]
|
|
261
|
+
return {
|
|
262
|
+
"question_id": question_id,
|
|
263
|
+
"tier": record.tier,
|
|
264
|
+
"value": answer.value,
|
|
265
|
+
"confidence": answer.confidence,
|
|
266
|
+
"status": record.status,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
__all__ = [
|
|
271
|
+
"DECIDER_AGENT",
|
|
272
|
+
"DEFAULT_TIER_MODELS",
|
|
273
|
+
"ClaudeCodeDecideBackend",
|
|
274
|
+
"DecisionQueue",
|
|
275
|
+
"DecisionStateError",
|
|
276
|
+
"PendingDecision",
|
|
277
|
+
"RecordedAnswer",
|
|
278
|
+
"answer_decision",
|
|
279
|
+
"pending_decisions",
|
|
280
|
+
]
|