Say a local commit publishes nothing (#129)
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:
1 parent
bd17c74c3a
commit
c4726d5673
4 files changed
+80
-3
No files matched your search
@@ -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``."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -12,8 +12,8 @@ from test_loop_milestones import item as the_item
|
||||
from test_loop_milestones import world as world # noqa: PLC0414 -- the fixture
|
||||
|
||||
from nisse_core.actions import Action
|
||||
from nisse_dev.canonical import ActionVerdict, ToolResult
|
||||
from nisse_dev.loop.actions import classify
|
||||
from nisse_dev.canonical import ActionVerdict, ToolCall, ToolResult
|
||||
from nisse_dev.loop.actions import classify, commits
|
||||
from nisse_dev.loop.items import ANSWER, loop_of
|
||||
|
||||
ROOT = "/srv/work/demo"
|
||||
@@ -355,3 +355,50 @@ def test_a_workspace_under_a_host_directory_is_still_the_workspace() -> None:
|
||||
Action("run_command", {"command": "rm -rf dist"}), "/home/ci/work/demo"
|
||||
)
|
||||
assert home.effect == "contained"
|
||||
|
||||
|
||||
# --- #129: a local commit is said to be one ------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("command", "commit"),
|
||||
[
|
||||
("git commit -m wip", True),
|
||||
("git add -A && git commit -qm 'fix'", True),
|
||||
("git -C sub commit -am x", True),
|
||||
("cd src && sudo git commit --amend --no-edit", True),
|
||||
("git status", False),
|
||||
("git log --grep commit", False),
|
||||
("echo git commit", False),
|
||||
],
|
||||
)
|
||||
def test_a_commit_is_recognised_anywhere_in_a_line(command: str, commit: bool) -> None:
|
||||
assert commits(Action("run_command", {"command": command})) is commit
|
||||
|
||||
|
||||
def test_a_local_commit_is_said_to_publish_nothing(world: dict[str, Any]) -> None:
|
||||
model = started(
|
||||
[
|
||||
call("write_file", path="w.py", content="W = 1\n"),
|
||||
call("run_command", command="git add -A && git commit -qm wip"),
|
||||
call("run_command", command="git status --short"),
|
||||
call("ask_human", question="?"),
|
||||
]
|
||||
)
|
||||
agent, transport = loop_agent(world, model)
|
||||
agent.intake()
|
||||
say(transport, world, "go")
|
||||
events = world["store"].transcript(the_item(world).id, "driver").read()
|
||||
calls = {
|
||||
e.event.call_id: e.event.arguments.get("command", "")
|
||||
for e in events
|
||||
if isinstance(e.event, ToolCall) and e.event.tool == "run_command"
|
||||
}
|
||||
results = {
|
||||
calls[e.event.call_id]: e.event.result
|
||||
for e in events
|
||||
if isinstance(e.event, ToolResult) and e.event.call_id in calls
|
||||
}
|
||||
assert "committed locally only" in results["git add -A && git commit -qm wip"]
|
||||
assert "There is no PR until then" in results["git add -A && git commit -qm wip"]
|
||||
assert "committed locally" not in results["git status --short"]
|
||||
Reference in new issue
Block a user