ai-development-protocol 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/agent.md +1083 -0
- package/dist/cli.js +1163 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +1192 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import path9 from "path";
|
|
5
|
+
|
|
6
|
+
// src/core/init.ts
|
|
7
|
+
import fs2 from "fs";
|
|
8
|
+
import path3 from "path";
|
|
9
|
+
|
|
10
|
+
// src/core/constants.ts
|
|
11
|
+
import path from "path";
|
|
12
|
+
var AI_DIR = ".ai";
|
|
13
|
+
var CONTEXT_DIR = path.join(AI_DIR, "context");
|
|
14
|
+
var DECISIONS_DIR = path.join(AI_DIR, "decisions");
|
|
15
|
+
var TASKS_DIR = path.join(AI_DIR, "tasks");
|
|
16
|
+
var REPORTS_DIR = path.join(AI_DIR, "reports");
|
|
17
|
+
var TASKS_SUBDIRS = ["planned", "active", "completed"];
|
|
18
|
+
var REPORTS_SUBDIRS = ["reviews", "security", "testing"];
|
|
19
|
+
var REQUIRED_CONTEXT_FILES = [
|
|
20
|
+
"project.md",
|
|
21
|
+
"requirements.md",
|
|
22
|
+
"architecture.md",
|
|
23
|
+
"development.md",
|
|
24
|
+
"operations.md"
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// src/utils/fs.ts
|
|
28
|
+
import fs from "fs";
|
|
29
|
+
import path2 from "path";
|
|
30
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
31
|
+
let current = path2.resolve(startDir);
|
|
32
|
+
while (true) {
|
|
33
|
+
if (fs.existsSync(path2.join(current, ".ai"))) {
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
if (fs.existsSync(path2.join(current, "agent.md"))) {
|
|
37
|
+
return current;
|
|
38
|
+
}
|
|
39
|
+
if (fs.existsSync(path2.join(current, ".git"))) {
|
|
40
|
+
return current;
|
|
41
|
+
}
|
|
42
|
+
const parent = path2.dirname(current);
|
|
43
|
+
if (parent === current) {
|
|
44
|
+
return path2.resolve(startDir);
|
|
45
|
+
}
|
|
46
|
+
current = parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function ensureDir(dirPath) {
|
|
50
|
+
if (!fs.existsSync(dirPath)) {
|
|
51
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function slugify(text) {
|
|
55
|
+
return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
56
|
+
}
|
|
57
|
+
function writeFileAtomic(filePath, content) {
|
|
58
|
+
ensureDir(path2.dirname(filePath));
|
|
59
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/templates/readme.ts
|
|
63
|
+
function generateReadme(projectName = "Project") {
|
|
64
|
+
return `# AI Project Knowledge Base (${projectName})
|
|
65
|
+
|
|
66
|
+
This directory maintains the living engineering knowledge for **${projectName}**, following the **AI Project Development Protocol (v1.0.0)**.
|
|
67
|
+
|
|
68
|
+
## How This Works
|
|
69
|
+
|
|
70
|
+
1. **Conversation is the Primary Interface**: The project owner interacts through natural dialogue.
|
|
71
|
+
2. **AI-Maintained**: The AI agent creates, updates, and synchronizes these artifacts progressively as decisions are confirmed.
|
|
72
|
+
3. **Living Knowledge**: Designed to be accumulative, preserving context, architectural decisions, and tasks without documentation drift.
|
|
73
|
+
|
|
74
|
+
## Directory Structure
|
|
75
|
+
|
|
76
|
+
\`\`\`text
|
|
77
|
+
.ai/
|
|
78
|
+
\u251C\u2500\u2500 README.md # This guide
|
|
79
|
+
\u251C\u2500\u2500 context/ # Authoritative system context
|
|
80
|
+
\u2502 \u251C\u2500\u2500 project.md # Identity, scope, state, and goals
|
|
81
|
+
\u2502 \u251C\u2500\u2500 requirements.md # Requirements, assumptions, and open questions
|
|
82
|
+
\u2502 \u251C\u2500\u2500 architecture.md # System structure, components, boundaries
|
|
83
|
+
\u2502 \u251C\u2500\u2500 development.md # Conventions, workflows, testing rules
|
|
84
|
+
\u2502 \u2514\u2500\u2500 operations.md # Deployment, environments, runbooks
|
|
85
|
+
\u251C\u2500\u2500 decisions/ # Architecture Decision Records (ADRs)
|
|
86
|
+
\u251C\u2500\u2500 tasks/ # Actionable work breakdown
|
|
87
|
+
\u2502 \u251C\u2500\u2500 planned/ # Ready to be started
|
|
88
|
+
\u2502 \u251C\u2500\u2500 active/ # Currently in progress
|
|
89
|
+
\u2502 \u2514\u2500\u2500 completed/ # Done and validated
|
|
90
|
+
\u2514\u2500\u2500 reports/ # Periodic evaluation records
|
|
91
|
+
\u251C\u2500\u2500 reviews/ # Code and design reviews
|
|
92
|
+
\u251C\u2500\u2500 security/ # Security audits
|
|
93
|
+
\u2514\u2500\u2500 testing/ # Test summaries
|
|
94
|
+
\`\`\`
|
|
95
|
+
|
|
96
|
+
## Protocol Reference
|
|
97
|
+
|
|
98
|
+
See \`agent.md\` in the project root for the complete specification.
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/templates/project.ts
|
|
103
|
+
function generateProjectContext(meta = {}) {
|
|
104
|
+
const name = meta.name || "My Project";
|
|
105
|
+
const purpose = meta.purpose || "To be defined through discovery.";
|
|
106
|
+
const problem = meta.problem || "To be identified through conversation.";
|
|
107
|
+
const targetUsers = meta.targetUsers || "To be determined.";
|
|
108
|
+
const valueProposition = meta.valueProposition || "To be articulated.";
|
|
109
|
+
return `# Project Identity & State
|
|
110
|
+
|
|
111
|
+
## 1. Project Identity
|
|
112
|
+
|
|
113
|
+
* **Project Name:** ${name}
|
|
114
|
+
* **Purpose:** ${purpose}
|
|
115
|
+
* **Problem:** ${problem}
|
|
116
|
+
* **Target Users:** ${targetUsers}
|
|
117
|
+
* **Value Proposition:** ${valueProposition}
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 2. Product Scope
|
|
122
|
+
|
|
123
|
+
### Current Goals
|
|
124
|
+
- [ ] Initialize project foundation and requirements discovery.
|
|
125
|
+
|
|
126
|
+
### MVP Scope
|
|
127
|
+
*To be defined.*
|
|
128
|
+
|
|
129
|
+
### Future Possibilities
|
|
130
|
+
*To be explored.*
|
|
131
|
+
|
|
132
|
+
### Out of Scope
|
|
133
|
+
*To be established.*
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 3. Project State
|
|
138
|
+
|
|
139
|
+
* **Current Goal:** Establish problem and user requirements.
|
|
140
|
+
* **Current Phase:** Level 1 \u2014 DISCOVERY
|
|
141
|
+
* **Confirmed Decisions:** Initial protocol structure established.
|
|
142
|
+
* **Active Work:** Initial discovery.
|
|
143
|
+
* **Open Questions:** See \`requirements.md\`.
|
|
144
|
+
* **Deferred Work:** None yet.
|
|
145
|
+
* **Known Constraints:** None yet.
|
|
146
|
+
* **Known Risks:** None identified.
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/templates/requirements.ts
|
|
151
|
+
function generateRequirementsContext() {
|
|
152
|
+
return `# Requirements & Discovery
|
|
153
|
+
|
|
154
|
+
## 1. Functional Requirements
|
|
155
|
+
|
|
156
|
+
*Each requirement should preserve rationale (Reason) and confirmation status.*
|
|
157
|
+
|
|
158
|
+
*No confirmed functional requirements yet.*
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 2. Non-Functional Requirements
|
|
163
|
+
|
|
164
|
+
* **Performance:** Standard expectations.
|
|
165
|
+
* **Security:** Secure by default.
|
|
166
|
+
* **Maintainability:** Modular and clean code standards.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 3. Constraints & Assumptions
|
|
171
|
+
|
|
172
|
+
### Constraints
|
|
173
|
+
* None established.
|
|
174
|
+
|
|
175
|
+
### Assumptions
|
|
176
|
+
* None established.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 4. Open Questions
|
|
181
|
+
|
|
182
|
+
<!-- Questions being explored. When answered, move to confirmed decisions and remove from here. -->
|
|
183
|
+
|
|
184
|
+
- What is the primary problem this project solves?
|
|
185
|
+
- Who is the initial target audience?
|
|
186
|
+
- What are the absolute minimum features required for the MVP?
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/templates/architecture.ts
|
|
191
|
+
function generateArchitectureContext() {
|
|
192
|
+
return `# Architecture & System Design
|
|
193
|
+
|
|
194
|
+
## 1. System Overview
|
|
195
|
+
|
|
196
|
+
*A high-level summary of the architecture once established.*
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 2. Core Components & Boundaries
|
|
201
|
+
|
|
202
|
+
*To be defined.*
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 3. Technology Stack
|
|
207
|
+
|
|
208
|
+
* **Runtime / Language:** To be determined.
|
|
209
|
+
* **Storage / Database:** To be determined.
|
|
210
|
+
* **Frameworks / Libraries:** To be determined.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 4. Architectural Decisions Index
|
|
215
|
+
|
|
216
|
+
*See \`.ai/decisions/\` for detailed ADRs.*
|
|
217
|
+
|
|
218
|
+
| ID | Title | Status | Date |
|
|
219
|
+
|---|---|---|---|
|
|
220
|
+
| - | No ADRs created yet | - | - |
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/templates/development.ts
|
|
225
|
+
function generateDevelopmentContext() {
|
|
226
|
+
return `# Development Workflow & Conventions
|
|
227
|
+
|
|
228
|
+
## 1. Environment & Setup
|
|
229
|
+
|
|
230
|
+
*Prerequisites and commands to get started.*
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 2. Coding Conventions
|
|
235
|
+
|
|
236
|
+
* Code style, linting, formatting rules, and naming conventions.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 3. Implementation Workflow
|
|
241
|
+
|
|
242
|
+
Before implementing significant features:
|
|
243
|
+
1. Understand the requirement.
|
|
244
|
+
2. Inspect existing implementation.
|
|
245
|
+
3. Identify affected components and boundaries.
|
|
246
|
+
4. Check relevant architecture and ADRs.
|
|
247
|
+
5. Identify dependencies and risks.
|
|
248
|
+
6. Produce a concise implementation approach.
|
|
249
|
+
7. Implement cleanly.
|
|
250
|
+
8. Validate (tests, build, type checks).
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 4. Testing & Validation Strategy
|
|
255
|
+
|
|
256
|
+
* Test commands, coverage expectations, and verification steps.
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/templates/operations.ts
|
|
261
|
+
function generateOperationsContext() {
|
|
262
|
+
return `# Operations & Deployment
|
|
263
|
+
|
|
264
|
+
## 1. Deployment Strategy
|
|
265
|
+
|
|
266
|
+
*Target hosting, CI/CD pipelines, containerization, and release process.*
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 2. Environment Configuration
|
|
271
|
+
|
|
272
|
+
*Required environment variables and secret management.*
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 3. Observability & Monitoring
|
|
277
|
+
|
|
278
|
+
*Logging standards, health checks, metrics, and error alerting.*
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/templates/agentBridge.ts
|
|
283
|
+
function generateAgentBridge(projectName = "Project") {
|
|
284
|
+
return `# AI Coding Assistant Instructions
|
|
285
|
+
|
|
286
|
+
You are the AI engineering partner for **${projectName}**, operating under the **AI Project Development Protocol** specified in \`agent.md\`.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## \u{1F680} Session Initialization (Every Fresh Session)
|
|
291
|
+
|
|
292
|
+
Whenever a new conversation session starts:
|
|
293
|
+
|
|
294
|
+
1. **Check Project State First**:
|
|
295
|
+
- Inspect \`.ai/context/project.md\` and \`.ai/context/requirements.md\` (or run \`npx aidp status\`).
|
|
296
|
+
- Identify: Current Phase, Current Goal, Active Tasks, and Open Questions.
|
|
297
|
+
|
|
298
|
+
2. **If This Is a Brand New / Greenfield Project (Level 0 or Level 1)**:
|
|
299
|
+
- Proactively greet the project owner:
|
|
300
|
+
> *"Tell me what you're thinking about. It doesn't need to be fully defined. We'll progressively turn the idea into a product, requirements, architecture, and implementation plan."*
|
|
301
|
+
- Guide the owner through discovery: problem, target users, core workflow, and MVP boundary.
|
|
302
|
+
- Do NOT start generating large boilerplate code until the core idea and architecture are confirmed.
|
|
303
|
+
|
|
304
|
+
3. **If the Project Is Already Underway (Level 2+)**:
|
|
305
|
+
- Briefly summarize where the project currently stands based on \`.ai/context/project.md\` and active tasks.
|
|
306
|
+
- Ask how the owner would like to proceed with the active or planned tasks.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## \u{1F4CB} Core Operating Rules (from \`agent.md\`)
|
|
311
|
+
|
|
312
|
+
* **Conversation is the Primary Interface**: The project owner interacts through chat; you (the AI) maintain the \`.ai/\` artifacts automatically.
|
|
313
|
+
* **Brainstorming vs. Decisions**: Treat exploratory ideas as proposals. Only record ADRs when the owner explicitly confirms a technical choice.
|
|
314
|
+
* **Preserve Reasoning**: Every architectural choice must be recorded as an ADR with Problem, Options, Decision, and Consequences.
|
|
315
|
+
* **Implementation Boundary**: Do not write production code until the owner signals clear implementation intent (e.g. *"Build it"*, *"Let's implement this"*).
|
|
316
|
+
* **Validation Truth**: Always run real tests and build commands in the terminal before marking any task as complete. Never claim validation that was not run.
|
|
317
|
+
`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/core/init.ts
|
|
321
|
+
import { fileURLToPath } from "url";
|
|
322
|
+
function initProtocol(options = {}) {
|
|
323
|
+
const root = path3.resolve(options.targetDir || process.cwd());
|
|
324
|
+
const aiPath = path3.join(root, AI_DIR);
|
|
325
|
+
const isAlreadyInit = fs2.existsSync(aiPath);
|
|
326
|
+
const createdFiles = [];
|
|
327
|
+
const skippedFiles = [];
|
|
328
|
+
ensureDir(aiPath);
|
|
329
|
+
ensureDir(path3.join(root, CONTEXT_DIR));
|
|
330
|
+
ensureDir(path3.join(root, DECISIONS_DIR));
|
|
331
|
+
for (const sub of TASKS_SUBDIRS) {
|
|
332
|
+
ensureDir(path3.join(root, TASKS_DIR, sub));
|
|
333
|
+
}
|
|
334
|
+
for (const sub of REPORTS_SUBDIRS) {
|
|
335
|
+
ensureDir(path3.join(root, REPORTS_DIR, sub));
|
|
336
|
+
}
|
|
337
|
+
const projectName = options.projectName || path3.basename(root);
|
|
338
|
+
const bridgeContent = generateAgentBridge(projectName);
|
|
339
|
+
const files = {
|
|
340
|
+
[path3.join(root, AI_DIR, "README.md")]: generateReadme(projectName),
|
|
341
|
+
[path3.join(root, CONTEXT_DIR, "project.md")]: generateProjectContext({
|
|
342
|
+
name: projectName,
|
|
343
|
+
...options.meta
|
|
344
|
+
}),
|
|
345
|
+
[path3.join(root, CONTEXT_DIR, "requirements.md")]: generateRequirementsContext(),
|
|
346
|
+
[path3.join(root, CONTEXT_DIR, "architecture.md")]: generateArchitectureContext(),
|
|
347
|
+
[path3.join(root, CONTEXT_DIR, "development.md")]: generateDevelopmentContext(),
|
|
348
|
+
[path3.join(root, CONTEXT_DIR, "operations.md")]: generateOperationsContext()
|
|
349
|
+
};
|
|
350
|
+
if (options.includeBridges !== false) {
|
|
351
|
+
files[path3.join(root, "AGENTS.md")] = bridgeContent;
|
|
352
|
+
files[path3.join(root, "CLAUDE.md")] = bridgeContent;
|
|
353
|
+
files[path3.join(root, ".cursorrules")] = bridgeContent;
|
|
354
|
+
files[path3.join(root, ".github", "copilot-instructions.md")] = bridgeContent;
|
|
355
|
+
const rootAgentMd = path3.join(root, "agent.md");
|
|
356
|
+
if (!fs2.existsSync(rootAgentMd) || options.force) {
|
|
357
|
+
try {
|
|
358
|
+
const sourceAgentMd = fileURLToPath(new URL("../../agent.md", import.meta.url));
|
|
359
|
+
if (fs2.existsSync(sourceAgentMd)) {
|
|
360
|
+
files[rootAgentMd] = fs2.readFileSync(sourceAgentMd, "utf-8");
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
367
|
+
if (fs2.existsSync(filePath) && !options.force) {
|
|
368
|
+
skippedFiles.push(path3.relative(root, filePath));
|
|
369
|
+
} else {
|
|
370
|
+
writeFileAtomic(filePath, content);
|
|
371
|
+
createdFiles.push(path3.relative(root, filePath));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const emptyDirs = [
|
|
375
|
+
path3.join(root, DECISIONS_DIR),
|
|
376
|
+
...TASKS_SUBDIRS.map((s) => path3.join(root, TASKS_DIR, s)),
|
|
377
|
+
...REPORTS_SUBDIRS.map((s) => path3.join(root, REPORTS_DIR, s))
|
|
378
|
+
];
|
|
379
|
+
for (const dir of emptyDirs) {
|
|
380
|
+
const keepPath = path3.join(dir, ".gitkeep");
|
|
381
|
+
if (!fs2.existsSync(keepPath) && fs2.readdirSync(dir).length === 0) {
|
|
382
|
+
writeFileAtomic(keepPath, "");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
aiPath,
|
|
387
|
+
createdFiles,
|
|
388
|
+
skippedFiles,
|
|
389
|
+
alreadyInitialized: isAlreadyInit && !options.force
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/core/status.ts
|
|
394
|
+
import fs6 from "fs";
|
|
395
|
+
import path7 from "path";
|
|
396
|
+
|
|
397
|
+
// src/core/adr.ts
|
|
398
|
+
import fs3 from "fs";
|
|
399
|
+
import path4 from "path";
|
|
400
|
+
|
|
401
|
+
// src/templates/adr.ts
|
|
402
|
+
function generateAdr(options) {
|
|
403
|
+
const padId = String(options.id).padStart(4, "0");
|
|
404
|
+
const dateStr = options.date || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
405
|
+
const status = options.status || "Accepted";
|
|
406
|
+
const problem = options.problem || "Describe the architectural context and problem here.";
|
|
407
|
+
const optionsList = options.options && options.options.length > 0 ? options.options.map((opt, i) => `${i + 1}. **${opt}**`).join("\n") : "1. **Option 1:** Description\n2. **Option 2:** Description";
|
|
408
|
+
const decision = options.decision || "Describe the decision made and rationale.";
|
|
409
|
+
const consequences = options.consequences || "Describe positive and negative implications.";
|
|
410
|
+
return `# ADR-${padId}: ${options.title}
|
|
411
|
+
|
|
412
|
+
* **Status:** ${status}
|
|
413
|
+
* **Date:** ${dateStr}
|
|
414
|
+
* **Deciders:** AI & Project Owner
|
|
415
|
+
|
|
416
|
+
## 1. Problem
|
|
417
|
+
${problem}
|
|
418
|
+
|
|
419
|
+
## 2. Options Considered
|
|
420
|
+
${optionsList}
|
|
421
|
+
|
|
422
|
+
## 3. Trade-offs
|
|
423
|
+
*Pros and cons evaluated during discussion.*
|
|
424
|
+
|
|
425
|
+
## 4. Decision
|
|
426
|
+
${decision}
|
|
427
|
+
|
|
428
|
+
## 5. Consequences
|
|
429
|
+
${consequences}
|
|
430
|
+
`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/core/adr.ts
|
|
434
|
+
function listAdrs(projectRoot = findProjectRoot()) {
|
|
435
|
+
const decisionsPath = path4.join(projectRoot, DECISIONS_DIR);
|
|
436
|
+
if (!fs3.existsSync(decisionsPath)) {
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
const files = fs3.readdirSync(decisionsPath).filter((f) => f.endsWith(".md") && /^\d{4}-/.test(f)).sort();
|
|
440
|
+
const records = [];
|
|
441
|
+
for (const filename of files) {
|
|
442
|
+
const filePath = path4.join(decisionsPath, filename);
|
|
443
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
444
|
+
const idMatch = filename.match(/^(\d{4})/);
|
|
445
|
+
const id = idMatch ? parseInt(idMatch[1], 10) : 0;
|
|
446
|
+
const formattedId = idMatch ? idMatch[1] : "0000";
|
|
447
|
+
const titleMatch = content.match(/^#\s+ADR-\d+:\s*(.+)$/m);
|
|
448
|
+
const title2 = titleMatch ? titleMatch[1].trim() : filename.replace(/^\d{4}-|\.md$/g, "");
|
|
449
|
+
const statusMatch = content.match(/\*\s*\*\*Status:\*\*\s*(.+)$/m);
|
|
450
|
+
const status = statusMatch ? statusMatch[1].trim() : "Unknown";
|
|
451
|
+
const dateMatch = content.match(/\*\s*\*\*Date:\*\*\s*(.+)$/m);
|
|
452
|
+
const date = dateMatch ? dateMatch[1].trim() : "";
|
|
453
|
+
records.push({
|
|
454
|
+
id,
|
|
455
|
+
formattedId,
|
|
456
|
+
title: title2,
|
|
457
|
+
status,
|
|
458
|
+
date,
|
|
459
|
+
filename,
|
|
460
|
+
filePath
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return records;
|
|
464
|
+
}
|
|
465
|
+
function getNextAdrId(projectRoot = findProjectRoot()) {
|
|
466
|
+
const adrs = listAdrs(projectRoot);
|
|
467
|
+
if (adrs.length === 0) return 1;
|
|
468
|
+
const maxId = Math.max(...adrs.map((a) => a.id));
|
|
469
|
+
return maxId + 1;
|
|
470
|
+
}
|
|
471
|
+
function createAdr(options) {
|
|
472
|
+
const root = options.projectRoot || findProjectRoot();
|
|
473
|
+
const nextId = getNextAdrId(root);
|
|
474
|
+
const formattedId = String(nextId).padStart(4, "0");
|
|
475
|
+
const slug = slugify(options.title);
|
|
476
|
+
const filename = `${formattedId}-${slug}.md`;
|
|
477
|
+
const decisionsPath = path4.join(root, DECISIONS_DIR);
|
|
478
|
+
ensureDir(decisionsPath);
|
|
479
|
+
const filePath = path4.join(decisionsPath, filename);
|
|
480
|
+
const content = generateAdr({
|
|
481
|
+
...options,
|
|
482
|
+
id: nextId
|
|
483
|
+
});
|
|
484
|
+
writeFileAtomic(filePath, content);
|
|
485
|
+
syncAdrWithArchitecture(root, {
|
|
486
|
+
formattedId,
|
|
487
|
+
title: options.title,
|
|
488
|
+
status: options.status || "Accepted",
|
|
489
|
+
date: options.date || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
490
|
+
filename
|
|
491
|
+
});
|
|
492
|
+
return {
|
|
493
|
+
id: nextId,
|
|
494
|
+
formattedId,
|
|
495
|
+
title: options.title,
|
|
496
|
+
status: options.status || "Accepted",
|
|
497
|
+
date: options.date || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
498
|
+
filename,
|
|
499
|
+
filePath
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function syncAdrWithArchitecture(projectRoot, adr) {
|
|
503
|
+
const archFile = path4.join(projectRoot, CONTEXT_DIR, "architecture.md");
|
|
504
|
+
if (!fs3.existsSync(archFile)) return;
|
|
505
|
+
let content = fs3.readFileSync(archFile, "utf-8");
|
|
506
|
+
const tableHeaderRegex = /(\| ID \| Title \| Status \| Date \|\r?\n\|---\|---\|---\|---\|\r?\n)([\s\S]*?)(?=\r?\n#|$)/;
|
|
507
|
+
const row = `| [ADR-${adr.formattedId}](../decisions/${adr.filename}) | ${adr.title} | ${adr.status} | ${adr.date} |`;
|
|
508
|
+
if (tableHeaderRegex.test(content)) {
|
|
509
|
+
content = content.replace(tableHeaderRegex, (match, header, body) => {
|
|
510
|
+
let rows = body.split(/\r?\n/).map((r) => r.trim()).filter((r) => r.length > 0 && !r.includes("No ADRs created yet"));
|
|
511
|
+
rows.push(row);
|
|
512
|
+
return `${header}${rows.join("\n")}
|
|
513
|
+
`;
|
|
514
|
+
});
|
|
515
|
+
writeFileAtomic(archFile, content);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/core/task.ts
|
|
520
|
+
import fs4 from "fs";
|
|
521
|
+
import path5 from "path";
|
|
522
|
+
|
|
523
|
+
// src/templates/task.ts
|
|
524
|
+
function generateTask(options) {
|
|
525
|
+
const status = options.status || "planned";
|
|
526
|
+
const createdDate = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
527
|
+
const goal = options.goal || "Describe the objective of this task.";
|
|
528
|
+
const reqRef = options.requirementsRef || "Refer to requirements.md";
|
|
529
|
+
const archRef = options.architectureRef || "Refer to architecture.md";
|
|
530
|
+
const stepsList = options.steps && options.steps.length > 0 ? options.steps.map((s) => `- [ ] ${s}`).join("\n") : "- [ ] Step 1: Design and setup\n- [ ] Step 2: Implementation\n- [ ] Step 3: Tests";
|
|
531
|
+
const validationList = options.validation && options.validation.length > 0 ? options.validation.map((v) => `- [ ] ${v}`).join("\n") : "- [ ] Unit and integration tests pass\n- [ ] Working behavior validated";
|
|
532
|
+
return `# ${options.id.toUpperCase()}: ${options.title}
|
|
533
|
+
|
|
534
|
+
* **Status:** ${status.toUpperCase()}
|
|
535
|
+
* **Created:** ${createdDate}
|
|
536
|
+
* **Category:** Implementation
|
|
537
|
+
|
|
538
|
+
## Goal
|
|
539
|
+
${goal}
|
|
540
|
+
|
|
541
|
+
## Requirements Reference
|
|
542
|
+
${reqRef}
|
|
543
|
+
|
|
544
|
+
## Architecture Reference
|
|
545
|
+
${archRef}
|
|
546
|
+
|
|
547
|
+
## Implementation Steps
|
|
548
|
+
${stepsList}
|
|
549
|
+
|
|
550
|
+
## Validation Criteria
|
|
551
|
+
${validationList}
|
|
552
|
+
`;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/core/task.ts
|
|
556
|
+
function listTasks(projectRoot = findProjectRoot()) {
|
|
557
|
+
const result = {
|
|
558
|
+
planned: [],
|
|
559
|
+
active: [],
|
|
560
|
+
completed: []
|
|
561
|
+
};
|
|
562
|
+
const tasksBase = path5.join(projectRoot, TASKS_DIR);
|
|
563
|
+
if (!fs4.existsSync(tasksBase)) {
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
for (const status of TASKS_SUBDIRS) {
|
|
567
|
+
const dir = path5.join(tasksBase, status);
|
|
568
|
+
if (!fs4.existsSync(dir)) continue;
|
|
569
|
+
const files = fs4.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
570
|
+
for (const filename of files) {
|
|
571
|
+
const filePath = path5.join(dir, filename);
|
|
572
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
573
|
+
const titleMatch = content.match(/^#\s+([^:]+):\s*(.+)$/m);
|
|
574
|
+
const id = titleMatch ? titleMatch[1].trim() : filename.replace(/\.md$/, "");
|
|
575
|
+
const title2 = titleMatch ? titleMatch[2].trim() : filename.replace(/\.md$/, "");
|
|
576
|
+
result[status].push({
|
|
577
|
+
id,
|
|
578
|
+
title: title2,
|
|
579
|
+
status,
|
|
580
|
+
filename,
|
|
581
|
+
filePath
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return result;
|
|
586
|
+
}
|
|
587
|
+
function getNextTaskId(projectRoot = findProjectRoot()) {
|
|
588
|
+
const all = listTasks(projectRoot);
|
|
589
|
+
const allTasks = [...all.planned, ...all.active, ...all.completed];
|
|
590
|
+
let maxNum = 0;
|
|
591
|
+
for (const t of allTasks) {
|
|
592
|
+
const m = t.filename.match(/task-(\d+)/i) || t.id.match(/task-(\d+)/i);
|
|
593
|
+
if (m) {
|
|
594
|
+
const num = parseInt(m[1], 10);
|
|
595
|
+
if (num > maxNum) maxNum = num;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return `TASK-${String(maxNum + 1).padStart(4, "0")}`;
|
|
599
|
+
}
|
|
600
|
+
function createTask(options) {
|
|
601
|
+
const root = options.projectRoot || findProjectRoot();
|
|
602
|
+
const id = options.id || getNextTaskId(root);
|
|
603
|
+
const status = options.status || "planned";
|
|
604
|
+
const slug = slugify(options.title);
|
|
605
|
+
const filename = `${id.toLowerCase()}-${slug}.md`;
|
|
606
|
+
const destDir = path5.join(root, TASKS_DIR, status);
|
|
607
|
+
ensureDir(destDir);
|
|
608
|
+
const filePath = path5.join(destDir, filename);
|
|
609
|
+
const content = generateTask({
|
|
610
|
+
...options,
|
|
611
|
+
id,
|
|
612
|
+
status
|
|
613
|
+
});
|
|
614
|
+
writeFileAtomic(filePath, content);
|
|
615
|
+
return {
|
|
616
|
+
id,
|
|
617
|
+
title: options.title,
|
|
618
|
+
status,
|
|
619
|
+
filename,
|
|
620
|
+
filePath
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function moveTask(taskIdOrName, newStatus, projectRoot = findProjectRoot()) {
|
|
624
|
+
const all = listTasks(projectRoot);
|
|
625
|
+
let found = null;
|
|
626
|
+
const search = taskIdOrName.toLowerCase();
|
|
627
|
+
for (const status of TASKS_SUBDIRS) {
|
|
628
|
+
const match = all[status].find(
|
|
629
|
+
(t) => t.id.toLowerCase() === search || t.filename.toLowerCase().includes(search)
|
|
630
|
+
);
|
|
631
|
+
if (match) {
|
|
632
|
+
found = match;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (!found) {
|
|
637
|
+
throw new Error(`Task matching "${taskIdOrName}" was not found.`);
|
|
638
|
+
}
|
|
639
|
+
if (found.status === newStatus) {
|
|
640
|
+
return found;
|
|
641
|
+
}
|
|
642
|
+
const oldPath = found.filePath;
|
|
643
|
+
const newDir = path5.join(projectRoot, TASKS_DIR, newStatus);
|
|
644
|
+
ensureDir(newDir);
|
|
645
|
+
const newPath = path5.join(newDir, found.filename);
|
|
646
|
+
let content = fs4.readFileSync(oldPath, "utf-8");
|
|
647
|
+
content = content.replace(/\*\s*\*\*Status:\*\*\s*(.+)$/m, `* **Status:** ${newStatus.toUpperCase()}`);
|
|
648
|
+
fs4.unlinkSync(oldPath);
|
|
649
|
+
writeFileAtomic(newPath, content);
|
|
650
|
+
return {
|
|
651
|
+
...found,
|
|
652
|
+
status: newStatus,
|
|
653
|
+
filePath: newPath
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/core/question.ts
|
|
658
|
+
import fs5 from "fs";
|
|
659
|
+
import path6 from "path";
|
|
660
|
+
function listOpenQuestions(projectRoot = findProjectRoot()) {
|
|
661
|
+
const reqPath = path6.join(projectRoot, CONTEXT_DIR, "requirements.md");
|
|
662
|
+
if (!fs5.existsSync(reqPath)) return [];
|
|
663
|
+
const content = fs5.readFileSync(reqPath, "utf-8");
|
|
664
|
+
const sectionMatch = content.match(/## 4\. Open Questions([\s\S]*?)(?=\r?\n##|$)/);
|
|
665
|
+
if (!sectionMatch) return [];
|
|
666
|
+
const lines = sectionMatch[1].split(/\r?\n/);
|
|
667
|
+
const questions = [];
|
|
668
|
+
for (const line of lines) {
|
|
669
|
+
const trimmed = line.trim();
|
|
670
|
+
if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
|
|
671
|
+
questions.push(trimmed.slice(2).trim());
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return questions;
|
|
675
|
+
}
|
|
676
|
+
function addOpenQuestion(question, projectRoot = findProjectRoot()) {
|
|
677
|
+
const reqPath = path6.join(projectRoot, CONTEXT_DIR, "requirements.md");
|
|
678
|
+
if (!fs5.existsSync(reqPath)) {
|
|
679
|
+
throw new Error(`requirements.md not found at ${reqPath}`);
|
|
680
|
+
}
|
|
681
|
+
let content = fs5.readFileSync(reqPath, "utf-8");
|
|
682
|
+
const sectionHeader = "## 4. Open Questions";
|
|
683
|
+
const idx = content.indexOf(sectionHeader);
|
|
684
|
+
if (idx === -1) {
|
|
685
|
+
content += `
|
|
686
|
+
|
|
687
|
+
${sectionHeader}
|
|
688
|
+
|
|
689
|
+
- ${question}
|
|
690
|
+
`;
|
|
691
|
+
} else {
|
|
692
|
+
content = content.replace(/(## 4\. Open Questions[\s\S]*?)(\r?\n\r?\n##|\r?\n?$)/, (match, prefix, suffix) => {
|
|
693
|
+
return `${prefix.trimEnd()}
|
|
694
|
+
- ${question}
|
|
695
|
+
${suffix}`;
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
writeFileAtomic(reqPath, content);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/core/status.ts
|
|
702
|
+
function getProjectStatus(projectRoot = findProjectRoot()) {
|
|
703
|
+
const projectFile = path7.join(projectRoot, CONTEXT_DIR, "project.md");
|
|
704
|
+
const isInitialized = fs6.existsSync(projectFile);
|
|
705
|
+
if (!isInitialized) {
|
|
706
|
+
return {
|
|
707
|
+
isInitialized: false,
|
|
708
|
+
name: path7.basename(projectRoot),
|
|
709
|
+
currentGoal: "Not initialized",
|
|
710
|
+
currentPhase: "Unknown",
|
|
711
|
+
adrs: [],
|
|
712
|
+
tasks: { planned: [], active: [], completed: [] },
|
|
713
|
+
openQuestions: []
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
const projectContent = fs6.readFileSync(projectFile, "utf-8");
|
|
717
|
+
const nameMatch = projectContent.match(/\*\s*\*\*Project Name:\*\*\s*(.+)$/m);
|
|
718
|
+
const name = nameMatch ? nameMatch[1].trim() : path7.basename(projectRoot);
|
|
719
|
+
const goalMatch = projectContent.match(/\*\s*\*\*Current Goal:\*\*\s*(.+)$/m);
|
|
720
|
+
const currentGoal = goalMatch ? goalMatch[1].trim() : "Unspecified";
|
|
721
|
+
const phaseMatch = projectContent.match(/\*\s*\*\*Current Phase:\*\*\s*(.+)$/m);
|
|
722
|
+
const currentPhase = phaseMatch ? phaseMatch[1].trim() : "Unknown";
|
|
723
|
+
const adrs = listAdrs(projectRoot);
|
|
724
|
+
const tasks = listTasks(projectRoot);
|
|
725
|
+
const openQuestions = listOpenQuestions(projectRoot);
|
|
726
|
+
return {
|
|
727
|
+
isInitialized: true,
|
|
728
|
+
name,
|
|
729
|
+
currentGoal,
|
|
730
|
+
currentPhase,
|
|
731
|
+
adrs,
|
|
732
|
+
tasks,
|
|
733
|
+
openQuestions
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/core/validate.ts
|
|
738
|
+
import fs7 from "fs";
|
|
739
|
+
import path8 from "path";
|
|
740
|
+
function validateProtocol(projectRoot = findProjectRoot()) {
|
|
741
|
+
const errors = [];
|
|
742
|
+
const warnings = [];
|
|
743
|
+
let checkedItems = 0;
|
|
744
|
+
const aiPath = path8.join(projectRoot, AI_DIR);
|
|
745
|
+
checkedItems++;
|
|
746
|
+
if (!fs7.existsSync(aiPath)) {
|
|
747
|
+
return {
|
|
748
|
+
valid: false,
|
|
749
|
+
errors: [`AI protocol directory '${AI_DIR}' does not exist at ${projectRoot}`],
|
|
750
|
+
warnings: [],
|
|
751
|
+
checkedItems
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
checkedItems++;
|
|
755
|
+
const readmePath = path8.join(aiPath, "README.md");
|
|
756
|
+
if (!fs7.existsSync(readmePath)) {
|
|
757
|
+
warnings.push(`Missing '${AI_DIR}/README.md' guide.`);
|
|
758
|
+
}
|
|
759
|
+
const contextPath = path8.join(projectRoot, CONTEXT_DIR);
|
|
760
|
+
checkedItems++;
|
|
761
|
+
if (!fs7.existsSync(contextPath)) {
|
|
762
|
+
errors.push(`Missing context directory at '${CONTEXT_DIR}'.`);
|
|
763
|
+
} else {
|
|
764
|
+
for (const file of REQUIRED_CONTEXT_FILES) {
|
|
765
|
+
checkedItems++;
|
|
766
|
+
const filePath = path8.join(contextPath, file);
|
|
767
|
+
if (!fs7.existsSync(filePath)) {
|
|
768
|
+
errors.push(`Missing required context file: '${path8.join(CONTEXT_DIR, file)}'.`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
const decisionsPath = path8.join(projectRoot, DECISIONS_DIR);
|
|
773
|
+
checkedItems++;
|
|
774
|
+
if (!fs7.existsSync(decisionsPath)) {
|
|
775
|
+
errors.push(`Missing decisions directory at '${DECISIONS_DIR}'.`);
|
|
776
|
+
} else {
|
|
777
|
+
const files = fs7.readdirSync(decisionsPath).filter((f) => f.endsWith(".md"));
|
|
778
|
+
for (const f of files) {
|
|
779
|
+
checkedItems++;
|
|
780
|
+
if (!/^\d{4}-[\w-]+\.md$/.test(f)) {
|
|
781
|
+
warnings.push(`ADR file '${f}' does not follow the naming convention: '0001-slug-title.md'.`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const tasksPath = path8.join(projectRoot, TASKS_DIR);
|
|
786
|
+
checkedItems++;
|
|
787
|
+
if (!fs7.existsSync(tasksPath)) {
|
|
788
|
+
errors.push(`Missing tasks directory at '${TASKS_DIR}'.`);
|
|
789
|
+
} else {
|
|
790
|
+
for (const sub of TASKS_SUBDIRS) {
|
|
791
|
+
checkedItems++;
|
|
792
|
+
const subPath = path8.join(tasksPath, sub);
|
|
793
|
+
if (!fs7.existsSync(subPath)) {
|
|
794
|
+
errors.push(`Missing task subfolder: '${path8.join(TASKS_DIR, sub)}'.`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const reportsPath = path8.join(projectRoot, REPORTS_DIR);
|
|
799
|
+
checkedItems++;
|
|
800
|
+
if (!fs7.existsSync(reportsPath)) {
|
|
801
|
+
warnings.push(`Missing reports directory at '${REPORTS_DIR}'.`);
|
|
802
|
+
} else {
|
|
803
|
+
for (const sub of REPORTS_SUBDIRS) {
|
|
804
|
+
checkedItems++;
|
|
805
|
+
const subPath = path8.join(reportsPath, sub);
|
|
806
|
+
if (!fs7.existsSync(subPath)) {
|
|
807
|
+
warnings.push(`Missing report subfolder: '${path8.join(REPORTS_DIR, sub)}'.`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
valid: errors.length === 0,
|
|
813
|
+
errors,
|
|
814
|
+
warnings,
|
|
815
|
+
checkedItems
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/utils/format.ts
|
|
820
|
+
var isColorSupported = !process.env.NO_COLOR && (process.stdout?.isTTY ?? false);
|
|
821
|
+
var colors = {
|
|
822
|
+
reset: isColorSupported ? "\x1B[0m" : "",
|
|
823
|
+
bold: isColorSupported ? "\x1B[1m" : "",
|
|
824
|
+
dim: isColorSupported ? "\x1B[2m" : "",
|
|
825
|
+
italic: isColorSupported ? "\x1B[3m" : "",
|
|
826
|
+
underline: isColorSupported ? "\x1B[4m" : "",
|
|
827
|
+
red: isColorSupported ? "\x1B[31m" : "",
|
|
828
|
+
green: isColorSupported ? "\x1B[32m" : "",
|
|
829
|
+
yellow: isColorSupported ? "\x1B[33m" : "",
|
|
830
|
+
blue: isColorSupported ? "\x1B[34m" : "",
|
|
831
|
+
magenta: isColorSupported ? "\x1B[35m" : "",
|
|
832
|
+
cyan: isColorSupported ? "\x1B[36m" : "",
|
|
833
|
+
gray: isColorSupported ? "\x1B[90m" : ""
|
|
834
|
+
};
|
|
835
|
+
function success(msg) {
|
|
836
|
+
return `${colors.green}\u2714${colors.reset} ${msg}`;
|
|
837
|
+
}
|
|
838
|
+
function info(msg) {
|
|
839
|
+
return `${colors.cyan}\u2139${colors.reset} ${msg}`;
|
|
840
|
+
}
|
|
841
|
+
function warn(msg) {
|
|
842
|
+
return `${colors.yellow}\u26A0${colors.reset} ${msg}`;
|
|
843
|
+
}
|
|
844
|
+
function error(msg) {
|
|
845
|
+
return `${colors.red}\u2716${colors.reset} ${msg}`;
|
|
846
|
+
}
|
|
847
|
+
function title(msg) {
|
|
848
|
+
return `${colors.bold}${colors.cyan}${msg}${colors.reset}`;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/cli.ts
|
|
852
|
+
function runCli(argv) {
|
|
853
|
+
const args = argv.slice(2);
|
|
854
|
+
const command = args[0] || "help";
|
|
855
|
+
switch (command) {
|
|
856
|
+
case "init": {
|
|
857
|
+
handleInit(args.slice(1));
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
case "status": {
|
|
861
|
+
handleStatus(args.slice(1));
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
case "validate": {
|
|
865
|
+
handleValidate(args.slice(1));
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
case "adr": {
|
|
869
|
+
handleAdr(args.slice(1));
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
case "task": {
|
|
873
|
+
handleTask(args.slice(1));
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
case "question": {
|
|
877
|
+
handleQuestion(args.slice(1));
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
case "help":
|
|
881
|
+
case "--help":
|
|
882
|
+
case "-h":
|
|
883
|
+
printHelp();
|
|
884
|
+
break;
|
|
885
|
+
case "version":
|
|
886
|
+
case "--version":
|
|
887
|
+
case "-v":
|
|
888
|
+
console.log("aidp (AI Development Protocol CLI) v1.0.0");
|
|
889
|
+
break;
|
|
890
|
+
default:
|
|
891
|
+
console.error(error(`Unknown command: "${command}"`));
|
|
892
|
+
console.log(info("Run `aidp help` for a list of available commands."));
|
|
893
|
+
process.exit(1);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function handleInit(args) {
|
|
897
|
+
let targetDir = process.cwd();
|
|
898
|
+
let projectName;
|
|
899
|
+
let force = false;
|
|
900
|
+
for (let i = 0; i < args.length; i++) {
|
|
901
|
+
if (args[i] === "--force" || args[i] === "-f") {
|
|
902
|
+
force = true;
|
|
903
|
+
} else if (args[i] === "--name" || args[i] === "-n") {
|
|
904
|
+
projectName = args[++i];
|
|
905
|
+
} else if (!args[i].startsWith("-")) {
|
|
906
|
+
targetDir = path9.resolve(args[i]);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
console.log(title("Initializing AI Project Development Protocol..."));
|
|
910
|
+
const res = initProtocol({ targetDir, projectName, force });
|
|
911
|
+
if (res.createdFiles.length > 0) {
|
|
912
|
+
console.log(success(`Created ${res.createdFiles.length} file(s) in ${res.aiPath}:`));
|
|
913
|
+
for (const f of res.createdFiles) {
|
|
914
|
+
console.log(` + ${f}`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (res.skippedFiles.length > 0) {
|
|
918
|
+
console.log(warn(`Skipped ${res.skippedFiles.length} existing file(s) (use --force to overwrite):`));
|
|
919
|
+
for (const f of res.skippedFiles) {
|
|
920
|
+
console.log(` ~ ${f}`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
console.log(success("Initialization complete!"));
|
|
924
|
+
}
|
|
925
|
+
function handleStatus(args) {
|
|
926
|
+
const targetDir = args[0] ? path9.resolve(args[0]) : findProjectRoot();
|
|
927
|
+
const status = getProjectStatus(targetDir);
|
|
928
|
+
if (!status.isInitialized) {
|
|
929
|
+
console.log(warn(`No AI Project Development Protocol found at ${targetDir}`));
|
|
930
|
+
console.log(info("Run `aidp init` to set up the protocol."));
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
console.log(title(`=== ${status.name} ===`));
|
|
934
|
+
console.log(`${colors.bold}Current Phase:${colors.reset} ${status.currentPhase}`);
|
|
935
|
+
console.log(`${colors.bold}Current Goal:${colors.reset} ${status.currentGoal}`);
|
|
936
|
+
console.log();
|
|
937
|
+
const totalTasks = status.tasks.planned.length + status.tasks.active.length + status.tasks.completed.length;
|
|
938
|
+
console.log(`${colors.bold}Tasks (${totalTasks}):${colors.reset}`);
|
|
939
|
+
console.log(` ${colors.blue}Active (${status.tasks.active.length}):${colors.reset}`);
|
|
940
|
+
for (const t of status.tasks.active) {
|
|
941
|
+
console.log(` * [${t.id}] ${t.title}`);
|
|
942
|
+
}
|
|
943
|
+
console.log(` ${colors.yellow}Planned (${status.tasks.planned.length}):${colors.reset}`);
|
|
944
|
+
for (const t of status.tasks.planned) {
|
|
945
|
+
console.log(` * [${t.id}] ${t.title}`);
|
|
946
|
+
}
|
|
947
|
+
console.log(` ${colors.green}Completed (${status.tasks.completed.length}):${colors.reset}`);
|
|
948
|
+
for (const t of status.tasks.completed) {
|
|
949
|
+
console.log(` * [${t.id}] ${t.title}`);
|
|
950
|
+
}
|
|
951
|
+
console.log();
|
|
952
|
+
console.log(`${colors.bold}Architectural Decisions (${status.adrs.length}):${colors.reset}`);
|
|
953
|
+
if (status.adrs.length === 0) {
|
|
954
|
+
console.log(" (None recorded yet)");
|
|
955
|
+
} else {
|
|
956
|
+
for (const adr of status.adrs) {
|
|
957
|
+
console.log(` * ADR-${adr.formattedId}: ${adr.title} [${adr.status}] (${adr.date})`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
console.log();
|
|
961
|
+
console.log(`${colors.bold}Open Questions (${status.openQuestions.length}):${colors.reset}`);
|
|
962
|
+
if (status.openQuestions.length === 0) {
|
|
963
|
+
console.log(" (No open questions)");
|
|
964
|
+
} else {
|
|
965
|
+
for (const q of status.openQuestions) {
|
|
966
|
+
console.log(` ? ${q}`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function handleValidate(args) {
|
|
971
|
+
const targetDir = args[0] ? path9.resolve(args[0]) : findProjectRoot();
|
|
972
|
+
console.log(title("Validating AI Project Development Protocol structure..."));
|
|
973
|
+
const report = validateProtocol(targetDir);
|
|
974
|
+
if (report.warnings.length > 0) {
|
|
975
|
+
console.log(warn(`Warnings (${report.warnings.length}):`));
|
|
976
|
+
for (const w of report.warnings) {
|
|
977
|
+
console.log(` \u26A0 ${w}`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (report.errors.length > 0) {
|
|
981
|
+
console.log(error(`Errors (${report.errors.length}):`));
|
|
982
|
+
for (const e of report.errors) {
|
|
983
|
+
console.log(` \u2716 ${e}`);
|
|
984
|
+
}
|
|
985
|
+
console.log(error(`Validation FAILED with ${report.errors.length} error(s).`));
|
|
986
|
+
process.exit(1);
|
|
987
|
+
} else {
|
|
988
|
+
console.log(success(`Validation PASSED! Checked ${report.checkedItems} items.`));
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
function handleAdr(args) {
|
|
992
|
+
const sub = args[0];
|
|
993
|
+
if (sub === "list") {
|
|
994
|
+
const root = findProjectRoot();
|
|
995
|
+
const adrs = listAdrs(root);
|
|
996
|
+
console.log(title(`Architecture Decision Records (${adrs.length}):`));
|
|
997
|
+
if (adrs.length === 0) {
|
|
998
|
+
console.log(info("No ADRs found. Create one with `aidp adr new <title>`."));
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
for (const adr of adrs) {
|
|
1002
|
+
console.log(` ADR-${adr.formattedId} [${adr.status}] - ${adr.title} (${adr.date})`);
|
|
1003
|
+
}
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (sub === "new") {
|
|
1007
|
+
const titleParts = [];
|
|
1008
|
+
let status = "Accepted";
|
|
1009
|
+
for (let i = 1; i < args.length; i++) {
|
|
1010
|
+
if (args[i] === "--status" && args[i + 1]) {
|
|
1011
|
+
status = args[++i];
|
|
1012
|
+
} else {
|
|
1013
|
+
titleParts.push(args[i]);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
const titleStr = titleParts.join(" ").trim();
|
|
1017
|
+
if (!titleStr) {
|
|
1018
|
+
console.error(error("Please provide a title for the ADR: `aidp adr new <title>`"));
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
const record = createAdr({ title: titleStr, status });
|
|
1022
|
+
console.log(success(`Created ADR-${record.formattedId}: ${record.title}`));
|
|
1023
|
+
console.log(info(`File saved to: ${record.filePath}`));
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
console.error(error(`Unknown adr subcommand: "${sub}"`));
|
|
1027
|
+
console.log(info("Available subcommands: `aidp adr list`, `aidp adr new <title>`"));
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
function handleTask(args) {
|
|
1031
|
+
const sub = args[0];
|
|
1032
|
+
if (sub === "list") {
|
|
1033
|
+
const root = findProjectRoot();
|
|
1034
|
+
const tasks = listTasks(root);
|
|
1035
|
+
console.log(title("Project Tasks:"));
|
|
1036
|
+
for (const s of ["active", "planned", "completed"]) {
|
|
1037
|
+
console.log(`
|
|
1038
|
+
${colors.bold}${s.toUpperCase()} (${tasks[s].length}):${colors.reset}`);
|
|
1039
|
+
if (tasks[s].length === 0) {
|
|
1040
|
+
console.log(" (None)");
|
|
1041
|
+
} else {
|
|
1042
|
+
for (const t of tasks[s]) {
|
|
1043
|
+
console.log(` * [${t.id}] ${t.title}`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (sub === "new") {
|
|
1050
|
+
const titleParts = [];
|
|
1051
|
+
let status = "planned";
|
|
1052
|
+
let goal;
|
|
1053
|
+
for (let i = 1; i < args.length; i++) {
|
|
1054
|
+
if (args[i] === "--status" && args[i + 1]) {
|
|
1055
|
+
status = args[++i];
|
|
1056
|
+
} else if (args[i] === "--active") {
|
|
1057
|
+
status = "active";
|
|
1058
|
+
} else if (args[i] === "--goal" && args[i + 1]) {
|
|
1059
|
+
goal = args[++i];
|
|
1060
|
+
} else {
|
|
1061
|
+
titleParts.push(args[i]);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
const titleStr = titleParts.join(" ").trim();
|
|
1065
|
+
if (!titleStr) {
|
|
1066
|
+
console.error(error("Please provide a title for the task: `aidp task new <title>`"));
|
|
1067
|
+
process.exit(1);
|
|
1068
|
+
}
|
|
1069
|
+
const task = createTask({ title: titleStr, status, goal });
|
|
1070
|
+
console.log(success(`Created task [${task.id}] in ${task.status}: ${task.title}`));
|
|
1071
|
+
console.log(info(`File saved to: ${task.filePath}`));
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (sub === "move") {
|
|
1075
|
+
const taskId = args[1];
|
|
1076
|
+
const targetStatus = args[2];
|
|
1077
|
+
if (!taskId || !targetStatus) {
|
|
1078
|
+
console.error(error("Usage: `aidp task move <taskId> <planned|active|completed>`"));
|
|
1079
|
+
process.exit(1);
|
|
1080
|
+
}
|
|
1081
|
+
if (!["planned", "active", "completed"].includes(targetStatus)) {
|
|
1082
|
+
console.error(error(`Invalid status "${targetStatus}". Must be planned, active, or completed.`));
|
|
1083
|
+
process.exit(1);
|
|
1084
|
+
}
|
|
1085
|
+
try {
|
|
1086
|
+
const moved = moveTask(taskId, targetStatus);
|
|
1087
|
+
console.log(success(`Moved [${moved.id}] "${moved.title}" to ${moved.status}`));
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
console.error(error(err.message));
|
|
1090
|
+
process.exit(1);
|
|
1091
|
+
}
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
console.error(error(`Unknown task subcommand: "${sub}"`));
|
|
1095
|
+
console.log(info("Available subcommands: `aidp task list`, `aidp task new <title>`, `aidp task move <id> <status>`"));
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
function handleQuestion(args) {
|
|
1099
|
+
const sub = args[0];
|
|
1100
|
+
if (sub === "list") {
|
|
1101
|
+
const root = findProjectRoot();
|
|
1102
|
+
const questions = listOpenQuestions(root);
|
|
1103
|
+
console.log(title(`Open Questions (${questions.length}):`));
|
|
1104
|
+
if (questions.length === 0) {
|
|
1105
|
+
console.log(info("No open questions tracked."));
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
for (const q of questions) {
|
|
1109
|
+
console.log(` ? ${q}`);
|
|
1110
|
+
}
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (sub === "add") {
|
|
1114
|
+
const qStr = args.slice(1).join(" ").trim();
|
|
1115
|
+
if (!qStr) {
|
|
1116
|
+
console.error(error("Please provide a question: `aidp question add <question>`"));
|
|
1117
|
+
process.exit(1);
|
|
1118
|
+
}
|
|
1119
|
+
addOpenQuestion(qStr);
|
|
1120
|
+
console.log(success(`Added open question to requirements.md: "${qStr}"`));
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
console.error(error(`Unknown question subcommand: "${sub}"`));
|
|
1124
|
+
console.log(info("Available subcommands: `aidp question list`, `aidp question add <question>`"));
|
|
1125
|
+
process.exit(1);
|
|
1126
|
+
}
|
|
1127
|
+
function printHelp() {
|
|
1128
|
+
console.log(`
|
|
1129
|
+
${colors.bold}${colors.cyan}AI Project Development Protocol (AIDP) CLI${colors.reset}
|
|
1130
|
+
Version 1.0.0 \u2014 Tool-agnostic protocol automation and management
|
|
1131
|
+
|
|
1132
|
+
${colors.bold}USAGE:${colors.reset}
|
|
1133
|
+
aidp <command> [options]
|
|
1134
|
+
|
|
1135
|
+
${colors.bold}COMMANDS:${colors.reset}
|
|
1136
|
+
${colors.green}init [path]${colors.reset} Scaffold the .ai/ protocol directory & context files
|
|
1137
|
+
--name, -n <name> Set project name
|
|
1138
|
+
--force, -f Overwrite existing files
|
|
1139
|
+
|
|
1140
|
+
${colors.green}status [path]${colors.reset} Show project state, active tasks, ADRs, questions
|
|
1141
|
+
${colors.green}validate [path]${colors.reset} Verify protocol directory structure & context
|
|
1142
|
+
|
|
1143
|
+
${colors.green}adr list${colors.reset} List all Architecture Decision Records
|
|
1144
|
+
${colors.green}adr new <title> [options]${colors.reset} Create a new numbered ADR
|
|
1145
|
+
--status <status> Status (Proposed | Accepted | Rejected | Superseded)
|
|
1146
|
+
|
|
1147
|
+
${colors.green}task list${colors.reset} List all tasks by lifecycle status
|
|
1148
|
+
${colors.green}task new <title> [options]${colors.reset} Create a new task specification
|
|
1149
|
+
--active Set status to active (default: planned)
|
|
1150
|
+
--goal <text> Set task goal
|
|
1151
|
+
${colors.green}task move <id> <status>${colors.reset} Move task between planned | active | completed
|
|
1152
|
+
|
|
1153
|
+
${colors.green}question list${colors.reset} List open questions from requirements.md
|
|
1154
|
+
${colors.green}question add <question>${colors.reset} Add a new open question to requirements.md
|
|
1155
|
+
|
|
1156
|
+
${colors.green}help, --help${colors.reset} Show this help message
|
|
1157
|
+
${colors.green}version, --version${colors.reset} Display version number
|
|
1158
|
+
`);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// bin/cli.ts
|
|
1162
|
+
runCli(process.argv);
|
|
1163
|
+
//# sourceMappingURL=cli.js.map
|