polymath-society 0.2.25 → 0.2.27

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/cli.js CHANGED
@@ -22834,6 +22834,8 @@ __export(pipeline_exports, {
22834
22834
  runPipeline: () => runPipeline,
22835
22835
  stages: () => stages,
22836
22836
  startKeepAwake: () => startKeepAwake,
22837
+ trackStageChild: () => trackStageChild,
22838
+ untrackStageChild: () => untrackStageChild,
22837
22839
  upToDateSkip: () => upToDateSkip
22838
22840
  });
22839
22841
  import { spawn as spawn7 } from "child_process";
@@ -22849,8 +22851,8 @@ function machineBudget(totalMemBytes = os8.totalmem(), cpus = os8.availableParal
22849
22851
  }
22850
22852
  function apportionHeavy(budget) {
22851
22853
  const grade = Math.max(2, Math.floor(budget * 0.45));
22852
- const digest = Math.max(1, Math.floor(budget * 0.17));
22853
- const walkthrough = Math.max(1, Math.floor(budget * 0.17));
22854
+ const digest = Math.max(1, Math.floor(budget * 0.13));
22855
+ const walkthrough = Math.max(1, Math.floor(budget * 0.25));
22854
22856
  const delegation = Math.max(1, budget - grade - digest - walkthrough - 2);
22855
22857
  return { grade, digest, walkthrough, delegation };
22856
22858
  }
