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.
- package/.gemstack/state.json +6 -6
- package/CHANGELOG.md +54 -0
- package/README.md +66 -9
- package/RELEASE_NOTES.md +40 -0
- package/{gemstack-ai-1.3.0.tgz → gemstack-ai-1.4.0.tgz} +0 -0
- package/package.json +2 -2
- package/specs/009-context-capsule/context-capsule.json +4 -4
- package/specs/010-agent-swarm-visual-qa/.gemstack.json +5 -0
- package/specs/010-agent-swarm-visual-qa/closure.json +59 -0
- package/specs/010-agent-swarm-visual-qa/plan.md +759 -0
- package/specs/010-agent-swarm-visual-qa/spec.md +842 -0
- package/specs/010-agent-swarm-visual-qa/swarm.json +49 -0
- package/specs/010-agent-swarm-visual-qa/tasks.md +873 -0
- package/specs/010-agent-swarm-visual-qa/visual-qa.json +41 -0
- package/src/cli.js +8 -0
- package/src/commands/context.js +1 -1
- package/src/commands/swarm.js +111 -0
- package/src/commands/verify.js +38 -0
- package/src/commands/visual.js +82 -0
- package/src/lib/closure-context.js +9 -1
- package/src/lib/swarm.js +639 -0
- package/src/lib/visual-qa.js +499 -0
|
@@ -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;
|
package/src/commands/context.js
CHANGED
|
@@ -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;
|
package/src/commands/verify.js
CHANGED
|
@@ -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+\*\*(
|
|
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);
|