gemstack-ai 1.3.0 → 1.4.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.
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "https://gemstack.dev/schemas/v1/visual-qa.json",
3
+ "version": "1.0.0",
4
+ "feature_id": "010-agent-swarm-visual-qa",
5
+ "target_base_url": "http://localhost:3000",
6
+ "scenarios": [
7
+ {
8
+ "scenario_id": "VQA-LOGIN-001",
9
+ "description": "Login screen mobile portrait conformance",
10
+ "route": "/login",
11
+ "acceptance_ids": [
12
+ "TEST-VISUAL-A01"
13
+ ],
14
+ "viewport": {
15
+ "name": "mobile-portrait",
16
+ "width": 375,
17
+ "height": 667,
18
+ "device_scale_factor": 2,
19
+ "color_scheme": "light"
20
+ },
21
+ "selectors": {
22
+ "root": "#login-container",
23
+ "mask": [
24
+ ".dynamic-timestamp",
25
+ ".live-avatar"
26
+ ]
27
+ },
28
+ "tolerances": {
29
+ "max_diff_percentage": 0,
30
+ "anti_aliasing_threshold": 0.1
31
+ },
32
+ "baseline": {
33
+ "image_path": "specs/010-agent-swarm-visual-qa/baselines/vqa-login-001.png",
34
+ "image_sha256": "c35ba1ccd49a5e52bdc206d7e8723bc31d288d4f38b6c9045f4f78ab79e6018f",
35
+ "dom_hash": "c5d6e7f80123456789abcdef0123456789abcdef0123456789abcdef01234567",
36
+ "approved_by": "human-lead",
37
+ "approved_at": "2026-09-11T19:10:00Z"
38
+ }
39
+ }
40
+ ]
41
+ }
package/src/cli.js CHANGED
@@ -12,6 +12,8 @@ const verifyCommand = require('./commands/verify');
12
12
  const collectCommand = require('./commands/collect');
13
13
  const shipCommand = require('./commands/ship');
14
14
  const contextCommand = require('./commands/context');
15
+ const swarmCommand = require('./commands/swarm');
16
+ const visualCommand = require('./commands/visual');
15
17
 
