@use-aistack/cli 0.8.0 → 0.9.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.9.0" : "0.0.0-dev";
8
8
 
9
9
  // src/api.ts
10
10
  var BASE_URL = process.env.AISTACK_URL || "https://aistack.to";
@@ -2133,192 +2133,628 @@ async function hasRecentFile(roots, matches, sinceMs, opts = {}) {
2133
2133
  return false;
2134
2134
  }
2135
2135
 
2136
- // ../workflow-rules/src/metricRules.ts
2137
- var METRIC_RULES_V1 = "metric-rules/v1";
2136
+ // ../workflow-rules/src/daily.ts
2137
+ var WORKFLOW_AGGREGATES_V2 = "workflow-aggregates/v2";
2138
+ var LOG_BUCKETS_V1 = "log-buckets/v1";
2139
+ var EMPTY_PHASE_TOTALS = Object.freeze({
2140
+ scout: 0,
2141
+ build: 0,
2142
+ verify: 0,
2143
+ handoff: 0,
2144
+ unknown: 0
2145
+ });
2146
+ var EFFORT_LEVELS = [
2147
+ "low",
2148
+ "medium",
2149
+ "high",
2150
+ "other"
2151
+ ];
2152
+ function effortLevelOf(effort) {
2153
+ switch (effort.toLowerCase()) {
2154
+ case "low":
2155
+ case "minimal":
2156
+ return "low";
2157
+ case "medium":
2158
+ return "medium";
2159
+ case "high":
2160
+ case "xhigh":
2161
+ case "max":
2162
+ case "ultra":
2163
+ return "high";
2164
+ default:
2165
+ return "other";
2166
+ }
2167
+ }
2168
+ function logBucket(value) {
2169
+ if (!(value >= 1)) return 0;
2170
+ return Math.floor(Math.log2(value)) + 1;
2171
+ }
2172
+ function bucketRange(bucket) {
2173
+ if (bucket <= 0) return { low: 0, high: 1 };
2174
+ return { low: 2 ** (bucket - 1), high: 2 ** bucket };
2175
+ }
2176
+ function bucketMid(bucket) {
2177
+ const { low, high } = bucketRange(bucket);
2178
+ return Math.sqrt(Math.max(low, 0.25) * high);
2179
+ }
2180
+ function medianBucket(buckets) {
2181
+ const total = buckets.reduce((sum, row) => sum + row.count, 0);
2182
+ if (total <= 0) return void 0;
2183
+ const sorted = [...buckets].sort((a, b) => a.bucket - b.bucket);
2184
+ const middle = (total + 1) / 2;
2185
+ let seen = 0;
2186
+ for (const row of sorted) {
2187
+ seen += row.count;
2188
+ if (seen >= middle) return row.bucket;
2189
+ }
2190
+ return sorted[sorted.length - 1]?.bucket;
2191
+ }
2138
2192
  function median(values) {
2139
2193
  if (values.length === 0) return void 0;
2140
2194
  const sorted = [...values].sort((a, b) => a - b);
2141
2195
  const mid = Math.floor(sorted.length / 2);
2142
2196
  const midValue = sorted[mid];
2143
- if (midValue === void 0) return void 0;
2144
2197
  return sorted.length % 2 === 0 ? (sorted[mid - 1] + midValue) / 2 : midValue;
2145
2198
  }
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;
2199
+ function addPhaseTotals(into, from) {
2200
+ for (const phase of Object.keys(into)) {
2201
+ into[phase] += from[phase] ?? 0;
2202
+ }
2203
+ }
2204
+ function sumBy(rows, key, add, clone) {
2205
+ const merged = /* @__PURE__ */ new Map();
2206
+ for (const row of rows) {
2207
+ const k = key(row);
2208
+ const held = merged.get(k);
2209
+ if (held) add(held, row);
2210
+ else merged.set(k, clone(row));
2211
+ }
2212
+ return [...merged.values()];
2213
+ }
2214
+ function foldCells(rows, field) {
2215
+ return sumBy(
2216
+ rows,
2217
+ (row) => `${row.weekdayUtc}:${row.hourUtc}`,
2218
+ (into, from) => {
2219
+ into[field] += from[field];
2220
+ },
2221
+ (row) => ({ ...row })
2222
+ ).sort((a, b) => a.weekdayUtc - b.weekdayUtc || a.hourUtc - b.hourUtc);
2223
+ }
2224
+ function foldModels(rows) {
2225
+ return sumBy(
2226
+ rows,
2227
+ (row) => row.model,
2228
+ (into, from) => {
2229
+ into.tokens += from.tokens;
2230
+ },
2231
+ (row) => ({ ...row })
2232
+ ).sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
2233
+ }
2234
+ function foldLengths(rows) {
2235
+ return sumBy(
2236
+ rows,
2237
+ (row) => String(row.bucket),
2238
+ (into, from) => {
2239
+ into.sessions += from.sessions;
2240
+ addPhaseTotals(into.phaseSec, from.phaseSec);
2241
+ into.merged += from.merged;
2242
+ into.verified += from.verified;
2243
+ into.mergedVerified += from.mergedVerified;
2244
+ into.openedWithScout += from.openedWithScout;
2245
+ },
2246
+ (row) => ({ ...row, phaseSec: { ...row.phaseSec } })
2247
+ ).sort((a, b) => a.bucket - b.bucket);
2248
+ }
2249
+ function foldHarnessDays(days) {
2250
+ const first = days[0];
2251
+ if (!first) throw new Error("foldHarnessDays needs at least one day");
2252
+ const versions = (values) => [...new Set(values)].sort().join(" \xB7 ");
2253
+ const out = {
2254
+ harness: first.harness,
2255
+ sessions: days.reduce((sum, day) => sum + day.sessions, 0),
2256
+ startHours: sumBy(
2257
+ days.flatMap((day) => day.startHours),
2258
+ (row) => String(row.hourUtc),
2259
+ (into, from) => {
2260
+ into.sessions += from.sessions;
2261
+ },
2262
+ (row) => ({ ...row })
2263
+ ).sort((a, b) => a.hourUtc - b.hourUtc),
2264
+ activity: foldCells(
2265
+ days.flatMap((day) => day.activity),
2266
+ "events"
2267
+ )
2268
+ };
2269
+ const phases = days.flatMap((day) => day.phase ? [day.phase] : []);
2270
+ if (phases.length > 0) {
2271
+ const phaseSec = { ...EMPTY_PHASE_TOTALS };
2272
+ const phaseEvents = { ...EMPTY_PHASE_TOTALS };
2273
+ for (const phase of phases) {
2274
+ addPhaseTotals(phaseSec, phase.phaseSec);
2275
+ addPhaseTotals(phaseEvents, phase.phaseEvents);
2276
+ }
2277
+ out.phase = {
2278
+ ruleVersion: versions(phases.map((phase) => phase.ruleVersion)),
2279
+ sessions: phases.reduce((sum, phase) => sum + phase.sessions, 0),
2280
+ phaseSec,
2281
+ phaseEvents,
2282
+ waitingSec: phases.reduce((sum, phase) => sum + phase.waitingSec, 0),
2283
+ idleSec: phases.reduce((sum, phase) => sum + phase.idleSec, 0),
2284
+ sessionsWithVerify: phases.reduce(
2285
+ (sum, phase) => sum + phase.sessionsWithVerify,
2286
+ 0
2287
+ ),
2288
+ sessionsWithHandoff: phases.reduce(
2289
+ (sum, phase) => sum + phase.sessionsWithHandoff,
2290
+ 0
2291
+ ),
2292
+ bucketRuleVersion: versions(
2293
+ phases.map((phase) => phase.bucketRuleVersion)
2294
+ ),
2295
+ lengths: foldLengths(phases.flatMap((phase) => phase.lengths))
2296
+ };
2297
+ }
2298
+ const routings = days.flatMap((day) => day.routing ? [day.routing] : []);
2299
+ if (routings.length > 0) {
2300
+ out.routing = {
2301
+ main: foldModels(routings.flatMap((routing) => routing.main)),
2302
+ subagents: foldModels(routings.flatMap((routing) => routing.subagents))
2303
+ };
2304
+ }
2305
+ const delegations = days.flatMap(
2306
+ (day) => day.delegation ? [day.delegation] : []
2307
+ );
2308
+ if (delegations.length > 0) {
2309
+ out.delegation = {
2310
+ mainToolCalls: delegations.reduce((sum, d) => sum + d.mainToolCalls, 0),
2311
+ subagentToolCalls: delegations.reduce(
2312
+ (sum, d) => sum + d.subagentToolCalls,
2313
+ 0
2314
+ ),
2315
+ widestFanOut: Math.max(...delegations.map((d) => d.widestFanOut)),
2316
+ mostSubagents: Math.max(...delegations.map((d) => d.mostSubagents))
2317
+ };
2318
+ }
2319
+ const efforts = days.flatMap((day) => day.effort ?? []);
2320
+ if (days.some((day) => day.effort)) {
2321
+ out.effort = sumBy(
2322
+ efforts,
2323
+ (row) => row.level,
2324
+ (into, from) => {
2325
+ into.turns += from.turns;
2326
+ },
2327
+ (row) => ({ ...row })
2328
+ ).sort(
2329
+ (a, b) => EFFORT_LEVELS.indexOf(a.level) - EFFORT_LEVELS.indexOf(b.level)
2330
+ );
2331
+ }
2332
+ const thinkings = days.flatMap((day) => day.thinking ? [day.thinking] : []);
2333
+ if (thinkings.length > 0) {
2334
+ out.thinking = {
2335
+ thinkingTokens: thinkings.reduce((sum, t) => sum + t.thinkingTokens, 0),
2336
+ responseTokens: thinkings.reduce((sum, t) => sum + t.responseTokens, 0)
2337
+ };
2338
+ }
2339
+ const durations = days.flatMap(
2340
+ (day) => day.turnDurations ? [day.turnDurations] : []
2341
+ );
2342
+ if (durations.length > 0) {
2343
+ out.turnDurations = {
2344
+ bucketRuleVersion: versions(durations.map((d) => d.bucketRuleVersion)),
2345
+ buckets: sumBy(
2346
+ durations.flatMap((d) => d.buckets),
2347
+ (row) => String(row.bucket),
2348
+ (into, from) => {
2349
+ into.turns += from.turns;
2350
+ },
2351
+ (row) => ({ ...row })
2352
+ ).sort((a, b) => a.bucket - b.bucket)
2353
+ };
2354
+ }
2355
+ const questions = days.flatMap(
2356
+ (day) => day.questions ? [day.questions] : []
2357
+ );
2358
+ if (questions.length > 0) {
2359
+ out.questions = {
2360
+ asked: questions.reduce((sum, q) => sum + q.asked, 0),
2361
+ turns: questions.reduce((sum, q) => sum + q.turns, 0)
2362
+ };
2363
+ }
2364
+ if (days.some((day) => day.webSearches !== void 0)) {
2365
+ out.webSearches = days.reduce(
2366
+ (sum, day) => sum + (day.webSearches ?? 0),
2367
+ 0
2368
+ );
2369
+ }
2370
+ return out;
2371
+ }
2372
+ function foldGitDays(days) {
2373
+ const versions = (values) => [...new Set(values)].sort().join(" \xB7 ");
2374
+ return {
2375
+ testFileRuleVersion: versions(days.map((d) => d.testFileRuleVersion)),
2376
+ fileTypeRuleVersion: versions(days.map((d) => d.fileTypeRuleVersion)),
2377
+ commitSetRuleVersion: versions(days.map((d) => d.commitSetRuleVersion)),
2378
+ commits: days.reduce((sum, d) => sum + d.commits, 0),
2379
+ lateNightCommits: days.reduce((sum, d) => sum + d.lateNightCommits, 0),
2380
+ additions: days.reduce((sum, d) => sum + d.additions, 0),
2381
+ removals: days.reduce((sum, d) => sum + d.removals, 0),
2382
+ changedLinesPerCommit: days.flatMap((d) => [...d.changedLinesPerCommit]),
2383
+ testFileCommits: days.reduce((sum, d) => sum + d.testFileCommits, 0),
2384
+ changedLinesByExtension: sumBy(
2385
+ days.flatMap((d) => d.changedLinesByExtension),
2386
+ (row) => row.extension,
2387
+ (into, from) => {
2388
+ into.changedLines += from.changedLines;
2389
+ },
2390
+ (row) => ({ ...row })
2391
+ ).sort((a, b) => a.extension.localeCompare(b.extension)),
2392
+ withheldExtensionLines: days.reduce(
2393
+ (sum, d) => sum + d.withheldExtensionLines,
2394
+ 0
2395
+ ),
2396
+ weekdayHourCells: foldCells(
2397
+ days.flatMap((d) => d.weekdayHourCells),
2398
+ "commits"
2399
+ )
2400
+ };
2401
+ }
2402
+ function foldWorkflowDays(days, options) {
2403
+ if (days.length === 0) return void 0;
2404
+ const byHarness = /* @__PURE__ */ new Map();
2405
+ for (const day of days) {
2406
+ for (const harness of day.harnesses) {
2407
+ const held = byHarness.get(harness.harness) ?? [];
2408
+ held.push(harness);
2409
+ byHarness.set(harness.harness, held);
2410
+ }
2411
+ }
2412
+ const parallelProjectDays = days.flatMap(
2413
+ (day) => day.parallelProjects === void 0 ? [] : [day.parallelProjects]
2414
+ );
2415
+ const webSearchDays = days.filter(
2416
+ (day) => day.harnesses.some((harness) => harness.webSearches !== void 0)
2417
+ ).length;
2418
+ return {
2419
+ aggregateVersion: options.aggregateVersion,
2420
+ ...options.utcOffsetMinutes === void 0 ? {} : { utcOffsetMinutes: options.utcOffsetMinutes },
2421
+ dates: [...new Set(days.map((day) => day.date))].sort(),
2422
+ harnesses: [...byHarness.values()].map(foldHarnessDays).sort((a, b) => a.harness.localeCompare(b.harness)),
2423
+ git: foldGitDays(days.map((day) => day.git)),
2424
+ ...parallelProjectDays.length === 0 ? {} : { parallelProjects: Math.max(...parallelProjectDays) },
2425
+ parallelProjectDays,
2426
+ webSearchDays
2427
+ };
2428
+ }
2429
+
2430
+ // ../workflow-rules/src/reading.ts
2431
+ function playbookHarnesses(reading) {
2432
+ return reading.harnesses.filter((harness) => harness.phase !== void 0);
2433
+ }
2434
+ function startHoursUtc(reading) {
2435
+ const counts = /* @__PURE__ */ new Map();
2436
+ for (const harness of reading.harnesses) {
2437
+ for (const cell of harness.startHours) {
2438
+ counts.set(cell.hourUtc, (counts.get(cell.hourUtc) ?? 0) + cell.sessions);
2439
+ }
2440
+ }
2441
+ return counts;
2442
+ }
2443
+ function ownerLocalHour(hourUtc, offsetMinutes) {
2444
+ return Math.floor(((hourUtc * 60 + offsetMinutes) / 60 % 24 + 24) % 24);
2445
+ }
2446
+ function modalStartHour(reading) {
2447
+ const offsetMinutes = reading.utcOffsetMinutes;
2448
+ if (offsetMinutes === void 0) return void 0;
2449
+ const counts = /* @__PURE__ */ new Map();
2450
+ for (const [hourUtc, sessions] of startHoursUtc(reading)) {
2451
+ const hour = ownerLocalHour(hourUtc, offsetMinutes);
2452
+ counts.set(hour, (counts.get(hour) ?? 0) + sessions);
2453
+ }
2454
+ if (counts.size === 0) return void 0;
2455
+ return [...counts.entries()].sort(
2456
+ (a, b) => b[1] - a[1] || a[0] - b[0]
2457
+ )[0]?.[0];
2150
2458
  }
