ll-skills 1.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -2
- package/README.md +134 -69
- package/agents/ll-executor.md +96 -0
- package/agents/ll-reviewer.md +76 -0
- package/agents/ll-scout.md +83 -0
- package/agents/ll-verifier.md +127 -0
- package/assets/preamble.md +70 -0
- package/assets/settings.suggested.json +19 -0
- package/bin/install.js +311 -54
- package/hooks/ll-precompact.js +49 -0
- package/hooks/ll-skills-check-update.js +1 -1
- package/hooks/ll-state.js +158 -0
- package/package.json +4 -2
- package/scripts/fixtures/empty/.gitkeep +0 -0
- package/scripts/fixtures/git-history.sh +49 -0
- package/scripts/fixtures/project/BACKLOG.md +11 -0
- package/scripts/fixtures/project/PLAN.md +66 -0
- package/scripts/fixtures/project/PROGRESS.md +57 -0
- package/scripts/fixtures/project/ROADMAP.md +28 -0
- package/scripts/fixtures/project/VERIFICATION.md +17 -0
- package/scripts/fixtures/project/decisions/DEC-0041-cents.md +16 -0
- package/scripts/fixtures/project/phases/07/PLAN.md +145 -0
- package/scripts/fixtures/project/src/a.ts +3 -0
- package/scripts/fixtures/project/src/pay.ts +5 -0
- package/scripts/fixtures/project/test/a.test.ts +5 -0
- package/scripts/ll-tools.js +664 -0
- package/scripts/smoke-test.sh +340 -0
- package/skills/ll-brainstorm/SKILL.md +180 -0
- package/skills/ll-brainstorm/references/decision-policy.md +118 -0
- package/skills/ll-brainstorm/references/techniques.md +75 -0
- package/skills/ll-close/SKILL.md +69 -0
- package/skills/ll-close/references/delivery.md +75 -0
- package/skills/ll-close/references/retrospective.md +48 -0
- package/skills/ll-decide/SKILL.md +135 -0
- package/skills/ll-decide/references/decision-policy.md +118 -0
- package/skills/ll-decide/references/decision-room.md +62 -0
- package/skills/ll-decide/references/disarm.md +79 -0
- package/skills/ll-decide/references/feedback-ingestion.md +90 -0
- package/skills/ll-decide/references/interview.md +102 -0
- package/skills/ll-decide/references/plan-skeleton.md +150 -0
- package/skills/ll-decide/references/premise-gate.md +83 -0
- package/skills/ll-decide/references/premortem.md +90 -0
- package/skills/ll-decide/references/review-spec.md +91 -0
- package/skills/ll-goal/SKILL.md +64 -0
- package/skills/ll-goal/references/goal-template.md +90 -0
- package/skills/ll-implement/SKILL.md +118 -0
- package/skills/ll-implement/references/briefs.md +123 -0
- package/skills/ll-implement/references/decision-policy.md +118 -0
- package/skills/ll-implement/references/phase-conversation.md +76 -0
- package/skills/ll-implement/references/phase-plan.md +111 -0
- package/skills/ll-oncall/SKILL.md +123 -0
- package/skills/ll-oncall/references/deploy-preflight.md +57 -0
- package/skills/ll-oncall/references/federation.md +110 -0
- package/skills/ll-oncall/references/watch-brief.md +39 -0
- package/skills/ll-refine/SKILL.md +116 -0
- package/skills/ll-refine/references/production-access.md +48 -0
- package/skills/ll-refine/references/visual-gate.md +100 -0
- package/skills/ll-research/SKILL.md +87 -0
- package/skills/ll-research/references/citation-check.md +38 -0
- package/skills/ll-research/references/front-brief.md +41 -0
- package/skills/ll-research/references/market-mode.md +83 -0
- package/skills/ll-resume/SKILL.md +75 -0
- package/skills/ll-update/SKILL.md +75 -0
- package/skills/ll-verify/SKILL.md +75 -0
- package/skills/ll-verify/references/verifier-briefs.md +116 -0
- package/agents/ll-implementador.md +0 -23
- package/skills/ll-atualizar/SKILL.md +0 -68
- package/skills/ll-decidir-antes/SKILL.md +0 -81
- package/skills/ll-decidir-antes/referencias/protocolo-entrevista.md +0 -112
- package/skills/ll-decidir-antes/referencias/template-spec.md +0 -238
- package/skills/ll-desarmar/SKILL.md +0 -254
- package/skills/ll-desarmar/referencias/execucao-adversarial.md +0 -217
- package/skills/ll-desarmar/referencias/humanos-e-substitutos.md +0 -116
- package/skills/ll-desarmar/referencias/placar-e-realimentacao.md +0 -140
- package/skills/ll-orquestrar/SKILL.md +0 -100
- package/skills/ll-pesquisar/SKILL.md +0 -159
- package/skills/ll-pesquisar/referencias/frente-de-pesquisa.md +0 -147
- package/skills/ll-pesquisar/referencias/sintese-e-fontes.md +0 -148
- package/skills/ll-pesquisar-mercado/SKILL.md +0 -112
- package/skills/ll-pesquisar-mercado/referencias/dossie.md +0 -375
- package/skills/ll-pesquisar-mercado/referencias/indice-e-fechamento.md +0 -122
- package/skills/ll-pesquisar-mercado/referencias/padroes-de-pesquisa.md +0 -149
- package/skills/ll-verificar-entrega/SKILL.md +0 -73
- package/skills/ll-verificar-entrega/referencias/briefs-auditoria.md +0 -291
- package/skills/ll-voltar-do-futuro/SKILL.md +0 -239
- package/skills/ll-voltar-do-futuro/referencias/anti-padroes-e-fundamentos.md +0 -201
- package/skills/ll-voltar-do-futuro/referencias/vetores-e-testes.md +0 -228
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// ll-state.js — ll-skills SessionStart hook. Injects the epilogue and the live
|
|
4
|
+
// state of the project into the new session. Reads stdin {cwd, source}.
|
|
5
|
+
// Never writes, never touches stderr, always exits 0.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { execFileSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const MAX_LINES = 60;
|
|
12
|
+
const J = path.join;
|
|
13
|
+
|
|
14
|
+
// <ll-shared:state>
|
|
15
|
+
// Copied verbatim into hooks/ll-state.js. Self-contained: only `fs` and `path`.
|
|
16
|
+
function readAnchoredBlock(file, name) {
|
|
17
|
+
let txt;
|
|
18
|
+
try { txt = fs.readFileSync(file, 'utf8'); } catch { return null; }
|
|
19
|
+
const all = txt.split('\n');
|
|
20
|
+
const open = new RegExp('^<!--\\s*' + name + '\\s*-->\\s*$');
|
|
21
|
+
const close = new RegExp('^<!--\\s*/' + name + '\\s*-->\\s*$');
|
|
22
|
+
let start = -1, end = -1;
|
|
23
|
+
for (let i = 0; i < all.length; i++) {
|
|
24
|
+
if (start < 0) { if (open.test(all[i])) start = i; } else if (close.test(all[i])) { end = i; break; }
|
|
25
|
+
}
|
|
26
|
+
if (start < 0 || end < 0) return null;
|
|
27
|
+
return { file, start, end, all, lines: all.slice(start + 1, end) };
|
|
28
|
+
}
|
|
29
|
+
function llSplitFlow(str) { // top-level commas only
|
|
30
|
+
const out = [];
|
|
31
|
+
let cur = '', depth = 0, q = null;
|
|
32
|
+
for (const ch of str) {
|
|
33
|
+
if (q) { cur += ch; if (ch === q) q = null; continue; }
|
|
34
|
+
if (ch === '"' || ch === "'") { q = ch; cur += ch; continue; }
|
|
35
|
+
if (ch === '[' || ch === '{') depth++;
|
|
36
|
+
else if (ch === ']' || ch === '}') depth--;
|
|
37
|
+
else if (ch === ',' && depth === 0) { out.push(cur); cur = ''; continue; }
|
|
38
|
+
cur += ch;
|
|
39
|
+
}
|
|
40
|
+
if (cur.trim() !== '') out.push(cur);
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function llScalar(raw) {
|
|
44
|
+
const s = String(raw).trim();
|
|
45
|
+
if (/^\[[\s\S]*\]$/.test(s)) return llSplitFlow(s.slice(1, -1)).map(llScalar);
|
|
46
|
+
if (/^\{[\s\S]*\}$/.test(s)) {
|
|
47
|
+
const o = {};
|
|
48
|
+
for (const part of llSplitFlow(s.slice(1, -1))) {
|
|
49
|
+
const m = /^\s*([A-Za-z_][\w-]*)\s*:\s*([\s\S]*)$/.exec(part);
|
|
50
|
+
if (m) o[m[1]] = llScalar(m[2]);
|
|
51
|
+
}
|
|
52
|
+
return o;
|
|
53
|
+
}
|
|
54
|
+
if (/^(true|yes)$/i.test(s)) return true;
|
|
55
|
+
if (/^(false|no)$/i.test(s)) return false;
|
|
56
|
+
if (/^".*"$/.test(s) || /^'.*'$/.test(s)) return s.slice(1, -1);
|
|
57
|
+
return s;
|
|
58
|
+
}
|
|
59
|
+
function parseStateBlockLite(bl) {
|
|
60
|
+
const st = { phase: null, milestones: {}, order: [] };
|
|
61
|
+
let inM = false;
|
|
62
|
+
for (let i = 0; i < bl.length; i++) {
|
|
63
|
+
const line = bl[i].replace(/\s+$/, '');
|
|
64
|
+
if (!line.trim() || /^\s*#/.test(line)) continue;
|
|
65
|
+
let m = /^phase:\s*(.*)$/.exec(line);
|
|
66
|
+
if (m) { st.phase = String(llScalar(m[1])); inM = false; continue; }
|
|
67
|
+
if (/^milestones:\s*$/.test(line)) { inM = true; continue; }
|
|
68
|
+
m = /^(\s*)(M\d+|G-\d+):\s*\{(.*)\}\s*$/.exec(line);
|
|
69
|
+
if (m && inM) {
|
|
70
|
+
st.milestones[m[2]] = llScalar('{' + m[3] + '}');
|
|
71
|
+
st.order.push({ id: m[2], index: i, indent: m[1] });
|
|
72
|
+
} else if (/^\S/.test(line)) inM = false;
|
|
73
|
+
}
|
|
74
|
+
return st;
|
|
75
|
+
}
|
|
76
|
+
function boardFromState(st) {
|
|
77
|
+
const ids = Object.keys((st && st.milestones) || {});
|
|
78
|
+
const passed = ids.filter((i) => st.milestones[i].passes === true).length;
|
|
79
|
+
return { phase: st ? st.phase : null, passed, total: ids.length, list: ids,
|
|
80
|
+
text: 'phase ' + ((st && st.phase) || '?') + ' · milestones ' + passed + '/' + ids.length };
|
|
81
|
+
}
|
|
82
|
+
// </ll-shared:state>
|
|
83
|
+
|
|
84
|
+
function git(args, cwd) {
|
|
85
|
+
try {
|
|
86
|
+
return execFileSync('git', args, { cwd, timeout: 3000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
87
|
+
} catch { return ''; }
|
|
88
|
+
}
|
|
89
|
+
function nonEmpty(s) { return s.split('\n').filter((l) => l.trim() !== ''); }
|
|
90
|
+
|
|
91
|
+
function readStdin() {
|
|
92
|
+
try { return JSON.parse(fs.readFileSync(0, 'utf8')); } catch { return {}; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Last `## Epilogue` section of PROGRESS.md: {head: linesBefore, body: sectionLines}.
|
|
96
|
+
function splitEpilogue(all) {
|
|
97
|
+
let at = -1;
|
|
98
|
+
for (let i = 0; i < all.length; i++) if (/^## Epilogue/.test(all[i])) at = i;
|
|
99
|
+
if (at < 0) return { head: all, body: [] };
|
|
100
|
+
let end = all.length;
|
|
101
|
+
for (let i = at + 1; i < all.length; i++) if (/^## /.test(all[i])) { end = i; break; }
|
|
102
|
+
return { head: all.slice(0, at), body: nonEmpty(all.slice(at, end).join('\n')).slice(0, 25) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function build(cwd, source) {
|
|
106
|
+
const progress = J(cwd, 'PROGRESS.md');
|
|
107
|
+
let raw = null;
|
|
108
|
+
try { raw = fs.readFileSync(progress, 'utf8'); } catch { raw = null; }
|
|
109
|
+
if (raw === null && !fs.existsSync(J(cwd, 'phases'))) return null;
|
|
110
|
+
|
|
111
|
+
const out = [];
|
|
112
|
+
const st = raw === null ? null : parseStateBlockLite((readAnchoredBlock(progress, 'll-state') || { lines: [] }).lines);
|
|
113
|
+
const board = boardFromState(st);
|
|
114
|
+
const phase = (st && st.phase) || 'NN';
|
|
115
|
+
|
|
116
|
+
if (source === 'compact') {
|
|
117
|
+
out.push('[post-compaction] re-read phases/' + phase + '/PLAN.md and the milestone board below before continuing; the files on disk are the truth, not the summary.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const all = raw === null ? [] : raw.split('\n');
|
|
121
|
+
const { head, body } = splitEpilogue(all);
|
|
122
|
+
if (body.length) out.push('', ...body);
|
|
123
|
+
const recent = nonEmpty(head.join('\n')).slice(-15);
|
|
124
|
+
if (recent.length) out.push('', 'recent PROGRESS:', ...recent);
|
|
125
|
+
|
|
126
|
+
const status = nonEmpty(git(['status', '--short'], cwd));
|
|
127
|
+
if (status.length) {
|
|
128
|
+
out.push('', 'git status:', ...status.slice(0, 20));
|
|
129
|
+
if (status.length > 20) out.push(' (+' + (status.length - 20) + ' more)');
|
|
130
|
+
}
|
|
131
|
+
const wt = nonEmpty(git(['worktree', 'list'], cwd));
|
|
132
|
+
if (wt.length > 1) out.push('', 'worktrees:', ...wt);
|
|
133
|
+
|
|
134
|
+
let waiting = [];
|
|
135
|
+
try {
|
|
136
|
+
waiting = fs.readdirSync(J(cwd, 'decisions')).sort()
|
|
137
|
+
.filter((n) => /\.md$/.test(n) && /WAITING/.test(fs.readFileSync(J(cwd, 'decisions', n), 'utf8')))
|
|
138
|
+
.slice(0, 8);
|
|
139
|
+
} catch { waiting = []; }
|
|
140
|
+
if (waiting.length) out.push('', 'WAITING on you: ' + waiting.join(', '));
|
|
141
|
+
|
|
142
|
+
out.push('', board.text);
|
|
143
|
+
if (out.length <= MAX_LINES) return out;
|
|
144
|
+
return out.slice(0, MAX_LINES - 2).concat(['', board.text]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function main() {
|
|
148
|
+
const input = readStdin();
|
|
149
|
+
const cwd = typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd();
|
|
150
|
+
const lines = build(cwd, input.source);
|
|
151
|
+
if (!lines || !lines.length) return;
|
|
152
|
+
process.stdout.write(JSON.stringify({
|
|
153
|
+
hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: lines.join('\n').trim() },
|
|
154
|
+
}) + '\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try { main(); } catch { /* silence */ }
|
|
158
|
+
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ll-skills",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.2",
|
|
4
|
+
"description": "Evidence-driven development pipeline for Claude Code: router preamble, 11 skills (brainstorm, research, decide, goal, implement, verify, close, resume, refine, oncall, update), 4 agents, 3 hooks and a state helper",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ll-skills": "bin/install.js"
|
|
7
7
|
},
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"skills",
|
|
11
11
|
"agents",
|
|
12
12
|
"hooks",
|
|
13
|
+
"scripts",
|
|
14
|
+
"assets",
|
|
13
15
|
"CHANGELOG.md",
|
|
14
16
|
"README.md"
|
|
15
17
|
],
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Builds a throwaway git repo from scripts/fixtures/project/ into <target-dir>.
|
|
3
|
+
# Five commits on five distinct days inside a nine-day span, in the order
|
|
4
|
+
# test(M1) -> feat(M1) -> feat(M10) -> feat(M2) -> test(M2)
|
|
5
|
+
# so that `tdd-gate M1` passes, `tdd-gate M2` fails and `phase-stats` reports 5 days with work.
|
|
6
|
+
# src/pay.ts is never touched after the first commit: VERIFICATION.md records its sha256.
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
TARGET="${1:-}"
|
|
10
|
+
if [ -z "$TARGET" ]; then
|
|
11
|
+
echo "usage: git-history.sh <target-dir>" >&2
|
|
12
|
+
exit 2
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
SRC="$HERE/project"
|
|
17
|
+
|
|
18
|
+
rm -rf "$TARGET"
|
|
19
|
+
mkdir -p "$TARGET"
|
|
20
|
+
cp -R "$SRC/." "$TARGET/"
|
|
21
|
+
cd "$TARGET"
|
|
22
|
+
|
|
23
|
+
G() { git -c user.name=fixture -c user.email=fixture@example.com -c commit.gpgsign=false "$@"; }
|
|
24
|
+
|
|
25
|
+
commit_on() { # commit_on <day-offset> <subject>
|
|
26
|
+
local day="$1" subject="$2" stamp
|
|
27
|
+
stamp="2026-09-0${day}T10:00:00+00:00"
|
|
28
|
+
GIT_AUTHOR_DATE="$stamp" GIT_COMMITTER_DATE="$stamp" G commit -q -m "$subject"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
git init -q -b main .
|
|
32
|
+
G add -A
|
|
33
|
+
commit_on 1 "test(M1): red cases"
|
|
34
|
+
|
|
35
|
+
printf '\nexport const RECONCILE_VERSION = 2;\n' >> src/a.ts
|
|
36
|
+
G add src/a.ts
|
|
37
|
+
commit_on 3 "feat(M1): reconcile"
|
|
38
|
+
|
|
39
|
+
printf 'export const UNRELATED = true;\n' > src/unrelated.ts
|
|
40
|
+
G add src/unrelated.ts
|
|
41
|
+
commit_on 5 "feat(M10): unrelated"
|
|
42
|
+
|
|
43
|
+
printf 'export function ingest(lines: string[]): number {\n return lines.length;\n}\n' > src/ingest.ts
|
|
44
|
+
G add src/ingest.ts
|
|
45
|
+
commit_on 7 "feat(M2): thing"
|
|
46
|
+
|
|
47
|
+
printf "import { ingest } from '../src/ingest';\n\ntest('ingest counts lines', () => {\n expect(ingest(['a'])).toBe(1);\n});\n" > test/ingest.test.ts
|
|
48
|
+
G add test/ingest.test.ts
|
|
49
|
+
commit_on 9 "test(M2): late test"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# BACKLOG — fixture project
|
|
2
|
+
|
|
3
|
+
| id | born (phase / commit) | type | closing condition (executable) | state |
|
|
4
|
+
|---|---|---|---|---|
|
|
5
|
+
| B-014 | 07 / a1b2c3d | stub | `true` exit 0 | OPEN |
|
|
6
|
+
| B-015 | 07 / b2c3d4e | deviation | `false` exit 0 | OPEN |
|
|
7
|
+
| B-016 | 06 / c3d4e5f | debt | `true` exit 0 | CLOSED |
|
|
8
|
+
|
|
9
|
+
types: deviation | stub | test-not-run | debt | domain-question
|
|
10
|
+
|
|
11
|
+
Closed only by `ll-tools.js backlog-reconcile --run` (the condition passed), never by memory.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# PLAN — fixture project
|
|
2
|
+
|
|
3
|
+
## §0 Precedence
|
|
4
|
+
|
|
5
|
+
This file is self-contained and the ONLY entry of the work. §2 > §3 > §4 > §5. Decisions in §3 are a
|
|
6
|
+
contract — do not re-litigate. Project CLAUDE.md > this file > phase plans > briefs.
|
|
7
|
+
|
|
8
|
+
## §1 Objective and truths
|
|
9
|
+
|
|
10
|
+
One sentence of delivery: every provider event is reconciled exactly once and every mismatch is
|
|
11
|
+
visible to the operator.
|
|
12
|
+
|
|
13
|
+
- T1 Every provider event produces exactly one row in `billing_events`. — `npm test -- a.test.ts`
|
|
14
|
+
- T2 A replayed batch adds no rows. — `npm test -- replay.test.ts`
|
|
15
|
+
- T3 Cent-level mismatches are reported, never dropped. — `npm test -- pay.test.ts`
|
|
16
|
+
|
|
17
|
+
Requirements: REQ-k (phase 07), REQ-m (phase 08).
|
|
18
|
+
|
|
19
|
+
## §2 Invariants
|
|
20
|
+
|
|
21
|
+
I-01 NEVER delete, disable or weaken a test or acceptance criterion. [owner, 2026-08-23]
|
|
22
|
+
I-02 Do not fill a gap with a plausible interpretation: report and ask. [owner, CLAUDE.md]
|
|
23
|
+
|
|
24
|
+
## §3 Owner decisions
|
|
25
|
+
|
|
26
|
+
| id | question | decision | by | date | against recommendation? | reversible? |
|
|
27
|
+
|---|---|---|---|---|---|---|
|
|
28
|
+
| DEC-0041 | cents divergence | pending | — | — | — | 1 migration |
|
|
29
|
+
|
|
30
|
+
## §4 Requirements from the premortem
|
|
31
|
+
|
|
32
|
+
R-01 A silent duplicate is worse than a rejected batch. (from F-01)
|
|
33
|
+
|
|
34
|
+
## §5 Negative scope · freedoms · reserved
|
|
35
|
+
|
|
36
|
+
Out of scope: customer portal. Free: module names, migration layout. Owner only: production deploy.
|
|
37
|
+
|
|
38
|
+
## §6 Global acceptance
|
|
39
|
+
|
|
40
|
+
CA-01 WHEN a batch is ingested twice THE SYSTEM SHALL keep one row per event · tolerance 0 ·
|
|
41
|
+
`npm test -- a.test.ts`
|
|
42
|
+
|
|
43
|
+
## §7 Execution protocol
|
|
44
|
+
|
|
45
|
+
Session fable/high; scout sonnet/medium; contract executor opus/high; verifier opus/high.
|
|
46
|
+
Max 3 executors per wave. Push only with everything green.
|
|
47
|
+
|
|
48
|
+
## §8 Phases
|
|
49
|
+
|
|
50
|
+
See ROADMAP.md. Current phase: 07.
|
|
51
|
+
|
|
52
|
+
## §9 Environment
|
|
53
|
+
|
|
54
|
+
`BILLING_PROVIDER_KEY` (name only, never the value).
|
|
55
|
+
|
|
56
|
+
## §10 What is left for this to survive without the owner
|
|
57
|
+
|
|
58
|
+
A provider sandbox key for the integration test.
|
|
59
|
+
|
|
60
|
+
## §11 Sources
|
|
61
|
+
|
|
62
|
+
Owner conversation 2026-08-23; docs/research-billing/SUMMARY.md.
|
|
63
|
+
|
|
64
|
+
## Errata
|
|
65
|
+
|
|
66
|
+
(append-only; empty)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# PROGRESS — fixture project
|
|
2
|
+
|
|
3
|
+
<!-- ll-state -->
|
|
4
|
+
phase: 07
|
|
5
|
+
milestones:
|
|
6
|
+
M1: { passes: true, commit: a1b2c3d, accepted_at: 2026-09-01T14:02:11Z }
|
|
7
|
+
M2: { passes: false, reason: "acceptance red: 2 tests fail" }
|
|
8
|
+
M3: { passes: false, state: ANSWERED_NO, reason: "spike ran; answer is no", propagates_to: [M5] }
|
|
9
|
+
<!-- /ll-state -->
|
|
10
|
+
|
|
11
|
+
## Rules for all agents
|
|
12
|
+
|
|
13
|
+
- Only the main session writes this file. Executors return blocks; the session appends them.
|
|
14
|
+
- Never delete, disable or weaken a test or an acceptance criterion.
|
|
15
|
+
- Commit per milestone with `type(Mn): what`.
|
|
16
|
+
|
|
17
|
+
## Phase 07
|
|
18
|
+
|
|
19
|
+
- [2026-09-01T13:40Z] phase 07 opened — billing reconciliation
|
|
20
|
+
- [2026-09-01T13:50Z] wave 1/3 — tracer + schema
|
|
21
|
+
- [2026-09-01T14:02Z] M1 acceptance green — npm test -- a.test.ts
|
|
22
|
+
- [2026-09-01T14:03Z] M1 marked passes true
|
|
23
|
+
|
|
24
|
+
### M1 — 2026-09-01 14:02
|
|
25
|
+
|
|
26
|
+
built: reconcile() with idempotent key; 3 behavior cases
|
|
27
|
+
commits: test(M1): red cases · feat(M1): reconcile
|
|
28
|
+
commands: npm test -- a.test.ts → "Tests: 3 passed, 3 total"
|
|
29
|
+
deviations: none · questions: none · backlog: B-014 e2e cart not run (no fixture)
|
|
30
|
+
not_verified: backoff under real network latency
|
|
31
|
+
|
|
32
|
+
- [2026-09-03T09:12Z] wave 2/3 — reconciliation core
|
|
33
|
+
- [2026-09-03T09:40Z] spike for M3 started (cent-level mismatch)
|
|
34
|
+
- [2026-09-03T11:05Z] DEC-0041 opened — cents divergence, waiting on the owner
|
|
35
|
+
- [2026-09-05T08:30Z] M2 executor returned; acceptance red (2 tests fail)
|
|
36
|
+
- [2026-09-05T08:31Z] M2 marked passes false — reason recorded
|
|
37
|
+
- [2026-09-05T10:20Z] B-015 born — TODO markers left in src/billing
|
|
38
|
+
- [2026-09-05T16:44Z] spike for M3 answered: no
|
|
39
|
+
- [2026-09-07T09:00Z] M3 marked ANSWERED_NO — propagates to M5
|
|
40
|
+
- [2026-09-07T09:05Z] wave 3/3 held: M4, M5, M6 depend on the blocked branch
|
|
41
|
+
- [2026-09-07T12:18Z] plan-lint run on phases/07/PLAN.md — defects recorded
|
|
42
|
+
- [2026-09-07T15:10Z] file overlap noticed between M5 and M6 on src/pay.ts
|
|
43
|
+
- [compaction 2026-09-07T15:11Z · auto · HEAD b2c3d4e] re-read phases/07/PLAN.md and the milestone board before continuing.
|
|
44
|
+
- [2026-09-09T08:00Z] verification round for phase 07 started
|
|
45
|
+
- [2026-09-09T08:40Z] VERIFICATION.md written — one criterion stale
|
|
46
|
+
- [2026-09-09T09:15Z] backlog reviewed: B-014 and B-015 still open
|
|
47
|
+
- [2026-09-09T09:30Z] phase 07 stopped at the epilogue
|
|
48
|
+
|
|
49
|
+
## Deferred verification
|
|
50
|
+
|
|
51
|
+
- SC-03 load under 10k rows · `ll-verify --criterion SC-03` · born phase 05
|
|
52
|
+
|
|
53
|
+
## Epilogue — phase 07 — 2026-09-09
|
|
54
|
+
|
|
55
|
+
passed: M1 · left: M2 (acceptance red), M3 (BLOCKED: DEC-0041) · WAITING: DEC-0041 (cents rounding) ·
|
|
56
|
+
new backlog: B-014, B-015 · actions that need you: decide DEC-0041 before wave 3
|
|
57
|
+
▶ Next — `/clear`, then `ll-implement 8`
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# ROADMAP — fixture project
|
|
2
|
+
|
|
3
|
+
| phase | name | depends_on | requirements | state |
|
|
4
|
+
|---|---|---|---|---|
|
|
5
|
+
| 05 | payment intake | — | REQ-a | DONE (docs/history/v1.0) |
|
|
6
|
+
| 06 | provider adapters | 05 | REQ-b | DONE (docs/history/v1.0) |
|
|
7
|
+
| 07 | billing reconciliation | 05, 06 | REQ-k | ACTIVE |
|
|
8
|
+
| 08 | reconciliation reporting | 07 | REQ-m | PLANNED |
|
|
9
|
+
|
|
10
|
+
## Phase 07 — billing reconciliation
|
|
11
|
+
|
|
12
|
+
Objective: every provider event lands in `billing_events` exactly once and mismatches are visible.
|
|
13
|
+
|
|
14
|
+
Success criteria:
|
|
15
|
+
- SC-01 The reconciliation job ingests a provider batch and writes one row per event.
|
|
16
|
+
- SC-02 A replayed batch produces no duplicate rows.
|
|
17
|
+
- SC-03 Cent-level mismatches are reported instead of silently dropped.
|
|
18
|
+
- SC-04 The operator can list tolerated rows for a day.
|
|
19
|
+
|
|
20
|
+
Deferred ideas: customer portal; accounting export.
|
|
21
|
+
|
|
22
|
+
## Phase 08 — reconciliation reporting
|
|
23
|
+
|
|
24
|
+
Objective: the operator reads yesterday's reconciliation without opening the database.
|
|
25
|
+
|
|
26
|
+
Success criteria:
|
|
27
|
+
- SC-01 A daily report lists ingested, tolerated and rejected rows.
|
|
28
|
+
- SC-02 The report is reachable from the operator console.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# VERIFICATION — phase 07 — 2026-09-09
|
|
2
|
+
|
|
3
|
+
verdict: APPROVED_WITH_RESERVATIONS · product: OK · process: FAIL (I-01 weakened without a DEC)
|
|
4
|
+
|
|
5
|
+
| C | criterion | command | exit | file:line | sha256 | freshness | state |
|
|
6
|
+
|---|---|---|---|---|---|---|---|
|
|
7
|
+
| SC-01 | one row per provider event | npm test -- a.test.ts | 0 | src/pay.ts:3 | sha256:946c8f6b2d9a | FRESH | VERIFIED |
|
|
8
|
+
| SC-02 | a replayed batch adds no rows | npm test -- ingest.test.ts | 0 | src/a.ts:1 | sha256:0000dead1234 | FRESH | VERIFIED |
|
|
9
|
+
| SC-03 | cent mismatches are reported | — | — | test/a.test.ts:3 | — | — | PRESENT_NO_BEHAVIOR |
|
|
10
|
+
|
|
11
|
+
BLOCKS: process — the tolerance assertion in `test/a.test.ts` was loosened with no DEC.
|
|
12
|
+
|
|
13
|
+
Disconfirmation: 1 partial requirement (SC-02 happy path only); 1 uncovered error path (provider timeout).
|
|
14
|
+
|
|
15
|
+
What this verification does NOT prove: behavior under real provider latency.
|
|
16
|
+
|
|
17
|
+
Deferred: SC-03 until the fixture exists (BACKLOG B-014).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# DEC-0041 — cents divergence: reject the line or accept and flag?
|
|
2
|
+
|
|
3
|
+
- class: QUESTION · layer: product · impact: HIGH · revert: 1 migration
|
|
4
|
+
- grounding: internal (src/pay.ts:1; 3 cases in test/) | external (research-billing/F02, 2026-09-05)
|
|
5
|
+
- options:
|
|
6
|
+
|
|
7
|
+
| option | what starts to hold | cost | what is lost | revert |
|
|
8
|
+
|---|---|---|---|---|
|
|
9
|
+
| A accept + flag (recommended) | the KPI the owner reads stays whole | 1 day | strict totals | 1 commit |
|
|
10
|
+
| B reject the line | totals are exact | 2 days | manual queue grows | 1 migration |
|
|
11
|
+
|
|
12
|
+
- depends_on: — · conditions: M3, M5
|
|
13
|
+
- status: WAITING — asked 2026-09-03T11:05Z, no answer yet
|
|
14
|
+
- owner's words (verbatim): —
|
|
15
|
+
- consequences by rule: SC-03 in ROADMAP measures "tolerated rows" only under option A.
|
|
16
|
+
- superseded_by: —
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Phase 07 — billing reconciliation
|
|
2
|
+
|
|
3
|
+
Objective (from ROADMAP): every provider event lands in `billing_events` exactly once and mismatches
|
|
4
|
+
are visible. Success criteria: SC-01..SC-04 (pasted from ROADMAP.md).
|
|
5
|
+
Decisions: phases/07/DECISIONS.md · Context: phases/07/CODE-CONTEXT.md
|
|
6
|
+
|
|
7
|
+
## Milestones
|
|
8
|
+
|
|
9
|
+
<!-- ll-milestones -->
|
|
10
|
+
milestones:
|
|
11
|
+
- id: M1
|
|
12
|
+
name: end-to-end tracer
|
|
13
|
+
files: [src/a.ts, test/a.test.ts]
|
|
14
|
+
depends_on: []
|
|
15
|
+
tdd: yes
|
|
16
|
+
acceptance: "npm test -- a.test.ts"
|
|
17
|
+
stop: none
|
|
18
|
+
truths: [T1]
|
|
19
|
+
exclusive: []
|
|
20
|
+
model: opus/high
|
|
21
|
+
verification: internal
|
|
22
|
+
- id: M2
|
|
23
|
+
name: provider batch ingest
|
|
24
|
+
files: [src/ingest.ts, test/ingest.test.ts]
|
|
25
|
+
depends_on: [M1]
|
|
26
|
+
tdd: yes
|
|
27
|
+
acceptance: "npm test -- ingest.test.ts"
|
|
28
|
+
stop: none
|
|
29
|
+
truths: [T2]
|
|
30
|
+
exclusive: []
|
|
31
|
+
model: opus/high
|
|
32
|
+
verification: internal
|
|
33
|
+
- id: M3
|
|
34
|
+
name: cent mismatch spike
|
|
35
|
+
files: [src/spike.ts]
|
|
36
|
+
depends_on: [M1]
|
|
37
|
+
tdd: no
|
|
38
|
+
acceptance: "npm run spike -- cents"
|
|
39
|
+
stop: none
|
|
40
|
+
truths: [T3]
|
|
41
|
+
exclusive: []
|
|
42
|
+
model: sonnet/medium
|
|
43
|
+
verification: internal
|
|
44
|
+
- id: M4
|
|
45
|
+
name: reconcile join
|
|
46
|
+
files: [src/join.ts, test/join.test.ts]
|
|
47
|
+
depends_on: [M2, M3]
|
|
48
|
+
tdd: yes
|
|
49
|
+
acceptance: "npm test -- join.test.ts"
|
|
50
|
+
stop: none
|
|
51
|
+
truths: [T1, T2]
|
|
52
|
+
exclusive: [test:db]
|
|
53
|
+
model: opus/high
|
|
54
|
+
verification: internal
|
|
55
|
+
- id: M5
|
|
56
|
+
name: tolerated rows report
|
|
57
|
+
files: [src/pay.ts, test/pay.test.ts]
|
|
58
|
+
depends_on: [M3]
|
|
59
|
+
tdd: yes
|
|
60
|
+
acceptance: "npm test -- pay.test.ts"
|
|
61
|
+
stop: owner
|
|
62
|
+
truths: [T3]
|
|
63
|
+
exclusive: []
|
|
64
|
+
model: opus/high
|
|
65
|
+
verification: external
|
|
66
|
+
- id: M6
|
|
67
|
+
name: mismatch flag column
|
|
68
|
+
files: [src/pay.ts, src/schema.ts]
|
|
69
|
+
depends_on: [M3]
|
|
70
|
+
tdd: no
|
|
71
|
+
stop: none
|
|
72
|
+
truths: [T3]
|
|
73
|
+
exclusive: [test:db]
|
|
74
|
+
model: sonnet/medium
|
|
75
|
+
verification: internal
|
|
76
|
+
- id: M7
|
|
77
|
+
name: operator listing
|
|
78
|
+
files: [src/list.ts, test/list.test.ts]
|
|
79
|
+
depends_on: [M9, M4]
|
|
80
|
+
tdd: yes
|
|
81
|
+
acceptance: "npm test -- list.test.ts"
|
|
82
|
+
stop: none
|
|
83
|
+
truths: [T1]
|
|
84
|
+
exclusive: []
|
|
85
|
+
model: sonnet/medium
|
|
86
|
+
verification: internal
|
|
87
|
+
<!-- /ll-milestones -->
|
|
88
|
+
|
|
89
|
+
### M1 — end-to-end tracer
|
|
90
|
+
|
|
91
|
+
read_first:
|
|
92
|
+
- src/a.ts:1-3 — the shape to copy for the batch signature
|
|
93
|
+
- test/a.test.ts:1-5 — test layout
|
|
94
|
+
action: export `reconcile(batch)` keyed by `(provider, event_id)`.
|
|
95
|
+
behavior (tdd: yes): duplicate event, new event, empty batch.
|
|
96
|
+
acceptance: `npm test -- a.test.ts` exit 0; last line pasted.
|
|
97
|
+
|
|
98
|
+
### M2 — provider batch ingest
|
|
99
|
+
|
|
100
|
+
read_first:
|
|
101
|
+
- src/a.ts:1-3 — the reconcile entry point
|
|
102
|
+
action: read a provider batch file and hand each event to `reconcile`.
|
|
103
|
+
behavior (tdd: yes): malformed line, empty file, 3-event file.
|
|
104
|
+
acceptance: `npm test -- ingest.test.ts` exit 0.
|
|
105
|
+
|
|
106
|
+
### M3 — cent mismatch spike
|
|
107
|
+
|
|
108
|
+
read_first:
|
|
109
|
+
- src/pay.ts:1-5 — the current tolerance rule
|
|
110
|
+
action: measure how many rows of the September batch differ by cents only.
|
|
111
|
+
acceptance: `npm run spike -- cents` exit 0 with the count on the last line.
|
|
112
|
+
|
|
113
|
+
### M4 — reconcile join
|
|
114
|
+
|
|
115
|
+
read_first:
|
|
116
|
+
- src/a.ts:1-3 — reconcile signature
|
|
117
|
+
action: join ingested events with the ledger rows; keep the idempotency key.
|
|
118
|
+
behavior (tdd: yes): matched, unmatched, duplicate.
|
|
119
|
+
acceptance: `npm test -- join.test.ts` exit 0.
|
|
120
|
+
|
|
121
|
+
### M5 — tolerated rows report
|
|
122
|
+
|
|
123
|
+
read_first:
|
|
124
|
+
- src/pay.ts:1-5 — `tolerated()` is the rule to reuse
|
|
125
|
+
action: list rows tolerated for a day; the owner unblocks the format.
|
|
126
|
+
behavior (tdd: yes): under tolerance, over tolerance, exactly at tolerance.
|
|
127
|
+
acceptance: `npm test -- pay.test.ts` exit 0.
|
|
128
|
+
|
|
129
|
+
### M6 — mismatch flag column
|
|
130
|
+
|
|
131
|
+
read_first:
|
|
132
|
+
- src/pay.ts:1-5 — where the flag is computed
|
|
133
|
+
action: add the `mismatch_flag` column and write it from `tolerated()`.
|
|
134
|
+
|
|
135
|
+
### M7 — operator listing
|
|
136
|
+
|
|
137
|
+
read_first:
|
|
138
|
+
- src/pay.ts:1-5 — the tolerated rule the listing filters on
|
|
139
|
+
action: expose the tolerated rows through the operator console.
|
|
140
|
+
behavior (tdd: yes): empty day, one row, paging.
|
|
141
|
+
acceptance: `npm test -- list.test.ts` exit 0.
|
|
142
|
+
|
|
143
|
+
## Waves (printed by `ll-tools.js waves`)
|
|
144
|
+
|
|
145
|
+
| wave | milestones | builds |
|