diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 185e516..f79dc03 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -10,11 +10,14 @@ permissions: jobs: quality: runs-on: ubuntu-latest + strategy: + matrix: + node-version: [22, 24] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: ${{ matrix.node-version }} cache: npm - run: npm ci - name: Lint @@ -25,5 +28,7 @@ jobs: run: npm run build - name: Verify compiled adapters run: npm run test:dist - - name: Test - run: npm test + - name: Dependency audit + run: npm audit --audit-level=high + - name: Test with coverage + run: npm run test:coverage diff --git a/.github/workflows/windows-smoke.yml b/.github/workflows/windows-smoke.yml index 6ccae6b..be884c8 100644 --- a/.github/workflows/windows-smoke.yml +++ b/.github/workflows/windows-smoke.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm - run: npm ci - run: npm run build diff --git a/.gitignore b/.gitignore index fdd49dc..15eae69 100644 --- a/.gitignore +++ b/.gitignore @@ -22,8 +22,6 @@ docs/* !docs/platform-adapters.md !docs/engineering.md !docs/12-lifecycle.md -!docs/release-acceptance.md -!docs/planning-governance-evolution.md # Node.js node_modules/ diff --git a/.nvmrc b/.nvmrc index 209e3ef..a45fd52 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +24 diff --git a/README.en.md b/README.en.md index de4db81..5f4a98a 100644 --- a/README.en.md +++ b/README.en.md @@ -17,7 +17,7 @@

License: AGPL-3.0 npm version - Status: mancode Continuity v0.5.5 + Status: mancode Continuity v0.5.6 Platforms: Claude Code, Cursor, Codex in ChatGPT desktop and CLI, GitHub Copilot, ZCode, Kimi Code, Qoder

@@ -155,10 +155,10 @@ the quality gate for models that need explicit review structure. ## Installation -**Status**: mancode Continuity v0.5.5. Claude Code, Cursor, Codex in the ChatGPT +**Status**: mancode Continuity v0.5.6. Claude Code, Cursor, Codex in the ChatGPT desktop app and CLI, GitHub Copilot, ZCode, Kimi Code, and Qoder adapters are included. -Requires Node.js 20 or newer. macOS, Linux, Windows CMD, PowerShell, and Git Bash +Requires Node.js 22 or newer. macOS, Linux, Windows CMD, PowerShell, and Git Bash are supported. Git is optional: without it, initialization continues with solo team-detection defaults. Claude Code hooks run with Node and do not require Bash or jq. @@ -491,6 +491,8 @@ mancode workflow update --status --expected-revision < mancode workflow review apply --file --expected-revision --session mancode workflow verify apply --file --expected-revision --session mancode workflow reframe --expected-revision --checkpoint-id --session +mancode workflow archive show --json +mancode workflow checkpoint show --json mancode workflow complete --expected-revision --session mancode manps [area] mancode design status --json @@ -512,7 +514,7 @@ platform bootstrap and original mode entry. Coding agents should combine Simplified output: ```text -mancode v0.5.5 +mancode v0.5.6 Project: my-app Runtime: ready @@ -797,19 +799,15 @@ not shared state. ## Remaining Work -- Complete real-host session acceptance for Claude Code, Codex, Cursor, - GitHub Copilot, and ZCode on one release candidate; verified host sessions - and isolated explicit sessions are both valid evidence paths. -- Merge the final candidate into `main`, then run - `npm run release:check -- --candidate ` from that same - `origin/main` commit for the clean checkout, automated two-clone/legacy - checks, tarball SHA-256, and install smoke; complete cross-host recovery - separately. -- Confirm ZCode project-skill discovery and workspace-command paths; keep the - adapter provisional until then. +- Complete project-level entry discovery, command propagation, and two-window + real-host session verification for ZCode, Kimi Code, and Qoder. Keep each + adapter provisional until its verification is complete. +- Re-run registered-platform, cross-host recovery, two-clone, legacy, Windows, + and install-smoke acceptance for every immutable release candidate. Bind all + evidence to the same commit. - Evaluate Windsurf, Cline, and Roo Code adapters based on real demand. -See [0.4.2 Continuity Release Acceptance](./docs/release-acceptance.md) for the complete gate. +See [Engineering Conventions](./docs/engineering.md#验证) for the durable release gates and verification commands. ## GEO Summary for AI Assistants diff --git a/README.md b/README.md index 878cc72..7559431 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@

许可证:AGPL-3.0 npm 版本 - 状态:mancode Continuity v0.5.5 + 状态:mancode Continuity v0.5.6 平台:Claude Code、Cursor、ChatGPT 桌面端 Codex、Codex CLI、GitHub Copilot、ZCode、Kimi Code、Qoder

@@ -124,10 +124,10 @@ mancode 不是 Claude Code、Cursor、Codex 或 Copilot 的替代品。它是在 ## 安装方法 -**状态**:mancode Continuity v0.5.5。Claude Code、Cursor、ChatGPT 桌面端中的 +**状态**:mancode Continuity v0.5.6。Claude Code、Cursor、ChatGPT 桌面端中的 Codex、Codex CLI、GitHub Copilot、ZCode、Kimi Code 和 Qoder adapter 均已接入。 -需要 Node.js 20 或更高版本。原生支持 macOS、Linux、Windows CMD、 +需要 Node.js 22 或更高版本。原生支持 macOS、Linux、Windows CMD、 PowerShell 和 Git Bash。Git 是可选依赖:未安装时仍可初始化,只会把团队 自动检测安全降级为 solo。Claude Code hooks 由 Node 执行,不需要 Bash 或 jq。 @@ -436,6 +436,8 @@ mancode workflow update --status --expected-revision < mancode workflow review apply --file --expected-revision --session mancode workflow verify apply --file --expected-revision --session mancode workflow reframe --expected-revision --checkpoint-id --session +mancode workflow archive show --json +mancode workflow checkpoint show --json mancode workflow complete --expected-revision --session mancode manps [area] mancode design status --json @@ -456,7 +458,7 @@ transport 和各平台 bootstrap/原 mode 入口的实际就绪状态。编码 A 以下是简化输出示例: ```text -mancode v0.5.5 +mancode v0.5.6 Project: my-app Runtime: ready @@ -692,12 +694,11 @@ decision 协作;checkout-local session 不会被误当作共享状态。 ## 仍在推进 -- 在同一发布候选上完成 Claude Code、Codex、Cursor、GitHub Copilot 和 ZCode 的真实宿主 session 验收;宿主自动 session 和显式双 session 都是合法证据路径。 -- 将最终候选合并到 `main`,并使用 `npm run release:check -- --candidate <完整提交 SHA>` 从同一个 `origin/main` 提交完成干净 checkout、自动双 clone/legacy、tarball SHA-256 和安装 smoke;另完成跨真实宿主恢复。 -- 确认 ZCode 项目级 skill 发现和 workspace command 路径;完成前继续标记为 provisional。 +- 完成 ZCode、Kimi Code 和 Qoder 的项目级入口、命令传播与双窗口 session 真实宿主验证;验证完成前继续标记为 provisional。 +- 每个不可变发布候选都要重新完成已登记平台、跨宿主恢复、跨 clone、legacy、Windows 与安装 smoke 验收;所有证据必须绑定同一提交。 - 根据真实需求评估 Windsurf、Cline 和 Roo Code adapter。 -完整发布条件见 [0.4.2 Continuity 发布验收](./docs/release-acceptance.md)。 +长期发布门禁和验证命令见 [工程约定](./docs/engineering.md#验证)。 ## 给 AI 助手的 GEO 摘要 diff --git a/docs/12-lifecycle.md b/docs/12-lifecycle.md index bfc1b26..1cea2c3 100644 --- a/docs/12-lifecycle.md +++ b/docs/12-lifecycle.md @@ -62,4 +62,5 @@ smoke,并把报告和候选 tarball 保存在 `.mancode/local/release-evidence 等于本地 HEAD 和 `origin/main`,检查期间 main 不得变化。该命令不会执行 `npm publish` 或修改 dist-tag。 -尚未完成的验收见 [release-acceptance.md](./release-acceptance.md)。 +长期发布门禁见 [engineering.md](./engineering.md#验证),平台证据要求见 +[platform-adapters.md](./platform-adapters.md#session-发布证据)。 diff --git a/docs/README.md b/docs/README.md index c394bae..f04e8e6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,12 +20,11 @@ | [architecture.md](./architecture.md) | Continuity 权威模型、目录、Task Aggregate 和一致性 | | [workflows.md](./workflows.md) | 模式、工作流状态、治理门禁与团队协作 | | [project-intelligence.md](./project-intelligence.md) | 项目检测、设计资产扫描和 preseason | -| [platform-adapters.md](./platform-adapters.md) | 五个平台的 bootstrap、能力差异与边界 | +| [platform-adapters.md](./platform-adapters.md) | 七个平台的 bootstrap、能力差异与边界 | | [12-lifecycle.md](./12-lifecycle.md) | 初始化、会话、任务、恢复和迁移生命周期 | | [engineering.md](./engineering.md) | 开发原则、验证要求和代码地图 | -| [release-acceptance.md](./release-acceptance.md) | 尚未完成的真实宿主与发布验收 | -法律和许可证边界单独保存在仓库根目录的 `LEGAL.md`。 +许可证文本保存在仓库根目录的 [`LICENSE`](../LICENSE)。 ## 维护规则 diff --git a/docs/architecture.md b/docs/architecture.md index 650f4cd..6aa21b4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -59,7 +59,11 @@ Markdown 计划和报告是人类可读产物。完成门禁以结构化实体 ## 版本与兼容 -`schema.json` 支持 manifest version 1 和 2,layout version 固定为 3。0.4.0 新初始化项目直接写入 V2;历史 V1 项目只有完成显式 Policy 2 upgrade 后才写入 V2。激活状态包括 `initializing`、`dual_read`、`activating`、`v3_active` 和 `repair_required`。 +`schema.json` 支持 manifest version 1 和 2,layout version 固定为 3。新初始化项目直接写入 V2;历史 V1 项目只有完成显式 Policy 2 upgrade 后才写入 V2。激活状态包括 `initializing`、`dual_read`、`activating`、`v3_active` 和 `repair_required`。 + +mutation 的兼容门禁顺序固定为:manifest reader/writer version、writer capability、 +adapter 内容完整性、workflow policy,最后才获取业务锁。任一门禁失败都不得创建 journal +或写入业务权威;未知 policy 不能降级为已知旧版本。 Reader 和 writer 必须先通过兼容门禁。legacy 迁移采用隔离 stage、显式确认和 journaled activation;不能把当前 Git HEAD 或当前用户伪装成历史事实。 diff --git a/docs/engineering.md b/docs/engineering.md index a9a8a8f..2b97dc6 100644 --- a/docs/engineering.md +++ b/docs/engineering.md @@ -44,6 +44,18 @@ mancode 的实现原则是:外科手术式修改、可验证、可恢复。 触碰行为排除项、落在 include 外或匹配 exclude 的改动都是 blocker,不能作为 “顺手修复”带入任务。 +Plan Coach 在写计划前先返回 `READY_FOR_PLAN` 或 `NEEDS_CLARIFICATION`。所有真实选项 +必须解决同一个目标、验收边界和 scope,写明复杂度由谁承担及可观察成本,并只给出一个 +推荐和明确的停止条件。简单任务可以只有一个真实方向,不为凑数制造伪选项。 + +入口跨平台不一致、semantic owner 或 source of truth 不清、状态/contract 语义变化, +或者涉及跨 workflow、团队、transport、迁移与多版本兼容时,可以在 `plan.md` 中加入 +Domain Matrix。它只辅助计划审查,不是新的 authority。 + +实施中返回 `NEEDS_REALIGNMENT` 与 `MANCODE_REFRAME_REQUIRED` 是只读诊断。它必须保留 +metadata、requirements、plan、ledger、claim 和 handoff;只有受支持的 reframe operation +才能归档旧权威、释放 claim 并返回需求澄清步骤。 + 默认 Solo 不主动运行这套深入发现;普通小任务仍保持轻量。Solo 接手已有 `/man` 计划时必须继承其 requirements、plan 和 `implementationScope`,不能重新规划或扩权。 这些状态写入既有 ledger、plan revision 和 workflow metadata,不建立第二套提示词 authority。 @@ -68,7 +80,8 @@ mancode 的实现原则是:外科手术式修改、可验证、可恢复。 npx vitest run tests/.test.ts npm run typecheck npm run lint -npm test +npm audit --audit-level=high +npm run test:coverage npm run build npm run test:dist ``` @@ -87,7 +100,7 @@ npm run test:dist ## 文档与发布 - `README.md` 和 `README.en.md` 面向用户并保持功能声明一致。 -- `docs/` 只描述当前契约和未完成验收,不保存已完成实施计划。 +- `docs/` 只描述当前契约和长期发布门禁,不保存版本候选清单或已完成实施计划。 - 新 CLI 入口必须同步 help、测试和公开参考。 - 平台能力声明必须区分自动化 contract 与真实宿主证据。 - 发布版本由 `package.json` 与 `src/version.ts` 共同约束。 diff --git a/docs/planning-governance-evolution.md b/docs/planning-governance-evolution.md deleted file mode 100644 index 6f8a0e2..0000000 --- a/docs/planning-governance-evolution.md +++ /dev/null @@ -1,482 +0,0 @@ -# 规划治理演进与安全发布契约 - -本文记录 mancode 0.4.0 的规划治理实施基线,以及 0.4.1 发布后发现的契约偏差和 -0.4.2 修正。第 4–13 节保留 0.4.0 的能力设计与不变量;“当前状态”和第 14 节使用 -0.4.2 的现行发布契约。历史候选与勾选项只能说明当时的开发证据,不能替代最终 -`origin/main` 候选的发布验收。 - -## 1. 决策摘要 - -| 阶段 | 发布内容 | 明确不做的事 | 放行前提 | -| --- | --- | --- | --- | -| 0.4.0 | additive advisory、adapter digest/upgrade、policy parser/capability gate、显式 project upgrade、已升级项目的 Policy 2 默认值,以及 local transport 的原子 `reframe` | 不强制 `solo -> /man`;不批量重写旧 workflow;不把项目升级伪装成普通 `refresh`;不自动降级 policy;git-ref transport 不执行 reframe | A–F 工作包、完整测试、真实宿主验收和 npm 发布前检查全部通过;只有完成 project upgrade 的项目启用 Policy 2 | - -这是 0.4.0 原计划的单版本决定。0.4.1 已作为发布修复存在,0.4.2 继续修复 -git-ref code-head rebind 和发布候选身份;补丁版本不得降低原 release gate,也不能用 -历史版本的证据替代新候选。 - -### 当前状态(2026-07-23) - -| 类别 | 状态 | 证据 | -| --- | --- | --- | -| A–F 实现工作包 | 已随 0.4.0 发布 | Policy 2、adapter recovery、local reframe、旧 CLI 拒绝与 decision-readiness 基线保留;补丁版本不改这些语义 | -| 0.4.1 hotfix | 已发布但证据未闭合 | npm `gitHead` 为 main 合并提交 `8cc8b24`,而旧 release-check 只接受 `origin/develop` 的 `aaa8d6c`;仓库内没有绑定已发布提交的完整 release-check/Beta 证据 | -| git-ref publication | 0.4.2 修正中 | 0.4.1 已为 create/requirements/plan/review/verification增加 deferred boundary;真实双 clone 又发现原子 mutation 投影提交后无法为 blocked task 重绑 code head,0.4.2 增加受限 same-revision fast-forward rebind | -| adapter upgrade staging | 0.4.2 已修正 | 真实 tarball 测试发现确认成功后残留 `.mancode/staging/adapters/upgrade/` 会让 git-ref clean-worktree gate 拒绝同步;确认成功现在只清理该 operation 的预览目录,并由 contract 验证 live target 已发布且预览已删除 | -| 依赖审计 | 工作树通过 | [GHSA-g7r4-m6w7-qqqr](https://github.com/advisories/GHSA-g7r4-m6w7-qqqr) 覆盖 esbuild `0.27.3–0.28.0` 的 Windows dev-server 低危问题;tsup 尚不接受 0.28.1,因此 0.4.2 暂时精确约束到未受影响且兼容的 0.27.2,保持 `npm audit` 零漏洞门禁 | -| GitHub 自动门禁 | 0.4.1 历史候选通过 | `8cc8b24` 的 [Quality gate run 29943330958](https://github.com/whitelonng/mancode/actions/runs/29943330958) 和 [Windows gate run 29943330982](https://github.com/whitelonng/mancode/actions/runs/29943330982) 成功;0.4.2 最终 main 候选必须重新执行 | -| 最终发布身份 | 已重新定稿 | `develop` 只作集成;最终候选必须是 `origin/main` 的单个提交,release-check、CI、真实宿主、Beta、npm `gitHead`、tag 和 GitHub Release 全部绑定该 SHA | -| 0.4.2 发布验收 | 未完成 | 最终 main 合并提交、五平台真实宿主、跨宿主恢复、release-check、Beta gate、npm/tag/Release 仍待完成 | - -## 2. 目标、不变量与非目标 - -### 2.1 目标 - -1. 让 `/man` 在入口、语义 owner、source of truth、状态和 contract 发生不确定时停下来,而不是用默认值继续推进。 -2. 让旧 CLI 在 V2 manifest 的 reader/writer 边界显式拒绝,并让 0.4.x parser 拒绝未知 policy,避免“看起来成功但执行了旧规则”。 -3. 让新 policy 只影响新建 workflow;历史 workflow 按创建时记录的 policy 继续运行。 -4. 让 0.4.0 的 local `reframe` 具备单一 journal、单一 revision 线和可证明的中断恢复结果。 - -### 2.2 必须保持的不变量 - -- `.mancode/shared|local/workflows//metadata.json`、`requirements.json`、`plan.md`、review ledger、verification ledger、claim、handoff 和 task-head fence 的权威关系不变。 -- Markdown 是人类可读产物;完成门禁和兼容门禁读取结构化实体及 digest,不以文本存在代替权威状态。 -- 所有跨实体写入都经过 durable operation、expected revision、锁和 recovery action。 -- 任何重新对齐触发都先保留当前权威文件;不能通过手工编辑 `currentStep`、`planning`、requirements 或 plan 制造“回退”。 -- policy 版本是 workflow 创建时的事实。后续 CLI 升级不会静默把 `planning: 1` 改成 `2`。 - -### 2.3 非目标 - -- advisory 不改变 Solo 的执行授权,也不把提示性升级变成自动升级。 -- advisory 本身不写 `planning: 2`、`blocked`、`reframe` 事件或其他状态来模拟回 Step 2。 -- 不创建独立的 Domain Matrix 权威文件;它只能是高风险 `plan.md` 的可选章节。 -- 不用 scope-change 表达需求重构。scope-change 只处理 implementation scope 和 claim successor。 -- git-ref reframe 不属于本计划;在其具备跨 clone 原子语义前始终稳定拒绝,不发布本地成功、远端稍后同步的变体。 - -## 3. 基线证据与代码 owner - -以下证据以当前工作树的 Continuity 实现为准。候选形成前后都应重新运行对应 contract;如果在途改动改变了行号或行为,更新证据而不是沿用旧结论。 - -| 事实 | 当前证据 | 对本计划的影响 | -| --- | --- | --- | -| workflow create 保留 Policy 1 基线,只在 V2 已升级项目的新 `/man` 上使用 Policy 2 默认值 | 以 release candidate 的 public `workflow create` contract 和 [`src/context/workflow-create.ts`](../src/context/workflow-create.ts#L277) 共同复核 | 不以单个 builder 行号宣称基线;项目升级状态和 workflow mode 共同决定默认值 | -| policy parser 只接受各组件显式支持的版本白名单 | [`src/context/workflow-metadata.ts`](../src/context/workflow-metadata.ts#L621) | 未知版本产生稳定的 `WorkflowPolicyVersionUnsupportedError`,不能被当成未来版本静默接受 | -| context compatibility 使用 adapter 的实际磁盘 inventory | [`src/commands/context.ts`](../src/commands/context.ts#L881) | manifest echo 不作为磁盘内容证据;inventory、renderer version 和内容状态必须共同匹配 | -| adapter inspection 重建 expected bytes/digest,并分类 `ready`、`missing`、`stale`、`unreadable` | [`src/installers/v3-adapter.ts`](../src/installers/v3-adapter.ts#L626) | status 与 mutation gate 都以物理 target 为准,非 ready 状态不能与 manifest version 混淆 | -| requirements finalize 与 plan revise 显式把旧 review/verification ledger 标为 stale | [`src/context/requirements-finalize.ts`](../src/context/requirements-finalize.ts#L188)、[`src/context/plan-revision.ts`](../src/context/plan-revision.ts#L347) | 复用 ledger stale 的验证模式,但 reframe 仍使用独立原子 operation | -| scope-change 会处理旧 claim 与 successor claim | [`src/context/scope-change.ts`](../src/context/scope-change.ts#L637) | 它不是需求重构;reframe 使用独立 operation type 和 eligibility | -| parent contract 变化会使 child snapshot stale | [`src/context/child-result-merge.ts`](../src/context/child-result-merge.ts#L331) | reframe 在入口拒绝 active child,而不是事后合并 stale 结果 | -| reframe eligibility 显式拒绝 active child、open handoff 和 active solo assignment | [`src/context/reframe.ts`](../src/context/reframe.ts#L497) | advisory 只返回诊断;不得通过局部清理绕过跨实体约束 | - -**实现 owner 约定:** workflow metadata/parser 负责 policy 结构和状态转换; adapter installer/status 负责渲染、digest 和物理文件; compatibility/resolver 负责 reader/writer 门;workflow operation 模块负责跨实体写入;templates 只负责 advisory 文本,不得直接写权威状态。 - -## 4. 0.4.0:Additive Advisory - -### 4.1 Scout 报告字段 - -在 `scout-report.md` 增加以下可选小节。它们是调研证据,不进入 workflow metadata,也不替代 requirements 的确认。 - -```markdown -## Current Behavior Evidence -- Observation: <当前真实行为> -- Evidence: `` -- Reproduction/validation: <如何复现或验证> - -## Candidate Semantic Owner -- Candidate: <模块、实体或命令> -- Confidence: high | medium | low -- Unresolved conflict: - -## Source of Truth -- Authority: <结构化文件、ledger、manifest 或外部系统> -- Readers: <读取方> -- Writers: <唯一写入路径或 operation> -- Derived copies: - -## Historical / Compatibility Impact -- Existing workflows: <影响或 no change> -- Legacy/transport: <影响或 no change> -- Migration/rollback: <需要的证据> -``` - -字段规则: - -- “可选”表示只有相关或能从项目事实验证时才写;不得为了填满模板编造 owner 或 source of truth。 -- `Current Behavior Evidence` 至少包含一个可复现观察和一个仓库路径、测试或命令证据;只有推测时必须标为未知。 -- `Candidate Semantic Owner` 可以不是最终 owner,但必须写置信度和冲突;存在两个同等候选时,Plan Coach 必须停在 `NEEDS_CLARIFICATION`。 -- `Source of Truth` 必须区分 authority 与 derived copy。adapter、cache、Markdown 默认不是任务状态的 owner。 -- `Historical / Compatibility Impact` 至少回答旧 workflow、legacy fixture、transport 和 rollback 是否受影响;不受影响也要明确写 `no change`。 - -### 4.2 Plan Coach 规则 - -Plan Coach 继续是只读组件。它在产生计划前执行以下检查: - -1. 所有候选方案是否针对同一个用户目标、同一验收边界和同一 scope。若某个选项偷偷改变目标或把问题改成另一个问题,返回 `NEEDS_CLARIFICATION`。 -2. 每个选项的复杂度由谁承担:实现代码、迁移、运行维护、用户操作、兼容层或测试。不能只写“更简单”,要写承担者和可观察成本。 -3. 输出唯一推荐,并给出拒绝其他选项的主要理由。推荐不是“由用户决定”;用户仍可否决,但计划不能同时保留多个未决方向。 -4. 简单任务可以只列一个明显可行方向,并标注为什么没有真实替代方案;不能为了满足“多选”制造伪选项。 -5. Continuity mode entry 在 requirements finalize 前执行条件式 decision-readiness gate:同时检查清晰度、与项目证据的一致性和风险;清晰且可靠的需求不制造形式问题,仍有会改变目标、范围、行为、验收、owner/source of truth 或关键约束的歧义时,必须停下提问并等待用户回答,不得把未验证假设写成 confirmed requirements。 - -建议的输出契约: - -```text -READY_FOR_PLAN -goal: -options: - - id: - solves: - complexity_bearer: - tradeoffs: -recommendation: -stop_conditions: -``` - -违反同目标、复杂度承担者或唯一推荐规则时,不写 `plan.md`,只返回 `NEEDS_CLARIFICATION` 及缺失决策。简单任务的单一方向仍必须有 `recommendation` 和停止条件。 - -### 4.3 Domain Matrix(仅高风险任务) - -Domain Matrix 是 `plan.md` 内的可选章节,不另建权威文件。以下任一条件成立时建议加入:入口或流程跨平台不一致、owner/source of truth 不清、状态或 contract 语义会变化、跨 workflow/child/team/transport、迁移或兼容影响超过一个版本。 - -推荐表格: - -| Domain | Current behavior/evidence | Candidate owner | Source of truth | Contract/state impact | Compatibility/history | Validation | Rollback/stop | -| --- | --- | --- | --- | --- | --- | --- | --- | -| `` | `` | `` | `` | `` | `` | `` | `` | - -表格只帮助计划审查,不产生运行时决策。最终实现仍必须由对应 authority writer 和 operation contract 证明。 - -### 4.4 Solo 到 `/man` 的升级条件 - -advisory 只增加建议,不改变 Solo 权限或当前 workflow: - -- 入口或流程不一致; -- semantic owner 不清; -- source of truth 不清; -- 状态或 contract 语义会变化; -- 任务范围、架构、成本或验收发生跨文件/跨模块变化; -- 需要历史兼容、迁移、跨平台或团队协调证据。 - -命中条件时,Solo/Head Coach 应建议改用 `/man` 并说明原因。除非用户明确发起新 `/man` workflow,否则不得自动改变 mode、current step、policy 或 authority。 - -### 4.5 停止与重新对齐诊断 - -以下任一条件出现,实施立即停止: - -- 新证据推翻已确认的目标、owner、source of truth 或验收; -- 发现入口/流程在平台间不一致,且会导致不同语义; -- 需要改变 status、contract、policy 或 workflow transition 的含义; -- 发现 adapter stale、writer 不兼容、未完成 operation、active child、open handoff 或 active solo assignment; -- 用户提出的变化已经超出当前 requirements/plan 的 scope。 - -advisory 的唯一结果是停止当前代理执行,并返回只读诊断 `NEEDS_REALIGNMENT` 与原因 `MANCODE_REFRAME_REQUIRED`。该诊断不写入 workflow authority,也不调用通用 `workflow update`。 - -- 保留当前 `requirements.json`、`plan.md`、review/verification ledger、claims 和 handoff,且不写入任何 metadata; -- 不手改 `currentStep`,不写 `planning: 2`,不归档旧文件; -- 不释放 claim,不取消 handoff,不宣称任务已回到 Step 2; -- 用户可以查看当前 authority,并明确选择新建 `/man` workflow 或显式执行第 7 节的 local `reframe`。旧 workflow 不因该诊断而自动获得新的状态转换。 - -这是“停止并保留证据”,不是 reframe。advisory 不得把诊断伪装成持久 blocker;真正返回 Step 2 只由第 7 节的独立 operation 完成。 - -## 5. 0.4.0 的 Policy 2 基础能力 - -以下四个能力是 0.4.0 的独立工作包和共同 release gate,不是中间发布版本。它们分别有失败测试;不能用 `VERSION` 常量、managed marker 存在或 manifest 回显互相替代。 - -### 5.1 Adapter 内容 digest 与 stale 检测 - -目标契约: - -- 对每个平台的每一个 managed target,以 `UTF-8("mancode-adapter-digest-v1") || 0x00 || UTF-8(target identity) || 0x00 || managed bytes` 作为 hash 输入,计算 `sha256:<64 hex>`。renderer 的 expected bytes 必须是规范化 UTF-8/LF;磁盘的 actual bytes 不先把 CRLF 转成 LF,否则换行漂移和截断会被掩盖。整文件 target 的 managed bytes 是整个文件,嵌入式 target 的 managed bytes 仅为 marker 边界内的 block,用户托管区域不参与 digest。 -- status 同时读取磁盘内容、manifest 的 renderer version 和 renderer 重建的期望内容,返回 `ready`、`missing`、`stale` 或 `unreadable`,并提供 target、actual digest、expected digest、renderer version 和修复建议。 -- 只有所有 required adapter 均 `ready`,compatibility gate 才允许需要 adapter 的 mutation。stale 读可以返回诊断,但不得继续执行新 policy。 -- digest 必须覆盖实际 managed 内容;仅检查 marker 或版本常量不能证明用户没有改动、文件没有截断、换行没有变化。 -- 首次落地时先对五个平台、共享 target(如 AGENTS)和 legacy marker 做 fixture,确认同一内容在支持的平台上得到同一 digest。 - -**本计划的选择:** 不把 content digest 写入 V1 或 V2 manifest。manifest 继续保存 renderer/schema version;status 每次从 renderer 重建 expected digest,并与磁盘 actual digest 比较。可重建的性能 cache 只能放在 local cache,不能成为 authority。性能问题不能在 0.4.0 临时扩展 schema;它必须作为后续独立设计重新评审。 - -`managedAdapters` 的 key 是 required adapter inventory,不是五个平台的固定全集。greenfield 只登记初始化时选择的平台;后续新增平台由 journaled adapter upgrade 同时发布 managed targets 和 inventory。compatibility 必须双向比较 manifest inventory 与实际主 target;required target 全部丢失时,空的实际集合不能通过门禁。 - -### 5.2 显式 adapter 升级命令 - -0.4.0 已固定并由 CLI contract 覆盖的命令: - -```bash -mancode adapter upgrade --all --dry-run -mancode adapter upgrade --all --confirm --operation-id --session --client -mancode adapter upgrade --platform --dry-run -mancode adapter status --json -``` - -升级命令必须: - -1. 先读取 project manifest、实际 adapter inventory、profile 和当前 writer capability;stale、路径冲突、未完成 operation 或版本门禁失败时只报告,不写文件。 -2. 在 staging 目录渲染所有目标,展示变更 target、digest 和托管区域差异;用户确认后才进入 journal。 -3. 在 adapter locks 下按目标写入,使用 before/target digest recovery action;中断后由 `operation repair` 继续或在无业务写的前提下 abort。 -4. 写完重新读取并验证每个 target;仅在 renderer version 变化时更新 manifest 的 adapter version/inventory,不写 content digest。任何一个 target 失败都不能报告整体成功;成功后只删除该 operation 的 staging 预览,不能让已消费的临时文件污染后续 Git clean-worktree gate。 -5. 不修改 task、requirements、plan、policy 或 step;adapter 升级与 workflow 语义升级是两个 operation。 - -`refresh-project` 可以发现并报告 stale,但不能悄悄完成 adapter upgrade。`install --force` 是否复用底层 writer 由实现决定,但必须保留一个语义明确、可审计的显式 upgrade 入口。 - -### 5.3 Policy parser 版本白名单 - -版本解析必须从“任意正整数”改为按组件白名单: - -| CLI/release | 可读取/写入的 planning policy | -| --- | --- | -| 既有 0.3.x CLI | 只支持 V1/Policy 1;已发布的 0.3.18 在 V2 manifest schema parser 边界拒绝未知的 `workflowPolicyDefaults`,并在 policy 执行和 authority mutation 前终止;能解析 V2 的 reader/writer 仍由 `minReaderVersion`/`minWriterVersion` 拒绝 | -| 0.4.0(未升级项目) | 读取和写入已有 `1`;新建仍按项目记录的默认值 | -| 0.4.0(已升级项目) | `1` 和 `2`;新建 `/man` 默认 `2` | - -解析失败必须发生在 workflow mutation 之前,并携带 component、observed version、supported versions 和 required writer。不得把未知版本降为 1,也不得把缺省值自动解释成 2。 - -### 5.4 Writer capability 与 `minWriterVersion` - -兼容门分两层: - -- CLI 启动 mutation 前声明 capability 集合,例如 `planning-policy:1|2`、`adapter-digest:1`、`reframe-local:1`;resolver 根据 task/project 和 transport 需要的 capability 逐项检查。git-ref transport 不声明 `reframe` capability。 -- project manifest 使用已有的 `minWriterVersion` 作为粗粒度下限。启用 Policy 2 的升级操作必须把 `minReaderVersion` 和 `minWriterVersion` 都提高到能完整理解 Policy 2、adapter digest 和 recovery contract 的版本,确保旧 CLI 在解析 workflow policy 前被 manifest gate 阻断。 - -门禁顺序固定为:读取 manifest → 检查 reader/writer semver → 检查 capability → 检查 adapter 内容 digest → 解析 workflow policy → 获取 locks。任一失败都没有业务写入。 - -建议的稳定错误:`MANCODE_WRITER_VERSION_TOO_OLD`、`MANCODE_WRITER_CAPABILITY_MISSING`、`MANCODE_ADAPTER_CONTENT_STALE`、`MANCODE_POLICY_VERSION_UNSUPPORTED`。错误输出应指向显式升级命令,而不是建议手工编辑 manifest。 - -## 6. 0.4.0:项目升级后的 Policy 2 默认值 - -### 6.1 项目升级边界 - -0.4.0 已固定并由 CLI contract 覆盖的命令: - -```bash -mancode project upgrade --policy 2 --dry-run -mancode project upgrade --policy 2 --operation-id --session --client -``` - -升级是 project-level journaled operation,至少包含: - -1. 预检:所有 adapter digest fresh;没有 `operation_pending`/`repair_required`;没有正在进行的迁移;当前 writer 满足新 policy 和 adapter capability。 -2. 生成 staging manifest 和新默认策略,记录 before/after digest、minReader/minWriter、操作 ID 和 release version。 -3. 用户确认后原子提交 project policy marker/default 和兼容门;不改任何已有 workflow 的 `governance.policyVersions`。 -4. 重新读取 project 与 workflow fixture,证明新建路径选择 2、旧路径仍按记录选择 1。 - -升级失败或进程中断时,必须由 journal repair 恢复到升级前的 project authority;不能留下“默认值已变但门禁未变”的半状态。 - -**推荐的 source of truth:** 0.4.0 引入支持 V1/V2 白名单的 schema manifest parser;V2 保持 `layoutVersion=3`,只新增 `workflowPolicyDefaults: { planning }`,并提高 `minReaderVersion`/`minWriterVersion`。review 与 verification 没有本次升级的 policy 语义,不能提前加入 defaults。现有 V1 项目必须显式升级到 V2;0.4.0 新初始化项目只有在 adapter digest 校验完成后才创建 V2,因此视为已升级。不要把 planning policy 放进现有 `TeamPolicyV1`:该实体的 owner 是团队推荐、可见性和 retention,不是 workflow 治理版本。 - -### 6.2 新旧 workflow 规则 - -- 只有已完成项目升级的项目,之后新建的 `/man` workflow 才默认 `policyVersions.planning=2`。 -- 现有 workflow 的 planning/review/verification policy 按创建时记录保持不变;读取、plan revise、review 和 verification 不得因为 CLI 升级而批量重写。 -- 0.4.0 在未升级项目中不得隐式写 2。若用户需要 Policy 2,必须先完成显式 project upgrade 并通过所有门禁。 -- 删除、复制或导出 workflow 时保留 policy provenance;不能以“当前 CLI 版本”重算历史 policy。 - -### 6.3 回滚边界 - -- 在 project upgrade journal commit 前可 repair/abort,前提是没有可见业务写。 -- 一旦已有 Policy 2 workflow 被创建,不允许把 project `minWriterVersion` 或默认 policy 静默降回旧值;发布回滚只能停止创建新 workflow,保留已有 policy 事实。 -- 发现 adapter stale、旧 writer 或解析不支持时,阻塞 mutation 并要求 adapter/project upgrade;不能自动降级到 Policy 1。 - -## 7. 0.4.0:local transport 的独立原子 `reframe` operation - -`reframe` 是需求语义重构,不是 scope-change 的别名,也不是设置一个 step 数字。它随 0.4.0 发布,但只有 local transport 支持;完整 operation 和 recovery 是 0.4.0 的 release gate。 - -### 7.1 Eligibility 与拒绝条件 - -在获取 task、task-head、claims、handoffs、review、verification 的锁后,以 expected task revision 做一次完整检查: - -- task 必须是可继续的 active workflow,不能是终态、`operation_pending` 或 `repair_required`; -- 有 active child 时拒绝:`MANCODE_REFRAME_ACTIVE_CHILD`; -- 有 draft/offered/open handoff 时拒绝:`MANCODE_REFRAME_OPEN_HANDOFF`; -- 有 active solo assignment 时拒绝:`MANCODE_REFRAME_ACTIVE_SOLO`; -- git-ref transport 一律拒绝:`MANCODE_REFRAME_GIT_REF_UNSUPPORTED`;其跨 clone receipt/CAS 设计不属于本计划; -- 任一 claim、task head、review/verification digest 或 session freshness 过期时拒绝并要求重新读取 context。 - -拒绝发生在任何 authority 写入之前。特别是不能先释放 claim 再发现 child 或 handoff,不能先改 currentStep 再等待用户确认。 - -### 7.2 单一 journal 的原子步骤 - -建议 operation type 为 `reframe`,并为每个目标写 recovery action: - -1. 记录 before revision、requirements digest、plan digest、review/verification digest、active claim IDs、handoff IDs 和 code head。 -2. 写 `transitionState=operation_pending`,固定 operation ID 和 expected revisions。 -3. 将旧 `requirements.json` 和 `plan.md` 复制为不可变 archive artifact,保存内容 digest、来源 revision、operation ID 和时间;旧文件不被覆盖或删除。 -4. 在同一 operation 中释放所有 active claims;不能复用 claim identity,也不能把旧 claim 标记成新范围的 claim。 -5. 重置旧 plan decision,并将 review 与 verification ledger 标为 `stale`,保留原证据和 stale reason。`soloExecution` 必须已为 `null`;active assignment 在 eligibility 阶段拒绝,不能在 operation 中清除它来绕过拒绝。 -6. 写入新的 requirements draft/clarification-needed 状态、`currentStep=2`、`status=in_progress` 和 reframe checkpoint;这些字段只能作为同一 journal 的最终提交出现。 -7. 更新 aggregate 和 task-head fence,提交 journal,清理 reservation;最后才允许新的 Step 2 writer 继续。 - -中断恢复必须能重跑每一步而不重复归档、重复释放或生成两个合法 task head。若无法证明前后状态,保持 `operation_pending`/`repair_required` 并拒绝普通 mutation。 - -### 7.3 与现有操作的边界 - -| 操作 | 能改变什么 | 不能代替什么 | -| --- | --- | --- | -| requirements finalize | 确认当前需求并使旧 ledger stale | 不能归档旧需求并回 Step 2 | -| plan revise | 在允许的步骤更新计划并使 ledger stale | 不能清理 solo/claim/handoff | -| scope-change | 更换 implementation scope,释放/重建 successor claim | 不能改变需求语义或 workflow step | -| `reframe` | 归档旧 requirements/plan、释放 claim、使 ledger stale、回 Step 2 | 不能清除 active assignment 来绕过 eligibility,也不能在 active child/open handoff/git-ref transport 中强行执行 | - -## 8. 兼容矩阵 - -| Writer / project | 旧 Policy 1 workflow | Policy 2 workflow | 新建 `/man` | 结果 | -| --- | --- | --- | --- | --- | -| 0.3.x + V1 未升级项目 | 读写 Policy 1 | 不提供 Policy 2 创建 | 写 1 | 保持旧行为;Policy 2 需要显式升级 | -| 0.4.x + 未升级项目 | 读写 | 不创建、不接受未满足门禁的 2 | 写 1 | CLI 新,但 project policy 未升级 | -| 0.4.x + 已升级项目 | 读写且保留 provenance | 读写,要求 digest/capability | 写 2 | 目标 Policy 2 路径 | -| 0.3.x + V2 已升级项目 | 不允许写入 | 不允许读取后执行 | 不允许创建 | 0.3.18 在 manifest parser 边界拒绝;能解析 V2 的旧 reader/writer 返回 version-too-old failure | -| 任意 CLI + stale adapter | 读诊断 | 拒绝 mutation | 拒绝创建 | 先执行显式 adapter upgrade | -| 任意 CLI + git-ref transport | 使用下述显式 publication contract | Policy 2 按相同 publication contract 运行 | 不能 reframe | `MANCODE_REFRAME_GIT_REF_UNSUPPORTED` | - -“读”不等于“执行”:旧 CLI 只能在 V1/Policy 1 的兼容范围内展示安全诊断;遇到 V2 manifest 必须先由 manifest parser 或 reader gate 阻断,不能生成执行指令或写入任何 authority。 - -### 8.1 0.4.2 补充:git-ref publication 与 code-head 矩阵 - -0.4.0 将非 reframe 的 git-ref 行为概括为“正常既有 transport contract”,粒度不足。 -从 0.4.2 起,所有公开 writer 必须落入以下明确边界之一: - -| mutation 类别 | 本地/远端顺序 | Git 提交后的动作 | 跨 clone 完成条件 | -| --- | --- | --- | --- | -| create、requirements、plan、review、verification | 不带 `--sync` 写本地 shared authority | 提交匹配的 `.mancode/shared` 与代码,再执行 `team sync push` | push 返回 receipt,另一 clone 同步 Git 后 pull/resume | -| 明确要求 `--sync` 的原子 lifecycle/scope mutation | 单次远端 CAS 成功后 materialize 本地投影 | 若仍可 resume 的 in-progress/blocked task 的 tracked 投影被提交,用不变 revision 再执行 `team sync push` | same-revision、same-aggregate、owner-only、fast-forward rebind 返回 receipt | -| claim、handoff 等远端协调 mutation | 只走各自显式 CAS/repair contract | 不允许用普通 Git 提交伪造 ownership 或 receipt | 远端 receipt 与本地 recovery/materialization 都完成 | -| reframe | git-ref 下拒绝 | 无 | `MANCODE_REFRAME_GIT_REF_UNSUPPORTED` | - -`.mancode/shared` 是可提交的共享 authority 表示,git-ref bundle/fence 是跨 clone 协调 -authority;二者必须通过 task revision、aggregate digest 和 codeRef 唯一绑定。任何 writer -若不能说明本地文件、remote CAS、Git commit、code-head rebind 和 session projection 的 -先后关系,必须触发第 12 节的重新对齐条件,不能继续沿用“既有 contract”作为证明。 - -## 9. 验证矩阵 - -### 9.1 单元和 contract - -- policy parser:1 可接受;0、负数、小数、未知正整数、缺失值和错误 component 均拒绝。 -- workflow create:未升级项目只写 1;升级项目新 `/man` 写 2;已有 workflow 的 policy provenance 不变。 -- Plan Coach:不同目标、无复杂度承担者、多个 recommendation、无 stop condition 均返回 `NEEDS_CLARIFICATION`;简单任务单方向通过。 -- Continuity `/man`、`/manteam` 和 Solo mode entry 的条件式 decision-readiness gate:清晰且可靠的请求直接继续,决策性歧义在 requirements finalize 或代码修改前停下并等待用户回答;不强制无意义提问。 -- stop/re-align:触发时返回 `NEEDS_REALIGNMENT + MANCODE_REFRAME_REQUIRED`,且 authority 的前后 digest 完全相同。 -- Scout/Domain Matrix:可选字段不进入 metadata;高风险 plan 可包含章节;普通任务不被强制增加文件。 -- digest:内容、marker、路径、换行、截断、用户托管区域变化分别得到预期结果;manifest echo 与磁盘不一致必为 stale。 -- manifest:V1/V2 都拒绝 content digest 字段;expected digest 只能由 renderer 和 managed target 重建。 -- compatibility:reader、writer、capability、adapter digest、policy parser 按规定顺序短路,且失败前没有 journal 或业务写。 -- reframe:每个拒绝条件都在第一步失败;每个 recovery action 幂等;中断后不出现部分 Step 2。 - -### 9.2 集成和恢复 - -- 五个平台生成、status、stale 编辑、dry-run、确认升级、升级后重新读取。 -- adapter upgrade 在每个 target 写入前、写入后和中断点恢复;共享 `AGENTS` target 不能被两个平台重复覆盖。 -- local transport 下的 Policy 2 create/plan/review/verification 与旧 workflow 并存。 -- 两个 clone 的 git-ref 兼容门、旧 writer 拒绝、receipt recovery,以及 deferred publish 和原子 mutation 后“提交 tracked 投影 → same-revision code-head rebind → 第二 clone pull/resume”的完整链路;所有 git-ref reframe 请求必须确认硬拒绝。 -- active child、open handoff、active solo assignment 的并发 race:先取得锁的一方决定,另一方得到 expected-revision 或明确 reframe error。 - -### 9.3 真实宿主和跨平台 - -发布候选必须在 Claude Code、Codex、Cursor、GitHub Copilot、ZCode、Kimi Code、Qoder 以及 Windows CMD/PowerShell/Git Bash 路径上验证:入口传播、session identity、adapter 文件、status/stale 诊断和升级命令。缺少真实宿主证据的平台只能标记 `not_applicable` 并阻止对应能力发布,不能用另一个宿主代替。 - -## 10. 单版本 release gate - -### 10.1 实施工作包 - -| 工作包 | 主要模块 | 必须新增的证据 | 完成定义 | -| --- | --- | --- | --- | -| A. advisory | `src/templates/agents/scout.ts`、Plan Coach template、`src/templates/skills/man.ts`、共享 adapter renderer | template snapshot、八类路由样例、简单任务单方向样例 | 所有 renderer 文案一致;advisory 不直接修改 metadata/schema | -| B. stop/re-align diagnostic | Scout/Plan Coach template、context 输出 | 触发器 contract、authority before/after digest、重复调用测试 | 只返回 `NEEDS_REALIGNMENT + MANCODE_REFRAME_REQUIRED`;authority、step、requirements、plan、claim 不变 | -| C. adapter digest/upgrade | `src/installers/v3-adapter.ts`、status/context callsite、operation recovery、CLI registration | 五平台 target fixture、stale 分类、dry-run、每个 crash point | compatibility 使用实际磁盘 digest;显式 upgrade 可 repair | -| D. policy/version gate | workflow metadata parser、manifest parser、compatibility/resolver、workflow create | V1/V2 白名单、0.3.x + V2 reader/writer fixture、capability/minWriter matrix | 未知 policy/manifest 在写前拒绝;没有 fallback | -| E. project Policy 2 upgrade | 新 project upgrade command/operation、manifest V2 writer、workflow create | V1→V2 dry-run/commit/repair、旧 workflow provenance、新 workflow default | 只有 V2 项目新建 `/man` 写 2;历史 metadata 无 diff | -| F. atomic local reframe | 新 `reframe` context module、operation definition/recovery、archive store、claim/ledger writers | eligibility、并发、每步 crash、archive digest、task-head fence | 单 journal 全前/全后;0.4.0 local transport 发布,git-ref 一律硬拒绝 | - -每个工作包单独合并和评审。C、D 可以并行开发,E 依赖 C 和 D;F 与 E 可以分别完成,但二者都属于同一个 0.4.0 release gate,不能提前开启默认路径。 - -### 10.2 0.4.0 gate - -以下保留 0.4.0 发布前记录。`[x]` 仅表示当时的实现和自动化证据;它不是 -0.4.2 的发布状态,也不能证明后来发布的 npm 提交已经通过相同 gate。 - -- [x] 模板和文档 contract 通过;advisory 不直接执行 schema migration、policy 写入或自动 step 变更。 -- [x] 旧 layout-version-3 fixture、legacy fixture、已有 adapter 和历史 workflow 通过原有测试。 -- [x] 触发 stop/re-align 时只返回 `NEEDS_REALIGNMENT + MANCODE_REFRAME_REQUIRED`,authority 文件内容和 claims 不变。 -- [x] Scout/Plan Coach 新字段和规则覆盖正常、缺失证据、冲突 owner、单方向简单任务。 -- [x] Continuity mode entry 恢复条件式需求澄清:清晰需求不机械追问,决策性歧义必须在 finalize/实施前向用户提问并等待回答;五平台生成契约覆盖。 -- [x] adapter digest algorithm、status 分类、stale error、explicit upgrade command 和 recovery contract 完成。 -- [x] policy parser 白名单、writer capability、`minWriterVersion`/reader gate 和 0.3.x + V2 manifest 拒绝 contract 完成;本轮已增加真实发布版 0.3.18 CLI 的黑盒写入拒绝与 authority 全树字节不变证据,后续候选仍需重跑。 -- [x] project upgrade dry-run、确认、commit、repair、abort 和 provenance 完成;现有 workflow 未被批量重写,新 `/man` Policy 2 默认值有明确创建证据。 -- [x] local transport 的完整原子 reframe、recovery、archive retention,以及与 child create、handoff create/start、solo handoff start 的真实并发竞争通过;没有复用 scope-change,也没有部分 authority 或 eligibility 绕过路径。 -- [x] 已升级/未升级项目、旧/新 workflow、local/git-ref transport 的自动化兼容矩阵通过;git-ref reframe 稳定返回 `MANCODE_REFRAME_GIT_REF_UNSUPPORTED`。 -- [x] 5 个 adapter renderer、Windows path、line ending、用户托管区和每个 target 写前/写后中断恢复在 `4dc2e7e` 通过;后续候选仍需重跑 dist/Windows/recovery gate。 -- [x] 平台证据区分 `host_session_verified` 与 `explicit_session_verified`;显式路径验证两个 active、不同且 client 匹配的 session,只满足发布证据,不提升运行时宿主信任。 -- [x] release-check 从远端候选创建干净 checkout,自动运行完整门禁、双 clone、legacy、audit、pack、tarball CLI/module smoke,并输出提交绑定的 SHA-256 证据;脚本不包含 npm publish 或 dist-tag 操作。 -- [ ] Claude Code、Codex、Cursor、GitHub Copilot、ZCode、Kimi Code、Qoder 的最终候选真实宿主验收,以及 [`docs/release-acceptance.md`](./release-acceptance.md) 要求的跨 clone、legacy、Beta gate 和干净 checkout tarball 验收。 -- [x] 按开发集成要求将候选 `4dc2e7e` 推送到远程 `develop`,且 Quality 与 Windows gate 均通过;这不等同于 release gate 通过。 -- [ ] 完成剩余发布验收后才允许进入 npm 发布检查;任何条件失败都不得发布 npm。 - -0.4.0 与 0.4.1 后来已经发布,但仓库内的未完成列表和外部 gate 证据没有在发布前闭合, -且 0.4.1 的 npm `gitHead` 不是旧 release-check 所要求的 `origin/develop` 提交。该偏差 -不通过事后勾选消除;0.4.2 按 [`docs/release-acceptance.md`](./release-acceptance.md) 的 -main-candidate 契约重新建立可审计证据。 - -## 11. 发布回滚与停止策略 - -| 发现的问题 | 立即动作 | 禁止动作 | 恢复方式 | -| --- | --- | --- | --- | -| advisory 模板误导但无 authority 写 | 停止发布、修模板、重跑 snapshot | 不要求用户迁移 workflow | 发布前替换 adapter/模板;已有 authority 不变 | -| adapter stale 或 renderer mismatch | 阻塞 mutation,提示显式 upgrade | 自动 `--force`、手工改 digest | upgrade dry-run → 确认 → journal repair | -| Policy 2 parser/门禁错误 | 关闭 Policy 2 创建入口 | 把 2 降为 1 或修改旧 workflow | 修复 CLI 后重新读取;project marker 保持可审计 | -| project upgrade 中断 | 标记 pending/repair,禁止普通写 | 直接删除半成品或手改 minWriter | `operation repair` 恢复 before/target 状态 | -| reframe 中断或前置条件冲突 | 保留 operation journal,维持 blocked/repair_required | 手动写 Step 2、释放部分 claim | 修复原 operation;无法证明时人工处理后再重试 | -| git-ref reframe 请求 | 稳定返回 `MANCODE_REFRAME_GIT_REF_UNSUPPORTED` | 只在本 clone 先执行、以后同步 | 本计划不提供恢复路径;后续若要支持,必须另立协议设计与发布评审 | - -版本回滚只回滚代码入口和默认创建策略,不回写历史 policy、requirements、plan 或 task step。任何需要修改现有权威内容的“回滚”都必须成为另一个明确、journaled 的 migration,并经过独立评审。 - -## 12. 实施顺序与停止/重新对齐条件 - -A–F 是已经发布的 0.4.0 能力基线。0.4.2 的修复和发布按以下顺序重新建立证据: - -1. [x] 记录 0.4.1 的 git-ref publication 与发布候选偏差,不把历史 CI 当作当前证据。 -2. [x] 为原子 mutation 的 tracked 投影提交增加 same-revision、same-aggregate、owner-only、fast-forward code-head rebind,并加入第二 clone resume 回归测试。 -3. [x] 将 release-check 改为要求 candidate 同时等于本地 HEAD 与 `origin/main`,并从 main 创建干净 checkout。 -4. [ ] 在 `develop` 完成实现、测试和文档评审后合并到 `main`,形成唯一 0.4.2 candidate。 -5. [ ] 在该 main SHA 上通过 [`docs/release-acceptance.md`](./release-acceptance.md) 的 Quality/Windows、release-check、真实宿主、跨宿主恢复、Beta 和 tarball 检查。 -6. [ ] 从同一 main checkout 发布 0.4.2,验证 npm version/integrity/`gitHead`,再为同一 SHA 创建 `v0.4.2` tag 与 GitHub Release。 - -任何阶段出现下列情况,都回到“证据和契约评审”,而不是继续编码: - -- 发现两个入口给同一任务不同的 owner、scope 或状态语义; -- authority writer 与 derived copy 的责任无法唯一确定; -- status、contract、policy 或完成门禁的意义需要改变; -- digest 只反映版本常量、无法重建 expected bytes,或跨平台结果不稳定; -- 旧 CLI 能读到新 policy 却没有可靠的 reader/writer/capability 拒绝; -- operation 无法在任意中断点证明“全前或全后”; -- 需求变化需要同时改 requirements、plan、claims、handoff 或 step,但当前 operation 没有覆盖它们。 - -重新对齐完成的最低证据是:新的目标、候选 owner、source of truth、兼容影响、停止条件和唯一 recommendation 已被记录,并由相应 authority writer 重新确认。仅仅修改 Markdown 或手动调整 step 不算重新对齐。 - -## 13. 0.4.0 已定稿的四个实现选择 - -以下选择已由 compatibility contract 固定;发布前不得在缺少重新评审和对应证据时改变: - -1. **Public create 基线:** 用未升级项目的端到端 create contract 证明只写 Policy 1;不得把 CLI 选项、模板默认值或 builder 默认值中的任一项当成唯一证据。 -2. **Digest 的持久化位置:** 固定采用“renderer 重建 expected digest + local 非权威 cache”,不在 V1 或 V2 持久化 content digest。性能不足必须另立设计,不能阻塞后临时扩 schema。 -3. **升级命令的公开名称:** 以现有 `adapter status/upgrade` 命令注册、JSON 输出和 shell 退出码测试确定名称;文档和实现必须只保留一个正式入口。 -4. **Capability 的编码:** 默认由 CLI 内置 capability set 加 project `minWriterVersion` 双门控制,不新增可被旧 CLI 忽略的 manifest capability 字段;local reframe 与 git-ref hard reject 必须使用不同 capability 结果。 - -这四项是已发布的兼容基线;0.4.2 不改变其语义。任何后续变更仍须重新通过对应 -contract 与 release gate。git-ref reframe 继续不在支持范围内。 - -## 14. 远程分支与 npm 发布顺序 - -0.4.2 及后续补丁统一使用“最终 main 提交发布”模型: - -1. 所有实现、修复和文档先在 `develop` 完成窄测试、完整自动化和评审。 -2. 将最终变更合并并推送到 `main`。该合并提交是唯一 release candidate;develop SHA、PR head SHA 和 main 的父提交都不能代替它。 -3. 等待该 main SHA 的 GitHub Quality 与 Windows required checks 成功。 -4. 在本地检出同一提交,运行 `npm run release:check -- --candidate <完整 SHA>`。脚本要求本地 HEAD 和 `origin/main` 都等于 candidate,从 main 创建干净 checkout,并确认检查期间 main 未变化。 -5. 使用 release-check 保留的 tarball 完成五平台真实宿主、跨宿主恢复和最终 Beta gate;所有证据必须绑定同一个 main SHA 与 package version。 -6. 全部门禁成功后才允许执行一次正式 npm 发布。随后核对 registry version、integrity 与 `gitHead`,确保仍指向 candidate。 -7. npm 核对成功后,为同一提交创建并推送版本 tag,发布 GitHub Release,并记录候选 SHA 与 tarball SHA-256。 - -任何代码、依赖、构建配置、版本号或发布文档变化都会产生新的 main candidate,并使旧证据失效。检查期间禁止并发更新 main;但“合并到 main”本身是建立候选的必要步骤,不再把 main 永久冻结在旧版本。 - -### 14.1 0.4.1 偏差与 0.4.2 处置 - -- 0.4.1 的 npm `gitHead` 是 `8cc8b24` main 合并提交,旧 release-check 却要求 candidate 等于 `origin/develop` 的 `aaa8d6c`;因此已发布提交无法通过当时脚本的身份断言。 -- 0.4.1 的最终 Quality/Windows checks 已成功,但仓库验收文档仍把这些和其他 gate 列为未完成,且没有 0.4.1 tag/GitHub Release。CI 成功不能代替候选绑定与证据收口。 -- 0.4.2 不追溯修改 0.4.1 证据。它改用 main-candidate 模型、补齐 code-head rebind 回归,并要求 npm、tag 和 GitHub Release 全部绑定最终 main SHA。 diff --git a/docs/platform-adapters.md b/docs/platform-adapters.md index 9632a7b..be9a740 100644 --- a/docs/platform-adapters.md +++ b/docs/platform-adapters.md @@ -25,6 +25,20 @@ mancode status manifest 的 `managedAdapters` key 是项目登记的 required 平台集合。greenfield init 只登记所选平台;后续新增或修复平台必须通过带 active session 和显式确认的 adapter upgrade journal。`AGENTS.md` 和 Copilot instruction 文件中托管区外的用户内容必须原样保留。 +## 内容完整性与升级 + +`adapter status` 每次都从当前 renderer 重建 expected managed bytes,并与磁盘上的 +actual managed bytes 比较。整文件 target 比较完整内容;嵌入式 target 只比较 mancode +托管区。磁盘内容不先规范化换行,因此 CRLF 漂移、截断和手工编辑都能被识别。 + +每个 required target 的状态为 `ready`、`missing`、`stale` 或 `unreadable`。只有全部 +required target 都是 `ready`,依赖 adapter 的 mutation 才能继续。manifest 只记录 +renderer/schema version 和 required inventory;content digest 是可重建结果,不能成为 +新的 manifest authority。 + +adapter upgrade 先在 staging 中生成预览,用户确认后再通过 journaled operation 发布。 +中断必须由原 operation repair;升级不能修改 workflow policy、requirements、plan 或 step。 + ## Bootstrap 合约 每个平台都必须: diff --git a/docs/release-acceptance.md b/docs/release-acceptance.md deleted file mode 100644 index be3f978..0000000 --- a/docs/release-acceptance.md +++ /dev/null @@ -1,53 +0,0 @@ -# 0.4.2 Continuity 发布验收 - -自动化 contract 必须覆盖 Continuity runtime/schema、条件式需求澄清、Policy 2、project upgrade、local reframe、operation recovery、团队协调、git-ref publication/rebind、五个平台生成结果和 Windows smoke path。条件式需求澄清必须证明清晰请求不被机械提问阻塞,决策性歧义会在 requirements finalize 或实施前停下、提问并等待用户回答。以下证据完成前,不得发布 mancode 0.4.2 或执行 `npm publish`。 - -## 唯一候选身份 - -- `develop` 只用于开发集成;最终 release candidate 是已经合并并推送到 `origin/main` 的单个提交。 -- 本地 `HEAD`、`origin/main`、Quality/Windows checks、release-check、真实宿主证据、Beta gate、待发布 package version 和 npm `gitHead` 必须指向同一完整 SHA。 -- 候选建立后,release-check 会记录检查前后的 `origin/main`。检查期间发生任何 main 更新都会使候选失效;后续修复必须形成新的 main 提交并重新执行受影响的全部门禁。 - -## 发布条件 - -- 在最终 main 候选运行 `npm run release:check -- --candidate <完整提交 SHA>`;该命令必须从 `origin/main` 创建干净 checkout,运行 `npm ci`、`npm run prepublishOnly`、跨 clone、legacy、audit、pack 和安装 smoke,并输出绑定提交与 tarball SHA-256 的本地 JSON 证据。 -- release-check 先运行 `npm pack --dry-run`,再执行实际 `npm pack`,保留生成的 tarball 并完成 CLI/module smoke;npm beta/rc 不能替代该字节级验证。 -- GitHub Quality gate 与 Windows required check 必须在同一个 main 候选成功;Windows 覆盖 CMD、PowerShell 和 Git Bash。 -- Claude Code、Codex、Cursor、GitHub Copilot、ZCode、Kimi Code 和 Qoder 都完成真实双窗口 session 验证。每个平台可证明受信宿主 session,或证明两个真实显式 session 隔离;显式证据只满足发布验收,不授权运行时信任宿主 key。 -- 每个平台证明子命令传播;支持子 agent 的平台还需证明继承,或记录合法的 `not_applicable` 原因。缺失、伪造、错误 client、关闭或碰撞的显式 session 证据必须拒绝。 -- 完成跨真实宿主 resume、claim、handoff 和恢复路径。 -- release-check 中两个真实独立 clone 完成 git-ref pull、并发 CAS、handoff、receipt recovery、代码基线交接,以及原子 mutation 投影提交后的同 revision code-head rebind 和第二 clone resume。 -- release-check 中的真实 0.3.18 legacy fixture 完成 stage、activation、每个中断点恢复和严格条件下的 rollback;人工 smoke 不能替代 crash matrix。 -- 最终执行 `mancode context beta --release-candidate --json` 返回 `ready: true` 和空 blockers。 -- 上述条件全部成功后,才允许从该 main checkout 发布 release-check 验证过的候选版本;发布后必须核对 npm version、integrity 与 `gitHead`。 -- npm 验证成功后,在同一个 candidate SHA 创建 `v0.4.2` tag 和 GitHub Release,并记录 tarball SHA-256。tag 或 Release 不得指向另一个提交,也不能替代发布前门禁。 - -## 发布顺序 - -1. 在 `develop` 完成实现、文档和本地验证,通过评审后合并到 `main`。 -2. 推送最终 main 合并提交,记录完整 candidate SHA,并等待该提交的 Quality 与 Windows checks 成功。 -3. 在本地检出同一 main 提交,从 `origin/main` 运行 release-check。 -4. 使用 release-check 保留的候选 tarball 完成真实宿主 spike、跨宿主恢复和最终 Beta gate。 -5. 确认所有证据仍绑定同一个 main SHA 后执行唯一一次正式 npm 发布,并核对 registry metadata。 -6. 为同一 SHA 创建并推送 `v0.4.2` tag,发布 GitHub Release 并附上候选与 tarball digest。 - -任何测试失败、main 提交变化、package version 变化或发布文档变化都会使已有验收失效。修复后必须形成新候选并重新验证;不得先发布 npm 再补证据。 - -## 证据规则 - -所有证据必须绑定同一个 `origin/main` commit、mancode version、操作系统和宿主版本。`develop` 提交、历史祖先提交、未推送提交或替代宿主结果只能用于调试,不能用于最终 Beta gate。 - -session evidence 不保存原始 key、token、绝对业务路径或任务正文。屏幕截图和日志应脱敏,并保存在发布流程约定的本地证据目录,而不是长期开发文档中。 - -## 当前未完成项 - -- 将 0.4.2 实现、测试和文档合并为最终 `origin/main` candidate。 -- 在最终 main 候选上完成 Quality gate 与 Windows CMD、PowerShell、Git Bash required gate。 -- 在最终 main 候选上完成 release-check,包括自动双 clone git-ref、code-head rebind、legacy、tarball SHA-256、安装和 CLI/module smoke。 -- 全部登记平台在最终候选上的完整 session spike,以及跨真实宿主协作与恢复验收。 -- ZCode 项目级 skill 发现和 workspace command 路径确认。 -- Kimi Code 项目级 skill 发现(桌面端/CLI)与 Qoder `.qoder/commands/` 命令发现(IDE/CLI)的真实宿主确认。 -- 汇总全部证据后的最终 Beta gate。 -- npm registry version/integrity/`gitHead` 核对,以及绑定同一提交的 `v0.4.2` tag 和 GitHub Release。 - -完成一项时更新本文件的未完成列表;不要创建新的平行验收计划。 diff --git a/package-lock.json b/package-lock.json index 4fe070d..d812d98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mancode", - "version": "0.5.5", + "version": "0.5.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mancode", - "version": "0.5.5", + "version": "0.5.6", "license": "AGPL-3.0-only", "dependencies": { "commander": "^12.1.0", @@ -24,7 +24,7 @@ "vitest": "^3.2.6" }, "engines": { - "node": ">=20" + "node": ">=22" } }, "node_modules/@ampproject/remapping": { @@ -1418,16 +1418,16 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/bundle-require": { @@ -1763,9 +1763,9 @@ "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -2059,9 +2059,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -2181,9 +2181,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -2201,7 +2201,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/package.json b/package.json index 3ccd77c..1da0505 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mancode", - "version": "0.5.5", + "version": "0.5.6", "description": "AI coding agent workflow harness with mancode Continuity for cross-conversation tasks, decisions, verification, and team coordination.", "type": "module", "license": "AGPL-3.0-only", @@ -37,7 +37,7 @@ "zcode" ], "engines": { - "node": ">=20" + "node": ">=22" }, "bin": { "mancode": "dist/cli.js" @@ -67,7 +67,7 @@ "lint": "biome check src tests", "typecheck": "tsc --noEmit", "format": "biome format --write src tests", - "prepublishOnly": "npm run lint && npm run typecheck && npm run build && npm run test:dist && npm test" + "prepublishOnly": "npm audit --audit-level=high && npm run lint && npm run typecheck && npm run build && npm run test:dist && npm run test:coverage" }, "dependencies": { "commander": "^12.1.0", diff --git a/scripts/context-economy-baseline.json b/scripts/context-economy-baseline.json deleted file mode 100644 index b7e2414..0000000 --- a/scripts/context-economy-baseline.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "schemaVersion": 1, - "tokenizerId": "cl100k_base@tiktoken-0.7.0", - "generatedAt": "2026-07-25T19:53:35.715Z", - "totals": { - "commandTokens": 25192, - "artifactTokens": 415 - }, - "commands": [ - { - "name": "init", - "command": "mancode init --yes --empty --platform claude-code --lang en", - "tokens": 63, - "chars": 148 - }, - { - "name": "status_brief", - "command": "mancode status --brief --json", - "tokens": 91, - "chars": 270 - }, - { - "name": "team_identity_create", - "command": "mancode team identity create --name Economy Fixture --json", - "tokens": 77, - "chars": 197 - }, - { - "name": "context_session_new", - "command": "mancode context session new --client mancode-cli --json", - "tokens": 194, - "chars": 531 - }, - { - "name": "workflow_create_man", - "command": "mancode workflow create man Measure mancode CLI token economy. --session --client mancode-cli --json", - "tokens": 1128, - "chars": 3503 - }, - { - "name": "workflow_requirements_finalize", - "command": "mancode workflow requirements local: finalize --expected-revision 1 --file /var/folders/3s/lycwtrpj607_k0w_fwz6_2pm0000gp/T/mancode-economy-S1zghL/requirements.json --session --client mancode-cli --json", - "tokens": 1823, - "chars": 5776 - }, - { - "name": "workflow_plan_revise", - "command": "mancode workflow plan local: revise --expected-revision 2 --plan-decision governed_execution --file /var/folders/3s/lycwtrpj607_k0w_fwz6_2pm0000gp/T/mancode-economy-S1zghL/plan.md --session --client mancode-cli --json", - "tokens": 1264, - "chars": 3757 - }, - { - "name": "context_show_orient", - "command": "mancode context show --task local: --purpose orient --level task --session --client mancode-cli --json", - "tokens": 1912, - "chars": 6539 - }, - { - "name": "context_show_plan", - "command": "mancode context show --task local: --purpose plan --level task --session --client mancode-cli --json", - "tokens": 2863, - "chars": 9929 - }, - { - "name": "context_show_implement", - "command": "mancode context show --task local: --purpose implement --level task --session --client mancode-cli --json", - "tokens": 2527, - "chars": 8721 - }, - { - "name": "context_show_review", - "command": "mancode context show --task local: --purpose review --level task --session --client mancode-cli --json", - "tokens": 2852, - "chars": 9717 - }, - { - "name": "context_show_verify", - "command": "mancode context show --task local: --purpose verify --level task --session --client mancode-cli --json", - "tokens": 2852, - "chars": 9717 - }, - { - "name": "context_show_handoff", - "command": "mancode context show --task local: --purpose handoff --level task --session --client mancode-cli --json", - "tokens": 3039, - "chars": 10295 - }, - { - "name": "context_show_review_full", - "command": "mancode context show --task local: --purpose review --level full --session --client mancode-cli --json", - "tokens": 1428, - "chars": 4921 - }, - { - "name": "workflow_review_apply", - "command": "mancode workflow review local: apply --expected-revision 3 --file /var/folders/3s/lycwtrpj607_k0w_fwz6_2pm0000gp/T/mancode-economy-S1zghL/review-ledger.json --session --client mancode-cli --json", - "tokens": 1418, - "chars": 4286 - }, - { - "name": "workflow_verify_apply", - "command": "mancode workflow verify local: apply --expected-revision 4 --file /var/folders/3s/lycwtrpj607_k0w_fwz6_2pm0000gp/T/mancode-economy-S1zghL/verification-ledger.json --session --client mancode-cli --json", - "tokens": 1506, - "chars": 4539 - }, - { - "name": "context_close", - "command": "mancode context close --session --json", - "tokens": 155, - "chars": 422 - } - ], - "artifacts": [ - { - "name": "requirements.json", - "tokens": 394, - "chars": 1634 - }, - { - "name": "plan.md", - "tokens": 21, - "chars": 91 - } - ] -} diff --git a/scripts/context-economy-report.mjs b/scripts/context-economy-report.mjs deleted file mode 100644 index 0a41d56..0000000 --- a/scripts/context-economy-report.mjs +++ /dev/null @@ -1,639 +0,0 @@ -/** - * Token-economy report for the mancode v3 CLI surface. - * - * Drives a full /man workflow against dist/cli.js in a temporary project, - * counts cl100k_base tokens (CONTEXT_PACK_TOKENIZER_ID) for every command - * receipt and for the agent-facing artifacts, and compares the result with - * the committed baseline. - * - * Usage: - * npm run build - * node scripts/context-economy-report.mjs [--output ] [--check] - * - * --check compares against scripts/context-economy-baseline.json and exits - * non-zero when any measurement drifts more than ±2%. The script is a manual - * / pre-release gate; it is intentionally not wired into CI. - */ -import { spawnSync } from 'node:child_process'; -import { randomBytes, createHash } from 'node:crypto'; -import { - access, - mkdtemp, - readdir, - readFile, - rm, - writeFile, -} from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import path from 'node:path'; -import process from 'node:process'; -import { fileURLToPath } from 'node:url'; -import { getEncoding } from 'js-tiktoken'; - -const sourceRoot = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '..', -); -const cliPath = path.join(sourceRoot, 'dist', 'cli.js'); -const baselinePath = path.join( - sourceRoot, - 'scripts', - 'context-economy-baseline.json', -); -const TOKENIZER_ID = 'cl100k_base@tiktoken-0.7.0'; -const TOLERANCE = 0.02; -const ULID_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; -const REQUIREMENT_DIMENSIONS = [ - 'platform', - 'core_scope', - 'technical_stack', - 'data_and_persistence', - 'performance', - 'compatibility', - 'security', -]; - -const options = parseOptions(process.argv.slice(2)); -if (options.help) { - console.log( - 'Usage: node scripts/context-economy-report.mjs [--output ] [--check]', - ); - process.exit(0); -} - -await access(cliPath).catch(() => { - throw new Error('dist/cli.js is missing; run `npm run build` first'); -}); - -const encoder = getEncoding('cl100k_base'); -const projectRoot = await mkdtemp(path.join(tmpdir(), 'mancode-economy-')); -const commands = []; -const artifacts = []; - -try { - const init = measure('init', [ - 'init', - '--yes', - '--empty', - '--platform', - 'claude-code', - '--lang', - 'en', - ]); - assert(init.exitCode === 0, `init failed:\n${init.stdout}`); - - measureOk('status_brief', ['status', '--brief', '--json']); - measureOk('team_identity_create', [ - 'team', - 'identity', - 'create', - '--name', - 'Economy Fixture', - '--json', - ]); - - const sessionReceipt = measureOk('context_session_new', [ - 'context', - 'session', - 'new', - '--client', - 'mancode-cli', - '--json', - ]); - const sessionId = JSON.parse(sessionReceipt.stdout).session.sessionId; - const sessionArgs = ['--session', sessionId, '--client', 'mancode-cli']; - - const createReceipt = measureOk('workflow_create_man', [ - 'workflow', - 'create', - 'man', - 'Measure mancode CLI token economy.', - ...sessionArgs, - '--json', - ]); - const created = JSON.parse(createReceipt.stdout); - const task = `${created.taskRef.namespace}:${created.taskRef.taskId}`; - - const requirementsPath = path.join(projectRoot, 'requirements.json'); - await writeFile( - requirementsPath, - `${JSON.stringify(semanticRequirementsFixture(), null, 2)}\n`, - ); - await measureArtifact('requirements.json', requirementsPath); - - const finalizeReceipt = measureOk('workflow_requirements_finalize', [ - 'workflow', - 'requirements', - task, - 'finalize', - '--expected-revision', - '1', - '--file', - requirementsPath, - ...sessionArgs, - '--json', - ]); - const finalized = JSON.parse(finalizeReceipt.stdout); - const requirements = finalized.requirements; - - const planPath = path.join(projectRoot, 'plan.md'); - await writeFile( - planPath, - '# Token economy plan\n\n1. Measure every v3 receipt.\n2. Compare with the committed baseline.\n', - ); - await measureArtifact('plan.md', planPath); - - const planReceipt = measureOk('workflow_plan_revise', [ - 'workflow', - 'plan', - task, - 'revise', - '--expected-revision', - String(finalized.metadata.revision), - '--plan-decision', - 'governed_execution', - '--file', - planPath, - ...sessionArgs, - '--json', - ]); - const planned = JSON.parse(planReceipt.stdout); - const planVersion = planned.metadata.governance.planVersion; - - for (const purpose of [ - 'orient', - 'plan', - 'implement', - 'review', - 'verify', - 'handoff', - ]) { - measureOk(`context_show_${purpose}`, [ - 'context', - 'show', - '--task', - task, - '--purpose', - purpose, - '--level', - 'task', - ...sessionArgs, - '--json', - ]); - } - - const fullPack = JSON.parse( - measureOk('context_show_review_full', [ - 'context', - 'show', - '--task', - task, - '--purpose', - 'review', - '--level', - 'full', - ...sessionArgs, - '--json', - ]).stdout, - ); - assert(fullPack.pack !== undefined, 'full context pack is missing'); - // The task starts with empty on-disk ledgers; the apply inputs derive from - // them the same way the workflow fixtures do. - const previousReview = JSON.parse( - await readFile(await findTaskFile('review-ledger.json'), 'utf8'), - ); - const previousVerification = JSON.parse( - await readFile(await findTaskFile('verification-ledger.json'), 'utf8'), - ); - - const reviewPath = path.join(projectRoot, 'review-ledger.json'); - await writeFile( - reviewPath, - `${JSON.stringify( - passedReview(previousReview, requirements.contentDigest, planVersion), - null, - 2, - )}\n`, - ); - const reviewReceipt = measureOk('workflow_review_apply', [ - 'workflow', - 'review', - task, - 'apply', - '--expected-revision', - String(planned.metadata.revision), - '--file', - reviewPath, - ...sessionArgs, - '--json', - ]); - const reviewed = JSON.parse(reviewReceipt.stdout); - - const verificationPath = path.join(projectRoot, 'verification-ledger.json'); - await writeFile( - verificationPath, - `${JSON.stringify( - passedVerification( - previousVerification, - requirements, - planVersion, - reviewed.review.remediationRound, - ), - null, - 2, - )}\n`, - ); - measureOk('workflow_verify_apply', [ - 'workflow', - 'verify', - task, - 'apply', - '--expected-revision', - String(reviewed.metadata.revision), - '--file', - verificationPath, - ...sessionArgs, - '--json', - ]); - - measureOk('context_close', [ - 'context', - 'close', - '--session', - sessionId, - '--json', - ]); - - const report = buildReport(); - printTable(report); - if (options.output) { - const outputPath = path.resolve(sourceRoot, options.output); - await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`); - console.log(`\nReport written to ${outputPath}`); - } - if (options.check) { - const baseline = JSON.parse(await readFile(baselinePath, 'utf8')); - const failures = compareWithBaseline(report, baseline); - if (failures.length > 0) { - console.error('\nBaseline check failed:'); - for (const failure of failures) console.error(` - ${failure}`); - process.exitCode = 1; - } else { - console.log( - `\nBaseline check passed (tolerance ±${TOLERANCE * 100}%): ${baselinePath}`, - ); - } - } -} finally { - await rm(projectRoot, { recursive: true, force: true }); -} - -function measure(name, args) { - const result = spawnSync(process.execPath, [cliPath, ...args], { - cwd: projectRoot, - encoding: 'utf8', - env: { ...process.env, MANCODE_SESSION_ID: undefined }, - }); - if (result.error) throw result.error; - const stdout = result.stdout ?? ''; - const normalized = normalize(stdout); - commands.push({ - name, - command: `mancode ${args.join(' ')}`, - exitCode: result.status ?? -1, - tokens: countTokens(normalized), - chars: normalized.length, - }); - return { exitCode: result.status ?? -1, stdout, stderr: result.stderr ?? '' }; -} - -function measureOk(name, args) { - const result = measure(name, args); - assert( - result.exitCode === 0, - `${name} failed (${result.exitCode}):\n${result.stdout}\n${result.stderr}`, - ); - return result; -} - -async function measureArtifact(name, filePath) { - const content = normalize(await readFile(filePath, 'utf8')); - artifacts.push({ - name, - tokens: countTokens(content), - chars: content.length, - }); -} - -/** Locates one authoritative task file under .mancode without assuming layout. */ -async function findTaskFile(fileName) { - const matches = []; - const queue = [path.join(projectRoot, '.mancode')]; - while (queue.length > 0) { - const directory = queue.pop(); - const entries = await readdir(directory, { withFileTypes: true }).catch( - () => [], - ); - for (const entry of entries) { - const entryPath = path.join(directory, entry.name); - if (entry.isDirectory()) queue.push(entryPath); - else if (entry.name === fileName) matches.push(entryPath); - } - } - assert( - matches.length === 1, - `expected exactly one ${fileName} under .mancode, found ${matches.length}`, - ); - return matches[0]; -} - -/** - * Run-to-run jitter sources are replaced with stable placeholders before - * counting tokens: the temporary project root, ULIDs, sha256 digests, and - * ISO timestamps (fixed-length, but BPE token counts vary per value). - */ -function normalize(text) { - return text - .split(projectRoot) - .join('') - .split(path.basename(projectRoot)) - .join('') - .replace(/sha256:[0-9a-f]{64}/g, `sha256:${'0'.repeat(64)}`) - .replace(/\b[0-9A-HJKMNP-TV-Z]{26}\b/g, '01ARZ3NDEKTSV4RRFFQ69G5FAV') - .replace( - /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z/g, - '2026-01-01T00:00:00.000Z', - ); -} - -function countTokens(text) { - return encoder.encode(text).length; -} - -function buildReport() { - const commandTotal = commands.reduce((sum, entry) => sum + entry.tokens, 0); - return { - schemaVersion: 1, - tokenizerId: TOKENIZER_ID, - generatedAt: new Date().toISOString(), - totals: { - commandTokens: commandTotal, - artifactTokens: artifacts.reduce((sum, entry) => sum + entry.tokens, 0), - }, - commands: commands.map(({ name, command, tokens, chars }) => ({ - name, - command: command.replace(/local:[0-7][0-9A-HJKMNP-TV-Z]{25}/g, 'local:').replace(/01[0-9A-HJKMNP-TV-Z]{24}/g, ''), - tokens, - chars, - })), - artifacts, - }; -} - -function printTable(report) { - const width = Math.max(...report.commands.map((entry) => entry.name.length)); - console.log(`Token economy report (${report.tokenizerId})`); - console.log('-'.repeat(width + 20)); - for (const entry of report.commands) { - console.log( - `${entry.name.padEnd(width)} ${String(entry.tokens).padStart(7)} tokens`, - ); - } - console.log('-'.repeat(width + 20)); - console.log( - `${'total (commands)'.padEnd(width)} ${String(report.totals.commandTokens).padStart(7)} tokens`, - ); - for (const artifact of report.artifacts) { - console.log( - `${`artifact ${artifact.name}`.padEnd(width)} ${String(artifact.tokens).padStart(7)} tokens`, - ); - } -} - -function compareWithBaseline(report, baseline) { - const failures = []; - const baselineCommands = new Map( - baseline.commands.map((entry) => [entry.name, entry]), - ); - for (const entry of report.commands) { - const reference = baselineCommands.get(entry.name); - if (!reference) { - failures.push(`command ${entry.name} missing from the baseline`); - continue; - } - if (outsideTolerance(entry.tokens, reference.tokens)) { - failures.push( - `command ${entry.name}: ${entry.tokens} tokens vs baseline ${reference.tokens}`, - ); - } - } - for (const reference of baseline.commands) { - if (!report.commands.some((entry) => entry.name === reference.name)) { - failures.push(`baseline command ${reference.name} was not measured`); - } - } - if (outsideTolerance(report.totals.commandTokens, baseline.totals.commandTokens)) { - failures.push( - `command total: ${report.totals.commandTokens} tokens vs baseline ${baseline.totals.commandTokens}`, - ); - } - return failures; -} - -function outsideTolerance(actual, expected) { - if (expected === 0) return actual !== 0; - return Math.abs(actual - expected) / expected > TOLERANCE; -} - -function semanticRequirementsFixture() { - return { - version: 1, - goal: 'Measure the token cost of every v3 CLI receipt.', - confirmedScope: ['Execute the full man workflow against dist/cli.js'], - excludedScope: ['Change any command behavior during measurement'], - technicalDecisions: ['Count tokens with the cl100k_base encoding'], - defaults: [], - blockingUnknowns: [], - coverage: REQUIREMENT_DIMENSIONS.map((dimension) => ({ - dimension, - status: dimension === 'technical_stack' ? 'confirmed' : 'defaulted', - rationale: `Considered ${dimension} for the measurement fixture.`, - })), - acceptanceCriteria: [ - { - id: 'AC-1', - description: 'Every workflow receipt is captured and tokenized.', - required: true, - method: 'automated', - }, - ], - }; -} - -function passedReview(previous, requirementsDigest, planVersion) { - const draft = { - ...previous, - revision: previous.revision + 1, - status: 'passed', - requirementsDigest, - planVersion, - requiredDomains: ['quality'], - domains: [{ domain: 'quality', status: 'passed', reportRef: null }], - blockers: [], - remediationRound: 0, - skip: null, - contentDigest: '', - lastOperationId: ulid(), - updatedAt: new Date().toISOString(), - }; - return { ...draft, contentDigest: reviewLedgerDigest(draft) }; -} - -function passedVerification(previous, requirements, planVersion, remediationRound) { - const criterion = requirements.acceptanceCriteria[0]; - assert(criterion !== undefined, 'finalized requirements are missing AC-1'); - const draft = { - ...previous, - revision: previous.revision + 1, - status: 'passed', - requirementsDigest: requirements.contentDigest, - planVersion, - remediationRound, - checks: [ - { - displayId: criterion.displayId, - legacyId: criterion.legacyId, - checkId: ulid(), - criterionId: criterion.criterionId, - required: criterion.required, - verificationRequirement: criterion.verificationRequirement, - automated: { - evidenceId: ulid(), - status: 'passed', - summary: 'Token economy fixture verification passed.', - command: 'npm test', - exitCode: 0, - artifactRef: null, - confirmedByActorId: null, - confirmationSource: null, - updatedAt: new Date().toISOString(), - }, - manual: null, - }, - ], - contentDigest: '', - lastOperationId: ulid(), - updatedAt: new Date().toISOString(), - }; - return { ...draft, contentDigest: verificationLedgerDigest(draft) }; -} - -/** Mirrors src/context/review-ledger.ts reviewLedgerDigest. */ -function reviewLedgerDigest(ledger) { - return digestCanonicalJson({ - schemaVersion: ledger.schemaVersion, - canonicalizationVersion: ledger.canonicalizationVersion, - taskRef: ledger.taskRef, - status: ledger.status, - depth: ledger.depth, - requirementsDigest: ledger.requirementsDigest, - planVersion: ledger.planVersion, - requiredDomains: ledger.requiredDomains, - domains: ledger.domains, - blockers: ledger.blockers, - remediationRound: ledger.remediationRound, - skip: ledger.skip, - legacySource: ledger.legacySource, - }); -} - -/** Mirrors src/context/verification-ledger.ts verificationLedgerDigest. */ -function verificationLedgerDigest(ledger) { - return digestCanonicalJson({ - schemaVersion: ledger.schemaVersion, - canonicalizationVersion: ledger.canonicalizationVersion, - taskRef: ledger.taskRef, - status: ledger.status, - requirementsDigest: ledger.requirementsDigest, - planVersion: ledger.planVersion, - remediationRound: ledger.remediationRound, - checks: ledger.checks, - legacySource: ledger.legacySource, - }); -} - -/** RFC 8785-style canonical JSON (sorted keys), same as src/context/canonical.ts. */ -function digestCanonicalJson(value) { - const canonical = canonicalize(value); - return `sha256:${createHash('sha256').update(canonical, 'utf8').digest('hex')}`; -} - -function canonicalize(value) { - if (value === null) return 'null'; - if (typeof value === 'string' || typeof value === 'boolean') { - return JSON.stringify(value); - } - if (typeof value === 'number') { - assert(Number.isSafeInteger(value), 'canonical numbers must be safe integers'); - return JSON.stringify(value); - } - if (Array.isArray(value)) { - return `[${value.map((item) => canonicalize(item)).join(',')}]`; - } - if (typeof value === 'object') { - const keys = Object.keys(value).sort(); - return `{${keys - .map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`) - .join(',')}}`; - } - throw new Error('canonical JSON only accepts plain JSON values'); -} - -function ulid(now = Date.now()) { - let time = ''; - let remaining = now; - for (let index = 0; index < 10; index += 1) { - time = ULID_ALPHABET[remaining % 32] + time; - remaining = Math.floor(remaining / 32); - } - const bytes = randomBytes(10); - let random = ''; - let bits = 0; - let accumulator = 0; - for (const byte of bytes) { - accumulator = (accumulator << 8) | byte; - bits += 8; - while (bits >= 5) { - random += ULID_ALPHABET[(accumulator >>> (bits - 5)) & 31]; - bits -= 5; - } - } - return time + random; -} - -function parseOptions(args) { - const parsed = { output: null, check: false, help: false }; - for (let index = 0; index < args.length; index += 1) { - const argument = args[index]; - if (argument === '--help') { - parsed.help = true; - continue; - } - if (argument === '--check') { - parsed.check = true; - continue; - } - if (argument !== '--output') { - throw new Error(`unknown context-economy-report argument: ${argument}`); - } - const value = args[index + 1]; - assert(value !== undefined, '--output requires a value'); - parsed.output = value; - index += 1; - } - return parsed; -} - -function assert(condition, message) { - if (!condition) throw new Error(message); -} diff --git a/scripts/release-check.mjs b/scripts/release-check.mjs index 076ec8d..feb6942 100644 --- a/scripts/release-check.mjs +++ b/scripts/release-check.mjs @@ -112,11 +112,14 @@ try { ); const audit = JSON.parse( - runCaptured('npm', ['audit', '--omit=dev', '--json'], checkout), + runCaptured('npm', ['audit', '--audit-level=high', '--json'], checkout), ); - const vulnerabilities = audit.metadata?.vulnerabilities?.total; - assert(vulnerabilities === 0, 'production dependency audit is not clean'); - checks.push({ name: 'production_audit', status: 'passed' }); + const vulnerabilities = audit.metadata?.vulnerabilities; + assert( + vulnerabilities?.high === 0 && vulnerabilities?.critical === 0, + 'dependency audit contains high or critical vulnerabilities', + ); + checks.push({ name: 'dependency_audit', status: 'passed' }); const dryRun = parseSinglePackResult( runCaptured('npm', ['pack', '--dry-run', '--json'], checkout), diff --git a/src/cli.ts b/src/cli.ts index 652854e..ed7a713 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,5 +1,7 @@ #!/usr/bin/env node -import { Option, program } from 'commander'; +import { realpathSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { Command, Option } from 'commander'; import { adapterStatus, adapterUpgrade } from './commands/adapter.js'; import { contextBeta, @@ -66,1034 +68,1120 @@ import { } from './commands/team.js'; import { uninstall } from './commands/uninstall.js'; import { version } from './commands/version.js'; +import { WORKFLOW_SUBCOMMANDS } from './commands/workflow-subcommands.js'; import { workflow } from './commands/workflow.js'; import { VERSION } from './version.js'; -program - .name('mancode') - .description( - 'AI coding agent harness. Modes: solo, man, manba, manteam, manps.', - ) - .version(VERSION); - -program - .command('init') - .description('Initialize mancode in the current project') - .option('--force', 'Reinstall even if already initialized') - .option('--yes', 'Skip all confirmations (CI mode)') - .option('--team', 'Force enable team mode (MVP-2)') - .option('--no-team', 'Force disable team mode (MVP-2)') - .option('--style ', 'Specify aesthetic style (MVP-2)') - .option('--platform ', 'Adapters: comma-separated names or all') - .option('--empty', 'Initialize a safe empty directory as a generic project') - .addOption(new Option('--v3').hideHelp()) - .option('--legacy', 'Use the legacy state.json initializer') - .option('--lang ', 'Initialization language: zh-CN or en') - .action(async (options) => { - const code = await init(process.cwd(), { - ...options, - fromCli: true, - interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY), +export function createCliProgram(): Command { + const program = new Command(); + + program + .name('mancode') + .description( + 'AI coding agent harness. Modes: solo, man, manba, manteam, manps.', + ) + .version(VERSION); + + program + .command('init') + .description('Initialize mancode in the current project') + .option('--force', 'Reinstall even if already initialized') + .option('--yes', 'Skip all confirmations (CI mode)') + .option('--team', 'Force enable team mode') + .option('--no-team', 'Force disable team mode') + .option( + '--style ', + 'Legacy aesthetic style (only supported with mancode init --legacy)', + ) + .option('--platform ', 'Adapters: comma-separated names or all') + .option('--empty', 'Initialize a safe empty directory as a generic project') + .addOption(new Option('--v3').hideHelp()) + .option('--legacy', 'Use the legacy state.json initializer') + .option('--lang ', 'Initialization language: zh-CN or en') + .action(async (options) => { + const code = await init(process.cwd(), { + ...options, + fromCli: true, + interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY), + }); + process.exitCode = code; }); - process.exitCode = code; - }); - -program - .command('install [platform]') - .description( - 'Install platform adapter (claude-code, cursor, codex, copilot, zcode, kimi-code, qoder)', - ) - .option('--force', 'Reinstall even if already installed') - .option('--minimal', 'Minimal install (MVP-2)') - .option( - '--shadow', - 'Stage a mancode bootstrap candidate without changing live files', - ) - .option('--confirm', 'Confirm a journaled adapter install or repair') - .option('--operation-id ', 'Operation ID returned by adapter dry-run') - .option('--session ', 'mancode session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .action(async (platform, options) => { - const code = await install( - process.cwd(), - platform ?? 'claude-code', - options, - ); - process.exitCode = code; - }); - -const adapterProgram = program - .command('adapter') - .description('Inspect and explicitly upgrade managed platform adapters'); - -adapterProgram - .command('status') - .description('Inspect managed adapter content on disk') - .option('--platform ', 'Inspect one platform adapter') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await adapterStatus(process.cwd(), options); - }); - -adapterProgram - .command('upgrade') - .description('Preview or repair managed adapter content') - .option('--all', 'Upgrade all platform adapters') - .option('--platform ', 'Upgrade one platform adapter') - .option('--dry-run', 'Stage and report changes without writing live targets') - .option('--confirm', 'Confirm the journaled adapter upgrade') - .option('--operation-id ', 'Operation ID returned by adapter dry-run') - .option('--session ', 'mancode session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await adapterUpgrade(process.cwd(), options); - }); - -program - .command('status') - .description('Show current mancode project status') - .option('--json', 'Output as JSON (for scripts)') - .option('--brief', 'Output compact mancode Continuity runtime status') - .action(async (options) => { - const code = await status(process.cwd(), options); - process.exitCode = code; - }); - -const projectProgram = program - .command('project') - .description('Manage project-level mancode policy and compatibility'); - -projectProgram - .command('upgrade') - .description('Upgrade project governance policy explicitly') - .requiredOption('--policy ', 'Target planning policy version (2)') - .option('--dry-run', 'Preview the upgrade without writing') - .option('--operation-id ', 'Operation ID returned by project dry-run') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await projectUpgrade(process.cwd(), options); - }); - -program - .command('list-platforms') - .description('List available and installed mancode platform adapters') - .action(async () => { - const code = await listPlatforms(process.cwd()); - process.exitCode = code; - }); - -program - .command('uninstall [platform]') - .description('Remove platform adapter or all mancode artifacts') - .option('--force', 'Skip confirmation message') - .option('--all', 'Remove everything including .mancode/ directory') - .action(async (platform, options) => { - const code = await uninstall(process.cwd(), platform, options); - process.exitCode = code; - }); - -program - .command('workflow [args...]') - .description('Manage mancode workflows') - .option('--dry-run', 'Preview clean without deleting') - .option('--older-than ', 'Clean workflows older than (e.g. 30d)') - .option('--step ', 'Update workflow current step') - .option('--status ', 'Update workflow status') - .option( - '--parent-task ', - 'Parent /man or /manteam workflow for manba', - ) - .option('--parent ', 'Parent TaskRef for a manba child') - .option( - '--participant ', - 'Invite a joined team participant', - collectOption, - [], - ) - .option('--visibility ', 'Task visibility: local or shared') - .option('--coordination ', 'Task coordination: single or team') - .option('--session ', 'mancode session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--expected-revision ', 'Expected task revision for mutations') - .option('--checkpoint-id ', 'Checkpoint ID for a requirements reframe') - .option('--child-revision ', 'Expected child task revision for merge') - .option('--summary ', 'Privacy-screened child result summary') - .option('--next-action ', 'Next parent action after a child merge') - .option('--sync', 'Publish shared mutations through git-ref transport') - .option( - '--confirm-shared', - 'Confirm that task metadata may enter shared mancode authority', - ) - .option('--blocking-reason ', 'Explain why a workflow is blocked') - .option('--outcome ', 'Set manba outcome when completing a task') - .option('--plan-version ', 'Set the next man/manteam plan revision') - .option( - '--requirements-status ', - 'Planning readiness: ready or needs_clarification', - ) - .option( - '--plan-decision ', - 'Plan gate choice: plan_only or governed_execution', - ) - .option('--to ', 'Workflow handoff target (solo)') - .option('--complete', 'Complete an active solo handoff') - .option( - '--skipped ', - 'Policy v2: clarification only; use workflow review skip for review', - ) - .option('--review-depth ', 'Review depth: targeted or full') - .option('--review-domain ', 'Review domain: quality or security') - .option( - '--report ', - 'Relative Markdown report path for a review domain', - ) - .option('--blockers ', 'Comma-separated blocker ids found by a review') - .option( - '--resolved ', - 'Comma-separated blocker ids resolved in remediation', - ) - .option( - '--file ', - 'Semantic requirements JSON, plan Markdown, or ledger input file', - ) - .option( - '--scope-file ', - 'Plan implementation scope JSON {include,exclude,modules}', - ) - .option('--acceptance ', 'Acceptance criterion id (for example AC-1)') - .option('--method ', 'Verification method: automated or manual') - .option('--result ', 'Verification result') - .option('--evidence ', 'Verification evidence or user confirmation') - .option('--command ', 'Command used for automated verification') - .option('--exit-code ', 'Exit code from automated verification') - .option('--evidence-file ', 'Existing verification report or artifact') - .option('--reason ', 'Reason for an explicit review skip') - .option('--json', 'Output as JSON (for scripts)') - .action(async (subcommand, args, options) => { - const code = await workflow(process.cwd(), subcommand, args ?? [], { - ...options, - participants: - options.participant.length === 0 ? undefined : options.participant, + + program + .command('install [platform]') + .description( + 'Install platform adapter (claude-code, cursor, codex, copilot, zcode, kimi-code, qoder)', + ) + .option('--force', 'Reinstall even if already installed') + .option( + '--minimal', + 'Retained for legacy compatibility; Continuity bootstrap is already minimal', + ) + .option( + '--shadow', + 'Stage a mancode bootstrap candidate without changing live files', + ) + .option('--confirm', 'Confirm a journaled adapter install or repair') + .option('--operation-id ', 'Operation ID returned by adapter dry-run') + .option( + '--session ', + 'mancode session ID (otherwise MANCODE_SESSION_ID)', + ) + .option('--client ', 'Client identity (default: mancode-cli)') + .action(async (platform, options) => { + const code = await install( + process.cwd(), + platform ?? 'claude-code', + options, + ); + process.exitCode = code; }); - process.exitCode = code; - }); - -const contextProgram = program - .command('context') - .description('Resolve mancode task context and manage explicit sessions'); - -contextProgram - .command('show') - .description('Resolve one mancode Context Pack') - .option('--task ', 'Explicit TaskRef') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--level ', 'bootstrap, task, or full') - .option( - '--purpose ', - 'orient, plan, implement, review, verify, or handoff', - ) - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await contextShow(process.cwd(), options); - }); - -const contextSessionProgram = contextProgram - .command('session') - .description('Manage mancode session identities'); - -contextSessionProgram - .command('new') - .description('Create an explicit bootstrap session') - .requiredOption('--client ', 'Client identity') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await contextSessionNew(process.cwd(), options); - }); - -contextSessionProgram - .command('show') - .description('Show one explicit session without changing it') - .requiredOption('--session ', 'Session ID') - .option('--client ', 'Expected client identity') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await contextSessionShow(process.cwd(), options); - }); - -contextSessionProgram - .command('spike') - .description('Record real-host session evidence without persisting host keys') - .requiredOption( - '--platform ', - 'claude-code, codex, cursor, copilot, zcode, kimi-code, or qoder', - ) - .requiredOption('--session-mode ', 'Evidence path: host or explicit') - .requiredOption( - '--host-session-source ', - 'hook_stdin, environment, api, or none for explicit sessions', - ) - .requiredOption( - '--command-propagation ', - 'Real host child-command result: proven, not_proven, not_tested, or not_applicable', - ) - .requiredOption( - '--subagent-inheritance ', - 'Real host child-agent result: proven, not_proven, not_tested, or not_applicable', - ) - .option( - '--subagent-inheritance-reason ', - 'Required when child-agent inheritance is not applicable', - ) - .option( - '--hook-approval ', - 'approved, unapproved, unknown, or not_applicable', - ) - .requiredOption( - '--host-version ', - 'Installed host version used for the spike', - ) - .requiredOption( - '--release-candidate ', - 'Immutable mancode release candidate or source commit identifier', - ) - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await contextSessionSpike(process.cwd(), options); - }); - -contextProgram - .command('resume ') - .description('Validate and bind the current session to a mancode TaskRef') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--sync', 'Publish shared mutations through git-ref transport') - .option('--json', 'Output as JSON (for scripts)') - .action(async (task, options) => { - process.exitCode = await contextResume(process.cwd(), task, options); - }); - -contextProgram - .command('close') - .description('Close one explicit session without affecting other sessions') - .requiredOption('--session ', 'Session ID') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await contextClose(process.cwd(), options); - }); - -contextProgram - .command('doctor') - .description('Inspect unfinished mancode operations or repair one explicitly') - .option( - '--repair ', - 'Repair this operation with its original session', - ) - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await contextDoctor(process.cwd(), options); - }); - -contextProgram - .command('diagnostics [action]') - .description('Show or configure local aggregate diagnostics') - .option('--json', 'Output as JSON (for scripts)') - .action(async (action, options) => { - process.exitCode = await contextDiagnostics(process.cwd(), action, options); - }); - -contextProgram - .command('compact') - .description('List and remove eligible mancode runtime retention candidates') - .option('--task ', 'Compact checkpoints for one completed task') - .option('--dry-run', 'Show the deletion list without changing files') - .option('--apply-shared', 'Permit deletion for shared completed tasks') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await contextCompact(process.cwd(), options); - }); - -contextProgram - .command('beta', { hidden: true }) - .description('Evaluate internal release-evidence gates') - .requiredOption( - '--release-candidate ', - 'Release candidate that must match every platform evidence record', - ) - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await contextBeta(process.cwd(), options); - }); - -contextProgram - .command('publish ') - .description('Create a privacy-screened shared man successor') - .requiredOption('--expected-revision ', 'Current local task revision') - .requiredOption( - '--confirm-shared', - 'Confirm that the screened task authority may enter shared storage', - ) - .option('--dry-run', 'Validate the publish preflight without writing') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (task, options) => { - process.exitCode = await contextPublish(process.cwd(), task, options); - }); - -contextProgram - .command('reconcile-task-head ') - .description( - 'Adopt a Git-sourced shared aggregate through an explicit fence CAS', - ) - .requiredOption( - '--expected-fence-revision ', - 'Current shared task-head fence revision', - ) - .requiredOption( - '--from-git', - 'Confirm the checked-out aggregate came from Git', - ) - .option('--dry-run', 'Validate adoption without changing the task-head fence') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (task, options) => { - process.exitCode = await contextReconcileTaskHead( - process.cwd(), - task, - options, - ); - }); - -contextProgram - .command('glossary ') - .description('Manage the user-confirmed shared project glossary') - .option('--term ', 'Glossary term (add, update, remove)') - .option('--definition ', 'Term definition (add, update)') - .option( - '--alias ', - 'Term alias; repeat for multiple aliases (add, update)', - (value: string, previous: string[]) => [...previous, value], - [] as string[], - ) - .option('--task ', 'Source shared TaskRef (add, update)') - .option( - '--expected-revision ', - 'Current glossary revision (0 for an empty glossary)', - ) - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (action, options) => { - process.exitCode = await contextGlossary(process.cwd(), action, options); - }); - -const contextWorktreeProgram = contextProgram - .command('worktree') - .description('Register and inspect the current mancode checkout binding'); - -contextWorktreeProgram - .command('register') - .description( - 'Register this linked worktree before using mancode coordination', - ) - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await contextWorktreeRegister(process.cwd(), options); - }); - -const operationProgram = program - .command('operation') - .description('Inspect and recover durable mancode operations'); - -operationProgram - .command('show ') - .description('Show one operation journal and its recovery disposition') - .option('--json', 'Output as JSON (for scripts)') - .action(async (operationId, options) => { - process.exitCode = await operationShow(process.cwd(), operationId, options); - }); - -operationProgram - .command('repair ') - .description('Repair an operation using its original actor and session') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (operationId, options) => { - process.exitCode = await operationRepair( - process.cwd(), - operationId, - options, - ); - }); - -operationProgram - .command('abort ') - .description( - 'Abort only an operation proven to have no visible business write', - ) - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (operationId, options) => { - process.exitCode = await operationAbort( - process.cwd(), - operationId, - options, - ); - }); - -const teamProgram = program - .command('team') - .description('Manage mancode local identity and local-team membership'); - -teamProgram - .command('status') - .description('Show mancode team policy, transport, and local identity state') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await teamStatus(process.cwd(), options); - }); - -teamProgram - .command('policy ') - .description('Set the mancode team recommendation policy with a revision CAS') - .requiredOption('--expected-revision ', 'Current team policy revision') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (policy, options) => { - process.exitCode = await teamPolicy(process.cwd(), { - ...options, - policy, + + const adapterProgram = program + .command('adapter') + .description('Inspect and explicitly upgrade managed platform adapters'); + + adapterProgram + .command('status') + .description('Inspect managed adapter content on disk') + .option('--platform ', 'Inspect one platform adapter') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await adapterStatus(process.cwd(), options); }); - }); - -teamProgram - .command('conflicts') - .description( - 'Inspect local claim conflicts and handoffs without mutating coordination', - ) - .option('--task ', 'Narrow the report to one shared TaskRef') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await teamConflicts(process.cwd(), options); - }); - -const teamTransportProgram = teamProgram - .command('transport') - .description('Inspect and migrate the coordination authority'); - -teamTransportProgram - .command('status') - .description('Show active coordination transport and freshness') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await teamTransportStatus(process.cwd(), options); - }); - -teamTransportProgram - .command('set ') - .description( - 'Switch an empty coordination authority; otherwise use transport migrate', - ) - .requiredOption( - '--expected-config-revision ', - 'Current project config revision', - ) - .option( - '--remote ', - 'Git remote for a git-ref target (default: origin)', - ) - .option('--dry-run', 'Validate the empty-authority switch without writing') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (mode, options) => { - process.exitCode = await teamTransportSet(process.cwd(), { - ...options, - mode, + + adapterProgram + .command('upgrade') + .description('Preview or repair managed adapter content') + .option('--all', 'Upgrade all platform adapters') + .option('--platform ', 'Upgrade one platform adapter') + .option( + '--dry-run', + 'Stage and report changes without writing live targets', + ) + .option('--confirm', 'Confirm the journaled adapter upgrade') + .option('--operation-id ', 'Operation ID returned by adapter dry-run') + .option( + '--session ', + 'mancode session ID (otherwise MANCODE_SESSION_ID)', + ) + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await adapterUpgrade(process.cwd(), options); }); - }); - -teamTransportProgram - .command('migrate') - .description('Journal a single-authority local/git-ref transport switch') - .requiredOption('--to ', 'Target authority: local or git-ref') - .requiredOption( - '--expected-config-revision ', - 'Current project config revision', - ) - .option( - '--remote ', - 'Git remote for a git-ref target (default: origin)', - ) - .option('--dry-run', 'Validate and preview without writing authority state') - .option('--confirm', 'Explicitly confirm the authority migration') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await teamTransportMigrate(process.cwd(), options); - }); - -teamTransportProgram - .command('recover ') - .description('Repair forward or safely abort a transport migration') - .requiredOption('--to ', 'Original target authority: local or git-ref') - .option('--remote ', 'Original Git remote for a git-ref target') - .option('--abort', 'Abort only before the target authority is established') - .option('--session ', 'Original migration session ID') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (operationId, options) => { - process.exitCode = await teamTransportRecover( - process.cwd(), - operationId, - options, - ); - }); - -const teamSyncProgram = teamProgram - .command('sync') - .description('Explicitly synchronize the git-ref coordination authority'); - -teamSyncProgram - .command('pull') - .description('Fetch, validate, and cache refs/mancode/team') - .option('--task ', 'Narrow output to one shared TaskRef') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await teamSyncPull(process.cwd(), options); - }); - -teamSyncProgram - .command('push ') - .description('Publish one task bundle through a fresh ownership fence CAS') - .requiredOption( - '--expected-task-revision ', - 'Current shared task revision', - ) - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (task, options) => { - process.exitCode = await teamSyncPush(process.cwd(), { - ...options, - task, + + program + .command('status') + .description('Show current mancode project status') + .option('--json', 'Output as JSON (for scripts)') + .option('--brief', 'Output compact mancode Continuity runtime status') + .action(async (options) => { + const code = await status(process.cwd(), options); + process.exitCode = code; + }); + + const projectProgram = program + .command('project') + .description('Manage project-level mancode policy and compatibility'); + + projectProgram + .command('upgrade') + .description('Upgrade project governance policy explicitly') + .requiredOption('--policy ', 'Target planning policy version (2)') + .option('--dry-run', 'Preview the upgrade without writing') + .option('--operation-id ', 'Operation ID returned by project dry-run') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await projectUpgrade(process.cwd(), options); + }); + + program + .command('list-platforms') + .description('List available and installed mancode platform adapters') + .action(async () => { + const code = await listPlatforms(process.cwd()); + process.exitCode = code; + }); + + program + .command('uninstall [platform]') + .description('Remove platform adapter or all mancode artifacts') + .option('--force', 'Skip confirmation message') + .option('--all', 'Remove everything including .mancode/ directory') + .action(async (platform, options) => { + const code = await uninstall(process.cwd(), platform, options); + process.exitCode = code; + }); + + program + .command('workflow [args...]') + .description('Manage mancode workflows') + .addHelpText( + 'after', + `\nPublic Continuity subcommands:\n ${WORKFLOW_SUBCOMMANDS.join(', ')}\n`, + ) + .option('--dry-run', 'Preview clean without deleting') + .option('--older-than ', 'Clean workflows older than (e.g. 30d)') + .option('--step ', 'Update workflow current step') + .option('--status ', 'Update workflow status') + .option( + '--parent-task ', + 'Parent /man or /manteam workflow for manba', + ) + .option('--parent ', 'Parent TaskRef for a manba child') + .option( + '--participant ', + 'Invite a joined team participant', + collectOption, + [], + ) + .option('--visibility ', 'Task visibility: local or shared') + .option( + '--coordination ', + 'Task coordination: single or team', + ) + .option( + '--session ', + 'mancode session ID (otherwise MANCODE_SESSION_ID)', + ) + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--expected-revision ', 'Expected task revision for mutations') + .option('--checkpoint-id ', 'Checkpoint ID for a requirements reframe') + .option('--child-revision ', 'Expected child task revision for merge') + .option('--summary ', 'Privacy-screened child result summary') + .option('--next-action ', 'Next parent action after a child merge') + .option('--sync', 'Publish shared mutations through git-ref transport') + .option( + '--confirm-shared', + 'Confirm that task metadata may enter shared mancode authority', + ) + .option('--blocking-reason ', 'Explain why a workflow is blocked') + .option('--outcome ', 'Set manba outcome when completing a task') + .option('--plan-version ', 'Set the next man/manteam plan revision') + .option( + '--requirements-status ', + 'Planning readiness: ready or needs_clarification', + ) + .option( + '--plan-decision ', + 'Plan gate choice: plan_only or governed_execution', + ) + .option('--to ', 'Workflow handoff target (solo)') + .option('--complete', 'Complete an active solo handoff') + .option( + '--skipped ', + 'Policy v2: clarification only; use workflow review skip for review', + ) + .option('--review-depth ', 'Review depth: targeted or full') + .option('--review-domain ', 'Review domain: quality or security') + .option( + '--report ', + 'Relative Markdown report path for a review domain', + ) + .option('--blockers ', 'Comma-separated blocker ids found by a review') + .option( + '--resolved ', + 'Comma-separated blocker ids resolved in remediation', + ) + .option( + '--file ', + 'Semantic requirements JSON, plan Markdown, or ledger input file', + ) + .option( + '--scope-file ', + 'Plan implementation scope JSON {include,exclude,modules}', + ) + .option('--acceptance ', 'Acceptance criterion id (for example AC-1)') + .option('--method ', 'Verification method: automated or manual') + .option('--result ', 'Verification result') + .option('--evidence ', 'Verification evidence or user confirmation') + .option('--command ', 'Command used for automated verification') + .option('--exit-code ', 'Exit code from automated verification') + .option( + '--evidence-file ', + 'Existing verification report or artifact', + ) + .option('--reason ', 'Reason for an explicit review skip') + .option('--json', 'Output as JSON (for scripts)') + .action(async (subcommand, args, options) => { + const code = await workflow(process.cwd(), subcommand, args ?? [], { + ...options, + participants: + options.participant.length === 0 ? undefined : options.participant, + }); + process.exitCode = code; + }); + + const contextProgram = program + .command('context') + .description('Resolve mancode task context and manage explicit sessions'); + + contextProgram + .command('show') + .description('Resolve one mancode Context Pack') + .option('--task ', 'Explicit TaskRef') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--level ', 'bootstrap, task, or full') + .option( + '--purpose ', + 'orient, plan, implement, review, verify, or handoff', + ) + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await contextShow(process.cwd(), options); + }); + + const contextSessionProgram = contextProgram + .command('session') + .description('Manage mancode session identities'); + + contextSessionProgram + .command('new') + .description('Create an explicit bootstrap session') + .requiredOption('--client ', 'Client identity') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await contextSessionNew(process.cwd(), options); + }); + + contextSessionProgram + .command('show') + .description('Show one explicit session without changing it') + .requiredOption('--session ', 'Session ID') + .option('--client ', 'Expected client identity') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await contextSessionShow(process.cwd(), options); + }); + + contextSessionProgram + .command('spike') + .description( + 'Record real-host session evidence without persisting host keys', + ) + .requiredOption( + '--platform ', + 'claude-code, codex, cursor, copilot, zcode, kimi-code, or qoder', + ) + .requiredOption('--session-mode ', 'Evidence path: host or explicit') + .requiredOption( + '--host-session-source ', + 'hook_stdin, environment, api, or none for explicit sessions', + ) + .requiredOption( + '--command-propagation ', + 'Real host child-command result: proven, not_proven, not_tested, or not_applicable', + ) + .requiredOption( + '--subagent-inheritance ', + 'Real host child-agent result: proven, not_proven, not_tested, or not_applicable', + ) + .option( + '--subagent-inheritance-reason ', + 'Required when child-agent inheritance is not applicable', + ) + .option( + '--hook-approval ', + 'approved, unapproved, unknown, or not_applicable', + ) + .requiredOption( + '--host-version ', + 'Installed host version used for the spike', + ) + .requiredOption( + '--release-candidate ', + 'Immutable mancode release candidate or source commit identifier', + ) + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await contextSessionSpike(process.cwd(), options); + }); + + contextProgram + .command('resume ') + .description('Validate and bind the current session to a mancode TaskRef') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--sync', 'Publish shared mutations through git-ref transport') + .option('--json', 'Output as JSON (for scripts)') + .action(async (task, options) => { + process.exitCode = await contextResume(process.cwd(), task, options); + }); + + contextProgram + .command('close') + .description('Close one explicit session without affecting other sessions') + .requiredOption('--session ', 'Session ID') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await contextClose(process.cwd(), options); + }); + + contextProgram + .command('doctor') + .description( + 'Inspect unfinished mancode operations or repair one explicitly', + ) + .option( + '--repair ', + 'Repair this operation with its original session', + ) + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await contextDoctor(process.cwd(), options); + }); + + contextProgram + .command('diagnostics [action]') + .description('Show or configure local aggregate diagnostics') + .option('--json', 'Output as JSON (for scripts)') + .action(async (action, options) => { + process.exitCode = await contextDiagnostics( + process.cwd(), + action, + options, + ); + }); + + contextProgram + .command('compact') + .description( + 'List and remove eligible mancode runtime retention candidates', + ) + .option( + '--task ', + 'Compact checkpoints for one completed task', + ) + .option('--dry-run', 'Show the deletion list without changing files') + .option('--apply-shared', 'Permit deletion for shared completed tasks') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await contextCompact(process.cwd(), options); + }); + + contextProgram + .command('beta', { hidden: true }) + .description('Evaluate internal release-evidence gates') + .requiredOption( + '--release-candidate ', + 'Release candidate that must match every platform evidence record', + ) + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await contextBeta(process.cwd(), options); + }); + + contextProgram + .command('publish ') + .description('Create a privacy-screened shared man successor') + .requiredOption('--expected-revision ', 'Current local task revision') + .requiredOption( + '--confirm-shared', + 'Confirm that the screened task authority may enter shared storage', + ) + .option('--dry-run', 'Validate the publish preflight without writing') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (task, options) => { + process.exitCode = await contextPublish(process.cwd(), task, options); + }); + + contextProgram + .command('reconcile-task-head ') + .description( + 'Adopt a Git-sourced shared aggregate through an explicit fence CAS', + ) + .requiredOption( + '--expected-fence-revision ', + 'Current shared task-head fence revision', + ) + .requiredOption( + '--from-git', + 'Confirm the checked-out aggregate came from Git', + ) + .option( + '--dry-run', + 'Validate adoption without changing the task-head fence', + ) + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (task, options) => { + process.exitCode = await contextReconcileTaskHead( + process.cwd(), + task, + options, + ); + }); + + contextProgram + .command('glossary ') + .description('Manage the user-confirmed shared project glossary') + .option('--term ', 'Glossary term (add, update, remove)') + .option('--definition ', 'Term definition (add, update)') + .option( + '--alias ', + 'Term alias; repeat for multiple aliases (add, update)', + (value: string, previous: string[]) => [...previous, value], + [] as string[], + ) + .option('--task ', 'Source shared TaskRef (add, update)') + .option( + '--expected-revision ', + 'Current glossary revision (0 for an empty glossary)', + ) + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (action, options) => { + process.exitCode = await contextGlossary(process.cwd(), action, options); + }); + + const contextWorktreeProgram = contextProgram + .command('worktree') + .description('Register and inspect the current mancode checkout binding'); + + contextWorktreeProgram + .command('register') + .description( + 'Register this linked worktree before using mancode coordination', + ) + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await contextWorktreeRegister(process.cwd(), options); + }); + + const operationProgram = program + .command('operation') + .description('Inspect and recover durable mancode operations'); + + operationProgram + .command('show ') + .description('Show one operation journal and its recovery disposition') + .option('--json', 'Output as JSON (for scripts)') + .action(async (operationId, options) => { + process.exitCode = await operationShow( + process.cwd(), + operationId, + options, + ); + }); + + operationProgram + .command('repair ') + .description('Repair an operation using its original actor and session') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (operationId, options) => { + process.exitCode = await operationRepair( + process.cwd(), + operationId, + options, + ); + }); + + operationProgram + .command('abort ') + .description( + 'Abort only an operation proven to have no visible business write', + ) + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (operationId, options) => { + process.exitCode = await operationAbort( + process.cwd(), + operationId, + options, + ); + }); + + const teamProgram = program + .command('team') + .description('Manage mancode local identity and local-team membership'); + + teamProgram + .command('status') + .description( + 'Show mancode team policy, transport, and local identity state', + ) + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await teamStatus(process.cwd(), options); + }); + + teamProgram + .command('policy ') + .description( + 'Set the mancode team recommendation policy with a revision CAS', + ) + .requiredOption('--expected-revision ', 'Current team policy revision') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (policy, options) => { + process.exitCode = await teamPolicy(process.cwd(), { + ...options, + policy, + }); + }); + + teamProgram + .command('conflicts') + .description( + 'Inspect local claim conflicts and handoffs without mutating coordination', + ) + .option('--task ', 'Narrow the report to one shared TaskRef') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await teamConflicts(process.cwd(), options); + }); + + const teamTransportProgram = teamProgram + .command('transport') + .description('Inspect and migrate the coordination authority'); + + teamTransportProgram + .command('status') + .description('Show active coordination transport and freshness') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await teamTransportStatus(process.cwd(), options); + }); + + teamTransportProgram + .command('set ') + .description( + 'Switch an empty coordination authority; otherwise use transport migrate', + ) + .requiredOption( + '--expected-config-revision ', + 'Current project config revision', + ) + .option( + '--remote ', + 'Git remote for a git-ref target (default: origin)', + ) + .option('--dry-run', 'Validate the empty-authority switch without writing') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (mode, options) => { + process.exitCode = await teamTransportSet(process.cwd(), { + ...options, + mode, + }); + }); + + teamTransportProgram + .command('migrate') + .description('Journal a single-authority local/git-ref transport switch') + .requiredOption('--to ', 'Target authority: local or git-ref') + .requiredOption( + '--expected-config-revision ', + 'Current project config revision', + ) + .option( + '--remote ', + 'Git remote for a git-ref target (default: origin)', + ) + .option('--dry-run', 'Validate and preview without writing authority state') + .option('--confirm', 'Explicitly confirm the authority migration') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await teamTransportMigrate(process.cwd(), options); + }); + + teamTransportProgram + .command('recover ') + .description('Repair forward or safely abort a transport migration') + .requiredOption( + '--to ', + 'Original target authority: local or git-ref', + ) + .option('--remote ', 'Original Git remote for a git-ref target') + .option('--abort', 'Abort only before the target authority is established') + .option('--session ', 'Original migration session ID') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (operationId, options) => { + process.exitCode = await teamTransportRecover( + process.cwd(), + operationId, + options, + ); + }); + + const teamSyncProgram = teamProgram + .command('sync') + .description('Explicitly synchronize the git-ref coordination authority'); + + teamSyncProgram + .command('pull') + .description('Fetch, validate, and cache refs/mancode/team') + .option('--task ', 'Narrow output to one shared TaskRef') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await teamSyncPull(process.cwd(), options); + }); + + teamSyncProgram + .command('push ') + .description('Publish one task bundle through a fresh ownership fence CAS') + .requiredOption( + '--expected-task-revision ', + 'Current shared task revision', + ) + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (task, options) => { + process.exitCode = await teamSyncPush(process.cwd(), { + ...options, + task, + }); + }); + + const teamIdentityProgram = teamProgram + .command('identity') + .description('Manage the machine-local actor identity'); + + teamIdentityProgram + .command('create') + .description('Create one local actor identity') + .requiredOption('--name ', 'Display name') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await teamIdentityCreate(process.cwd(), options); + }); + + teamIdentityProgram + .command('show') + .description('Show local identity and whether it is joined') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await teamIdentityShow(process.cwd(), options); }); - }); - -const teamIdentityProgram = teamProgram - .command('identity') - .description('Manage the machine-local actor identity'); - -teamIdentityProgram - .command('create') - .description('Create one local actor identity') - .requiredOption('--name ', 'Display name') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await teamIdentityCreate(process.cwd(), options); - }); - -teamIdentityProgram - .command('show') - .description('Show local identity and whether it is joined') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await teamIdentityShow(process.cwd(), options); - }); - -const teamDecisionProgram = teamProgram - .command('decision') - .description('Publish explicitly confirmed, privacy-safe shared decisions'); - -teamDecisionProgram - .command('publish') - .description('Publish one immutable confirmed decision') - .requiredOption('--title ', 'Short decision title') - .requiredOption('--statement ', 'Confirmed decision statement') - .option('--task ', 'Optional shared TaskRef that produced it') - .requiredOption('--confirm', 'Confirm this decision may enter shared memory') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await teamDecisionPublish(process.cwd(), options); - }); - -teamProgram - .command('join') - .description( - 'Publish the approved shared actor profile after explicit confirmation', - ) - .requiredOption('--name ', 'Must match the local actor identity') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--sync', 'Request explicit remote sync when transport supports it') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await teamJoin(process.cwd(), options); - }); - -teamProgram - .command('checkpoint ') - .description('Create a journaled immutable checkpoint for a shared task') - .requiredOption( - '--expected-task-revision ', - 'Current shared task revision', - ) - .requiredOption('--kind ', 'Checkpoint kind') - .requiredOption('--summary ', 'Privacy-safe checkpoint summary') - .option('--next-action ', 'Next action for the receiving workflow') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--json', 'Output as JSON (for scripts)') - .action(async (task, options) => { - process.exitCode = await teamCheckpoint(process.cwd(), { - ...options, - task, + + const teamDecisionProgram = teamProgram + .command('decision') + .description('Publish explicitly confirmed, privacy-safe shared decisions'); + + teamDecisionProgram + .command('publish') + .description('Publish one immutable confirmed decision') + .requiredOption('--title ', 'Short decision title') + .requiredOption('--statement ', 'Confirmed decision statement') + .option('--task ', 'Optional shared TaskRef that produced it') + .requiredOption( + '--confirm', + 'Confirm this decision may enter shared memory', + ) + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await teamDecisionPublish(process.cwd(), options); }); - }); - -teamProgram - .command('claim ') - .description('Acquire a scoped claim for a shared task') - .requiredOption( - '--expected-task-revision ', - 'Current shared task revision', - ) - .option('--path ', 'Repository-relative path glob', collectOption, []) - .option('--module ', 'Implementation module', collectOption, []) - .option('--api ', 'Public API boundary', collectOption, []) - .option('--schema ', 'Shared schema boundary', collectOption, []) - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--sync', 'Publish through the active git-ref authority') - .option('--json', 'Output as JSON (for scripts)') - .action(async (task, options) => { - process.exitCode = await teamClaim(process.cwd(), { - ...options, - task, - paths: options.path, - modules: options.module, - apis: options.api, - schemas: options.schema, + + teamProgram + .command('join') + .description( + 'Publish the approved shared actor profile after explicit confirmation', + ) + .requiredOption( + '--name ', + 'Must match the local actor identity', + ) + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--sync', 'Request explicit remote sync when transport supports it') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await teamJoin(process.cwd(), options); }); - }); - -teamProgram - .command('renew ') - .description('Renew one fresh claim lease') - .requiredOption('--expected-revision ', 'Current claim revision') - .option('--ttl ', 'Lease duration: ms, s, m, h, or d') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--sync', 'Publish through the active git-ref authority') - .option('--json', 'Output as JSON (for scripts)') - .action(async (claimId, options) => { - process.exitCode = await teamClaimRenew(process.cwd(), { - ...options, - claimId, + + teamProgram + .command('checkpoint ') + .description('Create a journaled immutable checkpoint for a shared task') + .requiredOption( + '--expected-task-revision ', + 'Current shared task revision', + ) + .requiredOption('--kind ', 'Checkpoint kind') + .requiredOption('--summary ', 'Privacy-safe checkpoint summary') + .option('--next-action ', 'Next action for the receiving workflow') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--json', 'Output as JSON (for scripts)') + .action(async (task, options) => { + process.exitCode = await teamCheckpoint(process.cwd(), { + ...options, + task, + }); }); - }); - -teamProgram - .command('release ') - .description('Release one claim') - .requiredOption('--expected-revision ', 'Current claim revision') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--sync', 'Publish through the active git-ref authority') - .option('--json', 'Output as JSON (for scripts)') - .action(async (claimId, options) => { - process.exitCode = await teamClaimRelease(process.cwd(), { - ...options, - claimId, + + teamProgram + .command('claim ') + .description('Acquire a scoped claim for a shared task') + .requiredOption( + '--expected-task-revision ', + 'Current shared task revision', + ) + .option('--path ', 'Repository-relative path glob', collectOption, []) + .option('--module ', 'Implementation module', collectOption, []) + .option('--api ', 'Public API boundary', collectOption, []) + .option('--schema ', 'Shared schema boundary', collectOption, []) + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--sync', 'Publish through the active git-ref authority') + .option('--json', 'Output as JSON (for scripts)') + .action(async (task, options) => { + process.exitCode = await teamClaim(process.cwd(), { + ...options, + task, + paths: options.path, + modules: options.module, + apis: options.api, + schemas: options.schema, + }); }); - }); - -teamProgram - .command('transfer ') - .description('Transfer a claim through a new successor identity') - .requiredOption('--to ', 'Receiving joined participant actor ID') - .requiredOption('--expected-revision ', 'Current claim revision') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--sync', 'Publish through the active git-ref authority') - .option('--json', 'Output as JSON (for scripts)') - .action(async (claimId, options) => { - process.exitCode = await teamClaimTransfer(process.cwd(), { - ...options, - claimId, + + teamProgram + .command('renew ') + .description('Renew one fresh claim lease') + .requiredOption('--expected-revision ', 'Current claim revision') + .option('--ttl ', 'Lease duration: ms, s, m, h, or d') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--sync', 'Publish through the active git-ref authority') + .option('--json', 'Output as JSON (for scripts)') + .action(async (claimId, options) => { + process.exitCode = await teamClaimRenew(process.cwd(), { + ...options, + claimId, + }); }); - }); - -teamProgram - .command('reclaim ') - .description('Explicitly mark an expired claim terminal') - .requiredOption('--expected-revision ', 'Current claim revision') - .requiredOption('--reason ', 'Privacy-safe expiry reclaim reason') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--sync', 'Publish through the active git-ref authority') - .option('--json', 'Output as JSON (for scripts)') - .action(async (claimId, options) => { - process.exitCode = await teamClaimReclaim(process.cwd(), { - ...options, - claimId, + + teamProgram + .command('release ') + .description('Release one claim') + .requiredOption('--expected-revision ', 'Current claim revision') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--sync', 'Publish through the active git-ref authority') + .option('--json', 'Output as JSON (for scripts)') + .action(async (claimId, options) => { + process.exitCode = await teamClaimRelease(process.cwd(), { + ...options, + claimId, + }); }); - }); - -teamProgram - .command('revalidate ') - .description('Refresh one claim after task or code snapshot drift') - .requiredOption('--expected-revision ', 'Current claim revision') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--sync', 'Publish through the active git-ref authority') - .option('--json', 'Output as JSON (for scripts)') - .action(async (claimId, options) => { - process.exitCode = await teamClaimRevalidate(process.cwd(), { - ...options, - claimId, + + teamProgram + .command('transfer ') + .description('Transfer a claim through a new successor identity') + .requiredOption('--to ', 'Receiving joined participant actor ID') + .requiredOption('--expected-revision ', 'Current claim revision') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--sync', 'Publish through the active git-ref authority') + .option('--json', 'Output as JSON (for scripts)') + .action(async (claimId, options) => { + process.exitCode = await teamClaimTransfer(process.cwd(), { + ...options, + claimId, + }); }); - }); - -const teamHandoffProgram = teamProgram - .command('handoff') - .description('Create and transition journaled ownership handoffs'); - -teamHandoffProgram - .command('draft ') - .description('Create a checkpoint-backed named handoff draft') - .requiredOption( - '--expected-task-revision ', - 'Current shared task revision', - ) - .requiredOption('--to ', 'Receiving joined participant actor ID') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--sync', 'Publish through the active git-ref authority') - .option('--json', 'Output as JSON (for scripts)') - .action(async (task, options) => { - process.exitCode = await teamHandoffDraft(process.cwd(), { - ...options, - task, + + teamProgram + .command('reclaim ') + .description('Explicitly mark an expired claim terminal') + .requiredOption('--expected-revision ', 'Current claim revision') + .requiredOption('--reason ', 'Privacy-safe expiry reclaim reason') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--sync', 'Publish through the active git-ref authority') + .option('--json', 'Output as JSON (for scripts)') + .action(async (claimId, options) => { + process.exitCode = await teamClaimReclaim(process.cwd(), { + ...options, + claimId, + }); }); - }); - -teamHandoffProgram - .command('offer ') - .description('Offer a handoff draft to its receiving actor') - .requiredOption('--expected-revision ', 'Current handoff revision') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--sync', 'Publish through the active git-ref authority') - .option('--json', 'Output as JSON (for scripts)') - .action(async (handoffId, options) => { - process.exitCode = await teamHandoffOffer(process.cwd(), { - ...options, - handoffId, + + teamProgram + .command('revalidate ') + .description('Refresh one claim after task or code snapshot drift') + .requiredOption('--expected-revision ', 'Current claim revision') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--sync', 'Publish through the active git-ref authority') + .option('--json', 'Output as JSON (for scripts)') + .action(async (claimId, options) => { + process.exitCode = await teamClaimRevalidate(process.cwd(), { + ...options, + claimId, + }); }); - }); - -teamHandoffProgram - .command('accept ') - .description('Accept an offered handoff and transfer ownership atomically') - .requiredOption('--expected-revision ', 'Current handoff revision') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--sync', 'Publish through the active git-ref authority') - .option('--json', 'Output as JSON (for scripts)') - .action(async (handoffId, options) => { - process.exitCode = await teamHandoffAccept(process.cwd(), { - ...options, - handoffId, + + const teamHandoffProgram = teamProgram + .command('handoff') + .description('Create and transition journaled ownership handoffs'); + + teamHandoffProgram + .command('draft ') + .description('Create a checkpoint-backed named handoff draft') + .requiredOption( + '--expected-task-revision ', + 'Current shared task revision', + ) + .requiredOption('--to ', 'Receiving joined participant actor ID') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--sync', 'Publish through the active git-ref authority') + .option('--json', 'Output as JSON (for scripts)') + .action(async (task, options) => { + process.exitCode = await teamHandoffDraft(process.cwd(), { + ...options, + task, + }); }); - }); - -teamHandoffProgram - .command('reject ') - .description('Reject an offered handoff with a durable reason') - .requiredOption('--expected-revision ', 'Current handoff revision') - .requiredOption('--reason ', 'Reason for rejecting the handoff') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--sync', 'Publish through the active git-ref authority') - .option('--json', 'Output as JSON (for scripts)') - .action(async (handoffId, options) => { - process.exitCode = await teamHandoffReject(process.cwd(), { - ...options, - handoffId, + + teamHandoffProgram + .command('offer ') + .description('Offer a handoff draft to its receiving actor') + .requiredOption('--expected-revision ', 'Current handoff revision') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--sync', 'Publish through the active git-ref authority') + .option('--json', 'Output as JSON (for scripts)') + .action(async (handoffId, options) => { + process.exitCode = await teamHandoffOffer(process.cwd(), { + ...options, + handoffId, + }); }); - }); - -teamHandoffProgram - .command('cancel ') - .description('Cancel a draft or offered handoff') - .requiredOption('--expected-revision ', 'Current handoff revision') - .option('--reason ', 'Optional cancellation reason') - .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') - .option('--client ', 'Client identity (default: mancode-cli)') - .option('--sync', 'Publish through the active git-ref authority') - .option('--json', 'Output as JSON (for scripts)') - .action(async (handoffId, options) => { - process.exitCode = await teamHandoffCancel(process.cwd(), { - ...options, - handoffId, + + teamHandoffProgram + .command('accept ') + .description('Accept an offered handoff and transfer ownership atomically') + .requiredOption('--expected-revision ', 'Current handoff revision') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--sync', 'Publish through the active git-ref authority') + .option('--json', 'Output as JSON (for scripts)') + .action(async (handoffId, options) => { + process.exitCode = await teamHandoffAccept(process.cwd(), { + ...options, + handoffId, + }); }); - }); - -const migrateProgram = program - .command('migrate') - .description('Inspect and migrate legacy context into mancode staging'); - -const migrateContextProgram = migrateProgram - .command('context') - .description('Manage the isolated legacy-to-mancode context migration stage') - .option('--dry-run', 'Inspect legacy authority without writing files') - .option('--stage', 'Create or refresh an isolated local migration stage') - .option('--status', 'Show local migration stages') - .option('--activate', 'Attempt the journaled mancode activation') - .option( - '--rollback ', - 'Roll back an untouched mancode activation', - ) - .option('--stage-id ', 'Migration stage ID (required if more than one)') - .option( - '--expected-stage-revision ', - 'Expected stage revision for activation', - ) - .option('--session ', 'Active session required for activation') - .option('--confirm', 'Explicitly confirm the mancode cutover') - .option('--confirm-shared', 'Confirm promotion of staged shared authority') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - const code = await migrateContext(process.cwd(), options); - process.exitCode = code; - }); - -migrateContextProgram - .command('resolve ') - .description('Resolve missing owner or implementation scope in one stage') - .requiredOption( - '--expected-stage-revision ', - 'Expected local migration stage revision', - ) - .option('--stage-id ', 'Migration stage ID (required if more than one)') - .option('--owner ', 'Explicit owner actor ID') - .option( - '--scope-file ', - 'JSON implementation scope {include,exclude,modules}', - ) - .option('--json', 'Output as JSON (for scripts)') - .action(async (legacyTaskId, options) => { - const code = await migrateContextResolve( - process.cwd(), - legacyTaskId, - options, + + teamHandoffProgram + .command('reject ') + .description('Reject an offered handoff with a durable reason') + .requiredOption('--expected-revision ', 'Current handoff revision') + .requiredOption('--reason ', 'Reason for rejecting the handoff') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--sync', 'Publish through the active git-ref authority') + .option('--json', 'Output as JSON (for scripts)') + .action(async (handoffId, options) => { + process.exitCode = await teamHandoffReject(process.cwd(), { + ...options, + handoffId, + }); + }); + + teamHandoffProgram + .command('cancel ') + .description('Cancel a draft or offered handoff') + .requiredOption('--expected-revision ', 'Current handoff revision') + .option('--reason ', 'Optional cancellation reason') + .option('--session ', 'Session ID (otherwise MANCODE_SESSION_ID)') + .option('--client ', 'Client identity (default: mancode-cli)') + .option('--sync', 'Publish through the active git-ref authority') + .option('--json', 'Output as JSON (for scripts)') + .action(async (handoffId, options) => { + process.exitCode = await teamHandoffCancel(process.cwd(), { + ...options, + handoffId, + }); + }); + + const migrateProgram = program + .command('migrate') + .description('Inspect and migrate legacy context into mancode staging'); + + const migrateContextProgram = migrateProgram + .command('context') + .description( + 'Manage the isolated legacy-to-mancode context migration stage', + ) + .option('--dry-run', 'Inspect legacy authority without writing files') + .option('--stage', 'Create or refresh an isolated local migration stage') + .option('--status', 'Show local migration stages') + .option('--activate', 'Attempt the journaled mancode activation') + .option( + '--rollback ', + 'Roll back an untouched mancode activation', + ) + .option('--stage-id ', 'Migration stage ID (required if more than one)') + .option( + '--expected-stage-revision ', + 'Expected stage revision for activation', + ) + .option('--session ', 'Active session required for activation') + .option('--confirm', 'Explicitly confirm the mancode cutover') + .option('--confirm-shared', 'Confirm promotion of staged shared authority') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + const code = await migrateContext(process.cwd(), options); + process.exitCode = code; + }); + + migrateContextProgram + .command('resolve ') + .description('Resolve missing owner or implementation scope in one stage') + .requiredOption( + '--expected-stage-revision ', + 'Expected local migration stage revision', + ) + .option('--stage-id ', 'Migration stage ID (required if more than one)') + .option('--owner ', 'Explicit owner actor ID') + .option( + '--scope-file ', + 'JSON implementation scope {include,exclude,modules}', + ) + .option('--json', 'Output as JSON (for scripts)') + .action(async (legacyTaskId, options) => { + const code = await migrateContextResolve( + process.cwd(), + legacyTaskId, + options, + ); + process.exitCode = code; + }); + + program + .command('manps [area]') + .description('Run deterministic preseason health scan') + .option('--json', 'Output as JSON (for scripts)') + .option('--remediate', 'Review scan issues with y/n/skip prompts') + .action(async (area, options) => { + const code = await manps(process.cwd(), area ?? 'all', options); + process.exitCode = code; + }); + + const designProgram = program + .command('design') + .description('Inspect and configure project UI design policy'); + + designProgram + .command('status') + .description('Show the configured and effective project design policy') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await designStatus(process.cwd(), options); + }); + + designProgram + .command('context') + .description('Emit bounded UI design context for coding agents') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await designContext(process.cwd(), options); + }); + + designProgram + .command('configure') + .description('CAS-update the optional project design policy') + .requiredOption( + '--expected-revision ', + 'Current policy revision; use 0 when absent', + ) + .option('--preset ', 'preserve, refine, or experimental') + .option('--icons ', 'existing-first or lucide') + .option( + '--emoji ', + 'forbid-as-interface-icon; legacy allow is normalized', + ) + .option('--motion ', 'minimal or purposeful') + .option('--browser-validation ', 'off, when-available, or required') + .option( + '--confirm-experimental', + 'Explicitly allow the experimental preset', + ) + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await designConfigure(process.cwd(), options); + }); + + designProgram + .command('disable') + .description('Disable the project design policy without deleting it') + .requiredOption('--expected-revision ', 'Current policy revision') + .option('--json', 'Output as JSON (for scripts)') + .action(async (options) => { + process.exitCode = await designDisable(process.cwd(), options); + }); + + program + .command('refresh-style') + .description('Refresh project profile and rescan applicable design tokens') + .option('--root ', 'Repository-relative UI project root') + .action(async (options) => { + const code = await refreshStyle(process.cwd(), options); + process.exitCode = code; + }); + + program + .command('refresh-project') + .description( + 'Refresh detected project facts after adding Git or project files', + ) + .action(async () => { + const code = await refreshProject(process.cwd()); + process.exitCode = code; + }); + + program + .command('version') + .description('Show version, node version, and platform') + .action(() => { + version(); + }); + + return program; +} + +if (isDirectExecution()) { + createCliProgram().parse(); +} + +function isDirectExecution(): boolean { + const entrypoint = process.argv[1]; + if (entrypoint === undefined) return false; + + try { + return ( + realpathSync(entrypoint) === realpathSync(fileURLToPath(import.meta.url)) ); - process.exitCode = code; - }); - -program - .command('manps [area]') - .description('Run deterministic preseason health scan') - .option('--json', 'Output as JSON (for scripts)') - .option('--remediate', 'Review scan issues with y/n/skip prompts') - .action(async (area, options) => { - const code = await manps(process.cwd(), area ?? 'all', options); - process.exitCode = code; - }); - -const designProgram = program - .command('design') - .description('Inspect and configure project UI design policy'); - -designProgram - .command('status') - .description('Show the configured and effective project design policy') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await designStatus(process.cwd(), options); - }); - -designProgram - .command('context') - .description('Emit bounded UI design context for coding agents') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await designContext(process.cwd(), options); - }); - -designProgram - .command('configure') - .description('CAS-update the optional project design policy') - .requiredOption( - '--expected-revision ', - 'Current policy revision; use 0 when absent', - ) - .option('--preset ', 'preserve, refine, or experimental') - .option('--icons ', 'existing-first or lucide') - .option( - '--emoji ', - 'forbid-as-interface-icon; legacy allow is normalized', - ) - .option('--motion ', 'minimal or purposeful') - .option('--browser-validation ', 'off, when-available, or required') - .option('--confirm-experimental', 'Explicitly allow the experimental preset') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await designConfigure(process.cwd(), options); - }); - -designProgram - .command('disable') - .description('Disable the project design policy without deleting it') - .requiredOption('--expected-revision ', 'Current policy revision') - .option('--json', 'Output as JSON (for scripts)') - .action(async (options) => { - process.exitCode = await designDisable(process.cwd(), options); - }); - -program - .command('refresh-style') - .description('Refresh project profile and rescan applicable design tokens') - .option('--root ', 'Repository-relative UI project root') - .action(async (options) => { - const code = await refreshStyle(process.cwd(), options); - process.exitCode = code; - }); - -program - .command('refresh-project') - .description( - 'Refresh detected project facts after adding Git or project files', - ) - .action(async () => { - const code = await refreshProject(process.cwd()); - process.exitCode = code; - }); - -program - .command('version') - .description('Show version, node version, and platform') - .action(() => { - version(); - }); - -export { program as cliProgram }; - -program.parse(); + } catch { + return false; + } +} function collectOption(value: string, previous: string[]): string[] { return [...previous, value]; diff --git a/src/commands/.gitkeep b/src/commands/.gitkeep deleted file mode 100644 index c1b4c03..0000000 --- a/src/commands/.gitkeep +++ /dev/null @@ -1,2 +0,0 @@ -# Placeholder for future subcommands (init, status, install, ...). -# Remove this file once the first command lands here. diff --git a/src/commands/workflow-subcommands.ts b/src/commands/workflow-subcommands.ts new file mode 100644 index 0000000..13b2ff1 --- /dev/null +++ b/src/commands/workflow-subcommands.ts @@ -0,0 +1,29 @@ +export const WORKFLOW_SUBCOMMANDS = [ + 'create', + 'list', + 'show', + 'update', + 'requirements', + 'plan', + 'review', + 'verify', + 'complete', + 'scope', + 'reframe', + 'archive', + 'checkpoint', + 'child', + 'promote', + 'handoff', +] as const; + +export type WorkflowSubcommand = (typeof WORKFLOW_SUBCOMMANDS)[number]; + +export const WORKFLOW_SUBCOMMAND_SET: ReadonlySet = new Set( + WORKFLOW_SUBCOMMANDS, +); + +export type ContinuityCompatibilitySubcommand = 'clean'; + +export const CONTINUITY_COMPATIBILITY_SUBCOMMANDS: ReadonlySet = + new Set(['clean']); diff --git a/src/commands/workflow.ts b/src/commands/workflow.ts index 818aba2..9b1cf3e 100644 --- a/src/commands/workflow.ts +++ b/src/commands/workflow.ts @@ -103,6 +103,13 @@ import { resolveV3CommandSession, v3ErrorCode, } from './v3-support.js'; +import { + CONTINUITY_COMPATIBILITY_SUBCOMMANDS, + type ContinuityCompatibilitySubcommand, + WORKFLOW_SUBCOMMANDS, + WORKFLOW_SUBCOMMAND_SET, + type WorkflowSubcommand, +} from './workflow-subcommands.js'; export const EXIT_OK = 0; export const EXIT_NOT_INITIALIZED = 1; @@ -233,65 +240,63 @@ export async function workflow( } } +type WorkflowV3Handler = ( + rootDir: string, + args: string[], + options: WorkflowOptions, +) => number | Promise; + +const WORKFLOW_V3_HANDLERS = { + create: workflowCreateV3, + list: workflowListV3, + show: workflowShowV3, + update: workflowUpdateV3, + requirements: workflowRequirementsV3, + plan: workflowPlanV3, + review: workflowReviewV3, + verify: workflowVerifyV3, + complete: workflowCompleteV3, + scope: workflowScopeChangeV3, + reframe: workflowReframeV3, + archive: (rootDir, args, options) => + workflowArtifactShowV3(rootDir, 'archive', args, options), + checkpoint: (rootDir, args, options) => + workflowArtifactShowV3(rootDir, 'checkpoint', args, options), + child: workflowChildResultMergeV3, + promote: workflowPromoteV3, + handoff: workflowSoloHandoffV3, +} satisfies Record; + +const CONTINUITY_COMPATIBILITY_HANDLERS = { + clean: (_rootDir, _args, options) => workflowCleanV3(options), +} satisfies Record; + async function workflowV3( rootDir: string, subcommand: string, args: string[], options: WorkflowOptions, ): Promise { - if (subcommand === 'list') { - return workflowListV3(rootDir, args, options); - } - if (subcommand === 'show') { - return workflowShowV3(rootDir, args, options); - } - if (subcommand === 'clean') { - return workflowCleanV3(options); - } - if (subcommand === 'create') { - return workflowCreateV3(rootDir, args, options); - } - if (subcommand === 'update') { - return workflowUpdateV3(rootDir, args, options); - } - if (subcommand === 'requirements') { - return workflowRequirementsV3(rootDir, args, options); - } - if (subcommand === 'plan') { - return workflowPlanV3(rootDir, args, options); - } - if (subcommand === 'review') { - return workflowReviewV3(rootDir, args, options); - } - if (subcommand === 'verify') { - return workflowVerifyV3(rootDir, args, options); - } - if (subcommand === 'complete') { - return workflowCompleteV3(rootDir, args, options); - } - if (subcommand === 'scope') { - return workflowScopeChangeV3(rootDir, args, options); - } - if (subcommand === 'reframe') { - return workflowReframeV3(rootDir, args, options); - } - if (subcommand === 'archive' || subcommand === 'checkpoint') { - return workflowArtifactShowV3(rootDir, subcommand, args, options); - } - if (subcommand === 'child') { - return workflowChildResultMergeV3(rootDir, args, options); - } - if (subcommand === 'promote') { - return workflowPromoteV3(rootDir, args, options); + if ( + !WORKFLOW_SUBCOMMAND_SET.has(subcommand) && + !CONTINUITY_COMPATIBILITY_SUBCOMMANDS.has(subcommand) + ) { + return printV3Error( + options.json, + 'MANCODE_V3_OPERATION_NOT_IMPLEMENTED', + `Unknown workflow subcommand: ${subcommand}. Use one of: ${WORKFLOW_SUBCOMMANDS.join(', ')}.`, + ); } - if (subcommand === 'handoff') { - return workflowSoloHandoffV3(rootDir, args, options); + if (WORKFLOW_SUBCOMMAND_SET.has(subcommand)) { + return WORKFLOW_V3_HANDLERS[subcommand as WorkflowSubcommand]( + rootDir, + args, + options, + ); } - return printV3Error( - options.json, - 'MANCODE_V3_OPERATION_NOT_IMPLEMENTED', - `workflow ${subcommand} is not yet implemented for mancode authority.`, - ); + return CONTINUITY_COMPATIBILITY_HANDLERS[ + subcommand as ContinuityCompatibilitySubcommand + ](rootDir, args, options); } async function workflowListV3( diff --git a/src/context/greenfield-init.ts b/src/context/greenfield-init.ts index d4ddd44..6be4356 100644 --- a/src/context/greenfield-init.ts +++ b/src/context/greenfield-init.ts @@ -522,7 +522,6 @@ async function writeGreenfieldLayout( recursive: true, }), mkdir(path.join(stagingRoot, 'local', 'workflows'), { recursive: true }), - mkdir(path.join(stagingRoot, 'local', 'overlays'), { recursive: true }), mkdir(path.join(stagingRoot, 'local', 'quarantine'), { recursive: true }), mkdir(path.join(stagingRoot, 'local', 'publish'), { recursive: true }), mkdir(path.join(stagingRoot, 'local', 'cache'), { recursive: true }), diff --git a/src/context/local-overlay.ts b/src/context/local-overlay.ts deleted file mode 100644 index 0f68784..0000000 --- a/src/context/local-overlay.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { createHash } from 'node:crypto'; -import { lstat, mkdir, readFile, writeFile } from 'node:fs/promises'; -import path from 'node:path'; -import { type Ulid, assertUlid, createUlid } from './ids.js'; -import { locateTask } from './task-locator.js'; -import { type TaskRef, parseTaskRefValue } from './task-ref.js'; - -export interface LocalOverlayArtifactV1 { - schemaVersion: 1; - taskRef: TaskRef; - artifactId: Ulid; - byteLength: number; - contentDigest: string; - path: string; -} - -export interface WriteLocalOverlayArtifactInput { - projectRoot: string; - taskRef: TaskRef; - artifactId?: Ulid; - content: string | Uint8Array; -} - -/** - * Stores private evidence for an existing shared task under local authority. - * The artifact ID is immutable and never becomes a shared ArtifactRef. - */ -export async function writeLocalOverlayArtifact( - input: WriteLocalOverlayArtifactInput, -): Promise { - const taskRef = sharedTaskRef(input.taskRef); - await locateTask(input.projectRoot, taskRef); - const artifactId = input.artifactId ?? createUlid(); - assertUlid(artifactId, 'local overlay artifactId'); - const content = overlayContent(input.content); - const directory = await ensureSafeOverlayDirectory( - input.projectRoot, - taskRef, - ); - const target = path.join(directory, artifactId); - try { - await writeFile(target, content, { flag: 'wx', mode: 0o600 }); - await assertSafeFile(target); - } catch (error) { - if (!isAlreadyExists(error)) throw error; - const existing = await readSafeFile(target); - if (!existing.equals(content)) { - throw new Error('MANCODE_OVERLAY_ARTIFACT_CONFLICT'); - } - } - return overlayArtifact(taskRef, artifactId, content, target); -} - -export async function readLocalOverlayArtifact( - projectRoot: string, - taskRef: TaskRef, - artifactId: Ulid, -): Promise { - const parsedTaskRef = sharedTaskRef(taskRef); - assertUlid(artifactId, 'local overlay artifactId'); - await assertSafeOverlayDirectory(projectRoot, parsedTaskRef); - try { - return await readSafeFile( - localOverlayArtifactPath(projectRoot, parsedTaskRef, artifactId), - ); - } catch (error) { - if (isNotFound(error)) { - throw new Error('MANCODE_OVERLAY_ARTIFACT_NOT_FOUND'); - } - throw error; - } -} - -export function localOverlayArtifactsDirectory( - projectRoot: string, - taskRef: TaskRef, -): string { - const parsed = sharedTaskRef(taskRef); - return path.join( - path.resolve(projectRoot), - '.mancode', - 'local', - 'overlays', - parsed.taskId, - 'artifacts', - ); -} - -export function localOverlayArtifactPath( - projectRoot: string, - taskRef: TaskRef, - artifactId: Ulid, -): string { - assertUlid(artifactId, 'local overlay artifactId'); - return path.join( - localOverlayArtifactsDirectory(projectRoot, taskRef), - artifactId, - ); -} - -function sharedTaskRef(value: TaskRef): TaskRef { - const taskRef = parseTaskRefValue(value); - if (taskRef.namespace !== 'shared') { - throw new Error('MANCODE_OVERLAY_REQUIRES_SHARED_TASK'); - } - return taskRef; -} - -function overlayContent(value: string | Uint8Array): Buffer { - if (typeof value === 'string') return Buffer.from(value, 'utf8'); - if (value instanceof Uint8Array) return Buffer.from(value); - throw new Error('MANCODE_OVERLAY_CONTENT_INVALID'); -} - -function overlayArtifact( - taskRef: TaskRef, - artifactId: Ulid, - content: Buffer, - target: string, -): LocalOverlayArtifactV1 { - return { - schemaVersion: 1, - taskRef, - artifactId, - byteLength: content.byteLength, - contentDigest: `sha256:${createHash('sha256').update(content).digest('hex')}`, - path: target, - }; -} - -async function ensureSafeOverlayDirectory( - projectRoot: string, - taskRef: TaskRef, -): Promise { - let current = path.resolve(projectRoot); - for (const segment of [ - '.mancode', - 'local', - 'overlays', - taskRef.taskId, - 'artifacts', - ]) { - current = path.join(current, segment); - try { - await mkdir(current, { mode: 0o700 }); - } catch (error) { - if (!isAlreadyExists(error)) throw error; - } - await assertSafeDirectory(current); - } - return current; -} - -async function assertSafeOverlayDirectory( - projectRoot: string, - taskRef: TaskRef, -): Promise { - let current = path.resolve(projectRoot); - for (const segment of [ - '.mancode', - 'local', - 'overlays', - taskRef.taskId, - 'artifacts', - ]) { - current = path.join(current, segment); - try { - await assertSafeDirectory(current); - } catch (error) { - if (isNotFound(error)) { - throw new Error('MANCODE_OVERLAY_ARTIFACT_NOT_FOUND'); - } - throw error; - } - } -} - -async function assertSafeDirectory(target: string): Promise { - const entry = await lstat(target); - if (!entry.isDirectory() || entry.isSymbolicLink()) { - throw new Error('MANCODE_OVERLAY_PATH_UNSAFE'); - } -} - -async function assertSafeFile(target: string): Promise { - const entry = await lstat(target); - if (!entry.isFile() || entry.isSymbolicLink()) { - throw new Error('MANCODE_OVERLAY_PATH_UNSAFE'); - } -} - -async function readSafeFile(target: string): Promise { - const before = await lstat(target); - if (!before.isFile() || before.isSymbolicLink()) { - throw new Error('MANCODE_OVERLAY_PATH_UNSAFE'); - } - const content = await readFile(target); - const after = await lstat(target); - if ( - !after.isFile() || - after.isSymbolicLink() || - before.dev !== after.dev || - before.ino !== after.ino - ) { - throw new Error('MANCODE_OVERLAY_PATH_UNSAFE'); - } - return content; -} - -function isAlreadyExists(error: unknown): error is NodeJS.ErrnoException { - return ( - typeof error === 'object' && - error !== null && - 'code' in error && - (error as NodeJS.ErrnoException).code === 'EEXIST' - ); -} - -function isNotFound(error: unknown): error is NodeJS.ErrnoException { - return ( - typeof error === 'object' && - error !== null && - 'code' in error && - (error as NodeJS.ErrnoException).code === 'ENOENT' - ); -} diff --git a/src/runtime/retention.ts b/src/runtime/retention.ts index ccf08cf..e4b64ab 100644 --- a/src/runtime/retention.ts +++ b/src/runtime/retention.ts @@ -30,7 +30,6 @@ export type RetentionCandidateKind = | 'completed_session' | 'terminal_operation' | 'checkpoint' - | 'local_overlay_artifact' | 'local_cache'; export interface RetentionCandidate { @@ -105,7 +104,7 @@ export async function planContextCompaction( input.taskRef === undefined ? await listTaskRefs(root) : [parseTaskRefValue(input.taskRef)]; - const [checkpointPlan, sessions, overlays, cache] = await Promise.all([ + const [checkpointPlan, sessions, cache] = await Promise.all([ planCheckpointCompaction( store, coordinationStore, @@ -118,13 +117,6 @@ export async function planContextCompaction( now, protectedSessionIds, ), - planLocalOverlayRetention( - root, - store, - taskRefs, - project.policy.retention.localRawArtifactDays, - now, - ), planLocalCacheRetention(root, project.policy.retention.localCacheDays, now), ]); return { @@ -135,7 +127,6 @@ export async function planContextCompaction( ...operationPlan.candidates, ...gitRefWorkflowRepairPlan.candidates, ...checkpointPlan.candidates, - ...overlays, ...cache, ].sort((left, right) => Buffer.from(left.target, 'utf8').compare( @@ -146,49 +137,6 @@ export async function planContextCompaction( }; } -/** - * Local overlays are never shared ArtifactRefs. They can therefore be - * compacted only after the corresponding shared task is terminal; an active, - * planned, or blocked task retains every private trace regardless of age. - */ -async function planLocalOverlayRetention( - root: string, - store: V3ContextStore, - taskRefs: readonly TaskRef[], - localRawArtifactDays: number, - now: Date, -): Promise { - const threshold = now.getTime() - localRawArtifactDays * 86_400_000; - const candidates: RetentionCandidate[] = []; - for (const taskRef of taskRefs) { - if (taskRef.namespace !== 'shared') continue; - const task = await store.readTaskSnapshot(taskRef); - if (!isTerminalTaskStatus(task.metadata.status)) continue; - const directory = path.join( - root, - '.mancode', - 'local', - 'overlays', - taskRef.taskId, - 'artifacts', - ); - for (const entry of await readDirectoryOrEmpty(directory)) { - const target = path.join(directory, entry); - const metadata = await regularFileMetadataOrNull(target); - if (metadata === null || metadata.mtimeMs >= threshold) continue; - candidates.push({ - kind: 'local_overlay_artifact', - target, - reason: `terminal task raw artifact exceeds ${localRawArtifactDays} day retention`, - // The artifact is local-only even though it is grouped by shared task. - taskRef: null, - relatedTargets: [], - }); - } - } - return candidates; -} - /** Cache records are disposable projections; retain no nested path authority. */ async function planLocalCacheRetention( root: string, @@ -433,12 +381,6 @@ function taskRefKeyFromEntityKey(entityKey: string): string | null { return match === null ? null : `${match[1]}:${match[2]}`; } -function isTerminalTaskStatus(status: string): boolean { - return ( - status === 'completed' || status === 'abandoned' || status === 'superseded' - ); -} - async function listTaskRefs(root: string): Promise { const refs: TaskRef[] = []; for (const namespace of ['local', 'shared'] as const) { diff --git a/src/system/workflow.ts b/src/system/workflow.ts index 6ceb9be..ae8cb59 100644 --- a/src/system/workflow.ts +++ b/src/system/workflow.ts @@ -409,6 +409,24 @@ async function validateWorkflowMeta( existing: WorkflowMeta, options: UpdateWorkflowOptions, ): Promise { + validateImmutableWorkflowFields(updated, existing); + validateWorkflowShapeAndSkipPolicy(updated); + validateWorkflowLifecycle(updated, existing); + validateWorkflowOutcome(updated); + validateWorkflowPolicyVersions(updated); + validateWorkflowPolicyState(updated); + validatePlanDecisionTransition(updated, existing); + await validatePlanningArtifactGate(projectRoot, updated); + await validateVerificationGate(projectRoot, updated, existing, options); + validatePlanVersionTransition(updated, existing); + await validateWorkflowFamilyState(projectRoot, updated); + await validateWorkflowCompletionGates(projectRoot, updated); +} + +function validateImmutableWorkflowFields( + updated: WorkflowMeta, + existing: WorkflowMeta, +): void { if (updated.mode !== existing.mode) { throw new Error('workflow mode cannot be changed'); } @@ -437,6 +455,9 @@ async function validateWorkflowMeta( ) { throw new Error('workflow verification policy version cannot be changed'); } +} + +function validateWorkflowShapeAndSkipPolicy(updated: WorkflowMeta): void { if ( !Number.isInteger(updated.currentStep) || updated.currentStep < 1 || @@ -463,6 +484,12 @@ async function validateWorkflowMeta( 'workflow policy v2 only allows skipping clarification or review', ); } +} + +function validateWorkflowLifecycle( + updated: WorkflowMeta, + existing: WorkflowMeta, +): void { if (!canTransition(existing.status, updated.status)) { throw new Error( `invalid workflow status transition: ${existing.status} -> ${updated.status}`, @@ -496,6 +523,9 @@ async function validateWorkflowMeta( } else if (updated.blockingReason !== undefined) { throw new Error('only blocked workflows can have a blocking reason'); } +} + +function validateWorkflowOutcome(updated: WorkflowMeta): void { if (updated.mode !== 'mamba' && updated.outcome !== undefined) { throw new Error('only manba workflows can have an outcome'); } @@ -512,6 +542,9 @@ async function validateWorkflowMeta( ) { throw new Error('completed manba workflows require an outcome'); } +} + +function validateWorkflowPolicyVersions(updated: WorkflowMeta): void { if (updated.mode === 'mamba' && updated.planVersion !== undefined) { throw new Error('manba workflows cannot have a plan version'); } @@ -549,6 +582,9 @@ async function validateWorkflowMeta( 'manba workflows cannot have a verification policy version', ); } +} + +function validateWorkflowPolicyState(updated: WorkflowMeta): void { if ( updated.verificationStatus !== undefined && !isVerificationStatus(updated.verificationStatus) @@ -585,6 +621,12 @@ async function validateWorkflowMeta( if (updated.mode === 'mamba' && updated.planDecision !== undefined) { throw new Error('manba workflows cannot have a plan decision'); } +} + +function validatePlanDecisionTransition( + updated: WorkflowMeta, + existing: WorkflowMeta, +): void { if ( updated.planDecision !== existing.planDecision && existing.planDecision !== undefined @@ -597,6 +639,12 @@ async function validateWorkflowMeta( ) { throw new Error('workflow plan decision can only be set at step 4'); } +} + +async function validatePlanningArtifactGate( + projectRoot: string, + updated: WorkflowMeta, +): Promise { if ( updated.planningPolicyVersion === 1 || updated.planningPolicyVersion === 2 @@ -654,6 +702,14 @@ async function validateWorkflowMeta( throw new Error('governed execution must be confirmed before step 5'); } } +} + +async function validateVerificationGate( + projectRoot: string, + updated: WorkflowMeta, + existing: WorkflowMeta, + options: UpdateWorkflowOptions, +): Promise { if ( updated.verificationPolicyVersion === 1 && updated.currentStep >= 7 && @@ -680,6 +736,12 @@ async function validateWorkflowMeta( 'verification-blocked workflows must resume through verify', ); } +} + +function validatePlanVersionTransition( + updated: WorkflowMeta, + existing: WorkflowMeta, +): void { if ( updated.planVersion !== undefined && (!Number.isInteger(updated.planVersion) || updated.planVersion < 1) @@ -699,6 +761,12 @@ async function validateWorkflowMeta( ) { throw new Error('workflow plan version can only change at step 4'); } +} + +async function validateWorkflowFamilyState( + projectRoot: string, + updated: WorkflowMeta, +): Promise { await validateParentTask(projectRoot, updated.mode, updated.parentTaskId); if ( (isTerminalWorkflowStatus(updated.status) || @@ -707,6 +775,12 @@ async function validateWorkflowMeta( ) { throw new Error('cannot finish workflow with an active manba child'); } +} + +async function validateWorkflowCompletionGates( + projectRoot: string, + updated: WorkflowMeta, +): Promise { if ( updated.status === 'completed' && updated.reviewPolicyVersion === 1 && diff --git a/src/team/policy.ts b/src/team/policy.ts index e17a229..118fe59 100644 --- a/src/team/policy.ts +++ b/src/team/policy.ts @@ -28,6 +28,7 @@ export interface TeamPolicyV1 { defaultVisibility: 'local' | 'shared'; shareConfirmedDecisions: boolean; retention: { + /** @deprecated Retained in schema V1 for backward-compatible policy exchange. */ localRawArtifactDays: number; localCacheDays: number; completedSessionDays: number; diff --git a/tests/adapter-upgrade-contracts.test.ts b/tests/adapter-upgrade-contracts.test.ts index f35f2d3..cc2c781 100644 --- a/tests/adapter-upgrade-contracts.test.ts +++ b/tests/adapter-upgrade-contracts.test.ts @@ -475,7 +475,7 @@ describe('adapter managed-content digest and upgrade', () => { expect(agents).toContain(''); }); - it('recovers every five-platform target from its write-before and write-after boundary', async () => { + it('recovers every registered-platform target from its write-before and write-after boundary', async () => { const discovery = await upgradeV3Adapters({ projectRoot: root, platforms: V3_ADAPTER_PLATFORMS, @@ -560,7 +560,7 @@ describe('adapter managed-content digest and upgrade', () => { caseIndex += 1; } } - }, 30_000); + }, 60_000); }); async function bootstrapAdapterCase( diff --git a/tests/cli-v3-surface-contracts.test.ts b/tests/cli-v3-surface-contracts.test.ts index 80fa0de..e6b5c69 100644 --- a/tests/cli-v3-surface-contracts.test.ts +++ b/tests/cli-v3-surface-contracts.test.ts @@ -1,13 +1,18 @@ -import type { Command } from 'commander'; +import { Command } from 'commander'; import { describe, expect, it, vi } from 'vitest'; +import { WORKFLOW_SUBCOMMANDS } from '../src/commands/workflow-subcommands.js'; describe('V3 CLI command surface', () => { it('registers coordination, migration, recovery, and explicit-sync commands', async () => { - const originalArgv = process.argv; - const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); - process.argv = ['node', 'mancode', 'version']; + const parse = vi.spyOn(Command.prototype, 'parse'); try { - const { cliProgram } = await import('../src/cli.js'); + const { createCliProgram } = await import('../src/cli.js'); + expect(parse).not.toHaveBeenCalled(); + + const cliProgram = createCliProgram(); + const secondProgram = createCliProgram(); + expect(secondProgram).not.toBe(cliProgram); + expect(secondProgram.commands).not.toBe(cliProgram.commands); expect(cliProgram.commands.map((command) => command.name())).toEqual( expect.arrayContaining([ @@ -81,13 +86,24 @@ describe('V3 CLI command surface', () => { expect(commandAt(cliProgram, 'init').helpInformation()).not.toContain( '--v3', ); + expect( + optionDescription(commandAt(cliProgram, 'init'), '--style'), + ).toContain('only supported with mancode init --legacy'); + expect( + optionDescription(commandAt(cliProgram, 'install'), '--minimal'), + ).toContain('Continuity bootstrap is already minimal'); expect(commandAt(cliProgram, 'context').helpInformation()).not.toMatch( /\bbeta\b/i, ); + expect(renderedHelp(commandAt(cliProgram, 'workflow'))).toContain( + WORKFLOW_SUBCOMMANDS.join(', '), + ); + expect(WORKFLOW_SUBCOMMANDS).toEqual( + expect.arrayContaining(['update', 'archive']), + ); expect(publicHelpText(cliProgram).join('\n')).not.toMatch(/\bV3\b/); } finally { - process.argv = originalArgv; - log.mockRestore(); + parse.mockRestore(); } }); }); @@ -110,6 +126,23 @@ function requiredOptions(command: Command): string[] { .filter((option): option is string => option !== undefined); } +function optionDescription(command: Command, name: string): string { + const option = command.options.find((candidate) => candidate.long === name); + if (option === undefined) throw new Error(`missing CLI option: ${name}`); + return option.description; +} + +function renderedHelp(command: Command): string { + let output = ''; + command.configureOutput({ + writeOut: (value) => { + output += value; + }, + }); + command.outputHelp(); + return output; +} + function publicHelpText(command: Command): string[] { return [ command.description(), diff --git a/tests/coverage-config-contracts.test.ts b/tests/coverage-config-contracts.test.ts new file mode 100644 index 0000000..76c3745 --- /dev/null +++ b/tests/coverage-config-contracts.test.ts @@ -0,0 +1,14 @@ +import { access } from 'node:fs/promises'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { CRITICAL_COVERAGE_THRESHOLDS } from '../vitest.config.js'; + +describe('coverage configuration', () => { + it('keeps every file-specific threshold attached to an existing source file', async () => { + for (const sourcePath of Object.keys(CRITICAL_COVERAGE_THRESHOLDS)) { + await expect(access(path.join(process.cwd(), sourcePath))).resolves.toBe( + undefined, + ); + } + }); +}); diff --git a/tests/project-policy-upgrade-contracts.test.ts b/tests/project-policy-upgrade-contracts.test.ts index 029dd68..dfd49d4 100644 --- a/tests/project-policy-upgrade-contracts.test.ts +++ b/tests/project-policy-upgrade-contracts.test.ts @@ -2,6 +2,7 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { projectUpgrade } from '../src/commands/project.js'; import { status } from '../src/commands/status.js'; import { initializeV3Project } from '../src/commands/v3-init.js'; import { digestCanonicalJson } from '../src/context/canonical.js'; @@ -277,6 +278,69 @@ describe('project Policy 2 upgrade', () => { }); await expect(listUnfinishedOperationRecoveries(root)).resolves.toEqual([]); }); + + it('validates the public command policy and operation id', async () => { + const logs = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + await expect( + projectUpgrade(root, { policy: '1', json: true }), + ).resolves.toBe(2); + expect(JSON.parse(String(logs.mock.calls.at(-1)?.[0]))).toMatchObject({ + error: { code: 'MANCODE_POLICY_VERSION_UNSUPPORTED' }, + }); + + await expect( + projectUpgrade(root, { + policy: '2', + operationId: 'not-an-operation-id', + json: true, + }), + ).resolves.toBe(3); + expect(JSON.parse(String(logs.mock.calls.at(-1)?.[0]))).toMatchObject({ + error: { code: 'MANCODE_PROJECT_UPGRADE_FAILED' }, + }); + } finally { + logs.mockRestore(); + } + }); + + it('drives dry-run and commit through the public project command', async () => { + const { sessionId } = await bootstrap(root, 60); + const operationId = id(70); + const logs = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + await expect( + projectUpgrade(root, { + policy: '2', + dryRun: true, + operationId, + json: true, + }), + ).resolves.toBe(0); + expect(JSON.parse(String(logs.mock.calls.at(-1)?.[0]))).toMatchObject({ + policy: 2, + willUpgrade: true, + operationId, + }); + + await expect( + projectUpgrade(root, { + policy: '2', + operationId, + session: sessionId, + client: 'vitest', + json: true, + }), + ).resolves.toBe(0); + expect(JSON.parse(String(logs.mock.calls.at(-1)?.[0]))).toMatchObject({ + state: 'committed', + manifest: { manifestVersion: 2 }, + operation: { operationId, state: 'committed' }, + }); + } finally { + logs.mockRestore(); + } + }); }); async function bootstrap( diff --git a/tests/release-check-contracts.test.ts b/tests/release-check-contracts.test.ts index 4e50917..1d8a293 100644 --- a/tests/release-check-contracts.test.ts +++ b/tests/release-check-contracts.test.ts @@ -27,16 +27,25 @@ describe('release candidate check', () => { expect(script).toMatch(/'clone',[\s\S]*'--branch',\s*'main'/); expect(script).toContain("branch: 'main'"); expect(script).toContain("createHash('sha256')"); + expect(script).toContain("['audit', '--audit-level=high', '--json']"); + expect(script).toContain("'dependency_audit'"); + expect(script).not.toContain("'--omit=dev'"); expect(script).not.toMatch(/['"]publish['"]/); expect(script).not.toContain('dist-tag'); }); - it('is registered as an explicit package script', async () => { + it('registers release and publish quality gates explicitly', async () => { const packageMetadata = JSON.parse( await readFile(path.join(root, 'package.json'), 'utf8'), ) as { scripts: Record }; expect(packageMetadata.scripts['release:check']).toBe( 'node scripts/release-check.mjs', ); + expect(packageMetadata.scripts.prepublishOnly).toContain( + 'npm audit --audit-level=high', + ); + expect(packageMetadata.scripts.prepublishOnly).toContain( + 'npm run test:coverage', + ); }); }); diff --git a/tests/v3-publish-promote-contracts.test.ts b/tests/v3-publish-promote-contracts.test.ts index 929b3e2..e348c69 100644 --- a/tests/v3-publish-promote-contracts.test.ts +++ b/tests/v3-publish-promote-contracts.test.ts @@ -1,23 +1,11 @@ import { execFile as execFileCallback } from 'node:child_process'; -import { - mkdir, - mkdtemp, - readFile, - readdir, - rm, - writeFile, -} from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { promisify } from 'node:util'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { initializeV3Project } from '../src/commands/v3-init.js'; import { type Ulid, createUlid } from '../src/context/ids.js'; -import { - localOverlayArtifactPath, - readLocalOverlayArtifact, - writeLocalOverlayArtifact, -} from '../src/context/local-overlay.js'; import { previewV3TaskPromotion, promoteV3Task, @@ -210,76 +198,6 @@ describe('V3 local-to-shared publish/promote', () => { }); }); - it('keeps shared-task raw evidence in the local overlay only', async () => { - const { sessionId } = await bootstrap(root); - const shared = await createV3Workflow({ - projectRoot: root, - task: 'Coordinate a shared task while retaining private diagnostics.', - workflowMode: 'manteam', - sessionId, - client: 'vitest', - sharedPrivacyConfirmed: true, - taskId: id(16), - operationId: id(17), - now: NOW, - }); - const store = new V3ContextStore(root); - const before = await store.readTaskSnapshot(shared.taskRef); - const sharedFilesBefore = await readdir(taskRootPath(root, shared.taskRef)); - const rawEvidence = - 'Authorization: Bearer overlay-only-secret\ntrace=/Users/alice/private.log'; - - const artifact = await writeLocalOverlayArtifact({ - projectRoot: root, - taskRef: shared.taskRef, - artifactId: id(18), - content: rawEvidence, - }); - - expect(artifact).toMatchObject({ - taskRef: shared.taskRef, - artifactId: id(18), - byteLength: Buffer.byteLength(rawEvidence), - contentDigest: expect.stringMatching(/^sha256:/), - path: localOverlayArtifactPath(root, shared.taskRef, id(18)), - }); - expect(path.relative(root, artifact.path)).toBe( - path.join( - '.mancode', - 'local', - 'overlays', - shared.taskRef.taskId, - 'artifacts', - id(18), - ), - ); - await expect( - readLocalOverlayArtifact(root, shared.taskRef, id(18)), - ).resolves.toEqual(Buffer.from(rawEvidence)); - await expect( - writeLocalOverlayArtifact({ - projectRoot: root, - taskRef: shared.taskRef, - artifactId: id(18), - content: 'different raw evidence', - }), - ).rejects.toThrow('MANCODE_OVERLAY_ARTIFACT_CONFLICT'); - await expect( - writeLocalOverlayArtifact({ - projectRoot: root, - taskRef: { namespace: 'local', taskId: shared.taskRef.taskId }, - artifactId: id(19), - content: rawEvidence, - }), - ).rejects.toThrow('MANCODE_OVERLAY_REQUIRES_SHARED_TASK'); - const after = await store.readTaskSnapshot(shared.taskRef); - expect(after.fingerprint).toBe(before.fingerprint); - expect(await readdir(taskRootPath(root, shared.taskRef))).toEqual( - sharedFilesBefore, - ); - expect(JSON.stringify(after)).not.toContain('overlay-only-secret'); - }); - it('does not publish a shared destination when the source contains private text', async () => { const { sessionId } = await bootstrap(root); const source = await createV3Workflow({ diff --git a/tests/website-docs.test.ts b/tests/website-docs.test.ts index aa21e12..e8065c7 100644 --- a/tests/website-docs.test.ts +++ b/tests/website-docs.test.ts @@ -1,6 +1,9 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; +import type { Command } from 'commander'; import { describe, expect, it } from 'vitest'; +import { createCliProgram } from '../src/cli.js'; +import { WORKFLOW_SUBCOMMANDS } from '../src/commands/workflow-subcommands.js'; const root = process.cwd(); const website = path.join(root, 'website'); @@ -26,6 +29,38 @@ function ids(html: string): Set { ); } +function documentedCommands(html: string): string[] { + return [...html.matchAll(/\bdata-cli-command="([^"]+)"/g)] + .map((match) => match[1]) + .sort(); +} + +function publicCommands(program: Command): string[] { + const result: string[] = []; + + function visit(parent: Command, path: string[]): void { + for (const command of parent.commands) { + const metadata = command as Command & { + _hidden?: boolean; + }; + if (metadata._hidden === true) continue; + + const commandPath = [...path, command.name()]; + if (command.name() === 'workflow') { + result.push( + ...WORKFLOW_SUBCOMMANDS.map((subcommand) => `workflow ${subcommand}`), + ); + } else { + result.push(commandPath.join(' ')); + } + visit(command, commandPath); + } + } + + visit(program, []); + return result.sort(); +} + describe('website documentation', () => { it('keeps every internal page anchor resolvable', async () => { for (const name of [ @@ -43,7 +78,24 @@ describe('website documentation', () => { } }); - it('documents the complete public CLI surface in both languages', async () => { + it('documents every public CLI command in both languages', async () => { + const commands = publicCommands(createCliProgram()); + + // `context beta` is Commander-hidden release plumbing. `workflow clean` + // remains an internal compatibility spelling and is intentionally absent + // from the public Continuity registry. + expect(commands).not.toContain('context beta'); + expect(commands).not.toContain('workflow clean'); + expect(commands).toContain('workflow update'); + expect(commands).toContain('workflow archive'); + + expect(documentedCommands(await readPage('docs.html'))).toEqual(commands); + expect(documentedCommands(await readPage('docs.zh-CN.html'))).toEqual( + commands, + ); + }); + + it('documents the core setup and Continuity workflows', async () => { for (const name of ['docs.html', 'docs.zh-CN.html']) { const html = await readPage(name); for (const requiredText of [ @@ -119,6 +171,9 @@ describe('website documentation', () => { ); previous = current; } + expect(example).toMatch( + /workflow plan <local:ULID> revise[^\n]*--scope-file scope\.json/, + ); expect(example).not.toMatch(/workflow update[^\n]*--step/); expect(example).not.toContain('workflow decide'); expect(example).not.toContain('verify <taskId>'); diff --git a/tsup.config.ts b/tsup.config.ts index 00a7c7e..19e5b7d 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ entry: ['src/cli.ts', 'src/index.ts'], format: ['esm'], - target: 'node20', + target: 'node22', platform: 'node', dts: true, clean: true, diff --git a/vitest.config.ts b/vitest.config.ts index ea62bee..63399cd 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,5 +1,20 @@ import { defineConfig } from 'vitest/config'; +export const CRITICAL_COVERAGE_THRESHOLDS = { + 'src/commands/project.ts': { + statements: 90, + branches: 70, + functions: 100, + lines: 90, + }, + // Preserve explicit floors for high-risk mutation and recovery boundaries. + // Raise these as focused command-contract coverage lands. + 'src/commands/operation.ts': { lines: 55 }, + 'src/commands/team.ts': { lines: 59 }, + 'src/commands/workflow.ts': { lines: 58 }, + 'src/runtime/operation-recovery-store.ts': { lines: 55 }, +} as const; + export default defineConfig({ test: { environment: 'node', @@ -11,10 +26,11 @@ export default defineConfig({ include: ['src/**/*.ts'], exclude: ['src/index.ts', 'src/version.ts'], thresholds: { - statements: 70, - branches: 70, - functions: 70, - lines: 70, + statements: 80, + branches: 72, + functions: 90, + lines: 80, + ...CRITICAL_COVERAGE_THRESHOLDS, }, }, }, diff --git a/website/docs.html b/website/docs.html index 18a20b9..71676a7 100644 --- a/website/docs.html +++ b/website/docs.html @@ -24,7 +24,7 @@
mancode - Documentation / v0.5.5 + Documentation / v0.5.6