2459
+
2460
+ // ../workflow-rules/src/componentRules.ts
2461
+ var COMPONENT_RULES_V2 = "component-rules/v2";
2462
+ var gitCoverage = () => 1;
2463
+ function harnessShare(input, counts) {
2464
+ const synced = input.reading.harnesses.length;
2465
+ if (synced === 0) return 0;
2466
+ return counts(input) / synced;
2467
+ }
2468
+ function topShare(entries) {
2469
+ const total = entries.reduce((sum, entry) => sum + entry.value, 0);
2470
+ if (total <= 0) return void 0;
2471
+ const top = Math.max(...entries.map((entry) => entry.value));
2472
+ return top / total;
2473
+ }
2474
+ var COMPONENT_RULES = [
2475
+ {
2476
+ id: "activity-heatmap",
2477
+ version: COMPONENT_RULES_V2,
2478
+ label: "of events fall in the three busiest hours of the day",
2479
+ unit: "share",
2480
+ // Three of twenty-four hours is an eighth of the clock. A day spread evenly
2481
+ // lands near it; a night owl runs far above it.
2482
+ band: { low: 0.125, high: 0.35 },
2483
+ evaluate: ({ reading }) => {
2484
+ const byHour = /* @__PURE__ */ new Map();
2485
+ for (const harness of reading.harnesses) {
2486
+ for (const cell of harness.activity) {
2487
+ byHour.set(
2488
+ cell.hourUtc,
2489
+ (byHour.get(cell.hourUtc) ?? 0) + cell.events
2490
+ );
2491
+ }
2492
+ }
2493
+ const total = [...byHour.values()].reduce((sum, n) => sum + n, 0);
2494
+ if (total <= 0) return void 0;
2495
+ const busiest = [...byHour.values()].sort((a, b) => b - a).slice(0, 3);
2496
+ return busiest.reduce((sum, n) => sum + n, 0) / total;
2497
+ },
2498
+ coverage: (input) => harnessShare(
2499
+ input,
2500
+ ({ reading }) => reading.harnesses.filter((harness) => harness.activity.length > 0).length
2501
+ )
2502
+ },
2503
+ {
2504
+ id: "start-hours",
2505
+ version: COMPONENT_RULES_V2,
2506
+ label: "is the most common start hour",
2507
+ unit: "hour",
2508
+ // A band on a clock face means little; the row is never ranked, and the
2509
+ // figure is a position rather than a size. Kept for shape.
2510
+ band: { low: 9, high: 18 },
2511
+ evaluate: ({ reading }) => modalStartHour(reading),
2512
+ coverage: (input) => harnessShare(
2513
+ input,
2514
+ ({ reading }) => reading.harnesses.filter((harness) => harness.startHours.length > 0).length
2515
+ )
2516
+ },
2517
+ {
2518
+ id: "phase-playbook",
2519
+ version: COMPONENT_RULES_V2,
2520
+ label: "median measured session",
2521
+ unit: "minutes",
2522
+ band: { low: 10, high: 60 },
2523
+ evaluate: ({ reading }) => {
2524
+ const bucket = medianBucket(
2525
+ playbookHarnesses(reading).flatMap(
2526
+ (harness) => (harness.phase?.lengths ?? []).map((row) => ({
2527
+ bucket: row.bucket,
2528
+ count: row.sessions
2529
+ }))
2530
+ )
2531
+ );
2532
+ return bucket === void 0 ? void 0 : bucketMid(bucket);
2533
+ },
2534
+ coverage: (input) => harnessShare(input, ({ reading }) => playbookHarnesses(reading).length)
2535
+ },
2536
+ {
2537
+ id: "git-ledger",
2538
+ version: COMPONENT_RULES_V2,
2539
+ label: "of changed lines are removals",
2540
+ unit: "share",
2541
+ // Most work adds more than it takes away. A ledger that removes as much as
2542
+ // it adds is the surprising one, and so is one that never removes.
2543
+ band: { low: 0.15, high: 0.35 },
2544
+ evaluate: ({ reading }) => {
2545
+ const changed = reading.git.additions + reading.git.removals;
2546
+ return changed > 0 ? reading.git.removals / changed : void 0;
2547
+ },
2548
+ coverage: gitCoverage
2549
+ },
2550
+ {
2551
+ id: "coding-languages",
2552
+ version: COMPONENT_RULES_V2,
2553
+ label: "of changed lines are one file type",
2554
+ unit: "share",
2555
+ band: { low: 0.35, high: 0.7 },
2556
+ evaluate: ({ reading }) => {
2557
+ const named = reading.git.changedLinesByExtension.map((row) => ({
2558
+ value: row.changedLines
2559
+ }));
2560
+ const total = named.reduce((sum, row) => sum + row.value, 0) + reading.git.withheldExtensionLines;
2561
+ if (total <= 0 || named.length === 0) return void 0;
2562
+ return Math.max(...named.map((row) => row.value)) / total;
2563
+ },
2564
+ coverage: gitCoverage
2565
+ },
2566
+ {
2567
+ id: "kit",
2568
+ version: COMPONENT_RULES_V2,
2569
+ label: "of skill and MCP calls go to one artifact",
2570
+ unit: "share",
2571
+ band: { low: 0.15, high: 0.4 },
2572
+ evaluate: ({ kit }) => {
2573
+ if (!kit) return void 0;
2574
+ const byName = /* @__PURE__ */ new Map();
2575
+ for (const harness of kit) {
2576
+ for (const atom of [...harness.skills, ...harness.mcpServers]) {
2577
+ byName.set(atom.name, (byName.get(atom.name) ?? 0) + atom.callShare);
2578
+ }
2579
+ }
2580
+ return topShare([...byName.values()].map((share) => ({ value: share })));
2581
+ },
2582
+ coverage: (input) => harnessShare(
2583
+ input,
2584
+ ({ kit }) => (kit ?? []).filter(
2585
+ (harness) => harness.skills.length > 0 || harness.mcpServers.length > 0
2586
+ ).length
2587
+ )
2588
+ },
2589
+ {
2590
+ id: "model-routing",
2591
+ version: COMPONENT_RULES_V2,
2592
+ label: "of main-loop tokens run on one model",
2593
+ unit: "share",
2594
+ band: { low: 0.4, high: 0.85 },
2595
+ evaluate: ({ reading }) => {
2596
+ const main = reading.harnesses.flatMap((harness) => [
2597
+ ...harness.routing?.main ?? []
2598
+ ]);
2599
+ const byModel = /* @__PURE__ */ new Map();
2600
+ for (const row of main) {
2601
+ byModel.set(row.model, (byModel.get(row.model) ?? 0) + row.tokens);
2602
+ }
2603
+ return topShare(
2604
+ [...byModel.values()].map((tokens) => ({ value: tokens }))
2605
+ );
2606
+ },
2607
+ coverage: (input) => harnessShare(
2608
+ input,
2609
+ ({ reading }) => reading.harnesses.filter((harness) => harness.routing).length
2610
+ )
2611
+ },
2612
+ {
2613
+ id: "delegation",
2614
+ version: COMPONENT_RULES_V2,
2615
+ label: "of tool calls run inside a subagent",
2616
+ unit: "share",
2617
+ band: { low: 0, high: 0.3 },
2618
+ evaluate: ({ reading }) => {
2619
+ let main = 0;
2620
+ let subagents = 0;
2621
+ for (const harness of reading.harnesses) {
2622
+ main += harness.delegation?.mainToolCalls ?? 0;
2623
+ subagents += harness.delegation?.subagentToolCalls ?? 0;
2624
+ }
2625
+ const total = main + subagents;
2626
+ return total > 0 ? subagents / total : void 0;
2627
+ },
2628
+ coverage: (input) => harnessShare(
2629
+ input,
2630
+ ({ reading }) => reading.harnesses.filter((harness) => harness.delegation).length
2631
+ )
2632
+ }
2633
+ ];
2634
+
2635
+ // ../workflow-rules/src/metricRules.ts
2636
+ var METRIC_RULES_V2 = "metric-rules/v2";
2151
2637
  var METRIC_RULES = [
2152
2638
  {
2153
2639
  id: "late-night-commits",
2154
- version: METRIC_RULES_V1,
2640
+ version: METRIC_RULES_V2,
2155
2641
  label: "of commits land between 23:00 and 03:00",
2156
2642
  kind: "exact",
2157
2643
  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.
2644
+ counts: "all",
2161
2645
  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;
2646
+ evaluate: (reading) => {
2647
+ const git = reading.git;
2648
+ if (git.commits === 0) return void 0;
2649
+ return git.lateNightCommits / git.commits;
2166
2650
  }
2167
2651
  },
2168
2652
  {
2169
2653
  id: "parallel-projects",
2170
- version: METRIC_RULES_V1,
2654
+ version: METRIC_RULES_V2,
2171
2655
  label: "projects run in parallel on a median active day",
2172
2656
  kind: "proxy",
2173
2657
  unit: "count",
2174
- harnessSupport: "all",
2658
+ counts: "all",
2175
2659
  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
- )
2660
+ evaluate: (reading) => median(reading.parallelProjectDays)
2196
2661
  },
