@buba_71/levit 0.3.4 → 0.8.2

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 (83) hide show
  1. package/README.md +192 -7
  2. package/dist/bin/cli.js +67 -24
  3. package/dist/src/commands/decision.js +74 -0
  4. package/dist/src/commands/feature.js +159 -0
  5. package/dist/src/commands/handoff.js +71 -0
  6. package/dist/src/commands/index.js +13 -0
  7. package/dist/src/commands/init.js +53 -39
  8. package/dist/src/commands/validate.js +84 -0
  9. package/dist/src/core/cli_args.js +35 -0
  10. package/dist/src/core/error_helper.js +93 -0
  11. package/dist/src/core/errors.js +25 -0
  12. package/dist/src/core/frontmatter.js +62 -0
  13. package/dist/src/core/ids.js +34 -0
  14. package/dist/src/core/levit_project.js +30 -0
  15. package/dist/src/core/logger.js +77 -0
  16. package/dist/src/core/table.js +63 -0
  17. package/dist/src/core/write_file.js +15 -0
  18. package/dist/src/init.js +9 -0
  19. package/dist/src/readers/decision_reader.js +47 -0
  20. package/dist/src/readers/feature_reader.js +48 -0
  21. package/dist/src/readers/handoff_reader.js +47 -0
  22. package/dist/src/services/decision_service.js +49 -0
  23. package/dist/src/services/feature_service.js +89 -0
  24. package/dist/src/services/handoff_service.js +37 -0
  25. package/dist/src/services/manifest_service.js +89 -0
  26. package/dist/src/services/project_service.js +39 -0
  27. package/dist/src/services/validation_service.js +461 -0
  28. package/dist/src/types/domain.js +2 -0
  29. package/dist/src/types/index.js +2 -0
  30. package/dist/src/types/manifest.js +26 -0
  31. package/dist/tests/cli/integration.test.js +165 -0
  32. package/dist/tests/services/decision_service.test.js +27 -0
  33. package/dist/tests/services/feature_service.test.js +42 -0
  34. package/dist/tests/services/handoff_service.test.js +28 -0
  35. package/dist/tests/services/manifest_service.test.js +189 -0
  36. package/dist/tests/services/validation_service.test.js +196 -0
  37. package/package.json +7 -2
  38. package/templates/default/.github/workflows/README.md +56 -0
  39. package/templates/default/.github/workflows/levit-validate.yml +93 -0
  40. package/templates/default/.gitlab-ci.yml +73 -0
  41. package/templates/default/.levit/AGENT_CONTRACT.md +34 -0
  42. package/templates/default/.levit/AGENT_ONBOARDING.md +29 -0
  43. package/templates/default/.levit/evals/README.md +14 -0
  44. package/templates/default/.levit/evals/conformance.eval.ts +16 -0
  45. package/templates/default/.levit/features/INTENT.md +32 -0
  46. package/templates/default/.levit/features/README.md +11 -0
  47. package/templates/default/.levit/handoff/.gitkeep +0 -0
  48. package/templates/default/.levit/prompts/global-rules.md +10 -0
  49. package/templates/default/.levit/prompts/refactoring-guidelines.md +9 -0
  50. package/templates/default/.levit/workflows/example-task.md +9 -0
  51. package/templates/default/.levit/workflows/submit-for-review.md +18 -0
  52. package/templates/default/HUMAN_AGENT_MANAGER.md +654 -0
  53. package/templates/default/MIGRATION_GUIDE.md +597 -0
  54. package/templates/default/README.md +49 -11
  55. package/templates/default/SOCIAL_CONTRACT.md +5 -0
  56. package/templates/symfony/.github/workflows/README.md +56 -0
  57. package/templates/symfony/.github/workflows/levit-validate.yml +82 -0
  58. package/templates/symfony/.gitlab-ci.yml +62 -0
  59. package/templates/symfony/.levit/AGENT_CONTRACT.md +34 -0
  60. package/templates/symfony/.levit/AGENT_ONBOARDING.md +124 -0
  61. package/templates/symfony/.levit/decisions/.gitkeep +0 -0
  62. package/templates/symfony/.levit/features/INTENT.md +32 -0
  63. package/templates/symfony/.levit/features/README.md +11 -0
  64. package/templates/symfony/.levit/handoff/.gitkeep +0 -0
  65. package/templates/symfony/.levit/prompts/global-rules.md +10 -0
  66. package/templates/symfony/.levit/prompts/refactoring-guidelines.md +9 -0
  67. package/templates/symfony/.levit/workflows/example-task.md +9 -0
  68. package/templates/symfony/.levit/workflows/submit-for-review.md +18 -0
  69. package/templates/symfony/HUMAN_AGENT_MANAGER.md +654 -0
  70. package/templates/symfony/MIGRATION_GUIDE.md +597 -0
  71. package/templates/symfony/README.md +101 -0
  72. package/templates/symfony/SOCIAL_CONTRACT.md +34 -0
  73. package/dist/tests/init.test.js +0 -58
  74. package/templates/default/features/README.md +0 -12
  75. /package/templates/default/{agents → .levit/agents}/AGENTS.md +0 -0
  76. /package/templates/default/{agents → .levit/agents}/boundaries.md +0 -0
  77. /package/templates/default/{package.json → .levit/decisions/.gitkeep} +0 -0
  78. /package/templates/default/{docs → .levit/docs}/architecture.md +0 -0
  79. /package/templates/default/{pipelines → .levit/pipelines}/README.md +0 -0
  80. /package/templates/default/{roles → .levit/roles}/README.md +0 -0
  81. /package/templates/default/{roles → .levit/roles}/devops.md +0 -0
  82. /package/templates/default/{roles → .levit/roles}/qa.md +0 -0
  83. /package/templates/default/{roles → .levit/roles}/security.md +0 -0
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const node_assert_1 = __importDefault(require("node:assert"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ const decision_service_1 = require("../../src/services/decision_service");
12
+ (0, node_test_1.default)("DecisionService.createDecision generates correct structure", () => {
13
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "decision-service-test-"));
14
+ const decisionsDir = node_path_1.default.join(tempDir, ".levit", "decisions");
15
+ fs_extra_1.default.ensureDirSync(decisionsDir);
16
+ decision_service_1.DecisionService.createDecision(tempDir, {
17
+ title: "Test Architecture",
18
+ id: "005",
19
+ featureRef: ".levit/features/001.md"
20
+ });
21
+ const expectedFile = node_path_1.default.join(decisionsDir, "ADR-005-test-architecture.md");
22
+ node_assert_1.default.ok(fs_extra_1.default.existsSync(expectedFile));
23
+ const content = fs_extra_1.default.readFileSync(expectedFile, "utf-8");
24
+ node_assert_1.default.ok(content.includes("id: ADR-005"));
25
+ node_assert_1.default.ok(content.includes("depends_on: [.levit/features/001.md]"));
26
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
27
+ });
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const node_assert_1 = __importDefault(require("node:assert"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ const feature_service_1 = require("../../src/services/feature_service");
12
+ (0, node_test_1.default)("FeatureService.createFeature generates correct file", () => {
13
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "feature-service-test-"));
14
+ const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
15
+ fs_extra_1.default.ensureDirSync(featuresDir);
16
+ feature_service_1.FeatureService.createFeature(tempDir, {
17
+ title: "Unit Test Feature",
18
+ slug: "unit-test-feature",
19
+ id: "999"
20
+ });
21
+ const expectedFile = node_path_1.default.join(featuresDir, "999-unit-test-feature.md");
22
+ node_assert_1.default.ok(fs_extra_1.default.existsSync(expectedFile));
23
+ const content = fs_extra_1.default.readFileSync(expectedFile, "utf-8");
24
+ node_assert_1.default.ok(content.includes("id: 999"));
25
+ // Slug is currently only used for filename, not in content
26
+ // assert.ok(content.includes("slug: unit-test-feature"));
27
+ node_assert_1.default.ok(content.includes("# INTENT: Unit Test Feature"));
28
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
29
+ });
30
+ (0, node_test_1.default)("FeatureService.createFeature auto-generates ID", () => {
31
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "feature-service-test-"));
32
+ const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
33
+ fs_extra_1.default.ensureDirSync(featuresDir);
34
+ feature_service_1.FeatureService.createFeature(tempDir, {
35
+ title: "Auto ID Feature",
36
+ slug: "auto-id"
37
+ });
38
+ // Should be 001-auto-id.md since directory is empty
39
+ const expectedFile = node_path_1.default.join(featuresDir, "001-auto-id.md");
40
+ node_assert_1.default.ok(fs_extra_1.default.existsSync(expectedFile));
41
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
42
+ });
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const node_assert_1 = __importDefault(require("node:assert"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ const handoff_service_1 = require("../../src/services/handoff_service");
12
+ (0, node_test_1.default)("HandoffService.createHandoff generates correct file name", () => {
13
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "handoff-service-test-"));
14
+ const handoffDir = node_path_1.default.join(tempDir, ".levit", "handoff");
15
+ fs_extra_1.default.ensureDirSync(handoffDir);
16
+ const date = new Date().toISOString().slice(0, 10);
17
+ handoff_service_1.HandoffService.createHandoff(tempDir, {
18
+ feature: ".levit/features/login.md",
19
+ role: "qa"
20
+ });
21
+ // format: YYYY-MM-DD-filename-role.md
22
+ const expectedFile = node_path_1.default.join(handoffDir, `${date}-login-qa.md`);
23
+ node_assert_1.default.ok(fs_extra_1.default.existsSync(expectedFile));
24
+ const content = fs_extra_1.default.readFileSync(expectedFile, "utf-8");
25
+ node_assert_1.default.ok(content.includes(`id: HAND-${date}-qa`));
26
+ node_assert_1.default.ok(content.includes("owner: qa"));
27
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
28
+ });
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const node_assert_1 = __importDefault(require("node:assert"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ const manifest_service_1 = require("../../src/services/manifest_service");
12
+ (0, node_test_1.default)("ManifestService.read returns default manifest when file doesn't exist", () => {
13
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
14
+ const manifest = manifest_service_1.ManifestService.read(tempDir);
15
+ node_assert_1.default.strictEqual(manifest.project.name, "my-project");
16
+ node_assert_1.default.strictEqual(manifest.version, "1.0.0");
17
+ node_assert_1.default.deepStrictEqual(manifest.features, []);
18
+ node_assert_1.default.deepStrictEqual(manifest.roles, []);
19
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
20
+ });
21
+ (0, node_test_1.default)("ManifestService.read reads existing manifest", () => {
22
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
23
+ const manifestPath = node_path_1.default.join(tempDir, "levit.json");
24
+ const testManifest = {
25
+ version: "1.0.0",
26
+ project: {
27
+ name: "test-project",
28
+ description: "Test"
29
+ },
30
+ governance: {
31
+ autonomy_level: "medium",
32
+ risk_tolerance: "medium"
33
+ },
34
+ features: [],
35
+ roles: [],
36
+ constraints: {},
37
+ paths: {
38
+ features: "features",
39
+ decisions: ".levit/decisions",
40
+ handoffs: ".levit/handoff"
41
+ }
42
+ };
43
+ fs_extra_1.default.writeJsonSync(manifestPath, testManifest, { spaces: 2 });
44
+ const manifest = manifest_service_1.ManifestService.read(tempDir);
45
+ node_assert_1.default.strictEqual(manifest.project.name, "test-project");
46
+ node_assert_1.default.strictEqual(manifest.governance.autonomy_level, "medium");
47
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
48
+ });
49
+ (0, node_test_1.default)("ManifestService.write creates manifest file", () => {
50
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
51
+ const manifestPath = node_path_1.default.join(tempDir, "levit.json");
52
+ const testManifest = {
53
+ version: "1.0.0",
54
+ project: {
55
+ name: "write-test"
56
+ },
57
+ governance: {
58
+ autonomy_level: "low",
59
+ risk_tolerance: "low"
60
+ },
61
+ features: [],
62
+ roles: [],
63
+ constraints: {},
64
+ paths: {
65
+ features: "features",
66
+ decisions: ".levit/decisions",
67
+ handoffs: ".levit/handoff"
68
+ }
69
+ };
70
+ manifest_service_1.ManifestService.write(tempDir, testManifest);
71
+ node_assert_1.default.ok(fs_extra_1.default.existsSync(manifestPath));
72
+ const written = fs_extra_1.default.readJsonSync(manifestPath);
73
+ node_assert_1.default.strictEqual(written.project.name, "write-test");
74
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
75
+ });
76
+ (0, node_test_1.default)("ManifestService.sync discovers features from filesystem", () => {
77
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
78
+ const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
79
+ fs_extra_1.default.ensureDirSync(featuresDir);
80
+ // Create a test feature file
81
+ const featureContent = `---
82
+ id: 001
83
+ status: active
84
+ owner: human
85
+ last_updated: 2026-01-01
86
+ risk_level: low
87
+ depends_on: []
88
+ ---
89
+
90
+ # INTENT: Test Feature
91
+ `;
92
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "001-test-feature.md"), featureContent);
93
+ const manifest = manifest_service_1.ManifestService.sync(tempDir);
94
+ node_assert_1.default.strictEqual(manifest.features.length, 1);
95
+ node_assert_1.default.strictEqual(manifest.features[0].id, "001");
96
+ node_assert_1.default.strictEqual(manifest.features[0].slug, "test-feature");
97
+ node_assert_1.default.strictEqual(manifest.features[0].title, "Test Feature");
98
+ node_assert_1.default.strictEqual(manifest.features[0].status, "active");
99
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
100
+ });
101
+ (0, node_test_1.default)("ManifestService.sync discovers roles from filesystem", () => {
102
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
103
+ const rolesDir = node_path_1.default.join(tempDir, ".levit", "roles");
104
+ fs_extra_1.default.ensureDirSync(rolesDir);
105
+ // Create a test role file
106
+ const roleContent = `# Security Role
107
+ This is the security role description.
108
+ `;
109
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(rolesDir, "security.md"), roleContent);
110
+ const manifest = manifest_service_1.ManifestService.sync(tempDir);
111
+ node_assert_1.default.strictEqual(manifest.roles.length, 1);
112
+ node_assert_1.default.strictEqual(manifest.roles[0].name, "security");
113
+ node_assert_1.default.strictEqual(manifest.roles[0].description, "Security Role");
114
+ node_assert_1.default.strictEqual(manifest.roles[0].path, ".levit/roles/security.md");
115
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
116
+ });
117
+ (0, node_test_1.default)("ManifestService.sync ignores README files", () => {
118
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
119
+ const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
120
+ fs_extra_1.default.ensureDirSync(featuresDir);
121
+ // Create README and INTENT files (should be ignored)
122
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "README.md"), "# Features");
123
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "INTENT.md"), "# Intent Template");
124
+ // Create actual feature
125
+ const featureContent = `---
126
+ id: 001
127
+ status: active
128
+ owner: human
129
+ last_updated: 2026-01-01
130
+ risk_level: low
131
+ depends_on: []
132
+ ---
133
+
134
+ # INTENT: Real Feature
135
+ `;
136
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "001-real-feature.md"), featureContent);
137
+ const manifest = manifest_service_1.ManifestService.sync(tempDir);
138
+ node_assert_1.default.strictEqual(manifest.features.length, 1);
139
+ node_assert_1.default.strictEqual(manifest.features[0].slug, "real-feature");
140
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
141
+ });
142
+ (0, node_test_1.default)("ManifestService.sync handles missing directories gracefully", () => {
143
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
144
+ // No features or roles directories
145
+ const manifest = manifest_service_1.ManifestService.sync(tempDir);
146
+ node_assert_1.default.deepStrictEqual(manifest.features, []);
147
+ node_assert_1.default.deepStrictEqual(manifest.roles, []);
148
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
149
+ });
150
+ (0, node_test_1.default)("ManifestService.sync handles features with invalid frontmatter gracefully", () => {
151
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
152
+ const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
153
+ fs_extra_1.default.ensureDirSync(featuresDir);
154
+ // Create feature with invalid frontmatter (missing closing ---)
155
+ const invalidFeature = `---
156
+ id: 001
157
+ status: active
158
+ # Missing closing delimiter
159
+
160
+ # INTENT: Invalid Feature
161
+ `;
162
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "001-invalid.md"), invalidFeature);
163
+ // Should not throw, but feature might have default values
164
+ const manifest = manifest_service_1.ManifestService.sync(tempDir);
165
+ // Should still discover the file (by filename)
166
+ node_assert_1.default.strictEqual(manifest.features.length, 1);
167
+ node_assert_1.default.strictEqual(manifest.features[0].id, "001");
168
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
169
+ });
170
+ (0, node_test_1.default)("ManifestService.sync extracts title from INTENT header", () => {
171
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
172
+ const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
173
+ fs_extra_1.default.ensureDirSync(featuresDir);
174
+ const featureContent = `---
175
+ id: 002
176
+ status: draft
177
+ owner: human
178
+ last_updated: 2026-01-01
179
+ risk_level: medium
180
+ depends_on: []
181
+ ---
182
+
183
+ # INTENT: Complex Feature Title With Spaces
184
+ `;
185
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "002-complex.md"), featureContent);
186
+ const manifest = manifest_service_1.ManifestService.sync(tempDir);
187
+ node_assert_1.default.strictEqual(manifest.features[0].title, "Complex Feature Title With Spaces");
188
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
189
+ });
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const node_assert_1 = __importDefault(require("node:assert"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ const validation_service_1 = require("../../src/services/validation_service");
12
+ (0, node_test_1.default)("ValidationService reports errors if core directories are missing", () => {
13
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
14
+ // Validate empty dir
15
+ const result = validation_service_1.ValidationService.validate(tempDir);
16
+ // Expect invalid
17
+ node_assert_1.default.strictEqual(result.valid, false);
18
+ node_assert_1.default.ok(result.metrics.errors > 0, "Should have errors");
19
+ // Check specific error code presence
20
+ const missingDirs = result.issues.filter(i => i.code === "MISSING_DIRECTORY");
21
+ node_assert_1.default.ok(missingDirs.length > 0, "Should report missing directories");
22
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
23
+ });
24
+ (0, node_test_1.default)("ValidationService passes for valid structure", () => {
25
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-succ-"));
26
+ // Scout scaffolding: .levit/features, .levit/decisions, .levit/handoff, core files
27
+ const dirs = [".levit/features", ".levit/decisions", ".levit/handoff"];
28
+ dirs.forEach(d => fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, d)));
29
+ const files = ["SOCIAL_CONTRACT.md", ".levit/AGENT_CONTRACT.md", ".levit/AGENT_ONBOARDING.md"];
30
+ files.forEach(f => fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, f), "content"));
31
+ const result = validation_service_1.ValidationService.validate(tempDir);
32
+ node_assert_1.default.strictEqual(result.valid, true);
33
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
34
+ });
35
+ (0, node_test_1.default)("ValidationService detects feature with invalid frontmatter", () => {
36
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
37
+ const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
38
+ fs_extra_1.default.ensureDirSync(featuresDir);
39
+ // Create core structure
40
+ fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/decisions"));
41
+ fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/handoff"));
42
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, "SOCIAL_CONTRACT.md"), "content");
43
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_CONTRACT.md"), "content");
44
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_ONBOARDING.md"), "content");
45
+ // Create feature with missing required fields
46
+ const invalidFeature = `---
47
+ id: 001
48
+ status: active
49
+ # Missing: owner, last_updated, risk_level, depends_on
50
+
51
+ # INTENT: Test Feature
52
+ `;
53
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "001-test.md"), invalidFeature);
54
+ const result = validation_service_1.ValidationService.validate(tempDir);
55
+ node_assert_1.default.strictEqual(result.valid, false);
56
+ const frontmatterErrors = result.issues.filter(i => i.code === "INVALID_FRONTMATTER");
57
+ node_assert_1.default.ok(frontmatterErrors.length > 0, "Should report invalid frontmatter");
58
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
59
+ });
60
+ (0, node_test_1.default)("ValidationService detects feature without INTENT header", () => {
61
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
62
+ const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
63
+ fs_extra_1.default.ensureDirSync(featuresDir);
64
+ // Create core structure
65
+ fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/decisions"));
66
+ fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/handoff"));
67
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, "SOCIAL_CONTRACT.md"), "content");
68
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_CONTRACT.md"), "content");
69
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_ONBOARDING.md"), "content");
70
+ // Create feature without INTENT header
71
+ const featureWithoutIntent = `---
72
+ id: 001
73
+ status: active
74
+ owner: human
75
+ last_updated: 2026-01-01
76
+ risk_level: low
77
+ depends_on: []
78
+ ---
79
+
80
+ # Some other header
81
+ `;
82
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "001-test.md"), featureWithoutIntent);
83
+ const result = validation_service_1.ValidationService.validate(tempDir);
84
+ node_assert_1.default.strictEqual(result.valid, false);
85
+ const structureErrors = result.issues.filter(i => i.code === "INVALID_STRUCTURE");
86
+ node_assert_1.default.ok(structureErrors.length > 0, "Should report missing INTENT header");
87
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
88
+ });
89
+ (0, node_test_1.default)("ValidationService detects decision with invalid frontmatter", () => {
90
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
91
+ const decisionsDir = node_path_1.default.join(tempDir, ".levit/decisions");
92
+ fs_extra_1.default.ensureDirSync(decisionsDir);
93
+ // Create core structure
94
+ fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit", "features"));
95
+ fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit", "handoff"));
96
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, "SOCIAL_CONTRACT.md"), "content");
97
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_CONTRACT.md"), "content");
98
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_ONBOARDING.md"), "content");
99
+ // Create decision with invalid YAML
100
+ const invalidDecision = `---
101
+ id: ADR-001
102
+ status: draft
103
+ owner: human
104
+ last_updated: 2026-01-01
105
+ risk_level: low
106
+ depends_on: [invalid: yaml: syntax]
107
+ ---
108
+
109
+ # ADR 001: Test
110
+ `;
111
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(decisionsDir, "ADR-001-test.md"), invalidDecision);
112
+ const result = validation_service_1.ValidationService.validate(tempDir);
113
+ node_assert_1.default.strictEqual(result.valid, false);
114
+ const frontmatterErrors = result.issues.filter(i => i.code === "INVALID_FRONTMATTER");
115
+ node_assert_1.default.ok(frontmatterErrors.length > 0, "Should report invalid YAML");
116
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
117
+ });
118
+ (0, node_test_1.default)("ValidationService detects handoff with missing frontmatter", () => {
119
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
120
+ const handoffDir = node_path_1.default.join(tempDir, ".levit/handoff");
121
+ fs_extra_1.default.ensureDirSync(handoffDir);
122
+ // Create core structure
123
+ fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit", "features"));
124
+ fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit", "decisions"));
125
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, "SOCIAL_CONTRACT.md"), "content");
126
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_CONTRACT.md"), "content");
127
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_ONBOARDING.md"), "content");
128
+ // Create handoff without frontmatter
129
+ const handoffWithoutFrontmatter = `# Agent Handoff
130
+ No frontmatter here
131
+ `;
132
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(handoffDir, "2026-01-01-test.md"), handoffWithoutFrontmatter);
133
+ const result = validation_service_1.ValidationService.validate(tempDir);
134
+ node_assert_1.default.strictEqual(result.valid, false);
135
+ const frontmatterErrors = result.issues.filter(i => i.code === "INVALID_FRONTMATTER");
136
+ node_assert_1.default.ok(frontmatterErrors.length > 0, "Should report missing frontmatter");
137
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
138
+ });
139
+ (0, node_test_1.default)("ValidationService reports warning when no features exist", () => {
140
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
141
+ const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
142
+ fs_extra_1.default.ensureDirSync(featuresDir);
143
+ // Create core structure
144
+ fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/decisions"));
145
+ fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/handoff"));
146
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, "SOCIAL_CONTRACT.md"), "content");
147
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_CONTRACT.md"), "content");
148
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_ONBOARDING.md"), "content");
149
+ // No features in features directory
150
+ const result = validation_service_1.ValidationService.validate(tempDir);
151
+ node_assert_1.default.strictEqual(result.valid, true, "Should be valid (only warning)");
152
+ node_assert_1.default.ok(result.metrics.warnings > 0, "Should have warnings");
153
+ const noFeaturesWarning = result.issues.find(i => i.code === "NO_FEATURES");
154
+ node_assert_1.default.ok(noFeaturesWarning, "Should report no features warning");
155
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
156
+ });
157
+ (0, node_test_1.default)("ValidationService counts files scanned correctly", () => {
158
+ const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
159
+ const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
160
+ fs_extra_1.default.ensureDirSync(featuresDir);
161
+ // Create core structure
162
+ fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/decisions"));
163
+ fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/handoff"));
164
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, "SOCIAL_CONTRACT.md"), "content");
165
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_CONTRACT.md"), "content");
166
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_ONBOARDING.md"), "content");
167
+ // Create valid feature
168
+ const validFeature = `---
169
+ id: 001
170
+ status: active
171
+ owner: human
172
+ last_updated: 2026-01-01
173
+ risk_level: low
174
+ depends_on: []
175
+ ---
176
+
177
+ # INTENT: Test Feature
178
+ `;
179
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "001-test.md"), validFeature);
180
+ // Create valid decision
181
+ const validDecision = `---
182
+ id: ADR-001
183
+ status: draft
184
+ owner: human
185
+ last_updated: 2026-01-01
186
+ risk_level: low
187
+ depends_on: []
188
+ ---
189
+
190
+ # ADR 001: Test Decision
191
+ `;
192
+ fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/decisions", "ADR-001-test.md"), validDecision);
193
+ const result = validation_service_1.ValidationService.validate(tempDir);
194
+ node_assert_1.default.strictEqual(result.metrics.filesScanned, 2, "Should scan 2 files (1 feature + 1 decision)");
195
+ fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
196
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buba_71/levit",
3
- "version": "0.3.4",
3
+ "version": "0.8.2",
4
4
  "description": "Hybrid starter kit for Antigravity projects",
