@arvorco/relentless 0.3.1 → 0.4.2
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/.claude/commands/relentless.convert.md +25 -0
- package/.claude/skills/analyze/SKILL.md +113 -40
- package/.claude/skills/analyze/templates/analysis-report.md +138 -0
- package/.claude/skills/checklist/SKILL.md +143 -51
- package/.claude/skills/checklist/templates/checklist.md +43 -11
- package/.claude/skills/clarify/SKILL.md +70 -11
- package/.claude/skills/constitution/SKILL.md +61 -3
- package/.claude/skills/constitution/templates/constitution.md +241 -160
- package/.claude/skills/constitution/templates/prompt.md +150 -20
- package/.claude/skills/convert/SKILL.md +248 -0
- package/.claude/skills/implement/SKILL.md +82 -34
- package/.claude/skills/plan/SKILL.md +136 -27
- package/.claude/skills/plan/templates/plan.md +92 -9
- package/.claude/skills/specify/SKILL.md +110 -19
- package/.claude/skills/specify/templates/spec.md +40 -5
- package/.claude/skills/tasks/SKILL.md +75 -1
- package/.claude/skills/tasks/templates/tasks.md +5 -4
- package/CHANGELOG.md +63 -1
- package/MANUAL.md +40 -0
- package/README.md +262 -10
- package/bin/relentless.ts +292 -5
- package/package.json +2 -2
- package/relentless/config.json +46 -2
- package/relentless/constitution.md +2 -2
- package/relentless/prompt.md +97 -18
- package/src/agents/amp.ts +53 -13
- package/src/agents/claude.ts +70 -15
- package/src/agents/codex.ts +73 -14
- package/src/agents/droid.ts +68 -14
- package/src/agents/exec.ts +96 -0
- package/src/agents/gemini.ts +59 -16
- package/src/agents/opencode.ts +188 -9
- package/src/cli/fallback-order.ts +210 -0
- package/src/cli/index.ts +63 -0
- package/src/cli/mode-flag.ts +198 -0
- package/src/cli/review-flags.ts +192 -0
- package/src/config/loader.ts +16 -1
- package/src/config/schema.ts +157 -2
- package/src/execution/runner.ts +144 -21
- package/src/init/scaffolder.ts +285 -25
- package/src/prd/parser.ts +92 -1
- package/src/prd/types.ts +136 -0
- package/src/review/index.ts +92 -0
- package/src/review/prompt.ts +293 -0
- package/src/review/runner.ts +337 -0
- package/src/review/tasks/docs.ts +529 -0
- package/src/review/tasks/index.ts +80 -0
- package/src/review/tasks/lint.ts +436 -0
- package/src/review/tasks/quality.ts +760 -0
- package/src/review/tasks/security.ts +452 -0
- package/src/review/tasks/test.ts +456 -0
- package/src/review/tasks/typecheck.ts +323 -0
- package/src/review/types.ts +139 -0
- package/src/routing/cascade.ts +310 -0
- package/src/routing/classifier.ts +338 -0
- package/src/routing/estimate.ts +270 -0
- package/src/routing/fallback.ts +512 -0
- package/src/routing/index.ts +124 -0
- package/src/routing/registry.ts +501 -0
- package/src/routing/report.ts +570 -0
- package/src/routing/router.ts +287 -0
- package/src/tui/App.tsx +2 -0
- package/src/tui/TUIRunner.tsx +103 -8
- package/src/tui/components/CurrentStory.tsx +23 -1
- package/src/tui/hooks/useTUI.ts +1 -0
- package/src/tui/types.ts +9 -0
package/src/init/scaffolder.ts
CHANGED
|
@@ -20,11 +20,242 @@
|
|
|
20
20
|
* - Does not modify project root files (CLAUDE.md, AGENTS.md, etc.)
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
23
|
+
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import chalk from "chalk";
|
|
26
26
|
import { checkAgentHealth } from "../agents/registry";
|
|
27
|
-
import { DEFAULT_CONFIG } from "../config/schema";
|
|
27
|
+
import { DEFAULT_CONFIG, type Mode } from "../config/schema";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Explanation of Auto Mode shown to users during init
|
|
31
|
+
*/
|
|
32
|
+
export const AUTO_MODE_EXPLANATION = `
|
|
33
|
+
Smart Auto Mode automatically routes tasks to the most cost-effective model
|
|
34
|
+
based on complexity. Simple tasks use free/cheap models, complex tasks use
|
|
35
|
+
premium models. This typically saves 50-75% on API costs.
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Descriptions for each cost mode
|
|
40
|
+
*/
|
|
41
|
+
export const MODE_DESCRIPTIONS: Record<Mode, string> = {
|
|
42
|
+
free: "Free tier models only (OpenCode) - Maximum savings",
|
|
43
|
+
cheap: "Budget models (Haiku, Gemini Flash, GPT-5.2 low effort) - High savings, good quality",
|
|
44
|
+
good: "Balanced models (Sonnet, GPT-5.2 medium effort) - Good savings, high quality (Recommended)",
|
|
45
|
+
genius: "Premium models (Opus, GPT-5.2 high effort) - No savings, maximum quality",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Estimated savings percentages for each mode
|
|
50
|
+
*/
|
|
51
|
+
const MODE_SAVINGS: Record<Mode, string> = {
|
|
52
|
+
free: "~95%",
|
|
53
|
+
cheap: "~75%",
|
|
54
|
+
good: "~50%",
|
|
55
|
+
genius: "~0%",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Interface for Auto Mode configuration options
|
|
60
|
+
*/
|
|
61
|
+
export interface InitAutoModeOptions {
|
|
62
|
+
/** Skip the interactive prompt and use provided values */
|
|
63
|
+
skipPrompt?: boolean;
|
|
64
|
+
/** Whether Auto Mode should be enabled */
|
|
65
|
+
enabled?: boolean;
|
|
66
|
+
/** Default mode to use */
|
|
67
|
+
defaultMode?: Mode;
|
|
68
|
+
/** Generate YAML config instead of JSON */
|
|
69
|
+
generateYaml?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Interface for readline mock (for testing)
|
|
74
|
+
*/
|
|
75
|
+
interface ReadlineInterface {
|
|
76
|
+
question: (prompt: string, callback: (answer: string) => void) => void;
|
|
77
|
+
close: () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Options for promptAutoModeConfig
|
|
82
|
+
*/
|
|
83
|
+
export interface PromptAutoModeOptions {
|
|
84
|
+
/** Readline interface (can be mocked for testing) */
|
|
85
|
+
readline: ReadlineInterface;
|
|
86
|
+
/** Default mode if user presses Enter */
|
|
87
|
+
defaultMode: Mode;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Result from promptAutoModeConfig
|
|
92
|
+
*/
|
|
93
|
+
export interface AutoModeConfigResult {
|
|
94
|
+
enabled: boolean;
|
|
95
|
+
defaultMode?: Mode;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get estimated savings percentage for a mode
|
|
100
|
+
*/
|
|
101
|
+
export function getEstimatedSavings(mode: Mode): string {
|
|
102
|
+
return MODE_SAVINGS[mode];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse Auto Mode related CLI flags
|
|
107
|
+
*/
|
|
108
|
+
export function parseAutoModeFlags(options: {
|
|
109
|
+
yes?: boolean;
|
|
110
|
+
noAutoMode?: boolean;
|
|
111
|
+
}): InitAutoModeOptions {
|
|
112
|
+
// --no-auto-mode takes priority over --yes
|
|
113
|
+
if (options.noAutoMode) {
|
|
114
|
+
return {
|
|
115
|
+
skipPrompt: true,
|
|
116
|
+
enabled: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (options.yes) {
|
|
121
|
+
return {
|
|
122
|
+
skipPrompt: true,
|
|
123
|
+
enabled: true,
|
|
124
|
+
defaultMode: "good",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
skipPrompt: false,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if project already has Auto Mode config
|
|
135
|
+
*/
|
|
136
|
+
export async function hasExistingAutoModeConfig(projectDir: string): Promise<boolean> {
|
|
137
|
+
const relentlessDir = join(projectDir, "relentless");
|
|
138
|
+
|
|
139
|
+
// Check config.json
|
|
140
|
+
const configJsonPath = join(relentlessDir, "config.json");
|
|
141
|
+
if (existsSync(configJsonPath)) {
|
|
142
|
+
try {
|
|
143
|
+
const content = await Bun.file(configJsonPath).text();
|
|
144
|
+
const config = JSON.parse(content);
|
|
145
|
+
if (config.autoMode !== undefined) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// Ignore parse errors
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check relentless.config.yaml
|
|
154
|
+
const yamlPath = join(relentlessDir, "relentless.config.yaml");
|
|
155
|
+
if (existsSync(yamlPath)) {
|
|
156
|
+
try {
|
|
157
|
+
const content = await Bun.file(yamlPath).text();
|
|
158
|
+
if (content.includes("autoMode:")) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// Ignore read errors
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Prompt user for Auto Mode configuration
|
|
171
|
+
*/
|
|
172
|
+
export async function promptAutoModeConfig(
|
|
173
|
+
options: PromptAutoModeOptions
|
|
174
|
+
): Promise<AutoModeConfigResult> {
|
|
175
|
+
const { readline, defaultMode } = options;
|
|
176
|
+
|
|
177
|
+
return new Promise((resolve) => {
|
|
178
|
+
// First question: Enable Auto Mode?
|
|
179
|
+
readline.question(
|
|
180
|
+
chalk.yellow("Enable Smart Auto Mode? (Saves 50-75% on costs) [Y/n]: "),
|
|
181
|
+
(enableAnswer) => {
|
|
182
|
+
const normalizedAnswer = enableAnswer.trim().toLowerCase();
|
|
183
|
+
|
|
184
|
+
// Empty or affirmative answers enable Auto Mode
|
|
185
|
+
if (
|
|
186
|
+
normalizedAnswer === "" ||
|
|
187
|
+
normalizedAnswer === "y" ||
|
|
188
|
+
normalizedAnswer === "yes"
|
|
189
|
+
) {
|
|
190
|
+
// Second question: Select mode
|
|
191
|
+
readline.question(
|
|
192
|
+
chalk.yellow(`Default mode? [free/cheap/good/genius] (${defaultMode}): `),
|
|
193
|
+
(modeAnswer) => {
|
|
194
|
+
const trimmedMode = modeAnswer.trim().toLowerCase();
|
|
195
|
+
let selectedMode: Mode = defaultMode;
|
|
196
|
+
|
|
197
|
+
if (trimmedMode === "") {
|
|
198
|
+
selectedMode = defaultMode;
|
|
199
|
+
} else if (["free", "cheap", "good", "genius"].includes(trimmedMode)) {
|
|
200
|
+
selectedMode = trimmedMode as Mode;
|
|
201
|
+
} else {
|
|
202
|
+
// Invalid mode - use default
|
|
203
|
+
selectedMode = defaultMode;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
resolve({
|
|
207
|
+
enabled: true,
|
|
208
|
+
defaultMode: selectedMode,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
} else {
|
|
213
|
+
// User declined
|
|
214
|
+
resolve({
|
|
215
|
+
enabled: false,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Generate YAML config content for Auto Mode
|
|
225
|
+
*/
|
|
226
|
+
export function generateAutoModeYamlConfig(config: AutoModeConfigResult): string {
|
|
227
|
+
const lines: string[] = [];
|
|
228
|
+
|
|
229
|
+
lines.push("# Relentless Auto Mode Configuration");
|
|
230
|
+
lines.push("# Generated by relentless init");
|
|
231
|
+
lines.push("");
|
|
232
|
+
lines.push("autoMode:");
|
|
233
|
+
lines.push(` enabled: ${config.enabled}`);
|
|
234
|
+
|
|
235
|
+
if (config.enabled && config.defaultMode) {
|
|
236
|
+
lines.push(` defaultMode: ${config.defaultMode}`);
|
|
237
|
+
lines.push("");
|
|
238
|
+
lines.push(" # Harness fallback order (uncomment to customize)");
|
|
239
|
+
lines.push(" # fallbackOrder:");
|
|
240
|
+
lines.push(" # - claude");
|
|
241
|
+
lines.push(" # - amp");
|
|
242
|
+
lines.push(" # - opencode");
|
|
243
|
+
lines.push(" # - codex");
|
|
244
|
+
lines.push(" # - gemini");
|
|
245
|
+
lines.push(" # - droid");
|
|
246
|
+
lines.push("");
|
|
247
|
+
lines.push(" # Custom model mappings by complexity (uncomment to customize)");
|
|
248
|
+
lines.push(" # modeModels:");
|
|
249
|
+
lines.push(" # simple: haiku-4.5");
|
|
250
|
+
lines.push(" # medium: sonnet-4.5");
|
|
251
|
+
lines.push(" # complex: opus-4.5");
|
|
252
|
+
lines.push(" # expert: opus-4.5");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
lines.push("");
|
|
256
|
+
|
|
257
|
+
return lines.join("\n");
|
|
258
|
+
}
|
|
28
259
|
|
|
29
260
|
/**
|
|
30
261
|
* Get the relentless root directory
|
|
@@ -47,14 +278,6 @@ function getRelentlessRoot(): string {
|
|
|
47
278
|
|
|
48
279
|
const relentlessRoot = getRelentlessRoot();
|
|
49
280
|
|
|
50
|
-
/**
|
|
51
|
-
* Files to create in the relentless/ directory
|
|
52
|
-
* These can be force-updated with -f flag
|
|
53
|
-
*/
|
|
54
|
-
const RELENTLESS_FILES: Record<string, () => string> = {
|
|
55
|
-
"config.json": () => JSON.stringify(DEFAULT_CONFIG, null, 2),
|
|
56
|
-
};
|
|
57
|
-
|
|
58
281
|
/**
|
|
59
282
|
* Default progress.txt content for a new feature with YAML frontmatter
|
|
60
283
|
*/
|
|
@@ -81,7 +304,11 @@ patterns: []
|
|
|
81
304
|
/**
|
|
82
305
|
* Initialize Relentless in a project
|
|
83
306
|
*/
|
|
84
|
-
export async function initProject(
|
|
307
|
+
export async function initProject(
|
|
308
|
+
projectDir: string = process.cwd(),
|
|
309
|
+
force: boolean = false,
|
|
310
|
+
autoModeOptions: InitAutoModeOptions = {}
|
|
311
|
+
): Promise<void> {
|
|
85
312
|
console.log(chalk.bold.blue(`\n🚀 ${force ? "Reinstalling" : "Initializing"} Relentless\n`));
|
|
86
313
|
|
|
87
314
|
// Check installed agents
|
|
@@ -112,17 +339,50 @@ export async function initProject(projectDir: string = process.cwd(), force: boo
|
|
|
112
339
|
// Create relentless files (can be force-updated)
|
|
113
340
|
console.log(chalk.dim("\nCreating relentless files..."));
|
|
114
341
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
342
|
+
// Generate config with Auto Mode settings if provided
|
|
343
|
+
const configPath = join(relentlessDir, "config.json");
|
|
344
|
+
const existingAutoMode = await hasExistingAutoModeConfig(projectDir);
|
|
345
|
+
|
|
346
|
+
if (existsSync(configPath) && !force) {
|
|
347
|
+
console.log(` ${chalk.yellow("⚠")} relentless/config.json already exists, skipping`);
|
|
348
|
+
} else {
|
|
349
|
+
// Build config with Auto Mode settings
|
|
350
|
+
const config = { ...DEFAULT_CONFIG };
|
|
351
|
+
|
|
352
|
+
// Apply Auto Mode options if provided
|
|
353
|
+
if (autoModeOptions.skipPrompt) {
|
|
354
|
+
config.autoMode = {
|
|
355
|
+
...config.autoMode,
|
|
356
|
+
enabled: autoModeOptions.enabled ?? false,
|
|
357
|
+
};
|
|
358
|
+
if (autoModeOptions.enabled && autoModeOptions.defaultMode) {
|
|
359
|
+
config.autoMode.defaultMode = autoModeOptions.defaultMode;
|
|
360
|
+
}
|
|
361
|
+
} else if (!existingAutoMode) {
|
|
362
|
+
// Set default autoMode if no existing config
|
|
363
|
+
config.autoMode = {
|
|
364
|
+
...config.autoMode,
|
|
365
|
+
enabled: autoModeOptions.enabled ?? false,
|
|
366
|
+
};
|
|
367
|
+
if (autoModeOptions.enabled && autoModeOptions.defaultMode) {
|
|
368
|
+
config.autoMode.defaultMode = autoModeOptions.defaultMode;
|
|
369
|
+
}
|
|
121
370
|
}
|
|
122
371
|
|
|
123
|
-
await Bun.write(
|
|
124
|
-
const action =
|
|
125
|
-
console.log(` ${chalk.green("✓")} relentless
|
|
372
|
+
await Bun.write(configPath, JSON.stringify(config, null, 2));
|
|
373
|
+
const action = force ? "updated" : "created";
|
|
374
|
+
console.log(` ${chalk.green("✓")} relentless/config.json (${action})`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Generate YAML config if requested
|
|
378
|
+
if (autoModeOptions.generateYaml && autoModeOptions.enabled !== undefined) {
|
|
379
|
+
const yamlPath = join(relentlessDir, "relentless.config.yaml");
|
|
380
|
+
const yamlContent = generateAutoModeYamlConfig({
|
|
381
|
+
enabled: autoModeOptions.enabled,
|
|
382
|
+
defaultMode: autoModeOptions.defaultMode,
|
|
383
|
+
});
|
|
384
|
+
await Bun.write(yamlPath, yamlContent);
|
|
385
|
+
console.log(` ${chalk.green("✓")} relentless/relentless.config.yaml (created)`);
|
|
126
386
|
}
|
|
127
387
|
|
|
128
388
|
// Note: constitution.md and prompt.md are NOT created here
|
|
@@ -171,6 +431,7 @@ export async function initProject(projectDir: string = process.cwd(), force: boo
|
|
|
171
431
|
"specify",
|
|
172
432
|
"plan",
|
|
173
433
|
"tasks",
|
|
434
|
+
"convert",
|
|
174
435
|
"checklist",
|
|
175
436
|
"clarify",
|
|
176
437
|
"analyze",
|
|
@@ -382,6 +643,7 @@ export async function initProject(projectDir: string = process.cwd(), force: boo
|
|
|
382
643
|
"relentless.checklist.md",
|
|
383
644
|
"relentless.clarify.md",
|
|
384
645
|
"relentless.constitution.md",
|
|
646
|
+
"relentless.convert.md",
|
|
385
647
|
"relentless.implement.md",
|
|
386
648
|
"relentless.plan.md",
|
|
387
649
|
"relentless.specify.md",
|
|
@@ -573,6 +835,8 @@ For full documentation, see: https://github.com/ArvorCo/Relentless
|
|
|
573
835
|
console.log(chalk.dim("\n4. Convert to JSON and run:"));
|
|
574
836
|
console.log(` ${chalk.cyan("relentless convert relentless/features/NNN-feature/tasks.md --feature NNN-feature")}`);
|
|
575
837
|
console.log(` ${chalk.cyan("relentless run --feature NNN-feature --tui")}`);
|
|
838
|
+
console.log(chalk.dim("\nUpgrade note:"));
|
|
839
|
+
console.log(chalk.dim("If you are upgrading Relentless, re-run /relentless.constitution to refresh prompt.md with the latest instructions."));
|
|
576
840
|
console.log("");
|
|
577
841
|
}
|
|
578
842
|
|
|
@@ -662,11 +926,7 @@ export function listFeatures(projectDir: string): string[] {
|
|
|
662
926
|
return [];
|
|
663
927
|
}
|
|
664
928
|
|
|
665
|
-
|
|
666
|
-
const output = new TextDecoder().decode(entries.stdout);
|
|
667
|
-
|
|
668
|
-
return output
|
|
669
|
-
.split("\n")
|
|
929
|
+
return readdirSync(featuresDir)
|
|
670
930
|
.map((s) => s.trim())
|
|
671
931
|
.filter((s) => s && s !== ".gitkeep");
|
|
672
932
|
}
|
package/src/prd/parser.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Parses PRD markdown files and converts them to prd.json format
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { PRDSchema, type PRD, type UserStory } from "./types";
|
|
7
|
+
import { PRDSchema, type PRD, type UserStory, type ExecutionHistory, type EscalationAttempt } from "./types";
|
|
8
|
+
import type { Mode, HarnessName } from "../config/schema";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Check if a criterion line is valid (not a file path, divider, etc.)
|
|
@@ -59,6 +60,26 @@ export function parsePRDMarkdown(content: string): Partial<PRD> {
|
|
|
59
60
|
continue;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
// Parse routing preference line
|
|
64
|
+
if (trimmed.match(/^(\*\*Routing Preference\*\*|Routing Preference):/i)) {
|
|
65
|
+
const raw = trimmed.replace(/^(\*\*Routing Preference\*\*|Routing Preference):/i, "").trim();
|
|
66
|
+
const lower = raw.toLowerCase();
|
|
67
|
+
const modeMatch = lower.match(/\b(free|cheap|good|genius)\b/);
|
|
68
|
+
const allowFreeMatch = lower.match(/allow\s+free:\s*(yes|no)/);
|
|
69
|
+
const harnessMatch = lower.match(/\b(claude|amp|opencode|codex|droid|gemini)\b/);
|
|
70
|
+
const modelMatch = raw.match(/\/([^\s]+)/);
|
|
71
|
+
|
|
72
|
+
prd.routingPreference = {
|
|
73
|
+
raw,
|
|
74
|
+
type: lower.includes("auto") ? "auto" : harnessMatch ? "harness" : undefined,
|
|
75
|
+
mode: modeMatch ? (modeMatch[1] as Mode) : undefined,
|
|
76
|
+
allowFree: allowFreeMatch ? allowFreeMatch[1] === "yes" : undefined,
|
|
77
|
+
harness: harnessMatch ? (harnessMatch[1] as HarnessName) : undefined,
|
|
78
|
+
model: modelMatch ? modelMatch[1].replace(/[,)]$/, "") : undefined,
|
|
79
|
+
};
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
// Parse section headers (## Section)
|
|
63
84
|
if (trimmed.startsWith("## ")) {
|
|
64
85
|
currentSection = trimmed.replace("## ", "").toLowerCase();
|
|
@@ -258,6 +279,10 @@ export function createPRD(parsed: Partial<PRD>, featureName?: string): PRD {
|
|
|
258
279
|
})),
|
|
259
280
|
};
|
|
260
281
|
|
|
282
|
+
if (parsed.routingPreference) {
|
|
283
|
+
prd.routingPreference = parsed.routingPreference;
|
|
284
|
+
}
|
|
285
|
+
|
|
261
286
|
// Validate
|
|
262
287
|
return PRDSchema.parse(prd);
|
|
263
288
|
}
|
|
@@ -419,3 +444,69 @@ export async function prioritizeStory(
|
|
|
419
444
|
previousPriority,
|
|
420
445
|
};
|
|
421
446
|
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Result of updating story execution history
|
|
450
|
+
*/
|
|
451
|
+
export interface UpdateExecutionResult {
|
|
452
|
+
success: boolean;
|
|
453
|
+
error?: string;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Update a story's execution history after it completes
|
|
458
|
+
*
|
|
459
|
+
* @param prdPath - Path to the prd.json file
|
|
460
|
+
* @param storyId - The ID of the completed story
|
|
461
|
+
* @param executionData - Execution data to save
|
|
462
|
+
* @returns Result indicating success or failure
|
|
463
|
+
*/
|
|
464
|
+
export async function updateStoryExecution(
|
|
465
|
+
prdPath: string,
|
|
466
|
+
storyId: string,
|
|
467
|
+
executionData: {
|
|
468
|
+
attempts: number;
|
|
469
|
+
escalations: EscalationAttempt[];
|
|
470
|
+
actualCost: number;
|
|
471
|
+
actualHarness: HarnessName;
|
|
472
|
+
actualModel: string;
|
|
473
|
+
inputTokens?: number;
|
|
474
|
+
outputTokens?: number;
|
|
475
|
+
}
|
|
476
|
+
): Promise<UpdateExecutionResult> {
|
|
477
|
+
// Load current PRD
|
|
478
|
+
const prd = await loadPRD(prdPath);
|
|
479
|
+
|
|
480
|
+
// Find the story
|
|
481
|
+
const storyIndex = prd.userStories.findIndex((s) => s.id === storyId);
|
|
482
|
+
if (storyIndex === -1) {
|
|
483
|
+
return {
|
|
484
|
+
success: false,
|
|
485
|
+
error: `Story ${storyId} not found in PRD`,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Build execution history
|
|
490
|
+
const executionHistory: ExecutionHistory = {
|
|
491
|
+
attempts: executionData.attempts,
|
|
492
|
+
escalations: executionData.escalations,
|
|
493
|
+
actualCost: executionData.actualCost,
|
|
494
|
+
actualHarness: executionData.actualHarness,
|
|
495
|
+
actualModel: executionData.actualModel,
|
|
496
|
+
inputTokens: executionData.inputTokens,
|
|
497
|
+
outputTokens: executionData.outputTokens,
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// Update the story with execution history
|
|
501
|
+
prd.userStories[storyIndex] = {
|
|
502
|
+
...prd.userStories[storyIndex],
|
|
503
|
+
execution: executionHistory,
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// Save the updated PRD
|
|
507
|
+
await savePRD(prd, prdPath);
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
success: true,
|
|
511
|
+
};
|
|
512
|
+
}
|
package/src/prd/types.ts
CHANGED
|
@@ -5,6 +5,128 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { ModeSchema, ComplexitySchema, HarnessNameSchema } from "../config/schema";
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Routing Metadata Schemas (US-026)
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Routing metadata schema for user stories.
|
|
16
|
+
*
|
|
17
|
+
* Contains classification and routing decisions made during the
|
|
18
|
+
* `/relentless.tasks` phase after complexity classification.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* {
|
|
22
|
+
* complexity: "medium",
|
|
23
|
+
* harness: "claude",
|
|
24
|
+
* model: "sonnet-4.5",
|
|
25
|
+
* mode: "good",
|
|
26
|
+
* estimatedCost: 0.15,
|
|
27
|
+
* classificationReasoning: "Keywords suggest medium complexity"
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
export const RoutingMetadataSchema = z.object({
|
|
31
|
+
/** Task complexity level determined by the classifier */
|
|
32
|
+
complexity: ComplexitySchema,
|
|
33
|
+
/** Selected harness for executing this story */
|
|
34
|
+
harness: HarnessNameSchema,
|
|
35
|
+
/** Selected model within the harness */
|
|
36
|
+
model: z.string(),
|
|
37
|
+
/** Mode used for routing decision (free, cheap, good, genius) */
|
|
38
|
+
mode: ModeSchema,
|
|
39
|
+
/** Estimated cost in USD based on token estimation */
|
|
40
|
+
estimatedCost: z.number().nonnegative(),
|
|
41
|
+
/** Human-readable explanation of classification reasoning (optional) */
|
|
42
|
+
classificationReasoning: z.string().optional(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type RoutingMetadata = z.infer<typeof RoutingMetadataSchema>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Escalation attempt result type.
|
|
49
|
+
*
|
|
50
|
+
* Tracks the result of each execution attempt.
|
|
51
|
+
*/
|
|
52
|
+
export const EscalationResultSchema = z.enum(["success", "failure", "rate_limited"]);
|
|
53
|
+
|
|
54
|
+
export type EscalationResult = z.infer<typeof EscalationResultSchema>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Escalation attempt schema.
|
|
58
|
+
*
|
|
59
|
+
* Records a single execution attempt with its outcome.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* {
|
|
63
|
+
* attempt: 1,
|
|
64
|
+
* harness: "claude",
|
|
65
|
+
* model: "haiku-4.5",
|
|
66
|
+
* result: "failure",
|
|
67
|
+
* error: "Task too complex",
|
|
68
|
+
* cost: 0.02,
|
|
69
|
+
* duration: 45000
|
|
70
|
+
* }
|
|
71
|
+
*/
|
|
72
|
+
export const EscalationAttemptSchema = z.object({
|
|
73
|
+
/** Attempt number (1-indexed) */
|
|
74
|
+
attempt: z.number().int().min(1),
|
|
75
|
+
/** Harness used for this attempt */
|
|
76
|
+
harness: HarnessNameSchema,
|
|
77
|
+
/** Model used for this attempt */
|
|
78
|
+
model: z.string(),
|
|
79
|
+
/** Result of the attempt */
|
|
80
|
+
result: EscalationResultSchema,
|
|
81
|
+
/** Error message if the attempt failed (optional) */
|
|
82
|
+
error: z.string().optional(),
|
|
83
|
+
/** Actual cost for this attempt in USD */
|
|
84
|
+
cost: z.number().nonnegative(),
|
|
85
|
+
/** Duration of the attempt in milliseconds */
|
|
86
|
+
duration: z.number().nonnegative(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export type EscalationAttempt = z.infer<typeof EscalationAttemptSchema>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Execution history schema for user stories.
|
|
93
|
+
*
|
|
94
|
+
* Contains execution details after the story has been executed,
|
|
95
|
+
* including escalation attempts and actual costs.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* {
|
|
99
|
+
* attempts: 2,
|
|
100
|
+
* escalations: [...],
|
|
101
|
+
* actualCost: 0.17,
|
|
102
|
+
* actualHarness: "claude",
|
|
103
|
+
* actualModel: "sonnet-4.5",
|
|
104
|
+
* inputTokens: 5000,
|
|
105
|
+
* outputTokens: 3000
|
|
106
|
+
* }
|
|
107
|
+
*/
|
|
108
|
+
export const ExecutionHistorySchema = z.object({
|
|
109
|
+
/** Total number of attempts made */
|
|
110
|
+
attempts: z.number().int().min(1),
|
|
111
|
+
/** Array of escalation attempts with details */
|
|
112
|
+
escalations: z.array(EscalationAttemptSchema),
|
|
113
|
+
/** Total actual cost across all attempts in USD */
|
|
114
|
+
actualCost: z.number().nonnegative(),
|
|
115
|
+
/** Final harness that successfully completed the story */
|
|
116
|
+
actualHarness: HarnessNameSchema,
|
|
117
|
+
/** Final model that successfully completed the story */
|
|
118
|
+
actualModel: z.string(),
|
|
119
|
+
/** Total input tokens used across all attempts (optional) */
|
|
120
|
+
inputTokens: z.number().nonnegative().optional(),
|
|
121
|
+
/** Total output tokens used across all attempts (optional) */
|
|
122
|
+
outputTokens: z.number().nonnegative().optional(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export type ExecutionHistory = z.infer<typeof ExecutionHistorySchema>;
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// User Story Schema
|
|
129
|
+
// ============================================================================
|
|
8
130
|
|
|
9
131
|
/**
|
|
10
132
|
* User story schema
|
|
@@ -22,6 +144,10 @@ export const UserStorySchema = z.object({
|
|
|
22
144
|
phase: z.string().optional(), // Phase marker (e.g., "Setup", "Foundation", "Stories", "Polish")
|
|
23
145
|
research: z.boolean().optional(), // Requires research phase before implementation
|
|
24
146
|
skipped: z.boolean().optional(), // Whether the story was skipped by user command
|
|
147
|
+
// Routing metadata - populated during task planning phase (US-026)
|
|
148
|
+
routing: RoutingMetadataSchema.optional(),
|
|
149
|
+
// Execution history - populated after story execution (US-026)
|
|
150
|
+
execution: ExecutionHistorySchema.optional(),
|
|
25
151
|
});
|
|
26
152
|
|
|
27
153
|
export type UserStory = z.infer<typeof UserStorySchema>;
|
|
@@ -33,6 +159,16 @@ export const PRDSchema = z.object({
|
|
|
33
159
|
project: z.string(),
|
|
34
160
|
branchName: z.string(),
|
|
35
161
|
description: z.string(),
|
|
162
|
+
routingPreference: z
|
|
163
|
+
.object({
|
|
164
|
+
type: z.enum(["auto", "harness"]).optional(),
|
|
165
|
+
mode: ModeSchema.optional(),
|
|
166
|
+
allowFree: z.boolean().optional(),
|
|
167
|
+
harness: HarnessNameSchema.optional(),
|
|
168
|
+
model: z.string().optional(),
|
|
169
|
+
raw: z.string().optional(),
|
|
170
|
+
})
|
|
171
|
+
.optional(),
|
|
36
172
|
userStories: z.array(UserStorySchema),
|
|
37
173
|
});
|
|
38
174
|
|