@axiomatic-labs/claudeflow 2.13.35 → 2.13.37

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 (2) hide show
  1. package/lib/panel.js +103 -3
  2. package/package.json +1 -1
package/lib/panel.js CHANGED
@@ -286,6 +286,30 @@ function getEnforcementInfo(cwd) {
286
286
  };
287
287
  }
288
288
 
289
+ function getActivePlanInfo(cwd) {
290
+ const p = path.join(cwd, '.claudeflow', 'state', 'active-plan.json');
291
+ let raw;
292
+ try { raw = fs.readFileSync(p, 'utf8'); }
293
+ catch { return { available: false }; }
294
+ let record;
295
+ try { record = JSON.parse(raw); }
296
+ catch (err) { return { available: false, error: err.message }; }
297
+ const captured = record.captured_at ? Date.parse(record.captured_at) : null;
298
+ const ageSeconds = captured ? Math.floor((Date.now() - captured) / 1000) : null;
299
+ const content = typeof record.content === 'string' ? record.content : '';
300
+ return {
301
+ available: true,
302
+ path: record.path || null,
303
+ source: record.source || null,
304
+ sessionId: record.session_id || null,
305
+ capturedAt: record.captured_at || null,
306
+ ageSeconds,
307
+ sha256: record.sha256 || null,
308
+ contentBytes: record.contentBytes || content.length,
309
+ summaryPreview: content.slice(0, 240).replace(/\s+/g, ' ').trim(),
310
+ };
311
+ }
312
+
289
313
  function getRemindersInfo(cwd) {
290
314
  const helpers = loadReminderHelpers(cwd);
291
315
  if (!helpers || typeof helpers.listReminderCatalog !== 'function') {
@@ -512,6 +536,7 @@ function collectStatus(cwd) {
512
536
  activeRun: getActiveRunInfo(cwd),
513
537
  reminders: getRemindersInfo(cwd),
514
538
  enforcement: getEnforcementInfo(cwd),
539
+ activePlan: getActivePlanInfo(cwd),
515
540
  logs: getLogsInfo(cwd),
516
541
  doctor: getDoctorInfo(cwd),
517
542
  };
@@ -703,6 +728,7 @@ const SECTIONS = [
703
728
  { id: 'hooks', label: 'Hooks' },
704
729
  { id: 'reminders', label: 'Reminders' },
705
730
  { id: 'logs', label: 'Logs' },
731
+ { id: 'activePlan', label: 'Active plan' },
706
732
  { id: 'mcp', label: 'MCP & observer' },
707
733
  { id: 'setupContext', label: 'Setup context' },
708
734
  { id: 'activeRun', label: 'Active run' },
@@ -760,6 +786,7 @@ async function loadClaudeMdPreview() {
760
786
 
761
787
  async function refresh() {
762
788
  const open = captureOpenDetails();
789
+ const prevPlanSha = state && state.activePlan && state.activePlan.sha256;
763
790
  try {
764
791
  const r = await fetch('/api/status');
765
792
  state = await r.json();
@@ -767,12 +794,16 @@ async function refresh() {
767
794
  showToast && showToast('Refresh failed: ' + e.message, 'err');
768
795
  return;
769
796
  }
797
+ // Invalidate cached active-plan body if the underlying sha256 changed.
798
+ const newPlanSha = state && state.activePlan && state.activePlan.sha256;
799
+ if (newPlanSha !== prevPlanSha) activePlanCache = null;
770
800
  renderHeader();
771
801
  renderNav();
772
802
  renderContent();
773
803
  restoreOpenDetails(open);
774
804
  // If preview was visible/open, ensure it stays loaded after re-render.
775
805
  if (open.has('claude-md-preview')) loadClaudeMdPreview();
806
+ if (open.has('active-plan-full')) loadActivePlanPreview();
776
807
  // Refresh logs entries silently on the Logs tab so new events appear.
777
808
  if (active === 'logs') loadLogsPage();
778
809
  }
@@ -804,6 +835,7 @@ function severityFor(id) {
804
835
  case 'claudeMd': return !s.claudeMd.exists ? 'err' : (s.claudeMd.optOut ? 'info' : 'ok');
805
836
  case 'hooks': return s.hooks.warnings > 0 ? 'warn' : 'ok';
806
837
  case 'reminders': return s.reminders && s.reminders.disabledCount > 0 ? 'info' : 'ok';
838
+ case 'activePlan': return s.activePlan && s.activePlan.available ? 'info' : 'info';
807
839
  case 'logs': {
808
840
  if (!s.logs) return 'info';
809
841
  if (s.logs.recentBlockedCount > 0) return 'err';
@@ -856,6 +888,7 @@ function renderContent() {
856
888
  hooks: renderHooks,
857
889
  reminders: renderReminders,
858
890
  logs: renderLogs,
891
+ activePlan: renderActivePlan,
859
892
  mcp: renderMcp,
860
893
  setupContext: renderSetup,
861
894
  activeRun: renderRun,
@@ -883,6 +916,9 @@ function renderOverview() {
883
916
  const ef = s.enforcement && s.enforcement.available
884
917
  ? (s.enforcement.on ? \`ON (\${s.enforcement.count} blocking hooks)\` : \`OFF — \${s.enforcement.count} blocking hooks silenced\`)
885
918
  : 'unavailable';
919
+ const activePlanRow = s.activePlan && s.activePlan.available
920
+ ? \`\${s.activePlan.path || '(inline)'} · approved \${s.activePlan.ageSeconds == null ? 'unknown' : (s.activePlan.ageSeconds < 60 ? s.activePlan.ageSeconds + 's' : Math.floor(s.activePlan.ageSeconds / 60) + 'm')} ago · \${fmtBytes(s.activePlan.contentBytes || 0)}\`
921
+ : 'none';
886
922
  const reminderTokens = s.logs && s.logs.latestReminderTokens;
887
923
  const reminderStatus = s.logs && s.logs.latestReminderStatus;
888
924
  const lg = s.logs && s.logs.available
@@ -905,6 +941,7 @@ function renderOverview() {
905
941
  \${row('Active run', ar, { kind: 'info', text: s.activeRun.active ? '·' : '·' })}
906
942
  \${row('Reminders', rm, { kind: s.reminders && s.reminders.available ? (s.reminders.disabledCount ? 'info' : 'ok') : 'err', text: s.reminders && s.reminders.disabledCount ? '·' : '✓' })}
907
943
  \${row('Enforcement', ef, { kind: s.enforcement && s.enforcement.available ? (s.enforcement.on ? 'ok' : 'err') : 'err', text: s.enforcement && s.enforcement.available ? (s.enforcement.on ? '✓' : '✗') : '·' })}
944
+ \${row('Active plan', activePlanRow, { kind: s.activePlan && s.activePlan.available ? 'info' : 'info', text: '·' })}
908
945
  \${row('Logs', lg, { kind: logsKind, text: logsKind === 'err' ? '✗' : logsKind === 'warn' ? '!' : '·' })}
909
946
  \${row('Doctor', dc, { kind: s.doctor.issueCount === 0 ? 'ok' : 'warn', text: s.doctor.issueCount === 0 ? '✓' : '!' })}
910
947
  </div>\`;
@@ -1184,6 +1221,38 @@ function renderLogs() {
1184
1221
  \${logsState.allLoaded || logsState.entries.length === 0 ? '' : '<div style="margin-top:12px"><button id="logs-load-more">Load older</button></div>'}\`;
1185
1222
  }
1186
1223
 
1224
+ function renderActivePlan() {
1225
+ const p = state.activePlan;
1226
+ if (!p || !p.available) {
1227
+ return \`<h2>Active plan</h2>
1228
+ <p class="sub">No approved plan on disk. <code>.claudeflow/state/active-plan.json</code> doesn't exist.</p>
1229
+ <p class="muted" style="font-size:12px">When you approve a plan via ExitPlanMode in Claude Code, the
1230
+ <code>capture-approved-plan</code> hook will persist it here. The next time you run
1231
+ <code>claudeflow-build</code>, phase-0 will prompt to use it as the source of truth.</p>\`;
1232
+ }
1233
+ const age = p.ageSeconds == null
1234
+ ? 'unknown age'
1235
+ : p.ageSeconds < 60 ? p.ageSeconds + 's ago'
1236
+ : p.ageSeconds < 3600 ? Math.floor(p.ageSeconds / 60) + 'm ago'
1237
+ : p.ageSeconds < 86400 ? Math.floor(p.ageSeconds / 3600) + 'h ago'
1238
+ : Math.floor(p.ageSeconds / 86400) + 'd ago';
1239
+ const sha = p.sha256 ? p.sha256.slice(0, 12) + '…' : '(none)';
1240
+ const preview = escapeHtml(p.summaryPreview || '(empty)');
1241
+ return \`<h2>Active plan</h2>
1242
+ <p class="sub">Read-only view of the latest approved plan persisted by the <code>ExitPlanMode</code> hook. The terminal prompt in <code>claudeflow-build</code> phase-0 is the only place you can ACT on this — the panel is informational.</p>
1243
+ <div class="card">
1244
+ \${row('Path', p.path || '(inline plan, no file path)')}
1245
+ \${row('Source', p.source || 'unknown')}
1246
+ \${row('Approved', age + ' · ' + (p.capturedAt || 'unknown timestamp'))}
1247
+ \${row('Session id', p.sessionId || '(unknown)')}
1248
+ \${row('Sha256', sha)}
1249
+ \${row('Size', fmtBytes(p.contentBytes || 0))}
1250
+ </div>
1251
+ <p class="sub" style="margin-top:14px">Preview</p>
1252
+ <pre style="max-height:120px">\${preview}</pre>
1253
+ <details data-detail-id="active-plan-full"><summary>Full plan content</summary><div id="active-plan-body" class="muted">Click to load…</div></details>\`;
1254
+ }
1255
+
1187
1256
  function renderMcp() {
1188
1257
  const m = state.mcp;
1189
1258
  if (!m.configFound) return '<h2>MCP & observer</h2><p class="muted">.mcp.json not found</p>';
@@ -1348,13 +1417,35 @@ document.addEventListener('change', (e) => {
1348
1417
  }
1349
1418
  });
1350
1419
 
1420
+ let activePlanCache = null;
1421
+ async function loadActivePlanPreview() {
1422
+ const target = document.getElementById('active-plan-body');
1423
+ if (!target) return;
1424
+ if (target.dataset.loaded === '1') return;
1425
+ target.dataset.loaded = '1';
1426
+ target.textContent = 'Loading…';
1427
+ try {
1428
+ if (!activePlanCache) {
1429
+ const r = await fetch('/api/active-plan');
1430
+ activePlanCache = await r.text();
1431
+ }
1432
+ const pre = document.createElement('pre');
1433
+ pre.textContent = activePlanCache;
1434
+ target.innerHTML = '';
1435
+ target.appendChild(pre);
1436
+ } catch (e) {
1437
+ target.textContent = 'Failed to load: ' + e.message;
1438
+ target.dataset.loaded = '';
1439
+ }
1440
+ }
1441
+
1351
1442
  // Lazy-load CLAUDE.md preview when the user opens the details element.
1352
1443
  // The "toggle" event does not bubble by default — capture phase keeps delegation working.
1353
1444
  document.addEventListener('toggle', (e) => {
1354
1445
  const t = e.target;
1355
- if (t && t.tagName === 'DETAILS' && t.dataset.detailId === 'claude-md-preview' && t.open) {
1356
- loadClaudeMdPreview();
1357
- }
1446
+ if (!t || t.tagName !== 'DETAILS' || !t.open) return;
1447
+ if (t.dataset.detailId === 'claude-md-preview') loadClaudeMdPreview();
1448
+ else if (t.dataset.detailId === 'active-plan-full') loadActivePlanPreview();
1358
1449
  }, true);
1359
1450
 
1360
1451
  async function toggleEnforcement(disable) {
@@ -1437,6 +1528,15 @@ function handler(cwd) {
1437
1528
  if (route === '/' || route === '/index.html') return send(200, renderHtml(), 'text/html; charset=utf-8');
1438
1529
  if (route === '/api/status') return send(200, JSON.stringify(collectStatus(cwd)), 'application/json');
1439
1530
  if (route === '/api/claude-md') return send(200, readClaudeMdSafe(cwd), 'text/plain; charset=utf-8');
1531
+ if (route === '/api/active-plan') {
1532
+ const p = path.join(cwd, '.claudeflow', 'state', 'active-plan.json');
1533
+ try {
1534
+ const r = JSON.parse(fs.readFileSync(p, 'utf8'));
1535
+ return send(200, r.content || '', 'text/plain; charset=utf-8');
1536
+ } catch {
1537
+ return send(404, '', 'text/plain');
1538
+ }
1539
+ }
1440
1540
  if (route === '/api/logs') {
1441
1541
  const limit = Math.min(parseInt(url.searchParams.get('limit'), 10) || 100, 500);
1442
1542
  const before = url.searchParams.get('before') || null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.35",
3
+ "version": "2.13.37",
4
4
  "description": "Claudeflow — AI-powered development toolkit for Claude Code. Skills, agents, hooks, and quality gates that ship production apps.",
5
5
  "bin": {
6
6
  "claudeflow": "./bin/cli.js"