orchestr8 2.6.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.blueprint/agents/AGENT_BA_CASS.md +2 -112
  2. package/.blueprint/agents/AGENT_DEVELOPER_CODEY.md +1 -40
  3. package/.blueprint/agents/AGENT_SPECIFICATION_ALEX.md +1 -40
  4. package/.blueprint/agents/AGENT_TESTER_NIGEL.md +3 -51
  5. package/.blueprint/agents/GUARDRAILS.md +42 -0
  6. package/.blueprint/features/feature_compressed-feedback/FEATURE_SPEC.md +136 -0
  7. package/.blueprint/features/feature_compressed-feedback/IMPLEMENTATION_PLAN.md +40 -0
  8. package/.blueprint/features/feature_lazy-business-context/FEATURE_SPEC.md +140 -0
  9. package/.blueprint/features/feature_lazy-business-context/IMPLEMENTATION_PLAN.md +54 -0
  10. package/.blueprint/features/feature_model-native-features/FEATURE_SPEC.md +174 -0
  11. package/.blueprint/features/feature_model-native-features/IMPLEMENTATION_PLAN.md +45 -0
  12. package/.blueprint/features/feature_shared-guardrails/FEATURE_SPEC.md +119 -0
  13. package/.blueprint/features/feature_shared-guardrails/IMPLEMENTATION_PLAN.md +34 -0
  14. package/.blueprint/features/feature_shared-guardrails/story-extract-guardrails.md +60 -0
  15. package/.blueprint/features/feature_shared-guardrails/story-update-init-commands.md +63 -0
  16. package/.blueprint/features/feature_slim-agent-prompts/FEATURE_SPEC.md +145 -0
  17. package/.blueprint/features/feature_slim-agent-prompts/IMPLEMENTATION_PLAN.md +87 -0
  18. package/.blueprint/features/feature_slim-agent-prompts/story-create-runtime-prompt-template.md +59 -0
  19. package/.blueprint/features/feature_slim-agent-prompts/story-create-slim-agent-prompts.md +65 -0
  20. package/.blueprint/features/feature_slim-agent-prompts/story-skill-integration.md +53 -0
  21. package/.blueprint/features/feature_smart-story-routing/FEATURE_SPEC.md +147 -0
  22. package/.blueprint/features/feature_smart-story-routing/IMPLEMENTATION_PLAN.md +73 -0
  23. package/.blueprint/features/feature_template-extraction/FEATURE_SPEC.md +134 -0
  24. package/.blueprint/features/feature_template-extraction/IMPLEMENTATION_PLAN.md +46 -0
  25. package/.blueprint/features/feature_upstream-summaries/FEATURE_SPEC.md +150 -0
  26. package/.blueprint/features/feature_upstream-summaries/IMPLEMENTATION_PLAN.md +70 -0
  27. package/.blueprint/prompts/TEMPLATE.md +65 -0
  28. package/.blueprint/prompts/alex-runtime.md +48 -0
  29. package/.blueprint/prompts/cass-runtime.md +45 -0
  30. package/.blueprint/prompts/codey-implement-runtime.md +50 -0
  31. package/.blueprint/prompts/codey-plan-runtime.md +46 -0
  32. package/.blueprint/prompts/nigel-runtime.md +46 -0
  33. package/.blueprint/templates/STORY_TEMPLATE.md +96 -0
  34. package/.blueprint/templates/TEST_TEMPLATE.md +76 -0
  35. package/README.md +82 -18
  36. package/SKILL.md +180 -80
  37. package/package.json +1 -1
  38. package/src/business-context.js +91 -0
  39. package/src/classifier.js +173 -0
  40. package/src/feedback.js +47 -17
  41. package/src/handoff.js +148 -0
  42. package/src/index.js +51 -1
  43. package/src/tools/index.js +27 -0
  44. package/src/tools/prompts.js +45 -0
  45. package/src/tools/schemas.js +38 -0
  46. package/src/tools/validation.js +83 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Smart Story Routing - Feature Classifier Module
