blun-king-cli 9.0.0 → 9.0.1

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.
Files changed (55) hide show
  1. package/LIESMICH.txt +1 -7
  2. package/README.md +4 -16
  3. package/bin/blun.js +248 -160
  4. package/bin/core-bootstrap.js +47 -0
  5. package/bin/king.js +277 -1
  6. package/bin/launcher-mode.js +2 -1
  7. package/bin/launcher-runtime.js +221 -0
  8. package/bin/plugin-bootstrap.js +0 -0
  9. package/bin/private-paths.js +0 -0
  10. package/bin/update-lease.js +399 -0
  11. package/bin/update-notice.js +1094 -0
  12. package/blun.mjs +4060 -6667
  13. package/package.json +3 -10
  14. package/skills/screenshot-lesen/SKILL.md +0 -1
  15. package/skills/web-lesen/SKILL.md +0 -1
  16. package/telegram-plugin/dist/bridge.mjs +1 -21
  17. package/mnemo/access_routes.js +0 -692
  18. package/mnemo/agent_governance.js +0 -4242
  19. package/mnemo/agent_mail.js +0 -901
  20. package/mnemo/bootstrap_auto.js +0 -137
  21. package/mnemo/brief_coordination.js +0 -226
  22. package/mnemo/code_read_tools.js +0 -375
  23. package/mnemo/context_preview_tools.js +0 -603
  24. package/mnemo/embeddings.js +0 -66
  25. package/mnemo/external_repo_ops.js +0 -575
  26. package/mnemo/facts/example-project-rules.json +0 -90
  27. package/mnemo/facts/example.json +0 -34
  28. package/mnemo/identity_schema.sql +0 -139
  29. package/mnemo/journal_schema.js +0 -561
  30. package/mnemo/loop_doctor_tools.js +0 -661
  31. package/mnemo/mail_secret_refs.js +0 -150
  32. package/mnemo/mcp.js +0 -9309
  33. package/mnemo/memory_consolidation.js +0 -1914
  34. package/mnemo/memory_health_tools.js +0 -165
  35. package/mnemo/package.json +0 -79
  36. package/mnemo/protected_scope_gate.js +0 -627
  37. package/mnemo/resource_access_control.js +0 -684
  38. package/mnemo/runtime_governance.js +0 -1256
  39. package/mnemo/runtime_turn_gate.js +0 -862
  40. package/mnemo/sandbox.js +0 -143
  41. package/mnemo/schema.sql +0 -389
  42. package/mnemo/shared_utils.js +0 -763
  43. package/mnemo/skills/agent-auto-resume/SKILL.md +0 -56
  44. package/mnemo/skills/agent_hand/SKILL.md +0 -43
  45. package/mnemo/skills/agent_hand/run.js +0 -63
  46. package/mnemo/skills/book_flight/SKILL.md +0 -34
  47. package/mnemo/skills/external_repo_review/SKILL.md +0 -43
  48. package/mnemo/skills/external_repo_review/run.js +0 -73
  49. package/mnemo/skills/pay_invoice/SKILL.md +0 -34
  50. package/mnemo/team_quality_ops.js +0 -944
  51. package/mnemo/timeline_report_tools.js +0 -810
  52. package/mnemo/write_gate_risk.js +0 -80
  53. package/mnemo/writer_health.js +0 -152
  54. package/skills/doku-ingestion/SKILL.md +0 -48
  55. package/skills/doku-ingestion/ingest_docs.py +0 -133
