polymath-society 0.2.18 → 0.2.19

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
@@ -33,9 +33,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
33
33
  ));
34
34
 
35
35
  // src/dataHome.ts
36
- import { existsSync } from "fs";
36
+ import { existsSync, readFileSync } from "fs";
37
37
  import os from "os";
38
38
  import path from "path";
39
+ function hasCompletedAnalysis(root) {
40
+ try {
41
+ const a = JSON.parse(readFileSync(path.join(root, "coding", "aggregate.json"), "utf-8"));
42
+ return !!String(a.bigPicture ?? "").trim();
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
39
47
  function electronAppData() {
40
48
  const candidates = [
41
49
  "/Applications/Polymath.app/Contents/Resources/standalone/.data",
@@ -44,14 +52,18 @@ function electronAppData() {
44
52
  for (const c of candidates) if (existsSync(path.join(c, "coding"))) return c;
45
53
  return null;
46
54
  }
47
- function resolveDataRoot(env = process.env, cwd = process.cwd(), appData = electronAppData) {
55
+ function resolveDataRoot(env = process.env, cwd = process.cwd(), appData = electronAppData, home = os.homedir()) {
48
56
  const fromEnv = env.POLYMATH_DATA_DIR;
49
57
  if (fromEnv && fromEnv.trim()) return path.resolve(fromEnv);
50
58
  const local = path.join(cwd, ".data");
51
- if (existsSync(local)) return local;
59
+ const dflt = path.join(home, ".polymath-society", "data");
60
+ if (existsSync(local) && hasCompletedAnalysis(local)) return local;
52
61
  const fromApp = appData();
62
+ if (fromApp && hasCompletedAnalysis(fromApp)) return fromApp;
63
+ if (hasCompletedAnalysis(dflt)) return dflt;
64
+ if (existsSync(local)) return local;
53
65
  if (fromApp) return fromApp;
54
- return path.join(os.homedir(), ".polymath-society", "data");
66
+ return dflt;
55
67
  }
56
68
  var init_dataHome = __esm({
57
69
  "src/dataHome.ts"() {
@@ -2042,7 +2054,7 @@ __export(plan_exports, {
2042
2054
  tierFromStrings: () => tierFromStrings
2043
2055
  });
2044
2056
  import { promises as fs34 } from "fs";
2045
- import os9 from "os";
2057
+ import os10 from "os";
2046
2058
  import path39 from "path";
2047
2059
  function tierFromStrings(...hints) {
2048
2060
  for (const h of hints) {
@@ -2063,7 +2075,7 @@ function tierFromChatgptPlan(planType) {
2063
2075
  if (s === "free") return "gpt_free";
2064
2076
  return "unknown";
2065
2077
  }
2066
- async function detectCodexPlan(home = os9.homedir()) {
2078
+ async function detectCodexPlan(home = os10.homedir()) {
2067
2079
  try {
2068
2080
  const raw = JSON.parse(await fs34.readFile(path39.join(home, ".codex", "auth.json"), "utf8"));
2069
2081
  for (const tok of [raw.tokens?.id_token, raw.tokens?.access_token]) {
@@ -2079,7 +2091,7 @@ async function detectCodexPlan(home = os9.homedir()) {
2079
2091
  }
2080
2092
  return "unknown";
2081
2093
  }
2082
- async function detectPlan(home = os9.homedir()) {
2094
+ async function detectPlan(home = os10.homedir()) {
2083
2095
  let tier = "unknown";
2084
2096
  try {
2085
2097
  const raw = JSON.parse(await fs34.readFile(path39.join(home, ".claude.json"), "utf8"));
@@ -2173,7 +2185,7 @@ __export(wizard_exports, {
2173
2185
  sayExportLinks: () => sayExportLinks
2174
2186
  });
2175
2187
  import { promises as fs35 } from "fs";
2176
- import os10 from "os";
2188
+ import os11 from "os";
2177
2189
  import path40 from "path";
2178
2190
  import * as readline from "node:readline/promises";
2179
2191
  async function scanJsonlDir(label, dir) {
@@ -2313,7 +2325,7 @@ async function runWizard() {
2313
2325
  for (let i = 0; i < agentScans.length; i++) {
2314
2326
  const s = agentScans[i];
2315
2327
  const idx = present.indexOf(agentIds[i]);
2316
- say(s.files > 0 ? ` ${idx + 1}. ${green("\u2713")} ${bold(s.label)} \u2014 ${s.files.toLocaleString()} log files \xB7 newest ${s.newest} ${dim(s.dir.replace(os10.homedir(), "~"))}` : ` ${dim("\xB7")} ${dim(`${s.label} \u2014 none found`)} ${dim(s.dir.replace(os10.homedir(), "~"))}`);
2328
+ say(s.files > 0 ? ` ${idx + 1}. ${green("\u2713")} ${bold(s.label)} \u2014 ${s.files.toLocaleString()} log files \xB7 newest ${s.newest} ${dim(s.dir.replace(os11.homedir(), "~"))}` : ` ${dim("\xB7")} ${dim(`${s.label} \u2014 none found`)} ${dim(s.dir.replace(os11.homedir(), "~"))}`);
2317
2329
  }
2318
2330
  say(dim(" (log files include forks and agent runs \u2014 the report separates your real sessions out)"));
2319
2331
  if (present.length === 0) {
@@ -2376,9 +2388,9 @@ async function runWizard() {
2376
2388
  }
2377
2389
  };
2378
2390
  say(dim(" Scanning for known export formats (Spotlight + the usual folders, a few seconds)..."));
2379
- for (const c of await deterministicDiscover(os10.homedir())) add(await identifyPath(c));
2380
- for (const f of await scanForExports(path40.join(os10.homedir(), "Downloads"))) add(f);
2381
- for (const f of await scanForObsidianVaults(os10.homedir())) add(f);
2391
+ for (const c of await deterministicDiscover(os11.homedir())) add(await identifyPath(c));
2392
+ for (const f of await scanForExports(path40.join(os11.homedir(), "Downloads"))) add(f);
2393
+ for (const f of await scanForObsidianVaults(os11.homedir())) add(f);
2382
2394
  for (const f of [...previous?.exports.included ?? [], ...previous?.exports.excluded ?? []]) add(f);
2383
2395
  if (!found.length) {
2384
2396
  say(dim(" Nothing found in the usual places."));
@@ -2386,7 +2398,7 @@ async function runWizard() {
2386
2398
  try {
2387
2399
  const { invokeAgent: invokeAgent3 } = await Promise.resolve().then(() => (init_agent(), agent_exports));
2388
2400
  say(dim(" Searching (your agent writes its own filesystem searches; nothing leaves this machine; 90s max)..."));
2389
- const candidates = await agentDiscoverCandidates((inv) => invokeAgent3(inv), os10.homedir(), []);
2401
+ const candidates = await agentDiscoverCandidates((inv) => invokeAgent3(inv), os11.homedir(), []);
2390
2402
  for (const c of candidates) add(await identifyPath(c));
2391
2403
  } catch (e) {
2392
2404
  say(dim(` Agent search unavailable (${e instanceof Error ? e.message.slice(0, 80) : "failed"}).`));
@@ -2394,7 +2406,7 @@ async function runWizard() {
2394
2406
  }
2395
2407
  if (!found.length) {
2396
2408
  const folder = await ask(` Got them in a specific folder? ${dim("(path, or Enter to skip)")}: `);
2397
- if (folder) for (const f of await scanForExports(folder.replace(/^~(?=$|\/)/, os10.homedir()))) add(f);
2409
+ if (folder) for (const f of await scanForExports(folder.replace(/^~(?=$|\/)/, os11.homedir()))) add(f);
2398
2410
  }
2399
2411
  }
2400
2412
  if (found.length) {
@@ -2419,7 +2431,7 @@ async function runWizard() {
2419
2431
  approvedExports = manifest.exports.included;
2420
2432
  await writeManifest(dataDir, manifest);
2421
2433
  const nIn = manifest.exports.included.length;
2422
- say(` ${green("\u2713")} Saved to ${path40.join(dataDir, "exports-manifest.json").replace(os10.homedir(), "~")} \u2014 ${nIn} source${nIn === 1 ? "" : "s"} approved, ${manifest.exports.excluded.length} excluded (re-run anytime to change).`);
2434
+ say(` ${green("\u2713")} Saved to ${path40.join(dataDir, "exports-manifest.json").replace(os11.homedir(), "~")} \u2014 ${nIn} source${nIn === 1 ? "" : "s"} approved, ${manifest.exports.excluded.length} excluded (re-run anytime to change).`);
2423
2435
  for (const f of manifest.exports.excluded) say(` ${dim(`\u2717 ${path40.basename(f.path)} \u2014 excluded, never read past identification.`)}`);
2424
2436
  if (manifest.exports.included.some((f) => f.kind !== "unknown")) {
2425
2437
  say(` ${dim("These are ingested and analyzed locally when you pick the full analysis below.")}`);
@@ -4447,7 +4459,7 @@ import { promises as fs14 } from "fs";
4447
4459
  import path19 from "path";
4448
4460
 
4449
4461
  // ../../lib/agents/shared/agent.ts
4450
- import { readFileSync } from "fs";
4462
+ import { readFileSync as readFileSync2 } from "fs";
4451
4463
  import path13 from "path";
4452
4464
 
4453
4465
  // ../../lib/agents/shared/cliAdapter.ts
@@ -19895,7 +19907,7 @@ var ADAPTERS2 = {
19895
19907
  };
19896
19908
  function preferredEngine() {
19897
19909
  try {
19898
- const p = JSON.parse(readFileSync(path13.join(process.cwd(), ".data", "profile.json"), "utf-8"));
19910
+ const p = JSON.parse(readFileSync2(path13.join(process.cwd(), ".data", "profile.json"), "utf-8"));
19899
19911
  const e = p?.preferredEngine;
19900
19912
  return e === "cli" || e === "codex" || e === "cursor" || e === "api" ? e : null;
19901
19913
  } catch {
@@ -23066,6 +23078,8 @@ init_exportLinks();
23066
23078
  // src/publicCodingPageApi.ts
23067
23079
  import { promises as fs28 } from "fs";
23068
23080
  import path32 from "path";
23081
+ import os7 from "os";
23082
+ import crypto3 from "crypto";
23069
23083
 
23070
23084
  // ../../lib/agents/coding/publicPage.ts
23071
23085
  import { promises as fs27 } from "fs";
@@ -24465,6 +24479,49 @@ async function handleAnalysisShare(dataDir, kind, full) {
24465
24479
  return { status: 502, json: { error: e.message.slice(0, 200) } };
24466
24480
  }
24467
24481
  }
24482
+ function computeMachineUnion(machines) {
24483
+ const counts = /* @__PURE__ */ new Map();
24484
+ for (const m of Object.values(machines)) for (const s of m.sessions ?? []) counts.set(s.id, (counts.get(s.id) ?? 0) + 1);
24485
+ return {
24486
+ machines: Object.entries(machines).map(([id, m]) => ({ id, label: m.label ?? id, sessions: (m.sessions ?? []).length, syncedAt: m.syncedAt ?? null })),
24487
+ uniqueSessions: counts.size,
24488
+ overlappingSessions: [...counts.values()].filter((n) => n > 1).length
24489
+ };
24490
+ }
24491
+ async function handleAnalysisSync(dataDir) {
24492
+ const cfg = centralConfig();
24493
+ if (!cfg.configured) return { status: 503, json: { error: "central services are disabled in this build" } };
24494
+ const auth = await getAccessToken(dataDir);
24495
+ if (!auth) return { status: 401, json: { error: "sign in with Google (top right) first" } };
24496
+ const mFile = path32.join(dataDir, "machine.json");
24497
+ let machine = await readJson4(mFile, null);
24498
+ if (!machine?.id) {
24499
+ machine = { id: crypto3.randomBytes(8).toString("hex"), label: os7.hostname().replace(/\.local$/, "") };
24500
+ await fs28.writeFile(mFile, JSON.stringify(machine, null, 2));
24501
+ }
24502
+ const rows = await readJson4(
24503
+ path32.join(dataDir, "coding", "sessions.json"),
24504
+ []
24505
+ );
24506
+ const sessions = rows.filter((r) => r.klass === "interactive" && !r.duplicateOf && r.sessionId).map((r) => ({ id: r.sessionId, start: r.start?.slice(0, 10) ?? null, chars: r.humanChars ?? 0, project: r.project ?? null, source: r.source ?? null }));
24507
+ if (!sessions.length) return { status: 409, json: { error: "nothing to sync yet \u2014 run the analysis first" } };
24508
+ const prior = await readOwnReportContent(cfg, auth.token, auth.identity.userId);
24509
+ try {
24510
+ const machines = prior.machines && typeof prior.machines === "object" ? prior.machines : {};
24511
+ machines[machine.id] = { label: machine.label, syncedAt: (/* @__PURE__ */ new Date()).toISOString(), sessions };
24512
+ const combined = computeMachineUnion(machines);
24513
+ const merged = { format: "coding-pr1", ...prior, machines, combined };
24514
+ const up = await fetch(`${cfg.supabaseUrl}/rest/v1/rpc/submit_report`, {
24515
+ method: "POST",
24516
+ headers: { apikey: cfg.supabaseAnonKey, authorization: `Bearer ${auth.token}`, "content-type": "application/json" },
24517
+ body: JSON.stringify({ p_session_id: null, p_content: JSON.stringify(merged) })
24518
+ });
24519
+ if (!up.ok) return { status: 502, json: { error: `sync failed (HTTP ${up.status})` } };
24520
+ return { status: 200, json: { ok: true, machine: machine.label, ...combined } };
24521
+ } catch (e) {
24522
+ return { status: 502, json: { error: e.message.slice(0, 200) } };
24523
+ }
24524
+ }
24468
24525
 
24469
24526
  // src/server.ts
24470
24527
  var __dirname = path33.dirname(fileURLToPath2(import.meta.url));
@@ -24792,6 +24849,11 @@ function startServer(opts) {
24792
24849
  json(res, out.status, out.json);
24793
24850
  return;
24794
24851
  }
24852
+ if (req.method === "POST" && route === "/api/analysis/sync") {
24853
+ const out = await handleAnalysisSync(dataDir);
24854
+ json(res, out.status, out.json);
24855
+ return;
24856
+ }
24795
24857
  if (req.method === "POST" && route === "/api/analysis/share") {
24796
24858
  const body = JSON.parse(await readBody(req) || "{}");
24797
24859
  const out = await handleAnalysisShare(dataDir, body.kind === "chat" ? "chat" : "coding", {
@@ -24855,7 +24917,7 @@ function startServer(opts) {
24855
24917
 
24856
24918
  // src/pipeline.ts
24857
24919
  import { spawn as spawn7 } from "child_process";
24858
- import os8 from "os";
24920
+ import os9 from "os";
24859
24921
  import path36 from "path";
24860
24922
  import { existsSync as existsSync5, promises as fs31 } from "fs";
24861
24923
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -24867,10 +24929,10 @@ import { promises as fs30 } from "fs";
24867
24929
  import path35 from "path";
24868
24930
 
24869
24931
  // ../../lib/agents/shared/paths.ts
24870
- import os7 from "os";
24932
+ import os8 from "os";
24871
24933
  import path34 from "path";
24872
24934
  function polymathHome() {
24873
- return process.env.POLYMATH_HOME || path34.join(os7.homedir(), ".polymath-society");
24935
+ return process.env.POLYMATH_HOME || path34.join(os8.homedir(), ".polymath-society");
24874
24936
  }
24875
24937
  function agentDir(agent) {
24876
24938
  return path34.join(polymathHome(), agent);
@@ -25029,7 +25091,7 @@ async function startRunLog(clientInfo = {}, opts = {}) {
25029
25091
 
25030
25092
  // src/pipeline.ts
25031
25093
  var MODULE_DIR2 = path36.dirname(fileURLToPath3(import.meta.url));
25032
- function machineBudget(totalMemBytes = os8.totalmem(), cpus = os8.availableParallelism?.() ?? os8.cpus().length, envOverride = process.env.POLYMATH_CONC) {
25094
+ function machineBudget(totalMemBytes = os9.totalmem(), cpus = os9.availableParallelism?.() ?? os9.cpus().length, envOverride = process.env.POLYMATH_CONC) {
25033
25095
  const envB = Number(envOverride);
25034
25096
  if (Number.isFinite(envB) && envB >= 1) return Math.min(32, Math.floor(envB));
25035
25097
  const byMem = Math.floor(totalMemBytes / 2 ** 30 * 0.35 / 0.45);
package/dist/index.js CHANGED
@@ -21785,9 +21785,17 @@ Apply the user's LAST instruction and return the complete updated payload.`;
21785
21785
  }
21786
21786
 
21787
21787
  // src/dataHome.ts
21788
- import { existsSync as existsSync4 } from "fs";
21788
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
21789
21789
  import os6 from "os";
21790
21790
  import path26 from "path";
21791
+ function hasCompletedAnalysis(root) {
21792
+ try {
21793
+ const a = JSON.parse(readFileSync2(path26.join(root, "coding", "aggregate.json"), "utf-8"));
21794
+ return !!String(a.bigPicture ?? "").trim();
21795
+ } catch {
21796
+ return false;
21797
+ }
21798
+ }
21791
21799
  function electronAppData() {
21792
21800
  const candidates = [
21793
21801
  "/Applications/Polymath.app/Contents/Resources/standalone/.data",
@@ -21796,14 +21804,18 @@ function electronAppData() {
21796
21804
  for (const c of candidates) if (existsSync4(path26.join(c, "coding"))) return c;
21797
21805
  return null;
21798
21806
  }
21799
- function resolveDataRoot(env = process.env, cwd = process.cwd(), appData = electronAppData) {
21807
+ function resolveDataRoot(env = process.env, cwd = process.cwd(), appData = electronAppData, home = os6.homedir()) {
21800
21808
  const fromEnv = env.POLYMATH_DATA_DIR;
21801
21809
  if (fromEnv && fromEnv.trim()) return path26.resolve(fromEnv);
21802
21810
  const local = path26.join(cwd, ".data");
21803
- if (existsSync4(local)) return local;
21811
+ const dflt = path26.join(home, ".polymath-society", "data");
21812
+ if (existsSync4(local) && hasCompletedAnalysis(local)) return local;
21804
21813
  const fromApp = appData();
21814
+ if (fromApp && hasCompletedAnalysis(fromApp)) return fromApp;
21815
+ if (hasCompletedAnalysis(dflt)) return dflt;
21816
+ if (existsSync4(local)) return local;
21805
21817
  if (fromApp) return fromApp;
21806
- return path26.join(os6.homedir(), ".polymath-society", "data");
21818
+ return dflt;
21807
21819
  }
21808
21820
 
21809
21821
  // src/manifest.ts
@@ -21879,6 +21891,8 @@ var EXPORT_LINKS = [
21879
21891
  // src/publicCodingPageApi.ts
21880
21892
  import { promises as fs28 } from "fs";
21881
21893
  import path32 from "path";
21894
+ import os7 from "os";
21895
+ import crypto3 from "crypto";
21882
21896
 
21883
21897
  // ../../lib/agents/coding/publicPage.ts
21884
21898
  import { promises as fs27 } from "fs";
@@ -23278,6 +23292,49 @@ async function handleAnalysisShare(dataDir, kind, full) {
23278
23292
  return { status: 502, json: { error: e.message.slice(0, 200) } };
23279
23293
  }
23280
23294
  }
23295
+ function computeMachineUnion(machines) {
23296
+ const counts = /* @__PURE__ */ new Map();
23297
+ for (const m of Object.values(machines)) for (const s of m.sessions ?? []) counts.set(s.id, (counts.get(s.id) ?? 0) + 1);
23298
+ return {
23299
+ machines: Object.entries(machines).map(([id, m]) => ({ id, label: m.label ?? id, sessions: (m.sessions ?? []).length, syncedAt: m.syncedAt ?? null })),
23300
+ uniqueSessions: counts.size,
23301
+ overlappingSessions: [...counts.values()].filter((n) => n > 1).length
23302
+ };
23303
+ }
23304
+ async function handleAnalysisSync(dataDir) {
23305
+ const cfg = centralConfig();
23306
+ if (!cfg.configured) return { status: 503, json: { error: "central services are disabled in this build" } };
23307
+ const auth = await getAccessToken(dataDir);
23308
+ if (!auth) return { status: 401, json: { error: "sign in with Google (top right) first" } };
23309
+ const mFile = path32.join(dataDir, "machine.json");
23310
+ let machine = await readJson4(mFile, null);
23311
+ if (!machine?.id) {
23312
+ machine = { id: crypto3.randomBytes(8).toString("hex"), label: os7.hostname().replace(/\.local$/, "") };
23313
+ await fs28.writeFile(mFile, JSON.stringify(machine, null, 2));
23314
+ }
23315
+ const rows = await readJson4(
23316
+ path32.join(dataDir, "coding", "sessions.json"),
23317
+ []
23318
+ );
23319
+ const sessions = rows.filter((r) => r.klass === "interactive" && !r.duplicateOf && r.sessionId).map((r) => ({ id: r.sessionId, start: r.start?.slice(0, 10) ?? null, chars: r.humanChars ?? 0, project: r.project ?? null, source: r.source ?? null }));
23320
+ if (!sessions.length) return { status: 409, json: { error: "nothing to sync yet \u2014 run the analysis first" } };
23321
+ const prior = await readOwnReportContent(cfg, auth.token, auth.identity.userId);
23322
+ try {
23323
+ const machines = prior.machines && typeof prior.machines === "object" ? prior.machines : {};
23324
+ machines[machine.id] = { label: machine.label, syncedAt: (/* @__PURE__ */ new Date()).toISOString(), sessions };
23325
+ const combined = computeMachineUnion(machines);
23326
+ const merged = { format: "coding-pr1", ...prior, machines, combined };
23327
+ const up = await fetch(`${cfg.supabaseUrl}/rest/v1/rpc/submit_report`, {
23328
+ method: "POST",
23329
+ headers: { apikey: cfg.supabaseAnonKey, authorization: `Bearer ${auth.token}`, "content-type": "application/json" },
23330
+ body: JSON.stringify({ p_session_id: null, p_content: JSON.stringify(merged) })
23331
+ });
23332
+ if (!up.ok) return { status: 502, json: { error: `sync failed (HTTP ${up.status})` } };
23333
+ return { status: 200, json: { ok: true, machine: machine.label, ...combined } };
23334
+ } catch (e) {
23335
+ return { status: 502, json: { error: e.message.slice(0, 200) } };
23336
+ }
23337
+ }
23281
23338
 
23282
23339
  // src/server.ts
23283
23340
  var __dirname = path33.dirname(fileURLToPath2(import.meta.url));
@@ -23605,6 +23662,11 @@ function startServer(opts) {
23605
23662
  json(res, out.status, out.json);
23606
23663
  return;
23607
23664
  }
23665
+ if (req.method === "POST" && route === "/api/analysis/sync") {
23666
+ const out = await handleAnalysisSync(dataDir);
23667
+ json(res, out.status, out.json);
23668
+ return;
23669
+ }
23608
23670
  if (req.method === "POST" && route === "/api/analysis/share") {
23609
23671
  const body = JSON.parse(await readBody(req) || "{}");
23610
23672
  const out = await handleAnalysisShare(dataDir, body.kind === "chat" ? "chat" : "coding", {
@@ -17191,6 +17191,9 @@ async function main() {
17191
17191
  return m ? `--- "${s.title}" \xB7 ${(s.start || "").slice(0, 10)} ---
17192
17192
  ${m.text.slice(0, 3e3)}` : "";
17193
17193
  }))).filter(Boolean);
17194
+ const digestDays = (await fs11.readFile(path13.join(CODING, "day-digest.json"), "utf-8").then(JSON.parse).catch(() => ({ days: [] }))).days ?? [];
17195
+ const digestLines = digestDays.slice().sort((a, b) => a.date < b.date ? -1 : 1).slice(-21).map((d) => `${d.date}: ${d.overall ?? ""}
17196
+ ${(d.conversations ?? []).slice(0, 4).map((c) => ` - ${c.title ?? ""}: ${c.summary ?? ""}`).join("\n")}`).join("\n").slice(0, 12e3);
17194
17197
  const SYS = "You are a Claude Code power-user coach. You judge ONE person against the COMPLETE catalog of best ways to use Claude Code (the rubric below, sourced from Boris Cherny + first-party Claude Code features) and write a comprehensive, honest gap analysis, ORDERED most-difference-making \u2192 least. Rules: (1) Go through the whole catalog and surface the techniques they are NOT doing (or under-using) that would genuinely help \u2014 respecting each tip's [EMPHASIZE]/[DE-EMPHASIZE]/[GATE] curation. (2) ORDER by differenceRank (1 = biggest unlock for THIS person). Follow the rubric's PRIME DIRECTIVE: if usage.parallelism.alreadyParallel is FALSE \u2192 'Do more in parallel' is rank 1; else \u2192 'Invest in your CLAUDE.md / tune your context' is rank 1. Then Plan mode, then 'Give Claude test cases so it can auto-verify and run longer' (Boris's 'most important thing' \u2014 rank it high, right after plan mode), then subagents, and the rest per the rubric order, skipping any that don't apply. (3) GATE the conditional tips on the STRUCTURAL usage facts, NOT on keywords \u2014 and a tip whose gate FAILS is OMITTED from recommendations entirely, never included as a softened/conditional card (recommending something their data says doesn't apply, or that they already do, reads as not having looked): loops require real PR/branch volume in usage.collaboration, else OMIT; the Chrome tip requires ~zero browser-driving in usage.chrome, else OMIT and credit in alreadyStrong; forking uses the RECENT count in usage.forking \u2014 if they fork in the recent window they already do it, OMIT and credit; remote/mobile/EC2 fires only when usage.surface.workstationBound is TRUE; plan mode by usage.planMode; subagents by usage.subagents. If they already do a thing, it goes in alreadyStrong, NOT in recommendations. (4) \u26D4 CRITICAL \u2014 this person BUILDS Claude Code tooling, so their transcripts are full of the words 'worktree', 'pull request', 'fork', 'plan mode', 'by the way', 'teleport'. A keyword in the transcript is NOT evidence they use the feature. Rely on the STRUCTURAL facts only. (5) Ground every recommendation in a RECEIPT \u2014 a structural fact from their usage or a concrete session pain point \u2014 and CONVINCE with a specific case for how it helps THIS person, not a generic benefit. (6) Tailor every action to the Claude Desktop Mac app; NEVER give terminal-only `claude -p` advice. (7) Honor config.deliberateChoices (they stay on `main` on purpose \u2014 do NOT tell them to hand-juggle worktrees; parallel SESSIONS instead) and config.claudeAutoTools (never tell them to 'use' a tool Claude drives itself; for 'explain with visuals' the user just ASKS for a visual \u2014 do not name the tool). (8) Also name what they ALREADY do well. (9) SELF-CONTAINED but not verbose \u2014 lead with the PROBLEM, then the SOLUTION, then HOW to use it in their context. Plain language, no jargon, no enumerating multiple dates. An essay-like rec is a failed rec; so is a cryptic one. Follow this writing rubric strictly:\n\n" + writing + sourceLookupNote();
17195
17198
  const prompt = `THE RUBRIC \u2014 the complete catalog of best ways to use Claude Code. Judge the person against ALL of it, and ORDER by differenceRank per its PRIME DIRECTIVE:
17196
17199
 
@@ -17200,6 +17203,9 @@ ${techniques}
17200
17203
  WHAT THIS PERSON ACTUALLY DOES (STRUCTURAL usage facts + config \u2014 ground truth; trust these over anything in the transcripts):
17201
17204
  ${JSON.stringify(usage, null, 1)}
17202
17205
 
17206
+ THEIR RECENT DAYS, task by task (dated ground material \u2014 draw applyItHere moments from HERE and from the excerpts, never from imagination):
17207
+ ${digestLines}
17208
+
17203
17209
  SESSION EXCERPTS (for pain points / friction / grounding ONLY \u2014 never as proof they USE a feature; ">> " = the person, "[AI]" = the model):
17204
17210
  ${excerpts.join("\n\n").slice(0, 6e4)}
17205
17211
 
@@ -17207,11 +17213,12 @@ ${excerpts.join("\n\n").slice(0, 6e4)}
17207
17213
  Produce the gap analysis. Be thorough: cover every relevant catalog item they're missing, ordered most-difference-making first. Output a SINGLE fenced json block (every string a complete, punctuated, no-fluff sentence):
17208
17214
  \`\`\`json
17209
17215
  {
17210
- "recommendations": [ { "title": "Boris's OWN / the feature's general wording for the tip (e.g. 'Invest in your CLAUDE.md', 'Start every complex task in Plan mode', 'Give Claude test cases so it can auto-verify and run longer', 'Code from anywhere with a remote instance', 'Fork your session', 'Use /btw for side queries') \u2014 NOT a hyper-specific name. For the verification tip the title MUST contain 'verify its own work' or 'check its own work' so the report picks the right icon.", "category": "rubric tip", "status": "not-doing|under-using", "differenceRank": 1, "gap": "LEAD WITH THE PROBLEM in plain language \u2014 what it's currently costing them, grounded in a STRUCTURAL fact. One clear, self-contained sentence (~12-18 words), no jargon. If they already do it somewhat, frame it as 'you do X; the gap is pushing it further'.", "whyItHelps": "The SOLUTION, then HOW to use it in THEIR context with a concrete example. Plain language, self-contained, ~30-45 words. Two crisp beats: fix, then their-context application. HARD RULE: no jargon, and NEVER name a Claude-auto tool. EXAMPLE (plan mode): 'Skipping the plan makes Claude pause to ask and drift. Fix: iterate the whole approach in Plan mode first, and drop in concrete expected results as test cases so Claude self-verifies and runs autonomously far longer.'", "payoff": 5 } ],
17216
+ "recommendations": [ { "title": "Boris's OWN / the feature's general wording for the tip (e.g. 'Invest in your CLAUDE.md', 'Start every complex task in Plan mode', 'Give Claude test cases so it can auto-verify and run longer', 'Code from anywhere with a remote instance', 'Fork your session', 'Use /btw for side queries') \u2014 NOT a hyper-specific name. For the verification tip the title MUST contain 'verify its own work' or 'check its own work' so the report picks the right icon.", "category": "rubric tip", "status": "not-doing|under-using", "differenceRank": 1, "gap": "LEAD WITH THE PROBLEM in plain language \u2014 what it's currently costing them, grounded in a STRUCTURAL fact. One clear, self-contained sentence (~12-18 words), no jargon. If they already do it somewhat, frame it as 'you do X; the gap is pushing it further'.", "whyItHelps": "The SOLUTION, then HOW to use it in THEIR context with a concrete example. Plain language, self-contained, ~30-45 words. Two crisp beats: fix, then their-context application. HARD RULE: no jargon, and NEVER name a Claude-auto tool. EXAMPLE (plan mode): 'Skipping the plan makes Claude pause to ask and drift. Fix: iterate the whole approach in Plan mode first, and drop in concrete expected results as test cases so Claude self-verifies and runs autonomously far longer.'", "payoff": 5, "applyItHere": [ { "moment": "A SPECIFIC dated moment from THEIR OWN days/excerpts \u2014 name the repo/project and the actual task in their vocabulary (~15-25 words).", "insteadDo": "How this exact technique changes that exact moment \u2014 concrete, imperative, their context (~15-25 words)." } ] } ],
17211
17217
  "alreadyStrong": [ { "title": "", "note": "What they already do well, concretely." } ]
17212
17218
  }
17213
17219
  \`\`\`
17214
- \`differenceRank\` is a strict 1..N ordering (1 = highest). Assign it per the PRIME DIRECTIVE and the rubric's general order; the report renders recommendations sorted by it. \`payoff\` (1-5) is a secondary strength cue. Recommend ONLY from the catalog.`;
17220
+ \`differenceRank\` is a strict 1..N ordering (1 = highest). Assign it per the PRIME DIRECTIVE and the rubric's general order; the report renders recommendations sorted by it. \`payoff\` (1-5) is a secondary strength cue. Recommend ONLY from the catalog.
17221
+ \`applyItHere\` HARD RULES: 3 entries per recommendation when the material supports 3, minimum 2 \u2014 and every moment must be VISIBLY present in THEIR RECENT DAYS or the excerpts (a real dated task, named the way they name it). NEVER invent, generalize, or reuse the same moment across recommendations. If the material truly supports only one real moment, give that one \u2014 a fabricated example is worse than a short list.`;
17215
17222
  const sha = promptSha([SYS, prompt, "sonnet"]);
17216
17223
  if (!process.argv.includes("--refresh")) {
17217
17224
  const prev = await reusableOutput(path13.join(CODING, "gap.json"), sha, (o) => Array.isArray(o.recommendations) && o.recommendations.length > 0);
package/dist/web/app.js CHANGED
@@ -7291,6 +7291,8 @@ var import_jsx_runtime = __toESM(require_jsx_runtime());
7291
7291
  function ShareAnalysisRow({ kind, label }) {
7292
7292
  const [state, setState] = (0, import_react.useState)("idle");
7293
7293
  const [err, setErr] = (0, import_react.useState)("");
7294
+ const [sync, setSync] = (0, import_react.useState)("idle");
7295
+ const [syncMsg, setSyncMsg] = (0, import_react.useState)("");
7294
7296
  const send = async () => {
7295
7297
  setState("sending");
7296
7298
  try {
@@ -7309,23 +7311,55 @@ function ShareAnalysisRow({ kind, label }) {
7309
7311
  setState("error");
7310
7312
  }
7311
7313
  };
7314
+ const doSync = async () => {
7315
+ setSync("sending");
7316
+ setSyncMsg("");
7317
+ try {
7318
+ const r = await fetch("/api/analysis/sync", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" }).then((r2) => r2.json());
7319
+ if (r.ok) {
7320
+ const n = r.machines?.length ?? 1;
7321
+ const overlap = r.overlappingSessions ? ` \xB7 ${r.overlappingSessions} overlapping, counted once` : "";
7322
+ setSyncMsg(`Synced \u2713 ${r.machine} \u2014 ${r.uniqueSessions.toLocaleString()} unique sessions across ${n} machine${n === 1 ? "" : "s"}${overlap}`);
7323
+ setSync("idle");
7324
+ } else {
7325
+ setSyncMsg(r.error || "couldn't sync");
7326
+ setSync("error");
7327
+ }
7328
+ } catch {
7329
+ setSyncMsg("couldn't sync \u2014 try again");
7330
+ setSync("error");
7331
+ }
7332
+ };
7312
7333
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mb-4 flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5 rounded-xl border border-line bg-paper px-3.5 py-2.5", children: [
7313
7334
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-[12px] text-muted", children: [
7314
7335
  "Optional: share this full ",
7315
7336
  label,
7316
- " as feedback for Polymath (will greatly help us make this better!). Private \u2014 nothing gets a public link."
7337
+ " as feedback for Polymath (will greatly help us make this better!). Private \u2014 nothing gets a public link.",
7338
+ kind === "coding" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mt-1 block", children: "Work on two machines? Sync each to your account and they combine, overlap counted once." })
7317
7339
  ] }),
7318
- state === "done" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "shrink-0 text-[12px] font-medium text-ink", children: "Shared \u2713" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "flex shrink-0 items-center gap-2", children: [
7319
- state === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[11px] text-amber-700", children: err }),
7320
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
7340
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "flex shrink-0 items-center gap-2", children: [
7341
+ syncMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `text-[11px] ${sync === "error" ? "text-amber-700" : "text-ink"}`, children: syncMsg }),
7342
+ kind === "coding" && !syncMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
7321
7343
  "button",
7322
7344
  {
7323
- onClick: send,
7324
- disabled: state === "sending",
7345
+ onClick: doSync,
7346
+ disabled: sync === "sending",
7325
7347
  className: "rounded-full border border-line px-3 py-1 text-[12px] text-ink transition-colors hover:border-accent disabled:opacity-50",
7326
- children: state === "sending" ? "Sending\u2026" : "Share it"
7348
+ children: sync === "sending" ? "Syncing\u2026" : "Sync to account"
7327
7349
  }
7328
- )
7350
+ ),
7351
+ state === "done" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[12px] font-medium text-ink", children: "Shared \u2713" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
7352
+ state === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[11px] text-amber-700", children: err }),
7353
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
7354
+ "button",
7355
+ {
7356
+ onClick: send,
7357
+ disabled: state === "sending",
7358
+ className: "rounded-full border border-line px-3 py-1 text-[12px] text-ink transition-colors hover:border-accent disabled:opacity-50",
7359
+ children: state === "sending" ? "Sending\u2026" : "Share it"
7360
+ }
7361
+ )
7362
+ ] })
7329
7363
  ] })
7330
7364
  ] });
7331
7365
  }
@@ -9640,7 +9674,14 @@ function FrontierPanel({ f, detail, coaching, gap, parallelism }) {
9640
9674
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-1 text-[12.5px] leading-relaxed text-ink", children: [
9641
9675
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "font-semibold text-accent", children: "Why it helps you \u2014 " }),
9642
9676
  r.whyItHelps
9643
- ] })
9677
+ ] }),
9678
+ r.applyItHere?.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-2 border-t border-line pt-2", children: [
9679
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-[11px] font-semibold uppercase tracking-wider text-faint", children: "Where this would have hit, in your own work" }),
9680
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "mt-1.5 space-y-2", children: r.applyItHere.map((ex, j) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { className: "leading-snug", children: [
9681
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-[12px] font-semibold text-ink", children: ex.insteadDo }),
9682
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mt-0.5 text-[11.5px] text-ink/80", children: ex.moment })
9683
+ ] }, j)) })
9684
+ ] }) : null
9644
9685
  ] }),
9645
9686
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "hidden flex-none items-center self-stretch sm:flex", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TipVisual, { title: r.title }) })
9646
9687
  ] }) }, i)) }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polymath-society",
3
- "version": "0.2.18",
3
+ "version": "0.2.19",
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",