trackops 2.0.6 → 2.2.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.
Files changed (60) hide show
  1. package/README.md +307 -701
  2. package/bin/trackops.js +24 -16
  3. package/lib/config.js +265 -58
  4. package/lib/control.js +830 -292
  5. package/lib/init.js +46 -16
  6. package/lib/opera-bootstrap.js +85 -45
  7. package/lib/opera-phase-dod.js +485 -0
  8. package/lib/opera.js +8 -5
  9. package/lib/plans.js +1329 -0
  10. package/lib/quality-assert.js +49 -0
  11. package/lib/quality.js +1759 -0
  12. package/lib/release.js +18 -11
  13. package/lib/server.js +504 -192
  14. package/lib/skills.js +94 -41
  15. package/locales/en.json +249 -15
  16. package/locales/es.json +249 -15
  17. package/package.json +3 -2
  18. package/scripts/quality-unit-tests.js +130 -0
  19. package/scripts/skills-marketplace-smoke.js +156 -124
  20. package/scripts/smoke-tests.js +378 -71
  21. package/scripts/sync-skill-version.js +29 -19
  22. package/scripts/validate-skill.js +188 -103
  23. package/skills/trackops/SKILL.md +25 -7
  24. package/skills/trackops/locales/en/SKILL.md +25 -7
  25. package/skills/trackops/locales/en/references/activation.md +3 -3
  26. package/skills/trackops/locales/en/references/workflow.md +5 -4
  27. package/skills/trackops/references/activation.md +3 -3
  28. package/skills/trackops/references/workflow.md +5 -4
  29. package/skills/trackops/skill.json +29 -29
  30. package/skills/trackops-quality-guard/SKILL.md +78 -0
  31. package/skills/trackops-quality-guard/agents/openai.yaml +7 -0
  32. package/skills/trackops-quality-guard/locales/en/SKILL.md +78 -0
  33. package/skills/trackops-quality-guard/locales/en/references/commands.md +36 -0
  34. package/skills/trackops-quality-guard/locales/en/references/decision-policy.md +16 -0
  35. package/skills/trackops-quality-guard/locales/en/references/output-format.md +24 -0
  36. package/skills/trackops-quality-guard/references/commands.md +36 -0
  37. package/skills/trackops-quality-guard/references/decision-policy.md +16 -0
  38. package/skills/trackops-quality-guard/references/output-format.md +24 -0
  39. package/skills/trackops-quality-guard/skill.json +28 -0
  40. package/templates/skills/opera-skill/SKILL.md +12 -0
  41. package/templates/skills/opera-skill/locales/en/SKILL.md +12 -0
  42. package/templates/skills/trackops-quality-guard/SKILL.md +72 -0
  43. package/templates/skills/trackops-quality-guard/locales/en/SKILL.md +72 -0
  44. package/templates/skills/trackops-quality-guard/locales/en/references/commands.md +30 -0
  45. package/templates/skills/trackops-quality-guard/locales/en/references/decision-policy.md +14 -0
  46. package/templates/skills/trackops-quality-guard/locales/en/references/output-format.md +21 -0
  47. package/templates/skills/trackops-quality-guard/references/commands.md +30 -0
  48. package/templates/skills/trackops-quality-guard/references/decision-policy.md +14 -0
  49. package/templates/skills/trackops-quality-guard/references/output-format.md +21 -0
  50. package/ui/js/api.js +93 -26
  51. package/ui/js/app.js +13 -7
  52. package/ui/js/filters.js +49 -29
  53. package/ui/js/time-tracker.js +41 -28
  54. package/ui/js/views/board.js +22 -14
  55. package/ui/js/views/dashboard.js +206 -49
  56. package/ui/js/views/execution.js +7 -3
  57. package/ui/js/views/plans.js +284 -0
  58. package/ui/js/views/scrum.js +25 -13
  59. package/ui/js/views/sidebar.js +9 -8
  60. package/ui/js/views/tasks.js +238 -134
@@ -8,9 +8,10 @@ const os = require("os");
8
8
  const path = require("path");
9
9
  const { spawn, spawnSync } = require("child_process");
10
10
 
11
- const ROOT = path.resolve(__dirname, "..");
12
- const BIN = path.join(ROOT, "bin", "trackops.js");
13
- const SKILL_VALIDATE = path.join(ROOT, "scripts", "validate-skill.js");
11
+ const ROOT = path.resolve(__dirname, "..");
12
+ const BIN = path.join(ROOT, "bin", "trackops.js");
13
+ const SKILL_VALIDATE = path.join(ROOT, "scripts", "validate-skill.js");
14
+ const controlLib = require(path.join(ROOT, "lib", "control.js"));
14
15
 
