@hunter-harness/workflow-harness 0.2.79 → 0.2.81
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 +24 -3
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +13 -3
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +13 -3
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +13 -3
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +13 -3
- package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +15 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +13 -3
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +13 -3
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +13 -3
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +13 -3
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +158 -44
- package/harness/bundles/general/claude-code/scripts/harness_context.py +77 -3
- package/harness/bundles/general/claude-code/scripts/harness_events.py +35 -2
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +6 -1
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +24 -3
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +13 -3
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +13 -3
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +13 -3
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +13 -3
- package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +15 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +13 -3
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +13 -3
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +13 -3
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +13 -3
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +158 -44
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +77 -3
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +35 -2
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +6 -1
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +24 -3
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +13 -3
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +13 -3
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +13 -3
- package/harness/bundles/general/codex/harness-plan/SKILL.md +13 -3
- package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +15 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +13 -3
- package/harness/bundles/general/codex/harness-run/SKILL.md +13 -3
- package/harness/bundles/general/codex/harness-submit/SKILL.md +13 -3
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +13 -3
- package/harness/bundles/general/codex/scripts/harness_archive.py +158 -44
- package/harness/bundles/general/codex/scripts/harness_context.py +77 -3
- package/harness/bundles/general/codex/scripts/harness_events.py +35 -2
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +6 -1
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +24 -3
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +13 -3
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +13 -3
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +13 -3
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +13 -3
- package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +15 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +13 -3
- package/harness/bundles/general/cursor/harness-run/SKILL.md +13 -3
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +13 -3
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +13 -3
- package/harness/bundles/general/cursor/scripts/harness_archive.py +158 -44
- package/harness/bundles/general/cursor/scripts/harness_context.py +77 -3
- package/harness/bundles/general/cursor/scripts/harness_events.py +35 -2
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +6 -1
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +13 -3
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +24 -3
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +13 -3
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +13 -3
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +13 -3
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +13 -3
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +13 -3
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +15 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +13 -3
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +13 -3
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +13 -3
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +13 -3
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +158 -44
- package/harness/bundles/java/claude-code/scripts/harness_context.py +77 -3
- package/harness/bundles/java/claude-code/scripts/harness_events.py +35 -2
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +6 -1
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +13 -3
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +24 -3
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +13 -3
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +13 -3
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +13 -3
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +13 -3
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +13 -3
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +15 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +13 -3
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +13 -3
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +13 -3
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +13 -3
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +158 -44
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +77 -3
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +35 -2
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +6 -1
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +13 -3
- package/harness/bundles/java/codex/harness-archive/SKILL.md +24 -3
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +13 -3
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +13 -3
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +13 -3
- package/harness/bundles/java/codex/harness-package/SKILL.md +13 -3
- package/harness/bundles/java/codex/harness-plan/SKILL.md +13 -3
- package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +15 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +13 -3
- package/harness/bundles/java/codex/harness-run/SKILL.md +13 -3
- package/harness/bundles/java/codex/harness-submit/SKILL.md +13 -3
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +13 -3
- package/harness/bundles/java/codex/scripts/harness_archive.py +158 -44
- package/harness/bundles/java/codex/scripts/harness_context.py +77 -3
- package/harness/bundles/java/codex/scripts/harness_events.py +35 -2
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +6 -1
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +13 -3
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +24 -3
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +13 -3
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +13 -3
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +13 -3
- package/harness/bundles/java/cursor/harness-package/SKILL.md +13 -3
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +13 -3
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +15 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +13 -3
- package/harness/bundles/java/cursor/harness-run/SKILL.md +13 -3
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +13 -3
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +13 -3
- package/harness/bundles/java/cursor/scripts/harness_archive.py +158 -44
- package/harness/bundles/java/cursor/scripts/harness_context.py +77 -3
- package/harness/bundles/java/cursor/scripts/harness_events.py +35 -2
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +6 -1
- package/harness/manifests/general/claude-code.json +19 -19
- package/harness/manifests/general/codebuddy.json +19 -19
- package/harness/manifests/general/codex.json +19 -19
- package/harness/manifests/general/cursor.json +19 -19
- package/harness/manifests/java/claude-code.json +21 -21
- package/harness/manifests/java/codebuddy.json +21 -21
- package/harness/manifests/java/codex.json +21 -21
- package/harness/manifests/java/cursor.json +21 -21
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
|
@@ -10232,6 +10232,42 @@ def cmd_repair_cli(args: argparse.Namespace) -> int:
|
|
|
10232
10232
|
return code
|
|
10233
10233
|
|
|
10234
10234
|
|
|
10235
|
+
def resolve_republish_project_root(explicit: Path | None, archive_dir: Path | None) -> Path:
|
|
10236
|
+
"""Locate the project root for a republish without requiring --project.
|
|
10237
|
+
|
|
10238
|
+
``find_project_root`` walks ``p.parents``, which excludes ``p`` itself; it is
|
|
10239
|
+
built for a change/archive subdirectory. Handing it the cwd therefore skips
|
|
10240
|
+
the very directory the operator is standing in and keeps climbing — in a real
|
|
10241
|
+
run that resolved ``E:/WorkProject/kb-sdd`` to its parent and made the command
|
|
10242
|
+
look for archives that were never there.
|
|
10243
|
+
|
|
10244
|
+
Order: explicit flag, then the archive's own location, then cwd and its
|
|
10245
|
+
parents (cwd included), then the deployed script's location — the script
|
|
10246
|
+
lives at ``<project>/.claude|.codebuddy/skills/scripts/`` after install.
|
|
10247
|
+
"""
|
|
10248
|
+
if explicit is not None:
|
|
10249
|
+
return explicit.resolve()
|
|
10250
|
+
if archive_dir is not None:
|
|
10251
|
+
return find_project_root(archive_dir)
|
|
10252
|
+
|
|
10253
|
+
def first_with_archive(start: Path) -> Path | None:
|
|
10254
|
+
start = start.resolve()
|
|
10255
|
+
for candidate in (start, *start.parents):
|
|
10256
|
+
if (candidate / ".harness" / "archive").is_dir():
|
|
10257
|
+
return candidate
|
|
10258
|
+
return None
|
|
10259
|
+
|
|
10260
|
+
found = first_with_archive(Path.cwd())
|
|
10261
|
+
if found is not None:
|
|
10262
|
+
return found
|
|
10263
|
+
found = first_with_archive(Path(__file__).resolve().parent)
|
|
10264
|
+
if found is not None:
|
|
10265
|
+
return found
|
|
10266
|
+
# Nothing carries an archive directory; report against cwd so the error names
|
|
10267
|
+
# a path the operator recognises instead of some ancestor.
|
|
10268
|
+
return Path.cwd().resolve()
|
|
10269
|
+
|
|
10270
|
+
|
|
10235
10271
|
def resolve_archive_dir_for_change(
|
|
10236
10272
|
project_root: Path,
|
|
10237
10273
|
change_key: str,
|
|
@@ -10272,7 +10308,10 @@ def _republish_knowledge_entry(archive_dir: Path) -> dict[str, tuple[str, str, b
|
|
|
10272
10308
|
change_key=str(summary.get("changeName") or archive_dir.name),
|
|
10273
10309
|
archive_id=archive_dir.name,
|
|
10274
10310
|
producer_version=SCHEMA_VERSION,
|
|
10275
|
-
|
|
10311
|
+
# Derived from the archive, never wall-clock: `now_iso()` here made every
|
|
10312
|
+
# rebuild produce different bytes, so a package could never be compared
|
|
10313
|
+
# against a stored one and the "deterministic package" claim was false.
|
|
10314
|
+
created_at=_archive_created_at(summary_path),
|
|
10276
10315
|
)
|
|
10277
10316
|
if not candidates:
|
|
10278
10317
|
return {}
|
|
@@ -10285,12 +10324,28 @@ def _republish_knowledge_entry(archive_dir: Path) -> dict[str, tuple[str, str, b
|
|
|
10285
10324
|
}
|
|
10286
10325
|
|
|
10287
10326
|
|
|
10327
|
+
def read_durable_archive_receipt(project_root: Path, change_key: str) -> dict[str, Any] | None:
|
|
10328
|
+
"""The `<key>.remote.json` written after a confirmed durable upload, if any."""
|
|
10329
|
+
receipt_path = (
|
|
10330
|
+
project_root / ".harness" / "state" / "local" / "archive-packages"
|
|
10331
|
+
/ f"{change_key}.remote.json"
|
|
10332
|
+
)
|
|
10333
|
+
if not receipt_path.is_file():
|
|
10334
|
+
return None
|
|
10335
|
+
try:
|
|
10336
|
+
receipt = read_json(receipt_path)
|
|
10337
|
+
except (OSError, json.JSONDecodeError, TypeError, ValueError):
|
|
10338
|
+
return None
|
|
10339
|
+
return receipt if isinstance(receipt, dict) else None
|
|
10340
|
+
|
|
10341
|
+
|
|
10288
10342
|
def cmd_republish(
|
|
10289
10343
|
*,
|
|
10290
10344
|
change_key: str,
|
|
10291
10345
|
archive_dir: Path | None,
|
|
10292
10346
|
project_root: Path | None,
|
|
10293
10347
|
dry_run: bool,
|
|
10348
|
+
inject_knowledge: bool = True,
|
|
10294
10349
|
) -> tuple[int, dict[str, Any]]:
|
|
10295
10350
|
"""Rebuild and re-upload the core ZIP for an already sealed archive.
|
|
10296
10351
|
|
|
@@ -10311,7 +10366,7 @@ def cmd_republish(
|
|
|
10311
10366
|
payload["reasonCode"] = "ARCHIVE_CHANGE_KEY_INVALID"
|
|
10312
10367
|
return 2, payload
|
|
10313
10368
|
|
|
10314
|
-
root = (project_root
|
|
10369
|
+
root = resolve_republish_project_root(project_root, archive_dir)
|
|
10315
10370
|
payload["projectRoot"] = str(root)
|
|
10316
10371
|
matches: list[str] = []
|
|
10317
10372
|
if archive_dir is None:
|
|
@@ -10338,53 +10393,103 @@ def cmd_republish(
|
|
|
10338
10393
|
payload["reasonCode"] = "ARCHIVE_SUMMARY_MISSING"
|
|
10339
10394
|
return 1, payload
|
|
10340
10395
|
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
10396
|
+
extra_entries: dict[str, tuple[str, str, bytes]] = {}
|
|
10397
|
+
if inject_knowledge:
|
|
10398
|
+
try:
|
|
10399
|
+
extra_entries = _republish_knowledge_entry(archive_dir)
|
|
10400
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
10401
|
+
# Knowledge is additive: never fail a re-delivery over it.
|
|
10402
|
+
payload.setdefault("warnings", []).append(
|
|
10403
|
+
f"knowledge candidate regeneration skipped: {exc}"
|
|
10404
|
+
)
|
|
10405
|
+
extra_entries = {}
|
|
10349
10406
|
payload["knowledgeCandidatesInjected"] = "candidates/knowledge.json" in extra_entries
|
|
10350
10407
|
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10408
|
+
# Build once, into a preview path, so the bytes can be compared against any
|
|
10409
|
+
# durable receipt before an upload is attempted. The builder refuses to write
|
|
10410
|
+
# outside the project, so the preview lands beside the real packages under a
|
|
10411
|
+
# distinct name; it must never be mistaken for a pending upload.
|
|
10412
|
+
preview_path = (
|
|
10413
|
+
root / ".harness" / "state" / "local" / "archive-packages"
|
|
10414
|
+
/ f"{change_key}.preview.zip"
|
|
10415
|
+
)
|
|
10416
|
+
try:
|
|
10417
|
+
package = build_archive_package(
|
|
10418
|
+
root,
|
|
10419
|
+
archive_dir,
|
|
10420
|
+
change_key,
|
|
10421
|
+
output_path=preview_path,
|
|
10422
|
+
extra_entries=extra_entries,
|
|
10358
10423
|
)
|
|
10424
|
+
except (OSError, ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
10425
|
+
payload["error"] = f"archive package build failed: {exc}"
|
|
10426
|
+
payload["reasonCode"] = "ARCHIVE_PACKAGE_BUILD_FAILED"
|
|
10427
|
+
return 1, payload
|
|
10428
|
+
finally:
|
|
10359
10429
|
try:
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10430
|
+
preview_path.unlink(missing_ok=True)
|
|
10431
|
+
except OSError:
|
|
10432
|
+
pass
|
|
10433
|
+
|
|
10434
|
+
payload.update(
|
|
10435
|
+
{
|
|
10436
|
+
"fileCount": package["fileCount"],
|
|
10437
|
+
"sizeBytes": package["sizeBytes"],
|
|
10438
|
+
"knowledgeCandidateCount": package["knowledgeCandidateCount"],
|
|
10439
|
+
"packageSha256": package["packageSha256"],
|
|
10440
|
+
"manifestSha256": package["manifestSha256"],
|
|
10441
|
+
"files": package["paths"],
|
|
10442
|
+
}
|
|
10443
|
+
)
|
|
10444
|
+
|
|
10445
|
+
# The platform stores exactly one immutable package per change key. A rebuild
|
|
10446
|
+
# that differs by even one byte is refused server-side, so decide here rather
|
|
10447
|
+
# than spending an upload to learn it.
|
|
10448
|
+
durable = read_durable_archive_receipt(root, change_key)
|
|
10449
|
+
stored_sha = str((durable or {}).get("packageSha256") or "")
|
|
10450
|
+
if durable is not None and str(durable.get("archiveStatus") or "") == "durable":
|
|
10451
|
+
payload["remoteReceipt"] = {
|
|
10452
|
+
"archiveId": durable.get("archiveId"),
|
|
10453
|
+
"archiveStatus": durable.get("archiveStatus"),
|
|
10454
|
+
"knowledgeStatus": durable.get("knowledgeStatus"),
|
|
10455
|
+
"packageSha256": stored_sha,
|
|
10456
|
+
"recordedAt": durable.get("recordedAt"),
|
|
10457
|
+
}
|
|
10458
|
+
if stored_sha and stored_sha == package["packageSha256"]:
|
|
10459
|
+
payload.update({
|
|
10378
10460
|
"ok": True,
|
|
10379
|
-
"reasonCode": "
|
|
10380
|
-
"
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10461
|
+
"reasonCode": "ARCHIVE_ALREADY_PUBLISHED",
|
|
10462
|
+
"message": (
|
|
10463
|
+
"远端已存有字节一致的同一个包,无需重传"
|
|
10464
|
+
f"(archiveId={durable.get('archiveId')})。"
|
|
10465
|
+
),
|
|
10466
|
+
})
|
|
10467
|
+
return 0, payload
|
|
10468
|
+
payload.update({
|
|
10469
|
+
"ok": False,
|
|
10470
|
+
"reasonCode": "ARCHIVE_REMOTE_IMMUTABLE_CONFLICT",
|
|
10471
|
+
"error": (
|
|
10472
|
+
"该 change 在远端已是 durable 归档,服务端对同一 change key 只保存一个"
|
|
10473
|
+
"不可变包,不接受不同字节的替换。"
|
|
10474
|
+
),
|
|
10475
|
+
"nextAction": (
|
|
10476
|
+
"补传只适用于『从未成功上传』或『字节完全一致的重试』。"
|
|
10477
|
+
+ (
|
|
10478
|
+
"本次重建注入了 "
|
|
10479
|
+
f"{package['knowledgeCandidateCount']} 条知识候选"
|
|
10480
|
+
"(原包上传时还没有 candidates/knowledge.json),因此字节必然不同——"
|
|
10481
|
+
"已发布归档无法从客户端追加知识条目,需要平台侧提供重新索引或归档版本化能力。"
|
|
10482
|
+
"若只是想重试同一个包(例如知识索引失败),加 --no-knowledge-injection "
|
|
10483
|
+
"可重建与封存目录完全一致的字节。"
|
|
10484
|
+
if payload.get("knowledgeCandidatesInjected")
|
|
10485
|
+
else "本地归档内容与已上传的包不一致,请先确认哪一份才是应保留的事实。"
|
|
10486
|
+
)
|
|
10487
|
+
),
|
|
10488
|
+
})
|
|
10489
|
+
return 1, payload
|
|
10490
|
+
|
|
10491
|
+
if dry_run:
|
|
10492
|
+
payload.update({"ok": True, "reasonCode": "ARCHIVE_REPUBLISH_PREVIEW"})
|
|
10388
10493
|
return 0, payload
|
|
10389
10494
|
|
|
10390
10495
|
result = auto_push_archive_core(
|
|
@@ -10407,6 +10512,7 @@ def cmd_republish_cli(args: argparse.Namespace) -> int:
|
|
|
10407
10512
|
archive_dir=resolve_path(args.archive_dir) if args.archive_dir else None,
|
|
10408
10513
|
project_root=resolve_path(args.project) if args.project else None,
|
|
10409
10514
|
dry_run=bool(args.dry_run),
|
|
10515
|
+
inject_knowledge=not bool(args.no_knowledge_injection),
|
|
10410
10516
|
)
|
|
10411
10517
|
emit_json(payload)
|
|
10412
10518
|
return code
|
|
@@ -10590,6 +10696,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
10590
10696
|
action="store_true",
|
|
10591
10697
|
help="build the package and report its contents without uploading",
|
|
10592
10698
|
)
|
|
10699
|
+
p_republish.add_argument(
|
|
10700
|
+
"--no-knowledge-injection",
|
|
10701
|
+
action="store_true",
|
|
10702
|
+
help=(
|
|
10703
|
+
"rebuild exactly what the sealed archive holds; needed to retry the "
|
|
10704
|
+
"same bytes the server already stored"
|
|
10705
|
+
),
|
|
10706
|
+
)
|
|
10593
10707
|
p_republish.add_argument("--json", action="store_true", default=True)
|
|
10594
10708
|
p_republish.set_defaults(func=cmd_republish_cli)
|
|
10595
10709
|
|
|
@@ -1165,9 +1165,11 @@ def close_transition(
|
|
|
1165
1165
|
"holder": lease.get("owner"),
|
|
1166
1166
|
"leasePhase": lease.get("phase"),
|
|
1167
1167
|
}
|
|
1168
|
+
# Owner and phase already matched above, so nobody took this lease over: a
|
|
1169
|
+
# takeover rewrites the file and would have produced CONTEXT_LEASE_MISMATCH.
|
|
1170
|
+
# A lapsed deadline therefore proves only that the phase outlived the TTL.
|
|
1168
1171
|
expiry = _parse_time(lease.get("expiresAt"))
|
|
1169
|
-
|
|
1170
|
-
return {"ok": False, "code": "CONTEXT_LEASE_EXPIRED"}
|
|
1172
|
+
lease_lapsed = expiry is None or _now() >= expiry
|
|
1171
1173
|
|
|
1172
1174
|
previous_hash = transitions[-1].get("receiptHash") if transitions else None
|
|
1173
1175
|
attempt = (
|
|
@@ -1192,6 +1194,12 @@ def close_transition(
|
|
|
1192
1194
|
"closedAt": _now().isoformat(),
|
|
1193
1195
|
"previousReceiptHash": previous_hash,
|
|
1194
1196
|
}
|
|
1197
|
+
if lease_lapsed:
|
|
1198
|
+
# Recorded, not hidden: the phase ran past its lease without a renewal.
|
|
1199
|
+
receipt["leaseLapsed"] = {
|
|
1200
|
+
"expiresAt": lease.get("expiresAt"),
|
|
1201
|
+
"acquiredAt": lease.get("acquiredAt"),
|
|
1202
|
+
}
|
|
1195
1203
|
receipt["receiptHash"] = _payload_hash(receipt)
|
|
1196
1204
|
_append_ndjson(paths["transitions"], receipt)
|
|
1197
1205
|
paths["lease"].unlink(missing_ok=True)
|
|
@@ -1267,7 +1275,11 @@ def _begin_transition_unlocked(
|
|
|
1267
1275
|
"legacyBootstrap": True,
|
|
1268
1276
|
"message": (
|
|
1269
1277
|
"没有交接凭证。v2 计划应有 committed 的 meta/publication-journals/*.json "
|
|
1270
|
-
"作为补录证据;legacy 计划需先完成 plan 阶段的 close"
|
|
1278
|
+
"作为补录证据;legacy 计划需先完成 plan 阶段的 close。"
|
|
1279
|
+
"首次进入某阶段(还没有任何 transition 凭证)时不要用 context begin——"
|
|
1280
|
+
"改跑 `harness_gate.py begin --change-dir .harness/changes/<change> "
|
|
1281
|
+
"--phase <phase> --run-id <phase>_<uuid4hex> --note \"<触发指令>\"`,"
|
|
1282
|
+
"它会领取租约并写好 phase.start。"
|
|
1271
1283
|
),
|
|
1272
1284
|
}
|
|
1273
1285
|
receipt = transitions[-1]
|
|
@@ -1458,6 +1470,53 @@ def cancel_prepared_context(
|
|
|
1458
1470
|
}
|
|
1459
1471
|
|
|
1460
1472
|
|
|
1473
|
+
def renew_lease(
|
|
1474
|
+
project: Path,
|
|
1475
|
+
change: str,
|
|
1476
|
+
*,
|
|
1477
|
+
executor: str,
|
|
1478
|
+
ttl_seconds: int = 3600,
|
|
1479
|
+
) -> dict[str, Any]:
|
|
1480
|
+
"""Extend the current phase lease in place.
|
|
1481
|
+
|
|
1482
|
+
Only the recorded owner may renew, and only while no other executor has
|
|
1483
|
+
taken the lease over. Renewal touches nothing but ``expiresAt``, so it is
|
|
1484
|
+
safe to call on a heartbeat during a long phase.
|
|
1485
|
+
"""
|
|
1486
|
+
project = project.resolve()
|
|
1487
|
+
try:
|
|
1488
|
+
_, _, state_root = _contract(project, change)
|
|
1489
|
+
except ValueError as exc:
|
|
1490
|
+
return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
|
|
1491
|
+
paths = _paths(state_root)
|
|
1492
|
+
if not paths["lease"].is_file():
|
|
1493
|
+
return {"ok": False, "code": "CONTEXT_LEASE_REQUIRED"}
|
|
1494
|
+
with _exclusive_state_lock(paths["runtime"] / "branch-selection.lock"):
|
|
1495
|
+
try:
|
|
1496
|
+
lease = _read_json(paths["lease"])
|
|
1497
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
1498
|
+
return {"ok": False, "code": "CONTEXT_LEASE_INVALID"}
|
|
1499
|
+
if lease.get("owner") != executor:
|
|
1500
|
+
return {
|
|
1501
|
+
"ok": False,
|
|
1502
|
+
"code": "CONTEXT_LEASE_MISMATCH",
|
|
1503
|
+
"holder": lease.get("owner"),
|
|
1504
|
+
}
|
|
1505
|
+
expiry = _parse_time(lease.get("expiresAt"))
|
|
1506
|
+
lapsed = expiry is None or _now() >= expiry
|
|
1507
|
+
lease["expiresAt"] = (
|
|
1508
|
+
_now() + dt.timedelta(seconds=max(1, int(ttl_seconds)))
|
|
1509
|
+
).isoformat()
|
|
1510
|
+
lease["renewedAt"] = _now().isoformat()
|
|
1511
|
+
_write_json_atomic(paths["lease"], lease)
|
|
1512
|
+
return {
|
|
1513
|
+
"ok": True,
|
|
1514
|
+
"code": "CONTEXT_LEASE_RENEWED",
|
|
1515
|
+
"lease": lease,
|
|
1516
|
+
"wasLapsed": lapsed,
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
|
|
1461
1520
|
def context_view(project: Path, change: str) -> dict[str, Any]:
|
|
1462
1521
|
project = Path(project).resolve()
|
|
1463
1522
|
try:
|
|
@@ -1536,6 +1595,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1536
1595
|
begin.add_argument("--change", required=True)
|
|
1537
1596
|
begin.add_argument("--phase", required=True)
|
|
1538
1597
|
begin.add_argument("--executor", required=True)
|
|
1598
|
+
renew = sub.add_parser(
|
|
1599
|
+
"renew", help="extend the current phase lease without touching context"
|
|
1600
|
+
)
|
|
1601
|
+
renew.add_argument("--json", action="store_true")
|
|
1602
|
+
renew.add_argument("--project", required=True, type=Path)
|
|
1603
|
+
renew.add_argument("--change", required=True)
|
|
1604
|
+
renew.add_argument("--executor", required=True)
|
|
1605
|
+
renew.add_argument("--ttl-seconds", type=int, default=3600)
|
|
1539
1606
|
configure = sub.add_parser("configure-plan")
|
|
1540
1607
|
configure.add_argument("--json", action="store_true")
|
|
1541
1608
|
configure.add_argument("--project", required=True, type=Path)
|
|
@@ -1588,6 +1655,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1588
1655
|
phase=args.phase,
|
|
1589
1656
|
executor=args.executor,
|
|
1590
1657
|
)
|
|
1658
|
+
elif args.command == "renew":
|
|
1659
|
+
result = renew_lease(
|
|
1660
|
+
args.project,
|
|
1661
|
+
args.change,
|
|
1662
|
+
executor=args.executor,
|
|
1663
|
+
ttl_seconds=args.ttl_seconds,
|
|
1664
|
+
)
|
|
1591
1665
|
elif args.command == "configure-plan":
|
|
1592
1666
|
result = configure_phase_plan(
|
|
1593
1667
|
args.project,
|
|
@@ -1904,6 +1904,7 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1904
1904
|
file=sys.stderr,
|
|
1905
1905
|
)
|
|
1906
1906
|
phase_closed = False
|
|
1907
|
+
phase_start_duplicate = False
|
|
1907
1908
|
projection_error: str | None = None
|
|
1908
1909
|
attempt_error: str | None = None
|
|
1909
1910
|
auto_sealed_events: list[dict[str, Any]] = []
|
|
@@ -1951,10 +1952,25 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1951
1952
|
except ValueError as exc:
|
|
1952
1953
|
projection_error = str(exc)
|
|
1953
1954
|
elif args.type == "phase.start":
|
|
1955
|
+
# Re-appending the same (phase, run_id) is the caller restating a
|
|
1956
|
+
# start that already exists — `gate begin` writes one, and the
|
|
1957
|
+
# logging protocol used to ask for a second. Treat it as a no-op
|
|
1958
|
+
# rather than a duplicate; two identical phase.start events make
|
|
1959
|
+
# `plan finalize` unrecoverable via PHASE_START_DUPLICATE.
|
|
1960
|
+
if run_id is not None and any(
|
|
1961
|
+
item.get("phase") == args.phase
|
|
1962
|
+
and item.get("type") == "phase.start"
|
|
1963
|
+
and item.get("run_id") == run_id
|
|
1964
|
+
for item in existing_events
|
|
1965
|
+
):
|
|
1966
|
+
phase_start_duplicate = True
|
|
1954
1967
|
# HH-WF-20260730-001: a new phase.start must not be appended
|
|
1955
1968
|
# while a prior attempt for the same phase is still open —
|
|
1956
1969
|
# auto-seal it first so timing never has two open attempts.
|
|
1957
|
-
open_attempts =
|
|
1970
|
+
open_attempts = (
|
|
1971
|
+
[] if phase_start_duplicate
|
|
1972
|
+
else open_attempts_for_phase(existing_events, args.phase)
|
|
1973
|
+
)
|
|
1958
1974
|
if open_attempts:
|
|
1959
1975
|
inferred_reason = infer_auto_seal_reason(
|
|
1960
1976
|
open_attempts[-1].get("events") or []
|
|
@@ -1964,7 +1980,12 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1964
1980
|
phase=args.phase,
|
|
1965
1981
|
seal_reason=inferred_reason,
|
|
1966
1982
|
)
|
|
1967
|
-
if
|
|
1983
|
+
if (
|
|
1984
|
+
not phase_closed
|
|
1985
|
+
and not phase_start_duplicate
|
|
1986
|
+
and projection_error is None
|
|
1987
|
+
and attempt_error is None
|
|
1988
|
+
):
|
|
1968
1989
|
for seal_event in auto_sealed_events:
|
|
1969
1990
|
atomic_append_line(
|
|
1970
1991
|
path,
|
|
@@ -1976,6 +1997,18 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1976
1997
|
)
|
|
1977
1998
|
except (OSError, TimeoutError, ValueError) as exc:
|
|
1978
1999
|
return emit_error(f"append failed: {exc}", as_json=as_json)
|
|
2000
|
+
if phase_start_duplicate:
|
|
2001
|
+
if as_json:
|
|
2002
|
+
print(json.dumps({
|
|
2003
|
+
"ok": True,
|
|
2004
|
+
"skipped": True,
|
|
2005
|
+
"reason": "phase-start-already-recorded",
|
|
2006
|
+
"phase": args.phase,
|
|
2007
|
+
"run_id": event.get("run_id"),
|
|
2008
|
+
}, ensure_ascii=False))
|
|
2009
|
+
else:
|
|
2010
|
+
print("ok (phase.start already recorded for this run id)")
|
|
2011
|
+
return 0
|
|
1979
2012
|
if phase_closed:
|
|
1980
2013
|
return emit_error(
|
|
1981
2014
|
"PHASE_ALREADY_CLOSED: refusing a second phase.end for the same "
|
|
@@ -735,7 +735,12 @@ def _validate_plan_start(
|
|
|
735
735
|
if len(matching) > 1:
|
|
736
736
|
return _result_error(
|
|
737
737
|
"PHASE_START_DUPLICATE",
|
|
738
|
-
f"found {len(matching)} matching plan phase.start events"
|
|
738
|
+
f"found {len(matching)} matching plan phase.start events for run id "
|
|
739
|
+
f"{run_id}. `harness_gate.py begin` already appends phase.start; a "
|
|
740
|
+
"second manual append of the same run id creates this state. Recover "
|
|
741
|
+
"with a fresh run id and the next attempt number (attempts are global "
|
|
742
|
+
"per phase, not per run id): append phase.start with both, then "
|
|
743
|
+
"finalize with the same pair.",
|
|
739
744
|
)
|
|
740
745
|
return {"ok": True, "phaseStartCount": 1}
|
|
741
746
|
|
|
@@ -3,7 +3,7 @@ name: harness-archive
|
|
|
3
3
|
description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到 .harness/archive/,含归档元数据。仅当用户显式调用
|
|
4
4
|
/harness-archive 时使用;不得被其他阶段 skill 自动接续触发。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=3132d081a7790973; overlay=none; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-archive — 变更归档
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -144,10 +144,21 @@ python <skills-root>/scripts/harness_archive.py republish --change <change-key>
|
|
|
144
144
|
python <skills-root>/scripts/harness_archive.py republish --change <change-key> --json # 重建并上传
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
脚本在 `<skills-root>/scripts/` 共享,**不在 `harness-archive/scripts/`**(见 read-protocol 第 0 条)。
|
|
148
|
+
|
|
149
|
+
在项目根内任意目录直接跑即可,**不需要** `--project`。
|
|
150
|
+
|
|
147
151
|
归档目录是封存的(after-manifest 覆盖其每个字节),因此 `republish` **不写回**归档目录:
|
|
148
152
|
缺失的 `candidates/knowledge.json` 只在内存中按已归档 summary 生成并放进包里。平台的知识
|
|
149
153
|
条目只来自这个包;`--scope all` 走的是 branch_file 通道,不进知识管道。
|
|
150
154
|
|
|
155
|
+
**服务端一个 change key 只存一个不可变包**,所以补传只适用于「从未成功上传」与「字节完全一致的
|
|
156
|
+
重试」。已 durable 的 change 会在本地就判出结果:字节一致 → `ARCHIVE_ALREADY_PUBLISHED`
|
|
157
|
+
(exit 0,不重传);字节不同 → `ARCHIVE_REMOTE_IMMUTABLE_CONFLICT`(exit 1,**不发起上传**)。
|
|
158
|
+
**已发布归档无法从客户端补知识条目**——注入候选必然改变字节,服务端拒绝替换;要补需要平台侧
|
|
159
|
+
提供重新索引或归档版本化能力。
|
|
160
|
+
只想重试**同一个包**(例如知识索引失败)时加 `--no-knowledge-injection`,它按封存目录原样重建,字节与已存包一致。
|
|
161
|
+
|
|
151
162
|
### 三、文件移动只用内置工具或 PowerShell
|
|
152
163
|
|
|
153
164
|
移动用 Read+Write+验证 或 PowerShell;**禁止 Bash mv/cp/rm**。移动失败时不删除原目录,报错退出让用户手动处理。
|
|
@@ -247,13 +258,23 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 su
|
|
|
247
258
|
|
|
248
259
|
`events.ndjson` 为唯一事实源(schema_version 3,兼容读取 v1/v2;`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
|
|
249
260
|
|
|
261
|
+
**`phase.start` 由 `harness_gate.py begin` 写,不要再手工追加一次。** 两条同 `run-id` 的
|
|
262
|
+
`phase.start` 会让 `plan finalize` 以 `PHASE_START_DUPLICATE` 卡死,而且手工那次会先触发
|
|
263
|
+
auto-seal、把正在开始的 attempt 封成 `RECOVERED`。要补触发指令说明就带 `--note` 跑 `gate begin`。
|
|
264
|
+
(重复追加现已按 `(phase, run-id)` 判为幂等 no-op,但依赖它不如不写。)
|
|
265
|
+
|
|
250
266
|
```powershell
|
|
251
|
-
|
|
267
|
+
# 阶段开始:gate begin 负责,note 在这里给
|
|
268
|
+
python <skills-root>/scripts/harness_gate.py begin --change-dir ".harness/changes/<change-name>" --phase <phase> --note "<触发指令>"
|
|
269
|
+
# 阶段中的其他事件才用 append
|
|
270
|
+
python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type <command|issue|verification> --run-id <phase-run-id> --note "<摘要>"
|
|
252
271
|
```
|
|
253
272
|
|
|
254
273
|
> **脚本接线**:`harness_events.py append`;`harness_archive.py finalize`;`harness_preflight.py check`;`harness_ledger.py can-reuse`;`harness_service.py ensure/stop`(须 `--files`/`serviceStart.inputFiles`)。JSON 输出按 D13 护栏解读。
|
|
255
274
|
|
|
256
275
|
> **Task 4 §6.1 写入契约**:普通 `append` = 加锁 -> 追加一行 -> fsync -> 解锁,**不 load 历史、不渲染**(O(1),跨进程锁 `events.ndjson.lock`,UUID 用完整 `uuid4().hex` 无需去重扫描)。仅 `--type phase.end` append 在追加成功后渲染一次 `execution-log.md`;显式 `harness_events.py render` 随时从完整 events 重建;`harness_archive.py finalize` 在 collect 前强制 render 一次。高频 command append 期间 log 可能滞后,phase 边界保持最新。
|
|
257
276
|
|
|
258
|
-
每个阶段的 `phase.start` 与对应 `phase.end` 必须复用同一 `--run-id` / `--attempt`;阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`。重试同一阶段时生成新的 run-id 并增加 `--attempt <n
|
|
277
|
+
每个阶段的 `phase.start` 与对应 `phase.end` 必须复用同一 `--run-id` / `--attempt`;阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`。重试同一阶段时生成新的 run-id 并增加 `--attempt <n>`,不得覆盖或伪装成一次执行。**`attempt` 按 phase 全局递增,不是按 run-id**:一个 run-id 只绑定一个 attempt,重试必须「新 run-id + 下一个 attempt」两者同时换,只换其一会撞 `EVENT_ATTEMPT_CONFLICT` 或 `PHASE_ALREADY_CLOSED`。已发布的 plan 用 `harness_plan_finalize.py republish` 自动分配两者。
|
|
278
|
+
|
|
279
|
+
阶段跑得久(plan/run 常见)时用 `harness_context.py renew --project . --change <cn> --executor <tool>` 续租;租约到期本身不再阻断 `close`(同一 owner 的过期租约不构成冲突,收据里记 `leaseLapsed`),但续租能让 `view` 的状态如实反映在跑。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
|
|
259
280
|
> 片段:logging · phase=`archive`
|
|
@@ -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=3132d081a7790973; overlay=none; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-codebase-map — 代码库地图
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -94,13 +94,23 @@ description: 分析现有代码库并生成 .harness/codebase/map/
|
|
|
94
94
|
|
|
95
95
|
`events.ndjson` 为唯一事实源(schema_version 3,兼容读取 v1/v2;`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
|
|
96
96
|
|
|
97
|
+
**`phase.start` 由 `harness_gate.py begin` 写,不要再手工追加一次。** 两条同 `run-id` 的
|
|
98
|
+
`phase.start` 会让 `plan finalize` 以 `PHASE_START_DUPLICATE` 卡死,而且手工那次会先触发
|
|
99
|
+
auto-seal、把正在开始的 attempt 封成 `RECOVERED`。要补触发指令说明就带 `--note` 跑 `gate begin`。
|
|
100
|
+
(重复追加现已按 `(phase, run-id)` 判为幂等 no-op,但依赖它不如不写。)
|
|
101
|
+
|
|
97
102
|
```powershell
|
|
98
|
-
|
|
103
|
+
# 阶段开始:gate begin 负责,note 在这里给
|
|
104
|
+
python <skills-root>/scripts/harness_gate.py begin --change-dir ".harness/changes/<change-name>" --phase <phase> --note "<触发指令>"
|
|
105
|
+
# 阶段中的其他事件才用 append
|
|
106
|
+
python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type <command|issue|verification> --run-id <phase-run-id> --note "<摘要>"
|
|
99
107
|
```
|
|
100
108
|
|
|
101
109
|
> **脚本接线**:`harness_events.py append`;`harness_archive.py finalize`;`harness_preflight.py check`;`harness_ledger.py can-reuse`;`harness_service.py ensure/stop`(须 `--files`/`serviceStart.inputFiles`)。JSON 输出按 D13 护栏解读。
|
|
102
110
|
|
|
103
111
|
> **Task 4 §6.1 写入契约**:普通 `append` = 加锁 -> 追加一行 -> fsync -> 解锁,**不 load 历史、不渲染**(O(1),跨进程锁 `events.ndjson.lock`,UUID 用完整 `uuid4().hex` 无需去重扫描)。仅 `--type phase.end` append 在追加成功后渲染一次 `execution-log.md`;显式 `harness_events.py render` 随时从完整 events 重建;`harness_archive.py finalize` 在 collect 前强制 render 一次。高频 command append 期间 log 可能滞后,phase 边界保持最新。
|
|
104
112
|
|
|
105
|
-
每个阶段的 `phase.start` 与对应 `phase.end` 必须复用同一 `--run-id` / `--attempt`;阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`。重试同一阶段时生成新的 run-id 并增加 `--attempt <n
|
|
113
|
+
每个阶段的 `phase.start` 与对应 `phase.end` 必须复用同一 `--run-id` / `--attempt`;阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`。重试同一阶段时生成新的 run-id 并增加 `--attempt <n>`,不得覆盖或伪装成一次执行。**`attempt` 按 phase 全局递增,不是按 run-id**:一个 run-id 只绑定一个 attempt,重试必须「新 run-id + 下一个 attempt」两者同时换,只换其一会撞 `EVENT_ATTEMPT_CONFLICT` 或 `PHASE_ALREADY_CLOSED`。已发布的 plan 用 `harness_plan_finalize.py republish` 自动分配两者。
|
|
114
|
+
|
|
115
|
+
阶段跑得久(plan/run 常见)时用 `harness_context.py renew --project . --change <cn> --executor <tool>` 续租;租约到期本身不再阻断 `close`(同一 owner 的过期租约不构成冲突,收据里记 `leaseLapsed`),但续租能让 `view` 的状态如实反映在跑。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
|
|
106
116
|
> 片段:logging · phase=`codebase-map`;默认写 reports,有未归档变更时另写 change events
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: harness-knowledge-ingest
|
|
3
3
|
description: 确认归档 ZIP 已上传并由 Hunter Platform 在服务端解包、校验和入库。客户端不再构建或维护本地知识索引。
|
|
4
4
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
5
|
+
<!-- generated by harness_deploy.py; core=3132d081a7790973; overlay=none; agent=codebuddy; do not edit -->
|
|
6
6
|
# harness-knowledge-ingest
|
|
7
7
|
|
|
8
8
|
知识 ingest 完全由 Hunter Platform 负责。归档完成时,客户端生成一个确定性 ZIP;
|
|
@@ -81,13 +81,23 @@ ZIP 已按收据策略清理,且平台下载接口可以恢复原包。
|
|
|
81
81
|
|
|
82
82
|
`events.ndjson` 为唯一事实源(schema_version 3,兼容读取 v1/v2;`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
|
|
83
83
|
|
|
84
|
+
**`phase.start` 由 `harness_gate.py begin` 写,不要再手工追加一次。** 两条同 `run-id` 的
|
|
85
|
+
`phase.start` 会让 `plan finalize` 以 `PHASE_START_DUPLICATE` 卡死,而且手工那次会先触发
|
|
86
|
+
auto-seal、把正在开始的 attempt 封成 `RECOVERED`。要补触发指令说明就带 `--note` 跑 `gate begin`。
|
|
87
|
+
(重复追加现已按 `(phase, run-id)` 判为幂等 no-op,但依赖它不如不写。)
|
|
88
|
+
|
|
84
89
|
```powershell
|
|
85
|
-
|
|
90
|
+
# 阶段开始:gate begin 负责,note 在这里给
|
|
91
|
+
python <skills-root>/scripts/harness_gate.py begin --change-dir ".harness/changes/<change-name>" --phase <phase> --note "<触发指令>"
|
|
92
|
+
# 阶段中的其他事件才用 append
|
|
93
|
+
python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type <command|issue|verification> --run-id <phase-run-id> --note "<摘要>"
|
|
86
94
|
```
|
|
87
95
|
|
|
88
96
|
> **脚本接线**:`harness_events.py append`;`harness_archive.py finalize`;`harness_preflight.py check`;`harness_ledger.py can-reuse`;`harness_service.py ensure/stop`(须 `--files`/`serviceStart.inputFiles`)。JSON 输出按 D13 护栏解读。
|
|
89
97
|
|
|
90
98
|
> **Task 4 §6.1 写入契约**:普通 `append` = 加锁 -> 追加一行 -> fsync -> 解锁,**不 load 历史、不渲染**(O(1),跨进程锁 `events.ndjson.lock`,UUID 用完整 `uuid4().hex` 无需去重扫描)。仅 `--type phase.end` append 在追加成功后渲染一次 `execution-log.md`;显式 `harness_events.py render` 随时从完整 events 重建;`harness_archive.py finalize` 在 collect 前强制 render 一次。高频 command append 期间 log 可能滞后,phase 边界保持最新。
|
|
91
99
|
|
|
92
|
-
每个阶段的 `phase.start` 与对应 `phase.end` 必须复用同一 `--run-id` / `--attempt`;阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`。重试同一阶段时生成新的 run-id 并增加 `--attempt <n
|
|
100
|
+
每个阶段的 `phase.start` 与对应 `phase.end` 必须复用同一 `--run-id` / `--attempt`;阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`。重试同一阶段时生成新的 run-id 并增加 `--attempt <n>`,不得覆盖或伪装成一次执行。**`attempt` 按 phase 全局递增,不是按 run-id**:一个 run-id 只绑定一个 attempt,重试必须「新 run-id + 下一个 attempt」两者同时换,只换其一会撞 `EVENT_ATTEMPT_CONFLICT` 或 `PHASE_ALREADY_CLOSED`。已发布的 plan 用 `harness_plan_finalize.py republish` 自动分配两者。
|
|
101
|
+
|
|
102
|
+
阶段跑得久(plan/run 常见)时用 `harness_context.py renew --project . --change <cn> --executor <tool>` 续租;租约到期本身不再阻断 `close`(同一 owner 的过期租约不构成冲突,收据里记 `leaseLapsed`),但续租能让 `view` 的状态如实反映在跑。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
|
|
93
103
|
> 片段:logging · phase=`knowledge-ingest` · 记录 package hash、服务端状态与失败重试路径
|