You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Companion to #3243. That issue covers the retrieval plane (what context to
inject); this one covers the authority plane (what may change the loop). They
meet at one place — §5's sequencing — and are otherwise independent.
Evidence basis.main at a4b87649a (2026-08-13T14:44:24-07:00). Every
"shipped/not shipped" claim below was checked against the tree, not against the
issue that proposed it.
1. The requirement (Mac, 2026-08-13)
A plugin is more than a skill or a custom tool. It can:
change the definition of done — the exit condition of the turn loop
block turn completion to do synchronous work (author a witness, run it,
read the oracle flip) before the turn may finish
append to the system prompt when installed, and to the volatile part per turn
subscribe to lifecycle events — before/after enter loop, before/after exit
loop, on error/abort — where the event cannot complete until the plugin
answers OK
fire its own events
write to traces in a form that supports training custom models from
trace + oracle flip
be written in Python against an SDK, with Rust running parts of the loop
through it
The first customer is Oxagen's paid verification plugin.
2. The search: five issues, and the two that matter were closed five days ago
Both were closed in the 2026-08-12 mission-scope sweep with the same reason
("doesn't serve Stella's 4 mission goals… reopen with a mission-tie if that's
wrong"). A paid plugin platform is that mission tie.#2716 in particular is
not a nice-to-have here — see §4, O4.
3. The headline finding
Stella already has two plugin planes. They have complementary strengths, they
do not know about each other, and the plugin system is largely the work of
merging them.
97 declared event names, dotted and namespaced, matched exactly, by
wildcard (tool.*), or globally (*).
9 of them can block (names.rs:243-253): tool.call.requested, file.created/updated/deleted, command.started, git.commit/push.requested, pull_request.requested, deployment.requested.
Decision vocabulary: Allow | Modify{payload} | Deny{reason} | RequireApproval
(bus.rs:142). The chain folds Modify into the payload and stops on Deny
(bus.rs:489-540).
Explicitly open to extensions: "Extensions may emit custom names — the
catalog is the contract for what the host emits, not a closed set"
(names.rs:3-4).
Two hard limits. Handlers are synchronous closures
(Fn(&HookEvent) -> HookDecision, bus.rs:190) — a blocking handler cannot await. And registration requires compiled Rust.
Plane B — settings-declared shell hooks (out-of-process, any language)
JSON payload on stdin, JSON decision on stdout — same HookDecision
vocabulary as Plane A, folded by hooks/decision.rs::run_decision_hooks.
Async with a real timeout: 60 s default, 10 min hard ceiling
(hooks.rs:86-89).
Language-agnostic by construction. Python works today, with no SDK and no
new protocol.
Why this matters more than it looks
Three of the seven requirements in §1 are already shipped on Plane B:
"the system prompt needs to be updated/appended when the plugin is installed" — SessionStart stdout is appended to the system prompt, budgeted at 4,000 chars
with a visible truncation marker, fired exactly once per session
(crates/stella-cli/src/agent/engine.rs:459-524). The byte-stability
contract is stated there and is the plugin author's side of the bargain.
"the plugin needs to be called prior to a turn being called done" and "a way of blocking turn completion" — stop_hook_feedback
(crates/stella-core/src/driver/user_hooks.rs:254-308) runs at completion,
and a Deny{reason}holds the turn open and keeps stepping, injecting the
reason as a marked tail message. This is the verification plugin's core
mechanic, already built.
"blocking work" — Deny from a shell hook may take up to 10 minutes, in a
subprocess, without holding an engine thread.
And one property is deliberately, correctly in place for exactly this use case:
"Blocking policy hooks are never timed or skipped — a slow Deny must
still run." — bus.rs:283-285
4. Five opinions on the design as it exists
O1 — The "future object" is the wrong primitive. The deny-loop you already have is the right one.
The requirement says a plugin "has authority to return a future object that
promises something that isn't there yet." I'd argue against building that.
A future is a poor fit for three reasons specific to this codebase:
It is unreplayable. Determinism and replayability are non-negotiable for
the agent loop (CLAUDE.md). A promise resolved out-of-band has no deterministic
position in the journal; a Deny{reason} followed by more steps has exactly
one.
It has no natural failure mode. A future that never resolves wedges the
turn with no diagnostic. Deny already has the documented posture: a broken
Stop hook is a diagnostic and never a block (user_hooks.rs:300-306), because
"a broken Stop hook must not wedge the turn into never completing."
You already have the loop. Verification is inherently iterative: author
witness → run → no flip → tell the worker why → it works more → re-check. That is deny-and-continue. A promise models it as one long await; the deny loop
models it as what it is.
What actually needs to change, and it is small:
The once-per-turn latch.stop_fired (user_hooks.rs:262-265) fires the
Stop hooks at most once per turn, whatever they decide. Correct as a
footgun-guard for shell hooks; wrong for verification, which must be
consulted again after the worker responds to its feedback. Replace the
boolean with a bounded counter (the loop-steer precedent — MAX_LOOP_STEERS
— is the shape, and The last stuck-loop steer does not say it is the last one #2810 is the live lesson that the last iteration must
say it is the last one).
Deny{reason} carries only a String. For verification the plugin must
hand back structure: which witness, which test, flip achieved or not, the
digest. Widen the payload — this is invariant feat: multi-provider adapters (Gemini/Vertex/Bedrock) + STELLA logomark #5's "a caller has to branch on
it" test, and here the caller (the driver, and the trace fold) genuinely does.
RequireApproval at a turn boundary is currently surfaced as inapplicable
(user_hooks.rs:284-296). For a paid plugin that may need a human ("your
verification budget is exhausted, continue?") that will want a real answer.
O2 — Do not let plugins write traces. Make them emit journal events and leave the trace a fold.
The requirement is "plugins write to traces in a manner that supports using the
trace files + oracle flip to train custom models" — with the honest caveat "i
don't know the right architecture pattern on how to do this yet."
The pattern is already written down in this repo, in the module that would
receive the writes. crates/stella-cli/src/trace.rs:8-18:
Assembly, not a second capture path. …this module deliberately captures
nothing at runtime — it is a fold over what the closeout just settled… A
second live capture path would duplicate the journal and could drift from it;
the journal is already the append-only trace store.
So: a plugin emits events into the journal; the trace fold picks them up.
Nothing writes traces.jsonl but the fold. Consequences, all of them good:
Plugin-contributed facts inherit replayability for free.
They inherit TRACE_SCHEMA_VERSION (currently 3), whose own doc explains why
a reader must skip an unknown version rather than guess (trace.rs:50-66) —
precisely the discipline a training-data consumer needs.
The oracle-flip half is not new work: RewardLabel/RewardPolicy are already
on the record (stella_pipeline::reward), and the flip oracle already exists
in crates/stella-pipeline/src/verify.rs.
The one thing to add is a plugin event namespace in the bus catalog
(plugin.<id>.*), which names.rs:3-4 already contemplates, plus the fold arm
that reads it.
O3 — Out-of-process is the better substrate here, not the weaker one.
The instinct is usually that in-process = powerful, subprocess = fallback.
For this requirement it is inverted, and the code says so:
Plane A (in-process)
Plane B (subprocess)
Events
97
5
Can block
9 events
3 events
Async / long work
no — sync closure, cannot await
yes — 10 min ceiling, own process
Language
Rust only
any
Isolation
none — a panic is caught, a hang is not
process boundary
Packaging
recompile
a file path
A verification pass that runs a test suite is exactly the workload Plane A
cannot host and Plane B can. So the plugin system should be Plane B extended
with Plane A's event catalog, not Plane A opened up to dynamic loading. That is
also what #2684 already did for two events — the pattern is proved, it just needs
generalizing.
O4 — Authority is the real blocker, and it is the thing that was closed.
This is my strongest recommendation. A plugin that changes the definition of
done is the highest-authority object in the system — higher than a tool, higher
than a rule, because it decides when work stops. Today there is no vocabulary to
say so:
No RiskLevel, no requires_approval, no contract version.
ToolPolicy is session-global config, not per-caller — so "this plugin may
veto completion, that one may only observe" is inexpressible.
Hooks concatenate across scopes (crates/stella-cli/src/settings/merge.rs:39, :157-159) — no scope can remove another's. Fine for operator hooks; wrong for
plugins, where uninstall must actually uninstall.
Without this, a paid verification plugin and a hostile plugin have identical
authority, and the only control is "did the user put it in settings.json." That
is not a platform an enterprise buys.
Recommendation: reopen #2716 with the mission tie its close invited —
dataset generation for open-weight models (mission goal 4) runs on
plugin-authored trace records, and those are only trustworthy if the authoring
principal is named. It is a prerequisite, not a parallel track.
Note also #2780 (open): the evolution-surface ledger. A plugin is a sixth way
Stella changes itself, and that issue is the reviewed table where its evidence/authority/rollback row belongs.
O5 — Python: do not embed. And MCP is the candidate to beat.
Three existing out-of-process patterns, zero embedded interpreters
(rg pyo3 finds only tree-sitter-python, a grammar):
shell hooks — JSON on stdin, JSON decision on stdout
MCP — JSON-RPC over stdio, with a full client in stella-mcp
Embedding CPython would mean the GIL on an async runtime, a packaging story for
every platform Stella ships to, and a concretion inside crates that invariant #1
keeps free of them. The SDK should be a thin Python client over a protocol the
host already speaks.
Which protocol is the real decision, and MCP deserves a serious look. As a
plugin transport it brings, already built and already tested in-tree: stdio
JSON-RPC, a registry + stella mcp search/install, OAuth with auto-refreshing
tokens (.stella/private/mcp_oauth.json), a 401 suppression cache, per-server
enable/disable, and — decisively — an official, mature Python SDK Oxagen would
not have to write or maintain.
What MCP does not give you is the lifecycle-event half: it has no notion of
"the host is about to finish a turn; may it?" That is the delta, and it is a
documented protocol extension rather than a new protocol.
The honest counterweight: MCP tools are currently hardcoded read_only: false, speculation_safe: false (crates/stella-mcp/src/toolset.rs:776)
and are therefore invisible to every ReadOnlyTools view, and #2793's bypass is
an MCP-shaped hole. Both are fixable and both are worth fixing anyway.
I would not decide this from the armchair. The question is narrow enough to
answer with a spike: implement the verification plugin's Stop-hook path twice —
once as a settings-declared shell hook with a Python script, once as an MCP
server with a lifecycle extension — and compare on packaging, auth, and how much
protocol we end up owning.
5. What I would sequence
Deliberately ordered so the first two are useful even if the platform never
ships, and nothing irreversible happens before authority exists.
P0 — Reopen and land the authority vocabulary (#2716).Principal, RiskLevel, ToolContract, AuthzGate. Close #2793's bypass in the same
breath, since a plugin platform inherits it. Nothing else here should start
first.
P1 — Make the existing Stop-hook path production-grade for verification.
Bounded re-consultation instead of the once-per-turn latch; structured Deny
payload; RequireApproval answerable at a turn boundary. This alone makes the
verification plugin buildable as a shell hook, in Python, today.
P2 — Plugin event namespace + trace fold arm.plugin.<id>.* in the bus
catalog; the trace fold reads them. Delivers the training-data requirement
without a second capture path.
P3 — Loop-lifecycle events, and only then make them blocking. The names you
want (agent.turn.*, step.*) exist and emit, but none is in BLOCKING — agent.turn.parked is documented as "deliberately NOT in BLOCKING"
(names.rs:36-39). Adding turn/loop events to the blocking set is a real
change: blocking handlers are never timed or skipped by design, so a wedged
plugin on before_enter_loop stalls every turn forever. Blocking loop events
need Plane B's subprocess+timeout shape, not Plane A's sync closure.
P4 — Manifest, identity, install, uninstall (#1400's core, minus the
marketplace). Note #1400's non-negotiable rule still applies and is worth
re-reading before anyone writes a line: "the control plane is a guest, never a
component", enforced by rg -i 'oxagen' crates/ → zero matches. The Oxagen
verification plugin must be indistinguishable from any third party's, which is
a constraint on P0–P3 too, not just on P4.
P5 — The Python SDK, over whichever protocol the P1 spike settles.
P6 — Plugins as a steering source.#3243's SteeringSet gains a fifth
variant. Designing the source enum in that epic's Phase 2 with this in mind
costs nothing now.
6. What I did not verify
I did not run anything. No plugin, no hook, no trace was executed; every
claim is from reading main@a4b87649a.
The one load-bearing claim was checked, and holds.Stop-hook Deny
really does produce more steps: dispatch_completion
(crates/stella-core/src/driver/completion.rs:183-189) pushes the reason as a STOP_HOOK_MARKER_PREFIX-marked user message and returns None, so no TurnOutcome is produced and the step loop continues. The assistant message
is pushed to history first (:170-178), deliberately, "so the hook's
feedback answers a recorded turn." What I have not run is a live hook
end-to-end; this is a read of both halves of the call, not an execution.
The MCP recommendation in O5 is an argument, not a finding. I did not
check whether MCP's notification model can carry a blocking host→server
request, which is the question the spike exists to answer.
Companion to #3243. That issue covers the retrieval plane (what context to
inject); this one covers the authority plane (what may change the loop). They
meet at one place — §5's sequencing — and are otherwise independent.
Evidence basis.
mainata4b87649a(2026-08-13T14:44:24-07:00). Every"shipped/not shipped" claim below was checked against the tree, not against the
issue that proposed it.
1. The requirement (Mac, 2026-08-13)
A plugin is more than a skill or a custom tool. It can:
read the oracle flip) before the turn may finish
loop, on error/abort — where the event cannot complete until the plugin
answers OK
trace + oracle flip
through it
The first customer is Oxagen's paid verification plugin.
2. The search: five issues, and the two that matter were closed five days ago
ToolContract,AuthzGate,Principal,RiskLevelUserPromptSubmit,SubagentStart/StopStop,PreCompact,Modify, decisionscrates/stella-cli/src/trace.rs)Verification of the two NOT_PLANNED closes:
Both were closed in the 2026-08-12 mission-scope sweep with the same reason
("doesn't serve Stella's 4 mission goals… reopen with a mission-tie if that's
wrong"). A paid plugin platform is that mission tie. #2716 in particular is
not a nice-to-have here — see §4, O4.
3. The headline finding
Stella already has two plugin planes. They have complementary strengths, they
do not know about each other, and the plugin system is largely the work of
merging them.
Plane A —
HookBus(in-process, compiled)crates/stella-core/src/bus.rs+crates/stella-core/src/bus/names.rswildcard (
tool.*), or globally (*).names.rs:243-253):tool.call.requested,file.created/updated/deleted,command.started,git.commit/push.requested,pull_request.requested,deployment.requested.Allow | Modify{payload} | Deny{reason} | RequireApproval(
bus.rs:142). The chain foldsModifyinto the payload and stops onDeny(
bus.rs:489-540).catalog is the contract for what the host emits, not a closed set"
(
names.rs:3-4).quarantine after 3 consecutive overruns (
bus.rs:277-292), andforward_tois now a bounded
mpsc::Senderwith aForwardDroppedcounter (bus.rs:809).(
Fn(&HookEvent) -> HookDecision,bus.rs:190) — a blocking handlercannot await. And registration requires compiled Rust.
Plane B — settings-declared shell hooks (out-of-process, any language)
crates/stella-core/src/hooks.rs+crates/stella-core/src/driver/user_hooks.rsSessionStart,PreToolUse,PostToolUse,Stop,PreCompact.HookDecisionvocabulary as Plane A, folded by
hooks/decision.rs::run_decision_hooks.(
hooks.rs:86-89).new protocol.
Why this matters more than it looks
Three of the seven requirements in §1 are already shipped on Plane B:
"the system prompt needs to be updated/appended when the plugin is installed" —
SessionStartstdout is appended to the system prompt, budgeted at 4,000 charswith a visible truncation marker, fired exactly once per session
(
crates/stella-cli/src/agent/engine.rs:459-524). The byte-stabilitycontract is stated there and is the plugin author's side of the bargain.
"the plugin needs to be called prior to a turn being called done" and
"a way of blocking turn completion" —
stop_hook_feedback(
crates/stella-core/src/driver/user_hooks.rs:254-308) runs at completion,and a
Deny{reason}holds the turn open and keeps stepping, injecting thereason as a marked tail message. This is the verification plugin's core
mechanic, already built.
"blocking work" —
Denyfrom a shell hook may take up to 10 minutes, in asubprocess, without holding an engine thread.
And one property is deliberately, correctly in place for exactly this use case:
4. Five opinions on the design as it exists
O1 — The "future object" is the wrong primitive. The deny-loop you already have is the right one.
The requirement says a plugin "has authority to return a future object that
promises something that isn't there yet." I'd argue against building that.
A future is a poor fit for three reasons specific to this codebase:
the agent loop (CLAUDE.md). A promise resolved out-of-band has no deterministic
position in the journal; a
Deny{reason}followed by more steps has exactlyone.
turn with no diagnostic.
Denyalready has the documented posture: a brokenStop hook is a diagnostic and never a block (
user_hooks.rs:300-306), because"a broken Stop hook must not wedge the turn into never completing."
witness → run → no flip → tell the worker why → it works more → re-check. That
is deny-and-continue. A promise models it as one long await; the deny loop
models it as what it is.
What actually needs to change, and it is small:
stop_fired(user_hooks.rs:262-265) fires theStop hooks at most once per turn, whatever they decide. Correct as a
footgun-guard for shell hooks; wrong for verification, which must be
consulted again after the worker responds to its feedback. Replace the
boolean with a bounded counter (the loop-steer precedent —
MAX_LOOP_STEERS— is the shape, and The last stuck-loop steer does not say it is the last one #2810 is the live lesson that the last iteration must
say it is the last one).
Deny{reason}carries only aString. For verification the plugin musthand back structure: which witness, which test, flip achieved or not, the
digest. Widen the payload — this is invariant feat: multi-provider adapters (Gemini/Vertex/Bedrock) + STELLA logomark #5's "a caller has to branch on
it" test, and here the caller (the driver, and the trace fold) genuinely does.
RequireApprovalat a turn boundary is currently surfaced as inapplicable(
user_hooks.rs:284-296). For a paid plugin that may need a human ("yourverification budget is exhausted, continue?") that will want a real answer.
O2 — Do not let plugins write traces. Make them emit journal events and leave the trace a fold.
The requirement is "plugins write to traces in a manner that supports using the
trace files + oracle flip to train custom models" — with the honest caveat "i
don't know the right architecture pattern on how to do this yet."
The pattern is already written down in this repo, in the module that would
receive the writes.
crates/stella-cli/src/trace.rs:8-18:So: a plugin emits events into the journal; the trace fold picks them up.
Nothing writes
traces.jsonlbut the fold. Consequences, all of them good:TRACE_SCHEMA_VERSION(currently 3), whose own doc explains whya reader must skip an unknown version rather than guess (
trace.rs:50-66) —precisely the discipline a training-data consumer needs.
.stella/private/0700, file 0600, everystring leaf through
redact_secrets, and nothing entersstore.db, soinvariant feat: port Phase 3/4/5 crates (context/fleet/graph/mcp/media/pipeline/tui) from the monorepo #3's content-free gate is not even in the path (
trace.rs:30-37).A plugin writing traces directly would route around all of that.
RewardLabel/RewardPolicyare alreadyon the record (
stella_pipeline::reward), and the flip oracle already existsin
crates/stella-pipeline/src/verify.rs.The one thing to add is a plugin event namespace in the bus catalog
(
plugin.<id>.*), whichnames.rs:3-4already contemplates, plus the fold armthat reads it.
O3 — Out-of-process is the better substrate here, not the weaker one.
The instinct is usually that in-process = powerful, subprocess = fallback.
For this requirement it is inverted, and the code says so:
A verification pass that runs a test suite is exactly the workload Plane A
cannot host and Plane B can. So the plugin system should be Plane B extended
with Plane A's event catalog, not Plane A opened up to dynamic loading. That is
also what #2684 already did for two events — the pattern is proved, it just needs
generalizing.
O4 — Authority is the real blocker, and it is the thing that was closed.
This is my strongest recommendation. A plugin that changes the definition of
done is the highest-authority object in the system — higher than a tool, higher
than a rule, because it decides when work stops. Today there is no vocabulary to
say so:
Principal— the pre-dispatch seams see{tool, input}and nothing aboutwho is asking (tool-first: the governance half — ToolContract, AuthzGate port + Principal, ToolCtx events, contracts over the serve wire, manifest parity #2716's own survey).
RiskLevel, norequires_approval, no contract version.ToolPolicyis session-global config, not per-caller — so "this plugin mayveto completion, that one may only observe" is inexpressible.
crates/stella-cli/src/settings/merge.rs:39,:157-159) — no scope can remove another's. Fine for operator hooks; wrong forplugins, where uninstall must actually uninstall.
chains. A plugin system built on today's seams inherits that hole on day one.
Without this, a paid verification plugin and a hostile plugin have identical
authority, and the only control is "did the user put it in settings.json." That
is not a platform an enterprise buys.
Recommendation: reopen #2716 with the mission tie its close invited —
dataset generation for open-weight models (mission goal 4) runs on
plugin-authored trace records, and those are only trustworthy if the authoring
principal is named. It is a prerequisite, not a parallel track.
Note also #2780 (open): the evolution-surface ledger. A plugin is a sixth way
Stella changes itself, and that issue is the reviewed table where its
evidence/authority/rollbackrow belongs.O5 — Python: do not embed. And MCP is the candidate to beat.
Three existing out-of-process patterns, zero embedded interpreters
(
rg pyo3finds onlytree-sitter-python, a grammar):STELLA_INPUT_*env(
crates/stella-tools/src/custom.rs:32-67)stella-mcpEmbedding CPython would mean the GIL on an async runtime, a packaging story for
every platform Stella ships to, and a concretion inside crates that invariant #1
keeps free of them. The SDK should be a thin Python client over a protocol the
host already speaks.
Which protocol is the real decision, and MCP deserves a serious look. As a
plugin transport it brings, already built and already tested in-tree: stdio
JSON-RPC, a registry +
stella mcp search/install, OAuth with auto-refreshingtokens (
.stella/private/mcp_oauth.json), a 401 suppression cache, per-serverenable/disable, and — decisively — an official, mature Python SDK Oxagen would
not have to write or maintain.
What MCP does not give you is the lifecycle-event half: it has no notion of
"the host is about to finish a turn; may it?" That is the delta, and it is a
documented protocol extension rather than a new protocol.
The honest counterweight: MCP tools are currently hardcoded
read_only: false, speculation_safe: false(crates/stella-mcp/src/toolset.rs:776)and are therefore invisible to every
ReadOnlyToolsview, and #2793's bypass isan MCP-shaped hole. Both are fixable and both are worth fixing anyway.
I would not decide this from the armchair. The question is narrow enough to
answer with a spike: implement the verification plugin's Stop-hook path twice —
once as a settings-declared shell hook with a Python script, once as an MCP
server with a lifecycle extension — and compare on packaging, auth, and how much
protocol we end up owning.
5. What I would sequence
Deliberately ordered so the first two are useful even if the platform never
ships, and nothing irreversible happens before authority exists.
P0 — Reopen and land the authority vocabulary (#2716).
Principal,RiskLevel,ToolContract,AuthzGate. Close #2793's bypass in the samebreath, since a plugin platform inherits it. Nothing else here should start
first.
P1 — Make the existing Stop-hook path production-grade for verification.
Bounded re-consultation instead of the once-per-turn latch; structured
Denypayload;
RequireApprovalanswerable at a turn boundary. This alone makes theverification plugin buildable as a shell hook, in Python, today.
P2 — Plugin event namespace + trace fold arm.
plugin.<id>.*in the buscatalog; the trace fold reads them. Delivers the training-data requirement
without a second capture path.
P3 — Loop-lifecycle events, and only then make them blocking. The names you
want (
agent.turn.*,step.*) exist and emit, but none is inBLOCKING—agent.turn.parkedis documented as "deliberately NOT inBLOCKING"(
names.rs:36-39). Adding turn/loop events to the blocking set is a realchange: blocking handlers are never timed or skipped by design, so a wedged
plugin on
before_enter_loopstalls every turn forever. Blocking loop eventsneed Plane B's subprocess+timeout shape, not Plane A's sync closure.
P4 — Manifest, identity, install, uninstall (#1400's core, minus the
marketplace). Note #1400's non-negotiable rule still applies and is worth
re-reading before anyone writes a line: "the control plane is a guest, never a
component", enforced by
rg -i 'oxagen' crates/ → zero matches. The Oxagenverification plugin must be indistinguishable from any third party's, which is
a constraint on P0–P3 too, not just on P4.
P5 — The Python SDK, over whichever protocol the P1 spike settles.
P6 — Plugins as a steering source. #3243's
SteeringSetgains a fifthvariant. Designing the
sourceenum in that epic's Phase 2 with this in mindcosts nothing now.
6. What I did not verify
claim is from reading
main@a4b87649a.Stop-hookDenyreally does produce more steps:
dispatch_completion(
crates/stella-core/src/driver/completion.rs:183-189) pushes the reason as aSTOP_HOOK_MARKER_PREFIX-marked user message and returnsNone, so noTurnOutcomeis produced and the step loop continues. The assistant messageis pushed to history first (
:170-178), deliberately, "so the hook'sfeedback answers a recorded turn." What I have not run is a live hook
end-to-end; this is a read of both halves of the call, not an execution.
check whether MCP's notification model can carry a blocking host→server
request, which is the question the spike exists to answer.
pub constdeclarations inbus/names.rs, not a hand-audit that each is emitted. HookBus lifecycle event catalog exists (session.*, model.*, step.*) but nothing calls emit_named at those boundaries #1133 wired theemitters; I did not re-verify each one.
against the AGPL/commercial dual track. That interacts with Epic: Stella Apps — a vendor-neutral extension platform (TOML manifest · OAuth lifecycle · host API · marketplace) #1400's "an app is
data, never Rust in this tree" rule and deserves its own read.
Refs #1400, #2716, #2836, #2684, #1133, #459, #1042, #2793, #2780, #3243.