---
description: harness .harness 状态目录分层协议。用于减少根目录散乱、统一 archive 前后结构，并兼容旧路径。
---

# State Layout Protocol

## 升级与本地状态保护边界

`.harness/archive/`、`.harness/changes/` 是 protected local roots。CLI init/configure/refresh/rules 操作前必须：

1. 综合 project marker、adapter `.harness-build.json`、managed block、恢复事务和非空 `.harness` 判断 `absent / valid / partial / recovery-required`；
2. 对 protected roots 记录文件数、目录数、字节数、Merkle root，以及 archive 首末 identity；
3. 事务 journal 声明每个允许修改的路径；任何未声明的 protected-root mutation 立即回滚；
4. partial 或 recovery-required 状态 fail closed，输出 sentinels、inventory 与恢复指引，禁止当成空项目自动 init。

事务提交前后 protected inventory 必须一致；除非当前命令显式以该 protected root 为操作目标。删除 `project.yaml` 但保留 archive，或删除 `.harness` 但保留 adapter marker，均属于 partial state，不是首次安装。

## 目标

`.harness/changes/<change-name>/` 是变更状态真相源，但根目录不得继续堆放所有文件。新产物按子目录分层；旧路径保留读取兼容。

## 双根布局（split-v1，2026-07 起）

新 Change 将**静态合同**与**动态运行状态**分离到两个唯一所有者：

```text
.harness/changes/<change-name>/        # contractRoot：静态合同（spec/plans/meta）
.harness/state/changes/<change-name>/  # stateRoot：动态状态（events/logs/ledger/tracking/reports/runtime）
```

- 合同由 `meta/change-context.json` 的 `stateOwnership.contractRoot` / `stateOwnership.runtimeRoot` 声明（schemaVersion 2）。
- 动态产物（`events.ndjson`、`logs/execution-log.md`、`evidence/verification-ledger.json`、`evidence/test-tracking.json`、reports、runtime）只写 stateRoot。
- 统一解析入口：`harness_paths.py` —— `resolve_change_layout()` 返回完整双根；`resolve_state_dir_for_contract()` 供 events/ledger/test_guard/gate 内部路由。
- **legacy-colocated**：未声明 `stateOwnership` 的旧 Change 继续读写共址布局；resolve 只读，**绝不静默搬迁**；显式迁移须 copy → hash verify → atomic pointer switch。
- 功能分支不得携带共享动态状态副本；checkpoint/archive 的 evidence snapshot 从 stateRoot 生成。

## 推荐结构

```text
.harness/changes/<change-name>/
├── meta/
│   ├── change-context.json
│   ├── worktree.json
│   ├── manifest.json
│   └── archive-meta.md
├── logs/
│   └── execution-log.md
├── spec/
├── plans/
├── evidence/
│   ├── verification-ledger.json
│   └── run-task-status.md
├── reports/
│   ├── test/
│   ├── review/
│   ├── package/
│   └── final/
│       └── summary-data.json
├── sqls/
├── scripts/
├── runtime/
│   ├── service-session.json
│   ├── run-sessions/<session-id>/
│   │   ├── session.json
│   │   ├── stdout.log
│   │   └── stderr.log
│   ├── environment-receipts/
│   ├── invalidations/
│   └── fixback/batches/
└── backups/
    └── uncommitted-tests/
```

## service-session.json 与 serviceStart 契约

`runtime/service-session.json`（由 `harness_service.py ensure` 写入）记录 AI 托管服务会话：`pid`、`startedBy`（`AI`/`User`）、`moduleInputsHash`、`moduleInputsFiles`、`profile`、`startCommandHash`、`overlayPath`、`command`、`startedAt`。

服务复用（§5.3）必须**同时**比对 `moduleInputsHash` + `startCommandHash` + `profile` + `overlayPath` + 进程身份（pid 存活 + create time 匹配 `startedAt`）。任一变化 -> AI 自动 restart；身份无法确认 -> `needs-user-decision`；非 AI 用户进程永不自动 kill。

`build-profile.json` 的 `serviceStart.inputFiles`（glob 列表，相对 project 展开）是 `moduleInputsHash` 的来源。`harness_service.py ensure` 取 CLI `--files` ∪ `serviceStart.inputFiles` 计算依赖闭包；**空输入被拒绝**（exit 非 0），**不得生成可复用的空指纹**。通用项目 detect 无法猜 module 源，`inputFiles` 默认空数组，须人工配置。

过期的 service session 只有在进程身份明确不匹配时才允许由 `retire-stale` 移入
`runtime/retired-service-sessions/`；该动作保留原始回执且不终止进程。受管命令、
环境会话和 fixback 的完整合同见 `execution-session-protocol.md`。

## 归档上传重试状态（§8）

知识 ingest 由 Hunter Platform 在接收归档 ZIP 后执行，本地不再创建 `.harness/knowledge`
或 maintenance-outbox。尚未被服务端确认持久化的包保存在：

```text
.harness/state/local/archive-packages/
  <change-key>.zip              # 确定性核心归档包
  <change-key>.upload.json      # 上传/失败收据（如存在）
```

ZIP 只包含 summary、spec、plans、archive-meta、change-context 和包 manifest。上传失败保留；
每次 finalize 都先生成 ZIP 与对应回执，再按 `credentials.local.yaml` 或
`project.yaml` 的 `server.url` + `server.token_env` 环境变量解析远端凭据。缺少凭据、
网络失败、无效收据或知识状态为 `indexing`/`failed` 时均保留这两个按 change 命名的文件，
因此可枚举 `*.upload.json` 独立重试，不会由后一次归档覆盖前一次。只有 CLI 已核对
package SHA-256，且服务端同时确认 `archive_status=durable` 与
`knowledge_status=ready` 时，才清理对应 ZIP 和回执。回执的 `uploadStatus` 为
`pending|failed|ready`，`reasonCode` 使用 `ARCHIVE_UPLOAD_*` 或
`ARCHIVE_KNOWLEDGE_*` 稳定错误码；`indexing` 必须映射为 `pending`，不得误报失败。
远端知识查询失败不得创建本地 fallback。