@@ -1,810 +0,0 @@
1
- "use strict";
2
-
3
- const { parseMaybeJson } = require("./shared_utils");
4
-
5
- const TIMELINE_REPORT_TOOL_DEFS = {
6
- mem_project_timeline_report: {
7
- description: "Render a token-budgeted project dossier: recent timeline, live-readiness gates, open defects, claims, briefs, handoffs, decisions, and concrete next actions.",
8
- inputSchema: {
9
- type: "object",
10
- properties: {
11
- project: { type: "string" },
12
- agent_name: { type: "string" },
13
- days: { type: "integer", default: 30 },
14
- token_budget: { type: "integer", default: 3000 },
15
- max_items: { type: "integer", default: 8 },
16
- live_focus: { type: "boolean", default: true },
17
- include_doc: { type: "boolean", default: true },
18
- },
19
- required: ["project"],
20
- },
21
- },
22
- };
23
-
24
- function handleTimelineReportTool(db, name, args) {
25
- if (!TIMELINE_REPORT_TOOL_DEFS[name]) return { handled: false };
26
- return { handled: true, result: projectTimelineReport(db, args || {}) };
27
- }
28
-
29
- function projectTimelineReport(db, args) {
30
- const project = clean(args.project || args.name, 160);
31
- if (!project) return { error: "project required" };
32
- const agentName = normalizeAgentId(args.agent_name);
33
- const days = clampInt(args.days, 30, 1, 3650);
34
- const tokenBudget = clampInt(args.token_budget, 3000, 800, 24000);
35
- const maxItems = clampInt(args.max_items, 8, 3, 50);
36
- const liveFocus = args.live_focus !== false;
37
- const includeDoc = args.include_doc !== false;
38
- const sinceIso = new Date(Date.now() - days * 86400000).toISOString();
39
-
40
- const registry = selectProjectRegistry(db, project);
41
- const rules = selectProjectRules(db, project);
42
- const siteContract = selectSiteContract(db, project);
43
- const latestGolden = selectLatestGoldenCheck(db, project);
44
- const openFindings = selectOpenFindings(db, project, maxItems);
45
- const activeClaims = selectActiveClaims(db, project, maxItems);
46
- const autonomyTasks = selectAutonomyTasks(db, project, maxItems);
47
- const departmentCoverage = selectDepartmentCoverage(db, project);
48
- const pendingBriefs = selectPendingBriefs(db, project, agentName, maxItems);
49
- const handoffs = selectRecentHandoffs(db, project, agentName, sinceIso, maxItems);
50
- const decisions = selectRecentDecisions(db, project, sinceIso, maxItems);
51
- const actions = selectRecentActions(db, project, agentName, sinceIso, maxItems);
52
- const memories = selectMemorySnippets(db, project, sinceIso, maxItems);
53
- const timeline = buildTimeline({
54
- registry,
55
- rules,
56
- siteContract,
57
- latestGolden,
58
- openFindings,
59
- activeClaims,
60
- autonomyTasks,
61
- pendingBriefs,
62
- handoffs,
63
- decisions,
64
- actions,
65
- memories,
66
- maxItems,
67
- });
68
- const readiness = deriveLiveReadiness({
69
- project,
70
- registry,
71
- rules,
72
- siteContract,
73
- latestGolden,
74
- openFindings,
75
- activeClaims,
76
- autonomyTasks,
77
- liveFocus,
78
- });
79
- const nextActions = deriveNextActions(readiness, {
80
- openFindings,
81
- activeClaims,
82
- autonomyTasks,
83
- pendingBriefs,
84
- latestGolden,
85
- });
86
-
87
- const report = {
88
- ok: true,
89
- generated_at: new Date().toISOString(),
90
- project,
91
- agent_name: agentName || null,
92
- days,
93
- live_focus: liveFocus,
94
- token_budget: tokenBudget,
95
- summary: {
96
- status: readiness.status,
97
- readiness_percent: readiness.readiness_percent,
98
- blockers: readiness.blockers.length,
99
- warnings: readiness.warnings.length,
100
- open_findings: openFindings.length,
101
- active_claims: activeClaims.length,
102
- autonomy_open_or_review: autonomyTasks.filter((t) => !["done", "closed", "resolved"].includes(String(t.status || "").toLowerCase())).length,
103
- pending_or_dispatched_briefs: pendingBriefs.length,
104
- latest_golden_check_status: latestGolden ? latestGolden.status : null,
105
- last_activity_at: timeline.length ? timeline[0].at : null,
106
- },
107
- live_readiness: readiness,
108
- next_actions: nextActions,
109
- sections: {
110
- registry: registry ? redactRow(registry) : null,
111
- project_rules: rules ? redactRuleRow(rules) : null,
112
- site_contract: siteContract ? redactSiteContract(siteContract) : null,
113
- latest_golden_check: latestGolden ? redactRow(latestGolden) : null,
114
- open_findings: openFindings.map(redactRow),
115
- active_claims: activeClaims.map(redactRow),
116
- autonomy_tasks: autonomyTasks.map(redactRow),
117
- department_coverage: departmentCoverage.map(redactRow),
118
- pending_briefs: pendingBriefs.map(redactRow),
119
- recent_handoffs: handoffs.map(redactRow),
120
- recent_decisions: decisions.map(redactRow),
121
- recent_actions: actions.map(redactRow),
122
- memory_snippets: memories.map(redactRow),
123
- timeline,
124
- },
125
- };
126
-
127
- if (includeDoc) {
128
- const rendered = renderReport(report, tokenBudget);
129
- report.doc = rendered.doc;
130
- report.estimated_doc_tokens = rendered.estimated_tokens;
131
- report.truncated = rendered.truncated;
132
- }
133
- report.next_step = readiness.status === "ready"
134
- ? "Assign a reviewer to run the golden checks and capture the evidence before marking the project live."
135
- : "Work the blockers in next_actions first, then re-run mem_project_timeline_report until status is ready.";
136
- return report;
137
- }
138
-
139
- function selectProjectRegistry(db, project) {
140
- if (!tableExists(db, "project_registry")) return null;
141
- return safeGet(db, "SELECT * FROM project_registry WHERE lower(name)=lower(?) LIMIT 1", [project]);
142
- }
143
-
144
- function selectProjectRules(db, project) {
145
- if (!tableExists(db, "project_rules")) return null;
146
- return safeGet(db, "SELECT * FROM project_rules WHERE lower(project)=lower(?) LIMIT 1", [project]);
147
- }
148
-
149
- function selectSiteContract(db, project) {
150
- if (!tableExists(db, "site_contract")) return null;
151
- return safeGet(db, "SELECT * FROM site_contract WHERE lower(project)=lower(?) LIMIT 1", [project]);
152
- }
153
-
154
- function selectLatestGoldenCheck(db, project) {
155
- if (!tableExists(db, "golden_check_run")) return null;
156
- return safeGet(db, "SELECT id, project, agent_name, status, command, summary, evidence_json, findings_json, created_at FROM golden_check_run WHERE lower(project)=lower(?) ORDER BY created_at DESC LIMIT 1", [project]);
157
- }
158
-
159
- function selectOpenFindings(db, project, limit) {
160
- if (!tableExists(db, "quality_finding")) return [];
161
- return safeAll(db, `
162
- SELECT id, project, category, severity, title, url, expected, actual, status, source_agent, updated_at, created_at
163
- FROM quality_finding
164
- WHERE lower(project)=lower(?)
165
- AND COALESCE(status,'open') NOT IN ('resolved','closed','done')
166
- ORDER BY CASE lower(severity)
167
- WHEN 'critical' THEN 0 WHEN 'crit' THEN 0 WHEN 'c' THEN 0 WHEN 'p0' THEN 0
168
- WHEN 'high' THEN 1 WHEN 'h' THEN 1 WHEN 'p1' THEN 1
169
- WHEN 'medium' THEN 2 WHEN 'm' THEN 2 WHEN 'p2' THEN 2
170
- ELSE 3 END, updated_at DESC
171
- LIMIT ?`, [project, limit]);
172
- }
173
-
174
- function selectActiveClaims(db, project, limit) {
175
- if (!tableExists(db, "work_claim")) return [];
176
- return safeAll(db, `
177
- SELECT id, project, file_path, agent_name, summary, claimed_at, expires_at, status
178
- FROM work_claim
179
- WHERE lower(project)=lower(?)
180
- AND COALESCE(status,'active')='active'
181
- AND (expires_at IS NULL OR expires_at > strftime('%Y-%m-%dT%H:%M:%fZ','now'))
182
- ORDER BY expires_at ASC
183
- LIMIT ?`, [project, limit]);
184
- }
185
-
186
- function selectAutonomyTasks(db, project, limit) {
187
- if (!tableExists(db, "autonomy_task")) return [];
188
- return safeAll(db, `
189
- SELECT id, project, department_name, title, category, severity, status, assigned_agent, reviewer_agent, updated_at, created_at
190
- FROM autonomy_task
191
- WHERE lower(project)=lower(?)
192
- AND COALESCE(status,'open') NOT IN ('done','closed','resolved')
193
- ORDER BY CASE lower(severity)
194
- WHEN 'critical' THEN 0 WHEN 'crit' THEN 0 WHEN 'c' THEN 0 WHEN 'p0' THEN 0
195
- WHEN 'high' THEN 1 WHEN 'h' THEN 1 WHEN 'p1' THEN 1
196
- WHEN 'medium' THEN 2 WHEN 'm' THEN 2 WHEN 'p2' THEN 2
197
- ELSE 3 END, updated_at DESC
198
- LIMIT ?`, [project, limit]);
199
- }
200
-
201
- function selectDepartmentCoverage(db, project) {
202
- if (!tableExists(db, "autonomy_task")) return [];
203
- return safeAll(db, `
204
- SELECT department_name, status, COUNT(*) AS count
205
- FROM autonomy_task
206
- WHERE lower(project)=lower(?)
207
- GROUP BY department_name, status
208
- ORDER BY department_name ASC, status ASC`, [project]);
209
- }
210
-
211
- function selectPendingBriefs(db, project, agentName, limit) {
212
- if (!tableExists(db, "agent_brief")) return [];
213
- const params = [];
214
- const where = ["status IN ('pending','dispatched')"];
215
- if (agentName) {
216
- where.push("lower(agent_name)=lower(?)");
217
- params.push(agentName);
218
- }
219
- where.push("(content LIKE ? OR meta_json LIKE ?)");
220
- params.push(like(project), like(project));
221
- params.push(limit);
222
- return safeAll(db, `
223
- SELECT id, agent_name, source_agent, status, created_at, dispatched_at, substr(content,1,260) AS preview
224
- FROM agent_brief
225
- WHERE ${where.join(" AND ")}
226
- ORDER BY created_at DESC
227
- LIMIT ?`, params);
228
- }
229
-
230
- function selectRecentHandoffs(db, project, agentName, sinceIso, limit) {
231
- if (!tableExists(db, "session_handoff")) return [];
232
- const params = [sinceIso, project, like(project), like(project)];
233
- const where = [
234
- "created_at >= ?",
235
- "(lower(project)=lower(?) OR summary LIKE ? OR meta_json LIKE ?)",
236
- ];
237
- if (agentName) {
238
- where.push("lower(agent_name)=lower(?)");
239
- params.push(agentName);
240
- }
241
- params.push(limit);
242
- return safeAll(db, `
243
- SELECT id, agent_name, project, summary, changed_files, tests, deploys, blockers, next_actions, created_at
244
- FROM session_handoff
245
- WHERE ${where.join(" AND ")}
246
- ORDER BY created_at DESC
247
- LIMIT ?`, params);
248
- }
249
-
250
- function selectRecentDecisions(db, project, sinceIso, limit) {
251
- if (!tableExists(db, "decision_log")) return [];
252
- return safeAll(db, `
253
- SELECT id, scope, title, substr(body,1,320) AS body, decided_by, decided_at, status, files_affected, entities_affected
254
- FROM decision_log
255
- WHERE decided_at >= ?
256
- AND COALESCE(status,'active')='active'
257
- AND (lower(scope)=lower(?) OR title LIKE ? OR body LIKE ? OR files_affected LIKE ? OR entities_affected LIKE ?)
258
- ORDER BY decided_at DESC
259
- LIMIT ?`, [sinceIso, project, like(project), like(project), like(project), like(project), limit]);
260
- }
261
-
262
- function selectRecentActions(db, project, agentName, sinceIso, limit) {
263
- if (!tableExists(db, "agent_action")) return [];
264
- const params = [sinceIso, project, like(project), like(project), like(project), like(project), like(project)];
265
- const where = [
266
- "started_at >= ?",
267
- "(lower(target)=lower(?) OR target LIKE ? OR topic LIKE ? OR payload_json LIKE ? OR result_json LIKE ? OR meta_json LIKE ?)",
268
- ];
269
- if (agentName) {
270
- where.push("lower(agent_name)=lower(?)");
271
- params.push(agentName);
272
- }
273
- params.push(limit);
274
- return safeAll(db, `
275
- SELECT id, agent_name, action_kind, target, status, topic, started_at, finished_at
276
- FROM agent_action
277
- WHERE ${where.join(" AND ")}
278
- ORDER BY started_at DESC
279
- LIMIT ?`, params);
280
- }
281
-
282
- function selectMemorySnippets(db, project, sinceIso, limit) {
283
- if (!tableExists(db, "memory")) return [];
284
- return safeAll(db, `
285
- SELECT id, kind, actor, topic, importance, occurred_at, substr(text,1,300) AS preview
286
- FROM memory
287
- WHERE occurred_at >= ?
288
- AND (topic LIKE ? OR text LIKE ?)
289
- ORDER BY importance DESC, occurred_at DESC
290
- LIMIT ?`, [sinceIso, like(project), like(project), limit]);
291
- }
292
-
293
- function buildTimeline(data) {
294
- const events = [];
295
- const max = Math.max(6, data.maxItems * 3);
296
- addEvent(events, "registry", data.registry && data.registry.updated_at, "project registry updated", data.registry && data.registry.live_status, data.registry && data.registry.updated_by);
297
- addEvent(events, "rules", data.rules && data.rules.updated_at, "project rules updated", data.rules && data.rules.notes, data.rules && data.rules.updated_by);
298
- addEvent(events, "site_contract", data.siteContract && data.siteContract.updated_at, "site contract updated", data.siteContract && data.siteContract.notes, data.siteContract && data.siteContract.updated_by);
299
- addEvent(events, "golden_check", data.latestGolden && data.latestGolden.created_at, "latest golden check", data.latestGolden && `${data.latestGolden.status}: ${data.latestGolden.summary || ""}`, data.latestGolden && data.latestGolden.agent_name);
300
-
301
- for (const row of data.openFindings || []) addEvent(events, "finding", row.updated_at || row.created_at, row.title, `${row.severity || ""} ${row.category || ""} ${row.status || ""}`, row.source_agent, row.id);
302
- for (const row of data.activeClaims || []) addEvent(events, "claim", row.claimed_at, row.file_path || row.summary, row.summary, row.agent_name, row.id);
303
- for (const row of data.autonomyTasks || []) addEvent(events, "autonomy_task", row.updated_at || row.created_at, row.title, `${row.department_name || ""} ${row.status || ""}`, row.assigned_agent, row.id);
304
- for (const row of data.pendingBriefs || []) addEvent(events, "brief", row.created_at, row.preview, row.status, row.agent_name, row.id);
305
- for (const row of data.handoffs || []) addEvent(events, "handoff", row.created_at, row.summary, row.next_actions || row.blockers, row.agent_name, row.id);
306
- for (const row of data.decisions || []) addEvent(events, "decision", row.decided_at, row.title, row.body, row.decided_by, row.id);
307
- for (const row of data.actions || []) addEvent(events, "action", row.started_at, row.action_kind || row.topic || row.target, row.status, row.agent_name, row.id);
308
- for (const row of data.memories || []) addEvent(events, "memory", row.occurred_at, row.preview, row.topic, row.actor, row.id);
309
-
310
- return events
311
- .filter((event) => event.at)
312
- .sort((a, b) => Date.parse(b.at) - Date.parse(a.at))
313
- .slice(0, max)
314
- .map((event) => redactRow(event));
315
- }
316
-
317
- function addEvent(events, kind, at, title, detail, actor, refId) {
318
- if (!at || !title) return;
319
- events.push({
320
- at,
321
- kind,
322
- title: clean(title, 180),
323
- detail: clean(detail, 220),
324
- actor: actor || null,
325
- ref_id: refId || null,
326
- });
327
- }
328
-
329
- function deriveLiveReadiness(data) {
330
- const checklist = parseMaybeJson(data.registry && data.registry.health_checklist, {});
331
- const missingBlocks = parseList(data.registry && data.registry.missing_blocks);
332
- const requiredGates = collectRequiredGates(data.rules, data.siteContract, data.liveFocus);
333
- const gateRows = requiredGates.map((gate) => {
334
- const state = gateStatus(gate, checklist);
335
- return {
336
- gate,
337
- status: state.status,
338
- source: state.source,
339
- value: state.value === undefined ? null : state.value,
340
- };
341
- });
342
- const requirements = standardRequirements(data.registry, data.rules, data.siteContract, checklist);
343
- const blockers = [];
344
- const warnings = [];
345
-
346
- if (!data.registry) {
347
- blockers.push(makeIssue("missing_project_registry", "critical", "No project_registry row exists.", "Create or seed project_registry before assigning live work."));
348
- }
349
- if (!data.rules) {
350
- blockers.push(makeIssue("missing_project_rules", "high", "No project_rules row exists.", "Create project rules for nav, auth, language, pricing, checkout, VAT, design, deploy and required gates."));
351
- }
352
- if (data.liveFocus && !data.siteContract) {
353
- warnings.push(makeIssue("missing_site_contract", "medium", "No site_contract row exists.", "Store canonical URLs, paths, forbidden hosts, locales, header/menu/footer/logo/auth/pricing/checkout checks."));
354
- }
355
- for (const item of missingBlocks) {
356
- blockers.push(makeIssue("registry_missing_block", "high", clean(item, 160), "Clear missing_blocks in project_registry only after evidence exists."));
357
- }
358
- for (const gate of gateRows) {
359
- if (gate.status === "block") blockers.push(makeIssue(`gate_blocked:${gate.gate}`, "high", `Required gate '${gate.gate}' is blocked.`, "Fix the gate and update project_registry.health_checklist."));
360
- if (gate.status === "unknown") warnings.push(makeIssue(`gate_unknown:${gate.gate}`, "medium", `Required gate '${gate.gate}' is unknown.`, "Run the check and record pass/fail in project_registry.health_checklist."));
361
- }
362
- for (const requirement of requirements) {
363
- if (requirement.status === "block") blockers.push(makeIssue(`requirement_blocked:${requirement.key}`, "high", `${requirement.label} is blocked.`, requirement.next_action));
364
- if (requirement.status === "unknown") warnings.push(makeIssue(`requirement_unknown:${requirement.key}`, "medium", `${requirement.label} is not documented.`, requirement.next_action));
365
- }
366
- for (const finding of data.openFindings || []) {
367
- if (isHighSeverity(finding.severity)) {
368
- blockers.push(makeIssue(`finding:${finding.id}`, severityName(finding.severity), finding.title, "Fix the finding, capture verification, then mark it resolved."));
369
- }
370
- }
371
- if (data.latestGolden) {
372
- const status = String(data.latestGolden.status || "").toLowerCase();
373
- if (["fail", "failed", "block", "blocked", "red"].includes(status)) {
374
- blockers.push(makeIssue("golden_check_failed", "high", data.latestGolden.summary || "Latest golden check failed.", "Fix failures and persist a passing mem_site_golden_check_report."));
375
- } else if (!["pass", "passed", "ok", "green", "approved"].includes(status)) {
376
- warnings.push(makeIssue("golden_check_unclear", "medium", `Latest golden check status is '${data.latestGolden.status}'.`, "Run a fresh golden check with clear pass/fail evidence."));
377
- }
378
- } else if (data.liveFocus) {
379
- warnings.push(makeIssue("golden_check_missing", "medium", "No golden check history found.", "Run mem_site_golden_check_plan, execute checks, then store mem_site_golden_check_report."));
380
- }
381
- if ((data.activeClaims || []).length) {
382
- warnings.push(makeIssue("active_claims", "medium", `${data.activeClaims.length} active work claim(s) exist.`, "Wait for release or coordinate before marking the project ready."));
383
- }
384
- if ((data.autonomyTasks || []).some((task) => ["blocked", "review"].includes(String(task.status || "").toLowerCase()))) {
385
- warnings.push(makeIssue("autonomy_review_or_blocked", "medium", "Autonomy tasks are still blocked or waiting for review.", "Route blocked/review tasks to their reviewer before live sign-off."));
386
- }
387
-
388
- const passedGates = gateRows.filter((gate) => gate.status === "pass").map((gate) => gate.gate);
389
- const blockedGates = gateRows.filter((gate) => gate.status === "block").map((gate) => gate.gate);
390
- const unknownGates = gateRows.filter((gate) => gate.status === "unknown").map((gate) => gate.gate);
391
- const status = blockers.length ? "block" : (warnings.length || unknownGates.length ? "attention" : "ready");
392
- const readinessPercent = Math.max(0, Math.min(100, 100 - blockers.length * 18 - warnings.length * 6 - unknownGates.length * 3));
393
-
394
- return {
395
- status,
396
- readiness_percent: readinessPercent,
397
- required_gates: requiredGates,
398
- gate_status: gateRows,
399
- requirements,
400
- passed_gates: passedGates,
401
- blocked_gates: blockedGates,
402
- unknown_gates: unknownGates,
403
- blockers: uniqueIssues(blockers),
404
- warnings: uniqueIssues(warnings),
405
- };
406
- }
407
-
408
- function collectRequiredGates(rules, siteContract, liveFocus) {
409
- const fromRules = parseList(rules && rules.required_gates);
410
- if (fromRules.length) return unique(fromRules.map((item) => clean(item, 80)).filter(Boolean));
411
- const fromSite = parseList(siteContract && siteContract.required_checks);
412
- if (fromSite.length) return unique(fromSite.map((item) => clean(item, 80)).filter(Boolean));
413
- if (!liveFocus) return ["planning", "implementation", "review"];
414
- return [
415
- "nav",
416
- "links",
417
- "header_footer",
418
- "logo",
419
- "mobile",
420
- "i18n",
421
- "auth",
422
- "pricing",
423
- "checkout",
424
- "vat",
425
- "legal",
426
- "deploy",
427
- "monitoring",
428
- ];
429
- }
430
-
431
- function standardRequirements(registry, rules, siteContract, checklist) {
432
- const defs = [
433
- { key: "nav", label: "Navigation/menu contract", fields: [rules && rules.canonical_nav, siteContract && siteContract.menu_rules], gates: ["nav", "menu", "navigation"], next: "Document canonical menu links and verify every locale/domain." },
434
- { key: "header_footer", label: "Header/footer consistency", fields: [siteContract && siteContract.header_rules, siteContract && siteContract.footer_rules], gates: ["header_footer", "header", "footer"], next: "Store header/footer rules and prove pages match the canonical shell." },
435
- { key: "logo", label: "Logo and dark-mode assets", fields: [siteContract && siteContract.logo_rules], gates: ["logo", "dark_logo", "darkmode_logo"], next: "Document logo rules including light/dark variants and verify them visually." },
436
- { key: "links", label: "Internal/legal links", fields: [siteContract && siteContract.paths, siteContract && siteContract.forbidden_hosts], gates: ["links", "legal_links", "forbidden_hosts"], next: "Run link checks for all stored paths/locales and remove forbidden cross-domain links." },
437
- { key: "mobile", label: "Mobile responsiveness", fields: [siteContract && siteContract.mobile_viewports], gates: ["mobile", "responsive"], next: "Run mobile viewport checks and store evidence." },
438
- { key: "i18n", label: "Language parity", fields: [registry && registry.langs, rules && rules.language_matrix, siteContract && siteContract.required_locales], gates: ["i18n", "language", "languages", "locales"], next: "Store required locales and verify every menu/legal/pricing path in each language." },
439
- { key: "auth", label: "Auth crossover", fields: [registry && registry.auth_system, rules && rules.auth_matrix, siteContract && siteContract.auth_rules], gates: ["auth", "login", "sso"], next: "Document shared auth rules and test one login across all related surfaces." },
440
- { key: "pricing", label: "Pricing source of truth", fields: [rules && rules.pricing_rules, siteContract && siteContract.pricing_rules], gates: ["pricing", "prices"], next: "Store pricing rules and verify every page displaying prices uses the same source." },
441
- { key: "checkout", label: "Checkout and billing", fields: [registry && registry.stripe_product_ids, rules && rules.checkout_rules, siteContract && siteContract.checkout_rules], gates: ["checkout", "billing", "stripe"], next: "Document checkout flow, product IDs, webhooks, and customer self-service checks." },
442
- { key: "vat", label: "VAT/OSS handling", fields: [registry && registry.vat_status, rules && rules.vat_rules], gates: ["vat", "oss", "tax"], next: "Record VAT/OSS decision and where VAT checks are already implemented." },
443
- { key: "deploy", label: "Deploy and rollback", fields: [registry && registry.server, registry && registry.pm2_processes, registry && registry.nginx_files, rules && rules.deploy_rules], gates: ["deploy", "server", "rollback"], next: "Record server/process/proxy coordinates and deploy/rollback rule." },
444
- { key: "legal", label: "Legal pages and public claims", fields: [siteContract && siteContract.paths, rules && rules.notes], gates: ["legal", "privacy", "terms", "impressum"], next: "Verify legal pages, locale routing, and public claims before launch." },
445
- ];
446
- return defs.map((def) => evaluateRequirement(def, checklist));
447
- }
448
-
449
- function evaluateRequirement(def, checklist) {
450
- const gate = firstGateStatus(def.gates, checklist);
451
- if (gate.status === "block") {
452
- return { key: def.key, label: def.label, status: "block", source: gate.source, next_action: def.next };
453
- }
454
- if (gate.status === "pass") {
455
- return { key: def.key, label: def.label, status: "pass", source: gate.source, next_action: null };
456
- }
457
- const hasField = (def.fields || []).some((value) => hasMeaningfulValue(value));
458
- return {
459
- key: def.key,
460
- label: def.label,
461
- status: hasField ? "documented" : "unknown",
462
- source: hasField ? "registry/rules/site_contract" : "missing",
463
- next_action: hasField ? null : def.next,
464
- };
465
- }
466
-
467
- function firstGateStatus(keys, checklist) {
468
- for (const key of keys || []) {
469
- const status = gateStatus(key, checklist);
470
- if (status.status !== "unknown") return status;
471
- }
472
- return { status: "unknown", source: "health_checklist", value: null };
473
- }
474
-
475
- function gateStatus(gate, checklist) {
476
- const normalized = normalizeKey(gate);
477
- if (!checklist || typeof checklist !== "object" || Array.isArray(checklist)) {
478
- return { status: "unknown", source: "health_checklist", value: null };
479
- }
480
- for (const [key, value] of Object.entries(checklist)) {
481
- if (normalizeKey(key) !== normalized) continue;
482
- return { status: normalizeStatus(value), source: `health_checklist.${key}`, value: redactValue(value) };
483
- }
484
- return { status: "unknown", source: "health_checklist", value: null };
485
- }
486
-
487
- function normalizeStatus(value) {
488
- if (value === true) return "pass";
489
- if (value === false) return "block";
490
- if (value && typeof value === "object" && !Array.isArray(value)) {
491
- if ("status" in value) return normalizeStatus(value.status);
492
- if ("passed" in value) return normalizeStatus(value.passed);
493
- if ("ok" in value) return normalizeStatus(value.ok);
494
- if ("done" in value) return normalizeStatus(value.done);
495
- }
496
- const text = String(value == null ? "" : value).toLowerCase().trim();
497
- if (["pass", "passed", "ok", "done", "green", "approved", "true", "yes", "ready"].includes(text)) return "pass";
498
- if (["block", "blocked", "fail", "failed", "red", "missing", "false", "no", "broken"].includes(text)) return "block";
499
- return "unknown";
500
- }
501
-
502
- function deriveNextActions(readiness, data) {
503
- const actions = [];
504
- for (const issue of readiness.blockers.slice(0, 8)) {
505
- actions.push({ priority: "P0", source: issue.id, action: issue.next_action || issue.title });
506
- }
507
- for (const issue of readiness.warnings.slice(0, 8)) {
508
- actions.push({ priority: "P1", source: issue.id, action: issue.next_action || issue.title });
509
- }
510
- for (const finding of (data.openFindings || []).filter((row) => !isHighSeverity(row.severity)).slice(0, 5)) {
511
- actions.push({ priority: "P2", source: `finding:${finding.id}`, action: `Resolve ${finding.category || "finding"}: ${clean(finding.title, 140)}` });
512
- }
513
- for (const task of (data.autonomyTasks || []).slice(0, 5)) {
514
- actions.push({ priority: task.status === "blocked" ? "P1" : "P2", source: `autonomy_task:${task.id}`, action: `${task.department_name || "department"}: ${clean(task.title, 140)}` });
515
- }
516
- if (!actions.length && readiness.status === "ready") {
517
- actions.push({ priority: "P1", source: "review", action: "Run final reviewer pass with site golden checks, deploy evidence, and handoff capture." });
518
- }
519
- return uniqueActions(actions).slice(0, 12);
520
- }
521
-
522
- function renderReport(report, tokenBudget) {
523
- const lines = [];
524
- lines.push(`# Project timeline report: ${report.project}`);
525
- lines.push("");
526
- lines.push(`Generated: ${report.generated_at}`);
527
- lines.push(`Window: ${report.days} days`);
528
- lines.push(`Status: ${report.summary.status} (${report.summary.readiness_percent}%)`);
529
- lines.push(`Open: ${report.summary.open_findings} finding(s), ${report.summary.active_claims} active claim(s), ${report.summary.autonomy_open_or_review} autonomy task(s), ${report.summary.pending_or_dispatched_briefs} brief(s)`);
530
- lines.push("");
531
-
532
- lines.push("## Live readiness");
533
- addIssueLines(lines, "Blockers", report.live_readiness.blockers);
534
- addIssueLines(lines, "Warnings", report.live_readiness.warnings);
535
- lines.push(`Passed gates: ${report.live_readiness.passed_gates.join(", ") || "none"}`);
536
- lines.push(`Unknown gates: ${report.live_readiness.unknown_gates.join(", ") || "none"}`);
537
- lines.push("");
538
-
539
- lines.push("## Next actions");
540
- if (report.next_actions.length) {
541
- for (const action of report.next_actions) lines.push(`- ${action.priority} ${action.source}: ${action.action}`);
542
- } else {
543
- lines.push("- No next action detected.");
544
- }
545
- lines.push("");
546
-
547
- lines.push("## Project coordinates");
548
- const registry = report.sections.registry;
549
- if (registry) {
550
- pushKV(lines, "Domain", registry.domain);
551
- pushKV(lines, "Live URL", registry.live_url);
552
- pushKV(lines, "Staging URL", registry.staging_url);
553
- pushKV(lines, "Repo", registry.repo);
554
- pushKV(lines, "Server", registry.server);
555
- pushKV(lines, "Auth", registry.auth_system);
556
- pushKV(lines, "VAT", registry.vat_status);
557
- pushKV(lines, "Languages", registry.langs);
558
- pushKV(lines, "Live status", registry.live_status);
559
- } else {
560
- lines.push("- No registry row.");
561
- }
562
- lines.push("");
563
-
564
- lines.push("## Requirement map");
565
- for (const requirement of report.live_readiness.requirements) {
566
- lines.push(`- ${requirement.key}: ${requirement.status}${requirement.source ? ` (${requirement.source})` : ""}`);
567
- }
568
- lines.push("");
569
-
570
- renderTable(lines, "Open findings", report.sections.open_findings, (row) => `${row.severity || ""} ${row.category || ""} #${row.id}: ${row.title}${row.url ? ` (${row.url})` : ""}`);
571
- renderTable(lines, "Active claims", report.sections.active_claims, (row) => `${row.agent_name || "agent"} ${row.file_path || ""}: ${row.summary || ""}`);
572
- renderTable(lines, "Autonomy tasks", report.sections.autonomy_tasks, (row) => `${row.severity || ""} ${row.status || ""} ${row.department_name || ""} #${row.id}: ${row.title}`);
573
- renderTable(lines, "Pending/dispatched briefs", report.sections.pending_briefs, (row) => `${row.status || ""} ${row.agent_name || ""} #${row.id}: ${row.preview || ""}`);
574
- renderTable(lines, "Recent handoffs", report.sections.recent_handoffs, (row) => `${row.created_at || ""} ${row.agent_name || ""}: ${row.summary || ""}`);
575
- renderTable(lines, "Recent decisions", report.sections.recent_decisions, (row) => `${row.decided_at || ""} #${row.id}: ${row.title}`);
576
- renderTable(lines, "Timeline", report.sections.timeline, (row) => `${row.at || ""} ${row.kind || ""}: ${row.title || ""}${row.actor ? ` (${row.actor})` : ""}`);
577
-
578
- let doc = lines.join("\n");
579
- const maxChars = Math.max(1200, tokenBudget * 4);
580
- let truncated = false;
581
- if (doc.length > maxChars) {
582
- doc = doc.slice(0, maxChars - 120).trimEnd() + "\n\n[truncated: raise token_budget or lower max_items for a smaller report]";
583
- truncated = true;
584
- }
585
- return { doc, estimated_tokens: estimateTokens(doc), truncated };
586
- }
587
-
588
- function addIssueLines(lines, title, issues) {
589
- lines.push(`${title}:`);
590
- if (!issues || !issues.length) {
591
- lines.push("- none");
592
- return;
593
- }
594
- for (const issue of issues) {
595
- lines.push(`- ${issue.severity || "medium"} ${issue.id}: ${issue.title}${issue.next_action ? ` -> ${issue.next_action}` : ""}`);
596
- }
597
- }
598
-
599
- function renderTable(lines, title, rows, formatter) {
600
- lines.push(`## ${title}`);
601
- if (!rows || !rows.length) {
602
- lines.push("- none");
603
- lines.push("");
604
- return;
605
- }
606
- for (const row of rows) lines.push(`- ${clean(formatter(row), 320)}`);
607
- lines.push("");
608
- }
609
-
610
- function pushKV(lines, key, value) {
611
- if (hasMeaningfulValue(value)) lines.push(`- ${key}: ${clean(renderValue(value), 260)}`);
612
- }
613
-
614
- function makeIssue(id, severity, title, nextAction) {
615
- return {
616
- id,
617
- severity: severity || "medium",
618
- title: clean(title, 220),
619
- next_action: clean(nextAction, 240),
620
- };
621
- }
622
-
623
- function uniqueIssues(issues) {
624
- const seen = new Set();
625
- const out = [];
626
- for (const issue of issues || []) {
627
- const key = issue.id || issue.title;
628
- if (!key || seen.has(key)) continue;
629
- seen.add(key);
630
- out.push(issue);
631
- }
632
- return out;
633
- }
634
-
635
- function uniqueActions(actions) {
636
- const seen = new Set();
637
- const out = [];
638
- for (const action of actions || []) {
639
- const key = `${action.priority}:${action.source}:${action.action}`;
640
- if (seen.has(key)) continue;
641
- seen.add(key);
642
- out.push(action);
643
- }
644
- return out;
645
- }
646
-
647
- function redactRuleRow(row) {
648
- const out = redactRow(row);
649
- for (const key of ["canonical_nav", "allowed_domains", "auth_matrix", "language_matrix", "pricing_rules", "checkout_rules", "vat_rules", "deploy_rules", "design_rules", "required_gates"]) {
650
- if (out[key]) out[key] = summarizeJsonish(out[key], 420);
651
- }
652
- return out;
653
- }
654
-
655
- function redactSiteContract(row) {
656
- const out = redactRow(row);
657
- for (const key of ["target_urls", "paths", "forbidden_hosts", "required_locales", "mobile_viewports", "desktop_viewports", "required_checks"]) {
658
- if (out[key]) out[key] = summarizeJsonish(out[key], 300);
659
- }
660
- for (const key of ["header_rules", "menu_rules", "footer_rules", "logo_rules", "auth_rules", "pricing_rules", "checkout_rules"]) {
661
- if (out[key]) out[key] = summarizeJsonish(out[key], 420);
662
- }
663
- return out;
664
- }
665
-
666
- function summarizeJsonish(value, max) {
667
- const parsed = parseMaybeJson(value, null);
668
- if (parsed == null || parsed === value) return redact(value, max);
669
- if (Array.isArray(parsed)) return parsed.slice(0, 12).map((item) => redactValue(item));
670
- if (parsed && typeof parsed === "object") {
671
- const out = {};
672
- for (const key of Object.keys(parsed).slice(0, 12)) out[key] = redactValue(parsed[key]);
673
- return out;
674
- }
675
- return parsed;
676
- }
677
-
678
- function redactRow(row) {
679
- const out = {};
680
- for (const [key, value] of Object.entries(row || {})) out[key] = redactValue(value);
681
- return out;
682
- }
683
-
684
- function redactValue(value) {
685
- if (typeof value === "string") return redact(value, 420);
686
- if (Array.isArray(value)) return value.slice(0, 20).map(redactValue);
687
- if (value && typeof value === "object") {
688
- const out = {};
689
- for (const [key, item] of Object.entries(value).slice(0, 20)) out[key] = redactValue(item);
690
- return out;
691
- }
692
- return value;
693
- }
694
-
695
- function parseList(value) {
696
- const parsed = parseMaybeJson(value, value);
697
- if (!parsed) return [];
698
- if (Array.isArray(parsed)) return parsed.map((item) => renderValue(item)).filter(Boolean);
699
- if (parsed && typeof parsed === "object") return Object.keys(parsed).filter(Boolean);
700
- return String(parsed)
701
- .split(/[\n,;|]/)
702
- .map((item) => clean(item, 120))
703
- .filter(Boolean)
704
- .filter((item) => item !== "[]" && item !== "{}");
705
- }
706
-
707
- function renderValue(value) {
708
- if (value == null) return "";
709
- if (typeof value === "string") return value;
710
- if (typeof value === "number" || typeof value === "boolean") return String(value);
711
- try { return JSON.stringify(value); } catch { return String(value); }
712
- }
713
-
714
- function hasMeaningfulValue(value) {
715
- if (value == null) return false;
716
- if (typeof value === "boolean") return value;
717
- if (typeof value === "number") return true;
718
- const text = String(value).trim();
719
- if (!text || text === "[]" || text === "{}" || text.toLowerCase() === "null") return false;
720
- return true;
721
- }
722
-
723
- function normalizeKey(value) {
724
- return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
725
- }
726
-
727
- function severityName(value) {
728
- const text = String(value || "").toLowerCase();
729
- if (["critical", "crit", "c", "p0"].includes(text)) return "critical";
730
- if (["high", "h", "p1"].includes(text)) return "high";
731
- if (["medium", "m", "p2"].includes(text)) return "medium";
732
- return text || "low";
733
- }
734
-
735
- function isHighSeverity(value) {
736
- return ["critical", "crit", "c", "p0", "high", "h", "p1"].includes(String(value || "").toLowerCase());
737
- }
738
-
739
- function severityRank(value) {
740
- const text = String(value || "").toLowerCase();
741
- if (["critical", "crit", "c", "p0"].includes(text)) return 0;
742
- if (["high", "h", "p1"].includes(text)) return 1;
743
- if (["medium", "m", "p2"].includes(text)) return 2;
744
- return 3;
745
- }
746
-
747
- function safeGet(db, sql, params) {
748
- try {
749
- return db.prepare(sql).get(...(params || [])) || null;
750
- } catch {
751
- return null;
752
- }
753
- }
754
-
755
- function safeAll(db, sql, params) {
756
- try {
757
- return db.prepare(sql).all(...(params || []));
758
- } catch {
759
- return [];
760
- }
761
- }
762
-
763
- function tableExists(db, name) {
764
- try { return !!db.prepare("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name=?").get(name); } catch { return false; }
765
- }
766
-
767
- function clean(value, max) {
768
- const text = String(value || "").replace(/\s+/g, " ").trim();
769
- if (!text) return "";
770
- if (!max || text.length <= max) return text;
771
- return text.slice(0, max - 1).trimEnd() + "...";
772
- }
773
-
774
- function normalizeAgentId(value) {
775
- return String(value || "").trim().toLowerCase();
776
- }
777
-
778
- function clampInt(value, fallback, min, max) {
779
- const parsed = Number.parseInt(value, 10);
780
- if (!Number.isFinite(parsed)) return fallback;
781
- return Math.max(min, Math.min(max, parsed));
782
- }
783
-
784
- function like(term) {
785
- return `%${String(term || "").replace(/[%_]/g, "")}%`;
786
- }
787
-
788
- function unique(items) {
789
- return Array.from(new Set((items || []).filter(Boolean)));
790
- }
791
-
792
- function estimateTokens(text) {
793
- return Math.ceil(String(text || "").length / 4);
794
- }
795
-
796
- function redact(value, max) {
797
- let text = String(value || "");
798
- text = text.replace(/<private>[\s\S]*?<\/private>/gi, "[private]");
799
- text = text.replace(/\b(sk|pk|rk|ghp|gho|github_pat)_[A-Za-z0-9_=-]{12,}\b/g, "[secret]");
800
- text = text.replace(/\b(password|passwd|token|secret|api[_-]?key)\s*[:=]\s*[^,\s;}]+/gi, "$1=[secret]");
801
- text = text.replace(/\s+/g, " ").trim();
802
- if (!text || !max || text.length <= max) return text;
803
- return text.slice(0, max - 1).trimEnd() + "...";
804
- }
805
-
806
- module.exports = {
807
- TIMELINE_REPORT_TOOL_DEFS,
808
- handleTimelineReportTool,
809
- projectTimelineReport,
810
- };