2197
2662
  {
2198
2663
  id: "thinking-share",
2199
- version: METRIC_RULES_V1,
2664
+ version: METRIC_RULES_V2,
2200
2665
  label: "of response tokens are thinking",
2201
2666
  kind: "proxy",
2202
2667
  unit: "share",
2203
- harnessSupport: ["codex", "opencode", "pi-mono"],
2668
+ counts: (harness) => harness.thinking !== void 0,
2204
2669
  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;
2670
+ evaluate: (reading) => {
2210
2671
  let thinking = 0;
2211
2672
  let response = 0;
2212
- for (const s of sessions) {
2213
- thinking += s.thinkingTokens ?? 0;
2214
- response += s.responseTokens ?? 0;
2673
+ for (const harness of reading.harnesses) {
2674
+ thinking += harness.thinking?.thinkingTokens ?? 0;
2675
+ response += harness.thinking?.responseTokens ?? 0;
2215
2676
  }
2216
2677
  return response > 0 ? thinking / response : void 0;
2217
2678
  }
2218
2679
  },
2219
2680
  {
2220
- id: "high-effort-turns",
2221
- version: METRIC_RULES_V1,
2681
+ id: "effort-levels",
2682
+ version: METRIC_RULES_V2,
2222
2683
  label: "of turns run at high effort",
2223
2684
  kind: "exact",
2224
2685
  unit: "share",
2225
- // Only Claude Code and Codex record an effort field (#196 research).
2226
- harnessSupport: ["claude-code", "codex"],
2686
+ counts: (harness) => harness.effort !== void 0,
2227
2687
  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;
2688
+ evaluate: (reading) => {
2231
2689
  let high = 0;
2232
2690
  let total = 0;
2233
- for (const s of sessions) {
2234
- high += s.effortTurns?.high ?? 0;
2235
- total += s.effortTurns?.total ?? 0;
2691
+ for (const harness of reading.harnesses) {
2692
+ for (const row of harness.effort ?? []) {
2693
+ total += row.turns;
2694
+ if (row.level === "high") high += row.turns;
2695
+ }
2236
2696
  }
2237
2697
  return total > 0 ? high / total : void 0;
2238
2698
  }
2239
2699
  },
2240
2700
  {
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",
2701
+ id: "turn-duration",
2702
+ version: METRIC_RULES_V2,
2703
+ label: "median turn duration",
2257
2704
  kind: "exact",
2258
2705
  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;
2706
+ counts: (harness) => harness.turnDurations !== void 0,
2707
+ band: { low: 0.25, high: 2 },
2708
+ evaluate: (reading) => {
2709
+ const bucket = medianBucket(
2710
+ reading.harnesses.flatMap(
2711
+ (harness) => (harness.turnDurations?.buckets ?? []).map((row) => ({
2712
+ bucket: row.bucket,
2713
+ count: row.turns
2714
+ }))
2715
+ )
2716
+ );
2717
+ return bucket === void 0 ? void 0 : bucketMid(bucket) / 60;
2266
2718
  }
2267
2719
  },
2268
2720
  {
2269
2721
  id: "question-back-share",
2270
- version: METRIC_RULES_V1,
2722
+ version: METRIC_RULES_V2,
2271
2723
  label: "of turns end with a question back to the human",
2272
2724
  kind: "proxy",
2273
2725
  unit: "share",
2274
- harnessSupport: ["claude-code", "codex", "opencode"],
2726
+ counts: (harness) => harness.questions !== void 0,
2275
2727
  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;
2728
+ evaluate: (reading) => {
2281
2729
  let asked = 0;
2282
2730
  let turns = 0;
2283
- for (const s of sessions) {
2284
- asked += s.questionBackTurns ?? 0;
2285
- turns += s.totalTurns ?? 0;
2731
+ for (const harness of reading.harnesses) {
2732
+ asked += harness.questions?.asked ?? 0;
2733
+ turns += harness.questions?.turns ?? 0;
2286
2734
  }
2287
2735
  return turns > 0 ? asked / turns : void 0;
2288
2736
  }
2289
2737
  },
2290
2738
  {
2291
2739
  id: "web-searches-per-active-day",
2292
- version: METRIC_RULES_V1,
2740
+ version: METRIC_RULES_V2,
2293
2741
  label: "web searches per active day, inside the harness",
2294
2742
  kind: "proxy",
2295
2743
  unit: "count",
2296
- harnessSupport: ["claude-code", "codex", "opencode"],
2744
+ counts: (harness) => harness.webSearches !== void 0,
2297
2745
  band: { low: 0, high: 4 },
2298
- evaluate: (facts) => {
2299
- const days = facts.activeDays?.filter(
2300
- (day) => day.webSearches !== void 0
2746
+ evaluate: (reading) => {
2747
+ if (reading.webSearchDays === 0) return void 0;
2748
+ const total = reading.harnesses.reduce(
2749
+ (sum, harness) => sum + (harness.webSearches ?? 0),
2750
+ 0
2301
2751
  );
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;
2752
+ return total / reading.webSearchDays;
2305
2753
  }
2306
2754
  }
2307
2755
  ];
2308
2756
 
2309
2757
  // ../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
2758
  var PHASES = [
2323
2759
  "scout",
2324
2760
  "build",
@@ -2327,39 +2763,6 @@ var PHASES = [
2327
2763
  "unknown"
2328
2764
  ];
2329
2765
 
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
2766
  // ../workflow-rules/src/phaseRules.ts
2364
2767
  var PHASE_RULES_V1 = "phase-rules/v1";
2365
2768
  var UNKNOWN_GATE = 0.2;
@@ -2778,8 +3181,49 @@ function deriveSessionPhases(events, ruleSet = PHASE_RULES_V1, harness) {
2778
3181
  };
2779
3182
  }
2780
3183
 
2781
- // ../workflow-rules/src/playbook.ts
2782
- var MIN_PER_SEC = 1 / 60;
3184
+ // ../workflow-rules/src/workflowRows.ts
3185
+ function metricRowId(metricId) {
3186
+ return `metric:${metricId}`;
3187
+ }
3188
+ function componentRowId(componentId) {
3189
+ return `component:${componentId}`;
3190
+ }
3191
+ var WORKFLOW_ROW_ORDER = [
3192
+ {
3193
+ rowId: "component:activity-heatmap",
3194
+ name: "When work happens",
3195
+ flat: false
3196
+ },
3197
+ { rowId: "component:start-hours", name: "Session start times", flat: false },
3198
+ {
3199
+ rowId: "metric:late-night-commits",
3200
+ name: "Late-night commits",
3201
+ flat: true
3202
+ },
3203
+ { rowId: "component:phase-playbook", name: "Session length", flat: false },
3204
+ { rowId: "component:git-ledger", name: "Lines changed", flat: false },
3205
+ { rowId: "component:coding-languages", name: "Languages", flat: false },
3206
+ { rowId: "component:kit", name: "Skills and MCP", flat: false },
3207
+ { rowId: "component:model-routing", name: "Models used", flat: false },
3208
+ { rowId: "component:delegation", name: "Subagents", flat: false },
3209
+ { rowId: "metric:effort-levels", name: "Effort levels", flat: false },
3210
+ { rowId: "metric:thinking-share", name: "Thinking tokens", flat: false },
3211
+ { rowId: "metric:turn-duration", name: "Turn length", flat: false },
3212
+ { rowId: "metric:question-back-share", name: "Questions asked", flat: true },
3213
+ {
3214
+ rowId: "metric:web-searches-per-active-day",
3215
+ name: "Web searches",
3216
+ flat: true
3217
+ },
3218
+ { rowId: "metric:parallel-projects", name: "Parallel projects", flat: true }
3219
+ ];
3220
+ var ORDER_INDEX = new Map(
3221
+ WORKFLOW_ROW_ORDER.map((row, index) => [row.rowId, index])
3222
+ );
3223
+ var KNOWN_ROW_IDS = /* @__PURE__ */ new Set([
3224
+ ...METRIC_RULES.map((rule) => metricRowId(rule.id)),
3225
+ ...COMPONENT_RULES.map((rule) => componentRowId(rule.id))
3226
+ ]);
2783
3227
 
2784
3228
  // src/harness/shared/window.ts
2785
3229
  var DEFAULT_WINDOW_DAYS = 30;
@@ -3040,19 +3484,8 @@ function buildPayload(input) {
3040
3484
  function toPayloadWorkflow(extraction) {
3041
3485
  return {
3042
3486
  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
3487
+ utcOffsetMinutes: extraction.utcOffsetMinutes,
3488
+ days: extraction.days
3056
3489
  };
3057
3490
  }
3058
3491
  function mergeKeptPrivate(halves) {
@@ -3085,7 +3518,7 @@ function buildSyncBody(built, syncConfig, autoSync, trigger = "manual", workflow
3085
3518
  }
3086
3519
 
3087
3520
  // src/workflow/reducer.ts
3088
- var WORKFLOW_AGGREGATE_VERSION = "workflow-aggregates/v1";
3521
+ var WORKFLOW_AGGREGATE_VERSION = WORKFLOW_AGGREGATES_V2;
3089
3522
  function createWorkflowLocalSources() {
3090
3523
  return { projectWorkspaces: /* @__PURE__ */ new Set(), activeProjectDays: /* @__PURE__ */ new Map() };
3091
3524
  }
@@ -3097,10 +3530,10 @@ var emptyPhase = () => ({
3097
3530
  unknown: 0
3098
3531
  });
3099
3532
  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
3533
  var bump2 = (map, key, amount = 1) => {
3102
3534
  map.set(key, (map.get(key) ?? 0) + amount);
3103
3535
  };
3536
+ var utcDateOf = (ms) => new Date(ms).toISOString().slice(0, 10);
3104
3537
  var PHASE_RANK = {
3105
3538
  verify: 4,
3106
3539
  handoff: 3,
@@ -3145,18 +3578,9 @@ function reduceEventBatches(recorded, harness) {
3145
3578
  }
3146
3579
  return output;
3147
3580
  }
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
- };
3581
+ var hasVerifyRun = (events, harness) => events.some(
3582
+ (event) => deriveSessionPhases([event], PHASE_RULES_V1, harness).phaseEvents.verify > 0
3583
+ );
3160
3584
  function shellIncludes(arg, head) {
3161
3585
  return arg.split(/(?:&&|\|\||;|\|)/).some((part) => part.trim() === head || part.trim().startsWith(`${head} `));
3162
3586
  }
@@ -3174,10 +3598,47 @@ function sessionState() {
3174
3598
  lastTs: void 0
3175
3599
  };
3176
3600
  }
3601
+ function dayState() {
3602
+ return {
3603
+ sessions: 0,
3604
+ startHours: /* @__PURE__ */ new Map(),
3605
+ phase: {
3606
+ sessions: 0,
3607
+ phaseSec: emptyPhase(),
3608
+ phaseEvents: emptyPhase(),
3609
+ waitingSec: 0,
3610
+ idleSec: 0,
3611
+ sessionsWithVerify: 0,
3612
+ sessionsWithHandoff: 0,
3613
+ lengths: /* @__PURE__ */ new Map()
3614
+ },
3615
+ routing: { main: /* @__PURE__ */ new Map(), subagents: /* @__PURE__ */ new Map() },
3616
+ hasRouting: false,
3617
+ delegation: {
3618
+ mainToolCalls: 0,
3619
+ subagentToolCalls: 0,
3620
+ widestFanOut: 0,
3621
+ mostSubagents: 0
3622
+ },
3623
+ hasDelegation: false,
3624
+ activity: /* @__PURE__ */ new Map(),
3625
+ effort: /* @__PURE__ */ new Map(),
3626
+ hasEffort: false,
3627
+ thinking: { thinkingTokens: 0, responseTokens: 0 },
3628
+ hasThinking: false,
3629
+ turnDurations: /* @__PURE__ */ new Map(),
3630
+ hasDurations: false,
3631
+ questions: { asked: 0, turns: 0 },
3632
+ hasQuestions: false,
3633
+ webSearches: 0,
3634
+ hasWebSearches: false
3635
+ };
3636
+ }
3177
3637
  function createHarnessWorkflowReducer(harness, localSources = createWorkflowLocalSources()) {
3178
3638
  const sessions = /* @__PURE__ */ new Map();
3179
- const activity = /* @__PURE__ */ new Map();
3639
+ const eventCells = /* @__PURE__ */ new Map();
3180
3640
  const webSearchesByDate = /* @__PURE__ */ new Map();
3641
+ const eventDates = /* @__PURE__ */ new Set();
3181
3642
  let finished;
3182
3643
  const getSession = (key) => {
3183
3644
  let state = sessions.get(key);
@@ -3197,7 +3658,7 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3197
3658
  state.parentSession ??= observation.parentSession;
3198
3659
  state.sidechain ||= observation.sidechain === true;
3199
3660
  const at = new Date(observation.tsMs);
3200
- const date = at.toISOString().slice(0, 10);
3661
+ const date = utcDateOf(observation.tsMs);
3201
3662
  if (observation.projectWorkspace) {
3202
3663
  state.projectWorkspaces.add(observation.projectWorkspace);
3203
3664
  localSources.projectWorkspaces.add(observation.projectWorkspace);
@@ -3208,7 +3669,10 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3208
3669
  event: [observation.tsMs, observation.tool, arg],
3209
3670
  ...observation.batchId ? { batchId: observation.batchId } : {}
3210
3671
  });
3211
- bump2(activity, `${at.getUTCDay()}:${at.getUTCHours()}`);
3672
+ eventDates.add(date);
3673
+ const cells = eventCells.get(date) ?? /* @__PURE__ */ new Map();
3674
+ bump2(cells, `${at.getUTCDay()}:${at.getUTCHours()}`);
3675
+ eventCells.set(date, cells);
3212
3676
  if (["WebSearch", "web_search", "websearch"].includes(observation.tool))
3213
3677
  bump2(webSearchesByDate, date);
3214
3678
  } else if (observation.type === "response") {
@@ -3234,135 +3698,118 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3234
3698
  },
3235
3699
  finish() {
3236
3700
  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()
3701
+ const days = /* @__PURE__ */ new Map();
3702
+ const dayOf = (date) => {
3703
+ let state = days.get(date);
3704
+ if (!state) {
3705
+ state = dayState();
3706
+ days.set(date, state);
3707
+ }
3708
+ return state;
3244
3709
  };
3245
- let waitingSec = 0;
3246
- let idleSec = 0;
3247
- let mainToolCalls = 0;
3248
- let subagentToolCalls = 0;
3710
+ const windowPhaseSec = emptyPhase();
3249
3711
  let phaseSessionCount = 0;
3250
3712
  for (const state of sessions.values()) {
3713
+ if (state.firstTs === void 0) continue;
3714
+ const day = dayOf(utcDateOf(state.firstTs));
3251
3715
  const events = reduceEventBatches(state.events, harness);
3252
3716
  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
3717
+ day.sessions++;
3718
+ const startHour = new Date(state.firstTs).getUTCHours();
3719
+ day.startHours.set(startHour, (day.startHours.get(startHour) ?? 0) + 1);
3720
+ const phases = deriveSessionPhases(events, PHASE_RULES_V1, harness);
3721
+ if (state.events.length > 0) phaseSessionCount++;
3722
+ day.phase.sessions++;
3723
+ for (const phase of PHASES) {
3724
+ day.phase.phaseSec[phase] += phases.phaseSec[phase];
3725
+ day.phase.phaseEvents[phase] += phases.phaseEvents[phase];
3726
+ windowPhaseSec[phase] += phases.phaseSec[phase];
3727
+ }
3728
+ day.phase.waitingSec += phases.waitingSec;
3729
+ day.phase.idleSec += phases.idleSec;
3730
+ if (phases.phaseEvents.verify > 0) day.phase.sessionsWithVerify++;
3731
+ if (phases.phaseEvents.handoff > 0) day.phase.sessionsWithHandoff++;
3732
+ const measuredSec = PHASES.reduce(
3733
+ (sum, phase) => sum + phases.phaseSec[phase],
3734
+ 0
3266
3735
  );
3267
- const durations = responses.flatMap(
3268
- (response) => response.durationSec === void 0 ? [] : [response.durationSec]
3736
+ const bucket = logBucket(measuredSec / 60);
3737
+ const merged = events.some(
3738
+ ([, tool, arg]) => ["Bash", "bash", "shell", "local_shell", "exec_command"].includes(
3739
+ tool
3740
+ ) && shellIncludes(arg, "gh pr merge")
3269
3741
  );
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
- }
3742
+ const verified = hasVerifyRun(events, harness);
3743
+ const openedWithScout = (events[0] ? deriveSessionPhases([events[0]], PHASE_RULES_V1, harness).phaseEvents.scout : 0) > 0;
3744
+ const length = day.phase.lengths.get(bucket) ?? {
3745
+ bucket,
3746
+ sessions: 0,
3747
+ phaseSec: emptyPhase(),
3748
+ merged: 0,
3749
+ verified: 0,
3750
+ mergedVerified: 0,
3751
+ openedWithScout: 0
3289
3752
  };
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);
3753
+ length.sessions++;
3754
+ for (const phase of PHASES)
3755
+ length.phaseSec[phase] += phases.phaseSec[phase];
3756
+ if (merged) length.merged++;
3757
+ if (verified) length.verified++;
3758
+ if (merged && verified) length.mergedVerified++;
3759
+ if (openedWithScout) length.openedWithScout++;
3760
+ day.phase.lengths.set(bucket, length);
3300
3761
  const routing = state.sidechain || state.parentSession ? "subagents" : "main";
3301
3762
  for (const response of responses) {
3302
3763
  if (!response.model) continue;
3764
+ day.hasRouting = true;
3303
3765
  bump2(
3304
- modelTokens[routing],
3766
+ day.routing[routing],
3305
3767
  response.model,
3306
3768
  response.routingTokens ?? response.responseTokens ?? 0
3307
3769
  );
3308
3770
  }
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];
3771
+ if (routing === "subagents") {
3772
+ day.delegation.subagentToolCalls += state.events.length;
3773
+ day.hasDelegation ||= state.events.length > 0;
3774
+ } else day.delegation.mainToolCalls += state.events.length;
3775
+ for (const response of responses) {
3776
+ if (response.effort) {
3777
+ day.hasEffort = true;
3778
+ const level = effortLevelOf(response.effort);
3779
+ day.effort.set(level, (day.effort.get(level) ?? 0) + 1);
3780
+ }
3781
+ if (response.thinkingTokens !== void 0) {
3782
+ day.hasThinking = true;
3783
+ day.thinking.thinkingTokens += response.thinkingTokens;
3784
+ day.thinking.responseTokens += response.responseTokens ?? 0;
3785
+ }
3786
+ if (response.durationSec !== void 0) {
3787
+ day.hasDurations = true;
3788
+ const durationBucket = logBucket(response.durationSec);
3789
+ day.turnDurations.set(
3790
+ durationBucket,
3791
+ (day.turnDurations.get(durationBucket) ?? 0) + 1
3792
+ );
3793
+ }
3794
+ }
3795
+ if (harness !== "pi-mono") {
3796
+ day.hasQuestions = true;
3797
+ day.questions.turns += state.turns.size;
3798
+ day.questions.asked += [...state.turns.values()].filter(
3799
+ Boolean
3800
+ ).length;
3316
3801
  }
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
3802
  }
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;
3803
+ for (const date of eventDates) {
3804
+ const day = dayOf(date);
3805
+ for (const [key, events] of eventCells.get(date) ?? []) {
3806
+ bump2(day.activity, key, events);
3807
+ }
3808
+ if (harness !== "pi-mono") {
3809
+ day.hasWebSearches = true;
3810
+ day.webSearches = webSearchesByDate.get(date) ?? 0;
3363
3811
  }
3364
3812
  }
3365
- const projectsByDate = localSources.activeProjectDays;
3366
3813
  const childrenByParent = /* @__PURE__ */ new Map();
3367
3814
  for (const state of sessions.values()) {
3368
3815
  if (!state.parentSession) continue;
@@ -3370,10 +3817,16 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3370
3817
  children.push(state);
3371
3818
  childrenByParent.set(state.parentSession, children);
3372
3819
  }
3373
- let widestFanOut = 0;
3374
- let mostSubagents = 0;
3375
- for (const children of childrenByParent.values()) {
3376
- mostSubagents = Math.max(mostSubagents, children.length);
3820
+ for (const [parentKey, children] of childrenByParent) {
3821
+ const parent = sessions.get(parentKey);
3822
+ const anchor = parent?.firstTs ?? Math.min(...children.map((child) => child.firstTs ?? Infinity));
3823
+ if (!Number.isFinite(anchor)) continue;
3824
+ const day = dayOf(utcDateOf(anchor));
3825
+ day.hasDelegation = true;
3826
+ day.delegation.mostSubagents = Math.max(
3827
+ day.delegation.mostSubagents,
3828
+ children.length
3829
+ );
3377
3830
  const boundaries = children.flatMap((child) => [
3378
3831
  { ts: child.firstTs ?? 0, delta: 1 },
3379
3832
  { ts: child.lastTs ?? child.firstTs ?? 0, delta: -1 }
@@ -3382,61 +3835,109 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
3382
3835
  let active = 0;
3383
3836
  for (const boundary of boundaries) {
3384
3837
  active += boundary.delta;
3385
- widestFanOut = Math.max(widestFanOut, active);
3838
+ day.delegation.widestFanOut = Math.max(
3839
+ day.delegation.widestFanOut,
3840
+ active
3841
+ );
3386
3842
  }
3387
3843
  }
3844
+ localSources.activeProjectDays.clear();
3845
+ for (const state of sessions.values()) {
3846
+ if (state.firstTs === void 0 || state.lastTs === void 0) continue;
3847
+ let day = Date.parse(`${utcDateOf(state.firstTs)}T00:00:00Z`);
3848
+ const lastDay = Date.parse(`${utcDateOf(state.lastTs)}T00:00:00Z`);
3849
+ while (day <= lastDay) {
3850
+ const date = utcDateOf(day);
3851
+ const projects = localSources.activeProjectDays.get(date) ?? /* @__PURE__ */ new Set();
3852
+ for (const project of state.projectWorkspaces) projects.add(project);
3853
+ if (projects.size > 0)
3854
+ localSources.activeProjectDays.set(date, projects);
3855
+ day += 864e5;
3856
+ }
3857
+ }
3858
+ const attributed = PHASES.reduce(
3859
+ (sum, phase) => sum + windowPhaseSec[phase],
3860
+ 0
3861
+ );
3862
+ const unknown = attributed === 0 ? 0 : windowPhaseSec.unknown / attributed;
3863
+ const routesModels = harness === "claude-code" || harness === "opencode";
3388
3864
  const asRows = (map) => {
3389
3865
  const safe = /* @__PURE__ */ new Map();
3390
3866
  for (const [model, tokens] of map) {
3391
3867
  bump2(safe, sanitizeModelId(model), tokens);
3392
3868
  }
3393
- return [...safe].map(([model, tokens]) => ({ model, tokens }));
3869
+ return [...safe].map(([model, tokens]) => ({ model, tokens })).sort(
3870
+ (a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model)
3871
+ );
3394
3872
  };
3395
- const hasDelegation = subagentToolCalls > 0 || mostSubagents > 0 || widestFanOut > 0;
3396
3873
  finished = {
3397
3874
  aggregateVersion: WORKFLOW_AGGREGATE_VERSION,
3398
3875
  harness,
3399
- phase: {
3876
+ gate: {
3400
3877
  ruleVersion: PHASE_RULES_V1,
3401
3878
  publishable: phaseSessionCount > 0 && unknown <= UNKNOWN_GATE,
3402
3879
  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
- }))
3880
+ unknownShare: unknown
3417
3881
  },
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)
3882
+ days: [...days].sort(([a], [b]) => a.localeCompare(b)).map(([date, day]) => ({
3883
+ date,
3884
+ harness,
3885
+ sessions: day.sessions,
3886
+ startHours: [...day.startHours].map(([hourUtc, count]) => ({ hourUtc, sessions: count })).sort((a, b) => a.hourUtc - b.hourUtc),
3887
+ ...day.phase.sessions > 0 ? {
3888
+ phase: {
3889
+ ruleVersion: PHASE_RULES_V1,
3890
+ sessions: day.phase.sessions,
3891
+ phaseSec: day.phase.phaseSec,
3892
+ phaseEvents: day.phase.phaseEvents,
3893
+ waitingSec: day.phase.waitingSec,
3894
+ idleSec: day.phase.idleSec,
3895
+ sessionsWithVerify: day.phase.sessionsWithVerify,
3896
+ sessionsWithHandoff: day.phase.sessionsWithHandoff,
3897
+ bucketRuleVersion: LOG_BUCKETS_V1,
3898
+ lengths: [...day.phase.lengths.values()].sort(
3899
+ (a, b) => a.bucket - b.bucket
3900
+ )
3901
+ }
3902
+ } : {},
3903
+ ...routesModels && day.hasRouting ? {
3904
+ routing: {
3905
+ main: asRows(day.routing.main),
3906
+ subagents: asRows(day.routing.subagents)
3907
+ }
3908
+ } : {},
3909
+ ...day.hasDelegation ? { delegation: day.delegation } : {},
3910
+ activity: [...day.activity].map(([key, events]) => {
3911
+ const [weekdayUtc, hourUtc] = key.split(":").map(Number);
3912
+ return {
3913
+ weekdayUtc: weekdayUtc ?? 0,
3914
+ hourUtc: hourUtc ?? 0,
3915
+ events
3916
+ };
3917
+ }).sort(
3918
+ (a, b) => a.weekdayUtc - b.weekdayUtc || a.hourUtc - b.hourUtc
3919
+ ),
3920
+ ...day.hasEffort ? {
3921
+ effort: EFFORT_LEVELS.flatMap((level) => {
3922
+ const turns = day.effort.get(level) ?? 0;
3923
+ return turns > 0 ? [{ level, turns }] : [];
3924
+ })
3925
+ } : {},
3926
+ ...day.hasThinking ? { thinking: day.thinking } : {},
3927
+ ...day.hasDurations ? {
3928
+ turnDurations: {
3929
+ bucketRuleVersion: LOG_BUCKETS_V1,
3930
+ buckets: [...day.turnDurations].map(([bucket, turns]) => ({ bucket, turns })).sort((a, b) => a.bucket - b.bucket)
3931
+ }
3932
+ } : {},
3933
+ ...day.hasQuestions ? { questions: day.questions } : {},
3934
+ ...day.hasWebSearches ? { webSearches: day.webSearches } : {}
3935
+ }))
3436
3936
  };
3437
3937
  sessions.clear();
3438
- activity.clear();
3938
+ eventCells.clear();
3439
3939
  webSearchesByDate.clear();
3940
+ eventDates.clear();
3440
3941
  return finished;
3441
3942
  }
3442
3943
  };
@@ -5962,11 +6463,11 @@ var defaultRunner = (cwd, args) => {
5962
6463
  return null;
5963
6464
  }
5964
6465
  };