15
16
  function getNpmCommand() {
16
17
  return process.platform === "win32" ? "npm.cmd" : "npm";
@@ -72,17 +73,41 @@ function wait(ms) {
72
73
  return new Promise((resolve) => setTimeout(resolve, ms));
73
74
  }
74
75
 
75
- function get(port, pathname, host = "127.0.0.1") {
76
- return new Promise((resolve, reject) => {
77
- const req = http.get({ host, port, path: pathname }, (res) => {
78
- let body = "";
79
- res.setEncoding("utf8");
76
+ function get(port, pathname, host = "127.0.0.1") {
77
+ return new Promise((resolve, reject) => {
78
+ const req = http.get({ host, port, path: pathname }, (res) => {
79
+ let body = "";
80
+ res.setEncoding("utf8");
80
81
  res.on("data", (chunk) => { body += chunk; });
81
82
  res.on("end", () => resolve({ status: res.statusCode, body }));
82
83
  });
83
- req.on("error", reject);
84
- });
85
- }
84
+ req.on("error", reject);
85
+ });
86
+ }
87
+
88
+ function requestJson(method, port, pathname, payload = null, host = "127.0.0.1") {
89
+ return new Promise((resolve, reject) => {
90
+ const body = payload == null ? "" : JSON.stringify(payload);
91
+ const req = http.request({
92
+ host,
93
+ port,
94
+ path: pathname,
95
+ method,
96
+ headers: {
97
+ "Content-Type": "application/json; charset=utf-8",
98
+ "Content-Length": Buffer.byteLength(body),
99
+ },
100
+ }, (res) => {
101
+ let responseBody = "";
102
+ res.setEncoding("utf8");
103
+ res.on("data", (chunk) => { responseBody += chunk; });
104
+ res.on("end", () => resolve({ status: res.statusCode, body: responseBody }));
105
+ });
106
+ req.on("error", reject);
107
+ if (body) req.write(body);
108
+ req.end();
109
+ });
110
+ }
86
111
 
87
112
  function extractLocalPort(output) {
88
113
  const match = String(output || "").match(/- Local:\s+http:\/\/[^\s:]+:(\d+)/);
@@ -315,13 +340,18 @@ async function main() {
315
340
  assert.ok(fs.existsSync(path.join(nonEmptyProject, "app", "package.json")));
316
341
  assert.ok(fs.existsSync(path.join(nonEmptyProject, "ops", "project_control.json")));
317
342
 
318
- writeJson(path.join(splitProject, "app", "package.json"), {
319
- name: "split-demo",
320
- version: "1.0.0",
321
- dependencies: { openai: "^4.0.0" },
322
- scripts: { test: "echo ok" },
323
- });
324
- runNode([BIN, "opera", "install", "--locale", "en", "--non-interactive"], splitProject);
343
+ writeJson(path.join(splitProject, "app", "package.json"), {
344
+ name: "split-demo",
345
+ version: "1.0.0",
346
+ dependencies: { openai: "^4.0.0" },
347
+ scripts: { test: "echo ok" },
348
+ });
349
+ const skillCatalog = runNode([BIN, "skill", "catalog"], splitProject);
350
+ assert.match(skillCatalog, /trackops-quality-guard/);
351
+ assert.doesNotMatch(skillCatalog, /opera-quality-guard/);
352
+ runNode([BIN, "skill", "install", "trackops-quality-guard"], splitProject);
353
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "trackops-quality-guard", "SKILL.md")));
354
+ runNode([BIN, "opera", "install", "--locale", "en", "--non-interactive"], splitProject);
325
355
 
326
356
  assert.ok(fs.existsSync(path.join(splitProject, "ops", "genesis.md")));
327
357
  assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agent", "hub", "agent.md")));
@@ -342,13 +372,15 @@ async function main() {
342
372
  assert.strictEqual(operaControl.meta.opera.installed, true);
343
373
  assert.strictEqual(operaControl.meta.opera.bootstrap.mode, "agent_handoff");
344
374
  assert.strictEqual(operaControl.meta.opera.bootstrap.status, "awaiting_agent");
345
- assert.ok(operaControl.meta.opera.skills.includes("opera-skill"));
346
- assert.ok(operaControl.meta.opera.skills.includes("project-starter-skill"));
347
- assert.ok(operaControl.meta.opera.skills.includes("opera-contract-auditor"));
348
- assert.ok(operaControl.meta.opera.skills.includes("opera-policy-guard"));
349
- assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "opera-skill", "SKILL.md")));
350
- assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "opera-skill", "references", "phase-dod.md")));
351
- assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "project-starter-skill", "references", "opera-cycle.md")));
375
+ assert.ok(operaControl.meta.opera.skills.includes("opera-skill"));
376
+ assert.ok(operaControl.meta.opera.skills.includes("trackops-quality-guard"));
377
+ assert.ok(operaControl.meta.opera.skills.includes("project-starter-skill"));
378
+ assert.ok(operaControl.meta.opera.skills.includes("opera-contract-auditor"));
379
+ assert.ok(operaControl.meta.opera.skills.includes("opera-policy-guard"));
380
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "opera-skill", "SKILL.md")));
381
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "trackops-quality-guard", "SKILL.md")));
382
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "opera-skill", "references", "phase-dod.md")));
383
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "project-starter-skill", "references", "opera-cycle.md")));
352
384
  assert.ok(operaControl.meta.environment.requiredKeys.includes("OPENAI_API_KEY"));
