gswd 0.1.0
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/agents/gswd/architecture-drafter.md +70 -0
- package/agents/gswd/brainstorm-alternatives.md +60 -0
- package/agents/gswd/devils-advocate.md +57 -0
- package/agents/gswd/icp-persona.md +58 -0
- package/agents/gswd/integrations-checker.md +68 -0
- package/agents/gswd/journey-mapper.md +69 -0
- package/agents/gswd/market-researcher.md +54 -0
- package/agents/gswd/positioning.md +54 -0
- package/bin/gswd-tools.cjs +716 -0
- package/lib/audit.ts +959 -0
- package/lib/bootstrap.ts +617 -0
- package/lib/compile.ts +940 -0
- package/lib/config.ts +164 -0
- package/lib/imagine-agents.ts +154 -0
- package/lib/imagine-gate.ts +156 -0
- package/lib/imagine-input.ts +242 -0
- package/lib/imagine-synthesis.ts +402 -0
- package/lib/imagine.ts +433 -0
- package/lib/parse.ts +196 -0
- package/lib/render.ts +200 -0
- package/lib/specify-agents.ts +332 -0
- package/lib/specify-journeys.ts +410 -0
- package/lib/specify-nfr.ts +208 -0
- package/lib/specify-roles.ts +122 -0
- package/lib/specify.ts +773 -0
- package/lib/state.ts +305 -0
- package/package.json +26 -0
- package/templates/gswd/ARCHITECTURE.template.md +17 -0
- package/templates/gswd/AUDIT.template.md +31 -0
- package/templates/gswd/COMPETITION.template.md +18 -0
- package/templates/gswd/DECISIONS.template.md +18 -0
- package/templates/gswd/GTM.template.md +18 -0
- package/templates/gswd/ICP.template.md +18 -0
- package/templates/gswd/IMAGINE.template.md +24 -0
- package/templates/gswd/INTEGRATIONS.template.md +7 -0
- package/templates/gswd/JOURNEYS.template.md +7 -0
- package/templates/gswd/NFR.template.md +7 -0
- package/templates/gswd/PROJECT.template.md +21 -0
- package/templates/gswd/REQUIREMENTS.template.md +31 -0
- package/templates/gswd/ROADMAP.template.md +21 -0
- package/templates/gswd/SPEC.template.md +19 -0
- package/templates/gswd/STATE.template.md +15 -0
package/lib/render.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSWD Render Module — Terminal UI rendering functions
|
|
3
|
+
*
|
|
4
|
+
* All functions return strings (not print to stdout).
|
|
5
|
+
* Zero external dependencies. Uses only string operations.
|
|
6
|
+
*
|
|
7
|
+
* Schema: GSWD_SPEC.md Section 7.1-7.3
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ─── Status Symbols ──────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export const SYMBOLS = {
|
|
13
|
+
complete: '\u2713', // ✓
|
|
14
|
+
failed: '\u2717', // ✗
|
|
15
|
+
inProgress: '\u25C6', // ◆
|
|
16
|
+
pending: '\u25CB', // ○
|
|
17
|
+
autoApproved: '\u26A1', // ⚡
|
|
18
|
+
warning: '\u26A0', // ⚠
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pad or truncate a string to exact width.
|
|
25
|
+
* If str exceeds width, truncate and append '...' (total = width).
|
|
26
|
+
*/
|
|
27
|
+
export function padRight(str: string, width: number): string {
|
|
28
|
+
if (str.length > width) {
|
|
29
|
+
return str.slice(0, width - 3) + '...';
|
|
30
|
+
}
|
|
31
|
+
return str + ' '.repeat(width - str.length);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Banner ──────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Render a stage banner matching GSWD_SPEC Section 7.1.
|
|
38
|
+
*
|
|
39
|
+
* ```
|
|
40
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
41
|
+
* GSWD ► {STAGE NAME}
|
|
42
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* Banner width = 55 characters. Stage name is uppercased.
|
|
46
|
+
*/
|
|
47
|
+
export function renderBanner(stageName: string): string {
|
|
48
|
+
const BANNER_WIDTH = 55;
|
|
49
|
+
const border = '\u2501'.repeat(BANNER_WIDTH);
|
|
50
|
+
const title = ` GSWD \u25B6 ${stageName.toUpperCase()}`;
|
|
51
|
+
return `${border}\n${title}\n${border}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Checkpoint Box ──────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Render a checkpoint box matching GSWD_SPEC Section 7.2.
|
|
58
|
+
*
|
|
59
|
+
* Box total width: 56 characters (54 inner + 2 border characters).
|
|
60
|
+
* Uses single-line box-drawing characters.
|
|
61
|
+
*
|
|
62
|
+
* @param type - Header text (e.g., "Decision Required")
|
|
63
|
+
* @param context - Context paragraph
|
|
64
|
+
* @param options - Numbered options list
|
|
65
|
+
* @param actionPrompt - Action prompt (appears after → )
|
|
66
|
+
*/
|
|
67
|
+
export function renderCheckpoint(
|
|
68
|
+
type: string,
|
|
69
|
+
context: string,
|
|
70
|
+
options: string[],
|
|
71
|
+
actionPrompt: string,
|
|
72
|
+
): string {
|
|
73
|
+
const BOX_WIDTH = 56;
|
|
74
|
+
const INNER_WIDTH = BOX_WIDTH - 4; // 52 (accounting for "│ " and " │")
|
|
75
|
+
|
|
76
|
+
const topBorder = `\u250C${'\u2500'.repeat(BOX_WIDTH - 2)}\u2510`;
|
|
77
|
+
const midBorder = `\u251C${'\u2500'.repeat(BOX_WIDTH - 2)}\u2524`;
|
|
78
|
+
const bottomBorder = `\u2514${'\u2500'.repeat(BOX_WIDTH - 2)}\u2518`;
|
|
79
|
+
|
|
80
|
+
const line = (text: string): string => {
|
|
81
|
+
const padded = padRight(text, INNER_WIDTH);
|
|
82
|
+
return `\u2502 ${padded} \u2502`;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const blankLine = line('');
|
|
86
|
+
|
|
87
|
+
const lines: string[] = [];
|
|
88
|
+
lines.push(topBorder);
|
|
89
|
+
lines.push(line(type));
|
|
90
|
+
lines.push(midBorder);
|
|
91
|
+
lines.push(line(context));
|
|
92
|
+
lines.push(blankLine);
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < options.length; i++) {
|
|
95
|
+
lines.push(line(`${i + 1}) ${options[i]}`));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
lines.push(blankLine);
|
|
99
|
+
lines.push(line(`\u2192 ${actionPrompt}`));
|
|
100
|
+
lines.push(bottomBorder);
|
|
101
|
+
|
|
102
|
+
return lines.join('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── Next Up ─────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Render a Next Up block matching GSWD_SPEC Section 7.3.
|
|
109
|
+
*
|
|
110
|
+
* ```
|
|
111
|
+
* ✓ Wrote: file1, file2, ...
|
|
112
|
+
*
|
|
113
|
+
* Next up:
|
|
114
|
+
* {nextCommand}
|
|
115
|
+
*
|
|
116
|
+
* Tip:
|
|
117
|
+
* If context is getting crowded, run /clear.
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function renderNextUp(
|
|
121
|
+
files: string[],
|
|
122
|
+
nextCommand: string,
|
|
123
|
+
alsoAvailable?: string[],
|
|
124
|
+
): string {
|
|
125
|
+
const parts: string[] = [];
|
|
126
|
+
|
|
127
|
+
parts.push(`${SYMBOLS.complete} Wrote: ${files.join(', ')}`);
|
|
128
|
+
parts.push('');
|
|
129
|
+
parts.push('Next up:');
|
|
130
|
+
parts.push(` ${nextCommand}`);
|
|
131
|
+
parts.push('');
|
|
132
|
+
parts.push('Tip:');
|
|
133
|
+
parts.push(' If context is getting crowded, run /clear.');
|
|
134
|
+
|
|
135
|
+
if (alsoAvailable && alsoAvailable.length > 0) {
|
|
136
|
+
parts.push('');
|
|
137
|
+
parts.push('Also available:');
|
|
138
|
+
for (const item of alsoAvailable) {
|
|
139
|
+
parts.push(` - ${item}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return parts.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── Status Line ─────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Render a single status line with the appropriate symbol.
|
|
150
|
+
*
|
|
151
|
+
* Maps: done/pass -> ✓, fail -> ✗, in_progress -> ◆, not_started -> ○
|
|
152
|
+
*/
|
|
153
|
+
export function renderStatusLine(stage: string, status: string): string {
|
|
154
|
+
let symbol: string;
|
|
155
|
+
switch (status) {
|
|
156
|
+
case 'done':
|
|
157
|
+
case 'pass':
|
|
158
|
+
symbol = SYMBOLS.complete;
|
|
159
|
+
break;
|
|
160
|
+
case 'fail':
|
|
161
|
+
symbol = SYMBOLS.failed;
|
|
162
|
+
break;
|
|
163
|
+
case 'in_progress':
|
|
164
|
+
symbol = SYMBOLS.inProgress;
|
|
165
|
+
break;
|
|
166
|
+
case 'not_started':
|
|
167
|
+
symbol = SYMBOLS.pending;
|
|
168
|
+
break;
|
|
169
|
+
default:
|
|
170
|
+
symbol = SYMBOLS.pending;
|
|
171
|
+
}
|
|
172
|
+
return `${symbol} ${stage}: ${status}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Progress Bar ────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Render an ASCII progress bar.
|
|
179
|
+
*
|
|
180
|
+
* ```
|
|
181
|
+
* Progress: ████████░░ 80%
|
|
182
|
+
* ```
|
|
183
|
+
*
|
|
184
|
+
* @param current - Current value
|
|
185
|
+
* @param total - Total value
|
|
186
|
+
* @param width - Bar width in characters (default 10)
|
|
187
|
+
*/
|
|
188
|
+
export function renderProgressBar(
|
|
189
|
+
current: number,
|
|
190
|
+
total: number,
|
|
191
|
+
width: number = 10,
|
|
192
|
+
): string {
|
|
193
|
+
const ratio = total === 0 ? 0 : Math.min(current / total, 1);
|
|
194
|
+
const filled = Math.round(ratio * width);
|
|
195
|
+
const empty = width - filled;
|
|
196
|
+
const percent = Math.round(ratio * 100);
|
|
197
|
+
|
|
198
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty);
|
|
199
|
+
return `Progress: ${bar} ${percent}%`;
|
|
200
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSWD Specify Agents Module — Agent definitions, sequential-then-parallel orchestration
|
|
3
|
+
*
|
|
4
|
+
* Provides the 3 specify agents: journey-mapper (sequential), then
|
|
5
|
+
* architecture-drafter + integrations-checker (parallel).
|
|
6
|
+
*
|
|
7
|
+
* Follows the imagine-agents.ts pattern but with phased orchestration.
|
|
8
|
+
*
|
|
9
|
+
* Schema: GSWD_SPEC.md Section 8.3, Section 11.1
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import type { Journey, FunctionalRequirement } from './specify-journeys.js';
|
|
14
|
+
|
|
15
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export interface SpecifyAgentDefinition {
|
|
18
|
+
/** Agent name matching file name (without .md) */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Path to agent definition file */
|
|
21
|
+
definitionPath: string;
|
|
22
|
+
/** Phase: 'sequential' runs first, 'parallel' runs after sequential completes */
|
|
23
|
+
phase: 'sequential' | 'parallel';
|
|
24
|
+
/** Minimum headings expected in agent output */
|
|
25
|
+
requiredHeadings: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Integration {
|
|
29
|
+
/** Integration ID in I-001 format */
|
|
30
|
+
id: string;
|
|
31
|
+
/** Integration name */
|
|
32
|
+
name: string;
|
|
33
|
+
/** Steps to set up the integration */
|
|
34
|
+
setupSteps: string[];
|
|
35
|
+
/** Authentication method */
|
|
36
|
+
authMethod: string;
|
|
37
|
+
/** Monthly cost or quota description */
|
|
38
|
+
costQuota: string;
|
|
39
|
+
/** Fallback if integration unavailable */
|
|
40
|
+
fallback: string;
|
|
41
|
+
/** Approval status — MANDATORY field */
|
|
42
|
+
status: 'approved' | 'deferred with fallback' | 'rejected';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ArchitectureComponent {
|
|
46
|
+
/** Component ID in C-001 format */
|
|
47
|
+
id: string;
|
|
48
|
+
/** Component name */
|
|
49
|
+
name: string;
|
|
50
|
+
/** What this component does */
|
|
51
|
+
responsibility: string;
|
|
52
|
+
/** Other C-XXX IDs this depends on */
|
|
53
|
+
dependencies: string[];
|
|
54
|
+
/** FR-XXX IDs this component implements */
|
|
55
|
+
linkedFRs: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AgentResult {
|
|
59
|
+
/** Agent name */
|
|
60
|
+
agent: string;
|
|
61
|
+
/** Agent output content */
|
|
62
|
+
content: string;
|
|
63
|
+
/** Completion status */
|
|
64
|
+
status: 'complete' | 'failed';
|
|
65
|
+
/** Error message if failed */
|
|
66
|
+
error?: string;
|
|
67
|
+
/** Execution time in milliseconds */
|
|
68
|
+
duration_ms?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type SpawnFn = (prompt: string) => Promise<string>;
|
|
72
|
+
|
|
73
|
+
// ─── Agent Definitions ───────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The 3 specify agents from GSWD_SPEC Section 11.1.
|
|
77
|
+
* journey-mapper runs first (sequential); others run after (parallel).
|
|
78
|
+
*/
|
|
79
|
+
export const SPECIFY_AGENTS: SpecifyAgentDefinition[] = [
|
|
80
|
+
{
|
|
81
|
+
name: 'journey-mapper',
|
|
82
|
+
definitionPath: 'agents/gswd/journey-mapper.md',
|
|
83
|
+
phase: 'sequential',
|
|
84
|
+
requiredHeadings: ['### J-'],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'architecture-drafter',
|
|
88
|
+
definitionPath: 'agents/gswd/architecture-drafter.md',
|
|
89
|
+
phase: 'parallel',
|
|
90
|
+
requiredHeadings: ['### Components', '### Data Model'],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'integrations-checker',
|
|
94
|
+
definitionPath: 'agents/gswd/integrations-checker.md',
|
|
95
|
+
phase: 'parallel',
|
|
96
|
+
requiredHeadings: ['## Integrations'],
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// ─── Prompt Building ─────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
export interface SpecifyAgentContext {
|
|
103
|
+
/** DECISIONS.md content */
|
|
104
|
+
decisionsContent: string;
|
|
105
|
+
/** IMAGINE.md content (optional) */
|
|
106
|
+
imagineContent?: string;
|
|
107
|
+
/** Extracted journeys (available after journey-mapper) */
|
|
108
|
+
journeys?: Journey[];
|
|
109
|
+
/** Extracted FRs (available after FR extraction) */
|
|
110
|
+
frs?: FunctionalRequirement[];
|
|
111
|
+
/** Auto policy config content */
|
|
112
|
+
autoPolicy?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build the prompt for a specify agent.
|
|
117
|
+
*
|
|
118
|
+
* - journey-mapper: gets DECISIONS.md + IMAGINE.md
|
|
119
|
+
* - architecture-drafter: gets journeys + FRs + DECISIONS.md
|
|
120
|
+
* - integrations-checker: gets journeys + FRs + DECISIONS.md + auto policy
|
|
121
|
+
*/
|
|
122
|
+
export function buildSpecifyAgentPrompt(
|
|
123
|
+
agent: SpecifyAgentDefinition,
|
|
124
|
+
context: SpecifyAgentContext,
|
|
125
|
+
): string {
|
|
126
|
+
let definition: string;
|
|
127
|
+
try {
|
|
128
|
+
definition = fs.readFileSync(agent.definitionPath, 'utf-8');
|
|
129
|
+
} catch {
|
|
130
|
+
definition = `Agent: ${agent.name}\nRole: Specify agent`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const sections: string[] = [definition];
|
|
134
|
+
|
|
135
|
+
// Always include decisions
|
|
136
|
+
if (context.decisionsContent) {
|
|
137
|
+
sections.push(`<decisions>\n${context.decisionsContent}\n</decisions>`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
switch (agent.name) {
|
|
141
|
+
case 'journey-mapper':
|
|
142
|
+
if (context.imagineContent) {
|
|
143
|
+
sections.push(`<imagine>\n${context.imagineContent}\n</imagine>`);
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
case 'architecture-drafter':
|
|
148
|
+
if (context.frs && context.frs.length > 0) {
|
|
149
|
+
const frSummary = context.frs
|
|
150
|
+
.map((fr) => `- ${fr.id}: ${fr.description} [${fr.scope}/${fr.priority}]`)
|
|
151
|
+
.join('\n');
|
|
152
|
+
sections.push(`<functional_requirements>\n${frSummary}\n</functional_requirements>`);
|
|
153
|
+
}
|
|
154
|
+
if (context.journeys && context.journeys.length > 0) {
|
|
155
|
+
const journeySummary = context.journeys
|
|
156
|
+
.map((j) => `- ${j.id}: ${j.name} (${j.type}, ${j.steps.length} steps)`)
|
|
157
|
+
.join('\n');
|
|
158
|
+
sections.push(`<journeys>\n${journeySummary}\n</journeys>`);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case 'integrations-checker':
|
|
163
|
+
if (context.frs && context.frs.length > 0) {
|
|
164
|
+
const frSummary = context.frs
|
|
165
|
+
.map((fr) => `- ${fr.id}: ${fr.description} [${fr.scope}/${fr.priority}]`)
|
|
166
|
+
.join('\n');
|
|
167
|
+
sections.push(`<functional_requirements>\n${frSummary}\n</functional_requirements>`);
|
|
168
|
+
}
|
|
169
|
+
if (context.journeys && context.journeys.length > 0) {
|
|
170
|
+
const journeySummary = context.journeys
|
|
171
|
+
.map((j) => `- ${j.id}: ${j.name} (${j.type}, ${j.steps.length} steps)`)
|
|
172
|
+
.join('\n');
|
|
173
|
+
sections.push(`<journeys>\n${journeySummary}\n</journeys>`);
|
|
174
|
+
}
|
|
175
|
+
if (context.autoPolicy) {
|
|
176
|
+
sections.push(`<auto_policy>\n${context.autoPolicy}\n</auto_policy>`);
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
sections.push(
|
|
182
|
+
`Produce your output now. Include all required headings: ${agent.requiredHeadings.join(', ')}.`
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return sections.join('\n\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ─── Orchestration ───────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Orchestrate specify agents with sequential-then-parallel pattern.
|
|
192
|
+
*
|
|
193
|
+
* Phase 1 (sequential): Run journey-mapper, wait for completion.
|
|
194
|
+
* Phase 2 (parallel): Run architecture-drafter + integrations-checker concurrently.
|
|
195
|
+
*
|
|
196
|
+
* Failed agents are marked with status: 'failed' but do not crash orchestration.
|
|
197
|
+
*
|
|
198
|
+
* @param agents - Agent definitions (defaults to SPECIFY_AGENTS)
|
|
199
|
+
* @param context - Shared context for prompt building
|
|
200
|
+
* @param spawnFn - Function that spawns an agent (Task() wrapper)
|
|
201
|
+
* @returns All agent results including failures
|
|
202
|
+
*/
|
|
203
|
+
export async function orchestrateSpecifyAgents(
|
|
204
|
+
agents: SpecifyAgentDefinition[],
|
|
205
|
+
context: SpecifyAgentContext,
|
|
206
|
+
spawnFn: SpawnFn,
|
|
207
|
+
): Promise<AgentResult[]> {
|
|
208
|
+
const results: AgentResult[] = [];
|
|
209
|
+
|
|
210
|
+
// Phase 1: Sequential agents
|
|
211
|
+
const sequentialAgents = agents.filter((a) => a.phase === 'sequential');
|
|
212
|
+
for (const agent of sequentialAgents) {
|
|
213
|
+
const result = await runSingleAgent(agent, context, spawnFn);
|
|
214
|
+
results.push(result);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Phase 2: Parallel agents
|
|
218
|
+
const parallelAgents = agents.filter((a) => a.phase === 'parallel');
|
|
219
|
+
if (parallelAgents.length > 0) {
|
|
220
|
+
const parallelPromises = parallelAgents.map((agent) =>
|
|
221
|
+
runSingleAgent(agent, context, spawnFn)
|
|
222
|
+
);
|
|
223
|
+
const parallelResults = await Promise.all(parallelPromises);
|
|
224
|
+
results.push(...parallelResults);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return results;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Run a single agent and return its result.
|
|
232
|
+
*/
|
|
233
|
+
async function runSingleAgent(
|
|
234
|
+
agent: SpecifyAgentDefinition,
|
|
235
|
+
context: SpecifyAgentContext,
|
|
236
|
+
spawnFn: SpawnFn,
|
|
237
|
+
): Promise<AgentResult> {
|
|
238
|
+
const start = Date.now();
|
|
239
|
+
try {
|
|
240
|
+
const prompt = buildSpecifyAgentPrompt(agent, context);
|
|
241
|
+
const content = await spawnFn(prompt);
|
|
242
|
+
return {
|
|
243
|
+
agent: agent.name,
|
|
244
|
+
content,
|
|
245
|
+
status: 'complete',
|
|
246
|
+
duration_ms: Date.now() - start,
|
|
247
|
+
};
|
|
248
|
+
} catch (err: unknown) {
|
|
249
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
250
|
+
return {
|
|
251
|
+
agent: agent.name,
|
|
252
|
+
content: '',
|
|
253
|
+
status: 'failed',
|
|
254
|
+
error: message,
|
|
255
|
+
duration_ms: Date.now() - start,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─── Integration Validation ──────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Valid integration status values.
|
|
264
|
+
*/
|
|
265
|
+
export const VALID_INTEGRATION_STATUSES = [
|
|
266
|
+
'approved',
|
|
267
|
+
'deferred with fallback',
|
|
268
|
+
'rejected',
|
|
269
|
+
] as const;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Validate that an integration has all required fields including mandatory Status.
|
|
273
|
+
*/
|
|
274
|
+
export function validateIntegration(
|
|
275
|
+
integration: Integration,
|
|
276
|
+
): { valid: boolean; errors: string[] } {
|
|
277
|
+
const errors: string[] = [];
|
|
278
|
+
|
|
279
|
+
// ID format
|
|
280
|
+
if (!/^I-\d{3,}$/.test(integration.id)) {
|
|
281
|
+
errors.push(`Invalid integration ID format: ${integration.id} (expected I-XXX)`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Name non-empty
|
|
285
|
+
if (!integration.name || integration.name.trim() === '') {
|
|
286
|
+
errors.push(`Integration ${integration.id} has empty name`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Status is mandatory and must be a valid value
|
|
290
|
+
if (!VALID_INTEGRATION_STATUSES.includes(integration.status)) {
|
|
291
|
+
errors.push(
|
|
292
|
+
`Integration ${integration.id} has invalid status: "${integration.status}" (expected: ${VALID_INTEGRATION_STATUSES.join(', ')})`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Deferred integrations must have a fallback
|
|
297
|
+
if (integration.status === 'deferred with fallback') {
|
|
298
|
+
if (!integration.fallback || integration.fallback.trim() === '') {
|
|
299
|
+
errors.push(
|
|
300
|
+
`Integration ${integration.id} is deferred but has no fallback`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { valid: errors.length === 0, errors };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Validate that a component has correct ID format and required fields.
|
|
310
|
+
*/
|
|
311
|
+
export function validateComponent(
|
|
312
|
+
component: ArchitectureComponent,
|
|
313
|
+
): { valid: boolean; errors: string[] } {
|
|
314
|
+
const errors: string[] = [];
|
|
315
|
+
|
|
316
|
+
// ID format
|
|
317
|
+
if (!/^C-\d{3,}$/.test(component.id)) {
|
|
318
|
+
errors.push(`Invalid component ID format: ${component.id} (expected C-XXX)`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Name non-empty
|
|
322
|
+
if (!component.name || component.name.trim() === '') {
|
|
323
|
+
errors.push(`Component ${component.id} has empty name`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Responsibility non-empty
|
|
327
|
+
if (!component.responsibility || component.responsibility.trim() === '') {
|
|
328
|
+
errors.push(`Component ${component.id} has empty responsibility`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { valid: errors.length === 0, errors };
|
|
332
|
+
}
|