Orchestrata Ultimate › Plans › spec-021-port-map
SPEC-021 Port Map — OpenCode Integration → orchestrata-ultimate
Source: plans/spec-021-port-map.md at 8a2cb42ad · last changed 2026-07-27 · first committed 2026-07-13 · 518 lines.
Prose pending.
Plan text
SPEC-021 Port Map — OpenCode Integration → orchestrata-ultimate
Read-only audit. Source:
/home/mohamed/workspace/orchestrata/open-code/(frozen). Deployed twin:~/.opencode/(running@opencode-ai/plugin@1.3.15, confirmed via~/.opencode/node_modules/@opencode-ai/plugin/package.json). Oracle:specs/021-opencode-integration/spec.md+checklists/requirements.md(295 REQ-021 rows, not 299 — see drift note) +checklists/testing.md.Classifications: PORT-CLEAN (works as-is) · RETARGET (needs concrete path/id/schema changes, named below) · BUILD-NEW (no backend exists in ultimate) · OBSOLETE/SUPERSEDED (ultimate already does this, differently or better).
0. CRITICAL — read this before planning any wave
Most of the plugin's advertised lifecycle behavior almost certainly never fires in the
deployed app, independent of any porting work. orchestrata.ts registers 8 top-level hook
keys on the object it returns:
session.status, session.created, session.idle, tool.execute.before,
tool.execute.after, message.updated, file.edited, experimental.session.compacting
The actual Hooks interface exported by @opencode-ai/plugin — checked in both the
version pinned by the source config/package.json (1.3.3, reference copy at
open-code/reference/node_modules/@opencode-ai/plugin/dist/index.d.ts:123-247) and the
version physically installed at the live deployment target, ~/.opencode/node_modules/@opencode-ai/plugin
(resolved to 1.3.15 per its package.json, type surface at
~/.opencode/node_modules/@opencode-ai/plugin/dist/index.d.ts:142-267) — contains only:
event, config, tool, auth, provider, chat.message, chat.params, chat.headers,
permission.ask, command.execute.before, tool.execute.before, shell.env,
tool.execute.after, experimental.chat.messages.transform,
experimental.chat.system.transform, experimental.session.compacting,
experimental.text.complete, tool.definition
Of the plugin's 8 registered keys, only 3 are real: tool.execute.before,
tool.execute.after, experimental.session.compacting. The other 5 —
session.status, session.created, session.idle, message.updated, file.edited — are
not part of the Hooks type in either version. Session-lifecycle and file-edit
notifications do exist in the SDK, but only as members of the discriminated Event union
consumed through the single generic event hook (@opencode-ai/sdk types.gen.d.ts:602:
EventSessionStatus | EventSessionIdle | ... | EventFileEdited | EventSessionCreated | ...,
with EventSessionIdle at line 413 type: "session.idle", etc.) — i.e. the correct pattern
is event: async ({ event }) => { switch (event.type) { case "session.idle": ... } }, not a
dedicated "session.idle" key on the returned object.
Because OrchestrataPlugin (open-code/plugins/orchestrata.ts:19) has no return-type
annotation (Plugin is never applied to it), TypeScript performs no excess-property check
and the file compiles cleanly despite exporting 5 dead keys. Whether the actual Go/Bun
OpenCode host loosely dispatches by matching hooks[event.type] for any string (in which
case these might work despite being unlisted) cannot be confirmed from static .d.ts files
alone — this must be verified by runtime instrumentation (e.g. temporarily add
console.error at the top of each of the 5 handlers and observe a live OpenCode session)
before treating any of the following as "working code to port":
- Session auto-start/resume + banner (US-002, FR-002)
- Session auto-end + git-log achievements + archive (US-002, FR-003)
- KG sync queue flush + conversation queue flush tied to session lifecycle (US-007, US-009)
- Deception detection audit (US-004, FR-007) — triggers on
session.idle - "remember when" memory search + "close the session" detection (US-008, FR-012) —
triggers on
message.updated - The entire 5-gate code review pipeline (US-003, FR-005/FR-006) — triggers on
file.edited
Only these are on solid ground: KG context injection + policy enforcement + git checkpoint
(tool.execute.before), tool-call/operation logging (tool.execute.after), the compaction
context injector (experimental.session.compacting), and all 28 custom tool entries (the
tool field IS in the real Hooks interface, and each tool definition structurally matches
ToolDefinition — tool.d.ts — even though none of them call the exported tool() helper;
TS structural typing accepts the plain object literals). Recommendation: any rebuild should
re-home the 5 dead-hook behaviors onto a single event handler with a switch(event.type),
not copy the current key names verbatim.
1. Spec-vs-artifact drift (spec claims that don't survive contact with the code)
| # | Claim | Reality | Evidence |
|---|---|---|---|
| D1 | FR-001: plugin hooks into "all 7 lifecycle events" | Code registers 8 keys (session.status is an uncounted 8th), and per §0 only 3 of the 8 are real SDK hooks | orchestrata.ts:254,287,568,673,798,856,1213,1728 |
| D2 | FR-009/US-006: "24 custom tools (9 session, 12 KG, 3 conversation)" | Actual count is 28: 9 session + 14 KG + 3 conversation + 2 code-review (code-review, code-fix) |
tool grep at orchestrata.ts:1758-3231; KG tools include kg-trace-req (2546), kg-session-audit (2626), kg-daemon-start/stop/status (2204/2311/2338), kg-cxp-thread (2762) — none counted in the spec's "12" |
| D3 | checklists/requirements.md REQ-021-207..217 enumerate the KG/conversation tools |
Checklist covers only 8 of 14 KG tools and 2 of 3 conversation tools — kg-daemon-* (3), kg-trace-req, kg-session-audit, kg-cxp-thread, conv-query, code-review, code-fix have zero REQ-021 rows |
requirements.md:373-413 vs orchestrata.ts tool list |
| D4 | FR-003 / US-002: KG sync queue + conversation queue flush happen "on session.idle" |
Both flushes actually run inline inside the session.created handler (comments even say "before closing" / "on session close" while living in the create path) — requirements.md's own checklist correctly locates them under "Session Created" (lines 70-99), contradicting spec.md FR-003 |
orchestrata.ts:406-557 (inside the "session.created" block, lines 287-562) |
| D5 | US-003 / FR-005: "5-Gate Code Review Pipeline" | The runtime's own banner and summary text call it "3-GATE" (🔍 COMPREHENSIVE CODE REVIEW (3-GATE)) and the "ALL GATES PASSED" success message only lists Gates 1-3, even though Gates 4 (complexity/nesting) and 5 (naming/design) do execute and can add warnings/errors |
orchestrata.ts:1619,1624-1627 |
| D6 | Spec Appendix ("Visual Architecture > Class Diagram") and specs/021-opencode-integration/compliance-audit.md describe SPEC-021 |
Both contain a TodoistIntegration class diagram with create_project/create_task/sync_tasks methods — content from an unrelated spec/template, never replaced. compliance-audit.md (12,560 lines / 613 KB) is a stale auto-generated report comparing against TodoistIntegration, not the OpenCode plugin at all |
spec.md:1701-1713; compliance-audit.md:1-60 |
| D7 | Metadata: "Status: Complete" | Given D1 (5/8 hooks likely dead) and the concrete bugs in §2, "Complete" overstates verified working behavior | spec.md:8 |
2. Concrete bugs found in the source artifact (carry-forward risk if ported verbatim)
These are defects in the source TypeScript/Python, independent of porting — flagged so none of them get silently reproduced.
kg-daemon-start/stop/statusprocess-matching is broken. The daemon is launched vianohup python3 -c '...'(orchestrata.ts:2224) whose argv never contains the literal string"kg-sync-daemon", yetkg-daemon-startcheckspgrep -f "kg-sync-daemon"(2215),kg-daemon-stoprunspkill -f "kg-sync-daemon"(2321), andkg-daemon-statusre-checks the same pattern (2348). None of the three can ever find the process they started — start never detects "already running" (duplicate daemons possible), stop never kills it, status always reports NOT running even while it is.conv-sync's embedded Cypher is not valid Cypher.orchestrata.ts:2898-2906containsWITH c \n OPTIONAL MATCH (os:OrchestrataSession {seq: $seq}) \n IF os IS NOT NULL \n MERGE (c)-[:FOR_SESSION]->(os)— bareIFis not Cypher grammar (noCASE/APOC). Every invocation ofconv-syncwithpendingCount > 0throws a Neo4j syntax error, caught only generically asSync failed: .... The equivalent flush insidesession.created(orchestrata.ts:519-523) omits this broken clause and is fine.code-fix's mutable-default-arg regex has unbalanced parentheses,orchestrata.ts:3207:r'def \w+\s*\(([^)]*=)\s*\[)(,|\))'— contains a stray unmatched)(and a literal\[that looks like a typo for\)).re.subwith this pattern raisesre.errorin Python at runtime; the wholecode-fixtool invocation fails every time (uncaught inside thepython3 -cheredoc, surfaces as a genericFix failed: ...in TS).- Gate 3's bare-
except:-with-passcheck references an undefined variable.orchestrata.ts:1380:for i, line in enumerate(lines):—linesis defined only in the separate Gate 4 try-block (1461), not in the Gate 3 block (1352-1454) where this loop lives. Any file containing a bareexcept:triggers aNameError, which the wrappingexcept Exception as eat1453silently swallows into a genericCONTENT CHECKwarning — meaning the bare-except detector (and everything textually after it in that same try-block: logging checks, mutable-default check,@tracestraceability check) silently stops running for any file that has the very pattern it's supposed to flag. session_cli.py end --achievements "..."/--upcoming "..."loses all but the last value.session_cli.py'sendsubcommand defines--achievementswithnargs="*"(~/.claude/tools/session_cli.py:503) — i.e. it expects--achievements "A" "B" "C". But the plugin buildsachievements.join('" --achievements "')(orchestrata.ts:629,1115,1857), producing--achievements "A" --achievements "B" --achievements "C"— repeated occurrences of the same flag in argparse overwrite, they don't accumulate, soargs.achievementsends up["C"]only. Every session close with 2+ achievements silently drops all but the last one. Same shape of bug applies to--upcoming.- Project-scope leakage via substring match. Every KG/Redis-gated tool checks
directory.includes("orchestrata")(e.g.orchestrata.ts:1986, 2033, 2055...). This substring also matches/home/mohamed/workspace/orchestrata(frozen source),orchestrata-ultimate(an earlier scaffold),orchestrata-ultimate-architecture(this repo), andorchestrata-backup-*— meaning any of these sibling checkouts opened in OpenCode would fire the same production Neo4j/Redis writes. Not new in the port, but should be tightened (e.g. exact directory match or a project-id file check) rather than carried forward. packages/path filter never matches ultimate's tree.file.edited's gate (orchestrata.ts:1217:filePath?.includes("packages/") && filePath?.endsWith(".py")) and the module-import derivation (1332-1337: splits on/packages/, buildsorchestrata.{pkg}.{module}) both hard-assume the 26-package monorepo layout. Ultimate's source lives atsrc/orchestrata_ultimate/...with nopackages/segment and a single top-level import package (orchestrata_ultimate.*, notorchestrata.<pkg>.<module>) — seeCLAUDE.mdQuick Reference. Even if the hook fired (§0), the filter would never trigger and the import-check logic would build a nonexistent module path.
3. Functional-unit inventory & classification
3.1 Lifecycle hooks (orchestrata.ts top-level keys)
| Hook | Line | Real SDK hook? | Class | Notes |
|---|---|---|---|---|
session.status |
254 | No (not in Hooks; would need event+EventSessionStatus) |
BUILD-NEW (if wanted at all) | Purely a toast/log side-effect; low value — candidate to drop rather than rebuild |
session.created |
287 | No (→ EventSessionCreated via event) |
RETARGET | Session banner logic is otherwise sound and maps onto ultimate's session_cli.py/context.json schema almost exactly (verified, §5) |
session.idle |
568 | No (→ EventSessionIdle via event) |
RETARGET | Deception detector logic (pure JS/regex, no external deps) is portable as-is once re-homed onto event |
tool.execute.before |
673 | Yes | RETARGET | KG-context-injection subprocess call retargets to an already-existing class, near-free (§6.6); policy-enforcement + git-checkpoint + bash-safety blocks are logic-portable as-is |
tool.execute.after |
798 | Yes | PORT-CLEAN | Pure tool-call bookkeeping + session_cli.py log-work/log-artifact calls, already using the shared session CLI |
message.updated |
856 | No (→ EventMessageUpdated via event) |
RETARGET | "remember when" search directly parses context.json's session_history[].{purpose,achievements,log} — schema matches ultimate's context.json (verified, §5); "close the session" sequence reuses session_cli.py end (inherits bug #5) |
file.edited |
1213 | No (→ EventFileEdited via event) |
RETARGET / consider OBSOLETE | Path filter is dead on arrival (bug #7); recommend replacing the entire embedded heredoc with scripts/code-review.py <file> --format json (accepts single-file paths, has --fix; gate-by-gate coverage compared in §6) rather than porting the heredoc |
experimental.session.compacting |
1728 | Yes | PORT-CLEAN | Trivial session_cli.py status --json read + output.context.push |
3.2 Helper functions (pure logic, no I/O — trivially portable)
| Function | Line | Class |
|---|---|---|
checkMcpHealth |
63 | RETARGET — MCP ports 3456/3457 (document-reader/writer) are OpenCode-era concepts; no equivalent MCP servers found registered in ultimate's tool registry (tools.registry, 13 volk tools — none are "document-reader/writer"). Either BUILD-NEW those MCP servers or drop this check. |
removeQuotedContent |
75 | PORT-CLEAN (pure regex) |
detectDeception |
85 | PORT-CLEAN (pure regex + closure over toolCallHistory) — contingent on tool.execute.after populating history, which is fine (§3.1) |
enforceTestingPolicy |
184 | OBSOLETE/SUPERSEDED — ultimate already enforces "no mocking" server-side via .claude/hooks/no-doubles-py.sh (PreToolUse) + forbidden-doubles-stop.sh (Stop) + code-review.py/test-review.py T-rules, independent of which editor/IDE is used. Porting a second, weaker regex-only copy for OpenCode only adds drift risk. |
enforceImportPolicy |
208 | OBSOLETE/SUPERSEDED — "no relative imports" is one of the CLAUDE.md Key Rules and is checked by the existing review tooling (code-review.py, architecture_checker.py); same reasoning as above |
enforceDevelopmentPolicy |
220 | OBSOLETE/SUPERSEDED — .claude/hooks/no-banned-deps.sh (PreToolUse on pyproject.toml writes) + .claude/banned-libraries.txt + code-review.py's BANNED-DEP gate already cover pip/uv enforcement per CLAUDE.md |
3.3 Custom tools (28 total — see D2)
| Group | Tools | Class |
|---|---|---|
| Session (9) | session-status, session-begin, session-end, session-log-work, session-log-achievement, session-add-work, session-complete-work, session-update-state, session-log-choice | PORT-CLEAN — ~/.claude/tools/session_cli.py is the exact same binary already used project-wide (verified subcommand-by-subcommand in §5); no retargeting needed beyond fixing bug #5 |
| KG query (kg-query, kg-find-file, kg-find-entity, kg-stats, kg-cxp-thread) | 5 | RETARGET — raw Cypher hardcodes old, still-basically-correct labels (Class/Function/File are the real canonical labels per §6.1 — good news), but needs (a) project-qualified id filtering per §6.2 instead of bare path/name matches, and (b) for kg-cxp-thread specifically, resolving whether the live graph uses CXPPacket (confirmed real, cxp_auditor.py:78,81,114,150-151, indexed on thread_id) or canonical Message (canonical_schema.py:134) — the writer and the canonical schema disagree, confirm which before writing Cypher |
| KG query (kg-trace-req) | 1 | RETARGET — uses yet a third, internally-inconsistent label set (CodeClass/CodeFunction/Requirement/Specification) that matches none of the real canonical labels (Class/Function/Requirement/Spec, §6.1) — needs a full rewrite, not just an id-scheme fix |
| KG query (kg-session-audit) | 1 | RETARGET (schema is real, just different names) — assumes OrchestrataSession label + IN_SESSION relationship; neither exists in ultimate (confirmed, zero grep hits). Real label is Session (canonical_schema.py:81, session/neo4j_store.py:36), keyed by session_id/id, with confirmed edges (:Session)-[:BELONGS_TO]->(:Portfolio), (:Fact)-[:ABOUT]->(:Session), (:Session)-[:NEXT_SESSION]->(:Session), (:WorkItem)-[:CARRIED_OVER_FROM/TO]->(:Session); Violation (:141) and Escalation (:143) labels are real but the exact edge linking them to Session was not traced end-to-end (see §4) — confirm before writing the retargeted query |
| KG lifecycle (kg-sync, kg-queue-status, kg-flush-queue, kg-daemon-start/stop/status) | 6 | RETARGET (smaller than expected) — FileSynchronizer.sync_file() exists at knowledge_graph/synchronizer.py:71-72, same class/method name, only the import path changes (§6.3); no standalone single-file sync script exists, so the queue-consumer code should call FileSynchronizer.sync_file() directly in Python exactly as the old plugin does now, just with the new import. kg-daemon-* additionally carries bug #1 verbatim if ported unchanged. |
| KG context (kg-file-context) | 1 | Near PORT-CLEAN — KnowledgeGraphContext.get_file_context() already exists at the identical method signature, is already traced to REQ-021-004 (session/kg_context.py:23,84,85, §6.6) — this piece of SPEC-021 is effectively already ported/wired; only the import package-prefix (orchestrata.session.kg_context → orchestrata_ultimate.session.kg_context) needs changing |
| Conversation (conv-sync, conv-status, conv-query) | 3 | BUILD-NEW, confirmed — no Conversation/ConversationTurn/HAS_TURN labels or consumer exist anywhere in src/orchestrata_ultimate/ (§6.7, grep-confirmed zero hits, including the supposedly-relevant REDIS_KG_SYNC_PREFIX constant which turns out to be dead code). This is a whole missing subsystem, not a retarget. conv-sync additionally carries bug #2 verbatim if ported unchanged. |
| Code review (code-review, code-fix) | 2 | OBSOLETE/SUPERSEDED, prefer replacing with scripts/code-review.py — but note §6.8: that script covers syntax/lint/mypy/bare-except/mutable-defaults/@traces/naming, but is missing a generic secret detector, feature-envy detection, and nesting-depth (those three exist only in verification/review/checks_structure.py or claude/hooks/error_gate/checks_structure.py, not in code-review.py itself) — full Gate 3/4/5 parity requires wiring more than just code-review.py alone. code-fix carries bug #3 verbatim if ported unchanged. |
3.4 Agents, commands, config
| Unit | Class | Notes |
|---|---|---|
agents/auto-kg-context.md, agents/kg-context-agent.md |
RETARGET (docs only) | Pure prompt/markdown guidance, no executable logic; just needs path/label examples updated to ultimate's tree + schema. Both files are byte-identical between source and deployed twin (diff confirmed) and between open-code/ and open-code/reference/ mirrors. |
commands/query-kg.md |
RETARGET (docs only) | Same — just usage examples to update once the underlying kg-query tool is retargeted |
install.sh |
PORT-CLEAN (mechanically) | Deploys agents/, commands/, config/*.json, tools/, plugins/ to fixed ~/.opencode / ~/.config/opencode/plugins paths — none of these paths are project-specific, so the script itself needs no change; only its payload (the plugin/tools) needs retargeting |
config/opencode.json |
RETARGET | Source (open-code/config/opencode.json) is a 2-line stub ({"$schema": ...}); the deployed ~/.opencode/opencode.json has since been hand-extended with model, small_model, a permission block (denying .claude/** reads), and "plugin": ["./plugins/orchestrata.ts"] — this drift between source-of-truth and deployed-reality should be reconciled (decide which is canonical) before any redeploy overwrites the hand-tuned permissions |
config/package.json |
RETARGET | Source pins @opencode-ai/plugin@1.3.3; live install resolved 1.3.15 (and an even newer 1.3.17 is requested by the deployed package.json's own dependency line) — pin should be bumped to match whatever is actually verified working, given §0's hook-surface change risk between versions |
config/bun.lock |
RETARGET | Regenerate after bumping the plugin version |
3.5 Python KG query tools (tools/)
| File | Class | Notes |
|---|---|---|
tools/kg_query (bash wrapper) |
RETARGET | cd /home/mohamed/workspace/orchestrata && uv run python3 /home/mohamed/.opencode/tools/kg_query_inner.py "$@" — hardcodes the old monorepo path as the uv run project root (line 5) while invoking the script from the deployed twin's path, not a source-relative one (line 6). Needs both paths updated: cwd → ultimate's repo root (for uv run to resolve the right venv/pyproject), script path → wherever it's redeployed. |
tools/kg_query.py |
RETARGET | Hardcodes sys.path insertions for /home/mohamed/workspace/orchestrata/packages/orchestrata-{knowledge,core}/src (lines 16-39) and imports orchestrata.knowledge.config.Neo4jConfig / orchestrata.knowledge.neo4j_client.Neo4jClient — old monorepo package structure. Ultimate's equivalent is confirmed real at orchestrata_ultimate.knowledge_graph.config.Neo4jConfig / orchestrata_ultimate.knowledge_graph.neo4j_client.Neo4jClient (§6.5) with a matching async connect()->bool/query(cypher,params)->list[dict]/close() surface — just repoint the imports and drop the sys.path hacking, or replace outright with ultimate's own scripts/neo4j_query.py (see OBSOLETE row below) |
tools/kg_query_inner.py |
RETARGET | Same old-monorepo import as above, plus its whole reason for existing — the "project scoping" rewrite (lines 94-232) — is built around a single global (p:Project)-[:HAS_FILE]->(f:File) reachability pattern with a bare Project.id match. Confirmed (§6.2): ultimate's Project/Class/Function/Module/File nodes are keyed with project-qualified ids (f"{project}:{qualified_name_or_path}", per the live knowledge_graph/sync.py writer — dated 2026-07-12, very recent), though the schema module's own code_entity_id() helper hasn't caught up and still returns bare ids. This means the entire 30+-relationship-type graph-traversal rewrite in this file is obsolete in favor of a plain {project}: prefix filter/match — much simpler than what this file does today. |
tools/neo4j_query.py |
OBSOLETE/SUPERSEDED — ultimate already has scripts/neo4j_query.py ("CLI for querying the Neo4j knowledge graph", per codebase map) which is presumably schema-current; the old standalone tool (with its Neo4jClient/Neo4jConfig mock-class fallback) duplicates that with stale imports |
|
tools/simple_kg_query.py |
OBSOLETE/SUPERSEDED — same reasoning; a "connectivity test" variant of the same stale pattern |
3.6 Redis queue infra
| Key | Type | Class |
|---|---|---|
orchestrata:kg:sync:queue |
SET | RETARGET, not BUILD-NEW — FileSynchronizer.sync_file() exists in ultimate at knowledge_graph/synchronizer.py:71-72 (§6.3), same method name/purpose; the SADD/SCARD/SPOP producer/consumer pattern is unchanged, only the import path (orchestrata.knowledge.synchronizer → orchestrata_ultimate.knowledge_graph.synchronizer) needs updating |
orchestrata:conversation:queue |
LIST | BUILD-NEW, entirely — no consumer, no Conversation/ConversationTurn graph schema exists in ultimate at all (grep-confirmed zero hits across src/orchestrata_ultimate/) |
4. What I did NOT get to / could not fully verify
- The exact relationship name linking
Violation/Escalationnodes toSessionnodes (needed to correctly retargetkg-session-audit's Cypher) — confirmed only thatSession,Violation, andEscalationlabels are each independently real (§6, §3.3), and thatgateway/audit.py:39merges(s:Session {session_id: $connection_id}); did not trace the audit/violation-writer code far enough to name the actual edge type. A builder should grepgovernance/audit.py,governance/violations.py, andgateway/audit.pyfor how they connect an event node to its owning session before writing this tool's Cypher. - Whether
CXPPacketor canonicalMessageis the label actually queried against in the live running graph (§3.3/§6.1) — both exist in code (writer vs. canonical-schema-intent) but which one has real data was not checked against a live Neo4j instance. code-review.py's three coverage gaps (generic secret detection, feature-envy, nesting depth — §6.8) mean a builder wiring thefile.editedgate ontocode-review.pyalone will under-deliver relative to the old plugin's Gate 3/4 unlesschecks_structure.pyand/orclaude/hooks/error_gate/checks_structure.pyare also wired in, or new checks added.- I did not execute or instrument a live OpenCode session to empirically confirm or
refute §0's hook-dispatch hypothesis — this is a static-analysis conclusion from two
installed
.d.tsfiles, not a runtime trace. Treat §0 as "very likely, high-confidence, but not empirically observed." - I did not review
open-code/reference/node_modules/in full (only the plugin/sdk type stubs relevant to hook dispatch) — thebun.lock/package.jsondependency graph beyond@opencode-ai/pluginandzodwas not audited. - compliance-audit.md (613 KB / 12,560 lines) was only skimmed for its ~50-line summary,
per the size-safety rule; I did not read the rest, but the summary already establishes it's
auditing the wrong spec (
TodoistIntegration) so the remaining 12,500 lines are unlikely to contain anything SPEC-021-relevant. - REQ-021 cross-references above are traced at the group level (e.g. "REQ-021-054..070"), not restated per-row; a full row-by-row REQ-021 ↔ code-line mapping table (295 rows) was not produced given the size of this task — the groupings in §3 should be sufficient to assign wave ownership without enumerating every row.
5. Verified PORT-CLEAN surface (cross-checked directly against ultimate)
session_cli.py— same absolute path (~/.claude/tools/session_cli.py) already used project-wide; not project-specific, needs no retargeting. Subcommands the plugin calls —status --json,begin --purpose,end --achievements/--upcoming,log-work --desc --details,log-artifact --path --lines --summary,log-choice --choice --reason,log-achievement --item,add-work --text --priority --notes,complete-work --text,update-state --version --branch --test-state --status,--json— all exist with matching flag names (session_cli.py:475-593), except thenargs="*"accumulation semantics bug (#5 above) which affects--achievements/--upcomingspecifically.context.jsonschema — top-level keysstate,deferred_work,session_history(confirmed via direct read of this repo's owncontext.json);session_history[]entries carryseq,when,purpose,achievements,log,upcoming— exactly the fields the "remember when" Python one-liner (orchestrata.ts:961-996) expects.statecarriesversion,git_branch,status_line,test_state— matching the session banner'sstate?.version/.git_branch/.test_statereads andcmd_status'sstate.model_dump()(session_cli.py:117-126)..claude/hooks/already independently implements the security-relevant half of US-005 (no-mock, no-banned-deps, no-xdist) as Claude Code PreToolUse/Stop hooks — this is why §3.2 marks those three policy functions OBSOLETE/SUPERSEDED rather than RETARGET.
6. Ultimate-side equivalents (KG schema, sync, review pipeline)
Confirmed by a dedicated research pass over the current src/orchestrata_ultimate/ tree
(all citations below are that pass's, cross-checked, plus a few items I confirmed
independently as noted).
6.1 Canonical KG labels / relationships
Labels are plain, undecorated names — not CodeClass/CodeFunction/CodeFile as
kg-trace-req assumes (canonical_schema.py):
Module (74), Class (75), Function (76 — methods are also Function nodes, carrying a
class_id property, no separate Method label), Spec (77), Requirement (78),
RequirementItem (79), Project (82), Package (94), File (95), Directory (96).
Canonical relationships settled by canonical_schema.py (lines 151-153) are only three:
REL_DEFINES = "DEFINES" (the one containment edge, replacing writer-specific
HAS_CLASS/HAS_FUNCTION/HAS_METHOD), REL_TESTED_BY = "TESTED_BY",
REL_TRACES_TO = "TRACES_TO" (always targets RequirementItem, never Requirement directly
— comment at lines 149-150). The module's own docstring (46-49) states these are the only
canonical names; HAS_FILE/CONTAINS/DEFINES_REQUIREMENT/HAS_ITEM/HAS_METHOD still
live in the older, uncoordinated knowledge_graph/schema.py (lines 63-83), which per that
module's own comment remains "the live writer surface until a later wave migrates." There is
no HAS_REQUIREMENT relationship in canonical_schema.py at all — it exists only as a
literal string in semantic_graph/spec_trace_enricher.py:207, an unrelated writer. This means
kg_query_inner.py's project-scoping rewrite (which anchors through HAS_FILE/HAS_SPEC/
HAS_REQUIREMENT) is targeting relationship names that are, at best, legacy/non-canonical and
at worst don't exist under those exact names for Requirement.
6.2 Node-ID convention — project-qualified ids ARE real, but two co-existing schemes
Confirmed: canonical_schema.py's docstring (15-26) documents the move from bare
<relpath>::<qualname> to project-qualified <project>:<relpath>::<qualname> (dated
2026-07-12 — i.e. very recent). However the module's own code_entity_id() helper
(canonical_schema.py:371-396, still called from knowledge_graph/ontology_mapping.py:132)
has not been updated and still returns the bare f"{relpath}::{qualname}" — while the
actual writer (knowledge_graph/sync.py) builds ids inline, already project-qualified,
bypassing that helper:
- Class/Function id:
f"{project}:{qname}"whereqnameisqualified_nameorf"{mod_path}.{name}"(sync.py:318-333,363-375) - Module id:
f"{project}:module:{path}"(sync.py:292-301) - Project node:
MERGE (p:Project {id: $id}) SET p.slug = $slug, ...withid = name = slug = project(sync.py:198-220); canonical merge key percanonical_schema.py:183/300isslug, constraintproject_slug - File id:
f"{project_id}:{path}"(knowledge_graph/kg_context.py:596-606, matchescanonical_schema.py:196's documented writer key)
Net effect for the port: any retargeted Cypher should build ids as
f"{project}:{qualified_name_or_path}" directly (matching the live sync.py writer), not
call the stale code_entity_id() helper, and not assume a global Project reachability
traversal the way kg_query_inner.py does — a direct {project}: prefix match/filter is now
the correct (and cheaper) scoping mechanism.
6.3 FileSynchronizer — EXISTS, 1:1 rename target
src/orchestrata_ultimate/knowledge_graph/synchronizer.py:38, class FileSynchronizer
(@traces(spec="008", story="US-001", reqs=["REQ-008-022","REQ-008-025"])), with
sync_file(self, file_path: str) -> SyncResult (line 71-72, same name/purpose as the old
plugin's usage) plus sync_project(...) (384) and sync_project_with_resolution(...) (434).
Import path changes only the package prefix:
orchestrata.knowledge.synchronizer → orchestrata_ultimate.knowledge_graph.synchronizer;
class and method names are unchanged. This retargets the KG-sync-queue consumer tools
(kg-sync, kg-flush-queue, kg-daemon-*) from BUILD-NEW to a much smaller RETARGET: the consumer
logic itself (SPOP loop, error handling) is unchanged, only the import line and module path.
6.4 No standalone "sync one file" script exists — use FileSynchronizer directly, not a CLI
Checked all three candidate full-repo scripts; none accepts a single source-file path:
scripts/ingest_orchestrata_ultimate.py— full-repo (or--prune-stale-labels) only, no file arg at all (main()at line 239 walksSOURCE_DIRunconditionally); targets the Memgraph semantic graph, not Neo4j — wrong target entirely for this port.scripts/sync_ast_to_neo4j.py incremental --ast --project --modules(subcommand at lines 253-262) takes a comma-separated module-path list against a pre-built AST JSON, not a raw file path — not a drop-in single-file sync.scripts/bootstrap_graphs.py— whole-graph bootstrap/check/plan only, no file/target arg (_build_parser()line 305).
Conclusion: the old plugin's python3 scripts/sync_monorepo_to_neo4j.py (full-repo,
background) and the ad-hoc from orchestrata.knowledge.synchronizer import FileSynchronizer; syncer.sync_file(f) (single-file, queue-driven) map onto two different existing ultimate
tools: full/background sync → scripts/ingest_orchestrata_ultimate.py (if repointed at the
Memgraph graph is acceptable) or scripts/bootstrap_graphs.py --neo4j; single-file
incremental sync → call FileSynchronizer.sync_file() directly from Python (as the old plugin
already does), no new CLI needed.
6.5 Neo4jClient/Neo4jConfig — EXISTS, stable import path, matching async surface
knowledge_graph/neo4j_client.py:42 — class Neo4jClient(Neo4jNodeCreationMixin, Neo4jFileQueryMixin) with async def connect(self) -> bool (64), async def close(self) -> None (94), async def query(self, cypher, parameters=None) -> list[dict] (273).
knowledge_graph/config.py:27 — @dataclass class Neo4jConfig with uri, username,
password, database, max_pool_size, max_connection_lifetime, connection_timeout
fields. Import paths: orchestrata_ultimate.knowledge_graph.neo4j_client.Neo4jClient /
orchestrata_ultimate.knowledge_graph.config.Neo4jConfig — same package-prefix rename as
§6.3, method surface (connect()->bool, query(cypher,params)->list[dict], close())
matches the old plugin's expectations closely. (A separate sync Neo4jSyncRepository also
exists for non-async call sites, e.g. session/kg_context.py, core/graph.)
6.6 KnowledgeGraphContext.get_file_context — ALREADY PORTED
This is not a retarget — it's done. src/orchestrata_ultimate/session/kg_context.py:23
carries @traces(spec="021", story="US-001", reqs=["REQ-021-004"]) on the class itself and
again on the method (line 84) — confirmed by direct grep (grep -rn 'spec="021"' src/ → exactly these 2 lines, nothing else in the whole tree). Import path is identical
save the package prefix to the old plugin's usage:
orchestrata.session.kg_context.KnowledgeGraphContext →
orchestrata_ultimate.session.kg_context.KnowledgeGraphContext. Signature:
get_file_context(self, file_path: str) -> dict, returning
{"file": ..., "lines": ..., "entities": [{"type", "name", "has_docstring"}, ...], "specs": [spec_id, ...]} (lines 85-106) — a reasonable superset of what the old plugin printed. The
class also exposes connect()/close()/connected, get_spec_context(spec_id) (108), and
get_class_context(class_name) (151) — more than the old plugin used.
This reclassifies tool.execute.before's KG-context-injection call and the kg-file-context
tool from RETARGET to near-PORT-CLEAN: swap the import path, done. (A different,
broader knowledge_graph/context_injector.py:404 ContextInjector class also exists but has
no get_file_context method — don't confuse the two.)
6.7 Redis KG-sync-queue / conversation-queue consumers — confirmed BUILD-NEW
No consumer or producer exists for either Redis key pattern anywhere in src/ or scripts/.
A constant REDIS_KG_SYNC_PREFIX = "orchestrata:kg:sync"
(knowledge_graph/constants.py:25) exists but is dead — referenced only by its own
definition and a unit test asserting its literal value
(tests/knowledge_graph/test_constants.py:38); nothing produces to or consumes from it.
HistoryManager (knowledge_graph/history.py:31) optionally accepts a redis.Redis client
but is an in-memory change-history log, unrelated. Conversation/ConversationTurn/
HAS_TURN do not exist anywhere in src/orchestrata_ultimate/ (zero grep hits) — they
appear only in the old plugin file itself and in planning docs. This confirms §3.3/§3.6's
BUILD-NEW classification for the whole conversation-tracking subsystem, with no shortcut
available.
6.8 scripts/code-review.py — real gate-by-gate coverage (supersedes most of Gates 1-5)
CLI: paths (nargs="+", files or directories) + --output/-o, --format {text,json,markdown}, --min-severity {ERROR,WARNING,INFO}, --fix, --format-code,
--max-files, --diagnostics, --stdout (code-review.py:1888-1932). Checkers wired in
main() (1950-1962): RuffFormatChecker, RuffChecker, MyPyChecker, StyleChecker,
ChecklistChecker, ConnectionStringChecker, TraceabilityChecker, plus a standalone
check_banned_dependencies (1845). Confirmed check-by-check against the old plugin's Gate
3/4/5 items:
| Old-plugin check | In code-review.py? |
Citation |
|---|---|---|
Bare except: detection |
Yes (BARE_EXCEPT, ERROR) |
code-review.py:722-735 |
| Mutable default args | Yes (MUTABLE_DEFAULT_ARGUMENT) |
code-review.py:737-751 |
| God object (old plugin: >20 methods) | Different threshold/location — >15 public methods (also >300 class lines, >500 file lines), lives in verification/review/checks_structure.py:328-388 (check_god_objects), not in code-review.py itself (also duplicated at claude/hooks/error_gate/checks_structure.py:421) |
see citation |
| Feature envy (>15 self-refs) | NOT FOUND anywhere in the repo (grepped feature_envy/FEATURE_ENVY, zero hits). Closest relative is the opposite check, METHOD_NO_SELF_ACCESS (flags methods that don't use self) at code-review.py:925-962 |
— |
| Hardcoded secrets/passwords/API keys/tokens | Partial, not generic. ConnectionStringChecker (code-review.py:1521-1720) only flags hardcoded connection URIs/IPs/localhost:port and a fixed ENV_VAR_KEYS set (NEO4J_PASSWORD, DB_PASSWORD, etc., 1528-1543) — no generic password/API-key/token literal-string regex exists anywhere in code-review.py |
code-review.py:1521-1720 |
@traces decorator presence |
Yes — TraceabilityChecker (class-level at 1358, method-level at 1383) |
code-review.py:1281-1383 |
| snake_case/PascalCase naming | Yes — StyleChecker._check_naming_conventions, CLASS_NAMING/FUNCTION_NAMING regexes |
code-review.py:573-609 |
| Nesting depth | NOT in code-review.py (grepped nesting/NESTING/max_depth, zero hits) — lives only in claude/hooks/error_gate/checks_structure.py |
— |
Conclusion: scripts/code-review.py is a strong but incomplete drop-in replacement for
the embedded 5-gate heredoc — it covers syntax/format/lint/mypy, bare-except, mutable
defaults, @traces presence, and naming outright, but is missing a generic secret-detector,
a feature-envy check, and (in this script specifically) nesting-depth — those three would
need to be added to code-review.py (or pulled from checks_structure.py /
claude/hooks/error_gate/) if full Gate 3/4 parity with the old plugin is actually wanted,
rather than assumed available.
7. Counts
| Class | Count | Units |
|---|---|---|
| PORT-CLEAN / ALREADY DONE | 6 | tool.execute.after hook; experimental.session.compacting hook; all 9 session tools; install.sh; session_cli.py/context.json integration surface; KnowledgeGraphContext.get_file_context() — already exists in ultimate, already traced to REQ-021-004 (§6.6) |
| RETARGET | ~22 | session.created, session.idle, tool.execute.before, message.updated, file.edited hooks (5, contingent on the §0 event-rewrite); 5 KG-query tools + kg-trace-req + kg-session-audit (7, schema now confirmed real — just wrong names/ids in the source); kg-sync/queue-status/flush-queue/daemon×3 (6, smaller lift than expected — FileSynchronizer exists, §6.3); 2 agents + 1 command (3, docs-only); config×3 (opencode.json/package.json/bun.lock — now 1 fewer item since kg-file-context moved to PORT-CLEAN) |
| BUILD-NEW | ~6 | conv-sync/status/query (3, confirmed no backend at all, §6.7); Redis conversation-queue consumer; possibly session.status hook if kept; MCP document-reader/writer servers (if checkMcpHealth is kept) — KG-sync-queue consumer is now RETARGET not BUILD-NEW since FileSynchronizer exists |
| OBSOLETE/SUPERSEDED | 6 | enforceTestingPolicy, enforceImportPolicy, enforceDevelopmentPolicy (already covered by .claude/hooks/* + code-review.py/test-review.py); tools/neo4j_query.py, tools/simple_kg_query.py (ultimate has scripts/neo4j_query.py); code-review/code-fix tools (prefer scripts/code-review.py, though note §6.8's 3 coverage gaps) |
(Counts are unit-groupings, not the raw 295-REQ tally — see §4 for why a full row-level count wasn't produced.)
8. Suggested W1/W2 work packages, in dependency order
W1 — foundation (must land before anything else is meaningful)
- Verify §0 empirically. Instrument a live OpenCode session (temporary
console.errorat the top ofsession.created/session.idle/message.updated/file.edited) and confirm whether they fire at all. This single result determines whether W1.2-W1.4 below are a rewrite-onto-eventjob or a "these never ran, design fresh" job. - Only one KG-schema question remains open per §4/§6: the exact relationship name linking
Violation/EscalationtoSession(needed forkg-session-audit). Everything else (canonical labels/ids,FileSynchronizer,KnowledgeGraphContext.get_file_context, which full-sync script to use) is now confirmed — see §6. - Decide canonical config: reconcile source
open-code/config/opencode.json(2-line stub) vs. the deployed, hand-extended~/.opencode/opencode.json(model/permission/plugin block) before any redeploy can safely overwrite it. - Bump
@opencode-ai/pluginpin to the version actually verified in W1.1, regeneratebun.lock.
W2 — rebuild the hook surface onto event (depends on W1.1)
- Re-home
session.created,session.idle,message.updated,file.edited,session.statuslogic into a singleevent: async ({event}) => switch(event.type){...}handler. - Fix bug #5 (
session_cli.py end --achievements) while doing so — pass a real space-separated multi-arg invocation, not the quote-splicing hack. - Retarget
file.edited's path filter frompackages/*.pytosrc/orchestrata_ultimate/**/*.py(andtests/**/*.pyif desired) and replace the entire embedded 5-gate heredoc with a call toscripts/code-review.py <file> --format json --min-severity ERROR(fixes bugs #3, #4 for free by deleting the code that had them). Per §6.8,code-review.pyalone does not fully replace Gates 3/4: no generic secret detector, no feature-envy check, no nesting-depth check. Decide explicitly whether to (a) accept the gap, (b) also wire inverification/review/checks_structure.pyfor god-object/nesting, or (c) add the missing checks tocode-review.pydirectly — don't assume "replace with code-review.py" is 100% parity by default. - Retarget the KG-context-injection subprocess call in
tool.execute.beforetoorchestrata_ultimate.session.kg_context.KnowledgeGraphContext(§6.6) — this one is nearly free, the class already exists with a matching signature and is already traced to REQ-021-004.
W3 — retarget the 28 tools (depends on W1.2)
- Session tools (9): near-zero work — already PORT-CLEAN, just fix bug #5 in
session-end. - KG query tools (kg-query, kg-find-file, kg-find-entity, kg-stats, kg-cxp-thread, kg-trace-req, kg-file-context): rewrite Cypher/labels per confirmed §6 schema.
- kg-sync/queue-status/flush-queue/daemon×3: rewrite around confirmed sync backend; fix bug #1 (pgrep/pkill pattern) while doing so, or drop the daemon tools if a background sync process isn't actually wanted going forward.
- kg-session-audit: needs a real query against
Session+Violation/Escalation edges (§6), notOrchestrataSession/IN_SESSION. - Drop
code-review/code-fixtools in favor of exposingscripts/code-review.py(with--fix) as a tool wrapper instead of reimplementing checks inline.
W4 — build the conversation-tracking subsystem from scratch (US-007, independently schedulable, no dependency on W1-W3 other than the Redis queue key convention)
- Design
Conversation/ConversationTurn/HAS_TURNadditions tocanonical_schema.py(per this project's own "ontology before graph import" rule — design the schema addition before writing any ingestion code). - Build the queue consumer (fixing bug #2's invalid Cypher while doing so).
- Wire
conv-sync/conv-status/conv-querytools against it.
Deprioritize / consider dropping rather than porting:
enforceTestingPolicy/enforceImportPolicy/enforceDevelopmentPolicy(OBSOLETE/SUPERSEDED, §3.2) — do not reimplement; if OpenCode-side enforcement is wanted at all, it should shell out to the existing.claude/hooks/*logic orcode-review.py's BANNED-DEP gate rather than maintaining a second regex set.checkMcpHealth/ MCP document-reader/writer awareness — no such MCP servers exist in ultimate's tool registry; either build them first (separate spec) or drop this check.session.statushook — lowest value of the 5 dead hooks (pure toast notification); probably not worth resurrecting even after theeventrewrite.