@mthanhlm/autodev 0.4.3 → 0.5.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.
Files changed (73) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/PUBLISH.md +9 -40
  3. package/README.md +70 -91
  4. package/autodev/bin/autodev-tools.cjs +587 -811
  5. package/autodev/templates/brief.md +19 -0
  6. package/autodev/templates/context.md +16 -0
  7. package/autodev/templates/plan.md +26 -46
  8. package/autodev/templates/run.md +20 -0
  9. package/bin/install.js +229 -342
  10. package/commands/autodev/index.md +117 -9
  11. package/commands/autodev/status.md +22 -0
  12. package/hooks/autodev-auto-format.js +3 -3
  13. package/hooks/autodev-git-guard.js +5 -7
  14. package/hooks/autodev-paths.js +3 -3
  15. package/package.json +4 -5
  16. package/scripts/run-tests.cjs +10 -0
  17. package/agents/autodev-codebase-domain.md +0 -25
  18. package/agents/autodev-codebase-quality.md +0 -25
  19. package/agents/autodev-codebase-runtime.md +0 -25
  20. package/agents/autodev-codebase-structure.md +0 -25
  21. package/agents/autodev-review-integration.md +0 -30
  22. package/agents/autodev-review-polish.md +0 -30
  23. package/agents/autodev-review-quality.md +0 -30
  24. package/agents/autodev-review-security.md +0 -30
  25. package/agents/autodev-task-worker.md +0 -39
  26. package/autodev/templates/codebase/domain.md +0 -13
  27. package/autodev/templates/codebase/quality.md +0 -13
  28. package/autodev/templates/codebase/runtime.md +0 -13
  29. package/autodev/templates/codebase/structure.md +0 -13
  30. package/autodev/templates/codebase/summary.md +0 -13
  31. package/autodev/templates/config.json +0 -22
  32. package/autodev/templates/project-state.md +0 -13
  33. package/autodev/templates/project.md +0 -24
  34. package/autodev/templates/requirements.md +0 -14
  35. package/autodev/templates/review.md +0 -27
  36. package/autodev/templates/roadmap.md +0 -17
  37. package/autodev/templates/state.md +0 -13
  38. package/autodev/templates/summary.md +0 -22
  39. package/autodev/templates/task-summary.md +0 -18
  40. package/autodev/templates/task.md +0 -23
  41. package/autodev/templates/track-state.md +0 -14
  42. package/autodev/templates/track.md +0 -24
  43. package/autodev/templates/uat.md +0 -18
  44. package/autodev/workflows/autodev.md +0 -79
  45. package/autodev/workflows/cleanup.md +0 -51
  46. package/autodev/workflows/execute-phase.md +0 -127
  47. package/autodev/workflows/explore-codebase.md +0 -66
  48. package/autodev/workflows/help.md +0 -110
  49. package/autodev/workflows/new-project.md +0 -101
  50. package/autodev/workflows/plan-phase.md +0 -126
  51. package/autodev/workflows/progress.md +0 -18
  52. package/autodev/workflows/review-phase.md +0 -73
  53. package/autodev/workflows/review-plan.md +0 -55
  54. package/autodev/workflows/review-task.md +0 -70
  55. package/autodev/workflows/verify-work.md +0 -57
  56. package/commands/autodev/cleanup.md +0 -23
  57. package/commands/autodev/execute-phase.md +0 -29
  58. package/commands/autodev/explore-codebase.md +0 -33
  59. package/commands/autodev/help.md +0 -18
  60. package/commands/autodev/new-project.md +0 -30
  61. package/commands/autodev/plan-phase.md +0 -26
  62. package/commands/autodev/progress.md +0 -18
  63. package/commands/autodev/review-phase.md +0 -29
  64. package/commands/autodev/review-task.md +0 -25
  65. package/commands/autodev/verify-work.md +0 -24
  66. package/hooks/autodev-context-monitor.js +0 -59
  67. package/hooks/autodev-phase-boundary.sh +0 -49
  68. package/hooks/autodev-prompt-guard.js +0 -55
  69. package/hooks/autodev-read-guard.js +0 -42
  70. package/hooks/autodev-session-state.sh +0 -51
  71. package/hooks/autodev-statusline.js +0 -78
  72. package/hooks/autodev-workflow-guard.js +0 -43
  73. package/hooks/hooks.json +0 -89
