@hyperdrive.bot/bmad-workflow 1.0.26 → 1.0.27

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 +24 -15
  33. package/dist/services/agents/agent-runner-factory.js +95 -15
  34. package/dist/services/agents/channel-agent-runner.d.ts +76 -0
  35. package/dist/services/agents/channel-agent-runner.js +256 -0
  36. package/dist/services/agents/channel-session-manager.d.ts +126 -0
  37. package/dist/services/agents/channel-session-manager.js +260 -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
@@ -35,6 +35,7 @@ export default class EpicsCreate extends Command {
35
35
  prefix: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
36
36
  reference: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
37
37
  start: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
38
+ stream: import("@oclif/core/interfaces").BooleanFlag<boolean>;
38
39
  agent: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
39
40
  cwd: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
40
41
  model: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Lock Acquire Command
3
+ *
4
+ * Acquires exclusive ownership of a story file via a git-committed `.lock` file.
5
+ * Prevents parallel agents from working on the same story simultaneously.
6
+ *
7
+ * Flow: validate → git pull → check existing lock → create .lock → git add+commit+push
8
+ *
9
+ * @example
10
+ * ```bash
11
+ * bmad-workflow lock acquire --story docs/qa/stories/X.md --agent pirlo --session 550e8400
12
+ * ```
13
+ */
14
+ import { Command } from '@oclif/core';
15
+ export default class LockAcquireCommand extends Command {
16
+ static description: string;
17
+ static examples: {
18
+ command: string;
19
+ description: string;
20
+ }[];
21
+ static flags: {
22
+ agent: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
23
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
+ session: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
25
+ 'stale-threshold': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
26
+ story: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
27
+ };
28
+ private gitOps;
29
+ private lockService;
30
+ private logger;
31
+ private pushConflictHandler;
32
+ run(): Promise<void>;
33
+ /**
34
+ * Clean up a lock file from disk and git state after failure
35
+ */
36
+ private cleanupLockFile;
37
+ /**
38
+ * Output failure message in JSON or human-readable format, then exit 1
39
+ */
40
+ private fail;
41
+ /**
42
+ * Get current git branch name
43
+ */
44
+ private getCurrentBranch;
45
+ private initServices;
46
+ /**
47
+ * Parse duration string (e.g., '2h', '30m', '1d') to milliseconds
48
+ */
49
+ private parseDuration;
50
+ /**
51
+ * Output success message in JSON or human-readable format
52
+ */
53
+ private succeed;
54
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Lock Acquire Command
3
+ *
4
+ * Acquires exclusive ownership of a story file via a git-committed `.lock` file.
5
+ * Prevents parallel agents from working on the same story simultaneously.
6
+ *
7
+ * Flow: validate → git pull → check existing lock → create .lock → git add+commit+push
8
+ *
9
+ * @example
10
+ * ```bash
11
+ * bmad-workflow lock acquire --story docs/qa/stories/X.md --agent pirlo --session 550e8400
12
+ * ```
13
+ */
14
+ import { execSync } from 'node:child_process';
15
+ import { Command, Flags } from '@oclif/core';
16
+ import fs from 'fs-extra';
17
+ import { LockConflictError } from '../../models/lock.js';
18
+ import { FileManager } from '../../services/file-system/file-manager.js';
19
+ import { GlobMatcher } from '../../services/file-system/glob-matcher.js';
20
+ import { GitOps } from '../../services/git/git-ops.js';
21
+ import { PushConflictHandler } from '../../services/git/push-conflict-handler.js';
22
+ import { LockService } from '../../services/lock/lock-service.js';
23
+ import { createLogger } from '../../utils/logger.js';
24
+ const DURATION_MULTIPLIERS = {
25
+ d: 86_400_000,
26
+ h: 3_600_000,
27
+ m: 60_000,
28
+ s: 1000,
29
+ };
30
+ export default class LockAcquireCommand extends Command {
31
+ static description = 'Acquire a lock on a story file';
32
+ static examples = [
33
+ {
34
+ command: '<%= config.bin %> lock acquire --story docs/qa/stories/X.md --agent pirlo --session 550e8400-e29b-41d4-a716-446655440000',
35
+ description: 'Acquire lock on a story for the pirlo agent',
36
+ },
37
+ ];
38
+ static flags = {
39
+ agent: Flags.string({ description: 'Agent name claiming the lock', required: true }),
40
+ json: Flags.boolean({ description: 'Output as JSON' }),
41
+ session: Flags.string({ description: 'Session UUID', required: true }),
42
+ 'stale-threshold': Flags.string({
43
+ default: '2h',
44
+ description: 'Duration before lock is stale (e.g., 2h, 30m, 1d)',
45
+ }),
46
+ story: Flags.string({ description: 'Path to story file', required: true }),
47
+ };
48
+ gitOps;
49
+ lockService;
50
+ logger;
51
+ pushConflictHandler;
52
+ async run() {
53
+ this.initServices();
54
+ const { flags } = await this.parse(LockAcquireCommand);
55
+ const cwd = process.cwd();
56
+ const lockFilePath = flags.story + '.lock';
57
+ // AC 3: Validate story file 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 4: Parse stale threshold
63
+ const thresholdMs = this.parseDuration(flags['stale-threshold']);
64
+ if (thresholdMs === null) {
65
+ return this.fail(flags, 'Invalid duration format: ' + flags['stale-threshold'] + ', expected e.g. 2h, 30m, 1d');
66
+ }
67
+ // AC 2: Pull latest and check existing lock
68
+ try {
69
+ this.gitOps.pull(cwd);
70
+ }
71
+ catch {
72
+ this.logger.warn('git pull failed — proceeding with local state');
73
+ }
74
+ const locked = await this.lockService.isLocked(flags.story);
75
+ if (locked) {
76
+ const stale = await this.lockService.isStale(flags.story, thresholdMs);
77
+ if (!stale) {
78
+ const lockInfo = await this.lockService.getLockInfo(flags.story);
79
+ if (lockInfo) {
80
+ return this.fail(flags, `Lock already held by ${lockInfo.agent_name} since ${lockInfo.started_at}`);
81
+ }
82
+ }
83
+ // AC 4: Stale lock — release and proceed
84
+ this.logger.info('Removing stale lock');
85
+ await this.lockService.release(flags.story);
86
+ }
87
+ // AC 1, 5, 6: Acquire lock with cleanup on failure
88
+ let lockFileCreated = false;
89
+ try {
90
+ const branch = this.getCurrentBranch(cwd);
91
+ await this.lockService.acquire(flags.story, flags.agent, flags.session, branch);
92
+ lockFileCreated = true;
93
+ this.gitOps.add(lockFilePath, cwd);
94
+ this.gitOps.commit('acquire: ' + flags.story + ' (' + flags.agent + ')', cwd);
95
+ const pushResult = this.gitOps.push(cwd);
96
+ if (!pushResult.success) {
97
+ if (pushResult.isNonFastForward) {
98
+ // AC 5: Handle race condition via PushConflictHandler
99
+ this.pushConflictHandler.handleConflict(flags.story, lockFilePath, cwd);
100
+ // If handleConflict returns (didn't throw), push succeeded on retry
101
+ }
102
+ else {
103
+ throw new Error('Git push failed: ' + (pushResult.stderr || 'unknown error'));
104
+ }
105
+ }
106
+ }
107
+ catch (error) {
108
+ // AC 6: Cleanup — no partial lock files remain
109
+ if (lockFileCreated) {
110
+ this.cleanupLockFile(lockFilePath, cwd);
111
+ }
112
+ if (error instanceof LockConflictError) {
113
+ return this.fail(flags, `Lock claimed by ${error.lockContent.agent_name} during race — backing off`);
114
+ }
115
+ return this.fail(flags, error.message);
116
+ }
117
+ // AC 7, 8: Success output
118
+ this.succeed(flags);
119
+ }
120
+ /**
121
+ * Clean up a lock file from disk and git state after failure
122
+ */
123
+ cleanupLockFile(lockFilePath, cwd) {
124
+ try {
125
+ fs.removeSync(lockFilePath);
126
+ }
127
+ catch {
128
+ /* ignore */
129
+ }
130
+ try {
131
+ this.gitOps.remove(lockFilePath, cwd);
132
+ }
133
+ catch {
134
+ /* ignore */
135
+ }
136
+ }
137
+ /**
138
+ * Output failure message in JSON or human-readable format, then exit 1
139
+ */
140
+ fail(flags, message) {
141
+ if (flags.json) {
142
+ this.log(JSON.stringify({ error: message, story: flags.story, success: false }));
143
+ this.exit(1);
144
+ }
145
+ this.error(message, { exit: 1 });
146
+ }
147
+ /**
148
+ * Get current git branch name
149
+ */
150
+ getCurrentBranch(cwd) {
151
+ try {
152
+ return execSync('git branch --show-current', { cwd, encoding: 'utf8' }).trim() || 'unknown';
153
+ }
154
+ catch {
155
+ return 'unknown';
156
+ }
157
+ }
158
+ initServices() {
159
+ this.logger = createLogger({ namespace: 'commands:lock:acquire' });
160
+ const fileManager = new FileManager(this.logger);
161
+ const globMatcher = new GlobMatcher(fileManager, this.logger);
162
+ this.lockService = new LockService(fileManager, globMatcher, this.logger);
163
+ this.gitOps = new GitOps();
164
+ this.pushConflictHandler = new PushConflictHandler(this.gitOps);
165
+ }
166
+ /**
167
+ * Parse duration string (e.g., '2h', '30m', '1d') to milliseconds
168
+ */
169
+ parseDuration(duration) {
170
+ const match = /^(\d+)(h|m|d|s)$/.exec(duration);
171
+ if (!match)
172
+ return null;
173
+ const [, value, unit] = match;
174
+ return Number.parseInt(value, 10) * DURATION_MULTIPLIERS[unit];
175
+ }
176
+ /**
177
+ * Output success message in JSON or human-readable format
178
+ */
179
+ succeed(flags) {
180
+ if (flags.json) {
181
+ this.log(JSON.stringify({
182
+ agent: flags.agent,
183
+ session: flags.session,
184
+ story: flags.story,
185
+ success: true,
186
+ timestamp: new Date().toISOString(),
187
+ }));
188
+ }
189
+ else {
190
+ this.log('✓ Lock acquired: ' + flags.story + ' (agent: ' + flags.agent + ', session: ' + flags.session + ')');
191
+ }
192
+ }
193
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Lock Cleanup Command
3
+ *
4
+ * Finds and removes all stale story locks in a single git commit+push.
5
+ * Supports dry-run mode, custom stale thresholds, and JSON output.
6
+ *
7
+ * Flow: parse flags → git pull → find stale locks → (dry-run | delete+commit+push) → output
8
+ *
9
+ * @example
10
+ * ```bash
11
+ * bmad-workflow lock cleanup
12
+ * bmad-workflow lock cleanup --stale-threshold 30m --dry-run
13
+ * bmad-workflow lock cleanup --json
14
+ * ```
15
+ */
16
+ import { Command } from '@oclif/core';
17
+ export default class LockCleanupCommand extends Command {
18
+ static description: string;
19
+ static examples: {
20
+ command: string;
21
+ description: string;
22
+ }[];
23
+ static flags: {
24
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
25
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
+ 'stale-threshold': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
27
+ };
28
+ private cleanupService;
29
+ private gitOps;
30
+ private lockService;
31
+ private logger;
32
+ run(): Promise<void>;
33
+ private initServices;
34
+ /**
35
+ * Check if a lock's started_at is older than threshold
36
+ */
37
+ private isOverThreshold;
38
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Lock Cleanup Command
3
+ *
4
+ * Finds and removes all stale story locks in a single git commit+push.
5
+ * Supports dry-run mode, custom stale thresholds, and JSON output.
6
+ *
7
+ * Flow: parse flags → git pull → find stale locks → (dry-run | delete+commit+push) → output
8
+ *
9
+ * @example
10
+ * ```bash
11
+ * bmad-workflow lock cleanup
12
+ * bmad-workflow lock cleanup --stale-threshold 30m --dry-run
13
+ * bmad-workflow lock cleanup --json
14
+ * ```
15
+ */
16
+ import { Command, Flags } from '@oclif/core';
17
+ import { FileManager } from '../../services/file-system/file-manager.js';
18
+ import { GlobMatcher } from '../../services/file-system/glob-matcher.js';
19
+ import { GitOps } from '../../services/git/git-ops.js';
20
+ import { LockCleanupService } from '../../services/lock/lock-cleanup.js';
21
+ import { LockService } from '../../services/lock/lock-service.js';
22
+ import { parseDuration } from '../../utils/duration.js';
23
+ import { createLogger } from '../../utils/logger.js';
24
+ /** Default directory to scan for lock files */
25
+ const STORIES_DIR = 'docs';
26
+ export default class LockCleanupCommand extends Command {
27
+ static description = 'Remove all stale story locks in a single commit';
28
+ static examples = [
29
+ {
30
+ command: '<%= config.bin %> lock cleanup',
31
+ description: 'Remove all stale locks (default 2h threshold)',
32
+ },
33
+ {
34
+ command: '<%= config.bin %> lock cleanup --stale-threshold 30m --dry-run',
35
+ description: 'Preview locks that would be cleaned with 30m threshold',
36
+ },
37
+ {
38
+ command: '<%= config.bin %> lock cleanup --json',
39
+ description: 'Output cleanup results as JSON',
40
+ },
41
+ ];
42
+ static flags = {
43
+ 'dry-run': Flags.boolean({ description: 'Show what would be cleaned without acting' }),
44
+ json: Flags.boolean({ description: 'Output as JSON' }),
45
+ 'stale-threshold': Flags.string({
46
+ default: '2h',
47
+ description: 'Duration before lock is stale (e.g., 2h, 30m, 1d)',
48
+ }),
49
+ };
50
+ cleanupService;
51
+ gitOps;
52
+ lockService;
53
+ logger;
54
+ async run() {
55
+ this.initServices();
56
+ const { flags } = await this.parse(LockCleanupCommand);
57
+ // AC 4: Parse stale threshold — exit 1 on invalid format
58
+ let thresholdMs;
59
+ try {
60
+ thresholdMs = parseDuration(flags['stale-threshold']);
61
+ }
62
+ catch {
63
+ const msg = `Invalid duration format: ${flags['stale-threshold']}, expected e.g. 2h, 30m, 1d`;
64
+ if (flags.json) {
65
+ this.log(JSON.stringify({ error: msg, removed: 0 }));
66
+ }
67
+ this.error(msg, { exit: 1 });
68
+ }
69
+ // AC 7: Pull latest before scanning
70
+ const cwd = process.cwd();
71
+ try {
72
+ this.gitOps.pull(cwd);
73
+ }
74
+ catch {
75
+ this.logger.warn('git pull failed — proceeding with local state');
76
+ }
77
+ // Find stale locks with metadata for output
78
+ const allLocks = await this.lockService.list(STORIES_DIR);
79
+ const staleLocks = allLocks.filter((entry) => entry.isStale || this.isOverThreshold(entry.lock.started_at, thresholdMs));
80
+ // Also check for locks that list() skipped (malformed) — use findStaleLocks for completeness
81
+ const staleLockPaths = await this.lockService.findStaleLocks(STORIES_DIR, thresholdMs);
82
+ // Build lock details from the metadata we have
83
+ const lockDetails = staleLocks.map((entry) => ({
84
+ agent: entry.lock.agent_name,
85
+ started_at: entry.lock.started_at,
86
+ story: entry.storyPath,
87
+ }));
88
+ // If findStaleLocks found more than list() (malformed locks), count them
89
+ const totalStaleCount = Math.max(staleLockPaths.length, staleLocks.length);
90
+ // AC 1, 5, 6: No stale locks
91
+ if (totalStaleCount === 0) {
92
+ if (flags.json) {
93
+ if (flags['dry-run']) {
94
+ this.log(JSON.stringify({ dry_run: true, locks: [], would_remove: 0 }));
95
+ }
96
+ else {
97
+ this.log(JSON.stringify({ locks: [], removed: 0 }));
98
+ }
99
+ }
100
+ else {
101
+ this.log('No stale locks found');
102
+ }
103
+ return;
104
+ }
105
+ // AC 5, 9: Dry-run mode — list but don't act
106
+ if (flags['dry-run']) {
107
+ if (flags.json) {
108
+ this.log(JSON.stringify({ dry_run: true, locks: lockDetails, would_remove: totalStaleCount }));
109
+ }
110
+ else {
111
+ this.log(`Would remove ${totalStaleCount} stale lock(s):`);
112
+ for (const detail of lockDetails) {
113
+ this.log(` ${detail.story} — agent: ${detail.agent}, started_at: ${detail.started_at}`);
114
+ }
115
+ }
116
+ return;
117
+ }
118
+ // AC 2: Batch cleanup with single commit+push via LockCleanupService
119
+ const result = await this.cleanupService.cleanupStaleLocks(thresholdMs, cwd);
120
+ // AC 8: Output
121
+ if (flags.json) {
122
+ this.log(JSON.stringify({ locks: lockDetails, removed: result.cleaned }));
123
+ }
124
+ else {
125
+ this.log(`✓ Cleaned up ${result.cleaned} stale lock(s):`);
126
+ for (const detail of lockDetails) {
127
+ this.log(` ${detail.story} — agent: ${detail.agent}`);
128
+ }
129
+ }
130
+ }
131
+ initServices() {
132
+ this.logger = createLogger({ namespace: 'commands:lock:cleanup' });
133
+ const fileManager = new FileManager(this.logger);
134
+ const globMatcher = new GlobMatcher(fileManager, this.logger);
135
+ this.lockService = new LockService(fileManager, globMatcher, this.logger);
136
+ this.gitOps = new GitOps();
137
+ this.cleanupService = new LockCleanupService(this.lockService, this.gitOps, this.logger, STORIES_DIR);
138
+ }
139
+ /**
140
+ * Check if a lock's started_at is older than threshold
141
+ */
142
+ isOverThreshold(startedAt, thresholdMs) {
143
+ const ts = new Date(startedAt).getTime();
144
+ if (Number.isNaN(ts))
145
+ return true;
146
+ return ts + thresholdMs < Date.now();
147
+ }
148
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Lock List Command
3
+ *
4
+ * Lists all current locks across all story directories.
5
+ * Displays results in a table or JSON format.
6
+ *
7
+ * @example
8
+ * ```bash
9
+ * bmad-workflow lock list
10
+ * bmad-workflow lock list --stale-only
11
+ * bmad-workflow lock list --json
12
+ * ```
13
+ */
14
+ import { Command } from '@oclif/core';
15
+ export default class LockListCommand extends Command {
16
+ static description: string;
17
+ static examples: {
18
+ command: string;
19
+ description: string;
20
+ }[];
21
+ static flags: {
22
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
+ 'stale-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
+ };
25
+ private lockService;
26
+ private logger;
27
+ private pathResolver;
28
+ run(): Promise<void>;
29
+ private initServices;
30
+ private truncate;
31
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Lock List Command
3
+ *
4
+ * Lists all current locks across all story directories.
5
+ * Displays results in a table or JSON format.
6
+ *
7
+ * @example
8
+ * ```bash
9
+ * bmad-workflow lock list
10
+ * bmad-workflow lock list --stale-only
11
+ * bmad-workflow lock list --json
12
+ * ```
13
+ */
14
+ import { execSync } from 'node:child_process';
15
+ import { Command, Flags } from '@oclif/core';
16
+ import chalk from 'chalk';
17
+ import { FileManager } from '../../services/file-system/file-manager.js';
18
+ import { GlobMatcher } from '../../services/file-system/glob-matcher.js';
19
+ import { PathResolver } from '../../services/file-system/path-resolver.js';
20
+ import { LockService } from '../../services/lock/lock-service.js';
21
+ import { createLogger } from '../../utils/logger.js';
22
+ export default class LockListCommand extends Command {
23
+ static description = 'List all current story locks';
24
+ static examples = [
25
+ {
26
+ command: '<%= config.bin %> lock list',
27
+ description: 'List all locks across story directories',
28
+ },
29
+ {
30
+ command: '<%= config.bin %> lock list --stale-only',
31
+ description: 'Show only stale locks',
32
+ },
33
+ {
34
+ command: '<%= config.bin %> lock list --json',
35
+ description: 'Output all locks as JSON',
36
+ },
37
+ ];
38
+ static flags = {
39
+ json: Flags.boolean({ description: 'Output as JSON' }),
40
+ 'stale-only': Flags.boolean({ description: 'Show only stale locks' }),
41
+ };
42
+ lockService;
43
+ logger;
44
+ pathResolver;
45
+ async run() {
46
+ this.initServices();
47
+ const { flags } = await this.parse(LockListCommand);
48
+ // AC 9: Pull latest before reading lock state
49
+ try {
50
+ execSync('git pull --ff-only', { cwd: process.cwd(), stdio: 'pipe' });
51
+ }
52
+ catch {
53
+ this.logger.warn('git pull failed — proceeding with local state');
54
+ }
55
+ // Get all story directories
56
+ const allDirs = this.pathResolver.getAllStoryDirs();
57
+ // Get all locks with stale status
58
+ let locks = await this.lockService.listLocks(allDirs);
59
+ // AC 6: Filter stale-only if requested
60
+ if (flags['stale-only']) {
61
+ locks = locks.filter((l) => l.stale);
62
+ }
63
+ // AC 8: No locks found
64
+ if (locks.length === 0) {
65
+ if (flags.json) {
66
+ this.log(JSON.stringify({ locks: [] }, null, 2));
67
+ }
68
+ else {
69
+ this.log(chalk.yellow('No locks found'));
70
+ }
71
+ return;
72
+ }
73
+ // AC 7: JSON output
74
+ if (flags.json) {
75
+ const output = {
76
+ locks: locks.map((l) => ({
77
+ agent: l.agent_name,
78
+ branch: l.branch,
79
+ session: l.agent_session_id,
80
+ stale: l.stale,
81
+ started_at: l.started_at,
82
+ story: l.story_file,
83
+ })),
84
+ };
85
+ this.log(JSON.stringify(output, null, 2));
86
+ return;
87
+ }
88
+ // AC 5: Table output
89
+ this.log('');
90
+ this.log(chalk.cyan.bold('Story'.padEnd(40)) +
91
+ chalk.cyan.bold('Agent'.padEnd(20)) +
92
+ chalk.cyan.bold('Session'.padEnd(38)) +
93
+ chalk.cyan.bold('Started At'.padEnd(24)) +
94
+ chalk.cyan.bold('Branch'.padEnd(20)) +
95
+ chalk.cyan.bold('Stale'));
96
+ this.log(chalk.gray('─'.repeat(150)));
97
+ for (const lock of locks) {
98
+ const staleLabel = lock.stale ? chalk.red('yes') : chalk.green('no');
99
+ const story = this.truncate(lock.story_file, 38);
100
+ const agent = this.truncate(lock.agent_name, 18);
101
+ const branch = this.truncate(lock.branch, 18);
102
+ this.log(story.padEnd(40) +
103
+ agent.padEnd(20) +
104
+ lock.agent_session_id.padEnd(38) +
105
+ lock.started_at.padEnd(24) +
106
+ branch.padEnd(20) +
107
+ staleLabel);
108
+ }
109
+ this.log('');
110
+ this.log(chalk.white(`Found ${locks.length} lock(s)`));
111
+ this.log('');
112
+ }
113
+ initServices() {
114
+ this.logger = createLogger({ namespace: 'commands:lock:list' });
115
+ const fileManager = new FileManager(this.logger);
116
+ const globMatcher = new GlobMatcher(fileManager, this.logger);
117
+ this.pathResolver = new PathResolver(fileManager, this.logger);
118
+ this.lockService = new LockService(fileManager, globMatcher, this.logger);
119
+ }
120
+ truncate(str, maxLength) {
121
+ return str.length > maxLength ? str.slice(0, maxLength - 3) + '...' : str;
122
+ }
123
+ }
@@ -0,0 +1,42 @@
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 } from '@oclif/core';
15
+ export default class LockReleaseCommand extends Command {
16
+ static description: string;
17
+ static examples: {
18
+ command: string;
19
+ description: string;
20
+ }[];
21
+ static flags: {
22
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
+ story: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
24
+ };
25
+ private gitOps;
26
+ private lockService;
27
+ private logger;
28
+ run(): Promise<void>;
29
+ /**
30
+ * Output failure message in JSON or human-readable format, then exit 1
31
+ */
32
+ private fail;
33
+ private initServices;
34
+ /**
35
+ * Output idempotent success when lock was already released by another agent (AC 4)
36
+ */
37
+ private succeedAlreadyReleased;
38
+ /**
39
+ * Output success message in JSON or human-readable format
40
+ */
41
+ private succeed;
42
+ }