trackops 2.1.0 → 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 (37) hide show
  1. package/README.md +14 -2
  2. package/lib/opera.js +1 -1
  3. package/lib/skills.js +94 -41
  4. package/package.json +6 -6
  5. package/scripts/skills-marketplace-smoke.js +156 -124
  6. package/scripts/smoke-tests.js +21 -14
  7. package/scripts/sync-skill-version.js +29 -19
  8. package/scripts/validate-skill.js +188 -103
  9. package/skills/trackops/SKILL.md +25 -7
  10. package/skills/trackops/locales/en/SKILL.md +25 -7
  11. package/skills/trackops/locales/en/references/activation.md +3 -3
  12. package/skills/trackops/locales/en/references/workflow.md +5 -4
  13. package/skills/trackops/references/activation.md +3 -3
  14. package/skills/trackops/references/workflow.md +5 -4
  15. package/skills/trackops/skill.json +2 -2
  16. package/skills/trackops-quality-guard/SKILL.md +78 -0
  17. package/skills/trackops-quality-guard/agents/openai.yaml +7 -0
  18. package/skills/trackops-quality-guard/locales/en/SKILL.md +78 -0
  19. package/skills/trackops-quality-guard/locales/en/references/commands.md +36 -0
  20. package/skills/trackops-quality-guard/locales/en/references/decision-policy.md +16 -0
  21. package/skills/trackops-quality-guard/locales/en/references/output-format.md +24 -0
  22. package/skills/trackops-quality-guard/references/commands.md +36 -0
  23. package/skills/trackops-quality-guard/references/decision-policy.md +16 -0
  24. package/skills/trackops-quality-guard/references/output-format.md +24 -0
  25. package/skills/trackops-quality-guard/skill.json +28 -0
  26. package/templates/skills/opera-skill/SKILL.md +4 -0
  27. package/templates/skills/opera-skill/locales/en/SKILL.md +4 -0
  28. package/templates/skills/trackops-quality-guard/SKILL.md +72 -0
  29. package/templates/skills/trackops-quality-guard/locales/en/SKILL.md +72 -0
  30. package/templates/skills/trackops-quality-guard/locales/en/references/commands.md +30 -0
  31. package/templates/skills/trackops-quality-guard/locales/en/references/decision-policy.md +14 -0
  32. package/templates/skills/trackops-quality-guard/locales/en/references/output-format.md +21 -0
  33. package/templates/skills/trackops-quality-guard/references/commands.md +30 -0
  34. package/templates/skills/trackops-quality-guard/references/decision-policy.md +14 -0
  35. package/templates/skills/trackops-quality-guard/references/output-format.md +21 -0
  36. package/templates/skills/opera-quality-guard/SKILL.md +0 -26
  37. package/templates/skills/opera-quality-guard/locales/en/SKILL.md +0 -26
package/README.md CHANGED
@@ -31,7 +31,7 @@ No sustituye tu producto. Ordena el trabajo que ocurre alrededor del producto:
31
31
  - define una fuente de verdad operativa dentro del repo
32
32
  - separa producto y operaciones para que no se mezclen
33
33
  - da contexto persistente a los agentes entre sesiones
34
- - expone estado, tareas, calidad y release desde CLI y dashboard
34
+ - expone estado, tareas, Quality Guard, readiness y release desde CLI y dashboard
35
35
 
36
36
  Si quieres ir más allá, puedes instalar OPERA, la metodología opcional que añade discovery, contrato operativo, readiness y un equipo coordinado de especialistas.
37
37
 
@@ -152,6 +152,12 @@ trackops dashboard
152
152
  trackops quality status
153
153
  ```
154
154
 
155
+ Si quieres una skill pública centrada en esta capa de auditoría, verificación y readiness:
156
+
157
+ ```bash
158
+ npx skills add Baxahaun/trackops --skill trackops-quality-guard --agent "*" --global -y
159
+ ```
160
+
155
161
  Para release:
156
162
 
157
163
  ```bash
