@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
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # levit-kit
2
2
 
3
- Hybrid starter kit for **Antigravity** projects
3
+ Hybrid starter kit for **AI-Driven Development (AIDD)** projects
4
4
  (template + CLI + agentic conventions)
5
5
 
6
6
  ---
@@ -22,7 +22,7 @@ Hybrid starter kit for **Antigravity** projects
22
22
 
23
23
  ## Why levit-kit exists
24
24
 
25
- Levit-kit provides a **clean, readable, and predictable starting point** for initiating projects with Google Antigravity.
25
+ Levit-kit provides a **clean, readable, and predictable starting point** for initiating projects with **AI-Driven Development (AIDD)**.
26
26
 
27
27
  It does not attempt to automate development, orchestrate agents, or generate business logic.
28
28
 
@@ -34,7 +34,8 @@ Its role is simple:
34
34
  ## What levit-kit does
35
35
 
36
36
  - Initializes a standardized project structure
37
- - Installs explicit conventions
37
+ - Installs explicit conventions for **AI-Driven Development (AIDD)**
38
+ - Provides a protocol for Human-AI collaboration (Intents, Decisions, Evals)
38
39
  - Facilitates human and agent onboarding
39
40
  - Reduces unnecessary variability between projects
40
41
 
@@ -57,25 +58,209 @@ These limits are intentional.
57
58
  Levit-kit is used **once**, at the very beginning of a project.
58
59
 
59
60
  ```bash
61
+ # Default template (generic)
62
+ npx @buba_71/levit init my-project
63
+
64
+ # Symfony template
65
+ npx @buba_71/levit init my-project symfony
66
+
67
+ # Interactive template selection (if multiple templates available)
60
68
  npx @buba_71/levit init my-project
61
69
  ```
62
70
 
63
71
  ### Commands & Options
64
72
 
65
- - `init <name>`: Initializes a new project in the specified directory.
73
+ - `init <name> [template]`: Initializes a new project in the specified directory. Optionally specify a template (default, symfony).
74
+ - `feature new`: Creates a new feature intent (auto-assigns ID).
75
+ - `feature list`: Lists all features with their status.
76
+ - `feature status <id> <status>`: Updates a feature's status.
77
+ - `decision new`: Creates a new Architecture Decision Record (ADR) (auto-assigns ID).
78
+ - `handoff new`: Creates a workspace handoff brief for an agent.
79
+ - `validate`: Validates project structure and cognitive scaffolding.
80
+ - `--json`: Outputs machine-readable JSON (works with all commands).
66
81
  - `-v, --version`: Displays the current version.
67
82
  - `-h, --help`: Displays the help message.
68
83
 
84
+ ### Available Templates
85
+
86
+ - **default**: Generic template for any type of project (structure only)
87
+ - **symfony**: Template with Symfony-specific guidelines in `.levit/AGENT_ONBOARDING.md` (structure only)
88
+
89
+ > **Important**: Templates provide **structure and governance**, not business logic or framework files.
90
+ > For Symfony projects, create your Symfony application separately (e.g., `composer create-project symfony/skeleton`)
91
+ > and integrate it with the Levit-Kit structure.
92
+
69
93
  ### What happens during init?
70
94
 
71
95
  The `init` command:
72
96
  1. Creates a new project directory.
73
- 2. Copies the **default levit-kit template**.
74
- 3. Includes a base `.gitignore` and `package.json`.
75
- 4. Exits immediately.
97
+ 2. Copies the **levit-kit template** (structure only, no business logic).
98
+ 3. Generates a **`levit.json` manifest** with project metadata, governance rules, discovered roles, and constraints.
99
+ 4. Includes a base `.gitignore`.
100
+ 5. Exits immediately.
101
+
102
+ > **Note**: Templates provide only the **AIDD governance structure** (`.levit/` directory with all governance),
103
+ > not project-specific configuration files. You should create your project files (package.json, composer.json, etc.)
104
+ > separately according to your needs.
105
+
106
+ The `levit.json` file serves as the **central machine-readable contract** for AI agents, containing:
107
+ - Project name and description
108
+ - Governance settings (autonomy level, risk tolerance)
109
+ - Active features and available roles
110
+ - Technical constraints (file size limits, allowed dependencies, forbidden patterns)
111
+ - Paths to key directories
76
112
 
