@buba_71/levit 0.4.0 → 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 (74) hide show
  1. package/README.md +182 -10
  2. package/dist/bin/cli.js +58 -48
  3. package/dist/src/commands/decision.js +34 -19
  4. package/dist/src/commands/feature.js +117 -19
  5. package/dist/src/commands/handoff.js +34 -12
  6. package/dist/src/commands/index.js +3 -1
  7. package/dist/src/commands/init.js +53 -39
  8. package/dist/src/commands/validate.js +84 -0
  9. package/dist/src/core/error_helper.js +93 -0
  10. package/dist/src/core/errors.js +25 -0
  11. package/dist/src/core/frontmatter.js +62 -0
  12. package/dist/src/core/logger.js +77 -0
  13. package/dist/src/core/table.js +63 -0
  14. package/dist/src/init.js +9 -0
  15. package/dist/src/readers/decision_reader.js +47 -0
  16. package/dist/src/readers/feature_reader.js +48 -0
  17. package/dist/src/readers/handoff_reader.js +47 -0
  18. package/dist/src/services/decision_service.js +49 -0
  19. package/dist/src/services/feature_service.js +89 -0
  20. package/dist/src/services/handoff_service.js +37 -0
  21. package/dist/src/services/manifest_service.js +89 -0
  22. package/dist/src/services/project_service.js +39 -0
  23. package/dist/src/services/validation_service.js +461 -0
  24. package/dist/src/types/domain.js +2 -0
  25. package/dist/src/types/index.js +2 -0
  26. package/dist/src/types/manifest.js +26 -0
  27. package/dist/tests/{init.test.js → cli/integration.test.js} +79 -39
  28. package/dist/tests/services/decision_service.test.js +27 -0
  29. package/dist/tests/services/feature_service.test.js +42 -0
  30. package/dist/tests/services/handoff_service.test.js +28 -0
  31. package/dist/tests/services/manifest_service.test.js +189 -0
  32. package/dist/tests/services/validation_service.test.js +196 -0
  33. package/package.json +7 -2
  34. package/templates/default/.github/workflows/README.md +56 -0
  35. package/templates/default/.github/workflows/levit-validate.yml +93 -0
  36. package/templates/default/.gitlab-ci.yml +73 -0
  37. package/templates/default/.levit/AGENT_CONTRACT.md +34 -0
  38. package/templates/default/.levit/AGENT_ONBOARDING.md +5 -4
  39. package/templates/default/.levit/decisions/.gitkeep +0 -0
  40. package/templates/default/{features → .levit/features}/INTENT.md +9 -0
  41. package/templates/default/{features → .levit/features}/README.md +2 -2
  42. package/templates/default/.levit/handoff/.gitkeep +0 -0
  43. package/templates/default/HUMAN_AGENT_MANAGER.md +654 -0
  44. package/templates/default/MIGRATION_GUIDE.md +597 -0
  45. package/templates/default/README.md +26 -7
  46. package/templates/symfony/.github/workflows/README.md +56 -0
  47. package/templates/symfony/.github/workflows/levit-validate.yml +82 -0
  48. package/templates/symfony/.gitlab-ci.yml +62 -0
  49. package/templates/symfony/.levit/AGENT_CONTRACT.md +34 -0
  50. package/templates/symfony/.levit/AGENT_ONBOARDING.md +124 -0
  51. package/templates/symfony/.levit/decisions/.gitkeep +0 -0
  52. package/templates/symfony/.levit/features/INTENT.md +32 -0
  53. package/templates/symfony/.levit/features/README.md +11 -0
  54. package/templates/symfony/.levit/handoff/.gitkeep +0 -0
  55. package/templates/symfony/.levit/prompts/global-rules.md +10 -0
  56. package/templates/symfony/.levit/prompts/refactoring-guidelines.md +9 -0
  57. package/templates/symfony/.levit/workflows/example-task.md +9 -0
  58. package/templates/symfony/.levit/workflows/submit-for-review.md +18 -0
  59. package/templates/symfony/HUMAN_AGENT_MANAGER.md +654 -0
  60. package/templates/symfony/MIGRATION_GUIDE.md +597 -0
  61. package/templates/symfony/README.md +101 -0
  62. package/templates/symfony/SOCIAL_CONTRACT.md +34 -0
  63. package/templates/default/.levit/decision-record.md +0 -19
  64. package/templates/default/package.json +0 -11
  65. /package/templates/default/{agents → .levit/agents}/AGENTS.md +0 -0
  66. /package/templates/default/{agents → .levit/agents}/boundaries.md +0 -0
  67. /package/templates/default/{docs → .levit/docs}/architecture.md +0 -0
  68. /package/templates/default/{evals → .levit/evals}/README.md +0 -0
  69. /package/templates/default/{evals → .levit/evals}/conformance.eval.ts +0 -0
  70. /package/templates/default/{pipelines → .levit/pipelines}/README.md +0 -0
  71. /package/templates/default/{roles → .levit/roles}/README.md +0 -0
  72. /package/templates/default/{roles → .levit/roles}/devops.md +0 -0
  73. /package/templates/default/{roles → .levit/roles}/qa.md +0 -0
  74. /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
 