@@ -181,7 +187,7 @@ It does not replace your product. It organizes the work around the product:
181
187
  - defines an operational source of truth inside the repository
182
188
  - keeps product and operations separate
183
189
  - gives agents persistent context across sessions
184
- - exposes status, tasks, quality, and release state through CLI and dashboard
190
+ - exposes status, tasks, Quality Guard, readiness, and release state through CLI and dashboard
185
191
 
186
192
  If you need a stricter operating model, you can install OPERA, the optional methodology that adds discovery, an operating contract, readiness checks, and a coordinated team of specialists.
187
193
 
@@ -302,6 +308,12 @@ trackops dashboard
302
308
  trackops quality status
303
309
  ```
304
310
 
311
+ If you want a public skill focused on this audit, verification, and readiness layer:
312
+
313
+ ```bash
314
+ npx skills add Baxahaun/trackops --skill trackops-quality-guard --agent "*" --global -y
315
+ ```
316
+
305
317
  For release:
306
318
 
307
319
  ```bash
package/lib/opera.js CHANGED
@@ -15,7 +15,7 @@ const fmt = require("./cli-format");
15
15
  const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
16
16
  const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
17
17
  const OPERA_VERSION = require("../package.json").version;
18
- const AUXILIARY_SKILLS = ["opera-skill", "opera-quality-guard", "project-starter-skill", "opera-contract-auditor", "opera-policy-guard"];
18
+ const AUXILIARY_SKILLS = ["opera-skill", "trackops-quality-guard", "project-starter-skill", "opera-contract-auditor", "opera-policy-guard"];
19
19
 