5965
- var emptyResult = () => ({
6466
+ var emptyGitDay = () => ({
5966
6467
  testFileRuleVersion: TEST_FILE_RULE_VERSION,
5967
6468
  fileTypeRuleVersion: FILE_TYPE_RULE_VERSION,
5968
6469
  commitSetRuleVersion: COMMIT_SET_RULE_VERSION,
5969
- totalCommits: 0,
6470
+ commits: 0,
5970
6471
  lateNightCommits: 0,
5971
6472
  additions: 0,
5972
6473
  removals: 0,
@@ -6116,9 +6617,25 @@ function extractGitWorkflow(options) {
6116
6617
  const root = run(directory, ["rev-parse", "--show-toplevel"])?.trim();
6117
6618
  if (root) roots.add(root);
6118
6619
  }
6119
- const result = emptyResult();
6120
- const extensionLines = /* @__PURE__ */ new Map();
6121
- const cells = /* @__PURE__ */ new Map();
6620
+ const days = /* @__PURE__ */ new Map();
6621
+ const dayOf = (date) => {
6622
+ let day = days.get(date);
6623
+ if (!day) {
6624
+ const {
6625
+ changedLinesByExtension: _extensions,
6626
+ weekdayHourCells: _cells,
6627
+ ...rest
6628
+ } = emptyGitDay();
6629
+ day = {
6630
+ ...rest,
6631
+ changedLinesPerCommit: [],
6632
+ extensionLines: /* @__PURE__ */ new Map(),
6633
+ cells: /* @__PURE__ */ new Map()
6634
+ };
6635
+ days.set(date, day);
6636
+ }
6637
+ return day;
6638
+ };
6122
6639
  const seenCommits = /* @__PURE__ */ new Set();
6123
6640
  for (const root of roots) {
6124
6641
  const history = run(root, [
@@ -6133,17 +6650,25 @@ function extractGitWorkflow(options) {
6133
6650
  let current;
6134
6651
  const finishCommit = () => {
6135
6652
  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++;
6653
+ const day = dayOf(current.date);
6654
+ day.commits++;
6655
+ day.additions += current.additions;
6656
+ day.removals += current.removals;
6657
+ day.changedLinesPerCommit.push(current.changedLines);
6658
+ if (current.touchesTest) day.testFileCommits++;
6141
6659
  const { weekdayUtc, hourUtc } = current.cell;
6142
6660
  if (isLateNight(localHour(hourUtc, options.utcOffsetMinutes))) {
6143
- result.lateNightCommits++;
6661
+ day.lateNightCommits++;
6144
6662
  }
6145
6663
  const cellKey = `${weekdayUtc}:${hourUtc}`;
6146
- cells.set(cellKey, (cells.get(cellKey) ?? 0) + 1);
6664
+ day.cells.set(cellKey, (day.cells.get(cellKey) ?? 0) + 1);
6665
+ day.withheldExtensionLines += current.withheldLines;
6666
+ for (const [extension, lines2] of current.extensionLines) {
6667
+ day.extensionLines.set(
6668
+ extension,
6669
+ (day.extensionLines.get(extension) ?? 0) + lines2
6670
+ );
6671
+ }
6147
6672
  };
6148
6673
  const fields = history.split("\0");
6149
6674
  for (let fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
@@ -6156,12 +6681,15 @@ function extractGitWorkflow(options) {
6156
6681
  const included = Number.isFinite(authoredMs) && authoredMs >= options.fromMs && authoredMs <= options.toMs && !seenCommits.has(hash);
6157
6682
  current = {
6158
6683
  included,
6684
+ date: included ? new Date(authoredMs).toISOString().slice(0, 10) : "",
6159
6685
  cell: utcCell(included ? authoredMs : 0),
6160
6686
  authored: false,
6161
6687
  additions: 0,
6162
6688
  removals: 0,
6163
6689
  changedLines: 0,
6164
- touchesTest: false
6690
+ touchesTest: false,
6691
+ withheldLines: 0,
6692
+ extensionLines: /* @__PURE__ */ new Map()
6165
6693
  };
6166
6694
  if (included) seenCommits.add(hash);
6167
6695
  continue;
@@ -6184,20 +6712,34 @@ function extractGitWorkflow(options) {
6184
6712
  if (fileChangedLines <= 0) continue;
6185
6713
  const extension = path6.extname(file).toLowerCase();
6186
6714
  if (APPROVED_EXTENSIONS.has(extension)) {
6187
- extensionLines.set(
6715
+ current.extensionLines.set(
6188
6716
  extension,
6189
- (extensionLines.get(extension) ?? 0) + fileChangedLines
6717
+ (current.extensionLines.get(extension) ?? 0) + fileChangedLines
6190
6718
  );
6191
- } else result.withheldExtensionLines += fileChangedLines;
6719
+ } else current.withheldLines += fileChangedLines;
6192
6720
  }
6193
6721
  finishCommit();
6194
6722
  }
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;
6723
+ return {
6724
+ days: [...days].sort(([a], [b]) => a.localeCompare(b)).map(([date, day]) => {
6725
+ const { extensionLines, cells, ...rest } = day;
6726
+ return {
6727
+ date,
6728
+ ...rest,
6729
+ changedLinesByExtension: [...extensionLines].map(([extension, changedLines]) => ({ extension, changedLines })).sort((a, b) => a.extension.localeCompare(b.extension)),
6730
+ weekdayHourCells: [...cells].map(([key, commits]) => {
6731
+ const [weekdayUtc, hourUtc] = key.split(":").map(Number);
6732
+ return {
6733
+ weekdayUtc: weekdayUtc ?? 0,
6734
+ hourUtc: hourUtc ?? 0,
6735
+ commits
6736
+ };
6737
+ }).sort(
6738
+ (a, b) => a.weekdayUtc - b.weekdayUtc || a.hourUtc - b.hourUtc
6739
+ )
6740
+ };
6741
+ })
6742
+ };
6201
6743
  }
6202
6744
 
6203
6745
  // src/workflow/extract.ts
@@ -6218,50 +6760,44 @@ function machineUtcOffsetMinutes(now = /* @__PURE__ */ new Date()) {
6218
6760
  return -now.getTimezoneOffset();
6219
6761
  }
6220
6762
  function buildWorkflowExtraction(harnessWorkflows, git, utcOffsetMinutes = machineUtcOffsetMinutes()) {
6763
+ const harnessDays = /* @__PURE__ */ new Map();
6221
6764
  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);
6765
+ for (const { aggregate, local } of harnessWorkflows) {
6766
+ for (const { date, ...day } of aggregate.days) {
6767
+ const rows = harnessDays.get(date) ?? [];
6768
+ const { phase, ...safe } = day;
6769
+ rows.push(
6770
+ aggregate.gate.publishable && phase ? { ...safe, phase } : safe
6771
+ );
6772
+ harnessDays.set(date, rows);
6773
+ }
6227
6774
  for (const [date, workspaces] of local.activeProjectDays) {
6228
6775
  const projects = projectDays.get(date) ?? /* @__PURE__ */ new Set();
6229
6776
  for (const project of workspaces) projects.add(project);
6230
6777
  projectDays.set(date, projects);
6231
6778
  }
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
6779
  }
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
- ];
6780
+ const gitDays = /* @__PURE__ */ new Map();
6781
+ for (const { date, ...day } of git.days) gitDays.set(date, day);
6782
+ const dates = [
6783
+ .../* @__PURE__ */ new Set([
6784
+ ...harnessDays.keys(),
6785
+ ...gitDays.keys(),
6786
+ ...projectDays.keys()
6787
+ ])
6788
+ ].sort();
6256
6789
  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
6790
+ aggregateVersion: WORKFLOW_AGGREGATES_V2,
6791
+ utcOffsetMinutes,
6792
+ days: dates.map((date) => {
6793
+ const projects = projectDays.get(date)?.size;
6794
+ return {
6795
+ date,
6796
+ harnesses: harnessDays.get(date) ?? [],
6797
+ git: gitDays.get(date) ?? emptyGitDay(),
6798
+ ...projects === void 0 ? {} : { parallelProjects: projects }
6799
+ };
6800
+ })
6265
6801
  };
6266
6802
  }
6267
6803
 
@@ -6472,16 +7008,23 @@ var MODEL_ROLLUP = 0.01;
6472
7008
  var PHASE_ORDER = ["scout", "build", "verify", "handoff", "unknown"];
6473
7009
  function workflowBlock(workflow, host) {
6474
7010
  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
- );
7011
+ const folded = foldWorkflowDays(workflow.days, {
7012
+ aggregateVersion: workflow.aggregateVersion,
7013
+ utcOffsetMinutes: workflow.utcOffsetMinutes
7014
+ });
7015
+ const harnesses = folded?.harnesses ?? [];
7016
+ const withPlaybook = harnesses.filter((h) => h.phase);
7017
+ const sessions = harnesses.reduce((a, h) => a + h.sessions, 0);
6480
7018
  const ruleVersions = [
6481
7019
  ...new Set(withPlaybook.map((h) => h.phase?.ruleVersion ?? ""))
6482
7020
  ].filter(Boolean);
6483
7021
  out.push(
6484
- `workflow ${workflow.harnesses.length} harness${workflow.harnesses.length === 1 ? "" : "es"} \xB7 ${sessions} sessions \xB7 ${workflow.aggregateVersion}`
7022
+ `workflow ${harnesses.length} harness${harnesses.length === 1 ? "" : "es"} \xB7 ${sessions} sessions \xB7 ${workflow.aggregateVersion}`
7023
+ );
7024
+ const first = folded?.dates[0];
7025
+ const last = folded?.dates.at(-1);
7026
+ out.push(
7027
+ `days ${workflow.days.length} day${workflow.days.length === 1 ? "" : "s"}${first && last ? ` \xB7 ${first} to ${last}` : ""}`
6485
7028
  );
6486
7029
  const seconds = PHASE_ORDER.map(
6487
7030
  (phase) => withPlaybook.reduce((a, h) => a + (h.phase?.phaseSec[phase] ?? 0), 0)
@@ -6493,12 +7036,9 @@ function workflowBlock(workflow, host) {
6493
7036
  ).join(" \xB7 ");
6494
7037
  out.push(` ${mix} \xB7 ${ruleVersions.join(", ")}`);
6495
7038
  }
6496
- const git = workflow.git;
6497
- out.push(
6498
- `git ${git.totalCommits} commits \xB7 ${fmtTokens(git.additions + git.removals)} lines changed`
6499
- );
7039
+ const git = folded?.git;
6500
7040
  out.push(
6501
- `metrics ${workflow.metrics.length} measured \xB7 ${[...new Set(workflow.metrics.map((m) => m.ruleVersion))].join(", ")}`
7041
+ `git ${git?.commits ?? 0} commits \xB7 ${fmtTokens((git?.additions ?? 0) + (git?.removals ?? 0))} lines changed`
6502
7042
  );
6503
7043
  out.push(` (Publish workflow is on for ${host})`);
6504
7044
  return out;