5
5
  "author": "David BUBA",
6
6
  "license": "MIT",
@@ -19,14 +19,19 @@
19
19
  "main": "dist/src/init.js",
20
20
  "scripts": {
21
21
  "build": "tsc && chmod +x dist/bin/cli.js",
22
+ "prepublishOnly": "npm run build",
22
23
  "start": "node dist/bin/cli.js",
23
24
  "test": "npm run build && node --test dist/tests"
24
25
  },
25
26
  "dependencies": {
26
- "fs-extra": "^11.2.0"
27
+ "fs-extra": "^11.2.0",
28
+ "js-yaml": "^4.1.1",
29
+ "chalk": "^4.1.2",
30
+ "cli-table3": "^0.6.5"
27
31
  },
28
32
  "devDependencies": {
29
33
  "@types/fs-extra": "^11.0.4",
34
+ "@types/js-yaml": "^4.0.9",
30
35
  "@types/node": "^20.11.0",
31
36
  "typescript": "^5.3.0"
32
37
  }
@@ -0,0 +1,56 @@
1
+ # GitHub Actions Workflows
2
+
3
+ This directory contains CI/CD workflows for levit-kit projects.
4
+
5
+ ## Available Workflows
6
+
7
+ ### `levit-validate.yml`
8
+
9
+ Validates the levit-kit project structure on every push and pull request.
10
+
11
+ **What it does**:
12
+ - Runs `levit validate` to check project structure
13
+ - Fails the build if validation errors are found
14
+ - Uploads validation results as artifacts
15
+ - Displays results in GitHub Actions summary
16
+
17
+ **When it runs**:
18
+ - On pushes to `main`, `master`, or `develop` branches
19
+ - On pull requests targeting these branches
20
+
21
+ **How to use**:
22
+ 1. This workflow is automatically included in levit-kit projects
23
+ 2. No configuration needed - it works out of the box
24
+ 3. Customize the `on:` section if you use different branch names
25
+
26
+ **Customization**:
27
+ Edit `.github/workflows/levit-validate.yml` to:
28
+ - Change branch names
29
+ - Add additional validation steps
30
+ - Integrate with other workflows
31
+ - Add notifications
32
+
33
+ ## Adding Custom Workflows
34
+
35
+ You can add additional workflows for:
36
+ - Feature status checks
37
+ - Decision record validation
38
+ - Handoff completeness checks
39
+ - Custom evals
40
+
41
+ Example structure:
42
+ ```yaml
43
+ name: Custom Validation
44
+ on: [push, pull_request]
45
+ jobs:
46
+ custom-check:
47
+ runs-on: ubuntu-latest
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+ - run: your-custom-script.sh
51
+ ```
52
+
53
+ ---
54
+
55
+ *For more information, see the [levit-kit documentation](https://github.com/buba71/levit-kit).*
56
+
@@ -0,0 +1,93 @@
1
+ name: Validate Levit Project
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, master, develop ]
6
+ pull_request:
7
+ branches: [ main, master, develop ]
8
+
9
+ jobs:
10
+ validate:
11
+ name: Validate levit-kit Structure
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - name: Checkout code
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Setup Node.js
19
+ uses: actions/setup-node@v4
20
+ with:
21
+ node-version: '20'
22
+ cache: 'npm'
23
+
24
+ - name: Validate levit-kit structure
25
+ id: validate
26
+ run: |
27
+ npx @buba_71/levit validate --json > validation.json 2>&1 || VALIDATION_EXIT=$?
28
+ if [ -n "$VALIDATION_EXIT" ]; then
29
+ echo "validation_failed=true" >> $GITHUB_OUTPUT
30
+ else
31
+ echo "validation_failed=false" >> $GITHUB_OUTPUT
32
+ fi
33
+
34
+ - name: Display validation results
35
+ if: always()
36
+ run: |
37
+ if [ -f validation.json ]; then
38
+ echo "## Validation Results" >> $GITHUB_STEP_SUMMARY
39
+ echo '```json' >> $GITHUB_STEP_SUMMARY
40
+ cat validation.json >> $GITHUB_STEP_SUMMARY
41
+ echo '```' >> $GITHUB_STEP_SUMMARY
42
+
43
+ # Check for errors in JSON output
44
+ ERRORS=$(node -e "
45
+ try {
46
+ const fs = require('fs');
47
+ const content = fs.readFileSync('validation.json', 'utf8');
48
+ const lines = content.split('\\n').filter(l => l.trim());
49
+ let errorCount = 0;
50
+ for (const line of lines) {
51
+ try {
52
+ const obj = JSON.parse(line);
53
+ // Check for ERROR level or validation failure messages
54
+ if (obj.level === 'ERROR' ||
55
+ (obj.message && (
56
+ obj.message.toLowerCase().includes('error') ||
57
+ obj.message.toLowerCase().includes('validation failed') ||
58
+ obj.message.toLowerCase().includes('failed')
59
+ ))) {
60
+ errorCount++;
61
+ }
62
+ } catch (e) {
63
+ // Non-JSON lines might be error messages
64
+ if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
65
+ errorCount++;
66
+ }
67
+ }
68
+ }
69
+ console.log(errorCount);
70
+ } catch (e) {
71
+ console.log('0');
72
+ }
73
+ " || echo "0")
74
+
75
+ if [ "$ERRORS" -gt 0 ] || [ "${{ steps.validate.outputs.validation_failed }}" == "true" ]; then
76
+ echo "❌ Validation failed"
77
+ exit 1
78
+ else
79
+ echo "✅ Validation passed"
80
+ fi
81
+ else
82
+ echo "⚠️ No validation output found"
83
+ exit 1
84
+ fi
85
+
86
+ - name: Upload validation results
87
+ if: always()
88
+ uses: actions/upload-artifact@v3
89
+ with:
90
+ name: validation-results
91
+ path: validation.json
92
+ if-no-files-found: ignore
93
+