From 0d1840da96c76af07f8df98213896fb8e940bc3a Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 24 Sep 2026 13:25:57 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(agentctl):=20governance=20visibility?= =?UTF-8?q?=20P0=20=E2=80=94=20table/brief/archive=20+=20MCP=20reads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add human-readable --format table, one-page brief, claim archive via docs apply CAS, and four MCP readonly tools. Includes P1 console design doc and engineering-tool test evidence. --- .../2026-09-24-governance-console-plan.md | 174 ++++++++ .../2026-09-24-governance-visibility-p0.md | 62 +++ ...24-governance-visibility-p0-test-report.md | 143 ++++++ .../archive-dryrun.txt | 2 + .../brief.txt | 16 + .../claim-table.txt | 3 + .../cleanup-table.txt | 7 + .../mcp-tools.txt | 23 + .../queue-table.txt | 3 + .../status-table.txt | 15 + scripts/agentctl.py | 338 ++++++++++++-- scripts/agentctl_visibility.py | 411 ++++++++++++++++++ scripts/mcp/agentctl-mcp.mjs | 64 +++ scripts/tests/test_agent_worktree.py | 2 +- scripts/tests/test_agentctl.py | 2 + scripts/tests/test_agentctl_catalog.py | 3 + scripts/tests/test_agentctl_visibility.py | 159 +++++++ scripts/tests/test_codegraph_governance.py | 3 +- scripts/tests/test_ops_guards.py | 2 +- scripts/tests/test_pi_subagents.py | 3 + 20 files changed, 1407 insertions(+), 28 deletions(-) create mode 100644 .doc/ops/short-term/2026-09-24-governance-console-plan.md create mode 100644 .doc/ops/short-term/2026-09-24-governance-visibility-p0.md create mode 100644 .doc/test-reports/2026-09-24-governance-visibility-p0-test-report.md create mode 100644 .doc/test-reports/assets/2026-09-24-governance-visibility-p0/archive-dryrun.txt create mode 100644 .doc/test-reports/assets/2026-09-24-governance-visibility-p0/brief.txt create mode 100644 .doc/test-reports/assets/2026-09-24-governance-visibility-p0/claim-table.txt create mode 100644 .doc/test-reports/assets/2026-09-24-governance-visibility-p0/cleanup-table.txt create mode 100644 .doc/test-reports/assets/2026-09-24-governance-visibility-p0/mcp-tools.txt create mode 100644 .doc/test-reports/assets/2026-09-24-governance-visibility-p0/queue-table.txt create mode 100644 .doc/test-reports/assets/2026-09-24-governance-visibility-p0/status-table.txt create mode 100644 scripts/agentctl_visibility.py create mode 100644 scripts/tests/test_agentctl_visibility.py diff --git a/.doc/ops/short-term/2026-09-24-governance-console-plan.md b/.doc/ops/short-term/2026-09-24-governance-console-plan.md new file mode 100644 index 00000000..601b6f6c --- /dev/null +++ b/.doc/ops/short-term/2026-09-24-governance-console-plan.md @@ -0,0 +1,174 @@ +--- +title: "治理控制台 P1 方案" +task_id: "governance-visibility-p0-2026-09-24" +status: planned +created: 2026-09-24 +updated: 2026-09-24 +owner: Cursor +task_type: research +related_p0: governance-visibility-p0-2026-09-24 +--- + +# 治理控制台全景设计(P1) + +覆盖方向:**A 状态可视化**、**B 流程自动化**、**C MCP 只读巡览**、**D PI 派发包生成**。 +P0 已落地:`--format table`、`brief`、MCP 四只读工具、`claim archive`。本文件是后续实施蓝图,**不在 P0 编码**。 + +## 1. 目标 + +给人类与 Agent 一个本机治理面:一眼看到任务/租约/队列/漂移/清理积压,并把高频手工步骤(认领、归档、集成候选、PI packet)收成白名单动作——**全部复用既有租约与 `docs apply` CAS,不新增旁路写入口**。 + +## 2. 架构 + +```text +Browser (localhost) ──► static SPA ──► agentctl serve (127.0.0.1) + │ +Cursor MCP / CLI ────────────────────────────┤ + ▼ + agentctl 核心 + (SQLite / leases / docs apply / queue / catalog) +``` + +| 组件 | 职责 | 约束 | +|---|---|---| +| `agentctl serve` | HTTP JSON API + 静态文件;仅绑 `127.0.0.1` | 无鉴权外放;动作白名单;写路径内部调 `with`/`docs apply` | +| 静态 SPA | 单页四区:任务租约 / 队列集成 / 文档漂移 / 清理积压 | **不进产品 `web/`**,避免发布门禁耦合;建议路径 `scripts/governance-console/` | +| CLI 增强 | `claim new`、`integrate prepare`、`pi packet` | 与 HTTP 动作共用同一 Python 函数 | + +跨机权威认领不在本方案重复设计,衔接 [coordination-db-daemon phased plan](../remediation/2026-09-23-coordination-db-daemon-phased-plan.md)。 + +## 3. HTTP API(草案) + +基址:`http://127.0.0.1:7432`(端口可配,默认避开产品 8080/8081)。 + +### 3.1 只读 + +| Method | Path | 映射 | +|---|---|---| +| GET | `/api/status` | `collect_status_payload` + 可选 `?format=table` | +| GET | `/api/brief` | `cmd_brief` Markdown 或 JSON | +| GET | `/api/queue` | `queue list` | +| GET | `/api/claims` | `claim list` | +| GET | `/api/cleanup` | `cleanup report` | +| GET | `/api/docs/drift` | `docs drift` | +| GET | `/api/catalog/drift` | `catalog drift` | +| GET | `/api/daemon` | `daemon status` | + +### 3.2 动作白名单(均要求 `task` + 可选 `authorization_ref`) + +| Method | Path | 行为 | +|---|---|---| +| POST | `/api/claim/new` | 见 §4.1:snapshot→delta→apply 封装 | +| POST | `/api/claim/archive` | 既有 `claim archive --apply` | +| POST | `/api/integrate/prepare` | 见 §4.2:候选 worktree + `--no-ff` | +| POST | `/api/pi/packet` | 见 §4.3:生成 dispatch JSON | + +拒绝:任意 shell、env-service、push、晋升、删 worktree、绕过租约的文件写。 + +## 4. B/D CLI 完整版 + +### 4.1 `agentctl claim new`(B) + +```bash +agentctl claim new --task "$ID" --owner Cursor --mutex \ + [--branch feature/$ID] [--worktree .worktrees/$ID] +``` + +内部: + +1. `docs snapshot --file CURRENT --record $ID` +2. 若 `sha256` 已存在 → 拒绝或 `--replace` 显式更新 +3. 构造一行 `| owner | task | … | in_progress |` +4. `docs apply`(单行 CAS;无需 `@document`) + +消除当前 skill 里 snapshot → 手写 JSON → apply 三步。 + +### 4.2 `agentctl integrate prepare`(B) + +```bash +agentctl integrate prepare --task "$ID" --expected-dev \ + [--integrate-id integrate-$ID] +``` + +内部(仍在租约内): + +1. 从 freeze 的 `expected-dev` 建 `feature/integrate-` worktree(若已存在则复用并校验干净) +2. 在 integrate worktree `task register`(scope 含 git:dev 所需能力) +3. `--no-ff` 合并任务分支到候选 HEAD +4. 输出 `{integrate_task, worktree, head}` 供后续 `validate` / `queue enqueue` + +不替代 daemon ff-only;只去掉「手工第二个 worktree」摩擦。 + +### 4.3 `agentctl pi packet`(D) + +```bash +agentctl pi packet --task "$ID" --role research|planning|… \ + --goal '…' --authorization-ref '…' \ + [--approved-command '…']… +``` + +从 task 行读取 `worktree`/`HEAD`,生成: + +```json +{ + "task_id": "…", + "request_id": "", + "role_id": "research", + "goal": "…", + "authorization_ref": "…", + "source_sha": "", + "input_files": [], + "timeout_seconds": 600, + "approved_commands": [] +} +``` + +写出到 `.doc/test-reports/assets/pi-subagents///packet.json`(路径仍须在 `pi_subagents` 租约约定内)。不自动 `run`——生成与派发分离,避免误触模型。 + +## 5. 前端 SPA(A) + +单页四区,无路由也可: + +1. **任务 / 租约**:活跃 tasks、live runs、held claims;链到 worktree 路径文本。 +2. **队列与集成**:pending/claimed;按钮「prepare」(调 `/api/integrate/prepare`,需确认)。 +3. **文档漂移**:MATRIX hard regressions + catalog drift。 +4. **清理积压**:`cleanup report` 表;仅展示,删除仍人工/`agent-worktree remove`。 + +技术选型建议:零构建或 Vite 单包静态资源,依赖控制台已有 `brief`/`--format table` 文案风格,不套产品 Admin UI。 + +## 6. 与 P0 / 既有系统衔接 + +| 能力 | 状态 | +|---|---| +| `--format table` / `brief` / MCP 只读 / `claim archive` | **P0 done** | +| MCP 再暴露 `brief`、`claim_archive` | P1 可选 | +| `serve` + SPA | P1 | +| `claim new` / `integrate prepare` / `pi packet` | P1 | +| 跨机 claim 权威 | 见 coordination-db-daemon 方案,本控制台只读本地 SQLite | + +## 7. 分阶段实施顺序 + +1. **P1-a**:抽出 `collect_*` 为稳定库(P0 已部分完成)→ `agentctl serve` 只读 API + 极简 HTML。 +2. **P1-b**:`claim new` + 控制台认领表单。 +3. **P1-c**:`integrate prepare` + 队列区按钮。 +4. **P1-d**:`pi packet` + 可选把 packet 路径注入 `pi-subagent-dispatch` skill。 +5. **P1-e**(可选):把 SPA 挂进 Cursor 简单 Browser tab 工作流说明。 + +## 8. 验收标准(实施时) + +- 只读 API 与 CLI 同构(同 payload 字段)。 +- 任一写动作在审计表留下 run_id;CURRENT/MATRIX 变更有 `docs apply` 收据。 +- `serve` 默认拒绝非 loopback;集成测试用临时端口。 +- 工程工具单测:`scripts/tests/test_agentctl_serve.py` + 既有 visibility 测试。 +- 不改产品 compose / 不占用 8080。 + +## 9. 非目标 + +- 产品 AdminConsole 内嵌治理页。 +- 远程多用户 Web 登录。 +- 自动 `--force` 清理 worktree。 +- 替代 Herdr / 产品 PI Runner。 + +## 10. 授权与下一步 + +本文件为方案;实施需单独授权并新开 task(建议 `governance-console-p1-YYYY-MM-DD`),先 `catalog overlap` 再编码。 diff --git a/.doc/ops/short-term/2026-09-24-governance-visibility-p0.md b/.doc/ops/short-term/2026-09-24-governance-visibility-p0.md new file mode 100644 index 00000000..00ad6ddf --- /dev/null +++ b/.doc/ops/short-term/2026-09-24-governance-visibility-p0.md @@ -0,0 +1,62 @@ +--- +title: "治理可视化 P0" +task_id: "governance-visibility-p0-2026-09-24" +status: in_progress +created: 2026-09-24 +updated: 2026-09-24 +owner: Cursor +task_type: implementation +task_tier: small +policy_revision: "agent-governance-2026-09-21-r1 + cursor-governance-2026-09-24-r1 @ a2afa67d" +branch: feature/governance-visibility-p0-2026-09-24 +worktree: .worktrees/governance-visibility-p0-2026-09-24 +base_dev_sha: "a2afa67d471c4edce9a6a31600811779549c8cce" +write_scope: ["scripts", ".doc"] +allowed_environments: [] +head_sha: "" +mutex: "agentctl-visibility" +review_state: pending +integration_state: pending +deployment: not_required +cleanup: pending +cleanup_owner: "独立维护者" +cleanup_trigger: "合入 dev 且验收通过后" +remote_sync: not_requested +--- + +# 治理可视化 P0 + +## 用户目标与授权 + +- 用户目标:实施治理可视化快赢(人类可读 status/brief、MCP 只读工具、claim archive)并撰写治理控制台 P1 方案。 +- 交付终点:P0 代码合入 local `dev`;工程工具单测与真实控制台证据;P1 设计文档落盘。 +- 禁止:push、晋升 test/main、跨机协调实施、产品 web/cmd/internal 改动。 + +## 范围与非目标 + +- 允许:`scripts/agentctl.py`、`scripts/mcp/agentctl-mcp.mjs`、新 visibility 模块与单测、`.doc/ops/short-term`、`.doc/test-reports`、archive 追加。 +- 非目标:dashboard 前后端、`claim new`、`integrate prepare`、`pi packet`(仅 P1 文档)、跨机协调。 + +## 相关问题与经验 + +- 关联:R04 CURRENT 膨胀(政策审查)、`ISSUE-20260923-coordination-authority`(不实施,仅衔接说明)。 +- catalog:`R-governance-visibility-p0`;overlap 探测无 hard/soft 命中。 + +## 步骤与验收 + +1. [x] 建场 / register / CURRENT 认领 +2. [ ] `--format table` 渲染 +3. [ ] `brief` +4. [ ] MCP 只读工具 +4 +5. [ ] `claim archive` +6. [ ] 单测 + 真实运行报告 +7. [ ] P1 控制台方案文档 +8. [ ] validate → queue enqueue → 收口 + +## 验证与证据 + +按验证契约「工程工具验收」:`python3 -B -m unittest discover -s scripts/tests -p 'test_*.py' -v` + 真实 `--format table`/`brief`/`claim archive --dry-run` 控制台取样。 + +## 收口与交接 + +待实施完成后填写。 diff --git a/.doc/test-reports/2026-09-24-governance-visibility-p0-test-report.md b/.doc/test-reports/2026-09-24-governance-visibility-p0-test-report.md new file mode 100644 index 00000000..8b1a874e --- /dev/null +++ b/.doc/test-reports/2026-09-24-governance-visibility-p0-test-report.md @@ -0,0 +1,143 @@ +# 治理可视化 P0 测试报告 + +## 一、 测试背景 (Background) + +agentctl 治理状态此前仅输出 JSON,CURRENT 并行认领表 done 行滞留(R04),MCP 缺少 queue/claim/cleanup/catalog 只读巡览工具。本轮实现人类可读渲染、`brief`、`claim archive` dry-run/apply 路径与 MCP +4。 + +## 二、 测试目标 (Test Objectives) + +1. `--format table` 对 status/queue/claim/cleanup 输出可读表且默认 json 兼容。 +2. `brief` 聚合 daemon/tasks/cleanup/drift。 +3. `claim archive` 默认 dry-run,按 retain-days 筛选 done 行。 +4. MCP tools/list 含 queue_list、claim_status、cleanup_report、catalog_drift。 +5. 工程工具单测全绿。 + +## 三、 测试方案与环境 (Test Plan & Environment) + +- OS: Linux;Python 3;worktree `.worktrees/governance-visibility-p0-2026-09-24` +- 层级:验证契约「工程工具验收」——`unittest discover -s scripts/tests` +- 不适用产品 PI 全链路(NOT REQUIRED:纯 scripts/.doc 工程工具) + +## 四、 预期结果 (Expected Results) + +- table 模式含任务/队列表头;json 默认不变 +- brief 为 Markdown 一页 +- archive dry-run 返回 eligible_count 与 candidates +- 单测 226 OK(含新增 11 项 visibility) + +## 五、 实际测试结果与判定 (Actual Results & Evaluation) + +### TC1 status --format table + +- **Input**: `python3 scripts/agentctl.py status --format table` +- **Actual Output**: + +``` +=== status --format table === +agentctl status main=/home/weichen/projects/Agent-System epoch=a994d494-0d9 version=1 + +## tasks active=1 closed=152 total=153 +task owner state worktree branch +----------------------------------- ------ ------ ---------------------------------------------- ------------ +governance-visibility-p0-2026-09-24 Cursor active .worktrees/governance-visibility-p0-2026-09-24 feature/gove + +## runs live=0 +(no live runs) + +## queue depth=0 +(queue empty) + +## policy loaded=ok revision=agent-governance-2026-09-23-fav-constraint-restored +``` + +- **Verdict**: PASS +- **Duration**: <1s + +### TC2 brief + +- **Input**: `python3 scripts/agentctl.py brief` +- **Actual Output**: + +``` +=== brief === +# agentctl brief (2026-09-24 13:25) + +- daemon: **ok** manager.lock.held=True queue pending=0 +- tasks: active=1 blocked=0 live runs=0 integrate queue=0 +- cleanup pending: 4 / 4 worktrees +- MATRIX drift hard alerts: 0 + +## Active tasks +- `governance-visibility-p0-2026-09-24` owner=Cursor state=active wt=.worktrees/governance-visibility-p0-2026-09-24 + +## Blockers / alerts +- cleanup backlog: 4 worktrees not removable + +## Integrate queue +- (empty) +``` + +- **Verdict**: PASS + +### TC3 claim archive dry-run + +- **Input**: `claim archive --task governance-visibility-p0-2026-09-24 --retain-days 7` +- **Actual Output** (节选 JSON): + +``` +=== claim archive dry-run === +{"archive_file": ".doc/archive/ops/2026-09-21-current-history.md", "candidates": [{"branch": "feature/initial-load-performance-plan", "eligible": true, "line": "| Codex | 2026-09-17-initial-load-performance-plan | feature/initial-load-performance-plan | .worktrees/feature-initial-load-performance-plan | loading-performance-audit | done |", "mutex": "loading-performance-audit", "owner": "Codex", "parsed_date": "2026-09-17", "reason": "older_than_7d", "status": "done", "task": "2026-09-17-initial-load-performance-plan", "worktree": ".worktrees/feature-initial-load-performance-plan"}], "done_scanned": 85, "eligible_count": 1, "include_undated": false, "retain_days": 7, "skipped": [{"branch": "feature/non-author-closeout-2026-09-24", "eligible": false, "line": "| Cursor | non-author-closeout-2026-09-24 | feature/non-author-closeout-2026-09-24 | .worktrees/non-author-closeout-2026-09-24 | docs+git:dev | done |", "mutex": "docs+git:dev", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "non-author-closeout-2026-09-24", "worktree": ".worktrees/non-author-closeout-2026-09-24"}, {"branch": "feature/inte +``` + +- **Verdict**: PASS(done_scanned=85,eligible_count=1,默认不改文件) + +### TC4 MCP tools/list + +- **Input**: NDJSON `tools/list` against `scripts/mcp/agentctl-mcp.mjs` +- **Actual Output**: + +``` +catalog_drift +catalog_overlap_ack +catalog_overlap_probe +catalog_requirement_add +catalog_search +catalog_status +claim_status +cleanup_report +docs_apply +docs_snapshot +lease_run +quarantine_status +queue_enqueue +queue_list +status +task_close +task_register +validate +--- +has queue_list true +has claim_status true +has cleanup_report true +has catalog_drift true +``` + +- **Verdict**: PASS + +### TC5 unittest suite + +- **Input**: `python3 -B -m unittest discover -s scripts/tests -p 'test_*.py' -v` +- **Actual Output**: `Ran 226 tests in ~132s` → **OK** +- **Verdict**: PASS + +## 六、 自动化测试套件汇总 (Test Suite Execution) + +| 套件 | 结果 | +|---|---| +| `scripts/tests` unittest discover | 226 OK | +| visibility 新增 | 11 OK | +| 产品 go test / 真实 PI | NOT REQUIRED(工程工具) | + +## 七、 测试结论与后续优化建议 (Conclusion & Next Steps) + +P0 验收达标。后续按 [.doc/ops/short-term/2026-09-24-governance-console-plan.md](../ops/short-term/2026-09-24-governance-console-plan.md) 实施 P1(serve/SPA/claim new/integrate prepare/pi packet)。证据附件:`.doc/test-reports/assets/2026-09-24-governance-visibility-p0/`。 diff --git a/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/archive-dryrun.txt b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/archive-dryrun.txt new file mode 100644 index 00000000..ad1cdba9 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/archive-dryrun.txt @@ -0,0 +1,2 @@ +=== claim archive dry-run === +{"archive_file": ".doc/archive/ops/2026-09-21-current-history.md", "candidates": [{"branch": "feature/initial-load-performance-plan", "eligible": true, "line": "| Codex | 2026-09-17-initial-load-performance-plan | feature/initial-load-performance-plan | .worktrees/feature-initial-load-performance-plan | loading-performance-audit | done |", "mutex": "loading-performance-audit", "owner": "Codex", "parsed_date": "2026-09-17", "reason": "older_than_7d", "status": "done", "task": "2026-09-17-initial-load-performance-plan", "worktree": ".worktrees/feature-initial-load-performance-plan"}], "done_scanned": 85, "eligible_count": 1, "include_undated": false, "retain_days": 7, "skipped": [{"branch": "feature/non-author-closeout-2026-09-24", "eligible": false, "line": "| Cursor | non-author-closeout-2026-09-24 | feature/non-author-closeout-2026-09-24 | .worktrees/non-author-closeout-2026-09-24 | docs+git:dev | done |", "mutex": "docs+git:dev", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "non-author-closeout-2026-09-24", "worktree": ".worktrees/non-author-closeout-2026-09-24"}, {"branch": "feature/integrate-non-author-docs-2026-09-24", "eligible": false, "line": "| Cursor | integrate-non-author-docs-2026-09-24 | feature/integrate-non-author-docs-2026-09-24 | .worktrees/integrate-non-author-docs-2026-09-24 | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "integrate-non-author-docs-2026-09-24", "worktree": ".worktrees/integrate-non-author-docs-2026-09-24"}, {"branch": "feature/non-author-member-access-2026-09-24", "eligible": false, "line": "| Cursor | non-author-member-access-docs-2026-09-24 | feature/non-author-member-access-2026-09-24 | .worktrees/non-author-member-access-2026-09-24 | docs+matrix | done |", "mutex": "docs+matrix", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "non-author-member-access-docs-2026-09-24", "worktree": ".worktrees/non-author-member-access-2026-09-24"}, {"branch": "feature/non-author-member-access-2026-09-24", "eligible": false, "line": "| Cursor | non-author-member-access-2026-09-24 | feature/non-author-member-access-2026-09-24 | .worktrees/non-author-member-access-2026-09-24 | docgov+iam-authz+workflow-gov | done |", "mutex": "docgov+iam-authz+workflow-gov", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "non-author-member-access-2026-09-24", "worktree": ".worktrees/non-author-member-access-2026-09-24"}, {"branch": "feature/integrate-free-collab-2026-09-24", "eligible": false, "line": "| Cursor | integrate-free-collab-2026-09-24 | feature/integrate-free-collab-2026-09-24 | .worktrees/integrate-free-collab-2026-09-24 | integrate-dev | done |", "mutex": "integrate-dev", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "integrate-free-collab-2026-09-24", "worktree": ".worktrees/integrate-free-collab-2026-09-24"}, {"branch": "feature/cursor-governance-hooks-2026-09-24", "eligible": false, "line": "| Cursor | cursor-governance-hooks-2026-09-24 | feature/cursor-governance-hooks-2026-09-24 | .worktrees/cursor-governance-hooks-2026-09-24 | cursor-governance | done |", "mutex": "cursor-governance", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "cursor-governance-hooks-2026-09-24", "worktree": ".worktrees/cursor-governance-hooks-2026-09-24"}, {"branch": "feature/governance-catalog-2026-09-24", "eligible": false, "line": "| Cursor | governance-catalog-2026-09-24 | feature/governance-catalog-2026-09-24 | .worktrees/governance-catalog-2026-09-24 | agent-workflow-governance+catalog | done |", "mutex": "agent-workflow-governance+catalog", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "governance-catalog-2026-09-24", "worktree": ".worktrees/governance-catalog-2026-09-24"}, {"branch": "feature/subagent-workflow-standard-2026-09-24", "eligible": false, "line": "| Codex | subagent-workflow-standard-2026-09-24-land | feature/subagent-workflow-standard-2026-09-24 | .worktrees/subagent-workflow-standard-2026-09-24 | pi-subagent-integration | done |", "mutex": "pi-subagent-integration", "owner": "Codex", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "subagent-workflow-standard-2026-09-24-land", "worktree": ".worktrees/subagent-workflow-standard-2026-09-24"}, {"branch": "feature/subagent-workflow-standard-2026-09-24", "eligible": false, "line": "| Cursor/Codex | subagent-workflow-standard-2026-09-24-executor | feature/subagent-workflow-standard-2026-09-24 | .worktrees/subagent-workflow-standard-2026-09-24 | pi-subagent-executor | done |", "mutex": "pi-subagent-executor", "owner": "Cursor/Codex", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "subagent-workflow-standard-2026-09-24-executor", "worktree": ".worktrees/subagent-workflow-standard-2026-09-24"}, {"branch": "feature/subagent-workflow-standard-2026-09-24", "eligible": false, "line": "| Codex | subagent-workflow-standard-2026-09-24-config-r2 | feature/subagent-workflow-standard-2026-09-24 | .worktrees/subagent-workflow-standard-2026-09-24 | subagent-workflow-docs | done |", "mutex": "subagent-workflow-docs", "owner": "Codex", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "subagent-workflow-standard-2026-09-24-config-r2", "worktree": ".worktrees/subagent-workflow-standard-2026-09-24"}, {"branch": "feature/subagent-workflow-standard-2026-09-24", "eligible": false, "line": "| Codex | subagent-workflow-standard-2026-09-24 | feature/subagent-workflow-standard-2026-09-24 | .worktrees/subagent-workflow-standard-2026-09-24 | subagent-workflow-docs | done |", "mutex": "subagent-workflow-docs", "owner": "Codex", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "subagent-workflow-standard-2026-09-24", "worktree": ".worktrees/subagent-workflow-standard-2026-09-24"}, {"branch": "feature/integrate-gate-nightly-fixes-b-2026-09-24", "eligible": false, "line": "| Cursor | integrate-gate-nightly-fixes-b-2026-09-24 | feature/integrate-gate-nightly-fixes-b-2026-09-24 | .worktrees/integrate-gate-nightly-fixes-b-2026-09-24 | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "integrate-gate-nightly-fixes-b-2026-09-24", "worktree": ".worktrees/integrate-gate-nightly-fixes-b-2026-09-24"}, {"branch": "feature/integrate-gate-nightly-fixes-2026-09-24", "eligible": false, "line": "| Cursor | integrate-gate-nightly-fixes-2026-09-24 | feature/integrate-gate-nightly-fixes-2026-09-24 | .worktrees/integrate-gate-nightly-fixes-2026-09-24 | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "integrate-gate-nightly-fixes-2026-09-24", "worktree": ".worktrees/integrate-gate-nightly-fixes-2026-09-24"}, {"branch": "feature/integrate-docs-gate-promo-report-2026-09-24", "eligible": false, "line": "| Cursor | integrate-docs-gate-promo-report-2026-09-24 | feature/integrate-docs-gate-promo-report-2026-09-24 | .worktrees/integrate-docs-gate-promo-report-2026-09-24 | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "integrate-docs-gate-promo-report-2026-09-24", "worktree": ".worktrees/integrate-docs-gate-promo-report-2026-09-24"}, {"branch": "feature/promote-rc-beta3-c-2026-09-24", "eligible": false, "line": "| Cursor | promote-rc-beta3-c-2026-09-24 | feature/promote-rc-beta3-c-2026-09-24 | .worktrees/promote-rc-beta3-c-2026-09-24 | git:test+env:test+docs | done |", "mutex": "git:test+env:test+docs", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "promote-rc-beta3-c-2026-09-24", "worktree": ".worktrees/promote-rc-beta3-c-2026-09-24"}, {"branch": "feature/dev-full-gate-test-promo-2026-09-24", "eligible": false, "line": "| Cursor | dev-full-gate-test-promo-2026-09-24 | feature/dev-full-gate-test-promo-2026-09-24 | .worktrees/dev-full-gate-test-promo-2026-09-24 | git:dev+env:dev+env:test+docs | done |", "mutex": "git:dev+env:dev+env:test+docs", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "dev-full-gate-test-promo-2026-09-24", "worktree": ".worktrees/dev-full-gate-test-promo-2026-09-24"}, {"branch": "feature/gate-promo-current-close-2026-09-24", "eligible": false, "line": "| Cursor | gate-promo-current-close-2026-09-24 | feature/gate-promo-current-close-2026-09-24 | .worktrees/gate-promo-current-close-2026-09-24 | docs:admin+docs | done |", "mutex": "docs:admin+docs", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "gate-promo-current-close-2026-09-24", "worktree": ".worktrees/gate-promo-current-close-2026-09-24"}, {"branch": "feature/docs-gate-promo-report-2026-09-24", "eligible": false, "line": "| Cursor | docs-gate-promo-report-2026-09-24 | feature/docs-gate-promo-report-2026-09-24 | .worktrees/docs-gate-promo-report-2026-09-24 | docs+git:dev | done |", "mutex": "docs+git:dev", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "docs-gate-promo-report-2026-09-24", "worktree": ".worktrees/docs-gate-promo-report-2026-09-24"}, {"branch": "feature/admin-password-reset-2026-09-24", "eligible": false, "line": "| Cursor | admin-password-reset-2026-09-24 | feature/admin-password-reset-2026-09-24 | .worktrees/admin-password-reset-2026-09-24 | auth+env:test | done |", "mutex": "auth+env:test", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "admin-password-reset-2026-09-24", "worktree": ".worktrees/admin-password-reset-2026-09-24"}, {"branch": "fix/fix-fav-staging-uuid-2026-09-24", "eligible": false, "line": "| Cursor | fix-fav-staging-uuid-2026-09-24 | fix/fix-fav-staging-uuid-2026-09-24 | .worktrees/fix-fav-staging-uuid-2026-09-24 | artifactvault | done |", "mutex": "artifactvault", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "fix-fav-staging-uuid-2026-09-24", "worktree": ".worktrees/fix-fav-staging-uuid-2026-09-24"}, {"branch": "fix/gate-e1a-unique-project-name-2026-09-24", "eligible": false, "line": "| Cursor | gate-e1a-unique-project-name-2026-09-24 | fix/gate-e1a-unique-project-name-2026-09-24 | .worktrees/gate-e1a-unique-project-name-2026-09-24 | docnotify | done |", "mutex": "docnotify", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "gate-e1a-unique-project-name-2026-09-24", "worktree": ".worktrees/gate-e1a-unique-project-name-2026-09-24"}, {"branch": "fix/gate-unhook-docquery-2026-09-24", "eligible": false, "line": "| Cursor | gate-unhook-docquery-2026-09-24 | fix/gate-unhook-docquery-2026-09-24 | .worktrees/gate-unhook-docquery-2026-09-24 | server+docquery | done |", "mutex": "server+docquery", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "gate-unhook-docquery-2026-09-24", "worktree": ".worktrees/gate-unhook-docquery-2026-09-24"}, {"branch": "feature/integrate-jev-assist-land-2026-09-24", "eligible": false, "line": "| Cursor | integrate-jev-assist-land-2026-09-24 | feature/integrate-jev-assist-land-2026-09-24 | .worktrees/integrate-jev-assist-land-2026-09-24 | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "integrate-jev-assist-land-2026-09-24", "worktree": ".worktrees/integrate-jev-assist-land-2026-09-24"}, {"branch": "feature/integrate-codegraph-fixup2-2026-09-24", "eligible": false, "line": "| Cursor | integrate-codegraph-fixup2-2026-09-24 | feature/integrate-codegraph-fixup2-2026-09-24 | (removed) | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "integrate-codegraph-fixup2-2026-09-24", "worktree": "(removed)"}, {"branch": "feature/integrate-codegraph-governance-2026-09-24", "eligible": false, "line": "| Cursor | integrate-codegraph-governance-2026-09-24 | feature/integrate-codegraph-governance-2026-09-24 | (removed) | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "integrate-codegraph-governance-2026-09-24", "worktree": "(removed)"}, {"branch": "feature/codegraph-governance-2026-09-24", "eligible": false, "line": "| Cursor | codegraph-governance-r2-2026-09-24 | feature/codegraph-governance-2026-09-24 | .worktrees/codegraph-governance-2026-09-24 | agent-workflow-governance+codegraph | done |", "mutex": "agent-workflow-governance+codegraph", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "codegraph-governance-r2-2026-09-24", "worktree": ".worktrees/codegraph-governance-2026-09-24"}, {"branch": "feature/jev-dev-shadow-on-2026-09-24", "eligible": false, "line": "| Cursor | jev-dev-shadow-on-2026-09-24 | feature/jev-dev-shadow-on-2026-09-24 | .worktrees/jev-dev-shadow-on-2026-09-24 | env:dev+jevdecide | done |", "mutex": "env:dev+jevdecide", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "jev-dev-shadow-on-2026-09-24", "worktree": ".worktrees/jev-dev-shadow-on-2026-09-24"}, {"branch": "feature/jev-product-closeout-2026-09-24", "eligible": false, "line": "| Cursor | jev-product-closeout-2026-09-24 | feature/jev-product-closeout-2026-09-24 | .worktrees/jev-product-closeout-2026-09-24 | jevdecide+coordinator | done |", "mutex": "jevdecide+coordinator", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "jev-product-closeout-2026-09-24", "worktree": ".worktrees/jev-product-closeout-2026-09-24"}, {"branch": "feature/jev-product-shadow-2026-09-24", "eligible": false, "line": "| Cursor | jev-product-shadow-2026-09-24 | feature/jev-product-shadow-2026-09-24 | .worktrees/jev-product-shadow-2026-09-24 | jevdecide+coordinator | done |", "mutex": "jevdecide+coordinator", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "jev-product-shadow-2026-09-24", "worktree": ".worktrees/jev-product-shadow-2026-09-24"}, {"branch": "feature/jev-mcp-docs-done-2026-09-24", "eligible": false, "line": "| Cursor | jev-mcp-docs-done-2026-09-24 | feature/jev-mcp-docs-done-2026-09-24 | .worktrees/jev-mcp-docs-done-2026-09-24 | jev-mcp+docs | done |", "mutex": "jev-mcp+docs", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "jev-mcp-docs-done-2026-09-24", "worktree": ".worktrees/jev-mcp-docs-done-2026-09-24"}, {"branch": "feature/jev-mcp-integration-2026-09-24", "eligible": false, "line": "| Cursor | jev-mcp-integration-2026-09-24 | feature/jev-mcp-integration-2026-09-24 | .worktrees/jev-mcp-integration-2026-09-24 | jev-mcp+docs | done |", "mutex": "jev-mcp+docs", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "jev-mcp-integration-2026-09-24", "worktree": ".worktrees/jev-mcp-integration-2026-09-24"}, {"branch": "feature/integrate-intent-plan-u9u12-land-2026-09-24", "eligible": false, "line": "| Cursor | integrate-intent-plan-u9u12-land-2026-09-24 | feature/integrate-intent-plan-u9u12-land-2026-09-24 | .worktrees/integrate-intent-plan-u9u12-land-2026-09-24 | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-24", "reason": "within_retain_window", "status": "done", "task": "integrate-intent-plan-u9u12-land-2026-09-24", "worktree": ".worktrees/integrate-intent-plan-u9u12-land-2026-09-24"}, {"branch": "feature/integrate-pi-narrative-docs-2026-09-23", "eligible": false, "line": "| Cursor | integrate-pi-narrative-docs-2026-09-23 | feature/integrate-pi-narrative-docs-2026-09-23 | .worktrees/integrate-pi-narrative-final-closeout-b-2026-09-23 | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "integrate-pi-narrative-docs-2026-09-23", "worktree": ".worktrees/integrate-pi-narrative-final-closeout-b-2026-09-23"}, {"branch": "feature/pi-narrative-final-closeout-b-2026-09-23", "eligible": false, "line": "| Cursor | pi-narrative-final-closeout-b-2026-09-23 | feature/pi-narrative-final-closeout-b-2026-09-23 | .worktrees/pi-narrative-final-closeout-2026-09-23 | docgov | done |", "mutex": "docgov", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "pi-narrative-final-closeout-b-2026-09-23", "worktree": ".worktrees/pi-narrative-final-closeout-2026-09-23"}, {"branch": "feature/chat-semantic-d3-matrix-land-2026-09-23", "eligible": false, "line": "| Cursor | chat-semantic-d3-matrix-land-2026-09-23 | feature/chat-semantic-d3-matrix-land-2026-09-23 | .worktrees/chat-semantic-d3-matrix-land-2026-09-23 | chat | done |", "mutex": "chat", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "chat-semantic-d3-matrix-land-2026-09-23", "worktree": ".worktrees/chat-semantic-d3-matrix-land-2026-09-23"}, {"branch": "feature/closeout-claim-removed-2026-09-23", "eligible": false, "line": "| Cursor | closeout-claim-removed-2026-09-23 | feature/closeout-claim-removed-2026-09-23 | (removed) | git:dev+worktree-cleanup | done |", "mutex": "git:dev+worktree-cleanup", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "closeout-claim-removed-2026-09-23", "worktree": "(removed)"}, {"branch": "feature/module-batches-ag-status-fix-2026-09-23", "eligible": false, "line": "| Cursor | module-batches-ag-status-fix-2026-09-23 | feature/module-batches-ag-status-fix-2026-09-23 | .worktrees/module-batches-ag-status-fix-2026-09-23 | module-decoupling-spec | done |", "mutex": "module-decoupling-spec", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "module-batches-ag-status-fix-2026-09-23", "worktree": ".worktrees/module-batches-ag-status-fix-2026-09-23"}, {"branch": "feature/fav-constraint-current-2026-09-23", "eligible": false, "line": "| Cursor | fav-constraint-current-2026-09-23 | feature/fav-constraint-current-2026-09-23 | (removed) | git:dev+docs | done |", "mutex": "git:dev+docs", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "fav-constraint-current-2026-09-23", "worktree": "(removed)"}, {"branch": "feature/closeout-worktree-cleanup-2026-09-23", "eligible": false, "line": "| Cursor | closeout-worktree-cleanup-2026-09-23 | feature/closeout-worktree-cleanup-2026-09-23 | .worktrees/closeout-worktree-cleanup-2026-09-23 | git:dev+worktree-cleanup | done |", "mutex": "git:dev+worktree-cleanup", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "closeout-worktree-cleanup-2026-09-23", "worktree": ".worktrees/closeout-worktree-cleanup-2026-09-23"}, {"branch": "feature/remaining-worktrees-closeout-2026-09-23", "eligible": false, "line": "| Cursor | remaining-worktrees-closeout-2026-09-23 | feature/remaining-worktrees-closeout-2026-09-23 | (removed) | git:dev+worktree-cleanup | done |", "mutex": "git:dev+worktree-cleanup", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "remaining-worktrees-closeout-2026-09-23", "worktree": "(removed)"}, {"branch": "feature/worktree-closeout-cleanup-2026-09-23", "eligible": false, "line": "| Cursor | worktree-closeout-cleanup-2026-09-23 | feature/worktree-closeout-cleanup-2026-09-23 | .worktrees/worktree-closeout-cleanup-2026-09-23 | git:dev+worktree-cleanup | done |", "mutex": "git:dev+worktree-cleanup", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "worktree-closeout-cleanup-2026-09-23", "worktree": ".worktrees/worktree-closeout-cleanup-2026-09-23"}, {"branch": "feature/worktree-closeout-batch-2026-09-23", "eligible": false, "line": "| Cursor | worktree-closeout-batch-2026-09-23 | feature/worktree-closeout-batch-2026-09-23 | (removed) | git:dev+worktree-cleanup | done |", "mutex": "git:dev+worktree-cleanup", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "worktree-closeout-batch-2026-09-23", "worktree": "(removed)"}, {"branch": "feature/agentctl-cursor-integration-close-2026-09-23", "eligible": false, "line": "| Cursor | agentctl-cursor-integration-close-2026-09-23 | feature/agentctl-cursor-integration-close-2026-09-23 | .worktrees/agentctl-cursor-integration-close-2026-09-23 | agentctl-cursor-integration-close | done |", "mutex": "agentctl-cursor-integration-close", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "agentctl-cursor-integration-close-2026-09-23", "worktree": ".worktrees/agentctl-cursor-integration-close-2026-09-23"}, {"branch": "feature/integrate-agentctl-cursor-integration-2026-09-23", "eligible": false, "line": "| Cursor | integrate-agentctl-cursor-integration-2026-09-23 | feature/integrate-agentctl-cursor-integration-2026-09-23 | .worktrees/integrate-agentctl-cursor-integration-2026-09-23 | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "integrate-agentctl-cursor-integration-2026-09-23", "worktree": ".worktrees/integrate-agentctl-cursor-integration-2026-09-23"}, {"branch": "feature/agentctl-cursor-integration-2026-09-23", "eligible": false, "line": "| Cursor | agentctl-cursor-integration-2026-09-23 | feature/agentctl-cursor-integration-2026-09-23 | .worktrees/agentctl-cursor-integration-2026-09-23 | agentctl-cursor-integration | done |", "mutex": "agentctl-cursor-integration", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "agentctl-cursor-integration-2026-09-23", "worktree": ".worktrees/agentctl-cursor-integration-2026-09-23"}, {"branch": "feature/agentctl-mgmt-go-live-close-2026-09-23", "eligible": false, "line": "| 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 |", "mutex": "agentctl-mgmt-go-live-close", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "agentctl-mgmt-go-live-close-2026-09-23", "worktree": ".worktrees/agentctl-mgmt-go-live-close-2026-09-23"}, {"branch": "feature/integrate-agentctl-mgmt-go-live-2026-09-23", "eligible": false, "line": "| 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 |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "integrate-agentctl-mgmt-go-live-2026-09-23", "worktree": ".worktrees/integrate-agentctl-mgmt-go-live-2026-09-23"}, {"branch": "feature/agentctl-mgmt-go-live-2026-09-23", "eligible": false, "line": "| 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 |", "mutex": "agentctl-mgmt-go-live", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "agentctl-mgmt-go-live-2026-09-23", "worktree": ".worktrees/agentctl-mgmt-go-live-2026-09-23"}, {"branch": "feature/w3-u6-current-fix-2026-09-23", "eligible": false, "line": "| Cursor | w3-u6-current-fix-2026-09-23 | feature/w3-u6-current-fix-2026-09-23 | .worktrees/w3-u6-current-fix-2026-09-23 | w3-u6-current-fix | done |", "mutex": "w3-u6-current-fix", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "w3-u6-current-fix-2026-09-23", "worktree": ".worktrees/w3-u6-current-fix-2026-09-23"}, {"branch": "feature/integrate-w3-u6-closeout-2026-09-23", "eligible": false, "line": "| Cursor | integrate-w3-u6-closeout-2026-09-23 | feature/integrate-w3-u6-closeout-2026-09-23 | .worktrees/integrate-w3-u6-closeout-2026-09-23 | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "integrate-w3-u6-closeout-2026-09-23", "worktree": ".worktrees/integrate-w3-u6-closeout-2026-09-23"}, {"branch": "feature/integrate-matrix-record-id-2026-09-23", "eligible": false, "line": "| Cursor | integrate-matrix-record-id-2026-09-23 | feature/integrate-matrix-record-id-2026-09-23 | .worktrees/integrate-matrix-record-id-2026-09-23 | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "integrate-matrix-record-id-2026-09-23", "worktree": ".worktrees/integrate-matrix-record-id-2026-09-23"}, {"branch": "feature/w3-u6-closeout-2026-09-23", "eligible": false, "line": "| Cursor | w3-u6-closeout-2026-09-23 | feature/w3-u6-closeout-2026-09-23 | .worktrees/w3-u6-closeout-2026-09-23 | w3-u6-closeout | done |", "mutex": "w3-u6-closeout", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "w3-u6-closeout-2026-09-23", "worktree": ".worktrees/w3-u6-closeout-2026-09-23"}, {"branch": "feature/matrix-record-id-2026-09-23", "eligible": false, "line": "| Cursor | matrix-record-id-2026-09-23 | feature/matrix-record-id-2026-09-23 | .worktrees/matrix-record-id-2026-09-23 | matrix-record-id | done |", "mutex": "matrix-record-id", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "matrix-record-id-2026-09-23", "worktree": ".worktrees/matrix-record-id-2026-09-23"}, {"branch": "feature/w2-closeout-2026-09-23", "eligible": false, "line": "| Cursor | w2-closeout-2026-09-23 | feature/w2-closeout-2026-09-23 | .worktrees/w2-closeout-2026-09-23 | w2-closeout | done |", "mutex": "w2-closeout", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "w2-closeout-2026-09-23", "worktree": ".worktrees/w2-closeout-2026-09-23"}, {"branch": "feature/current-projection-2026-09-23", "eligible": false, "line": "| Cursor | current-projection-2026-09-23 | feature/current-projection-2026-09-23 | .worktrees/current-projection-2026-09-23 | current-projection | done |", "mutex": "current-projection", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "current-projection-2026-09-23", "worktree": ".worktrees/current-projection-2026-09-23"}, {"branch": "feature/agentctl-daemon-2026-09-23", "eligible": false, "line": "| Cursor | agentctl-daemon-2026-09-23 | feature/agentctl-daemon-2026-09-23 | .worktrees/agentctl-daemon-2026-09-23 | agentctl-daemon | done |", "mutex": "agentctl-daemon", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "agentctl-daemon-2026-09-23", "worktree": ".worktrees/agentctl-daemon-2026-09-23"}, {"branch": "feature/agentctl-handoff-2026-09-23", "eligible": false, "line": "| Cursor | agentctl-handoff-2026-09-23 | feature/agentctl-handoff-2026-09-23 | .worktrees/agentctl-handoff-2026-09-23 | agentctl-handoff | done |", "mutex": "agentctl-handoff", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "agentctl-handoff-2026-09-23", "worktree": ".worktrees/agentctl-handoff-2026-09-23"}, {"branch": "feature/agentctl-claim-2026-09-23", "eligible": false, "line": "| Cursor | agentctl-claim-2026-09-23 | feature/agentctl-claim-2026-09-23 | .worktrees/agentctl-claim-2026-09-23 | agentctl-claim | done |", "mutex": "agentctl-claim", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "agentctl-claim-2026-09-23", "worktree": ".worktrees/agentctl-claim-2026-09-23"}, {"branch": "feature/agent-governance-config-2026-09-23", "eligible": false, "line": "| Cursor | agent-governance-config-2026-09-23 | feature/agent-governance-config-2026-09-23 | .worktrees/agent-governance-config-2026-09-23 | agent-governance-config | done |", "mutex": "agent-governance-config", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "agent-governance-config-2026-09-23", "worktree": ".worktrees/agent-governance-config-2026-09-23"}, {"branch": "feature/coordination-db-daemon-impl-plan-2026-09-23", "eligible": false, "line": "| Cursor | coordination-db-daemon-impl-plan-2026-09-23 | feature/coordination-db-daemon-impl-plan-2026-09-23 | .worktrees/coordination-db-daemon-impl-plan-2026-09-23 | coordination-db-daemon-impl-plan | done |", "mutex": "coordination-db-daemon-impl-plan", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "coordination-db-daemon-impl-plan-2026-09-23", "worktree": ".worktrees/coordination-db-daemon-impl-plan-2026-09-23"}, {"branch": "feature/coordination-db-daemon-2026-09-23", "eligible": false, "line": "| Cursor | coordination-db-daemon-2026-09-23 | feature/coordination-db-daemon-2026-09-23 | .worktrees/coordination-db-daemon-2026-09-23 | coordination-db-daemon | done |", "mutex": "coordination-db-daemon", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "coordination-db-daemon-2026-09-23", "worktree": ".worktrees/coordination-db-daemon-2026-09-23"}, {"branch": "feature/integrate-pi-runner-response-size-2026-09-23", "eligible": false, "line": "| Cursor | integrate-pi-runner-response-size-r2-2026-09-23 | feature/integrate-pi-runner-response-size-2026-09-23 | .worktrees/integrate-pi-runner-response-size-2026-09-23 | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "integrate-pi-runner-response-size-r2-2026-09-23", "worktree": ".worktrees/integrate-pi-runner-response-size-2026-09-23"}, {"branch": "feature/modular-decouple-residual-2026-09-23", "eligible": false, "line": "| Cursor | modular-decouple-residual-b-2026-09-23 | feature/modular-decouple-residual-2026-09-23 | .worktrees/modular-decouple-residual-2026-09-23 | modular-decouple-residual | done |", "mutex": "modular-decouple-residual", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "modular-decouple-residual-b-2026-09-23", "worktree": ".worktrees/modular-decouple-residual-2026-09-23"}, {"branch": "feature/modular-refactor-impl-plan-2026-09-23", "eligible": false, "line": "| Cursor | modular-refactor-impl-plan-2026-09-23 | feature/modular-refactor-impl-plan-2026-09-23 | .worktrees/modular-refactor-impl-plan-2026-09-23 | docs-remediation-modular-impl-plan | done |", "mutex": "docs-remediation-modular-impl-plan", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "modular-refactor-impl-plan-2026-09-23", "worktree": ".worktrees/modular-refactor-impl-plan-2026-09-23"}, {"branch": "feature/modular-refactor-review-2026-09-23", "eligible": false, "line": "| Cursor | modular-refactor-review-2026-09-23 | feature/modular-refactor-review-2026-09-23 | .worktrees/modular-refactor-review-2026-09-23 | ops-audit-modular-refactor-review | done |", "mutex": "ops-audit-modular-refactor-review", "owner": "Cursor", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "modular-refactor-review-2026-09-23", "worktree": ".worktrees/modular-refactor-review-2026-09-23"}, {"branch": "feature/status-reconcile-2026-09-22", "eligible": false, "line": "| Cursor | 2026-09-22-status-reconcile | feature/status-reconcile-2026-09-22 | .worktrees/status-reconcile-2026-09-22 | ops-status-reconcile | done |", "mutex": "ops-status-reconcile", "owner": "Cursor", "parsed_date": "2026-09-22", "reason": "within_retain_window", "status": "done", "task": "2026-09-22-status-reconcile", "worktree": ".worktrees/status-reconcile-2026-09-22"}, {"branch": "feature/dev-prod-ready-test-promotion-2026-09-22", "eligible": false, "line": "| Cursor | 2026-09-22-dev-prod-ready-test-promotion | feature/dev-prod-ready-test-promotion-2026-09-22 | .worktrees/dev-prod-ready-test-promotion-2026-09-22 | dev-runtime-migration+release-gate+test-promotion | done |", "mutex": "dev-runtime-migration+release-gate+test-promotion", "owner": "Cursor", "parsed_date": "2026-09-22", "reason": "within_retain_window", "status": "done", "task": "2026-09-22-dev-prod-ready-test-promotion", "worktree": ".worktrees/dev-prod-ready-test-promotion-2026-09-22"}, {"branch": "feature/integrate-dev-prod-ready-test-promotion-2026-09-22", "eligible": false, "line": "| Cursor | integrate-dev-prod-ready-test-promotion-2026-09-22 | feature/integrate-dev-prod-ready-test-promotion-2026-09-22 | .worktrees/integrate-dev-prod-ready-test-promotion-2026-09-22 | git:dev | done |", "mutex": "git:dev", "owner": "Cursor", "parsed_date": "2026-09-22", "reason": "within_retain_window", "status": "done", "task": "integrate-dev-prod-ready-test-promotion-2026-09-22", "worktree": ".worktrees/integrate-dev-prod-ready-test-promotion-2026-09-22"}, {"branch": "feature/promote-test-2026-09-22", "eligible": false, "line": "| Cursor | promote-test-2026-09-22 | feature/promote-test-2026-09-22 | .worktrees/promote-test-2026-09-22 | git:test | done |", "mutex": "git:test", "owner": "Cursor", "parsed_date": "2026-09-22", "reason": "within_retain_window", "status": "done", "task": "promote-test-2026-09-22", "worktree": ".worktrees/promote-test-2026-09-22"}, {"branch": "feature/agent-workflow-governance-plan", "eligible": false, "line": "| Codex | 2026-09-21-agent-workflow-governance-adoption | feature/agent-workflow-governance-plan | (本机 clone 后不存在) | agent-workflow-governance | done |", "mutex": "agent-workflow-governance", "owner": "Codex", "parsed_date": "2026-09-21", "reason": "within_retain_window", "status": "done", "task": "2026-09-21-agent-workflow-governance-adoption", "worktree": "(本机 clone 后不存在)"}, {"branch": "feature/issue-learning-2026-09-23", "eligible": false, "line": "| Codex | issue-learning-2026-09-23 | feature/issue-learning-2026-09-23 | .worktrees/issue-learning-2026-09-23 | issue-learning-docs | done |", "mutex": "issue-learning-docs", "owner": "Codex", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "issue-learning-2026-09-23", "worktree": ".worktrees/issue-learning-2026-09-23"}, {"branch": "feature/systemic-issue-analysis-2026-09-23", "eligible": false, "line": "| Codex | systemic-issue-analysis-2026-09-23 | feature/systemic-issue-analysis-2026-09-23 | .worktrees/systemic-issue-analysis-2026-09-23 | systemic-issue-analysis-docs | done |", "mutex": "systemic-issue-analysis-docs", "owner": "Codex", "parsed_date": "2026-09-23", "reason": "within_retain_window", "status": "done", "task": "systemic-issue-analysis-2026-09-23", "worktree": ".worktrees/systemic-issue-analysis-2026-09-23"}, {"branch": "feature/modular-contract-standard-2026-09-22", "eligible": false, "line": "| Codex | modular-contract-standard-2026-09-22 | feature/modular-contract-standard-2026-09-22 | .worktrees/modular-contract-standard-2026-09-22 | modular-contract-docs | done |", "mutex": "modular-contract-docs", "owner": "Codex", "parsed_date": "2026-09-22", "reason": "within_retain_window", "status": "done", "task": "modular-contract-standard-2026-09-22", "worktree": ".worktrees/modular-contract-standard-2026-09-22"}, {"branch": "feature/herdr-orchestration-v12", "eligible": false, "line": "| Codex | herdr-orchestration-v12 | feature/herdr-orchestration-v12 | .worktrees/herdr-orchestration-v12 | herdr-policy+scripts | done |", "mutex": "herdr-policy+scripts", "owner": "Codex", "parsed_date": null, "reason": "undated_skipped", "status": "done", "task": "herdr-orchestration-v12", "worktree": ".worktrees/herdr-orchestration-v12"}, {"branch": "feature/agent-governance-implementation", "eligible": false, "line": "| Codex | 2026-09-21-agent-governance-implementation | feature/agent-governance-implementation | .worktrees/agent-governance-implementation | agent-governance | done |", "mutex": "agent-governance", "owner": "Codex", "parsed_date": "2026-09-21", "reason": "within_retain_window", "status": "done", "task": "2026-09-21-agent-governance-implementation", "worktree": ".worktrees/agent-governance-implementation"}, {"branch": "feature/force-material-chunk-index-2026-09-21", "eligible": false, "line": "| Cursor | 2026-09-21-force-material-chunk-index | feature/force-material-chunk-index-2026-09-21 | .worktrees/force-material-chunk-index-2026-09-21 | bizdev+materialindex | done |", "mutex": "bizdev+materialindex", "owner": "Cursor", "parsed_date": "2026-09-21", "reason": "within_retain_window", "status": "done", "task": "2026-09-21-force-material-chunk-index", "worktree": ".worktrees/force-material-chunk-index-2026-09-21"}, {"branch": "feature/workflow-scope-weighting-2026-09-21", "eligible": false, "line": "| Cursor | 2026-09-21-workflow-scope-weighting | feature/workflow-scope-weighting-2026-09-21 | .worktrees/workflow-scope-weighting-2026-09-21 | intent+web+docgov | done |", "mutex": "intent+web+docgov", "owner": "Cursor", "parsed_date": "2026-09-21", "reason": "within_retain_window", "status": "done", "task": "2026-09-21-workflow-scope-weighting", "worktree": ".worktrees/workflow-scope-weighting-2026-09-21"}, {"branch": "feature/upload-parse-ux-2026-09-21", "eligible": false, "line": "| Cursor | 2026-09-21-upload-parse-ux-dedup | feature/upload-parse-ux-2026-09-21 | .worktrees/upload-parse-ux-2026-09-21 | server+web+storage | done |", "mutex": "server+web+storage", "owner": "Cursor", "parsed_date": "2026-09-21", "reason": "within_retain_window", "status": "done", "task": "2026-09-21-upload-parse-ux-dedup", "worktree": ".worktrees/upload-parse-ux-2026-09-21"}, {"branch": "feature/mineru-doc-parsing-2026-09-21", "eligible": false, "line": "| Cursor | 2026-09-21-mineru-doc-parsing | feature/mineru-doc-parsing-2026-09-21 | .worktrees/mineru-doc-parsing-2026-09-21 | storage+bizdev+kb | done |", "mutex": "storage+bizdev+kb", "owner": "Cursor", "parsed_date": "2026-09-21", "reason": "within_retain_window", "status": "done", "task": "2026-09-21-mineru-doc-parsing", "worktree": ".worktrees/mineru-doc-parsing-2026-09-21"}, {"branch": "fix/intent-confirm-stage-aware-2026-09-21", "eligible": false, "line": "| Cursor | 2026-09-21-intent-confirm-stage-aware | fix/intent-confirm-stage-aware-2026-09-21 | .worktrees/intent-confirm-stage-aware-2026-09-21 | intent+web+docgov | done |", "mutex": "intent+web+docgov", "owner": "Cursor", "parsed_date": "2026-09-21", "reason": "within_retain_window", "status": "done", "task": "2026-09-21-intent-confirm-stage-aware", "worktree": ".worktrees/intent-confirm-stage-aware-2026-09-21"}, {"branch": "feature/taskrun-status-card-2026-09-21", "eligible": false, "line": "| Cursor | 2026-09-21-taskrun-status-card | feature/taskrun-status-card-2026-09-21 | .worktrees/feature-taskrun-status-card-2026-09-21 | chat+web+taskrun | done |", "mutex": "chat+web+taskrun", "owner": "Cursor", "parsed_date": "2026-09-21", "reason": "within_retain_window", "status": "done", "task": "2026-09-21-taskrun-status-card", "worktree": ".worktrees/feature-taskrun-status-card-2026-09-21"}, {"branch": "feature/agent-workflow-governance-plan", "eligible": false, "line": "| Codex | 2026-09-21-agent-workflow-governance-plan | feature/agent-workflow-governance-plan | .worktrees/feature-agent-workflow-governance-plan | agent-workflow-governance-plan | done |", "mutex": "agent-workflow-governance-plan", "owner": "Codex", "parsed_date": "2026-09-21", "reason": "within_retain_window", "status": "done", "task": "2026-09-21-agent-workflow-governance-plan", "worktree": ".worktrees/feature-agent-workflow-governance-plan"}, {"branch": "fix/workflow-doc-key-unification-2026-09-21", "eligible": false, "line": "| Cursor | 2026-09-21-workflow-doc-key-unification | fix/workflow-doc-key-unification-2026-09-21 | .worktrees/fix-workflow-doc-key-unification-2026-09-21 | docgov+web | done |", "mutex": "docgov+web", "owner": "Cursor", "parsed_date": "2026-09-21", "reason": "within_retain_window", "status": "done", "task": "2026-09-21-workflow-doc-key-unification", "worktree": ".worktrees/fix-workflow-doc-key-unification-2026-09-21"}, {"branch": "fix/dev-multiuser-four-issues-2026-09-21", "eligible": false, "line": "| Cursor | 2026-09-21-dev-multiuser-four-issues | fix/dev-multiuser-four-issues-2026-09-21 | .worktrees/fix-dev-multiuser-four-issues | projectinstance+docgov | done |", "mutex": "projectinstance+docgov", "owner": "Cursor", "parsed_date": "2026-09-21", "reason": "within_retain_window", "status": "done", "task": "2026-09-21-dev-multiuser-four-issues", "worktree": ".worktrees/fix-dev-multiuser-four-issues"}], "status": "dry_run"} diff --git a/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/brief.txt b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/brief.txt new file mode 100644 index 00000000..d0a611f8 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/brief.txt @@ -0,0 +1,16 @@ +=== brief === +# agentctl brief (2026-09-24 13:25) + +- daemon: **ok** manager.lock.held=True queue pending=0 +- tasks: active=1 blocked=0 live runs=0 integrate queue=0 +- cleanup pending: 4 / 4 worktrees +- MATRIX drift hard alerts: 0 + +## Active tasks +- `governance-visibility-p0-2026-09-24` owner=Cursor state=active wt=.worktrees/governance-visibility-p0-2026-09-24 + +## Blockers / alerts +- cleanup backlog: 4 worktrees not removable + +## Integrate queue +- (empty) diff --git a/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/claim-table.txt b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/claim-table.txt new file mode 100644 index 00000000..69b086ba --- /dev/null +++ b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/claim-table.txt @@ -0,0 +1,3 @@ +=== claim list --format table === +claim list count=0 +(no claims) diff --git a/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/cleanup-table.txt b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/cleanup-table.txt new file mode 100644 index 00000000..915fb5f1 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/cleanup-table.txt @@ -0,0 +1,7 @@ +=== cleanup report --format table === +cleanup report total=3 allowed=0 blocked/pending=3 +path status owner reason +---------------------------------------------- ------- ------ ------ +.worktrees/governance-visibility-p0-2026-09-24 blocked Cursor - +.worktrees/promote-rc-beta3-c-2026-09-24 blocked Cursor - +.worktrees/sync-local-test-origin-2026-09-24 blocked Cursor - diff --git a/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/mcp-tools.txt b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/mcp-tools.txt new file mode 100644 index 00000000..60a7953d --- /dev/null +++ b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/mcp-tools.txt @@ -0,0 +1,23 @@ +catalog_drift +catalog_overlap_ack +catalog_overlap_probe +catalog_requirement_add +catalog_search +catalog_status +claim_status +cleanup_report +docs_apply +docs_snapshot +lease_run +quarantine_status +queue_enqueue +queue_list +status +task_close +task_register +validate +--- +has queue_list true +has claim_status true +has cleanup_report true +has catalog_drift true diff --git a/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/queue-table.txt b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/queue-table.txt new file mode 100644 index 00000000..a8c202cb --- /dev/null +++ b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/queue-table.txt @@ -0,0 +1,3 @@ +=== queue list --format table === +queue list count=0 +(queue empty) diff --git a/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/status-table.txt b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/status-table.txt new file mode 100644 index 00000000..22b0e222 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-24-governance-visibility-p0/status-table.txt @@ -0,0 +1,15 @@ +=== status --format table === +agentctl status main=/home/weichen/projects/Agent-System epoch=a994d494-0d9 version=1 + +## tasks active=1 closed=152 total=153 +task owner state worktree branch +----------------------------------- ------ ------ ---------------------------------------------- ------------ +governance-visibility-p0-2026-09-24 Cursor active .worktrees/governance-visibility-p0-2026-09-24 feature/gove + +## runs live=0 +(no live runs) + +## queue depth=0 +(queue empty) + +## policy loaded=ok revision=agent-governance-2026-09-23-fav-constraint-restored diff --git a/scripts/agentctl.py b/scripts/agentctl.py index a1585480..5653c7e4 100755 --- a/scripts/agentctl.py +++ b/scripts/agentctl.py @@ -27,6 +27,7 @@ sys.dont_write_bytecode = True os.environ.setdefault('PYTHONDONTWRITEBYTECODE', '1') import agentctl_w4 as w4 # noqa: E402 import agentctl_catalog as catalog # noqa: E402 +import agentctl_visibility as visibility # noqa: E402 VERSION = 1 POLICY_REL = 'configs/agent-governance.json' @@ -163,6 +164,18 @@ def emit(value, stream=sys.stdout): print(json.dumps(value, ensure_ascii=False, sort_keys=True), file=stream, flush=True) +def emit_payload(payload, fmt='json', renderer=None, stream=sys.stdout): + """Emit JSON (default) or a human table via an optional renderer callable.""" + if (fmt or 'json') == 'table' and renderer is not None: + text = renderer(payload) + if not text.endswith('\n'): + text += '\n' + stream.write(text) + stream.flush() + return + emit(payload, stream) + + def digest(value): return hashlib.sha256(value.encode() if isinstance(value, str) else value).hexdigest() @@ -1781,7 +1794,8 @@ def queue_list(repo, args): states).fetchall() else: rows = db.execute('SELECT * FROM queue_items ORDER BY created ASC').fetchall() - emit({'status': 'ok', 'items': [queue_row(r) for r in rows]}) + payload = {'status': 'ok', 'items': [queue_row(r) for r in rows]} + emit_payload(payload, getattr(args, 'format', 'json'), visibility.render_queue_table) return 0 @@ -2211,7 +2225,8 @@ def cleanup_report(repo, args=None): item['cleanup_owner'] = (active[0]['owner'] if active else (rows[0]['owner'] if rows else None)) record_audit(repo, 'cleanup_report', payload) - emit(payload) + emit_payload(payload, getattr(args, 'format', 'json') if args is not None else 'json', + visibility.render_cleanup_table) return 0 @@ -2457,7 +2472,280 @@ def claim_list(repo, args): "SELECT * FROM claims WHERE state='held' ORDER BY updated DESC").fetchall()] except sqlite3.Error: rows = [] - emit({'status': 'ok', 'claims': rows}) + payload = {'status': 'ok', 'claims': rows} + emit_payload(payload, getattr(args, 'format', 'json'), visibility.render_claim_table) + return 0 + + +def claim_archive(repo, args): + """Migrate done CURRENT claim rows to archive via docs apply @document CAS.""" + task = task_row(repo, args.task) + if repo.current != Path(task['worktree']) or repo.current == repo.main: + deny('WORKTREE_MISMATCH', 'claim archive must run inside the task worktree') + caps = json.loads(task['capabilities']) if isinstance(task['capabilities'], str) else task['capabilities'] + if 'docs:admin' not in caps: + deny('CAPABILITY_REQUIRED', 'claim archive rewrites CURRENT via @document and needs docs:admin') + current_rel = '.doc/ops/CURRENT.md' + archive_rel = '.doc/archive/ops/2026-09-21-current-history.md' + current_path = safe_path(repo.current, current_rel) + content = current_path.read_text(encoding='utf-8') + rows = visibility.parse_current_claim_rows(content) + selected = visibility.select_archive_candidates( + rows, + retain_days=getattr(args, 'retain_days', 7), + include_undated=bool(getattr(args, 'include_undated', False)), + ) + eligible = [r for r in selected if r.get('eligible')] + payload = { + 'status': 'dry_run', + 'retain_days': getattr(args, 'retain_days', 7), + 'include_undated': bool(getattr(args, 'include_undated', False)), + 'done_scanned': len(selected), + 'eligible_count': len(eligible), + 'candidates': eligible, + 'skipped': [r for r in selected if not r.get('eligible')], + 'archive_file': archive_rel, + } + if not getattr(args, 'apply', False): + emit(payload) + return 0 + if not eligible: + emit({**payload, 'status': 'noop', 'message': 'no eligible done rows to archive'}) + return 0 + remove_ids = [r['task'] for r in eligible] + new_content = visibility.build_archived_current(content, remove_ids) + archive_date = time.strftime('%Y-%m-%d', time.localtime()) + section = visibility.build_archive_section(eligible, archive_date) + request = { + 'request_id': f"{args.task}-claim-archive-{archive_date.replace('-', '')}", + 'changes': [{ + 'file': current_rel, + 'record_id': '@document', + 'expected_sha256': digest(content), + 'replacement': new_content, + }], + } + req_path = Path(tempfile.mkstemp(prefix='agentctl-claim-archive-', suffix='.json')[1]) + try: + req_path.write_text(json.dumps(request, ensure_ascii=False, indent=2), encoding='utf-8') + apply_args = argparse.Namespace(task=args.task, request=str(req_path)) + rc = docs_apply(repo, apply_args) + if rc not in (0, None): + return rc + finally: + with contextlib.suppress(OSError): + req_path.unlink() + archive_path = safe_path(repo.current, archive_rel) + with Lease(repo, task, [f'path:{archive_rel}'], f'{args.task}-archive-append', + digest(section)) as lease: + if lease.replay: + emit({'status': 'replayed', 'run_id': lease.request_id}) + return lease.row['exit_code'] or 0 + existing = archive_path.read_text(encoding='utf-8') if archive_path.is_file() else '' + atomic_write(archive_path, existing.rstrip() + '\n' + section) + lease.finish() + emit({ + 'status': 'archived', + 'archived_count': len(eligible), + 'tasks': remove_ids, + 'archive_file': archive_rel, + 'current_sha256': digest(new_content), + }) + return 0 + + +def collect_status_payload(repo): + identity = repo.identity() + policy = policy_status(repo.main) + try: + with repo.db(True) as db: + ensure_schema(db) + except Denied: + pass + with repo.db() as db: + tasks = [dict(r) for r in db.execute('SELECT * FROM tasks')] + runs = [dict(r) for r in db.execute( + "SELECT * FROM runs WHERE state IN ('running','queued','recovery_required','busy','suspect','quarantine')")] + try: + queue = [dict(r) for r in db.execute( + "SELECT * FROM queue_items WHERE state IN ('pending','claimed') ORDER BY priority DESC, created")] + except sqlite3.Error: + queue = [] + for row in runs: + row['supervisor_alive'] = alive_status(row['pid'], row['start'], row['boot']) + return { + 'status': 'ok', + 'lock_root': str(repo.root), + 'main': str(repo.main), + 'version': VERSION, + 'epoch': identity['epoch'], + 'tasks': tasks, + 'runs': runs, + 'queue': queue, + 'policy': policy, + } + + +def collect_cleanup_payload(repo): + import agent_worktree as aw # noqa: E402 + payload = aw.build_cleanup_report(repo.main, repo.main) + with repo.db() as db: + try: + ensure_schema(db) + except Exception: + pass + for item in payload.get('worktrees', []): + path = item.get('path') + if not path: + continue + rows = [dict(r) for r in db.execute( + 'SELECT id,owner,state FROM tasks WHERE worktree=?', (path,)).fetchall()] + item['tasks'] = rows + active = [r for r in rows if r.get('state') == 'active'] + item['cleanup_owner'] = (active[0]['owner'] if active + else (rows[0]['owner'] if rows else None)) + return payload + + +def collect_docs_drift_payload(repo): + """Same logic as docs_drift but returns payload without emitting.""" + matrix_path = repo.main / '.doc/status/FEATURE-MATRIX.md' + if not matrix_path.is_file(): + return {'status': 'error', 'error': 'FEATURE-MATRIX.md missing'} + tip = git(repo.main, 'show', 'refs/heads/dev:.doc/status/FEATURE-MATRIX.md', check=False) + content = tip.stdout if tip.returncode == 0 else matrix_path.read_text() + current = matrix_states(content) + previous = {} + with repo.db(True) as db: + ensure_schema(db) + for row in db.execute('SELECT record_id, status FROM matrix_state_snapshot'): + previous[row['record_id']] = row['status'] + regressions = [] + for record_id, prev in previous.items(): + now = current.get(record_id) + if prev == 'done' and now is not None and now != 'done': + regressions.append({ + 'record_id': record_id, 'previous': prev, 'current': now, + 'severity': 'hard', 'message': 'MATRIX status regressed from done', + }) + elif prev == 'done' and record_id not in current: + regressions.append({ + 'record_id': record_id, 'previous': prev, 'current': None, + 'severity': 'hard', 'message': 'MATRIX done row disappeared', + }) + now_ts = time.time() + db.execute('DELETE FROM matrix_state_snapshot') + for record_id, status in current.items(): + db.execute( + 'INSERT INTO matrix_state_snapshot(record_id, status, updated) VALUES(?,?,?)', + (record_id, status, now_ts)) + open_rows = sorted( + [{'record_id': rid, 'status': st} for rid, st in current.items() + if st not in ('done', 'removed')], + key=lambda item: item['record_id']) + return { + 'status': 'ok', + 'file': '.doc/status/FEATURE-MATRIX.md', + 'record_count': len(current), + 'previous_count': len(previous), + 'regressions': regressions, + 'open_capabilities': open_rows, + 'baseline': 'first_snapshot' if not previous else 'compared', + } + + +def collect_daemon_payload(repo): + try: + identity = repo.identity() + except Denied as exc: + return {'status': 'denied', 'code': exc.code, 'message': exc.message} + lock = manager_lock_status(repo) + heartbeat = read_daemon_heartbeat(repo) + heartbeat_age = None + if heartbeat and isinstance(heartbeat.get('updated'), (int, float)): + heartbeat_age = max(0.0, time.time() - float(heartbeat['updated'])) + pending = claimed = 0 + try: + with repo.db() as db: + try: + pending = db.execute( + "SELECT count(*) FROM queue_items WHERE state='pending'").fetchone()[0] + claimed = db.execute( + "SELECT count(*) FROM queue_items WHERE state='claimed'").fetchone()[0] + except sqlite3.Error: + pending = claimed = 0 + except Denied: + pass + return { + 'status': 'ok', + 'lock_root': str(repo.root), + 'epoch': identity.get('epoch'), + 'manager_lock': lock, + 'heartbeat': heartbeat, + 'heartbeat_age_seconds': heartbeat_age, + 'queue': {'pending': pending, 'claimed': claimed}, + } + + +def collect_catalog_drift_payload(repo): + matrix_path = repo.main / '.doc/status/FEATURE-MATRIX.md' + try: + content = git(repo.main, 'show', 'refs/heads/dev:.doc/status/FEATURE-MATRIX.md') + except Exception: + content = matrix_path.read_text(encoding='utf-8') if matrix_path.is_file() else '' + states = matrix_states(content) if content else {} + with repo.db(True) as db: + return catalog.catalog_drift(db, matrix_states=states) + + +def cmd_brief(repo, args): + """One-page Markdown briefing aggregating status/drift/cleanup/catalog/daemon.""" + status_payload = collect_status_payload(repo) + try: + docs = collect_docs_drift_payload(repo) + except Exception as exc: # noqa: BLE001 + docs = {'status': 'error', 'error': str(exc)} + try: + cleanup = collect_cleanup_payload(repo) + except Exception as exc: # noqa: BLE001 + cleanup = {'status': 'error', 'error': str(exc), 'worktrees': []} + try: + cat = collect_catalog_drift_payload(repo) + except Exception as exc: # noqa: BLE001 + cat = {'status': 'error', 'error': str(exc)} + try: + daemon = collect_daemon_payload(repo) + except Exception as exc: # noqa: BLE001 + daemon = {'status': 'error', 'error': str(exc)} + text = visibility.build_brief( + status=status_payload, + docs_drift=docs, + cleanup=cleanup, + catalog_drift=cat, + daemon=daemon, + ) + if getattr(args, 'format', 'markdown') == 'json': + emit({ + 'status': 'ok', + 'brief': text, + 'sources': { + 'status': { + 'active_tasks': len([ + t for t in (status_payload.get('tasks') or []) + if t.get('state') not in ('done', 'cancelled') + ]), + 'runs': len(status_payload.get('runs') or []), + 'queue': len(status_payload.get('queue') or []), + }, + 'docs_drift': docs, + 'cleanup': {'worktrees': len((cleanup or {}).get('worktrees') or [])}, + 'catalog_drift': cat, + 'daemon': daemon, + }, + }) + else: + sys.stdout.write(text) + sys.stdout.flush() return 0 @@ -2678,7 +2966,12 @@ def daemon_status(repo, args): def parser(): p=argparse.ArgumentParser(description=__doc__);p.add_argument('--repo',default='.') sub=p.add_subparsers(dest='action',required=True) - for name in ('init','status','doctor'):sub.add_parser(name) + for name in ('init','status','doctor'): + sp=sub.add_parser(name) + if name in ('status','doctor'): + sp.add_argument('--format',choices=['json','table'],default='json') + brief=sub.add_parser('brief', help='One-page governance briefing (Markdown)') + brief.add_argument('--format',choices=['markdown','json'],default='markdown') task=sub.add_parser('task').add_subparsers(dest='task_action',required=True) reg=task.add_parser('register');reg.add_argument('--id',required=True);reg.add_argument('--owner',required=True);reg.add_argument('--scope',action='append',required=True);reg.add_argument('--capability',action='append',default=[]) reg.add_argument('--requirement',help='Catalog R-ID to bind at register') @@ -2705,12 +2998,13 @@ def parser(): q=sub.add_parser('queue').add_subparsers(dest='queue_action',required=True) qe=q.add_parser('enqueue');qe.add_argument('--task',required=True);qe.add_argument('--expected-dev',required=True) qe.add_argument('--head');qe.add_argument('--priority',type=int,default=0);qe.add_argument('--request-id') - ql=q.add_parser('list');ql.add_argument('--all',action='store_true') + ql=q.add_parser('list');ql.add_argument('--all',action='store_true');ql.add_argument('--format',choices=['json','table'],default='json') qc=q.add_parser('cancel');qc.add_argument('--task',required=True);qc.add_argument('--id',required=True);qc.add_argument('--reason',required=True) qs=q.add_parser('schedule');qs.add_argument('--drain',action='store_true') cu=sub.add_parser('cleanup').add_subparsers(dest='cleanup_action',required=True) cs=cu.add_parser('status');cs.add_argument('--path');cs.add_argument('--branch') - cu.add_parser('report', help='Batch cleanup readiness for .worktrees (never deletes)') + crep=cu.add_parser('report', help='Batch cleanup readiness for .worktrees (never deletes)') + crep.add_argument('--format',choices=['json','table'],default='json') rec=sub.add_parser('recover');rec.add_argument('--run',required=True);rec.add_argument('--reason',required=True) op=sub.add_parser('operation');op.add_argument('run') for action in ('freeze','unfreeze'): @@ -2735,6 +3029,12 @@ def parser(): crl=cl.add_parser('release');crl.add_argument('--task',required=True);crl.add_argument('--resource',required=True);crl.add_argument('--reason',default='') cst=cl.add_parser('status');cst.add_argument('--resource',required=True);cst.add_argument('--json',action='store_true') clist=cl.add_parser('list');clist.add_argument('--all',action='store_true') + clist.add_argument('--format',choices=['json','table'],default='json') + carc=cl.add_parser('archive', help='Archive done CURRENT claim rows via docs apply') + carc.add_argument('--task',required=True) + carc.add_argument('--retain-days',type=int,default=7) + carc.add_argument('--include-undated',action='store_true') + carc.add_argument('--apply',action='store_true') cc=cl.add_parser('cancel');cc.add_argument('--task',required=True);cc.add_argument('--id',required=True);cc.add_argument('--reason',required=True) qtn=sub.add_parser('quarantine').add_subparsers(dest='quarantine_action',required=True) qtn.add_parser('sweep') @@ -2801,6 +3101,8 @@ def main(): pass elif args.action == 'claim' and getattr(args, 'claim_action', None) in ('status', 'list'): require_policy(repo) # status/list need schema constants but are non-mutating + elif args.action == 'claim' and getattr(args, 'claim_action', None) == 'archive' and not getattr(args, 'apply', False): + require_policy(repo) # dry-run is non-mutating elif args.action == 'quarantine' and getattr(args, 'quarantine_action', None) == 'list': require_policy(repo) # list needs TTL/schema constants elif args.action == 'daemon' and getattr(args, 'daemon_action', None) == 'status': @@ -2817,25 +3119,10 @@ def main(): require_policy(repo) if args.action=='init':initialize(repo) elif args.action in ('status','doctor'): - identity=repo.identity() - policy=policy_status(repo.main) - try: - with repo.db(True) as db: - ensure_schema(db) - except Denied: - pass - with repo.db() as db: - tasks=[dict(r) for r in db.execute('SELECT * FROM tasks')] - runs=[dict(r) for r in db.execute( - "SELECT * FROM runs WHERE state IN ('running','queued','recovery_required','busy','suspect','quarantine')")] - try: - queue=[dict(r) for r in db.execute( - "SELECT * FROM queue_items WHERE state IN ('pending','claimed') ORDER BY priority DESC, created")] - except sqlite3.Error: - queue=[] - for row in runs:row['supervisor_alive']=alive_status(row['pid'],row['start'],row['boot']) - emit({'status':'ok','lock_root':str(repo.root),'main':str(repo.main),'version':VERSION,'epoch':identity['epoch'], - 'tasks':tasks,'runs':runs,'queue':queue,'policy':policy}) + payload = collect_status_payload(repo) + emit_payload(payload, getattr(args, 'format', 'json'), visibility.render_status_table) + elif args.action=='brief': + return cmd_brief(repo, args) elif args.action=='task': if args.task_action=='register': if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]{0,127}',args.id):deny('INVALID_TASK','Invalid task id') @@ -2941,6 +3228,7 @@ def main(): if args.claim_action=='release':return claim_release(repo,args) if args.claim_action=='status':return claim_status(repo,args) if args.claim_action=='list':return claim_list(repo,args) + if args.claim_action=='archive':return claim_archive(repo,args) if args.claim_action=='cancel':return claim_cancel(repo,args) elif args.action=='quarantine': if args.quarantine_action=='sweep':return quarantine_sweep(repo) diff --git a/scripts/agentctl_visibility.py b/scripts/agentctl_visibility.py new file mode 100644 index 00000000..7465293c --- /dev/null +++ b/scripts/agentctl_visibility.py @@ -0,0 +1,411 @@ +#!/usr/bin/env python3 +"""Pure rendering / brief / CURRENT claim-archive helpers for agentctl. + +No I/O against the live repo except via callers. Safe to unit-test in isolation. +""" +from __future__ import annotations + +import datetime as _dt +import re +from typing import Any, Iterable, Optional + +TASK_DATE_RE = re.compile(r'(? str: + """Render a plain monospace table. Empty rows → header-only block.""" + cols = [str(h) for h in headers] + body = [[_cell(v) for v in row] for row in rows] + widths = [len(c) for c in cols] + for row in body: + for i, cell in enumerate(row): + if i < len(widths): + widths[i] = max(widths[i], len(cell)) + else: + widths.append(len(cell)) + while len(row) < len(widths): + row.append('') + def line(cells): + return ' '.join(cell.ljust(widths[i]) for i, cell in enumerate(cells)) + out = [line(cols), line(['-' * w for w in widths])] + out.extend(line(row) for row in body) + return '\n'.join(out) + + +def _cell(value: Any) -> str: + if value is None: + return '-' + text = str(value) + return text.replace('\n', ' ').strip() or '-' + + +def _duration(created: Any, now: Optional[float] = None) -> str: + try: + ts = float(created) + except (TypeError, ValueError): + return '-' + age = (now if now is not None else _dt.datetime.now().timestamp()) - ts + if age < 0: + age = 0 + if age < 60: + return f'{int(age)}s' + if age < 3600: + return f'{int(age // 60)}m' + if age < 86400: + return f'{int(age // 3600)}h' + return f'{int(age // 86400)}d' + + +def render_status_table(payload: dict, *, now: Optional[float] = None) -> str: + """Human table for `agentctl status` payload.""" + lines = [ + f"agentctl status main={payload.get('main', '-')} " + f"epoch={_short(payload.get('epoch'))} version={payload.get('version', '-')}", + '', + ] + tasks = payload.get('tasks') or [] + active = [t for t in tasks if t.get('state') not in ('done', 'cancelled')] + terminal = [t for t in tasks if t.get('state') in ('done', 'cancelled')] + lines.append(f'## tasks active={len(active)} closed={len(terminal)} total={len(tasks)}') + task_rows = [ + [ + t.get('id'), + t.get('owner'), + t.get('state'), + _short_path(t.get('worktree')), + _short(t.get('branch')), + ] + for t in active + ] + lines.append(format_table(['task', 'owner', 'state', 'worktree', 'branch'], task_rows) if task_rows + else '(no active tasks)') + lines.append('') + runs = payload.get('runs') or [] + lines.append(f'## runs live={len(runs)}') + run_rows = [ + [ + r.get('id'), + r.get('task'), + r.get('state'), + r.get('supervisor_alive'), + _short(r.get('kind') or r.get('action')), + ] + for r in runs + ] + lines.append(format_table(['run', 'task', 'state', 'alive', 'kind'], run_rows) if run_rows + else '(no live runs)') + lines.append('') + queue = payload.get('queue') or [] + lines.append(f'## queue depth={len(queue)}') + q_rows = [ + [ + q.get('id'), + q.get('task'), + q.get('state'), + q.get('priority'), + _short(q.get('head_sha')), + _duration(q.get('created'), now), + ] + for q in queue + ] + lines.append(format_table(['id', 'task', 'state', 'pri', 'head', 'age'], q_rows) if q_rows + else '(queue empty)') + policy = payload.get('policy') or {} + lines.append('') + lines.append( + f"## policy loaded={policy.get('loaded', policy.get('status', '-'))} " + f"revision={policy.get('policy_revision') or policy.get('revision') or '-'}" + ) + return '\n'.join(lines) + '\n' + + +def render_queue_table(payload: dict, *, now: Optional[float] = None) -> str: + items = payload.get('items') or [] + rows = [ + [ + i.get('id'), + i.get('task'), + i.get('state'), + i.get('priority'), + _short(i.get('head_sha')), + _short(i.get('expected_dev')), + _duration(i.get('created'), now), + ] + for i in items + ] + header = f"queue list count={len(items)}\n" + body = format_table(['id', 'task', 'state', 'pri', 'head', 'expected_dev', 'age'], rows) if rows \ + else '(queue empty)' + return header + body + '\n' + + +def render_claim_table(payload: dict, *, now: Optional[float] = None) -> str: + claims = payload.get('claims') or [] + rows = [ + [ + c.get('id') or c.get('resource'), + c.get('task'), + c.get('resource'), + c.get('state'), + _duration(c.get('updated') or c.get('created'), now), + ] + for c in claims + ] + header = f"claim list count={len(claims)}\n" + body = format_table(['id', 'task', 'resource', 'state', 'age'], rows) if rows else '(no claims)' + return header + body + '\n' + + +def render_cleanup_table(payload: dict) -> str: + trees = payload.get('worktrees') or [] + ready = [t for t in trees if t.get('cleanup_allowed') or t.get('status') == 'allowed'] + blocked = [t for t in trees if t not in ready] + rows = [] + for t in trees: + status = 'allowed' if (t.get('cleanup_allowed') or t.get('status') == 'allowed') else 'blocked' + reason = t.get('blocked_reason') or t.get('reason') or '' + if not reason and isinstance(t.get('gates'), list): + fails = [g.get('name') for g in t['gates'] if g.get('status') != 'pass'] + reason = ','.join(fails[:3]) + rows.append([ + _short_path(t.get('path') or t.get('worktree')), + status, + t.get('cleanup_owner') or '-', + reason or '-', + ]) + header = ( + f"cleanup report total={len(trees)} allowed={len(ready)} " + f"blocked/pending={len(blocked)}\n" + ) + body = format_table(['path', 'status', 'owner', 'reason'], rows) if rows else '(no worktrees)' + return header + body + '\n' + + +def build_brief( + *, + status: Optional[dict] = None, + docs_drift: Optional[dict] = None, + cleanup: Optional[dict] = None, + catalog_drift: Optional[dict] = None, + daemon: Optional[dict] = None, + now: Optional[float] = None, +) -> str: + """Aggregate read-only sources into a one-page Markdown briefing.""" + status = status or {} + docs_drift = docs_drift or {} + cleanup = cleanup or {} + catalog_drift = catalog_drift or {} + daemon = daemon or {} + tasks = status.get('tasks') or [] + active = [t for t in tasks if t.get('state') not in ('done', 'cancelled')] + blocked = [t for t in active if t.get('state') == 'blocked'] + runs = status.get('runs') or [] + queue = status.get('queue') or [] + trees = cleanup.get('worktrees') or [] + pending_cleanup = [ + t for t in trees + if not (t.get('cleanup_allowed') or t.get('status') == 'allowed') + ] + regressions = docs_drift.get('regressions') or catalog_drift.get('regressions') or [] + hard = [r for r in regressions if r.get('severity') == 'hard' or 'done' in str(r.get('message', ''))] + stamp = _dt.datetime.fromtimestamp(now or _dt.datetime.now().timestamp()).strftime('%Y-%m-%d %H:%M') + lines = [ + f'# agentctl brief ({stamp})', + '', + f"- daemon: **{daemon.get('status', 'unknown')}** " + f"manager.lock.held={((daemon.get('manager_lock') or {}).get('held'))} " + f"queue pending={((daemon.get('queue') or {}).get('pending', '-'))}", + f"- tasks: active={len(active)} blocked={len(blocked)} " + f"live runs={len(runs)} integrate queue={len(queue)}", + f"- cleanup pending: {len(pending_cleanup)} / {len(trees)} worktrees", + f"- MATRIX drift hard alerts: {len(hard)}", + '', + '## Active tasks', + ] + if active: + for t in active[:20]: + lines.append( + f"- `{t.get('id')}` owner={t.get('owner')} state={t.get('state')} " + f"wt={_short_path(t.get('worktree'))}" + ) + if len(active) > 20: + lines.append(f'- … and {len(active) - 20} more') + else: + lines.append('- (none)') + lines.append('') + lines.append('## Blockers / alerts') + if blocked: + for t in blocked: + lines.append(f"- blocked task `{t.get('id')}`") + if hard: + for r in hard[:10]: + lines.append( + f"- MATRIX regression `{r.get('record_id')}`: " + f"{r.get('previous')}→{r.get('current')}" + ) + if pending_cleanup: + lines.append(f"- cleanup backlog: {len(pending_cleanup)} worktrees not removable") + if not blocked and not hard and not pending_cleanup: + lines.append('- (none)') + lines.append('') + lines.append('## Integrate queue') + if queue: + for q in queue[:10]: + lines.append( + f"- `{q.get('task')}` state={q.get('state')} pri={q.get('priority')} " + f"head={_short(q.get('head_sha'))}" + ) + else: + lines.append('- (empty)') + return '\n'.join(lines) + '\n' + + +def parse_current_claim_rows(content: str) -> list[dict]: + """Extract parallel-claim table rows from CURRENT.md body.""" + section = _claim_section_body(content) + if section is None: + return [] + rows = [] + for line in section.splitlines(): + stripped = line.strip() + if not stripped.startswith('|'): + continue + cells = [c.strip() for c in stripped.strip('|').split('|')] + if not cells or len(cells) < 6: + continue + if cells[0].casefold() in ('owner', '---') or set(cells[0]) <= {'-', ':'}: + continue + if all(re.fullmatch(r':?-+:?', c.replace(' ', '') or '-') for c in cells): + continue + rows.append({ + 'owner': cells[0], + 'task': cells[1], + 'branch': cells[2], + 'worktree': cells[3], + 'mutex': cells[4], + 'status': cells[5], + 'line': stripped, + }) + return rows + + +def parse_task_date(task_id: str) -> Optional[_dt.date]: + match = TASK_DATE_RE.search(task_id or '') + if not match: + return None + try: + return _dt.date(int(match.group(1)), int(match.group(2)), int(match.group(3))) + except ValueError: + return None + + +def select_archive_candidates( + rows: list[dict], + *, + retain_days: int = 7, + today: Optional[_dt.date] = None, + include_undated: bool = False, +) -> list[dict]: + """Pick status=done rows older than retain_days (by date embedded in task id).""" + today = today or _dt.date.today() + cutoff = today - _dt.timedelta(days=max(0, int(retain_days))) + selected = [] + for row in rows: + if (row.get('status') or '').strip() != 'done': + continue + parsed = parse_task_date(row.get('task') or '') + entry = dict(row) + entry['parsed_date'] = parsed.isoformat() if parsed else None + if parsed is None: + entry['eligible'] = bool(include_undated) + entry['reason'] = 'undated' if include_undated else 'undated_skipped' + selected.append(entry) + continue + if parsed <= cutoff: + entry['eligible'] = True + entry['reason'] = f'older_than_{retain_days}d' + else: + entry['eligible'] = False + entry['reason'] = 'within_retain_window' + selected.append(entry) + return selected + + +def build_archived_current(content: str, remove_task_ids: Iterable[str]) -> str: + """Return CURRENT.md with listed claim rows removed from the parallel table.""" + remove = set(remove_task_ids) + start, end = _claim_section_span(content) + if start is None: + raise ValueError('CURRENT.md missing ## 并行认领 section') + lines = content.splitlines(keepends=True) + kept = [] + for line in lines[start:end]: + stripped = line.strip() + if stripped.startswith('|'): + cells = [c.strip() for c in stripped.strip('|').split('|')] + if len(cells) >= 2 and cells[1] in remove: + continue + kept.append(line) + return ''.join(lines[:start] + kept + lines[end:]) + + +def build_archive_section(rows: list[dict], archive_date: str) -> str: + """Markdown fragment to append to the CURRENT history archive.""" + lines = [ + '', + f'## 并行认领归档 {archive_date}', + '', + f'由 `agentctl claim archive` 从 CURRENT 迁出(status=done)。共 {len(rows)} 行。', + '', + CLAIM_HEADER, + '|---|---|---|---|---|---|', + ] + for row in rows: + lines.append( + f"| {row.get('owner', '')} | {row.get('task', '')} | {row.get('branch', '')} | " + f"{row.get('worktree', '')} | {row.get('mutex', '')} | {row.get('status', '')} |" + ) + lines.append('') + return '\n'.join(lines) + + +def _claim_section_body(content: str) -> Optional[str]: + start, end = _claim_section_span(content) + if start is None: + return None + return ''.join(content.splitlines(keepends=True)[start:end]) + + +def _claim_section_span(content: str) -> tuple[Optional[int], Optional[int]]: + lines = content.splitlines(keepends=True) + start = None + for i, line in enumerate(lines): + if line.startswith(CLAIM_SECTION): + start = i + break + if start is None: + return None, None + end = len(lines) + for j in range(start + 1, len(lines)): + if lines[j].startswith('## '): + end = j + break + return start, end + + +def _short(value: Any, n: int = 12) -> str: + if value is None: + return '-' + text = str(value) + return text if len(text) <= n else text[:n] + + +def _short_path(value: Any) -> str: + if not value: + return '-' + text = str(value).replace('\\', '/') + if '/.worktrees/' in text: + return '.worktrees/' + text.split('/.worktrees/', 1)[1] + return text if len(text) <= 48 else '…' + text[-47:] diff --git a/scripts/mcp/agentctl-mcp.mjs b/scripts/mcp/agentctl-mcp.mjs index 70c75c49..d05a218e 100755 --- a/scripts/mcp/agentctl-mcp.mjs +++ b/scripts/mcp/agentctl-mcp.mjs @@ -222,6 +222,46 @@ const TOOLS = [ description: 'Catalog table counts and gate/vector config summary', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, }, + { + name: 'queue_list', + description: 'List integrate queue items (pending/claimed by default)', + inputSchema: { + type: 'object', + properties: { + all: { type: 'boolean', description: 'include terminal queue items' }, + format: { type: 'string', enum: ['json', 'table'] }, + }, + additionalProperties: false, + }, + }, + { + name: 'claim_status', + description: 'List held claims (or all with all=true)', + inputSchema: { + type: 'object', + properties: { + all: { type: 'boolean' }, + format: { type: 'string', enum: ['json', 'table'] }, + }, + additionalProperties: false, + }, + }, + { + name: 'cleanup_report', + description: 'Batch cleanup readiness for .worktrees (never deletes)', + inputSchema: { + type: 'object', + properties: { + format: { type: 'string', enum: ['json', 'table'] }, + }, + additionalProperties: false, + }, + }, + { + name: 'catalog_drift', + description: 'Refresh catalog features from MATRIX; report done→non-done regressions', + inputSchema: { type: 'object', properties: {}, additionalProperties: false }, + }, ]; function callTool(name, args = {}) { @@ -332,6 +372,30 @@ function callTool(name, args = {}) { const r = runAgentctl(['catalog', 'status']); return r.ok ? textResult(r.stdout.trim()) : errResult(r.stderr || r.stdout || r.error); } + case 'queue_list': { + const argv = ['queue', 'list']; + if (args.all) argv.push('--all'); + if (args.format) argv.push('--format', args.format); + const r = runAgentctl(argv); + return r.ok ? textResult(r.stdout.trim() || '{"items":[]}') : errResult(r.stderr || r.stdout || r.error); + } + case 'claim_status': { + const argv = ['claim', 'list']; + if (args.all) argv.push('--all'); + if (args.format) argv.push('--format', args.format); + const r = runAgentctl(argv); + return r.ok ? textResult(r.stdout.trim() || '{"claims":[]}') : errResult(r.stderr || r.stdout || r.error); + } + case 'cleanup_report': { + const argv = ['cleanup', 'report']; + if (args.format) argv.push('--format', args.format); + const r = runAgentctl(argv, { timeout: 180000 }); + return r.ok ? textResult(r.stdout.trim()) : errResult(r.stderr || r.stdout || r.error); + } + case 'catalog_drift': { + const r = runAgentctl(['catalog', 'drift']); + return r.ok ? textResult(r.stdout.trim()) : errResult(r.stderr || r.stdout || r.error); + } default: return errResult(`unknown tool: ${name}`); } diff --git a/scripts/tests/test_agent_worktree.py b/scripts/tests/test_agent_worktree.py index f0578e31..03f6a20b 100644 --- a/scripts/tests/test_agent_worktree.py +++ b/scripts/tests/test_agent_worktree.py @@ -24,7 +24,7 @@ class WorktreeTest(unittest.TestCase): self.root = Path(self.tmp.name) / "repo" self.root.mkdir() (self.root / "scripts").mkdir() - for name in ("agent-worktree.sh", "agent_worktree.py", "agentctl.py", "agentctl", "agentctl_w4.py", "agentctl_catalog.py"): + for name in ("agent-worktree.sh", "agent_worktree.py", "agentctl.py", "agentctl", "agentctl_w4.py", "agentctl_catalog.py", "agentctl_visibility.py"): shutil.copy2(SOURCE / name, self.root / "scripts" / name) (self.root / "configs").mkdir(parents=True, exist_ok=True) shutil.copy2(SOURCE.parent / "configs" / "agent-governance.json", diff --git a/scripts/tests/test_agentctl.py b/scripts/tests/test_agentctl.py index 65e0c55b..2e07f9ed 100644 --- a/scripts/tests/test_agentctl.py +++ b/scripts/tests/test_agentctl.py @@ -38,6 +38,7 @@ class AgentctlTests(unittest.TestCase): shutil.copy2(SOURCE, self.root / "scripts" / "agentctl.py") shutil.copy2(SOURCE.parent / "agentctl_w4.py", self.root / "scripts" / "agentctl_w4.py") shutil.copy2(SOURCE.parent / "agentctl_catalog.py", self.root / "scripts" / "agentctl_catalog.py") + shutil.copy2(SOURCE.parent / "agentctl_visibility.py", self.root / "scripts" / "agentctl_visibility.py") install_governance_policy(self.root) (self.root / ".gitignore").write_text(".locks/\n.worktrees/\n", encoding="utf-8") for directory in ("src/a", "src/b", ".doc/ops", ".doc/status"): @@ -1553,6 +1554,7 @@ class ProcessPlatformTests(unittest.TestCase): shutil.copy2(SOURCE, root / "scripts" / "agentctl.py") shutil.copy2(SOURCE.parent / "agentctl_w4.py", root / "scripts" / "agentctl_w4.py") shutil.copy2(SOURCE.parent / "agentctl_catalog.py", root / "scripts" / "agentctl_catalog.py") + shutil.copy2(SOURCE.parent / "agentctl_visibility.py", root / "scripts" / "agentctl_visibility.py") install_governance_policy(root) (root / ".gitignore").write_text(".locks/\n.worktrees/\n", encoding="utf-8") (root / ".doc/ops").mkdir(parents=True) diff --git a/scripts/tests/test_agentctl_catalog.py b/scripts/tests/test_agentctl_catalog.py index 35a735c2..713cfc6f 100644 --- a/scripts/tests/test_agentctl_catalog.py +++ b/scripts/tests/test_agentctl_catalog.py @@ -33,6 +33,9 @@ class CatalogTests(unittest.TestCase): shutil.copy2(SOURCE, self.root / "scripts" / "agentctl.py") shutil.copy2(SOURCE.parent / "agentctl_w4.py", self.root / "scripts" / "agentctl_w4.py") shutil.copy2(CATALOG_SOURCE, self.root / "scripts" / "agentctl_catalog.py") + vis = SOURCE.parent / "agentctl_visibility.py" + if vis.exists(): + shutil.copy2(vis, self.root / "scripts" / "agentctl_visibility.py") install_governance_policy(self.root) (self.root / ".gitignore").write_text(".locks/\n.worktrees/\n", encoding="utf-8") for directory in ("src/a", "src/b", ".doc/ops", ".doc/status"): diff --git a/scripts/tests/test_agentctl_visibility.py b/scripts/tests/test_agentctl_visibility.py new file mode 100644 index 00000000..6a9fc4d9 --- /dev/null +++ b/scripts/tests/test_agentctl_visibility.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""Unit tests for agentctl visibility helpers (table/brief/claim-archive).""" + +import datetime as dt +import pathlib +import sys +import unittest + +SCRIPTS = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SCRIPTS)) +sys.dont_write_bytecode = True + +import agentctl_visibility as vis # noqa: E402 + + +SAMPLE_CURRENT = """# 当前焦点 + +## 焦点 + +1. something + +## 并行认领 + +| owner | task | branch | worktree | mutex | status | +|---|---|---|---|---|---| +| Cursor | keep-active-2026-09-24 | feature/keep | .worktrees/keep | m1 | in_progress | +| Cursor | recent-done-2026-09-23 | feature/recent | .worktrees/recent | m2 | done | +| Codex | old-done-2026-09-10 | feature/old | .worktrees/old | m3 | done | +| Cursor | undated-done | feature/undated | .worktrees/u | m4 | done | + +## 集成槽位 + +| holder | phase | +|---|---| +| - | idle | +""" + + +class FormatTableTests(unittest.TestCase): + def test_format_table_aligns_columns(self): + text = vis.format_table(['a', 'bb'], [['1', '22'], ['333', '4']]) + lines = text.splitlines() + self.assertEqual(len(lines), 4) + self.assertIn('a', lines[0]) + self.assertTrue(lines[1].startswith('-')) + + def test_render_status_table_active_only(self): + payload = { + 'main': '/repo', + 'epoch': 'abc-epoch', + 'version': 1, + 'tasks': [ + {'id': 't1', 'owner': 'A', 'state': 'active', 'worktree': '/repo/.worktrees/t1', 'branch': 'feature/t1'}, + {'id': 't2', 'owner': 'B', 'state': 'done', 'worktree': '/repo/.worktrees/t2', 'branch': 'feature/t2'}, + ], + 'runs': [], + 'queue': [{'id': 'q1', 'task': 't1', 'state': 'pending', 'priority': 1, 'head_sha': 'deadbeef', 'created': 0}], + 'policy': {'loaded': 'ok', 'policy_revision': 'r1'}, + } + text = vis.render_status_table(payload, now=100.0) + self.assertIn('active=1', text) + self.assertIn('t1', text) + self.assertNotIn('t2', text.split('## runs')[0]) # closed not in active table + self.assertIn('queue depth=1', text) + + +class BriefTests(unittest.TestCase): + def test_build_brief_markdown(self): + text = vis.build_brief( + status={ + 'tasks': [{'id': 't1', 'owner': 'A', 'state': 'active', 'worktree': '/x/.worktrees/t1'}], + 'runs': [], + 'queue': [], + }, + docs_drift={'regressions': []}, + cleanup={'worktrees': [{'path': 'a', 'cleanup_allowed': False}]}, + catalog_drift={'regressions': []}, + daemon={'status': 'ok', 'manager_lock': {'held': True}, 'queue': {'pending': 0}}, + now=1_700_000_000, + ) + self.assertTrue(text.startswith('# agentctl brief')) + self.assertIn('daemon: **ok**', text) + self.assertIn('`t1`', text) + self.assertIn('cleanup backlog: 1', text) + + +class ClaimArchiveTests(unittest.TestCase): + def test_parse_current_claim_rows(self): + rows = vis.parse_current_claim_rows(SAMPLE_CURRENT) + self.assertEqual(len(rows), 4) + self.assertEqual(rows[0]['task'], 'keep-active-2026-09-24') + self.assertEqual(rows[2]['status'], 'done') + + def test_parse_task_date(self): + self.assertEqual(vis.parse_task_date('foo-2026-09-10'), dt.date(2026, 9, 10)) + self.assertEqual(vis.parse_task_date('2026-09-10-bar'), dt.date(2026, 9, 10)) + self.assertIsNone(vis.parse_task_date('no-date-here')) + + def test_select_archive_candidates_respects_retain_window(self): + rows = vis.parse_current_claim_rows(SAMPLE_CURRENT) + selected = vis.select_archive_candidates( + rows, retain_days=7, today=dt.date(2026, 9, 24), include_undated=False) + eligible = [r for r in selected if r['eligible']] + self.assertEqual([r['task'] for r in eligible], ['old-done-2026-09-10']) + skipped_reasons = {r['task']: r['reason'] for r in selected if not r['eligible']} + self.assertEqual(skipped_reasons['recent-done-2026-09-23'], 'within_retain_window') + self.assertEqual(skipped_reasons['undated-done'], 'undated_skipped') + + def test_select_include_undated(self): + rows = vis.parse_current_claim_rows(SAMPLE_CURRENT) + selected = vis.select_archive_candidates( + rows, retain_days=0, today=dt.date(2026, 9, 24), include_undated=True) + eligible_ids = {r['task'] for r in selected if r['eligible']} + self.assertIn('undated-done', eligible_ids) + self.assertIn('old-done-2026-09-10', eligible_ids) + self.assertIn('recent-done-2026-09-23', eligible_ids) + self.assertNotIn('keep-active-2026-09-24', eligible_ids) + + def test_build_archived_current_removes_rows(self): + out = vis.build_archived_current(SAMPLE_CURRENT, ['old-done-2026-09-10', 'undated-done']) + self.assertNotIn('old-done-2026-09-10', out) + self.assertNotIn('undated-done', out) + self.assertIn('keep-active-2026-09-24', out) + self.assertIn('## 集成槽位', out) + self.assertIn('## 焦点', out) + + def test_build_archive_section(self): + rows = [{'owner': 'A', 'task': 't', 'branch': 'b', 'worktree': 'w', 'mutex': 'm', 'status': 'done'}] + section = vis.build_archive_section(rows, '2026-09-24') + self.assertIn('## 并行认领归档 2026-09-24', section) + self.assertIn('| A | t | b | w | m | done |', section) + + +class QueueClaimCleanupRenderTests(unittest.TestCase): + def test_render_queue_and_claims(self): + q = vis.render_queue_table({'items': [ + {'id': '1', 'task': 't', 'state': 'pending', 'priority': 0, + 'head_sha': 'abc', 'expected_dev': 'def', 'created': 0}, + ]}, now=10) + self.assertIn('queue list count=1', q) + c = vis.render_claim_table({'claims': [ + {'id': 'c1', 'task': 't', 'resource': 'claim:x', 'state': 'held', 'updated': 0}, + ]}, now=10) + self.assertIn('claim list count=1', c) + + def test_render_cleanup(self): + text = vis.render_cleanup_table({'worktrees': [ + {'path': '/repo/.worktrees/a', 'cleanup_allowed': True, 'cleanup_owner': 'X'}, + {'path': '/repo/.worktrees/b', 'status': 'blocked', 'gates': [ + {'name': 'dev_ancestor', 'status': 'fail'}, + ]}, + ]}) + self.assertIn('allowed=1', text) + self.assertIn('blocked/pending=1', text) + self.assertIn('dev_ancestor', text) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/tests/test_codegraph_governance.py b/scripts/tests/test_codegraph_governance.py index 0387b78d..036b68d4 100644 --- a/scripts/tests/test_codegraph_governance.py +++ b/scripts/tests/test_codegraph_governance.py @@ -21,7 +21,8 @@ class CodegraphGovernanceTest(unittest.TestCase): self.root.mkdir() (self.root / 'scripts').mkdir() for name in ('agent-worktree.sh', 'agent_worktree.py', 'agentctl.py', - 'agentctl', 'agentctl_w4.py', 'agentctl_catalog.py'): + 'agentctl', 'agentctl_w4.py', 'agentctl_catalog.py', + 'agentctl_visibility.py'): shutil.copy2(SOURCE / name, self.root / 'scripts' / name) (self.root / 'configs').mkdir(parents=True, exist_ok=True) shutil.copy2(SOURCE.parent / 'configs' / 'agent-governance.json', diff --git a/scripts/tests/test_ops_guards.py b/scripts/tests/test_ops_guards.py index 4bad4894..1b20f59c 100644 --- a/scripts/tests/test_ops_guards.py +++ b/scripts/tests/test_ops_guards.py @@ -19,7 +19,7 @@ class OpsGuardsTest(unittest.TestCase): self.root = Path(self.tmp.name) / "repo" self.root.mkdir() (self.root / "scripts").mkdir() - for name in ("env-service.sh", "candidate-images-build.sh", "agentctl.py", "agentctl", "agentctl_w4.py", "agentctl_catalog.py"): + for name in ("env-service.sh", "candidate-images-build.sh", "agentctl.py", "agentctl", "agentctl_w4.py", "agentctl_catalog.py", "agentctl_visibility.py"): shutil.copy2(SOURCE / name, self.root / "scripts" / name) (self.root / ".gitignore").write_text(".worktrees/\n.locks/\nconfigs/.env.*\n!configs/.env.*.example\ndata/\n") (self.root / ".dockerignore").write_text(".doc\n.locks\n.worktrees\n") diff --git a/scripts/tests/test_pi_subagents.py b/scripts/tests/test_pi_subagents.py index e1ba8c9d..989704d1 100644 --- a/scripts/tests/test_pi_subagents.py +++ b/scripts/tests/test_pi_subagents.py @@ -149,6 +149,9 @@ class PiSubagentsTests(unittest.TestCase): catalog_src = ROOT / "scripts" / "agentctl_catalog.py" if catalog_src.exists(): shutil.copy2(catalog_src, self.root / "scripts" / "agentctl_catalog.py") + vis_src = ROOT / "scripts" / "agentctl_visibility.py" + if vis_src.exists(): + shutil.copy2(vis_src, self.root / "scripts" / "agentctl_visibility.py") if SOURCE_EXEC.exists(): shutil.copy2(SOURCE_EXEC, self.root / "scripts" / "pi_subagents.py") if SOURCE_MCP.exists(): From f29a69b8a17fbf95d3208a04579799d0126e759f Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 24 Sep 2026 13:26:07 +0000 Subject: [PATCH 2/2] docs(ops): claim CURRENT row for governance-visibility-p0 --- .doc/ops/CURRENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 01256641..b56d6a50 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -18,6 +18,7 @@ | owner | task | branch | worktree | mutex | status | |---|---|---|---|---|---| +| Cursor | governance-visibility-p0-2026-09-24 | feature/governance-visibility-p0-2026-09-24 | .worktrees/governance-visibility-p0-2026-09-24 | agentctl-visibility | in_progress | | Cursor | worktree-cleanup-closeout-2026-09-24 | feature/worktree-cleanup-closeout-2026-09-24 | .worktrees/worktree-cleanup-closeout-2026-09-24 | worktree-cleanup | in_progress | | Cursor | non-author-closeout-2026-09-24 | feature/non-author-closeout-2026-09-24 | .worktrees/non-author-closeout-2026-09-24 | docs+git:dev | done | | Cursor | integrate-non-author-docs-2026-09-24 | feature/integrate-non-author-docs-2026-09-24 | .worktrees/integrate-non-author-docs-2026-09-24 | git:dev | done |