Development Plan M1.2, as agreed in discussion. Part of #78 (M1.1 did the scoping; this does the attribution).
What changes
The canonical log stays one record per work item. What each model is shown is now a projection of it — Transcript.view() in the new views.py — and the planner, executor and helper build their context from that instead of the raw log. read() stays raw for the record, cursors and recovery.
Viewer
Sees
planner
operator messages, its own turns, facts. No executor or helper work.
executor
(from the approved plan on) operator messages, its own turns, facts, a helper's handback as [helper report, <status>] …
helper
[approved plan] …, operator messages, facts, its own turns, the executor's work as narration: [executor] run_command(make) → failed: …
expert
unchanged (stateless; sees only the question)
Everything that isn't the viewer's own reaches it as a labelled user message: [orchestrator] …, [operator, Matrix] …, [operator, comment on PR #165] …, [operator, Matrix, sent while idle] …. Projected events keep their log ids, so after cursors and compaction ranges still line up.
Facts. A new canonical fact event (already in L7's table), written by the orchestrator when:
a work item opens;
a plan is approved or rejected;
a milestone starts, finishes, stops or escalates;
a milestone is published (PR number and URL) or not, or publishing fails;
a review comment is answered;
the work moves to the next milestone;
the task is completed or abandoned.
Both renderers render a raw fact as [orchestrator] … too. nisse_dev.dump shows them as FACT:.
PlanSubmitted is no longer rendered as assistant text: the planner's own submit_plan call already carries the plan. It stays in the log, for the executor's scope and the dump.
Gitea comment sources now carry their number (gitea:pr:165:comment:7, gitea:issue:9:comment:4) so the label can say where a comment was made.
Docs:
Architecture L6 now spells out the helper's view (the plan and narration) and the label rule;
M1.2 is marked Done in the Development Plan.
Tests
New tests/test_views.py:
Run 2's scenario: after milestone 1 and next milestone, the planner's rendered request holds only its own submit_plan calls as assistant turns. It holds the milestone-finished fact, PR #165 and its URL, the approval fact and the carried idle message, and none of the executor's text.
Executor:
sees milestone 1 started;
sees a review comment labelled comment on PR #165;
still sees its own earlier turn as its own.
Helper report: the executor sees the report and not the helper's turns.
Helper: sees the plan and narration; its own calls are unchanged, and the executor's calls are not shown as calls.
Planner: sees nothing of other roles.
Cursors: a view keeps log ids, so cursors work.
Labels and closing:
both renderers label facts;
operator labels are right per channel;
closing a task records its fact.
Plus a dump test, and two existing assertions updated.
Lint, unit (524), integration (19 passed, 7 opt-in skipped) and the Quint tests all pass locally.
Development Plan **M1.2**, as agreed in discussion. Part of #78 (M1.1 did the scoping; this does the attribution).
## What changes
The canonical log stays one record per work item. What each model is shown is now a **projection** of it — `Transcript.view()` in the new `views.py` — and the planner, executor and helper build their context from that instead of the raw log. `read()` stays raw for the record, cursors and recovery.
| Viewer | Sees |
|---|---|
| planner | operator messages, its own turns, facts. **No executor or helper work.** |
| executor | (from the approved plan on) operator messages, its own turns, facts, a helper's handback as `[helper report, <status>] …` |
| helper | `[approved plan] …`, operator messages, facts, its own turns, the executor's work as narration: `[executor] run_command(make) → failed: …` |
| expert | unchanged (stateless; sees only the question) |
Everything that isn't the viewer's own reaches it as a labelled **user** message: `[orchestrator] …`, `[operator, Matrix] …`, `[operator, comment on PR #165] …`, `[operator, Matrix, sent while idle] …`. Projected events keep their log ids, so `after` cursors and compaction ranges still line up.
**Facts.** A new canonical `fact` event (already in L7's table), written by the orchestrator when:
- a work item opens;
- a plan is approved or rejected;
- a milestone starts, finishes, stops or escalates;
- a milestone is published (PR number and URL) or not, or publishing fails;
- a review comment is answered;
- the work moves to the next milestone;
- the task is completed or abandoned.
Both renderers render a raw `fact` as `[orchestrator] …` too. `nisse_dev.dump` shows them as `FACT:`.
`PlanSubmitted` is no longer rendered as assistant text: the planner's own `submit_plan` call already carries the plan. It stays in the log, for the executor's scope and the dump.
Gitea comment sources now carry their number (`gitea:pr:165:comment:7`, `gitea:issue:9:comment:4`) so the label can say where a comment was made.
**Docs:**
- Architecture L6 now spells out the helper's view (the plan and narration) and the label rule;
- M1.2 is marked Done in the Development Plan.
## Tests
New `tests/test_views.py`:
- **Run 2's scenario:** after milestone 1 and `next milestone`, the planner's rendered request holds only its own `submit_plan` calls as assistant turns. It holds the milestone-finished fact, PR #165 and its URL, the approval fact and the carried idle message, and none of the executor's text.
- **Executor:**
- sees `milestone 1 started`;
- sees a review comment labelled `comment on PR #165`;
- still sees its own earlier turn as its own.
- **Helper report:** the executor sees the report and not the helper's turns.
- **Helper:** sees the plan and narration; its own calls are unchanged, and the executor's calls are not shown as calls.
- **Planner:** sees nothing of other roles.
- **Cursors:** a view keeps log ids, so cursors work.
- **Labels and closing:**
- both renderers label facts;
- operator labels are right per channel;
- closing a task records its fact.
Plus a dump test, and two existing assertions updated.
Lint, unit (524), integration (19 passed, 7 opt-in skipped) and the Quint tests all pass locally.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Development Plan M1.2, as agreed in discussion. Part of #78 (M1.1 did the scoping; this does the attribution).
What changes
The canonical log stays one record per work item. What each model is shown is now a projection of it —
Transcript.view()in the newviews.py— and the planner, executor and helper build their context from that instead of the raw log.read()stays raw for the record, cursors and recovery.[helper report, <status>] …[approved plan] …, operator messages, facts, its own turns, the executor's work as narration:[executor] run_command(make) → failed: …Everything that isn't the viewer's own reaches it as a labelled user message:
[orchestrator] …,[operator, Matrix] …,[operator, comment on PR #165] …,[operator, Matrix, sent while idle] …. Projected events keep their log ids, soaftercursors and compaction ranges still line up.Facts. A new canonical
factevent (already in L7's table), written by the orchestrator when:Both renderers render a raw
factas[orchestrator] …too.nisse_dev.dumpshows them asFACT:.PlanSubmittedis no longer rendered as assistant text: the planner's ownsubmit_plancall already carries the plan. It stays in the log, for the executor's scope and the dump.Gitea comment sources now carry their number (
gitea:pr:165:comment:7,gitea:issue:9:comment:4) so the label can say where a comment was made.Docs:
Tests
New
tests/test_views.py:next milestone, the planner's rendered request holds only its ownsubmit_plancalls as assistant turns. It holds the milestone-finished fact, PR #165 and its URL, the approval fact and the carried idle message, and none of the executor's text.milestone 1 started;comment on PR #165;Plus a dump test, and two existing assertions updated.
Lint, unit (524), integration (19 passed, 7 opt-in skipped) and the Quint tests all pass locally.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE