@hunter-harness/workflow-harness 0.2.64 → 0.2.65

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/contracts/workflow-policy.json +1 -1
  3. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +6 -5
  5. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +11 -5
  6. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-run/SKILL.md +2 -2
  10. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
  11. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/scripts/harness_archive.py +320 -35
  14. package/harness/bundles/general/claude-code/scripts/harness_context.py +43 -25
  15. package/harness/bundles/general/claude-code/scripts/harness_gate.py +22 -27
  16. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +155 -16
  17. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +31 -5
  18. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  19. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +1 -1
  20. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  21. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +6 -5
  22. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +11 -5
  23. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  24. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  26. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +2 -2
  27. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
  28. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  29. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +320 -35
  31. package/harness/bundles/general/codebuddy/scripts/harness_context.py +43 -25
  32. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +22 -27
  33. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +155 -16
  34. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +31 -5
  35. package/harness/bundles/general/codex/.harness-build.json +1 -1
  36. package/harness/bundles/general/codex/contracts/workflow-policy.json +1 -1
  37. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  38. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +6 -5
  39. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +11 -5
  40. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  41. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  42. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  43. package/harness/bundles/general/codex/harness-run/SKILL.md +2 -2
  44. package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
  45. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  46. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/scripts/harness_archive.py +320 -35
  48. package/harness/bundles/general/codex/scripts/harness_context.py +43 -25
  49. package/harness/bundles/general/codex/scripts/harness_gate.py +22 -27
  50. package/harness/bundles/general/codex/scripts/harness_ledger.py +155 -16
  51. package/harness/bundles/general/codex/scripts/harness_runtime.py +31 -5
  52. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  53. package/harness/bundles/general/cursor/contracts/workflow-policy.json +1 -1
  54. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  55. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +6 -5
  56. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +11 -5
  57. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  58. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  59. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  60. package/harness/bundles/general/cursor/harness-run/SKILL.md +2 -2
  61. package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
  62. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  63. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  64. package/harness/bundles/general/cursor/scripts/harness_archive.py +320 -35
  65. package/harness/bundles/general/cursor/scripts/harness_context.py +43 -25
  66. package/harness/bundles/general/cursor/scripts/harness_gate.py +22 -27
  67. package/harness/bundles/general/cursor/scripts/harness_ledger.py +155 -16
  68. package/harness/bundles/general/cursor/scripts/harness_runtime.py +31 -5
  69. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  70. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +1 -1
  71. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  72. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  73. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +6 -5
  74. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +11 -5
  75. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  76. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  77. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  78. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  79. package/harness/bundles/java/claude-code/harness-run/SKILL.md +2 -2
  80. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -2
  81. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  82. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  83. package/harness/bundles/java/claude-code/scripts/harness_archive.py +320 -35
  84. package/harness/bundles/java/claude-code/scripts/harness_context.py +43 -25
  85. package/harness/bundles/java/claude-code/scripts/harness_gate.py +22 -27
  86. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +155 -16
  87. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +31 -5
  88. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  89. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +1 -1
  90. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  91. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  92. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +6 -5
  93. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +11 -5
  94. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  95. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  96. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  97. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  98. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +2 -2
  99. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -2
  100. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  101. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  102. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +320 -35
  103. package/harness/bundles/java/codebuddy/scripts/harness_context.py +43 -25
  104. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +22 -27
  105. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +155 -16
  106. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +31 -5
  107. package/harness/bundles/java/codex/.harness-build.json +1 -1
  108. package/harness/bundles/java/codex/contracts/workflow-policy.json +1 -1
  109. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  110. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  111. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +6 -5
  112. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +11 -5
  113. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  114. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  115. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  116. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  117. package/harness/bundles/java/codex/harness-run/SKILL.md +2 -2
  118. package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -2
  119. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  120. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  121. package/harness/bundles/java/codex/scripts/harness_archive.py +320 -35
  122. package/harness/bundles/java/codex/scripts/harness_context.py +43 -25
  123. package/harness/bundles/java/codex/scripts/harness_gate.py +22 -27
  124. package/harness/bundles/java/codex/scripts/harness_ledger.py +155 -16
  125. package/harness/bundles/java/codex/scripts/harness_runtime.py +31 -5
  126. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  127. package/harness/bundles/java/cursor/contracts/workflow-policy.json +1 -1
  128. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  129. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  130. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +6 -5
  131. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +11 -5
  132. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  133. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  134. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  135. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  136. package/harness/bundles/java/cursor/harness-run/SKILL.md +2 -2
  137. package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -2
  138. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  139. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  140. package/harness/bundles/java/cursor/scripts/harness_archive.py +320 -35
  141. package/harness/bundles/java/cursor/scripts/harness_context.py +43 -25
  142. package/harness/bundles/java/cursor/scripts/harness_gate.py +22 -27
  143. package/harness/bundles/java/cursor/scripts/harness_ledger.py +155 -16
  144. package/harness/bundles/java/cursor/scripts/harness_runtime.py +31 -5
  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
