mumei-dashboard 0.2.2 → 0.2.4

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.
@@ -43902,15 +43902,15 @@ async function aggregateHooksTopN(filePath, topN, windowH, now = /* @__PURE__ */
43902
43902
  const counts = /* @__PURE__ */ new Map();
43903
43903
  for await (const e of readJsonl(filePath)) {
43904
43904
  if (!e.ts || e.ts < cutoff) continue;
43905
- if (!e.rule_id) continue;
43906
- const slot = counts.get(e.rule_id) ?? { count: 0, decisions: /* @__PURE__ */ new Map() };
43905
+ if (!e.hook_id) continue;
43906
+ const slot = counts.get(e.hook_id) ?? { count: 0, decisions: /* @__PURE__ */ new Map() };
43907
43907
  slot.count += 1;
43908
43908
  const dec = e.decision || "noop";
43909
43909
  slot.decisions.set(dec, (slot.decisions.get(dec) ?? 0) + 1);
43910
- counts.set(e.rule_id, slot);
43910
+ counts.set(e.hook_id, slot);
43911
43911
  }
43912
43912
  const rows = [];
43913
- for (const [rule_id, slot] of counts) {
43913
+ for (const [hook_id, slot] of counts) {
43914
43914
  let topDecision = "noop";
43915
43915
  let topCount = -1;
43916
43916
  for (const [d, c] of slot.decisions) {
@@ -43919,7 +43919,7 @@ async function aggregateHooksTopN(filePath, topN, windowH, now = /* @__PURE__ */
43919
43919
  topDecision = d;
43920
43920
  }
43921
43921
  }
43922
- rows.push({ rule_id, count: slot.count, decision: topDecision });
43922
+ rows.push({ hook_id, count: slot.count, decision: topDecision });
43923
43923
  }
43924
43924
  rows.sort((a, b) => b.count - a.count);
43925
43925
  return rows.slice(0, topN);
@@ -43998,13 +43998,29 @@ var WINDOW_MS = 24 * 36e5;
43998
43998
  async function buildActivity(args) {
43999
43999
  const { projectRoot, limit, now = /* @__PURE__ */ new Date() } = args;
44000
44000
  const cutoff = new Date(now.getTime() - WINDOW_MS).toISOString();
44001
- const [commits, reviews, phases, hooks] = await Promise.all([
44001
+ const [commits, reviews, phases, hooks, subagents, archives] = await Promise.all([
44002
44002
  collectCommits(projectRoot, cutoff),
44003
44003
  collectReviews(projectRoot, cutoff),
44004
44004
  collectPhaseChanges(projectRoot, cutoff),
44005
- collectHooks(projectRoot, cutoff)
44005
+ collectHooks(projectRoot, cutoff),
44006
+ collectSubagents(projectRoot, cutoff),
44007
+ collectArchives(projectRoot, cutoff)
44006
44008
  ]);
44007
- const all = [...commits, ...reviews, ...phases, ...hooks];
44009
+ const denyHooks = hooks.filter(
44010
+ (e) => e.kind === "hook" && (e.decision === "deny" || e.decision === "block")
44011
+ );
44012
+ const cap = (events, n) => {
44013
+ const sorted = [...events].sort((a, b) => b.ts.localeCompare(a.ts));
44014
+ return sorted.slice(0, Math.max(0, n));
44015
+ };
44016
+ const all = [
44017
+ ...cap(commits, 10),
44018
+ ...cap(reviews, 10),
44019
+ ...cap(phases, 10),
44020
+ ...cap(denyHooks, 10),
44021
+ ...cap(subagents, 10),
44022
+ ...cap(archives, 5)
44023
+ ];
44008
44024
  all.sort((a, b) => b.ts.localeCompare(a.ts));
44009
44025
  return all.slice(0, Math.max(0, limit));
44010
44026
  }
@@ -44088,8 +44104,11 @@ async function collectPhaseChanges(projectRoot, cutoff) {
44088
44104
  ts,
44089
44105
  kind: "phase",
44090
44106
  slug,
44091
- from: phase,
44092
- // Without an explicit transition log we record the current phase as both endpoints.
44107
+ // Audit-log does not record phase transitions today (Out of Scope:
44108
+ // 新規 audit-log entry kind の追加). Without history we cannot
44109
+ // recover the previous phase, so emit from=null and let the UI
44110
+ // render '→ <to>' instead of pretending from === to.
44111
+ from: null,
44093
44112
  to: phase
44094
44113
  });
44095
44114
  } catch {
@@ -44102,9 +44121,9 @@ async function collectHooks(projectRoot, cutoff) {
44102
44121
  const out = [];
44103
44122
  for await (const e of readJsonl(file)) {
44104
44123
  if (!e.ts || e.ts < cutoff) continue;
44105
- if (!e.rule_id) continue;
44124
+ if (!e.hook_id) continue;
44106
44125
  const decision = e.decision || "noop";
44107
- out.push({ ts: e.ts, kind: "hook", rule_id: e.rule_id, decision });
44126
+ out.push({ ts: e.ts, kind: "hook", hook_id: e.hook_id, decision });
44108
44127
  }
44109
44128
  return out;
44110
44129
  }
@@ -44138,6 +44157,96 @@ async function collectStateFiles(projectRoot) {
44138
44157
  }
44139
44158
  return out;
44140
44159
  }
44160
+ var VALID_AGENTS = /* @__PURE__ */ new Set([
44161
+ "spec-compliance-reviewer",
44162
+ "security-reviewer",
44163
+ "adversarial-reviewer",
44164
+ "requirements-reviewer",
44165
+ "design-reviewer",
44166
+ "tasks-reviewer",
44167
+ "issue-validator",
44168
+ "memory-curator"
44169
+ ]);
44170
+ async function collectSubagents(projectRoot, cutoff) {
44171
+ const out = [];
44172
+ for (const file of await collectCostLogFiles(projectRoot)) {
44173
+ const slug = featureKeyForCostLog(file, projectRoot);
44174
+ if (!slug) continue;
44175
+ for await (const e of readJsonl(file)) {
44176
+ if (!e.ts || e.ts < cutoff) continue;
44177
+ if (!e.agent || !VALID_AGENTS.has(e.agent)) continue;
44178
+ if (e.phase !== "before" && e.phase !== "after") continue;
44179
+ const tokensTotal = (e.input_tokens ?? 0) + (e.output_tokens ?? 0);
44180
+ out.push({
44181
+ ts: e.ts,
44182
+ kind: "subagent",
44183
+ slug,
44184
+ agent: e.agent,
44185
+ phase: e.phase,
44186
+ tokens_total: tokensTotal
44187
+ });
44188
+ }
44189
+ }
44190
+ return out;
44191
+ }
44192
+ async function collectArchives(projectRoot, cutoff) {
44193
+ const archiveRoot = sp2__default.join(projectRoot, ".mumei", "archive");
44194
+ const out = [];
44195
+ for (const month of await safeReaddir2(archiveRoot)) {
44196
+ if (!month.isDirectory()) continue;
44197
+ const monthDir = sp2__default.join(archiveRoot, month.name);
44198
+ for (const slugEnt of await safeReaddir2(monthDir)) {
44199
+ if (!slugEnt.isDirectory()) continue;
44200
+ const slugDir = sp2__default.join(monthDir, slugEnt.name);
44201
+ try {
44202
+ const s = await stat(slugDir);
44203
+ const ts = s.mtime.toISOString();
44204
+ if (ts < cutoff) continue;
44205
+ out.push({
44206
+ ts,
44207
+ kind: "archive",
44208
+ slug: slugEnt.name,
44209
+ to: sp2__default.relative(projectRoot, slugDir)
44210
+ });
44211
+ } catch {
44212
+ }
44213
+ }
44214
+ }
44215
+ return out;
44216
+ }
44217
+ async function collectCostLogFiles(projectRoot) {
44218
+ const mumeiDir = sp2__default.join(projectRoot, ".mumei");
44219
+ const out = [];
44220
+ for (const sub of ["specs", "plans"]) {
44221
+ const dir = sp2__default.join(mumeiDir, sub);
44222
+ for (const ent of await safeReaddir2(dir)) {
44223
+ if (ent.isDirectory()) {
44224
+ out.push(sp2__default.join(dir, ent.name, "cost-log.jsonl"));
44225
+ }
44226
+ }
44227
+ }
44228
+ for (const month of await safeReaddir2(sp2__default.join(mumeiDir, "archive"))) {
44229
+ if (!month.isDirectory()) continue;
44230
+ const monthDir = sp2__default.join(mumeiDir, "archive", month.name);
44231
+ for (const slug of await safeReaddir2(monthDir)) {
44232
+ if (slug.isDirectory()) {
44233
+ out.push(sp2__default.join(monthDir, slug.name, "cost-log.jsonl"));
44234
+ }
44235
+ }
44236
+ }
44237
+ return out;
44238
+ }
44239
+ function featureKeyForCostLog(file, projectRoot) {
44240
+ const rel = sp2__default.relative(sp2__default.join(projectRoot, ".mumei"), file);
44241
+ const segments = rel.split(sp2__default.sep);
44242
+ if (segments.length === 3 && (segments[0] === "specs" || segments[0] === "plans")) {
44243
+ return segments[1] ?? null;
44244
+ }
44245
+ if (segments.length === 4 && segments[0] === "archive") {
44246
+ return segments[2] ?? null;
44247
+ }
44248
+ return null;
44249
+ }
44141
44250
  async function safeReaddir2(dir) {
44142
44251
  try {
44143
44252
  return await readdir(dir, { withFileTypes: true });
@@ -44147,6 +44256,21 @@ async function safeReaddir2(dir) {
44147
44256
  }
44148
44257
  var exec2 = promisify(execFile);
44149
44258
  var MEMO_TTL_MS = 5e3;
44259
+ var LOG_LEVEL_RANK = {
44260
+ trace: 10,
44261
+ debug: 20,
44262
+ info: 30,
44263
+ warn: 40,
44264
+ error: 50,
44265
+ fatal: 60
44266
+ };
44267
+ function shouldLog(level) {
44268
+ const configured = process.env.MUMEI_DASHBOARD_LOG_LEVEL ?? "warn";
44269
+ return (LOG_LEVEL_RANK[level] ?? 0) >= (LOG_LEVEL_RANK[configured] ?? 40);
44270
+ }
44271
+ function logAtLevel(level, msg) {
44272
+ if (shouldLog(level)) process.stderr.write(msg);
44273
+ }
44150
44274
  var memo = /* @__PURE__ */ new Map();
44151
44275
  async function resolveTasksFile(projectRoot, featureKey) {
44152
44276
  const direct = sp2__default.join(projectRoot, ".mumei", "specs", featureKey, "tasks.md");
@@ -44173,9 +44297,9 @@ async function resolveTasksFile(projectRoot, featureKey) {
44173
44297
  } catch {
44174
44298
  }
44175
44299
  try {
44176
- const months = await fs.readdir(sp2__default.join(projectRoot, ".mumei", "archive"), {
44300
+ const months = (await fs.readdir(sp2__default.join(projectRoot, ".mumei", "archive"), {
44177
44301
  withFileTypes: true
44178
- });
44302
+ })).sort((a, b) => b.name.localeCompare(a.name));
44179
44303
  for (const month of months) {
44180
44304
  if (!month.isDirectory()) continue;
44181
44305
  const monthDir = sp2__default.join(projectRoot, ".mumei", "archive", month.name);
@@ -44209,12 +44333,14 @@ async function buildWaveplan(args) {
44209
44333
  if (stale) {
44210
44334
  try {
44211
44335
  await access(sp2__default.join(projectRoot, ".mumei", "plans", featureKey));
44212
- process.stderr.write(
44336
+ logAtLevel(
44337
+ "debug",
44213
44338
  `[tasks-bridge] plan-vehicle feature ${featureKey} has no tasks.md by design \u2014 returning empty waveplan
44214
44339
  `
44215
44340
  );
44216
44341
  } catch {
44217
- process.stderr.write(
44342
+ logAtLevel(
44343
+ "debug",
44218
44344
  `[tasks-bridge] no tasks.md found for ${featureKey} (specs and plans both absent) \u2014 returning empty waveplan
44219
44345
  `
44220
44346
  );
@@ -44228,10 +44354,8 @@ async function buildWaveplan(args) {
44228
44354
  waveplan = await parseTasksMdViaBash({ pluginRoot, tasksFile: tf, featureKey, projectRoot });
44229
44355
  } catch (err) {
44230
44356
  const message = err instanceof Error ? err.message : String(err);
44231
- process.stderr.write(
44232
- `[tasks-bridge] parseTasksMdViaBash failed for ${featureKey}: ${message}
44233
- `
44234
- );
44357
+ logAtLevel("warn", `[tasks-bridge] parseTasksMdViaBash failed for ${featureKey}: ${message}
44358
+ `);
44235
44359
  waveplan = [];
44236
44360
  }
44237
44361
  memo.set(memoKey, { ts: Date.now(), payload: waveplan });
@@ -44344,9 +44468,11 @@ async function buildFeatureDetail(args) {
44344
44468
  featureKey,
44345
44469
  reviews
44346
44470
  });
44471
+ const archived = dir.absDir.includes(`${sp2__default.sep}archive${sp2__default.sep}`);
44347
44472
  return {
44348
44473
  slug,
44349
44474
  planVehicle,
44475
+ archived,
44350
44476
  timeline,
44351
44477
  acs,
44352
44478
  waveplan: waveplan.map((w) => ({
@@ -44389,7 +44515,9 @@ async function resolveFeatureDir(projectRoot, featureKey) {
44389
44515
  const archiveRoot = sp2__default.join(projectRoot, ".mumei", "archive");
44390
44516
  try {
44391
44517
  const fs = await import('fs/promises');
44392
- const months = await fs.readdir(archiveRoot, { withFileTypes: true });
44518
+ const months = (await fs.readdir(archiveRoot, { withFileTypes: true })).sort(
44519
+ (a, b) => b.name.localeCompare(a.name)
44520
+ );
44393
44521
  for (const month of months) {
44394
44522
  if (!month.isDirectory()) continue;
44395
44523
  const monthDir = sp2__default.join(archiveRoot, month.name);
@@ -44513,41 +44641,181 @@ async function loadCostPerIter(args) {
44513
44641
  };
44514
44642
  });
44515
44643
  }
44516
- async function buildTimeline(args) {
44517
- const out = [];
44644
+ async function readStateJson(featureDir) {
44645
+ try {
44646
+ const body = await readFile(sp2__default.join(featureDir, "state.json"), "utf8");
44647
+ return JSON.parse(body);
44648
+ } catch {
44649
+ return null;
44650
+ }
44651
+ }
44652
+ async function tryFileMtime(featureDir, rel, label, out) {
44653
+ try {
44654
+ const s = await stat(sp2__default.join(featureDir, rel));
44655
+ out.push({ ts: s.mtime.toISOString(), event: label, ref: null });
44656
+ } catch {
44657
+ }
44658
+ }
44659
+ async function collectSpecReviewEvents(featureDir, out) {
44660
+ const dir = sp2__default.join(featureDir, "spec-reviews");
44661
+ let entries;
44662
+ try {
44663
+ entries = await readdir(dir, { withFileTypes: true });
44664
+ } catch {
44665
+ return;
44666
+ }
44667
+ for (const ent of entries) {
44668
+ if (!ent.isFile() || !ent.name.endsWith(".json")) continue;
44669
+ const m = /^.+Z-(requirements|design|tasks)\.json$/.exec(ent.name);
44670
+ if (!m) continue;
44671
+ const doc = m[1];
44672
+ const fp = sp2__default.join(dir, ent.name);
44673
+ try {
44674
+ const body = JSON.parse(await readFile(fp, "utf8"));
44675
+ if (!body.verdict) continue;
44676
+ const s = await stat(fp);
44677
+ out.push({
44678
+ ts: s.mtime.toISOString(),
44679
+ event: `spec-review/${doc} iter ${body.iteration ?? 1} ${body.verdict}`,
44680
+ ref: null
44681
+ });
44682
+ } catch {
44683
+ }
44684
+ }
44685
+ }
44686
+ function escapeRegex(s) {
44687
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
44688
+ }
44689
+ async function collectImplementationCommits(projectRoot, featureKey, out) {
44690
+ const idMatch = /^REQ-\d+/.exec(featureKey);
44691
+ const slugRe = new RegExp(`(?:^|\\W)${escapeRegex(featureKey)}(?:\\W|$)`);
44692
+ const idRe = idMatch ? new RegExp(`(?:^|\\W)${escapeRegex(idMatch[0])}(?:\\W|$)`) : null;
44693
+ let stdout;
44518
44694
  try {
44519
- const s = await stat(sp2__default.join(args.featureDir, "state.json"));
44520
- out.push({ ts: s.birthtime.toISOString(), event: "created", ref: null });
44695
+ const r = await exec3("git", ["log", "-n", "200", "--format=%cI%x09%H%x09%s"], {
44696
+ cwd: projectRoot,
44697
+ maxBuffer: 4 * 1024 * 1024
44698
+ });
44699
+ stdout = r.stdout;
44521
44700
  } catch {
44701
+ return;
44522
44702
  }
44523
- for (const r of args.reviews) {
44703
+ for (const line of stdout.split("\n").filter(Boolean)) {
44704
+ const [ts, sha, ...rest] = line.split(" ");
44705
+ if (!ts || !sha) continue;
44706
+ const subj = rest.join(" ");
44707
+ if (!slugRe.test(subj) && !(idRe && idRe.test(subj))) continue;
44708
+ const wm = /Wave\s+(\d+)/i.exec(subj);
44709
+ const event = wm ? `Wave ${wm[1]} commit: ${subj}` : `commit: ${subj}`;
44710
+ out.push({ ts, event, ref: sha });
44711
+ }
44712
+ }
44713
+ async function buildSpecTimeline(args) {
44714
+ const out = [];
44715
+ const { featureDir, projectRoot, featureKey, reviews } = args;
44716
+ await tryFileMtime(featureDir, "requirements.md", "requirements.md drafted", out);
44717
+ await tryFileMtime(featureDir, "design.md", "design.md drafted", out);
44718
+ await tryFileMtime(featureDir, "tasks.md", "tasks.md drafted", out);
44719
+ await collectSpecReviewEvents(featureDir, out);
44720
+ const state = await readStateJson(featureDir);
44721
+ if (state?.approved_at) {
44722
+ out.push({ ts: state.approved_at, event: "approved by user", ref: null });
44723
+ }
44724
+ if (state?.phase && state.phase !== "plan") {
44725
+ try {
44726
+ const s = await stat(sp2__default.join(featureDir, "state.json"));
44727
+ out.push({
44728
+ ts: s.mtime.toISOString(),
44729
+ event: `phase: (unknown) \u2192 ${state.phase}`,
44730
+ ref: null
44731
+ });
44732
+ } catch {
44733
+ }
44734
+ }
44735
+ for (const r of reviews) {
44736
+ const wavePart = typeof r.wave === "number" ? `Wave ${r.wave} ` : "";
44737
+ out.push({
44738
+ ts: r.ts,
44739
+ event: `review ${wavePart}iter ${r.iteration} ${r.verdict}`.replace(/\s+/g, " ").trim(),
44740
+ ref: null
44741
+ });
44742
+ }
44743
+ await collectImplementationCommits(projectRoot, featureKey, out);
44744
+ if (featureDir.includes(`${sp2__default.sep}archive${sp2__default.sep}`)) {
44745
+ try {
44746
+ const s = await stat(featureDir);
44747
+ out.push({ ts: s.mtime.toISOString(), event: "archived", ref: null });
44748
+ } catch {
44749
+ }
44750
+ }
44751
+ return out;
44752
+ }
44753
+ async function buildPlanTimeline(args) {
44754
+ const out = [];
44755
+ const { featureDir, projectRoot, featureKey, reviews } = args;
44756
+ await tryFileMtime(featureDir, "plan.md", "plan.md captured", out);
44757
+ const state = await readStateJson(featureDir);
44758
+ if (typeof state?.task_completed_count === "number" && state.task_completed_count > 0) {
44759
+ try {
44760
+ const s = await stat(sp2__default.join(featureDir, "state.json"));
44761
+ const ts = s.mtime.toISOString();
44762
+ for (let i = 1; i <= state.task_completed_count; i++) {
44763
+ out.push({
44764
+ ts,
44765
+ event: `task ${i} completed`,
44766
+ ref: null
44767
+ });
44768
+ }
44769
+ } catch {
44770
+ }
44771
+ }
44772
+ if (state?.pending_review) {
44773
+ try {
44774
+ const s = await stat(sp2__default.join(featureDir, "state.json"));
44775
+ out.push({ ts: s.mtime.toISOString(), event: "pending review", ref: null });
44776
+ } catch {
44777
+ }
44778
+ }
44779
+ for (const r of reviews) {
44524
44780
  out.push({
44525
44781
  ts: r.ts,
44526
44782
  event: `review iter ${r.iteration} ${r.verdict}`,
44527
44783
  ref: null
44528
44784
  });
44529
44785
  }
44530
- try {
44531
- const { stdout } = await exec3(
44532
- "git",
44533
- [
44534
- "log",
44535
- "-n",
44536
- "20",
44537
- "--format=%cI%x09%H%x09%s",
44538
- "--",
44539
- sp2__default.relative(args.projectRoot, args.featureDir)
44540
- ],
44541
- { cwd: args.projectRoot, maxBuffer: 1024 * 1024 }
44542
- );
44543
- for (const line of stdout.split("\n").filter(Boolean)) {
44544
- const [ts, sha, ...rest] = line.split(" ");
44545
- if (!ts || !sha) continue;
44546
- out.push({ ts, event: `commit: ${rest.join(" ")}`, ref: sha });
44786
+ await collectImplementationCommits(projectRoot, featureKey, out);
44787
+ if (featureDir.includes(`${sp2__default.sep}archive${sp2__default.sep}`)) {
44788
+ try {
44789
+ const s = await stat(featureDir);
44790
+ out.push({ ts: s.mtime.toISOString(), event: "archived", ref: null });
44791
+ } catch {
44547
44792
  }
44548
- } catch {
44549
44793
  }
44550
- return out.sort((a, b) => a.ts.localeCompare(b.ts));
44794
+ return out;
44795
+ }
44796
+ function dedupTimeline(events) {
44797
+ const sorted = [...events].sort((a, b) => a.ts.localeCompare(b.ts));
44798
+ const out = [];
44799
+ for (const ev of sorted) {
44800
+ const prev = out[out.length - 1];
44801
+ if (!prev) {
44802
+ out.push(ev);
44803
+ continue;
44804
+ }
44805
+ const sameSecond = prev.ts.slice(0, 19) === ev.ts.slice(0, 19);
44806
+ if (sameSecond && prev.event === ev.event) continue;
44807
+ if (sameSecond && prev.ref === null && ev.ref !== null && /^phase: .* → /.test(prev.event)) {
44808
+ out[out.length - 1] = ev;
44809
+ continue;
44810
+ }
44811
+ out.push(ev);
44812
+ }
44813
+ return out;
44814
+ }
44815
+ async function buildTimeline(args) {
44816
+ const isSpec = /(?:^|[\\/])specs[\\/]/.test(args.featureDir) || /(?:^|[\\/])archive[\\/][^\\/]+[\\/]REQ-\d+-/.test(args.featureDir);
44817
+ const events = isSpec ? await buildSpecTimeline(args) : await buildPlanTimeline(args);
44818
+ return dedupTimeline(events);
44551
44819
  }
44552
44820
  var exec4 = promisify(execFile);
44553
44821
  var PHASE_NEXT = {
@@ -44823,7 +45091,7 @@ function buildMeta(args) {
44823
45091
  async function buildMetaStats(args) {
44824
45092
  const { projectRoot, now = /* @__PURE__ */ new Date() } = args;
44825
45093
  const mumeiDir = sp2__default.join(projectRoot, ".mumei");
44826
- const costFiles = await collectCostLogFiles(mumeiDir);
45094
+ const costFiles = await collectCostLogFiles2(mumeiDir);
44827
45095
  const hookStatsFile = sp2__default.join(mumeiDir, ".hook-stats.jsonl");
44828
45096
  const reviewDirs = await collectReviewDirs2(mumeiDir);
44829
45097
  const gitTimestamps = await collectGitTimestamps(projectRoot);
@@ -44841,7 +45109,7 @@ async function buildMetaStats(args) {
44841
45109
  eventCount24h: eventCount24h2
44842
45110
  };
44843
45111
  }
44844
- async function collectCostLogFiles(mumeiDir) {
45112
+ async function collectCostLogFiles2(mumeiDir) {
44845
45113
  const out = [];
44846
45114
  out.push(sp2__default.join(mumeiDir, "cost-log.jsonl"));
44847
45115
  for (const sub of ["specs", "plans"]) {
@@ -46703,6 +46971,9 @@ function classify(mumeiDir, absPath) {
46703
46971
  if (tailJoined === "cost-log.jsonl") {
46704
46972
  return { kind: "cost-log", slug: finalSlug, subroot, filePath: absPath };
46705
46973
  }
46974
+ if (tailJoined === "tasks.md") {
46975
+ return { kind: "tasks", slug: finalSlug, subroot, filePath: absPath };
46976
+ }
46706
46977
  if (tail[0] === "reviews" && /\.json$/.test(tail[1] ?? "")) {
46707
46978
  return { kind: "review", slug: finalSlug, subroot, filePath: absPath };
46708
46979
  }
@@ -46779,13 +47050,13 @@ function registerSse(app2, args) {
46779
47050
  reply.raw.flushHeaders?.();
46780
47051
  const c = { id: nextId++, reply };
46781
47052
  clients.add(c);
46782
- app2.log.info({ clientId: c.id, total: clients.size }, "sse client connected");
47053
+ app2.log.debug({ clientId: c.id, total: clients.size }, "sse client connected");
46783
47054
  reply.raw.write(`: open ${(/* @__PURE__ */ new Date()).toISOString()}
46784
47055
 
46785
47056
  `);
46786
47057
  req.raw.on("close", () => {
46787
47058
  clients.delete(c);
46788
- app2.log.info({ clientId: c.id, total: clients.size }, "sse client disconnected");
47059
+ app2.log.debug({ clientId: c.id, total: clients.size }, "sse client disconnected");
46789
47060
  });
46790
47061
  });
46791
47062
  return {
@@ -46813,13 +47084,13 @@ function registerSse(app2, args) {
46813
47084
  }
46814
47085
  };
46815
47086
  }
46816
- function handleRawEvent(raw, projectRoot, emit, debouncer, debounceMs) {
47087
+ function handleRawEvent(raw, _projectRoot, emit, debouncer, debounceMs) {
46817
47088
  switch (raw.kind) {
46818
47089
  case "state": {
46819
47090
  if (!raw.slug) return;
46820
47091
  const slug = raw.slug;
46821
47092
  debouncer.schedule(
46822
- `feature.update::${slug}`,
47093
+ `feature.update::${slug}::state`,
46823
47094
  debounceMs,
46824
47095
  () => emit({ type: "feature.update", slug })
46825
47096
  );
@@ -46840,9 +47111,9 @@ function handleRawEvent(raw, projectRoot, emit, debouncer, debounceMs) {
46840
47111
  if (!raw.slug) return;
46841
47112
  const slug = raw.slug;
46842
47113
  debouncer.schedule(
46843
- `feature.update::${slug}`,
47114
+ `feature.update::${slug}::review`,
46844
47115
  debounceMs,
46845
- () => emit({ type: "feature.update", slug })
47116
+ () => emit({ type: "feature.update", slug, affects: ["reviews"] })
46846
47117
  );
46847
47118
  debouncer.schedule(
46848
47119
  "activity.changed::review",
@@ -46852,6 +47123,11 @@ function handleRawEvent(raw, projectRoot, emit, debouncer, debounceMs) {
46852
47123
  return;
46853
47124
  }
46854
47125
  case "hook-stats": {
47126
+ debouncer.schedule(
47127
+ "feature.update::hooks",
47128
+ debounceMs,
47129
+ () => emit({ type: "feature.update", affects: ["hooks"] })
47130
+ );
46855
47131
  debouncer.schedule(
46856
47132
  "activity.changed::hook",
46857
47133
  debounceMs,
@@ -46859,10 +47135,25 @@ function handleRawEvent(raw, projectRoot, emit, debouncer, debounceMs) {
46859
47135
  );
46860
47136
  return;
46861
47137
  }
47138
+ case "tasks": {
47139
+ if (!raw.slug) return;
47140
+ const slug = raw.slug;
47141
+ debouncer.schedule(
47142
+ `feature.update::${slug}::tasks`,
47143
+ debounceMs,
47144
+ () => emit({ type: "feature.update", slug })
47145
+ );
47146
+ debouncer.schedule(
47147
+ "activity.changed::tasks",
47148
+ debounceMs,
47149
+ () => emit({ type: "activity.changed" })
47150
+ );
47151
+ return;
47152
+ }
46862
47153
  }
46863
47154
  }
46864
47155
  async function trendTokens(args) {
46865
- const files = await collectCostLogFiles2(args.projectRoot);
47156
+ const files = await collectCostLogFiles3(args.projectRoot);
46866
47157
  return aggregateTokensByDay(files, args.days, args.now);
46867
47158
  }
46868
47159
  async function trendReviews(args) {
@@ -46873,7 +47164,7 @@ async function trendHooks(args) {
46873
47164
  const file = sp2__default.join(args.projectRoot, ".mumei", ".hook-stats.jsonl");
46874
47165
  return aggregateHooksTopN(file, args.topN, args.windowH, args.now);
46875
47166
  }
46876
- async function collectCostLogFiles2(projectRoot) {
47167
+ async function collectCostLogFiles3(projectRoot) {
46877
47168
  const mumeiDir = sp2__default.join(projectRoot, ".mumei");
46878
47169
  const out = [sp2__default.join(mumeiDir, "cost-log.jsonl")];
46879
47170
  for (const sub of ["specs", "plans"]) {
@@ -46934,7 +47225,7 @@ function resolveProjectRoot(start = process.cwd()) {
46934
47225
  var PROJECT_ROOT = process.env.MUMEI_DASHBOARD_PROJECT_ROOT ? sp2__default.resolve(process.env.MUMEI_DASHBOARD_PROJECT_ROOT) : resolveProjectRoot();
46935
47226
  var MUMEI_DIR = sp2__default.join(PROJECT_ROOT, ".mumei");
46936
47227
  var PORT = Number(process.env.MUMEI_DASHBOARD_PORT ?? "3001");
46937
- var LOG_LEVEL = process.env.MUMEI_DASHBOARD_LOG_LEVEL ?? "info";
47228
+ var LOG_LEVEL = process.env.MUMEI_DASHBOARD_LOG_LEVEL ?? "warn";
46938
47229
  var CORS_ORIGINS = process.env.MUMEI_DASHBOARD_CORS_ORIGINS?.split(",").map((s) => s.trim()).filter(Boolean) ?? [
46939
47230
  `http://localhost:${PORT}`,
46940
47231
  `http://127.0.0.1:${PORT}`,
@@ -47113,7 +47404,7 @@ app.setErrorHandler((err, req, reply) => {
47113
47404
  });
47114
47405
  });
47115
47406
  var sse = registerSse(app, { projectRoot: PROJECT_ROOT });
47116
- var LOGO_ASCII = ` _
47407
+ var LOGO_ASCII = ` _
47117
47408
  _ __ ___ _ _ _ __ ___ ___(_)
47118
47409
  | '_ \` _ \\| | | | '_ \` _ \\ / _ \\ |
47119
47410
  | | | | | | |_| | | | | | | __/ |