@@ -3,31 +3,119 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
- const DEFAULT_CONFIG = {
7
- project: {
8
- type: null
9
- },
10
- workflow: {
11
- research: false,
12
- review_after_execute: true
13
- },
14
- execution: {},
15
- git: {
16
- mode: 'read-only'
17
- },
18
- hooks: {
6
+ const DEFAULT_STATE = {
7
+ version: 1,
8
+ project_type: null,
9
+ stage: 'briefing',
10
+ current_item: null,
11
+ last_result: 'unverified',
12
+ last_run_at: null,
13
+ last_run_path: null,
14
+ settings: {
15
+ git_mode: 'read-only',
19
16
  auto_format: true,
20
- context_warnings: true,
21
- read_guard: true,
22
- workflow_guard: true,
23
- prompt_guard: true,
24
- git_guard: true,
25
- session_state: true,
26
- phase_boundary: true
17
+ background_tasks_disabled: true
27
18
  }
28
19
  };
29
20
 
30
- const CODEBASE_FILES = ['structure.md', 'domain.md', 'runtime.md', 'quality.md', 'summary.md'];
21
+ const CODE_FILE_EXTENSIONS = new Set([
22
+ '.js', '.jsx', '.cjs', '.mjs',
23
+ '.ts', '.tsx',
24
+ '.py',
25
+ '.go',
26
+ '.rs',
27
+ '.java',
28
+ '.rb',
29
+ '.php',
30
+ '.cs',
31
+ '.cpp',
32
+ '.cc',
33
+ '.c',
34
+ '.h',
35
+ '.hpp',
36
+ '.swift',
37
+ '.kt',
38
+ '.kts',
39
+ '.scala'
40
+ ]);
41
+
42
+ const NESTED_CODE_MARKER_FILES = new Set([
43
+ 'package.json',
44
+ 'package-lock.json',
45
+ 'pnpm-lock.yaml',
46
+ 'yarn.lock',
47
+ 'tsconfig.json',
48
+ 'jsconfig.json',
49
+ 'pyproject.toml',
50
+ 'requirements.txt',
51
+ 'Pipfile',
52
+ 'Cargo.toml',
53
+ 'go.mod',
54
+ 'pom.xml',
55
+ 'build.gradle',
56
+ 'Dockerfile'
57
+ ]);
58
+
59
+ const IGNORED_SCAN_DIRS = new Set([
60
+ '.autodev',
61
+ '.claude',
62
+ '.git',
63
+ 'node_modules',
64
+ 'dist',
65
+ 'build',
66
+ 'coverage',
67
+ 'tmp',
68
+ 'vendor'
69
+ ]);
70
+
71
+ function fileExists(filePath) {
72
+ try {
73
+ return fs.existsSync(filePath);
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ function isFile(filePath) {
80
+ try {
81
+ return fs.statSync(filePath).isFile();
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ function ensureDir(dirPath) {
88
+ fs.mkdirSync(dirPath, { recursive: true });
89
+ }
90
+
91
+ function readText(filePath) {
92
+ try {
93
+ return fs.readFileSync(filePath, 'utf8');
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ function readJson(filePath, fallback = null) {
100
+ try {
101
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
102
+ } catch {
103
+ return fallback;
104
+ }
105
+ }
106
+
107
+ function writeJson(filePath, value) {
108
+ ensureDir(path.dirname(filePath));
109
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
110
+ }
111
+
112
+ function slugify(value) {
113
+ return String(value || 'run')
114
+ .toLowerCase()
115
+ .replace(/[^a-z0-9]+/g, '-')
116
+ .replace(/^-+|-+$/g, '')
117
+ .replace(/-+/g, '-') || 'run';
118
+ }
31
119
 
32
120
  function findWorkspaceRoot(startDir) {
33
121
  let cursor = path.resolve(startDir || process.cwd());
@@ -62,70 +150,63 @@ function rootPaths(cwd) {
62
150
  return {
63
151
  workspaceRoot,
64
152
  root,
65
- config: path.join(root, 'config.json'),
66
- project: path.join(root, 'PROJECT.md'),
67
- state: path.join(root, 'STATE.md'),
68
- activeTrack: path.join(root, 'ACTIVE_TRACK'),
69
- codebaseDir: path.join(root, 'codebase'),
70
- codebaseSummary: path.join(root, 'codebase', 'summary.md'),
71
- tracksDir: path.join(root, 'tracks')
153
+ brief: path.join(root, 'brief.md'),
154
+ context: path.join(root, 'context.md'),
155
+ plan: path.join(root, 'plan.md'),
156
+ state: path.join(root, 'state.json'),
157
+ runsDir: path.join(root, 'runs'),
158
+ legacyDir: path.join(root, 'legacy')
72
159
  };
73
160
  }
74
161
 
75
- function readText(filePath) {
76
- try {
77
- return fs.readFileSync(filePath, 'utf8');
78
- } catch {
79
- return null;
80
- }
162
+ function codeFileNameLooksReal(entryName) {
163
+ return CODE_FILE_EXTENSIONS.has(path.extname(entryName).toLowerCase());
81
164
  }
82
165
 
83
- function readJson(filePath, fallback = null) {
84
- try {
85
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
86
- } catch {
87
- return fallback;
166
+ function directoryContainsCodeMarker(dirPath, depth = 2) {
167
+ if (depth < 0 || !fileExists(dirPath)) {
168
+ return false;
88
169
  }
89
- }
90
170
 
91
- function fileExists(filePath) {
171
+ let entries;
92
172
  try {
93
- return fs.existsSync(filePath);
173
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
94
174
  } catch {
95
175
  return false;
96
176
  }
97
- }
98
177
 
99
- function slugify(value) {
100
- return String(value)
101
- .toLowerCase()
102
- .replace(/[^a-z0-9]+/g, '-')
103
- .replace(/^-+|-+$/g, '')
104
- .replace(/-+/g, '-') || 'track';
105
- }
178
+ for (const entry of entries) {
179
+ if (entry.name.startsWith('.')) {
180
+ continue;
181
+ }
182
+
183
+ const fullPath = path.join(dirPath, entry.name);
184
+ if (entry.isFile()) {
185
+ if (codeFileNameLooksReal(entry.name) || NESTED_CODE_MARKER_FILES.has(entry.name)) {
186
+ return true;
187
+ }
188
+ continue;
189
+ }
190
+
191
+ if (!entry.isDirectory() || depth === 0 || IGNORED_SCAN_DIRS.has(entry.name)) {
192
+ continue;
193
+ }
106
194
 
107
- function padPhase(number) {
108
- return String(number).padStart(2, '0');
195
+ if (directoryContainsCodeMarker(fullPath, depth - 1)) {
196
+ return true;
197
+ }
198
+ }
199
+
200
+ return false;
109
201
  }
110
202
 
111
203
  function detectExistingCodebase(cwd) {
112
204
  const workspaceRoot = findWorkspaceRoot(cwd);
113
- const knownFiles = [
114
- 'package.json',
115
- 'package-lock.json',
116
- 'pnpm-lock.yaml',
117
- 'yarn.lock',
118
- 'tsconfig.json',
119
- 'jsconfig.json',
120
- 'pyproject.toml',
121
- 'requirements.txt',
122
- 'Pipfile',
123
- 'Cargo.toml',
124
- 'go.mod',
125
- 'pom.xml',
126
- 'build.gradle',
127
- 'Dockerfile'
128
- ];
205
+
206
+ if ([...NESTED_CODE_MARKER_FILES].some(name => fileExists(path.join(workspaceRoot, name)))) {
207
+ return true;
208
+ }
209
+
129
210
  const knownDirs = [
130
211
  'src',
131
212
  'app',
@@ -141,10 +222,6 @@ function detectExistingCodebase(cwd) {
141
222
  '__tests__'
142
223
  ];
143
224
 
144
- if (knownFiles.some(name => fileExists(path.join(workspaceRoot, name)))) {
145
- return true;
146
- }
147
-
148
225
  if (knownDirs.some(name => fileExists(path.join(workspaceRoot, name)))) {
149
226
  return true;
150
227
  }
@@ -152,842 +229,526 @@ function detectExistingCodebase(cwd) {
152
229
  try {
153
230
  const entries = fs.readdirSync(workspaceRoot, { withFileTypes: true });
154
231
  return entries.some(entry => {
155
- if (entry.name === '.autodev' || entry.name === '.claude' || entry.name.startsWith('.')) {
232
+ if (entry.name.startsWith('.') || IGNORED_SCAN_DIRS.has(entry.name)) {
156
233
  return false;
157
234
  }
158
- return entry.isDirectory() || /\.(js|ts|tsx|jsx|py|go|rs|java|rb|php|cs|cpp|c|swift|kt|sql|json|yaml|yml|md)$/.test(entry.name);
235
+
236
+ if (entry.isFile()) {
237
+ return codeFileNameLooksReal(entry.name);
238
+ }
239
+
240
+ if (!entry.isDirectory()) {
241
+ return false;
242
+ }
243
+
244
+ return directoryContainsCodeMarker(path.join(workspaceRoot, entry.name));
159
245
  });
160
246
  } catch {
161
247
  return false;
162
248
  }
163
249
  }
164
250
 
165
- function mergeSection(defaultValue, actualValue) {
166
- if (defaultValue && typeof defaultValue === 'object' && !Array.isArray(defaultValue)) {
167
- return {
168
- ...defaultValue,
169
- ...((actualValue && typeof actualValue === 'object' && !Array.isArray(actualValue)) ? actualValue : {})
170
- };
251
+ function normalizeProjectType(value, fallbackCwd = process.cwd()) {
252
+ if (value === 'brownfield' || value === 'greenfield') {
253
+ return value;
171
254
  }
172
- return actualValue !== undefined ? actualValue : defaultValue;
255
+ return detectExistingCodebase(fallbackCwd) ? 'brownfield' : 'greenfield';
173
256
  }
174
257
 
175
- function loadConfig(cwd) {
176
- const config = readJson(rootPaths(cwd).config, {}) || {};
177
- return {
178
- project: mergeSection(DEFAULT_CONFIG.project, config.project),
179
- workflow: mergeSection(DEFAULT_CONFIG.workflow, config.workflow),
180
- execution: mergeSection(DEFAULT_CONFIG.execution, config.execution),
181
- git: mergeSection(DEFAULT_CONFIG.git, config.git),
182
- hooks: mergeSection(DEFAULT_CONFIG.hooks, config.hooks)
183
- };
258
+ function normalizeStage(value) {
259
+ const allowed = new Set(['briefing', 'context', 'planning', 'execution', 'blocked', 'complete']);
260
+ return allowed.has(value) ? value : DEFAULT_STATE.stage;
184
261
  }
185
262
 
186
- function readActiveTrack(cwd) {
187
- const value = readText(rootPaths(cwd).activeTrack);
188
- return value ? value.trim() || null : null;
189
- }
263
+ function normalizeCurrentItem(value) {
264
+ if (value === null || value === undefined || value === '' || value === 'none') {
265
+ return null;
266
+ }
190
267
 
191
- function trackDir(cwd, slug) {
192
- return path.join(rootPaths(cwd).tracksDir, slug);
268
+ const raw = String(value).trim();
269
+ return /^\d+$/.test(raw) ? raw.padStart(2, '0') : null;
193
270
  }
194
271
 
195
- function trackPaths(cwd, slug = readActiveTrack(cwd)) {
196
- if (!slug) {
272
+ function loadState(cwd) {
273
+ const paths = rootPaths(cwd);
274
+ const state = readJson(paths.state, null);
275
+ if (!state || typeof state !== 'object' || Array.isArray(state)) {
197
276
  return null;
198
277
  }
199
278
 
200
- const dir = trackDir(cwd, slug);
201
279
  return {
202
- slug,
203
- dir,
204
- track: path.join(dir, 'TRACK.md'),
205
- requirements: path.join(dir, 'REQUIREMENTS.md'),
206
- roadmap: path.join(dir, 'ROADMAP.md'),
207
- state: path.join(dir, 'STATE.md'),
208
- phasesDir: path.join(dir, 'phases')
280
+ ...DEFAULT_STATE,
281
+ ...state,
282
+ project_type: normalizeProjectType(state.project_type, cwd),
283
+ stage: normalizeStage(state.stage),
284
+ current_item: normalizeCurrentItem(state.current_item),
285
+ settings: {
286
+ ...DEFAULT_STATE.settings,
287
+ ...((state.settings && typeof state.settings === 'object') ? state.settings : {})
288
+ }
209
289
  };
210
290
  }
211
291
 
212
- function listTracks(cwd) {
213
- const tracksDir = rootPaths(cwd).tracksDir;
214
- if (!fileExists(tracksDir)) {
215
- return [];
216
- }
217
-
218
- return fs.readdirSync(tracksDir, { withFileTypes: true })
219
- .filter(entry => entry.isDirectory())
220
- .map(entry => {
221
- const paths = trackPaths(cwd, entry.name);
222
- const trackName = (() => {
223
- const content = readText(paths.track);
224
- const match = content && content.match(/^#\s+Track:\s*(.+)$/m);
225
- return match ? match[1].trim() : entry.name;
226
- })();
227
-
228
- return {
229
- slug: entry.name,
230
- name: trackName,
231
- paths,
232
- active: readActiveTrack(cwd) === entry.name
233
- };
234
- })
235
- .sort((left, right) => left.slug.localeCompare(right.slug));
292
+ function saveState(cwd, state) {
293
+ const paths = rootPaths(cwd);
294
+ writeJson(paths.state, {
295
+ ...DEFAULT_STATE,
296
+ ...state,
297
+ project_type: normalizeProjectType(state.project_type, cwd),
298
+ stage: normalizeStage(state.stage),
299
+ current_item: normalizeCurrentItem(state.current_item),
300
+ settings: {
301
+ ...DEFAULT_STATE.settings,
302
+ ...((state.settings && typeof state.settings === 'object') ? state.settings : {})
303
+ }
304
+ });
236
305
  }
237
306
 
238
- function parseRoadmap(content) {
239
- const phases = [];
240
- if (!content) {
241
- return phases;
307
+ function archiveLegacyState(cwd) {
308
+ const paths = rootPaths(cwd);
309
+ if (!fileExists(paths.root) || fileExists(paths.state)) {
310
+ return null;
242
311
  }
243
312
 
244
- const matcher = /^##\s+Phase\s+(\d+)(?:\s+\[([a-z-]+)\])?\s*:\s*(.+)$/gm;
245
- let match;
246
- while ((match = matcher.exec(content)) !== null) {
247
- phases.push({
248
- number: Number(match[1]),
249
- type: match[2] || 'feature',
250
- name: match[3].trim()
251
- });
252
- }
253
- return phases;
254
- }
313
+ const entries = fs.readdirSync(paths.root).filter(entry => entry !== 'legacy');
314
+ const legacyMarkers = new Set([
315
+ 'ACTIVE_TRACK',
316
+ 'PROJECT.md',
317
+ 'STATE.md',
318
+ 'TRACK.md',
319
+ 'codebase',
320
+ 'tracks'
321
+ ]);
255
322
 
256
- function phasePaths(cwd, slug, phase) {
257
- const track = trackPaths(cwd, slug);
258
- if (!track) {
323
+ if (!entries.some(entry => legacyMarkers.has(entry) || /\.(md|json)$/.test(entry))) {
259
324
  return null;
260
325
  }
261
326
 
262
- const prefix = padPhase(phase.number);
263
- const dirName = `${prefix}-${phase.type}-${slugify(phase.name)}`;
264
- const dir = path.join(track.phasesDir, dirName);
265
- return {
266
- ...phase,
267
- padded: prefix,
268
- dir,
269
- planPath: path.join(dir, `${prefix}-PLAN.md`),
270
- summaryPath: path.join(dir, `${prefix}-SUMMARY.md`),
271
- reviewPath: path.join(dir, `${prefix}-REVIEW.md`),
272
- uatPath: path.join(dir, `${prefix}-UAT.md`)
273
- };
274
- }
327
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
328
+ const archiveDir = path.join(paths.legacyDir, timestamp);
329
+ ensureDir(archiveDir);
275
330
 
276
- function readSingleLineField(content, label) {
277
- if (!content) {
278
- return null;
331
+ for (const entry of entries) {
332
+ fs.renameSync(path.join(paths.root, entry), path.join(archiveDir, entry));
279
333
  }
280
- const match = content.match(new RegExp(`^${label}:\\s*(.+)$`, 'mi'));
281
- return match ? match[1].trim() : null;
334
+
335
+ return archiveDir;
282
336
  }
283
337
 
284
- function parseStateSnapshot(content) {
285
- if (!content) {
286
- return null;
287
- }
338
+ function bootstrapProject(cwd, options = {}) {
339
+ const paths = rootPaths(cwd);
340
+ const archivedLegacyPath = archiveLegacyState(cwd);
341
+ ensureDir(paths.root);
342
+ ensureDir(paths.runsDir);
343
+
344
+ const current = loadState(cwd);
345
+ const nextState = current || {
346
+ ...DEFAULT_STATE,
347
+ project_type: normalizeProjectType(options.projectType, cwd)
348
+ };
288
349
 
289
- const currentPhaseRaw = readSingleLineField(content, 'Current Phase');
290
- const currentPhaseNumber = currentPhaseRaw && /^\d+$/.test(currentPhaseRaw)
291
- ? Number(currentPhaseRaw)
292
- : null;
350
+ saveState(cwd, nextState);
293
351
 
294
352
  return {
295
- currentPhase: currentPhaseNumber,
296
- currentPhaseType: readSingleLineField(content, 'Current Phase Type'),
297
- currentStep: readSingleLineField(content, 'Current Step'),
298
- currentTask: readSingleLineField(content, 'Current Task'),
299
- currentTaskStatus: readSingleLineField(content, 'Current Task Status'),
300
- status: readSingleLineField(content, 'Status'),
301
- nextCommand: readSingleLineField(content, 'Next Command')
353
+ cwd,
354
+ workspace_root: paths.workspaceRoot,
355
+ autodev_root: paths.root,
356
+ state_path: paths.state,
357
+ brief_path: paths.brief,
358
+ context_path: paths.context,
359
+ plan_path: paths.plan,
360
+ runs_dir: paths.runsDir,
361
+ existing_code_detected: detectExistingCodebase(cwd),
362
+ archived_legacy_path: archivedLegacyPath,
363
+ state: loadState(cwd)
302
364
  };
303
365
  }
304
366
 
305
- function readStateSnapshot(filePath) {
306
- return parseStateSnapshot(readText(filePath));
367
+ function itemIdFromTitleNumber(value) {
368
+ return String(value).padStart(2, '0');
307
369
  }
308
370
 
309
- function isBlockedTaskStatus(value) {
310
- return typeof value === 'string' && /^blocked/i.test(value.trim());
311
- }
312
-
313
- function parseTaskDependsOn(value) {
314
- if (!value || /^none$/i.test(value)) {
315
- return [];
316
- }
317
-
318
- return String(value)
319
- .split(/[,\s]+/)
371
+ function parseInlineList(value) {
372
+ return String(value || '')
373
+ .split(',')
320
374
  .map(part => part.trim())
321
- .filter(Boolean)
322
- .map(part => {
323
- const match = part.match(/(\d+)/);
324
- return match ? Number(match[1]) : null;
325
- })
326
- .filter(number => Number.isFinite(number));
375
+ .filter(Boolean);
327
376
  }
328
377
 
329
- function readTaskTitle(content, fallbackNumber) {
330
- if (!content) {
331
- return `Task ${String(fallbackNumber).padStart(2, '0')}`;
332
- }
333
-
334
- const heading = content.match(/^#\s+Task\s+\d+\s*:\s*(.+)$/mi);
335
- if (heading) {
336
- return heading[1].trim();
337
- }
338
-
339
- const generic = content.match(/^#\s+(.+)$/m);
340
- return generic ? generic[1].trim() : `Task ${String(fallbackNumber).padStart(2, '0')}`;
341
- }
342
-
343
- function listTasksForPhaseDetails(phaseDetails) {
344
- if (!phaseDetails || !fileExists(phaseDetails.dir)) {
345
- return [];
378
+ function parseItemSection(sectionContent) {
379
+ const lines = sectionContent.split('\n');
380
+ const titleMatch = lines[0].match(/^##\s+Item\s+(\d+)\s*:\s*(.+)$/);
381
+ if (!titleMatch) {
382
+ return null;
346
383
  }
347
384
 
348
- const entries = fs.readdirSync(phaseDetails.dir, { withFileTypes: true })
349
- .filter(entry => entry.isFile())
350
- .map(entry => entry.name);
351
-
352
- const tasks = entries
353
- .map(name => {
354
- const match = name.match(/^TASK-(\d+)\.md$/);
355
- if (!match) {
356
- return null;
357
- }
358
-
359
- const number = Number(match[1]);
360
- const padded = String(number).padStart(2, '0');
361
- const taskPath = path.join(phaseDetails.dir, name);
362
- const summaryPath = path.join(phaseDetails.dir, `TASK-${padded}-SUMMARY.md`);
363
- const content = readText(taskPath);
364
- const status = readSingleLineField(content, 'Status') || 'pending';
365
- const dependsOn = parseTaskDependsOn(readSingleLineField(content, 'Depends On'));
366
- const summaryExists = fileExists(summaryPath);
367
-
368
- return {
369
- number,
370
- padded,
371
- title: readTaskTitle(content, number),
372
- status,
373
- dependsOn,
374
- taskPath,
375
- summaryPath,
376
- summaryExists
377
- };
378
- })
379
- .filter(Boolean)
380
- .sort((left, right) => left.number - right.number);
381
-
382
- const doneSet = new Set(tasks.filter(task => task.summaryExists).map(task => task.number));
383
-
384
- return tasks.map(task => ({
385
- ...task,
386
- ready: !task.summaryExists && task.dependsOn.every(dep => doneSet.has(dep)),
387
- effectiveStatus: task.summaryExists ? 'done' : task.status
388
- }));
389
- }
390
-
391
- function nextExecutableTask(tasks) {
392
- return tasks.find(task => !task.summaryExists && task.ready) || null;
393
- }
394
-
395
- function lastCompletedTask(tasks) {
396
- const completed = tasks.filter(task => task.summaryExists);
397
- return completed.length > 0 ? completed[completed.length - 1] : null;
398
- }
399
-
400
- function lastTaskNumber(tasks) {
401
- return tasks.reduce((highest, task) => Math.max(highest, task.number), 0);
402
- }
403
-
404
- function hasDependencyDeadlock(tasks) {
405
- return tasks.some(task => !task.summaryExists) && !nextExecutableTask(tasks);
406
- }
385
+ const item = {
386
+ id: itemIdFromTitleNumber(titleMatch[1]),
387
+ number: Number(titleMatch[1]),
388
+ title: titleMatch[2].trim(),
389
+ status: 'pending',
390
+ goal: '',
391
+ files: [],
392
+ acceptance: [],
393
+ verification: []
394
+ };
407
395
 
408
- function listPhases(cwd, slug = readActiveTrack(cwd)) {
409
- const track = trackPaths(cwd, slug);
410
- if (!track || !fileExists(track.roadmap)) {
411
- return [];
412
- }
396
+ let activeSection = null;
413
397
 
414
- return parseRoadmap(readText(track.roadmap)).map(phase => {
415
- const details = phasePaths(cwd, slug, phase);
416
- const planExists = fileExists(details.planPath);
417
- const summaryExists = fileExists(details.summaryPath);
418
- const reviewExists = fileExists(details.reviewPath);
419
- const uatExists = fileExists(details.uatPath);
420
- const tasks = listTasksForPhaseDetails(details);
421
- const taskCount = tasks.length;
422
- const taskDoneCount = tasks.filter(task => task.summaryExists).length;
423
- const status = uatExists
424
- ? 'verified'
425
- : reviewExists
426
- ? 'reviewed'
427
- : summaryExists
428
- ? 'executed'
429
- : planExists && taskCount > 0 && taskDoneCount === taskCount
430
- ? 'tasks_complete'
431
- : planExists && taskDoneCount > 0
432
- ? 'executing'
433
- : planExists
434
- ? 'planned'
435
- : 'pending';
398
+ for (let index = 1; index < lines.length; index += 1) {
399
+ const line = lines[index];
400
+ const trimmed = line.trim();
436
401
 
437
- return {
438
- ...details,
439
- trackSlug: slug,
440
- planExists,
441
- summaryExists,
442
- reviewExists,
443
- uatExists,
444
- taskCount,
445
- taskDoneCount,
446
- nextTask: nextExecutableTask(tasks)?.number || null,
447
- status
448
- };
449
- });
450
- }
451
-
452
- function resolvePhase(cwd, slug, requestedPhase, mode) {
453
- const phases = listPhases(cwd, slug);
454
- if (phases.length === 0) {
455
- return null;
456
- }
402
+ if (!trimmed) {
403
+ continue;
404
+ }
457
405
 
458
- if (requestedPhase) {
459
- const numeric = Number(requestedPhase);
460
- return phases.find(phase => phase.number === numeric) || null;
461
- }
406
+ const statusMatch = trimmed.match(/^Status:\s*(.+)$/);
407
+ if (statusMatch) {
408
+ const rawStatus = statusMatch[1].trim().toLowerCase();
409
+ item.status = ['pending', 'in_progress', 'done', 'blocked'].includes(rawStatus)
410
+ ? rawStatus
411
+ : 'pending';
412
+ activeSection = null;
413
+ continue;
414
+ }
462
415
 
463
- const track = trackPaths(cwd, slug);
464
- const trackState = track ? readStateSnapshot(track.state) : null;
465
- const currentStatePhase = trackState?.currentPhase
466
- ? phases.find(phase => phase.number === trackState.currentPhase) || null
467
- : null;
416
+ const goalMatch = trimmed.match(/^Goal:\s*(.+)$/);
417
+ if (goalMatch) {
418
+ item.goal = goalMatch[1].trim();
419
+ activeSection = null;
420
+ continue;
421
+ }
468
422
 
469
- if (mode === 'plan') {
470
- if (currentStatePhase && (
471
- isBlockedTaskStatus(trackState.currentTaskStatus)
472
- || trackState.currentStep === 'planning'
473
- || trackState.currentStep === 'plan_review'
474
- )) {
475
- return currentStatePhase;
423
+ const filesMatch = trimmed.match(/^Files:\s*(.+)$/);
424
+ if (filesMatch) {
425
+ item.files = parseInlineList(filesMatch[1]);
426
+ activeSection = null;
427
+ continue;
476
428
  }
477
- return phases.find(phase => !phase.planExists) || phases[0];
478
- }
479
429
 
480
- if (mode === 'execute') {
481
- if (currentStatePhase && (
482
- trackState?.currentStep === 'execution'
483
- || trackState?.currentStep === 'task_review'
484
- )) {
485
- return currentStatePhase;
430
+ const sectionMatch = trimmed.match(/^(Files|Acceptance|Verification):\s*$/);
431
+ if (sectionMatch) {
432
+ activeSection = sectionMatch[1].toLowerCase();
433
+ continue;
486
434
  }
487
- return phases.find(phase => phase.planExists && !phase.summaryExists) || null;
488
- }
489
435
 
490
- if (mode === 'task_review') {
491
- if (currentStatePhase && trackState?.currentStep === 'task_review') {
492
- return currentStatePhase;
436
+ if (!trimmed.startsWith('- ')) {
437
+ activeSection = null;
438
+ continue;
493
439
  }
494
- return phases.find(phase => phase.planExists && !phase.summaryExists && phase.taskDoneCount > 0) || null;
495
- }
496
440
 
497
- if (mode === 'review') {
498
- if (currentStatePhase && trackState?.currentStep === 'review') {
499
- return currentStatePhase;
441
+ const value = trimmed.slice(2).trim();
442
+ if (!value) {
443
+ continue;
500
444
  }
501
- return phases.find(phase => phase.summaryExists && !phase.reviewExists) || null;
502
- }
503
445
 
504
- if (mode === 'verify') {
505
- if (currentStatePhase && trackState?.currentStep === 'verification') {
506
- return currentStatePhase;
446
+ if (activeSection === 'files') {
447
+ item.files.push(value);
448
+ } else if (activeSection === 'acceptance') {
449
+ item.acceptance.push(value);
450
+ } else if (activeSection === 'verification') {
451
+ item.verification.push(value);
507
452
  }
508
- return phases.find(phase => phase.reviewExists && !phase.uatExists)
509
- || [...phases].reverse().find(phase => phase.reviewExists)
510
- || null;
511
453
  }
512
454
 
513
- return phases[0];
455
+ return item;
514
456
  }
515
457
 
516
- function buildRoute(cwd) {
517
- const paths = rootPaths(cwd);
518
- const config = loadConfig(cwd);
519
- const existingCodebase = detectExistingCodebase(cwd);
520
- const projectType = config.project.type || (existingCodebase ? 'brownfield' : 'greenfield');
521
- const projectExists = fileExists(paths.project);
522
- const codebaseMapExists = fileExists(paths.codebaseSummary);
523
- const activeTrack = readActiveTrack(cwd);
524
- const tracks = listTracks(cwd);
525
-
526
- if (!projectExists) {
458
+ function parsePlan(content) {
459
+ if (!content) {
527
460
  return {
528
- kind: 'init_project',
529
- command: '/autodev',
530
- manualCommand: '/autodev-new-project',
531
- reason: existingCodebase ? 'existing_code_detected_without_project_state' : 'project_state_missing',
532
- projectType
461
+ goal: '',
462
+ done_definition: [],
463
+ items: []
533
464
  };
534
465
  }
535
466
 
536
- if (projectType === 'brownfield' && !codebaseMapExists) {
537
- return {
538
- kind: 'explore_codebase',
539
- command: '/autodev',
540
- manualCommand: '/autodev-explore-codebase',
541
- reason: 'brownfield_project_needs_codebase_map',
542
- projectType
543
- };
467
+ const goalMatch = content.match(/^Goal:\s*(.+)$/mi);
468
+ const doneDefinition = [];
469
+ const doneMatch = content.match(/^Done Definition:\s*\n((?:- .+\n?)*)/mi);
470
+ if (doneMatch) {
471
+ for (const line of doneMatch[1].split('\n')) {
472
+ const bullet = line.trim().match(/^- (.+)$/);
473
+ if (bullet) {
474
+ doneDefinition.push(bullet[1].trim());
475
+ }
476
+ }
544
477
  }
545
478
 
546
- if (!activeTrack) {
547
- return {
548
- kind: 'track_select',
549
- command: '/autodev',
550
- manualCommand: null,
551
- reason: tracks.length > 0 ? 'no_active_track_selected' : 'no_tracks_created',
552
- projectType
553
- };
554
- }
479
+ const headings = [...content.matchAll(/^##\s+Item\s+\d+\s*:\s*.+$/gm)];
480
+ const items = [];
555
481
 
556
- const track = trackPaths(cwd, activeTrack);
557
- if (!track || !fileExists(track.track) || !fileExists(track.roadmap) || !fileExists(track.state)) {
558
- return {
559
- kind: 'track_setup',
560
- command: '/autodev',
561
- manualCommand: null,
562
- reason: 'active_track_missing_required_artifacts',
563
- projectType,
564
- trackSlug: activeTrack
565
- };
482
+ for (let index = 0; index < headings.length; index += 1) {
483
+ const start = headings[index].index;
484
+ const end = index + 1 < headings.length ? headings[index + 1].index : content.length;
485
+ const section = content.slice(start, end).trim();
486
+ const item = parseItemSection(section);
487
+ if (item) {
488
+ items.push(item);
489
+ }
566
490
  }
567
491
 
568
- const phases = listPhases(cwd, activeTrack);
569
- const trackState = readStateSnapshot(track.state);
570
- if (phases.length === 0) {
571
- return {
572
- kind: 'track_setup',
573
- command: '/autodev',
574
- manualCommand: null,
575
- reason: 'active_track_has_no_phases',
576
- projectType,
577
- trackSlug: activeTrack
578
- };
579
- }
492
+ return {
493
+ goal: goalMatch ? goalMatch[1].trim() : '',
494
+ done_definition: doneDefinition,
495
+ items
496
+ };
497
+ }
580
498
 
581
- const currentStatePhase = trackState?.currentPhase
582
- ? phases.find(phase => phase.number === trackState.currentPhase) || null
499
+ function determineNextItem(items, preferredItemId) {
500
+ const preferred = preferredItemId
501
+ ? items.find(item => item.id === normalizeCurrentItem(preferredItemId))
583
502
  : null;
584
503
 
585
- if (currentStatePhase && (
586
- isBlockedTaskStatus(trackState?.currentTaskStatus)
587
- )) {
588
- return {
589
- kind: 'plan_phase',
590
- command: '/autodev',
591
- manualCommand: `/autodev-plan-phase ${currentStatePhase.number}`,
592
- reason: 'blocked_phase_requires_replanning',
593
- projectType,
594
- trackSlug: activeTrack,
595
- phaseNumber: currentStatePhase.number
596
- };
597
- }
598
-
599
- if (currentStatePhase && trackState?.currentStep === 'execution') {
600
- return {
601
- kind: 'execute_phase',
602
- command: '/autodev',
603
- manualCommand: `/autodev-execute-phase ${currentStatePhase.number}`,
604
- reason: 'phase_execution_in_progress',
605
- projectType,
606
- trackSlug: activeTrack,
607
- phaseNumber: currentStatePhase.number
608
- };
609
- }
610
-
611
- if (currentStatePhase && trackState?.currentStep === 'task_review') {
612
- return {
613
- kind: 'task_review',
614
- command: '/autodev',
615
- manualCommand: `/autodev-review-task ${currentStatePhase.number}`,
616
- reason: 'task_execution_checkpoint_pending_user_review',
617
- projectType,
618
- trackSlug: activeTrack,
619
- phaseNumber: currentStatePhase.number
620
- };
621
- }
622
-
623
- if (currentStatePhase && trackState?.currentStep === 'plan_review') {
624
- return {
625
- kind: 'plan_review',
626
- command: '/autodev',
627
- manualCommand: `/autodev-execute-phase ${currentStatePhase.number}`,
628
- reason: 'phase_plan_awaits_review',
629
- projectType,
630
- trackSlug: activeTrack,
631
- phaseNumber: currentStatePhase.number
632
- };
504
+ if (preferred && ['pending', 'in_progress'].includes(preferred.status)) {
505
+ return preferred;
633
506
  }
634
507
 
635
- if (currentStatePhase && trackState?.currentStep === 'review' && !currentStatePhase.reviewExists) {
636
- return {
637
- kind: 'review_phase',
638
- command: '/autodev',
639
- manualCommand: `/autodev-review-phase ${currentStatePhase.number}`,
640
- reason: 'phase_review_in_progress',
641
- projectType,
642
- trackSlug: activeTrack,
643
- phaseNumber: currentStatePhase.number
644
- };
645
- }
508
+ return items.find(item => item.status === 'in_progress')
509
+ || items.find(item => item.status === 'pending')
510
+ || null;
511
+ }
646
512
 
647
- if (currentStatePhase && trackState?.currentStep === 'verification' && !currentStatePhase.uatExists) {
513
+ function buildRoute(cwd) {
514
+ const paths = rootPaths(cwd);
515
+ const state = loadState(cwd) || {
516
+ ...DEFAULT_STATE,
517
+ project_type: normalizeProjectType(null, cwd)
518
+ };
519
+ const plan = parsePlan(readText(paths.plan));
520
+ const briefExists = fileExists(paths.brief);
521
+ const contextExists = fileExists(paths.context);
522
+ const projectType = normalizeProjectType(state.project_type, cwd);
523
+ const blockedItem = plan.items.find(item => item.status === 'blocked') || null;
524
+ const nextItem = determineNextItem(plan.items, state.current_item);
525
+ const allDone = plan.items.length > 0 && plan.items.every(item => item.status === 'done');
526
+
527
+ if (!briefExists) {
648
528
  return {
649
- kind: 'verify_phase',
650
- command: '/autodev',
651
- manualCommand: `/autodev-verify-work ${currentStatePhase.number}`,
652
- reason: 'phase_verification_in_progress',
653
- projectType,
654
- trackSlug: activeTrack,
655
- phaseNumber: currentStatePhase.number
529
+ kind: 'init_brief',
530
+ reason: 'brief_missing',
531
+ command: '/autodev'
656
532
  };
657
533
  }
658
534
 
659
- const nextReview = phases.find(phase => phase.summaryExists && !phase.reviewExists);
660
- if (nextReview && config.workflow.review_after_execute !== false) {
535
+ if (projectType === 'brownfield' && !contextExists) {
661
536
  return {
662
- kind: 'review_phase',
663
- command: '/autodev',
664
- manualCommand: `/autodev-review-phase ${nextReview.number}`,
665
- reason: 'phase_executed_but_not_reviewed',
666
- projectType,
667
- trackSlug: activeTrack,
668
- phaseNumber: nextReview.number
537
+ kind: 'build_context',
538
+ reason: 'brownfield_context_missing',
539
+ command: '/autodev'
669
540
  };
670
541
  }
671
542
 
672
- const nextVerify = phases.find(phase => phase.reviewExists && !phase.uatExists);
673
- if (nextVerify) {
543
+ if (plan.items.length === 0) {
674
544
  return {
675
- kind: 'verify_phase',
676
- command: '/autodev',
677
- manualCommand: `/autodev-verify-work ${nextVerify.number}`,
678
- reason: 'phase_reviewed_but_not_verified',
679
- projectType,
680
- trackSlug: activeTrack,
681
- phaseNumber: nextVerify.number
545
+ kind: 'plan',
546
+ reason: 'plan_missing_or_empty',
547
+ command: '/autodev'
682
548
  };
683
549
  }
684
550
 
685
- const nextTaskReview = phases.find(phase => phase.planExists && !phase.summaryExists && phase.taskDoneCount > 0);
686
- if (nextTaskReview) {
551
+ if (state.stage === 'blocked' || blockedItem) {
687
552
  return {
688
- kind: 'task_review',
553
+ kind: 'repair_plan',
554
+ reason: blockedItem ? 'plan_item_blocked' : 'state_marked_blocked',
689
555
  command: '/autodev',
690
- manualCommand: `/autodev-review-task ${nextTaskReview.number}`,
691
- reason: 'phase_task_completed_awaiting_user_review',
692
- projectType,
693
- trackSlug: activeTrack,
694
- phaseNumber: nextTaskReview.number
556
+ current_item: blockedItem ? blockedItem.id : state.current_item
695
557
  };
696
558
  }
697
559
 
698
- const nextPlannedReview = phases.find(phase => phase.planExists && !phase.summaryExists);
699
- if (nextPlannedReview) {
560
+ if (nextItem) {
700
561
  return {
701
- kind: 'plan_review',
562
+ kind: 'execute',
563
+ reason: nextItem.status === 'in_progress' ? 'item_in_progress' : 'pending_plan_item',
702
564
  command: '/autodev',
703
- manualCommand: `/autodev-execute-phase ${nextPlannedReview.number}`,
704
- reason: 'phase_planned_awaiting_user_review',
705
- projectType,
706
- trackSlug: activeTrack,
707
- phaseNumber: nextPlannedReview.number
565
+ current_item: nextItem.id
708
566
  };
709
567
  }
710
568
 
711
- const nextPlan = phases.find(phase => !phase.planExists);
712
- if (nextPlan) {
569
+ if (allDone) {
713
570
  return {
714
- kind: 'plan_phase',
715
- command: '/autodev',
716
- manualCommand: `/autodev-plan-phase ${nextPlan.number}`,
717
- reason: 'phase_not_planned',
718
- projectType,
719
- trackSlug: activeTrack,
720
- phaseNumber: nextPlan.number
571
+ kind: 'complete',
572
+ reason: 'all_plan_items_done',
573
+ command: '/autodev'
721
574
  };
722
575
  }
723
576
 
724
577
  return {
725
- kind: 'track_complete',
726
- command: '/autodev',
727
- manualCommand: '/autodev-cleanup',
728
- reason: 'active_track_fully_verified',
729
- projectType,
730
- trackSlug: activeTrack
578
+ kind: 'plan',
579
+ reason: 'plan_needs_refresh',
580
+ command: '/autodev'
731
581
  };
732
582
  }
733
583
 
734
- function buildProgress(cwd) {
584
+ function buildStatus(cwd) {
735
585
  const paths = rootPaths(cwd);
736
- const initialized = fileExists(paths.project);
737
586
  const existingCodebase = detectExistingCodebase(cwd);
738
- const config = loadConfig(cwd);
739
- const projectType = config.project.type || (existingCodebase ? 'brownfield' : 'greenfield');
740
- const activeTrack = readActiveTrack(cwd);
741
- const tracks = listTracks(cwd);
742
- const phases = activeTrack ? listPhases(cwd, activeTrack) : [];
743
- const codebaseMapExists = fileExists(paths.codebaseSummary);
587
+ const autodevExists = fileExists(paths.root);
588
+ const state = loadState(cwd);
589
+ const projectType = normalizeProjectType(state?.project_type, cwd);
590
+ const plan = parsePlan(readText(paths.plan));
591
+ const counts = {
592
+ total: plan.items.length,
593
+ pending: plan.items.filter(item => item.status === 'pending').length,
594
+ in_progress: plan.items.filter(item => item.status === 'in_progress').length,
595
+ done: plan.items.filter(item => item.status === 'done').length,
596
+ blocked: plan.items.filter(item => item.status === 'blocked').length
597
+ };
744
598
  const route = buildRoute(cwd);
599
+ const currentItem = determineNextItem(plan.items, state?.current_item);
745
600
 
746
601
  return {
747
- initialized,
748
- projectType,
749
- existingCodebase,
750
- codebaseMapExists,
751
- activeTrack,
752
- tracks,
753
- phases,
754
- counts: {
755
- trackCount: tracks.length,
756
- total: phases.length,
757
- planned: phases.filter(phase => phase.planExists).length,
758
- executed: phases.filter(phase => phase.summaryExists).length,
759
- reviewed: phases.filter(phase => phase.reviewExists).length,
760
- verified: phases.filter(phase => phase.uatExists).length,
761
- tasks: phases.reduce((total, phase) => total + phase.taskCount, 0),
762
- tasksDone: phases.reduce((total, phase) => total + phase.taskDoneCount, 0)
763
- },
764
- route,
765
- nextCommand: route.command,
766
- manualNextCommand: route.manualCommand || null
602
+ cwd,
603
+ workspace_root: paths.workspaceRoot,
604
+ autodev_root: paths.root,
605
+ autodev_exists: autodevExists,
606
+ state_exists: fileExists(paths.state),
607
+ existing_code_detected: existingCodebase,
608
+ project_type: projectType,
609
+ stage: state?.stage || DEFAULT_STATE.stage,
610
+ current_item: currentItem ? currentItem.id : normalizeCurrentItem(state?.current_item),
611
+ last_result: state?.last_result || DEFAULT_STATE.last_result,
612
+ last_run_at: state?.last_run_at || null,
613
+ last_run_path: state?.last_run_path || null,
614
+ settings: state?.settings || { ...DEFAULT_STATE.settings },
615
+ brief_path: paths.brief,
616
+ brief_exists: fileExists(paths.brief),
617
+ context_path: paths.context,
618
+ context_exists: fileExists(paths.context),
619
+ context_required: projectType === 'brownfield',
620
+ plan_path: paths.plan,
621
+ plan_exists: fileExists(paths.plan),
622
+ plan_goal: plan.goal,
623
+ plan_done_definition: plan.done_definition,
624
+ plan_items: plan.items,
625
+ counts,
626
+ route
767
627
  };
768
628
  }
769
629
 
770
- function renderProgressTable(progress) {
771
- if (!progress.initialized) {
772
- return 'No .autodev project found.\nNext: /autodev';
773
- }
774
-
630
+ function renderStatusTable(status) {
775
631
  const lines = [
776
- 'Autodev Progress',
632
+ 'Autodev Status',
777
633
  '',
778
- `Project Type: ${progress.projectType}`,
779
- `Codebase Map: ${progress.codebaseMapExists ? 'ready' : 'missing'}`,
780
- `Tracks: ${progress.counts.trackCount}`,
781
- `Active Track: ${progress.activeTrack || 'none'}`,
782
- `Phases: ${progress.counts.total}`,
783
- `Planned: ${progress.counts.planned}`,
784
- `Executed: ${progress.counts.executed}`,
785
- `Reviewed: ${progress.counts.reviewed}`,
786
- `Verified: ${progress.counts.verified}`,
787
- `Tasks: ${progress.counts.tasks}`,
788
- `Task Summaries: ${progress.counts.tasksDone}`,
789
- `Next: ${progress.route.command}`,
790
- progress.route.manualCommand ? `Manual Shortcut: ${progress.route.manualCommand}` : null,
791
- '',
792
- '| Phase | Type | Name | Tasks | Done | Plan | Summary | Review | UAT | Status |',
793
- '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |'
794
- ].filter(Boolean);
634
+ `Project Type: ${status.project_type}`,
635
+ `Stage: ${status.stage}`,
636
+ `Brief: ${status.brief_exists ? 'ready' : 'missing'}`,
637
+ `Context: ${status.context_required ? (status.context_exists ? 'ready' : 'missing') : 'not required'}`,
638
+ `Plan: ${status.plan_exists ? `${status.counts.total} item(s)` : 'missing'}`,
639
+ `Current Item: ${status.current_item || 'none'}`,
640
+ `Last Result: ${status.last_result}`,
641
+ `Next: ${status.route.command}`,
642
+ `Route: ${status.route.kind} (${status.route.reason})`
643
+ ];
795
644
 
796
- for (const phase of progress.phases) {
645
+ if (status.plan_items.length > 0) {
797
646
  lines.push(
798
- `| ${phase.number} | ${phase.type} | ${phase.name} | ${phase.taskCount} | ${phase.taskDoneCount} | ${phase.planExists ? 'yes' : 'no'} | ${phase.summaryExists ? 'yes' : 'no'} | ${phase.reviewExists ? 'yes' : 'no'} | ${phase.uatExists ? 'yes' : 'no'} | ${phase.status} |`
647
+ '',
648
+ '| Item | Status | Title |',
649
+ '| --- | --- | --- |'
799
650
  );
800
- }
801
651
 
802
- if (progress.tracks.length > 0) {
803
- lines.push('', '| Track | Active | Name |', '| --- | --- | --- |');
804
- for (const track of progress.tracks) {
805
- lines.push(`| ${track.slug} | ${track.active ? 'yes' : 'no'} | ${track.name} |`);
652
+ for (const item of status.plan_items) {
653
+ lines.push(`| ${item.id} | ${item.status} | ${item.title} |`);
806
654
  }
807
655
  }
808
656
 
809
657
  return lines.join('\n');
810
658
  }
811
659
 
812
- function dotGet(object, key) {
813
- return key.split('.').reduce((current, part) => (
814
- current && Object.prototype.hasOwnProperty.call(current, part) ? current[part] : undefined
815
- ), object);
816
- }
660
+ function updateState(cwd, updates = {}) {
661
+ const current = loadState(cwd) || bootstrapProject(cwd).state;
662
+ const next = {
663
+ ...current,
664
+ ...updates,
665
+ project_type: updates.project_type !== undefined
666
+ ? normalizeProjectType(updates.project_type, cwd)
667
+ : current.project_type,
668
+ stage: updates.stage !== undefined ? normalizeStage(updates.stage) : current.stage,
669
+ current_item: updates.current_item !== undefined
670
+ ? normalizeCurrentItem(updates.current_item)
671
+ : current.current_item,
672
+ settings: {
673
+ ...current.settings,
674
+ ...((updates.settings && typeof updates.settings === 'object') ? updates.settings : {})
675
+ }
676
+ };
677
+
678
+ if (updates.last_run_path !== undefined || updates.last_result !== undefined || updates.touch === true) {
679
+ next.last_run_at = new Date().toISOString();
680
+ }
817
681
 
818
- function codebasePaths(cwd) {
819
- const codebaseDir = rootPaths(cwd).codebaseDir;
820
- return Object.fromEntries(CODEBASE_FILES.map(name => [name.replace(/\.md$/, ''), path.join(codebaseDir, name)]));
682
+ saveState(cwd, next);
683
+ return loadState(cwd);
821
684
  }
822
685
 
823
- function buildStatus(cwd) {
686
+ function createRunReport(cwd, kind = 'run') {
824
687
  const paths = rootPaths(cwd);
825
- const route = buildRoute(cwd);
826
- const config = loadConfig(cwd);
827
- const existingCodebase = detectExistingCodebase(cwd);
828
- const projectType = config.project.type || (existingCodebase ? 'brownfield' : 'greenfield');
829
- const activeTrack = readActiveTrack(cwd);
830
- const tracks = listTracks(cwd);
688
+ ensureDir(paths.runsDir);
831
689
 
832
- return {
833
- cwd,
834
- workspace_root: paths.workspaceRoot,
835
- autodev_exists: fileExists(paths.root),
836
- project_exists: fileExists(paths.project),
837
- existing_code_detected: existingCodebase,
838
- project_type: projectType,
839
- project_path: paths.project,
840
- project_state_path: paths.state,
841
- active_track_path: paths.activeTrack,
842
- active_track: activeTrack,
843
- track_count: tracks.length,
844
- tracks: tracks.map(track => ({
845
- slug: track.slug,
846
- name: track.name,
847
- active: track.active
848
- })),
849
- codebase_map_exists: fileExists(paths.codebaseSummary),
850
- route
851
- };
852
- }
690
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
691
+ const filePath = path.join(paths.runsDir, `${stamp}-${slugify(kind)}.md`);
692
+ const content = [
693
+ '# Run Report',
694
+ '',
695
+ `Kind: ${kind}`,
696
+ `Started: ${new Date().toISOString()}`,
697
+ 'Outcome: pending',
698
+ '',
699
+ '## Observed',
700
+ '- [facts only]',
701
+ '',
702
+ '## Inferred',
703
+ '- [none]',
704
+ '',
705
+ '## Files Changed',
706
+ '- [path]',
707
+ '',
708
+ '## Verification',
709
+ '- [command and result]',
710
+ '',
711
+ '## Unknowns',
712
+ '- [none]',
713
+ ''
714
+ ].join('\n');
853
715
 
854
- function buildCleanupPayload(cwd) {
855
- const paths = rootPaths(cwd);
856
- const route = buildRoute(cwd);
857
- const activeTrack = readActiveTrack(cwd);
858
- const track = trackPaths(cwd, activeTrack);
859
- const phases = activeTrack ? listPhases(cwd, activeTrack) : [];
860
- const verifiedPhases = phases.filter(phase => phase.uatExists);
861
- const completedPhases = phases.filter(phase => phase.summaryExists || phase.reviewExists || phase.uatExists);
716
+ fs.writeFileSync(filePath, content, 'utf8');
862
717
 
863
718
  return {
864
- cwd,
865
- autodev_exists: fileExists(paths.root),
866
- project_exists: fileExists(paths.project),
867
- project_state_path: paths.state,
868
- active_track_path: paths.activeTrack,
869
- active_track: activeTrack,
870
- track_path: track ? track.dir : null,
871
- track_state_path: track ? track.state : null,
872
- phases_dir: track ? track.phasesDir : null,
873
- archive_root: path.join(paths.root, 'archive'),
874
- route,
875
- phase_counts: {
876
- total: phases.length,
877
- completed: completedPhases.length,
878
- verified: verifiedPhases.length
879
- },
880
- verified_phase_numbers: verifiedPhases.map(phase => phase.number),
881
- verified_phase_dirs: verifiedPhases.map(phase => phase.dir)
719
+ kind,
720
+ path: filePath
882
721
  };
883
722
  }
884
723
 
885
- function initPayload(cwd, mode, requestedPhase) {
886
- const paths = rootPaths(cwd);
887
- const config = loadConfig(cwd);
888
- const route = buildRoute(cwd);
889
- const existingCodebase = detectExistingCodebase(cwd);
890
- const projectType = config.project.type || (existingCodebase ? 'brownfield' : 'greenfield');
891
- const activeTrack = readActiveTrack(cwd);
892
- const tracks = listTracks(cwd);
893
- const track = trackPaths(cwd, activeTrack);
894
- const phaseMode = mode === 'review-phase'
895
- ? 'review'
896
- : mode === 'verify-work'
897
- ? 'verify'
898
- : mode === 'review-task'
899
- ? 'task_review'
900
- : mode === 'execute-phase' || mode === 'review-plan'
901
- ? 'execute'
902
- : mode === 'plan-phase'
903
- ? 'plan'
904
- : null;
905
- const phase = phaseMode && activeTrack ? resolvePhase(cwd, activeTrack, requestedPhase, phaseMode) : null;
906
- const tasks = phase ? listTasksForPhaseDetails(phase) : [];
907
- const nextTask = nextExecutableTask(tasks);
908
- const lastCompleted = lastCompletedTask(tasks);
909
- const highestTaskNumber = lastTaskNumber(tasks);
910
- const dependencyDeadlock = hasDependencyDeadlock(tasks);
911
- const trackStateSnapshot = track ? readStateSnapshot(track.state) : null;
912
- const currentTaskNumber = (() => {
913
- const raw = trackStateSnapshot?.currentTask;
914
- if (raw && /^\d+$/.test(raw)) {
915
- return Number(raw);
724
+ function dotGet(object, key) {
725
+ return key.split('.').reduce((current, part) => (
726
+ current && Object.prototype.hasOwnProperty.call(current, part) ? current[part] : undefined
727
+ ), object);
728
+ }
729
+
730
+ function parseOptions(args) {
731
+ const options = {};
732
+
733
+ for (let index = 0; index < args.length; index += 1) {
734
+ const arg = args[index];
735
+ if (!arg.startsWith('--')) {
736
+ continue;
916
737
  }
917
- return lastCompleted ? lastCompleted.number : null;
918
- })();
919
- const currentTask = tasks.find(task => task.number === currentTaskNumber) || lastCompleted || null;
920
- const codebase = codebasePaths(cwd);
921
738
 
922
- return {
923
- cwd,
924
- autodev_exists: fileExists(paths.root),
925
- project_exists: fileExists(paths.project),
926
- existing_code_detected: existingCodebase,
927
- project_type: projectType,
928
- config_path: paths.config,
929
- project_path: paths.project,
930
- project_state_path: paths.state,
931
- active_track_path: paths.activeTrack,
932
- active_track: activeTrack,
933
- track_count: tracks.length,
934
- tracks: tracks.map(trackItem => ({
935
- slug: trackItem.slug,
936
- name: trackItem.name,
937
- active: trackItem.active,
938
- path: trackItem.paths.dir
939
- })),
940
- codebase_dir: paths.codebaseDir,
941
- codebase_map_exists: fileExists(paths.codebaseSummary),
942
- codebase_paths: codebase,
943
- track_path: track ? track.dir : null,
944
- track_doc_path: track ? track.track : null,
945
- requirements_path: track ? track.requirements : null,
946
- roadmap_path: track ? track.roadmap : null,
947
- track_state_path: track ? track.state : null,
948
- track_state: trackStateSnapshot,
949
- phases_dir: track ? track.phasesDir : null,
950
- route,
951
- phase_found: Boolean(phase),
952
- phase_number: phase ? phase.number : null,
953
- phase_type: phase ? phase.type : null,
954
- phase_name: phase ? phase.name : null,
955
- phase_dir: phase ? phase.dir : null,
956
- plan_path: phase ? phase.planPath : null,
957
- summary_path: phase ? phase.summaryPath : null,
958
- review_path: phase ? phase.reviewPath : null,
959
- uat_path: phase ? phase.uatPath : null,
960
- plan_exists: phase ? phase.planExists : false,
961
- summary_exists: phase ? phase.summaryExists : false,
962
- review_exists: phase ? phase.reviewExists : false,
963
- uat_exists: phase ? phase.uatExists : false,
964
- tasks_dir: phase ? phase.dir : null,
965
- task_count: tasks.length,
966
- task_done_count: tasks.filter(task => task.summaryExists).length,
967
- all_tasks_done: tasks.length > 0 && tasks.every(task => task.summaryExists),
968
- dependency_deadlock: dependencyDeadlock,
969
- last_task_number: highestTaskNumber,
970
- next_task_number: nextTask ? nextTask.number : null,
971
- next_task_path: nextTask ? nextTask.taskPath : null,
972
- next_task_summary_path: nextTask ? nextTask.summaryPath : null,
973
- current_task_number: currentTask ? currentTask.number : null,
974
- current_task_path: currentTask ? currentTask.taskPath : null,
975
- current_task_summary_path: currentTask ? currentTask.summaryPath : null,
976
- last_completed_task_number: lastCompleted ? lastCompleted.number : null,
977
- last_completed_task_summary_path: lastCompleted ? lastCompleted.summaryPath : null,
978
- tasks: tasks.map(task => ({
979
- number: task.number,
980
- title: task.title,
981
- status: task.effectiveStatus,
982
- depends_on: task.dependsOn,
983
- ready: task.ready,
984
- task_path: task.taskPath,
985
- summary_path: task.summaryPath,
986
- summary_exists: task.summaryExists
987
- })),
988
- workflow: config.workflow,
989
- git_mode: config.git.mode
990
- };
739
+ const key = arg.slice(2).replace(/-/g, '_');
740
+ const next = args[index + 1];
741
+
742
+ if (!next || next.startsWith('--')) {
743
+ options[key] = true;
744
+ continue;
745
+ }
746
+
747
+ options[key] = next;
748
+ index += 1;
749
+ }
750
+
751
+ return options;
991
752
  }
992
753
 
993
754
  function printJson(value) {
@@ -995,8 +756,8 @@ function printJson(value) {
995
756
  }
996
757
 
997
758
  function main() {
998
- const args = process.argv.slice(2);
999
759
  const cwd = process.cwd();
760
+ const args = process.argv.slice(2);
1000
761
 
1001
762
  if (args.length === 0) {
1002
763
  process.stderr.write('autodev-tools: command required\n');
@@ -1005,14 +766,27 @@ function main() {
1005
766
 
1006
767
  const [command, ...rest] = args;
1007
768
 
1008
- if (command === 'progress') {
1009
- const mode = rest[0] || 'table';
1010
- const progress = buildProgress(cwd);
1011
- if (mode === 'json') {
1012
- printJson(progress);
769
+ if (command === 'bootstrap') {
770
+ const options = parseOptions(rest);
771
+ printJson(bootstrapProject(cwd, {
772
+ projectType: options.project_type
773
+ }));
774
+ return;
775
+ }
776
+
777
+ if (command === 'status') {
778
+ const mode = rest[0] === 'table' ? 'table' : 'json';
779
+ const status = buildStatus(cwd);
780
+ if (mode === 'table') {
781
+ process.stdout.write(`${renderStatusTable(status)}\n`);
1013
782
  return;
1014
783
  }
1015
- process.stdout.write(`${renderProgressTable(progress)}\n`);
784
+ printJson(status);
785
+ return;
786
+ }
787
+
788
+ if (command === 'route') {
789
+ printJson(buildRoute(cwd));
1016
790
  return;
1017
791
  }
1018
792
 
@@ -1022,7 +796,9 @@ function main() {
1022
796
  process.stderr.write('autodev-tools: config key required\n');
1023
797
  process.exit(1);
1024
798
  }
1025
- const value = dotGet(loadConfig(cwd), key);
799
+
800
+ const state = loadState(cwd) || DEFAULT_STATE;
801
+ const value = dotGet(state, key);
1026
802
  if (typeof value === 'object' && value !== null) {
1027
803
  printJson(value);
1028
804
  } else if (value !== undefined) {
@@ -1031,35 +807,41 @@ function main() {
1031
807
  return;
1032
808
  }
1033
809
 
1034
- if (command === 'status') {
1035
- printJson(buildStatus(cwd));
1036
- return;
1037
- }
1038
-
1039
- if (command === 'cleanup') {
1040
- printJson(buildCleanupPayload(cwd));
1041
- return;
1042
- }
810
+ if (command === 'update') {
811
+ const options = parseOptions(rest);
812
+ const updates = {};
1043
813
 
1044
- if (command === 'route') {
1045
- const mode = rest[0] || 'json';
1046
- const route = buildRoute(cwd);
1047
- if (mode === 'json') {
1048
- printJson(route);
1049
- return;
814
+ if (options.project_type !== undefined) {
815
+ updates.project_type = options.project_type;
816
+ }
817
+ if (options.stage !== undefined) {
818
+ updates.stage = options.stage;
1050
819
  }
1051
- process.stdout.write(`${route.command}\n`);
820
+ if (options.current_item !== undefined) {
821
+ updates.current_item = options.current_item;
822
+ }
823
+ if (options.last_result !== undefined) {
824
+ updates.last_result = options.last_result;
825
+ }
826
+ if (options.last_run_path !== undefined) {
827
+ updates.last_run_path = options.last_run_path;
828
+ }
829
+ if (options.auto_format !== undefined) {
830
+ updates.settings = {
831
+ auto_format: options.auto_format !== 'false'
832
+ };
833
+ }
834
+ if (options.touch === true) {
835
+ updates.touch = true;
836
+ }
837
+
838
+ printJson(updateState(cwd, updates));
1052
839
  return;
1053
840
  }
1054
841
 
1055
- if (command === 'init') {
1056
- const mode = rest[0];
1057
- const phase = rest[1];
1058
- if (!mode) {
1059
- process.stderr.write('autodev-tools: init mode required\n');
1060
- process.exit(1);
1061
- }
1062
- printJson(initPayload(cwd, mode, phase));
842
+ if (command === 'create-run') {
843
+ const kind = rest[0] || 'run';
844
+ printJson(createRunReport(cwd, kind));
1063
845
  return;
1064
846
  }
1065
847
 
@@ -1072,26 +854,20 @@ if (require.main === module) {
1072
854
  }
1073
855
 
1074
856
  module.exports = {
857
+ DEFAULT_STATE,
1075
858
  autodevDir,
1076
- buildCleanupPayload,
1077
- buildProgress,
859
+ bootstrapProject,
1078
860
  buildRoute,
1079
861
  buildStatus,
862
+ createRunReport,
1080
863
  detectExistingCodebase,
1081
864
  findWorkspaceRoot,
1082
- initPayload,
1083
- listPhases,
1084
- listTracks,
1085
- loadConfig,
1086
- parseRoadmap,
1087
- readActiveTrack,
1088
- renderProgressTable,
1089
- resolvePhase,
865
+ loadState,
866
+ normalizeCurrentItem,
867
+ normalizeProjectType,
868
+ parsePlan,
869
+ renderStatusTable,
1090
870
  rootPaths,
1091
- trackPaths,
1092
- listTasksForPhaseDetails,
1093
- lastCompletedTask,
1094
- lastTaskNumber,
1095
- hasDependencyDeadlock,
1096
- nextExecutableTask
871
+ saveState,
872
+ updateState
1097
873
  };