declare-cc 0.4.5 → 0.5.0

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
@@ -1564,6 +1564,9 @@ function install(isGlobal, runtime = 'claude') {
1564
1564
  const updateCheckCommand = isGlobal
1565
1565
  ? buildHookCommand(targetDir, 'declare-check-update.js')
1566
1566
  : 'node ' + dirName + '/hooks/declare-check-update.js';
1567
+ const activityCommand = isGlobal
1568
+ ? buildHookCommand(targetDir, 'declare-activity.js')
1569
+ : 'node ' + dirName + '/hooks/declare-activity.js';
1567
1570
 
1568
1571
  // Enable experimental agents for Gemini CLI (required for custom sub-agents)
1569
1572
  if (isGemini) {
@@ -1600,6 +1603,21 @@ function install(isGlobal, runtime = 'claude') {
1600
1603
  });
1601
1604
  console.log(` ${green}✓${reset} Configured update check hook`);
1602
1605
  }
1606
+
1607
+ // Configure PreToolUse + PostToolUse hooks for activity feed (Claude Code only)
1608
+ if (!isOpencode && !isGemini) {
1609
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
1610
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
1611
+
1612
+ const hasActivityPre = settings.hooks.PreToolUse.some(e =>
1613
+ e.hooks && e.hooks.some(h => h.command && h.command.includes('declare-activity'))
1614
+ );
1615
+ if (!hasActivityPre) {
1616
+ settings.hooks.PreToolUse.push({ hooks: [{ type: 'command', command: activityCommand }] });
1617
+ settings.hooks.PostToolUse.push({ hooks: [{ type: 'command', command: activityCommand }] });
1618
+ console.log(` ${green}✓${reset} Configured activity feed hooks`);
1619
+ }
1620
+ }
1603
1621
  }
1604
1622
 
1605
1623
  // Write file manifest for future modification detection
@@ -1618,11 +1636,13 @@ function install(isGlobal, runtime = 'claude') {
1618
1636
  function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true) {
1619
1637
  const isOpencode = runtime === 'opencode';
1620
1638
 
1621
- if (shouldInstallStatusline && !isOpencode) {
1622
- settings.statusLine = {
1623
- type: 'command',
1624
- command: statuslineCommand
1625
- };
1639
+ // Statusline is a global UI element — only configure it for global installs.
1640
+ // Local installs must not write statusLine: it would point to a project-specific
1641
+ // path that breaks when the project moves or is deleted.
1642
+ // Users who only do local installs should run `npx declare-cc --claude --global`
1643
+ // once to get the statusline, or configure it manually.
1644
+ if (shouldInstallStatusline && !isOpencode && isGlobal) {
1645
+ settings.statusLine = { type: 'command', command: statuslineCommand };
1626
1646
  console.log(` ${green}✓${reset} Configured statusline`);
1627
1647
  }
1628
1648
 
@@ -1639,9 +1659,12 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
1639
1659
  if (runtime === 'gemini') program = 'Gemini';
1640
1660
 
1641
1661
  const command = isOpencode ? '/declare-help' : '/declare:help';
1662
+ const statuslineNote = (!isGlobal && !isOpencode)
1663
+ ? `\n ${yellow}Tip:${reset} For the context-window statusline, run once globally:\n ${dim}npx declare-cc --claude --global${reset}\n`
1664
+ : '';
1642
1665
  console.log(`
1643
1666
  ${green}Done!${reset} Launch ${program} and run ${cyan}${command}${reset}.
1644
-
1667
+ ${statuslineNote}
1645
1668
  ${cyan}Docs & source:${reset} https://github.com/decocms/declare-cc
1646
1669
  `);
1647
1670
  }
@@ -188,25 +188,28 @@ If `--confirm` flag was set, pause after successful verification:
188
188
  - "Wave N complete and verified. Proceed to Wave N+1? (yes/no)"
189
189
  - Wait for user confirmation before continuing.
190
190
 
191
- **3e. Update action statuses in PLAN.md:**
191
+ **3e. Propagate statuses after each wave:**
192
192
 
193
- After successful wave verification, update each completed action's status in the milestone's PLAN.md file:
193
+ After successful wave verification, run sync-status to update PLAN.md, MILESTONES.md, and FUTURE.md atomically. This keeps the dashboard live as waves complete:
194
194
 
195
- 1. Use the `milestoneFolderPath` from Step 2 to locate the PLAN.md file.
196
- 2. Read the PLAN.md file.
197
- 3. For each action in the completed wave, find `**Status:** PENDING` (or `**Status:** ACTIVE`) for that action and change it to `**Status:** DONE`.
198
- 4. Write the updated PLAN.md back.
195
+ ```bash
196
+ node dist/declare-tools.cjs sync-status
197
+ ```
198
+
199
+ Do not manually edit PLAN.md or MILESTONES.md — sync-status handles all of it correctly.
200
+
201
+ **Step 4: After all waves complete, propagate statuses.**
199
202
 
200
- **Step 4: After all waves complete, check milestone completion.**
203
+ Run sync-status to propagate action milestone → declaration completion automatically:
204
+
205
+ ```bash
206
+ node dist/declare-tools.cjs sync-status
207
+ ```
201
208
 
202
- If `milestoneCompletable` is true from the final verify-wave result:
203
- 1. Read `.planning/MILESTONES.md`.
204
- 2. Find the row for M-XX in the milestones table.
205
- 3. Change its Status from PENDING or ACTIVE to DONE.
206
- 4. Write the updated MILESTONES.md back.
207
- 5. Display: "Milestone M-XX marked as DONE (pending verification)."
209
+ Parse the result. It will show which actions, milestones, and declarations were marked DONE.
210
+ Display: "Status propagated — [summary from sync-status result]."
208
211
 
209
- Proceed to Step 5. Do NOT display a completion banner yet -- that happens in Step 8 after verification.
212
+ Proceed to Step 5. Do NOT display a completion banner yet that happens in Step 8 after verification.
210
213
 
211
214
  **Step 5: Milestone truth verification.**
212
215
 
@@ -1329,7 +1329,7 @@ var require_help = __commonJS({
1329
1329
  usage: "/declare:help"
1330
1330
  }
1331
1331
  ],
1332
- version: "0.4.4"
1332
+ version: "0.4.7"
1333
1333
  };
1334
1334
  }
1335
1335
  module2.exports = { runHelp: runHelp2 };
