@hyperdrive.bot/bmad-workflow 1.0.18 → 1.0.19

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 (98) hide show
  1. package/dist/commands/config/show.js +8 -2
  2. package/dist/commands/decompose.js +26 -5
  3. package/dist/commands/epics/create.d.ts +1 -0
  4. package/dist/commands/mcp/add.d.ts +16 -0
  5. package/dist/commands/mcp/add.js +77 -0
  6. package/dist/commands/mcp/credential/get.d.ts +14 -0
  7. package/dist/commands/mcp/credential/get.js +35 -0
  8. package/dist/commands/mcp/credential/list.d.ts +17 -0
  9. package/dist/commands/mcp/credential/list.js +67 -0
  10. package/dist/commands/mcp/credential/remove.d.ts +18 -0
  11. package/dist/commands/mcp/credential/remove.js +84 -0
  12. package/dist/commands/mcp/credential/set.d.ts +16 -0
  13. package/dist/commands/mcp/credential/set.js +41 -0
  14. package/dist/commands/mcp/credential/validate.d.ts +12 -0
  15. package/dist/commands/mcp/credential/validate.js +150 -0
  16. package/dist/commands/mcp/list.d.ts +17 -0
  17. package/dist/commands/mcp/list.js +80 -0
  18. package/dist/commands/mcp/logs.d.ts +15 -0
  19. package/dist/commands/mcp/logs.js +64 -0
  20. package/dist/commands/mcp/preset.d.ts +15 -0
  21. package/dist/commands/mcp/preset.js +84 -0
  22. package/dist/commands/mcp/remove.d.ts +14 -0
  23. package/dist/commands/mcp/remove.js +36 -0
  24. package/dist/commands/mcp/start.d.ts +12 -0
  25. package/dist/commands/mcp/start.js +80 -0
  26. package/dist/commands/mcp/status.d.ts +30 -0
  27. package/dist/commands/mcp/status.js +180 -0
  28. package/dist/commands/mcp/stop.d.ts +12 -0
  29. package/dist/commands/mcp/stop.js +47 -0
  30. package/dist/commands/stories/create.d.ts +1 -0
  31. package/dist/commands/stories/develop.d.ts +1 -0
  32. package/dist/commands/stories/qa.js +5 -2
  33. package/dist/commands/stories/review.d.ts +124 -0
  34. package/dist/commands/stories/review.js +516 -0
  35. package/dist/commands/workflow.d.ts +8 -0
  36. package/dist/commands/workflow.js +110 -2
  37. package/dist/mcp/types.d.ts +99 -0
  38. package/dist/mcp/types.js +7 -0
  39. package/dist/mcp/utils/docker-utils.d.ts +56 -0
  40. package/dist/mcp/utils/docker-utils.js +108 -0
  41. package/dist/mcp/utils/template-loader.d.ts +21 -0
  42. package/dist/mcp/utils/template-loader.js +60 -0
  43. package/dist/models/agent-options.d.ts +10 -1
  44. package/dist/models/workflow-config.d.ts +77 -0
  45. package/dist/models/workflow-result.d.ts +7 -0
  46. package/dist/services/agents/claude-agent-runner.js +19 -3
  47. package/dist/services/file-system/path-resolver.d.ts +10 -0
  48. package/dist/services/file-system/path-resolver.js +12 -0
  49. package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
  50. package/dist/services/mcp/mcp-config-manager.js +146 -0
  51. package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
  52. package/dist/services/mcp/mcp-context-injector.js +168 -0
  53. package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
  54. package/dist/services/mcp/mcp-credential-manager.js +124 -0
  55. package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
  56. package/dist/services/mcp/mcp-health-checker.js +162 -0
  57. package/dist/services/mcp/types/health-types.d.ts +31 -0
  58. package/dist/services/mcp/types/health-types.js +7 -0
  59. package/dist/services/orchestration/dependency-graph-executor.js +1 -1
  60. package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
  61. package/dist/services/orchestration/task-decomposition-service.js +90 -36
  62. package/dist/services/orchestration/workflow-orchestrator.d.ts +54 -2
  63. package/dist/services/orchestration/workflow-orchestrator.js +303 -17
  64. package/dist/services/review/ai-review-scanner.d.ts +66 -0
  65. package/dist/services/review/ai-review-scanner.js +142 -0
  66. package/dist/services/review/coderabbit-scanner.d.ts +25 -0
  67. package/dist/services/review/coderabbit-scanner.js +31 -0
  68. package/dist/services/review/index.d.ts +20 -0
  69. package/dist/services/review/index.js +15 -0
  70. package/dist/services/review/lint-scanner.d.ts +46 -0
  71. package/dist/services/review/lint-scanner.js +172 -0
  72. package/dist/services/review/review-config.d.ts +62 -0
  73. package/dist/services/review/review-config.js +91 -0
  74. package/dist/services/review/review-phase-executor.d.ts +69 -0
  75. package/dist/services/review/review-phase-executor.js +152 -0
  76. package/dist/services/review/review-queue.d.ts +98 -0
  77. package/dist/services/review/review-queue.js +174 -0
  78. package/dist/services/review/review-reporter.d.ts +94 -0
  79. package/dist/services/review/review-reporter.js +386 -0
  80. package/dist/services/review/scanner-factory.d.ts +42 -0
  81. package/dist/services/review/scanner-factory.js +60 -0
  82. package/dist/services/review/self-heal-loop.d.ts +58 -0
  83. package/dist/services/review/self-heal-loop.js +132 -0
  84. package/dist/services/review/severity-classifier.d.ts +17 -0
  85. package/dist/services/review/severity-classifier.js +314 -0
  86. package/dist/services/review/tech-debt-tracker.d.ts +52 -0
  87. package/dist/services/review/tech-debt-tracker.js +245 -0
  88. package/dist/services/review/types.d.ts +93 -0
  89. package/dist/services/review/types.js +23 -0
  90. package/dist/services/validation/config-validator.d.ts +84 -0
  91. package/dist/services/validation/config-validator.js +78 -0
  92. package/dist/utils/credential-utils.d.ts +14 -0
  93. package/dist/utils/credential-utils.js +19 -0
  94. package/dist/utils/duration.d.ts +41 -0
  95. package/dist/utils/duration.js +89 -0
  96. package/dist/utils/shared-flags.d.ts +1 -0
  97. package/dist/utils/shared-flags.js +11 -2
  98. package/package.json +4 -2
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Review Types
3
+ *
4
+ * Core type definitions for the automated code review system.
5
+ * Scanners return raw output; classification into severity levels
6
+ * is handled separately by SeverityClassifier (Story 1.2).
7
+ *
8
+ * Severity model maps to BMAD governance:
9
+ * CRITICAL = NON-NEGOTIABLE (blocks pipeline)
10
+ * HIGH = MUST (blocks pipeline)
11
+ * MEDIUM = SHOULD (documented as tech debt)
12
+ * LOW = MAY (noted only)
13
+ */
14
+ /**
15
+ * Issue severity levels aligned with BMAD governance tiers
16
+ */
17
+ export declare enum Severity {
18
+ CRITICAL = "CRITICAL",
19
+ HIGH = "HIGH",
20
+ LOW = "LOW",
21
+ MEDIUM = "MEDIUM"
22
+ }
23
+ /**
24
+ * Review verdict — PASS allows pipeline to continue, FAIL blocks it
25
+ */
26
+ export type ReviewVerdict = 'FAIL' | 'PASS';
27
+ /**
28
+ * Context provided to scanners describing what to review
29
+ */
30
+ export interface ReviewContext {
31
+ /** Base branch to diff against (e.g., "main", "develop") */
32
+ baseBranch: string;
33
+ /** List of changed file paths (relative to projectRoot) */
34
+ changedFiles: string[];
35
+ /** Absolute path to the project root directory */
36
+ projectRoot: string;
37
+ /** List of reference file paths for context (e.g., architecture docs) */
38
+ referenceFiles: string[];
39
+ /** Absolute path to the story markdown file */
40
+ storyFile: string;
41
+ /** Story identifier (e.g., "PROJ-story-1.001") */
42
+ storyId: string;
43
+ }
44
+ /**
45
+ * Raw output from a scanner before classification
46
+ */
47
+ export interface RawReviewOutput {
48
+ /** Raw scanner output (lint text, AI response, etc.) */
49
+ raw: string;
50
+ /** Identifier of the scanner that produced this output (e.g., "eslint", "claude-ai", "coderabbit") */
51
+ source: string;
52
+ }
53
+ /**
54
+ * A single classified issue with severity and location
55
+ */
56
+ export interface ClassifiedIssue {
57
+ /** File path where the issue was found */
58
+ file: string;
59
+ /** Suggested fix (optional) */
60
+ fix?: string;
61
+ /** Description of the issue */
62
+ issue: string;
63
+ /** Line number where the issue occurs */
64
+ line: number;
65
+ /** Severity level of the issue */
66
+ severity: Severity;
67
+ }
68
+ /**
69
+ * Final review result after classification and optional self-heal iterations
70
+ */
71
+ export interface ReviewResult {
72
+ /** All classified issues found during review */
73
+ issues: ClassifiedIssue[];
74
+ /** Number of self-heal iterations performed (0 if no retries) */
75
+ iterations: number;
76
+ /** Optional summary message */
77
+ message?: string;
78
+ /** Overall verdict — PASS or FAIL */
79
+ verdict: ReviewVerdict;
80
+ }
81
+ /**
82
+ * Scanner interface — all scanner implementations (lint, AI, CodeRabbit) must implement this.
83
+ * Mirrors the AIProviderRunner pattern: single async method with typed input/output.
84
+ */
85
+ export interface ReviewScanner {
86
+ /**
87
+ * Scan the given context and produce raw review output
88
+ *
89
+ * @param context - Review context describing what to scan
90
+ * @returns Raw output from the scanner
91
+ */
92
+ scan(context: ReviewContext): Promise<RawReviewOutput>;
93
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Review Types
3
+ *
4
+ * Core type definitions for the automated code review system.
5
+ * Scanners return raw output; classification into severity levels
6
+ * is handled separately by SeverityClassifier (Story 1.2).
7
+ *
8
+ * Severity model maps to BMAD governance:
9
+ * CRITICAL = NON-NEGOTIABLE (blocks pipeline)
10
+ * HIGH = MUST (blocks pipeline)
11
+ * MEDIUM = SHOULD (documented as tech debt)
12
+ * LOW = MAY (noted only)
13
+ */
14
+ /**
15
+ * Issue severity levels aligned with BMAD governance tiers
16
+ */
17
+ export var Severity;
18
+ (function (Severity) {
19
+ Severity["CRITICAL"] = "CRITICAL";
20
+ Severity["HIGH"] = "HIGH";
21
+ Severity["LOW"] = "LOW";
22
+ Severity["MEDIUM"] = "MEDIUM";
23
+ })(Severity || (Severity = {}));
@@ -8,6 +8,53 @@
8
8
  import type pino from 'pino';
9
9
  import { z } from 'zod';
10
10
  import type { FileManager } from '../file-system/file-manager.js';
11
+ /**
12
+ * Zod schema for review configuration section
13
+ *
14
+ * Defines scanners, severity thresholds, self-heal limits, and path rules
15
+ * for automated code review in the BMAD workflow pipeline.
16
+ */
17
+ export declare const reviewConfigSchema: z.ZodObject<{
18
+ enabled: z.ZodDefault<z.ZodBoolean>;
19
+ pathRules: z.ZodOptional<z.ZodArray<z.ZodObject<{
20
+ focus: z.ZodString;
21
+ pattern: z.ZodString;
22
+ }, z.core.$strip>>>;
23
+ scanners: z.ZodDefault<z.ZodArray<z.ZodEnum<{
24
+ ai: "ai";
25
+ coderabbit: "coderabbit";
26
+ lint: "lint";
27
+ }>>>;
28
+ selfHeal: z.ZodDefault<z.ZodObject<{
29
+ fixAgent: z.ZodDefault<z.ZodString>;
30
+ fixTimeout: z.ZodDefault<z.ZodNumber>;
31
+ maxIterations: z.ZodDefault<z.ZodNumber>;
32
+ }, z.core.$strip>>;
33
+ severity: z.ZodDefault<z.ZodObject<{
34
+ blockOn: z.ZodDefault<z.ZodArray<z.ZodEnum<{
35
+ CRITICAL: "CRITICAL";
36
+ HIGH: "HIGH";
37
+ LOW: "LOW";
38
+ MEDIUM: "MEDIUM";
39
+ }>>>;
40
+ documentOn: z.ZodDefault<z.ZodArray<z.ZodEnum<{
41
+ CRITICAL: "CRITICAL";
42
+ HIGH: "HIGH";
43
+ LOW: "LOW";
44
+ MEDIUM: "MEDIUM";
45
+ }>>>;
46
+ ignoreOn: z.ZodDefault<z.ZodArray<z.ZodEnum<{
47
+ CRITICAL: "CRITICAL";
48
+ HIGH: "HIGH";
49
+ LOW: "LOW";
50
+ MEDIUM: "MEDIUM";
51
+ }>>>;
52
+ }, z.core.$strip>>;
53
+ }, z.core.$strip>;
54
+ /**
55
+ * Inferred TypeScript type from review config Zod schema
56
+ */
57
+ export type ReviewConfig = z.infer<typeof reviewConfigSchema>;
11
58
  /**
12
59
  * Zod schema for core configuration
13
60
  *
@@ -22,6 +69,43 @@ declare const configSchema: z.ZodObject<{
22
69
  qa: z.ZodOptional<z.ZodObject<{
23
70
  qaLocation: z.ZodOptional<z.ZodString>;
24
71
  }, z.core.$strip>>;
72
+ review: z.ZodOptional<z.ZodObject<{
73
+ enabled: z.ZodDefault<z.ZodBoolean>;
74
+ pathRules: z.ZodOptional<z.ZodArray<z.ZodObject<{
75
+ focus: z.ZodString;
76
+ pattern: z.ZodString;
77
+ }, z.core.$strip>>>;
78
+ scanners: z.ZodDefault<z.ZodArray<z.ZodEnum<{
79
+ ai: "ai";
80
+ coderabbit: "coderabbit";
81
+ lint: "lint";
82
+ }>>>;
83
+ selfHeal: z.ZodDefault<z.ZodObject<{
84
+ fixAgent: z.ZodDefault<z.ZodString>;
85
+ fixTimeout: z.ZodDefault<z.ZodNumber>;
86
+ maxIterations: z.ZodDefault<z.ZodNumber>;
87
+ }, z.core.$strip>>;
88
+ severity: z.ZodDefault<z.ZodObject<{
89
+ blockOn: z.ZodDefault<z.ZodArray<z.ZodEnum<{
90
+ CRITICAL: "CRITICAL";
91
+ HIGH: "HIGH";
92
+ LOW: "LOW";
93
+ MEDIUM: "MEDIUM";
94
+ }>>>;
95
+ documentOn: z.ZodDefault<z.ZodArray<z.ZodEnum<{
96
+ CRITICAL: "CRITICAL";
97
+ HIGH: "HIGH";
98
+ LOW: "LOW";
99
+ MEDIUM: "MEDIUM";
100
+ }>>>;
101
+ ignoreOn: z.ZodDefault<z.ZodArray<z.ZodEnum<{
102
+ CRITICAL: "CRITICAL";
103
+ HIGH: "HIGH";
104
+ LOW: "LOW";
105
+ MEDIUM: "MEDIUM";
106
+ }>>>;
107
+ }, z.core.$strip>>;
108
+ }, z.core.$strip>>;
25
109
  }, z.core.$strip>;
26
110
  /**
27
111
  * Inferred TypeScript type from Zod schema
@@ -7,6 +7,77 @@
7
7
  */
8
8
  import { z } from 'zod';
9
9
  import { ValidationError } from '../../utils/errors.js';
10
+ /**
11
+ * Valid scanner identifiers for automated code review
12
+ */
13
+ const VALID_SCANNERS = ['ai', 'lint', 'coderabbit'];
14
+ /**
15
+ * Valid severity levels
16
+ */
17
+ const VALID_SEVERITIES = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'];
18
+ /**
19
+ * Zod schema for review.severity configuration
20
+ */
21
+ const severityArraySchema = z.array(z.enum(VALID_SEVERITIES, {
22
+ message: `Invalid severity level. Valid values: ${VALID_SEVERITIES.join(', ')}`,
23
+ }));
24
+ /**
25
+ * Zod schema for review.pathRules entries
26
+ */
27
+ const pathRuleSchema = z.object({
28
+ focus: z.string().min(1, { message: 'Path rule focus description is required' }),
29
+ pattern: z.string().min(1, { message: 'Path rule pattern is required' }),
30
+ });
31
+ /**
32
+ * Zod schema for review configuration section
33
+ *
34
+ * Defines scanners, severity thresholds, self-heal limits, and path rules
35
+ * for automated code review in the BMAD workflow pipeline.
36
+ */
37
+ export const reviewConfigSchema = z
38
+ .object({
39
+ enabled: z.boolean().default(false),
40
+ pathRules: z.array(pathRuleSchema).optional(),
41
+ scanners: z
42
+ .array(z.enum(VALID_SCANNERS, {
43
+ message: `Unknown scanner name. Valid values: ${VALID_SCANNERS.join(', ')}`,
44
+ }))
45
+ .default(['ai', 'lint']),
46
+ selfHeal: z
47
+ .object({
48
+ fixAgent: z.string().default('dev'),
49
+ fixTimeout: z.number().int().positive({ message: 'selfHeal.fixTimeout must be a positive integer' }).default(300_000),
50
+ maxIterations: z
51
+ .number()
52
+ .int()
53
+ .positive({ message: 'selfHeal.maxIterations must be a positive integer' })
54
+ .default(3),
55
+ })
56
+ .default(() => ({ fixAgent: 'dev', fixTimeout: 300_000, maxIterations: 3 })),
57
+ severity: z
58
+ .object({
59
+ blockOn: severityArraySchema.default(['CRITICAL', 'HIGH']),
60
+ documentOn: severityArraySchema.default(['MEDIUM']),
61
+ ignoreOn: severityArraySchema.default(['LOW']),
62
+ })
63
+ .default(() => ({
64
+ blockOn: ['CRITICAL', 'HIGH'],
65
+ documentOn: ['MEDIUM'],
66
+ ignoreOn: ['LOW'],
67
+ })),
68
+ })
69
+ .superRefine((data, ctx) => {
70
+ // Validate that blockOn and ignoreOn do not overlap
71
+ const blockSet = new Set(data.severity.blockOn);
72
+ const overlapping = data.severity.ignoreOn.filter((s) => blockSet.has(s));
73
+ if (overlapping.length > 0) {
74
+ ctx.addIssue({
75
+ code: z.ZodIssueCode.custom,
76
+ message: `severity.blockOn and severity.ignoreOn must not overlap. Overlapping: ${overlapping.join(', ')}`,
77
+ path: ['severity'],
78
+ });
79
+ }
80
+ });
10
81
  /**
11
82
  * Zod schema for core configuration
12
83
  *
@@ -25,6 +96,7 @@ const configSchema = z.object({
25
96
  qaLocation: z.string().optional(),
26
97
  })
27
98
  .optional(),
99
+ review: reviewConfigSchema.optional(),
28
100
  });
29
101
  /**
30
102
  * ConfigValidator service validates configuration files against schemas
@@ -105,11 +177,17 @@ export class ConfigValidator {
105
177
  devStoryLocation: 'docs/stories',
106
178
  'prd.prdFile': 'docs/prd.md',
107
179
  'qa.qaLocation': 'docs/qa',
180
+ 'review.scanners': '[ai, lint]',
181
+ 'review.severity.blockOn': '[CRITICAL, HIGH]',
182
+ 'review.selfHeal.maxIterations': '3',
108
183
  };
109
184
  const fieldDisplayNames = {
110
185
  devStoryLocation: 'Story directory path (devStoryLocation)',
111
186
  'prd.prdFile': 'PRD file path (prd.prdFile)',
112
187
  'qa.qaLocation': 'QA location path (qa.qaLocation)',
188
+ 'review.scanners': 'Review scanners (review.scanners)',
189
+ 'review.severity.blockOn': 'Review severity blockOn (review.severity.blockOn)',
190
+ 'review.selfHeal.maxIterations': 'Self-heal max iterations (review.selfHeal.maxIterations)',
113
191
  };
114
192
  const example = examples[fieldName] || 'path/to/directory';
115
193
  const displayName = fieldDisplayNames[fieldName] || fieldName;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Credential utility functions
3
+ *
4
+ * Shared helpers for credential display masking.
5
+ */
6
+ /**
7
+ * Mask a credential value for display.
8
+ * Shows first 3 chars + '***...***' + last 3 chars.
9
+ * For values shorter than 8 chars, mask entirely as '***'.
10
+ *
11
+ * @param value - The raw credential value
12
+ * @returns Masked string safe for display
13
+ */
14
+ export declare function maskValue(value: string): string;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Credential utility functions
3
+ *
4
+ * Shared helpers for credential display masking.
5
+ */
6
+ /**
7
+ * Mask a credential value for display.
8
+ * Shows first 3 chars + '***...***' + last 3 chars.
9
+ * For values shorter than 8 chars, mask entirely as '***'.
10
+ *
11
+ * @param value - The raw credential value
12
+ * @returns Masked string safe for display
13
+ */
14
+ export function maskValue(value) {
15
+ if (value.length < 8) {
16
+ return '***';
17
+ }
18
+ return `${value.slice(0, 3)}***...***${value.slice(-3)}`;
19
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Duration Parsing Utility
3
+ *
4
+ * Parses human-readable duration strings (e.g., "30s", "5m", "1h", "90m")
5
+ * into milliseconds. Also accepts raw millisecond numbers for backward
6
+ * compatibility with existing --timeout flag usage.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * parseDuration('30s') // 30_000
11
+ * parseDuration('5m') // 300_000
12
+ * parseDuration('45m') // 2_700_000
13
+ * parseDuration('1h') // 3_600_000
14
+ * parseDuration('1.5h') // 5_400_000
15
+ * parseDuration('2700000') // 2_700_000 (raw ms, backward compat)
16
+ * parseDuration(2700000) // 2_700_000 (numeric passthrough)
17
+ * ```
18
+ */
19
+ /**
20
+ * Parse a duration string or number into milliseconds.
21
+ *
22
+ * Accepted formats:
23
+ * - `"30s"` — seconds
24
+ * - `"5m"` — minutes
25
+ * - `"1h"` — hours
26
+ * - `"1.5h"` — fractional units
27
+ * - `"2700000"` — raw milliseconds (string)
28
+ * - `2700000` — raw milliseconds (number)
29
+ *
30
+ * @param input - Duration string or number
31
+ * @returns Duration in milliseconds
32
+ * @throws Error if the input format is invalid or value is non-positive
33
+ */
34
+ export declare function parseDuration(input: number | string): number;
35
+ /**
36
+ * Format a millisecond duration into a human-readable string.
37
+ *
38
+ * @param ms - Duration in milliseconds
39
+ * @returns Formatted string (e.g., "45m", "1.5h", "30s")
40
+ */
41
+ export declare function formatDuration(ms: number): string;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Duration Parsing Utility
3
+ *
4
+ * Parses human-readable duration strings (e.g., "30s", "5m", "1h", "90m")
5
+ * into milliseconds. Also accepts raw millisecond numbers for backward
6
+ * compatibility with existing --timeout flag usage.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * parseDuration('30s') // 30_000
11
+ * parseDuration('5m') // 300_000
12
+ * parseDuration('45m') // 2_700_000
13
+ * parseDuration('1h') // 3_600_000
14
+ * parseDuration('1.5h') // 5_400_000
15
+ * parseDuration('2700000') // 2_700_000 (raw ms, backward compat)
16
+ * parseDuration(2700000) // 2_700_000 (numeric passthrough)
17
+ * ```
18
+ */
19
+ /** Duration unit multipliers in milliseconds */
20
+ const UNIT_MS = {
21
+ h: 3_600_000,
22
+ m: 60_000,
23
+ s: 1_000,
24
+ };
25
+ /**
26
+ * Parse a duration string or number into milliseconds.
27
+ *
28
+ * Accepted formats:
29
+ * - `"30s"` — seconds
30
+ * - `"5m"` — minutes
31
+ * - `"1h"` — hours
32
+ * - `"1.5h"` — fractional units
33
+ * - `"2700000"` — raw milliseconds (string)
34
+ * - `2700000` — raw milliseconds (number)
35
+ *
36
+ * @param input - Duration string or number
37
+ * @returns Duration in milliseconds
38
+ * @throws Error if the input format is invalid or value is non-positive
39
+ */
40
+ export function parseDuration(input) {
41
+ // Numeric passthrough
42
+ if (typeof input === 'number') {
43
+ if (input <= 0 || !Number.isFinite(input)) {
44
+ throw new Error(`Invalid timeout value: ${input}. Must be a positive number.`);
45
+ }
46
+ return Math.round(input);
47
+ }
48
+ const trimmed = input.trim();
49
+ if (trimmed.length === 0) {
50
+ throw new Error('Timeout value cannot be empty.');
51
+ }
52
+ // Try matching duration pattern: number + unit suffix
53
+ const match = trimmed.match(/^(\d+(?:\.\d+)?)\s*(s|m|h)$/i);
54
+ if (match) {
55
+ const value = Number.parseFloat(match[1]);
56
+ const unit = match[2].toLowerCase();
57
+ const multiplier = UNIT_MS[unit];
58
+ const ms = Math.round(value * multiplier);
59
+ if (ms <= 0) {
60
+ throw new Error(`Invalid timeout value: ${trimmed}. Must result in a positive duration.`);
61
+ }
62
+ return ms;
63
+ }
64
+ // Try raw numeric string (milliseconds)
65
+ const numeric = Number(trimmed);
66
+ if (!Number.isNaN(numeric) && Number.isFinite(numeric) && numeric > 0) {
67
+ return Math.round(numeric);
68
+ }
69
+ throw new Error(`Invalid timeout format: "${trimmed}". ` +
70
+ `Expected a duration like "30s", "5m", "1h", "90m", or raw milliseconds like "2700000".`);
71
+ }
72
+ /**
73
+ * Format a millisecond duration into a human-readable string.
74
+ *
75
+ * @param ms - Duration in milliseconds
76
+ * @returns Formatted string (e.g., "45m", "1.5h", "30s")
77
+ */
78
+ export function formatDuration(ms) {
79
+ if (ms >= 3_600_000) {
80
+ const hours = ms / 3_600_000;
81
+ return Number.isInteger(hours) ? `${hours}h` : `${hours.toFixed(1)}h`;
82
+ }
83
+ if (ms >= 60_000) {
84
+ const minutes = ms / 60_000;
85
+ return Number.isInteger(minutes) ? `${minutes}m` : `${minutes.toFixed(1)}m`;
86
+ }
87
+ const seconds = ms / 1_000;
88
+ return Number.isInteger(seconds) ? `${seconds}s` : `${seconds.toFixed(1)}s`;
89
+ }
@@ -27,6 +27,7 @@ export declare const agentFlags: {
27
27
  provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
28
28
  task: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
29
  timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
30
+ 'review-timeout': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
30
31
  'max-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
31
32
  'retry-backoff': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
32
33
  };
@@ -15,6 +15,7 @@
15
15
  * ```
16
16
  */
17
17
  import { Flags } from '@oclif/core';
18
+ import { parseDuration } from './duration.js';
18
19
  /**
19
20
  * Agent, Task, and Provider flags for customizing command behavior
20
21
  *
@@ -44,9 +45,17 @@ export const agentFlags = {
44
45
  description: 'Override which task command to execute (e.g., develop-story, draft, review-implementation). Defaults to command-appropriate task.',
45
46
  helpGroup: 'Agent Customization',
46
47
  }),
47
- timeout: Flags.integer({
48
+ timeout: Flags.custom({
49
+ parse: async (input) => parseDuration(input),
50
+ })({
48
51
  default: 2_700_000,
49
- description: 'Agent execution timeout in milliseconds (default: 2700000 = 45 minutes)',
52
+ description: 'Agent execution timeout accepts durations like 30s, 5m, 1h, 90m, or raw milliseconds (default: 45m)',
53
+ helpGroup: 'Resilience',
54
+ }),
55
+ 'review-timeout': Flags.custom({
56
+ parse: async (input) => parseDuration(input),
57
+ })({
58
+ description: 'AI review scanner timeout — overrides --timeout for review phase only (default: 5m)',
50
59
  helpGroup: 'Resilience',
51
60
  }),
52
61
  'max-retries': Flags.integer({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hyperdrive.bot/bmad-workflow",
3
3
  "description": "AI-driven development workflow orchestration CLI for BMAD projects",
4
- "version": "1.0.18",
4
+ "version": "1.0.19",
5
5
  "author": {
6
6
  "name": "DevSquad",
7
7
  "email": "marcelo@devsquad.email",
@@ -14,6 +14,7 @@
14
14
  "url": "https://gitlab.com/dev_squad/repo/cli/bmad-orchestrator/issues"
15
15
  },
16
16
  "dependencies": {
17
+ "@hyperdrive.bot/plugin-telemetry": "file:../telemetry-plugin",
17
18
  "@oclif/core": "^4",
18
19
  "@oclif/plugin-help": "^6",
19
20
  "bcrypt": "^6.0.0",
@@ -89,7 +90,8 @@
89
90
  "dirname": "bmad-workflow",
90
91
  "commands": "./dist/commands",
91
92
  "plugins": [
92
- "@oclif/plugin-help"
93
+ "@oclif/plugin-help",
94
+ "@hyperdrive.bot/plugin-telemetry"
93
95
  ],
94
96
  "topicSeparator": " "
95
97
  },