@@ -22882,11 +22884,12 @@ function stages(o) {
22882
22884
  // so they run WHILE grading runs (grading is the wall-clock giant; these
22883
22885
  // finish inside its shadow). Budget: grade 8 + digest 3 + walkthrough 3 +
22884
22886
  // two single-call stages ≈ 16 peak subprocesses (measured-safe envelope).
22885
- { script: "coding-grade.mts", label: "AI-grade every substantial session", llm: true, args: gradeArgs, batch: "heavy" },
22886
- { script: "coding-day-digest.mts", label: "Day-by-day digests", llm: true, args: ["--model", narrativeModel, "--conc", String(h.digest)], out: "coding/day-digest.json", batch: "heavy" },
22887
- { script: "coding-walkthrough.mts", label: "Per-session walkthroughs", llm: true, args: ["--model", narrativeModel, "--conc", String(h.walkthrough)], batch: "heavy" },
22888
- { script: "coding-frontier-detail.mts", label: "What you're doing at the frontier", llm: true, args: [], out: "coding/frontier-detail.json", batch: "heavy" },
22889
- { script: "coding-delegation.mts", label: "Delegation patterns", llm: true, args: ["--model", model, "--conc", String(h.delegation)], out: "coding/delegation-rollup.json", batch: "heavy" },
22887
+ { script: "coding-grade.mts", label: "AI-grade every substantial session", llm: true, weight: 30, args: gradeArgs, batch: "heavy" },
22888
+ { script: "coding-day-digest.mts", label: "Day-by-day digests", llm: true, weight: 17, args: ["--model", narrativeModel, "--conc", String(h.digest)], out: "coding/day-digest.json", batch: "heavy" },
22889
+ // walkthrough weight 28 = was 55 at 2 workers, ~28 at the rebalanced 4
22890
+ { script: "coding-walkthrough.mts", label: "Per-session walkthroughs", llm: true, weight: 28, args: ["--model", narrativeModel, "--conc", String(h.walkthrough)], batch: "heavy" },
22891
+ { script: "coding-frontier-detail.mts", label: "What you're doing at the frontier", llm: true, weight: 3, args: [], out: "coding/frontier-detail.json", batch: "heavy" },
22892
+ { script: "coding-delegation.mts", label: "Delegation patterns", llm: true, weight: 2, args: ["--model", model, "--conc", String(h.delegation)], out: "coding/delegation-rollup.json", batch: "heavy" },
22890
22893
  // The FULL focus stage: flow-rule judgment + the paste-into-CLAUDE.md block.
22891
22894
  // The deterministic --skip-llm run above keeps the numbers current every
22892
22895
  // run; this one produces the AI block — it was MISSING from the graded tier
@@ -22894,14 +22897,14 @@ function stages(o) {
22894
22897
  // the companion), so overnight full runs shipped a report with no flow
22895
22898
  // rules and nothing failed loud (found 2026-07-15). Both LLM passes are
22896
22899
  // prompt-hash gated, so an unchanged corpus re-run costs zero calls.
22897
- { script: "coding-focus.mts", label: "Flow rules + focus judgment", llm: true, args: [], out: "coding/focus.json", batch: "heavy" },
22900
+ { script: "coding-focus.mts", label: "Flow rules + focus judgment", llm: true, weight: 6, args: [], out: "coding/focus.json", batch: "heavy" },
22898
22901
  // ---- corpus aggregate (reads grades; deterministic rollup) ----
22899
22902
  { script: "coding-aggregate.mts", label: "Corpus aggregate \u2014 ability ceiling + flow", llm: false, args: [], out: "coding/aggregate.json" },
22900
22903
  // ---- grade readers (grades/* is complete once aggregate ran) ----
22901
22904
  // No default --model here: the agglomerate script defaults itself to Opus
22902
22905
  // (one call per run; the axis verdicts are the most judgment-heavy output).
22903
22906
  // An explicit user model choice still wins.
22904
- { script: "coding-agglomerate.mts", label: "Agglomerated criteria + verdicts + signature moves", llm: true, args: o.model ? ["--model", o.model] : [], out: "coding/agglomerate.json", batch: "grade-readers" },
22907
+ { script: "coding-agglomerate.mts", label: "Agglomerated criteria + verdicts + signature moves", llm: true, weight: 4, args: o.model ? ["--model", o.model] : [], out: "coding/agglomerate.json", batch: "grade-readers" },
22905
22908
  // Domain-expertise map DISABLED (owner call 2026-07-22): the single most
22906
22909
  // expensive stage of the whole run ($13.88 of $49.34 measured — 129 sonnet
22907
22910
  // calls) and for most users the hiring-bar filter correctly returns ZERO
@@ -22910,22 +22913,22 @@ function stages(o) {
22910
22913
  // fold into the grade call); the report renders nothing when the artifact
22911
22914
  // is absent.
22912
22915
  // ---- final synthesis (reads the finished aggregates + batch outputs) ----
22913
- { script: "coding-coaching.mts", label: "Coaching + feel-seen copy", llm: true, args: [], out: "coding/coaching.json", batch: "synthesis" },
22914
- { script: "coding-gap.mts", label: "Gap-to-close vs best practice", llm: true, args: [], out: "coding/gap.json", batch: "synthesis" },
22916
+ { script: "coding-coaching.mts", label: "Coaching + feel-seen copy", llm: true, weight: 2, args: [], out: "coding/coaching.json", batch: "synthesis" },
22917
+ { script: "coding-gap.mts", label: "Gap-to-close vs best practice", llm: true, weight: 5, args: [], out: "coding/gap.json", batch: "synthesis" },
22915
22918
  // Work brief — the hiring-manager narrative + continuous timeline. Consumes
22916
22919
  // day-digest.json (heavy batch, already complete) + concurrency/projects;
22917
22920
  // disjoint output, so it rides the synthesis batch. Defaults itself to Opus
22918
22921
  // (the journey's significance calls are judgment-heavy); an explicit model
22919
22922
  // choice wins. Prompt-hash gated + up-to-date-skip like its siblings.
22920
- { script: "coding-workbrief.mts", label: "Work brief \u2014 narrative timeline", llm: true, args: o.model ? ["--model", o.model] : [], out: "coding/workbrief.json", batch: "synthesis" },
22923
+ { script: "coding-workbrief.mts", label: "Work brief \u2014 narrative timeline", llm: true, weight: 3, args: o.model ? ["--model", o.model] : [], out: "coding/workbrief.json", batch: "synthesis" },
22921
22924
  // Growth reads grades/* + deterministic artifacts only (no per-session AI
22922
22925
  // calls — it deliberately ignores the AI recency window and covers the full
22923
22926
  // history); disjoint output, so it rides the synthesis batch. Like
22924
22927
  // agglomerate it defaults itself to Opus; an explicit model choice wins.
22925
- { script: "coding-growth.mts", label: "Growth arcs + learning rate", llm: true, args: o.model ? ["--model", o.model] : [], out: "coding/growth.json", batch: "synthesis" },
22928
+ { script: "coding-growth.mts", label: "Growth arcs + learning rate", llm: true, weight: 5, args: o.model ? ["--model", o.model] : [], out: "coding/growth.json", batch: "synthesis" },
22926
22929
  // nutshell stays LAST + alone: it merges INTO aggregate.json while the two
22927
22930
  // above read the compiled profile — racing them re-reads mid-write.
22928
- { script: "coding-nutshell.mts", label: "Big-picture nutshell", llm: true, args: [], out: "coding/aggregate.json" }
22931
+ { script: "coding-nutshell.mts", label: "Big-picture nutshell", llm: true, weight: 1, args: [], out: "coding/aggregate.json" }
22929
22932
  // merges bigPicture INTO aggregate.json
22930
22933
  ];
22931
22934
  }
@@ -22947,18 +22950,54 @@ function stageInvocation(repoRoot, scriptRel, args) {
22947
22950
  }
22948
22951
  throw new Error(`pipeline stage ${scriptRel}: neither repo scripts/ nor bundled dist/pipeline/ found`);
22949
22952
  }
