Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
{
"mcpServers": {
"code-intel": {
"command": "code-intel",
"args": [
"serve",
"--mcp",
"--repo",
"code-intel-pipeline"
],
"description": "code-intel: agent query surface. Read-only; gates nowhere. Data source differs per tool — get_gate_verdict / get_facts / get_evidence / get_audit_status project the last committed run; get_change_impact reads the committed import graph but compares it against the live --repo-path and labels the result stale-advisory; plan_structural_edit scans the live checkout and writes nothing. --repo is pinned because a worktree directory name is not the name run commit publishes under; --repo-path defaults to the working directory."
},
"repowise": {
"command": "repowise",
"args": [
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to **code-intel-pipeline** are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **`code-intel serve --mcp`:agent 原生查询面上线,管线在写码中途终于能被问一句话**(#54、#58 提案 3)。stdio MCP server,六个工具:`get_gate_verdict`(权威 run 的门禁结论 + 第一条失败规则 + 最小重跑命令)、`get_facts`(按 artifact type/schema/子串查已验证事实)、`get_evidence`(一条 finding 的证据链:产物、sha256、记录时的 snapshot;查不到明说 `unbacked`,不装真)、`get_audit_status`(各科室结论;没跑过 audit 明说 `unavailable`,不装绿)、`get_change_impact`(默认 stale-advisory——CLI 在这里 fail-closed 正是管线写码时隐形的原因,#58 定为 critical)、`plan_structural_edit`(ast-grep 预览,`repositoryMutation=false`)。**只读,不裁决**:门禁判定照旧只走 CLI 与 CI,查询面被 prompt injection 说服也改不了结论;唯一会执行东西的 `plan_structural_edit` 在跑之前拿注册表核对自己的 capability 声明,一旦出现 `repo_mutation` 直接拒绝(有测试为证)。路径参数复用 `change_impact` / `evidence_query` 的既有请求类型,JSON 进来的路径和 `--changed` 打进来的走同一道越界闸。工具拒答走 `isError` 结果而不是 JSON-RPC error——"还没跑过 run"是答案,不是传输故障。声明的 MCP 版本收窄到 `2025-06-18` 一版:更早的修订要求 JSON-RPC 批量支持,本 transport 一行一条消息,宣称就会让客户端发批量后挂死;走错路的批量收到显式 `-32600` 而不是被静默丢弃。仓库 `.mcp.json` 已注册;README 与 SKILL.md 改为主推查询面,全量扫描降为深检模式,并按工具写清数据来源(四个是已提交 run 的投影,`get_change_impact` 是已提交 import 图 × 当前工作树,`plan_structural_edit` 扫的是当前工作树)。

## [0.7.0-beta.6] — 2026-08-05

这一批的主线是「门禁自己说的话得能被核对」:扫描面、锚点、巨石身份、退出码、schema 五处都从体感口径换成可计算口径——覆盖率不再靠感觉、锚点不再靠猜、巨石不再靠数数、gate finding 不再伪装成崩溃、产物不再违反自家 schema。另一条线是写路径的第一块落地(span 寻址补丁)和文档语言首选项。
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,33 @@ code-intel audit --operation scope --repo C:\path\to\repo --since <git-ref>

## Agent 工作流

### 先接查询面,全量扫描是深检模式

Agent 平时问单点问题走 MCP,不必为一个问题跑一整轮 pipeline:

```powershell
code-intel serve --mcp --repo <name>
```

stdio MCP server,按需起、随 session 生灭。注册进 `.mcp.json` 后 Claude Code / Codex 直连可调。**数据来源逐个工具不同**,读答案时按这一列判它有多新:

| 工具 | 回答什么 | 数据来源 |
|---|---|---|
| `get_gate_verdict` | 权威 run 的门禁结论、第一条失败规则、最小重跑命令 | 已提交 run;附 freshness |
| `get_facts` | 按 artifact type / schema / 子串查已验证事实(热点、import 边、scorecard、符号) | 已提交 run(digest 已校验) |
| `get_evidence` | 一条 finding 的证据链:哪些产物提到它、各自 sha256、记录时的 snapshot | 已提交 run |
| `get_audit_status` | 各科室审计结论、评分、覆盖;没跑过 audit 会明说"不可用"而不是装绿 | 已提交 run |
| `get_change_impact` | 改这些文件会波及谁、该先跑哪些测试 | **已提交 import 图 × 当前 `--repo-path`**;默认标 stale-advisory 并同时给出 recorded/current 两个 snapshot identity |
| `plan_structural_edit` | ast-grep 结构改写预览,只出匹配清单,不落盘 | **当前工作树**(不是已提交 run) |

**这个面只读,不裁决。** 门禁判定照旧只走 CLI 与 CI 路径——查询面被 prompt injection 说服也改不了结论。唯一会执行东西的工具是 `plan_structural_edit`,它在跑之前拿注册表核对自己的 capability 声明,一旦声明里出现 `repo_mutation` 就直接拒绝。

`--repo` 建议显式给:worktree 的目录名不是 `run commit` 发布时用的仓名,不给就会去查错仓的 run。`--repo-path` 默认取工作目录。

全量 `code-intel <path> --mode normal` 留给深检和出证据,不是日常问答的入口。

### 结构门禁

Agent 开始改代码前:

```powershell
Expand Down
33 changes: 33 additions & 0 deletions crates/code-intel-cli/src/change_impact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,39 @@ pub(crate) struct ChangeImpactRequest {
changed: Vec<String>,
}

impl ChangeImpactRequest {
/// Build a request from already-typed values instead of argv.
///
/// `changed` goes through the same `normalize_relative` guard the
/// `--changed` flag uses, so a path arriving as a JSON string over the MCP
/// surface cannot escape the repository by a route the flag parser closes.
/// Reusing the guard is the point; re-stating it here would be the bug.
pub(crate) fn new(
artifact_root: PathBuf,
repo: String,
repo_path: PathBuf,
changed: Vec<String>,
) -> Result<Self, ImpactError> {
let mut changed = changed
.iter()
.map(|path| normalize_relative(path))
.collect::<Result<Vec<_>, _>>()?;
changed.sort();
changed.dedup();
if changed.is_empty() {
return Err(ImpactError::Contract(
"at least one changed path is required".into(),
));
}
Ok(Self {
artifact_root,
repo,
repo_path,
changed,
})
}
}

impl ChangeImpactInvocation {
pub(crate) fn parse(raw: &[String]) -> Result<Self, ImpactError> {
if raw.first().map(String::as_str) != Some("impact") {
Expand Down
6 changes: 4 additions & 2 deletions crates/code-intel-cli/src/cli/command_catalog/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use serde_json::{json, Value};
use crate::{
admissibility, artifact_index, audit_report, change_agenda, change_impact, change_risk,
compatibility_retirement_ticket, decision_port, decision_record, doctor_bootstrap, edit_apply,
edit_impact, evidence_query, invocation_identity, model_channels, ponytail_gate, providers,
repin, run_cli, run_commit, session_evidence, snapshot, survival_scan,
edit_impact, evidence_query, invocation_identity, mcp_serve, model_channels, ponytail_gate,
providers, repin, run_cli, run_commit, session_evidence, snapshot, survival_scan,
};

use super::legacy::{
Expand Down Expand Up @@ -85,6 +85,7 @@ enum CompatibilityRoute {
Decision,
RunExecute,
RunDagCoordinate,
Serve,
Governance,
}

Expand Down Expand Up @@ -450,6 +451,7 @@ fn execute_compatibility(command: CompatibilityCommand) -> i32 {
CompatibilityRoute::RunExecute | CompatibilityRoute::RunDagCoordinate => {
run_cli::run_raw(raw)
}
CompatibilityRoute::Serve => mcp_serve::run_raw(raw),
CompatibilityRoute::Governance => ponytail_gate::run_raw(raw),
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/code-intel-cli/src/cli/command_catalog/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::cli::help_contract::{HELP_ALIASES, HELP_COMMAND};

mod edit_routes;
mod run_routes;
mod serve_routes;
mod types;

pub(super) use types::{CommandRoute, LegacyRoute, RawRoute, VersionRoute};
Expand Down Expand Up @@ -540,6 +541,7 @@ pub(super) const COMMAND_ROUTES: &[CommandRoute] = &[
},
CommandRoute::Raw(run_routes::EXECUTE),
CommandRoute::Raw(run_routes::DAG_COORDINATE),
CommandRoute::Raw(serve_routes::MCP),
raw_route! {
command: "governance",
subcommand: None,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//! The agent-native query surface (#54, #58 proposal 3).
//!
//! Split out of `routes.rs` for the reason `edit_routes` and `run_routes`
//! were: the route table is the file every new command touches, and it already
//! sits just under this repository's own god-file threshold. The seam here is
//! the transport — this is the only route that speaks a protocol rather than
//! argv-in / stdout-out, and the only one whose process lifetime is a client
//! session rather than a single answer.

/// `serve` takes no subcommand so the transport stays a flag: `--mcp` is the
/// only one today, and a future transport should be `serve --http`, not a
/// second route with a duplicated contract. The parser refuses when no
/// transport is named rather than defaulting to one, because "which protocol
/// is this process speaking on stdio" is not a question to guess at.
///
/// `LocalWrite` and `ProcessSpawn` are declared for the single tool that
/// executes anything — `plan_structural_edit` stages an ast-grep preview into
/// a temporary directory. `RepoMutation` is absent and the handler refuses if
/// the registry ever declares it, so the effect set here is the enforced
/// boundary, not a description of intent.
pub(super) const MCP: super::RawRoute = super::RawRoute {
command: "serve",
subcommand: None,
argument_offset: 1,
id: super::CompatibilityRoute::Serve,
contract: super::CommandContract {
stability: super::CommandStability::Public,
controller: super::ControllerOwnership::AgentSession,
authority: super::CommandAuthority::Conditional(
super::AuthorityCondition::CommittedOrStaleAdvisory,
),
effects: &[
super::CommandEffect::RepoRead,
super::CommandEffect::LocalWrite,
super::CommandEffect::ProcessSpawn,
],
output_contract: super::OutputContract::Stdout {
identities: &[
"text-format:mcp-jsonrpc-stream.v1",
"code-intel-mcp-gate-verdict.v1",
"code-intel-mcp-evidence-chain.v1",
"code-intel-mcp-audit-status.v1",
"code-intel-mcp-structural-edit-plan.v1",
"code-intel-mcp-tool-error.v1",
"code-intel-evidence-query.v1",
"code-intel-change-impact.v1",
],
},
exit_contract: super::ExitContract::Exact(&[0, 64, 74]),
retirement_condition:
"retire only through a versioned agent query-surface replacement; the served payloads \
are projections and may be retired individually with their source contracts",
},
};
2 changes: 1 addition & 1 deletion crates/code-intel-cli/src/cli/command_catalog/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ fn unified_route_inventory_owns_version_primary_raw_and_legacy_dispatch() {
.iter()
.filter(|route| matches!(route, CommandRoute::Raw(_)))
.count(),
31
32
);
assert_eq!(
COMMAND_ROUTES
Expand Down
1 change: 1 addition & 0 deletions crates/code-intel-cli/src/cli/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,7 @@ Commands:
run execute --repo <repo-root> --out <run-staging-directory> --authority-root <publication-root> --final-name <name> [--profile default|strict|offline] [--manifest <integrations.json>] [--max-concurrency <n>] [--session-evidence <session-evidence.json>]
run dag-coordinate --repo <repo-root> --out <run-staging-directory> [--manifest <integrations.json>] [--max-concurrency <n>] [--session-evidence <session-evidence.json>]
run commit --source-root <A09-artifact-root> --authority-root <publication-root> --manifest-ref <artifact-ref.json> --final-name <name>
serve --mcp [--repo-path <checkout>] [--repo <name>] [--artifact-root <root>] [--manifest <integrations.json>] (stdio MCP query surface over the committed run; read-only, gates nowhere)
benchmark orientation --out <directory> [--repetitions <2..10>]
benchmark tools --corpus <corpus.json> --runs <runs.json> --artifact-root <directory> --out <directory>
governance ponytail-gate --request <request.json|->
Expand Down
34 changes: 33 additions & 1 deletion crates/code-intel-cli/src/evidence_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use serde_json::{json, Value};
use crate::committed_evidence::{self, CommittedEvidence, EvidenceError};

const DEFAULT_LIMIT: usize = 20;
const MAX_LIMIT: usize = 100;
pub(crate) const MAX_LIMIT: usize = 100;
const PREVIEW_CHARS: usize = 400;

pub(crate) fn run_raw(raw: &[String]) -> i32 {
Expand Down Expand Up @@ -43,6 +43,38 @@ pub(crate) struct EvidenceQueryRequest {
}

impl EvidenceQueryRequest {
/// Build a request from already-typed values instead of argv.
///
/// The MCP query surface receives its filters as JSON, not as flags, but
/// must not therefore get a laxer request: the limit bound checked here is
/// the same `1..=MAX_LIMIT` the flag parser enforces, read from the same
/// constant. A second copy of that range is how the two surfaces would
/// drift.
pub(crate) fn new(
artifact_root: PathBuf,
repo: String,
repo_path: Option<PathBuf>,
artifact_schema: Option<String>,
artifact_type: Option<String>,
contains: Option<String>,
limit: usize,
) -> Result<Self, QueryError> {
if !(1..=MAX_LIMIT).contains(&limit) {
return Err(QueryError::Contract(
"limit must be an integer in 1..=100".into(),
));
}
Ok(Self {
artifact_root,
repo,
repo_path,
artifact_schema,
artifact_type,
contains,
limit,
})
}

pub(crate) fn parse(raw: &[String]) -> Result<Self, QueryError> {
if raw.first().map(String::as_str) != Some("query") {
return Err(QueryError::Contract("usage: artifact query --artifact-root <root> --repo <name> [--repo-path <path>] [--artifact-schema <schema>] [--type <artifact-type>] [--contains <text>] [--limit <1..100>]".into()));
Expand Down
1 change: 1 addition & 0 deletions crates/code-intel-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ mod hospital_score;
mod impact_graph;
mod invocation_identity;
mod language_pref;
mod mcp_serve;
mod method_catalog;
mod model_channels;
mod orchestration;
Expand Down
Loading
Loading