Say a local commit publishes nothing (#129)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m54s
CI / e2e (pull_request) Successful in 3m15s

The local driver sometimes ran `git commit` itself instead of calling
finish_milestone, then told the operator a PR was waiting when none had
been opened. A commit still runs (it's contained, and publishing takes
whatever is there), but when the driver's command commits anywhere in
its line, the harness adds to its result: committed locally only;
nothing is published until finish_milestone, and there is no PR until
then. Structural, at the moment the model would draw the wrong
conclusion (P6), rather than left to it noticing.

Closes #129

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-08 14:28:59 +00:00
1 parent bd17c74c3a
commit c4726d5673
4 files changed
+80 -3

No files matched your search

+18
View File
@@ -35,6 +35,8 @@ from nisse_core.actions import (
classify_shell,
inside,
resolve,
split_shell,
unwrap,
worst,
)
@@ -87,6 +89,22 @@ _BRANCHES = (
)
def commits(action: Action) -> bool:
"""Whether the call runs a `git commit` anywhere in its line (#129)."""
if action.tool != "run_command":
return False
for argv, _ in split_shell(str(action.arguments.get("command", ""))) or []:
argv = unwrap(argv)
if argv[:1] != ["git"]:
continue
args = argv[1:]
while args and args[0] in ("-C", "-c"):
args = args[2:]
if args[:1] == ["commit"]:
return True
return False
def classify(action: Action, root: str) -> Verdict:
"""The fast path's verdict on one execution tool call in the workspace
at ``root``."""
+10 -1
View File
@@ -44,7 +44,7 @@ from nisse_dev.expert import BudgetExhausted, ExpertDesk
from nisse_dev.git import GitError
from nisse_dev.gitea import GiteaClient, GiteaError, Issue, PullRequest
from nisse_dev.llm import LLMError, RoleMap
from nisse_dev.loop.actions import classify
from nisse_dev.loop.actions import classify, commits
from nisse_dev.loop.classifier import IN_SCOPE, ActionClassifier, Scope
from nisse_dev.loop.driver import ASK_ALLOW, Driver, RunOutcome, Screening
from nisse_dev.loop.help import ASK_EXPERT, DELEGATE, Helpers
@@ -92,6 +92,12 @@ _NEXT = re.compile(rf"(?:do|work on) {_ISSUE} (?:next|first|now)")
# A command that names its item applies to it, wherever it is said (L5).
_NAMED = re.compile(r"(go|approve|abandon|stop|allow) (?:([a-z0-9][a-z0-9-]*))?#(\d+)")
# At top level these need an item; without one they are ambiguous (L5).
# Added to a command's result when it commits (#129).
_COMMITTED = (
"committed locally only: nothing is published until you call "
"finish_milestone, which pushes the milestone's branch and opens or "
"updates its PR. There is no PR until then."
)
_NEEDS_ITEM = frozenset({"go", "approve", "abandon", "allow"})
# Near-misses for abandon: asked about, never taken for it or for a reply.
_NEAR_ABANDON = frozenset(
@@ -1717,6 +1723,9 @@ class LoopAgent:
screening = Screening("ask", self._ask_text(item, action, why), key)
else:
decision, screening = "ran", Screening("run")
if screening.decision == "run" and commits(action):
# #129: a local commit looked like publishing to the model.
screening = replace(screening, note=_COMMITTED)
self._verdict(item_id, "driver", call, verdict, decision, judged)
return screening
+3
View File
@@ -50,6 +50,7 @@ class Screening:
decision: Literal["run", "refuse", "ask"]
text: str = "" # the call's result when not run; for ask, the question
key: str = "" # for ask: what an allow would bind to (L3)
note: str = "" # for run: what the harness adds to the call's result
class Driver:
@@ -198,6 +199,8 @@ class Driver:
break
outcome = workspace.dispatch(call.tool, call.arguments)
result, ok = outcome.result, outcome.success
if screening.note:
result = f"{result}\n\n[harness] {screening.note}"
transcript.append(
ToolResult(call_id=call.call_id, result=result, success=ok)
)