@@ -3096,6 +3096,167 @@ var require_sync_status = __commonJS({
3096
3096
  }
3097
3097
  });
3098
3098
 
3099
+ // src/commands/get-exec-plan.js
3100
+ var require_get_exec_plan = __commonJS({
3101
+ "src/commands/get-exec-plan.js"(exports2, module2) {
3102
+ "use strict";
3103
+ var { existsSync, readFileSync, readdirSync } = require("node:fs");
3104
+ var { join } = require("node:path");
3105
+ var { parseFlag } = require_parse_args();
3106
+ var { buildDagFromDisk } = require_build_dag();
3107
+ var { findMilestoneFolder } = require_milestone_folders();
3108
+ function parseFrontmatter(fmText) {
3109
+ const result = {};
3110
+ const lines = fmText.split("\n");
3111
+ let i = 0;
3112
+ while (i < lines.length) {
3113
+ const line = lines[i];
3114
+ const keyMatch = line.match(/^(\w[\w_]*):\s*(.*)/);
3115
+ if (!keyMatch) {
3116
+ i++;
3117
+ continue;
3118
+ }
3119
+ const [, key, rest] = keyMatch;
3120
+ if (rest.trim() === "") {
3121
+ const children = [];
3122
+ i++;
3123
+ while (i < lines.length && (lines[i].startsWith(" ") || lines[i].trim() === "")) {
3124
+ const child = lines[i];
3125
+ const listItem = child.match(/^\s+-\s+(.*)/);
3126
+ const nestedKey = child.match(/^\s+(\w[\w_]*):\s*(.*)/);
3127
+ if (listItem) {
3128
+ children.push({ type: "item", value: listItem[1].replace(/^["']|["']$/g, "") });
3129
+ } else if (nestedKey) {
3130
+ children.push({ type: "key", key: nestedKey[1], value: nestedKey[2] });
3131
+ }
3132
+ i++;
3133
+ }
3134
+ if (children.length > 0 && children[0].type === "item") {
3135
+ result[key] = children.map((c) => c.value);
3136
+ } else if (children.length > 0 && children[0].type === "key") {
3137
+ result[key] = {};
3138
+ let subKey = null;
3139
+ let subItems = [];
3140
+ for (const c of children) {
3141
+ if (c.type === "key") {
3142
+ if (subKey) result[key][subKey] = subItems;
3143
+ subKey = c.key;
3144
+ subItems = c.value.trim() ? [c.value.replace(/^["']|["']$/g, "")] : [];
3145
+ } else if (c.type === "item") {
3146
+ subItems.push(c.value);
3147
+ }
3148
+ }
3149
+ if (subKey) result[key][subKey] = subItems;
3150
+ }
3151
+ } else {
3152
+ result[key] = rest.trim().replace(/^["']|["']$/g, "");
3153
+ i++;
3154
+ }
3155
+ }
3156
+ return result;
3157
+ }
3158
+ function extractTag(content, tag) {
3159
+ const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i");
3160
+ const m = content.match(re);
3161
+ return m ? m[1].trim() : null;
3162
+ }
3163
+ function parseTasks(tasksContent) {
3164
+ const tasks = [];
3165
+ const taskRe = /<task([^>]*)>([\s\S]*?)<\/task>/gi;
3166
+ let m;
3167
+ while ((m = taskRe.exec(tasksContent)) !== null) {
3168
+ const attrs = m[1];
3169
+ const body = m[2];
3170
+ const typeMatch = attrs.match(/type="([^"]+)"/);
3171
+ const taskType = typeMatch ? typeMatch[1] : "auto";
3172
+ tasks.push({
3173
+ type: taskType,
3174
+ name: extractTag(body, "name") || "",
3175
+ action: extractTag(body, "action"),
3176
+ verify: extractTag(body, "verify"),
3177
+ done: extractTag(body, "done"),
3178
+ howToVerify: extractTag(body, "how-to-verify"),
3179
+ resumeSignal: extractTag(body, "resume-signal"),
3180
+ whatBuilt: extractTag(body, "what-built")
3181
+ });
3182
+ }
3183
+ return tasks;
3184
+ }
3185
+ function findExecPlan(milestoneFolder, actionId) {
3186
+ if (!existsSync(milestoneFolder)) return null;
3187
+ const entries = readdirSync(milestoneFolder);
3188
+ const match = entries.find(
3189
+ (f) => f.toUpperCase().startsWith(actionId.toUpperCase() + "-EXEC-PLAN") || f.toUpperCase().startsWith("EXEC-PLAN-" + actionId.replace(/^A-/, ""))
3190
+ );
3191
+ return match ? join(milestoneFolder, match) : null;
3192
+ }
3193
+ function runGetExecPlan2(cwd, args) {
3194
+ const actionId = parseFlag(args, "action");
3195
+ if (!actionId) {
3196
+ return { error: "Missing --action flag. Usage: get-exec-plan --action A-XX" };
3197
+ }
3198
+ const graphResult = buildDagFromDisk(cwd);
3199
+ if ("error" in graphResult) return graphResult;
3200
+ const { dag } = graphResult;
3201
+ const action = dag.getNode(actionId);
3202
+ if (!action) return { error: `Action not found: ${actionId}` };
3203
+ const upstreamMilestones = dag.getUpstream(actionId).filter((n) => n.type === "milestone");
3204
+ if (upstreamMilestones.length === 0) return { error: `No milestone found for action ${actionId}` };
3205
+ const milestone = upstreamMilestones[0];
3206
+ const planningDir = join(cwd, ".planning");
3207
+ const milestoneFolder = findMilestoneFolder(planningDir, milestone.id);
3208
+ if (!milestoneFolder) {
3209
+ return { error: `Milestone folder not found for ${milestone.id}` };
3210
+ }
3211
+ const execPlanPath = findExecPlan(milestoneFolder, actionId);
3212
+ if (!execPlanPath) {
3213
+ return {
3214
+ actionId,
3215
+ actionTitle: action.title,
3216
+ status: action.status,
3217
+ milestoneId: milestone.id,
3218
+ milestoneTitle: milestone.title,
3219
+ execPlan: null,
3220
+ summaryExists: false
3221
+ };
3222
+ }
3223
+ const raw = readFileSync(execPlanPath, "utf-8");
3224
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
3225
+ const frontmatter = fmMatch ? parseFrontmatter(fmMatch[1]) : {};
3226
+ const objective = extractTag(raw, "objective");
3227
+ const tasksRaw = extractTag(raw, "tasks");
3228
+ const tasks = tasksRaw ? parseTasks(tasksRaw) : [];
3229
+ const successCriteria = extractTag(raw, "success_criteria");
3230
+ const verification = extractTag(raw, "verification");
3231
+ const summaryPath = join(milestoneFolder, `${actionId}-SUMMARY.md`);
3232
+ const summaryExists = existsSync(summaryPath);
3233
+ const summaryContent = summaryExists ? readFileSync(summaryPath, "utf-8") : null;
3234
+ return {
3235
+ actionId,
3236
+ actionTitle: action.title,
3237
+ status: action.status,
3238
+ milestoneId: milestone.id,
3239
+ milestoneTitle: milestone.title,
3240
+ execPlan: {
3241
+ wave: frontmatter.wave ? Number(frontmatter.wave) : null,
3242
+ autonomous: frontmatter.autonomous === "true" || frontmatter.autonomous === true,
3243
+ dependsOn: Array.isArray(frontmatter.depends_on) ? frontmatter.depends_on : [],
3244
+ filesModified: Array.isArray(frontmatter.files_modified) ? frontmatter.files_modified : [],
3245
+ declarations: Array.isArray(frontmatter.declarations) ? frontmatter.declarations : [],
3246
+ mustHaves: frontmatter.must_haves || null,
3247
+ objective,
3248
+ tasks,
3249
+ successCriteria,
3250
+ verification
3251
+ },
3252
+ summaryExists,
3253
+ summaryContent
3254
+ };
3255
+ }
3256
+ module2.exports = { runGetExecPlan: runGetExecPlan2 };
3257
+ }
3258
+ });
3259
+
3099
3260
  // src/commands/quick-task.js
3100
3261
  var require_quick_task = __commonJS({
3101
3262
  "src/commands/quick-task.js"(exports2, module2) {
@@ -3666,6 +3827,7 @@ var require_server = __commonJS({
3666
3827
  var path = require("node:path");
3667
3828
  var { runLoadGraph: runLoadGraph2 } = require_load_graph();
3668
3829
  var { runStatus: runStatus2 } = require_status();
3830
+ var { runGetExecPlan: runGetExecPlan2 } = require_get_exec_plan();
3669
3831
  var MIME_TYPES = {
3670
3832
  ".html": "text/html; charset=utf-8",
3671
3833
  ".js": "application/javascript; charset=utf-8",
@@ -3762,6 +3924,26 @@ var require_server = __commonJS({
3762
3924
  sendJson(res, 500, { error: String(err) });
3763
3925
  }
3764
3926
  }
3927
+ function handleActivity(res, cwd) {
3928
+ const activityFile = path.join(cwd, ".planning", "activity.jsonl");
3929
+ if (!fs.existsSync(activityFile)) {
3930
+ sendJson(res, 200, { events: [] });
3931
+ return;
3932
+ }
3933
+ try {
3934
+ const lines = fs.readFileSync(activityFile, "utf-8").split("\n").filter(Boolean).slice(-100);
3935
+ const events = lines.map((l) => {
3936
+ try {
3937
+ return JSON.parse(l);
3938
+ } catch {
3939
+ return null;
3940
+ }
3941
+ }).filter(Boolean).reverse();
3942
+ sendJson(res, 200, { events });
3943
+ } catch (err) {
3944
+ sendJson(res, 500, { error: String(err) });
3945
+ }
3946
+ }
3765
3947
  var sseClients = /* @__PURE__ */ new Set();
3766
3948
  function broadcastChange() {
3767
3949
  for (const client of sseClients) {
@@ -3775,14 +3957,30 @@ var require_server = __commonJS({
3775
3957
  function watchPlanning(cwd) {
3776
3958
  const planningDir = path.join(cwd, ".planning");
3777
3959
  if (!fs.existsSync(planningDir)) return;
3778
- let debounceTimer = null;
3960
+ let graphTimer = null;
3961
+ let activityTimer = null;
3962
+ const activityFile = path.join(planningDir, "activity.jsonl");
3779
3963
  try {
3780
- fs.watch(planningDir, { recursive: true }, () => {
3781
- if (debounceTimer) clearTimeout(debounceTimer);
3782
- debounceTimer = setTimeout(() => {
3783
- broadcastChange();
3784
- debounceTimer = null;
3785
- }, 200);
3964
+ fs.watch(planningDir, { recursive: true }, (_evt, filename) => {
3965
+ if (filename && filename.endsWith("activity.jsonl")) {
3966
+ if (activityTimer) clearTimeout(activityTimer);
3967
+ activityTimer = setTimeout(() => {
3968
+ for (const client of sseClients) {
3969
+ try {
3970
+ client.write("event: activity\ndata: {}\n\n");
3971
+ } catch {
3972
+ sseClients.delete(client);
3973
+ }
3974
+ }
3975
+ activityTimer = null;
3976
+ }, 50);
3977
+ } else {
3978
+ if (graphTimer) clearTimeout(graphTimer);
3979
+ graphTimer = setTimeout(() => {
3980
+ broadcastChange();
3981
+ graphTimer = null;
3982
+ }, 200);
3983
+ }
3786
3984
  });
3787
3985
  } catch (_) {
3788
3986
  }
@@ -3829,6 +4027,16 @@ var require_server = __commonJS({
3829
4027
  handleMilestone(res, cwd, milestoneMatch[1]);
3830
4028
  return;
3831
4029
  }
4030
+ if (urlPath === "/api/activity") {
4031
+ handleActivity(res, cwd);
4032
+ return;
4033
+ }
4034
+ const actionMatch = urlPath.match(/^\/api\/action\/([^/]+)$/);
4035
+ if (actionMatch) {
4036
+ const result = runGetExecPlan2(cwd, ["--action", actionMatch[1]]);
4037
+ sendJson(res, result.error ? 404 : 200, result);
4038
+ return;
4039
+ }
3832
4040
  const publicDir = getPublicDir(cwd);
3833
4041
  if (urlPath === "/") {
3834
4042
  const indexPath = path.join(publicDir, "index.html");
@@ -3918,6 +4126,7 @@ var { runComputePerformance } = require_compute_performance();
3918
4126
  var { runRenegotiate } = require_renegotiate();
3919
4127
  var { runCompleteMilestone } = require_complete_milestone();
3920
4128
  var { runSyncStatus } = require_sync_status();
4129
+ var { runGetExecPlan } = require_get_exec_plan();
3921
4130
  var { runQuickTask } = require_quick_task();
3922
4131
  var { runAddTodo, runCheckTodos, runCompleteTodo } = require_todo();
3923
4132
  var { runConfigGet } = require_config_get();
@@ -4094,6 +4303,13 @@ function main() {
4094
4303
  if (result.error) process.exit(1);
4095
4304
  break;
4096
4305
  }
4306
+ case "get-exec-plan": {
4307
+ const cwdGep = parseCwdFlag(args) || process.cwd();
4308
+ const result = runGetExecPlan(cwdGep, args.slice(1));
4309
+ console.log(JSON.stringify(result));
4310
+ if (result.error) process.exit(1);
4311
+ break;
4312
+ }
4097
4313
  case "execute": {
4098
4314
  const cwdExecute = parseCwdFlag(args) || process.cwd();
4099
4315
  const result = runExecute(cwdExecute, args.slice(1));
@@ -146,6 +146,7 @@ async function loadData() {
146
146
  renderStatusBar();
147
147
  renderGraph();
148
148
  updateLastUpdated();
149
+ checkProjectComplete(graph);
149
150
 
150
151
  // Re-apply selection highlight if node still exists
151
152
  if (selectedNodeId) {
@@ -650,10 +651,16 @@ function renderPanelChain(item, type) {
650
651
  const causedBy = actions.filter(a => (a.causes || []).includes(s.item.id));
651
652
  if (causedBy.length) html += chainTagSection('Actions', causedBy, 'action');
652
653
  }
653
- if (s.type === 'action' && s.item.produces) {
654
- html += `<div style="margin-top:14px">
655
- <div class="detail-label">Produces</div>
656
- <div class="detail-value" style="margin-top:5px">${escHtml(s.item.produces)}</div>
654
+ if (s.type === 'action') {
655
+ if (s.item.produces) {
656
+ html += `<div style="margin-top:14px">
657
+ <div class="detail-label">Produces</div>
658
+ <div class="detail-value" style="margin-top:5px">${escHtml(s.item.produces)}</div>
659
+ </div>`;
660
+ }
661
+ // Exec-plan placeholder — filled asynchronously after render
662
+ html += `<div id="exec-plan-detail" style="margin-top:16px">
663
+ <div class="detail-label" style="opacity:0.4">Loading exec-plan…</div>
657
664
  </div>`;
658
665
  }
659
666
  }
@@ -667,6 +674,12 @@ function renderPanelChain(item, type) {
667
674
  selectNode(tag.dataset.chainId, tag.dataset.chainType);
668
675
  });
669
676
  });
677
+
678
+ // If an action is focused, fetch and render its exec-plan
679
+ const focusSection = sections.find(s => s.role === 'focus');
680
+ if (focusSection && focusSection.type === 'action') {
681
+ loadExecPlan(focusSection.item.id);
682
+ }
670
683
  }
671
684
 
672
685
  function chainTagSection(label, items, type) {
@@ -680,6 +693,121 @@ function chainTagSection(label, items, type) {
680
693
  </div>`;
681
694
  }
682
695
 
696
+ /**
697
+ * Fetch /api/action/:id and render the exec-plan into #exec-plan-detail.
698
+ * @param {string} actionId
699
+ */
700
+ async function loadExecPlan(actionId) {
701
+ const container = document.getElementById('exec-plan-detail');
702
+ if (!container) return;
703
+
704
+ try {
705
+ const res = await fetch(`/api/action/${encodeURIComponent(actionId)}`);
706
+ const data = await res.json();
707
+
708
+ if (data.error || !data.execPlan) {
709
+ container.innerHTML = `<div class="detail-label" style="opacity:0.4">No exec-plan found</div>`;
710
+ return;
711
+ }
712
+
713
+ const ep = data.execPlan;
714
+ let html = '';
715
+
716
+ // Execution metadata bar
717
+ const metaParts = [];
718
+ if (ep.wave != null) metaParts.push(`Wave ${ep.wave}`);
719
+ if (ep.autonomous != null) metaParts.push(ep.autonomous ? '⚡ Autonomous' : '🧑 Checkpoint');
720
+ if (ep.dependsOn && ep.dependsOn.length) metaParts.push(`Depends: ${ep.dependsOn.join(', ')}`);
721
+ if (data.summaryExists) metaParts.push('✓ Executed');
722
+
723
+ if (metaParts.length) {
724
+ html += `<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px">
725
+ ${metaParts.map(p => `<span style="background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:2px 8px;font-size:10px;font-weight:600;color:var(--text-dim)">${p}</span>`).join('')}
726
+ </div>`;
727
+ }
728
+
729
+ // Files modified
730
+ if (ep.filesModified && ep.filesModified.length) {
731
+ html += `<div style="margin-bottom:14px">
732
+ <div class="detail-label">Files</div>
733
+ <div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:5px">
734
+ ${ep.filesModified.map(f => `<span style="background:var(--act-bg);border:1px solid var(--act-border);color:var(--act-color);border-radius:4px;padding:2px 7px;font-size:10px;font-family:monospace">${escHtml(f)}</span>`).join('')}
735
+ </div>
736
+ </div>`;
737
+ }
738
+
739
+ // Objective
740
+ if (ep.objective) {
741
+ html += `<div style="margin-bottom:14px">
742
+ <div class="detail-label">Objective</div>
743
+ <div class="detail-value" style="margin-top:5px;white-space:pre-wrap">${escHtml(ep.objective)}</div>
744
+ </div>`;
745
+ }
746
+
747
+ // Tasks
748
+ if (ep.tasks && ep.tasks.length) {
749
+ html += `<div style="margin-bottom:14px">
750
+ <div class="detail-label">Tasks (${ep.tasks.length})</div>
751
+ <div style="margin-top:8px;display:flex;flex-direction:column;gap:8px">
752
+ ${ep.tasks.map((t, i) => {
753
+ const isCheckpoint = t.type && t.type.includes('checkpoint');
754
+ const typeColor = isCheckpoint ? 'var(--renegotiated-color)' : 'var(--act-color)';
755
+ const typeBg = isCheckpoint ? 'var(--renegotiated-bg)' : 'var(--act-bg)';
756
+ const typeBorder = isCheckpoint ? 'var(--renegotiated-border)' : 'var(--act-border)';
757
+ const taskText = t.action || t.whatBuilt || '';
758
+ const verifyText = t.howToVerify || t.verify || '';
759
+ return `<div style="background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:10px 12px">
760
+ <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
761
+ <span style="font-size:10px;font-weight:700;color:var(--text-dim)">${i + 1}</span>
762
+ <span style="font-weight:600;font-size:12px;color:var(--text-bright);flex:1">${escHtml(t.name)}</span>
763
+ <span style="background:${typeBg};color:${typeColor};border:1px solid ${typeBorder};border-radius:4px;padding:1px 6px;font-size:9px;font-weight:700;white-space:nowrap">${escHtml(t.type)}</span>
764
+ </div>
765
+ ${taskText ? `<div style="font-size:11px;color:var(--text-dim);white-space:pre-wrap;margin-bottom:6px">${escHtml(taskText.slice(0, 300))}${taskText.length > 300 ? '…' : ''}</div>` : ''}
766
+ ${verifyText ? `<div style="font-size:10px;color:var(--text-dim);border-top:1px solid var(--border);padding-top:6px;margin-top:4px;white-space:pre-wrap"><span style="font-weight:700">Verify:</span> ${escHtml(verifyText.slice(0, 200))}${verifyText.length > 200 ? '…' : ''}</div>` : ''}
767
+ </div>`;
768
+ }).join('')}
769
+ </div>
770
+ </div>`;
771
+ }
772
+
773
+ // Must-haves
774
+ if (ep.mustHaves) {
775
+ if (ep.mustHaves.truths && ep.mustHaves.truths.length) {
776
+ html += `<div style="margin-bottom:14px">
777
+ <div class="detail-label">Must be true</div>
778
+ <ul style="margin-top:6px;padding-left:16px;display:flex;flex-direction:column;gap:3px">
779
+ ${ep.mustHaves.truths.map(t => `<li style="font-size:11px;color:var(--text-dim)">${escHtml(t)}</li>`).join('')}
780
+ </ul>
781
+ </div>`;
782
+ }
783
+ if (ep.mustHaves.artifacts && ep.mustHaves.artifacts.length) {
784
+ html += `<div style="margin-bottom:14px">
785
+ <div class="detail-label">Artifacts</div>
786
+ <div style="display:flex;flex-direction:column;gap:4px;margin-top:6px">
787
+ ${ep.mustHaves.artifacts.map(a => `<div style="font-size:11px;color:var(--text-dim)">
788
+ <span style="font-family:monospace;color:var(--act-color)">${escHtml(a.path || '')}</span>
789
+ ${a.provides ? ` — ${escHtml(a.provides)}` : ''}
790
+ </div>`).join('')}
791
+ </div>
792
+ </div>`;
793
+ }
794
+ }
795
+
796
+ // Success criteria
797
+ if (ep.successCriteria) {
798
+ html += `<div style="margin-bottom:8px">
799
+ <div class="detail-label">Success criteria</div>
800
+ <div class="detail-value" style="margin-top:5px;white-space:pre-wrap">${escHtml(ep.successCriteria)}</div>
801
+ </div>`;
802
+ }
803
+
804
+ container.innerHTML = html || `<div class="detail-label" style="opacity:0.4">No exec-plan details</div>`;
805
+
806
+ } catch (e) {
807
+ if (container) container.innerHTML = `<div class="detail-label" style="opacity:0.4">Could not load exec-plan</div>`;
808
+ }
809
+ }
810
+
683
811
  // ─── Focus mode — FLIP technique ──────────────────────────────────────────────
684
812
  // Exiting nodes: removed from flow instantly (→ flex re-centers), then overlaid
685
813
  // at their original positions via position:fixed for the directional slide-out.
@@ -1087,6 +1215,144 @@ document.getElementById('canvas-wrap').addEventListener('scroll', () => {
1087
1215
  if (graphData) requestAnimationFrame(() => drawEdges());
1088
1216
  });
1089
1217
 
1218
+ // ─── Confetti ────────────────────────────────────────────────────────────────
1219
+ // Fires once when all declarations reach a completed state (DONE/KEPT/HONORED).
1220
+ // Pure canvas — no external deps.
1221
+
1222
+ let confettiFired = false;
1223
+
1224
+ const COMPLETED_STATES = new Set(['DONE', 'KEPT', 'HONORED']);
1225
+
1226
+ /**
1227
+ * Check if all declarations are complete and fire confetti if so.
1228
+ * @param {{ declarations: any[] } | null} graph
1229
+ */
1230
+ function checkProjectComplete(graph) {
1231
+ if (confettiFired) return;
1232
+ if (!graph || !graph.declarations || graph.declarations.length === 0) return;
1233
+ const allDone = graph.declarations.every(d => COMPLETED_STATES.has(d.status));
1234
+ if (!allDone) return;
1235
+ confettiFired = true;
1236
+ fireConfetti();
1237
+ }
1238
+
1239
+ function fireConfetti() {
1240
+ const canvas = document.createElement('canvas');
1241
+ canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999';
1242
+ document.body.appendChild(canvas);
1243
+
1244
+ const ctx = canvas.getContext('2d');
1245
+ canvas.width = window.innerWidth;
1246
+ canvas.height = window.innerHeight;
1247
+
1248
+ const COLORS = [
1249
+ '#5ba3ff', '#a66bff', '#34d399', // brand: blue, purple, green
1250
+ '#fbbf24', '#f87171', '#38bdf8', // yellow, red, sky
1251
+ '#ffffff', // white
1252
+ ];
1253
+
1254
+ const count = 180;
1255
+ const pieces = Array.from({ length: count }, () => ({
1256
+ x: Math.random() * canvas.width,
1257
+ y: Math.random() * canvas.height * -0.5 - 10,
1258
+ w: 6 + Math.random() * 8,
1259
+ h: 3 + Math.random() * 5,
1260
+ rot: Math.random() * Math.PI * 2,
1261
+ rotV: (Math.random() - 0.5) * 0.2,
1262
+ vx: (Math.random() - 0.5) * 4,
1263
+ vy: 2 + Math.random() * 4,
1264
+ color: COLORS[Math.floor(Math.random() * COLORS.length)],
1265
+ alpha: 1,
1266
+ }));
1267
+
1268
+ let frame;
1269
+ function draw() {
1270
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1271
+ let alive = 0;
1272
+ for (const p of pieces) {
1273
+ p.x += p.vx;
1274
+ p.y += p.vy;
1275
+ p.vy += 0.07; // gravity
1276
+ p.vx *= 0.99; // drag
1277
+ p.rot += p.rotV;
1278
+ // fade out once off-screen bottom
1279
+ if (p.y > canvas.height * 0.85) p.alpha -= 0.025;
1280
+ if (p.alpha <= 0) continue;
1281
+ alive++;
1282
+ ctx.save();
1283
+ ctx.globalAlpha = p.alpha;
1284
+ ctx.translate(p.x, p.y);
1285
+ ctx.rotate(p.rot);
1286
+ ctx.fillStyle = p.color;
1287
+ ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
1288
+ ctx.restore();
1289
+ }
1290
+ if (alive > 0) {
1291
+ frame = requestAnimationFrame(draw);
1292
+ } else {
1293
+ canvas.remove();
1294
+ }
1295
+ }
1296
+ frame = requestAnimationFrame(draw);
1297
+
1298
+ // Safety cleanup after 8s
1299
+ setTimeout(() => { cancelAnimationFrame(frame); canvas.remove(); }, 8000);
1300
+ }
1301
+
1302
+ // ─── Activity feed ────────────────────────────────────────────────────────────
1303
+
1304
+ const $activityFeed = document.getElementById('activity-feed');
1305
+ const $activityList = document.getElementById('activity-list');
1306
+ const $activityPulse = document.getElementById('activity-pulse');
1307
+ const $activityToggle = document.getElementById('activity-toggle');
1308
+ let activityExpanded = false;
1309
+
1310
+ $activityToggle.addEventListener('click', () => {
1311
+ activityExpanded = !activityExpanded;
1312
+ $activityFeed.classList.toggle('expanded', activityExpanded);
1313
+ });
1314
+
1315
+ /**
1316
+ * Fetch /api/activity and render the event list.
1317
+ */
1318
+ async function loadActivity() {
1319
+ try {
1320
+ const res = await fetch('/api/activity');
1321
+ const { events } = await res.json();
1322
+ if (!events || events.length === 0) return;
1323
+
1324
+ $activityFeed.classList.add('has-events');
1325
+
1326
+ const html = events.slice(0, 50).map(ev => {
1327
+ const time = new Date(ev.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
1328
+
1329
+ if (ev.tool === 'Task') {
1330
+ const icon = ev.phase === 'start' ? '⟳' : '✓';
1331
+ const cls = ev.phase === 'start' ? 'agent-start' : 'agent-done';
1332
+ const label = ev.phase === 'start'
1333
+ ? `Spawning ${ev.agent || 'agent'}: ${ev.desc}`
1334
+ : `Done: ${ev.desc}`;
1335
+ return `<div class="activity-event"><span class="ae-time">${time}</span><span class="ae-icon">${icon}</span><span class="ae-text ${cls}">${escHtml(label)}</span></div>`;
1336
+ }
1337
+ if (ev.tool === 'Bash') {
1338
+ const icon = ev.phase === 'start' ? '▶' : '■';
1339
+ return `<div class="activity-event"><span class="ae-time">${time}</span><span class="ae-icon" style="opacity:0.5">${icon}</span><span class="ae-text bash">${escHtml(ev.cmd || '')}</span></div>`;
1340
+ }
1341
+ if (ev.tool === 'Write') {
1342
+ return `<div class="activity-event"><span class="ae-time">${time}</span><span class="ae-icon" style="opacity:0.5">✎</span><span class="ae-text write">${escHtml(ev.file || '')}</span></div>`;
1343
+ }
1344
+ return '';
1345
+ }).filter(Boolean).join('');
1346
+
1347
+ $activityList.innerHTML = html || $activityList.innerHTML;
1348
+
1349
+ // Flash pulse
1350
+ $activityPulse.classList.add('live');
1351
+ clearTimeout($activityPulse._timer);
1352
+ $activityPulse._timer = setTimeout(() => $activityPulse.classList.remove('live'), 3000);
1353
+ } catch (_) {}
1354
+ }
1355
+
1090
1356
  // ─── Live updates via Server-Sent Events ─────────────────────────────────────
1091
1357
  // Server watches .planning/ with fs.watch and pushes a 'change' event.
1092
1358
  // Client re-renders only when idle (not mid-animation).
@@ -1097,6 +1363,9 @@ function connectSSE() {
1097
1363
  if (focusNodeId || focusCleanupTimer) return; // skip during animation
1098
1364
  loadData();
1099
1365
  });
1366
+ es.addEventListener('activity', () => {
1367
+ loadActivity();
1368
+ });
1100
1369
  es.addEventListener('error', () => {
1101
1370
  // Connection dropped — reconnect after 3s
1102
1371
  es.close();
@@ -1110,3 +1379,4 @@ connectSSE();
1110
1379
 
1111
1380
  showLoading();
1112
1381
  loadData();
1382
+ loadActivity();
@@ -146,6 +146,7 @@
146
146
  overflow: auto;
147
147
  background: var(--bg);
148
148
  padding-right: var(--panel-width);
149
+ padding-bottom: 40px; /* room for activity feed */
149
150
  }
150
151
 
151
152
  #canvas-container {
@@ -545,6 +546,85 @@
545
546
  z-index: 50;
546
547
  }
547
548
  #focus-hint.visible { opacity: 1; }
549
+ /* ── Activity feed ── */
550
+ #activity-feed {
551
+ position: fixed;
552
+ bottom: 0;
553
+ left: 0;
554
+ right: var(--panel-width);
555
+ height: 36px;
556
+ background: var(--surface);
557
+ border-top: 1px solid var(--border);
558
+ display: flex;
559
+ align-items: center;
560
+ overflow: hidden;
561
+ z-index: 20;
562
+ transition: height 0.2s ease;
563
+ }
564
+ #activity-feed.expanded { height: 180px; align-items: flex-start; }
565
+ #activity-feed.has-events { border-top-color: var(--act-border); }
566
+
567
+ #activity-toggle {
568
+ flex-shrink: 0;
569
+ padding: 0 12px;
570
+ font-size: 10px;
571
+ font-weight: 700;
572
+ letter-spacing: 0.06em;
573
+ color: var(--text-dim);
574
+ cursor: pointer;
575
+ user-select: none;
576
+ display: flex;
577
+ align-items: center;
578
+ gap: 6px;
579
+ height: 36px;
580
+ }
581
+ #activity-toggle:hover { color: var(--text-bright); }
582
+ #activity-pulse {
583
+ width: 6px; height: 6px;
584
+ border-radius: 50%;
585
+ background: var(--act-color);
586
+ opacity: 0;
587
+ transition: opacity 0.3s;
588
+ }
589
+ #activity-pulse.live { opacity: 1; animation: pulse 1.5s ease-in-out infinite; }
590
+ @keyframes pulse {
591
+ 0%, 100% { opacity: 1; }
592
+ 50% { opacity: 0.3; }
593
+ }
594
+
595
+ #activity-list {
596
+ flex: 1;
597
+ overflow-y: auto;
598
+ overflow-x: hidden;
599
+ padding: 0 8px;
600
+ font-size: 11px;
601
+ font-family: monospace;
602
+ display: flex;
603
+ flex-direction: column;
604
+ gap: 0;
605
+ }
606
+ #activity-feed:not(.expanded) #activity-list { flex-direction: row; align-items: center; gap: 16px; overflow: hidden; white-space: nowrap; }
607
+
608
+ .activity-event {
609
+ padding: 4px 0;
610
+ color: var(--text-dim);
611
+ border-bottom: 1px solid var(--surface2);
612
+ display: flex;
613
+ align-items: baseline;
614
+ gap: 8px;
615
+ flex-shrink: 0;
616
+ }
617
+ #activity-feed:not(.expanded) .activity-event { border: none; padding: 0; }
618
+ .activity-event:last-child { border-bottom: none; }
619
+
620
+ .ae-time { opacity: 0.4; font-size: 10px; flex-shrink: 0; }
621
+ .ae-icon { flex-shrink: 0; }
622
+ .ae-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
623
+ .ae-text.agent-start { color: var(--act-color); font-weight: 600; }
624
+ .ae-text.agent-done { color: var(--mile-color); }
625
+ .ae-text.bash { color: var(--text-dim); }
626
+ .ae-text.write { color: var(--decl-color); }
627
+
548
628
  </style>
549
629
  </head>
550
630
  <body>
@@ -595,6 +675,17 @@
595
675
  </div>
596
676
  </div>
597
677
 
678
+ <!-- Activity feed — live agent/tool event stream -->
679
+ <div id="activity-feed">
680
+ <div id="activity-toggle">
681
+ <div id="activity-pulse"></div>
682
+ <span id="activity-label">ACTIVITY</span>
683
+ </div>
684
+ <div id="activity-list">
685
+ <span style="color:var(--text-dim);font-size:10px;opacity:0.4">No activity yet — agents will appear here when running</span>
686
+ </div>
687
+ </div>
688
+
598
689
  <!-- Loading/error overlay -->
599
690
  <div id="overlay">
600
691
  <div class="spinner"></div>
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ // Declare activity hook — PreToolUse + PostToolUse
3
+ // Writes interesting tool events to .planning/activity.jsonl
4
+ // Server watches .planning/ via fs.watch and pushes SSE events to dashboard.
5
+ //
6
+ // Installed for PreToolUse and PostToolUse hook events.
7
+ // Runs fast: read stdin → decide → append one line → exit.
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+
15
+ const cwd = process.cwd();
16
+ const planningDir = path.join(cwd, '.planning');
17
+ const activityFile = path.join(planningDir, 'activity.jsonl');
18
+
19
+ // Only write if .planning/ exists (i.e. this is a Declare project)
20
+ if (!fs.existsSync(planningDir)) process.exit(0);
21
+
22
+ let raw = '';
23
+ process.stdin.setEncoding('utf8');
24
+ process.stdin.on('data', c => raw += c);
25
+ process.stdin.on('end', () => {
26
+ try {
27
+ const data = JSON.parse(raw);
28
+ const event = buildEvent(data);
29
+ if (!event) process.exit(0);
30
+
31
+ // Ensure file exists
32
+ if (!fs.existsSync(activityFile)) fs.writeFileSync(activityFile, '');
33
+
34
+ // Append event + trim to last 200 lines
35
+ const line = JSON.stringify(event) + '\n';
36
+ fs.appendFileSync(activityFile, line);
37
+
38
+ // Trim to last 200 lines to avoid unbounded growth
39
+ const content = fs.readFileSync(activityFile, 'utf8');
40
+ const lines = content.split('\n').filter(Boolean);
41
+ if (lines.length > 200) {
42
+ fs.writeFileSync(activityFile, lines.slice(-200).join('\n') + '\n');
43
+ }
44
+ } catch (_) {
45
+ // Silent fail — never block Claude
46
+ }
47
+ process.exit(0);
48
+ });
49
+
50
+ /**
51
+ * Build an activity event from a hook payload, or return null to skip.
52
+ * @param {any} data
53
+ * @returns {object|null}
54
+ */
55
+ function buildEvent(data) {
56
+ const tool = data.tool_name || '';
57
+ const input = data.tool_input || {};
58
+ const response = data.tool_response;
59
+ const hookEvent = data.hook_event_name || ''; // PreToolUse or PostToolUse
60
+ const ts = Date.now();
61
+ const phase = hookEvent === 'PostToolUse' ? 'done' : 'start';
62
+
63
+ // Task spawns — most important for agent visibility
64
+ if (tool === 'Task') {
65
+ return {
66
+ ts, phase, tool: 'Task',
67
+ desc: input.description || '',
68
+ agent: input.subagent_type || '',
69
+ // truncate prompt to avoid massive payloads
70
+ prompt: (input.prompt || '').slice(0, 200),
71
+ bg: hookEvent === 'PostToolUse',
72
+ };
73
+ }
74
+
75
+ // Bash commands that involve declare-tools (execution steps)
76
+ if (tool === 'Bash') {
77
+ const cmd = input.command || '';
78
+ if (cmd.includes('declare-tools') || cmd.includes('/declare:')) {
79
+ return { ts, phase, tool: 'Bash', cmd: cmd.slice(0, 200) };
80
+ }
81
+ return null; // skip noisy general bash
82
+ }
83
+
84
+ // Write tool — track planning file changes
85
+ if (tool === 'Write' && hookEvent === 'PostToolUse') {
86
+ const fp = input.file_path || '';
87
+ if (fp.includes('.planning/')) {
88
+ return { ts, phase: 'done', tool: 'Write', file: fp.replace(cwd, '.') };
89
+ }
90
+ return null;
91
+ }
92
+
93
+ return null;
94
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ // Check for Declare updates in background, write result to cache
3
+ // Called by SessionStart hook - runs once per session
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const { spawn } = require('child_process');
9
+
10
+ const homeDir = os.homedir();
11
+ const cwd = process.cwd();
12
+ const cacheDir = path.join(homeDir, '.claude', 'cache');
13
+ const cacheFile = path.join(cacheDir, 'declare-update-check.json');
14
+
15
+ // VERSION file locations (check project first, then global)
16
+ const projectVersionFile = path.join(cwd, '.claude', 'declare', 'VERSION');
17
+ const globalVersionFile = path.join(homeDir, '.claude', 'declare', 'VERSION');
18
+
19
+ // Ensure cache directory exists
20
+ if (!fs.existsSync(cacheDir)) {
21
+ fs.mkdirSync(cacheDir, { recursive: true });
22
+ }
23
+
24
+ // Run check in background (spawn background process, windowsHide prevents console flash)
25
+ const child = spawn(process.execPath, ['-e', `
26
+ const fs = require('fs');
27
+ const { execSync } = require('child_process');
28
+
29
+ const cacheFile = ${JSON.stringify(cacheFile)};
30
+ const projectVersionFile = ${JSON.stringify(projectVersionFile)};
31
+ const globalVersionFile = ${JSON.stringify(globalVersionFile)};
32
+
33
+ // Check project directory first (local install), then global
34
+ let installed = '0.0.0';
35
+ try {
36
+ if (fs.existsSync(projectVersionFile)) {
37
+ installed = fs.readFileSync(projectVersionFile, 'utf8').trim();
38
+ } else if (fs.existsSync(globalVersionFile)) {
39
+ installed = fs.readFileSync(globalVersionFile, 'utf8').trim();
40
+ }
41
+ } catch (e) {}
42
+
43
+ let latest = null;
44
+ try {
45
+ latest = execSync('npm view declare-cc version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
46
+ } catch (e) {}
47
+
48
+ const result = {
49
+ update_available: latest && installed !== latest,
50
+ installed,
51
+ latest: latest || 'unknown',
52
+ checked: Math.floor(Date.now() / 1000)
53
+ };
54
+
55
+ fs.writeFileSync(cacheFile, JSON.stringify(result));
56
+ `], {
57
+ stdio: 'ignore',
58
+ windowsHide: true,
59
+ detached: true // Required on Windows for proper process detachment
60
+ });
61
+
62
+ child.unref();
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code Statusline - Declare Edition
3
+ // Shows: model | current task | directory | context usage
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ // Read JSON from stdin
10
+ let input = '';
11
+ process.stdin.setEncoding('utf8');
12
+ process.stdin.on('data', chunk => input += chunk);
13
+ process.stdin.on('end', () => {
14
+ try {
15
+ const data = JSON.parse(input);
16
+ const model = data.model?.display_name || 'Claude';
17
+ const dir = data.workspace?.current_dir || process.cwd();
18
+ const session = data.session_id || '';
19
+ const remaining = data.context_window?.remaining_percentage;
20
+
21
+ // Context window display (shows USED percentage scaled to 80% limit)
22
+ // Claude Code enforces an 80% context limit, so we scale to show 100% at that point
23
+ let ctx = '';
24
+ if (remaining != null) {
25
+ const rem = Math.round(remaining);
26
+ const rawUsed = Math.max(0, Math.min(100, 100 - rem));
27
+ // Scale: 80% real usage = 100% displayed
28
+ const used = Math.min(100, Math.round((rawUsed / 80) * 100));
29
+
30
+ // Build progress bar (10 segments)
31
+ const filled = Math.floor(used / 10);
32
+ const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
33
+
34
+ // Color based on scaled usage (thresholds adjusted for new scale)
35
+ if (used < 63) { // ~50% real
36
+ ctx = ` \x1b[32m${bar} ${used}%\x1b[0m`;
37
+ } else if (used < 81) { // ~65% real
38
+ ctx = ` \x1b[33m${bar} ${used}%\x1b[0m`;
39
+ } else if (used < 95) { // ~76% real
40
+ ctx = ` \x1b[38;5;208m${bar} ${used}%\x1b[0m`;
41
+ } else {
42
+ ctx = ` \x1b[5;31m💀 ${bar} ${used}%\x1b[0m`;
43
+ }
44
+ }
45
+
46
+ // Current task from todos
47
+ let task = '';
48
+ const homeDir = os.homedir();
49
+ const todosDir = path.join(homeDir, '.claude', 'todos');
50
+ if (session && fs.existsSync(todosDir)) {
51
+ try {
52
+ const files = fs.readdirSync(todosDir)
53
+ .filter(f => f.startsWith(session) && f.includes('-agent-') && f.endsWith('.json'))
54
+ .map(f => ({ name: f, mtime: fs.statSync(path.join(todosDir, f)).mtime }))
55
+ .sort((a, b) => b.mtime - a.mtime);
56
+
57
+ if (files.length > 0) {
58
+ try {
59
+ const todos = JSON.parse(fs.readFileSync(path.join(todosDir, files[0].name), 'utf8'));
60
+ const inProgress = todos.find(t => t.status === 'in_progress');
61
+ if (inProgress) task = inProgress.activeForm || '';
62
+ } catch (e) {}
63
+ }
64
+ } catch (e) {
65
+ // Silently fail on file system errors - don't break statusline
66
+ }
67
+ }
68
+
69
+ // Declare update available?
70
+ let gsdUpdate = '';
71
+ const cacheFile = path.join(homeDir, '.claude', 'cache', 'declare-update-check.json');
72
+ if (fs.existsSync(cacheFile)) {
73
+ try {
74
+ const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
75
+ if (cache.update_available) {
76
+ gsdUpdate = '\x1b[33m⬆ /declare:update\x1b[0m │ ';
77
+ }
78
+ } catch (e) {}
79
+ }
80
+
81
+ // Output
82
+ const dirname = path.basename(dir);
83
+ if (task) {
84
+ process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[1m${task}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}`);
85
+ } else {
86
+ process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}`);
87
+ }
88
+ } catch (e) {
89
+ // Silent fail - don't break statusline on parse errors
90
+ }
91
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "declare-cc",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
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"
@@ -10,6 +10,7 @@
10
10
  "commands",
11
11
  "agents",
12
12
  "dist",
13
+ "hooks",
13
14
  "scripts",
14
15
  "workflows",
15
16
  "templates"