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 +29 -6
- package/commands/declare/execute.md +17 -14
- package/dist/declare-tools.cjs +224 -8
- package/dist/public/app.js +274 -4
- package/dist/public/index.html +91 -0
- package/hooks/declare-activity.js +94 -0
- package/hooks/declare-check-update.js +62 -0
- package/hooks/declare-statusline.js +91 -0
- package/package.json +2 -1
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
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
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.
|
|
191
|
+
**3e. Propagate statuses after each wave:**
|
|
192
192
|
|
|
193
|
-
After successful wave verification, update
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
|
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
|
|
package/dist/declare-tools.cjs
CHANGED
|
@@ -1329,7 +1329,7 @@ var require_help = __commonJS({
|
|
|
1329
1329
|
usage: "/declare:help"
|
|
1330
1330
|
}
|
|
1331
1331
|
],
|
|
1332
|
-
version: "0.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
|
|
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 (
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
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));
|
package/dist/public/app.js
CHANGED
|
@@ -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'
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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();
|
package/dist/public/index.html
CHANGED
|
@@ -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.
|
|
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"
|