From 632fd87b0f4e39113d332c2e0888e50e2b26a29d Mon Sep 17 00:00:00 2001 From: SSFSKIM Date: Sat, 17 Oct 2026 20:40:31 -0700 Subject: [PATCH 1/3] to-human: the report view becomes its own plugin, so it folds other sessions' sminos messages (doperpowers 7.171.0, to-human 0.1.0) The engine runs none of a plugin's render hooks on a row that plugin submitted (debug log: 'ui.render skipped: re-entry (the plugin's own code raised it)'). The bridge delivers sminos messages as doperpowers prompts, so the view, a doperpowers mod, never saw them and they drew as the person's prompts in the report. The view moves to to-human/, a plugin that lists doperpowers under dependencies: at equal rank the dependent sits outside, so it still sees the ask mod's rows before the ask draws them. Its band now draws under what the hooks beneath draw (the ask's open questions) instead of replacing it. A prompt row is read by its sminos frame: from human it stays, from another session it is working record. Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/marketplace.json | 14 +++- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- .github/ci-kit.yml | 11 +++ .version-bump.json | 20 ++++- CLAUDE.md | 5 +- hooks/hooks.json | 2 +- hooks/mods/ask.tsx | 8 +- hooks/mods/register.tsx | 6 -- package.json | 2 +- skills/sminos/scripts/sminos.py | 2 +- skills/sminos/scripts/sminos_marks.py | 2 +- tests/sminos/marks_parity.py | 2 +- to-human/.claude-plugin/plugin.json | 20 +++++ to-human/hooks/hooks.json | 6 ++ to-human/hooks/mods/register.tsx | 16 ++++ {hooks => to-human/hooks}/mods/to-human.tsx | 58 +++++++++++---- to-human/tests/mods/run.sh | 8 ++ .../tests}/mods/to-human-native.test.ts | 0 .../tests}/mods/to-human.test.ts | 73 +++++++++++++++++-- 20 files changed, 215 insertions(+), 44 deletions(-) create mode 100644 to-human/.claude-plugin/plugin.json create mode 100644 to-human/hooks/hooks.json create mode 100644 to-human/hooks/mods/register.tsx rename {hooks => to-human/hooks}/mods/to-human.tsx (95%) create mode 100755 to-human/tests/mods/run.sh rename {tests => to-human/tests}/mods/to-human-native.test.ts (100%) rename {tests => to-human/tests}/mods/to-human.test.ts (90%) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5d6e60d5d6b..af8e0acd88a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "doperpowers", "description": "Emerges by humans, Converges by Agent", - "version": "7.170.0", + "version": "7.171.0", "source": "./", "author": { "name": "SSFSKIM", @@ -42,6 +42,18 @@ }, "homepage": "https://github.com/SSFSKIM/doperpowers/tree/main/devcloud", "license": "MIT" + }, + { + "name": "to-human", + "description": "The report view under the to-human output style: messages fold to their spans, the working record (tool calls, agent and other sessions' messages) behind [ working record ] buttons", + "version": "0.1.0", + "source": "./to-human", + "author": { + "name": "SSFSKIM", + "email": "supremekim17@gmail.com" + }, + "homepage": "https://github.com/SSFSKIM/doperpowers/tree/main/to-human", + "license": "MIT" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 0e5c472b5e3..a5119826e08 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "doperpowers", "description": "Emerges by humans, Converges by Agents", - "version": "7.170.0", + "version": "7.171.0", "author": { "name": "SSFSKIM", "email": "supremekim17@gmail.com" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 209663b5b34..22072c0a9ba 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "doperpowers", - "version": "7.170.0", + "version": "7.171.0", "description": "One software-development methodology for coding agents, sized by the work: brainstorm, design, spec, TDD, review, ship — one spec that carries its plan of work as milestones: one milestone runs in order from the spec, several run a fresh subagent per milestone reviewed at each dependency frontier, and the same method runs unattended on the board loop.", "author": { "name": "SSFSKIM", diff --git a/.github/ci-kit.yml b/.github/ci-kit.yml index 1c5b19b85fe..4a93d57a3b2 100644 --- a/.github/ci-kit.yml +++ b/.github/ci-kit.yml @@ -88,6 +88,17 @@ gates: timeout: 15 paths: ["devcloud/**", ".github/**"] + # The to-human plugin's mods (the report view), installed as the mods gate is. + - name: to-human-mods + run: | + for i in 1 2 3; do + timeout 300 npm i -g @anthropic-ai/claude-code bun && break + [ "$i" = 3 ] && exit 1; sleep 10 + done + bash to-human/tests/mods/run.sh + timeout: 15 + paths: ["to-human/**", ".github/**"] + - name: triaging-feedback run: cd application-agents/triaging-feedback && npm ci && npm test timeout: 10 diff --git a/.version-bump.json b/.version-bump.json index 6261898dd99..5e9797b7567 100644 --- a/.version-bump.json +++ b/.version-bump.json @@ -31,7 +31,8 @@ "scripts/bump-version.sh", ".claude", ".worktrees", - "devcloud" + "devcloud", + "to-human" ] } }, @@ -51,6 +52,23 @@ "root": "devcloud", "exclude": [] } + }, + { + "name": "to-human", + "files": [ + { + "path": "to-human/.claude-plugin/plugin.json", + "field": "version" + }, + { + "path": ".claude-plugin/marketplace.json", + "field": "plugins[name=to-human].version" + } + ], + "audit": { + "root": "to-human", + "exclude": [] + } } ] } diff --git a/CLAUDE.md b/CLAUDE.md index 5e3d7cc4cbf..fe3b5d9c1a6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,10 +78,11 @@ CLI adapter, Codex portal packaging — are automatically irrelevant. | Path | What it is | |------|-----------| | `agents/` | Registered agents, addressed as `doperpowers:`: `critique` (design critic, debated over SendMessage); `reviewer-low` / `reviewer-medium` / `reviewer-high` (one rubric body, pinned to sol/high, sol/xhigh, astra/high through the local gateway — the single-reviewer rungs of `review-code`, dispatchable from any session or subagent; every reviewer agent, and `adversarial-reviewer`, blocks `Agent` and `Skill` so a reviewer cannot route back into `review-code` and start a review inside its own) and `adversarial-reviewer` (its spec/plan challenge reviewer); `qa-loop` (opus/high) is the board's review loop — the seat that owns the ticket (an Architect or an Executor) dispatches it on the PR it opened and stays bound through review, and it owns the engine rounds, the compliance audit, triage, fix waves, the merge, and the trail, returning only a spec conflict, a design gap, or a dismissal request; it blocks `Skill` so no repository instruction can route it into a review skill, and keeps `Agent` for the reviewers and fixers it dispatches (a PR nobody owns draws a review stand-in seat, which dispatches the same agent); `plan-executor` (opus/high) executes a spec's Plan of Work — or a ticket body — for the session that authored it, messaging that session at the checkpoints it judges meaningful and returning at completion or a genuine block — SDE controller when the Plan of Work has several milestones, sequential for one; dispatched by the board Architect past its build edge, and by execspec's step 4 in an interactive session (a one-milestone spec in an interactive session is run inline by its author); `task-executor` (opus/high) / `task-reviewer` (sol/xhigh) are subagent-driven-execution's per-milestone workers — the invariant worker contracts live here so a controller's dispatch carries only the task's paths and context (the reviewer is read-only and can neither spawn agents nor load skills; the executor keeps `Agent` for orientation fan-out, its body rules out spawning a reviewer, and it may be downgraded to sonnet per dispatch). Both executors run detached from the human: `AskUserQuestion` is disallowed in their definitions, and whatever needs an input only the human or the work's owners can give — environmental friction, an approval, a fork — is escalated up the dispatch chain (the task-executor's return to its controller, the plan-executor's checkpoint message or escalation return to its dispatching session) until the main session puts it to the human and relays the answer down. The three pins — plan-executor and task-executor opus/high, task-reviewer sol/xhigh — change only on an explicit instruction, by edit or by dispatch override, whatever model wrote the spec (usually the frontier one): the frontier's work is the design and the spec; running the plan and building its milestones is close-frontier work. The panel workflow reaches the reviewers by `agentType`, overriding model and effort per rung. | -| `skills/` | The core product — one dir per skill, each with `SKILL.md` (frontmatter `name` + `description` that drives auto-trigger). 15 skills. `brainstorming` closes and presents the design; `execspec` writes the living spec from the approved design, reviews it, and executes it (its `references/living-spec.md` is the document's doctrine, read by SDE, decomposing, and the board protocols). Worker protocols for the board loop (architect / implement / spike / the review stand-in) are plain files under `issue-tracker/references/`, deliberately not skills — nothing invokes them by name; the dispatcher pins their path into the spawn bootstrap. The review loop lives there too: `references/review-loop.md` is its operator manual, `review-standin-protocol.md` and `review-standin-bootstrap.md` the stand-in's, and `scripts/review-dispatch.sh` the trigger for the reviews nobody owns. `sminos` is the project family: a committed `.smns` names a project, every session working under it (spawned seat or one the human opened) is a member with no join step, and what crosses sessions — `say` to peers (untagged is a record, `@alias` delivers, `@all` wakes every live member), a member's `` to the human, joining, an in-charge line, spawning a sibling, retiring — is one event in the project's append-only log, one file per writer under `$SMINOS_HOME/projects//log/`. With a remote in `.smns` the log is a git working tree mirrored through it, and `sminos daemon` (one per machine; `daemon install` writes a launchd agent on macOS, a systemd user unit on Linux) syncs the mirror, delivers remote messages to local members, and publishes their state, so a project spans machines. A project whose `.smns` names a desk hub instead (`project init --hub`, on a machine enrolled by `sminos hub enroll`, which reads the enrollment code — it carries the org key — from a hidden prompt or stdin and refuses it as an argument; `hub show` prints the machine's identity, never a secret) syncs through the hub alone, never through git: `daemon run` supervises `daemon uplink`, a child process holding the hub's stream, which uploads the machine's own writers sealed with authentication codes and verifies every event the hub sends before it enters the log, and the machine writes under its enrolled name. Over the same stream the uplink serves the desk's session plane and its RPCs, the daemon being the execution boundary: presence from the engine's session records, each subscribed session's transcript and its bridge's stream joined in one order, the bridges' pending requests and their settlements, and the outcome of each delivery of a message from outside the machine; an RPC (steer, interrupt, answer, decide, model, compact, spawn, retire, resume) runs only once its code, the machine, project, and session it names, the at-most-once ledger (`daemon/rpcs.json`), its age, the machine's policy, and, for an answer or a decision, the digest of the request still pending all hold, and its result is sent again until the hub acknowledges it. The policy (`daemon/policy.json`, set by `sminos daemon policy [--read-only on|off] [--ceiling MODE|settings]`) can make the machine read-only — no RPC runs, nothing from outside is delivered — and caps a spawn's permission mode; Resume runs `claude --bg --resume ` with no other flag once no process of the machine holds the session, and stops any copy the engine starts instead. Such a project's remote keeps only the log's archive, which the one machine the hub designates for it writes on a schedule (`SMINOS_ARCHIVE_EVERY`, 15 minutes) from a clone at `projects//archive/`, never read back into the log. The CLI is `scripts/sminos.py` with `sminos_daemon.py` (mirror and delivery), `sminos_hub.py` (enrollment), `sminos_uplink.py` and `sminos_hublog.py` (the hub's stream, and one project's log plane over it), `sminos_plane.py` (the session plane: presence and subscriptions, each transcript's offset index and its worker thread, so a subscription pages a session's whole history — an ended one's too — off the uplink's thread), `sminos_jobs.py` (the subagents' status, background jobs and MCP names derived from the rows, and `output`'s containment; pinned with MAWS's fake machine by `tests/sminos/dx4-jobs.json`; plan `docs/doperpowers/plans/2026-10-17-desk-dc8-machine.md`), `sminos_requests.py` and `sminos_outcomes.py` (pending requests and their settlements, and delivery outcomes, as the hub's sealed messages), `sminos_policy.py` (the machine's policy and its known checkouts), `sminos_rpc.py` (the RPCs: their checks, the ledger, execution; plan `docs/doperpowers/plans/2026-10-15-desk-dc7-controls.md`), `sminos_codes.py` (the codes, pinned to the hub's vectors by `tests/sminos/codes_parity.py`), `sminos_archive.py` (the archive), `sminos_marks.py` (the to-human mark grammar, ported from `hooks/mods/to-human.tsx` and pinned by `tests/sminos/marks_parity.py`), `sminos_bridge.py` (the child the bridge mod spawns, serving the session's endpoint at `$SMINOS_HOME/bridge/.sock`, and `sminos watch`), `sminos_control.py` (`steer`, `interrupt`, `model`, `compact`, `decide`, `answer`, and a delivery's last hop to a live session: the bridge, else a `--bg` job's supervisor `reply` for a human's message, else the inbox frame), and the chart and TUI; a session the bridge mod runs in is watched and driven from any terminal on the machine with `sminos watch|steer|interrupt|model|compact|decide|answer `, its protocol DX5 in `docs/doperpowers/plans/2026-10-15-arkho-desk-c2-bridge.md`; tests `tests/sminos/run-sminos-tests.sh`, which isolate `HOME` and `SMINOS_HOME` — a working copy's CLI run against the real `~/.claude/sminos` performs the one-way import of the old per-host `chats/` on its first call. Design in `docs/doperpowers/specs/2026-10-13-smns-project-log-design.md`; the hub side in `docs/doperpowers/plans/2026-10-15-desk-dc4-uplink.md`, a child of the desk's composite design in MAWS. | +| `skills/` | The core product — one dir per skill, each with `SKILL.md` (frontmatter `name` + `description` that drives auto-trigger). 15 skills. `brainstorming` closes and presents the design; `execspec` writes the living spec from the approved design, reviews it, and executes it (its `references/living-spec.md` is the document's doctrine, read by SDE, decomposing, and the board protocols). Worker protocols for the board loop (architect / implement / spike / the review stand-in) are plain files under `issue-tracker/references/`, deliberately not skills — nothing invokes them by name; the dispatcher pins their path into the spawn bootstrap. The review loop lives there too: `references/review-loop.md` is its operator manual, `review-standin-protocol.md` and `review-standin-bootstrap.md` the stand-in's, and `scripts/review-dispatch.sh` the trigger for the reviews nobody owns. `sminos` is the project family: a committed `.smns` names a project, every session working under it (spawned seat or one the human opened) is a member with no join step, and what crosses sessions — `say` to peers (untagged is a record, `@alias` delivers, `@all` wakes every live member), a member's `` to the human, joining, an in-charge line, spawning a sibling, retiring — is one event in the project's append-only log, one file per writer under `$SMINOS_HOME/projects//log/`. With a remote in `.smns` the log is a git working tree mirrored through it, and `sminos daemon` (one per machine; `daemon install` writes a launchd agent on macOS, a systemd user unit on Linux) syncs the mirror, delivers remote messages to local members, and publishes their state, so a project spans machines. A project whose `.smns` names a desk hub instead (`project init --hub`, on a machine enrolled by `sminos hub enroll`, which reads the enrollment code — it carries the org key — from a hidden prompt or stdin and refuses it as an argument; `hub show` prints the machine's identity, never a secret) syncs through the hub alone, never through git: `daemon run` supervises `daemon uplink`, a child process holding the hub's stream, which uploads the machine's own writers sealed with authentication codes and verifies every event the hub sends before it enters the log, and the machine writes under its enrolled name. Over the same stream the uplink serves the desk's session plane and its RPCs, the daemon being the execution boundary: presence from the engine's session records, each subscribed session's transcript and its bridge's stream joined in one order, the bridges' pending requests and their settlements, and the outcome of each delivery of a message from outside the machine; an RPC (steer, interrupt, answer, decide, model, compact, spawn, retire, resume) runs only once its code, the machine, project, and session it names, the at-most-once ledger (`daemon/rpcs.json`), its age, the machine's policy, and, for an answer or a decision, the digest of the request still pending all hold, and its result is sent again until the hub acknowledges it. The policy (`daemon/policy.json`, set by `sminos daemon policy [--read-only on|off] [--ceiling MODE|settings]`) can make the machine read-only — no RPC runs, nothing from outside is delivered — and caps a spawn's permission mode; Resume runs `claude --bg --resume ` with no other flag once no process of the machine holds the session, and stops any copy the engine starts instead. Such a project's remote keeps only the log's archive, which the one machine the hub designates for it writes on a schedule (`SMINOS_ARCHIVE_EVERY`, 15 minutes) from a clone at `projects//archive/`, never read back into the log. The CLI is `scripts/sminos.py` with `sminos_daemon.py` (mirror and delivery), `sminos_hub.py` (enrollment), `sminos_uplink.py` and `sminos_hublog.py` (the hub's stream, and one project's log plane over it), `sminos_plane.py` (the session plane: presence and subscriptions, each transcript's offset index and its worker thread, so a subscription pages a session's whole history — an ended one's too — off the uplink's thread), `sminos_jobs.py` (the subagents' status, background jobs and MCP names derived from the rows, and `output`'s containment; pinned with MAWS's fake machine by `tests/sminos/dx4-jobs.json`; plan `docs/doperpowers/plans/2026-10-17-desk-dc8-machine.md`), `sminos_requests.py` and `sminos_outcomes.py` (pending requests and their settlements, and delivery outcomes, as the hub's sealed messages), `sminos_policy.py` (the machine's policy and its known checkouts), `sminos_rpc.py` (the RPCs: their checks, the ledger, execution; plan `docs/doperpowers/plans/2026-10-15-desk-dc7-controls.md`), `sminos_codes.py` (the codes, pinned to the hub's vectors by `tests/sminos/codes_parity.py`), `sminos_archive.py` (the archive), `sminos_marks.py` (the to-human mark grammar, ported from `to-human/hooks/mods/to-human.tsx` and pinned by `tests/sminos/marks_parity.py`), `sminos_bridge.py` (the child the bridge mod spawns, serving the session's endpoint at `$SMINOS_HOME/bridge/.sock`, and `sminos watch`), `sminos_control.py` (`steer`, `interrupt`, `model`, `compact`, `decide`, `answer`, and a delivery's last hop to a live session: the bridge, else a `--bg` job's supervisor `reply` for a human's message, else the inbox frame), and the chart and TUI; a session the bridge mod runs in is watched and driven from any terminal on the machine with `sminos watch|steer|interrupt|model|compact|decide|answer `, its protocol DX5 in `docs/doperpowers/plans/2026-10-15-arkho-desk-c2-bridge.md`; tests `tests/sminos/run-sminos-tests.sh`, which isolate `HOME` and `SMINOS_HOME` — a working copy's CLI run against the real `~/.claude/sminos` performs the one-way import of the old per-host `chats/` on its first call. Design in `docs/doperpowers/specs/2026-10-13-smns-project-log-design.md`; the hub side in `docs/doperpowers/plans/2026-10-15-desk-dc4-uplink.md`, a child of the desk's composite design in MAWS. | | `application-agents/` | Deployed agent applications that are not skills — nothing invokes them by name from a session. `triaging-feedback/`: the feedback→triage poller (TypeScript, its own `npm test`), its worker protocol, and setup docs; the worker-host under `infra/` runs it. | -| `hooks/` | The `kairos` proactive mode: `kairos.sh` on `SessionStart` injects the `kairos` skill's body at startup and after every compaction for a session in the mode — launched with `KAIROS=1`, or switched on mid-session by typing `/kairos`, which `kairos-toggle.sh` (on `UserPromptExpansion`) records as a flag file under `~/.claude/kairos/`; `/kairos off` removes it. Every other session gets nothing. `smns-record.sh` on `Stop` and `SessionStart` passes the payload to `sminos record`, which joins the session to its project and exports what it sent its human since the last export (its `` spans, code-aware, and its async asks and their answers) into the project's log; it is bounded (5 s, `SMNS_RECORD_TIMEOUT`), exits 0 on any failure, and does nothing outside a project or in sminos's own internal forks (`SMNS_INTERNAL`). `mods/` holds the plugin's function hooks ("mods"): `hooks.json` names `mods/register.tsx` under `modules`, which the engine loads only under `CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1` and ignores elsewhere. `to-human.tsx` is the consumer of the `to-human` output style on every surface (``, with old ``, ``, `` and `` marks read as aliases of it; tags in inline code or fenced blocks stay literal; an assistant message draws as its marked spans, each body a `Markdown` element on every surface (a render answer to a remote surface carries one props and only the first engine node draws there; on the terminal the engine's block carries its row margin between label and body), and the view's rows stand a line apart as the engine's do; a run of working record — unmarked messages, tool calls and their results — between two rows the human reads folds behind one `[ working record ]` button; a question asked through `AskUserQuestion` or the ask mod's tool stays in the report, and so do agent spawns through `Agent` or `Workflow` and the notification that one failed, and an API error that ends the main turn (sent and received agent messages and the other finish notifications fold into the working record); buttons unfold a run, a message, or the whole transcript; on the terminal a span's link to a local file, `[name](/abs/path#L42)`, draws behind a file glyph as a `file:` link, and a plain click on it opens VS Code at the line, a cmd-click staying the terminal's); `to-human-stream.sh` on `MessageDisplay` covers the stretch the module cannot reach, drawing the same labels flush by flush while the message streams (the engine mounts an `AssistantMessage` only once a block is whole), and the module's parser reads either that form or the raw tags; `kairos.tsx` draws a `[ kairos off ]` / `[ kairos on ]` button in the prompt footer that runs `/kairos` or `/kairos off` on a click, reading the mode's state from the shell hooks and never writing it; `agents.tsx` adds an `[ N subagents ]` button beside the kairos switch (registered ahead of it, wrapping its tree) from the session's first subagent on, which opens a pane drawing the session's agents as the tree they spawned in (`$.agent.list()`'s `parentId`), and a press on a row draws that agent's transcript file beneath the tree, following its tail while it runs. `ask.tsx` registers `mcp__doperpowers__AskUserQuestion`, the model's one way to ask the human in an interactive session: the built-in's input plus `async`. Blocking, it opens the engine's own dialog (a `$.ui.ask` raised inside the tool's hook and widened to the full question set by the mod's own `tool.call` hook on `AskUserQuestion`, correlated by a token in the header) and returns the engine's text; async, it returns at once, the questions wait behind `[ answer ]` in the call's row and in an `N open questions` band above the prompt, and the answer reaches the model as `context` on its next tool result, or as a plugin prompt when the session is idle, or with the human's next prompt after a stop. The built-in is deferred and refused while the tool exists; design in `docs/doperpowers/specs/2026-10-13-ask-async-design.md`. `compact.tsx` registers two tools the model calls, `mcp__doperpowers__context_usage` (the status line's context figures and where the engine compacts on its own, at no cost) and `mcp__doperpowers__compact` (the model compacting its own context at a checkpoint of its choosing): the engine compacts only between turns, so the tool answers that the compaction runs when the turn ends and the model ends its turn; at the `turn.complete` of a main-conversation turn the model answered the mod calls `$.session.compact` (the `/compact` path: one model call over the live conversation, so the model that chose to compact writes the summary, the PreCompact hook's instructions merged in) and submits the `resume` prompt the model wrote, whatever the outcome, so an unwatched session never stalls; nothing is aborted, so background shells, monitors, MCP servers and subagents run on (a `$.turn.abort` variant was rejected: it kills Monitors, whose shell runner gets no `caller` and so does not spare `turn-abort`). Interactive sessions only (headless has no compactor a plugin can call); a subagent's call to either is refused (`context_usage` would read the parent's figures). The engine hands every subagent every MCP tool and a hook cannot filter per agent, so the agent definitions this repo owns, and the user's own under `~/.claude/agents/`, withhold both through `disallowedTools`; built-in types (general-purpose, Explore) and forks still list them. The engine admits one unmatched registration of an event per module, and `$` may only be passed to functions in the same file, so the mod takes `session.start` and `turn.complete` with matchers (`isInteractive: true`, `reason: "answer"`) rather than sharing kairos's and agents' registrations. `interim.ts` drops, at `prompt.submit`, an agent's task notification the engine marks interim (it stopped with a background shell, monitor or workflow of its own still running) when its whole result is the engine's waiting placeholder, the agent's transcript shows the hand-back contract on (so the engine wrote that sentence, not the agent; reached through the notification's own `` link, so the hook keeps no session state, and searched with `grep`, past `$.fs.read`'s 4 MiB), and it tells the session nothing its conversation does not hold (the session's own loop spawned the agent or the conversation, read as it stands through `$.session.messages({ as: "api" })`, holds an earlier notice of it, and of any worktree it names), so a subagent parked on its own work no longer wakes the session for a turn with nothing to act on; a pointer to a delivered report passes (it can name a message that went to an ended parent), as do the first notice of each worktree and of each orphan, the final notification, and whatever a check errs on or cannot settle. Each drop still leaves the engine's "Prompt dropped by a hook" line, a system notice no render hook reaches. `appeal.tsx` (with `appeal-judge.tsx`, the logic that takes no `$`) lets the agent contest an auto-mode classifier block: a `tool.call` hook on every tool gives each appealable block an id and a note (not `[Data Exfiltration]`, not `[User Deny Rules]`), `mcp__doperpowers__appeal` puts one appeal per block to an independent judge (`$.model.complete`, opus at high effort, reading the blocked call whole, the argument as the agent's claim, the instruction files, the `user`/`policy`/`flag` settings' `autoMode` and `permissions.deny` — never the merged settings a repository feeds — the answered async asks, and the conversations with provenance-stripped rows named), and a grant leaves a one-shot pass that the `tool.check` hook applies to the identical re-run in the same loop within 10 minutes, only over core's plain `ask` (no rule, hook, setting, decider or ceiling); any `prompt.submit` ends every pass; the log is `~/.claude/appeals/.jsonl` and `/appeals` prints the last 30 days with the autoMode entries the grants suggest, for the human to apply (design `docs/doperpowers/specs/2026-10-17-auto-mode-appeal-design.md`). `bridge.tsx` (registered first, so its hooks are the outermost) makes the session reachable from outside it on this machine: at `session.start` it spawns a session-lifetime child, `sminos bridge --session `, which serves the session's endpoint at `$SMINOS_HOME/bridge/.sock` (the socket answering is the session's bridge presence); the mod posts the session's events there from one poster loop (each chunk of the main loop and of every subagent, tool calls and results, turns, subagents, usage, a heartbeat, and a notice for every row the session keeps, as it is written, each loop's last notice the snapshot's cut, seeded, for a loop older than the module, from its transcript tail at the module's first hello; every post carries the hello's nonce, so a reloaded module's old instance is refused and stops) and reads commands from the child's stdout: steer (a user row the loop reads at its next request), interrupt (the turn aborted, each Bash the abort moved to the background stopped, and two user rows in before the turn can end: Esc's marker whole, the only text a transcript's reader knows an interrupt by, then a note telling the model where it came from and what was stopped), model and effort (rewritten on the main loop's requests, never `$.config.set`), compact (at the turn's end when asked during one), a log event delivered as the human's words once per `(writer, seq)`, and an answer from outside to a permission prompt or a question, settled once on the engine's own `tool_decision` record of who decided. It exports the main loop's `` spans and async questions to the project's log as their rows are written, through the record path's own writer and `(kind, src)` keys, so the Stop hook's export finds them there. Never in `SMNS_INTERNAL`. `bridge.tsx` keeps the hooks and the code that takes `$` (the session start, the child, control, the wait for a decision); its other concerns sit beside it, none taking `$`, each importing only the ones listed before it: `bridge-state.tsx` (the one state object, its types, `resetBridge`), `bridge-stream.tsx` (the live state every resync carries, and the poster), `bridge-requests.tsx` (requests from outside and their settlement), `bridge-rows.tsx` (each kept row's notice, and the export to the host), `bridge-agents.tsx` (the agent state seeded and refreshed from `$.agent.list()`, so agents that predate the module are known). The protocol is DX5 in `docs/doperpowers/plans/2026-10-15-arkho-desk-c2-bridge.md`; tests `tests/mods/bridge.test.ts` and `tests/sminos/test-bridge*.sh`, live checks `tests/mods/live-*-check.py` (they spend model calls; run by hand). `/secret KEY` ships in the `cua` plugin (SSFSKIM/cua, `hooks/mods/secrets.tsx`), same store `~/.config/claude-secrets/KEY`; the guard that keeps secrets out of what the model reads and the transcript keeps is the devcloud plugin's (`devcloud/`). Tests in `tests/mods/`, run with `tests/mods/run-mods-tests.sh`; the streaming hook's are `tests/claude-code/test-to-human-stream-hook.sh`. `compact-instructions.md` is printed by a `PreCompact` hook on every compaction, auto or `/compact`: the harness appends a PreCompact hook's stdout to the summary request as "Additional Instructions" (confirmed in the 2.1.283 source and by a canary hook), after the stock nine-section template that tells the summarizer to follow its structure — text that does not say it overrides the template is read as a hint and barely moves the summary (2026-09-28: the first wording produced summaries indistinguishable from no instructions), so each summary is written as the note a memory-reset self resumes from — pointers for what is on disk, copies of what lived only in the conversation (the summarizer can call no tools, and the transcript path is appended after the summary). Subagent compactions do not receive it. The upstream `SessionStart` bootstrap that used to live here (injecting the `using-doperpowers` skill index) was torn out; skills load from `skills/` directly. | +| `hooks/` | The `kairos` proactive mode: `kairos.sh` on `SessionStart` injects the `kairos` skill's body at startup and after every compaction for a session in the mode — launched with `KAIROS=1`, or switched on mid-session by typing `/kairos`, which `kairos-toggle.sh` (on `UserPromptExpansion`) records as a flag file under `~/.claude/kairos/`; `/kairos off` removes it. Every other session gets nothing. `smns-record.sh` on `Stop` and `SessionStart` passes the payload to `sminos record`, which joins the session to its project and exports what it sent its human since the last export (its `` spans, code-aware, and its async asks and their answers) into the project's log; it is bounded (5 s, `SMNS_RECORD_TIMEOUT`), exits 0 on any failure, and does nothing outside a project or in sminos's own internal forks (`SMNS_INTERNAL`). `mods/` holds the plugin's function hooks ("mods"): `hooks.json` names `mods/register.tsx` under `modules`, which the engine loads only under `CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1` and ignores elsewhere. `to-human-stream.sh` on `MessageDisplay` draws the `` mark's labels flush by flush while a message streams (the engine mounts an `AssistantMessage` only once a block is whole); the report view that folds the transcript to those marks is the `to-human` plugin (`to-human/`), whose parser reads either that form or the raw tags; `kairos.tsx` draws a `[ kairos off ]` / `[ kairos on ]` button in the prompt footer that runs `/kairos` or `/kairos off` on a click, reading the mode's state from the shell hooks and never writing it; `agents.tsx` adds an `[ N subagents ]` button beside the kairos switch (registered ahead of it, wrapping its tree) from the session's first subagent on, which opens a pane drawing the session's agents as the tree they spawned in (`$.agent.list()`'s `parentId`), and a press on a row draws that agent's transcript file beneath the tree, following its tail while it runs. `ask.tsx` registers `mcp__doperpowers__AskUserQuestion`, the model's one way to ask the human in an interactive session: the built-in's input plus `async`. Blocking, it opens the engine's own dialog (a `$.ui.ask` raised inside the tool's hook and widened to the full question set by the mod's own `tool.call` hook on `AskUserQuestion`, correlated by a token in the header) and returns the engine's text; async, it returns at once, the questions wait behind `[ answer ]` in the call's row and in an `N open questions` band above the prompt, and the answer reaches the model as `context` on its next tool result, or as a plugin prompt when the session is idle, or with the human's next prompt after a stop. The built-in is deferred and refused while the tool exists; design in `docs/doperpowers/specs/2026-10-13-ask-async-design.md`. `compact.tsx` registers two tools the model calls, `mcp__doperpowers__context_usage` (the status line's context figures and where the engine compacts on its own, at no cost) and `mcp__doperpowers__compact` (the model compacting its own context at a checkpoint of its choosing): the engine compacts only between turns, so the tool answers that the compaction runs when the turn ends and the model ends its turn; at the `turn.complete` of a main-conversation turn the model answered the mod calls `$.session.compact` (the `/compact` path: one model call over the live conversation, so the model that chose to compact writes the summary, the PreCompact hook's instructions merged in) and submits the `resume` prompt the model wrote, whatever the outcome, so an unwatched session never stalls; nothing is aborted, so background shells, monitors, MCP servers and subagents run on (a `$.turn.abort` variant was rejected: it kills Monitors, whose shell runner gets no `caller` and so does not spare `turn-abort`). Interactive sessions only (headless has no compactor a plugin can call); a subagent's call to either is refused (`context_usage` would read the parent's figures). The engine hands every subagent every MCP tool and a hook cannot filter per agent, so the agent definitions this repo owns, and the user's own under `~/.claude/agents/`, withhold both through `disallowedTools`; built-in types (general-purpose, Explore) and forks still list them. The engine admits one unmatched registration of an event per module, and `$` may only be passed to functions in the same file, so the mod takes `session.start` and `turn.complete` with matchers (`isInteractive: true`, `reason: "answer"`) rather than sharing kairos's and agents' registrations. `interim.ts` drops, at `prompt.submit`, an agent's task notification the engine marks interim (it stopped with a background shell, monitor or workflow of its own still running) when its whole result is the engine's waiting placeholder, the agent's transcript shows the hand-back contract on (so the engine wrote that sentence, not the agent; reached through the notification's own `` link, so the hook keeps no session state, and searched with `grep`, past `$.fs.read`'s 4 MiB), and it tells the session nothing its conversation does not hold (the session's own loop spawned the agent or the conversation, read as it stands through `$.session.messages({ as: "api" })`, holds an earlier notice of it, and of any worktree it names), so a subagent parked on its own work no longer wakes the session for a turn with nothing to act on; a pointer to a delivered report passes (it can name a message that went to an ended parent), as do the first notice of each worktree and of each orphan, the final notification, and whatever a check errs on or cannot settle. Each drop still leaves the engine's "Prompt dropped by a hook" line, a system notice no render hook reaches. `appeal.tsx` (with `appeal-judge.tsx`, the logic that takes no `$`) lets the agent contest an auto-mode classifier block: a `tool.call` hook on every tool gives each appealable block an id and a note (not `[Data Exfiltration]`, not `[User Deny Rules]`), `mcp__doperpowers__appeal` puts one appeal per block to an independent judge (`$.model.complete`, opus at high effort, reading the blocked call whole, the argument as the agent's claim, the instruction files, the `user`/`policy`/`flag` settings' `autoMode` and `permissions.deny` — never the merged settings a repository feeds — the answered async asks, and the conversations with provenance-stripped rows named), and a grant leaves a one-shot pass that the `tool.check` hook applies to the identical re-run in the same loop within 10 minutes, only over core's plain `ask` (no rule, hook, setting, decider or ceiling); any `prompt.submit` ends every pass; the log is `~/.claude/appeals/.jsonl` and `/appeals` prints the last 30 days with the autoMode entries the grants suggest, for the human to apply (design `docs/doperpowers/specs/2026-10-17-auto-mode-appeal-design.md`). `bridge.tsx` (registered first, so its hooks are the outermost) makes the session reachable from outside it on this machine: at `session.start` it spawns a session-lifetime child, `sminos bridge --session `, which serves the session's endpoint at `$SMINOS_HOME/bridge/.sock` (the socket answering is the session's bridge presence); the mod posts the session's events there from one poster loop (each chunk of the main loop and of every subagent, tool calls and results, turns, subagents, usage, a heartbeat, and a notice for every row the session keeps, as it is written, each loop's last notice the snapshot's cut, seeded, for a loop older than the module, from its transcript tail at the module's first hello; every post carries the hello's nonce, so a reloaded module's old instance is refused and stops) and reads commands from the child's stdout: steer (a user row the loop reads at its next request), interrupt (the turn aborted, each Bash the abort moved to the background stopped, and two user rows in before the turn can end: Esc's marker whole, the only text a transcript's reader knows an interrupt by, then a note telling the model where it came from and what was stopped), model and effort (rewritten on the main loop's requests, never `$.config.set`), compact (at the turn's end when asked during one), a log event delivered as the human's words once per `(writer, seq)`, and an answer from outside to a permission prompt or a question, settled once on the engine's own `tool_decision` record of who decided. It exports the main loop's `` spans and async questions to the project's log as their rows are written, through the record path's own writer and `(kind, src)` keys, so the Stop hook's export finds them there. Never in `SMNS_INTERNAL`. `bridge.tsx` keeps the hooks and the code that takes `$` (the session start, the child, control, the wait for a decision); its other concerns sit beside it, none taking `$`, each importing only the ones listed before it: `bridge-state.tsx` (the one state object, its types, `resetBridge`), `bridge-stream.tsx` (the live state every resync carries, and the poster), `bridge-requests.tsx` (requests from outside and their settlement), `bridge-rows.tsx` (each kept row's notice, and the export to the host), `bridge-agents.tsx` (the agent state seeded and refreshed from `$.agent.list()`, so agents that predate the module are known). The protocol is DX5 in `docs/doperpowers/plans/2026-10-15-arkho-desk-c2-bridge.md`; tests `tests/mods/bridge.test.ts` and `tests/sminos/test-bridge*.sh`, live checks `tests/mods/live-*-check.py` (they spend model calls; run by hand). `/secret KEY` ships in the `cua` plugin (SSFSKIM/cua, `hooks/mods/secrets.tsx`), same store `~/.config/claude-secrets/KEY`; the guard that keeps secrets out of what the model reads and the transcript keeps is the devcloud plugin's (`devcloud/`). Tests in `tests/mods/`, run with `tests/mods/run-mods-tests.sh`; the streaming hook's are `tests/claude-code/test-to-human-stream-hook.sh`. `compact-instructions.md` is printed by a `PreCompact` hook on every compaction, auto or `/compact`: the harness appends a PreCompact hook's stdout to the summary request as "Additional Instructions" (confirmed in the 2.1.283 source and by a canary hook), after the stock nine-section template that tells the summarizer to follow its structure — text that does not say it overrides the template is read as a hint and barely moves the summary (2026-09-28: the first wording produced summaries indistinguishable from no instructions), so each summary is written as the note a memory-reset self resumes from — pointers for what is on disk, copies of what lived only in the conversation (the summarizer can call no tools, and the transcript path is appended after the summary). Subagent compactions do not receive it. The upstream `SessionStart` bootstrap that used to live here (injecting the `using-doperpowers` skill index) was torn out; skills load from `skills/` directly. | | `devcloud/` | A second plugin in this marketplace (its entry found by name, versioned on its own: `scripts/bump-version.sh --plugin devcloud`), opt-in, design in `docs/doperpowers/specs/2026-10-15-devcloud-design.md`. Today its secrets guard mod, `hooks/mods/secrets.tsx` (pure parts beside it: `secrets-detect.tsx` the token detector, `secrets-handles.tsx` the handles and their restoration, `secrets-sources.tsx` the store layout and credential-file parsers, `secrets-shell.tsx` when a value may be restored into a Bash command — inert, in a plain string or word, in a plain command, never quoted — and `secrets-reads.tsx` the exact read a notice names where it is not, both proved by `devcloud/tests/mods/shell-exec.ts` under bash and zsh): stored `/secret` values (cua's store, read here), credential-file values and token-shaped strings become handles (`[secret:KEY]`, `[redacted:#.]`, ...) in tool results and their records, every appended row, oversized outputs' `tool-results/` files, the system prompt's sections, context blocks, attachments and compactions; handles in Write/Edit/MultiEdit/NotebookEdit/Bash input are restored to their bytes before the call runs; nothing is refused. It sets its guard marker in `$.state` (`devcloud.secrets-guard`, typed in `types/devcloud.d.ts`) so `cua`'s scrubbing stands down. What it does not reach is listed in `devcloud/README.md`. Tests `devcloud/tests/mods/run.sh` (the `devcloud-mods` gate). The host image and boot program and the `devenv` skill come next. | +| `to-human/` | A third plugin in this marketplace (versioned on its own: `scripts/bump-version.sh --plugin to-human`), the report view of a session under the `to-human` output style. It is a plugin of its own because the engine runs none of a plugin's render hooks on a row that plugin submitted (the debug log's `ui.render skipped: re-entry (the plugin's own code raised it)`), and doperpowers submits prompts: the bridge's sminos deliveries, the ask's answers, the compact mod's resume prompt. Its `plugin.json` lists doperpowers under `dependencies`, which puts its hooks outside doperpowers' on every event (at equal rank the dependent is outer), so it sees the ask mod's rows before the ask draws them; its band above the prompt draws its line under what the ask's band draws there. `hooks/mods/to-human.tsx` is the consumer of the `to-human` output style on every surface (``, with old ``, ``, `` and `` marks read as aliases of it; tags in inline code or fenced blocks stay literal; an assistant message draws as its marked spans, each body a `Markdown` element on every surface (a render answer to a remote surface carries one props and only the first engine node draws there; on the terminal the engine's block carries its row margin between label and body), and the view's rows stand a line apart as the engine's do; a run of working record — unmarked messages, tool calls and their results — between two rows the human reads folds behind one `[ working record ]` button; a question asked through `AskUserQuestion` or the ask mod's tool stays in the report, and so do agent spawns through `Agent` or `Workflow` and the notification that one failed, and an API error that ends the main turn (sent and received agent messages, the other finish notifications, and a sminos delivery whose frame names a sender other than `human` fold into the working record); buttons unfold a run, a message, or the whole transcript; on the terminal a span's link to a local file, `[name](/abs/path#L42)`, draws behind a file glyph as a `file:` link, and a plain click on it opens VS Code at the line, a cmd-click staying the terminal's). System notices (`Prompt dropped by a hook: ...`) draw through no render hook of any plugin, so neither view can hide them. Tests `to-human/tests/mods/run.sh` (the `to-human-mods` gate). | | `output-styles/` | Output styles the harness auto-loads from the plugin. `to-human.md`: for sessions whose human reads a report stream (`` as a message sent to them, one that opens with a `⚠ Critical` block when work was lost, degraded, blocked, or significantly slowed, the block ending on how far that is handled and whether it needs them, and for what, and names in its body any lesser friction, with how to resolve or mitigate it, input asked for through `mcp__doperpowers__AskUserQuestion` with `async` when the model can work on; old ``, ``, `` and `` marks read as aliases of ``) instead of the transcript (selected per launch via `outputStyle`; design in `docs/doperpowers/specs/2026-09-05-sminos-human-stream-design.md`). | | `.claude-plugin/` | Claude Code manifest (`plugin.json`) + dev `marketplace.json`. The manifest's `workflows` key registers `skills/review-code/workflows/code-review.js` as the named workflow `doperpowers:code-review`: the Workflow tool's `scriptPath` passes the ordinary read-path check (the working directory and added directories only), so a path into the plugin cache is always refused — 56 such failures in this machine's transcripts, 2026-09-02 → 10-04, every one of them — and every caller passes the name. | | `.codex-plugin/`, `.cursor-plugin/`, `.kimi-plugin/`, `.opencode/`, `.pi/`, `.agents/`, `gemini-extension.json` | Per-harness plugin manifests/adapters. `.codex-plugin` is a local manifest only (the external fork sync was retired in v7.23.0; script recoverable from git history). | diff --git a/hooks/hooks.json b/hooks/hooks.json index dd2f9d50272..da184d3bced 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,5 +1,5 @@ { - "description": "mods (function hooks, under CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1): the to-human view of the transcript, mcp__doperpowers__AskUserQuestion (the built-in's input plus async, which withdraws the built-in), the kairos button in the prompt footer, the agents pane, the model's own compact / context_usage tools (compaction at the end of the turn that asked, then the resume prompt it wrote), and the bridge (the session's live stream and its control from another terminal on the machine — sminos watch, steer, interrupt, model, compact, decide, answer — through a child, sminos bridge, serving its endpoint; its marks and async asks reach the project's log as they are written). to-human-stream.sh (MessageDisplay) draws the same mark while a message streams; old , , and marks read as aliases of it. kairos: proactive mode — on from launch with KAIROS=1 or mid-session with /kairos, kept across compaction and resume; /kairos off ends it. compact-instructions.md: PreCompact prints it, so every compaction's summary is written as the note a memory-reset self resumes from. smns-record.sh (SessionStart, Stop): a session in a project joins its sminos log at its start, and its marks and async asks reach the log as messages to the host (the bridge's export, where it runs, has put them there already)", + "description": "mods (function hooks, under CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1): mcp__doperpowers__AskUserQuestion (the built-in's input plus async, which withdraws the built-in), the kairos button in the prompt footer, the agents pane, the model's own compact / context_usage tools (compaction at the end of the turn that asked, then the resume prompt it wrote), and the bridge (the session's live stream and its control from another terminal on the machine — sminos watch, steer, interrupt, model, compact, decide, answer — through a child, sminos bridge, serving its endpoint; its marks and async asks reach the project's log as they are written). to-human-stream.sh (MessageDisplay) draws the mark while a message streams (the report view that folds the transcript to those marks is the to-human plugin); old , , and marks read as aliases of it. kairos: proactive mode — on from launch with KAIROS=1 or mid-session with /kairos, kept across compaction and resume; /kairos off ends it. compact-instructions.md: PreCompact prints it, so every compaction's summary is written as the note a memory-reset self resumes from. smns-record.sh (SessionStart, Stop): a session in a project joins its sminos log at its start, and its marks and async asks reach the log as messages to the host (the bridge's export, where it runs, has put them there already)", "modules": [ "./mods/register.tsx" ], diff --git a/hooks/mods/ask.tsx b/hooks/mods/ask.tsx index 3f76300d842..1dd9053c02f 100644 --- a/hooks/mods/ask.tsx +++ b/hooks/mods/ask.tsx @@ -1147,8 +1147,8 @@ export function openCount(open: readonly OpenAsk[]): string { /** * The line above the prompt while an async ask is open: how many questions * wait, and `[ answer ]` for the oldest, since the ask's own row scrolls away - * while the model works. Registered before the to-human view, whose band - * draws without calling on, so this one is outer and draws above it. + * while the model works. The to-human view (its own plugin, outside this + * one) draws its line under this. */ export function registerAskBand(on: On) { on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => { @@ -1225,8 +1225,8 @@ async function askStart($: EngineInterface) { * through `$.ui.ask` and returns the engine's own result text; an async ask * returns at once and is answered from its `[ answer ]`, the answer handed * to the model; the built-in is withdrawn from the model, and a declined ask - * ends the model's turn as the built-in's does. Registered after the - * to-human view, so its row hooks are inner to that view's. + * ends the model's turn as the built-in's does. The to-human view, a plugin + * that depends on this one, sits outside its row hooks. */ export function registerAsk(on: On) { // Under MAWS (MAWS_NATIVE_TO_HUMAN set) the host offers its own ask tool, diff --git a/hooks/mods/register.tsx b/hooks/mods/register.tsx index 445cb715cfc..0321bf95cd2 100644 --- a/hooks/mods/register.tsx +++ b/hooks/mods/register.tsx @@ -7,7 +7,6 @@ import { registerBridge } from './bridge' import { registerCompact } from './compact' import { registerInterim } from './interim' import { registerKairos } from './kairos' -import { registerToHuman } from './to-human' /** * The plugin's one hooks module: each mod registers its own hooks. The engine @@ -21,12 +20,7 @@ export function register(on: On) { // After the bridge: its outer hooks see the appeal's notice on a block's // result and its allow on a passed re-run, as the session showed them. registerAppeal(on) - // Before to-human: its band above the prompt draws without calling on, so - // the ask's line there must be the outer hook, drawing above it. registerAskBand(on) - registerToHuman(on) - // After to-human: hooks registered later sit inside its own, where the - // ask's transcript rows must be. registerAsk(on) // Before kairos: both draw in the footer's mode slot, and the agents button // wraps what kairos drew there. diff --git a/package.json b/package.json index 5100d9b77b2..69196d99961 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doperpowers", - "version": "7.170.0", + "version": "7.171.0", "description": "Doperpowers skills and runtime bootstrap for coding agents", "type": "module", "keywords": [ diff --git a/skills/sminos/scripts/sminos.py b/skills/sminos/scripts/sminos.py index 3b613ff4dac..2cde2b8ac90 100644 --- a/skills/sminos/scripts/sminos.py +++ b/skills/sminos/scripts/sminos.py @@ -148,7 +148,7 @@ SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) if SCRIPT_DIR not in sys.path: sys.path.insert(0, SCRIPT_DIR) -import sminos_marks # noqa: E402 — the mark grammar of hooks/mods/to-human.tsx, ported +import sminos_marks # noqa: E402 — the mark grammar of to-human/hooks/mods/to-human.tsx, ported LAUNCHER = os.path.join(SCRIPT_DIR, "sminos") # The engine's permission modes (2.1.300), least permissive first. diff --git a/skills/sminos/scripts/sminos_marks.py b/skills/sminos/scripts/sminos_marks.py index 8daa2a9ee5e..4ddb9d0fc30 100644 --- a/skills/sminos/scripts/sminos_marks.py +++ b/skills/sminos/scripts/sminos_marks.py @@ -1,5 +1,5 @@ """The marks a session writes to its human, read the way the `to-human` mod -reads them for display: a Python port of `parse` in hooks/mods/to-human.tsx. +reads them for display: a Python port of `parse` in to-human/hooks/mods/to-human.tsx. `` is the one mark; the old ``, ``, `` and `` read as its aliases. A tag inside inline code diff --git a/tests/sminos/marks_parity.py b/tests/sminos/marks_parity.py index 106646d17da..b94e509f826 100644 --- a/tests/sminos/marks_parity.py +++ b/tests/sminos/marks_parity.py @@ -1,5 +1,5 @@ """Parity fixtures for sminos_marks.parse: the `parse` cases of -tests/mods/to-human.test.ts ("register" and "tags in code"), ported with +to-human/tests/mods/to-human.test.ts ("register" and "tags in code"), ported with their expected values unchanged. Prints one `ok ` or `FAIL ` line per case, for run-sminos-tests.sh to count. diff --git a/to-human/.claude-plugin/plugin.json b/to-human/.claude-plugin/plugin.json new file mode 100644 index 00000000000..10c7aa67340 --- /dev/null +++ b/to-human/.claude-plugin/plugin.json @@ -0,0 +1,20 @@ +{ + "name": "to-human", + "description": "The report view of a session under the to-human output style: assistant messages fold to their spans, and tool calls, unmarked text and deliveries from agents and other sessions fold behind [ working record ] buttons", + "version": "0.1.0", + "author": { + "name": "SSFSKIM", + "email": "supremekim17@gmail.com" + }, + "homepage": "https://github.com/SSFSKIM/doperpowers/tree/main/to-human", + "repository": "https://github.com/SSFSKIM/doperpowers", + "license": "MIT", + "dependencies": [ + "doperpowers" + ], + "keywords": [ + "to-human", + "transcript", + "mods" + ] +} diff --git a/to-human/hooks/hooks.json b/to-human/hooks/hooks.json new file mode 100644 index 00000000000..f83ed6d7b2f --- /dev/null +++ b/to-human/hooks/hooks.json @@ -0,0 +1,6 @@ +{ + "description": "mods (function hooks, under CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1): the to-human view of the transcript — from the first mark on, assistant messages draw as their marked spans and the working record (unmarked messages, tool calls and their results, agent messages, notifications, and messages delivered from other sessions) folds behind [ working record ] buttons; the band above the prompt switches between the report and the full transcript", + "modules": [ + "./mods/register.tsx" + ] +} diff --git a/to-human/hooks/mods/register.tsx b/to-human/hooks/mods/register.tsx new file mode 100644 index 00000000000..9e36bfa6b84 --- /dev/null +++ b/to-human/hooks/mods/register.tsx @@ -0,0 +1,16 @@ +import type { On } from 'claude-code' + +import { registerToHuman } from './to-human' + +/** + * The plugin's one hooks module. The view is a plugin of its own, not one of + * doperpowers' mods: the engine runs none of a plugin's render hooks on a row + * that plugin submitted, and doperpowers submits prompts (the bridge's + * deliveries from other sessions, the ask's answers, the compact mod's resume + * prompt) that the view folds. It lists doperpowers under `dependencies`, so + * its hooks sit outside doperpowers' on every event, where it sees the ask + * mod's rows before the ask draws them. + */ +export function register(on: On) { + registerToHuman(on) +} diff --git a/hooks/mods/to-human.tsx b/to-human/hooks/mods/to-human.tsx similarity index 95% rename from hooks/mods/to-human.tsx rename to to-human/hooks/mods/to-human.tsx index 33c0cb829b4..756c434a48a 100644 --- a/hooks/mods/to-human.tsx +++ b/to-human/hooks/mods/to-human.tsx @@ -1,6 +1,7 @@ import type { EngineInterface, On, PromptOrigin, RenderElement, RenderInput } from 'claude-code' -import { ASK_TOOL } from './ask' +/** The doperpowers ask mod's tool, the model's one way to ask the human in an interactive session. */ +const ASK_TOOL = 'mcp__doperpowers__AskUserQuestion' /** * The mark the `to-human` output style asks the model to write: a message @@ -329,9 +330,21 @@ const DELIVERED: ReadonlySet = new Set([ 'plugin', ]) +/** The origins the person's own gesture stamps: Enter at the prompt, and a Remote Control client's message. */ +const TYPED: ReadonlySet = new Set(['composer', 'bridge']) + +/** + * The line sminos opens every delivery with, whichever way it reaches the + * session (the bridge's prompt, a background job's reply, the inbox), naming + * who sent it: `human` for the person, else another session's alias. + */ +const SMINOS_FRAME = /^\[sminos message from (\S+) id=[0-9a-f]{8}\]/ + /** The props of a prompt row that say what it is. */ export type PromptRowProps = { origin?: Pick + /** What the row shows; a delivery's text opens with its sender's frame. */ + text?: string /** Present when someone other than the person sent it: an agent, a teammate, another session, a channel. */ from?: { name: string } /** Present on a notification row: the background task it reports on, an agent's run among them. */ @@ -342,7 +355,10 @@ export type PromptRowProps = { * The person's own words stay visible. Messages delivered by agents and * background notifications belong to the working record, except the * failure of an agent the report shows the spawn of (`spawns`, the calls' - * ids): the spawn stays in the report, so its failure does too. + * ids): the spawn stays in the report, so its failure does too. A sminos + * delivery is read by its frame: the person's, sent from elsewhere, stays, + * and another session's is working record, though the bridge submits both as + * the person's words. */ export function promptRow(props: PromptRowProps, spawns: ReadonlySet = new Set()): RowKind { const { task } = props @@ -352,7 +368,12 @@ export function promptRow(props: PromptRowProps, spawns: ReadonlySet = n if (props.from !== undefined || task !== undefined) { return 'record' } - return props.origin !== undefined && DELIVERED.has(props.origin.kind) ? 'record' : 'user' + const kind = props.origin?.kind + const sender = kind !== undefined && TYPED.has(kind) ? undefined : SMINOS_FRAME.exec(props.text ?? '')?.[1] + if (sender !== undefined) { + return sender === 'human' ? 'user' : 'record' + } + return kind !== undefined && DELIVERED.has(kind) ? 'record' : 'user' } /** @@ -725,7 +746,8 @@ const native = async ($: EngineInterface, e: object) => * behind buttons, until a row is unfolded by its button or the whole * transcript by the band above the prompt. Agent spawns and their failures * stay in the report, and so does an API error that ends the turn; agent - * messages and finish notifications are working record. + * messages, finish notifications and other sessions' messages are working + * record. */ export function registerToHuman(on: On) { let hasSeenMark = false @@ -1020,26 +1042,32 @@ export function registerToHuman(on: On) { }) // The band above the prompt names the view and switches it: the whole - // transcript as the engine draws it, or the report. + // transcript as the engine draws it, or the report. Its line goes under + // what the hooks beneath draw there (the ask's open questions), which this + // plugin's hooks sit outside of. on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => { if (await native($, e)) return next(e) if (!hasSeenMark || e.props.hasSurvey) { return next(e) } + const inner = await next(e) const { Box, Text, Button } = $.ui.resolve(e) return ( - - to-human view · -