@hyperdrive.bot/bmad-workflow 1.0.25 → 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.
- 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 +24 -15
- package/dist/services/agents/agent-runner-factory.js +95 -15
- package/dist/services/agents/channel-agent-runner.d.ts +76 -0
- package/dist/services/agents/channel-agent-runner.js +256 -0
- package/dist/services/agents/channel-session-manager.d.ts +126 -0
- package/dist/services/agents/channel-session-manager.js +260 -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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
4
5
|
import { parseDuration } from '../utils/duration.js';
|
|
5
6
|
import { AssetResolver } from '../services/file-system/asset-resolver.js';
|
|
6
7
|
import { mergeExecutionConfig } from '../utils/config-merge.js';
|
|
@@ -8,7 +9,12 @@ import { createAgentRunner, isProviderSupported } from '../services/agents/agent
|
|
|
8
9
|
import { getRegisteredScannerNames } from '../services/review/scanner-factory.js';
|
|
9
10
|
import { Severity } from '../services/review/types.js';
|
|
10
11
|
import { FileManager } from '../services/file-system/file-manager.js';
|
|
12
|
+
import { GlobMatcher } from '../services/file-system/glob-matcher.js';
|
|
11
13
|
import { PathResolver } from '../services/file-system/path-resolver.js';
|
|
14
|
+
import { GitOps } from '../services/git/git-ops.js';
|
|
15
|
+
import { PushConflictHandler } from '../services/git/push-conflict-handler.js';
|
|
16
|
+
import { GitBackedLockService } from '../services/lock/git-backed-lock-service.js';
|
|
17
|
+
import { LockService } from '../services/lock/lock-service.js';
|
|
12
18
|
import { BatchProcessor } from '../services/orchestration/batch-processor.js';
|
|
13
19
|
import { InputDetector } from '../services/orchestration/input-detector.js';
|
|
14
20
|
import { StoryTypeDetector } from '../services/orchestration/story-type-detector.js';
|
|
@@ -102,6 +108,11 @@ export default class Workflow extends Command {
|
|
|
102
108
|
model: Flags.string({
|
|
103
109
|
description: 'Model to use for AI provider. Format varies by provider: Claude (claude-opus-4-5-20251101), Gemini (gemini-2.5-pro), OpenCode (anthropic/claude-3-5-sonnet)',
|
|
104
110
|
}),
|
|
111
|
+
'session-name': Flags.string({
|
|
112
|
+
char: 'n',
|
|
113
|
+
description: 'Session display name template. Supports {prefix}, {phase}, {story} placeholders. ' +
|
|
114
|
+
'Auto-generated from input filename if not provided.',
|
|
115
|
+
}),
|
|
105
116
|
prefix: Flags.string({
|
|
106
117
|
default: '',
|
|
107
118
|
description: 'Filename prefix for generated files and session directory name',
|
|
@@ -170,6 +181,10 @@ export default class Workflow extends Command {
|
|
|
170
181
|
default: false,
|
|
171
182
|
description: 'Skip epic creation phase',
|
|
172
183
|
}),
|
|
184
|
+
'skip-locking': Flags.boolean({
|
|
185
|
+
default: false,
|
|
186
|
+
description: 'Disable story locking (for single-agent runs)',
|
|
187
|
+
}),
|
|
173
188
|
'skip-stories': Flags.boolean({
|
|
174
189
|
default: false,
|
|
175
190
|
description: 'Skip story creation phase',
|
|
@@ -197,6 +212,16 @@ export default class Workflow extends Command {
|
|
|
197
212
|
default: 5000,
|
|
198
213
|
description: 'Backoff delay between retries in milliseconds',
|
|
199
214
|
}),
|
|
215
|
+
'use-channels': Flags.boolean({
|
|
216
|
+
default: false,
|
|
217
|
+
description: 'Use Channel transport for agent communication (requires pre-started Channel agents)',
|
|
218
|
+
helpGroup: 'Channel Transport',
|
|
219
|
+
}),
|
|
220
|
+
stream: Flags.boolean({
|
|
221
|
+
default: false,
|
|
222
|
+
description: 'Stream full Claude output to stdout in real-time (verbose passthrough of everything Claude does)',
|
|
223
|
+
helpGroup: 'Output',
|
|
224
|
+
}),
|
|
200
225
|
verbose: Flags.boolean({
|
|
201
226
|
char: 'v',
|
|
202
227
|
default: false,
|
|
@@ -234,6 +259,8 @@ export default class Workflow extends Command {
|
|
|
234
259
|
this.error('--cwd and --worktree are mutually exclusive. --worktree sets cwd automatically.', { exit: 1 });
|
|
235
260
|
}
|
|
236
261
|
const entities = flags['gut-entities'].split(',').map((e) => e.trim()).filter(Boolean);
|
|
262
|
+
// Preflight: validate gut entities exist before attempting worktree creation
|
|
263
|
+
this.validateGutEntities(entities);
|
|
237
264
|
const branchSlug = `workflow/${basename(inputPath, '.md').replace(/^PRD-/, '').toLowerCase()}-${Date.now()}`;
|
|
238
265
|
const installFlag = flags['worktree-install'] ? '--install' : '';
|
|
239
266
|
this.log(colors.info(`Creating isolated worktree: ${branchSlug}`));
|
|
@@ -296,8 +323,8 @@ export default class Workflow extends Command {
|
|
|
296
323
|
if (!isProviderSupported(merged.provider)) {
|
|
297
324
|
this.error(`Unsupported provider: ${merged.provider}. Use 'claude', 'gemini', or 'opencode'.`, { exit: 1 });
|
|
298
325
|
}
|
|
299
|
-
// Initialize services with parallel concurrency and
|
|
300
|
-
await this.initializeServices(merged.parallel, merged.provider, flags.verbose);
|
|
326
|
+
// Initialize services with parallel concurrency, provider, and locking
|
|
327
|
+
await this.initializeServices(merged.parallel, merged.provider, flags.verbose, !flags['skip-locking']);
|
|
301
328
|
// Register signal handlers
|
|
302
329
|
this.registerSignalHandlers();
|
|
303
330
|
// Initialize session scaffolder and create session structure
|
|
@@ -343,6 +370,7 @@ export default class Workflow extends Command {
|
|
|
343
370
|
dryRun: flags['dry-run'],
|
|
344
371
|
epicInterval: merged.epicInterval,
|
|
345
372
|
input: args.input,
|
|
373
|
+
lockingEnabled: !flags['skip-locking'],
|
|
346
374
|
maxRetries: merged.maxRetries,
|
|
347
375
|
mcp: flags.mcp || undefined,
|
|
348
376
|
mcpPhases: flags['mcp-phases'] ? flags['mcp-phases'].split(',').map((s) => s.trim()) : undefined,
|
|
@@ -369,6 +397,10 @@ export default class Workflow extends Command {
|
|
|
369
397
|
skipStories: flags['skip-stories'],
|
|
370
398
|
storyInterval: merged.storyInterval,
|
|
371
399
|
timeout: merged.timeout,
|
|
400
|
+
sessionName: flags['session-name'],
|
|
401
|
+
inputPath: args.input,
|
|
402
|
+
stream: flags.stream,
|
|
403
|
+
useChannels: flags['use-channels'],
|
|
372
404
|
verbose: flags.verbose,
|
|
373
405
|
};
|
|
374
406
|
// Validate --mcp-phases when --mcp is enabled
|
|
@@ -898,7 +930,7 @@ export default class Workflow extends Command {
|
|
|
898
930
|
* @param verbose - Enable verbose output mode
|
|
899
931
|
* @private
|
|
900
932
|
*/
|
|
901
|
-
async initializeServices(maxConcurrency = 3, provider = 'claude', verbose = false) {
|
|
933
|
+
async initializeServices(maxConcurrency = 3, provider = 'claude', verbose = false, lockingEnabled = false) {
|
|
902
934
|
// Create logger — suppress INFO logs in non-verbose mode to keep terminal clean
|
|
903
935
|
this.logger = createLogger({ level: verbose ? 'info' : 'warn', namespace: 'commands:workflow' });
|
|
904
936
|
this.logger.info({ provider }, 'Initializing services with AI provider');
|
|
@@ -923,6 +955,16 @@ export default class Workflow extends Command {
|
|
|
923
955
|
nonTTY: !process.stdout.isTTY,
|
|
924
956
|
verbose,
|
|
925
957
|
});
|
|
958
|
+
// Conditionally instantiate lock service for multi-session coordination
|
|
959
|
+
let lockService;
|
|
960
|
+
if (lockingEnabled) {
|
|
961
|
+
const globMatcher = new GlobMatcher(fileManager, this.logger);
|
|
962
|
+
const gitOps = new GitOps();
|
|
963
|
+
const conflictHandler = new PushConflictHandler(gitOps);
|
|
964
|
+
const lockSvc = new LockService(fileManager, globMatcher, this.logger);
|
|
965
|
+
lockService = new GitBackedLockService(lockSvc, gitOps, conflictHandler);
|
|
966
|
+
this.logger.info('GitBackedLockService instantiated for multi-session coordination');
|
|
967
|
+
}
|
|
926
968
|
// Merge scaffolder callbacks with reporter callbacks for dual-channel output (AC: #1)
|
|
927
969
|
const callbacks = this.mergeCallbacks(this.createScaffolderCallbacks(), this.reporter.getCallbacks());
|
|
928
970
|
// Create orchestrator with merged callbacks
|
|
@@ -933,6 +975,7 @@ export default class Workflow extends Command {
|
|
|
933
975
|
epicParser,
|
|
934
976
|
fileManager,
|
|
935
977
|
inputDetector,
|
|
978
|
+
lockService,
|
|
936
979
|
logger: this.logger,
|
|
937
980
|
pathResolver,
|
|
938
981
|
prdParser,
|
|
@@ -975,4 +1018,77 @@ export default class Workflow extends Command {
|
|
|
975
1018
|
this.log('\n' + colors.warning('⚠️ Cancellation requested... completing current operation'));
|
|
976
1019
|
});
|
|
977
1020
|
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Validate that all requested gut entities exist before attempting worktree creation.
|
|
1023
|
+
* Reads .gut/config.json directly for fast validation without subprocess overhead.
|
|
1024
|
+
* Fails fast with clear error message listing valid entities and fuzzy suggestions.
|
|
1025
|
+
*/
|
|
1026
|
+
validateGutEntities(entityNames) {
|
|
1027
|
+
const gutConfigPath = join(process.cwd(), '.gut', 'config.json');
|
|
1028
|
+
if (!existsSync(gutConfigPath)) {
|
|
1029
|
+
this.error('gut is not initialized in this workspace. Run \'gut init\' first.', { exit: 1 });
|
|
1030
|
+
}
|
|
1031
|
+
let allEntities;
|
|
1032
|
+
try {
|
|
1033
|
+
const content = readFileSync(gutConfigPath, 'utf8');
|
|
1034
|
+
const config = JSON.parse(content);
|
|
1035
|
+
allEntities = config.entities || [];
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
this.error('Failed to read .gut/config.json. Ensure the file is valid JSON.', { exit: 1 });
|
|
1039
|
+
}
|
|
1040
|
+
const entityMap = new Map(allEntities.map((e) => [e.name, e]));
|
|
1041
|
+
const invalidEntities = entityNames.filter((name) => !entityMap.has(name));
|
|
1042
|
+
if (invalidEntities.length > 0) {
|
|
1043
|
+
// Entity name doesn't exist in config — show suggestions and bail
|
|
1044
|
+
this.showEntityError(invalidEntities, allEntities);
|
|
1045
|
+
}
|
|
1046
|
+
// Entity names are valid — now check if paths actually exist (uncloned submodules, missing repos)
|
|
1047
|
+
const uninitializedEntities = [];
|
|
1048
|
+
for (const name of entityNames) {
|
|
1049
|
+
const entity = entityMap.get(name);
|
|
1050
|
+
const entityPath = join(process.cwd(), entity.path);
|
|
1051
|
+
const hasContent = existsSync(join(entityPath, 'package.json')) ||
|
|
1052
|
+
existsSync(join(entityPath, '.git')) ||
|
|
1053
|
+
existsSync(join(entityPath, 'Cargo.toml')) ||
|
|
1054
|
+
existsSync(join(entityPath, 'go.mod'));
|
|
1055
|
+
if (!hasContent) {
|
|
1056
|
+
uninitializedEntities.push({ name, path: entity.path });
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
if (uninitializedEntities.length > 0) {
|
|
1060
|
+
const details = uninitializedEntities
|
|
1061
|
+
.map((e) => ` • ${e.name} @ ${e.path}`)
|
|
1062
|
+
.join('\n');
|
|
1063
|
+
this.error(`These gut entities are registered but their paths are empty or uninitialized:\n${details}\n\n` +
|
|
1064
|
+
'This usually means the git submodule was not cloned. Try:\n' +
|
|
1065
|
+
uninitializedEntities.map((e) => ` git submodule update --init ${e.path}`).join('\n') +
|
|
1066
|
+
'\n\nOr clone via gut:\n' +
|
|
1067
|
+
uninitializedEntities.map((e) => ` gut entity clone ${e.name}`).join('\n'), { exit: 1 });
|
|
1068
|
+
}
|
|
1069
|
+
this.log(colors.info(`Preflight: all ${entityNames.length} gut entities validated ✓`));
|
|
1070
|
+
}
|
|
1071
|
+
showEntityError(invalidEntities, allEntities) {
|
|
1072
|
+
// Build helpful error with suggestions
|
|
1073
|
+
const suggestions = allEntities
|
|
1074
|
+
.filter((e) => invalidEntities.some((invalid) => e.name.includes(invalid) ||
|
|
1075
|
+
invalid.includes(e.name) ||
|
|
1076
|
+
invalid.split('-').some((part) => part.length > 2 && e.name.includes(part))))
|
|
1077
|
+
.map((e) => e.name)
|
|
1078
|
+
.slice(0, 5);
|
|
1079
|
+
let msg = `Invalid --gut-entities: ${invalidEntities.join(', ')}\n\n`;
|
|
1080
|
+
msg += `Available entities (${allEntities.length} total):\n`;
|
|
1081
|
+
msg += allEntities
|
|
1082
|
+
.slice(0, 15)
|
|
1083
|
+
.map((e) => ` • ${e.name} (${e.type}) @ ${e.path}`)
|
|
1084
|
+
.join('\n');
|
|
1085
|
+
if (allEntities.length > 15) {
|
|
1086
|
+
msg += `\n ... and ${allEntities.length - 15} more (run 'gut entity list' to see all)`;
|
|
1087
|
+
}
|
|
1088
|
+
if (suggestions.length > 0) {
|
|
1089
|
+
msg += `\n\nDid you mean: ${suggestions.join(', ')}?`;
|
|
1090
|
+
}
|
|
1091
|
+
msg += '\n\nTo add a missing entity: gut entity add <name> <type> -p <path>';
|
|
1092
|
+
this.error(msg, { exit: 1 });
|
|
1093
|
+
}
|
|
978
1094
|
}
|
|
@@ -15,6 +15,17 @@ export type OnPromptCallback = (prompt: string, options: AgentOptionsWithoutProm
|
|
|
15
15
|
* Callback invoked when a response is received from Claude
|
|
16
16
|
*/
|
|
17
17
|
export type OnResponseCallback = (result: AgentResult) => Promise<void> | void;
|
|
18
|
+
/**
|
|
19
|
+
* Callback invoked for each meaningful stream event during agent execution.
|
|
20
|
+
* Only fired when the provider supports streaming (Claude with stream-json).
|
|
21
|
+
* Receives a human-readable summary of what the agent is doing.
|
|
22
|
+
*/
|
|
23
|
+
export type OnStreamCallback = (summary: string) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Callback invoked for every raw stream-json line (verbose mode).
|
|
26
|
+
* Receives the parsed assistant text or tool_result output verbatim.
|
|
27
|
+
*/
|
|
28
|
+
export type OnStreamVerboseCallback = (text: string) => void;
|
|
18
29
|
/**
|
|
19
30
|
* Options for executing a Claude AI agent
|
|
20
31
|
*/
|
|
@@ -46,6 +57,17 @@ export interface AgentOptions {
|
|
|
46
57
|
* Callback invoked when response is received (for logging)
|
|
47
58
|
*/
|
|
48
59
|
onResponse?: OnResponseCallback;
|
|
60
|
+
/**
|
|
61
|
+
* Callback invoked for each meaningful stream event during execution.
|
|
62
|
+
* Receives a human-readable summary like "🔧 Edit: src/foo.ts" or "💬 Running tests..."
|
|
63
|
+
* Only works with Claude provider (stream-json output mode).
|
|
64
|
+
*/
|
|
65
|
+
onStream?: OnStreamCallback;
|
|
66
|
+
/**
|
|
67
|
+
* Callback invoked with full raw text from every assistant message and tool result.
|
|
68
|
+
* Used for verbose/passthrough mode where the caller wants to see everything Claude outputs.
|
|
69
|
+
*/
|
|
70
|
+
onStreamVerbose?: OnStreamVerboseCallback;
|
|
49
71
|
/**
|
|
50
72
|
* The prompt to execute with the agent
|
|
51
73
|
*/
|
|
@@ -63,4 +85,15 @@ export interface AgentOptions {
|
|
|
63
85
|
* Timeout in milliseconds (default: 1800000ms = 30 minutes)
|
|
64
86
|
*/
|
|
65
87
|
timeout?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Phase name context (used by Channel transport for callback reporting)
|
|
90
|
+
*/
|
|
91
|
+
phaseName?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Session display name passed to Claude via --name flag
|
|
94
|
+
*
|
|
95
|
+
* When set, the spawned Claude session gets this name, making it
|
|
96
|
+
* visible in /resume and terminal title. Claude-only feature.
|
|
97
|
+
*/
|
|
98
|
+
sessionName?: string;
|
|
66
99
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Transport mechanism used to execute the agent
|
|
3
|
+
*/
|
|
4
|
+
export type TransportType = 'subprocess' | 'channel';
|
|
5
|
+
/**
|
|
6
|
+
* Result of executing an AI agent
|
|
3
7
|
*/
|
|
4
8
|
export interface AgentResult {
|
|
5
9
|
/**
|
|
@@ -26,4 +30,9 @@ export interface AgentResult {
|
|
|
26
30
|
* Whether the agent execution was successful
|
|
27
31
|
*/
|
|
28
32
|
success: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Transport mechanism used to execute the agent.
|
|
35
|
+
* Optional for backward compatibility — defaults to 'subprocess' when omitted.
|
|
36
|
+
*/
|
|
37
|
+
transport?: TransportType;
|
|
29
38
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch Result Model
|
|
3
|
+
*
|
|
4
|
+
* Represents the outcome of a lock-aware story dispatch operation.
|
|
5
|
+
* Used by LockedStoryDispatcher to communicate whether a story was
|
|
6
|
+
* processed, skipped (locked), or failed.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Result of dispatching a story through the lock-aware dispatcher
|
|
10
|
+
*/
|
|
11
|
+
export interface DispatchResult {
|
|
12
|
+
/** Outcome of the dispatch operation */
|
|
13
|
+
status: 'failed' | 'processed' | 'skipped';
|
|
14
|
+
/** Human-readable reason (populated for skipped/failed) */
|
|
15
|
+
reason?: string;
|
|
16
|
+
}
|
package/dist/models/index.d.ts
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export * from './agent-options.js';
|
|
5
5
|
export * from './agent-result.js';
|
|
6
|
+
export * from './dispatch.js';
|
|
6
7
|
export * from './phase-result.js';
|
|
7
8
|
export * from './provider.js';
|
|
8
9
|
export * from './story.js';
|
|
9
10
|
export * from './workflow-callbacks.js';
|
|
10
11
|
export * from './workflow-config.js';
|
|
11
12
|
export * from './workflow-result.js';
|
|
13
|
+
export type { SessionConfig, SessionHandle } from '../services/agents/channel-session-manager.js';
|
|
14
|
+
export { SessionStartTimeoutError } from '../services/agents/channel-session-manager.js';
|
package/dist/models/index.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export * from './agent-options.js';
|
|
5
5
|
export * from './agent-result.js';
|
|
6
|
+
export * from './dispatch.js';
|
|
6
7
|
export * from './phase-result.js';
|
|
7
8
|
export * from './provider.js';
|
|
8
9
|
export * from './story.js';
|
|
9
10
|
export * from './workflow-callbacks.js';
|
|
10
11
|
export * from './workflow-config.js';
|
|
11
12
|
export * from './workflow-result.js';
|
|
13
|
+
export { SessionStartTimeoutError } from '../services/agents/channel-session-manager.js';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock File Model
|
|
3
|
+
*
|
|
4
|
+
* Defines the structure and validation schema for story lock files.
|
|
5
|
+
* Lock files prevent concurrent agent sessions from modifying the same story.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { BaseError } from '../utils/errors.js';
|
|
9
|
+
/**
|
|
10
|
+
* Lock file content interface
|
|
11
|
+
*
|
|
12
|
+
* Represents the JSON structure stored in `.lock` files alongside story files.
|
|
13
|
+
*/
|
|
14
|
+
export interface LockFileContent {
|
|
15
|
+
/** The unique session ID of the agent */
|
|
16
|
+
agent_session_id: string;
|
|
17
|
+
/** The agent name that acquired the lock */
|
|
18
|
+
agent_name: string;
|
|
19
|
+
/** ISO 8601 timestamp when the lock was acquired */
|
|
20
|
+
started_at: string;
|
|
21
|
+
/** The git branch the agent is working on */
|
|
22
|
+
branch: string;
|
|
23
|
+
/** Path to the story file being locked */
|
|
24
|
+
story_file: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Zod schema for runtime validation of lock file content
|
|
28
|
+
*
|
|
29
|
+
* Validates all 5 fields with strict typing:
|
|
30
|
+
* - agent_session_id must be a valid UUID
|
|
31
|
+
* - started_at must be a valid ISO 8601 datetime
|
|
32
|
+
* - All other string fields must be non-empty
|
|
33
|
+
* - Unknown keys are rejected (.strict())
|
|
34
|
+
*/
|
|
35
|
+
export declare const lockFileSchema: z.ZodObject<{
|
|
36
|
+
agent_session_id: z.ZodString;
|
|
37
|
+
agent_name: z.ZodString;
|
|
38
|
+
started_at: z.ZodString;
|
|
39
|
+
branch: z.ZodString;
|
|
40
|
+
story_file: z.ZodString;
|
|
41
|
+
}, z.core.$strict>;
|
|
42
|
+
/**
|
|
43
|
+
* Lock conflict error for story locking failures
|
|
44
|
+
*
|
|
45
|
+
* Used when an agent attempts to acquire a lock on a story that is already locked
|
|
46
|
+
* by another agent session.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* Push rejected error for lock acquire failures
|
|
50
|
+
*
|
|
51
|
+
* Used when a git push is rejected during lock acquisition (non-fast-forward
|
|
52
|
+
* after retry exhaustion, network errors, etc.).
|
|
53
|
+
*/
|
|
54
|
+
export declare class PushRejectedError extends BaseError {
|
|
55
|
+
/**
|
|
56
|
+
* Create a new PushRejectedError
|
|
57
|
+
*
|
|
58
|
+
* @param params - Object with storyFile and reason
|
|
59
|
+
* @example
|
|
60
|
+
* throw new PushRejectedError({ storyFile: 'docs/stories/STORY-1.001.md', reason: 'non-fast-forward' })
|
|
61
|
+
*/
|
|
62
|
+
constructor({ storyFile, reason }: {
|
|
63
|
+
storyFile: string;
|
|
64
|
+
reason: string;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export declare class LockConflictError extends BaseError {
|
|
68
|
+
/**
|
|
69
|
+
* The existing lock content that caused the conflict
|
|
70
|
+
*/
|
|
71
|
+
readonly lockContent: LockFileContent;
|
|
72
|
+
/**
|
|
73
|
+
* Create a new LockConflictError
|
|
74
|
+
*
|
|
75
|
+
* @param lockContent - The existing lock file content causing the conflict
|
|
76
|
+
* @example
|
|
77
|
+
* throw new LockConflictError(existingLock)
|
|
78
|
+
*/
|
|
79
|
+
constructor(lockContent: LockFileContent);
|
|
80
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock File Model
|
|
3
|
+
*
|
|
4
|
+
* Defines the structure and validation schema for story lock files.
|
|
5
|
+
* Lock files prevent concurrent agent sessions from modifying the same story.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { BaseError, ERR_LOCK } from '../utils/errors.js';
|
|
9
|
+
/**
|
|
10
|
+
* Zod schema for runtime validation of lock file content
|
|
11
|
+
*
|
|
12
|
+
* Validates all 5 fields with strict typing:
|
|
13
|
+
* - agent_session_id must be a valid UUID
|
|
14
|
+
* - started_at must be a valid ISO 8601 datetime
|
|
15
|
+
* - All other string fields must be non-empty
|
|
16
|
+
* - Unknown keys are rejected (.strict())
|
|
17
|
+
*/
|
|
18
|
+
export const lockFileSchema = z.object({
|
|
19
|
+
agent_session_id: z.string().uuid(),
|
|
20
|
+
agent_name: z.string().min(1),
|
|
21
|
+
started_at: z.string().datetime(),
|
|
22
|
+
branch: z.string().min(1),
|
|
23
|
+
story_file: z.string().min(1),
|
|
24
|
+
}).strict();
|
|
25
|
+
/**
|
|
26
|
+
* Lock conflict error for story locking failures
|
|
27
|
+
*
|
|
28
|
+
* Used when an agent attempts to acquire a lock on a story that is already locked
|
|
29
|
+
* by another agent session.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Push rejected error for lock acquire failures
|
|
33
|
+
*
|
|
34
|
+
* Used when a git push is rejected during lock acquisition (non-fast-forward
|
|
35
|
+
* after retry exhaustion, network errors, etc.).
|
|
36
|
+
*/
|
|
37
|
+
export class PushRejectedError extends BaseError {
|
|
38
|
+
/**
|
|
39
|
+
* Create a new PushRejectedError
|
|
40
|
+
*
|
|
41
|
+
* @param params - Object with storyFile and reason
|
|
42
|
+
* @example
|
|
43
|
+
* throw new PushRejectedError({ storyFile: 'docs/stories/STORY-1.001.md', reason: 'non-fast-forward' })
|
|
44
|
+
*/
|
|
45
|
+
constructor({ storyFile, reason }) {
|
|
46
|
+
super(`Push rejected while acquiring lock for "${storyFile}": ${reason}`, ERR_LOCK, { reason, story_file: storyFile }, 'A git push was rejected during lock acquisition. This may indicate a concurrent operation or network issue.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class LockConflictError extends BaseError {
|
|
50
|
+
/**
|
|
51
|
+
* The existing lock content that caused the conflict
|
|
52
|
+
*/
|
|
53
|
+
lockContent;
|
|
54
|
+
/**
|
|
55
|
+
* Create a new LockConflictError
|
|
56
|
+
*
|
|
57
|
+
* @param lockContent - The existing lock file content causing the conflict
|
|
58
|
+
* @example
|
|
59
|
+
* throw new LockConflictError(existingLock)
|
|
60
|
+
*/
|
|
61
|
+
constructor(lockContent) {
|
|
62
|
+
super(`Story "${lockContent.story_file}" is locked by agent "${lockContent.agent_name}" (session: ${lockContent.agent_session_id})`, ERR_LOCK, {
|
|
63
|
+
agent_name: lockContent.agent_name,
|
|
64
|
+
story_file: lockContent.story_file,
|
|
65
|
+
session_id: lockContent.agent_session_id,
|
|
66
|
+
}, `Wait for agent "${lockContent.agent_name}" to finish or release the lock manually.`);
|
|
67
|
+
this.lockContent = lockContent;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -37,6 +37,14 @@ export interface PhaseResult {
|
|
|
37
37
|
* @default false
|
|
38
38
|
*/
|
|
39
39
|
skipped: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Count of stories skipped due to lock conflicts
|
|
42
|
+
*
|
|
43
|
+
* Only populated when lockingEnabled is true and another session
|
|
44
|
+
* held the lock on a story at dispatch time.
|
|
45
|
+
* @default 0
|
|
46
|
+
*/
|
|
47
|
+
skippedCount?: number;
|
|
40
48
|
/**
|
|
41
49
|
* Count of successful operations in this phase
|
|
42
50
|
*
|
package/dist/models/provider.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
export const PROVIDER_CONFIGS = {
|
|
8
8
|
claude: {
|
|
9
9
|
command: 'claude',
|
|
10
|
-
flags: ['--dangerously-skip-permissions', '--
|
|
10
|
+
flags: ['--dangerously-skip-permissions', '-p', '--output-format', 'stream-json', '--verbose'],
|
|
11
11
|
modelFlag: '--model',
|
|
12
12
|
supportsFileReferences: true,
|
|
13
13
|
},
|
|
@@ -98,6 +98,27 @@ export interface LayerContext {
|
|
|
98
98
|
*/
|
|
99
99
|
failureCount?: number;
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Reason why the factory fell back from Channel transport to subprocess.
|
|
103
|
+
*/
|
|
104
|
+
export type ChannelFallbackReason = 'not_discovered' | 'connection_refused' | 'timeout' | 'health_check_failed';
|
|
105
|
+
/**
|
|
106
|
+
* Context for Channel connect/disconnect lifecycle events.
|
|
107
|
+
*/
|
|
108
|
+
export interface ChannelContext {
|
|
109
|
+
sessionId: string;
|
|
110
|
+
port: number;
|
|
111
|
+
agentType: string;
|
|
112
|
+
phaseName: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Context for Channel fallback events (Channel → subprocess).
|
|
116
|
+
*/
|
|
117
|
+
export interface ChannelFallbackContext {
|
|
118
|
+
agentType: string;
|
|
119
|
+
reason: ChannelFallbackReason;
|
|
120
|
+
phaseName: string;
|
|
121
|
+
}
|
|
101
122
|
/**
|
|
102
123
|
* Context for spawn (agent execution) lifecycle events
|
|
103
124
|
*
|
|
@@ -160,6 +181,10 @@ export interface SpawnContext {
|
|
|
160
181
|
* Worker ID for pipelined execution
|
|
161
182
|
*/
|
|
162
183
|
workerId?: number;
|
|
184
|
+
/**
|
|
185
|
+
* Transport mechanism used: 'subprocess' (claude -p) or 'channel' (persistent session)
|
|
186
|
+
*/
|
|
187
|
+
transport?: 'subprocess' | 'channel';
|
|
163
188
|
/**
|
|
164
189
|
* Additional metadata
|
|
165
190
|
*/
|
|
@@ -248,4 +273,16 @@ export interface WorkflowCallbacks {
|
|
|
248
273
|
* Called when an error occurs
|
|
249
274
|
*/
|
|
250
275
|
onError?: (context: ErrorContext) => void;
|
|
276
|
+
/**
|
|
277
|
+
* Called when a Channel agent connection is established (first successful envelope exchange)
|
|
278
|
+
*/
|
|
279
|
+
onChannelConnect?: (context: ChannelContext) => void;
|
|
280
|
+
/**
|
|
281
|
+
* Called when a Channel agent connection is terminated
|
|
282
|
+
*/
|
|
283
|
+
onChannelDisconnect?: (context: ChannelContext) => void;
|
|
284
|
+
/**
|
|
285
|
+
* Called when the factory falls back from Channel transport to subprocess
|
|
286
|
+
*/
|
|
287
|
+
onChannelFallback?: (context: ChannelFallbackContext) => void;
|
|
251
288
|
}
|
|
@@ -70,6 +70,14 @@ export interface WorkflowConfig {
|
|
|
70
70
|
* @default 3
|
|
71
71
|
*/
|
|
72
72
|
parallel: number;
|
|
73
|
+
/**
|
|
74
|
+
* Enable git-backed story locking for multi-session coordination
|
|
75
|
+
*
|
|
76
|
+
* When true, stories are locked before processing and unlocked after completion,
|
|
77
|
+
* preventing parallel sessions from working the same story simultaneously.
|
|
78
|
+
* @default false
|
|
79
|
+
*/
|
|
80
|
+
lockingEnabled?: boolean;
|
|
73
81
|
/**
|
|
74
82
|
* Enable pipelined workflow execution
|
|
75
83
|
*
|
|
@@ -255,6 +263,40 @@ export interface WorkflowConfig {
|
|
|
255
263
|
* @default 5000 (5 seconds)
|
|
256
264
|
*/
|
|
257
265
|
retryBackoffMs?: number;
|
|
266
|
+
/**
|
|
267
|
+
* Enable Channel transport for agent execution
|
|
268
|
+
*
|
|
269
|
+
* When true, the AgentRunnerFactory discovers Channel-capable agents
|
|
270
|
+
* via listAgents() and returns a ChannelAgentRunner when a match is found.
|
|
271
|
+
* Falls back to subprocess runner when no Channel agent is discovered.
|
|
272
|
+
* @default false
|
|
273
|
+
*/
|
|
274
|
+
useChannels?: boolean;
|
|
275
|
+
/**
|
|
276
|
+
* Timeout for Channel agent requests in milliseconds
|
|
277
|
+
*
|
|
278
|
+
* Applied to ChannelAgentRunner HTTP requests when useChannels is true.
|
|
279
|
+
* @default 300_000 (5 minutes)
|
|
280
|
+
*/
|
|
281
|
+
channelTimeout?: number;
|
|
282
|
+
/**
|
|
283
|
+
* Session display name template for spawned Claude sessions
|
|
284
|
+
*
|
|
285
|
+
* When set, each spawned Claude subprocess gets a named session
|
|
286
|
+
* visible in /resume and terminal title. Supports placeholders:
|
|
287
|
+
* - {story} — replaced with story number (e.g., "1.001")
|
|
288
|
+
* - {phase} — replaced with phase name (e.g., "dev", "epic", "story")
|
|
289
|
+
* - {prefix} — replaced with the derived prefix from inputPath
|
|
290
|
+
*/
|
|
291
|
+
sessionName?: string;
|
|
292
|
+
/**
|
|
293
|
+
* Original input file path — used for auto-generating session names
|
|
294
|
+
*
|
|
295
|
+
* When set, resolveSessionName() derives a compact prefix from this path
|
|
296
|
+
* and auto-generates session names when no explicit --session-name is given.
|
|
297
|
+
* Absent for programmatic callers that don't supply a file path.
|
|
298
|
+
*/
|
|
299
|
+
inputPath?: string;
|
|
258
300
|
/**
|
|
259
301
|
* Detailed output mode
|
|
260
302
|
*
|
|
@@ -262,4 +304,12 @@ export interface WorkflowConfig {
|
|
|
262
304
|
* @default false
|
|
263
305
|
*/
|
|
264
306
|
verbose: boolean;
|
|
307
|
+
/**
|
|
308
|
+
* Stream full Claude output to stdout in real-time
|
|
309
|
+
*
|
|
310
|
+
* When true, raw assistant text from Claude's stream-json output is
|
|
311
|
+
* piped directly to stdout so callers can see exactly what's happening.
|
|
312
|
+
* @default false
|
|
313
|
+
*/
|
|
314
|
+
stream?: boolean;
|
|
265
315
|
}
|