20
20
  function nowIso() {
21
21
  return new Date().toISOString();
package/lib/skills.js CHANGED
@@ -8,15 +8,16 @@ const { t, setLocale } = require("./i18n");
8
8
  const { resolveSkillFile } = require("./resources");
9
9
  const fmt = require("./cli-format");
10
10
 
11
- const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
12
- const INSTALLED_SKILL_PRIORITY = [
13
- "opera-skill",
14
- "project-starter-skill",
15
- "opera-contract-auditor",
16
- "opera-policy-guard",
17
- "commiter",
18
- "changelog-updater",
19
- ];
11
+ const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
12
+ const INSTALLED_SKILL_PRIORITY = [
13
+ "opera-skill",
14
+ "project-starter-skill",
15
+ "trackops-quality-guard",
16
+ "opera-contract-auditor",
17
+ "opera-policy-guard",
18
+ "commiter",
19
+ "changelog-updater",
20
+ ];
20
21
 
21
22
  function copyDirRecursive(src, dest) {
22
23
  fs.mkdirSync(dest, { recursive: true });
@@ -31,20 +32,62 @@ function copyDirRecursive(src, dest) {
31
32
  }
32
33
  }
33
34
 
34
- function parseFrontmatter(content) {
35
- const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
36
- if (!match) return {};
37
- const fm = {};
38
- for (const line of match[1].split("\n")) {
39
- const sep = line.indexOf(":");
40
- if (sep > 0) {
41
- const key = line.slice(0, sep).trim().replace(/^["']|["']$/g, "");
42
- const val = line.slice(sep + 1).trim().replace(/^["']|["']$/g, "");
43
- fm[key] = val;
44
- }
45
- }
46
- return fm;
47
- }
35
+ function stripFrontmatterValue(value) {
36
+ return String(value || "").trim().replace(/^["']|["']$/g, "");
37
+ }
38
+
39
+ function parseFrontmatter(content) {
40
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
41
+ if (!match) return {};
42
+ const lines = match[1].split("\n");
43
+ const root = {};
44
+ const stack = [{ indent: -1, container: root }];
45
+
46
+ for (let index = 0; index < lines.length; index += 1) {
47
+ const line = lines[index].replace(/\t/g, " ");
48
+ const trimmed = line.trim();
49
+ if (!trimmed || trimmed.startsWith("#")) continue;
50
+ const indent = line.match(/^ */)[0].length;
51
+
52
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
53
+ stack.pop();
54
+ }
55
+
56
+ const current = stack[stack.length - 1].container;
57
+ if (trimmed.startsWith("- ")) {
58
+ if (Array.isArray(current)) {
59
+ current.push(stripFrontmatterValue(trimmed.slice(2)));
60
+ }
61
+ continue;
62
+ }
63
+
64
+ const sep = trimmed.indexOf(":");
65
+ if (sep <= 0 || Array.isArray(current)) continue;
66
+
67
+ const key = stripFrontmatterValue(trimmed.slice(0, sep));
68
+ const rawValue = trimmed.slice(sep + 1).trim();
69
+ if (rawValue) {
70
+ current[key] = stripFrontmatterValue(rawValue);
71
+ continue;
72
+ }
73
+
74
+ let child = {};
75
+ for (let peekIndex = index + 1; peekIndex < lines.length; peekIndex += 1) {
76
+ const peekLine = lines[peekIndex].replace(/\t/g, " ");
77
+ const peekTrimmed = peekLine.trim();
78
+ if (!peekTrimmed || peekTrimmed.startsWith("#")) continue;
79
+ const peekIndent = peekLine.match(/^ */)[0].length;
80
+ if (peekIndent <= indent) break;
81
+ child = peekTrimmed.startsWith("- ") ? [] : {};
82
+ break;
83
+ }
84
+
85
+ current[key] = child;
86
+ stack.push({ indent, container: child });
87
+ }
88
+
89
+ return root;
90
+ }
48
91
 
49
92
  function getSkillsDir(root) {
50
93
  return config.ensureContext(root).paths.skillsDir;
@@ -54,29 +97,39 @@ function getRegistryPath(root) {
54
97
  return config.ensureContext(root).paths.registryPath;
55
98
  }
56
99
 
57
- function catalogSkills() {
58
- if (!fs.existsSync(SKILLS_TEMPLATES_DIR)) return [];
59
- return fs.readdirSync(SKILLS_TEMPLATES_DIR, { withFileTypes: true })
60
- .filter((e) => e.isDirectory())
61
- .map((e) => {
62
- const skillMd = path.join(SKILLS_TEMPLATES_DIR, e.name, "SKILL.md");
63
- if (!fs.existsSync(skillMd)) return null;
64
- const fm = parseFrontmatter(fs.readFileSync(skillMd, "utf8"));
65
- return { name: e.name, description: fm.description || "", version: fm.version || "1.0" };
66
- })
67
- .filter(Boolean);
68
- }
100
+ function catalogSkills() {
101
+ if (!fs.existsSync(SKILLS_TEMPLATES_DIR)) return [];
102
+ return fs.readdirSync(SKILLS_TEMPLATES_DIR, { withFileTypes: true })
103
+ .filter((e) => e.isDirectory())
104
+ .map((e) => {
105
+ const skillMd = path.join(SKILLS_TEMPLATES_DIR, e.name, "SKILL.md");
106
+ if (!fs.existsSync(skillMd)) return null;
107
+ const fm = parseFrontmatter(fs.readFileSync(skillMd, "utf8"));
108
+ return {
109
+ name: e.name,
110
+ description: fm.description || "",
111
+ version: fm.metadata?.version || fm.version || "1.0",
112
+ triggers: Array.isArray(fm.metadata?.triggers) ? fm.metadata.triggers : [],
113
+ };
114
+ })
115
+ .filter(Boolean)
116
+ .sort((a, b) => a.name.localeCompare(b.name));
117
+ }
69
118
 
70
119
  function installedSkills(root) {
71
120
  const skillsDir = getSkillsDir(root);
72
121
  if (!fs.existsSync(skillsDir)) return [];
73
122
  const skills = fs.readdirSync(skillsDir, { withFileTypes: true })
74
- .filter((e) => e.isDirectory() && fs.existsSync(path.join(skillsDir, e.name, "SKILL.md")))
75
- .map((e) => {
76
- const fm = parseFrontmatter(fs.readFileSync(path.join(skillsDir, e.name, "SKILL.md"), "utf8"));
77
- return { name: e.name, description: fm.description || "", version: fm.version || "1.0" };
78
- })
79
- .filter(Boolean);
123
+ .filter((e) => e.isDirectory() && fs.existsSync(path.join(skillsDir, e.name, "SKILL.md")))
124
+ .map((e) => {
125
+ const fm = parseFrontmatter(fs.readFileSync(path.join(skillsDir, e.name, "SKILL.md"), "utf8"));
126
+ return {
127
+ name: e.name,
128
+ description: fm.description || "",
129
+ version: fm.metadata?.version || fm.version || "1.0",
130
+ };
131
+ })
132
+ .filter(Boolean);
80
133
  skills.sort((a, b) => {
81
134
  const ai = INSTALLED_SKILL_PRIORITY.indexOf(a.name);
82
135
  const bi = INSTALLED_SKILL_PRIORITY.indexOf(b.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackops",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Local control and coordination system for AI-agent software development",
5
5
  "main": "lib/control.js",
6
6
  "bin": {
@@ -49,11 +49,11 @@
49
49
  "engines": {
50
50
  "node": ">=18"
51
51
  },
52
- "scripts": {
53
- "postinstall": "node scripts/postinstall-locale.js",
54
- "test": "node --test scripts/quality-unit-tests.js && node scripts/smoke-tests.js",
55
- "test:unit": "node --test scripts/quality-unit-tests.js",
56
- "test:smoke": "node scripts/smoke-tests.js",
52
+ "scripts": {
53
+ "postinstall": "node scripts/postinstall-locale.js",
54
+ "test": "node --test scripts/quality-unit-tests.js && node scripts/smoke-tests.js",
55
+ "test:unit": "node --test scripts/quality-unit-tests.js",
56
+ "test:smoke": "node scripts/smoke-tests.js",
57
57
  "skill:sync-version": "node scripts/sync-skill-version.js",
58
58
  "skill:validate": "node scripts/validate-skill.js",
59
59
  "skill:smoke": "node scripts/skills-marketplace-smoke.js",
@@ -1,124 +1,156 @@
1
- #!/usr/bin/env node
2
-
3
- const assert = require("assert");
4
- const fs = require("fs");
5
- const os = require("os");
6
- const path = require("path");
7
- const { spawnSync } = require("child_process");
8
-
9
- const ROOT = path.resolve(__dirname, "..");
10
- const SKILL_DIR = path.join(ROOT, "skills", "trackops");
11
- const SKILL_CONFIG = JSON.parse(fs.readFileSync(path.join(SKILL_DIR, "skill.json"), "utf8"));
12
-
13
- function run(command, args, cwd, envOverrides = {}) {
14
- const shell = process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
15
- return spawnSync(command, args, {
16
- cwd,
17
- encoding: "utf8",
18
- env: { ...process.env, ...envOverrides },
19
- shell,
20
- });
21
- }
22
-
23
- function getNpxCommand() {
24
- return process.platform === "win32" ? "npx.cmd" : "npx";
25
- }
26
-
27
- function ensureOk(result, context) {
28
- const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
29
- assert.strictEqual(result.status, 0, output || context);
30
- return output;
31
- }
32
-
33
- function initGitRepo(repo) {
34
- ensureOk(run("git", ["init"], repo), "git init failed");
35
- ensureOk(run("git", ["config", "user.email", "skills-smoke@example.com"], repo), "git config email failed");
36
- ensureOk(run("git", ["config", "user.name", "Skills Smoke"], repo), "git config name failed");
37
- ensureOk(run("git", ["add", "."], repo), "git add failed");
38
- ensureOk(run("git", ["commit", "-m", "skills smoke fixture"], repo), "git commit failed");
39
- }
40
-
41
- function buildIsolatedEnv(homeRoot) {
42
- const env = {
43
- HOME: homeRoot,
44
- USERPROFILE: homeRoot,
45
- APPDATA: path.join(homeRoot, "AppData", "Roaming"),
46
- LOCALAPPDATA: path.join(homeRoot, "AppData", "Local"),
47
- XDG_CONFIG_HOME: path.join(homeRoot, ".config"),
48
- };
49
- for (const value of Object.values(env)) {
50
- fs.mkdirSync(value, { recursive: true });
51
- }
52
- return env;
53
- }
54
-
55
- function findInstalledSkill(rootDir, skillName) {
56
- const matches = [];
57
-
58
- function walk(dir) {
59
- const entries = fs.readdirSync(dir, { withFileTypes: true });
60
- for (const entry of entries) {
61
- const fullPath = path.join(dir, entry.name);
62
- if (entry.isDirectory()) {
63
- walk(fullPath);
64
- continue;
65
- }
66
- if (entry.isFile() && entry.name === "SKILL.md" && path.basename(path.dirname(fullPath)) === skillName) {
67
- matches.push(path.dirname(fullPath));
68
- }
69
- }
70
- }
71
-
72
- walk(rootDir);
73
- return matches;
74
- }
75
-
76
- function main() {
77
- const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "trackops-skills-smoke-"));
78
- const sourceRepo = path.join(tempRoot, "source");
79
- const sourceSkillDir = path.join(sourceRepo, "skills", "trackops");
80
- const homeRoot = path.join(tempRoot, "home");
81
-
82
- fs.mkdirSync(path.dirname(sourceSkillDir), { recursive: true });
83
- fs.cpSync(SKILL_DIR, sourceSkillDir, { recursive: true });
84
- initGitRepo(sourceRepo);
85
-
86
- const env = buildIsolatedEnv(homeRoot);
87
- const distribution = SKILL_CONFIG.distribution || {};
88
- const skillName = distribution.skill || SKILL_CONFIG.name;
89
-
90
- const listResult = run(
91
- getNpxCommand(),
92
- ["--yes", "skills", "add", sourceRepo, "--list", "--skill", skillName, "--full-depth", "-y"],
93
- ROOT,
94
- env,
95
- );
96
- const listOutput = ensureOk(listResult, "skills list failed");
97
- assert.match(listOutput, /\btrackops\b/i, listOutput);
98
-
99
- const installResult = run(
100
- getNpxCommand(),
101
- ["--yes", "skills", "add", sourceRepo, "--skill", skillName, "--full-depth", "--global", "--agent", "codex", "--copy", "-y"],
102
- ROOT,
103
- env,
104
- );
105
- ensureOk(installResult, "skills install failed");
106
-
107
- const installed = findInstalledSkill(homeRoot, skillName);
108
- assert.ok(installed.length >= 1, `trackops skill was not installed under ${homeRoot}`);
109
-
110
- const installedSkillDir = installed[0];
111
- assert.ok(fs.existsSync(path.join(installedSkillDir, "references", "activation.md")));
112
- assert.ok(fs.existsSync(path.join(installedSkillDir, "skill.json")));
113
- assert.ok(!fs.existsSync(path.join(installedSkillDir, "scripts", "bootstrap-trackops.js")));
114
-
115
- fs.rmSync(tempRoot, { recursive: true, force: true });
116
- console.log("skills marketplace smoke OK");
117
- }
118
-
119
- try {
120
- main();
121
- } catch (error) {
122
- console.error(error.message);
123
- process.exit(1);
124
- }
1
+ #!/usr/bin/env node
2
+
3
+ const assert = require("assert");
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const { spawnSync } = require("child_process");
8
+
9
+ const ROOT = path.resolve(__dirname, "..");
10
+ const SKILLS_ROOT = path.join(ROOT, "skills");
11
+ const REQUIRED_PUBLIC_SKILLS = ["trackops", "trackops-quality-guard"];
12
+
13
+ const REQUIRED_FILES_BY_SKILL = {
14
+ trackops: [
15
+ "references/activation.md",
16
+ "references/workflow.md",
17
+ "references/troubleshooting.md",
18
+ "skill.json",
19
+ ],
20
+ "trackops-quality-guard": [
21
+ "references/commands.md",
22
+ "references/decision-policy.md",
23
+ "references/output-format.md",
24
+ "skill.json",
25
+ ],
26
+ };
27
+
28
+ function run(command, args, cwd, envOverrides = {}) {
29
+ const shell = process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
30
+ return spawnSync(command, args, {
31
+ cwd,
32
+ encoding: "utf8",
33
+ env: { ...process.env, ...envOverrides },
34
+ shell,
35
+ });
36
+ }
37
+
38
+ function getNpxCommand() {
39
+ return process.platform === "win32" ? "npx.cmd" : "npx";
40
+ }
41
+
42
+ function ensureOk(result, context) {
43
+ const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
44
+ assert.strictEqual(result.status, 0, output || context);
45
+ return output;
46
+ }
47
+
48
+ function initGitRepo(repo) {
49
+ ensureOk(run("git", ["init"], repo), "git init failed");
50
+ ensureOk(run("git", ["config", "user.email", "skills-smoke@example.com"], repo), "git config email failed");
51
+ ensureOk(run("git", ["config", "user.name", "Skills Smoke"], repo), "git config name failed");
52
+ ensureOk(run("git", ["add", "."], repo), "git add failed");
53
+ ensureOk(run("git", ["commit", "-m", "skills smoke fixture"], repo), "git commit failed");
54
+ }
55
+
56
+ function buildIsolatedEnv(homeRoot) {
57
+ const env = {
58
+ HOME: homeRoot,
59
+ USERPROFILE: homeRoot,
60
+ APPDATA: path.join(homeRoot, "AppData", "Roaming"),
61
+ LOCALAPPDATA: path.join(homeRoot, "AppData", "Local"),
62
+ XDG_CONFIG_HOME: path.join(homeRoot, ".config"),
63
+ };
64
+ for (const value of Object.values(env)) {
65
+ fs.mkdirSync(value, { recursive: true });
66
+ }
67
+ return env;
68
+ }
69
+
70
+ function findInstalledSkill(rootDir, skillName) {
71
+ const matches = [];
72
+
73
+ function walk(dir) {
74
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
75
+ for (const entry of entries) {
76
+ const fullPath = path.join(dir, entry.name);
77
+ if (entry.isDirectory()) {
78
+ walk(fullPath);
79
+ continue;
80
+ }
81
+ if (entry.isFile() && entry.name === "SKILL.md" && path.basename(path.dirname(fullPath)) === skillName) {
82
+ matches.push(path.dirname(fullPath));
83
+ }
84
+ }
85
+ }
86
+
87
+ walk(rootDir);
88
+ return matches;
89
+ }
90
+
91
+ function discoverPublicSkills() {
92
+ return fs.readdirSync(SKILLS_ROOT, { withFileTypes: true })
93
+ .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(SKILLS_ROOT, entry.name, "skill.json")))
94
+ .map((entry) => {
95
+ const dir = path.join(SKILLS_ROOT, entry.name);
96
+ const config = JSON.parse(fs.readFileSync(path.join(dir, "skill.json"), "utf8"));
97
+ return { name: entry.name, dir, config };
98
+ })
99
+ .sort((a, b) => a.name.localeCompare(b.name));
100
+ }
101
+
102
+ function main() {
103
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "trackops-skills-smoke-"));
104
+ const sourceRepo = path.join(tempRoot, "source");
105
+ const homeRoot = path.join(tempRoot, "home");
106
+
107
+ fs.mkdirSync(sourceRepo, { recursive: true });
108
+ fs.cpSync(SKILLS_ROOT, path.join(sourceRepo, "skills"), { recursive: true });
109
+ initGitRepo(sourceRepo);
110
+
111
+ const env = buildIsolatedEnv(homeRoot);
112
+ const skills = discoverPublicSkills();
113
+ for (const skillName of REQUIRED_PUBLIC_SKILLS) {
114
+ assert.ok(skills.some((entry) => entry.name === skillName), `missing public skill ${skillName}`);
115
+ }
116
+
117
+ for (const entry of skills) {
118
+ const distribution = entry.config.distribution || {};
119
+ const skillName = distribution.skill || entry.config.name;
120
+
121
+ const listResult = run(
122
+ getNpxCommand(),
123
+ ["--yes", "skills", "add", sourceRepo, "--list", "--skill", skillName, "--full-depth", "-y"],
124
+ ROOT,
125
+ env,
126
+ );
127
+ const listOutput = ensureOk(listResult, `skills list failed for ${skillName}`);
128
+ assert.ok(listOutput.includes(skillName), listOutput);
129
+
130
+ const installResult = run(
131
+ getNpxCommand(),
132
+ ["--yes", "skills", "add", sourceRepo, "--skill", skillName, "--full-depth", "--global", "--agent", "codex", "--copy", "-y"],
133
+ ROOT,
134
+ env,
135
+ );
136
+ ensureOk(installResult, `skills install failed for ${skillName}`);
137
+
138
+ const installed = findInstalledSkill(homeRoot, skillName);
139
+ assert.ok(installed.length >= 1, `${skillName} was not installed under ${homeRoot}`);
140
+
141
+ const installedSkillDir = installed[0];
142
+ for (const relativeFile of REQUIRED_FILES_BY_SKILL[skillName] || []) {
143
+ assert.ok(fs.existsSync(path.join(installedSkillDir, relativeFile)), `missing ${relativeFile} for ${skillName}`);
144
+ }
145
+ }
146
+
147
+ fs.rmSync(tempRoot, { recursive: true, force: true });
148
+ console.log("skills marketplace smoke OK");
149
+ }
150
+
151
+ try {
152
+ main();
153
+ } catch (error) {
154
+ console.error(error.message);
155
+ process.exit(1);
156
+ }
@@ -340,13 +340,18 @@ async function main() {
340
340
  assert.ok(fs.existsSync(path.join(nonEmptyProject, "app", "package.json")));
341
341
  assert.ok(fs.existsSync(path.join(nonEmptyProject, "ops", "project_control.json")));
342
342
 
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
- 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);
350
355
 
351
356
  assert.ok(fs.existsSync(path.join(splitProject, "ops", "genesis.md")));
352
357
  assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agent", "hub", "agent.md")));
@@ -367,13 +372,15 @@ async function main() {
367
372
  assert.strictEqual(operaControl.meta.opera.installed, true);
368
373
  assert.strictEqual(operaControl.meta.opera.bootstrap.mode, "agent_handoff");
369
374
  assert.strictEqual(operaControl.meta.opera.bootstrap.status, "awaiting_agent");
370
- assert.ok(operaControl.meta.opera.skills.includes("opera-skill"));
371
- assert.ok(operaControl.meta.opera.skills.includes("project-starter-skill"));
372
- assert.ok(operaControl.meta.opera.skills.includes("opera-contract-auditor"));
373
- assert.ok(operaControl.meta.opera.skills.includes("opera-policy-guard"));
374
- assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "opera-skill", "SKILL.md")));
375
- assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "opera-skill", "references", "phase-dod.md")));
376
- 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")));
377
384
  assert.ok(operaControl.meta.environment.requiredKeys.includes("OPENAI_API_KEY"));
378
385
  const envRootText = fs.readFileSync(path.join(splitProject, ".env"), "utf8");
379
386
  assert.match(envRootText, /OPENAI_API_KEY=/);
@@ -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();