gemstack-ai 1.2.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.
Files changed (37) hide show
  1. package/.gemstack/state.json +11 -10
  2. package/CHANGELOG.md +97 -0
  3. package/README.md +89 -9
  4. package/RELEASE_NOTES.md +77 -0
  5. package/{gemstack-ai-1.2.0.tgz → gemstack-ai-1.4.0.tgz} +0 -0
  6. package/handoff.md +14 -12
  7. package/package.json +2 -2
  8. package/specs/008-cost-provider-safety-gates/.gemstack.json +5 -0
  9. package/specs/008-cost-provider-safety-gates/closure.json +59 -0
  10. package/specs/008-cost-provider-safety-gates/plan.md +456 -0
  11. package/specs/008-cost-provider-safety-gates/spec.md +633 -0
  12. package/specs/008-cost-provider-safety-gates/tasks.md +635 -0
  13. package/specs/009-context-capsule/closure.json +59 -0
  14. package/specs/009-context-capsule/context-capsule.json +428 -0
  15. package/specs/009-context-capsule/plan.md +663 -0
  16. package/specs/009-context-capsule/spec.md +913 -0
  17. package/specs/009-context-capsule/tasks.md +720 -0
  18. package/specs/010-agent-swarm-visual-qa/.gemstack.json +5 -0
  19. package/specs/010-agent-swarm-visual-qa/closure.json +59 -0
  20. package/specs/010-agent-swarm-visual-qa/plan.md +759 -0
  21. package/specs/010-agent-swarm-visual-qa/spec.md +842 -0
  22. package/specs/010-agent-swarm-visual-qa/swarm.json +49 -0
  23. package/specs/010-agent-swarm-visual-qa/tasks.md +873 -0
  24. package/specs/010-agent-swarm-visual-qa/visual-qa.json +41 -0
  25. package/src/cli.js +10 -0
  26. package/src/commands/context.js +95 -0
  27. package/src/commands/swarm.js +111 -0
  28. package/src/commands/verify.js +92 -0
  29. package/src/commands/visual.js +82 -0
  30. package/src/lib/closure-context.js +18 -1
  31. package/src/lib/context-capsule.js +594 -0
  32. package/src/lib/cost-ledger.js +355 -0
  33. package/src/lib/provider-boundary.js +186 -0
  34. package/src/lib/provider-registry.js +265 -0
  35. package/src/lib/safety-gates.js +277 -0
  36. package/src/lib/swarm.js +639 -0
  37. 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
@@ -11,6 +11,9 @@ const installCommand = require('./commands/install');
11
11
  const verifyCommand = require('./commands/verify');
12
12
  const collectCommand = require('./commands/collect');
13
13
  const shipCommand = require('./commands/ship');
14
+ const contextCommand = require('./commands/context');
15
+ const swarmCommand = require('./commands/swarm');
16
+ const visualCommand = require('./commands/visual');
14
17
 
