0019: Orion is toolkit-agnostic; nj-agents ships as the default, not a hardcoded dependency
- Status: Accepted
- Date: 2026-09-03
- Load-bearing: yes β a config shape (
toolkit.stagesas a map) enforces this by construction, not just by prose; seeinternal/config/toolkit.go. - Related: 0001 (this decision is 0001 enforced by shape rather than by prose)
- Later:
internal/njagentswas renamedinternal/toolkit. Thenjagents.goline numbers below describe the code when this was decided.
Contextβ
Every prior integration decision (0001, 0002) assumed nj-agents as the toolkit, because until now it was the only one. That assumption leaked into code as literal constants and strings rather than a configurable choice, at four verified sites:
internal/njagents/njagents.go:30βRepoURLis aconstpointing atgithub.com/navjyotnishant/nj-agents, not a value read from config.internal/njagents/njagents.go:34βRequiredSkillsis a package-levelvarnaming nj-agents' own skill names (pre-push-review,review-secrets,pr-describe,pm-plan,scaffold-project,review-tests-build).internal/njagents/njagents.go:64βRequiredDocsnames nj-agents' own shared contract file,CONVENTIONS.md.internal/supervisor/prompts.goβ stage prompt strings tell the agent to invoke specific nj-agents skills by name (/capture-intent,/scaffold-project,/pm-plan, and others), rather than reading which command a stage should run from anywhere configurable.
Each site was a reasonable shortcut when there was one toolkit to write against. Together they mean a second toolkit cannot be adopted without editing Orion's own source β the opposite of the division of labour 0001 describes, where a toolkit supplies methodology inside a stage and Orion owns everything around it. A future maintainer reading only the code at these four sites, without this record, could reasonably conclude that a single hardcoded vendor was the deliberate design β it was never evaluated and rejected, it simply hadn't been needed yet.
Decisionβ
Orion is toolkit-agnostic. A project may declare, in orion.json, a
toolkit block naming a different skill repository and what each stage
delegates to inside it. internal/config/toolkit.go is the enforcement
point:
toolkit.stagesmaps a stage to a COMMAND, and never to an order β e.g."review": "/my-org-review"β and never a list or an ordering key (order,sequence,stage_order,pipelineare rejected by name with an error that cites this ADR).- A map can only answer "what does the review stage run", which is
methodology inside a stage. A list could answer "what runs after review",
which is control flow across stages. The two look nearly identical in
JSON, which is why the shape is validated in
parseToolkitrather than merely documented β a maintainer relaxing the map to accept a list later would silently regrant a toolkit the one authority it must never hold. - Orion retains ownership of artifact paths, gates and verdicts regardless of which toolkit a stage's command belongs to. A configured command is invoked as one step inside a stage Orion already sequences; it reports a verdict back (PASS/WARN/BLOCK, an exit code) and does not decide whether the next stage runs, does not merge, and does not keep its own competing record of what happened. This is 0001 applied to a configurable toolkit rather than the one Orion happened to be written against.
Consequencesβ
- nj-agents remains the shipped default.
njagents.RepoURL,RequiredSkillsandRequiredDocsare unchanged and still govern what Orion validates and clones when a project declares nothing. Nothing about this decision migrates an existing project or edits itsorion.json. - An absent
toolkitblock is a supported, zero-change configuration.defaultToolkitfillsToolkit.Repofromnjagents.RepoURLand leavesToolkit.Stagesempty, and an empty stage command is a normal answer that falls back to Orion's own built-in prompt β never an error and never "run nothing". A project that never opensorion.jsonkeeps behaving exactly as it did before this ADR. - The four sites named above are not being rewritten by this record. They
remain correct as nj-agents' own defaults; what changes is that a second
toolkit no longer requires editing them, because
toolkit.stagesis the configurable seam that sits in front of them. - Four choices made at the same planning pass, not evaluated as
alternatives but worth recording alongside the shape decision above:
- Foreign cloning requires confirmation. Orion will clone a
project-declared
toolkit.repothat is not nj-agents, but only with the operator's confirmation first β a config file is not sufficient authorization to rungit cloneagainst an arbitrary URL unattended. - Running the toolkit's
install.shstays optional, not automatic on discovery.Clonedeliberately does not invoke it: cloning copies files, while running a freshly downloaded repository's installer executes third-party code and edits the user's runner configuration β two different risk levels that a single "toolkit found" event must not conflate. - Both stage-name spellings are accepted, and a collision between them
is rejected.
canonicalStagesmaps synonyms (design/spec,test/verify,ship/pr,implement/build) to one canonical key, but if a project'stoolkit.stagesnames the same canonical stage twice under different spellings with different commands,parseToolkiterrors rather than silently picking whichever key map iteration reached last. - The vendor directory is derived from the repository name, not fixed
at
vendor/nj-agents.VendorDirForkeys the clone path offrepoLeaf(repoURL), so a project's own toolkit lands beside β not on top of β Orion's managed nj-agents clone. The default repo still resolves to the samevendor/nj-agentspath, so no existing clone moves.
- Foreign cloning requires confirmation. Orion will clone a
project-declared
- This makes 0001
enforced by shape rather than by prose: before this decision, "a
toolkit never owns control flow across stages" was a rule a reader had to
trust an integrator to have followed. After it,
toolkit.stagescannot represent an order at all βparseToolkitrejects the list shape and the ordering-key names outright β so the rule holds even for an integrator who never read 0001.