@hunter-harness/workflow-harness 0.2.74 → 0.2.76
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 +30 -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 +2 -1
- package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +2 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +2 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +2 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +90 -10
- package/harness/bundles/general/claude-code/scripts/harness_change.py +95 -0
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +101 -1
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +30 -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 +2 -1
- package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +2 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +2 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +90 -10
- package/harness/bundles/general/codebuddy/scripts/harness_change.py +95 -0
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +101 -1
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +30 -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 +2 -1
- package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +2 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +2 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +2 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +90 -10
- package/harness/bundles/general/codex/scripts/harness_change.py +95 -0
- package/harness/bundles/general/codex/scripts/harness_runtime.py +101 -1
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +30 -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 +2 -1
- package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +2 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +2 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +90 -10
- package/harness/bundles/general/cursor/scripts/harness_change.py +95 -0
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +101 -1
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +2 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +30 -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 +2 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +2 -1
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +2 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +2 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +90 -10
- package/harness/bundles/java/claude-code/scripts/harness_change.py +95 -0
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +101 -1
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +2 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +30 -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 +2 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +2 -1
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +2 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +2 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +90 -10
- package/harness/bundles/java/codebuddy/scripts/harness_change.py +95 -0
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +101 -1
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +2 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +30 -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 +2 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +2 -1
- package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +2 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +2 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +2 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +90 -10
- package/harness/bundles/java/codex/scripts/harness_change.py +95 -0
- package/harness/bundles/java/codex/scripts/harness_runtime.py +101 -1
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +2 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +30 -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 +2 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +2 -1
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +2 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +2 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +90 -10
- package/harness/bundles/java/cursor/scripts/harness_change.py +95 -0
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +101 -1
- package/harness/manifests/general/claude-code.json +18 -18
- package/harness/manifests/general/codebuddy.json +18 -18
- package/harness/manifests/general/codex.json +18 -18
- package/harness/manifests/general/cursor.json +18 -18
- package/harness/manifests/java/claude-code.json +20 -20
- package/harness/manifests/java/codebuddy.json +20 -20
- package/harness/manifests/java/codex.json +20 -20
- package/harness/manifests/java/cursor.json +20 -20
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
|
@@ -8552,8 +8552,17 @@ def cmd_finalize(
|
|
|
8552
8552
|
payload["knowledgeMaintenance"] = _knowledge_maintenance_from_archive_push(
|
|
8553
8553
|
push_result
|
|
8554
8554
|
)
|
|
8555
|
-
|
|
8556
|
-
payload
|
|
8555
|
+
remote_durability = remote_durable_archive_durability(
|
|
8556
|
+
payload.get("archiveDurability"), push_result
|
|
8557
|
+
)
|
|
8558
|
+
if remote_durability is not None:
|
|
8559
|
+
payload["archiveDurability"] = remote_durability
|
|
8560
|
+
# 报告必须跟着改口:上传发生在 summary 落盘之后,不回写就会出现
|
|
8561
|
+
# "回执 durable / 报告 local-only" 的自相矛盾
|
|
8562
|
+
if not persist_archive_durability(summary_path, remote_durability):
|
|
8563
|
+
warnings.append(
|
|
8564
|
+
"远端归档已持久化,但 summary-data.json 的 archiveDurability 回写失败"
|
|
8565
|
+
)
|
|
8557
8566
|
if push_result.get("warning"):
|
|
8558
8567
|
warnings.append(str(push_result["warning"]))
|
|
8559
8568
|
|
|
@@ -9121,8 +9130,57 @@ def build_archive_package(
|
|
|
9121
9130
|
}
|
|
9122
9131
|
|
|
9123
9132
|
|
|
9133
|
+
def remote_durable_archive_durability(
|
|
9134
|
+
current: Any, push_result: Mapping[str, Any]
|
|
9135
|
+
) -> dict[str, Any] | None:
|
|
9136
|
+
"""远端上传成功后的耐久性投影;未 durable 时返回 None。
|
|
9137
|
+
|
|
9138
|
+
summary-data.json 在归档流程早期就以 ARCHIVED_LOCAL_ONLY 写盘,而 ZIP 上传在
|
|
9139
|
+
那之后才发生。此前上传成功只改内存里的 payload、还把对象覆盖成裸字符串,落盘
|
|
9140
|
+
报告从没跟着改——于是同一次归档,回执说 durable、报告说 local-only,平台和用户
|
|
9141
|
+
读到的是后者。这里保持对象形状并带上回执标识,供调用方同时更新 payload 与文件。
|
|
9142
|
+
"""
|
|
9143
|
+
if str(push_result.get("archiveStatus") or "") != "durable":
|
|
9144
|
+
return None
|
|
9145
|
+
projected = dict(current) if isinstance(current, dict) else {}
|
|
9146
|
+
projected.update({
|
|
9147
|
+
"status": "ARCHIVED_REMOTE_DURABLE",
|
|
9148
|
+
# 已远端持久化:不再声称"只存在于本地、可能随工作区丢失"
|
|
9149
|
+
"risk": None,
|
|
9150
|
+
"archiveId": push_result.get("archiveId"),
|
|
9151
|
+
"uploadStatus": push_result.get("uploadStatus"),
|
|
9152
|
+
"knowledgeStatus": push_result.get("knowledgeStatus"),
|
|
9153
|
+
})
|
|
9154
|
+
return projected
|
|
9155
|
+
|
|
9156
|
+
|
|
9157
|
+
def persist_archive_durability(summary_path: Path, durability: dict[str, Any]) -> bool:
|
|
9158
|
+
"""把耐久性结论回写到已落盘的 summary,其余字段原样保留。
|
|
9159
|
+
|
|
9160
|
+
summary-data.json 本就被排除在归档校验和覆盖之外(它在覆盖快照之后才写),
|
|
9161
|
+
所以这次回写不会破坏 manifest 对账。
|
|
9162
|
+
"""
|
|
9163
|
+
try:
|
|
9164
|
+
summary = json.loads(Path(summary_path).read_text(encoding="utf-8-sig"))
|
|
9165
|
+
except (OSError, json.JSONDecodeError):
|
|
9166
|
+
return False
|
|
9167
|
+
if not isinstance(summary, dict):
|
|
9168
|
+
return False
|
|
9169
|
+
summary["archiveDurability"] = durability
|
|
9170
|
+
try:
|
|
9171
|
+
write_json(Path(summary_path), summary)
|
|
9172
|
+
except OSError:
|
|
9173
|
+
return False
|
|
9174
|
+
return True
|
|
9175
|
+
|
|
9176
|
+
|
|
9124
9177
|
def auto_push_managed_snapshot(project_root: Path) -> dict[str, Any]:
|
|
9125
|
-
"""Best-effort upload of rules
|
|
9178
|
+
"""Best-effort upload of rules, Codebase Map and archive deliverables via remote sync.
|
|
9179
|
+
|
|
9180
|
+
走 `harness-push`(remote-sync)而非 legacy `push`(proposal)。后者写的是
|
|
9181
|
+
project_files_current,不产生分支快照,平台的「分支文件」与「项目资料」两个视图
|
|
9182
|
+
都读不到;归档交付物在那条路上还会被 policy-never 全部跳过。
|
|
9183
|
+
"""
|
|
9126
9184
|
project_root = project_root.resolve()
|
|
9127
9185
|
credentials = _resolve_archive_remote_credentials(project_root, os.environ)
|
|
9128
9186
|
if not credentials["configured"]:
|
|
@@ -9141,11 +9199,16 @@ def auto_push_managed_snapshot(project_root: Path) -> dict[str, Any]:
|
|
|
9141
9199
|
"reasonCode": "MANAGED_SNAPSHOT_UPLOAD_DEFERRED",
|
|
9142
9200
|
"warning": f"项目规则与架构地图上传已延后:{exc}",
|
|
9143
9201
|
}
|
|
9202
|
+
# 显式列出五个 scope 而非 `all`:含义固定,不随 `all` 的展开定义漂移。
|
|
9203
|
+
# branch_files 覆盖归档交付物(plans/spec/reports/docs),其余四个覆盖配置、
|
|
9204
|
+
# 规则、架构地图与指令入口。
|
|
9144
9205
|
command = [
|
|
9145
9206
|
*launcher,
|
|
9146
9207
|
"--yes",
|
|
9147
9208
|
"hunter-harness",
|
|
9148
|
-
"push",
|
|
9209
|
+
"harness-push",
|
|
9210
|
+
"--scope",
|
|
9211
|
+
"config,rules,architecture,instructions,branch_files",
|
|
9149
9212
|
"--yes",
|
|
9150
9213
|
"--non-interactive",
|
|
9151
9214
|
"--json",
|
|
@@ -9159,6 +9222,10 @@ def auto_push_managed_snapshot(project_root: Path) -> dict[str, Any]:
|
|
|
9159
9222
|
command,
|
|
9160
9223
|
cwd=str(project_root),
|
|
9161
9224
|
text=True,
|
|
9225
|
+
# 不指定 encoding 时中文 Windows 会按 cp936 解码 UTF-8,静默损坏 CLI
|
|
9226
|
+
# 的中文输出,并可能让下面的 json.loads 失败。
|
|
9227
|
+
encoding="utf-8",
|
|
9228
|
+
errors="replace",
|
|
9162
9229
|
stdout=subprocess.PIPE,
|
|
9163
9230
|
stderr=subprocess.PIPE,
|
|
9164
9231
|
timeout=180,
|
|
@@ -9176,24 +9243,37 @@ def auto_push_managed_snapshot(project_root: Path) -> dict[str, Any]:
|
|
|
9176
9243
|
except (json.JSONDecodeError, TypeError):
|
|
9177
9244
|
output = None
|
|
9178
9245
|
if completed.returncode != 0 or not isinstance(output, dict):
|
|
9179
|
-
|
|
9246
|
+
# 失败时丢掉 stdout/stderr 等于把线索一起丢了:plan/spec 与规则、架构地图
|
|
9247
|
+
# 全靠这条推送上平台,用户在平台上只会看到"东西没上去",却无从查为什么。
|
|
9248
|
+
# 保留有界的诊断片段(push 本身拒绝上传 secret,这里再截断兜底)。
|
|
9249
|
+
detail = (completed.stderr or completed.stdout or "").strip()
|
|
9250
|
+
failure: dict[str, Any] = {
|
|
9180
9251
|
"ok": False,
|
|
9181
9252
|
"skipped": False,
|
|
9182
9253
|
"exitCode": completed.returncode,
|
|
9183
9254
|
"reasonCode": "MANAGED_SNAPSHOT_UPLOAD_FAILED",
|
|
9255
|
+
"detail": detail[-2_048:],
|
|
9184
9256
|
"warning": (
|
|
9185
|
-
"
|
|
9186
|
-
"
|
|
9257
|
+
"项目规则与架构地图未能同步到平台(归档 plan/spec/report 也随这条推送上传);"
|
|
9258
|
+
"归档不受影响,稍后可运行 npx hunter-harness harness-push "
|
|
9259
|
+
"--scope config,rules,architecture,instructions,branch_files "
|
|
9260
|
+
"--yes --non-interactive 重试。"
|
|
9187
9261
|
),
|
|
9188
9262
|
}
|
|
9263
|
+
cli_code = output.get("code") or output.get("reason_code") if isinstance(output, dict) else None
|
|
9264
|
+
if isinstance(cli_code, str) and cli_code:
|
|
9265
|
+
failure["cliCode"] = cli_code
|
|
9266
|
+
return failure
|
|
9267
|
+
# harness-push 的回执用 summary.applied 计已落盘的操作数(legacy push 用 submitted)。
|
|
9189
9268
|
summary = output.get("summary")
|
|
9190
|
-
|
|
9269
|
+
applied = summary.get("applied", 0) if isinstance(summary, dict) else 0
|
|
9191
9270
|
return {
|
|
9192
9271
|
"ok": bool(output.get("ok", True)),
|
|
9193
9272
|
"skipped": False,
|
|
9194
9273
|
"projectId": output.get("project_id"),
|
|
9195
|
-
"submitted":
|
|
9196
|
-
|
|
9274
|
+
"submitted": applied if isinstance(applied, int) else 0,
|
|
9275
|
+
# 无变更时 harness-push 直接短路为 no_changes,不产生快照——即"仅在有更新时推送"。
|
|
9276
|
+
"unchanged": output.get("outcome") == "no_changes",
|
|
9197
9277
|
}
|
|
9198
9278
|
|
|
9199
9279
|
|
|
@@ -1081,6 +1081,90 @@ def cmd_resolve(args: argparse.Namespace) -> int:
|
|
|
1081
1081
|
)
|
|
1082
1082
|
|
|
1083
1083
|
|
|
1084
|
+
def declare_product_ownership(
|
|
1085
|
+
project_root: Path, change_id: str, *, product_paths: list[str]
|
|
1086
|
+
) -> dict[str, Any]:
|
|
1087
|
+
"""把 `ownership.productPaths` 写进 change 契约。
|
|
1088
|
+
|
|
1089
|
+
这个字段此前没有任何写入方:plan 的 validate_product_ownership 只校验、缺失时
|
|
1090
|
+
软放行,而归档的 compute_ownership_diff 会把全部改动判成 foreignPaths,
|
|
1091
|
+
filesChanged=0 直接触发 DIFF_ZERO_WITH_NONEMPTY_COMMIT。两端口径不一致,中间
|
|
1092
|
+
没工具能补——只能手改契约。这里给出正规入口。
|
|
1093
|
+
|
|
1094
|
+
规则与 plan 校验一致:只收精确文件或目录前缀,不接受通配。
|
|
1095
|
+
"""
|
|
1096
|
+
resolved = resolve_change(project_root, change_id)
|
|
1097
|
+
if not resolved.get("ok"):
|
|
1098
|
+
return resolved
|
|
1099
|
+
change_dir = Path(resolved["changeDir"])
|
|
1100
|
+
normalized = sorted({
|
|
1101
|
+
str(item).strip().replace("\\", "/").removeprefix("./")
|
|
1102
|
+
for item in product_paths
|
|
1103
|
+
if isinstance(item, str) and item.strip()
|
|
1104
|
+
})
|
|
1105
|
+
if not normalized:
|
|
1106
|
+
return {
|
|
1107
|
+
"ok": False,
|
|
1108
|
+
"code": "PLAN_PRODUCT_PATHS_REQUIRED",
|
|
1109
|
+
"message": "至少声明一条 productPaths(精确文件或目录前缀)",
|
|
1110
|
+
}
|
|
1111
|
+
unsupported = sorted(p for p in normalized if any(ch in p for ch in "*?[]"))
|
|
1112
|
+
if unsupported:
|
|
1113
|
+
return {
|
|
1114
|
+
"ok": False,
|
|
1115
|
+
"code": "PLAN_PRODUCT_PATHS_GLOB_UNSUPPORTED",
|
|
1116
|
+
"message": "productPaths 只接受精确文件或目录前缀,不支持通配:" + ", ".join(unsupported),
|
|
1117
|
+
"unsupportedPaths": unsupported,
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
context_path = change_dir / CHANGE_CONTEXT_REL
|
|
1121
|
+
try:
|
|
1122
|
+
context = _read_json(context_path)
|
|
1123
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1124
|
+
return {"ok": False, "code": "CHANGE_CONTEXT_INVALID", "message": str(exc)}
|
|
1125
|
+
if not isinstance(context, dict):
|
|
1126
|
+
return {"ok": False, "code": "CHANGE_CONTEXT_INVALID", "message": "change-context.json 不是对象"}
|
|
1127
|
+
|
|
1128
|
+
ownership = context.get("ownership")
|
|
1129
|
+
ownership = dict(ownership) if isinstance(ownership, dict) else {}
|
|
1130
|
+
if ownership.get("productPaths") == normalized:
|
|
1131
|
+
return {
|
|
1132
|
+
"ok": True,
|
|
1133
|
+
"code": "PRODUCT_OWNERSHIP_DECLARED",
|
|
1134
|
+
"idempotent": True,
|
|
1135
|
+
"changeId": change_id,
|
|
1136
|
+
"productPaths": normalized,
|
|
1137
|
+
"path": str(context_path),
|
|
1138
|
+
}
|
|
1139
|
+
ownership["productPaths"] = normalized
|
|
1140
|
+
context["ownership"] = ownership
|
|
1141
|
+
_write_json(context_path, context)
|
|
1142
|
+
return {
|
|
1143
|
+
"ok": True,
|
|
1144
|
+
"code": "PRODUCT_OWNERSHIP_DECLARED",
|
|
1145
|
+
"idempotent": False,
|
|
1146
|
+
"changeId": change_id,
|
|
1147
|
+
"productPaths": normalized,
|
|
1148
|
+
"path": str(context_path),
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def cmd_declare_ownership(args: argparse.Namespace) -> int:
|
|
1153
|
+
project = resolve_main_project_root()
|
|
1154
|
+
payload = declare_product_ownership(
|
|
1155
|
+
project, args.change, product_paths=list(args.product_path)
|
|
1156
|
+
)
|
|
1157
|
+
if payload.get("ok"):
|
|
1158
|
+
emit(payload, as_json=bool(args.json))
|
|
1159
|
+
return 0
|
|
1160
|
+
return emit_error(
|
|
1161
|
+
str(payload.get("code", "PRODUCT_OWNERSHIP_FAILED")),
|
|
1162
|
+
str(payload.get("message", "declare ownership failed")),
|
|
1163
|
+
as_json=bool(args.json),
|
|
1164
|
+
extra={k: v for k, v in payload.items() if k not in {"ok", "message"}},
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
|
|
1084
1168
|
def cmd_migrate(args: argparse.Namespace) -> int:
|
|
1085
1169
|
project = resolve_main_project_root()
|
|
1086
1170
|
if not args.change:
|
|
@@ -1451,6 +1535,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1451
1535
|
p_migrate.add_argument("--change", required=True)
|
|
1452
1536
|
p_migrate.set_defaults(func=cmd_migrate)
|
|
1453
1537
|
|
|
1538
|
+
p_ownership = sub.add_parser(
|
|
1539
|
+
"declare-ownership", parents=[shared],
|
|
1540
|
+
help="declare ownership.productPaths so archive can project the real diff",
|
|
1541
|
+
)
|
|
1542
|
+
p_ownership.add_argument("--change", required=True)
|
|
1543
|
+
p_ownership.add_argument(
|
|
1544
|
+
"--product-path", required=True, action="append",
|
|
1545
|
+
help="exact file or directory prefix (no globs); repeatable",
|
|
1546
|
+
)
|
|
1547
|
+
p_ownership.set_defaults(func=cmd_declare_ownership)
|
|
1548
|
+
|
|
1454
1549
|
p_claim = sub.add_parser("claim", parents=[shared])
|
|
1455
1550
|
p_claim.add_argument("--change", required=True)
|
|
1456
1551
|
p_claim.add_argument("--phase", required=True)
|
|
@@ -157,6 +157,35 @@ def _private_evidence_root(explicit: Path | None = None) -> Path:
|
|
|
157
157
|
Path.home() / ".harness" / "private-evidence").expanduser().resolve()
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
def private_evidence_root_for(source: Path, explicit: Path | None = None) -> Path:
|
|
161
|
+
"""私有隔离根:必须与源同一驱动器。
|
|
162
|
+
|
|
163
|
+
`os.replace` 跨驱动器会直接失败(Windows 上是 WinError 17)。默认根
|
|
164
|
+
`~/.harness/private-evidence` 在 Windows 落 C 盘,项目在别的盘时隔离必然失败
|
|
165
|
+
——而隔离是归档的硬前置,等于整条归档路被堵死。同盘时沿用配置/默认值;不同盘
|
|
166
|
+
时退到源所在驱动器根下的 `.harness-private-evidence`(天然在任何项目根之外)。
|
|
167
|
+
POSIX 上 drive 恒为空串,行为不变。
|
|
168
|
+
"""
|
|
169
|
+
source = source.expanduser().resolve()
|
|
170
|
+
preferred = _private_evidence_root(explicit)
|
|
171
|
+
if preferred.drive.lower() == source.drive.lower():
|
|
172
|
+
return preferred
|
|
173
|
+
return (Path(source.anchor) / ".harness-private-evidence").resolve()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _infer_project_root(change_root: Path) -> Path | None:
|
|
177
|
+
"""change 目录形如 `<project>/.harness/changes/<name>`:取路径里 `.harness` 段的父目录。
|
|
178
|
+
|
|
179
|
+
不做"向上找存在 .harness 的目录"式搜索——用户 HOME 下通常也有 `.harness`,
|
|
180
|
+
那样会把 HOME 误判成项目根,把合法的临时隔离位置一并拒掉。
|
|
181
|
+
"""
|
|
182
|
+
parts = change_root.parts
|
|
183
|
+
for index in range(len(parts) - 1, 0, -1):
|
|
184
|
+
if parts[index] == ".harness":
|
|
185
|
+
return Path(*parts[:index])
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
160
189
|
def _secure_private_path(path: Path, *, directory: bool) -> str:
|
|
161
190
|
"""Best-effort owner-only permissions, with an explicit ACL result on Windows."""
|
|
162
191
|
mode = 0o700 if directory else 0o600
|
|
@@ -288,11 +317,12 @@ def quarantine_sensitive_evidence(
|
|
|
288
317
|
change_root: Path,
|
|
289
318
|
reason: str = "sensitive evidence",
|
|
290
319
|
private_root: Path | None = None,
|
|
320
|
+
project_root: Path | None = None,
|
|
291
321
|
) -> dict[str, Any]:
|
|
292
322
|
"""Atomically move legacy plaintext evidence outside any publishable tree."""
|
|
293
323
|
source = source.expanduser().resolve()
|
|
294
324
|
change_root = change_root.expanduser().resolve()
|
|
295
|
-
private =
|
|
325
|
+
private = private_evidence_root_for(source, private_root)
|
|
296
326
|
if not source.is_file():
|
|
297
327
|
return {"ok": False, "reasonCode": "SENSITIVE_EVIDENCE_SOURCE_MISSING", "sourcePath": str(source)}
|
|
298
328
|
if private == change_root or private.is_relative_to(change_root):
|
|
@@ -301,6 +331,25 @@ def quarantine_sensitive_evidence(
|
|
|
301
331
|
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
|
|
302
332
|
"error": "private quarantine root must be outside the publishable change root",
|
|
303
333
|
}
|
|
334
|
+
# 归档的密钥扫描门禁按**项目根**判定(SECRET_SCAN_PRIVATE_PATH_IN_COPY_ROOT)。
|
|
335
|
+
# 这里此前只看 change_root,比门禁宽——于是隔离"成功"了,门禁再拒一次,调用方
|
|
336
|
+
# 得多试一轮才找到合法位置。对齐到同一条边界,错就错在当场。
|
|
337
|
+
resolved_project = (
|
|
338
|
+
project_root.expanduser().resolve()
|
|
339
|
+
if project_root is not None
|
|
340
|
+
else _infer_project_root(change_root)
|
|
341
|
+
)
|
|
342
|
+
if resolved_project is not None and (
|
|
343
|
+
private == resolved_project or private.is_relative_to(resolved_project)
|
|
344
|
+
):
|
|
345
|
+
return {
|
|
346
|
+
"ok": False,
|
|
347
|
+
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
|
|
348
|
+
"error": (
|
|
349
|
+
"private quarantine root must be outside the project root "
|
|
350
|
+
f"({resolved_project}); archive's secret scan rejects paths inside it"
|
|
351
|
+
),
|
|
352
|
+
}
|
|
304
353
|
try:
|
|
305
354
|
raw_digest = "sha256:" + hashlib.sha256(source.read_bytes()).hexdigest()
|
|
306
355
|
quarantine_dir = private / uuid.uuid4().hex
|
|
@@ -1850,6 +1899,37 @@ def doctor(project: Path, change_dir: Path, *, agent: str) -> dict[str, Any]:
|
|
|
1850
1899
|
return {"ok": True, "action": "doctor", **payload}
|
|
1851
1900
|
|
|
1852
1901
|
|
|
1902
|
+
def cmd_quarantine_evidence(args: argparse.Namespace) -> dict[str, Any]:
|
|
1903
|
+
"""隔离命令:把明文敏感证据原子移出可发布树,逐条返回结果。
|
|
1904
|
+
|
|
1905
|
+
单条失败不吞掉其余条目——归档阻断项常常一次报多个文件,逐条报告才能一轮修完。
|
|
1906
|
+
"""
|
|
1907
|
+
project = Path(args.project).expanduser().resolve()
|
|
1908
|
+
change_root = Path(args.change_dir).expanduser().resolve()
|
|
1909
|
+
private_root = Path(args.private_root) if args.private_root else None
|
|
1910
|
+
entries: list[dict[str, Any]] = []
|
|
1911
|
+
for raw in args.file:
|
|
1912
|
+
candidate = Path(raw)
|
|
1913
|
+
source = candidate if candidate.is_absolute() else change_root / candidate
|
|
1914
|
+
outcome = quarantine_sensitive_evidence(
|
|
1915
|
+
source,
|
|
1916
|
+
change_root=change_root,
|
|
1917
|
+
reason=args.reason,
|
|
1918
|
+
private_root=private_root,
|
|
1919
|
+
project_root=project,
|
|
1920
|
+
)
|
|
1921
|
+
entries.append({"file": raw, **outcome})
|
|
1922
|
+
failed = [item for item in entries if not item.get("ok")]
|
|
1923
|
+
return {
|
|
1924
|
+
"ok": not failed,
|
|
1925
|
+
"action": "quarantine-evidence",
|
|
1926
|
+
"code": "SENSITIVE_EVIDENCE_QUARANTINED" if not failed else "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
|
|
1927
|
+
"quarantined": len(entries) - len(failed),
|
|
1928
|
+
"failed": len(failed),
|
|
1929
|
+
"entries": entries,
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
|
|
1853
1933
|
def build_parser() -> argparse.ArgumentParser:
|
|
1854
1934
|
parser = argparse.ArgumentParser(prog="harness_runtime.py")
|
|
1855
1935
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
@@ -1858,6 +1938,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1858
1938
|
p_doctor.add_argument("--change-dir", required=True)
|
|
1859
1939
|
p_doctor.add_argument("--agent", choices=sorted(_ADAPTERS), required=True)
|
|
1860
1940
|
p_doctor.add_argument("--json", action="store_true")
|
|
1941
|
+
# 归档的 SENSITIVE_EVIDENCE_UNQUARANTINED 是硬阻断,此前却没有任何命令行入口
|
|
1942
|
+
# ——调用方只能写 python -c + sys.path hack 去调内部函数。
|
|
1943
|
+
p_quarantine = sub.add_parser(
|
|
1944
|
+
"quarantine-evidence",
|
|
1945
|
+
help="move plaintext sensitive evidence outside the publishable tree",
|
|
1946
|
+
)
|
|
1947
|
+
p_quarantine.add_argument("--project", required=True)
|
|
1948
|
+
p_quarantine.add_argument("--change-dir", required=True)
|
|
1949
|
+
p_quarantine.add_argument(
|
|
1950
|
+
"--file", required=True, action="append",
|
|
1951
|
+
help="path to quarantine, relative to --change-dir or absolute; repeatable",
|
|
1952
|
+
)
|
|
1953
|
+
p_quarantine.add_argument("--reason", default="sensitive evidence")
|
|
1954
|
+
p_quarantine.add_argument(
|
|
1955
|
+
"--private-root", default=None,
|
|
1956
|
+
help="override the private root; must be outside the project root and on its drive",
|
|
1957
|
+
)
|
|
1958
|
+
p_quarantine.add_argument("--json", action="store_true")
|
|
1861
1959
|
p_adapter = sub.add_parser("adapter")
|
|
1862
1960
|
p_adapter.add_argument("--agent", choices=sorted(_ADAPTERS), required=True)
|
|
1863
1961
|
p_adapter.add_argument("--change", required=True)
|
|
@@ -1899,6 +1997,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1899
1997
|
try:
|
|
1900
1998
|
if args.command == "doctor":
|
|
1901
1999
|
result = doctor(Path(args.project), Path(args.change_dir), agent=args.agent)
|
|
2000
|
+
elif args.command == "quarantine-evidence":
|
|
2001
|
+
result = cmd_quarantine_evidence(args)
|
|
1902
2002
|
elif args.command == "adapter":
|
|
1903
2003
|
result = {"ok": True, "action": "adapter", **adapter_worktree(args.agent, args.change)}
|
|
1904
2004
|
elif args.command == "_worker":
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"schema_version": 2,
|
|
3
3
|
"profile": "general",
|
|
4
4
|
"adapter": "claude-code",
|
|
5
|
-
"bundle_version": "0.2.
|
|
5
|
+
"bundle_version": "0.2.65",
|
|
6
6
|
"requires": {
|
|
7
|
-
"minimumCliVersion": "0.2.
|
|
7
|
+
"minimumCliVersion": "0.2.84",
|
|
8
8
|
"capabilities": [
|
|
9
9
|
"sync@2",
|
|
10
10
|
"rules-sync@1",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
{
|
|
27
27
|
"path": ".harness-build.json",
|
|
28
|
-
"sha256": "
|
|
28
|
+
"sha256": "afc63fd684e23521c2f50072e6cca9b7f1b2b8888b5d6297006e4a4734dece62"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "agents/harness-evaluator.md",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
"path": "harness-archive/SKILL.md",
|
|
100
|
-
"sha256": "
|
|
100
|
+
"sha256": "6f4b9061fac582da91d6f49e9c9366cb9c9db7b43e5a9826ce6698c0c82666ae"
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
"path": "harness-archive/templates/summary-data-template.json",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"path": "harness-codebase-map/SKILL.md",
|
|
116
|
-
"sha256": "
|
|
116
|
+
"sha256": "96c33bd02dc458171174eef38036a4ecd8ca55a8d40c5b9eff9d5082baa01da9"
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
"path": "harness-codebase-map/templates/ARCHITECTURE.md",
|
|
@@ -153,11 +153,11 @@
|
|
|
153
153
|
},
|
|
154
154
|
{
|
|
155
155
|
"path": "harness-knowledge-ingest/SKILL.md",
|
|
156
|
-
"sha256": "
|
|
156
|
+
"sha256": "6661efc406f2ce89547afcc375b44688a43cde96a83b487d56701a2e7e85b055"
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
"path": "harness-knowledge-query/SKILL.md",
|
|
160
|
-
"sha256": "
|
|
160
|
+
"sha256": "032e4123cb0c61233aa6674f9fdc93617edea856f20cf526c350d86d7a3aa3b4"
|
|
161
161
|
},
|
|
162
162
|
{
|
|
163
163
|
"path": "harness-plan/checklist.md",
|
|
@@ -173,15 +173,15 @@
|
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
175
|
"path": "harness-plan/SKILL.md",
|
|
176
|
-
"sha256": "
|
|
176
|
+
"sha256": "5b4d279fc3c8708d6050195e6195bfb688edb3fc63d18320886d0e960f6c6a57"
|
|
177
177
|
},
|
|
178
178
|
{
|
|
179
179
|
"path": "harness-pull/SKILL.md",
|
|
180
|
-
"sha256": "
|
|
180
|
+
"sha256": "b6746c4de3f880f5e8bf6b84868ce915c4eb078a3344d4de24f77c18d6cb27b0"
|
|
181
181
|
},
|
|
182
182
|
{
|
|
183
183
|
"path": "harness-push/SKILL.md",
|
|
184
|
-
"sha256": "
|
|
184
|
+
"sha256": "0d96f9391fc090d7d5c0ff9f443b9d81c762ac08d53d41dc1346e1eb44eedba6"
|
|
185
185
|
},
|
|
186
186
|
{
|
|
187
187
|
"path": "harness-review/checklist.md",
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
},
|
|
198
198
|
{
|
|
199
199
|
"path": "harness-review/SKILL.md",
|
|
200
|
-
"sha256": "
|
|
200
|
+
"sha256": "1e087a09f1d81cc3e12be17869cd40d47ed8c9f2f54bea2471d051a95d827f4b"
|
|
201
201
|
},
|
|
202
202
|
{
|
|
203
203
|
"path": "harness-run/checklist.md",
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
},
|
|
214
214
|
{
|
|
215
215
|
"path": "harness-run/SKILL.md",
|
|
216
|
-
"sha256": "
|
|
216
|
+
"sha256": "2417fb4b0f261735c7e130ff3e7146568ad74aa41c5844b562c6420b6edf5f25"
|
|
217
217
|
},
|
|
218
218
|
{
|
|
219
219
|
"path": "harness-submit/checklist.md",
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
},
|
|
226
226
|
{
|
|
227
227
|
"path": "harness-submit/SKILL.md",
|
|
228
|
-
"sha256": "
|
|
228
|
+
"sha256": "4e2f38d337bc848db1f93852369e421f4f0827afa213efd19354cae798bc0ca5"
|
|
229
229
|
},
|
|
230
230
|
{
|
|
231
231
|
"path": "harness-sync/reference.md",
|
|
@@ -233,7 +233,7 @@
|
|
|
233
233
|
},
|
|
234
234
|
{
|
|
235
235
|
"path": "harness-sync/SKILL.md",
|
|
236
|
-
"sha256": "
|
|
236
|
+
"sha256": "0d6a7937acf93cccab0a1d9545c5fc065d0db874a4740908b5c1516b279f3284"
|
|
237
237
|
},
|
|
238
238
|
{
|
|
239
239
|
"path": "harness-test/checklist.md",
|
|
@@ -257,7 +257,7 @@
|
|
|
257
257
|
},
|
|
258
258
|
{
|
|
259
259
|
"path": "harness-test/SKILL.md",
|
|
260
|
-
"sha256": "
|
|
260
|
+
"sha256": "67b2095d756cd3d7b2b14875b97c1df4b55ec1402c21e12903569e4d4efecc6b"
|
|
261
261
|
},
|
|
262
262
|
{
|
|
263
263
|
"path": "protocols/archive-report-protocol.md",
|
|
@@ -329,11 +329,11 @@
|
|
|
329
329
|
},
|
|
330
330
|
{
|
|
331
331
|
"path": "scripts/harness_archive.py",
|
|
332
|
-
"sha256": "
|
|
332
|
+
"sha256": "2637b2a82bfbd6c594183bb35cfa9ba8c42e8c77f67ccdecce628066f93d8048"
|
|
333
333
|
},
|
|
334
334
|
{
|
|
335
335
|
"path": "scripts/harness_change.py",
|
|
336
|
-
"sha256": "
|
|
336
|
+
"sha256": "f4e2d4ab9ee13e93cf200555b6b116342ad044dcdd5dfa25fa7e7ca435dd1882"
|
|
337
337
|
},
|
|
338
338
|
{
|
|
339
339
|
"path": "scripts/harness_check_gate.py",
|
|
@@ -437,7 +437,7 @@
|
|
|
437
437
|
},
|
|
438
438
|
{
|
|
439
439
|
"path": "scripts/harness_runtime.py",
|
|
440
|
-
"sha256": "
|
|
440
|
+
"sha256": "493edc124eda6b79be6242c7d94ec8d619cb14ff134afbfa154795749374091c"
|
|
441
441
|
},
|
|
442
442
|
{
|
|
443
443
|
"path": "scripts/harness_service.py",
|