🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

coding-agent-harness

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

coding-agent-harness - npm Package Compare versions

Comparing version
1.0.4
to
1.0.5
+217
docs-release/archi.../system-explainer/01-system-overview.md
# 01 — 系统全景
## 这是什么,解决什么问题
在 AI 编程工具(Codex、Claude Code、Gemini CLI)普及之前,
"任务管理"对开发者来说意味着 Jira ticket 或 GitHub issue——
这些工具是为人类设计的,Agent 读不懂,也无法从中派生出可验证的状态。
**Coding Agent Harness** 解决的核心问题是:
> 当 Agent 在你的代码仓库里工作时,如何确保它的工作有迹可查、有门可守、有人可审?
它不是 Agent 本身,也不是给人用的任务管理工具。
它是一个**仓库原生的 Agent 操作层**(repository-native operating layer)——
给 Agent 提供可执行的结构化上下文,让 Agent 能从文件中恢复执行,
而不依赖之前的聊天记忆。
核心设计理念只有一句话:
> **把重要状态保存在 Agent 能读的 Markdown 文件里,然后用 CLI 从这些文件派生出
> 状态、检查、迁移计划和 Dashboard 视图。**
---
## 为什么叫 "Harness"
"Harness"在工程语境中是"约束装置"的意思——用来约束、引导、测量一个系统的行为,
而不是替代它。就像测试框架中的 "test harness" 不是测试本身,
而是让测试可以被组织、执行和验证的基础设施。
Coding Agent Harness 不是 Agent 的替代品,而是 Agent 的约束装置:
- 任务有生命周期(创建 → 执行 → 审查 → 收口)
- 审查有门禁(Agent 不能自己给自己打通关)
- 状态有记录(每次变更都写入 Markdown,可 git blame)
- 人类有检查入口(本地 Dashboard + Workbench 审查确认)
---
## 和 Jira / Linear / GitHub Issues 的本质区别
| | Jira / Linear / GitHub Issues | Coding Agent Harness |
| --- | --- | --- |
| 设计给谁 | 人类协作 | Agent 执行 + 人类审查 |
| 状态存在哪 | 外部 SaaS 数据库 | 仓库内的 Markdown 文件 |
| Agent 能读吗 | 需要 API 集成 | 直接读文件 |
| 能 git diff 吗 | 不能 | 可以 |
| 能离线工作吗 | 不能 | 可以 |
| 能从文件恢复执行吗 | 不能 | 可以 |
---
## Level 0 — 四个大块
先看最高层。整个系统由四个大块组成:
```mermaid
flowchart LR
A["📦 Package\n发布的 npm 包\n(CLI + 模板 + 标准 + Preset)"]
B["📁 Target Repo\n用户的项目仓库\n(文档树 + 状态文件)"]
C["⚙️ Runtime\n运行时引擎\n(扫描 + 检查 + 生成)"]
D["👤 Human\n人类检查入口\n(Dashboard + 审查确认)"]
A -->|"scaffold + validate"| B
B -->|"读取"| C
C -->|"生成"| D
D -->|"review-confirm"| B
```
- **Package**:你 `npm install` 的那个东西,包含 CLI、模板、标准文档、Preset 包
- **Target Repo**:你的项目,harness 在里面创建 `docs/` 文档树来记录任务状态
- **Runtime**:CLI 运行时,扫描文档树、验证合规、生成 Dashboard
- **Human**:浏览器里看 Dashboard,在 Workbench 里做审查确认
注意这个循环的方向:**Package 写入 Target Repo,Runtime 读取 Target Repo,
Human 通过 review-confirm 再写回 Target Repo**。
整个系统是一个以 Markdown 文件为中心的读写循环,没有任何隐藏状态。
---
## Level 1 — 每个大块里有什么
### Package 里有什么
```mermaid
flowchart TD
PKG["📦 Package\ncoding-agent-harness@npm"]
PKG --> CLI["harness CLI\nscripts/harness.mjs\n唯一命令入口"]
PKG --> Lib["核心库\nscripts/lib/\n~30 个模块,6 个功能层"]
PKG --> Templates["任务模板\ntemplates/\n任务骨架文件(task_plan / visual_map 等)"]
PKG --> References["操作标准\nreferences/\n可复制到目标仓库的规范文档"]
PKG --> Presets["Preset 包\npresets/\n可复用任务方法(legacy-migration / module 等)"]
```
Package 是**只读的**——它提供工具和模板,但不存储任何状态。
状态全部在 Target Repo 里。
### Target Repo 里有什么
```mermaid
flowchart TD
REPO["📁 Target Repo\n你的项目仓库"]
REPO --> Entry["AGENTS.md\nAgent 入口和路由\n(告诉 Agent 去哪里找上下文)"]
REPO --> Caps[".harness-capabilities.json\n启用了哪些能力模块"]
REPO --> Docs["docs/\n文档树(harness 的工作区)"]
Docs --> Planning["09-PLANNING/\n任务目录 + 模块目录"]
Docs --> Ledger["Harness-Ledger.md\n全局账本(所有任务汇总)"]
Docs --> Walkthrough["10-WALKTHROUGH/\n收口证据和 Closeout SSoT"]
Docs --> Reference["11-REFERENCE/\n本地操作标准(从 Package 复制过来)"]
Docs --> Governance["01-GOVERNANCE/\nLesson 沉淀库"]
```
每个任务对应 `docs/09-PLANNING/TASKS/<task-id>/` 下的一个目录,
里面有 `task_plan.md`、`progress.md`、`visual_map.md`、`review.md` 等文件。
### Runtime 做什么
```mermaid
flowchart TD
RT["⚙️ Runtime\nharness CLI 运行时"]
RT --> Scan["扫描\n读取所有任务文件\n解析状态 / 阶段 / 审查 / Lesson"]
RT --> Check["检查\n9 个验证器验证合规性\n输出 failures / warnings"]
RT --> Generate["生成\n输出 Dashboard HTML + JSON 索引\n输出治理索引表"]
RT --> Migrate["迁移\n分析旧项目差距\n生成迁移动作队列"]
RT --> Lifecycle["生命周期\n执行状态转换命令\n写入 Governance Sync"]
```
Runtime 是**无状态的**——每次运行都从 Markdown 文件重新读取,
不缓存任何中间状态(除了 `harness dev` 的文件监听)。
---
## Level 2 — 核心概念词汇表
| 概念 | 一句话解释 | 在哪里 |
| --- | --- | --- |
| **Task** | 一个有生命周期的工作单元 | `docs/09-PLANNING/TASKS/<id>/` |
| **Budget** | 任务复杂度:`simple` / `standard` / `complex`,决定门禁严格程度 | `task_plan.md` |
| **Phase** | Visual Map 中的执行阶段,有状态和完成度 | `visual_map.md` |
| **Capability** | 可选功能模块,如 `dashboard`、`adversarial-review` | `.harness-capabilities.json` |
| **Review Gate** | 阻止任务完成的审查门禁,必须人工确认才能通过 | `INDEX.md` + `review.md` |
| **Governance Sync** | 任务状态变更时自动更新全局账本的原子操作 | `Harness-Ledger.md` |
| **Preset** | 可复用的任务方法包,如 `legacy-migration`、`module` | `presets/<id>/` |
| **Lesson** | 从任务中沉淀的可复用经验 | `docs/01-GOVERNANCE/lessons/` |
| **Tombstone** | 软删除 / 合并 / 被取代的任务标记 | `task_plan.md` 中的特殊块 |
| **lifecycleState** | 从任务状态 + 审查状态综合派生的队列分类 | 运行时派生,不存文件 |
---
## Level 2 — 设计决策
### 为什么用 Markdown 而不是数据库
这是最常被问到的问题。
**选择 Markdown 的原因**:
1. **Agent 可读**:所有主流 AI 编程工具都能读写 Markdown,不需要特殊 API
2. **Git 原生**:状态变更可以 diff、可以 blame、可以回滚,审计链天然存在
3. **人类可读**:不需要工具就能直接查看状态,降低工具依赖
4. **离线工作**:不依赖外部服务,断网也能用
5. **可移植**:换 Agent 工具不需要迁移数据
6. **单一事实源**:避免 Markdown、JSON、SQLite 三份事实互相漂移
**代价**:
- 解析 Markdown 比查数据库慢(但对于任务管理规模,这不是瓶颈)
- 格式约束需要靠检查器维护,而不是数据库 schema 强制
- 并发写入需要文件锁(`governance-sync` 的锁机制)
**被考虑但否决的方案**:
- **SQLite**:Git diff 不友好,引入二进制文件,且当前规模(几百任务)不需要
- **JSON**:适合机器解析但不适合 Agent 理解叙述性上下文
- **YAML/TOML**:不适合承载 brief、执行策略这类长文本内容
### 为什么是 npm 包而不是 SaaS
Agent 需要在本地文件系统上读写状态。SaaS 会引入网络依赖、认证、延迟,
破坏 Agent 的自主执行能力。npm 包让任何能运行 Node.js 的环境都能直接使用,
无需账号或网络。`package.json` 的 `dependencies` 为空——零运行时依赖。
### 为什么 review-confirm 必须是人工操作
`review-confirm` 是整个系统里**唯一不能被 Agent 自动执行**的操作。
原因:
> Agent 不能给自己的工作打通关。
这个边界不是一开始就有的。最初 Dashboard workbench 的 review action 没有 Agent/Human 区分。
后来通过竞品分析(Taskr competitive intake)识别出"Agent 自动确认 review"是 P0 风险,
才引入了 Git 提交门禁:`review-confirm` 会把带有 Git `user.name` / `user.email` 的
人工确认审计字段写入任务 `INDEX.md`,并做两次 Git 原子提交——第一次提交确认字段,
第二次提交包含第一个 commit SHA 的最终审计记录。这个 Git commit 是**可审计的人类签名**,
证明有真实的人类看过这个任务。
### 为什么派生状态不存储在文件里
`lifecycleState`、`taskQueues`、`reviewQueueState` 这些派生状态每次运行时重新计算,
不写回 Markdown 文件。原因有三:
1. **避免事实漂移**:如果派生状态也写回文件,就有了两份事实源,任何一份过期都会造成误报
2. **防止绕过门禁**:如果 Agent 能直接修改派生字段,就能绕过 review-confirm 的门禁
3. **治理规则即代码**:scanner 的推导规则本身就是治理规则的机器可读表达,每次运行重新计算等于每次都重新执行一遍治理检查
---
## 下一步
- 想理解代码怎么组织的 → [02-module-dependency.md](02-module-dependency.md)
- 想理解一个任务从头到尾怎么走 → [03-task-lifecycle.md](03-task-lifecycle.md)
- 想理解检查器在验什么 → [04-check-and-governance.md](04-check-and-governance.md)
- 想理解 Dashboard 数据从哪来 → [05-data-flow.md](05-data-flow.md)
- 想理解 Preset 和迁移怎么工作 → [06-preset-and-migration.md](06-preset-and-migration.md)
# 02 — 代码模块依赖关系
## Level 0 — 入口在哪
所有命令都从一个文件进来:
```mermaid
flowchart LR
User["用户 / Agent\n$ harness <command> [target]"] -->|"解析参数 + 分发"| Entry["scripts/harness.mjs\n唯一 CLI 入口"]
```
`harness.mjs` 做两件事:解析命令行参数,然后分发给对应的 command 模块或直接调用核心库。
它本身不包含任何业务逻辑。
---
## Level 1 — 命令如何分发
```mermaid
flowchart TD
Entry["scripts/harness.mjs"]
Entry -->|"dashboard\ndev"| DashCmd["scripts/dashboard-command.mjs\nDashboard 生成 + 动态服务"]
Entry -->|"migrate-plan\nmigrate-run\nmigrate-verify"| MigCmd["scripts/migration-command.mjs\n迁移三阶段命令"]
Entry -->|"new-task / task-start\ntask-phase / task-review\ntask-complete / review-confirm\ntask-tombstone"| TaskCmd["scripts/task-command.mjs\n任务生命周期命令"]
Entry -->|"preset catalog\npreset install\npreset uninstall"| PresetCmd["scripts/preset-command.mjs\nPreset 管理命令"]
Entry -->|"check / status / init\ngovernance / lesson-promote\n..."| Core["lib/harness-core.mjs\n(直接调用)"]
```
四个 command 模块各自负责一个领域,其余命令直接调用 `harness-core.mjs`。
**为什么这样分**:command 模块处理的是有复杂交互逻辑的命令(多步骤、需要读写多个文件、
有用户提示),而简单的查询类命令(`check`、`status`)直接调用核心库更简洁。
---
## Level 2 — harness-core.mjs 是什么
`harness-core.mjs` 是一个 **facade(门面)**,它自己不写任何业务逻辑,
只是把 `lib/` 下所有模块的导出重新 re-export 出来。
这样设计的好处:外部代码只需要 `import from "./lib/harness-core.mjs"` 就能拿到所有功能,
不需要知道具体在哪个子模块里。
```mermaid
flowchart TD
Core["harness-core.mjs\n(纯 re-export facade)"]
Core --> G1["① 核心工具层\ncore-shared + markdown-utils"]
Core --> G2["② 任务扫描层\ntask-scanner + review-model + lesson-candidates"]
Core --> G3["③ 检查与治理层\ncheck-profiles + governance-sync + governance-index"]
Core --> G4["④ Dashboard 层\ndashboard-data + dashboard-writer + workbench"]
Core --> G5["⑤ 任务生命周期层\ntask-lifecycle + review-gates + review-confirm"]
Core --> G6["⑥ 迁移与 Preset 层\nmigration-planner + preset-registry + tombstone"]
```
下面逐层展开。
---
## Level 3 — 六个功能层详解
### ① 核心工具层
这两个模块是所有其他模块的基础,几乎每个模块都会 import 它们:
```mermaid
flowchart LR
CoreShared["core-shared.mjs\n路径解析 / 常量枚举\n文件读写 / locale 处理\n模板渲染"]
MarkdownUtils["markdown-utils.mjs\nMarkdown 表格提取\n行更新 / 列查找\n依赖列表拆分"]
```
`core-shared` 定义了所有允许的枚举值,是整个系统的"类型系统":
| 枚举 | 允许值 |
| --- | --- |
| `allowedTaskStates` | `not_started / planned / in_progress / review / blocked / done` |
| `allowedTaskBudgets` | `simple / standard / complex` |
| `allowedPhaseStates` | `planned / in_progress / review / blocked / done / skipped` |
| `allowedCapabilities` | `core / module-parallel / subagent-worker / adversarial-review / ...` |
`markdown-utils` 提供了对 Markdown 表格的结构化操作——这是整个系统能从 Markdown 文件
派生状态的技术基础。
---
### ② 任务扫描层
负责读取 `docs/09-PLANNING/TASKS/` 下的所有文件,解析出结构化数据:
```mermaid
flowchart TD
TaskScanner["task-scanner.mjs\n扫描所有任务目录\n解析状态 / 预算 / 阶段 / 元数据"]
TaskScanner --> ReviewModel["task-review-model.mjs\n审查确认解析\n生命周期队列派生\ntombstone 解析"]
TaskScanner --> LessonCandidates["task-lesson-candidates.mjs\nLesson candidate 状态解析\n决策完成判定"]
ReviewModel --> CoreShared
ReviewModel --> MarkdownUtils
TaskScanner --> CoreShared
TaskScanner --> MarkdownUtils
```
`task-review-model` 里有几个关键的**派生函数**——它们不读文件,
只根据已解析的数据计算出新的状态:
| 函数 | 输入 | 输出 |
| --- | --- | --- |
| `deriveLifecycleState()` | taskState + reviewStatus + tombstone | `lifecycleState`(队列分类) |
| `deriveTaskQueues()` | lifecycleState + materials + lessons | `taskQueues[]`(属于哪些队列) |
| `deriveReviewQueueState()` | findings + confirmation | `reviewQueueState` |
| `parseTaskTombstone()` | task_plan.md 内容 | 软删除 / 合并 / 被取代状态 |
这些派生函数是**纯函数**,相同输入永远得到相同输出,便于测试和调试。
---
### ③ 检查与治理层
负责验证合规性,以及维护全局索引的原子写入:
```mermaid
flowchart TD
CheckProfiles["check-profiles.mjs\nbuildStatus() 编排 9 个验证器\n返回 failures + warnings + tasks"]
CheckProfiles --> V1["validateCapabilities\n能力注册表一致性"]
CheckProfiles --> V2["validateReviewSchema\nreview.md 结构"]
CheckProfiles --> V3["validateVisualMaps\nvisual_map 合规"]
CheckProfiles --> V4["validatePlanContracts\n任务合约标记"]
CheckProfiles --> V5["validateTaskPresetContracts\nPreset 合约"]
CheckProfiles --> V6["validateContextDocs\n上下文文档完整性"]
CheckProfiles --> V7["validateGovernanceTableBoundaries\n表格边界"]
CheckProfiles --> V8["validateSubagentAuthorization\nsubagent 授权"]
CheckProfiles --> V9["validateTaskCompletionConsistency\n完成一致性"]
CheckProfiles --> GitSummary["git-status-summary.mjs\nGit 状态摘要(dirty files 等)"]
GovSync["governance-sync.mjs\n原子锁 + 行级更新 + Git commit\n(任务状态变更时自动调用)"]
GovIndex["governance-index-generator.mjs\n重建全局索引表\n(手动触发)"]
GovIndex --> GovSync
```
**重要区分**:`governance-sync` 和 `check-profiles` 没有依赖关系。
- `check-profiles`:只读,验证状态,不写文件
- `governance-sync`:只写,更新账本,不做验证
---
### ④ Dashboard 层
负责把扫描结果转换成 HTML Dashboard:
```mermaid
flowchart TD
DashData["dashboard-data.mjs\nbuildDashboardBundle()\n收集 status + documents + tables + graph + adoption"]
DashData --> CheckProfiles["check-profiles.mjs\n(调用 buildStatus)"]
DashData --> DashWriter["dashboard-writer.mjs\n写入 HTML + JSON 文件\n(静态快照模式)"]
DashData --> StatusRenderer["status-dashboard-renderer.mjs\n渲染状态摘要文本"]
DashWorkbench["dashboard-workbench.mjs\nDev 动态服务\nHTTP server + 文件监听 + 自动刷新\n(harness dev 命令)"]
```
`DashWorkbench` 和 `DashData` / `DashWriter` 是**独立的**:
- `DashData` + `DashWriter`:生成静态快照(只读)
- `DashWorkbench`:启动本地 HTTP 服务,支持 Workbench 写操作
---
### ⑤ 任务生命周期层
负责执行所有任务状态转换命令:
```mermaid
flowchart TD
TaskLifecycle["task-lifecycle.mjs\n生命周期命令实现\nnew-task / task-start / task-phase\ntask-review / task-complete"]
TaskLifecycle --> ReviewGates["task-lifecycle/review-gates.mjs\n门禁验证逻辑\n(进入 review 前的检查)"]
TaskLifecycle --> ReviewConfirm["task-lifecycle/review-confirm.mjs\n人工确认执行\n(review-confirm 命令)"]
TaskLifecycle --> TextUtils["task-lifecycle/text-utils.mjs\n文本追加工具\n(向 Markdown 文件追加内容)"]
TaskLifecycle --> GovSync["governance-sync.mjs\n状态变更时同步账本"]
TaskLifecycle --> MigPreset["task-migration-preset.mjs\n迁移 preset 上下文注入"]
ReviewConfirm --> GitGate["review-confirm-git-gate.mjs\nGit 原子提交门禁\n(写入人工确认块 + commit)"]
```
`review-confirm` 是整个生命周期层里最特殊的命令——它是唯一需要 Git 原子提交的操作,
也是唯一不能被 Agent 自动执行的操作(见 [01-system-overview.md](01-system-overview.md) 的设计决策)。
---
### ⑥ 迁移与 Preset 层
```mermaid
flowchart TD
PresetReg["preset-registry.mjs\n读取 presets/ YAML\n验证包完整性\n分层发现(project / user / bundled)"]
PresetEngine["preset-engine.mjs\n执行 preset entrypoints\n(template / script / check 类型)"]
PresetAudit["preset-audit-contracts.mjs\n验证 preset 合约完整性"]
PresetResource["preset-resource-contracts.mjs\n验证 preset 资源声明"]
MigPlanner["migration-planner.mjs\n分析目标仓库差距\n生成迁移动作队列"]
MigSupport["migration-support.mjs\nsession 管理 / locale 探测\nGit 状态检查 / full-cutover 验证"]
Tombstone["task-tombstone-commands.mjs\n软删除 / 合并 / 重开命令"]
LessonSed["task-lesson-sedimentation.mjs\nLesson 沉淀任务创建"]
LessonMaint["lesson-maintenance.mjs\nLesson 库维护"]
TaskIndex["task-index.mjs\n任务索引生成"]
MigPlanner --> MigSupport
PresetEngine --> PresetReg
```
---
## 一张完整的依赖总图(参考用)
如果你已经理解了上面的分层,这张图可以作为查阅索引:
```mermaid
flowchart TD
Entry["harness.mjs"] --> DashCmd & MigCmd & TaskCmd & PresetCmd & Core["harness-core.mjs"]
Core --> CoreShared & MarkdownUtils
Core --> TaskScanner --> ReviewModel & LessonCandidates
Core --> CheckProfiles --> GitSummary
Core --> GovSync
Core --> GovIndex --> GovSync
Core --> DashData --> DashWriter & StatusRenderer
Core --> DashWorkbench
Core --> TaskLifecycle --> ReviewGates & ReviewConfirm & TextUtils & GovSync & MigPreset
ReviewConfirm --> GitGate
Core --> PresetReg
Core --> PresetEngine --> PresetReg
Core --> MigPlanner --> MigSupport
Core --> Tombstone
Core --> LessonSed
Core --> LessonMaint
Core --> TaskIndex
```
---
## Level 2 — 模块命名规律
理解命名规律可以帮你快速定位代码:
| 前缀 / 后缀 | 含义 | 例子 |
| --- | --- | --- |
| `task-` | 与任务相关 | `task-scanner`, `task-lifecycle`, `task-review-model` |
| `dashboard-` | 与 Dashboard 相关 | `dashboard-data`, `dashboard-writer`, `dashboard-workbench` |
| `governance-` | 与治理 / 账本相关 | `governance-sync`, `governance-index-generator` |
| `migration-` | 与迁移相关 | `migration-planner`, `migration-support` |
| `preset-` | 与 Preset 相关 | `preset-registry`, `preset-engine`, `preset-audit-contracts` |
| `check-` | 验证器 | `check-profiles`, `check-module-parallel` |
| `-command.mjs` | CLI 命令模块 | `task-command`, `dashboard-command` |
| `-utils.mjs` | 工具函数 | `markdown-utils`, `text-utils` |
| `-gates.mjs` | 门禁逻辑 | `review-gates`, `review-confirm-git-gate` |
# 03 — 任务生命周期
## Level 0 — 一个任务的一生
一个任务从创建到收口,经历六个状态:
```mermaid
flowchart LR
A["not_started\n目录已创建"] --> B["planned\n计划已填写"] --> C["in_progress\n正在执行"]
C --> D["review\n等待人工审查"]
D --> E["done\n收口完成"]
C -->|"外部阻塞"| F["blocked"] -->|"解除"| C
D -->|"打回重做"| C
```
每个状态转换都有对应的 CLI 命令触发。`planned` 状态在实践中通常被跳过——
Agent 创建任务后直接进入 `in_progress`。
---
## Level 1 — 状态与命令的对应关系
```mermaid
flowchart TD
NS["not_started\n任务目录已创建\n文件已 scaffold"]
IP["in_progress\n正在执行"]
R["review\n等待人工审查"]
D["done\n收口完成"]
BL["blocked\n外部依赖阻塞"]
NS -->|"harness task-start"| IP
IP -->|"harness task-review"| R
R -->|"harness review-confirm\n(人工操作)"| D
IP -->|"harness task-phase --blocked"| BL
BL -->|"harness task-start"| IP
R -->|"打回重做"| IP
```
**关键点**:`review-confirm` 是整个系统里**唯一不能被 Agent 自动执行**的命令。
它需要真实的人类操作,并会写入带有 Git `user.name` / `user.email` 的可审计确认块。
---
## Level 2 — Budget 决定门禁严格程度
Budget 是任务的复杂度等级,直接决定审查门禁有多严:
| 门禁项 | simple | standard | complex |
| --- | --- | --- | --- |
| 需要 Visual Map 阶段进度 | ✗ | ✓ | ✓ |
| 需要 lesson_candidates.md | ✗ | ✓ | ✓ |
| 需要 Agent 写 review.md | ✗ | ✓ | ✓ |
| 需要关闭所有 blocking findings | ✗ | ✓ | ✓ |
| 需要 Walkthrough 链接 | ✗ | ✓ | ✓ |
| 需要 Lesson 决策完成 | ✗ | ✓ | ✓ |
| 需要人工 review-confirm | ✗ | ✓ | ✓ |
`simple` 任务可以直接从 `in_progress` 跳到 `done`,没有任何门禁。
`standard` 和 `complex` 的门禁完全相同——区别在于 `complex` 任务通常需要 subagent 授权和对抗审查。
---
## Level 3 — task-review 的门禁细节
当 Agent 执行 `harness task-review` 时,系统在**进入 review 状态之前**做三项检查
(`review-gates.mjs`):
```mermaid
flowchart TD
Trigger["harness task-review task-123"]
Trigger --> C1{"当前状态 == in_progress?"}
C1 -->|"否"| E1["❌ 拒绝\n状态不对"]
C1 -->|"是(且 budget != simple)"| C2{"lesson_candidates.md 存在?"}
C2 -->|"否"| E2["❌ 拒绝\n缺 lesson candidates"]
C2 -->|"是"| C3{"Visual Map 有至少一个阶段\n记录了进度或证据?"}
C3 -->|"否"| E3["❌ 拒绝\n无阶段进度记录"]
C3 -->|"是"| OK["✅ 进入 review 状态"]
```
"阶段记录了进度"的判定(`review-gates.mjs`):
- `phase.completion > 0`,或
- `phase.state` 是 `in_progress / review / blocked / done`,或
- `phase.evidenceStatus` 是 `partial / present / waived`
进入 review 状态后,Agent 需要写 `review.md`,填写 findings 表格。
---
## Level 3 — review-confirm 的门禁细节
当人类执行 `harness review-confirm` 时,系统在**执行确认之前**做四项检查:
```mermaid
flowchart TD
Trigger["harness review-confirm task-123"]
Trigger --> C1{"确认文本与任务 ID 匹配?"}
C1 -->|"否"| E1["❌ 拒绝\n确认文本不对"]
C1 -->|"是"| C2{"无阻塞性审查发现?\n(P0/P1/P2 open findings)"}
C2 -->|"否"| E2["❌ 拒绝\n还有未关闭的 blocking findings"]
C2 -->|"是"| C3{"Git 工作树干净?"}
C3 -->|"否"| E3["❌ 拒绝\n有未提交的改动"]
C3 -->|"是"| Exec["✅ 执行确认"]
Exec --> Write1["写入确认审计字段\n到 INDEX.md"]
Write1 --> Commit1["Git 提交 #1\nchore: confirm review task-123"]
Commit1 --> Commit2["Git 提交 #2\nchore: record review confirmation audit task-123"]
```
**两次提交策略**:第一次提交 `INDEX.md` 中的确认字段,第二次提交最终审计元数据。
这样即使第二次提交失败,第一次提交也已经锁定了确认记录。
**Task Audit Metadata 确认字段**(写入 `INDEX.md`):
```markdown
## Task Audit Metadata
| Field | Value |
| --- | --- |
| Human Review Status | confirmed |
| Confirmation ID | HRC-<timestamp> |
| Confirmed At | <ISO timestamp> |
| Reviewer | <git user.name> |
| Reviewer Email | <git user.email> |
| Confirm Text | <task id confirmation> |
| Evidence Checked | <evidence path> |
| Review Commit SHA | <git commit sha> |
| Audit Status | committed |
```
---
## Level 3 — lifecycleState 派生逻辑
`lifecycleState` 是从任务状态 + 审查状态综合派生的,不存储在文件里,每次运行时重新计算。
派生函数 `deriveLifecycleState()` 的完整决策树(按优先级顺序):
| 条件 | lifecycleState |
| --- | --- |
| `reviewStatus == "blocked-open-findings"` | `review-blocked` |
| `closeoutStatus == "closed"` 且 `reviewStatus != "confirmed"` | `closed-review-pending` |
| `closeoutStatus == "closed"` | `closed` |
| `state == "blocked"` | `blocked` |
| `state == "done"` | `closing` |
| `state == "review"` | `in_review` |
| `state == "in_progress"` | `active` |
| `state == "planned"` 或 `"not_started"` | `ready` |
| 其他 | `unknown` |
---
## Level 3 — 生命周期队列
任务根据当前状态被自动分配到不同队列,这些队列在 Dashboard 中可见。
**一个任务可以同时属于多个队列**(比如同时在 `missing-materials` 和 `blocked`)。
队列分配逻辑(`deriveTaskQueues()`):
```mermaid
flowchart TD
Start["任务"]
Start --> T1{"tombstone.deletionState != active?"}
T1 -->|"是"| Q_DEL["soft-deleted-superseded 队列"]
T1 -->|"否"| T2{"有物料问题?"}
T2 -->|"是"| Q_MAT["missing-materials 队列"]
T2 -->|"否"| T3{"有阻塞原因?"}
T3 -->|"是"| Q_BLK["blocked 队列"]
T3 -->|"否"| T4{"已提交审查 + 准备确认\n+ 无 lesson 工作\n+ 无其他队列?"}
T4 -->|"是"| Q_REV["review 队列"]
T4 -->|"否"| T5{"有 lesson 工作?"}
T5 -->|"是"| Q_LES["lessons 队列"]
T5 -->|"否"| T6{"审查已确认?"}
T6 -->|"是,closeoutStatus=closed"| Q_FIN["finalized 队列"]
T6 -->|"是,其他"| Q_CON["confirmed 队列"]
T6 -->|"否,队列为空"| Q_ACT["active 队列"]
```
**阻塞原因来源**:物料问题、P0-P2 阻塞性 findings、状态冲突、过时的扫描器版本。
---
## Level 4 — Governance Sync:状态变更如何写入账本
任务状态每次变更,都会触发 `syncTaskGovernance()`,原子地更新 `Harness-Ledger.md`。
**锁机制**(`governance-sync.mjs`):
```mermaid
sequenceDiagram
participant CLI as harness CLI
participant Lock as .harness/locks/governance-sync.lock
participant Ledger as docs/Harness-Ledger.md
participant Git
CLI->>Lock: fs.openSync(lockPath, "wx")\n(排他写入,EEXIST 则抛错)
Note over Lock: 如果锁已存在 → GovernanceSyncError\ncode: governance-lock-exists
CLI->>Ledger: 更新 task-123 对应的行
CLI->>Git: git add + git commit
Git-->>CLI: commit SHA
CLI->>Lock: fs.unlinkSync(lockPath)\n(删除锁文件)
```
锁文件使用 `wx` 标志(写入+排他)创建,这是 Node.js 文件系统的原子操作——
如果文件已存在,`openSync` 会抛出 `EEXIST` 错误,不会覆盖。
**与 `governance rebuild` 的区别**:
| 操作 | 触发方式 | 写入目标 | 频率 |
| --- | --- | --- | --- |
| `syncTaskGovernance` | 自动(每次状态变更) | `Harness-Ledger.md` 对应行 | 高频 |
| `rebuildGovernanceIndexes` | 手动(`harness governance rebuild`) | `docs/09-PLANNING/generated/` 索引表 | 低频 |
---
## Level 3 — Tombstone:软删除与合并
任务可以被软删除、合并或被取代,而不是物理删除。
Tombstone 块追加到 `task_plan.md` 末尾(不替换原内容),保留历史审计链。
支持的操作:
- `supersedeTask()`:标记为被新任务替代
- `softDeleteTask()`:软删除
- `archiveTask()`:归档
- `reopenTask()`:移除 Tombstone 块,重新激活任务
**Tombstone 块格式**:
```markdown
## Task Tombstone
| Field | Value |
| --- | --- |
| State | superseded |
| Superseded By | new-task-id |
| Reason | <reason text> |
| Operator | coordinator |
| Timestamp | <ISO timestamp> |
| Reopen Eligible | yes |
| Archive Eligible | no |
```
---
## Level 2 — 设计决策
### 为什么需要 lifecycleState 这个派生状态
`task.state` 是 Agent 手写进 `progress.md` 的原始执行阶段,只有粗粒度值,
且存在大量历史别名(`complete`、`completed`、`doing`、`active` 等)。
这个字段无法区分"Agent 说自己完成了"和"人工确认完成了",
也无法区分"等待人审"和"材料缺失"。
`lifecycleState` 从多个文件综合推导,是 Dashboard 的主生命周期语义。
驱动这个设计的核心场景是:一个 `task.state = review` 的任务,
可能实际上处于"缺材料"、"有 open P0 finding"、"等待人审"三种完全不同的治理状态,
而旧模型把这三种情况全部混入同一个 review 队列。
### 为什么一个任务可以同时属于多个队列
一个任务可以同时"等待人审"(Review 队列)且"有未决 lesson candidate"(Lessons 队列),
这两件事的责任方不同(前者是 human reviewer,后者是 coordinator),
退出条件也不同,不应该合并成一个状态。多队列模型让每个治理关注点独立可追踪。
### 为什么 Tombstone 不直接删除任务目录
文档库没有数据库级外键,物理删除后会留下孤儿引用
(Ledger、Closeout SSoT、其他任务的 `Supersedes` 字段都可能指向被删任务)。
Tombstone 标记让 Soft-deleted / Superseded 队列可以只读追溯"为什么这个任务不在活跃队列里"。
### 为什么 review-confirm 需要两次 Git 提交
两次提交让 audit commit 的 SHA 成为不可篡改的时间戳。
第一次提交确认本身,第二次提交包含第一个 commit SHA 的审计记录。
如果只写文件不提交,Agent 可以在不留 Git 历史的情况下伪造确认状态。
检查器可以验证 confirmation commit 是否真实存在于 Git 历史中。
### 为什么 governance-sync 用文件锁而不是 Git 自身的锁
Git 自身的锁(`.git/index.lock`)只保护 index 操作,
不保护 Markdown 文件的读-改-写序列。两个并发 CLI 进程可以同时读取同一个 governance 表、
各自修改、然后先后提交,导致后者覆盖前者的行列更新。
文件锁的粒度是"整个 governance sync 操作",而不是单次 git 命令。
### 为什么 simple budget 跳过所有门禁
simple 任务对应 trivial 级别的改动(文档修正、配置调整),
强制经过 `task-review → review-confirm → task-complete` 三步会让 overhead 超过任务本身的价值。
这是有意的快速路径,不是遗漏。
### Lesson 系统的设计意图
Lesson 系统把任务执行中发现的可复用经验从"聊天里提到过"变成
"可追踪、可审查、可沉淀到标准文档"的治理对象。
Lesson candidate 决策必须在 `review-confirm` 之前完成,
因为 `review-confirm` 是责任转移点——人工确认后,任务进入 finalization,
此时再要求 Agent 补 lesson 决策会造成责任归属混乱。
# 04 — 检查体系与治理
## Level 0 — 检查的目的
`harness check` 和 `harness status` 的核心问题是:
> **这个仓库的文档状态是否合规?**
"合规"的定义因场景不同而不同,所以有三种 profile。
每种 profile 对应不同的使用场景,运行不同的验证器子集。
---
## Level 1 — 三种 Check Profile
```mermaid
flowchart LR
subgraph "source-package"
SP["验证 harness 自身\n发布包的完整性\n(CI 用)"]
end
subgraph "private-harness"
PH["验证私有运行仓库\n(如 .harness-private)"]
end
subgraph "target-project"
TP["验证用户的目标项目\n(最常用,默认)"]
end
```
| Profile | 典型命令 | 用途 |
| --- | --- | --- |
| `source-package` | `harness check --profile source-package .` | CI 验证 harness 自身发布包,检查 staged 文件边界(不允许 `.harness-private/` 或生成的 dashboard 被 tracked) |
| `private-harness` | `harness check --profile private-harness .harness-private` | 验证私有运行记录 |
| `target-project` | `harness check ~/my-app`(默认) | 验证用户项目合规,运行完整的 9 个验证器 |
**source-package 的特殊检查**:除了运行验证器,还会调用 `validateSourcePackageBoundary()`,
检查 git staged 文件中是否包含了不应发布的内容(`.harness-private/`、生成的 dashboard 等)。
---
## Level 2 — buildStatus() 调用了哪些验证器
`buildStatus()` 是检查的核心函数,它按顺序调用 9 个验证器:
```mermaid
flowchart TD
BS["buildStatus()"]
BS --> V1["① validateCapabilities\n能力注册表与实际文件是否一致\n依赖关系是否满足"]
BS --> V2["② validateReviewSchema\nreview.md 是否有必要的章节\nfindings 表格格式是否合规"]
BS --> V3["③ validateVisualMaps\nvisual_map.md 格式是否合规\n阶段字段是否完整"]
BS --> V4["④ validatePlanContracts\n任务文件是否有 Task Contract 标记"]
BS --> V5["⑤ validateTaskPresetContracts\nPreset 任务的合约是否完整\n资源声明是否存在"]
BS --> V6["⑥ validateContextDocs\n上下文文档(brief 等)是否存在"]
BS --> V7["⑦ validateGovernanceTableBoundaries\n治理表格的内容是否合规\n(不允许执行日志、临时 prompt 等)"]
BS --> V8["⑧ validateSubagentAuthorization\nsubagent 授权状态是否记录\n授权字段是否完整"]
BS --> V9["⑨ validateTaskCompletionConsistency\n已完成任务的 Visual Map 阶段是否也完成"]
```
每个验证器返回 `failures`(硬失败,必须修复)和 `warnings`(软警告,建议修复)。
> **注意**:`check-module-parallel.mjs` 存在于 `scripts/lib/` 但**不在** `buildStatus()` 的调用链中,它是独立工具,用于验证模块并行工作的 worktree 隔离。
---
## Level 3 — 每个验证器检查什么
### ① validateCapabilities
读取 `.harness-capabilities.json`,检查:
- 声明的能力是否都是合法的能力名(在 `allowedCapabilities` 枚举中)
- 能力的依赖是否都已启用(如 `subagent-worker` 需要先有 `module-parallel`)
- 能力对应的 artifact 路径是否存在
### ② validateReviewSchema
扫描所有 `review.md` 文件,检查每个文件是否包含 4 个必需章节(用字符串匹配,支持中英文):
1. `Reviewer Identity` / `审查者身份`
2. `Confidence Challenge` / `信心挑战`
3. `Evidence Checked` / `已检查证据`
4. `Final Confidence Basis` / `最终信心依据`
对于 findings 表格,还会检查:
- 必须有 Severity(P0-P3)、Open(yes/no)、Disposition、Blocks Release 列
- **P0/P1 severity 的 finding 不能同时 open=yes 或 blocks=yes**(这是硬失败)
- `accepted-risk` / `deferred` disposition 必须有 follow-up routing
- Evidence ID 引用(`E-\d+`)必须在 Evidence ID 表中存在
对于 verifier-backed review,还需要 `template_id: harness-verifier/v1` 和 `verdict: pass|fail|inconclusive`。
### ③ validateVisualMaps
检查 `visual_map.md` 中的 Phase ID 表必须包含 9 列:
`Phase ID, Depends On, State, Completion, Output, Required Evidence, Evidence Status, Blocking Risk, Owner / Handoff`
验证规则:
- `State` 必须在 `allowedPhaseStates` 中
- `Evidence Status` 必须在 `allowedEvidenceStatus` 中
- `Completion` 必须是 0-100 的整数
- `state=done` 时 `completion` 必须 = 100
- `state=planned` 时 `completion` 必须 = 0
- canonical 源的 visual map 需要 `Visual Map Contract: v1.0` 标记
### ④ validatePlanContracts
检查 `task_plan.md` 是否包含 `Task Contract: harness-task/v1` 标记行。
这是任务被 harness 识别的最基本要求。
### ⑤ validateTaskPresetContracts
对于使用了 Preset 的任务,检查:
- Preset 声明的资源文件是否存在
- `references/INDEX.md` 中是否有对应的索引行
- `task_plan.md` 的"Preset Required Reads"中是否列出了必需阅读
### ⑦ validateGovernanceTableBoundaries
检查 5 个全局治理表格的内容合规性:
| 表格 | 不允许出现的内容 |
| --- | --- |
| Feature-SSoT | 模块级细节、过长的证据描述 |
| Harness-Ledger | 执行日志、临时修复 prompt、原始对话记录 |
| Closeout-SSoT | 执行日志、原始对话记录 |
| Regression-SSoT | 执行日志、临时 prompt |
| Cadence-Ledger | 原始对话记录、临时 prompt |
**时间边界**:2026-05-24 之前的行标记为 legacy,仅产生 warning;之后的行产生 failure。
### ⑧ validateSubagentAuthorization
扫描所有 `execution_strategy.md` 文件,查找 **Subagent Authorization** 表。
对于 worker 角色且 status 为 `authorized` 的行,检查 4 个字段完整性:
`Authorized By, Authorized At, Scope, Worktree / Branch`
字段值必须是"具体的"(非空、非占位符 `[...]`、非 `pending/n/a/none/—` 等)。
若 **Subagent Delegation Decision** 表中 worker 的 decision=`ask-user`,
则必须在 **User Authorization Decision** 表中找到对应的已解决行。
**warning vs failure**:strict 模式下产生 failure;adoption 模式下产生 warning。
### ⑨ validateTaskCompletionConsistency
检查 `task.state=done` 的任务,其 Visual Map 中的所有 phase 是否也都完成。
"完成"的判定:`phase.state=skipped` 或 `(phase.state=done 且 phase.completion=100)`。
若存在不完整的 phase:
- `closeoutStatus=closed` → **failure**
- 否则 → **warning**
---
## Level 2 — 检查输出结构
`harness status --json` 输出的核心字段:
```mermaid
flowchart TD
Output["status JSON"]
Output --> F["failures[]\n硬失败,必须修复\n(阻止 CI 通过)"]
Output --> W["warnings[]\n软警告,建议修复\n(不阻止 CI)"]
Output --> T["tasks[]\n所有任务的结构化数据"]
Output --> C["capabilities[]\n能力注册表状态"]
Output --> G["git\nGit 状态摘要(dirty files 等)"]
T --> TF["每个 task 包含:\nid / title / state / budget\nlifecycleState / reviewQueueState\ntaskQueues[] / phases[]\ncloseoutStatus / tombstone\nlessonCandidateDecisionComplete\nbriefSource / visualMapSource\ntaskPreset / presetVersion\nhandoffs[]"]
```
---
## Level 3 — 治理索引重建
`harness governance rebuild --apply` 从任务扫描结果重建全局索引表:
```mermaid
flowchart TD
Cmd["harness governance rebuild --apply"]
Cmd --> Scan["collectTasks()\n扫描所有任务"]
Scan --> Filter["过滤掉 deletionState == deleted 的任务"]
Filter --> Sort["按 id 字母序排序"]
Sort --> Gen["生成 governance surfaces"]
Gen --> TI["docs/09-PLANNING/generated/task-index.md\n所有任务的汇总索引表"]
Gen --> MI["docs/09-PLANNING/generated/module-index.md\n模块步骤索引表"]
TI --> Atomic["原子写入\n(governance-sync 锁 + git commit)"]
MI --> Atomic
```
这个操作是**手动触发**的,不会在每次任务状态变更时自动运行。
自动运行的是 `syncTaskGovernance()`,它只更新 `Harness-Ledger.md` 的对应行。
**为什么分开**:`Harness-Ledger.md` 是高频写入的账本(每次状态变更都更新),
而 `generated/` 索引表是低频的全量重建(需要扫描所有任务,成本较高)。
把两者分开可以避免每次状态变更都触发全量扫描。
---
## Level 2 — 设计决策
### 为什么检查器分 failures 和 warnings 两级
两级从一开始就存在,设计动机是**迁移兼容性**:
- 新安装的项目(`strict=true`)对缺失文件报 failure 并阻断 CI
- 旧项目在 `safe-adoption` 模式下,同样的缺失只报 `adoption-needed: ...` warning,不阻断
这让 harness 可以在不破坏现有用户的情况下逐步收紧规范。
没有考虑过三级或更多级别——两级已经足够区分"必须修复"和"建议迁移"。
### 为什么 governance 表格有时间边界
2026-05-24 之前的行标记为 legacy,仅产生 warning;之后的行产生 failure。
这是因为 governance 表格的内容规范是后来引入的,
不能让历史数据突然变成硬失败阻断所有操作。
时间边界让新写入的行必须合规,同时给历史数据留出迁移窗口。
### 为什么 subagent authorization 区分 strict 和 adoption 模式
subagent 授权检查的严格程度取决于项目的成熟度:
- 新项目从一开始就要求完整的授权记录(strict → failure)
- 旧项目在迁移过程中可能有大量历史任务缺少授权记录(adoption → warning)
这避免了"接入 harness 后所有历史任务突然报错"的体验问题。
### 为什么 validateTaskCompletionConsistency 区分 closed 和非 closed
如果一个任务已经 `closeoutStatus=closed`(人工确认收口),
但 Visual Map 中还有未完成的 phase,这是一个严重的不一致——
说明收口确认时遗漏了检查,必须报 failure。
如果任务还没有 closed,同样的不一致只是 warning——
可能是 Agent 还在工作中,或者某些 phase 会被标记为 skipped。
# 05 — 数据流:从 Markdown 到 Dashboard
## Level 0 — 数据的起点和终点
```mermaid
flowchart LR
A["📄 Markdown 文件\n(docs/ 下的源文件)"]
B["⚙️ Scanner\n(解析 + 验证)"]
C["📊 Dashboard\n(HTML + JSON)"]
A -->|"读取"| B -->|"生成"| C
```
所有数据都来自 Markdown 文件,没有数据库,没有外部服务。
每次运行都从文件系统重新读取,不缓存任何中间状态。
---
## Level 1 — 哪些文件是数据源
```mermaid
flowchart TD
Sources["数据源文件"]
Sources --> TP["task_plan.md\n预算 / 标题 / 元数据 / Preset 信息\nTask Contract 标记"]
Sources --> PR["progress.md\n当前状态 / 操作日志"]
Sources --> VM["visual_map.md\n阶段列表 / 完成度 / 证据状态"]
Sources --> RV["review.md\nfindings 表格 / 人工确认块"]
Sources --> BR["brief.md\n任务摘要"]
Sources --> LC["lesson_candidates.md\nLesson 候选 / 决策状态"]
Sources --> HL["Harness-Ledger.md\n全局账本(所有任务汇总行)"]
Sources --> ES["execution_strategy.md\nsubagent 授权状态"]
```
---
## Level 2 — Scanner 如何处理这些文件
Scanner 层(`task-scanner.mjs` + `task-review-model.mjs`)把原始 Markdown 解析成结构化对象。
### collectTasks() 的发现流程
```mermaid
flowchart TD
CT["collectTasks()"]
CT --> Discover["listTaskPlanPaths()\n扫描两个根目录:\n09-PLANNING/TASKS/\n09-PLANNING/MODULES/\n过滤模板和归档目录"]
Discover --> ReadFiles["对每个任务目录读取 9 个文件\ntask_plan / brief / progress\nreview / visual_map\nexecution_strategy\nlesson_candidates / findings / context"]
ReadFiles --> Parse["解析各文件"]
Parse --> P1["parseTaskBudget()\n从 task_plan.md 提取 budget"]
Parse --> P2["parseTaskState()\n从 progress.md 提取 state"]
Parse --> P3["parsePhases()\n从 visual_map.md 提取阶段列表"]
Parse --> P4["parseAgentReviewSubmission()\n从 review.md 提取 Agent 提交状态"]
Parse --> P5["parseReviewConfirmation()\n从 review.md 提取人工确认块"]
Parse --> P6["parseLessonCandidateStatus()\n从 lesson_candidates.md 提取决策状态"]
Parse --> P7["parseTaskTombstone()\n从 task_plan.md 提取软删除状态"]
P1 & P2 & P3 & P4 & P5 & P6 & P7 --> Derive["派生计算(纯函数)"]
Derive --> LS["deriveLifecycleState()\n综合推导 lifecycleState"]
Derive --> QS["deriveTaskQueues()\n决定任务属于哪些队列"]
Derive --> RQS["deriveReviewQueueState()\n推导 reviewQueueState"]
```
### parseTaskState() 的格式
从 `progress.md` 的 `## Current Status` 或 `## Status` 标题后的第一行提取状态:
```markdown
## Current Status
in_progress
```
支持中文别名(`进行中` → `in_progress`)。如果格式不符合预期,会降级到遗留表格解析模式。
### parsePhases() 的表格格式
从 `visual_map.md` 中查找 `Phase ID` 列标题的表格,提取 9 个字段:
```markdown
| Phase ID | Depends On | State | Completion | Output | Required Evidence | Evidence Status | Blocking Risk | Owner / Handoff |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| P1 | — | done | 100 | ... | E-001 | present | low | coordinator |
```
依赖字段支持逗号、分号、`&` 分隔的多值。
---
## Level 2 — buildStatus() 把 Scanner 结果组装成什么
```mermaid
flowchart TD
BS["buildStatus()"]
BS --> Tasks["tasks[]\n每个任务的完整结构化数据\n(见下方字段列表)"]
BS --> Failures["failures[]\n硬失败列表"]
BS --> Warnings["warnings[]\n软警告列表"]
BS --> Caps["capabilities[]\n能力注册表状态"]
BS --> Git["git\nGit 状态摘要(dirty files 等)"]
BS --> Summary["summary\nbriefCoverage / visualMapCoverage\nfullCutoverEligible 等汇总指标"]
```
**task 对象的完整字段**:
| 字段 | 含义 |
| --- | --- |
| `path` | 相对路径 |
| `title` | 任务标题 |
| `state` | 任务状态(`in_progress / done / ...`) |
| `stateSource` | 状态来源(`valid / invalid`) |
| `budget` | 预算等级(`simple / standard / complex`) |
| `lifecycleState` | 派生的生命周期状态 |
| `reviewQueueState` | 审查队列状态 |
| `taskQueues[]` | 所属队列列表 |
| `phases[]` | 阶段列表(含 id / state / completion / evidenceStatus) |
| `closeoutStatus` | 收口状态 |
| `tombstone` | 软删除信息 |
| `briefSource` | brief 来源(`standalone / missing / ...`) |
| `visualMapSource` | visual map 来源(`canonical / legacy / missing`) |
| `taskPreset` | 使用的 Preset ID |
| `presetVersion` | Preset 版本 |
| `handoffs` | 交接信息数组 |
| `lessonCandidateDecisionComplete` | Lesson 决策是否完成 |
---
## Level 3 — buildDashboardBundle() 在 status 基础上再加什么
`buildDashboardBundle()` 调用 `buildStatus()` 后,再额外收集四类数据:
```mermaid
flowchart TD
Bundle["buildDashboardBundle()"]
Bundle --> Status["status\n(来自 buildStatus)"]
Bundle --> Documents["documents[]\n所有 Markdown 文档的内容\n每个文档:id / path / title / type / content"]
Bundle --> Tables["tables[]\n从文档中提取的 Markdown 表格\n每个表格:列定义 + 行数据"]
Bundle --> Graph["graph\n任务依赖关系图\nnodes(任务/阶段/模块/步骤)\nedges(依赖/包含/交接关系)"]
Bundle --> Adoption["adoption\n迁移采纳状态分析\n(warnings 分类汇总,含优先级和修复建议)"]
```
### documents 收集范围
`collectMarkdownDocuments()` 收集哪些文件:
```mermaid
flowchart TD
Collect["collectMarkdownDocuments()"]
Collect --> Fixed["固定路径(存在时收集)\nHarness-Ledger.md\n09-PLANNING/Module-Registry.md\n05-TEST-QA/Regression-SSoT.md\n10-WALKTHROUGH/Closeout-SSoT.md"]
Collect --> Walkthrough["10-WALKTHROUGH/ 下所有 .md\n(排除 _archive/ 和 _ 开头文件)"]
Collect --> TaskDocs["每个任务目录下:\nbrief / task_plan / execution_strategy\nvisual_map / lesson_candidates\nprogress / review / findings\nreferences/INDEX.md / artifacts/INDEX.md"]
Collect --> ModuleDocs["09-PLANNING/MODULES/ 下:\n每个模块的 module_plan.md\n每个模块的 brief.md"]
Collect --> Lessons["01-GOVERNANCE/lessons/ 下所有 .md"]
```
收集后统一过滤掉 `_archive/`、`_task-template/`、`_optional-structures/` 路径。
### graph 数据结构
图包含两类元素:
- **nodes**:任务节点、阶段节点、模块节点、步骤节点
- **edges**:依赖关系(阶段 → 阶段)、包含关系(任务 → 阶段)、交接关系(步骤 → 步骤)
如果依赖的源节点不存在,会创建一个 `external-dependency` 类型的虚拟节点。
---
## Level 2 — 两种 Dashboard 生成模式
```mermaid
flowchart LR
subgraph "静态模式(只读快照)"
SC["harness dashboard\n--out-dir ./out"]
SC --> SH["index.html\n(内联所有资源)"]
SC --> SJ["dashboard-data.json\n(供外部工具读取)"]
SC --> SF["status.json / tables.json\ndocuments.json / graph.json\nadoption.json"]
end
subgraph "动态模式(Workbench)"
DC["harness dev"]
DC --> HTTP["本地 HTTP 服务\nlocalhost:PORT"]
DC --> Watch["文件监听\n轮询模式,每 1 秒检查一次\n变化后延迟 250ms 重新生成"]
HTTP --> Browser["浏览器实时查看\n支持 review-confirm 等写操作"]
end
```
**关键边界**:静态 Dashboard 是只读的,不能触发任何写操作。
只有 `harness dev`(Workbench 模式)才能执行 `review-confirm`、`task-start` 等写操作。
### Dashboard HTML 生成方式
Dashboard HTML 通过字符串拼接生成(非模板引擎)。
`app.js` 通过 manifest 或直接读取获得——如果存在 manifest,
按顺序读取并拼接多个源文件(`app-src/` 目录下的模块化源码)。
payload 中的 `<` 被转义为 `<` 防止 HTML 注入。
### 文件监听实现
`dashboard-workbench.mjs` 的文件监听采用**轮询模式**(`startPollingWatch()`):
- 每 1000ms 检查一次目录树的最新修改时间(mtime)
- 检测到变化时,延迟 250ms 后触发重新生成(防抖)
- 监听范围:整个 `target.docsRoot`,排除 `.git`、`node_modules`、`tmp`
---
## Level 3 — markdown-utils.mjs 的核心能力
整个系统能从 Markdown 文件派生状态,技术基础是 `markdown-utils.mjs` 提供的表格解析能力:
| 函数 | 用途 |
| --- | --- |
| `markdownTableRows()` | 提取所有表格行 |
| `parseAllMarkdownTables()` | 解析文档中所有表格,返回结构化对象数组 |
| `splitMarkdownRow()` | 分割行单元格(处理转义管道符和代码块) |
| `tableAfterHeading()` | 定位特定标题后的表格 |
| `getCell()` | 按列名获取单元格(支持多个别名) |
| `splitList()` | 分割逗号/分号/加号分隔的列表 |
| `splitDependencies()` | 分割依赖,过滤 `none/n/a` 等占位符 |
**代码块内的管道符**:`splitMarkdownRow()` 会跟踪代码块状态,
代码块内的 `|` 不会被当作列分隔符,保留原始内容。
---
## Level 2 — 设计决策
### 为什么 Dashboard 是纯 HTML + vanilla JS,不用 React/Vite
harness 通过 `npx` 分发,引入 React/Vite 意味着用户每次运行都要拉取大量构建依赖,
破坏零依赖的可移植性。静态 HTML 可以直接从 `file://` 打开,
也可以作为 CI 证据快照分享,不需要任何运行时。
app-src 里的 vanilla JS 组件(`DashboardShell`、`SidebarNav`、`TableView` 等)
通过 manifest 按顺序拼接,每个文件 < 600 行,git diff 可读,不需要 webpack/esbuild。
### 为什么静态 Dashboard 是只读的
静态 Dashboard 的定位是"可分享的证据快照"——它可以被 CI 生成、离线打开、
发给外部审查者。这些场景下写操作没有安全边界(没有 CSRF/Origin/Host 校验)。
写操作只能在 Workbench 模式下执行,因为 Workbench server 绑定 `127.0.0.1`
并有完整的安全校验链。
### 为什么 `harness dev` 和 `harness dashboard` 是两个命令
`harness dashboard` 生成静态只读快照(适合 CI、迁移报告、离线证据)。
`harness dev` 启动本地动态 Workbench server,支持文件 watch、自动刷新、
review-confirm 写操作。两者的边界是:**静态快照可以分享,动态 Workbench 只能本地用**。
### 为什么文件监听用轮询而不是 fs.watch
`fs.watch` 在 macOS 上对深层目录树有已知的漏报问题,
而 harness 的 docs 目录结构是多层嵌套的 Markdown 文件树。
轮询方案实现简单、行为可预期、不引入 chokidar 等第三方依赖(与零依赖原则一致)。
1 秒轮询 + 250ms debounce 对人类编辑场景足够。
### 为什么不引入 SQLite 或 JSON 数据库
引入 JSON/SQLite 若没有清楚的 authority 边界,会形成 Markdown、JSON、SQLite 三份事实互相漂移。
Git review 对 Markdown/JSON 友好,对 SQLite diff 不友好。
当前规模是"几百任务",generated JSON + indexed in-memory filtering 足够。
决策是:Markdown 是唯一事实源,generated JSON index 是可再生缓存,
SQLite 只在任务数量和查询复杂度超过 JSON 能承载时才考虑引入,
且只能作为可再生查询缓存,不能手写、不能作为权威事实源。
# 06 — Preset 系统与迁移引擎
## Level 0 — 两个独立子系统
本文档覆盖两个相关但独立的子系统:
```mermaid
flowchart LR
A["Preset 系统\n可复用的任务方法包\n(new-task 时注入)"]
B["迁移引擎\n旧项目接入 harness\n(migrate-* 命令)"]
A -->|"legacy-migration preset\n是迁移引擎的专用 Preset"| B
```
它们的关系:迁移引擎有一个专用 Preset(`legacy-migration`),但 Preset 系统本身是通用的,
任何类型的任务都可以有自己的 Preset。
---
## Part 1 — Preset 系统
### Level 1 — Preset 是什么
Preset 是一个**可复用的任务方法包**,打包了特定类型任务所需的:
- 模板追加内容(在标准模板基础上追加 preset 专属内容)
- 执行脚本(plan / scaffold 阶段运行)
- 检查脚本(验证 preset 合规)
- 资源声明(参考文件、工件、必需阅读)
```mermaid
flowchart TD
Preset["Preset 包\npresets/<id>/"]
Preset --> Manifest["preset.yaml\n包清单(必须)"]
Preset --> Templates["templates/\n模板追加文件"]
Preset --> Scripts["scripts/\nplan / scaffold 脚本"]
Preset --> Checks["checks/\n检查脚本"]
Preset --> Workbench["workbench/\nDashboard 面板定义(可选)"]
```
### Level 1 — 分层发现:三个搜索层
Preset 按 **project → user → builtin** 的顺序搜索,同名 Preset 采用**首匹配优先**策略:
```mermaid
flowchart LR
P["project 层\n<target>/.coding-agent-harness/presets/\n(优先级最高)"]
U["user 层\n~/.coding-agent-harness/presets/\n(次优先)"]
B["builtin 层\npackage/presets/\n(最低优先级)"]
P -->|"未找到时继续"| U -->|"未找到时继续"| B
```
这个设计允许:
- **项目级覆盖**:在项目里放一个同名 Preset,覆盖 builtin 版本
- **用户级覆盖**:在 home 目录放 Preset,对所有项目生效
- **builtin 兜底**:package 自带的 Preset 作为默认实现
`harness preset install --project <preset-id>` 把 Preset 安装到项目层;
`harness preset uninstall --project <preset-id>` 从项目层移除(不影响 user 和 builtin 层)。
### Level 2 — preset.yaml 的结构
```mermaid
flowchart TD
YAML["preset.yaml"]
YAML --> Meta["基本信息\nid / version / purpose\ncompatibleBudgets / localeSupport"]
YAML --> Task["task 配置\nkind / requiresFromSession\nprojectLevelOnly"]
YAML --> EP["entrypoints\n(见下)"]
YAML --> WS["writeScopes\n限定写入路径(安全边界)"]
YAML --> Resources["resources\nreferences / artifacts / context.requiredReads"]
YAML --> Audit["audit\nmanifestRequired: true\nevidenceFiles[]"]
```
### Level 3 — Entrypoint 类型系统
每个 entrypoint 有两个维度:**name**(触发时机)和 **type**(执行方式):
```mermaid
flowchart LR
subgraph "Name(触发时机)"
N1["newTask\nharness new-task --preset X 时"]
N2["plan\n迁移计划阶段"]
N3["scaffold\n批量补全阶段"]
N4["check\n验证阶段"]
end
subgraph "Type(执行方式)"
T1["template\n追加模板内容到任务文件"]
T2["script\n运行 Node.js 脚本"]
T3["check\n运行检查脚本"]
end
```
三种执行方式的区别:
- **template**:渲染模板文件,将结果追加到任务文件(`task_plan.md`、`execution_strategy.md` 等)
- **script**:执行 Node.js 脚本,脚本可读写文件系统,返回结果作为 audit 证据
- **check**:执行检查脚本,验证 preset 应用的完整性,失败时阻止任务创建
每个 entrypoint 还声明 `writes`(允许写入的路径 glob)和 `reads`(允许读取的路径 glob)。
### Level 3 — writeScopes 安全边界
`writeScopes` 是一个路径白名单,限制 Preset 只能写入声明的目录。
`assertPresetWriteScope()` 在每次文件写入时检查相对路径是否匹配任何 scope。
```
writeScopes:
tasks:
path: docs/09-PLANNING/TASKS/**
access: write
```
支持 `path/**` 通配符表示目录及其所有子目录。
相对路径必须规范化(无 `../`、无绝对路径),防止目录遍历攻击。
### Level 2 — Preset 的生命周期
```mermaid
sequenceDiagram
participant User
participant CLI as harness CLI
participant Registry as preset-registry.mjs
participant Engine as preset-engine.mjs
participant TaskDir as 任务目录
User->>CLI: harness new-task my-task --preset legacy-migration --from-session session.json
CLI->>Registry: listPresetPackages()\n(分层发现,首匹配优先)
Registry-->>CLI: preset 对象(含 manifest + 路径)
CLI->>TaskDir: 创建标准任务骨架(brief / task_plan / visual_map 等)
CLI->>Engine: executeEntrypoint("newTask", preset, taskDir)
Engine->>TaskDir: 追加 preset 模板内容到任务文件
CLI->>TaskDir: 写入 preset metadata 到 task_plan.md
CLI->>TaskDir: 生成证据包(preset-manifest.json / preset-audit.json)
CLI-->>User: 任务创建完成
```
### Level 1 — 当前可用 Preset
| Preset ID | 用途 | 兼容 Budget | 特殊要求 |
| --- | --- | --- | --- |
| `legacy-migration` | 旧 harness 项目迁移到 v1.0 | complex | 需要 `--from-session session.json` |
| `lesson-sedimentation` | Lesson 沉淀任务 | standard, complex | 无 |
| `module` | 模块并行工作任务 | standard, complex | 需要 `--module <module-id>` |
| `standard-task` | 标准任务方法 | standard, complex | 无 |
---
## Part 2 — 迁移引擎
### Level 1 — 迁移的三个阶段
```mermaid
flowchart LR
A["① migrate-plan\n分析差距\n生成动作队列"] --> B["② migrate-run\n执行迁移动作\n写入 session 记录"] --> C["③ migrate-verify\n验证迁移结果\n对比 baseline"]
```
### Level 2 — migrate-plan 做什么
`buildMigrationPlan()` 识别 6 类差距:
```mermaid
flowchart TD
Plan["harness migrate-plan"]
Plan --> Scan["扫描目标仓库\nbuildStatus(非严格模式)"]
Scan --> Detect["检测已有能力\nreadCapabilityRegistry()"]
Detect --> Gaps["识别差距(6 类)"]
Gaps --> TaskActions["taskActions\n缺失 execution_strategy.md\n缺失 visual_map.md 的活跃任务"]
Gaps --> ReviewActions["reviewActions\n缺失审查字段\n(Reviewer Identity 等)"]
Gaps --> LegacyActions["legacyActions\n缺失必需的参考文件"]
Gaps --> LegacyResiduals["legacyResiduals\n历史任务的合同差距\n(不应自动迁移)"]
Gaps --> WeakBriefs["weakBriefTasks\nbrief.md 质量不达标"]
Gaps --> UnknownClass["unknownClassificationTasks\n任务分类不明确"]
```
**动作队列格式**:每个 taskAction 包含 `taskId`、`path`、`files[]`、`commands[]`、`action` 描述。
`commands[]` 是可以直接执行的 harness CLI 命令列表。
### Level 2 — migrate-verify 的两种模式
```mermaid
flowchart TD
Verify["harness migrate-verify session.json"]
Verify --> Normal["普通模式\n重新运行检查\n报告 failures / warnings"]
Verify --> FullCutover["--full-cutover 模式\n最严格验证(8 个条件)"]
FullCutover --> C1{"session.result == complete?"}
C1 -->|"否"| F1["FAIL"]
C1 -->|"是"| C2{"无 strictDeferred?"}
C2 -->|"否"| F2["FAIL"]
C2 -->|"是"| C3{"所有计数器 == 0?\n(warnings / taskActions\nreviewSchemaGaps / legacyResiduals 等)"}
C3 -->|"否"| F3["FAIL(列出非零计数器)"]
C3 -->|"是"| C4{"fullCutoverEligible == true?"}
C4 -->|"否"| F4["FAIL"]
C4 -->|"是"| P1["PASS(完全切换就绪)"]
```
`--full-cutover` 是迁移完成的最终验收标准:8 个条件全部满足才算通过。
---
## Part 3 — Capability 注册表
### Level 1 — 能力依赖图
```mermaid
flowchart TD
Core["core\n(必选,所有其他能力的基础)"]
Core --> ModParallel["module-parallel\n模块注册表 + 并行工作"]
Core --> AdvReview["adversarial-review\n对抗审查报告\n(别名:review-contract)"]
Core --> LongRunning["long-running-task\n长时间运行任务合约"]
Core --> Dashboard["dashboard\n本地 HTML Dashboard"]
Core --> SafeAdoption["safe-adoption\n旧项目平滑接入"]
ModParallel --> SubagentWorker["subagent-worker\ncommit-backed worker 交接协议"]
```
### Level 2 — 每个能力的 selectWhen
| 能力 | 何时启用 |
| --- | --- |
| `core` | 始终,这是必选基础 |
| `module-parallel` | 项目有 2 个以上独立模块需要并行所有权时 |
| `subagent-worker` | 代码变更 subagent 需要在独立 worktree 工作并 commit 交接时 |
| `adversarial-review` | 发布、架构、安全、数据或策略风险需要独立审查产物时 |
| `long-running-task` | Agent 可能跨多个 loop 运行而无需每步用户确认时 |
| `dashboard` | 需要本地只读状态可视化时 |
| `safe-adoption` | 将 v1.0 接入已有 harness 项目而不重写历史时 |
### Level 2 — Preset 资源声明(resources)
Preset 可以声明三类资源,这些资源会在任务创建时自动生成,并由检查器验证:
| 资源类型 | 含义 | 验证方式 |
| --- | --- | --- |
| `resources.references` | Preset 提供的参考文件 | 文件存在 + `references/INDEX.md` 中有索引 + `task_plan.md` 中有必需阅读声明 |
| `resources.artifacts` | Preset 生成的工件 | 文件存在 + `artifacts/INDEX.md` 中有索引 |
| `context.requiredReads` | 必需阅读的参考文件 ID | 在两个索引中都有记录 |
这是一个**三层验证链**:文件存在 → 索引表中有记录 → task_plan.md 中有必需阅读声明。
任何一层缺失都会产生 failure。
---
## Part 4 — 设计决策
### 为什么 Preset 不是一开始就有的
Preset 系统是从"旧项目迁移太复杂"这个需求演进出来的。最初只有 `new-task --budget complex`,
没有任何 preset 概念。当用户提出旧项目迁移需求时,最初的方案是发明一个新的 "Ultra" 任务等级。
研究后发现问题不是 Complex 不够用,而是"没有 preset 时每个 Agent 都要自己想迁移流程该怎么拆"。
三个独立 subagent 的对抗审查都指向同一结论——preset 是正确抽象,Ultra 是过度设计。
否决 Ultra 的理由:
1. Ultra 会引入第二套任务系统,破坏 simple/standard/complex 的一致性
2. 问题的根因不是 Complex 承载不了,而是没有预填骨架
3. Preset 是通用抽象,不只服务迁移——任何类型的任务都可以有自己的 Preset
### 为什么 Preset manifest 用 YAML
选择 YAML 是因为可读性——preset manifest 需要人工编写和审查,YAML 比 JSON 少引号和括号。
选择自写轻量解析器(`parseSimpleYaml()`)而不是引入 `js-yaml` 是为了保持零依赖。
JS 格式被排除是因为 preset 需要跨工具可审计,不能是可执行代码。
### 为什么分层发现(project/user/builtin)
分层发现比 preset 系统本身晚了两天引入。最初只从 package 的 `presets/` 固定路径读取。
分层设计解决的核心问题是:
- 不同项目可以有自己的私有 preset(project 层)
- 用户可以安装跨项目共享的 preset(user 层)
- Package bundled preset 作为兜底,不需要安装就能用(builtin 层)
优先级顺序(project > user > builtin)确保项目级定制能覆盖全局默认。
### 为什么迁移分三个阶段而不是一步到位
核心考量是"不能自动迁移"——preset 只搭骨架,不自动改历史文档,
不自动 stage 或 commit。真正写入目标仓库前,仍然要先让用户确认写入范围和迁移深度。
- `migrate-plan`:只读分析,无副作用
- `migrate-run`:有写入的执行,需要用户确认
- `migrate-verify`:事后验证,确认迁移结果
三步分离让用户在每个有副作用的节点都有确认机会。
### 为什么 full-cutover 验证这么严格
`full-cutover` 是不可逆的声明——一旦宣称完成迁移,后续 Agent 就不会再把这个项目当成迁移目标处理。
更宽松的方案(只检查 strict pass)被否决,因为真实项目验证(471 个任务)证明了
即使 strict pass,仍然可能有 weak brief 或 legacy-only visual map 残留。
### writeScopes 安全边界
writeScopes 和 preset 系统同时引入,不是后来补的安全加固。
Preset 是第三方可安装的包,如果不限制写入范围,恶意或错误的 preset 可以覆盖任意文件。
运行时强制检查路径,拒绝 `../` 开头的路径和绝对路径(path traversal 防护)。
# 01 — System Overview
## What this is and what problem it solves
Before AI coding tools (Codex, Claude Code, Gemini CLI) became widespread,
"task management" for developers meant Jira tickets or GitHub issues —
tools designed for humans that Agents can't read and can't derive verifiable state from.
**Coding Agent Harness** solves a core problem:
> When an Agent is working in your repository, how do you ensure its work is traceable,
> gated, and reviewable?
It's not an Agent itself, and it's not a task management tool for humans.
It's a **repository-native operating layer** — it gives Agents executable structured context
so they can resume execution from files without relying on previous chat memory.
The core design philosophy in one sentence:
> **Store important state in Markdown files that Agents can read, then use the CLI to derive
> status, checks, migration plans, and Dashboard views from those files.**
---
## Why it's called a "Harness"
"Harness" in an engineering context means a constraining device — something used to constrain,
guide, and measure a system's behavior without replacing it. Just like a "test harness" in
testing isn't the tests themselves, but the infrastructure that lets tests be organized,
executed, and verified.
Coding Agent Harness isn't a replacement for Agents — it's a harness for them:
- Tasks have a lifecycle (create → execute → review → closeout)
- Reviews have gates (Agents can't approve their own work)
- State is recorded (every change is written to Markdown, git-blammable)
- Humans have a review entry point (local Dashboard + Workbench review confirmation)
---
## The essential difference from Jira / Linear / GitHub Issues
| | Jira / Linear / GitHub Issues | Coding Agent Harness |
| --- | --- | --- |
| Designed for | Human collaboration | Agent execution + human review |
| State lives in | External SaaS database | Markdown files in the repository |
| Can Agents read it? | Requires API integration | Read files directly |
| Can you git diff it? | No | Yes |
| Works offline? | No | Yes |
| Can resume execution from files? | No | Yes |
---
## Level 0 — Four main blocks
Start at the highest level. The whole system is made up of four blocks:
```mermaid
flowchart LR
A["📦 Package\nPublished npm package\n(CLI + templates + standards + Presets)"]
B["📁 Target Repo\nUser's project repository\n(doc tree + state files)"]
C["⚙️ Runtime\nRuntime engine\n(scan + check + generate)"]
D["👤 Human\nHuman review entry point\n(Dashboard + review confirmation)"]
A -->|"scaffold + validate"| B
B -->|"read"| C
C -->|"generate"| D
D -->|"review-confirm"| B
```
- **Package**: What you `npm install` — contains the CLI, templates, standards docs, and Preset packages
- **Target Repo**: Your project, where harness creates a `docs/` tree to record task state
- **Runtime**: The CLI runtime that scans the doc tree, validates compliance, and generates the Dashboard
- **Human**: Views the Dashboard in a browser, performs review confirmation in the Workbench
Note the direction of this loop: **Package writes to Target Repo, Runtime reads from Target Repo,
Human writes back to Target Repo via review-confirm**.
The whole system is a read-write loop centered on Markdown files, with no hidden state.
---
## Level 1 — What's inside each block
### What's in the Package
```mermaid
flowchart TD
PKG["📦 Package\ncoding-agent-harness@npm"]
PKG --> CLI["harness CLI\nscripts/harness.mjs\nSingle command entry point"]
PKG --> Lib["Core library\nscripts/lib/\n~30 modules, 6 functional layers"]
PKG --> Templates["Task templates\ntemplates/\nTask scaffold files (task_plan / visual_map etc.)"]
PKG --> References["Operating standards\nreferences/\nSpec docs that can be copied to target repos"]
PKG --> Presets["Preset packages\npresets/\nReusable task methods (legacy-migration / module etc.)"]
```
The Package is **read-only** — it provides tools and templates but stores no state.
All state lives in the Target Repo.
### What's in the Target Repo
```mermaid
flowchart TD
REPO["📁 Target Repo\nYour project repository"]
REPO --> Entry["AGENTS.md\nAgent entry point and routing\n(tells Agents where to find context)"]
REPO --> Caps[".harness-capabilities.json\nWhich capability modules are enabled"]
REPO --> Docs["docs/\nDoc tree (harness workspace)"]
Docs --> Planning["09-PLANNING/\nTask directory + module directory"]
Docs --> Ledger["Harness-Ledger.md\nGlobal ledger (all tasks summarized)"]
Docs --> Walkthrough["10-WALKTHROUGH/\nCloseout evidence and Closeout SSoT"]
Docs --> Reference["11-REFERENCE/\nLocal operating standards (copied from Package)"]
Docs --> Governance["01-GOVERNANCE/\nLesson library"]
```
Each task corresponds to a directory under `docs/09-PLANNING/TASKS/<task-id>/`,
containing files like `task_plan.md`, `progress.md`, `visual_map.md`, `review.md`, etc.
### What the Runtime does
```mermaid
flowchart TD
RT["⚙️ Runtime\nharness CLI runtime"]
RT --> Scan["Scan\nRead all task files\nParse state / phases / review / Lessons"]
RT --> Check["Check\n9 validators verify compliance\nOutput failures / warnings"]
RT --> Generate["Generate\nOutput Dashboard HTML + JSON index\nOutput governance index tables"]
RT --> Migrate["Migrate\nAnalyze legacy project gaps\nGenerate migration action queue"]
RT --> Lifecycle["Lifecycle\nExecute state transition commands\nWrite Governance Sync"]
```
The Runtime is **stateless** — every run re-reads from Markdown files from scratch,
caching no intermediate state (except file watching in `harness dev`).
---
## Level 2 — Core concept glossary
| Concept | One-line explanation | Where |
| --- | --- | --- |
| **Task** | A unit of work with a lifecycle | `docs/09-PLANNING/TASKS/<id>/` |
| **Budget** | Task complexity: `simple` / `standard` / `complex`, determines gate strictness | `task_plan.md` |
| **Phase** | An execution phase in the Visual Map, with state and completion | `visual_map.md` |
| **Capability** | Optional feature module, e.g. `dashboard`, `adversarial-review` | `.harness-capabilities.json` |
| **Review Gate** | A review gate that blocks task completion, requires human confirmation to pass | `review.md` |
| **Governance Sync** | Atomic operation that auto-updates the global ledger on task state changes | `Harness-Ledger.md` |
| **Preset** | A reusable task method package, e.g. `legacy-migration`, `module` | `presets/<id>/` |
| **Lesson** | Reusable knowledge distilled from a task | `docs/01-GOVERNANCE/lessons/` |
| **Tombstone** | Marker for soft-deleted / merged / superseded tasks | Special block in `task_plan.md` |
| **lifecycleState** | Queue classification derived from task state + review state combined | Derived at runtime, not stored in files |
---
## Level 2 — Design decisions
### Why Markdown instead of a database
This is the most frequently asked question.
**Reasons for choosing Markdown**:
1. **Agent-readable**: All major AI coding tools can read and write Markdown without special APIs
2. **Git-native**: State changes can be diffed, blamed, and rolled back — audit trail is built in
3. **Human-readable**: State can be viewed directly without tools, reducing tool dependency
4. **Works offline**: No external service dependency, works without network
5. **Portable**: Switching Agent tools doesn't require data migration
6. **Single source of truth**: Avoids drift between Markdown, JSON, and SQLite as three separate facts
**Trade-offs**:
- Parsing Markdown is slower than querying a database (but not a bottleneck at task management scale)
- Format constraints must be maintained by validators rather than enforced by database schema
- Concurrent writes need file locking (`governance-sync` lock mechanism)
**Alternatives considered and rejected**:
- **SQLite**: Not git-diff-friendly, introduces binary files, and current scale (hundreds of tasks) doesn't need it
- **JSON**: Good for machine parsing but not for Agents understanding narrative context
- **YAML/TOML**: Not suited for carrying long-form content like briefs and execution strategies
### Why an npm package instead of SaaS
Agents need to read and write state on the local filesystem. SaaS would introduce network
dependency, authentication, and latency, breaking Agents' autonomous execution capability.
An npm package lets any environment that can run Node.js use it directly, with no account
or network required. `package.json` `dependencies` is empty — zero runtime dependencies.
### Why review-confirm must be a manual operation
`review-confirm` is the **only operation in the entire system that cannot be automatically
executed by an Agent**.
The reason:
> Agents cannot approve their own work.
This boundary wasn't there from the start. Initially the Dashboard workbench review action
had no Agent/Human distinction. Later, through competitive analysis (Taskr competitive intake),
"Agent auto-confirming review" was identified as a P0 risk, which led to introducing the
Git commit gate: `review-confirm` writes human confirmation audit fields with Git
`user.name` / `user.email` to the task `INDEX.md`, and makes two atomic Git commits:
the first commits the confirmation fields, and the second commits the final audit
record containing the first commit's SHA.
This Git commit is an **auditable human signature** proving a real human reviewed the task.
### Why derived state isn't stored in files
`lifecycleState`, `taskQueues`, and `reviewQueueState` are derived state that gets
recalculated on every run and never written back to Markdown files. Three reasons:
1. **Avoid fact drift**: If derived state were also written to files, there would be two sources
of truth, and either one going stale would cause false reports
2. **Prevent gate bypass**: If Agents could directly modify derived fields, they could bypass
the review-confirm gate
3. **Governance rules as code**: The scanner's derivation rules are themselves a machine-readable
expression of governance rules — recalculating on every run is equivalent to re-executing
the governance check every time
---
## Next steps
- Want to understand how the code is organized → [02-module-dependency.md](02-module-dependency.md)
- Want to understand how a task flows from start to finish → [03-task-lifecycle.md](03-task-lifecycle.md)
- Want to understand what the validators check → [04-check-and-governance.md](04-check-and-governance.md)
- Want to understand where Dashboard data comes from → [05-data-flow.md](05-data-flow.md)
- Want to understand how Presets and migration work → [06-preset-and-migration.md](06-preset-and-migration.md)
# 02 — Code Module Dependencies
## Level 0 — Where's the entry point
All commands come through a single file:
```mermaid
flowchart LR
User["User / Agent\n$ harness <command> [target]"] -->|"parse args + dispatch"| Entry["scripts/harness.mjs\nSingle CLI entry point"]
```
`harness.mjs` does two things: parses command-line arguments, then dispatches to the
corresponding command module or calls the core library directly.
It contains no business logic itself.
---
## Level 1 — How commands are dispatched
```mermaid
flowchart TD
Entry["scripts/harness.mjs"]
Entry -->|"dashboard\ndev"| DashCmd["scripts/dashboard-command.mjs\nDashboard generation + dynamic serving"]
Entry -->|"migrate-plan\nmigrate-run\nmigrate-verify"| MigCmd["scripts/migration-command.mjs\nMigration three-phase commands"]
Entry -->|"new-task / task-start\ntask-phase / task-review\ntask-complete / review-confirm\ntask-tombstone"| TaskCmd["scripts/task-command.mjs\nTask lifecycle commands"]
Entry -->|"preset catalog\npreset install\npreset uninstall"| PresetCmd["scripts/preset-command.mjs\nPreset management commands"]
Entry -->|"check / status / init\ngovernance / lesson-promote\n..."| Core["lib/harness-core.mjs\n(called directly)"]
```
Four command modules each own one domain; other commands call `harness-core.mjs` directly.
**Why this split**: Command modules handle commands with complex interaction logic
(multi-step, reading/writing multiple files, user prompts), while simple query commands
(`check`, `status`) are cleaner calling the core library directly.
---
## Level 2 — What is harness-core.mjs
`harness-core.mjs` is a **facade** — it contains no business logic itself,
it just re-exports everything from all modules under `lib/`.
The benefit of this design: external code only needs to
`import from "./lib/harness-core.mjs"` to get all functionality,
without knowing which sub-module something lives in.
```mermaid
flowchart TD
Core["harness-core.mjs\n(pure re-export facade)"]
Core --> G1["① Core utilities layer\ncore-shared + markdown-utils"]
Core --> G2["② Task scanning layer\ntask-scanner + review-model + lesson-candidates"]
Core --> G3["③ Check and governance layer\ncheck-profiles + governance-sync + governance-index"]
Core --> G4["④ Dashboard layer\ndashboard-data + dashboard-writer + workbench"]
Core --> G5["⑤ Task lifecycle layer\ntask-lifecycle + review-gates + review-confirm"]
Core --> G6["⑥ Migration and Preset layer\nmigration-planner + preset-registry + tombstone"]
```
Let's expand each layer.
---
## Level 3 — Six functional layers in detail
### ① Core utilities layer
These two modules are the foundation for all other modules — almost every module imports them:
```mermaid
flowchart LR
CoreShared["core-shared.mjs\nPath resolution / constant enums\nFile read/write / locale handling\nTemplate rendering"]
MarkdownUtils["markdown-utils.mjs\nMarkdown table extraction\nRow updates / column lookup\nDependency list splitting"]
```
`core-shared` defines all allowed enum values — it's the "type system" for the whole system:
| Enum | Allowed values |
| --- | --- |
| `allowedTaskStates` | `not_started / planned / in_progress / review / blocked / done` |
| `allowedTaskBudgets` | `simple / standard / complex` |
| `allowedPhaseStates` | `planned / in_progress / review / blocked / done / skipped` |
| `allowedCapabilities` | `core / module-parallel / subagent-worker / adversarial-review / ...` |
`markdown-utils` provides structured operations on Markdown tables — this is the technical
foundation that lets the whole system derive state from Markdown files.
---
### ② Task scanning layer
Responsible for reading all files under `docs/09-PLANNING/TASKS/` and parsing them into
structured data:
```mermaid
flowchart TD
TaskScanner["task-scanner.mjs\nScans all task directories\nParses state / budget / phases / metadata"]
TaskScanner --> ReviewModel["task-review-model.mjs\nReview confirmation parsing\nLifecycle queue derivation\nTombstone parsing"]
TaskScanner --> LessonCandidates["task-lesson-candidates.mjs\nLesson candidate status parsing\nDecision completion determination"]
ReviewModel --> CoreShared
ReviewModel --> MarkdownUtils
TaskScanner --> CoreShared
TaskScanner --> MarkdownUtils
```
`task-review-model` contains several key **derivation functions** — they don't read files,
they compute new state from already-parsed data:
| Function | Input | Output |
| --- | --- | --- |
| `deriveLifecycleState()` | taskState + reviewStatus + tombstone | `lifecycleState` (queue classification) |
| `deriveTaskQueues()` | lifecycleState + materials + lessons | `taskQueues[]` (which queues it belongs to) |
| `deriveReviewQueueState()` | findings + confirmation | `reviewQueueState` |
| `parseTaskTombstone()` | task_plan.md content | soft-delete / merge / superseded state |
These derivation functions are **pure functions** — same input always produces same output,
making them easy to test and debug.
---
### ③ Check and governance layer
Responsible for validating compliance and maintaining atomic writes to global indexes:
```mermaid
flowchart TD
CheckProfiles["check-profiles.mjs\nbuildStatus() orchestrates 9 validators\nReturns failures + warnings + tasks"]
CheckProfiles --> V1["validateCapabilities\nCapability registry consistency"]
CheckProfiles --> V2["validateReviewSchema\nreview.md structure"]
CheckProfiles --> V3["validateVisualMaps\nvisual_map compliance"]
CheckProfiles --> V4["validatePlanContracts\nTask contract markers"]
CheckProfiles --> V5["validateTaskPresetContracts\nPreset contracts"]
CheckProfiles --> V6["validateContextDocs\nContext doc completeness"]
CheckProfiles --> V7["validateGovernanceTableBoundaries\nTable boundaries"]
CheckProfiles --> V8["validateSubagentAuthorization\nSubagent authorization"]
CheckProfiles --> V9["validateTaskCompletionConsistency\nCompletion consistency"]
CheckProfiles --> GitSummary["git-status-summary.mjs\nGit status summary (dirty files etc.)"]
GovSync["governance-sync.mjs\nAtomic lock + row-level update + Git commit\n(auto-called on task state changes)"]
GovIndex["governance-index-generator.mjs\nRebuilds global index tables\n(manually triggered)"]
GovIndex --> GovSync
```
**Important distinction**: `governance-sync` and `check-profiles` have no dependency on each other.
- `check-profiles`: read-only, validates state, writes no files
- `governance-sync`: write-only, updates the ledger, does no validation
---
### ④ Dashboard layer
Responsible for converting scan results into an HTML Dashboard:
```mermaid
flowchart TD
DashData["dashboard-data.mjs\nbuildDashboardBundle()\nCollects status + documents + tables + graph + adoption"]
DashData --> CheckProfiles["check-profiles.mjs\n(calls buildStatus)"]
DashData --> DashWriter["dashboard-writer.mjs\nWrites HTML + JSON files\n(static snapshot mode)"]
DashData --> StatusRenderer["status-dashboard-renderer.mjs\nRenders status summary text"]
DashWorkbench["dashboard-workbench.mjs\nDev dynamic serving\nHTTP server + file watching + auto-refresh\n(harness dev command)"]
```
`DashWorkbench` and `DashData` / `DashWriter` are **independent**:
- `DashData` + `DashWriter`: generates static snapshots (read-only)
- `DashWorkbench`: starts a local HTTP server, supports Workbench write operations
---
### ⑤ Task lifecycle layer
Responsible for executing all task state transition commands:
```mermaid
flowchart TD
TaskLifecycle["task-lifecycle.mjs\nLifecycle command implementations\nnew-task / task-start / task-phase\ntask-review / task-complete"]
TaskLifecycle --> ReviewGates["task-lifecycle/review-gates.mjs\nGate validation logic\n(checks before entering review)"]
TaskLifecycle --> ReviewConfirm["task-lifecycle/review-confirm.mjs\nHuman confirmation execution\n(review-confirm command)"]
TaskLifecycle --> TextUtils["task-lifecycle/text-utils.mjs\nText append utilities\n(appending content to Markdown files)"]
TaskLifecycle --> GovSync["governance-sync.mjs\nSync ledger on state changes"]
TaskLifecycle --> MigPreset["task-migration-preset.mjs\nMigration Preset context injection"]
ReviewConfirm --> GitGate["review-confirm-git-gate.mjs\nGit atomic commit gate\n(writes human confirmation block + commit)"]
```
`review-confirm` is the most special command in the entire lifecycle layer — it's the only
operation that requires a Git atomic commit, and the only one that cannot be automatically
executed by an Agent (see design decisions in [01-system-overview.md](01-system-overview.md)).
---
### ⑥ Migration and Preset layer
```mermaid
flowchart TD
PresetReg["preset-registry.mjs\nReads presets/ YAML\nValidates package completeness\nLayered discovery (project / user / bundled)"]
PresetEngine["preset-engine.mjs\nExecutes Preset entrypoints\n(template / script / check types)"]
PresetAudit["preset-audit-contracts.mjs\nValidates Preset contract completeness"]
PresetResource["preset-resource-contracts.mjs\nValidates Preset resource declarations"]
MigPlanner["migration-planner.mjs\nAnalyzes target repo gaps\nGenerates migration action queue"]
MigSupport["migration-support.mjs\nSession management / locale detection\nGit status check / full-cutover verification"]
Tombstone["task-tombstone-commands.mjs\nSoft-delete / merge / reopen commands"]
LessonSed["task-lesson-sedimentation.mjs\nLesson sedimentation task creation"]
LessonMaint["lesson-maintenance.mjs\nLesson library maintenance"]
TaskIndex["task-index.mjs\nTask index generation"]
MigPlanner --> MigSupport
PresetEngine --> PresetReg
```
---
## Complete dependency map (reference)
If you've understood the layering above, this diagram serves as a lookup index:
```mermaid
flowchart TD
Entry["harness.mjs"] --> DashCmd & MigCmd & TaskCmd & PresetCmd & Core["harness-core.mjs"]
Core --> CoreShared & MarkdownUtils
Core --> TaskScanner --> ReviewModel & LessonCandidates
Core --> CheckProfiles --> GitSummary
Core --> GovSync
Core --> GovIndex --> GovSync
Core --> DashData --> DashWriter & StatusRenderer
Core --> DashWorkbench
Core --> TaskLifecycle --> ReviewGates & ReviewConfirm & TextUtils & GovSync & MigPreset
ReviewConfirm --> GitGate
Core --> PresetReg
Core --> PresetEngine --> PresetReg
Core --> MigPlanner --> MigSupport
Core --> Tombstone
Core --> LessonSed
Core --> LessonMaint
Core --> TaskIndex
```
---
## Level 2 — Module naming patterns
Understanding naming patterns helps you locate code quickly:
| Prefix / suffix | Meaning | Examples |
| --- | --- | --- |
| `task-` | Task-related | `task-scanner`, `task-lifecycle`, `task-review-model` |
| `dashboard-` | Dashboard-related | `dashboard-data`, `dashboard-writer`, `dashboard-workbench` |
| `governance-` | Governance / ledger-related | `governance-sync`, `governance-index-generator` |
| `migration-` | Migration-related | `migration-planner`, `migration-support` |
| `preset-` | Preset-related | `preset-registry`, `preset-engine`, `preset-audit-contracts` |
| `check-` | Validators | `check-profiles`, `check-module-parallel` |
| `-command.mjs` | CLI command modules | `task-command`, `dashboard-command` |
| `-utils.mjs` | Utility functions | `markdown-utils`, `text-utils` |
| `-gates.mjs` | Gate logic | `review-gates`, `review-confirm-git-gate` |
# 03 — Task Lifecycle
## Level 0 — A task's life
A task goes through six states from creation to closeout:
```mermaid
flowchart LR
A["not_started\nDirectory created"] --> B["planned\nPlan filled in"] --> C["in_progress\nExecuting"]
C --> D["review\nAwaiting human review"]
D --> E["done\nCloseout complete"]
C -->|"external block"| F["blocked"] -->|"unblocked"| C
D -->|"sent back for rework"| C
```
Each state transition is triggered by a corresponding CLI command. The `planned` state is
typically skipped in practice — Agents create a task and go directly to `in_progress`.
---
## Level 1 — States and their corresponding commands
```mermaid
flowchart TD
NS["not_started\nTask directory created\nFiles scaffolded"]
IP["in_progress\nExecuting"]
R["review\nAwaiting human review"]
D["done\nCloseout complete"]
BL["blocked\nBlocked by external dependency"]
NS -->|"harness task-start"| IP
IP -->|"harness task-review"| R
R -->|"harness review-confirm\n(manual operation)"| D
IP -->|"harness task-phase --blocked"| BL
BL -->|"harness task-start"| IP
R -->|"sent back for rework"| IP
```
**Key point**: `review-confirm` is the **only command in the entire system that cannot be
automatically executed by an Agent**. It requires a real human operation and writes an
auditable confirmation block with Git `user.name` / `user.email`.
---
## Level 2 — Budget determines gate strictness
Budget is the task's complexity level, and it directly determines how strict the review gates are:
| Gate | simple | standard | complex |
| --- | --- | --- | --- |
| Requires Visual Map phase progress | ✗ | ✓ | ✓ |
| Requires lesson_candidates.md | ✗ | ✓ | ✓ |
| Requires Agent to write review.md | ✗ | ✓ | ✓ |
| Requires all blocking findings closed | ✗ | ✓ | ✓ |
| Requires Walkthrough link | ✗ | ✓ | ✓ |
| Requires Lesson decision complete | ✗ | ✓ | ✓ |
| Requires human review-confirm | ✗ | ✓ | ✓ |
`simple` tasks can jump directly from `in_progress` to `done` with no gates.
`standard` and `complex` have identical gates — the difference is that `complex` tasks
typically require subagent authorization and adversarial review.
---
## Level 3 — Gate details for task-review
When an Agent runs `harness task-review`, the system performs three checks
**before entering review state** (`review-gates.mjs`):
```mermaid
flowchart TD
Trigger["harness task-review task-123"]
Trigger --> C1{"Current state == in_progress?"}
C1 -->|"no"| E1["❌ Rejected\nWrong state"]
C1 -->|"yes (and budget != simple)"| C2{"lesson_candidates.md exists?"}
C2 -->|"no"| E2["❌ Rejected\nMissing lesson candidates"]
C2 -->|"yes"| C3{"Visual Map has at least one phase\nwith recorded progress or evidence?"}
C3 -->|"no"| E3["❌ Rejected\nNo phase progress recorded"]
C3 -->|"yes"| OK["✅ Enter review state"]
```
How "phase has recorded progress" is determined (`review-gates.mjs`):
- `phase.completion > 0`, or
- `phase.state` is `in_progress / review / blocked / done`, or
- `phase.evidenceStatus` is `partial / present / waived`
After entering review state, the Agent needs to write `review.md` and fill in the findings table.
---
## Level 3 — Gate details for review-confirm
When a human runs `harness review-confirm`, the system performs four checks
**before executing the confirmation**:
```mermaid
flowchart TD
Trigger["harness review-confirm task-123"]
Trigger --> C1{"Confirmation text matches task ID?"}
C1 -->|"no"| E1["❌ Rejected\nWrong confirmation text"]
C1 -->|"yes"| C2{"No blocking review findings?\n(P0/P1/P2 open findings)"}
C2 -->|"no"| E2["❌ Rejected\nStill has unclosed blocking findings"]
C2 -->|"yes"| C3{"Git working tree clean?"}
C3 -->|"no"| E3["❌ Rejected\nHas uncommitted changes"]
C3 -->|"yes"| Exec["✅ Execute confirmation"]
Exec --> Write1["Write confirmation audit fields\nto INDEX.md"]
Write1 --> Commit1["Git commit #1\nchore: confirm review task-123"]
Commit1 --> Commit2["Git commit #2\nchore: record review confirmation audit task-123"]
```
**Two-commit strategy**: The first commit covers confirmation fields in `INDEX.md`; the second
commits the final audit metadata. Even if the second commit fails, the first commit has
already locked in the confirmation record.
**Task Audit Metadata confirmation fields** (written to `INDEX.md`):
```markdown
## Task Audit Metadata
| Field | Value |
| --- | --- |
| Human Review Status | confirmed |
| Confirmation ID | HRC-<timestamp> |
| Confirmed At | <ISO timestamp> |
| Reviewer | <git user.name> |
| Reviewer Email | <git user.email> |
| Confirm Text | <task id confirmation> |
| Evidence Checked | <evidence path> |
| Review Commit SHA | <git commit sha> |
| Audit Status | committed |
```
---
## Level 3 — lifecycleState derivation logic
`lifecycleState` is derived from task state + review state combined. It's not stored in files
and is recalculated on every run.
The complete decision tree for the derivation function `deriveLifecycleState()` (in priority order):
| Condition | lifecycleState |
| --- | --- |
| `reviewStatus == "blocked-open-findings"` | `review-blocked` |
| `closeoutStatus == "closed"` and `reviewStatus != "confirmed"` | `closed-review-pending` |
| `closeoutStatus == "closed"` | `closed` |
| `state == "blocked"` | `blocked` |
| `state == "done"` | `closing` |
| `state == "review"` | `in_review` |
| `state == "in_progress"` | `active` |
| `state == "planned"` or `"not_started"` | `ready` |
| other | `unknown` |
---
## Level 3 — Lifecycle queues
Tasks are automatically assigned to different queues based on their current state, and these
queues are visible in the Dashboard. **A task can belong to multiple queues simultaneously**
(e.g., both `missing-materials` and `blocked` at the same time).
Queue assignment logic (`deriveTaskQueues()`):
```mermaid
flowchart TD
Start["Task"]
Start --> T1{"tombstone.deletionState != active?"}
T1 -->|"yes"| Q_DEL["soft-deleted-superseded queue"]
T1 -->|"no"| T2{"Has materials issues?"}
T2 -->|"yes"| Q_MAT["missing-materials queue"]
T2 -->|"no"| T3{"Has blocking reasons?"}
T3 -->|"yes"| Q_BLK["blocked queue"]
T3 -->|"no"| T4{"Review submitted + ready for confirmation\n+ no lesson work\n+ no other queues?"}
T4 -->|"yes"| Q_REV["review queue"]
T4 -->|"no"| T5{"Has lesson work?"}
T5 -->|"yes"| Q_LES["lessons queue"]
T5 -->|"no"| T6{"Review confirmed?"}
T6 -->|"yes, closeoutStatus=closed"| Q_FIN["finalized queue"]
T6 -->|"yes, other"| Q_CON["confirmed queue"]
T6 -->|"no, queue empty"| Q_ACT["active queue"]
```
**Sources of blocking reasons**: materials issues, P0-P2 blocking findings, state conflicts,
outdated scanner version.
---
## Level 4 — Governance Sync: how state changes are written to the ledger
Every task state change triggers `syncTaskGovernance()`, which atomically updates `Harness-Ledger.md`.
**Lock mechanism** (`governance-sync.mjs`):
```mermaid
sequenceDiagram
participant CLI as harness CLI
participant Lock as .harness/locks/governance-sync.lock
participant Ledger as docs/Harness-Ledger.md
participant Git
CLI->>Lock: fs.openSync(lockPath, "wx")\n(exclusive write, throws EEXIST if exists)
Note over Lock: If lock exists → GovernanceSyncError\ncode: governance-lock-exists
CLI->>Ledger: Update the row for task-123
CLI->>Git: git add + git commit
Git-->>CLI: commit SHA
CLI->>Lock: fs.unlinkSync(lockPath)\n(delete lock file)
```
The lock file is created with the `wx` flag (write + exclusive) — this is an atomic Node.js
filesystem operation. If the file already exists, `openSync` throws `EEXIST` and won't overwrite.
**Difference from `governance rebuild`**:
| Operation | How triggered | Write target | Frequency |
| --- | --- | --- | --- |
| `syncTaskGovernance` | Automatic (on every state change) | Corresponding row in `Harness-Ledger.md` | High frequency |
| `rebuildGovernanceIndexes` | Manual (`harness governance rebuild`) | `docs/09-PLANNING/generated/` index tables | Low frequency |
---
## Level 3 — Tombstone: soft-delete and merge
Tasks can be soft-deleted, merged, or superseded rather than physically deleted.
The Tombstone block is appended to the end of `task_plan.md` (not replacing existing content),
preserving the historical audit trail.
Supported operations:
- `supersedeTask()`: mark as replaced by a new task
- `softDeleteTask()`: soft-delete
- `archiveTask()`: archive
- `reopenTask()`: remove the Tombstone block and reactivate the task
**Tombstone block format**:
```markdown
## Task Tombstone
| Field | Value |
| --- | --- |
| State | superseded |
| Superseded By | new-task-id |
| Reason | <reason text> |
| Operator | coordinator |
| Timestamp | <ISO timestamp> |
| Reopen Eligible | yes |
| Archive Eligible | no |
```
---
## Level 2 — Design decisions
### Why lifecycleState is needed as a derived state
`task.state` is the raw execution phase that Agents write into `progress.md` — it only has
coarse-grained values and has many historical aliases (`complete`, `completed`, `doing`,
`active`, etc.). This field can't distinguish "Agent says it's done" from "human confirmed
it's done", and can't distinguish "waiting for human review" from "missing materials".
`lifecycleState` is derived from multiple files and is the primary lifecycle semantic for
the Dashboard. The core scenario driving this design: a task with `task.state = review`
might actually be in three completely different governance states — "missing materials",
"has open P0 finding", or "waiting for human review" — but the old model lumped all three
into the same review queue.
### Why a task can belong to multiple queues simultaneously
A task can simultaneously be "waiting for human review" (Review queue) and "has pending
lesson candidate" (Lessons queue). These two things have different responsible parties
(the former is the human reviewer, the latter is the coordinator) and different exit
conditions — they shouldn't be merged into one state. The multi-queue model lets each
governance concern be tracked independently.
### Why Tombstone doesn't physically delete the task directory
The document library has no database-level foreign keys. Physical deletion would leave
orphan references (the Ledger, Closeout SSoT, and other tasks' `Supersedes` fields may
all point to the deleted task). Tombstone markers let the Soft-deleted / Superseded queue
provide read-only traceability for "why isn't this task in the active queue".
### Why review-confirm requires two Git commits
Two commits make the audit commit's SHA an immutable timestamp. The first commit covers
the confirmation itself; the second commit contains an audit record with the first commit's
SHA. If only files were written without committing, Agents could forge confirmation state
without leaving a Git history. The validator can verify that the confirmation commit
actually exists in Git history.
### Why governance-sync uses a file lock instead of Git's own lock
Git's own lock (`.git/index.lock`) only protects index operations, not the read-modify-write
sequence on Markdown files. Two concurrent CLI processes could simultaneously read the same
governance table, each modify it, then commit one after the other — the second would
overwrite the first's row updates. The file lock's granularity is "the entire governance
sync operation", not a single git command.
### Why simple budget skips all gates
Simple tasks correspond to trivial changes (doc corrections, config adjustments). Forcing
them through `task-review → review-confirm → task-complete` would make the overhead exceed
the value of the task itself. This is an intentional fast path, not an oversight.
### The design intent of the Lesson system
The Lesson system transforms reusable knowledge discovered during task execution from
"mentioned in chat" into a governance object that is "trackable, reviewable, and
sedimentable into standard docs". Lesson candidate decisions must be completed before
`review-confirm`, because `review-confirm` is the responsibility transfer point — once
human confirmation is done, the task enters finalization, and requiring the Agent to
add lesson decisions at that point would create accountability confusion.
# 04 — Check System and Governance
## Level 0 — The purpose of checks
The core question behind `harness check` and `harness status` is:
> **Is this repository's documentation state compliant?**
The definition of "compliant" varies by context, which is why there are three profiles.
Each profile corresponds to a different use case and runs a different subset of validators.
---
## Level 1 — Three check profiles
```mermaid
flowchart LR
subgraph "source-package"
SP["Validates harness itself\nPublished package integrity\n(for CI)"]
end
subgraph "private-harness"
PH["Validates private operating repo\n(e.g. .harness-private)"]
end
subgraph "target-project"
TP["Validates user's target project\n(most common, default)"]
end
```
| Profile | Typical command | Purpose |
| --- | --- | --- |
| `source-package` | `harness check --profile source-package .` | CI validation of harness's own published package; checks staged file boundaries (`.harness-private/` or generated dashboard must not be tracked) |
| `private-harness` | `harness check --profile private-harness .harness-private` | Validates private operating records |
| `target-project` | `harness check ~/my-app` (default) | Validates user project compliance, runs the full set of 9 validators |
**Special check for source-package**: In addition to running validators, it calls
`validateSourcePackageBoundary()` to check whether git staged files contain content
that shouldn't be published (`.harness-private/`, generated dashboard, etc.).
---
## Level 2 — Which validators does buildStatus() call
`buildStatus()` is the core check function. It calls 9 validators in sequence:
```mermaid
flowchart TD
BS["buildStatus()"]
BS --> V1["① validateCapabilities\nCapability registry vs actual files consistency\nDependency requirements satisfied"]
BS --> V2["② validateReviewSchema\nreview.md has required sections\nFindings table format is compliant"]
BS --> V3["③ validateVisualMaps\nvisual_map.md format is compliant\nPhase fields are complete"]
BS --> V4["④ validatePlanContracts\nTask files have Task Contract marker"]
BS --> V5["⑤ validateTaskPresetContracts\nPreset task contracts are complete\nResource declarations exist"]
BS --> V6["⑥ validateContextDocs\nContext docs (brief etc.) exist"]
BS --> V7["⑦ validateGovernanceTableBoundaries\nGovernance table content is compliant\n(no execution logs, temp prompts, etc.)"]
BS --> V8["⑧ validateSubagentAuthorization\nSubagent authorization state is recorded\nAuthorization fields are complete"]
BS --> V9["⑨ validateTaskCompletionConsistency\nCompleted tasks' Visual Map phases are also complete"]
```
Each validator returns `failures` (hard failures, must fix) and `warnings` (soft warnings, recommended to fix).
> **Note**: `check-module-parallel.mjs` exists in `scripts/lib/` but is **not** in `buildStatus()`'s
> call chain — it's a standalone tool for validating worktree isolation in module parallel work.
---
## Level 3 — What each validator checks
### ① validateCapabilities
Reads `.harness-capabilities.json` and checks:
- Whether declared capabilities are all valid capability names (in the `allowedCapabilities` enum)
- Whether capability dependencies are all enabled (e.g. `subagent-worker` requires `module-parallel` first)
- Whether the artifact paths corresponding to capabilities exist
### ② validateReviewSchema
Scans all `review.md` files and checks whether each contains 4 required sections
(string matching, supports both English and Chinese):
1. `Reviewer Identity`
2. `Confidence Challenge`
3. `Evidence Checked`
4. `Final Confidence Basis`
For findings tables, also checks:
- Must have Severity (P0-P3), Open (yes/no), Disposition, Blocks Release columns
- **P0/P1 severity findings cannot have open=yes or blocks=yes simultaneously** (hard failure)
- `accepted-risk` / `deferred` disposition must have follow-up routing
- Evidence ID references (`E-\d+`) must exist in the Evidence ID table
For verifier-backed reviews, also requires `template_id: harness-verifier/v1` and
`verdict: pass|fail|inconclusive`.
### ③ validateVisualMaps
Checks that the Phase ID table in `visual_map.md` must contain 9 columns:
`Phase ID, Depends On, State, Completion, Output, Required Evidence, Evidence Status, Blocking Risk, Owner / Handoff`
Validation rules:
- `State` must be in `allowedPhaseStates`
- `Evidence Status` must be in `allowedEvidenceStatus`
- `Completion` must be an integer from 0-100
- When `state=done`, `completion` must = 100
- When `state=planned`, `completion` must = 0
- Visual maps from canonical sources require a `Visual Map Contract: v1.0` marker
### ④ validatePlanContracts
Checks whether `task_plan.md` contains a `Task Contract: harness-task/v1` marker line.
This is the most basic requirement for a task to be recognized by harness.
### ⑤ validateTaskPresetContracts
For tasks that use a Preset, checks:
- Whether the resource files declared by the Preset exist
- Whether `references/INDEX.md` has the corresponding index row
- Whether the "Preset Required Reads" in `task_plan.md` lists the required reads
### ⑦ validateGovernanceTableBoundaries
Checks content compliance for 5 global governance tables:
| Table | Content not allowed |
| --- | --- |
| Feature-SSoT | Module-level details, overly long evidence descriptions |
| Harness-Ledger | Execution logs, temporary fix prompts, raw conversation records |
| Closeout-SSoT | Execution logs, raw conversation records |
| Regression-SSoT | Execution logs, temporary prompts |
| Cadence-Ledger | Raw conversation records, temporary prompts |
**Time boundary**: Rows before 2026-05-24 are marked as legacy and only produce warnings;
rows after that date produce failures.
### ⑧ validateSubagentAuthorization
Scans all `execution_strategy.md` files for the **Subagent Authorization** table.
For rows with worker role and status `authorized`, checks completeness of 4 fields:
`Authorized By, Authorized At, Scope, Worktree / Branch`
Field values must be "concrete" (non-empty, not placeholder `[...]`, not `pending/n/a/none/—` etc.).
If the **Subagent Delegation Decision** table has a worker with decision=`ask-user`,
there must be a corresponding resolved row in the **User Authorization Decision** table.
**warning vs failure**: produces failure in strict mode; produces warning in adoption mode.
### ⑨ validateTaskCompletionConsistency
Checks tasks with `task.state=done` to see whether all phases in their Visual Map are also complete.
"Complete" is determined as: `phase.state=skipped` or `(phase.state=done and phase.completion=100)`.
If incomplete phases exist:
- `closeoutStatus=closed` → **failure**
- Otherwise → **warning**
---
## Level 2 — Check output structure
Core fields in `harness status --json` output:
```mermaid
flowchart TD
Output["status JSON"]
Output --> F["failures[]\nHard failures, must fix\n(blocks CI)"]
Output --> W["warnings[]\nSoft warnings, recommended to fix\n(doesn't block CI)"]
Output --> T["tasks[]\nStructured data for all tasks"]
Output --> C["capabilities[]\nCapability registry state"]
Output --> G["git\nGit status summary (dirty files etc.)"]
T --> TF["Each task contains:\nid / title / state / budget\nlifecycleState / reviewQueueState\ntaskQueues[] / phases[]\ncloseoutStatus / tombstone\nlessonCandidateDecisionComplete\nbriefSource / visualMapSource\ntaskPreset / presetVersion\nhandoffs[]"]
```
---
## Level 3 — Governance index rebuild
`harness governance rebuild --apply` rebuilds global index tables from task scan results:
```mermaid
flowchart TD
Cmd["harness governance rebuild --apply"]
Cmd --> Scan["collectTasks()\nScan all tasks"]
Scan --> Filter["Filter out tasks where deletionState == deleted"]
Filter --> Sort["Sort alphabetically by id"]
Sort --> Gen["Generate governance surfaces"]
Gen --> TI["docs/09-PLANNING/generated/task-index.md\nSummary index table for all tasks"]
Gen --> MI["docs/09-PLANNING/generated/module-index.md\nModule step index table"]
TI --> Atomic["Atomic write\n(governance-sync lock + git commit)"]
MI --> Atomic
```
This operation is **manually triggered** and does not run automatically on every task state change.
What runs automatically is `syncTaskGovernance()`, which only updates the corresponding row
in `Harness-Ledger.md`.
**Why they're separate**: `Harness-Ledger.md` is a high-frequency write ledger (updated on
every state change), while the `generated/` index tables are low-frequency full rebuilds
(require scanning all tasks, which is costly). Keeping them separate avoids triggering a
full scan on every state change.
---
## Level 2 — Design decisions
### Why the validator has two levels: failures and warnings
Both levels existed from the start. The design motivation was **migration compatibility**:
- Newly installed projects (`strict=true`) report failure for missing files and block CI
- Legacy projects in `safe-adoption` mode report the same missing files as
`adoption-needed: ...` warnings without blocking
This lets harness gradually tighten standards without breaking existing users.
Three or more levels were never considered — two levels are sufficient to distinguish
"must fix" from "recommended migration".
### Why governance tables have a time boundary
Rows before 2026-05-24 are marked as legacy and only produce warnings; rows after produce failures.
This is because governance table content standards were introduced later, and historical data
can't suddenly become hard failures that block all operations. The time boundary requires
newly written rows to be compliant while giving historical data a migration window.
### Why subagent authorization distinguishes strict and adoption modes
The strictness of subagent authorization checks depends on project maturity:
- New projects require complete authorization records from the start (strict → failure)
- Legacy projects during migration may have many historical tasks missing authorization
records (adoption → warning)
This avoids the experience problem of "all historical tasks suddenly reporting errors
after adopting harness".
### Why validateTaskCompletionConsistency distinguishes closed vs non-closed
If a task already has `closeoutStatus=closed` (human-confirmed closeout) but still has
incomplete phases in the Visual Map, this is a serious inconsistency — it means the
closeout confirmation missed a check, and must report failure.
If the task isn't closed yet, the same inconsistency is only a warning — the Agent may
still be working, or some phases may be marked as skipped later.
# 05 — Data Flow: From Markdown to Dashboard
## Level 0 — Where data starts and ends
```mermaid
flowchart LR
A["📄 Markdown files\n(source files under docs/)"]
B["⚙️ Scanner\n(parse + validate)"]
C["📊 Dashboard\n(HTML + JSON)"]
A -->|"read"| B -->|"generate"| C
```
All data comes from Markdown files — no database, no external services.
Every run re-reads from the filesystem from scratch, caching no intermediate state.
---
## Level 1 — Which files are data sources
```mermaid
flowchart TD
Sources["Data source files"]
Sources --> TP["task_plan.md\nBudget / title / metadata / Preset info\nTask Contract marker"]
Sources --> PR["progress.md\nCurrent state / operation log"]
Sources --> VM["visual_map.md\nPhase list / completion / evidence status"]
Sources --> RV["review.md\nFindings table / human confirmation block"]
Sources --> BR["brief.md\nTask summary"]
Sources --> LC["lesson_candidates.md\nLesson candidates / decision state"]
Sources --> HL["Harness-Ledger.md\nGlobal ledger (all tasks summarized)"]
Sources --> ES["execution_strategy.md\nSubagent authorization state"]
```
---
## Level 2 — How the Scanner processes these files
The Scanner layer (`task-scanner.mjs` + `task-review-model.mjs`) parses raw Markdown
into structured objects.
### collectTasks() discovery flow
```mermaid
flowchart TD
CT["collectTasks()"]
CT --> Discover["listTaskPlanPaths()\nScans two root directories:\n09-PLANNING/TASKS/\n09-PLANNING/MODULES/\nFilters out template and archive directories"]
Discover --> ReadFiles["For each task directory, reads 9 files:\ntask_plan / brief / progress\nreview / visual_map\nexecution_strategy\nlesson_candidates / findings / context"]
ReadFiles --> Parse["Parse each file"]
Parse --> P1["parseTaskBudget()\nExtract budget from task_plan.md"]
Parse --> P2["parseTaskState()\nExtract state from progress.md"]
Parse --> P3["parsePhases()\nExtract phase list from visual_map.md"]
Parse --> P4["parseAgentReviewSubmission()\nExtract Agent submission state from review.md"]
Parse --> P5["parseReviewConfirmation()\nExtract human confirmation block from review.md"]
Parse --> P6["parseLessonCandidateStatus()\nExtract decision state from lesson_candidates.md"]
Parse --> P7["parseTaskTombstone()\nExtract soft-delete state from task_plan.md"]
P1 & P2 & P3 & P4 & P5 & P6 & P7 --> Derive["Derivation (pure functions)"]
Derive --> LS["deriveLifecycleState()\nDerive lifecycleState from combined inputs"]
Derive --> QS["deriveTaskQueues()\nDetermine which queues the task belongs to"]
Derive --> RQS["deriveReviewQueueState()\nDerive reviewQueueState"]
```
### parseTaskState() format
Extracts state from the first line after the `## Current Status` or `## Status` heading
in `progress.md`:
```markdown
## Current Status
in_progress
```
Supports Chinese aliases (`进行中` → `in_progress`). Falls back to legacy table parsing
mode if the format doesn't match expectations.
### parsePhases() table format
Finds the table with a `Phase ID` column header in `visual_map.md` and extracts 9 fields:
```markdown
| Phase ID | Depends On | State | Completion | Output | Required Evidence | Evidence Status | Blocking Risk | Owner / Handoff |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| P1 | — | done | 100 | ... | E-001 | present | low | coordinator |
```
The dependency field supports multiple values separated by commas, semicolons, or `&`.
---
## Level 2 — What buildStatus() assembles from Scanner results
```mermaid
flowchart TD
BS["buildStatus()"]
BS --> Tasks["tasks[]\nComplete structured data for each task\n(see field list below)"]
BS --> Failures["failures[]\nHard failure list"]
BS --> Warnings["warnings[]\nSoft warning list"]
BS --> Caps["capabilities[]\nCapability registry state"]
BS --> Git["git\nGit status summary (dirty files etc.)"]
BS --> Summary["summary\nbriefCoverage / visualMapCoverage\nfullCutoverEligible and other aggregate metrics"]
```
**Complete fields of a task object**:
| Field | Meaning |
| --- | --- |
| `path` | Relative path |
| `title` | Task title |
| `state` | Task state (`in_progress / done / ...`) |
| `stateSource` | State source (`valid / invalid`) |
| `budget` | Budget level (`simple / standard / complex`) |
| `lifecycleState` | Derived lifecycle state |
| `reviewQueueState` | Review queue state |
| `taskQueues[]` | List of queues the task belongs to |
| `phases[]` | Phase list (with id / state / completion / evidenceStatus) |
| `closeoutStatus` | Closeout status |
| `tombstone` | Soft-delete information |
| `briefSource` | Brief source (`standalone / missing / ...`) |
| `visualMapSource` | Visual map source (`canonical / legacy / missing`) |
| `taskPreset` | Preset ID used |
| `presetVersion` | Preset version |
| `handoffs` | Handoff information array |
| `lessonCandidateDecisionComplete` | Whether Lesson decision is complete |
---
## Level 3 — What buildDashboardBundle() adds on top of status
`buildDashboardBundle()` calls `buildStatus()` then additionally collects four types of data:
```mermaid
flowchart TD
Bundle["buildDashboardBundle()"]
Bundle --> Status["status\n(from buildStatus)"]
Bundle --> Documents["documents[]\nContent of all Markdown documents\nEach document: id / path / title / type / content"]
Bundle --> Tables["tables[]\nMarkdown tables extracted from documents\nEach table: column definitions + row data"]
Bundle --> Graph["graph\nTask dependency graph\nnodes (tasks/phases/modules/steps)\nedges (dependency/containment/handoff relationships)"]
Bundle --> Adoption["adoption\nMigration adoption state analysis\n(warnings categorized with priority and fix suggestions)"]
```
### documents collection scope
Which files `collectMarkdownDocuments()` collects:
```mermaid
flowchart TD
Collect["collectMarkdownDocuments()"]
Collect --> Fixed["Fixed paths (collected when they exist)\nHarness-Ledger.md\n09-PLANNING/Module-Registry.md\n05-TEST-QA/Regression-SSoT.md\n10-WALKTHROUGH/Closeout-SSoT.md"]
Collect --> Walkthrough["All .md files under 10-WALKTHROUGH/\n(excluding _archive/ and files starting with _)"]
Collect --> TaskDocs["Under each task directory:\nbrief / task_plan / execution_strategy\nvisual_map / lesson_candidates\nprogress / review / findings\nreferences/INDEX.md / artifacts/INDEX.md"]
Collect --> ModuleDocs["Under 09-PLANNING/MODULES/:\nmodule_plan.md for each module\nbrief.md for each module"]
Collect --> Lessons["All .md files under 01-GOVERNANCE/lessons/"]
```
After collection, uniformly filters out `_archive/`, `_task-template/`, and
`_optional-structures/` paths.
### graph data structure
The graph contains two types of elements:
- **nodes**: task nodes, phase nodes, module nodes, step nodes
- **edges**: dependency relationships (phase → phase), containment relationships (task → phase),
handoff relationships (step → step)
If the source node of a dependency doesn't exist, a virtual node of type
`external-dependency` is created.
---
## Level 2 — Two Dashboard generation modes
```mermaid
flowchart LR
subgraph "Static mode (read-only snapshot)"
SC["harness dashboard\n--out-dir ./out"]
SC --> SH["index.html\n(all resources inlined)"]
SC --> SJ["dashboard-data.json\n(for external tools to read)"]
SC --> SF["status.json / tables.json\ndocuments.json / graph.json\nadoption.json"]
end
subgraph "Dynamic mode (Workbench)"
DC["harness dev"]
DC --> HTTP["Local HTTP server\nlocalhost:PORT"]
DC --> Watch["File watching\nPolling mode, checks every 1 second\nRegenerates after 250ms delay on change"]
HTTP --> Browser["Live browser view\nSupports review-confirm and other write operations"]
end
```
**Key boundary**: The static Dashboard is read-only and cannot trigger any write operations.
Only `harness dev` (Workbench mode) can execute write operations like `review-confirm`
and `task-start`.
### Dashboard HTML generation
Dashboard HTML is generated via string concatenation (no template engine).
`app.js` is obtained via manifest or direct read — if a manifest exists, it reads and
concatenates multiple source files in order (modular source code under `app-src/`).
`<` in the payload is escaped to `&lt;` to prevent HTML injection.
### File watching implementation
`dashboard-workbench.mjs` file watching uses **polling mode** (`startPollingWatch()`):
- Checks the latest modification time (mtime) of the directory tree every 1000ms
- When a change is detected, triggers regeneration after a 250ms delay (debounce)
- Watch scope: the entire `target.docsRoot`, excluding `.git`, `node_modules`, `tmp`
---
## Level 3 — Core capabilities of markdown-utils.mjs
The technical foundation that lets the whole system derive state from Markdown files is
the table parsing capability provided by `markdown-utils.mjs`:
| Function | Purpose |
| --- | --- |
| `markdownTableRows()` | Extract all table rows |
| `parseAllMarkdownTables()` | Parse all tables in a document, return array of structured objects |
| `splitMarkdownRow()` | Split row cells (handles escaped pipe characters and code blocks) |
| `tableAfterHeading()` | Locate the table after a specific heading |
| `getCell()` | Get a cell by column name (supports multiple aliases) |
| `splitList()` | Split comma/semicolon/plus-separated lists |
| `splitDependencies()` | Split dependencies, filtering out `none/n/a` placeholders |
**Pipe characters inside code blocks**: `splitMarkdownRow()` tracks code block state —
`|` inside code blocks is not treated as a column separator and the original content is preserved.
---
## Level 2 — Design decisions
### Why Dashboard is plain HTML + vanilla JS, not React/Vite
harness is distributed via `npx`. Introducing React/Vite would mean users pull in large
build dependencies on every run, breaking the zero-dependency portability. Static HTML can
be opened directly from `file://` and shared as a CI evidence snapshot without any runtime.
The vanilla JS components in app-src (`DashboardShell`, `SidebarNav`, `TableView`, etc.)
are concatenated in order via manifest. Each file is < 600 lines, git-diff readable,
no webpack/esbuild needed.
### Why the static Dashboard is read-only
The static Dashboard's role is "shareable evidence snapshot" — it can be generated by CI,
opened offline, and sent to external reviewers. In these scenarios, write operations have
no security boundary (no CSRF/Origin/Host validation). Write operations can only be
executed in Workbench mode, because the Workbench server binds to `127.0.0.1` and has
a complete security validation chain.
### Why `harness dev` and `harness dashboard` are two separate commands
`harness dashboard` generates a static read-only snapshot (suitable for CI, migration
reports, offline evidence). `harness dev` starts a local dynamic Workbench server with
file watching, auto-refresh, and review-confirm write operations. The boundary is:
**static snapshots can be shared; the dynamic Workbench is local-only**.
### Why file watching uses polling instead of fs.watch
`fs.watch` has known missed-event issues on macOS for deep directory trees, and harness's
docs directory structure is a multi-level nested Markdown file tree. The polling approach
is simple to implement, has predictable behavior, and doesn't introduce third-party
dependencies like chokidar (consistent with the zero-dependency principle).
1-second polling + 250ms debounce is sufficient for human editing scenarios.
### Why not introduce SQLite or a JSON database
Introducing JSON/SQLite without a clear authority boundary would create drift between
Markdown, JSON, and SQLite as three separate facts. Git review is friendly to Markdown/JSON
diffs but not to SQLite diffs. Current scale is "hundreds of tasks" — generated JSON +
indexed in-memory filtering is sufficient.
The decision: Markdown is the single source of truth, generated JSON index is a
regenerable cache, and SQLite is only considered when task count and query complexity
exceed what JSON can handle — and even then, only as a regenerable query cache, never
hand-written, never as an authoritative source of truth.
# 06 — Preset System and Migration Engine
## Level 0 — Two independent subsystems
This document covers two related but independent subsystems:
```mermaid
flowchart LR
A["Preset system\nReusable task method packages\n(injected at new-task time)"]
B["Migration engine\nOnboarding legacy projects to harness\n(migrate-* commands)"]
A -->|"legacy-migration Preset\nis the migration engine's dedicated Preset"| B
```
Their relationship: the migration engine has a dedicated Preset (`legacy-migration`), but
the Preset system itself is general-purpose — any type of task can have its own Preset.
---
## Part 1 — Preset System
### Level 1 — What is a Preset
A Preset is a **reusable task method package** that bundles everything a specific type of
task needs:
- Template append content (appended on top of standard templates)
- Execution scripts (run during plan / scaffold phases)
- Check scripts (validate Preset compliance)
- Resource declarations (reference files, artifacts, required reads)
```mermaid
flowchart TD
Preset["Preset package\npresets/<id>/"]
Preset --> Manifest["preset.yaml\nPackage manifest (required)"]
Preset --> Templates["templates/\nTemplate append files"]
Preset --> Scripts["scripts/\nPlan / scaffold scripts"]
Preset --> Checks["checks/\nCheck scripts"]
Preset --> Workbench["workbench/\nDashboard panel definitions (optional)"]
```
### Level 1 — Layered discovery: three search layers
Presets are searched in **project → user → builtin** order, with **first-match-wins** for
same-named Presets:
```mermaid
flowchart LR
P["project layer\n<target>/.coding-agent-harness/presets/\n(highest priority)"]
U["user layer\n~/.coding-agent-harness/presets/\n(second priority)"]
B["builtin layer\npackage/presets/\n(lowest priority)"]
P -->|"continue if not found"| U -->|"continue if not found"| B
```
This design allows:
- **Project-level override**: put a same-named Preset in the project to override the builtin version
- **User-level override**: put a Preset in the home directory to apply to all projects
- **Builtin fallback**: Presets bundled with the package serve as the default implementation
`harness preset install --project <preset-id>` installs a Preset to the project layer;
`harness preset uninstall --project <preset-id>` removes it from the project layer
(doesn't affect user and builtin layers).
### Level 2 — preset.yaml structure
```mermaid
flowchart TD
YAML["preset.yaml"]
YAML --> Meta["Basic info\nid / version / purpose\ncompatibleBudgets / localeSupport"]
YAML --> Task["task config\nkind / requiresFromSession\nprojectLevelOnly"]
YAML --> EP["entrypoints\n(see below)"]
YAML --> WS["writeScopes\nRestrict write paths (security boundary)"]
YAML --> Resources["resources\nreferences / artifacts / context.requiredReads"]
YAML --> Audit["audit\nmanifestRequired: true\nevidenceFiles[]"]
```
### Level 3 — Entrypoint type system
Each entrypoint has two dimensions: **name** (when it triggers) and **type** (how it executes):
```mermaid
flowchart LR
subgraph "Name (when it triggers)"
N1["newTask\nWhen harness new-task --preset X is run"]
N2["plan\nMigration plan phase"]
N3["scaffold\nBulk completion phase"]
N4["check\nValidation phase"]
end
subgraph "Type (how it executes)"
T1["template\nAppend template content to task files"]
T2["script\nRun a Node.js script"]
T3["check\nRun a check script"]
end
```
Differences between the three execution types:
- **template**: Renders a template file and appends the result to task files
(`task_plan.md`, `execution_strategy.md`, etc.)
- **script**: Executes a Node.js script that can read/write the filesystem;
returns results as audit evidence
- **check**: Executes a check script that validates Preset application completeness;
blocks task creation on failure
Each entrypoint also declares `writes` (allowed write path globs) and `reads`
(allowed read path globs).
### Level 3 — writeScopes security boundary
`writeScopes` is a path allowlist that restricts Presets to only writing to declared directories.
`assertPresetWriteScope()` checks whether the relative path matches any scope on every file write.
```
writeScopes:
tasks:
path: docs/09-PLANNING/TASKS/**
access: write
```
Supports `path/**` wildcards for a directory and all its subdirectories.
Relative paths must be normalized (no `../`, no absolute paths) to prevent path traversal attacks.
### Level 2 — Preset lifecycle
```mermaid
sequenceDiagram
participant User
participant CLI as harness CLI
participant Registry as preset-registry.mjs
participant Engine as preset-engine.mjs
participant TaskDir as Task directory
User->>CLI: harness new-task my-task --preset legacy-migration --from-session session.json
CLI->>Registry: listPresetPackages()\n(layered discovery, first-match-wins)
Registry-->>CLI: Preset object (with manifest + path)
CLI->>TaskDir: Create standard task scaffold (brief / task_plan / visual_map etc.)
CLI->>Engine: executeEntrypoint("newTask", preset, taskDir)
Engine->>TaskDir: Append Preset template content to task files
CLI->>TaskDir: Write Preset metadata to task_plan.md
CLI->>TaskDir: Generate evidence bundle (preset-manifest.json / preset-audit.json)
CLI-->>User: Task creation complete
```
### Level 1 — Currently available Presets
| Preset ID | Purpose | Compatible Budget | Special requirements |
| --- | --- | --- | --- |
| `legacy-migration` | Migrate legacy harness projects to v1.0 | complex | Requires `--from-session session.json` |
| `lesson-sedimentation` | Lesson sedimentation tasks | standard, complex | None |
| `module` | Module parallel work tasks | standard, complex | Requires `--module <module-id>` |
| `standard-task` | Standard task method | standard, complex | None |
---
## Part 2 — Migration Engine
### Level 1 — Three phases of migration
```mermaid
flowchart LR
A["① migrate-plan\nAnalyze gaps\nGenerate action queue"] --> B["② migrate-run\nExecute migration actions\nWrite session record"] --> C["③ migrate-verify\nVerify migration results\nCompare against baseline"]
```
### Level 2 — What migrate-plan does
`buildMigrationPlan()` identifies 6 types of gaps:
```mermaid
flowchart TD
Plan["harness migrate-plan"]
Plan --> Scan["Scan target repo\nbuildStatus (non-strict mode)"]
Scan --> Detect["Detect existing capabilities\nreadCapabilityRegistry()"]
Detect --> Gaps["Identify gaps (6 types)"]
Gaps --> TaskActions["taskActions\nActive tasks missing execution_strategy.md\nor visual_map.md"]
Gaps --> ReviewActions["reviewActions\nMissing review fields\n(Reviewer Identity etc.)"]
Gaps --> LegacyActions["legacyActions\nMissing required reference files"]
Gaps --> LegacyResiduals["legacyResiduals\nContract gaps in historical tasks\n(should not be auto-migrated)"]
Gaps --> WeakBriefs["weakBriefTasks\nbrief.md quality below threshold"]
Gaps --> UnknownClass["unknownClassificationTasks\nTask classification unclear"]
```
**Action queue format**: Each taskAction contains `taskId`, `path`, `files[]`, `commands[]`,
and an `action` description. `commands[]` is a list of harness CLI commands that can be
executed directly.
### Level 2 — Two modes of migrate-verify
```mermaid
flowchart TD
Verify["harness migrate-verify session.json"]
Verify --> Normal["Normal mode\nRe-run checks\nReport failures / warnings"]
Verify --> FullCutover["--full-cutover mode\nStrictest validation (8 conditions)"]
FullCutover --> C1{"session.result == complete?"}
C1 -->|"no"| F1["FAIL"]
C1 -->|"yes"| C2{"No strictDeferred?"}
C2 -->|"no"| F2["FAIL"]
C2 -->|"yes"| C3{"All counters == 0?\n(warnings / taskActions\nreviewSchemaGaps / legacyResiduals etc.)"}
C3 -->|"no"| F3["FAIL (list non-zero counters)"]
C3 -->|"yes"| C4{"fullCutoverEligible == true?"}
C4 -->|"no"| F4["FAIL"]
C4 -->|"yes"| P1["PASS (full cutover ready)"]
```
`--full-cutover` is the final acceptance criteria for migration completion: all 8 conditions
must be satisfied to pass.
---
## Part 3 — Capability Registry
### Level 1 — Capability dependency graph
```mermaid
flowchart TD
Core["core\n(required, foundation for all other capabilities)"]
Core --> ModParallel["module-parallel\nModule registry + parallel work"]
Core --> AdvReview["adversarial-review\nAdversarial review reports\n(alias: review-contract)"]
Core --> LongRunning["long-running-task\nLong-running task contracts"]
Core --> Dashboard["dashboard\nLocal HTML Dashboard"]
Core --> SafeAdoption["safe-adoption\nSmooth onboarding for legacy projects"]
ModParallel --> SubagentWorker["subagent-worker\nCommit-backed worker handoff protocol"]
```
### Level 2 — selectWhen for each capability
| Capability | When to enable |
| --- | --- |
| `core` | Always — this is the required foundation |
| `module-parallel` | When the project has 2+ independent modules needing parallel ownership |
| `subagent-worker` | When code-change subagents need to work in isolated worktrees and commit handoffs |
| `adversarial-review` | When release, architecture, security, data, or policy risks require independent review artifacts |
| `long-running-task` | When Agents may run across multiple loops without per-step user confirmation |
| `dashboard` | When local read-only state visualization is needed |
| `safe-adoption` | When onboarding v1.0 into an existing harness project without rewriting history |
### Level 2 — Preset resource declarations (resources)
Presets can declare three types of resources. These are auto-generated at task creation
time and validated by the checker:
| Resource type | Meaning | Validation |
| --- | --- | --- |
| `resources.references` | Reference files provided by the Preset | File exists + indexed in `references/INDEX.md` + required reads declared in `task_plan.md` |
| `resources.artifacts` | Artifacts generated by the Preset | File exists + indexed in `artifacts/INDEX.md` |
| `context.requiredReads` | Reference file IDs that must be read | Recorded in both indexes |
This is a **three-layer validation chain**: file exists → indexed in index table →
required reads declared in task_plan.md. Any missing layer produces a failure.
---
## Part 4 — Design decisions
### Why Presets weren't there from the start
The Preset system evolved from the need "migrating legacy projects is too complex". Initially
there was only `new-task --budget complex` with no Preset concept. When users raised the
legacy project migration need, the initial proposal was to invent a new "Ultra" task level.
Research found the problem wasn't that Complex was insufficient — it was "without Presets,
every Agent has to figure out how to structure the migration process from scratch". Three
independent subagent adversarial reviews all pointed to the same conclusion: Preset is the
right abstraction, Ultra is over-engineering.
Reasons for rejecting Ultra:
1. Ultra would introduce a second task system, breaking the consistency of simple/standard/complex
2. The root cause wasn't that Complex couldn't handle it, but that there was no pre-filled scaffold
3. Preset is a general abstraction, not just for migration — any type of task can have its own Preset
### Why Preset manifests use YAML
YAML was chosen for readability — Preset manifests need to be written and reviewed by humans,
and YAML has fewer quotes and brackets than JSON. A lightweight custom parser (`parseSimpleYaml()`)
was written instead of introducing `js-yaml` to maintain zero dependencies. JS format was
ruled out because Presets need to be cross-tool auditable and can't be executable code.
### Why layered discovery (project/user/builtin)
Layered discovery was introduced two days after the Preset system itself. Initially it only
read from the package's fixed `presets/` path. The core problem layered design solves:
- Different projects can have their own private Presets (project layer)
- Users can install Presets shared across projects (user layer)
- Package bundled Presets serve as fallback, usable without installation (builtin layer)
Priority order (project > user > builtin) ensures project-level customization can override
global defaults.
### Why migration is split into three phases instead of one step
The core consideration is "can't auto-migrate" — Presets only scaffold structure, they don't
automatically modify historical docs, auto-stage, or auto-commit. Before actually writing to
the target repo, users still need to confirm the write scope and migration depth.
- `migrate-plan`: read-only analysis, no side effects
- `migrate-run`: write execution, requires user confirmation
- `migrate-verify`: post-hoc verification, confirms migration results
Three separate steps give users a confirmation opportunity at every node with side effects.
### Why full-cutover verification is so strict
`full-cutover` is an irreversible declaration — once migration is declared complete,
subsequent Agents won't treat this project as a migration target anymore. A more lenient
approach (only checking strict pass) was rejected because real-world project validation
(471 tasks) proved that even with a strict pass, weak briefs or legacy-only visual maps
could still remain.
### writeScopes security boundary
writeScopes and the Preset system were introduced simultaneously — it wasn't a security
hardening added later. Presets are third-party installable packages, and without write
scope restrictions, a malicious or buggy Preset could overwrite arbitrary files. The
runtime enforces path checks, rejecting paths starting with `../` and absolute paths
(path traversal protection).
# Coding Agent Harness — Architecture Explainer
This set of documents helps you understand the system architecture of `coding-agent-harness`.
Whether you want to contribute code, integrate it into your own project, or just figure out
"how does this thing actually work" — this is the best place to start.
Each document uses a **top-down, layer-by-layer** approach: it gives you a big picture first
to build an overall mental model, then dives into each module. You can stop at any layer —
you don't need to read every detail.
---
## Why these docs exist
The `coding-agent-harness` codebase itself isn't complex, but its **design intent** isn't
easy to read directly from the code.
For example:
- Why is state stored in Markdown files instead of a database?
- Why are there three check profiles instead of one?
- What's the difference between `governance-sync` and `governance rebuild`, and why are they separate?
- Why must `review-confirm` be a manual operation — why can't it be automated?
The answers to these questions are scattered across design decisions, historical evolution,
and operating standards. These docs bring them together so you can understand the system's
"why" without digging through git log.
---
## Reading order
Reading in order works best, about 15-25 minutes per doc:
| File | Topic | What you'll understand |
| --- | --- | --- |
| [01-system-overview.md](01-system-overview.md) | System overview | What this is, what problem it solves, what the four main blocks do |
| [02-module-dependency.md](02-module-dependency.md) | Code modules | How the CLI dispatches, how the 30+ modules in lib/ are layered, dependency relationships |
| [03-task-lifecycle.md](03-task-lifecycle.md) | Task lifecycle | The full flow of a task from creation to closeout, gates, and the queue system |
| [04-check-and-governance.md](04-check-and-governance.md) | Check system | Three profiles, what each of the 9 validators checks, how governance indexes are rebuilt |
| [05-data-flow.md](05-data-flow.md) | Data flow | How Markdown files become a Dashboard, the boundary between two generation modes |
| [06-preset-and-migration.md](06-preset-and-migration.md) | Preset and migration | Preset package structure and entrypoint type system, three phases of legacy project migration |
---
## Document conventions
Each file uses `Level 0 / 1 / 2 / 3` to mark depth:
- **Level 0**: Highest level, 3-5 big blocks, builds overall understanding (required reading)
- **Level 1**: Expands the blocks, shows sub-modules (recommended)
- **Level 2**: Dives into sub-modules, explains internal logic (read as needed)
- **Level 3**: Most detailed, function-level flows (reference use)
You can stop at Level 1 and go deeper only when you need to.
---
## Quick lookup
If you have a specific question, jump directly to the relevant file:
| I want to know… | Go to |
| --- | --- |
| What problem this system solves | [01 — System overview](01-system-overview.md) |
| What `harness check` validates | [04 — Check system](04-check-and-governance.md) |
| What a task's `review` state means | [03 — Task lifecycle](03-task-lifecycle.md) |
| Where Dashboard data comes from | [05 — Data flow](05-data-flow.md) |
| How to write a Preset | [06 — Preset and migration](06-preset-and-migration.md) |
| What a module in the code does | [02 — Code modules](02-module-dependency.md) |
| How to migrate a legacy project | [06 — Preset and migration](06-preset-and-migration.md) |
# Coding Agent Harness — 架构解释文档
这组文档帮助你理解 `coding-agent-harness` 的系统架构。
无论你是想贡献代码、集成到自己的项目、还是只是想搞清楚"这东西到底怎么工作的",
这里都是最好的起点。
每篇文档采用**自顶向下、逐层展开**的方式:先给你一张大图建立整体感,
再一个模块一个模块地深入。你可以在任意层级停下来——不需要读完所有细节。
---
## 为什么要有这套文档
`coding-agent-harness` 的代码库本身并不复杂,但它的**设计意图**不容易从代码里直接读出来。
比如:
- 为什么状态存在 Markdown 文件里,而不是数据库?
- 为什么有三种 check profile,而不是一种?
- `governance-sync` 和 `governance rebuild` 有什么区别,为什么要分开?
- `review-confirm` 为什么必须是人工操作,不能自动化?
这些问题的答案散落在设计决策、历史演进和操作规范里。
这套文档把它们集中起来,让你不需要翻 git log 就能理解系统的"为什么"。
---
## 阅读顺序
按顺序读效果最好,每篇 15-25 分钟:
| 文件 | 主题 | 你会理解什么 |
| --- | --- | --- |
| [01-system-overview.md](01-system-overview.md) | 系统全景 | 这个东西是什么,解决什么问题,四个大块分别做什么 |
| [02-module-dependency.md](02-module-dependency.md) | 代码模块 | CLI 怎么分发,lib/ 里 30+ 个模块怎么分层,依赖关系 |
| [03-task-lifecycle.md](03-task-lifecycle.md) | 任务生命周期 | 一个任务从创建到收口的完整流转、门禁和队列系统 |
| [04-check-and-governance.md](04-check-and-governance.md) | 检查体系 | 三种 profile,9 个验证器各验什么,治理索引如何重建 |
| [05-data-flow.md](05-data-flow.md) | 数据流 | Markdown 文件如何变成 Dashboard,两种生成模式的边界 |
| [06-preset-and-migration.md](06-preset-and-migration.md) | Preset 与迁移 | Preset 包结构和 entrypoint 类型系统,旧项目迁移三阶段 |
---
## 文档约定
每个文件用 `Level 0 / 1 / 2 / 3` 标注层级深度:
- **Level 0**:最高层,3-5 个大块,建立整体感(必读)
- **Level 1**:展开大块,看清子模块(推荐读)
- **Level 2**:深入子模块,理解内部逻辑(按需读)
- **Level 3**:最细节,函数级别的流程(查阅用)
可以只读到 Level 1 就停,有需要再往下看。
---
## 快速定位
如果你有具体问题,直接跳到对应文件:
| 我想知道… | 去哪里 |
| --- | --- |
| 这个系统解决什么问题 | [01 — 系统全景](01-system-overview.md) |
| `harness check` 在验什么 | [04 — 检查体系](04-check-and-governance.md) |
| 任务的 `review` 状态是什么意思 | [03 — 任务生命周期](03-task-lifecycle.md) |
| Dashboard 数据从哪来 | [05 — 数据流](05-data-flow.md) |
| Preset 怎么写 | [06 — Preset 与迁移](06-preset-and-migration.md) |
| 代码里某个模块是干什么的 | [02 — 代码模块](02-module-dependency.md) |
| 旧项目怎么迁移进来 | [06 — Preset 与迁移](06-preset-and-migration.md) |
# Demo task - Task Package Index
Task Contract: harness-task/v1
## Task Identity
| Field | Value |
| --- | --- |
| Task ID | `demo-task` |
| Budget | `standard` |
| Preset | `none` |
| Module | `n/a` |
| Long-running | `no` |
| Created | 2026-05-25 |
## Task Audit Metadata
| Field | Value |
| --- | --- |
| Created By | historical-backfill |
| Created At | 2026-05-25 |
| Command Shape | n/a |
| Budget | standard |
| Template Source | examples/minimal-project historical fixture |
| Task Creator | n/a |
| Task Creator Source | legacy-unavailable |
| Human Review Status | not-confirmed |
| Confirmation ID | n/a |
| Confirmed At | n/a |
| Reviewer | n/a |
| Reviewer Email | n/a |
| Confirm Text | n/a |
| Evidence Checked | n/a |
| Review Commit SHA | n/a |
| Audit Source | migrated-legacy-scaffold |
| Audit Status | migrated |
| Exception Reason | Existing demo task predates scaffold provenance enforcement. |
| Message | n/a |
| Migration Status | migrated |
| Migrated From | brief.md#Scaffold Provenance |
| Legacy Extra Fields | {} |
| Migration Notes | example fixture backfilled to INDEX audit metadata |
## Core Contract Files
| File | Purpose |
| --- | --- |
| `brief.md` | Human-readable task summary and context entry. |
| `task_plan.md` | Current task goal, scope, selected budget, acceptance, and operating decisions. |
| `visual_map.md` | Phase map, evidence status, next lifecycle commands, and supporting diagrams. |
| `progress.md` | Execution log, verification evidence, decisions, and handoff notes. |
## Standard Task Files
| File | Purpose |
| --- | --- |
| `execution_strategy.md` | Execution mode, ownership, conflict control, and evidence strategy. |
| `findings.md` | Findings, research notes, accepted risks, and unresolved questions. |
| `lesson_candidates.md` | Task-local lesson candidate decisions before closeout. |
| `review.md` | Agent review submission, adversarial review, findings, evidence, and routing. |
# Additional Permission for Generated Harness Materials
Copyright (c) 2026 ZeyuLi (FairladyZ)
Coding Agent Harness is licensed under the GNU Affero General Public License
version 3 or any later version (`AGPL-3.0-or-later`). This file grants an
additional permission under section 7 of the AGPL.
## Generated Harness Materials
For the purposes of this permission, "Generated Harness Materials" means files
created, copied, rendered, scaffolded, or installed into a target project by
Coding Agent Harness from bundled or user-provided templates, presets, skills,
reference packs, examples, or documentation skeletons. This includes generated
or installed project governance files such as `AGENTS.md`, `CLAUDE.md`, task
plans, review files, ledgers, walkthroughs, generated dashboard data, and other
target-project harness documents.
## Permission
You may use, copy, modify, merge, publish, distribute, sublicense, and/or sell
Generated Harness Materials under the license terms of the target project or
under any other terms chosen by that target project's copyright holder.
The AGPL does not apply to a target project, to that target project's source
code, or to Generated Harness Materials solely because Coding Agent Harness was
used to create, copy, render, scaffold, install, or update those materials.
## Limits
This additional permission does not apply to Coding Agent Harness itself,
including its CLI/runtime source code, dashboard implementation, package source,
or the bundled templates, presets, skills, reference packs, examples, and
documentation as distributed in this repository or package.
This permission does not grant trademark rights or remove any third-party
license obligations that may apply independently.
export const allowedPhaseKinds = new Set(["init", "execution", "gate"]);
export const allowedPhaseActors = new Set(["agent", "human", "coordinator"]);
export function normalizePhaseKind(value) {
const normalized = String(value || "")
.replace(/`/g, "")
.trim()
.toLowerCase()
.replaceAll("_", "-");
if (!normalized) return "execution";
if (normalized === "exec" || normalized === "implementation") return "execution";
if (normalized === "prep" || normalized === "discussion") return "init";
if (normalized === "review" || normalized === "closeout") return "gate";
return normalized;
}
export function normalizePhaseActor(value) {
const normalized = String(value || "")
.replace(/`/g, "")
.trim()
.toLowerCase()
.replaceAll("_", "-");
return normalized || "agent";
}
export function isExecutionPhase(phase) {
return normalizePhaseKind(phase?.kind) === "execution";
}
export function nonSkippedPhases(phases = []) {
return phases.filter((phase) => phase.state !== "skipped");
}
export function implementationPhases(phases = []) {
return nonSkippedPhases(phases).filter(isExecutionPhase);
}
export function phaseCompletionAverage(phases = []) {
const scored = implementationPhases(phases);
if (scored.length === 0) return 0;
return Math.round(scored.reduce((sum, phase) => sum + phase.completion, 0) / scored.length);
}
export function phaseHasRecordedProgress(phase) {
return (
phase.completion > 0 ||
["in_progress", "review", "blocked", "done"].includes(String(phase.state || "").toLowerCase()) ||
["partial", "present", "waived"].includes(String(phase.evidenceStatus || "").toLowerCase())
);
}
import path from "node:path";
import { normalizeTarget, toPosix } from "./core-shared.mjs";
import { capabilityDefinitions, readCapabilityRegistry } from "./capability-registry.mjs";
import { summarizeGitState } from "./git-status-summary.mjs";
import { collectTasks, taskCutoverCounters } from "./task-scanner.mjs";
export function buildStatusData(targetInput, options = {}) {
const target = targetInput?.projectRoot ? targetInput : normalizeTarget(targetInput);
const validationMode = options.validationMode || "data-only";
const gitState = options.gitState || summarizeGitState(target);
const registry = options.capabilityState?.registry || readCapabilityRegistry(target);
const detected = options.capabilityState?.detected || [];
const capabilityWarnings = options.capabilityState?.warnings || [];
const failures = [...(options.failures || [])];
const warnings = [...(options.warnings || [])];
const legacy = options.legacy || { status: "skipped", code: 0, stdout: "", stderr: "" };
const tasks = options.tasks || collectTasks(target, {
requireGeneratedScaffoldProvenance: options.requireGeneratedScaffoldProvenance === true,
taskPlanPaths: options.taskPlanPaths,
closeoutContent: options.closeoutContent,
});
const briefReady = tasks.filter((task) => task.briefSource === "standalone").length;
const briefMissing = tasks.length - briefReady;
const capabilityNames = new Map(registry.capabilities.map((capability) => [capability.name, capability]));
for (const capability of detected) {
if (!capabilityNames.has(capability)) capabilityNames.set(capability, { name: capability, state: "configured" });
}
const cutoverCounters = taskCutoverCounters(tasks);
const fullCutoverEligible =
validationMode === "validated" &&
failures.length === 0 &&
warnings.length === 0 &&
cutoverCounters.legacyVisualOnlyCount === 0 &&
cutoverCounters.unknownClassificationCount === 0 &&
cutoverCounters.weakBriefCount === 0 &&
cutoverCounters.missingCanonicalVisualMapCount === 0;
return {
project: {
name: path.basename(target.projectRoot),
root: `TARGET:${target.docsOnly ? toPosix(path.relative(target.projectRoot, target.docsRoot)) : "."}`,
docsOnly: target.docsOnly,
},
schemaVersion: 2,
generatedAt: options.generatedAt || new Date().toISOString(),
mode: registry.mode,
checkState: {
status: failures.length > 0 ? "fail" : warnings.length > 0 ? "warn" : "pass",
validationMode,
failures: failures.length,
warnings: warnings.length,
details: { failures, warnings },
legacy,
},
git: gitState.summary,
summary: {
tasks: tasks.length,
briefCoverage: {
ready: briefReady,
missing: briefMissing,
total: tasks.length,
},
visualMapCoverage: {
canonical: tasks.filter((task) => task.visualMapSource === "canonical").length,
legacyOnly: cutoverCounters.legacyVisualOnlyCount,
missing: tasks.filter((task) => task.visualMapStatus === "missing").length,
total: tasks.length,
},
fullCutoverEligible,
legacyVisualOnlyCount: cutoverCounters.legacyVisualOnlyCount,
unknownClassificationCount: cutoverCounters.unknownClassificationCount,
weakBriefCount: cutoverCounters.weakBriefCount,
visualMapRequiredCount: cutoverCounters.visualMapRequiredCount,
missingCanonicalVisualMapCount: cutoverCounters.missingCanonicalVisualMapCount,
},
capabilities: [...capabilityNames.values()].map((capability) => ({
name: capability.name,
state: capability.state || "configured",
dependencyStatus: capabilityDefinitions[capability.name]?.dependencies.every((dependency) => capabilityNames.has(dependency))
? "valid"
: "invalid",
warnings: capabilityWarnings.filter((warning) => warning.includes(capability.name)),
})),
tasks,
handoffs: tasks.flatMap((task) => task.handoffs || []),
recentActivity: tasks.slice(0, 8).map((task) => ({ at: new Date().toISOString(), type: "task", summary: task.title })),
};
}
import { spawnSync } from "node:child_process";
import path from "node:path";
import { toPosix } from "./core-shared.mjs";
import { firstColumn, markdownTableRows } from "./markdown-utils.mjs";
export const taskAuditHeadingPattern = /^##\s*(?:Task Audit Metadata|任务审计元数据)\s*$/im;
export const scaffoldProvenanceHeadingPattern = /^##\s*(?:Scaffold Provenance|脚手架来源)\s*$/im;
export const humanReviewConfirmationHeadingPattern = /^##\s*(?:Human Review Confirmation|人工审查确认)\s*$/im;
export const taskAuditFieldOrder = [
"Created By",
"Created At",
"Command Shape",
"Budget",
"Template Source",
"Task Creator",
"Task Creator Source",
"Human Review Status",
"Confirmation ID",
"Confirmed At",
"Reviewer",
"Reviewer Email",
"Confirm Text",
"Evidence Checked",
"Review Commit SHA",
"Audit Source",
"Audit Status",
"Exception Reason",
"Message",
"Migration Status",
"Migrated From",
"Legacy Extra Fields",
"Migration Notes",
];
export function readGitIdentity(projectRoot) {
const gitRoot = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: projectRoot, encoding: "utf8" });
if (gitRoot.status !== 0) {
return { name: "n/a", email: "n/a", display: "n/a", source: "git-unavailable" };
}
const name = spawnSync("git", ["config", "--get", "user.name"], { cwd: projectRoot, encoding: "utf8" }).stdout.trim();
const email = spawnSync("git", ["config", "--get", "user.email"], { cwd: projectRoot, encoding: "utf8" }).stdout.trim();
if (!name && !email) return { name: "n/a", email: "n/a", display: "n/a", source: "git-config-missing" };
const display = name && email ? `${name} <${email}>` : name || email;
return { name: name || "n/a", email: email || "n/a", display, source: "git-config" };
}
export function buildCreationTaskAudit(scaffoldProvenance, { projectRoot }) {
const creator = readGitIdentity(projectRoot);
return {
"Created By": scaffoldProvenance.createdBy || "harness new-task",
"Created At": scaffoldProvenance.createdAt || "",
"Command Shape": scaffoldProvenance.command || "",
"Budget": scaffoldProvenance.budget || "",
"Template Source": scaffoldProvenance.templateSource || "",
"Task Creator": creator.display,
"Task Creator Source": creator.source,
"Human Review Status": "not-confirmed",
"Confirmation ID": "n/a",
"Confirmed At": "n/a",
"Reviewer": "n/a",
"Reviewer Email": "n/a",
"Confirm Text": "n/a",
"Evidence Checked": "n/a",
"Review Commit SHA": "n/a",
"Audit Source": "native-index",
"Audit Status": "created",
"Exception Reason": scaffoldProvenance.exceptionReason || "n/a",
"Message": "n/a",
"Migration Status": "native",
"Migrated From": "n/a",
"Legacy Extra Fields": "{}",
"Migration Notes": "n/a",
};
}
export function taskAuditTemplateValues(fields = {}) {
const values = {};
for (const field of taskAuditFieldOrder) {
const key = `TASK_AUDIT_${field.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_|_$/g, "")}`;
values[key] = markdownCell(fields[field] || "n/a");
}
return values;
}
export function renderTaskAuditMetadata(fields = {}, { locale = "en-US" } = {}) {
const heading = locale === "zh-CN" ? "## 任务审计元数据" : "## Task Audit Metadata";
const rows = taskAuditFieldOrder.map((field) => `| ${field} | ${markdownCell(fields[field] ?? "n/a")} |`);
return `${heading}\n\n| Field | Value |\n| --- | --- |\n${rows.join("\n")}\n`;
}
export function replaceTaskAuditMetadata(content, fields, options = {}) {
const rendered = renderTaskAuditMetadata(fields, options).trimEnd();
const text = String(content || "").trimEnd();
const match = findHeadingBlock(text, taskAuditHeadingPattern);
if (match) return `${text.slice(0, match.start)}${rendered}\n${text.slice(match.end).replace(/^\n+/, "\n")}`;
const identityMatch = findHeadingBlock(text, /^##\s*(?:Task Identity|任务身份)\s*$/im);
if (identityMatch) {
return `${text.slice(0, identityMatch.end).trimEnd()}\n\n${rendered}\n\n${text.slice(identityMatch.end).trimStart()}`;
}
return `${text}\n\n${rendered}\n`;
}
export function parseTaskAuditMetadata(content, { required = false } = {}) {
const block = extractTaskAuditBlock(content);
const fields = block ? fieldsFromMarkdownBlock(block.body) : new Map();
const issues = [];
if (required && fields.size === 0) issues.push({ code: "missing-task-audit-metadata", message: "missing Task Audit Metadata section" });
if (fields.size > 0) {
for (const field of ["Created By", "Created At", "Budget", "Template Source", "Task Creator Source", "Human Review Status", "Audit Status"]) {
if (!isConcreteAuditField(fields.get(field.toLowerCase()))) issues.push({ code: `missing-task-audit-${slugField(field)}`, message: `Task Audit Metadata missing ${field}` });
}
const createdBy = normalizeToken(fields.get("created by"));
const createdAt = fields.get("created at") || "";
const budget = normalizeToken(fields.get("budget"));
const creatorSource = normalizeToken(fields.get("task creator source"));
const reviewStatus = normalizeToken(fields.get("human review status"));
const auditStatus = normalizeToken(fields.get("audit status"));
if (isConcreteAuditField(fields.get("created by")) && !["harness-new-task", "manual-exception", "historical-backfill"].includes(createdBy)) {
issues.push({ code: "invalid-task-audit-created-by", message: `Task Audit Metadata invalid Created By: ${fields.get("created by")}` });
}
if (createdBy === "manual-exception" && !isConcreteAuditField(fields.get("exception reason"))) {
issues.push({ code: "missing-task-audit-exception-reason", message: "Task Audit Metadata manual-exception requires Exception Reason" });
}
if (isConcreteAuditField(createdAt) && normalizeToken(createdAt) !== "legacy-unavailable" && !isValidDateOnly(createdAt)) {
issues.push({ code: "invalid-task-audit-created-at", message: `Task Audit Metadata invalid Created At: ${createdAt}` });
}
if (isConcreteAuditField(fields.get("budget")) && !["simple", "standard", "complex", "legacy-unavailable"].includes(budget)) {
issues.push({ code: "invalid-task-audit-budget", message: `Task Audit Metadata invalid Budget: ${fields.get("budget")}` });
}
if (isConcreteAuditField(fields.get("task creator source")) && !["git-config", "git-config-missing", "git-unavailable", "legacy-unavailable"].includes(creatorSource)) {
issues.push({ code: "invalid-task-audit-task-creator-source", message: `Task Audit Metadata invalid Task Creator Source: ${fields.get("task creator source")}` });
}
if (isConcreteAuditField(fields.get("human review status")) && !["not-confirmed", "confirmed"].includes(reviewStatus)) {
issues.push({ code: "invalid-task-audit-human-review-status", message: `Task Audit Metadata invalid Human Review Status: ${fields.get("human review status")}` });
}
if (isConcreteAuditField(fields.get("audit status")) && !["created", "committed", "migrated"].includes(auditStatus)) {
issues.push({ code: "invalid-task-audit-audit-status", message: `Task Audit Metadata invalid Audit Status: ${fields.get("audit status")}` });
}
if (reviewStatus === "confirmed") {
for (const field of ["Confirmation ID", "Confirmed At", "Reviewer", "Reviewer Email", "Confirm Text", "Evidence Checked", "Review Commit SHA"]) {
if (!isConcreteAuditField(fields.get(field.toLowerCase()))) issues.push({ code: `missing-task-audit-${slugField(field)}`, message: `Task Audit Metadata confirmed review missing ${field}` });
}
}
}
return {
present: fields.size > 0,
fields,
issues,
summary: taskAuditSummary(fields, issues),
};
}
export function legacyAuditIssues(target, taskDir, { briefContent = "", reviewContent = "" } = {}) {
const issues = [];
const relativeDir = toPosix(path.relative(target.projectRoot, taskDir));
if (scaffoldProvenanceHeadingPattern.test(briefContent)) {
issues.push(legacyIssue(`${relativeDir}/brief.md`, "legacy-scaffold-provenance", "legacy Scaffold Provenance must be migrated to INDEX.md"));
}
if (humanReviewConfirmationHeadingPattern.test(reviewContent)) {
issues.push(legacyIssue(`${relativeDir}/review.md`, "legacy-human-review-confirmation", "legacy Human Review Confirmation must be migrated to INDEX.md"));
}
return issues;
}
export function taskAuditMaterialIssues(target, taskDir, audit) {
const relativeIndexPath = `${toPosix(path.relative(target.projectRoot, taskDir))}/INDEX.md`;
return (audit.issues || []).map((issue) => ({
code: issue.code,
severity: "P1",
queue: "missing-materials",
sourcePath: `TARGET:${relativeIndexPath}`,
sourceLine: 0,
owner: "agent",
message: issue.message,
allowedWritePaths: [relativeIndexPath],
forbiddenActions: ["human-confirm", "edit-unrelated-task", "fabricate-evidence"],
validationCommands: ["node scripts/harness.mjs check --profile target-project <target>"],
confidence: "exact",
repairable: true,
}));
}
export function scaffoldProvenanceSummaryFromTaskAudit(audit) {
const fields = audit?.fields || new Map();
return {
required: true,
present: Boolean(audit?.present),
createdBy: normalizeToken(fields.get("created by")),
command: fields.get("command shape") || "",
createdAt: fields.get("created at") || "",
budget: normalizeToken(fields.get("budget")),
templateSource: fields.get("template source") || "",
exceptionReason: fields.get("exception reason") || "",
issues: audit?.issues || [],
};
}
export function reviewConfirmationFromTaskAudit(audit, { taskKey = "" } = {}) {
const fields = audit?.fields || new Map();
if (!audit?.present) return null;
const status = normalizeToken(fields.get("human review status"));
if (status !== "confirmed") return { confirmed: false, missingFields: [] };
const required = ["Confirmation ID", "Confirmed At", "Reviewer", "Reviewer Email", "Confirm Text", "Evidence Checked", "Review Commit SHA", "Audit Status"];
const missing = required.filter((field) => !isConcreteAuditField(fields.get(field.toLowerCase())));
const confirmText = fields.get("confirm text") || "";
const commitSha = fields.get("review commit sha") || "";
const auditStatus = fields.get("audit status") || "";
const auditSource = fields.get("audit source") || "";
const migratedLegacy = auditSource === "migrated-legacy-review";
const confirmTextMismatch = Boolean(!migratedLegacy && taskKey && isConcreteAuditField(confirmText) && !taskKeysMatch(confirmText, taskKey));
const commitShaInvalid = Boolean(!migratedLegacy && isConcreteAuditField(commitSha) && !/^[0-9a-f]{7,40}$/i.test(commitSha));
const auditStatusInvalid = Boolean(isConcreteAuditField(auditStatus) && auditStatus.trim().toLowerCase() !== "committed");
const invalidFields = [
...(confirmTextMismatch ? ["Confirm Text match"] : []),
...(commitShaInvalid ? ["Review Commit SHA valid"] : []),
...(auditStatusInvalid ? ["Audit Status committed"] : []),
];
return {
confirmed: missing.length === 0 && invalidFields.length === 0,
missingFields: [...missing, ...invalidFields],
confirmationId: fields.get("confirmation id") || "",
confirmedAt: fields.get("confirmed at") || "",
reviewer: fields.get("reviewer") || "",
reviewerEmail: fields.get("reviewer email") || "",
taskKey,
taskKeyMismatch: false,
confirmText,
confirmTextMismatch,
evidenceChecked: fields.get("evidence checked") || "",
commitSha,
commitShaInvalid,
auditStatus,
auditStatusInvalid,
auditSource,
migratedFrom: fields.get("migrated from") || "",
gitAudit: migratedLegacy ? { valid: true, migrated: true } : null,
gitAuditInvalid: false,
};
}
export function stripLegacyAuditBlocks(content) {
return stripHeadingBlock(stripHeadingBlock(content, scaffoldProvenanceHeadingPattern), humanReviewConfirmationHeadingPattern);
}
export function extractLegacyBlock(content, pattern) {
const block = findHeadingBlock(content, pattern);
if (!block) return null;
return {
...block,
body: String(content || "").slice(block.headingEnd, block.end),
raw: String(content || "").slice(block.start, block.end),
};
}
export function fieldsFromMarkdownBlock(block) {
const fields = new Map();
const tableRows = markdownTableRows(block);
const header = tableRows[0] || [];
const fieldIndex = firstColumn(header, ["Field", "字段"]);
const valueIndex = firstColumn(header, ["Value", "值"]);
if (fieldIndex >= 0 && valueIndex >= 0) {
for (const row of tableRows.slice(1).filter((candidate) => !candidate.every((cell) => /^:?-{3,}:?$/.test(cell)))) {
const key = String(row[fieldIndex] || "").replace(/`/g, "").trim();
if (key) fields.set(key.toLowerCase(), String(row[valueIndex] || "").replace(/`/g, "").trim());
}
}
for (const line of String(block || "").split(/\r?\n/)) {
if (line.trim().startsWith("|")) continue;
const match = line.match(/^\s*(?:[-*]\s*)?([^::|]+?)\s*[::]\s*(.+?)\s*$/);
if (match) fields.set(match[1].trim().toLowerCase(), match[2].trim());
}
return fields;
}
export function isConcreteAuditField(value) {
const raw = String(value || "").replace(/`/g, "").trim();
return Boolean(raw) && !/^(n\/a|na|none|pending(?:[-_ ].*)?|todo|tbd|\[.*\]|-|—|–|不适用|无|待定|\{\})$/i.test(raw) && !/\{\{[^}]+\}\}/.test(raw);
}
export function legacyExtraFieldsJson(entries) {
const extra = {};
for (const [field, value] of entries) {
if (!field || !isConcreteAuditField(value)) continue;
extra[field] = value;
}
return JSON.stringify(extra);
}
function extractTaskAuditBlock(content) {
const block = findHeadingBlock(content, taskAuditHeadingPattern);
if (!block) return null;
return { ...block, body: String(content || "").slice(block.headingEnd, block.end) };
}
function findHeadingBlock(content, pattern) {
const text = String(content || "");
const match = text.match(pattern);
if (!match || match.index === undefined) return null;
const headingEnd = text.indexOf("\n", match.index);
const bodyStart = headingEnd < 0 ? text.length : headingEnd + 1;
const next = text.slice(bodyStart).search(/^##\s+/m);
const end = next < 0 ? text.length : bodyStart + next;
return { start: match.index, headingEnd: bodyStart, end };
}
function stripHeadingBlock(content, pattern) {
const text = String(content || "");
const block = findHeadingBlock(text, pattern);
if (!block) return text;
return `${text.slice(0, block.start).trimEnd()}\n\n${text.slice(block.end).trimStart()}`.replace(/\n{3,}/g, "\n\n");
}
function taskAuditSummary(fields, issues) {
return {
present: fields.size > 0,
createdBy: normalizeToken(fields.get("created by")),
command: fields.get("command shape") || "",
createdAt: fields.get("created at") || "",
budget: normalizeToken(fields.get("budget")),
templateSource: fields.get("template source") || "",
taskCreator: fields.get("task creator") || "",
taskCreatorSource: fields.get("task creator source") || "",
humanReviewStatus: normalizeToken(fields.get("human review status")),
confirmationId: fields.get("confirmation id") || "",
confirmedAt: fields.get("confirmed at") || "",
reviewer: fields.get("reviewer") || "",
reviewerEmail: fields.get("reviewer email") || "",
confirmText: fields.get("confirm text") || "",
evidenceChecked: fields.get("evidence checked") || "",
reviewCommitSha: fields.get("review commit sha") || "",
auditSource: fields.get("audit source") || "",
auditStatus: normalizeToken(fields.get("audit status")),
exceptionReason: fields.get("exception reason") || "",
message: fields.get("message") || "",
migrationStatus: normalizeToken(fields.get("migration status")),
migratedFrom: fields.get("migrated from") || "",
legacyExtraFields: fields.get("legacy extra fields") || "{}",
migrationNotes: fields.get("migration notes") || "",
issues,
};
}
function legacyIssue(relativePath, code, message) {
return {
code,
severity: "P1",
queue: "missing-materials",
sourcePath: `TARGET:${relativePath}`,
sourceLine: 0,
owner: "agent",
message,
allowedWritePaths: [relativePath],
forbiddenActions: ["human-confirm", "edit-unrelated-task", "fabricate-evidence"],
validationCommands: ["node scripts/harness.mjs migrate-task-audit-index --apply <target>"],
confidence: "exact",
repairable: true,
};
}
function markdownCell(value) {
return String(value ?? "").replace(/\r?\n/g, "<br>").replace(/\|/g, "\\|");
}
function normalizeToken(value) {
return String(value || "").replace(/`/g, "").trim().toLowerCase().replaceAll("_", "-").replace(/\s+/g, "-");
}
function slugField(value) {
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}
function taskKeysMatch(candidate, expected) {
const left = String(candidate || "").replace(/`/g, "").trim();
const right = String(expected || "").replace(/`/g, "").trim();
return left === right || right.endsWith(`/${left}`);
}
function isValidDateOnly(value) {
const raw = String(value || "").trim();
const match = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) return false;
const date = new Date(`${raw}T00:00:00.000Z`);
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === raw;
}
import fs from "node:fs";
import path from "node:path";
import { normalizeTarget, readFileSafe, toPosix } from "./core-shared.mjs";
import { listTaskPlanPaths, taskIdForDirectory } from "./task-scanner.mjs";
import {
extractLegacyBlock,
fieldsFromMarkdownBlock,
humanReviewConfirmationHeadingPattern,
isConcreteAuditField,
legacyExtraFieldsJson,
parseTaskAuditMetadata,
replaceTaskAuditMetadata,
scaffoldProvenanceHeadingPattern,
stripLegacyAuditBlocks,
taskAuditFieldOrder,
} from "./task-audit-metadata.mjs";
import { firstColumn, markdownTableRows } from "./markdown-utils.mjs";
const scaffoldFieldMap = new Map([
["created by", "Created By"],
["command", "Command Shape"],
["command shape", "Command Shape"],
["created at", "Created At"],
["budget", "Budget"],
["template source", "Template Source"],
["exception reason", "Exception Reason"],
]);
const reviewFieldMap = new Map([
["confirmation id", "Confirmation ID"],
["confirmed at", "Confirmed At"],
["reviewer", "Reviewer"],
["reviewer email", "Reviewer Email"],
["confirm text", "Confirm Text"],
["evidence", "Evidence Checked"],
["evidence checked", "Evidence Checked"],
["commit sha", "Review Commit SHA"],
["review commit sha", "Review Commit SHA"],
["audit status", "Audit Status"],
["message", "Message"],
]);
const requiredScaffold = ["Created By", "Created At", "Command Shape", "Budget", "Template Source"];
const requiredReview = ["Confirmation ID", "Confirmed At", "Reviewer", "Reviewer Email", "Confirm Text", "Evidence Checked", "Review Commit SHA", "Audit Status"];
export function planTaskAuditIndexMigration(targetInput) {
const target = normalizeTarget(targetInput);
const actions = [];
const failures = [];
for (const taskPlanPath of listTaskPlanPaths(target)) {
const taskDir = path.dirname(taskPlanPath);
const taskId = taskIdForDirectory(target, taskDir);
const indexPath = path.join(taskDir, "INDEX.md");
const briefPath = path.join(taskDir, "brief.md");
const reviewPath = path.join(taskDir, "review.md");
const indexContent = readFileSafe(indexPath);
const briefContent = readFileSafe(briefPath);
const reviewContent = readFileSafe(reviewPath);
const scaffoldBlock = extractLegacyBlock(briefContent, scaffoldProvenanceHeadingPattern);
const reviewBlock = extractLegacyBlock(reviewContent, humanReviewConfirmationHeadingPattern);
const audit = parseTaskAuditMetadata(indexContent, { required: true });
if (!scaffoldBlock && !reviewBlock && audit.issues.length === 0) continue;
const parsed = parseLegacyAudit({ taskId, indexContent, scaffoldBlock, reviewBlock });
if (parsed.failures.length) {
failures.push(...parsed.failures.map((failure) => ({ taskId, path: `TARGET:${toPosix(path.relative(target.projectRoot, taskDir))}`, failure })));
continue;
}
actions.push({
taskId,
path: `TARGET:${toPosix(path.relative(target.projectRoot, taskDir))}`,
legacyBlocks: [scaffoldBlock ? "brief.md#Scaffold Provenance" : "", reviewBlock ? "review.md#Human Review Confirmation" : ""].filter(Boolean),
fields: parsed.fields,
});
}
return {
operation: "migrate-task-audit-index",
target: target.projectRoot,
result: failures.length ? "blocked" : "planned",
summary: {
tasks: listTaskPlanPaths(target).length,
actions: actions.length,
failures: failures.length,
legacyAuditBlocks: actions.reduce((sum, action) => sum + action.legacyBlocks.length, 0) + failures.length,
},
actions,
failures,
};
}
export function applyTaskAuditIndexMigration(targetInput) {
const target = normalizeTarget(targetInput);
const plan = planTaskAuditIndexMigration(targetInput);
if (plan.failures.length) {
const error = new Error(`Task audit INDEX migration failed during plan: ${plan.failures.map((failure) => `${failure.taskId}: ${failure.failure}`).join("; ")}`);
error.plan = plan;
throw error;
}
const writes = [];
for (const action of plan.actions) {
const taskDir = path.join(target.projectRoot, action.path.replace(/^TARGET:/, ""));
const indexPath = path.join(taskDir, "INDEX.md");
const briefPath = path.join(taskDir, "brief.md");
const reviewPath = path.join(taskDir, "review.md");
const indexContent = readFileSafe(indexPath);
const briefContent = readFileSafe(briefPath);
const reviewContent = readFileSafe(reviewPath);
writes.push({
indexPath,
briefPath,
reviewPath,
before: { indexContent, briefContent, reviewContent },
indexContent: replaceTaskAuditMetadata(indexContent, action.fields).replace(/\n?$/, "\n"),
briefContent: stripLegacyAuditBlocks(briefContent).replace(/\n?$/, "\n"),
reviewContent: stripLegacyAuditBlocks(reviewContent).replace(/\n?$/, "\n"),
});
}
try {
for (const write of writes) {
fs.writeFileSync(write.indexPath, write.indexContent);
fs.writeFileSync(write.briefPath, write.briefContent);
fs.writeFileSync(write.reviewPath, write.reviewContent);
}
verifyAppliedWrites(writes);
} catch (cause) {
for (const write of writes) {
fs.writeFileSync(write.indexPath, write.before.indexContent);
fs.writeFileSync(write.briefPath, write.before.briefContent);
fs.writeFileSync(write.reviewPath, write.before.reviewContent);
}
const error = new Error(`Task audit INDEX migration apply failed and was rolled back: ${cause.message}`);
error.cause = cause;
error.plan = plan;
throw error;
}
return { ...plan, result: "applied" };
}
function verifyAppliedWrites(writes) {
for (const write of writes) {
const audit = parseTaskAuditMetadata(readFileSafe(write.indexPath), { required: true });
if (audit.issues.length) throw new Error(`${write.indexPath}: ${audit.issues.map((issue) => issue.message).join("; ")}`);
if (scaffoldProvenanceHeadingPattern.test(readFileSafe(write.briefPath))) throw new Error(`${write.briefPath}: legacy Scaffold Provenance remains`);
if (humanReviewConfirmationHeadingPattern.test(readFileSafe(write.reviewPath))) throw new Error(`${write.reviewPath}: legacy Human Review Confirmation remains`);
}
}
function parseLegacyAudit({ taskId, indexContent, scaffoldBlock, reviewBlock }) {
const audit = parseTaskAuditMetadata(indexContent);
const fields = {};
for (const field of taskAuditFieldOrder) fields[field] = audit.fields.get(field.toLowerCase()) || "n/a";
const failures = [];
const extraEntries = [];
applyHistoricalBackfillDefaults(fields, taskId);
normalizeExistingAuditFields(fields, taskId, extraEntries);
if (scaffoldBlock) {
const scaffold = fieldsFromMarkdownBlock(scaffoldBlock.body);
for (const [legacyKey, canonical] of scaffoldFieldMap) {
const value = scaffold.get(legacyKey);
if (isConcreteAuditField(value)) fields[canonical] = value;
}
for (const field of requiredScaffold) {
if (!isConcreteAuditField(fields[field])) failures.push(`Scaffold Provenance missing ${field}`);
}
for (const [key, value] of scaffold) {
if (!scaffoldFieldMap.has(key) && isConcreteAuditField(value)) extraEntries.push([titleField(key), value]);
}
}
if (reviewBlock) {
const { fields: review, shape: reviewShape } = parseLegacyReviewFields(reviewBlock.body);
const concreteConfirmation = concreteReviewConfirmationValues(review);
if (concreteConfirmation.length === 0) {
fields["Human Review Status"] = "not-confirmed";
fields["Migration Status"] = "migrated";
fields["Migration Notes"] = "removed placeholder-only legacy Human Review Confirmation";
} else {
for (const [legacyKey, canonical] of reviewFieldMap) {
const value = review.get(legacyKey);
if (isConcreteAuditField(value)) fields[canonical] = value;
}
const legacyTaskKey = review.get("task key") || "";
if (isConcreteAuditField(legacyTaskKey) && legacyTaskKey !== taskId && !taskId.endsWith(`/${legacyTaskKey}`)) failures.push(`Human Review Confirmation Task Key mismatch: ${legacyTaskKey}`);
if (reviewShape === "field-value") {
for (const field of requiredReview) {
if (!isConcreteAuditField(fields[field])) failures.push(`Human Review Confirmation missing ${field}`);
}
if (isConcreteAuditField(fields["Audit Status"]) && String(fields["Audit Status"]).trim().toLowerCase() !== "committed") failures.push(`Human Review Confirmation invalid Audit Status: ${fields["Audit Status"]}`);
} else {
for (const field of ["Confirmed At", "Reviewer"]) {
if (!isConcreteAuditField(fields[field])) failures.push(`Human Review Confirmation missing ${field}`);
}
for (const field of requiredReview) {
if (!isConcreteAuditField(fields[field])) {
fields[field] = "legacy-unavailable";
extraEntries.push([`Missing ${field}`, "legacy-unavailable"]);
}
}
fields["Audit Status"] = "committed";
}
if (reviewShape === "field-value" && isConcreteAuditField(fields["Review Commit SHA"]) && !/^[0-9a-f]{7,40}$/i.test(fields["Review Commit SHA"])) failures.push("Human Review Confirmation invalid Review Commit SHA");
fields["Human Review Status"] = "confirmed";
fields["Audit Source"] = "migrated-legacy-review";
fields["Audit Status"] = String(fields["Audit Status"] || "").toLowerCase() === "committed" ? "committed" : fields["Audit Status"];
fields["Migration Status"] = "migrated";
fields["Migrated From"] = "review.md#Human Review Confirmation";
fields["Migration Notes"] = reviewShape === "field-value"
? "migrated legacy review confirmation; native INDEX git audit not required"
: "migrated loose legacy review confirmation; missing native audit fields recorded as legacy-unavailable";
for (const [key, value] of review) {
if (!reviewFieldMap.has(key) && key !== "task key" && isConcreteAuditField(value)) extraEntries.push([titleField(key), value]);
}
}
}
if (extraEntries.length) {
const mergedExtra = mergeLegacyExtraFields(fields["Legacy Extra Fields"], extraEntries);
if (mergedExtra.failure) failures.push(mergedExtra.failure);
else fields["Legacy Extra Fields"] = mergedExtra.json;
}
return { fields, failures };
}
function mergeLegacyExtraFields(existingValue, entries) {
let merged = {};
if (isConcreteAuditField(existingValue)) {
try {
merged = JSON.parse(existingValue);
if (!merged || Array.isArray(merged) || typeof merged !== "object") throw new Error("not an object");
} catch {
return { failure: "Legacy Extra Fields contains invalid JSON" };
}
}
const next = legacyExtraFieldsJson(entries);
try {
merged = { ...merged, ...JSON.parse(next) };
} catch {
return { failure: "Legacy Extra Fields migration generated invalid JSON" };
}
return { json: JSON.stringify(merged) };
}
function applyHistoricalBackfillDefaults(fields, taskId) {
const createdAt = String(taskId || "").match(/\d{4}-\d{2}-\d{2}/)?.[0] || "legacy-unavailable";
const defaults = {
"Created By": "historical-backfill",
"Created At": createdAt,
"Command Shape": "legacy-unavailable",
"Budget": "legacy-unavailable",
"Template Source": "legacy-task-index-migration",
"Task Creator": "legacy-unavailable",
"Task Creator Source": "legacy-unavailable",
"Human Review Status": "not-confirmed",
"Audit Source": "migrated-index-backfill",
"Audit Status": "migrated",
"Migration Status": "migrated",
};
for (const [field, value] of Object.entries(defaults)) {
if (!isConcreteAuditField(fields[field])) fields[field] = value;
}
}
function normalizeExistingAuditFields(fields, taskId, extraEntries) {
const allowedCreatedBy = new Set(["harness-new-task", "manual-exception", "historical-backfill"]);
const allowedBudget = new Set(["simple", "standard", "complex", "legacy-unavailable"]);
const allowedCreatorSource = new Set(["git-config", "git-config-missing", "git-unavailable", "legacy-unavailable"]);
const allowedReviewStatus = new Set(["not-confirmed", "confirmed"]);
const allowedAuditStatus = new Set(["created", "committed", "migrated"]);
if (!allowedCreatedBy.has(normalizeToken(fields["Created By"]))) replacePreserving(fields, extraEntries, "Created By", "historical-backfill");
if (normalizeToken(fields["Created At"]) !== "legacy-unavailable" && !isValidDateOnly(fields["Created At"])) replacePreserving(fields, extraEntries, "Created At", String(taskId || "").match(/\d{4}-\d{2}-\d{2}/)?.[0] || "legacy-unavailable");
if (!allowedBudget.has(normalizeToken(fields["Budget"]))) replacePreserving(fields, extraEntries, "Budget", "legacy-unavailable");
const creatorSource = normalizeToken(fields["Task Creator Source"]);
if (!allowedCreatorSource.has(creatorSource)) {
const normalized = /^git-config(?:-|$)/.test(creatorSource) ? "git-config" : "legacy-unavailable";
replacePreserving(fields, extraEntries, "Task Creator Source", normalized);
}
if (!allowedReviewStatus.has(normalizeToken(fields["Human Review Status"]))) replacePreserving(fields, extraEntries, "Human Review Status", "not-confirmed");
if (!allowedAuditStatus.has(normalizeToken(fields["Audit Status"]))) {
const replacement = normalizeToken(fields["Human Review Status"]) === "confirmed"
? "committed"
: normalizeToken(fields["Audit Source"]) === "native-index"
? "created"
: "migrated";
replacePreserving(fields, extraEntries, "Audit Status", replacement);
}
}
function replacePreserving(fields, extraEntries, field, replacement) {
if (isConcreteAuditField(fields[field])) extraEntries.push([`Original ${field}`, fields[field]]);
fields[field] = replacement;
}
function parseLegacyReviewFields(body) {
const fields = fieldsFromMarkdownBlock(body);
const rows = markdownTableRows(body);
let shape = hasFieldValueTable(rows) ? "field-value" : "loose";
for (const row of rows.slice(1).filter((candidate) => !candidate.every((cell) => /^:?-{3,}:?$/.test(cell)))) {
const header = rows[0] || [];
const confirmedAtIndex = firstColumn(header, ["Confirmed At"]);
const reviewerIndex = firstColumn(header, ["Reviewer"]);
const messageIndex = firstColumn(header, ["Message"]);
const evidenceIndex = firstColumn(header, ["Evidence", "Evidence Checked"]);
if (confirmedAtIndex >= 0 && row[confirmedAtIndex]) fields.set("confirmed at", row[confirmedAtIndex]);
if (reviewerIndex >= 0 && row[reviewerIndex]) fields.set("reviewer", row[reviewerIndex]);
if (messageIndex >= 0 && row[messageIndex]) fields.set("message", row[messageIndex]);
if (evidenceIndex >= 0 && row[evidenceIndex]) fields.set("evidence checked", row[evidenceIndex]);
}
if (!rows.length && /^\s*[-*]\s*[^::|]+?\s*[::]/m.test(String(body || ""))) shape = "loose";
return { fields, shape };
}
function hasFieldValueTable(rows) {
const header = rows[0] || [];
return firstColumn(header, ["Field", "字段"]) >= 0 && firstColumn(header, ["Value", "值"]) >= 0;
}
function concreteReviewConfirmationValues(fields) {
const confirmationKeys = [
"confirmation id",
"confirmed at",
"reviewer",
"reviewer email",
"confirm text",
"evidence",
"evidence checked",
"commit sha",
"review commit sha",
];
return confirmationKeys.map((key) => fields.get(key)).filter(isConcreteAuditField);
}
function titleField(value) {
return String(value || "")
.split(/\s+/)
.filter(Boolean)
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
.join(" ");
}
function normalizeToken(value) {
return String(value || "").replace(/`/g, "").trim().toLowerCase().replaceAll("_", "-").replace(/\s+/g, "-");
}
function isValidDateOnly(value) {
const raw = String(value || "").trim();
const match = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) return false;
const date = new Date(`${raw}T00:00:00.000Z`);
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === raw;
}
import fs from "node:fs";
import path from "node:path";
import { toPosix } from "../core-shared.mjs";
import {
appendLongRunningContractFile,
moduleTemplateFiles,
taskFilesForBudget,
} from "./template-files.mjs";
export function planCreateTaskChanges({ target, directory, normalizedModuleKey, normalizedLocale, normalizedBudget, longRunning, presetContext }) {
const changes = [];
if (normalizedModuleKey) {
const moduleDirectory = path.dirname(directory);
for (const [destination, source] of moduleTemplateFiles({ locale: normalizedLocale })) {
const destinationPath = path.join(moduleDirectory, destination);
if (fs.existsSync(destinationPath)) continue;
changes.push({
destination: toPosix(path.relative(target.projectRoot, destinationPath)),
source,
action: "create",
});
}
}
for (const [destination, source] of appendLongRunningContractFile(taskFilesForBudget({ budget: normalizedBudget, locale: normalizedLocale }), {
locale: normalizedLocale,
longRunning,
})) {
changes.push({
destination: toPosix(path.relative(target.projectRoot, path.join(directory, destination))),
source,
action: "create",
});
}
if (presetContext) {
for (const evidence of presetContext.evidenceFiles || []) {
changes.push({ destination: toPosix(evidence.relativePath), source: evidence.source, action: "create" });
}
for (const resource of presetContext.resourceFiles || []) {
changes.push({ destination: toPosix(resource.relativePath), source: resource.source, action: "create" });
}
for (const [kind, rows] of Object.entries(presetContext.resourceIndexRows || {})) {
if (!rows.length) continue;
const destination = kind === "references" ? "references/INDEX.md" : "artifacts/INDEX.md";
changes.push({
destination: toPosix(path.relative(target.projectRoot, path.join(directory, destination))),
source: `preset-${kind}-index`,
action: "update",
});
}
}
return changes;
}
export function refreshPresetCommandAudit(target, presetContext, { commandWriteScopes = [], dryRun = false } = {}) {
const scopes = [...new Set(commandWriteScopes.filter(Boolean))];
presetContext.audit = {
...presetContext.audit,
presetWriteScopes: presetContext.audit.writeScopes || [],
commandWriteScopes: scopes,
};
for (const evidence of presetContext.evidenceFiles || []) {
if (evidence.source !== "preset-audit") continue;
evidence.content = `${JSON.stringify(presetContext.audit, null, 2)}\n`;
if (dryRun) continue;
fs.writeFileSync(path.join(target.projectRoot, evidence.relativePath), evidence.content);
}
}
import fs from "node:fs";
import path from "node:path";
import {
lessonCandidatesFile,
readFileSafe,
todayDate,
toPosix,
visualMapFile,
} from "../core-shared.mjs";
import {
firstColumn,
updateMarkdownTableRow,
} from "../markdown-utils.mjs";
import {
normalizePhaseActor,
normalizePhaseKind,
} from "../phase-kind.mjs";
import { parseLessonCandidateStatus } from "../task-lesson-candidates.mjs";
export function advanceLifecyclePhase(target, taskDir, event) {
const visualMapPath = path.join(taskDir, visualMapFile);
if (!fs.existsSync(visualMapPath)) return "";
const content = readFileSafe(visualMapPath);
let updated = false;
const phaseUpdate = updateMarkdownTableRow(content, /^Phase ID$/i, (header, row) => {
if (updated || !phaseMatchesLifecycleEvent(header, row, event)) return null;
updated = true;
const next = [...row];
const stateIndex = firstColumn(header, ["State", "状态"]);
const completionIndex = firstColumn(header, ["Completion", "完成度"]);
const evidenceIndex = firstColumn(header, ["Evidence Status", "证据状态"]);
if (stateIndex >= 0) next[stateIndex] = "done";
if (completionIndex >= 0) next[completionIndex] = "100";
if (evidenceIndex >= 0) next[evidenceIndex] = "present";
return next;
});
if (!updated || phaseUpdate.content === content) return "";
fs.writeFileSync(visualMapPath, phaseUpdate.content);
return toPosix(path.relative(target.projectRoot, visualMapPath));
}
export function autoRecordNoLessonCandidateDecision(target, taskDir) {
const candidatePath = path.join(taskDir, lessonCandidatesFile);
if (!fs.existsSync(candidatePath)) return "";
const content = readFileSafe(candidatePath);
const status = parseLessonCandidateStatus(content);
if (status.rows.length > 0 || status.declaredStatus !== "pending-review") return "";
const next = replaceNoLessonCandidateFields(content);
if (next === content) return "";
fs.writeFileSync(candidatePath, next);
return toPosix(path.relative(target.projectRoot, candidatePath));
}
function phaseMatchesLifecycleEvent(header, row, event) {
const kindIndex = firstColumn(header, ["Kind", "阶段类型", "类型"]);
if (kindIndex < 0) return false;
const actorIndex = firstColumn(header, ["Actor", "执行者", "角色"]);
const exitCommandIndex = firstColumn(header, ["Exit Command", "出口命令", "退出命令"]);
const outputIndex = firstColumn(header, ["Output", "产出"]);
const kind = normalizePhaseKind(row[kindIndex]);
const actor = actorIndex >= 0 ? normalizePhaseActor(row[actorIndex]) : "agent";
const exitCommand = String(row[exitCommandIndex] || "");
const output = String(row[outputIndex] || "");
if (event === "task-start") return kind === "init" && actor === "agent";
if (event === "task-review") {
return kind === "gate" && actor === "agent" && (/\btask-review\b/.test(exitCommand) || /Agent Review Submission/i.test(output));
}
if (event === "task-complete") {
return kind === "gate" && actor === "agent" && /\btask-complete\b/.test(exitCommand);
}
return false;
}
function replaceNoLessonCandidateFields(content) {
let next = String(content || "");
const replacements = [
[/(\|\s*Task-level status\s*\|\s*)[^|]+(\|)/i, "$1no-candidate-accepted $2"],
[/(\|\s*Review decision\s*\|\s*)[^|]+(\|)/i, "$1accepted-no-candidate $2"],
[/(\|\s*Closeout token\s*\|\s*)[^|]+(\|)/i, "$1checked-none:auto-no-candidate $2"],
[/(\|\s*Last updated\s*\|\s*)[^|]+(\|)/i, `$1${todayDate()} $2`],
];
for (const [pattern, replacement] of replacements) next = next.replace(pattern, replacement);
const reason = "Agent review found no reusable lesson candidates in this task; the empty candidate table is recorded as checked for closeout.";
return next.replace(
/(## No-Candidate Reason\s*\n)([\s\S]*?)(?=\n## |\s*$)/,
`$1\n${reason}\n`,
);
}
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import {
lessonCandidatesFile,
longRunningTaskContractFile,
nowTimestamp,
readFileSafe,
toPosix,
visualMapFile,
} from "../core-shared.mjs";
import {
collectReviewRisks,
isBlockingReviewRisk,
taskScannerVersion,
} from "../task-review-model.mjs";
import { markdownCell } from "./text-utils.mjs";
export function renderAgentReviewSubmission({ target, taskDir, canonicalTaskId, message, evidence }) {
const timestamp = nowTimestamp();
const submissionId = `ARS-${timestamp.replace(/[^0-9]/g, "").slice(0, 14)}`;
const materialsHash = hashTaskMaterials(taskDir);
const reviewContent = readFileSafe(path.join(taskDir, "review.md"));
const openFindings = collectReviewRisks(reviewContent).filter(isBlockingReviewRisk).length;
const evidenceSummary = evidence || message || "Agent submitted task for human review.";
return [
"## Agent Review Submission",
"",
"| Field | Value |",
"| --- | --- |",
`| Submission ID | ${submissionId} |`,
`| Submitted At | ${timestamp} |`,
"| Submitted By | agent |",
`| Task Key | ${canonicalTaskId} |`,
`| Materials Checklist Hash | ${materialsHash} |`,
`| Evidence Summary | ${markdownCell(evidenceSummary)} |`,
`| Open Findings Count | ${openFindings} |`,
`| Scanner Version | ${taskScannerVersion} |`,
`| Target | TARGET:${toPosix(path.relative(target.projectRoot, taskDir))} |`,
"",
].join("\n");
}
export function replaceAgentReviewSubmission(content, block) {
const trimmed = String(content || "").trimEnd();
if (/^##\s*(?:Agent Review Submission|Agent 审查提交|Agent 提交审查)\s*$/im.test(trimmed)) {
return `${trimmed.replace(/^##\s*(?:Agent Review Submission|Agent 审查提交|Agent 提交审查)\s*$[\s\S]*?(?=^##\s+|(?![\s\S]))/im, `${block.trimEnd()}\n\n`)}\n`;
}
return `${trimmed}\n\n${block.trimEnd()}\n`;
}
function hashTaskMaterials(taskDir) {
const hash = crypto.createHash("sha256");
for (const fileName of ["brief.md", "task_plan.md", visualMapFile, lessonCandidatesFile, "progress.md", "review.md", "findings.md", longRunningTaskContractFile]) {
const filePath = path.join(taskDir, fileName);
if (!fs.existsSync(filePath)) continue;
hash.update(fileName);
hash.update("\0");
hash.update(readFileSafe(filePath));
hash.update("\0");
}
return hash.digest("hex").slice(0, 16);
}
import path from "node:path";
import { localizedTemplateSource, todayDate } from "../core-shared.mjs";
import { markdownCell } from "./text-utils.mjs";
function shellArg(value) {
const text = String(value || "");
if (/^[A-Za-z0-9._/:=@+-]+$/.test(text)) return text;
return `'${text.replaceAll("'", "'\\''")}'`;
}
function commandPathArg(value, fallback) {
const text = String(value || fallback || ".").trim() || ".";
if (text === ".") return ".";
return path.isAbsolute(text) ? fallback : text;
}
function renderNewTaskCommand({ taskId, title, locale, budget, longRunning, moduleKey, preset, fromSession, targetInput }) {
const parts = ["harness", "new-task"];
if (taskId) parts.push(taskId);
parts.push("--budget", budget, "--locale", locale);
if (title) parts.push("--title", title);
if (moduleKey) parts.push("--module", moduleKey);
if (preset && preset !== "none") parts.push("--preset", preset);
if (fromSession) parts.push("--from-session", commandPathArg(fromSession, "<session.json>"));
if (longRunning) parts.push("--long-running");
parts.push(commandPathArg(targetInput, "<target>"));
return parts.map(shellArg).join(" ");
}
export function buildScaffoldProvenance({ taskId, normalizedTaskId, title, locale, budget, longRunning, moduleKey, preset, fromSession, targetInput, automaticTaskId = false }) {
return {
createdBy: "harness new-task",
command: markdownCell(renderNewTaskCommand({
taskId: automaticTaskId ? "" : taskId || normalizedTaskId,
title,
locale,
budget,
longRunning,
moduleKey,
preset,
fromSession,
targetInput,
})),
createdAt: todayDate(),
budget,
templateSource: localizedTemplateSource("templates/planning/brief.md", locale),
exceptionReason: "n/a",
};
}
import { localizedTemplateSource, longRunningTaskContractFile, visualMapFile, lessonCandidatesFile } from "../core-shared.mjs";
export function taskTemplateFiles({ locale = "en-US" } = {}) {
return [
["INDEX.md", "templates/planning/INDEX.md"],
["brief.md", "templates/planning/brief.md"],
["task_plan.md", "templates/planning/task_plan.md"],
["execution_strategy.md", "templates/planning/execution_strategy.md"],
[visualMapFile, "templates/planning/visual_map.md"],
["findings.md", "templates/planning/findings.md"],
[lessonCandidatesFile, "templates/planning/lesson_candidates.md"],
["progress.md", "templates/planning/progress.md"],
["review.md", "templates/planning/review.md"],
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
}
export function simpleTaskTemplateFiles({ locale = "en-US" } = {}) {
return [
["INDEX.md", "templates/planning/INDEX.md"],
["brief.md", "templates/planning/brief.md"],
["task_plan.md", "templates/planning/task_plan.md"],
[visualMapFile, "templates/planning/visual_map.simple.md"],
["progress.md", "templates/planning/progress.md"],
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
}
export function optionalTaskTemplateFiles({ locale = "en-US" } = {}) {
return [
["references/INDEX.md", "templates/planning/optional/references/INDEX.md"],
["artifacts/INDEX.md", "templates/planning/optional/artifacts/INDEX.md"],
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
}
export function moduleTemplateFiles({ locale = "en-US" } = {}) {
return [
["brief.md", "templates/planning/module_brief.md"],
["module_plan.md", "templates/planning/module_plan.md"],
["execution_strategy.md", "templates/planning/execution_strategy.md"],
[visualMapFile, "templates/planning/visual_map.md"],
["session_prompt.md", "templates/planning/module_session_prompt.md"],
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
}
export function taskFilesForBudget({ budget, locale }) {
if (budget === "simple") return simpleTaskTemplateFiles({ locale });
if (budget === "complex") return [...taskTemplateFiles({ locale }), ...optionalTaskTemplateFiles({ locale })];
return taskTemplateFiles({ locale });
}
export function appendLongRunningContractFile(files, { locale, longRunning }) {
if (!longRunning) return files;
return [...files, [longRunningTaskContractFile, localizedTemplateSource("templates/planning/long-running-task-contract.md", locale)]];
}
import {
allowedTaskStates,
allowedTaskBudgets,
taskContractMarker,
} from "./core-shared.mjs";
import { tableAfterHeading, firstColumn } from "./markdown-utils.mjs";
export function parseTaskState(progressContent) {
return parseTaskStateInfo(progressContent).state;
}
export function parseTaskBudget(taskPlanContent) {
const match =
String(taskPlanContent || "").match(/^Selected budget\s*[::]\s*([^\n]+)/im) ||
String(taskPlanContent || "").match(/^选择预算\s*[::]\s*([^\n]+)/im);
if (!match) return "standard";
const raw = match[1].replace(/`/g, "").trim().toLowerCase();
const normalized = raw.replaceAll("_", "-").replace(/\s+/g, "-");
if (allowedTaskBudgets.has(normalized)) return normalized;
if (["long-running", "longrunning", "module-parallel"].includes(normalized)) return "complex";
return "standard";
}
function parseMetadataLine(content, labels) {
const escaped = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
const match = String(content || "").match(new RegExp(`^(?:${escaped})\\s*[::]\\s*([^\\n]+)`, "im"));
return match ? match[1].replace(/`/g, "").trim() : "";
}
function normalizeMetadataValue(value, fallback = "") {
const normalized = String(value || "")
.replace(/`/g, "")
.trim()
.toLowerCase()
.replaceAll("_", "-")
.replace(/\s+/g, "-");
return normalized || fallback;
}
export function parseTaskMetadata(taskPlanContent) {
const content = String(taskPlanContent || "");
const kind = normalizeMetadataValue(parseMetadataLine(content, ["Task Kind", "任务类型"]), "general");
const preset = normalizeMetadataValue(parseMetadataLine(content, ["Task Preset", "Preset", "任务预设"]), "none");
const presetVersion = parseMetadataLine(content, ["Preset Version", "预设版本"]);
const migrationTargetLevel = normalizeMetadataValue(
parseMetadataLine(content, ["Migration Target Level", "Target Level", "迁移目标等级", "目标等级"]),
"",
);
const migrationAchievedLevel = normalizeMetadataValue(
parseMetadataLine(content, ["Migration Achieved Level", "Achieved Level", "迁移实际完成等级", "实际完成等级"]),
"",
);
const evidenceBundle = parseMetadataLine(content, ["Evidence Bundle", "证据包"]);
return {
kind,
preset,
presetVersion,
migrationTargetLevel,
migrationAchievedLevel,
evidenceBundle,
};
}
export function parseTaskContractInfo(taskPlanContent) {
const content = String(taskPlanContent || "");
const explicit =
content.match(/^Task Contract\s*[::]\s*`?([^`\n]+)`?\s*$/im) ||
content.match(/^任务合同\s*[::]\s*`?([^`\n]+)`?\s*$/im);
const version = explicit ? explicit[1].trim() : "";
return {
version,
generated: version === "harness-task/v1" || content.includes(taskContractMarker),
};
}
export function parseTaskStateInfo(progressContent) {
const match = progressContent.match(/^##\s*(?:Current Status|Status|状态)\s*[::]?\s*(?:\n\s*)?([^\n]+)/im);
if (!match) return inferLegacyTaskState(progressContent);
const raw = match[1].replace(/`/g, "").trim();
if (!raw || raw.includes("|") || /^[-*]\s+/.test(raw)) return inferLegacyTaskState(progressContent);
const aliases = new Map([
["进行中", "in_progress"],
["已完成", "done"],
["未开始", "not_started"],
["计划中", "planned"],
["审查中", "review"],
["已阻塞", "blocked"],
["pending", "planned"],
]);
const normalized = aliases.get(raw) || raw.toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
return allowedTaskStates.has(normalized)
? { state: normalized, source: "explicit", raw }
: { state: "unknown", source: "invalid", raw };
}
function inferLegacyTaskState(progressContent) {
const { header, rows } = tableAfterHeading(progressContent, /^(Status|状态)$/i);
const statusIndex = firstColumn(header, ["Status", "状态"]);
if (statusIndex < 0 || rows.length === 0) return { state: "unknown", source: "missing", raw: "" };
const states = rows.map((row) => normalizeLegacyState(row[statusIndex])).filter(Boolean);
if (states.includes("blocked")) return { state: "blocked", source: "legacy-table", raw: "blocked" };
if (states.includes("in_progress")) return { state: "in_progress", source: "legacy-table", raw: "in_progress" };
if (states.includes("review")) return { state: "review", source: "legacy-table", raw: "review" };
if (states.length > 0 && states.every((state) => state === "done")) return { state: "done", source: "legacy-table", raw: "done" };
if (states.some((state) => ["planned", "not_started"].includes(state))) return { state: "planned", source: "legacy-table", raw: "planned" };
return { state: "unknown", source: "missing", raw: "" };
}
function normalizeLegacyState(value) {
const raw = String(value || "").replace(/`/g, "").trim().toLowerCase();
if (!raw || /^(none|n\/a|na|-|—|–|无)$/.test(raw)) return "";
if (/block|阻塞|blocked/.test(raw)) return "blocked";
if (/in[-_\s]?progress|doing|active|进行中|当前|working/.test(raw)) return "in_progress";
if (/review|审查|审核|验证中/.test(raw)) return "review";
if (/done|complete|completed|merged|closed|完成|已完成/.test(raw)) return "done";
if (/pending|planned|todo|not[-_\s]?started|未开始|计划/.test(raw)) return "planned";
return "";
}
# {{TASK_TITLE}} - 任务包索引
Task Contract: harness-task/v1
## 任务身份
| Field | Value |
| --- | --- |
| Task ID | `{{TASK_ID}}` |
| Budget | `{{TASK_BUDGET}}` |
| Preset | `{{TASK_PRESET}}` |
| Module | `{{TASK_MODULE}}` |
| Long-running | `{{TASK_LONG_RUNNING}}` |
| Created | {{DATE}} |
## 任务审计元数据
| Field | Value |
| --- | --- |
| Created By | {{TASK_AUDIT_CREATED_BY}} |
| Created At | {{TASK_AUDIT_CREATED_AT}} |
| Command Shape | {{TASK_AUDIT_COMMAND_SHAPE}} |
| Budget | {{TASK_AUDIT_BUDGET}} |
| Template Source | {{TASK_AUDIT_TEMPLATE_SOURCE}} |
| Task Creator | {{TASK_AUDIT_TASK_CREATOR}} |
| Task Creator Source | {{TASK_AUDIT_TASK_CREATOR_SOURCE}} |
| Human Review Status | {{TASK_AUDIT_HUMAN_REVIEW_STATUS}} |
| Confirmation ID | {{TASK_AUDIT_CONFIRMATION_ID}} |
| Confirmed At | {{TASK_AUDIT_CONFIRMED_AT}} |
| Reviewer | {{TASK_AUDIT_REVIEWER}} |
| Reviewer Email | {{TASK_AUDIT_REVIEWER_EMAIL}} |
| Confirm Text | {{TASK_AUDIT_CONFIRM_TEXT}} |
| Evidence Checked | {{TASK_AUDIT_EVIDENCE_CHECKED}} |
| Review Commit SHA | {{TASK_AUDIT_REVIEW_COMMIT_SHA}} |
| Audit Source | {{TASK_AUDIT_AUDIT_SOURCE}} |
| Audit Status | {{TASK_AUDIT_AUDIT_STATUS}} |
| Exception Reason | {{TASK_AUDIT_EXCEPTION_REASON}} |
| Message | {{TASK_AUDIT_MESSAGE}} |
| Migration Status | {{TASK_AUDIT_MIGRATION_STATUS}} |
| Migrated From | {{TASK_AUDIT_MIGRATED_FROM}} |
| Legacy Extra Fields | {{TASK_AUDIT_LEGACY_EXTRA_FIELDS}} |
| Migration Notes | {{TASK_AUDIT_MIGRATION_NOTES}} |
## 核心合同文件
| 文件 | 用途 |
| --- | --- |
| `brief.md` | 面向人和下一轮 agent 的任务摘要与上下文入口。 |
| `task_plan.md` | 当前任务目标、范围、所选预算、验收标准和执行决策。 |
| `visual_map.md` | 阶段图、证据状态、下一步生命周期命令和支持性图表。 |
| `progress.md` | 执行日志、验证证据、决策和交接记录。 |
## 标准任务文件
standard 和 complex 任务包含以下文件。
| 文件 | 用途 |
| --- | --- |
| `execution_strategy.md` | 执行模式、owner、冲突控制和证据策略。 |
| `findings.md` | 发现、研究记录、已接受风险和未解决问题。 |
| `lesson_candidates.md` | closeout 前的任务本地 lesson candidate 决策。 |
| `review.md` | Agent Review Submission、对抗审查、findings、evidence 和 routing。 |
## 可选索引
| 索引 | 用途 |
| --- | --- |
| `references/INDEX.md` | 参考资料和 preset 提供的 required reads。 |
| `artifacts/INDEX.md` | 生成产物、证据包、截图、报告和命令输出。 |
## Preset 摘要
本节由系统渲染。Preset 不能新增自定义根级文件,也不能任意追加根 `INDEX.md` 内容。
| Field | Value |
| --- | --- |
| Preset | `{{TASK_PRESET}}` |
| Preset Version | `{{TASK_PRESET_VERSION}}` |
| Evidence Bundle | `{{TASK_EVIDENCE_BUNDLE}}` |
| Resource Indexes | `references/INDEX.md`; `artifacts/INDEX.md` |
## 更新规则
- 状态和决策写入 `progress.md`。
- 任务专属目标和验收标准写入 `task_plan.md`。
- 大段命令输出、截图、报告和生成文件放入 `artifacts/INDEX.md`。
- 源材料、外部链接和 preset required reads 放入 `references/INDEX.md`。
# Visual Map / 可视化图谱
Visual Map Contract: v1.0
本文件是任务图表集合,不只是阶段路线图。只有对人或 agent 理解任务有实际帮助的图才放进来。
## 图表索引(Map Index)
| ID | Type | Purpose | Required For Understanding | Source Evidence | Promotion Candidate |
| --- | --- | --- | --- | --- | --- |
| MAP-01 | phase | 展示执行阶段和依赖关系 | yes | `task_plan.md` | no |
## 阶段关系图(Phase Graph)
```mermaid
flowchart LR
INIT01["INIT-01 范围与上下文\nkind=init"] --> EXEC01["EXEC-01 实现切片\nkind=execution"]
EXEC01 --> GATE01["GATE-01 直接完成\nkind=gate"]
```
## 阶段表(Phase Table,表头供 checker 解析)
| Phase ID | Kind | Depends On | State | Completion | Output | Required Evidence | Exit Command | Actor | Evidence Status | Blocking Risk | Owner / Handoff |
| --- | --- | --- | --- | ---: | --- | --- | --- | --- | --- | --- | --- |
| INIT-01 | init | none | planned | 0 | 任务边界已清楚到可以执行 | `task_plan.md` | `harness task-start {{TASK_ID}}` | agent | missing | none | coordinator |
| EXEC-01 | execution | INIT-01 | planned | 0 | 简单实现或文档变更已完成 | diff、command 或 artifact path | `harness task-phase {{TASK_ID}} EXEC-01 --state done --completion 100 --evidence present` | agent | missing | [risk] | [owner] |
| GATE-01 | gate | EXEC-01 | planned | 0 | 直接完成任务 | progress update 和最终证据说明 | `harness task-complete {{TASK_ID}} --message "<summary>"` | agent | missing | [risk] | coordinator |
允许的 `State`:`planned`, `in_progress`, `review`, `blocked`, `done`, `skipped`。
允许的 `Evidence Status`:`missing`, `partial`, `present`, `waived`。
允许的 `Kind`:`init`, `execution`, `gate`。
允许的 `Actor`:`agent`, `human`, `coordinator`。
`Completion` 使用 `0..100` 的整数;`done` 应为 `100`,`planned` 应为 `0`,`skipped` 不计入 dashboard 总完成度。dashboard 的实现完成度只由非 skipped 的 `execution` 阶段计算;`init` 和 `gate` 阶段表达生命周期门禁、下一步命令和责任人,不拉低实现完成度。
## 支持性图表(Supporting Maps)
按需添加,不要求每类都存在:
- architecture:模块、组件、服务结构。
- sequence:前端、后端、服务、数据库、agent 时序。
- data-flow:数据流转和所有权。
- state:状态机或生命周期。
- topology:repo、服务、worker、worktree 拓扑。
- decision:方案分叉和决策树。
function presetsView() {
ensurePresetState();
const catalog = bundle.presetCatalog || { summary: {}, roots: [], presets: [] };
let presets = filteredPresets();
syncVisiblePresetSelection(presets);
presets = filteredPresets();
const selected = selectedPreset(presets);
syncPresetUninstallScope(selected);
return `<div class="presets-page stack">
<section class="flow-panel preset-command-center">
<div class="section-head">
<div>
<p class="eyebrow">${t("presetCatalog")}</p>
<h2>${t("presetCatalog")}</h2>
<p class="subtle">${t("presetCatalogSubtitle")}</p>
</div>
<span class="preset-count-pill">${presets.length}/${catalog.summary?.total || 0}</span>
</div>
<div class="preset-priority-strip" aria-label="${escapeAttr(t("presetPriorityTitle"))}">
${presetPriorityStep("project", 1)}
${presetPriorityStep("user", 2)}
${presetPriorityStep("builtin", 3)}
</div>
<div class="preset-toolbar">
<div class="input-group">
<input data-preset-search value="${escapeAttr(state.presetQuery)}" placeholder="${escapeAttr(t("presetSearchPlaceholder"))}" aria-label="${escapeAttr(t("presetSearch"))}">
</div>
<div class="preset-source-tabs" role="tablist" aria-label="${escapeAttr(t("presetSourceFilter"))}">
${presetSourceOptions().map((source) => presetSourceButton(source)).join("")}
</div>
</div>
</section>
<section class="preset-workspace">
<div class="flow-panel preset-collection-panel">
<div class="preset-panel-heading">
<div>
<h3>${t("presetCollection")}</h3>
<p>${t("presetCollectionHint")}</p>
</div>
</div>
<div class="preset-catalog-list">
${presets.map((preset) => presetCard(preset, selected ? presetKey(selected) : "")).join("") || emptyState(t("noPresets"))}
</div>
</div>
<div class="preset-detail-workspace stack">
${presetDetailPanel(selected)}
${presetLayerStackPanel(selected)}
</div>
<aside class="preset-context-actions stack">
${presetActionPanel(selected)}
${presetImportPanel()}
${presetRestorePanel()}
${presetSummaryPanel(catalog)}
</aside>
</section>
</div>`;
}
function ensurePresetState() {
const presets = bundle.presetCatalog?.presets || [];
if (!state.selectedPresetKey && state.selectedPresetId) {
const legacySelection = presets.find((preset) => preset.id === state.selectedPresetId);
if (legacySelection) state.selectedPresetKey = presetKey(legacySelection);
}
if (!state.selectedPresetKey && presets[0]) {
state.selectedPresetKey = presetKey(presets[0]);
state.presetUninstallConfirm = "";
}
if (state.selectedPresetKey && !presets.some((preset) => presetKey(preset) === state.selectedPresetKey) && presets[0]) {
state.selectedPresetKey = presetKey(presets[0]);
state.presetUninstallConfirm = "";
}
}
function presetSourceOptions() {
return [
["all", t("allPresets")],
["project", t("presetSourceProject")],
["user", t("presetSourceUser")],
["builtin", t("presetSourceBuiltin")],
];
}
function presetSourceButton([source, labelText]) {
const active = state.presetSourceFilter === source;
const count = source === "all" ? (bundle.presetCatalog?.summary?.total || 0) : (bundle.presetCatalog?.summary?.[source] || 0);
return `<button type="button" class="${active ? "active" : ""}" data-preset-source-filter="${escapeAttr(source)}" role="tab" aria-selected="${active ? "true" : "false"}">
<span>${escapeHtml(labelText)}</span>
<strong>${count}</strong>
</button>`;
}
function filteredPresets() {
const query = String(state.presetQuery || "").trim().toLowerCase();
return (bundle.presetCatalog?.presets || []).filter((preset) => {
if (state.presetSourceFilter !== "all" && preset.source !== state.presetSourceFilter) return false;
return presetMatchesQuery(preset, query);
});
}
function presetMatchesQuery(preset, query = state.presetQuery) {
const normalizedQuery = String(query || "").trim().toLowerCase();
if (!normalizedQuery) return true;
return [
preset.id,
preset.source,
preset.purpose,
preset.taskKind,
preset.manifestPath,
preset.version,
...(preset.compatibleBudgets || []),
].some((value) => String(value || "").toLowerCase().includes(normalizedQuery));
}
function syncVisiblePresetSelection(visiblePresets) {
if (!visiblePresets.length) {
state.selectedPresetKey = "";
state.presetUninstallConfirm = "";
return;
}
if (!visiblePresets.some((preset) => presetKey(preset) === state.selectedPresetKey)) {
state.selectedPresetKey = presetKey(visiblePresets[0]);
state.presetUninstallConfirm = "";
}
}
function selectedPreset(visiblePresets = filteredPresets()) {
return visiblePresets.find((preset) => presetKey(preset) === state.selectedPresetKey) || visiblePresets[0] || null;
}
function presetCard(preset, selectedId) {
const key = presetKey(preset);
const selected = key === selectedId;
return `<article class="preset-card ${selected ? "active" : ""} ${preset.effective ? "effective" : "shadowed"}">
<div class="preset-card-topline">
<button type="button" class="preset-card-select" data-preset-select="${escapeAttr(key)}" aria-pressed="${selected ? "true" : "false"}">
<span class="card-id">${escapeHtml(preset.id)}</span>
</button>
<div class="preset-card-tools">
${presetSourceBadge(preset.source)}
${presetStatusBadge(preset)}
<button type="button" class="copy-inline" data-copy-preset-id="${escapeAttr(preset.id)}" title="${escapeAttr(t("copyPresetId"))}">${t("copyIdShort")}</button>
</div>
</div>
<button type="button" class="preset-card-body" data-preset-select="${escapeAttr(key)}">
<span>${escapeHtml(preset.purpose || t("none"))}</span>
</button>
<div class="preset-card-meta">
<span>${t("manifestVersion")}: ${escapeHtml(formatPresetVersion(preset))}</span>
<span>${t("taskKind")}: ${escapeHtml(preset.taskKind || t("none"))}</span>
<span>${t("budgets")}: ${escapeHtml((preset.compatibleBudgets || []).join(", ") || t("none"))}</span>
</div>
<code class="preset-manifest-path">${escapeHtml(preset.manifestPath || "")}</code>
</article>`;
}
function presetKey(preset) {
return preset?.key || `${preset?.source || "unknown"}:${preset?.id || ""}`;
}
function presetSourceRank(source) {
return { project: 1, user: 2, builtin: 3 }[source] || 9;
}
function presetLayersForId(id) {
return (bundle.presetCatalog?.presets || [])
.filter((preset) => preset.id === id)
.sort((a, b) => presetSourceRank(a.source) - presetSourceRank(b.source));
}
function syncPresetUninstallScope(preset) {
if (preset && ["project", "user"].includes(preset.source)) state.presetUninstallScope = preset.source;
}
function presetPriorityStep(source, index) {
return `<div class="preset-priority-step">
<span>${index}</span>
<strong>${escapeHtml(t(`presetSource_${source}`) || source)}</strong>
</div>`;
}
function presetSourceBadge(source) {
const normalized = String(source || "unknown");
return `<span class="tag preset-source-badge ${escapeAttr(normalized)}">${escapeHtml(t(`presetSource_${normalized}`) || normalized)}</span>`;
}
function presetStatusBadge(preset) {
return `<span class="tag ${preset.effective ? "pass" : "warn"}">${escapeHtml(preset.effective ? t("presetEffective") : t("presetShadowed"))}</span>`;
}
function formatPresetVersion(preset) {
return preset?.version ?? t("none");
}
function presetSummaryPanel(catalog) {
const roots = catalog.roots || [];
return `<section class="side-panel preset-summary-panel">
<h3>${t("presetSources")}</h3>
<p class="preset-helper">${t("presetSourcesHint")}</p>
<div class="metrics-grid compact">
${metric(t("presetSourceProject"), catalog.summary?.project || 0)}
${metric(t("presetSourceUser"), catalog.summary?.user || 0)}
${metric(t("presetSourceBuiltin"), catalog.summary?.builtin || 0)}
</div>
<div class="preset-roots">
${roots.map((root) => `<div><strong>${escapeHtml(t(`presetSource_${root.source}`) || root.source)}</strong><code>${escapeHtml(root.path || "")}</code></div>`).join("")}
</div>
</section>`;
}
function presetDetailPanel(preset) {
if (!preset) return `<section class="flow-panel preset-detail-panel">${emptyState(t("noPresets"))}</section>`;
const inspectCommand = `harness preset inspect ${preset.id} --json .`;
const checkCommand = `harness preset check ${preset.id} --json .`;
const commandRows = preset.effective
? `${presetCommandRow(inspectCommand)}${presetCommandRow(checkCommand)}`
: `<div class="preset-command-warning">${escapeHtml(t("presetCommandsEffectiveOnly"))}</div>`;
return `<section class="flow-panel preset-detail-panel">
<div class="preset-detail-hero">
<div>
<div class="preset-detail-title-row">
<h3>${escapeHtml(preset.id)}</h3>
<button type="button" class="copy-inline" data-copy-preset-id="${escapeAttr(preset.id)}">${t("copyPresetId")}</button>
</div>
<p>${escapeHtml(preset.purpose || "")}</p>
</div>
<div class="preset-detail-badges">
${presetSourceBadge(preset.source)}
${presetStatusBadge(preset)}
</div>
</div>
<dl class="preset-detail-list">
${presetDetailRow(t("manifestVersion"), formatPresetVersion(preset))}
${presetDetailRow(t("source"), t(`presetSource_${preset.source}`) || preset.source)}
${presetDetailRow(t("status"), preset.effective ? t("presetEffective") : t("presetShadowed"))}
${presetDetailRow(t("taskKind"), preset.taskKind || t("none"))}
${presetDetailRow(t("budgets"), (preset.compatibleBudgets || []).join(", ") || t("none"))}
${presetDetailRow(t("inputs"), preset.inputCount || 0)}
${presetDetailRow(t("references"), preset.referenceCount || 0)}
${presetDetailRow(t("artifacts"), preset.artifactCount || 0)}
${presetDetailRow(t("writeScopes"), preset.writeScopeCount || 0)}
${presetDetailRow(t("requiredReads"), preset.requiredReadCount || 0)}
</dl>
<div class="preset-path-block">
<span>${t("manifestPath")}</span>
<code class="preset-manifest-path">${escapeHtml(preset.manifestPath || "")}</code>
</div>
<div class="preset-command-list">
${commandRows}
</div>
</section>`;
}
function presetDetailRow(labelText, value) {
return `<div><dt>${escapeHtml(labelText)}</dt><dd>${escapeHtml(String(value ?? ""))}</dd></div>`;
}
function presetCommandRow(command) {
return `<div class="preset-command-row">
<code>${escapeHtml(command)}</code>
<button type="button" class="copy-inline" data-copy-preset-command="${escapeAttr(command)}">${t("copyCommand")}</button>
</div>`;
}
function presetLayerStackPanel(preset) {
if (!preset) return "";
const layers = presetLayersForId(preset.id);
return `<section class="flow-panel preset-layer-panel">
<div class="preset-panel-heading">
<div>
<h3>${t("presetLayerStack")}</h3>
<p>${t("presetLayerStackHint")}</p>
</div>
</div>
<div class="preset-layer-list">
${layers.map((layer) => `<button type="button" class="preset-layer-row ${presetKey(layer) === presetKey(preset) ? "active" : ""}" data-preset-select="${escapeAttr(presetKey(layer))}">
<span class="preset-layer-rank">${presetSourceRank(layer.source)}</span>
<span>
<strong>${escapeHtml(t(`presetSource_${layer.source}`) || layer.source)}</strong>
<small>${t("manifestVersion")}: ${escapeHtml(formatPresetVersion(layer))}</small>
</span>
${presetStatusBadge(layer)}
</button>`).join("")}
</div>
</section>`;
}
function presetActionPanel(preset) {
const staticNote = canUseWorkbenchAction("preset-install") ? "" : `<p class="lesson-action-note">${escapeHtml(t("presetWorkbenchRequired"))}</p>`;
const lockedUninstallScope = preset && ["project", "user"].includes(preset.source) ? preset.source : "";
const confirmMatches = Boolean(preset && state.presetUninstallConfirm.trim() === preset.id);
const canCheck = canUseWorkbenchAction("preset-check") && preset && preset.effective;
const canUninstall = canUseWorkbenchAction("preset-uninstall") && preset && preset.source !== "builtin" && confirmMatches;
return `<section class="side-panel preset-action-panel">
<div class="preset-panel-heading">
<div>
<h3>${t("presetContextActions")}</h3>
<p>${preset ? escapeHtml(preset.id) : t("noPresets")}</p>
</div>
</div>
${staticNote}
${presetActionResult()}
<div class="preset-action-group">
<h4>${t("presetCheck")}</h4>
<p>${preset?.effective ? t("presetCheckHint") : t("presetShadowedActionHint")}</p>
<button data-preset-check="${escapeAttr(preset?.id || "")}" ${canCheck ? "" : "disabled"}>${t("presetCheckSelected")}</button>
</div>
<div class="preset-action-group danger">
<h4>${t("presetUninstallSelected")}</h4>
<p>${preset?.source === "builtin" ? t("presetBuiltinImmutable") : t("presetUninstallHint")}</p>
<label>${t("scope")}<select data-preset-uninstall-scope ${lockedUninstallScope ? "disabled" : ""}>
${presetScopeOptions(lockedUninstallScope || state.presetUninstallScope)}
</select></label>
<div class="preset-confirm-row">
<label>${t("confirmPresetId")}<input data-preset-uninstall-confirm value="${escapeAttr(state.presetUninstallConfirm)}" placeholder="${escapeAttr(preset?.id || "")}"></label>
<button type="button" data-preset-fill-confirm="${escapeAttr(preset?.id || "")}" ${preset && preset.source !== "builtin" ? "" : "disabled"}>${t("useSelectedId")}</button>
</div>
${preset && preset.source !== "builtin" && !confirmMatches ? `<p class="preset-confirm-warning">${escapeHtml(t("presetConfirmRequired"))}</p>` : ""}
<button data-preset-uninstall="${escapeAttr(preset?.id || "")}" ${canUninstall ? "" : "disabled"}>${t("presetUninstallSelected")}</button>
</div>
</section>`;
}
function presetImportPanel() {
return `<section class="side-panel preset-action-panel">
<div class="preset-panel-heading">
<div>
<h3>${t("presetImportTitle")}</h3>
<p>${t("presetImportHint")}</p>
</div>
</div>
<div class="preset-action-group">
<label>${t("source")}<input data-preset-install-source value="${escapeAttr(state.presetInstallSource)}" placeholder="${escapeAttr(t("presetInstallSourcePlaceholder"))}"></label>
<label>${t("scope")}<select data-preset-install-scope>
${presetScopeOptions(state.presetInstallScope)}
</select></label>
<label class="check-row"><input type="checkbox" data-preset-install-force ${state.presetInstallForce ? "checked" : ""}> ${t("forceOverwrite")}</label>
<button data-preset-install ${canUseWorkbenchAction("preset-install") ? "" : "disabled"}>${t("presetInstall")}</button>
</div>
</section>`;
}
function presetRestorePanel() {
return `<section class="side-panel preset-action-panel">
<div class="preset-panel-heading">
<div>
<h3>${t("presetRestoreBundled")}</h3>
<p>${t("presetRestoreBundledHint")}</p>
</div>
</div>
<div class="preset-action-group">
<label>${t("scope")}<select data-preset-seed-scope>
${presetScopeOptions(state.presetSeedScope)}
</select></label>
<label class="check-row"><input type="checkbox" data-preset-seed-force ${state.presetSeedForce ? "checked" : ""}> ${t("forceOverwrite")}</label>
<button data-preset-seed ${canUseWorkbenchAction("preset-seed") ? "" : "disabled"}>${t("presetRestoreBundled")}</button>
</div>
</section>`;
}
function presetScopeOptions(current) {
return [["project", t("presetSourceProject")], ["user", t("presetSourceUser")]]
.map(([value, labelText]) => `<option value="${value}" ${current === value ? "selected" : ""}>${escapeHtml(labelText)}</option>`)
.join("");
}
function presetActionResult() {
const result = state.presetActionResult;
if (!result) return "";
const klass = result.ok ? "success" : "failed";
return `<div class="workbench-action-result ${klass}">
<strong>${escapeHtml(result.title || "")}</strong>
<span>${escapeHtml(result.message || "")}</span>
</div>`;
}
/* Preset Workbench */
.presets-page {
min-width: 0;
}
.preset-command-center,
.preset-collection-panel,
.preset-detail-panel,
.preset-layer-panel,
.preset-context-actions,
.preset-context-actions .side-panel {
min-width: 0;
}
.preset-count-pill {
border: 1px solid var(--line);
border-radius: 999px;
background: var(--paper-2);
color: var(--muted);
font-size: 12px;
font-weight: 800;
padding: 7px 11px;
}
.preset-priority-strip {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin: 18px 0;
}
.preset-priority-step {
display: flex;
align-items: center;
gap: 10px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--paper-2);
padding: 10px 12px;
min-width: 0;
}
.preset-priority-step span {
display: inline-grid;
place-items: center;
width: 22px;
height: 22px;
border-radius: 999px;
background: var(--accent);
color: white;
font-size: 12px;
font-weight: 900;
flex: 0 0 auto;
}
.preset-priority-step strong {
font-size: 13px;
overflow-wrap: anywhere;
}
.preset-toolbar {
display: grid;
grid-template-columns: minmax(220px, 1fr) auto;
gap: 14px;
align-items: center;
}
.preset-source-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
}
.preset-source-tabs button {
display: inline-flex;
align-items: center;
gap: 8px;
border: 1px solid var(--line);
background: var(--paper);
color: var(--ink);
border-radius: 8px;
padding: 9px 12px;
font-size: 12px;
font-weight: 800;
}
.preset-source-tabs button.active {
border-color: var(--accent);
background: var(--paper-2);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 20%, transparent);
}
.preset-workspace {
display: grid;
grid-template-columns: minmax(300px, 0.82fr) minmax(420px, 1.25fr) minmax(300px, 0.86fr);
gap: 18px;
align-items: start;
}
.preset-panel-heading {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
margin-bottom: 14px;
}
.preset-panel-heading h3 {
margin: 0;
font-size: 16px;
line-height: 1.2;
}
.preset-panel-heading p,
.preset-helper,
.preset-action-group p {
margin: 4px 0 0;
color: var(--muted);
font-size: 12px;
line-height: 1.45;
}
.preset-catalog-list {
display: grid;
gap: 12px;
}
.preset-card {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--paper);
padding: 14px;
display: grid;
gap: 10px;
min-width: 0;
}
.preset-card.active {
border-color: var(--accent);
background: var(--paper-2);
box-shadow: inset 3px 0 0 var(--accent);
}
.preset-card.shadowed {
opacity: 0.78;
}
.preset-card-topline {
display: flex;
justify-content: space-between;
gap: 10px;
align-items: flex-start;
min-width: 0;
}
.preset-card-tools,
.preset-detail-badges {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: flex-end;
flex: 0 1 auto;
}
.preset-card-select,
.preset-card-body,
.preset-layer-row {
appearance: none;
border: 0;
background: transparent;
color: inherit;
padding: 0;
text-align: left;
cursor: pointer;
min-width: 0;
}
.preset-card-select {
flex: 1 1 auto;
}
.card-id {
display: block;
font-size: 15px;
font-weight: 850;
line-height: 1.25;
overflow-wrap: anywhere;
}
.preset-card-body {
display: block;
width: 100%;
}
.preset-card-body span,
.preset-detail-panel p {
display: block;
margin: 0;
color: var(--muted);
font-size: 13px;
line-height: 1.55;
overflow-wrap: anywhere;
}
.preset-card-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
color: var(--muted);
font-size: 11px;
font-weight: 800;
}
.preset-source-badge.project {
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
}
.copy-inline {
appearance: none;
border: 1px solid var(--line);
background: var(--paper);
color: var(--ink);
border-radius: 7px;
padding: 5px 8px;
font-size: 11px;
font-weight: 800;
line-height: 1.1;
cursor: pointer;
white-space: nowrap;
}
.copy-inline:hover,
.preset-card-select:hover .card-id,
.preset-card-body:hover span,
.preset-layer-row:hover strong {
color: var(--accent);
}
.preset-detail-hero {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 18px;
align-items: start;
padding-bottom: 16px;
border-bottom: 1px solid var(--line);
}
.preset-detail-title-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.preset-detail-title-row h3 {
margin: 0;
font-size: 22px;
line-height: 1.15;
overflow-wrap: anywhere;
}
.preset-detail-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0;
margin: 16px 0;
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
}
.preset-detail-list div {
display: grid;
gap: 4px;
padding: 11px 12px;
border-right: 1px solid var(--line);
border-bottom: 1px solid var(--line);
min-width: 0;
}
.preset-detail-list div:nth-child(2n) {
border-right: 0;
}
.preset-detail-list dt {
color: var(--muted);
font-size: 11px;
font-weight: 800;
}
.preset-detail-list dd {
margin: 0;
font-size: 14px;
font-weight: 850;
line-height: 1.3;
overflow-wrap: anywhere;
}
.preset-path-block {
display: grid;
gap: 8px;
margin-top: 14px;
}
.preset-path-block > span {
color: var(--muted);
font-size: 11px;
font-weight: 800;
}
.preset-manifest-path {
display: block;
max-width: 100%;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
border: 1px solid var(--line);
background: var(--paper-2);
border-radius: 8px;
padding: 8px;
color: var(--muted);
font-size: 11px;
line-height: 1.45;
}
.preset-command-list {
display: grid;
gap: 8px;
margin-top: 14px;
}
.preset-command-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
border: 1px solid var(--line);
background: var(--paper-2);
border-radius: 8px;
padding: 8px;
min-width: 0;
}
.preset-command-row code {
white-space: normal;
overflow-wrap: anywhere;
}
.preset-command-warning,
.preset-confirm-warning {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--paper-2);
color: var(--muted);
font-size: 12px;
line-height: 1.45;
padding: 10px;
}
.preset-confirm-warning {
color: #b91c1c;
}
.preset-layer-list {
display: grid;
gap: 8px;
}
.preset-layer-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--paper);
padding: 10px;
}
.preset-layer-row.active {
border-color: var(--accent);
background: var(--paper-2);
}
.preset-layer-rank {
display: inline-grid;
place-items: center;
width: 24px;
height: 24px;
border-radius: 999px;
background: var(--paper-2);
color: var(--muted);
font-size: 12px;
font-weight: 900;
}
.preset-layer-row strong,
.preset-layer-row small {
display: block;
overflow-wrap: anywhere;
}
.preset-layer-row small {
color: var(--muted);
font-size: 11px;
margin-top: 2px;
}
.preset-roots {
display: grid;
gap: 10px;
margin-top: 14px;
}
.preset-roots div {
display: grid;
gap: 6px;
min-width: 0;
}
.preset-roots strong {
font-size: 12px;
}
.preset-action-panel {
display: grid;
gap: 12px;
}
.preset-action-group {
display: grid;
gap: 10px;
border-top: 1px solid var(--line);
padding-top: 14px;
min-width: 0;
}
.preset-action-group h4 {
margin: 0;
font-size: 13px;
font-weight: 850;
}
.preset-action-group label {
display: grid;
gap: 6px;
font-size: 12px;
font-weight: 800;
color: var(--muted);
}
.preset-action-group input,
.preset-action-group select {
width: 100%;
min-width: 0;
}
.preset-action-group .check-row {
display: flex;
align-items: center;
gap: 8px;
}
.preset-action-group .check-row input {
width: auto;
}
.preset-confirm-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: end;
}
.preset-action-group.danger button:not(:disabled):last-child {
border-color: #b91c1c;
color: #b91c1c;
}
@media (max-width: 1400px) {
.preset-workspace {
grid-template-columns: minmax(280px, 0.9fr) minmax(0, 1.1fr);
}
.preset-context-actions {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
}
@media (max-width: 760px) {
.preset-toolbar,
.preset-priority-strip,
.preset-workspace,
.preset-context-actions,
.preset-detail-hero,
.preset-detail-list,
.preset-command-row,
.preset-confirm-row {
grid-template-columns: 1fr;
}
.preset-source-tabs,
.preset-card-tools,
.preset-detail-badges {
justify-content: flex-start;
}
.preset-detail-list div,
.preset-detail-list div:nth-child(2n) {
border-right: 0;
}
}
# {{TASK_TITLE}} - Task Package Index
Task Contract: harness-task/v1
## Task Identity
| Field | Value |
| --- | --- |
| Task ID | `{{TASK_ID}}` |
| Budget | `{{TASK_BUDGET}}` |
| Preset | `{{TASK_PRESET}}` |
| Module | `{{TASK_MODULE}}` |
| Long-running | `{{TASK_LONG_RUNNING}}` |
| Created | {{DATE}} |
## Task Audit Metadata
| Field | Value |
| --- | --- |
| Created By | {{TASK_AUDIT_CREATED_BY}} |
| Created At | {{TASK_AUDIT_CREATED_AT}} |
| Command Shape | {{TASK_AUDIT_COMMAND_SHAPE}} |
| Budget | {{TASK_AUDIT_BUDGET}} |
| Template Source | {{TASK_AUDIT_TEMPLATE_SOURCE}} |
| Task Creator | {{TASK_AUDIT_TASK_CREATOR}} |
| Task Creator Source | {{TASK_AUDIT_TASK_CREATOR_SOURCE}} |
| Human Review Status | {{TASK_AUDIT_HUMAN_REVIEW_STATUS}} |
| Confirmation ID | {{TASK_AUDIT_CONFIRMATION_ID}} |
| Confirmed At | {{TASK_AUDIT_CONFIRMED_AT}} |
| Reviewer | {{TASK_AUDIT_REVIEWER}} |
| Reviewer Email | {{TASK_AUDIT_REVIEWER_EMAIL}} |
| Confirm Text | {{TASK_AUDIT_CONFIRM_TEXT}} |
| Evidence Checked | {{TASK_AUDIT_EVIDENCE_CHECKED}} |
| Review Commit SHA | {{TASK_AUDIT_REVIEW_COMMIT_SHA}} |
| Audit Source | {{TASK_AUDIT_AUDIT_SOURCE}} |
| Audit Status | {{TASK_AUDIT_AUDIT_STATUS}} |
| Exception Reason | {{TASK_AUDIT_EXCEPTION_REASON}} |
| Message | {{TASK_AUDIT_MESSAGE}} |
| Migration Status | {{TASK_AUDIT_MIGRATION_STATUS}} |
| Migrated From | {{TASK_AUDIT_MIGRATED_FROM}} |
| Legacy Extra Fields | {{TASK_AUDIT_LEGACY_EXTRA_FIELDS}} |
| Migration Notes | {{TASK_AUDIT_MIGRATION_NOTES}} |
## Core Contract Files
| File | Purpose |
| --- | --- |
| `brief.md` | Human-readable task summary and context entry. |
| `task_plan.md` | Current task goal, scope, selected budget, acceptance, and operating decisions. |
| `visual_map.md` | Phase map, evidence status, next lifecycle commands, and supporting diagrams. |
| `progress.md` | Execution log, verification evidence, decisions, and handoff notes. |
## Standard Task Files
These files exist for standard and complex tasks.
| File | Purpose |
| --- | --- |
| `execution_strategy.md` | Execution mode, ownership, conflict control, and evidence strategy. |
| `findings.md` | Findings, research notes, accepted risks, and unresolved questions. |
| `lesson_candidates.md` | Task-local lesson candidate decisions before closeout. |
| `review.md` | Agent review submission, adversarial review, findings, evidence, and routing. |
## Optional Indexes
| Index | Purpose |
| --- | --- |
| `references/INDEX.md` | References and preset-provided required reads. |
| `artifacts/INDEX.md` | Generated outputs, evidence bundles, screenshots, reports, and command artifacts. |
## Preset Summary
This section is system-rendered. Presets may not add custom root-level files or arbitrary root `INDEX.md` content.
| Field | Value |
| --- | --- |
| Preset | `{{TASK_PRESET}}` |
| Preset Version | `{{TASK_PRESET_VERSION}}` |
| Evidence Bundle | `{{TASK_EVIDENCE_BUNDLE}}` |
| Resource Indexes | `references/INDEX.md`; `artifacts/INDEX.md` |
## Update Rules
- Update status and decisions in `progress.md`.
- Keep task-specific goals and acceptance in `task_plan.md`.
- Put large command output, screenshots, reports, and generated files in `artifacts/INDEX.md`.
- Put source material, external links, and preset required reads in `references/INDEX.md`.
# [Task Name] - Visual Map
Visual Map Contract: v1.0
This file is the task's diagram collection. It is not only a phase roadmap.
Include only diagrams that materially help a human or agent understand the task.
## Map Index
| ID | Type | Purpose | Required For Understanding | Source Evidence | Promotion Candidate |
| --- | --- | --- | --- | --- | --- |
| MAP-01 | phase | Show the execution phases and dependencies | yes | `task_plan.md` | no |
## Phase Graph
```mermaid
flowchart LR
INIT01["INIT-01 Scope and Context\nkind=init"] --> EXEC01["EXEC-01 Implementation Slice\nkind=execution"]
EXEC01 --> GATE01["GATE-01 Direct Completion\nkind=gate"]
```
## Phase Table
| Phase ID | Kind | Depends On | State | Completion | Output | Required Evidence | Exit Command | Actor | Evidence Status | Blocking Risk | Owner / Handoff |
| --- | --- | --- | --- | ---: | --- | --- | --- | --- | --- | --- | --- |
| INIT-01 | init | none | planned | 0 | Task scope is clear enough to execute | `task_plan.md` | `harness task-start {{TASK_ID}}` | agent | missing | none | coordinator |
| EXEC-01 | execution | INIT-01 | planned | 0 | Simple implementation or documentation change is complete | diff, command, or artifact path | `harness task-phase {{TASK_ID}} EXEC-01 --state done --completion 100 --evidence present` | agent | missing | [risk] | [owner] |
| GATE-01 | gate | EXEC-01 | planned | 0 | Direct task completion | progress update and final evidence note | `harness task-complete {{TASK_ID}} --message "<summary>"` | agent | missing | [risk] | coordinator |
Allowed Kind: init, execution, gate.
Allowed Actor: agent, human, coordinator.
Allowed Evidence Status: missing, partial, present, waived.
Dashboard implementation completion is computed from non-skipped `execution` phases only. `init` and `gate` phases route lifecycle readiness and next actions; they must not make implementation progress look incomplete.
## Supporting Maps
Add optional diagrams only when useful:
- architecture: module, component, or service structure.
- sequence: frontend/backend/service/database/agent interaction.
- data-flow: data movement and ownership.
- state: state machine or lifecycle.
- topology: repo, service, worker, or worktree layout.
- decision: branch and tradeoff tree.
## Map Notes
- Use `missing` when no evidence has been checked.
- Use `partial` when some evidence exists but required checks remain.
- Use `present` when the phase has sufficient evidence for its current claim.
- Use `waived` only when the reason and owner are recorded in `progress.md`.
+7
-0
# Changelog
## 1.0.5
- Relicense the public package from MIT to AGPL-3.0-or-later.
- Add an additional permission for Generated Harness Materials so target
project files generated or installed by Coding Agent Harness can follow the
target project's own license terms.
## 1.0.4

@@ -4,0 +11,0 @@

+1
-1

@@ -228,3 +228,3 @@ # Architecture Overview

and Soft-deleted / Superseded. Agent-authored submissions can request review,
but only a strict `Human Review Confirmation` block marks the task as
but only committed Task Audit Metadata in `INDEX.md` marks the task as
`confirmed`.

@@ -231,0 +231,0 @@

@@ -205,3 +205,3 @@ # 架构总览

Review 队列只展示已经提交审查材料包、并且可以等待人工确认的任务。缺少审查提交、证据不完整、仍有 Lesson 路由、存在阻塞发现,或历史收口债务的任务,会进入独立的生命周期队列:Missing Materials、Blocked、Lessons、Confirmed / Finalized、Soft-deleted / Superseded。Agent 写入的提交只能请求审查;只有严格的 `Human Review Confirmation` 块存在时,任务才是 `confirmed`。
Review 队列只展示已经提交审查材料包、并且可以等待人工确认的任务。缺少审查提交、证据不完整、仍有 Lesson 路由、存在阻塞发现,或历史收口债务的任务,会进入独立的生命周期队列:Missing Materials、Blocked、Lessons、Confirmed / Finalized、Soft-deleted / Superseded。Agent 写入的提交只能请求审查;只有 `INDEX.md` 中已提交的 Task Audit Metadata 才能把任务标记为 `confirmed`。

@@ -208,0 +208,0 @@ ## 迁移轨道

@@ -188,3 +188,3 @@ # Agent Installation Guide

```bash
harness new-task phase-2-lifecycle \
harness new-task \
--title "Phase 2 task lifecycle" \

@@ -194,7 +194,7 @@ --locale en-US \

harness task-start phase-2-lifecycle \
harness task-start <task-id-from-new-task-output> \
--message "Start lifecycle slice implementation" \
/path/to/project
harness task-log phase-2-lifecycle \
harness task-log <task-id-from-new-task-output> \
--message "Completed CLI and template updates" \

@@ -204,8 +204,8 @@ --evidence "command:TARGET:npm-test:passed" \

harness review-confirm TASKS/phase-2-lifecycle \
harness review-confirm <task-id-from-new-task-output> \
--reviewer "Human Reviewer" \
--confirm phase-2-lifecycle \
--confirm <task-id-from-new-task-output> \
/path/to/project
harness task-complete phase-2-lifecycle \
harness task-complete <task-id-from-new-task-output> \
--message "Verification loop completed" \

@@ -218,2 +218,3 @@ /path/to/project

- Do not manually copy task templates or create partial task folders. `harness check` enforces the file set created by `new-task`.
- `new-task --title "..."` generates a default ID like `YYYY-MM-DD-phase-2-task-lifecycle-a1b2c3d4` to reduce collisions when multiple people or agents create tasks in the same repository. Pass an explicit `<task-id>` only when a coordinator needs a stable compatibility ID.
- `new-task --budget simple` creates `brief.md`, `task_plan.md`, `visual_map.md`, and `progress.md`.

@@ -225,3 +226,3 @@ - `new-task` defaults to `standard` and creates the simple files plus `execution_strategy.md`, `findings.md`, `lesson_candidates.md`, and `review.md`.

- `task-log` only appends execution records. Evidence uses `type:PATH:summary`, for example `command:TARGET:npm-test:passed`.
- `review-confirm` appends a human review confirmation to `review.md` and a log entry to `progress.md`. It must reject open P0/P1/P2 findings marked `Open: yes` or `Blocks Release: yes`.
- `review-confirm` writes human review confirmation audit fields to the task `INDEX.md` through gated commits. It must reject open P0/P1/P2 findings marked `Open: yes` or `Blocks Release: yes`.
- CLI-owned lifecycle and lesson commands auto-commit allowlisted writes in a clean Git root. Dirty state appears in `status` / dashboard warnings and blocks those mechanical commits. Agent-owned manual edits still need proactive commits; deferred commits must record the no-commit reason, owner, and next step.

@@ -228,0 +229,0 @@ - `status --json` keeps old `task.state` for compatibility and adds `lifecycleState`, `reviewStatus`, `closeoutStatus`, and `stateConflicts`. `done` means implementation finished; it does not mean `closed`.

@@ -207,3 +207,3 @@ # Agent 安装指南

```bash
harness new-task phase-2-lifecycle \
harness new-task \
--title "阶段二任务生命周期" \

@@ -213,7 +213,7 @@ --locale zh-CN \

harness task-start phase-2-lifecycle \
harness task-start <new-task 输出的 task-id> \
--message "开始实现生命周期切片" \
/path/to/project
harness task-log phase-2-lifecycle \
harness task-log <new-task 输出的 task-id> \
--message "完成 CLI 与模板更新" \

@@ -223,8 +223,8 @@ --evidence "command:TARGET:npm-test:passed" \

harness review-confirm TASKS/phase-2-lifecycle \
harness review-confirm <new-task 输出的 task-id> \
--reviewer "Human Reviewer" \
--confirm phase-2-lifecycle \
--confirm <new-task 输出的 task-id> \
/path/to/project
harness task-complete phase-2-lifecycle \
harness task-complete <new-task 输出的 task-id> \
--message "验证闭环完成" \

@@ -238,2 +238,4 @@ /path/to/project

`new-task` 创建的预算文件集校验。
- `new-task --title "..."` 默认生成类似 `YYYY-MM-DD-phase-2-task-lifecycle-a1b2c3d4`
的任务 ID,避免多人或多 agent 同仓协作时重名;只有需要固定兼容 ID 时才传显式 `<task-id>`。
- `new-task --budget simple` 创建 `brief.md`、`task_plan.md`、`visual_map.md`

@@ -249,3 +251,3 @@ 和 `progress.md`。

`command:TARGET:npm-test:passed`。
- `review-confirm` 会向 `review.md` 追加人工审查确认,并向 `progress.md` 追加日志;如果存在 `Open: yes` 或 `Blocks Release: yes` 的开放 P0/P1/P2 finding,必须拒绝确认。
- `review-confirm` 会通过受控提交把人工确认审计字段写入任务 `INDEX.md`;如果存在 `Open: yes` 或 `Blocks Release: yes` 的开放 P0/P1/P2 finding,必须拒绝确认。
- CLI-owned lifecycle 和 lesson 命令会在干净 Git root 中自动提交 allowlisted 写入;dirty 状态会出现在 `status` / dashboard 的警告里,并阻塞这些机械化提交。Agent 手工改动仍要主动提交,不能提交时记录 no-commit reason、owner 和下一步。

@@ -252,0 +254,0 @@ - `status --json` 保留旧 `task.state` 用于兼容,并新增 `lifecycleState`、`reviewStatus`、`closeoutStatus` 和 `stateConflicts`。`done` 只表示实现完成,不等于 `closed`。

@@ -31,2 +31,25 @@ # Preset Development

## Dashboard Management
The Dashboard exposes a Presets view for the target project. Static dashboards
show a read-only catalog of discovered project, user, and bundled presets,
including source, purpose, compatible budgets, task kind, manifest path, and
resource counts.
Use the local dynamic Workbench when you want to manage presets from the web UI:
```bash
harness dev /path/to/project
```
In Workbench mode, the Presets view can check presets, install a local preset
directory, `.zip` archive, or bundled preset id into the project or user scope,
seed bundled presets into either scope, and uninstall project/user presets.
Bundled package presets are immutable from the Dashboard: they can be inspected,
checked, and used as install or seed sources, but not edited or deleted.
The CLI and filesystem remain canonical. The Dashboard calls the same preset
registry operations as `harness preset ...`; it does not store independent preset
state.
## Package Layout

@@ -188,2 +211,3 @@

harness preset install ./my-preset
harness preset install ./my-preset.zip
harness preset install ./my-preset --project /path/to/project

@@ -195,3 +219,3 @@ harness preset install legacy-migration --force

harness preset inspect custom-review --json /path/to/project
harness new-task custom-review-task --preset custom-review --subject "API contracts" /path/to/project
harness new-task --title "Custom review task" --preset custom-review --subject "API contracts" /path/to/project
harness preset uninstall custom-review

@@ -205,3 +229,3 @@ ```

1. Run `harness preset check ./my-preset`.
2. Install into an isolated HOME or disposable environment.
2. Install the folder and, if distributing an archive, install the `.zip` into an isolated HOME or disposable environment.
3. Create at least one task with `harness new-task --preset`.

@@ -208,0 +232,0 @@ 4. For reference bundles, create two different tasks from the same preset and verify both contain the same shared `references/` files and independent audit/evidence bundles.

@@ -8,3 +8,4 @@ # Task State Machine And Lifecycle Queues

- `progress.md` stores raw `task.state` and execution evidence.
- `review.md` stores Agent Review Submission, material findings, and Human Review Confirmation.
- `review.md` stores Agent Review Submission, material findings, and review evidence.
- `INDEX.md` stores Task Audit Metadata, including task creation and human review confirmation audit fields.
- `lesson_candidates.md` records lesson candidate decisions and sedimentation routing.

@@ -36,4 +37,19 @@ - `10-WALKTHROUGH/Closeout-SSoT.md` records closeout status and links walkthrough evidence.

`task-review` means the agent submitted a review packet. It does not mean human approval. `review-confirm` is the Human Review Confirmation gate. `task-complete` / closeout is not a substitute for review confirmation.
`task-review` means the agent submitted a review packet. It does not mean human approval. `review-confirm` is the human confirmation gate and writes its audit fields to `INDEX.md`. `task-complete` / closeout is not a substitute for review confirmation.
## Phase Kind Map
`visual_map.md` is the machine-readable phase timeline. New phase tables may include `Kind`, `Exit Command`, and `Actor` columns:
| Kind | Purpose | Counts Toward Implementation Completion | Typical Exit |
| --- | --- | --- | --- |
| `init` | Scope, context, budget, and execution strategy. | no | `harness task-start <task-id>` |
| `execution` | Implementation, documentation, and verification slices. | yes | `harness task-phase <task-id> <phase-id> --state done --completion 100 --evidence present` |
| `gate` | Agent review submission, human confirmation, lesson routing, walkthrough, and closeout. | no | `harness task-review`, `harness review-confirm`, or `harness task-complete` |
Older phase tables without `Kind` remain valid and are treated as `execution`.
The Dashboard implementation score uses non-skipped `execution` phases only.
Gate phases explain the next lifecycle action and owner; they do not make a finished implementation look incomplete.
Agents may run `Exit Command` values with `Actor: agent`. `Actor: human` gates, especially `review-confirm`, require explicit human action.
## Derived State

@@ -44,3 +60,4 @@

Progress["progress.md<br/>task.state + evidence"]
Review["review.md<br/>Agent Review Submission + findings + Human Review Confirmation"]
Index["INDEX.md<br/>Task Audit Metadata"]
Review["review.md<br/>Agent Review Submission + findings + evidence"]
Lessons["lesson_candidates.md<br/>decision + sedimentation route"]

@@ -52,2 +69,3 @@ Closeout["Closeout-SSoT.md<br/>closeout row + walkthrough"]

Progress --> Scanner
Index --> Scanner
Review --> Scanner

@@ -69,3 +87,3 @@ Lessons --> Scanner

| `task.state` | `progress.md` | Raw execution stage. |
| `reviewStatus` | `review.md` + findings + Human Review Confirmation | Separates missing review, agent-submitted review, blockers, and human confirmation. |
| `reviewStatus` | `INDEX.md` Task Audit Metadata + `review.md` findings/submission | Separates missing review, agent-submitted review, blockers, and human confirmation. |
| `closeoutStatus` | `Closeout-SSoT.md` | Separates missing, pending, and closed closeout. |

@@ -84,5 +102,5 @@ | `lifecycleState` | scanner-derived | Main Dashboard lifecycle meaning. |

| Standard / complex task is missing required files, sections, evidence, lesson decision, or review submission | `missing-materials` | Needs agent repair; not part of the human review queue. |
| `task-review` was submitted, materials are ready, and Human Review Confirmation is missing | `review-submitted` | Truly waiting for human review. |
| Human Review Confirmation exists, but closeout / ledger / lessons are not fully closed | `confirmed-finalization-pending` | Accountability moved to the reviewer, but governance closeout remains. |
| Human Review Confirmation exists, and closeout / ledger / lesson routing are complete | `finalized` | Truly complete and traceable. |
| `task-review` was submitted, materials are ready, and `INDEX.md` does not show human confirmation | `review-submitted` | Truly waiting for human review. |
| `INDEX.md` shows human confirmation, but closeout / ledger / lessons are not fully closed | `confirmed-finalization-pending` | Accountability moved to the reviewer, but governance closeout remains. |
| `INDEX.md` shows human confirmation, and closeout / ledger / lesson routing are complete | `finalized` | Truly complete and traceable. |
| `task.state = blocked` without a review blocker | `active-blocked` | Execution is blocked. |

@@ -100,5 +118,5 @@ | `task.state = in_progress` | `active` | Work is active. |

| `blocked-open-findings` | There is an open P0-P2 finding or a finding that blocks release / confirmation. |
| `confirmed` | `Human Review Confirmation` exists. |
| `confirmed` | `INDEX.md` Task Audit Metadata has `Human Review Status: confirmed` and committed audit fields. |
Agent self-review, subagent review, and coordinator review can only move a task toward `submitted`. Only `review-confirm` or an explicit Dashboard Workbench human confirmation writes `Human Review Confirmation`.
Agent self-review, subagent review, and coordinator review can only move a task toward `submitted`. Only `review-confirm` or an explicit Dashboard Workbench human confirmation writes the confirmation audit fields in `INDEX.md`.

@@ -177,5 +195,4 @@ ## Lifecycle Queues

Lifecycle->>Lifecycle: verify clean Git state, identity, hooks, allowlist
Lifecycle->>Docs: write Human Review Confirmation
Lifecycle->>Docs: append review-confirm log
Lifecycle->>Lifecycle: commit allowlisted review/progress files
Lifecycle->>Docs: write confirmation fields to INDEX.md
Lifecycle->>Lifecycle: commit allowlisted INDEX.md
Lifecycle->>Docs: record confirmation commit SHA + committed audit status

@@ -187,3 +204,3 @@ API->>Scanner: regenerate dashboard snapshot

Strict rule: an agent can prepare review evidence and submit the task for review, but the task is not human-confirmed until the Human Review Confirmation block exists. Confirmation must use gated auto-commit: the CLI and Workbench reject dirty Git state, missing commit identity, hook/preflight failure, or writes outside the current task `review.md` / `progress.md` allowlist, and return recovery guidance.
Strict rule: an agent can prepare review evidence and submit the task for review, but the task is not human-confirmed until the task `INDEX.md` contains committed confirmation audit fields. Confirmation must use gated auto-commit: the CLI and Workbench reject dirty Git state, missing commit identity, hook/preflight failure, or writes outside the current task `INDEX.md` allowlist, and return recovery guidance.

@@ -190,0 +207,0 @@ CLI-owned mechanical writes and agent-owned manual slices have different boundaries. `new-task`, `task-*`, `task-phase`, `module-step`, `review-confirm`, `lesson-sediment`, and `lesson-promote --apply` acquire a lock, restrict writes to an allowlist, and auto-commit in a clean Git root. When an agent manually edits code, templates, or task docs, it still needs to proactively commit after verification; if it cannot commit, it must record the no-commit reason, owner, and next step, and must not mix unrelated dirty changes into the task commit.

@@ -8,3 +8,4 @@ # 任务状态机与生命周期队列

- `progress.md` 记录原始 `task.state` 和执行证据。
- `review.md` 记录 Agent Review Submission、material findings 和 Human Review Confirmation。
- `review.md` 记录 Agent Review Submission、material findings 和审查证据。
- `INDEX.md` 记录任务审计元数据,包括任务创建和人工确认审计字段。
- `lesson_candidates.md` 记录 lesson candidate 的人工判定和后续沉淀路由。

@@ -36,4 +37,19 @@ - `10-WALKTHROUGH/Closeout-SSoT.md` 记录任务是否完成收口,并链接 walkthrough。

`task-review` 表示 Agent 提交审查材料包,不表示人工批准。`review-confirm` 才表示 Human Review Confirmation。`task-complete` / closeout 也不是 review confirmation 的替代品。
`task-review` 表示 Agent 提交审查材料包,不表示人工批准。`review-confirm` 才表示人工确认门禁,并把审计字段写入 `INDEX.md`。`task-complete` / closeout 也不是 review confirmation 的替代品。
## 阶段类型地图
`visual_map.md` 是机器可读的阶段时间线。新的阶段表可以包含 `Kind`、`Exit Command` 和 `Actor` 三列:
| Kind | 作用 | 是否计入实现完成度 | 典型出口 |
| --- | --- | --- | --- |
| `init` | 范围、上下文、预算和执行策略。 | 否 | `harness task-start <task-id>` |
| `execution` | 实现、文档和验证切片。 | 是 | `harness task-phase <task-id> <phase-id> --state done --completion 100 --evidence present` |
| `gate` | Agent 提交审查、人工确认、lesson routing、walkthrough 和 closeout。 | 否 | `harness task-review`、`harness review-confirm` 或 `harness task-complete` |
旧阶段表没有 `Kind` 也继续有效,默认按 `execution` 处理。
Dashboard 实现完成度只计算非 skipped 的 `execution` 阶段。
Gate 阶段用于解释下一步生命周期动作和责任人,不会让已完成的实现看起来未完成。
Agent 只能执行 `Actor: agent` 的 `Exit Command`。`Actor: human` 的 gate,尤其是 `review-confirm`,必须由人工明确执行。
## 派生状态

@@ -44,3 +60,4 @@

Progress["progress.md<br/>task.state + evidence"]
Review["review.md<br/>Agent Review Submission + findings + Human Review Confirmation"]
Index["INDEX.md<br/>Task Audit Metadata"]
Review["review.md<br/>Agent Review Submission + findings + evidence"]
Lessons["lesson_candidates.md<br/>decision + sedimentation route"]

@@ -52,2 +69,3 @@ Closeout["Closeout-SSoT.md<br/>closeout row + walkthrough"]

Progress --> Scanner
Index --> Scanner
Review --> Scanner

@@ -69,3 +87,3 @@ Lessons --> Scanner

| `task.state` | `progress.md` | 原始执行阶段。 |
| `reviewStatus` | `review.md` + findings + Human Review Confirmation | 区分缺审查、Agent 已提交审查、阻塞、人工确认。 |
| `reviewStatus` | `INDEX.md` Task Audit Metadata + `review.md` findings/submission | 区分缺审查、Agent 已提交审查、阻塞、人工确认。 |
| `closeoutStatus` | `Closeout-SSoT.md` | 区分收口缺失、待处理、已关闭。 |

@@ -84,5 +102,5 @@ | `lifecycleState` | scanner 派生 | Dashboard 的主生命周期语义。 |

| 标准/复杂任务缺必需文件、章节、证据、lesson decision 或 review submission | `missing-materials` | 需要 Agent 补材料,不属于人审队列。 |
| 已执行 `task-review`,材料齐全,且未 Human Review Confirmation | `review-submitted` | 真正等待人审。 |
| 已 Human Review Confirmation,但 closeout / ledger / lessons 仍未全部收口 | `confirmed-finalization-pending` | 责任已转移给确认人,但治理收口仍待完成。 |
| 已 Human Review Confirmation,且 closeout / ledger / lesson routing 完成 | `finalized` | 真正完成,可只读追溯。 |
| 已执行 `task-review`,材料齐全,且 `INDEX.md` 尚未显示人工确认 | `review-submitted` | 真正等待人审。 |
| `INDEX.md` 已显示人工确认,但 closeout / ledger / lessons 仍未全部收口 | `confirmed-finalization-pending` | 责任已转移给确认人,但治理收口仍待完成。 |
| `INDEX.md` 已显示人工确认,且 closeout / ledger / lesson routing 完成 | `finalized` | 真正完成,可只读追溯。 |
| `task.state = blocked` 但没有 review blocker | `active-blocked` | 执行阻塞。 |

@@ -100,5 +118,5 @@ | `task.state = in_progress` | `active` | 执行中。 |

| `blocked-open-findings` | 有 open P0-P2 finding,或 finding 阻塞发布 / 确认。 |
| `confirmed` | 已写入 `Human Review Confirmation`。 |
| `confirmed` | `INDEX.md` Task Audit Metadata 包含 `Human Review Status: confirmed` 和已提交审计字段。 |
Agent 自查、subagent 审查和 coordinator 审查都只能让任务接近 `submitted`。只有 `review-confirm` 或 Workbench 的明确人工确认动作会写入 `Human Review Confirmation`。
Agent 自查、subagent 审查和 coordinator 审查都只能让任务接近 `submitted`。只有 `review-confirm` 或 Workbench 的明确人工确认动作会把确认审计字段写入 `INDEX.md`。

@@ -184,5 +202,4 @@ ## 生命周期队列

Lifecycle->>Lifecycle: verify Git clean, identity, hooks, allowlist
Lifecycle->>Docs: write Human Review Confirmation
Lifecycle->>Docs: append review-confirm log
Lifecycle->>Lifecycle: commit allowlisted review/progress files
Lifecycle->>Docs: write confirmation fields to INDEX.md
Lifecycle->>Lifecycle: commit allowlisted INDEX.md
Lifecycle->>Docs: record confirmation commit SHA + committed audit status

@@ -194,3 +211,3 @@ API->>Scanner: regenerate dashboard snapshot

严格规则:Agent 可以准备 review evidence,也可以提交审查;但任务只有在 Human Review Confirmation block 存在后,才算人工确认。确认动作必须通过 gated auto-commit:Git 状态不干净、提交身份缺失、hook/preflight 失败,或待写文件超出当前任务 `review.md` / `progress.md` 白名单时,CLI 和 Workbench 都会拒绝并返回恢复建议。
严格规则:Agent 可以准备 review evidence,也可以提交审查;但任务只有在任务 `INDEX.md` 包含已提交的确认审计字段后,才算人工确认。确认动作必须通过 gated auto-commit:Git 状态不干净、提交身份缺失、hook/preflight 失败,或待写文件超出当前任务 `INDEX.md` 白名单时,CLI 和 Workbench 都会拒绝并返回恢复建议。

@@ -197,0 +214,0 @@ CLI-owned 机械写入和 agent-owned 手工切片是两条边界。`new-task`、`task-*`、`task-phase`、`module-step`、`review-confirm`、`lesson-sediment` 和 `lesson-promote --apply` 会在干净 Git root 中加锁、限制写入范围并自动提交。Agent 手工编辑代码、模板或任务文档时仍要在验证后主动提交;无法提交时必须记录 no-commit reason、owner 和下一步,不能把 unrelated dirty changes 混入任务提交。

+657
-17

@@ -1,21 +0,661 @@

MIT License
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (c) 2026 ZeyuLi (FairladyZ)
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Preamble
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
{
"name": "coding-agent-harness",
"version": "1.0.4",
"version": "1.0.5",
"description": "Document governance kernel for long-running coding agents.",

@@ -47,2 +47,3 @@ "type": "module",

"LICENSE",
"LICENSE-EXCEPTION.md",
"references/",

@@ -60,3 +61,3 @@ "skills/",

},
"license": "MIT"
"license": "AGPL-3.0-or-later"
}

@@ -70,2 +70,22 @@ # Coding Agent Harness

### Reusable Presets For Task Families
A preset is a versioned, declarative task method package. It does not install a
new agent and it does not replace the Harness. It tells `harness new-task` how
to create a task for a repeatable work type: what task kind to set, which inputs
to ask for, which shared references or artifacts to copy into the task, which
files the agent must read first, and what audit/evidence files should prove the
task was created correctly.
Use presets when a group of tasks share the same setup. For example, several
interface tasks may all depend on the same upstream microservice contract,
fixture packet, runbook, and verification checklist. Instead of repeating that
context in every prompt, put it in a preset and create each task with
`harness new-task --preset <preset-id> ...`.
Harness ships bundled presets, `harness init` seeds them into the target project,
and teams can add project-local presets under `.coding-agent-harness/presets/`.
The `preset-creator` Skill is for authoring these preset packages; the Harness
CLI is what checks, installs, lists, and applies them.
### Safe Migration For Existing Projects

@@ -161,2 +181,6 @@

The Workbench includes a Presets view for checking, installing, seeding, and
uninstalling project or user presets. Static dashboards show the same preset
catalog as read-only evidence.
Generate a static Dashboard that can be opened offline:

@@ -286,2 +310,4 @@

- Architecture overview: [`docs-release/architecture/overview.md`](docs-release/architecture/overview.md)
- Deep architecture explainer (zh-CN): [`docs-release/architecture/system-explainer/`](docs-release/architecture/system-explainer/) — system design, module dependencies, task lifecycle, check system, data flow, and preset/migration engine with design rationale
- Deep architecture explainer (en-US): [`docs-release/architecture/system-explainer/en-US/`](docs-release/architecture/system-explainer/en-US/)

@@ -294,2 +320,8 @@ ## Star History

MIT
AGPL-3.0-or-later with an additional permission for Generated Harness
Materials.
See [`LICENSE`](LICENSE) and [`LICENSE-EXCEPTION.md`](LICENSE-EXCEPTION.md).
The additional permission keeps files generated or installed into a target
project from becoming AGPL-covered solely because Coding Agent Harness created
or updated them.

@@ -70,2 +70,18 @@ # Coding Agent Harness

### 面向任务族的可复用 Preset
Preset 是一个可版本化、声明式的任务方法包。它不是安装一个新 Agent,也不是替代
Harness;它告诉 `harness new-task` 如何为某一类可重复工作创建任务:应该设置什么
Task Kind、需要询问哪些输入、要把哪些共享 Reference 或 Artifact 复制进任务、Agent
必须先读哪些文件,以及要生成哪些 audit / evidence 文件来证明任务创建正确。
当一组任务共享同一套启动上下文时,就适合用 Preset。比如多个接口任务都依赖同一个
上游微服务 contract、fixture packet、runbook 和验证清单,就不应该每次都把这些内容塞进
prompt;应该把它们放进 Preset,然后用
`harness new-task --preset <preset-id> ...` 创建每个任务。
Harness 自带内置 Preset,`harness init` 会把它们 seed 到目标项目,团队也可以在
`.coding-agent-harness/presets/` 下维护项目级 Preset。`preset-creator` Skill 用来制作
这些 Preset 包;真正检查、安装、列出和应用 Preset 的是 Harness CLI。
### 旧项目也能迁移

@@ -284,2 +300,4 @@

- 架构说明:[`docs-release/architecture/overview.md`](docs-release/architecture/overview.md)
- 深度架构解析(中文):[`docs-release/architecture/system-explainer/`](docs-release/architecture/system-explainer/) — 系统设计、模块依赖、任务生命周期、检查体系、数据流、Preset 与迁移引擎,含设计决策背景
- Deep architecture explainer (en-US): [`docs-release/architecture/system-explainer/en-US/`](docs-release/architecture/system-explainer/en-US/)

@@ -292,2 +310,6 @@ ## Star History

MIT
AGPL-3.0-or-later,并附带 Generated Harness Materials 额外许可。
详见 [`LICENSE`](LICENSE) 和 [`LICENSE-EXCEPTION.md`](LICENSE-EXCEPTION.md)。
该额外许可确保 Harness 生成或安装到目标项目里的文件,不会仅仅因为由
Coding Agent Harness 创建或更新,就自动变成 AGPL 覆盖范围。

@@ -51,3 +51,3 @@ # Harness Ledger

```bash
harness new-task <task-id>
harness new-task --title "<title>"
harness task-start <task-id>

@@ -54,0 +54,0 @@ harness task-phase <task-id> <phase-id> --state done

@@ -6,4 +6,34 @@ import {

} from "../lib/harness-core.mjs";
import {
applyTaskAuditIndexMigration,
planTaskAuditIndexMigration,
} from "../lib/task-audit-migration.mjs";
export function runMigrationCommand(command, { args, takeFlag, takeOption, targetArg }) {
if (command === "migrate-task-audit-index") {
const json = takeFlag("--json");
const apply = takeFlag("--apply");
const planOnly = takeFlag("--plan");
try {
const result = apply && !planOnly
? applyTaskAuditIndexMigration(targetArg())
: planTaskAuditIndexMigration(targetArg());
if (json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(`Task audit INDEX migration ${result.result}: ${result.target}`);
console.log(`actions: ${result.summary.actions}`);
console.log(`legacy audit blocks: ${result.summary.legacyAuditBlocks}`);
console.log(`failures: ${result.summary.failures}`);
for (const action of result.actions || []) console.log(`- ${action.taskId}: ${action.legacyBlocks.join(", ")}`);
for (const failure of result.failures || []) console.error(`Failure: ${failure.taskId}: ${failure.failure}`);
}
process.exit(result.failures?.length ? 1 : 0);
} catch (error) {
if (json && error.plan) console.error(JSON.stringify(error.plan, null, 2));
else console.error(error.message);
process.exit(1);
}
}
if (command === "migrate-plan") {

@@ -10,0 +40,0 @@ const json = takeFlag("--json");

@@ -1,2 +0,1 @@

import fs from "node:fs";
import {

@@ -31,8 +30,3 @@ confirmTaskReview,

const parsed = parseNewTaskArgs(args, { preset, fromSession });
const taskId = parsed.taskId;
if (!taskId) {
console.error("Missing task id");
process.exit(2);
}
console.log(JSON.stringify(createTask(parsed.target, taskId, { title, locale, dryRun, moduleKey, budget, longRunning, preset, fromSession, presetArgs: parsed.presetArgs }), null, 2));
console.log(JSON.stringify(createTask(parsed.target, parsed.taskId, { title, locale, dryRun, moduleKey, budget, longRunning, preset, fromSession, presetArgs: parsed.presetArgs, automaticTaskId: parsed.automaticTaskId }), null, 2));
} catch (error) {

@@ -233,16 +227,11 @@ console.error(error.message);

function parseNewTaskArgs(args, { preset = "", fromSession = "" } = {}) {
function parseNewTaskArgs(args, { preset = "" } = {}) {
const values = [...args];
let taskId = "";
if (!fromSession) {
taskId = values.shift() || "";
} else if (values.length > 0 && !values[0].startsWith("-") && !(values.length === 1 && fs.existsSync(values[0]))) {
taskId = values.shift();
}
const presetPackage = preset ? readPresetPackageForNewTask(preset, values) : null;
if (!taskId && fromSession) taskId = presetPackage?.task?.defaultTaskId || "harness-v1-migration";
const parsed = splitPresetArgsAndTarget(values, presetPackage);
const parsed = splitPresetArgsAndPositionals(values, presetPackage);
const resolved = resolveNewTaskPositionals(parsed.positionals);
return {
taskId,
target: parsed.target || ".",
taskId: resolved.taskId,
target: resolved.target || ".",
automaticTaskId: !resolved.taskId,
presetArgs: parsed.presetArgs,

@@ -280,5 +269,5 @@ };

function splitPresetArgsAndTarget(values, presetPackage) {
function splitPresetArgsAndPositionals(values, presetPackage) {
const presetArgs = [];
const targetCandidates = [];
const positionals = [];
const declaredFlags = new Map(Object.values(presetPackage?.inputs || {}).filter((input) => input.flag).map((input) => [input.flag, input]));

@@ -301,10 +290,7 @@ for (let index = 0; index < values.length; index += 1) {

} else {
targetCandidates.push(value);
positionals.push(value);
}
}
if (targetCandidates.length > 1) {
throw new Error(`Too many positional arguments for new-task: ${targetCandidates.join(", ")}`);
}
return {
target: targetCandidates[0] || "",
positionals,
presetArgs,

@@ -314,2 +300,17 @@ };

function isPathLikePositional(value) {
return value === "." || value === ".." || value.startsWith("/") || value.startsWith("~/") || value.includes("/") || value.includes("\\");
}
function resolveNewTaskPositionals(positionals) {
if (positionals.length === 0) return { taskId: "", target: "" };
if (positionals.length === 1) {
const [value] = positionals;
if (isPathLikePositional(value)) return { taskId: "", target: value };
return { taskId: value, target: "" };
}
if (positionals.length === 2) return { taskId: positionals[0], target: positionals[1] };
throw new Error(`Too many positional arguments for new-task: ${positionals.join(", ")}`);
}
function formatTaskCommandError(error) {

@@ -316,0 +317,0 @@ const lines = [error.message];

@@ -90,2 +90,3 @@ #!/usr/bin/env node

harness migrate-plan [--json] [--limit n] [target]
harness migrate-task-audit-index [--plan] [--apply] [--json] [target]
harness migrate-run [--locale zh-CN|en-US] [--assume-locale] [--allow-dirty] [--plan-only] [--out-dir folder] [--session-dir folder] [target]

@@ -97,6 +98,6 @@ harness migrate-verify [--json] [--full-cutover] <session.json>

harness preset check <id> [--json] [target]
harness preset install <path-or-builtin-id> [--project] [--force] [--json] [target]
harness preset install <folder|zip|builtin-id> [--project] [--force] [--json] [target]
harness preset seed [--project] [--force] [--dry-run] [--json] [target]
harness preset uninstall <id> [--project] [--json] [target]
harness new-task <task-id> [--module key] [--budget simple|standard|complex] [--preset id] [--from-session session.json] [--long-running] [--title title] [--locale zh-CN|en-US] [--dry-run] [target]
harness new-task [task-id] [--module key] [--budget simple|standard|complex] [--preset id] [--from-session session.json] [--long-running] [--title title] [--locale zh-CN|en-US] [--dry-run] [target]
harness task-start <task-id> [--message text] [target]

@@ -132,2 +133,5 @@ harness task-phase <task-id> <phase-id> [--state done] [--completion 100] [--evidence present] [target]

Use "harness preset seed" to repair or re-run preset seeding.
Use "harness preset install" with a local preset folder, .zip archive, or
bundled preset id. Preset archives must contain preset.yaml at the archive
root or inside one top-level folder.
Use "harness preset list --json" to see available presets, their source,

@@ -230,3 +234,3 @@ purpose, compatible budgets, and manifest path. Use "harness preset inspect

}
} else if (["migrate-plan", "migrate-run", "migrate-verify"].includes(command)) {
} else if (["migrate-plan", "migrate-run", "migrate-verify", "migrate-task-audit-index"].includes(command)) {
runMigrationCommand(command, { args, takeFlag, takeOption, targetArg });

@@ -233,0 +237,0 @@ } else if (command === "governance") {

@@ -13,2 +13,3 @@ import fs from "node:fs";

readFileSafe,
readJsonSafe,
readBundledTemplate,

@@ -100,4 +101,5 @@ walkFiles,

try {
const raw = JSON.parse(fs.readFileSync(registryPath, "utf8"));
let readError = null;
const raw = readJsonSafe(registryPath, null, { onError: (error) => { readError = error; } });
if (raw) {
const locale = normalizeLocale(raw.locale);

@@ -112,5 +114,4 @@ const capabilities = Array.isArray(raw.capabilities)

return { mode: "declared-capability", path: registryPath, capabilities, raw, locale, errors: [] };
} catch (error) {
return { mode: "declared-capability", path: registryPath, capabilities: [], raw: null, errors: [error.message] };
}
return { mode: "declared-capability", path: registryPath, capabilities: [], raw: null, errors: [readError?.message || "invalid .harness-capabilities.json"] };
}

@@ -163,3 +164,3 @@

try {
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const manifest = readJsonSafe(manifestPath, null);
if (!Array.isArray(manifest) || manifest.length === 0) {

@@ -228,3 +229,3 @@ return [`dashboard asset manifest must list source files: ${manifestName}`];

try {
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"));
const pkg = readJsonSafe(path.join(repoRoot, "package.json"), {});
return pkg.version || "";

@@ -269,15 +270,10 @@ } catch {

function listPackageFiles() {
const files = [];
function walk(relativePath) {
const full = path.join(repoRoot, relativePath);
if (!fs.existsSync(full)) return;
const stat = fs.statSync(full);
if (stat.isDirectory()) {
for (const entry of fs.readdirSync(full)) walk(path.join(relativePath, entry));
return;
}
if (stat.isFile()) files.push(toPosix(relativePath));
}
for (const entry of skillPackageEntries()) walk(entry);
return files.sort();
return skillPackageEntries()
.flatMap((entry) => {
const fullPath = path.join(repoRoot, entry);
if (!fs.existsSync(fullPath)) return [];
if (fs.statSync(fullPath).isFile()) return [toPosix(path.relative(repoRoot, fullPath))];
return walkFiles(fullPath).map((file) => toPosix(path.relative(repoRoot, file)));
})
.sort();
}

@@ -333,3 +329,3 @@

try {
const pkg = JSON.parse(fs.readFileSync(path.join(targetRoot, "package.json"), "utf8"));
const pkg = readJsonSafe(path.join(targetRoot, "package.json"), {});
return pkg.version || "";

@@ -546,3 +542,3 @@ } catch {

if (!fs.existsSync(packagePath)) throw new Error("init --add-npm-scripts requires an existing package.json");
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
const pkg = readJsonSafe(packagePath, {});
const scripts = { ...(pkg.scripts || {}) };

@@ -549,0 +545,0 @@ const additions = {

import fs from "node:fs";
import path from "node:path";
import { walkFiles } from "./core-shared.mjs";

@@ -20,18 +21,10 @@ function stripMarkdownCode(value) {

if (!fs.existsSync(modulesRoot)) return [];
const results = [];
function walk(dir) {
for (const entry of fs.readdirSync(dir)) {
const full = path.join(dir, entry);
const relativePath = rel(path.relative(targetRoot, full));
const stat = fs.statSync(full);
if (stat.isDirectory()) {
if (relativePath.includes("/_archive/") || relativePath.endsWith("/_task-template")) continue;
walk(full);
} else if (/\/TASKS\/[^/]+\/task_plan\.md$/.test(relativePath)) {
results.push(relativePath);
}
}
}
walk(modulesRoot);
return results;
return walkFiles(modulesRoot, {
dirFilter: (_dirName, fullPath) => {
const relativePath = rel(path.relative(targetRoot, fullPath));
return !relativePath.includes("/_archive/") && !relativePath.endsWith("/_task-template");
},
})
.map((file) => rel(path.relative(targetRoot, file)))
.filter((relativePath) => /\/TASKS\/[^/]+\/task_plan\.md$/.test(relativePath));
}

@@ -38,0 +31,0 @@

@@ -25,17 +25,9 @@ import fs from "node:fs";

} from "./markdown-utils.mjs";
import { capabilityDefinitions, validateCapabilities } from "./capability-registry.mjs";
import { validateCapabilities } from "./capability-registry.mjs";
import { readPresetPackage } from "./preset-registry.mjs";
import { validateTaskPresetAuditSnapshot } from "./preset-audit-contracts.mjs";
import { validatePresetResourcesForTask } from "./preset-resource-contracts.mjs";
import {
collectTasks,
listTaskPlanPaths,
readVisualMapContractFile,
parsePhases,
taskCutoverCounters,
} from "./task-scanner.mjs";
import {
normalizeReviewBoolean,
reviewFindingColumns,
} from "./task-review-model.mjs";
import { collectTasks, listTaskPlanPaths, parseTaskBudget, readVisualMapContractFile, parsePhases } from "./task-scanner.mjs";
import { normalizeReviewBoolean, reviewFindingColumns } from "./task-review-model.mjs";
import { allowedPhaseActors, allowedPhaseKinds } from "./phase-kind.mjs";
import { validateTaskCompletionConsistency } from "./task-completion-consistency.mjs";

@@ -46,2 +38,3 @@ import { validatePlanContracts } from "./check-task-contracts.mjs";

import { summarizeGitState } from "./git-status-summary.mjs";
import { buildStatusData } from "./status-builder.mjs";
export { renderDashboard } from "./status-dashboard-renderer.mjs";

@@ -149,6 +142,6 @@

export function validateVisualMaps(target) {
export function validateVisualMaps(target, { taskPlanPaths } = {}) {
const failures = [];
const warnings = [];
for (const taskPlanPath of listTaskPlanPaths(target)) {
for (const taskPlanPath of taskPlanPaths || listTaskPlanPaths(target)) {
const taskDir = path.dirname(taskPlanPath);

@@ -167,3 +160,6 @@ const visualMapPath = path.join(taskDir, visualMapFile);

const phases = parsePhases(visualMap.content);
const budget = parseTaskBudget(taskPlan);
for (const phase of phases) {
if (!allowedPhaseKinds.has(phase.kind)) failures.push(`${relative} phase ${phase.id} invalid kind: ${phase.kind}`);
if (!allowedPhaseActors.has(phase.actor)) failures.push(`${relative} phase ${phase.id} invalid actor: ${phase.actor}`);
if (!allowedPhaseStates.has(phase.state)) failures.push(`${relative} phase ${phase.id} invalid state: ${phase.state}`);

@@ -183,2 +179,5 @@ if (!allowedEvidenceStatus.has(phase.evidenceStatus)) {

if (visualMap.source === "canonical" && phases.length === 0) warnings.push(`${relative} has no Visual Map phase table`);
if (visualMap.source === "canonical" && budget !== "simple" && phases.length > 0 && !phases.some((phase) => phase.kind === "execution" && phase.state !== "skipped")) {
failures.push(`${relative} requires at least one non-skipped execution phase`);
}
if (visualMap.source === "legacy" && fs.existsSync(legacyPath)) {

@@ -193,3 +192,3 @@ warnings.push(`${relative} missing; legacy visual_roadmap.md is rewrite input only`);

export function validateTaskPresetContracts(target) {
export function validateTaskPresetContracts(target, { tasks } = {}) {
const failures = [];

@@ -202,3 +201,3 @@ const allowedMigrationLevels = new Set([

]);
for (const task of collectTasks(target)) {
for (const task of tasks || collectTasks(target)) {
if (!task.taskPreset || task.taskPreset === "none") continue;

@@ -336,6 +335,9 @@ let presetPackage = null;

const contractStrict = Boolean(options.strict) || (capabilityState.registry.mode !== "legacy-compat" && !safeAdoptionMode);
const taskPlanPaths = listTaskPlanPaths(target);
const closeoutContent = readFileSafe(path.join(target.docsRoot, "10-WALKTHROUGH/Closeout-SSoT.md"));
const tasks = collectTasks(target, { requireGeneratedScaffoldProvenance: contractStrict, taskPlanPaths, closeoutContent });
const reviews = validateReviewSchema(target, { strict: contractStrict });
const visualMaps = validateVisualMaps(target);
const planContracts = validatePlanContracts(target, { strict: contractStrict });
const presetContracts = validateTaskPresetContracts(target);
const visualMaps = validateVisualMaps(target, { taskPlanPaths });
const planContracts = validatePlanContracts(target, { strict: contractStrict, taskPlanPaths });
const presetContracts = validateTaskPresetContracts(target, { tasks });
const contextDocs = validateContextDocs(target, { strict: contractStrict });

@@ -351,3 +353,2 @@ const governanceBoundaries = validateGovernanceTableBoundaries(target);

const tasks = collectTasks(target);
const taskCompletionConsistency = validateTaskCompletionConsistency(tasks);

@@ -359,2 +360,8 @@ failures.push(...taskCompletionConsistency.failures);

for (const task of tasks) {
for (const issue of task.materialIssues || []) {
if (!String(issue.code || "").startsWith("missing-task-audit") && !String(issue.code || "").startsWith("legacy-")) continue;
const message = `${String(issue.sourcePath || task.path).replace(/^TARGET:/, "")} ${issue.message}`;
if (contractStrict || options.strictLegacy) failures.push(message);
else warnings.push(`adoption-needed: ${message}`);
}
if (task.stateSource === "invalid") {

@@ -366,64 +373,11 @@ const message = `${task.path}/progress.md invalid task state: ${task.stateRaw}`;

}
const capabilityNames = new Map(capabilityState.registry.capabilities.map((capability) => [capability.name, capability]));
for (const detected of capabilityState.detected) {
if (!capabilityNames.has(detected)) capabilityNames.set(detected, { name: detected, state: "configured" });
}
const cutoverCounters = taskCutoverCounters(tasks);
const fullCutoverEligible =
failures.length === 0 &&
warnings.length === 0 &&
cutoverCounters.legacyVisualOnlyCount === 0 &&
cutoverCounters.unknownClassificationCount === 0 &&
cutoverCounters.weakBriefCount === 0 &&
cutoverCounters.missingCanonicalVisualMapCount === 0;
return {
project: {
name: path.basename(target.projectRoot),
root: `TARGET:${target.docsOnly ? toPosix(path.relative(target.projectRoot, target.docsRoot)) : "."}`,
docsOnly: target.docsOnly,
},
schemaVersion: 2,
generatedAt: new Date().toISOString(),
mode: capabilityState.registry.mode,
checkState: {
status: failures.length > 0 ? "fail" : warnings.length > 0 ? "warn" : "pass",
failures: failures.length,
warnings: warnings.length,
details: { failures, warnings },
legacy,
},
git: gitState.summary,
summary: {
tasks: tasks.length,
briefCoverage: {
ready: briefReady,
missing: briefMissing,
total: tasks.length,
},
visualMapCoverage: {
canonical: tasks.filter((task) => task.visualMapSource === "canonical").length,
legacyOnly: cutoverCounters.legacyVisualOnlyCount,
missing: tasks.filter((task) => task.visualMapStatus === "missing").length,
total: tasks.length,
},
fullCutoverEligible,
legacyVisualOnlyCount: cutoverCounters.legacyVisualOnlyCount,
unknownClassificationCount: cutoverCounters.unknownClassificationCount,
weakBriefCount: cutoverCounters.weakBriefCount,
visualMapRequiredCount: cutoverCounters.visualMapRequiredCount,
missingCanonicalVisualMapCount: cutoverCounters.missingCanonicalVisualMapCount,
},
capabilities: [...capabilityNames.values()].map((capability) => ({
name: capability.name,
state: capability.state || "configured",
dependencyStatus: capabilityDefinitions[capability.name]?.dependencies.every((dependency) => capabilityNames.has(dependency))
? "valid"
: "invalid",
warnings: capabilityState.warnings.filter((warning) => warning.includes(capability.name)),
})),
return buildStatusData(target, {
capabilityState,
gitState,
legacy,
failures,
warnings,
tasks,
handoffs: tasks.flatMap((task) => task.handoffs || []),
recentActivity: tasks.slice(0, 8).map((task) => ({ at: new Date().toISOString(), type: "task", summary: task.title })),
};
validationMode: "validated",
});
}

@@ -14,4 +14,5 @@ import fs from "node:fs";

} from "./task-scanner.mjs";
import { parseTaskAuditMetadata } from "./task-audit-metadata.mjs";
export function validatePlanContracts(target, { strict = true } = {}) {
export function validatePlanContracts(target, { strict = true, taskPlanPaths } = {}) {
const failures = [];

@@ -23,12 +24,19 @@ const warnings = [];

};
for (const taskPlanPath of listTaskPlanPaths(target)) {
for (const taskPlanPath of taskPlanPaths || listTaskPlanPaths(target)) {
const taskDir = path.dirname(taskPlanPath);
const relativeDir = toPosix(path.relative(target.projectRoot, taskDir));
const taskPlanContent = readFileSafe(taskPlanPath);
const indexContent = readFileSafe(path.join(taskDir, "INDEX.md"));
const budget = parseTaskBudget(taskPlanContent);
const taskContract = parseTaskContractInfo(taskPlanContent);
const taskAudit = parseTaskAuditMetadata(indexContent, { required: strict && taskContract.generated });
if (!taskContract.generated) {
warnings.push(`adoption-needed: ${relativeDir} missing Task Contract: harness-task/v1 marker`);
}
for (const fileName of requiredTaskFilesForBudget(budget)) {
for (const issue of taskAudit.issues) {
if (taskContract.generated || taskAudit.present) failures.push(`${relativeDir}/INDEX.md ${issue.message}`);
else report(`${relativeDir}/INDEX.md ${issue.message}`);
}
const indexRequired = /^Task Package Index\s*[::]\s*(required|yes|true|必需|必须|required)\s*$/im.test(taskPlanContent);
for (const fileName of requiredTaskFilesForBudget(budget, { indexRequired })) {
if (!fs.existsSync(path.join(taskDir, fileName))) {

@@ -43,4 +51,4 @@ if (taskContract.generated) failures.push(`${relativeDir} missing ${fileName}`);

function requiredTaskFilesForBudget(budget) {
const simpleFiles = ["brief.md", "task_plan.md", visualMapFile, "progress.md"];
function requiredTaskFilesForBudget(budget, { indexRequired = false } = {}) {
const simpleFiles = [...(indexRequired ? ["INDEX.md"] : []), "brief.md", "task_plan.md", visualMapFile, "progress.md"];
if (budget === "simple") return simpleFiles;

@@ -47,0 +55,0 @@ const standardFiles = [...simpleFiles, "execution_strategy.md", "findings.md", lessonCandidatesFile, "review.md"];

@@ -73,2 +73,11 @@ import fs from "node:fs";

export function readJsonSafe(filePath, fallback = null, { onError } = {}) {
try {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch (error) {
if (typeof onError === "function") onError(error);
return fallback;
}
}
export function readBundledTemplate(source) {

@@ -82,5 +91,6 @@ const sourcePath = path.join(repoRoot, source);

export function walkFiles(root) {
export function walkFiles(root, options = {}) {
const results = [];
if (!fs.existsSync(root)) return results;
const dirFilter = typeof options.dirFilter === "function" ? options.dirFilter : () => true;
function walk(dir) {

@@ -92,2 +102,3 @@ for (const entry of fs.readdirSync(dir)) {

if ([".git", "node_modules", "tmp"].includes(entry)) continue;
if (!dirFilter(entry, full)) continue;
walk(full);

@@ -186,4 +197,12 @@ } else {

export function renderTaskTemplate(content, { taskId, title, locale, budget = "standard" }) {
export function renderTaskTemplate(content, { taskId, title, locale, budget = "standard", moduleKey = "", preset = "none", presetVersion = "", evidenceBundle = "", longRunning = false, scaffoldProvenance = {}, taskAudit = {} }) {
const date = todayDate();
const provenance = {
createdBy: scaffoldProvenance.createdBy || "harness new-task",
command: scaffoldProvenance.command || "harness new-task [task-id] <target>",
createdAt: scaffoldProvenance.createdAt || date,
budget: scaffoldProvenance.budget || budget,
templateSource: scaffoldProvenance.templateSource || "templates/planning/brief.md",
exceptionReason: scaffoldProvenance.exceptionReason || "n/a",
};
return String(content)

@@ -195,2 +214,36 @@ .replaceAll("{{TASK_ID}}", taskId)

.replaceAll("{{TASK_BUDGET}}", budget)
.replaceAll("{{TASK_MODULE}}", moduleKey || "n/a")
.replaceAll("{{TASK_PRESET}}", preset || "none")
.replaceAll("{{TASK_PRESET_VERSION}}", presetVersion || "n/a")
.replaceAll("{{TASK_EVIDENCE_BUNDLE}}", evidenceBundle || "n/a")
.replaceAll("{{TASK_LONG_RUNNING}}", longRunning ? "yes" : "no")
.replaceAll("{{SCAFFOLD_CREATED_BY}}", provenance.createdBy)
.replaceAll("{{SCAFFOLD_COMMAND}}", provenance.command)
.replaceAll("{{SCAFFOLD_CREATED_AT}}", provenance.createdAt)
.replaceAll("{{SCAFFOLD_BUDGET}}", provenance.budget)
.replaceAll("{{SCAFFOLD_TEMPLATE_SOURCE}}", provenance.templateSource)
.replaceAll("{{SCAFFOLD_EXCEPTION_REASON}}", provenance.exceptionReason)
.replaceAll("{{TASK_AUDIT_CREATED_BY}}", taskAudit["Created By"] || provenance.createdBy)
.replaceAll("{{TASK_AUDIT_CREATED_AT}}", taskAudit["Created At"] || provenance.createdAt)
.replaceAll("{{TASK_AUDIT_COMMAND_SHAPE}}", taskAudit["Command Shape"] || provenance.command)
.replaceAll("{{TASK_AUDIT_BUDGET}}", taskAudit.Budget || provenance.budget)
.replaceAll("{{TASK_AUDIT_TEMPLATE_SOURCE}}", taskAudit["Template Source"] || provenance.templateSource)
.replaceAll("{{TASK_AUDIT_TASK_CREATOR}}", taskAudit["Task Creator"] || "n/a")
.replaceAll("{{TASK_AUDIT_TASK_CREATOR_SOURCE}}", taskAudit["Task Creator Source"] || "git-unavailable")
.replaceAll("{{TASK_AUDIT_HUMAN_REVIEW_STATUS}}", taskAudit["Human Review Status"] || "not-confirmed")
.replaceAll("{{TASK_AUDIT_CONFIRMATION_ID}}", taskAudit["Confirmation ID"] || "n/a")
.replaceAll("{{TASK_AUDIT_CONFIRMED_AT}}", taskAudit["Confirmed At"] || "n/a")
.replaceAll("{{TASK_AUDIT_REVIEWER}}", taskAudit.Reviewer || "n/a")
.replaceAll("{{TASK_AUDIT_REVIEWER_EMAIL}}", taskAudit["Reviewer Email"] || "n/a")
.replaceAll("{{TASK_AUDIT_CONFIRM_TEXT}}", taskAudit["Confirm Text"] || "n/a")
.replaceAll("{{TASK_AUDIT_EVIDENCE_CHECKED}}", taskAudit["Evidence Checked"] || "n/a")
.replaceAll("{{TASK_AUDIT_REVIEW_COMMIT_SHA}}", taskAudit["Review Commit SHA"] || "n/a")
.replaceAll("{{TASK_AUDIT_AUDIT_SOURCE}}", taskAudit["Audit Source"] || "native-index")
.replaceAll("{{TASK_AUDIT_AUDIT_STATUS}}", taskAudit["Audit Status"] || "created")
.replaceAll("{{TASK_AUDIT_EXCEPTION_REASON}}", taskAudit["Exception Reason"] || provenance.exceptionReason)
.replaceAll("{{TASK_AUDIT_MESSAGE}}", taskAudit.Message || "n/a")
.replaceAll("{{TASK_AUDIT_MIGRATION_STATUS}}", taskAudit["Migration Status"] || "native")
.replaceAll("{{TASK_AUDIT_MIGRATED_FROM}}", taskAudit["Migrated From"] || "n/a")
.replaceAll("{{TASK_AUDIT_LEGACY_EXTRA_FIELDS}}", taskAudit["Legacy Extra Fields"] || "{}")
.replaceAll("{{TASK_AUDIT_MIGRATION_NOTES}}", taskAudit["Migration Notes"] || "n/a")
.replaceAll("[simple / standard / complex]", budget)

@@ -197,0 +250,0 @@ .replaceAll("[simple / standard / long-running / module-parallel]", budget)

import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import {
legacyChecker,
repoRoot,
builtinPresetRoot,
normalizeTarget,
projectPresetRoot,
readFileSafe,

@@ -18,2 +22,3 @@ sanitizeText,

longRunningTaskContractFile,
userPresetRoot,
} from "./core-shared.mjs";

@@ -25,4 +30,4 @@ import {

} from "./markdown-utils.mjs";
import { readCapabilityRegistry } from "./capability-registry.mjs";
import { buildStatus } from "./check-profiles.mjs";
import { readCapabilityRegistry, validateCapabilities } from "./capability-registry.mjs";
import { buildStatusData } from "./status-builder.mjs";
import {

@@ -34,6 +39,10 @@ listTaskPlanPaths,

import { writeDashboardDirectory, writeDashboardFile } from "./dashboard-writer.mjs";
import { listPresetPackageLayers } from "./preset-registry.mjs";
import { validateGovernanceTableBoundaries } from "./governance-table-boundary.mjs";
import { summarizeGitState } from "./git-status-summary.mjs";
export function collectMarkdownDocuments(target) {
const docs = collectDashboardDocumentPaths(target);
return docs.map((file, index) => {
export function collectMarkdownDocuments(target, options = {}) {
const docs = collectDashboardDocumentPaths(target, options);
return docs.map((entry, index) => {
const file = typeof entry === "string" ? entry : entry.file;
const content = sanitizeText(readFileSafe(file));

@@ -47,2 +56,3 @@ const source = prefixedPath(target, file);

content,
...(entry.partial ? { partial: true, partialReason: entry.partialReason || "partial", taskId: entry.taskId || "" } : {}),
};

@@ -52,4 +62,5 @@ });

function collectDashboardDocumentPaths(target) {
function collectDashboardDocumentPaths(target, options = {}) {
const selected = new Set();
const partial = new Map();
const addDocsPath = (relativePath) => {

@@ -74,3 +85,7 @@ const file = path.join(target.docsRoot, relativePath);

}
for (const taskPlanPath of listTaskPlanPaths(target)) {
const tasksByPlanPath = new Map((options.tasks || []).map((task) => [
path.join(target.projectRoot, String(task.taskPlanPath || "").replace(/^TARGET:/, "")),
task,
]));
for (const taskPlanPath of options.taskPlanPaths || listTaskPlanPaths(target)) {
const taskDir = path.dirname(taskPlanPath);

@@ -80,12 +95,25 @@ const progress = readFileSafe(path.join(taskDir, "progress.md"));

const active = isActiveTaskState(state);
const documentNames = active
? ["brief.md", "task_plan.md", "execution_strategy.md", visualMapFile, legacyVisualRoadmapFile, lessonCandidatesFile, longRunningTaskContractFile, "progress.md", "review.md", "findings.md"]
const task = tasksByPlanPath.get(taskPlanPath);
const historicalClosed = !active && task?.closeoutStatus === "closed";
const documentNames = historicalClosed
? ["brief.md"]
: ["brief.md", "task_plan.md", "execution_strategy.md", visualMapFile, legacyVisualRoadmapFile, lessonCandidatesFile, longRunningTaskContractFile, "progress.md", "review.md", "findings.md"];
for (const fileName of documentNames) {
const file = path.join(taskDir, fileName);
if (fs.existsSync(file)) selected.add(file);
if (fs.existsSync(file)) {
selected.add(file);
if (historicalClosed) {
partial.set(file, {
partial: true,
partialReason: "historical-closed",
taskId: task?.id || path.basename(taskDir),
});
}
}
}
for (const indexFile of ["references/INDEX.md", "artifacts/INDEX.md"]) {
const file = path.join(taskDir, indexFile);
if (fs.existsSync(file)) selected.add(file);
if (!historicalClosed) {
for (const indexFile of ["references/INDEX.md", "artifacts/INDEX.md"]) {
const file = path.join(taskDir, indexFile);
if (fs.existsSync(file)) selected.add(file);
}
}

@@ -104,3 +132,4 @@ }

.filter((file) => !file.includes(`${path.sep}_optional-structures${path.sep}`))
.sort();
.sort()
.map((file) => ({ file, ...(partial.get(file) || {}) }));
}

@@ -156,3 +185,13 @@

const phaseId = `phase:${task.id}:${phase.id}`;
addNode({ id: phaseId, type: "phase", label: phase.id, state: phase.state, completion: phase.completion, taskId: task.id });
addNode({
id: phaseId,
type: "phase",
label: phase.id,
state: phase.state,
completion: phase.completion,
kind: phase.kind,
actor: phase.actor,
exitCommand: phase.exitCommand,
taskId: task.id,
});
addEdge({ from: `task:${task.id}`, to: phaseId, type: "contains" });

@@ -398,11 +437,80 @@ for (const dependency of phase.dependsOn || []) {

export function buildDashboardBundle(targetInput, options = {}) {
const status = buildStatus(targetInput, options);
const target = normalizeTarget(targetInput);
const documents = { documents: collectMarkdownDocuments(target) };
const taskPlanPaths = listTaskPlanPaths(target);
const capabilityState = validateCapabilities(target);
const gitState = summarizeGitState(target);
const declaredCapabilities = new Set(capabilityState.registry.capabilities.map((capability) => capability.name));
const shouldRunLegacy = !options.skipLegacyCheck && (capabilityState.registry.mode === "legacy-compat" || declaredCapabilities.has("safe-adoption"));
const legacy = shouldRunLegacy ? runDashboardLegacyCheck(target) : { status: "skipped", code: 0, stdout: "", stderr: "" };
const legacyWarnings = legacy.status === "fail" ? [`adoption-needed: legacy check failed: ${(legacy.stderr || legacy.stdout).trim()}`] : [];
const governanceBoundaries = validateGovernanceTableBoundaries(target);
const status = buildStatusData(target, {
...options,
capabilityState,
gitState,
taskPlanPaths,
legacy,
failures: [...capabilityState.failures, ...governanceBoundaries.failures],
warnings: [...capabilityState.warnings, ...legacyWarnings, ...governanceBoundaries.warnings, ...gitState.warnings],
});
const documents = { documents: collectMarkdownDocuments(target, { taskPlanPaths, tasks: status.tasks }) };
const tables = collectTables(documents.documents);
const graph = collectGraph(status, tables);
const adoption = collectAdoption(status);
return sanitizeDeep({ status, tables, documents, graph, adoption });
const presetCatalog = collectPresetCatalog(targetInput, target, options);
return sanitizeDeep({ status, tables, documents, graph, adoption, presetCatalog });
}
function runDashboardLegacyCheck(target) {
const checkTarget = target.docsOnly ? target.projectRoot : target.input;
const result = spawnSync(process.execPath, [legacyChecker, checkTarget], {
cwd: repoRoot,
encoding: "utf8",
});
return {
status: result.status === 0 ? "pass" : "fail",
code: result.status ?? 1,
stdout: result.stdout || "",
stderr: result.stderr || "",
};
}
export function collectPresetCatalog(targetInput, target = normalizeTarget(targetInput), options = {}) {
const home = options.home || "";
const presets = listPresetPackageLayers({ targetInput: target.projectRoot, home }).map((preset) => ({
key: `${preset.source}:${preset.id}`,
id: preset.id,
version: preset.version,
source: preset.source,
effective: preset.effective === true,
purpose: preset.purpose,
compatibleBudgets: preset.compatibleBudgets,
manifestPath: preset.manifestRelativePath,
manifestSha256: preset.manifestSha256,
taskKind: preset.task?.kind || "",
inputCount: Object.keys(preset.inputs || {}).length,
referenceCount: Object.keys(preset.resources?.references || {}).length,
artifactCount: Object.keys(preset.resources?.artifacts || {}).length,
writeScopeCount: Object.keys(preset.writeScopes || {}).length,
evidenceFileCount: Object.keys(preset.evidence?.files || {}).length,
requiredReadCount: Array.isArray(preset.context?.requiredReads) ? preset.context.requiredReads.length : 0,
checkStatus: "unknown",
}));
const countSource = (source) => presets.filter((preset) => preset.source === source).length;
return {
summary: {
total: presets.length,
project: countSource("project"),
user: countSource("user"),
builtin: countSource("builtin"),
},
roots: [
{ source: "project", path: projectPresetRoot(target.projectRoot) },
{ source: "user", path: home ? path.join(path.resolve(home), ".coding-agent-harness/presets") : userPresetRoot },
{ source: "builtin", path: builtinPresetRoot },
],
presets,
};
}
export function writeDashboardFolder(outDir, targetInput, options = {}) {

@@ -409,0 +517,0 @@ const target = normalizeTarget(targetInput);

@@ -12,2 +12,9 @@ import crypto from "node:crypto";

import { writeDashboardFolder } from "./dashboard-data.mjs";
import {
checkPresetPackage,
installPresetPackage,
listPresetPackages,
seedBundledPresets,
uninstallPresetPackage,
} from "./preset-registry.mjs";

@@ -40,3 +47,3 @@ const jsonHeaders = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };

csrfToken,
writableActions: ["review-complete", "lesson-sedimentation-task"],
writableActions: ["review-complete", "lesson-sedimentation-task", "preset-check", "preset-install", "preset-seed", "preset-uninstall"],
target: target.projectRoot,

@@ -99,2 +106,68 @@ autoRefresh: autoRefresh === true,

if (requestUrl.pathname === "/api/presets/check" && request.method === "POST") {
assertTrustedWorkbenchRequest(request, { origin, csrfToken });
const body = await readJsonBody(request);
const id = String(body.id || "");
if (!id) {
writeJson(response, 400, { error: "Missing preset id" });
return;
}
const result = checkPresetPackage(id, { targetInput: target.projectRoot });
writeJson(response, 200, result);
return;
}
if (requestUrl.pathname === "/api/presets/install" && request.method === "POST") {
assertTrustedWorkbenchRequest(request, { origin, csrfToken });
const body = await readJsonBody(request);
const source = String(body.source || "");
if (!source) {
writeJson(response, 400, { error: "Missing preset source" });
return;
}
if (/^https?:\/\//i.test(source)) {
writeJson(response, 400, { error: "Network preset sources are not supported by the dashboard workbench." });
return;
}
const scope = normalizePresetScope(body.scope);
const result = installPresetPackage(source, { force: body.force === true, scope, targetInput: target.projectRoot });
regenerate();
writeJson(response, 200, { ...result, scope });
return;
}
if (requestUrl.pathname === "/api/presets/seed" && request.method === "POST") {
assertTrustedWorkbenchRequest(request, { origin, csrfToken });
const body = await readJsonBody(request);
const scope = normalizePresetScope(body.scope);
const result = seedBundledPresets({ force: body.force === true, dryRun: body.dryRun === true, scope, targetInput: target.projectRoot });
if (body.dryRun !== true) regenerate();
writeJson(response, 200, result);
return;
}
if (requestUrl.pathname === "/api/presets/uninstall" && request.method === "POST") {
assertTrustedWorkbenchRequest(request, { origin, csrfToken });
const body = await readJsonBody(request);
const id = String(body.id || "");
if (!id) {
writeJson(response, 400, { error: "Missing preset id" });
return;
}
if (String(body.confirmText || "").trim() !== id) {
writeJson(response, 400, { error: "Preset uninstall requires typing the preset id." });
return;
}
const scope = normalizePresetScope(body.scope);
const discovered = listPresetPackages({ targetInput: target.projectRoot }).find((preset) => preset.id === id);
if (discovered?.source === "builtin") {
writeJson(response, 409, { error: "Builtin preset cannot be uninstalled from the dashboard workbench.", id, source: "builtin" });
return;
}
const result = uninstallPresetPackage(id, { scope, targetInput: target.projectRoot });
regenerate();
writeJson(response, 200, { ...result, scope });
return;
}
if (request.method !== "GET" && request.method !== "HEAD") {

@@ -132,2 +205,8 @@ writeJson(response, 405, { error: "Method not allowed" });

function normalizePresetScope(value) {
const scope = String(value || "project");
if (scope !== "project" && scope !== "user") throw new Error(`Invalid preset scope: ${scope}`);
return scope;
}
function isTaskInReviewQueue(task) {

@@ -134,0 +213,0 @@ return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { readJsonSafe } from "./core-shared.mjs";
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "../..");
const dashboardTemplateRoot = path.join(repoRoot, "templates/dashboard");

@@ -31,2 +34,3 @@ const dashboardMarker = ".harness-dashboard";

writeJsonFile(path.join(target, "data/adoption.json"), bundle.adoption);
writeJsonFile(path.join(target, "data/presetCatalog.json"), bundle.presetCatalog);
fs.writeFileSync(

@@ -118,3 +122,3 @@ path.join(target, "assets/dashboard-data.js"),

if (!fs.existsSync(manifestPath)) return fs.readFileSync(path.join(templateRoot, "assets/app.js"), "utf8");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const manifest = readJsonSafe(manifestPath, null);
if (!Array.isArray(manifest) || manifest.length === 0) throw new Error(`Invalid dashboard app manifest: ${manifestPath}`);

@@ -121,0 +125,0 @@ return `${manifest.map((relativePath) => {

@@ -30,3 +30,3 @@ import fs from "node:fs";

if (dirty) {
warnings.push(`dirty-state: ${entries.length} uncommitted Git path(s) will block CLI-owned auto-commit; commit them or record owner/no-commit reason before lifecycle commands.`);
warnings.push(`dirty-state: ${entries.length} uncommitted Git path(s) may block CLI-owned auto-commit when they overlap a command write scope or are staged; commit them or record owner/no-commit reason before lifecycle commands.`);
}

@@ -33,0 +33,0 @@ return {

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import crypto from "node:crypto";
import { spawnSync } from "node:child_process";
import { readBundledTemplate, readFileSafe, repoRoot, todayDate, toPosix, visualMapFile } from "./core-shared.mjs";
import { readBundledTemplate, readFileSafe, readJsonSafe, repoRoot, todayDate, toPosix, visualMapFile } from "./core-shared.mjs";
import { collectTasks } from "./task-scanner.mjs";
import { firstColumn, splitMarkdownRow, updateMarkdownTableRow } from "./markdown-utils.mjs";
import { markdownCell } from "./task-lifecycle/text-utils.mjs";
import { appendMarkdownTableRow, firstColumn, fitMarkdownTableRow, splitMarkdownRow, upsertMarkdownTableRow } from "./markdown-utils.mjs";

@@ -19,34 +20,10 @@ export class GovernanceSyncError extends Error {

export function beginGovernanceSync(target, { operation = "governance-sync", dryRun = false } = {}) {
export function beginGovernanceSync(target, { operation = "governance-sync", dryRun = false, allowDirtyWorktree = false, allowedRelativePaths = [] } = {}) {
if (dryRun) return { target, dryRun, operation, git: inspectGit(target.projectRoot), lockPath: "", active: false };
const lockPath = path.join(target.projectRoot, ".harness/locks/governance-sync.lock");
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
let fd = null;
try {
fd = fs.openSync(lockPath, "wx");
fs.writeFileSync(
fd,
`${JSON.stringify({
operation,
pid: process.pid,
host: process.env.HOSTNAME || "",
branch: currentBranch(target.projectRoot),
targetRoot: target.projectRoot,
startedAt: new Date().toISOString(),
}, null, 2)}\n`,
);
} catch (error) {
if (fd !== null) fs.closeSync(fd);
throw new GovernanceSyncError("Governance sync lock already exists; refusing concurrent registry writes.", {
code: "governance-lock-exists",
details: { lockPath, error: error.message },
recovery: [
`Inspect ${lockPath}.`,
"If no process owns the lock, remove it manually and retry.",
],
});
}
if (fd !== null) fs.closeSync(fd);
acquireGovernanceSyncLock(lockPath, target, { operation });
const gitState = inspectGit(target.projectRoot);
const allowed = [...new Set((allowedRelativePaths || []).filter(Boolean).map(toPosix))].sort();
if (gitState.inGit) {

@@ -61,3 +38,3 @@ if (real(gitState.gitRoot) !== real(target.projectRoot)) {

}
if (gitState.entries.length > 0) {
if (gitState.entries.length > 0 && !allowDirtyWorktree) {
releaseGovernanceSync({ lockPath, active: true });

@@ -70,7 +47,77 @@ throw new GovernanceSyncError("Governance sync requires a clean Git working tree before CLI-owned writes.", {

}
if (gitState.entries.length > 0 && allowDirtyWorktree) {
try {
assertDirtyCompatibleWithWriteScope(gitState.entries, allowed);
} catch (error) {
releaseGovernanceSync({ lockPath, active: true });
throw error;
}
}
assertCommitIdentity(target.projectRoot);
}
return { target, dryRun, operation, git: gitState, lockPath, active: true };
const initialDirtyEntries = gitState.inGit ? gitState.entries.map((entry) => ({
...entry,
fingerprint: fingerprintEntry(target.projectRoot, entry),
})) : [];
return { target, dryRun, operation, git: gitState, initialDirtyEntries, lockPath, active: true };
}
function acquireGovernanceSyncLock(lockPath, target, { operation }) {
for (let attempt = 0; attempt < 2; attempt += 1) {
let fd = null;
try {
fd = fs.openSync(lockPath, "wx");
fs.writeFileSync(
fd,
`${JSON.stringify({
operation,
pid: process.pid,
host: governanceLockHost(),
branch: currentBranch(target.projectRoot),
targetRoot: target.projectRoot,
startedAt: new Date().toISOString(),
}, null, 2)}\n`,
);
fs.closeSync(fd);
return;
} catch (error) {
if (fd !== null) fs.closeSync(fd);
if (error?.code === "EEXIST" && attempt === 0 && removeStaleGovernanceSyncLock(lockPath)) continue;
throw governanceLockExistsError(lockPath, error);
}
}
}
function removeStaleGovernanceSyncLock(lockPath) {
const lockContent = readFileSafe(lockPath);
const lock = readJsonSafe(lockPath, null);
if (!lock) return false;
if (lock.host !== governanceLockHost()) return false;
if (!Number.isInteger(lock?.pid) || lock.pid <= 0) return false;
try {
process.kill(lock.pid, 0);
return false;
} catch (error) {
if (error?.code !== "ESRCH") return false;
}
if (readFileSafe(lockPath) !== lockContent) return false;
fs.rmSync(lockPath);
return true;
}
function governanceLockHost() {
return process.env.HOSTNAME || os.hostname() || "";
}
function governanceLockExistsError(lockPath, error) {
return new GovernanceSyncError("Governance sync lock already exists; refusing concurrent registry writes.", {
code: "governance-lock-exists",
details: { lockPath, error: error?.message || String(error) },
recovery: [
`Inspect ${lockPath}.`,
"If no process owns the lock, remove it manually and retry.",
],
});
}
export function releaseGovernanceSync(context) {

@@ -88,4 +135,4 @@ if (!context?.active || !context.lockPath) return;

if (context?.dryRun || !context?.git?.inGit) return { committed: false, reason: context?.git?.inGit ? "dry-run" : "not-git", allowedPaths: allowed };
assertOnlyAllowedChanged(context.target.projectRoot, allowed);
if (allowed.length === 0) return { committed: false, reason: "no-allowed-paths", allowedPaths: allowed };
assertNoUnexpectedOutsideChanges(context.target.projectRoot, allowed, context.initialDirtyEntries || []);
git(context.target.projectRoot, ["add", "--", ...allowed]);

@@ -95,14 +142,22 @@ assertOnlyAllowedStaged(context.target.projectRoot, allowed);

if (staged.length === 0) return { committed: false, reason: "no-changes", allowedPaths: allowed };
const commitResult = git(context.target.projectRoot, ["commit", "-m", message], { allowFailure: true });
const commitResult = git(context.target.projectRoot, ["-c", "core.hooksPath=/dev/null", "commit", "--no-verify", "-m", message], { allowFailure: true });
if (commitResult.status !== 0) {
let outsideChanges = null;
try {
assertNoUnexpectedOutsideChanges(context.target.projectRoot, allowed, context.initialDirtyEntries || []);
} catch (error) {
outsideChanges = error.details || null;
}
throw new GovernanceSyncError("Governance sync wrote files but Git commit failed.", {
code: "governance-git-commit-failed",
details: { stdout: commitResult.stdout.trim(), stderr: commitResult.stderr.trim(), allowedPaths: allowed },
details: { stdout: commitResult.stdout.trim(), stderr: commitResult.stderr.trim(), allowedPaths: allowed, outsideChanges },
recovery: [
`Inspect files: ${allowed.join(", ")}`,
`Then run: git add -- ${allowed.join(" ")} && git commit -m ${JSON.stringify(message)}`,
`Then run: git add -- ${allowed.join(" ")} && git -c core.hooksPath=/dev/null commit --no-verify -m ${JSON.stringify(message)}`,
],
});
}
assertClean(context.target.projectRoot);
assertLastCommitOnlyAllowed(context.target.projectRoot, allowed);
assertNoUnexpectedOutsideChanges(context.target.projectRoot, allowed, context.initialDirtyEntries || []);
assertWriteScopeClean(context.target.projectRoot, allowed);
return { committed: true, commitSha: git(context.target.projectRoot, ["rev-parse", "HEAD"]).stdout.trim(), allowedPaths: allowed };

@@ -146,3 +201,3 @@ }

];
fs.writeFileSync(ledgerPath, appendRow(content, /^ID$/i, row));
fs.writeFileSync(ledgerPath, appendMarkdownTableRow(content, /^ID$/i, row));
}

@@ -177,3 +232,3 @@ changes.push({ destination: ledgerRelative, action: dryRun ? "would-sync-governance" : "sync-governance", surface: "harness-ledger" });

];
fs.writeFileSync(ledgerPath, upsertRow(content, /^ID$/i, (header, existing) => rowMatchesPlan(header, existing, planPath), row));
fs.writeFileSync(ledgerPath, upsertMarkdownTableRow(content, /^ID$/i, (header, existing) => rowMatchesPlan(header, existing, planPath), row));
}

@@ -205,3 +260,3 @@ return { destination: relative, action: dryRun ? "would-sync-governance" : "sync-governance", surface: "harness-ledger" };

];
fs.writeFileSync(registryPath, upsertRow(content, /^ID$/i, (header, existing) => rowMatchesModule(header, existing, moduleKey, modulePlan), row));
fs.writeFileSync(registryPath, upsertMarkdownTableRow(content, /^ID$/i, (header, existing) => rowMatchesModule(header, existing, moduleKey, modulePlan), row));
}

@@ -296,3 +351,3 @@ return { destination: relative, action: dryRun ? "would-sync-governance" : "sync-governance", surface: "module-registry" };

const stepId = moduleStepId(task);
const label = markdownCell(task.title || task.shortId || task.id).replace(/"/g, "'");
const label = fitMarkdownTableRow([task.title || task.shortId || task.id], 1)[0].replace(/"/g, "'");
if (index === 0) return ` ${stepId}["${label}"]`;

@@ -325,3 +380,3 @@ const previous = moduleStepId(tasks[index - 1]);

| --- | --- | --- | ---: | --- | --- | --- | --- | --- |
${rows.map((row) => `| ${fitRow(row, 9).join(" | ")} |`).join("\n")}
${rows.map((row) => `| ${fitMarkdownTableRow(row, 9).join(" | ")} |`).join("\n")}

@@ -342,6 +397,6 @@ Allowed Evidence Status: missing, partial, present, waived.

while (end < lines.length && lines[end].trim().startsWith("|")) end += 1;
lines.splice(index + 2, end - index - 2, ...rows.map((row) => `| ${fitRow(row, header.length).join(" | ")} |`));
lines.splice(index + 2, end - index - 2, ...rows.map((row) => `| ${fitMarkdownTableRow(row, header.length).join(" | ")} |`));
return `${lines.join("\n").trimEnd()}\n`;
}
return `${String(content || "").trimEnd()}\n\n${rows.map((row) => `| ${fitRow(row, row.length).join(" | ")} |`).join("\n")}\n`;
return `${String(content || "").trimEnd()}\n\n${rows.map((row) => `| ${fitMarkdownTableRow(row, row.length).join(" | ")} |`).join("\n")}\n`;
}

@@ -375,28 +430,2 @@

function upsertRow(content, headerPattern, matcher, row) {
const updated = updateMarkdownTableRow(content, headerPattern, (header, existing) => (matcher(header, existing) ? fitRow(row, header.length) : null));
if (updated.matched) return updated.content;
return appendRow(content, headerPattern, row);
}
function appendRow(content, headerPattern, row) {
const lines = String(content || "").split(/\r?\n/);
for (let index = 0; index < lines.length; index += 1) {
if (!lines[index].trim().startsWith("|")) continue;
const header = splitMarkdownRow(lines[index]);
if (!header.some((cell) => headerPattern.test(cell))) continue;
let insertAt = index + 2;
while (insertAt < lines.length && lines[insertAt].trim().startsWith("|")) insertAt += 1;
lines.splice(insertAt, 0, `| ${fitRow(row, header.length).join(" | ")} |`);
return lines.join("\n");
}
return `${String(content || "").trimEnd()}\n\n| ${row.join(" | ")} |\n`;
}
function fitRow(row, length) {
const next = row.map((cell) => markdownCell(cell));
while (next.length < length) next.push("");
return next.slice(0, length);
}
function rowMatchesPlan(header, row, planPath) {

@@ -461,15 +490,24 @@ const planIndex = firstColumn(header, ["Task Plan", "Plan", "当前产物"]);

function assertOnlyAllowedChanged(root, allowedPaths) {
const outside = statusEntries(root).filter((entry) => !allowedPaths.includes(entry.path));
if (outside.length > 0) {
throw new GovernanceSyncError("Governance sync produced changes outside the allowlist.", {
code: "governance-allowlist-violation",
details: { disallowed: outside, allowedPaths },
recovery: ["Inspect the extra paths; the CLI will not stage or commit unrelated files."],
function assertDirtyCompatibleWithWriteScope(entries, allowedPaths) {
const allowed = new Set(allowedPaths);
const overlapping = entries.filter((entry) => allowed.has(entry.path));
if (overlapping.length > 0) {
throw new GovernanceSyncError("Governance sync write scope overlaps existing dirty files; refusing to overwrite user-owned changes.", {
code: "governance-write-scope-dirty",
details: { overlapping, allowedPaths },
recovery: ["Commit, move, or remove the overlapping files before retrying this lifecycle command."],
});
}
const outsideStaged = entries.filter((entry) => entry.index !== " " && entry.index !== "?" && !allowed.has(entry.path));
if (outsideStaged.length > 0) {
throw new GovernanceSyncError("Git index contains staged files outside the governance sync write scope.", {
code: "governance-index-outside-write-scope",
details: { disallowed: outsideStaged, allowedPaths },
recovery: ["Unstage unrelated files before retrying the lifecycle command."],
});
}
}
function assertOnlyAllowedStaged(root, allowedPaths) {
const outside = statusEntries(root).filter((entry) => entry.index !== " " && !allowedPaths.includes(entry.path));
const outside = statusEntries(root).filter((entry) => entry.index !== " " && entry.index !== "?" && !allowedPaths.includes(entry.path));
if (outside.length > 0) {

@@ -484,9 +522,53 @@ throw new GovernanceSyncError("Git index contains staged files outside the governance sync allowlist.", {

function assertClean(root) {
function assertNoUnexpectedOutsideChanges(root, allowedPaths, initialDirtyEntries) {
const allowed = new Set(allowedPaths);
const initialByPath = new Map(
(initialDirtyEntries || [])
.filter((entry) => !allowed.has(entry.path))
.map((entry) => [entry.path, entry]),
);
const unexpected = [];
const changed = [];
for (const entry of statusEntries(root)) {
if (allowed.has(entry.path)) continue;
const current = { ...entry, fingerprint: fingerprintEntry(root, entry) };
const initial = initialByPath.get(entry.path);
if (!initial) {
unexpected.push(current);
} else if (initial.raw !== current.raw || initial.fingerprint !== current.fingerprint) {
changed.push({ before: initial, after: current });
}
}
if (unexpected.length > 0 || changed.length > 0) {
throw new GovernanceSyncError("Governance sync produced changes outside its write scope.", {
code: "governance-allowlist-violation",
details: { unexpected, changed, allowedPaths },
recovery: ["Inspect the extra paths; the CLI will not stage or commit unrelated files."],
});
}
}
function assertLastCommitOnlyAllowed(root, allowedPaths) {
const committed = git(root, ["diff-tree", "--no-commit-id", "--name-only", "-r", "-z", "HEAD"]).stdout
.split("\0")
.filter(Boolean)
.map(toPosix);
const outside = committed.filter((file) => !allowedPaths.includes(file));
if (outside.length > 0) {
throw new GovernanceSyncError("Governance sync commit contains files outside its write scope.", {
code: "governance-commit-allowlist-violation",
details: { disallowed: outside, committed, allowedPaths },
recovery: ["Inspect the last commit and remove any files that are not owned by the lifecycle command."],
});
}
}
function assertWriteScopeClean(root, allowedPaths) {
const entries = statusEntries(root);
if (entries.length > 0) {
throw new GovernanceSyncError("Governance sync commit completed but working tree is not clean.", {
const remaining = entries.filter((entry) => allowedPaths.includes(entry.path));
if (remaining.length > 0) {
throw new GovernanceSyncError("Governance sync commit completed but write scope is not clean.", {
code: "governance-post-commit-dirty",
details: { entries },
recovery: ["Inspect remaining files before continuing."],
details: { entries: remaining, allowedPaths },
recovery: ["Inspect remaining write-scope files before continuing."],
});

@@ -496,2 +578,17 @@ }

function fingerprintEntry(root, entry) {
const absolute = path.join(root, entry.path);
try {
const stat = fs.lstatSync(absolute);
if (stat.isSymbolicLink()) return `symlink:${fs.readlinkSync(absolute)}`;
if (stat.isFile()) {
return `file:${stat.size}:${crypto.createHash("sha256").update(fs.readFileSync(absolute)).digest("hex")}`;
}
if (stat.isDirectory()) return "directory";
return `${stat.mode}:${stat.size}`;
} catch {
return "missing";
}
}
function statusEntries(root) {

@@ -498,0 +595,0 @@ return git(root, ["status", "--porcelain=v1", "--untracked-files=all"]).stdout

@@ -5,2 +5,3 @@ export * from "./core-shared.mjs";

export * from "./task-scanner.mjs";
export * from "./status-builder.mjs";
export * from "./check-profiles.mjs";

@@ -7,0 +8,0 @@ export * from "./dashboard-data.mjs";

@@ -159,1 +159,34 @@ import { sanitizeText, slug } from "./core-shared.mjs";

}
export function upsertMarkdownTableRow(content, headerPattern, matcher, row) {
const updated = updateMarkdownTableRow(content, headerPattern, (header, existing) => (matcher(header, existing) ? fitMarkdownTableRow(row, header.length) : null));
if (updated.matched) return updated.content;
return appendMarkdownTableRow(content, headerPattern, row);
}
export function appendMarkdownTableRow(content, headerPattern, row) {
const lines = String(content || "").split(/\r?\n/);
for (let index = 0; index < lines.length; index += 1) {
if (!lines[index].trim().startsWith("|")) continue;
const header = splitMarkdownRow(lines[index]);
if (!header.some((cell) => headerPattern.test(cell))) continue;
let insertAt = index + 2;
while (insertAt < lines.length && lines[insertAt].trim().startsWith("|")) insertAt += 1;
lines.splice(insertAt, 0, `| ${fitMarkdownTableRow(row, header.length).join(" | ")} |`);
return lines.join("\n");
}
return `${String(content || "").trimEnd()}\n\n| ${row.map(markdownTableCell).join(" | ")} |\n`;
}
export function fitMarkdownTableRow(row, length) {
const next = row.map(markdownTableCell);
while (next.length < length) next.push("");
return next.slice(0, length);
}
function markdownTableCell(value) {
return String(value || "")
.replace(/\r?\n/g, " ")
.replaceAll("|", "\\|")
.trim();
}

@@ -9,2 +9,3 @@ import fs from "node:fs";

readFileSafe,
readJsonSafe,
existsInDocs,

@@ -367,8 +368,5 @@ walkFiles,

const warnings = [];
let session;
try {
session = JSON.parse(fs.readFileSync(sessionPath, "utf8"));
} catch (error) {
return { operation: "migrate-verify", status: "fail", failures: [`invalid session json: ${error.message}`], warnings };
}
let readError = null;
const session = readJsonSafe(sessionPath, null, { onError: (error) => { readError = error; } });
if (!session) return { operation: "migrate-verify", status: "fail", failures: [`invalid session json: ${readError?.message || "unknown parse error"}`], warnings };
if (session.operation !== "migrate-run") failures.push("session operation is not migrate-run");

@@ -375,0 +373,0 @@ if (session.schemaVersion !== 1 && session.version !== 1) failures.push("session missing schema version");

@@ -5,3 +5,3 @@ import fs from "node:fs";

import { spawnSync } from "node:child_process";
import { repoRoot, taskContractMarker, toPosix, visualMapFile } from "./core-shared.mjs";
import { readJsonSafe, repoRoot, taskContractMarker, toPosix, visualMapFile } from "./core-shared.mjs";
import { verifyMigrationSession } from "./migration-planner.mjs";

@@ -29,8 +29,5 @@ import { buildPresetAudit, renderPresetTemplate } from "./preset-registry.mjs";

if (!fs.existsSync(filePath)) throw new Error(`Preset input file not found for ${declaration.flag || name}: ${rawValue}`);
let value;
try {
value = JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch (error) {
throw new Error(`Invalid preset JSON input ${declaration.flag || name}: ${error.message}`);
}
let readError = null;
const value = readJsonSafe(filePath, null, { onError: (error) => { readError = error; } });
if (value === null) throw new Error(`Invalid preset JSON input ${declaration.flag || name}: ${readError?.message || "unknown parse error"}`);
if (declaration.validateOperation && value.operation !== declaration.validateOperation) {

@@ -468,3 +465,3 @@ throw new Error(`${preset.id} preset requires ${declaration.flag || name} operation ${declaration.validateOperation}`);

try {
return JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version || "unknown";
return readJsonSafe(path.join(repoRoot, "package.json"), {}).version || "unknown";
} catch {

@@ -471,0 +468,0 @@ return "unknown";

import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import crypto from "node:crypto";
import zlib from "node:zlib";
import { builtinPresetRoot, projectPresetRoot, repoRoot, toPosix, userPresetRoot, userPresetRootForHome } from "./core-shared.mjs";

@@ -9,5 +11,13 @@

const allowedEvidenceTypes = new Set(["text", "json", "input-json", "preset-audit", "preset-manifest", "write-scope", "migration-verify", "migration-ledger", "dashboard-hash", "target-git-status", "target-commit", "harness-version", "generated-at"]);
const allowedNewTaskTemplateKeys = new Set(["taskPlanAppend", "executionStrategyAppend", "visualMapAppend", "findingsSeed", "reviewSeed", "prompt"]);
const maxPresetArchiveBytes = 25 * 1024 * 1024;
const maxPresetArchiveUncompressedBytes = 50 * 1024 * 1024;
const maxPresetArchiveEntries = 500;
export function listPresetPackages({ targetInput = "", home = "" } = {}) {
const seen = new Set();
return listPresetPackageLayers({ targetInput, home }).filter((preset) => preset.effective);
}
export function listPresetPackageLayers({ targetInput = "", home = "" } = {}) {
const effectiveIds = new Set();
const presets = [];

@@ -19,5 +29,6 @@ for (const { root, source } of presetSearchRoots({ targetInput, home })) {

if (!id) continue;
if (seen.has(id)) continue;
seen.add(id);
presets.push(readPresetPackage(id, { targetInput, home }));
const preset = readPresetPackageFromPath(path.join(root, id), source);
const effective = !effectiveIds.has(preset.id);
if (effective) effectiveIds.add(preset.id);
presets.push({ ...preset, effective });
}

@@ -72,3 +83,3 @@ }

function readPresetPackageFromPath(directory) {
function readPresetPackageFromPath(directory, source = "local") {
const manifestPath = path.join(directory, "preset.yaml");

@@ -79,3 +90,3 @@ assertPresetDirectory(directory);

const manifest = parseSimpleYaml(raw);
return normalizePresetManifest(manifest, { id: normalizePresetId(manifest.id || path.basename(directory)), manifestPath, raw, source: "local" });
return normalizePresetManifest(manifest, { id: normalizePresetId(manifest.id || path.basename(directory)), manifestPath, raw, source });
}

@@ -102,34 +113,39 @@

if (!source) throw new Error("Missing preset source");
const sourcePath = resolveInstallSource(source);
const stagedPreset = readPresetPackageFromPath(sourcePath);
const stagedReport = validatePresetPackage(stagedPreset);
if (stagedReport.failures.length) throw new Error(`Invalid preset package ${stagedPreset.id}: ${stagedReport.failures.join("; ")}`);
const id = stagedPreset.id;
if (!id) throw new Error("Preset manifest missing id");
const destination = scope === "project" ? projectPresetDestination(id, targetInput) : userPresetDestination(id, { home });
if (fs.existsSync(destination)) {
if (!force) throw new Error(`Preset already installed: ${id}. Re-run with --force to overwrite.`);
}
fs.mkdirSync(path.dirname(destination), { recursive: true });
const tempDestination = path.join(path.dirname(destination), `.${id}.install-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`);
fs.rmSync(tempDestination, { recursive: true, force: true });
copyDirectory(sourcePath, tempDestination);
const resolvedSource = resolveInstallSource(source);
try {
const tempPreset = readPresetPackageFromPath(tempDestination);
const tempReport = validatePresetPackage(tempPreset);
if (tempReport.failures.length) throw new Error(`Invalid preset package ${id}: ${tempReport.failures.join("; ")}`);
fs.rmSync(destination, { recursive: true, force: true });
fs.renameSync(tempDestination, destination);
const preset = readPresetPackage(id, scope === "project" ? { targetInput, home } : { home });
return {
installed: true,
id: preset.id,
version: preset.version,
source: preset.source,
destination: toPosix(destination),
manifestPath: preset.manifestRelativePath,
};
} catch (error) {
const sourcePath = resolvedSource.path;
const stagedPreset = readPresetPackageFromPath(sourcePath);
const stagedReport = validatePresetPackage(stagedPreset);
if (stagedReport.failures.length) throw new Error(`Invalid preset package ${stagedPreset.id}: ${stagedReport.failures.join("; ")}`);
const id = stagedPreset.id;
if (!id) throw new Error("Preset manifest missing id");
const destination = scope === "project" ? projectPresetDestination(id, targetInput) : userPresetDestination(id, { home });
if (fs.existsSync(destination)) {
if (!force) throw new Error(`Preset already installed: ${id}. Re-run with --force to overwrite.`);
}
fs.mkdirSync(path.dirname(destination), { recursive: true });
const tempDestination = path.join(path.dirname(destination), `.${id}.install-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`);
fs.rmSync(tempDestination, { recursive: true, force: true });
throw error;
copyDirectory(sourcePath, tempDestination);
try {
const tempPreset = readPresetPackageFromPath(tempDestination);
const tempReport = validatePresetPackage(tempPreset);
if (tempReport.failures.length) throw new Error(`Invalid preset package ${id}: ${tempReport.failures.join("; ")}`);
fs.rmSync(destination, { recursive: true, force: true });
fs.renameSync(tempDestination, destination);
const preset = readPresetPackage(id, scope === "project" ? { targetInput, home } : { home });
return {
installed: true,
id: preset.id,
version: preset.version,
source: preset.source,
destination: toPosix(destination),
manifestPath: preset.manifestRelativePath,
};
} catch (error) {
fs.rmSync(tempDestination, { recursive: true, force: true });
throw error;
}
} finally {
resolvedSource.cleanup();
}

@@ -234,3 +250,7 @@ }

}
for (const templatePath of Object.values(preset.newTaskTemplates)) {
for (const [templateKey, templatePath] of Object.entries(preset.newTaskTemplates)) {
if (!allowedNewTaskTemplateKeys.has(templateKey)) {
failures.push(`unsupported newTask template: ${templateKey}`);
continue;
}
const absolute = path.join(preset.directory, templatePath);

@@ -602,8 +622,137 @@ if (!isInside(preset.directory, absolute)) failures.push(`template escapes preset package: ${templatePath}`);

const localPath = path.resolve(source);
if (fs.existsSync(path.join(localPath, "preset.yaml"))) return localPath;
if (fs.existsSync(path.join(localPath, "preset.yaml"))) return { path: localPath, cleanup: () => {} };
if (fs.existsSync(localPath) && fs.statSync(localPath).isFile()) {
if (!localPath.toLowerCase().endsWith(".zip")) throw new Error(`Preset source file must be a .zip archive: ${toPosix(localPath)}`);
return resolveZipInstallSource(localPath);
}
const builtinPath = path.join(builtinPresetRoot, normalizePresetId(source));
if (fs.existsSync(path.join(builtinPath, "preset.yaml"))) return builtinPath;
if (fs.existsSync(path.join(builtinPath, "preset.yaml"))) return { path: builtinPath, cleanup: () => {} };
throw new Error(`Preset source not found: ${source}`);
}
function resolveZipInstallSource(sourcePath) {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "harness-preset-archive-"));
try {
extractPresetZip(sourcePath, tempRoot);
return {
path: presetRootFromExtractedArchive(tempRoot),
cleanup: () => fs.rmSync(tempRoot, { recursive: true, force: true }),
};
} catch (error) {
fs.rmSync(tempRoot, { recursive: true, force: true });
throw error;
}
}
function presetRootFromExtractedArchive(tempRoot) {
if (fs.existsSync(path.join(tempRoot, "preset.yaml"))) return tempRoot;
const children = fs.readdirSync(tempRoot, { withFileTypes: true })
.filter((entry) => entry.name !== "__MACOSX" && entry.name !== ".DS_Store");
const presetDirs = children
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(tempRoot, entry.name))
.filter((directory) => fs.existsSync(path.join(directory, "preset.yaml")));
if (presetDirs.length === 1) return presetDirs[0];
throw new Error("Preset archive must contain preset.yaml at the archive root or inside one top-level directory.");
}
function extractPresetZip(sourcePath, destinationRoot) {
const archiveStat = fs.statSync(sourcePath);
if (archiveStat.size > maxPresetArchiveBytes) throw new Error("Preset archive file is too large.");
const archive = fs.readFileSync(sourcePath);
const eocdOffset = findZipEndOfCentralDirectory(archive);
const entryCount = archive.readUInt16LE(eocdOffset + 10);
const centralSize = archive.readUInt32LE(eocdOffset + 12);
const centralOffset = archive.readUInt32LE(eocdOffset + 16);
if (entryCount === 0xffff || centralSize === 0xffffffff || centralOffset === 0xffffffff) {
throw new Error("Zip64 preset archives are not supported.");
}
if (entryCount > maxPresetArchiveEntries) throw new Error(`Preset archive has too many entries: ${entryCount}`);
if (centralOffset + centralSize > archive.length) throw new Error("Invalid preset archive central directory.");
const written = new Set();
let cursor = centralOffset;
let totalUncompressed = 0;
for (let index = 0; index < entryCount; index += 1) {
if (archive.readUInt32LE(cursor) !== 0x02014b50) throw new Error("Invalid preset archive central directory entry.");
const flags = archive.readUInt16LE(cursor + 8);
const method = archive.readUInt16LE(cursor + 10);
const compressedSize = archive.readUInt32LE(cursor + 20);
const uncompressedSize = archive.readUInt32LE(cursor + 24);
const nameLength = archive.readUInt16LE(cursor + 28);
const extraLength = archive.readUInt16LE(cursor + 30);
const commentLength = archive.readUInt16LE(cursor + 32);
const externalAttributes = archive.readUInt32LE(cursor + 38);
const localOffset = archive.readUInt32LE(cursor + 42);
const rawName = archive.slice(cursor + 46, cursor + 46 + nameLength).toString(flags & 0x0800 ? "utf8" : "utf8");
cursor += 46 + nameLength + extraLength + commentLength;
if (shouldSkipZipEntry(rawName)) continue;
if (flags & 0x0001) throw new Error(`Encrypted preset archive entries are not supported: ${rawName}`);
if (method !== 0 && method !== 8) throw new Error(`Unsupported preset archive compression method ${method}: ${rawName}`);
const mode = (externalAttributes >>> 16) & 0o170000;
if (mode === 0o120000) throw new Error(`Preset archive must not contain symlinks: ${rawName}`);
const entryName = safeZipEntryName(rawName);
if (!entryName) continue;
if (entryName.endsWith("/")) {
fs.mkdirSync(path.join(destinationRoot, entryName), { recursive: true });
continue;
}
if (written.has(entryName)) throw new Error(`Preset archive contains duplicate entry: ${entryName}`);
if (uncompressedSize > maxPresetArchiveUncompressedBytes - totalUncompressed) throw new Error("Preset archive is too large.");
const data = readZipEntryData(archive, { localOffset, compressedSize, uncompressedSize, method, name: entryName });
totalUncompressed += data.length;
const destination = path.resolve(destinationRoot, entryName);
if (!isInside(path.resolve(destinationRoot), destination)) throw new Error(`Preset archive entry escapes extraction root: ${rawName}`);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.writeFileSync(destination, data);
written.add(entryName);
}
}
function findZipEndOfCentralDirectory(archive) {
const minOffset = Math.max(0, archive.length - 22 - 65535);
for (let offset = archive.length - 22; offset >= minOffset; offset -= 1) {
if (archive.readUInt32LE(offset) === 0x06054b50) return offset;
}
throw new Error("Invalid preset zip archive: end of central directory not found.");
}
function readZipEntryData(archive, { localOffset, compressedSize, uncompressedSize, method, name }) {
if (localOffset + 30 > archive.length || archive.readUInt32LE(localOffset) !== 0x04034b50) {
throw new Error(`Invalid preset archive local header: ${name}`);
}
const localNameLength = archive.readUInt16LE(localOffset + 26);
const localExtraLength = archive.readUInt16LE(localOffset + 28);
const dataStart = localOffset + 30 + localNameLength + localExtraLength;
const dataEnd = dataStart + compressedSize;
if (dataEnd > archive.length) throw new Error(`Invalid preset archive entry size: ${name}`);
const compressed = archive.slice(dataStart, dataEnd);
let data;
try {
data = method === 0 ? Buffer.from(compressed) : zlib.inflateRawSync(compressed, { maxOutputLength: uncompressedSize });
} catch (error) {
throw new Error(`Preset archive entry could not be decompressed within its declared size: ${name}`);
}
if (data.length !== uncompressedSize) throw new Error(`Preset archive entry size mismatch: ${name}`);
return data;
}
function shouldSkipZipEntry(rawName) {
const normalized = String(rawName || "").replace(/\\/g, "/");
return normalized === "__MACOSX/" || normalized.startsWith("__MACOSX/") || normalized.endsWith("/.DS_Store") || normalized === ".DS_Store";
}
function safeZipEntryName(rawName) {
if (String(rawName).includes("\0")) throw new Error("Preset archive entry contains NUL byte.");
const withSlashes = String(rawName || "").replace(/\\/g, "/");
if (/^[A-Za-z]:/.test(withSlashes) || withSlashes.startsWith("/")) {
throw new Error(`Preset archive entry must be relative: ${rawName}`);
}
const normalized = path.posix.normalize(withSlashes);
if (normalized === "." || normalized === "") return "";
if (normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
throw new Error(`Preset archive entry escapes extraction root: ${rawName}`);
}
return withSlashes.endsWith("/") ? `${normalized}/` : normalized;
}
function copyDirectory(source, destination) {

@@ -610,0 +759,0 @@ fs.mkdirSync(destination, { recursive: true });

@@ -144,3 +144,3 @@ import path from "node:path";

details: { disallowed },
recovery: ["Limit review-confirm writes to the current task review.md and progress.md files."],
recovery: ["Limit review-confirm writes to the current task INDEX.md file."],
});

@@ -147,0 +147,0 @@ }

@@ -0,1 +1,3 @@

import { implementationPhases } from "./phase-kind.mjs";
export function renderDashboard(status) {

@@ -6,7 +8,8 @@ const taskCards = status.tasks

.map(
(phase) => `<div class="phase ${escapeHtml(phase.state)}">
<div class="phase-top"><strong>${escapeHtml(phase.id)}</strong><span>${phase.completion}%</span></div>
(phase) => `<div class="phase ${escapeHtml(phase.state)} ${escapeHtml(phase.kind || "execution")}">
<div class="phase-top"><strong>${escapeHtml(phase.id)}</strong><span>${escapeHtml(phase.kind || "execution")} · ${phase.completion}%</span></div>
<div class="phase-output">${escapeHtml(phase.output)}</div>
<div class="meter"><i style="width:${phase.completion}%"></i></div>
<div class="muted">${escapeHtml(phase.state)} · evidence ${escapeHtml(phase.evidenceStatus)}</div>
<div class="muted">${escapeHtml(phase.state)} · actor ${escapeHtml(phase.actor || "agent")} · evidence ${escapeHtml(phase.evidenceStatus)}</div>
${phase.exitCommand ? `<div class="muted">exit ${escapeHtml(phase.exitCommand)}</div>` : ""}
</div>`,

@@ -95,3 +98,3 @@ )

function evidenceCompletion(phases) {
const scored = phases.filter((phase) => phase.state !== "skipped");
const scored = implementationPhases(phases);
if (scored.length === 0) return 0;

@@ -98,0 +101,0 @@ const score = scored.reduce((sum, phase) => {

@@ -0,1 +1,3 @@

import { implementationPhases } from "./phase-kind.mjs";
export function validateTaskCompletionConsistency(tasks) {

@@ -6,3 +8,11 @@ const failures = [];

if (task.state !== "done") continue;
const incompletePhases = (task.phases || []).filter(
const phases = task.phases || [];
const executionPhases = implementationPhases(phases);
if (phases.length > 0 && executionPhases.length === 0) {
const message = `${task.visualMapPath} done task has no non-skipped Visual Map execution phase`;
if (task.closeoutStatus === "closed") failures.push(message);
else warnings.push(message);
continue;
}
const incompletePhases = executionPhases.filter(
(phase) => phase.state !== "skipped" && (phase.state !== "done" || phase.completion !== 100),

@@ -9,0 +19,0 @@ );

@@ -8,3 +8,2 @@ import fs from "node:fs";

lessonCandidatesFile,
longRunningTaskContractFile,
allowedTaskStates,

@@ -19,7 +18,5 @@ allowedTaskBudgets,

readBundledTemplate,
localizedTemplateSource,
todayDate,
localDate,
datePrefix,
nowTimestamp,
normalizeTaskId,

@@ -40,21 +37,27 @@ renderTaskTemplate,

collectTasks,
collectReviewRisks,
isBlockingReviewRisk,
listTaskPlanPaths,
parseTaskBudget,
taskIdForDirectory,
taskScannerVersion,
} from "./task-scanner.mjs";
import { getColumn, firstColumn, updateMarkdownTableRow } from "./markdown-utils.mjs";
import { validateLifecycleTransition, validateReviewEntryGate } from "./task-lifecycle/review-gates.mjs";
import { advanceLifecyclePhase, autoRecordNoLessonCandidateDecision } from "./task-lifecycle/phase-sync.mjs";
import { confirmTaskReview as confirmTaskReviewWithContext } from "./task-lifecycle/review-confirm.mjs";
import { appendProgressLog } from "./task-lifecycle/text-utils.mjs";
import { buildScaffoldProvenance } from "./task-lifecycle/scaffold-provenance.mjs";
import { buildCreationTaskAudit } from "./task-audit-metadata.mjs";
import {
getColumn,
firstColumn,
updateMarkdownTableRow,
} from "./markdown-utils.mjs";
renderAgentReviewSubmission,
replaceAgentReviewSubmission,
} from "./task-lifecycle/review-submission.mjs";
import {
validateLifecycleTransition,
validateReviewEntryGate,
} from "./task-lifecycle/review-gates.mjs";
import { confirmTaskReview as confirmTaskReviewWithContext } from "./task-lifecycle/review-confirm.mjs";
import { appendProgressLog, markdownCell } from "./task-lifecycle/text-utils.mjs";
appendLongRunningContractFile,
moduleTemplateFiles,
taskFilesForBudget,
} from "./task-lifecycle/template-files.mjs";
import {
planCreateTaskChanges,
refreshPresetCommandAudit,
} from "./task-lifecycle/create-task-helpers.mjs";
import {
beginGovernanceSync,

@@ -68,41 +71,2 @@ commitGovernanceSync,

function taskTemplateFiles({ locale = "en-US" } = {}) {
return [
["brief.md", "templates/planning/brief.md"],
["task_plan.md", "templates/planning/task_plan.md"],
["execution_strategy.md", "templates/planning/execution_strategy.md"],
[visualMapFile, "templates/planning/visual_map.md"],
["findings.md", "templates/planning/findings.md"],
[lessonCandidatesFile, "templates/planning/lesson_candidates.md"],
["progress.md", "templates/planning/progress.md"],
["review.md", "templates/planning/review.md"],
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
}
function simpleTaskTemplateFiles({ locale = "en-US" } = {}) {
return [
["brief.md", "templates/planning/brief.md"],
["task_plan.md", "templates/planning/task_plan.md"],
[visualMapFile, "templates/planning/visual_map.md"],
["progress.md", "templates/planning/progress.md"],
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
}
function optionalTaskTemplateFiles({ locale = "en-US" } = {}) {
return [
["references/INDEX.md", "templates/planning/optional/references/INDEX.md"],
["artifacts/INDEX.md", "templates/planning/optional/artifacts/INDEX.md"],
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
}
function moduleTemplateFiles({ locale = "en-US" } = {}) {
return [
["brief.md", "templates/planning/module_brief.md"],
["module_plan.md", "templates/planning/module_plan.md"],
["execution_strategy.md", "templates/planning/execution_strategy.md"],
[visualMapFile, "templates/planning/visual_map.md"],
["session_prompt.md", "templates/planning/module_session_prompt.md"],
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
}
function taskRoot(target, taskId, { moduleKey = "" } = {}) {

@@ -182,13 +146,2 @@ const normalizedTaskId = normalizeTaskId(taskId);

function taskFilesForBudget({ budget, locale }) {
if (budget === "simple") return simpleTaskTemplateFiles({ locale });
if (budget === "complex") return [...taskTemplateFiles({ locale }), ...optionalTaskTemplateFiles({ locale })];
return taskTemplateFiles({ locale });
}
function appendLongRunningContractFile(files, { locale, longRunning }) {
if (!longRunning) return files;
return [...files, [longRunningTaskContractFile, localizedTemplateSource("templates/planning/long-running-task-contract.md", locale)]];
}
function updateProgressState(content, state, locale) {

@@ -215,3 +168,27 @@ const label = stateLabel(state, locale);

export function createTask(targetInput, taskId, { title = "", locale = "en-US", dryRun = false, moduleKey = "", budget = "standard", longRunning = false, preset = "", fromSession = "", presetArgs = [] } = {}) {
function automaticTaskSlug(seed) {
return normalizeTaskId(seed || "task").slice(0, 48).replace(/-+$/g, "") || "task";
}
function randomTaskSuffix() {
return crypto.randomBytes(4).toString("hex");
}
function resolveTaskIdentity({ target, taskId, title, presetPackage, moduleKey, automaticTaskId }) {
if (!automaticTaskId) {
const rawNormalized = normalizeTaskId(taskId || (presetPackage?.task?.defaultTaskId || ""));
const normalizedTaskId = ensureDatePrefix(rawNormalized);
if (!normalizedTaskId) throw new Error("Missing task id");
return { normalizedTaskId, semanticSlug: bareSlug(normalizedTaskId) };
}
const semanticSlug = automaticTaskSlug(title || presetPackage?.task?.defaultTaskId || "task");
for (let attempt = 0; attempt < 8; attempt += 1) {
const normalizedTaskId = `${localDate()}-${semanticSlug}-${randomTaskSuffix()}`;
if (!fs.existsSync(taskRoot(target, normalizedTaskId, { moduleKey }))) return { normalizedTaskId, semanticSlug };
}
throw new Error(`Unable to allocate automatic task id for: ${semanticSlug}`);
}
export function createTask(targetInput, taskId, { title = "", locale = "en-US", dryRun = false, moduleKey = "", budget = "standard", longRunning = false, preset = "", fromSession = "", presetArgs = [], automaticTaskId = false } = {}) {
const requestedPreset = preset || (moduleKey ? "module" : "");

@@ -229,7 +206,6 @@ const normalizedPreset = normalizeTaskPresetInput(requestedPreset, { targetInput });

if (presetPackage?.task?.requiresFromSession === true && !fromSession) throw new Error(`${normalizedPreset} preset requires --from-session`);
const rawNormalized = normalizeTaskId(taskId || (presetPackage?.task?.defaultTaskId || ""));
const normalizedTaskId = ensureDatePrefix(rawNormalized);
if (!normalizedTaskId) throw new Error("Missing task id");
const semanticSlug = bareSlug(normalizedTaskId);
const normalizedModuleKey = moduleKey ? normalizeTaskId(moduleKey) : "";
const identity = resolveTaskIdentity({ target, taskId, title, presetPackage, moduleKey: normalizedModuleKey, automaticTaskId });
const normalizedTaskId = identity.normalizedTaskId;
const semanticSlug = identity.semanticSlug;
const normalizedLocale = normalizeLocale(locale || readCapabilityRegistry(target).locale);

@@ -239,2 +215,16 @@ const taskTitle = title || (normalizedPreset === "legacy-migration" ? "Harness v1 legacy migration" : semanticSlug);

if (fs.existsSync(directory)) throw new Error(`Task already exists: ${normalizedTaskId}`);
const scaffoldProvenance = buildScaffoldProvenance({
taskId,
normalizedTaskId,
title,
locale: normalizedLocale,
budget: normalizedBudget,
longRunning,
moduleKey: normalizedModuleKey,
preset: normalizedPreset,
fromSession,
targetInput: presetInputs?.targetInput || targetInput,
automaticTaskId,
});
const baseTaskAudit = buildCreationTaskAudit(scaffoldProvenance, { projectRoot: target.projectRoot });
const evaluatedPresetValues = presetPackage ? evaluateTemplateValues(presetPackage, presetInputs.inputs, { taskId: normalizedTaskId, taskTitle, moduleKey: normalizedModuleKey }) : null;

@@ -251,4 +241,33 @@ const presetContext = presetPackage

: null;
const task = {
id: taskIdForDirectory(target, directory),
shortId: normalizedTaskId,
title: taskTitle,
module: normalizedModuleKey || null,
path: `TARGET:${toPosix(path.relative(target.projectRoot, directory))}`,
locale: normalizedLocale,
budget: normalizedBudget,
kind: presetContext?.kind || "general",
preset: normalizedPreset,
presetVersion: presetContext?.presetVersion || "",
presetAudit: presetContext?.audit || null,
migrationTargetLevel: presetContext?.migrationTargetLevel || "",
migrationAchievedLevel: presetContext?.migrationAchievedLevel || "",
evidenceBundle: presetContext?.evidenceBundle || "",
longRunning,
};
const plannedChanges = planCreateTaskChanges({
target,
directory,
normalizedModuleKey,
normalizedLocale,
normalizedBudget,
longRunning,
presetContext,
task,
});
const plannedGovernance = syncTaskGovernance(target, task, { event: "new-task", state: "planned", message: "task registered by CLI", dryRun: true });
const plannedWriteScopes = governanceRelativePaths([...plannedChanges, ...plannedGovernance.changes]);
const changes = [];
const governanceContext = beginGovernanceSync(target, { operation: `new-task ${normalizedTaskId}`, dryRun });
const governanceContext = beginGovernanceSync(target, { operation: `new-task ${normalizedTaskId}`, dryRun, allowDirtyWorktree: true, allowedRelativePaths: plannedWriteScopes });
try {

@@ -275,2 +294,9 @@ if (normalizedModuleKey) {

budget: normalizedBudget,
moduleKey: normalizedModuleKey,
preset: normalizedPreset,
presetVersion: presetContext?.presetVersion || "",
evidenceBundle: presetContext?.evidenceBundle || "",
longRunning,
scaffoldProvenance,
taskAudit: buildCreationTaskAudit({ ...scaffoldProvenance, templateSource: source }, { projectRoot: target.projectRoot }),
}),

@@ -301,2 +327,14 @@ );

budget: normalizedBudget,
moduleKey: normalizedModuleKey,
preset: normalizedPreset,
presetVersion: presetContext?.presetVersion || "",
evidenceBundle: presetContext?.evidenceBundle || "",
longRunning,
scaffoldProvenance: {
...scaffoldProvenance,
templateSource: source,
},
taskAudit: destination === "INDEX.md"
? buildCreationTaskAudit({ ...scaffoldProvenance, templateSource: source }, { projectRoot: target.projectRoot })
: baseTaskAudit,
}), presetContext),

@@ -347,19 +385,2 @@ );

}
const task = {
id: taskIdForDirectory(target, directory),
shortId: normalizedTaskId,
title: taskTitle,
module: normalizedModuleKey || null,
path: `TARGET:${toPosix(path.relative(target.projectRoot, directory))}`,
locale: normalizedLocale,
budget: normalizedBudget,
kind: presetContext?.kind || "general",
preset: normalizedPreset,
presetVersion: presetContext?.presetVersion || "",
presetAudit: presetContext?.audit || null,
migrationTargetLevel: presetContext?.migrationTargetLevel || "",
migrationAchievedLevel: presetContext?.migrationAchievedLevel || "",
evidenceBundle: presetContext?.evidenceBundle || "",
longRunning,
};
const governance = syncTaskGovernance(target, task, { event: "new-task", state: "planned", message: "task registered by CLI", dryRun });

@@ -386,17 +407,2 @@ changes.push(...governance.changes);

function refreshPresetCommandAudit(target, presetContext, { commandWriteScopes = [], dryRun = false } = {}) {
const scopes = [...new Set(commandWriteScopes.filter(Boolean))];
presetContext.audit = {
...presetContext.audit,
presetWriteScopes: presetContext.audit.writeScopes || [],
commandWriteScopes: scopes,
};
for (const evidence of presetContext.evidenceFiles || []) {
if (evidence.source !== "preset-audit") continue;
evidence.content = `${JSON.stringify(presetContext.audit, null, 2)}\n`;
if (dryRun) continue;
fs.writeFileSync(path.join(target.projectRoot, evidence.relativePath), evidence.content);
}
}
export function updateTaskLifecycle(targetInput, taskId, { event = "task-log", state = "", message = "", evidence = "" } = {}) {

@@ -417,2 +423,3 @@ const target = normalizeTarget(targetInput);

reviewContent: readFileSafe(path.join(taskDir, "review.md")),
indexContent: readFileSafe(path.join(taskDir, "INDEX.md")),
reviewTaskKey: canonicalTaskId,

@@ -430,2 +437,4 @@ projectRoot: target.projectRoot,

const allowedPaths = [toPosix(path.relative(target.projectRoot, progressPath))];
const advancedPhasePath = advanceLifecyclePhase(target, taskDir, event);
if (advancedPhasePath) allowedPaths.push(advancedPhasePath);
if (event === "task-review") {

@@ -448,2 +457,4 @@ const reviewPath = path.join(taskDir, "review.md");

allowedPaths.push(toPosix(path.relative(target.projectRoot, reviewPath)));
const lessonDecisionPath = autoRecordNoLessonCandidateDecision(target, taskDir);
if (lessonDecisionPath) allowedPaths.push(lessonDecisionPath);
}

@@ -479,46 +490,2 @@ const task =

}
function renderAgentReviewSubmission({ target, taskDir, canonicalTaskId, message, evidence }) {
const timestamp = nowTimestamp();
const submissionId = `ARS-${timestamp.replace(/[^0-9]/g, "").slice(0, 14)}`;
const materialsHash = hashTaskMaterials(taskDir);
const reviewContent = readFileSafe(path.join(taskDir, "review.md"));
const openFindings = collectReviewRisks(reviewContent).filter(isBlockingReviewRisk).length;
const evidenceSummary = evidence || message || "Agent submitted task for human review.";
return [
"## Agent Review Submission",
"",
"| Field | Value |",
"| --- | --- |",
`| Submission ID | ${submissionId} |`,
`| Submitted At | ${timestamp} |`,
"| Submitted By | agent |",
`| Task Key | ${canonicalTaskId} |`,
`| Materials Checklist Hash | ${materialsHash} |`,
`| Evidence Summary | ${markdownCell(evidenceSummary)} |`,
`| Open Findings Count | ${openFindings} |`,
`| Scanner Version | ${taskScannerVersion} |`,
`| Target | TARGET:${toPosix(path.relative(target.projectRoot, taskDir))} |`,
"",
].join("\n");
}
function replaceAgentReviewSubmission(content, block) {
const trimmed = String(content || "").trimEnd();
if (/^##\s*(?:Agent Review Submission|Agent 审查提交|Agent 提交审查)\s*$/im.test(trimmed)) {
return `${trimmed.replace(/^##\s*(?:Agent Review Submission|Agent 审查提交|Agent 提交审查)\s*$[\s\S]*?(?=^##\s+|(?![\s\S]))/im, `${block.trimEnd()}\n\n`)}\n`;
}
return `${trimmed}\n\n${block.trimEnd()}\n`;
}
function hashTaskMaterials(taskDir) {
const hash = crypto.createHash("sha256");
for (const fileName of ["brief.md", "task_plan.md", visualMapFile, lessonCandidatesFile, "progress.md", "review.md", "findings.md", longRunningTaskContractFile]) {
const filePath = path.join(taskDir, fileName);
if (!fs.existsSync(filePath)) continue;
hash.update(fileName);
hash.update("\0");
hash.update(readFileSafe(filePath));
hash.update("\0");
}
return hash.digest("hex").slice(0, 16);
}
export function updateTaskPhase(targetInput, taskId, phaseId, { state = "", completion = "", evidenceStatus = "" } = {}) {

@@ -525,0 +492,0 @@ const target = normalizeTarget(targetInput);

@@ -9,2 +9,8 @@ import fs from "node:fs";

import {
parseTaskAuditMetadata,
readGitIdentity,
replaceTaskAuditMetadata,
taskAuditFieldOrder,
} from "../task-audit-metadata.mjs";
import {
collectReviewRisks,

@@ -19,3 +25,3 @@ isBlockingReviewRisk,

import { validateHumanReviewConfirmation } from "./review-gates.mjs";
import { appendProgressLog, markdownCell } from "./text-utils.mjs";
import { markdownCell } from "./text-utils.mjs";

@@ -32,4 +38,5 @@ export function confirmTaskReview({ target, taskDir, findTaskByDirectory }, { reviewer = "Human Reviewer", message = "", confirmText = "", evidence = "" } = {}) {

const reviewPath = path.join(taskDir, "review.md");
const progressPath = path.join(taskDir, "progress.md");
const indexPath = path.join(taskDir, "INDEX.md");
const reviewContent = readFileSafe(reviewPath);
const indexContent = readFileSafe(indexPath);
const budget = parseTaskBudget(readFileSafe(path.join(taskDir, "task_plan.md")));

@@ -49,31 +56,32 @@ const candidateStatus = parseLessonCandidateStatus(readFileSafe(path.join(taskDir, lessonCandidatesFile)));

}
const gitGate = prepareReviewConfirmGitGate(target.projectRoot, [reviewPath, progressPath]);
const gitGate = prepareReviewConfirmGitGate(target.projectRoot, [indexPath]);
const timestamp = nowTimestamp();
const confirmationId = `HRC-${timestamp.replace(/[^0-9]/g, "").slice(0, 14)}`;
const safeReviewer = markdownCell(reviewer || "Human Reviewer");
const safeReviewerEmail = markdownCell(process.env.GIT_AUTHOR_EMAIL || process.env.GIT_COMMITTER_EMAIL || "reviewer@example.invalid");
const safeMessage = markdownCell(message || "Human review confirmed");
const safeEvidence = markdownCell(evidence || `TARGET:docs/09-PLANNING/${canonicalTaskId}/review.md`);
const renderConfirmationBlock = ({ commitSha = "pending", auditStatus = "commit-pending" } = {}) =>
`## Human Review Confirmation\n\n| Field | Value |\n| --- | --- |\n| Confirmation ID | ${confirmationId} |\n| Confirmed At | ${timestamp} |\n| Reviewer | ${safeReviewer} |\n| Reviewer Email | ${safeReviewerEmail} |\n| Task Key | ${canonicalTaskId} |\n| Confirm Text | ${markdownCell(confirmText)} |\n| Evidence Checked | ${safeEvidence} |\n| Commit SHA | ${markdownCell(commitSha)} |\n| Audit Status | ${markdownCell(auditStatus)} |\n| Message | ${safeMessage} |\n`;
const confirmationBlock = renderConfirmationBlock();
const nextReview = replaceReviewConfirmation(reviewContent, confirmationBlock);
fs.writeFileSync(reviewPath, nextReview.endsWith("\n") ? nextReview : `${nextReview}\n`);
let progressContent = readFileSafe(progressPath);
progressContent = appendProgressLog(progressContent, {
event: "review-confirm",
message: message || `Human review confirmed by ${reviewer}`,
evidence: evidence || `TARGET:docs/09-PLANNING/${canonicalTaskId}/review.md`,
actor: reviewer || "Human Reviewer",
const identity = readGitIdentity(target.projectRoot);
const baseAuditFields = taskAuditFieldsFromIndex(indexContent);
const buildAuditFields = ({ commitSha = "pending", auditStatus = "commit-pending" } = {}) => ({
...baseAuditFields,
"Human Review Status": "confirmed",
"Confirmation ID": confirmationId,
"Confirmed At": timestamp,
"Reviewer": reviewer || identity.name || "Human Reviewer",
"Reviewer Email": identity.email || "n/a",
"Confirm Text": markdownCell(confirmText),
"Evidence Checked": evidence || `TARGET:docs/09-PLANNING/${canonicalTaskId}/review.md`,
"Review Commit SHA": commitSha,
"Audit Source": "native-index",
"Audit Status": auditStatus,
"Message": message || "Human review confirmed",
"Migration Status": baseAuditFields["Migration Status"] || "native",
});
fs.writeFileSync(progressPath, progressContent.endsWith("\n") ? progressContent : `${progressContent}\n`);
fs.writeFileSync(indexPath, ensureTrailingNewline(replaceTaskAuditMetadata(indexContent, buildAuditFields(), { locale: target.locale })));
const audit = commitReviewConfirmationGate(gitGate, {
taskId: canonicalTaskId,
reviewPath,
reviewPath: indexPath,
message: message || `Human review confirmed by ${reviewer}`,
writeFinalAudit(commitSha) {
const currentReview = readFileSafe(reviewPath);
const finalReview = replaceReviewConfirmation(currentReview, renderConfirmationBlock({ commitSha, auditStatus: "committed" }));
fs.writeFileSync(reviewPath, finalReview.endsWith("\n") ? finalReview : `${finalReview}\n`);
const currentIndex = readFileSafe(indexPath);
const finalIndex = replaceTaskAuditMetadata(currentIndex, buildAuditFields({ commitSha, auditStatus: "committed" }), { locale: target.locale });
fs.writeFileSync(indexPath, ensureTrailingNewline(finalIndex));
},

@@ -99,8 +107,11 @@ });

function replaceReviewConfirmation(content, block) {
const trimmed = String(content || "").trimEnd();
if (/^##\s*(?:Human Review Confirmation|人工审查确认)\s*$/im.test(trimmed)) {
return trimmed.replace(/^##\s*(?:Human Review Confirmation|人工审查确认)\s*$[\s\S]*?(?=^##\s+|(?![\s\S]))/im, `${block.trimEnd()}\n\n`);
}
return `${trimmed}\n\n${block.trimEnd()}\n`;
function taskAuditFieldsFromIndex(content) {
const audit = parseTaskAuditMetadata(content, { required: true });
const fields = {};
for (const field of taskAuditFieldOrder) fields[field] = audit.fields.get(field.toLowerCase()) || "n/a";
return fields;
}
function ensureTrailingNewline(content) {
return content.endsWith("\n") ? content : `${content}\n`;
}

@@ -9,2 +9,3 @@ import fs from "node:fs";

isBlockingReviewRisk,
parseTaskAuditMetadata,
parsePhases,

@@ -14,4 +15,8 @@ parseReviewConfirmation,

} from "../task-scanner.mjs";
import {
implementationPhases,
phaseHasRecordedProgress,
} from "../phase-kind.mjs";
export function validateLifecycleTransition({ event, currentState, budget, reviewContent = "", reviewTaskKey = "", projectRoot = "", taskDir = "" }) {
export function validateLifecycleTransition({ event, currentState, budget, reviewContent = "", indexContent = "", reviewTaskKey = "", projectRoot = "", taskDir = "" }) {
if (event === "task-review" && currentState !== "in_progress") {

@@ -29,3 +34,3 @@ throw new Error(`task-review requires current state in_progress; current state is ${currentState || "unknown"}`);

}
if (!parseReviewConfirmation(reviewContent, { taskKey: reviewTaskKey, projectRoot, taskDir })?.confirmed) {
if (!parseReviewConfirmation(reviewContent, { taskKey: reviewTaskKey, taskAudit: parseTaskAuditMetadata(indexContent), projectRoot, taskDir, indexPath: path.join(taskDir, "INDEX.md") })?.confirmed) {
throw new Error("Human review must be confirmed before task-complete. Run review-confirm first.");

@@ -43,11 +48,9 @@ }

const phases = parsePhases(readVisualMapContractFile(taskDir).content);
const actionablePhases = phases.filter((phase) => phase.state !== "skipped");
const hasRecordedPhaseProgress = actionablePhases.some(
(phase) =>
phase.completion > 0 ||
["in_progress", "review", "blocked", "done"].includes(phase.state) ||
["partial", "present", "waived"].includes(phase.evidenceStatus),
);
const actionablePhases = implementationPhases(phases);
if (phases.length > 0 && actionablePhases.length === 0) {
throw new Error("task-review requires at least one non-skipped Visual Map execution phase.");
}
const hasRecordedPhaseProgress = actionablePhases.some(phaseHasRecordedProgress);
if (actionablePhases.length > 0 && !hasRecordedPhaseProgress) {
throw new Error("task-review requires at least one Visual Map phase progress update. Run task-phase before entering human review.");
throw new Error("task-review requires at least one Visual Map execution phase progress update. Run task-phase before entering human review.");
}

@@ -54,0 +57,0 @@ }

@@ -15,6 +15,11 @@ import fs from "node:fs";

} from "./markdown-utils.mjs";
import {
implementationPhases,
phaseHasRecordedProgress,
} from "./phase-kind.mjs";
import { validateReviewConfirmationGitAudit } from "./review-confirm-git-gate.mjs";
import { isLessonCandidateDecisionComplete } from "./task-lesson-candidates.mjs";
import { reviewConfirmationFromTaskAudit } from "./task-audit-metadata.mjs";
export const taskScannerVersion = "task-scanner/2026-05-23-lifecycle-queues";
export const taskScannerVersion = "task-scanner/2026-05-25-phase-kind";
export const reviewFindingColumns = {

@@ -162,11 +167,8 @@ severity: ["Severity", "严重级别", "优先级"],

}
const actionablePhases = (phases || []).filter((phase) => phase.state !== "skipped");
const hasPhaseEvidence = actionablePhases.some(
(phase) =>
phase.completion > 0 ||
["in_progress", "review", "blocked", "done"].includes(String(phase.state || "").toLowerCase()) ||
["partial", "present", "waived"].includes(String(phase.evidenceStatus || "").toLowerCase()),
);
if (actionablePhases.length > 0 && !hasPhaseEvidence) {
addIssue("phase-incomplete", "Visual Map has no phase progress or evidence yet.", `TARGET:${visualMapFile}`);
const actionablePhases = implementationPhases(phases || []);
const hasPhaseEvidence = actionablePhases.some(phaseHasRecordedProgress);
if ((phases || []).length > 0 && actionablePhases.length === 0) {
addIssue("missing-execution-phase", "Visual Map has no non-skipped execution phase.", `TARGET:${visualMapFile}`);
} else if (actionablePhases.length > 0 && !hasPhaseEvidence) {
addIssue("phase-incomplete", "Visual Map has no execution phase progress or evidence yet.", `TARGET:${visualMapFile}`);
}

@@ -186,3 +188,3 @@ }

export function deriveTaskQueues({ id, title, reviewStatus, reviewSubmission, reviewConfirmation, reviewQueueState, materialIssues, risks, stateConflicts, lessonCandidates, closeoutStatus, tombstone, taskDir, target }) {
export function deriveTaskQueues({ id, title, state, budget, reviewStatus, reviewSubmission, reviewConfirmation, reviewQueueState, materialIssues, risks, stateConflicts, lessonCandidates, closeoutStatus, tombstone, taskDir, target }) {
const queueReasons = [];

@@ -233,2 +235,10 @@ const pushReason = (reason) => {

}
if (budget !== "simple" && reviewSubmission?.submitted && reviewQueueState === "needs-material" && !queueReasons.some((reason) => reason.queue === "missing-materials")) {
pushReason({
code: "review-closeout-materials-incomplete",
queue: "missing-materials",
sourcePath: "TARGET:docs/10-WALKTHROUGH/Closeout-SSoT.md",
message: "Agent review was submitted, but closeout materials are not ready for human confirmation.",
});
}
const hasLessonWork = lessonCandidates?.status === "needs-promotion" || lessonCandidates?.promotionState === "queued" || lessonCandidates?.openCount > 0;

@@ -239,3 +249,3 @@ const taskQueues = [];

} else {
if ((materialIssues || []).length > 0) taskQueues.push("missing-materials");
if ((materialIssues || []).length > 0 || queueReasons.some((reason) => reason.queue === "missing-materials")) taskQueues.push("missing-materials");
if (queueReasons.some((reason) => reason.queue === "blocked")) taskQueues.push("blocked");

@@ -246,2 +256,3 @@ if (reviewSubmission?.submitted && reviewQueueState === "ready-to-confirm" && !reviewConfirmation?.confirmed && !hasLessonWork && !taskQueues.includes("blocked") && !taskQueues.includes("missing-materials")) {

if (hasLessonWork) taskQueues.push("lessons");
if (budget === "simple" && state === "done" && closeoutStatus === "closed") taskQueues.push("finalized");
if (reviewStatus === "confirmed") taskQueues.push(closeoutStatus === "closed" ? "finalized" : "confirmed");

@@ -257,56 +268,30 @@ }

export function parseReviewConfirmation(reviewContent, { taskKey = "", projectRoot = "", taskDir = "", reviewPath = "", progressPath = "" } = {}) {
const match = String(reviewContent || "").match(/^##\s*(?:Human Review Confirmation|人工审查确认)\s*$([\s\S]*?)(?=^##\s+|(?![\s\S]))/im);
if (!match) return null;
const fields = fieldsFromMarkdownBlock(match[1] || "");
const required = ["Confirmation ID", "Confirmed At", "Reviewer", "Reviewer Email", "Task Key", "Confirm Text", "Evidence Checked", "Commit SHA", "Audit Status"];
const missing = required.filter((field) => !isConcreteField(fields.get(field.toLowerCase())));
const confirmedTaskKey = fields.get("task key") || "";
const confirmText = fields.get("confirm text") || "";
const commitSha = fields.get("commit sha") || "";
const auditStatus = fields.get("audit status") || "";
const taskKeyMismatch = Boolean(taskKey && isConcreteField(confirmedTaskKey) && !taskKeysMatch(confirmedTaskKey, taskKey));
const confirmTextMismatch = Boolean(taskKey && isConcreteField(confirmText) && !taskKeysMatch(confirmText, taskKey));
const commitShaInvalid = Boolean(isConcreteField(commitSha) && !/^[0-9a-f]{7,40}$/i.test(commitSha));
const auditStatusInvalid = Boolean(isConcreteField(auditStatus) && auditStatus.trim().toLowerCase() !== "committed");
let gitAudit = null;
if (missing.length === 0 && !taskKeyMismatch && !confirmTextMismatch && !commitShaInvalid && !auditStatusInvalid) {
gitAudit = validateReviewConfirmationGitAudit({
projectRoot,
taskId: taskKey,
reviewPath: reviewPath || (taskDir ? path.join(taskDir, "review.md") : ""),
progressPath: progressPath || (taskDir ? path.join(taskDir, "progress.md") : ""),
commitSha,
});
export function parseReviewConfirmation(reviewContent, { taskKey = "", taskAudit = null, projectRoot = "", taskDir = "", indexPath = "", reviewPath = "", progressPath = "" } = {}) {
if (taskAudit) {
const confirmation = reviewConfirmationFromTaskAudit(taskAudit, { taskKey });
if (
confirmation?.confirmed &&
confirmation.auditSource !== "migrated-legacy-review" &&
projectRoot &&
(indexPath || taskDir) &&
confirmation.commitSha
) {
const gitAudit = validateReviewConfirmationGitAudit({
projectRoot,
taskId: taskKey,
reviewPath: indexPath || path.join(taskDir, "INDEX.md"),
progressPath: "",
commitSha: confirmation.commitSha,
});
return {
...confirmation,
confirmed: confirmation.confirmed && gitAudit.valid,
missingFields: gitAudit.valid ? confirmation.missingFields : [...confirmation.missingFields, "Review Commit SHA git audit"],
gitAudit,
gitAuditInvalid: !gitAudit.valid,
};
}
return confirmation;
}
const gitAuditInvalid = Boolean(gitAudit && !gitAudit.valid);
const invalidFields = [
...(taskKeyMismatch ? ["Task Key match"] : []),
...(confirmTextMismatch ? ["Confirm Text match"] : []),
...(commitShaInvalid ? ["Commit SHA valid"] : []),
...(auditStatusInvalid ? ["Audit Status committed"] : []),
...(gitAuditInvalid ? ["Commit SHA git audit"] : []),
];
if (fields.size > 0) {
return {
confirmed: missing.length === 0 && invalidFields.length === 0,
missingFields: [...missing, ...invalidFields],
confirmationId: fields.get("confirmation id") || "",
confirmedAt: fields.get("confirmed at") || "",
reviewer: fields.get("reviewer") || "",
reviewerEmail: fields.get("reviewer email") || "",
taskKey: confirmedTaskKey,
taskKeyMismatch,
confirmText,
confirmTextMismatch,
evidenceChecked: fields.get("evidence checked") || "",
commitSha,
commitShaInvalid,
auditStatus,
auditStatusInvalid,
gitAudit,
gitAuditInvalid,
};
}
return { confirmed: false, missingFields: required };
return null;
}

@@ -337,4 +322,5 @@

export function deriveLifecycleState({ state = "unknown", reviewStatus = "missing", closeoutStatus = "missing" } = {}) {
export function deriveLifecycleState({ state = "unknown", reviewStatus = "missing", closeoutStatus = "missing", budget = "standard" } = {}) {
if (reviewStatus === "blocked-open-findings") return "review-blocked";
if (budget === "simple" && closeoutStatus === "closed") return "closed";
if (closeoutStatus === "closed" && reviewStatus !== "confirmed") return "closed-review-pending";

@@ -365,3 +351,3 @@ if (closeoutStatus === "closed") return "closed";

export function collectStateConflicts({ state, reviewStatus, closeoutStatus, lifecycleState }) {
export function collectStateConflicts({ state, reviewStatus, closeoutStatus, lifecycleState, budget = "standard" }) {
const conflicts = [];

@@ -371,3 +357,3 @@ if (state === "done" && closeoutStatus !== "closed") {

}
if (closeoutStatus === "closed" && reviewStatus !== "confirmed") {
if (closeoutStatus === "closed" && reviewStatus !== "confirmed" && budget !== "simple") {
conflicts.push({ code: "closed-without-human-review", severity: "warn", message: "Task is closed, but human review confirmation is still missing." });

@@ -374,0 +360,0 @@ }

import fs from "node:fs";
import path from "node:path";
import {
allowedTaskStates,
allowedTaskBudgets,
visualMapFile,

@@ -10,5 +8,5 @@ legacyVisualRoadmapFile,

longRunningTaskContractFile,
taskContractMarker,
toPosix,
readFileSafe,
readJsonSafe,
walkFiles,

@@ -24,2 +22,19 @@ titleFromMarkdown,

import {
normalizePhaseActor,
normalizePhaseKind,
phaseCompletionAverage,
} from "./phase-kind.mjs";
import {
legacyAuditIssues,
parseTaskAuditMetadata,
scaffoldProvenanceSummaryFromTaskAudit,
taskAuditMaterialIssues,
} from "./task-audit-metadata.mjs";
import {
parseTaskBudget,
parseTaskContractInfo,
parseTaskMetadata,
parseTaskStateInfo,
} from "./task-metadata.mjs";
import {
isLessonCandidateDecisionComplete,

@@ -46,2 +61,9 @@ parseLessonCandidateStatus,

export {
parseTaskBudget,
parseTaskContractInfo,
parseTaskMetadata,
parseTaskState,
parseTaskStateInfo,
} from "./task-metadata.mjs";
export {
collectReviewRisks,

@@ -60,2 +82,5 @@ deriveLifecycleState,

export {
parseTaskAuditMetadata,
} from "./task-audit-metadata.mjs";
export {
allowedLessonCandidateRowStatuses,

@@ -68,114 +93,2 @@ allowedLessonCandidateTaskStatuses,

export function parseTaskState(progressContent) {
return parseTaskStateInfo(progressContent).state;
}
export function parseTaskBudget(taskPlanContent) {
const match =
String(taskPlanContent || "").match(/^Selected budget\s*[::]\s*([^\n]+)/im) ||
String(taskPlanContent || "").match(/^选择预算\s*[::]\s*([^\n]+)/im);
if (!match) return "standard";
const raw = match[1].replace(/`/g, "").trim().toLowerCase();
const normalized = raw.replaceAll("_", "-").replace(/\s+/g, "-");
if (allowedTaskBudgets.has(normalized)) return normalized;
if (["long-running", "longrunning", "module-parallel"].includes(normalized)) return "complex";
return "standard";
}
function parseMetadataLine(content, labels) {
const escaped = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
const match = String(content || "").match(new RegExp(`^(?:${escaped})\\s*[::]\\s*([^\\n]+)`, "im"));
return match ? match[1].replace(/`/g, "").trim() : "";
}
function normalizeMetadataValue(value, fallback = "") {
const normalized = String(value || "")
.replace(/`/g, "")
.trim()
.toLowerCase()
.replaceAll("_", "-")
.replace(/\s+/g, "-");
return normalized || fallback;
}
export function parseTaskMetadata(taskPlanContent) {
const content = String(taskPlanContent || "");
const kind = normalizeMetadataValue(parseMetadataLine(content, ["Task Kind", "任务类型"]), "general");
const preset = normalizeMetadataValue(parseMetadataLine(content, ["Task Preset", "Preset", "任务预设"]), "none");
const presetVersion = parseMetadataLine(content, ["Preset Version", "预设版本"]);
const migrationTargetLevel = normalizeMetadataValue(
parseMetadataLine(content, ["Migration Target Level", "Target Level", "迁移目标等级", "目标等级"]),
"",
);
const migrationAchievedLevel = normalizeMetadataValue(
parseMetadataLine(content, ["Migration Achieved Level", "Achieved Level", "迁移实际完成等级", "实际完成等级"]),
"",
);
const evidenceBundle = parseMetadataLine(content, ["Evidence Bundle", "证据包"]);
return {
kind,
preset,
presetVersion,
migrationTargetLevel,
migrationAchievedLevel,
evidenceBundle,
};
}
export function parseTaskContractInfo(taskPlanContent) {
const content = String(taskPlanContent || "");
const explicit =
content.match(/^Task Contract\s*[::]\s*`?([^`\n]+)`?\s*$/im) ||
content.match(/^任务合同\s*[::]\s*`?([^`\n]+)`?\s*$/im);
const version = explicit ? explicit[1].trim() : "";
return {
version,
generated: version === "harness-task/v1" || content.includes(taskContractMarker),
};
}
export function parseTaskStateInfo(progressContent) {
const match = progressContent.match(/^##\s*(?:Current Status|Status|状态)\s*[::]?\s*(?:\n\s*)?([^\n]+)/im);
if (!match) return inferLegacyTaskState(progressContent);
const raw = match[1].replace(/`/g, "").trim();
if (!raw || raw.includes("|") || /^[-*]\s+/.test(raw)) return inferLegacyTaskState(progressContent);
const aliases = new Map([
["进行中", "in_progress"],
["已完成", "done"],
["未开始", "not_started"],
["计划中", "planned"],
["审查中", "review"],
["已阻塞", "blocked"],
["pending", "planned"],
]);
const normalized = aliases.get(raw) || raw.toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
return allowedTaskStates.has(normalized)
? { state: normalized, source: "explicit", raw }
: { state: "unknown", source: "invalid", raw };
}
function inferLegacyTaskState(progressContent) {
const { header, rows } = tableAfterHeading(progressContent, /^(Status|状态)$/i);
const statusIndex = firstColumn(header, ["Status", "状态"]);
if (statusIndex < 0 || rows.length === 0) return { state: "unknown", source: "missing", raw: "" };
const states = rows.map((row) => normalizeLegacyState(row[statusIndex])).filter(Boolean);
if (states.includes("blocked")) return { state: "blocked", source: "legacy-table", raw: "blocked" };
if (states.includes("in_progress")) return { state: "in_progress", source: "legacy-table", raw: "in_progress" };
if (states.includes("review")) return { state: "review", source: "legacy-table", raw: "review" };
if (states.length > 0 && states.every((state) => state === "done")) return { state: "done", source: "legacy-table", raw: "done" };
if (states.some((state) => ["planned", "not_started"].includes(state))) return { state: "planned", source: "legacy-table", raw: "planned" };
return { state: "unknown", source: "missing", raw: "" };
}
function normalizeLegacyState(value) {
const raw = String(value || "").replace(/`/g, "").trim().toLowerCase();
if (!raw || /^(none|n\/a|na|-|—|–|无)$/.test(raw)) return "";
if (/block|阻塞|blocked/.test(raw)) return "blocked";
if (/in[-_\s]?progress|doing|active|进行中|当前|working/.test(raw)) return "in_progress";
if (/review|审查|审核|验证中/.test(raw)) return "review";
if (/done|complete|completed|merged|closed|完成|已完成/.test(raw)) return "done";
if (/pending|planned|todo|not[-_\s]?started|未开始|计划/.test(raw)) return "planned";
return "";
}
export function parsePhases(taskPlanContent) {

@@ -186,2 +99,3 @@ const { header, rows } = tableAfterHeading(taskPlanContent, /^Phase ID$/i);

id: firstColumn(header, ["Phase ID", "阶段 ID"]),
kind: firstColumn(header, ["Kind", "阶段类型", "类型"]),
dependsOn: firstColumn(header, ["Depends On", "依赖"]),

@@ -192,2 +106,4 @@ state: firstColumn(header, ["State", "状态"]),

requiredEvidence: firstColumn(header, ["Required Evidence", "必要证据"]),
exitCommand: firstColumn(header, ["Exit Command", "出口命令", "退出命令"]),
actor: firstColumn(header, ["Actor", "执行者", "角色"]),
evidenceStatus: firstColumn(header, ["Evidence Status", "证据状态"]),

@@ -199,2 +115,3 @@ blockingRisk: firstColumn(header, ["Blocking Risk", "阻塞风险"]),

id: row[indexes.id] || "",
kind: normalizePhaseKind(row[indexes.kind]),
dependsOn: splitDependencies(row[indexes.dependsOn] || ""),

@@ -205,2 +122,4 @@ state: row[indexes.state] || "planned",

requiredEvidence: splitList(row[indexes.requiredEvidence] || ""),
exitCommand: row[indexes.exitCommand] || "",
actor: normalizePhaseActor(row[indexes.actor]),
evidenceStatus: row[indexes.evidenceStatus] || "missing",

@@ -322,4 +241,6 @@ blockingRisk: row[indexes.blockingRisk] || "",

export function collectTasks(target) {
return listTaskPlanPaths(target).map((taskPlanPath) => {
export function collectTasks(target, { requireGeneratedScaffoldProvenance = false, taskPlanPaths, closeoutContent } = {}) {
const paths = taskPlanPaths || listTaskPlanPaths(target);
const closeout = closeoutContent ?? readFileSafe(path.join(target.docsRoot, "10-WALKTHROUGH/Closeout-SSoT.md"));
return paths.map((taskPlanPath) => {
const taskDir = path.dirname(taskPlanPath);

@@ -329,2 +250,3 @@ const taskPlan = readFileSafe(taskPlanPath);

const executionStrategyPath = path.join(taskDir, "execution_strategy.md");
const indexPath = path.join(taskDir, "INDEX.md");
const progressPath = path.join(taskDir, "progress.md");

@@ -338,2 +260,3 @@ const reviewPath = path.join(taskDir, "review.md");

const review = readFileSafe(reviewPath);
const indexContent = readFileSafe(indexPath);
const parsedLessonCandidates = parseLessonCandidateStatus(readFileSafe(lessonCandidatesPath));

@@ -345,9 +268,3 @@ const lessonDetailIssues = validateLessonCandidateDetailArtifacts(target, taskDir, parsedLessonCandidates);

const phases = parsePhases(visualMap.content);
const completion =
phases.length > 0
? Math.round(
phases.filter((phase) => phase.state !== "skipped").reduce((sum, phase) => sum + phase.completion, 0) /
Math.max(1, phases.filter((phase) => phase.state !== "skipped").length),
)
: 0;
const completion = phaseCompletionAverage(phases);
const relative = toPosix(path.relative(target.projectRoot, taskDir));

@@ -362,2 +279,6 @@ const id = taskIdForDirectory(target, taskDir);

const taskContract = parseTaskContractInfo(taskPlan);
const taskAudit = parseTaskAuditMetadata(indexContent, {
required: requireGeneratedScaffoldProvenance && taskContract.generated,
});
const scaffoldProvenance = { summary: scaffoldProvenanceSummaryFromTaskAudit(taskAudit) };
const explicitModule = id.startsWith("MODULES/") ? id.split("/")[1] : null;

@@ -372,4 +293,6 @@ const legacyCandidate = brief.source !== "standalone" || visualMap.status === "legacy-only" || !fs.existsSync(executionStrategyPath);

taskKey: identity.taskKey,
taskAudit,
projectRoot: target.projectRoot,
taskDir,
indexPath,
reviewPath,

@@ -379,4 +302,7 @@ progressPath,

const reviewStatus = taskReviewStatus({ reviewContent: review, risks, confirmation: reviewConfirmation, submission: reviewSubmission });
const closeoutInfo = taskCloseoutInfo(target, taskPlanPath);
const lifecycleState = deriveLifecycleState({ state: stateInfo.state, reviewStatus, closeoutStatus: closeoutInfo.status });
const closeoutInfo = taskCloseoutInfo(target, taskPlanPath, closeout);
const effectiveCloseoutStatus = budget === "simple" && stateInfo.state === "done" && completion === 100
? "closed"
: closeoutInfo.status;
const lifecycleState = deriveLifecycleState({ state: stateInfo.state, reviewStatus, closeoutStatus: effectiveCloseoutStatus, budget });
const materialReadiness = assessMaterialsReadiness({

@@ -395,6 +321,11 @@ budget,

lifecycleState,
closeoutStatus: closeoutInfo.status,
closeoutStatus: effectiveCloseoutStatus,
}),
});
const stateConflicts = collectStateConflicts({ state: stateInfo.state, reviewStatus, closeoutStatus: closeoutInfo.status, lifecycleState });
const materialIssues = [
...materialReadiness.issues,
...taskAuditMaterialIssues(target, taskDir, taskAudit),
...legacyAuditIssues(target, taskDir, { briefContent: brief.content, reviewContent: review }),
];
const stateConflicts = collectStateConflicts({ state: stateInfo.state, reviewStatus, closeoutStatus: effectiveCloseoutStatus, lifecycleState, budget });
const reviewQueueState = deriveReviewQueueState({

@@ -404,7 +335,7 @@ state: stateInfo.state,

reviewStatus,
closeoutStatus: closeoutInfo.status,
closeoutStatus: effectiveCloseoutStatus,
budget,
walkthroughPath: closeoutInfo.walkthroughPath,
lessonCandidateDecisionComplete: isLessonCandidateDecisionComplete(lessonCandidates),
materialsReady: materialReadiness.ready,
materialsReady: materialIssues.length === 0,
deletionState: tombstone.deletionState,

@@ -421,7 +352,7 @@ });

reviewQueueState,
materialIssues: materialReadiness.issues,
materialIssues,
risks,
stateConflicts,
lessonCandidates,
closeoutStatus: closeoutInfo.status,
closeoutStatus: effectiveCloseoutStatus,
tombstone,

@@ -472,2 +403,4 @@ taskDir,

migrationSnapshot: collectMigrationSnapshot(target, metadata),
scaffoldProvenance: scaffoldProvenance.summary,
taskAudit: taskAudit.summary,
lifecycleState,

@@ -479,8 +412,8 @@ reviewStatus,

reviewConfirmation,
materialsReady: materialReadiness.ready,
materialIssues: materialReadiness.issues,
materialsReady: materialIssues.length === 0,
materialIssues,
taskQueues: queueModel.taskQueues,
queueReasons: queueModel.queueReasons,
repairPrompt: queueModel.repairPrompt,
closeoutStatus: closeoutInfo.status,
closeoutStatus: effectiveCloseoutStatus,
walkthroughPath: closeoutInfo.walkthroughPath ? `TARGET:${closeoutInfo.walkthroughPath}` : "",

@@ -529,8 +462,3 @@ lessonCandidatePath: fs.existsSync(lessonCandidatesPath)

const sessionPath = bundlePath ? path.join(bundlePath, "session.json") : "";
let session = null;
try {
session = sessionPath && fs.existsSync(sessionPath) ? JSON.parse(fs.readFileSync(sessionPath, "utf8")) : null;
} catch {
session = null;
}
const session = sessionPath && fs.existsSync(sessionPath) ? readJsonSafe(sessionPath, null) : null;
const summary = session?.plan?.summary || {};

@@ -561,4 +489,3 @@ return {

function taskCloseoutInfo(target, taskPlanPath) {
const closeout = readFileSafe(path.join(target.docsRoot, "10-WALKTHROUGH/Closeout-SSoT.md"));
function taskCloseoutInfo(target, taskPlanPath, closeout) {
if (!closeout.trim()) return { status: "missing", walkthroughPath: "" };

@@ -565,0 +492,0 @@ const docsRelative = `docs/${toPosix(path.relative(target.docsRoot, taskPlanPath))}`;

@@ -155,10 +155,10 @@ ---

```bash
harness new-task <task-id> --title "<title>" --locale zh-CN|en-US /path/to/project
harness task-start <task-id> --message "<what started>" /path/to/project
harness task-log <task-id> --message "<what changed>" --evidence "command:TARGET:path:summary" /path/to/project
harness task-block <task-id> --message "<blocker>" /path/to/project
harness task-review <task-id> --message "<ready for review>" /path/to/project
harness review-confirm <task-id> --message "<human confirmation>" /path/to/project
harness task-complete <task-id> --message "<closeout>" /path/to/project
harness lesson-promote <task-id> <candidate-id> --dry-run /path/to/project
harness new-task --title "<title>" --locale zh-CN|en-US /path/to/project
harness task-start <task-id-from-new-task-output> --message "<what started>" /path/to/project
harness task-log <task-id-from-new-task-output> --message "<what changed>" --evidence "command:TARGET:path:summary" /path/to/project
harness task-block <task-id-from-new-task-output> --message "<blocker>" /path/to/project
harness task-review <task-id-from-new-task-output> --message "<ready for review>" /path/to/project
harness review-confirm <task-id-from-new-task-output> --confirm <task-id-from-new-task-output> --message "<human confirmation>" /path/to/project
harness task-complete <task-id-from-new-task-output> --message "<closeout>" /path/to/project
harness lesson-promote <task-id-from-new-task-output> <candidate-id> --dry-run /path/to/project
harness task-list --json /path/to/project

@@ -170,2 +170,3 @@ ```

- `new-task --budget complex` 在完整任务文件之外创建 optional references/artifacts 索引。
- `new-task --title "<title>"` 默认生成 `YYYY-MM-DD-<title-slug>-<8hex>` 任务 ID,降低多人和多 agent 同仓创建任务时的重名概率;只有 coordinator 需要固定兼容 ID 时才传显式 `<task-id>`。
- 已存在任务不会被覆盖;旧项目迁移时先 `task-list --json`,再决定复用旧任务还是开新任务。

@@ -172,0 +173,0 @@ - 状态推进只写 `progress.md`,不得重写历史 `task_plan.md`。

@@ -33,1 +33,12 @@ # {{TASK_TITLE}}

Replace this line with the first concrete action before implementation starts.
## Scaffold Provenance
| Field | Value |
| --- | --- |
| Created By | {{SCAFFOLD_CREATED_BY}} |
| Command Shape | {{SCAFFOLD_COMMAND}} |
| Created At | {{SCAFFOLD_CREATED_AT}} |
| Budget | {{SCAFFOLD_BUDGET}} |
| Template Source | {{SCAFFOLD_TEMPLATE_SOURCE}} |
| Exception Reason | {{SCAFFOLD_EXCEPTION_REASON}} |

@@ -27,3 +27,3 @@ # {{TASK_TITLE}}

- 生命周期状态:未开始
- 必需文件:`task_plan.md`、`execution_strategy.md`、`visual_map.md`、`progress.md`、`findings.md`、`review.md`
- 必需文件:`INDEX.md`、`task_plan.md`、`execution_strategy.md`、`visual_map.md`、`progress.md`、`findings.md`、`review.md`
- 完成条件:验证证据必须记录到 `progress.md`

@@ -30,0 +30,0 @@

@@ -59,2 +59,3 @@ # 模块会话提示词(Module Session Prompt)

- 汇报状态时区分 `task.state`、`lifecycleState`、`reviewStatus` 和 `closeoutStatus`;`done` 只表示实现步骤完成,不等于 `closed`。
- 把当前任务 `visual_map.md` 阶段表作为生命周期地图。切片结束时检查当前 gate phase;只有 `Actor` 为 `agent` 的 `Exit Command` 才由 Agent 执行。
- 更新 docs/09-PLANNING/MODULES/<module-key>/module_plan.md。

@@ -61,0 +62,0 @@ - 写 review.md,或记录 review skipped-with-reason。需要人工确认审查完成时,必须通过本地 dashboard workbench,或由 coordinator 执行 `harness review-confirm`;存在开放 P0/P1/P2 finding 时不得确认。

@@ -81,20 +81,2 @@ # [任务名称] - 审查

## Human Review Confirmation(人工审查确认)
本节只能由人工 reviewer,或代表明确人工确认的命令 / workbench 动作填写。Agent 提交审查、自查、subagent 审查都不能满足这个门禁。
| Field | Value |
| --- | --- |
| Confirmation ID | [由 review-confirm 生成] |
| Confirmed At | [timestamp] |
| Reviewer | [人工 reviewer 姓名] |
| Reviewer Email | [邮箱,如可用] |
| Task Key | {{TASK_ID}} |
| Confirm Text | [必须匹配任务 ID 或命令要求的确认短语] |
| Evidence Checked | [review packet / tests / diff 摘要] |
| Commit SHA | [由 review-confirm 生成的确认提交 SHA] |
| Audit Status | committed / blocked |
任务仍属于缺材料、阻塞或 Lessons 路由时,不要填写本节。`review-confirm` 与 Dashboard Workbench 必须在 Git dirty、提交身份缺失、hook 失败,或写入超出当前任务 `review.md` / `progress.md` 白名单时拒绝确认。
## 残余风险

@@ -101,0 +83,0 @@

# [任务名称]
Task Contract: harness-task/v1
Task Package Index: required

@@ -15,21 +16,8 @@ ## 目标

## 任务信息架构预算
## 预算选择
| 预算 | 适用场景 | 必需结构 |
| --- | --- | --- |
| simple | 单 owner、无 subagent、证据深度为 L0/L1、不需要正式 review gate | `brief.md`、`task_plan.md`、`visual_map.md`、`progress.md` |
| standard | 常规功能、修复或文档改动 | `brief.md`、`task_plan.md`、`execution_strategy.md`、`visual_map.md`、`findings.md`、`lesson_candidates.md`、`progress.md`、`review.md` |
| complex | 需要 L2/L3 证据、subagent/reviewer、外部参考、生成产物,或超过 5 个切片 | standard 文件,并额外创建 `references/INDEX.md` 与 `artifacts/INDEX.md` |
选择预算:{{TASK_BUDGET}}
可选子目录按触发条件创建,不作为默认脚手架:
选择理由:[为什么本任务适合这个预算]
- `lessons/LC-*.md`:进入 `needs-promotion` 的 lesson candidate 的任务本地详情文件。
- `references/INDEX.md`:任务本地资料、外部链接、reviewer 输入包、跨仓上下文。
- `artifacts/INDEX.md`:命令输出、截图、fixture、生成报告、审查记录等证据。
- `slices/<slice-id>/`:多切片任务。每个切片使用 `brief.md`、`evidence.md`、`review.md`。
没有真实触发条件时,不创建可选目录;已创建则必须有 index 和明确用途。
## 上下文包(Context Packet)

@@ -41,50 +29,2 @@

路径前缀约定:
- `PUBLIC:`:公开源仓库中的文件。
- `PRIVATE:`:私有 harness 仓库中的文件。
- `TARGET:`:已安装目标项目中的文件。
- `EXTERNAL:` 或 `URL:`:外部资料。
## 执行与可视化文件
不要手工复制本模板来创建任务目录。必须使用 `harness new-task`,让所选预算自动
创建正确文件集,并让 `harness check` 能按同一契约校验。
`execution_strategy.md` 和 `visual_map.md` 是本任务的同级合同文件,不嵌入 `task_plan.md`。这样 dashboard 和 checker 可以稳定读取。
| 预算 | 必需文件 |
| --- | --- |
| simple | `brief.md`、`task_plan.md`、`visual_map.md`、`progress.md` |
| standard | simple 文件,加 `execution_strategy.md`、`findings.md`、`lesson_candidates.md`、`review.md` |
| complex | standard 文件,加 `references/INDEX.md`、`artifacts/INDEX.md` |
| long-running 附加项 | 选择 `--long-running` 时额外创建 `long-running-task-contract.md` |
文件职责:
| 合同文件 | 用途 |
| --- | --- |
| `brief.md` | 面向人和下一轮 agent 的任务摘要与上下文包 |
| `task_plan.md` | 目标、范围、预算、验收与执行决策 |
| `execution_strategy.md` | 执行模式、subagent 使用、冲突控制、证据深度、交接规则 |
| `visual_map.md` | 图表集合:阶段图、可选架构/时序/数据流/状态图、完成度、证据状态、阻塞风险 |
| `progress.md` | 执行日志、决策和交接 |
| `findings.md` | 发现、研究记录和未解决风险 |
| `lesson_candidates.md` | 任务本地教训候选队列。人工审查确认前必须接受无候选、拒绝候选,或排队 promotion |
| `lessons/LC-*.md` | 可选的任务本地 lesson 详情文件,趁源任务上下文还新鲜写出,并由 `Detail Artifact` 链接 |
| `review.md` | Agent Review Submission、对抗性审查、release review、外部 reviewer 结论 |
| `references/INDEX.md` | complex 任务的资料包和参考索引 |
| `artifacts/INDEX.md` | complex 任务的生成证据和产物索引 |
| `long-running-task-contract.md` | 连续执行权限、循环规则和停止条件 |
旧任务可以保留历史嵌入式段落作为 fallback;新任务必须使用独立文件。
## 产物索引(Artifact Index)
简单任务可在这里登记关键证据。产物较多时,创建 `artifacts/INDEX.md` 并在此引用 ID。
| Artifact ID | 类型 | 路径 | 摘要 |
| --- | --- | --- | --- |
| A-001 | command / diff / fixture / screenshot / review / report | PUBLIC:path 或 PRIVATE:path 或 TARGET:path 或 EXTERNAL:path 或 URL:https://example.com | [这份证据证明了什么] |
## 步骤

@@ -91,0 +31,0 @@

@@ -17,5 +17,5 @@ # Visual Map / 可视化图谱

flowchart LR
PH01["PH-01 计划确认"] --> PH02["PH-02 实现"]
PH02 --> PH03["PH-03 验证"]
PH03 --> PH04["PH-04 审查与收口"]
INIT01["INIT-01 范围与上下文\nkind=init"] --> EXEC01["EXEC-01 实现切片\nkind=execution"]
EXEC01 --> GATE01["GATE-01 Agent 提交审查\nkind=gate"]
GATE01 --> GATE02["GATE-02 人工审查确认\nkind=gate"]
```

@@ -25,5 +25,8 @@

| Phase ID | Depends On | State | Completion | Output | Required Evidence | Evidence Status | Blocking Risk | Owner / Handoff |
| --- | --- | --- | ---: | --- | --- | --- | --- | --- |
| PH-01 | none | planned | 0 | [计划输出] | [必需证据] | missing | none | coordinator |
| Phase ID | Kind | Depends On | State | Completion | Output | Required Evidence | Exit Command | Actor | Evidence Status | Blocking Risk | Owner / Handoff |
| --- | --- | --- | --- | ---: | --- | --- | --- | --- | --- | --- | --- |
| INIT-01 | init | none | planned | 0 | 任务计划和执行策略已确认 | `task_plan.md`; `execution_strategy.md` | `harness task-start {{TASK_ID}}` | agent | missing | none | coordinator |
| EXEC-01 | execution | INIT-01 | planned | 0 | 有边界的实现、文档切片和验证证据 | diff、commands、worker handoff 或 artifact path | `harness task-phase {{TASK_ID}} EXEC-01 --state done --completion 100 --evidence present` | agent | missing | [risk] | [owner] |
| GATE-01 | gate | EXEC-01 | planned | 0 | Agent Review Submission | `review.md`、progress update、lesson routing | `harness task-review {{TASK_ID}} --message "<summary>"` | agent | missing | [risk] | coordinator |
| GATE-02 | gate | GATE-01 | planned | 0 | Human Review Confirmation | review packet 和人工确认 | `harness review-confirm {{TASK_ID}} --confirm {{TASK_ID}}` | human | missing | Agent 不能代办人工确认 | human |

@@ -34,4 +37,8 @@ 允许的 `State`:`planned`, `in_progress`, `review`, `blocked`, `done`, `skipped`。

`Completion` 使用 `0..100` 的整数;`done` 应为 `100`,`planned` 应为 `0`,`skipped` 不计入 dashboard 总完成度。dashboard 以阶段表计算进度,不从正文推断。
允许的 `Kind`:`init`, `execution`, `gate`。
允许的 `Actor`:`agent`, `human`, `coordinator`。
`Completion` 使用 `0..100` 的整数;`done` 应为 `100`,`planned` 应为 `0`,`skipped` 不计入 dashboard 总完成度。dashboard 的实现完成度只由非 skipped 的 `execution` 阶段计算;`init` 和 `gate` 阶段表达生命周期门禁、下一步命令和责任人,不拉低实现完成度。
## 支持性图表(Supporting Maps)

@@ -38,0 +45,0 @@

@@ -31,4 +31,28 @@ # 执行工作流标准

8. 主动提交已验证的、有意义的中间成果;commit message 应说明变更类型和范围。除非用户明确要求暂不提交、检查失败、dirty 归属不清,或安全边界阻止干净提交,否则不要把已完成切片长期留在未提交状态;延期提交必须写明 no-commit reason、owner 和下一步。
9. 机械化 Harness 写入优先使用 CLI lifecycle 命令。CLI-owned 写入会加锁、限制 allowlist 并自动提交,也会拒绝 dirty Git 状态;agent-owned 手工编辑仍需要明确任务提交或延期提交理由。
9. 机械化 Harness 写入优先使用 CLI lifecycle 命令。CLI-owned 写入会加锁、限制 allowlist 并自动提交。`new-task` 可以保留无关 dirty 文件,并只提交本次命令自己的 write scope;但 write scope 重叠 dirty 或无关 staged 文件仍会阻塞命令。其他 lifecycle 命令仍可能要求 clean tree。agent-owned 手工编辑仍需要明确任务提交或延期提交理由。
10. 把 `visual_map.md` 当作生命周期阶段地图。`init` 阶段准备工作,`execution` 阶段定义实现完成度,`gate` 阶段定义审查、人工确认、lesson routing 和 closeout。只有当前操作者匹配阶段 `Actor` 时,才执行该阶段 `Exit Command`;Agent 不得执行 `human` gate。
11. 新任务目录必须在 `INDEX.md` 保留机器可读任务审计元数据。正常路径是 `Created By: harness new-task`;手工创建只能作为 `manual-exception`,并写清具体原因;历史迁移使用 `historical-backfill`。
## 任务包结构
使用 `harness new-task` 创建任务目录,不要手工复制任务文件。CLI 会按所选预算创建文件集,并在 `INDEX.md` 记录任务审计元数据,供 `harness check` 校验。
| 预算 | 必需文件 |
| --- | --- |
| simple | `INDEX.md`、`brief.md`、`task_plan.md`、`visual_map.md`、`progress.md` |
| standard | simple 文件,加 `execution_strategy.md`、`findings.md`、`lesson_candidates.md`、`review.md` |
| complex | standard 文件,加 `references/INDEX.md`、`artifacts/INDEX.md` |
| long-running 附加项 | 选择 `--long-running` 时额外创建 `long-running-task-contract.md` |
`INDEX.md` 是任务包导航页,也是审计元数据 SSoT。它只指向合同文件和可选索引,不替代这些文件。
可选目录只在触发时创建:
- `references/INDEX.md`:任务本地资料、外部链接、reviewer 输入包、preset 提供的 required reads。
- `artifacts/INDEX.md`:命令输出、截图、fixture、生成报告、审查记录和其他证据。
- `lessons/LC-*.md`:进入 `needs-promotion` 的 lesson candidate 的任务本地详情文件。
- `slices/<slice-id>/`:明确启用多切片任务包时使用。
Preset 包不能新增或替换根级 base scaffold 文档。Preset 只能追加固定基础文档的受控 section,或把隔离资源写入 `references/**` 和 `artifacts/**`。resource rows 只能登记在 `references/INDEX.md` 和 `artifacts/INDEX.md`;根 `INDEX.md` 最多显示系统渲染的 preset summary 字段。
## 完成任务后

@@ -43,7 +67,8 @@

7. 写 walkthrough,引用 task plan、review、证据、residual、Regression SSoT 和 commit。
8. 执行 Lessons 检查:新任务默认先写 `lesson_candidates.md` 并交给人工审查;人工标记后可记录 `queued-promotion`,再由维护命令写 promoted lesson 详情文档。没有可复用候选时记录 `no-candidate-accepted`;旧任务兼容可记录 `checked-none: <reason>`。
9. 最后更新 Harness Ledger,因为它记录本轮上下文维护的最终状态。
10. 完成 commit / PR / release note,并确认本任务工作区没有未解释的遗留改动。
11. 如使用 worker,coordinator 集成 worker commit 后运行最终 gates,并记录 integration evidence。
12. 如使用 worktree,按 `worktree-standard.md` 清理或记录保留原因。
8. 确认当前 `visual_map.md` lifecycle gate 已执行,或已记录 blocker。
9. 执行 Lessons 检查:新任务默认先写 `lesson_candidates.md` 并交给人工审查;人工标记后可记录 `queued-promotion`,再由维护命令写 promoted lesson 详情文档。没有可复用候选时记录 `no-candidate-accepted`;旧任务兼容可记录 `checked-none: <reason>`。
10. 最后更新 Harness Ledger,因为它记录本轮上下文维护的最终状态。
11. 完成 commit / PR / release note,并确认本任务工作区没有未解释的遗留改动。
12. 如使用 worker,coordinator 集成 worker commit 后运行最终 gates,并记录 integration evidence。
13. 如使用 worktree,按 `worktree-standard.md` 清理或记录保留原因。

@@ -50,0 +75,0 @@ ## 提交规范

@@ -15,2 +15,14 @@ const bundle = window.__HARNESS_DASHBOARD__ || {};

warningPage: 1,
presetQuery: "",
presetSourceFilter: "all",
selectedPresetKey: "",
selectedPresetId: "",
presetActionResult: null,
presetInstallSource: "",
presetInstallScope: "project",
presetInstallForce: false,
presetSeedScope: "project",
presetSeedForce: false,
presetUninstallScope: "project",
presetUninstallConfirm: "",
renderMode: "rendered",

@@ -17,0 +29,0 @@ theme: localStorage.getItem("harness.theme") || "system",

@@ -32,2 +32,3 @@ function t(key) {

${routeLink("#/modules", t("moduleView"), "modules")}
${routeLink("#/presets", t("presetCatalog"), "presets")}
<button data-language-toggle>${locale === "zh" ? "EN" : "中文"}</button>

@@ -59,2 +60,3 @@ <button data-theme-toggle>${themeLabel()}</button>

if (route.name === "modules") return modulesView(route.id);
if (route.name === "presets") return presetsView();
if (route.name === "tasks") return taskIndex();

@@ -71,2 +73,3 @@ return overview();

if (parts[0] === "modules") return { name: "modules", id: parts[1] || "" };
if (parts[0] === "presets") return { name: "presets" };
if (parts[0] === "tasks") return { name: "tasks" };

@@ -73,0 +76,0 @@ return { name: "overview" };

@@ -19,2 +19,5 @@ function overview() {

const status = bundle.status?.checkState?.status || "unknown";
const validationMode = bundle.status?.checkState?.validationMode || "validated";
const dataOnly = validationMode === "data-only";
const displayState = dataOnly ? "snapshot" : status;
const failures = bundle.status?.checkState?.failures || 0;

@@ -27,5 +30,5 @@ const warnings = bundle.status?.checkState?.warnings || 0;

return `<section class="status-card-group">
<div class="status-primary ${status}">
<span>${t("readiness")}</span>
<strong>${label(status)}</strong>
<div class="status-primary ${displayState}">
<span>${dataOnly ? t("snapshotStatus") : t("readiness")}</span>
<strong>${dataOnly ? t("snapshot") : label(status)}</strong>
<p>${nextActionText()}</p>

@@ -51,2 +54,3 @@ </div>

function nextActionText() {
if ((bundle.status?.checkState?.validationMode || "validated") === "data-only") return t("snapshotNotValidated");
const failures = bundle.status?.checkState?.failures || 0;

@@ -53,0 +57,0 @@ if (failures > 0) return t("resolveBlockers");

@@ -75,13 +75,53 @@ function taskDetail(route) {

function phaseTimeline(task) {
const knownKinds = new Set(["init", "execution", "gate"]);
const groups = [
["init", "Init"],
["execution", "Execution"],
["gate", "Gate"],
["other", "Other / Invalid"],
];
const phases = task.phases || [];
const grouped = groups
.map(([kind, label]) => {
const items = kind === "other"
? phases.filter((phase) => !knownKinds.has(phase.kind || "execution"))
: phases.filter((phase) => (phase.kind || "execution") === kind);
if (!items.length) return "";
return `<div class="phase-kind-group ${escapeAttr(kind)}">
<h3>${escapeHtml(label)}</h3>
${items.map(phaseStep).join("")}
</div>`;
})
.join("");
return `<section class="phase-timeline">
<h2>${t("phaseTimeline")}</h2>
${(task.phases || []).map((phase) => `<div class="phase-step ${phase.state}">
<strong>${escapeHtml(phase.id)}</strong>
<span>${phase.completion}%</span>
<p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
${progressBar(phase.completion)}
</div>`).join("") || emptyState(t("noPhaseData"))}
${grouped || emptyState(t("noPhaseData"))}
</section>`;
}
function phaseStep(phase) {
const kind = phase.kind || "execution";
const actor = phase.actor || "agent";
const knownKind = ["init", "execution", "gate"].includes(kind);
const kindLabel = knownKind ? escapeHtml(kind) : `<span class="tag warn">${escapeHtml(kind)}</span>`;
const phaseKindClass = knownKind ? kind : "other";
return `<div class="phase-step ${escapeAttr(phase.state)} ${escapeAttr(phaseKindClass)}">
<div class="phase-step-head">
<strong>${escapeHtml(phase.id)}</strong>
<span>${kindLabel} · ${phase.completion}%</span>
</div>
<p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
${progressBar(phase.completion)}
<div class="phase-meta">
${phaseMetaTag(actor)}
${tag(phase.evidenceStatus || "missing")}
</div>
${phase.exitCommand ? `<code class="phase-exit-command">${escapeHtml(phase.exitCommand)}</code>` : ""}
</div>`;
}
function phaseMetaTag(value) {
return `<span class="tag">${escapeHtml(String(value || "unknown").replaceAll("_", " "))}</span>`;
}
function taskDocSection(task, fileName, title, required) {

@@ -88,0 +128,0 @@ const doc = taskDocument(task, fileName);

@@ -31,3 +31,5 @@ function taskDocument(task, fileName) {

function label(value) {
return t(`state_${value}`) || String(value || "unknown").replaceAll("_", " ");
const key = `state_${value}`;
const translated = t(key);
return translated === key ? String(value || "unknown").replaceAll("_", " ") : translated;
}

@@ -34,0 +36,0 @@

@@ -52,2 +52,64 @@ window.setModulePage = function(moduleKey, page) {

}));
document.querySelectorAll("[data-preset-search]").forEach((input) => input.addEventListener("input", () => {
state.presetQuery = input.value;
app();
}));
document.querySelectorAll("[data-preset-source-filter]").forEach((button) => button.addEventListener("click", () => {
state.presetSourceFilter = button.dataset.presetSourceFilter || "all";
state.selectedPresetKey = "";
state.presetUninstallConfirm = "";
app();
}));
document.querySelectorAll("[data-preset-select]").forEach((button) => button.addEventListener("click", () => {
state.selectedPresetKey = button.dataset.presetSelect || "";
state.selectedPresetId = "";
const selectedPreset = (bundle.presetCatalog?.presets || []).find((preset) => presetKey(preset) === state.selectedPresetKey);
if (selectedPreset && state.presetSourceFilter !== "all" && selectedPreset.source !== state.presetSourceFilter) {
state.presetSourceFilter = selectedPreset.source;
}
if (selectedPreset && !presetMatchesQuery(selectedPreset)) state.presetQuery = "";
if (selectedPreset && ["project", "user"].includes(selectedPreset.source)) state.presetUninstallScope = selectedPreset.source;
state.presetUninstallConfirm = "";
app();
}));
document.querySelectorAll("[data-preset-install-source]").forEach((input) => input.addEventListener("input", () => {
state.presetInstallSource = input.value;
}));
document.querySelectorAll("[data-preset-install-scope]").forEach((select) => select.addEventListener("change", () => {
state.presetInstallScope = select.value || "project";
}));
document.querySelectorAll("[data-preset-install-force]").forEach((input) => input.addEventListener("change", () => {
state.presetInstallForce = input.checked;
}));
document.querySelectorAll("[data-preset-seed-scope]").forEach((select) => select.addEventListener("change", () => {
state.presetSeedScope = select.value || "project";
}));
document.querySelectorAll("[data-preset-seed-force]").forEach((input) => input.addEventListener("change", () => {
state.presetSeedForce = input.checked;
}));
document.querySelectorAll("[data-preset-uninstall-scope]").forEach((select) => select.addEventListener("change", () => {
state.presetUninstallScope = select.value || "project";
}));
document.querySelectorAll("[data-preset-uninstall-confirm]").forEach((input) => input.addEventListener("input", () => {
state.presetUninstallConfirm = input.value;
}));
document.querySelectorAll("[data-preset-fill-confirm]").forEach((button) => button.addEventListener("click", () => {
state.presetUninstallConfirm = button.dataset.presetFillConfirm || "";
app();
}));
document.querySelectorAll("[data-preset-check]").forEach((button) => button.addEventListener("click", () => runPresetAction("check", { id: button.dataset.presetCheck || "" })));
document.querySelectorAll("[data-preset-install]").forEach((button) => button.addEventListener("click", () => runPresetAction("install", {
source: state.presetInstallSource,
scope: state.presetInstallScope,
force: state.presetInstallForce,
})));
document.querySelectorAll("[data-preset-seed]").forEach((button) => button.addEventListener("click", () => runPresetAction("seed", {
scope: state.presetSeedScope,
force: state.presetSeedForce,
})));
document.querySelectorAll("[data-preset-uninstall]").forEach((button) => button.addEventListener("click", () => runPresetAction("uninstall", {
id: button.dataset.presetUninstall || "",
scope: state.presetUninstallScope,
confirmText: state.presetUninstallConfirm,
})));
document.querySelectorAll("[data-review-queue-tab]").forEach((button) => button.addEventListener("click", () => {

@@ -99,2 +161,3 @@ state.reviewQueueTab = button.dataset.reviewQueueTab || "review";

bindCopyTaskNameButtons(document);
bindPresetCopyButtons(document);
bindRepairPromptButtons(document);

@@ -182,2 +245,41 @@ bindLessonSedimentationButtons(document);

async function runPresetAction(action, body) {
state.presetActionResult = { ok: true, title: t("presetActionRunning"), message: action };
app();
try {
const response = await fetch(`/api/presets/${action}`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-harness-csrf": state.runtime?.csrfToken || "",
},
body: JSON.stringify(body),
});
const payload = await response.json();
if (!response.ok) throw payload;
state.presetActionResult = {
ok: true,
title: t("presetActionSuccess"),
message: presetActionMessage(action, payload),
};
app();
if (["install", "seed", "uninstall"].includes(action)) setTimeout(() => window.location.reload(), 650);
} catch (error) {
state.presetActionResult = {
ok: false,
title: t("presetActionFailed"),
message: error?.error || error?.message || String(error || action),
};
app();
}
}
function presetActionMessage(action, payload) {
if (action === "check") return `${payload.id || ""} ${payload.status || ""}`.trim();
if (action === "install") return `${payload.id || ""} -> ${payload.scope || ""}`.trim();
if (action === "seed") return `${payload.created || 0} ${t("created")} · ${payload.skipped || 0} ${t("skipped")}`;
if (action === "uninstall") return `${payload.id || ""} ${payload.removed ? t("removed") : t("notInstalled")}`.trim();
return action;
}
function renderDrawerContent(taskId) {

@@ -262,2 +364,31 @@ const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);

function bindPresetCopyButtons(root) {
root.querySelectorAll("[data-copy-preset-id]").forEach((button) => button.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
const presetId = button.dataset.copyPresetId || "";
const defaultText = button.textContent;
try {
await copyText(presetId);
button.textContent = t("copyTaskNameSuccess");
} catch {
button.textContent = t("copyTaskNameFailed");
}
setTimeout(() => { button.textContent = defaultText; }, 1200);
}));
root.querySelectorAll("[data-copy-preset-command]").forEach((button) => button.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
const command = button.dataset.copyPresetCommand || "";
const defaultText = button.textContent;
try {
await copyText(command);
button.textContent = t("copyTaskNameSuccess");
} catch {
button.textContent = t("copyTaskNameFailed");
}
setTimeout(() => { button.textContent = defaultText; }, 1200);
}));
}
function bindRepairPromptButtons(root) {

@@ -264,0 +395,0 @@ root.querySelectorAll("[data-copy-repair-prompt]").forEach((button) => button.addEventListener("click", async (event) => {

@@ -8,3 +8,4 @@ [

"css-src/40-detail-modules-migration.css",
"css-src/45-presets.css",
"css-src/50-responsive-overrides.css"
]

@@ -10,4 +10,5 @@ [

"app-src/50-migration.js",
"app-src/55-presets.js",
"app-src/60-shared.js",
"app-src/90-bindings.js"
]

@@ -297,2 +297,6 @@ @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap');

.status-primary.snapshot {
border-color: rgba(71, 85, 105, 0.3);
}
.status-primary.fail {

@@ -299,0 +303,0 @@ border-color: rgba(225, 29, 72, 0.3);

@@ -39,2 +39,19 @@ /* Detail view panels */

.phase-kind-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-column: 1 / -1;
gap: 12px;
}
.phase-kind-group h3 {
grid-column: 1 / -1;
margin: 4px 0 0;
color: var(--muted);
font-size: 12px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0;
}
.phase-step {

@@ -50,2 +67,18 @@ background: var(--paper);

.phase-step.init {
border-left: 4px solid #64748b;
}
.phase-step.execution {
border-left: 4px solid var(--accent);
}
.phase-step.gate {
border-left: 4px solid #9f7aea;
}
.phase-step.other {
border-left: 4px solid var(--warn);
}
.card-header-actions {

@@ -69,2 +102,15 @@ display: flex;

.phase-step-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.phase-step-head span {
color: var(--muted);
font-size: 12px;
white-space: nowrap;
}
.phase-step p {

@@ -78,2 +124,18 @@ color: var(--muted);

.phase-meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.phase-exit-command {
display: block;
margin-top: 10px;
overflow-wrap: anywhere;
color: var(--ink);
font-size: 11px;
line-height: 1.4;
}
.detail-grid {

@@ -80,0 +142,0 @@ display: grid;

@@ -10,2 +10,4 @@ window.HarnessI18n = {

"readiness": "Readiness",
"snapshotStatus": "Snapshot status",
"snapshot": "Snapshot",
"tasks": "Tasks",

@@ -27,2 +29,3 @@ "blockers": "Blockers",

"noBlockers": "No blockers in this snapshot.",
"snapshotNotValidated": "Snapshot only; run validated status before release decisions.",
"firstLook": "First look",

@@ -137,2 +140,3 @@ "projectFlow": "Project Flow",

"state_fail": "fail",
"state_snapshot": "snapshot",
"state_in_progress": "in progress",

@@ -269,3 +273,68 @@ "state_planned": "planned",

"reviewCompleteSuccess": "Review confirmed. Reloading snapshot...",
"reviewCompleteFailed": "Review confirmation failed"
"reviewCompleteFailed": "Review confirmation failed",
"presetCatalog": "Presets",
"presetCatalogSubtitle": "Manage project, user, and bundled task method packages. Precedence is project first, then user, then bundled.",
"presetSearch": "Search presets",
"presetSearchPlaceholder": "Search id, purpose, kind, source, or path",
"presetSourceFilter": "Preset source filter",
"allPresets": "All",
"presetSourceProject": "Project",
"presetSourceUser": "User",
"presetSourceBuiltin": "Builtin",
"presetSource_project": "Project",
"presetSource_user": "User",
"presetSource_builtin": "Builtin",
"presetEffective": "Effective",
"presetShadowed": "Shadowed",
"presetSources": "Preset Sources",
"presetSourcesHint": "Roots are read in precedence order. Higher layers override lower layers with the same id.",
"presetPriorityTitle": "Preset precedence",
"presetCollection": "Preset collection",
"presetCollectionHint": "Select a preset to inspect its manifest and actions.",
"presetLayerStack": "Layer stack for this id",
"presetLayerStackHint": "Project presets override user presets. User presets override bundled presets.",
"presetContextActions": "Selected preset actions",
"presetCheckHint": "Validate the currently selected preset manifest.",
"presetShadowedActionHint": "This layer is shadowed. CLI check and inspect commands resolve the effective preset by id, so they are disabled here.",
"presetCheckSelected": "Check selected preset",
"presetUninstallSelected": "Uninstall selected layer",
"presetUninstallHint": "Scope is locked to the selected project or user layer.",
"presetBuiltinImmutable": "Bundled presets are package defaults and cannot be uninstalled.",
"presetImportTitle": "Import preset",
"presetImportHint": "Install a local folder, .zip archive, or bundled preset id into a project or user layer.",
"presetRestoreBundled": "Restore bundled presets",
"presetRestoreBundledHint": "Copies bundled presets into the selected local layer. CLI name: preset seed.",
"presetCommandsEffectiveOnly": "CLI inspect/check commands resolve the effective preset by id. This shadowed layer is shown for comparison only.",
"noPresets": "No presets found.",
"version": "Version",
"manifestVersion": "Manifest version",
"manifestPath": "Manifest path",
"source": "Source",
"scope": "Scope",
"taskKind": "Task kind",
"budgets": "Budgets",
"inputs": "Inputs",
"writeScopes": "Write scopes",
"requiredReads": "Required reads",
"presetActions": "Preset Actions",
"presetWorkbenchRequired": "Use the dynamic workbench to manage presets. Static snapshots are read-only.",
"presetCheck": "Check preset",
"presetInstall": "Install preset",
"presetSeed": "Restore bundled presets",
"presetUninstall": "Uninstall preset",
"presetInstallSourcePlaceholder": "Local folder, .zip, or bundled preset id",
"forceOverwrite": "Force overwrite",
"confirmPresetId": "Confirm preset id",
"copyPresetId": "Copy preset ID",
"copyIdShort": "Copy ID",
"copyCommand": "Copy command",
"useSelectedId": "Use selected ID",
"presetConfirmRequired": "Use the selected ID before uninstalling.",
"presetActionRunning": "Running preset action",
"presetActionSuccess": "Preset action completed",
"presetActionFailed": "Preset action failed",
"created": "created",
"skipped": "skipped",
"removed": "removed",
"notInstalled": "not installed"
},

@@ -280,2 +349,4 @@ "zh": {

"readiness": "发布状态",
"snapshotStatus": "快照状态",
"snapshot": "快照",
"tasks": "任务",

@@ -297,2 +368,3 @@ "blockers": "阻塞",

"noBlockers": "当前快照没有阻塞项。",
"snapshotNotValidated": "这里只是快照;发布决策前请运行完整验证状态。",
"firstLook": "第一眼",

@@ -400,2 +472,3 @@ "projectFlow": "项目流程图",

"state_fail": "失败",
"state_snapshot": "快照",
"state_in_progress": "进行中",

@@ -539,4 +612,69 @@ "state_planned": "计划中",

"reviewCompleteSuccess": "审查已确认,正在刷新快照...",
"reviewCompleteFailed": "审查确认失败"
"reviewCompleteFailed": "审查确认失败",
"presetCatalog": "Preset",
"presetCatalogSubtitle": "管理项目级、用户级和内置任务方法包。优先级是项目级、用户级、内置。",
"presetSearch": "搜索 preset",
"presetSearchPlaceholder": "搜索 ID、用途、类型、来源或路径",
"presetSourceFilter": "Preset 来源筛选",
"allPresets": "全部",
"presetSourceProject": "项目级",
"presetSourceUser": "用户级",
"presetSourceBuiltin": "内置",
"presetSource_project": "项目级",
"presetSource_user": "用户级",
"presetSource_builtin": "内置",
"presetEffective": "生效",
"presetShadowed": "被覆盖",
"presetSources": "Preset 来源",
"presetSourcesHint": "系统按优先级读取来源;同名时高优先级会覆盖低优先级。",
"presetPriorityTitle": "Preset 优先级",
"presetCollection": "Preset 列表",
"presetCollectionHint": "选择一个 preset,查看 manifest 详情和可用操作。",
"presetLayerStack": "同名层级",
"presetLayerStackHint": "项目级覆盖用户级,用户级覆盖内置。",
"presetContextActions": "当前 preset 操作",
"presetCheckHint": "校验当前选中的 preset manifest。",
"presetShadowedActionHint": "当前层级已被覆盖。CLI 检查和详情命令会按 ID 解析到生效 preset,因此这里禁用。",
"presetCheckSelected": "检查当前 preset",
"presetUninstallSelected": "卸载当前层级",
"presetUninstallHint": "范围会锁定到当前选中的项目级或用户级层。",
"presetBuiltinImmutable": "内置 preset 是包默认内容,不能卸载。",
"presetImportTitle": "导入 preset",
"presetImportHint": "把本地文件夹、.zip 压缩包或内置 preset ID 安装到项目级或用户级。",
"presetRestoreBundled": "恢复内置 preset",
"presetRestoreBundledHint": "把包内置 presets 写入选中的本地层级。CLI 名称是 preset seed。",
"presetCommandsEffectiveOnly": "CLI 详情/检查命令会按 ID 解析到生效 preset;当前被覆盖层只用于对比查看。",
"noPresets": "没有找到 preset。",
"version": "版本",
"manifestVersion": "Manifest 版本",
"manifestPath": "Manifest 路径",
"source": "来源",
"scope": "范围",
"taskKind": "任务类型",
"budgets": "预算",
"inputs": "输入项",
"writeScopes": "写入范围",
"requiredReads": "必读引用",
"presetActions": "Preset 操作",
"presetWorkbenchRequired": "管理 preset 需要使用动态 workbench;静态快照只读。",
"presetCheck": "检查 preset",
"presetInstall": "安装 preset",
"presetSeed": "恢复内置 preset",
"presetUninstall": "卸载 preset",
"presetInstallSourcePlaceholder": "本机文件夹、.zip 或内置 preset ID",
"forceOverwrite": "强制覆盖",
"confirmPresetId": "确认 preset ID",
"copyPresetId": "复制 preset ID",
"copyIdShort": "复制 ID",
"copyCommand": "复制命令",
"useSelectedId": "使用当前 ID",
"presetConfirmRequired": "卸载前请先使用当前 ID 完成确认。",
"presetActionRunning": "正在执行 preset 操作",
"presetActionSuccess": "Preset 操作完成",
"presetActionFailed": "Preset 操作失败",
"created": "已创建",
"skipped": "已跳过",
"removed": "已移除",
"notInstalled": "未安装"
}
};

@@ -27,3 +27,3 @@ # {{TASK_TITLE}}

- Lifecycle state: not_started
- Required files: `task_plan.md`, `execution_strategy.md`, `visual_map.md`, `progress.md`, `findings.md`, `review.md`
- Required files: `INDEX.md`, `task_plan.md`, `execution_strategy.md`, `visual_map.md`, `progress.md`, `findings.md`, `review.md`
- Required closeout: verification evidence recorded in `progress.md`

@@ -30,0 +30,0 @@

@@ -37,2 +37,3 @@ # Module Worker Session Prompt

- `done` means the implementation step finished. It is not `closed` until closeout evidence is recorded.
- Use the current task `visual_map.md` phase table as the lifecycle map. At the end of a slice, inspect the current gate phase and follow its `Exit Command` only when its `Actor` is `agent`.
- If review is required, update `review.md`. Human review completion must be confirmed through the local dashboard workbench or by the coordinator with `harness review-confirm`; do not mark it complete while open P0/P1/P2 findings remain.

@@ -39,0 +40,0 @@

@@ -64,20 +64,2 @@ # [Task Name] - Review

## Human Review Confirmation
This section must only be completed by a human reviewer or by a command/workbench action acting on explicit human confirmation. Agent review submission, self-review, and subagent review do not satisfy this gate.
| Field | Value |
| --- | --- |
| Confirmation ID | [generated by review-confirm] |
| Confirmed At | [timestamp] |
| Reviewer | [human name] |
| Reviewer Email | [email, if available] |
| Task Key | {{TASK_ID}} |
| Confirm Text | [must match the task id or confirmation phrase required by the command] |
| Evidence Checked | [review packet / tests / diff summary] |
| Commit SHA | [confirmation commit SHA generated by review-confirm] |
| Audit Status | committed / blocked |
Do not fill this section when the task still belongs in Missing Materials, Blocked, or Lessons routing. `review-confirm` and the Dashboard Workbench must reject dirty Git state, missing commit identity, hook failures, or writes outside the current task `review.md` / `progress.md` allowlist.
## Evidence Checked

@@ -84,0 +66,0 @@

# [Task Name]
Task Contract: harness-task/v1
Task Package Index: required

@@ -14,12 +15,8 @@ ## Goal

## Task Budget
## Selected Budget
| Budget | Use When | Required Structure |
| --- | --- | --- |
| simple | One owner, no subagent, L0/L1 evidence, no formal review gate | `brief.md`, `task_plan.md`, `visual_map.md`, `progress.md` |
| standard | Normal feature, fix, or documentation change | `brief.md`, `task_plan.md`, `execution_strategy.md`, `visual_map.md`, `findings.md`, `lesson_candidates.md`, `progress.md`, `review.md` |
| complex | Multi-hour work, L2/L3 evidence, subagent/reviewer, or optional artifact/reference indexes | Standard files plus `references/INDEX.md` and `artifacts/INDEX.md` |
Selected budget: {{TASK_BUDGET}}
Rationale: [why this budget fits this task]
## Context Packet

@@ -31,38 +28,2 @@

## Required Files
Do not hand-copy this template to create task directories. Use `harness new-task`
so the selected budget creates the correct file set and `harness check` can
enforce it.
| Budget | Required Files |
| --- | --- |
| simple | `brief.md`, `task_plan.md`, `visual_map.md`, `progress.md` |
| standard | simple files plus `execution_strategy.md`, `findings.md`, `lesson_candidates.md`, `review.md` |
| complex | standard files plus `references/INDEX.md`, `artifacts/INDEX.md` |
| long-running add-on | `long-running-task-contract.md` when `--long-running` is selected |
Optional subdirectories are created only when triggered:
- `lessons/LC-*.md`: task-local detail artifacts for lesson candidates marked `needs-promotion`.
- `references/INDEX.md`: complex-task source package and reference index.
- `artifacts/INDEX.md`: complex-task generated evidence and artifact index.
File purposes:
| Contract File | Purpose |
| --- | --- |
| `brief.md` | Human-readable task summary and current context packet |
| `task_plan.md` | Goal, scope, budget, acceptance, and operating decisions |
| `execution_strategy.md` | Operating model, allocation, conflict control, and evidence strategy |
| `visual_map.md` | Diagram collection: phase map, optional architecture/sequence/data-flow/state diagrams, completion state, evidence state, and blocking risk |
| `progress.md` | Execution log, decisions, and handoff |
| `findings.md` | Findings, research notes, and unresolved risks |
| `lesson_candidates.md` | Task-local lesson candidate queue. Human review must accept no-candidate, reject candidates, or queue promotion before review confirmation |
| `lessons/LC-*.md` | Optional task-local lesson detail artifacts written while source context is fresh and linked from `Detail Artifact` |
| `review.md` | Agent review submission, adversarial review, or specialist review report |
| `references/INDEX.md` | Complex-task source package and reference index |
| `artifacts/INDEX.md` | Complex-task generated evidence and artifact index |
| `long-running-task-contract.md` | Continuous execution permission, loop rules, and stop conditions |
## Steps

@@ -69,0 +30,0 @@

@@ -18,5 +18,5 @@ # [Task Name] - Visual Map

flowchart LR
PH01["PH-01 Plan"] --> PH02["PH-02 Implement"]
PH02 --> PH03["PH-03 Verify"]
PH03 --> PH04["PH-04 Review and Closeout"]
INIT01["INIT-01 Scope and Context\nkind=init"] --> EXEC01["EXEC-01 Implementation Slice\nkind=execution"]
EXEC01 --> GATE01["GATE-01 Agent Review Submission\nkind=gate"]
GATE01 --> GATE02["GATE-02 Human Review Confirmation\nkind=gate"]
```

@@ -26,11 +26,15 @@

| Phase ID | Depends On | State | Completion | Output | Required Evidence | Evidence Status | Blocking Risk | Owner / Handoff |
| --- | --- | --- | ---: | --- | --- | --- | --- | --- |
| PH-01 | none | planned | 0 | Approved task plan and execution strategy | `task_plan.md`, `execution_strategy.md` | missing | none | coordinator |
| PH-02 | PH-01 | planned | 0 | Scoped implementation or document update | diff, worker handoff, or artifact path | missing | [risk] | [owner] |
| PH-03 | PH-02 | planned | 0 | Verification evidence | commands, logs, screenshots, or runtime proof | missing | [risk] | [owner] |
| PH-04 | PH-03 | planned | 0 | Review disposition and closeout updates | `review.md`, progress update, ledger updates | missing | [risk] | coordinator |
| Phase ID | Kind | Depends On | State | Completion | Output | Required Evidence | Exit Command | Actor | Evidence Status | Blocking Risk | Owner / Handoff |
| --- | --- | --- | --- | ---: | --- | --- | --- | --- | --- | --- | --- |
| INIT-01 | init | none | planned | 0 | Approved task plan and execution strategy | `task_plan.md`, `execution_strategy.md` | `harness task-start {{TASK_ID}}` | agent | missing | none | coordinator |
| EXEC-01 | execution | INIT-01 | planned | 0 | Scoped implementation, document update, and verification evidence | diff, commands, worker handoff, or artifact path | `harness task-phase {{TASK_ID}} EXEC-01 --state done --completion 100 --evidence present` | agent | missing | [risk] | [owner] |
| GATE-01 | gate | EXEC-01 | planned | 0 | Agent Review Submission | `review.md`, progress update, lesson routing | `harness task-review {{TASK_ID}} --message "<summary>"` | agent | missing | [risk] | coordinator |
| GATE-02 | gate | GATE-01 | planned | 0 | Human Review Confirmation | review packet and human confirmation | `harness review-confirm {{TASK_ID}} --confirm {{TASK_ID}}` | human | missing | agent must not perform human confirmation | human |
Allowed Kind: init, execution, gate.
Allowed Actor: agent, human, coordinator.
Allowed Evidence Status: missing, partial, present, waived.
Dashboard implementation completion is computed from non-skipped `execution` phases only. `init` and `gate` phases route lifecycle readiness and next actions; they must not make implementation progress look incomplete.
## Supporting Maps

@@ -37,0 +41,0 @@

@@ -16,8 +16,34 @@ # Execution Workflow Standard

7. Proactively commit each verified, meaningful slice. A completed slice should not remain only as unstaged or staged working-tree state unless the user explicitly asked to defer commits, checks failed, dirty ownership is unclear, or a documented blocker prevents a clean commit. Deferred commits require a no-commit reason, owner, and next step.
8. Use CLI lifecycle commands for mechanical Harness writes whenever available. CLI-owned writes use locked, allowlisted auto-commit and refuse dirty Git state; agent-owned manual edits still need an explicit task commit or deferred-commit rationale.
9. Close the loop by updating walkthrough, SSoT, regression, ledger, or docs artifacts when the work changes durable project knowledge. New non-simple tasks should keep `lesson_candidates.md` reviewable before human review confirmation.
8. Use CLI lifecycle commands for mechanical Harness writes whenever available. CLI-owned writes use locked, allowlisted auto-commit. `new-task` may preserve unrelated dirty files while committing only its own write scope; overlapping dirty paths or unrelated staged files still block the command. Other lifecycle commands may still require a clean tree. Agent-owned manual edits still need an explicit task commit or deferred-commit rationale.
9. Treat `visual_map.md` as the lifecycle phase map. `init` phases prepare work, `execution` phases define implementation completion, and `gate` phases define review, human confirmation, lesson routing, and closeout. Follow a phase `Exit Command` only when its `Actor` matches the current operator; agents must not perform `human` gates.
10. New task directories must carry machine-readable Task Audit Metadata in `INDEX.md`. The normal path is `Created By: harness new-task`; manual creation is allowed only as `manual-exception` with a concrete reason, and historical migration must use `historical-backfill`.
11. Close the loop by updating walkthrough, SSoT, regression, ledger, or docs artifacts when the work changes durable project knowledge. New non-simple tasks should keep `lesson_candidates.md` reviewable before human review confirmation.
## Task Package Structure
Use `harness new-task` instead of hand-copying task files. The CLI creates the selected budget file set and records task audit metadata in `INDEX.md` so `harness check` can validate it.
| Budget | Required Files |
| --- | --- |
| simple | `INDEX.md`, `brief.md`, `task_plan.md`, `visual_map.md`, `progress.md` |
| standard | simple files plus `execution_strategy.md`, `findings.md`, `lesson_candidates.md`, `review.md` |
| complex | standard files plus `references/INDEX.md`, `artifacts/INDEX.md` |
| long-running add-on | `long-running-task-contract.md` when `--long-running` is selected |
`INDEX.md` is the task package navigation page and audit metadata SSoT. It points to contract files and optional indexes but does not replace them.
Optional directories are created only when triggered:
- `references/INDEX.md`: task-local sources, external links, reviewer input packages, and preset-provided required reads.
- `artifacts/INDEX.md`: command output, screenshots, fixtures, generated reports, reviews, and other evidence.
- `lessons/LC-*.md`: task-local lesson detail artifacts for candidates marked `needs-promotion`.
- `slices/<slice-id>/`: multi-slice task material, when a task explicitly uses slice packets.
Preset packages must not add or replace root-level base scaffold documents. Presets may append controlled sections to fixed base documents or add isolated resources under `references/**` and `artifacts/**`. Resource rows belong only in `references/INDEX.md` and `artifacts/INDEX.md`; the root `INDEX.md` may show only system-rendered preset summary fields.
## Required Checklist
- Goal, scope, acceptance criteria, and constraints are written down.
- Task Audit Metadata is present in `INDEX.md`, or the manual/historical exception reason is explicit.
- The task package `INDEX.md` is present for new generated tasks.
- Current repo state and ownership boundaries were checked.

@@ -27,2 +53,3 @@ - Implementation notes identify changed surfaces.

- Verified slices have commit SHAs, or the no-commit reason, owner, and next step are written down.
- The current lifecycle gate in `visual_map.md` has either been executed or has a recorded blocker.
- Review status and material findings are recorded.

@@ -29,0 +56,0 @@ - Residuals are explicit and assigned.

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display