@multileaf/ai-workflow 0.8.0 → 0.10.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/CHANGELOG.md +27 -0
- package/README.md +12 -1
- package/dist/blocking-reasons.d.ts +67 -0
- package/dist/blocking-reasons.js +181 -0
- package/dist/command-evidence.d.ts +32 -0
- package/dist/command-evidence.js +61 -0
- package/dist/heartbeat.d.ts +61 -0
- package/dist/heartbeat.js +158 -0
- package/dist/hook-installer.d.ts +30 -0
- package/dist/hook-installer.js +52 -0
- package/dist/human-approval.d.ts +16 -1
- package/dist/human-approval.js +69 -5
- package/dist/recommendations.js +16 -1
- package/dist/risk-assess.d.ts +50 -0
- package/dist/risk-assess.js +273 -0
- package/dist/usage-import.d.ts +51 -0
- package/dist/usage-import.js +190 -0
- package/dist/usage-sidecar.d.ts +6 -0
- package/dist/usage-sidecar.js +14 -1
- package/dist/workflow.js +448 -24
- package/docs/README.md +2 -0
- package/docs/architecture.md +50 -0
- package/docs/backlog.md +13 -0
- package/docs/continuation-inventory.md +21 -0
- package/docs/decision-model-evaluation.md +4 -0
- package/docs/opportunities/OPP-001-proportional-ceremony-light-lane.md +62 -0
- package/docs/opportunities/OPP-002-risk-assess-command.md +70 -0
- package/docs/opportunities/OPP-003-adversarial-review-package.md +63 -0
- package/docs/opportunities/OPP-004-exact-continuation-commands.md +59 -0
- package/docs/opportunities/OPP-005-friction-benchmark.md +62 -0
- package/docs/opportunities/OPP-006-emission-usability-test-policy.md +57 -0
- package/docs/opportunities/OPP-007-decision-findings-triage-scope-watch.md +54 -0
- package/docs/opportunities/OPP-008-embedding-covering-tests-judge-context.md +52 -0
- package/docs/opportunities/README.md +33 -0
- package/docs/product-requirements.md +3 -0
- package/docs/product-vision.md +1 -0
- package/docs/quality.md +10 -0
- package/docs/release-process.md +9 -1
- package/docs/semantic-mapping.md +4 -0
- package/docs/token-efficiency.md +53 -0
- package/package.json +1 -1
- package/resources/package.yaml +3 -0
- package/resources/skills/code-review.md +2 -0
- package/resources/skills/review-judge-a.md +24 -0
- package/resources/skills/review-judge-b.md +24 -0
- package/resources/skills/review-refuter.md +28 -0
- package/resources/skills/ticket.md +2 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { UsageSidecarEntry } from "./usage-sidecar.js";
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic importers that turn local harness session logs into
|
|
4
|
+
* measured sidecar entries (the switchboard pattern: raw logs -> canonical
|
|
5
|
+
* ledger via per-source converters; no LLM, no network, no timestamps).
|
|
6
|
+
*
|
|
7
|
+
* Formats were validated against real logs:
|
|
8
|
+
* - Claude Code: the .jsonl transcripts under ~/.claude/projects/<munged-root>
|
|
9
|
+
* carry message.usage with all four tiers; stream chunks
|
|
10
|
+
* duplicate per message.id, so entries are deduplicated by message id.
|
|
11
|
+
* - Codex: the recursive rollout-*.jsonl tree under ~/.codex/sessions — token_usage_record entries; input_tokens includes the cached subset; session_meta.cwd
|
|
12
|
+
* scopes a session to this project.
|
|
13
|
+
* - Gemini (antigravity-cli): the transcript_full.jsonl logs under the ~/.gemini/antigravity-cli/brain sessions — PLANNER_RESPONSE entries with
|
|
14
|
+
* input/cache_read/output. Sessions do not record their working
|
|
15
|
+
* directory, so Gemini imports cannot be scoped per project.
|
|
16
|
+
*
|
|
17
|
+
* - Pi (pi.dev): the session JSONL tree under ~/.pi/agent/sessions, grouped
|
|
18
|
+
* by project directory. Assistant messages carry per-message usage with
|
|
19
|
+
* all four tiers plus the provider and model, and dedicated usage entries
|
|
20
|
+
* (for example cache warming) record model-attributed consumption that
|
|
21
|
+
* never becomes a message.
|
|
22
|
+
*
|
|
23
|
+
* Import is idempotent: each event derives a stable command identity
|
|
24
|
+
* (`import.<source>.<hash>`), and events already present in the ledger
|
|
25
|
+
* are skipped, so reruns never double-count.
|
|
26
|
+
*/
|
|
27
|
+
export type ImportSource = "claude-code" | "codex" | "gemini" | "pi";
|
|
28
|
+
/** An event ready to append: no seq (assigned by the ledger). */
|
|
29
|
+
export type ImportedUsageEvent = Omit<UsageSidecarEntry, "seq">;
|
|
30
|
+
export type ImportResult = {
|
|
31
|
+
source: ImportSource;
|
|
32
|
+
files: number;
|
|
33
|
+
events: number;
|
|
34
|
+
imported: number;
|
|
35
|
+
skipped: number;
|
|
36
|
+
};
|
|
37
|
+
/** Claude Code: one JSONL transcript per session; dedupe assistant chunks by message id. */
|
|
38
|
+
export declare function parseClaudeCodeTranscript(content: string): ImportedUsageEvent[];
|
|
39
|
+
/** Codex: rollout JSONL; returns undefined when the session belongs to another project. */
|
|
40
|
+
export declare function parseCodexSession(content: string, projectRoot: string): ImportedUsageEvent[] | undefined;
|
|
41
|
+
/** Gemini (antigravity-cli): transcript_full JSONL; PLANNER_RESPONSE steps carry token counts. */
|
|
42
|
+
export declare function parseGeminiTranscript(content: string): ImportedUsageEvent[];
|
|
43
|
+
/** Pi (pi.dev): session JSONL; assistant messages and dedicated usage entries both carry per-message tiers and provider. */
|
|
44
|
+
export declare function parsePiSession(content: string): ImportedUsageEvent[];
|
|
45
|
+
/** Merge imported events into a ledger idempotently (already-present command ids are skipped). */
|
|
46
|
+
export declare function mergeImportedEvents(ledger: UsageSidecarEntry[], events: ImportedUsageEvent[]): {
|
|
47
|
+
ledger: UsageSidecarEntry[];
|
|
48
|
+
imported: number;
|
|
49
|
+
skipped: number;
|
|
50
|
+
};
|
|
51
|
+
export declare function summarizeImport(result: ImportResult): string;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const zeroTiers = () => ({
|
|
3
|
+
"cache-read": 0,
|
|
4
|
+
"fresh-input": 0,
|
|
5
|
+
"cache-write": 0,
|
|
6
|
+
output: 0,
|
|
7
|
+
});
|
|
8
|
+
const tiersOf = (values) => ({
|
|
9
|
+
...zeroTiers(),
|
|
10
|
+
...Object.fromEntries(Object.entries(values).filter(([, value]) => value > 0)),
|
|
11
|
+
});
|
|
12
|
+
function event(source, identity, tiers, extra = {}) {
|
|
13
|
+
if (!USAGE_TIERS_NONZERO(tiers))
|
|
14
|
+
return undefined;
|
|
15
|
+
const hash = createHash("sha256").update(identity).digest("hex").slice(0, 12);
|
|
16
|
+
const imported = {
|
|
17
|
+
command: `import.${source}.e${hash}`,
|
|
18
|
+
label: "measured",
|
|
19
|
+
tiers,
|
|
20
|
+
harness: source === "gemini" ? "gemini-cli" : source,
|
|
21
|
+
...(extra.model ? { model: extra.model } : {}),
|
|
22
|
+
...(extra.provider ? { provider: extra.provider } : {}),
|
|
23
|
+
...(extra.harnessVersion ? { harnessVersion: extra.harnessVersion } : {}),
|
|
24
|
+
};
|
|
25
|
+
return imported;
|
|
26
|
+
}
|
|
27
|
+
function USAGE_TIERS_NONZERO(tiers) {
|
|
28
|
+
return Object.values(tiers).some((value) => value > 0);
|
|
29
|
+
}
|
|
30
|
+
function safeParse(line) {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(line);
|
|
33
|
+
return parsed && typeof parsed === "object" ? parsed : undefined;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Claude Code: one JSONL transcript per session; dedupe assistant chunks by message id. */
|
|
40
|
+
export function parseClaudeCodeTranscript(content) {
|
|
41
|
+
const events = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
for (const line of content.split("\n")) {
|
|
44
|
+
if (!line.includes('"usage"'))
|
|
45
|
+
continue;
|
|
46
|
+
const entry = safeParse(line);
|
|
47
|
+
if (!entry || entry.type !== "assistant")
|
|
48
|
+
continue;
|
|
49
|
+
const message = entry.message;
|
|
50
|
+
const usage = message?.usage;
|
|
51
|
+
if (!message || !usage)
|
|
52
|
+
continue;
|
|
53
|
+
const messageId = String(message.id ?? "");
|
|
54
|
+
if (!messageId || seen.has(messageId))
|
|
55
|
+
continue;
|
|
56
|
+
seen.add(messageId);
|
|
57
|
+
const parsed = event("claude-code", `${entry.sessionId ?? ""}:${messageId}`, tiersOf({
|
|
58
|
+
"fresh-input": usage.input_tokens ?? 0,
|
|
59
|
+
"cache-write": usage.cache_creation_input_tokens ?? 0,
|
|
60
|
+
"cache-read": usage.cache_read_input_tokens ?? 0,
|
|
61
|
+
output: usage.output_tokens ?? 0,
|
|
62
|
+
}), {
|
|
63
|
+
model: typeof message.model === "string" ? message.model : undefined,
|
|
64
|
+
harnessVersion: typeof entry.version === "string" ? entry.version : undefined,
|
|
65
|
+
});
|
|
66
|
+
if (parsed)
|
|
67
|
+
events.push(parsed);
|
|
68
|
+
}
|
|
69
|
+
return events;
|
|
70
|
+
}
|
|
71
|
+
/** Codex: rollout JSONL; returns undefined when the session belongs to another project. */
|
|
72
|
+
export function parseCodexSession(content, projectRoot) {
|
|
73
|
+
const events = [];
|
|
74
|
+
let sessionCwd;
|
|
75
|
+
let cliVersion;
|
|
76
|
+
let model;
|
|
77
|
+
for (const line of content.split("\n")) {
|
|
78
|
+
const entry = safeParse(line);
|
|
79
|
+
if (!entry)
|
|
80
|
+
continue;
|
|
81
|
+
const payload = (entry.payload ?? {});
|
|
82
|
+
if (entry.type === "session_meta") {
|
|
83
|
+
sessionCwd = typeof payload.cwd === "string" ? payload.cwd : undefined;
|
|
84
|
+
cliVersion = typeof payload.cli_version === "string" ? payload.cli_version : undefined;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (entry.type === "turn_context" && typeof payload.model === "string")
|
|
88
|
+
model = payload.model;
|
|
89
|
+
if (entry.type !== "token_usage_record")
|
|
90
|
+
continue;
|
|
91
|
+
const usage = payload.usage;
|
|
92
|
+
if (!usage)
|
|
93
|
+
continue;
|
|
94
|
+
const cached = usage.cached_input_tokens ?? 0;
|
|
95
|
+
const cacheWrite = usage.cache_write_input_tokens ?? 0;
|
|
96
|
+
const input = usage.input_tokens ?? 0;
|
|
97
|
+
const parsed = event("codex", `${payload.response_id ?? createHash("sha256").update(line).digest("hex").slice(0, 16)}:${payload.turn_id ?? ""}:${entry.ordinal ?? ""}`, tiersOf({
|
|
98
|
+
"cache-read": cached,
|
|
99
|
+
"cache-write": cacheWrite,
|
|
100
|
+
"fresh-input": Math.max(0, input - cached - cacheWrite),
|
|
101
|
+
output: usage.output_tokens ?? 0,
|
|
102
|
+
}), { model, harnessVersion: cliVersion });
|
|
103
|
+
if (parsed)
|
|
104
|
+
events.push(parsed);
|
|
105
|
+
}
|
|
106
|
+
if (sessionCwd === undefined)
|
|
107
|
+
return undefined;
|
|
108
|
+
if (sessionCwd.replace(/\/+$/, "") !== projectRoot.replace(/\/+$/, ""))
|
|
109
|
+
return undefined;
|
|
110
|
+
return events;
|
|
111
|
+
}
|
|
112
|
+
/** Gemini (antigravity-cli): transcript_full JSONL; PLANNER_RESPONSE steps carry token counts. */
|
|
113
|
+
export function parseGeminiTranscript(content) {
|
|
114
|
+
const events = [];
|
|
115
|
+
for (const line of content.split("\n")) {
|
|
116
|
+
if (!line.includes("input_tokens"))
|
|
117
|
+
continue;
|
|
118
|
+
const entry = safeParse(line);
|
|
119
|
+
if (!entry || entry.type !== "PLANNER_RESPONSE")
|
|
120
|
+
continue;
|
|
121
|
+
const num = (value) => (typeof value === "number" ? value : 0);
|
|
122
|
+
const parsed = event("gemini", `${entry.created_at ?? ""}:${entry.step_index ?? ""}:${createHash("sha256").update(line).digest("hex").slice(0, 16)}`, tiersOf({
|
|
123
|
+
"fresh-input": num(entry.input_tokens),
|
|
124
|
+
"cache-read": num(entry.cache_read_tokens),
|
|
125
|
+
output: num(entry.output_tokens),
|
|
126
|
+
}));
|
|
127
|
+
if (parsed)
|
|
128
|
+
events.push(parsed);
|
|
129
|
+
}
|
|
130
|
+
return events;
|
|
131
|
+
}
|
|
132
|
+
/** Pi (pi.dev): session JSONL; assistant messages and dedicated usage entries both carry per-message tiers and provider. */
|
|
133
|
+
export function parsePiSession(content) {
|
|
134
|
+
const events = [];
|
|
135
|
+
for (const line of content.split("\n")) {
|
|
136
|
+
if (!line.includes('"usage"'))
|
|
137
|
+
continue;
|
|
138
|
+
const entry = safeParse(line);
|
|
139
|
+
if (!entry)
|
|
140
|
+
continue;
|
|
141
|
+
const message = entry.message;
|
|
142
|
+
const usage = (message?.usage ?? entry.usage);
|
|
143
|
+
if (!usage)
|
|
144
|
+
continue;
|
|
145
|
+
if (entry.type === "message" && message?.role !== "assistant")
|
|
146
|
+
continue;
|
|
147
|
+
const parsed = event("pi", String(entry.id ?? createHash("sha256").update(line).digest("hex").slice(0, 16)), tiersOf({
|
|
148
|
+
"fresh-input": usage.input ?? 0,
|
|
149
|
+
"cache-read": usage.cacheRead ?? 0,
|
|
150
|
+
"cache-write": usage.cacheWrite ?? 0,
|
|
151
|
+
output: usage.output ?? 0,
|
|
152
|
+
}), {
|
|
153
|
+
model: typeof message?.model === "string"
|
|
154
|
+
? message.model
|
|
155
|
+
: typeof entry.model === "string"
|
|
156
|
+
? entry.model
|
|
157
|
+
: undefined,
|
|
158
|
+
provider: typeof message?.provider === "string"
|
|
159
|
+
? message.provider
|
|
160
|
+
: typeof entry.provider === "string"
|
|
161
|
+
? entry.provider
|
|
162
|
+
: undefined,
|
|
163
|
+
});
|
|
164
|
+
if (parsed)
|
|
165
|
+
events.push(parsed);
|
|
166
|
+
}
|
|
167
|
+
return events;
|
|
168
|
+
}
|
|
169
|
+
/** Merge imported events into a ledger idempotently (already-present command ids are skipped). */
|
|
170
|
+
export function mergeImportedEvents(ledger, events) {
|
|
171
|
+
const existing = new Set(ledger.map((entry) => entry.command));
|
|
172
|
+
const fresh = events.filter((candidate) => !existing.has(candidate.command));
|
|
173
|
+
return {
|
|
174
|
+
ledger: [
|
|
175
|
+
...ledger,
|
|
176
|
+
...fresh.map((candidate, index) => ({ ...candidate, seq: ledger.length + index + 1 })),
|
|
177
|
+
],
|
|
178
|
+
imported: fresh.length,
|
|
179
|
+
skipped: events.length - fresh.length,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function summarizeImport(result) {
|
|
183
|
+
return [
|
|
184
|
+
`source: ${result.source}`,
|
|
185
|
+
`files scanned: ${result.files}`,
|
|
186
|
+
`usage events found: ${result.events}`,
|
|
187
|
+
`imported: ${result.imported} (label: measured)`,
|
|
188
|
+
`skipped (already in ledger): ${result.skipped}`,
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
package/dist/usage-sidecar.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export type UsageSidecarEntry = {
|
|
|
25
25
|
label: UsageLabel;
|
|
26
26
|
tiers: Record<UsageTier, number>;
|
|
27
27
|
model?: string;
|
|
28
|
+
provider?: string;
|
|
28
29
|
harness?: string;
|
|
29
30
|
harnessVersion?: string;
|
|
30
31
|
requirement?: string;
|
|
@@ -46,6 +47,11 @@ export type UsageSidecarSummary = {
|
|
|
46
47
|
tokens: number;
|
|
47
48
|
label: UsageLabel;
|
|
48
49
|
}[];
|
|
50
|
+
byProvider: {
|
|
51
|
+
provider: string;
|
|
52
|
+
tokens: number;
|
|
53
|
+
label: UsageLabel;
|
|
54
|
+
}[];
|
|
49
55
|
};
|
|
50
56
|
export declare function validateUsageSidecarEntry(entry: UsageSidecarEntry): UsageSidecarEntry;
|
|
51
57
|
export declare function parseUsageSidecar(content: string): UsageSidecarEntry[];
|
package/dist/usage-sidecar.js
CHANGED
|
@@ -32,6 +32,8 @@ export function validateUsageSidecarEntry(entry) {
|
|
|
32
32
|
throw new Error("Usage sidecar entry needs at least one non-zero tier.");
|
|
33
33
|
if (entry.model !== undefined && !entry.model.trim())
|
|
34
34
|
throw new Error("Usage sidecar model must be a non-empty string when present.");
|
|
35
|
+
if (entry.provider !== undefined && !entry.provider.trim())
|
|
36
|
+
throw new Error("Usage sidecar provider must be a non-empty string when present.");
|
|
35
37
|
if (entry.requirement !== undefined && !/^REQ-\d+$/.test(entry.requirement))
|
|
36
38
|
throw new Error("Usage sidecar requirement must use the REQ-### identifier format.");
|
|
37
39
|
if (entry.workflow !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(entry.workflow))
|
|
@@ -84,6 +86,7 @@ export function parseUsageSidecar(content) {
|
|
|
84
86
|
label: pick("label"),
|
|
85
87
|
tiers: Object.fromEntries(USAGE_TIERS.map((tier) => [tier, tiers[tier] ?? 0])),
|
|
86
88
|
...(optional("model") === undefined ? {} : { model: optional("model") }),
|
|
89
|
+
...(optional("provider") === undefined ? {} : { provider: optional("provider") }),
|
|
87
90
|
...(optional("harness") === undefined ? {} : { harness: optional("harness") }),
|
|
88
91
|
...(optional("harness_version") === undefined
|
|
89
92
|
? {}
|
|
@@ -100,7 +103,7 @@ export function parseUsageSidecar(content) {
|
|
|
100
103
|
export function serializeUsageSidecar(entries) {
|
|
101
104
|
const lines = [`schema: ${USAGE_SIDECAR_SCHEMA}`, "entries:"];
|
|
102
105
|
for (const entry of entries) {
|
|
103
|
-
lines.push(` - seq: ${entry.seq}`, ` command: ${entry.command}`, ` label: ${entry.label}`, " tiers:", ...USAGE_TIERS.map((tier) => ` ${tier}: ${entry.tiers[tier]}`), ...(entry.model === undefined ? [] : [` model: ${entry.model}`]), ...(entry.harness === undefined ? [] : [` harness: ${entry.harness}`]), ...(entry.harnessVersion === undefined
|
|
106
|
+
lines.push(` - seq: ${entry.seq}`, ` command: ${entry.command}`, ` label: ${entry.label}`, " tiers:", ...USAGE_TIERS.map((tier) => ` ${tier}: ${entry.tiers[tier]}`), ...(entry.model === undefined ? [] : [` model: ${entry.model}`]), ...(entry.provider === undefined ? [] : [` provider: ${entry.provider}`]), ...(entry.harness === undefined ? [] : [` harness: ${entry.harness}`]), ...(entry.harnessVersion === undefined
|
|
104
107
|
? []
|
|
105
108
|
: [` harness_version: ${entry.harnessVersion}`]), ...(entry.requirement === undefined ? [] : [` requirement: ${entry.requirement}`]), ...(entry.workflow === undefined ? [] : [` workflow: ${entry.workflow}`]));
|
|
106
109
|
}
|
|
@@ -124,6 +127,7 @@ export function summarizeUsageSidecar(entries) {
|
|
|
124
127
|
tiers: Object.fromEntries(USAGE_TIERS.map((tier) => [tier, 0])),
|
|
125
128
|
cacheHitRate: undefined,
|
|
126
129
|
byModel: [],
|
|
130
|
+
byProvider: [],
|
|
127
131
|
byHarness: [],
|
|
128
132
|
};
|
|
129
133
|
const tiers = Object.fromEntries(USAGE_TIERS.map((tier) => [
|
|
@@ -154,6 +158,9 @@ export function summarizeUsageSidecar(entries) {
|
|
|
154
158
|
tiers,
|
|
155
159
|
...(billableInput > 0 ? { cacheHitRate: tiers["cache-read"] / billableInput } : {}),
|
|
156
160
|
byModel: group("model").map(({ name, tokens, label }) => ({ model: name, tokens, label })),
|
|
161
|
+
byProvider: group("provider")
|
|
162
|
+
.filter(({ name }) => name !== "unattributed")
|
|
163
|
+
.map(({ name, tokens, label }) => ({ provider: name, tokens, label })),
|
|
157
164
|
byHarness: group("harness").map(({ name, tokens, label }) => ({
|
|
158
165
|
harness: name,
|
|
159
166
|
tokens,
|
|
@@ -183,5 +190,11 @@ export function serializeUsageSidecarSummary(summary) {
|
|
|
183
190
|
...summary.byHarness.map(({ harness, tokens, label }) => ` - ${harness}: ${tokens.toLocaleString("en-US")} (${label})`),
|
|
184
191
|
]
|
|
185
192
|
: []),
|
|
193
|
+
...(summary.byProvider.length
|
|
194
|
+
? [
|
|
195
|
+
"by provider:",
|
|
196
|
+
...summary.byProvider.map(({ provider, tokens, label }) => ` - ${provider}: ${tokens.toLocaleString("en-US")} (${label})`),
|
|
197
|
+
]
|
|
198
|
+
: []),
|
|
186
199
|
].join("\n");
|
|
187
200
|
}
|