15
18
  async function main() {
16
19
  const { command, args, flags } = parser.parse(process.argv);
@@ -24,6 +27,9 @@ Commands:
24
27
  verify Run complete integrity, state, memory and security audit (alias: audit)
25
28
  collect Collect mechanical test matrix and closure evidence
26
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
27
33
  list List available skills
28
34
  show Show content of a skill
29
35
  handoff Show content of handoff.md
@@ -48,6 +54,10 @@ Options:
48
54
  case 'audit': await verifyCommand(flags); break;
49
55
  case 'collect': await collectCommand(flags); break;
50
56
  case 'ship': await shipCommand(flags); break;
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;
51
61
  case 'list': await listCommand(flags); break;
52
62
  case 'show': await showCommand(args[0], flags); break;
53
63
  case 'hooks': hooksCommand.installHooks(flags.target); break;
@@ -0,0 +1,95 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const logger = require('../lib/logger');
4
+ const fssafe = require('../lib/filesystem-safe');
5
+ const {
6
+ generateContextCapsule,
7
+ validateContextCapsule
8
+ } = require('../lib/context-capsule');
9
+
10
+ async function contextCommand(args = [], flags = {}) {
11
+ const subcommand = args[0] || 'show';
12
+ const targetDir = flags.target ? path.resolve(flags.target) : process.cwd();
13
+
14
+ let activeSpec = (args[1] && !args[1].startsWith('-') ? args[1] : null) || flags.feature;
15
+ if (!activeSpec) {
16
+ const stateFile = path.join(targetDir, '.gemstack/state.json');
17
+ if (fs.existsSync(stateFile)) {
18
+ try {
19
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
20
+ activeSpec = state.active_spec;
21
+ } catch (_) {}
22
+ }
23
+ }
24
+
25
+ if (!activeSpec) {
26
+ // Check if specs/009-context-capsule or specs/current exists
27
+ if (fs.existsSync(path.join(targetDir, 'specs/009-context-capsule'))) {
28
+ activeSpec = 'specs/009-context-capsule';
29
+ } else if (fs.existsSync(path.join(targetDir, 'specs/current'))) {
30
+ activeSpec = 'specs/current';
31
+ } else {
32
+ logger.error('No active feature found or specified via --feature.');
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ switch (subcommand) {
38
+ case 'generate': {
39
+ logger.info(`Generating context capsule for "${activeSpec}" in: ${targetDir}`);
40
+ try {
41
+ const res = generateContextCapsule(targetDir, activeSpec);
42
+ logger.ok(`Context capsule generated successfully: ${res.path} (${res.byteLength} bytes, ${res.invariantsCount} invariants).`);
43
+ } catch (err) {
44
+ logger.error(`Generation failed: ${err.message}`);
45
+ process.exit(1);
46
+ }
47
+ break;
48
+ }
49
+
50
+ case 'show': {
51
+ const capsuleFile = path.join(targetDir, activeSpec, 'context-capsule.json');
52
+ if (!fs.existsSync(capsuleFile)) {
53
+ logger.error(`Context capsule not found at: ${capsuleFile}`);
54
+ process.exit(1);
55
+ }
56
+ const raw = fs.readFileSync(capsuleFile, 'utf8');
57
+ if (flags.json) {
58
+ console.log(raw);
59
+ } else {
60
+ const parsed = JSON.parse(raw);
61
+ console.log('=== Gemstack Context Capsule ===');
62
+ console.log(`Schema Version: ${parsed.schema_version}`);
63
+ console.log(`Project: ${parsed.project ? parsed.project.name : 'unknown'}`);
64
+ console.log(`Feature: ${parsed.project ? parsed.project.active_feature : 'unknown'}`);
65
+ console.log(`Lifecycle: ${parsed.project ? parsed.project.lifecycle_status : 'unknown'}`);
66
+ console.log(`Sources: ${parsed.provenance ? parsed.provenance.sources.length : 0} files`);
67
+ console.log(`Invariants: ${parsed.canonical_invariants ? parsed.canonical_invariants.length : 0} rules`);
68
+ console.log(`Contracts: ${parsed.frozen_contracts ? parsed.frozen_contracts.length : 0} contracts`);
69
+ console.log(`Acceptance: ${parsed.acceptance_matrix ? parsed.acceptance_matrix.total_required : 0} tests`);
70
+ }
71
+ break;
72
+ }
73
+
74
+ case 'verify': {
75
+ logger.info(`Auditing context capsule for "${activeSpec}" in: ${targetDir}`);
76
+ const res = validateContextCapsule(targetDir, activeSpec);
77
+ if (res.valid) {
78
+ logger.ok(`[OK] Context capsule is VALID and FRESH for ${activeSpec}.`);
79
+ } else {
80
+ logger.error(`[${res.state}] Context capsule validation failed.`);
81
+ for (const f of res.findings) {
82
+ logger.error(` - [${f.code}] ${f.message}`);
83
+ }
84
+ process.exit(1);
85
+ }
86
+ break;
87
+ }
88
+
89
+ default:
90
+ logger.error(`Unknown context subcommand: ${subcommand}. Valid subcommands: generate, show, verify.`);
91
+ process.exit(1);
92
+ }
93
+ }
94
+
95
+ module.exports = contextCommand;
@@ -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;
@@ -376,6 +376,98 @@ module.exports = async (flags) => {
376
376
  logger.ok('Sin spec activa configurada para validación de evidencia de cierre.');
377
377
  }
378
378
 
379
+ // 5.1 Verificación de Políticas de Costos y Proveedores (Upgrade C - Read-Only)
380
+ const { loadCostLedger } = require('../lib/cost-ledger');
381
+ let costLedgerFound = false;
382
+ const ledgerCandidates = [];
383
+ if (loadedState && loadedState.active_spec) {
384
+ ledgerCandidates.push(fssafe.resolveSafe(targetDir, path.join(loadedState.active_spec, 'cost-ledger.json')));
385
+ }
386
+ ledgerCandidates.push(fssafe.resolveSafe(targetDir, 'cost-ledger.json'));
387
+ ledgerCandidates.push(fssafe.resolveSafe(targetDir, '.gemstack/cost-ledger.json'));
388
+
389
+ for (const lPath of ledgerCandidates) {
390
+ if (fs.existsSync(lPath)) {
391
+ costLedgerFound = true;
392
+ const res = loadCostLedger(lPath);
393
+ if (res.findings.length > 0) {
394
+ for (const f of res.findings) {
395
+ if (f.is_blocking) {
396
+ logger.error(`[COST_SAFETY_BLOCKER] ${f.code}: ${f.details}`);
397
+ totalErrors++;
398
+ } else {
399
+ logger.warn(`[COST_SAFETY_WARNING] ${f.code}: ${f.details}`);
400
+ totalWarnings++;
401
+ }
402
+ }
403
+ } else {
404
+ logger.ok(`Cost ledger verificado (${path.basename(lPath)}): íntegro y sin secretos.`);
405
+ }
406
+ break;
407
+ }
408
+ }
409
+
410
+ if (!costLedgerFound) {
411
+ logger.info('[LEGACY] [LEGACY_NO_PROVIDERS_DECLARED] No se detectaron declaraciones de costos o proveedores (Modo Legacy Provider-Free).');
412
+ }
413
+
414
+ // 5.2 Verificación de Context Capsule (Upgrade D - Read-Only)
415
+ logger.info('--- 5.2 Verificación de Context Capsule (Read-Only) ---');
416
+ if (loadedState && loadedState.active_spec) {
417
+ const { validateContextCapsule } = require('../lib/context-capsule');
418
+ const capResult = validateContextCapsule(targetDir, loadedState.active_spec);
419
+ if (capResult.valid) {
420
+ logger.ok(`Context capsule verificado y fresco (${loadedState.active_spec}/context-capsule.json).`);
421
+ } else if (capResult.state === 'MISSING') {
422
+ logger.info(`[LEGACY] No se detectó context-capsule.json en "${loadedState.active_spec}" (Modo Legacy Context-Free).`);
423
+ } else {
424
+ for (const f of capResult.findings) {
425
+ logger.error(`[${f.code}] ${f.message}`);
426
+ totalErrors++;
427
+ }
428
+ }
429
+ } else {
430
+ logger.ok('Sin spec activa configurada para verificación de context capsule.');
431
+ }
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
+
379
471
  // 6. Seguridad Local y Anti-Silent Failures en Tests
380
472
  logger.info('--- 6/6 Verificación de Seguridad y Test Runners ---');
381
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) {
@@ -334,6 +334,23 @@ function resolveRelevantFiles(rootPath, featureDir, planBindings, taskList, plan
334
334
  if (fs.existsSync(path.join(rootPath, planPath))) filesSet.add(planPath);
335
335
  if (fs.existsSync(path.join(rootPath, tasksPath))) filesSet.add(tasksPath);
336
336
 
337
+ const featureLedger = (relFeature + '/cost-ledger.json').replace(/^\.\//, '');
338
+ if (fs.existsSync(path.join(rootPath, featureLedger))) filesSet.add(featureLedger);
339
+ if (fs.existsSync(path.join(rootPath, 'cost-ledger.json'))) filesSet.add('cost-ledger.json');
340
+ if (fs.existsSync(path.join(rootPath, '.gemstack/cost-ledger.json'))) filesSet.add('.gemstack/cost-ledger.json');
341
+
342
+ const featureCapsule = (relFeature + '/context-capsule.json').replace(/^\.\//, '');
343
+ if (fs.existsSync(path.join(rootPath, featureCapsule))) filesSet.add(featureCapsule);
344
+ if (fs.existsSync(path.join(rootPath, '.gemstack/context-capsule.json'))) filesSet.add('.gemstack/context-capsule.json');
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
+
337
354
  for (const b of (planBindings || [])) {
338
355
  if (b.file && fs.existsSync(path.join(rootPath, b.file))) {
339
356
  filesSet.add(b.file);