spectoflow 0.21.0 → 0.22.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/README.md +126 -76
- package/bin/spectoflow.js +30 -3
- package/package.json +1 -1
- package/templates/dashboard/files.js +108 -0
- package/templates/dashboard/public/app.js +280 -13
- package/templates/dashboard/public/designs/console.css +35 -9
- package/templates/dashboard/public/designs/console.js +37 -0
- package/templates/dashboard/public/i18n.js +24 -24
- package/templates/dashboard/public/icons.js +8 -6
- package/templates/dashboard/public/index.html +83 -32
- package/templates/dashboard/public/styles.css +66 -21
- package/templates/dashboard/server.js +29 -18
- package/templates/dashboard/summarize.js +25 -6
- package/templates/lib/store.js +43 -0
|
@@ -207,7 +207,7 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
207
207
|
.tflag.fail { color:var(--s-blocked); border:1px solid color-mix(in srgb,var(--s-blocked) 45%,var(--line)); }
|
|
208
208
|
|
|
209
209
|
/* Requests tab (tasks awaiting review: to_validate / to_analyze) */
|
|
210
|
-
.requests-wrap { padding:26px 28px 60px; max-width:
|
|
210
|
+
.requests-wrap { padding:26px 28px 60px; max-width:1400px; }
|
|
211
211
|
.requests-list { display:flex; flex-direction:column; gap:7px; }
|
|
212
212
|
.request-row { cursor:pointer; display:flex; align-items:center; gap:10px; background:var(--surface); border:1px solid var(--line); border-radius:10px; padding:9px 12px; font-size:13px; transition:border-color .15s; }
|
|
213
213
|
.request-row:hover,.request-row:focus-visible { border-color:color-mix(in srgb,var(--signal) 40%,var(--line)); }
|
|
@@ -220,8 +220,15 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
220
220
|
.placeholder-empty { margin-top:8px; padding:40px; border:1px dashed var(--line); border-radius:var(--radius); text-align:center; color:var(--faint); font-style:italic; }
|
|
221
221
|
|
|
222
222
|
/* Backlog tab — flat, sortable, filterable table of every task */
|
|
223
|
-
.backlog-wrap { padding:26px 28px 60px; max-width:
|
|
223
|
+
.backlog-wrap { padding:26px 28px 60px; max-width:1500px; }
|
|
224
224
|
.backlog-wrap .filters { padding:0 0 14px; }
|
|
225
|
+
.bl-add-form { display:none; align-items:center; gap:8px; flex-wrap:wrap; padding:0 0 14px; }
|
|
226
|
+
.bl-add-form.is-open { display:flex; }
|
|
227
|
+
.bl-add-input { font:inherit; font-size:12.5px; padding:6px 11px; border:1px solid var(--line); border-radius:8px; background:var(--surface); color:var(--ink); }
|
|
228
|
+
.bl-add-input:focus { outline:2px solid var(--signal); outline-offset:1px; }
|
|
229
|
+
.bl-add-title { flex:1; min-width:220px; }
|
|
230
|
+
.bl-add-phase,.bl-add-owner { width:150px; }
|
|
231
|
+
.bl-add-error { font-size:12px; color:var(--s-blocked); }
|
|
225
232
|
.table-scroll { width:100%; overflow-x:auto; border:1px solid var(--line); border-radius:var(--radius); }
|
|
226
233
|
.backlog-table { width:100%; min-width:720px; border-collapse:collapse; font-size:12.5px; }
|
|
227
234
|
.backlog-table thead th { position:sticky; top:0; z-index:1; background:var(--surface-2); text-align:left; padding:9px 12px; font-family:var(--mono); font-size:10.5px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); white-space:nowrap; cursor:pointer; user-select:none; border-bottom:1px solid var(--line); }
|
|
@@ -243,8 +250,12 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
243
250
|
@media (max-width:640px){ .backlog-wrap { padding:18px 14px 40px; } }
|
|
244
251
|
|
|
245
252
|
/* Info tab (project overview — config, runners, counts, specs, active workflow) */
|
|
246
|
-
.info-wrap { padding:26px 28px 60px; max-width:
|
|
247
|
-
|
|
253
|
+
.info-wrap { padding:26px 28px 60px; max-width:1500px; }
|
|
254
|
+
/* a fixed 2-column grid, not auto-fit: with 5 sections (2 + 1 full-span + 2), more columns would
|
|
255
|
+
leave the 2-item rows with genuinely empty trailing cells — the same dead space this was meant to
|
|
256
|
+
fix, just moved inside the grid. 2 columns always fill completely; the wider .info-wrap above is
|
|
257
|
+
what actually puts the freed width to use, by making each card roomier. */
|
|
258
|
+
.info-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:14px; margin-top:8px; align-items:start; }
|
|
248
259
|
.info-section { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:16px 18px; }
|
|
249
260
|
.info-section-head { display:flex; align-items:center; gap:8px; margin:0 0 12px; }
|
|
250
261
|
.info-section-ico { width:16px; height:16px; flex-shrink:0; display:inline-flex; align-items:center; justify-content:center; color:var(--cool); }
|
|
@@ -275,6 +286,43 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
275
286
|
/* workflow diagram */
|
|
276
287
|
.wf-wrap,.team-wrap { padding:26px 28px 60px; max-width:1000px; }
|
|
277
288
|
.wf-wrap { max-width:1340px; } /* wider so the horizontal pipeline fits its 9 steps on one line */
|
|
289
|
+
.team-wrap { max-width:1500px; } /* the agent/skill .cards grid fills more columns instead of sitting capped */
|
|
290
|
+
|
|
291
|
+
/* Files tab — a two-pane explorer: a collapsible tree, and the selected file's preview/editor */
|
|
292
|
+
.files-wrap { padding:26px 28px 60px; max-width:1600px; height:calc(100vh - 130px); display:flex; flex-direction:column; }
|
|
293
|
+
.files-body { flex:1; min-height:0; display:flex; gap:16px; margin-top:8px; }
|
|
294
|
+
.files-tree-col { width:280px; flex-shrink:0; display:flex; flex-direction:column; gap:8px; min-height:0; }
|
|
295
|
+
.files-toolbar { display:flex; gap:8px; }
|
|
296
|
+
.files-toolbar .btn { font-size:12px; padding:6px 11px; }
|
|
297
|
+
.files-create-form { display:none; flex-direction:column; gap:6px; padding:10px; border:1px solid var(--line); border-radius:var(--radius); background:var(--surface); }
|
|
298
|
+
.files-create-form.is-open { display:flex; }
|
|
299
|
+
.files-create-form .bl-add-input { width:100%; }
|
|
300
|
+
.files-create-actions { display:flex; gap:8px; }
|
|
301
|
+
.files-create-actions .btn { font-size:12px; padding:6px 11px; }
|
|
302
|
+
.files-tree { flex:1; min-height:0; overflow:auto; border:1px solid var(--line); border-radius:var(--radius); padding:8px; background:var(--surface); }
|
|
303
|
+
.files-content-col { flex:1; min-width:0; display:flex; flex-direction:column; min-height:0; border:1px solid var(--line); border-radius:var(--radius); background:var(--surface); overflow:hidden; }
|
|
304
|
+
.files-empty,.files-binary { padding:40px; text-align:center; color:var(--faint); font-style:italic; }
|
|
305
|
+
.f-row { display:flex; align-items:center; gap:6px; padding:4px 6px; border-radius:6px; cursor:pointer; font-size:12.5px; white-space:nowrap; color:var(--muted); }
|
|
306
|
+
.f-row:hover { background:var(--surface-2); color:var(--ink); }
|
|
307
|
+
.f-row.is-active { background:var(--surface-2); color:var(--signal); font-weight:600; }
|
|
308
|
+
.f-row .f-chevron { width:11px; height:11px; flex-shrink:0; transition:transform .15s; color:var(--faint); }
|
|
309
|
+
.f-row.is-open .f-chevron { transform:rotate(90deg); }
|
|
310
|
+
.f-row .f-name { overflow:hidden; text-overflow:ellipsis; }
|
|
311
|
+
.f-children { margin-left:13px; border-left:1px solid var(--line); padding-left:6px; }
|
|
312
|
+
.f-children[hidden] { display:none; }
|
|
313
|
+
.files-toolbar-row { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:10px 14px; border-bottom:1px solid var(--line); }
|
|
314
|
+
.files-path { font-family:var(--mono); font-size:12px; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
315
|
+
.files-actions { display:flex; gap:8px; flex-shrink:0; }
|
|
316
|
+
.files-actions .btn { font-size:12px; padding:6px 11px; }
|
|
317
|
+
.files-body-col { flex:1; min-height:0; display:flex; flex-direction:column; }
|
|
318
|
+
.files-view { flex:1; min-height:0; overflow:auto; padding:16px 20px; }
|
|
319
|
+
.files-editor { flex:1; min-height:0; width:100%; border:0; resize:none; padding:16px 20px; font-family:var(--mono); font-size:12.5px; line-height:1.6; background:var(--surface); color:var(--ink); outline:none; }
|
|
320
|
+
.files-iframe { flex:1; min-height:0; width:100%; border:0; background:#fff; }
|
|
321
|
+
.files-md-toolbar { display:flex; gap:6px; padding:8px 14px; border-bottom:1px solid var(--line); }
|
|
322
|
+
.files-md-toolbar button { font-family:var(--mono); font-size:12px; padding:4px 9px; border:1px solid var(--line); border-radius:6px; background:var(--surface-2); color:var(--ink); cursor:pointer; }
|
|
323
|
+
.files-saved-tip { font-size:11.5px; color:var(--s-done); font-family:var(--mono); }
|
|
324
|
+
.files-error-tip { font-size:11.5px; color:var(--s-blocked); }
|
|
325
|
+
@media (max-width:820px){ .files-body { flex-direction:column; } .files-tree-col { width:auto; max-height:220px; } .files-wrap { height:auto; } }
|
|
278
326
|
.wf-diagram { display:block; margin-top:8px; }
|
|
279
327
|
.wf-step { display:flex; align-items:center; }
|
|
280
328
|
.wf-node { display:flex; align-items:center; gap:8px; padding:11px 15px; border:1px solid var(--line); border-radius:var(--radius); background:var(--surface); cursor:pointer; transition:border-color .18s,transform .15s,box-shadow .15s; user-select:none; }
|
|
@@ -373,7 +421,7 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
373
421
|
.chat-fab.is-open { box-shadow:inset 0 0 0 2px var(--signal); }
|
|
374
422
|
.chat { position:fixed; right:20px; bottom:88px; z-index:18; width:min(420px,94vw); height:min(600px,78vh); background:var(--surface); border:1px solid var(--line); border-radius:calc(var(--radius) + 2px); box-shadow:var(--shadow); display:flex; flex-direction:column; overflow:hidden; transform-origin:bottom right; transition:opacity .16s,transform .16s; }
|
|
375
423
|
.chat[aria-hidden="true"] { opacity:0; transform:scale(.96) translateY(8px); pointer-events:none; }
|
|
376
|
-
.chat-head { display:flex; align-items:center; gap:10px; padding:14px 16px; border-bottom:1px solid var(--line); background:
|
|
424
|
+
.chat-head { display:flex; align-items:center; gap:10px; padding:14px 16px; border-bottom:1px solid var(--line); background:color-mix(in srgb,var(--surface-2) 40%,transparent); }
|
|
377
425
|
.chat-head-text { display:flex; flex-direction:column; gap:1px; min-width:0; }
|
|
378
426
|
.chat-title { font-weight:700; font-size:15px; }
|
|
379
427
|
.chat-sub { font-size:10px; color:var(--faint); text-transform:uppercase; letter-spacing:.04em; }
|
|
@@ -548,8 +596,10 @@ body.booting .ring-svg circle:last-of-type { transform-origin:center; animation:
|
|
|
548
596
|
.kpi.has-accent { border-left-width:3px; border-left-style:solid; }
|
|
549
597
|
|
|
550
598
|
/* Settings tab */
|
|
551
|
-
.settings-wrap { max-width:
|
|
552
|
-
.settings-
|
|
599
|
+
.settings-wrap { max-width:1120px; margin:0 auto; padding:20px 22px; }
|
|
600
|
+
.settings-groups { display:grid; grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); gap:16px; margin:14px 0; }
|
|
601
|
+
.settings-card { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:18px; display:flex; flex-direction:column; gap:16px; margin:0; }
|
|
602
|
+
.settings-card-title { margin:0; font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); }
|
|
553
603
|
.settings-field { display:flex; flex-direction:column; gap:6px; }
|
|
554
604
|
.settings-field-label { font-size:11px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
|
|
555
605
|
.settings-select { font-family:inherit; font-size:14px; padding:9px 11px; border:1px solid var(--line); border-radius:9px; background:var(--surface-2); color:var(--ink); cursor:pointer; }
|
|
@@ -575,7 +625,9 @@ body.booting .ring-svg circle:last-of-type { transform-origin:center; animation:
|
|
|
575
625
|
/* ---- Customize (Settings → dashboards/skills/agents) — reuses .card/.chat-ta/.btn styling so it
|
|
576
626
|
reads as part of the same system, not a bolted-on sub-app. ---- */
|
|
577
627
|
.cz-wrap { margin-top:28px; }
|
|
578
|
-
|
|
628
|
+
#czRoot { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:12px; margin-top:12px; }
|
|
629
|
+
#czRoot.has-open { grid-template-columns:1fr; } /* one block's form open — stack instead of leaving empty cells beside the other two */
|
|
630
|
+
.cz-block { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:14px 16px; margin-top:0; }
|
|
579
631
|
.cz-head { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
|
580
632
|
.cz-head h3 { margin:0; font-size:13.5px; font-weight:700; }
|
|
581
633
|
.cz-add { font-size:12.5px; padding:6px 13px; }
|
|
@@ -790,7 +842,7 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
|
|
|
790
842
|
/* Obsidian Ops fonts: JetBrains Mono (mono + display) + IBM Plex Sans (body) */
|
|
791
843
|
:root[data-design="obsidian"] { --sans:'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif; --mono:'JetBrains Mono', ui-monospace, monospace; --display:'JetBrains Mono', ui-monospace, monospace; }
|
|
792
844
|
|
|
793
|
-
/* --- Neon Command:
|
|
845
|
+
/* --- Neon Command: violet + cyan control-room, flat surfaces (no gradients/aurora glow) --- */
|
|
794
846
|
:root[data-design="neon-command"] {
|
|
795
847
|
--bg:#0d0b1a; --surface:#171233; --surface-2:#1f1940; --line:#2e2857;
|
|
796
848
|
--ink:#eae6ff; --muted:#a99fd6; --faint:#6f689e;
|
|
@@ -799,13 +851,7 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
|
|
|
799
851
|
--radius:16px; --shadow:0 18px 50px rgba(0,0,0,.55);
|
|
800
852
|
--sans:'Sora', system-ui, sans-serif; --display:'Space Grotesk', system-ui, sans-serif;
|
|
801
853
|
}
|
|
802
|
-
:root[data-design="neon-command"] body {
|
|
803
|
-
background:
|
|
804
|
-
radial-gradient(1200px 600px at 15% -5%, rgba(124,77,255,.22), transparent 60%),
|
|
805
|
-
radial-gradient(1000px 520px at 100% 8%, rgba(0,229,255,.14), transparent 55%),
|
|
806
|
-
var(--bg);
|
|
807
|
-
background-attachment:fixed;
|
|
808
|
-
}
|
|
854
|
+
:root[data-design="neon-command"] body { background:var(--bg); }
|
|
809
855
|
:root[data-design="neon-command"] .kpi,
|
|
810
856
|
:root[data-design="neon-command"] .ocard,
|
|
811
857
|
:root[data-design="neon-command"] .card,
|
|
@@ -814,14 +860,13 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
|
|
|
814
860
|
:root[data-design="neon-command"] .settings-readonly,
|
|
815
861
|
:root[data-design="neon-command"] .wf-pop,
|
|
816
862
|
:root[data-design="neon-command"] .approval {
|
|
817
|
-
background:
|
|
863
|
+
background:var(--surface);
|
|
818
864
|
border-color:color-mix(in srgb,var(--signal) 22%,var(--line));
|
|
819
|
-
backdrop-filter:blur(10px);
|
|
820
865
|
}
|
|
821
|
-
:root[data-design="neon-command"] .topbar { background:color-mix(in srgb,var(--bg)
|
|
866
|
+
:root[data-design="neon-command"] .topbar { background:color-mix(in srgb,var(--bg) 92%,transparent); border-bottom-color:color-mix(in srgb,var(--signal) 22%,var(--line)); }
|
|
822
867
|
:root[data-design="neon-command"] .btn.primary,
|
|
823
|
-
:root[data-design="neon-command"] .run-btn { background:
|
|
824
|
-
:root[data-design="neon-command"] .brand-name {
|
|
868
|
+
:root[data-design="neon-command"] .run-btn { background:var(--signal); color:#fff; border-color:transparent; }
|
|
869
|
+
:root[data-design="neon-command"] .brand-name { color:var(--signal); }
|
|
825
870
|
:root[data-design="neon-command"] .tab.is-active { background:color-mix(in srgb,var(--signal) 20%,transparent); box-shadow:inset 0 -2px 0 var(--cool); }
|
|
826
871
|
:root[data-design="neon-command"] .wf-circle { backdrop-filter:blur(6px); }
|
|
827
872
|
:root[data-design="neon-command"][data-theme="light"] {
|
|
@@ -13,6 +13,7 @@ const { startRun } = require('./runner');
|
|
|
13
13
|
const { runSummarize } = require('./summarize');
|
|
14
14
|
const orchestrator = require('./orchestrator');
|
|
15
15
|
const agentsRegistry = require('../lib/agents-registry');
|
|
16
|
+
const files = require('./files');
|
|
16
17
|
|
|
17
18
|
const PORT = process.env.SPECTOFLOW_PORT ? Number(process.env.SPECTOFLOW_PORT) : 4319;
|
|
18
19
|
const PUBLIC = path.join(__dirname, 'public');
|
|
@@ -71,26 +72,9 @@ function writeConfig(patch){
|
|
|
71
72
|
fs.writeFileSync(cp, JSON.stringify(cfg, null, 2) + '\n');
|
|
72
73
|
return cfg;
|
|
73
74
|
}
|
|
74
|
-
// Next free T-### id across every plan file (absolute paths from store.readPlans).
|
|
75
|
-
function nextTaskId(){
|
|
76
|
-
let max = 0;
|
|
77
|
-
for (const pl of store.readPlans(ROOT)) {
|
|
78
|
-
try { const t = fs.readFileSync(pl.file, 'utf8'); const re = /\bT-(\d+)/g; let m; while ((m = re.exec(t))) max = Math.max(max, Number(m[1])); } catch {}
|
|
79
|
-
}
|
|
80
|
-
return 'T-' + String(max + 1).padStart(3, '0');
|
|
81
|
-
}
|
|
82
75
|
// Promote an attention item into a real checkbox task under an `## Attention` phase.
|
|
83
76
|
function promoteAttention(item){
|
|
84
|
-
|
|
85
|
-
let file = plans[0] && plans[0].file;
|
|
86
|
-
if (!file) { file = path.join(ROOT, 'plans', 'inbox.md'); fs.mkdirSync(path.dirname(file), { recursive: true }); if (!fs.existsSync(file)) fs.writeFileSync(file, '# Inbox\n'); }
|
|
87
|
-
const id = nextTaskId();
|
|
88
|
-
let text = fs.readFileSync(file, 'utf8');
|
|
89
|
-
const line = `- [ ] ${id} ${String(item.text).replace(/\s+/g, ' ').trim()} @user ~standard`;
|
|
90
|
-
if (/^##\s+Attention\s*$/m.test(text)) text = text.replace(/^(##\s+Attention\s*)$/m, `$1\n${line}`);
|
|
91
|
-
else { if (!text.endsWith('\n')) text += '\n'; text += `\n## Attention\n${line}\n`; }
|
|
92
|
-
fs.writeFileSync(file, text);
|
|
93
|
-
return { id, file };
|
|
77
|
+
return store.addTask(ROOT, { phase: 'Attention', title: item.text, owner: 'user' });
|
|
94
78
|
}
|
|
95
79
|
|
|
96
80
|
function watch(dir){ try{ fs.watch(dir,{recursive:false},()=>emit({type:'change'})); }catch(_){} }
|
|
@@ -131,12 +115,39 @@ const server = http.createServer(async (req,res)=>{
|
|
|
131
115
|
return sendJSON(res, 200, { content: fs.readFileSync(real, 'utf8') });
|
|
132
116
|
}
|
|
133
117
|
|
|
118
|
+
// ---- File Explorer: browse/read/write/create anywhere under the project root ----
|
|
119
|
+
if (p === '/api/files/tree' && req.method === 'GET') return sendJSON(res, 200, { tree: files.tree(ROOT) });
|
|
120
|
+
if (p === '/api/files/read' && req.method === 'GET') {
|
|
121
|
+
const rel = new URL(req.url, 'http://x').searchParams.get('path') || '';
|
|
122
|
+
const r = files.readFile(ROOT, rel);
|
|
123
|
+
return sendJSON(res, r.error ? 400 : 200, r);
|
|
124
|
+
}
|
|
125
|
+
if (p === '/api/files/write' && req.method === 'POST') {
|
|
126
|
+
const { path: rel, content } = await body(req);
|
|
127
|
+
const r = files.writeFile(ROOT, rel, content);
|
|
128
|
+
if (r.error) return sendJSON(res, 400, r);
|
|
129
|
+
emit({ type: 'change' }); return sendJSON(res, 200, r);
|
|
130
|
+
}
|
|
131
|
+
if (p === '/api/files/mkdir' && req.method === 'POST') {
|
|
132
|
+
const { path: rel } = await body(req);
|
|
133
|
+
const r = files.mkdir(ROOT, rel);
|
|
134
|
+
if (r.error) return sendJSON(res, 400, r);
|
|
135
|
+
emit({ type: 'change' }); return sendJSON(res, 200, r);
|
|
136
|
+
}
|
|
137
|
+
|
|
134
138
|
if(p==='/api/events'){
|
|
135
139
|
res.writeHead(200,{'Content-Type':'text/event-stream','Cache-Control':'no-cache',Connection:'keep-alive'});
|
|
136
140
|
res.write('data: '+JSON.stringify({type:'hello'})+'\n\n');
|
|
137
141
|
clients.add(res); req.on('close',()=>clients.delete(res)); return;
|
|
138
142
|
}
|
|
139
143
|
|
|
144
|
+
// ---- manual task creation: add a checkbox task straight to a plan, no agent involved ----
|
|
145
|
+
if(p==='/api/task'&&req.method==='POST'){
|
|
146
|
+
const { title, phase, file, owner, level } = await body(req);
|
|
147
|
+
if(!title||!String(title).trim()) return sendJSON(res,400,{error:'A title is required.'});
|
|
148
|
+
const t = store.addTask(ROOT,{ title:String(title).trim(), phase, file, owner, level });
|
|
149
|
+
emit({type:'change'}); return sendJSON(res,200,{task:t});
|
|
150
|
+
}
|
|
140
151
|
if(p.startsWith('/api/task/')&&req.method==='PATCH'){
|
|
141
152
|
const id=decodeURIComponent(p.split('/')[3]||''); const patch=await body(req);
|
|
142
153
|
const file=findPlanFileForTask(id); if(!file) return sendJSON(res,404,{error:`Task ${id} not found.`});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
/*
|
|
3
|
-
* Chat "Summarize" — condenses the
|
|
3
|
+
* Chat "Summarize" — condenses the group-chat log down to one digest message, via the same
|
|
4
4
|
* configured agent runner (kept separate from runner.js since it captures the child's raw stdout as
|
|
5
5
|
* one summary, not sentinel-parsed lines; unit-testable without an HTTP server, same as runner.js).
|
|
6
|
+
* Deliberately REPLACES runtime.messages rather than appending: a digest that leaves the full history
|
|
7
|
+
* sitting right below it doesn't condense anything — it just adds noise on top of noise.
|
|
6
8
|
*/
|
|
7
9
|
const { spawn } = require('child_process');
|
|
8
10
|
const store = require('../lib/store');
|
|
@@ -16,8 +18,9 @@ function formatLog(messages, limit = DEFAULT_LIMIT) {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
// Summarizes runtime.messages (excluding prior summaries, so re-summarizing doesn't compound) into
|
|
19
|
-
// one new message of kind 'summary'. Returns { child } on success or { error } —
|
|
20
|
-
// shape, without the sentinel-line streaming runner.js does (this is a one-shot
|
|
21
|
+
// one new message of kind 'summary', REPLACING the log. Returns { child } on success or { error } —
|
|
22
|
+
// mirrors startRun's shape, without the sentinel-line streaming runner.js does (this is a one-shot
|
|
23
|
+
// digest, not a task run).
|
|
21
24
|
function runSummarize(root, { agent } = {}, emit) {
|
|
22
25
|
const cfg = store.readConfig(root);
|
|
23
26
|
const which = agent || cfg.agent || 'claude';
|
|
@@ -27,9 +30,11 @@ function runSummarize(root, { agent } = {}, emit) {
|
|
|
27
30
|
const rt = store.readRuntime(root);
|
|
28
31
|
const messages = (rt.messages || []).filter((m) => m.kind !== 'summary');
|
|
29
32
|
if (!messages.length) return { error: 'Nothing to summarize yet.' };
|
|
33
|
+
const summarizedIds = new Set(messages.map((m) => m.id));
|
|
30
34
|
|
|
31
35
|
const prompt = 'Summarize this project\'s recent activity log in 3-6 concise bullet points — '
|
|
32
|
-
+ 'what was built, decided, or is blocked. Reply with the summary only
|
|
36
|
+
+ 'what was built, decided, or is blocked. Reply with the summary text only: no preamble, no '
|
|
37
|
+
+ 'sentinel lines, and do not read, search or edit any files — answer using only the log below.'
|
|
33
38
|
+ '\n\n' + formatLog(messages);
|
|
34
39
|
|
|
35
40
|
const parts = cmdStr.split(/\s+/).filter(Boolean);
|
|
@@ -43,8 +48,22 @@ function runSummarize(root, { agent } = {}, emit) {
|
|
|
43
48
|
child.stderr && child.stderr.on('data', (d) => { out += d.toString(); });
|
|
44
49
|
child.on('close', (code) => {
|
|
45
50
|
const text = out.trim() || (code === 0 ? '(no output)' : `summarize failed (exit ${code})`);
|
|
46
|
-
const
|
|
47
|
-
|
|
51
|
+
const summary = {
|
|
52
|
+
id: 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
53
|
+
at: new Date().toISOString(),
|
|
54
|
+
kind: 'summary',
|
|
55
|
+
role: which,
|
|
56
|
+
agent: which,
|
|
57
|
+
text,
|
|
58
|
+
};
|
|
59
|
+
// Re-read fresh (not the earlier snapshot): drop exactly the messages that went INTO this
|
|
60
|
+
// summary, keep anything that arrived while the agent was running — condensing the log must
|
|
61
|
+
// never silently lose a message someone sent in the meantime.
|
|
62
|
+
const fresh = store.readRuntime(root);
|
|
63
|
+
fresh.messages = (fresh.messages || []).filter((m) => !summarizedIds.has(m.id));
|
|
64
|
+
fresh.messages.push(summary);
|
|
65
|
+
store.writeRuntime(root, fresh);
|
|
66
|
+
if (emit) { emit({ type: 'message', message: summary }); emit({ type: 'change' }); }
|
|
48
67
|
});
|
|
49
68
|
return { child };
|
|
50
69
|
}
|
package/templates/lib/store.js
CHANGED
|
@@ -139,6 +139,48 @@ function addTaskComment(projectRoot, file, id, text, author) {
|
|
|
139
139
|
return false;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// Next free T-### id across every plan file — scans for the highest existing T-<n> and returns n+1.
|
|
143
|
+
function nextTaskId(projectRoot) {
|
|
144
|
+
let max = 0;
|
|
145
|
+
for (const pl of readPlans(projectRoot)) {
|
|
146
|
+
for (const ph of pl.phases) {
|
|
147
|
+
for (const t of ph.tasks) {
|
|
148
|
+
const m = /^T-(\d+)$/.exec(t.id);
|
|
149
|
+
if (m) max = Math.max(max, Number(m[1]));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return 'T-' + String(max + 1).padStart(3, '0');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Add a new checkbox task, generating its id. `file` (a basename from readPlans, e.g. "0001-x.md")
|
|
157
|
+
// picks which plan; when omitted, the first existing plan file is used, or `plans/inbox.md` is
|
|
158
|
+
// created if the project has no plans yet. `phase` is the "## " heading the task is filed under —
|
|
159
|
+
// created at the end of the file if it doesn't already exist. Returns { id, file }.
|
|
160
|
+
function addTask(projectRoot, { file, phase, title, owner, level, status } = {}) {
|
|
161
|
+
const dirName = resolvePlansDir(projectRoot, readConfig(projectRoot));
|
|
162
|
+
const dir = path.join(projectRoot, dirName);
|
|
163
|
+
const plans = readPlans(projectRoot);
|
|
164
|
+
let fp;
|
|
165
|
+
if (file && plans.some((pl) => pl.file === file)) fp = path.join(dir, file);
|
|
166
|
+
else if (plans[0]) fp = path.join(dir, plans[0].file);
|
|
167
|
+
else { fs.mkdirSync(dir, { recursive: true }); fp = path.join(dir, 'inbox.md'); if (!fs.existsSync(fp)) writeAtomic(fp, '# Inbox\n'); }
|
|
168
|
+
|
|
169
|
+
const id = nextTaskId(projectRoot);
|
|
170
|
+
const phaseName = (phase && String(phase).trim()) || 'Backlog';
|
|
171
|
+
const line = buildTaskLine({
|
|
172
|
+
indent: '', id, title: String(title).replace(/\s+/g, ' ').trim(),
|
|
173
|
+
owner: (owner && String(owner).trim()) || null, level: level || 'standard', status: status || 'todo',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
let text = fs.readFileSync(fp, 'utf8');
|
|
177
|
+
const headingRe = new RegExp(`^##\\s+${phaseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm');
|
|
178
|
+
if (headingRe.test(text)) text = text.replace(headingRe, (m) => `${m}\n${line}`);
|
|
179
|
+
else { if (!text.endsWith('\n')) text += '\n'; text += `\n## ${phaseName}\n${line}\n`; }
|
|
180
|
+
writeAtomic(fp, text);
|
|
181
|
+
return { id, file: path.basename(fp) };
|
|
182
|
+
}
|
|
183
|
+
|
|
142
184
|
function writeAtomic(fp, content) {
|
|
143
185
|
const tmp = fp + '.tmp';
|
|
144
186
|
fs.writeFileSync(tmp, content, 'utf8');
|
|
@@ -331,6 +373,7 @@ function readSkills(projectRoot) {
|
|
|
331
373
|
|
|
332
374
|
module.exports = {
|
|
333
375
|
parseTaskLine, buildTaskLine, parsePlan, readPlans, readSpecs, updateTaskLine, addTaskComment,
|
|
376
|
+
nextTaskId, addTask,
|
|
334
377
|
readRuntime, writeRuntime, parseAgentLine, appendMessage, readConfig, readWorkflow, readProject,
|
|
335
378
|
readAgents, readSkills, readCustomDashboards, recordSnapshot, resolvePlansDir, resolveSpecsDir,
|
|
336
379
|
};
|