multimodel-dev-os 3.1.0 → 3.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 (53) hide show
  1. package/README.md +1 -0
  2. package/docs/.vitepress/config.js +1 -1
  3. package/docs/index.md +5 -5
  4. package/docs/npm-publishing.md +5 -5
  5. package/docs/package-safety.md +16 -0
  6. package/docs/public/llms-full.txt +1 -1
  7. package/docs/public/llms.txt +1 -1
  8. package/docs/public/sitemap.xml +5 -0
  9. package/docs/release-policy.md +6 -5
  10. package/docs/testing.md +12 -2
  11. package/docs/v3-roadmap.md +8 -2
  12. package/package.json +5 -2
  13. package/scripts/build-cli.js +45 -3
  14. package/scripts/check-build-fresh.js +52 -0
  15. package/scripts/install.ps1 +1 -1
  16. package/scripts/install.sh +1 -1
  17. package/scripts/verify.js +128 -12
  18. package/scripts/verify.sh +10 -0
  19. package/src/catalog/loader.js +117 -0
  20. package/src/cli/args.js +118 -0
  21. package/src/cli/help.js +60 -0
  22. package/src/cli/main.js +5718 -0
  23. package/src/core/globals.js +52 -0
  24. package/src/core/hashes.js +15 -0
  25. package/src/core/policy.js +36 -0
  26. package/src/core/security.js +61 -0
  27. package/src/core/yaml.js +136 -0
  28. package/src/plugin/manifest.js +95 -0
  29. package/src/registry/sources.js +40 -0
  30. package/src/registry/validation.js +45 -0
  31. package/tests/README.md +37 -0
  32. package/tests/fixtures/README.md +22 -0
  33. package/tests/fixtures/custom-template-example/README.md +10 -0
  34. package/tests/fixtures/proposals/approved-append-line.md +28 -0
  35. package/tests/fixtures/proposals/approved-create-file.md +29 -0
  36. package/tests/fixtures/proposals/approved-replace-text.md +30 -0
  37. package/tests/fixtures/proposals/existing-create-file-no-overwrite.md +29 -0
  38. package/tests/fixtures/proposals/no-operations.md +18 -0
  39. package/tests/fixtures/proposals/path-traversal.md +29 -0
  40. package/tests/fixtures/proposals/pending-proposal.md +29 -0
  41. package/tests/fixtures/proposals/protected-path.md +29 -0
  42. package/tests/fixtures/proposals/replace-multiple-without-allow.md +30 -0
  43. package/tests/fixtures/registry-overrides/README.md +20 -0
  44. package/tests/smoke/README.md +37 -0
  45. package/tests/smoke/cli-smoke.md +49 -0
  46. package/tests/unit/build-output.test.js +40 -0
  47. package/tests/unit/catalog-loader.test.js +44 -0
  48. package/tests/unit/path-safety.test.js +62 -0
  49. package/tests/unit/plugin-manifest.test.js +94 -0
  50. package/tests/unit/prepublish-guard.test.js +35 -0
  51. package/tests/unit/registry-policy.test.js +46 -0
  52. package/tests/unit/registry-url-validation.test.js +64 -0
  53. package/tests/unit/yaml.test.js +92 -0