## 读取兼容

所有 skill 读取状态时必须先读新路径，再兼容旧路径：

| 类型 | 新路径 | 旧路径兼容 |
|---|---|---|
| execution log | `logs/execution-log.md` | `execution-log.md` |
| ledger | `evidence/verification-ledger.json` | `verification-ledger.json` |
| worktree | `meta/worktree.json` | `worktree.json` |
| run status | `evidence/run-task-status.md` | `run-task-status.md` |
| final summary data | `reports/final/summary-data.json` | `summary-data.json` |

## 写入规则

新版本 skill 默认写新路径。为平滑迁移，允许同时在旧路径写一个简短指针文件，但不得再把大量产物堆在根目录。

## change-context.json

每个变更目录建议尽早写入：

```json
{
  "changeName": "<change-name>",
  "stateDir": ".harness/changes/<change-name>",
  "logsDir": ".harness/changes/<change-name>/logs",
  "evidenceDir": ".harness/changes/<change-name>/evidence",
  "reportsDir": ".harness/changes/<change-name>/reports",
  "scriptsDir": ".harness/changes/<change-name>/scripts",
  "archiveTarget": ".harness/archive/YYYY-MM-DD-<change-name>"
}
```

后续阶段应优先从该文件读取路径，避免手拼 change-name 导致路径拼写错误。

## state-snapshot.json（cluster 3 §3.6）

`meta/state-snapshot.json`（由 `harness_state.py` 写入）集中记录 project/worktree root、不可变 `changeBase`、当前 HEAD，以及 profile/rules/map/knowledge/diff 各段指纹与相关文件。Plan 首次捕获把当时 HEAD 写入 `changeBase`；run/test/review/submit/archive 刷新时只能更新当前 HEAD 和各段指纹。失效时由脚本刷新，**不得仅凭缓存跳过代码或验证门禁，也不得把后续 HEAD 覆盖为变更基线**。

schema：

```json
{
  "schemaVersion": 1,
  "generatedAt": "<iso>",
  "changeName": "<change-name>",
  "changeBase": "<首次 Plan 捕获的 sha>",
  "project": {"root": "<abs>"},
  "worktree": {"root": "<abs>"},
  "git": {"base": "<sha>", "head": "<sha>"},
  "segments": {
    "<segment>": {"fingerprint": "sha256:...", "files": ["..."], "capturedAt": "<iso>"}
  }
}
```

各段独立失效：`is_segment_stale(snapshot, segment, current_fingerprint)` 比较单段指纹；段不存在 → stale（需采集）。`refresh_segments(..., segments=["profile"])` 只重采受影响段，其他段保留原 capturedAt/fingerprint（缓存失效只重采受影响段）。`git.base` 是 `changeBase` 的兼容镜像，不是每次 capture 的 HEAD。

segment 的文件集由调用方（各 skill）决定：`capture_snapshot(..., segment_files={"profile": [...], "rules": [...]})`。snapshot 只负责采集 + 比对 + 失效，不负责发现文件。git 段记录 base/head；diff 段指纹由调用方按需用 `harness_ledger.compute_diff_hash` 采集后填入 segment_files。

## archive 结构

archive 后保持同样分层结构，不把 `.bak`、脚本、manifest 或其他生成文件堆在 archive 根目录。

未提交但用于验证的测试文件放入：

```text
backups/uncommitted-tests/
```

并在 `summary-data.json.uncommittedTestEvidence[]` 中说明。

## 项目配置：`.harness/config/harness.json`

Harness skill 读取的项目级配置（不提交 git）。缺失时使用下列默认值，**不得因缺失而阻断流程**——记 `decision` 事件说明使用了默认值。

```json
{
  "defaultWorktree": false,
  "knowledge": {
    "manualReview": false
  }
}
```

| 字段 | 类型 | 默认 | 用途 |
|------|------|------|------|
| `defaultWorktree` | boolean | `false` | plan「设计审批包」中 worktree 推荐的预填值；用户可在审批包内覆盖 |
| `knowledge.manualReview` | boolean | `false` | `true` 时 knowledge-ingest promote 等高价值操作需人工确认；`false` 时按 skill 默认策略 |

读取顺序：`.harness/config/harness.json` → 缺失则默认值。plan 阶段 5 设计审批包须读取 `defaultWorktree` 作为 worktree 选项的推荐值。

## 可恢复文件事务

schema v3 事务在本地 `transactions/<transaction-id>/` 保存 journal、before snapshot、staged payload、after manifest 和小型 status 投影。journal 固定 project identity、执行器版本、目标 Bundle identity、ownership manifest hash、plan hash 与 snapshot digest。

未完成事务同时镜像到用户级恢复根：

```text
${HUNTER_HARNESS_RECOVERY_ROOT:-~/.hunter-harness/recovery}/<project-path-hash>/transactions/<transaction-id>/
```

因此项目内 `.harness` 局部丢失后，`status` 仍可只读发现恢复项。`resume` 仅在 plan/snapshot/staged digest 与已完成操作的当前文件状态全部匹配时继续 pending operations；任一不匹配均 fail closed。`rollback` 在写回 before snapshot 前重新校验 snapshot digest。事务提交或回滚成功后删除用户级镜像；本地 committed 事务仍按保留策略提供显式回滚。
