Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"name": "doperpowers",
"description": "Emerges by humans, Converges by Agent",
"version": "7.174.0",
"version": "7.175.0",
"source": "./",
"author": {
"name": "SSFSKIM",
Expand Down Expand Up @@ -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 <message> 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"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "doperpowers",
"description": "Emerges by humans, Converges by Agents",
"version": "7.174.0",
"version": "7.175.0",
"author": {
"name": "SSFSKIM",
"email": "supremekim17@gmail.com"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "doperpowers",
"version": "7.174.0",
"version": "7.175.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",
Expand Down
11 changes: 11 additions & 0 deletions .github/ci-kit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/**"]

# The devcloud host: unit tests, then the image built and booted in containers
# against stand-ins (hosted runner's Docker). bun runs the guard's detector
# over the image's filesystem.
Expand Down
20 changes: 19 additions & 1 deletion .version-bump.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"scripts/bump-version.sh",
".claude",
".worktrees",
"devcloud"
"devcloud",
"to-human"
]
}
},
Expand All @@ -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": []
}
}
]
}
5 changes: 3 additions & 2 deletions CLAUDE.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions docs/doperpowers/TECH-DEBT.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,18 @@ the spec's Decision Log entry "the branch review round"
audit of interactive to-human sessions, counting final turns with text
and no mark, that finds the same order of rate.

- **The ask mod's transcript row does not draw the person's "Chat about this"
feedback or an answer's notes.** `askRow` reduces the feedback to a "User
declined to answer questions" row and `answerRows` drops added notes
(`hooks/mods/ask.tsx`). An async answer delivered while the session is idle
arrives as a plugin prompt carrying their words whole, which the view keeps
as the person's (`ASK_DELIVERY` in `to-human.tsx`); an answer that reaches
the model while it is busy, as context on a tool result, leaves no delivery
row, so the report shows neither. Fix: draw the feedback and notes in the
ask row; the delivery row could then fold as a duplicate. Trigger: the
person's feedback or notes found missing from a report, or the next edit to
`askRow`/`answerRows`.

## to-human view in the fullscreen transcript (2026-10-16, engine 2.1.302)

- **A wheel or trackpad scroll up jumps back down, 6–30 rows at a time.** It
Expand Down
2 changes: 1 addition & 1 deletion hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -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 <message> marks and async asks reach the project's log as they are written). to-human-stream.sh (MessageDisplay) draws the same <message> mark while a message streams; old <to-human>, <essential>, <need-input> and <insight> 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 <message> 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 <message> marks and async asks reach the project's log as they are written). to-human-stream.sh (MessageDisplay) draws the <message> mark while a message streams (the report view that folds the transcript to those marks is the to-human plugin); old <to-human>, <essential>, <need-input> and <insight> 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 <message> 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"
],
Expand Down
8 changes: 4 additions & 4 deletions hooks/mods/ask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 0 additions & 6 deletions hooks/mods/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "doperpowers",
"version": "7.174.0",
"version": "7.175.0",
"description": "Doperpowers skills and runtime bootstrap for coding agents",
"type": "module",
"keywords": [
Expand Down
2 changes: 1 addition & 1 deletion skills/sminos/scripts/sminos.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,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.
Expand Down
2 changes: 1 addition & 1 deletion skills/sminos/scripts/sminos_marks.py
Original file line number Diff line number Diff line change
@@ -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.

`<message>` is the one mark; the old `<to-human>`, `<essential>`,
`<need-input>` and `<insight>` read as its aliases. A tag inside inline code
Expand Down
2 changes: 1 addition & 1 deletion tests/sminos/marks_parity.py
Original file line number Diff line number Diff line change
@@ -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 <name>` or `FAIL <name>`
line per case, for run-sminos-tests.sh to count.

Expand Down
20 changes: 20 additions & 0 deletions to-human/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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 <message> 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"
]
}
6 changes: 6 additions & 0 deletions to-human/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -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 <message> 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"
]
}
16 changes: 16 additions & 0 deletions to-human/hooks/mods/register.tsx
Original file line number Diff line number Diff line change
@@ -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)
}
90 changes: 75 additions & 15 deletions hooks/mods/to-human.tsx → to-human/hooks/mods/to-human.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -329,9 +330,50 @@ const DELIVERED: ReadonlySet<string> = 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<string> = 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. A
* `send` opens `[sminos message from <sender> id=<id>]`; anything else (a
* `say`) opens `[sminos chat <project> <ref> from <sender> → @you|@all]`,
* with ` | unread N` before the bracket when messages are waiting. The
* project's name is free text in its `.smns` (spaces allowed), so the chat
* frame is read from its tail, where the sender sits, not from its head.
*/
const SMINOS_FRAMES = [
/^\[sminos message from (\S+) id=[0-9a-f]{8}\]/,
/^\[sminos chat [^\n]* from (\S+) → @(?:you|all)(?: \| unread \d+)?\]/,
]

/** Who a sminos delivery's opening line names as its sender, if it opens with one. */
function deliverySender(text: string): string | undefined {
for (const frame of SMINOS_FRAMES) {
const sender = frame.exec(text)?.[1]
if (sender !== undefined) {
return sender
}
}
return undefined
}

/**
* How the doperpowers ask mod opens the plugin prompt that hands the model
* the person's answer to an async question (its `deliveryText`), possibly
* after the framing the engine stores before a plugin prompt. It stays the
* person's: the ask's own row does not draw the feedback a person gives with
* "Chat about this" or the notes added to an answer, so this row carries
* their words whole.
*/
const ASK_DELIVERY = /^(?:The doperpowers plugin sent a message:\s*)?The human responded to your async AskUserQuestion \(call /

/** The props of a prompt row that say what it is. */
export type PromptRowProps = {
origin?: Pick<PromptOrigin, 'kind'>
/** 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. */
Expand All @@ -342,7 +384,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. The ask mod's delivery of an async answer stays too.
*/
export function promptRow(props: PromptRowProps, spawns: ReadonlySet<string> = new Set()): RowKind {
const { task } = props
Expand All @@ -352,7 +397,15 @@ export function promptRow(props: PromptRowProps, spawns: ReadonlySet<string> = 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
if (ASK_DELIVERY.test(props.text ?? '')) {
return 'user'
}
const sender = kind !== undefined && TYPED.has(kind) ? undefined : deliverySender(props.text ?? '')
if (sender !== undefined) {
return sender === 'human' ? 'user' : 'record'
}
return kind !== undefined && DELIVERED.has(kind) ? 'record' : 'user'
}

/**
Expand Down Expand Up @@ -725,7 +778,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
Expand Down Expand Up @@ -1020,26 +1074,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 (
<Box marginTop={1}>
<Text dimColor>to-human view · </Text>
<Button
key={MODE}
label={isFullTranscript ? 'report only' : 'full transcript'}
dimColor
onPress={() => {
isFullTranscript = !isFullTranscript
}}
/>
<Box flexDirection="column">
{inner}
<Box marginTop={1}>
<Text dimColor>to-human view · </Text>
<Button
key={MODE}
label={isFullTranscript ? 'report only' : 'full transcript'}
dimColor
onPress={() => {
isFullTranscript = !isFullTranscript
}}
/>
</Box>
</Box>
)
})
Expand Down
8 changes: 8 additions & 0 deletions to-human/tests/mods/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Runs the to-human plugin's mod tests. `claude plugin test <dir>` runs every
# *.test.ts under a plugin folder, and to-human/ is one whose only TypeScript
# tests are these, so it takes the folder as it is.
set -euo pipefail

root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
claude plugin test "$root"
Loading
Loading