1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
//! Role + Class: the role.class.sub label taxonomy (Class::ALL is the source of truth).
/// The top-level ROLE of a classified record (GOLD §2). The first dot-segment of every
/// [`Class::path`]. A multi-label record can span roles (e.g. an AUQ answer is both
/// [`Role::User`] and [`Role::Agent`]).
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Role {
/// The human operator.
User,
/// The assistant (incl. its tool I/O and peer communication).
Agent,
/// Claude Code machinery (notifications, compaction, slash wrappers, interrupts, schedule).
Harness,
}
#[allow(dead_code)]
impl Role {
/// The stable lowercase slug (the first dot-segment of a [`Class::path`]).
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Role::User => "user",
Role::Agent => "agent",
Role::Harness => "harness",
}
}
}
/// A LEAF class in the role.class.sub taxonomy (GOLD §2). One variant per leaf; the dotted
/// [`Class::path`] is the canonical wire/selector form and [`Class::role`] its top-level role.
/// A record carries a `Vec<Class>` (multi-label, GOLD §3) via [`Record::classify`].
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Class {
/// `user.message` - genuine human prose (incl. slash-command `<command-args>`).
UserMessage,
/// `user.answer` - an AskUserQuestion answer (the Q+options+answer unit). Dual-labeled
/// with [`Class::AgentToolResult`] (it rides on the answering tool_result carrier).
UserAnswer,
/// `user.rejection` - a plan/tool rejection carrying a typed instruction. Dual-labeled
/// with [`Class::AgentToolResult`].
UserRejection,
/// `user.unsent` - a SUPERSEDED turn-opener draft: sent, esc-recalled into the input
/// box, edited and re-sent, leaving the original on disk sharing the resend's
/// parentUuid. Assigned at the SCAN layer (the superseded set needs a LATER sibling,
/// which a pure per-record classify cannot see); outside turn numbering; 99% never
/// drew a reply. A recalled-then-ABANDONED message has no sibling and is
/// structurally undetectable; a QUEUED text edited before dispatch never becomes a
/// user record at all (it survives only in `queue-operation` lines).
UserUnsent,
/// `user.rewound` - a turn-opener the conversation was REWOUND past: it was sent,
/// it drew a reply, and Claude Code's own `parentUuid` chain now threads around it,
/// so the model no longer receives it or anything under it. The discriminator
/// against [`Class::UserUnsent`] is exactly that reply: a recalled draft has no
/// assistant descendant, a rewound turn does (measured over the shared-parent user
/// pairs of a real corpus: 868 without, 55 with, 0 on the chain). Assigned at the
/// SCAN layer like `user.unsent`, for the same reason - the fact lives in the DAG
/// around the record, not in the record. Outside turn numbering; addressable.
UserRewound,
/// `user.queued` - the human's text as it sat in the input QUEUE: a
/// `queue-operation` line carrying `content` (an `enqueue`, a `popAll` recall to
/// the input box, or a `remove` with the text). Not in the surviving conversation
/// (no `message{}`); the dispatched twin, when there is one, is the later
/// `user.message` record. Automation riders (a `<task-notification>` / peer
/// message queued by the harness) are NOT the human and carry no label here;
/// content-less `dequeue` lines carry nothing to search. The queue line has no
/// join key (measured: 4-6 keys, no promptId/uuid), so `dispatched` is never
/// asserted - only the verbatim `operation` + `reason` facts ride the hit.
UserQueued,
/// `agent.message` - the assistant's visible end-of-turn text block(s).
AgentMessage,
/// `agent.thinking` - a thinking block (see the GOLD-gap note re `redacted_thinking`).
AgentThinking,
/// `agent.thinking.narration` - a narration-tagged thinking block: an API-issued
/// one-sentence summary of the reasoning beside it, NOT the reasoning itself. The
/// tag hides inside the base64 `signature` (see `model/narration.rs`); the first
/// leaf whose path is prefixed by another leaf (`-t agent.thinking` selects both).
AgentThinkingNarration,
/// `agent.tool.use` - a tool_use block (incl. a pending elicitation sidecar marker).
AgentToolUse,
/// `agent.tool.result` - a tool_result block (incl. errored).
AgentToolResult,
/// `agent.communication.inbox` - a received peer message / spawn prompt / subagent return.
CommInbox,
/// `agent.communication.sent` - a sent peer message (`SendMessage`) or a spawn.
CommSent,
/// `agent.communication.signal` - a control/status comm (idle_notification, shutdown_*).
CommSignal,
/// `agent.communication.channel` - a csift-channel delivery: a hook-injected
/// `hook_additional_context` attachment whose FIRST content string opens with the
/// envelope header `[csift-channel v1 …]`. It is a MESSAGE addressed at this lane by
/// another lane (or by a sender outside Claude Code), so it is the RICHEST view of a
/// record that also carries [`Class::MetaHook`] - and unlike that leaf it is admitted
/// by a DEFAULT scan: a delivery no ordinary query surfaces is a delivery nobody can
/// audit.
CommChannel,
/// `harness.notification.workflow` - a `<task-notification>` for a dynamic/OMC workflow.
NotificationWorkflow,
/// `harness.notification.monitor` - a monitor/cron cadence completion pulse.
NotificationMonitor,
/// `harness.notification.subagent` - a spawned-subagent completion pulse (renamed from
/// [`AutomationKind::Agent`] so it never collides with the `agent` role).
NotificationSubagent,
/// `harness.notification.background-command` - a `&`-detached shell command pulse.
NotificationBackgroundCommand,
/// `harness.notification.task` - any other / unclassified `<task-notification>`.
NotificationTask,
/// `harness.compaction.summary` - the `isCompactSummary` summary record.
CompactionSummary,
/// `harness.compaction.boundary` - the `system`/`compact_boundary` metrics record.
CompactionBoundary,
/// `harness.command.invocation` - a `<command-name>…` slash-command wrapper.
CommandInvocation,
/// `harness.command.stdout` - a `<local-command-stdout>…` local-command output.
CommandStdout,
/// `harness.interrupt.user` - `[Request interrupted by user]`.
InterruptUser,
/// `harness.interrupt.tool` - `[Request interrupted by user for tool use]`.
InterruptTool,
/// `harness.schedule.wakeup` - a fired `ScheduleWakeup` TIMER tick (its injected
/// [`SCHEDULE_WAKEUP_MARKER`] prompt). Distinct from [`Class::MetaLoop`] (the
/// autonomous-loop driver prose); the timer is the harness scheduler firing.
ScheduleWakeup,
/// `harness.schedule.fire` - the PROMPT a scheduled task fires: the `isMeta`
/// `type:"user"` record Claude Code's scheduler submits on its own when a cron entry or
/// a `ScheduleWakeup` timer comes due, carrying `promptSource:"system"` (the stamp the
/// submit path puts on every isMeta submission) and, on the builds that write one, a
/// sibling `system`/`scheduled_task_fire` record naming the instant it fired.
///
/// The armed text reaches the record VERBATIM - the fire path applies exactly one
/// rewrite, the autonomous-loop sentinel resolution, and every other prompt falls
/// through it unchanged - so a fired prompt carries no marker of its own and looked,
/// until this leaf, exactly like an unmodeled isMeta pseudo-turn: excluded, unsearchable,
/// absent from every census. The tick prompts that DO carry a marker keep their own
/// leaves ([`Class::ScheduleWakeup`] / [`Class::MetaLoop`]), which is why this arm sits
/// after them; an inbound peer message, which shares the isMeta + `promptSource` stamp,
/// is refused through the same `is_peer_message` predicate `user.queued` uses.
///
/// DELIVERED: the model receives the prompt and answers it as an ordinary turn. It does
/// not OPEN one, for the same reason no isMeta record does - the operator did not type it.
ScheduleFire,
/// `harness.resume.prompt` - the repair PROMPT Claude Code's loader appends when a
/// resumed transcript ends on a dangling user record (an esc-recalled draft never
/// resent, any unanswered prompt): an `isMeta` `type:"user"` record carrying
/// [`RESUME_PROMPT_MARKER`]. It is delivered to the model, yet it is the LOADER's text,
/// not the operator's - so it never opens a turn and never counts as a human message.
/// (Named `harness.schedule.continuation` through v0.11.x; the record comes from the
/// resume loader, never from the scheduler, so the leaf moved to the family it belongs
/// to. Same predicate, same records.)
ResumePrompt,
/// `harness.resume.placeholder` - the repair PLACEHOLDER spliced in right after a
/// resumed transcript's trailing user record so the loaded conversation does not end on
/// an unanswered prompt: an assistant record with the [`SYNTHETIC_MODEL`] model, no
/// `isApiErrorMessage`, and exactly [`RESUME_PLACEHOLDER_TEXT`] as its content. It is
/// delivered, and it is not the assistant's own text. Whether it closes a repair PAIR
/// (its parent is a [`Class::ResumePrompt`] record) is a FACT on the hit, not a second
/// leaf: the same splice writes the unpaired form after any other trailing user record.
ResumePlaceholder,
/// `harness.meta.hook` - hook-injected feedback (stop-hook / `<local-command-caveat>` /
/// edit-failed-retry), not the operator.
MetaHook,
/// `harness.meta.loop` - an autonomous-loop driver tick (`# Autonomous loop tick` /
/// `Run the autonomous check`).
MetaLoop,
/// `harness.meta.attachment` - any OTHER `type:"attachment"` record's payload
/// (edited_text_file, compact_file_reference, file snapshots, todos, …). Scanned only
/// under `search --attachments` / `--count-by attachment` (or an explicit `show`
/// address); the matchable text is the VERBATIM payload JSON. A hook-context payload
/// stays the more specific [`Class::MetaHook`].
MetaAttachment,
/// `harness.meta.turn-duration` - the `system`/`turn_duration` end-of-turn
/// telemetry record (`durationMs`, `messageCount`, and the optional
/// `pendingBackgroundAgentCount` / `pendingWorkflowCount`): the structured body
/// behind the REPL's "Done in Ns" / "Waiting for N agents" lines, which never land
/// on disk themselves. No `message{}`: not in the surviving conversation.
MetaTurnDuration,
/// `harness.meta.away-summary` - the `system`/`away_summary` recap the harness
/// generates (a side model call, config-gated) when the operator returns after 5+
/// minutes away. Model-generated prose, yet no `message{}`: shown in the UI, not in
/// the surviving conversation (measured 1,196/1,198 never re-appear downstream).
MetaAwaySummary,
/// `harness.meta.stop-hooks` - the `system`/`stop_hook_summary` execution ledger
/// of the Stop hooks that ran at turn end (`hookInfos[].command` + durations,
/// `hookErrors`, `preventedContinuation`). Distinct from [`Class::MetaHook`], which
/// is the text a hook INJECTED into the model; this record is the run itself and
/// its `hookAdditionalContext` is empty on every measured instance.
MetaStopHooks,
/// `harness.meta.snapshot` - a `file-history-snapshot` (the per-prompt tracked-file
/// version table) or a `file-history-delta` (one path's version bump) line: the
/// instrument `recover` reads for external settings writes (v0.9.4), now
/// searchable by path and version. No `message{}`; a snapshot line has no
/// top-level timestamp (the excerpt carries the nested one).
MetaSnapshot,
/// `harness.meta.system` (v0.10.1) - the catch-all for every OTHER `type:"system"`
/// subtype the harness writes for its own UI:
/// `informational` (e.g. the Remote Control disconnect warning, `level:"warning"`),
/// `api_error`, `model_refusal_fallback` / `model_refusal_no_fallback`,
/// `agents_killed`, `local_command`, `scheduled_task_fire`, and any subtype a
/// future build adds. Renders `[<subtype> <level>] <content>`. No `message{}`, so
/// invisible by the same instrument as the rest of this family; gated like them.
/// EXCEPT `local_command` (a slash command's own echo and stdout), which the
/// request assembler re-mints as a user message: that ONE subtype is delivered,
/// per record, via [`Record::delivery_override`], so a bare `-t harness` surfaces
/// it although this leaf's default says invisible.
MetaSystem,
}
#[allow(dead_code)]
impl Class {
/// LLM-VISIBILITY (v0.9.4): true when the record's content is part of the
/// conversation the model actually receives or produces. Exactly TWO leaves are
/// invisible, each with a measured instrument:
/// - `user.unsent`: a superseded draft is NOT in the surviving conversation -
/// Claude Code's own `compactMetadata.preservedMessages` accounting excludes
/// every draft uuid (0 of 772 measured), and the conversation DAG threads
/// through the resend sibling, never the draft. (Wording law: "not in the
/// surviving conversation", never "the model never saw it" - a few drafts
/// drew real replies before the retraction.)
/// - `harness.compaction.boundary`: a system record with NO message field at
/// all - pure compaction metrics. (The compaction SUMMARY is visible: the
/// DAG threads through it; `isVisibleInTranscriptOnly` is a display flag on
/// summaries, not a delivery flag, and `isMeta` is an authorship flag -
/// neither is a visibility instrument.)
///
/// v0.10.0 adds the promoted non-record line types, all invisible by the same
/// instrument as the boundary: ZERO of them carries a `message{}` field (measured
/// over every non-record line type in the corpus; every user/assistant record
/// does), and Claude Code's
/// own source labels them REPL-render internals. DAG threading is NOT the
/// instrument here - later user records name a `turn_duration` uuid as parentUuid
/// (chain continuity), and `preservedMessages` lists these uuids at the same rate
/// as messages (it is a tail window, not a visibility filter).
///
/// The leaf default is not the whole answer, because delivery is decided per
/// RECORD: Claude Code's request assembler runs ONE drop predicate over the
/// record list, and it disagrees with this table in three cases - a
/// `system`/`local_command` record IS sent (re-minted as a user message), while an
/// `isVirtual` record and the `<synthetic>` API-error placeholder are NOT. That is
/// [`Record::delivery_override`], and it is what a bare ROLE selector consults; the
/// glob form and explicit paths reach every leaf regardless.
#[must_use]
pub fn llm_visible(self) -> bool {
!matches!(
self,
Class::UserUnsent
| Class::UserRewound
| Class::UserQueued
| Class::CompactionBoundary
| Class::MetaTurnDuration
| Class::MetaAwaySummary
| Class::MetaStopHooks
| Class::MetaSnapshot
| Class::MetaSystem
)
}
/// Every leaf [`Class`] in taxonomy order (GOLD §2). The single source of truth for
/// enumerating the class space - P2 builds the `-t` selector table from it, and tests
/// assert `path()`/`role()` exhaustively over it (a new variant added to the enum but not
/// here is caught by the `all_classes_cover_the_enum` test). Order: user, agent (+comm),
/// harness (notification, compaction, command, interrupt, schedule, meta).
pub const ALL: &'static [Class] = &[
Class::UserMessage,
Class::UserAnswer,
Class::UserRejection,
Class::UserUnsent,
Class::UserRewound,
Class::UserQueued,
Class::AgentMessage,
Class::AgentThinking,
Class::AgentThinkingNarration,
Class::AgentToolUse,
Class::AgentToolResult,
Class::CommInbox,
Class::CommSent,
Class::CommSignal,
Class::CommChannel,
Class::NotificationWorkflow,
Class::NotificationMonitor,
Class::NotificationSubagent,
Class::NotificationBackgroundCommand,
Class::NotificationTask,
Class::CompactionSummary,
Class::CompactionBoundary,
Class::CommandInvocation,
Class::CommandStdout,
Class::InterruptUser,
Class::InterruptTool,
Class::ScheduleWakeup,
Class::ScheduleFire,
Class::ResumePrompt,
Class::ResumePlaceholder,
Class::MetaHook,
Class::MetaLoop,
Class::MetaAttachment,
Class::MetaTurnDuration,
Class::MetaAwaySummary,
Class::MetaStopHooks,
Class::MetaSnapshot,
Class::MetaSystem,
];
/// The canonical dotted path (GOLD §2) - the `-t` selector form (P2) and render label.
#[must_use]
pub fn path(self) -> &'static str {
match self {
Class::UserMessage => "user.message",
Class::UserAnswer => "user.answer",
Class::UserRejection => "user.rejection",
Class::UserUnsent => "user.unsent",
Class::UserRewound => "user.rewound",
Class::UserQueued => "user.queued",
Class::AgentMessage => "agent.message",
Class::AgentThinking => "agent.thinking",
Class::AgentThinkingNarration => "agent.thinking.narration",
Class::AgentToolUse => "agent.tool.use",
Class::AgentToolResult => "agent.tool.result",
Class::CommInbox => "agent.communication.inbox",
Class::CommSent => "agent.communication.sent",
Class::CommSignal => "agent.communication.signal",
Class::CommChannel => "agent.communication.channel",
Class::NotificationWorkflow => "harness.notification.workflow",
Class::NotificationMonitor => "harness.notification.monitor",
Class::NotificationSubagent => "harness.notification.subagent",
Class::NotificationBackgroundCommand => "harness.notification.background-command",
Class::NotificationTask => "harness.notification.task",
Class::CompactionSummary => "harness.compaction.summary",
Class::CompactionBoundary => "harness.compaction.boundary",
Class::CommandInvocation => "harness.command.invocation",
Class::CommandStdout => "harness.command.stdout",
Class::InterruptUser => "harness.interrupt.user",
Class::InterruptTool => "harness.interrupt.tool",
Class::ScheduleWakeup => "harness.schedule.wakeup",
Class::ScheduleFire => "harness.schedule.fire",
Class::ResumePrompt => "harness.resume.prompt",
Class::ResumePlaceholder => "harness.resume.placeholder",
Class::MetaHook => "harness.meta.hook",
Class::MetaLoop => "harness.meta.loop",
Class::MetaAttachment => "harness.meta.attachment",
Class::MetaTurnDuration => "harness.meta.turn-duration",
Class::MetaAwaySummary => "harness.meta.away-summary",
Class::MetaStopHooks => "harness.meta.stop-hooks",
Class::MetaSnapshot => "harness.meta.snapshot",
Class::MetaSystem => "harness.meta.system",
}
}
/// The top-level role (the first dot-segment of [`Class::path`]). Exhaustive (no
/// wildcard) so a future leaf forces an explicit role decision at compile time.
#[must_use]
pub fn role(self) -> Role {
match self {
Class::UserMessage
| Class::UserAnswer
| Class::UserRejection
| Class::UserUnsent
| Class::UserRewound
| Class::UserQueued => Role::User,
Class::AgentMessage
| Class::AgentThinking
| Class::AgentThinkingNarration
| Class::AgentToolUse
| Class::AgentToolResult
| Class::CommInbox
| Class::CommSent
| Class::CommSignal
| Class::CommChannel => Role::Agent,
Class::NotificationWorkflow
| Class::NotificationMonitor
| Class::NotificationSubagent
| Class::NotificationBackgroundCommand
| Class::NotificationTask
| Class::CompactionSummary
| Class::CompactionBoundary
| Class::CommandInvocation
| Class::CommandStdout
| Class::InterruptUser
| Class::InterruptTool
| Class::ScheduleWakeup
| Class::ScheduleFire
| Class::ResumePrompt
| Class::ResumePlaceholder
| Class::MetaHook
| Class::MetaLoop
| Class::MetaAttachment
| Class::MetaTurnDuration
| Class::MetaAwaySummary
| Class::MetaStopHooks
| Class::MetaSnapshot
| Class::MetaSystem => Role::Harness,
}
}
}