Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions decisions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Decisions

_A decision report mined from your SpecStory coding histories._

Teams make dozens of consequential decisions inside coding sessions - which database,
which naming convention, which layout - and they evaporate into scrollback. Decisions
reads the `.specstory/history` transcripts your coding agents already write and
recovers them into an **evidence-cited decision log** (think auto-mined ADRs):

- **Decided** - firm choices still standing
- **Changed** - decisions later superseded, with the full chain
(e.g. Postgres -> SQLite -> DuckDB) and what replaced them
- **Open** - questions raised but never resolved
- **Entities** - the thing each decision was about (a component, system, UI element,
file, or convention), used to build per-entity decision timelines

## Insights

Beyond the log itself, the report surfaces:

- **Churn hotspots** - entities whose decisions were reversed 2+ times (design thrash,
unstable requirements)
- **Provisional-decision debt** - "for now" / temporary decisions that were never
revisited (each one is latent rework)
- **Re-litigated decisions** - settled entities where a later open question re-opened
the debate
- **Decisions lacking rationale** - flagged in the written report (the ones nobody will
be able to reconstruct later)

## How it works

A **deterministic engine** (plain Node, zero dependencies, no LLM or network) does the
high-recall pass: signal grammars find candidate decisions in user turns ("let's go
with...", "actually, switch to...", "should we...?", "for now..."), entities are
extracted from the matching sentence, candidates cluster into per-entity timelines per
project, and supersession/status falls out of timeline order. Duplicated prompt text is
deduped and ubiquitous entities (e.g. a product name that appears everywhere) never
chain unrelated decisions. Output is deterministic - byte-identical across runs.

The **calling agent** then does the high-precision pass from the engine's JSON: drops
false positives, resolves referents ("I like option 1" -> what option 1 actually was,
by reading the cited evidence), names entities, and writes the final report.

## Install

From a clone of this repo:

```zsh
cd decisions
./install.sh
```

That bundles the engine and the skill into `~/.agents/skills/decisions` and symlinks it
into `~/.claude/skills/decisions`, so `/decisions` is available from **any** Claude Code
session in **any** project. It is self-contained - it does not read from any other
skill's directory. Re-run `./install.sh` any time to update.

Requirements: Node >= 22.5 (for `node:sqlite`), and the SpecStory CLI capturing
histories into `.specstory/history/`.

## Use

Start a new Claude Code session (skills load at session start), then:

```
/decisions
```

or just ask: _"what did we decide this month?"_, _"what's still undecided?"_,
_"what decisions changed?"_. With no arguments it asks three short questions - **Scope**
(which repos), **Window** (all time / 30 / 90 days), **Goal** (full report / open
decisions / changed decisions / insights) - then runs and saves the report to
`.specstory/decisions/<YYYY-MM-DD>-decisions.md`.

Sample digest shape:

```
decisions report - 124 decision(s) across 5 project(s) (window: all time)

Insights
Churn hotspots (decisions reversed 2+ times)
- EventStore (api): changed 2 time(s) - Postgres -> SQLite -> DuckDB
Provisional decisions never revisited ("for now" debt)
- nav (website, 2026-03-11): "It's ok for now if /specstory isn't linked to from the nav"

## website
Decided
- root logo: "let's make the logo in the top left be the SpecStory logo" [decided] · 2026-03-11
.specstory/history/2026-03-11_19-52-23Z.md:80802
Changed
(none)
Open
- ads config: "should I leave these or do different config in google ads console?" [open] · 2026-04-09
```

Each decision carries its evidence ref (`path:line`) so every claim is checkable.

## Develop

The engine is plain Node (ESM, zero dependencies, `node:sqlite`). Run the tests:

```zsh
cd decisions
npm test
```

The engine's own corpus lives at `~/.specstory/decisions.db` (never shared with other
skills); use `--db /tmp/scratch.db` while developing.

## License

