trackops 2.0.4 → 2.0.6

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 (92) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +660 -575
  3. package/bin/trackops.js +127 -106
  4. package/lib/cli-format.js +118 -0
  5. package/lib/config.js +352 -326
  6. package/lib/control.js +408 -246
  7. package/lib/env.js +234 -222
  8. package/lib/i18n.js +5 -4
  9. package/lib/init.js +390 -282
  10. package/lib/locale.js +41 -41
  11. package/lib/opera-bootstrap.js +1066 -880
  12. package/lib/opera.js +615 -444
  13. package/lib/preferences.js +74 -74
  14. package/lib/registry.js +214 -214
  15. package/lib/release.js +56 -56
  16. package/lib/runtime-state.js +144 -144
  17. package/lib/skills.js +114 -89
  18. package/lib/workspace.js +259 -248
  19. package/locales/en.json +311 -167
  20. package/locales/es.json +314 -170
  21. package/package.json +61 -58
  22. package/scripts/postinstall-locale.js +21 -21
  23. package/scripts/skills-marketplace-smoke.js +124 -124
  24. package/scripts/smoke-tests.js +563 -517
  25. package/scripts/sync-skill-version.js +21 -21
  26. package/scripts/validate-skill.js +103 -103
  27. package/skills/trackops/SKILL.md +126 -122
  28. package/skills/trackops/agents/openai.yaml +7 -7
  29. package/skills/trackops/locales/en/SKILL.md +126 -122
  30. package/skills/trackops/locales/en/references/activation.md +94 -90
  31. package/skills/trackops/locales/en/references/troubleshooting.md +73 -67
  32. package/skills/trackops/locales/en/references/workflow.md +55 -32
  33. package/skills/trackops/references/activation.md +94 -90
  34. package/skills/trackops/references/troubleshooting.md +73 -67
  35. package/skills/trackops/references/workflow.md +55 -32
  36. package/skills/trackops/skill.json +29 -29
  37. package/templates/hooks/post-checkout +2 -2
  38. package/templates/hooks/post-commit +2 -2
  39. package/templates/hooks/post-merge +2 -2
  40. package/templates/opera/agent.md +28 -27
  41. package/templates/opera/architecture/dependency-graph.md +24 -24
  42. package/templates/opera/architecture/runtime-automation.md +24 -24
  43. package/templates/opera/architecture/runtime-operations.md +34 -34
  44. package/templates/opera/en/agent.md +22 -21
  45. package/templates/opera/en/architecture/dependency-graph.md +24 -24
  46. package/templates/opera/en/architecture/runtime-automation.md +24 -24
  47. package/templates/opera/en/architecture/runtime-operations.md +34 -34
  48. package/templates/opera/en/reviews/delivery-audit.md +18 -18
  49. package/templates/opera/en/reviews/integration-audit.md +18 -18
  50. package/templates/opera/en/router.md +24 -19
  51. package/templates/opera/references/autonomy-and-recovery.md +117 -117
  52. package/templates/opera/references/opera-cycle.md +193 -193
  53. package/templates/opera/registry.md +28 -28
  54. package/templates/opera/reviews/delivery-audit.md +18 -18
  55. package/templates/opera/reviews/integration-audit.md +18 -18
  56. package/templates/opera/router.md +54 -49
  57. package/templates/skills/changelog-updater/SKILL.md +69 -69
  58. package/templates/skills/commiter/SKILL.md +99 -99
  59. package/templates/skills/opera-contract-auditor/SKILL.md +38 -38
  60. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -38
  61. package/templates/skills/opera-policy-guard/SKILL.md +26 -26
  62. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -26
  63. package/templates/skills/opera-skill/SKILL.md +279 -0
  64. package/templates/skills/opera-skill/locales/en/SKILL.md +279 -0
  65. package/templates/skills/opera-skill/locales/en/references/phase-dod.md +138 -0
  66. package/templates/skills/opera-skill/references/phase-dod.md +138 -0
  67. package/templates/skills/project-starter-skill/SKILL.md +150 -131
  68. package/templates/skills/project-starter-skill/locales/en/SKILL.md +143 -105
  69. package/templates/skills/project-starter-skill/references/opera-cycle.md +195 -193
  70. package/ui/css/base.css +284 -284
  71. package/ui/css/charts.css +425 -425
  72. package/ui/css/components.css +1107 -1107
  73. package/ui/css/onboarding.css +133 -133
  74. package/ui/css/terminal.css +125 -125
  75. package/ui/css/timeline.css +58 -58
  76. package/ui/css/tokens.css +284 -284
  77. package/ui/favicon.svg +5 -5
  78. package/ui/index.html +99 -99
  79. package/ui/js/charts.js +526 -526
  80. package/ui/js/console-logger.js +172 -172
  81. package/ui/js/filters.js +247 -247
  82. package/ui/js/icons.js +129 -129
  83. package/ui/js/keyboard.js +229 -229
  84. package/ui/js/router.js +142 -142
  85. package/ui/js/theme.js +100 -100
  86. package/ui/js/time-tracker.js +248 -248
  87. package/ui/js/views/dashboard.js +870 -870
  88. package/ui/js/views/flash.js +47 -47
  89. package/ui/js/views/projects.js +745 -745
  90. package/ui/js/views/scrum.js +476 -476
  91. package/ui/js/views/settings.js +331 -331
  92. package/ui/js/views/timeline.js +265 -265
