@hunter-harness/workflow-harness 0.2.38 → 0.2.39
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/harness-archive/SKILL.md +10 -7
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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 +2 -2
- package/harness/bundles/general/claude-code/harness-review/reference.md +20 -5
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_archive.py +446 -20
- package/harness/bundles/general/claude-code/scripts/harness_events.py +273 -21
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +56 -32
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +197 -3
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +126 -2
- package/harness/bundles/general/claude-code/scripts/harness_report_model.py +28 -6
- package/harness/bundles/general/claude-code/scripts/harness_review.py +118 -17
- package/harness/bundles/general/claude-code/scripts/harness_service.py +219 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +10 -7
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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 +2 -2
- package/harness/bundles/general/codebuddy/harness-review/reference.md +20 -5
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_archive.py +446 -20
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +273 -21
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +56 -32
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +197 -3
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +126 -2
- package/harness/bundles/general/codebuddy/scripts/harness_report_model.py +28 -6
- package/harness/bundles/general/codebuddy/scripts/harness_review.py +118 -17
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +219 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +10 -7
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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 +2 -2
- package/harness/bundles/general/codex/harness-review/reference.md +20 -5
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_archive.py +446 -20
- package/harness/bundles/general/codex/scripts/harness_events.py +273 -21
- package/harness/bundles/general/codex/scripts/harness_gate.py +56 -32
- package/harness/bundles/general/codex/scripts/harness_integration.py +197 -3
- package/harness/bundles/general/codex/scripts/harness_ledger.py +126 -2
- package/harness/bundles/general/codex/scripts/harness_report_model.py +28 -6
- package/harness/bundles/general/codex/scripts/harness_review.py +118 -17
- package/harness/bundles/general/codex/scripts/harness_service.py +219 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +10 -7
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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 +2 -2
- package/harness/bundles/general/cursor/harness-review/reference.md +20 -5
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_archive.py +446 -20
- package/harness/bundles/general/cursor/scripts/harness_events.py +273 -21
- package/harness/bundles/general/cursor/scripts/harness_gate.py +56 -32
- package/harness/bundles/general/cursor/scripts/harness_integration.py +197 -3
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +126 -2
- package/harness/bundles/general/cursor/scripts/harness_report_model.py +28 -6
- package/harness/bundles/general/cursor/scripts/harness_review.py +118 -17
- package/harness/bundles/general/cursor/scripts/harness_service.py +219 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +10 -7
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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 +2 -2
- package/harness/bundles/java/claude-code/harness-review/reference.md +20 -5
- 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-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +446 -20
- package/harness/bundles/java/claude-code/scripts/harness_events.py +273 -21
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +56 -32
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +197 -3
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +126 -2
- package/harness/bundles/java/claude-code/scripts/harness_report_model.py +28 -6
- package/harness/bundles/java/claude-code/scripts/harness_review.py +118 -17
- package/harness/bundles/java/claude-code/scripts/harness_service.py +219 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +10 -7
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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 +2 -2
- package/harness/bundles/java/codebuddy/harness-review/reference.md +20 -5
- 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-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +446 -20
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +273 -21
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +56 -32
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +197 -3
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +126 -2
- package/harness/bundles/java/codebuddy/scripts/harness_report_model.py +28 -6
- package/harness/bundles/java/codebuddy/scripts/harness_review.py +118 -17
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +219 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +10 -7
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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 +2 -2
- package/harness/bundles/java/codex/harness-review/reference.md +20 -5
- 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-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +446 -20
- package/harness/bundles/java/codex/scripts/harness_events.py +273 -21
- package/harness/bundles/java/codex/scripts/harness_gate.py +56 -32
- package/harness/bundles/java/codex/scripts/harness_integration.py +197 -3
- package/harness/bundles/java/codex/scripts/harness_ledger.py +126 -2
- package/harness/bundles/java/codex/scripts/harness_report_model.py +28 -6
- package/harness/bundles/java/codex/scripts/harness_review.py +118 -17
- package/harness/bundles/java/codex/scripts/harness_service.py +219 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +10 -7
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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 +2 -2
- package/harness/bundles/java/cursor/harness-review/reference.md +20 -5
- 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-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +446 -20
- package/harness/bundles/java/cursor/scripts/harness_events.py +273 -21
- package/harness/bundles/java/cursor/scripts/harness_gate.py +56 -32
- package/harness/bundles/java/cursor/scripts/harness_integration.py +197 -3
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +126 -2
- package/harness/bundles/java/cursor/scripts/harness_report_model.py +28 -6
- package/harness/bundles/java/cursor/scripts/harness_review.py +118 -17
- package/harness/bundles/java/cursor/scripts/harness_service.py +219 -0
- package/harness/manifests/general/claude-code.json +24 -24
- package/harness/manifests/general/codebuddy.json +24 -24
- package/harness/manifests/general/codex.json +24 -24
- package/harness/manifests/general/cursor.json +24 -24
- package/harness/manifests/java/claude-code.json +26 -26
- package/harness/manifests/java/codebuddy.json +26 -26
- package/harness/manifests/java/codex.json +26 -26
- package/harness/manifests/java/cursor.json +26 -26
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=6a2c743a1f8ceb0b; overlay=none; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-archive — 变更归档
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -43,7 +43,7 @@ disallowed-tools:
|
|
|
43
43
|
自动调用边界:
|
|
44
44
|
|
|
45
45
|
- 归档涉及移动/删除原变更目录,支持被其他 skill 调用,但调用前必须确保前置条件已满足;也支持用户显式 `/harness-archive` 调用。
|
|
46
|
-
-
|
|
46
|
+
- 默认归档前确认是阻断检查点。若 `harness_archive.py auto-gate` 返回 `ARCHIVE_AUTO_GATE_SATISFIED`(Submit 或 Merge 已终态,archive-boundary snapshot 存在且全部归档门禁满足),后续 Archive 可无 AskQuestion 自动运行;其他情况仍须确认。
|
|
47
47
|
|
|
48
48
|
## 前置条件
|
|
49
49
|
|
|
@@ -106,9 +106,12 @@ disallowed-tools:
|
|
|
106
106
|
- **Read `checklist.md`** — 归档前检查项
|
|
107
107
|
- 发现多个未归档变更 → 让用户选择或终止
|
|
108
108
|
|
|
109
|
-
### Phase 2:确认归档
|
|
109
|
+
### Phase 2:确认归档 / 自动门禁
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
先运行 `harness_archive.py auto-gate --change-dir ".harness/changes/<change-name>" --intent <intent> --json`。
|
|
112
|
+
|
|
113
|
+
- `autoArchiveAllowed=true`:记录 gate receipt,直接进入 Phase 3;无需 AskQuestion。
|
|
114
|
+
- 否则:blocking user confirmation 让用户确认归档操作。**用户拒绝 → 终止流程,不执行任何操作。**
|
|
112
115
|
|
|
113
116
|
- **Read `reference.md`** — 确认对话框的内容格式
|
|
114
117
|
|
|
@@ -143,9 +146,9 @@ blocking user confirmation 让用户确认归档操作。**用户拒绝 → 终
|
|
|
143
146
|
|
|
144
147
|
扫描排除 `.harness/archive/`;多个未归档变更 → 让用户选择或终止,不批量归档。
|
|
145
148
|
|
|
146
|
-
###
|
|
149
|
+
### 二、归档前确认或自动门禁
|
|
147
150
|
|
|
148
|
-
blocking user confirmation
|
|
151
|
+
`auto-gate` 只有在 Submit/Merge 终态、`meta/state-snapshot.json` 含 archive-boundary base、且 `status.archivable=true` 时才允许无确认执行。未满足时 blocking user confirmation;用户拒绝 → 终止,不执行任何操作。
|
|
149
152
|
|
|
150
153
|
### 三、文件移动只用内置工具或 PowerShell
|
|
151
154
|
|
|
@@ -218,7 +221,7 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 fi
|
|
|
218
221
|
|
|
219
222
|
## 交互白名单
|
|
220
223
|
|
|
221
|
-
|
|
224
|
+
仅当 `auto-gate` 未满足时,允许归档确认(Phase 2 blocking user confirmation);拒绝 → 终止,不执行任何移动。
|
|
222
225
|
|
|
223
226
|
## 执行日志
|
|
224
227
|
|
|
@@ -69,7 +69,7 @@ const risks = list(current.findings).length
|
|
|
69
69
|
const statusTone = (raw) => {
|
|
70
70
|
const status = String(raw || "UNKNOWN").toUpperCase();
|
|
71
71
|
if (/FAIL|ERROR|BLOCKED/.test(status)) return "danger";
|
|
72
|
-
if (/WARN|CONDITIONAL|PARTIAL|NOT_RUN|UNKNOWN|SKIP/.test(status)) return "warning";
|
|
72
|
+
if (/WARN|CONDITIONAL|PARTIAL|NOT_RUN|UNKNOWN|SKIP|EVIDENCE_MISSING/.test(status)) return "warning";
|
|
73
73
|
if (/ADVISORY|REUSED|NOT_APPLICABLE/.test(status)) return "neutral";
|
|
74
74
|
return "success";
|
|
75
75
|
};
|
|
@@ -77,7 +77,7 @@ const STATUS_LABELS = {
|
|
|
77
77
|
OK: "通过", PASS: "通过", PASSED: "通过", CONDITIONAL_OK: "有条件通过",
|
|
78
78
|
WARN: "警告", ADVISORY: "建议", FAIL: "失败", FAILED: "失败", ERROR: "错误",
|
|
79
79
|
BLOCKED: "阻塞", NOT_RUN: "未运行", SKIPPED: "已跳过",
|
|
80
|
-
NOT_APPLICABLE: "不适用", UNKNOWN: "未知"
|
|
80
|
+
NOT_APPLICABLE: "不适用", UNKNOWN: "未知", EVIDENCE_MISSING: "证据缺失"
|
|
81
81
|
};
|
|
82
82
|
const pill = (raw) => {
|
|
83
83
|
const status = String(raw || "UNKNOWN").toUpperCase();
|
|
@@ -115,7 +115,17 @@ const verificationStatus = (value) => {
|
|
|
115
115
|
};
|
|
116
116
|
const verificationNote = (key, raw) => {
|
|
117
117
|
const value = record(raw);
|
|
118
|
-
if (key === "unitTests")
|
|
118
|
+
if (key === "unitTests") {
|
|
119
|
+
const unique = numeric(value.uniqueTestCount);
|
|
120
|
+
const uniqueLabel = unique === null ? "唯一用例未记录" : `唯一 ${unique} 项`;
|
|
121
|
+
return `${value.passRate || `${value.run ?? 0} 次执行`} · ${uniqueLabel} · 重跑 ${value.rerunCount ?? 0}`;
|
|
122
|
+
}
|
|
123
|
+
if (key === "dbCompatibility") {
|
|
124
|
+
const evidence = record(verification.dbCompatibilityEvidence);
|
|
125
|
+
return evidence.status === "EVIDENCE_MISSING"
|
|
126
|
+
? "缺少可验证的类型化账本/收据"
|
|
127
|
+
: evidence.reason || evidence.evidenceHash || "类型化兼容性证据";
|
|
128
|
+
}
|
|
119
129
|
if (key === "apiTests") return `${value.passRate || `${value.passed ?? 0}/${value.total ?? 0}`} · 阻塞 ${value.blocked ?? 0}`;
|
|
120
130
|
if (key === "browserE2E") return `${value.passed ?? 0}/${value.total ?? 0} 通过 · ${value.failed ?? 0} 失败 · 重试 ${value.retries ?? 0}`;
|
|
121
131
|
if (value.checks !== undefined) return `${value.checks} 项检查`;
|
|
@@ -135,6 +145,8 @@ const groups = [
|
|
|
135
145
|
const statuses = values.map(verificationStatus);
|
|
136
146
|
const groupStatus = statuses.some((status) => /FAIL|ERROR|BLOCKED/.test(String(status)))
|
|
137
147
|
? "FAIL"
|
|
148
|
+
: statuses.some((status) => /EVIDENCE_MISSING/.test(String(status)))
|
|
149
|
+
? "EVIDENCE_MISSING"
|
|
138
150
|
: statuses.some((status) => /WARN|CONDITIONAL|PARTIAL/.test(String(status)))
|
|
139
151
|
? "WARN"
|
|
140
152
|
: statuses.length && statuses.every((status) => /NOT_APPLICABLE/.test(String(status)))
|
|
@@ -147,6 +159,13 @@ const groups = [
|
|
|
147
159
|
});
|
|
148
160
|
const passedGroups = groups.filter((group) => group.status === "OK").length;
|
|
149
161
|
const productCommit = identity.productCommit || data.productCommit || data.finalCommit;
|
|
162
|
+
const identityChain = [
|
|
163
|
+
["检查点", identity.checkpointCommit || data.checkpointCommit],
|
|
164
|
+
["产品", productCommit],
|
|
165
|
+
["功能尖端", identity.featureTip || data.featureTip],
|
|
166
|
+
["合并", identity.mergeCommit || data.mergeCommit || identity.featureMergeHash],
|
|
167
|
+
["发布尖端", identity.releaseTip || data.releaseTip || identity.releaseTipHash]
|
|
168
|
+
].filter(([, value]) => value).map(([label, value]) => `${label} ${shortHash(value)}`).join(" → ") || "N/A";
|
|
150
169
|
const wallClock = timing.workflowWallClockMs ?? record(data.durations).totalMs;
|
|
151
170
|
|
|
152
171
|
const groupHtml = groups.map((group) => `
|
|
@@ -201,7 +220,7 @@ const html = `<!doctype html>
|
|
|
201
220
|
${recordOnly ? '<div class="record-only">归档意图:仅记录 · 未请求发布</div>' : ""}
|
|
202
221
|
</section>
|
|
203
222
|
<section class="metrics">
|
|
204
|
-
<article class="metric"><small
|
|
223
|
+
<article class="metric"><small>产品提交链</small><strong><code title="${esc(identityChain)}">${esc(identityChain)}</code></strong></article>
|
|
205
224
|
<article class="metric"><small>验证概览</small><strong>${passedGroups}/${groups.length} 组通过</strong><small>按后端 / Geo / 前端 / 浏览器 / API</small></article>
|
|
206
225
|
<article class="metric"><small>风险与动作</small><strong>${risks.length} / ${actions.length}</strong><small>当前风险 / 人工动作</small></article>
|
|
207
226
|
<article class="metric"><small>全流程耗时</small><strong>${esc(duration(wallClock))}</strong><small>活动 ${esc(duration(timing.stageActiveExecutionMs))}</small></article>
|
|
@@ -234,6 +253,7 @@ const html = `<!doctype html>
|
|
|
234
253
|
<details><summary>命令证据(${commands.length})</summary><div class="table-wrap"><table><thead><tr><th>阶段</th><th>命令</th><th>结果</th></tr></thead><tbody>${commandHtml}</tbody></table></div></details>
|
|
235
254
|
<details><summary>技术元数据</summary><div><dl>
|
|
236
255
|
<dt>产品提交</dt><dd><code>${esc(productCommit || "N/A")}</code></dd>
|
|
256
|
+
<dt>检查点 → 产品 → 功能尖端 → 合并 → 发布尖端</dt><dd><code>${esc(identityChain)}</code></dd>
|
|
237
257
|
<dt>产品树哈希</dt><dd><code>${esc(identity.productTreeHash || data.productTreeHash || "N/A")}</code></dd>
|
|
238
258
|
<dt>环境哈希</dt><dd><code>${esc(identity.environmentHash || data.environmentHash || "N/A")}</code></dd>
|
|
239
259
|
<dt>基线提交</dt><dd><code>${esc(identity.baseCommit || data.baseCommit || "N/A")}</code></dd>
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=6a2c743a1f8ceb0b; overlay=none; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-codebase-map — 代码库地图
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=6a2c743a1f8ceb0b; overlay=none; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-knowledge-ingest
|
|
25
25
|
|
|
26
26
|
从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
|
package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py
CHANGED
|
@@ -2654,6 +2654,55 @@ def build_snapshot(project: Path) -> KnowledgeSnapshot:
|
|
|
2654
2654
|
)
|
|
2655
2655
|
|
|
2656
2656
|
|
|
2657
|
+
def archive_delta(
|
|
2658
|
+
archive_records: list[dict[str, Any]],
|
|
2659
|
+
previous_index: dict[str, Any] | None,
|
|
2660
|
+
) -> dict[str, set[str]]:
|
|
2661
|
+
"""Classify archive summaries without treating the whole corpus as dirty."""
|
|
2662
|
+
previous_items = (
|
|
2663
|
+
previous_index.get("archives", {}).get("items", [])
|
|
2664
|
+
if isinstance(previous_index, dict)
|
|
2665
|
+
else []
|
|
2666
|
+
)
|
|
2667
|
+
previous = {
|
|
2668
|
+
str(item.get("summaryData")): str(item.get("summarySha256") or "")
|
|
2669
|
+
for item in previous_items
|
|
2670
|
+
if isinstance(item, dict) and item.get("summaryData")
|
|
2671
|
+
}
|
|
2672
|
+
current = {
|
|
2673
|
+
str(item.get("summaryData")): str(item.get("summarySha256") or "")
|
|
2674
|
+
for item in archive_records
|
|
2675
|
+
if item.get("summaryData")
|
|
2676
|
+
}
|
|
2677
|
+
added = set(current) - set(previous)
|
|
2678
|
+
changed = {
|
|
2679
|
+
path for path in set(current) & set(previous)
|
|
2680
|
+
if current[path] != previous[path]
|
|
2681
|
+
}
|
|
2682
|
+
return {
|
|
2683
|
+
"added": added,
|
|
2684
|
+
"changed": changed,
|
|
2685
|
+
"removed": set(previous) - set(current),
|
|
2686
|
+
"dirty": added | changed,
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
|
|
2690
|
+
def restore_unchanged_entry_state(
|
|
2691
|
+
entry: dict[str, Any],
|
|
2692
|
+
previous_entries: dict[str, dict[str, Any]],
|
|
2693
|
+
*,
|
|
2694
|
+
is_dirty: bool,
|
|
2695
|
+
) -> None:
|
|
2696
|
+
"""Keep historical lifecycle decisions out of a warm ingest hot path."""
|
|
2697
|
+
if is_dirty:
|
|
2698
|
+
return
|
|
2699
|
+
previous = previous_entries.get(str(entry.get("id") or ""))
|
|
2700
|
+
if previous is None:
|
|
2701
|
+
return
|
|
2702
|
+
entry["status"] = previous.get("status", entry.get("status"))
|
|
2703
|
+
entry["lifecycle"] = json_clone(previous.get("lifecycle", entry.get("lifecycle", {})))
|
|
2704
|
+
|
|
2705
|
+
|
|
2657
2706
|
def build_index(
|
|
2658
2707
|
project: Path,
|
|
2659
2708
|
incremental: bool = True,
|
|
@@ -2684,19 +2733,21 @@ def build_index(
|
|
|
2684
2733
|
ensure_knowledge_dirs(knowledge)
|
|
2685
2734
|
reporter.emit("snapshot", len(summary_paths), len(summary_paths), force=True)
|
|
2686
2735
|
|
|
2736
|
+
old_index: dict[str, Any] | None = None
|
|
2737
|
+
index_path = knowledge / "index.json"
|
|
2738
|
+
if index_path.exists():
|
|
2739
|
+
try:
|
|
2740
|
+
old_index = read_json(index_path)
|
|
2741
|
+
except (OSError, json.JSONDecodeError):
|
|
2742
|
+
old_index = None
|
|
2743
|
+
delta = archive_delta(archive_records, old_index)
|
|
2744
|
+
|
|
2687
2745
|
# No-op fast path: inputs (archive checksums + config + schema) are unchanged.
|
|
2688
2746
|
# Write nothing — entries, sqlite, index and views all stay byte-identical,
|
|
2689
2747
|
# so a repeated ingest is a true no-op (design §3.5, cluster 6, UT-025).
|
|
2690
2748
|
# Also require index.sqlite to exist so a query never sees a stale index.json
|
|
2691
2749
|
# pointing at a missing sqlite (API-009 single ensure-current).
|
|
2692
2750
|
if incremental:
|
|
2693
|
-
old_index: dict[str, Any] | None = None
|
|
2694
|
-
index_path = knowledge / "index.json"
|
|
2695
|
-
if index_path.exists():
|
|
2696
|
-
try:
|
|
2697
|
-
old_index = read_json(index_path)
|
|
2698
|
-
except (OSError, json.JSONDecodeError):
|
|
2699
|
-
old_index = None
|
|
2700
2751
|
if (
|
|
2701
2752
|
isinstance(old_index, dict)
|
|
2702
2753
|
and old_index.get("inputsHash") == inputs_hash
|
|
@@ -2716,6 +2767,10 @@ def build_index(
|
|
|
2716
2767
|
"sqliteRebuild": 0,
|
|
2717
2768
|
"sqliteUpsert": 0,
|
|
2718
2769
|
"sqliteDelete": 0,
|
|
2770
|
+
"archivesAdded": 0,
|
|
2771
|
+
"archivesChanged": 0,
|
|
2772
|
+
"archivesRemoved": 0,
|
|
2773
|
+
"archivesDelta": 0,
|
|
2719
2774
|
}
|
|
2720
2775
|
)
|
|
2721
2776
|
result = dict(old_index)
|
|
@@ -2751,7 +2806,17 @@ def build_index(
|
|
|
2751
2806
|
"sqliteDelete": 0,
|
|
2752
2807
|
"summaryHashesReused": 0,
|
|
2753
2808
|
"summaryHashesComputed": 0,
|
|
2809
|
+
"archivesAdded": len(delta["added"]),
|
|
2810
|
+
"archivesChanged": len(delta["changed"]),
|
|
2811
|
+
"archivesRemoved": len(delta["removed"]),
|
|
2812
|
+
"archivesDelta": len(delta["dirty"]) + len(delta["removed"]),
|
|
2813
|
+
}
|
|
2814
|
+
previous_entries = {
|
|
2815
|
+
str(entry.get("id")): entry
|
|
2816
|
+
for _, entry in load_entry_files(knowledge)
|
|
2817
|
+
if entry.get("id")
|
|
2754
2818
|
}
|
|
2819
|
+
dirty_entry_ids: set[str] = set()
|
|
2755
2820
|
|
|
2756
2821
|
for summary_index, summary_path in enumerate(summary_paths, start=1):
|
|
2757
2822
|
summary_rel = rel_to_project(project, summary_path)
|
|
@@ -2790,6 +2855,15 @@ def build_index(
|
|
|
2790
2855
|
archive_entries = rebind_cached_archive_entries(
|
|
2791
2856
|
project, summary_path, summary_hash, archive_entries
|
|
2792
2857
|
)
|
|
2858
|
+
is_dirty = summary_rel in delta["dirty"] or not incremental
|
|
2859
|
+
for entry in archive_entries:
|
|
2860
|
+
restore_unchanged_entry_state(
|
|
2861
|
+
entry,
|
|
2862
|
+
previous_entries,
|
|
2863
|
+
is_dirty=is_dirty,
|
|
2864
|
+
)
|
|
2865
|
+
if is_dirty and entry.get("id"):
|
|
2866
|
+
dirty_entry_ids.add(str(entry["id"]))
|
|
2793
2867
|
# API-006/RET-40: entries from archives failing the publication
|
|
2794
2868
|
# gate stay quarantined (candidate only, never active/promoted).
|
|
2795
2869
|
archive_rel = rel_to_project(project, archive_dir_from_summary(summary_path))
|
|
@@ -2833,9 +2907,24 @@ def build_index(
|
|
|
2833
2907
|
seen.add(fp)
|
|
2834
2908
|
deduped.append(entry)
|
|
2835
2909
|
|
|
2836
|
-
|
|
2910
|
+
freshness_entries = [
|
|
2911
|
+
entry
|
|
2912
|
+
for entry in deduped
|
|
2913
|
+
if (
|
|
2914
|
+
str(entry.get("id") or "") in dirty_entry_ids
|
|
2915
|
+
or (
|
|
2916
|
+
entry.get("status") in {"active", "candidate"}
|
|
2917
|
+
and str(entry.get("id") or "") not in previous_entries
|
|
2918
|
+
)
|
|
2919
|
+
)
|
|
2920
|
+
]
|
|
2921
|
+
ingest_mode.update(apply_batch_git_freshness(project, freshness_entries))
|
|
2837
2922
|
reporter.emit("near-deduplicate", 0, len(deduped), force=True)
|
|
2838
|
-
near_dedupe = dedupe_near_duplicates(
|
|
2923
|
+
near_dedupe = dedupe_near_duplicates(
|
|
2924
|
+
deduped,
|
|
2925
|
+
progress=reporter,
|
|
2926
|
+
dirty_entry_ids=dirty_entry_ids if incremental else None,
|
|
2927
|
+
)
|
|
2839
2928
|
ingest_mode["nearDuplicatesMerged"] = near_dedupe["merged"]
|
|
2840
2929
|
ingest_mode["similarityCandidates"] = near_dedupe["similarityCandidates"]
|
|
2841
2930
|
ingest_mode["exactSimilarityComparisons"] = near_dedupe["exactSimilarityComparisons"]
|
|
@@ -2920,7 +3009,22 @@ def build_index(
|
|
|
2920
3009
|
)
|
|
2921
3010
|
|
|
2922
3011
|
reporter.emit("sqlite", 0, len(indexed_entries), force=True)
|
|
2923
|
-
|
|
3012
|
+
sqlite_dirty_entries = [
|
|
3013
|
+
entry
|
|
3014
|
+
for entry in indexed_entries
|
|
3015
|
+
if (
|
|
3016
|
+
str(entry.get("id") or "") in dirty_entry_ids
|
|
3017
|
+
or (
|
|
3018
|
+
entry.get("status") in {"active", "candidate"}
|
|
3019
|
+
and str(entry.get("id") or "") not in previous_entries
|
|
3020
|
+
)
|
|
3021
|
+
)
|
|
3022
|
+
]
|
|
3023
|
+
sqlite_stats = write_sqlite(
|
|
3024
|
+
knowledge / "index.sqlite",
|
|
3025
|
+
indexed_entries,
|
|
3026
|
+
dirty_entries=sqlite_dirty_entries if incremental else None,
|
|
3027
|
+
)
|
|
2924
3028
|
ingest_mode.update(sqlite_stats)
|
|
2925
3029
|
ingest_mode["sqliteRowsTouched"] = (
|
|
2926
3030
|
sqlite_stats.get("sqliteUpsert", 0) + sqlite_stats.get("sqliteDelete", 0)
|
|
@@ -3288,13 +3392,19 @@ def assert_persistence_invariants(
|
|
|
3288
3392
|
)
|
|
3289
3393
|
|
|
3290
3394
|
|
|
3291
|
-
def write_sqlite(
|
|
3395
|
+
def write_sqlite(
|
|
3396
|
+
path: Path,
|
|
3397
|
+
entries: list[dict[str, Any]],
|
|
3398
|
+
*,
|
|
3399
|
+
dirty_entries: list[dict[str, Any]] | None = None,
|
|
3400
|
+
) -> dict[str, Any]:
|
|
3292
3401
|
"""Persist entries to SQLite using a transactional dirty-set.
|
|
3293
3402
|
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3403
|
+
The normal path compares existing row payloads. Incremental callers may
|
|
3404
|
+
provide the known dirty entries instead, avoiding a full JSON digest of
|
|
3405
|
+
unchanged historical rows. Both paths delete rows absent from the current
|
|
3406
|
+
closure in one transaction. When the schema version changes or the table
|
|
3407
|
+
is missing, performs a full rebuild.
|
|
3298
3408
|
"""
|
|
3299
3409
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
3300
3410
|
stats: dict[str, Any] = {
|
|
@@ -3315,18 +3425,52 @@ def write_sqlite(path: Path, entries: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
3315
3425
|
need_rebuild = (not table_exists) or existing_version != SQLITE_SCHEMA_VERSION
|
|
3316
3426
|
|
|
3317
3427
|
existing_map: dict[str, str] = {}
|
|
3318
|
-
|
|
3428
|
+
existing_status: dict[str, str] = {}
|
|
3429
|
+
dirty_mode = dirty_entries is not None and not need_rebuild
|
|
3430
|
+
if not need_rebuild and not dirty_mode:
|
|
3319
3431
|
rows = con.execute("select id, entry_json from entries").fetchall()
|
|
3320
3432
|
existing_map = {str(row[0]): str(row[1]) for row in rows}
|
|
3433
|
+
elif dirty_mode:
|
|
3434
|
+
rows = con.execute("select id, status from entries").fetchall()
|
|
3435
|
+
existing_map = {str(row[0]): "" for row in rows}
|
|
3436
|
+
existing_status = {str(row[0]): str(row[1] or "") for row in rows}
|
|
3321
3437
|
|
|
3322
3438
|
new_map: dict[str, str] = {}
|
|
3323
|
-
|
|
3439
|
+
entries_to_serialize = list(dirty_entries or []) if dirty_mode else list(entries)
|
|
3440
|
+
if dirty_mode:
|
|
3441
|
+
# HH-KNOW-20260730-002: archive-delta dirty sets miss lifecycle-only
|
|
3442
|
+
# transitions (promote/demote/conflict/supersede) on unchanged
|
|
3443
|
+
# archives. Reconcile those by status without digesting every row.
|
|
3444
|
+
seen_serialize_ids = {
|
|
3445
|
+
str(entry.get("id") or "")
|
|
3446
|
+
for entry in entries_to_serialize
|
|
3447
|
+
if isinstance(entry, dict) and entry.get("id")
|
|
3448
|
+
}
|
|
3449
|
+
for entry in entries:
|
|
3450
|
+
if not isinstance(entry, dict) or not entry.get("id"):
|
|
3451
|
+
continue
|
|
3452
|
+
eid = str(entry["id"])
|
|
3453
|
+
if eid in seen_serialize_ids:
|
|
3454
|
+
continue
|
|
3455
|
+
if existing_status.get(eid) != str(entry.get("status") or ""):
|
|
3456
|
+
entries_to_serialize.append(entry)
|
|
3457
|
+
seen_serialize_ids.add(eid)
|
|
3458
|
+
for entry in entries_to_serialize:
|
|
3324
3459
|
if isinstance(entry, dict) and entry.get("id"):
|
|
3325
3460
|
new_map[str(entry["id"])] = json.dumps(entry, ensure_ascii=False)
|
|
3326
3461
|
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3462
|
+
current_ids = {
|
|
3463
|
+
str(entry["id"])
|
|
3464
|
+
for entry in entries
|
|
3465
|
+
if isinstance(entry, dict) and entry.get("id")
|
|
3466
|
+
}
|
|
3467
|
+
to_upsert = (
|
|
3468
|
+
list(new_map)
|
|
3469
|
+
if dirty_mode
|
|
3470
|
+
else [eid for eid in new_map if new_map[eid] != existing_map.get(eid)]
|
|
3471
|
+
)
|
|
3472
|
+
to_delete = [eid for eid in existing_map if eid not in current_ids]
|
|
3473
|
+
stats["sqliteUnchanged"] = len(entries) - len(to_upsert)
|
|
3330
3474
|
|
|
3331
3475
|
if not need_rebuild and not to_upsert and not to_delete:
|
|
3332
3476
|
# nothing to do; leave the file (and its mtime) untouched
|
|
@@ -3372,6 +3516,11 @@ def write_sqlite(path: Path, entries: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
3372
3516
|
con.execute(f"pragma user_version = {SQLITE_SCHEMA_VERSION}")
|
|
3373
3517
|
stats["sqliteRebuild"] = 1
|
|
3374
3518
|
# full rebuild => every entry must be (re)inserted
|
|
3519
|
+
new_map = {
|
|
3520
|
+
str(entry["id"]): json.dumps(entry, ensure_ascii=False)
|
|
3521
|
+
for entry in entries
|
|
3522
|
+
if isinstance(entry, dict) and entry.get("id")
|
|
3523
|
+
}
|
|
3375
3524
|
to_upsert = list(new_map.keys())
|
|
3376
3525
|
to_delete = []
|
|
3377
3526
|
|
|
@@ -4121,6 +4270,62 @@ def sync_status(
|
|
|
4121
4270
|
action = "ingested"
|
|
4122
4271
|
reasons = []
|
|
4123
4272
|
|
|
4273
|
+
# HH-KNOW-20260730-001: an up-to-date index is not the whole story --
|
|
4274
|
+
# maintenance-outbox items enqueued by `harness archive` still need to be
|
|
4275
|
+
# drained (auto-supersede/reverify-stale/judge-export) before the project
|
|
4276
|
+
# is truly caught up. Drain here whenever the index is current: either it
|
|
4277
|
+
# was just refreshed above, or it was already current on entry. When the
|
|
4278
|
+
# caller did not request an update and the index is stale, skip the drain
|
|
4279
|
+
# -- maintain_knowledge() would otherwise trigger an implicit rebuild via
|
|
4280
|
+
# its own build_index(incremental=True) call, contradicting "don't force
|
|
4281
|
+
# rebuild" for a plain status check.
|
|
4282
|
+
index_current = not reasons
|
|
4283
|
+
outbox_root = _outbox_root(project)
|
|
4284
|
+
maintenance: dict[str, Any]
|
|
4285
|
+
next_action: str | None = None
|
|
4286
|
+
maintain_hint = (
|
|
4287
|
+
"python harness/harness-knowledge-ingest/scripts/harness_knowledge.py "
|
|
4288
|
+
"maintain --project <project> --drain"
|
|
4289
|
+
)
|
|
4290
|
+
|
|
4291
|
+
if index_current:
|
|
4292
|
+
drain_result = drain_maintenance_outbox(project)
|
|
4293
|
+
outbox_ok = bool(drain_result.get("ok")) and drain_result.get("remaining", 0) == 0
|
|
4294
|
+
maintenance = {
|
|
4295
|
+
"attempted": True,
|
|
4296
|
+
"skipped": False,
|
|
4297
|
+
"ok": outbox_ok,
|
|
4298
|
+
"processed": drain_result.get("processed", 0),
|
|
4299
|
+
"remaining": drain_result.get("remaining", 0),
|
|
4300
|
+
"staleRunningRecovered": drain_result.get("staleRunningRecovered", 0),
|
|
4301
|
+
"results": drain_result.get("results", []),
|
|
4302
|
+
}
|
|
4303
|
+
if not outbox_ok:
|
|
4304
|
+
next_action = (
|
|
4305
|
+
"Maintenance outbox is not fully drained. Retry `hunter-harness sync`, "
|
|
4306
|
+
f"or run `{maintain_hint}` to process the remaining items."
|
|
4307
|
+
)
|
|
4308
|
+
else:
|
|
4309
|
+
pending_count = len(list((outbox_root / "pending").glob("*.json"))) if outbox_root.is_dir() else 0
|
|
4310
|
+
failed_count = len(list((outbox_root / "failed").glob("*.json"))) if outbox_root.is_dir() else 0
|
|
4311
|
+
running_count = len(list((outbox_root / "running").glob("*.json"))) if outbox_root.is_dir() else 0
|
|
4312
|
+
outbox_ok = pending_count == 0 and failed_count == 0
|
|
4313
|
+
maintenance = {
|
|
4314
|
+
"attempted": False,
|
|
4315
|
+
"skipped": True,
|
|
4316
|
+
"skippedReason": "index out of date; rerun with --update to rebuild and drain",
|
|
4317
|
+
"ok": outbox_ok,
|
|
4318
|
+
"pending": pending_count,
|
|
4319
|
+
"failed": failed_count,
|
|
4320
|
+
"running": running_count,
|
|
4321
|
+
}
|
|
4322
|
+
next_action = (
|
|
4323
|
+
"Run `hunter-harness sync` (or sync --update) to rebuild the index; "
|
|
4324
|
+
"the maintenance outbox drain is skipped while the index is stale."
|
|
4325
|
+
)
|
|
4326
|
+
|
|
4327
|
+
ok = index_current and bool(maintenance.get("ok"))
|
|
4328
|
+
|
|
4124
4329
|
result = {
|
|
4125
4330
|
"project": str(project),
|
|
4126
4331
|
"upToDate": not reasons,
|
|
@@ -4131,6 +4336,9 @@ def sync_status(
|
|
|
4131
4336
|
"index": str(index_path),
|
|
4132
4337
|
"sqlite": str(sqlite_path),
|
|
4133
4338
|
},
|
|
4339
|
+
"maintenance": maintenance,
|
|
4340
|
+
"ok": ok,
|
|
4341
|
+
"nextAction": next_action,
|
|
4134
4342
|
}
|
|
4135
4343
|
if refreshed is not None:
|
|
4136
4344
|
result["index"] = summarize_index(refreshed)
|
|
@@ -4522,16 +4730,33 @@ def dedupe_near_duplicates(
|
|
|
4522
4730
|
threshold: float = NEAR_DUPLICATE_THRESHOLD,
|
|
4523
4731
|
*,
|
|
4524
4732
|
progress: ProgressReporter | None = None,
|
|
4733
|
+
dirty_entry_ids: set[str] | None = None,
|
|
4525
4734
|
max_exact_comparisons: int = 50_000,
|
|
4526
4735
|
time_budget_ms: int = 2_000,
|
|
4527
4736
|
) -> dict[str, Any]:
|
|
4528
|
-
"""Merge near duplicates through sparse buckets and an explicit work budget.
|
|
4737
|
+
"""Merge near duplicates through sparse buckets and an explicit work budget.
|
|
4738
|
+
|
|
4739
|
+
Full / offline path (``dirty_entry_ids is None``): keep historical semantics —
|
|
4740
|
+
every non-superseded status participates, and pairs stay scoped to the same
|
|
4741
|
+
``(archive, type)`` group.
|
|
4742
|
+
|
|
4743
|
+
Online hot path (``dirty_entry_ids`` provided): exclude stale/superseded from
|
|
4744
|
+
the comparison corpus, keep archive scoping, and only emit pairs that touch
|
|
4745
|
+
at least one dirty entry (HH-KNOW-20260730-002).
|
|
4746
|
+
"""
|
|
4529
4747
|
started = time.monotonic()
|
|
4530
4748
|
deadline = started + max(0, time_budget_ms) / 1_000
|
|
4531
4749
|
merges: list[dict[str, Any]] = []
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4750
|
+
if dirty_entry_ids is None:
|
|
4751
|
+
active_entries = [
|
|
4752
|
+
entry for entry in entries if entry.get("status") != "superseded"
|
|
4753
|
+
]
|
|
4754
|
+
else:
|
|
4755
|
+
active_entries = [
|
|
4756
|
+
entry
|
|
4757
|
+
for entry in entries
|
|
4758
|
+
if entry.get("status") in {"active", "candidate"}
|
|
4759
|
+
]
|
|
4535
4760
|
normalized = {id(entry): entry_compare_text(entry) for entry in active_entries}
|
|
4536
4761
|
similarity_cache: dict[tuple[int, int], float] = {}
|
|
4537
4762
|
similarity_candidates = 0
|
|
@@ -4540,6 +4765,8 @@ def dedupe_near_duplicates(
|
|
|
4540
4765
|
|
|
4541
4766
|
grouped: dict[tuple[str, str], list[dict[str, Any]]] = {}
|
|
4542
4767
|
for entry in active_entries:
|
|
4768
|
+
# Same-archive scoping is intentional: near-dup merges are intra-archive.
|
|
4769
|
+
# Cross-archive evolution/conflict is handled by supersede/review paths.
|
|
4543
4770
|
key = (
|
|
4544
4771
|
str(entry.get("source", {}).get("archive") or ""),
|
|
4545
4772
|
str(entry.get("type") or ""),
|
|
@@ -4580,6 +4807,13 @@ def dedupe_near_duplicates(
|
|
|
4580
4807
|
for right_identity in members[left_index + 1 :]:
|
|
4581
4808
|
if left_identity == right_identity:
|
|
4582
4809
|
continue
|
|
4810
|
+
if dirty_entry_ids is not None and not (
|
|
4811
|
+
str(entry_by_identity[left_identity].get("id") or "")
|
|
4812
|
+
in dirty_entry_ids
|
|
4813
|
+
or str(entry_by_identity[right_identity].get("id") or "")
|
|
4814
|
+
in dirty_entry_ids
|
|
4815
|
+
):
|
|
4816
|
+
continue
|
|
4583
4817
|
candidate_pairs.add(tuple(sorted((left_identity, right_identity))))
|
|
4584
4818
|
|
|
4585
4819
|
def similarity(left: dict[str, Any], right: dict[str, Any]) -> float:
|
|
@@ -5451,15 +5685,35 @@ def maintain_knowledge(project: Path, archive_id: str) -> dict[str, Any]:
|
|
|
5451
5685
|
}
|
|
5452
5686
|
|
|
5453
5687
|
|
|
5454
|
-
def drain_maintenance_outbox(
|
|
5455
|
-
|
|
5688
|
+
def drain_maintenance_outbox(
|
|
5689
|
+
project: Path, limit: int = 50, *, stale_running_seconds: float = 300.0
|
|
5690
|
+
) -> dict[str, Any]:
|
|
5691
|
+
"""Advance all retryable maintenance items in one bounded invocation.
|
|
5692
|
+
|
|
5693
|
+
Crash recovery: a `running` item left behind by a process that died
|
|
5694
|
+
mid-maintenance never returns to `pending`/`failed` on its own, so it
|
|
5695
|
+
would stall forever. Once a `running` item is older than
|
|
5696
|
+
`stale_running_seconds` it is treated as stuck and re-included here.
|
|
5697
|
+
`claim_outbox` returns a `running` item as-is (it only claims from
|
|
5698
|
+
pending/failed/pending-judge), and `maintain_knowledge` does NOT early
|
|
5699
|
+
return for `status == "running"` (only for `"completed"`) -- it falls
|
|
5700
|
+
through to the try block and retries the maintenance steps in place, so
|
|
5701
|
+
simply including the stale id in the drain batch is sufficient recovery.
|
|
5702
|
+
"""
|
|
5456
5703
|
project = project.resolve()
|
|
5457
5704
|
root = _outbox_root(project)
|
|
5458
|
-
|
|
5705
|
+
now = time.time()
|
|
5706
|
+
retryable_ids = {
|
|
5459
5707
|
path.stem
|
|
5460
5708
|
for status in ("pending", "failed")
|
|
5461
5709
|
for path in (root / status).glob("*.json")
|
|
5462
|
-
}
|
|
5710
|
+
}
|
|
5711
|
+
stale_running_ids = {
|
|
5712
|
+
path.stem
|
|
5713
|
+
for path in (root / "running").glob("*.json")
|
|
5714
|
+
if now - path.stat().st_mtime >= stale_running_seconds
|
|
5715
|
+
}
|
|
5716
|
+
all_archive_ids = sorted(retryable_ids | stale_running_ids)
|
|
5463
5717
|
archive_ids = all_archive_ids[:max(0, limit)]
|
|
5464
5718
|
results = [maintain_knowledge(project, archive_id) for archive_id in archive_ids]
|
|
5465
5719
|
return {
|
|
@@ -5467,6 +5721,7 @@ def drain_maintenance_outbox(project: Path, limit: int = 50) -> dict[str, Any]:
|
|
|
5467
5721
|
"project": str(project),
|
|
5468
5722
|
"processed": len(results),
|
|
5469
5723
|
"remaining": max(0, len(all_archive_ids) - len(results)),
|
|
5724
|
+
"staleRunningRecovered": len(stale_running_ids & set(archive_ids)),
|
|
5470
5725
|
"results": results,
|
|
5471
5726
|
}
|
|
5472
5727
|
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=6a2c743a1f8ceb0b; overlay=none; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-knowledge-query
|
|
25
25
|
|
|
26
26
|
并行 change 中查询必须传 `--change <change-id>`;成功后以该 change 的 `meta/knowledge-context.json` 为稳定指针。全局 `context-packs/latest.json` 仅向后兼容,不得用于判断多个 active change 的上下文归属。
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=6a2c743a1f8ceb0b; overlay=none; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-plan — 需求规划
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
- Bash(codegraph *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=6a2c743a1f8ceb0b; overlay=none; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-review — 代码审查
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -146,7 +146,7 @@ review 结果默认只作参考,不阻塞 submit/archive;报告措辞遵循
|
|
|
146
146
|
|
|
147
147
|
### 四、Shell 安全 / 敏感信息 / 证据化报告 / CodeGraph 探索
|
|
148
148
|
|
|
149
|
-
git diff/log 命令通过 `powershell.exe -Command "..."` 执行;review-report 中如发现明文 token/密码/密钥,必须列入 RED 问题并在报告中以 `<TOKEN_REDACTED>` 等占位符引用;RED/YELLOW/OK 结论必须基于实际 diff 内容,不得凭印象判断。代码探索必须优先使用 CodeGraph MCP 工具(`mcp__codegraph__codegraph_explore`),不允许通过普通 Bash 调 codegraph 命令(已列入 `disallowed-tools
|
|
149
|
+
git diff/log 命令通过 `powershell.exe -Command "..."` 执行;review-report 中如发现明文 token/密码/密钥,必须列入 RED 问题并在报告中以 `<TOKEN_REDACTED>` 等占位符引用;RED/YELLOW/OK 结论必须基于实际 diff 内容,不得凭印象判断。代码探索必须优先使用 CodeGraph MCP 工具(`mcp__codegraph__codegraph_explore`),不允许通过普通 Bash 调 codegraph 命令(已列入 `disallowed-tools`);采纳任何返回源码前,按 `reference.md` 的 C12 合同校验 executionRoot/rootPath、worktreeId、repositoryId、HEAD 与 index snapshot,`IDENTITY_MISMATCH` 必须读取实际 worktree,不得当作 stale 忽略;MCP 不可用时降级为 Grep/Glob + Read,并在执行日志记录降级原因。遵循 `../protocols/powershell-protocol.md` / `sensitive-info-protocol.md` / `evidence-based-reporting-protocol.md`。
|
|
150
150
|
|
|
151
151
|
### 五、state snapshot 与源码重验
|
|
152
152
|
|