Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
451512d
feat(coordination): T-ACN-016 Agent Reporter, Governed Launcher, Host…
Kucell Jul 29, 2026
46ff2db
fix(coordination): T-ACN-016 修复 — 桥接枚举化、Agent Reporter 治理约束、Governed …
Kucell Jul 29, 2026
01aefe3
fix(coordination): 强制受治理启动上下文
Kucell Jul 29, 2026
f4ee533
fix(coordination): 完成受治理启动闭环
Kucell Jul 29, 2026
75d001e
fix(coordination): 加固启动失败审计与 agentCommand 安全校验
Kucell Jul 29, 2026
4695e0b
fix(coordination): 收紧派发失败授权边界
Kucell Jul 29, 2026
1ea13c6
feat(coordination): T-ACN-017 Claude Code Hook Adapter — SessionStart…
Kucell Jul 29, 2026
157d477
fix(coordination): T-ACN-017 Claude Hook Adapter repair — split modul…
Kucell Jul 29, 2026
900beff
fix(coordination): T-ACN-017-R2 Claude Hook Adapter repair — bridge, …
Kucell Jul 29, 2026
f9629c8
fix(coordination): T-ACN-017-R4 Claude Hook Adapter — public CLI, Age…
Kucell Jul 30, 2026
54eafe4
feat(coordination): add governed agent launch CLI
Kucell Jul 30, 2026
778410f
feat(coordination): allow governed launch agent args
Kucell Jul 30, 2026
09816ca
fix(coordination): expose fenced lease CLI
Kucell Jul 30, 2026
199572e
fix(coordination): bind launch context to agent session
Kucell Jul 30, 2026
747b900
fix(coordination): bind reporter delivery target
Kucell Jul 30, 2026
f77dfb5
fix(coordination): launch agents in governed worktree
Kucell Jul 30, 2026
7ce1b01
feat(claude): install native coordination hooks
Kucell Jul 30, 2026
ab57ae1
fix(claude): deliver headless hook events synchronously
Kucell Jul 30, 2026
f2e1613
fix(coordination): report governed child exit outcomes
Kucell Jul 30, 2026
1cb4a5a
fix(coordination): block timed out governed agents
Kucell Jul 30, 2026
9e88534
fix(coordination): release monitor journal lock
Kucell Jul 30, 2026
185fe7c
fix(coordination): reclaim locks from dead owners
Kucell Jul 30, 2026
4fc7bc9
fix(coordination): preserve watchdog block outcome
Kucell Jul 30, 2026
e034ef5
fix(coordination): preserve monitor timeout receipts
Kucell Jul 30, 2026
88917f2
fix(coordination): notify on governed child recovery
Kucell Jul 30, 2026
c4c43d3
feat(coordination): configure governed agent timeout
Kucell Jul 30, 2026
2736ea0
fix(coordination): complete governed child lifecycle
Kucell Jul 31, 2026
7a00955
chore(release): v1.9.0
Kucell Jul 31, 2026
fe14f32
fix(team-pack): reject invalid manifests before apply
Kucell Jul 31, 2026
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
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
},
"metadata": {
"description": "AI Agent Governance Framework for Claude Code, Cursor, Windsurf, Gemini CLI and more. Run /cortex-setup after installation to complete project initialization.",
"version": "1.8.0"
"version": "1.9.0"
},
"plugins": [
{
"name": "cortex-agent",
"source": "./",
"description": "Governance framework for AI coding assistants. Provides slash-command workflows (/arch-design, /ship, /parallel…), specialized sub-agents (planner, implementer, code-reviewer…), and 9 reusable skills. Run /cortex-setup after install for full project setup.",
"version": "1.8.0",
"version": "1.9.0",
"author": {
"name": "Kucell"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cortex-agent",
"version": "1.8.0",
"version": "1.9.0",
"description": "AI Agent Governance Framework — structured Rules, Workflows, Skills, Sub-agents, and Hooks for Cursor, Claude Code, Windsurf and more.",
"author": {
"name": "Kucell",
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.9.0] - 2026-07-31

### Added

- **Claude Code Release A 自动闭环**:新增 Agent Reporter、Governed Launcher、
Claude Hooks Adapter 与受治理 launch context。Codex 可派发一个或多个真实
Claude Code Agent,并通过 Coordination Journal、Notification Pump 和官方
Codex App Server 在原主对话接收进展、阻塞、失败和待审核事件。
- **受治理子进程生命周期**:新增 fenced lease 周期续租、journal-only
heartbeat、显式 handoff 终态保护、异常/超时恢复、最终 lease release 和脱敏
child receipt。
- **启动持久化握手**:Launcher 在 `task.accepted` 持久化后才允许 monitor
访问 Journal,避免 launcher/monitor 并发写入造成 hash-chain 竞争。
- 新增 `cortex-agent secrets <store|verify|list|audit>` 公共命令,通过项目
Secrets skill 使用 macOS Keychain 等后端;`store` 仅接受
`--from-env`,`verify --provider npm` 只返回认证身份,不输出凭证。
Expand All @@ -17,6 +28,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
零写入 dispatch dry-run,以及显式受治理的人工 dispatch;自动 dispatch、
daemon 和 trigger 仍保持关闭。

### Changed

- Claude Code 项目设置可安装原生协调 Hooks;headless Hook 事件同步写入
Reporter,`Stop` 和进程退出码 0 不推断任务完成。
- Agent Reporter 的 ownership、identity、project 和 notification target 只从
私有受治理上下文和 Task 快照取得,Agent 参数不能覆盖治理字段。

### Security

- Governed Launcher 只允许显式白名单中的绝对可执行 Host,拒绝相对路径、
隐式 Node fallback、未知参数和原始 JSON 事件。
- Receipt 和通知不保存 prompt、command、文件正文、私有路径、session、凭据、
Hook payload 或精确 token;关键事件保持 pending,绝不自动 ACK。
- Release A 聚焦回归 272/272 PASS;真实双 Claude Agent 的 Task、lease、receipt
与 Codex thread wakeup 均通过,独立简单对话消息已由原主对话实际接收。

## [1.8.0] - 2026-07-29

### Added
Expand Down
12 changes: 12 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ const {
inbox,
waitpoints,
coordination,
lease,
notification,
mcp,
agent,
hook,
managementQuery,
phaseZeroAutomation,
dashboard,
Expand Down Expand Up @@ -109,6 +112,12 @@ for (let i = 0; i < args.length; i++) {
if (arg === "--strict") {
options.strict = true;
}
if (arg === "--stdin") {
const value = args[i + 1];
options.stdin = value && !value.startsWith("--") ? value : "";
} else if (arg && arg.startsWith("--stdin=")) {
options.stdin = arg.slice("--stdin=".length);
}
}

function detectLangFromProject(dir) {
Expand Down Expand Up @@ -165,6 +174,7 @@ const l1Ctx = options.project
case "waitpoints": waitpoints(ctx); break;
case "task":
case "event": coordination(ctx); break;
case "lease": lease(ctx); break;
case "notification": await notification(ctx); break;
case "mcp": await mcp(ctx); break;
case "query": managementQuery(ctx); break;
Expand All @@ -174,6 +184,8 @@ const l1Ctx = options.project
case "dashboard": dashboard(ctx); break;
case "team": await teamPack(ctx); break;
case "secrets": secrets(l1Ctx); break;
case "agent": agent(ctx); break;
case "hook": hook(ctx); break;
case "help": args.includes("--json") ? cliHelp(ctx) : printHelp(); break;
case "dev": await dev(ctx); break;
case undefined:
Expand Down
265 changes: 265 additions & 0 deletions bin/cortex-claude-hook
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
#!/usr/bin/env node
"use strict";

// ─── Cortex Claude Code Hook Executable (T-ACN-017-R4) ──────────────────────
//
// Standalone entrypoint for Claude Code hooks. Bridges hook events to the
// Coordination Application Service via the Agent Reporter (never direct
// createEvent/submit). Derives identity exclusively from CORTEX_LAUNCH_CONTEXT.
// Stdin governance fields and unknown fields are rejected. Receipts are
// redacted per P-003 §11.1 / §13.5.
//
// CLI: cortex-claude-hook <hook-name> < bounded-stdin.json
// Exit: 0 = ok, 1 = user error, 2 = internal error
//
// Safety contract:
// - Identity from CORTEX_LAUNCH_CONTEXT only
// - Stdin ≤ 64 KiB, governance fields rejected, unknown fields rejected
// - SessionStart: validates context, does NOT submit event (launcher authoritative)
// - Stop/SubagentStop: nonterminal, never submit events
// - Receipt: only ok/code/eventType/emitted/timestamp; never prompt/session/path/command/credentials
// - Zero external deps beyond Node.js built-ins and project modules

const fs = require("node:fs");
const path = require("node:path");

// ─── Constants ───────────────────────────────────────────────────────────────

const MAX_STDIN_BYTES = 64 * 1024;

const GOVERNANCE_FIELDS = new Set([
"taskId", "projectId", "actorId", "kind", "sessionId",
"correlationId", "coordinatorId", "launchId",
"targets", "repository", "sequence", "workflowGate",
"notificationPolicy", "producer",
]);

// Hook-specific allowed stdin fields — imported from handlers module
const { HOOK_ALLOWED_STDIN_FIELDS } = require("../lib/coordination/claude-hook-handlers");

// ─── Stdin reader ────────────────────────────────────────────────────────────

function readStdin() {
return new Promise((resolve, reject) => {
const chunks = [];
let total = 0;
process.stdin.on("data", (chunk) => {
total += chunk.length;
if (total > MAX_STDIN_BYTES) {
reject(new Error("Stdin exceeds maximum size"));
process.stdin.destroy();
return;
}
chunks.push(chunk);
});
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
process.stdin.on("error", reject);
});
}

// ─── Governance field rejector ───────────────────────────────────────────────

function rejectGovernanceFields(payload) {
if (!payload || typeof payload !== "object") return { safe: payload, rejected: [] };
const rejected = [];
const safe = {};
for (const [key, value] of Object.entries(payload)) {
if (GOVERNANCE_FIELDS.has(key)) rejected.push(key);
else safe[key] = value;
}
return { safe, rejected };
}

// ─── Hook-specific schema validator ──────────────────────────────────────────
// Rejects fields not in the allowlist for this hook type.

function validateHookSchema(hookName, payload) {
const allowed = HOOK_ALLOWED_STDIN_FIELDS[hookName];
if (!allowed) return { safe: payload, rejected: [] };
if (!payload || typeof payload !== "object") return { safe: payload, rejected: [] };
const rejected = [];
const safe = {};
for (const [key, value] of Object.entries(payload)) {
if (allowed.includes(key)) safe[key] = value;
else rejected.push(key);
}
return { safe, rejected };
}

// ─── Project root resolution ─────────────────────────────────────────────────
// 1. From CORTEX_LAUNCH_CONTEXT: walk up from the context file's directory
// looking for .agent/
// 2. Fallback: use cwd and verify it contains .agent/

function findProjectRoot() {
const contextFile = process.env.CORTEX_LAUNCH_CONTEXT;
if (contextFile && typeof contextFile === "string" && contextFile.length > 0) {
let dir = path.dirname(path.resolve(contextFile));
for (let i = 0; i < 10; i++) {
if (fs.existsSync(path.join(dir, ".agent"))) return dir;
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
}
const cwd = process.cwd();
if (fs.existsSync(path.join(cwd, ".agent"))) return cwd;
return cwd;
}

// ─── Coordination service resolution ─────────────────────────────────────────

function resolveCoordinationService(projectRoot) {
const runtimeDir = path.join(projectRoot, ".agent", "runtime", "coordination");
const { CoordinationApplicationService } = require("../lib/coordination/application-service");
return CoordinationApplicationService.open(runtimeDir, { journal: { lock: false } });
}

// ─── Build error receipt ────────────────────────────────────────────────────
// Per P-003 §11.1 / §13.5: only ok, eventType, code, timestamp.
// NEVER prompt, session, path, command, payload, token, or credentials.

function buildErrorReceipt(ok, code, eventType) {
const receipt = { ok, code, timestamp: new Date().toISOString() };
if (eventType !== undefined && eventType !== null) {
receipt.eventType = eventType;
}
return receipt;
}

// ─── Main ────────────────────────────────────────────────────────────────────

async function main() {
const hookName = process.argv[2];
if (!hookName || typeof hookName !== "string") {
process.stdout.write(JSON.stringify(buildErrorReceipt(false, "ERR_HOOK_NAME_REQUIRED")) + "\n");
process.exit(1);
}

const KNOWN_HOOKS = ["SessionStart", "PostToolUse", "TestStart", "Notification", "Permission", "ReadyForReview", "Stop", "SubagentStop"];
if (!KNOWN_HOOKS.includes(hookName)) {
process.stdout.write(JSON.stringify(buildErrorReceipt(false, "ERR_UNKNOWN_HOOK")) + "\n");
process.exit(1);
}

// Read bounded stdin
let rawPayload = {};
try {
const stdinText = await readStdin();
if (stdinText.trim().length > 0) {
rawPayload = JSON.parse(stdinText);
}
} catch (err) {
process.stdout.write(JSON.stringify(buildErrorReceipt(false, "ERR_STDIN_INVALID")) + "\n");
process.exit(1);
}

// Reject governance fields in stdin
const { safe: noGovernance, rejected: govRejected } = rejectGovernanceFields(rawPayload);
if (govRejected.length > 0) {
process.stdout.write(JSON.stringify(buildErrorReceipt(false, "ERR_GOVERNANCE_FIELD_REJECTED")) + "\n");
process.exit(1);
}

// Validate hook-specific schema (reject unknown fields)
const { safe: validatedPayload, rejected: unknownRejected } = validateHookSchema(hookName, noGovernance);
if (unknownRejected.length > 0) {
process.stdout.write(JSON.stringify(buildErrorReceipt(false, "ERR_UNKNOWN_FIELD_REJECTED")) + "\n");
process.exit(1);
}

// ─── Stop / SubagentStop — no context or service required ─────────────────
// Nonterminal events. Never submit to the Journal.

if (hookName === "Stop") {
process.stdout.write(JSON.stringify({
ok: true, code: "STOP_RECORDED", eventType: null, emitted: false,
timestamp: new Date().toISOString(),
}) + "\n");
process.exit(0);
}

if (hookName === "SubagentStop") {
process.stdout.write(JSON.stringify({
ok: true, code: "SUBAGENT_STOP_RECORDED", eventType: null, emitted: false,
timestamp: new Date().toISOString(),
}) + "\n");
process.exit(0);
}

// ─── SessionStart — validate context, no event ───────────────────────────
// Validates CORTEX_LAUNCH_CONTEXT. Does NOT submit task.accepted — the
// launcher is authoritative. The hook is a validation gate only.

if (hookName === "SessionStart") {
const contextFile = process.env.CORTEX_LAUNCH_CONTEXT;
if (!contextFile || typeof contextFile !== "string" || contextFile.length === 0) {
process.stdout.write(JSON.stringify(buildErrorReceipt(false, "ERR_NO_GOVERNED_CONTEXT", "task.accepted")) + "\n");
process.exit(1);
}
let contextOk = false;
try {
const stat = fs.statSync(contextFile);
if (!(stat.mode & 0o077)) {
const content = fs.readFileSync(contextFile, "utf8");
const context = JSON.parse(content);
if (context && context.taskId && context.projectId) contextOk = true;
}
} catch { /* fail closed */ }
if (!contextOk) {
process.stdout.write(JSON.stringify(buildErrorReceipt(false, "ERR_CONTEXT_INVALID", "task.accepted")) + "\n");
process.exit(1);
}
// Launcher is authoritative — no event submitted
process.stdout.write(JSON.stringify({
ok: true, code: "ACCEPTED", eventType: "task.accepted",
timestamp: new Date().toISOString(),
}) + "\n");
process.exit(0);
}

// ─── Hooks that require governed context and service ─────────────────────
// PostToolUse, TestStart, Notification, Permission, ReadyForReview

const contextFile = process.env.CORTEX_LAUNCH_CONTEXT;
if (!contextFile || typeof contextFile !== "string" || contextFile.length === 0) {
process.stdout.write(JSON.stringify(buildErrorReceipt(false, "ERR_CONTEXT_REQUIRED", HOOK_EVENT_MAP[hookName])) + "\n");
process.exit(1);
}

const projectRoot = findProjectRoot();
let service;
try {
service = resolveCoordinationService(projectRoot);
} catch (err) {
process.stdout.write(JSON.stringify(buildErrorReceipt(false, "ERR_SERVICE_UNAVAILABLE")) + "\n");
process.exit(1);
}

let result;
try {
const { executeClaudeHook } = require("../lib/coordination/claude-hook-cli");
result = executeClaudeHook(service, hookName, validatedPayload);
} catch (err) {
service.close();
process.stdout.write(JSON.stringify(buildErrorReceipt(false, "ERR_INTERNAL")) + "\n");
process.exit(2);
}

service.close();

if (!result.ok) {
process.stdout.write(JSON.stringify(buildErrorReceipt(false, result.code, result.eventType)) + "\n");
process.exit(1);
}

process.stdout.write(JSON.stringify(result) + "\n");
process.exit(0);
}

const { HOOK_EVENT_MAP } = require("../lib/coordination/claude-hook-handlers");

main().catch((err) => {
process.stdout.write(JSON.stringify(buildErrorReceipt(false, "ERR_INTERNAL")) + "\n");
process.exit(2);
});
Loading
Loading