@@ -981,28 +981,24 @@ def read_identity(skills_root: Path) -> dict[str, Any]:
981
981
  return identity
982
982
 
983
983
 
984
+ def resolve_skills_root(raw: str | None) -> Path | None:
985
+ """Resolve an explicit root or the installed bundle beside this script."""
986
+ if isinstance(raw, str) and raw.strip():
987
+ return Path(raw).expanduser().resolve()
988
+ adjacent = SCRIPTS_DIR.parent.resolve()
989
+ return adjacent if (adjacent / ".harness-build.json").is_file() else None
990
+
991
+
984
992
  def change_code_root(change_dir: Path) -> Path:
985
993
  project = change_dir.parents[2]
986
- metadata = (
987
- (change_dir / "meta" / "change-context.json", ("worktreeRoot",)),
988
- (change_dir / "meta" / "worktree.json", ("worktreePath", "path")),
989
- )
990
- for context_path, fields in metadata:
991
- if not context_path.is_file():
992
- continue
993
- try:
994
- context = json.loads(context_path.read_text(encoding="utf-8"))
995
- raw_root = next(
996
- (str(context.get(field) or "").strip() for field in fields if context.get(field)),
997
- "",
998
- )
999
- if raw_root:
1000
- raw_path = Path(raw_root).expanduser()
1001
- candidate = (raw_path if raw_path.is_absolute() else project / raw_path).resolve()
1002
- if candidate.is_dir():
1003
- return candidate
1004
- except (OSError, ValueError, json.JSONDecodeError):
1005
- pass
994
+ inferred = hl.infer_execution_project_root(change_dir)
995
+ if inferred is not None:
996
+ return inferred
997
+ if hl.declares_execution_worktree(change_dir):
998
+ raise ValueError(
999
+ "EXECUTION_WORKTREE_INVALID: declared worktree is missing, not a Git root, "
1000
+ "or belongs to another repository"
1001
+ )
1006
1002
  return project
1007
1003
 
1008
1004
 
@@ -2388,16 +2384,15 @@ def cmd_begin(args: argparse.Namespace) -> int:
2388
2384
  return emit_error("POLICY_LOAD_FAILED", str(exc), as_json=as_json)
2389
2385
 
2390
2386
  executor_tool = args.executor_tool or os.environ.get("HUNTER_HARNESS_TOOL")
2391
- if not args.skills_root:
2387
+ skills_root = resolve_skills_root(args.skills_root)
2388
+ if skills_root is None:
2392
2389
  return emit_error(
2393
2390
  "BUNDLE_IDENTITY_REQUIRED",
2394
2391
  "--skills-root is required; refresh the selected Harness adapter if identity is missing",
2395
2392
  as_json=as_json,
2396
2393
  )
2397
2394
  try:
2398
- identity = validate_identity(
2399
- project, Path(args.skills_root).expanduser().resolve(), executor_tool
2400
- )
2395
+ identity = validate_identity(project, skills_root, executor_tool)
2401
2396
  except (OSError, json.JSONDecodeError, ValueError) as exc:
2402
2397
  return emit_error("BUNDLE_IDENTITY_INVALID", str(exc), as_json=as_json)
2403
2398
  executor_tool = executor_tool or str(identity.get("adapter") or "") or None