Apache-2.0.
84 changes: 84 additions & 0 deletions decisions/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
name: decisions
description: SpecStory Decisions - a decision report mined from SpecStory coding histories (any agent - Claude Code, Codex, Cursor, Gemini, and more). It finds the decisions made in a window of sessions, the decisions that were later changed (with the supersession chain), the still-open decisions, and the entities they were about - plus insights like churn hotspots and "for now" provisional-decision debt. Use when someone asks "what did we decide", "what decisions changed", "what's still undecided", "why did we choose X", or wants a decision log / ADR-style report over a .specstory/history corpus.
argument-hint: "Enter = guided setup · or plain English, e.g. 'last 30 days, just the open decisions'"
allowed-tools: Bash, Read, Write, AskUserQuestion
license: Apache-2.0
metadata:
author: SpecStory
version: "1.0.0"
---

# Decisions

Teams make dozens of small, consequential decisions inside coding sessions - which database,
which naming scheme, which layout - and they evaporate. **Decisions** recovers them from
SpecStory histories into an evidence-cited decision report (a lightweight, auto-mined ADR log):
what was **decided**, what was later **changed** (and what superseded it), what is still
**open**, and **which entities** (components, systems, UI elements) each decision was about.

The work is split in two, and the split is the design:

- A **deterministic engine** (`scripts/decisions.mjs`) does high-RECALL extraction: signal
grammars find candidate decisions in user turns, entities cluster them into per-entity
timelines, supersession and insights fall out of timeline order. Every candidate carries its
**quote** and an **evidence ref** (`path:line`). No LLM, no network in the engine.
- **You do the high-PRECISION pass**: drop false positives, resolve referents, name entities,
and write the report. Do not read raw transcripts wholesale - work from the engine's JSON and
open specific evidence refs only when a quote needs context.

## Default flow

1. **Index the corpus** (its own DB - nothing shared with other skills):
```bash
node "${CLAUDE_SKILL_DIR}/scripts/decisions.mjs" index --projects <parent-of-repos> --db <db>
# or a single tree: --scan <root> or a single history dir: --dir <dir>
```

2. **Mine decisions** and capture both views:
```bash
node "${CLAUDE_SKILL_DIR}/scripts/decisions.mjs" decisions --db <db> [--days N] # digest
node "${CLAUDE_SKILL_DIR}/scripts/decisions.mjs" decisions --db <db> [--days N] --json # for your pass
```
The JSON has `decisions[]` (project, status decided|changed|open, provisional flag, entity,
summary/quote, date, evidence, supersededBy, chain) and `insights` (churn, provisional
debt, reopened).