77
113
  Levit-kit does not remain in the project after initialization and installs no dependencies.
78
114
 
115
+
116
+ ---
117
+
118
+ ## The AIDD Workflow (Human + Agent)
119
+
120
+ Levit-kit installs a cognitive pipeline in your project:
121
+
122
+ 1. **Human Intent**: You define *what* you want using `levit feature new`.
123
+ 2. **Agent Onboarding**: Your AI reads `.levit/AGENT_ONBOARDING.md` to learn your rules.
124
+ 3. **Collaborative Decision**: The agent or human proposes technical choices using `levit decision new` (stored in `.levit/decisions/`).
125
+ 4. **Handoff**: You package the task for an agent using `levit handoff new`.
126
+ 5. **Verification**: You or the agent run quality tests in `.levit/evals/`.
127
+ 6. **Review**: The agent submits its work following the protocol in `.levit/workflows/`.
128
+
129
+ > **📖 New to managing AI agents?** After initializing a project, read `HUMAN_AGENT_MANAGER.md` for a comprehensive guide on effectively managing AI agents, including best practices, anti-patterns, and troubleshooting.
130
+ >
131
+ > **🔄 Migrating an existing project?** See `MIGRATION_GUIDE.md` for step-by-step instructions on adopting levit-kit in your existing codebase.
132
+ >
133
+ > **🚀 Setting up CI/CD?** Templates for GitHub Actions and GitLab CI are included in all projects for automatic validation.
134
+
135
+ ---
136
+
137
+ ## Examples
138
+
139
+ ### Example 1: Creating a New Feature
140
+
141
+ ```bash
142
+ # Interactive mode (prompts for input)
143
+ $ levit feature new
144
+ Feature title: User Authentication
145
+ Feature slug [user-authentication]:
146
+ Created .levit/features/001-user-authentication.md
147
+
148
+ # Non-interactive mode (with flags)
149
+ $ levit feature new --title "User Authentication" --slug user-authentication --yes
150
+ Created .levit/features/001-user-authentication.md
151
+ ```
152
+
153
+ The created feature file includes:
154
+ - Frontmatter with metadata (id, status, owner, risk_level, etc.)
155
+ - Template sections: Vision, Success Criteria, Boundaries, Technical Constraints, Agent Task
156
+
157
+ ### Example 2: Creating an Architecture Decision
158
+
159
+ ```bash
160
+ # Link a decision to a feature
161
+ $ levit decision new --title "Use PostgreSQL for user data" --feature .levit/features/001-user-authentication.md --yes
162
+ Created .levit/decisions/ADR-001-use-postgresql-for-user-data.md
163
+
164
+ # Auto-assign ID
165
+ $ levit decision new --title "Implement JWT authentication" --yes
166
+ Created .levit/decisions/ADR-002-implement-jwt-authentication.md
167
+ ```
168
+
169
+ ### Example 3: Creating an Agent Handoff
170
+
171
+ ```bash
172
+ # Handoff a feature to a developer agent
173
+ $ levit handoff new --feature .levit/features/001-user-authentication.md --role developer --yes
174
+ Created .levit/handoff/2026-01-01-001-user-authentication-developer.md
175
+
176
+ # Handoff to security reviewer
177
+ $ levit handoff new --feature .levit/features/001-user-authentication.md --role security --yes
178
+ Created .levit/handoff/2026-01-01-001-user-authentication-security.md
179
+ ```
180
+
181
+ ### Example 4: Validating Project Structure
182
+
183
+ ```bash
184
+ # Human-readable output
185
+ $ levit validate
186
+ 🔍 Validating project cognitive scaffolding...
187
+ ✨ All cognitive scaffolding checks passed!
188
+
189
+ # JSON output (for automation)
190
+ $ levit validate --json
191
+ {"level":"INFO","message":"🔍 Validating project cognitive scaffolding...","timestamp":"2026-01-01T12:00:00.000Z"}
192
+ {"level":"INFO","message":"✨ All cognitive scaffolding checks passed!","timestamp":"2026-01-01T12:00:00.000Z"}
193
+ ```
194
+
195
+ ### Example 5: Complete Workflow
196
+
197
+ Here's a complete workflow from feature creation to agent handoff:
198
+
199
+ ```bash
200
+ # 1. Initialize project
201
+ $ npx @buba_71/levit init my-api-project
202
+ $ cd my-api-project
203
+
204
+ # 2. Create a feature
205
+ $ levit feature new --title "API Rate Limiting" --slug api-rate-limiting --yes
206
+ Created .levit/features/001-api-rate-limiting.md
207
+
208
+ # 3. Create a technical decision
209
+ $ levit decision new --title "Use Redis for rate limiting" --feature .levit/features/001-api-rate-limiting.md --yes
210
+ Created .levit/decisions/ADR-001-use-redis-for-rate-limiting.md
211
+
212
+ # 4. Handoff to developer
213
+ $ levit handoff new --feature .levit/features/001-api-rate-limiting.md --role developer --yes
214
+ Created .levit/handoff/2026-01-01-001-api-rate-limiting-developer.md
215
+
216
+ # 5. Agent reads handoff and implements
217
+ # (Agent reads .levit/handoff/2026-01-01-001-api-rate-limiting-developer.md)
218
+
219
+ # 6. Validate after implementation
220
+ $ levit validate
221
+ 🔍 Validating project cognitive scaffolding...
222
+ ✨ All cognitive scaffolding checks passed!
223
+ ```
224
+
225
+ ### Example 6: Working with the Manifest
226
+
227
+ The `levit.json` manifest is automatically synced when you create features or decisions. You can also manually inspect it:
228
+
229
+ ```json
230
+ {
231
+ "version": "1.0.0",
232
+ "project": {
233
+ "name": "my-api-project",
234
+ "description": "AI-Driven Development project powered by levit-kit"
235
+ },
236
+ "governance": {
237
+ "autonomy_level": "low",
238
+ "risk_tolerance": "low"
239
+ },
240
+ "features": [
241
+ {
242
+ "id": "001",
243
+ "slug": "api-rate-limiting",
244
+ "status": "active",
245
+ "title": "API Rate Limiting",
246
+ "path": ".levit/features/001-api-rate-limiting.md"
247
+ }
248
+ ],
249
+ "roles": [
250
+ {
251
+ "name": "developer",
252
+ "description": "Developer Role",
253
+ "path": "roles/developer.md"
254
+ }
255
+ ],
256
+ "constraints": {
257
+ "max_file_size": 1000000,
258
+ "allowed_dependencies": [],
259
+ "forbidden_patterns": []
260
+ }
261
+ }
262
+ ```
263
+
79
264
  ---