@@ -0,0 +1,52 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join, resolve, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { parseYaml } from './yaml.js';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ // When bundled, __dirname will resolve to F:\multimodel-dev-os\bin or equivalent
10
+ // So resolving '..' will give the project root directory
11
+ export const sourceRoot = resolve(__dirname, '..');
12
+
13
+ let pkgVersion = '3.0.2';
14
+ try {
15
+ const pkgData = JSON.parse(readFileSync(resolve(sourceRoot, 'package.json'), 'utf8'));
16
+ pkgVersion = pkgData.version;
17
+ } catch (e) {}
18
+
19
+ export const version = pkgVersion;
20
+
21
+ export function loadTemplates(customPath) {
22
+ let path = customPath || join(sourceRoot, '.ai', 'templates', 'registry.yaml');
23
+ try {
24
+ if (existsSync(path)) {
25
+ const templatesRegistry = parseYaml(readFileSync(path, 'utf8'));
26
+ return templatesRegistry.templates || {};
27
+ }
28
+ } catch (e) {}
29
+ return {
30
+ 'general-app': {
31
+ name: 'general-app',
32
+ description: 'Baseline generic fallback profile for standard backend systems.',
33
+ stack: 'Universal backends baseline structure',
34
+ skill: 'example-skill.md',
35
+ skillDesc: 'Generic baseline instructions and coding standards.',
36
+ status: 'stable',
37
+ maturity: 'production-ready',
38
+ required_files: ['AGENTS.md', 'MEMORY.md', 'TASKS.md', 'RUNBOOK.md', '.ai/config.yaml']
39
+ }
40
+ };
41
+ }
42
+
43
+ export function loadAdapters(customPath) {
44
+ let path = customPath || join(sourceRoot, '.ai', 'adapters', 'registry.yaml');
45
+ try {
46
+ if (existsSync(path)) {
47
+ const adaptersRegistry = parseYaml(readFileSync(path, 'utf8'));
48
+ return adaptersRegistry.adapters || {};
49
+ }
50
+ } catch (e) {}
51
+ return {};
52
+ }
@@ -0,0 +1,15 @@
1
+ import { createHash } from 'crypto';
2
+ import { readFileSync } from 'fs';
3
+
4
+ export function computeSHA256(content) {
5
+ return createHash('sha256').update(content, 'utf8').digest('hex');
6
+ }
7
+
8
+ export function hashFile(filePath) {
9
+ try {
10
+ const data = readFileSync(filePath);
11
+ return createHash('sha256').update(data).digest('hex');
12
+ } catch (e) {
13
+ return '';
14
+ }
15
+ }
@@ -0,0 +1,36 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { sourceRoot } from './globals.js';
4
+ import { parseYaml } from './yaml.js';
5
+
6
+ export function loadRegistryPolicy(targetDir) {
7
+ const defaults = {
8
+ allow_remote_registries: false,
9
+ allow_http_localhost: false,
10
+ require_approval_for_remote_sync: true,
11
+ require_checksum: true,
12
+ require_signature: false,
13
+ allow_untrusted_install: false,
14
+ allowed_write_roots: ['.ai/', 'adapters/'],
15
+ blocked_paths: ['.env', '.npmrc', '.git/', 'node_modules/', 'package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'],
16
+ max_plugin_files: 20,
17
+ max_plugin_size_kb: 100,
18
+ max_registry_cache_size_kb: 512,
19
+ allowed_file_extensions: ['.md', '.yaml', '.yml', '.json']
20
+ };
21
+ const paths = [];
22
+ if (targetDir) {
23
+ paths.push(join(targetDir, '.ai', 'policies', 'registry-policy.yaml'));
24
+ }
25
+ paths.push(join(sourceRoot, '.ai', 'policies', 'registry-policy.yaml'));
26
+
27
+ for (const p of paths) {
28
+ if (existsSync(p)) {
29
+ try {
30
+ const parsed = parseYaml(readFileSync(p, 'utf8'));
31
+ return { ...defaults, ...parsed };
32
+ } catch (e) {}
33
+ }
34
+ }
35
+ return defaults;
36
+ }
@@ -0,0 +1,61 @@
1
+ export function shouldIgnorePath(relPath) {
2
+ const normalized = relPath.replace(/\\/g, '/');
3
+ const segments = normalized.split('/');
4
+
5
+ // Ignored folders
6
+ const ignoredFolders = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];
7
+ for (const seg of segments) {
8
+ if (ignoredFolders.includes(seg)) return true;
9
+ }
10
+
11
+ // Special check for docs/.vitepress/dist and docs/.vitepress/cache
12
+ if (normalized.includes('docs/.vitepress/dist') || normalized.includes('docs/.vitepress/cache')) {
13
+ return true;
14
+ }
15
+
16
+ // Ignore generated memory and intelligence runtime files
17
+ if (
18
+ normalized.endsWith('memory.hash.json') ||
19
+ normalized.endsWith('memory.summary.md') ||
20
+ normalized.endsWith('feedback-log.jsonl') ||
21
+ normalized.endsWith('learning-rules.md') ||
22
+ normalized.endsWith('apply-log.jsonl') ||
23
+ normalized.includes('.ai/proposals/')
24
+ ) {
25
+ return true;
26
+ }
27
+
28
+ // Skip secret-like files/patterns
29
+ const lower = normalized.toLowerCase();
30
+ const filePart = segments[segments.length - 1];
31
+ if (
32
+ lower.endsWith('.env') ||
33
+ lower.includes('.env.') ||
34
+ lower.endsWith('.npmrc') ||
35
+ lower.endsWith('.keystore') ||
36
+ lower.endsWith('.jks') ||
37
+ lower.endsWith('.key') ||
38
+ lower.endsWith('.pem') ||
39
+ lower.endsWith('credentials.json') ||
40
+ filePart === 'id_rsa' ||
41
+ filePart === 'id_dsa' ||
42
+ filePart === 'id_ecdsa' ||
43
+ filePart === 'id_ed25519'
44
+ ) {
45
+ return true;
46
+ }
47
+
48
+ return false;
49
+ }
50
+
51
+ export function isSafePath(filePath, policy = {}) {
52
+ const normPath = filePath.replace(/\\/g, '/').trim();
53
+ const allowed_write_roots = policy.allowed_write_roots || ['.ai/', 'adapters/'];
54
+ const blocked_paths = policy.blocked_paths || ['.env', '.npmrc', '.git/', 'node_modules/', 'package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'];
55
+
56
+ const isSafeSubdir = allowed_write_roots.some(prefix => normPath.startsWith(prefix));
57
+ const hasTraversal = normPath.includes('..') || normPath.startsWith('/') || /^[a-zA-Z]:/.test(normPath);
58
+ const isBlacklisted = blocked_paths.some(black => normPath.includes(black) || normPath.split('/').includes(black.replace(/\/$/, '')));
59
+
60
+ return isSafeSubdir && !hasTraversal && !isBlacklisted;
61
+ }
@@ -0,0 +1,136 @@
1
+ export function parseFlowArray(str) {
2
+ const contents = str.slice(1, -1).trim();
3
+ if (!contents) return [];
4
+
5
+ const result = [];
6
+ const regex = /"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|([^,\s][^,]*[^,\s]|[^,\s])/g;
7
+ let match;
8
+ while ((match = regex.exec(contents)) !== null) {
9
+ if (match[1] !== undefined) {
10
+ result.push(match[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\'));
11
+ } else if (match[2] !== undefined) {
12
+ result.push(match[2].replace(/\\'/g, "'").replace(/\\\\/g, '\\'));
13
+ } else if (match[3] !== undefined) {
14
+ let val = match[3].trim();
15
+ if (val === 'true') val = true;
16
+ else if (val === 'false') val = false;
17
+ else if (val === 'null') val = null;
18
+ else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
19
+ result.push(val);
20
+ }
21
+ }
22
+ return result;
23
+ }
24
+
25
+ export function parseYaml(content) {
26
+ try {
27
+ const root = {};
28
+ const stack = [{ obj: root, indent: -1, key: null, isArray: false }];
29
+
30
+ const lines = content.split(/\r?\n/);
31
+ for (let line of lines) {
32
+ // Find comment index outside quotes
33
+ let commentIdx = -1;
34
+ let insideDouble = false;
35
+ let insideSingle = false;
36
+ for (let i = 0; i < line.length; i++) {
37
+ const char = line[i];
38
+ if (char === '"' && (i === 0 || line[i-1] !== '\\')) {
39
+ insideDouble = !insideDouble;
40
+ } else if (char === "'" && (i === 0 || line[i-1] !== '\\')) {
41
+ insideSingle = !insideSingle;
42
+ } else if (char === '#' && !insideDouble && !insideSingle) {
43
+ commentIdx = i;
44
+ break;
45
+ }
46
+ }
47
+ if (commentIdx !== -1) {
48
+ line = line.substring(0, commentIdx);
49
+ }
50
+ line = line.trimEnd();
51
+ if (!line.trim()) continue;
52
+
53
+ const indent = line.match(/^ */)[0].length;
54
+ let trimmed = line.trim();
55
+
56
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
57
+ stack.pop();
58
+ }
59
+
60
+ const parent = stack[stack.length - 1];
61
+
62
+ if (trimmed.startsWith('-')) {
63
+ trimmed = trimmed.substring(1).trim();
64
+ if (!Array.isArray(parent.obj)) {
65
+ const grandparent = stack[stack.length - 2];
66
+ if (grandparent) {
67
+ grandparent.obj[parent.key] = [];
68
+ parent.obj = grandparent.obj[parent.key];
69
+ }
70
+ }
71
+
72
+ const colonIdx = trimmed.indexOf(':');
73
+ if (colonIdx === -1) {
74
+ let val = trimmed;
75
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
76
+ val = val.substring(1, val.length - 1);
77
+ }
78
+ if (val.startsWith('[') && val.endsWith(']')) {
79
+ val = parseFlowArray(val);
80
+ }
81
+ parent.obj.push(val);
82
+ } else {
83
+ const key = trimmed.substring(0, colonIdx).trim();
84
+ let val = trimmed.substring(colonIdx + 1).trim();
85
+ let isQuoted = false;
86
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
87
+ val = val.substring(1, val.length - 1);
88
+ isQuoted = true;
89
+ }
90
+ if (val.startsWith('[') && val.endsWith(']')) {
91
+ val = parseFlowArray(val);
92
+ } else if (!isQuoted) {
93
+ if (val === 'true') val = true;
94
+ else if (val === 'false') val = false;
95
+ else if (val === 'null') val = null;
96
+ else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
97
+ }
98
+
99
+ const newObj = { [key]: val };
100
+ parent.obj.push(newObj);
101
+ stack.push({ obj: newObj, indent: indent, key: key, isArray: false });
102
+ }
103
+ } else {
104
+ const colonIdx = trimmed.indexOf(':');
105
+ if (colonIdx === -1) continue;
106
+
107
+ const key = trimmed.substring(0, colonIdx).trim();
108
+ let val = trimmed.substring(colonIdx + 1).trim();
109
+ let isQuoted = false;
110
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
111
+ val = val.substring(1, val.length - 1);
112
+ isQuoted = true;
113
+ }
114
+ if (val.startsWith('[') && val.endsWith(']')) {
115
+ val = parseFlowArray(val);
116
+ } else if (!isQuoted) {
117
+ if (val === 'true') val = true;
118
+ else if (val === 'false') val = false;
119
+ else if (val === 'null') val = null;
120
+ else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
121
+ }
122
+
123
+ if (val === '') {
124
+ parent.obj[key] = {};
125
+ stack.push({ obj: parent.obj[key], indent: indent, key: key, isArray: false });
126
+ } else {
127
+ parent.obj[key] = val;
128
+ }
129
+ }
130
+ }
131
+ return root;
132
+ } catch (e) {
133
+ console.warn(`\x1b[33m[WARNING] Failed to parse YAML: ${e.message}\x1b[0m`);
134
+ return {};
135
+ }
136
+ }
@@ -0,0 +1,95 @@
1
+ export function validatePluginManifest(manifestObj) {
2
+ const errors = [];
3
+
4
+ const reqKeys = ['name', 'slug', 'version', 'description', 'author'];
5
+ reqKeys.forEach(k => {
6
+ if (manifestObj[k] === undefined || manifestObj[k] === null) {
7
+ errors.push(`Missing required key: ${k}`);
8
+ } else if (typeof manifestObj[k] !== 'string') {
9
+ errors.push(`Key '${k}' must be a string`);
10
+ } else if (k === 'slug') {
11
+ if (!/^[a-z0-9-_]+$/i.test(manifestObj[k])) {
12
+ errors.push(`Key 'slug' must be alphanumeric with dashes or underscores only`);
13
+ }
14
+ }
15
+ });
16
+
17
+ if (manifestObj.allowed_file_patterns !== undefined) {
18
+ if (!Array.isArray(manifestObj.allowed_file_patterns)) {
19
+ errors.push(`allowed_file_patterns must be an array`);
20
+ } else {
21
+ manifestObj.allowed_file_patterns.forEach(pat => {
22
+ if (typeof pat !== 'string') {
23
+ errors.push(`allowed_file_patterns item must be a string: ${pat}`);
24
+ return;
25
+ }
26
+ const normPattern = pat.replace(/\\/g, '/').trim();
27
+ const isSafeSubdir = [
28
+ '.ai/plugins/',
29
+ '.ai/registries/',
30
+ '.ai/templates/',
31
+ '.ai/skills/',
32
+ '.ai/checks/',
33
+ '.ai/prompts/',
34
+ '.ai/adapters/',
35
+ 'adapters/'
36
+ ].some(prefix => normPattern.startsWith(prefix));
37
+
38
+ const hasTraversal = normPattern.includes('..') || normPattern.startsWith('/');
39
+ const isBlacklisted = [
40
+ '.env',
41
+ '.npmrc',
42
+ '.git/',
43
+ 'node_modules/',
44
+ 'package.json',
45
+ 'package-lock.json'
46
+ ].some(black => normPattern.includes(black));
47
+
48
+ if (!isSafeSubdir || hasTraversal || isBlacklisted) {
49
+ errors.push(`File pattern '${pat}' violates safety boundaries`);
50
+ }
51
+ });
52
+ }
53
+ }
54
+
55
+ if (manifestObj.denied_file_patterns !== undefined) {
56
+ if (!Array.isArray(manifestObj.denied_file_patterns)) {
57
+ errors.push(`denied_file_patterns must be an array`);
58
+ } else {
59
+ manifestObj.denied_file_patterns.forEach(pat => {
60
+ if (typeof pat !== 'string') {
61
+ errors.push(`denied_file_patterns item must be a string: ${pat}`);
62
+ }
63
+ });
64
+ }
65
+ }
66
+
67
+ if (manifestObj.workflows !== undefined) {
68
+ if (typeof manifestObj.workflows !== 'object' || Array.isArray(manifestObj.workflows)) {
69
+ errors.push(`workflows must be an object`);
70
+ }
71
+ }
72
+
73
+ if (manifestObj.templates !== undefined) {
74
+ if (typeof manifestObj.templates !== 'object' || Array.isArray(manifestObj.templates)) {
75
+ errors.push(`templates must be an object`);
76
+ }
77
+ }
78
+
79
+ if (manifestObj.adapters !== undefined) {
80
+ if (typeof manifestObj.adapters !== 'object' || Array.isArray(manifestObj.adapters)) {
81
+ errors.push(`adapters must be an object`);
82
+ }
83
+ }
84
+
85
+ if (manifestObj.safety_notes !== undefined) {
86
+ if (typeof manifestObj.safety_notes !== 'string') {
87
+ errors.push(`safety_notes must be a string`);
88
+ }
89
+ }
90
+
91
+ return {
92
+ success: errors.length === 0,
93
+ errors
94
+ };
95
+ }
@@ -0,0 +1,40 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { sourceRoot } from '../core/globals.js';
4
+ import { parseYaml } from '../core/yaml.js';
5
+
6
+ export function loadRegistrySources() {
7
+ const paths = [
8
+ join(sourceRoot, '.ai', 'registries', 'sources.yaml')
9
+ ];
10
+ for (const p of paths) {
11
+ if (existsSync(p)) {
12
+ try {
13
+ const parsed = parseYaml(readFileSync(p, 'utf8'));
14
+ return parsed.sources || [];
15
+ } catch (e) {}
16
+ }
17
+ }
18
+ return [{ name: 'bundled', type: 'local', url: '.ai/plugins/catalog.yaml', enabled: true, trust_level: 'trusted', safety_policy: 'sandboxed', signature_required: false, checksum_required: false }];
19
+ }
20
+
21
+ export function saveRegistrySources(sources) {
22
+ const path = join(sourceRoot, '.ai', 'registries', 'sources.yaml');
23
+ let yaml = '# Registry Sources Configuration\n';
24
+ yaml += '# Remote registries are DISABLED by default.\n';
25
+ yaml += '# Enable via .ai/policies/registry-policy.yaml (set allow_remote_registries: true)\n\n';
26
+ yaml += 'sources:\n';
27
+ sources.forEach(s => {
28
+ yaml += ` - name: "${s.name}"\n`;
29
+ yaml += ` type: "${s.type}"\n`;
30
+ yaml += ` url: "${s.url}"\n`;
31
+ yaml += ` enabled: ${s.enabled}\n`;
32
+ yaml += ` trust_level: "${s.trust_level}"\n`;
33
+ yaml += ` safety_policy: "${s.safety_policy}"\n`;
34
+ yaml += ` signature_required: ${s.signature_required}\n`;
35
+ yaml += ` checksum_required: ${s.checksum_required}\n`;
36
+ if (s.last_synced_at) yaml += ` last_synced_at: "${s.last_synced_at}"\n`;
37
+ if (s.pinned_commit_or_hash) yaml += ` pinned_commit_or_hash: "${s.pinned_commit_or_hash}"\n`;
38
+ });
39
+ writeFileSync(path, yaml, 'utf8');
40
+ }
@@ -0,0 +1,45 @@
1
+ export function validateRegistryUrl(urlStr, policy = {}) {
2
+ if (!urlStr || typeof urlStr !== 'string') {
3
+ throw new Error('Registry URL must be a non-empty string.');
4
+ }
5
+
6
+ // Reject empty/whitespace/control characters
7
+ if (urlStr.trim() === '' || /\s/.test(urlStr) || /[\x00-\x1F\x7F-\x9F]/.test(urlStr)) {
8
+ throw new Error('Registry URL must not contain whitespace or control characters.');
9
+ }
10
+
11
+ // Reject single quotes, double quotes, backticks
12
+ if (/['"`]/.test(urlStr)) {
13
+ throw new Error('Registry URL must not contain quotes or backticks.');
14
+ }
15
+
16
+ // Reject shell metacharacters
17
+ if (/[\$\;\&\|<>\(\)\*]/.test(urlStr)) {
18
+ throw new Error('Registry URL must not contain shell metacharacters.');
19
+ }
20
+
21
+ let parsedUrl;
22
+ try {
23
+ parsedUrl = new URL(urlStr);
24
+ } catch (e) {
25
+ throw new Error('Registry URL is malformed or invalid.');
26
+ }
27
+
28
+ // Reject username/password credentials
29
+ if (parsedUrl.username || parsedUrl.password) {
30
+ throw new Error('Registry URL must not contain credentials.');
31
+ }
32
+
33
+ const protocol = parsedUrl.protocol;
34
+ const allowedProtocols = ['https:'];
35
+
36
+ if (policy.allow_http_localhost === true) {
37
+ if (parsedUrl.hostname === 'localhost' || parsedUrl.hostname === '127.0.0.1') {
38
+ allowedProtocols.push('http:');
39
+ }
40
+ }
41
+
42
+ if (!allowedProtocols.includes(protocol)) {
43
+ throw new Error(`Registry URL protocol '${protocol}' is not allowed. Only HTTPS is permitted.`);
44
+ }
45
+ }
@@ -0,0 +1,37 @@
1
+ # MultiModel Dev OS Testing Suite
2
+
3
+ This directory contains the testing manuals, schemas, and smoke routines to protect the code quality of `multimodel-dev-os`.
4
+
5
+ ---
6
+
7
+ ## 1. Testing Strategy
8
+
9
+ We enforce a multi-tiered validation approach to protect release packaging:
10
+
11
+ ```
12
+ ┌────────────────────────────────────────────────────────┐
13
+ │ Tier 1: Structural Verification (scripts/verify.js) │
14
+ │ Enforces 100+ files and contract schemas checkouts │
15
+ └──────────────────────────┬─────────────────────────────┘
16
+
17
+ ┌──────────────────────────▼─────────────────────────────┐
18
+ │ Tier 2: CLI Smoke Checks (bin/multimodel-dev-os.js) │
19
+ │ Validates command signature help and version trackers │
20
+ └──────────────────────────┬─────────────────────────────┘
21
+
22
+ ┌──────────────────────────▼─────────────────────────────┐
23
+ │ Tier 3: Template QA checks │
24
+ │ Scaffolds tech templates to confirm zero-warnings runs │
25
+ └────────────────────────────────────────────────────────┘
26
+ ```
27
+
28
+ ---
29
+
30
+ ## 2. Running the Linter Verification
31
+
32
+ Always execute the strict linter before committing or tagging:
33
+ ```bash
34
+ npm run verify
35
+ ```
36
+
37
+ This dynamic zero-dependency Node.js pipeline audits files structures, config directories, schemas, and package dry-run tarball footprint constraints.
@@ -0,0 +1,22 @@
1
+ # Integration Fixture Strategy
2
+
3
+ To confirm that `multimodel-dev-os` behaves consistently across distinct operating systems and target directory boundaries, future automated integrations tests should utilize the fixtures detailed here.
4
+
5
+ ---
6
+
7
+ ## 1. Planned Fixtures Layout
8
+
9
+ 1. **`tests/fixtures/pristine/`**:
10
+ - Representing an empty repository where `init` command can execute with zero conflict.
11
+ 2. **`tests/fixtures/cluttered/`**:
12
+ - Represents a repository containing legacy rule files to test conflict resolution and `-f, --force` parameters.
13
+ 3. **`tests/fixtures/broken-config/`**:
14
+ - Formatted with syntactic invalid config configurations to verify validate catches and exits with appropriate non-zero error logs.
15
+
16
+ ---
17
+
18
+ ## 2. Dynamic Fixture Auditing
19
+
20
+ When writing assertions:
21
+ - Never commit active `.cursorrules` or `.gemini/` folders directly inside the templates' fixture folders to avoid index pollutions.
22
+ - Use dry-run actions (`-d, --dry-run`) to assert targeted writes without mutative writes to actual testing paths.
@@ -0,0 +1,10 @@
1
+ # Custom Template Test Fixture
2
+
3
+ This directory serves as a fixture for testing custom template validation and initialization flows.
4
+ It contains the bare minimum files to simulate a template source package:
5
+
6
+ * AGENTS.md
7
+ * MEMORY.md
8
+ * TASKS.md
9
+ * RUNBOOK.md
10
+ * .ai/config.yaml
@@ -0,0 +1,28 @@
1
+ ---
2
+ id: proposal-20260611-000001
3
+ created_at: 2026-06-11T00:00:01Z
4
+ title: Approved Append Line Proposal
5
+ problem: Test append line behavior.
6
+ evidence: N/A
7
+ risk_level: low
8
+ affected_files:
9
+ - tests/fixtures/custom-template-example/append-target.md
10
+ suggested_change: Append a line to the target.
11
+ verify_command: npm run verify
12
+ rollback_plan: git checkout -- tests/fixtures/custom-template-example/append-target.md
13
+ approval_status: approved
14
+ ---
15
+
16
+ # Approved Append Line Proposal
17
+
18
+ ```json
19
+ {
20
+ "operations": [
21
+ {
22
+ "type": "append_line",
23
+ "path": "tests/fixtures/custom-template-example/append-target.md",
24
+ "line": "added line content"
25
+ }
26
+ ]
27
+ }
28
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ id: proposal-20260611-000000
3
+ created_at: 2026-06-11T00:00:00Z
4
+ title: Approved Create File Proposal
5
+ problem: Missing custom templates in tests.
6
+ evidence: Directory is empty.
7
+ risk_level: low
8
+ affected_files:
9
+ - tests/fixtures/custom-template-example/new-file.md
10
+ suggested_change: Create a new markdown file.
11
+ verify_command: npm run verify
12
+ rollback_plan: rm tests/fixtures/custom-template-example/new-file.md
13
+ approval_status: approved
14
+ ---
15
+
16
+ # Approved Create File Proposal
17
+
18
+ ```json
19
+ {
20
+ "operations": [
21
+ {
22
+ "type": "create_file",
23
+ "path": "tests/fixtures/custom-template-example/new-file.md",
24
+ "content": "new file content\n",
25
+ "overwrite": true
26
+ }
27
+ ]
28
+ }
29
+ ```
@@ -0,0 +1,30 @@
1
+ ---
2
+ id: proposal-20260611-000002
3
+ created_at: 2026-06-11T00:00:02Z
4
+ title: Approved Replace Text Proposal
5
+ problem: Test replace text behavior.
6
+ evidence: N/A
7
+ risk_level: low
8
+ affected_files:
9
+ - tests/fixtures/custom-template-example/replace-target.md
10
+ suggested_change: Replace placeholder in the target.
11
+ verify_command: npm run verify
12
+ rollback_plan: git checkout -- tests/fixtures/custom-template-example/replace-target.md
13
+ approval_status: approved
14
+ ---
15
+
16
+ # Approved Replace Text Proposal
17
+
18
+ ```json
19
+ {
20
+ "operations": [
21
+ {
22
+ "type": "replace_text",
23
+ "path": "tests/fixtures/custom-template-example/replace-target.md",
24
+ "find": "placeholder",
25
+ "replace": "replaced content",
26
+ "allow_multiple": false
27
+ }
28
+ ]
29
+ }
30
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ id: proposal-20260611-000004
3
+ created_at: 2026-06-11T00:00:04Z
4
+ title: Existing Create File No Overwrite Proposal
5
+ problem: Test existing file without overwrite flag.
6
+ evidence: N/A
7
+ risk_level: low
8
+ affected_files:
9
+ - tests/fixtures/custom-template-example/existing.md
10
+ suggested_change: Create file without overwrite flag.
11
+ verify_command: npm run verify
12
+ rollback_plan: N/A
13
+ approval_status: approved
14
+ ---
15
+
16
+ # Existing Create File No Overwrite Proposal
17
+
18
+ ```json
19
+ {
20
+ "operations": [
21
+ {
22
+ "type": "create_file",
23
+ "path": "tests/fixtures/custom-template-example/existing.md",
24
+ "content": "new content\n",
25
+ "overwrite": false
26
+ }
27
+ ]
28
+ }
29
+ ```