Ways System Architecture¶
Diagrams of the ways trigger system: what runs on each hook event, how a way is matched and admitted, and what state the engine keeps. The prose here is kept to what a diagram needs. The fire rule with its thresholds is stated once in engine-reference.md, the hook table and the switches in hooks-and-ways.md, and the event log fields in reference/events.md.
Two names recur below:
{SESSIONS_ROOT}is$XDG_RUNTIME_DIR/claude-sessions, or/tmp/.claude-sessions-{uid}where that variable is unset (%LOCALAPPDATA%/claude-ways/sessionson Windows).{SESSIONS_ROOT}/{session_id}/holds one session's state.- Firing state is kept per agent. The main agent's engagement, way tokens, way epochs, epoch counter and check fires sit at the session root. A subagent's sit under
{SESSIONS_ROOT}/{session_id}/agents/{agent_id}/, and its token position, context window and model are read from its own transcript. Way markers are per agent too:ways/{way_id}/.marker.{agent_id}, withmainfor the main agent. A way shown to a subagent therefore does not silence it for the main agent, or the reverse.
Where the runtime lives¶
| Root | Path | Holds |
|---|---|---|
| App | $XDG_DATA_HOME/agent-ways |
The source checkout, the built binaries, and the shipped ways under hooks/ways/. Replaced on update. |
| Projection | ~/.claude (and each extra target) |
hooks/ways, skills, agents, commands and each binary under bin/ are linked (or copied) from the app. ways reconcile three-way merges the repo's settings.json hooks block into the target's settings.json (ADR-142, ADR-184). |
| User config | $XDG_CONFIG_HOME/agent-ways |
config.yaml, your own ways under ways/, the judge settings in agent.yaml, and per-target targets/<key>/config.yaml. Survives updates. |
| State | $XDG_STATE_HOME/agent-ways |
events.jsonl, and the per-session subagent switches under subagent-switch/. |
| Cache | $XDG_CACHE_HOME/agent-ways/user |
The embedding corpora (ways-corpus-en.jsonl, ways-corpus-multi.jsonl), embed-manifest.json with the calibration, and the GGUF models. Regenerable. |
| Session | {SESSIONS_ROOT}/{session_id} |
Markers, per-agent firing state, the subagent stash, the last response. Cleared on SessionStart startup, compact and clear. |
How a Session Flows¶
A session from the user's side, showing which lane each injection comes from:
sequenceDiagram
participant U as User
participant C as Claude Code
participant W as ways hook
participant G as Ways agent (judge)
participant S as Subagent
Note over U,S: SessionStart - check-state.sh shows core.md and the session-start ways
rect rgba(21, 101, 192, 0.15)
Note over U,G: Prompt lane
U->>C: "Let's fix the auth bug and add tests"
C->>W: UserPromptSubmit → ways hook prompt
W->>W: match (late-interaction, g(s) fallback, keyword floor τ_k)
W->>G: judge the candidates (at most 8, with the last turn)
G-->>W: P(yes) per way
W-->>C: inject ways with P(yes) ≥ 0.3
Note right of W: a blocked way leaves no marker and keeps its refire budget
end
rect rgba(106, 27, 154, 0.15)
Note over C,W: Tool lanes, before the tool runs
C->>W: PreToolUse:Bash → ways hook command (git log auth/)
Note right of W: no way matches, the command proceeds
C->>W: PreToolUse:Edit → ways hook file (config/auth.yaml)
W-->>C: config way, before the edit happens
end
rect rgba(0, 105, 92, 0.15)
Note over C,W: Post-tool lanes, after the tool runs
C->>W: PostToolUse → ways hook post-tool, ways hook queued
W->>W: postcheck.sh scripts request reactive fires
W->>G: operator messages queued mid-turn, matched and judged like a prompt
W-->>C: fired ways
end
rect rgba(230, 81, 0, 0.15)
Note over C,S: Delegation
C->>W: PreToolUse:Task → ways hook task (match, write stash)
C->>S: subagent starts
W-->>S: SubagentStart → ways hook subagent-start injects the stashed ways
Note right of S: the subagent keeps its own markers and refire state
S-->>C: findings
end
rect rgba(21, 101, 192, 0.15)
Note over U,W: A later prompt on the same topic
U->>C: "Now check the tests again"
Note right of W: testing way inside its refire window → way_suppressed (refire)
Note right of W: re-disclosed once its refire fraction of the window has passed
end
rect rgba(198, 40, 40, 0.15)
Note over U,S: Auto-compact
C->>W: SessionStart:compact → ways hook session-start
W->>W: clear {SESSIONS_ROOT}/{session_id}/
W-->>C: core.md again from the state scan
Note right of C: every way can fire again on its next match
end
Hook Flow¶
Every script under hooks/ways/ that touches ways is a thin adapter for one ways hook <event> call (ADR-504 §11). The other hooks in the same settings.json block are shown in grey.
flowchart LR
classDef event fill:#1565C0,stroke:#0D47A1,color:#fff
classDef script fill:#6A1B9A,stroke:#4A148C,color:#fff
classDef gate fill:#E65100,stroke:#BF360C,color:#fff
classDef output fill:#2E7D32,stroke:#1B5E20,color:#fff
classDef other fill:#78909C,stroke:#546E7A,color:#fff
SS["SessionStart<br/>startup · compact · resume · clear"]:::event
UP[UserPromptSubmit]:::event
PB["PreToolUse<br/>Bash"]:::event
PF["PreToolUse<br/>Edit, Write"]:::event
PT["PreToolUse<br/>Task"]:::event
PC["PreToolUse<br/>TaskCreate"]:::event
SA[SubagentStart]:::event
PO["PostToolUse<br/>Edit, Write, Bash, Task"]:::event
PX["PostToolUseFailure<br/>Edit, Write, Bash, Task"]:::event
ST[Stop]:::event
TC[TaskCreated]:::event
CM["clear-markers.sh → ways hook session-start<br/>(startup, compact, clear)"]:::script
CS["check-state.sh → ways hook state"]:::script
CP["check-prompt.sh → ways hook prompt"]:::script
CB["check-bash-pre.sh → ways hook command"]:::script
CF["check-file-pre.sh → ways hook file"]:::script
CT["check-task-pre.sh → ways hook task"]:::script
MT["mark-tasks-active.sh → ways hook tasks-active"]:::script
IS["inject-subagent.sh → ways hook subagent-start"]:::script
PP["check-post.sh → ways hook post-tool"]:::script
QQ["check-queued.sh → ways hook queued"]:::script
CR["check-response.sh → ways hook stop"]:::script
SS --> CM
SS --> CS
UP --> CP
UP --> CS
PB --> CB
PF --> CF
PT --> CT
PC --> MT
SA --> IS
PO --> PP
PX --> PP
PO --> QQ
ST --> CR
On{"Switched on?<br/>ways.enabled · subagent switches<br/>· defined agent"}:::gate
CS --> On
CP --> On
CB --> On
CF --> On
CT --> On
IS --> On
PP --> On
QQ --> On
On -->|prompt, queued, command, file| Match["Matcher<br/>keyword · semantic"]:::gate
On -->|state| State["core.md · state triggers"]:::gate
On -->|post-tool| Post["postcheck.sh exits 0"]:::gate
On -->|task| TaskM["Matcher on the Task prompt<br/>scope: subagent"]:::gate
On -->|subagent-start| Claim["claim the oldest stash"]:::gate
Match -->|prompt, queued| Judge{"Relevance judge<br/>ways agent socket"}:::gate
Match -->|command, file| Refire
Judge -->|pass| Refire
Judge -->|block| Blocked["not shown, no marker"]:::other
State --> Refire
Post --> Refire
Refire{"Inside the refire window,<br/>or over the 10,000-char budget?<br/>(no budget on the state lane)"}:::gate
Refire -->|no| Out["inject, stamp the per-agent marker"]:::output
Refire -->|yes| Supp["way_suppressed"]:::other
TaskM --> Stash[("{SESSIONS_ROOT}/{sid}/subagent-stash/")]:::output
Stash -.-> Claim
Claim --> Emit["emit fresh, record the fire<br/>under the subagent's id"]:::output
CM --> Clear["clear {SESSIONS_ROOT}/{sid}/,<br/>log session_start"]:::output
CR --> Rec["record the last response"]:::output
MT --> TA["write tasks-active (dormant)"]:::other
SS -.-> O1["check-setup.sh · check-config-updates.sh<br/>· ways init · ways corpus --if-stale"]:::other
SS -.-> O2["issues-pull.sh"]:::other
UP -.-> O2
PO -.->|Bash| O2
PB -.-> O3["strip-session-link-pre.sh"]:::other
ST -.-> O4["attend-drain-stop.sh"]:::other
TC -.-> O5["issues-task-created.sh"]:::other
check-setup.sh, check-config-updates.sh, ways init and ways corpus --if-stale run on startup only (ways init also on clear). check-queued.sh scans for the main agent only, because operator messages are queued to it.
Subagent Injection¶
A Task prompt is visible on PreToolUse:Task, but the subagent only exists at SubagentStart. A stash file bridges the two:
sequenceDiagram
participant A as Main agent
participant CC as Claude Code
participant CT as check-task-pre.sh (ways hook task)
participant S as Stash dir
participant IS as inject-subagent.sh (ways hook subagent-start)
participant SA as Subagent
rect rgba(21, 101, 192, 0.15)
Note over A,S: Phase 1 - PreToolUse:Task
A->>CC: Task(prompt: "Review the PR for security issues")
CC->>CT: PreToolUse:Task
alt subagent_type names a defined agent
Note right of CT: project, user or plugin agents/*.md - no stash
else ways switched off for subagents (session, project or user)
CT->>CT: log injection_suppressed, no stash
else
CT->>CT: keyword and late-interaction match over the Task prompt (scope: subagent)
CT->>S: write {ts}.json with the matched way ids
Note right of S: {SESSIONS_ROOT}/{sid}/subagent-stash/{ts}.json
end
end
rect rgba(106, 27, 154, 0.15)
Note over CC,SA: Phase 2 - SubagentStart
CC->>SA: spawn subagent
CC->>IS: SubagentStart
IS->>S: claim the oldest stash (rename, read, delete)
alt ways switched off for subagents
Note right of IS: the claimed stash is discarded
else
IS->>IS: if a teammate, write the teammate marker in its agent dir
IS->>IS: render each way, record the fire under the subagent's id
IS->>SA: additionalContext
end
end
The stashed ways are emitted whatever the parent has already been shown. Each fire is then recorded under the subagent's own id, so the subagent's later hooks follow their own refire windows.
Scope Filtering¶
The scope field controls where a way can inject:
flowchart LR
classDef agent fill:#1565C0,stroke:#0D47A1,color:#fff
classDef sub fill:#6A1B9A,stroke:#4A148C,color:#fff
classDef both fill:#00695C,stroke:#004D40,color:#fff
classDef team fill:#E65100,stroke:#BF360C,color:#fff
Way["{name}.md<br/>scope: ?"]
Way -->|"scope: agent"| AG["Not a teammate<br/>the main agent's lanes, and a plain<br/>subagent's own tool lanes"]:::agent
Way -->|"scope: subagent"| SB["Subagents, through the stash only<br/>task → subagent-start"]:::sub
Way -->|"scope: agent, subagent"| BOTH["Both paths<br/>(most shipped ways)"]:::both
Way -->|"scope: teammate"| TM["Teammates<br/>task stash with a team name"]:::team
Way -->|"no scope field"| DEF["ways.default_scope<br/>(agent unless configured)"]:::agent
A running agent's scope is teammate when its state directory holds the teammate marker, and agent otherwise (session::detect_scope). A plain subagent is therefore agent scope on its own tool lanes (command, file, post-tool), so a scope: agent way can fire inside it. scope: subagent is matched only on the Task prompt and reaches the subagent through the SubagentStart stash. Ways are on for subagents by default. The subagent switches (ways.subagents: false, ways session subagents off) turn off every lane that would inject into one.
Parallel Subagent Handling¶
Several Task calls in one message write separate stash files, consumed oldest first. Each SubagentStart claims its file by renaming it, so two subagents never take the same stash.
sequenceDiagram
participant CT as check-task-pre.sh
participant S as Stash dir
participant IS as inject-subagent.sh
rect rgba(21, 101, 192, 0.12)
CT->>S: write {ts1}.json (Task A)
CT->>S: write {ts2}.json (Task B)
end
rect rgba(106, 27, 154, 0.12)
IS->>S: claim {ts1}.json (oldest) → Subagent A
IS->>S: claim {ts2}.json (oldest) → Subagent B
end
Note over S: empty after both are claimed
Disclosure Cadence¶
Each (way, agent) pair in a session moves through these states. A way's refire: value is a fraction of the agent's context window (ADR-126): once 1.0, rare 0.4, normal 0.15, frequent 0.05, or a number.
stateDiagram-v2
classDef notShown fill:#C62828,stroke:#B71C1C,color:#fff,font-weight:bold
classDef shown fill:#2E7D32,stroke:#1B5E20,color:#fff,font-weight:bold
classDef eligible fill:#E65100,stroke:#BF360C,color:#fff,font-weight:bold
state "NotShown (no marker)" as NotShown
state "Shown (marker holds token_pos)" as Shown
state "Eligible again" as Eligible
[*] --> NotShown
NotShown --> Shown: match, judge pass, fits the budget
NotShown --> NotShown: judge block (way_judged), no marker
NotShown --> NotShown: no room (way_suppressed context_cap)
Shown --> Shown: match inside the window (way_suppressed refire)
Shown --> Eligible: refire × window tokens consumed
Eligible --> Shown: match, judge pass, fits (way_redisclosed)
Eligible --> Eligible: judge block or no room
Shown --> NotShown: SessionStart compact or clear
Eligible --> NotShown: SessionStart compact or clear
class NotShown notShown
class Shown shown
class Eligible eligible
The marker is {SESSIONS_ROOT}/{session_id}/ways/{way_id}/.marker.{agent_id}. The judge runs on the prompt and queued lanes only, so the judge-block edges apply there. A session-start state way shows once per marker reset rather than on a refire window.
Trigger Matching¶
How prompts and tool input reach a way:
flowchart LR
classDef input fill:#1565C0,stroke:#0D47A1,color:#fff
classDef scan fill:#6A1B9A,stroke:#4A148C,color:#fff
classDef match fill:#00695C,stroke:#004D40,color:#fff
classDef gate fill:#E65100,stroke:#BF360C,color:#fff
classDef output fill:#2E7D32,stroke:#1B5E20,color:#fff
classDef silent fill:#78909C,stroke:#546E7A,color:#fff
subgraph Input
Prompt["User prompt<br/>(fences and URLs masked)"]:::input
Queued["Queued operator messages"]:::input
Cmd["Bash command + description<br/>+ prose since the last human turn"]:::input
File["File path"]:::input
TaskP["Task prompt"]:::input
end
Cand["collect_candidates<br/>project .claude/ways<br/>> user $XDG_CONFIG_HOME/agent-ways/ways<br/>> core ~/.claude/hooks/ways<br/>(a higher root shadows the same id, tree order)"]:::scan
Pre["scope · when: preconditions<br/>(disabled domains and ways already dropped)"]:::scan
Prompt --> Cand
Queued --> Cand
Cmd --> Cand
File --> Cand
TaskP --> Cand
Cand --> Pre
Pre --> KW["pattern: on prompt, queued, task"]:::match
Pre --> DIR["commands: on the command<br/>pattern: on the description<br/>files: on the path"]:::match
Pre --> SEM["semantic<br/>late-interaction or g(s) fallback"]:::match
KW --> Floor{"g(s) ≥ τ_k?<br/>fails open · pattern_strict bypasses"}:::gate
Floor -->|no| KG["way_keyword_gated"]:::silent
Floor -->|yes| Hits
DIR --> Hits
SEM --> Hits
Hits["order_hits"]:::scan
Hits -->|task| Stash[("subagent stash")]:::output
Hits -->|prompt, queued| Judge{"relevance judge"}:::gate
Hits -->|command, file| Show
Judge -->|pass| Show
Judge -->|block| JB["way_judged block"]:::silent
Show{"disabled? refire window?<br/>context budget?"}:::gate
Show -->|admitted| Out["additionalContext"]:::output
Show -->|held| WS["way_suppressed"]:::silent
The bash semantic lane uses the single-vector scores only. The file lane is regex only. Disabled domains and ways are dropped when the candidates are collected, so they never boost a child or take a judge slot, and they are checked again when a way is shown.
Semantic Matching¶
The semantic channel on the prompt, queued and task surfaces is the ADR-160 late-interaction matcher. The single-vector calibrated gate (ADR-156) decides only when late-interaction cannot run. Its probabilities are computed on every scan, because the keyword floor and near-miss logging read them on both paths.
flowchart TB
classDef input fill:#1565C0,stroke:#0D47A1,color:#fff
classDef process fill:#6A1B9A,stroke:#4A148C,color:#fff
classDef check fill:#E65100,stroke:#BF360C,color:#fff
classDef yes fill:#2E7D32,stroke:#1B5E20,color:#fff
classDef side fill:#78909C,stroke:#546E7A,color:#fff
Red["reduce_for_embed<br/>prompt + last response, sentence salience<br/>(ADR-130, ADR-155)"]:::input
Chunk{"≥ 2 sentence chunks<br/>and the EN engine present?"}:::check
Red --> Chunk
subgraph LI["Late-interaction (ADR-160), EN corpus only"]
Batch["embed every chunk in one way-embed batch<br/>vs ways-corpus-en.jsonl"]:::process
Rank["per way: peak cosine over chunks<br/>per chunk: softmax share over the top 8 (τ 0.08)"]:::process
Admit{"summed share ≥ 0.15<br/>or peak ≥ 0.50?"}:::check
Confirm{"won chunk vs the way's body<br/>≥ 0.35?"}:::check
Batch --> Rank --> Admit -->|yes| Confirm
end
subgraph SV["Single vector (ADR-156)"]
Vec["one vector per model<br/>EN, plus multilingual when localized"]:::process
Cal["g(s) = σ(a·s + b)<br/>fit stored in embed-manifest.json"]:::process
Tau{"g(s) ≥ τ_s?<br/>0.5, or 0.40 with parent boost"}:::check
Vec --> Cal --> Tau
end
Chunk -->|yes| Batch
Chunk -->|"no (fallback)"| Tau
Red --> Vec
Confirm -->|yes| F1["FIRE<br/>semantic:late-interaction:en"]:::yes
Tau -->|yes| F2["FIRE<br/>semantic:embedding:en or :multi"]:::yes
Cal -.->|"g(s) within 0.05 below τ_s"| NM["way_nearmiss"]:::side
Cal -.-> KF["keyword floor τ_k"]:::side
The late-interaction operating points are hand-set and uncalibrated. Parent boost lowers τ_s, so it has no effect when late-interaction decides. On a localized install the multilingual lane can fire only on the fallback path. engine-reference.md states the rule with its sources.
Relevance Gate and the Ways Agent¶
On the prompt and queued lanes, the ways the matcher would show are sent in one request to a hosted yes/no judge before any fire is recorded (ADR-196). The judge runs in the ways agent, one resident daemon per user on a Unix socket of mode 0600 (ADR-502). A hook starts it on demand. It exits when idle, and when its binary is replaced. It holds the provider key, so hooks never read one, and it does judging and key custody only. Matching stays in the ways process. ways agent status reports the running agent, ways agent key manages keys, and ways settings sets gate.mode (enforce, shadow, off) and gate.engine. No key file means no gate.
What the judge sees, its threshold, cap, timeout, cost and failure behaviour are explained in the relevance judge.
Telemetry & Tuning¶
flowchart LR
classDef match fill:#6A1B9A,stroke:#4A148C,color:#fff
classDef log fill:#1565C0,stroke:#0D47A1,color:#fff
classDef read fill:#00695C,stroke:#004D40,color:#fff
Scan["scan lanes"]:::match
Gate["relevance gate"]:::match
Show["way_scored / subagent-start"]:::match
Hook["ways hook"]:::match
Scan -->|"way_nearmiss · way_keyword_gated"| EV
Gate -->|"way_judged · judge_call<br/>gate_capped · gate_fallback"| EV
Show -->|"way_fired · way_redisclosed<br/>(+ fire_score on semantic fires)<br/>way_suppressed · check_fired"| EV
Hook -->|"session_start · injection_suppressed"| EV
EV[("$XDG_STATE_HOME/agent-ways/events.jsonl<br/>tail-compacted at ~32 MiB")]:::log
EV --> R1["ways session<br/>ways · fires · replay · live · dump"]:::read
EV --> R2["ways tune precision · ways tune stats"]:::read
EV --> R3["ways agent cost"]:::read
fire_score is the deciding score of the semantic channel that fired: the summed share for late-interaction, g(s) for the single-vector path. The trigger field says which. The tuning loop is described in hooks-and-ways.md, and every event's fields in reference/events.md.
Macro Injection¶
Ways with macro: prepend|append run a script that queries live state when the way is shown:
sequenceDiagram
participant Lane as scan lane
participant Show as way_scored
participant Macro as macro.sh
participant Out as additionalContext
Lane->>Show: way id, session, trigger, budget
rect rgba(198, 40, 40, 0.12)
Show->>Show: refire check for this agent
alt inside the refire window
Show-->>Lane: nothing (way_suppressed refire)
else static body does not fit the budget
Show-->>Lane: nothing (way_suppressed context_cap)
end
end
rect rgba(21, 101, 192, 0.15)
alt macro: prepend
Show->>Macro: run (project macros only in trusted projects)
Macro-->>Out: dynamic context
Show-->>Out: static body
else macro: append
Show-->>Out: static body
Show->>Macro: run
Macro-->>Out: dynamic context
else no macro
Show-->>Out: static body
end
end
rect rgba(46, 125, 50, 0.15)
Show->>Show: lock, re-check refire, admit to the budget
Show->>Show: record the fire, stamp the marker and token position
Note right of Show: eligible again after the refire fraction of the window
end
A project-local macro runs only if the project is listed in ~/.claude/trusted-project-macros.
Directory Structure¶
$XDG_DATA_HOME/agent-ways/hooks/ways/ # shipped ways, projected as ~/.claude/hooks/ways/
├── core.md # base guidance, shown by the state scan
├── macro.sh # core.md's macro: the Available Ways table
├── require-ways.sh # shared by the adapters: runs ~/.claude/bin/ways hook <event>
│
├── clear-markers.sh # SessionStart → ways hook session-start
├── check-state.sh # SessionStart, UserPromptSubmit → ways hook state
├── check-prompt.sh # UserPromptSubmit → ways hook prompt
├── check-bash-pre.sh # PreToolUse:Bash → ways hook command
├── check-file-pre.sh # PreToolUse:Edit|Write → ways hook file
├── check-task-pre.sh # PreToolUse:Task → ways hook task
├── mark-tasks-active.sh # PreToolUse:TaskCreate → ways hook tasks-active (dormant marker)
├── inject-subagent.sh # SubagentStart → ways hook subagent-start
├── check-post.sh # PostToolUse, PostToolUseFailure → ways hook post-tool
├── check-queued.sh # PostToolUse → ways hook queued
├── check-response.sh # Stop → ways hook stop
│
├── check-setup.sh # SessionStart:startup → notice when the ways binary is missing
├── strip-session-link-pre.sh # PreToolUse:Bash → deny publishing a session link (ADR-167)
├── attend-drain-stop.sh # Stop → attend inbox --drain (ADR-172)
├── issues-pull.sh # SessionStart, UserPromptSubmit, PostToolUse:Bash → gh-tasks pull (ADR-180)
├── issues-task-created.sh # TaskCreated → reject unprefixed duplicates of mirrored issues (ADR-180)
├── check-bash-bound.py # Bash guard (ADR-181), shipped but not wired
│
└── {domain}/{way}/{way}.md # softwaredev, meta, documentation, ea, workstation, data, itops, ...
├── macro.sh # optional dynamic content
├── postcheck.sh # optional reactive firing on PostToolUse
└── {child}/{child}.md # ways nest for progressive disclosure
$XDG_CONFIG_HOME/agent-ways/ways/ # your own ways, same layout, survive updates
$PROJECT/.claude/ways/ # project ways, same layout, highest precedence
check-config-updates.sh sits one level up, in hooks/.
Script Relationships¶
flowchart LR
classDef trigger fill:#1565C0,stroke:#0D47A1,color:#fff
classDef shared fill:#6A1B9A,stroke:#4A148C,color:#fff
classDef util fill:#00695C,stroke:#004D40,color:#fff
classDef ext fill:#E65100,stroke:#BF360C,color:#fff
classDef other fill:#78909C,stroke:#546E7A,color:#fff
AD["check-*.sh · clear-markers.sh<br/>inject-subagent.sh · mark-tasks-active.sh"]:::trigger
RW["require-ways.sh<br/>ways hook <event>"]:::shared
AD --> RW
RW --> HOOK["ways hook"]:::shared
HOOK --> SCAN["scan lanes"]:::shared
HOOK --> SHOW["show"]:::shared
HOOK --> SESS["session state<br/>{SESSIONS_ROOT}"]:::shared
SCAN --> EMB["way-embed<br/>(subprocess, EN and multilingual GGUF)"]:::util
SCAN --> AGENT["ways agent<br/>(Unix socket, judge + key custody)"]:::util
AGENT --> API["provider API<br/>Anthropic or OpenRouter"]:::ext
SHOW --> MAC["macro.sh · postcheck.sh"]:::util
IP["issues-pull.sh · issues-task-created.sh"]:::other --> GT["gh-tasks"]:::other
AS["attend-drain-stop.sh"]:::other --> AT["attend inbox"]:::other
SL["strip-session-link-pre.sh"]:::other --> DENY["PreToolUse deny"]:::other
Multi-Trigger Semantics¶
What happens when one prompt matches several ways:
flowchart TB
classDef prompt fill:#1565C0,stroke:#0D47A1,color:#fff
classDef pattern fill:#6A1B9A,stroke:#4A148C,color:#fff
classDef gate fill:#E65100,stroke:#BF360C,color:#fff
classDef output fill:#2E7D32,stroke:#1B5E20,color:#fff
classDef silent fill:#78909C,stroke:#546E7A,color:#fff
Prompt["'Let's review the PR and fix the bug'"]:::prompt
Prompt --> KW1["github: pattern match"]:::pattern
Prompt --> KW2["debugging: pattern match"]:::pattern
Prompt --> KW3["quality: semantic fire"]:::pattern
KW1 --> F1{"g(s) ≥ τ_k?"}:::gate
KW2 --> F2{"g(s) ≥ τ_k?"}:::gate
F1 -->|no| G1["way_keyword_gated"]:::silent
F1 -->|yes| ORD["order_hits<br/>fixed admission order"]:::gate
F2 -->|yes| ORD
KW3 --> ORD
ORD --> J{"relevance judge<br/>one request, at most 8"}:::gate
J -->|block| B["way_judged block"]:::silent
J -->|pass| R{"per way: inside the refire window?"}:::gate
R -->|yes| S["way_suppressed refire"]:::silent
R -->|no| C{"fits the 10,000-char budget?"}:::gate
C -->|no| S2["way_suppressed context_cap"]:::silent
C -->|yes| O["shown"]:::output
Each way keeps its own marker, so several ways can fire from one prompt and each re-discloses on its own refire: cadence. A child that fired only because its parent fired earlier in the same scan is withheld when that parent is not shown.
Project-Local Override¶
flowchart TB
classDef proj fill:#E65100,stroke:#BF360C,color:#fff
classDef user fill:#6A1B9A,stroke:#4A148C,color:#fff
classDef core fill:#1565C0,stroke:#0D47A1,color:#fff
classDef marker fill:#00695C,stroke:#004D40,color:#fff
classDef skip fill:#78909C,stroke:#546E7A,color:#fff
T["way id softwaredev/delivery/github"] --> P
P{"1. Project<br/>$PROJECT/.claude/ways/"}:::proj
P -->|found| UseP["project way"]:::proj
P -->|not found| U{"2. User<br/>$XDG_CONFIG_HOME/agent-ways/ways/"}:::user
U -->|found| UseU["user way"]:::user
U -->|not found| C{"3. Core<br/>~/.claude/hooks/ways/<br/>→ $XDG_DATA_HOME/agent-ways/hooks/ways/"}:::core
C -->|found| UseC["shipped way"]:::core
C -->|not found| Skip["no way"]:::skip
UseP --> Mark["one marker per id and agent<br/>{SESSIONS_ROOT}/{sid}/ways/softwaredev/delivery/github/.marker.{agent_id}"]:::marker
UseU --> Mark
UseC --> Mark
The first root that has the id shadows it in every root below (ADR-143), for matching and for rendering alike. Project macros and postchecks run only if the project is listed in ~/.claude/trusted-project-macros.