Audience: LLM implementers and developers building or extending the Code plugin for Qbix. Self-contained for the Code plugin; assumes familiarity with Safebox (
Safebox/README.md) and Grokers (Grokers/Grokers.md).
- Overview & Philosophy
- Plugin Dependencies
- Plugin File Structure
- Stream Types — What Code Owns and What It Reads
- Workspace Model
- Attribute Indexes for Search
- The Workflow Family
- Tools
- Capabilities
- Event Handlers
- Contract Verification (A/B/C)
- Ripple to Callers
- Git Projection
- Naming Conventions
- Installation Script
- What Code Doesn’t Invent — Substrate Primitives Used As-Is
- Open Questions
- Workflow Signaling and Resume Semantics
The Code plugin applies Safebox’s governed workflow primitive to source code, using Grokers’ typed knowledge graph as the substrate. It introduces the absolute minimum of new ontology — four Code-owned stream types, two capabilities, twelve tools, three workflows — and otherwise rides entirely on what Grokers, Safebox, and Streams already provide.
What this plugin does: takes a Grokers-indexed codebase, clones it into a workspace, proposes structured modifications using the Grokers contract attributes (preconditions, postconditions, sideEffects, invariants — all prose strings written by the Grokers analyzer agent), verifies contract preservation, ripples to callers when contracts change incompatibly, runs covering tests, and projects successful sprints to git as long-lived branches.
What this plugin does not do: index codebases (that’s Grokers), provide the workflow primitive (that’s Safebox), provide the substrate (that’s Qbix Streams).
The architectural commitment, lifted from the Safebox arXiv paper: workflows are domain-agnostic; what makes a workflow useful is the right combination of substrate primitives. Code is the worked example for source code as the domain. Other domain plugins (infrastructure, regulatory filings, scientific data) follow the same pattern.
Qbix Streams (Qbix core)
├── Grokers ≥ 0.1
│ for: Grokers/repo, Grokers/symbol, Grokers/extern,
│ Grokers/calls, Grokers/covers, Grokers/concept,
│ Grokers/clue, ontology.json
└── Safebox ≥ 0.37
for: workflows, tools, capabilities, action proposals,
governance, conventions, sandbox API,
Streams.ontology()
│
Code (this plugin) ≥ 0.1
│
Safebots, future agent layers
Code does NOT extend Grokers’ ontology. It uses Grokers types as-is. The only new types Code defines are bookkeeping: Code/sprint, Code/finding, Code/branch, Code/testRun.
Code/
package.json @qbix/code, peer-deps Safebox + Grokers
README.md this document
config/
plugin.json dependency declaration, event handlers, stream type defaults
workflows/ workflow JSON definitions seeded at install
code-modify.json
code-rename.json
code-document.json
code-cloneRepo.json
code-audit.json
code-notify.json
classes/
Code.php static helpers (gitProjection, branchName, sourceRepoOf)
Code/
Code.js Node executor RPC entry (cloneWorkspace, destroyWorkspace, pushBranch)
Workspace.js ZFS snapshot/clone/destroy via child_process
Projection.js git add+commit+push via child_process
files/
Safebox/
tools/ tool source — codeFile paths relative here
find/code/callers.js
find/code/coveringTests.js
propose/code/rewrite.js
verify/code/contract.js
apply/code/rewrite.js
ripple/code/callers.js
clone/code/repo.js
clone/code/upstream.js
refresh/code/comprehension.js
run/code/tests.js
complete/code/sprint.js
summarize/code/changes.js
audit/code/finding.js
scan/code/symbols.js
notify/code/subscribers.js
capabilities/
Code/
workspace.js materializes a workspace ZFS clone
repo.js materializes a Grokers/repo via git clone + ZFS create
testRun.js materializes Code/testRun
branch.js materializes Code/branch
handlers/
Code/
after/
Q_Plugin_install.php triggers migration
message/
Code_workspaceStarted.php ZFS clone after Code/workspaceStarted message
Code_sprintCompleted.php git push after Code/sprintCompleted message
Streams/
after/
Streams_Stream_save_Grokers_repo.php ZFS destroy on workspace close (closedTime set)
scripts/
Code/
0.1-Code.mysql.php install: stream type registration, tool/capability/workflow seeding,
default convention seeding for PHP/JS/Python
The files/Safebox/{tools,capabilities}/ layout is mandatory — it matches the canonical Safebox plugin convention so the Safebox/codeFilePlugin: 'Code' attribute resolves the path correctly.
| Type | Purpose | Materialized by |
|---|---|---|
Code/sprint |
A code modification sprint — workflow execution producing one workspace repo, one set of modifications, optionally one git branch. | created by workflows; not externally materialized |
Code/finding |
An audit observation needing investigation. | created by audit/code/finding tool |
Code/branch |
A git branch produced by a successful sprint, with commit sha. | Safebox/capability/Code/branch |
Code/testRun |
A test execution record with pass/fail and captured output. | Safebox/capability/Code/testRun |
| Type | Used for |
|---|---|
Grokers/repo |
source and workspace repos — the unit operations target |
Grokers/module |
plugin/package containment unit; maps to a Qbix plugin or equivalent |
Grokers/class |
class containment — a bucket of methods |
Grokers/method |
callable named unit (class method or top-level function) |
Grokers/symbol |
non-class, non-method symbols — constants, type aliases, traits, decorators, enum members, etc. |
Grokers/component |
Qbix tool definition or framework-registered component (Q.Tool.define, React component, etc.) |
Grokers/hook |
event-bridge stream with three-phase semantics (before/primary/after); promoted from Grokers/extern category in Grokers update 2 |
Grokers/extern |
bridge node for cross-process dispatch — config keys (config), text strings (text), templates (template), HTTP endpoints (endpoint), ORM tables (table), message types, socket events, env vars, and other categories. Hooks are now their own stream type (see above). |
Grokers/pattern |
per-callee aggregate of arg distributions and suspected bugs across all call sites; read by audit/code/finding |
Grokers/concept |
confirmed concepts injected into LLM prompts in propose/code/rewrite |
Grokers/clue |
unresolved investigations — informs audit findings; v0.1+ clue types include possible-dynamic-tool-instantiation, possible-dynamic-dispatch, possible-implicit-handler, possible-metaprogramming, possible-dynamic-event-binding |
Grokers/test |
tests that cover a symbol — filtered execution via find/code/coveringTests |
Tranche dependency. The split of
Grokers/symbolintoGrokers/module,Grokers/class,Grokers/methodplus residualGrokers/symbolis requested in the Code-to-Grokers feature request, tranche 1. Until tranche 1 lands on the Grokers side, Code’s tools fall back to filteringGrokers/symbolstreams byGrokers/typeattribute (function/method/class/constant). Tools that depend on tranche 2 (HTML visitor, handlers/ walker, plugin.json visitor, JS event property-path resolver) report partial coverage withnoteoutputs explaining what’s missing. See §17.
Per Grokers’ config/ontology.json, these are prose strings written by the LLM analyzer agent (not arrays, not structured fields):
Grokers/preconditions— what must be true to call this symbolGrokers/postconditions— what is true after it returnsGrokers/sideEffects— DB writes, stream mutations, events fired, filesystem changesGrokers/invariants— properties that hold throughout executionGrokers/doc— generated documentation (markdown)Grokers/qualName— fully-qualified symbol nameGrokers/file— relative file pathGrokers/language— enum: php / javascript / typescript / python / go / rust / swift / java / c / cppGrokers/symbolHash,Grokers/astHash— for staleness detection
Contract attributes apply to Grokers/method streams (and to Grokers/class for class-level invariants). Code’s tools must handle all four contract attributes being absent — comprehension may not yet have run on a symbol, in which case the LLM is instructed to re-derive contract from source.
A Grokers/pattern stream is a materialized aggregate per callee — one stream per Grokers/method (or Grokers/symbol) with at least one caller. Maintained incrementally by Grokers’ indexer aggregator after every regrok pass. Reading the aggregate replaces scanning every Grokers/calls relation to compute distributions.
Grokers/callee— stream name of the calleeGrokers/callerCount— distinct caller symbolsGrokers/callSiteCount— total call site edgesGrokers/arg0dist— JSON bucket map of first-arg shapes, e.g.{"dynamic:this": 31, "literal:true": 8}Grokers/arg1dist— JSON bucket map for second-arg positionGrokers/suspectedBugs— count of call sites where hints flaggedrequiresPairedRemove=truebut no matching.remove()was found in the same symbol — surfaces likely Q.Event leak patterns and similarGrokers/updatedTime— Unix ms timestamp of the last aggregation pass
The substrate convention: containment relations are named after the contained-end stream type, walked bidirectionally via isCategory. A bucket walked with isCategory=true returns its contents; a contained stream walked with isCategory=false returns its bucket(s).
Containment relations:
| Relation | Bucket → contents | Contents → bucket |
|---|---|---|
Grokers/module |
repo → modules in it | module → its repo |
Grokers/class |
module → classes in it (or repo → classes) | class → its module |
Grokers/method |
class → its methods, or module → top-level functions | method → its class or module |
Grokers/symbol |
repo or module → other symbols (constants, type aliases, etc.) | symbol → its container |
Verb-relations (cross-cutting dependencies, not containment):
| Relation | Direction | What Code uses it for |
|---|---|---|
Grokers/calls |
caller → callee | finding callers (reverse via isCategory=true); finding callees (forward via isCategory=false) |
Grokers/invokes |
symbol → hook (or extern) | symbol fires a hook (e.g. Q.handle("Foo.bar", ...), Q::event(...) → targets Grokers/hook/Foo.bar); extra carries `phase: “before” |
Grokers/subscribes |
symbol → hook | symbol registers to receive a hook event (e.g. Q.Event.set calls); pairs with Grokers/invokes to form a dispatch bridge |
Grokers/handles |
handler-method → hook | for PHP handlers/<plugin>/<phase>/<event>.php convention AND plugin.json handlersAfterEvent registrations (materialized by Grokers’ config-connector); extra carries `phase: “before” |
Grokers/activates |
config-file symbol → handler method | for plugin.json handlersAfterEvent/handlersBeforeEvent/handlersValidateEvent registrations |
Grokers/instantiates |
caller-symbol or markup-symbol → component | a Q::tool() call, a Q.Tool.create() call, or a data-tool="..." attribute each emit one of these to a Grokers/component |
Grokers/reads |
symbol → extern | symbol reads from a config/template/registry (e.g. Q.Template.load, Q.Config.get) |
Grokers/writes |
symbol → extern | symbol writes to a registry (e.g. Q.Template.set, Q.Config.set) |
Grokers/covers |
test → symbol | finding tests covering a symbol (reverse) |
Grokers/concept |
repo → concept | injecting confirmed concepts into prompts |
Grokers/pattern |
callee → pattern stream | reaching the per-callee aggregate from a callee symbol |
Per Grokers’ ontology, weight on these relations stores the source line number, and extra is a JSON-encoded object carrying:
{
line, certainty, ctrl?, ctrlType?, note?,
arg0kind, arg0text?, arg0val?,
arg1kind, arg1text?, arg1val?
}
arg0kind/arg1kind values:
"literal"— static literal extractable at parse time;arg0valcarries the parsed value"dynamic"— non-literal expression;arg0textcarries the raw node text (e.g."this","$var","fn()")"absent"— argument position not present in the call"array"— array literal;arg0valholds JSON-encoded element list
The arg fields matter especially for Q.Event.prototype.set calls in Qbix codebases: arg0text="this" means the handler is keyed to a Q.Tool instance and gets auto-removed on tool destroy (no manual cleanup needed); arg0val=true means page-unload auto-removal; a string-literal key means the call requires a paired .remove(key) in the destroy path or it’s a leak. Code’s ripple/code/callers consumes these fields to classify caller adaptations more accurately than line numbers alone.
A code modification workspace is a copy of the source Grokers/repo plus its symbol streams, sitting at a different filesystem path. Symbols modified inside a workspace are independent from the originals.
(A) Streams::fork — the substrate-native primitive. The Streams plugin ships Streams::fork($asUserId, $publisherId, $streamName, $ordinal, $toPublisherId, $toStreamName, $options). It copies fields, sets up a JSON fork chain on the new stream, fires Streams/fork/{type} before/after events, and posts a Streams/forked message on the new stream. The fork chain enables transparent message-history traversal across the source and fork via Streams_Message::forkChain and the workspace cascade in Streams::related (the cascade is on the v1.3.1 roadmap; not yet active). The convention is toPublisherId = "{publisherId}~{workspaceId}".
(B) Plain new stream with back-reference attribute — Code’s transitional approach. The clone/code/repo tool creates a fresh Grokers/repo stream under the community publisher, with a Code/sourceRepoUri attribute pointing back at the source. No fork chain, no shared message history. Side effects (ZFS clone, attribute population) happen via the Code/workspaceStarted message hook.
Streams::fork is a substrate-level PHP primitive. To use it from a Safebox tool, the tool would need an Action.propose('Streams/fork', ...) action type — and Safebox v0.37 does not yet expose Streams/fork in its Action.php switch statement (only Streams/create, Streams/update, Streams/relate, Streams/post-message, etc.). Until Safebox adds Streams/fork as a governed action type, Code tools cannot use the substrate’s fork primitive directly.
The transitional approach is functionally adequate — Code’s workflows don’t depend on fork-chain message inheritance. When Safebox adds Streams/fork as an action type, clone/code/repo will switch to using it and gain the fork-chain benefits at no cost to the calling workflow.
- Reads the source repo’s
Grokers/repoHash. - Computes a workspace
repoHashdeterministically from(sourceRepoHash, workspaceLabel, timestamp), slugged to 16 hex chars per Grokers convention. - Proposes
Streams/create:Grokers/repowith the new hash,Code/sourceRepoUriandCode/sourceRepoHashattributes pointing back, andSafebox/status='pending'. - Posts
Code/workspaceStartedon the new workspace repo. TheCode_workspaceStartedhandler runszfs snapshotandzfs clone, updates the workspace repo’sGrokers/repoRootandGrokers/zfsDatasetto point at the clone’s mountpoint, and copies the source’sCode/testCommandattribute onto the workspace if not already inherited.
After the handler completes, the workspace looks like a fresh Grokers/repo at a different mountpoint. Symbol modifications inside the workspace become Streams/update actions on Grokers/symbol/{newRepoHash}/... streams; the originals at Grokers/symbol/{sourceRepoHash}/... are untouched. When the workspace is closed (via Streams::close), the save-after hook detects closedTime was just set on a stream carrying Code/zfsCloneName and tears down the ZFS clone.
Code uses the substrate’s built-in attribute-indexing mechanism — Streams_Stream::registerRelations plus syncRelations. When a stream’s attribute changes, syncRelations automatically creates/destroys relations of the form attribute/{key}={value} between the index stream and the source stream. Tools query the index via Streams.related(idxPub, idxName, 'attribute/{key}={value}', isCategory=true, opts) to find streams by attribute value.
This is exactly the mechanism Grokers’ search buckets (Grokers/search/symbols, etc.) use. Code adopts the same pattern rather than inventing parallel infrastructure.
| Index stream | Stream type | Indexed attributes |
|---|---|---|
Safebox/index/Code/finding |
Code/finding |
Safebox/status, Code/severity, Code/findingType, Code/status |
Safebox/index/Code/branch |
Code/branch |
Safebox/status |
Safebox/index/Code/testRun |
Code/testRun |
Safebox/status, Code/passed |
(v0.3 also registered an index for Code/sprint, but no v0.1 tool actually creates streams of that type — the workload itself is Safebox/workload, and per-sprint metadata lives on the workspace Grokers/repo. The unused index has been removed in v0.4.)
Registration happens via:
Streams_Stream::registerRelations(
'', // any publisher
'Code/finding', // stream type
$executorId, // index publisher
'Safebox/index/Code/finding', // index stream
array('Safebox/status', 'Code/severity', 'Code/findingType', 'Code/status')
);After registration, anywhere a Code/finding stream is saved with Code/severity = 'high', syncRelations creates an attribute/Code/severity=high relation between the index and the finding. Tools find all high-severity findings via:
var rels = await Streams.related(executorId, 'Safebox/index/Code/finding',
'attribute/Code/severity=high', true, { limit: 100 });Per the substrate convention this is how all attribute-based search works — Grokers uses it for symbols, Safebox uses it for actions, Code uses it for findings/sprints/branches/testRuns. No bespoke search bucket infrastructure.
v0.1 ships eight workflows with full step DAGs. The shape mirrors safebots.ai/coding.html: each workflow forks a workspace, reads context, decides, applies, verifies, completes. Composition between workflows happens through messages and handlers — not a central orchestrator.
15+ steps including progress messages and conditional terminal paths:
clone-workspace— create workspace repo (transitional pattern; will useStreams::forkwhen available)find-covering-tests— locate tests covering this symbol viaGrokers/coverspropose-rewrite— LLM proposal grounded in concepts + conventions + contractverify-contract— A/B/C classificationpost-rewrite-proposed— firesCode/rewriteProposedprogress messageapply-rewrite—Streams/updateon workspace symbol; returns line counts and shift parametersshift-line-numbers— firesCode/lineShiftso the PHP handler bulk-shifts subsequent symbols’ position-in-file (Streams/filerelation weights) and outgoing verb-relations’ weights via SHIFT MODE ofStreams::updateRelations. Advisory; Grokers’ regrok overwrites with authoritative line numbers.ripple-callers— only when class C; bounded bydepthRemaining; firesCode/rippleStarted/Code/rippleEnded/Code/depthExhausted/Code/checkpointRequestedper outcomenotify-subscribers— only when class C; fans out subscriber + hook + extern + associated-peer notifications with phase metadata (the associated walk surfaces LLM-discovered semantic peers per Grokers Update 4)post-tests-started— firesCode/testRunStartedrun-tests— execute viaSafebox/capability/Code/testRunpost-tests-completed— firesCode/testRunCompletedsummarize-changes— walksCode/rewriteAppliedmessages on workspace repo, produces markdown changelogrefresh-comprehension— postsCode/regrokRequestedadvisory message and triggers Grokers pattern aggregatorcomplete-sprint-success/complete-sprint-with-findings/complete-sprint-aborted— three terminal variants; see §18
Edge conditions:
apply-rewrite → shift-line-numbers(always)shift-line-numbers → ripple-callersif class is Cshift-line-numbers → post-tests-startedif class is A or B (skip ripple+notify)ripple-callers → notify-subscribersifcheckpointRequested != truepost-tests-completed → summarize-changesifpassed === true; else→ complete-sprint-abortedrefresh-comprehension → complete-sprint-with-findingsifdepthCapReached == true; else→ complete-sprint-success
A failing test takes the abort path. The workspace stays for inspection. Depth-exhausted cascade takes the with-findings path; the workspace is valid but not all callers were processed.
6 steps. No contract verification (rename preserves contract by construction). Per-caller fan-out is left to follow-on workloads spawned by the orchestrator. Includes summarize + complete-sprint to produce a git commit when projection is configured.
5 steps. Reuses propose/code/rewrite with a docstring-only change description; the LLM constraint enforces “do not modify the function body.”
1 step (clone-upstream) wrapping the clone/code/upstream tool, which calls the Code/repo capability for git clone + ZFS dataset creation, and creates a fresh Grokers/repo stream. Used for initial deployment, periodic refresh (clone at a newer commit; old repo stays as historical record), and historical analysis (clone at an older commit). All three uses go through the same workflow.
2 steps:
scan-symbols— walks symbols in aGrokers/repomatching an audit query (missingContract,minComplexity,language,qualNamePattern, attribute equality,minSuspectedBugs). Read-only.emit-findings— fan-outaudit/code/findingper candidate, creatingCode/findingstreams for human or downstream-agent investigation.
The query is a structured object passed in by the caller. Examples: {missingContract: true} finds symbols missing one or more Grokers/preconditions/postconditions/sideEffects/invariants attributes; {minComplexity: 30, language: "javascript"} finds expensive JS symbols; {attributeKey: "Grokers/securitySensitive", attributeValue: true} finds symbols pre-flagged; {minSuspectedBugs: 1} finds callees whose Grokers/pattern aggregate flagged at least one likely-leak call site (most commonly Q.Event.set calls with string-literal keys missing a paired .remove()).
2 steps. Parallel to code/audit but walks the Grokers/clue graph rather than symbol attributes. Used for surfacing static-analysis boundaries (possible-dynamic-dispatch, possible-implicit-handler, etc.) and Grokers-derived semantic observations (possible-doc-discrepancy from tier-2 grok pass — see §18.9). Filters by clueType and minSeverity.
2 steps. Walks Grokers/selector streams (per Grokers Update 4 — populated by the css.js, html.js, handlebars.js, jsx.js visitors during indexing) for CSS quality issues. Three scanType values:
cssConflicts— surface selectors withGrokers/hasConflict=trueand at least N (default 2) contributing rule blocks. Multiple files defining competing properties for the same selector means cascade order determines outcome — brittle to refactor. Severity scales with rule count.unusedSelectors— selectors defined in CSS (Grokers/ruleCount > 0) but never referenced in HTML or templates (Grokers/usageCount === 0). Dead style rules. Low severity.undefinedSelectors— selectors referenced in HTML or templates (Grokers/usageCount > 0) but no CSS rule defines them (Grokers/ruleCount === 0). Likely broken or stale styling. High severity.
Findings emit with findingType: "css-{scanType}". The reason text in each finding includes the selector text, specificity tuple, and counts.
1 step wrapping notify/code/subscribers. Fans out subscriber + hook-bridge + extern-bridge + associated-peer notifications. The hook walks include phase metadata so recipients understand whether the contract change affects a before/primary/after handler relationship. The associated walk (per Grokers Update 4) traverses Grokers/associated edges in both directions, surfacing LLM-discovered semantic peers — pairs the analyzer flagged as semantic-pair, always-used-together, precondition, postcondition, alternative, or replaces. Same machinery code/modify uses inline; exposed as a workflow so external parties can trigger it directly (e.g., when an upstream contract changes outside a Code workflow’s scope).
code/split— decompose a function into helperscode/inline— eliminate a helper, propagate bodycode/test— generate tests for under-covered symbolscode/migrate— fan out parallelcode/modifysub-workloadscode/createSymbol— for new-code authoring scenarios (anticipated v0.5). When implemented, will walkGrokers/example(a relation, not attribute) from the relevantGrokers/patternstream — typically the one withpatternType=component-file-structurefor the target module — to find the top-weighted reference implementations, then read each example method’s file at itsstartLine/endLineextras for RAG injection into the generation prompt. Per Grokers Update 4 plus correction:Grokers/examplecarriesscore(also the relation weight),reason,startLine,endLineextras.
All 20 tools follow the Safebox plugin-shipped tool convention:
- File path:
files/Safebox/tools/<verb>/code/<noun>.js - Stream name:
Safebox/tool/<verb>/code/<noun> Safebox/codeFile:<verb>/code/<noun>.js(relative tofiles/Safebox/tools/)Safebox/codeFilePlugin:'Code'Safebox/sha256: SHA-256 of the file at install time
Per Safebox’s runtime contract enforcement, every Action.propose call’s type must be in the tool’s declared Safebox/actionTypes. v0.1 uses bare action verbs only (e.g., Streams/update) — no colon-suffixed forms — because Safebox v0.37’s action-type regex ^[A-Za-z][A-Za-z0-9_./-]{0,127}$ rejects colons. The stream type the action targets goes inside the payload as streamType, not in the action type string.
| Tool | actionTypes |
|---|---|
find/code/callers |
[] |
find/code/coveringTests |
[] |
propose/code/rewrite |
[] |
verify/code/contract |
[] |
apply/code/rewrite |
[ Streams/update, Streams/message ] |
ripple/code/callers |
[ Streams/create, Streams/message, Streams/update ] (sub-workloads + progress messages + checkpoint state) |
clone/code/repo |
[ Streams/create, Streams/message ] (workspace repo + workspaceStarted) |
clone/code/upstream |
[ Streams/create ] (fresh Grokers/repo) |
refresh/code/comprehension |
[ Streams/message ] |
run/code/tests |
[ Streams/message ] (test result; capability creates the testRun stream) |
complete/code/sprint |
[ Streams/message, Streams/update ] |
summarize/code/changes |
[] |
audit/code/finding |
[ Streams/create ] |
scan/code/symbols |
[] |
notify/code/subscribers |
[ Streams/message ] |
post/code/progress |
[ Streams/message ] (generic progress-message proxy used by workflow JSON at step boundaries) |
request/code/checkpoint |
[ Streams/message, Streams/update ] (pause-for-review at cascade boundary) |
shift/code/lineNumbers |
[ Streams/message ] (posts Code/lineShift for the PHP after-handler to pick up; PHP-side does the bulk relation+attribute updates via Streams::updateRelation and setAttribute) |
scan/code/clues |
[] (read-only walk of Grokers/clue streams, filtered by clueType and minSeverity) |
scan/code/selectors |
[] (read-only walk of Grokers/selector streams; supports cssConflicts / unusedSelectors / undefinedSelectors scan types per Grokers Update 4) |
Tools use only the canonical Safebox sandbox API:
Streams.get(publisherId, streamName)— single stream fetch (cached batch)Streams.related(publisherId, streamName, type, isCategory, opts)— relation traversalStreams.fetch(publisherId, streamName)— capability-triggered materializationStreams.getMessages(publisherId, streamName, opts)— message log readStreams.ontology(pluginName)— fetch and parseconfig/ontology.json(for grounding LLM prompts)Action.propose(type, payload)— governed writeRuntime.llm(spec)— LLM call with{model, system, messages, maxTokens, temperature}Crypto.sha256(text)— for deterministic stream name slugging (alwaysawaited)
Stream attributes returned to the sandbox arrive as JSON strings — every tool inlines parseAttrs(s) to unmarshal:
function parseAttrs(s) {
if (!s) return {};
var raw = s.attributes || (s.fields && s.fields.attributes) || '{}';
try { return JSON.parse(raw); } catch (e) { return {}; }
}apply/code/rewrite posts Code/rewriteApplied on two streams: the symbol itself (audit trail) and the workspace repo (so summarize/code/changes can find the modification list with one getMessages call instead of walking every symbol). This is the v0.1 pattern for surfacing per-sprint modifications without bespoke indexing.
The workspace-repo cross-post and other progress messages also flow to the workload stream when workflow JSON wires it through. See §18 for the full progress-message vocabulary.
Four capabilities, each materializing one stream type:
Materializes a Code/workspace (or workspace Grokers/repo) by taking a ZFS snapshot of the source repo’s dataset and creating a writable clone. Returns Grokers/repoRoot (mountpoint), Grokers/zfsDataset (clone name), Code/zfsCloneName, Code/zfsSnapshot. Two trigger paths:
- (a) Substrate-fork path (when Safebox v0.38+ exposes
Streams/forkas a governed action type): theStreams/after/Streams/fork/Grokers/repohook invokes this capability. The capability code is the same; only the trigger differs. - (b) Message-driven path (Safebox v0.37 transitional, what v0.1 ships): the
Code/workspaceStartedmessage handler invokes the equivalent ZFS work viaQ_Utils::sendToNode → Code/cloneWorkspace.
Materializes a fresh Grokers/repo by cloning an upstream git repository at a specific commit into a new ZFS dataset. Used by clone/code/upstream (and therefore by the code/cloneRepo workflow). Reads Code/upstreamUrl, Code/clonedAtCommit, Code/zfsDatasetName from pending stream attributes; writes back Grokers/repoRoot, Grokers/zfsDataset, Grokers/clonedAtCommit, Grokers/upstreamUrl, Code/clonedAt.
Materializes a Code/testRun stream by running the workspace repo’s Code/testCommand in the cloned filesystem (Grokers/repoRoot). Returns pass/fail, exit code, duration, captured stdout/stderr. The test command is read from the workspace repo’s Code/testCommand attribute, inherited from the source repo at clone time:
{
"command": "npm",
"args": ["test"],
"timeout": 600000
}Materializes a Code/branch stream by running git add + commit + push against the workspace’s Grokers/repoRoot, targeting the Grokers/gitProjection.remote configured on the workspace repo (inherited from source).
All four capabilities use Protocol.System.exec for shell-out. Safebox’s standard executor build does NOT expose Protocol.System — it’s available only on Code-enabled hardened executor builds. When unavailable, the capabilities return Safebox/status='pending-host-action' with the input attributes forwarded; the PHP-side handlers (Code_workspaceStarted for ZFS, Code_sprintCompleted for git) run the equivalent operation in the privileged Node executor process via Q_Utils::sendToNode. Production deployments use the PHP-handler path.
Five handlers, registered in config/plugin.json under handlersAfterEvent. The hook keys match Qbix Streams’ event-naming convention exactly (the Streams/after/ prefix is required for save-after hooks scoped by stream type; the Streams/message/ prefix dispatches by message type).
Fires once when the plugin is installed. Wraps the install routine in scripts/Code/0.1-Code.mysql.php — registers stream types, seeds built-in tools/capabilities/workflows, and seeds the default Safebox/convention/code/* streams (PHP, JS, Python).
Fires when clone/code/repo posts Code/workspaceStarted on a new workspace repo. Reads the source dataset from the source repo’s Code/zfsDataset, RPCs to the Node executor (Code/cloneWorkspace), takes the ZFS snapshot, creates the writable clone, then writes back to the workspace stream:
Grokers/repoRoot← clone mountpointGrokers/zfsDataset← clone dataset nameCode/zfsCloneName← same as zfsDatasetCode/zfsSnapshot← snapshot nameCode/testCommand← copied from source repo (if not already set)Safebox/status←'ready'
If the source has no Code/zfsDataset, the handler marks the workspace 'ready-substrate-only' and skips ZFS work — useful for testing workflow logic without filesystem operations. ZFS failure marks Safebox/status='failed' with Code/error set; the handler does not throw, since the message-after hook runs after the message commits and substrate rollback isn’t possible at that point.
Fires when complete/code/sprint posts Code/sprintCompleted. Reads the workspace’s Grokers/gitProjection; if absent, no-op (substrate-only sprint). If present:
- Resolves the verb (
modify,rename, etc.) by parsing the workload’sSafebox/workflowattribute - Renders the branch name from
Grokers/gitProjection.branchTemplateor the plugin’sCode.branchTemplateDefault - Composes a commit message including the sprint summary (cross-posted as
Code/sprintCompleted’schangeDescriptionfield bycomplete/code/sprint) plus workload provenance - RPCs to
Code/pushBranchwhich runsgit add -A && git checkout -B <branch> && git commit -m <msg> && git push --force-with-lease - Posts
Code/sprintPushed(success) orCode/sprintPushFailed(failure) on the workspace as audit trail
Fires when shift/code/lineNumbers posts Code/lineShift on a workspace repo (typically as the step right after apply/code/rewrite in the modify workflow). The handler reads the message instructions (filePublisherId, fileStreamName, shiftStartLine, shiftDelta, modifiedSymbolPublisherId, modifiedSymbolStreamName) and calls Code::shiftLineNumbers, which uses the SHIFT MODE of Streams::updateRelations (string weight values "+N" / "-N" triggering a single SQL UPDATE per relation type):
- Walks
Streams/filefrom the file stream once (sorted by weight) to enumerate symbols-in-file. Files are indexed asStreams/filestreams; each symbol relates TO its containing file via theStreams/filerelation type withweight= symbol start line. - For symbols at line ≥
shiftStartLine(excluding the modified symbol itself), bulk-shifts theStreams/filerelation’s weight via oneStreams::updateRelationscall witharray('Streams/file' => '+N')andminWeightfilter. - For each shifted symbol, walks its outgoing verb-relations (
Grokers/calls,Grokers/reads,Grokers/writes,Grokers/invokes,Grokers/subscribes,Grokers/handles,Grokers/instantiates,Grokers/activates) once to collect target coordinates, then bulk-shifts via oneStreams::updateRelationsper family with the same SHIFT MODE syntax. - For the modified symbol itself, doesn’t shift its position-in-file (start line typically doesn’t move when its body grows or shrinks). Instead, clears all its outgoing verb-relations via
Streams::unrelateso regrok emits fresh ones from the new AST.
Posts Code/lineShifted ack with counts (shiftedSymbols, shiftedRelations, clearedRelations, errorCount). Best-effort and advisory — Grokers’ regrok of the file is authoritative. Known limitation: SHIFT MODE updates weight only, not extra.line (relation extras are JSON-encoded and not per-row rewritable in pure SQL); see §18.8 for details.
Fires after any save on a Grokers/repo stream. The handler checks whether closedTime was just set (transition from null to non-null in $params['changes']) AND the stream has a Code/zfsCloneName attribute. If both hold, it’s a workspace fork being closed — RPCs to Code/destroyWorkspace to tear down the ZFS clone. Otherwise no-op.
This is the substrate-native rollback path. A workflow that fails verification calls Streams::close() on its workspace; the substrate fires the save event with closedTime in changes; this handler tears down ZFS state.
(v0.3 had a separate Streams/Stream/close/Grokers/repo hook key, but Qbix Streams doesn’t fire that event — close is just a save with closedTime set. v0.1 ships the correct save-after pattern.)
When Safebox adds Streams/fork as a governed action type, clone/code/repo switches to using it; the substrate fires Streams/after/Streams/fork/Grokers/repo and the after-hook replaces Code_workspaceStarted. The hook’s job is the same — set up ZFS state and copy attributes — but the trigger becomes substrate-native rather than message-driven.
The verify/code/contract tool re-derives a contract from new source and compares to the existing prose-string contract attributes. Classification:
- A — preserved: equivalent contract (modulo wording).
- B — upward-compatible: preconditions relaxed OR postconditions strengthened OR side effects removed. Existing callers still satisfy.
- C — incompatible: preconditions strengthened OR postconditions weakened OR new side effects added. Some callers will break.
The LLM is instructed with a conservative bias: when uncertain between B and C, prefer C. Misclassifying B as C costs one extra ripple pass that finds all callers still satisfy; misclassifying C as B causes silent breakage.
Short-circuit: byte-equivalent source after trim → trivially A, no LLM call.
Triggered by code/modify only when verify returns C. The ripple/code/callers tool walks Grokers/calls reverse from the modified symbol, classifies each caller against the new contract, and spawns code/modify sub-workloads for callers needing semantic changes.
Per-edge extra carries {line, certainty, ctrl?, ctrlType?, arg0kind, arg0text?, arg0val?, arg1kind, arg1text?, arg1val?}. The classifier uses the arg fields heavily: arg0text="this" on a Q.Event.set call site means the handler is keyed to a tool instance and gets auto-removed on tool destroy — no manual cleanup work needed regardless of the contract change. A string-literal key without a matching .remove() in the same caller is already flagged via Grokers/pattern.suspectedBugs and gets escalated rather than auto-rippled.
Recursion is bounded by depthRemaining (default 5, configurable). Most ripples terminate within 2-3 levels.
Per-caller verdicts are LLM judgments and can be wrong. Mitigations: (a) tests in the next workflow step catch breakage; (b) the Grokers/pattern.suspectedBugs count gives the LLM prior knowledge of likely-broken sites before it judges; (c) future versions can add a Safebox/judgment gate for high-stakes symbols.
Each Grokers/repo stream may carry an optional Grokers/gitProjection attribute:
{
"remote": "git@github.com:org/repo.git",
"branchTemplate": "safebox/code-{verb}/{workloadShortId}",
"credentialRef": "Safebox/credentials/git-org-repo"
}If absent, the repo is substrate-only — no git activity. If present, successful sprints push to the remote on a templated branch.
The plugin never pulls. When upstream has moved on, the operator runs a fresh clone (via clone/code/repo against the new commit) which produces a new Grokers/repo stream coexisting with the old as historical record.
Branches are long-lived. The plugin does not delete or merge them — that’s the upstream side’s prerogative.
Per Safebox’s existing usage (verified against scripts/Safebox/0.5-Streams.mysql.php):
| Layer | Convention | Example |
|---|---|---|
| Workflows | Safebox/workflow/<namespace>/<noun> lowercase |
Safebox/workflow/code/modify |
| Tools | Safebox/tool/<verb>/<namespace>/<noun> lowercase |
Safebox/tool/find/code/callers |
| Capabilities | Safebox/capability/<TargetStreamType> mirrors target |
Safebox/capability/Code/testRun |
| Code-owned stream types | Code/<type> (capitalized first segment) |
Code/sprint, Code/finding |
| Code-owned message types | Code/<event> or Code/<event>/<sub> |
Code/sprintCompleted, Code/workspaceStarted, Code/rewriteApplied, Code/testRunPassed, Code/testRunFailed |
| Code-owned attribute keys | Code/<key> |
Code/sourceRepoHash, Code/zfsDataset |
scripts/Code/0.1-Code.mysql.php runs at install time. It:
- Verifies Safebox ≥ 0.37 and Grokers ≥ 0.1 are installed.
- Registers attribute-index templates via
Streams_Stream::registerRelationsfor eachCode/*type — see §6. - For each tool file in
files/Safebox/tools/, computes sha256 and creates aSafebox/tool/<verb>/code/<noun>stream withSafebox/codeFile,Safebox/codeFilePlugin: 'Code', declaredSafebox/inputs,Safebox/outputs,Safebox/actionTypes. SetsSafebox/approved: 'true'per the Safebox install convention for built-in tools. - For each capability file, same pattern — creates
Safebox/capability/Code/<noun>streams. - Loads each workflow JSON file from
config/workflows/, creates aSafebox/workflow/code/<noun>stream with the steps and edges asCode/stepsandCode/edgesattributes. - Seeds default
Safebox/convention/code/*streams (PHP, JavaScript, Python) withSafebox/conventionDomain: 'code',Safebox/severity: 'recommended',Safebox/approved: 'true', andappliesTo: { language }. TheSafebox/conventionstream type is owned by the Safebox plugin (substrate-level shared namespace); Code seeds only the code-domain subtree. See §18.11 for the layered context model and the cross-domain note.
The script is idempotent. Re-running updates sha256 hashes if files changed; doesn’t duplicate streams.
This section is a catalog of substrate features Code uses without modification. Anyone extending Code should look here first before adding new infrastructure.
| Substrate primitive | What Code uses it for |
|---|---|
Streams_Stream::registerRelations + syncRelations |
All attribute-based search (severity, status, etc.). No bespoke indexing. |
Streams::fork (substrate) |
Long-term workspace primitive; Code v0.1 ships transitional pattern until Streams/fork becomes a governed action type. |
Streams_Message::forkChain |
Message-history traversal across forks; relevant once Code switches to Streams::fork. |
Streams.ontology(pluginName) |
Grounding LLM prompts in Grokers’ actual schema, not hand-written prose. |
Grokers’ Grokers/calls relation with line-number weight and extra={ctrl,certainty,...} |
Caller traversal, ripple classification — line numbers and certainty fed into LLM prompts. |
Grokers’ Grokers/covers relation with orthogonalityScore in extra |
Test selection for code/modify to avoid full-suite runs. |
Grokers’ confirmed-concepts pattern (Grokers/conceptStatus = 'confirmed') |
LLM prompt grounding in propose/code/rewrite. |
| Safebox’s tool sandbox API | All tool execution. No Protocol.* in tools. |
Safebox’s Action.propose action surface |
All writes. The colon-suffixed actionTypes allowlist gates each tool’s authority. |
| Safebox’s capability dispatcher | Code/testRun and Code/branch materialization via Streams.fetch on stream type matching. |
Safebox’s Streams/category for index streams |
All four Safebox/index/Code/* streams are plain category streams. |
- Safebox v0.37
Streams/createallowlist. Safebox v0.37’sAction.phpvalidates thatStreams/createactions target a type from a known whitelist. The Code plugin creates four types not yet in that list:Code/finding,Code/testRun,Grokers/repo,Safebox/workload. Action proposals targeting these will fail until Safebox v0.38 expands the allowlist (or Safebox is patched in-place by the operator). The tools that would otherwise fail areaudit/code/finding,run/code/tests,clone/code/upstream, andripple/code/callers’s sub-workload spawn — i.e., most of v0.1. $_materializableTypesregistry. Safebox v0.37’s capability dispatcher dispatchesStreams.fetchto a capability when the requested stream type is in$_materializableTypes. v0.1 needsCode/testRun,Code/branch,Code/workspace, andGrokers/reporegistered there. The capabilities exist and self-declare theirSafebox/streamTypes; the substrate-side dispatcher needs the matching whitelist update.- Workflow JSON template syntax. The workflow JSON files use placeholder syntax like
{steps.X.Y},{input.X|default:n},{workload.shortId}, andforEach: "{steps.X.candidates}". This syntax is what the Safebox orchestrator should consume but hasn’t been verified against the orchestrator’s actual implementation. The DAG structure (steps, edges with conditions) is correct; the placeholder rendering may need adjustment.
The Code-to-Grokers feature request specifies four tranches of work that progressively widen what Code can correctly resolve. v0.1 ships with code that gracefully degrades when each tranche is absent.
- Tranche 1 — ontology promotion (
Grokers/module,Grokers/class,Grokers/methodstream types with same-named bucket relations; renamelistensTo→subscribes; new clue types). Until tranche 1 lands, Code’s tools fall back to filteringGrokers/symbolstreams byGrokers/typeattribute. Tools that would walkGrokers/methodfrom a class to find sibling methods instead use qualName-prefix matching against the class’s qualName. Functionally correct but slower and less precise. - Tranche 2 — Qbix coverage visitors (toposort type tracking; HTML/template visitor for
data-tool; PHPhandlers/directory walker;plugin.jsonconfig visitor; Qbix hint files). Partial: per Grokers update 2, the config-connector (the plugin.json visitor) has shipped, materializing handlers fromhandlersAfterEvent/handlersBeforeEvent/handlersValidateEventregistrations asGrokers/handlesandGrokers/activatesedges with phase metadata. The remaining tranche-2 items are still pending. Until they land:
-
“Find all activations of a tool” misses
data-toolHTML attributes — typically 30-50% of activation sites in a UI-heavy plugin -
“Update all callers” silently misses dynamic dispatch sites (
call_user_func,array_map, JS bracket calls) -
JS
Q.Eventsubscriptions on tool instances may not resolve to canonical hook pathsTools that would consume these emit a
notefield in their output explaining what’s missing; consumers should surface this to operators.
- Tranche 3 — JS event resolution and audit-quality aggregators (JS event property-path resolver; multi-pass cross-statement reasoning;
moduleAggregateandeventGraphaggregators). Until tranche 3 lands, JSQ.Eventsubscriptions on tool instances may not resolve to canonical extern paths; thecode/auditworkflow has nomoduleAggregateto query for “riskiest module” findings. - Tranche 4 — markdown visitor.
code/document/repoworkflow (not in v0.1 anyway) is gated on this.
Streams::updateRelationsSHIFT MODE patch. Code’sshiftLineNumbersPHP method (called by theCode/lineShiftafter-handler) uses string weight values"+N"/"-N"onStreams::updateRelationsto issue bulk SQL UPDATEs instead of per-row updates. This requires the Streams plugin to merge the patch shipped alongside Code asStreams-updateRelations-shift-patch.php. The patch is backwards-compatible (literal numeric weights still produce the original Cartesian-product insert-or-overwrite). Until the Streams plugin merges it,Code::shiftLineNumbersthrows on the firstupdateRelationscall and the workflow’s terminal step records the failure in the ack message — the workflow itself continues (since shift is advisory) but with a longer staleness window until regrok arrives.Streams/forkas a governed action type. Once Safebox adds it,clone/code/reposwitches to use it for proper fork-chain semantics. v0.1’s transitional message-driven approach works in the meantime.- Workspace cascade in
Streams::related. The Streams plugin’s 1.3.1 roadmap includes workspace-aware relation cascading. When this lands, Code’s tools that walk Grokers/calls etc. on a workspace repo automatically see edges from the source repo without re-creating them. - Grokers reanalysis as a workload.
refresh/code/comprehensionposts an advisoryCode/regrokRequestedmessage and runs the Grokers pattern aggregator (no LLM, no API cost — pure DB aggregation from the existingGrokers/callsrelation extras). Full per-symbol re-comprehension is still advisory until Grokers ships its workload-based reanalysis API. Protocol.Systemin the standard executor build. Today, capabilities fall back to PHP-side handlers viaQ_Utils::sendToNode. IfProtocol.Systemlands in the standard sandbox, the capabilities run inline with full audit visibility through the sandbox’s invocation hash.- Per-symbol governance gates. For high-stakes symbols (security paths, public APIs), the workflow should require a
Safebox/judgmentvote on the contract classification before ripple proceeds. The infrastructure exists in Safebox but Code doesn’t yet ship a finding-class judgment. Grokers::contextSiblingsfrom inside the Q.Sandbox. Per Grokers Update 4,contextSiblingsis exposed as a Node export (Grokers/classes/Grokers/harness/tools) and a PHP static (Grokers::contextSiblings). Both surfaces are outside the Safebox sandbox, which doesn’t have arbitraryrequire(). Until a sandbox-callable primitive exists (a Safebox capability returning the JSON, or a new sandbox API methodGrokers.contextSiblings), Code’spropose/code/rewritewalks the underlying relations (class siblings viaGrokers/method, pattern co-members viaGrokers/pattern,Grokers/associatedpeers) directly. This duplicates Grokers’ definition of “what counts as a relevant sibling”; will swap to the public call when the sandbox-callable surface lands.
A note for downstream consumers (Safebots team, future tooling): Code v0.1’s tools assemble their own graph context — propose/code/rewrite fetches conventions, concepts, ontology, framework patterns, and Grokers-detected codebase patterns directly. The full layered model is documented in §18.11 (Safebox/convention for human-authored prose, Grokers/pattern for machine-derived conventions, Grokers/concept for confirmed framework concepts). This is appropriate for already-resolved-coordinates actions (the user already specified which symbol to modify; the graph walk is mechanical follow-through). It is NOT appropriate for intent-resolution work like “find what events a new tool should subscribe to given this description.” That class of work — interpreting freeform user intent into specific symbol coordinates and assembling LLM context across multiple workflows — belongs to Safebots, not Code. Code provides action verbs and structured tools; Safebots calls them with already-resolved arguments. See Code-for-Safebots.md (the parallel handoff document) for the integration contract.
These are the natural integration points as the substrate evolves. Items 1–3 are required for v0.1 to actually run end-to-end on Safebox; items 4–7 are graceful-degradation cases that progressively widen as Grokers tranches land; item 8 is required for full bulk-shift performance (the workflow degrades gracefully without it); items 9–14 are quality-of-life improvements that don’t block correctness.
This section answers six questions raised by the Safebots team on how to coordinate during long-running workflows and ripple cascades. The behavior described is what Code v0.1 implements; the message vocabulary lets Safebots translate workflow execution into real-time chat UX.
The code/modify workflow accepts depthRemaining as input. The default is 3; the hard ceiling enforced in config/plugin.json is 5. Behavior at each step:
- When
code/modifyruns withdepthRemaining = Nand verify-contract returns class C, ripple-callers spawns sub-workloads, each receivingdepthRemaining = N - 1. - When a sub-workload’s
depthRemainingreaches0, the next ripple invocation does not spawn further sub-workloads. Instead it posts aCode/depthExhaustedmessage listing the unprocessed callers and returnsdepthCapReached: true. - The terminal step branches: when
depthCapReached == true, the workflow ends withCode/sprintCompletedWithFindings(the findings list includes the depth-exhausted note); otherwiseCode/sprintCompleted(full success).
There is no per-invocation symbol-count cap distinct from depth in v0.1. Depth is the only cascade limit. If a single function has 100 callers and it’s modified at depth 3, all 100 are processed at depth 3 (then their callers at depth 2, etc.). This is a deliberate v0.1 simplification — production usage will surface whether a symbol-count cap is needed.
Code workflows post the following message types on the workload stream during execution. Safebots subscribes to Streams/message on the workload (or to Code/* patterns specifically) and translates each into chat output.
| Message type | Fired by | Instructions schema |
|---|---|---|
Code/rewriteProposed |
post-rewrite-proposed step (after verify) | { symbolStreamName, verifyClass, depthRemaining, changeNotes } |
Code/rewriteApplied |
apply/code/rewrite tool | { symbolPublisherId, symbolStreamName, sourceLength, docstringLength, verifyClass, modifiedLines?, testsPassedSoFar?, depthRemaining? } (cross-posted on symbol and workspace repo) |
Code/rippleStarted |
ripple/code/callers tool | { parentSymbol, callerSymbols, callerCount, depthRemaining } |
Code/rippleEnded |
ripple/code/callers tool | { parentSymbol, modifiedCallers, skippedCallers, failedCallers, depthRemaining, spawnedSubWorkloadCount } |
Code/depthExhausted |
ripple/code/callers tool when depth hits zero | { parentSymbol, unprocessedCallers, depthRemaining: 0, suggestedFollowupAction } |
Code/checkpointRequested |
request/code/checkpoint tool, or ripple when pauseAtClassCRipple |
{ parentSymbol, plannedCallers, callerCount, depthRemaining, note, requestedTime } |
Code/testRunStarted |
post-tests-started step | { testCount, repoStreamName } |
Code/testRunCompleted |
post-tests-completed step | { passed, failureSummary, testRunStreamName } |
Code/sprintCompleted |
complete/code/sprint with outcome:"success" |
{ workloadName, changeDescription, completedAt } |
Code/sprintCompletedWithFindings |
complete/code/sprint with outcome:"partial" |
{ workloadName, changeDescription, completedAt, findings, findingCount } |
Code/sprintAborted |
complete/code/sprint with outcome:"aborted" |
{ workloadName, changeDescription, completedAt, abortReason } |
All progress messages are posted on the workload stream, whose coordinates are passed to tools as workloadPublisherId and workloadStreamName from the orchestrator’s {workload.publisherId} / {workload.streamName} template variables. Workspace-repo cross-posts of Code/rewriteApplied are additional, to support summarize-changes without walking every symbol.
The Safebox/tool/post/code/progress tool is the mechanism workflow JSONs use to fire any progress message at a step boundary. It’s a transparent proxy — workflow JSON specifies the messageType and instructions; the tool does the propose-message action. Workflows can add new progress messages by adding a step that calls post/code/progress.
The workload stream’s Safebox/status follows the substrate’s standard lifecycle: pending → running → terminal. The terminal state for Code workflows is signaled by which sprint-completion message fires:
| Terminal message | Workspace state | Recoverability |
|---|---|---|
Code/sprintCompleted |
Valid; gitPush handler triggers | Workflow done; nothing to recover |
Code/sprintCompletedWithFindings |
Valid; gitPush does NOT auto-trigger | Safebots reviews findings, optionally schedules followup workflows; current workspace can be merged manually |
Code/sprintAborted |
Possibly invalid (test failure left mid-state) | Safebots should not merge; user should inspect workspace; failed tests visible via Code/testRun stream |
Failures during ripple processing — when individual caller classifications return class: 'unknown' because a stream couldn’t be retrieved or the LLM verdict was unparseable — do NOT abort the cascade. Each failed caller is captured in failedCallers of the Code/rippleEnded message. The workflow continues; Safebots surfaces these as items needing review.
If 8 of 12 caller rewrites succeed and the 9th sub-workload aborts (its tests fail), the parent workflow’s state is determined by:
- The 8 successful rewrites are present in the workspace (already committed via apply/code/rewrite).
- The 9th sub-workload’s terminal message is
Code/sprintAbortedon its own workload, but it doesn’t propagate to abort the parent. - The parent workflow’s tests run on the workspace state (containing 8 successful rewrites). If those pass, the parent terminates with
Code/sprintCompletedWithFindingsand the failed sub-workload is referenced as a finding. - Safebots can resume by scheduling a fresh
code/modifyagainst the failed caller (with revised changeDescription) using the same workspace.
The code/modify workflow accepts an optional pauseAtClassCRipple: true input. When set, ripple/code/callers posts Code/checkpointRequested instead of spawning sub-workloads, sets Safebox/awaitsCheckpoint: true on the workload, and returns checkpointRequested: true.
The workflow’s edge from ripple-callers to notify-subscribers is gated on checkpointRequested != true. When checkpoint is requested, the workflow halts at the ripple step.
Safebots resume protocol:
- Detects the awaiting state via
Safebox/awaitsCheckpointattribute on the workload. - Reads the checkpoint details from the
Code/checkpointRequestedmessage (planned callers, depth, etc.). - Surfaces to the user via chat.
- On approval: schedules a new
code/modifyworkflow per caller (or one parent that handles them), with adjusted depth budget andpauseAtClassCRipple: falseto let the cascade run unchecked once approved. - On rejection: closes the parent workload as cancelled (sets
Safebox/statustoaborted).
For non-checkpoint flows (pauseAtClassCRipple: false or unset), the cascade proceeds without pause and Safebots surfaces a simpler “rippling N callers, depth M” status without interactive gates.
notify/code/subscribers runs synchronously within the workflow when verify-contract returns class C. The workflow’s edge from notify-subscribers to post-tests-started waits for the notify step to complete (i.e., for all Streams/message actions to be proposed).
However, the notification messages are themselves async: the proposed Streams/message actions get queued, the substrate delivers them, and downstream subscribers pick them up at their own pace. The workflow does not wait for downstream acknowledgment.
The notificationActionIds output contains the action IDs of each proposed notification. Safebots can poll their state independently via Streams_Action::fetchByActionIds if it needs to confirm downstream propagation before signaling final completion to the user.
The workload stream’s Streams/message log contains every Code/* progress message fired during execution. After a Safebots disconnect:
- Safebots fetches the workload stream and reads its
Safebox/statusattribute to determine if execution is in-progress, paused (awaiting checkpoint), or terminal. - If not terminal, Safebots fetches the workload’s message log via
Streams_Message::fetchFor()to enumerate progress messages already fired. - Safebots reconstructs which messages have been shown to the user (it tracks this in its own chat-side state) and replays the unshown ones.
- Safebots subscribes to the workload’s message stream for new messages (via the standard Streams/message subscription mechanism).
All Code/* progress message instructions are stable on replay — they reference stream coordinates and counts, not transient state. Replay is idempotent.
If Safebots was tracking a workflow that hit checkpoint while disconnected, the Safebox/awaitsCheckpoint attribute survives — the workflow is still paused, ready for Safebots to surface the checkpoint when it reconnects.
With this signaling vocabulary in place, Safebots can:
- Stream progress as a chat thread: “Rewrote
Streams_Stream::beforeSave(Class C, 12 callers identified). Processing callers… Rewrote 8 of 12. Verifying… Tests passing… Depth limit reached at depth 3; 4 callers in deeper graph saved as findings. Sprint complete with findings — review?” - Offer interactive checkpoints: “About to ripple 12 callers, depth 3. Proceed? [Approve | Reject | Review]”
- Recover from disconnect cleanly via the message log replay
- Surface partial-success states distinctly from full success and from failure
- Translate failed caller classifications into explicit findings the user can act on
When apply/code/rewrite updates a Grokers/symbol’s content and the new source has a different line count than the old, every symbol defined later in the same file now sits at a stale position-in-file, and every Grokers/calls/Grokers/reads/Grokers/writes/etc. relation originating in those later symbols carries stale weight (= line number) and extra.line.
The substrate convention used here: files are indexed as Streams/file streams (the existing Streams plugin type), and each symbol relates TO its containing file via the Streams/file relation type with weight = the symbol’s start line in the file. Walking Streams/file from a file stream with isCategory=true returns the symbols inside, ordered by line.
Code’s modify workflow inserts a shift-line-numbers step immediately after apply-rewrite. The step posts a Code/lineShift message which the PHP after-handler Code/after/Streams_message_Code_lineShift picks up and uses to invoke Code::shiftLineNumbers. The shift implementation uses the SHIFT MODE of Streams::updateRelations — string weight values of form "+N" or "-N" trigger a single SQL UPDATE per relation type that adds the signed delta to every matching row’s weight, with minWeight restricting the update to relations at or after the shift point.
Concretely:
- Walk
Streams/filefrom the file stream (one query, sorted by weight) to enumerate symbols-in-file. - Partition: the modified symbol gets handled separately (verb-relations cleared, not shifted); other symbols at line ≥
shiftStartLinego into the shift batch. - Bulk-shift
Streams/filerelation weights via oneStreams::updateRelationscall witharray('Streams/file' => '+30')and the from-symbols as IN-list filter. - For each shifted symbol, walk its outgoing verb-relations (
Grokers/calls,Grokers/reads, etc.) once to collect target stream coordinates, then callStreams::updateRelationsper verb-type to bulk-shift the matching subset. - For the modified symbol itself, clear all outgoing verb-relations via
Streams::unrelate(not shift) — relations from the old AST aren’t recoverable by mechanical shift; regrok will emit fresh ones.
After completion, posts a Code/lineShifted ack message with counts (shiftedSymbols, shiftedRelations, clearedRelations, errorCount).
This is best-effort, advisory. Authoritative line numbers come from Grokers’ regrok of the file (triggered later via refresh/code/comprehension). The bulk-shift narrows the staleness window — typically seconds — between apply-rewrite and regrok during which other workflow steps (especially ripple/code/callers, which reads relation extras for call-site context) would otherwise see stale line numbers.
Known limitation: extra.line is not updated by SHIFT MODE. The bulk SQL UPDATE operates on the weight column directly; it cannot easily rewrite the JSON-encoded extra field per-row in pure SQL. For consumers that read line number from weight (the substrate-canonical place), this is a non-issue. For consumers that read extra.line (which is redundant with weight in the normal case), values are stale until Grokers’ regrok runs and re-emits both consistently. ripple/code/callers reads extra.line for context — its values may be off-by-N briefly between shift and regrok.
Multi-symbol concurrent edits in the same file are NOT handled in v0.1: if two code/modify workflows run in parallel on different symbols in the same file, their shifts can race. v0.1 expects Safebots to serialize multi-symbol same-file modifications.
This implementation requires Streams::updateRelations to support SHIFT MODE (string weight values). The patch ships as Streams-updateRelations-shift-patch.php for the Streams plugin team to merge. Until merged, Code::shiftLineNumbers will throw on the first updateRelations call and the workflow’s terminal step will record the failure in the ack message — the workflow itself continues (since shift is advisory) but with a longer staleness window until regrok arrives.
Per Code-to-Grokers Update 1 Item 2, Grokers’ tier-2 grok pass will emit Grokers/clue streams of type possible-doc-discrepancy when a method’s docblock semantically disagrees with the LLM-derived contract (preconditions, postconditions, sideEffects, invariants).
Code consumes these via the code/auditClues workflow:
// Find all docblock discrepancies of medium+ severity
{
workflow: "Safebox/workflow/code/auditClues",
inputs: {
repoPublisherId, repoStreamName, communityId,
clueType: "possible-doc-discrepancy",
minSeverity: "medium",
findingType: "docDiscrepancy",
descriptionPrefix: "Docblock disagrees with derived contract:"
}
}Each surfaced finding can be fixed by the existing code/document workflow, passing the discrepancy’s description as docInstructions. Safebots can offer a one-click “fix all docblock discrepancies” that schedules these in batch.
The scan/code/clues tool that powers auditClues also accepts other clueType values (possible-dynamic-tool-instantiation, possible-dynamic-dispatch, etc.) for parallel use cases.
Per Grokers Update 4, the pattern aggregator runs automatically after indexing and emits Grokers/pattern streams with structural pattern types: naming-convention, naming-convention-php, naming-convention-js, indentation, css-namespace-prefix, css-var-naming, component-file-structure, semantic-pattern. Each carries Grokers/patternScope (e.g. repo or module:Streams), Grokers/patternValue (the convention as a string), Grokers/patternTemplate (JSON for structural patterns), and Grokers/patternCoverage (fraction of similar code that conforms, 0.0–1.0).
propose/code/rewrite walks the repo’s Grokers/pattern streams, filters to relevant types and scopes, and injects each as a one-line constraint into the LLM system prompt. Filtering rules:
naming-convention-phponly included when the symbol’s language is PHP;naming-convention-jsonly when JavaScript.- Module-scoped patterns only included when the symbol’s
Grokers/modulematches the pattern’s scope (or is a sub-module). - Patterns with coverage below 50% are dropped — they’re not actually conventions, just noise.
call-arg-distributionpatterns are excluded entirely — too granular for a propose prompt; relevant for verify/audit instead.
Each included pattern surfaces in the prompt as:
- **naming-convention-php** (module:Streams, 87% coverage): Class names follow Streams_Pascal style; method names are camelCase
- **indentation** (repo, 96% coverage): tabs, width 1
The LLM gets these alongside concepts and conventions, treating them as constraints on the rewrite. Coverage % gives the model a soft signal: 96% means follow it; 60% means there’s a real exception class.
Patterns are read fresh per workflow run, not cached. The aggregator runs after every regrok per Update 4, so patterns stay current.
When propose/code/rewrite builds the LLM prompt, it pulls from three different layers of codebase knowledge. Each layer has different authorship, currency, and authority. Knowing which is which matters for picking the right one when extending Code’s tools.
Layer 1: Safebox/convention — human-authored prose, prescriptive.
Stream type owned and registered by the Safebox plugin (substrate-level). Multiple plugins write to it, scoped by domain via the Safebox/conventionDomain attribute. Code seeds three baseline streams during install (Safebox/convention/code/php, Safebox/convention/code/js, Safebox/convention/code/python) carrying Safebox/conventionDomain: 'code'. Safebots will eventually seed chat-domain conventions (Safebox/convention/chat/voice, etc.) carrying Safebox/conventionDomain: 'chat'.
Each stream’s content is markdown prose authored or approved by a human. Attributes:
Safebox/conventionDomain—'code' | 'chat' | 'governance' | 'cross-domain'Safebox/approved—'true' | 'false'(governance signoff)Safebox/severity—'mandatory' | 'recommended' | 'preferred'(how strictly to enforce)appliesTo— domain-specific JSON:{ language?, framework? }for code,{ channelType?, role? }for chatkind—'convention'(allows mixing with other configurable streams in the same bucket)source— provenance:'Code-builtin','operator-authored','agent-proposed', etc.
propose/code/rewrite filters by Safebox/conventionDomain === 'code' (implicit — it walks Safebox/convention related to the repo and reads appliesTo.language to match the symbol) and only injects conventions where Safebox/approved === 'true'. The prose goes into the system prompt under ## Conventions.
Use Layer 1 for: rules that aren’t yet baked into enough code for frequency analysis to detect (“never use eval”), security/architectural rules (“controller → service → repository”), team norms with rationale, anything where human judgment about the “should” is the source of truth.
Layer 2: Grokers/pattern — machine-derived, descriptive.
Stream type owned by Grokers. Populated automatically by Grokers’ pattern-aggregator after each repo index. Each stream represents an observed pattern in the codebase, not an authored rule. Attributes carry Grokers/patternType (naming-convention, indentation, css-namespace-prefix, etc.), Grokers/patternScope ('repo' or 'module:Streams'), Grokers/patternValue (the convention as a string), Grokers/patternCoverage (fraction of similar code that conforms, 0.0–1.0), and Grokers/patternTemplate (JSON for structural patterns).
propose/code/rewrite walks repo-scoped and module-scoped patterns, filters out low-coverage ones (< 50% — noise rather than convention), and injects each as a one-line constraint under ## Detected codebase patterns. The coverage fraction is included so the LLM has a soft signal about how strict to be.
Use Layer 2 for: anything observable from frequency analysis — naming, indentation, file structure, CSS namespace prefixes, argument distributions. Patterns stay current automatically as the codebase evolves; conventions don’t.
Layer 3: Grokers/concept — confirmed framework concepts, definitions.
Stream type owned by Grokers. Populated by Grokers’ concept-discovery LLM pass. Each stream describes a confirmed framework concept (e.g., “Q.Tool — a registered DOM component with Qbix’s lifecycle integration”). Attributes carry Grokers/conceptStatus (only 'confirmed' concepts are eligible for prompt injection), Grokers/conceptDescription, examples, and so on.
propose/code/rewrite walks the repo’s confirmed concepts and injects descriptions under ## Confirmed concepts in this codebase. This gives the LLM glossary-level grounding so it knows what Q.Tool or Streams.fetch means without re-deriving it from source.
Use Layer 3 for: definitional context that should anchor the LLM’s understanding of the codebase’s vocabulary, distinct from rules about how to write code.
Decision rule when extending Code’s tools
| If you want to express… | Use this layer |
|---|---|
| “Don’t ever use eval” | Safebox/convention (prescriptive, security) |
| “This codebase uses 4-space indents” | Grokers/pattern (machine-derived) |
| “Streams_Stream is Qbix’s substrate stream object” | Grokers/concept (definitional) |
| “Class methods should follow Streams_Pascal” | Both work — convention if it’s a hard rule, pattern if it’s an observation. Prefer pattern (auto-updates as codebase evolves) unless human authority matters. |
| “All security-sensitive functions must validate inputs” | Safebox/convention (rule with rationale; not derivable from frequency) |
| “94% of methods in Streams plugin use camelCase” | Grokers/pattern (this is exactly what it’s for) |
| “When firing Q::event, prefer the third-arg phase form for new code” | Safebox/convention (recommended practice; codebase mixes both forms) |
Why three layers rather than one
The naive question is whether all three could fold into a single “code-context” stream type. The answer is no — they have different writers, different lifecycles, and different correctness criteria.
Conventions are slow-moving and human-authored — they last as long as the rule does, regardless of code churn. Patterns are fast-moving and code-derived — they update on every regrok. Concepts are mid-frequency and LLM-derived — they update when an analyzer pass confirms a new pattern as a named concept.
Mixing them would mean the layer with the slowest update cadence (conventions) would either get stale-stamped on every regrok, or would silently fall behind the others. Keeping them separate lets each layer maintain currency on its own schedule, and lets propose/code/rewrite combine them with appropriate weights at prompt time.
Cross-domain note for Safebots
When Safebots reads Safebox/convention for chat behavior (brand voice, persona, internal-process), it filters by Safebox/conventionDomain === 'chat'. Code’s Safebox/conventionDomain === 'code' streams don’t appear in that filter, and vice versa. Both share the namespace; neither sees the other’s defaults. The Safebox plugin owns the type registration and the schema; consumers (Code, Safebots, future plugins) seed and read their own subtree by domain.