@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.
Files changed (154) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +24 -3
  3. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +13 -3
  4. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +13 -3
  5. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +13 -3
  6. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +13 -3
  7. package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-push/SKILL.md +15 -1
  9. package/harness/bundles/general/claude-code/harness-review/SKILL.md +13 -3
  10. package/harness/bundles/general/claude-code/harness-run/SKILL.md +13 -3
  11. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +13 -3
  12. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-test/SKILL.md +13 -3
  14. package/harness/bundles/general/claude-code/scripts/harness_archive.py +158 -44
  15. package/harness/bundles/general/claude-code/scripts/harness_context.py +77 -3
  16. package/harness/bundles/general/claude-code/scripts/harness_events.py +35 -2
  17. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +6 -1
  18. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  19. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +24 -3
  20. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +13 -3
  21. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +13 -3
  22. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +13 -3
  23. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +13 -3
  24. package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-push/SKILL.md +15 -1
  26. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +13 -3
  27. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +13 -3
  28. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +13 -3
  29. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +13 -3
  31. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +158 -44
  32. package/harness/bundles/general/codebuddy/scripts/harness_context.py +77 -3
  33. package/harness/bundles/general/codebuddy/scripts/harness_events.py +35 -2
  34. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +6 -1
  35. package/harness/bundles/general/codex/.harness-build.json +1 -1
  36. package/harness/bundles/general/codex/harness-archive/SKILL.md +24 -3
  37. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +13 -3
  38. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +13 -3
  39. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +13 -3
  40. package/harness/bundles/general/codex/harness-plan/SKILL.md +13 -3
  41. package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
  42. package/harness/bundles/general/codex/harness-push/SKILL.md +15 -1
  43. package/harness/bundles/general/codex/harness-review/SKILL.md +13 -3
  44. package/harness/bundles/general/codex/harness-run/SKILL.md +13 -3
  45. package/harness/bundles/general/codex/harness-submit/SKILL.md +13 -3
  46. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-test/SKILL.md +13 -3
  48. package/harness/bundles/general/codex/scripts/harness_archive.py +158 -44
  49. package/harness/bundles/general/codex/scripts/harness_context.py +77 -3
  50. package/harness/bundles/general/codex/scripts/harness_events.py +35 -2
  51. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +6 -1
  52. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  53. package/harness/bundles/general/cursor/harness-archive/SKILL.md +24 -3
  54. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +13 -3
  55. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +13 -3
  56. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +13 -3
  57. package/harness/bundles/general/cursor/harness-plan/SKILL.md +13 -3
  58. package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
  59. package/harness/bundles/general/cursor/harness-push/SKILL.md +15 -1
  60. package/harness/bundles/general/cursor/harness-review/SKILL.md +13 -3
  61. package/harness/bundles/general/cursor/harness-run/SKILL.md +13 -3
  62. package/harness/bundles/general/cursor/harness-submit/SKILL.md +13 -3
  63. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  64. package/harness/bundles/general/cursor/harness-test/SKILL.md +13 -3
  65. package/harness/bundles/general/cursor/scripts/harness_archive.py +158 -44
  66. package/harness/bundles/general/cursor/scripts/harness_context.py +77 -3
  67. package/harness/bundles/general/cursor/scripts/harness_events.py +35 -2
  68. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +6 -1
  69. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  70. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +13 -3
  71. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +24 -3
  72. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +13 -3
  73. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +13 -3
  74. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +13 -3
  75. package/harness/bundles/java/claude-code/harness-package/SKILL.md +13 -3
  76. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +13 -3
  77. package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
  78. package/harness/bundles/java/claude-code/harness-push/SKILL.md +15 -1
  79. package/harness/bundles/java/claude-code/harness-review/SKILL.md +13 -3
  80. package/harness/bundles/java/claude-code/harness-run/SKILL.md +13 -3
  81. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +13 -3
  82. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  83. package/harness/bundles/java/claude-code/harness-test/SKILL.md +13 -3
  84. package/harness/bundles/java/claude-code/scripts/harness_archive.py +158 -44
  85. package/harness/bundles/java/claude-code/scripts/harness_context.py +77 -3
  86. package/harness/bundles/java/claude-code/scripts/harness_events.py +35 -2
  87. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +6 -1
  88. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  89. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +13 -3
  90. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +24 -3
  91. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +13 -3
  92. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +13 -3
  93. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +13 -3
  94. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +13 -3
  95. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +13 -3
  96. package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
  97. package/harness/bundles/java/codebuddy/harness-push/SKILL.md +15 -1
  98. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +13 -3
  99. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +13 -3
  100. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +13 -3
  101. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  102. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +13 -3
  103. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +158 -44
  104. package/harness/bundles/java/codebuddy/scripts/harness_context.py +77 -3
  105. package/harness/bundles/java/codebuddy/scripts/harness_events.py +35 -2
  106. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +6 -1
  107. package/harness/bundles/java/codex/.harness-build.json +1 -1
  108. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +13 -3
  109. package/harness/bundles/java/codex/harness-archive/SKILL.md +24 -3
  110. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +13 -3
  111. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +13 -3
  112. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +13 -3
  113. package/harness/bundles/java/codex/harness-package/SKILL.md +13 -3
  114. package/harness/bundles/java/codex/harness-plan/SKILL.md +13 -3
  115. package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
  116. package/harness/bundles/java/codex/harness-push/SKILL.md +15 -1
  117. package/harness/bundles/java/codex/harness-review/SKILL.md +13 -3
  118. package/harness/bundles/java/codex/harness-run/SKILL.md +13 -3
  119. package/harness/bundles/java/codex/harness-submit/SKILL.md +13 -3
  120. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  121. package/harness/bundles/java/codex/harness-test/SKILL.md +13 -3
  122. package/harness/bundles/java/codex/scripts/harness_archive.py +158 -44
  123. package/harness/bundles/java/codex/scripts/harness_context.py +77 -3
  124. package/harness/bundles/java/codex/scripts/harness_events.py +35 -2
  125. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +6 -1
  126. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  127. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +13 -3
  128. package/harness/bundles/java/cursor/harness-archive/SKILL.md +24 -3
  129. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +13 -3
  130. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +13 -3
  131. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +13 -3
  132. package/harness/bundles/java/cursor/harness-package/SKILL.md +13 -3
  133. package/harness/bundles/java/cursor/harness-plan/SKILL.md +13 -3
  134. package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
  135. package/harness/bundles/java/cursor/harness-push/SKILL.md +15 -1
  136. package/harness/bundles/java/cursor/harness-review/SKILL.md +13 -3
  137. package/harness/bundles/java/cursor/harness-run/SKILL.md +13 -3
  138. package/harness/bundles/java/cursor/harness-submit/SKILL.md +13 -3
  139. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  140. package/harness/bundles/java/cursor/harness-test/SKILL.md +13 -3
  141. package/harness/bundles/java/cursor/scripts/harness_archive.py +158 -44
  142. package/harness/bundles/java/cursor/scripts/harness_context.py +77 -3
  143. package/harness/bundles/java/cursor/scripts/harness_events.py +35 -2
  144. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +6 -1
  145. package/harness/manifests/general/claude-code.json +19 -19
  146. package/harness/manifests/general/codebuddy.json +19 -19
  147. package/harness/manifests/general/codex.json +19 -19
  148. package/harness/manifests/general/cursor.json +19 -19
  149. package/harness/manifests/java/claude-code.json +21 -21
  150. package/harness/manifests/java/codebuddy.json +21 -21
  151. package/harness/manifests/java/codex.json +21 -21
  152. package/harness/manifests/java/cursor.json +21 -21
  153. package/hunter-workflow-family.json +5 -5
  154. 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
