spectoflow 0.30.0 → 0.31.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 +23 -0
- package/bin/spectoflow.js +22 -2
- package/lib/adapters.js +12 -0
- package/lib/dashboard/ops.js +16 -0
- package/lib/dashboard/public/app.js +54 -0
- package/lib/dashboard/public/i18n.js +6 -0
- package/lib/dashboard/public/index.html +11 -0
- package/lib/dashboard/public/styles.css +25 -0
- package/lib/dashboard/routes.js +2 -0
- package/lib/init.js +23 -0
- package/lib/workflow-detect.js +152 -0
- package/package.json +1 -1
- package/templates/SPECTOFLOW.md +22 -1
- package/templates/config.json +1 -0
package/README.md
CHANGED
|
@@ -101,6 +101,7 @@ spectoflow list agents, skills and the workflow a
|
|
|
101
101
|
spectoflow agents list the team personas
|
|
102
102
|
spectoflow skills list the procedures
|
|
103
103
|
spectoflow workflow show the enabled pipeline steps
|
|
104
|
+
spectoflow workflow suggest [--apply] which steps fit the project now (code yet? tests? E2E? infra/data?)
|
|
104
105
|
|
|
105
106
|
spectoflow --version (-v) print the version
|
|
106
107
|
spectoflow --help (-h) show help (append -h to any command for its help)
|
|
@@ -280,6 +281,8 @@ off by default):
|
|
|
280
281
|
noted yourself — edit / resolve / delete, or **validate → task**.
|
|
281
282
|
- **Backlog** — a flat sortable/filterable, paginated table of every task, defaulting to open work.
|
|
282
283
|
- **Workflow** — the pipeline as step cards; click one to enable/disable it, which edits `workflow.md`.
|
|
284
|
+
**Analyze the project** proposes the steps that fit it now, each with its reason — you apply the ones you
|
|
285
|
+
tick (see [A workflow that fits the project](#a-workflow-that-fits-the-project)).
|
|
283
286
|
- **Agents & Skills** — enriched cards that open a full-body markdown drawer.
|
|
284
287
|
- **Files** — a project file tree with read / write / create, syntax-highlighted (self-hosted Prism.js).
|
|
285
288
|
- **Bloc note** *(off by default)* — a per-project post-it Markdown scratchpad.
|
|
@@ -398,6 +401,26 @@ agents then read and grow your second brain through that server: nothing is copi
|
|
|
398
401
|
A run started from either of those can't write into it, and a learned fact never goes into a project's chat
|
|
399
402
|
log.
|
|
400
403
|
|
|
404
|
+
## A workflow that fits the project
|
|
405
|
+
|
|
406
|
+
The workflow (`.spectoflow/workflow.md`) isn't one-size-fits-all anymore.
|
|
407
|
+
|
|
408
|
+
- **At `init`**, spectoflow looks at the files: is there code yet, tests, an end-to-end setup (Playwright,
|
|
409
|
+
Cypress), infrastructure (Terraform, Helm) or data (dbt, notebooks)? A project still in design gets
|
|
410
|
+
Brainstorm, Analysis, Spec and Plan — and *Develop*, *Unit tests* and *Review* stay off until there is code.
|
|
411
|
+
Integration and end-to-end tests are switched on only if the project already has them. `init` tells you
|
|
412
|
+
what it switched off and why. An existing `workflow.md` is never touched.
|
|
413
|
+
- **At the first session**, your agent reviews it with you: a file scan can't tell a prototype from a
|
|
414
|
+
product, the agent can, from your docs, specs and plans.
|
|
415
|
+
- **As the project moves on**, when a request needs a step that is off (asked to write code while *Develop*
|
|
416
|
+
is off…), the agent asks you first — or, with Personalize → *Let the agent enable workflow steps when
|
|
417
|
+
needed* (`workflowAutoEnable` in `config.json`), it enables the step itself and tells you. It never
|
|
418
|
+
disables a step on its own.
|
|
419
|
+
- **For a project initialized before this**, run `spectoflow workflow suggest` (add `--apply` to apply), or
|
|
420
|
+
use Workflow → **Analyze the project** in the dashboard and tick the changes you want.
|
|
421
|
+
|
|
422
|
+
You can still switch any step on or off by hand, anytime.
|
|
423
|
+
|
|
401
424
|
## Agents vs skills
|
|
402
425
|
|
|
403
426
|
Agents (`.spectoflow/agents/`) are **stable team personas** (Product Manager, Developer, QA Engineer…).
|
package/bin/spectoflow.js
CHANGED
|
@@ -374,6 +374,26 @@ function configCmd() {
|
|
|
374
374
|
} catch (e) { console.log(`${c.y('!')} ${e.message}`); process.exitCode = 1; }
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
// ---- workflow: show it, or suggest the steps that fit the project now (D75) ----
|
|
378
|
+
function workflowCmd() {
|
|
379
|
+
if (argv[1] !== 'suggest') { console.log(wordmark()); printWorkflow(false); return; }
|
|
380
|
+
const root = process.cwd();
|
|
381
|
+
if (!fs.existsSync(path.join(root, '.spectoflow', 'workflow.md'))) { console.log('No spectoflow project here. Run: spectoflow init'); process.exitCode = 1; return; }
|
|
382
|
+
const wd = require('../lib/workflow-detect');
|
|
383
|
+
const cfg = store.readConfig(root);
|
|
384
|
+
const s = wd.suggest(root, { projectType: cfg.projectType });
|
|
385
|
+
const apply = argv.includes('--apply');
|
|
386
|
+
console.log(wordmark());
|
|
387
|
+
console.log(` ${c.bold('spectoflow workflow suggest')}${apply ? c.dim(' (apply)') : ''}`);
|
|
388
|
+
console.log(` ${c.dim('detected:')} ${s.projectType} project · ${s.phase === 'design' ? 'design phase (no code yet)' : 'has code'}${s.truncated ? c.dim(' (large project: scan stopped early)') : ''}\n`);
|
|
389
|
+
if (!s.changes.length) { console.log(` ${c.g('✓')} The workflow already matches the project.\n`); return; }
|
|
390
|
+
const w = Math.max(...s.changes.map((ch) => ch.name.length));
|
|
391
|
+
for (const ch of s.changes) console.log(` ${ch.to ? c.g('●') : c.dim('○')} ${ch.name.padEnd(w)} ${ch.from ? 'on' : 'off'} ${c.amber('→')} ${ch.to ? c.g('on ') : c.y('off')} ${c.dim(wd.REASONS[ch.reason])}`);
|
|
392
|
+
if (!apply) { console.log(`\n ${c.dim('apply them:')} ${c.g('spectoflow workflow suggest --apply')} ${c.dim('— or pick in the dashboard: Workflow → Analyze the project')}\n`); return; }
|
|
393
|
+
const changed = wd.applySteps(root, Object.fromEntries(s.changes.map((ch) => [ch.name, ch.to])));
|
|
394
|
+
console.log(`\n ${c.g('✓')} ${changed.length} step(s) updated in .spectoflow/workflow.md\n`);
|
|
395
|
+
}
|
|
396
|
+
|
|
377
397
|
// ---- brain: the user's second brain (~/.spectoflow/brain.md), shared by every project ----
|
|
378
398
|
function brainCmd() {
|
|
379
399
|
const brain = require('../lib/brain');
|
|
@@ -699,7 +719,7 @@ const HELP = {
|
|
|
699
719
|
(or the bundled kit when run outside a project) at a glance.`,
|
|
700
720
|
agents: `${c.bold('spectoflow agents')}\n List the stable team personas (name · capability · role).`,
|
|
701
721
|
skills: `${c.bold('spectoflow skills')}\n List the evolving procedures (name · capability · what it does).`,
|
|
702
|
-
workflow: `${c.bold('spectoflow workflow')}\n Show the pipeline steps, marking which are enabled (●) or disabled (○)
|
|
722
|
+
workflow: `${c.bold('spectoflow workflow')} ${c.dim('[suggest [--apply]]')}\n Show the pipeline steps, marking which are enabled (●) or disabled (○).\n ${c.g('suggest')} look at the project (is there code yet? tests? an E2E setup? infra or data?) and\n list the steps worth switching on or off, with the reason — nothing is written\n ${c.g('suggest --apply')} apply them (only those lines of .spectoflow/workflow.md change)`,
|
|
703
723
|
brain: `${c.bold('spectoflow brain')} ${c.dim('[setup [--dry-run]]')}\n
|
|
704
724
|
Your second brain — what spectoflow has learned about you (profile, preferences, working style,
|
|
705
725
|
things to avoid), in ${c.dim('~/.spectoflow/brain.md')}, shared by all your projects and editable in the
|
|
@@ -727,7 +747,7 @@ const fns = {
|
|
|
727
747
|
brain: brainCmd,
|
|
728
748
|
agents: () => { console.log(wordmark()); printAgents(false); },
|
|
729
749
|
skills: () => { console.log(wordmark()); printSkills(false); },
|
|
730
|
-
workflow:
|
|
750
|
+
workflow: workflowCmd,
|
|
731
751
|
skill: () => runCustomize('skill'),
|
|
732
752
|
agent: () => runCustomize('agent'),
|
|
733
753
|
};
|
package/lib/adapters.js
CHANGED
|
@@ -30,6 +30,10 @@ learn one (a stated preference, a correction of how you work, their role), recor
|
|
|
30
30
|
\`::spectoflow learn category=<profile|preferences|workflow|avoid> msg=<fact>\` if that tool is unavailable. Never
|
|
31
31
|
secrets or sensitive data.
|
|
32
32
|
|
|
33
|
+
**Workflow.** If a request needs a step that is disabled in \`.spectoflow/workflow.md\` (e.g. writing code while
|
|
34
|
+
*Develop* is off): enable it yourself and say so when \`.spectoflow/config.json\` → \`workflowAutoEnable\` is \`true\`;
|
|
35
|
+
otherwise ask first. Never disable a step without the user.
|
|
36
|
+
|
|
33
37
|
- Command: \`/spectoflow\` (\`init\` / \`status\` / or just a request).
|
|
34
38
|
- Dashboard: \`spectoflow dashboard\` → http://localhost:4319
|
|
35
39
|
- Artifacts are markdown in \`specs/\` and \`plans/\`; volatile state in \`.spectoflow/runtime.json\`.
|
|
@@ -48,6 +52,10 @@ is clear — then execute. See the Clarify reflex in \`.spectoflow/SPECTOFLOW.md
|
|
|
48
52
|
learn one (a stated preference, a correction of how you work, their role), record it with \`brain_learn\`, or print
|
|
49
53
|
\`::spectoflow learn category=<profile|preferences|workflow|avoid> msg=<fact>\` if that tool is unavailable. Never
|
|
50
54
|
secrets or sensitive data.
|
|
55
|
+
|
|
56
|
+
**Workflow.** If a request needs a step that is disabled in \`.spectoflow/workflow.md\` (e.g. writing code while
|
|
57
|
+
*Develop* is off): enable it yourself and say so when \`.spectoflow/config.json\` → \`workflowAutoEnable\` is \`true\`;
|
|
58
|
+
otherwise ask first. Never disable a step without the user.
|
|
51
59
|
`;
|
|
52
60
|
|
|
53
61
|
const GEMINI_MD = `# GEMINI.md — spectoflow
|
|
@@ -64,6 +72,10 @@ is clear — then execute. See the Clarify reflex in \`.spectoflow/SPECTOFLOW.md
|
|
|
64
72
|
learn one (a stated preference, a correction of how you work, their role), record it with \`brain_learn\`, or print
|
|
65
73
|
\`::spectoflow learn category=<profile|preferences|workflow|avoid> msg=<fact>\` if that tool is unavailable. Never
|
|
66
74
|
secrets or sensitive data.
|
|
75
|
+
|
|
76
|
+
**Workflow.** If a request needs a step that is disabled in \`.spectoflow/workflow.md\` (e.g. writing code while
|
|
77
|
+
*Develop* is off): enable it yourself and say so when \`.spectoflow/config.json\` → \`workflowAutoEnable\` is \`true\`;
|
|
78
|
+
otherwise ask first. Never disable a step without the user.
|
|
67
79
|
`;
|
|
68
80
|
|
|
69
81
|
const SLASH_CMD = `---
|
package/lib/dashboard/ops.js
CHANGED
|
@@ -20,6 +20,7 @@ const detect = require('../detect');
|
|
|
20
20
|
const brain = require('../brain');
|
|
21
21
|
const brainSetup = require('../brain-setup');
|
|
22
22
|
const globalConfig = require('../global-config');
|
|
23
|
+
const workflowDetect = require('../workflow-detect');
|
|
23
24
|
|
|
24
25
|
const PKG_VERSION = require('../../package.json').version;
|
|
25
26
|
|
|
@@ -71,6 +72,7 @@ function writeConfig(root, patch, detectOpts) {
|
|
|
71
72
|
if (Array.isArray(patch.expandedPhases)) cfg.expandedPhases = patch.expandedPhases.filter((v) => typeof v === 'string');
|
|
72
73
|
if (typeof patch.activeTab === 'string' && patch.activeTab.trim()) cfg.activeTab = patch.activeTab.trim();
|
|
73
74
|
if (typeof patch.chatOpen === 'boolean') cfg.chatOpen = patch.chatOpen;
|
|
75
|
+
if (typeof patch.workflowAutoEnable === 'boolean') cfg.workflowAutoEnable = patch.workflowAutoEnable;
|
|
74
76
|
// kanbanColumns: reject the whole patch (leave the current value untouched) rather than silently
|
|
75
77
|
// filtering out bad entries — an invalid/unknown status id here means the client sent something it
|
|
76
78
|
// shouldn't have, and a real product-safety rule (never persist zero visible columns) applies too.
|
|
@@ -200,6 +202,20 @@ const ops = {
|
|
|
200
202
|
return changed(ctx, { ok: true });
|
|
201
203
|
},
|
|
202
204
|
|
|
205
|
+
// Re-analysis (D75): what would fit the project now, and applying the steps the user picked. Apply
|
|
206
|
+
// recomputes the suggestion server-side and writes only those of the listed names it still contains.
|
|
207
|
+
'workflow.suggest': async (root) => {
|
|
208
|
+
const s = workflowDetect.suggest(root, { projectType: store.readConfig(root).projectType });
|
|
209
|
+
return { ...s, reasons: workflowDetect.REASONS };
|
|
210
|
+
},
|
|
211
|
+
'workflow.apply': async (root, { names }, ctx) => {
|
|
212
|
+
if (!Array.isArray(names) || !names.every((n) => typeof n === 'string')) bad('names must be a list of step names.');
|
|
213
|
+
const s = workflowDetect.suggest(root);
|
|
214
|
+
const picked = s.changes.filter((c) => names.includes(c.name));
|
|
215
|
+
const written = workflowDetect.applySteps(root, Object.fromEntries(picked.map((c) => [c.name, c.to])));
|
|
216
|
+
return written.length ? changed(ctx, { changed: written }) : { changed: written };
|
|
217
|
+
},
|
|
218
|
+
|
|
203
219
|
'run.start': async (root, { prompt, agent, display }, ctx) => {
|
|
204
220
|
text(prompt, 'Empty request.');
|
|
205
221
|
const r = startRun(root, { prompt, agent, display, learn: !ctx.remote }, ctx.emit);
|
|
@@ -1009,6 +1009,57 @@ function renderWorkflow(){
|
|
|
1009
1009
|
box.append(pipe);
|
|
1010
1010
|
if(wfPopStep) renderWfPop(); // re-anchor / refresh an open popover after a live re-render
|
|
1011
1011
|
}
|
|
1012
|
+
// ---- Workflow → Analyze the project (D75): what the file analysis would switch on or off now. Nothing is
|
|
1013
|
+
// written until the user applies the ticked changes. Lives outside #wfDiagram so SSE re-renders keep it open.
|
|
1014
|
+
let wfSuggestion=null, wfSuggestPicked=null, wfSuggestNote='';
|
|
1015
|
+
async function analyzeWorkflow(){
|
|
1016
|
+
const box=$('#wfSuggest'); if(!box) return;
|
|
1017
|
+
box.hidden=false; box.innerHTML=''; box.append(el('div','empty',t('drawer.loading')));
|
|
1018
|
+
try{
|
|
1019
|
+
const r=await fetch(withProject('/api/workflow/suggest'));
|
|
1020
|
+
const d=await r.json().catch(()=>({}));
|
|
1021
|
+
if(!r.ok) throw new Error(d.error||'error');
|
|
1022
|
+
wfSuggestion=d; wfSuggestPicked=new Set(d.changes.map(c=>c.name)); wfSuggestNote='';
|
|
1023
|
+
}catch(err){ wfSuggestion=null; wfSuggestNote=t('workflow.analyzeError'); }
|
|
1024
|
+
renderWfSuggest();
|
|
1025
|
+
}
|
|
1026
|
+
function closeWfSuggest(){ wfSuggestion=null; wfSuggestNote=''; const box=$('#wfSuggest'); if(box){ box.hidden=true; box.innerHTML=''; } }
|
|
1027
|
+
function renderWfSuggest(){
|
|
1028
|
+
const box=$('#wfSuggest'); if(!box) return;
|
|
1029
|
+
box.innerHTML='';
|
|
1030
|
+
if(!wfSuggestion){ if(wfSuggestNote){ box.hidden=false; box.append(el('div','wf-suggest-note',wfSuggestNote)); } return; }
|
|
1031
|
+
const d=wfSuggestion; box.hidden=false;
|
|
1032
|
+
const head=el('div','wf-suggest-head');
|
|
1033
|
+
head.append(el('span','wf-suggest-detected',t('workflow.detected',{type:t('workflow.type.'+d.projectType),phase:t('workflow.phase.'+d.phase)})));
|
|
1034
|
+
const x=el('button','wf-suggest-close','×'); x.type='button'; x.title=t('workflow.dismiss'); x.setAttribute('aria-label',t('workflow.dismiss')); x.addEventListener('click',closeWfSuggest);
|
|
1035
|
+
head.append(x); box.append(head);
|
|
1036
|
+
if(wfSuggestNote) box.append(el('div','wf-suggest-note',wfSuggestNote));
|
|
1037
|
+
if(!d.changes.length){ box.append(el('div','wf-suggest-ok',t('workflow.matches'))); return; }
|
|
1038
|
+
const list=el('div','wf-suggest-list');
|
|
1039
|
+
d.changes.forEach(c=>{
|
|
1040
|
+
const row=el('label','wf-suggest-row');
|
|
1041
|
+
const cb=el('input'); cb.type='checkbox'; cb.checked=wfSuggestPicked.has(c.name);
|
|
1042
|
+
cb.addEventListener('change',()=>{ if(cb.checked) wfSuggestPicked.add(c.name); else wfSuggestPicked.delete(c.name); applyBtn.disabled=!wfSuggestPicked.size; });
|
|
1043
|
+
const change=el('span','wf-suggest-change '+(c.to?'is-on':'is-off'),`${c.from?t('workflow.on'):t('workflow.off')} → ${c.to?t('workflow.on'):t('workflow.off')}`);
|
|
1044
|
+
row.append(cb, el('span','wf-suggest-name',c.name), change, el('span','wf-suggest-reason',t('workflow.reason.'+c.reason)));
|
|
1045
|
+
list.append(row);
|
|
1046
|
+
});
|
|
1047
|
+
box.append(list);
|
|
1048
|
+
const acts=el('div','wf-suggest-actions');
|
|
1049
|
+
const applyBtn=el('button','btn primary',t('workflow.apply')); applyBtn.type='button'; applyBtn.disabled=!wfSuggestPicked.size;
|
|
1050
|
+
applyBtn.addEventListener('click',async()=>{
|
|
1051
|
+
applyBtn.disabled=true; flash();
|
|
1052
|
+
try{
|
|
1053
|
+
const r=await fetch(withProject('/api/workflow/apply'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({names:[...wfSuggestPicked]})});
|
|
1054
|
+
const res=await r.json().catch(()=>({}));
|
|
1055
|
+
if(!r.ok) throw new Error(res.error||'error');
|
|
1056
|
+
wfSuggestion=null; wfSuggestNote=t('workflow.applied',{n:(res.changed||[]).length});
|
|
1057
|
+
}catch(err){ wfSuggestNote=t('workflow.analyzeError'); }
|
|
1058
|
+
renderWfSuggest(); scheduleLoad();
|
|
1059
|
+
});
|
|
1060
|
+
const cancel=el('button','btn',t('workflow.dismiss')); cancel.type='button'; cancel.addEventListener('click',closeWfSuggest);
|
|
1061
|
+
acts.append(applyBtn,cancel); box.append(acts);
|
|
1062
|
+
}
|
|
1012
1063
|
function wfDetailRow(k,v){ const r=el('div','wf-detail-row'); r.append(el('span','wf-detail-k',k), el('span','wf-detail-v',v)); return r; }
|
|
1013
1064
|
function wfPopFill(pop, s, idx){
|
|
1014
1065
|
const skill=(P.skills||[]).find(x=>x.name===s.skill);
|
|
@@ -1229,6 +1280,7 @@ function renderSettings(){
|
|
|
1229
1280
|
setModeSelects(c.mode||'semi');
|
|
1230
1281
|
setLangSelect(c.language||'en');
|
|
1231
1282
|
setAgentSelects();
|
|
1283
|
+
const wfAuto=$('#setWorkflowAuto'); if(wfAuto) wfAuto.checked=!!c.workflowAutoEnable;
|
|
1232
1284
|
// design switcher — options from the DESIGNS registry (designs.js)
|
|
1233
1285
|
const dsel=$('#setDesign');
|
|
1234
1286
|
if(dsel){
|
|
@@ -2558,6 +2610,8 @@ $('#blAddCancel').addEventListener('click', closeBacklogAddForm);
|
|
|
2558
2610
|
$('#blAddSubmit').addEventListener('click', submitBacklogAdd);
|
|
2559
2611
|
$('#blAddTitle').addEventListener('keydown', e=>{ if(e.key==='Enter') submitBacklogAdd(); });
|
|
2560
2612
|
// attention tab: add a note + filter chips
|
|
2613
|
+
$('#wfAnalyzeBtn').addEventListener('click',analyzeWorkflow);
|
|
2614
|
+
$('#setWorkflowAuto').addEventListener('change',(e)=>{ if(P&&P.config) P.config.workflowAutoEnable=e.target.checked; saveSetting({workflowAutoEnable:e.target.checked}); });
|
|
2561
2615
|
$('#brainAutoAdd').addEventListener('change',(e)=>brainAct(()=>brainCall('POST','/api/brain/settings',{autoAdd:e.target.checked})));
|
|
2562
2616
|
$('#attnAddBtn').addEventListener('click',()=>{ const t=$('#attnInput'); const v=t.value.trim(); if(v){ addAttn(v); t.value=''; } });
|
|
2563
2617
|
$('#attnInput').addEventListener('keydown',e=>{ if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){ const v=e.target.value.trim(); if(v){ addAttn(v); e.target.value=''; } } });
|
|
@@ -90,6 +90,7 @@ en: {
|
|
|
90
90
|
'drawer.agent':'Agent','drawer.skill':'Skill','drawer.file':'File · {rel}',
|
|
91
91
|
'drawer.loading':'Loading…','drawer.loadError':'Could not load this file.','files.title':'Files','files.sub':'Browse the project\'s files — view Markdown & HTML, edit any text file, create new ones.','files.newFile':'+ File','files.newFolder':'+ Folder','files.pickFile':'Select a file to view it.','files.empty':'No files yet.','files.edit':'Edit','files.preview':'Preview','files.save':'Save','files.saved':'✓ saved','files.saveError':'Could not save this file.','files.loadError':'Could not load this file.','files.binary':'This file can\'t be previewed here (not text).','files.discardConfirm':'Discard unsaved changes?','files.newFilePrompt':'New file name (e.g. todo.md):','files.newFolderPrompt':'New folder name:','files.projectRoot':'project root','files.creatingIn':'Creating in: {path}','files.refresh':'Refresh','files.discard':'Discard','files.create':'Create',
|
|
92
92
|
'notes.sub':'A freeform scratchpad for this project — Markdown, autosaved as you type. Only you (and whoever else opens this dashboard) can see it.','notes.saving':'Saving…',
|
|
93
|
+
'workflow.analyze':'Analyze the project','workflow.analyzeTitle':'Look at the project and suggest which steps fit it now','workflow.detected':'Detected: {type} · {phase}','workflow.type.app':'application','workflow.type.infra':'infrastructure','workflow.type.data':'data','workflow.phase.design':'design phase, no code yet','workflow.phase.build':'has code','workflow.matches':'Your workflow already matches the project.','workflow.apply':'Apply','workflow.dismiss':'Close','workflow.applied':'{n} step(s) updated.','workflow.analyzeError':'Could not analyze the project.','workflow.on':'on','workflow.off':'off','workflow.reason.always':'always useful','workflow.reason.design-no-code':'no code yet','workflow.reason.has-code':'the project has code','workflow.reason.has-tests':'the project has tests','workflow.reason.infra-no-tests':'infrastructure project with no tests','workflow.reason.data-quality':'data project — data quality tests','workflow.reason.has-integration-tests':'integration tests exist','workflow.reason.no-integration-tests':'no integration tests yet','workflow.reason.has-e2e-setup':'an end-to-end test setup exists','workflow.reason.no-e2e-setup':'no end-to-end test setup','settings.workflowAuto':'Let the agent enable workflow steps when needed','settings.workflowAutoHint':'Off: it asks you first. It never disables a step on its own.',
|
|
93
94
|
'brain.sub':'What spectoflow has learned about you, shared by all your projects and given to your agent in every session. Add or fix anything.','brain.autoAdd':'Add what the agent learns directly','brain.autoAddOn':'New facts are added right away — you can fix or delete them here.','brain.autoAddOff':'New facts wait in “To confirm” until you accept them.','brain.agents':'Reachable by:','brain.agentWired':'Connected: this agent reads and grows your second brain','brain.agentNotWired':'Not connected yet','brain.setupHint':'Run {cmd} to connect the others.','brain.noAgents':'No coding agent found on this machine.','brain.tooMany':'{n} entries — all of it is given to your agent in every session. Consider removing what is no longer true.','brain.empty':'Nothing yet. As you work, your agent notes durable things about you here — your role, your preferences, how you like to work, what to avoid. You can also add them yourself below.','brain.toConfirm':'To confirm','brain.confirm':'Confirm','brain.confirmAll':'Confirm all','brain.reject':'Reject','brain.cat.profile':'Profile','brain.cat.preferences':'Preferences','brain.cat.workflow':'Working style','brain.cat.avoid':'Avoid','brain.catHint.profile':'Who you are: role, skills, context.','brain.catHint.preferences':'Tools, languages, code style, formats you prefer.','brain.catHint.workflow':'How you like the agent to work with you.','brain.catHint.avoid':'What the agent should never do.','brain.catEmpty':'Nothing here yet.','brain.addPlaceholder':'Add a fact…','brain.duplicate':'Already in your second brain.','brain.byAgent':'learned by the agent','brain.byYou':'added by you','brain.path':'Stored in {path} — never inside a project.','brain.error':'Could not reach your second brain.',
|
|
94
95
|
'meeting.sub':'One dated note per day for this project — write it yourself, or have the active agent draft it from recent tasks and chat activity.','meeting.history':'History','meeting.today':'today','meeting.generate':'Generate','meeting.generateTitle':'Generate today\'s note from recent activity','meeting.overwriteWarn':'This will overwrite today\'s note.','meeting.generateAnyway':'Generate anyway',
|
|
95
96
|
'chat.widgetTitle':'Run an agent','chat.widgetSub':'Quick access · full view in the Chat tab',
|
|
@@ -210,6 +211,7 @@ fr: {
|
|
|
210
211
|
'drawer.agent':'Agent','drawer.skill':'Compétence','drawer.file':'Fichier · {rel}',
|
|
211
212
|
'drawer.loading':'Chargement…','drawer.loadError':'Impossible de charger ce fichier.','files.title':'Fichiers','files.sub':'Parcourez les fichiers du projet — visualisez Markdown et HTML, modifiez tout fichier texte, créez-en de nouveaux.','files.newFile':'+ Fichier','files.newFolder':'+ Dossier','files.pickFile':'Sélectionnez un fichier pour l’afficher.','files.empty':'Aucun fichier pour l’instant.','files.edit':'Modifier','files.preview':'Aperçu','files.save':'Enregistrer','files.saved':'✓ enregistré','files.saveError':'Impossible d’enregistrer ce fichier.','files.loadError':'Impossible de charger ce fichier.','files.binary':'Ce fichier ne peut pas être prévisualisé ici (non textuel).','files.discardConfirm':'Abandonner les modifications non enregistrées ?','files.newFilePrompt':'Nom du nouveau fichier (ex. todo.md) :','files.newFolderPrompt':'Nom du nouveau dossier :','files.projectRoot':'racine du projet','files.creatingIn':'Création dans : {path}','files.refresh':'Actualiser','files.discard':'Annuler','files.create':'Créer',
|
|
212
213
|
'notes.sub':'Un bloc-note libre pour ce projet — Markdown, enregistré automatiquement au fil de la frappe. Visible uniquement par vous (et quiconque ouvre ce tableau de bord).','notes.saving':'Enregistrement…',
|
|
214
|
+
'workflow.analyze':'Analyser le projet','workflow.analyzeTitle':'Examiner le projet et proposer les étapes qui lui conviennent maintenant','workflow.detected':'Détecté : {type} · {phase}','workflow.type.app':'application','workflow.type.infra':'infrastructure','workflow.type.data':'données','workflow.phase.design':'phase de conception, pas encore de code','workflow.phase.build':'contient du code','workflow.matches':'Votre workflow correspond déjà au projet.','workflow.apply':'Appliquer','workflow.dismiss':'Fermer','workflow.applied':'{n} étape(s) mise(s) à jour.','workflow.analyzeError':'Impossible d’analyser le projet.','workflow.on':'activée','workflow.off':'désactivée','workflow.reason.always':'toujours utile','workflow.reason.design-no-code':'pas encore de code','workflow.reason.has-code':'le projet contient du code','workflow.reason.has-tests':'le projet a des tests','workflow.reason.infra-no-tests':'projet d’infrastructure sans tests','workflow.reason.data-quality':'projet de données — tests de qualité des données','workflow.reason.has-integration-tests':'des tests d’intégration existent','workflow.reason.no-integration-tests':'pas encore de tests d’intégration','workflow.reason.has-e2e-setup':'une configuration de tests de bout en bout existe','workflow.reason.no-e2e-setup':'pas de configuration de tests de bout en bout','settings.workflowAuto':'Laisser l’agent activer les étapes du workflow quand il faut','settings.workflowAutoHint':'Désactivé : il vous demande d’abord. Il ne désactive jamais une étape de lui-même.',
|
|
213
215
|
'brain.sub':'Ce que spectoflow a appris sur vous, partagé par tous vos projets et donné à votre agent à chaque session. Ajoutez ou corrigez ce que vous voulez.','brain.autoAdd':'Ajouter directement ce que l’agent apprend','brain.autoAddOn':'Les nouveaux faits sont ajoutés tout de suite — vous pouvez les corriger ou les supprimer ici.','brain.autoAddOff':'Les nouveaux faits attendent dans « À confirmer » jusqu’à votre validation.','brain.agents':'Accessible par :','brain.agentWired':'Connecté : cet agent lit et enrichit votre second cerveau','brain.agentNotWired':'Pas encore connecté','brain.setupHint':'Lancez {cmd} pour connecter les autres.','brain.noAgents':'Aucun agent de code trouvé sur cette machine.','brain.tooMany':'{n} entrées — tout est donné à votre agent à chaque session. Pensez à retirer ce qui n’est plus vrai.','brain.empty':'Rien pour l’instant. Au fil de votre travail, votre agent note ici des choses durables sur vous — votre rôle, vos préférences, votre façon de travailler, ce qu’il faut éviter. Vous pouvez aussi les ajouter vous-même ci-dessous.','brain.toConfirm':'À confirmer','brain.confirm':'Confirmer','brain.confirmAll':'Tout confirmer','brain.reject':'Rejeter','brain.cat.profile':'Profil','brain.cat.preferences':'Préférences','brain.cat.workflow':'Façon de travailler','brain.cat.avoid':'À éviter','brain.catHint.profile':'Qui vous êtes : rôle, compétences, contexte.','brain.catHint.preferences':'Outils, langues, style de code, formats que vous préférez.','brain.catHint.workflow':'Comment vous aimez que l’agent travaille avec vous.','brain.catHint.avoid':'Ce que l’agent ne doit jamais faire.','brain.catEmpty':'Rien ici pour l’instant.','brain.addPlaceholder':'Ajouter un fait…','brain.duplicate':'Déjà dans votre second cerveau.','brain.byAgent':'appris par l’agent','brain.byYou':'ajouté par vous','brain.path':'Stocké dans {path} — jamais dans un projet.','brain.error':'Impossible d’accéder à votre second cerveau.',
|
|
214
216
|
'meeting.sub':'Une note datée par jour pour ce projet — rédigez-la vous-même, ou laissez l’agent actif la rédiger à partir des tâches récentes et du chat.','meeting.history':'Historique','meeting.today':'aujourd’hui','meeting.generate':'Générer','meeting.generateTitle':'Générer la note du jour à partir de l’activité récente','meeting.overwriteWarn':'Cela va écraser la note d’aujourd’hui.','meeting.generateAnyway':'Générer quand même',
|
|
215
217
|
'chat.widgetTitle':'Lancer un agent','chat.widgetSub':'Accès rapide · vue complète dans l’onglet Chat',
|
|
@@ -330,6 +332,7 @@ es: {
|
|
|
330
332
|
'drawer.agent':'Agente','drawer.skill':'Habilidad','drawer.file':'Archivo · {rel}',
|
|
331
333
|
'drawer.loading':'Cargando…','drawer.loadError':'No se pudo cargar este archivo.','files.title':'Archivos','files.sub':'Explora los archivos del proyecto — visualiza Markdown y HTML, edita cualquier archivo de texto, crea otros nuevos.','files.newFile':'+ Archivo','files.newFolder':'+ Carpeta','files.pickFile':'Selecciona un archivo para verlo.','files.empty':'Aún no hay archivos.','files.edit':'Editar','files.preview':'Vista previa','files.save':'Guardar','files.saved':'✓ guardado','files.saveError':'No se pudo guardar este archivo.','files.loadError':'No se pudo cargar este archivo.','files.binary':'Este archivo no se puede previsualizar aquí (no es texto).','files.discardConfirm':'¿Descartar los cambios sin guardar?','files.newFilePrompt':'Nombre del nuevo archivo (p. ej. todo.md):','files.newFolderPrompt':'Nombre de la nueva carpeta:','files.projectRoot':'raíz del proyecto','files.creatingIn':'Creando en: {path}','files.refresh':'Actualizar','files.discard':'Descartar','files.create':'Crear',
|
|
332
334
|
'notes.sub':'Un bloc de notas libre para este proyecto — Markdown, guardado automáticamente mientras escribes. Solo tú (y quien más abra este panel) puedes verlo.','notes.saving':'Guardando…',
|
|
335
|
+
'workflow.analyze':'Analizar el proyecto','workflow.analyzeTitle':'Examinar el proyecto y proponer los pasos que le convienen ahora','workflow.detected':'Detectado: {type} · {phase}','workflow.type.app':'aplicación','workflow.type.infra':'infraestructura','workflow.type.data':'datos','workflow.phase.design':'fase de diseño, aún sin código','workflow.phase.build':'tiene código','workflow.matches':'Tu workflow ya corresponde al proyecto.','workflow.apply':'Aplicar','workflow.dismiss':'Cerrar','workflow.applied':'{n} paso(s) actualizado(s).','workflow.analyzeError':'No se pudo analizar el proyecto.','workflow.on':'activado','workflow.off':'desactivado','workflow.reason.always':'siempre útil','workflow.reason.design-no-code':'aún no hay código','workflow.reason.has-code':'el proyecto tiene código','workflow.reason.has-tests':'el proyecto tiene tests','workflow.reason.infra-no-tests':'proyecto de infraestructura sin tests','workflow.reason.data-quality':'proyecto de datos — tests de calidad de datos','workflow.reason.has-integration-tests':'existen tests de integración','workflow.reason.no-integration-tests':'aún no hay tests de integración','workflow.reason.has-e2e-setup':'existe una configuración de tests end-to-end','workflow.reason.no-e2e-setup':'no hay configuración de tests end-to-end','settings.workflowAuto':'Dejar que el agente active pasos del workflow cuando haga falta','settings.workflowAutoHint':'Desactivado: te pregunta antes. Nunca desactiva un paso por su cuenta.',
|
|
333
336
|
'brain.sub':'Lo que spectoflow ha aprendido sobre ti, compartido por todos tus proyectos y dado a tu agente en cada sesión. Añade o corrige lo que quieras.','brain.autoAdd':'Añadir directamente lo que aprende el agente','brain.autoAddOn':'Los nuevos datos se añaden al instante — puedes corregirlos o borrarlos aquí.','brain.autoAddOff':'Los nuevos datos esperan en «Por confirmar» hasta que los aceptes.','brain.agents':'Accesible por:','brain.agentWired':'Conectado: este agente lee y amplía tu segundo cerebro','brain.agentNotWired':'Aún no conectado','brain.setupHint':'Ejecuta {cmd} para conectar los demás.','brain.noAgents':'No se encontró ningún agente de código en esta máquina.','brain.tooMany':'{n} entradas — todo se da a tu agente en cada sesión. Plantéate quitar lo que ya no sea cierto.','brain.empty':'Nada todavía. Mientras trabajas, tu agente anota aquí cosas duraderas sobre ti — tu rol, tus preferencias, cómo te gusta trabajar, qué evitar. También puedes añadirlas tú abajo.','brain.toConfirm':'Por confirmar','brain.confirm':'Confirmar','brain.confirmAll':'Confirmar todo','brain.reject':'Rechazar','brain.cat.profile':'Perfil','brain.cat.preferences':'Preferencias','brain.cat.workflow':'Forma de trabajar','brain.cat.avoid':'Evitar','brain.catHint.profile':'Quién eres: rol, habilidades, contexto.','brain.catHint.preferences':'Herramientas, idiomas, estilo de código, formatos que prefieres.','brain.catHint.workflow':'Cómo te gusta que el agente trabaje contigo.','brain.catHint.avoid':'Lo que el agente nunca debe hacer.','brain.catEmpty':'Nada aquí todavía.','brain.addPlaceholder':'Añadir un dato…','brain.duplicate':'Ya está en tu segundo cerebro.','brain.byAgent':'aprendido por el agente','brain.byYou':'añadido por ti','brain.path':'Guardado en {path} — nunca dentro de un proyecto.','brain.error':'No se pudo acceder a tu segundo cerebro.',
|
|
334
337
|
'meeting.sub':'Una nota fechada por día para este proyecto — escríbela tú mismo, o deja que el agente activo la redacte a partir de las tareas y el chat recientes.','meeting.history':'Historial','meeting.today':'hoy','meeting.generate':'Generar','meeting.generateTitle':'Generar la nota de hoy a partir de la actividad reciente','meeting.overwriteWarn':'Esto sobrescribirá la nota de hoy.','meeting.generateAnyway':'Generar de todos modos',
|
|
335
338
|
'chat.widgetTitle':'Ejecutar un agente','chat.widgetSub':'Acceso rápido · vista completa en la pestaña Chat',
|
|
@@ -450,6 +453,7 @@ de: {
|
|
|
450
453
|
'drawer.agent':'Agent','drawer.skill':'Skill','drawer.file':'Datei · {rel}',
|
|
451
454
|
'drawer.loading':'Lädt…','drawer.loadError':'Diese Datei konnte nicht geladen werden.','files.title':'Dateien','files.sub':'Durchsuche die Projektdateien — Markdown & HTML ansehen, jede Textdatei bearbeiten, neue erstellen.','files.newFile':'+ Datei','files.newFolder':'+ Ordner','files.pickFile':'Wähle eine Datei aus, um sie anzuzeigen.','files.empty':'Noch keine Dateien.','files.edit':'Bearbeiten','files.preview':'Vorschau','files.save':'Speichern','files.saved':'✓ gespeichert','files.saveError':'Diese Datei konnte nicht gespeichert werden.','files.loadError':'Diese Datei konnte nicht geladen werden.','files.binary':'Diese Datei kann hier nicht angezeigt werden (kein Text).','files.discardConfirm':'Nicht gespeicherte Änderungen verwerfen?','files.newFilePrompt':'Name der neuen Datei (z. B. todo.md):','files.newFolderPrompt':'Name des neuen Ordners:','files.projectRoot':'Projektstamm','files.creatingIn':'Erstellen in: {path}','files.refresh':'Aktualisieren','files.discard':'Verwerfen','files.create':'Erstellen',
|
|
452
455
|
'notes.sub':'Ein freier Notizblock für dieses Projekt — Markdown, automatisch gespeichert während der Eingabe. Nur du (und wer sonst dieses Dashboard öffnet) siehst ihn.','notes.saving':'Speichert…',
|
|
456
|
+
'workflow.analyze':'Projekt analysieren','workflow.analyzeTitle':'Das Projekt ansehen und vorschlagen, welche Schritte jetzt passen','workflow.detected':'Erkannt: {type} · {phase}','workflow.type.app':'Anwendung','workflow.type.infra':'Infrastruktur','workflow.type.data':'Daten','workflow.phase.design':'Entwurfsphase, noch kein Code','workflow.phase.build':'enthält Code','workflow.matches':'Dein Workflow passt bereits zum Projekt.','workflow.apply':'Anwenden','workflow.dismiss':'Schließen','workflow.applied':'{n} Schritt(e) aktualisiert.','workflow.analyzeError':'Das Projekt konnte nicht analysiert werden.','workflow.on':'an','workflow.off':'aus','workflow.reason.always':'immer nützlich','workflow.reason.design-no-code':'noch kein Code','workflow.reason.has-code':'das Projekt enthält Code','workflow.reason.has-tests':'das Projekt hat Tests','workflow.reason.infra-no-tests':'Infrastrukturprojekt ohne Tests','workflow.reason.data-quality':'Datenprojekt — Datenqualitätstests','workflow.reason.has-integration-tests':'Integrationstests vorhanden','workflow.reason.no-integration-tests':'noch keine Integrationstests','workflow.reason.has-e2e-setup':'ein End-to-End-Test-Setup ist vorhanden','workflow.reason.no-e2e-setup':'kein End-to-End-Test-Setup','settings.workflowAuto':'Den Agenten Workflow-Schritte bei Bedarf aktivieren lassen','settings.workflowAutoHint':'Aus: er fragt dich vorher. Er deaktiviert nie selbst einen Schritt.',
|
|
453
457
|
'brain.sub':'Was spectoflow über dich gelernt hat — geteilt von all deinen Projekten und deinem Agenten in jeder Sitzung mitgegeben. Ergänze oder korrigiere, was du willst.','brain.autoAdd':'Was der Agent lernt, direkt hinzufügen','brain.autoAddOn':'Neue Fakten werden sofort hinzugefügt — du kannst sie hier korrigieren oder löschen.','brain.autoAddOff':'Neue Fakten warten unter „Zu bestätigen“, bis du sie annimmst.','brain.agents':'Erreichbar für:','brain.agentWired':'Verbunden: dieser Agent liest und erweitert dein zweites Gehirn','brain.agentNotWired':'Noch nicht verbunden','brain.setupHint':'Führe {cmd} aus, um die anderen zu verbinden.','brain.noAgents':'Kein Coding-Agent auf diesem Rechner gefunden.','brain.tooMany':'{n} Einträge — alles wird deinem Agenten in jeder Sitzung mitgegeben. Entferne, was nicht mehr stimmt.','brain.empty':'Noch nichts. Während du arbeitest, notiert dein Agent hier Dauerhaftes über dich — deine Rolle, deine Vorlieben, wie du gern arbeitest, was zu vermeiden ist. Du kannst sie auch unten selbst hinzufügen.','brain.toConfirm':'Zu bestätigen','brain.confirm':'Bestätigen','brain.confirmAll':'Alle bestätigen','brain.reject':'Ablehnen','brain.cat.profile':'Profil','brain.cat.preferences':'Vorlieben','brain.cat.workflow':'Arbeitsweise','brain.cat.avoid':'Vermeiden','brain.catHint.profile':'Wer du bist: Rolle, Fähigkeiten, Kontext.','brain.catHint.preferences':'Werkzeuge, Sprachen, Code-Stil, Formate, die du bevorzugst.','brain.catHint.workflow':'Wie der Agent mit dir arbeiten soll.','brain.catHint.avoid':'Was der Agent nie tun soll.','brain.catEmpty':'Hier ist noch nichts.','brain.addPlaceholder':'Fakt hinzufügen…','brain.duplicate':'Schon in deinem zweiten Gehirn.','brain.byAgent':'vom Agenten gelernt','brain.byYou':'von dir hinzugefügt','brain.path':'Gespeichert in {path} — nie in einem Projekt.','brain.error':'Dein zweites Gehirn ist nicht erreichbar.',
|
|
454
458
|
'meeting.sub':'Eine datierte Notiz pro Tag für dieses Projekt — schreibe sie selbst, oder lass sie vom aktiven Agenten aus den letzten Aufgaben und dem Chat entwerfen.','meeting.history':'Verlauf','meeting.today':'heute','meeting.generate':'Generieren','meeting.generateTitle':'Die heutige Notiz aus der letzten Aktivität generieren','meeting.overwriteWarn':'Dies überschreibt die heutige Notiz.','meeting.generateAnyway':'Trotzdem generieren',
|
|
455
459
|
'chat.widgetTitle':'Agenten ausführen','chat.widgetSub':'Schnellzugriff · vollständige Ansicht im Chat-Tab',
|
|
@@ -570,6 +574,7 @@ pt: {
|
|
|
570
574
|
'drawer.agent':'Agente','drawer.skill':'Habilidade','drawer.file':'Ficheiro · {rel}',
|
|
571
575
|
'drawer.loading':'A carregar…','drawer.loadError':'Não foi possível carregar este ficheiro.','files.title':'Ficheiros','files.sub':'Percorra os ficheiros do projeto — veja Markdown e HTML, edite qualquer ficheiro de texto, crie novos.','files.newFile':'+ Ficheiro','files.newFolder':'+ Pasta','files.pickFile':'Selecione um ficheiro para o ver.','files.empty':'Ainda não há ficheiros.','files.edit':'Editar','files.preview':'Pré-visualizar','files.save':'Guardar','files.saved':'✓ guardado','files.saveError':'Não foi possível guardar este ficheiro.','files.loadError':'Não foi possível carregar este ficheiro.','files.binary':'Este ficheiro não pode ser pré-visualizado aqui (não é texto).','files.discardConfirm':'Descartar alterações não guardadas?','files.newFilePrompt':'Nome do novo ficheiro (ex. todo.md):','files.newFolderPrompt':'Nome da nova pasta:','files.projectRoot':'raiz do projeto','files.creatingIn':'A criar em: {path}','files.refresh':'Atualizar','files.discard':'Descartar','files.create':'Criar',
|
|
572
576
|
'notes.sub':'Um bloco de notas livre para este projeto — Markdown, guardado automaticamente enquanto escreve. Só você (e quem mais abrir este painel) o vê.','notes.saving':'A guardar…',
|
|
577
|
+
'workflow.analyze':'Analisar o projeto','workflow.analyzeTitle':'Examinar o projeto e propor os passos que lhe convêm agora','workflow.detected':'Detetado: {type} · {phase}','workflow.type.app':'aplicação','workflow.type.infra':'infraestrutura','workflow.type.data':'dados','workflow.phase.design':'fase de conceção, ainda sem código','workflow.phase.build':'tem código','workflow.matches':'O seu workflow já corresponde ao projeto.','workflow.apply':'Aplicar','workflow.dismiss':'Fechar','workflow.applied':'{n} passo(s) atualizado(s).','workflow.analyzeError':'Não foi possível analisar o projeto.','workflow.on':'ativado','workflow.off':'desativado','workflow.reason.always':'sempre útil','workflow.reason.design-no-code':'ainda sem código','workflow.reason.has-code':'o projeto tem código','workflow.reason.has-tests':'o projeto tem testes','workflow.reason.infra-no-tests':'projeto de infraestrutura sem testes','workflow.reason.data-quality':'projeto de dados — testes de qualidade de dados','workflow.reason.has-integration-tests':'existem testes de integração','workflow.reason.no-integration-tests':'ainda sem testes de integração','workflow.reason.has-e2e-setup':'existe uma configuração de testes ponta a ponta','workflow.reason.no-e2e-setup':'sem configuração de testes ponta a ponta','settings.workflowAuto':'Deixar o agente ativar passos do workflow quando necessário','settings.workflowAutoHint':'Desativado: pergunta-lhe primeiro. Nunca desativa um passo por conta própria.',
|
|
573
578
|
'brain.sub':'O que o spectoflow aprendeu sobre si, partilhado por todos os seus projetos e dado ao seu agente em cada sessão. Acrescente ou corrija o que quiser.','brain.autoAdd':'Adicionar diretamente o que o agente aprende','brain.autoAddOn':'Os novos factos são adicionados de imediato — pode corrigi-los ou apagá-los aqui.','brain.autoAddOff':'Os novos factos esperam em «A confirmar» até os aceitar.','brain.agents':'Acessível por:','brain.agentWired':'Ligado: este agente lê e enriquece o seu segundo cérebro','brain.agentNotWired':'Ainda não ligado','brain.setupHint':'Execute {cmd} para ligar os outros.','brain.noAgents':'Nenhum agente de código encontrado nesta máquina.','brain.tooMany':'{n} entradas — tudo é dado ao seu agente em cada sessão. Pense em retirar o que já não é verdade.','brain.empty':'Ainda nada. À medida que trabalha, o seu agente anota aqui coisas duradouras sobre si — o seu papel, as suas preferências, como gosta de trabalhar, o que evitar. Também as pode adicionar abaixo.','brain.toConfirm':'A confirmar','brain.confirm':'Confirmar','brain.confirmAll':'Confirmar tudo','brain.reject':'Rejeitar','brain.cat.profile':'Perfil','brain.cat.preferences':'Preferências','brain.cat.workflow':'Forma de trabalhar','brain.cat.avoid':'A evitar','brain.catHint.profile':'Quem é: papel, competências, contexto.','brain.catHint.preferences':'Ferramentas, línguas, estilo de código, formatos que prefere.','brain.catHint.workflow':'Como gosta que o agente trabalhe consigo.','brain.catHint.avoid':'O que o agente nunca deve fazer.','brain.catEmpty':'Ainda nada aqui.','brain.addPlaceholder':'Adicionar um facto…','brain.duplicate':'Já está no seu segundo cérebro.','brain.byAgent':'aprendido pelo agente','brain.byYou':'adicionado por si','brain.path':'Guardado em {path} — nunca dentro de um projeto.','brain.error':'Não foi possível aceder ao seu segundo cérebro.',
|
|
574
579
|
'meeting.sub':'Uma nota datada por dia para este projeto — escreva-a você mesmo, ou deixe o agente ativo redigi-la a partir das tarefas e do chat recentes.','meeting.history':'Histórico','meeting.today':'hoje','meeting.generate':'Gerar','meeting.generateTitle':'Gerar a nota de hoje a partir da atividade recente','meeting.overwriteWarn':'Isto vai substituir a nota de hoje.','meeting.generateAnyway':'Gerar mesmo assim',
|
|
575
580
|
'chat.widgetTitle':'Executar um agente','chat.widgetSub':'Acesso rápido · vista completa no separador Chat',
|
|
@@ -690,6 +695,7 @@ it: {
|
|
|
690
695
|
'drawer.agent':'Agente','drawer.skill':'Skill','drawer.file':'File · {rel}',
|
|
691
696
|
'drawer.loading':'Caricamento…','drawer.loadError':'Impossibile caricare questo file.','files.title':'File','files.sub':'Sfoglia i file del progetto — visualizza Markdown e HTML, modifica qualsiasi file di testo, creane di nuovi.','files.newFile':'+ File','files.newFolder':'+ Cartella','files.pickFile':'Seleziona un file per visualizzarlo.','files.empty':'Nessun file ancora.','files.edit':'Modifica','files.preview':'Anteprima','files.save':'Salva','files.saved':'✓ salvato','files.saveError':'Impossibile salvare questo file.','files.loadError':'Impossibile caricare questo file.','files.binary':'Questo file non può essere visualizzato qui (non è testo).','files.discardConfirm':'Scartare le modifiche non salvate?','files.newFilePrompt':'Nome del nuovo file (es. todo.md):','files.newFolderPrompt':'Nome della nuova cartella:','files.projectRoot':'radice del progetto','files.creatingIn':'Creazione in: {path}','files.refresh':'Aggiorna','files.discard':'Scarta','files.create':'Crea',
|
|
692
697
|
'notes.sub':'Un blocco note libero per questo progetto — Markdown, salvato automaticamente mentre scrivi. Solo tu (e chiunque altro apra questa dashboard) puoi vederlo.','notes.saving':'Salvataggio…',
|
|
698
|
+
'workflow.analyze':'Analizza il progetto','workflow.analyzeTitle':'Esaminare il progetto e proporre i passi adatti ora','workflow.detected':'Rilevato: {type} · {phase}','workflow.type.app':'applicazione','workflow.type.infra':'infrastruttura','workflow.type.data':'dati','workflow.phase.design':'fase di progettazione, ancora nessun codice','workflow.phase.build':'contiene codice','workflow.matches':'Il tuo workflow corrisponde già al progetto.','workflow.apply':'Applica','workflow.dismiss':'Chiudi','workflow.applied':'{n} passo/i aggiornato/i.','workflow.analyzeError':'Impossibile analizzare il progetto.','workflow.on':'attivo','workflow.off':'disattivo','workflow.reason.always':'sempre utile','workflow.reason.design-no-code':'ancora nessun codice','workflow.reason.has-code':'il progetto contiene codice','workflow.reason.has-tests':'il progetto ha dei test','workflow.reason.infra-no-tests':'progetto di infrastruttura senza test','workflow.reason.data-quality':'progetto di dati — test di qualità dei dati','workflow.reason.has-integration-tests':'esistono test di integrazione','workflow.reason.no-integration-tests':'ancora nessun test di integrazione','workflow.reason.has-e2e-setup':'esiste una configurazione di test end-to-end','workflow.reason.no-e2e-setup':'nessuna configurazione di test end-to-end','settings.workflowAuto':'Lascia che l’agente attivi i passi del workflow quando serve','settings.workflowAutoHint':'Disattivato: ti chiede prima. Non disattiva mai un passo da solo.',
|
|
693
699
|
'brain.sub':'Ciò che spectoflow ha imparato su di te, condiviso da tutti i tuoi progetti e dato al tuo agente in ogni sessione. Aggiungi o correggi ciò che vuoi.','brain.autoAdd':'Aggiungi direttamente ciò che l’agente impara','brain.autoAddOn':'I nuovi fatti vengono aggiunti subito — puoi correggerli o eliminarli qui.','brain.autoAddOff':'I nuovi fatti attendono in «Da confermare» finché non li accetti.','brain.agents':'Raggiungibile da:','brain.agentWired':'Collegato: questo agente legge e arricchisce il tuo secondo cervello','brain.agentNotWired':'Non ancora collegato','brain.setupHint':'Esegui {cmd} per collegare gli altri.','brain.noAgents':'Nessun agente di programmazione trovato su questa macchina.','brain.tooMany':'{n} voci — tutto viene dato al tuo agente in ogni sessione. Valuta di rimuovere ciò che non è più vero.','brain.empty':'Ancora niente. Mentre lavori, il tuo agente annota qui cose durature su di te — il tuo ruolo, le tue preferenze, come ti piace lavorare, cosa evitare. Puoi anche aggiungerle tu qui sotto.','brain.toConfirm':'Da confermare','brain.confirm':'Conferma','brain.confirmAll':'Conferma tutto','brain.reject':'Rifiuta','brain.cat.profile':'Profilo','brain.cat.preferences':'Preferenze','brain.cat.workflow':'Modo di lavorare','brain.cat.avoid':'Da evitare','brain.catHint.profile':'Chi sei: ruolo, competenze, contesto.','brain.catHint.preferences':'Strumenti, lingue, stile di codice, formati che preferisci.','brain.catHint.workflow':'Come ti piace che l’agente lavori con te.','brain.catHint.avoid':'Ciò che l’agente non deve mai fare.','brain.catEmpty':'Ancora niente qui.','brain.addPlaceholder':'Aggiungi un fatto…','brain.duplicate':'Già nel tuo secondo cervello.','brain.byAgent':'imparato dall’agente','brain.byYou':'aggiunto da te','brain.path':'Salvato in {path} — mai dentro un progetto.','brain.error':'Impossibile raggiungere il tuo secondo cervello.',
|
|
694
700
|
'meeting.sub':'Una nota datata al giorno per questo progetto — scrivila tu stesso, oppure lascia che l’agente attivo la scriva a partire dalle attività e dalla chat recenti.','meeting.history':'Cronologia','meeting.today':'oggi','meeting.generate':'Genera','meeting.generateTitle':'Genera la nota di oggi dall’attività recente','meeting.overwriteWarn':'Questo sovrascriverà la nota di oggi.','meeting.generateAnyway':'Genera comunque',
|
|
695
701
|
'chat.widgetTitle':'Avvia un agente','chat.widgetSub':'Accesso rapido · vista completa nella scheda Chat',
|
|
@@ -217,6 +217,10 @@
|
|
|
217
217
|
<div class="wf-wrap">
|
|
218
218
|
<h2 class="panel-title" data-i18n="workflow.title">Active workflow</h2>
|
|
219
219
|
<p class="panel-sub" data-i18n-html="workflow.sub">The pipeline your requests flow through — from idea to shipped, spec-driven. <strong>Click a step</strong> to see what it does and turn it on or off (this edits <code>.spectoflow/workflow.md</code>). Enabled steps run in order; the orchestrator gates each one by your <em>mode</em> and <em>policy</em>. Optional steps stay off until you switch them on.</p>
|
|
220
|
+
<div class="wf-analyze">
|
|
221
|
+
<button class="btn" id="wfAnalyzeBtn" type="button" data-i18n="workflow.analyze" data-i18n-title="workflow.analyzeTitle" title="Look at the project and suggest which steps fit it now">Analyze the project</button>
|
|
222
|
+
<div class="wf-suggest" id="wfSuggest" hidden></div>
|
|
223
|
+
</div>
|
|
220
224
|
<div class="wf-diagram" id="wfDiagram"></div>
|
|
221
225
|
</div>
|
|
222
226
|
</section>
|
|
@@ -397,6 +401,13 @@
|
|
|
397
401
|
<option value="manual" data-i18n="mode.manual.desc">manual — approve every step</option>
|
|
398
402
|
</select>
|
|
399
403
|
</label>
|
|
404
|
+
<label class="settings-check">
|
|
405
|
+
<input type="checkbox" id="setWorkflowAuto">
|
|
406
|
+
<span class="settings-check-text">
|
|
407
|
+
<span class="settings-check-label" data-i18n="settings.workflowAuto">Let the agent enable workflow steps when needed</span>
|
|
408
|
+
<span class="settings-check-hint" data-i18n="settings.workflowAutoHint">Off: it asks you first. It never disables a step on its own.</span>
|
|
409
|
+
</span>
|
|
410
|
+
</label>
|
|
400
411
|
</div>
|
|
401
412
|
<div class="settings-card">
|
|
402
413
|
<h3 class="settings-card-title" data-i18n="settings.group.appearance">Appearance & language</h3>
|
|
@@ -659,6 +659,31 @@ body.booting .ring-svg circle:last-of-type { transform-origin:center; animation:
|
|
|
659
659
|
.attn-edit { width:100%; min-height:64px; font-family:inherit; font-size:14px; padding:8px; border:1px solid var(--cool); border-radius:8px; background:var(--surface-2); color:var(--ink); box-sizing:border-box; }
|
|
660
660
|
.attn-actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:11px; }
|
|
661
661
|
|
|
662
|
+
/* ---- Workflow → Analyze the project ---- */
|
|
663
|
+
.wf-analyze { display:flex; flex-direction:column; align-items:flex-start; gap:10px; margin:2px 0 16px; }
|
|
664
|
+
.wf-suggest { align-self:stretch; border:1px solid var(--line); border-left:3px solid var(--signal); border-radius:var(--radius); background:var(--surface); padding:12px 14px; display:flex; flex-direction:column; gap:10px; max-width:820px; }
|
|
665
|
+
.wf-suggest[hidden] { display:none; }
|
|
666
|
+
.wf-suggest-head { display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
|
667
|
+
.wf-suggest-detected { font-size:13px; font-weight:600; }
|
|
668
|
+
.wf-suggest-close { border:none; background:none; color:var(--muted); font-size:18px; line-height:1; cursor:pointer; padding:0 4px; }
|
|
669
|
+
.wf-suggest-close:hover { color:var(--ink); }
|
|
670
|
+
.wf-suggest-ok, .wf-suggest-note { font-size:13px; color:var(--muted); }
|
|
671
|
+
.wf-suggest-list { display:flex; flex-direction:column; gap:6px; }
|
|
672
|
+
.wf-suggest-row { display:grid; grid-template-columns:auto minmax(110px,max-content) auto 1fr; align-items:center; gap:6px 12px; font-size:13px; cursor:pointer; padding:6px 8px; border-radius:8px; background:var(--surface-2); }
|
|
673
|
+
.wf-suggest-row input { accent-color:var(--signal); margin:0; }
|
|
674
|
+
.wf-suggest-name { font-weight:600; }
|
|
675
|
+
.wf-suggest-change { font-family:var(--mono); font-size:11px; border:1px solid var(--line); border-radius:999px; padding:1px 8px; white-space:nowrap; }
|
|
676
|
+
.wf-suggest-change.is-on { color:var(--s-done); border-color:color-mix(in srgb,var(--s-done) 45%,var(--line)); }
|
|
677
|
+
.wf-suggest-change.is-off { color:var(--muted); }
|
|
678
|
+
.wf-suggest-reason { color:var(--muted); font-size:12.5px; }
|
|
679
|
+
.wf-suggest-actions { display:flex; gap:8px; }
|
|
680
|
+
@media (max-width:620px){ .wf-suggest-row { grid-template-columns:auto 1fr; } .wf-suggest-change, .wf-suggest-reason { grid-column:2; } }
|
|
681
|
+
.settings-check { display:flex; align-items:flex-start; gap:9px; cursor:pointer; }
|
|
682
|
+
.settings-check input { accent-color:var(--signal); width:15px; height:15px; margin:2px 0 0; flex:none; }
|
|
683
|
+
.settings-check-text { display:flex; flex-direction:column; gap:3px; }
|
|
684
|
+
.settings-check-label { font-size:13px; font-weight:600; }
|
|
685
|
+
.settings-check-hint { font-size:12px; color:var(--muted); }
|
|
686
|
+
|
|
662
687
|
/* ---- Second brain ---- */
|
|
663
688
|
.brain-wrap { max-width:1080px; margin:0 auto; padding:20px 22px 32px; }
|
|
664
689
|
.brain-bar { display:flex; flex-wrap:wrap; align-items:center; gap:6px 14px; margin:4px 0 10px; }
|
package/lib/dashboard/routes.js
CHANGED
|
@@ -20,6 +20,8 @@ const ROUTES = [
|
|
|
20
20
|
['PATCH', /^\/api\/task\/[^/]+$/, 'task.update', (_u, b, p) => ({ id: seg(p, 3), patch: b })],
|
|
21
21
|
['POST', /^\/api\/task\/[^/]+\/comment$/, 'task.comment', (_u, b, p) => ({ id: seg(p, 3), text: b.text, action: b.action })],
|
|
22
22
|
['POST', '/api/workflow/toggle', 'workflow.toggle', (_u, b) => b],
|
|
23
|
+
['GET', '/api/workflow/suggest', 'workflow.suggest', () => ({})],
|
|
24
|
+
['POST', '/api/workflow/apply', 'workflow.apply', (_u, b) => b],
|
|
23
25
|
['POST', '/api/run', 'run.start', (_u, b) => b],
|
|
24
26
|
['POST', '/api/chat/summarize', 'chat.summarize', (_u, b) => b],
|
|
25
27
|
['POST', '/api/meeting/generate', 'meeting.generate', (_u, b) => b],
|
package/lib/init.js
CHANGED
|
@@ -14,6 +14,7 @@ const manifest = require('./manifest');
|
|
|
14
14
|
const mcp = require('./mcp');
|
|
15
15
|
const store = require('./store');
|
|
16
16
|
const globalConfig = require('./global-config');
|
|
17
|
+
const workflowDetect = require('./workflow-detect');
|
|
17
18
|
|
|
18
19
|
function copyDir(src, dst) {
|
|
19
20
|
fs.mkdirSync(dst, { recursive: true });
|
|
@@ -70,6 +71,7 @@ function runInit({ target, templatesDir, version, agentsArg, defaults }) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
const spectoflowDir = path.join(target, '.spectoflow');
|
|
74
|
+
const workflowExisted = fs.existsSync(path.join(spectoflowDir, 'workflow.md'));
|
|
73
75
|
copyDir(templatesDir, spectoflowDir);
|
|
74
76
|
|
|
75
77
|
const frameworkFiles = ownership.listFrameworkFiles(templatesDir);
|
|
@@ -87,6 +89,15 @@ function runInit({ target, templatesDir, version, agentsArg, defaults }) {
|
|
|
87
89
|
cfg.agent = agentsArg ? agents[0] : ((detected.includes(d.agent) || !detected.length) ? d.agent : detected[0]);
|
|
88
90
|
if (!agents.includes(cfg.agent)) agents.unshift(cfg.agent);
|
|
89
91
|
cfg.runners = { ...cfg.runners, ...adapters.defaultRunners(agents) };
|
|
92
|
+
// Fit the freshly copied workflow to the project (D75). An existing workflow.md is the user's: never touched.
|
|
93
|
+
let workflowFit = null;
|
|
94
|
+
if (!workflowExisted) {
|
|
95
|
+
const fit = workflowDetect.detect(target);
|
|
96
|
+
const changed = workflowDetect.applySteps(target, Object.fromEntries(Object.entries(fit.steps).map(([k, v]) => [k, v.enabled])));
|
|
97
|
+
cfg.projectType = fit.projectType;
|
|
98
|
+
cfg.workflowReview = 'pending';
|
|
99
|
+
workflowFit = { ...fit, changed };
|
|
100
|
+
}
|
|
90
101
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
91
102
|
|
|
92
103
|
const plansDirName = store.resolvePlansDir(target, cfg);
|
|
@@ -118,6 +129,18 @@ function runInit({ target, templatesDir, version, agentsArg, defaults }) {
|
|
|
118
129
|
if (!giText.includes(line)) fs.appendFileSync(gi, ((fs.existsSync(gi) && fs.readFileSync(gi, 'utf8').length) ? '\n' : '') + line + '\n');
|
|
119
130
|
}
|
|
120
131
|
|
|
132
|
+
if (workflowFit) {
|
|
133
|
+
// One clause per (on/off, reason): "off: Develop, Unit tests, Review (no code yet)".
|
|
134
|
+
const groups = new Map();
|
|
135
|
+
for (const n of workflowFit.changed) {
|
|
136
|
+
const s = workflowFit.steps[n], key = `${s.enabled ? 'on' : 'off'}|${s.reason}`;
|
|
137
|
+
groups.set(key, [...(groups.get(key) || []), n]);
|
|
138
|
+
}
|
|
139
|
+
const clauses = [...groups].map(([key, names]) => { const [state, reason] = key.split('|'); return `${state}: ${names.join(', ')} (${workflowDetect.REASONS[reason]})`; });
|
|
140
|
+
const phase = workflowFit.phase === 'design' ? 'design phase' : `${workflowFit.projectType} project with code`;
|
|
141
|
+
notes.push(`Workflow fitted to the project (${phase})${clauses.length ? ' — ' + clauses.join('; ') : ' — the default steps fit'}. Your agent will review it with you; change it anytime in the dashboard's Workflow tab.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
121
144
|
const unwired = require('./brain-setup').status().filter((a) => !a.wired);
|
|
122
145
|
if (unwired.length) notes.push(`Your second brain isn't connected to ${unwired.map((a) => a.label).join(', ')} yet — run: spectoflow brain setup (once per machine).`);
|
|
123
146
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Fits the workflow to the project (docs/workflow-autoconfig-design.md, DECISIONS D75). A deterministic,
|
|
4
|
+
* zero-dependency look at the files: is there code yet (phase: design or build), what kind of project it is
|
|
5
|
+
* (app / infra / data), and which test setups already exist. From that, which of the kit's workflow steps
|
|
6
|
+
* make sense now — each decision with a reason code. `init` applies it to a freshly copied workflow.md;
|
|
7
|
+
* `spectoflow workflow suggest` and the dashboard's Workflow tab offer it for an existing project. Steps
|
|
8
|
+
* the user added themselves are never touched. The agent refines this with the user (it can tell a
|
|
9
|
+
* prototype from a product; a file scan can't).
|
|
10
|
+
*/
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', '.spectoflow', 'vendor', 'dist', 'build', 'target', '.venv', 'venv', '__pycache__', 'coverage', '.next', '.nuxt', 'out']);
|
|
15
|
+
const CODE_EXT = new Set(['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.kt', '.cs', '.rb', '.php', '.swift', '.c', '.cpp', '.h', '.vue', '.svelte', '.dart', '.scala', '.ex', '.exs']);
|
|
16
|
+
const MANIFESTS = new Set(['pyproject.toml', 'setup.py', 'requirements.txt', 'go.mod', 'Cargo.toml', 'pom.xml', 'build.gradle', 'build.gradle.kts', 'Gemfile', 'composer.json', 'pubspec.yaml', 'mix.exs']);
|
|
17
|
+
const INFRA_FILES = new Set(['Pulumi.yaml', 'Chart.yaml', 'kustomization.yaml', 'ansible.cfg']);
|
|
18
|
+
const TEST_DIRS = new Set(['test', 'tests', '__tests__']);
|
|
19
|
+
const E2E_DEPS = ['@playwright/test', 'playwright', 'cypress', 'puppeteer'];
|
|
20
|
+
const LIMITS = { depth: 4, entries: 5000 };
|
|
21
|
+
|
|
22
|
+
const isTestFile = (name) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(name) || /_test\.(go|py)$/.test(name) || /^test_.*\.py$/.test(name) || /Tests?\.(java|kt|cs)$/.test(name) || /_spec\.rb$/.test(name);
|
|
23
|
+
|
|
24
|
+
function readPackageJson(fp, s) {
|
|
25
|
+
let pkg;
|
|
26
|
+
try { pkg = JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return; }
|
|
27
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
28
|
+
const scripts = pkg.scripts || {};
|
|
29
|
+
const realScripts = Object.entries(scripts).filter(([k, v]) => !(k === 'test' && /no test specified/.test(String(v))));
|
|
30
|
+
if (Object.keys(deps).length || realScripts.length) s.manifests.push(fp);
|
|
31
|
+
if (scripts.test && !/no test specified/.test(String(scripts.test))) s.unitTests = true;
|
|
32
|
+
if (E2E_DEPS.some((d) => deps[d])) s.e2e = true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// → { sourceFiles, manifests, unitTests, e2e, integration, infra, data, truncated }
|
|
36
|
+
function scan(root, limits = LIMITS) {
|
|
37
|
+
const s = { sourceFiles: 0, manifests: [], unitTests: false, e2e: false, integration: false, infra: false, data: false, truncated: false };
|
|
38
|
+
let seen = 0;
|
|
39
|
+
const walk = (dir, depth, dirs) => {
|
|
40
|
+
if (s.truncated) return;
|
|
41
|
+
let list;
|
|
42
|
+
try { list = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
43
|
+
for (const e of list) {
|
|
44
|
+
if (++seen > limits.entries) { s.truncated = true; return; }
|
|
45
|
+
const fp = path.join(dir, e.name);
|
|
46
|
+
if (e.isDirectory()) {
|
|
47
|
+
if (SKIP_DIRS.has(e.name) || e.name.startsWith('.') || depth >= limits.depth) continue;
|
|
48
|
+
walk(fp, depth + 1, [...dirs, e.name]);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!e.isFile()) continue;
|
|
52
|
+
const name = e.name, ext = path.extname(name).toLowerCase();
|
|
53
|
+
if (name === 'package.json') readPackageJson(fp, s);
|
|
54
|
+
else if (MANIFESTS.has(name) || ext === '.csproj') s.manifests.push(fp);
|
|
55
|
+
if (/^(playwright|cypress)\.config\.[cm]?[jt]s$/.test(name) || name === 'cypress.json') s.e2e = true;
|
|
56
|
+
if (ext === '.tf' || INFRA_FILES.has(name)) s.infra = true;
|
|
57
|
+
if (name === 'dbt_project.yml' || ext === '.ipynb' || (ext === '.py' && dirs.includes('dags'))) s.data = true;
|
|
58
|
+
if (!CODE_EXT.has(ext)) continue;
|
|
59
|
+
const underTests = dirs.some((d) => TEST_DIRS.has(d));
|
|
60
|
+
if (dirs.includes('e2e') || dirs.includes('cypress')) { s.e2e = true; continue; }
|
|
61
|
+
if (underTests && dirs.includes('integration')) { s.integration = true; continue; }
|
|
62
|
+
if (underTests || isTestFile(name)) { s.unitTests = true; continue; }
|
|
63
|
+
s.sourceFiles++;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
walk(root, 0, []);
|
|
67
|
+
return s;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const STEPS = ['Brainstorm', 'Analysis', 'Spec', 'Plan', 'Develop', 'Unit tests', 'Integration tests', 'End-to-end tests', 'Review'];
|
|
71
|
+
|
|
72
|
+
// → { projectType, phase, signals, steps: { <name>: { enabled, reason } } }
|
|
73
|
+
function detect(root, limits) {
|
|
74
|
+
const signals = scan(root, limits);
|
|
75
|
+
const hasCode = signals.sourceFiles > 0 || signals.manifests.length > 0 || signals.unitTests || signals.infra || signals.data;
|
|
76
|
+
const projectType = signals.infra && signals.sourceFiles < 10 ? 'infra' : signals.data ? 'data' : 'app';
|
|
77
|
+
const phase = hasCode ? 'build' : 'design';
|
|
78
|
+
const on = (reason) => ({ enabled: true, reason }), off = (reason) => ({ enabled: false, reason });
|
|
79
|
+
const steps = { Brainstorm: on('always'), Analysis: on('always'), Spec: on('always'), Plan: on('always') };
|
|
80
|
+
if (phase === 'design') {
|
|
81
|
+
for (const n of ['Develop', 'Unit tests', 'Integration tests', 'End-to-end tests', 'Review']) steps[n] = off('design-no-code');
|
|
82
|
+
} else {
|
|
83
|
+
steps.Develop = on('has-code');
|
|
84
|
+
steps.Review = on('has-code');
|
|
85
|
+
steps['Unit tests'] = projectType === 'infra' ? (signals.unitTests ? on('has-tests') : off('infra-no-tests'))
|
|
86
|
+
: projectType === 'data' ? on('data-quality') : on('has-code');
|
|
87
|
+
steps['Integration tests'] = signals.integration ? on('has-integration-tests') : off('no-integration-tests');
|
|
88
|
+
steps['End-to-end tests'] = projectType === 'app' && signals.e2e ? on('has-e2e-setup') : off('no-e2e-setup');
|
|
89
|
+
}
|
|
90
|
+
return { projectType, phase, signals, steps };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The step's name as store.readWorkflow() reports it: annotation stripped first, then "(optional)".
|
|
94
|
+
function stepName(rest) {
|
|
95
|
+
const ann = rest.match(/\{([^}]*)\}\s*$/);
|
|
96
|
+
if (ann) rest = rest.slice(0, ann.index).trim();
|
|
97
|
+
return rest.replace(/\s*\(optional\)\s*$/i, '').trim();
|
|
98
|
+
}
|
|
99
|
+
const LINE_RE = /^(\s*- \[)( |x|X)(\]\s+)(.*?)(\r?)$/;
|
|
100
|
+
|
|
101
|
+
function workflowPath(root) { return path.join(root, '.spectoflow', 'workflow.md'); }
|
|
102
|
+
|
|
103
|
+
// Current state of the steps detection knows about → [{ name, enabled }] in file order.
|
|
104
|
+
function currentSteps(root) {
|
|
105
|
+
let text;
|
|
106
|
+
try { text = fs.readFileSync(workflowPath(root), 'utf8'); } catch { return []; }
|
|
107
|
+
return text.split('\n').map((l) => l.match(LINE_RE)).filter(Boolean).map((m) => ({ name: stepName(m[4]), enabled: m[2].trim() !== '' }));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// What re-analysis would change → { projectType, phase, currentType, changes: [{ name, from, to, reason }] }
|
|
111
|
+
function suggest(root, { projectType: currentType, limits } = {}) {
|
|
112
|
+
const d = detect(root, limits);
|
|
113
|
+
const changes = currentSteps(root)
|
|
114
|
+
.filter((s) => d.steps[s.name] && d.steps[s.name].enabled !== s.enabled)
|
|
115
|
+
.map((s) => ({ name: s.name, from: s.enabled, to: d.steps[s.name].enabled, reason: d.steps[s.name].reason }));
|
|
116
|
+
return { projectType: d.projectType, phase: d.phase, currentType: currentType || null, changes, truncated: d.signals.truncated };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Set the checkbox of each named step to `target[name]` (true/false). Only those lines change, byte for
|
|
120
|
+
// byte otherwise. → names actually changed.
|
|
121
|
+
function applySteps(root, target) {
|
|
122
|
+
const fp = workflowPath(root);
|
|
123
|
+
const lines = fs.readFileSync(fp, 'utf8').split('\n');
|
|
124
|
+
const changed = [];
|
|
125
|
+
for (let i = 0; i < lines.length; i++) {
|
|
126
|
+
const m = lines[i].match(LINE_RE);
|
|
127
|
+
if (!m) continue;
|
|
128
|
+
const name = stepName(m[4]);
|
|
129
|
+
if (!Object.prototype.hasOwnProperty.call(target, name)) continue;
|
|
130
|
+
const want = target[name] ? 'x' : ' ';
|
|
131
|
+
if ((m[2].trim() ? 'x' : ' ') === want) continue;
|
|
132
|
+
lines[i] = m[1] + want + m[3] + m[4] + m[5];
|
|
133
|
+
if (!changed.includes(name)) changed.push(name);
|
|
134
|
+
}
|
|
135
|
+
if (changed.length) fs.writeFileSync(fp, lines.join('\n'));
|
|
136
|
+
return changed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const REASONS = {
|
|
140
|
+
always: 'always useful',
|
|
141
|
+
'design-no-code': 'no code yet',
|
|
142
|
+
'has-code': 'the project has code',
|
|
143
|
+
'has-tests': 'the project has tests',
|
|
144
|
+
'infra-no-tests': 'infrastructure project with no tests',
|
|
145
|
+
'data-quality': 'data project — data quality tests',
|
|
146
|
+
'has-integration-tests': 'integration tests exist',
|
|
147
|
+
'no-integration-tests': 'no integration tests yet',
|
|
148
|
+
'has-e2e-setup': 'an end-to-end test setup exists',
|
|
149
|
+
'no-e2e-setup': 'no end-to-end test setup',
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
module.exports = { scan, detect, suggest, applySteps, currentSteps, STEPS, REASONS, LIMITS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectoflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Agent-agnostic spec-driven development framework + real-time local control plane. Markdown artifacts, intent router, workflow-by-scope.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spec-driven-development",
|
package/templates/SPECTOFLOW.md
CHANGED
|
@@ -85,7 +85,9 @@ whole file. This lets the dashboard and you co-edit without clobbering. Reflect
|
|
|
85
85
|
4. **Gate** — by `mode` (`.spectoflow/config.json`): **autopilot** proceeds · **semi** (default)
|
|
86
86
|
confirms if ambiguous/borderline/risky **and always for a Major** · **manual** confirms each step.
|
|
87
87
|
5. **Load** — read the enabled steps from `.spectoflow/workflow.md` (single source of truth), plus the
|
|
88
|
-
`.spectoflow/skills/` needed for those steps. Load only what this task needs.
|
|
88
|
+
`.spectoflow/skills/` needed for those steps. Load only what this task needs. **If the request needs a
|
|
89
|
+
step that is disabled** (asked to implement while *Develop* is off, writing code while *Unit tests* is
|
|
90
|
+
off…), see *Keep the workflow in step with the project* below — never silently skip it.
|
|
89
91
|
6. **Run** — execute. A **policy gate** (`.spectoflow/policy.md`) can interrupt at any point, any mode.
|
|
90
92
|
|
|
91
93
|
## New / empty project → Intake
|
|
@@ -127,6 +129,25 @@ clarifying question there.
|
|
|
127
129
|
|
|
128
130
|
- The **active workflow** is `.spectoflow/workflow.md` — a checklist of enabled steps, editable (also
|
|
129
131
|
from the dashboard). It is the single source; do not restate workflows elsewhere.
|
|
132
|
+
|
|
133
|
+
### Keep the workflow in step with the project
|
|
134
|
+
|
|
135
|
+
`init` fitted `workflow.md` to what the files showed (is there code yet, tests, an E2E setup, infra or data).
|
|
136
|
+
A file scan can't tell a prototype from a product, or a project still in design from one being built — you
|
|
137
|
+
can, from the docs, specs and plans. Enable or disable a step by changing only its checkbox line.
|
|
138
|
+
|
|
139
|
+
- **First review** — when `.spectoflow/config.json` → `workflowReview` is `"pending"`: early in the session,
|
|
140
|
+
compare the enabled steps with what you understand of the project (phase, type, what the user is doing
|
|
141
|
+
now). Propose the changes that fit, one line of reason each; apply what the user accepts; then set
|
|
142
|
+
`workflowReview` to `"done"`. If nothing needs changing, just say it looks right and set it to `"done"`.
|
|
143
|
+
- **As the project moves on** — when a request needs a step that is disabled:
|
|
144
|
+
- `workflowAutoEnable` is `false` (default): tell the user which step and why, and ask before doing that
|
|
145
|
+
part of the work;
|
|
146
|
+
- `workflowAutoEnable` is `true`: enable that step yourself and tell the user you did.
|
|
147
|
+
- You may **enable** a step on your own only under `workflowAutoEnable: true`. **Disabling** a step always
|
|
148
|
+
goes through the user, whatever the setting.
|
|
149
|
+
- `spectoflow workflow suggest` (and the dashboard's Workflow → *Analyze the project*) re-runs the file
|
|
150
|
+
analysis; point the user to it when the project changed a lot.
|
|
130
151
|
- **Capabilities** (`.spectoflow/capabilities.md`) are a palette; the project type selects the active ones.
|
|
131
152
|
- **Agents** (`.spectoflow/agents/`) are stable team personas (Developer, QA Engineer, …). **Skills**
|
|
132
153
|
(`.spectoflow/skills/`) are the evolving procedures. A workflow step → a capability → its agent →
|