declare-cc 0.5.0 → 0.5.3

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/bin/install.js CHANGED
@@ -1567,6 +1567,9 @@ function install(isGlobal, runtime = 'claude') {
1567
1567
  const activityCommand = isGlobal
1568
1568
  ? buildHookCommand(targetDir, 'declare-activity.js')
1569
1569
  : 'node ' + dirName + '/hooks/declare-activity.js';
1570
+ const serverCommand = isGlobal
1571
+ ? buildHookCommand(targetDir, 'declare-server.js')
1572
+ : 'node ' + dirName + '/hooks/declare-server.js';
1570
1573
 
1571
1574
  // Enable experimental agents for Gemini CLI (required for custom sub-agents)
1572
1575
  if (isGemini) {
@@ -1594,16 +1597,22 @@ function install(isGlobal, runtime = 'claude') {
1594
1597
 
1595
1598
  if (!hasGsdUpdateHook) {
1596
1599
  settings.hooks.SessionStart.push({
1597
- hooks: [
1598
- {
1599
- type: 'command',
1600
- command: updateCheckCommand
1601
- }
1602
- ]
1600
+ hooks: [{ type: 'command', command: updateCheckCommand }]
1603
1601
  });
1604
1602
  console.log(` ${green}✓${reset} Configured update check hook`);
1605
1603
  }
1606
1604
 
1605
+ // Dashboard server hook — starts/restarts server for this project on SessionStart
1606
+ const hasServerHook = settings.hooks.SessionStart.some(entry =>
1607
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes('declare-server'))
1608
+ );
1609
+ if (!hasServerHook) {
1610
+ settings.hooks.SessionStart.push({
1611
+ hooks: [{ type: 'command', command: serverCommand }]
1612
+ });
1613
+ console.log(` ${green}✓${reset} Configured dashboard server hook`);
1614
+ }
1615
+
1607
1616
  // Configure PreToolUse + PostToolUse hooks for activity feed (Claude Code only)
1608
1617
  if (!isOpencode && !isGemini) {
1609
1618
  if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
@@ -6,71 +6,60 @@ allowed-tools:
6
6
 
7
7
  Open the Declare interactive DAG dashboard — a live web UI showing declarations, milestones, and actions as a navigable graph.
8
8
 
9
- **Step 1: Check if the server is already running.**
9
+ **Step 1: Resolve the port for this project.**
10
+
11
+ Each project gets its own stable port derived from the project path. Check if it's already been assigned:
10
12
 
11
13
  ```bash
12
- curl -sf http://localhost:3847/api/graph -o /dev/null && echo "RUNNING" || echo "NOT_RUNNING"
14
+ cat .planning/server.port 2>/dev/null || echo "NOT_SET"
13
15
  ```
14
16
 
15
- **Step 2: Start the server if it is not running.**
17
+ If a port file exists, use that port. If not, the SessionStart hook hasn't fired yet — use the default port 3847 and set PORT to it.
16
18
 
17
- If the output from Step 1 is `NOT_RUNNING`:
19
+ ```bash
20
+ PORT=$(cat .planning/server.port 2>/dev/null || echo "3847")
21
+ echo "PORT=$PORT"
22
+ ```
18
23
 
19
- Start the server in the background, capturing its PID:
24
+ **Step 2: Check if the server is already running on that port.**
20
25
 
21
26
  ```bash
22
- nohup node dist/declare-tools.cjs serve --port 3847 > /tmp/declare-dashboard.log 2>&1 &
23
- echo $!
27
+ curl -sf http://localhost:${PORT}/api/graph -o /dev/null && echo "RUNNING" || echo "NOT_RUNNING"
24
28
  ```
25
29
 
26
- Then wait briefly and confirm it started:
30
+ **Step 3: Start the server if it is not running.**
31
+
32
+ If `NOT_RUNNING`:
27
33
 
28
34
  ```bash
29
- sleep 1 && curl -sf http://localhost:3847/api/graph -o /dev/null && echo "STARTED" || echo "FAILED"
35
+ nohup node dist/declare-tools.cjs serve --port ${PORT} > /tmp/declare-dashboard.log 2>&1 &
36
+ sleep 1 && curl -sf http://localhost:${PORT}/api/graph -o /dev/null && echo "STARTED" || echo "FAILED"
30
37
  ```
31
38
 
32
- If the result is `FAILED`, report the error and show the last lines of the log:
33
-
39
+ If `FAILED`:
34
40
  ```bash
35
41
  tail -20 /tmp/declare-dashboard.log
36
42
  ```
37
43
 
38
- **Step 3: Open the dashboard in the browser.**
39
-
40
- Detect the OS and open the URL:
44
+ **Step 4: Open the dashboard in the browser.**
41
45
 
42
46
  ```bash
43
47
  if [[ "$OSTYPE" == "darwin"* ]]; then
44
- open http://localhost:3847
48
+ open http://localhost:${PORT}
45
49
  else
46
- xdg-open http://localhost:3847 2>/dev/null || echo "Visit http://localhost:3847 in your browser"
50
+ xdg-open http://localhost:${PORT} 2>/dev/null || echo "Visit http://localhost:${PORT} in your browser"
47
51
  fi
48
52
  ```
49
53
 
50
- **Step 4: Confirm to the user.**
51
-
52
- Show the user:
53
-
54
- ```
55
- Dashboard running at http://localhost:3847
56
-
57
- The graph auto-refreshes every 5 seconds.
58
- Click any node to inspect its details.
54
+ **Step 5: Confirm to the user.**
59
55
 
60
- Server log: /tmp/declare-dashboard.log
61
- To stop: kill <PID>
62
56
  ```
57
+ Dashboard running at http://localhost:[PORT]
63
58
 
64
- Where `<PID>` is the process ID captured in Step 2 (or a reminder to find it with `lsof -ti :3847`).
65
-
66
- If the server was already running (Step 1 returned `RUNNING`), say "Server was already running."
59
+ The graph updates live as agents run and files change.
60
+ Click any node to inspect details and exec-plan.
67
61
 
68
- **Step 5: Tail server output (optional).**
69
-
70
- If `$ARGUMENTS` contains `--tail` or `--log`:
71
-
72
- ```bash
73
- tail -f /tmp/declare-dashboard.log
62
+ To stop: kill $(lsof -ti :[PORT])
74
63
  ```
75
64
 
76
- Otherwise, skip this step.
65
+ If the server was already running (Step 2 returned `RUNNING`), say "Server was already running."
@@ -1329,7 +1329,7 @@ var require_help = __commonJS({
1329
1329
  usage: "/declare:help"
1330
1330
  }
1331
1331
  ],
1332
- version: "0.4.7"
1332
+ version: "0.5.2"
1333
1333
  };
1334
1334
  }
1335
1335
  module2.exports = { runHelp: runHelp2 };
@@ -2987,6 +2987,13 @@ var require_sync_status = __commonJS({
2987
2987
  actionResults.push({ id: action.id, milestone: m.id, changed: false, reason: "already DONE" });
2988
2988
  continue;
2989
2989
  }
2990
+ const summaryPath = join(folderPath, `${action.id}-SUMMARY.md`);
2991
+ if (existsSync(summaryPath)) {
2992
+ planContent = updateActionStatus(planContent, action.id, "DONE");
2993
+ planDirty = true;
2994
+ actionResults.push({ id: action.id, milestone: m.id, changed: true, reason: "SUMMARY.md exists" });
2995
+ continue;
2996
+ }
2990
2997
  if (milestoneAlreadyDone) {
2991
2998
  planContent = updateActionStatus(planContent, action.id, "DONE");
2992
2999
  planDirty = true;
@@ -203,24 +203,103 @@ function renderStatusBar() {
203
203
 
204
204
  // ─── Node element builder ─────────────────────────────────────────────────────
205
205
 
206
+ const COMPLETED = new Set(['DONE','KEPT','HONORED']);
207
+ const IN_PROGRESS_STORED = new Set(['ACTIVE']);
208
+
209
+ /**
210
+ * Compute derived workflow status for a milestone from its action statuses.
211
+ * This overrides the stored MILESTONES.md status so the dashboard always
212
+ * reflects reality even if sync-status hasn't been called.
213
+ *
214
+ * @param {{ id: string, status: string, hasPlan: boolean }} milestone
215
+ * @param {Array<{ id: string, status: string, causes: string[] }>} allActions
216
+ * @returns {{ displayStatus: string, doneCount: number, totalCount: number }}
217
+ */
218
+ function deriveMilestoneStatus(milestone, allActions) {
219
+ // Authoritative integrity/terminal states — always trust these
220
+ if (['KEPT','HONORED','BROKEN','RENEGOTIATED'].includes(milestone.status)) {
221
+ const myActions = allActions.filter(a => (a.causes||[]).includes(milestone.id));
222
+ return { displayStatus: milestone.status, doneCount: myActions.filter(a=>COMPLETED.has(a.status)).length, totalCount: myActions.length };
223
+ }
224
+
225
+ const myActions = allActions.filter(a => (a.causes||[]).includes(milestone.id));
226
+ const doneCount = myActions.filter(a => COMPLETED.has(a.status)).length;
227
+ const totalCount = myActions.length;
228
+
229
+ let displayStatus;
230
+ if (totalCount === 0) {
231
+ displayStatus = milestone.hasPlan ? 'PLANNED' : 'PENDING';
232
+ } else if (doneCount === totalCount) {
233
+ displayStatus = 'DONE';
234
+ } else if (doneCount > 0) {
235
+ displayStatus = 'EXECUTING';
236
+ } else {
237
+ displayStatus = 'PLANNED';
238
+ }
239
+
240
+ return { displayStatus, doneCount, totalCount };
241
+ }
242
+
243
+ /**
244
+ * Compute derived workflow status for a declaration from its milestone statuses.
245
+ * @param {{ id: string, status: string, milestones: string[] }} declaration
246
+ * @param {Array<{ id: string, displayStatus: string }>} enrichedMilestones
247
+ * @returns {string}
248
+ */
249
+ function deriveDeclarationStatus(declaration, enrichedMilestones) {
250
+ if (['KEPT','HONORED','BROKEN','RENEGOTIATED'].includes(declaration.status)) return declaration.status;
251
+
252
+ const myMilestones = enrichedMilestones.filter(m => (declaration.milestones||[]).includes(m.id));
253
+ if (myMilestones.length === 0) return 'PENDING';
254
+
255
+ const doneCount = myMilestones.filter(m => COMPLETED.has(m.displayStatus)).length;
256
+ const executingCount = myMilestones.filter(m => m.displayStatus === 'EXECUTING').length;
257
+ const plannedCount = myMilestones.filter(m => m.displayStatus === 'PLANNED').length;
258
+
259
+ if (doneCount === myMilestones.length) return 'DONE';
260
+ if (executingCount > 0 || doneCount > 0) return 'EXECUTING';
261
+ if (plannedCount > 0) return 'PLANNED';
262
+ return 'PENDING';
263
+ }
264
+
206
265
  /**
207
266
  * Build a node DOM element.
208
- * @param {{ id: string, title?: string, statement?: string, status?: string }} item
267
+ * @param {object} item
209
268
  * @param {'declaration'|'milestone'|'action'} type
269
+ * @param {{ displayStatus?: string, doneCount?: number, totalCount?: number }} [derived]
210
270
  * @returns {HTMLElement}
211
271
  */
212
- function buildNodeEl(item, type) {
272
+ function buildNodeEl(item, type, derived = {}) {
273
+ const displayStatus = derived.displayStatus || item.status || 'PENDING';
213
274
  const el = document.createElement('div');
214
- el.className = `node node-${type} status-${statusClass(item.status || 'pending')}`;
215
- el.dataset.nodeId = item.id;
275
+ el.className = `node node-${type} status-${statusClass(displayStatus)}`;
276
+ el.dataset.nodeId = item.id;
216
277
  el.dataset.nodeType = type;
217
278
 
218
279
  const title = item.title || item.statement || item.id;
219
280
 
281
+ // Progress bar for milestones with actions
282
+ let progressHtml = '';
283
+ if (type === 'milestone' && derived.totalCount > 0) {
284
+ const pct = Math.round((derived.doneCount / derived.totalCount) * 100);
285
+ const countLabel = `${derived.doneCount}/${derived.totalCount}`;
286
+ progressHtml = `
287
+ <div class="node-progress" title="${countLabel} actions done">
288
+ <div class="node-progress-fill" style="width:${pct}%"></div>
289
+ </div>`;
290
+ }
291
+
292
+ // Badge label — show progress count for executing milestones
293
+ let badgeLabel = displayStatus;
294
+ if (type === 'milestone' && displayStatus === 'EXECUTING' && derived.totalCount > 0) {
295
+ badgeLabel = `${derived.doneCount}/${derived.totalCount} DONE`;
296
+ }
297
+
220
298
  el.innerHTML = `
221
299
  <div class="node-id">${item.id}</div>
222
300
  <div class="node-title">${truncate(title, 55)}</div>
223
- <span class="status-badge">${item.status || 'PENDING'}</span>
301
+ <span class="status-badge">${badgeLabel}</span>
302
+ ${progressHtml}
224
303
  `;
225
304
 
226
305
  el.addEventListener('click', () => selectNode(item.id, type));
@@ -234,22 +313,37 @@ function renderGraph() {
234
313
 
235
314
  const { declarations, milestones, actions } = graphData;
236
315
 
316
+ // ── Compute derived statuses from action data (always reflects reality) ──────
317
+ // Milestones
318
+ const enrichedMilestones = (milestones || []).map(m => ({
319
+ ...m,
320
+ ...deriveMilestoneStatus(m, actions || []),
321
+ }));
322
+
323
+ // Declarations
324
+ const enrichedDeclarations = (declarations || []).map(d => ({
325
+ ...d,
326
+ displayStatus: deriveDeclarationStatus(d, enrichedMilestones),
327
+ }));
328
+
237
329
  // Clear containers
238
330
  $nodesDecls.innerHTML = '';
239
331
  $nodesMiles.innerHTML = '';
240
332
  $nodesActs.innerHTML = '';
241
333
 
242
- // Render declarations
243
- (declarations || []).forEach(d => {
244
- $nodesDecls.appendChild(buildNodeEl(d, 'declaration'));
334
+ // Render
335
+ enrichedDeclarations.forEach(d => {
336
+ $nodesDecls.appendChild(buildNodeEl(d, 'declaration', { displayStatus: d.displayStatus }));
245
337
  });
246
338
 
247
- // Render milestones
248
- (milestones || []).forEach(m => {
249
- $nodesMiles.appendChild(buildNodeEl(m, 'milestone'));
339
+ enrichedMilestones.forEach(m => {
340
+ $nodesMiles.appendChild(buildNodeEl(m, 'milestone', {
341
+ displayStatus: m.displayStatus,
342
+ doneCount: m.doneCount,
343
+ totalCount: m.totalCount,
344
+ }));
250
345
  });
251
346
 
252
- // Render actions
253
347
  (actions || []).forEach(a => {
254
348
  $nodesActs.appendChild(buildNodeEl(a, 'action'));
255
349
  });
@@ -1230,7 +1324,13 @@ const COMPLETED_STATES = new Set(['DONE', 'KEPT', 'HONORED']);
1230
1324
  function checkProjectComplete(graph) {
1231
1325
  if (confettiFired) return;
1232
1326
  if (!graph || !graph.declarations || graph.declarations.length === 0) return;
1233
- const allDone = graph.declarations.every(d => COMPLETED_STATES.has(d.status));
1327
+ // Use derived statuses (computed from actions) not stored MILESTONES.md status
1328
+ const enriched = (graph.milestones || []).map(m => ({
1329
+ ...m, ...deriveMilestoneStatus(m, graph.actions || []),
1330
+ }));
1331
+ const allDone = graph.declarations.every(d =>
1332
+ COMPLETED_STATES.has(deriveDeclarationStatus(d, enriched))
1333
+ );
1234
1334
  if (!allDone) return;
1235
1335
  confettiFired = true;
1236
1336
  fireConfetti();
@@ -41,6 +41,15 @@
41
41
  --act-done-bg: #08180f;
42
42
  --act-done-border: #123428;
43
43
 
44
+ /* workflow progress tones */
45
+ --planned-color: #5ba3ff;
46
+ --planned-bg: #091828;
47
+ --planned-border: #12305a;
48
+
49
+ --executing-color: #fbbf24;
50
+ --executing-bg: #1a1200;
51
+ --executing-border:#3d2c00;
52
+
44
53
  --broken-color: #ff4d6d;
45
54
  --broken-bg: #2a0a10;
46
55
  --broken-border: #5a1520;
@@ -264,6 +273,41 @@
264
273
  opacity: 0.82;
265
274
  }
266
275
 
276
+ /* Workflow progress states — computed from action data, not MILESTONES.md */
277
+ .node.status-planned {
278
+ background: var(--planned-bg);
279
+ border-color: var(--planned-border);
280
+ color: var(--planned-color);
281
+ opacity: 0.9;
282
+ }
283
+ .node.status-executing {
284
+ background: var(--executing-bg);
285
+ border-color: var(--executing-border);
286
+ color: var(--executing-color);
287
+ box-shadow: 0 0 0 1px var(--executing-border), 0 0 12px rgba(251,191,36,0.15);
288
+ }
289
+ .node.status-planned .node-title { color: var(--planned-color); }
290
+ .node.status-executing .node-title { color: var(--executing-color); }
291
+
292
+ /* Progress bar inside milestone node */
293
+ .node-progress {
294
+ margin-top: 7px;
295
+ height: 3px;
296
+ background: rgba(255,255,255,0.08);
297
+ border-radius: 2px;
298
+ overflow: hidden;
299
+ }
300
+ .node-progress-fill {
301
+ height: 100%;
302
+ border-radius: 2px;
303
+ transition: width 0.4s ease;
304
+ }
305
+ .status-executing .node-progress-fill { background: var(--executing-color); }
306
+ .status-planned .node-progress-fill { background: var(--planned-color); opacity:0.3; }
307
+ .status-done .node-progress-fill,
308
+ .status-kept .node-progress-fill,
309
+ .status-honored .node-progress-fill { background: var(--act-done-color); }
310
+
267
311
  .node.status-broken {
268
312
  background: var(--broken-bg);
269
313
  border-color: var(--broken-border);
@@ -81,10 +81,22 @@ function buildEvent(data) {
81
81
  return null; // skip noisy general bash
82
82
  }
83
83
 
84
- // Write tool — track planning file changes
84
+ // Write tool — track planning file changes + auto-sync on SUMMARY.md writes
85
85
  if (tool === 'Write' && hookEvent === 'PostToolUse') {
86
86
  const fp = input.file_path || '';
87
87
  if (fp.includes('.planning/')) {
88
+ // When an executor writes A-XX-SUMMARY.md, auto-run sync-status so
89
+ // PLAN.md and MILESTONES.md update immediately without manual intervention
90
+ if (fp.includes('-SUMMARY.md')) {
91
+ const { execSync } = require('child_process');
92
+ try {
93
+ execSync(`node "${path.join(cwd, '.claude', 'declare-tools.cjs')}" sync-status`, {
94
+ cwd,
95
+ timeout: 10000,
96
+ stdio: 'ignore',
97
+ });
98
+ } catch (_) { /* silent — never block Claude */ }
99
+ }
88
100
  return { ts, phase: 'done', tool: 'Write', file: fp.replace(cwd, '.') };
89
101
  }
90
102
  return null;
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ // Declare dashboard server — SessionStart hook
3
+ //
4
+ // On every Claude Code session start (for a Declare project):
5
+ // 1. Derive a stable port for this project (hash of cwd, range 3847-4846)
6
+ // 2. If a server is already running on that port for THIS project, leave it
7
+ // 3. If a server is running on that port for a DIFFERENT project, kill it
8
+ // 4. Start a fresh server for the current project
9
+ // 5. Write the port to .planning/server.port for /declare:dashboard to read
10
+ //
11
+ // This means each project gets its own port, servers survive between sessions,
12
+ // and switching projects always gives you the right server.
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const http = require('http');
19
+ const cp = require('child_process');
20
+
21
+ const cwd = process.cwd();
22
+ const planningDir = path.join(cwd, '.planning');
23
+
24
+ // Only run for Declare projects
25
+ if (!fs.existsSync(planningDir)) process.exit(0);
26
+
27
+ const PORT_BASE = 3847;
28
+ const PORT_RANGE = 1000; // ports 3847–4846
29
+
30
+ /**
31
+ * Simple djb2 hash to derive a stable port from the project path.
32
+ * @param {string} str
33
+ * @returns {number} port in [PORT_BASE, PORT_BASE + PORT_RANGE)
34
+ */
35
+ function projectPort(str) {
36
+ let h = 5381;
37
+ for (let i = 0; i < str.length; i++) {
38
+ h = ((h << 5) + h) ^ str.charCodeAt(i);
39
+ h = h >>> 0; // keep unsigned 32-bit
40
+ }
41
+ return PORT_BASE + (h % PORT_RANGE);
42
+ }
43
+
44
+ const port = projectPort(cwd);
45
+ const portFile = path.join(planningDir, 'server.port');
46
+ const bundle = path.join(cwd, '.claude', 'declare-tools.cjs');
47
+
48
+ // If bundle doesn't exist, nothing to do
49
+ if (!fs.existsSync(bundle)) process.exit(0);
50
+
51
+ /**
52
+ * Ask the running server on `port` which cwd it's serving, via /api/graph.
53
+ * Returns the project name or null if nothing is running / not a Declare server.
54
+ */
55
+ function checkRunningServer(port, callback) {
56
+ const req = http.get(`http://127.0.0.1:${port}/api/graph`, { timeout: 1500 }, res => {
57
+ let body = '';
58
+ res.on('data', c => body += c);
59
+ res.on('end', () => {
60
+ try {
61
+ const data = JSON.parse(body);
62
+ // If it has a valid graph response it's our server
63
+ callback(data && !data.error ? 'declare' : null);
64
+ } catch { callback(null); }
65
+ });
66
+ });
67
+ req.on('error', () => callback(null));
68
+ req.on('timeout', () => { req.destroy(); callback(null); });
69
+ }
70
+
71
+ /**
72
+ * Kill any process currently listening on the given port.
73
+ */
74
+ function killPort(port) {
75
+ try {
76
+ const pid = cp.execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: 'utf8' }).trim();
77
+ if (pid) {
78
+ pid.split('\n').forEach(p => {
79
+ try { process.kill(parseInt(p), 'SIGTERM'); } catch {}
80
+ });
81
+ }
82
+ } catch {}
83
+ }
84
+
85
+ /**
86
+ * Start the dashboard server for this project in the background.
87
+ */
88
+ function startServer() {
89
+ const child = cp.spawn(process.execPath, [bundle, 'serve', '--port', String(port)], {
90
+ cwd,
91
+ detached: true,
92
+ stdio: 'ignore',
93
+ });
94
+ child.unref();
95
+
96
+ // Write port file so /declare:dashboard knows where to open
97
+ setTimeout(() => {
98
+ try { fs.writeFileSync(portFile, String(port)); } catch {}
99
+ }, 800);
100
+ }
101
+
102
+ // Check if something is already running on this port
103
+ checkRunningServer(port, status => {
104
+ if (status === 'declare') {
105
+ // A Declare server is already up on our port.
106
+ // It might be from a previous session for this same project — reuse it.
107
+ // Write port file to make sure /declare:dashboard finds it.
108
+ try { fs.writeFileSync(portFile, String(port)); } catch {}
109
+ process.exit(0);
110
+ }
111
+
112
+ // Nothing running (or non-Declare process) — kill whatever is there and start fresh
113
+ killPort(port);
114
+ setTimeout(startServer, 300);
115
+ process.exit(0);
116
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "declare-cc",
3
- "version": "0.5.0",
3
+ "version": "0.5.3",
4
4
  "description": "A future-driven meta-prompting engine for agentic development, rooted in declared futures and causal graph structure.",
5
5
  "bin": {
6
6
  "declare-cc": "bin/install.js"