@eir-labs/coltrane 0.24.36 → 0.24.37
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/agents/session-analyst.json +28 -0
- package/agents/session-review-verifier.json +31 -0
- package/agents/session-reviewer.json +32 -0
- package/dist/src/claude_invoker.d.ts +26 -0
- package/dist/src/claude_invoker.js +57 -1
- package/dist/src/claude_invoker.js.map +1 -1
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/mcp.js +6 -0
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/server.js +26 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/domain_types/session-analysis.json +59 -0
- package/domain_types/session-census.json +72 -0
- package/domain_types/session-review-verdict.json +48 -0
- package/domain_types/session-review.json +86 -0
- package/domain_types/session-target.json +26 -0
- package/package.json +1 -1
- package/releases.json +41 -0
- package/skills/gig-census/fixtures/basic.json +33 -0
- package/skills/gig-census/fixtures/ledger.jsonl +3 -0
- package/skills/gig-census/fixtures/outputs/00000000-0000-4000-8000-00000000cens.jsonl +4 -0
- package/skills/gig-census/meta.json +20 -0
- package/skills/gig-census/skill.mjs +114 -0
- package/skills/session-census/fixtures/basic.json +10 -0
- package/skills/session-census/fixtures/sample.jsonl +6 -0
- package/skills/session-census/meta.json +25 -0
- package/skills/session-census/skill.mjs +141 -0
- package/standards/session-review-v0.json +104 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// The deterministic half of a session review (determinism 1.0): read one Claude Code transcript and
|
|
2
|
+
// COUNT it. No model, no judgement — every number here is a fact a reviewer can be held to, and the
|
|
3
|
+
// review seat downstream may claim nothing this census does not carry.
|
|
4
|
+
//
|
|
5
|
+
// A transcript is JSONL, one record per line, and large (this session's is 23MB), so it is streamed
|
|
6
|
+
// line by line and never held whole. What comes out is small enough for a seat to read.
|
|
7
|
+
import { createReadStream, statSync } from "node:fs";
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
|
|
10
|
+
const TOOL_BUCKET = (name) =>
|
|
11
|
+
name.startsWith("mcp__") ? `mcp:${name.split("__")[1] ?? "?"}` : name;
|
|
12
|
+
|
|
13
|
+
export default async function run(input) {
|
|
14
|
+
// A ROOT skill chair is handed the gig payload as the runtime holds it: keyed by type slug
|
|
15
|
+
// (`{"session-target": {transcript}}`). A chair fed by upstream outputs gets the merged data
|
|
16
|
+
// instead. Accept both rather than depending on where in a standard this chair happens to sit.
|
|
17
|
+
const target = (input && typeof input === "object" && input["session-target"]) || input || {};
|
|
18
|
+
const path = target && target.transcript ? String(target.transcript) : "";
|
|
19
|
+
// A census that cannot read its transcript THROWS: returning an explanation object would fail the
|
|
20
|
+
// seal's schema check instead, and the caller would read "required property 'records' missing"
|
|
21
|
+
// rather than the reason. Fail with the reason.
|
|
22
|
+
if (!path) throw new Error("session-census: no `transcript` path in the input");
|
|
23
|
+
|
|
24
|
+
let bytes = 0;
|
|
25
|
+
try {
|
|
26
|
+
bytes = statSync(path).size;
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error(`session-census: cannot read "${path}"`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const tools = new Map();
|
|
32
|
+
const operatorByText = new Map();
|
|
33
|
+
const denials = [];
|
|
34
|
+
const errors = [];
|
|
35
|
+
let records = 0;
|
|
36
|
+
let assistantTurns = 0;
|
|
37
|
+
let firstTs = null;
|
|
38
|
+
let lastTs = null;
|
|
39
|
+
let interruptions = 0;
|
|
40
|
+
let inputTokens = 0;
|
|
41
|
+
let outputTokens = 0;
|
|
42
|
+
let cacheRead = 0;
|
|
43
|
+
let cacheWrite = 0;
|
|
44
|
+
let maxContext = 0;
|
|
45
|
+
const seenMessageIds = new Set();
|
|
46
|
+
|
|
47
|
+
const rl = createInterface({ input: createReadStream(path, { encoding: "utf8" }), crlfDelay: Infinity });
|
|
48
|
+
for await (const line of rl) {
|
|
49
|
+
if (!line.trim()) continue;
|
|
50
|
+
let r;
|
|
51
|
+
try { r = JSON.parse(line); } catch { continue; }
|
|
52
|
+
records += 1;
|
|
53
|
+
const ts = typeof r.timestamp === "string" ? r.timestamp : null;
|
|
54
|
+
if (ts) { if (firstTs === null) firstTs = ts; lastTs = ts; }
|
|
55
|
+
|
|
56
|
+
if (r.type === "user") {
|
|
57
|
+
const c = r.message?.content;
|
|
58
|
+
// A STRING content is the operator typing; a list is a tool result coming back.
|
|
59
|
+
if (typeof c === "string") {
|
|
60
|
+
const text = c.trim();
|
|
61
|
+
// The harness injects its own user-shaped records (command output, reminders). An operator
|
|
62
|
+
// message is what is left once those are excluded — named, so the exclusion is auditable.
|
|
63
|
+
const injected =
|
|
64
|
+
text.startsWith("<command-") || text.startsWith("<local-command") ||
|
|
65
|
+
text.startsWith("[Request interrupted") || text.includes("<system-reminder>") ||
|
|
66
|
+
// Harness-delivered, not typed by the operator: a background task reporting in, and a message
|
|
67
|
+
// from another session. Counting either as the operator's would credit them with decisions
|
|
68
|
+
// nobody made.
|
|
69
|
+
text.startsWith("<task-notification>") || text.startsWith("<cross-session-message");
|
|
70
|
+
if (text.startsWith("[Request interrupted")) interruptions += 1;
|
|
71
|
+
// A repeated message (a cron firing the same prompt) is ONE decision said many times, not many
|
|
72
|
+
// decisions: collapse it to a count, first and last seen. Otherwise the census is mostly echo,
|
|
73
|
+
// and a seat reading it would weigh the loop's cadence as if it were the operator's attention.
|
|
74
|
+
if (!injected && text) {
|
|
75
|
+
const key = text.length > 400 ? `${text.slice(0, 400)}…` : text;
|
|
76
|
+
const prior = operatorByText.get(key);
|
|
77
|
+
if (prior) { prior.count += 1; prior.last = ts; }
|
|
78
|
+
else operatorByText.set(key, { text: key, count: 1, first: ts, last: ts });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (r.type === "assistant") {
|
|
84
|
+
const msg = r.message ?? {};
|
|
85
|
+
const id = msg.id;
|
|
86
|
+
if (id && !seenMessageIds.has(id)) {
|
|
87
|
+
seenMessageIds.add(id);
|
|
88
|
+
assistantTurns += 1;
|
|
89
|
+
const u = msg.usage ?? {};
|
|
90
|
+
inputTokens += u.input_tokens ?? 0;
|
|
91
|
+
outputTokens += u.output_tokens ?? 0;
|
|
92
|
+
cacheRead += u.cache_read_input_tokens ?? 0;
|
|
93
|
+
cacheWrite += u.cache_creation_input_tokens ?? 0;
|
|
94
|
+
const ctx = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
|
|
95
|
+
if (ctx > maxContext) maxContext = ctx;
|
|
96
|
+
}
|
|
97
|
+
for (const b of msg.content ?? []) {
|
|
98
|
+
if (b && b.type === "tool_use" && typeof b.name === "string") {
|
|
99
|
+
const k = TOOL_BUCKET(b.name);
|
|
100
|
+
tools.set(k, (tools.get(k) ?? 0) + 1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// A tool result carrying a refusal or an error is evidence a reviewer should not have to find.
|
|
106
|
+
if (r.type === "user" && Array.isArray(r.message?.content)) {
|
|
107
|
+
for (const b of r.message.content) {
|
|
108
|
+
if (!b || b.type !== "tool_result") continue;
|
|
109
|
+
const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content ?? "");
|
|
110
|
+
if (/permission[^\n]{0,40}denied|Blocked by classifier/i.test(text)) {
|
|
111
|
+
denials.push({ at: ts, text: text.slice(0, 300) });
|
|
112
|
+
} else if (b.is_error) {
|
|
113
|
+
errors.push({ at: ts, text: text.slice(0, 300) });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
// The Signal core requires a non-empty `source`: what this reading is OF. For a census that is the
|
|
121
|
+
// transcript it counted, named so a sealed census can be traced back to the file it read.
|
|
122
|
+
source: `session-census://${path}`,
|
|
123
|
+
transcript: path,
|
|
124
|
+
bytes,
|
|
125
|
+
records,
|
|
126
|
+
span: { first: firstTs, last: lastTs },
|
|
127
|
+
turns: {
|
|
128
|
+
assistant: assistantTurns,
|
|
129
|
+
input_tokens: inputTokens,
|
|
130
|
+
output_tokens: outputTokens,
|
|
131
|
+
cache_read_tokens: cacheRead,
|
|
132
|
+
cache_write_tokens: cacheWrite,
|
|
133
|
+
max_context_tokens: maxContext,
|
|
134
|
+
},
|
|
135
|
+
tools: [...tools.entries()].sort((a, b) => b[1] - a[1]).map(([name, calls]) => ({ name, calls })),
|
|
136
|
+
operator_messages: [...operatorByText.values()],
|
|
137
|
+
interruptions,
|
|
138
|
+
denials,
|
|
139
|
+
errors: errors.slice(0, 40),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slug": "session-review-v0",
|
|
3
|
+
"domain": "session-review",
|
|
4
|
+
"agent_slugs": [
|
|
5
|
+
"session-analyst",
|
|
6
|
+
"session-reviewer",
|
|
7
|
+
"session-review-verifier"
|
|
8
|
+
],
|
|
9
|
+
"phases": [
|
|
10
|
+
{
|
|
11
|
+
"name": "census",
|
|
12
|
+
"chairs": [
|
|
13
|
+
{
|
|
14
|
+
"role": "census",
|
|
15
|
+
"skill_slug": "session-census",
|
|
16
|
+
"depends_on": [],
|
|
17
|
+
"input_contract": [
|
|
18
|
+
"session-target"
|
|
19
|
+
],
|
|
20
|
+
"output_contract": [
|
|
21
|
+
"session-census"
|
|
22
|
+
],
|
|
23
|
+
"required_skills": []
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "analyse",
|
|
29
|
+
"chairs": [
|
|
30
|
+
{
|
|
31
|
+
"role": "analyse",
|
|
32
|
+
"agent_slug": "session-analyst",
|
|
33
|
+
"depends_on": [
|
|
34
|
+
"census"
|
|
35
|
+
],
|
|
36
|
+
"input_contract": [
|
|
37
|
+
"session-census"
|
|
38
|
+
],
|
|
39
|
+
"output_contract": [
|
|
40
|
+
"session-analysis"
|
|
41
|
+
],
|
|
42
|
+
"required_skills": [],
|
|
43
|
+
"turn_reserve": 4
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "review",
|
|
49
|
+
"chairs": [
|
|
50
|
+
{
|
|
51
|
+
"role": "write-review",
|
|
52
|
+
"agent_slug": "session-reviewer",
|
|
53
|
+
"depends_on": [
|
|
54
|
+
"census",
|
|
55
|
+
"analyse"
|
|
56
|
+
],
|
|
57
|
+
"input_contract": [
|
|
58
|
+
"session-census",
|
|
59
|
+
"session-analysis"
|
|
60
|
+
],
|
|
61
|
+
"output_contract": [
|
|
62
|
+
"session-review"
|
|
63
|
+
],
|
|
64
|
+
"required_skills": [],
|
|
65
|
+
"turn_reserve": 4
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "check",
|
|
71
|
+
"chairs": [
|
|
72
|
+
{
|
|
73
|
+
"role": "check-review",
|
|
74
|
+
"agent_slug": "session-review-verifier",
|
|
75
|
+
"depends_on": [
|
|
76
|
+
"census",
|
|
77
|
+
"write-review"
|
|
78
|
+
],
|
|
79
|
+
"input_contract": [
|
|
80
|
+
"session-census",
|
|
81
|
+
"session-review"
|
|
82
|
+
],
|
|
83
|
+
"output_contract": [
|
|
84
|
+
"session-review-verdict"
|
|
85
|
+
],
|
|
86
|
+
"required_skills": [],
|
|
87
|
+
"turn_reserve": 4
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"status": "draft",
|
|
93
|
+
"input_types": [
|
|
94
|
+
"session-target"
|
|
95
|
+
],
|
|
96
|
+
"output_types": [
|
|
97
|
+
"session-census",
|
|
98
|
+
"session-analysis",
|
|
99
|
+
"session-review",
|
|
100
|
+
"session-review-verdict"
|
|
101
|
+
],
|
|
102
|
+
"max_examine_rounds": 2,
|
|
103
|
+
"description": "A formal review of one working session, from a census counted by code rather than from anyone's memory. The input is a session-target: the path to a Claude Code transcript. The census chair is SKILL-BACKED (session-census, determinism 1.0, no model): it streams the .jsonl — 23MB and 13,000 records for the session this was built on — and emits counts, the tool histogram, the token classes and peak context, the operator's own messages with identical ones collapsed to a count, the interruptions, the denials and the errors. Nothing a model has to summarize, and nothing a model may contradict. An INTERPRET seat sits between the counts and the prose — composeStandard refuses a CREATE seat fed only by sensing, and it is right: the patterns (where the cost went, what was done twice, where what was ASKED FOR diverges from what the session SPENT ITSELF ON) are a separate judgement from the prose that reports them. The review seat then writes what the session was: the arc, the measured cost, what went wrong, the operator's decisions quoted from their own words, and every claim paired with the census path behind it. The verify seat resolves each path, checks each quote VERBATIM against the operator's messages, and names what the census carries that the review ignored; a failing verdict re-runs the writer with the verdict fed back. A review that flatters the work is a failed review, and a quote the operator never wrote is the failure the verify seat exists to catch."
|
|
104
|
+
}
|