Orchestrata Ultimate › Plans › backlog-plan
Session-Tooling Backlog — Detailed Implementation Plan
Source: plans/backlog-plan.md at 8a2cb42ad · last changed 2026-07-27 · first committed 2026-07-04 · 273 lines.
Prose pending.
Plan text
Session-Tooling Backlog — Detailed Implementation Plan
Plan for the 6 remaining backlog items (Phase 9 / VizInt is done). Written after a file-and-line investigation of the session-hook + CLI system. Several items turned out to be smaller or differently-shaped than their backlog wording — those corrections are called out per item and change the recommended priority.
0. Shared context (applies to items 1–6)
Dual copies + redeploy. The session system is version-controlled under
claude/ and deployed to ~/.claude/ by claude/install.sh:
| In-repo (edit here) | Deployed to | install.sh |
|---|---|---|
claude/tools/session/*.py (package + models.py) |
~/.claude/tools/session/ |
L35 |
claude/tools/session_cli.py |
~/.claude/tools/session_cli.py |
L39–40 |
claude/tools/kg_sync_jsonl.py |
~/.claude/tools/kg_sync_jsonl.py |
— |
claude/hooks/*.py |
~/.claude/hooks/ |
L63–92 |
claude/commands/*.md (slash commands) |
~/.claude/commands/ |
L97–99 |
Repo and deployed copies are currently byte-identical. Workflow for every item:
edit the in-repo claude/… copy → run bash claude/install.sh → confirm
diff -q repo-vs-deployed is clean.
Guardrails:
- Never hand-edit
context.json/context.md— go throughsession_cli.py(SESSION_SYSTEM.md:167–169). - Import identity:
session_cli.pymust import via theorchestratasymlink, notsession(strict-pydantic duplicate-class trap). .claude/skills/in this repo is unrelated (onlyast-toolkit); the real slash-command surface isclaude/commands/*.md.
Recommended sequencing (cheapest / highest-signal first):
| Order | Item | Effort | Why here |
|---|---|---|---|
| 1 | #4 orphaned hook delete | XS | one-file delete, zero risk |
| 2 | #2 session-number drift | S | already self-heals; mostly decide + document |
| 3 | #1 SessionStart/End warning | S | partly already works; small nuanced edit |
| 4 | #5 goal exposure | M | clear user value, well-scoped |
| 5 | #6 RTM port | M–L | most code; unblocks the cts RTM step |
| 6 | #3 incremental KG sync | M | optimization, explicitly "if histories grow large" — defer |
Item 4 — Delete the orphaned legacy hook (P3 · effort XS)
Verified state. claude/hooks/session_auto_start.py (161 LOC, git-tracked) is a
legacy SessionStart hook superseded by session_resume_context.py. It is not
copied or registered anywhere: grep session_auto_start claude/install.sh → 0 hits;
~/.claude/hooks/session_auto_start.py does not exist; not in ~/.claude/settings.json.
No Python/shell reference to it exists in the main tree.
Do not confuse with src/orchestrata_ultimate/session/hooks/session_auto_start_hook.py
(538 LOC, _hook suffix, has live tests) — that one stays.
Steps.
git rm claude/hooks/session_auto_start.py.- Fix stale prose at
AGENTS.md:92("Thesession_auto_start.pyhook begins a new session…"). - Adjust the §11 note in
claude/SESSION_SYSTEM.md:220–226(drop or mark removed). - Mark backlog item done via
session_cli complete-work --contains "orphaned legacy hook".
Verify. bash claude/install.sh still succeeds; SessionStart still works (resume banner appears). No redeploy of the deleted file needed (never deployed).
Risk. None.
Item 2 — Reconcile session-number drift (P2 · effort S)
Correction — it already self-heals. The next session number is derived by
ContextManager.next_session_num() (claude/tools/session/context.py:470–490) as
max(max_from_history, max_from_context, max_from_tags) + 1 — it already reconciles
all three sources. Called from SessionController.begin() (session.py:174–175).
Verified drift (reversed from the original S163 case). Tracker
state.active_session = 193 (context.json:3); git tags/commits/CHANGELOG max = 190;
context.history.md max = 188. Tracker is now 3 ahead, because auto-closed
and open sessions bump the tracker but emit no session-NNN git tag (only the cts
close path tags — SESSION_SYSTEM.md:104–106). Sessions 191, 192 are "auto-closed",
193 is open — none tagged.
The single owner: field ProjectState.active_session (models.py:173); deriver
ContextManager.next_session_num() (context.py:470).
Options (pick one; recommend A+C).
- A — stop the drift at the source (root-cause fix). Make the auto-close path
(
session.py:_auto_close_unclosed(), setslength="auto-closed"at L659) also emit asession-NNN-<date>git tag, matching whatctsdoes. Then tags never lag the tracker. - B — reconcile redundant derivers.
ProjectPortfolio.next_session_num(models.py:194–198) andProjectContext.next_session_num(models.py:315–319) domax(seq)+1ignoring git tags — a latent second source of a divergent number. Route all callers throughcontext.py::next_session_num()or make the model variants tag-aware. - C — accept + document. Since
next_session_num()self-heals (nextbegin→max(193,190,188)+1 = 194), no stored counter needs editing (and must not be hand-edited). Add a one-line note todocs/SESSION_MANAGEMENT.mdand close the item.
Steps (A+C). Add a _tag_session(seq) helper (extract from the cts tagger) and call
it from _auto_close_unclosed(); backfill missing tags 191/192 once (git tag session-191-… <sha>);
document in docs/SESSION_MANAGEMENT.md; complete-work.
Verify. Run a begin/auto-close cycle in a scratch dir; confirm a tag is created and
next_session_num() stays monotonic. Cross-check spec REQs specs/006-session-management/spec.md:330–331.
Risk. Low. Tag creation must be idempotent (git tag -f or existence check) to avoid dup-tag errors.
Item 1 — Warn on sessions ENDED without a handoff (P3 · effort S)
Correction — the SessionStart warning already covers this. The SessionStart hook
claude/hooks/session_resume_context.py:69 gates only on if sess and not sess.get("handoff")
— it does not inspect length, so it already fires for ended-without-handoff (verified
against context.json seq 191 = length="auto-closed", no handoff). The real gaps are:
- It inspects only one session (the
active_session, elsehist[-1]; L65–68) — an older ended-without-handoff session is missed. - The message doesn't distinguish "open/thin" from "ended without handoff."
- The SessionEnd hook
session_handoff_reminder.py:59is the one that literally excludes ended sessions:if sess.get("length"): return— so it never nags at close time.
Schema truth (not what the item guessed): SessionRecord.length: str|None
(models.py:86) is the ENDED marker (None=open, non-null=ended); handoff: HandoffData|None
(models.py:94) is presence. There is no status/ended_at on a session record.
Steps.
- In
session_resume_context.py(L63–78): scan all ofhistfors.get("length") is not None and not s.get("handoff"); branch the warning text:- open + no handoff → "session N is open with no handoff yet"
- ended + no handoff → "session N ended (auto-closed) with NO handoff recorded".
- (Decide with user) In
session_handoff_reminder.py: relax L59 so an ended session with no handoff still nags at SessionEnd (currently only open sessions do). - Edit in-repo copies →
bash claude/install.sh→diff -qrepo vs~/.claude/hooks/clean.
Verify. Craft a scratch context.json with (a) ended+no-handoff and (b) open+no-handoff
records; run each hook via stdin JSON; confirm the right message. Add a unit test under
tests/session/ if one exists for these hooks.
Risk. Low. Pure text/branch logic. Remember both copies must stay identical.
Item 5 — Expose goal-setting to the user (P2 · effort M)
Verified state. SPEC-023 goal subsystem is fully built but has no user entry point.
- Controller methods:
SessionController.set_goal(statement, criteria)/get_goal()/get_goal_status()/update_goal_status()/add_blocker()/clear_blocker()(claude/tools/session/session.py:49–143). - Schema:
Goal+GoalCriterion(claude/tools/session/models.py:113–167); stored incontext.jsonstate.goalviaupdate_state(goal=…). No separate file/Neo4j. - Gate:
claude/hooks/session_close_hook.py:584–616soft-blocksctswhen a goal is active with unmet criteria — and its message already references a/goal abandoncommand that doesn't exist yet. - Two parallel copies interoperate via the same
state.goalfield (CLI usesorchestratapackage, gate usesorchestrata_ultimatesrc) — a new CLI writer is read correctly by the gate.
Steps.
- CLI subcommands in
claude/tools/session_cli.py(3-edit pattern per existing commands):- Handlers
cmd_set_goal,cmd_get_goal,cmd_goal_status,cmd_abandon_goal,cmd_complete_criterion(model aftercmd_add_workL290 /cmd_set_handoff). - Parsers in
build_parser()(~L364):set-goal --statement … --criterion "id:desc" …(repeatable,nargs="*"likeset-handoffL431),get-goal,goal-status,abandon-goal,meet-criterion --id …. - Register in the
COMMANDSdict (L471–485). - Import
Goal, GoalCriterionfromorchestrata.models; buildGoalCriterion(id, description)from the--criterionargs; callctrl.set_goal(statement, criteria).
- Handlers
- Slash command
claude/commands/goal.md— mirrorclaude/commands/session.md(plain markdown, no frontmatter):/goal set,/goal show,/goal met <id>,/goal abandon, each mapping to apython3 ~/.claude/tools/session_cli.py …invocation. Picked up by install.sh L97–99. - (Optional) fix the dangling
/goal abandonreference insession_close_hook.py:601once real. bash claude/install.sh; confirm~/.claude/commands/goal.mdand CLI subcommands exist.
Verify. session_cli set-goal --statement "X" --criterion "c1:do Y" → get-goal shows it →
session_cli status reflects the goal → the cts gate blocks until meet-criterion c1 or
abandon-goal. Add a CLI round-trip test if tests/ covers the CLI.
Risk. Low–med. Keep the orchestrata-not-session import identity. Criterion-arg parsing
("id:description") needs a clear delimiter + validation. Redeploy both CLI + command.
Item 6 — Enable full RTM generation at session close (P2 · effort M–L)
Verified state. session_close_hook.py::generate_rtm() (L172–268) runs three
scripts/… at cts and warns "not found" because they're absent:
generate_traced_rtm_simple.py(L198),generate_rtm.py(gated onplans/code-index.jsonexisting, L216–217),generate_detailed_rtm.py(L233). The hook scrapes stdout for"trace annotations","Unique requirement IDs",Verified:,Missing:— the ported scripts already print exactly these (no hook change needed).
Source scripts live in /home/mohamed/workspace/orchestrata/scripts/:
generate_traced_rtm_simple.py(63 LOC) — importsorchestrata.core.traceability(collect_traces/generate_rtm/generate_rtm_markdown); scans dirspackages/+claude/.generate_rtm.py(1129 LOC) — stdlib-only, self-contained (own AST extractor); readsspecs/NNN-*/checklists/requirements.md+spec.mdvia a hardcodedspec_registry(IDs 000–032); scanspackages/+claude/. Does not read code-index.json.generate_detailed_rtm.py(141 LOC) — stdlib-only; reads the complete-RTM json, writes the detailed md.
Key facts that shrink the work:
- Target already exports the API the simple script needs:
orchestrata_ultimate.core.traceability→collect_traces(L215),generate_rtm(347),generate_rtm_markdown(392). Only the import path changes (orchestrata.→orchestrata_ultimate.). generate_rtm.py/generate_detailed_rtm.pyare stdlib → nopyproject.tomlchanges.plans/code-index.jsonis only an existence gate (no script reads it) — providing any file, or relaxing the gate, is enough.- Target's existing
docs/rtm.{json,md}(fromcore/traceability_utils.py::write_rtm) is a different generator writing different paths — no conflict with theplans/*RTM.
Steps.
- Copy the three scripts into
scripts/and fix:generate_traced_rtm_simple.py: importorchestrata_ultimate.core.traceability; scan dir"src"(not"packages", L17) + keep"claude".generate_rtm.py: changeRTMGenerator("packages/", "claude/")(L1049) andpackages_dir.rglob(L189) tosrc/; extendspec_registry(L480–513) with the extra target specs050–071(optional — unlisted are silently skipped).generate_detailed_rtm.py: cosmetic path-shorten (L120–122) — retargetsrc/orchestrata_ultimate/or leave (paths just won't shorten).
- code-index.json gate — choose:
- (a) generate a target
plans/code-index.json(small module→{classes,functions} index — write a ~40-line generator oversrc/, or derive from the existingplans/ast-ultimate.json); or - (b) relax
session_close_hook.py:216to not require it (the file is unused by scripts). Recommend (a) sogenerate_rtm.py's step actually runs and the coverage numbers are real.
- (a) generate a target
- Smoke-run each script from repo root (
uv run python scripts/generate_traced_rtm_simple.py), confirm it writesplans/rtm-traced-only.*, prints the scraped strings, and finds >0 traces (provespackages→srcretarget worked — else every REQ shows MISSING). - Confirm the hook's git-add list (L242–250) matches the produced files.
Verify. Run a full cts in a scratch session; confirm the RTM step reports trace/req/
verified/missing counts instead of "not found", and the plans/*.md/json outputs commit.
Risks.
packages/→src/retarget is the make-or-break — miss it and 0 traces are found.docs/rtm.*(7.4 MB) already exists from a different engine — don't confuse the two RTM families; they coexist.- These scripts scan
src/(~large) — validate the 60–180 s hook timeouts are adequate.
Item 3 — Incremental KG sync (P3 · effort M · recommend DEFER)
Correction — wrong subsystem in the item wording. The measured "0.5 s / 2826 nodes"
full rebuild is a flat JSONL export, triggered by the remember hook, not a
Neo4j sync and not SNS:
claude/tools/kg_sync_jsonl.py::sync()(L85–96) re-reads all transcript turns andos.replace-rewrites the entirekg_nodes.jsonlevery call — no cursor, no append.- Called from
claude/hooks/remember_hook.py:61on everyrememberprompt (only caller). - The Neo4j-on-SNS path
session_open_hook.py::sync_kg()(L182–203) shells toscripts/sync_monorepo_to_neo4j.py, which doesn't exist → silent no-op today.
Source of truth: Claude Code transcripts ~/.claude/projects/<cwd>/*.jsonl (append-only);
nodes are flat per-turn dicts {session_id, seq, role, text, ts, uuid} — no edges.
Incremental design (JSONL path).
- Watermark: per-transcript-file
(path → last byte offset / last line count / last uuid), stored in a sidecarkg_sync_state.jsonin the transcript dir (wherekg_nodes.jsonllives). Transcript files are append-only, so byte-offset is the cheapest cursor. - Do NOT key on
seq— it's renumbered per file each run (L74), not a global cursor.uuid(L81) is the stable dedup key if switching to append/upsert. - Change
sync()(L85–96) to append only turns past each file's watermark and update the sidecar; change_iter_turns()(L54–82) to seek/skip already-ingested bytes.
Steps. Add sidecar read/write; convert sync() from overwrite to append-past-watermark
with uuid dedupe; keep a --rebuild flag for a full pass. Redeploy the deployed copy.
Verify. Run remember twice; confirm the 2nd call reads only new transcript bytes
(instrument node count / timing) and kg_nodes.jsonl is unchanged for unchanged history.
Risk. Med — transcript files can be rewritten/compacted by the harness; on file
shrink/hash-mismatch, fall back to a full rebuild of that file. Because the current cost is
0.5 s, this is a scalability guard, not a current problem — the backlog says "if histories
grow large." Recommend deferring until a transcript set makes the rebuild visibly slow.
Cross-item checklist (do for every code change)
- [ ] Edit the in-repo
claude/…copy (never the deployed~/.claude/…directly). - [ ]
bash claude/install.sh; thendiff -rq claude/hooks ~/.claude/hooks(and tools/commands) is clean. - [ ] Preserve the
orchestrata-not-sessionimport identity in CLI edits. - [ ] Close each finished item via
session_cli complete-work(never hand-edit context.json). - [ ] Update
claude/SESSION_SYSTEM.md/docs/SESSION_MANAGEMENT.mdwhere behavior changes.