@fractary/faber-cli 1.5.25 → 1.5.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/README.md +0 -1
- package/dist/commands/plan/index.d.ts.map +1 -1
- package/dist/commands/plan/index.js +59 -47
- package/dist/lib/anthropic-client.d.ts +68 -22
- package/dist/lib/anthropic-client.d.ts.map +1 -1
- package/dist/lib/anthropic-client.js +82 -127
- package/dist/utils/validation.d.ts +10 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +20 -3
- package/package.json +2 -2
- package/schemas/plan.schema.json +231 -138
package/README.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/plan/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/plan/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsDpC;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAqB3C"}
|
|
@@ -10,6 +10,7 @@ import { RepoClient } from '../../lib/repo-client.js';
|
|
|
10
10
|
import { ConfigManager } from '../../lib/config.js';
|
|
11
11
|
import { prompt } from '../../utils/prompt.js';
|
|
12
12
|
import { validateWorkIds, validateLabels, validateWorkflowName, validatePlanId, } from '../../utils/validation.js';
|
|
13
|
+
import { getRunDir, getPlanPath, getActiveRunIdPath } from '@fractary/faber';
|
|
13
14
|
import fs from 'fs/promises';
|
|
14
15
|
import path from 'path';
|
|
15
16
|
/**
|
|
@@ -323,7 +324,7 @@ async function planSingleIssue(issue, config, repoClient, anthropicClient, optio
|
|
|
323
324
|
const { organization, project } = getRepoInfoFromConfig(config);
|
|
324
325
|
const branch = `feature/${issue.number}`;
|
|
325
326
|
const worktree = `~/.claude-worktrees/${organization}-${project}-${issue.number}`;
|
|
326
|
-
// Generate plan
|
|
327
|
+
// Generate deterministic plan from resolved workflow
|
|
327
328
|
if (outputFormat === 'text') {
|
|
328
329
|
console.log(chalk.gray(' → Generating plan...'));
|
|
329
330
|
process.stdout.write(''); // Force flush
|
|
@@ -334,7 +335,7 @@ async function planSingleIssue(issue, config, repoClient, anthropicClient, optio
|
|
|
334
335
|
issueDescription: issue.description,
|
|
335
336
|
issueNumber: issue.number,
|
|
336
337
|
});
|
|
337
|
-
const planId = plan.
|
|
338
|
+
const planId = plan.id;
|
|
338
339
|
// Create branch without checking it out (so it won't conflict with worktree creation)
|
|
339
340
|
if (!options.noBranch) {
|
|
340
341
|
if (outputFormat === 'text') {
|
|
@@ -386,17 +387,24 @@ async function planSingleIssue(issue, config, repoClient, anthropicClient, optio
|
|
|
386
387
|
throw error;
|
|
387
388
|
}
|
|
388
389
|
}
|
|
390
|
+
// Clean up worktree-local state that may have been copied from source branch.
|
|
391
|
+
// .active-run-id tracks the active run in a specific worktree and should not
|
|
392
|
+
// carry over to new worktrees (causes false conflict detection in workflow-run).
|
|
393
|
+
try {
|
|
394
|
+
const activeRunIdInWorktree = getActiveRunIdPath(worktreePath);
|
|
395
|
+
await fs.unlink(activeRunIdInWorktree).catch(() => { });
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// Non-fatal: if cleanup fails, workflow-run will handle conflict detection
|
|
399
|
+
}
|
|
389
400
|
}
|
|
390
401
|
// Write plan to worktree
|
|
391
402
|
if (!options.noWorktree) {
|
|
392
403
|
// Validate plan ID format (prevent path traversal via malicious plan IDs)
|
|
393
404
|
validatePlanId(planId);
|
|
394
|
-
const
|
|
395
|
-
await fs.mkdir(
|
|
396
|
-
|
|
397
|
-
const planPath = path.join(planDir, `${planId}.json`);
|
|
398
|
-
// Note: path.join automatically normalizes and prevents basic traversal,
|
|
399
|
-
// but we validate the plan ID format as an additional layer of defense
|
|
405
|
+
const runDir = getRunDir(planId, worktreePath);
|
|
406
|
+
await fs.mkdir(runDir, { recursive: true });
|
|
407
|
+
const planPath = getPlanPath(planId, worktreePath);
|
|
400
408
|
await fs.writeFile(planPath, JSON.stringify(plan, null, 2));
|
|
401
409
|
if (outputFormat === 'text') {
|
|
402
410
|
console.log(chalk.gray(` → Plan written to ${planPath}`));
|
|
@@ -404,33 +412,33 @@ async function planSingleIssue(issue, config, repoClient, anthropicClient, optio
|
|
|
404
412
|
}
|
|
405
413
|
// Generate detailed comment for GitHub issue
|
|
406
414
|
const planSummary = generatePlanComment(plan, issue.workflow, worktreePath, planId, issue.number);
|
|
407
|
-
// Update GitHub issue
|
|
415
|
+
// Update GitHub issue: post comment, then try to add label separately
|
|
416
|
+
// (posting comment and adding label in a single updateIssue call caused
|
|
417
|
+
// duplicate comments when the label didn't exist, because the comment was
|
|
418
|
+
// already created before addLabels failed, and the retry re-posted it)
|
|
408
419
|
if (outputFormat === 'text') {
|
|
409
420
|
console.log(chalk.gray(` → Updating GitHub issue...`));
|
|
410
421
|
}
|
|
422
|
+
// Post comment first (essential operation)
|
|
423
|
+
await repoClient.updateIssue({
|
|
424
|
+
id: issue.number.toString(),
|
|
425
|
+
comment: planSummary,
|
|
426
|
+
});
|
|
427
|
+
// Try to ensure faber:planned label exists, create if needed (non-fatal)
|
|
411
428
|
try {
|
|
429
|
+
const { labelExists, createLabel } = await import('../../utils/labels.js');
|
|
430
|
+
const exists = await labelExists('faber:planned');
|
|
431
|
+
if (!exists) {
|
|
432
|
+
await createLabel({ name: 'faber:planned', description: 'FABER workflow planned', color: '0e8a16' });
|
|
433
|
+
}
|
|
412
434
|
await repoClient.updateIssue({
|
|
413
435
|
id: issue.number.toString(),
|
|
414
|
-
comment: planSummary,
|
|
415
436
|
addLabel: 'faber:planned',
|
|
416
437
|
});
|
|
417
438
|
}
|
|
418
439
|
catch (error) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
(error.message.includes('not found') ||
|
|
422
|
-
error.message.includes('faber:planned') ||
|
|
423
|
-
error.message.includes('--add-label'))) {
|
|
424
|
-
if (outputFormat === 'text') {
|
|
425
|
-
console.log(chalk.yellow(` ⚠️ Label 'faber:planned' not found, adding comment only`));
|
|
426
|
-
}
|
|
427
|
-
await repoClient.updateIssue({
|
|
428
|
-
id: issue.number.toString(),
|
|
429
|
-
comment: planSummary,
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
throw error;
|
|
440
|
+
if (outputFormat === 'text') {
|
|
441
|
+
console.log(chalk.yellow(` ⚠️ Could not add 'faber:planned' label: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
434
442
|
}
|
|
435
443
|
}
|
|
436
444
|
if (outputFormat === 'text') {
|
|
@@ -450,43 +458,47 @@ function generatePlanComment(plan, workflow, worktreePath, planId, issueNumber)
|
|
|
450
458
|
let comment = `🤖 **Workflow Plan Created**\n\n`;
|
|
451
459
|
comment += `**Plan ID:** \`${planId}\`\n`;
|
|
452
460
|
comment += `**Workflow:** \`${workflow}\`\n`;
|
|
453
|
-
// Add workflow inheritance
|
|
454
|
-
if (plan.
|
|
455
|
-
comment += `**
|
|
461
|
+
// Add workflow inheritance chain if available
|
|
462
|
+
if (plan.workflow?.inheritance_chain?.length > 1) {
|
|
463
|
+
comment += `**Inheritance:** ${plan.workflow.inheritance_chain.join(' → ')}\n`;
|
|
456
464
|
}
|
|
465
|
+
comment += `**Autonomy:** \`${plan.autonomy || 'guarded'}\`\n`;
|
|
457
466
|
comment += `\n---\n\n`;
|
|
458
|
-
// Add plan summary by phase
|
|
459
|
-
if (plan.phases
|
|
467
|
+
// Add plan summary by phase (workflow.phases is an object keyed by phase name)
|
|
468
|
+
if (plan.workflow?.phases) {
|
|
460
469
|
comment += `### Workflow Phases\n\n`;
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
470
|
+
const phaseOrder = ['frame', 'architect', 'build', 'evaluate', 'release'];
|
|
471
|
+
phaseOrder.forEach((phaseName, index) => {
|
|
472
|
+
const phase = plan.workflow.phases[phaseName];
|
|
473
|
+
if (!phase)
|
|
474
|
+
return;
|
|
475
|
+
const enabled = phase.enabled !== false;
|
|
476
|
+
const statusIcon = enabled ? '✅' : '⏭️';
|
|
477
|
+
comment += `**${index + 1}. ${phaseName}** ${statusIcon}${!enabled ? ' *(skipped)*' : ''}\n\n`;
|
|
464
478
|
if (phase.description) {
|
|
465
479
|
comment += `*${phase.description}*\n\n`;
|
|
466
480
|
}
|
|
467
|
-
// Show steps
|
|
468
|
-
|
|
469
|
-
phase.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
481
|
+
// Show all step arrays: pre_steps, steps, post_steps
|
|
482
|
+
const allSteps = [
|
|
483
|
+
...(phase.pre_steps || []),
|
|
484
|
+
...(phase.steps || []),
|
|
485
|
+
...(phase.post_steps || []),
|
|
486
|
+
];
|
|
487
|
+
if (allSteps.length > 0) {
|
|
488
|
+
allSteps.forEach((step) => {
|
|
489
|
+
comment += ` - **${step.id}**`;
|
|
490
|
+
if (step.name) {
|
|
491
|
+
comment += ` — ${step.name}`;
|
|
474
492
|
}
|
|
475
493
|
comment += `\n`;
|
|
476
494
|
});
|
|
477
495
|
}
|
|
478
|
-
else if (phase.tasks && Array.isArray(phase.tasks)) {
|
|
479
|
-
phase.tasks.forEach((task) => {
|
|
480
|
-
const taskDesc = task.description || task.name || task;
|
|
481
|
-
comment += ` - ${taskDesc}\n`;
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
496
|
comment += `\n`;
|
|
485
497
|
});
|
|
486
498
|
}
|
|
487
499
|
comment += `---\n\n`;
|
|
488
500
|
comment += `### Plan Location\n\n`;
|
|
489
|
-
const planPath = `${worktreePath}/.fractary/
|
|
501
|
+
const planPath = `${worktreePath}/.fractary/faber/runs/${planId}/plan.json`;
|
|
490
502
|
comment += `| File | Path |\n`;
|
|
491
503
|
comment += `|------|------|\n`;
|
|
492
504
|
comment += `| Plan | \`${planPath}\` |\n\n`;
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Anthropic
|
|
2
|
+
* Plan Builder (formerly Anthropic Client)
|
|
3
3
|
*
|
|
4
|
-
* Generates workflow plans
|
|
4
|
+
* Generates deterministic workflow plans from resolved workflow configurations.
|
|
5
|
+
* The plan structure is built programmatically from the resolved workflow —
|
|
6
|
+
* no LLM call is needed because the plan is a direct representation of the
|
|
7
|
+
* workflow definition with issue/branch metadata attached.
|
|
8
|
+
*
|
|
9
|
+
* This aligns the CLI plan format with the faber-planner agent format,
|
|
10
|
+
* ensuring workflow-run can consume plans from either source.
|
|
5
11
|
*/
|
|
12
|
+
import { type ResolvedWorkflow } from '@fractary/faber';
|
|
6
13
|
import type { LoadedFaberConfig } from '../types/config.js';
|
|
7
14
|
interface GeneratePlanInput {
|
|
8
15
|
workflow: string;
|
|
@@ -10,27 +17,70 @@ interface GeneratePlanInput {
|
|
|
10
17
|
issueDescription: string;
|
|
11
18
|
issueNumber: number;
|
|
12
19
|
}
|
|
20
|
+
export interface WorkflowPlanItem {
|
|
21
|
+
target: string;
|
|
22
|
+
work_id: string;
|
|
23
|
+
planning_mode: 'work_id';
|
|
24
|
+
issue: {
|
|
25
|
+
number: number;
|
|
26
|
+
title: string;
|
|
27
|
+
url: string;
|
|
28
|
+
};
|
|
29
|
+
target_context: null;
|
|
30
|
+
branch: {
|
|
31
|
+
name: string;
|
|
32
|
+
status: 'new' | 'ready' | 'resume';
|
|
33
|
+
};
|
|
34
|
+
worktree: string | null;
|
|
35
|
+
}
|
|
13
36
|
export interface WorkflowPlan {
|
|
14
|
-
|
|
37
|
+
id: string;
|
|
38
|
+
created: string;
|
|
15
39
|
created_by: string;
|
|
16
40
|
cli_version: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
41
|
+
metadata: {
|
|
42
|
+
org: string;
|
|
43
|
+
project: string;
|
|
44
|
+
subproject: string;
|
|
45
|
+
year: string;
|
|
46
|
+
month: string;
|
|
47
|
+
day: string;
|
|
48
|
+
hour: string;
|
|
49
|
+
minute: string;
|
|
50
|
+
second: string;
|
|
51
|
+
};
|
|
52
|
+
source: {
|
|
53
|
+
input: string;
|
|
54
|
+
work_id: string;
|
|
55
|
+
planning_mode: 'work_id';
|
|
56
|
+
target_match: null;
|
|
57
|
+
expanded_from: null;
|
|
58
|
+
};
|
|
59
|
+
workflow: {
|
|
20
60
|
id: string;
|
|
21
|
-
|
|
61
|
+
resolved_at: string;
|
|
62
|
+
inheritance_chain: string[];
|
|
63
|
+
phases: ResolvedWorkflow['phases'];
|
|
64
|
+
};
|
|
65
|
+
autonomy: string;
|
|
66
|
+
phases_to_run: string[] | null;
|
|
67
|
+
step_to_run: string | null;
|
|
68
|
+
additional_instructions: string | null;
|
|
69
|
+
items: WorkflowPlanItem[];
|
|
70
|
+
execution: {
|
|
71
|
+
mode: 'sequential' | 'parallel';
|
|
72
|
+
max_concurrent: number;
|
|
73
|
+
status: 'pending';
|
|
74
|
+
started_at: null;
|
|
75
|
+
completed_at: null;
|
|
76
|
+
results: never[];
|
|
22
77
|
};
|
|
23
|
-
branch: string;
|
|
24
|
-
worktree: string;
|
|
25
|
-
workflow: string;
|
|
26
|
-
phases: any[];
|
|
27
78
|
[key: string]: any;
|
|
28
79
|
}
|
|
29
80
|
/**
|
|
30
|
-
*
|
|
81
|
+
* Plan Builder (exported as AnthropicClient for backward compatibility)
|
|
31
82
|
*/
|
|
32
83
|
export declare class AnthropicClient {
|
|
33
|
-
private client;
|
|
34
84
|
private config;
|
|
35
85
|
private git;
|
|
36
86
|
private ajv;
|
|
@@ -45,17 +95,13 @@ export declare class AnthropicClient {
|
|
|
45
95
|
*/
|
|
46
96
|
private validatePlan;
|
|
47
97
|
/**
|
|
48
|
-
* Generate workflow plan
|
|
98
|
+
* Generate deterministic workflow plan.
|
|
99
|
+
*
|
|
100
|
+
* Builds the plan structure directly from the resolved workflow configuration,
|
|
101
|
+
* matching the format produced by the faber-planner agent. No LLM call is needed
|
|
102
|
+
* because the plan is a direct representation of the workflow definition.
|
|
49
103
|
*/
|
|
50
104
|
generatePlan(input: GeneratePlanInput): Promise<WorkflowPlan>;
|
|
51
|
-
/**
|
|
52
|
-
* Construct planning prompt for Claude
|
|
53
|
-
*/
|
|
54
|
-
private constructPlanningPrompt;
|
|
55
|
-
/**
|
|
56
|
-
* Extract JSON from Claude response
|
|
57
|
-
*/
|
|
58
|
-
private extractJsonFromResponse;
|
|
59
105
|
/**
|
|
60
106
|
* Extract repository organization and project name using SDK Git class
|
|
61
107
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic-client.d.ts","sourceRoot":"","sources":["../../src/lib/anthropic-client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"anthropic-client.d.ts","sourceRoot":"","sources":["../../src/lib/anthropic-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,50 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Anthropic
|
|
2
|
+
* Plan Builder (formerly Anthropic Client)
|
|
3
3
|
*
|
|
4
|
-
* Generates workflow plans
|
|
4
|
+
* Generates deterministic workflow plans from resolved workflow configurations.
|
|
5
|
+
* The plan structure is built programmatically from the resolved workflow —
|
|
6
|
+
* no LLM call is needed because the plan is a direct representation of the
|
|
7
|
+
* workflow definition with issue/branch metadata attached.
|
|
8
|
+
*
|
|
9
|
+
* This aligns the CLI plan format with the faber-planner agent format,
|
|
10
|
+
* ensuring workflow-run can consume plans from either source.
|
|
5
11
|
*/
|
|
6
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
7
12
|
import Ajv from 'ajv';
|
|
8
13
|
import fs from 'fs/promises';
|
|
9
14
|
import path from 'path';
|
|
10
15
|
import { fileURLToPath } from 'url';
|
|
11
16
|
import { Git, WorkflowResolver } from '@fractary/faber';
|
|
12
|
-
import {
|
|
17
|
+
import { slugify } from '../utils/validation.js';
|
|
13
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
19
|
const __dirname = path.dirname(__filename);
|
|
15
20
|
/**
|
|
16
|
-
*
|
|
21
|
+
* Plan Builder (exported as AnthropicClient for backward compatibility)
|
|
17
22
|
*/
|
|
18
23
|
export class AnthropicClient {
|
|
19
24
|
constructor(config) {
|
|
20
25
|
this.config = config;
|
|
21
26
|
this.git = new Git();
|
|
22
|
-
// validateFormats: false suppresses warnings about unknown formats (uri, date-time)
|
|
23
|
-
// These formats are used for documentation, not strict validation
|
|
24
27
|
this.ajv = new Ajv({ strict: false, validateFormats: false });
|
|
25
|
-
const apiKey = config.anthropic?.api_key;
|
|
26
|
-
if (!apiKey) {
|
|
27
|
-
throw new Error('Anthropic API key not found. Set ANTHROPIC_API_KEY environment variable.');
|
|
28
|
-
}
|
|
29
|
-
this.client = new Anthropic({
|
|
30
|
-
apiKey,
|
|
31
|
-
});
|
|
32
28
|
}
|
|
33
29
|
/**
|
|
34
30
|
* Load plan JSON schema for validation
|
|
35
31
|
*/
|
|
36
32
|
async loadPlanSchema() {
|
|
37
33
|
if (this.planSchema) {
|
|
38
|
-
return;
|
|
34
|
+
return;
|
|
39
35
|
}
|
|
40
36
|
try {
|
|
41
|
-
// Schema bundled with CLI package (cli/schemas/plan.schema.json)
|
|
42
37
|
const schemaPath = path.resolve(__dirname, '../../schemas/plan.schema.json');
|
|
43
38
|
const schemaContent = await fs.readFile(schemaPath, 'utf8');
|
|
44
39
|
this.planSchema = JSON.parse(schemaContent);
|
|
45
40
|
}
|
|
46
41
|
catch (error) {
|
|
47
|
-
// Schema not found or invalid - log warning but don't fail
|
|
48
42
|
console.warn('Warning: Could not load plan schema for validation:', error instanceof Error ? error.message : 'Unknown error');
|
|
49
43
|
this.planSchema = null;
|
|
50
44
|
}
|
|
@@ -54,7 +48,6 @@ export class AnthropicClient {
|
|
|
54
48
|
*/
|
|
55
49
|
validatePlan(plan) {
|
|
56
50
|
if (!this.planSchema) {
|
|
57
|
-
// Schema not loaded - skip validation
|
|
58
51
|
return;
|
|
59
52
|
}
|
|
60
53
|
const validate = this.ajv.compile(this.planSchema);
|
|
@@ -65,137 +58,99 @@ export class AnthropicClient {
|
|
|
65
58
|
}
|
|
66
59
|
}
|
|
67
60
|
/**
|
|
68
|
-
* Generate workflow plan
|
|
61
|
+
* Generate deterministic workflow plan.
|
|
62
|
+
*
|
|
63
|
+
* Builds the plan structure directly from the resolved workflow configuration,
|
|
64
|
+
* matching the format produced by the faber-planner agent. No LLM call is needed
|
|
65
|
+
* because the plan is a direct representation of the workflow definition.
|
|
69
66
|
*/
|
|
70
67
|
async generatePlan(input) {
|
|
71
|
-
// Load plan schema for validation
|
|
72
68
|
await this.loadPlanSchema();
|
|
73
69
|
// Resolve workflow with inheritance (uses SDK WorkflowResolver)
|
|
74
70
|
const resolver = new WorkflowResolver({ projectRoot: process.cwd() });
|
|
75
71
|
const workflowConfig = await resolver.resolveWorkflow(input.workflow);
|
|
76
|
-
//
|
|
77
|
-
const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z/, '').replace('T', '-');
|
|
78
|
-
const planId = `fractary-faber-${input.issueNumber}-${timestamp}`;
|
|
79
|
-
// Construct prompt for Claude
|
|
80
|
-
const prompt = this.constructPlanningPrompt(input, workflowConfig);
|
|
81
|
-
// Call Claude API
|
|
82
|
-
const response = await this.client.messages.create({
|
|
83
|
-
model: 'claude-sonnet-4-5-20250929',
|
|
84
|
-
max_tokens: 8192,
|
|
85
|
-
messages: [
|
|
86
|
-
{
|
|
87
|
-
role: 'user',
|
|
88
|
-
content: prompt,
|
|
89
|
-
},
|
|
90
|
-
],
|
|
91
|
-
});
|
|
92
|
-
// Extract plan JSON from response
|
|
93
|
-
const content = response.content[0];
|
|
94
|
-
if (content.type !== 'text') {
|
|
95
|
-
throw new Error('Unexpected response type from Claude API');
|
|
96
|
-
}
|
|
97
|
-
// Validate response size (prevent DoS)
|
|
98
|
-
validateJsonSize(content.text, 1024 * 1024); // 1MB limit
|
|
99
|
-
const planJson = this.extractJsonFromResponse(content.text);
|
|
100
|
-
// Add metadata
|
|
72
|
+
// Extract repo info
|
|
101
73
|
const { organization, project } = await this.extractRepoInfo();
|
|
74
|
+
// Generate plan ID matching faber-planner format: {org}-{project}-{subproject}-{timestamp}
|
|
75
|
+
const now = new Date();
|
|
76
|
+
const year = now.getFullYear().toString();
|
|
77
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
78
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
79
|
+
const hour = String(now.getHours()).padStart(2, '0');
|
|
80
|
+
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
81
|
+
const second = String(now.getSeconds()).padStart(2, '0');
|
|
82
|
+
const timestamp = `${year}${month}${day}-${hour}${minute}${second}`;
|
|
83
|
+
const subproject = `issue-${input.issueNumber}`;
|
|
84
|
+
const planId = `${slugify(organization)}-${slugify(project)}-${input.issueNumber}-${timestamp}`;
|
|
85
|
+
// Build the plan deterministically — no LLM call needed
|
|
102
86
|
const plan = {
|
|
103
|
-
|
|
104
|
-
|
|
87
|
+
id: planId,
|
|
88
|
+
created: now.toISOString(),
|
|
105
89
|
created_by: 'cli',
|
|
106
90
|
cli_version: '1.3.2',
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
91
|
+
metadata: {
|
|
92
|
+
org: organization,
|
|
93
|
+
project,
|
|
94
|
+
subproject,
|
|
95
|
+
year,
|
|
96
|
+
month,
|
|
97
|
+
day,
|
|
98
|
+
hour,
|
|
99
|
+
minute,
|
|
100
|
+
second,
|
|
101
|
+
},
|
|
102
|
+
source: {
|
|
103
|
+
input: `--work-id ${input.issueNumber}`,
|
|
104
|
+
work_id: input.issueNumber.toString(),
|
|
105
|
+
planning_mode: 'work_id',
|
|
106
|
+
target_match: null,
|
|
107
|
+
expanded_from: null,
|
|
108
|
+
},
|
|
109
|
+
workflow: {
|
|
110
|
+
id: workflowConfig.id,
|
|
111
|
+
resolved_at: now.toISOString(),
|
|
112
|
+
inheritance_chain: workflowConfig.inheritance_chain,
|
|
113
|
+
phases: workflowConfig.phases,
|
|
114
|
+
},
|
|
115
|
+
autonomy: workflowConfig.autonomy?.level || 'guarded',
|
|
116
|
+
phases_to_run: null,
|
|
117
|
+
step_to_run: null,
|
|
118
|
+
additional_instructions: null,
|
|
119
|
+
items: [{
|
|
120
|
+
target: subproject,
|
|
121
|
+
work_id: input.issueNumber.toString(),
|
|
122
|
+
planning_mode: 'work_id',
|
|
123
|
+
issue: {
|
|
124
|
+
number: input.issueNumber,
|
|
125
|
+
title: input.issueTitle,
|
|
126
|
+
url: `https://github.com/${organization}/${project}/issues/${input.issueNumber}`,
|
|
127
|
+
},
|
|
128
|
+
target_context: null,
|
|
129
|
+
branch: {
|
|
130
|
+
name: `feat/${input.issueNumber}`,
|
|
131
|
+
status: 'new',
|
|
132
|
+
},
|
|
133
|
+
worktree: null,
|
|
134
|
+
}],
|
|
135
|
+
execution: {
|
|
136
|
+
mode: 'sequential',
|
|
137
|
+
max_concurrent: 1,
|
|
138
|
+
status: 'pending',
|
|
139
|
+
started_at: null,
|
|
140
|
+
completed_at: null,
|
|
141
|
+
results: [],
|
|
112
142
|
},
|
|
113
|
-
branch: `feature/${input.issueNumber}`,
|
|
114
|
-
worktree: `~/.claude-worktrees/${organization}-${project}-${input.issueNumber}`,
|
|
115
|
-
workflow: input.workflow,
|
|
116
143
|
};
|
|
117
144
|
// Validate plan against schema
|
|
118
145
|
this.validatePlan(plan);
|
|
119
146
|
return plan;
|
|
120
147
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Construct planning prompt for Claude
|
|
123
|
-
*/
|
|
124
|
-
constructPlanningPrompt(input, workflowConfig) {
|
|
125
|
-
return `You are a workflow planning assistant for the FABER system. Your task is to generate a structured workflow plan based on the provided issue and workflow configuration.
|
|
126
|
-
|
|
127
|
-
**Issue Information:**
|
|
128
|
-
- Number: #${input.issueNumber}
|
|
129
|
-
- Title: ${input.issueTitle}
|
|
130
|
-
- Description: ${input.issueDescription}
|
|
131
|
-
|
|
132
|
-
**Workflow Type:** ${input.workflow}
|
|
133
|
-
|
|
134
|
-
**Workflow Configuration:**
|
|
135
|
-
${JSON.stringify(workflowConfig, null, 2)}
|
|
136
|
-
|
|
137
|
-
**Your Task:**
|
|
138
|
-
Generate a complete workflow plan that includes:
|
|
139
|
-
1. All phases from the workflow configuration
|
|
140
|
-
2. Specific steps for each phase based on the issue requirements
|
|
141
|
-
3. Success criteria for each phase
|
|
142
|
-
4. Estimated complexity
|
|
143
|
-
|
|
144
|
-
**Output Format:**
|
|
145
|
-
Return ONLY a valid JSON object with the following structure:
|
|
146
|
-
|
|
147
|
-
\`\`\`json
|
|
148
|
-
{
|
|
149
|
-
"phases": [
|
|
150
|
-
{
|
|
151
|
-
"phase": "phase_name",
|
|
152
|
-
"description": "What this phase accomplishes",
|
|
153
|
-
"steps": [
|
|
154
|
-
{
|
|
155
|
-
"action": "specific action to take",
|
|
156
|
-
"details": "additional context or requirements"
|
|
157
|
-
}
|
|
158
|
-
],
|
|
159
|
-
"success_criteria": [
|
|
160
|
-
"criterion 1",
|
|
161
|
-
"criterion 2"
|
|
162
|
-
],
|
|
163
|
-
"complexity": "low|medium|high"
|
|
164
|
-
}
|
|
165
|
-
],
|
|
166
|
-
"overall_complexity": "low|medium|high",
|
|
167
|
-
"estimated_phases": 4,
|
|
168
|
-
"special_considerations": [
|
|
169
|
-
"Any special notes or warnings"
|
|
170
|
-
]
|
|
171
|
-
}
|
|
172
|
-
\`\`\`
|
|
173
|
-
|
|
174
|
-
Generate the plan now:`;
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Extract JSON from Claude response
|
|
178
|
-
*/
|
|
179
|
-
extractJsonFromResponse(text) {
|
|
180
|
-
// Try to find JSON in code blocks
|
|
181
|
-
const jsonBlockMatch = text.match(/```json\s*\n([\s\S]*?)\n```/);
|
|
182
|
-
if (jsonBlockMatch) {
|
|
183
|
-
return JSON.parse(jsonBlockMatch[1]);
|
|
184
|
-
}
|
|
185
|
-
// Try to find JSON in the text
|
|
186
|
-
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
187
|
-
if (jsonMatch) {
|
|
188
|
-
return JSON.parse(jsonMatch[0]);
|
|
189
|
-
}
|
|
190
|
-
throw new Error('Could not extract JSON from Claude response');
|
|
191
|
-
}
|
|
192
148
|
/**
|
|
193
149
|
* Extract repository organization and project name using SDK Git class
|
|
194
150
|
*/
|
|
195
151
|
async extractRepoInfo() {
|
|
196
152
|
try {
|
|
197
153
|
const remoteUrl = this.git.exec('remote get-url origin');
|
|
198
|
-
// Parse git@github.com:organization/project.git or https://github.com/organization/project.git
|
|
199
154
|
const match = remoteUrl.match(/[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
200
155
|
if (match) {
|
|
201
156
|
return {
|
|
@@ -89,9 +89,18 @@ export declare function validateSafePath(filePath: string, baseDir?: string): st
|
|
|
89
89
|
* @throws Error if too large
|
|
90
90
|
*/
|
|
91
91
|
export declare function validateJsonSize(jsonString: string, maxSizeBytes?: number): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Slugify a string for use in identifiers (plan IDs, paths, etc.)
|
|
94
|
+
* Converts to lowercase, replaces non-alphanumeric with hyphens, trims hyphens.
|
|
95
|
+
*
|
|
96
|
+
* @param input - String to slugify
|
|
97
|
+
* @returns Slugified string (max 50 chars)
|
|
98
|
+
*/
|
|
99
|
+
export declare function slugify(input: string): string;
|
|
92
100
|
/**
|
|
93
101
|
* Validates plan ID format
|
|
94
|
-
* Plan IDs follow format:
|
|
102
|
+
* Plan IDs follow format: {org}-{project}-{work-id}-{YYYYMMDD}-{HHMMSS}
|
|
103
|
+
* Also accepts legacy format: fractary-faber-{work-id}-{YYYYMMDD}-{HHMMSS}
|
|
95
104
|
*
|
|
96
105
|
* @param planId - Plan ID to validate
|
|
97
106
|
* @returns True if valid
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAY1E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrG;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQ7E;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAWtD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBzD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAYpD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBvD;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAYlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAoC3E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,GAAE,MAAoB,GAAG,OAAO,CAYhG;AAED
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAY1E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrG;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQ7E;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAWtD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBzD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAYpD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBvD;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAYlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAoC3E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,GAAE,MAAoB,GAAG,OAAO,CAYhG;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAYtD"}
|
package/dist/utils/validation.js
CHANGED
|
@@ -189,18 +189,35 @@ export function validateJsonSize(jsonString, maxSizeBytes = 1024 * 1024) {
|
|
|
189
189
|
}
|
|
190
190
|
return true;
|
|
191
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Slugify a string for use in identifiers (plan IDs, paths, etc.)
|
|
194
|
+
* Converts to lowercase, replaces non-alphanumeric with hyphens, trims hyphens.
|
|
195
|
+
*
|
|
196
|
+
* @param input - String to slugify
|
|
197
|
+
* @returns Slugified string (max 50 chars)
|
|
198
|
+
*/
|
|
199
|
+
export function slugify(input) {
|
|
200
|
+
return input
|
|
201
|
+
.toLowerCase()
|
|
202
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
203
|
+
.replace(/^-|-$/g, '')
|
|
204
|
+
.slice(0, 50);
|
|
205
|
+
}
|
|
192
206
|
/**
|
|
193
207
|
* Validates plan ID format
|
|
194
|
-
* Plan IDs follow format:
|
|
208
|
+
* Plan IDs follow format: {org}-{project}-{work-id}-{YYYYMMDD}-{HHMMSS}
|
|
209
|
+
* Also accepts legacy format: fractary-faber-{work-id}-{YYYYMMDD}-{HHMMSS}
|
|
195
210
|
*
|
|
196
211
|
* @param planId - Plan ID to validate
|
|
197
212
|
* @returns True if valid
|
|
198
213
|
* @throws Error if invalid
|
|
199
214
|
*/
|
|
200
215
|
export function validatePlanId(planId) {
|
|
201
|
-
|
|
216
|
+
// Accepts one or more slug segments followed by -{digits}-{8digits}-{6digits}
|
|
217
|
+
// Only [a-z0-9-] allowed, which inherently prevents path traversal
|
|
218
|
+
const planIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*-\d+-\d{8}-\d{6}$/;
|
|
202
219
|
if (!planIdPattern.test(planId)) {
|
|
203
|
-
throw new Error(`Invalid plan ID format: "${planId}". Expected format:
|
|
220
|
+
throw new Error(`Invalid plan ID format: "${planId}". Expected format: {org}-{project}-{work-id}-{YYYYMMDD}-{HHMMSS}`);
|
|
204
221
|
}
|
|
205
222
|
return true;
|
|
206
223
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fractary/faber-cli",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.27",
|
|
4
4
|
"description": "FABER CLI - Command-line interface for FABER development toolkit",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@fractary/core": "^0.
|
|
40
|
+
"@fractary/core": "^0.7.20",
|
|
41
41
|
"@fractary/faber": "^2.4.14",
|
|
42
42
|
"ajv": "^8.12.0",
|
|
43
43
|
"chalk": "^5.0.0",
|
package/schemas/plan.schema.json
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/fractary/faber/main/plugins/faber/config/schemas/plan.schema.json",
|
|
4
4
|
"title": "FABER Plan Schema",
|
|
5
|
-
"description": "JSON Schema for FABER workflow plans generated by CLI or
|
|
5
|
+
"description": "JSON Schema for FABER workflow plans generated by CLI or faber-planner agent",
|
|
6
6
|
"type": "object",
|
|
7
|
-
"required": ["
|
|
7
|
+
"required": ["id", "created_by", "created", "workflow", "items"],
|
|
8
8
|
"additionalProperties": true,
|
|
9
9
|
"properties": {
|
|
10
10
|
"$schema": {
|
|
@@ -12,15 +12,14 @@
|
|
|
12
12
|
"format": "uri",
|
|
13
13
|
"description": "JSON Schema reference for IDE validation"
|
|
14
14
|
},
|
|
15
|
-
"
|
|
15
|
+
"id": {
|
|
16
16
|
"type": "string",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"examples": ["fractary-faber-258-20260106-143022"]
|
|
17
|
+
"description": "Unique plan identifier (format: {org}-{project}-{work-id}-{YYYYMMDD}-{HHMMSS})",
|
|
18
|
+
"examples": ["corthosai-etl-corthion-ai-258-20260106-143022", "fractary-faber-258-20260106-143022"]
|
|
20
19
|
},
|
|
21
20
|
"created_by": {
|
|
22
21
|
"type": "string",
|
|
23
|
-
"enum": ["cli", "
|
|
22
|
+
"enum": ["cli", "faber-planner", "api"],
|
|
24
23
|
"description": "Tool that created the plan"
|
|
25
24
|
},
|
|
26
25
|
"cli_version": {
|
|
@@ -29,202 +28,296 @@
|
|
|
29
28
|
"description": "Version of CLI that created the plan (if created_by: cli)",
|
|
30
29
|
"examples": ["3.4.0"]
|
|
31
30
|
},
|
|
32
|
-
"
|
|
31
|
+
"created": {
|
|
33
32
|
"type": "string",
|
|
34
33
|
"format": "date-time",
|
|
35
34
|
"description": "ISO 8601 timestamp when plan was created"
|
|
36
35
|
},
|
|
37
|
-
"
|
|
36
|
+
"metadata": {
|
|
38
37
|
"type": "object",
|
|
39
|
-
"
|
|
40
|
-
"description": "Issue metadata - links plan to originating work item",
|
|
38
|
+
"description": "Structured metadata for analytics and partitioning",
|
|
41
39
|
"properties": {
|
|
42
|
-
"
|
|
40
|
+
"org": {
|
|
43
41
|
"type": "string",
|
|
44
|
-
"
|
|
45
|
-
"description": "Issue tracking system"
|
|
42
|
+
"description": "GitHub organization"
|
|
46
43
|
},
|
|
47
|
-
"
|
|
44
|
+
"project": {
|
|
48
45
|
"type": "string",
|
|
49
|
-
"description": "
|
|
46
|
+
"description": "Project/repository name"
|
|
50
47
|
},
|
|
51
|
-
"
|
|
48
|
+
"subproject": {
|
|
52
49
|
"type": "string",
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
}
|
|
50
|
+
"description": "Subproject identifier (e.g., issue-258, ipeds-admissions)"
|
|
51
|
+
},
|
|
52
|
+
"year": { "type": "string" },
|
|
53
|
+
"month": { "type": "string" },
|
|
54
|
+
"day": { "type": "string" },
|
|
55
|
+
"hour": { "type": "string" },
|
|
56
|
+
"minute": { "type": "string" },
|
|
57
|
+
"second": { "type": "string" }
|
|
56
58
|
}
|
|
57
59
|
},
|
|
58
|
-
"
|
|
59
|
-
"type": "
|
|
60
|
-
"description": "
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
"source": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"description": "How the plan was sourced",
|
|
63
|
+
"properties": {
|
|
64
|
+
"input": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Original user input"
|
|
67
|
+
},
|
|
68
|
+
"work_id": {
|
|
69
|
+
"type": ["string", "null"],
|
|
70
|
+
"description": "Work item ID (if work_id planning mode)"
|
|
71
|
+
},
|
|
72
|
+
"planning_mode": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"enum": ["work_id", "target"],
|
|
75
|
+
"description": "How the plan was initiated"
|
|
76
|
+
},
|
|
77
|
+
"target_match": {
|
|
78
|
+
"type": ["object", "null"],
|
|
79
|
+
"description": "Target pattern match info (if target planning mode)"
|
|
80
|
+
},
|
|
81
|
+
"expanded_from": {
|
|
82
|
+
"type": ["string", "null"],
|
|
83
|
+
"description": "Parent plan if this was expanded from a multi-target plan"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
65
86
|
},
|
|
66
87
|
"workflow": {
|
|
67
|
-
"type": "
|
|
68
|
-
"
|
|
88
|
+
"type": "object",
|
|
89
|
+
"required": ["id", "inheritance_chain", "phases"],
|
|
90
|
+
"description": "Full resolved workflow with all steps and prompts",
|
|
91
|
+
"properties": {
|
|
92
|
+
"id": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"description": "Workflow identifier (e.g., fractary-faber:default, dataset-create)"
|
|
95
|
+
},
|
|
96
|
+
"resolved_at": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"format": "date-time",
|
|
99
|
+
"description": "When the workflow was resolved"
|
|
100
|
+
},
|
|
101
|
+
"inheritance_chain": {
|
|
102
|
+
"type": "array",
|
|
103
|
+
"items": { "type": "string" },
|
|
104
|
+
"description": "Workflow inheritance chain from child to root ancestor"
|
|
105
|
+
},
|
|
106
|
+
"phases": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"description": "Resolved phases with merged steps from inheritance chain",
|
|
109
|
+
"properties": {
|
|
110
|
+
"frame": { "$ref": "#/definitions/resolved_phase" },
|
|
111
|
+
"architect": { "$ref": "#/definitions/resolved_phase" },
|
|
112
|
+
"build": { "$ref": "#/definitions/resolved_phase" },
|
|
113
|
+
"evaluate": { "$ref": "#/definitions/resolved_phase" },
|
|
114
|
+
"release": { "$ref": "#/definitions/resolved_phase" }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
69
118
|
},
|
|
70
|
-
"
|
|
119
|
+
"autonomy": {
|
|
71
120
|
"type": "string",
|
|
72
|
-
"enum": ["
|
|
73
|
-
"description": "
|
|
121
|
+
"enum": ["supervised", "assisted", "autonomous", "guarded"],
|
|
122
|
+
"description": "Autonomy level for workflow execution"
|
|
74
123
|
},
|
|
75
|
-
"
|
|
76
|
-
"type": "
|
|
77
|
-
"
|
|
78
|
-
"description": "
|
|
124
|
+
"phases_to_run": {
|
|
125
|
+
"type": ["array", "null"],
|
|
126
|
+
"items": { "type": "string" },
|
|
127
|
+
"description": "Phase filter (null = run all enabled phases)"
|
|
79
128
|
},
|
|
80
|
-
"
|
|
81
|
-
"type": "
|
|
82
|
-
"
|
|
83
|
-
"type": "string"
|
|
84
|
-
},
|
|
85
|
-
"description": "Special notes or warnings for this workflow"
|
|
129
|
+
"step_to_run": {
|
|
130
|
+
"type": ["string", "null"],
|
|
131
|
+
"description": "Step filter (null = run all steps)"
|
|
86
132
|
},
|
|
87
|
-
"
|
|
133
|
+
"additional_instructions": {
|
|
134
|
+
"type": ["string", "null"],
|
|
135
|
+
"description": "Additional instructions for the executor"
|
|
136
|
+
},
|
|
137
|
+
"items": {
|
|
88
138
|
"type": "array",
|
|
89
|
-
"description": "
|
|
139
|
+
"description": "Plan items (one per target/work-id)",
|
|
90
140
|
"items": {
|
|
91
|
-
"$ref": "#/definitions/
|
|
141
|
+
"$ref": "#/definitions/plan_item"
|
|
92
142
|
}
|
|
93
143
|
},
|
|
94
|
-
"
|
|
144
|
+
"execution": {
|
|
95
145
|
"type": "object",
|
|
96
|
-
"description": "
|
|
146
|
+
"description": "Execution metadata",
|
|
97
147
|
"properties": {
|
|
98
|
-
"
|
|
148
|
+
"mode": {
|
|
99
149
|
"type": "string",
|
|
100
|
-
"
|
|
150
|
+
"enum": ["sequential", "parallel"],
|
|
151
|
+
"description": "Execution mode for multiple items"
|
|
101
152
|
},
|
|
102
|
-
"
|
|
103
|
-
"type": "
|
|
104
|
-
"
|
|
153
|
+
"max_concurrent": {
|
|
154
|
+
"type": "integer",
|
|
155
|
+
"minimum": 1,
|
|
156
|
+
"description": "Max concurrent executions (if parallel mode)"
|
|
105
157
|
},
|
|
106
|
-
"
|
|
158
|
+
"status": {
|
|
107
159
|
"type": "string",
|
|
108
|
-
"
|
|
160
|
+
"enum": ["pending", "in_progress", "completed", "failed"],
|
|
161
|
+
"description": "Execution status"
|
|
162
|
+
},
|
|
163
|
+
"started_at": {
|
|
164
|
+
"type": ["string", "null"],
|
|
165
|
+
"format": "date-time"
|
|
166
|
+
},
|
|
167
|
+
"completed_at": {
|
|
168
|
+
"type": ["string", "null"],
|
|
169
|
+
"format": "date-time"
|
|
170
|
+
},
|
|
171
|
+
"results": {
|
|
172
|
+
"type": "array",
|
|
173
|
+
"description": "Per-item execution results"
|
|
109
174
|
}
|
|
110
175
|
}
|
|
111
|
-
},
|
|
112
|
-
"autonomy": {
|
|
113
|
-
"type": "string",
|
|
114
|
-
"enum": ["supervised", "assisted", "autonomous", "guarded"],
|
|
115
|
-
"description": "Autonomy level for workflow execution"
|
|
116
176
|
}
|
|
117
177
|
},
|
|
118
178
|
"definitions": {
|
|
119
|
-
"
|
|
179
|
+
"resolved_phase": {
|
|
120
180
|
"type": "object",
|
|
121
|
-
"
|
|
181
|
+
"description": "A resolved workflow phase with merged steps",
|
|
122
182
|
"properties": {
|
|
123
|
-
"
|
|
124
|
-
"type": "
|
|
125
|
-
"description": "
|
|
183
|
+
"enabled": {
|
|
184
|
+
"type": "boolean",
|
|
185
|
+
"description": "Whether this phase is enabled"
|
|
126
186
|
},
|
|
127
187
|
"description": {
|
|
128
188
|
"type": "string",
|
|
129
|
-
"description": "
|
|
189
|
+
"description": "Phase description"
|
|
130
190
|
},
|
|
131
191
|
"steps": {
|
|
132
192
|
"type": "array",
|
|
133
|
-
"description": "
|
|
193
|
+
"description": "Resolved steps for this phase",
|
|
134
194
|
"items": {
|
|
135
|
-
"$ref": "#/definitions/
|
|
195
|
+
"$ref": "#/definitions/workflow_step"
|
|
136
196
|
}
|
|
137
197
|
},
|
|
138
|
-
"
|
|
198
|
+
"pre_steps": {
|
|
139
199
|
"type": "array",
|
|
140
|
-
"
|
|
141
|
-
"items": {
|
|
142
|
-
"type": "string"
|
|
143
|
-
}
|
|
200
|
+
"items": { "$ref": "#/definitions/workflow_step" }
|
|
144
201
|
},
|
|
145
|
-
"
|
|
146
|
-
"type": "
|
|
147
|
-
"
|
|
148
|
-
|
|
202
|
+
"post_steps": {
|
|
203
|
+
"type": "array",
|
|
204
|
+
"items": { "$ref": "#/definitions/workflow_step" }
|
|
205
|
+
},
|
|
206
|
+
"require_approval": {
|
|
207
|
+
"type": "boolean"
|
|
208
|
+
},
|
|
209
|
+
"max_retries": {
|
|
210
|
+
"type": "integer",
|
|
211
|
+
"minimum": 0
|
|
212
|
+
},
|
|
213
|
+
"result_handling": {
|
|
214
|
+
"$ref": "#/definitions/result_handling"
|
|
149
215
|
}
|
|
150
216
|
}
|
|
151
217
|
},
|
|
152
|
-
"
|
|
218
|
+
"workflow_step": {
|
|
153
219
|
"type": "object",
|
|
154
|
-
"required": ["
|
|
220
|
+
"required": ["id", "prompt"],
|
|
221
|
+
"description": "A resolved workflow step with executable prompt",
|
|
155
222
|
"properties": {
|
|
156
|
-
"
|
|
223
|
+
"id": {
|
|
157
224
|
"type": "string",
|
|
158
|
-
"description": "
|
|
225
|
+
"description": "Step identifier (e.g., build-engineer, evaluate-deploy-apply-test)"
|
|
159
226
|
},
|
|
160
|
-
"
|
|
227
|
+
"name": {
|
|
161
228
|
"type": "string",
|
|
162
|
-
"description": "
|
|
229
|
+
"description": "Human-readable step name"
|
|
163
230
|
},
|
|
164
|
-
"
|
|
165
|
-
"type":
|
|
166
|
-
"description": "
|
|
231
|
+
"description": {
|
|
232
|
+
"type": "string",
|
|
233
|
+
"description": "Step description"
|
|
167
234
|
},
|
|
168
235
|
"prompt": {
|
|
169
236
|
"type": "string",
|
|
170
|
-
"description": "
|
|
237
|
+
"description": "Executable slash command for this step (e.g., /fractary-faber-code:engineer --work-id {work_id})"
|
|
238
|
+
},
|
|
239
|
+
"source": {
|
|
240
|
+
"type": "string",
|
|
241
|
+
"description": "Source workflow that defines this step (set during merge resolution)"
|
|
171
242
|
},
|
|
172
|
-
"
|
|
243
|
+
"guards": {
|
|
173
244
|
"type": "object",
|
|
174
|
-
"description": "
|
|
245
|
+
"description": "Guard conditions for step execution",
|
|
246
|
+
"properties": {
|
|
247
|
+
"skip_if": { "type": "string" },
|
|
248
|
+
"require_if": { "type": "string" }
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
"result_handling": {
|
|
252
|
+
"$ref": "#/definitions/result_handling"
|
|
253
|
+
},
|
|
254
|
+
"position": {
|
|
255
|
+
"type": "string",
|
|
256
|
+
"enum": ["pre_step", "step", "post_step"],
|
|
257
|
+
"description": "Position type within the phase"
|
|
175
258
|
}
|
|
176
259
|
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
"
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{
|
|
197
|
-
"
|
|
198
|
-
"description": "
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
260
|
+
},
|
|
261
|
+
"result_handling": {
|
|
262
|
+
"type": "object",
|
|
263
|
+
"description": "Result handling configuration",
|
|
264
|
+
"properties": {
|
|
265
|
+
"on_success": { "type": "string" },
|
|
266
|
+
"on_warning": { "type": "string" },
|
|
267
|
+
"on_failure": { "type": "string" },
|
|
268
|
+
"on_pending_input": { "type": "string" }
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
"plan_item": {
|
|
272
|
+
"type": "object",
|
|
273
|
+
"description": "A single plan item (one per target or work-id)",
|
|
274
|
+
"properties": {
|
|
275
|
+
"target": {
|
|
276
|
+
"type": "string",
|
|
277
|
+
"description": "Target identifier"
|
|
278
|
+
},
|
|
279
|
+
"work_id": {
|
|
280
|
+
"type": ["string", "null"],
|
|
281
|
+
"description": "Work item ID (null if target mode)"
|
|
282
|
+
},
|
|
283
|
+
"planning_mode": {
|
|
284
|
+
"type": "string",
|
|
285
|
+
"enum": ["work_id", "target"]
|
|
286
|
+
},
|
|
287
|
+
"issue": {
|
|
288
|
+
"type": ["object", "null"],
|
|
289
|
+
"description": "Issue metadata (null if target mode)",
|
|
290
|
+
"properties": {
|
|
291
|
+
"number": { "type": "integer" },
|
|
292
|
+
"title": { "type": "string" },
|
|
293
|
+
"url": { "type": "string", "format": "uri" }
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
"target_context": {
|
|
297
|
+
"type": ["object", "null"],
|
|
298
|
+
"description": "Target-specific context (null if work_id mode)"
|
|
299
|
+
},
|
|
300
|
+
"branch": {
|
|
301
|
+
"type": "object",
|
|
302
|
+
"properties": {
|
|
303
|
+
"name": { "type": "string" },
|
|
304
|
+
"status": {
|
|
305
|
+
"type": "string",
|
|
306
|
+
"enum": ["new", "ready", "resume"]
|
|
307
|
+
},
|
|
308
|
+
"resume_from": {
|
|
309
|
+
"type": "object",
|
|
310
|
+
"properties": {
|
|
311
|
+
"phase": { "type": "string" },
|
|
312
|
+
"step": { "type": "string" }
|
|
313
|
+
}
|
|
218
314
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
],
|
|
224
|
-
"complexity": "medium"
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
"worktree": {
|
|
318
|
+
"type": ["string", "null"]
|
|
225
319
|
}
|
|
226
|
-
|
|
227
|
-
"autonomy": "supervised"
|
|
320
|
+
}
|
|
228
321
|
}
|
|
229
|
-
|
|
322
|
+
}
|
|
230
323
|
}
|