3. **Your precision pass** - for each candidate:
- **Drop non-decisions.** The grammars are deliberately loose; an imperative instruction
with no choice in it ("look at all the places where...") is not a decision. Keep only
genuine choices between alternatives, policies, namings, or commitments.
- **Resolve referents.** Quotes like "I like option 1" or "go with your recommendation" are
real decisions with invisible content. `Read` the evidence file around the ref line to
recover WHAT was chosen, and restate it ("chose option 1: server-side rendering for the
landing page").
- **Name entities.** Many candidates are `(unnamed)` - infer the entity from the quote and
context (a component, route, system, convention). Merge near-duplicate decisions.
- **Keep the evidence.** Every decision in your report cites its `path:line`. Never invent
a decision that has no engine candidate behind it.

4. **Write the report** and save it to `.specstory/decisions/<YYYY-MM-DD>-decisions.md`:
- **Summary** - counts, projects, window.
- **Insights** - churn hotspots (entities re-decided 2+ times, with the chain), provisional
"for now" debt (never revisited - each is latent rework), re-litigated decisions (settled,
then re-questioned), and one you judge yourself: **decisions lacking any stated rationale**
(flag them; they are the ones nobody will be able to reconstruct later).
- **Per project:** **Decided** (entity, decision, date, rationale if stated, evidence) ·
**Changed** (old -> new, when, and why if stated) · **Open** (the question, how long it has
been open, and your suggested next step for each).
Also offer `--out <file>` to keep the raw digest beside your written report.

## Guided start

Invoked bare, ask three short questions (`AskUserQuestion` or plain chat), then run:

- **Scope** - which repos / parent directory of `.specstory/history` corpora?
- **Window** - all time (default - supersession needs history), or last 30 / 90 days?
- **Goal** - the full **report**, just **open decisions**, just **changed decisions**, or the
**insights** (churn + debt)?

## Conventions

Node ESM only, zero dependencies, Node >= 22.5. No em dashes anywhere (use " - "). The engine
is deterministic (byte-identical across runs on the same corpus); all judgment - filtering,
naming, rationale, next steps - is yours.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## 2026-06-02 09:00:00Z

<!-- Claude Code Session dddddddd-0000-4000-8000-000000000001 (2026-06-02_09-00-00Z) -->

_**User**_

let's use Postgres for the EventStore persistence layer

---

_**Agent (claude-opus-4-20250514)**_

Tool use: **Edit** `src/EventStore.ts`
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## 2026-06-09 09:00:00Z

<!-- Claude Code Session dddddddd-0000-4000-8000-000000000002 (2026-06-09_09-00-00Z) -->

_**User**_

actually, let's switch the EventStore to SQLite instead of Postgres

---

_**Agent (claude-opus-4-20250514)**_

Tool use: **Edit** `src/EventStore.ts`
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## 2026-06-10 09:00:00Z

<!-- Claude Code Session dddddddd-0000-4000-8000-000000000004 (2026-06-10_09-00-00Z) -->

_**User**_

for now, keep the API key handling in AuthClient as a plain environment variable

---

_**Agent (claude-opus-4-20250514)**_

Tool use: **Edit** `src/AuthClient.ts`
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## 2026-06-15 09:00:00Z

<!-- Claude Code Session dddddddd-0000-4000-8000-000000000003 (2026-06-15_09-00-00Z) -->

_**User**_

on second thought, switch the EventStore to DuckDB

---

_**Agent (claude-opus-4-20250514)**_

Tool use: **Edit** `src/EventStore.ts`
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## 2026-06-18 09:00:00Z

<!-- Claude Code Session dddddddd-0000-4000-8000-000000000005 (2026-06-18_09-00-00Z) -->

_**User**_

should we use tabs or a sidebar for the SettingsPanel layout?

---

_**Agent (claude-opus-4-20250514)**_

Tool use: **Edit** `src/SettingsPanel.tsx`
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## 2026-06-20 09:00:00Z

<!-- Claude Code Session dddddddd-0000-4000-8000-000000000006 (2026-06-20_09-00-00Z) -->

_**User**_

go with kebab-case for all the CLI flag names in FlagParser

---

_**Agent (claude-opus-4-20250514)**_

Tool use: **Edit** `src/FlagParser.ts`
27 changes: 27 additions & 0 deletions decisions/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Install the decisions skill so it is available from any Claude Code session.
#
# Self-contained: bundles decisions' own engine. No dependency on any other skill.
# Re-run any time to update. Pass a target dir to override the default.
set -euo pipefail

HERE="$(cd "$(dirname "$0")" && pwd)" # .../decisions
DEST="${1:-$HOME/.agents/skills/decisions}"

if [ ! -f "$HERE/scripts/decisions.mjs" ]; then
echo "error: engine not found at $HERE/scripts - run this from the decisions/ directory of a clone." >&2
exit 1
fi

mkdir -p "$DEST"
rm -rf "$DEST/scripts"
cp -R "$HERE/scripts" "$DEST/scripts" # bundle the self-contained engine
cp "$HERE/SKILL.md" "$DEST/SKILL.md" # SKILL.md calls ${CLAUDE_SKILL_DIR}/scripts/decisions.mjs

mkdir -p "$HOME/.claude/skills"
ln -sfn "$DEST" "$HOME/.claude/skills/decisions"

echo "installed decisions:"
echo " skill -> $DEST"
echo " linked -> $HOME/.claude/skills/decisions"
echo "Open a new Claude Code session, then run /decisions (skills load at session start)."
11 changes: 11 additions & 0 deletions decisions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "specstory-decisions",
"version": "1.0.0",
"description": "A decision report mined from SpecStory coding histories. Standalone skill.",
"type": "module",
"license": "Apache-2.0",
"engines": { "node": ">=22.5" },
"scripts": {
"test": "node --test tests/*.test.mjs"
}
}
71 changes: 71 additions & 0 deletions decisions/scripts/decisions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env node
// decisions engine - CLI entry. A standalone skill: no dependency on any other skill.
// Modules under lib/ (zero npm deps, Node built-ins only):
// lib/patterns.mjs regexes / classifiers lib/parse.mjs transcript -> beats
// lib/discover.mjs project + file discovery lib/db.mjs SQLite corpus schema
// lib/indexer.mjs incremental indexing lib/decide.mjs decision mining + insights
//
// Subcommands:
// index --dir <history-dir> [--dir ...] | --projects <parent> | --scan <root> [--db <path>] [--force]
// decisions [--db <path>] [--days N] [--json] [--out <file>]
// (also accepts the discovery flags above: it will index first, then render)
//
// Default DB: ~/.specstory/decisions.db (its own corpus; shares nothing with other skills).
import { join } from 'node:path'
import { homedir } from 'node:os'
import { writeFileSync } from 'node:fs'
import { openDb } from './lib/db.mjs'
import { indexCorpus } from './lib/indexer.mjs'
import { computeDecisions, renderDigest, decisionsJson } from './lib/decide.mjs'

