Skip to content

Commit 60782b7

Browse files
authored
Merge pull request #66 from jerfowler/issue-60-step-count-validation
feat: add stepCount parameter for plan validation (Issue #60)
2 parents fea1326 + 445760f commit 60782b7

16 files changed

+1686
-84
lines changed

src/tools/report-progress.ts

Lines changed: 138 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22
* Report progress tool - Progress updates without file exposure
33
* Updates progress markers without exposing file operations
44
* Enhanced with context status tracking (Issue #51)
5+
* Enhanced with stepCount metadata usage (Issue #60)
56
*/
67

78
import { ServerConfig } from '../types.js';
89
import { TaskContextManager, ProgressUpdate, ProgressReportResult } from '../core/TaskContextManager.js';
910
import { validateRequiredString, validateRequiredConfig } from '../utils/validation.js';
1011
import { ErrorLogEntry } from '../logging/ErrorLogger.js';
1112
import type { ContextStatus, CapabilityChanges } from '../types/context-types.js';
13+
import { parsePlanCheckboxes } from '../utils/plan-parser.js';
14+
import { PlanMetadata } from '../types/plan-metadata.js';
15+
import * as fs from '../utils/fs-extra-safe.js';
16+
import path from 'path';
1217
import debug from 'debug';
1318

14-
15-
const log = debug('agent-comm:tools:reportprogress');
19+
const log = debug('agent-comm:tools:report-progress');
1620
/**
1721
* Report progress updates without file exposure
1822
*/
@@ -245,31 +249,8 @@ export async function reportProgress(
245249
// Continue processing - don't throw
246250
}
247251

248-
// Log extremely large step numbers but don't block (for resilience)
249-
if (step > 100) {
250-
log('Warning: Extremely large step number detected: %d at index %d', step, index);
251-
// Log unusual condition for analysis but don't block
252-
if (config.errorLogger) {
253-
const errorEntry: ErrorLogEntry = {
254-
timestamp: new Date(),
255-
source: 'validation',
256-
operation: 'report_progress',
257-
agent,
258-
...(taskId && { taskId }),
259-
error: {
260-
message: `Extremely large step number detected: ${step} at index ${index}`,
261-
name: 'ValidationWarning'
262-
},
263-
context: {
264-
tool: 'report_progress',
265-
parameters: { unusualStep: step, typicalMax: 100, updateIndex: index }
266-
},
267-
severity: 'medium' // Not blocking, just unusual
268-
};
269-
await config.errorLogger.logError(errorEntry);
270-
}
271-
// Continue processing - don't throw
272-
}
252+
// Note: Extremely large step number validation moved to after stepCount determination
253+
// to avoid double logging when step is both large and out of range
273254

274255
if (typeof status !== 'string' || !['COMPLETE', 'IN_PROGRESS', 'PENDING', 'BLOCKED'].includes(status)) {
275256
const error = new Error(`Update at index ${index}: status must be one of COMPLETE, IN_PROGRESS, PENDING, BLOCKED`);
@@ -330,7 +311,136 @@ export async function reportProgress(
330311
...(typeof blocker === 'string' && blocker.trim() && { blocker: blocker.trim() })
331312
});
332313
}
333-
314+
315+
// Validate step numbers against stepCount if metadata exists (Issue #60)
316+
try {
317+
const startTime = Date.now();
318+
const taskPath = path.join(config.commDir, agent, taskId ?? 'current-task');
319+
const metadataPath = path.join(taskPath, 'PLAN.metadata.json');
320+
321+
let stepCount: number | undefined;
322+
323+
// Try to read metadata first for performance
324+
if (await fs.pathExists(metadataPath)) {
325+
try {
326+
const metadata = await fs.readJSON(metadataPath) as PlanMetadata;
327+
stepCount = metadata.stepCount;
328+
log('Using cached stepCount from metadata: %d', stepCount);
329+
} catch (error) {
330+
log('Failed to read metadata, falling back to plan parsing: %s', (error as Error).message);
331+
}
332+
}
333+
334+
// Fall back to parsing PLAN.md if no metadata
335+
if (stepCount === undefined) {
336+
const planPath = path.join(taskPath, 'PLAN.md');
337+
if (await fs.pathExists(planPath)) {
338+
const planContent = await fs.readFile(planPath, 'utf8');
339+
const checkboxes = parsePlanCheckboxes(planContent);
340+
stepCount = checkboxes.length;
341+
log('Parsed stepCount from PLAN.md: %d', stepCount);
342+
}
343+
}
344+
345+
const validationTime = Date.now() - startTime;
346+
log('Step validation completed in %dms', validationTime);
347+
348+
if (validationTime > 10) {
349+
log('PERFORMANCE WARNING: Step validation took %dms (>10ms threshold)', validationTime);
350+
}
351+
352+
// Validate all step numbers are within range and check for extremely large steps
353+
if (stepCount !== undefined) {
354+
for (const update of updates) {
355+
if (update.step > stepCount) {
356+
// Step is out of range - this takes priority over "extremely large" warning
357+
const errorMessage = `Step ${update.step} is out of range (max: ${stepCount})`;
358+
359+
if (config.errorLogger) {
360+
const errorEntry: ErrorLogEntry = {
361+
timestamp: new Date(),
362+
source: 'validation',
363+
operation: 'report_progress',
364+
agent,
365+
...(taskId && { taskId }),
366+
error: {
367+
message: errorMessage,
368+
name: 'StepOutOfRangeWarning',
369+
code: 'STEP_OUT_OF_RANGE'
370+
},
371+
context: {
372+
tool: 'report_progress',
373+
parameters: {
374+
invalidStep: update.step,
375+
maxStep: stepCount
376+
}
377+
},
378+
severity: 'medium'
379+
};
380+
await config.errorLogger.logError(errorEntry);
381+
}
382+
383+
// Log warning but continue processing (permissive handling)
384+
log('Warning: %s', errorMessage);
385+
} else if (update.step > 100) {
386+
// Step is within range but extremely large - log separately
387+
log('Warning: Extremely large step number detected: %d', update.step);
388+
if (config.errorLogger) {
389+
const errorEntry: ErrorLogEntry = {
390+
timestamp: new Date(),
391+
source: 'validation',
392+
operation: 'report_progress',
393+
agent,
394+
...(taskId && { taskId }),
395+
error: {
396+
message: `Extremely large step number detected: ${update.step}`,
397+
name: 'ValidationWarning'
398+
},
399+
context: {
400+
tool: 'report_progress',
401+
parameters: { unusualStep: update.step, typicalMax: 100, maxStep: stepCount }
402+
},
403+
severity: 'medium'
404+
};
405+
await config.errorLogger.logError(errorEntry);
406+
}
407+
}
408+
}
409+
} else {
410+
// No stepCount available, fall back to extremely large step validation only
411+
for (const update of updates) {
412+
if (update.step > 100) {
413+
log('Warning: Extremely large step number detected: %d', update.step);
414+
if (config.errorLogger) {
415+
const errorEntry: ErrorLogEntry = {
416+
timestamp: new Date(),
417+
source: 'validation',
418+
operation: 'report_progress',
419+
agent,
420+
...(taskId && { taskId }),
421+
error: {
422+
message: `Extremely large step number detected: ${update.step}`,
423+
name: 'ValidationWarning'
424+
},
425+
context: {
426+
tool: 'report_progress',
427+
parameters: { unusualStep: update.step, typicalMax: 100 }
428+
},
429+
severity: 'medium'
430+
};
431+
await config.errorLogger.logError(errorEntry);
432+
}
433+
}
434+
}
435+
}
436+
} catch (error) {
437+
// Log error but only fail if it's a validation error
438+
if ((error as Error).message.includes('out of range')) {
439+
throw error;
440+
}
441+
log('Non-critical error during step validation: %s', (error as Error).message);
442+
}
443+
334444
// Create connection for the agent with optional taskId
335445
const connection = {
336446
id: `report-progress-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,

src/tools/submit-plan.ts

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Submit plan tool - Plan submission without file exposure
33
* Accepts plan content and handles file creation internally
44
* Enhanced with optional context reporting (Issue #51)
5+
* Enhanced with stepCount parameter for efficient validation (Issue #60)
56
*/
67

78
import { ServerConfig } from '../types.js';
@@ -10,10 +11,13 @@ import { validateRequiredString, validateRequiredConfig } from '../utils/validat
1011
import { AgentCommError } from '../types.js';
1112
import { ErrorLogEntry } from '../logging/ErrorLogger.js';
1213
import type { AgentContextData, ContextEstimate } from '../types/context-types.js';
14+
import { parsePlanCheckboxes, validateStepCount } from '../utils/plan-parser.js';
15+
import { PlanMetadata } from '../types/plan-metadata.js';
16+
import * as fs from '../utils/fs-extra-safe.js';
17+
import path from 'path';
1318
import debug from 'debug';
1419

15-
16-
const log = debug('agent-comm:tools:submitplan');
20+
const log = debug('agent-comm:tools:submit-plan');
1721
interface PlanValidationResult {
1822
valid: boolean;
1923
checkboxCount: number;
@@ -81,14 +85,15 @@ export async function submitPlan(
8185
const content = validateRequiredString(args['content'], 'content');
8286
const agent = validateRequiredString(args['agent'], 'agent');
8387
const taskId = args['taskId'] as string | undefined; // Optional taskId parameter
88+
const stepCount = args['stepCount'] as number | undefined; // Optional stepCount parameter (Issue #60)
8489

8590
// Optional context parameters (Issue #51)
8691
const agentContext = args['agentContext'] as AgentContextData | undefined;
8792
const contextEstimate = args['contextEstimate'] as ContextEstimate | undefined;
8893

8994
// Validate plan format before submission
9095
const validation = validatePlanFormat(content);
91-
96+
9297
if (!validation.valid) {
9398
const errorMessage = [
9499
'Plan format validation failed:',
@@ -140,7 +145,51 @@ export async function submitPlan(
140145

141146
throw new AgentCommError(errorMessage, 'PLAN_FORMAT_INVALID');
142147
}
143-
148+
149+
// Validate stepCount if provided (Issue #60)
150+
if (stepCount !== undefined) {
151+
const startTime = Date.now();
152+
const checkboxes = parsePlanCheckboxes(content);
153+
const actualCount = checkboxes.length;
154+
const validationTime = Date.now() - startTime;
155+
156+
log('Step count validation: expected=%d, actual=%d, time=%dms', stepCount, actualCount, validationTime);
157+
158+
if (validationTime > 10) {
159+
log('PERFORMANCE WARNING: Step validation took %dms (>10ms threshold)', validationTime);
160+
}
161+
162+
if (!validateStepCount(stepCount, actualCount)) {
163+
const errorMessage = `Step count mismatch: expected ${stepCount}, actual ${actualCount}`;
164+
165+
if (config.errorLogger) {
166+
const errorEntry: ErrorLogEntry = {
167+
timestamp: new Date(),
168+
source: 'validation',
169+
operation: 'submit_plan',
170+
agent,
171+
error: {
172+
message: errorMessage,
173+
name: 'StepCountMismatchError',
174+
code: 'STEP_COUNT_MISMATCH'
175+
},
176+
context: {
177+
tool: 'submit-plan',
178+
parameters: {
179+
expectedStepCount: stepCount,
180+
actualStepCount: actualCount,
181+
planContent: content.substring(0, 500)
182+
}
183+
},
184+
severity: 'medium'
185+
};
186+
await config.errorLogger.logError(errorEntry);
187+
}
188+
189+
throw new AgentCommError(errorMessage, 'STEP_COUNT_MISMATCH');
190+
}
191+
}
192+
144193
// Create connection for the agent with optional taskId
145194
const connection = {
146195
id: `submit-plan-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
@@ -178,5 +227,43 @@ export async function submitPlan(
178227
});
179228
}
180229

181-
return await contextManager.submitPlan(content, connection);
230+
const result = await contextManager.submitPlan(content, connection);
231+
232+
// Create PLAN.metadata.json after successful submission (Issue #60)
233+
try {
234+
const startTime = Date.now();
235+
236+
// Calculate actual step count if not provided
237+
const actualStepCount = stepCount ?? parsePlanCheckboxes(content).length;
238+
239+
// Get the task path from context manager or use default
240+
const taskPath = path.join(config.commDir, agent, taskId ?? 'current-task');
241+
242+
// Create metadata object
243+
const metadata: PlanMetadata = {
244+
stepCount: actualStepCount,
245+
agent,
246+
...(taskId && { taskId }),
247+
createdAt: new Date().toISOString(),
248+
checkboxPattern: 'markdown',
249+
version: '2.0.0'
250+
};
251+
252+
// Write metadata file
253+
const metadataPath = path.join(taskPath, 'PLAN.metadata.json');
254+
await fs.ensureDir(taskPath);
255+
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
256+
257+
const metadataTime = Date.now() - startTime;
258+
log('Created PLAN.metadata.json with stepCount=%d in %dms', actualStepCount, metadataTime);
259+
260+
if (metadataTime > 10) {
261+
log('PERFORMANCE WARNING: Metadata creation took %dms (>10ms threshold)', metadataTime);
262+
}
263+
} catch (error) {
264+
// Log error but don't fail the submission - metadata is optimization
265+
log('Failed to create PLAN.metadata.json: %s', (error as Error).message);
266+
}
267+
268+
return result;
182269
}

0 commit comments

Comments
 (0)