@rune-kit/rune 2.4.0 → 2.7.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/README.md +47 -17
- package/compiler/__tests__/executive-dashboards.test.js +285 -0
- package/compiler/__tests__/inject.test.js +128 -0
- package/compiler/__tests__/orchestrators.test.js +151 -0
- package/compiler/__tests__/org-templates.test.js +447 -0
- package/compiler/__tests__/pack-split.test.js +141 -1
- package/compiler/__tests__/parser.test.js +147 -1
- package/compiler/__tests__/scripts-bundling.test.js +10 -11
- package/compiler/__tests__/skill-index.test.js +218 -0
- package/compiler/__tests__/status.test.js +336 -0
- package/compiler/__tests__/templates.test.js +245 -0
- package/compiler/__tests__/visualizer.test.js +325 -0
- package/compiler/adapters/antigravity.js +18 -4
- package/compiler/bin/rune.js +90 -1
- package/compiler/doctor.js +283 -2
- package/compiler/emitter.js +490 -17
- package/compiler/parser.js +255 -4
- package/compiler/status.js +342 -0
- package/compiler/visualizer.js +622 -0
- package/hooks/hooks.json +12 -0
- package/hooks/intent-router/index.cjs +108 -0
- package/hooks/pre-tool-guard/index.cjs +177 -68
- package/package.json +63 -63
- package/skills/autopsy/SKILL.md +48 -1
- package/skills/brainstorm/SKILL.md +2 -0
- package/skills/completion-gate/SKILL.md +26 -1
- package/skills/context-engine/SKILL.md +93 -2
- package/skills/cook/SKILL.md +794 -648
- package/skills/debug/SKILL.md +409 -392
- package/skills/deploy/SKILL.md +2 -0
- package/skills/docs/SKILL.md +28 -3
- package/skills/fix/SKILL.md +284 -281
- package/skills/mcp-builder/SKILL.md +53 -1
- package/skills/onboard/SKILL.md +58 -1
- package/skills/perf/SKILL.md +34 -1
- package/skills/plan/SKILL.md +372 -342
- package/skills/preflight/SKILL.md +396 -360
- package/skills/retro/SKILL.md +95 -1
- package/skills/review/SKILL.md +535 -489
- package/skills/scope-guard/SKILL.md +1 -0
- package/skills/scout/SKILL.md +1 -0
- package/skills/sentinel/SKILL.md +353 -299
- package/skills/sentinel/references/policy-driven-constraints.md +424 -0
- package/skills/session-bridge/SKILL.md +58 -2
- package/skills/session-bridge/references/evolutionary-memory-patterns.md +312 -0
- package/skills/team/SKILL.md +16 -1
- package/skills/test/SKILL.md +587 -585
- package/skills/verification/SKILL.md +1 -0
- package/skills/watchdog/SKILL.md +2 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describe, test } from 'node:test';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { parseSkill } from '../parser.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const BUSINESS_SKILLS_DIR = path.resolve(__dirname, '../../../Business/skills');
|
|
10
|
+
|
|
11
|
+
// Signal naming pattern
|
|
12
|
+
const SIGNAL_NAME_PATTERN = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/;
|
|
13
|
+
|
|
14
|
+
const ORCHESTRATORS = ['launch-product', 'quarterly-review', 'compliance-audit', 'customer-lifecycle'];
|
|
15
|
+
|
|
16
|
+
// Skip Business tier tests when Business repo is not available (CI)
|
|
17
|
+
const HAS_BUSINESS = existsSync(BUSINESS_SKILLS_DIR);
|
|
18
|
+
|
|
19
|
+
(HAS_BUSINESS ? describe : describe.skip)('Business orchestrators', () => {
|
|
20
|
+
test('skills directory exists', () => {
|
|
21
|
+
assert.ok(existsSync(BUSINESS_SKILLS_DIR), `${BUSINESS_SKILLS_DIR} should exist`);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
for (const name of ORCHESTRATORS) {
|
|
25
|
+
const skillPath = path.join(BUSINESS_SKILLS_DIR, name, 'SKILL.md');
|
|
26
|
+
|
|
27
|
+
describe(name, () => {
|
|
28
|
+
test('SKILL.md exists', () => {
|
|
29
|
+
assert.ok(existsSync(skillPath), `${skillPath} should exist`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Skip remaining tests if file doesn't exist (CI without Business repo)
|
|
33
|
+
if (!existsSync(skillPath)) return;
|
|
34
|
+
|
|
35
|
+
const content = readFileSync(skillPath, 'utf-8');
|
|
36
|
+
const parsed = parseSkill(content, skillPath);
|
|
37
|
+
|
|
38
|
+
test('has correct name', () => {
|
|
39
|
+
assert.strictEqual(parsed.name, name);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('is L1 layer', () => {
|
|
43
|
+
assert.strictEqual(parsed.layer, 'L1', `${name} must be L1 orchestrator`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('uses opus model', () => {
|
|
47
|
+
assert.strictEqual(parsed.model, 'opus', `${name} should use opus for complex orchestration`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('has description', () => {
|
|
51
|
+
assert.ok(parsed.description.length > 50, `${name} should have a meaningful description`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('is context fork', () => {
|
|
55
|
+
assert.strictEqual(parsed.contextFork, true, `${name} should fork context`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('has emit signals', () => {
|
|
59
|
+
assert.ok(parsed.signals, `${name} must have signals`);
|
|
60
|
+
assert.ok(parsed.signals.emit.length > 0, `${name} must emit at least one signal`);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('has listen signals', () => {
|
|
64
|
+
assert.ok(parsed.signals.listen.length > 0, `${name} must listen to at least one signal`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('all signals follow naming convention', () => {
|
|
68
|
+
for (const signal of [...parsed.signals.emit, ...parsed.signals.listen]) {
|
|
69
|
+
assert.ok(SIGNAL_NAME_PATTERN.test(signal), `signal "${signal}" must match lowercase.dot.notation`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('has HARD-GATE block', () => {
|
|
74
|
+
assert.ok(parsed.hardGates.length > 0, `${name} must have at least one HARD-GATE`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('has cross-references to other skills', () => {
|
|
78
|
+
assert.ok(parsed.crossRefs.length >= 3, `${name} should reference at least 3 other skills`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('has Workflow Phases section', () => {
|
|
82
|
+
assert.ok(parsed.sections.has('Workflow Phases'), `${name} must have ## Workflow Phases`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('has Signal Map section', () => {
|
|
86
|
+
assert.ok(parsed.sections.has('Signal Map'), `${name} must have ## Signal Map`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('has Cross-Domain Connections section', () => {
|
|
90
|
+
assert.ok(parsed.sections.has('Cross-Domain Connections'), `${name} must have ## Cross-Domain Connections`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('has Connections section', () => {
|
|
94
|
+
assert.ok(parsed.sections.has('Connections'), `${name} must have ## Connections`);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('has Constraints section', () => {
|
|
98
|
+
assert.ok(parsed.sections.has('Constraints'), `${name} must have ## Constraints`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('references multiple domain packs (cross-domain)', () => {
|
|
102
|
+
const body = parsed.body;
|
|
103
|
+
const domains = ['finance', 'legal', 'hr', 'product', 'sales', 'support', 'enterprise-search', 'data-science'];
|
|
104
|
+
const referencedDomains = domains.filter((d) => body.toLowerCase().includes(d));
|
|
105
|
+
assert.ok(
|
|
106
|
+
referencedDomains.length >= 3,
|
|
107
|
+
`${name} must reference at least 3 domains, found: ${referencedDomains.join(', ')}`,
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('emits business.* signals', () => {
|
|
112
|
+
const businessSignals = parsed.signals.emit.filter((s) => s.startsWith('business.'));
|
|
113
|
+
assert.ok(businessSignals.length > 0, `${name} must emit at least one business.* signal`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('body is substantial (300-500+ lines)', () => {
|
|
117
|
+
const lineCount = content.split('\n').length;
|
|
118
|
+
assert.ok(lineCount >= 100, `${name} should be at least 100 lines, got ${lineCount}`);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
(HAS_BUSINESS ? describe : describe.skip)('orchestrator signal consistency', () => {
|
|
125
|
+
// Verify orchestrators don't declare signals that overlap with each other
|
|
126
|
+
const allEmitted = new Set();
|
|
127
|
+
const duplicates = [];
|
|
128
|
+
|
|
129
|
+
for (const name of ORCHESTRATORS) {
|
|
130
|
+
const skillPath = path.join(BUSINESS_SKILLS_DIR, name, 'SKILL.md');
|
|
131
|
+
if (!existsSync(skillPath)) continue;
|
|
132
|
+
|
|
133
|
+
const content = readFileSync(skillPath, 'utf-8');
|
|
134
|
+
const parsed = parseSkill(content, skillPath);
|
|
135
|
+
|
|
136
|
+
for (const signal of parsed.signals.emit) {
|
|
137
|
+
if (allEmitted.has(signal)) {
|
|
138
|
+
duplicates.push({ signal, skill: name });
|
|
139
|
+
}
|
|
140
|
+
allEmitted.add(signal);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
test('no duplicate emit signals across orchestrators', () => {
|
|
145
|
+
assert.strictEqual(
|
|
146
|
+
duplicates.length,
|
|
147
|
+
0,
|
|
148
|
+
`Duplicate signals: ${duplicates.map((d) => `${d.signal} (${d.skill})`).join(', ')}`,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describe, test } from 'node:test';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { parseOrgConfig } from '../parser.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const BUSINESS_DIR = path.resolve(__dirname, '../../../Business');
|
|
10
|
+
const ORG_TEMPLATES_DIR = path.join(BUSINESS_DIR, 'org-templates');
|
|
11
|
+
const SKILLS_DIR = path.resolve(__dirname, '../../skills');
|
|
12
|
+
|
|
13
|
+
// ─── parseOrgConfig unit tests ────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
describe('parseOrgConfig', () => {
|
|
16
|
+
const minimalTemplate = `---
|
|
17
|
+
name: test-org
|
|
18
|
+
description: Test organization template
|
|
19
|
+
version: "1.0.0"
|
|
20
|
+
tier: business
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Organization: Test Template
|
|
24
|
+
|
|
25
|
+
## Teams
|
|
26
|
+
|
|
27
|
+
| Team | Lead | Domain Packs | Members |
|
|
28
|
+
|------|------|-------------|---------|
|
|
29
|
+
| Engineering | CTO | — | eng-team |
|
|
30
|
+
| Product | VP Product | @rune-pro/product | product-team |
|
|
31
|
+
|
|
32
|
+
## Roles
|
|
33
|
+
|
|
34
|
+
| Role | Permissions | Approval Authority |
|
|
35
|
+
|------|------------|-------------------|
|
|
36
|
+
| admin | all | Can override any gate |
|
|
37
|
+
| contributor | write | Requires admin approval |
|
|
38
|
+
|
|
39
|
+
## Policies
|
|
40
|
+
|
|
41
|
+
### Code Review
|
|
42
|
+
- **Minimum reviewers**: 2
|
|
43
|
+
- **Self-merge allowed**: No
|
|
44
|
+
|
|
45
|
+
### Security
|
|
46
|
+
- **Dependency audit frequency**: Weekly
|
|
47
|
+
- **Secret rotation**: Monthly
|
|
48
|
+
|
|
49
|
+
### Deployment
|
|
50
|
+
- **Staging required**: Yes
|
|
51
|
+
- **Production deploy window**: Weekdays 09:00-16:00
|
|
52
|
+
|
|
53
|
+
## Approval Flows
|
|
54
|
+
|
|
55
|
+
### Feature Launch
|
|
56
|
+
\`\`\`
|
|
57
|
+
contributor proposes → admin approves → deploy
|
|
58
|
+
\`\`\`
|
|
59
|
+
|
|
60
|
+
### Budget Approval
|
|
61
|
+
\`\`\`
|
|
62
|
+
< $5,000: admin approves
|
|
63
|
+
> $5,000: board approves
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
## Governance Level
|
|
67
|
+
|
|
68
|
+
**Moderate** — Balanced speed and safety.
|
|
69
|
+
|
|
70
|
+
- sentinel: enforce mode
|
|
71
|
+
- preflight: full checks
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
test('parses frontmatter correctly', () => {
|
|
75
|
+
const result = parseOrgConfig(minimalTemplate);
|
|
76
|
+
assert.strictEqual(result.name, 'test-org');
|
|
77
|
+
assert.strictEqual(result.description, 'Test organization template');
|
|
78
|
+
assert.strictEqual(result.version, '1.0.0');
|
|
79
|
+
assert.strictEqual(result.tier, 'business');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('parses teams table', () => {
|
|
83
|
+
const result = parseOrgConfig(minimalTemplate);
|
|
84
|
+
assert.strictEqual(result.teams.length, 2);
|
|
85
|
+
assert.strictEqual(result.teams[0].team, 'Engineering');
|
|
86
|
+
assert.strictEqual(result.teams[0].lead, 'CTO');
|
|
87
|
+
assert.strictEqual(result.teams[1].team, 'Product');
|
|
88
|
+
assert.strictEqual(result.teams[1].domain_packs, '@rune-pro/product');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('parses roles table', () => {
|
|
92
|
+
const result = parseOrgConfig(minimalTemplate);
|
|
93
|
+
assert.strictEqual(result.roles.length, 2);
|
|
94
|
+
assert.strictEqual(result.roles[0].role, 'admin');
|
|
95
|
+
assert.strictEqual(result.roles[0].permissions, 'all');
|
|
96
|
+
assert.strictEqual(result.roles[1].role, 'contributor');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('parses policies into structured map', () => {
|
|
100
|
+
const result = parseOrgConfig(minimalTemplate);
|
|
101
|
+
assert.ok(result.policies.code_review, 'Expected code_review policy');
|
|
102
|
+
assert.ok(result.policies.security, 'Expected security policy');
|
|
103
|
+
assert.ok(result.policies.deployment, 'Expected deployment policy');
|
|
104
|
+
|
|
105
|
+
const cr = result.policies.code_review;
|
|
106
|
+
assert.strictEqual(cr.length, 2);
|
|
107
|
+
assert.strictEqual(cr[0].key, 'minimum_reviewers');
|
|
108
|
+
assert.strictEqual(cr[0].value, '2');
|
|
109
|
+
assert.strictEqual(cr[1].key, 'self-merge_allowed');
|
|
110
|
+
assert.strictEqual(cr[1].value, 'No');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('parses security policies', () => {
|
|
114
|
+
const result = parseOrgConfig(minimalTemplate);
|
|
115
|
+
const sec = result.policies.security;
|
|
116
|
+
assert.strictEqual(sec.length, 2);
|
|
117
|
+
assert.strictEqual(sec[0].key, 'dependency_audit_frequency');
|
|
118
|
+
assert.strictEqual(sec[0].value, 'Weekly');
|
|
119
|
+
assert.strictEqual(sec[1].key, 'secret_rotation');
|
|
120
|
+
assert.strictEqual(sec[1].value, 'Monthly');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('parses deployment policies', () => {
|
|
124
|
+
const result = parseOrgConfig(minimalTemplate);
|
|
125
|
+
const dep = result.policies.deployment;
|
|
126
|
+
assert.strictEqual(dep.length, 2);
|
|
127
|
+
assert.strictEqual(dep[0].key, 'staging_required');
|
|
128
|
+
assert.strictEqual(dep[0].value, 'Yes');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('parses approval flows', () => {
|
|
132
|
+
const result = parseOrgConfig(minimalTemplate);
|
|
133
|
+
assert.ok(result.approvalFlows.feature_launch, 'Expected feature_launch flow');
|
|
134
|
+
assert.ok(result.approvalFlows.budget_approval, 'Expected budget_approval flow');
|
|
135
|
+
assert.ok(
|
|
136
|
+
result.approvalFlows.feature_launch.includes('contributor proposes'),
|
|
137
|
+
'Expected contributor proposes in feature launch flow',
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('parses governance level', () => {
|
|
142
|
+
const result = parseOrgConfig(minimalTemplate);
|
|
143
|
+
assert.strictEqual(result.governanceLevel.level, 'moderate');
|
|
144
|
+
assert.ok(result.governanceLevel.settings.length >= 2);
|
|
145
|
+
assert.ok(result.governanceLevel.settings[0].includes('sentinel'));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('handles missing sections gracefully', () => {
|
|
149
|
+
const sparse = `---
|
|
150
|
+
name: sparse
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
# Sparse Org
|
|
154
|
+
|
|
155
|
+
## Teams
|
|
156
|
+
|
|
157
|
+
No table here.
|
|
158
|
+
|
|
159
|
+
## Governance Level
|
|
160
|
+
|
|
161
|
+
**Minimal** — Fast.
|
|
162
|
+
|
|
163
|
+
- basic checks
|
|
164
|
+
`;
|
|
165
|
+
const result = parseOrgConfig(sparse);
|
|
166
|
+
assert.strictEqual(result.name, 'sparse');
|
|
167
|
+
assert.strictEqual(result.teams.length, 0);
|
|
168
|
+
assert.strictEqual(result.roles.length, 0);
|
|
169
|
+
assert.deepStrictEqual(result.policies, {});
|
|
170
|
+
assert.deepStrictEqual(result.approvalFlows, {});
|
|
171
|
+
assert.strictEqual(result.governanceLevel.level, 'minimal');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('handles empty content', () => {
|
|
175
|
+
const result = parseOrgConfig('');
|
|
176
|
+
assert.strictEqual(result.name, '');
|
|
177
|
+
assert.strictEqual(result.teams.length, 0);
|
|
178
|
+
assert.strictEqual(result.roles.length, 0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('preserves filePath', () => {
|
|
182
|
+
const result = parseOrgConfig(minimalTemplate, '/test/org.md');
|
|
183
|
+
assert.strictEqual(result.filePath, '/test/org.md');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ─── Business org template file validation ────────────────────────
|
|
188
|
+
|
|
189
|
+
// Skip Business tier tests when Business repo is not available (CI)
|
|
190
|
+
const HAS_BUSINESS = existsSync(BUSINESS_DIR);
|
|
191
|
+
|
|
192
|
+
(HAS_BUSINESS ? describe : describe.skip)('Business org-templates file validation', () => {
|
|
193
|
+
const templateFiles = [];
|
|
194
|
+
|
|
195
|
+
if (existsSync(ORG_TEMPLATES_DIR)) {
|
|
196
|
+
for (const file of readdirSync(ORG_TEMPLATES_DIR)) {
|
|
197
|
+
if (file.endsWith('.md')) {
|
|
198
|
+
templateFiles.push({
|
|
199
|
+
name: file.replace('.md', ''),
|
|
200
|
+
path: path.join(ORG_TEMPLATES_DIR, file),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
test('org-templates directory exists', () => {
|
|
207
|
+
assert.ok(existsSync(ORG_TEMPLATES_DIR), 'Expected Business/org-templates/ to exist');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('at least 3 org templates exist', () => {
|
|
211
|
+
assert.ok(templateFiles.length >= 3, `Expected >= 3 templates, got ${templateFiles.length}`);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('required templates exist (startup, mid-size, enterprise)', () => {
|
|
215
|
+
const names = templateFiles.map((t) => t.name);
|
|
216
|
+
assert.ok(names.includes('startup'), 'Missing startup template');
|
|
217
|
+
assert.ok(names.includes('mid-size'), 'Missing mid-size template');
|
|
218
|
+
assert.ok(names.includes('enterprise'), 'Missing enterprise template');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
for (const template of templateFiles) {
|
|
222
|
+
describe(`org-templates/${template.name}.md`, () => {
|
|
223
|
+
const content = readFileSync(template.path, 'utf-8');
|
|
224
|
+
const parsed = parseOrgConfig(content, template.path);
|
|
225
|
+
|
|
226
|
+
test('has valid frontmatter with name', () => {
|
|
227
|
+
assert.ok(parsed.name, 'Missing name in frontmatter');
|
|
228
|
+
assert.strictEqual(parsed.name, template.name);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('has description', () => {
|
|
232
|
+
assert.ok(parsed.description, 'Missing description');
|
|
233
|
+
assert.ok(parsed.description.length > 10, 'Description too short');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('has version', () => {
|
|
237
|
+
assert.ok(parsed.version, 'Missing version');
|
|
238
|
+
assert.match(parsed.version, /^\d+\.\d+\.\d+$/, 'Version must be semver');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('tier is business', () => {
|
|
242
|
+
assert.strictEqual(parsed.tier, 'business');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('has Teams section with entries', () => {
|
|
246
|
+
assert.ok(parsed.teams.length > 0, 'Teams table is empty');
|
|
247
|
+
for (const team of parsed.teams) {
|
|
248
|
+
assert.ok(team.team, 'Team row missing team name');
|
|
249
|
+
assert.ok(team.lead, 'Team row missing lead');
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('has Roles section with entries', () => {
|
|
254
|
+
assert.ok(parsed.roles.length > 0, 'Roles table is empty');
|
|
255
|
+
for (const role of parsed.roles) {
|
|
256
|
+
assert.ok(role.role, 'Role row missing role name');
|
|
257
|
+
assert.ok(role.permissions, 'Role row missing permissions');
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('has Policies section', () => {
|
|
262
|
+
const policyKeys = Object.keys(parsed.policies);
|
|
263
|
+
assert.ok(policyKeys.length > 0, 'No policies found');
|
|
264
|
+
assert.ok(policyKeys.includes('code_review'), 'Missing Code Review policy');
|
|
265
|
+
assert.ok(policyKeys.includes('security'), 'Missing Security policy');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('has code review policy with minimum_reviewers', () => {
|
|
269
|
+
const cr = parsed.policies.code_review;
|
|
270
|
+
assert.ok(cr, 'Missing code_review policy');
|
|
271
|
+
const minReviewers = cr.find((r) => r.key === 'minimum_reviewers');
|
|
272
|
+
assert.ok(minReviewers, 'Missing minimum_reviewers in Code Review policy');
|
|
273
|
+
const count = parseInt(minReviewers.value, 10);
|
|
274
|
+
assert.ok(count >= 1, 'minimum_reviewers must be >= 1');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('has security policy with dependency audit', () => {
|
|
278
|
+
const sec = parsed.policies.security;
|
|
279
|
+
assert.ok(sec, 'Missing security policy');
|
|
280
|
+
const audit = sec.find((r) => r.key === 'dependency_audit_frequency');
|
|
281
|
+
assert.ok(audit, 'Missing dependency_audit_frequency in Security policy');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('has Approval Flows section', () => {
|
|
285
|
+
const flowKeys = Object.keys(parsed.approvalFlows);
|
|
286
|
+
assert.ok(flowKeys.length > 0, 'No approval flows found');
|
|
287
|
+
assert.ok(flowKeys.includes('feature_launch'), 'Missing Feature Launch flow');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('has valid governance level', () => {
|
|
291
|
+
const validLevels = ['minimal', 'moderate', 'maximum'];
|
|
292
|
+
assert.ok(
|
|
293
|
+
validLevels.includes(parsed.governanceLevel.level),
|
|
294
|
+
`Governance level '${parsed.governanceLevel.level}' not in [${validLevels.join(', ')}]`,
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('governance settings reference sentinel', () => {
|
|
299
|
+
const hasSentinel = parsed.governanceLevel.settings.some((s) => s.toLowerCase().includes('sentinel'));
|
|
300
|
+
assert.ok(hasSentinel, 'Governance settings should reference sentinel mode');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('## Governance Level section exists in body', () => {
|
|
304
|
+
assert.ok(content.includes('## Governance Level'), 'Missing ## Governance Level heading');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('references rune pack names correctly', () => {
|
|
308
|
+
// All pack references should be @rune-pro/* or @rune-business/*
|
|
309
|
+
const packRefs = content.match(/@rune-(?:pro|business)\/[\w-]+/g) || [];
|
|
310
|
+
for (const ref of packRefs) {
|
|
311
|
+
assert.match(ref, /^@rune-(pro|business)\/[a-z][\w-]*$/, `Invalid pack reference: ${ref}`);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ─── Governance level scaling tests ───────────────────────────────
|
|
319
|
+
|
|
320
|
+
(HAS_BUSINESS ? describe : describe.skip)('org template governance scaling', () => {
|
|
321
|
+
const templateData = {};
|
|
322
|
+
for (const name of ['startup', 'mid-size', 'enterprise']) {
|
|
323
|
+
const filePath = path.join(ORG_TEMPLATES_DIR, `${name}.md`);
|
|
324
|
+
if (existsSync(filePath)) {
|
|
325
|
+
templateData[name] = parseOrgConfig(readFileSync(filePath, 'utf-8'), filePath);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
test('startup has minimal governance', () => {
|
|
330
|
+
assert.ok(templateData.startup, 'Missing startup template');
|
|
331
|
+
assert.strictEqual(templateData.startup.governanceLevel.level, 'minimal');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('mid-size has moderate governance', () => {
|
|
335
|
+
assert.ok(templateData['mid-size'], 'Missing mid-size template');
|
|
336
|
+
assert.strictEqual(templateData['mid-size'].governanceLevel.level, 'moderate');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('enterprise has maximum governance', () => {
|
|
340
|
+
assert.ok(templateData.enterprise, 'Missing enterprise template');
|
|
341
|
+
assert.strictEqual(templateData.enterprise.governanceLevel.level, 'maximum');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('team count scales: startup < mid-size < enterprise', () => {
|
|
345
|
+
const s = templateData.startup?.teams.length || 0;
|
|
346
|
+
const m = templateData['mid-size']?.teams.length || 0;
|
|
347
|
+
const e = templateData.enterprise?.teams.length || 0;
|
|
348
|
+
assert.ok(s < m, `startup teams (${s}) should be < mid-size (${m})`);
|
|
349
|
+
assert.ok(m <= e, `mid-size teams (${m}) should be <= enterprise (${e})`);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('role count scales: startup < mid-size < enterprise', () => {
|
|
353
|
+
const s = templateData.startup?.roles.length || 0;
|
|
354
|
+
const m = templateData['mid-size']?.roles.length || 0;
|
|
355
|
+
const e = templateData.enterprise?.roles.length || 0;
|
|
356
|
+
assert.ok(s < m, `startup roles (${s}) should be < mid-size (${m})`);
|
|
357
|
+
assert.ok(m <= e, `mid-size roles (${m}) should be <= enterprise (${e})`);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('startup minimum_reviewers <= mid-size <= enterprise', () => {
|
|
361
|
+
const getMinReviewers = (data) => {
|
|
362
|
+
const cr = data?.policies.code_review;
|
|
363
|
+
if (!cr) return 0;
|
|
364
|
+
const r = cr.find((x) => x.key === 'minimum_reviewers');
|
|
365
|
+
return r ? parseInt(r.value, 10) : 0;
|
|
366
|
+
};
|
|
367
|
+
const s = getMinReviewers(templateData.startup);
|
|
368
|
+
const m = getMinReviewers(templateData['mid-size']);
|
|
369
|
+
const e = getMinReviewers(templateData.enterprise);
|
|
370
|
+
assert.ok(s <= m, `startup reviewers (${s}) should be <= mid-size (${m})`);
|
|
371
|
+
assert.ok(m <= e, `mid-size reviewers (${m}) should be <= enterprise (${e})`);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('enterprise has more policy categories than startup', () => {
|
|
375
|
+
const sKeys = Object.keys(templateData.startup?.policies || {}).length;
|
|
376
|
+
const eKeys = Object.keys(templateData.enterprise?.policies || {}).length;
|
|
377
|
+
assert.ok(eKeys >= sKeys, `enterprise policies (${eKeys}) should be >= startup (${sKeys})`);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('enterprise has more approval flows than startup', () => {
|
|
381
|
+
const sFlows = Object.keys(templateData.startup?.approvalFlows || {}).length;
|
|
382
|
+
const eFlows = Object.keys(templateData.enterprise?.approvalFlows || {}).length;
|
|
383
|
+
assert.ok(eFlows >= sFlows, `enterprise flows (${eFlows}) should be >= startup (${sFlows})`);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('enterprise governance settings are more comprehensive', () => {
|
|
387
|
+
const sSettings = templateData.startup?.governanceLevel.settings.length || 0;
|
|
388
|
+
const eSettings = templateData.enterprise?.governanceLevel.settings.length || 0;
|
|
389
|
+
assert.ok(eSettings >= sSettings, `enterprise settings (${eSettings}) should be >= startup (${sSettings})`);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// ─── Sentinel + preflight org integration ─────────────────────────
|
|
394
|
+
|
|
395
|
+
describe('sentinel/preflight org policy integration', () => {
|
|
396
|
+
const sentinelPath = path.join(SKILLS_DIR, 'sentinel', 'SKILL.md');
|
|
397
|
+
const preflightPath = path.join(SKILLS_DIR, 'preflight', 'SKILL.md');
|
|
398
|
+
|
|
399
|
+
test('sentinel references .rune/org/org.md', () => {
|
|
400
|
+
const content = readFileSync(sentinelPath, 'utf-8');
|
|
401
|
+
assert.ok(
|
|
402
|
+
content.includes('.rune/org/org.md'),
|
|
403
|
+
'sentinel should reference .rune/org/org.md for org policy loading',
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('sentinel has Organization Policy Enforcement step', () => {
|
|
408
|
+
const content = readFileSync(sentinelPath, 'utf-8');
|
|
409
|
+
assert.ok(
|
|
410
|
+
content.includes('Organization Policy Enforcement'),
|
|
411
|
+
'sentinel should have Organization Policy Enforcement step',
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test('sentinel handles missing org config gracefully', () => {
|
|
416
|
+
const content = readFileSync(sentinelPath, 'utf-8');
|
|
417
|
+
assert.ok(content.includes('no org config'), 'sentinel should handle missing org config');
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('preflight references .rune/org/org.md', () => {
|
|
421
|
+
const content = readFileSync(preflightPath, 'utf-8');
|
|
422
|
+
assert.ok(content.includes('.rune/org/org.md'), 'preflight should reference .rune/org/org.md for org requirements');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('preflight has Organization Approval Requirements step', () => {
|
|
426
|
+
const content = readFileSync(preflightPath, 'utf-8');
|
|
427
|
+
assert.ok(
|
|
428
|
+
content.includes('Organization Approval Requirements'),
|
|
429
|
+
'preflight should have Organization Approval Requirements step',
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('preflight handles missing org config gracefully', () => {
|
|
434
|
+
const content = readFileSync(preflightPath, 'utf-8');
|
|
435
|
+
assert.ok(content.includes('no org config'), 'preflight should handle missing org config');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('sentinel step is numbered 4.86 (between contract 4.85 and six-gate 4.9)', () => {
|
|
439
|
+
const content = readFileSync(sentinelPath, 'utf-8');
|
|
440
|
+
assert.ok(content.includes('Step 4.86'), 'sentinel org policy step should be numbered 4.86');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test('preflight step is numbered 4.6 (between domain hooks 4.5 and composite score 4.8)', () => {
|
|
444
|
+
const content = readFileSync(preflightPath, 'utf-8');
|
|
445
|
+
assert.ok(content.includes('Step 4.6'), 'preflight org requirements step should be numbered 4.6');
|
|
446
|
+
});
|
|
447
|
+
});
|