Help us improve
Share bugs, ideas, or general feedback.
From akb-sessions
Ingests a coding session JSONL into an AKB vault as structured notes, drafting session reports, TILs, tasks, ideas, and decisions.
npx claudepluginhub dnotitia/akb --plugin akb-sessionsHow this skill is triggered — by the user, by Claude, or both
Slash command
/akb-sessions:session-ingest [<jsonl_path>] --vault {name} [--lang {lang}] [--delegate {target}][<jsonl_path>] --vault {name} [--lang {lang}] [--delegate {target}]This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Multi-agent workflow that analyzes a session JSONL file and writes structured notes to an AKB vault.
Creates session notes from JSONL transcripts and git history when automatic capture failed. Useful for backfilling missing notes.
Logs session accomplishments, file changes, commits, decisions, and next steps for future recall. Triggered by phrases like 'save diary' or 'wrapping up'.
Wraps up coding sessions: appends devlog summaries with accomplishments, decisions, blockers, next steps; captures undocumented research/debugging; audits session link health; updates project index.
Share bugs, ideas, or general feedback.
Multi-agent workflow that analyzes a session JSONL file and writes structured notes to an AKB vault.
Workflow classification. LLM-wiki Ingest workflow. Cognitive-debt layer — it captures the in-flight reasoning of a coding session (how the team arrived at an understanding, the decisions and dead ends), turning ephemeral working context into persistent compiled-truth notes (TIL / decision / idea / task) before it evaporates. The four drafter subagents share this classification.
--vault {name} provided.Phase 0 — Resolve parse args, resolve JSONL path (auto or explicit), maybe delegate & exit
Phase 1 — Prepare detect duplicates, fetch open tasks
Phase 2 — Draft read JSONL, identify commits/repo, draft session note
Phase 3 — Sub-Draft launch til / task / idea / decision drafters; triage ideas
Phase 4 — Write dedup ↔ finalize ↔ write session, sub-notes, resolve tasks
Resolve invocation arguments and the JSONL path. Optionally fire-and-forget to a different target's CLI and exit early. All Phase 1+ work assumes $VAULT and $JSONL_PATH are set and the JSONL file exists.
Parse positional [jsonl_path] (optional) and flags:
--vault {name} (required) — hard-fail with --vault {name} required. if absent--lang {language} (optional) — preferred language for synthesized note body--delegate {claude|codex} (optional) — fire-and-forget to a different target's CLI and exit earlyStore as $JSONL_PATH, $VAULT, $LANG_FLAG, $DELEGATE.
The current target was baked in at build time. If --delegate matches it, abort to prevent infinite recursion.
SELF="claude"
if [ -n "$DELEGATE" ] && [ "$DELEGATE" = "$SELF" ]; then
echo "Refusing $SELF -> $SELF delegate (would recurse)." >&2
exit 2
fi
If $JSONL_PATH was supplied positionally, use it. Otherwise discover the current session's JSONL automatically — discovery is target-specific.
Claude target — ${CLAUDE_SESSION_ID} template substitution + $PWD encoding.
The Claude Code skill runtime substitutes ${CLAUDE_SESSION_ID} with the current session's UUID at render time (see the substitution table at code.claude.com/docs/en/skills.md) — it is NOT a shell environment variable. Do not gate the path on an env-var check; the substitution has already happened by the time bash runs.
if [ -z "$JSONL_PATH" ]; then
ENCODED=$(printf '%s' "$PWD" | tr '/.' '--') # both / and . map to -
JSONL_PATH="$HOME/.claude/projects/${ENCODED}/${CLAUDE_SESSION_ID}.jsonl"
fi
If the substitution did not happen (e.g., this skill was somehow invoked outside a Claude session), the resulting $JSONL_PATH will contain the literal text ${CLAUDE_SESSION_ID} and the file-exists check below will catch it.
Verify the resolved path exists before proceeding:
if [ ! -f "$JSONL_PATH" ]; then
echo "JSONL path not readable: $JSONL_PATH" >&2
exit 2
fi
If $DELEGATE is set, hand the ingest work to a different target and exit. The current session does no ingest work — the receiver runs Phase 1+ on the same JSONL. claude / codex spawn the target's CLI in the background.
Each target runs the receiver unattended while limiting blast radius if the JSONL contains injected content:
claude: --permission-mode acceptEdits auto-approves file edits; Bash/MCP rely on the skill's allowed-tools.codex: -s workspace-write -c approval_policy="never" — confines file writes to the workspace AND skips approval pauses. --skip-git-repo-check lets the receiver start in a non-git pwd (delegate does not require the JSONL's project to be checked out). Avoid --dangerously-bypass-approvals-and-sandbox: that drops the sandbox entirely, letting prompt-injection in the JSONL escalate to arbitrary shell.LOG="${TMPDIR:-/tmp}/session-ingest-delegate-$(date +%s)-$$.log"
ARGS="\"$JSONL_PATH\" --vault \"$VAULT\""
[ -n "$LANG_FLAG" ] && ARGS="$ARGS --lang \"$LANG_FLAG\""
case "$DELEGATE" in
claude)
nohup claude -p "/session-ingest $ARGS" --permission-mode acceptEdits \
>"$LOG" 2>&1 &
echo "Delegated to claude (pid $!, log $LOG)."
;;
codex)
# $session-ingest must NOT shell-expand. Single-quote the literal,
# then concatenate the double-quoted $ARGS so the file path stays quoted.
# --skip-git-repo-check: the receiver may run in a non-git pwd (delegate
# does not require the JSONL's project to be checked out); codex exec
# otherwise aborts before starting.
PROMPT='$session-ingest '"$ARGS"
nohup codex exec --skip-git-repo-check -s workspace-write -c 'approval_policy="never"' "$PROMPT" \
>"$LOG" 2>&1 &
echo "Delegated to codex (pid $!, log $LOG)."
;;
"")
: ;;
*)
echo "Unknown --delegate target: $DELEGATE (expected: claude, codex)" >&2
exit 2
;;
esac
if [ -n "$DELEGATE" ]; then
exit 0
fi
Before reading the full JSONL, check whether this session was already ingested.
Read the first ~10 lines of the JSONL (Read with limit: 10). The opening line may be a noise event without identifying fields (CC sessions sometimes start with permission-mode or file-history-snapshot, neither of which carries sessionId); scan all 10 to find the first line that decisively identifies the schema:
sessionId field (e.g., {"type": "worktree-state", "sessionId": "..."} or {"type": "user", "sessionId": "..."}), the JSONL is CC schema (flat per-line events). Set jsonl_schema = "cc".payload object containing id (e.g., {"timestamp": "...", "type": "session_meta", "payload": {"id": "...", "cwd": "..."}}), the JSONL is Codex schema (event-wrapped). Set jsonl_schema = "codex"."Unrecognized JSONL schema: $JSONL_PATH" — do not guess.Schema detection is independent of runtime.target: a Codex JSONL ingested from a Claude harness still uses Codex parsing, and vice versa.
Reuse the first-10-lines window from schema detection and read the last few lines of the JSONL. Capture everything the head window carries in one pass — later phases reuse these values rather than re-scanning. Field lookup is schema-aware:
For jsonl_schema == "cc":
sessionId field as session_id. (Lines without sessionId such as file-history-snapshot are skipped — they precede the first content event in some sessions.)worktree-state entry is present, store worktreeSession.originalCwd as session_cwd; otherwise leave session_cwd unset.For jsonl_schema == "codex":
type == "session_meta" (typically the first line). Store payload.id as session_id and payload.cwd as session_cwd.For both schemas:
timestamp → jsonl_last_timestamp.Store:
session_id — the resolved session/thread idjsonl_last_timestamp — the ISO 8601 timestamp of the final entrysession_cwd — the source session's working directory (or unset)mcp__akb__akb_search(
query="{session_id}",
vault="{vault_name}",
collection="sessions",
tags=["session:{session_id}"],
type="session",
limit=1
)
No match → sync_mode = "new", proceed to Phase 2.
Match found → read the existing session note:
mcp__akb__akb_get(vault="{vault_name}", doc_id="{matched_doc_id}")
→ existing_session_doc_id, existing_tags, existing_session_content
Extract the last-synced:{timestamp} tag value from the existing note's tags. Store as last_synced_timestamp. Preserve existing_session_content (the full markdown body) for Phase 2-3 and Phase 4-1.
jsonl_last_timestamp <= last_synced_timestamp → no new content since last sync → stop:
"Session {session_id} already synced up to {last_synced_timestamp}. No new content. Skipping."jsonl_last_timestamp > last_synced_timestamp → proceed to the new-content assessment below.Read the JSONL entries with timestamps after last_synced_timestamp. Filter out noise. The filter list is schema-aware:
For jsonl_schema == "cc":
type: "system" messagestype: "permission-mode" entriestype: "file-history-snapshot" entries/exit, /quit, or session-end signalsisMeta: true entriesFor jsonl_schema == "codex":
type: "turn_context" entries (per-turn configuration)type: "event_msg" with payload.type in {task_started, token_count} (system markers and telemetry)type: "response_item" with payload.type == "reasoning" (model-internal thinking, analogous to CC thinking blocks)type: "event_msg" with payload.type == "user_message") containing only /exit, /quit, or session-end signalstype: "session_meta" (metadata-only, already consumed for session_id extraction)After filtering, judge whether the remaining entries represent meaningful work — substantive user messages, assistant responses with real content, tool calls that changed files, or decisions made.
Not meaningful (e.g., resume then immediately exit, only system noise) → stop:
"Session {session_id} has entries after {last_synced_timestamp} but no meaningful new work. Skipping."
Meaningful → set sync_mode = "update", store existing_session_doc_id and last_synced_timestamp, proceed to Phase 1-2.
Before reading the full JSONL, retrieve project-scoped open tasks from the vault so the session drafter can identify which tasks this session resolved.
Parallel execution with Phase 1-1: the project-name parse step below runs immediately (no MCP), and the akb_search for open tasks is independent of Phase 1-1's duplicate-detection search. Issue Phase 1-1's existing-session search and Phase 1-2's open-task search in a single parallel tool-use block. Act on Phase 1-2 results only after Phase 1-1 greenlights continuation (otherwise discard).
Project name is the basename of the source session's working directory. Use session_cwd (captured in Phase 1-1) when set — it is authoritative metadata from the JSONL, and cross-target delegate (e.g. Claude → Codex) puts the receiver in a different pwd than where the session originated, so pwd is not a reliable fallback.
session_cwd set → project_name is its basename. (For CC worktree sessions this is originalCwd, the unworktreed project root, not the worktree name.)session_cwd unset (CC session with no worktree-state entry) → use the current working directory's basename. The CC JSONL parent directory uses a lossy encoding (/ and . both map to -), so decoding it is unreliable and not used.If derivation fails entirely, set project_name to the empty string. The downstream open-tasks query becomes a no-op (no project: tag match) — degraded but safe.
mcp__akb__akb_search(
query="open task",
vault="{vault_name}",
collection="sessions/tasks",
type="task",
tags=["project:{project_name}"],
limit=20
)
From the results, exclude any document whose tags contain resolved. Store the remaining results as open_tasks — a list of {doc_id, title, summary} for each match.
If no results or the query fails, set open_tasks to an empty list and continue.
The main agent acts as the session drafter. Read the JSONL file, extract session information, and produce both a session draft (navigator skeleton) and a session context for sub-agents. The session draft is finalized later in Phase 4-0 after sub-drafter results are reconciled.
Read the JSONL file. It contains the complete conversation history: user messages, assistant responses, tool calls, and results. The field-name layout differs by jsonl_schema (set in Phase 1-1) — apply the analogous mapping when extracting content:
| Concept | CC schema | Codex schema |
|---|---|---|
| User message | type: "user" entry, message.content[] with type: "text" | type: "event_msg", payload.type: "user_message" |
| Assistant text response | type: "assistant" entry, message.content[] with type: "text" | type: "event_msg", payload.type: "agent_message" (and/or response_item with payload.type: "message") |
| Tool call | type: "assistant" entry, message.content[] with type: "tool_use" (nested inside the assistant turn — NOT a top-level event type) | type: "response_item", payload.type: "function_call" |
| Tool result | type: "user" entry, message.content[] with type: "tool_result" (nested inside a user-role turn that wraps the tool reply) | type: "response_item", payload.type: "function_call_output" |
Update mode (sync_mode = "update"): Focus JSONL analysis on entries with timestamps after last_synced_timestamp — this is where the new information lives. The session draft you produce must be a combined compiled truth: read existing_session_content (retrieved in Phase 1-1) and integrate its Summary / Narrative / Artifacts / Knowledge Produced / Open Questions with the new work from the resumed portion. The written draft is the new canonical state of the session, not a delta.
New mode (sync_mode = "new"): Read the full JSONL as normal.
Focus on extracting:
Skip tool call/result noise — focus on user messages and assistant text responses. Reusable lessons and long-lived decisions will be extracted by the Phase 3 drafters; the session draft itself does not need to generalize.
Scan the JSONL for git commit activity. Extract commit hashes to determine the range of changes made during the session.
If commits were found, run:
git log --oneline {earliest_commit_hash}^..{latest_commit_hash}
git diff --stat {earliest_commit_hash}^..{latest_commit_hash}
If no commits were found in the JSONL, skip these commands.
Also collect the repository URL:
git remote get-url origin
Store the result as repo_url. If the command fails (no remote configured), set repo_url to null.
Compose the session draft per references/session-template.md — that file carries the full draft template, section-omission rules, update-mode handling, and authoring guidelines. The session note is a navigator + narrative + timeline; deep knowledge (reusable lessons, long-lived decisions, follow-up tasks, creative ideas) belongs to the Phase 3 sub-notes and must not be restated here.
Determine the session's primary theme (debugging vs. feature build vs. refactor) and let it guide the emphasis in ## Narrative. Number ## Open Questions bullets Q1, Q2, … so Phase 3 sub-drafters can target them via resolves_question: Q{n}.
Compare the work performed in the session (from Phase 2-1) against the open_tasks list (from Phase 1-2).
A task is "resolved" if the session performed substantial work directly addressing the task's topic. Partial completion counts — any remaining work will be captured as a new task by the task-drafter.
For each resolved task, record:
doc_id — from the open_tasks entrytitle — the task's titlepath — the task's path (for Knowledge Produced link)reason — brief explanation of how it was addressed (e.g., "Implemented the retry logic described in the task")Store as resolved_tasks list. If no tasks were resolved, set to an empty list.
Build a lightweight session context as a roadmap for the Phase 3 drafter agents. This supplements the session draft and JSONL — it helps agents orient quickly.
Session Context:
- Project: {project name}
- Repository: {repo_url from Phase 2-2, or "N/A"}
- Date: {YYYY-MM-DD from JSONL or today}
- Session JSONL: {$JSONL_PATH resolved in Phase 0}
- Work performed: {brief list of main topics/tasks}
- Committed changes: {from Phase 2-2 — commit messages and files changed, or "None"}
- Problems encountered (for possible troubleshooting TILs): {brief list}
- Long-lived decisions considered (for possible Decision notes): {brief list}
- Rejected alternatives (anti-pattern memory for Decisions): {brief list}
- Tools/technologies used: {notable APIs, libraries, frameworks}
- Vault name: {resolved vault_name from --vault}
- Collections: {plugin's default_collections mapping — includes decisions}
- Ingest mode: {new|update}
- Last ingested timestamp: {ISO timestamp from existing session note, or "N/A" if new}
- JSONL analysis start: {last_synced_timestamp if update, or "beginning" if new}
- Open tasks: [{doc_id}] {title} (for each task in open_tasks, or "None")
- Resolved tasks: [{doc_id}] {title} — {reason} (for each task in resolved_tasks, or "None")
- Open Questions (session draft): Q1: {text}, Q2: {text}, ... (each question available for sub-drafters to resolve via `resolves_question: Q{n}`)
Keep this concise — it is a roadmap, not a comprehensive record.
Launch 4 agents simultaneously in a single message. Each agent receives the session context, a session headline (frontmatter + > **Summary:** line + numbered ## Open Questions list only — not the full Narrative), and the JSONL path. The JSONL is the primary source; drafters do not need Narrative prose.
Agent(
subagent_type="akb-sessions:til-drafter",
description="TIL learning notes",
prompt="[Session Context]\n\n## Session Headline\n{frontmatter + Summary + Open Questions}\n\nExtract learnings from the JSONL, validate against vault, and compose TIL notes."
)
Agent(
subagent_type="akb-sessions:task-drafter",
description="Follow-up task notes",
prompt="[Session Context]\n\n## Session Headline\n{frontmatter + Summary + Open Questions}\n\nIdentify follow-up tasks from the JSONL and validate against vault."
)
Agent(
subagent_type="akb-sessions:idea-drafter",
description="Creative idea notes",
prompt="[Session Context]\n\n## Session Headline\n{frontmatter + Summary + Open Questions}\n\nGenerate creative ideas inspired by the JSONL and validate against vault."
)
Agent(
subagent_type="akb-sessions:decision-drafter",
description="Long-lived decision notes",
prompt="[Session Context]\n\n## Session Headline\n{frontmatter + Summary + Open Questions}\n\nExtract long-lived architectural/product decisions (ADR) from the JSONL, validate against vault, and compose Decision notes."
)
| Agent | Model | Purpose | Output |
|---|---|---|---|
| til-drafter | sonnet | Learnings + vault validation | 0–N learning notes with dispositions + til_kind tag |
| task-drafter | sonnet | Follow-up tasks + vault validation | 0–N task notes with dispositions |
| idea-drafter | opus | Creative ideas + vault validation | 0–N idea notes with dispositions |
| decision-drafter | sonnet | Long-lived decisions (ADR) + vault validation | 0–N decision notes with dispositions |
Each agent receives:
> **Summary:** line + numbered ## Open Questions. The full Narrative is not sent — drafters read the JSONL directly for domain-specific detail.vault_name and collections for duplicate checkingEach agent drafts notes, then validates them against the vault using akb_search. Drafts are returned with a disposition (create / append; supersede additionally for decisions), optional resolves_question: Q{n} when a draft addresses a specific session Open Question, and related_to doc IDs. Duplicates are reported as skipped and excluded from the output.
idea-drafter specific. Idea drafts additionally carry impact / effort / confidence enum assessments. The drafter silent-drops any idea it rates impact: low before returning (no report, no skipped block). The main agent re-triages the surviving drafts in Phase 3-2.
Agents that find nothing to write return "No {type} notes for this session."
After the idea-drafter agent returns, the main agent filters the surviving idea drafts using their impact / effort / confidence frontmatter fields. The drafter already silent-dropped any idea it rated impact: low before returning; this phase is a second gate that catches cases the drafter missed and removes high-cost, low-conviction ideas.
Scope. Applies only to idea drafts. TIL / task / decision drafts pass through unchanged.
Inputs. Each idea draft from Phase 3-1 with disposition ∈ {create, append}.
Automatic drop rules — apply in order, drop on first match:
impact == low — drop. Safety net for the drafter's self-filter.effort == large AND confidence == low — drop. High-cost, low-conviction ideas clutter the vault.All remaining idea drafts pass through to Phase 4.
No user prompt. No skipped block written. No vault write. Dropped drafts are discarded in-memory; they do not feed Phase 4-0 or Phase 4-2. If a dropped idea set resolves_question: Q{n}, discard that mapping as well — the Q ID stays in the session's ## Open Questions.
Save all drafts with create, append, or (decisions only) supersede disposition directly — no user approval needed. Each drafter sets status: active in its ---DRAFT--- block; Phase 4-1 / 4-2 pass it through verbatim.
In-memory reconciliation between the session draft and the sub-note drafts before any AKB write. Single in-context pass — no extra LLM call, no MCP call, no per-pair loop. Inputs: session draft body, all sub-note drafts with disposition ∈ {create, append, supersede}, resolved_tasks list.
Step A — Promote Open Questions. For each sub-note draft with resolves_question: normalize the value (strip / upper / ^Q\d+$; non-conforming → log warning + no-match), find the matching Q{n} bullet in the session's ## Open Questions, and remove that bullet entirely — the answer surfaces in ## Knowledge Produced (Step C). Multiple sub-notes can target the same Q{n}; the bullet is removed once. If no session question matches, log a warning and leave the sub-note unchanged.
Step B — Cross-Type Content Overlap Detection. For each sub-note, extract its headline sentence — TIL: > **Key Insight:** …; Task: > **Next Action:** …; Idea: > **Inspiration:** … plus Proposal first sentence; Decision: > **Context:** … plus Decision first sentence. Scan the session Narrative and Artifacts prose for sentences that match a headline. A sentence matches when at least two of these hold: (1) core noun phrases overlap, (2) assertion direction is the same (not negated / qualified differently), (3) reasoning or evidence cited is the same. One criterion alone is not enough — preserve the Narrative sentence if in doubt. On a match, remove the redundant sentence or replace it with a brief pointer ("…the lesson captured in Knowledge Produced"); do not rewrite surrounding prose beyond what grammatical flow needs. Overlap resolution always favors sub-notes — never edit a sub-note body here.
Step C — Finalize Knowledge Produced. Build the block from deduplicated sub-notes plus resolved_tasks, in the order TIL → Idea → Task → Decision → Resolved:
## Knowledge Produced
- 🧠 TIL: [{title}](sessions%2Flearnings%2F{doc_path}.md) — {one-line hook from Key Insight}
- 💡 Idea: [{title}](sessions%2Fideas%2F{doc_path}.md) — {one-line hook from Inspiration}
- ✅ Task: [{title}](sessions%2Ftasks%2F{doc_path}.md) — {one-line hook from Next Action}
- 📐 Decision: [{title}](sessions%2Fdecisions%2F{doc_path}.md) — {one-line hook from Decision first sentence}
- ♻️ Resolved: [{title}](sessions%2Ftasks%2F{path}.md) — {reason from resolved_tasks}
For newly-drafted sub-notes the link's doc_path is substituted at render time from each Phase 4-2 put response. For resolved tasks the link uses the existing task's path captured in Phase 2-4. See references/note-templates.md → "Cross-Note Links" for why this hybrid (body link + depends_on frontmatter) is used.
Step D — Final omissions. Omit ## Knowledge Produced entirely when no sub-notes were produced and no tasks resolved. Omit ## Open Questions if Step A left zero unresolved bullets. The resulting body is what Phase 4-1 writes — do not mutate further.
The session note is written first because sub-notes reference it via depends_on. Sub-notes always use the full URI akb://{vault_name}/doc/{session_doc_path} (always-URI cross-ref convention; future-proof against vault tier splits).
New mode (sync_mode = "new"). Extract metadata from the draft's ---DRAFT--- block and akb_put(vault="{vault_name}", ...) with type="session", collection={draft.collection}, status={draft.status}, tags=["session", "claude-code", "session", "session:{session_id}", "project:{project_name}", "last-synced:{jsonl_last_timestamp}", ...topic_tags], and the finalized body from Phase 4-0. Capture session_doc_id and session_doc_path.
Update mode (sync_mode = "update"). The session note already exists. Phase 2-3 + 4-0 produced a full compiled-truth replacement plus exactly one new timeline entry. Apply the timeline merge procedure — Mode A (full-body merge) (see appendix) against existing_session_content. Build the updated tag list (replace the old last-synced:{old_timestamp} with last-synced:{jsonl_last_timestamp}; preserve all other tags; add any new topic tags). Then akb_update(vault="{vault_name}", doc_id=existing_session_doc_id, content={merged}, tags={updated}, summary={draft.summary}, message="Update from resumed session: {brief description}"). Assemble session_uri = akb://{vault_name}/doc/{existing_session_doc_path} for sub-notes' depends_on.
Issue all sub-note akb_put calls in a single parallel tool-use block. Sub-notes are independent of each other; only the session note's order matters (via depends_on).
For each sub-note, extract metadata from the draft and call akb_put(vault="{vault_name}", ...) with collection={draft.collection}, type={draft.type}, status={draft.status}, tags=["session", "claude-code", ...draft_tags], depends_on=["akb://{vault_name}/doc/{session_doc_path}"], related_to=[...draft.related_to] (full URIs as emitted by drafters). Collection per type: TIL → sessions/learnings, Task → sessions/tasks, Idea → sessions/ideas, Decision → sessions/decisions.
Idea notes — append four namespaced tags to draft_tags before put: category:{draft.category}, impact:{draft.impact}, effort:{draft.effort}, confidence:{draft.confidence}. Drop duplicates (the drafter may have already emitted some as tags). These four tags are the durable carriers for the external maintenance layer's consolidation synthesis and staleness checks.
Task notes — after the task document is created, also create a lightweight todo via akb_todo(vault="{vault_name}", title=draft.title, priority={P0→urgent, P1→high, P2→normal, P3→low}, ref_doc=task_doc_id, note=draft.summary).
append disposition — the drafter has already merged compiled truth + one new timeline entry. Read the existing document with akb_get(vault="{vault_name}", doc_id=draft.append_target), apply timeline merge Mode A against draft.body, and akb_update(vault="{vault_name}", doc_id=draft.append_target, content={merged}, tags=[...existing, ...new from draft], summary=draft.summary, message="Appended from session ingest: {brief description}").
supersede disposition (decisions only) — write the new decision via akb_put(vault="{vault_name}", ..., tags=[..., "supersedes:{old_doc_id}"]) (tags use bare doc_id; the : already binds key:value). Capture new_doc_id and new_doc_path; compose new_doc_ref = akb://{vault_name}/doc/{new_doc_path}. Then read the old decision via akb_get(vault="{vault_name}", doc_id=draft.supersedes), apply timeline merge Mode B with new_entry = "- **{today}** | Superseded by {new_title} ({new_doc_ref}). {reason}. [Source: session:{session_id}, {today}]", and akb_update(vault="{vault_name}", doc_id=draft.supersedes, content={merged}, tags=[...old, "superseded-by:{new_doc_id}"], status="superseded", message="Superseded by {new_doc_ref} in session ingest"). Setting status="superseded" removes the old decision from the external maintenance layer's active-document pools.
If resolved_tasks is non-empty, fetch open todos once: akb_todos(vault="{vault_name}", status="open") → open_todos. For each resolved task:
open_todos in memory for a todo whose ref_doc matches the task's doc_id. If found, akb_todo_update(vault="{vault_name}", todo_id={matched}, status="done", note="Resolved in session {session_doc_id}").akb_get(vault="{vault_name}", doc_id={task_doc_id}), then compose the resolution timeline entry - **{today YYYY-MM-DD}** | Task resolved in session:{session_id}. {reason}. [Source: session:{session_id}, {today YYYY-MM-DD}] and apply timeline merge Mode B against task_content (compiled-truth zone preserved as-is).akb_update(vault="{vault_name}", doc_id={task_doc_id}, content={merged}, tags=[...existing, "resolved"], status="archived", message="Resolved in session {session_doc_id}").If no matching todo is found, still archive the task document. If the up-front akb_todos fails, skip all todo updates this ingest but archive the task documents anyway.
If a drafter agent (Phase 3) fails or returns an error:
If an akb_put fails partway through Phase 4:
"session:{session_id}" tagIf Phase 4-0 Step A finds a resolves_question that does not match any session Open Question ID:
references/session-template.md — session draft template, section-omission rules, update-mode handling, authoring guidelines.references/note-templates.md — sub-note templates (TIL / task / idea / decision).agents/ — full drafter-agent instructions.Shared procedure for every skill that writes back to an existing note. Two modes — every caller picks one explicitly.
Shared helper. split_body(content) — locate the first standalone --- line that is followed (eventually) by a ## Timeline heading. Return (body_above, timeline_entries). If no ## Timeline heading exists, return (content, []).
Used when the caller has produced a rewritten compiled-truth body (the new canonical state) plus exactly one new timeline entry. Callers: session-ingest Phase 4-2 (session update), session-ingest Phase 4-3 (sub-note append), and ingest-jira (issue upsert).
Inputs: draft_body (full new body — includes --- + ## Timeline + one new bullet), existing_content (current doc).
(draft_above, draft_entries) = split_body(draft_body). draft_entries must contain exactly one bullet. If it contains zero, treat as drafter error → fall back to akb_update with draft_body unchanged and log a warning.(_, existing_entries) = split_body(existing_content).{draft_above}
---
## Timeline
{draft_entries[0]}
{existing_entries}
Used when the caller produced only a new timeline bullet and wants the existing compiled-truth zone preserved verbatim. Callers: session-ingest Phase 4-4 (task resolve) and the decision supersede path, ingest-doc re-ingest, and ingest-confluence re-ingest.
Inputs: new_entry (a single bullet line, possibly with sub-bullets), existing_content.
(existing_above, existing_entries) = split_body(existing_content).existing_content had no ## Timeline section, append "\n\n---\n\n## Timeline\n\n" to existing_above before proceeding.{existing_above}
---
## Timeline
{new_entry}
{existing_entries}