@@ -58,36 +58,208 @@ These limits are intentional.
58
58
  Levit-kit is used **once**, at the very beginning of a project.
59
59
 
60
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)
61
68
  npx @buba_71/levit init my-project
62
69
  ```
63
70
 
64
71
  ### Commands & Options
65
72
 
66
- - `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).
67
81
  - `-v, --version`: Displays the current version.
68
82
  - `-h, --help`: Displays the help message.
69
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
+
70
93
  ### What happens during init?
71
94
 
72
95
  The `init` command:
73
96
  1. Creates a new project directory.
74
- 2. Copies the **default levit-kit template**.
75
- 3. Includes a base `.gitignore` and `package.json`.
76
- 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
77
112
 
78
113
  Levit-kit does not remain in the project after initialization and installs no dependencies.
79
114
 
115
+
80
116
  ---
81
117
 
82
118
  ## The AIDD Workflow (Human + Agent)
83
119
 
84
120
  Levit-kit installs a cognitive pipeline in your project:
85
121
 
86
- 1. **Human Intent**: You define *what* you want in `features/INTENT.md`.
122
+ 1. **Human Intent**: You define *what* you want using `levit feature new`.
87
123
  2. **Agent Onboarding**: Your AI reads `.levit/AGENT_ONBOARDING.md` to learn your rules.
88
- 3. **Collaborative Decision**: The agent proposes technical choices in `.levit/decision-record.md`.
89
- 4. **Verification**: You or the agent run quality tests in `evals/`.
90
- 5. **Review**: The agent submits its work following a strict protocol.
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
+ ```
91
263
 
92
264
  ---
93
265
 
package/dist/bin/cli.js CHANGED
@@ -8,10 +8,14 @@ const init_1 = require("../src/commands/init");
8
8
  const decision_1 = require("../src/commands/decision");
9
9
  const feature_1 = require("../src/commands/feature");
10
10
  const handoff_1 = require("../src/commands/handoff");
11
+ const validate_1 = require("../src/commands/validate");
11
12
  const version_1 = require("../src/core/version");
12
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");
13
17
  function showHelp() {
14
- console.log(`
18
+ logger_1.Logger.info(`
15
19
  Usage: levit [command] [options]
16
20
 
17
21
  Commands:
@@ -19,14 +23,19 @@ Commands:
19
23
  feature new Create a new feature intent file (wizard)
20
24
  decision new Create a new decision record (wizard)
21
25
  handoff new Create an agent handoff brief (wizard)
26
+ validate Validate project cognitive scaffolding (cognitive linter)
22
27
 
23
28
  Options:
24
29
  -v, --version Show version number
25
30
  -h, --help Show help
31
+ --json Output in JSON format
26
32
  `);
27
33
  }
