agent-postflight 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bharath Natarajan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # postflight
2
+
3
+ **read your agent's run back.**
4
+
5
+ vercel shipped agent observability into deploys this week. langsmith, langfuse, braintrust,
6
+ helicone all did it before them. the dashboard is table stakes now. it was never the point.
7
+
8
+ the point of a run is to make the *next* run better. postflight is a tiny, local, harness-neutral
9
+ CLI that turns an agent transcript into a **flight review + the skill to extract** — not a chart to
10
+ stare at.
11
+
12
+ ```
13
+ npx agent-postflight
14
+ ```
15
+
16
+ no signup, no SDK, no platform. it reads your most recent Claude Code run and tells you where the
17
+ loop leaked.
18
+
19
+ ## the demo is the argument
20
+
21
+ run it on a real session:
22
+
23
+ ```
24
+ $ npx agent-postflight
25
+
26
+ # postflight — flight review
27
+
28
+ **725 tool calls** across 1,434 turns · 20 distinct tools · 3d 8h (resumed) · 1,319,024 tokens generated
29
+
30
+ > worth a look: 26 tool errors (4%) · ~57,015 tokens re-reading the same things · 8 redundant read patterns · 8 retry loops
31
+
32
+ ## redundant work — the agent re-derived the same thing
33
+ - `Read` × 27 — `site/agent-worker/src/index.ts` · ~29,128 tokens
34
+ - `Read` × 8 — `README.md` · ~1,344 tokens
35
+ - `Read` × 7 — `site/index.html` · ~9,396 tokens
36
+
37
+ ## the skill to extract →
38
+ the agent ran `Read` on index.ts 27× — re-deriving the same thing instead of remembering it.
39
+ capture its durable facts once (a skill, or a line in CLAUDE.md), and the next run reads it once.
40
+ ```
41
+
42
+ that `Read × 27` is a real number from a real session. the agent read the same file twenty-seven
43
+ times because nothing told it to remember. that's not a model problem. it's a missing loop — and a
44
+ missing loop is a skill waiting to be written.
45
+
46
+ ## what it finds
47
+
48
+ - **redundant work** — the same file read, the same command run, the same search repeated. with a
49
+ rough token cost for the re-reads.
50
+ - **retry loops** — a tool that errored, then got tried again (and whether it ever recovered).
51
+ - **heaviest turns** — where the output tokens actually went.
52
+ - **run shape** — the dominant tool-to-tool pattern.
53
+ - **the skill to extract** — the one thing repeated enough that it should be captured once.
54
+ `--skill` writes a `SKILL.md` scaffold for it. that's the loop, closed.
55
+
56
+ ## usage
57
+
58
+ ```
59
+ postflight review your latest Claude Code run
60
+ postflight path/to/run.jsonl review a specific transcript
61
+ postflight --list list recent runs
62
+ postflight --json findings as JSON (pipe it anywhere)
63
+ postflight --skill write the proposed skill to .claude/skills/<name>/SKILL.md
64
+ ```
65
+
66
+ ## harness-neutral by design
67
+
68
+ the Claude Code JSONL reader is the first adapter. the analysis never sees the format — so
69
+ OpenAI Agents SDK, LangGraph, and raw-log adapters are additive, not rewrites. (Claude Code
70
+ first because it's where the transcripts already live on disk. more adapters next.)
71
+
72
+ ## the honest caveat
73
+
74
+ if your runs are short and tight, postflight will tell you so and find nothing to fix — that's a
75
+ good run, not a failure. and the loop-closing (`--skill`) gives you a *scaffold*, not a finished
76
+ skill; you still write the durable facts. the value is that it points at exactly the right thing to
77
+ capture. it will not fabricate a problem to look useful.
78
+
79
+ ## why this exists
80
+
81
+ built by [Bharath Natarajan](https://bharath.sh) — this is loop engineering as a tool: a run should
82
+ teach the next run. observability shows you the run. postflight closes the loop.
83
+
84
+ MIT.
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ // postflight — read your agent's run back.
3
+ // usage: postflight [transcript.jsonl] [--json] [--skill] [--list]
4
+ // with no path, it finds your most recent Claude Code transcript.
5
+
6
+ import { readdirSync, statSync, existsSync, writeFileSync, mkdirSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { homedir } from "node:os";
9
+ import { parseTranscript } from "../lib/parse.js";
10
+ import { analyze } from "../lib/analyze.js";
11
+ import { toMarkdown, skillMarkdown } from "../lib/report.js";
12
+
13
+ const args = process.argv.slice(2);
14
+ const flags = new Set(args.filter((a) => a.startsWith("--")));
15
+ const positional = args.filter((a) => !a.startsWith("--"));
16
+
17
+ function findTranscripts() {
18
+ const base = join(homedir(), ".claude", "projects");
19
+ if (!existsSync(base)) return [];
20
+ const out = [];
21
+ for (const proj of readdirSync(base)) {
22
+ const dir = join(base, proj);
23
+ let st;
24
+ try {
25
+ st = statSync(dir);
26
+ } catch {
27
+ continue;
28
+ }
29
+ if (!st.isDirectory()) continue;
30
+ for (const f of readdirSync(dir)) {
31
+ if (f.endsWith(".jsonl")) {
32
+ const p = join(dir, f);
33
+ try {
34
+ out.push({ path: p, mtime: statSync(p).mtimeMs, project: proj });
35
+ } catch {}
36
+ }
37
+ }
38
+ }
39
+ return out.sort((a, b) => b.mtime - a.mtime);
40
+ }
41
+
42
+ function main() {
43
+ if (flags.has("--help") || flags.has("-h")) {
44
+ console.log(
45
+ `postflight — read your agent's run back.\n\n` +
46
+ `usage:\n` +
47
+ ` postflight [transcript.jsonl] review a run (default: your latest Claude Code run)\n` +
48
+ ` postflight --list list recent runs\n` +
49
+ ` postflight --json emit findings as JSON\n` +
50
+ ` postflight --skill write the proposed skill to .claude/skills/<name>/SKILL.md\n`
51
+ );
52
+ return;
53
+ }
54
+
55
+ const transcripts = findTranscripts();
56
+
57
+ if (flags.has("--list")) {
58
+ if (!transcripts.length) return console.log("no Claude Code transcripts found under ~/.claude/projects");
59
+ for (const t of transcripts.slice(0, 15)) {
60
+ console.log(`${new Date(t.mtime).toISOString().slice(0, 16).replace("T", " ")} ${t.project}`);
61
+ }
62
+ return;
63
+ }
64
+
65
+ let path = positional[0];
66
+ if (!path) {
67
+ if (!transcripts.length) {
68
+ console.error(
69
+ "postflight: no transcript given and none found under ~/.claude/projects.\n" +
70
+ "pass a path: postflight path/to/run.jsonl"
71
+ );
72
+ process.exit(1);
73
+ }
74
+ path = transcripts[0].path;
75
+ }
76
+ if (!existsSync(path)) {
77
+ console.error(`postflight: no such file: ${path}`);
78
+ process.exit(1);
79
+ }
80
+
81
+ const parsed = parseTranscript(path);
82
+ if (!parsed.toolCalls.length) {
83
+ console.error("postflight: no tool calls found in that transcript (nothing to review).");
84
+ process.exit(1);
85
+ }
86
+ const a = analyze(parsed);
87
+
88
+ if (flags.has("--json")) {
89
+ console.log(JSON.stringify(a, null, 2));
90
+ return;
91
+ }
92
+
93
+ process.stdout.write(toMarkdown(a, { source: path }));
94
+
95
+ if (flags.has("--skill")) {
96
+ if (!a.skill) {
97
+ console.error("\npostflight: nothing repeated enough to extract a skill from this run.");
98
+ return;
99
+ }
100
+ const dir = join(process.cwd(), ".claude", "skills", a.skill.name);
101
+ mkdirSync(dir, { recursive: true });
102
+ const out = join(dir, "SKILL.md");
103
+ writeFileSync(out, skillMarkdown(a.skill) + "\n");
104
+ console.error(`\npostflight: wrote proposed skill → ${out} (review before using)`);
105
+ }
106
+ }
107
+
108
+ main();
package/lib/analyze.js ADDED
@@ -0,0 +1,127 @@
1
+ // analyze.js — the flight review. Takes the harness-neutral parse output and finds
2
+ // the loops: redundant read-like calls, error/retry chains, and the dominant
3
+ // repeated workflow worth extracting into a skill. No dashboards — findings + a fix.
4
+
5
+ const APPROX_TOKENS_PER_CHAR = 0.27; // ~4 chars/token, for rough "re-read" cost
6
+
7
+ function slug(s) {
8
+ return (
9
+ (s || "pattern")
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, "-")
12
+ .replace(/^-+|-+$/g, "")
13
+ .slice(0, 40) || "pattern"
14
+ );
15
+ }
16
+
17
+ export function analyze(parsed) {
18
+ const { toolCalls } = parsed;
19
+
20
+ // --- tool histogram ---
21
+ const histogram = {};
22
+ for (const c of toolCalls) histogram[c.name] = (histogram[c.name] || 0) + 1;
23
+
24
+ // --- redundant read-like calls (same tool + same target, seen >1x) ---
25
+ const sigCount = new Map(); // sig -> { name, target, count, wastedChars }
26
+ for (const c of toolCalls) {
27
+ if (!c.readLike || !c.target) continue;
28
+ const sig = `${c.name}::${c.target}`;
29
+ const e = sigCount.get(sig) || { name: c.name, target: c.target, count: 0, chars: 0 };
30
+ e.count++;
31
+ e.chars += c.resultLen || 0;
32
+ sigCount.set(sig, e);
33
+ }
34
+ const redundant = [...sigCount.values()]
35
+ .filter((e) => e.count > 1)
36
+ .map((e) => ({
37
+ name: e.name,
38
+ target: e.target,
39
+ count: e.count,
40
+ // wasted = the repeats (count-1) worth of re-read result payload
41
+ wastedTokens: Math.round(((e.chars * (e.count - 1)) / e.count) * APPROX_TOKENS_PER_CHAR),
42
+ }))
43
+ .sort((a, b) => b.count - a.count || b.wastedTokens - a.wastedTokens);
44
+
45
+ // --- error / retry chains: an errored call followed by same-tool calls nearby ---
46
+ const errors = toolCalls.filter((c) => c.isError);
47
+ const retryChains = [];
48
+ for (let i = 0; i < toolCalls.length; i++) {
49
+ const c = toolCalls[i];
50
+ if (!c.isError) continue;
51
+ // look ahead up to 3 calls for a same-tool retry
52
+ let j = i + 1;
53
+ const chain = [c];
54
+ while (j < toolCalls.length && j <= i + 4) {
55
+ const n = toolCalls[j];
56
+ if (n.name === c.name) {
57
+ chain.push(n);
58
+ if (!n.isError) break; // recovered
59
+ }
60
+ j++;
61
+ }
62
+ if (chain.length > 1) {
63
+ retryChains.push({
64
+ name: c.name,
65
+ target: c.target,
66
+ attempts: chain.length,
67
+ recovered: !chain[chain.length - 1].isError,
68
+ atSeq: c.seq,
69
+ });
70
+ i = j; // skip past this chain
71
+ }
72
+ }
73
+
74
+ // --- heaviest turns by output tokens ---
75
+ const heaviest = [...parsed.heavy].sort((a, b) => b.outputTokens - a.outputTokens).slice(0, 3);
76
+
77
+ // --- run shape: the most common consecutive tool bigram (a separate stat) ---
78
+ const bigrams = new Map();
79
+ for (let i = 1; i < toolCalls.length; i++) {
80
+ const k = `${toolCalls[i - 1].name} → ${toolCalls[i].name}`;
81
+ bigrams.set(k, (bigrams.get(k) || 0) + 1);
82
+ }
83
+ const topBigram = [...bigrams.entries()].sort((a, b) => b[1] - a[1])[0];
84
+
85
+ // --- the skill to extract: the most re-derived read target, framed coherently ---
86
+ // if the agent re-read the same thing 3+ times, it never remembered it — that's a
87
+ // skill (or a CLAUDE.md fact) waiting to be written. This is the loop-closing move.
88
+ let skill = null;
89
+ const topRepeat = redundant[0];
90
+ if (topRepeat && topRepeat.count >= 3) {
91
+ const subject = topRepeat.target.split(/[\/\s]/).filter(Boolean).slice(-1)[0] || topRepeat.target;
92
+ skill = {
93
+ name: slug(`${subject}-context`),
94
+ kind: topRepeat.name,
95
+ target: topRepeat.target,
96
+ count: topRepeat.count,
97
+ why:
98
+ `the agent ran \`${topRepeat.name}\` on this **${topRepeat.count}×** — re-deriving the same thing ` +
99
+ `instead of remembering it. capture its durable facts once (a skill, or a line in CLAUDE.md/AGENTS.md), ` +
100
+ `and the next run reads it once.`,
101
+ };
102
+ }
103
+
104
+ const wastedTokens = redundant.reduce((a, r) => a + r.wastedTokens, 0);
105
+
106
+ return {
107
+ summary: {
108
+ toolCalls: toolCalls.length,
109
+ distinctTools: Object.keys(histogram).length,
110
+ assistantTurns: parsed.assistantTurns,
111
+ errorCount: errors.length,
112
+ errorRate: toolCalls.length ? errors.length / toolCalls.length : 0,
113
+ // honest token accounting: output = real generation. context re-read (cache)
114
+ // is reported separately — you can't sum per-turn input, it re-sends context.
115
+ outputTokens: parsed.usage.output,
116
+ contextReadTokens: parsed.usage.cacheRead,
117
+ wastedTokens,
118
+ durationMs: parsed.durationMs,
119
+ runShape: topBigram ? { pair: topBigram[0], count: topBigram[1] } : null,
120
+ },
121
+ histogram,
122
+ redundant: redundant.slice(0, 8),
123
+ retryChains: retryChains.slice(0, 8),
124
+ heaviest,
125
+ skill,
126
+ };
127
+ }
package/lib/parse.js ADDED
@@ -0,0 +1,135 @@
1
+ // parse.js — turn a Claude Code JSONL transcript into an ordered list of tool
2
+ // calls + token usage. Harness-neutral by design: the Claude Code reader is the
3
+ // first adapter; the shape it returns (a flat `events`/`toolCalls` model) is what
4
+ // every other adapter will produce, so analyze/report never learn the format.
5
+
6
+ import { readFileSync } from "node:fs";
7
+
8
+ // Normalize a tool call's input into a stable signature target, so "read the same
9
+ // file twice" collapses to one signature regardless of incidental key order.
10
+ function targetOf(name, input) {
11
+ input = input || {};
12
+ switch (name) {
13
+ case "Read":
14
+ case "Edit":
15
+ case "Write":
16
+ case "NotebookEdit":
17
+ return input.file_path || input.path || "";
18
+ case "Bash":
19
+ return (input.command || "").trim().replace(/\s+/g, " ");
20
+ case "Grep":
21
+ return `${input.pattern || ""} :: ${input.path || input.glob || ""}`;
22
+ case "Glob":
23
+ return input.pattern || "";
24
+ case "WebFetch":
25
+ return input.url || "";
26
+ case "WebSearch":
27
+ return input.query || "";
28
+ case "Task":
29
+ case "Agent":
30
+ return (input.description || input.subagent_type || "").trim();
31
+ default: {
32
+ try {
33
+ return JSON.stringify(input).slice(0, 200);
34
+ } catch {
35
+ return "";
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ // Tools that READ state and should be idempotent — repeating them verbatim is the
42
+ // smell postflight looks for. Repeating an Edit/Write is often legitimate.
43
+ const READ_LIKE = new Set(["Read", "Grep", "Glob", "Bash", "WebFetch", "WebSearch"]);
44
+
45
+ export function parseTranscript(path) {
46
+ const raw = readFileSync(path, "utf8");
47
+ const lines = raw.split("\n");
48
+
49
+ const toolCalls = []; // ordered
50
+ const resultsById = new Map(); // tool_use_id -> { isError, len }
51
+ const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
52
+ let assistantTurns = 0;
53
+ let userTurns = 0;
54
+ let firstTs = null;
55
+ let lastTs = null;
56
+ let seq = 0;
57
+ const heavy = []; // { seq, outputTokens, firstToolAfter }
58
+
59
+ for (const line of lines) {
60
+ const s = line.trim();
61
+ if (!s) continue;
62
+ let d;
63
+ try {
64
+ d = JSON.parse(s);
65
+ } catch {
66
+ continue;
67
+ }
68
+ if (d.timestamp) {
69
+ const t = Date.parse(d.timestamp);
70
+ if (!Number.isNaN(t)) {
71
+ firstTs = firstTs === null ? t : Math.min(firstTs, t);
72
+ lastTs = lastTs === null ? t : Math.max(lastTs, t);
73
+ }
74
+ }
75
+ const msg = d.message;
76
+ if (!msg || typeof msg !== "object") continue;
77
+
78
+ if (d.type === "assistant") {
79
+ assistantTurns++;
80
+ const u = msg.usage;
81
+ if (u) {
82
+ usage.input += u.input_tokens || 0;
83
+ usage.output += u.output_tokens || 0;
84
+ usage.cacheRead += u.cache_read_input_tokens || 0;
85
+ usage.cacheWrite += u.cache_creation_input_tokens || 0;
86
+ heavy.push({ turn: assistantTurns, outputTokens: u.output_tokens || 0 });
87
+ }
88
+ } else if (d.type === "user") {
89
+ userTurns++;
90
+ }
91
+
92
+ const content = msg.content;
93
+ if (!Array.isArray(content)) continue;
94
+ for (const b of content) {
95
+ if (!b || typeof b !== "object") continue;
96
+ if (b.type === "tool_use") {
97
+ toolCalls.push({
98
+ seq: seq++,
99
+ id: b.id || null,
100
+ name: b.name || "?",
101
+ target: targetOf(b.name, b.input),
102
+ readLike: READ_LIKE.has(b.name),
103
+ ts: d.timestamp ? Date.parse(d.timestamp) : null,
104
+ });
105
+ } else if (b.type === "tool_result") {
106
+ const id = b.tool_use_id;
107
+ if (id) {
108
+ let len = 0;
109
+ const c = b.content;
110
+ if (typeof c === "string") len = c.length;
111
+ else if (Array.isArray(c))
112
+ len = c.reduce((a, x) => a + (typeof x?.text === "string" ? x.text.length : 0), 0);
113
+ resultsById.set(id, { isError: !!b.is_error, len });
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ // attach results to their calls
120
+ for (const call of toolCalls) {
121
+ const r = call.id ? resultsById.get(call.id) : null;
122
+ call.isError = r ? r.isError : false;
123
+ call.resultLen = r ? r.len : 0;
124
+ }
125
+
126
+ return {
127
+ path,
128
+ toolCalls,
129
+ usage,
130
+ assistantTurns,
131
+ userTurns,
132
+ durationMs: firstTs !== null && lastTs !== null ? lastTs - firstTs : null,
133
+ heavy,
134
+ };
135
+ }
package/lib/report.js ADDED
@@ -0,0 +1,110 @@
1
+ // report.js — render the flight review as markdown (the human-readable argument)
2
+ // and the proposed skill as an extractable SKILL.md scaffold.
3
+
4
+ function fmt(n) {
5
+ return Math.round(n).toLocaleString("en-US");
6
+ }
7
+ function dur(ms) {
8
+ if (ms == null) return "unknown";
9
+ const s = Math.round(ms / 1000);
10
+ if (s < 60) return `${s}s`;
11
+ const m = Math.floor(s / 60);
12
+ if (m < 60) return `${m}m ${s % 60}s`;
13
+ const h = Math.floor(m / 60);
14
+ if (h < 24) return `${h}h ${m % 60}m`;
15
+ const d = Math.floor(h / 24);
16
+ return `${d}d ${h % 24}h (resumed)`;
17
+ }
18
+
19
+ export function toMarkdown(a, meta = {}) {
20
+ const s = a.summary;
21
+ const L = [];
22
+ L.push(`# postflight — flight review`);
23
+ if (meta.source) L.push(`\n\`${meta.source}\``);
24
+ L.push("");
25
+ L.push(
26
+ `**${fmt(s.toolCalls)} tool calls** across ${fmt(s.assistantTurns)} turns · ` +
27
+ `${fmt(s.distinctTools)} distinct tools · ${dur(s.durationMs)} · ` +
28
+ `${fmt(s.outputTokens)} tokens generated`
29
+ );
30
+
31
+ const flags = [];
32
+ if (s.errorCount) flags.push(`${s.errorCount} tool errors (${(s.errorRate * 100).toFixed(0)}%)`);
33
+ if (s.wastedTokens) flags.push(`~${fmt(s.wastedTokens)} tokens re-reading the same things`);
34
+ if (a.redundant.length) flags.push(`${a.redundant.length} redundant read patterns`);
35
+ if (a.retryChains.length) flags.push(`${a.retryChains.length} retry loops`);
36
+ if (flags.length) L.push(`\n> **worth a look:** ${flags.join(" · ")}`);
37
+ else L.push(`\n> clean run — no redundant reads or retry loops detected.`);
38
+
39
+ if (a.redundant.length) {
40
+ L.push(`\n## redundant work — the agent re-derived the same thing`);
41
+ for (const r of a.redundant) {
42
+ const w = r.wastedTokens ? ` · ~${fmt(r.wastedTokens)} tokens` : "";
43
+ L.push(`- \`${r.name}\` × **${r.count}** — \`${trunc(r.target, 76)}\`${w}`);
44
+ }
45
+ }
46
+
47
+ if (a.retryChains.length) {
48
+ L.push(`\n## retry loops — errored, then tried again`);
49
+ for (const c of a.retryChains) {
50
+ const ok = c.recovered ? "recovered" : "**never recovered**";
51
+ L.push(`- \`${c.name}\` — ${c.attempts} attempts, ${ok} — \`${trunc(c.target, 60)}\``);
52
+ }
53
+ }
54
+
55
+ if (a.heaviest.length) {
56
+ L.push(`\n## heaviest turns (by output tokens)`);
57
+ for (const h of a.heaviest) L.push(`- turn ${h.turn} — ${fmt(h.outputTokens)} tokens`);
58
+ }
59
+
60
+ const shape = s.runShape ? ` · run shape: \`${s.runShape.pair}\` ×${s.runShape.count}` : "";
61
+ L.push(`\n## tool mix`);
62
+ L.push(
63
+ Object.entries(a.histogram)
64
+ .sort((x, y) => y[1] - x[1])
65
+ .map(([n, c]) => `${n} ${c}`)
66
+ .join(" · ") + shape
67
+ );
68
+
69
+ L.push(`\n## the skill to extract →`);
70
+ if (a.skill) {
71
+ L.push(a.skill.why);
72
+ L.push("\n```markdown");
73
+ L.push(skillMarkdown(a.skill));
74
+ L.push("```");
75
+ L.push(
76
+ `\n_close the loop: save that as \`.claude/skills/${a.skill.name}/SKILL.md\` (or drop the facts into ` +
77
+ `CLAUDE.md), and the next run stops rediscovering it._`
78
+ );
79
+ } else {
80
+ L.push(`nothing repeated ≥3× this run — nothing worth extracting. that's a tight run.`);
81
+ }
82
+
83
+ return L.join("\n") + "\n";
84
+ }
85
+
86
+ export function skillMarkdown(skill) {
87
+ const subject = skill.target.split(/[\/\s]/).filter(Boolean).slice(-1)[0] || skill.target;
88
+ return [
89
+ `---`,
90
+ `name: ${skill.name}`,
91
+ `description: Use when you need to know about ${subject}. postflight saw the agent ${skill.kind} it ${skill.count}× in one run — capture it once here so it isn't re-derived.`,
92
+ `---`,
93
+ ``,
94
+ `# ${skill.name}`,
95
+ ``,
96
+ `\`${skill.target}\``,
97
+ ``,
98
+ `The agent re-read this ${skill.count} times in a single run. Write its durable facts here —`,
99
+ `what it is, the 3-5 things worth knowing, the gotchas — so the next run reads it once:`,
100
+ ``,
101
+ `- <key fact 1>`,
102
+ `- <key fact 2>`,
103
+ `- <gotcha>`,
104
+ ].join("\n");
105
+ }
106
+
107
+ function trunc(s, n) {
108
+ s = String(s || "");
109
+ return s.length > n ? s.slice(0, n - 1) + "…" : s;
110
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "agent-postflight",
3
+ "version": "0.1.0",
4
+ "description": "Read your agent's run back. A harness-neutral CLI that turns an agent transcript into a flight review + a skill to extract — so the next run is better, not just observed.",
5
+ "type": "module",
6
+ "bin": {
7
+ "agent-postflight": "bin/postflight.js",
8
+ "postflight": "bin/postflight.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node test/run.mjs"
12
+ },
13
+ "keywords": [
14
+ "agents",
15
+ "ai-agents",
16
+ "claude-code",
17
+ "observability",
18
+ "loop-engineering",
19
+ "agent-experience",
20
+ "cli",
21
+ "skills",
22
+ "mcp"
23
+ ],
24
+ "author": "Bharath Natarajan <hi@bharath.sh> (https://bharath.sh)",
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "files": [
30
+ "bin/",
31
+ "lib/",
32
+ "README.md",
33
+ "LICENSE"
34
+ ]
35
+ }