trackops 1.0.1 → 1.1.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 (57) hide show
  1. package/README.md +326 -270
  2. package/bin/trackops.js +102 -70
  3. package/lib/config.js +260 -35
  4. package/lib/control.js +517 -475
  5. package/lib/env.js +227 -0
  6. package/lib/i18n.js +61 -53
  7. package/lib/init.js +135 -46
  8. package/lib/locale.js +63 -0
  9. package/lib/opera-bootstrap.js +523 -0
  10. package/lib/opera.js +319 -170
  11. package/lib/registry.js +27 -13
  12. package/lib/release.js +56 -0
  13. package/lib/resources.js +42 -0
  14. package/lib/server.js +907 -554
  15. package/lib/skills.js +148 -124
  16. package/lib/workspace.js +260 -0
  17. package/locales/en.json +331 -139
  18. package/locales/es.json +331 -139
  19. package/package.json +7 -9
  20. package/scripts/skills-marketplace-smoke.js +124 -0
  21. package/scripts/smoke-tests.js +445 -0
  22. package/scripts/sync-skill-version.js +21 -0
  23. package/scripts/validate-skill.js +88 -0
  24. package/skills/trackops/SKILL.md +64 -0
  25. package/skills/trackops/agents/openai.yaml +3 -0
  26. package/skills/trackops/references/activation.md +39 -0
  27. package/skills/trackops/references/troubleshooting.md +34 -0
  28. package/skills/trackops/references/workflow.md +20 -0
  29. package/skills/trackops/scripts/bootstrap-trackops.js +201 -0
  30. package/skills/trackops/skill.json +29 -0
  31. package/templates/opera/en/agent.md +26 -0
  32. package/templates/opera/en/genesis.md +79 -0
  33. package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
  34. package/templates/opera/en/references/opera-cycle.md +62 -0
  35. package/templates/opera/en/registry.md +28 -0
  36. package/templates/opera/en/router.md +39 -0
  37. package/templates/opera/genesis.md +79 -94
  38. package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
  39. package/templates/skills/commiter/locales/en/SKILL.md +11 -0
  40. package/templates/skills/project-starter-skill/locales/en/SKILL.md +24 -0
  41. package/ui/css/panels.css +956 -953
  42. package/ui/index.html +1 -1
  43. package/ui/js/api.js +211 -194
  44. package/ui/js/app.js +200 -199
  45. package/ui/js/i18n.js +14 -0
  46. package/ui/js/onboarding.js +439 -437
  47. package/ui/js/state.js +130 -129
  48. package/ui/js/utils.js +175 -172
  49. package/ui/js/views/board.js +255 -254
  50. package/ui/js/views/execution.js +256 -256
  51. package/ui/js/views/insights.js +340 -339
  52. package/ui/js/views/overview.js +365 -364
  53. package/ui/js/views/settings.js +340 -202
  54. package/ui/js/views/sidebar.js +131 -132
  55. package/ui/js/views/skills.js +163 -162
  56. package/ui/js/views/tasks.js +406 -405
  57. package/ui/js/views/topbar.js +239 -183
