@use-aistack/cli 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/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/version.ts
7
- var CLI_VERSION = true ? "0.8.0" : "0.0.0-dev";
7
+ var CLI_VERSION = true ? "0.10.0" : "0.0.0-dev";
8
8
 
9
9
  // src/api.ts
10
10
  var BASE_URL = process.env.AISTACK_URL || "https://aistack.to";
@@ -122,6 +122,29 @@ async function syncPublish(token, bodyJson) {
122
122
  }
123
123
  return res.json();
124
124
  }
125
+ async function fetchDayManifest(baseUrl, token) {
126
+ const res = await fetch(`${baseUrl}/api/cli/sync-manifest`, {
127
+ headers: { "Content-Type": "application/json", ...authHeaders(token) }
128
+ });
129
+ if (res.status === 404) return null;
130
+ if (res.status === 401)
131
+ throw new Error(
132
+ "Authentication expired. Run `npx @use-aistack/cli login` again."
133
+ );
134
+ if (res.status === 403 || res.status === 429)
135
+ throw failure("Manifest fetch failed", res);
136
+ if (!res.ok) {
137
+ throw new Error(await formatHttpError(res, "Manifest fetch failed"));
138
+ }
139
+ const body = await res.json();
140
+ const retentionDays = typeof body.retentionDays === "number" && body.retentionDays > 0 ? body.retentionDays : 400;
141
+ const aggregateVersion = typeof body.aggregateVersion === "string" ? body.aggregateVersion : "";
142
+ const days = Array.isArray(body.days) ? body.days.flatMap((d) => {
143
+ const row = d;
144
+ return typeof row?.date === "string" && typeof row?.fingerprint === "string" ? [{ date: row.date, fingerprint: row.fingerprint }] : [];
145
+ }) : [];
146
+ return { retentionDays, aggregateVersion, days };
147
+ }
125
148
  async function setAutoSync(token, flag) {
126
149
  const res = await request("/api/cli/auto-sync", {
127
150
  method: "POST",
@@ -1592,6 +1615,64 @@ var asName = (v) => {
1592
1615
  const s = asStr(v);
1593
1616
  return s === null ? null : cleanName(s);
1594
1617
  };
1618
+ var utcDateOf = (ms) => new Date(ms).toISOString().slice(0, 10);
1619
+ function usageDayAcc(agg, tsMs) {
1620
+ const date = utcDateOf(tsMs);
1621
+ let day = agg.usageDays.get(date);
1622
+ if (!day) {
1623
+ day = {
1624
+ models: /* @__PURE__ */ new Map(),
1625
+ subagentTokens: 0,
1626
+ syntheticTokens: 0,
1627
+ projectDirs: /* @__PURE__ */ new Set()
1628
+ };
1629
+ agg.usageDays.set(date, day);
1630
+ }
1631
+ return day;
1632
+ }
1633
+ function noteUsageResponse(agg, response, sign = 1) {
1634
+ if (response.tsMs === null) return;
1635
+ const day = usageDayAcc(agg, response.tsMs);
1636
+ let m = day.models.get(response.modelKey);
1637
+ if (!m) {
1638
+ m = {
1639
+ counts: {
1640
+ input: 0,
1641
+ output: 0,
1642
+ cacheWrite5m: 0,
1643
+ cacheWrite1h: 0,
1644
+ cacheWriteUnsplit: 0,
1645
+ cacheRead: 0
1646
+ },
1647
+ costUSD: 0,
1648
+ unpricedTokens: 0
1649
+ };
1650
+ day.models.set(response.modelKey, m);
1651
+ }
1652
+ const c = response.counts;
1653
+ m.counts.input += sign * c.input;
1654
+ m.counts.output += sign * c.output;
1655
+ m.counts.cacheWrite5m += sign * c.cacheWrite5m;
1656
+ m.counts.cacheWrite1h += sign * c.cacheWrite1h;
1657
+ m.counts.cacheWriteUnsplit += sign * c.cacheWriteUnsplit;
1658
+ m.counts.cacheRead += sign * c.cacheRead;
1659
+ if (response.costUSD === null) m.unpricedTokens += sign * countsTotal(c);
1660
+ else m.costUSD += sign * response.costUSD;
1661
+ if (response.sidechain) day.subagentTokens += sign * countsTotal(c);
1662
+ }
1663
+ function noteSyntheticTokens(agg, tsMs, tokens) {
1664
+ if (tsMs === null) return;
1665
+ usageDayAcc(agg, tsMs).syntheticTokens += tokens;
1666
+ }
1667
+ function noteSessionStart(agg, sessionId, tsMs) {
1668
+ if (tsMs === null) return;
1669
+ const held = agg.sessionStarts.get(sessionId);
1670
+ if (held === void 0 || tsMs < held) agg.sessionStarts.set(sessionId, tsMs);
1671
+ }
1672
+ function noteProjectDay(agg, directory, tsMs) {
1673
+ if (tsMs === null) return;
1674
+ usageDayAcc(agg, tsMs).projectDirs.add(directory);
1675
+ }
1595
1676
  function createAggregate() {
1596
1677
  return {
1597
1678
  files: 0,
@@ -1631,7 +1712,9 @@ function createAggregate() {
1631
1712
  textBlocks: 0,
1632
1713
  webSearchRequests: 0,
1633
1714
  webFetchRequests: 0,
1634
- seen: /* @__PURE__ */ new Map()
1715
+ seen: /* @__PURE__ */ new Map(),
1716
+ usageDays: /* @__PURE__ */ new Map(),
1717
+ sessionStarts: /* @__PURE__ */ new Map()
1635
1718
  };
1636
1719
  }
1637
1720
  var bump = (m, k, n = 1) => m.set(k, (m.get(k) ?? 0) + n);
@@ -1649,7 +1732,16 @@ function emptyUsage() {
1649
1732
  };
1650
1733
  }
1651
1734
  var countsTotal = (t) => t.input + t.output + t.cacheWrite5m + t.cacheWrite1h + t.cacheWriteUnsplit + t.cacheRead;
1652
- function addModelUsage(agg, modelKey, counts, costUSD, messages = 1) {
1735
+ function addModelUsage(agg, modelKey, counts, costUSD, messages = 1, at) {
1736
+ if (at) {
1737
+ noteUsageResponse(agg, {
1738
+ tsMs: at.tsMs,
1739
+ modelKey,
1740
+ counts,
1741
+ costUSD,
1742
+ ...at.sidechain ? { sidechain: true } : {}
1743
+ });
1744
+ }
1653
1745
  let m = agg.byModel.get(modelKey);
1654
1746
  if (!m) {
1655
1747
  m = emptyUsage();
@@ -2133,192 +2225,634 @@ async function hasRecentFile(roots, matches, sinceMs, opts = {}) {
2133
2225
  return false;
2134
2226
  }
2135
2227
 
2136
- // ../workflow-rules/src/metricRules.ts
2137
- var METRIC_RULES_V1 = "metric-rules/v1";
2228
+ // ../workflow-rules/src/daily.ts
2229
+ var WORKFLOW_AGGREGATES_V2 = "workflow-aggregates/v2";
2230
+ var LOG_BUCKETS_V1 = "log-buckets/v1";
2231
+ var EMPTY_PHASE_TOTALS = Object.freeze({
2232
+ scout: 0,
2233
+ build: 0,
2234
+ verify: 0,
2235
+ handoff: 0,
2236
+ unknown: 0
2237
+ });
2238
+ var EFFORT_LEVELS = [
2239
+ "low",
2240
+ "medium",
2241
+ "high",
2242
+ "other"
2243
+ ];
2244
+ function effortLevelOf(effort) {
2245
+ switch (effort.toLowerCase()) {
2246
+ case "low":
2247
+ case "minimal":
2248
+ return "low";
2249
+ case "medium":
2250
+ return "medium";
2251
+ case "high":
2252
+ case "xhigh":
2253
+ case "max":
2254
+ case "ultra":
2255
+ return "high";
2256
+ default:
2257
+ return "other";
2258
+ }
2259
+ }
2260
+ function logBucket(value) {
2261
+ if (!(value >= 1)) return 0;
2262
+ return Math.floor(Math.log2(value)) + 1;
2263
+ }
2264
+ function bucketRange(bucket) {
2265
+ if (bucket <= 0) return { low: 0, high: 1 };
2266
+ return { low: 2 ** (bucket - 1), high: 2 ** bucket };
2267
+ }
2268
+ function bucketMid(bucket) {
2269
+ const { low, high } = bucketRange(bucket);
2270
+ return Math.sqrt(Math.max(low, 0.25) * high);
2271
+ }
2272
+ function medianBucket(buckets) {
2273
+ const total = buckets.reduce((sum, row) => sum + row.count, 0);
2274
+ if (total <= 0) return void 0;
2275
+ const sorted = [...buckets].sort((a, b) => a.bucket - b.bucket);
2276
+ const middle = (total + 1) / 2;
2277
+ let seen = 0;
2278
+ for (const row of sorted) {
2279
+ seen += row.count;
2280
+ if (seen >= middle) return row.bucket;
2281
+ }
2282
+ return sorted[sorted.length - 1]?.bucket;
2283
+ }
2138
2284
  function median(values) {
2139
2285
  if (values.length === 0) return void 0;
2140
2286
  const sorted = [...values].sort((a, b) => a - b);
2141
2287
  const mid = Math.floor(sorted.length / 2);
2142
2288
  const midValue = sorted[mid];
2143
- if (midValue === void 0) return void 0;
2144
2289
  return sorted.length % 2 === 0 ? (sorted[mid - 1] + midValue) / 2 : midValue;
2145
2290
  }
2146
- function shareOf(sessions, predicate) {
2147
- if (!sessions || sessions.length === 0) return void 0;
2148
- const hits = sessions.filter(predicate).length;
2149
- return hits / sessions.length;
2291
+ function addPhaseTotals(into, from) {
2292
+ for (const phase of Object.keys(into)) {
2293
+ into[phase] += from[phase] ?? 0;
2294
+ }
2295
+ }
2296
+ function sumBy(rows, key, add, clone) {
2297
+ const merged = /* @__PURE__ */ new Map();
2298
+ for (const row of rows) {
2299
+ const k = key(row);
2300
+ const held = merged.get(k);
2301
+ if (held) add(held, row);
2302
+ else merged.set(k, clone(row));
2303
+ }
2304
+ return [...merged.values()];
2305
+ }
2306
+ function foldCells(rows, field) {
2307
+ return sumBy(
2308
+ rows,
2309
+ (row) => `${row.weekdayUtc}:${row.hourUtc}`,
2310
+ (into, from) => {
2311
+ into[field] += from[field];
2312
+ },
2313
+ (row) => ({ ...row })
2314
+ ).sort((a, b) => a.weekdayUtc - b.weekdayUtc || a.hourUtc - b.hourUtc);
2315
+ }
2316
+ function foldModels(rows) {
2317
+ return sumBy(
2318
+ rows,
2319
+ (row) => row.model,
2320
+ (into, from) => {
2321
+ into.tokens += from.tokens;
2322
+ },
2323
+ (row) => ({ ...row })
2324
+ ).sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
2325
+ }
2326
+ function foldLengths(rows) {
2327
+ return sumBy(
2328
+ rows,
2329
+ (row) => String(row.bucket),
2330
+ (into, from) => {
2331
+ into.sessions += from.sessions;
2332
+ addPhaseTotals(into.phaseSec, from.phaseSec);
2333
+ into.merged += from.merged;
2334
+ into.verified += from.verified;
2335
+ into.mergedVerified += from.mergedVerified;
2336
+ into.openedWithScout += from.openedWithScout;
2337
+ },
2338
+ (row) => ({ ...row, phaseSec: { ...row.phaseSec } })
2339
+ ).sort((a, b) => a.bucket - b.bucket);
2340
+ }
2341
+ function foldHarnessDays(days) {
2342
+ const first = days[0];
2343
+ if (!first) throw new Error("foldHarnessDays needs at least one day");
2344
+ const versions = (values) => [...new Set(values)].sort().join(" \xB7 ");
2345
+ const out = {
2346
+ harness: first.harness,
2347
+ sessions: days.reduce((sum, day) => sum + day.sessions, 0),
2348
+ startHours: sumBy(
2349
+ days.flatMap((day) => day.startHours),
2350
+ (row) => String(row.hourUtc),
2351
+ (into, from) => {
2352
+ into.sessions += from.sessions;
2353
+ },
2354
+ (row) => ({ ...row })
2355
+ ).sort((a, b) => a.hourUtc - b.hourUtc),
2356
+ activity: foldCells(
2357
+ days.flatMap((day) => day.activity),
2358
+ "events"
2359
+ )
2360
+ };
2361
+ const phases = days.flatMap((day) => day.phase ? [day.phase] : []);
2362
+ if (phases.length > 0) {
2363
+ const phaseSec = { ...EMPTY_PHASE_TOTALS };
2364
+ const phaseEvents = { ...EMPTY_PHASE_TOTALS };
2365
+ for (const phase of phases) {
2366
+ addPhaseTotals(phaseSec, phase.phaseSec);
2367
+ addPhaseTotals(phaseEvents, phase.phaseEvents);
2368
+ }
2369
+ out.phase = {
2370
+ ruleVersion: versions(phases.map((phase) => phase.ruleVersion)),
2371
+ sessions: phases.reduce((sum, phase) => sum + phase.sessions, 0),
2372
+ phaseSec,
2373
+ phaseEvents,
2374
+ waitingSec: phases.reduce((sum, phase) => sum + phase.waitingSec, 0),
2375
+ idleSec: phases.reduce((sum, phase) => sum + phase.idleSec, 0),
2376
+ sessionsWithVerify: phases.reduce(
2377
+ (sum, phase) => sum + phase.sessionsWithVerify,
2378
+ 0
2379
+ ),
2380
+ sessionsWithHandoff: phases.reduce(
2381
+ (sum, phase) => sum + phase.sessionsWithHandoff,
2382
+ 0
2383
+ ),
2384
+ bucketRuleVersion: versions(
2385
+ phases.map((phase) => phase.bucketRuleVersion)
2386
+ ),
2387
+ lengths: foldLengths(phases.flatMap((phase) => phase.lengths))
2388
+ };
2389
+ }
2390
+ const routings = days.flatMap((day) => day.routing ? [day.routing] : []);
2391
+ if (routings.length > 0) {
2392
+ out.routing = {
2393
+ main: foldModels(routings.flatMap((routing) => routing.main)),
2394
+ subagents: foldModels(routings.flatMap((routing) => routing.subagents))
2395
+ };
2396
+ }
2397
+ const delegations = days.flatMap(
2398
+ (day) => day.delegation ? [day.delegation] : []
2399
+ );
2400
+ if (delegations.length > 0) {
2401
+ out.delegation = {
2402
+ mainToolCalls: delegations.reduce((sum, d) => sum + d.mainToolCalls, 0),
2403
+ subagentToolCalls: delegations.reduce(
2404
+ (sum, d) => sum + d.subagentToolCalls,
2405
+ 0
2406
+ ),
2407
+ widestFanOut: Math.max(...delegations.map((d) => d.widestFanOut)),
2408
+ mostSubagents: Math.max(...delegations.map((d) => d.mostSubagents))
2409
+ };
2410
+ }
2411
+ const efforts = days.flatMap((day) => day.effort ?? []);
2412
+ if (days.some((day) => day.effort)) {
2413
+ out.effort = sumBy(
2414
+ efforts,
2415
+ (row) => row.level,
2416
+ (into, from) => {
2417
+ into.turns += from.turns;
2418
+ },
2419
+ (row) => ({ ...row })
2420
+ ).sort(
2421
+ (a, b) => EFFORT_LEVELS.indexOf(a.level) - EFFORT_LEVELS.indexOf(b.level)
2422
+ );
2423
+ }
2424
+ const thinkings = days.flatMap((day) => day.thinking ? [day.thinking] : []);
2425
+ if (thinkings.length > 0) {
2426
+ out.thinking = {
2427
+ thinkingTokens: thinkings.reduce((sum, t) => sum + t.thinkingTokens, 0),
2428
+ responseTokens: thinkings.reduce((sum, t) => sum + t.responseTokens, 0)
2429
+ };
2430
+ }
2431
+ const durations = days.flatMap(
2432
+ (day) => day.turnDurations ? [day.turnDurations] : []
2433
+ );
2434
+ if (durations.length > 0) {
2435
+ out.turnDurations = {
2436
+ bucketRuleVersion: versions(durations.map((d) => d.bucketRuleVersion)),
2437
+ buckets: sumBy(
2438
+ durations.flatMap((d) => d.buckets),
2439
+ (row) => String(row.bucket),
2440
+ (into, from) => {
2441
+ into.turns += from.turns;
2442
+ },
2443
+ (row) => ({ ...row })
2444
+ ).sort((a, b) => a.bucket - b.bucket)
2445
+ };
2446
+ }
2447
+ const questions = days.flatMap(
2448
+ (day) => day.questions ? [day.questions] : []
2449
+ );
2450
+ if (questions.length > 0) {
2451
+ out.questions = {
2452
+ asked: questions.reduce((sum, q) => sum + q.asked, 0),
2453
+ turns: questions.reduce((sum, q) => sum + q.turns, 0)
2454
+ };
2455
+ }
2456
+ if (days.some((day) => day.webSearches !== void 0)) {
2457
+ out.webSearches = days.reduce(
2458
+ (sum, day) => sum + (day.webSearches ?? 0),
2459
+ 0
2460
+ );
2461
+ }
2462
+ return out;
2463
+ }
2464
+ function foldGitDays(days) {
2465
+ const versions = (values) => [...new Set(values)].sort().join(" \xB7 ");
2466
+ return {
2467
+ testFileRuleVersion: versions(days.map((d) => d.testFileRuleVersion)),
2468
+ fileTypeRuleVersion: versions(days.map((d) => d.fileTypeRuleVersion)),
2469
+ commitSetRuleVersion: versions(days.map((d) => d.commitSetRuleVersion)),
2470
+ commits: days.reduce((sum, d) => sum + d.commits, 0),
2471
+ lateNightCommits: days.reduce((sum, d) => sum + d.lateNightCommits, 0),
2472
+ additions: days.reduce((sum, d) => sum + d.additions, 0),
2473
+ removals: days.reduce((sum, d) => sum + d.removals, 0),
2474
+ changedLinesPerCommit: days.flatMap((d) => [...d.changedLinesPerCommit]),
2475
+ testFileCommits: days.reduce((sum, d) => sum + d.testFileCommits, 0),
2476
+ changedLinesByExtension: sumBy(
2477
+ days.flatMap((d) => d.changedLinesByExtension),
2478
+ (row) => row.extension,
2479
+ (into, from) => {
2480
+ into.changedLines += from.changedLines;
2481
+ },
2482
+ (row) => ({ ...row })
2483
+ ).sort((a, b) => a.extension.localeCompare(b.extension)),
2484
+ withheldExtensionLines: days.reduce(
2485
+ (sum, d) => sum + d.withheldExtensionLines,
2486
+ 0
2487
+ ),
2488
+ weekdayHourCells: foldCells(
2489
+ days.flatMap((d) => d.weekdayHourCells),
2490
+ "commits"
2491
+ )
2492
+ };
2493
+ }
2494
+ function foldWorkflowDays(days, options) {
2495
+ if (days.length === 0) return void 0;
2496
+ const byHarness = /* @__PURE__ */ new Map();
2497
+ for (const day of days) {
2498
+ for (const harness of day.harnesses) {
2499
+ const held = byHarness.get(harness.harness) ?? [];
2500
+ held.push(harness);
2501
+ byHarness.set(harness.harness, held);
2502
+ }
2503
+ }
2504
+ const parallelProjectDays = days.flatMap(
2505
+ (day) => day.parallelProjects === void 0 ? [] : [day.parallelProjects]
2506
+ );
2507
+ const webSearchDays = days.filter(
2508
+ (day) => day.harnesses.some((harness) => harness.webSearches !== void 0)
2509
+ ).length;
2510
+ return {
2511
+ aggregateVersion: options.aggregateVersion,
2512
+ ...options.utcOffsetMinutes === void 0 ? {} : { utcOffsetMinutes: options.utcOffsetMinutes },
2513
+ dates: [...new Set(days.map((day) => day.date))].sort(),
2514
+ harnesses: [...byHarness.values()].map(foldHarnessDays).sort((a, b) => a.harness.localeCompare(b.harness)),
2515
+ git: foldGitDays(days.map((day) => day.git)),
2516
+ ...parallelProjectDays.length === 0 ? {} : { parallelProjects: Math.max(...parallelProjectDays) },
2517
+ parallelProjectDays,
2518
+ gitDays: [...days].sort((a, b) => a.date.localeCompare(b.date)).map((day) => ({
2519
+ date: day.date,
2520
+ additions: day.git.additions,
2521
+ removals: day.git.removals,
2522
+ commits: day.git.commits
2523
+ })),
2524
+ webSearchDays
2525
+ };
2526
+ }
2527
+
2528
+ // ../workflow-rules/src/reading.ts
2529
+ function playbookHarnesses(reading) {
2530
+ return reading.harnesses.filter((harness) => harness.phase !== void 0);
2531
+ }
2532
+ function startHoursUtc(reading) {
2533
+ const counts = /* @__PURE__ */ new Map();
2534
+ for (const harness of reading.harnesses) {
2535
+ for (const cell of harness.startHours) {
2536
+ counts.set(cell.hourUtc, (counts.get(cell.hourUtc) ?? 0) + cell.sessions);
2537
+ }
2538
+ }
2539
+ return counts;
2540
+ }
2541
+ function ownerLocalHour(hourUtc, offsetMinutes) {
2542
+ return Math.floor(((hourUtc * 60 + offsetMinutes) / 60 % 24 + 24) % 24);
2543
+ }
2544
+ function modalStartHour(reading) {
2545
+ const offsetMinutes = reading.utcOffsetMinutes;
2546
+ if (offsetMinutes === void 0) return void 0;
2547
+ const counts = /* @__PURE__ */ new Map();
2548
+ for (const [hourUtc, sessions] of startHoursUtc(reading)) {
2549
+ const hour = ownerLocalHour(hourUtc, offsetMinutes);
2550
+ counts.set(hour, (counts.get(hour) ?? 0) + sessions);
2551
+ }
2552
+ if (counts.size === 0) return void 0;
2553
+ return [...counts.entries()].sort(
2554
+ (a, b) => b[1] - a[1] || a[0] - b[0]
2555
+ )[0]?.[0];
2150
2556
  }
2557
+
2558
+ // ../workflow-rules/src/componentRules.ts
2559
+ var COMPONENT_RULES_V2 = "component-rules/v2";
2560
+ var gitCoverage = () => 1;
2561
+ function harnessShare(input, counts) {
2562
+ const synced = input.reading.harnesses.length;
2563
+ if (synced === 0) return 0;
2564
+ return counts(input) / synced;
2565
+ }
2566
+ function topShare(entries) {
2567
+ const total = entries.reduce((sum, entry) => sum + entry.value, 0);
2568
+ if (total <= 0) return void 0;
2569
+ const top = Math.max(...entries.map((entry) => entry.value));
2570
+ return top / total;
2571
+ }
2572
+ var COMPONENT_RULES = [
2573
+ {
2574
+ id: "activity-heatmap",
2575
+ version: COMPONENT_RULES_V2,
2576
+ label: "of events fall in the three busiest hours of the day",
2577
+ unit: "share",
2578
+ // Three of twenty-four hours is an eighth of the clock. A day spread evenly
2579
+ // lands near it; a night owl runs far above it.
2580
+ band: { low: 0.125, high: 0.35 },
2581
+ evaluate: ({ reading }) => {
2582
+ const byHour = /* @__PURE__ */ new Map();
2583
+ for (const harness of reading.harnesses) {
2584
+ for (const cell of harness.activity) {
2585
+ byHour.set(
2586
+ cell.hourUtc,
2587
+ (byHour.get(cell.hourUtc) ?? 0) + cell.events
2588
+ );
2589
+ }
2590
+ }
2591
+ const total = [...byHour.values()].reduce((sum, n) => sum + n, 0);
2592
+ if (total <= 0) return void 0;
2593
+ const busiest = [...byHour.values()].sort((a, b) => b - a).slice(0, 3);
2594
+ return busiest.reduce((sum, n) => sum + n, 0) / total;
2595
+ },
2596
+ coverage: (input) => harnessShare(
2597
+ input,
2598
+ ({ reading }) => reading.harnesses.filter((harness) => harness.activity.length > 0).length
2599
+ )
2600
+ },
2601
+ {
2602
+ id: "start-hours",
2603
+ version: COMPONENT_RULES_V2,
2604
+ label: "is the most common start hour",
2605
+ unit: "hour",
2606
+ // A band on a clock face means little; the row is never ranked, and the
2607
+ // figure is a position rather than a size. Kept for shape.
2608
+ band: { low: 9, high: 18 },
2609
+ evaluate: ({ reading }) => modalStartHour(reading),
2610
+ coverage: (input) => harnessShare(
2611
+ input,
2612
+ ({ reading }) => reading.harnesses.filter((harness) => harness.startHours.length > 0).length
2613
+ )
2614
+ },
2615
+ {
2616
+ id: "phase-playbook",
2617
+ version: COMPONENT_RULES_V2,
2618
+ label: "median measured session",
2619
+ unit: "minutes",
2620
+ band: { low: 10, high: 60 },
2621
+ evaluate: ({ reading }) => {
2622
+ const bucket = medianBucket(
2623
+ playbookHarnesses(reading).flatMap(
2624
+ (harness) => (harness.phase?.lengths ?? []).map((row) => ({
2625
+ bucket: row.bucket,
2626
+ count: row.sessions
2627
+ }))
2628
+ )
2629
+ );
2630
+ return bucket === void 0 ? void 0 : bucketMid(bucket);
2631
+ },
2632
+ coverage: (input) => harnessShare(input, ({ reading }) => playbookHarnesses(reading).length)
2633
+ },
2634
+ {
2635
+ id: "git-ledger",
2636
+ version: COMPONENT_RULES_V2,
2637
+ label: "of changed lines are removals",
2638
+ unit: "share",
2639
+ // Most work adds more than it takes away. A ledger that removes as much as
2640
+ // it adds is the surprising one, and so is one that never removes.
2641
+ band: { low: 0.15, high: 0.35 },
2642
+ evaluate: ({ reading }) => {
2643
+ const changed = reading.git.additions + reading.git.removals;
2644
+ return changed > 0 ? reading.git.removals / changed : void 0;
2645
+ },
2646
+ coverage: gitCoverage
2647
+ },
2648
+ {
2649
+ id: "coding-languages",
2650
+ version: COMPONENT_RULES_V2,
2651
+ label: "of changed lines are one file type",
2652
+ unit: "share",
2653
+ band: { low: 0.35, high: 0.7 },
2654
+ evaluate: ({ reading }) => {
2655
+ const named = reading.git.changedLinesByExtension.map((row) => ({
2656
+ value: row.changedLines
2657
+ }));
2658
+ const total = named.reduce((sum, row) => sum + row.value, 0) + reading.git.withheldExtensionLines;
2659
+ if (total <= 0 || named.length === 0) return void 0;
2660
+ return Math.max(...named.map((row) => row.value)) / total;
2661
+ },
2662
+ coverage: gitCoverage
2663
+ },
2664
+ {
2665
+ id: "kit",
2666
+ version: COMPONENT_RULES_V2,
2667
+ label: "of skill and MCP calls go to one artifact",
2668
+ unit: "share",
2669
+ band: { low: 0.15, high: 0.4 },
2670
+ evaluate: ({ kit }) => {
2671
+ if (!kit) return void 0;
2672
+ const byName = /* @__PURE__ */ new Map();
2673
+ for (const harness of kit) {
2674
+ for (const atom of [...harness.skills, ...harness.mcpServers]) {
2675
+ byName.set(atom.name, (byName.get(atom.name) ?? 0) + atom.callShare);
2676
+ }
2677
+ }
2678
+ return topShare([...byName.values()].map((share) => ({ value: share })));
2679
+ },
2680
+ coverage: (input) => harnessShare(
2681
+ input,
2682
+ ({ kit }) => (kit ?? []).filter(
2683
+ (harness) => harness.skills.length > 0 || harness.mcpServers.length > 0
2684
+ ).length
2685
+ )
2686
+ },
2687
+ {
2688
+ id: "model-routing",
2689
+ version: COMPONENT_RULES_V2,
2690
+ label: "of main-loop tokens run on one model",
2691
+ unit: "share",
2692
+ band: { low: 0.4, high: 0.85 },
2693
+ evaluate: ({ reading }) => {
2694
+ const main = reading.harnesses.flatMap((harness) => [
2695
+ ...harness.routing?.main ?? []
2696
+ ]);
2697
+ const byModel = /* @__PURE__ */ new Map();
2698
+ for (const row of main) {
2699
+ byModel.set(row.model, (byModel.get(row.model) ?? 0) + row.tokens);
2700
+ }
2701
+ return topShare(
2702
+ [...byModel.values()].map((tokens) => ({ value: tokens }))
2703
+ );
2704
+ },
2705
+ coverage: (input) => harnessShare(
2706
+ input,
2707
+ ({ reading }) => reading.harnesses.filter((harness) => harness.routing).length
2708
+ )
2709
+ },
2710
+ {
2711
+ id: "delegation",
2712
+ version: COMPONENT_RULES_V2,
2713
+ label: "of tool calls run inside a subagent",
2714
+ unit: "share",
2715
+ band: { low: 0, high: 0.3 },
2716
+ evaluate: ({ reading }) => {
2717
+ let main = 0;
2718
+ let subagents = 0;
2719
+ for (const harness of reading.harnesses) {
2720
+ main += harness.delegation?.mainToolCalls ?? 0;
2721
+ subagents += harness.delegation?.subagentToolCalls ?? 0;
2722
+ }
2723
+ const total = main + subagents;
2724
+ return total > 0 ? subagents / total : void 0;
2725
+ },
2726
+ coverage: (input) => harnessShare(
2727
+ input,
2728
+ ({ reading }) => reading.harnesses.filter((harness) => harness.delegation).length
2729
+ )
2730
+ }
2731
+ ];
2732
+
2733
+ // ../workflow-rules/src/metricRules.ts
2734
+ var METRIC_RULES_V2 = "metric-rules/v2";
2151
2735
  var METRIC_RULES = [
2152
2736
  {
2153
2737
  id: "late-night-commits",
2154
- version: METRIC_RULES_V1,
2738
+ version: METRIC_RULES_V2,
2155
2739
  label: "of commits land between 23:00 and 03:00",
2156
2740
  kind: "exact",
2157
2741
  unit: "share",
2158
- harnessSupport: "all",
2159
- // Most commit activity clusters in daytime hours; a wide late-night
2160
- // share is the surprising case this metric exists to surface.
2742
+ counts: "all",
2161
2743
  band: { low: 0, high: 0.15 },
2162
- evaluate: (facts) => {
2163
- const git = facts.git;
2164
- if (!git || git.totalCommits === 0) return void 0;
2165
- return git.lateNightCommits / git.totalCommits;
2744
+ evaluate: (reading) => {
2745
+ const git = reading.git;
2746
+ if (git.commits === 0) return void 0;
2747
+ return git.lateNightCommits / git.commits;
2166
2748
  }
2167
2749
  },
2168
2750
  {
2169
2751
  id: "parallel-projects",
2170
- version: METRIC_RULES_V1,
2752
+ version: METRIC_RULES_V2,
2171
2753
  label: "projects run in parallel on a median active day",
2172
2754
  kind: "proxy",
2173
2755
  unit: "count",
2174
- harnessSupport: "all",
2756
+ counts: "all",
2175
2757
  band: { low: 1, high: 1.5 },
2176
- evaluate: (facts) => {
2177
- const days = facts.activeDays;
2178
- if (!days || days.length === 0) return void 0;
2179
- return median(days.map((d) => d.parallelProjectCount));
2180
- }
2181
- },
2182
- {
2183
- id: "model-switches-mid-run",
2184
- version: METRIC_RULES_V1,
2185
- label: "of sessions switch model mid-run",
2186
- kind: "exact",
2187
- unit: "share",
2188
- harnessSupport: "all",
2189
- band: { low: 0, high: 0.1 },
2190
- evaluate: (facts) => shareOf(
2191
- facts.sessions?.filter(
2192
- (session) => session.modelSwitched !== void 0
2193
- ),
2194
- (session) => session.modelSwitched === true
2195
- )
2758
+ evaluate: (reading) => median(reading.parallelProjectDays)
2196
2759
  },
2197
2760
  {
2198
2761
  id: "thinking-share",
2199
- version: METRIC_RULES_V1,
2762
+ version: METRIC_RULES_V2,
2200
2763
  label: "of response tokens are thinking",
2201
2764
  kind: "proxy",
2202
2765
  unit: "share",
2203
- harnessSupport: ["codex", "opencode", "pi-mono"],
2766
+ counts: (harness) => harness.thinking !== void 0,
2204
2767
  band: { low: 0.1, high: 0.3 },
2205
- evaluate: (facts) => {
2206
- const sessions = facts.sessions?.filter(
2207
- (session) => session.harness !== "claude-code" && session.thinkingTokens !== void 0 && session.responseTokens !== void 0
2208
- );
2209
- if (!sessions || sessions.length === 0) return void 0;
2768
+ evaluate: (reading) => {
2210
2769
  let thinking = 0;
2211
2770
  let response = 0;
2212
- for (const s of sessions) {
2213
- thinking += s.thinkingTokens ?? 0;
2214
- response += s.responseTokens ?? 0;
2771
+ for (const harness of reading.harnesses) {
2772
+ thinking += harness.thinking?.thinkingTokens ?? 0;
2773
+ response += harness.thinking?.responseTokens ?? 0;
2215
2774
  }
2216
2775
  return response > 0 ? thinking / response : void 0;
2217
2776
  }
2218
2777
  },
2219
2778
  {
2220
- id: "high-effort-turns",
2221
- version: METRIC_RULES_V1,
2779
+ id: "effort-levels",
2780
+ version: METRIC_RULES_V2,
2222
2781
  label: "of turns run at high effort",
2223
2782
  kind: "exact",
2224
2783
  unit: "share",
2225
- // Only Claude Code and Codex record an effort field (#196 research).
2226
- harnessSupport: ["claude-code", "codex"],
2784
+ counts: (harness) => harness.effort !== void 0,
2227
2785
  band: { low: 0.2, high: 0.5 },
2228
- evaluate: (facts) => {
2229
- const sessions = facts.sessions?.filter((s) => s.effortTurns);
2230
- if (!sessions || sessions.length === 0) return void 0;
2786
+ evaluate: (reading) => {
2231
2787
  let high = 0;
2232
2788
  let total = 0;
2233
- for (const s of sessions) {
2234
- high += s.effortTurns?.high ?? 0;
2235
- total += s.effortTurns?.total ?? 0;
2789
+ for (const harness of reading.harnesses) {
2790
+ for (const row of harness.effort ?? []) {
2791
+ total += row.turns;
2792
+ if (row.level === "high") high += row.turns;
2793
+ }
2236
2794
  }
2237
2795
  return total > 0 ? high / total : void 0;
2238
2796
  }
2239
2797
  },
2240
2798
  {
2241
- id: "effort-changes-mid-run",
2242
- version: METRIC_RULES_V1,
2243
- label: "of sessions change effort mid-run",
2244
- kind: "exact",
2245
- unit: "share",
2246
- harnessSupport: ["claude-code", "codex"],
2247
- band: { low: 0, high: 0.1 },
2248
- evaluate: (facts) => shareOf(
2249
- facts.sessions?.filter((s) => s.effortTurns),
2250
- (s) => s.effortChangedMidRun === true
2251
- )
2252
- },
2253
- {
2254
- id: "longest-turn-duration",
2255
- version: METRIC_RULES_V1,
2256
- label: "longest recorded turn duration",
2799
+ id: "turn-duration",
2800
+ version: METRIC_RULES_V2,
2801
+ label: "median turn duration",
2257
2802
  kind: "exact",
2258
2803
  unit: "minutes",
2259
- // Only Claude Code and opencode record per-turn duration (#196 research).
2260
- harnessSupport: ["claude-code", "opencode"],
2261
- band: { low: 5, high: 15 },
2262
- evaluate: (facts) => {
2263
- const durations = (facts.sessions ?? []).map((s) => s.longestTurnDurationSec).filter((v) => v !== void 0);
2264
- if (durations.length === 0) return void 0;
2265
- return Math.max(...durations) / 60;
2804
+ counts: (harness) => harness.turnDurations !== void 0,
2805
+ band: { low: 0.25, high: 2 },
2806
+ evaluate: (reading) => {
2807
+ const bucket = medianBucket(
2808
+ reading.harnesses.flatMap(
2809
+ (harness) => (harness.turnDurations?.buckets ?? []).map((row) => ({
2810
+ bucket: row.bucket,
2811
+ count: row.turns
2812
+ }))
2813
+ )
2814
+ );
2815
+ return bucket === void 0 ? void 0 : bucketMid(bucket) / 60;
2266
2816
  }
2267
2817
  },
2268
2818
  {
2269
2819
  id: "question-back-share",
2270
- version: METRIC_RULES_V1,
2820
+ version: METRIC_RULES_V2,
2271
2821
  label: "of turns end with a question back to the human",
2272
2822
  kind: "proxy",
2273
2823
  unit: "share",
2274
- harnessSupport: ["claude-code", "codex", "opencode"],
2824
+ counts: (harness) => harness.questions !== void 0,
2275
2825
  band: { low: 0, high: 0.15 },
2276
- evaluate: (facts) => {
2277
- const sessions = facts.sessions?.filter(
2278
- (session) => session.questionBackTurns !== void 0 && session.totalTurns !== void 0
2279
- );
2280
- if (!sessions || sessions.length === 0) return void 0;
2826
+ evaluate: (reading) => {
2281
2827
  let asked = 0;
2282
2828
  let turns = 0;
2283
- for (const s of sessions) {
2284
- asked += s.questionBackTurns ?? 0;
2285
- turns += s.totalTurns ?? 0;
2829
+ for (const harness of reading.harnesses) {
2830
+ asked += harness.questions?.asked ?? 0;
2831
+ turns += harness.questions?.turns ?? 0;
2286
2832
  }
2287
2833
  return turns > 0 ? asked / turns : void 0;
2288
2834
  }
2289
2835
  },
2290
2836
  {
2291
2837
  id: "web-searches-per-active-day",
2292
- version: METRIC_RULES_V1,
2838
+ version: METRIC_RULES_V2,
2293
2839
  label: "web searches per active day, inside the harness",
2294
2840
  kind: "proxy",
2295
2841
  unit: "count",
2296
- harnessSupport: ["claude-code", "codex", "opencode"],
2842
+ counts: (harness) => harness.webSearches !== void 0,
2297
2843
  band: { low: 0, high: 4 },
2298
- evaluate: (facts) => {
2299
- const days = facts.activeDays?.filter(
2300
- (day) => day.webSearches !== void 0
2844
+ evaluate: (reading) => {
2845
+ if (reading.webSearchDays === 0) return void 0;
2846
+ const total = reading.harnesses.reduce(
2847
+ (sum, harness) => sum + (harness.webSearches ?? 0),
2848
+ 0
2301
2849
  );
2302
- if (!days || days.length === 0) return void 0;
2303
- const total = days.reduce((sum, d) => sum + (d.webSearches ?? 0), 0);
2304
- return total / days.length;
2850
+ return total / reading.webSearchDays;
2305
2851
  }
2306
2852
  }
2307
2853
  ];
2308
2854
 
2309
2855
  // ../workflow-rules/src/types.ts
2310
- function harnessLabel(name) {
2311
- switch (name) {
2312
- case "claude-code":
2313
- return "Claude Code";
2314
- case "codex":
2315
- return "Codex";
2316
- case "opencode":
2317
- return "opencode";
2318
- case "pi-mono":
2319
- return "Pi";
2320
- }
2321
- }
2322
2856
  var PHASES = [
2323
2857
  "scout",
2324
2858
  "build",
@@ -2327,39 +2861,6 @@ var PHASES = [
2327
2861
  "unknown"
2328
2862
  ];
2329
2863
 
2330
- // ../workflow-rules/src/fit.ts
2331
- function coverageFor(harnessSupport, syncedHarnesses) {
2332
- if (harnessSupport === "all") return 1;
2333
- if (syncedHarnesses.length === 0) return 0;
2334
- const supported = new Set(harnessSupport);
2335
- const counted = syncedHarnesses.filter((h) => supported.has(h));
2336
- return counted.length / syncedHarnesses.length;
2337
- }
2338
- function coverageTag(harnessSupport, syncedHarnesses) {
2339
- if (harnessSupport === "all") return void 0;
2340
- const supported = new Set(harnessSupport);
2341
- const counted = syncedHarnesses.filter((h) => supported.has(h));
2342
- if (counted.length === 0 || counted.length === syncedHarnesses.length)
2343
- return void 0;
2344
- return `counts: ${counted.map(harnessLabel).join(" \xB7 ")}`;
2345
- }
2346
- function buildFitInputs(facts, syncedHarnesses) {
2347
- const rows = [];
2348
- for (const rule of METRIC_RULES) {
2349
- const value = rule.evaluate(facts);
2350
- if (value === void 0) continue;
2351
- rows.push({
2352
- metricId: rule.id,
2353
- ruleVersion: rule.version,
2354
- value,
2355
- band: rule.band,
2356
- coverage: coverageFor(rule.harnessSupport, syncedHarnesses),
2357
- coverageTag: coverageTag(rule.harnessSupport, syncedHarnesses)
2358
- });
2359
- }
2360
- return rows;
2361
- }
2362
-
2363
2864
  // ../workflow-rules/src/phaseRules.ts
2364
2865
  var PHASE_RULES_V1 = "phase-rules/v1";
2365
2866
  var UNKNOWN_GATE = 0.2;
@@ -2778,8 +3279,81 @@ function deriveSessionPhases(events, ruleSet = PHASE_RULES_V1, harness) {
2778
3279
  };
2779
3280
  }
2780
3281
 
2781
- // ../workflow-rules/src/playbook.ts
2782
- var MIN_PER_SEC = 1 / 60;
3282
+ // ../workflow-rules/src/usage.ts
3283
+ var MEASURED_DAYS_V1 = "measured-days/v1";
3284
+ function canonicalJson(value) {
3285
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
3286
+ if (value && typeof value === "object") {
3287
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
3288
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
3289
+ }
3290
+ return JSON.stringify(value) ?? "null";
3291
+ }
3292
+ function fnv1a64(text) {
3293
+ const prime = 0x100000001b3n;
3294
+ const mask = 0xffffffffffffffffn;
3295
+ let hash = 0xcbf29ce484222325n;
3296
+ const bytes = new TextEncoder().encode(text);
3297
+ for (const byte of bytes) {
3298
+ hash ^= BigInt(byte);
3299
+ hash = hash * prime & mask;
3300
+ }
3301
+ return hash.toString(16).padStart(16, "0");
3302
+ }
3303
+ function dayFingerprint(day) {
3304
+ return fnv1a64(
3305
+ canonicalJson({
3306
+ version: MEASURED_DAYS_V1,
3307
+ date: day.date,
3308
+ usage: day.usage,
3309
+ workflow: day.workflow
3310
+ })
3311
+ );
3312
+ }
3313
+
3314
+ // ../workflow-rules/src/workflowRows.ts
3315
+ function metricRowId(metricId) {
3316
+ return `metric:${metricId}`;
3317
+ }
3318
+ function componentRowId(componentId) {
3319
+ return `component:${componentId}`;
3320
+ }
3321
+ var WORKFLOW_ROW_ORDER = [
3322
+ {
3323
+ rowId: "component:activity-heatmap",
3324
+ name: "When work happens",
3325
+ flat: false
3326
+ },
3327
+ { rowId: "component:start-hours", name: "Session start times", flat: false },
3328
+ {
3329
+ rowId: "metric:late-night-commits",
3330
+ name: "Late-night commits",
3331
+ flat: true
3332
+ },
3333
+ { rowId: "component:phase-playbook", name: "Session length", flat: false },
3334
+ { rowId: "component:git-ledger", name: "Lines changed", flat: false },
3335
+ { rowId: "component:coding-languages", name: "Languages", flat: false },
3336
+ { rowId: "component:kit", name: "Skills and MCP", flat: false },
3337
+ { rowId: "component:model-routing", name: "Models used", flat: false },
3338
+ { rowId: "component:delegation", name: "Subagents", flat: false },
3339
+ { rowId: "metric:effort-levels", name: "Effort levels", flat: false },
3340
+ { rowId: "metric:thinking-share", name: "Thinking tokens", flat: false },
3341
+ { rowId: "metric:turn-duration", name: "Turn length", flat: false },
3342
+ { rowId: "metric:question-back-share", name: "Questions asked", flat: true },
3343
+ {
3344
+ rowId: "metric:web-searches-per-active-day",
3345
+ name: "Web searches",
3346
+ flat: true
3347
+ },
3348
+ { rowId: "metric:parallel-projects", name: "Parallel projects", flat: true }
3349
+ ];
3350
+ var ORDER_INDEX = new Map(
3351
+ WORKFLOW_ROW_ORDER.map((row, index) => [row.rowId, index])
3352
+ );
3353
+ var KNOWN_ROW_IDS = /* @__PURE__ */ new Set([
3354
+ ...METRIC_RULES.map((rule) => metricRowId(rule.id)),
3355
+ ...COMPONENT_RULES.map((rule) => componentRowId(rule.id))
3356
+ ]);
2783
3357
 
2784
3358
  // src/harness/shared/window.ts
2785
3359
  var DEFAULT_WINDOW_DAYS = 30;
@@ -3037,23 +3611,23 @@ function buildPayload(input) {
3037
3611
  }
3038
3612
  };
3039
3613
  }
3040
- function toPayloadWorkflow(extraction) {
3041
- return {
3042
- aggregateVersion: extraction.aggregateVersion,
3043
- harnesses: extraction.harnesses.map(
3044
- ({ facts: _local, aggregateVersion: _stamped, ...rest }) => rest
3045
- ),
3046
- git: extraction.git,
3047
- metrics: extraction.metricInputs.map((row) => ({
3048
- metricId: row.metricId,
3049
- ruleVersion: row.ruleVersion,
3050
- value: row.value,
3051
- band: row.band,
3052
- coverage: row.coverage,
3053
- ...row.coverageTag === void 0 ? {} : { coverageTag: row.coverageTag }
3054
- })),
3055
- utcOffsetMinutes: extraction.utcOffsetMinutes
3056
- };
3614
+ function applyDayConsent(days, syncConfig) {
3615
+ return days.map((day) => {
3616
+ const { workflow, usage, ...rest } = day;
3617
+ const out = { ...rest };
3618
+ if (usage) {
3619
+ out.usage = syncConfig.publishCost ? usage : {
3620
+ harnesses: usage.harnesses.map((h) => ({
3621
+ ...h,
3622
+ models: h.models.map(
3623
+ ({ usd: _usd, pricingTable: _table, ...model }) => model
3624
+ )
3625
+ }))
3626
+ };
3627
+ }
3628
+ if (workflow && syncConfig.publishWorkflow) out.workflow = workflow;
3629
+ return out;
3630
+ });
3057
3631
  }
3058
3632
  function mergeKeptPrivate(halves) {
3059
3633
  const out = {};
@@ -3072,11 +3646,18 @@ function mergeKeptPrivate(halves) {
3072
3646
  }
3073
3647
  return out;
3074
3648
  }
3075
- function buildSyncBody(built, syncConfig, autoSync, trigger = "manual", workflow, cliVersion) {
3649
+ function buildSyncBody(built, syncConfig, autoSync, trigger = "manual", measuredDays, cliVersion) {
3076
3650
  const payloads = built.map((b) => b.payload);
3077
3651
  const base = autoSync ? { payloads, autoSync, trigger } : { payloads, trigger };
3078
- const withWorkflow = workflow && syncConfig.publishWorkflow ? { ...base, workflow: toPayloadWorkflow(workflow) } : base;
3079
- const withVersion = cliVersion ? { ...withWorkflow, cliVersion } : withWorkflow;
3652
+ const withDays = measuredDays ? {
3653
+ ...base,
3654
+ measuredDays: {
3655
+ aggregateVersion: MEASURED_DAYS_V1,
3656
+ utcOffsetMinutes: measuredDays.utcOffsetMinutes,
3657
+ days: applyDayConsent(measuredDays.days, syncConfig)
3658
+ }
3659
+ } : base;
3660
+ const withVersion = cliVersion ? { ...withDays, cliVersion } : withDays;
3080
3661
  if (!syncConfig.reviewKeptPrivate) return withVersion;
3081
3662
  return {
3082
3663
  ...withVersion,
@@ -3085,7 +3666,7 @@ function buildSyncBody(built, syncConfig, autoSync, trigger = "manual", workflow
3085
3666
  }
3086
3667
 
3087
3668
  // src/workflow/reducer.ts
3088
- var WORKFLOW_AGGREGATE_VERSION = "workflow-aggregates/v1";
3669
+ var WORKFLOW_AGGREGATE_VERSION = WORKFLOW_AGGREGATES_V2;
3089
3670
  function createWorkflowLocalSources() {
3090
3671
  return { projectWorkspaces: /* @__PURE__ */ new Set(), activeProjectDays: /* @__PURE__ */ new Map() };
3091
3672
  }
@@ -3097,10 +3678,10 @@ var emptyPhase = () => ({
3097
3678
  unknown: 0
3098
3679
  });
3099
3680
  var finiteNonnegative = (value) => value !== void 0 && Number.isFinite(value) && value > 0 ? value : 0;
3100
- var isHighEffort = (effort) => ["high", "xhigh", "max", "ultra"].includes(effort.toLowerCase());
3101
3681
  var bump2 = (map, key, amount = 1) => {
3102
3682
  map.set(key, (map.get(key) ?? 0) + amount);
3103
3683
  };
3684
+ var utcDateOf2 = (ms) => new Date(ms).toISOString().slice(0, 10);
3104
3685
  var PHASE_RANK = {
3105
3686
  verify: 4,
3106
3687
  handoff: 3,
@@ -3145,18 +3726,9 @@ function reduceEventBatches(recorded, harness) {
3145
3726
  }
3146
3727
  return output;
3147
3728
  }
3148
- var verifyRuns = (events, harness) => {
3149
- const phases = events.map(
3150
- (event) => deriveSessionPhases([event], PHASE_RULES_V1, harness).phaseEvents.verify > 0 ? "verify" : "other"
3151
- );
3152
- let runs = 0;
3153
- let inside = false;
3154
- for (const phase of phases) {
3155
- if (phase === "verify" && !inside) runs++;
3156
- inside = phase === "verify";
3157
- }
3158
- return runs;
3159
- };
3729
+ var hasVerifyRun = (events, harness) => events.some(
3730
+ (event) => deriveSessionPhases([event], PHASE_RULES_V1, harness).phaseEvents.verify > 0
3731
+ );
3160
3732
  function shellIncludes(arg, head) {
3161
3733
  return arg.split(/(?:&&|\|\||;|\|)/).some((part) => part.trim() === head || part.trim().startsWith(`${head} `));
3162
3734
  }
@@ -3174,10 +3746,47 @@ function sessionState() {
3174
3746
  lastTs: void 0
3175
3747
  };
3176
3748
  }
3749
+ function dayState() {
3750
+ return {
3751
+ sessions: 0,
3752
+ startHours: /* @__PURE__ */ new Map(),
3753
+ phase: {
3754
+ sessions: 0,
3755
+ phaseSec: emptyPhase(),
3756
+ phaseEvents: emptyPhase(),
3757
+ waitingSec: 0,
3758
+ idleSec: 0,
3759
+ sessionsWithVerify: 0,
3760
+ sessionsWithHandoff: 0,
3761
+ lengths: /* @__PURE__ */ new Map()
3762
+ },
3763
+ routing: { main: /* @__PURE__ */ new Map(), subagents: /* @__PURE__ */ new Map() },
3764
+ hasRouting: false,
3765
+ delegation: {
3766
+ mainToolCalls: 0,
3767
+ subagentToolCalls: 0,
3768
+ widestFanOut: 0,
3769
+ mostSubagents: 0
3770
+ },
3771
+ hasDelegation: false,
3772
+ activity: /* @__PURE__ */ new Map(),
3773
+ effort: /* @__PURE__ */ new Map(),
3774
+ hasEffort: false,
3775
+ thinking: { thinkingTokens: 0, responseTokens: 0 },
3776
+ hasThinking: false,
3777
+ turnDurations: /* @__PURE__ */ new Map(),
3778
+ hasDurations: false,
3779
+ questions: { asked: 0, turns: 0 },
3780
+ hasQuestions: false,
3781
+ webSearches: 0,
3782
+ hasWebSearches: false
3783
+ };
3784
+ }
3177
3785
  function createHarnessWorkflowReducer(harness, localSources = createWorkflowLocalSources()) {
3178
3786
  const sessions = /* @__PURE__ */ new Map();
3179
- const activity = /* @__PURE__ */ new Map();
3787
+ const eventCells = /* @__PURE__ */ new Map();
3180
3788
  const webSearchesByDate = /* @__PURE__ */ new Map();
3789
+ const eventDates = /* @__PURE__ */ new Set();
3181
3790
  let finished;
3182
3791
  const getSession = (key) => {
3183
3792
  let state = sessions.get(key);
@@ -3197,7 +3806,7 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3197
3806
  state.parentSession ??= observation.parentSession;
3198
3807
  state.sidechain ||= observation.sidechain === true;
3199
3808
  const at = new Date(observation.tsMs);
3200
- const date = at.toISOString().slice(0, 10);
3809
+ const date = utcDateOf2(observation.tsMs);
3201
3810
  if (observation.projectWorkspace) {
3202
3811
  state.projectWorkspaces.add(observation.projectWorkspace);
3203
3812
  localSources.projectWorkspaces.add(observation.projectWorkspace);
@@ -3208,7 +3817,10 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3208
3817
  event: [observation.tsMs, observation.tool, arg],
3209
3818
  ...observation.batchId ? { batchId: observation.batchId } : {}
3210
3819
  });
3211
- bump2(activity, `${at.getUTCDay()}:${at.getUTCHours()}`);
3820
+ eventDates.add(date);
3821
+ const cells = eventCells.get(date) ?? /* @__PURE__ */ new Map();
3822
+ bump2(cells, `${at.getUTCDay()}:${at.getUTCHours()}`);
3823
+ eventCells.set(date, cells);
3212
3824
  if (["WebSearch", "web_search", "websearch"].includes(observation.tool))
3213
3825
  bump2(webSearchesByDate, date);
3214
3826
  } else if (observation.type === "response") {
@@ -3234,135 +3846,118 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3234
3846
  },
3235
3847
  finish() {
3236
3848
  if (finished) return finished;
3237
- const phaseSec = emptyPhase();
3238
- const phaseEvents = emptyPhase();
3239
- const sessionRows2 = [];
3240
- const facts = [];
3241
- const modelTokens = {
3242
- main: /* @__PURE__ */ new Map(),
3243
- subagents: /* @__PURE__ */ new Map()
3849
+ const days = /* @__PURE__ */ new Map();
3850
+ const dayOf = (date) => {
3851
+ let state = days.get(date);
3852
+ if (!state) {
3853
+ state = dayState();
3854
+ days.set(date, state);
3855
+ }
3856
+ return state;
3244
3857
  };
3245
- let waitingSec = 0;
3246
- let idleSec = 0;
3247
- let mainToolCalls = 0;
3248
- let subagentToolCalls = 0;
3858
+ const windowPhaseSec = emptyPhase();
3249
3859
  let phaseSessionCount = 0;
3250
3860
  for (const state of sessions.values()) {
3861
+ if (state.firstTs === void 0) continue;
3862
+ const day = dayOf(utcDateOf2(state.firstTs));
3251
3863
  const events = reduceEventBatches(state.events, harness);
3252
3864
  const responses = [...state.responses.values()];
3253
- const efforts = responses.flatMap(
3254
- (response) => response.effort ? [response.effort] : []
3255
- );
3256
- const models = new Set(
3257
- responses.flatMap(
3258
- (response) => response.model ? [response.model] : []
3259
- )
3260
- );
3261
- const thinkingResponses = responses.filter(
3262
- (response) => response.thinkingTokens !== void 0
3263
- );
3264
- const outputResponses = responses.filter(
3265
- (response) => response.responseTokens !== void 0
3865
+ day.sessions++;
3866
+ const startHour = new Date(state.firstTs).getUTCHours();
3867
+ day.startHours.set(startHour, (day.startHours.get(startHour) ?? 0) + 1);
3868
+ const phases = deriveSessionPhases(events, PHASE_RULES_V1, harness);
3869
+ if (state.events.length > 0) phaseSessionCount++;
3870
+ day.phase.sessions++;
3871
+ for (const phase of PHASES) {
3872
+ day.phase.phaseSec[phase] += phases.phaseSec[phase];
3873
+ day.phase.phaseEvents[phase] += phases.phaseEvents[phase];
3874
+ windowPhaseSec[phase] += phases.phaseSec[phase];
3875
+ }
3876
+ day.phase.waitingSec += phases.waitingSec;
3877
+ day.phase.idleSec += phases.idleSec;
3878
+ if (phases.phaseEvents.verify > 0) day.phase.sessionsWithVerify++;
3879
+ if (phases.phaseEvents.handoff > 0) day.phase.sessionsWithHandoff++;
3880
+ const measuredSec = PHASES.reduce(
3881
+ (sum, phase) => sum + phases.phaseSec[phase],
3882
+ 0
3266
3883
  );
3267
- const durations = responses.flatMap(
3268
- (response) => response.durationSec === void 0 ? [] : [response.durationSec]
3884
+ const bucket = logBucket(measuredSec / 60);
3885
+ const merged = events.some(
3886
+ ([, tool, arg]) => ["Bash", "bash", "shell", "local_shell", "exec_command"].includes(
3887
+ tool
3888
+ ) && shellIncludes(arg, "gh pr merge")
3269
3889
  );
3270
- const sessionFact = {
3271
- harness,
3272
- ...models.size > 0 ? { modelSwitched: models.size > 1 } : {},
3273
- ...thinkingResponses.length > 0 ? {
3274
- thinkingTokens: thinkingResponses.reduce(
3275
- (sum, response) => sum + (response.thinkingTokens ?? 0),
3276
- 0
3277
- )
3278
- } : {},
3279
- ...outputResponses.length > 0 ? {
3280
- responseTokens: outputResponses.reduce(
3281
- (sum, response) => sum + (response.responseTokens ?? 0),
3282
- 0
3283
- )
3284
- } : {},
3285
- ...harness === "pi-mono" ? {} : {
3286
- questionBackTurns: [...state.turns.values()].filter(Boolean).length,
3287
- totalTurns: state.turns.size
3288
- }
3890
+ const verified = hasVerifyRun(events, harness);
3891
+ const openedWithScout = (events[0] ? deriveSessionPhases([events[0]], PHASE_RULES_V1, harness).phaseEvents.scout : 0) > 0;
3892
+ const length = day.phase.lengths.get(bucket) ?? {
3893
+ bucket,
3894
+ sessions: 0,
3895
+ phaseSec: emptyPhase(),
3896
+ merged: 0,
3897
+ verified: 0,
3898
+ mergedVerified: 0,
3899
+ openedWithScout: 0
3289
3900
  };
3290
- if (efforts.length > 0) {
3291
- sessionFact.effortTurns = {
3292
- high: efforts.filter(isHighEffort).length,
3293
- total: efforts.length
3294
- };
3295
- sessionFact.effortChangedMidRun = new Set(efforts).size > 1;
3296
- }
3297
- if (durations.length > 0)
3298
- sessionFact.longestTurnDurationSec = Math.max(...durations);
3299
- facts.push(sessionFact);
3901
+ length.sessions++;
3902
+ for (const phase of PHASES)
3903
+ length.phaseSec[phase] += phases.phaseSec[phase];
3904
+ if (merged) length.merged++;
3905
+ if (verified) length.verified++;
3906
+ if (merged && verified) length.mergedVerified++;
3907
+ if (openedWithScout) length.openedWithScout++;
3908
+ day.phase.lengths.set(bucket, length);
3300
3909
  const routing = state.sidechain || state.parentSession ? "subagents" : "main";
3301
3910
  for (const response of responses) {
3302
3911
  if (!response.model) continue;
3912
+ day.hasRouting = true;
3303
3913
  bump2(
3304
- modelTokens[routing],
3914
+ day.routing[routing],
3305
3915
  response.model,
3306
3916
  response.routingTokens ?? response.responseTokens ?? 0
3307
3917
  );
3308
3918
  }
3309
- if (routing === "subagents") subagentToolCalls += state.events.length;
3310
- else mainToolCalls += state.events.length;
3311
- if (state.events.length > 0) phaseSessionCount++;
3312
- const phases = deriveSessionPhases(events, PHASE_RULES_V1, harness);
3313
- for (const phase of PHASES) {
3314
- phaseSec[phase] += phases.phaseSec[phase];
3315
- phaseEvents[phase] += phases.phaseEvents[phase];
3919
+ if (routing === "subagents") {
3920
+ day.delegation.subagentToolCalls += state.events.length;
3921
+ day.hasDelegation ||= state.events.length > 0;
3922
+ } else day.delegation.mainToolCalls += state.events.length;
3923
+ for (const response of responses) {
3924
+ if (response.effort) {
3925
+ day.hasEffort = true;
3926
+ const level = effortLevelOf(response.effort);
3927
+ day.effort.set(level, (day.effort.get(level) ?? 0) + 1);
3928
+ }
3929
+ if (response.thinkingTokens !== void 0) {
3930
+ day.hasThinking = true;
3931
+ day.thinking.thinkingTokens += response.thinkingTokens;
3932
+ day.thinking.responseTokens += response.responseTokens ?? 0;
3933
+ }
3934
+ if (response.durationSec !== void 0) {
3935
+ day.hasDurations = true;
3936
+ const durationBucket = logBucket(response.durationSec);
3937
+ day.turnDurations.set(
3938
+ durationBucket,
3939
+ (day.turnDurations.get(durationBucket) ?? 0) + 1
3940
+ );
3941
+ }
3942
+ }
3943
+ if (harness !== "pi-mono") {
3944
+ day.hasQuestions = true;
3945
+ day.questions.turns += state.turns.size;
3946
+ day.questions.asked += [...state.turns.values()].filter(
3947
+ Boolean
3948
+ ).length;
3316
3949
  }
3317
- waitingSec += phases.waitingSec;
3318
- idleSec += phases.idleSec;
3319
- const first = state.firstTs;
3320
- const classifications = events.map(
3321
- (event) => deriveSessionPhases([event], PHASE_RULES_V1, harness)
3322
- );
3323
- sessionRows2.push({
3324
- startHourUtc: first === void 0 ? 0 : new Date(first).getUTCHours(),
3325
- eventCount: events.length,
3326
- phaseSec: phases.phaseSec,
3327
- phaseEvents: phases.phaseEvents,
3328
- waitingSec: phases.waitingSec,
3329
- idleSec: phases.idleSec,
3330
- merged: events.some(
3331
- ([, tool, arg]) => ["Bash", "bash", "shell", "local_shell", "exec_command"].includes(
3332
- tool
3333
- ) && shellIncludes(arg, "gh pr merge")
3334
- ),
3335
- verifyRuns: verifyRuns(events, harness),
3336
- reviewRounds: events.filter(
3337
- ([, tool]) => ["mcp__curia__request_review", "request_review"].includes(tool)
3338
- ).length,
3339
- openedWithScout: (classifications[0]?.phaseEvents.scout ?? 0) > 0
3340
- });
3341
3950
  }
3342
- const attributed = PHASES.reduce(
3343
- (sum, phase) => sum + phaseSec[phase],
3344
- 0
3345
- );
3346
- const unknown = attributed === 0 ? 0 : phaseSec.unknown / attributed;
3347
- localSources.activeProjectDays.clear();
3348
- for (const state of sessions.values()) {
3349
- if (state.firstTs === void 0 || state.lastTs === void 0) continue;
3350
- let day = Date.parse(
3351
- `${new Date(state.firstTs).toISOString().slice(0, 10)}T00:00:00Z`
3352
- );
3353
- const lastDay = Date.parse(
3354
- `${new Date(state.lastTs).toISOString().slice(0, 10)}T00:00:00Z`
3355
- );
3356
- while (day <= lastDay) {
3357
- const date = new Date(day).toISOString().slice(0, 10);
3358
- const projects = localSources.activeProjectDays.get(date) ?? /* @__PURE__ */ new Set();
3359
- for (const project of state.projectWorkspaces) projects.add(project);
3360
- if (projects.size > 0)
3361
- localSources.activeProjectDays.set(date, projects);
3362
- day += 864e5;
3951
+ for (const date of eventDates) {
3952
+ const day = dayOf(date);
3953
+ for (const [key, events] of eventCells.get(date) ?? []) {
3954
+ bump2(day.activity, key, events);
3955
+ }
3956
+ if (harness !== "pi-mono") {
3957
+ day.hasWebSearches = true;
3958
+ day.webSearches = webSearchesByDate.get(date) ?? 0;
3363
3959
  }
3364
3960
  }
3365
- const projectsByDate = localSources.activeProjectDays;
3366
3961
  const childrenByParent = /* @__PURE__ */ new Map();
3367
3962
  for (const state of sessions.values()) {
3368
3963
  if (!state.parentSession) continue;
@@ -3370,10 +3965,16 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3370
3965
  children.push(state);
3371
3966
  childrenByParent.set(state.parentSession, children);
3372
3967
  }
3373
- let widestFanOut = 0;
3374
- let mostSubagents = 0;
3375
- for (const children of childrenByParent.values()) {
3376
- mostSubagents = Math.max(mostSubagents, children.length);
3968
+ for (const [parentKey, children] of childrenByParent) {
3969
+ const parent = sessions.get(parentKey);
3970
+ const anchor = parent?.firstTs ?? Math.min(...children.map((child) => child.firstTs ?? Infinity));
3971
+ if (!Number.isFinite(anchor)) continue;
3972
+ const day = dayOf(utcDateOf2(anchor));
3973
+ day.hasDelegation = true;
3974
+ day.delegation.mostSubagents = Math.max(
3975
+ day.delegation.mostSubagents,
3976
+ children.length
3977
+ );
3377
3978
  const boundaries = children.flatMap((child) => [
3378
3979
  { ts: child.firstTs ?? 0, delta: 1 },
3379
3980
  { ts: child.lastTs ?? child.firstTs ?? 0, delta: -1 }
@@ -3382,61 +3983,109 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3382
3983
  let active = 0;
3383
3984
  for (const boundary of boundaries) {
3384
3985
  active += boundary.delta;
3385
- widestFanOut = Math.max(widestFanOut, active);
3986
+ day.delegation.widestFanOut = Math.max(
3987
+ day.delegation.widestFanOut,
3988
+ active
3989
+ );
3386
3990
  }
3387
3991
  }
3992
+ localSources.activeProjectDays.clear();
3993
+ for (const state of sessions.values()) {
3994
+ if (state.firstTs === void 0 || state.lastTs === void 0) continue;
3995
+ let day = Date.parse(`${utcDateOf2(state.firstTs)}T00:00:00Z`);
3996
+ const lastDay = Date.parse(`${utcDateOf2(state.lastTs)}T00:00:00Z`);
3997
+ while (day <= lastDay) {
3998
+ const date = utcDateOf2(day);
3999
+ const projects = localSources.activeProjectDays.get(date) ?? /* @__PURE__ */ new Set();
4000
+ for (const project of state.projectWorkspaces) projects.add(project);
4001
+ if (projects.size > 0)
4002
+ localSources.activeProjectDays.set(date, projects);
4003
+ day += 864e5;
4004
+ }
4005
+ }
4006
+ const attributed = PHASES.reduce(
4007
+ (sum, phase) => sum + windowPhaseSec[phase],
4008
+ 0
4009
+ );
4010
+ const unknown = attributed === 0 ? 0 : windowPhaseSec.unknown / attributed;
4011
+ const routesModels = harness === "claude-code" || harness === "opencode";
3388
4012
  const asRows = (map) => {
3389
4013
  const safe = /* @__PURE__ */ new Map();
3390
4014
  for (const [model, tokens] of map) {
3391
4015
  bump2(safe, sanitizeModelId(model), tokens);
3392
4016
  }
3393
- return [...safe].map(([model, tokens]) => ({ model, tokens }));
4017
+ return [...safe].map(([model, tokens]) => ({ model, tokens })).sort(
4018
+ (a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model)
4019
+ );
3394
4020
  };
3395
- const hasDelegation = subagentToolCalls > 0 || mostSubagents > 0 || widestFanOut > 0;
3396
4021
  finished = {
3397
4022
  aggregateVersion: WORKFLOW_AGGREGATE_VERSION,
3398
4023
  harness,
3399
- phase: {
4024
+ gate: {
3400
4025
  ruleVersion: PHASE_RULES_V1,
3401
4026
  publishable: phaseSessionCount > 0 && unknown <= UNKNOWN_GATE,
3402
4027
  sessions: sessions.size,
3403
- phaseSec,
3404
- phaseEvents,
3405
- waitingSec,
3406
- idleSec,
3407
- unknownShare: unknown,
3408
- sessionRows: sessionRows2
3409
- },
3410
- facts: {
3411
- sessions: facts,
3412
- activeDays: [...projectsByDate].sort(([a], [b]) => a.localeCompare(b)).map(([date, projects]) => ({
3413
- date,
3414
- parallelProjectCount: projects.size,
3415
- ...harness === "pi-mono" ? {} : { webSearches: webSearchesByDate.get(date) ?? 0 }
3416
- }))
4028
+ unknownShare: unknown
3417
4029
  },
3418
- ...harness === "claude-code" || harness === "opencode" ? {
3419
- routing: {
3420
- main: asRows(modelTokens.main),
3421
- subagents: asRows(modelTokens.subagents)
3422
- }
3423
- } : {},
3424
- ...hasDelegation ? {
3425
- delegation: {
3426
- mainToolCalls,
3427
- subagentToolCalls,
3428
- widestFanOut,
3429
- mostSubagents
3430
- }
3431
- } : {},
3432
- activity: [...activity].map(([key, events]) => {
3433
- const [weekdayUtc, hourUtc] = key.split(":").map(Number);
3434
- return { weekdayUtc, hourUtc, events };
3435
- }).sort((a, b) => a.weekdayUtc - b.weekdayUtc || a.hourUtc - b.hourUtc)
4030
+ days: [...days].sort(([a], [b]) => a.localeCompare(b)).map(([date, day]) => ({
4031
+ date,
4032
+ harness,
4033
+ sessions: day.sessions,
4034
+ startHours: [...day.startHours].map(([hourUtc, count]) => ({ hourUtc, sessions: count })).sort((a, b) => a.hourUtc - b.hourUtc),
4035
+ ...day.phase.sessions > 0 ? {
4036
+ phase: {
4037
+ ruleVersion: PHASE_RULES_V1,
4038
+ sessions: day.phase.sessions,
4039
+ phaseSec: day.phase.phaseSec,
4040
+ phaseEvents: day.phase.phaseEvents,
4041
+ waitingSec: day.phase.waitingSec,
4042
+ idleSec: day.phase.idleSec,
4043
+ sessionsWithVerify: day.phase.sessionsWithVerify,
4044
+ sessionsWithHandoff: day.phase.sessionsWithHandoff,
4045
+ bucketRuleVersion: LOG_BUCKETS_V1,
4046
+ lengths: [...day.phase.lengths.values()].sort(
4047
+ (a, b) => a.bucket - b.bucket
4048
+ )
4049
+ }
4050
+ } : {},
4051
+ ...routesModels && day.hasRouting ? {
4052
+ routing: {
4053
+ main: asRows(day.routing.main),
4054
+ subagents: asRows(day.routing.subagents)
4055
+ }
4056
+ } : {},
4057
+ ...day.hasDelegation ? { delegation: day.delegation } : {},
4058
+ activity: [...day.activity].map(([key, events]) => {
4059
+ const [weekdayUtc, hourUtc] = key.split(":").map(Number);
4060
+ return {
4061
+ weekdayUtc: weekdayUtc ?? 0,
4062
+ hourUtc: hourUtc ?? 0,
4063
+ events
4064
+ };
4065
+ }).sort(
4066
+ (a, b) => a.weekdayUtc - b.weekdayUtc || a.hourUtc - b.hourUtc
4067
+ ),
4068
+ ...day.hasEffort ? {
4069
+ effort: EFFORT_LEVELS.flatMap((level) => {
4070
+ const turns = day.effort.get(level) ?? 0;
4071
+ return turns > 0 ? [{ level, turns }] : [];
4072
+ })
4073
+ } : {},
4074
+ ...day.hasThinking ? { thinking: day.thinking } : {},
4075
+ ...day.hasDurations ? {
4076
+ turnDurations: {
4077
+ bucketRuleVersion: LOG_BUCKETS_V1,
4078
+ buckets: [...day.turnDurations].map(([bucket, turns]) => ({ bucket, turns })).sort((a, b) => a.bucket - b.bucket)
4079
+ }
4080
+ } : {},
4081
+ ...day.hasQuestions ? { questions: day.questions } : {},
4082
+ ...day.hasWebSearches ? { webSearches: day.webSearches } : {}
4083
+ }))
3436
4084
  };
3437
4085
  sessions.clear();
3438
- activity.clear();
4086
+ eventCells.clear();
3439
4087
  webSearchesByDate.clear();
4088
+ eventDates.clear();
3440
4089
  return finished;
3441
4090
  }
3442
4091
  };
@@ -3460,7 +4109,8 @@ function ingestRecord(agg, raw, ctx) {
3460
4109
  const rec = asObj(raw);
3461
4110
  if (!rec) return;
3462
4111
  agg.records++;
3463
- agg.projectDirs.add(projectWorkspaceDirectory(rec) ?? ctx.projectDir);
4112
+ const projectDir = projectWorkspaceDirectory(rec) ?? ctx.projectDir;
4113
+ agg.projectDirs.add(projectDir);
3464
4114
  const version = asStr(rec.version);
3465
4115
  if (version) agg.ccVersions.add(cleanName(version));
3466
4116
  const sessionId = asStr(rec.sessionId);
@@ -3476,6 +4126,8 @@ function ingestRecord(agg, raw, ctx) {
3476
4126
  agg.lastTs = agg.lastTs === null ? ts : Math.max(agg.lastTs, ts);
3477
4127
  }
3478
4128
  }
4129
+ if (sessionId) noteSessionStart(agg, sessionId, tsMs);
4130
+ noteProjectDay(agg, projectDir, tsMs);
3479
4131
  const type = asStr(rec.type);
3480
4132
  if (type === "assistant") {
3481
4133
  ingestClaudeWorkflow(agg, rec, ctx, tsMs);
@@ -3587,7 +4239,9 @@ function ingestAssistant(agg, rec, tsMs) {
3587
4239
  const model = asName(msg.model) ?? "(unknown)";
3588
4240
  if (model.startsWith("<")) {
3589
4241
  agg.syntheticRecords++;
3590
- agg.syntheticTokens += countsTotal(readCounts(usage));
4242
+ const synthetic = countsTotal(readCounts(usage));
4243
+ agg.syntheticTokens += synthetic;
4244
+ noteSyntheticTokens(agg, tsMs, synthetic);
3591
4245
  return;
3592
4246
  }
3593
4247
  if (tsMs === null) agg.untimestampedResponses++;
@@ -3687,6 +4341,7 @@ function buildContribution(usage, model, sidechain, tsMs) {
3687
4341
  entries,
3688
4342
  total: entries.reduce((a, e) => a + countsTotal(e.counts), 0),
3689
4343
  sidechain,
4344
+ tsMs,
3690
4345
  webSearch: serverTools ? asNum(serverTools.web_search_requests) : 0,
3691
4346
  webFetch: serverTools ? asNum(serverTools.web_fetch_requests) : 0,
3692
4347
  mirroredIterationTypes: [...mirrored],
@@ -3710,6 +4365,11 @@ function applyContribution(agg, c, sign) {
3710
4365
  m.cacheRead += sign * counts.cacheRead;
3711
4366
  if (costUSD === null) m.unpricedTokens += sign * countsTotal(counts);
3712
4367
  else m.costUSD += sign * costUSD;
4368
+ noteUsageResponse(
4369
+ agg,
4370
+ { tsMs: c.tsMs, modelKey, counts, costUSD, sidechain: c.sidechain },
4371
+ sign
4372
+ );
3713
4373
  });
3714
4374
  if (c.sidechain) agg.sidechainTokens += sign * c.total;
3715
4375
  else agg.mainTokens += sign * c.total;
@@ -3976,12 +4636,14 @@ function ingestLine(agg, raw, state, sinceMs) {
3976
4636
  agg.firstTs = agg.firstTs === null ? tsMs : Math.min(agg.firstTs, tsMs);
3977
4637
  agg.lastTs = agg.lastTs === null ? tsMs : Math.max(agg.lastTs, tsMs);
3978
4638
  }
3979
- noteActivity(agg, state);
4639
+ noteActivity(agg, state, tsMs);
4640
+ noteProjectDay(agg, state.cwd ?? "(unknown)", tsMs);
3980
4641
  if (type === "event_msg" && payload) ingestEvent(agg, payload, state, tsMs);
3981
4642
  else if (type === "response_item" && payload)
3982
4643
  ingestItem(agg, payload, state, tsMs);
3983
4644
  }
3984
- function noteActivity(agg, state) {
4645
+ function noteActivity(agg, state, tsMs) {
4646
+ if (state.sessionId) noteSessionStart(agg, state.sessionId, tsMs);
3985
4647
  if (state.counted) return;
3986
4648
  state.counted = true;
3987
4649
  if (state.sessionId) agg.sessions.add(state.sessionId);
@@ -4012,7 +4674,9 @@ function ingestEvent(agg, payload, state, tsMs) {
4012
4674
  agg,
4013
4675
  modelKey,
4014
4676
  counts,
4015
- apiEquivalentCost(modelKey, counts, tsMs)
4677
+ apiEquivalentCost(modelKey, counts, tsMs),
4678
+ 1,
4679
+ { tsMs }
4016
4680
  );
4017
4681
  agg.mainTokens += total;
4018
4682
  if (tsMs !== null && state.sessionId) {
@@ -4415,10 +5079,14 @@ function ingestMessageRow(agg, state, row) {
4415
5079
  const session = sessionId ? state.sessions.get(sessionId) : void 0;
4416
5080
  if (sessionId) {
4417
5081
  agg.sessions.add(sessionId);
5082
+ noteSessionStart(agg, sessionId, tsMs);
4418
5083
  if (session?.version) agg.ccVersions.add(cleanName(session.version));
4419
5084
  }
4420
5085
  const cwd = asStr(row.cwd);
4421
- if (cwd) agg.projectDirs.add(cwd);
5086
+ if (cwd) {
5087
+ agg.projectDirs.add(cwd);
5088
+ noteProjectDay(agg, cwd, tsMs);
5089
+ }
4422
5090
  if (asStr(row.role) !== "assistant") return;
4423
5091
  agg.assistantRecords++;
4424
5092
  agg.distinctResponses++;
@@ -4441,7 +5109,9 @@ function ingestMessageRow(agg, state, row) {
4441
5109
  agg,
4442
5110
  modelKey,
4443
5111
  counts,
4444
- apiEquivalentCost(modelKey, counts, tsMs)
5112
+ apiEquivalentCost(modelKey, counts, tsMs),
5113
+ 1,
5114
+ { tsMs, sidechain: Boolean(session?.parentId) }
4445
5115
  );
4446
5116
  if (sessionId && tsMs !== null) {
4447
5117
  const completed = asNum(row.completedTsMs);
@@ -4917,7 +5587,8 @@ function ingestEntry(agg, raw, state, fold, sinceMs) {
4917
5587
  agg.firstTs = agg.firstTs === null ? tsMs : Math.min(agg.firstTs, tsMs);
4918
5588
  agg.lastTs = agg.lastTs === null ? tsMs : Math.max(agg.lastTs, tsMs);
4919
5589
  }
4920
- noteActivity2(agg, state);
5590
+ noteActivity2(agg, state, tsMs);
5591
+ noteProjectDay(agg, state.cwd ?? "(unknown)", tsMs);
4921
5592
  if (role === "assistant" && message) {
4922
5593
  agg.assistantRecords++;
4923
5594
  const served = asStr(message.responseModel);
@@ -4964,7 +5635,8 @@ function ingestEntry(agg, raw, state, fold, sinceMs) {
4964
5635
  countUsage(agg, fold, rec, 0, rec.usage, state.modelKey, tsMs);
4965
5636
  }
4966
5637
  }
4967
- function noteActivity2(agg, state) {
5638
+ function noteActivity2(agg, state, tsMs) {
5639
+ if (state.sessionId) noteSessionStart(agg, state.sessionId, tsMs);
4968
5640
  if (state.counted) return;
4969
5641
  state.counted = true;
4970
5642
  if (state.sessionId) agg.sessions.add(state.sessionId);
@@ -4993,7 +5665,9 @@ function countUsage(agg, fold, rec, msgTsMs, usageRaw, modelKey, tsMs, priceable
4993
5665
  agg,
4994
5666
  key,
4995
5667
  counts,
4996
- priceable ? apiEquivalentCost(key, counts, tsMs) : null
5668
+ priceable ? apiEquivalentCost(key, counts, tsMs) : null,
5669
+ 1,
5670
+ { tsMs }
4997
5671
  );
4998
5672
  agg.mainTokens += total;
4999
5673
  return "counted";
@@ -5944,6 +6618,152 @@ import { dirname as dirname7, join as join10 } from "path";
5944
6618
  // src/sync/stage.ts
5945
6619
  import { createHash as createHash2 } from "crypto";
5946
6620
 
6621
+ // src/usage/days.ts
6622
+ var round6 = (n) => Math.round(n * 1e6) / 1e6;
6623
+ function buildUsageDays(input) {
6624
+ const { aggregate: agg, harness, publishCost, projectWorkspaceId } = input;
6625
+ const sessionsByDay = /* @__PURE__ */ new Map();
6626
+ for (const startMs of agg.sessionStarts.values()) {
6627
+ const date = utcDateOf(startMs);
6628
+ sessionsByDay.set(date, (sessionsByDay.get(date) ?? 0) + 1);
6629
+ }
6630
+ const dates = [
6631
+ .../* @__PURE__ */ new Set([...agg.usageDays.keys(), ...sessionsByDay.keys()])
6632
+ ].sort();
6633
+ const out = /* @__PURE__ */ new Map();
6634
+ for (const date of dates) {
6635
+ const acc = agg.usageDays.get(date);
6636
+ const groups = /* @__PURE__ */ new Map();
6637
+ let unpriced = 0;
6638
+ for (const [modelKey, m] of acc?.models ?? []) {
6639
+ const id = sanitizeModelId(baseModelId(modelKey));
6640
+ let g = groups.get(id);
6641
+ if (!g) {
6642
+ g = {
6643
+ tokens: {
6644
+ input: 0,
6645
+ output: 0,
6646
+ cacheWrite: 0,
6647
+ cacheRead: 0,
6648
+ cacheWriteTtl: { fiveMinute: 0, oneHour: 0, unsplit: 0 }
6649
+ },
6650
+ costUSD: 0,
6651
+ unpricedTokens: 0,
6652
+ table: null
6653
+ };
6654
+ groups.set(id, g);
6655
+ }
6656
+ g.table ??= pricingTableFor(modelKey);
6657
+ g.tokens.input += m.counts.input;
6658
+ g.tokens.output += m.counts.output;
6659
+ g.tokens.cacheRead += m.counts.cacheRead;
6660
+ g.tokens.cacheWrite += m.counts.cacheWrite5m + m.counts.cacheWrite1h + m.counts.cacheWriteUnsplit;
6661
+ g.tokens.cacheWriteTtl.fiveMinute += m.counts.cacheWrite5m;
6662
+ g.tokens.cacheWriteTtl.oneHour += m.counts.cacheWrite1h;
6663
+ g.tokens.cacheWriteTtl.unsplit += m.counts.cacheWriteUnsplit;
6664
+ g.costUSD += m.costUSD;
6665
+ g.unpricedTokens += m.unpricedTokens;
6666
+ unpriced += m.unpricedTokens;
6667
+ }
6668
+ const models = [...groups.entries()].map(([model, g]) => {
6669
+ const { cacheWriteTtl, ...plain } = g.tokens;
6670
+ const tokens = g.tokens.cacheWrite > 0 ? { ...plain, cacheWriteTtl } : plain;
6671
+ const row = { model, tokens };
6672
+ if (publishCost && g.unpricedTokens === 0 && g.table !== null && countsTotalOf(tokens) > 0) {
6673
+ row.usd = round6(g.costUSD);
6674
+ row.pricingTable = g.table;
6675
+ }
6676
+ return row;
6677
+ }).filter((row) => countsTotalOf(row.tokens) > 0).sort(
6678
+ (a, b) => countsTotalOf(b.tokens) - countsTotalOf(a.tokens) || a.model.localeCompare(b.model)
6679
+ );
6680
+ out.set(date, {
6681
+ harness,
6682
+ sessions: sessionsByDay.get(date) ?? 0,
6683
+ projectKeys: [
6684
+ ...new Set([...acc?.projectDirs ?? []].map(projectWorkspaceId))
6685
+ ].sort(),
6686
+ models,
6687
+ subagentTokens: acc?.subagentTokens ?? 0,
6688
+ excludedTokens: { unpriced, synthetic: acc?.syntheticTokens ?? 0 }
6689
+ });
6690
+ }
6691
+ return out;
6692
+ }
6693
+ var countsTotalOf = (t) => t.input + t.output + t.cacheWrite + t.cacheRead;
6694
+ function mergeUsageDays(perHarness) {
6695
+ const out = /* @__PURE__ */ new Map();
6696
+ const dates = [
6697
+ ...new Set(perHarness.flatMap((days) => [...days.keys()]))
6698
+ ].sort();
6699
+ for (const date of dates) {
6700
+ const harnesses = [];
6701
+ for (const days of perHarness) {
6702
+ const day = days.get(date);
6703
+ if (day) harnesses.push(day);
6704
+ }
6705
+ out.set(date, { harnesses });
6706
+ }
6707
+ return out;
6708
+ }
6709
+ function buildMeasuredDays(input) {
6710
+ const workflowByDate = /* @__PURE__ */ new Map();
6711
+ for (const day of input.workflow ?? []) workflowByDate.set(day.date, day);
6712
+ const dates = [.../* @__PURE__ */ new Set([...input.usage.keys(), ...workflowByDate.keys()])].filter(
6713
+ (d) => /^\d{4}-\d{2}-\d{2}$/.test(d) && d >= input.from && d <= input.to
6714
+ ).sort();
6715
+ return dates.map((date) => {
6716
+ const usage = input.usage.get(date);
6717
+ const workflow = workflowByDate.get(date);
6718
+ return {
6719
+ date,
6720
+ ...usage ? { usage } : {},
6721
+ ...workflow ? { workflow } : {}
6722
+ };
6723
+ });
6724
+ }
6725
+
6726
+ // src/usage/diff.ts
6727
+ var MAX_DAY_WINDOW = 400;
6728
+ var DAY_MS = 864e5;
6729
+ function retentionFloor(todayUtc, days) {
6730
+ const span = Math.max(1, Math.min(days, MAX_DAY_WINDOW));
6731
+ const todayMs = Date.parse(`${todayUtc}T00:00:00.000Z`);
6732
+ return new Date(todayMs - (span - 1) * DAY_MS).toISOString().slice(0, 10);
6733
+ }
6734
+ function selectDaysToPublish(input) {
6735
+ const { local, manifest, todayUtc } = input;
6736
+ const retention = manifest?.retentionDays ?? MAX_DAY_WINDOW;
6737
+ const floor = retentionFloor(todayUtc, retention);
6738
+ const comparable = manifest !== null && manifest.aggregateVersion === MEASURED_DAYS_V1;
6739
+ const held = /* @__PURE__ */ new Map();
6740
+ if (comparable) {
6741
+ for (const day of manifest.days) held.set(day.date, day.fingerprint);
6742
+ }
6743
+ const send = [];
6744
+ const skipped = [];
6745
+ for (const day of [...local].sort((a, b) => a.date.localeCompare(b.date))) {
6746
+ if (day.date < floor) {
6747
+ skipped.push({ date: day.date, reason: "expired" });
6748
+ continue;
6749
+ }
6750
+ if (comparable && day.date !== todayUtc) {
6751
+ const fingerprint = held.get(day.date);
6752
+ if (fingerprint !== void 0 && fingerprint === dayFingerprint(day)) {
6753
+ skipped.push({ date: day.date, reason: "unchanged" });
6754
+ continue;
6755
+ }
6756
+ }
6757
+ send.push(day);
6758
+ }
6759
+ return {
6760
+ send,
6761
+ unchanged: skipped.filter((s) => s.reason === "unchanged").length,
6762
+ skipped,
6763
+ mode: comparable ? "diff" : "full"
6764
+ };
6765
+ }
6766
+
5947
6767
  // src/workflow/git.ts
5948
6768
  import { execFileSync as execFileSync2 } from "child_process";
5949
6769
  import path6 from "path";
@@ -5962,11 +6782,11 @@ var defaultRunner = (cwd, args) => {
5962
6782
  return null;
5963
6783
  }
5964
6784
  };
5965
- var emptyResult = () => ({
6785
+ var emptyGitDay = () => ({
5966
6786
  testFileRuleVersion: TEST_FILE_RULE_VERSION,
5967
6787
  fileTypeRuleVersion: FILE_TYPE_RULE_VERSION,
5968
6788
  commitSetRuleVersion: COMMIT_SET_RULE_VERSION,
5969
- totalCommits: 0,
6789
+ commits: 0,
5970
6790
  lateNightCommits: 0,
5971
6791
  additions: 0,
5972
6792
  removals: 0,
@@ -6116,9 +6936,25 @@ function extractGitWorkflow(options) {
6116
6936
  const root = run(directory, ["rev-parse", "--show-toplevel"])?.trim();
6117
6937
  if (root) roots.add(root);
6118
6938
  }
6119
- const result = emptyResult();
6120
- const extensionLines = /* @__PURE__ */ new Map();
6121
- const cells = /* @__PURE__ */ new Map();
6939
+ const days = /* @__PURE__ */ new Map();
6940
+ const dayOf = (date) => {
6941
+ let day = days.get(date);
6942
+ if (!day) {
6943
+ const {
6944
+ changedLinesByExtension: _extensions,
6945
+ weekdayHourCells: _cells,
6946
+ ...rest
6947
+ } = emptyGitDay();
6948
+ day = {
6949
+ ...rest,
6950
+ changedLinesPerCommit: [],
6951
+ extensionLines: /* @__PURE__ */ new Map(),
6952
+ cells: /* @__PURE__ */ new Map()
6953
+ };
6954
+ days.set(date, day);
6955
+ }
6956
+ return day;
6957
+ };
6122
6958
  const seenCommits = /* @__PURE__ */ new Set();
6123
6959
  for (const root of roots) {
6124
6960
  const history = run(root, [
@@ -6133,17 +6969,25 @@ function extractGitWorkflow(options) {
6133
6969
  let current;
6134
6970
  const finishCommit = () => {
6135
6971
  if (!current?.included || !current.authored) return;
6136
- result.totalCommits++;
6137
- result.additions += current.additions;
6138
- result.removals += current.removals;
6139
- result.changedLinesPerCommit.push(current.changedLines);
6140
- if (current.touchesTest) result.testFileCommits++;
6972
+ const day = dayOf(current.date);
6973
+ day.commits++;
6974
+ day.additions += current.additions;
6975
+ day.removals += current.removals;
6976
+ day.changedLinesPerCommit.push(current.changedLines);
6977
+ if (current.touchesTest) day.testFileCommits++;
6141
6978
  const { weekdayUtc, hourUtc } = current.cell;
6142
6979
  if (isLateNight(localHour(hourUtc, options.utcOffsetMinutes))) {
6143
- result.lateNightCommits++;
6980
+ day.lateNightCommits++;
6144
6981
  }
6145
6982
  const cellKey = `${weekdayUtc}:${hourUtc}`;
6146
- cells.set(cellKey, (cells.get(cellKey) ?? 0) + 1);
6983
+ day.cells.set(cellKey, (day.cells.get(cellKey) ?? 0) + 1);
6984
+ day.withheldExtensionLines += current.withheldLines;
6985
+ for (const [extension, lines2] of current.extensionLines) {
6986
+ day.extensionLines.set(
6987
+ extension,
6988
+ (day.extensionLines.get(extension) ?? 0) + lines2
6989
+ );
6990
+ }
6147
6991
  };
6148
6992
  const fields = history.split("\0");
6149
6993
  for (let fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
@@ -6156,12 +7000,15 @@ function extractGitWorkflow(options) {
6156
7000
  const included = Number.isFinite(authoredMs) && authoredMs >= options.fromMs && authoredMs <= options.toMs && !seenCommits.has(hash);
6157
7001
  current = {
6158
7002
  included,
7003
+ date: included ? new Date(authoredMs).toISOString().slice(0, 10) : "",
6159
7004
  cell: utcCell(included ? authoredMs : 0),
6160
7005
  authored: false,
6161
7006
  additions: 0,
6162
7007
  removals: 0,
6163
7008
  changedLines: 0,
6164
- touchesTest: false
7009
+ touchesTest: false,
7010
+ withheldLines: 0,
7011
+ extensionLines: /* @__PURE__ */ new Map()
6165
7012
  };
6166
7013
  if (included) seenCommits.add(hash);
6167
7014
  continue;
@@ -6184,20 +7031,34 @@ function extractGitWorkflow(options) {
6184
7031
  if (fileChangedLines <= 0) continue;
6185
7032
  const extension = path6.extname(file).toLowerCase();
6186
7033
  if (APPROVED_EXTENSIONS.has(extension)) {
6187
- extensionLines.set(
7034
+ current.extensionLines.set(
6188
7035
  extension,
6189
- (extensionLines.get(extension) ?? 0) + fileChangedLines
7036
+ (current.extensionLines.get(extension) ?? 0) + fileChangedLines
6190
7037
  );
6191
- } else result.withheldExtensionLines += fileChangedLines;
7038
+ } else current.withheldLines += fileChangedLines;
6192
7039
  }
6193
7040
  finishCommit();
6194
7041
  }
6195
- result.changedLinesByExtension = [...extensionLines].map(([extension, changedLines]) => ({ extension, changedLines })).sort((a, b) => a.extension.localeCompare(b.extension));
6196
- result.weekdayHourCells = [...cells].map(([key, commits]) => {
6197
- const [weekdayUtc, hourUtc] = key.split(":").map(Number);
6198
- return { weekdayUtc: weekdayUtc ?? 0, hourUtc: hourUtc ?? 0, commits };
6199
- }).sort((a, b) => a.weekdayUtc - b.weekdayUtc || a.hourUtc - b.hourUtc);
6200
- return result;
7042
+ return {
7043
+ days: [...days].sort(([a], [b]) => a.localeCompare(b)).map(([date, day]) => {
7044
+ const { extensionLines, cells, ...rest } = day;
7045
+ return {
7046
+ date,
7047
+ ...rest,
7048
+ changedLinesByExtension: [...extensionLines].map(([extension, changedLines]) => ({ extension, changedLines })).sort((a, b) => a.extension.localeCompare(b.extension)),
7049
+ weekdayHourCells: [...cells].map(([key, commits]) => {
7050
+ const [weekdayUtc, hourUtc] = key.split(":").map(Number);
7051
+ return {
7052
+ weekdayUtc: weekdayUtc ?? 0,
7053
+ hourUtc: hourUtc ?? 0,
7054
+ commits
7055
+ };
7056
+ }).sort(
7057
+ (a, b) => a.weekdayUtc - b.weekdayUtc || a.hourUtc - b.hourUtc
7058
+ )
7059
+ };
7060
+ })
7061
+ };
6201
7062
  }
6202
7063
 
6203
7064
  // src/workflow/extract.ts
@@ -6218,50 +7079,44 @@ function machineUtcOffsetMinutes(now = /* @__PURE__ */ new Date()) {
6218
7079
  return -now.getTimezoneOffset();
6219
7080
  }
6220
7081
  function buildWorkflowExtraction(harnessWorkflows, git, utcOffsetMinutes = machineUtcOffsetMinutes()) {
7082
+ const harnessDays = /* @__PURE__ */ new Map();
6221
7083
  const projectDays = /* @__PURE__ */ new Map();
6222
- const webSearches = /* @__PURE__ */ new Map();
6223
- const webSearchDays = /* @__PURE__ */ new Set();
6224
- const sessions = [];
6225
- for (const { aggregate: workflow, local } of harnessWorkflows) {
6226
- sessions.push(...workflow.facts.sessions);
7084
+ for (const { aggregate, local } of harnessWorkflows) {
7085
+ for (const { date, ...day } of aggregate.days) {
7086
+ const rows = harnessDays.get(date) ?? [];
7087
+ const { phase, ...safe } = day;
7088
+ rows.push(
7089
+ aggregate.gate.publishable && phase ? { ...safe, phase } : safe
7090
+ );
7091
+ harnessDays.set(date, rows);
7092
+ }
6227
7093
  for (const [date, workspaces] of local.activeProjectDays) {
6228
7094
  const projects = projectDays.get(date) ?? /* @__PURE__ */ new Set();
6229
7095
  for (const project of workspaces) projects.add(project);
6230
7096
  projectDays.set(date, projects);
6231
7097
  }
6232
- for (const day of workflow.facts.activeDays) {
6233
- if (day.webSearches === void 0) continue;
6234
- webSearchDays.add(day.date);
6235
- webSearches.set(
6236
- day.date,
6237
- (webSearches.get(day.date) ?? 0) + day.webSearches
6238
- );
6239
- }
6240
7098
  }
6241
- const facts = {
6242
- git: {
6243
- totalCommits: git.totalCommits,
6244
- lateNightCommits: git.lateNightCommits
6245
- },
6246
- sessions,
6247
- activeDays: [...projectDays].sort(([a], [b]) => a.localeCompare(b)).map(([date, projects]) => ({
6248
- date,
6249
- parallelProjectCount: projects.size,
6250
- ...webSearchDays.has(date) ? { webSearches: webSearches.get(date) ?? 0 } : {}
6251
- }))
6252
- };
6253
- const syncedHarnesses = [
6254
- ...new Set(harnessWorkflows.map(({ aggregate }) => aggregate.harness))
6255
- ];
7099
+ const gitDays = /* @__PURE__ */ new Map();
7100
+ for (const { date, ...day } of git.days) gitDays.set(date, day);
7101
+ const dates = [
7102
+ .../* @__PURE__ */ new Set([
7103
+ ...harnessDays.keys(),
7104
+ ...gitDays.keys(),
7105
+ ...projectDays.keys()
7106
+ ])
7107
+ ].sort();
6256
7108
  return {
6257
- aggregateVersion: WORKFLOW_AGGREGATE_VERSION,
6258
- harnesses: harnessWorkflows.map(({ aggregate }) => {
6259
- const { phase, ...safe } = aggregate;
6260
- return phase.publishable ? { ...safe, phase } : safe;
6261
- }),
6262
- git,
6263
- metricInputs: buildFitInputs(facts, syncedHarnesses),
6264
- utcOffsetMinutes
7109
+ aggregateVersion: WORKFLOW_AGGREGATES_V2,
7110
+ utcOffsetMinutes,
7111
+ days: dates.map((date) => {
7112
+ const projects = projectDays.get(date)?.size;
7113
+ return {
7114
+ date,
7115
+ harnesses: harnessDays.get(date) ?? [],
7116
+ git: gitDays.get(date) ?? emptyGitDay(),
7117
+ ...projects === void 0 ? {} : { parallelProjects: projects }
7118
+ };
7119
+ })
6265
7120
  };
6266
7121
  }
6267
7122
 
@@ -6470,18 +7325,25 @@ function payloadBlock(payload, width, ownWindow, stats) {
6470
7325
  }
6471
7326
  var MODEL_ROLLUP = 0.01;
6472
7327
  var PHASE_ORDER = ["scout", "build", "verify", "handoff", "unknown"];
6473
- function workflowBlock(workflow, host) {
7328
+ function workflowBlock(workflowDays, utcOffsetMinutes, host) {
6474
7329
  const out = [];
6475
- const withPlaybook = workflow.harnesses.filter((h) => h.phase);
6476
- const sessions = withPlaybook.reduce(
6477
- (a, h) => a + (h.phase?.sessions ?? 0),
6478
- 0
6479
- );
7330
+ const folded = foldWorkflowDays(workflowDays, {
7331
+ aggregateVersion: WORKFLOW_AGGREGATES_V2,
7332
+ utcOffsetMinutes
7333
+ });
7334
+ const harnesses = folded?.harnesses ?? [];
7335
+ const withPlaybook = harnesses.filter((h) => h.phase);
7336
+ const sessions = harnesses.reduce((a, h) => a + h.sessions, 0);
6480
7337
  const ruleVersions = [
6481
7338
  ...new Set(withPlaybook.map((h) => h.phase?.ruleVersion ?? ""))
6482
7339
  ].filter(Boolean);
6483
7340
  out.push(
6484
- `workflow ${workflow.harnesses.length} harness${workflow.harnesses.length === 1 ? "" : "es"} \xB7 ${sessions} sessions \xB7 ${workflow.aggregateVersion}`
7341
+ `workflow ${harnesses.length} harness${harnesses.length === 1 ? "" : "es"} \xB7 ${sessions} sessions \xB7 ${WORKFLOW_AGGREGATES_V2}`
7342
+ );
7343
+ const first = folded?.dates[0];
7344
+ const last = folded?.dates.at(-1);
7345
+ out.push(
7346
+ ` ${workflowDays.length} day${workflowDays.length === 1 ? "" : "s"}${first && last ? ` \xB7 ${first} to ${last}` : ""}`
6485
7347
  );
6486
7348
  const seconds = PHASE_ORDER.map(
6487
7349
  (phase) => withPlaybook.reduce((a, h) => a + (h.phase?.phaseSec[phase] ?? 0), 0)
@@ -6493,16 +7355,31 @@ function workflowBlock(workflow, host) {
6493
7355
  ).join(" \xB7 ");
6494
7356
  out.push(` ${mix} \xB7 ${ruleVersions.join(", ")}`);
6495
7357
  }
6496
- const git = workflow.git;
6497
- out.push(
6498
- `git ${git.totalCommits} commits \xB7 ${fmtTokens(git.additions + git.removals)} lines changed`
6499
- );
7358
+ const git = folded?.git;
6500
7359
  out.push(
6501
- `metrics ${workflow.metrics.length} measured \xB7 ${[...new Set(workflow.metrics.map((m) => m.ruleVersion))].join(", ")}`
7360
+ `git ${git?.commits ?? 0} commits \xB7 ${fmtTokens((git?.additions ?? 0) + (git?.removals ?? 0))} lines changed`
6502
7361
  );
6503
7362
  out.push(` (Publish workflow is on for ${host})`);
6504
7363
  return out;
6505
7364
  }
7365
+ function daysLine(measuredDays, selection) {
7366
+ const n = measuredDays.days.length;
7367
+ const head = `${n} day${n === 1 ? "" : "s"} to publish`;
7368
+ const unchanged = selection?.unchanged ?? 0;
7369
+ return unchanged > 0 ? `${head}, ${unchanged} unchanged` : head;
7370
+ }
7371
+ function daysBlock(measuredDays, selection) {
7372
+ const out = [`days ${daysLine(measuredDays, selection)}`];
7373
+ const first = measuredDays.days[0]?.date;
7374
+ const last = measuredDays.days.at(-1)?.date;
7375
+ const usageDays = measuredDays.days.filter((d) => d.usage).length;
7376
+ if (first && last) {
7377
+ out.push(
7378
+ ` ${first} to ${last} \xB7 ${usageDays} with usage \xB7 ${measuredDays.aggregateVersion}`
7379
+ );
7380
+ }
7381
+ return out;
7382
+ }
6506
7383
  function buildGateSummary(ctx) {
6507
7384
  const { body, keptPrivate, config, source, baseUrl } = ctx;
6508
7385
  const { payloads } = body;
@@ -6533,10 +7410,23 @@ function buildGateSummary(ctx) {
6533
7410
  out.push(...payloadBlock(payload, width, windows.size > 1, stats));
6534
7411
  }
6535
7412
  if (out[out.length - 1] === "") out.pop();
7413
+ if (body.measuredDays) {
7414
+ out.push("");
7415
+ out.push(...daysBlock(body.measuredDays, ctx.days));
7416
+ }
6536
7417
  out.push("");
6537
- out.push(
6538
- body.workflow ? workflowBlock(body.workflow, host).join("\n") : "workflow not published"
7418
+ const workflowDays = (body.measuredDays?.days ?? []).flatMap(
7419
+ (d) => d.workflow ? [d.workflow] : []
6539
7420
  );
7421
+ if (!config.publishWorkflow || !body.measuredDays) {
7422
+ out.push("workflow not published");
7423
+ } else if (workflowDays.length === 0) {
7424
+ out.push("workflow on, no changed day to publish");
7425
+ } else {
7426
+ out.push(
7427
+ ...workflowBlock(workflowDays, body.measuredDays.utcOffsetMinutes, host)
7428
+ );
7429
+ }
6540
7430
  const n = payloads.reduce((a, p8) => a + withheldCount(p8), 0);
6541
7431
  if (n > 0) {
6542
7432
  out.push("");
@@ -6578,6 +7468,7 @@ function buildGateSummary(ctx) {
6578
7468
  }
6579
7469
 
6580
7470
  // src/sync/stage.ts
7471
+ var utcDate2 = (ms) => new Date(ms).toISOString().slice(0, 10);
6581
7472
  function stageId(bodyJson) {
6582
7473
  return createHash2("sha256").update(bodyJson).digest("hex").slice(0, 12);
6583
7474
  }
@@ -6588,20 +7479,33 @@ async function stageSync(deps) {
6588
7479
  const adapters = deps.adaptersImpl ?? detectedAdapters;
6589
7480
  const windowDays = deps.windowDays ?? DEFAULT_WINDOW_DAYS;
6590
7481
  const projectWorkspaceId = deps.getProjectWorkspaceIdImpl ?? getProjectWorkspaceId;
7482
+ const fetchManifest = deps.fetchManifestImpl ?? fetchDayManifest;
6591
7483
  const { config, source } = await loadConfig({
6592
7484
  baseUrl: deps.baseUrl,
6593
7485
  ...token ? { token } : {}
6594
7486
  });
7487
+ let manifest = null;
7488
+ if (token) {
7489
+ try {
7490
+ manifest = await fetchManifest(deps.baseUrl, token);
7491
+ } catch {
7492
+ manifest = null;
7493
+ }
7494
+ }
7495
+ const retentionDays = Math.max(
7496
+ 1,
7497
+ Math.min(manifest?.retentionDays ?? MAX_DAY_WINDOW, MAX_DAY_WINDOW)
7498
+ );
6595
7499
  const built = [];
6596
7500
  const scanStats = {};
6597
7501
  const workflowScans = [];
7502
+ const usageScans = [];
6598
7503
  const sinceMs = windowStartMs(now, windowDays);
6599
- for (const adapter of await adapters(sinceMs)) {
6600
- const { aggregate, stats, workflow: workflow2, workflowLocal } = await adapter.scan({
6601
- sinceMs
6602
- });
7504
+ const daysSinceMs = windowStartMs(now, retentionDays);
7505
+ const active = await adapters(sinceMs);
7506
+ for (const adapter of active) {
7507
+ const { aggregate, stats } = await adapter.scan({ sinceMs });
6603
7508
  scanStats[adapter.name] = stats;
6604
- workflowScans.push({ aggregate: workflow2, local: workflowLocal });
6605
7509
  built.push(
6606
7510
  buildPayload({
6607
7511
  aggregate,
@@ -6615,19 +7519,51 @@ async function stageSync(deps) {
6615
7519
  })
6616
7520
  );
6617
7521
  }
7522
+ for (const adapter of active) {
7523
+ const { aggregate, workflow: workflow2, workflowLocal } = await adapter.scan({
7524
+ sinceMs: daysSinceMs
7525
+ });
7526
+ workflowScans.push({ aggregate: workflow2, local: workflowLocal });
7527
+ usageScans.push(
7528
+ buildUsageDays({
7529
+ harness: adapter.name,
7530
+ aggregate,
7531
+ publishCost: config.publishCost,
7532
+ projectWorkspaceId
7533
+ })
7534
+ );
7535
+ }
6618
7536
  const settings = (deps.getSettingsImpl ?? getSettings)();
6619
7537
  const workflow = workflowScans.length > 0 && config.publishWorkflow ? extractLocalWorkflow({
6620
7538
  harnesses: workflowScans,
6621
- fromMs: sinceMs,
7539
+ fromMs: daysSinceMs,
6622
7540
  toMs: now,
6623
7541
  ...deps.gitRunnerImpl ? { run: deps.gitRunnerImpl } : {}
6624
7542
  }) : void 0;
7543
+ const localDays = applyDayConsent(
7544
+ buildMeasuredDays({
7545
+ usage: mergeUsageDays(usageScans),
7546
+ ...workflow ? { workflow: workflow.days } : {},
7547
+ from: utcDate2(daysSinceMs),
7548
+ to: utcDate2(now)
7549
+ }),
7550
+ config
7551
+ );
7552
+ const days = selectDaysToPublish({
7553
+ local: localDays,
7554
+ manifest,
7555
+ todayUtc: utcDate2(now)
7556
+ });
6625
7557
  const body = buildSyncBody(
6626
7558
  built,
6627
7559
  config,
6628
7560
  settings.autoSync,
6629
7561
  deps.trigger,
6630
- workflow,
7562
+ active.length > 0 ? {
7563
+ aggregateVersion: MEASURED_DAYS_V1,
7564
+ utcOffsetMinutes: workflow?.utcOffsetMinutes ?? machineUtcOffsetMinutes(),
7565
+ days: days.send
7566
+ } : void 0,
6631
7567
  CLI_VERSION
6632
7568
  );
6633
7569
  const bodyJson = JSON.stringify(body);
@@ -6639,6 +7575,7 @@ async function stageSync(deps) {
6639
7575
  source,
6640
7576
  baseUrl: deps.baseUrl,
6641
7577
  scanStats,
7578
+ days,
6642
7579
  // The real terminal, so the inventory rows break where this window ends
6643
7580
  // (#217). A pipe reports nothing and the preview falls back to 80.
6644
7581
  width: process.stdout.columns
@@ -6661,7 +7598,8 @@ async function stageSync(deps) {
6661
7598
  config,
6662
7599
  token,
6663
7600
  stagedAt: now,
6664
- blockedReason
7601
+ blockedReason,
7602
+ days
6665
7603
  };
6666
7604
  }
6667
7605