trackops 2.0.3 → 2.0.4
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 +238 -0
- package/lib/init.js +2 -2
- package/lib/locale.js +41 -17
- package/lib/opera-bootstrap.js +68 -7
- package/lib/opera.js +10 -2
- package/lib/registry.js +18 -0
- package/lib/server.js +312 -207
- package/locales/en.json +4 -0
- package/locales/es.json +4 -0
- package/package.json +1 -1
- package/skills/trackops/locales/en/references/activation.md +15 -0
- package/skills/trackops/locales/en/references/troubleshooting.md +12 -0
- package/skills/trackops/references/activation.md +15 -0
- package/skills/trackops/references/troubleshooting.md +12 -0
- package/skills/trackops/skill.json +2 -2
- package/ui/css/base.css +19 -1
- package/ui/css/charts.css +106 -8
- package/ui/css/components.css +554 -17
- package/ui/css/onboarding.css +133 -0
- package/ui/css/panels.css +345 -406
- package/ui/css/terminal.css +125 -0
- package/ui/css/timeline.css +58 -0
- package/ui/css/tokens.css +170 -113
- package/ui/index.html +3 -0
- package/ui/js/api.js +49 -13
- package/ui/js/app.js +28 -32
- package/ui/js/charts.js +526 -0
- package/ui/js/filters.js +247 -0
- package/ui/js/icons.js +82 -57
- package/ui/js/keyboard.js +229 -0
- package/ui/js/onboarding.js +33 -42
- package/ui/js/router.js +20 -3
- package/ui/js/views/board.js +84 -114
- package/ui/js/views/dashboard.js +870 -0
- package/ui/js/views/projects.js +745 -0
- package/ui/js/views/scrum.js +476 -0
- package/ui/js/views/settings.js +197 -247
- package/ui/js/views/sidebar.js +37 -31
- package/ui/js/views/tasks.js +218 -101
- package/ui/js/views/timeline.js +265 -0
- package/ui/js/views/topbar.js +94 -107
- package/ui/app.js +0 -950
- package/ui/js/views/insights.js +0 -340
- package/ui/js/views/overview.js +0 -369
- package/ui/styles.css +0 -688
package/lib/server.js
CHANGED
|
@@ -7,13 +7,13 @@ const os = require("os");
|
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const { spawn, spawnSync } = require("child_process");
|
|
9
9
|
|
|
10
|
-
const config = require("./config");
|
|
11
|
-
const control = require("./control");
|
|
12
|
-
const env = require("./env");
|
|
13
|
-
const registry = require("./registry");
|
|
14
|
-
const { t, setLocale, getMessages } = require("./i18n");
|
|
15
|
-
const { normalizeLocale } = require("./locale");
|
|
16
|
-
const runtimeState = require("./runtime-state");
|
|
10
|
+
const config = require("./config");
|
|
11
|
+
const control = require("./control");
|
|
12
|
+
const env = require("./env");
|
|
13
|
+
const registry = require("./registry");
|
|
14
|
+
const { t, setLocale, getMessages } = require("./i18n");
|
|
15
|
+
const { normalizeLocale } = require("./locale");
|
|
16
|
+
const runtimeState = require("./runtime-state");
|
|
17
17
|
|
|
18
18
|
const UI_DIR = path.join(__dirname, "..", "ui");
|
|
19
19
|
const DEFAULT_HOST = "127.0.0.1";
|
|
@@ -42,19 +42,19 @@ function sendJson(res, statusCode, payload) {
|
|
|
42
42
|
res.end(JSON.stringify(payload));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function sendText(res, statusCode, message) {
|
|
46
|
-
res.writeHead(statusCode, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" });
|
|
47
|
-
res.end(message);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function readJsonFileSafe(filePath) {
|
|
51
|
-
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
52
|
-
try {
|
|
53
|
-
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
54
|
-
} catch (_error) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
45
|
+
function sendText(res, statusCode, message) {
|
|
46
|
+
res.writeHead(statusCode, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" });
|
|
47
|
+
res.end(message);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readJsonFileSafe(filePath) {
|
|
51
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
54
|
+
} catch (_error) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
58
|
|
|
59
59
|
function parseBody(req) {
|
|
60
60
|
return new Promise((resolve, reject) => {
|
|
@@ -365,61 +365,61 @@ function loadControlApi(projectRoot) {
|
|
|
365
365
|
return control.forProject(projectRoot);
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
function buildI18nPayload(controlState) {
|
|
368
|
+
function buildI18nPayload(controlState) {
|
|
369
369
|
const phases = config.getPhases(controlState);
|
|
370
370
|
const locale = config.getLocale(controlState);
|
|
371
371
|
const statusLabels = {};
|
|
372
372
|
for (const s of control.STATUS_ORDER) {
|
|
373
373
|
statusLabels[s] = control.statusLabel(s);
|
|
374
374
|
}
|
|
375
|
-
return { locale, statusLabels, phases, messages: getMessages(locale) };
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function buildOperaState(projectRoot, controlState) {
|
|
379
|
-
const context = config.ensureContext(projectRoot);
|
|
380
|
-
const operaBootstrap = require("./opera-bootstrap");
|
|
381
|
-
const bootstrapState =
|
|
382
|
-
controlState.meta?.opera?.bootstrap ||
|
|
383
|
-
operaBootstrap.detectLegacyBootstrap(context, controlState) ||
|
|
384
|
-
operaBootstrap.createAwaitingBootstrapState(context);
|
|
385
|
-
const qualityReport = readJsonFileSafe(path.join(context.paths.bootstrapDir, "quality-report.json"));
|
|
386
|
-
const localeDoctor = runtimeState.doctorLocale(controlState.meta?.locale || null);
|
|
387
|
-
|
|
388
|
-
return {
|
|
389
|
-
installed: config.isOperaInstalled(controlState),
|
|
390
|
-
version: controlState.meta?.opera?.version || null,
|
|
391
|
-
model: controlState.meta?.opera?.model || null,
|
|
392
|
-
stableTag: controlState.meta?.opera?.stableTag || null,
|
|
393
|
-
contractVersion: controlState.meta?.opera?.contractVersion || null,
|
|
394
|
-
contractReadiness: controlState.meta?.opera?.contractReadiness || qualityReport?.contractReadiness || "hypothesis",
|
|
395
|
-
qualityStatus: controlState.meta?.opera?.qualityStatus || qualityReport?.status || null,
|
|
396
|
-
qualityReport,
|
|
397
|
-
legacyStatus: controlState.meta?.opera?.legacyStatus || bootstrapState?.status || "supported",
|
|
398
|
-
localeSource: localeDoctor.source,
|
|
399
|
-
bootstrap: bootstrapState,
|
|
400
|
-
contractFile: context.paths.contractFile,
|
|
401
|
-
policyFile: context.paths.autonomyPolicyFile,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function getStatePayload(projectRef) {
|
|
406
|
-
const project = resolveProjectEntry(projectRef);
|
|
407
|
-
const api = loadControlApi(project.root);
|
|
408
|
-
const controlState = api.loadControl();
|
|
409
|
-
const runtime = api.refreshRepoRuntime({ quiet: true });
|
|
410
|
-
const envState = env.auditEnvironment(project.root, controlState);
|
|
411
|
-
const operaState = buildOperaState(project.root, controlState);
|
|
412
|
-
|
|
413
|
-
return {
|
|
414
|
-
project,
|
|
415
|
-
control: controlState,
|
|
416
|
-
derived: api.derive(controlState),
|
|
417
|
-
runtime,
|
|
418
|
-
env: envState,
|
|
419
|
-
opera: operaState,
|
|
420
|
-
docsDirty: api.getDocDrift(controlState),
|
|
421
|
-
i18n: buildI18nPayload(controlState),
|
|
422
|
-
generatedAt: new Date().toISOString(),
|
|
375
|
+
return { locale, statusLabels, phases, messages: getMessages(locale) };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function buildOperaState(projectRoot, controlState) {
|
|
379
|
+
const context = config.ensureContext(projectRoot);
|
|
380
|
+
const operaBootstrap = require("./opera-bootstrap");
|
|
381
|
+
const bootstrapState =
|
|
382
|
+
controlState.meta?.opera?.bootstrap ||
|
|
383
|
+
operaBootstrap.detectLegacyBootstrap(context, controlState) ||
|
|
384
|
+
operaBootstrap.createAwaitingBootstrapState(context);
|
|
385
|
+
const qualityReport = readJsonFileSafe(path.join(context.paths.bootstrapDir, "quality-report.json"));
|
|
386
|
+
const localeDoctor = runtimeState.doctorLocale(controlState.meta?.locale || null);
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
installed: config.isOperaInstalled(controlState),
|
|
390
|
+
version: controlState.meta?.opera?.version || null,
|
|
391
|
+
model: controlState.meta?.opera?.model || null,
|
|
392
|
+
stableTag: controlState.meta?.opera?.stableTag || null,
|
|
393
|
+
contractVersion: controlState.meta?.opera?.contractVersion || null,
|
|
394
|
+
contractReadiness: controlState.meta?.opera?.contractReadiness || qualityReport?.contractReadiness || "hypothesis",
|
|
395
|
+
qualityStatus: controlState.meta?.opera?.qualityStatus || qualityReport?.status || null,
|
|
396
|
+
qualityReport,
|
|
397
|
+
legacyStatus: controlState.meta?.opera?.legacyStatus || bootstrapState?.status || "supported",
|
|
398
|
+
localeSource: localeDoctor.source,
|
|
399
|
+
bootstrap: bootstrapState,
|
|
400
|
+
contractFile: context.paths.contractFile,
|
|
401
|
+
policyFile: context.paths.autonomyPolicyFile,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function getStatePayload(projectRef) {
|
|
406
|
+
const project = resolveProjectEntry(projectRef);
|
|
407
|
+
const api = loadControlApi(project.root);
|
|
408
|
+
const controlState = api.loadControl();
|
|
409
|
+
const runtime = api.refreshRepoRuntime({ quiet: true });
|
|
410
|
+
const envState = env.auditEnvironment(project.root, controlState);
|
|
411
|
+
const operaState = buildOperaState(project.root, controlState);
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
project,
|
|
415
|
+
control: controlState,
|
|
416
|
+
derived: api.derive(controlState),
|
|
417
|
+
runtime,
|
|
418
|
+
env: envState,
|
|
419
|
+
opera: operaState,
|
|
420
|
+
docsDirty: api.getDocDrift(controlState),
|
|
421
|
+
i18n: buildI18nPayload(controlState),
|
|
422
|
+
generatedAt: new Date().toISOString(),
|
|
423
423
|
};
|
|
424
424
|
}
|
|
425
425
|
|
|
@@ -650,29 +650,50 @@ async function handleApi(req, res, url) {
|
|
|
650
650
|
if (!body.root) { sendJson(res, 400, { ok: false, error: "Project path required." }); return; }
|
|
651
651
|
try {
|
|
652
652
|
const initMod = require("./init");
|
|
653
|
-
const result = initMod.initProject(body.root, { locale: body.locale || null });
|
|
654
|
-
if (body.withOpera) {
|
|
655
|
-
const opera = require("./opera");
|
|
656
|
-
await opera.install(result.root, {
|
|
657
|
-
locale: body.locale || null,
|
|
658
|
-
bootstrap: body.bootstrap !== false,
|
|
659
|
-
interactive: false,
|
|
660
|
-
answers: body.bootstrapAnswers || {},
|
|
661
|
-
bootstrapMode: body.bootstrapMode || "auto",
|
|
662
|
-
technicalLevel: body.technicalLevel || null,
|
|
663
|
-
projectState: body.projectState || null,
|
|
664
|
-
docsState: body.docsState || null,
|
|
665
|
-
decisionOwnership: body.decisionOwnership || null,
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
const project = registry.registerProject(result.root);
|
|
669
|
-
sendJson(res, 201, { ok: true, project, projects: registry.listProjects() });
|
|
653
|
+
const result = initMod.initProject(body.root, { locale: body.locale || null });
|
|
654
|
+
if (body.withOpera) {
|
|
655
|
+
const opera = require("./opera");
|
|
656
|
+
await opera.install(result.root, {
|
|
657
|
+
locale: body.locale || null,
|
|
658
|
+
bootstrap: body.bootstrap !== false,
|
|
659
|
+
interactive: false,
|
|
660
|
+
answers: body.bootstrapAnswers || {},
|
|
661
|
+
bootstrapMode: body.bootstrapMode || "auto",
|
|
662
|
+
technicalLevel: body.technicalLevel || null,
|
|
663
|
+
projectState: body.projectState || null,
|
|
664
|
+
docsState: body.docsState || null,
|
|
665
|
+
decisionOwnership: body.decisionOwnership || null,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
const project = registry.registerProject(result.root);
|
|
669
|
+
sendJson(res, 201, { ok: true, project, projects: registry.listProjects() });
|
|
670
670
|
} catch (err) {
|
|
671
671
|
sendJson(res, 500, { ok: false, error: err.message });
|
|
672
672
|
}
|
|
673
673
|
return;
|
|
674
674
|
}
|
|
675
675
|
|
|
676
|
+
/* POST /api/projects/purge-unavailable — Elimina todos los proyectos no disponibles */
|
|
677
|
+
if (req.method === "POST" && url.pathname === "/api/projects/purge-unavailable") {
|
|
678
|
+
try {
|
|
679
|
+
const { removed } = registry.purgeUnavailable();
|
|
680
|
+
sendJson(res, 200, { ok: true, removed, projects: registry.listProjects() });
|
|
681
|
+
} catch (err) {
|
|
682
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/* DELETE /api/projects/:id — Desregistrar proyecto */
|
|
688
|
+
if (req.method === "DELETE" && /^\/api\/projects\/[^/]+$/.test(url.pathname)) {
|
|
689
|
+
const projectId = decodeURIComponent(url.pathname.split("/").pop());
|
|
690
|
+
const exists = registry.loadRegistry().projects.find((p) => p.id === projectId);
|
|
691
|
+
if (!exists) { sendJson(res, 404, { ok: false, error: "Project not found." }); return; }
|
|
692
|
+
registry.unregisterById(projectId);
|
|
693
|
+
sendJson(res, 200, { ok: true, projects: registry.listProjects() });
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
676
697
|
if (req.method === "GET" && url.pathname === "/api/state") {
|
|
677
698
|
sendJson(res, 200, getStatePayload(url.searchParams.get("project")));
|
|
678
699
|
return;
|
|
@@ -720,7 +741,7 @@ async function handleApi(req, res, url) {
|
|
|
720
741
|
return;
|
|
721
742
|
}
|
|
722
743
|
|
|
723
|
-
if (req.method === "POST" && url.pathname === "/api/sync") {
|
|
744
|
+
if (req.method === "POST" && url.pathname === "/api/sync") {
|
|
724
745
|
const body = await parseBody(req);
|
|
725
746
|
const project = resolveProjectEntry(body.projectId || body.project || url.searchParams.get("project"));
|
|
726
747
|
const api = loadControlApi(project.root);
|
|
@@ -728,116 +749,200 @@ async function handleApi(req, res, url) {
|
|
|
728
749
|
api.syncDocs(controlState);
|
|
729
750
|
api.refreshRepoRuntime({ quiet: true });
|
|
730
751
|
sendJson(res, 200, { ok: true, state: getStatePayload(project.id) });
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* GET /api/analytics — Datos analíticos computados */
|
|
756
|
+
if (req.method === "GET" && url.pathname === "/api/analytics") {
|
|
757
|
+
const project = resolveProjectEntry(url.searchParams.get("project"));
|
|
758
|
+
const api = loadControlApi(project.root);
|
|
759
|
+
const controlState = api.loadControl();
|
|
760
|
+
const range = url.searchParams.get("range") || "30";
|
|
761
|
+
const tasks = controlState.tasks || [];
|
|
762
|
+
const now = new Date();
|
|
763
|
+
|
|
764
|
+
// Rango de fechas
|
|
765
|
+
const rangeDays = range === "all" ? 365 : parseInt(range, 10) || 30;
|
|
766
|
+
const startDate = new Date(now);
|
|
767
|
+
startDate.setDate(startDate.getDate() - rangeDays);
|
|
768
|
+
const startISO = startDate.toISOString().slice(0, 10);
|
|
769
|
+
|
|
770
|
+
// Velocity: completions por semana
|
|
771
|
+
const velocity = [];
|
|
772
|
+
const completions = tasks.flatMap(t =>
|
|
773
|
+
(t.history || []).filter(h => h.action === "complete" && h.at >= startISO).map(h => h.at.slice(0, 10))
|
|
774
|
+
);
|
|
775
|
+
const weekMap = new Map();
|
|
776
|
+
completions.forEach(dateStr => {
|
|
777
|
+
const d = new Date(dateStr);
|
|
778
|
+
const year = d.getFullYear();
|
|
779
|
+
const jan1 = new Date(year, 0, 1);
|
|
780
|
+
const weekNum = Math.ceil(((d - jan1) / 86400000 + jan1.getDay() + 1) / 7);
|
|
781
|
+
const weekKey = `${year}-W${String(weekNum).padStart(2, "0")}`;
|
|
782
|
+
weekMap.set(weekKey, (weekMap.get(weekKey) || 0) + 1);
|
|
783
|
+
});
|
|
784
|
+
for (const [week, completed] of [...weekMap.entries()].sort()) {
|
|
785
|
+
velocity.push({ week, completed });
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Burndown: remaining required tasks over time
|
|
789
|
+
const burndown = [];
|
|
790
|
+
const requiredTasks = tasks.filter(t => t.required !== false);
|
|
791
|
+
for (let d = new Date(startDate); d <= now; d.setDate(d.getDate() + 1)) {
|
|
792
|
+
const dateStr = d.toISOString().slice(0, 10);
|
|
793
|
+
const remaining = requiredTasks.filter(t => {
|
|
794
|
+
const completeEntry = (t.history || []).find(h => h.action === "complete");
|
|
795
|
+
return !completeEntry || completeEntry.at.slice(0, 10) > dateStr;
|
|
796
|
+
}).length;
|
|
797
|
+
burndown.push({ date: dateStr, remaining });
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Heatmap: activity counts per day
|
|
801
|
+
const heatmap = [];
|
|
802
|
+
const dayMap = new Map();
|
|
803
|
+
tasks.forEach(t => {
|
|
804
|
+
(t.history || []).forEach(h => {
|
|
805
|
+
if (h.at >= startISO) {
|
|
806
|
+
const day = h.at.slice(0, 10);
|
|
807
|
+
dayMap.set(day, (dayMap.get(day) || 0) + 1);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
for (const [date, count] of [...dayMap.entries()].sort()) {
|
|
812
|
+
heatmap.push({ date, count });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Average cycle time (start → complete)
|
|
816
|
+
let totalCycle = 0, cycleCount = 0;
|
|
817
|
+
tasks.forEach(t => {
|
|
818
|
+
const startEntry = (t.history || []).find(h => h.action === "start" || h.action === "create");
|
|
819
|
+
const completeEntry = (t.history || []).find(h => h.action === "complete");
|
|
820
|
+
if (startEntry && completeEntry) {
|
|
821
|
+
totalCycle += new Date(completeEntry.at) - new Date(startEntry.at);
|
|
822
|
+
cycleCount++;
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
const avgCycleTime = cycleCount > 0 ? Math.round(totalCycle / cycleCount) : 0;
|
|
826
|
+
|
|
827
|
+
sendJson(res, 200, {
|
|
828
|
+
ok: true,
|
|
829
|
+
velocity,
|
|
830
|
+
burndown: burndown.filter((_, i) => i % Math.max(1, Math.floor(burndown.length / 60)) === 0), // Max ~60 points
|
|
831
|
+
heatmap,
|
|
832
|
+
avgCycleTime,
|
|
833
|
+
range,
|
|
834
|
+
generatedAt: now.toISOString(),
|
|
835
|
+
});
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (req.method === "GET" && url.pathname === "/api/env") {
|
|
840
|
+
const project = resolveProjectEntry(url.searchParams.get("project"));
|
|
841
|
+
const api = loadControlApi(project.root);
|
|
842
|
+
const controlState = api.loadControl();
|
|
843
|
+
sendJson(res, 200, env.auditEnvironment(project.root, controlState));
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (req.method === "POST" && url.pathname === "/api/env/sync") {
|
|
848
|
+
const body = await parseBody(req);
|
|
849
|
+
const project = resolveProjectEntry(body.projectId || body.project || url.searchParams.get("project"));
|
|
850
|
+
const api = loadControlApi(project.root);
|
|
851
|
+
const controlState = api.loadControl();
|
|
852
|
+
const result = env.syncEnvironment(project.root, controlState);
|
|
853
|
+
api.syncDocs(api.loadControl());
|
|
854
|
+
api.refreshRepoRuntime({ quiet: true });
|
|
855
|
+
sendJson(res, 200, result);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (req.method === "GET" && url.pathname === "/api/opera/bootstrap") {
|
|
860
|
+
const project = resolveProjectEntry(url.searchParams.get("project"));
|
|
861
|
+
const api = loadControlApi(project.root);
|
|
862
|
+
const controlState = api.loadControl();
|
|
863
|
+
const operaState = buildOperaState(project.root, controlState);
|
|
864
|
+
const bootstrap = operaState.bootstrap;
|
|
865
|
+
sendJson(res, 200, {
|
|
866
|
+
ok: true,
|
|
867
|
+
mode: bootstrap.mode || null,
|
|
868
|
+
status: bootstrap.status || "awaiting_intake",
|
|
869
|
+
technicalLevel: bootstrap.technicalLevel || null,
|
|
870
|
+
projectState: bootstrap.projectState || null,
|
|
871
|
+
documentationState: bootstrap.documentationState || null,
|
|
872
|
+
decisionOwnership: bootstrap.decisionOwnership || null,
|
|
873
|
+
handoffFiles: bootstrap.handoffFiles || null,
|
|
874
|
+
intakeFiles: bootstrap.intakeFiles || null,
|
|
875
|
+
reviewFiles: bootstrap.reviewFiles || null,
|
|
876
|
+
contractVersion: operaState.contractVersion,
|
|
877
|
+
contractReadiness: operaState.contractReadiness,
|
|
878
|
+
legacyStatus: operaState.legacyStatus,
|
|
879
|
+
qualityReport: operaState.qualityReport,
|
|
880
|
+
});
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (req.method === "GET" && url.pathname === "/api/opera/handoff") {
|
|
885
|
+
const project = resolveProjectEntry(url.searchParams.get("project"));
|
|
886
|
+
const context = config.ensureContext(project.root);
|
|
887
|
+
const operaBootstrap = require("./opera-bootstrap");
|
|
888
|
+
const files = operaBootstrap.bootstrapFilePaths(context);
|
|
889
|
+
let handoffJson = null;
|
|
890
|
+
if (fs.existsSync(files.json)) {
|
|
891
|
+
try {
|
|
892
|
+
handoffJson = JSON.parse(fs.readFileSync(files.json, "utf8"));
|
|
893
|
+
} catch (_error) {
|
|
894
|
+
handoffJson = null;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
sendJson(res, 200, {
|
|
898
|
+
ok: true,
|
|
899
|
+
markdownFile: files.markdown,
|
|
900
|
+
jsonFile: files.json,
|
|
901
|
+
openQuestionsFile: files.openQuestions,
|
|
902
|
+
qualityReportFile: files.qualityReport,
|
|
903
|
+
markdown: fs.existsSync(files.markdown) ? fs.readFileSync(files.markdown, "utf8") : "",
|
|
904
|
+
json: handoffJson,
|
|
905
|
+
openQuestions: fs.existsSync(files.openQuestions) ? fs.readFileSync(files.openQuestions, "utf8") : "",
|
|
906
|
+
qualityReport: readJsonFileSafe(files.qualityReport),
|
|
907
|
+
});
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (req.method === "POST" && url.pathname === "/api/opera/bootstrap/intake") {
|
|
912
|
+
const body = await parseBody(req);
|
|
913
|
+
const project = resolveProjectEntry(body.projectId || body.project || url.searchParams.get("project"));
|
|
914
|
+
const opera = require("./opera");
|
|
915
|
+
const profile = await opera.runBootstrap(project.root, {
|
|
916
|
+
interactive: false,
|
|
917
|
+
bootstrapMode: body.bootstrapMode || "auto",
|
|
918
|
+
technicalLevel: body.technicalLevel || null,
|
|
919
|
+
projectState: body.projectState || null,
|
|
920
|
+
docsState: body.documentationState || body.docsState || null,
|
|
921
|
+
decisionOwnership: body.decisionOwnership || null,
|
|
922
|
+
answers: body.answers || {},
|
|
923
|
+
});
|
|
924
|
+
sendJson(res, 200, { ok: true, profile, state: getStatePayload(project.id) });
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (req.method === "GET" && url.pathname === "/api/opera/status") {
|
|
929
|
+
const project = resolveProjectEntry(url.searchParams.get("project"));
|
|
930
|
+
const api = loadControlApi(project.root);
|
|
931
|
+
const controlState = api.loadControl();
|
|
932
|
+
sendJson(res, 200, { ok: true, ...buildOperaState(project.root, controlState) });
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (req.method === "POST" && url.pathname === "/api/opera/bootstrap/resume") {
|
|
937
|
+
const body = await parseBody(req);
|
|
938
|
+
const project = resolveProjectEntry(body.projectId || body.project || url.searchParams.get("project"));
|
|
939
|
+
const opera = require("./opera");
|
|
940
|
+
const profile = await opera.runBootstrap(project.root, { interactive: false, resume: true });
|
|
941
|
+
sendJson(res, 200, { ok: true, profile, state: getStatePayload(project.id) });
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (req.method === "POST" && url.pathname === "/api/commands") {
|
|
841
946
|
const body = await parseBody(req);
|
|
842
947
|
const project = resolveProjectEntry(body.projectId || body.project || url.searchParams.get("project"));
|
|
843
948
|
const commandText = String(body.command || "").trim();
|
|
@@ -921,10 +1026,10 @@ async function handleApi(req, res, url) {
|
|
|
921
1026
|
|
|
922
1027
|
/* ── Skills Hub ── */
|
|
923
1028
|
|
|
924
|
-
if (req.method === "GET" && url.pathname === "/api/skills/local") {
|
|
925
|
-
const project = resolveProjectEntry(url.searchParams.get("project"));
|
|
926
|
-
const context = config.ensureContext(project.root);
|
|
927
|
-
const skillsDir = fs.existsSync(context.paths.skillsDir) ? context.paths.skillsDir : null;
|
|
1029
|
+
if (req.method === "GET" && url.pathname === "/api/skills/local") {
|
|
1030
|
+
const project = resolveProjectEntry(url.searchParams.get("project"));
|
|
1031
|
+
const context = config.ensureContext(project.root);
|
|
1032
|
+
const skillsDir = fs.existsSync(context.paths.skillsDir) ? context.paths.skillsDir : null;
|
|
928
1033
|
|
|
929
1034
|
const skills = [];
|
|
930
1035
|
if (skillsDir) {
|
|
@@ -961,7 +1066,7 @@ async function handleApi(req, res, url) {
|
|
|
961
1066
|
const catalog = [
|
|
962
1067
|
{ id: "changelog-updater", title: "Changelog Updater", description: "Mantiene automatizado el CHANGELOG basado en commits.", url: "https://skills.sh/changelog-updater.md" },
|
|
963
1068
|
{ id: "commiter", title: "Git Commiter", description: "Genera mensajes de commit strictos siguiendo Conventional Commits y Emojis.", url: "https://skills.sh/commiter.md" },
|
|
964
|
-
{ id: "project-starter-skill", title: "Project Starter", description: "Skill para discovery y estructuracion inicial guiada con TrackOps y OPERA.", url: "https://skills.sh/project-starter.md" },
|
|
1069
|
+
{ id: "project-starter-skill", title: "Project Starter", description: "Skill para discovery y estructuracion inicial guiada con TrackOps y OPERA.", url: "https://skills.sh/project-starter.md" },
|
|
965
1070
|
{ id: "tdd-master", title: "TDD Master", description: "Fuerza el ciclo Red-Green-Refactor en las implementaciones.", url: "https://skills.sh/tdd-master.md" },
|
|
966
1071
|
{ id: "e2e-tester", title: "E2E Tester", description: "Plantillas y comandos para frameworks de Test End-to-End.", url: "https://skills.sh/e2e-tester.md" }
|
|
967
1072
|
];
|
|
@@ -972,16 +1077,16 @@ async function handleApi(req, res, url) {
|
|
|
972
1077
|
return;
|
|
973
1078
|
}
|
|
974
1079
|
|
|
975
|
-
if (req.method === "POST" && url.pathname === "/api/skills/install") {
|
|
1080
|
+
if (req.method === "POST" && url.pathname === "/api/skills/install") {
|
|
976
1081
|
const body = await parseBody(req);
|
|
977
1082
|
const project = resolveProjectEntry(body.projectId || body.project || url.searchParams.get("project"));
|
|
978
1083
|
const skillId = body.skillId;
|
|
979
1084
|
|
|
980
1085
|
if (!skillId) { sendJson(res, 400, { ok: false, error: "Missing skillId parameter" }); return; }
|
|
981
1086
|
|
|
982
|
-
const context = config.ensureContext(project.root);
|
|
983
|
-
const skillsDir = context.paths.skillsDir;
|
|
984
|
-
if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
|
|
1087
|
+
const context = config.ensureContext(project.root);
|
|
1088
|
+
const skillsDir = context.paths.skillsDir;
|
|
1089
|
+
if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
|
|
985
1090
|
|
|
986
1091
|
const targetSkillDir = path.join(skillsDir, skillId);
|
|
987
1092
|
if (!fs.existsSync(targetSkillDir)) fs.mkdirSync(targetSkillDir, { recursive: true });
|
|
@@ -1012,7 +1117,7 @@ Instructions para el agente relativas a esta skill...
|
|
|
1012
1117
|
/* ── server start ── */
|
|
1013
1118
|
|
|
1014
1119
|
async function run(args = []) {
|
|
1015
|
-
startupRoot = config.resolveProjectRoot() || process.cwd();
|
|
1120
|
+
startupRoot = config.resolveProjectRoot() || process.cwd();
|
|
1016
1121
|
|
|
1017
1122
|
try {
|
|
1018
1123
|
const ctrl = config.loadControl(startupRoot);
|
package/locales/en.json
CHANGED
|
@@ -229,6 +229,7 @@
|
|
|
229
229
|
|
|
230
230
|
"bootstrap.header": "OPERA bootstrap",
|
|
231
231
|
"bootstrap.subtitle": "Classify the user and the project first. TrackOps will decide whether to continue in the terminal or route discovery to the agent.",
|
|
232
|
+
"bootstrap.instructions": "Answer with low|medium|high|senior, or press Enter to keep the default value when you are unsure.",
|
|
232
233
|
"bootstrap.question.technicalLevel": "User technical level",
|
|
233
234
|
"bootstrap.question.projectState": "Current project state",
|
|
234
235
|
"bootstrap.question.docsState": "Available documentation",
|
|
@@ -250,6 +251,9 @@
|
|
|
250
251
|
"bootstrap.pending": "OPERA bootstrap saved as pending. Resume with 'trackops opera bootstrap --resume'.",
|
|
251
252
|
"bootstrap.awaitingAgent": "OPERA bootstrap has been routed to the agent. Complete the handoff and resume with 'trackops opera bootstrap --resume'.",
|
|
252
253
|
"bootstrap.handoffFile": "Handoff file",
|
|
254
|
+
"bootstrap.next.handoff": "Next step: run 'trackops opera handoff --print', paste the context into your agent, and come back when ops/bootstrap/intake.json and ops/bootstrap/spec-dossier.md exist.",
|
|
255
|
+
"bootstrap.next.directCompleted": "Next step: review 'trackops opera status' and continue normal work with 'trackops next' and 'trackops sync'.",
|
|
256
|
+
"bootstrap.next.directPending": "Next step: complete the missing data and resume with 'trackops opera bootstrap --resume'.",
|
|
253
257
|
"bootstrap.infer.envSourceHint": "Environment files detected",
|
|
254
258
|
"bootstrap.noneDefined": "Not defined yet.",
|
|
255
259
|
"bootstrap.pendingValue": "Pending definition.",
|
package/locales/es.json
CHANGED
|
@@ -229,6 +229,7 @@
|
|
|
229
229
|
|
|
230
230
|
"bootstrap.header": "Bootstrap OPERA",
|
|
231
231
|
"bootstrap.subtitle": "Primero clasifica al usuario y el estado del proyecto. TrackOps decidira si continuar por terminal o derivar el arranque al agente.",
|
|
232
|
+
"bootstrap.instructions": "Responde con low|medium|high|senior o con sus equivalentes bajo|medio|alto. Si no sabes una respuesta, pulsa Enter para aceptar el valor por defecto.",
|
|
232
233
|
"bootstrap.question.technicalLevel": "Nivel tecnico del usuario",
|
|
233
234
|
"bootstrap.question.projectState": "Estado actual del proyecto",
|
|
234
235
|
"bootstrap.question.docsState": "Documentacion disponible",
|
|
@@ -250,6 +251,9 @@
|
|
|
250
251
|
"bootstrap.pending": "Bootstrap OPERA guardado como pendiente. Reanuda con 'trackops opera bootstrap --resume'.",
|
|
251
252
|
"bootstrap.awaitingAgent": "Bootstrap OPERA derivado al agente. Completa el handoff y reanuda con 'trackops opera bootstrap --resume'.",
|
|
252
253
|
"bootstrap.handoffFile": "Archivo de handoff",
|
|
254
|
+
"bootstrap.next.handoff": "Siguiente paso: ejecuta 'trackops opera handoff --print', pega el contexto en tu agente y vuelve cuando existan ops/bootstrap/intake.json y ops/bootstrap/spec-dossier.md.",
|
|
255
|
+
"bootstrap.next.directCompleted": "Siguiente paso: revisa 'trackops opera status' y continua el trabajo normal con 'trackops next' y 'trackops sync'.",
|
|
256
|
+
"bootstrap.next.directPending": "Siguiente paso: completa los datos pendientes y reanuda con 'trackops opera bootstrap --resume'.",
|
|
253
257
|
"bootstrap.infer.envSourceHint": "Detectados archivos de entorno",
|
|
254
258
|
"bootstrap.noneDefined": "Aun no definido.",
|
|
255
259
|
"bootstrap.pendingValue": "Pendiente de definir.",
|