@@ -2446,7 +2441,7 @@ def cmd_begin(args: argparse.Namespace) -> int:
2446
2441
  run_id=run_id,
2447
2442
  project=project,
2448
2443
  execution_root=execution_root,
2449
- skills_root=Path(args.skills_root).expanduser(),
2444
+ skills_root=skills_root,
2450
2445
  )
2451
2446
  except ValueError as exc:
2452
2447
  if gate_soft_allowed(severity_mode, args.phase, "capsule"):
@@ -2532,7 +2527,7 @@ def cmd_begin(args: argparse.Namespace) -> int:
2532
2527
  "projectRoot": str(project.resolve()),
2533
2528
  "stateRoot": str(state_root),
2534
2529
  "executionRoot": str(execution_root),
2535
- "skillsRoot": str(Path(args.skills_root).expanduser().resolve()),
2530
+ "skillsRoot": str(skills_root.resolve()),
2536
2531
  "repositoryId": hp.repository_identity(execution_root),
2537
2532
  "baseCommit": base_commit or current_head,
2538
2533
  "currentHead": current_head,
@@ -2574,7 +2569,7 @@ def cmd_begin(args: argparse.Namespace) -> int:
2574
2569
  "projectRoot": str(project),
2575
2570
  "stateRoot": capsule.get("stateRoot") if capsule else None,
2576
2571
  "executionRoot": capsule.get("executionRoot") if capsule else str(execution_root),
2577
- "skillsRoot": capsule.get("skillsRoot") if capsule else str(Path(args.skills_root).resolve()),
2572
+ "skillsRoot": capsule.get("skillsRoot") if capsule else str(skills_root.resolve()),
2578
2573
  "lease": claim.get("lease"),
2579
2574
  "identity": identity,
2580
2575
  "event": event_result,
