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.
- package/dist/assets/index-CSuEsvho.js +47 -0
- package/dist/assets/index-eNaOrofs.css +2 -0
- package/dist/index.html +2 -2
- package/dist/server/index.js +348 -57
- package/dist/server/index.js.map +1 -1
- package/package.json +7 -8
- package/dist/assets/index-CRaAzMoD.css +0 -1
- package/dist/assets/index-NKjzCALB.js +0 -84
package/dist/server/index.js
CHANGED
|
@@ -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.
|
|
43906
|
-
const slot = counts.get(e.
|
|
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.
|
|
43910
|
+
counts.set(e.hook_id, slot);
|
|
43911
43911
|
}
|
|
43912
43912
|
const rows = [];
|
|
43913
|
-
for (const [
|
|
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({
|
|
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
|
|
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
|
-
|
|
44092
|
-
//
|
|
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.
|
|
44124
|
+
if (!e.hook_id) continue;
|
|
44106
44125
|
const decision = e.decision || "noop";
|
|
44107
|
-
out.push({ ts: e.ts, kind: "hook",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44232
|
-
|
|
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
|
|
44517
|
-
|
|
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
|
|
44520
|
-
|
|
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
|
|
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
|
-
|
|
44531
|
-
|
|
44532
|
-
|
|
44533
|
-
|
|
44534
|
-
|
|
44535
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
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
|
|
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 ?? "
|
|
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
|
| | | | | | |_| | | | | | | __/ |
|