353
385
  const envRootText = fs.readFileSync(path.join(splitProject, ".env"), "utf8");
354
386
  assert.match(envRootText, /OPENAI_API_KEY=/);
@@ -399,6 +431,10 @@ async function main() {
399
431
  const plainStatus = runNode([BIN, "--plain", "status"], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
400
432
  assert.match(plainStatus, /\[BLOCKED\]|\[PENDING\]/);
401
433
  assert.doesNotMatch(plainStatus, /\u2500|\u23F3|\u26D4|\u2705/);
434
+ const blockedResume = runNode([BIN, "opera", "bootstrap", "--resume"], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
435
+ assert.match(blockedResume, /quality is BLOCKED|calidad esta BLOQUEADA/i);
436
+ assert.match(blockedResume, /Problem statement|Problema principal/i);
437
+ assert.match(blockedResume, /Target user|Usuario objetivo/i);
402
438
 
403
439
  const promptProject = path.join(tempRoot, "prompt-demo");
404
440
  fs.mkdirSync(promptProject, { recursive: true });
@@ -441,30 +477,164 @@ async function main() {
441
477
  assert.ok(resumedControl.meta.environment.requiredKeys.includes("STRIPE_SECRET_KEY"));
442
478
  assert.ok(fs.existsSync(path.join(splitProject, "ops", "contract", "operating-contract.json")));
443
479
  const operatingContract = readJson(path.join(splitProject, "ops", "contract", "operating-contract.json"));
444
- assert.strictEqual(operatingContract.version, 3);
445
- assert.strictEqual(operatingContract.userModel.language, "en");
446
- assert.strictEqual(operatingContract.userModel.decisionOwnership, "agent");
447
-
448
- const defaultDashboard = startDashboard(splitProject);
449
-
450
- try {
451
- const ready = await waitForDashboard(defaultDashboard);
480
+ assert.strictEqual(operatingContract.version, 3);
481
+ assert.strictEqual(operatingContract.userModel.language, "en");
482
+ assert.strictEqual(operatingContract.userModel.decisionOwnership, "agent");
483
+
484
+ const planFile = path.join(splitProject, "docs", "implementation-plan.md");
485
+ fs.mkdirSync(path.dirname(planFile), { recursive: true });
486
+ fs.writeFileSync(
487
+ planFile,
488
+ "# Implementation plan\n\n## Booking delivery\n- [P0] Booking orchestration [phase: E]\n - [P1] Define payment payload [phase: P]\n - [P0] Implement Stripe checkout [phase: E]\n- [P2] Release automation [phase: A]\n",
489
+ "utf8",
490
+ );
491
+
492
+ const scanPlans = JSON.parse(runNode([BIN, "plan", "scan", "--json"], splitProject));
493
+ assert.ok(scanPlans.candidates.some((candidate) => candidate.path === "docs/implementation-plan.md"));
494
+
495
+ const importedPlan = JSON.parse(runNode([BIN, "plan", "import", "--file", "docs/implementation-plan.md", "--source-id", "booking-plan", "--json"], splitProject));
496
+ assert.strictEqual(importedPlan.source.id, "booking-plan");
497
+ assert.ok(importedPlan.preview.summary.create >= 3);
498
+ const controlAfterImport = readJson(path.join(splitProject, "ops", "project_control.json"));
499
+ assert.strictEqual(controlAfterImport.meta.controlVersion, 3);
500
+ assert.ok(controlAfterImport.meta.plans.sources.some((source) => source.id === "booking-plan"));
501
+ assert.ok(!controlAfterImport.tasks.some((task) => task.origin?.sourceId === "booking-plan"));
502
+
503
+ const appliedPlan = JSON.parse(runNode([BIN, "plan", "apply", "booking-plan", "--json"], splitProject));
504
+ assert.strictEqual(appliedPlan.source.id, "booking-plan");
505
+ assert.ok(appliedPlan.preview.summary.managedTaskCount >= 3);
506
+ const controlAfterPlan = readJson(path.join(splitProject, "ops", "project_control.json"));
507
+ const importedTasks = controlAfterPlan.tasks.filter((task) => task.origin?.sourceId === "booking-plan");
508
+ assert.ok(importedTasks.length >= 3);
509
+ assert.ok(importedTasks.some((task) => task.parentId));
510
+ assert.ok(importedTasks.some((task) => Number.isFinite(Number(task.sequence))));
511
+ const derivedAfterPlan = controlLib.derive(controlAfterPlan);
512
+ assert.ok(derivedAfterPlan.tasks.some((task) => task.isParent));
513
+ assert.ok(derivedAfterPlan.actionableTasks.every((task) => task.isLeaf));
514
+ assert.ok(derivedAfterPlan.readyTasks.every((task) => task.isLeaf));
515
+
516
+ controlAfterPlan.tasks.push(
517
+ {
518
+ id: "next-leaf-a",
519
+ title: "Prepare active work regression",
520
+ phase: "E",
521
+ stream: "Operations",
522
+ priority: "P1",
523
+ status: "pending",
524
+ required: true,
525
+ dependsOn: [],
526
+ acceptance: [],
527
+ history: [],
528
+ origin: { kind: "manual" },
529
+ },
530
+ {
531
+ id: "next-leaf-b",
532
+ title: "Follow-up ready task",
533
+ phase: "E",
534
+ stream: "Operations",
535
+ priority: "P1",
536
+ status: "pending",
537
+ required: true,
538
+ dependsOn: ["next-leaf-a"],
539
+ acceptance: [],
540
+ history: [],
541
+ origin: { kind: "manual" },
542
+ },
543
+ );
544
+ writeJson(path.join(splitProject, "ops", "project_control.json"), controlAfterPlan);
545
+ runNode([BIN, "task", "start", "next-leaf-a"], splitProject);
546
+ const nextWithActiveWork = runNode([BIN, "next"], splitProject);
547
+ assert.match(nextWithActiveWork, /Active work:|Trabajo activo:/);
548
+ assert.match(nextWithActiveWork, /next-leaf-a/);
549
+ runNode([BIN, "task", "complete", "next-leaf-a"], splitProject);
550
+ const nextAfterActiveComplete = runNode([BIN, "next"], splitProject);
551
+ assert.match(nextAfterActiveComplete, /next-leaf-b/);
552
+
553
+ const coordinationControl = readJson(path.join(splitProject, "ops", "project_control.json"));
554
+ coordinationControl.tasks.push(
555
+ {
556
+ id: "user-exec-task",
557
+ title: "User-owned execution flow",
558
+ phase: "E",
559
+ stream: "Operations",
560
+ priority: "P1",
561
+ status: "pending",
562
+ required: true,
563
+ dependsOn: [],
564
+ acceptance: [],
565
+ history: [],
566
+ origin: { kind: "manual" },
567
+ execution: { owner: "user" },
568
+ },
569
+ {
570
+ id: "dashboard-verify-task",
571
+ title: "Dashboard verification flow",
572
+ phase: "E",
573
+ stream: "Operations",
574
+ priority: "P1",
575
+ status: "pending",
576
+ required: true,
577
+ dependsOn: [],
578
+ acceptance: [],
579
+ history: [],
580
+ origin: { kind: "manual" },
581
+ execution: { owner: "shared" },
582
+ },
583
+ {
584
+ id: "agent-exec-task",
585
+ title: "Agent execution flow",
586
+ phase: "E",
587
+ stream: "Operations",
588
+ priority: "P1",
589
+ status: "pending",
590
+ required: true,
591
+ dependsOn: [],
592
+ acceptance: [],
593
+ history: [],
594
+ origin: { kind: "manual" },
595
+ execution: { owner: "agent" },
596
+ },
597
+ );
598
+ writeJson(path.join(splitProject, "ops", "project_control.json"), coordinationControl);
599
+
600
+ runNode([BIN, "task", "start", "user-exec-task"], splitProject);
601
+ const controlAfterUserExec = readJson(path.join(splitProject, "ops", "project_control.json"));
602
+ assert.ok(controlAfterUserExec.meta.agentInbox.pending.some((item) => item.taskId === "user-exec-task" && item.kind === "await_user_report"));
603
+
604
+ const defaultDashboard = startDashboard(splitProject);
605
+
606
+ try {
607
+ const ready = await waitForDashboard(defaultDashboard);
452
608
  const state = await get(ready.port, "/api/state");
453
609
  const envPayload = await get(ready.port, "/api/env");
454
- const operaBootstrapPayload = await get(ready.port, "/api/opera/bootstrap");
455
- const operaHandoffPayload = await get(ready.port, "/api/opera/handoff");
456
- const localSkills = await get(ready.port, "/api/skills/local");
457
- const discoverSkills = await get(ready.port, "/api/skills/discover");
458
-
459
- assert.strictEqual(state.status, 200);
460
- assert.strictEqual(envPayload.status, 200);
461
- assert.strictEqual(operaBootstrapPayload.status, 200);
462
- assert.strictEqual(operaHandoffPayload.status, 200);
463
- assert.strictEqual(localSkills.status, 200);
464
- assert.strictEqual(discoverSkills.status, 200);
465
-
466
- const statePayload = JSON.parse(state.body);
467
- assert.strictEqual(statePayload.project.layout, "split");
610
+ const operaBootstrapPayload = await get(ready.port, "/api/opera/bootstrap");
611
+ const operaHandoffPayload = await get(ready.port, "/api/opera/handoff");
612
+ const localSkills = await get(ready.port, "/api/skills/local");
613
+ const discoverSkills = await get(ready.port, "/api/skills/discover");
614
+ const plansPayload = await get(ready.port, "/api/plans");
615
+ const planSourcePayload = await get(ready.port, "/api/plans/booking-plan");
616
+ const qualityPayload = await get(ready.port, "/api/quality");
617
+ const phaseReadinessPayload = await get(ready.port, "/api/quality/phase-readiness");
618
+ const releaseReadinessPayload = await get(ready.port, "/api/quality/release-readiness");
619
+ const promotionReadinessPayload = await get(ready.port, "/api/quality/promotion-readiness?target=production");
620
+ const waiversPayload = await get(ready.port, "/api/quality/waivers");
621
+
622
+ assert.strictEqual(state.status, 200);
623
+ assert.strictEqual(envPayload.status, 200);
624
+ assert.strictEqual(operaBootstrapPayload.status, 200);
625
+ assert.strictEqual(operaHandoffPayload.status, 200);
626
+ assert.strictEqual(localSkills.status, 200);
627
+ assert.strictEqual(discoverSkills.status, 200);
628
+ assert.strictEqual(plansPayload.status, 200);
629
+ assert.strictEqual(planSourcePayload.status, 200);
630
+ assert.strictEqual(qualityPayload.status, 200);
631
+ assert.strictEqual(phaseReadinessPayload.status, 200);
632
+ assert.strictEqual(releaseReadinessPayload.status, 200);
633
+ assert.strictEqual(promotionReadinessPayload.status, 200);
634
+ assert.strictEqual(waiversPayload.status, 200);
635
+
636
+ const statePayload = JSON.parse(state.body);
637
+ assert.strictEqual(statePayload.project.layout, "split");
468
638
  assert.strictEqual(statePayload.project.workspaceRoot, splitProject);
469
639
  assert.strictEqual(statePayload.project.appRoot, path.join(splitProject, "app"));
470
640
  assert.strictEqual(statePayload.project.opsRoot, path.join(splitProject, "ops"));
@@ -479,13 +649,80 @@ async function main() {
479
649
  assert.strictEqual(bootstrapState.status, "completed");
480
650
  assert.strictEqual(bootstrapState.contractVersion, 3);
481
651
  assert.strictEqual(bootstrapState.contractReadiness, "verified");
482
- const handoffState = JSON.parse(operaHandoffPayload.body);
483
- assert.ok(handoffState.markdown.includes("project-starter-skill"));
484
- assert.ok(handoffState.openQuestionsFile.endsWith("open-questions.md"));
485
-
486
- } finally {
487
- await stopDashboard(defaultDashboard);
488
- }
652
+ const handoffState = JSON.parse(operaHandoffPayload.body);
653
+ assert.ok(handoffState.markdown.includes("project-starter-skill"));
654
+ assert.ok(handoffState.openQuestionsFile.endsWith("open-questions.md"));
655
+ const plansState = JSON.parse(plansPayload.body);
656
+ assert.ok(plansState.sources.some((source) => source.id === "booking-plan"));
657
+ const planState = JSON.parse(planSourcePayload.body);
658
+ assert.strictEqual(planState.source.id, "booking-plan");
659
+ assert.ok(planState.preview.summary.managedTaskCount >= 3);
660
+ const qualityState = JSON.parse(qualityPayload.body);
661
+ assert.ok(qualityState.report.summary);
662
+ assert.ok(Array.isArray(qualityState.report.probes));
663
+ assert.ok(qualityState.releaseReadiness.status);
664
+ assert.ok(statePayload.quality.releaseReadiness.status);
665
+ assert.ok(!fs.existsSync(path.join(splitProject, "ops", "quality")), "las rutas GET de quality no deben crear storage");
666
+
667
+ const reviewVerify = await requestJson("POST", ready.port, "/api/quality/verify", {
668
+ projectId: statePayload.project.id,
669
+ scope: "review",
670
+ note: "dashboard smoke review",
671
+ });
672
+ assert.strictEqual(reviewVerify.status, 200);
673
+ const verifyState = JSON.parse(reviewVerify.body);
674
+ assert.strictEqual(verifyState.run.status, "skipped");
675
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "quality", "latest.json")));
676
+
677
+ const timerTask = statePayload.derived.tasks.find((task) => task.isLeaf && task.status === "pending");
678
+ assert.ok(timerTask, "deberia existir una tarea hoja pendiente para probar time tracking");
679
+ const timeStart = await requestJson("POST", ready.port, "/api/time/start", {
680
+ projectId: statePayload.project.id,
681
+ taskId: timerTask.id,
682
+ taskTitle: timerTask.title,
683
+ });
684
+ assert.strictEqual(timeStart.status, 201);
685
+ const timeStartPayload = JSON.parse(timeStart.body);
686
+ assert.strictEqual(timeStartPayload.autoTaskStarted, true);
687
+ const controlAfterTimerStart = readJson(path.join(splitProject, "ops", "project_control.json"));
688
+ const timedTask = controlAfterTimerStart.tasks.find((task) => task.id === timerTask.id);
689
+ assert.strictEqual(timedTask.status, "in_progress");
690
+ assert.ok((timedTask.history || []).some((entry) => entry.action === "start"));
691
+
692
+ const timeStop = await requestJson("POST", ready.port, "/api/time/stop", {
693
+ projectId: statePayload.project.id,
694
+ entryId: timeStartPayload.entry.id,
695
+ });
696
+ assert.strictEqual(timeStop.status, 200);
697
+
698
+ const dashboardVerify = await requestJson("POST", ready.port, "/api/tasks/dashboard-verify-task/action", {
699
+ projectId: statePayload.project.id,
700
+ action: "complete",
701
+ note: "user finished work",
702
+ actor: "user",
703
+ source: "dashboard_test",
704
+ });
705
+ assert.strictEqual(dashboardVerify.status, 200);
706
+ const controlAfterDashboardVerify = readJson(path.join(splitProject, "ops", "project_control.json"));
707
+ assert.ok(controlAfterDashboardVerify.meta.agentInbox.pending.some((item) => item.taskId === "dashboard-verify-task" && item.kind === "verify_status"));
708
+
709
+ const linkedCommand = await requestJson("POST", ready.port, "/api/commands", {
710
+ projectId: statePayload.project.id,
711
+ command: "echo agent-linked-task",
712
+ taskId: "agent-exec-task",
713
+ source: "execution_console",
714
+ });
715
+ assert.strictEqual(linkedCommand.status, 201);
716
+ await wait(400);
717
+ const controlAfterLinkedCommand = readJson(path.join(splitProject, "ops", "project_control.json"));
718
+ const linkedTask = controlAfterLinkedCommand.tasks.find((task) => task.id === "agent-exec-task");
719
+ assert.strictEqual(linkedTask.status, "in_progress");
720
+ assert.strictEqual(linkedTask.execution.lastActor, "agent");
721
+ assert.ok(linkedTask.execution.lastSessionId);
722
+
723
+ } finally {
724
+ await stopDashboard(defaultDashboard);
725
+ }
489
726
 
490
727
  const blocked4173 = await isPortFree(4173) ? await occupyPort(4173) : null;
491
728
  const fallbackDashboard = startDashboard(splitProject);
@@ -569,23 +806,93 @@ async function main() {
569
806
  const upgradeWithoutReset = runNodeResult([BIN, "opera", "upgrade", "--stable"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
570
807
  assert.strictEqual(upgradeWithoutReset.status, 0);
571
808
  assert.match(`${upgradeWithoutReset.stdout}\n${upgradeWithoutReset.stderr}`, /legacy/i);
572
- runNode([BIN, "opera", "upgrade", "--stable", "--reset"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
573
- const upgradedLegacyControl = readJson(legacyUnsupportedControlPath);
574
- assert.strictEqual(upgradedLegacyControl.meta.opera.legacyStatus, "supported");
575
- assert.strictEqual(upgradedLegacyControl.meta.opera.stableTag, "stable");
576
- assert.ok(fs.existsSync(path.join(legacyUnsupportedProject, "ops", ".tmp", "upgrade-backups")));
577
-
578
- const releaseProject = path.join(tempRoot, "release-demo");
579
- fs.mkdirSync(releaseProject, { recursive: true });
580
- initGitRepo(releaseProject);
581
- runNode([BIN, "init"], releaseProject);
582
- writeJson(path.join(releaseProject, "app", "package.json"), { name: "release-demo", version: "1.0.0" });
583
- fs.writeFileSync(path.join(releaseProject, "app", "index.js"), "console.log('release');\n", "utf8");
584
- commitAll(releaseProject, "split fixture");
585
- git(["checkout", "-b", "develop"], releaseProject);
586
- runNode([BIN, "release"], releaseProject);
587
- const publishFiles = git(["ls-tree", "--name-only", "master"], releaseProject).split(/\r?\n/).filter(Boolean);
588
- assert.ok(publishFiles.includes("package.json"));
809
+ runNode([BIN, "opera", "upgrade", "--stable", "--reset"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
810
+ const upgradedLegacyControl = readJson(legacyUnsupportedControlPath);
811
+ assert.strictEqual(upgradedLegacyControl.meta.opera.legacyStatus, "supported");
812
+ assert.strictEqual(upgradedLegacyControl.meta.opera.stableTag, "stable");
813
+ assert.ok(fs.existsSync(path.join(legacyUnsupportedProject, "ops", ".tmp", "upgrade-backups")));
814
+
815
+ const upgradeBackfillProject = path.join(tempRoot, "upgrade-backfill");
816
+ fs.mkdirSync(upgradeBackfillProject, { recursive: true });
817
+ initGitRepo(upgradeBackfillProject);
818
+ runNode([BIN, "init", "--locale", "en"], upgradeBackfillProject);
819
+ git(["checkout", "-b", "develop"], upgradeBackfillProject);
820
+ git(["remote", "add", "origin", "https://github.com/example/upgrade-backfill.git"], upgradeBackfillProject);
821
+ runNode([BIN, "opera", "install", "--locale", "en", "--no-bootstrap"], upgradeBackfillProject);
822
+ writeJson(path.join(upgradeBackfillProject, "ops", "bootstrap", "intake.json"), {
823
+ problemStatement: "Backfill test",
824
+ targetUser: "Developers",
825
+ singularDesiredOutcome: "Persist inferred metadata",
826
+ sourceOfTruth: "workspace",
827
+ payload: "release bundle",
828
+ });
829
+ writeJson(path.join(upgradeBackfillProject, "app", "package.json"), {
830
+ name: "upgrade-backfill",
831
+ version: "1.0.0",
832
+ scripts: {
833
+ test: `node -e "console.log('ok')"`,
834
+ "release:check": `node -e "console.log('release check ok')"`,
835
+ },
836
+ });
837
+ runNode([BIN, "opera", "upgrade", "--stable"], upgradeBackfillProject);
838
+ const backfilledIntake = readJson(path.join(upgradeBackfillProject, "ops", "bootstrap", "intake.json"));
839
+ const backfilledControl = readJson(path.join(upgradeBackfillProject, "ops", "project_control.json"));
840
+ assert.strictEqual(backfilledIntake.versionControl.provider, "github");
841
+ assert.strictEqual(backfilledIntake.versionControl.developmentBranch, "develop");
842
+ assert.strictEqual(backfilledIntake.deployment.mode, "manual");
843
+ assert.ok(backfilledIntake.deployment.smokeCommand);
844
+ assert.strictEqual(backfilledControl.meta.opera.bootstrap.discovery.versionControl.provider, "github");
845
+
846
+ const localeProjectEs = path.join(tempRoot, "locale-es");
847
+ fs.mkdirSync(localeProjectEs, { recursive: true });
848
+ runNode([BIN, "init", "--locale", "es"], localeProjectEs);
849
+ const localeEsQuality = runNode([BIN, "quality", "status"], localeProjectEs);
850
+ const localeEsStatus = runNode([BIN, "status"], localeProjectEs);
851
+ assert.match(localeEsQuality, /Estado de calidad/i);
852
+ assert.match(localeEsStatus, /Calidad/i);
853
+
854
+ const localeProjectEn = path.join(tempRoot, "locale-en");
855
+ fs.mkdirSync(localeProjectEn, { recursive: true });
856
+ runNode([BIN, "init", "--locale", "en"], localeProjectEn);
857
+ const localeEnQuality = runNode([BIN, "quality", "status"], localeProjectEn);
858
+ const localeEnStatus = runNode([BIN, "status"], localeProjectEn);
859
+ assert.match(localeEnQuality, /Quality status/i);
860
+ assert.match(localeEnStatus, /Quality/i);
861
+
862
+ const releaseProject = path.join(tempRoot, "release-demo");
863
+ fs.mkdirSync(releaseProject, { recursive: true });
864
+ initGitRepo(releaseProject);
865
+ runNode([BIN, "init"], releaseProject);
866
+ writeJson(path.join(releaseProject, "app", "package.json"), {
867
+ name: "release-demo",
868
+ version: "1.0.0",
869
+ scripts: {
870
+ test: `node -e "console.log('test ok')"`,
871
+ smoke: `node -e "console.log('smoke ok')"`,
872
+ "release:check": `node -e "console.log('release check ok')"`,
873
+ },
874
+ });
875
+ fs.writeFileSync(path.join(releaseProject, "app", "index.js"), "console.log('release');\n", "utf8");
876
+ const releaseControlPath = path.join(releaseProject, "ops", "project_control.json");
877
+ const releaseControl = readJson(releaseControlPath);
878
+ releaseControl.meta.quality = releaseControl.meta.quality || {};
879
+ releaseControl.meta.quality.verification = releaseControl.meta.quality.verification || {};
880
+ releaseControl.meta.quality.verification.smokeCommands = [`node -e "console.log('smoke ok')"`];
881
+ writeJson(releaseControlPath, releaseControl);
882
+ commitAll(releaseProject, "split fixture");
883
+ git(["checkout", "-b", "develop"], releaseProject);
884
+ const blockedRelease = runNodeResult([BIN, "release"], releaseProject);
885
+ assert.notStrictEqual(blockedRelease.status, 0, "release debe bloquearse si no hay evidencia de quality verify");
886
+ assert.match(`${blockedRelease.stdout}\n${blockedRelease.stderr}`, /quality readiness/i);
887
+ const missingBuildVerify = runNodeResult([BIN, "quality", "verify", "--scope", "build", "--json"], releaseProject);
888
+ assert.notStrictEqual(missingBuildVerify.status, 0, "verify --scope build debe fallar si no hay build configurado");
889
+ const missingBuildPayload = JSON.parse(missingBuildVerify.stdout);
890
+ assert.strictEqual(missingBuildPayload.results[0].status, "not_configured");
891
+ runNode([BIN, "quality", "verify", "--scope", "all"], releaseProject);
892
+ commitAll(releaseProject, "quality evidence");
893
+ runNode([BIN, "release"], releaseProject);
894
+ const publishFiles = git(["ls-tree", "--name-only", "master"], releaseProject).split(/\r?\n/).filter(Boolean);
895
+ assert.ok(publishFiles.includes("package.json"));
589
896
  assert.ok(publishFiles.includes(".env.example"));
590
897
  assert.ok(!publishFiles.includes("ops"));
591
898
  assert.ok(!publishFiles.includes(".trackops-workspace.json"));
@@ -1,21 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require("fs");
4
- const path = require("path");
5
-
6
- const ROOT = path.resolve(__dirname, "..");
7
- const PACKAGE_FILE = path.join(ROOT, "package.json");
8
- const SKILL_FILE = path.join(ROOT, "skills", "trackops", "skill.json");
9
-
10
- function main() {
11
- const pkg = JSON.parse(fs.readFileSync(PACKAGE_FILE, "utf8"));
12
- const skill = JSON.parse(fs.readFileSync(SKILL_FILE, "utf8"));
13
-
14
- skill.skillVersion = pkg.version;
15
- skill.trackopsVersion = pkg.version;
16
-
17
- fs.writeFileSync(SKILL_FILE, `${JSON.stringify(skill, null, 2)}\n`, "utf8");
18
- console.log(`Synced skills/trackops/skill.json to version ${pkg.version}.`);
19
- }
20
-
21
- main();
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const ROOT = path.resolve(__dirname, "..");
7
+ const PACKAGE_FILE = path.join(ROOT, "package.json");
8
+
9
+ function getSkillFiles() {
10
+ const skillsRoot = path.join(ROOT, "skills");
11
+ if (!fs.existsSync(skillsRoot)) return [];
12
+ return fs.readdirSync(skillsRoot, { withFileTypes: true })
13
+ .filter((entry) => entry.isDirectory())
14
+ .map((entry) => path.join(skillsRoot, entry.name, "skill.json"))
15
+ .filter((filePath) => fs.existsSync(filePath))
16
+ .sort((a, b) => a.localeCompare(b));
17
+ }
18
+
19
+ function main() {
20
+ const pkg = JSON.parse(fs.readFileSync(PACKAGE_FILE, "utf8"));
21
+ const skillFiles = getSkillFiles();
22
+ for (const skillFile of skillFiles) {
23
+ const skill = JSON.parse(fs.readFileSync(skillFile, "utf8"));
24
+ skill.skillVersion = pkg.version;
25
+ skill.trackopsVersion = pkg.version;
26
+ fs.writeFileSync(skillFile, `${JSON.stringify(skill, null, 2)}\n`, "utf8");
27
+ console.log(`Synced ${path.relative(ROOT, skillFile)} to version ${pkg.version}.`);
28
+ }
29
+ }
30
+
31
+ main();