- created_at=now_iso(),
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 or find_project_root(archive_dir or Path.cwd())).resolve()
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
- try:
10342
- extra_entries = _republish_knowledge_entry(archive_dir)
10343
- except (OSError, ValueError, json.JSONDecodeError) as exc:
10344
- # Knowledge is additive: never fail a re-delivery over it.
10345
- payload.setdefault("warnings", []).append(
10346
- f"knowledge candidate regeneration skipped: {exc}"
10347
- )
10348
- extra_entries = {}
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
- if dry_run:
10352
- # The package builder refuses to write outside the project, so the
10353
- # preview lands beside the real packages under a distinct name and is
10354
- # removed again; it must never be mistaken for a pending upload.
10355
- preview_path = (
10356
- root / ".harness" / "state" / "local" / "archive-packages"
10357
- / f"{change_key}.preview.zip"
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
- package = build_archive_package(
10361
- root,
10362
- archive_dir,
10363
- change_key,
10364
- output_path=preview_path,
10365
- extra_entries=extra_entries,
10366
- )
10367
- except (OSError, ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc:
10368
- payload["error"] = f"archive package build failed: {exc}"
10369
- payload["reasonCode"] = "ARCHIVE_PACKAGE_BUILD_FAILED"
10370
- return 1, payload
10371
- finally:
10372
- try:
10373
- preview_path.unlink(missing_ok=True)
10374
- except OSError:
10375
- pass
10376
- payload.update(
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": "ARCHIVE_REPUBLISH_PREVIEW",
10380
- "fileCount": package["fileCount"],
10381
- "sizeBytes": package["sizeBytes"],
10382
- "knowledgeCandidateCount": package["knowledgeCandidateCount"],
10383
- "packageSha256": package["packageSha256"],
10384
- "manifestSha256": package["manifestSha256"],
10385
- "files": package["paths"],
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
- if expiry is None or _now() >= expiry:
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 = open_attempts_for_phase(existing_events, args.phase)
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 not phase_closed and projection_error is None and attempt_error is None:
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
 
@@ -2,9 +2,9 @@
2
2
  "schema_version": 2,
3
3
  "profile": "general",
4
4
  "adapter": "claude-code",
5
- "bundle_version": "0.2.68",
5
+ "bundle_version": "0.2.70",
6
6
  "requires": {
7
- "minimumCliVersion": "0.2.87",
7
+ "minimumCliVersion": "0.2.88",
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": "db342ad36703f2a861b0df0e886fcbb25e81a4fa6b4cf9f47ade38a1d12a5a4b"
28
+ "sha256": "ba966d7563aa4c5cf5a170d9af6c80a4a1c87613061d2e8bf7858a1f53001690"
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": "992ce9f6978add8e56138bae4dddcacb45183f518b0bb00b901a95c23b13738b"
100
+ "sha256": "cb5686f5044f3dcf580497d8081ae0d7a3742a83c6910dc92d9622b21c4e3e94"
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": "e60e1437bdfea17c723e5384548a250b4b7646b37daf3a9abbc05478ea05a5ef"
116
+ "sha256": "f60acc504b6227448a5879240ed75b5762e587a5908ac0207c558e20f81fc098"
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": "be8eccf19cbbcf775bbebee4cf91a3fc69604588be48960b5216dcc6f6dfc53d"
156
+ "sha256": "c3a2cdadfb78f0b38503d117def71098a0a2919e3a8eb939e70b4796e3cd84cc"
157
157
  },
158
158
  {
159
159
  "path": "harness-knowledge-query/SKILL.md",
160
- "sha256": "da01386a0b83c2c9c2d99f5cc1baa0fefbbea523834e3dac77d0c6d7834b9963"
160
+ "sha256": "46a06f2b9610093d11dfbe9d8a27fd8a62129d66c7aa6bb82e759b7893ad9e9b"
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": "3aa5b3297f2c8566e0ede8a6408e0179c75d04ef58350fbac12729e8966181dc"
176
+ "sha256": "79e1c84a92a8bf6370cdc6691dc5cc92bd3e5ff4a720864949a1d3c7de7782a3"
177
177
  },
178
178
  {
179
179
  "path": "harness-pull/SKILL.md",
180
- "sha256": "47c33af5a0565b681ccce8c41a8cd1ba1b9cbe8b54f6a1c28309acae632a3eca"
180
+ "sha256": "29df6f2b5b351c01c97c47db42f835706add2bfe84b4edd0968da1289a2e54c9"
181
181
  },
182
182
  {
183
183
  "path": "harness-push/SKILL.md",
184
- "sha256": "f58e3aa0fb3545ae8f9820a8d13ca8069f800d20bc44813c016587e2fdc04975"
184
+ "sha256": "ee723583548b7fa73fe113bfbb73d840f144ba1bc059a627cb57289cd8518705"
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": "faddc29f7766391ef7a30eef0228fa8d13412ae506f1ce2128585cd8d02129bc"
200
+ "sha256": "a70669bf6deabb442a855535283e22ea6edb6c2e3d50dc65543ab47f7e01f5f3"
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": "367f5a1f9e848d1a094d785cc900b852dfdc24ef3951f5fe662fd0b922c26c54"
216
+ "sha256": "ad7fefb8891068fd4fbaf1890e21abcbe1a1eed037f3ab1cd751dca56bd651d0"
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": "29d8f550b6d6f287c162274d5274e67fc8ee827ba269d7531f845aa94515844a"
228
+ "sha256": "3c222757ff63d702fb69002704931dc6cca246256ff2a2a5a7d093f04410844e"
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": "c1dfbdc342866518833f844ddf01c54e8ae0eff0773cc0453fd1e544a3167452"
236
+ "sha256": "3c755bf5732e7d1f9945b08527668a33cde6e24a593b0636359d89a21418173e"
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": "ef03b216907be0066d5ce2785fe8742cf02b32164cbde7081ef29bd930208cf0"
260
+ "sha256": "3b2cc724648e75fc310fc09e44a1737c669fc12d5d0933b278c564a72c7fd05a"
261
261
  },
262
262
  {
263
263
  "path": "protocols/archive-report-protocol.md",
@@ -329,7 +329,7 @@
329
329
  },
330
330
  {
331
331
  "path": "scripts/harness_archive.py",
332
- "sha256": "0c3b8d9e14aedfd569bf38ce4f6d0ff9a168b3015f2b5fe685a58cb3e40f030b"
332
+ "sha256": "da88a0a922d24d402e3258041e3eb3a2c3d47dc42d930f10550161acf6864179"
333
333
  },
334
334
  {
335
335
  "path": "scripts/harness_change.py",
@@ -341,7 +341,7 @@
341
341
  },
342
342
  {
343
343
  "path": "scripts/harness_context.py",
344
- "sha256": "e61bbeb3073d77a34ac9aefb13421c187a8d16c8f95597d1ca50d91b0c6af1d2"
344
+ "sha256": "c7ad3199bda332e853a24e6268fa32dcc253adb6dfe7e0de7aa3b7f0ae7ae312"
345
345
  },
346
346
  {
347
347
  "path": "scripts/harness_deploy.py",
@@ -361,7 +361,7 @@
361
361
  },
362
362
  {
363
363
  "path": "scripts/harness_events.py",
364
- "sha256": "3116137be32bd696adce47c79f2af99a8f1517a7555bd1bbaffe27e17fd12fbc"
364
+ "sha256": "e992498f7256d11ae98d6e71be4f88fc8e08331311ca74372bbc890a720eae43"
365
365
  },
366
366
  {
367
367
  "path": "scripts/harness_execution_contracts.py",
@@ -413,7 +413,7 @@
413
413
  },
414
414
  {
415
415
  "path": "scripts/harness_plan_finalize.py",
416
- "sha256": "af12b53ab5503eb5c0ead5bd58e8da9d5389c649958ac678bf448e4c8a286d5b"
416
+ "sha256": "6f4dcee06f92f7ed7a3bd0ee615e9dd7e8fd921502b132305e37c4871b97537b"
417
417
  },
418
418
  {
419
419
  "path": "scripts/harness_preflight.py",