3
+ *
4
+ * Classifies features as "technical" or "user-facing" to determine
5
+ * whether the Cass (story writing) stage should be included in the pipeline.
6
+ */
7
+
8
+ // Technical keywords indicate infrastructure/internal work
9
+ const TECHNICAL_KEYWORDS = [
10
+ 'refactor',
11
+ 'token',
12
+ 'performance',
13
+ 'module',
14
+ 'internal',
15
+ 'infrastructure',
16
+ 'optimization',
17
+ 'extract',
18
+ 'compress',
19
+ 'cache',
20
+ 'schema',
21
+ 'validation',
22
+ 'helper',
23
+ 'utility',
24
+ 'config'
25
+ ];
26
+
27
+ // User-facing keywords indicate customer-visible features
28
+ const USER_FACING_KEYWORDS = [
29
+ 'user',
30
+ 'customer',
31
+ 'ui',
32
+ 'screen',
33
+ 'journey',
34
+ 'flow',
35
+ 'experience',
36
+ 'interface',
37
+ 'form',
38
+ 'button',
39
+ 'login',
40
+ 'signup',
41
+ 'dashboard',
42
+ 'notification',
43
+ 'email'
44
+ ];
45
+
46
+ /**
47
+ * Classify a feature specification as technical or user-facing
48
+ * @param {string} content - The feature specification content
49
+ * @returns {Object} Classification result with type, counts, and reason
50
+ */
51
+ function classifyFeature(content) {
52
+ const lowerContent = (content || '').toLowerCase();
53
+
54
+ let technicalCount = 0;
55
+ let userFacingCount = 0;
56
+ const technicalMatches = [];
57
+ const userFacingMatches = [];
58
+
59
+ // Count technical keyword matches
60
+ for (const keyword of TECHNICAL_KEYWORDS) {
61
+ const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
62
+ const matches = lowerContent.match(regex);
63
+ if (matches) {
64
+ technicalCount += matches.length;
65
+ technicalMatches.push(keyword);
66
+ }
67
+ }
68
+
69
+ // Count user-facing keyword matches
70
+ for (const keyword of USER_FACING_KEYWORDS) {
71
+ const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
72
+ const matches = lowerContent.match(regex);
73
+ if (matches) {
74
+ userFacingCount += matches.length;
75
+ userFacingMatches.push(keyword);
76
+ }
77
+ }
78
+
79
+ // Determine type - tie goes to user-facing (conservative default)
80
+ const type = technicalCount > userFacingCount ? 'technical' : 'user-facing';
81
+
82
+ // Build reason string
83
+ let reason;
84
+ if (technicalCount === 0 && userFacingCount === 0) {
85
+ reason = 'No keywords found, defaulting to user-facing';
86
+ } else if (technicalCount > userFacingCount) {
87
+ reason = `Technical keywords (${technicalMatches.join(', ')}) outweigh user-facing`;
88
+ } else if (userFacingCount > technicalCount) {
89
+ reason = `User-facing keywords (${userFacingMatches.join(', ')}) outweigh technical`;
90
+ } else {
91
+ reason = 'Tie between technical and user-facing, defaulting to user-facing';
92
+ }
93
+
94
+ return {
95
+ type,
96
+ technicalCount,
97
+ userFacingCount,
98
+ reason
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Parse story-related flags from command arguments
104
+ * @param {string[]} args - Array of command arguments
105
+ * @returns {Object} Parsed flags with override value
106
+ */
107
+ function parseStoryFlags(args) {
108
+ const argList = args || [];
109
+
110
+ if (argList.includes('--with-stories')) {
111
+ return { override: 'include' };
112
+ }
113
+
114
+ if (argList.includes('--skip-stories')) {
115
+ return { override: 'skip' };
116
+ }
117
+
118
+ return { override: null };
119
+ }
120
+
121
+ /**
122
+ * Determine whether stories should be included in the pipeline
123
+ * @param {string} featureType - 'technical' or 'user-facing'
124
+ * @param {string|null} override - 'include', 'skip', or null
125
+ * @returns {boolean} Whether to include stories in the pipeline
126
+ */
127
+ function shouldIncludeStories(featureType, override) {
128
+ // Override takes precedence
129
+ if (override === 'include') {
130
+ return true;
131
+ }
132
+ if (override === 'skip') {
133
+ return false;
134
+ }
135
+
136
+ // Default behavior based on classification
137
+ return featureType === 'user-facing';
138
+ }
139
+
140
+ /**
141
+ * Build queue state object with classification data
142
+ * @param {string} slug - Feature slug
143
+ * @param {string} featureType - 'technical' or 'user-facing'
144
+ * @param {boolean} includeStories - Whether stories are included
145
+ * @returns {Object} Queue state object with featureType and skippedCass fields
146
+ */
147
+ function buildClassifiedQueueState(slug, featureType, includeStories) {
148
+ return {
149
+ slug,
150
+ featureType,
151
+ skippedCass: !includeStories
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Log classification result to console
157
+ * @param {Object} result - Classification result from classifyFeature
158
+ */
159
+ function logClassification(result) {
160
+ console.log(`Feature classified as ${result.type}: ${result.reason}`);
161
+ console.log(` Technical indicators: ${result.technicalCount}`);
162
+ console.log(` User-facing indicators: ${result.userFacingCount}`);
163
+ }
164
+
165
+ module.exports = {
166
+ TECHNICAL_KEYWORDS,
167
+ USER_FACING_KEYWORDS,
168
+ classifyFeature,
169
+ parseStoryFlags,
170
+ shouldIncludeStories,
171
+ buildClassifiedQueueState,
172
+ logClassification
173
+ };
package/src/feedback.js CHANGED
@@ -3,6 +3,36 @@ const path = require('path');
3
3
 
4
4
  const CONFIG_FILE = '.claude/feedback-config.json';
5
5
 
6
+ /**
7
+ * Normalizes abbreviated keys to full names.
8
+ * Converts "rec" to "recommendation" while preserving existing full key.
9
+ * @param {Object} feedback - Raw feedback object
10
+ * @returns {Object} - Normalized feedback object
11
+ */
12
+ function normalizeFeedbackKeys(feedback) {
13
+ const normalized = { ...feedback };
14
+ if ('rec' in normalized && !('recommendation' in normalized)) {
15
+ normalized.recommendation = normalized.rec;
16
+ delete normalized.rec;
17
+ }
18
+ return normalized;
19
+ }
20
+
21
+ /**
22
+ * Parses FEEDBACK: JSON from agent output text.
23
+ * @param {string} output - Raw agent output
24
+ * @returns {Object|null} - Parsed feedback or null if not found/invalid
25
+ */
26
+ function parseFeedbackFromOutput(output) {
27
+ const match = output.match(/FEEDBACK:\s*(\{[^}]+\})/);
28
+ if (!match) return null;
29
+ try {
30
+ return JSON.parse(match[1]);
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
6
36
  /**
7
37
  * Returns the default feedback configuration.
8
38
  * Per FEATURE_SPEC.md defaults.
@@ -60,34 +90,32 @@ function writeConfig(config) {
60
90
  /**
61
91
  * Validates a feedback object against the schema.
62
92
  * Per FEATURE_SPEC.md:Rule 1.
93
+ * Accepts both "rec" and "recommendation" keys for recommendation field.
63
94
  * @param {object} feedback - Feedback object to validate
64
95
  * @returns {object} { valid: boolean, errors: string[] }
65
96
  */
66
97
  function validateFeedback(feedback) {
67
98
  const errors = [];
68
99
 
69
- if (!['alex', 'cass', 'nigel'].includes(feedback.about)) {
70
- errors.push('Invalid "about" field');
71
- }
72
-
73
- if (typeof feedback.rating !== 'number' ||
74
- feedback.rating < 1 ||
75
- feedback.rating > 5) {
76
- errors.push('Invalid "rating" field');
77
- }
78
-
79
- if (typeof feedback.confidence !== 'number' ||
80
- feedback.confidence < 0 ||
81
- feedback.confidence > 1) {
82
- errors.push('Invalid "confidence" field');
100
+ // Rating validation
101
+ if (typeof feedback.rating !== 'number' || !Number.isInteger(feedback.rating)) {
102
+ errors.push('rating must be an integer');
103
+ } else if (feedback.rating < 1 || feedback.rating > 5) {
104
+ errors.push('rating must be between 1 and 5');
83
105
  }
84
106
 
107
+ // Issues validation
85
108
  if (!Array.isArray(feedback.issues)) {
86
- errors.push('Invalid "issues" field');
109
+ errors.push('issues must be an array');
110
+ } else if (!feedback.issues.every(i => typeof i === 'string')) {
111
+ errors.push('issues must be an array of strings');
87
112
  }
88
113
 
89
- if (!['proceed', 'pause', 'revise'].includes(feedback.recommendation)) {
90
- errors.push('Invalid "recommendation" field');
114
+ // Recommendation validation - accept both "rec" and "recommendation" keys
115
+ const rec = feedback.recommendation || feedback.rec;
116
+ const validRecs = ['proceed', 'pause', 'revise'];
117
+ if (!validRecs.includes(rec)) {
118
+ errors.push(`recommendation must be one of: ${validRecs.join(', ')}`);
91
119
  }
92
120
 
93
121
  return { valid: errors.length === 0, errors };
@@ -163,6 +191,8 @@ module.exports = {
163
191
  getDefaultConfig,
164
192
  readConfig,
165
193
  writeConfig,
194
+ normalizeFeedbackKeys,
195
+ parseFeedbackFromOutput,
166
196
  validateFeedback,
167
197
  shouldPause,
168
198
  setConfigValue,
package/src/handoff.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Handoff summary helper functions.
3
+ * Parses and validates handoff summary format for agent-to-agent communication.
4
+ */
5
+
6
+ /**
7
+ * Parses a handoff summary and extracts key fields.
8
+ * @param {string} content - The handoff summary markdown content
9
+ * @returns {object} Parsed summary fields
10
+ */
11
+ function parseHandoffSummary(content) {
12
+ return {
13
+ hasHeading: /^## Handoff Summary/m.test(content),
14
+ forField: content.match(/\*\*For:\*\*\s*(.+)/)?.[1]?.trim(),
15
+ featureField: content.match(/\*\*Feature:\*\*\s*(.+)/)?.[1]?.trim(),
16
+ hasKeyDecisions: /### Key Decisions/m.test(content),
17
+ hasFilesCreated: /### Files Created/m.test(content),
18
+ hasOpenQuestions: /### Open Questions/m.test(content),
19
+ hasCriticalContext: /### Critical Context/m.test(content),
20
+ lineCount: content.split('\n').length
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Extracts a named section from the summary.
26
+ * @param {string} content - The handoff summary markdown content
27
+ * @param {string} sectionName - Name of the section (e.g., 'Key Decisions')
28
+ * @returns {string} The section content, or empty string if not found
29
+ */
30
+ function extractSection(content, sectionName) {
31
+ const regex = new RegExp(`### ${sectionName}\\n([\\s\\S]*?)(?=\\n###|$)`);
32
+ const match = content.match(regex);
33
+ return match ? match[1].trim() : '';
34
+ }
35
+
36
+ /**
37
+ * Counts bullet items in a section.
38
+ * @param {string} section - Section content
39
+ * @returns {number} Number of bullet items
40
+ */
41
+ function countBulletItems(section) {
42
+ return section.split('\n').filter(line => /^[-*]\s/.test(line)).length;
43
+ }
44
+
45
+ /**
46
+ * Extracts file paths from a section.
47
+ * @param {string} section - Section content
48
+ * @returns {string[]} Array of file paths
49
+ */
50
+ function extractFilePaths(section) {
51
+ const lines = section.split('\n').filter(line => /^[-*]\s/.test(line));
52
+ return lines.map(line => line.replace(/^[-*]\s+/, '').trim());
53
+ }
54
+
55
+ /**
56
+ * Validates a handoff summary against format rules.
57
+ * @param {string} content - The handoff summary markdown content
58
+ * @returns {object} { valid: boolean, errors: string[] }
59
+ */
60
+ function validateHandoffSummary(content) {
61
+ const errors = [];
62
+ const parsed = parseHandoffSummary(content);
63
+
64
+ if (!parsed.hasHeading) {
65
+ errors.push('Missing ## Handoff Summary heading');
66
+ }
67
+
68
+ if (!parsed.forField) {
69
+ errors.push('Missing **For:** field');
70
+ }
71
+
72
+ if (!parsed.featureField) {
73
+ errors.push('Missing **Feature:** field');
74
+ }
75
+
76
+ if (!parsed.hasKeyDecisions) {
77
+ errors.push('Missing ### Key Decisions section');
78
+ }
79
+
80
+ if (!parsed.hasFilesCreated) {
81
+ errors.push('Missing ### Files Created section');
82
+ }
83
+
84
+ if (!parsed.hasOpenQuestions) {
85
+ errors.push('Missing ### Open Questions section');
86
+ }
87
+
88
+ if (!parsed.hasCriticalContext) {
89
+ errors.push('Missing ### Critical Context section');
90
+ }
91
+
92
+ if (parsed.lineCount >= 30) {
93
+ errors.push(`Summary exceeds 30 lines (found ${parsed.lineCount})`);
94
+ }
95
+
96
+ // Validate Key Decisions bullet count
97
+ const keyDecisions = extractSection(content, 'Key Decisions');
98
+ const bulletCount = countBulletItems(keyDecisions);
99
+ if (bulletCount < 1 || bulletCount > 5) {
100
+ errors.push(`Key Decisions should have 1-5 items (found ${bulletCount})`);
101
+ }
102
+
103
+ return { valid: errors.length === 0, errors };
104
+ }
105
+
106
+ /**
107
+ * Returns the handoff file path for an agent.
108
+ * @param {string} featureDir - Feature directory path
109
+ * @param {string} agent - Agent name (alex, cass, nigel)
110
+ * @returns {string} Full path to handoff file
111
+ */
112
+ function getHandoffPath(featureDir, agent) {
113
+ return `${featureDir}/handoff-${agent.toLowerCase()}.md`;
114
+ }
115
+
116
+ /**
117
+ * Generates a handoff summary template.
118
+ * @param {string} forAgent - Target agent name
119
+ * @param {string} featureSlug - Feature slug
120
+ * @returns {string} Template markdown content
121
+ */
122
+ function getHandoffTemplate(forAgent, featureSlug) {
123
+ return `## Handoff Summary
124
+ **For:** ${forAgent}
125
+ **Feature:** ${featureSlug}
126
+
127
+ ### Key Decisions
128
+ -
129
+
130
+ ### Files Created
131
+ -
132
+
133
+ ### Open Questions
134
+ - None
135
+
136
+ ### Critical Context
137
+ `;
138
+ }
139
+
140
+ module.exports = {
141
+ parseHandoffSummary,
142
+ extractSection,
143
+ countBulletItems,
144
+ extractFilePaths,
145
+ validateHandoffSummary,
146
+ getHandoffPath,
147
+ getHandoffTemplate
148
+ };
package/src/index.js CHANGED
@@ -25,6 +25,32 @@ const {
25
25
  recommendThreshold,
26
26
  displayFeedbackInsights
27
27
  } = require('./insights');
28
+ const {
29
+ parseHandoffSummary,
30
+ extractSection,
31
+ countBulletItems,
32
+ extractFilePaths,
33
+ validateHandoffSummary,
34
+ getHandoffPath,
35
+ getHandoffTemplate
36
+ } = require('./handoff');
37
+ const {
38
+ needsBusinessContext,
39
+ parseIncludeBusinessContextFlag,
40
+ shouldIncludeBusinessContext,
41
+ buildQueueState,
42
+ generateBusinessContextDirective
43
+ } = require('./business-context');
44
+ const {
45
+ classifyFeature,
46
+ parseStoryFlags,
47
+ shouldIncludeStories,
48
+ buildClassifiedQueueState,
49
+ logClassification,
50
+ TECHNICAL_KEYWORDS,
51
+ USER_FACING_KEYWORDS
52
+ } = require('./classifier');
53
+ const tools = require('./tools');
28
54
 
29
55
  module.exports = {
30
56
  init,
@@ -56,5 +82,29 @@ module.exports = {
56
82
  calculateCalibration,
57
83
  correlateIssues,
58
84
  recommendThreshold,
59
- displayFeedbackInsights
85
+ displayFeedbackInsights,
86
+ // Handoff summary exports
87
+ parseHandoffSummary,
88
+ extractSection,
89
+ countBulletItems,
90
+ extractFilePaths,
91
+ validateHandoffSummary,
92
+ getHandoffPath,
93
+ getHandoffTemplate,
94
+ // Business context exports
95
+ needsBusinessContext,
96
+ parseIncludeBusinessContextFlag,
97
+ shouldIncludeBusinessContext,
98
+ buildQueueState,
99
+ generateBusinessContextDirective,
100
+ // Classifier module exports (smart story routing)
101
+ classifyFeature,
102
+ parseStoryFlags,
103
+ shouldIncludeStories,
104
+ buildClassifiedQueueState,
105
+ logClassification,
106
+ TECHNICAL_KEYWORDS,
107
+ USER_FACING_KEYWORDS,
108
+ // Tools module (model native features)
109
+ tools
60
110
  };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Tools Module - Model Native Features
3
+ * Exports tool schemas, validation, and prompt utilities
4
+ */
5
+
6
+ const { FEEDBACK_TOOL_SCHEMA, HANDOFF_TOOL_SCHEMA } = require('./schemas');
7
+ const { validateToolInput, normalizeFeedbackInput } = require('./validation');
8
+ const {
9
+ buildPromptMessages,
10
+ identifyCacheableContent,
11
+ SYSTEM_PROMPT_TEMPLATE,
12
+ USER_PROMPT_TEMPLATE
13
+ } = require('./prompts');
14
+
15
+ module.exports = {
16
+ // Schemas
17
+ FEEDBACK_TOOL_SCHEMA,
18
+ HANDOFF_TOOL_SCHEMA,
19
+ // Validation
20
+ validateToolInput,
21
+ normalizeFeedbackInput,
22
+ // Prompts
23
+ buildPromptMessages,
24
+ identifyCacheableContent,
25
+ SYSTEM_PROMPT_TEMPLATE,
26
+ USER_PROMPT_TEMPLATE
27
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Prompt Structure Utilities
3
+ * Helpers for system/user prompt separation and caching
4
+ */
5
+
6
+ const SYSTEM_PROMPT_TEMPLATE = {
7
+ role: 'system',
8
+ content: '[AGENT_SPEC]\n[GUARDRAILS]\n[TEMPLATES]',
9
+ cache_control: { type: 'ephemeral' }
10
+ };
11
+
12
+ const USER_PROMPT_TEMPLATE = {
13
+ role: 'user',
14
+ content: '[TASK_INSTRUCTIONS]\n[INPUTS]\n[OUTPUTS]'
15
+ };
16
+
17
+ /**
18
+ * Build prompt messages with system and user separation
19
+ * @param {string} staticContent - Content for system prompt (agent specs, guardrails)
20
+ * @param {string} dynamicContent - Content for user prompt (task instructions, inputs)
21
+ * @returns {Array<Object>} Array of message objects
22
+ */
23
+ function buildPromptMessages(staticContent, dynamicContent) {
24
+ return [
25
+ { ...SYSTEM_PROMPT_TEMPLATE, content: staticContent },
26
+ { ...USER_PROMPT_TEMPLATE, content: dynamicContent }
27
+ ];
28
+ }
29
+
30
+ /**
31
+ * Identify if content should be cached (static, reusable content)
32
+ * @param {string} content - Content to analyze
33
+ * @returns {boolean} True if content is cacheable
34
+ */
35
+ function identifyCacheableContent(content) {
36
+ const cacheablePatterns = ['AGENT_', 'GUARDRAIL', 'TEMPLATE', 'SPEC.md'];
37
+ return cacheablePatterns.some(p => content.includes(p));
38
+ }
39
+
40
+ module.exports = {
41
+ SYSTEM_PROMPT_TEMPLATE,
42
+ USER_PROMPT_TEMPLATE,
43
+ buildPromptMessages,
44
+ identifyCacheableContent
45
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Tool Schema Definitions
3
+ * Reusable schema constants for Claude tool use
4
+ */
5
+
6
+ const FEEDBACK_TOOL_SCHEMA = {
7
+ name: 'submit_feedback',
8
+ description: 'Submit quality rating for prior stage',
9
+ input_schema: {
10
+ type: 'object',
11
+ properties: {
12
+ rating: { type: 'number', minimum: 1, maximum: 5 },
13
+ issues: { type: 'array', items: { type: 'string' } },
14
+ recommendation: { enum: ['proceed', 'pause', 'revise'] }
15
+ },
16
+ required: ['rating', 'issues', 'recommendation']
17
+ }
18
+ };
19
+
20
+ const HANDOFF_TOOL_SCHEMA = {
21
+ name: 'submit_handoff',
22
+ description: 'Submit summary for next agent',
23
+ input_schema: {
24
+ type: 'object',
25
+ properties: {
26
+ from_agent: { type: 'string', enum: ['alex', 'cass', 'nigel', 'codey'] },
27
+ to_agent: { type: 'string', enum: ['alex', 'cass', 'nigel', 'codey'] },
28
+ summary: { type: 'string', maxLength: 500 },
29
+ artifacts: { type: 'array', items: { type: 'string' } }
30
+ },
31
+ required: ['from_agent', 'to_agent', 'summary']
32
+ }
33
+ };
34
+
35
+ module.exports = {
36
+ FEEDBACK_TOOL_SCHEMA,
37
+ HANDOFF_TOOL_SCHEMA
38
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Tool Input Validation Utilities
3
+ * Validates inputs against tool schema constraints
4
+ */
5
+
6
+ /**
7
+ * Validate tool input against a schema
8
+ * @param {Object} schema - Tool schema with input_schema
9
+ * @param {Object} input - Input to validate
10
+ * @returns {{ valid: boolean, errors: string[] }}
11
+ */
12
+ function validateToolInput(schema, input) {
13
+ const errors = [];
14
+ const props = schema.input_schema.properties;
15
+ const required = schema.input_schema.required || [];
16
+
17
+ // Check required fields
18
+ for (const field of required) {
19
+ if (!(field in input)) {
20
+ errors.push(`missing required field: ${field}`);
21
+ }
22
+ }
23
+
24
+ // Validate each field
25
+ for (const [key, value] of Object.entries(input)) {
26
+ const propSchema = props[key];
27
+ if (!propSchema) continue;
28
+
29
+ // Type: number with bounds
30
+ if (propSchema.type === 'number') {
31
+ if (typeof value !== 'number') {
32
+ errors.push(`${key} must be number`);
33
+ } else {
34
+ if (propSchema.minimum !== undefined && value < propSchema.minimum) {
35
+ errors.push(`${key} below minimum ${propSchema.minimum}`);
36
+ }
37
+ if (propSchema.maximum !== undefined && value > propSchema.maximum) {
38
+ errors.push(`${key} above maximum ${propSchema.maximum}`);
39
+ }
40
+ }
41
+ }
42
+
43
+ // Type: array
44
+ if (propSchema.type === 'array') {
45
+ if (!Array.isArray(value)) {
46
+ errors.push(`${key} must be array`);
47
+ }
48
+ }
49
+
50
+ // Type: string with maxLength
51
+ if (propSchema.type === 'string') {
52
+ if (typeof value !== 'string') {
53
+ errors.push(`${key} must be string`);
54
+ } else if (propSchema.maxLength && value.length > propSchema.maxLength) {
55
+ errors.push(`${key} exceeds maxLength ${propSchema.maxLength}`);
56
+ }
57
+ }
58
+
59
+ // Enum constraint
60
+ if (propSchema.enum && !propSchema.enum.includes(value)) {
61
+ errors.push(`${key} must be one of: ${propSchema.enum.join(', ')}`);
62
+ }
63
+ }
64
+
65
+ return { valid: errors.length === 0, errors };
66
+ }
67
+
68
+ /**
69
+ * Normalize feedback input to handle "rec" shorthand for "recommendation"
70
+ * @param {Object} input - Raw feedback input
71
+ * @returns {Object} - Normalized input
72
+ */
73
+ function normalizeFeedbackInput(input) {
74
+ if (input.rec && !input.recommendation) {
75
+ return { ...input, recommendation: input.rec };
76
+ }
77
+ return input;
78
+ }
79
+
80
+ module.exports = {
81
+ validateToolInput,
82
+ normalizeFeedbackInput
83
+ };