22953
+ function killLiveStages() {
22954
+ for (const c of liveStages) {
22955
+ try {
22956
+ if (process.platform === "win32" || !c.pid) c.kill("SIGKILL");
22957
+ else process.kill(-c.pid, "SIGKILL");
22958
+ } catch {
22959
+ }
22960
+ }
22961
+ liveStages.clear();
22962
+ }
22963
+ function hookStageKill() {
22964
+ if (killHooked) return;
22965
+ killHooked = true;
22966
+ process.on("exit", killLiveStages);
22967
+ for (const sig of ["SIGINT", "SIGTERM"]) {
22968
+ process.on(sig, () => {
22969
+ killLiveStages();
22970
+ process.exit(sig === "SIGINT" ? 130 : 143);
22971
+ });
22972
+ }
22973
+ }
22974
+ function trackStageChild(child) {
22975
+ hookStageKill();
22976
+ liveStages.add(child);
22977
+ }
22978
+ function untrackStageChild(child) {
22979
+ liveStages.delete(child);
22980
+ }
22950
22981
  function runStage(repoRoot, st, env, onLine) {
22951
22982
  return new Promise((resolve2, reject) => {
22952
22983
  const { cmd, argv, cwd } = stageInvocation(repoRoot, st.script, st.args);
22953
- const child = spawn7(cmd, argv, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
22984
+ hookStageKill();
22985
+ const child = spawn7(cmd, argv, { cwd, env, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32" });
22986
+ liveStages.add(child);
22954
22987
  const pump = (buf) => {
22955
22988
  if (!onLine) return;
22956
22989
  for (const line of buf.toString().split(/\r?\n/)) if (line.trim()) onLine(line);
22957
22990
  };
22958
22991
  child.stdout.on("data", pump);
22959
22992
  child.stderr.on("data", pump);
22960
- child.on("error", reject);
22961
- child.on("close", (code) => code === 0 ? resolve2() : reject(new Error(`${st.script} exited with code ${code}`)));
22993
+ child.on("error", (e) => {
22994
+ liveStages.delete(child);
22995
+ reject(e);
22996
+ });
22997
+ child.on("close", (code) => {
22998
+ liveStages.delete(child);
22999
+ code === 0 ? resolve2() : reject(new Error(`${st.script} exited with code ${code}`));
23000
+ });
22962
23001
  });
22963
23002
  }
22964
23003
  function hasPipeline(repoRoot) {
@@ -23099,6 +23138,7 @@ async function runPipeline(o) {
23099
23138
  o.onStage?.(++stageNo, list.length, `${st.label} \u2014 up to date, skipped`, st.llm);
23100
23139
  log.event(st.script.replace(/\.mts$/, ""), { ok: true, ms: 0, skipped: true });
23101
23140
  stageTimes.push({ script: st.script, label: st.label, ms: 0, ok: true, skipped: true });
23141
+ o.onStageDone?.(st.label, st.llm, true, st.script);
23102
23142
  return;
23103
23143
  }
23104
23144
  o.onStage?.(++stageNo, list.length, group.length > 1 ? `${st.label} (parallel \xD7${group.length})` : st.label, st.llm);
@@ -23116,6 +23156,7 @@ async function runPipeline(o) {
23116
23156
  const sha = st.out ? await fileSha(path38.join(dataRoot, st.out)) : null;
23117
23157
  log.event(st.script.replace(/\.mts$/, ""), { ok, ms: Date.now() - stStart, ...sha ? { sha } : {} });
23118
23158
  stageTimes.push({ script: st.script, label: st.label, ms: Date.now() - stStart, ok });
23159
+ o.onStageDone?.(st.label, st.llm, ok, st.script);
23119
23160
  }));
23120
23161
  if (!o.deterministicOnly && !skippedLlm && group[0].script === "coding-build.mts") {
23121
23162
  const gate2 = await evalUpToDate(codingDir, threshold, o.regrade, o);
@@ -23156,7 +23197,7 @@ async function runPipeline(o) {
23156
23197
  stopKeepAwake();
23157
23198
  return { failed, ...skippedLlm ? { skippedLlm } : {} };
23158
23199
  }
23159
- var MODULE_DIR2;
23200
+ var MODULE_DIR2, liveStages, killHooked;
23160
23201
  var init_pipeline2 = __esm({
23161
23202
  "src/pipeline.ts"() {
23162
23203
  "use strict";
@@ -23164,6 +23205,8 @@ var init_pipeline2 = __esm({
23164
23205
  init_dataHome();
23165
23206
  init_runLog();
23166
23207
  MODULE_DIR2 = path38.dirname(fileURLToPath3(import.meta.url));
23208
+ liveStages = /* @__PURE__ */ new Set();
23209
+ killHooked = false;
23167
23210
  }
23168
23211
  });
23169
23212
 
@@ -23237,15 +23280,22 @@ async function finalChatArtifacts(dataRoot) {
23237
23280
  function runStage2(repoRoot, st, env, onLine) {
23238
23281
  return new Promise((resolve2, reject) => {
23239
23282
  const { cmd, argv, cwd } = stageInvocation2(repoRoot, st.script, st.args);
23240
- const child = spawn8(cmd, argv, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
23283
+ const child = spawn8(cmd, argv, { cwd, env, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32" });
23284
+ trackStageChild(child);
23241
23285
  const pump = (buf) => {
23242
23286
  if (!onLine) return;
23243
23287
  for (const line of buf.toString().split(/\r?\n/)) if (line.trim()) onLine(line);
23244
23288
  };
23245
23289
  child.stdout.on("data", pump);
23246
23290
  child.stderr.on("data", pump);
23247
- child.on("error", reject);
23248
- child.on("close", (code) => code === 0 ? resolve2() : reject(new Error(`${st.script} exited with code ${code}`)));
23291
+ child.on("error", (e) => {
23292
+ untrackStageChild(child);
23293
+ reject(e);
23294
+ });
23295
+ child.on("close", (code) => {
23296
+ untrackStageChild(child);
23297
+ code === 0 ? resolve2() : reject(new Error(`${st.script} exited with code ${code}`));
23298
+ });
23249
23299
  });
23250
23300
  }
23251
23301
  async function runChatPipeline(o) {
@@ -23356,6 +23406,7 @@ var init_chatPipeline = __esm({
23356
23406
  "src/chatPipeline.ts"() {
23357
23407
  "use strict";
23358
23408
  init_manifest();
23409
+ init_pipeline2();
23359
23410
  init_central();
23360
23411
  init_dataHome();
23361
23412
  init_runLog();
@@ -24823,10 +24874,9 @@ function makeMultiBar(out = process.stderr) {
24823
24874
  const width = 16;
24824
24875
  const elapsedMin = Math.round((Date.now() - t0) / 6e4);
24825
24876
  const laneRow = ([name17, l]) => {
24826
- const real = l.within && l.within.total ? l.within.done / l.within.total : 0;
24827
- const creep = 0.9 * (1 - Math.exp(-(Date.now() - l.stageAt) / 9e4));
24828
- const withinFrac = Math.min(1, Math.max(real, l.i >= 1 && l.i <= l.total ? creep : 0));
24829
- const frac = l.total ? (l.i - 1 + withinFrac) / l.total : 0;
24877
+ const real = [...l.fracs.values()].reduce((a, f) => a + (f.total ? Math.min(1, f.done / f.total) : 0), 0);
24878
+ const withinFrac = Math.min(Math.max(0, l.total - (l.i - 1)), real);
24879
+ const frac = l.forced ?? (l.total ? (l.i - 1 + withinFrac) / l.total : 0);
24830
24880
  l.shown = Math.max(l.shown, Math.max(0, Math.min(1, frac)));
24831
24881
  const fill = Math.round(l.shown * width);
24832
24882
  return ` ${name17.padEnd(6)} [${"\u2588".repeat(fill)}${"\u2591".repeat(width - fill)}] ${String(Math.round(l.shown * 100)).padStart(3)}%`;
@@ -24866,36 +24916,56 @@ function makeMultiBar(out = process.stderr) {
24866
24916
  };
24867
24917
  return {
24868
24918
  lane(name17) {
24919
+ const getLane = () => {
24920
+ const l = lanes.get(name17) ?? { i: 0, total: 0, label: "", fracs: /* @__PURE__ */ new Map(), forced: null, stageAt: Date.now(), shown: 0 };
24921
+ lanes.set(name17, l);
24922
+ return l;
24923
+ };
24869
24924
  return {
24870
24925
  stage(i, total, label) {
24871
- const l = lanes.get(name17) ?? { i: 0, total: 0, label: "", within: null, stageAt: Date.now(), shown: 0 };
24926
+ const l = getLane();
24872
24927
  l.i = i;
24873
24928
  l.total = total;
24874
24929
  l.label = label;
24875
- l.within = null;
24876
24930
  l.stageAt = Date.now();
24877
- lanes.set(name17, l);
24931
+ for (const [k, f] of l.fracs) if (f.total && f.done >= f.total) l.fracs.delete(k);
24878
24932
  repaintAround(`\u25B8 ${name17} [${i}/${total}] ${label}`);
24879
24933
  },
24880
24934
  log(line) {
24881
24935
  const l = lanes.get(name17);
24882
24936
  if (l) {
24883
24937
  const m = /Σ\s+(\d+)\/(\d+)/.exec(line);
24884
- if (m) l.within = { done: Number(m[1]), total: Number(m[2]) };
24938
+ if (m) {
24939
+ const tag = /^\s*\[([^\]]+)\]/.exec(line)?.[1] ?? "_";
24940
+ l.fracs.set(tag, { done: Number(m[1]), total: Number(m[2]) });
24941
+ }
24885
24942
  }
24886
24943
  repaintAround(tty ? line : `[${name17}] ${line.trimStart()}`);
24887
24944
  },
24945
+ // The cli's duration-weighted fraction — supersedes local math.
24946
+ setFrac(f) {
24947
+ const l = getLane();
24948
+ l.forced = Math.max(0, Math.min(1, f));
24949
+ if (tty) repaintAround();
24950
+ },
24951
+ // A stage completed: its share now lives in `i`, so drop its running
24952
+ // fraction (fall back to the untagged slot for single stages).
24953
+ stageDone(key2) {
24954
+ const l = lanes.get(name17);
24955
+ if (l && !l.fracs.delete(key2)) l.fracs.delete("_");
24956
+ if (tty) repaintAround();
24957
+ },
24888
24958
  // Pin the lane at 100% immediately (up-to-date gate: the report is
24889
24959
  // ready NOW; remaining stages are silent housekeeping and must not
24890
24960
  // crawl the bar through partial percentages).
24891
24961
  finish(label) {
24892
- const l = lanes.get(name17) ?? { i: 0, total: 0, label: "", within: null, stageAt: Date.now(), shown: 0 };
24962
+ const l = getLane();
24893
24963
  l.total = l.total || 1;
24894
24964
  l.i = l.total + 1;
24895
24965
  l.label = label;
24896
- l.within = null;
24966
+ l.fracs.clear();
24967
+ l.forced = null;
24897
24968
  l.shown = 1;
24898
- lanes.set(name17, l);
24899
24969
  repaintAround(`\u25B8 ${name17} \u2713 ${label}`);
24900
24970
  }
24901
24971
  };
@@ -27998,7 +28068,14 @@ function startServer(opts2) {
27998
28068
  // through 127.0.0.1 no matter which name the person's tab uses.
27999
28069
  url: host === "127.0.0.1" ? `http://localhost:${port}` : serverUrl,
28000
28070
  port,
28001
- close: () => new Promise((r) => server.close(() => r())),
28071
+ // closeAllConnections BEFORE close: server.close() waits for every
28072
+ // open socket to drain, and the report tab holds a live progress
28073
+ // connection — an awaited close made Ctrl+C appear completely dead
28074
+ // (owner report 2026-07-22).
28075
+ close: () => new Promise((r) => {
28076
+ server.closeAllConnections?.();
28077
+ server.close(() => r());
28078
+ }),
28002
28079
  setProfile: (p) => {
28003
28080
  liveProfile = p;
28004
28081
  profileRev++;
@@ -28380,17 +28457,28 @@ Running the full analysis on ${backend.name === "cli" ? "Claude Code" : "Codex"}
28380
28457
  }, Number(process.env.POLYMATH_LIVE_REFRESH_MS || 45e3));
28381
28458
  const { makeMultiBar: makeMultiBar2 } = await Promise.resolve().then(() => (init_progressBar(), progressBar_exports));
28382
28459
  const mbar = makeMultiBar2();
28460
+ const withinFracs = { coding: /* @__PURE__ */ new Map(), chat: /* @__PURE__ */ new Map() };
28461
+ const sumFracs = (lane) => [...withinFracs[lane].values()].reduce((a, f) => a + (f.total ? Math.min(1, f.done / f.total) : 0), 0);
28383
28462
  const harvestWithin = (lane, line) => {
28384
28463
  const m = /Σ\s+(\d+)\/(\d+)/.exec(line);
28385
28464
  if (m && progress[lane] && !progress[lane].done) {
28386
- progress[lane].within = { done: Number(m[1]), total: Number(m[2]) };
28465
+ const tag = /^\s*\[([^\]]+)\]/.exec(line)?.[1] ?? "_";
28466
+ withinFracs[lane].set(tag, { done: Number(m[1]), total: Number(m[2]) });
28467
+ progress[lane].withinFrac = Math.round(sumFracs(lane) * 1e3) / 1e3;
28468
+ if (lane === "coding") {
28469
+ const f = codingFracRef();
28470
+ progress.coding.frac = f;
28471
+ codingLane.setFrac(f);
28472
+ }
28387
28473
  earlyHandle.setProgress(progress);
28388
28474
  }
28389
28475
  };
28476
+ let codingFracRef = () => 0;
28390
28477
  mbar.setLink(earlyHandle.url);
28391
28478
  const codingLane = mbar.lane("coding");
28392
28479
  const aiCount = { coding: 0, chat: 0 };
28393
- const aiSeen = { coding: 0, chat: 0 };
28480
+ const aiStarted = { coding: 0, chat: 0 };
28481
+ const aiDone = { coding: 0, chat: 0 };
28394
28482
  let codingPinned = false;
28395
28483
  if (await readPriorAnalysis()) {
28396
28484
  codingPinned = true;
@@ -28413,6 +28501,16 @@ Running the full analysis on ${backend.name === "cli" ? "Claude Code" : "Codex"}
28413
28501
  const { chatEngineStages: chatEngineStages2 } = await Promise.resolve().then(() => (init_chatPipeline(), chatPipeline_exports));
28414
28502
  aiCount.coding = stages2(pipeOpts).filter((st) => st.llm).length;
28415
28503
  aiCount.chat = chatEngineStages2().filter((st) => st.llm).length;
28504
+ const tagOf = (script) => script.replace(/^coding-|\.mts$/g, "");
28505
+ const codingWeights = new Map(stages2(pipeOpts).filter((st) => st.llm).map((st) => [tagOf(st.script), st.weight ?? 1]));
28506
+ const codingTotalW = [...codingWeights.values()].reduce((a, b) => a + b, 0) || 1;
28507
+ let codingDoneW = 0;
28508
+ const codingFrac = () => {
28509
+ let f = codingDoneW;
28510
+ for (const [tag, x] of withinFracs.coding) f += (codingWeights.get(tag) ?? 0) * (x.total ? Math.min(1, x.done / x.total) : 0);
28511
+ return Math.min(1, f / codingTotalW);
28512
+ };
28513
+ codingFracRef = codingFrac;
28416
28514
  const codingDone = runPipeline({
28417
28515
  ...pipeOpts,
28418
28516
  onUpToDate: () => {
@@ -28427,11 +28525,23 @@ Running the full analysis on ${backend.name === "cli" ? "Claude Code" : "Codex"}
28427
28525
  },
28428
28526
  onStage: (i, total, label, llm) => {
28429
28527
  if (codingPinned) return;
28430
- if (llm) aiSeen.coding++;
28431
- progress.coding = { ...progress.coding, i: aiSeen.coding, total: aiCount.coding, label, done: false, within: void 0 };
28528
+ if (llm) aiStarted.coding++;
28529
+ progress.coding = { ...progress.coding, i: llm ? aiDone.coding + 1 : aiDone.coding, total: aiCount.coding, label, done: false };
28530
+ earlyHandle.setProgress(progress);
28531
+ if (llm) codingLane.stage(aiDone.coding + 1, aiCount.coding, `${label} (AI)`);
28532
+ else if (aiStarted.coding === 0) codingLane.stage(0, aiCount.coding, label);
28533
+ },
28534
+ onStageDone: (label, llm, _ok, script) => {
28535
+ if (codingPinned || !llm) return;
28536
+ aiDone.coding++;
28537
+ const key2 = script.replace(/^coding-|\.mts$/g, "");
28538
+ codingDoneW += codingWeights.get(key2) ?? 0;
28539
+ if (!withinFracs.coding.delete(key2)) withinFracs.coding.delete("_");
28540
+ codingLane.stageDone(key2);
28541
+ const f = codingFrac();
28542
+ codingLane.setFrac(f);
28543
+ progress.coding = { ...progress.coding, i: Math.min(aiDone.coding + 1, aiCount.coding), total: aiCount.coding, done: false, frac: f, withinFrac: Math.round(sumFracs("coding") * 1e3) / 1e3 };
28432
28544
  earlyHandle.setProgress(progress);
28433
- if (llm) codingLane.stage(aiSeen.coding, aiCount.coding, `${label} (AI)`);
28434
- else if (aiSeen.coding === 0) codingLane.stage(0, aiCount.coding, label);
28435
28545
  },
28436
28546
  // While pinned, the scrolling stage output stays quiet ("prints hella
28437
28547
  // things when everything is cached", owner 2026-07-17) — warnings and
@@ -28503,11 +28613,14 @@ Running the full analysis on ${backend.name === "cli" ? "Claude Code" : "Codex"}
28503
28613
  },
28504
28614
  onStage: (i, total, label, llm) => {
28505
28615
  if (chatPinned) return;
28506
- if (llm) aiSeen.chat++;
28507
- progress.chat = { ...progress.chat, i: aiSeen.chat, total: aiCount.chat, label, done: false, within: void 0 };
28616
+ if (llm) {
28617
+ aiStarted.chat++;
28618
+ withinFracs.chat.clear();
28619
+ }
28620
+ progress.chat = { ...progress.chat, i: aiStarted.chat, total: aiCount.chat, label, done: false, withinFrac: void 0 };
28508
28621
  earlyHandle.setProgress(progress);
28509
- if (llm) chatLane.stage(aiSeen.chat, aiCount.chat, `${label} (AI)`);
28510
- else if (aiSeen.chat === 0) chatLane.stage(0, aiCount.chat, label);
28622
+ if (llm) chatLane.stage(aiStarted.chat, aiCount.chat, `${label} (AI)`);
28623
+ else if (aiStarted.chat === 0) chatLane.stage(0, aiCount.chat, label);
28511
28624
  },
28512
28625
  onLine: (line) => {
28513
28626
  harvestWithin("chat", line);
@@ -28633,14 +28746,20 @@ No analysis here yet \u2014 computing the instant deterministic metrics now (fre
28633
28746
  console.error(` (Ctrl-C to stop)
28634
28747
  `);
28635
28748
  if (opts2.open && !earlyHandle) openBrowser(handle.url);
28636
- process.on("SIGINT", async () => {
28637
- await handle.close();
28638
- process.exit(0);
28639
- });
28640
- process.on("SIGTERM", async () => {
28641
- await handle.close();
28642
- process.exit(0);
28643
- });
28749
+ let dying = false;
28750
+ const shutdown = (code) => async () => {
28751
+ if (dying) process.exit(code);
28752
+ dying = true;
28753
+ const hardExit = setTimeout(() => process.exit(code), 1e3);
28754
+ if (typeof hardExit.unref === "function") hardExit.unref();
28755
+ try {
28756
+ await handle.close();
28757
+ } catch {
28758
+ }
28759
+ process.exit(code);
28760
+ };
28761
+ process.on("SIGINT", shutdown(130));
28762
+ process.on("SIGTERM", shutdown(143));
28644
28763
  return;
28645
28764
  }
28646
28765
  const profile = await analyze({ codex: opts2.codex, idleGapMin: opts2.idleGapMin, tz: opts2.tz, spanHourMin: opts2.spanHourMin });
package/dist/index.js CHANGED
@@ -25606,7 +25606,14 @@ function startServer(opts2) {
25606
25606
  // through 127.0.0.1 no matter which name the person's tab uses.
25607
25607
  url: host === "127.0.0.1" ? `http://localhost:${port}` : serverUrl,
25608
25608
  port,
25609
- close: () => new Promise((r) => server.close(() => r())),
25609
+ // closeAllConnections BEFORE close: server.close() waits for every
25610
+ // open socket to drain, and the report tab holds a live progress
25611
+ // connection — an awaited close made Ctrl+C appear completely dead
25612
+ // (owner report 2026-07-22).
25613
+ close: () => new Promise((r) => {
25614
+ server.closeAllConnections?.();
25615
+ server.close(() => r());
25616
+ }),
25610
25617
  setProfile: (p) => {
25611
25618
  liveProfile = p;
25612
25619
  profileRev++;
@@ -16899,6 +16899,7 @@ function validate(raw, grades, costUsd) {
16899
16899
  const r = raw;
16900
16900
  const overall = typeof r.overall === "string" ? r.overall.trim() : "";
16901
16901
  if (!overall) return null;
16902
+ if (overall.length > 200 && !/[.!?…"'’)\]]$/.test(overall)) return null;
16902
16903
  if (!r.verdicts || typeof r.verdicts !== "object") return null;
16903
16904
  const keys = new Set(GRADED_CRITERIA.map((c) => c.key));
16904
16905
  const criteria = (Array.isArray(r.criteria) ? r.criteria : []).map((c) => {
package/dist/web/app.js CHANGED
@@ -12939,9 +12939,13 @@ function laneFrac(name, l) {
12939
12939
  m.label = l.label;
12940
12940
  m.at = Date.now();
12941
12941
  }
12942
- const real = l.within && l.within.total ? l.within.done / l.within.total : 0;
12943
- const creep = l.i >= 1 ? 0.9 * (1 - Math.exp(-(Date.now() - m.at) / 9e4)) : 0;
12944
- const w = Math.min(1, Math.max(real, creep));
12942
+ if (l.frac != null) {
12943
+ m.shown = Math.max(m.shown, Math.min(1, Math.max(0, l.frac)));
12944
+ laneMemo.set(name, m);
12945
+ return m.shown;
12946
+ }
12947
+ const real = l.withinFrac ?? (l.within && l.within.total ? l.within.done / l.within.total : 0);
12948
+ const w = Math.min(Math.max(0, l.total - (l.i - 1)), real);
12945
12949
  m.shown = Math.max(m.shown, Math.min(1, Math.max(0, (l.i - 1 + w) / l.total)));
12946
12950
  laneMemo.set(name, m);
12947
12951
  return m.shown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polymath-society",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "description": "Deterministic metrics from your local Claude Code / Codex agent logs — parallelism, flow, throughput, projects. Zero LLM, zero server, on-device.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",