Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions hermes_cli/kanban_completion_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@
"banner": 0,
}

# Role sets — referenced by the floor's honest-reject bypass (#74) and
# the workspace-diff and reviewer-fields gates below. Hoisted here so
# verify_runtime_floor can reference REVIEW_ROLES at call time without
# forward-ref gymnastics.
REVIEW_ROLES = {"tony", "tchalla", "vision", "reviewer"}
ORCHESTRATION_ROLES = {"jarvis", "pepper", "banner"}


@dataclass(frozen=True)
class RuntimeFloorViolation:
Expand All @@ -92,6 +99,7 @@ def verify_runtime_floor(
completed_at: int,
*,
allow_below_floor: bool = False,
is_honest_reject: bool = False,
) -> Optional[RuntimeFloorViolation]:
"""Return a violation if the worker's runtime is below its role floor.

Expand All @@ -101,12 +109,23 @@ def verify_runtime_floor(

A floor of 0 (or an unknown assignee, or a missing ``started_at``) is a
pass — we never invent floors for roles we don't know.

``is_honest_reject=True`` is a bypass for review-role tasks that
are completing with ``verdict: reject``. Closes hermes-jarvis#74:
the floor was designed against rubber-stamp APPROVES; penalizing
accurate fast rejects creates pressure to either fake-pad time or
rubber-stamp-approve, neither of which is good. Build/orchestration
roles get no bypass — a 60-second "implementation" that ends in
reject still warrants the floor.
"""
if allow_below_floor:
return None
if not assignee or started_at is None:
return None
floor = ROLE_RUNTIME_FLOORS_SECONDS.get(assignee.lower())
role = assignee.lower()
if is_honest_reject and role in REVIEW_ROLES:
return None
floor = ROLE_RUNTIME_FLOORS_SECONDS.get(role)
if not floor:
return None
actual = max(0, completed_at - int(started_at))
Expand All @@ -122,9 +141,6 @@ def verify_runtime_floor(
# Workspace-diff gate (#62)
# =====================================================================

REVIEW_ROLES = {"tony", "tchalla", "vision", "reviewer"}
ORCHESTRATION_ROLES = {"jarvis", "pepper", "banner"}


@dataclass(frozen=True)
class WorkspaceDiffViolation:
Expand Down Expand Up @@ -589,6 +605,14 @@ def _has_adversarial_structure(text: str) -> bool:
# Empty-marker tokens accepted as honest declarations.
_HONEST_EMPTY_MARKERS = {"none", "[]", "{}", "n/a"}

# hermes-jarvis#75: bullet form of honest-empty for test_quality.evidence.
# The reason must be ≥8 chars (1 non-whitespace + at least 7 more
# characters), mirroring the ``not_applicable`` shape already accepted
# for imports_match.
_BULLET_NOT_APPLICABLE_RE = re.compile(
r"(?mi)^\s*-\s*not_applicable\s*:\s*(\S.{7,})\s*$",
)


def _adversarial_value_substantive(captured: str) -> bool:
"""True if an adversarial_pass.* value's content shows the reviewer
Expand Down Expand Up @@ -657,6 +681,12 @@ def _field_present(field_key: str, text: str, *, code_change_context: bool = Fal
head = body.strip().split("\n", 1)[0].strip().lower()
if head in _HONEST_EMPTY_MARKERS:
return not code_change_context
# hermes-jarvis#75: accept ``- not_applicable: <≥8 char reason>``
# as a bullet equivalent of the inline honest-empty markers.
# Same code_change_context rule applies — code-touching reviews
# must produce real citations, not honest-empty escapes.
if _BULLET_NOT_APPLICABLE_RE.search(body):
return not code_change_context
return bool(_EVIDENCE_CITATION_RE.search(body))

# Other multi-key fields (currently none) fall through.
Expand Down
31 changes: 30 additions & 1 deletion hermes_cli/kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -3678,12 +3678,18 @@ def _v6_7_run_completion_gates(
no_reviewer_fields = no_rf_reason is not None
phantom_ok = phantom_ok_reason is not None
doc_drift_ok = doc_drift_reason is not None
# hermes-jarvis#74: honest-reject completion bypasses the floor for
# reviewers. Parsing the verdict here once so both the floor gate
# and any future gate can use it.
parsed_verdict = _v6_7_parse_verdict(result)
is_honest_reject = parsed_verdict == "reject"
violations: list = []
floor = verify_runtime_floor(
assignee=row["assignee"],
started_at=row["started_at"],
completed_at=now,
allow_below_floor=fast_ok,
is_honest_reject=is_honest_reject,
)
if floor is not None:
violations.append(floor)
Expand Down Expand Up @@ -4179,7 +4185,30 @@ def edit_completed_task_result(
summary: Optional[str] = None,
metadata: Optional[dict] = None,
) -> bool:
"""Backfill the user-visible result for an already completed task."""
"""Backfill the user-visible result for an already completed task.

.. warning::

This bypasses every v6.7/v6.8 verification gate (runtime
floor, workspace-diff, repo hygiene, reviewer fields,
adversarial pass, PR existence, doc drift, honest-reject
verdict parser, integrative-review-spawn). It exists for
human/CLI ops on a finished task (``hermes kanban edit``);
do NOT wire it into a worker tool surface or any agent
accessible path. Doing so would let a worker:

- Complete a task with a low-discipline result, then
re-write to a high-discipline one (or vice-versa) to
satisfy whatever gate ran first.
- Flip ``verdict: approve`` → ``verdict: reject`` after the
gate path already accepted the result.
- Insert phantom PR URLs that the gate already verified
against at completion time.

If you need worker access to amend a completed result, run
the full gate pipeline (``_v6_7_run_completion_gates``)
against the new content before the UPDATE.
"""
handoff_summary = summary if summary is not None else result
with write_txn(conn):
row = conn.execute(
Expand Down
Loading