@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.
- package/dist/commands/epics/create.d.ts +1 -0
- package/dist/commands/lock/acquire.d.ts +54 -0
- package/dist/commands/lock/acquire.js +193 -0
- package/dist/commands/lock/cleanup.d.ts +38 -0
- package/dist/commands/lock/cleanup.js +148 -0
- package/dist/commands/lock/list.d.ts +31 -0
- package/dist/commands/lock/list.js +123 -0
- package/dist/commands/lock/release.d.ts +42 -0
- package/dist/commands/lock/release.js +134 -0
- package/dist/commands/lock/status.d.ts +34 -0
- package/dist/commands/lock/status.js +109 -0
- package/dist/commands/stories/create.d.ts +1 -0
- package/dist/commands/stories/develop.d.ts +4 -0
- package/dist/commands/stories/develop.js +55 -5
- package/dist/commands/stories/qa.d.ts +1 -0
- package/dist/commands/stories/qa.js +31 -0
- package/dist/commands/stories/review.d.ts +1 -0
- package/dist/commands/workflow.d.ts +11 -0
- package/dist/commands/workflow.js +120 -4
- package/dist/models/agent-options.d.ts +33 -0
- package/dist/models/agent-result.d.ts +10 -1
- package/dist/models/dispatch.d.ts +16 -0
- package/dist/models/dispatch.js +8 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.js +2 -0
- package/dist/models/lock.d.ts +80 -0
- package/dist/models/lock.js +69 -0
- package/dist/models/phase-result.d.ts +8 -0
- package/dist/models/provider.js +1 -1
- package/dist/models/workflow-callbacks.d.ts +37 -0
- package/dist/models/workflow-config.d.ts +50 -0
- package/dist/services/agents/agent-runner-factory.d.ts +14 -15
- package/dist/services/agents/agent-runner-factory.js +56 -15
- package/dist/services/agents/channel-agent-runner.d.ts +76 -0
- package/dist/services/agents/channel-agent-runner.js +246 -0
- package/dist/services/agents/channel-session-manager.d.ts +119 -0
- package/dist/services/agents/channel-session-manager.js +250 -0
- package/dist/services/agents/claude-agent-runner.d.ts +9 -50
- package/dist/services/agents/claude-agent-runner.js +221 -199
- package/dist/services/agents/gemini-agent-runner.js +3 -0
- package/dist/services/agents/index.d.ts +1 -0
- package/dist/services/agents/index.js +1 -0
- package/dist/services/agents/opencode-agent-runner.js +3 -0
- package/dist/services/file-system/file-manager.d.ts +11 -0
- package/dist/services/file-system/file-manager.js +26 -0
- package/dist/services/git/git-ops.d.ts +58 -0
- package/dist/services/git/git-ops.js +73 -0
- package/dist/services/git/index.d.ts +3 -0
- package/dist/services/git/index.js +2 -0
- package/dist/services/git/push-conflict-handler.d.ts +32 -0
- package/dist/services/git/push-conflict-handler.js +84 -0
- package/dist/services/lock/git-backed-lock-service.d.ts +76 -0
- package/dist/services/lock/git-backed-lock-service.js +173 -0
- package/dist/services/lock/lock-cleanup.d.ts +49 -0
- package/dist/services/lock/lock-cleanup.js +85 -0
- package/dist/services/lock/lock-service.d.ts +143 -0
- package/dist/services/lock/lock-service.js +290 -0
- package/dist/services/orchestration/locked-story-dispatcher.d.ts +40 -0
- package/dist/services/orchestration/locked-story-dispatcher.js +84 -0
- package/dist/services/orchestration/workflow-orchestrator.d.ts +31 -0
- package/dist/services/orchestration/workflow-orchestrator.js +181 -31
- package/dist/services/review/ai-review-scanner.js +1 -0
- package/dist/services/review/review-phase-executor.js +3 -0
- package/dist/services/review/self-heal-loop.js +1 -0
- package/dist/services/review/types.d.ts +2 -0
- package/dist/utils/errors.d.ts +17 -1
- package/dist/utils/errors.js +18 -0
- package/dist/utils/session-naming.d.ts +23 -0
- package/dist/utils/session-naming.js +30 -0
- package/dist/utils/shared-flags.d.ts +1 -0
- package/dist/utils/shared-flags.js +5 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
}
|