28
34
  async function main() {
29
35
  const args = process.argv.slice(2);
36
+ if (args.includes("--json")) {
37
+ logger_1.Logger.setJsonMode(true);
38
+ }
30
39
  if (args.includes("-h") || args.includes("--help") || args.length === 0) {
31
40
  showHelp();
32
41
  process.exit(0);
@@ -35,57 +44,58 @@ async function main() {
35
44
  console.log(`levit-kit v${(0, version_1.getVersion)()}`);
36
45
  process.exit(0);
37
46
  }
38
- if (args[0] === "init") {
39
- const projectName = args[1];
40
- if (!projectName) {
41
- console.error("Error: Project name is required.");
42
- console.error("Usage: levit init <project-name>");
43
- process.exit(1);
44
- }
45
- // Basic validation: ensure it's a valid directory name
46
- if (!/^[a-z0-9-_]+$/i.test(projectName)) {
47
- console.error("Error: Invalid project name. Use only letters, numbers, dashes, and underscores.");
48
- process.exit(1);
49
- }
50
- const targetPath = node_path_1.default.resolve(process.cwd(), projectName);
51
- try {
52
- (0, init_1.initProject)(projectName, targetPath);
53
- }
54
- catch (error) {
55
- console.error(error instanceof Error ? `Error: ${error.message}` : "Unexpected error");
56
- process.exit(1);
57
- }
58
- }
59
- else if (args[0] === "feature") {
60
- try {
61
- await (0, feature_1.featureCommand)(args.slice(1), process.cwd());
62
- }
63
- catch (error) {
64
- console.error(error instanceof Error ? `Error: ${error.message}` : "Unexpected error");
65
- process.exit(1);
66
- }
67
- }
68
- else if (args[0] === "decision") {
69
- try {
70
- await (0, decision_1.decisionCommand)(args.slice(1), process.cwd());
71
- }
72
- catch (error) {
73
- console.error(error instanceof Error ? `Error: ${error.message}` : "Unexpected error");
74
- 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);
75
75
  }
76
76
  }
77
- else if (args[0] === "handoff") {
78
- try {
79
- await (0, handoff_1.handoffCommand)(args.slice(1), process.cwd());
77
+ catch (error) {
78
+ if (error instanceof errors_1.LevitError) {
79
+ (0, error_helper_1.displayError)(error);
80
80
  }
81
- catch (error) {
82
- console.error(error instanceof Error ? `Error: ${error.message}` : "Unexpected error");
83
- 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
+ }
84
98
  }
85
- }
86
- else {
87
- console.error(`Error: Unknown command "${args[0]}"`);
88
- showHelp();
89
99
  process.exit(1);
90
100
  }
91
101
  }
@@ -4,11 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.decisionCommand = decisionCommand;
7
- const node_path_1 = __importDefault(require("node:path"));
8
7
  const promises_1 = __importDefault(require("node:readline/promises"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
9
  const levit_project_1 = require("../core/levit_project");
10
10
  const cli_args_1 = require("../core/cli_args");
11
- const write_file_1 = require("../core/write_file");
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");
12
15
  const ids_1 = require("../core/ids");
13
16
  function normalizeSlug(input) {
14
17
  return input
@@ -21,39 +24,51 @@ async function decisionCommand(argv, cwd) {
21
24
  const { positional, flags } = (0, cli_args_1.parseArgs)(argv);
22
25
  const sub = positional[0];
23
26
  if (sub !== "new") {
24
- throw new Error('Usage: levit decision new [--title "..."] [--id "001"]');
27
+ throw new errors_1.LevitError(errors_1.LevitErrorCode.INVALID_COMMAND, 'Usage: levit decision new [--title "..."] [--id "001"]');
25
28
  }
26
29
  const projectRoot = (0, levit_project_1.requireLevitProjectRoot)(cwd);
27
30
  const yes = (0, cli_args_1.getBooleanFlag)(flags, "yes");
28
31
  const overwrite = (0, cli_args_1.getBooleanFlag)(flags, "force");
32
+ const isJsonMode = logger_1.Logger.getJsonMode();
29
33
  let title = (0, cli_args_1.getStringFlag)(flags, "title");
30
34
  let id = (0, cli_args_1.getStringFlag)(flags, "id");
31
35
  const featureRef = (0, cli_args_1.getStringFlag)(flags, "feature");
32
- const computedId = (0, ids_1.nextSequentialId)(node_path_1.default.join(projectRoot, ".levit", "decisions"), /^ADR-(\d+)-/);
33
36
  if (!yes) {
34
37
  const rl = promises_1.default.createInterface({ input: process.stdin, output: process.stdout });
35
38
  if (!title) {
36
39
  title = (await rl.question("Decision title: ")).trim();
37
40
  }
38
- if (!id) {
39
- id = (await rl.question(`Decision id [${computedId}]: `)).trim() || computedId;
40
- }
41
41
  await rl.close();
42
42
  }
43
- if (!id) {
44
- id = computedId;
45
- }
46
43
  if (!title) {
47
- throw new Error("Missing --title");
44
+ throw new errors_1.LevitError(errors_1.LevitErrorCode.MISSING_REQUIRED_ARG, "Missing --title");
48
45
  }
46
+ // Generate ID if not provided
49
47
  if (!id) {
50
- throw new Error("Missing --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
+ }
51
71
  }
52
- const slug = normalizeSlug(title);
53
- const fileName = `ADR-${id}-${slug}.md`;
54
- const decisionPath = node_path_1.default.join(projectRoot, ".levit", "decisions", fileName);
55
- const featureLine = featureRef ? `- **Feature**: ${featureRef}\n` : "";
56
- const content = `# ADR ${id}: ${title}\n\n- **Date**: [YYYY-MM-DD]\n- **Status**: [Draft / Proposed / Approved]\n${featureLine}\n## Context\n[fill]\n\n## Decision\n[fill]\n\n## Rationale\n[fill]\n\n## Alternatives Considered\n[fill]\n\n## Consequences\n[fill]\n`;
57
- (0, write_file_1.writeTextFile)(decisionPath, content, { overwrite });
58
- process.stdout.write(`Created ${node_path_1.default.relative(projectRoot, decisionPath)}\n`);
72
+ const createdPath = decision_service_1.DecisionService.createDecision(projectRoot, { title, featureRef, id, overwrite });
73
+ logger_1.Logger.success(`Created ${createdPath}`);
59
74
  }
@@ -4,11 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.featureCommand = featureCommand;
7
- const node_path_1 = __importDefault(require("node:path"));
8
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"));
9
10
  const levit_project_1 = require("../core/levit_project");
10
11
  const cli_args_1 = require("../core/cli_args");
11
- const write_file_1 = require("../core/write_file");
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");
12
16
  const ids_1 = require("../core/ids");
13
17
  function normalizeSlug(input) {
14
18
  return input
@@ -20,16 +24,31 @@ function normalizeSlug(input) {
20
24
  async function featureCommand(argv, cwd) {
21
25
  const { positional, flags } = (0, cli_args_1.parseArgs)(argv);
22
26
  const sub = positional[0];
23
- if (sub !== "new") {
24
- throw new Error('Usage: levit feature new [--title "..."] [--slug "..."] [--id "001"]');
25
- }
26
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) {
27
46
  const yes = (0, cli_args_1.getBooleanFlag)(flags, "yes");
28
47
  const overwrite = (0, cli_args_1.getBooleanFlag)(flags, "force");
48
+ const isJsonMode = logger_1.Logger.getJsonMode();
29
49
  let title = (0, cli_args_1.getStringFlag)(flags, "title");
30
50
  let slug = (0, cli_args_1.getStringFlag)(flags, "slug");
31
51
  let id = (0, cli_args_1.getStringFlag)(flags, "id");
32
- const computedId = (0, ids_1.nextSequentialId)(node_path_1.default.join(projectRoot, "features"), /^(\d+)-/);
33
52
  if (!yes) {
34
53
  const rl = promises_1.default.createInterface({ input: process.stdin, output: process.stdout });
35
54
  if (!title) {
@@ -39,23 +58,102 @@ async function featureCommand(argv, cwd) {
39
58
  const computed = normalizeSlug(title || "");
40
59
  slug = (await rl.question(`Feature slug [${computed}]: `)).trim() || computed;
41
60
  }
42
- if (!id) {
43
- id = (await rl.question(`Feature id [${computedId}]: `)).trim() || computedId;
44
- }
45
61
  await rl.close();
46
62
  }
47
- if (!id) {
48
- id = computedId;
49
- }
50
63
  if (!title) {
51
- throw new Error("Missing --title");
64
+ throw new errors_1.LevitError(errors_1.LevitErrorCode.MISSING_REQUIRED_ARG, "Missing --title");
52
65
  }
53
66
  if (!slug) {
54
- throw new Error("Missing --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(", ")}`);
55
156
  }
56
- const fileName = `${id}-${slug}.md`;
57
- const featurePath = node_path_1.default.join(projectRoot, "features", fileName);
58
- const content = `# INTENT: ${title}\n\n## 1. Vision (The \"Why\")\n- **User Story**: [fill]\n- **Priority**: [Low / Medium / High / Critical]\n\n## 2. Success Criteria (The \"What\")\n- [ ] Criterion 1\n\n## 3. Boundaries (The \"No\")\n- Non-goal 1\n\n## 4. Technical Constraints\n- [fill]\n\n## 5. Agent Task\n- [fill]\n`;
59
- (0, write_file_1.writeTextFile)(featurePath, content, { overwrite });
60
- process.stdout.write(`Created ${node_path_1.default.relative(projectRoot, featurePath)}\n`);
157
+ feature_service_1.FeatureService.updateFeatureStatus(projectRoot, featureId, newStatus);
158
+ logger_1.Logger.info(`Updated feature ${featureId} status to "${newStatus}"`);
61
159
  }