Skip to content

ADR-0025: Session Handoff Document with a Mandatory Failed-Approaches Section

  • Status: Accepted. Implemented in lib/session-handoff.cjs, written by /np:pause-work, gated on read by /np:resume-work.
  • Date: 2026-07-30
  • Supersedes: None
  • Related: ADR-0015 (the other handoff — agent-to-agent messaging, a different artefact), ADR-0013 (the store the failed approaches feed), ADR-0022 (the adjacent context-cost problem)

Context and Problem Statement

Three mechanisms already existed at the session boundary, and none of them is a re-entry brief:

  • /np:pause-work stamps STATE.session.stopped_at and a checkpoint pointer. That resumes a task — it says nothing about the reasoning around it.
  • /np:session-report aggregates metrics since the last pointer. Backward-looking accounting.
  • lib/handoff.cjs (ADR-0015) is agent-to-agent messaging inside a task. Different lifetime, different audience.

So a fresh session arrived holding the previous session's conclusions — the commits, the roadmap, the STATE cursor — but not its dead ends.

The failure that produces is specific, and it is not "context was lost". Context was kept in the wrong shape. A conversation summary preserves the structure of the conversation alongside its conclusions, so discarded approaches, refuted assumptions and debugging detours survive in the record as apparently-live options. The next session reads them as untried and re-attempts what the last session already disproved.

The learnings store cannot close this gap, and the reason is structural rather than a missing feature. np-learnings-extractor is spawned against a turn diff. An approach that was tried and reverted before it was ever committed does not appear in any diff, so it is invisible to the extractor by construction.

Decision Drivers

  • Negative knowledge is the expensive kind. What worked is recoverable from git log. What was tried and abandoned is recoverable from nowhere.
  • A skipped section and an empty section must not look identical. Otherwise "nothing failed" is indistinguishable from "I did not fill this in", and the field degrades to noise within a few sessions.
  • The next session must actually read it. A document nobody reads is worse than none, because it creates the appearance of continuity.
  • No secrets on disk. A handoff naturally wants to say "the transport needs credentials", and the difference between naming a variable and recording its value is the difference between useful and a leak.
  • [ADR-0002] Zero runtime dependencies.

Considered Options

  • A: Rely on the host's conversation compaction. Status quo for the narrative. Preserves the wrong shape, as described above.
  • B: Extend session-report with a forward-looking section. Conflates a metrics artefact with a briefing artefact; the report is generated from JSONL and has no place to put reasoning.
  • C: Extend ADR-0015 handoffs to a to: "*" session-scoped note. Reuses the mechanism but overloads a per-task messaging artefact with a per-session one; handoff-list becomes ambiguous.
  • D: A distinct six-section session document with a mandatory failed-approaches section. Chosen.

Decision Outcome

Chosen: Option D, named resume-doc at the CLI and RESUME.md on disk, deliberately not sharing the handoff-* verb family with ADR-0015.

Six sections, rendered in this order:

#SectionContent
1goalWhy the project exists, a few sentences. Not the session's goal — the project's.
2statusOne line: running | partial | blocked.
3active_filesOnly the paths that matter, each with the purpose it serves here. A bare path list is a directory listing.
4changesWhat was built or modified, each with its why. The diff already carries the what.
5failed_approachesMandatory. What was tried and abandoned, each with why_failed.
6next_stepsOrdered; at least one carries a concrete re-entry command.

Goal and status lead so that a reader who stops after ten lines still knows where the project stands.

Section 5 is the decision

Every other section is reconstructible: the goal from PROJECT.md, the changes from git log, the files from the diff, the next steps from roadmap.yaml and STATE.md. A failed approach is reconstructible from nothing, because a commit records what worked and never what was tried and reverted.

Omitting it is therefore not a stylistic choice. It discards the session's most expensive finding and hands the next session a clean slate on which it will repeat the same experiment.

An empty list is legitimate — a single mechanical task where the first approach worked is a real and unremarkable session — but it requires no_failed_approaches_reason. Deny-by-default, the same shape as the archive classifier's --allow-unknown: a session where nothing was abandoned and a session that skipped the section must not be the same bytes on disk.

Each failed_approaches entry also requires why_failed. Without the cause, the entry reads as an untried option and invites the retry it exists to prevent.

Feeding the learnings store

toLearningCandidates converts each failed approach to {pattern, outcome: 'failed'}. The outcome is always failed, which is what the ADR-0013 confidence calculation demotes — so a disproved approach ranks below a proven one instead of competing with it.

This is the only path by which an approach abandoned before commit can reach the store at all.

The read gate

/np:resume-work gains a Step 0 that runs before status routing and returns none | blocked | clear. While blocked, reading the handoff is the only permitted action: no source edits, no executor spawn, no STATE mutation.

Clearing the gate requires a 2–6 line summary in the reader's own words, stored on disk. The summary is mandatory because an ack nobody had to write is a checkbox, and a checkbox gets ticked without reading — which is precisely the state the gate exists to detect rather than create. The upper bound exists because restating the handoff is not summarising it.

Secret hygiene

The writer refuses tokens, AWS key ids, JWTs, private-key blocks, credentialed URLs, and assignments to secret-ish names. Variable names pass and are encouraged: reads STRIPE_SECRET_KEY from the environment is the useful form, and the value is the leak. The scan reports the offending field path so the author can fix the right line.

Consequences

Good, because:

  • The one class of knowledge git cannot hold now has a home, and it reaches the ranked learnings store.
  • pause-work and resume-work become a matched pair rather than two independent stamps.
  • The gate makes re-derivation expensive on purpose: reading is cheaper than the acknowledgement, so reading is the path of least resistance.

Bad, because:

  • Writing a good handoff is real work at the moment the operator most wants to stop. The mandatory section is a friction point by design, and it will occasionally be filled with a thin reason to get past it.
  • The ack floor is a heuristic. Two lines of plausible text satisfies it without comprehension; it raises the cost of skipping, it does not eliminate it.
  • RESUME.md is a second narrative surface alongside PROJECT.md and M<NNN>-CONTEXT.md. The scoping is different (session vs. project vs. milestone) but the distinction has to be maintained.

Implementation note worth keeping

The pattern-length cap covers approach and why_failed together, because both are concatenated into one learnings pattern. Capping only the approach let a long cause overflow the store's 4 KiB limit inside logLearning — a throw at write time, far from the field responsible.

More Information

  • Implementation: lib/session-handoff.cjs, CLI bin/np-tools/resume-doc.cjs (verbs write | lint | read | status | ack | learnings).
  • On disk: .nubos-pilot/RESUME.md (human), .nubos-pilot/state/session-handoff.json (tooling SSOT), .nubos-pilot/handoffs-session/<ts>-resume.md (archive; never overwritten).
  • Markdown is never parsed back — the JSON record is the SSOT and RESUME.md is the rendering. Round-tripping prose is how a format starts lying about its own contents.
  • Origin: the six-point structure and the observation that the failed-approaches section is the most-skipped and most-valuable one come from a user-supplied handoff guide (2026-07-30).