80
265
 
81
266
  ## Where does the levit command come from?
package/dist/bin/cli.js CHANGED
@@ -5,22 +5,37 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const init_1 = require("../src/commands/init");
8
+ const decision_1 = require("../src/commands/decision");
9
+ const feature_1 = require("../src/commands/feature");
10
+ const handoff_1 = require("../src/commands/handoff");
11
+ const validate_1 = require("../src/commands/validate");
8
12
  const version_1 = require("../src/core/version");
9
13
  const node_path_1 = __importDefault(require("node:path"));
14
+ const logger_1 = require("../src/core/logger");
15
+ const errors_1 = require("../src/core/errors");
16
+ const error_helper_1 = require("../src/core/error_helper");
10
17
  function showHelp() {
11
- console.log(`
18
+ logger_1.Logger.info(`
12
19
  Usage: levit [command] [options]
13
20
 
14
21
  Commands:
15
22
  init <project-name> Initialize a new levit project
23
+ feature new Create a new feature intent file (wizard)
24
+ decision new Create a new decision record (wizard)
25
+ handoff new Create an agent handoff brief (wizard)
26
+ validate Validate project cognitive scaffolding (cognitive linter)
16
27
 
17
28
  Options:
18
29
  -v, --version Show version number
19
30
  -h, --help Show help
31
+ --json Output in JSON format
20
32
  `);
21
33
  }
22
- function main() {
34
+ async function main() {
23
35
  const args = process.argv.slice(2);
36
+ if (args.includes("--json")) {
37
+ logger_1.Logger.setJsonMode(true);
38
+ }
24
39
  if (args.includes("-h") || args.includes("--help") || args.length === 0) {
25
40
  showHelp();
26
41
  process.exit(0);
@@ -29,31 +44,59 @@ function main() {
29
44
  console.log(`levit-kit v${(0, version_1.getVersion)()}`);
30
45
  process.exit(0);
31
46
  }
32
- if (args[0] === "init") {
33
- const projectName = args[1];
34
- if (!projectName) {
35
- console.error("Error: Project name is required.");
36
- console.error("Usage: levit init <project-name>");
37
- process.exit(1);
38
- }
39
- // Basic validation: ensure it's a valid directory name
40
- if (!/^[a-z0-9-_]+$/i.test(projectName)) {
41
- console.error("Error: Invalid project name. Use only letters, numbers, dashes, and underscores.");
42
- process.exit(1);
47
+ const command = args[0];
48
+ try {
49
+ switch (command) {
50
+ case "init":
51
+ const projectName = args[1];
52
+ const templateName = args[2] || undefined;
53
+ if (!projectName) {
54
+ throw new errors_1.LevitError(errors_1.LevitErrorCode.MISSING_REQUIRED_ARG, "Project name is required. Usage: levit init <project-name> [template]");
55
+ }
56
+ await (0, init_1.initProject)(projectName, node_path_1.default.resolve(process.cwd(), projectName), templateName);
57
+ break;
58
+ case "feature":
59
+ await (0, feature_1.featureCommand)(args.slice(1), process.cwd());
60
+ break;
61
+ case "decision":
62
+ await (0, decision_1.decisionCommand)(args.slice(1), process.cwd());
63
+ break;
64
+ case "handoff":
65
+ await (0, handoff_1.handoffCommand)(args.slice(1), process.cwd());
66
+ break;
67
+ case "validate":
68
+ await (0, validate_1.validateCommand)(args.slice(1), process.cwd());
69
+ break;
70
+ default:
71
+ (0, error_helper_1.displayError)(new errors_1.LevitError(errors_1.LevitErrorCode.INVALID_COMMAND, `Unknown command "${command}"`));
72
+ logger_1.Logger.info("");
73
+ showHelp();
74
+ process.exit(1);
43
75
  }
44
- const targetPath = node_path_1.default.resolve(process.cwd(), projectName);
45
- try {
46
- (0, init_1.initProject)(projectName, targetPath);
76
+ }
77
+ catch (error) {
78
+ if (error instanceof errors_1.LevitError) {
79
+ (0, error_helper_1.displayError)(error);
47
80
  }
48
- catch (error) {
49
- console.error(error instanceof Error ? `Error: ${error.message}` : "Unexpected error");
50
- process.exit(1);
81
+ else {
82
+ const isJsonMode = logger_1.Logger.getJsonMode();
83
+ if (isJsonMode) {
84
+ logger_1.Logger.error(JSON.stringify({
85
+ error: {
86
+ code: "UNEXPECTED_ERROR",
87
+ message: error instanceof Error ? error.message : "Unexpected error",
88
+ details: error instanceof Error ? error.stack : String(error),
89
+ },
90
+ }));
91
+ }
92
+ else {
93
+ logger_1.Logger.error(error instanceof Error ? error.message : "Unexpected error");
94
+ if (error instanceof Error && error.stack) {
95
+ logger_1.Logger.debug(error.stack);
96
+ }
97
+ }
51
98
  }
52
- }
53
- else {
54
- console.error(`Error: Unknown command "${args[0]}"`);
55
- showHelp();
56
99
  process.exit(1);
57
100
  }
58
101
  }
59
- main();
102
+ void main();
@@ -0,0 +1,74 @@
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
+ exports.decisionCommand = decisionCommand;
7
+ const promises_1 = __importDefault(require("node:readline/promises"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const levit_project_1 = require("../core/levit_project");
10
+ const cli_args_1 = require("../core/cli_args");
11
+ const logger_1 = require("../core/logger");
12
+ const decision_service_1 = require("../services/decision_service");
13
+ const errors_1 = require("../core/errors");
14
+ const table_1 = require("../core/table");
15
+ const ids_1 = require("../core/ids");
16
+ function normalizeSlug(input) {
17
+ return input
18
+ .trim()
19
+ .toLowerCase()
20
+ .replace(/[^a-z0-9\s-_]/g, "")
21
+ .replace(/\s+/g, "-");
22
+ }
23
+ async function decisionCommand(argv, cwd) {
24
+ const { positional, flags } = (0, cli_args_1.parseArgs)(argv);
25
+ const sub = positional[0];
26
+ if (sub !== "new") {
27
+ throw new errors_1.LevitError(errors_1.LevitErrorCode.INVALID_COMMAND, 'Usage: levit decision new [--title "..."] [--id "001"]');
28
+ }
29
+ const projectRoot = (0, levit_project_1.requireLevitProjectRoot)(cwd);
30
+ const yes = (0, cli_args_1.getBooleanFlag)(flags, "yes");
31
+ const overwrite = (0, cli_args_1.getBooleanFlag)(flags, "force");
32
+ const isJsonMode = logger_1.Logger.getJsonMode();
33
+ let title = (0, cli_args_1.getStringFlag)(flags, "title");
34
+ let id = (0, cli_args_1.getStringFlag)(flags, "id");
35
+ const featureRef = (0, cli_args_1.getStringFlag)(flags, "feature");
36
+ if (!yes) {
37
+ const rl = promises_1.default.createInterface({ input: process.stdin, output: process.stdout });
38
+ if (!title) {
39
+ title = (await rl.question("Decision title: ")).trim();
40
+ }
41
+ await rl.close();
42
+ }
43
+ if (!title) {
44
+ throw new errors_1.LevitError(errors_1.LevitErrorCode.MISSING_REQUIRED_ARG, "Missing --title");
45
+ }
46
+ // Generate ID if not provided
47
+ if (!id) {
48
+ const baseDir = node_path_1.default.join(projectRoot, ".levit", "decisions");
49
+ id = (0, ids_1.nextSequentialId)(baseDir, /^ADR-(\d+)-/);
50
+ }
51
+ // Preview before creation
52
+ if (!yes && !isJsonMode) {
53
+ const slug = normalizeSlug(title);
54
+ const decisionPath = node_path_1.default.relative(projectRoot, node_path_1.default.join(projectRoot, ".levit", "decisions", `ADR-${id}-${slug}.md`));
55
+ const preview = (0, table_1.createBox)("Decision Preview", {
56
+ "ID": `ADR-${id}`,
57
+ "Title": title,
58
+ "Feature": featureRef || "(none)",
59
+ "Path": decisionPath
60
+ });
61
+ logger_1.Logger.info("");
62
+ logger_1.Logger.info(preview);
63
+ logger_1.Logger.info("");
64
+ const rl = promises_1.default.createInterface({ input: process.stdin, output: process.stdout });
65
+ const confirm = await rl.question("Create this decision? [y/N]: ");
66
+ await rl.close();
67
+ if (confirm.toLowerCase() !== "y" && confirm.toLowerCase() !== "yes") {
68
+ logger_1.Logger.info("Cancelled.");
69
+ return;
70
+ }
71
+ }
72
+ const createdPath = decision_service_1.DecisionService.createDecision(projectRoot, { title, featureRef, id, overwrite });
73
+ logger_1.Logger.success(`Created ${createdPath}`);
74
+ }
@@ -0,0 +1,159 @@
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
+ exports.featureCommand = featureCommand;
7
+ const promises_1 = __importDefault(require("node:readline/promises"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const levit_project_1 = require("../core/levit_project");
11
+ const cli_args_1 = require("../core/cli_args");
12
+ const logger_1 = require("../core/logger");
13
+ const feature_service_1 = require("../services/feature_service");
14
+ const errors_1 = require("../core/errors");
15
+ const table_1 = require("../core/table");
16
+ const ids_1 = require("../core/ids");
17
+ function normalizeSlug(input) {
18
+ return input
19
+ .trim()
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9\s-_]/g, "")
22
+ .replace(/\s+/g, "-");
23
+ }
24
+ async function featureCommand(argv, cwd) {
25
+ const { positional, flags } = (0, cli_args_1.parseArgs)(argv);
26
+ const sub = positional[0];
27
+ const projectRoot = (0, levit_project_1.requireLevitProjectRoot)(cwd);
28
+ switch (sub) {
29
+ case "new":
30
+ await handleFeatureNew(projectRoot, flags);
31
+ break;
32
+ case "list":
33
+ handleFeatureList(projectRoot, flags);
34
+ break;
35
+ case "status":
36
+ await handleFeatureStatus(projectRoot, positional.slice(1), flags);
37
+ break;
38
+ default:
39
+ throw new errors_1.LevitError(errors_1.LevitErrorCode.INVALID_COMMAND, 'Usage: levit feature <new|list|status> [options]\n' +
40
+ ' new: Create a new feature\n' +
41
+ ' list: List all features\n' +
42
+ ' status: Update feature status (usage: levit feature status <id> <status>)');
43
+ }
44
+ }
45
+ async function handleFeatureNew(projectRoot, flags) {
46
+ const yes = (0, cli_args_1.getBooleanFlag)(flags, "yes");
47
+ const overwrite = (0, cli_args_1.getBooleanFlag)(flags, "force");
48
+ const isJsonMode = logger_1.Logger.getJsonMode();
49
+ let title = (0, cli_args_1.getStringFlag)(flags, "title");
50
+ let slug = (0, cli_args_1.getStringFlag)(flags, "slug");
51
+ let id = (0, cli_args_1.getStringFlag)(flags, "id");
52
+ if (!yes) {
53
+ const rl = promises_1.default.createInterface({ input: process.stdin, output: process.stdout });
54
+ if (!title) {
55
+ title = (await rl.question("Feature title: ")).trim();
56
+ }
57
+ if (!slug) {
58
+ const computed = normalizeSlug(title || "");
59
+ slug = (await rl.question(`Feature slug [${computed}]: `)).trim() || computed;
60
+ }
61
+ await rl.close();
62
+ }
63
+ if (!title) {
64
+ throw new errors_1.LevitError(errors_1.LevitErrorCode.MISSING_REQUIRED_ARG, "Missing --title");
65
+ }
66
+ if (!slug) {
67
+ throw new errors_1.LevitError(errors_1.LevitErrorCode.MISSING_REQUIRED_ARG, "Missing --slug");
68
+ }
69
+ // Generate ID if not provided
70
+ if (!id) {
71
+ const baseDir = node_path_1.default.join(projectRoot, ".levit", "features");
72
+ id = (0, ids_1.nextSequentialId)(baseDir, /^(\d+)-/);
73
+ }
74
+ // Preview before creation
75
+ if (!yes && !isJsonMode) {
76
+ const featurePath = node_path_1.default.relative(projectRoot, node_path_1.default.join(projectRoot, ".levit", "features", `${id}-${slug}.md`));
77
+ const preview = (0, table_1.createBox)("Feature Preview", {
78
+ "ID": id,
79
+ "Title": title,
80
+ "Slug": slug,
81
+ "Path": featurePath
82
+ });
83
+ logger_1.Logger.info("");
84
+ logger_1.Logger.info(preview);
85
+ logger_1.Logger.info("");
86
+ const rl = promises_1.default.createInterface({ input: process.stdin, output: process.stdout });
87
+ const confirm = await rl.question("Create this feature? [y/N]: ");
88
+ await rl.close();
89
+ if (confirm.toLowerCase() !== "y" && confirm.toLowerCase() !== "yes") {
90
+ logger_1.Logger.info("Cancelled.");
91
+ return;
92
+ }
93
+ }
94
+ const createdPath = feature_service_1.FeatureService.createFeature(projectRoot, { title, slug, id, overwrite });
95
+ logger_1.Logger.success(`Created ${createdPath}`);
96
+ }
97
+ function handleFeatureList(projectRoot, flags) {
98
+ const features = feature_service_1.FeatureService.listFeatures(projectRoot);
99
+ const isJsonMode = logger_1.Logger.getJsonMode();
100
+ if (features.length === 0) {
101
+ logger_1.Logger.info("No features found.");
102
+ return;
103
+ }
104
+ // Create formatted table
105
+ const table = (0, table_1.createTable)(["ID", "Status", "Title"]);
106
+ for (const feature of features) {
107
+ // Color status based on value
108
+ let statusDisplay = feature.status;
109
+ switch (feature.status) {
110
+ case "active":
111
+ statusDisplay = isJsonMode ? "active" : chalk_1.default.green("● active");
112
+ break;
113
+ case "draft":
114
+ statusDisplay = isJsonMode ? "draft" : chalk_1.default.yellow("○ draft");
115
+ break;
116
+ case "deprecated":
117
+ statusDisplay = isJsonMode ? "deprecated" : chalk_1.default.red("× deprecated");
118
+ break;
119
+ case "completed":
120
+ statusDisplay = isJsonMode ? "completed" : chalk_1.default.cyan("✓ completed");
121
+ break;
122
+ }
123
+ table.push([
124
+ feature.id,
125
+ statusDisplay,
126
+ feature.title
127
+ ]);
128
+ }
129
+ if (!isJsonMode) {
130
+ logger_1.Logger.info("");
131
+ logger_1.Logger.info(chalk_1.default.bold("Features:"));
132
+ }
133
+ (0, table_1.renderTable)(table, isJsonMode);
134
+ if (!isJsonMode) {
135
+ logger_1.Logger.info(`\nTotal: ${chalk_1.default.bold(features.length.toString())} feature(s)`);
136
+ logger_1.Logger.info("");
137
+ }
138
+ else {
139
+ logger_1.Logger.info(JSON.stringify({ total: features.length }));
140
+ }
141
+ }
142
+ async function handleFeatureStatus(projectRoot, args, flags) {
143
+ const featureId = args[0] || (0, cli_args_1.getStringFlag)(flags, "id");
144
+ let newStatus = args[1] || (0, cli_args_1.getStringFlag)(flags, "status");
145
+ if (!featureId) {
146
+ throw new errors_1.LevitError(errors_1.LevitErrorCode.MISSING_REQUIRED_ARG, "Missing feature ID. Usage: levit feature status <id> <status>");
147
+ }
148
+ const validStatuses = ['active', 'draft', 'deprecated', 'completed'];
149
+ if (!newStatus) {
150
+ const rl = promises_1.default.createInterface({ input: process.stdin, output: process.stdout });
151
+ newStatus = (await rl.question(`New status [active|draft|deprecated|completed]: `)).trim();
152
+ await rl.close();
153
+ }
154
+ if (!validStatuses.includes(newStatus)) {
155
+ throw new errors_1.LevitError(errors_1.LevitErrorCode.VALIDATION_FAILED, `Invalid status "${newStatus}". Must be one of: ${validStatuses.join(", ")}`);
156
+ }
157
+ feature_service_1.FeatureService.updateFeatureStatus(projectRoot, featureId, newStatus);
158
+ logger_1.Logger.info(`Updated feature ${featureId} status to "${newStatus}"`);
159
+ }
@@ -0,0 +1,71 @@
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
+ exports.handoffCommand = handoffCommand;
7
+ const promises_1 = __importDefault(require("node:readline/promises"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const levit_project_1 = require("../core/levit_project");
10
+ const cli_args_1 = require("../core/cli_args");
11
+ const logger_1 = require("../core/logger");
12
+ const handoff_service_1 = require("../services/handoff_service");
13
+ const errors_1 = require("../core/errors");
14
+ const table_1 = require("../core/table");
15
+ function isoDate() {
16
+ return new Date().toISOString().slice(0, 10);
17
+ }
18
+ async function handoffCommand(argv, cwd) {
19
+ const { positional, flags } = (0, cli_args_1.parseArgs)(argv);
20
+ const sub = positional[0];
21
+ if (sub !== "new") {
22
+ throw new errors_1.LevitError(errors_1.LevitErrorCode.INVALID_COMMAND, "Usage: levit handoff new --feature <.levit/features/001-...md> [--role developer|qa|security|devops]");
23
+ }
24
+ const projectRoot = (0, levit_project_1.requireLevitProjectRoot)(cwd);
25
+ const yes = (0, cli_args_1.getBooleanFlag)(flags, "yes");
26
+ const overwrite = (0, cli_args_1.getBooleanFlag)(flags, "force");
27
+ const isJsonMode = logger_1.Logger.getJsonMode();
28
+ let feature = (0, cli_args_1.getStringFlag)(flags, "feature");
29
+ let role = (0, cli_args_1.getStringFlag)(flags, "role");
30
+ if (!yes) {
31
+ const rl = promises_1.default.createInterface({ input: process.stdin, output: process.stdout });
32
+ if (!feature) {
33
+ feature = (await rl.question("Feature path (e.g. .levit/features/001-...md): ")).trim();
34
+ }
35
+ if (!role) {
36
+ role = (await rl.question("Role [developer]: ")).trim() || "developer";
37
+ }
38
+ await rl.close();
39
+ }
40
+ if (!feature) {
41
+ throw new errors_1.LevitError(errors_1.LevitErrorCode.MISSING_REQUIRED_ARG, "Missing --feature");
42
+ }
43
+ if (!role) {
44
+ role = "developer";
45
+ }
46
+ // Preview before creation
47
+ if (!yes && !isJsonMode) {
48
+ const safeRole = role.trim().toLowerCase();
49
+ const date = isoDate();
50
+ const fileName = `${date}-${node_path_1.default.basename(feature, node_path_1.default.extname(feature))}-${safeRole}.md`;
51
+ const handoffPath = node_path_1.default.relative(projectRoot, node_path_1.default.join(projectRoot, ".levit", "handoff", fileName));
52
+ const preview = (0, table_1.createBox)("Handoff Preview", {
53
+ "Date": date,
54
+ "Feature": feature,
55
+ "Role": safeRole,
56
+ "Path": handoffPath
57
+ });
58
+ logger_1.Logger.info("");
59
+ logger_1.Logger.info(preview);
60
+ logger_1.Logger.info("");
61
+ const rl = promises_1.default.createInterface({ input: process.stdin, output: process.stdout });
62
+ const confirm = await rl.question("Create this handoff? [y/N]: ");
63
+ await rl.close();
64
+ if (confirm.toLowerCase() !== "y" && confirm.toLowerCase() !== "yes") {
65
+ logger_1.Logger.info("Cancelled.");
66
+ return;
67
+ }
68
+ }
69
+ const createdPath = handoff_service_1.HandoffService.createHandoff(projectRoot, { feature, role, overwrite });
70
+ logger_1.Logger.success(`Created ${createdPath}`);
71
+ }