merge: candidate agentctl-cursor-integration Cursor capability layer

This commit is contained in:
WeiCHEN
2026-09-23 19:25:23 +08:00
19 changed files with 738 additions and 1 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"command": ".cursor/hooks/guard-git.sh",
"matcher": "^git ",
"failClosed": true
}
]
}
}
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Fail-closed red-line gate for destructive / unauthorized git. Does not replace agentctl leases.
set -euo pipefail
input=$(cat)
command=$(printf '%s' "$input" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("command") or "")')
ask() {
local user_msg="$1"
local agent_msg="$2"
python3 -c 'import json,sys; print(json.dumps({"permission":"ask","user_message":sys.argv[1],"agent_message":sys.argv[2]}))' "$user_msg" "$agent_msg"
exit 0
}
allow() {
echo '{"permission":"allow"}'
exit 0
}
# Normalize whitespace for matching
cmd=$(printf '%s' "$command" | tr '\n' ' ')
# Unauthorized push (user must confirm)
if printf '%s' "$cmd" | grep -Eq '(^|[[:space:]])git([[:space:]]+-[^[:space:]]+)*[[:space:]]+push([[:space:]]|$)'; then
ask "git push 需要显式用户授权(AGENTS remote_sync)。请确认后再继续。" \
"Hook blocked git push pending user authorization."
fi
# Destructive / force operations
if printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+.*(--force|-f)([[:space:]]|$)' \
|| printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+push[[:space:]].*\+'; then
ask "检测到 git --force / 强推。破坏性操作须用户明确授权。" \
"Hook requires confirmation for force git operation."
fi
if printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+reset[[:space:]]+.*--hard'; then
ask "git reset --hard 会丢弃工作区。须用户明确授权。" \
"Hook requires confirmation for git reset --hard."
fi
if printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+clean[[:space:]]+.*-[a-zA-Z]*f'; then
ask "git clean -f 会删除未跟踪文件。须用户明确授权。" \
"Hook requires confirmation for git clean -f."
fi
if printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+branch[[:space:]]+(-D|--delete[[:space:]]+--force)'; then
ask "强制删分支 (-D) 须用户明确授权;优先 git branch -d。" \
"Hook requires confirmation for git branch -D."
fi
if printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+worktree[[:space:]]+remove[[:space:]]+.*--force'; then
ask "git worktree remove --force 被 AGENTS 禁止升级。须用户明确授权。" \
"Hook requires confirmation for forced worktree remove."
fi
if printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+checkout[[:space:]]+--\.($|[[:space:]])'; then
ask "git checkout -- . 会丢弃全部本地修改。须用户明确授权。" \
"Hook requires confirmation for discarding all local changes."
fi
allow
+8
View File
@@ -0,0 +1,8 @@
{
"mcpServers": {
"agentctl": {
"command": "node",
"args": ["${workspaceFolder}/scripts/mcp/agentctl-mcp.mjs"]
}
}
}
+23
View File
@@ -0,0 +1,23 @@
---
description: Agent-System 写操作必须经 agentctl(租约、docs apply、候选合入);禁止绕过治理直接改共享文档或 push。
alwaysApply: true
---
# agentctl 强制治理
本仓库由 agentctl 管理面协调(SQLite + launchd daemon)。细则见 [AGENTS.md](../../AGENTS.md) 与 [操作规范](../../.doc/ops/2026-09-21-agent-worktree-workflow.md);流程技能见 `.cursor/skills/agentctl-*`。
## 硬边界
1. **写仓库**:必须 `agentctl with` / MCP `lease_run`。裸 `git commit` 会被 agentctl pre-commit hook 拒绝。
2. **共享文档**:`.doc/ops/CURRENT.md`、`.doc/status/FEATURE-MATRIX.md` 只能 `docs apply`(行级 CAS;MATRIX 键为 `record_id`)。
3. **合入 local dev**:`validate` 后 `queue enqueue`(daemon `--ff-only`)或受控 `integrate`;禁止直接改主目录 `dev` tip。
4. **禁止默认**:未授权 `git push`、晋升 test/main、`--force` 清理、卸载 launchd coordinator、改 gap-matrix / 升 MATRIX 状态值冒充发布。
## 会话路径
- 启动 → skill `agentctl-session-start`
- 建场 → `agentctl-worktree-setup`
- 写回 → `agentctl-lease-write`
- 合入/收口 → `agentctl-integrate-closeout`
- MCP 工具定义见 `.cursor/mcp.json` → `scripts/mcp/agentctl-mcp.mjs`
@@ -0,0 +1,53 @@
---
name: agentctl-integrate-closeout
description: Validate a merge candidate, enqueue for daemon ff-only integrate, and close out five-state. Use when ready to land a task on local dev.
---
# agentctl 集成与收口
权威:[操作规范](../../../.doc/ops/2026-09-21-agent-worktree-workflow.md)、[验证契约](../../../.doc/ops/2026-09-21-validation-contract.md)。常驻 daemon 已装载时走 **enqueue**,勿手动 `integrate`(除非 daemon 停)。
## 候选
```bash
D=$(git -C /path/to/Agent-System rev-parse HEAD) # 冻结 expected-dev
ID=integrate-<task>-YYYY-MM-DD
git worktree add -b feature/$ID .worktrees/$ID "$D"
cd .worktrees/$ID
python3 ../../scripts/agentctl.py task register --id "$ID" --owner Cursor --scope . --capability git:dev
python3 ../../scripts/agentctl.py with --task "$ID" \
--resource path:. --resource git:index --resource git:dev -- bash -lc '
git merge --no-ff -m "merge: candidate <feature> into dev" feature/<task>
'
```
第一父必须等于 `$D`。
## validate → enqueue(daemon ff)
```bash
python3 ../../scripts/agentctl.py validate --task "$ID" -- <验收命令>
python3 ../../scripts/agentctl.py queue enqueue \
--task "$ID" --expected-dev "$D" --head "$(git rev-parse HEAD)"
# 在任务 worktree 内 enqueue;数秒内 daemon 合入
python3 ../../scripts/agentctl.py queue list
python3 ../../scripts/agentctl.py daemon status
```
验收按验证契约选行;治理脚本用 `unittest discover`。
## 五态收口
| 字段 | 典型值 |
|---|---|
| review_state | accepted |
| integration_state | merged_local_dev |
| deployment | not_required 或 loaded_local_launchd |
| cleanup | pending(独立维护者;禁止 `--force`) |
| remote_sync | not_requested |
CURRENT 行 → `done`(`docs apply`);`task close --status done`。
## 清理前
确认 tip 为 local_dev 祖先、证据已持久化、无 tracked/untracked 遗留、无进程占用。唯一日志/对话归档进 `.doc/ops/evidence/` 后再 `worktree remove`。
@@ -0,0 +1,45 @@
---
name: agentctl-lease-write
description: Write under agentctl leases and docs apply CAS. Use before any git write, shared-doc edit, or when hitting UNMANAGED_DOCUMENT_CHANGE / EVIDENCE_REQUIRED.
---
# agentctl 租约写回
权威:[AGENTS §3–5](../../../AGENTS.md)、[操作规范](../../../.doc/ops/2026-09-21-agent-worktree-workflow.md)。
## 原则
- **任何仓库写**(含 `git add/commit`)必须在 `agentctl with` / MCP `lease_run` 租约内
- CURRENT / FEATURE-MATRIX **只能** `docs apply`(行级 CAS;MATRIX 键为 `record_id`)
- 直接 `git commit` 会被 agentctl-managed pre-commit hook 拒绝
## lease_run(CLI)
```bash
python3 scripts/agentctl.py with \
--task "$ID" \
--resource path:scripts --resource path:.doc --resource git:index \
-- bash -lc 'git add … && git commit -m "…"'
```
MCP:`lease_run` 的 `command` 为 **argv 数组**(无额外 shell)。
## docs apply(行级)
```bash
python3 scripts/agentctl.py docs snapshot --file .doc/ops/CURRENT.md --record "$RID"
# expected_sha256 = snapshot.sha256;改 replacement 后:
python3 scripts/agentctl.py docs apply --task "$ID" --request /tmp/req.json
```
FEATURE-MATRIX 升 `done` 需要 `docs:approve` + 可核验 evidence(`git cat-file -e`),否则 `EVIDENCE_REQUIRED`。
## 常见拒绝
| 码 | 处置 |
|---|---|
| `UNMANAGED_DOCUMENT_CHANGE` | 保留现场,先 snapshot/reconcile,勿覆盖 |
| `BASE_CHANGED` | 重新 snapshot,基于新 sha 再 apply |
| `AMBIGUOUS_RECORD` | 修重复 record_id / 模块名 |
| `CAPABILITY_REQUIRED` | 任务缺 docs:admin / docs:approve |
| `DOCUMENT_RECEIPT_REQUIRED` | 共享文档须经 apply 收据后再 commit |
@@ -0,0 +1,30 @@
---
name: agentctl-session-start
description: Start an Agent-System session under agentctl governance. Use at session start, when opening a worktree, or before any write to this repo.
---
# agentctl 会话启动
权威规范:[AGENTS.md](../../../AGENTS.md)、[操作规范](../../../.doc/ops/2026-09-21-agent-worktree-workflow.md)、[CURRENT](../../../.doc/ops/CURRENT.md)。
## 步骤
1. 读 `AGENTS.md`、`PROJECT.md`、`.doc/ops/CURRENT.md` 活跃摘要(勿跳过)。
2. 核对管理面:
```bash
python3 scripts/agentctl.py daemon status
# 期望:status=ok;launchd 已装载时 manager.lock.held=true
```
或 MCP 工具 `status`。
3. 若任务已存在:在注册 worktree 内工作,确认 `tasks.state=active`。
4. 若新任务:跟 [agentctl-worktree-setup](../agentctl-worktree-setup/SKILL.md)。
5. 任何仓库写 → [agentctl-lease-write](../agentctl-lease-write/SKILL.md);合入 → [agentctl-integrate-closeout](../agentctl-integrate-closeout/SKILL.md)。
## 禁止
- 主目录直接开发提交;未授权 `git push` / 晋升 test/main
- 绕过 `docs apply` 改 CURRENT / FEATURE-MATRIX
- 卸载或重启 launchd coordinator(除非用户明示)
@@ -0,0 +1,48 @@
---
name: agentctl-worktree-setup
description: Create an agentctl task worktree, register scopes/capabilities, and claim CURRENT. Use when starting a new feature/fix task in Agent-System.
---
# agentctl 建场
权威:[操作规范](../../../.doc/ops/2026-09-21-agent-worktree-workflow.md)。
## 命令序列
```bash
cd /path/to/Agent-System # 主目录(dev)
D=$(git rev-parse HEAD)
ID=<task-id-YYYY-MM-DD>
WT=.worktrees/$ID
git worktree add -b feature/$ID "$WT" "$D"
cd "$WT"
python3 ../../scripts/agentctl.py task register \
--id "$ID" --owner <Owner> \
--scope scripts --scope .doc \
--capability docs:publish
# 需要整文/行级 admin 时再加 --capability docs:admin;证据升 done 加 docs:approve
```
## CURRENT 认领(docs apply)
```bash
python3 ../../scripts/agentctl.py docs snapshot \
--file .doc/ops/CURRENT.md --record "$ID" > /tmp/claim-snap.json
# 新建行:expected_sha256=null
cat > /tmp/claim.json <<EOF
{"request_id":"$ID-claim-1","changes":[{
"file":".doc/ops/CURRENT.md","record_id":"$ID","expected_sha256":null,
"replacement":"| <Owner> | $ID | feature/$ID | $WT | <mutex> | in_progress |"
}]}
EOF
python3 ../../scripts/agentctl.py docs apply --task "$ID" --request /tmp/claim.json
```
## short-term 模板字段
`status`、`write_scope`、`mutex`、`base_dev_sha`、`review_state`/`integration_state`/`deployment`/`cleanup`/`remote_sync`。命名 `.doc/ops/short-term/YYYY-MM-DD-<slug>.md`。
## 注意
- 从确切 `dev` SHA 创建,不从他人 feature 分支启动
- 重叠 path/mutex 只允许一个写者
@@ -0,0 +1,38 @@
---
title: "agentctl Cursor 能力层(skills / MCP / rules / hooks)"
status: active
created: 2026-09-23
updated: 2026-09-23
owner: Cursor
task_id: agentctl-cursor-integration-2026-09-23
---
# agentctl Cursor 能力层
把已上线的 agentctl 管理面包装为 Cursor 原生能力,使 agent 默认走租约 / docs 收据 / 候选合入。
## 能力清单
| 层 | 路径 | 作用 |
|---|---|---|
| MCP | [`.cursor/mcp.json`](../../.cursor/mcp.json) → [`scripts/mcp/agentctl-mcp.mjs`](../../scripts/mcp/agentctl-mcp.mjs) | `status` / `task_*` / `lease_run` / `docs_*` / `validate` / `queue_enqueue` / `quarantine_status` |
| Skills | [`.cursor/skills/agentctl-*`](../../.cursor/skills/) | 会话启动、建场、租约写、集成收口 |
| Rule | [`.cursor/rules/agentctl-mandatory.mdc`](../../.cursor/rules/agentctl-mandatory.mdc) | alwaysApply:强制走 agentctl |
| Hooks | [`.cursor/hooks.json`](../../.cursor/hooks.json) + `guard-git.sh` | `git push` / 破坏性 git → `permission: ask` |
权威规则仍是 [AGENTS.md](../../AGENTS.md) 与 [操作规范](./2026-09-21-agent-worktree-workflow.md);本目录不复制全文。
## 启用
1. 合入 local `dev` 后,在 Cursor 中 **Reload Window** 或重载 MCP。
2. 确认 MCP 服务器 `agentctl` 出现在工具列表;hooks 出现在 Cursor Hooks 设置页。
3. 会话开始跟 skill `agentctl-session-start`(核对 `daemon status`)。
## 与管理面关系
- launchd `com.agentctl.coordinator` 持续 `queue schedule`;本能力层只封装 CLI,不替代 daemon。
- git hooks(pre-commit / pre-merge / reference-transaction)仍由 `agentctl hooks` 管理,与 Cursor `beforeShellExecution` 互补:前者执行租约/候选门禁,后者拦未授权 push/强推。
## 非目标
不 push;不晋升 test/main;不改 gap-matrix;不卸载 daemon。
+1
View File
@@ -18,6 +18,7 @@
| owner | task | branch | worktree | mutex | status |
|---|---|---|---|---|---|
| Cursor | agentctl-cursor-integration-2026-09-23 | feature/agentctl-cursor-integration-2026-09-23 | .worktrees/agentctl-cursor-integration-2026-09-23 | agentctl-cursor-integration | in_progress |
| Cursor | agentctl-mgmt-go-live-close-2026-09-23 | feature/agentctl-mgmt-go-live-close-2026-09-23 | .worktrees/agentctl-mgmt-go-live-close-2026-09-23 | agentctl-mgmt-go-live-close | done |
| Cursor | integrate-agentctl-mgmt-go-live-2026-09-23 | feature/integrate-agentctl-mgmt-go-live-2026-09-23 | .worktrees/integrate-agentctl-mgmt-go-live-2026-09-23 | git:dev | done |
| Cursor | agentctl-mgmt-go-live-2026-09-23 | feature/agentctl-mgmt-go-live-2026-09-23 | .worktrees/agentctl-mgmt-go-live-2026-09-23 | agentctl-mgmt-go-live | done |
@@ -0,0 +1,6 @@
OK status -> allow
OK push -> ask
OK reset -> ask
OK clean -> ask
OK force-wt -> ask
hooks OK
@@ -0,0 +1,20 @@
{
"initialize": {
"name": "agentctl",
"version": "1.0.0"
},
"tools": [
"status",
"task_register",
"task_close",
"lease_run",
"docs_snapshot",
"docs_apply",
"validate",
"queue_enqueue",
"quarantine_status"
],
"status_ok": true,
"daemon_ok": true,
"daemon_held": true
}
@@ -0,0 +1 @@
queue retry after parking untracked promotion receipts (DIRTY_TREE) at /tmp/ci-hold-receipts
@@ -0,0 +1,5 @@
OK agentctl-session-start lines 30
OK agentctl-worktree-setup lines 48
OK agentctl-lease-write lines 45
OK agentctl-integrate-closeout lines 53
skills OK
@@ -0,0 +1,2 @@
146:Ran 134 tests in 122.988s
148:OK (skipped=3)
@@ -0,0 +1,57 @@
---
title: "agentctl Cursor 能力层集成"
task_id: agentctl-cursor-integration-2026-09-23
status: in_progress
created: 2026-09-23
updated: 2026-09-23
owner: Cursor
task_type: implementation
task_tier: single
policy_revision: "agent-governance-2026-09-21-r1 @ 357e4ddb0fe62e12cf6d1321382376257b0fe674"
branch: feature/agentctl-cursor-integration-2026-09-23
worktree: .worktrees/agentctl-cursor-integration-2026-09-23
base_dev_sha: "357e4ddb0fe62e12cf6d1321382376257b0fe674"
write_scope: [".cursor", "scripts", ".doc", ".gitignore"]
allowed_environments: []
mutex: agentctl-cursor-integration
review_state: pending
integration_state: pending
deployment: not_required
cleanup: pending
cleanup_owner: "独立维护者"
cleanup_trigger: "合入 local_dev 且无进程占用后复查"
remote_sync: not_requested
---
# agentctl Cursor 能力层集成
## 用户目标
为 agentctl 规范建 Cursor skills / MCP / rules / hooks,并用本系统管理本项目。
## 范围
- `scripts/mcp/agentctl-mcp.mjs` + `.cursor/mcp.json`
- `.cursor/skills/agentctl-{session-start,worktree-setup,lease-write,integrate-closeout}/`
- `.cursor/rules/agentctl-mandatory.mdc`
- `.cursor/hooks.json` + `guard-git.sh`
- `.gitignore` 放行 skills/hooks/mcp
- `.doc/ops/2026-09-23-agentctl-cursor-integration.md`
## 验收
1. MCP tools/list + status
2. skills/rules frontmatter
3. guard-git:push/reset --hard → ask;status → allow
4. unittest discover 无回归
5. enqueue → daemon 合入
## 收口
| 字段 | 值 |
|---|---|
| review_state | pending |
| integration_state | pending |
| deployment | not_required |
| cleanup | pending |
| remote_sync | not_requested |
+7 -1
View File
@@ -42,10 +42,16 @@ temp/
*.swp
*.swo
*~
# Note: Keep .cursor/rules tracked for team/project rules
# Note: Keep project Cursor governance tracked (rules/skills/hooks/mcp)
.cursor/*
!.cursor/rules/
!.cursor/rules/**
!.cursor/skills/
!.cursor/skills/**
!.cursor/hooks.json
!.cursor/hooks/
!.cursor/hooks/**
!.cursor/mcp.json
# Build artifacts & Binaries
dist/
+12
View File
@@ -0,0 +1,12 @@
# agentctl MCP (stdio)
Zero-dependency Node MCP server wrapping `scripts/agentctl.py`.
```bash
# from repo root
node scripts/mcp/agentctl-mcp.mjs
```
Cursor loads it via [`.cursor/mcp.json`](../../.cursor/mcp.json). Reload MCP after checkout.
`lease_run` / `validate` take `command` as an argv array (no shell). Set `AGENTCTL_REPO` only when the script is not under the repo tree.
+310
View File
@@ -0,0 +1,310 @@
#!/usr/bin/env node
/**
* agentctl MCP (stdio, zero deps). Thin argv wrappers around scripts/agentctl.py.
* lease_run passes command as argv after `--` — no extra shell.
*/
import { spawnSync } from 'node:child_process';
import { createInterface } from 'node:readline';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import fs from 'node:fs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO = process.env.AGENTCTL_REPO
? path.resolve(process.env.AGENTCTL_REPO)
: path.resolve(__dirname, '../..');
const AGENTCTL = path.join(REPO, 'scripts', 'agentctl.py');
function runAgentctl(args, { cwd = REPO, timeout = 120000 } = {}) {
if (!fs.existsSync(AGENTCTL)) {
return { ok: false, code: 127, stdout: '', stderr: `agentctl missing: ${AGENTCTL}` };
}
const r = spawnSync('python3', ['-B', AGENTCTL, ...args], {
cwd,
encoding: 'utf8',
timeout,
maxBuffer: 8 * 1024 * 1024,
env: process.env,
});
return {
ok: r.status === 0,
code: r.status ?? 1,
stdout: r.stdout || '',
stderr: r.stderr || '',
error: r.error ? String(r.error) : undefined,
};
}
function textResult(payload) {
const body = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
return { content: [{ type: 'text', text: body }] };
}
function errResult(msg) {
return { content: [{ type: 'text', text: msg }], isError: true };
}
const TOOLS = [
{
name: 'status',
description: 'agentctl status + daemon status for session start checks',
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
},
{
name: 'task_register',
description: 'Register a task with scopes and optional capabilities',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
owner: { type: 'string' },
scopes: { type: 'array', items: { type: 'string' } },
capabilities: { type: 'array', items: { type: 'string' } },
},
required: ['id', 'owner', 'scopes'],
additionalProperties: false,
},
},
{
name: 'task_close',
description: 'Close a task as done or cancelled',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
status: { type: 'string', enum: ['done', 'cancelled'] },
},
required: ['id'],
additionalProperties: false,
},
},
{
name: 'lease_run',
description: 'Run argv under agentctl with lease (resources). Command is argv array, no shell.',
inputSchema: {
type: 'object',
properties: {
task: { type: 'string' },
resources: { type: 'array', items: { type: 'string' } },
command: { type: 'array', items: { type: 'string' }, minItems: 1 },
cwd: { type: 'string', description: 'optional worktree absolute path' },
},
required: ['task', 'resources', 'command'],
additionalProperties: false,
},
},
{
name: 'docs_snapshot',
description: 'Snapshot a shared doc row for CAS apply',
inputSchema: {
type: 'object',
properties: {
file: { type: 'string' },
record: { type: 'string' },
},
required: ['file', 'record'],
additionalProperties: false,
},
},
{
name: 'docs_apply',
description: 'Apply a docs request JSON file under a task',
inputSchema: {
type: 'object',
properties: {
task: { type: 'string' },
request: { type: 'string', description: 'path to request JSON' },
},
required: ['task', 'request'],
additionalProperties: false,
},
},
{
name: 'validate',
description: 'Validate candidate HEAD with an explicit command argv',
inputSchema: {
type: 'object',
properties: {
task: { type: 'string' },
command: { type: 'array', items: { type: 'string' }, minItems: 1 },
cwd: { type: 'string' },
},
required: ['task', 'command'],
additionalProperties: false,
},
},
{
name: 'queue_enqueue',
description: 'Enqueue a validated candidate for daemon ff-only integrate',
inputSchema: {
type: 'object',
properties: {
task: { type: 'string' },
expected_dev: { type: 'string' },
head: { type: 'string' },
priority: { type: 'number' },
},
required: ['task', 'expected_dev'],
additionalProperties: false,
},
},
{
name: 'quarantine_status',
description: 'List quarantine / suspect runs',
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
},
];
function callTool(name, args = {}) {
switch (name) {
case 'status': {
const a = runAgentctl(['status']);
const b = runAgentctl(['daemon', 'status']);
return textResult({
repo: REPO,
status: { ok: a.ok, code: a.code, stdout: a.stdout.trim(), stderr: a.stderr.trim() },
daemon: { ok: b.ok, code: b.code, stdout: b.stdout.trim(), stderr: b.stderr.trim() },
});
}
case 'task_register': {
const argv = ['task', 'register', '--id', args.id, '--owner', args.owner];
for (const s of args.scopes || []) argv.push('--scope', s);
for (const c of args.capabilities || []) argv.push('--capability', c);
const r = runAgentctl(argv);
return r.ok ? textResult(r.stdout.trim() || r.stderr.trim()) : errResult(r.stderr || r.stdout || r.error);
}
case 'task_close': {
const argv = ['task', 'close', '--id', args.id, '--status', args.status || 'done'];
const r = runAgentctl(argv);
return r.ok ? textResult(r.stdout.trim() || r.stderr.trim()) : errResult(r.stderr || r.stdout || r.error);
}
case 'lease_run': {
if (!Array.isArray(args.command) || args.command.length < 1) {
return errResult('lease_run.command must be a non-empty argv array');
}
const argv = ['with', '--task', args.task];
for (const res of args.resources || []) argv.push('--resource', res);
argv.push('--', ...args.command);
const r = runAgentctl(argv, { cwd: args.cwd || REPO, timeout: 600000 });
const body = [r.stdout, r.stderr].filter(Boolean).join('\n').trim();
return r.ok ? textResult(body) : errResult(body || r.error || `exit ${r.code}`);
}
case 'docs_snapshot': {
const r = runAgentctl(['docs', 'snapshot', '--file', args.file, '--record', args.record]);
return r.ok ? textResult(r.stdout.trim()) : errResult(r.stderr || r.stdout || r.error);
}
case 'docs_apply': {
const r = runAgentctl(['docs', 'apply', '--task', args.task, '--request', args.request]);
return r.ok ? textResult(r.stdout.trim()) : errResult(r.stderr || r.stdout || r.error);
}
case 'validate': {
if (!Array.isArray(args.command) || args.command.length < 1) {
return errResult('validate.command must be a non-empty argv array');
}
const argv = ['validate', '--task', args.task, '--', ...args.command];
const r = runAgentctl(argv, { cwd: args.cwd || REPO, timeout: 600000 });
const body = [r.stdout, r.stderr].filter(Boolean).join('\n').trim();
return r.ok ? textResult(body) : errResult(body || r.error || `exit ${r.code}`);
}
case 'queue_enqueue': {
const argv = ['queue', 'enqueue', '--task', args.task, '--expected-dev', args.expected_dev];
if (args.head) argv.push('--head', args.head);
if (args.priority != null) argv.push('--priority', String(args.priority));
const r = runAgentctl(argv, { cwd: args.cwd || REPO });
return r.ok ? textResult(r.stdout.trim()) : errResult(r.stderr || r.stdout || r.error);
}
case 'quarantine_status': {
const r = runAgentctl(['quarantine', 'list']);
return r.ok ? textResult(r.stdout.trim() || '{"items":[]}') : errResult(r.stderr || r.stdout || r.error);
}
default:
return errResult(`unknown tool: ${name}`);
}
}
function send(msg) {
const body = JSON.stringify(msg);
process.stdout.write(`Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`);
}
function handle(msg) {
if (!msg || typeof msg !== 'object') return;
if (msg.method && msg.id === undefined) {
// notification — ignore
return;
}
const { id, method, params } = msg;
try {
if (method === 'initialize') {
send({
jsonrpc: '2.0',
id,
result: {
protocolVersion: params?.protocolVersion || '2024-11-05',
capabilities: { tools: {} },
serverInfo: { name: 'agentctl', version: '1.0.0' },
},
});
return;
}
if (method === 'tools/list') {
send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
return;
}
if (method === 'tools/call') {
const name = params?.name;
const args = params?.arguments || {};
const result = callTool(name, args);
send({ jsonrpc: '2.0', id, result });
return;
}
if (method === 'ping') {
send({ jsonrpc: '2.0', id, result: {} });
return;
}
send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } });
} catch (e) {
send({ jsonrpc: '2.0', id, error: { code: -32000, message: String(e?.stack || e) } });
}
}
// Support both Content-Length framed MCP and newline-delimited JSON (for tests).
let buf = Buffer.alloc(0);
process.stdin.on('data', (chunk) => {
buf = Buffer.concat([buf, chunk]);
while (true) {
const headerEnd = buf.indexOf('\r\n\r\n');
if (headerEnd !== -1) {
const header = buf.slice(0, headerEnd).toString('utf8');
const m = /Content-Length:\s*(\d+)/i.exec(header);
if (!m) {
buf = buf.slice(headerEnd + 4);
continue;
}
const len = Number(m[1]);
const start = headerEnd + 4;
if (buf.length < start + len) return;
const body = buf.slice(start, start + len).toString('utf8');
buf = buf.slice(start + len);
try {
handle(JSON.parse(body));
} catch (e) {
process.stderr.write(`parse error: ${e}\n`);
}
continue;
}
// newline-delimited fallback for harness
const nl = buf.indexOf('\n');
if (nl === -1) return;
const line = buf.slice(0, nl).toString('utf8').trim();
buf = buf.slice(nl + 1);
if (!line || line.startsWith('Content-Length')) continue;
try {
handle(JSON.parse(line));
} catch (e) {
process.stderr.write(`ndjson parse error: ${e}\n`);
}
}
});
process.stdin.on('end', () => process.exit(0));