Issue 204: portable commit gates
Spec: Portable commit gates
Context
Harness CLI 4.0.4 generated .devpunks/commit-gate-contracts.json with a machine-specific absolute root. When the repository was cloned elsewhere or used through a linked worktree, the generated runner attempted to launch owner-local quality commands in the obsolete directory. Node returned a launch-time ENOENT, but the runner discarded the useful error message and reported only generic exit-1 failures.
Current scaffold output already emits repository-relative owner paths without an absolute root. Existing committed legacy contracts remain unsafe because the runner still treats their obsolete root field as authoritative.
Non-Goals
- Adding a new absolute-root field or another machine-specific contract locator.
- Redesigning Lefthook configuration, quality-command discovery, or staged-file selection.
- Weakening commit-gate failures, scaffold/check integrity, or command exit behavior.
- Defining general policy for arbitrary hand-authored absolute or traversing owner paths.
- Requiring an npm or baseline publication as part of this issue.
Requirements and Outcomes
OUT-001: Commit-gate owner execution follows the active checkout
Commit-gate owner directories resolve from the active Git worktree top level. Generated repository-relative owner paths work after cloning, moving a checkout, and within a linked worktree. A legacy machine-specific root may be read for compatibility but cannot select the execution directory.
OUT-002: Process-launch failures are actionable
When a quality command cannot launch, the aggregate failure identifies the owner, command kind, resolved working directory, and underlying launch error code or message while retaining available command stdout and stderr.
OUT-003: Existing commit-gate behavior remains stable
The repair preserves staged-file scoping, deletion exclusion, owner-relative file arguments, lint and format-check execution, aggregate failure reporting, and nonzero final status when any command fails.
Acceptance Criteria
- AC-001: A contract with a deliberately stale absolute legacy
rootexecutes owner commands beneath the current Git worktree root. Covers: OUT-001. - AC-002: The same owner-relative contract behavior succeeds in both an ordinary repository and a linked Git worktree without regenerating machine-specific paths. Covers: OUT-001.
- AC-003: Fresh scaffold output contains repository-relative owner
pathvalues and no machine-specificroot. Covers: OUT-001. - AC-004: A missing resolved owner directory reports owner, command kind, resolved cwd, and the underlying launch error code or message. Covers: OUT-002.
- AC-005: Existing root and nested owner staged-file tests continue to prove scoped arguments, ignored deletions, aggregate lint/format failures, and read-only checks. Covers: OUT-003.
Constraints
- Use
git rev-parse --show-toplevelsemantics for the active worktree; do not use the shared Git common directory. - Preserve the generated contract's current provider-neutral shape: quality commands, owner identity, and repository-relative path.
- Legacy
rootcompatibility is one-way: it may be tolerated in input but is non-authoritative. - Preserve existing command templates and staged-file substitution behavior.
Dependency Readiness
Ready:
- Parent branch: open PR #208 head
fix/issue-203-scaffold-convergenceat2d8839978075419cb9a8a70f296f078626d31a7d. - Child branch:
team/stefan/issue-204-portable-commit-gateswas created at the original issue #203 head and fast-forwarded onto the current PR #208 head before review; ancestry was verified withgit merge-base --is-ancestor.
Branch/Base Intent
The issue #204 branch must remain stacked directly on fix/issue-203-scaffold-convergence. Its pull request targets the issue #203 branch rather than main so the 203 → 204 stack remains explicit.
Accepted Technical Decisions
- Resolve the active worktree root at runner runtime and resolve each contract
pathbeneath it. - Ignore legacy
contract.rootas execution authority instead of attempting to rewrite or trust it. - Preserve launch-error metadata separately from process stdout and stderr so
ENOENTand equivalent failures remain visible. - Keep the current runner and scaffold producer seams; do not introduce a new service or contract schema field.
Accepted Testing Decisions
- Test through the generated runner's executable boundary with real temporary Git repositories and staged files.
- Add a stale-root relocation regression and linked-worktree regression.
- Add a missing-owner launch regression that proves cwd and underlying error visibility.
- Retain existing generated-gate/scaffold assertions proving no absolute root is emitted and command behavior remains read-only.
Verification Seams
node .agents/scripts/commit-gate-runner.mjs --contracts .devpunks/commit-gate-contracts.jsonin isolated repositories and linked worktrees.- The canonical source runner test suite under
apps/cli/src/data/scripts. - Scaffold output tests that decode generated contracts and execute the generated runner.
- Package-scoped CLI typecheck, lint/format checks, and build.
Parked Decisions
- Semantic
hi checkrejection of legacyrootfields is parked for a future issue; resume if desired-byte update drift proves insufficient migration guidance. - General confinement of arbitrary hand-authored owner paths is parked for a future security/contract-hardening issue; resume if such paths become a supported public contract.
Decision Log
| Decision | Evidence | Rationale |
|---|---|---|
| Active Git worktree root is authoritative | Issue #204 expected behavior and runner trace | It selects the checkout and index actually running the commit gate. |
| Legacy absolute root is ignored | Current generator is rootless while old committed contracts remain | New runner bytes repair existing contracts without trusting stale machine paths. |
| Launch metadata is retained | Node missing-cwd probe returns ENOENT with empty stdout/stderr | Generic exit 1 is not actionable without the discarded error and cwd. |
| Existing staged-file semantics remain stable | Current runner and generated-gate regression suites | Portability does not require changing quality-command scope or mutation behavior. |