@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +12 -1
  3. package/dist/blocking-reasons.d.ts +67 -0
  4. package/dist/blocking-reasons.js +181 -0
  5. package/dist/command-evidence.d.ts +32 -0
  6. package/dist/command-evidence.js +61 -0
  7. package/dist/heartbeat.d.ts +61 -0
  8. package/dist/heartbeat.js +158 -0
  9. package/dist/hook-installer.d.ts +30 -0
  10. package/dist/hook-installer.js +52 -0
  11. package/dist/human-approval.d.ts +16 -1
  12. package/dist/human-approval.js +69 -5
  13. package/dist/recommendations.js +16 -1
  14. package/dist/risk-assess.d.ts +50 -0
  15. package/dist/risk-assess.js +273 -0
  16. package/dist/usage-import.d.ts +51 -0
  17. package/dist/usage-import.js +190 -0
  18. package/dist/usage-sidecar.d.ts +6 -0
  19. package/dist/usage-sidecar.js +14 -1
  20. package/dist/workflow.js +448 -24
  21. package/docs/README.md +2 -0
  22. package/docs/architecture.md +50 -0
  23. package/docs/backlog.md +13 -0
  24. package/docs/continuation-inventory.md +21 -0
  25. package/docs/decision-model-evaluation.md +4 -0
  26. package/docs/opportunities/OPP-001-proportional-ceremony-light-lane.md +62 -0
  27. package/docs/opportunities/OPP-002-risk-assess-command.md +70 -0
  28. package/docs/opportunities/OPP-003-adversarial-review-package.md +63 -0
  29. package/docs/opportunities/OPP-004-exact-continuation-commands.md +59 -0
  30. package/docs/opportunities/OPP-005-friction-benchmark.md +62 -0
  31. package/docs/opportunities/OPP-006-emission-usability-test-policy.md +57 -0
  32. package/docs/opportunities/OPP-007-decision-findings-triage-scope-watch.md +54 -0
  33. package/docs/opportunities/OPP-008-embedding-covering-tests-judge-context.md +52 -0
  34. package/docs/opportunities/README.md +33 -0
  35. package/docs/product-requirements.md +3 -0
  36. package/docs/product-vision.md +1 -0
  37. package/docs/quality.md +10 -0
  38. package/docs/release-process.md +9 -1
  39. package/docs/semantic-mapping.md +4 -0
  40. package/docs/token-efficiency.md +53 -0
  41. package/package.json +1 -1
  42. package/resources/package.yaml +3 -0
  43. package/resources/skills/code-review.md +2 -0
  44. package/resources/skills/review-judge-a.md +24 -0
  45. package/resources/skills/review-judge-b.md +24 -0
  46. package/resources/skills/review-refuter.md +28 -0
  47. 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
+ }
@@ -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[];
@@ -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
  }