@@ -0,0 +1,445 @@
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
+ const SKILL_BOOTSTRAP = path.join(ROOT, "skills", "trackops", "scripts", "bootstrap-trackops.js");
15
+
16
+ function getNpmCommand() {
17
+ return process.platform === "win32" ? "npm.cmd" : "npm";
18
+ }
19
+
20
+ function runNode(args, cwd, envOverrides = {}) {
21
+ const result = spawnSync(process.execPath, args, {
22
+ cwd,
23
+ encoding: "utf8",
24
+ env: { ...process.env, ...envOverrides },
25
+ });
26
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout || `fallo ejecutando ${args.join(" ")}`);
27
+ return result.stdout;
28
+ }
29
+
30
+ function runNodeResult(args, cwd, envOverrides = {}) {
31
+ return spawnSync(process.execPath, args, {
32
+ cwd,
33
+ encoding: "utf8",
34
+ env: { ...process.env, ...envOverrides },
35
+ });
36
+ }
37
+
38
+ function runCommand(command, args, cwd, envOverrides = {}) {
39
+ return spawnSync(command, args, {
40
+ cwd,
41
+ encoding: "utf8",
42
+ env: { ...process.env, ...envOverrides },
43
+ });
44
+ }
45
+
46
+ function runNpm(args, cwd, envOverrides = {}) {
47
+ return spawnSync(getNpmCommand(), args, {
48
+ cwd,
49
+ encoding: "utf8",
50
+ env: { ...process.env, ...envOverrides },
51
+ shell: process.platform === "win32",
52
+ });
53
+ }
54
+
55
+ function git(args, cwd) {
56
+ const result = spawnSync("git", args, { cwd, encoding: "utf8" });
57
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout || `git ${args.join(" ")} fallo`);
58
+ return result.stdout.trim();
59
+ }
60
+
61
+ function wait(ms) {
62
+ return new Promise((resolve) => setTimeout(resolve, ms));
63
+ }
64
+
65
+ function get(port, pathname, host = "127.0.0.1") {
66
+ return new Promise((resolve, reject) => {
67
+ const req = http.get({ host, port, path: pathname }, (res) => {
68
+ let body = "";
69
+ res.setEncoding("utf8");
70
+ res.on("data", (chunk) => { body += chunk; });
71
+ res.on("end", () => resolve({ status: res.statusCode, body }));
72
+ });
73
+ req.on("error", reject);
74
+ });
75
+ }
76
+
77
+ function extractLocalPort(output) {
78
+ const match = String(output || "").match(/- Local:\s+http:\/\/[^\s:]+:(\d+)/);
79
+ return match ? Number(match[1]) : null;
80
+ }
81
+
82
+ function hasExternalIpv4() {
83
+ return Object.values(os.networkInterfaces()).some((entries) => (
84
+ (entries || []).some((entry) => entry && entry.family === "IPv4" && !entry.internal)
85
+ ));
86
+ }
87
+
88
+ function isPortFree(port, host = "127.0.0.1") {
89
+ return new Promise((resolve) => {
90
+ const server = net.createServer();
91
+ server.once("error", () => resolve(false));
92
+ server.once("listening", () => {
93
+ server.close(() => resolve(true));
94
+ });
95
+ server.listen(port, host);
96
+ });
97
+ }
98
+
99
+ function findFreePort(host = "127.0.0.1") {
100
+ return new Promise((resolve, reject) => {
101
+ const server = net.createServer();
102
+ server.once("error", reject);
103
+ server.listen(0, host, () => {
104
+ const address = server.address();
105
+ const port = typeof address === "object" && address ? address.port : null;
106
+ server.close((closeError) => {
107
+ if (closeError) reject(closeError);
108
+ else resolve(port);
109
+ });
110
+ });
111
+ });
112
+ }
113
+
114
+ function occupyPort(port, host = "127.0.0.1") {
115
+ return new Promise((resolve, reject) => {
116
+ const server = http.createServer((_req, res) => {
117
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
118
+ res.end("busy");
119
+ });
120
+ server.once("error", reject);
121
+ server.listen(port, host, () => resolve(server));
122
+ });
123
+ }
124
+
125
+ function startDashboard(cwd, args = [], envOverrides = {}) {
126
+ const env = { ...process.env, ...envOverrides };
127
+ const child = spawn(process.execPath, [BIN, "dashboard", ...args], {
128
+ cwd,
129
+ env,
130
+ stdio: ["ignore", "pipe", "pipe"],
131
+ });
132
+
133
+ let output = "";
134
+ child.stdout.on("data", (chunk) => { output += chunk.toString("utf8"); });
135
+ child.stderr.on("data", (chunk) => { output += chunk.toString("utf8"); });
136
+
137
+ return {
138
+ child,
139
+ output: () => output,
140
+ };
141
+ }
142
+
143
+ async function waitForDashboard(session) {
144
+ const startedAt = Date.now();
145
+
146
+ while (Date.now() - startedAt < 15000) {
147
+ if (session.child.exitCode != null) {
148
+ throw new Error(`el dashboard termino antes de iniciar:\n${session.output()}`);
149
+ }
150
+
151
+ const port = extractLocalPort(session.output());
152
+ if (port) {
153
+ try {
154
+ const response = await get(port, "/api/state");
155
+ if (response.status === 200) {
156
+ return { port, output: session.output() };
157
+ }
158
+ } catch (_error) {
159
+ // sigue esperando
160
+ }
161
+ }
162
+
163
+ await wait(250);
164
+ }
165
+
166
+ throw new Error(`el dashboard no respondio a tiempo:\n${session.output()}`);
167
+ }
168
+
169
+ async function stopDashboard(session) {
170
+ if (!session || !session.child || session.child.exitCode != null) return;
171
+ session.child.kill("SIGTERM");
172
+ await wait(500);
173
+ if (session.child.exitCode == null) {
174
+ session.child.kill("SIGKILL");
175
+ await wait(250);
176
+ }
177
+ }
178
+
179
+ function writeJson(filePath, data) {
180
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
181
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
182
+ }
183
+
184
+ function packCurrentPackage(tempRoot) {
185
+ const result = runNpm(["pack", ROOT], tempRoot);
186
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout || "npm pack fallo");
187
+ const tarballName = String(result.stdout || "").trim().split(/\r?\n/).pop();
188
+ const tarballPath = path.join(tempRoot, tarballName);
189
+ assert.ok(fs.existsSync(tarballPath), `no se encontro el tarball ${tarballPath}`);
190
+ return tarballPath;
191
+ }
192
+
193
+ function readJson(filePath) {
194
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
195
+ }
196
+
197
+ function initGitRepo(repo) {
198
+ git(["init"], repo);
199
+ git(["config", "user.email", "smoke@example.com"], repo);
200
+ git(["config", "user.name", "Smoke Runner"], repo);
201
+ }
202
+
203
+ function commitAll(repo, message) {
204
+ git(["add", "."], repo);
205
+ git(["commit", "-m", message], repo);
206
+ }
207
+
208
+ async function main() {
209
+ runNode([SKILL_VALIDATE], ROOT);
210
+
211
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "trackops-smoke-"));
212
+ const tarballPath = packCurrentPackage(tempRoot);
213
+ const packageVersion = readJson(path.join(ROOT, "package.json")).version;
214
+
215
+ const bootstrapHome = path.join(tempRoot, "bootstrap-home");
216
+ const bootstrapPrefix = path.join(tempRoot, "bootstrap-prefix");
217
+ fs.mkdirSync(bootstrapHome, { recursive: true });
218
+ fs.mkdirSync(bootstrapPrefix, { recursive: true });
219
+
220
+ const bootstrapEnv = {
221
+ TRACKOPS_BOOTSTRAP_HOME: bootstrapHome,
222
+ TRACKOPS_BOOTSTRAP_PREFIX: bootstrapPrefix,
223
+ TRACKOPS_BOOTSTRAP_INSTALL_SOURCE: tarballPath,
224
+ };
225
+
226
+ const firstBootstrap = runCommand(process.execPath, [SKILL_BOOTSTRAP], tempRoot, bootstrapEnv);
227
+ assert.strictEqual(firstBootstrap.status, 0, firstBootstrap.stderr || firstBootstrap.stdout || "bootstrap skill fallo");
228
+ assert.match(firstBootstrap.stdout, /TrackOps runtime .* is ready/i);
229
+
230
+ const runtimeStamp = readJson(path.join(bootstrapHome, ".trackops", "runtime.json"));
231
+ assert.strictEqual(runtimeStamp.runtimeVersion, packageVersion);
232
+ assert.strictEqual(runtimeStamp.skill, "trackops");
233
+ assert.strictEqual(runtimeStamp.bootstrapPolicy, "first_use");
234
+
235
+ const installedCli = path.join(bootstrapPrefix, "node_modules", "trackops", "bin", "trackops.js");
236
+ assert.ok(fs.existsSync(installedCli), "el runtime instalado debe existir dentro del prefijo aislado");
237
+ const installedVersion = runNode([installedCli, "--version"], tempRoot);
238
+ assert.strictEqual(installedVersion.trim(), packageVersion);
239
+
240
+ const secondBootstrap = runCommand(process.execPath, [SKILL_BOOTSTRAP], tempRoot, bootstrapEnv);
241
+ assert.strictEqual(secondBootstrap.status, 0, secondBootstrap.stderr || secondBootstrap.stdout || "bootstrap idempotente fallo");
242
+ assert.match(secondBootstrap.stdout, /already ready/i);
243
+
244
+ const untouchedRepo = path.join(tempRoot, "untouched-repo");
245
+ fs.mkdirSync(untouchedRepo, { recursive: true });
246
+ const bootstrapNoRepoMutation = runCommand(process.execPath, [SKILL_BOOTSTRAP], untouchedRepo, bootstrapEnv);
247
+ assert.strictEqual(bootstrapNoRepoMutation.status, 0, bootstrapNoRepoMutation.stderr || bootstrapNoRepoMutation.stdout || "bootstrap repetido fallo");
248
+ assert.ok(!fs.existsSync(path.join(untouchedRepo, "project_control.json")), "la skill global no debe crear artefactos de proyecto por si sola");
249
+
250
+ const helpOutput = runNode([BIN, "help"], ROOT);
251
+ assert.doesNotMatch(helpOutput, /\btrackops agent\b/i);
252
+ assert.match(helpOutput, /workspace status\|migrate/i);
253
+ assert.match(helpOutput, /env status\|sync/i);
254
+ assert.match(helpOutput, /release \[--push\]/i);
255
+
256
+ const versionOutput = runNode([BIN, "--version"], ROOT);
257
+ assert.strictEqual(versionOutput.trim(), packageVersion);
258
+
259
+ const splitProject = path.join(tempRoot, "split-demo");
260
+ fs.mkdirSync(splitProject, { recursive: true });
261
+ runNode([BIN, "init"], splitProject);
262
+
263
+ assert.ok(fs.existsSync(path.join(splitProject, ".trackops-workspace.json")));
264
+ assert.ok(fs.existsSync(path.join(splitProject, ".env")));
265
+ assert.ok(fs.existsSync(path.join(splitProject, ".env.example")));
266
+ assert.ok(fs.existsSync(path.join(splitProject, "app")));
267
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "project_control.json")));
268
+ assert.ok(fs.existsSync(path.join(splitProject, "app", ".env")));
269
+ assert.ok(fs.existsSync(path.join(splitProject, "app", ".env.example")));
270
+ assert.ok(!fs.existsSync(path.join(splitProject, "project_control.json")));
271
+ assert.ok(!fs.existsSync(path.join(splitProject, "task_plan.md")));
272
+
273
+ const splitControl = readJson(path.join(splitProject, "ops", "project_control.json"));
274
+ assert.strictEqual(splitControl.meta.workspace.layout, "split");
275
+ assert.strictEqual(splitControl.meta.environment.rootEnvFile, ".env");
276
+
277
+ const statusFromWorkspace = runNode([BIN, "status"], splitProject);
278
+ const statusFromApp = runNode([BIN, "status"], path.join(splitProject, "app"));
279
+ const statusFromOps = runNode([BIN, "status"], path.join(splitProject, "ops"));
280
+ assert.match(statusFromWorkspace, /Layout: split/);
281
+ assert.match(statusFromApp, /Layout: split/);
282
+ assert.match(statusFromOps, /Layout: split/);
283
+
284
+ const nextOutput = runNode([BIN, "next"], splitProject);
285
+ assert.match(nextOutput, /ops-bootstrap/);
286
+
287
+ runNode([BIN, "sync"], splitProject);
288
+ for (const file of ["task_plan.md", "progress.md", "findings.md"]) {
289
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", file)), `${file} no fue generado en ops/`);
290
+ }
291
+
292
+ const envStatus = runNode([BIN, "env", "status"], splitProject);
293
+ assert.match(envStatus, /Root \.env:/);
294
+ assert.match(envStatus, /App bridge:/);
295
+
296
+ const nonEmptyProject = path.join(tempRoot, "non-empty");
297
+ fs.mkdirSync(nonEmptyProject, { recursive: true });
298
+ writeJson(path.join(nonEmptyProject, "package.json"), { name: "existing-app", version: "1.0.0" });
299
+ const initNonEmpty = runNodeResult([BIN, "init"], nonEmptyProject);
300
+ assert.notStrictEqual(initNonEmpty.status, 0, "init debe abortar en directorios no vacios");
301
+ assert.match(`${initNonEmpty.stdout}\n${initNonEmpty.stderr}`, /workspace migrate/i);
302
+
303
+ writeJson(path.join(splitProject, "app", "package.json"), {
304
+ name: "split-demo",
305
+ version: "1.0.0",
306
+ dependencies: { openai: "^4.0.0" },
307
+ scripts: { test: "echo ok" },
308
+ });
309
+ runNode([BIN, "opera", "install", "--locale", "en", "--non-interactive"], splitProject);
310
+
311
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", "genesis.md")));
312
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agent", "hub", "agent.md")));
313
+ assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "_registry.md")));
314
+ assert.ok(!fs.existsSync(path.join(splitProject, "genesis.md")));
315
+
316
+ const operaControl = readJson(path.join(splitProject, "ops", "project_control.json"));
317
+ assert.strictEqual(operaControl.meta.locale, "en");
318
+ assert.strictEqual(operaControl.meta.opera.installed, true);
319
+ assert.ok(operaControl.meta.environment.requiredKeys.includes("OPENAI_API_KEY"));
320
+ const envRootText = fs.readFileSync(path.join(splitProject, ".env"), "utf8");
321
+ assert.match(envRootText, /OPENAI_API_KEY=/);
322
+
323
+ const defaultPortFree = await isPortFree(4173);
324
+ const defaultDashboard = startDashboard(splitProject);
325
+
326
+ try {
327
+ const ready = await waitForDashboard(defaultDashboard);
328
+ const state = await get(ready.port, "/api/state");
329
+ const envPayload = await get(ready.port, "/api/env");
330
+ const localSkills = await get(ready.port, "/api/skills/local");
331
+ const discoverSkills = await get(ready.port, "/api/skills/discover");
332
+
333
+ assert.strictEqual(state.status, 200);
334
+ assert.strictEqual(envPayload.status, 200);
335
+ assert.strictEqual(localSkills.status, 200);
336
+ assert.strictEqual(discoverSkills.status, 200);
337
+
338
+ const statePayload = JSON.parse(state.body);
339
+ assert.strictEqual(statePayload.project.layout, "split");
340
+ assert.strictEqual(statePayload.project.workspaceRoot, splitProject);
341
+ assert.strictEqual(statePayload.project.appRoot, path.join(splitProject, "app"));
342
+ assert.strictEqual(statePayload.project.opsRoot, path.join(splitProject, "ops"));
343
+ assert.ok(Array.isArray(statePayload.env.requiredKeys));
344
+ assert.ok(!Object.prototype.hasOwnProperty.call(statePayload.env, "values"));
345
+
346
+ const envState = JSON.parse(envPayload.body);
347
+ assert.ok(envState.requiredKeys.includes("OPENAI_API_KEY"));
348
+ assert.ok(envState.missingKeys.includes("OPENAI_API_KEY"));
349
+
350
+ if (defaultPortFree) {
351
+ assert.strictEqual(ready.port, 4173, `se esperaba usar 4173 cuando estaba libre:\n${ready.output}`);
352
+ }
353
+ } finally {
354
+ await stopDashboard(defaultDashboard);
355
+ }
356
+
357
+ const blocked4173 = await isPortFree(4173) ? await occupyPort(4173) : null;
358
+ const fallbackDashboard = startDashboard(splitProject);
359
+ try {
360
+ const ready = await waitForDashboard(fallbackDashboard);
361
+ assert.notStrictEqual(ready.port, 4173, `deberia haber evitado 4173 ocupado:\n${ready.output}`);
362
+ assert.match(ready.output, /Local:/);
363
+ } finally {
364
+ await stopDashboard(fallbackDashboard);
365
+ if (blocked4173) blocked4173.close();
366
+ }
367
+
368
+ const strictPort = await findFreePort();
369
+ const occupiedStrictPort = await occupyPort(strictPort);
370
+ try {
371
+ const strictResult = runNodeResult([BIN, "dashboard", "--strict-port", "--port", String(strictPort)], splitProject);
372
+ assert.notStrictEqual(strictResult.status, 0, "el dashboard deberia fallar con --strict-port si el puerto esta ocupado");
373
+ assert.match(`${strictResult.stdout}\n${strictResult.stderr}`, /already in use|esta en uso/i);
374
+ } finally {
375
+ occupiedStrictPort.close();
376
+ }
377
+
378
+ const publicPort = await findFreePort("0.0.0.0");
379
+ const publicDashboard = startDashboard(splitProject, ["--public", "--port", String(publicPort)]);
380
+ try {
381
+ const ready = await waitForDashboard(publicDashboard);
382
+ if (hasExternalIpv4()) {
383
+ assert.match(ready.output, /- Network:/);
384
+ }
385
+ } finally {
386
+ await stopDashboard(publicDashboard);
387
+ }
388
+
389
+ const noClipboardPort = await findFreePort();
390
+ const noClipboardDashboard = startDashboard(splitProject, ["--port", String(noClipboardPort)], {
391
+ PATH: "",
392
+ Path: "",
393
+ });
394
+ try {
395
+ const ready = await waitForDashboard(noClipboardDashboard);
396
+ const state = await get(ready.port, "/api/state");
397
+ assert.strictEqual(state.status, 200);
398
+ } finally {
399
+ await stopDashboard(noClipboardDashboard);
400
+ }
401
+
402
+ const legacyProject = path.join(tempRoot, "legacy-demo");
403
+ fs.mkdirSync(legacyProject, { recursive: true });
404
+ initGitRepo(legacyProject);
405
+ writeJson(path.join(legacyProject, "package.json"), { name: "legacy-demo", version: "1.0.0" });
406
+ runNode([BIN, "init", "--legacy-layout"], legacyProject);
407
+ runNode([BIN, "opera", "install", "--locale", "en", "--no-bootstrap"], legacyProject);
408
+ fs.writeFileSync(path.join(legacyProject, ".env"), "OPENAI_API_KEY=\n", "utf8");
409
+ fs.writeFileSync(path.join(legacyProject, ".env.example"), "OPENAI_API_KEY=\n", "utf8");
410
+ commitAll(legacyProject, "legacy fixture");
411
+
412
+ runNode([BIN, "workspace", "migrate"], legacyProject);
413
+ assert.ok(fs.existsSync(path.join(legacyProject, ".trackops-workspace.json")));
414
+ assert.ok(fs.existsSync(path.join(legacyProject, "app", "package.json")));
415
+ assert.ok(fs.existsSync(path.join(legacyProject, "ops", "project_control.json")));
416
+ assert.ok(fs.existsSync(path.join(legacyProject, ".env")));
417
+ assert.ok(fs.existsSync(path.join(legacyProject, "app", ".env")));
418
+ assert.ok(!fs.existsSync(path.join(legacyProject, "project_control.json")));
419
+ const migratedPackage = readJson(path.join(legacyProject, "app", "package.json"));
420
+ assert.ok(!migratedPackage.scripts || !migratedPackage.scripts["ops:status"]);
421
+ assert.match(git(["branch", "--list"], legacyProject), /backup\/trackops-workspace-/);
422
+
423
+ const releaseProject = path.join(tempRoot, "release-demo");
424
+ fs.mkdirSync(releaseProject, { recursive: true });
425
+ initGitRepo(releaseProject);
426
+ runNode([BIN, "init"], releaseProject);
427
+ writeJson(path.join(releaseProject, "app", "package.json"), { name: "release-demo", version: "1.0.0" });
428
+ fs.writeFileSync(path.join(releaseProject, "app", "index.js"), "console.log('release');\n", "utf8");
429
+ commitAll(releaseProject, "split fixture");
430
+ git(["checkout", "-b", "develop"], releaseProject);
431
+ runNode([BIN, "release"], releaseProject);
432
+ const publishFiles = git(["ls-tree", "--name-only", "master"], releaseProject).split(/\r?\n/).filter(Boolean);
433
+ assert.ok(publishFiles.includes("package.json"));
434
+ assert.ok(publishFiles.includes(".env.example"));
435
+ assert.ok(!publishFiles.includes("ops"));
436
+ assert.ok(!publishFiles.includes(".trackops-workspace.json"));
437
+
438
+ fs.rmSync(tempRoot, { recursive: true, force: true });
439
+ console.log("Smoke tests OK");
440
+ }
441
+
442
+ main().catch((error) => {
443
+ console.error(error.message);
444
+ process.exit(1);
445
+ });
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
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();
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
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_DIR = path.join(ROOT, "skills", "trackops");
9
+ const SKILL_FILE = path.join(SKILL_DIR, "skill.json");
10
+ const REQUIRED_FILES = [
11
+ path.join(SKILL_DIR, "SKILL.md"),
12
+ path.join(SKILL_DIR, "skill.json"),
13
+ path.join(SKILL_DIR, "scripts", "bootstrap-trackops.js"),
14
+ path.join(SKILL_DIR, "references", "activation.md"),
15
+ path.join(SKILL_DIR, "references", "workflow.md"),
16
+ path.join(SKILL_DIR, "references", "troubleshooting.md"),
17
+ ];
18
+
19
+ function fail(message) {
20
+ console.error(message);
21
+ process.exit(1);
22
+ }
23
+
24
+ function main() {
25
+ const pkg = JSON.parse(fs.readFileSync(PACKAGE_FILE, "utf8"));
26
+ const skill = JSON.parse(fs.readFileSync(SKILL_FILE, "utf8"));
27
+ const skillMd = fs.readFileSync(path.join(SKILL_DIR, "SKILL.md"), "utf8");
28
+
29
+ for (const file of REQUIRED_FILES) {
30
+ if (!fs.existsSync(file)) {
31
+ fail(`Missing required skill file: ${path.relative(ROOT, file)}`);
32
+ }
33
+ }
34
+
35
+ if (!Array.isArray(pkg.files) || !pkg.files.includes("skills/")) {
36
+ fail("package.json must publish the skills/ directory.");
37
+ }
38
+
39
+ if (skill.name !== "trackops") {
40
+ fail("skills/trackops/skill.json must declare name 'trackops'.");
41
+ }
42
+
43
+ if (skill.skillVersion !== pkg.version || skill.trackopsVersion !== pkg.version) {
44
+ fail(`skills/trackops/skill.json must be synced to package version ${pkg.version}.`);
45
+ }
46
+
47
+ if (skill.npmPackage !== pkg.name) {
48
+ fail(`skills/trackops/skill.json must target npm package '${pkg.name}'.`);
49
+ }
50
+
51
+ if (skill.bootstrapPolicy !== "first_use") {
52
+ fail("skills/trackops/skill.json must use bootstrapPolicy 'first_use'.");
53
+ }
54
+
55
+ const supportedAgents = Array.isArray(skill.supportedAgentsV1) ? skill.supportedAgentsV1 : [];
56
+ for (const agent of ["antigravity", "claude-code", "codex", "cursor", "gemini-cli", "github-copilot", "kiro-cli"]) {
57
+ if (!supportedAgents.includes(agent)) {
58
+ fail(`skills/trackops/skill.json must include supported agent '${agent}'.`);
59
+ }
60
+ }
61
+
62
+ if (!skill.distribution || skill.distribution.source !== "Baxahaun/trackops") {
63
+ fail("skills/trackops/skill.json must declare distribution.source 'Baxahaun/trackops'.");
64
+ }
65
+
66
+ if (skill.distribution.skill !== "trackops") {
67
+ fail("skills/trackops/skill.json must declare distribution.skill 'trackops'.");
68
+ }
69
+
70
+ if (skill.distribution.fullDepth !== true) {
71
+ fail("skills/trackops/skill.json must declare distribution.fullDepth true.");
72
+ }
73
+
74
+ for (const requiredPhrase of [
75
+ "npx skills add Baxahaun/trackops --skill trackops --full-depth",
76
+ "node scripts/bootstrap-trackops.js",
77
+ "trackops init",
78
+ "trackops opera install",
79
+ ]) {
80
+ if (!skillMd.includes(requiredPhrase)) {
81
+ fail(`skills/trackops/SKILL.md must mention '${requiredPhrase}'.`);
82
+ }
83
+ }
84
+
85
+ console.log("skills/trackops validated successfully.");
86
+ }
87
+
88
+ main();
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: "trackops"
3
+ description: "Global TrackOps skill that prepares your agent for local project orchestration and operational automation, ensures the runtime on first use, and guides per-project activation with optional OPERA."
4
+ metadata:
5
+ version: "1.1.0"
6
+ type: "global"
7
+ triggers:
8
+ - "install trackops"
9
+ - "skills.sh"
10
+ - "bootstrap trackops"
11
+ - "trackops init"
12
+ - "trackops opera install"
13
+ ---
14
+
15
+ # TrackOps
16
+
17
+ Use this skill in two layers:
18
+
19
+ 1. Global skill layer
20
+ Install it with:
21
+
22
+ ```bash
23
+ npx skills add Baxahaun/trackops --skill trackops --full-depth --global --agent codex -y
24
+ ```
25
+
26
+ Replace `codex` with any supported target: `antigravity`, `claude-code`, `codex`, `cursor`, `gemini-cli`, `github-copilot`, or `kiro-cli`.
27
+
28
+ Before relying on the CLI, run:
29
+
30
+ ```bash
31
+ node scripts/bootstrap-trackops.js
32
+ ```
33
+
34
+ 2. Local project layer
35
+ Activate TrackOps inside the current repository with:
36
+
37
+ ```bash
38
+ trackops init
39
+ ```
40
+
41
+ Add OPERA only when explicitly requested:
42
+
43
+ ```bash
44
+ trackops opera install
45
+ ```
46
+
47
+ Core rules:
48
+
49
+ - Treat the global skill install as non-invasive.
50
+ - In split workspaces, use `ops/project_control.json` as the operational source of truth.
51
+ - In legacy repos, use `project_control.json` at the repository root.
52
+ - Prefer `trackops status`, `trackops next`, and `trackops sync` over hand-editing generated docs.
53
+ - Treat `trackops init --with-opera` as a shortcut, not as the primary mental model.
54
+ - TrackOps manages `/.env` and `/.env.example` at workspace root. Do not print or persist secret values.
55
+ - Remember that skills installs from committed Git state.
56
+
57
+ Read references only when needed:
58
+
59
+ - `references/activation.md`
60
+ for install and activation flow
61
+ - `references/workflow.md`
62
+ for day-to-day repo operation
63
+ - `references/troubleshooting.md`
64
+ for bootstrap or environment issues
@@ -0,0 +1,3 @@
1
+ interface:
2
+ display_name: "TrackOps"
3
+ short_description: "Global TrackOps skill for local project orchestration and operational automation"
@@ -0,0 +1,39 @@
1
+ # Activation
2
+
3
+ ## Global install
4
+
5
+ The marketplace skill prepares TrackOps globally for the agent. It must not create repo files by itself.
6
+
7
+ Install it with:
8
+
9
+ ```bash
10
+ npx skills add Baxahaun/trackops --skill trackops --full-depth --global --agent codex -y
11
+ ```
12
+
13
+ Replace `codex` with any supported target: `antigravity`, `claude-code`, `codex`, `cursor`, `gemini-cli`, `github-copilot`, or `kiro-cli`.
14
+
15
+ Before using TrackOps commands, run:
16
+
17
+ ```bash
18
+ node scripts/bootstrap-trackops.js
19
+ ```
20
+
21
+ That bootstrap validates prerequisites, installs or updates the TrackOps runtime, and records state in `~/.trackops/runtime.json`.
22
+
23
+ ## Local activation
24
+
25
+ Inside a repository, the normal flow is:
26
+
27
+ ```bash
28
+ trackops init
29
+ trackops opera install
30
+ ```
31
+
32
+ Rules:
33
+
34
+ - Use `trackops init` when the repo is not yet managed by TrackOps.
35
+ - By default, `trackops init` creates a split workspace with `app/`, `ops/`, `/.env`, `/.env.example`, and `.trackops-workspace.json`.
36
+ - Use `trackops opera install` only after `trackops init` and only when the user wants OPERA.
37
+ - Use `trackops init --with-opera` only as an explicit shortcut.
38
+ - Use `trackops init --legacy-layout` only for compatibility with the old single-root layout.
39
+ - Never assume that a global skill install authorizes local repo mutations by default.