orchestr8 2.7.1 → 3.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/.blueprint/agents/AGENT_BA_CASS.md +18 -34
- package/.blueprint/agents/AGENT_DEVELOPER_CODEY.md +21 -28
- package/.blueprint/agents/AGENT_SPECIFICATION_ALEX.md +6 -0
- package/.blueprint/agents/AGENT_TESTER_NIGEL.md +5 -3
- package/.blueprint/agents/WHAT_WE_STAND_FOR.md +0 -0
- package/.blueprint/features/feature_interactive-alex/FEATURE_SPEC.md +263 -0
- package/.blueprint/features/feature_interactive-alex/IMPLEMENTATION_PLAN.md +69 -0
- package/.blueprint/features/feature_interactive-alex/handoff-alex.md +19 -0
- package/.blueprint/features/feature_interactive-alex/handoff-cass.md +21 -0
- package/.blueprint/features/feature_interactive-alex/handoff-nigel.md +19 -0
- package/.blueprint/features/feature_interactive-alex/story-flag-routing.md +54 -0
- package/.blueprint/features/feature_interactive-alex/story-iterative-drafting.md +65 -0
- package/.blueprint/features/feature_interactive-alex/story-pipeline-integration.md +66 -0
- package/.blueprint/features/feature_interactive-alex/story-session-lifecycle.md +75 -0
- package/.blueprint/features/feature_interactive-alex/story-system-spec-creation.md +57 -0
- package/.blueprint/features/feature_parallel-abort/FEATURE_SPEC.md +117 -0
- package/.blueprint/features/feature_parallel-confirm/FEATURE_SPEC.md +90 -0
- package/.blueprint/features/feature_parallel-features/FEATURE_SPEC.md +291 -0
- package/.blueprint/features/feature_parallel-features/IMPLEMENTATION_PLAN.md +73 -0
- package/.blueprint/features/feature_parallel-lock/FEATURE_SPEC.md +119 -0
- package/.blueprint/features/feature_parallel-logging/FEATURE_SPEC.md +105 -0
- package/.blueprint/features/feature_parallel-preflight/FEATURE_SPEC.md +141 -0
- package/.blueprint/prompts/codey-implement-runtime.md +1 -1
- package/.blueprint/prompts/nigel-runtime.md +1 -1
- package/.blueprint/ways_of_working/DEVELOPMENT_RITUAL.md +4 -4
- package/README.md +249 -0
- package/SKILL.md +35 -1
- package/bin/cli.js +187 -0
- package/package.json +2 -2
- package/src/index.js +61 -1
- package/src/init.js +21 -3
- package/src/interactive.js +338 -0
- package/src/parallel.js +1544 -0
- package/src/stack.js +320 -0
package/bin/cli.js
CHANGED
|
@@ -12,7 +12,28 @@ const {
|
|
|
12
12
|
setConfigValue: setFeedbackConfigValue,
|
|
13
13
|
resetConfig: resetFeedbackConfig
|
|
14
14
|
} = require('../src/feedback');
|
|
15
|
+
const {
|
|
16
|
+
displayStackConfig,
|
|
17
|
+
setStackConfigValue,
|
|
18
|
+
resetStackConfig
|
|
19
|
+
} = require('../src/stack');
|
|
15
20
|
const { displayFeedbackInsights } = require('../src/insights');
|
|
21
|
+
const {
|
|
22
|
+
formatStatus,
|
|
23
|
+
getDefaultConfig,
|
|
24
|
+
splitByLimit,
|
|
25
|
+
runParallel,
|
|
26
|
+
loadQueue,
|
|
27
|
+
cleanupWorktrees,
|
|
28
|
+
readParallelConfig,
|
|
29
|
+
writeParallelConfig,
|
|
30
|
+
getDefaultParallelConfig,
|
|
31
|
+
abortParallel,
|
|
32
|
+
getLockInfo,
|
|
33
|
+
getDetailedStatus,
|
|
34
|
+
formatDetailedStatus,
|
|
35
|
+
rollbackParallel
|
|
36
|
+
} = require('../src/parallel');
|
|
16
37
|
|
|
17
38
|
const args = process.argv.slice(2);
|
|
18
39
|
const command = args[0];
|
|
@@ -129,6 +150,154 @@ const commands = {
|
|
|
129
150
|
},
|
|
130
151
|
description: 'Manage feedback loop configuration'
|
|
131
152
|
},
|
|
153
|
+
'stack-config': {
|
|
154
|
+
fn: () => {
|
|
155
|
+
if (subArg === 'set') {
|
|
156
|
+
const key = args[2];
|
|
157
|
+
const value = args[3];
|
|
158
|
+
if (!key || !value) {
|
|
159
|
+
console.error('Usage: stack-config set <key> <value>');
|
|
160
|
+
console.error('Valid keys: language, runtime, packageManager, frameworks, testRunner, testCommand, linter, tools');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
setStackConfigValue(key, value);
|
|
164
|
+
} else if (subArg === 'reset') {
|
|
165
|
+
resetStackConfig();
|
|
166
|
+
console.log('Stack configuration reset to defaults.');
|
|
167
|
+
} else {
|
|
168
|
+
displayStackConfig();
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
description: 'View or modify project tech stack configuration'
|
|
172
|
+
},
|
|
173
|
+
'parallel-config': {
|
|
174
|
+
fn: () => {
|
|
175
|
+
if (subArg === 'set') {
|
|
176
|
+
const key = args[2];
|
|
177
|
+
const value = args[3];
|
|
178
|
+
if (!key || !value) {
|
|
179
|
+
console.error('Usage: parallel-config set <key> <value>');
|
|
180
|
+
console.error('Valid keys: cli, skill, skillFlags, worktreeDir, maxConcurrency, queueFile');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const config = readParallelConfig();
|
|
184
|
+
if (key === 'maxConcurrency') {
|
|
185
|
+
config[key] = parseInt(value, 10);
|
|
186
|
+
} else {
|
|
187
|
+
config[key] = value;
|
|
188
|
+
}
|
|
189
|
+
writeParallelConfig(config);
|
|
190
|
+
console.log(`Set ${key} = ${value}`);
|
|
191
|
+
} else if (subArg === 'reset') {
|
|
192
|
+
writeParallelConfig(getDefaultParallelConfig());
|
|
193
|
+
console.log('Parallel configuration reset to defaults.');
|
|
194
|
+
} else {
|
|
195
|
+
const config = readParallelConfig();
|
|
196
|
+
console.log('Parallel Configuration\n');
|
|
197
|
+
console.log(` cli: ${config.cli}`);
|
|
198
|
+
console.log(` skill: ${config.skill}`);
|
|
199
|
+
console.log(` skillFlags: ${config.skillFlags}`);
|
|
200
|
+
console.log(` worktreeDir: ${config.worktreeDir}`);
|
|
201
|
+
console.log(` maxConcurrency: ${config.maxConcurrency}`);
|
|
202
|
+
console.log(` maxFeatures: ${config.maxFeatures}`);
|
|
203
|
+
console.log(` timeout: ${config.timeout} min`);
|
|
204
|
+
console.log(` minDiskSpaceMB: ${config.minDiskSpaceMB}`);
|
|
205
|
+
console.log(` queueFile: ${config.queueFile}`);
|
|
206
|
+
console.log('\nTo change: orchestr8 parallel-config set <key> <value>');
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
description: 'View or modify parallel pipeline configuration'
|
|
210
|
+
},
|
|
211
|
+
parallel: {
|
|
212
|
+
fn: async () => {
|
|
213
|
+
if (subArg === 'status') {
|
|
214
|
+
const detailed = args.includes('--detailed') || args.includes('-d');
|
|
215
|
+
const lock = getLockInfo();
|
|
216
|
+
|
|
217
|
+
if (detailed) {
|
|
218
|
+
const details = getDetailedStatus();
|
|
219
|
+
console.log(formatDetailedStatus(details));
|
|
220
|
+
} else {
|
|
221
|
+
const queue = loadQueue();
|
|
222
|
+
|
|
223
|
+
if (!queue.features || queue.features.length === 0) {
|
|
224
|
+
if (lock) {
|
|
225
|
+
console.log(`Parallel execution in progress (PID: ${lock.pid})`);
|
|
226
|
+
console.log(`Started: ${lock.startedAt}`);
|
|
227
|
+
console.log(`Features: ${lock.features.join(', ')}`);
|
|
228
|
+
} else {
|
|
229
|
+
console.log('No parallel pipelines active.');
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log('Parallel Pipeline Status\n');
|
|
235
|
+
console.log(formatStatus(queue.features));
|
|
236
|
+
const summary = {
|
|
237
|
+
running: queue.features.filter(f => f.status === 'parallel_running').length,
|
|
238
|
+
pending: queue.features.filter(f => f.status === 'parallel_queued').length,
|
|
239
|
+
completed: queue.features.filter(f => f.status === 'parallel_complete').length,
|
|
240
|
+
failed: queue.features.filter(f => f.status === 'parallel_failed').length,
|
|
241
|
+
conflicts: queue.features.filter(f => f.status === 'merge_conflict').length
|
|
242
|
+
};
|
|
243
|
+
console.log(`\nRunning: ${summary.running} | Pending: ${summary.pending} | Completed: ${summary.completed} | Failed: ${summary.failed} | Conflicts: ${summary.conflicts}`);
|
|
244
|
+
|
|
245
|
+
// Show log paths for running/failed
|
|
246
|
+
const withLogs = queue.features.filter(f =>
|
|
247
|
+
f.logPath && (f.status === 'parallel_running' || f.status === 'parallel_failed')
|
|
248
|
+
);
|
|
249
|
+
if (withLogs.length > 0) {
|
|
250
|
+
console.log('\nLog files:');
|
|
251
|
+
withLogs.forEach(f => console.log(` ${f.slug}: ${f.logPath}`));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log('\nTip: Use --detailed for progress bars');
|
|
255
|
+
}
|
|
256
|
+
} else if (subArg === 'rollback') {
|
|
257
|
+
const dryRunFlag = args.includes('--dry-run');
|
|
258
|
+
const forceFlag = args.includes('--force');
|
|
259
|
+
await rollbackParallel({ dryRun: dryRunFlag, force: forceFlag });
|
|
260
|
+
} else if (subArg === 'cleanup') {
|
|
261
|
+
const cleaned = await cleanupWorktrees();
|
|
262
|
+
console.log(`Cleaned ${cleaned} worktree(s).`);
|
|
263
|
+
} else if (subArg === 'abort') {
|
|
264
|
+
const cleanupFlag = args.includes('--cleanup');
|
|
265
|
+
await abortParallel({ cleanup: cleanupFlag });
|
|
266
|
+
} else {
|
|
267
|
+
const slugs = args.slice(1).filter(a => !a.startsWith('--') && !a.startsWith('-'));
|
|
268
|
+
if (slugs.length === 0) {
|
|
269
|
+
console.error('Usage: orchestr8 parallel <slug1> <slug2> ... [options]');
|
|
270
|
+
console.error('\nOptions:');
|
|
271
|
+
console.error(' --dry-run Preview execution plan without running');
|
|
272
|
+
console.error(' --yes, -y Skip confirmation prompt');
|
|
273
|
+
console.error(' --force Override existing lock');
|
|
274
|
+
console.error(' --verbose Stream output to console (not just logs)');
|
|
275
|
+
console.error(' --skip-preflight Skip feature validation checks');
|
|
276
|
+
console.error(' --max-concurrency=N Set max parallel pipelines (default: 3)');
|
|
277
|
+
console.error('\nSubcommands:');
|
|
278
|
+
console.error(' parallel status Show status of all pipelines');
|
|
279
|
+
console.error(' parallel abort Stop all running pipelines');
|
|
280
|
+
console.error(' parallel cleanup Remove completed/aborted worktrees');
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const maxFlag = args.find(a => a.startsWith('--max-concurrency='));
|
|
285
|
+
const options = {
|
|
286
|
+
dryRun: args.includes('--dry-run'),
|
|
287
|
+
yes: args.includes('--yes') || args.includes('-y'),
|
|
288
|
+
force: args.includes('--force'),
|
|
289
|
+
verbose: args.includes('--verbose'),
|
|
290
|
+
skipPreflight: args.includes('--skip-preflight')
|
|
291
|
+
};
|
|
292
|
+
if (maxFlag) {
|
|
293
|
+
options.maxConcurrency = parseInt(maxFlag.split('=')[1], 10);
|
|
294
|
+
}
|
|
295
|
+
const result = await runParallel(slugs, options);
|
|
296
|
+
process.exit(result.success ? 0 : 1);
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
description: 'Run multiple feature pipelines in parallel using git worktrees'
|
|
300
|
+
},
|
|
132
301
|
help: {
|
|
133
302
|
fn: showHelp,
|
|
134
303
|
description: 'Show this help message'
|
|
@@ -163,6 +332,24 @@ Commands:
|
|
|
163
332
|
feedback-config View current feedback loop configuration
|
|
164
333
|
feedback-config set <key> <value> Modify a config value (minRatingThreshold, enabled)
|
|
165
334
|
feedback-config reset Reset feedback configuration to defaults
|
|
335
|
+
stack-config View current tech stack configuration
|
|
336
|
+
stack-config set <key> <value> Modify a config value (language, runtime, frameworks, etc.)
|
|
337
|
+
stack-config reset Reset tech stack configuration to defaults
|
|
338
|
+
parallel <slugs...> Run multiple feature pipelines in parallel
|
|
339
|
+
parallel <slugs...> --dry-run Show execution plan without running
|
|
340
|
+
parallel <slugs...> --yes Skip confirmation prompt
|
|
341
|
+
parallel <slugs...> --verbose Stream output to console
|
|
342
|
+
parallel <slugs...> --skip-preflight Skip feature validation checks
|
|
343
|
+
parallel status Show status of all parallel pipelines
|
|
344
|
+
parallel status --detailed Show progress bars and stage info
|
|
345
|
+
parallel abort Stop all running pipelines
|
|
346
|
+
parallel abort --cleanup Stop all and remove worktrees
|
|
347
|
+
parallel rollback Undo completed merges and cleanup failures
|
|
348
|
+
parallel rollback --dry-run Preview what would be rolled back
|
|
349
|
+
parallel cleanup Remove completed/aborted worktrees
|
|
350
|
+
parallel-config View parallel pipeline configuration
|
|
351
|
+
parallel-config set <key> <value> Modify config (cli, skill, skillFlags, etc.)
|
|
352
|
+
parallel-config reset Reset parallel configuration to defaults
|
|
166
353
|
help Show this help message
|
|
167
354
|
|
|
168
355
|
Examples:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orchestr8",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Multi-agent workflow framework for automated feature development",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"author": "NewmanJustice",
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
|
-
"url": "git+https://github.com/NewmanJustice/
|
|
23
|
+
"url": "git+https://github.com/NewmanJustice/agent-workflow.git"
|
|
24
24
|
},
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"engines": {
|
package/src/index.js
CHANGED
|
@@ -50,6 +50,37 @@ const {
|
|
|
50
50
|
TECHNICAL_KEYWORDS,
|
|
51
51
|
USER_FACING_KEYWORDS
|
|
52
52
|
} = require('./classifier');
|
|
53
|
+
const {
|
|
54
|
+
parseFlags: parseInteractiveFlags,
|
|
55
|
+
shouldEnterInteractiveMode,
|
|
56
|
+
createSession,
|
|
57
|
+
getSessionProgress,
|
|
58
|
+
handleCommand,
|
|
59
|
+
getNextSection,
|
|
60
|
+
markSectionComplete,
|
|
61
|
+
markSectionTBD,
|
|
62
|
+
gatherContext,
|
|
63
|
+
identifyGaps,
|
|
64
|
+
generateQuestions,
|
|
65
|
+
canFinalize,
|
|
66
|
+
generateSpec,
|
|
67
|
+
writeSpec,
|
|
68
|
+
generateHandoff,
|
|
69
|
+
getOutputPath,
|
|
70
|
+
SESSION_STATES,
|
|
71
|
+
SECTION_ORDER,
|
|
72
|
+
MIN_REQUIRED_SECTIONS,
|
|
73
|
+
SYSTEM_SPEC_QUESTIONS
|
|
74
|
+
} = require('./interactive');
|
|
75
|
+
const {
|
|
76
|
+
getDefaultStackConfig,
|
|
77
|
+
readStackConfig,
|
|
78
|
+
writeStackConfig,
|
|
79
|
+
resetStackConfig,
|
|
80
|
+
setStackConfigValue,
|
|
81
|
+
detectStackConfig,
|
|
82
|
+
displayStackConfig
|
|
83
|
+
} = require('./stack');
|
|
53
84
|
const tools = require('./tools');
|
|
54
85
|
|
|
55
86
|
module.exports = {
|
|
@@ -105,6 +136,35 @@ module.exports = {
|
|
|
105
136
|
logClassification,
|
|
106
137
|
TECHNICAL_KEYWORDS,
|
|
107
138
|
USER_FACING_KEYWORDS,
|
|
139
|
+
// Stack config exports
|
|
140
|
+
getDefaultStackConfig,
|
|
141
|
+
readStackConfig,
|
|
142
|
+
writeStackConfig,
|
|
143
|
+
resetStackConfig,
|
|
144
|
+
setStackConfigValue,
|
|
145
|
+
detectStackConfig,
|
|
146
|
+
displayStackConfig,
|
|
108
147
|
// Tools module (model native features)
|
|
109
|
-
tools
|
|
148
|
+
tools,
|
|
149
|
+
// Interactive mode exports
|
|
150
|
+
parseInteractiveFlags,
|
|
151
|
+
shouldEnterInteractiveMode,
|
|
152
|
+
createSession,
|
|
153
|
+
getSessionProgress,
|
|
154
|
+
handleCommand,
|
|
155
|
+
getNextSection,
|
|
156
|
+
markSectionComplete,
|
|
157
|
+
markSectionTBD,
|
|
158
|
+
gatherContext,
|
|
159
|
+
identifyGaps,
|
|
160
|
+
generateQuestions,
|
|
161
|
+
canFinalize,
|
|
162
|
+
generateSpec,
|
|
163
|
+
writeSpec,
|
|
164
|
+
generateHandoff,
|
|
165
|
+
getOutputPath,
|
|
166
|
+
SESSION_STATES,
|
|
167
|
+
SECTION_ORDER,
|
|
168
|
+
MIN_REQUIRED_SECTIONS,
|
|
169
|
+
SYSTEM_SPEC_QUESTIONS
|
|
110
170
|
};
|
package/src/init.js
CHANGED
|
@@ -2,6 +2,8 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const readline = require('readline');
|
|
4
4
|
|
|
5
|
+
const { detectStackConfig, writeStackConfig, CONFIG_FILE: STACK_CONFIG_FILE } = require('./stack');
|
|
6
|
+
|
|
5
7
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
6
8
|
const TARGET_DIR = process.cwd();
|
|
7
9
|
|
|
@@ -41,7 +43,8 @@ function updateGitignore() {
|
|
|
41
43
|
const entriesToAdd = [
|
|
42
44
|
'# agent-workflow',
|
|
43
45
|
'.claude/implement-queue.json',
|
|
44
|
-
'.claude/pipeline-history.json'
|
|
46
|
+
'.claude/pipeline-history.json',
|
|
47
|
+
'.claude/stack-config.json'
|
|
45
48
|
];
|
|
46
49
|
|
|
47
50
|
let content = '';
|
|
@@ -109,12 +112,27 @@ async function init() {
|
|
|
109
112
|
// Update .gitignore
|
|
110
113
|
updateGitignore();
|
|
111
114
|
|
|
115
|
+
// Auto-detect tech stack
|
|
116
|
+
const stackConfigPath = path.join(TARGET_DIR, STACK_CONFIG_FILE);
|
|
117
|
+
if (!fs.existsSync(stackConfigPath)) {
|
|
118
|
+
const detected = detectStackConfig(TARGET_DIR);
|
|
119
|
+
const hasValues = detected.language || detected.runtime;
|
|
120
|
+
if (hasValues) {
|
|
121
|
+
writeStackConfig(detected);
|
|
122
|
+
const parts = [detected.language, detected.runtime, ...detected.frameworks, detected.testRunner].filter(Boolean);
|
|
123
|
+
console.log(`Detected tech stack: ${parts.join(', ')}`);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
console.log('Stack config already exists, skipping detection');
|
|
127
|
+
}
|
|
128
|
+
|
|
112
129
|
console.log(`
|
|
113
130
|
orchestr8 initialized successfully!
|
|
114
131
|
|
|
115
132
|
Next steps:
|
|
116
|
-
1.
|
|
117
|
-
2.
|
|
133
|
+
1. Review your tech stack with \`npx orchestr8 stack-config\`
|
|
134
|
+
2. Add business context documents to .business_context/
|
|
135
|
+
3. Run /implement-feature in Claude Code to start your first feature
|
|
118
136
|
`);
|
|
119
137
|
}
|
|
120
138
|
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const SESSION_STATES = {
|
|
5
|
+
IDLE: 'idle',
|
|
6
|
+
GATHERING: 'gathering',
|
|
7
|
+
QUESTIONING: 'questioning',
|
|
8
|
+
DRAFTING: 'drafting',
|
|
9
|
+
FINALIZING: 'finalizing'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const SECTION_ORDER = ['intent', 'scope', 'actors', 'behaviour', 'dependencies'];
|
|
13
|
+
|
|
14
|
+
const MIN_REQUIRED_SECTIONS = ['intent', 'scope', 'actors'];
|
|
15
|
+
|
|
16
|
+
const SYSTEM_SPEC_QUESTIONS = ['purpose', 'actors', 'boundaries', 'rules'];
|
|
17
|
+
|
|
18
|
+
function parseFlags(args) {
|
|
19
|
+
return {
|
|
20
|
+
interactive: args.includes('--interactive'),
|
|
21
|
+
pauseAfter: args.find(a => a.startsWith('--pause-after='))?.split('=')[1] || null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shouldEnterInteractiveMode(flags, hasSystemSpec, hasFeatureSpec) {
|
|
26
|
+
if (flags.interactive) return { interactive: true, target: 'feature' };
|
|
27
|
+
if (!hasSystemSpec) return { interactive: true, target: 'system' };
|
|
28
|
+
if (!hasFeatureSpec) return { interactive: true, target: 'feature' };
|
|
29
|
+
return { interactive: false, target: null };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createSession(target) {
|
|
33
|
+
const sections = target === 'system'
|
|
34
|
+
? { purpose: null, actors: null, boundaries: null, rules: null }
|
|
35
|
+
: { intent: null, scope: null, actors: null, behaviour: null, dependencies: null };
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
target,
|
|
39
|
+
state: SESSION_STATES.IDLE,
|
|
40
|
+
sections,
|
|
41
|
+
current: target === 'system' ? 'purpose' : 'intent',
|
|
42
|
+
revisionCount: 0,
|
|
43
|
+
questionCount: 0,
|
|
44
|
+
startedAt: new Date().toISOString(),
|
|
45
|
+
aborted: false,
|
|
46
|
+
specWritten: false,
|
|
47
|
+
context: {},
|
|
48
|
+
feedback: []
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getSessionProgress(session) {
|
|
53
|
+
const sectionList = Object.keys(session.sections);
|
|
54
|
+
const complete = Object.values(session.sections).filter(
|
|
55
|
+
s => s === 'complete' || s === 'TBD'
|
|
56
|
+
).length;
|
|
57
|
+
const remaining = sectionList.length - complete;
|
|
58
|
+
return { complete, remaining, total: sectionList.length };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function handleCommand(session, command) {
|
|
62
|
+
const cmd = command.trim().toLowerCase();
|
|
63
|
+
const parts = command.trim().split(/\s+/);
|
|
64
|
+
const baseCmd = parts[0].toLowerCase();
|
|
65
|
+
|
|
66
|
+
if (baseCmd === '/approve' || baseCmd === 'yes') {
|
|
67
|
+
session.sections[session.current] = 'complete';
|
|
68
|
+
const nextSection = getNextSection(session);
|
|
69
|
+
if (nextSection) {
|
|
70
|
+
session.current = nextSection;
|
|
71
|
+
return { action: 'next', section: nextSection };
|
|
72
|
+
}
|
|
73
|
+
return { action: 'finalize' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (baseCmd === '/change') {
|
|
77
|
+
const feedback = parts.slice(1).join(' ');
|
|
78
|
+
session.revisionCount++;
|
|
79
|
+
session.feedback.push({ section: session.current, feedback });
|
|
80
|
+
return { action: 'revise', feedback };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (baseCmd === '/skip') {
|
|
84
|
+
session.sections[session.current] = 'TBD';
|
|
85
|
+
const nextSection = getNextSection(session);
|
|
86
|
+
if (nextSection) {
|
|
87
|
+
session.current = nextSection;
|
|
88
|
+
return { action: 'next', section: nextSection };
|
|
89
|
+
}
|
|
90
|
+
return { action: 'finalize' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (baseCmd === '/restart') {
|
|
94
|
+
session.sections[session.current] = null;
|
|
95
|
+
return { action: 'restart', section: session.current };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (baseCmd === '/abort') {
|
|
99
|
+
session.aborted = true;
|
|
100
|
+
return { action: 'abort' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (baseCmd === '/done') {
|
|
104
|
+
if (canFinalize(session)) {
|
|
105
|
+
return { action: 'finalize' };
|
|
106
|
+
}
|
|
107
|
+
return { action: 'incomplete', missing: getMissingSections(session) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { action: 'unknown', command: baseCmd };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getNextSection(session) {
|
|
114
|
+
const order = session.target === 'system'
|
|
115
|
+
? SYSTEM_SPEC_QUESTIONS
|
|
116
|
+
: SECTION_ORDER;
|
|
117
|
+
|
|
118
|
+
for (const section of order) {
|
|
119
|
+
const status = session.sections[section];
|
|
120
|
+
if (status !== 'complete' && status !== 'TBD') {
|
|
121
|
+
return section;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function markSectionComplete(session, section) {
|
|
128
|
+
if (section in session.sections) {
|
|
129
|
+
session.sections[section] = 'complete';
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function markSectionTBD(session, section) {
|
|
136
|
+
if (section in session.sections) {
|
|
137
|
+
session.sections[section] = 'TBD';
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function gatherContext(basePath = '.') {
|
|
144
|
+
const context = {
|
|
145
|
+
systemSpec: null,
|
|
146
|
+
businessContext: [],
|
|
147
|
+
templates: []
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const systemSpecPath = path.join(basePath, '.blueprint/system_specification/SYSTEM_SPEC.md');
|
|
151
|
+
if (fs.existsSync(systemSpecPath)) {
|
|
152
|
+
context.systemSpec = fs.readFileSync(systemSpecPath, 'utf8');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const businessContextDir = path.join(basePath, '.business_context');
|
|
156
|
+
if (fs.existsSync(businessContextDir)) {
|
|
157
|
+
const files = fs.readdirSync(businessContextDir);
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
if (file.endsWith('.md')) {
|
|
160
|
+
const content = fs.readFileSync(path.join(businessContextDir, file), 'utf8');
|
|
161
|
+
context.businessContext.push({ file, content });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const templatesDir = path.join(basePath, '.blueprint/templates');
|
|
167
|
+
if (fs.existsSync(templatesDir)) {
|
|
168
|
+
const files = fs.readdirSync(templatesDir);
|
|
169
|
+
for (const file of files) {
|
|
170
|
+
if (file.endsWith('.md')) {
|
|
171
|
+
const content = fs.readFileSync(path.join(templatesDir, file), 'utf8');
|
|
172
|
+
context.templates.push({ file, content });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return context;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function identifyGaps(session, userDescription) {
|
|
181
|
+
const gaps = [];
|
|
182
|
+
const sectionKeys = session.target === 'system'
|
|
183
|
+
? SYSTEM_SPEC_QUESTIONS
|
|
184
|
+
: SECTION_ORDER;
|
|
185
|
+
|
|
186
|
+
for (const section of sectionKeys) {
|
|
187
|
+
const hasKey = `has${section.charAt(0).toUpperCase() + section.slice(1)}`;
|
|
188
|
+
if (!userDescription[hasKey]) {
|
|
189
|
+
gaps.push(section);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return gaps.slice(0, 4);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function generateQuestions(gaps) {
|
|
197
|
+
const questionTemplates = {
|
|
198
|
+
intent: 'What is the primary goal or purpose of this feature?',
|
|
199
|
+
scope: 'What boundaries define what is in scope vs out of scope?',
|
|
200
|
+
actors: 'Who are the users or systems that will interact with this?',
|
|
201
|
+
behaviour: 'What are the key behaviours or flows to support?',
|
|
202
|
+
dependencies: 'What does this depend on or what depends on it?',
|
|
203
|
+
purpose: 'What is the overall purpose of this system?',
|
|
204
|
+
boundaries: 'What are the system boundaries and integration points?',
|
|
205
|
+
rules: 'What business rules or constraints apply?'
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return gaps.slice(0, 4).map(gap => ({
|
|
209
|
+
section: gap,
|
|
210
|
+
question: questionTemplates[gap] || `What information is needed for ${gap}?`
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getMissingSections(session) {
|
|
215
|
+
const required = session.target === 'system'
|
|
216
|
+
? ['purpose', 'actors', 'boundaries']
|
|
217
|
+
: MIN_REQUIRED_SECTIONS;
|
|
218
|
+
|
|
219
|
+
return required.filter(section => {
|
|
220
|
+
const status = session.sections[section];
|
|
221
|
+
return status !== 'complete' && status !== 'TBD';
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function canFinalize(session) {
|
|
226
|
+
const required = session.target === 'system'
|
|
227
|
+
? ['purpose', 'actors', 'boundaries']
|
|
228
|
+
: MIN_REQUIRED_SECTIONS;
|
|
229
|
+
|
|
230
|
+
return required.every(section => {
|
|
231
|
+
const status = session.sections[section];
|
|
232
|
+
return status === 'complete' || status === 'TBD';
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function generateSpec(session) {
|
|
237
|
+
const sections = [];
|
|
238
|
+
const sectionOrder = session.target === 'system'
|
|
239
|
+
? SYSTEM_SPEC_QUESTIONS
|
|
240
|
+
: SECTION_ORDER;
|
|
241
|
+
|
|
242
|
+
const title = session.target === 'system' ? 'System Spec' : 'Feature Spec';
|
|
243
|
+
sections.push(`# ${title}`);
|
|
244
|
+
|
|
245
|
+
for (const sectionName of sectionOrder) {
|
|
246
|
+
const status = session.sections[sectionName];
|
|
247
|
+
const heading = sectionName.charAt(0).toUpperCase() + sectionName.slice(1);
|
|
248
|
+
sections.push(`## ${heading}`);
|
|
249
|
+
|
|
250
|
+
if (status === 'TBD') {
|
|
251
|
+
sections.push('TBD');
|
|
252
|
+
} else if (status === 'complete') {
|
|
253
|
+
sections.push(`[Content for ${sectionName}]`);
|
|
254
|
+
} else {
|
|
255
|
+
sections.push('TBD');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
sections.push('');
|
|
260
|
+
sections.push('_Created via interactive session_');
|
|
261
|
+
|
|
262
|
+
return sections.join('\n');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function writeSpec(session, outputPath) {
|
|
266
|
+
const content = generateSpec(session);
|
|
267
|
+
const dir = path.dirname(outputPath);
|
|
268
|
+
|
|
269
|
+
if (!fs.existsSync(dir)) {
|
|
270
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fs.writeFileSync(outputPath, content);
|
|
274
|
+
session.specWritten = true;
|
|
275
|
+
return outputPath;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function generateHandoff(session, slug) {
|
|
279
|
+
const progress = getSessionProgress(session);
|
|
280
|
+
const endedAt = new Date().toISOString();
|
|
281
|
+
const startedAt = new Date(session.startedAt);
|
|
282
|
+
const durationMs = new Date(endedAt) - startedAt;
|
|
283
|
+
|
|
284
|
+
const lines = [
|
|
285
|
+
'## Handoff Summary',
|
|
286
|
+
'**For:** Cass',
|
|
287
|
+
`**Feature:** ${slug}`,
|
|
288
|
+
'',
|
|
289
|
+
'### Key Decisions',
|
|
290
|
+
`- Interactive mode used for ${session.target} spec creation`,
|
|
291
|
+
`- ${progress.complete}/${progress.total} sections completed`,
|
|
292
|
+
session.revisionCount > 0 ? `- ${session.revisionCount} revision(s) made` : null,
|
|
293
|
+
'',
|
|
294
|
+
'### Files Created',
|
|
295
|
+
session.target === 'system'
|
|
296
|
+
? '- .blueprint/system_specification/SYSTEM_SPEC.md'
|
|
297
|
+
: `- .blueprint/features/feature_${slug}/FEATURE_SPEC.md`,
|
|
298
|
+
'',
|
|
299
|
+
'### Open Questions',
|
|
300
|
+
progress.remaining > 0 ? `- ${progress.remaining} section(s) marked TBD` : '- None',
|
|
301
|
+
'',
|
|
302
|
+
'### Critical Context',
|
|
303
|
+
`Session duration: ${Math.round(durationMs / 1000)}s, Questions asked: ${session.questionCount}, Revisions: ${session.revisionCount}`
|
|
304
|
+
].filter(line => line !== null);
|
|
305
|
+
|
|
306
|
+
return lines.join('\n');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getOutputPath(session, slug, basePath = '.') {
|
|
310
|
+
if (session.target === 'system') {
|
|
311
|
+
return path.join(basePath, '.blueprint/system_specification/SYSTEM_SPEC.md');
|
|
312
|
+
}
|
|
313
|
+
return path.join(basePath, `.blueprint/features/feature_${slug}/FEATURE_SPEC.md`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = {
|
|
317
|
+
SESSION_STATES,
|
|
318
|
+
SECTION_ORDER,
|
|
319
|
+
MIN_REQUIRED_SECTIONS,
|
|
320
|
+
SYSTEM_SPEC_QUESTIONS,
|
|
321
|
+
parseFlags,
|
|
322
|
+
shouldEnterInteractiveMode,
|
|
323
|
+
createSession,
|
|
324
|
+
getSessionProgress,
|
|
325
|
+
handleCommand,
|
|
326
|
+
getNextSection,
|
|
327
|
+
markSectionComplete,
|
|
328
|
+
markSectionTBD,
|
|
329
|
+
gatherContext,
|
|
330
|
+
identifyGaps,
|
|
331
|
+
generateQuestions,
|
|
332
|
+
getMissingSections,
|
|
333
|
+
canFinalize,
|
|
334
|
+
generateSpec,
|
|
335
|
+
writeSpec,
|
|
336
|
+
generateHandoff,
|
|
337
|
+
getOutputPath
|
|
338
|
+
};
|