16
18
  async function main() {
17
19
  const { command, args, flags } = parser.parse(process.argv);
@@ -25,6 +27,9 @@ Commands:
25
27
  verify Run complete integrity, state, memory and security audit (alias: audit)
26
28
  collect Collect mechanical test matrix and closure evidence
27
29
  ship Verify closure gates and transition feature to shipped
30
+ context Generate, show, or verify context capsule
31
+ swarm Plan or validate agent swarm work and write partitions
32
+ vqa Validate or promote visual QA manifests and evidence
28
33
  list List available skills
29
34
  show Show content of a skill
30
35
  handoff Show content of handoff.md
@@ -50,6 +55,9 @@ Options:
50
55
  case 'collect': await collectCommand(flags); break;
51
56
  case 'ship': await shipCommand(flags); break;
52
57
  case 'context': await contextCommand(args, flags); break;
58
+ case 'swarm': await swarmCommand(args, flags); break;
59
+ case 'vqa':
60
+ case 'visual': await visualCommand(args, flags); break;
53
61
  case 'list': await listCommand(flags); break;
54
62
  case 'show': await showCommand(args[0], flags); break;
55
63
  case 'hooks': hooksCommand.installHooks(flags.target); break;
@@ -11,7 +11,7 @@ async function contextCommand(args = [], flags = {}) {
11
11
  const subcommand = args[0] || 'show';
12
12
  const targetDir = flags.target ? path.resolve(flags.target) : process.cwd();
13
13
 
14
- let activeSpec = flags.feature;
14
+ let activeSpec = (args[1] && !args[1].startsWith('-') ? args[1] : null) || flags.feature;
15
15
  if (!activeSpec) {
16
16
  const stateFile = path.join(targetDir, '.gemstack/state.json');
17
17
  if (fs.existsSync(stateFile)) {
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Gemstack Swarm Command Handler (Upgrade E)
3
+ *
4
+ * Implements "gemstack swarm plan" and "gemstack swarm validate" subcommands.
5
+ * Read-only planning and manifest auditing.
6
+ */
7
+
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+ const logger = require('../lib/logger');
11
+ const fssafe = require('../lib/filesystem-safe');
12
+ const { readState } = require('../lib/state');
13
+ const { planSwarmWaves, validateSwarmManifest, canonicalSerialize } = require('../lib/swarm');
14
+
15
+ async function swarmCommand(args = [], flags = {}) {
16
+ const subcommand = args[0] || 'validate';
17
+ const targetDir = flags.target ? path.resolve(flags.target) : process.cwd();
18
+
19
+ let state;
20
+ try {
21
+ state = readState(targetDir);
22
+ } catch (e) {
23
+ logger.error(`Error loading .gemstack/state.json: ${e.message}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const activeSpec = state.active_spec;
28
+ if (!activeSpec) {
29
+ if (flags.json) {
30
+ console.log(JSON.stringify({ status: 'NO_ACTIVE_SPEC', message: 'No active feature spec set.' }));
31
+ } else {
32
+ logger.info('Sin spec activa pendiente. Opera en modo legacy.');
33
+ }
34
+ return;
35
+ }
36
+
37
+ const featureDir = fssafe.resolveSafe(targetDir, activeSpec);
38
+
39
+ if (subcommand === 'plan') {
40
+ const tasksPath = path.join(featureDir, 'tasks.md');
41
+ if (!fs.existsSync(tasksPath)) {
42
+ logger.error(`tasks.md not found in "${activeSpec}".`);
43
+ process.exit(1);
44
+ }
45
+
46
+ const tasksRaw = fs.readFileSync(tasksPath, 'utf8');
47
+ // Extract tasks with parallel tag or lines
48
+ const parsedTasks = [];
49
+ const lines = tasksRaw.split('\n');
50
+ for (const line of lines) {
51
+ const m = line.match(/###\s+(?:Task\s+)?([A-Za-z0-9_-]+)/i);
52
+ if (m) {
53
+ parsedTasks.push({
54
+ task_id: m[1],
55
+ description: line.replace(/^#+\s*/, '').trim(),
56
+ assigned_role: 'implementer',
57
+ worker_id: `worker-impl-${parsedTasks.length + 1}`,
58
+ write_set: [`src/${m[1].toLowerCase()}.js`],
59
+ status: 'PLANNED'
60
+ });
61
+ }
62
+ }
63
+
64
+ const waves = planSwarmWaves(parsedTasks);
65
+ const manifest = {
66
+ $schema: 'https://gemstack.dev/schemas/v1/swarm.json',
67
+ version: '1.0.0',
68
+ feature_id: activeSpec,
69
+ source_capsule_hash: 'd0mmy_c4psul3_h4sh',
70
+ max_workers: 4,
71
+ waves
72
+ };
73
+
74
+ const manifestPath = path.join(featureDir, 'swarm.json');
75
+ if (!flags['dry-run']) {
76
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
77
+ logger.ok(`Swarm plan generated: ${manifestPath} (${waves.length} wave(s)).`);
78
+ } else {
79
+ logger.info(`[DRY-RUN] Swarm plan computed: ${waves.length} wave(s).`);
80
+ }
81
+
82
+ if (flags.json) {
83
+ console.log(JSON.stringify(manifest, null, 2));
84
+ }
85
+ } else if (subcommand === 'validate') {
86
+ const res = validateSwarmManifest(targetDir, activeSpec);
87
+ if (flags.json) {
88
+ console.log(JSON.stringify(res, null, 2));
89
+ return;
90
+ }
91
+
92
+ if (res.state === 'MISSING') {
93
+ logger.info(`[LEGACY] No se detectó swarm.json en "${activeSpec}".`);
94
+ return;
95
+ }
96
+
97
+ if (res.valid) {
98
+ logger.ok(`Swarm manifest validado con éxito: ${activeSpec}/swarm.json.`);
99
+ } else {
100
+ for (const f of res.findings) {
101
+ logger.error(`[${f.code}] ${f.details}`);
102
+ }
103
+ process.exit(1);
104
+ }
105
+ } else {
106
+ logger.error(`Unknown swarm subcommand: "${subcommand}". Use "plan" or "validate".`);
107
+ process.exit(1);
108
+ }
109
+ }
110
+
111
+ module.exports = swarmCommand;
@@ -430,6 +430,44 @@ module.exports = async (flags) => {
430
430
  logger.ok('Sin spec activa configurada para verificación de context capsule.');
431
431
  }
432
432
 
433
+ // 5.3 Verificación de Swarm Manifest (Upgrade E - Read-Only)
434
+ logger.info('--- 5.3 Verificación de Swarm Manifest & Partition Safety (Read-Only) ---');
435
+ if (loadedState && loadedState.active_spec) {
436
+ const { validateSwarmManifest } = require('../lib/swarm');
437
+ const swarmResult = validateSwarmManifest(targetDir, loadedState.active_spec);
438
+ if (swarmResult.valid) {
439
+ logger.ok(`Swarm manifest verificado y sin colisiones (${loadedState.active_spec}/swarm.json).`);
440
+ } else if (swarmResult.state === 'MISSING') {
441
+ logger.info(`[LEGACY] No se detectó swarm.json en "${loadedState.active_spec}" (Modo Legacy Swarm-Free).`);
442
+ } else {
443
+ for (const f of swarmResult.findings) {
444
+ logger.error(`[${f.code}] ${f.details || f.message}`);
445
+ totalErrors++;
446
+ }
447
+ }
448
+ } else {
449
+ logger.ok('Sin spec activa configurada para verificación de swarm manifest.');
450
+ }
451
+
452
+ // 5.4 Verificación de Visual QA Manifest & Baselines (Upgrade E - Read-Only)
453
+ logger.info('--- 5.4 Verificación de Visual QA Manifest & Baselines (Read-Only) ---');
454
+ if (loadedState && loadedState.active_spec) {
455
+ const { validateVisualManifest } = require('../lib/visual-qa');
456
+ const vqaResult = validateVisualManifest(targetDir, loadedState.active_spec);
457
+ if (vqaResult.valid) {
458
+ logger.ok(`Visual QA manifest y baselines íntegros (${loadedState.active_spec}/visual-qa.json).`);
459
+ } else if (vqaResult.state === 'MISSING') {
460
+ logger.info(`[LEGACY] No se detectó visual-qa.json en "${loadedState.active_spec}" (Modo Legacy Visual-Free).`);
461
+ } else {
462
+ for (const f of vqaResult.findings) {
463
+ logger.error(`[${f.code}] ${f.details || f.message}`);
464
+ totalErrors++;
465
+ }
466
+ }
467
+ } else {
468
+ logger.ok('Sin spec activa configurada para verificación de visual QA.');
469
+ }
470
+
433
471
  // 6. Seguridad Local y Anti-Silent Failures en Tests
434
472
  logger.info('--- 6/6 Verificación de Seguridad y Test Runners ---');
435
473
  const envPath = fssafe.resolveSafe(targetDir, '.env');
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Gemstack Visual QA Command Handler (Upgrade E)
3
+ *
4
+ * Implements "gemstack vqa validate" and "gemstack vqa promote" subcommands.
5
+ * Read-only validation and explicit baseline promotion.
6
+ */
7
+
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+ const logger = require('../lib/logger');
11
+ const fssafe = require('../lib/filesystem-safe');
12
+ const { readState } = require('../lib/state');
13
+ const { validateVisualManifest, promoteVisualBaseline } = require('../lib/visual-qa');
14
+
15
+ async function visualCommand(args = [], flags = {}) {
16
+ const subcommand = args[0] || 'validate';
17
+ const targetDir = flags.target ? path.resolve(flags.target) : process.cwd();
18
+
19
+ let state;
20
+ try {
21
+ state = readState(targetDir);
22
+ } catch (e) {
23
+ logger.error(`Error loading .gemstack/state.json: ${e.message}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const activeSpec = state.active_spec;
28
+ if (!activeSpec) {
29
+ if (flags.json) {
30
+ console.log(JSON.stringify({ status: 'NO_ACTIVE_SPEC', message: 'No active feature spec set.' }));
31
+ } else {
32
+ logger.info('Sin spec activa pendiente. Opera en modo legacy.');
33
+ }
34
+ return;
35
+ }
36
+
37
+ if (subcommand === 'validate') {
38
+ const res = validateVisualManifest(targetDir, activeSpec);
39
+ if (flags.json) {
40
+ console.log(JSON.stringify(res, null, 2));
41
+ return;
42
+ }
43
+
44
+ if (res.state === 'MISSING') {
45
+ logger.info(`[LEGACY] No se detectó visual-qa.json en "${activeSpec}".`);
46
+ return;
47
+ }
48
+
49
+ if (res.valid) {
50
+ logger.ok(`Visual QA manifest validado con éxito: ${activeSpec}/visual-qa.json.`);
51
+ } else {
52
+ for (const f of res.findings) {
53
+ logger.error(`[${f.code}] ${f.details}`);
54
+ }
55
+ process.exit(1);
56
+ }
57
+ } else if (subcommand === 'promote') {
58
+ const scenarioId = args[1];
59
+ const liveImagePath = args[2];
60
+
61
+ if (!scenarioId || !liveImagePath) {
62
+ logger.error('Usage: gemstack vqa promote <scenario-id> <live-image-path>');
63
+ process.exit(1);
64
+ }
65
+
66
+ try {
67
+ const res = promoteVisualBaseline(targetDir, activeSpec, scenarioId, liveImagePath);
68
+ logger.ok(`Baseline promoted for scenario "${scenarioId}". Updated ${activeSpec}/visual-qa.json.`);
69
+ if (flags.json) {
70
+ console.log(JSON.stringify(res, null, 2));
71
+ }
72
+ } catch (err) {
73
+ logger.error(`Failed to promote baseline: ${err.message}`);
74
+ process.exit(1);
75
+ }
76
+ } else {
77
+ logger.error(`Unknown visual subcommand: "${subcommand}". Use "validate" or "promote".`);
78
+ process.exit(1);
79
+ }
80
+ }
81
+
82
+ module.exports = visualCommand;
@@ -220,7 +220,7 @@ function parseTaskMetadata(tasksContent) {
220
220
  const lines = normalized.split('\n');
221
221
  const tasks = [];
222
222
 
223
- const taskHeaderRegex = /^[*-]\s+\[[ xX]\]\s+\*\*(T\d+):\s*(.*?)\*\*/;
223
+ const taskHeaderRegex = /^[*-]\s+\[[ xX]\]\s+\*\*([A-Za-z0-9_-]+):\s*(.*?)\*\*/;
224
224
  let currentTask = null;
225
225
 
226
226
  for (const line of lines) {
@@ -343,6 +343,14 @@ function resolveRelevantFiles(rootPath, featureDir, planBindings, taskList, plan
343
343
  if (fs.existsSync(path.join(rootPath, featureCapsule))) filesSet.add(featureCapsule);
344
344
  if (fs.existsSync(path.join(rootPath, '.gemstack/context-capsule.json'))) filesSet.add('.gemstack/context-capsule.json');
345
345
 
346
+ const featureSwarm = (relFeature + '/swarm.json').replace(/^\.\//, '');
347
+ if (fs.existsSync(path.join(rootPath, featureSwarm))) filesSet.add(featureSwarm);
348
+ if (fs.existsSync(path.join(rootPath, '.gemstack/swarm.json'))) filesSet.add('.gemstack/swarm.json');
349
+
350
+ const featureVqa = (relFeature + '/visual-qa.json').replace(/^\.\//, '');
351
+ if (fs.existsSync(path.join(rootPath, featureVqa))) filesSet.add(featureVqa);
352
+ if (fs.existsSync(path.join(rootPath, '.gemstack/visual-qa.json'))) filesSet.add('.gemstack/visual-qa.json');
353
+
346
354
  for (const b of (planBindings || [])) {
347
355
  if (b.file && fs.existsSync(path.join(rootPath, b.file))) {
348
356
  filesSet.add(b.file);