@@ -1,31 +1,31 @@
1
- #!/usr/bin/env node
2
-
3
- const assert = require("assert");
4
- const fs = require("fs");
5
- const http = require("http");
6
- const net = require("net");
7
- const os = require("os");
8
- const path = require("path");
9
- const { spawn, spawnSync } = require("child_process");
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");
14
-
15
- function getNpmCommand() {
16
- return process.platform === "win32" ? "npm.cmd" : "npm";
17
- }
18
-
19
- function runNode(args, cwd, envOverrides = {}) {
20
- const result = spawnSync(process.execPath, args, {
21
- cwd,
22
- encoding: "utf8",
23
- env: { ...process.env, ...envOverrides },
24
- });
25
- assert.strictEqual(result.status, 0, result.stderr || result.stdout || `fallo ejecutando ${args.join(" ")}`);
26
- return result.stdout;
27
- }
28
-
1
+ #!/usr/bin/env node
2
+
3
+ const assert = require("assert");
4
+ const fs = require("fs");
5
+ const http = require("http");
6
+ const net = require("net");
7
+ const os = require("os");
8
+ const path = require("path");
9
+ const { spawn, spawnSync } = require("child_process");
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");
14
+
15
+ function getNpmCommand() {
16
+ return process.platform === "win32" ? "npm.cmd" : "npm";
17
+ }
18
+
19
+ function runNode(args, cwd, envOverrides = {}) {
20
+ const result = spawnSync(process.execPath, args, {
21
+ cwd,
22
+ encoding: "utf8",
23
+ env: { ...process.env, ...envOverrides },
24
+ });
25
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout || `fallo ejecutando ${args.join(" ")}`);
26
+ return result.stdout;
27
+ }
28
+
29
29
  function runNodeResult(args, cwd, envOverrides = {}) {
30
30
  return spawnSync(process.execPath, args, {
31
31
  cwd,
@@ -34,521 +34,567 @@ function runNodeResult(args, cwd, envOverrides = {}) {
34
34
  });
35
35
  }
36
36
 
37
- function runCommand(command, args, cwd, envOverrides = {}) {
38
- return spawnSync(command, args, {
39
- cwd,
40
- encoding: "utf8",
41
- env: { ...process.env, ...envOverrides },
42
- });
43
- }
44
-
45
- function runNpm(args, cwd, envOverrides = {}) {
46
- return spawnSync(getNpmCommand(), args, {
37
+ function runNodeWithInput(args, cwd, input, envOverrides = {}) {
38
+ const result = spawnSync(process.execPath, args, {
47
39
  cwd,
48
40
  encoding: "utf8",
41
+ input,
49
42
  env: { ...process.env, ...envOverrides },
50
- shell: process.platform === "win32",
51
- });
52
- }
53
-
54
- function git(args, cwd) {
55
- const result = spawnSync("git", args, { cwd, encoding: "utf8" });
56
- assert.strictEqual(result.status, 0, result.stderr || result.stdout || `git ${args.join(" ")} fallo`);
57
- return result.stdout.trim();
58
- }
59
-
60
- function wait(ms) {
61
- return new Promise((resolve) => setTimeout(resolve, ms));
62
- }
63
-
64
- function get(port, pathname, host = "127.0.0.1") {
65
- return new Promise((resolve, reject) => {
66
- const req = http.get({ host, port, path: pathname }, (res) => {
67
- let body = "";
68
- res.setEncoding("utf8");
69
- res.on("data", (chunk) => { body += chunk; });
70
- res.on("end", () => resolve({ status: res.statusCode, body }));
71
- });
72
- req.on("error", reject);
73
43
  });
44
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout || `fallo ejecutando ${args.join(" ")}`);
45
+ return result.stdout;
74
46
  }
75
-
76
- function extractLocalPort(output) {
77
- const match = String(output || "").match(/- Local:\s+http:\/\/[^\s:]+:(\d+)/);
78
- return match ? Number(match[1]) : null;
79
- }
80
-
81
- function hasExternalIpv4() {
82
- return Object.values(os.networkInterfaces()).some((entries) => (
83
- (entries || []).some((entry) => entry && entry.family === "IPv4" && !entry.internal)
84
- ));
85
- }
86
-
87
- function isPortFree(port, host = "127.0.0.1") {
88
- return new Promise((resolve) => {
89
- const server = net.createServer();
90
- server.once("error", () => resolve(false));
91
- server.once("listening", () => {
92
- server.close(() => resolve(true));
93
- });
94
- server.listen(port, host);
95
- });
96
- }
97
-
98
- function findFreePort(host = "127.0.0.1") {
99
- return new Promise((resolve, reject) => {
100
- const server = net.createServer();
101
- server.once("error", reject);
102
- server.listen(0, host, () => {
103
- const address = server.address();
104
- const port = typeof address === "object" && address ? address.port : null;
105
- server.close((closeError) => {
106
- if (closeError) reject(closeError);
107
- else resolve(port);
108
- });
109
- });
110
- });
111
- }
112
-
113
- function occupyPort(port, host = "127.0.0.1") {
114
- return new Promise((resolve, reject) => {
115
- const server = http.createServer((_req, res) => {
116
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
117
- res.end("busy");
118
- });
119
- server.once("error", reject);
120
- server.listen(port, host, () => resolve(server));
121
- });
122
- }
123
-
124
- function startDashboard(cwd, args = [], envOverrides = {}) {
125
- const env = { ...process.env, ...envOverrides };
126
- const child = spawn(process.execPath, [BIN, "dashboard", ...args], {
127
- cwd,
128
- env,
129
- stdio: ["ignore", "pipe", "pipe"],
130
- });
131
-
132
- let output = "";
133
- child.stdout.on("data", (chunk) => { output += chunk.toString("utf8"); });
134
- child.stderr.on("data", (chunk) => { output += chunk.toString("utf8"); });
135
-
136
- return {
137
- child,
138
- output: () => output,
139
- };
140
- }
141
-
142
- async function waitForDashboard(session) {
143
- const startedAt = Date.now();
144
-
145
- while (Date.now() - startedAt < 15000) {
146
- if (session.child.exitCode != null) {
147
- throw new Error(`el dashboard termino antes de iniciar:\n${session.output()}`);
148
- }
149
-
150
- const port = extractLocalPort(session.output());
151
- if (port) {
152
- try {
153
- const response = await get(port, "/api/state");
154
- if (response.status === 200) {
155
- return { port, output: session.output() };
156
- }
157
- } catch (_error) {
158
- // sigue esperando
159
- }
160
- }
161
-
162
- await wait(250);
163
- }
164
-
165
- throw new Error(`el dashboard no respondio a tiempo:\n${session.output()}`);
166
- }
167
-
168
- async function stopDashboard(session) {
169
- if (!session || !session.child || session.child.exitCode != null) return;
170
- session.child.kill("SIGTERM");
171
- await wait(500);
172
- if (session.child.exitCode == null) {
173
- session.child.kill("SIGKILL");
174
- await wait(250);
175
- }
176
- }
177
-
178
- function writeJson(filePath, data) {
179
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
180
- fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
181
- }
182
-
183
- function packCurrentPackage(tempRoot) {
184
- const result = runNpm(["pack", ROOT], tempRoot);
185
- assert.strictEqual(result.status, 0, result.stderr || result.stdout || "npm pack fallo");
186
- const tarballName = String(result.stdout || "").trim().split(/\r?\n/).pop();
187
- const tarballPath = path.join(tempRoot, tarballName);
188
- assert.ok(fs.existsSync(tarballPath), `no se encontro el tarball ${tarballPath}`);
189
- return tarballPath;
190
- }
191
-
192
- function readJson(filePath) {
193
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
194
- }
195
-
196
- function initGitRepo(repo) {
197
- git(["init"], repo);
198
- git(["config", "user.email", "smoke@example.com"], repo);
199
- git(["config", "user.name", "Smoke Runner"], repo);
200
- }
201
-
202
- function commitAll(repo, message) {
203
- git(["add", "."], repo);
204
- git(["commit", "-m", message], repo);
205
- }
206
-
207
- async function main() {
208
- runNode([SKILL_VALIDATE], ROOT);
209
-
210
- const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "trackops-smoke-"));
211
- const tarballPath = packCurrentPackage(tempRoot);
212
- const packageVersion = readJson(path.join(ROOT, "package.json")).version;
213
-
214
- const bootstrapHome = path.join(tempRoot, "bootstrap-home");
215
- const bootstrapPrefix = path.join(tempRoot, "bootstrap-prefix");
216
- fs.mkdirSync(bootstrapHome, { recursive: true });
217
- fs.mkdirSync(bootstrapPrefix, { recursive: true });
218
-
219
- const bootstrapEnv = {
220
- TRACKOPS_BOOTSTRAP_HOME: bootstrapHome,
221
- TRACKOPS_BOOTSTRAP_PREFIX: bootstrapPrefix,
222
- };
223
-
224
- const explicitInstall = runNpm(["install", "-g", "--prefix", bootstrapPrefix, tarballPath], tempRoot, bootstrapEnv);
225
- assert.strictEqual(explicitInstall.status, 0, explicitInstall.stderr || explicitInstall.stdout || "instalacion global explicita fallo");
226
-
227
- const runtimeStamp = readJson(path.join(bootstrapHome, ".trackops", "runtime.json"));
228
- assert.ok(["es", "en"].includes(runtimeStamp.locale), "el bootstrap global debe fijar un idioma");
229
-
230
- const installedCli = path.join(bootstrapPrefix, "node_modules", "trackops", "bin", "trackops.js");
231
- assert.ok(fs.existsSync(installedCli), "el runtime instalado debe existir dentro del prefijo aislado");
232
- const installedVersion = runNode([installedCli, "--version"], tempRoot);
233
- assert.strictEqual(installedVersion.trim(), packageVersion);
234
-
47
+
48
+ function runCommand(command, args, cwd, envOverrides = {}) {
49
+ return spawnSync(command, args, {
50
+ cwd,
51
+ encoding: "utf8",
52
+ env: { ...process.env, ...envOverrides },
53
+ });
54
+ }
55
+
56
+ function runNpm(args, cwd, envOverrides = {}) {
57
+ return spawnSync(getNpmCommand(), args, {
58
+ cwd,
59
+ encoding: "utf8",
60
+ env: { ...process.env, ...envOverrides },
61
+ shell: process.platform === "win32",
62
+ });
63
+ }
64
+
65
+ function git(args, cwd) {
66
+ const result = spawnSync("git", args, { cwd, encoding: "utf8" });
67
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout || `git ${args.join(" ")} fallo`);
68
+ return result.stdout.trim();
69
+ }
70
+
71
+ function wait(ms) {
72
+ return new Promise((resolve) => setTimeout(resolve, ms));
73
+ }
74
+
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");
80
+ res.on("data", (chunk) => { body += chunk; });
81
+ res.on("end", () => resolve({ status: res.statusCode, body }));
82
+ });
83
+ req.on("error", reject);
84
+ });
85
+ }
86
+
87
+ function extractLocalPort(output) {
88
+ const match = String(output || "").match(/- Local:\s+http:\/\/[^\s:]+:(\d+)/);
89
+ return match ? Number(match[1]) : null;
90
+ }
91
+
92
+ function hasExternalIpv4() {
93
+ return Object.values(os.networkInterfaces()).some((entries) => (
94
+ (entries || []).some((entry) => entry && entry.family === "IPv4" && !entry.internal)
95
+ ));
96
+ }
97
+
98
+ function isPortFree(port, host = "127.0.0.1") {
99
+ return new Promise((resolve) => {
100
+ const server = net.createServer();
101
+ server.once("error", () => resolve(false));
102
+ server.once("listening", () => {
103
+ server.close(() => resolve(true));
104
+ });
105
+ server.listen(port, host);
106
+ });
107
+ }
108
+
109
+ function findFreePort(host = "127.0.0.1") {
110
+ return new Promise((resolve, reject) => {
111
+ const server = net.createServer();
112
+ server.once("error", reject);
113
+ server.listen(0, host, () => {
114
+ const address = server.address();
115
+ const port = typeof address === "object" && address ? address.port : null;
116
+ server.close((closeError) => {
117
+ if (closeError) reject(closeError);
118
+ else resolve(port);
119
+ });
120
+ });
121
+ });
122
+ }
123
+
124
+ function occupyPort(port, host = "127.0.0.1") {
125
+ return new Promise((resolve, reject) => {
126
+ const server = http.createServer((_req, res) => {
127
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
128
+ res.end("busy");
129
+ });
130
+ server.once("error", reject);
131
+ server.listen(port, host, () => resolve(server));
132
+ });
133
+ }
134
+
135
+ function startDashboard(cwd, args = [], envOverrides = {}) {
136
+ const env = { ...process.env, ...envOverrides };
137
+ const child = spawn(process.execPath, [BIN, "dashboard", ...args], {
138
+ cwd,
139
+ env,
140
+ stdio: ["ignore", "pipe", "pipe"],
141
+ });
142
+
143
+ let output = "";
144
+ child.stdout.on("data", (chunk) => { output += chunk.toString("utf8"); });
145
+ child.stderr.on("data", (chunk) => { output += chunk.toString("utf8"); });
146
+
147
+ return {
148
+ child,
149
+ output: () => output,
150
+ };
151
+ }
152
+
153
+ async function waitForDashboard(session) {
154
+ const startedAt = Date.now();
155
+
156
+ while (Date.now() - startedAt < 15000) {
157
+ if (session.child.exitCode != null) {
158
+ throw new Error(`el dashboard termino antes de iniciar:\n${session.output()}`);
159
+ }
160
+
161
+ const port = extractLocalPort(session.output());
162
+ if (port) {
163
+ try {
164
+ const response = await get(port, "/api/state");
165
+ if (response.status === 200) {
166
+ return { port, output: session.output() };
167
+ }
168
+ } catch (_error) {
169
+ // sigue esperando
170
+ }
171
+ }
172
+
173
+ await wait(250);
174
+ }
175
+
176
+ throw new Error(`el dashboard no respondio a tiempo:\n${session.output()}`);
177
+ }
178
+
179
+ async function stopDashboard(session) {
180
+ if (!session || !session.child || session.child.exitCode != null) return;
181
+ session.child.kill("SIGTERM");
182
+ await wait(500);
183
+ if (session.child.exitCode == null) {
184
+ session.child.kill("SIGKILL");
185
+ await wait(250);
186
+ }
187
+ }
188
+
189
+ function writeJson(filePath, data) {
190
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
191
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
192
+ }
193
+
194
+ function packCurrentPackage(tempRoot) {
195
+ const result = runNpm(["pack", ROOT], tempRoot);
196
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout || "npm pack fallo");
197
+ const tarballName = String(result.stdout || "").trim().split(/\r?\n/).pop();
198
+ const tarballPath = path.join(tempRoot, tarballName);
199
+ assert.ok(fs.existsSync(tarballPath), `no se encontro el tarball ${tarballPath}`);
200
+ return tarballPath;
201
+ }
202
+
203
+ function readJson(filePath) {
204
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
205
+ }
206
+
207
+ function initGitRepo(repo) {
208
+ git(["init"], repo);
209
+ git(["config", "user.email", "smoke@example.com"], repo);
210
+ git(["config", "user.name", "Smoke Runner"], repo);
211
+ }
212
+
213
+ function commitAll(repo, message) {
214
+ git(["add", "."], repo);
215
+ git(["commit", "-m", message], repo);
216
+ }
217
+
218
+ async function main() {
219
+ runNode([SKILL_VALIDATE], ROOT);
220
+
221
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "trackops-smoke-"));
222
+ const tarballPath = packCurrentPackage(tempRoot);
223
+ const packageVersion = readJson(path.join(ROOT, "package.json")).version;
224
+
225
+ const bootstrapHome = path.join(tempRoot, "bootstrap-home");
226
+ const bootstrapPrefix = path.join(tempRoot, "bootstrap-prefix");
227
+ fs.mkdirSync(bootstrapHome, { recursive: true });
228
+ fs.mkdirSync(bootstrapPrefix, { recursive: true });
229
+
230
+ const bootstrapEnv = {
231
+ TRACKOPS_BOOTSTRAP_HOME: bootstrapHome,
232
+ TRACKOPS_BOOTSTRAP_PREFIX: bootstrapPrefix,
233
+ };
234
+
235
+ const explicitInstall = runNpm(["install", "-g", "--prefix", bootstrapPrefix, tarballPath], tempRoot, bootstrapEnv);
236
+ assert.strictEqual(explicitInstall.status, 0, explicitInstall.stderr || explicitInstall.stdout || "instalacion global explicita fallo");
237
+
238
+ const runtimeStamp = readJson(path.join(bootstrapHome, ".trackops", "runtime.json"));
239
+ assert.ok(["es", "en"].includes(runtimeStamp.locale), "el bootstrap global debe fijar un idioma");
240
+
241
+ const installedCli = path.join(bootstrapPrefix, "node_modules", "trackops", "bin", "trackops.js");
242
+ assert.ok(fs.existsSync(installedCli), "el runtime instalado debe existir dentro del prefijo aislado");
243
+ const installedVersion = runNode([installedCli, "--version"], tempRoot);
244
+ assert.strictEqual(installedVersion.trim(), packageVersion);
245
+
235
246
  const helpOutput = runNode([BIN, "help"], ROOT);
