@fractary/faber-cli 1.5.26 → 1.5.28

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.
@@ -10,7 +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 } from '@fractary/faber';
13
+ import { getRunDir, getPlanPath, getActiveRunIdPath } from '@fractary/faber';
14
14
  import fs from 'fs/promises';
15
15
  import path from 'path';
16
16
  /**
@@ -324,7 +324,7 @@ async function planSingleIssue(issue, config, repoClient, anthropicClient, optio
324
324
  const { organization, project } = getRepoInfoFromConfig(config);
325
325
  const branch = `feature/${issue.number}`;
326
326
  const worktree = `~/.claude-worktrees/${organization}-${project}-${issue.number}`;
327
- // Generate plan via Anthropic API
327
+ // Generate deterministic plan from resolved workflow
328
328
  if (outputFormat === 'text') {
329
329
  console.log(chalk.gray(' → Generating plan...'));
330
330
  process.stdout.write(''); // Force flush
@@ -335,7 +335,7 @@ async function planSingleIssue(issue, config, repoClient, anthropicClient, optio
335
335
  issueDescription: issue.description,
336
336
  issueNumber: issue.number,
337
337
  });
338
- const planId = plan.plan_id;
338
+ const planId = plan.id;
339
339
  // Create branch without checking it out (so it won't conflict with worktree creation)
340
340
  if (!options.noBranch) {
341
341
  if (outputFormat === 'text') {
@@ -387,6 +387,16 @@ async function planSingleIssue(issue, config, repoClient, anthropicClient, optio
387
387
  throw error;
388
388
  }
389
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
+ }
390
400
  }
391
401
  // Write plan to worktree
392
402
  if (!options.noWorktree) {
@@ -402,33 +412,33 @@ async function planSingleIssue(issue, config, repoClient, anthropicClient, optio
402
412
  }
403
413
  // Generate detailed comment for GitHub issue
404
414
  const planSummary = generatePlanComment(plan, issue.workflow, worktreePath, planId, issue.number);
405
- // Update GitHub issue with plan_id
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)
406
419
  if (outputFormat === 'text') {
407
420
  console.log(chalk.gray(` → Updating GitHub issue...`));
408
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)
409
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
+ }
410
434
  await repoClient.updateIssue({
411
435
  id: issue.number.toString(),
412
- comment: planSummary,
413
436
  addLabel: 'faber:planned',
414
437
  });
415
438
  }
416
439
  catch (error) {
417
- // If label doesn't exist, just add comment without label
418
- if (error instanceof Error &&
419
- (error.message.includes('not found') ||
420
- error.message.includes('faber:planned') ||
421
- error.message.includes('--add-label'))) {
422
- if (outputFormat === 'text') {
423
- console.log(chalk.yellow(` ⚠️ Label 'faber:planned' not found, adding comment only`));
424
- }
425
- await repoClient.updateIssue({
426
- id: issue.number.toString(),
427
- comment: planSummary,
428
- });
429
- }
430
- else {
431
- throw error;
440
+ if (outputFormat === 'text') {
441
+ console.log(chalk.yellow(` ⚠️ Could not add 'faber:planned' label: ${error instanceof Error ? error.message : 'Unknown error'}`));
432
442
  }
433
443
  }
434
444
  if (outputFormat === 'text') {
@@ -448,37 +458,41 @@ function generatePlanComment(plan, workflow, worktreePath, planId, issueNumber)
448
458
  let comment = `🤖 **Workflow Plan Created**\n\n`;
449
459
  comment += `**Plan ID:** \`${planId}\`\n`;
450
460
  comment += `**Workflow:** \`${workflow}\`\n`;
451
- // Add workflow inheritance info if available
452
- if (plan.workflow_config?.inherits_from) {
453
- comment += `**Inherits from:** \`${plan.workflow_config.inherits_from}\`\n`;
461
+ // Add workflow inheritance chain if available
462
+ if (plan.workflow?.inheritance_chain?.length > 1) {
463
+ comment += `**Inheritance:** ${plan.workflow.inheritance_chain.join(' → ')}\n`;
454
464
  }
465
+ comment += `**Autonomy:** \`${plan.autonomy || 'guarded'}\`\n`;
455
466
  comment += `\n---\n\n`;
456
- // Add plan summary by phase
457
- if (plan.phases && Array.isArray(plan.phases)) {
467
+ // Add plan summary by phase (workflow.phases is an object keyed by phase name)
468
+ if (plan.workflow?.phases) {
458
469
  comment += `### Workflow Phases\n\n`;
459
- plan.phases.forEach((phase, index) => {
460
- comment += `**${index + 1}. ${phase.name || phase.phase}**\n\n`;
461
- // Show phase description if available
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`;
462
478
  if (phase.description) {
463
479
  comment += `*${phase.description}*\n\n`;
464
480
  }
465
- // Show steps/tasks
466
- if (phase.steps && Array.isArray(phase.steps)) {
467
- phase.steps.forEach((step) => {
468
- const action = step.action || step.name || step.description || step;
469
- comment += ` - **${action}**`;
470
- if (step.details) {
471
- comment += `: ${step.details}`;
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}`;
472
492
  }
473
493
  comment += `\n`;
474
494
  });
475
495
  }
476
- else if (phase.tasks && Array.isArray(phase.tasks)) {
477
- phase.tasks.forEach((task) => {
478
- const taskDesc = task.description || task.name || task;
479
- comment += ` - ${taskDesc}\n`;
480
- });
481
- }
482
496
  comment += `\n`;
483
497
  });
484
498
  }
@@ -1,8 +1,15 @@
1
1
  /**
2
- * Anthropic API Client
2
+ * Plan Builder (formerly Anthropic Client)
3
3
  *
4
- * Generates workflow plans via Claude API
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
- plan_id: string;
37
+ id: string;
38
+ created: string;
15
39
  created_by: string;
16
40
  cli_version: string;
17
- created_at: string;
18
- issue: {
19
- source: string;
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
- url: string;
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
- * Anthropic API Client
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 via Claude API
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;;;;GAIG;AASH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAK5D,UAAU,iBAAiB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE;QACL,MAAM,EAAE,MAAM,CAAC;QACf,EAAE,EAAE,MAAM,CAAC;QACX,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,GAAG,EAAE,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,UAAU,CAAM;gBAEZ,MAAM,EAAE,iBAAiB;IAiBrC;;OAEG;YACW,cAAc;IAiB5B;;OAEG;IACH,OAAO,CAAC,YAAY;IAepB;;OAEG;IACG,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC;IA8DnE;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAqD/B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAgB/B;;OAEG;YACW,eAAe;CAqB9B"}
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 API Client
2
+ * Plan Builder (formerly Anthropic Client)
3
3
  *
4
- * Generates workflow plans via Claude API
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 { validateJsonSize } from '../utils/validation.js';
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
- * Anthropic API Client
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; // Already loaded
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 via Claude API
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
- // Generate plan ID
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
- ...planJson,
104
- plan_id: planId,
87
+ id: planId,
88
+ created: now.toISOString(),
105
89
  created_by: 'cli',
106
90
  cli_version: '1.3.2',
107
- created_at: new Date().toISOString(),
108
- issue: {
109
- source: 'github',
110
- id: input.issueNumber.toString(),
111
- url: `https://github.com/${organization}/${project}/issues/${input.issueNumber}`,
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: fractary-faber-{work-id}-{YYYYMMDD}-{HHMMSS}
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;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAUtD"}
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"}
@@ -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: fractary-faber-{work-id}-{YYYYMMDD}-{HHMMSS}
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
- const planIdPattern = /^fractary-faber-\d+-\d{8}-\d{6}$/;
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: fractary-faber-{work-id}-{YYYYMMDD}-{HHMMSS}`);
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.26",
3
+ "version": "1.5.28",
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.5.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",
@@ -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 Claude plugin",
5
+ "description": "JSON Schema for FABER workflow plans generated by CLI or faber-planner agent",
6
6
  "type": "object",
7
- "required": ["plan_id", "created_by", "created_at", "workflow", "phases"],
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
- "plan_id": {
15
+ "id": {
16
16
  "type": "string",
17
- "pattern": "^fractary-faber-[0-9]+-[0-9]{8}-[0-9]{6}$",
18
- "description": "Unique plan identifier (format: fractary-faber-{work-id}-{YYYYMMDD}-{HHMMSS})",
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", "claude", "api"],
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
- "created_at": {
31
+ "created": {
33
32
  "type": "string",
34
33
  "format": "date-time",
35
34
  "description": "ISO 8601 timestamp when plan was created"
36
35
  },
37
- "issue": {
36
+ "metadata": {
38
37
  "type": "object",
39
- "required": ["source", "id"],
40
- "description": "Issue metadata - links plan to originating work item",
38
+ "description": "Structured metadata for analytics and partitioning",
41
39
  "properties": {
42
- "source": {
40
+ "org": {
43
41
  "type": "string",
44
- "enum": ["github", "jira", "linear", "manual"],
45
- "description": "Issue tracking system"
42
+ "description": "GitHub organization"
46
43
  },
47
- "id": {
44
+ "project": {
48
45
  "type": "string",
49
- "description": "Issue identifier (e.g., issue number)"
46
+ "description": "Project/repository name"
50
47
  },
51
- "url": {
48
+ "subproject": {
52
49
  "type": "string",
53
- "format": "uri",
54
- "description": "Full URL to issue"
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
- "branch": {
59
- "type": "string",
60
- "description": "Git branch name for this workflow (e.g., feature/258)"
61
- },
62
- "worktree": {
63
- "type": "string",
64
- "description": "Git worktree path (e.g., ~/.claude-worktrees/fractary-myproject-258)"
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": "string",
68
- "description": "Workflow type identifier (e.g., etl, bugfix, core)"
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
- "overall_complexity": {
119
+ "autonomy": {
71
120
  "type": "string",
72
- "enum": ["low", "medium", "high"],
73
- "description": "Overall estimated complexity"
121
+ "enum": ["supervised", "assisted", "autonomous", "guarded"],
122
+ "description": "Autonomy level for workflow execution"
74
123
  },
75
- "estimated_phases": {
76
- "type": "integer",
77
- "minimum": 1,
78
- "description": "Estimated number of phases to complete"
124
+ "phases_to_run": {
125
+ "type": ["array", "null"],
126
+ "items": { "type": "string" },
127
+ "description": "Phase filter (null = run all enabled phases)"
79
128
  },
80
- "special_considerations": {
81
- "type": "array",
82
- "items": {
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
- "phases": {
133
+ "additional_instructions": {
134
+ "type": ["string", "null"],
135
+ "description": "Additional instructions for the executor"
136
+ },
137
+ "items": {
88
138
  "type": "array",
89
- "description": "Ordered list of workflow phases",
139
+ "description": "Plan items (one per target/work-id)",
90
140
  "items": {
91
- "$ref": "#/definitions/phase"
141
+ "$ref": "#/definitions/plan_item"
92
142
  }
93
143
  },
94
- "metadata": {
144
+ "execution": {
95
145
  "type": "object",
96
- "description": "Additional metadata (flexible)",
146
+ "description": "Execution metadata",
97
147
  "properties": {
98
- "org": {
148
+ "mode": {
99
149
  "type": "string",
100
- "description": "GitHub organization"
150
+ "enum": ["sequential", "parallel"],
151
+ "description": "Execution mode for multiple items"
101
152
  },
102
- "project": {
103
- "type": "string",
104
- "description": "Project/repository name"
153
+ "max_concurrent": {
154
+ "type": "integer",
155
+ "minimum": 1,
156
+ "description": "Max concurrent executions (if parallel mode)"
105
157
  },
106
- "subproject": {
158
+ "status": {
107
159
  "type": "string",
108
- "description": "Subproject identifier (optional)"
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
- "phase": {
179
+ "resolved_phase": {
120
180
  "type": "object",
121
- "required": ["phase", "description", "steps"],
181
+ "description": "A resolved workflow phase with merged steps",
122
182
  "properties": {
123
- "phase": {
124
- "type": "string",
125
- "description": "Phase identifier (e.g., frame, architect, build, evaluate, release)"
183
+ "enabled": {
184
+ "type": "boolean",
185
+ "description": "Whether this phase is enabled"
126
186
  },
127
187
  "description": {
128
188
  "type": "string",
129
- "description": "What this phase accomplishes"
189
+ "description": "Phase description"
130
190
  },
131
191
  "steps": {
132
192
  "type": "array",
133
- "description": "Specific steps for this phase",
193
+ "description": "Resolved steps for this phase",
134
194
  "items": {
135
- "$ref": "#/definitions/step"
195
+ "$ref": "#/definitions/workflow_step"
136
196
  }
137
197
  },
138
- "success_criteria": {
198
+ "pre_steps": {
139
199
  "type": "array",
140
- "description": "Criteria to verify phase completion",
141
- "items": {
142
- "type": "string"
143
- }
200
+ "items": { "$ref": "#/definitions/workflow_step" }
144
201
  },
145
- "complexity": {
146
- "type": "string",
147
- "enum": ["low", "medium", "high"],
148
- "description": "Phase complexity estimate"
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
- "step": {
218
+ "workflow_step": {
153
219
  "type": "object",
154
- "required": ["action"],
220
+ "required": ["id", "prompt"],
221
+ "description": "A resolved workflow step with executable prompt",
155
222
  "properties": {
156
- "action": {
223
+ "id": {
157
224
  "type": "string",
158
- "description": "Specific action to take"
225
+ "description": "Step identifier (e.g., build-engineer, evaluate-deploy-apply-test)"
159
226
  },
160
- "details": {
227
+ "name": {
161
228
  "type": "string",
162
- "description": "Additional context or requirements"
229
+ "description": "Human-readable step name"
163
230
  },
164
- "command": {
165
- "type": ["string", "null"],
166
- "description": "Shell command to execute (optional)"
231
+ "description": {
232
+ "type": "string",
233
+ "description": "Step description"
167
234
  },
168
235
  "prompt": {
169
236
  "type": "string",
170
- "description": "LLM prompt for this step (optional)"
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
- "artifacts": {
243
+ "guards": {
173
244
  "type": "object",
174
- "description": "Expected artifacts from this step"
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
- "examples": [
180
- {
181
- "plan_id": "fractary-faber-258-20260106-143022",
182
- "created_by": "cli",
183
- "cli_version": "3.4.0",
184
- "created_at": "2026-01-06T14:30:22Z",
185
- "issue": {
186
- "source": "github",
187
- "id": "258",
188
- "url": "https://github.com/fractary/myproject/issues/258"
189
- },
190
- "branch": "feature/258",
191
- "worktree": "~/.claude-worktrees/fractary-myproject-258",
192
- "workflow": "etl",
193
- "overall_complexity": "medium",
194
- "estimated_phases": 5,
195
- "phases": [
196
- {
197
- "phase": "frame",
198
- "description": "Understand requirements and plan approach",
199
- "steps": [
200
- {
201
- "action": "Read issue description and extract requirements",
202
- "details": "Identify data source, target format, and validation rules"
203
- }
204
- ],
205
- "success_criteria": [
206
- "Requirements documented",
207
- "Data source identified"
208
- ],
209
- "complexity": "low"
210
- },
211
- {
212
- "phase": "architect",
213
- "description": "Design ETL pipeline architecture",
214
- "steps": [
215
- {
216
- "action": "Design data transformation logic",
217
- "details": "Map source fields to target schema"
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
- "success_criteria": [
221
- "Architecture documented",
222
- "Data flow mapped"
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
  }