function parseArgs(argv) {
const a = {
cmd: '', dirs: [], projects: '', scan: '',
db: join(homedir(), '.specstory', 'decisions.db'),
days: 0, maxBytes: 200_000_000, force: false, json: false, out: '',
}
let i = 0
if (argv[0] && !argv[0].startsWith('--')) { a.cmd = argv[0]; i = 1 }
for (; i < argv.length; i++) {
const t = argv[i]
if (t === '--dir') a.dirs.push(argv[++i])
else if (t === '--projects') a.projects = argv[++i]
else if (t === '--scan') a.scan = argv[++i]
else if (t === '--db') a.db = argv[++i]
else if (t === '--days') a.days = +argv[++i]
else if (t === '--max-bytes') a.maxBytes = +argv[++i]
else if (t === '--force') a.force = true
else if (t === '--json' || t === '--emit=json') a.json = true
else if (t === '--out') a.out = argv[++i]
}
if (!a.cmd) a.cmd = 'decisions'
return a
}

const ARGS = parseArgs(process.argv.slice(2))
const db = openDb(ARGS.db)

if (ARGS.cmd === 'index') {
const r = indexCorpus(db, ARGS)
if (r.error) { process.stderr.write(`decisions index: ${r.error}\n`); process.exit(2) }
process.stdout.write(`indexed ${r.indexed} sessions (${r.skippedKnown} unchanged, ${r.skippedBig} too large) across ${r.projects.length} project(s) -> ${ARGS.db}\n`)
} else if (ARGS.cmd === 'decisions' || ARGS.cmd === 'report') {
// Convenience: with discovery flags, index first (unbounded - supersession needs the
// full timeline even for a windowed report), then mine from the same corpus.
if (ARGS.dirs.length || ARGS.projects || ARGS.scan) {
const r = indexCorpus(db, { ...ARGS, days: 0 })
if (r.error) { process.stderr.write(`decisions: ${r.error}\n`); process.exit(2) }
}
const result = computeDecisions(db, { days: ARGS.days })
if (ARGS.json) {
process.stdout.write(JSON.stringify(decisionsJson(result), null, 2) + '\n')
} else {
const digest = renderDigest(result, { days: ARGS.days })
const text = digest.endsWith('\n') ? digest : digest + '\n'
process.stdout.write(text)
if (ARGS.out) writeFileSync(ARGS.out, text)
}
} else {
process.stderr.write('usage: decisions.mjs index|decisions [--dir D | --projects P | --scan R] [--db PATH] [--days N] [--json] [--out FILE]\n')
process.exit(2)
}
Loading