236
247
  assert.doesNotMatch(helpOutput, /\btrackops agent\b/i);
237
248
  assert.match(helpOutput, /workspace status\|migrate/i);
238
249
  assert.match(helpOutput, /env status\|sync/i);
239
250
  assert.match(helpOutput, /release \[--push\]/i);
240
-
241
- const versionOutput = runNode([BIN, "--version"], ROOT);
242
- assert.strictEqual(versionOutput.trim(), packageVersion);
243
-
244
- runNode([BIN, "locale", "set", "en"], tempRoot, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
245
- const localeGet = runNode([BIN, "locale", "get"], tempRoot, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
246
- assert.match(localeGet, /Effective language: en|Idioma efectivo: en/);
247
- const localeDoctor = runNode([BIN, "doctor", "locale"], tempRoot, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
248
- assert.match(localeDoctor, /Source: global|Origen: global/);
249
-
250
- const splitProject = path.join(tempRoot, "split-demo");
251
- fs.mkdirSync(splitProject, { recursive: true });
252
- runNode([BIN, "init", "--locale", "es"], splitProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
253
-
254
- assert.ok(fs.existsSync(path.join(splitProject, ".trackops-workspace.json")));
255
- assert.ok(fs.existsSync(path.join(splitProject, ".env")));
256
- assert.ok(fs.existsSync(path.join(splitProject, ".env.example")));
257
- assert.ok(fs.existsSync(path.join(splitProject, "app")));
258
- assert.ok(fs.existsSync(path.join(splitProject, "ops", "project_control.json")));
259
- assert.ok(fs.existsSync(path.join(splitProject, "app", ".env")));
260
- assert.ok(fs.existsSync(path.join(splitProject, "app", ".env.example")));
261
- assert.ok(!fs.existsSync(path.join(splitProject, "project_control.json")));
262
- assert.ok(!fs.existsSync(path.join(splitProject, "task_plan.md")));
263
-
264
- const splitControl = readJson(path.join(splitProject, "ops", "project_control.json"));
265
- assert.strictEqual(splitControl.meta.workspace.layout, "split");
266
- assert.strictEqual(splitControl.meta.environment.rootEnvFile, ".env");
267
- assert.strictEqual(splitControl.meta.locale, "es");
268
-
269
- const projectLocaleDoctor = runNode([BIN, "doctor", "locale"], splitProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
270
- assert.match(projectLocaleDoctor, /Project language: es|Idioma del proyecto: es/);
271
- assert.match(projectLocaleDoctor, /Source: project|Origen: proyecto/);
272
-
251
+ assert.match(helpOutput, /--plain|--a11y/i);
252
+
253
+ const versionOutput = runNode([BIN, "--version"], ROOT);
254
+ assert.strictEqual(versionOutput.trim(), packageVersion);
255
+
256
+ runNode([BIN, "locale", "set", "en"], tempRoot, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
257
+ const localeGet = runNode([BIN, "locale", "get"], tempRoot, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
258
+ assert.match(localeGet, /Effective language: en|Idioma efectivo: en/);
259
+ const localeDoctor = runNode([BIN, "doctor", "locale"], tempRoot, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
260
+ assert.match(localeDoctor, /Source: global|Origen: global/);
261
+
262
+ const splitProject = path.join(tempRoot, "split-demo");
263
+ fs.mkdirSync(splitProject, { recursive: true });
264
+ runNode([BIN, "init", "--locale", "es"], splitProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
265
+
266
+ assert.ok(fs.existsSync(path.join(splitProject, ".trackops-workspace.json")));
267
+ assert.ok(fs.existsSync(path.join(splitProject, ".env")));
268
+ assert.ok(fs.existsSync(path.join(splitProject, ".env.example")));
269
+ assert.ok(fs.existsSync(path.join(splitProject, "app")));
270
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "project_control.json")));
271
+ assert.ok(fs.existsSync(path.join(splitProject, "app", ".env")));
272
+ assert.ok(fs.existsSync(path.join(splitProject, "app", ".env.example")));
273
+ assert.ok(!fs.existsSync(path.join(splitProject, "project_control.json")));
274
+ assert.ok(!fs.existsSync(path.join(splitProject, "task_plan.md")));
275
+
276
+ const splitControl = readJson(path.join(splitProject, "ops", "project_control.json"));
277
+ assert.strictEqual(splitControl.meta.workspace.layout, "split");
278
+ assert.strictEqual(splitControl.meta.environment.rootEnvFile, ".env");
279
+ assert.strictEqual(splitControl.meta.locale, "es");
280
+
281
+ const projectLocaleDoctor = runNode([BIN, "doctor", "locale"], splitProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
282
+ assert.match(projectLocaleDoctor, /Project language: es|Idioma del proyecto: es/);
283
+ assert.match(projectLocaleDoctor, /Source: project|Origen: proyecto/);
284
+
273
285
  const statusFromWorkspace = runNode([BIN, "status"], splitProject);
274
286
  const statusFromApp = runNode([BIN, "status"], path.join(splitProject, "app"));
275
287
  const statusFromOps = runNode([BIN, "status"], path.join(splitProject, "ops"));
276
- assert.match(statusFromWorkspace, /Layout: split/);
277
- assert.match(statusFromApp, /Layout: split/);
278
- assert.match(statusFromOps, /Layout: split/);
288
+ assert.match(statusFromWorkspace, /Layout: split|Estructura: split/);
289
+ assert.match(statusFromApp, /Layout: split|Estructura: split/);
290
+ assert.match(statusFromOps, /Layout: split|Estructura: split/);
291
+ assert.match(statusFromWorkspace, /Git: not initialized|Git: no inicializado/i);
292
+ assert.doesNotMatch(statusFromWorkspace, /Branch: detached|Rama: detached/i);
279
293
 
280
294
  const nextOutput = runNode([BIN, "next"], splitProject);
281
295
  assert.match(nextOutput, /ops-bootstrap/);
282
296
 
297
+ const rerunInit = runNode([BIN, "init", "--locale", "es"], splitProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
298
+ assert.match(rerunInit, /Updated \.trackops-workspace\.json|Actualizado \.trackops-workspace\.json/);
299
+
283
300
  runNode([BIN, "sync"], splitProject);
284
- for (const file of ["task_plan.md", "progress.md", "findings.md"]) {
285
- assert.ok(fs.existsSync(path.join(splitProject, "ops", file)), `${file} no fue generado en ops/`);
286
- }
301
+ for (const file of ["task_plan.md", "progress.md", "findings.md"]) {
302
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", file)), `${file} no fue generado en ops/`);
303
+ }
287
304
 
288
305
  const envStatus = runNode([BIN, "env", "status"], splitProject);
289
- assert.match(envStatus, /Root \.env:/);
290
- assert.match(envStatus, /App bridge:/);
306
+ assert.match(envStatus, /Root \.env:|\.env raiz:/);
307
+ assert.match(envStatus, /App bridge:|Puente app:/);
291
308
 
292
309
  const nonEmptyProject = path.join(tempRoot, "non-empty");
293
310
  fs.mkdirSync(nonEmptyProject, { recursive: true });
294
311
  writeJson(path.join(nonEmptyProject, "package.json"), { name: "existing-app", version: "1.0.0" });
295
- const initNonEmpty = runNodeResult([BIN, "init"], nonEmptyProject);
296
- assert.notStrictEqual(initNonEmpty.status, 0, "init debe abortar en directorios no vacios");
297
- assert.match(`${initNonEmpty.stdout}\n${initNonEmpty.stderr}`, /workspace migrate/i);
298
-
299
- writeJson(path.join(splitProject, "app", "package.json"), {
300
- name: "split-demo",
301
- version: "1.0.0",
302
- dependencies: { openai: "^4.0.0" },
303
- scripts: { test: "echo ok" },
304
- });
305
- runNode([BIN, "opera", "install", "--locale", "en", "--non-interactive"], splitProject);
306
-
307
- assert.ok(fs.existsSync(path.join(splitProject, "ops", "genesis.md")));
308
- assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agent", "hub", "agent.md")));
309
- assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "_registry.md")));
310
- assert.ok(fs.existsSync(path.join(splitProject, "ops", "bootstrap", "agent-handoff.md")));
311
- assert.ok(fs.existsSync(path.join(splitProject, "ops", "bootstrap", "agent-handoff.json")));
312
- assert.ok(fs.existsSync(path.join(splitProject, "ops", "bootstrap", "open-questions.md")));
313
- assert.ok(fs.existsSync(path.join(splitProject, "ops", "architecture", "runtime-operations.md")));
314
- assert.ok(fs.existsSync(path.join(splitProject, "ops", "architecture", "dependency-graph.md")));
315
- assert.ok(fs.existsSync(path.join(splitProject, "ops", "architecture", "runtime-automation.md")));
316
- assert.ok(fs.existsSync(path.join(splitProject, "ops", "policy", "autonomy.json")));
317
- assert.ok(fs.existsSync(path.join(splitProject, "ops", "reviews", "integration-audit.md")));
318
- assert.ok(fs.existsSync(path.join(splitProject, "ops", "reviews", "delivery-audit.md")));
319
- assert.ok(!fs.existsSync(path.join(splitProject, "genesis.md")));
320
-
321
- const operaControl = readJson(path.join(splitProject, "ops", "project_control.json"));
322
- assert.strictEqual(operaControl.meta.locale, "en");
323
- assert.strictEqual(operaControl.meta.opera.installed, true);
324
- assert.strictEqual(operaControl.meta.opera.bootstrap.mode, "agent_handoff");
325
- assert.strictEqual(operaControl.meta.opera.bootstrap.status, "awaiting_agent");
326
- assert.ok(operaControl.meta.opera.skills.includes("project-starter-skill"));
327
- assert.ok(operaControl.meta.opera.skills.includes("opera-contract-auditor"));
328
- assert.ok(operaControl.meta.opera.skills.includes("opera-policy-guard"));
329
- assert.ok(operaControl.meta.environment.requiredKeys.includes("OPENAI_API_KEY"));
330
- const envRootText = fs.readFileSync(path.join(splitProject, ".env"), "utf8");
331
- assert.match(envRootText, /OPENAI_API_KEY=/);
332
-
333
- const handoffPrint = runNode([BIN, "opera", "handoff", "--print"], splitProject);
334
- assert.match(handoffPrint, /project-starter-skill/);
335
- const handoffJson = JSON.parse(runNode([BIN, "opera", "handoff", "--json"], splitProject));
336
- assert.strictEqual(handoffJson.skill, "project-starter-skill");
337
-
338
- const directProject = path.join(tempRoot, "direct-demo");
339
- fs.mkdirSync(directProject, { recursive: true });
340
- runNode([BIN, "init", "--locale", "en"], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
341
- writeJson(path.join(directProject, "app", "package.json"), { name: "direct-demo", version: "1.0.0" });
342
- runNode([
343
- BIN,
344
- "opera",
345
- "install",
346
- "--locale",
347
- "en",
348
- "--non-interactive",
349
- "--bootstrap-mode",
350
- "direct",
351
- "--technical-level",
352
- "senior",
353
- "--project-state",
354
- "existing_repo",
355
- "--docs-state",
356
- "spec_dossier",
357
- "--decision-ownership",
358
- "user",
359
- ], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
312
+ const initNonEmpty = runNode([BIN, "init", "--locale", "en"], nonEmptyProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
313
+ assert.match(initNonEmpty, /adopted into app\/|movio a app\//i);
314
+ assert.ok(fs.existsSync(path.join(nonEmptyProject, ".trackops-workspace.json")));
315
+ assert.ok(fs.existsSync(path.join(nonEmptyProject, "app", "package.json")));
316
+ assert.ok(fs.existsSync(path.join(nonEmptyProject, "ops", "project_control.json")));
317
+
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);
325
+
326
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "genesis.md")));
327
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agent", "hub", "agent.md")));
328
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "_registry.md")));
329
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "bootstrap", "agent-handoff.md")));
330
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "bootstrap", "agent-handoff.json")));
331
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "bootstrap", "open-questions.md")));
332
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "architecture", "runtime-operations.md")));
333
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "architecture", "dependency-graph.md")));
334
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "architecture", "runtime-automation.md")));
335
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "policy", "autonomy.json")));
336
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "reviews", "integration-audit.md")));
337
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "reviews", "delivery-audit.md")));
338
+ assert.ok(!fs.existsSync(path.join(splitProject, "genesis.md")));
339
+
340
+ const operaControl = readJson(path.join(splitProject, "ops", "project_control.json"));
341
+ assert.strictEqual(operaControl.meta.locale, "en");
342
+ assert.strictEqual(operaControl.meta.opera.installed, true);
343
+ assert.strictEqual(operaControl.meta.opera.bootstrap.mode, "agent_handoff");
344
+ 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")));
352
+ assert.ok(operaControl.meta.environment.requiredKeys.includes("OPENAI_API_KEY"));
353
+ const envRootText = fs.readFileSync(path.join(splitProject, ".env"), "utf8");
354
+ assert.match(envRootText, /OPENAI_API_KEY=/);
355
+
356
+ const handoffPrint = runNode([BIN, "opera", "handoff", "--print"], splitProject);
357
+ assert.match(handoffPrint, /project-starter-skill/);
358
+ const handoffJson = JSON.parse(runNode([BIN, "opera", "handoff", "--json"], splitProject));
359
+ assert.strictEqual(handoffJson.skill, "project-starter-skill");
360
+
361
+ const directProject = path.join(tempRoot, "direct-demo");
362
+ fs.mkdirSync(directProject, { recursive: true });
363
+ runNode([BIN, "init", "--locale", "en"], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
364
+ writeJson(path.join(directProject, "app", "package.json"), { name: "direct-demo", version: "1.0.0" });
365
+ runNode([
366
+ BIN,
367
+ "opera",
368
+ "install",
369
+ "--locale",
370
+ "en",
371
+ "--non-interactive",
372
+ "--bootstrap-mode",
373
+ "direct",
374
+ "--technical-level",
375
+ "senior",
376
+ "--project-state",
377
+ "existing_repo",
378
+ "--docs-state",
379
+ "spec_dossier",
380
+ "--decision-ownership",
381
+ "user",
382
+ ], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
360
383
  const directControl = readJson(path.join(directProject, "ops", "project_control.json"));
361
384
  assert.strictEqual(directControl.meta.opera.bootstrap.mode, "direct_cli");
362
385
  assert.strictEqual(directControl.meta.opera.bootstrap.status, "awaiting_intake");
363
-
364
- writeJson(path.join(splitProject, "ops", "bootstrap", "intake.json"), {
365
- version: 1,
366
- technicalLevel: "low",
367
- projectState: "idea",
368
- documentationState: "none",
369
- decisionOwnership: "agent",
370
- problemStatement: "users need a simple way to book and pay online",
371
- targetUser: "small studio owners",
372
- singularDesiredOutcome: "let users create and pay for bookings",
373
- userLanguage: "en",
374
- needsPlainLanguage: true,
375
- recommendedStack: ["nextjs"],
376
- externalServices: ["OpenAI", "Stripe"],
377
- sourceOfTruth: "primary bookings database",
378
- payload: "bookings dashboard",
379
- behaviorRules: ["keep explanations simple"],
380
- architecturalInvariants: ["keep app and ops separated"],
381
- inputSchema: { booking: { email: "string" } },
382
- outputSchema: { confirmation: { id: "string" } },
383
- pipeline: ["create booking", "confirm payment"],
384
- templates: ["booking-confirmation"],
385
- });
386
- fs.writeFileSync(
387
- path.join(splitProject, "ops", "bootstrap", "spec-dossier.md"),
388
- "# Spec dossier\n\n## Problem statement\nusers need a simple way to book and pay online\n\n## Target user\nsmall studio owners\n\n## Singular desired outcome\nlet users create and pay for bookings\n\n## Delivery target\nbookings dashboard\n\n## Source of truth\nprimary bookings database\n",
389
- "utf8",
390
- );
391
- runNode([BIN, "opera", "bootstrap", "--resume"], splitProject);
392
- const resumedControl = readJson(path.join(splitProject, "ops", "project_control.json"));
393
- assert.strictEqual(resumedControl.meta.opera.bootstrap.status, "completed");
394
- assert.strictEqual(resumedControl.meta.currentFocus, "let users create and pay for bookings");
395
- assert.ok(resumedControl.meta.environment.requiredKeys.includes("STRIPE_SECRET_KEY"));
396
- assert.ok(fs.existsSync(path.join(splitProject, "ops", "contract", "operating-contract.json")));
397
- const operatingContract = readJson(path.join(splitProject, "ops", "contract", "operating-contract.json"));
398
- assert.strictEqual(operatingContract.version, 3);
399
- assert.strictEqual(operatingContract.userModel.language, "en");
400
- assert.strictEqual(operatingContract.userModel.decisionOwnership, "agent");
401
-
402
- const defaultDashboard = startDashboard(splitProject);
403
-
404
- try {
405
- const ready = await waitForDashboard(defaultDashboard);
406
- const state = await get(ready.port, "/api/state");
407
- const envPayload = await get(ready.port, "/api/env");
408
- const operaBootstrapPayload = await get(ready.port, "/api/opera/bootstrap");
409
- const operaHandoffPayload = await get(ready.port, "/api/opera/handoff");
410
- const localSkills = await get(ready.port, "/api/skills/local");
411
- const discoverSkills = await get(ready.port, "/api/skills/discover");
412
-
413
- assert.strictEqual(state.status, 200);
414
- assert.strictEqual(envPayload.status, 200);
415
- assert.strictEqual(operaBootstrapPayload.status, 200);
416
- assert.strictEqual(operaHandoffPayload.status, 200);
417
- assert.strictEqual(localSkills.status, 200);
418
- assert.strictEqual(discoverSkills.status, 200);
419
-
420
- const statePayload = JSON.parse(state.body);
421
- assert.strictEqual(statePayload.project.layout, "split");
422
- assert.strictEqual(statePayload.project.workspaceRoot, splitProject);
423
- assert.strictEqual(statePayload.project.appRoot, path.join(splitProject, "app"));
424
- assert.strictEqual(statePayload.project.opsRoot, path.join(splitProject, "ops"));
425
- assert.ok(Array.isArray(statePayload.env.requiredKeys));
426
- assert.ok(!Object.prototype.hasOwnProperty.call(statePayload.env, "values"));
427
-
428
- const envState = JSON.parse(envPayload.body);
429
- assert.ok(envState.requiredKeys.includes("OPENAI_API_KEY"));
430
- assert.ok(envState.missingKeys.includes("OPENAI_API_KEY"));
431
-
432
- const bootstrapState = JSON.parse(operaBootstrapPayload.body);
433
- assert.strictEqual(bootstrapState.status, "completed");
434
- assert.strictEqual(bootstrapState.contractVersion, 3);
435
- assert.strictEqual(bootstrapState.contractReadiness, "verified");
436
- const handoffState = JSON.parse(operaHandoffPayload.body);
437
- assert.ok(handoffState.markdown.includes("project-starter-skill"));
438
- assert.ok(handoffState.openQuestionsFile.endsWith("open-questions.md"));
439
-
440
- } finally {
441
- await stopDashboard(defaultDashboard);
442
- }
443
-
444
- const blocked4173 = await isPortFree(4173) ? await occupyPort(4173) : null;
445
- const fallbackDashboard = startDashboard(splitProject);
446
- try {
447
- const ready = await waitForDashboard(fallbackDashboard);
448
- assert.notStrictEqual(ready.port, 4173, `deberia haber evitado 4173 ocupado:\n${ready.output}`);
449
- assert.match(ready.output, /Local:/);
450
- } finally {
451
- await stopDashboard(fallbackDashboard);
452
- if (blocked4173) blocked4173.close();
453
- }
454
-
455
- const strictPort = await findFreePort();
456
- const occupiedStrictPort = await occupyPort(strictPort);
457
- try {
458
- const strictResult = runNodeResult([BIN, "dashboard", "--strict-port", "--port", String(strictPort)], splitProject);
459
- assert.notStrictEqual(strictResult.status, 0, "el dashboard deberia fallar con --strict-port si el puerto esta ocupado");
460
- assert.match(`${strictResult.stdout}\n${strictResult.stderr}`, /already in use|esta en uso/i);
461
- } finally {
462
- occupiedStrictPort.close();
463
- }
464
-
465
- const publicPort = await findFreePort("0.0.0.0");
466
- const publicDashboard = startDashboard(splitProject, ["--public", "--port", String(publicPort)]);
467
- try {
468
- const ready = await waitForDashboard(publicDashboard);
469
- if (hasExternalIpv4()) {
470
- assert.match(ready.output, /- Network:/);
471
- }
472
- } finally {
473
- await stopDashboard(publicDashboard);
474
- }
475
-
476
- const noClipboardPort = await findFreePort();
477
- const noClipboardDashboard = startDashboard(splitProject, ["--port", String(noClipboardPort)], {
478
- PATH: "",
479
- Path: "",
480
- });
481
- try {
482
- const ready = await waitForDashboard(noClipboardDashboard);
483
- const state = await get(ready.port, "/api/state");
484
- assert.strictEqual(state.status, 200);
485
- } finally {
486
- await stopDashboard(noClipboardDashboard);
487
- }
488
-
489
- const legacyProject = path.join(tempRoot, "legacy-demo");
490
- fs.mkdirSync(legacyProject, { recursive: true });
491
- initGitRepo(legacyProject);
492
- writeJson(path.join(legacyProject, "package.json"), { name: "legacy-demo", version: "1.0.0" });
493
- runNode([BIN, "init", "--legacy-layout"], legacyProject);
494
- runNode([BIN, "opera", "install", "--locale", "en", "--no-bootstrap"], legacyProject);
495
- fs.writeFileSync(path.join(legacyProject, ".env"), "OPENAI_API_KEY=\n", "utf8");
496
- fs.writeFileSync(path.join(legacyProject, ".env.example"), "OPENAI_API_KEY=\n", "utf8");
497
- commitAll(legacyProject, "legacy fixture");
498
-
499
- runNode([BIN, "workspace", "migrate"], legacyProject);
500
- assert.ok(fs.existsSync(path.join(legacyProject, ".trackops-workspace.json")));
501
- assert.ok(fs.existsSync(path.join(legacyProject, "app", "package.json")));
502
- assert.ok(fs.existsSync(path.join(legacyProject, "ops", "project_control.json")));
503
- assert.ok(fs.existsSync(path.join(legacyProject, ".env")));
504
- assert.ok(fs.existsSync(path.join(legacyProject, "app", ".env")));
505
- assert.ok(!fs.existsSync(path.join(legacyProject, "project_control.json")));
506
- const migratedPackage = readJson(path.join(legacyProject, "app", "package.json"));
507
- assert.ok(!migratedPackage.scripts || !migratedPackage.scripts["ops:status"]);
508
- assert.match(git(["branch", "--list"], legacyProject), /backup\/trackops-workspace-/);
509
-
510
- const legacyUnsupportedProject = path.join(tempRoot, "legacy-unsupported");
511
- fs.mkdirSync(legacyUnsupportedProject, { recursive: true });
512
- initGitRepo(legacyUnsupportedProject);
513
- runNode([BIN, "init", "--locale", "en"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
514
- const legacyUnsupportedControlPath = path.join(legacyUnsupportedProject, "ops", "project_control.json");
515
- const legacyUnsupportedControl = readJson(legacyUnsupportedControlPath);
516
- legacyUnsupportedControl.meta.opera = {
517
- installed: true,
518
- version: "0.9.0",
519
- };
520
- writeJson(legacyUnsupportedControlPath, legacyUnsupportedControl);
386
+ assert.ok(fs.existsSync(path.join(directProject, "ops", "bootstrap", "intake.json")));
387
+ assert.ok(fs.existsSync(path.join(directProject, "ops", "bootstrap", "spec-dossier.md")));
388
+ assert.ok(fs.existsSync(path.join(directProject, "ops", "bootstrap", "open-questions.md")));
389
+ assert.ok(fs.existsSync(path.join(directProject, "ops", "bootstrap", "quality-report.json")));
390
+ const directOperaStatus = runNode([BIN, "opera", "status"], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
391
+ assert.match(directOperaStatus, /Intake:|Intake:/);
392
+ assert.match(directOperaStatus, /Spec dossier:|Specification brief:|Dossier de especificacion:/i);
393
+ assert.doesNotMatch(directOperaStatus, /The agent did not include|El agente no incluyo/i);
394
+ assert.doesNotMatch(directOperaStatus, /Handoff: ops[\\/]+bootstrap[\\/]+agent-handoff\.md/i);
395
+ const directHandoffSummary = runNode([BIN, "opera", "handoff"], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
396
+ assert.match(directHandoffSummary, /Guided bootstrap summary|Resumen del bootstrap guiado/i);
397
+ assert.doesNotMatch(directHandoffSummary, /Markdown handoff/i);
398
+
399
+ const plainStatus = runNode([BIN, "--plain", "status"], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
400
+ assert.match(plainStatus, /\[BLOCKED\]|\[PENDING\]/);
401
+ assert.doesNotMatch(plainStatus, /\u2500|\u23F3|\u26D4|\u2705/);
402
+
403
+ const promptProject = path.join(tempRoot, "prompt-demo");
404
+ fs.mkdirSync(promptProject, { recursive: true });
405
+ runNode([BIN, "init", "--locale", "en"], promptProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
406
+ writeJson(path.join(promptProject, "app", "package.json"), { name: "prompt-demo", version: "1.0.0" });
407
+ runNodeWithInput([BIN, "opera", "install", "--locale", "en"], promptProject, "\n", { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
408
+ assert.ok(!fs.existsSync(path.join(promptProject, "ops", "bootstrap", "agent-handoff.md")));
409
+
410
+ writeJson(path.join(splitProject, "ops", "bootstrap", "intake.json"), {
411
+ version: 1,
412
+ technicalLevel: "low",
413
+ projectState: "idea",
414
+ documentationState: "none",
415
+ decisionOwnership: "agent",
416
+ problemStatement: "users need a simple way to book and pay online",
417
+ targetUser: "small studio owners",
418
+ singularDesiredOutcome: "let users create and pay for bookings",
419
+ userLanguage: "en",
420
+ needsPlainLanguage: true,
421
+ recommendedStack: ["nextjs"],
422
+ externalServices: ["OpenAI", "Stripe"],
423
+ sourceOfTruth: "primary bookings database",
424
+ payload: "bookings dashboard",
425
+ behaviorRules: ["keep explanations simple"],
426
+ architecturalInvariants: ["keep app and ops separated"],
427
+ inputSchema: { booking: { email: "string" } },
428
+ outputSchema: { confirmation: { id: "string" } },
429
+ pipeline: ["create booking", "confirm payment"],
430
+ templates: ["booking-confirmation"],
431
+ });
432
+ fs.writeFileSync(
433
+ path.join(splitProject, "ops", "bootstrap", "spec-dossier.md"),
434
+ "# Spec dossier\n\n## Problem statement\nusers need a simple way to book and pay online\n\n## Target user\nsmall studio owners\n\n## Singular desired outcome\nlet users create and pay for bookings\n\n## Delivery target\nbookings dashboard\n\n## Source of truth\nprimary bookings database\n",
435
+ "utf8",
436
+ );
437
+ runNode([BIN, "opera", "bootstrap", "--resume"], splitProject);
438
+ const resumedControl = readJson(path.join(splitProject, "ops", "project_control.json"));
439
+ assert.strictEqual(resumedControl.meta.opera.bootstrap.status, "completed");
440
+ assert.strictEqual(resumedControl.meta.currentFocus, "let users create and pay for bookings");
441
+ assert.ok(resumedControl.meta.environment.requiredKeys.includes("STRIPE_SECRET_KEY"));
442
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "contract", "operating-contract.json")));
443
+ 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);
452
+ const state = await get(ready.port, "/api/state");
453
+ 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");
468
+ assert.strictEqual(statePayload.project.workspaceRoot, splitProject);
469
+ assert.strictEqual(statePayload.project.appRoot, path.join(splitProject, "app"));
470
+ assert.strictEqual(statePayload.project.opsRoot, path.join(splitProject, "ops"));
471
+ assert.ok(Array.isArray(statePayload.env.requiredKeys));
472
+ assert.ok(!Object.prototype.hasOwnProperty.call(statePayload.env, "values"));
473
+
474
+ const envState = JSON.parse(envPayload.body);
475
+ assert.ok(envState.requiredKeys.includes("OPENAI_API_KEY"));
476
+ assert.ok(envState.missingKeys.includes("OPENAI_API_KEY"));
477
+
478
+ const bootstrapState = JSON.parse(operaBootstrapPayload.body);
479
+ assert.strictEqual(bootstrapState.status, "completed");
480
+ assert.strictEqual(bootstrapState.contractVersion, 3);
481
+ 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
+ }
489
+
490
+ const blocked4173 = await isPortFree(4173) ? await occupyPort(4173) : null;
491
+ const fallbackDashboard = startDashboard(splitProject);
492
+ try {
493
+ const ready = await waitForDashboard(fallbackDashboard);
494
+ assert.notStrictEqual(ready.port, 4173, `deberia haber evitado 4173 ocupado:\n${ready.output}`);
495
+ assert.match(ready.output, /Local:/);
496
+ } finally {
497
+ await stopDashboard(fallbackDashboard);
498
+ if (blocked4173) blocked4173.close();
499
+ }
500
+
501
+ const strictPort = await findFreePort();
502
+ const occupiedStrictPort = await occupyPort(strictPort);
503
+ try {
504
+ const strictResult = runNodeResult([BIN, "dashboard", "--strict-port", "--port", String(strictPort)], splitProject);
505
+ assert.notStrictEqual(strictResult.status, 0, "el dashboard deberia fallar con --strict-port si el puerto esta ocupado");
506
+ assert.match(`${strictResult.stdout}\n${strictResult.stderr}`, /already in use|esta en uso/i);
507
+ } finally {
508
+ occupiedStrictPort.close();
509
+ }
510
+
511
+ const publicPort = await findFreePort("0.0.0.0");
512
+ const publicDashboard = startDashboard(splitProject, ["--public", "--port", String(publicPort)]);
513
+ try {
514
+ const ready = await waitForDashboard(publicDashboard);
515
+ if (hasExternalIpv4()) {
516
+ assert.match(ready.output, /- Network:/);
517
+ }
518
+ } finally {
519
+ await stopDashboard(publicDashboard);
520
+ }
521
+
522
+ const noClipboardPort = await findFreePort();
523
+ const noClipboardDashboard = startDashboard(splitProject, ["--port", String(noClipboardPort)], {
524
+ PATH: "",
525
+ Path: "",
526
+ });
527
+ try {
528
+ const ready = await waitForDashboard(noClipboardDashboard);
529
+ const state = await get(ready.port, "/api/state");
530
+ assert.strictEqual(state.status, 200);
531
+ } finally {
532
+ await stopDashboard(noClipboardDashboard);
533
+ }
534
+
535
+ const legacyProject = path.join(tempRoot, "legacy-demo");
536
+ fs.mkdirSync(legacyProject, { recursive: true });
537
+ initGitRepo(legacyProject);
538
+ writeJson(path.join(legacyProject, "package.json"), { name: "legacy-demo", version: "1.0.0" });
539
+ runNode([BIN, "init", "--legacy-layout"], legacyProject);
540
+ runNode([BIN, "opera", "install", "--locale", "en", "--no-bootstrap"], legacyProject);
541
+ fs.writeFileSync(path.join(legacyProject, ".env"), "OPENAI_API_KEY=\n", "utf8");
542
+ fs.writeFileSync(path.join(legacyProject, ".env.example"), "OPENAI_API_KEY=\n", "utf8");
543
+ commitAll(legacyProject, "legacy fixture");
544
+
545
+ runNode([BIN, "workspace", "migrate"], legacyProject);
546
+ assert.ok(fs.existsSync(path.join(legacyProject, ".trackops-workspace.json")));
547
+ assert.ok(fs.existsSync(path.join(legacyProject, "app", "package.json")));
548
+ assert.ok(fs.existsSync(path.join(legacyProject, "ops", "project_control.json")));
549
+ assert.ok(fs.existsSync(path.join(legacyProject, ".env")));
550
+ assert.ok(fs.existsSync(path.join(legacyProject, "app", ".env")));
551
+ assert.ok(!fs.existsSync(path.join(legacyProject, "project_control.json")));
552
+ const migratedPackage = readJson(path.join(legacyProject, "app", "package.json"));
553
+ assert.ok(!migratedPackage.scripts || !migratedPackage.scripts["ops:status"]);
554
+ assert.match(git(["branch", "--list"], legacyProject), /backup\/trackops-workspace-/);
555
+
556
+ const legacyUnsupportedProject = path.join(tempRoot, "legacy-unsupported");
557
+ fs.mkdirSync(legacyUnsupportedProject, { recursive: true });
558
+ initGitRepo(legacyUnsupportedProject);
559
+ runNode([BIN, "init", "--locale", "en"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
560
+ const legacyUnsupportedControlPath = path.join(legacyUnsupportedProject, "ops", "project_control.json");
561
+ const legacyUnsupportedControl = readJson(legacyUnsupportedControlPath);
562
+ legacyUnsupportedControl.meta.opera = {
563
+ installed: true,
564
+ version: "0.9.0",
565
+ };
566
+ writeJson(legacyUnsupportedControlPath, legacyUnsupportedControl);
521
567
  const legacyStatusOutput = runNode([BIN, "opera", "status"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
522
568
  assert.match(legacyStatusOutput, /legacy_unsupported/);
523
- const upgradeWithoutReset = runNodeResult([BIN, "opera", "upgrade", "--stable"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
524
- assert.strictEqual(upgradeWithoutReset.status, 0);
525
- assert.match(`${upgradeWithoutReset.stdout}\n${upgradeWithoutReset.stderr}`, /legacy/i);
526
- runNode([BIN, "opera", "upgrade", "--stable", "--reset"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
527
- const upgradedLegacyControl = readJson(legacyUnsupportedControlPath);
528
- assert.strictEqual(upgradedLegacyControl.meta.opera.legacyStatus, "supported");
529
- assert.strictEqual(upgradedLegacyControl.meta.opera.stableTag, "stable");
530
- assert.ok(fs.existsSync(path.join(legacyUnsupportedProject, "ops", ".tmp", "upgrade-backups")));
531
-
532
- const releaseProject = path.join(tempRoot, "release-demo");
533
- fs.mkdirSync(releaseProject, { recursive: true });
534
- initGitRepo(releaseProject);
535
- runNode([BIN, "init"], releaseProject);
536
- writeJson(path.join(releaseProject, "app", "package.json"), { name: "release-demo", version: "1.0.0" });
537
- fs.writeFileSync(path.join(releaseProject, "app", "index.js"), "console.log('release');\n", "utf8");
538
- commitAll(releaseProject, "split fixture");
539
- git(["checkout", "-b", "develop"], releaseProject);
540
- runNode([BIN, "release"], releaseProject);
541
- const publishFiles = git(["ls-tree", "--name-only", "master"], releaseProject).split(/\r?\n/).filter(Boolean);
542
- assert.ok(publishFiles.includes("package.json"));
543
- assert.ok(publishFiles.includes(".env.example"));
544
- assert.ok(!publishFiles.includes("ops"));
545
- assert.ok(!publishFiles.includes(".trackops-workspace.json"));
546
-
547
- fs.rmSync(tempRoot, { recursive: true, force: true });
548
- console.log("Smoke tests OK");
549
- }
550
-
551
- main().catch((error) => {
552
- console.error(error.message);
553
- process.exit(1);
554
- });
569
+ const upgradeWithoutReset = runNodeResult([BIN, "opera", "upgrade", "--stable"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
570
+ assert.strictEqual(upgradeWithoutReset.status, 0);
571
+ 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"));
589
+ assert.ok(publishFiles.includes(".env.example"));
590
+ assert.ok(!publishFiles.includes("ops"));
591
+ assert.ok(!publishFiles.includes(".trackops-workspace.json"));
592
+
593
+ fs.rmSync(tempRoot, { recursive: true, force: true });
594
+ console.log("Smoke tests OK");
595
+ }
596
+
597
+ main().catch((error) => {
598
+ console.error(error.message);
599
+ process.exit(1);
600
+ });