@hyperdrive.bot/bmad-workflow 1.0.26 → 1.0.28

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 (72) hide show
  1. package/dist/commands/epics/create.d.ts +1 -0
  2. package/dist/commands/lock/acquire.d.ts +54 -0
  3. package/dist/commands/lock/acquire.js +193 -0
  4. package/dist/commands/lock/cleanup.d.ts +38 -0
  5. package/dist/commands/lock/cleanup.js +148 -0
  6. package/dist/commands/lock/list.d.ts +31 -0
  7. package/dist/commands/lock/list.js +123 -0
  8. package/dist/commands/lock/release.d.ts +42 -0
  9. package/dist/commands/lock/release.js +134 -0
  10. package/dist/commands/lock/status.d.ts +34 -0
  11. package/dist/commands/lock/status.js +109 -0
  12. package/dist/commands/stories/create.d.ts +1 -0
  13. package/dist/commands/stories/develop.d.ts +4 -0
  14. package/dist/commands/stories/develop.js +55 -5
  15. package/dist/commands/stories/qa.d.ts +1 -0
  16. package/dist/commands/stories/qa.js +31 -0
  17. package/dist/commands/stories/review.d.ts +1 -0
  18. package/dist/commands/workflow.d.ts +11 -0
  19. package/dist/commands/workflow.js +120 -4
  20. package/dist/models/agent-options.d.ts +33 -0
  21. package/dist/models/agent-result.d.ts +10 -1
  22. package/dist/models/dispatch.d.ts +16 -0
  23. package/dist/models/dispatch.js +8 -0
  24. package/dist/models/index.d.ts +3 -0
  25. package/dist/models/index.js +2 -0
  26. package/dist/models/lock.d.ts +80 -0
  27. package/dist/models/lock.js +69 -0
  28. package/dist/models/phase-result.d.ts +8 -0
  29. package/dist/models/provider.js +1 -1
  30. package/dist/models/workflow-callbacks.d.ts +37 -0
  31. package/dist/models/workflow-config.d.ts +50 -0
  32. package/dist/services/agents/agent-runner-factory.d.ts +14 -15
  33. package/dist/services/agents/agent-runner-factory.js +56 -15
  34. package/dist/services/agents/channel-agent-runner.d.ts +76 -0
  35. package/dist/services/agents/channel-agent-runner.js +246 -0
  36. package/dist/services/agents/channel-session-manager.d.ts +119 -0
  37. package/dist/services/agents/channel-session-manager.js +250 -0
  38. package/dist/services/agents/claude-agent-runner.d.ts +9 -50
  39. package/dist/services/agents/claude-agent-runner.js +221 -199
  40. package/dist/services/agents/gemini-agent-runner.js +3 -0
  41. package/dist/services/agents/index.d.ts +1 -0
  42. package/dist/services/agents/index.js +1 -0
  43. package/dist/services/agents/opencode-agent-runner.js +3 -0
  44. package/dist/services/file-system/file-manager.d.ts +11 -0
  45. package/dist/services/file-system/file-manager.js +26 -0
  46. package/dist/services/git/git-ops.d.ts +58 -0
  47. package/dist/services/git/git-ops.js +73 -0
  48. package/dist/services/git/index.d.ts +3 -0
  49. package/dist/services/git/index.js +2 -0
  50. package/dist/services/git/push-conflict-handler.d.ts +32 -0
  51. package/dist/services/git/push-conflict-handler.js +84 -0
  52. package/dist/services/lock/git-backed-lock-service.d.ts +76 -0
  53. package/dist/services/lock/git-backed-lock-service.js +173 -0
  54. package/dist/services/lock/lock-cleanup.d.ts +49 -0
  55. package/dist/services/lock/lock-cleanup.js +85 -0
  56. package/dist/services/lock/lock-service.d.ts +143 -0
  57. package/dist/services/lock/lock-service.js +290 -0
  58. package/dist/services/orchestration/locked-story-dispatcher.d.ts +40 -0
  59. package/dist/services/orchestration/locked-story-dispatcher.js +84 -0
  60. package/dist/services/orchestration/workflow-orchestrator.d.ts +31 -0
  61. package/dist/services/orchestration/workflow-orchestrator.js +181 -31
  62. package/dist/services/review/ai-review-scanner.js +1 -0
  63. package/dist/services/review/review-phase-executor.js +3 -0
  64. package/dist/services/review/self-heal-loop.js +1 -0
  65. package/dist/services/review/types.d.ts +2 -0
  66. package/dist/utils/errors.d.ts +17 -1
  67. package/dist/utils/errors.js +18 -0
  68. package/dist/utils/session-naming.d.ts +23 -0
  69. package/dist/utils/session-naming.js +30 -0
  70. package/dist/utils/shared-flags.d.ts +1 -0
  71. package/dist/utils/shared-flags.js +5 -0
  72. package/package.json +3 -2
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Lock Release Command
3
+ *
4
+ * Releases an existing lock on a story file by deleting the `.lock` file,
5
+ * committing the removal, and pushing to remote.
6
+ *
7
+ * Flow: git pull → validate lock exists → delete .lock → git add+commit+push
8
+ *
9
+ * @example
10
+ * ```bash
11
+ * bmad-workflow lock release --story docs/qa/stories/X.md
12
+ * ```
13
+ */
14
+ import { Command, Flags } from '@oclif/core';
15
+ import fs from 'fs-extra';
16
+ import { FileManager } from '../../services/file-system/file-manager.js';
17
+ import { GlobMatcher } from '../../services/file-system/glob-matcher.js';
18
+ import { GitOps } from '../../services/git/git-ops.js';
19
+ import { LockService } from '../../services/lock/lock-service.js';
20
+ import { createLogger } from '../../utils/logger.js';
21
+ export default class LockReleaseCommand extends Command {
22
+ static description = 'Release a lock on a story file';
23
+ static examples = [
24
+ {
25
+ command: '<%= config.bin %> lock release --story docs/qa/stories/X.md',
26
+ description: 'Release the lock on a story file',
27
+ },
28
+ ];
29
+ static flags = {
30
+ json: Flags.boolean({ description: 'Output as JSON' }),
31
+ story: Flags.string({ description: 'Path to story file', required: true }),
32
+ };
33
+ gitOps;
34
+ lockService;
35
+ logger;
36
+ async run() {
37
+ this.initServices();
38
+ const { flags } = await this.parse(LockReleaseCommand);
39
+ const cwd = process.cwd();
40
+ const lockFilePath = flags.story + '.lock';
41
+ // Check lock existence BEFORE pull (needed to detect AC 4 race case)
42
+ const lockedBeforePull = await this.lockService.isLocked(flags.story);
43
+ // AC 2: Pull latest to ensure fresh state
44
+ try {
45
+ this.gitOps.pull(cwd);
46
+ }
47
+ catch {
48
+ this.logger.warn('git pull failed — proceeding with local state');
49
+ }
50
+ // Check lock existence AFTER pull
51
+ const lockedAfterPull = await this.lockService.isLocked(flags.story);
52
+ if (!lockedAfterPull) {
53
+ // AC 4: Lock existed before pull but gone after → another agent released it
54
+ if (lockedBeforePull) {
55
+ return this.succeedAlreadyReleased(flags);
56
+ }
57
+ // AC 3: Neither story file nor lock exists
58
+ const storyExists = await fs.pathExists(flags.story);
59
+ if (!storyExists) {
60
+ return this.fail(flags, 'Story file not found: ' + flags.story);
61
+ }
62
+ // AC 2: Story exists but no lock file
63
+ return this.fail(flags, 'No lock found for: ' + flags.story);
64
+ }
65
+ // AC 1: Delete the lock file
66
+ await this.lockService.release(flags.story);
67
+ // AC 1, 5: Stage deletion, commit, and push
68
+ this.gitOps.add(lockFilePath, cwd);
69
+ this.gitOps.commit('release: ' + flags.story, cwd);
70
+ const pushResult = this.gitOps.push(cwd);
71
+ if (!pushResult.success) {
72
+ // AC 5: Retry once after pull
73
+ try {
74
+ this.gitOps.pull(cwd);
75
+ const retryResult = this.gitOps.push(cwd);
76
+ if (!retryResult.success) {
77
+ return this.fail(flags, 'Failed to push lock release — manual intervention required');
78
+ }
79
+ }
80
+ catch {
81
+ return this.fail(flags, 'Failed to push lock release — manual intervention required');
82
+ }
83
+ }
84
+ // AC 6, 7: Success output
85
+ this.succeed(flags);
86
+ }
87
+ /**
88
+ * Output failure message in JSON or human-readable format, then exit 1
89
+ */
90
+ fail(flags, message) {
91
+ if (flags.json) {
92
+ this.log(JSON.stringify({ error: message, story: flags.story, success: false }));
93
+ this.exit(1);
94
+ }
95
+ this.error(message, { exit: 1 });
96
+ }
97
+ initServices() {
98
+ this.logger = createLogger({ namespace: 'commands:lock:release' });
99
+ const fileManager = new FileManager(this.logger);
100
+ const globMatcher = new GlobMatcher(fileManager, this.logger);
101
+ this.lockService = new LockService(fileManager, globMatcher, this.logger);
102
+ this.gitOps = new GitOps();
103
+ }
104
+ /**
105
+ * Output idempotent success when lock was already released by another agent (AC 4)
106
+ */
107
+ succeedAlreadyReleased(flags) {
108
+ if (flags.json) {
109
+ this.log(JSON.stringify({
110
+ story: flags.story,
111
+ success: true,
112
+ timestamp: new Date().toISOString(),
113
+ }));
114
+ }
115
+ else {
116
+ this.log('Lock already released for: ' + flags.story);
117
+ }
118
+ }
119
+ /**
120
+ * Output success message in JSON or human-readable format
121
+ */
122
+ succeed(flags) {
123
+ if (flags.json) {
124
+ this.log(JSON.stringify({
125
+ story: flags.story,
126
+ success: true,
127
+ timestamp: new Date().toISOString(),
128
+ }));
129
+ }
130
+ else {
131
+ this.log('✓ Lock released: ' + flags.story);
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Lock Status Command
3
+ *
4
+ * Queries the lock state for a specific story file.
5
+ * Returns one of three states: unlocked, locked, or stale.
6
+ *
7
+ * Exit codes:
8
+ * - 0: unlocked
9
+ * - 1: locked (active, non-stale)
10
+ * - 2: stale lock
11
+ *
12
+ * @example
13
+ * ```bash
14
+ * bmad-workflow lock status --story docs/stories/X.md
15
+ * bmad-workflow lock status --story docs/stories/X.md --json
16
+ * bmad-workflow lock status --story docs/stories/X.md; echo $?
17
+ * ```
18
+ */
19
+ import { Command } from '@oclif/core';
20
+ export default class LockStatusCommand extends Command {
21
+ static description: string;
22
+ static examples: {
23
+ command: string;
24
+ description: string;
25
+ }[];
26
+ static flags: {
27
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
28
+ story: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
29
+ };
30
+ private lockService;
31
+ private logger;
32
+ run(): Promise<void>;
33
+ private initServices;
34
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Lock Status Command
3
+ *
4
+ * Queries the lock state for a specific story file.
5
+ * Returns one of three states: unlocked, locked, or stale.
6
+ *
7
+ * Exit codes:
8
+ * - 0: unlocked
9
+ * - 1: locked (active, non-stale)
10
+ * - 2: stale lock
11
+ *
12
+ * @example
13
+ * ```bash
14
+ * bmad-workflow lock status --story docs/stories/X.md
15
+ * bmad-workflow lock status --story docs/stories/X.md --json
16
+ * bmad-workflow lock status --story docs/stories/X.md; echo $?
17
+ * ```
18
+ */
19
+ import { execSync } from 'node:child_process';
20
+ import { Command, Flags } from '@oclif/core';
21
+ import fs from 'fs-extra';
22
+ import { FileManager } from '../../services/file-system/file-manager.js';
23
+ import { GlobMatcher } from '../../services/file-system/glob-matcher.js';
24
+ import { LockService } from '../../services/lock/lock-service.js';
25
+ import { createLogger } from '../../utils/logger.js';
26
+ export default class LockStatusCommand extends Command {
27
+ static description = 'Check lock status for a story file';
28
+ static examples = [
29
+ {
30
+ command: '<%= config.bin %> lock status --story docs/stories/X.md',
31
+ description: 'Check lock status (human-readable)',
32
+ },
33
+ {
34
+ command: '<%= config.bin %> lock status --story docs/stories/X.md --json',
35
+ description: 'Check lock status as JSON',
36
+ },
37
+ {
38
+ command: '<%= config.bin %> lock status --story docs/stories/X.md; echo $?',
39
+ description: 'Use exit code for scripted checks (0=unlocked, 1=locked, 2=stale)',
40
+ },
41
+ ];
42
+ static flags = {
43
+ json: Flags.boolean({ description: 'Output as JSON' }),
44
+ story: Flags.string({ description: 'Path to the story file', required: true }),
45
+ };
46
+ lockService;
47
+ logger;
48
+ async run() {
49
+ this.initServices();
50
+ const { flags } = await this.parse(LockStatusCommand);
51
+ // AC 4: Validate story file exists
52
+ const storyExists = await fs.pathExists(flags.story);
53
+ if (!storyExists) {
54
+ if (flags.json) {
55
+ this.log(JSON.stringify({ error: 'Story file not found: ' + flags.story, state: 'error', story: flags.story }));
56
+ }
57
+ else {
58
+ this.error('Story file not found: ' + flags.story, { exit: 1 });
59
+ }
60
+ this.exit(1);
61
+ }
62
+ // AC 9: Pull latest before reading lock state
63
+ try {
64
+ execSync('git pull --ff-only', { cwd: process.cwd(), stdio: 'pipe' });
65
+ }
66
+ catch {
67
+ this.logger.warn('git pull failed — proceeding with local state');
68
+ }
69
+ // Query lock state
70
+ const lockInfo = await this.lockService.getLockInfo(flags.story);
71
+ if (lockInfo === null) {
72
+ // Unlocked
73
+ if (flags.json) {
74
+ this.log(JSON.stringify({ state: 'unlocked', story: flags.story }, null, 2));
75
+ }
76
+ else {
77
+ this.log('unlocked');
78
+ }
79
+ this.exit(0);
80
+ }
81
+ // Lock exists — check staleness
82
+ const stale = await this.lockService.isStale(flags.story);
83
+ const state = stale ? 'stale' : 'locked';
84
+ const exitCode = stale ? 2 : 1;
85
+ if (flags.json) {
86
+ this.log(JSON.stringify({
87
+ agent: lockInfo.agent_name,
88
+ branch: lockInfo.branch,
89
+ session: lockInfo.agent_session_id,
90
+ started_at: lockInfo.started_at,
91
+ state,
92
+ story: flags.story,
93
+ }, null, 2));
94
+ }
95
+ else if (stale) {
96
+ this.log(`stale lock by ${lockInfo.agent_name} since ${lockInfo.started_at} (session: ${lockInfo.agent_session_id}, branch: ${lockInfo.branch})`);
97
+ }
98
+ else {
99
+ this.log(`locked by ${lockInfo.agent_name} since ${lockInfo.started_at} (session: ${lockInfo.agent_session_id}, branch: ${lockInfo.branch})`);
100
+ }
101
+ this.exit(exitCode);
102
+ }
103
+ initServices() {
104
+ this.logger = createLogger({ namespace: 'commands:lock:status' });
105
+ const fileManager = new FileManager(this.logger);
106
+ const globMatcher = new GlobMatcher(fileManager, this.logger);
107
+ this.lockService = new LockService(fileManager, globMatcher, this.logger);
108
+ }
109
+ }
@@ -33,6 +33,7 @@ export default class StoriesCreateCommand extends Command {
33
33
  prefix: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
34
34
  reference: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
35
35
  start: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
36
+ stream: import("@oclif/core/interfaces").BooleanFlag<boolean>;
36
37
  agent: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
37
38
  cwd: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
38
39
  model: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -31,10 +31,13 @@ export default class StoriesDevelopCommand extends Command {
31
31
  }[];
32
32
  static flags: {
33
33
  interval: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
34
+ 'skip-locking': import("@oclif/core/interfaces").BooleanFlag<boolean>;
34
35
  qa: import("@oclif/core/interfaces").BooleanFlag<boolean>;
35
36
  'qa-prompt': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
36
37
  'qa-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
37
38
  reference: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
39
+ 'session-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
40
+ stream: import("@oclif/core/interfaces").BooleanFlag<boolean>;
38
41
  agent: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
39
42
  cwd: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
40
43
  model: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -48,6 +51,7 @@ export default class StoriesDevelopCommand extends Command {
48
51
  private agentRunner;
49
52
  private fileManager;
50
53
  private globMatcher;
54
+ private lockService;
51
55
  private logger;
52
56
  private pathResolver;
53
57
  private storyParserFactory;
@@ -20,6 +20,10 @@ import { createAgentRunner } from '../../services/agents/agent-runner-factory.js
20
20
  import { FileManager } from '../../services/file-system/file-manager.js';
21
21
  import { GlobMatcher } from '../../services/file-system/glob-matcher.js';
22
22
  import { PathResolver } from '../../services/file-system/path-resolver.js';
23
+ import { GitOps } from '../../services/git/git-ops.js';
24
+ import { PushConflictHandler } from '../../services/git/push-conflict-handler.js';
25
+ import { GitBackedLockService } from '../../services/lock/git-backed-lock-service.js';
26
+ import { LockService } from '../../services/lock/lock-service.js';
23
27
  import { StoryTypeDetector } from '../../services/orchestration/story-type-detector.js';
24
28
  import { StoryParserFactory } from '../../services/parsers/story-parser-factory.js';
25
29
  import * as colors from '../../utils/colors.js';
@@ -73,6 +77,11 @@ export default class StoriesDevelopCommand extends Command {
73
77
  default: 30,
74
78
  description: 'Seconds to wait between stories',
75
79
  }),
80
+ 'skip-locking': Flags.boolean({
81
+ allowNo: true,
82
+ default: true,
83
+ description: 'Disable story locking (for single-agent runs)',
84
+ }),
76
85
  qa: Flags.boolean({
77
86
  default: false,
78
87
  description: 'Run QA workflow after development completes',
@@ -91,11 +100,17 @@ export default class StoriesDevelopCommand extends Command {
91
100
  description: 'Additional context files for dev agents',
92
101
  multiple: true,
93
102
  }),
103
+ 'session-name': Flags.string({
104
+ char: 'n',
105
+ description: 'Session display name template. Supports {story} placeholder. ' +
106
+ 'Defaults to "dev-{story}" if not provided.',
107
+ }),
94
108
  };
95
109
  // Service instances
96
110
  agentRunner;
97
111
  fileManager;
98
112
  globMatcher;
113
+ lockService = null;
99
114
  logger;
100
115
  pathResolver;
101
116
  storyParserFactory;
@@ -107,9 +122,10 @@ export default class StoriesDevelopCommand extends Command {
107
122
  const { args, flags } = await this.parse(StoriesDevelopCommand);
108
123
  const startTime = Date.now();
109
124
  const correlationId = generateCorrelationId();
110
- // Initialize services with selected provider
125
+ // Initialize services with selected provider and locking
111
126
  const provider = (flags.provider || 'claude');
112
- this.initializeServices(provider);
127
+ const lockingEnabled = !flags['skip-locking'];
128
+ this.initializeServices(provider, lockingEnabled);
113
129
  this.logger.info({
114
130
  correlationId,
115
131
  flags,
@@ -244,12 +260,32 @@ export default class StoriesDevelopCommand extends Command {
244
260
  // Develop story
245
261
  const devSpinner = createSpinner('Running dev agent...');
246
262
  devSpinner.start();
263
+ // Resolve session name with {story} placeholder
264
+ const storyIdentifier = isEpicStory(storyMetadata) ? storyMetadata.number : storyMetadata.id;
265
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- oclif type inference limit with spread flags
266
+ const sessionNameTemplate = flags['session-name'];
267
+ const resolvedSessionName = sessionNameTemplate
268
+ ? sessionNameTemplate.replace(/\{story\}/g, storyIdentifier)
269
+ : `dev-${storyIdentifier}`;
270
+ // When --stream is set, stop the spinner and pipe raw output to stdout
271
+ const isStreaming = flags.stream === true;
272
+ if (isStreaming) {
273
+ devSpinner.stop();
274
+ this.log(colors.dim(`── [dev ${storyIdentifier}] streaming ──`));
275
+ }
247
276
  const result = await this.developStory({
248
277
  agent: flags.agent,
249
278
  cwd: flags.cwd,
250
279
  maxRetries: flags['max-retries'],
280
+ onStream: isStreaming ? undefined : (summary) => {
281
+ devSpinner.text = `[dev ${storyIdentifier}] ${summary}`;
282
+ },
283
+ onStreamVerbose: isStreaming ? (text) => {
284
+ process.stdout.write(text + '\n');
285
+ } : undefined,
251
286
  references: flags.reference,
252
287
  retryBackoff: flags['retry-backoff'],
288
+ sessionName: resolvedSessionName,
253
289
  storyMetadata,
254
290
  storyPath,
255
291
  task: flags.task,
@@ -274,7 +310,7 @@ export default class StoriesDevelopCommand extends Command {
274
310
  * Develop a single story
275
311
  */
276
312
  async developStory(options) {
277
- const { agent, cwd, maxRetries, references, retryBackoff, storyMetadata, storyPath, task, timeout } = options;
313
+ const { agent, cwd, maxRetries, onStream, onStreamVerbose, references, retryBackoff, sessionName, storyMetadata, storyPath, task, timeout } = options;
278
314
  const storyNumber = isEpicStory(storyMetadata) ? storyMetadata.number : storyMetadata.id;
279
315
  this.logger.info({ storyNumber, storyPath }, 'Starting story development');
280
316
  try {
@@ -309,6 +345,9 @@ export default class StoriesDevelopCommand extends Command {
309
345
  const result = await runAgentWithRetry(this.agentRunner, prompt, {
310
346
  agentType: 'dev',
311
347
  cwd,
348
+ onStream,
349
+ onStreamVerbose,
350
+ sessionName,
312
351
  timeout: timeout ?? 2_700_000,
313
352
  }, {
314
353
  backoffMs: retryBackoff,
@@ -391,7 +430,7 @@ export default class StoriesDevelopCommand extends Command {
391
430
  /**
392
431
  * Initialize service dependencies
393
432
  */
394
- initializeServices(provider = 'claude') {
433
+ initializeServices(provider = 'claude', lockingEnabled = false) {
395
434
  this.logger = createLogger({ namespace: 'commands:stories:develop' });
396
435
  this.logger.info({ provider }, 'Initializing services with AI provider');
397
436
  this.fileManager = new FileManager(this.logger);
@@ -400,7 +439,18 @@ export default class StoriesDevelopCommand extends Command {
400
439
  this.storyParserFactory = new StoryParserFactory(this.fileManager, this.logger);
401
440
  this.storyTypeDetector = new StoryTypeDetector(this.logger);
402
441
  this.agentRunner = createAgentRunner(provider, this.logger);
403
- this.logger.debug({ provider }, 'Services initialized successfully');
442
+ // Conditionally instantiate lock service for multi-session coordination
443
+ if (lockingEnabled) {
444
+ const gitOps = new GitOps();
445
+ const conflictHandler = new PushConflictHandler(gitOps);
446
+ const lockSvc = new LockService(this.fileManager, this.globMatcher, this.logger);
447
+ this.lockService = new GitBackedLockService(lockSvc, gitOps, conflictHandler);
448
+ this.logger.info('GitBackedLockService instantiated for multi-session coordination');
449
+ }
450
+ else {
451
+ this.lockService = null;
452
+ }
453
+ this.logger.debug({ lockingEnabled, provider }, 'Services initialized successfully');
404
454
  }
405
455
  /**
406
456
  * Match story files using glob pattern
@@ -40,6 +40,7 @@ export default class StoriesQaCommand extends Command {
40
40
  timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
41
41
  'agent-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
42
42
  'retry-backoff': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
43
+ stream: import("@oclif/core/interfaces").BooleanFlag<boolean>;
43
44
  };
44
45
  private agentRunner;
45
46
  private fileManager;
@@ -106,6 +106,11 @@ export default class StoriesQaCommand extends Command {
106
106
  description: 'Backoff delay between retries in milliseconds',
107
107
  helpGroup: 'Resilience',
108
108
  }),
109
+ stream: Flags.boolean({
110
+ default: false,
111
+ description: 'Stream full Claude output to stdout in real-time (verbose passthrough)',
112
+ helpGroup: 'Output',
113
+ }),
109
114
  };
110
115
  // Service instances
111
116
  agentRunner;
@@ -397,6 +402,7 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
397
402
  'max-retries': flags['max-retries'],
398
403
  'qa-prompt': flags['qa-prompt'],
399
404
  reference: flags.reference,
405
+ stream: flags.stream,
400
406
  });
401
407
  results.push(result);
402
408
  // Log result
@@ -423,10 +429,25 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
423
429
  const agentTimeout = flags.timeout ?? 2_700_000;
424
430
  const agentRetries = flags['agent-retries'];
425
431
  const retryBackoff = flags['retry-backoff'];
432
+ const isStreaming = flags.stream === true;
433
+ // Stream callbacks
434
+ const qaStreamLabel = `qa ${storyNumber}`;
435
+ const devStreamLabel = `dev-fix ${storyNumber}`;
436
+ const logStream = (label) => (summary) => {
437
+ this.logger.info({ phase: label }, summary);
438
+ };
439
+ const verboseStream = isStreaming ? (text) => {
440
+ process.stdout.write(text + '\n');
441
+ } : undefined;
442
+ if (isStreaming) {
443
+ this.log(colors.dim(`── [qa ${storyNumber}] Phase 1: QA Deep Dive ──`));
444
+ }
426
445
  const qaPrompt = this.buildQaPrompt(storyPath, flags['qa-prompt'], flags.reference);
427
446
  const qaResult = await runAgentWithRetry(this.agentRunner, qaPrompt, {
428
447
  agentType: 'tea',
429
448
  cwd: flags.cwd,
449
+ onStream: isStreaming ? undefined : logStream(qaStreamLabel),
450
+ onStreamVerbose: verboseStream,
430
451
  timeout: agentTimeout,
431
452
  }, {
432
453
  backoffMs: retryBackoff,
@@ -445,11 +466,16 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
445
466
  retriesUsed++;
446
467
  this.logger.info({ retry: retriesUsed, maxRetries: flags['max-retries'] }, 'Phase 2: Dev Fix-Forward');
447
468
  // Run Dev agent to fix issues (sequential retry loop by design)
469
+ if (isStreaming) {
470
+ this.log(colors.dim(`── [dev-fix ${storyNumber}] Retry ${retriesUsed} ──`));
471
+ }
448
472
  const devPrompt = this.buildDevFixPrompt(storyPath, flags['dev-prompt'], flags.reference);
449
473
  // eslint-disable-next-line no-await-in-loop
450
474
  const devResult = await runAgentWithRetry(this.agentRunner, devPrompt, {
451
475
  agentType: 'dev',
452
476
  cwd: flags.cwd,
477
+ onStream: isStreaming ? undefined : logStream(devStreamLabel),
478
+ onStreamVerbose: verboseStream,
453
479
  timeout: agentTimeout,
454
480
  }, {
455
481
  backoffMs: retryBackoff,
@@ -462,10 +488,15 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
462
488
  }
463
489
  // Phase 3: Re-run QA to validate fixes
464
490
  this.logger.info({ retry: retriesUsed }, 'Phase 3: QA Re-validation');
491
+ if (isStreaming) {
492
+ this.log(colors.dim(`── [qa ${storyNumber}] Re-validation after retry ${retriesUsed} ──`));
493
+ }
465
494
  // eslint-disable-next-line no-await-in-loop
466
495
  const reQaResult = await runAgentWithRetry(this.agentRunner, qaPrompt, {
467
496
  agentType: 'tea',
468
497
  cwd: flags.cwd,
498
+ onStream: isStreaming ? undefined : logStream(qaStreamLabel),
499
+ onStreamVerbose: verboseStream,
469
500
  timeout: agentTimeout,
470
501
  }, {
471
502
  backoffMs: retryBackoff,
@@ -60,6 +60,7 @@ export default class StoriesReviewCommand extends Command {
60
60
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
61
61
  'max-fix': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
62
62
  scanners: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
63
+ stream: import("@oclif/core/interfaces").BooleanFlag<boolean>;
63
64
  agent: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
64
65
  cwd: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
65
66
  model: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -37,6 +37,7 @@ export default class Workflow extends Command {
37
37
  pipeline: import("@oclif/core/interfaces").BooleanFlag<boolean>;
38
38
  'prd-interval': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
39
39
  model: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
40
+ 'session-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
40
41
  prefix: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
41
42
  'session-prefix': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
42
43
  provider: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -53,12 +54,15 @@ export default class Workflow extends Command {
53
54
  reference: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
54
55
  'skip-dev': import("@oclif/core/interfaces").BooleanFlag<boolean>;
55
56
  'skip-epics': import("@oclif/core/interfaces").BooleanFlag<boolean>;
57
+ 'skip-locking': import("@oclif/core/interfaces").BooleanFlag<boolean>;
56
58
  'skip-stories': import("@oclif/core/interfaces").BooleanFlag<boolean>;
57
59
  'story-interval': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
58
60
  timeout: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
59
61
  'review-timeout': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
60
62
  'max-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
61
63
  'retry-backoff': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
64
+ 'use-channels': import("@oclif/core/interfaces").BooleanFlag<boolean>;
65
+ stream: import("@oclif/core/interfaces").BooleanFlag<boolean>;
62
66
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
63
67
  };
64
68
  private cancelled;
@@ -192,4 +196,11 @@ export default class Workflow extends Command {
192
196
  * @private
193
197
  */
194
198
  private registerSignalHandlers;
199
+ /**
200
+ * Validate that all requested gut entities exist before attempting worktree creation.
201
+ * Reads .gut/config.json directly for fast validation without subprocess overhead.
202
+ * Fails fast with clear error message listing valid entities and fuzzy suggestions.
203
+ */
204
+ private validateGutEntities;
205
+ private showEntityError;
195
206
  }