Give each role its own view of the transcript (M1.2)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m27s
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m27s
A model no longer reads another role's turns as its own (#78). The log stays one record per work item; what a role is shown is projected from it by Transcript.view() (views.py), which the planner, executor and helper now build their context from: - own events unchanged; - operator messages labelled with their channel, e.g. [operator, Matrix] or [operator, comment on PR #165]; - orchestrator facts (a new `fact` event) as [orchestrator] statements; - the executor sees a helper's handback as [helper report], not its turns; - a helper sees the approved plan and the executor's milestone work as one-line narration; - the planner sees no other role's work at all. The orchestrator now writes facts at the points the models must know about: work item opened, plan approved or rejected, milestone started, finished or stopped, published (with PR number and URL) or not, review comment answered, next milestone, task completed or abandoned. 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
51e144e26b
commit
3fba958958
15 files changed
+517
-13
No files matched your search
@@ -22,7 +22,7 @@ from dataclasses import dataclass
|
||||
from typing import Any, Protocol
|
||||
|
||||
from nisse_dev.cancel import Cancel
|
||||
from nisse_dev.canonical import GateOpened, PlanSubmitted, UserMessage
|
||||
from nisse_dev.canonical import Fact, GateOpened, PlanSubmitted, UserMessage
|
||||
from nisse_dev.gates import (
|
||||
Approved,
|
||||
Gate,
|
||||
@@ -457,6 +457,11 @@ class Orchestrator:
|
||||
item = self._store.active_item()
|
||||
return self._store.transcript(item.id if item else None, actor)
|
||||
|
||||
def _fact(self, text: str) -> None:
|
||||
"""Record what the models must know and cannot see for themselves:
|
||||
every role is shown it, labelled as the orchestrator's (L6)."""
|
||||
self._log("orchestrator").append(Fact(text=text))
|
||||
|
||||
def _close_item(self, outcome: str, cause: str) -> None:
|
||||
item = self._store.active_item()
|
||||
if item is not None and item.status == "open":
|
||||
@@ -601,6 +606,7 @@ class Orchestrator:
|
||||
self._store.set_active(task_id, cause=f"operator command: use {project}")
|
||||
# What the operator said before `use` is part of this task (D1).
|
||||
self._store.carry_idle_messages(task_id)
|
||||
self._fact(f"work item opened: project {project}, task {task_id}")
|
||||
if self._board is not None:
|
||||
try:
|
||||
self._data["board"] = {
|
||||
@@ -738,6 +744,10 @@ class Orchestrator:
|
||||
channel,
|
||||
f"{name} rev-{rev} rejected by {result.approver}: {result.feedback}",
|
||||
)
|
||||
self._fact(
|
||||
f"{name} plan rev-{rev} rejected by {result.approver}; "
|
||||
"their feedback is the operator's message above"
|
||||
)
|
||||
self._data["gate"] = None
|
||||
back = PLANNING_ARCH if name == "arch" else MILESTONE_PLANNING
|
||||
self._transition(back, cause=f"plan rejected ({name} rev-{rev})")
|
||||
@@ -754,6 +764,7 @@ class Orchestrator:
|
||||
rev=f"rev-{rev}",
|
||||
channel=result.channel,
|
||||
)
|
||||
self._fact(f"{name} plan rev-{rev} approved by {result.approver}")
|
||||
if name == "arch":
|
||||
self._data["gate"] = None
|
||||
self._transition(
|
||||
@@ -774,6 +785,7 @@ class Orchestrator:
|
||||
)
|
||||
|
||||
def _execute_milestone(self, milestone: int) -> None:
|
||||
self._fact(f"milestone {milestone} started")
|
||||
if self._executor is None:
|
||||
# The stub completes at once, so it still leaves EXECUTING.
|
||||
self._transport.send_message(f"[stub] executing milestone {milestone}")
|
||||
@@ -783,6 +795,7 @@ class Orchestrator:
|
||||
with self._long_work():
|
||||
outcome = self._executor.run(self._log("executor"), milestone)
|
||||
if outcome.status == "stopped":
|
||||
self._fact(f"milestone {milestone} stopped by the operator")
|
||||
self._halt_for_stop(
|
||||
what=f"milestone {milestone}",
|
||||
note=outcome.note,
|
||||
@@ -794,9 +807,11 @@ class Orchestrator:
|
||||
)
|
||||
return
|
||||
if outcome.status == "completed":
|
||||
self._fact(f"milestone {milestone} finished: {outcome.note}")
|
||||
self._transport.send_message(f"milestone {milestone} done: {outcome.note}")
|
||||
self._publish_milestone(milestone)
|
||||
return
|
||||
self._fact(f"milestone {milestone} {outcome.status}: {outcome.note}")
|
||||
self._await_input(
|
||||
cause=f"milestone {milestone} {outcome.status}",
|
||||
prompt=f"milestone {milestone} stopped ({outcome.status}): {outcome.note}",
|
||||
@@ -860,8 +875,11 @@ class Orchestrator:
|
||||
):
|
||||
return # not taken: a watch attached later still sees it
|
||||
self._take_comment(board_state, comment)
|
||||
issue = (board_state or {}).get("issue")
|
||||
self._log("operator").append(
|
||||
UserMessage(text=comment.body, source=f"gitea:comment:{comment.id}")
|
||||
UserMessage(
|
||||
text=comment.body, source=f"gitea:issue:{issue}:comment:{comment.id}"
|
||||
)
|
||||
)
|
||||
self._apply_gate_reply(
|
||||
sender=comment.author,
|
||||
@@ -916,7 +934,10 @@ class Orchestrator:
|
||||
# The comment enters the transcript, so the executor sees review
|
||||
# feedback as conversation rather than as an out-of-band instruction.
|
||||
self._log("operator").append(
|
||||
UserMessage(text=comment.body, source=f"gitea:comment:{comment.id}")
|
||||
UserMessage(
|
||||
text=comment.body,
|
||||
source=f"gitea:pr:{pr.get('number')}:comment:{comment.id}",
|
||||
)
|
||||
)
|
||||
try:
|
||||
with self._long_work():
|
||||
@@ -959,6 +980,7 @@ class Orchestrator:
|
||||
if sha
|
||||
else "no code change was needed for that comment"
|
||||
)
|
||||
self._fact(f"review comment {comment.id}: {note}")
|
||||
self._transport.send_message(note)
|
||||
if pr.get("number"):
|
||||
self._publisher.comment(int(pr["number"]), note)
|
||||
@@ -973,6 +995,7 @@ class Orchestrator:
|
||||
def _publish_milestone(self, milestone: int) -> None:
|
||||
if self._publisher is None:
|
||||
# No PR to wait on, but PR_OPEN's commands still lead on.
|
||||
self._fact(f"milestone {milestone} done; no publisher, so no PR")
|
||||
self._transition(PR_OPEN, cause=f"[stub] milestone {milestone} done")
|
||||
self._transport.send_message(
|
||||
f"[stub] milestone {milestone} done; no publisher, so no PR — "
|
||||
@@ -991,6 +1014,7 @@ class Orchestrator:
|
||||
# The intent log already records what was in flight, so boot
|
||||
# reconciliation can settle it against Gitea. Gate rather than
|
||||
# retry blindly into a possible duplicate.
|
||||
self._fact(f"publishing milestone {milestone} failed: {exc}")
|
||||
self._await_input(
|
||||
cause=f"publishing milestone {milestone} failed: {exc}",
|
||||
prompt=f"could not publish milestone {milestone}: {exc}",
|
||||
@@ -1000,6 +1024,7 @@ class Orchestrator:
|
||||
if result.pull_request is None:
|
||||
# Nothing left EXECUTING behind: the operator decides what next,
|
||||
# and answering returns to the still-approved plan.
|
||||
self._fact(f"milestone {milestone} not published: {result.note}")
|
||||
self._await_input(
|
||||
cause=f"milestone {milestone}: {result.note}",
|
||||
prompt=(
|
||||
@@ -1009,6 +1034,10 @@ class Orchestrator:
|
||||
pending={"kind": "milestone_stopped", "milestone": milestone},
|
||||
)
|
||||
return
|
||||
self._fact(
|
||||
f"milestone {milestone} published: {result.note} "
|
||||
f"(PR #{result.pull_request.number}, {result.pull_request.url})"
|
||||
)
|
||||
self._unwatch("plan") # the gate is behind us; the PR is the thread now
|
||||
self._data["pr"] = {
|
||||
"number": result.pull_request.number,
|
||||
@@ -1036,6 +1065,7 @@ class Orchestrator:
|
||||
pr = self._data.get("pr") or {}
|
||||
project = self._project
|
||||
self.stop_watching()
|
||||
self._fact(f"work item abandoned by {sender}")
|
||||
self._close_item("abandoned", f"operator: abandon ({sender})")
|
||||
self._data = {**_fresh_data()}
|
||||
self._project = None
|
||||
@@ -1111,7 +1141,14 @@ class Orchestrator:
|
||||
command = command_form(text)
|
||||
if command == "next milestone":
|
||||
self._unwatch("review")
|
||||
pr = self._data.get("pr") or {}
|
||||
self._data["milestone"] = int(self._data.get("milestone", 1)) + 1
|
||||
self._fact(
|
||||
f"PR #{pr.get('number')} left for review; planning milestone "
|
||||
f"{self._data['milestone']}"
|
||||
if pr.get("number")
|
||||
else f"planning milestone {self._data['milestone']}"
|
||||
)
|
||||
self._data["gate"] = None
|
||||
self._data["pr"] = None
|
||||
self._transition(MILESTONE_PLANNING, cause="operator: next milestone")
|
||||
@@ -1119,6 +1156,7 @@ class Orchestrator:
|
||||
elif command == "project complete":
|
||||
board_state = self._data.get("board") or {}
|
||||
self.stop_watching()
|
||||
self._fact(f"work item completed: {sender} said project complete")
|
||||
self._close_item("completed", "operator: project complete")
|
||||
self._data = {**_fresh_data()}
|
||||
self._project = None
|
||||
|
||||
Reference in new issue
Block a user