@hunter-harness/workflow-harness 0.2.20 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/CONTEXT.md +20 -0
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/checklist.md +6 -0
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +6 -0
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/claude-code/scripts/harness_events.py +159 -1
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +127 -0
- package/harness/bundles/general/claude-code/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/CONTEXT.md +20 -0
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +6 -0
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +6 -0
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +159 -1
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +127 -0
- package/harness/bundles/general/codebuddy/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/CONTEXT.md +20 -0
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/checklist.md +6 -0
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/checklist.md +6 -0
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/codex/scripts/harness_events.py +159 -1
- package/harness/bundles/general/codex/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +127 -0
- package/harness/bundles/general/codex/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/CONTEXT.md +20 -0
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/checklist.md +6 -0
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/checklist.md +6 -0
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/cursor/scripts/harness_events.py +159 -1
- package/harness/bundles/general/cursor/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +127 -0
- package/harness/bundles/general/cursor/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/CONTEXT.md +20 -0
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +6 -0
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/claude-code/scripts/harness_events.py +159 -1
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +127 -0
- package/harness/bundles/java/claude-code/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/CONTEXT.md +20 -0
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +6 -0
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +159 -1
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +127 -0
- package/harness/bundles/java/codebuddy/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/CONTEXT.md +20 -0
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/checklist.md +6 -0
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/codex/scripts/harness_events.py +159 -1
- package/harness/bundles/java/codex/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +127 -0
- package/harness/bundles/java/codex/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/CONTEXT.md +20 -0
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/checklist.md +6 -0
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/cursor/scripts/harness_events.py +159 -1
- package/harness/bundles/java/cursor/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +127 -0
- package/harness/bundles/java/cursor/scripts/harness_migration_head.py +220 -0
- package/harness/manifests/general/claude-code.json +25 -17
- package/harness/manifests/general/codebuddy.json +25 -17
- package/harness/manifests/general/codex.json +25 -17
- package/harness/manifests/general/cursor.json +25 -17
- package/harness/manifests/java/claude-code.json +26 -18
- package/harness/manifests/java/codebuddy.json +26 -18
- package/harness/manifests/java/codex.json +26 -18
- package/harness/manifests/java/cursor.json +26 -18
- package/hunter-workflow-family.json +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ name: harness-codebase-map
|
|
|
3
3
|
description: 分析现有代码库并生成 .harness/codebase/map/
|
|
4
4
|
下的结构化代码库地图。用于陌生项目理解、重大改造前梳理、harness-sync 提示 map 过期后的人工刷新。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=cd03d22b60760428; overlay=none; agent=cursor; do not edit -->
|
|
7
7
|
# harness-codebase-map — 代码库地图
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-knowledge-ingest
|
|
|
3
3
|
description: 从 .harness/archive 归档整理、同步和维护项目知识索引。适用场景:ingest knowledge、sync
|
|
4
4
|
knowledge、rebuild knowledge index、promote knowledge、确认知识条目、检查知识库是否过期。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=cd03d22b60760428; overlay=none; agent=cursor; do not edit -->
|
|
7
7
|
# harness-knowledge-ingest
|
|
8
8
|
|
|
9
9
|
从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
|
|
@@ -3,7 +3,7 @@ name: harness-knowledge-query
|
|
|
3
3
|
description: 根据新需求、设计任务、代码修改请求或问题排查,在规划和编码前查询 .harness/knowledge
|
|
4
4
|
历史上下文。适用场景:query knowledge、查找历史需求、根据归档理解需求、继续之前类似开发、生成需求上下文包。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=cd03d22b60760428; overlay=none; agent=cursor; do not edit -->
|
|
7
7
|
# harness-knowledge-query
|
|
8
8
|
|
|
9
9
|
并行 change 中查询必须传 `--change <change-id>`;成功后以该 change 的 `meta/knowledge-context.json` 为稳定指针。全局 `context-packs/latest.json` 仅向后兼容,不得用于判断多个 active change 的上下文归属。
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: harness-plan
|
|
3
3
|
description: 将需求转化为设计文档+实施计划+完整测试场景表,必须在编码前完成。使用场景:需求分析、feature plan、技术方案设计、实现方案规划
|
|
4
4
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
5
|
+
<!-- generated by harness_deploy.py; core=cd03d22b60760428; overlay=none; agent=cursor; do not edit -->
|
|
6
6
|
# harness-plan — 需求规划
|
|
7
7
|
|
|
8
8
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-review
|
|
|
3
3
|
description: 6维度代码审查(架构/安全/规范/兼容/测试/性能),对照项目规则(见
|
|
4
4
|
.harness/context-index.json)和测试场景表,在隔离上下文运行。使用场景:代码审查、提交前检查、合并评审
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=cd03d22b60760428; overlay=none; agent=cursor; do not edit -->
|
|
7
7
|
# harness-review — 代码审查
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: harness-run
|
|
3
3
|
description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编译验证),逐变更簇实现计划中的任务。使用场景:开始编码、实现功能、写代码、TDD 编码
|
|
4
4
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
5
|
+
<!-- generated by harness_deploy.py; core=cd03d22b60760428; overlay=none; agent=cursor; do not edit -->
|
|
6
6
|
# harness-run — 需求编码
|
|
7
7
|
|
|
8
8
|
## Purpose
|
|
@@ -267,3 +267,9 @@ description: harness-run 的执行检查清单。仅在编码执行时读取。
|
|
|
267
267
|
- [ ] 如果新增筛选参数,设计中已明确非法值行为
|
|
268
268
|
- [ ] 如果采用“忽略并不过滤”,必须有用户确认记录
|
|
269
269
|
- [ ] 测试场景覆盖非法值
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
## Wave-2 — Migration head(H-7)
|
|
273
|
+
|
|
274
|
+
- [ ] 若变更触及 alembic/versions 或项目约定 migration 路径:列出 config / CI / env / manifest 联动文件
|
|
275
|
+
- [ ] 消费项目存在 .harness/config/migration-head.json 时,跑 harness_migration_head.py check --project . --json
|
|
@@ -3,7 +3,7 @@ name: harness-submit
|
|
|
3
3
|
description: 最终提交封装:验证→中文 commit→提交/推送;worktree 模式含 --no-ff
|
|
4
4
|
合并回主分支。使用场景:提交代码、commit、push、合并分支、merge to main、完成开发
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=cd03d22b60760428; overlay=none; agent=cursor; do not edit -->
|
|
7
7
|
# harness-submit — 最终提交(含 worktree 合并)
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -420,3 +420,9 @@ ledger 顶层写入 `"mergeFinalHash": "<journal pushedHead>"`;经 `harness_ga
|
|
|
420
420
|
- worktree: 已清理(迁根后)
|
|
421
421
|
- 下一步: `/harness-archive`
|
|
422
422
|
```
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
## Wave-2 — 验证复用与 merge 面(H-9)
|
|
426
|
+
|
|
427
|
+
- [ ] 代码冻结后,同 identity 的 unitTestFull 须 can-reuse;禁止无 invalidationCode 整套重跑
|
|
428
|
+
- [ ] integration verify 会合并 profile mergeVerification.requiredOnMerge(若配置)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: harness-sync
|
|
3
3
|
description: 检查并更新项目AI元数据(CodeGraph索引、harness-codebase-map分析、CLAUDE.md、AGENTS.md、.harness/完整性),确保Claude对代码库的理解与最新代码一致。当用户说'同步/更新索引/刷新元数据/检查一致性'时使用
|
|
4
4
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
5
|
+
<!-- generated by harness_deploy.py; core=cd03d22b60760428; overlay=none; agent=cursor; do not edit -->
|
|
6
6
|
# harness-sync — 元数据同步
|
|
7
7
|
|
|
8
8
|
## Purpose
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: harness-test
|
|
3
3
|
description: 测试执行:读取场景表,执行单元测试+API接口测试+数据兼容验证,输出测试报告。当用户说'跑测试/验证/跑用例/接口测试/单元测试'时使用
|
|
4
4
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
5
|
+
<!-- generated by harness_deploy.py; core=cd03d22b60760428; overlay=none; agent=cursor; do not edit -->
|
|
6
6
|
# harness-test — 测试执行
|
|
7
7
|
|
|
8
8
|
## Purpose
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# model-routing-protocol — 能力级模型路由(Wave-2 H-16)
|
|
2
|
+
|
|
3
|
+
> 本协议定义 **economy / balanced / frontier** 三档能力级。通用 Harness 规则与 skill **禁止**硬编码供应商模型名(如 `claude-*`、`gpt-*`、`gemini-*`)。宿主把能力级映射到具体模型;映射表属于项目/宿主配置,不属于本协议正文。
|
|
4
|
+
|
|
5
|
+
## 能力级
|
|
6
|
+
|
|
7
|
+
| 级别 | 适用任务 | 示例 |
|
|
8
|
+
|------|----------|------|
|
|
9
|
+
| `economy` | 机械提取、日志压缩、等待监控、格式化摘要 | 解析测试输出行、压缩长日志、轮询服务就绪 |
|
|
10
|
+
| `balanced` | 常规实现、常规修复、常规文档修订 | 按 plan 实现 CRUD、修 YELLOW、补 checklist |
|
|
11
|
+
| `frontier` | 架构取舍、安全/并发/迁移、最终审查、高风险判断 | 设计审批、RED 安全项、schema 迁移方案、merge 前终审 |
|
|
12
|
+
|
|
13
|
+
## 路由规则
|
|
14
|
+
|
|
15
|
+
1. Skill/任务描述用能力级(`--capability economy|balanced|frontier` 或文案「本任务=frontier」),不写供应商模型 id。
|
|
16
|
+
2. 宿主或 `HUNTER_HARNESS_MODEL` 仅作**遥测**(`executor_model`),不得反过来驱动通用规则分支。
|
|
17
|
+
3. **降级**(frontier→balanced→economy)必须由确定性命令结果或更高能力审查验收;不得用更低能力级的模型输出替代测试/门禁证据。
|
|
18
|
+
4. 无法映射某能力级时:fail closed,列出可用级别,禁止静默落到未知模型。
|
|
19
|
+
|
|
20
|
+
## 与验证的关系
|
|
21
|
+
|
|
22
|
+
能力级不改变 ledger / can-reuse / verification graph 合同。模型便宜不能成为跳过 `unitTestFull` 或忽略 `requiredOnMerge` 的理由。
|
|
@@ -24,7 +24,7 @@ import tempfile
|
|
|
24
24
|
import time
|
|
25
25
|
import uuid
|
|
26
26
|
from pathlib import Path
|
|
27
|
-
from typing import Any
|
|
27
|
+
from typing import Any, Sequence
|
|
28
28
|
|
|
29
29
|
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
30
30
|
if str(SCRIPTS_DIR) not in sys.path:
|
|
@@ -595,6 +595,132 @@ def append_event(
|
|
|
595
595
|
return {"ok": True, "event": event, "events_path": str(events_path_obj), "rendered": False}
|
|
596
596
|
|
|
597
597
|
|
|
598
|
+
def batch_append_events(
|
|
599
|
+
change_dir: Path,
|
|
600
|
+
events: Sequence[dict[str, Any]],
|
|
601
|
+
) -> dict[str, Any]:
|
|
602
|
+
"""Validate then append many events under one lock (Wave-2 H-15).
|
|
603
|
+
|
|
604
|
+
Any invalid item aborts the whole batch before mutation. ``phase.end`` items
|
|
605
|
+
are rejected here — use single ``append`` so render semantics stay explicit.
|
|
606
|
+
"""
|
|
607
|
+
if not isinstance(events, (list, tuple)) or not events:
|
|
608
|
+
return {
|
|
609
|
+
"ok": False,
|
|
610
|
+
"code": "BATCH_EMPTY",
|
|
611
|
+
"message": "batch-append requires a non-empty events array",
|
|
612
|
+
}
|
|
613
|
+
archived = archived_change_dir(change_dir)
|
|
614
|
+
if archived is not None:
|
|
615
|
+
return {
|
|
616
|
+
"ok": False,
|
|
617
|
+
"code": "ARCHIVED_CHANGE_IMMUTABLE",
|
|
618
|
+
"message": str(archived),
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
built: list[dict[str, Any]] = []
|
|
622
|
+
for index, raw in enumerate(events):
|
|
623
|
+
if not isinstance(raw, dict):
|
|
624
|
+
return {
|
|
625
|
+
"ok": False,
|
|
626
|
+
"code": "BATCH_ITEM_INVALID",
|
|
627
|
+
"message": f"events[{index}] must be an object",
|
|
628
|
+
"index": index,
|
|
629
|
+
}
|
|
630
|
+
type_ = str(raw.get("type") or "").strip()
|
|
631
|
+
phase = str(raw.get("phase") or "").strip()
|
|
632
|
+
if not type_ or type_ not in EVENT_TYPES:
|
|
633
|
+
return {
|
|
634
|
+
"ok": False,
|
|
635
|
+
"code": "EVENT_TYPE_INVALID",
|
|
636
|
+
"message": f"events[{index}]: unsupported type: {type_}",
|
|
637
|
+
"index": index,
|
|
638
|
+
}
|
|
639
|
+
if not phase:
|
|
640
|
+
return {
|
|
641
|
+
"ok": False,
|
|
642
|
+
"code": "EVENT_REQUIRED_FIELD",
|
|
643
|
+
"message": f"events[{index}]: phase required",
|
|
644
|
+
"index": index,
|
|
645
|
+
}
|
|
646
|
+
if type_ == "phase.end":
|
|
647
|
+
return {
|
|
648
|
+
"ok": False,
|
|
649
|
+
"code": "BATCH_PHASE_END_FORBIDDEN",
|
|
650
|
+
"message": f"events[{index}]: phase.end must use single append",
|
|
651
|
+
"index": index,
|
|
652
|
+
}
|
|
653
|
+
args = argparse.Namespace(
|
|
654
|
+
change_dir=str(change_dir),
|
|
655
|
+
phase=phase,
|
|
656
|
+
type=type_,
|
|
657
|
+
note=raw.get("note"),
|
|
658
|
+
kind=raw.get("kind"),
|
|
659
|
+
path=raw.get("path"),
|
|
660
|
+
run_id=raw.get("run_id"),
|
|
661
|
+
executor_tool=raw.get("executor_tool"),
|
|
662
|
+
command=raw.get("command"),
|
|
663
|
+
exit_code=raw.get("exit_code"),
|
|
664
|
+
duration_ms=raw.get("duration_ms"),
|
|
665
|
+
status=raw.get("status"),
|
|
666
|
+
name=raw.get("name"),
|
|
667
|
+
code=raw.get("code"),
|
|
668
|
+
severity=raw.get("severity"),
|
|
669
|
+
message=raw.get("message"),
|
|
670
|
+
decision=raw.get("decision"),
|
|
671
|
+
reason=raw.get("reason"),
|
|
672
|
+
issue_id=raw.get("issue_id"),
|
|
673
|
+
scope=raw.get("scope"),
|
|
674
|
+
target_event_id=raw.get("target_event_id"),
|
|
675
|
+
target_field=raw.get("target_field"),
|
|
676
|
+
old_value_hash=raw.get("old_value_hash"),
|
|
677
|
+
new_value_json=raw.get("new_value_json"),
|
|
678
|
+
renamed_from=raw.get("renamed_from"),
|
|
679
|
+
renamed_to=raw.get("renamed_to"),
|
|
680
|
+
change_uuid=raw.get("change_uuid"),
|
|
681
|
+
attempt=raw.get("attempt"),
|
|
682
|
+
executor_agent=raw.get("executor_agent"),
|
|
683
|
+
executor_model=raw.get("executor_model"),
|
|
684
|
+
handoff_from_tool=raw.get("handoff_from_tool"),
|
|
685
|
+
handoff_reason=raw.get("handoff_reason"),
|
|
686
|
+
trace_id=raw.get("trace_id"),
|
|
687
|
+
span_id=raw.get("span_id"),
|
|
688
|
+
parent_span_id=raw.get("parent_span_id"),
|
|
689
|
+
runner_ms=raw.get("runner_ms"),
|
|
690
|
+
orchestration_active_ms=raw.get("orchestration_active_ms"),
|
|
691
|
+
wall_clock_ms=raw.get("wall_clock_ms"),
|
|
692
|
+
user_wait_ms=raw.get("user_wait_ms"),
|
|
693
|
+
legacy_lenient=False,
|
|
694
|
+
json=True,
|
|
695
|
+
)
|
|
696
|
+
validation = validate_append_event(args)
|
|
697
|
+
if validation:
|
|
698
|
+
error_code, message = validation
|
|
699
|
+
return {
|
|
700
|
+
"ok": False,
|
|
701
|
+
"code": error_code,
|
|
702
|
+
"message": f"events[{index}]: {message}",
|
|
703
|
+
"index": index,
|
|
704
|
+
}
|
|
705
|
+
built.append(build_event(args, []))
|
|
706
|
+
|
|
707
|
+
events_path_obj = events_path(change_dir)
|
|
708
|
+
lock_path = events_path_obj.with_name(events_path_obj.name + ".lock")
|
|
709
|
+
with event_file_lock(lock_path):
|
|
710
|
+
for event in built:
|
|
711
|
+
atomic_append_line(
|
|
712
|
+
events_path_obj, json.dumps(event, ensure_ascii=False, separators=(",", ":"))
|
|
713
|
+
)
|
|
714
|
+
return {
|
|
715
|
+
"ok": True,
|
|
716
|
+
"action": "batch-append",
|
|
717
|
+
"count": len(built),
|
|
718
|
+
"events": built,
|
|
719
|
+
"events_path": str(events_path_obj),
|
|
720
|
+
"rendered": False,
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
|
|
598
724
|
def build_event(args: argparse.Namespace, existing: list[dict[str, Any]]) -> dict[str, Any]:
|
|
599
725
|
event: dict[str, Any] = {
|
|
600
726
|
"schema_version": SCHEMA_VERSION,
|
|
@@ -1295,6 +1421,25 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1295
1421
|
return 0
|
|
1296
1422
|
|
|
1297
1423
|
|
|
1424
|
+
def cmd_batch_append(args: argparse.Namespace) -> int:
|
|
1425
|
+
as_json = bool(args.json)
|
|
1426
|
+
change_dir = resolve_change_dir(args.change_dir)
|
|
1427
|
+
batch_path = Path(str(args.file)).expanduser()
|
|
1428
|
+
try:
|
|
1429
|
+
raw = json.loads(batch_path.read_text(encoding="utf-8-sig"))
|
|
1430
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1431
|
+
return emit_error(f"batch-append failed to read --file: {exc}", as_json=as_json)
|
|
1432
|
+
result = batch_append_events(change_dir, raw if isinstance(raw, list) else [])
|
|
1433
|
+
if not result.get("ok"):
|
|
1434
|
+
return emit_error(
|
|
1435
|
+
str(result.get("message") or "batch-append failed"),
|
|
1436
|
+
as_json=as_json,
|
|
1437
|
+
error_code=str(result.get("code") or "BATCH_APPEND_FAILED"),
|
|
1438
|
+
)
|
|
1439
|
+
emit_json(result, as_json=as_json)
|
|
1440
|
+
return 0
|
|
1441
|
+
|
|
1442
|
+
|
|
1298
1443
|
def cmd_render(args: argparse.Namespace) -> int:
|
|
1299
1444
|
as_json = bool(args.json)
|
|
1300
1445
|
change_dir = resolve_change_dir(args.change_dir)
|
|
@@ -1395,6 +1540,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1395
1540
|
)
|
|
1396
1541
|
p_append.set_defaults(func=cmd_append)
|
|
1397
1542
|
|
|
1543
|
+
p_batch = sub.add_parser(
|
|
1544
|
+
"batch-append",
|
|
1545
|
+
parents=[common],
|
|
1546
|
+
help="validate and append many events under one lock (H-15)",
|
|
1547
|
+
)
|
|
1548
|
+
p_batch.add_argument("--change-dir", required=True)
|
|
1549
|
+
p_batch.add_argument(
|
|
1550
|
+
"--file",
|
|
1551
|
+
required=True,
|
|
1552
|
+
help="JSON file containing a non-empty array of event objects",
|
|
1553
|
+
)
|
|
1554
|
+
p_batch.set_defaults(func=cmd_batch_append)
|
|
1555
|
+
|
|
1398
1556
|
p_render = sub.add_parser(
|
|
1399
1557
|
"render",
|
|
1400
1558
|
parents=[common],
|
|
@@ -862,8 +862,42 @@ class IntegrationTransaction:
|
|
|
862
862
|
|
|
863
863
|
return self._run_step("merge", action)
|
|
864
864
|
|
|
865
|
+
def _required_on_merge_commands(self) -> list[list[str]]:
|
|
866
|
+
"""Load build-profile mergeVerification.requiredOnMerge (Wave-2 H-9)."""
|
|
867
|
+
import harness_profile
|
|
868
|
+
|
|
869
|
+
profile = harness_profile.load_profile(self.project_root)
|
|
870
|
+
if not isinstance(profile, dict):
|
|
871
|
+
return []
|
|
872
|
+
merge = profile.get("mergeVerification")
|
|
873
|
+
if not isinstance(merge, dict):
|
|
874
|
+
return []
|
|
875
|
+
required = merge.get("requiredOnMerge")
|
|
876
|
+
if not isinstance(required, list):
|
|
877
|
+
return []
|
|
878
|
+
commands: list[list[str]] = []
|
|
879
|
+
for index, item in enumerate(required):
|
|
880
|
+
if isinstance(item, str) and item.strip():
|
|
881
|
+
commands.append(shlex.split(item.strip()))
|
|
882
|
+
continue
|
|
883
|
+
if not isinstance(item, dict):
|
|
884
|
+
raise ValueError(f"requiredOnMerge[{index}] must be string or object")
|
|
885
|
+
raw = item.get("command")
|
|
886
|
+
if isinstance(raw, list) and raw and all(isinstance(x, str) and x for x in raw):
|
|
887
|
+
commands.append([str(x) for x in raw])
|
|
888
|
+
elif isinstance(raw, str) and raw.strip():
|
|
889
|
+
commands.append(shlex.split(raw.strip()))
|
|
890
|
+
else:
|
|
891
|
+
raise ValueError(f"requiredOnMerge[{index}].command missing")
|
|
892
|
+
return commands
|
|
893
|
+
|
|
865
894
|
def verify(self, commands: Sequence[Sequence[str]] | None = None) -> dict[str, Any]:
|
|
866
895
|
commands = list(commands or [])
|
|
896
|
+
# Wave-2 H-9: append profile mergeVerification.requiredOnMerge commands.
|
|
897
|
+
try:
|
|
898
|
+
commands.extend(self._required_on_merge_commands())
|
|
899
|
+
except Exception as exc: # noqa: BLE001 — surface as verify plan error
|
|
900
|
+
raise VerifyPlanMissingError(f"mergeVerification load failed: {exc}") from exc
|
|
867
901
|
|
|
868
902
|
def action(journal: dict[str, Any]) -> None:
|
|
869
903
|
if not commands or any(
|
|
@@ -1055,6 +1055,7 @@ def decide_can_reuse(
|
|
|
1055
1055
|
requested_toolchain_hash: str | None = None,
|
|
1056
1056
|
requested_profile_hash: str | None = None,
|
|
1057
1057
|
requested_environment_hash: str | None = None,
|
|
1058
|
+
requested_db_schema_hash: str | None = None,
|
|
1058
1059
|
) -> dict[str, Any]:
|
|
1059
1060
|
ledger, ledger_path = load_ledger(change_dir)
|
|
1060
1061
|
if ledger is None:
|
|
@@ -1183,6 +1184,7 @@ def decide_can_reuse(
|
|
|
1183
1184
|
("toolchainHash", requested_toolchain_hash, "TOOLCHAIN_CHANGED"),
|
|
1184
1185
|
("profileHash", requested_profile_hash, "PROFILE_CHANGED"),
|
|
1185
1186
|
("environmentHash", requested_environment_hash, "ENVIRONMENT_CHANGED"),
|
|
1187
|
+
("dbSchemaHash", requested_db_schema_hash, "DB_SCHEMA_CHANGED"),
|
|
1186
1188
|
):
|
|
1187
1189
|
stored = entry.get(field)
|
|
1188
1190
|
if (
|
|
@@ -1249,6 +1251,7 @@ def decide_can_reuse(
|
|
|
1249
1251
|
"reuse": True,
|
|
1250
1252
|
"reason": "reuse",
|
|
1251
1253
|
"code": "REUSED",
|
|
1254
|
+
"invalidationCode": None,
|
|
1252
1255
|
"verification": verification,
|
|
1253
1256
|
"ledger_path": str(ledger_path) if ledger_path else None,
|
|
1254
1257
|
"inputsHash": stored_hash,
|
|
@@ -1258,6 +1261,90 @@ def decide_can_reuse(
|
|
|
1258
1261
|
}
|
|
1259
1262
|
|
|
1260
1263
|
|
|
1264
|
+
_INVALIDATION_ALIASES = {
|
|
1265
|
+
"INPUTS_HASH_CHANGED": "INPUT_CHANGED",
|
|
1266
|
+
"ENVIRONMENT_CHANGED": "ENV_CHANGED",
|
|
1267
|
+
"MISSING_FIELDS": "MISSING_IDENTITY_FIELD",
|
|
1268
|
+
"MISSING_V2_FIELDS": "MISSING_IDENTITY_FIELD",
|
|
1269
|
+
"LEDGER_MISSING": "MISSING_IDENTITY_FIELD",
|
|
1270
|
+
"VALIDATIONS_MISSING": "MISSING_IDENTITY_FIELD",
|
|
1271
|
+
"VALIDATION_MISSING": "MISSING_IDENTITY_FIELD",
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def _attach_invalidation_code(payload: dict[str, Any]) -> dict[str, Any]:
|
|
1276
|
+
"""Wave-2 H-9: expose stable invalidationCode alongside legacy code."""
|
|
1277
|
+
if payload.get("reuse") is True:
|
|
1278
|
+
payload.setdefault("invalidationCode", None)
|
|
1279
|
+
return payload
|
|
1280
|
+
code = str(payload.get("code") or "").strip()
|
|
1281
|
+
if code:
|
|
1282
|
+
payload["invalidationCode"] = _INVALIDATION_ALIASES.get(code, code)
|
|
1283
|
+
return payload
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def verification_graph_path(change_dir: Path) -> Path:
|
|
1287
|
+
return Path(change_dir) / "evidence" / "verification-graph.json"
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def upsert_verification_graph_node(
|
|
1291
|
+
change_dir: Path,
|
|
1292
|
+
*,
|
|
1293
|
+
verification: str,
|
|
1294
|
+
status: str,
|
|
1295
|
+
inputs_hash: str,
|
|
1296
|
+
command: str | None = None,
|
|
1297
|
+
toolchain_hash: str | None = None,
|
|
1298
|
+
environment_hash: str | None = None,
|
|
1299
|
+
db_schema_hash: str | None = None,
|
|
1300
|
+
diff_hash: str | None = None,
|
|
1301
|
+
) -> dict[str, Any]:
|
|
1302
|
+
"""Persist a verification graph node keyed by canonical identity (H-9)."""
|
|
1303
|
+
path = verification_graph_path(change_dir)
|
|
1304
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1305
|
+
graph: dict[str, Any]
|
|
1306
|
+
if path.is_file():
|
|
1307
|
+
try:
|
|
1308
|
+
loaded = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
1309
|
+
graph = loaded if isinstance(loaded, dict) else {"schemaVersion": 1, "nodes": []}
|
|
1310
|
+
except (OSError, json.JSONDecodeError):
|
|
1311
|
+
graph = {"schemaVersion": 1, "nodes": []}
|
|
1312
|
+
else:
|
|
1313
|
+
graph = {"schemaVersion": 1, "nodes": []}
|
|
1314
|
+
nodes = graph.get("nodes")
|
|
1315
|
+
if not isinstance(nodes, list):
|
|
1316
|
+
nodes = []
|
|
1317
|
+
identity = {
|
|
1318
|
+
"verification": verification,
|
|
1319
|
+
"inputsHash": inputs_hash,
|
|
1320
|
+
"command": command,
|
|
1321
|
+
"toolchainHash": toolchain_hash,
|
|
1322
|
+
"environmentHash": environment_hash,
|
|
1323
|
+
"dbSchemaHash": db_schema_hash,
|
|
1324
|
+
"diffHash": diff_hash,
|
|
1325
|
+
}
|
|
1326
|
+
node = {
|
|
1327
|
+
"identity": identity,
|
|
1328
|
+
"status": status,
|
|
1329
|
+
"recordedAt": dt.datetime.now().astimezone().isoformat(timespec="milliseconds"),
|
|
1330
|
+
}
|
|
1331
|
+
replaced = False
|
|
1332
|
+
for index, existing in enumerate(nodes):
|
|
1333
|
+
if not isinstance(existing, dict):
|
|
1334
|
+
continue
|
|
1335
|
+
existing_id = existing.get("identity")
|
|
1336
|
+
if isinstance(existing_id, dict) and existing_id == identity:
|
|
1337
|
+
nodes[index] = node
|
|
1338
|
+
replaced = True
|
|
1339
|
+
break
|
|
1340
|
+
if not replaced:
|
|
1341
|
+
nodes.append(node)
|
|
1342
|
+
graph["schemaVersion"] = 1
|
|
1343
|
+
graph["nodes"] = nodes
|
|
1344
|
+
write_ledger(path, graph) # atomic json write helper already used for ledger
|
|
1345
|
+
return {"ok": True, "path": str(path), "nodeCount": len(nodes), "replaced": replaced}
|
|
1346
|
+
|
|
1347
|
+
|
|
1261
1348
|
def cmd_hash(args: argparse.Namespace) -> int:
|
|
1262
1349
|
as_json = bool(args.json)
|
|
1263
1350
|
try:
|
|
@@ -1330,10 +1417,12 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
|
|
|
1330
1417
|
requested_toolchain_hash=getattr(args, "toolchain_hash", None),
|
|
1331
1418
|
requested_profile_hash=getattr(args, "profile_hash", None),
|
|
1332
1419
|
requested_environment_hash=getattr(args, "environment_hash", None),
|
|
1420
|
+
requested_db_schema_hash=getattr(args, "db_schema_hash", None),
|
|
1333
1421
|
)
|
|
1334
1422
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1335
1423
|
return emit_error(f"can-reuse failed: {exc}", as_json=as_json)
|
|
1336
1424
|
|
|
1425
|
+
payload = _attach_invalidation_code(payload)
|
|
1337
1426
|
emit_compact_or_verbose(
|
|
1338
1427
|
payload, as_json=as_json, verbose=verbose,
|
|
1339
1428
|
compact_fn=_compact_can_reuse_payload,
|
|
@@ -1470,6 +1559,7 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1470
1559
|
("toolchainHash", "toolchain_hash"),
|
|
1471
1560
|
("profileHash", "profile_hash"),
|
|
1472
1561
|
("environmentHash", "environment_hash"),
|
|
1562
|
+
("dbSchemaHash", "db_schema_hash"),
|
|
1473
1563
|
):
|
|
1474
1564
|
val = getattr(args, attr, None)
|
|
1475
1565
|
if _nonempty_str(val):
|
|
@@ -1548,6 +1638,37 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1548
1638
|
extra={"missing": missing},
|
|
1549
1639
|
)
|
|
1550
1640
|
write_ledger(out_path, ledger)
|
|
1641
|
+
try:
|
|
1642
|
+
upsert_verification_graph_node(
|
|
1643
|
+
change_dir,
|
|
1644
|
+
verification=verification,
|
|
1645
|
+
status=str(status),
|
|
1646
|
+
inputs_hash=str(inputs_hash),
|
|
1647
|
+
command=str(entry.get("command") or "") or None,
|
|
1648
|
+
toolchain_hash=(
|
|
1649
|
+
str(entry.get("toolchainHash")).strip()
|
|
1650
|
+
if _nonempty_str(entry.get("toolchainHash"))
|
|
1651
|
+
else None
|
|
1652
|
+
),
|
|
1653
|
+
environment_hash=(
|
|
1654
|
+
str(entry.get("environmentHash")).strip()
|
|
1655
|
+
if _nonempty_str(entry.get("environmentHash"))
|
|
1656
|
+
else None
|
|
1657
|
+
),
|
|
1658
|
+
db_schema_hash=(
|
|
1659
|
+
str(entry.get("dbSchemaHash")).strip()
|
|
1660
|
+
if _nonempty_str(entry.get("dbSchemaHash"))
|
|
1661
|
+
else None
|
|
1662
|
+
),
|
|
1663
|
+
diff_hash=(
|
|
1664
|
+
str(ledger.get("diffHash")).strip()
|
|
1665
|
+
if _nonempty_str(ledger.get("diffHash"))
|
|
1666
|
+
else None
|
|
1667
|
+
),
|
|
1668
|
+
)
|
|
1669
|
+
except (OSError, ValueError, TypeError):
|
|
1670
|
+
# Graph is advisory persistence; ledger write already succeeded.
|
|
1671
|
+
pass
|
|
1551
1672
|
except (OSError, ValueError, FileNotFoundError, json.JSONDecodeError) as exc:
|
|
1552
1673
|
return emit_error(f"record failed: {exc}", as_json=as_json)
|
|
1553
1674
|
|
|
@@ -1667,6 +1788,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1667
1788
|
default=None,
|
|
1668
1789
|
help="optional environment hash to compare against ledger entry (UT-017)",
|
|
1669
1790
|
)
|
|
1791
|
+
p_reuse.add_argument(
|
|
1792
|
+
"--db-schema-hash",
|
|
1793
|
+
default=None,
|
|
1794
|
+
help="optional DB schema hash to compare against ledger entry (H-9)",
|
|
1795
|
+
)
|
|
1670
1796
|
p_reuse.add_argument(
|
|
1671
1797
|
"--verbose",
|
|
1672
1798
|
action="store_true",
|
|
@@ -1708,6 +1834,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1708
1834
|
p_record.add_argument("--toolchain-hash", default=None)
|
|
1709
1835
|
p_record.add_argument("--profile-hash", default=None)
|
|
1710
1836
|
p_record.add_argument("--environment-hash", default=None)
|
|
1837
|
+
p_record.add_argument("--db-schema-hash", default=None)
|
|
1711
1838
|
p_record.add_argument("--deploy-artifact", default=None, help="package: built artifact path")
|
|
1712
1839
|
p_record.add_argument("--artifact-hash", default=None, help="package: artifact sha256")
|
|
1713
1840
|
p_record.add_argument(
|