@@ -412,6 +412,131 @@ def input_files_from_args(args: argparse.Namespace) -> tuple[list[str], Path | N
412
412
  return resolve_input_files(selected, project_root), project_root
413
413
 
414
414
 
415
+ def infer_execution_project_root(change_dir: Path) -> Path | None:
416
+ """Resolve the tree under test from trusted change metadata.
417
+
418
+ Worktree changes keep their durable contract under the main checkout while
419
+ product files live in the execution worktree. Requiring every caller to
420
+ reconstruct that split caused ledger commands to hash the main checkout or
421
+ fail repeatedly. Prefer the persisted execution root and otherwise leave
422
+ legacy callers unchanged.
423
+ """
424
+ change_dir = change_dir.expanduser().resolve()
425
+ project_root = next(
426
+ (ancestor.parent for ancestor in change_dir.parents if ancestor.name == ".harness"),
427
+ None,
428
+ )
429
+ if project_root is None:
430
+ return None
431
+
432
+ def git_common_dir(root: Path) -> Path | None:
433
+ process = subprocess.run(
434
+ ["git", "-C", str(root), "rev-parse", "--path-format=absolute", "--git-common-dir"],
435
+ text=True,
436
+ stdout=subprocess.PIPE,
437
+ stderr=subprocess.PIPE,
438
+ check=False,
439
+ )
440
+ if process.returncode != 0 or not process.stdout.strip():
441
+ return None
442
+ raw = Path(process.stdout.strip()).expanduser()
443
+ return raw.resolve() if raw.is_absolute() else (root / raw).resolve()
444
+
445
+ project_common_dir = git_common_dir(project_root)
446
+
447
+ def trusted_worktree(candidate: Path) -> Path | None:
448
+ if not candidate.is_dir():
449
+ return None
450
+ process = subprocess.run(
451
+ ["git", "-C", str(candidate), "rev-parse", "--show-toplevel"],
452
+ text=True,
453
+ stdout=subprocess.PIPE,
454
+ stderr=subprocess.PIPE,
455
+ check=False,
456
+ )
457
+ if process.returncode != 0 or not process.stdout.strip():
458
+ return None
459
+ top_level = Path(process.stdout.strip()).expanduser().resolve()
460
+ if top_level != candidate:
461
+ return None
462
+ # Origin URL + root commit identifies a repository lineage, not a
463
+ # linked checkout. Independent clones can share both. A real linked
464
+ # worktree must share the exact Git common directory with the main tree.
465
+ if project_common_dir is None or git_common_dir(top_level) != project_common_dir:
466
+ return None
467
+ return top_level
468
+
469
+ candidates = (
470
+ (
471
+ change_dir / "meta" / "change-context.json",
472
+ ("worktreePath", "path", "worktreeRoot"),
473
+ ),
474
+ (
475
+ change_dir / "meta" / "worktree.json",
476
+ ("worktreePath", "path", "worktreeRoot"),
477
+ ),
478
+ )
479
+ for metadata_path, keys in candidates:
480
+ if not metadata_path.is_file():
481
+ continue
482
+ try:
483
+ payload = json.loads(metadata_path.read_text(encoding="utf-8-sig"))
484
+ except (OSError, json.JSONDecodeError):
485
+ continue
486
+ if not isinstance(payload, dict):
487
+ continue
488
+ for key in keys:
489
+ raw = payload.get(key)
490
+ if not isinstance(raw, str) or not raw.strip():
491
+ continue
492
+ raw_path = Path(raw).expanduser()
493
+ candidate = (
494
+ raw_path.resolve()
495
+ if raw_path.is_absolute()
496
+ else (project_root / raw_path).resolve()
497
+ )
498
+ if key == "worktreeRoot" and candidate.name != change_dir.name:
499
+ nested = (candidate / change_dir.name).resolve()
500
+ trusted_nested = trusted_worktree(nested)
501
+ if trusted_nested is not None:
502
+ return trusted_nested
503
+ if trusted_worktree(candidate) is None:
504
+ continue
505
+ trusted_candidate = trusted_worktree(candidate)
506
+ if trusted_candidate is not None:
507
+ return trusted_candidate
508
+ return None
509
+
510
+
511
+ def declares_execution_worktree(change_dir: Path) -> bool:
512
+ """Return whether persisted metadata explicitly selects another checkout."""
513
+ for metadata_path in (
514
+ change_dir / "meta" / "change-context.json",
515
+ change_dir / "meta" / "worktree.json",
516
+ ):
517
+ if not metadata_path.is_file():
518
+ continue
519
+ try:
520
+ payload = json.loads(metadata_path.read_text(encoding="utf-8-sig"))
521
+ except (OSError, json.JSONDecodeError):
522
+ continue
523
+ if isinstance(payload, dict) and any(
524
+ isinstance(payload.get(key), str) and str(payload[key]).strip()
525
+ for key in ("worktreePath", "path", "worktreeRoot")
526
+ ):
527
+ return True
528
+ return False
529
+
530
+
531
+ def apply_inferred_project_root(args: argparse.Namespace, change_dir: Path) -> None:
532
+ """Fill an omitted --project without overriding an explicit caller choice."""
533
+ if getattr(args, "project", None):
534
+ return
535
+ inferred = infer_execution_project_root(change_dir)
536
+ if inferred is not None:
537
+ args.project = str(inferred)
538
+
539
+
415
540
  def sha256_file(path: Path) -> str:
416
541
  digest = hashlib.sha256()
417
542
  with path.open("rb") as handle:
@@ -1774,20 +1899,10 @@ def worktree_ready(ledger: dict[str, Any], change_dir: Path) -> bool:
1774
1899
  root = ledger.get("worktreeRoot")
1775
1900
  if root is not None and _nonempty_str(root):
1776
1901
  return True
1777
- meta = change_dir / "meta" / "worktree.json"
1778
- if not meta.is_file():
1779
- return False
1780
- try:
1781
- data = json.loads(meta.read_text(encoding="utf-8-sig"))
1782
- except (OSError, json.JSONDecodeError):
1783
- return False
1784
- if not isinstance(data, dict):
1785
- return False
1786
- if data.get("requested") is True and data.get("created") is True:
1787
- return True
1788
- if _nonempty_str(data.get("path")) or _nonempty_str(data.get("worktreeRoot")):
1789
- return True
1790
- return False
1902
+ # Metadata aliases are accepted only when they resolve to a real linked
1903
+ # worktree of this repository. This keeps install reuse aligned with the
1904
+ # execution root used by context and gate, including `worktreePath`.
1905
+ return infer_execution_project_root(change_dir) is not None
1791
1906
 
1792
1907
 
1793
1908
  def decide_can_reuse(
@@ -2242,6 +2357,7 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
2242
2357
  verbose = bool(getattr(args, "verbose", False))
2243
2358
  verification = args.verification
2244
2359
  change_dir = resolve_path(args.change_dir)
2360
+ apply_inferred_project_root(args, change_dir)
2245
2361
  ownership_check = frozen_ownership_check(change_dir)
2246
2362
  if not ownership_check.get("ok"):
2247
2363
  return emit_error(
@@ -2359,6 +2475,9 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
2359
2475
  return emit_error(f"can-reuse failed: {exc}", as_json=as_json)
2360
2476
 
2361
2477
  payload = _attach_invalidation_code(payload)
2478
+ payload["resolvedProjectRoot"] = (
2479
+ str(project_root) if project_root is not None else None
2480
+ )
2362
2481
  emit_compact_or_verbose(
2363
2482
  payload, as_json=as_json, verbose=verbose,
2364
2483
  compact_fn=_compact_can_reuse_payload,
@@ -2370,6 +2489,7 @@ def cmd_record(args: argparse.Namespace) -> int:
2370
2489
  as_json = bool(args.json)
2371
2490
  verbose = bool(getattr(args, "verbose", False))
2372
2491
  change_dir = resolve_path(args.change_dir)
2492
+ apply_inferred_project_root(args, change_dir)
2373
2493
  ownership_check = frozen_ownership_check(change_dir)
2374
2494
  if not ownership_check.get("ok"):
2375
2495
  return emit_error(
@@ -2836,11 +2956,26 @@ def cmd_record(args: argparse.Namespace) -> int:
2836
2956
  def cmd_diff_hash(args: argparse.Namespace) -> int:
2837
2957
  as_json = bool(args.json)
2838
2958
  repo_raw = getattr(args, "repo", None)
2839
- repo = Path(repo_raw).expanduser().resolve() if repo_raw else Path.cwd().resolve()
2840
2959
  base = getattr(args, "base", None)
2841
2960
  change_dir_raw = getattr(args, "change_dir", None)
2842
2961
  try:
2843
2962
  change_dir = resolve_path(change_dir_raw) if change_dir_raw else None
2963
+ repo = Path(repo_raw).expanduser().resolve() if repo_raw else Path.cwd().resolve()
2964
+ # Historical skill text passed ``--repo .`` even when the active change
2965
+ # ran in a linked worktree. Treat that shorthand like the default and
2966
+ # recover the authoritative execution root captured during Plan.
2967
+ repo_hint = str(repo_raw or "").strip().replace("\\", "/")
2968
+ if change_dir is not None and repo_hint in {"", ".", "./"}:
2969
+ inferred_repo = infer_execution_project_root(change_dir)
2970
+ if inferred_repo is not None:
2971
+ repo = inferred_repo
2972
+ elif declares_execution_worktree(change_dir):
2973
+ return emit_error(
2974
+ "change metadata declares an execution worktree that is missing or belongs "
2975
+ "to another repository",
2976
+ as_json=as_json,
2977
+ error_code="EXECUTION_WORKTREE_INVALID",
2978
+ )
2844
2979
  if change_dir is not None and _contract_is_v2(change_dir):
2845
2980
  resolved_base = str(base or "").strip()
2846
2981
  if not resolved_base:
@@ -3086,7 +3221,11 @@ def build_parser() -> argparse.ArgumentParser:
3086
3221
  parents=[shared_json],
3087
3222
  help="compute commit-invariant byte-level diff hash for a repo",
3088
3223
  )
3089
- p_diff.add_argument("--repo", default=None, help="repo root (default: cwd)")
3224
+ p_diff.add_argument(
3225
+ "--repo",
3226
+ default=None,
3227
+ help="repo root (default: change worktree when recorded, otherwise cwd)",
3228
+ )
3090
3229
  p_diff.add_argument("--base", default=None, help="base commit (default: root commit)")
3091
3230
  p_diff.add_argument(
3092
3231
  "--change-dir",
@@ -33,11 +33,15 @@ SCHEMA_VERSION = 1
33
33
  RUN_SESSION_SCHEMA_VERSION = 1
34
34
  RUN_TERMINAL_STATUSES = {"OK", "FAIL", "INCOMPLETE", "CANCELLED"}
35
35
  _CHANGE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
36
- SECRET_SCAN_RULES_VERSION = "secret-scan-v1"
36
+ SECRET_SCAN_RULES_VERSION = "secret-scan-v2"
37
37
  SECRET_SCAN_RECEIPT_REL = Path("meta") / "secret-scan-receipt.json"
38
38
  _SENSITIVE_ASSIGNMENT = re.compile(
39
- rb"(?i)[\"']?(?:password|passwd|token|secret|cookie|authorization|database[_-]?url)"
40
- rb"[\"']?\s*[:=]\s*[\"']?[^\s\"']{4,}"
39
+ rb"(?i)[\"']?(?:password|passwd|token|secret|cookie|database[_-]?url)"
40
+ rb"[\"']?\s*[:=]\s*[\"']?(?P<value>[^\s\"']{4,})"
41
+ )
42
+ _AUTHORIZATION_ASSIGNMENT = re.compile(
43
+ rb"(?i)[\"']?authorization[\"']?\s*[:=]\s*[\"']?"
44
+ rb"(?:(?:bearer|basic)\s+)?(?P<value>[^\s\"']{4,})"
41
45
  )
42
46
  _ADAPTERS = {
43
47
  "claude-code": (".worktrees", "harness/"),
@@ -193,8 +197,16 @@ def _sensitive_candidates(root: Path) -> list[dict[str, Any]]:
193
197
  raw = path.read_bytes()
194
198
  except OSError:
195
199
  continue
196
- match = _SENSITIVE_ASSIGNMENT.search(raw)
197
- if match is None:
200
+ finding = next(
201
+ (
202
+ match
203
+ for pattern in (_SENSITIVE_ASSIGNMENT, _AUTHORIZATION_ASSIGNMENT)
204
+ for match in pattern.finditer(raw)
205
+ if not _is_sensitive_placeholder(match.group("value"))
206
+ ),
207
+ None,
208
+ )
209
+ if finding is None:
198
210
  continue
199
211
  candidates.append({
200
212
  "path": path.relative_to(root).as_posix(),
@@ -204,6 +216,20 @@ def _sensitive_candidates(root: Path) -> list[dict[str, Any]]:
204
216
  return candidates
205
217
 
206
218
 
219
+ def _is_sensitive_placeholder(value: bytes) -> bool:
220
+ text = value.decode("utf-8", errors="ignore").strip().strip("`'").strip()
221
+ return bool(
222
+ re.fullmatch(r"<[^<>]+>", text)
223
+ or re.fullmatch(r"\$\{[A-Za-z_][A-Za-z0-9_]*\}", text)
224
+ or re.fullmatch(r"\{\{[^{}]+\}\}", text)
225
+ or re.fullmatch(
226
+ r"(?i)(?:example|placeholder|redacted|masked|bearer|basic|"
227
+ r"your[-_][A-Za-z0-9_-]+|x{4,})",
228
+ text,
229
+ )
230
+ )
231
+
232
+
207
233
  def publishable_tree_digest(root: Path) -> str:
208
234
  """Public digest helper shared by the archive publication gate."""
209
235
  return _publishable_tree_digest(root)
@@ -2,9 +2,9 @@
2
2
  "schema_version": 2,
3
3
  "profile": "general",
4
4
  "adapter": "claude-code",
5
- "bundle_version": "0.2.53",
5
+ "bundle_version": "0.2.54",
6
6
  "requires": {
7
- "minimumCliVersion": "0.2.73",
7
+ "minimumCliVersion": "0.2.74",
8
8
  "capabilities": [
9
9
  "sync@2",
10
10
  "rules-sync@1",
@@ -23,7 +23,7 @@
23
23
  "files": [
24
24
  {
25
25
  "path": ".harness-build.json",
26
- "sha256": "1b39101b4c9cf6dab32fb0e226f49497c91c9f1ca3508a06b41f105502b560fb"
26
+ "sha256": "a7cbdb7be11d56b509b88d5406209901dc1ae27ed0bab9f7baeb9910043efa33"
27
27
  },
28
28
  {
29
29
  "path": "agents/harness-evaluator.md",
@@ -79,7 +79,7 @@
79
79
  },
80
80
  {
81
81
  "path": "contracts/workflow-policy.json",
82
- "sha256": "3da83c2d769d536cb8bf356f1a03f0ad08eb19ff385ac36960bf53b5083b4f32"
82
+ "sha256": "3b671d438a687d800879863590ff04bd953c7d3eac6865bda3d85cdfc26fbc52"
83
83
  },
84
84
  {
85
85
  "path": "harness-archive/checklist.md",
@@ -95,7 +95,7 @@
95
95
  },
96
96
  {
97
97
  "path": "harness-archive/SKILL.md",
98
- "sha256": "946170c7b150b688dc83bddf4dc0f3488ced503c1d533b219a7bc2b6583794b1"
98
+ "sha256": "8436d5af255e01e178a4920a176bdc1192e429b637c2c1d1959e7f168bd98f25"
99
99
  },
100
100
  {
101
101
  "path": "harness-archive/templates/summary-data-template.json",
@@ -111,7 +111,7 @@
111
111
  },
112
112
  {
113
113
  "path": "harness-codebase-map/SKILL.md",
114
- "sha256": "9e2da46174dfa40f2b347e5e928feda8807021496cc0cd8ea99a5e43fac92ea0"
114
+ "sha256": "b19f8a3d4da1e471d3b857c7e914e4ecdef838812ae7b6e3c8a970eec7c7061b"
115
115
  },
116
116
  {
117
117
  "path": "harness-codebase-map/templates/ARCHITECTURE.md",
@@ -151,11 +151,11 @@
151
151
  },
152
152
  {
153
153
  "path": "harness-knowledge-ingest/SKILL.md",
154
- "sha256": "001b637a9c18a3e453b5af673b7da8bf86c564cbc5809270547b60a63b5d06e7"
154
+ "sha256": "7ece336a562d942fe033a09e0bcfc6454ca1762d6c3e0410a5550a93d4cf9e12"
155
155
  },
156
156
  {
157
157
  "path": "harness-knowledge-query/SKILL.md",
158
- "sha256": "ef858857be39df8c82279f774f5a6e3ce0531c3d555442de7d5d29cbca3e4e1e"
158
+ "sha256": "75f01294c4d81abab5d75dd10001607cd6779410c4b2d08202d00b03d13a8132"
159
159
  },
160
160
  {
161
161
  "path": "harness-plan/checklist.md",
@@ -171,7 +171,7 @@
171
171
  },
172
172
  {
173
173
  "path": "harness-plan/SKILL.md",
174
- "sha256": "b2f7b3427a1741bd4a757c06c3ce91ffb95092dcb87fc33c699fbcbffe7a40e6"
174
+ "sha256": "9922ba328a97497b9eef1937e00f4d79d5a754c0af4bc3f00ed2d60ba097ae1c"
175
175
  },
176
176
  {
177
177
  "path": "harness-review/checklist.md",
@@ -187,7 +187,7 @@
187
187
  },
188
188
  {
189
189
  "path": "harness-review/SKILL.md",
190
- "sha256": "b7dc70ed34e362858e4fe473cb6a03e7bcaf6b78df399459d7f28a304e121116"
190
+ "sha256": "35cd30eb98e61e751e3d92eed371e32ebf550d9919a759c34387c332ce1144a9"
191
191
  },
192
192
  {
193
193
  "path": "harness-run/checklist.md",
@@ -203,7 +203,7 @@
203
203
  },
204
204
  {
205
205
  "path": "harness-run/SKILL.md",
206
- "sha256": "e97ae481de895bf0b86a1171de9ed904ee0c50093c4ea655c31b40e2d514d27f"
206
+ "sha256": "ccadcd4170ca8e9c76611bfee9bda6cb016d627d5848de91da40d74fef28758b"
207
207
  },
208
208
  {
209
209
  "path": "harness-submit/checklist.md",
@@ -215,7 +215,7 @@
215
215
  },
216
216
  {
217
217
  "path": "harness-submit/SKILL.md",
218
- "sha256": "b0853f34ff2461a268158e015f71e2c0159ea20522437dba1709dd76848e099a"
218
+ "sha256": "a5ce09ea83ef86837a2a641f7c680f18c418c191e31674c1afd12102a25ac55a"
219
219
  },
220
220
  {
221
221
  "path": "harness-sync/reference.md",
@@ -223,7 +223,7 @@
223
223
  },
224
224
  {
225
225
  "path": "harness-sync/SKILL.md",
226
- "sha256": "c7d463eb390d99b5e566cb3fc970700d2d1bff07e56173d33b10644672db9102"
226
+ "sha256": "00d5d6ebe2b1a416b4c814e5a14fcb7c3ceb5c2e5219f9d929f702c92f123174"
227
227
  },
228
228
  {
229
229
  "path": "harness-test/checklist.md",
@@ -247,7 +247,7 @@
247
247
  },
248
248
  {
249
249
  "path": "harness-test/SKILL.md",
250
- "sha256": "51dc5197408d9c4961ad81e69ce3e1695e11e394ee3582da71fda4b11de80b3c"
250
+ "sha256": "9e66478326b1f042e705af7d6c7253bf86ced062770debe952337b92297b1f08"
251
251
  },
252
252
  {
253
253
  "path": "protocols/archive-report-protocol.md",
@@ -319,7 +319,7 @@
319
319
  },
320
320
  {
321
321
  "path": "scripts/harness_archive.py",
322
- "sha256": "3c6fc709e5a2876fd4bad4ec60402b7b3933af1ea6d15bb6d16f269c7d65bf2f"
322
+ "sha256": "f029938af890198a737d6fea746a9f5ed885f8c32a462a63cac79dbb0f0b8be0"
323
323
  },
324
324
  {
325
325
  "path": "scripts/harness_change.py",
@@ -331,7 +331,7 @@
331
331
  },
332
332
  {
333
333
  "path": "scripts/harness_context.py",
334
- "sha256": "5ef587edd57b0c819df1c37a448eda33f00a5b97b930c804290d1a83d9a4ad60"
334
+ "sha256": "464b59fea967bf123a7e203524b22ce68ead7db0250e686407ea29520853065f"
335
335
  },
336
336
  {
337
337
  "path": "scripts/harness_deploy.py",
@@ -363,7 +363,7 @@
363
363
  },
364
364
  {
365
365
  "path": "scripts/harness_gate.py",
366
- "sha256": "f6be8724c0a8a0691d235a4af30fa97dc2988235ae47b88bfbe7235994c63230"
366
+ "sha256": "662f3490217546c9a20ce910f14493133dcf1a5cd1127f6cc6c50747ac22618c"
367
367
  },
368
368
  {
369
369
  "path": "scripts/harness_headless.py",
@@ -375,7 +375,7 @@
375
375
  },
376
376
  {
377
377
  "path": "scripts/harness_ledger.py",
378
- "sha256": "5c33a09f3b073294afa21804697488dd5fa5601e79707be8b7db9f89be8e95f9"
378
+ "sha256": "58f45d0a30c124b2493fc4a4c66f40c85c9a3dda952fccafbdc05a9a7a135d02"
379
379
  },
380
380
  {
381
381
  "path": "scripts/harness_migration_head.py",
@@ -427,7 +427,7 @@
427
427
  },
428
428
  {
429
429
  "path": "scripts/harness_runtime.py",
430
- "sha256": "75341de64c53f6393d99cfd944d83578e55317c494a76273865baa87459abdfa"
430
+ "sha256": "3866980763961f178f44fc04330854b4a3e05867900418316f876bf7f4fca1b7"
431
431
  },
432
432
  {
433
433
  "path": "scripts/harness_service.py",