@cleocode/core 2026.4.11 → 2026.4.12
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/dist/codebase-map/analyzers/architecture.d.ts.map +1 -1
- package/dist/codebase-map/analyzers/architecture.js +0 -1
- package/dist/codebase-map/analyzers/architecture.js.map +1 -1
- package/dist/conduit/local-transport.d.ts +18 -8
- package/dist/conduit/local-transport.d.ts.map +1 -1
- package/dist/conduit/local-transport.js +23 -13
- package/dist/conduit/local-transport.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -1
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +6 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.js +175 -68950
- package/dist/index.js.map +1 -7
- package/dist/init.d.ts +1 -2
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +1 -2
- package/dist/init.js.map +1 -1
- package/dist/internal.d.ts +8 -3
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +13 -6
- package/dist/internal.js.map +1 -1
- package/dist/memory/learnings.d.ts +2 -2
- package/dist/memory/patterns.d.ts +6 -6
- package/dist/output.d.ts +32 -11
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +67 -67
- package/dist/output.js.map +1 -1
- package/dist/paths.js +80 -14
- package/dist/paths.js.map +1 -1
- package/dist/skills/dynamic-skill-generator.d.ts +0 -2
- package/dist/skills/dynamic-skill-generator.d.ts.map +1 -1
- package/dist/skills/dynamic-skill-generator.js.map +1 -1
- package/dist/store/agent-registry-accessor.d.ts +203 -12
- package/dist/store/agent-registry-accessor.d.ts.map +1 -1
- package/dist/store/agent-registry-accessor.js +618 -100
- package/dist/store/agent-registry-accessor.js.map +1 -1
- package/dist/store/api-key-kdf.d.ts +73 -0
- package/dist/store/api-key-kdf.d.ts.map +1 -0
- package/dist/store/api-key-kdf.js +84 -0
- package/dist/store/api-key-kdf.js.map +1 -0
- package/dist/store/cleanup-legacy.js +171 -0
- package/dist/store/cleanup-legacy.js.map +1 -0
- package/dist/store/conduit-sqlite.d.ts +184 -0
- package/dist/store/conduit-sqlite.d.ts.map +1 -0
- package/dist/store/conduit-sqlite.js +570 -0
- package/dist/store/conduit-sqlite.js.map +1 -0
- package/dist/store/global-salt.d.ts +78 -0
- package/dist/store/global-salt.d.ts.map +1 -0
- package/dist/store/global-salt.js +147 -0
- package/dist/store/global-salt.js.map +1 -0
- package/dist/store/migrate-signaldock-to-conduit.d.ts +81 -0
- package/dist/store/migrate-signaldock-to-conduit.d.ts.map +1 -0
- package/dist/store/migrate-signaldock-to-conduit.js +555 -0
- package/dist/store/migrate-signaldock-to-conduit.js.map +1 -0
- package/dist/store/nexus-sqlite.js +28 -3
- package/dist/store/nexus-sqlite.js.map +1 -1
- package/dist/store/signaldock-sqlite.d.ts +122 -19
- package/dist/store/signaldock-sqlite.d.ts.map +1 -1
- package/dist/store/signaldock-sqlite.js +401 -251
- package/dist/store/signaldock-sqlite.js.map +1 -1
- package/dist/store/sqlite-backup.js +122 -4
- package/dist/store/sqlite-backup.js.map +1 -1
- package/dist/system/backup.d.ts +0 -26
- package/dist/system/backup.d.ts.map +1 -1
- package/dist/system/runtime.d.ts +0 -2
- package/dist/system/runtime.d.ts.map +1 -1
- package/dist/system/runtime.js +3 -3
- package/dist/system/runtime.js.map +1 -1
- package/dist/tasks/add.d.ts +1 -1
- package/dist/tasks/add.d.ts.map +1 -1
- package/dist/tasks/add.js +98 -23
- package/dist/tasks/add.js.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +4 -1
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/find.d.ts.map +1 -1
- package/dist/tasks/find.js +4 -1
- package/dist/tasks/find.js.map +1 -1
- package/dist/tasks/labels.d.ts.map +1 -1
- package/dist/tasks/labels.js +4 -1
- package/dist/tasks/labels.js.map +1 -1
- package/dist/tasks/relates.d.ts.map +1 -1
- package/dist/tasks/relates.js +16 -4
- package/dist/tasks/relates.js.map +1 -1
- package/dist/tasks/show.d.ts.map +1 -1
- package/dist/tasks/show.js +4 -1
- package/dist/tasks/show.js.map +1 -1
- package/dist/tasks/update.d.ts.map +1 -1
- package/dist/tasks/update.js +32 -6
- package/dist/tasks/update.js.map +1 -1
- package/dist/validation/engine.d.ts.map +1 -1
- package/dist/validation/engine.js +16 -4
- package/dist/validation/engine.js.map +1 -1
- package/dist/validation/param-utils.d.ts +5 -3
- package/dist/validation/param-utils.d.ts.map +1 -1
- package/dist/validation/param-utils.js +8 -6
- package/dist/validation/param-utils.js.map +1 -1
- package/dist/validation/protocols/_shared.d.ts.map +1 -1
- package/dist/validation/protocols/_shared.js +13 -6
- package/dist/validation/protocols/_shared.js.map +1 -1
- package/package.json +7 -7
- package/src/adapters/__tests__/manager.test.ts +0 -1
- package/src/codebase-map/analyzers/architecture.ts +0 -1
- package/src/conduit/__tests__/local-credential-flow.test.ts +20 -18
- package/src/conduit/__tests__/local-transport.test.ts +14 -12
- package/src/conduit/local-transport.ts +23 -13
- package/src/config.ts +0 -1
- package/src/errors.ts +24 -0
- package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +2 -5
- package/src/init.ts +1 -2
- package/src/internal.ts +49 -2
- package/src/lifecycle/cant/lifecycle-rcasd.cant +133 -0
- package/src/memory/__tests__/engine-compat.test.ts +2 -2
- package/src/memory/__tests__/pipeline-manifest-sqlite.test.ts +4 -4
- package/src/observability/__tests__/index.test.ts +4 -4
- package/src/observability/__tests__/log-filter.test.ts +4 -4
- package/src/output.ts +73 -75
- package/src/sessions/__tests__/session-grade.integration.test.ts +1 -1
- package/src/sessions/__tests__/session-grade.test.ts +2 -2
- package/src/skills/__tests__/dynamic-skill-generator.test.ts +0 -2
- package/src/skills/dynamic-skill-generator.ts +0 -2
- package/src/store/__tests__/agent-registry-accessor.test.ts +807 -0
- package/src/store/__tests__/api-key-kdf.test.ts +113 -0
- package/src/store/__tests__/conduit-sqlite.test.ts +413 -0
- package/src/store/__tests__/global-salt.test.ts +195 -0
- package/src/store/__tests__/migrate-signaldock-to-conduit.test.ts +715 -0
- package/src/store/__tests__/signaldock-sqlite.test.ts +652 -0
- package/src/store/__tests__/sqlite-backup-global.test.ts +307 -3
- package/src/store/__tests__/sqlite-backup.test.ts +5 -1
- package/src/store/__tests__/t310-integration.test.ts +1150 -0
- package/src/store/agent-registry-accessor.ts +847 -140
- package/src/store/api-key-kdf.ts +104 -0
- package/src/store/conduit-sqlite.ts +655 -0
- package/src/store/global-salt.ts +175 -0
- package/src/store/migrate-signaldock-to-conduit.ts +669 -0
- package/src/store/signaldock-sqlite.ts +431 -254
- package/src/store/sqlite-backup.ts +185 -10
- package/src/system/backup.ts +2 -62
- package/src/system/runtime.ts +4 -6
- package/src/tasks/__tests__/error-hints.test.ts +256 -0
- package/src/tasks/add.ts +99 -9
- package/src/tasks/complete.ts +4 -1
- package/src/tasks/find.ts +4 -1
- package/src/tasks/labels.ts +4 -1
- package/src/tasks/relates.ts +16 -4
- package/src/tasks/show.ts +4 -1
- package/src/tasks/update.ts +32 -3
- package/src/validation/__tests__/error-hints.test.ts +97 -0
- package/src/validation/engine.ts +16 -1
- package/src/validation/param-utils.ts +10 -7
- package/src/validation/protocols/_shared.ts +14 -6
- package/src/validation/protocols/cant/architecture-decision.cant +80 -0
- package/src/validation/protocols/cant/artifact-publish.cant +95 -0
- package/src/validation/protocols/cant/consensus.cant +74 -0
- package/src/validation/protocols/cant/contribution.cant +82 -0
- package/src/validation/protocols/cant/decomposition.cant +92 -0
- package/src/validation/protocols/cant/implementation.cant +67 -0
- package/src/validation/protocols/cant/provenance.cant +88 -0
- package/src/validation/protocols/cant/release.cant +96 -0
- package/src/validation/protocols/cant/research.cant +66 -0
- package/src/validation/protocols/cant/specification.cant +67 -0
- package/src/validation/protocols/cant/testing.cant +88 -0
- package/src/validation/protocols/cant/validation.cant +65 -0
- package/src/validation/protocols/protocols-markdown/decomposition.md +0 -4
- package/templates/config.template.json +0 -1
- package/templates/global-config.template.json +0 -1
package/src/tasks/add.ts
CHANGED
|
@@ -39,7 +39,7 @@ import { resolveDefaultPipelineStage, validatePipelineStage } from './pipeline-s
|
|
|
39
39
|
*/
|
|
40
40
|
export interface AddTaskOptions {
|
|
41
41
|
title: string;
|
|
42
|
-
description
|
|
42
|
+
description?: string;
|
|
43
43
|
status?: TaskStatus;
|
|
44
44
|
priority?: TaskPriority;
|
|
45
45
|
type?: TaskType;
|
|
@@ -93,10 +93,16 @@ export function buildDefaultVerification(initializedAt: string): TaskVerificatio
|
|
|
93
93
|
*/
|
|
94
94
|
export function validateTitle(title: string): void {
|
|
95
95
|
if (!title || title.trim().length === 0) {
|
|
96
|
-
throw new CleoError(ExitCode.INVALID_INPUT, 'Task title is required'
|
|
96
|
+
throw new CleoError(ExitCode.INVALID_INPUT, 'Task title is required', {
|
|
97
|
+
fix: 'Provide a title: cleo add "<title>"',
|
|
98
|
+
details: { field: 'title' },
|
|
99
|
+
});
|
|
97
100
|
}
|
|
98
101
|
if (title.length > 200) {
|
|
99
|
-
throw new CleoError(ExitCode.VALIDATION_ERROR, 'Task title must be 200 characters or less'
|
|
102
|
+
throw new CleoError(ExitCode.VALIDATION_ERROR, 'Task title must be 200 characters or less', {
|
|
103
|
+
fix: 'Shorten title to 200 characters or fewer',
|
|
104
|
+
details: { field: 'title', expected: 200, actual: title.length },
|
|
105
|
+
});
|
|
100
106
|
}
|
|
101
107
|
}
|
|
102
108
|
|
|
@@ -109,6 +115,10 @@ export function validateStatus(status: string): asserts status is TaskStatus {
|
|
|
109
115
|
throw new CleoError(
|
|
110
116
|
ExitCode.VALIDATION_ERROR,
|
|
111
117
|
`Invalid status: ${status} (must be ${TASK_STATUSES.join('|')})`,
|
|
118
|
+
{
|
|
119
|
+
fix: `cleo add ... --status <${TASK_STATUSES.join('|')}>`,
|
|
120
|
+
details: { field: 'status', expected: TASK_STATUSES, actual: status },
|
|
121
|
+
},
|
|
112
122
|
);
|
|
113
123
|
}
|
|
114
124
|
}
|
|
@@ -152,6 +162,10 @@ export function normalizePriority(priority: string | number): TaskPriority {
|
|
|
152
162
|
throw new CleoError(
|
|
153
163
|
ExitCode.VALIDATION_ERROR,
|
|
154
164
|
`Invalid numeric priority: ${priority} (must be 1-9)`,
|
|
165
|
+
{
|
|
166
|
+
fix: `Use a numeric priority 1-9 or one of: ${VALID_PRIORITIES.join('|')}`,
|
|
167
|
+
details: { field: 'priority', expected: '1-9', actual: priority },
|
|
168
|
+
},
|
|
155
169
|
);
|
|
156
170
|
}
|
|
157
171
|
return mapped;
|
|
@@ -172,6 +186,10 @@ export function normalizePriority(priority: string | number): TaskPriority {
|
|
|
172
186
|
throw new CleoError(
|
|
173
187
|
ExitCode.VALIDATION_ERROR,
|
|
174
188
|
`Invalid priority: ${priority} (must be ${VALID_PRIORITIES.join('|')} or numeric 1-9)`,
|
|
189
|
+
{
|
|
190
|
+
fix: `cleo add ... --priority <${VALID_PRIORITIES.join('|')}>`,
|
|
191
|
+
details: { field: 'priority', expected: VALID_PRIORITIES, actual: priority },
|
|
192
|
+
},
|
|
175
193
|
);
|
|
176
194
|
}
|
|
177
195
|
|
|
@@ -194,6 +212,10 @@ export function validateTaskType(type: string): asserts type is TaskType {
|
|
|
194
212
|
throw new CleoError(
|
|
195
213
|
ExitCode.VALIDATION_ERROR,
|
|
196
214
|
`Invalid task type: ${type} (must be ${valid.join('|')})`,
|
|
215
|
+
{
|
|
216
|
+
fix: `cleo add ... --type <${valid.join('|')}>`,
|
|
217
|
+
details: { field: 'type', expected: valid, actual: type },
|
|
218
|
+
},
|
|
197
219
|
);
|
|
198
220
|
}
|
|
199
221
|
}
|
|
@@ -208,6 +230,10 @@ export function validateSize(size: string): asserts size is TaskSize {
|
|
|
208
230
|
throw new CleoError(
|
|
209
231
|
ExitCode.VALIDATION_ERROR,
|
|
210
232
|
`Invalid size: ${size} (must be ${valid.join('|')})`,
|
|
233
|
+
{
|
|
234
|
+
fix: `cleo add ... --size <${valid.join('|')}>`,
|
|
235
|
+
details: { field: 'size', expected: valid, actual: size },
|
|
236
|
+
},
|
|
211
237
|
);
|
|
212
238
|
}
|
|
213
239
|
}
|
|
@@ -223,6 +249,10 @@ export function validateLabels(labels: string[]): void {
|
|
|
223
249
|
throw new CleoError(
|
|
224
250
|
ExitCode.VALIDATION_ERROR,
|
|
225
251
|
`Invalid label format: '${trimmed}' (must be lowercase alphanumeric with hyphens/periods)`,
|
|
252
|
+
{
|
|
253
|
+
fix: `Labels must match pattern ^[a-z][a-z0-9.-]*$ (e.g. my-label, v1.0)`,
|
|
254
|
+
details: { field: 'labels', expected: '^[a-z][a-z0-9.-]*$', actual: trimmed },
|
|
255
|
+
},
|
|
226
256
|
);
|
|
227
257
|
}
|
|
228
258
|
}
|
|
@@ -237,6 +267,10 @@ export function validatePhaseFormat(phase: string): void {
|
|
|
237
267
|
throw new CleoError(
|
|
238
268
|
ExitCode.VALIDATION_ERROR,
|
|
239
269
|
`Invalid phase format: ${phase} (must be lowercase alphanumeric with hyphens)`,
|
|
270
|
+
{
|
|
271
|
+
fix: `Phase slugs must match pattern ^[a-z][a-z0-9-]*$ (e.g. dev-phase-1)`,
|
|
272
|
+
details: { field: 'phase', expected: '^[a-z][a-z0-9-]*$', actual: phase },
|
|
273
|
+
},
|
|
240
274
|
);
|
|
241
275
|
}
|
|
242
276
|
}
|
|
@@ -253,10 +287,17 @@ export function validateDepends(depends: string[], tasks: Task[]): void {
|
|
|
253
287
|
throw new CleoError(
|
|
254
288
|
ExitCode.VALIDATION_ERROR,
|
|
255
289
|
`Invalid dependency ID format: '${trimmed}' (must be T### format)`,
|
|
290
|
+
{
|
|
291
|
+
fix: 'Dependency IDs must match T### format (e.g. T123, T4567)',
|
|
292
|
+
details: { field: 'depends', expected: 'T###', actual: trimmed },
|
|
293
|
+
},
|
|
256
294
|
);
|
|
257
295
|
}
|
|
258
296
|
if (!existingIds.has(trimmed)) {
|
|
259
|
-
throw new CleoError(ExitCode.NOT_FOUND, `Dependency task not found: ${trimmed}
|
|
297
|
+
throw new CleoError(ExitCode.NOT_FOUND, `Dependency task not found: ${trimmed}`, {
|
|
298
|
+
fix: `cleo find "${trimmed}"`,
|
|
299
|
+
details: { field: 'depends', actual: trimmed },
|
|
300
|
+
});
|
|
260
301
|
}
|
|
261
302
|
}
|
|
262
303
|
}
|
|
@@ -294,6 +335,10 @@ export function validateParent(
|
|
|
294
335
|
throw new CleoError(
|
|
295
336
|
ExitCode.DEPTH_EXCEEDED,
|
|
296
337
|
`Cannot add child to ${parentId}: max hierarchy depth (${maxDepth}) would be exceeded`,
|
|
338
|
+
{
|
|
339
|
+
fix: 'Reparent this task under a higher-level epic',
|
|
340
|
+
details: { field: 'parentId', expected: `depth < ${maxDepth}`, actual: depth },
|
|
341
|
+
},
|
|
297
342
|
);
|
|
298
343
|
}
|
|
299
344
|
|
|
@@ -436,6 +481,10 @@ export async function addTask(
|
|
|
436
481
|
throw new CleoError(
|
|
437
482
|
ExitCode.VALIDATION_ERROR,
|
|
438
483
|
'Title and description must be different (anti-hallucination rule)',
|
|
484
|
+
{
|
|
485
|
+
fix: 'Provide --desc with a description different from the title',
|
|
486
|
+
details: { field: 'description' },
|
|
487
|
+
},
|
|
439
488
|
);
|
|
440
489
|
}
|
|
441
490
|
|
|
@@ -508,11 +557,18 @@ export async function addTask(
|
|
|
508
557
|
throw new CleoError(
|
|
509
558
|
ExitCode.VALIDATION_ERROR,
|
|
510
559
|
`Invalid dependency ID format: '${trimmed}' (must be T### format)`,
|
|
560
|
+
{
|
|
561
|
+
fix: 'Dependency IDs must match T### format (e.g. T123, T4567)',
|
|
562
|
+
details: { field: 'depends', expected: 'T###', actual: trimmed },
|
|
563
|
+
},
|
|
511
564
|
);
|
|
512
565
|
}
|
|
513
566
|
const exists = await dataAccessor.taskExists(trimmed);
|
|
514
567
|
if (!exists) {
|
|
515
|
-
throw new CleoError(ExitCode.NOT_FOUND, `Dependency task not found: ${trimmed}
|
|
568
|
+
throw new CleoError(ExitCode.NOT_FOUND, `Dependency task not found: ${trimmed}`, {
|
|
569
|
+
fix: `cleo find "${trimmed}"`,
|
|
570
|
+
details: { field: 'depends', actual: trimmed },
|
|
571
|
+
});
|
|
516
572
|
}
|
|
517
573
|
}
|
|
518
574
|
}
|
|
@@ -532,6 +588,10 @@ export async function addTask(
|
|
|
532
588
|
throw new CleoError(
|
|
533
589
|
ExitCode.NOT_FOUND,
|
|
534
590
|
`Phase '${phase}' not found. Valid phases: ${validPhases || 'none'}. Use --add-phase to create new.`,
|
|
591
|
+
{
|
|
592
|
+
fix: `cleo add ... --add-phase to create '${phase}', or use one of: ${validPhases || 'none'}`,
|
|
593
|
+
details: { field: 'phase', expected: Object.keys(phases), actual: phase },
|
|
594
|
+
},
|
|
535
595
|
);
|
|
536
596
|
}
|
|
537
597
|
// Create phase
|
|
@@ -554,12 +614,18 @@ export async function addTask(
|
|
|
554
614
|
// Parent hierarchy validation using targeted queries
|
|
555
615
|
if (parentId) {
|
|
556
616
|
if (!/^T\d{3,}$/.test(parentId)) {
|
|
557
|
-
throw new CleoError(ExitCode.INVALID_INPUT, `Invalid parent ID format: ${parentId}
|
|
617
|
+
throw new CleoError(ExitCode.INVALID_INPUT, `Invalid parent ID format: ${parentId}`, {
|
|
618
|
+
fix: 'Parent IDs must match T### format (e.g. T123)',
|
|
619
|
+
details: { field: 'parentId', expected: 'T###', actual: parentId },
|
|
620
|
+
});
|
|
558
621
|
}
|
|
559
622
|
// Validate parent exists
|
|
560
623
|
const parentTask = await dataAccessor.loadSingleTask(parentId);
|
|
561
624
|
if (!parentTask) {
|
|
562
|
-
throw new CleoError(ExitCode.PARENT_NOT_FOUND, `Parent task ${parentId} not found
|
|
625
|
+
throw new CleoError(ExitCode.PARENT_NOT_FOUND, `Parent task ${parentId} not found`, {
|
|
626
|
+
fix: `Use 'cleo find "${parentId}"' to search or create as standalone task`,
|
|
627
|
+
details: { field: 'parentId', actual: parentId },
|
|
628
|
+
});
|
|
563
629
|
}
|
|
564
630
|
|
|
565
631
|
// Read hierarchy limits from config via policy module
|
|
@@ -573,6 +639,14 @@ export async function addTask(
|
|
|
573
639
|
throw new CleoError(
|
|
574
640
|
ExitCode.DEPTH_EXCEEDED,
|
|
575
641
|
`Maximum nesting depth ${policy.maxDepth} would be exceeded`,
|
|
642
|
+
{
|
|
643
|
+
fix: 'Reparent this task under a higher-level epic',
|
|
644
|
+
details: {
|
|
645
|
+
field: 'parentId',
|
|
646
|
+
expected: `depth < ${policy.maxDepth}`,
|
|
647
|
+
actual: parentDepth + 1,
|
|
648
|
+
},
|
|
649
|
+
},
|
|
576
650
|
);
|
|
577
651
|
}
|
|
578
652
|
|
|
@@ -586,6 +660,14 @@ export async function addTask(
|
|
|
586
660
|
throw new CleoError(
|
|
587
661
|
ExitCode.SIBLING_LIMIT,
|
|
588
662
|
`Parent ${parentId} already has ${counted} children (limit: ${policy.maxSiblings})`,
|
|
663
|
+
{
|
|
664
|
+
fix: 'Create as standalone task or increase hierarchy.maxSiblings in config',
|
|
665
|
+
details: {
|
|
666
|
+
field: 'parentId',
|
|
667
|
+
expected: `<= ${policy.maxSiblings} siblings`,
|
|
668
|
+
actual: counted,
|
|
669
|
+
},
|
|
670
|
+
},
|
|
589
671
|
);
|
|
590
672
|
}
|
|
591
673
|
}
|
|
@@ -596,6 +678,14 @@ export async function addTask(
|
|
|
596
678
|
throw new CleoError(
|
|
597
679
|
ExitCode.SIBLING_LIMIT,
|
|
598
680
|
`Parent ${parentId} already has ${activeCount} active children (maxActiveSiblings=${policy.maxActiveSiblings})`,
|
|
681
|
+
{
|
|
682
|
+
fix: 'Complete or cancel an active sibling before adding a new task here',
|
|
683
|
+
details: {
|
|
684
|
+
field: 'parentId',
|
|
685
|
+
expected: `<= ${policy.maxActiveSiblings} active siblings`,
|
|
686
|
+
actual: activeCount,
|
|
687
|
+
},
|
|
688
|
+
},
|
|
599
689
|
);
|
|
600
690
|
}
|
|
601
691
|
|
|
@@ -676,7 +766,7 @@ export async function addTask(
|
|
|
676
766
|
const previewTask: Task = {
|
|
677
767
|
id: 'T???',
|
|
678
768
|
title: options.title,
|
|
679
|
-
description: options.description,
|
|
769
|
+
description: options.description ?? '',
|
|
680
770
|
status,
|
|
681
771
|
priority,
|
|
682
772
|
type: taskType,
|
|
@@ -767,7 +857,7 @@ export async function addTask(
|
|
|
767
857
|
const task: Task = {
|
|
768
858
|
id: taskId,
|
|
769
859
|
title: options.title,
|
|
770
|
-
description: options.description,
|
|
860
|
+
description: options.description ?? '',
|
|
771
861
|
status,
|
|
772
862
|
priority,
|
|
773
863
|
type: taskType,
|
package/src/tasks/complete.ts
CHANGED
|
@@ -115,7 +115,10 @@ export async function completeTask(
|
|
|
115
115
|
|
|
116
116
|
// Already done
|
|
117
117
|
if (task.status === 'done') {
|
|
118
|
-
throw new CleoError(ExitCode.TASK_COMPLETED, `Task ${options.taskId} is already completed
|
|
118
|
+
throw new CleoError(ExitCode.TASK_COMPLETED, `Task ${options.taskId} is already completed`, {
|
|
119
|
+
fix: `To reopen, run cleo update ${options.taskId} --status active`,
|
|
120
|
+
details: { field: 'status', expected: 'not done', actual: 'done' },
|
|
121
|
+
});
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
// Check if task has incomplete dependencies
|
package/src/tasks/find.ts
CHANGED
|
@@ -100,7 +100,10 @@ export async function findTasks(
|
|
|
100
100
|
accessor?: DataAccessor,
|
|
101
101
|
): Promise<FindTasksResult> {
|
|
102
102
|
if (!options.query && !options.id) {
|
|
103
|
-
throw new CleoError(ExitCode.INVALID_INPUT, 'Search query or --id is required'
|
|
103
|
+
throw new CleoError(ExitCode.INVALID_INPUT, 'Search query or --id is required', {
|
|
104
|
+
fix: 'cleo find "<query>"',
|
|
105
|
+
details: { field: 'query' },
|
|
106
|
+
});
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
const acc = accessor ?? (await getAccessor(cwd));
|
package/src/tasks/labels.ts
CHANGED
|
@@ -45,7 +45,10 @@ export async function showLabelTasks(
|
|
|
45
45
|
const { tasks } = await acc.queryTasks({ label });
|
|
46
46
|
|
|
47
47
|
if (tasks.length === 0) {
|
|
48
|
-
throw new CleoError(ExitCode.NOT_FOUND, `No tasks found with label '${label}'
|
|
48
|
+
throw new CleoError(ExitCode.NOT_FOUND, `No tasks found with label '${label}'`, {
|
|
49
|
+
fix: 'cleo labels list to see available labels',
|
|
50
|
+
details: { field: 'label', actual: label },
|
|
51
|
+
});
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
return {
|
package/src/tasks/relates.ts
CHANGED
|
@@ -20,7 +20,10 @@ export async function suggestRelated(
|
|
|
20
20
|
const { tasks: allTasks } = await acc.queryTasks({});
|
|
21
21
|
const task = allTasks.find((t) => t.id === taskId);
|
|
22
22
|
if (!task) {
|
|
23
|
-
throw new CleoError(ExitCode.NOT_FOUND, `Task ${taskId} not found
|
|
23
|
+
throw new CleoError(ExitCode.NOT_FOUND, `Task ${taskId} not found`, {
|
|
24
|
+
fix: `cleo find "${taskId}"`,
|
|
25
|
+
details: { field: 'taskId', actual: taskId },
|
|
26
|
+
});
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
const suggestions: Array<Pick<TaskRef, 'id' | 'title'> & { score: number; reason: string }> = [];
|
|
@@ -80,12 +83,18 @@ export async function addRelation(
|
|
|
80
83
|
|
|
81
84
|
const fromExists = await acc.taskExists(from);
|
|
82
85
|
if (!fromExists) {
|
|
83
|
-
throw new CleoError(ExitCode.NOT_FOUND, `Task ${from} not found
|
|
86
|
+
throw new CleoError(ExitCode.NOT_FOUND, `Task ${from} not found`, {
|
|
87
|
+
fix: `cleo find "${from}"`,
|
|
88
|
+
details: { field: 'from', actual: from },
|
|
89
|
+
});
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
const toExists = await acc.taskExists(to);
|
|
87
93
|
if (!toExists) {
|
|
88
|
-
throw new CleoError(ExitCode.NOT_FOUND, `Task ${to} not found
|
|
94
|
+
throw new CleoError(ExitCode.NOT_FOUND, `Task ${to} not found`, {
|
|
95
|
+
fix: `cleo find "${to}"`,
|
|
96
|
+
details: { field: 'to', actual: to },
|
|
97
|
+
});
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
// Persist to task_relations table via accessor (T5168 fix)
|
|
@@ -112,7 +121,10 @@ export async function listRelations(
|
|
|
112
121
|
const acc = accessor ?? (await getAccessor(cwd));
|
|
113
122
|
const task = await acc.loadSingleTask(taskId);
|
|
114
123
|
if (!task) {
|
|
115
|
-
throw new CleoError(ExitCode.NOT_FOUND, `Task ${taskId} not found
|
|
124
|
+
throw new CleoError(ExitCode.NOT_FOUND, `Task ${taskId} not found`, {
|
|
125
|
+
fix: `cleo find "${taskId}"`,
|
|
126
|
+
details: { field: 'taskId', actual: taskId },
|
|
127
|
+
});
|
|
116
128
|
}
|
|
117
129
|
|
|
118
130
|
// task.relates is populated from task_relations table by loadSingleTask
|
package/src/tasks/show.ts
CHANGED
|
@@ -35,7 +35,10 @@ export async function showTask(
|
|
|
35
35
|
accessor?: DataAccessor,
|
|
36
36
|
): Promise<TaskDetail> {
|
|
37
37
|
if (!taskId) {
|
|
38
|
-
throw new CleoError(ExitCode.INVALID_INPUT, 'Task ID is required'
|
|
38
|
+
throw new CleoError(ExitCode.INVALID_INPUT, 'Task ID is required', {
|
|
39
|
+
fix: 'cleo show T###',
|
|
40
|
+
details: { field: 'taskId' },
|
|
41
|
+
});
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
const acc = accessor ?? (await getAccessor(cwd));
|
package/src/tasks/update.ts
CHANGED
|
@@ -153,7 +153,14 @@ export async function updateTask(
|
|
|
153
153
|
const { validateStatusTransition } = await import('../validation/validation-rules.js');
|
|
154
154
|
const transitionViolations = validateStatusTransition(task.status, options.status);
|
|
155
155
|
if (transitionViolations.length > 0) {
|
|
156
|
-
throw new CleoError(ExitCode.VALIDATION_ERROR, transitionViolations[0].message
|
|
156
|
+
throw new CleoError(ExitCode.VALIDATION_ERROR, transitionViolations[0].message, {
|
|
157
|
+
fix: `Valid transitions from '${task.status}': see cleo update --help`,
|
|
158
|
+
details: {
|
|
159
|
+
field: transitionViolations[0].field ?? 'status',
|
|
160
|
+
expected: `valid transition from ${task.status}`,
|
|
161
|
+
actual: options.status,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
const oldStatus = task.status;
|
|
@@ -319,12 +326,19 @@ export async function updateTask(
|
|
|
319
326
|
// Validate target parent exists
|
|
320
327
|
const newParent = await acc.loadSingleTask(newParentId);
|
|
321
328
|
if (!newParent) {
|
|
322
|
-
throw new CleoError(ExitCode.PARENT_NOT_FOUND, `Parent task ${newParentId} not found
|
|
329
|
+
throw new CleoError(ExitCode.PARENT_NOT_FOUND, `Parent task ${newParentId} not found`, {
|
|
330
|
+
fix: `Use 'cleo find "${newParentId}"' to search or remove --parent flag`,
|
|
331
|
+
details: { field: 'parentId', actual: newParentId },
|
|
332
|
+
});
|
|
323
333
|
}
|
|
324
334
|
if (newParent.type === 'subtask') {
|
|
325
335
|
throw new CleoError(
|
|
326
336
|
ExitCode.INVALID_PARENT_TYPE,
|
|
327
337
|
`Cannot parent under subtask '${newParentId}'`,
|
|
338
|
+
{
|
|
339
|
+
fix: `Choose an epic or task as parent instead of subtask '${newParentId}'`,
|
|
340
|
+
details: { field: 'parentId', expected: 'epic or task', actual: 'subtask' },
|
|
341
|
+
},
|
|
328
342
|
);
|
|
329
343
|
}
|
|
330
344
|
|
|
@@ -334,6 +348,10 @@ export async function updateTask(
|
|
|
334
348
|
throw new CleoError(
|
|
335
349
|
ExitCode.CIRCULAR_REFERENCE,
|
|
336
350
|
`Moving '${options.taskId}' under '${newParentId}' would create a circular reference`,
|
|
351
|
+
{
|
|
352
|
+
fix: `Choose a parent that is not a descendant of ${options.taskId}`,
|
|
353
|
+
details: { field: 'parentId', actual: newParentId },
|
|
354
|
+
},
|
|
337
355
|
);
|
|
338
356
|
}
|
|
339
357
|
|
|
@@ -346,6 +364,14 @@ export async function updateTask(
|
|
|
346
364
|
throw new CleoError(
|
|
347
365
|
ExitCode.DEPTH_EXCEEDED,
|
|
348
366
|
`Maximum nesting depth ${policy.maxDepth} would be exceeded`,
|
|
367
|
+
{
|
|
368
|
+
fix: 'Choose a parent at a shallower level in the hierarchy',
|
|
369
|
+
details: {
|
|
370
|
+
field: 'parentId',
|
|
371
|
+
expected: `depth < ${policy.maxDepth}`,
|
|
372
|
+
actual: parentDepth + 1,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
349
375
|
);
|
|
350
376
|
}
|
|
351
377
|
|
|
@@ -362,7 +388,10 @@ export async function updateTask(
|
|
|
362
388
|
}
|
|
363
389
|
|
|
364
390
|
if (changes.length === 0) {
|
|
365
|
-
throw new CleoError(ExitCode.NO_CHANGE, 'No changes specified'
|
|
391
|
+
throw new CleoError(ExitCode.NO_CHANGE, 'No changes specified', {
|
|
392
|
+
fix: `Provide at least one field to update (e.g. cleo update ${options.taskId} --status active)`,
|
|
393
|
+
details: { field: 'options' },
|
|
394
|
+
});
|
|
366
395
|
}
|
|
367
396
|
|
|
368
397
|
task.updatedAt = now;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests: CleoError fix hints on validation-layer throws.
|
|
3
|
+
*
|
|
4
|
+
* Each test invokes a validation function with invalid input and asserts that
|
|
5
|
+
* the thrown CleoError carries a non-empty `fix` string and, where applicable,
|
|
6
|
+
* a `details.field` identifying the failing field.
|
|
7
|
+
*
|
|
8
|
+
* @task T341
|
|
9
|
+
* @epic T335
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from 'vitest';
|
|
13
|
+
import { CleoError } from '../../errors.js';
|
|
14
|
+
import { sanitizeFilePath } from '../engine.js';
|
|
15
|
+
import { loadManifestEntryByTaskId, loadManifestEntryFromFile } from '../protocols/_shared.js';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Helper
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
function assertErrorHints(
|
|
22
|
+
fn: () => unknown,
|
|
23
|
+
opts: { fixIncludes?: string; detailsField?: string },
|
|
24
|
+
): void {
|
|
25
|
+
let caught: unknown;
|
|
26
|
+
try {
|
|
27
|
+
fn();
|
|
28
|
+
} catch (err) {
|
|
29
|
+
caught = err;
|
|
30
|
+
}
|
|
31
|
+
expect(caught).toBeInstanceOf(CleoError);
|
|
32
|
+
const e = caught as CleoError;
|
|
33
|
+
expect(e.fix).toBeTruthy();
|
|
34
|
+
if (opts.fixIncludes) {
|
|
35
|
+
expect(e.fix).toContain(opts.fixIncludes);
|
|
36
|
+
}
|
|
37
|
+
if (opts.detailsField) {
|
|
38
|
+
expect(e.details).toBeDefined();
|
|
39
|
+
expect(e.details!.field).toBe(opts.detailsField);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// sanitizeFilePath — engine.ts
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
describe('error-hints: sanitizeFilePath', () => {
|
|
48
|
+
it('empty path — fix mentions file path, field=path', () => {
|
|
49
|
+
assertErrorHints(() => sanitizeFilePath(''), {
|
|
50
|
+
detailsField: 'path',
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('path with shell metachar — fix mentions shell metacharacters, field=path', () => {
|
|
55
|
+
assertErrorHints(() => sanitizeFilePath('/tmp/file;rm -rf /'), {
|
|
56
|
+
fixIncludes: 'metacharacter',
|
|
57
|
+
detailsField: 'path',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('path ending in backslash — fix mentions backslash, field=path', () => {
|
|
62
|
+
assertErrorHints(() => sanitizeFilePath('/tmp/file\\'), {
|
|
63
|
+
fixIncludes: 'backslash',
|
|
64
|
+
detailsField: 'path',
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('path with newline — fix mentions newline, field=path', () => {
|
|
69
|
+
assertErrorHints(() => sanitizeFilePath('/tmp/file\ninjected'), {
|
|
70
|
+
fixIncludes: 'newline',
|
|
71
|
+
detailsField: 'path',
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// loadManifestEntryByTaskId — protocols/_shared.ts
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
describe('error-hints: loadManifestEntryByTaskId', () => {
|
|
81
|
+
it('task not in manifest — fix mentions manifest entry, field=taskId', () => {
|
|
82
|
+
// This will always throw NOT_FOUND since no manifest exists in test env
|
|
83
|
+
assertErrorHints(() => loadManifestEntryByTaskId('T9999'), {
|
|
84
|
+
fixIncludes: 'manifest',
|
|
85
|
+
detailsField: 'taskId',
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('error-hints: loadManifestEntryFromFile', () => {
|
|
91
|
+
it('manifest file not found — fix mentions manifest file path, field=manifestFile', () => {
|
|
92
|
+
assertErrorHints(() => loadManifestEntryFromFile('/nonexistent/path/manifest.json'), {
|
|
93
|
+
fixIncludes: 'manifest',
|
|
94
|
+
detailsField: 'manifestFile',
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
package/src/validation/engine.ts
CHANGED
|
@@ -94,7 +94,10 @@ const SHELL_METACHARACTERS = new Set([
|
|
|
94
94
|
*/
|
|
95
95
|
export function sanitizeFilePath(path: string): string {
|
|
96
96
|
if (!path) {
|
|
97
|
-
throw new CleoError(ExitCode.VALIDATION_ERROR, 'Empty path provided'
|
|
97
|
+
throw new CleoError(ExitCode.VALIDATION_ERROR, 'Empty path provided', {
|
|
98
|
+
fix: 'Provide a non-empty file path',
|
|
99
|
+
details: { field: 'path' },
|
|
100
|
+
}); /* internal invariant — no user action */
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
for (const char of path) {
|
|
@@ -102,6 +105,10 @@ export function sanitizeFilePath(path: string): string {
|
|
|
102
105
|
throw new CleoError(
|
|
103
106
|
ExitCode.VALIDATION_ERROR,
|
|
104
107
|
`Path contains shell metacharacters - potential injection attempt: ${path}`,
|
|
108
|
+
{
|
|
109
|
+
fix: 'Remove shell metacharacters from the file path',
|
|
110
|
+
details: { field: 'path', actual: path },
|
|
111
|
+
},
|
|
105
112
|
);
|
|
106
113
|
}
|
|
107
114
|
}
|
|
@@ -110,6 +117,10 @@ export function sanitizeFilePath(path: string): string {
|
|
|
110
117
|
throw new CleoError(
|
|
111
118
|
ExitCode.VALIDATION_ERROR,
|
|
112
119
|
'Path ends with backslash - potential injection attempt',
|
|
120
|
+
{
|
|
121
|
+
fix: 'Remove trailing backslash from the file path',
|
|
122
|
+
details: { field: 'path', actual: path },
|
|
123
|
+
},
|
|
113
124
|
);
|
|
114
125
|
}
|
|
115
126
|
|
|
@@ -117,6 +128,10 @@ export function sanitizeFilePath(path: string): string {
|
|
|
117
128
|
throw new CleoError(
|
|
118
129
|
ExitCode.VALIDATION_ERROR,
|
|
119
130
|
'Path contains newline/carriage return - potential injection attempt',
|
|
131
|
+
{
|
|
132
|
+
fix: 'Remove newline characters from the file path',
|
|
133
|
+
details: { field: 'path', actual: path },
|
|
134
|
+
},
|
|
120
135
|
);
|
|
121
136
|
}
|
|
122
137
|
|
|
@@ -56,7 +56,7 @@ interface ParamDef {
|
|
|
56
56
|
items?: { type: ParamType };
|
|
57
57
|
cli?: ParamCliDef;
|
|
58
58
|
/** Schema config for dispatch adapter. */
|
|
59
|
-
|
|
59
|
+
dispatch?: ParamSchemaDef;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// ---------------------------------------------------------------------------
|
|
@@ -79,7 +79,7 @@ export interface JSONSchemaObject {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// ---------------------------------------------------------------------------
|
|
82
|
-
// 1.
|
|
82
|
+
// 1. buildDispatchInputSchema
|
|
83
83
|
// ---------------------------------------------------------------------------
|
|
84
84
|
|
|
85
85
|
/**
|
|
@@ -87,18 +87,18 @@ export interface JSONSchemaObject {
|
|
|
87
87
|
*
|
|
88
88
|
* Algorithm:
|
|
89
89
|
* 1. Iterate `def.params`
|
|
90
|
-
* 2. Skip params where schema `
|
|
90
|
+
* 2. Skip params where schema `dispatch.hidden === true`
|
|
91
91
|
* 3. Map ParamType → JSON Schema type
|
|
92
92
|
* 4. Collect names where `required === true` into `required[]`
|
|
93
93
|
* 5. Return { type: 'object', properties, required }
|
|
94
94
|
*/
|
|
95
|
-
export function
|
|
95
|
+
export function buildDispatchInputSchema(def: OperationDef): JSONSchemaObject {
|
|
96
96
|
const properties: Record<string, JsonSchemaProperty> = {};
|
|
97
97
|
const required: string[] = [];
|
|
98
98
|
|
|
99
99
|
for (const param of def.params ?? []) {
|
|
100
100
|
// Skip CLI-only params from dispatch schema
|
|
101
|
-
if (param.
|
|
101
|
+
if (param.dispatch?.hidden === true) continue;
|
|
102
102
|
|
|
103
103
|
const prop: JsonSchemaProperty = {
|
|
104
104
|
type: paramTypeToJsonSchema(param.type),
|
|
@@ -109,8 +109,8 @@ export function buildMcpInputSchema(def: OperationDef): JSONSchemaObject {
|
|
|
109
109
|
prop.items = { type: 'string' };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
if (param.
|
|
113
|
-
prop.enum = param.
|
|
112
|
+
if (param.dispatch?.enum) {
|
|
113
|
+
prop.enum = param.dispatch.enum;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
properties[param.name] = prop;
|
|
@@ -123,6 +123,9 @@ export function buildMcpInputSchema(def: OperationDef): JSONSchemaObject {
|
|
|
123
123
|
return { type: 'object', properties, required };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/** @deprecated Use {@link buildDispatchInputSchema} instead. */
|
|
127
|
+
export const buildMcpInputSchema = buildDispatchInputSchema;
|
|
128
|
+
|
|
126
129
|
function paramTypeToJsonSchema(t: ParamDef['type']): JsonSchemaType {
|
|
127
130
|
switch (t) {
|
|
128
131
|
case 'string':
|
|
@@ -46,7 +46,10 @@ export function loadManifestEntryByTaskId(taskId: string): ManifestEntryInput {
|
|
|
46
46
|
const manifestPath = getManifestPath();
|
|
47
47
|
const entry = findManifestEntry(taskId, manifestPath);
|
|
48
48
|
if (!entry) {
|
|
49
|
-
throw new CleoError(ExitCode.NOT_FOUND, `No manifest entry found for task ${taskId}
|
|
49
|
+
throw new CleoError(ExitCode.NOT_FOUND, `No manifest entry found for task ${taskId}`, {
|
|
50
|
+
fix: `Ensure the agent wrote a manifest entry for ${taskId} before validation`,
|
|
51
|
+
details: { field: 'taskId', actual: taskId },
|
|
52
|
+
});
|
|
50
53
|
}
|
|
51
54
|
return JSON.parse(entry) as ManifestEntryInput;
|
|
52
55
|
}
|
|
@@ -59,7 +62,10 @@ export function loadManifestEntryByTaskId(taskId: string): ManifestEntryInput {
|
|
|
59
62
|
*/
|
|
60
63
|
export function loadManifestEntryFromFile(manifestFile: string): ManifestEntryInput {
|
|
61
64
|
if (!existsSync(manifestFile)) {
|
|
62
|
-
throw new CleoError(ExitCode.NOT_FOUND, `Manifest file not found: ${manifestFile}
|
|
65
|
+
throw new CleoError(ExitCode.NOT_FOUND, `Manifest file not found: ${manifestFile}`, {
|
|
66
|
+
fix: `Ensure the manifest file exists at: ${manifestFile}`,
|
|
67
|
+
details: { field: 'manifestFile', actual: manifestFile },
|
|
68
|
+
});
|
|
63
69
|
}
|
|
64
70
|
return JSON.parse(readFileSync(manifestFile, 'utf-8')) as ManifestEntryInput;
|
|
65
71
|
}
|
|
@@ -78,11 +84,13 @@ export function throwIfStrictFailed(
|
|
|
78
84
|
): void {
|
|
79
85
|
if (!opts.strict || result.valid) return;
|
|
80
86
|
const code = PROTOCOL_EXIT_CODES[protocol];
|
|
87
|
+
const errorViolations = result.violations.filter((v) => v.severity === 'error');
|
|
81
88
|
throw new CleoError(
|
|
82
89
|
code,
|
|
83
|
-
`${protocol} protocol violations for ${taskId}: ${
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
`${protocol} protocol violations for ${taskId}: ${errorViolations.map((v) => v.message).join('; ')}`,
|
|
91
|
+
{
|
|
92
|
+
fix: `Review ${protocol} protocol requirements and correct the listed violations`,
|
|
93
|
+
details: { field: 'protocol', actual: protocol },
|
|
94
|
+
},
|
|
87
95
|
);
|
|
88
96
|
}
|