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
chipgraph/mcp/server.py
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
"""The MCP server itself (task M0-14): `build_server` wires up an `mcp` 2.x `MCPServer`
|
|
2
|
+
with the five M0 tools, and `run_stdio` runs it over stdio.
|
|
3
|
+
|
|
4
|
+
The `init` tool (M1-16) is meant for a project with no `.chipgraph.yml` yet: it previews,
|
|
5
|
+
and with `confirm`, writes the profile `chipgraph init` would write.
|
|
6
|
+
|
|
7
|
+
Each tool call loads a fresh `chipgraph.app.context.AppContext` from `start` (and
|
|
8
|
+
`profile_path`, if given): a stdio session is long-lived, so a project's
|
|
9
|
+
`.chipgraph.yml` may change between calls, and every tool must see the current one
|
|
10
|
+
(no caching across calls). All the actual work is delegated to the same
|
|
11
|
+
`chipgraph.app` functions `chipgraph.cli` uses, so behavior never drifts between the
|
|
12
|
+
CLI and the MCP surface.
|
|
13
|
+
|
|
14
|
+
`AppError`/`ConfigError` (and `chipgraph.core.engine.gate.GateError`) are caught and
|
|
15
|
+
turned into `mcp.server.mcpserver.exceptions.ToolError`, which the SDK reports back to
|
|
16
|
+
the caller as a tool result with `isError=True` and the message as text -- never an
|
|
17
|
+
unhandled exception that would crash the server loop.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import functools
|
|
24
|
+
import json
|
|
25
|
+
from collections import Counter
|
|
26
|
+
from collections.abc import Awaitable, Callable
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any, Literal
|
|
29
|
+
|
|
30
|
+
from mcp.server.mcpserver import MCPServer
|
|
31
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
32
|
+
|
|
33
|
+
from chipgraph import __version__
|
|
34
|
+
from chipgraph.adapters.runtime.claude_code import decisions as cc_decisions
|
|
35
|
+
from chipgraph.adapters.runtime.claude_code import review as cc_review
|
|
36
|
+
from chipgraph.adapters.runtime.claude_code import service as cc_service
|
|
37
|
+
from chipgraph.app.build import make_scheduler, project_graph
|
|
38
|
+
from chipgraph.app.checks import ProfileCheckRunner
|
|
39
|
+
from chipgraph.app.context import AppContext, default_identity, find_repo_root
|
|
40
|
+
from chipgraph.app.errors import AppError
|
|
41
|
+
from chipgraph.core.config.errors import ConfigError
|
|
42
|
+
from chipgraph.core.contracts import ArtifactRef, RuleInstance
|
|
43
|
+
from chipgraph.core.engine import gate as gate_mod
|
|
44
|
+
from chipgraph.core.engine.graph import BuildGraph, GraphError
|
|
45
|
+
from chipgraph.core.runtime import TaskQueue
|
|
46
|
+
from chipgraph.core.runtime.roles import find_role
|
|
47
|
+
from chipgraph.core.state import journal as journal_mod
|
|
48
|
+
from chipgraph.core.state.layout import StateLayout
|
|
49
|
+
from chipgraph.mcp import model_tools
|
|
50
|
+
|
|
51
|
+
__all__ = ["build_server", "run_stdio"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _latest_run_id(layout: StateLayout) -> str | None:
|
|
55
|
+
"""The id of the most recently created run, or `None` if there has never been one.
|
|
56
|
+
|
|
57
|
+
A minimal re-implementation of `chipgraph.cli._latest_run_id` (private to the CLI
|
|
58
|
+
module): both list `.chipgraph/state/runs` and take the lexicographically last id
|
|
59
|
+
(run ids are time-sortable, see `chipgraph.core.state.layout.new_run_id`).
|
|
60
|
+
"""
|
|
61
|
+
if not layout.runs_dir.is_dir():
|
|
62
|
+
return None
|
|
63
|
+
ids = sorted(p.name for p in layout.runs_dir.iterdir() if p.is_dir())
|
|
64
|
+
return ids[-1] if ids else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _fake_check_instance(check_id: str, block: str | None) -> RuleInstance:
|
|
68
|
+
"""A throwaway `RuleInstance` carrying just the params `ProfileCheckRunner` reads.
|
|
69
|
+
|
|
70
|
+
A minimal re-implementation of `chipgraph.cli._fake_instance` (private to the CLI
|
|
71
|
+
module): `chipgraph check`/the `check` tool run checks directly, outside the build
|
|
72
|
+
graph, so there is no real rule instance to hand the runner.
|
|
73
|
+
"""
|
|
74
|
+
params = {"block": block} if block is not None else {}
|
|
75
|
+
rule_id = "chipgraph/check"
|
|
76
|
+
safe = "".join(c if c.isalnum() else "_" for c in check_id)
|
|
77
|
+
output_path = f".chipgraph/tmp/check-{safe}-{block or 'all'}.json"
|
|
78
|
+
return RuleInstance(
|
|
79
|
+
rule_id=rule_id,
|
|
80
|
+
params=params,
|
|
81
|
+
outputs=(ArtifactRef(kind="report", path=output_path),),
|
|
82
|
+
instance_id=RuleInstance.make_id(rule_id, params),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _gated_instance(graph: BuildGraph, gate_id: str) -> str:
|
|
87
|
+
"""The one instance whose rule's gate is `gate_id` (a re-implementation of
|
|
88
|
+
`chipgraph.cli._gated_instance`); a `ToolError` naming the gates when not exactly one."""
|
|
89
|
+
found: list[str] = []
|
|
90
|
+
known: set[str] = set()
|
|
91
|
+
for iid, inst in graph.instances.items():
|
|
92
|
+
rule = graph.rules[inst.rule_id]
|
|
93
|
+
if not rule.gate:
|
|
94
|
+
continue
|
|
95
|
+
try:
|
|
96
|
+
formatted = gate_mod.gate_id_for(rule.gate, inst.params)
|
|
97
|
+
except gate_mod.GateError:
|
|
98
|
+
formatted = rule.gate
|
|
99
|
+
known.add(formatted)
|
|
100
|
+
if formatted == gate_id:
|
|
101
|
+
found.append(iid)
|
|
102
|
+
if len(found) != 1:
|
|
103
|
+
what = "no instance has" if not found else f"{len(found)} instances have"
|
|
104
|
+
raise ToolError(
|
|
105
|
+
f"{what} gate {gate_id!r}: pass instance; gates: {', '.join(sorted(known)) or 'none'}"
|
|
106
|
+
)
|
|
107
|
+
return found[0]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_SERIOUS = ("blocker", "major")
|
|
111
|
+
"""Review comment severities `status` lists one by one (the others are counted)."""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _read_json(path: Path) -> dict[str, Any] | None:
|
|
115
|
+
try:
|
|
116
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
117
|
+
except (OSError, ValueError):
|
|
118
|
+
return None
|
|
119
|
+
return data if isinstance(data, dict) else None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _last_checks(ctx: AppContext, run_id: str | None, task_id: str) -> list[dict[str, Any]]:
|
|
123
|
+
"""The check results of a task's last submit: the `check_result` events after its
|
|
124
|
+
last dispatch in the journal of the run that dispatched it."""
|
|
125
|
+
if run_id is None or not ctx.layout.journal(run_id).is_file():
|
|
126
|
+
return []
|
|
127
|
+
checks: list[dict[str, Any]] = []
|
|
128
|
+
for event in journal_mod.read(ctx.layout.journal(run_id)).events:
|
|
129
|
+
if event.rule_instance != task_id:
|
|
130
|
+
continue
|
|
131
|
+
if event.type == "agent_turn" and event.payload.get("phase") == "dispatch":
|
|
132
|
+
checks = []
|
|
133
|
+
elif event.type == "check_result":
|
|
134
|
+
checks.append(
|
|
135
|
+
{
|
|
136
|
+
"check_id": event.payload.get("check_id"),
|
|
137
|
+
"status": event.payload.get("status"),
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
return checks
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _block_report(
|
|
144
|
+
ctx: AppContext, graph: BuildGraph, selected: set[str], block: str
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
"""What a person reads of one block of a named target's run: each agent task (status,
|
|
147
|
+
files, checks of its last submit), the Critic's review (verdict, summary, the blocker
|
|
148
|
+
and major comments) and the pull request record (branch, head, URL)."""
|
|
149
|
+
queue = TaskQueue(ctx.layout)
|
|
150
|
+
report: dict[str, Any] = {"tasks": [], "review": None, "pr": None}
|
|
151
|
+
for iid in graph.topo_order():
|
|
152
|
+
if iid not in selected:
|
|
153
|
+
continue
|
|
154
|
+
instance = graph.instances[iid]
|
|
155
|
+
if instance.params.get("block") != block:
|
|
156
|
+
continue
|
|
157
|
+
rule = graph.rules[instance.rule_id]
|
|
158
|
+
out = instance.outputs[0].path if instance.outputs else None
|
|
159
|
+
if rule.kind == "agent":
|
|
160
|
+
record = queue.get(iid)
|
|
161
|
+
if record is not None:
|
|
162
|
+
files = (
|
|
163
|
+
sorted(record.accepted_hashes)
|
|
164
|
+
if record.status == "accepted"
|
|
165
|
+
else list(record.last_result.files_written if record.last_result else ())
|
|
166
|
+
)
|
|
167
|
+
report["tasks"].append(
|
|
168
|
+
{
|
|
169
|
+
"instance": iid,
|
|
170
|
+
"role": record.role,
|
|
171
|
+
"status": record.status,
|
|
172
|
+
"tries_used": record.state.tries_used,
|
|
173
|
+
"tries": record.tries,
|
|
174
|
+
"files": files,
|
|
175
|
+
"checks": _last_checks(ctx, record.run_id, iid),
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
if out is not None and cc_review.is_review_role(find_role(rule.role or "")):
|
|
179
|
+
data = _read_json(ctx.root / out)
|
|
180
|
+
if data is not None:
|
|
181
|
+
comments = [c for c in data.get("comments", []) if isinstance(c, dict)]
|
|
182
|
+
report["review"] = {
|
|
183
|
+
"path": out,
|
|
184
|
+
"verdict": data.get("verdict"),
|
|
185
|
+
"summary": data.get("summary"),
|
|
186
|
+
"comments": [
|
|
187
|
+
{
|
|
188
|
+
k: c.get(k)
|
|
189
|
+
for k in ("severity", "file", "line", "claim", "req_id")
|
|
190
|
+
if c.get(k) is not None
|
|
191
|
+
}
|
|
192
|
+
for c in comments
|
|
193
|
+
if c.get("severity") in _SERIOUS
|
|
194
|
+
],
|
|
195
|
+
"other_comments": sum(c.get("severity") not in _SERIOUS for c in comments),
|
|
196
|
+
}
|
|
197
|
+
elif rule.run is not None and rule.run.use == "open_pr" and out is not None:
|
|
198
|
+
data = _read_json(ctx.root / out)
|
|
199
|
+
if data is not None:
|
|
200
|
+
report["pr"] = {
|
|
201
|
+
k: data.get(k)
|
|
202
|
+
for k in ("gate", "review", "branch", "head_sha", "number", "url", "files")
|
|
203
|
+
}
|
|
204
|
+
return report
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _chain_status(ctx: AppContext, rid: str, states: dict[str, str]) -> dict[str, Any]:
|
|
208
|
+
"""The run's named target (`rtl:<block>`) in build order with each instance's state,
|
|
209
|
+
and a `_block_report` per block of it. Empty when the run's target is not a named
|
|
210
|
+
target or the graph cannot be built now (the same rows as `chipgraph status`)."""
|
|
211
|
+
empty: dict[str, Any] = {"target": None, "chain": [], "blocks": {}}
|
|
212
|
+
if ctx.resolved is None:
|
|
213
|
+
return empty
|
|
214
|
+
try:
|
|
215
|
+
target = journal_mod.read_manifest(ctx.layout, rid).target
|
|
216
|
+
empty["target"] = target
|
|
217
|
+
graph = project_graph(ctx)
|
|
218
|
+
if graph.targets is None or not graph.targets.is_target(target):
|
|
219
|
+
return empty
|
|
220
|
+
selected = graph.select(target)
|
|
221
|
+
except (AppError, GraphError, OSError, ValueError):
|
|
222
|
+
return empty
|
|
223
|
+
chain = [
|
|
224
|
+
{"instance": iid, "state": states.get(iid, "not run")}
|
|
225
|
+
for iid in graph.topo_order()
|
|
226
|
+
if iid in selected
|
|
227
|
+
]
|
|
228
|
+
names = sorted({b for iid in selected if (b := graph.instances[iid].params.get("block"))})
|
|
229
|
+
blocks = {name: _block_report(ctx, graph, selected, name) for name in names}
|
|
230
|
+
return {"target": target, "chain": chain, "blocks": blocks}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _json_safe(value: object) -> object:
|
|
234
|
+
"""Make a `ResolvedProfile.explain()` leaf value JSON-serialisable (tuples -> lists)."""
|
|
235
|
+
if isinstance(value, tuple | list):
|
|
236
|
+
return [_json_safe(item) for item in value]
|
|
237
|
+
if isinstance(value, dict):
|
|
238
|
+
return {str(k): _json_safe(v) for k, v in value.items()}
|
|
239
|
+
return value
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _guard[F: Callable[..., Awaitable[dict[str, Any]]]](func: F) -> F:
|
|
243
|
+
"""Turn `AppError`/`ConfigError`/`GateError` into an MCP `ToolError`.
|
|
244
|
+
|
|
245
|
+
Anything else propagates and the SDK reports it as an unexpected tool error
|
|
246
|
+
(still a tool result, never a crash of the server loop -- see `_handle_call_tool`
|
|
247
|
+
in `mcp.server.mcpserver.server`).
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
@functools.wraps(func)
|
|
251
|
+
async def wrapper(*args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
252
|
+
try:
|
|
253
|
+
return await func(*args, **kwargs)
|
|
254
|
+
except (AppError, ConfigError, gate_mod.GateError) as exc:
|
|
255
|
+
raise ToolError(str(exc)) from exc
|
|
256
|
+
|
|
257
|
+
return wrapper # type: ignore[return-value]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def build_server(start: Path, *, profile_path: Path | None = None) -> MCPServer:
|
|
261
|
+
"""Build the MCP server for the project rooted at (or above) `start`.
|
|
262
|
+
|
|
263
|
+
Registers `status`, `build`, `check`, `approve`, `plan_show`, `config_show` and the
|
|
264
|
+
rest below. `start` and
|
|
265
|
+
`profile_path` are the server's fixed project start directory / profile override
|
|
266
|
+
(mirroring the CLI's `-C/--dir` and `--profile`); everything else about the
|
|
267
|
+
project (the profile, the graph, the state) is re-read from disk on every call.
|
|
268
|
+
"""
|
|
269
|
+
server: MCPServer = MCPServer(name="chipgraph", version=__version__)
|
|
270
|
+
|
|
271
|
+
def _load_ctx() -> AppContext:
|
|
272
|
+
return AppContext.load(start, profile_path=profile_path)
|
|
273
|
+
|
|
274
|
+
@server.tool(
|
|
275
|
+
description=(
|
|
276
|
+
"Show the status of the latest run, or a given run id. For a named target "
|
|
277
|
+
"(rtl:<block>, plan:<block>) also its chain in build order and, per block, the "
|
|
278
|
+
"agent tasks (status, files, checks of the last submit), the Critic's verdict "
|
|
279
|
+
"with its blocker and major comments, and the pull request (branch, URL)."
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
@_guard
|
|
283
|
+
async def status(run_id: str | None = None) -> dict[str, Any]:
|
|
284
|
+
ctx = _load_ctx()
|
|
285
|
+
layout = ctx.layout
|
|
286
|
+
rid = run_id or _latest_run_id(layout)
|
|
287
|
+
if rid is None:
|
|
288
|
+
return {"runs": []}
|
|
289
|
+
|
|
290
|
+
journal_path = layout.journal(rid)
|
|
291
|
+
if not journal_path.is_file():
|
|
292
|
+
raise ToolError(f"no such run {rid!r}")
|
|
293
|
+
journal_read = journal_mod.read(journal_path)
|
|
294
|
+
run_state = journal_mod.replay(journal_read.events)
|
|
295
|
+
|
|
296
|
+
gate_of: dict[str, str] = {}
|
|
297
|
+
for event in journal_read.events:
|
|
298
|
+
if event.type == "gate_wait" and event.rule_instance is not None:
|
|
299
|
+
gate_of[event.rule_instance] = str(event.payload.get("gate", ""))
|
|
300
|
+
waiting = {
|
|
301
|
+
iid: gate_of.get(iid, "") for iid, st in run_state.rules.items() if st == "waiting_gate"
|
|
302
|
+
}
|
|
303
|
+
counts = Counter(run_state.rules.values())
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
"run_id": rid,
|
|
307
|
+
"started": run_state.started,
|
|
308
|
+
"stopped": run_state.stopped,
|
|
309
|
+
"rules": dict(run_state.rules),
|
|
310
|
+
"counts": dict(counts),
|
|
311
|
+
"waiting_gates": waiting,
|
|
312
|
+
**_chain_status(ctx, rid, dict(run_state.rules)),
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@server.tool(description="Build a target with the scheduler and return the run summary.")
|
|
316
|
+
@_guard
|
|
317
|
+
async def build(target: str, concurrency: int = 4) -> dict[str, Any]:
|
|
318
|
+
ctx = _load_ctx()
|
|
319
|
+
ctx.require_profile()
|
|
320
|
+
scheduler = make_scheduler(ctx, target, concurrency=concurrency)
|
|
321
|
+
summary = await scheduler.run(target)
|
|
322
|
+
return summary.model_dump(mode="json")
|
|
323
|
+
|
|
324
|
+
@server.tool(
|
|
325
|
+
description="Run checks directly against the working tree, without the build graph."
|
|
326
|
+
)
|
|
327
|
+
@_guard
|
|
328
|
+
async def check(
|
|
329
|
+
blocks: list[str] | None = None, only: list[str] | None = None
|
|
330
|
+
) -> dict[str, Any]:
|
|
331
|
+
ctx = _load_ctx()
|
|
332
|
+
resolved = ctx.require_profile()
|
|
333
|
+
|
|
334
|
+
check_ids = list(only) if only else sorted(resolved.profile.adapters)
|
|
335
|
+
raw_blocks = list(blocks) if blocks else sorted(resolved.profile.blocks)
|
|
336
|
+
block_list: list[str | None] = list(raw_blocks) if raw_blocks else [None]
|
|
337
|
+
|
|
338
|
+
runner = ProfileCheckRunner(ctx)
|
|
339
|
+
results = []
|
|
340
|
+
for check_id in check_ids:
|
|
341
|
+
for block in block_list:
|
|
342
|
+
instance = _fake_check_instance(check_id, block)
|
|
343
|
+
results.append(await runner.run(check_id, instance))
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
"ok": all(result.ok for result in results),
|
|
347
|
+
"results": [result.model_dump(mode="json") for result in results],
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@server.tool(
|
|
351
|
+
description="Run every deterministic check over the whole project, grouped by layer."
|
|
352
|
+
)
|
|
353
|
+
@_guard
|
|
354
|
+
async def audit(blocks: list[str] | None = None, ingest: bool = True) -> dict[str, Any]:
|
|
355
|
+
from chipgraph.packs.assist.audit import run_audit
|
|
356
|
+
|
|
357
|
+
ctx = _load_ctx()
|
|
358
|
+
ctx.require_profile()
|
|
359
|
+
block_list = list(blocks) if blocks else None
|
|
360
|
+
# `run_audit` is synchronous and drives its own event loop; run it in a worker
|
|
361
|
+
# thread so it does not clash with the MCP server's running loop.
|
|
362
|
+
report = await asyncio.to_thread(run_audit, ctx, ingest=ingest, blocks=block_list)
|
|
363
|
+
return report.model_dump(mode="json")
|
|
364
|
+
|
|
365
|
+
@server.tool(
|
|
366
|
+
description=(
|
|
367
|
+
"Record a person's approve or reject decision for a gate over a rule instance "
|
|
368
|
+
"(instance: optional when one instance has the gate). Only on a person's "
|
|
369
|
+
"explicit answer. `by` defaults to the local user (git user.name); a reject "
|
|
370
|
+
"with a note sends the note to the agent task whose output the gate covers."
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
@_guard
|
|
374
|
+
async def approve(
|
|
375
|
+
gate_id: str,
|
|
376
|
+
instance: str | None = None,
|
|
377
|
+
reject: bool = False,
|
|
378
|
+
by: str | None = None,
|
|
379
|
+
note: str | None = None,
|
|
380
|
+
) -> dict[str, Any]:
|
|
381
|
+
ctx = _load_ctx()
|
|
382
|
+
ctx.require_profile()
|
|
383
|
+
scheduler = make_scheduler(ctx, "*")
|
|
384
|
+
if instance is None:
|
|
385
|
+
instance = _gated_instance(scheduler.graph, gate_id)
|
|
386
|
+
rule_instance = scheduler.graph.instances.get(instance)
|
|
387
|
+
if rule_instance is None:
|
|
388
|
+
raise ToolError(
|
|
389
|
+
f"no such rule instance {instance!r}; known instances: "
|
|
390
|
+
f"{sorted(scheduler.graph.instances)}"
|
|
391
|
+
)
|
|
392
|
+
decision: Literal["approve", "reject"] = "reject" if reject else "approve"
|
|
393
|
+
who = by or default_identity()
|
|
394
|
+
approval = gate_mod.approve(
|
|
395
|
+
ctx.review,
|
|
396
|
+
ctx.store,
|
|
397
|
+
gate_id,
|
|
398
|
+
rule_instance,
|
|
399
|
+
by=who,
|
|
400
|
+
decision=decision,
|
|
401
|
+
note=note or "",
|
|
402
|
+
)
|
|
403
|
+
return approval.model_dump(mode="json")
|
|
404
|
+
|
|
405
|
+
@server.tool(
|
|
406
|
+
description=(
|
|
407
|
+
"A block's plan, for the person who approves gate plan:<block> (the data of "
|
|
408
|
+
"`chipgraph plan show`): gate status and approve command, plan_check errors "
|
|
409
|
+
"and warnings, modules in build order (summary, reqs, dependencies, write "
|
|
410
|
+
"sets, ports), REQ coverage, limits, assumptions, open questions. Read-only."
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
@_guard
|
|
414
|
+
async def plan_show(block: str) -> dict[str, Any]:
|
|
415
|
+
from chipgraph.packs.digital_rtl.plan.show import plan_summary
|
|
416
|
+
|
|
417
|
+
return plan_summary(_load_ctx(), block)
|
|
418
|
+
|
|
419
|
+
@server.tool(description="Show the merged project profile, optionally with each key's source.")
|
|
420
|
+
@_guard
|
|
421
|
+
async def config_show(explain: bool = False) -> dict[str, Any]:
|
|
422
|
+
ctx = _load_ctx()
|
|
423
|
+
resolved = ctx.require_profile()
|
|
424
|
+
if explain:
|
|
425
|
+
return {
|
|
426
|
+
"explain": [
|
|
427
|
+
{
|
|
428
|
+
"key": key,
|
|
429
|
+
"value": _json_safe(value),
|
|
430
|
+
"source": {"kind": source.kind, "location": source.location},
|
|
431
|
+
}
|
|
432
|
+
for key, value, source in resolved.explain()
|
|
433
|
+
]
|
|
434
|
+
}
|
|
435
|
+
return {"profile": resolved.profile.model_dump(mode="json")}
|
|
436
|
+
|
|
437
|
+
# --- runtime claude-code (M1-11): the task loop the plugin command drives ----------
|
|
438
|
+
# One lock for the three tools: the stdio session may run calls concurrently, and
|
|
439
|
+
# each reads and writes the same task queue.
|
|
440
|
+
runtime_lock = asyncio.Lock()
|
|
441
|
+
|
|
442
|
+
@server.tool(
|
|
443
|
+
description=(
|
|
444
|
+
"Run the build and hand out the agent tasks that are ready: tasks (start one "
|
|
445
|
+
"role subagent per task, in parallel), in_progress, and done or waiting."
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
@_guard
|
|
449
|
+
async def next_task(target: str = "*") -> dict[str, Any]:
|
|
450
|
+
async with runtime_lock:
|
|
451
|
+
return await cc_service.next_task(_load_ctx(), target)
|
|
452
|
+
|
|
453
|
+
@server.tool(
|
|
454
|
+
description=(
|
|
455
|
+
"The context of one dispatched task, for its role subagent: inputs as text, "
|
|
456
|
+
"outputs (the only files it may write), role, skills, checks."
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
@_guard
|
|
460
|
+
async def get_context(task_id: str) -> dict[str, Any]:
|
|
461
|
+
async with runtime_lock:
|
|
462
|
+
return await cc_service.get_context(_load_ctx(), task_id)
|
|
463
|
+
|
|
464
|
+
@server.tool(
|
|
465
|
+
description=(
|
|
466
|
+
"Hand a task back: the engine checks that only its outputs changed, that they "
|
|
467
|
+
"exist, and runs the rule's checks; then accepts or rejects it."
|
|
468
|
+
)
|
|
469
|
+
)
|
|
470
|
+
@_guard
|
|
471
|
+
async def submit(task_id: str, result: cc_service.SubmitReport | None = None) -> dict[str, Any]:
|
|
472
|
+
async with runtime_lock:
|
|
473
|
+
return await cc_service.submit(_load_ctx(), task_id, result)
|
|
474
|
+
|
|
475
|
+
# --- decide() in runtime claude-code (M1-12): questions for the decider subagent ---
|
|
476
|
+
|
|
477
|
+
@server.tool(
|
|
478
|
+
description=(
|
|
479
|
+
"Questions decide() queued for a model: for each, start one decider subagent "
|
|
480
|
+
"(agent, model, prompt as given) and pass its JSON answer to answer_decision."
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
@_guard
|
|
484
|
+
async def pending_decisions() -> dict[str, Any]:
|
|
485
|
+
async with runtime_lock:
|
|
486
|
+
return cc_decisions.pending_decisions(_load_ctx().layout)
|
|
487
|
+
|
|
488
|
+
@server.tool(
|
|
489
|
+
description=(
|
|
490
|
+
"Record the decider's answer to a pending question: value (one of its "
|
|
491
|
+
"choices), confidence from 0 to 1, and a short reason."
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
@_guard
|
|
495
|
+
async def answer_decision(
|
|
496
|
+
question_id: str, value: str, confidence: float, reason: str = ""
|
|
497
|
+
) -> dict[str, Any]:
|
|
498
|
+
async with runtime_lock:
|
|
499
|
+
try:
|
|
500
|
+
return cc_decisions.answer_decision(
|
|
501
|
+
_load_ctx().layout, question_id, value, confidence, reason
|
|
502
|
+
)
|
|
503
|
+
except cc_decisions.DecisionStateError as exc:
|
|
504
|
+
raise ToolError(str(exc)) from exc
|
|
505
|
+
|
|
506
|
+
@server.tool(
|
|
507
|
+
description="Retrieve a block and what it contains: modules, ports, registers, interrupts."
|
|
508
|
+
)
|
|
509
|
+
@_guard
|
|
510
|
+
async def model_block(name: str) -> dict[str, Any]:
|
|
511
|
+
ctx = _load_ctx()
|
|
512
|
+
return await model_tools.model_block(ctx, name)
|
|
513
|
+
|
|
514
|
+
@server.tool(description="Retrieve a module: ports, parameters, instances, instantiated_by.")
|
|
515
|
+
@_guard
|
|
516
|
+
async def model_module(name: str) -> dict[str, Any]:
|
|
517
|
+
ctx = _load_ctx()
|
|
518
|
+
return await model_tools.model_module(ctx, name)
|
|
519
|
+
|
|
520
|
+
@server.tool(description="Find entities by kind, name (glob pattern), or attributes.")
|
|
521
|
+
@_guard
|
|
522
|
+
async def model_find(
|
|
523
|
+
kind: str | None = None,
|
|
524
|
+
name: str | None = None,
|
|
525
|
+
limit: int = 100,
|
|
526
|
+
) -> dict[str, Any]:
|
|
527
|
+
ctx = _load_ctx()
|
|
528
|
+
return await model_tools.model_find(ctx, kind=kind, name=name, limit=limit)
|
|
529
|
+
|
|
530
|
+
@server.tool(description="Trace a requirement or entity: implements, verifies, derives_from.")
|
|
531
|
+
@_guard
|
|
532
|
+
async def model_trace(key: str) -> dict[str, Any]:
|
|
533
|
+
ctx = _load_ctx()
|
|
534
|
+
return await model_tools.model_trace(ctx, key)
|
|
535
|
+
|
|
536
|
+
@server.tool(description="Compute downstream impact from a change: reachable entities.")
|
|
537
|
+
@_guard
|
|
538
|
+
async def model_impact(key: str, max_depth: int = 5) -> dict[str, Any]:
|
|
539
|
+
ctx = _load_ctx()
|
|
540
|
+
return await model_tools.model_impact(ctx, key, max_depth=max_depth)
|
|
541
|
+
|
|
542
|
+
@server.tool(description="Find adjacent entities (one-hop neighbors).")
|
|
543
|
+
@_guard
|
|
544
|
+
async def model_neighbors(
|
|
545
|
+
key: str,
|
|
546
|
+
kinds: list[str] | None = None,
|
|
547
|
+
relation: str | None = None,
|
|
548
|
+
direction: str = "out",
|
|
549
|
+
) -> dict[str, Any]:
|
|
550
|
+
ctx = _load_ctx()
|
|
551
|
+
return await model_tools.model_neighbors(
|
|
552
|
+
ctx, key, kinds=kinds, relation=relation, direction=direction
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
@server.tool(description="Full-text search over entities and documents.")
|
|
556
|
+
@_guard
|
|
557
|
+
async def model_search(
|
|
558
|
+
text: str,
|
|
559
|
+
kinds: list[str] | None = None,
|
|
560
|
+
limit: int = 20,
|
|
561
|
+
) -> dict[str, Any]:
|
|
562
|
+
ctx = _load_ctx()
|
|
563
|
+
return await model_tools.model_search(ctx, text, kinds=kinds, limit=limit)
|
|
564
|
+
|
|
565
|
+
# --- /ask (M1-13): retrieval and the citation check the asker subagent uses ----------
|
|
566
|
+
|
|
567
|
+
@server.tool(
|
|
568
|
+
description=(
|
|
569
|
+
"The sources for one /ask question: typed Design Model lookups and full-text "
|
|
570
|
+
"hits in the project's documents, each with the exact citation to use "
|
|
571
|
+
"('model:<key>' or 'path:line'). Answer only from these; none means unknown."
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
@_guard
|
|
575
|
+
async def ask_context(question: str, limit: int = 12) -> dict[str, Any]:
|
|
576
|
+
from chipgraph.packs.assist.ask import ask_context as run_ask_context
|
|
577
|
+
|
|
578
|
+
result = run_ask_context(_load_ctx(), question, limit)
|
|
579
|
+
return result.model_dump(mode="json")
|
|
580
|
+
|
|
581
|
+
@server.tool(
|
|
582
|
+
description=(
|
|
583
|
+
"Check an /ask answer before it is shown: every citation must be an indexed "
|
|
584
|
+
"'path:line' or a 'model:<key>', and an answer that is not unknown needs at "
|
|
585
|
+
"least one. Returns ok and the verified answer, or the reasons to fix."
|
|
586
|
+
)
|
|
587
|
+
)
|
|
588
|
+
@_guard
|
|
589
|
+
async def ask_check(
|
|
590
|
+
answer: str, citations: list[str] | None = None, unknown: bool = False
|
|
591
|
+
) -> dict[str, Any]:
|
|
592
|
+
from chipgraph.packs.assist.ask import AskAnswer
|
|
593
|
+
from chipgraph.packs.assist.ask import ask_check as run_ask_check
|
|
594
|
+
|
|
595
|
+
submitted = AskAnswer(answer=answer, citations=tuple(citations or ()), unknown=unknown)
|
|
596
|
+
return run_ask_check(_load_ctx(), submitted).model_dump(mode="json")
|
|
597
|
+
|
|
598
|
+
# --- /triage (M1-14): classify a failing log; models answer through the decider ----
|
|
599
|
+
|
|
600
|
+
@server.tool(
|
|
601
|
+
description=(
|
|
602
|
+
"Classify a failing lint/sim/check log as infra, rtl, tb or spec, with a "
|
|
603
|
+
"summary, a suggestion and evidence. Give `path` (a log file inside the "
|
|
604
|
+
"project) or `log` (its text), and `check_id` if known. Rules answer first; "
|
|
605
|
+
"otherwise status is 'deferred': answer pending_decisions with the decider "
|
|
606
|
+
"subagent (answer_decision), then call triage again with the same arguments."
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
@_guard
|
|
610
|
+
async def triage(
|
|
611
|
+
path: str | None = None, log: str | None = None, check_id: str | None = None
|
|
612
|
+
) -> dict[str, Any]:
|
|
613
|
+
from chipgraph.packs.assist.triage import triage_log
|
|
614
|
+
|
|
615
|
+
ctx = _load_ctx()
|
|
616
|
+
profile = ctx.require_profile().profile
|
|
617
|
+
if (path is None) == (log is None):
|
|
618
|
+
raise ToolError("give exactly one of `path` (a log file) or `log` (its text)")
|
|
619
|
+
source: str | None = None
|
|
620
|
+
if path is not None:
|
|
621
|
+
root = ctx.root.resolve()
|
|
622
|
+
target = (root / path).resolve()
|
|
623
|
+
if not target.is_relative_to(root):
|
|
624
|
+
raise ToolError(f"{path!r} is outside the project; give a path inside it")
|
|
625
|
+
if not target.is_file():
|
|
626
|
+
raise ToolError(f"no log file {path!r} in the project")
|
|
627
|
+
log = target.read_text(encoding="utf-8", errors="replace")
|
|
628
|
+
source = target.relative_to(root).as_posix()
|
|
629
|
+
assert log is not None
|
|
630
|
+
# In Claude Code the model runs in the user's session (D35): queue the question
|
|
631
|
+
# for the decider subagent, whatever API runtime the profile names.
|
|
632
|
+
backend = cc_decisions.ClaudeCodeDecideBackend(ctx.layout, profile.models)
|
|
633
|
+
async with runtime_lock:
|
|
634
|
+
report = await triage_log(ctx, log, source=source, check_id=check_id, backend=backend)
|
|
635
|
+
return report.model_dump(mode="json")
|
|
636
|
+
|
|
637
|
+
# --- /init-chipgraph (M1-16): `chipgraph init`, previewed before it writes -------
|
|
638
|
+
|
|
639
|
+
@server.tool(
|
|
640
|
+
description=(
|
|
641
|
+
"Set chipgraph up in this project (works with no .chipgraph.yml): returns the "
|
|
642
|
+
".chipgraph.yml `chipgraph init` would write (a stub, or inferred from the repo "
|
|
643
|
+
"with from_learn). Writes it only with confirm=true, and never overwrites an "
|
|
644
|
+
"existing one without force=true. Show the preview to the user first."
|
|
645
|
+
)
|
|
646
|
+
)
|
|
647
|
+
@_guard
|
|
648
|
+
async def init(
|
|
649
|
+
project: str | None = None,
|
|
650
|
+
preset: str | None = None,
|
|
651
|
+
from_learn: bool = False,
|
|
652
|
+
confirm: bool = False,
|
|
653
|
+
force: bool = False,
|
|
654
|
+
) -> dict[str, Any]:
|
|
655
|
+
return await asyncio.to_thread(
|
|
656
|
+
_init_project,
|
|
657
|
+
find_repo_root(start),
|
|
658
|
+
project=project,
|
|
659
|
+
preset=preset,
|
|
660
|
+
from_learn=from_learn,
|
|
661
|
+
confirm=confirm,
|
|
662
|
+
force=force,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
return server
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
_INIT_NEXT_STEPS = (
|
|
669
|
+
"chipgraph config check # validate the profile",
|
|
670
|
+
"chipgraph doctor # check tools are on PATH",
|
|
671
|
+
"chipgraph ingest # build the Design Model (for /chipgraph:ask and /chipgraph:trace)",
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _init_preview(
|
|
676
|
+
root: Path, *, project: str | None, preset: str | None, from_learn: bool
|
|
677
|
+
) -> tuple[str, list[dict[str, str]]]:
|
|
678
|
+
"""The `.chipgraph.yml` text `chipgraph init` would write into `root`, and any other
|
|
679
|
+
file it would write (`--from-learn`'s naming rules), without writing anything.
|
|
680
|
+
|
|
681
|
+
The stub is `chipgraph.cli._init_content`, the CLI's own text. The `--from-learn`
|
|
682
|
+
text is composed from the same `chipgraph.learn` functions `init_from_learn` uses
|
|
683
|
+
(the write itself goes through `init_from_learn`; a test checks the two agree).
|
|
684
|
+
"""
|
|
685
|
+
if not from_learn:
|
|
686
|
+
from chipgraph.cli import _init_content
|
|
687
|
+
|
|
688
|
+
return _init_content(project or root.name, preset), []
|
|
689
|
+
|
|
690
|
+
from chipgraph.learn import (
|
|
691
|
+
_INIT_NAMING_RULES_REL,
|
|
692
|
+
build_naming_rules,
|
|
693
|
+
build_profile_dict,
|
|
694
|
+
dump_naming_rules_yaml,
|
|
695
|
+
learn,
|
|
696
|
+
)
|
|
697
|
+
from chipgraph.learn.draft import dump_yaml
|
|
698
|
+
|
|
699
|
+
result = learn(root)
|
|
700
|
+
rules = build_naming_rules(result)
|
|
701
|
+
others: list[dict[str, str]] = []
|
|
702
|
+
naming_ref: str | None = None
|
|
703
|
+
if rules is not None:
|
|
704
|
+
naming_ref = _INIT_NAMING_RULES_REL
|
|
705
|
+
others.append({"path": naming_ref, "content": dump_naming_rules_yaml(rules)})
|
|
706
|
+
header = (
|
|
707
|
+
"# chipgraph project profile written by `chipgraph init --from-learn` (DESIGN.md 8.6).\n"
|
|
708
|
+
"# Review it: each rule was inferred from the repo. Edit freely.\n"
|
|
709
|
+
)
|
|
710
|
+
data = build_profile_dict(result, naming_rules_ref=naming_ref)
|
|
711
|
+
return header + dump_yaml(data), others
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _init_project(
|
|
715
|
+
root: Path,
|
|
716
|
+
*,
|
|
717
|
+
project: str | None,
|
|
718
|
+
preset: str | None,
|
|
719
|
+
from_learn: bool,
|
|
720
|
+
confirm: bool,
|
|
721
|
+
force: bool,
|
|
722
|
+
) -> dict[str, Any]:
|
|
723
|
+
"""The `init` tool: preview `chipgraph init`, and with `confirm`, do it.
|
|
724
|
+
|
|
725
|
+
Mirrors the CLI command: refuses to overwrite an existing `.chipgraph.yml` unless
|
|
726
|
+
`force`; `from_learn` infers the profile (and naming rules) from the repo; otherwise
|
|
727
|
+
it writes the stub (`project` defaults to the root directory's name, `preset` adds
|
|
728
|
+
`extends`). `preset` and `project` are ignored with `from_learn`, as in the CLI.
|
|
729
|
+
"""
|
|
730
|
+
profile = root / ".chipgraph.yml"
|
|
731
|
+
exists = profile.is_file()
|
|
732
|
+
content, others = _init_preview(root, project=project, preset=preset, from_learn=from_learn)
|
|
733
|
+
reply: dict[str, Any] = {
|
|
734
|
+
"path": ".chipgraph.yml",
|
|
735
|
+
"exists": exists,
|
|
736
|
+
"written": False,
|
|
737
|
+
"content": content,
|
|
738
|
+
"other_files": others,
|
|
739
|
+
}
|
|
740
|
+
if exists and not force:
|
|
741
|
+
if confirm:
|
|
742
|
+
raise ToolError(".chipgraph.yml already exists; pass force=true to overwrite it")
|
|
743
|
+
reply["message"] = (
|
|
744
|
+
".chipgraph.yml already exists: nothing will be written unless force=true "
|
|
745
|
+
"(which overwrites it)."
|
|
746
|
+
)
|
|
747
|
+
return reply
|
|
748
|
+
if not confirm:
|
|
749
|
+
verb = "overwrite" if exists else "write"
|
|
750
|
+
reply["message"] = (
|
|
751
|
+
f"Preview only: nothing was written. Call init again with confirm=true to {verb} "
|
|
752
|
+
"these files."
|
|
753
|
+
)
|
|
754
|
+
return reply
|
|
755
|
+
|
|
756
|
+
if from_learn:
|
|
757
|
+
from chipgraph.learn import InitFromLearnError, init_from_learn
|
|
758
|
+
|
|
759
|
+
old = profile.read_bytes() if exists else None
|
|
760
|
+
if exists:
|
|
761
|
+
profile.unlink()
|
|
762
|
+
try:
|
|
763
|
+
_, naming_path = init_from_learn(root)
|
|
764
|
+
except Exception as exc:
|
|
765
|
+
if old is not None and not profile.exists():
|
|
766
|
+
profile.write_bytes(old) # keep the user's file if the write failed
|
|
767
|
+
if isinstance(exc, InitFromLearnError):
|
|
768
|
+
raise ToolError(str(exc)) from exc
|
|
769
|
+
raise
|
|
770
|
+
written = [".chipgraph.yml"]
|
|
771
|
+
if naming_path is not None:
|
|
772
|
+
written.append(naming_path.relative_to(root).as_posix())
|
|
773
|
+
reply["content"] = profile.read_text(encoding="utf-8")
|
|
774
|
+
else:
|
|
775
|
+
profile.write_text(content, encoding="utf-8")
|
|
776
|
+
written = [".chipgraph.yml"]
|
|
777
|
+
reply["written"] = True
|
|
778
|
+
reply["files_written"] = written
|
|
779
|
+
reply["message"] = f"wrote {', '.join(written)}; review it, then run the next steps."
|
|
780
|
+
reply["next_steps"] = list(_INIT_NEXT_STEPS)
|
|
781
|
+
return reply
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
async def run_stdio(start: Path, profile_path: Path | None = None) -> None:
|
|
785
|
+
"""Run the MCP server over stdio for the project rooted at (or above) `start`."""
|
|
786
|
+
server = build_server(start, profile_path=profile_path)
|
|
787
|
+
await server.run_stdio_async()
|