@soleri/core 9.13.0 → 9.14.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 (154) hide show
  1. package/dist/engine/bin/soleri-engine.js +7 -2
  2. package/dist/engine/bin/soleri-engine.js.map +1 -1
  3. package/dist/flows/types.d.ts +34 -30
  4. package/dist/flows/types.d.ts.map +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/knowledge-packs/community/.gitkeep +0 -0
  10. package/dist/knowledge-packs/knowledge-packs/community/.gitkeep +0 -0
  11. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/soleri-pack.json +10 -0
  12. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/accessibility.json +53 -0
  13. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/design-tokens.json +26 -0
  14. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/design.json +33 -0
  15. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/styling.json +44 -0
  16. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/ux-laws.json +36 -0
  17. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/ux.json +36 -0
  18. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/soleri-pack.json +10 -0
  19. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/architecture.json +143 -0
  20. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/commercial.json +16 -0
  21. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/communication.json +33 -0
  22. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/component.json +16 -0
  23. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/express.json +34 -0
  24. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/leadership.json +33 -0
  25. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/methodology.json +33 -0
  26. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/monorepo.json +33 -0
  27. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/other.json +73 -0
  28. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/performance.json +35 -0
  29. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/prisma.json +33 -0
  30. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/product-strategy.json +42 -0
  31. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/react.json +47 -0
  32. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/security.json +34 -0
  33. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/testing.json +33 -0
  34. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/tooling.json +85 -0
  35. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/typescript.json +34 -0
  36. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/workflow.json +46 -0
  37. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-uipro/soleri-pack.json +10 -0
  38. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-uipro/vault/design.json +2589 -0
  39. package/dist/knowledge-packs/knowledge-packs/starter/api-design/soleri-pack.json +9 -0
  40. package/dist/knowledge-packs/knowledge-packs/starter/api-design/vault/patterns.json +137 -0
  41. package/dist/knowledge-packs/knowledge-packs/starter/architecture/soleri-pack.json +10 -0
  42. package/dist/knowledge-packs/knowledge-packs/starter/architecture/vault/patterns.json +137 -0
  43. package/dist/knowledge-packs/knowledge-packs/starter/design/soleri-pack.json +10 -0
  44. package/dist/knowledge-packs/knowledge-packs/starter/design/vault/patterns.json +137 -0
  45. package/dist/knowledge-packs/knowledge-packs/starter/nodejs/soleri-pack.json +9 -0
  46. package/dist/knowledge-packs/knowledge-packs/starter/nodejs/vault/patterns.json +137 -0
  47. package/dist/knowledge-packs/knowledge-packs/starter/react/soleri-pack.json +9 -0
  48. package/dist/knowledge-packs/knowledge-packs/starter/react/vault/patterns.json +164 -0
  49. package/dist/knowledge-packs/knowledge-packs/starter/security/soleri-pack.json +10 -0
  50. package/dist/knowledge-packs/knowledge-packs/starter/security/vault/patterns.json +137 -0
  51. package/dist/knowledge-packs/knowledge-packs/starter/testing/soleri-pack.json +9 -0
  52. package/dist/knowledge-packs/knowledge-packs/starter/testing/vault/patterns.json +128 -0
  53. package/dist/knowledge-packs/knowledge-packs/starter/typescript/soleri-pack.json +9 -0
  54. package/dist/knowledge-packs/knowledge-packs/starter/typescript/vault/patterns.json +164 -0
  55. package/dist/knowledge-packs/salvador/salvador-craft/soleri-pack.json +10 -0
  56. package/dist/knowledge-packs/salvador/salvador-craft/vault/accessibility.json +53 -0
  57. package/dist/knowledge-packs/salvador/salvador-craft/vault/design-tokens.json +26 -0
  58. package/dist/knowledge-packs/salvador/salvador-craft/vault/design.json +33 -0
  59. package/dist/knowledge-packs/salvador/salvador-craft/vault/styling.json +44 -0
  60. package/dist/knowledge-packs/salvador/salvador-craft/vault/ux-laws.json +36 -0
  61. package/dist/knowledge-packs/salvador/salvador-craft/vault/ux.json +36 -0
  62. package/dist/knowledge-packs/salvador/salvador-engineering/soleri-pack.json +10 -0
  63. package/dist/knowledge-packs/salvador/salvador-engineering/vault/architecture.json +143 -0
  64. package/dist/knowledge-packs/salvador/salvador-engineering/vault/commercial.json +16 -0
  65. package/dist/knowledge-packs/salvador/salvador-engineering/vault/communication.json +33 -0
  66. package/dist/knowledge-packs/salvador/salvador-engineering/vault/component.json +16 -0
  67. package/dist/knowledge-packs/salvador/salvador-engineering/vault/express.json +34 -0
  68. package/dist/knowledge-packs/salvador/salvador-engineering/vault/leadership.json +33 -0
  69. package/dist/knowledge-packs/salvador/salvador-engineering/vault/methodology.json +33 -0
  70. package/dist/knowledge-packs/salvador/salvador-engineering/vault/monorepo.json +33 -0
  71. package/dist/knowledge-packs/salvador/salvador-engineering/vault/other.json +73 -0
  72. package/dist/knowledge-packs/salvador/salvador-engineering/vault/performance.json +35 -0
  73. package/dist/knowledge-packs/salvador/salvador-engineering/vault/prisma.json +33 -0
  74. package/dist/knowledge-packs/salvador/salvador-engineering/vault/product-strategy.json +42 -0
  75. package/dist/knowledge-packs/salvador/salvador-engineering/vault/react.json +47 -0
  76. package/dist/knowledge-packs/salvador/salvador-engineering/vault/security.json +34 -0
  77. package/dist/knowledge-packs/salvador/salvador-engineering/vault/testing.json +33 -0
  78. package/dist/knowledge-packs/salvador/salvador-engineering/vault/tooling.json +85 -0
  79. package/dist/knowledge-packs/salvador/salvador-engineering/vault/typescript.json +34 -0
  80. package/dist/knowledge-packs/salvador/salvador-engineering/vault/workflow.json +46 -0
  81. package/dist/knowledge-packs/salvador/salvador-uipro/soleri-pack.json +10 -0
  82. package/dist/knowledge-packs/salvador/salvador-uipro/vault/design.json +2589 -0
  83. package/dist/knowledge-packs/starter/architecture/soleri-pack.json +10 -0
  84. package/dist/knowledge-packs/starter/architecture/vault/patterns.json +137 -0
  85. package/dist/knowledge-packs/starter/design/soleri-pack.json +10 -0
  86. package/dist/knowledge-packs/starter/design/vault/patterns.json +137 -0
  87. package/dist/knowledge-packs/starter/security/soleri-pack.json +10 -0
  88. package/dist/knowledge-packs/starter/security/vault/patterns.json +137 -0
  89. package/dist/packs/index.d.ts +1 -1
  90. package/dist/packs/index.d.ts.map +1 -1
  91. package/dist/packs/index.js +1 -1
  92. package/dist/packs/index.js.map +1 -1
  93. package/dist/packs/resolver.d.ts +6 -0
  94. package/dist/packs/resolver.d.ts.map +1 -1
  95. package/dist/packs/resolver.js +20 -1
  96. package/dist/packs/resolver.js.map +1 -1
  97. package/dist/runtime/admin-setup-ops.js +1 -1
  98. package/dist/runtime/admin-setup-ops.js.map +1 -1
  99. package/dist/runtime/capture-ops.d.ts.map +1 -1
  100. package/dist/runtime/capture-ops.js +2 -1
  101. package/dist/runtime/capture-ops.js.map +1 -1
  102. package/dist/runtime/intake-ops.d.ts.map +1 -1
  103. package/dist/runtime/intake-ops.js +5 -5
  104. package/dist/runtime/intake-ops.js.map +1 -1
  105. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  106. package/dist/runtime/orchestrate-ops.js +26 -2
  107. package/dist/runtime/orchestrate-ops.js.map +1 -1
  108. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  109. package/dist/runtime/planning-extra-ops.js +5 -7
  110. package/dist/runtime/planning-extra-ops.js.map +1 -1
  111. package/dist/runtime/playbook-ops.d.ts.map +1 -1
  112. package/dist/runtime/playbook-ops.js +2 -1
  113. package/dist/runtime/playbook-ops.js.map +1 -1
  114. package/dist/runtime/schema-helpers.d.ts +7 -0
  115. package/dist/runtime/schema-helpers.d.ts.map +1 -0
  116. package/dist/runtime/schema-helpers.js +21 -0
  117. package/dist/runtime/schema-helpers.js.map +1 -0
  118. package/dist/runtime/sync-ops.d.ts.map +1 -1
  119. package/dist/runtime/sync-ops.js +3 -4
  120. package/dist/runtime/sync-ops.js.map +1 -1
  121. package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
  122. package/dist/runtime/vault-extra-ops.js +5 -4
  123. package/dist/runtime/vault-extra-ops.js.map +1 -1
  124. package/dist/skills/sync-skills.d.ts +26 -7
  125. package/dist/skills/sync-skills.d.ts.map +1 -1
  126. package/dist/skills/sync-skills.js +126 -32
  127. package/dist/skills/sync-skills.js.map +1 -1
  128. package/dist/skills/validate-skill-docs.d.ts +24 -0
  129. package/dist/skills/validate-skill-docs.d.ts.map +1 -0
  130. package/dist/skills/validate-skill-docs.js +476 -0
  131. package/dist/skills/validate-skill-docs.js.map +1 -0
  132. package/package.json +2 -2
  133. package/src/__tests__/deviation-detection.test.ts +49 -0
  134. package/src/enforcement/adapters/claude-code.test.ts +9 -9
  135. package/src/engine/bin/soleri-engine.ts +7 -2
  136. package/src/flows/types.ts +4 -0
  137. package/src/index.ts +15 -2
  138. package/src/packs/index.ts +6 -1
  139. package/src/packs/resolver.ts +24 -1
  140. package/src/runtime/admin-setup-ops.test.ts +2 -0
  141. package/src/runtime/admin-setup-ops.ts +1 -1
  142. package/src/runtime/capture-ops.ts +2 -1
  143. package/src/runtime/intake-ops.ts +7 -7
  144. package/src/runtime/orchestrate-ops.ts +29 -2
  145. package/src/runtime/planning-extra-ops.ts +35 -37
  146. package/src/runtime/playbook-ops.ts +2 -1
  147. package/src/runtime/schema-helpers.test.ts +45 -0
  148. package/src/runtime/schema-helpers.ts +19 -0
  149. package/src/runtime/sync-ops.ts +8 -9
  150. package/src/runtime/vault-extra-ops.ts +5 -4
  151. package/src/skills/__tests__/sync-skills.test.ts +102 -29
  152. package/src/skills/__tests__/validate-skill-docs.test.ts +58 -0
  153. package/src/skills/sync-skills.ts +146 -32
  154. package/src/skills/validate-skill-docs.ts +562 -0
package/src/index.ts CHANGED
@@ -608,6 +608,9 @@ export {
608
608
  export { loadPersona } from './persona/loader.js';
609
609
  export { generatePersonaInstructions, getRandomSignoff } from './persona/prompt-generator.js';
610
610
 
611
+ // ─── Schema Helpers ────────────────────────────────────────────────
612
+ export { coerceArray } from './runtime/schema-helpers.js';
613
+
611
614
  // ─── Runtime Factory ────────────────────────────────────────────────
612
615
  export { createAgentRuntime } from './runtime/runtime.js';
613
616
  export { createSemanticFacades } from './runtime/facades/index.js';
@@ -688,7 +691,12 @@ export {
688
691
  } from './packs/index.js';
689
692
  export type { PackState, PackTransition } from './packs/index.js';
690
693
  export { PackLockfile, inferPackType } from './packs/index.js';
691
- export { resolvePack, checkNpmVersion, checkVersionCompat } from './packs/index.js';
694
+ export {
695
+ resolvePack,
696
+ checkNpmVersion,
697
+ checkVersionCompat,
698
+ getBuiltinKnowledgePacksDirs,
699
+ } from './packs/index.js';
692
700
  export type {
693
701
  PackManifest,
694
702
  PackStatus,
@@ -717,7 +725,12 @@ export {
717
725
  checkSkillCompatibility,
718
726
  ApprovalRequiredError,
719
727
  } from './skills/sync-skills.js';
720
- export type { SkillEntry, SyncResult, ClassifySkillsOptions } from './skills/sync-skills.js';
728
+ export type {
729
+ SkillEntry,
730
+ SyncResult,
731
+ SyncOptions,
732
+ ClassifySkillsOptions,
733
+ } from './skills/sync-skills.js';
721
734
 
722
735
  // ─── Plugin System ──────────────────────────────────────────────────────
723
736
  export {
@@ -26,5 +26,10 @@ export { PackLifecycleManager } from './pack-lifecycle.js';
26
26
  export { PackLockfile, inferPackType, LOCKFILE_VERSION } from './lockfile.js';
27
27
  export type { LockEntry, PackType, PackSource, PackTier, LockfileData } from './lockfile.js';
28
28
 
29
- export { resolvePack, checkNpmVersion, checkVersionCompat } from './resolver.js';
29
+ export {
30
+ resolvePack,
31
+ checkNpmVersion,
32
+ checkVersionCompat,
33
+ getBuiltinKnowledgePacksDirs,
34
+ } from './resolver.js';
30
35
  export type { ResolvedPack, ResolveOptions } from './resolver.js';
@@ -10,10 +10,33 @@
10
10
  */
11
11
 
12
12
  import { existsSync } from 'node:fs';
13
- import { resolve, join } from 'node:path';
13
+ import { resolve, join, dirname } from 'node:path';
14
14
  import { execFileSync } from 'node:child_process';
15
15
  import { mkdirSync, readdirSync } from 'node:fs';
16
16
  import { tmpdir } from 'node:os';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ // ─── Built-in knowledge-packs discovery ──────────────────────────────
20
+
21
+ const _dirname =
22
+ typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url));
23
+
24
+ /**
25
+ * Discover the built-in `knowledge-packs/` directory shipped with @soleri/core.
26
+ * Tries multiple levels up from this file's compiled location to account for
27
+ * different installation layouts (monorepo dev, npm install, npx).
28
+ */
29
+ export function getBuiltinKnowledgePacksDirs(): string[] {
30
+ const dirs: string[] = [];
31
+ for (let i = 1; i <= 5; i++) {
32
+ const candidate = resolve(_dirname, ...Array<string>(i).fill('..'), 'knowledge-packs');
33
+ if (existsSync(candidate)) {
34
+ dirs.push(candidate);
35
+ break;
36
+ }
37
+ }
38
+ return dirs;
39
+ }
17
40
 
18
41
  // ─── Types ────────────────────────────────────────────────────────────
19
42
 
@@ -53,6 +53,8 @@ vi.mock('../skills/sync-skills.js', () => ({
53
53
  updated: [],
54
54
  skipped: [],
55
55
  failed: [],
56
+ removed: [],
57
+ cleanedGlobal: [],
56
58
  })),
57
59
  }));
58
60
 
@@ -425,7 +425,7 @@ export function createAdminSetupOps(runtime: AgentRuntime): OpDefinition[] {
425
425
  const agentName =
426
426
  runtime.persona?.name ??
427
427
  config.agentId.charAt(0).toUpperCase() + config.agentId.slice(1);
428
- skillsResults = syncSkillsToClaudeCode(skillsSourceDirs, agentName);
428
+ skillsResults = syncSkillsToClaudeCode(skillsSourceDirs, agentName, { global: true });
429
429
  } else {
430
430
  // Dry run — just discover what would be synced
431
431
  const skills = discoverSkills(skillsSourceDirs);
@@ -8,6 +8,7 @@
8
8
  import { z } from 'zod';
9
9
  import type { OpDefinition } from '../facades/types.js';
10
10
  import type { AgentRuntime } from './types.js';
11
+ import { coerceArray } from './schema-helpers.js';
11
12
  import { detectScope } from '../vault/scope-detector.js';
12
13
  import type { ScopeTier, ScopeDetectionResult } from '../vault/scope-detector.js';
13
14
  import { syncEntryToMarkdown } from '../vault/vault-markdown-sync.js';
@@ -35,7 +36,7 @@ export function createCaptureOps(runtime: AgentRuntime): OpDefinition[] {
35
36
  .enum(['agent', 'project', 'team'])
36
37
  .optional()
37
38
  .describe('Manual tier override. If omitted, tier is auto-detected from content.'),
38
- entries: z.array(
39
+ entries: coerceArray(
39
40
  z.object({
40
41
  id: z.string().optional(),
41
42
  type: z
@@ -9,6 +9,7 @@ import { z } from 'zod';
9
9
  import type { OpDefinition } from '../facades/types.js';
10
10
  import type { IntakePipeline } from '../intake/intake-pipeline.js';
11
11
  import type { TextIngester, IngestSource } from '../intake/text-ingester.js';
12
+ import { coerceArray } from './schema-helpers.js';
12
13
 
13
14
  /**
14
15
  * Create the 7 intake operations.
@@ -191,9 +192,9 @@ export function createIntakeOps(
191
192
  'Ingest multiple text items in one call. Each item has its own source metadata. Processed sequentially.',
192
193
  auth: 'write',
193
194
  schema: z.object({
194
- items: z
195
- .array(
196
- z.object({
195
+ items: coerceArray(
196
+ z
197
+ .object({
197
198
  text: z.string(),
198
199
  title: z.string(),
199
200
  sourceType: z.enum(['article', 'transcript', 'notes', 'documentation']).optional(),
@@ -201,10 +202,9 @@ export function createIntakeOps(
201
202
  author: z.string().optional(),
202
203
  domain: z.string().optional(),
203
204
  tags: z.array(z.string()).optional(),
204
- }),
205
- )
206
- .min(1)
207
- .describe('Array of items to ingest'),
205
+ })
206
+ .strict(),
207
+ ).describe('Array of items to ingest (at least 1)'),
208
208
  }),
209
209
  handler: async (params) => {
210
210
  if (!textIngester) return { error: 'Text ingester not configured (LLM client required)' };
@@ -136,12 +136,32 @@ export function applyWorkflowOverride(plan: OrchestrationPlan, override: Workflo
136
136
  }
137
137
  }
138
138
 
139
+ // Inject workflow prompt.md content if available
140
+ if (override.prompt) {
141
+ plan.workflowPrompt = override.prompt;
142
+ plan.workflowName = override.name;
143
+ }
144
+
139
145
  // Add workflow info to warnings for visibility
140
146
  plan.warnings.push(
141
147
  `Workflow override "${override.name}" applied (${override.gates.length} gate(s), ${override.tools.length} tool(s)).`,
142
148
  );
143
149
  }
144
150
 
151
+ // ---------------------------------------------------------------------------
152
+ // Workflow prompt preamble helper
153
+ // ---------------------------------------------------------------------------
154
+
155
+ /**
156
+ * Prepend workflow prompt content to a task prompt when available.
157
+ * Returns the original prompt unchanged if no workflow prompt is set.
158
+ */
159
+ function withWorkflowPreamble(taskPrompt: string, plan: OrchestrationPlan | undefined): string {
160
+ if (!plan?.workflowPrompt) return taskPrompt;
161
+ const header = plan.workflowName ? `## Workflow: ${plan.workflowName}` : '## Workflow';
162
+ return `${header}\n${plan.workflowPrompt}\n\n## Task\n${taskPrompt}`;
163
+ }
164
+
145
165
  // ---------------------------------------------------------------------------
146
166
  // In-memory plan store
147
167
  // ---------------------------------------------------------------------------
@@ -532,7 +552,7 @@ export function createOrchestrateOps(
532
552
  const tasks =
533
553
  entry?.plan.steps.map((s) => ({
534
554
  taskId: s.id,
535
- prompt: s.name,
555
+ prompt: withWorkflowPreamble(s.name, entry?.plan),
536
556
  workspace: process.cwd(),
537
557
  runtime: runtimeType,
538
558
  timeout: 300_000,
@@ -610,7 +630,8 @@ export function createOrchestrateOps(
610
630
  if (runtimeType && runtime.adapterRegistry) {
611
631
  const adapter = runtime.adapterRegistry.get(runtimeType);
612
632
  const entry = planStore.get(planId);
613
- const prompt = entry?.plan.summary ?? `Execute plan ${planId}`;
633
+ const rawPrompt = entry?.plan.summary ?? `Execute plan ${planId}`;
634
+ const prompt = withWorkflowPreamble(rawPrompt, entry?.plan);
614
635
 
615
636
  const adapterResult = await adapter.execute({
616
637
  runId: `${planId}-${Date.now()}`,
@@ -692,6 +713,11 @@ export function createOrchestrateOps(
692
713
  const healthStatus = contextHealth.check();
693
714
  const healthWarning = buildHealthWarning(healthStatus, vault);
694
715
 
716
+ // Build workflow preamble for the calling agent's context
717
+ const workflowPreamble = entry.plan.workflowPrompt
718
+ ? withWorkflowPreamble(entry.plan.summary, entry.plan)
719
+ : undefined;
720
+
695
721
  return {
696
722
  plan: { id: planId, status: 'executing' },
697
723
  session,
@@ -702,6 +728,7 @@ export function createOrchestrateOps(
702
728
  toolsCalled: executionResult.toolsCalled,
703
729
  durationMs: executionResult.durationMs,
704
730
  },
731
+ ...(workflowPreamble ? { workflowPreamble } : {}),
705
732
  ...(healthWarning ? { contextHealth: healthWarning } : {}),
706
733
  };
707
734
  }
@@ -13,6 +13,7 @@ import fs from 'node:fs';
13
13
  import { z } from 'zod';
14
14
  import type { OpDefinition } from '../facades/types.js';
15
15
  import type { AgentRuntime } from './types.js';
16
+ import { coerceArray } from './schema-helpers.js';
16
17
  import type { DriftItem, TaskEvidence } from '../planning/planner.js';
17
18
  import type { PlanRunManifest } from '../flows/types.js';
18
19
  import { getPlanRunDir } from '../flows/executor.js';
@@ -64,25 +65,24 @@ export function createPlanningExtraOps(runtime: AgentRuntime): OpDefinition[] {
64
65
  )
65
66
  .optional()
66
67
  .describe('Rejected alternative approaches (replaces existing)'),
67
- addTasks: z
68
- .array(
69
- z.object({
70
- title: z.string(),
71
- description: z.string(),
72
- phase: z
73
- .string()
74
- .optional()
75
- .describe('Phase this task belongs to (e.g., "wave-1", "discovery")'),
76
- milestone: z
77
- .string()
78
- .optional()
79
- .describe('Milestone this task contributes to (e.g., "v1.0", "mvp")'),
80
- parentTaskId: z.string().optional().describe('Parent task ID for sub-task hierarchy'),
81
- }),
82
- )
68
+ addTasks: coerceArray(
69
+ z.object({
70
+ title: z.string(),
71
+ description: z.string(),
72
+ phase: z
73
+ .string()
74
+ .optional()
75
+ .describe('Phase this task belongs to (e.g., "wave-1", "discovery")'),
76
+ milestone: z
77
+ .string()
78
+ .optional()
79
+ .describe('Milestone this task contributes to (e.g., "v1.0", "mvp")'),
80
+ parentTaskId: z.string().optional().describe('Parent task ID for sub-task hierarchy'),
81
+ }),
82
+ )
83
83
  .optional()
84
84
  .describe('Tasks to append'),
85
- removeTasks: z.array(z.string()).optional().describe('Task IDs to remove'),
85
+ removeTasks: coerceArray(z.string()).optional().describe('Task IDs to remove'),
86
86
  }),
87
87
  handler: async (params) => {
88
88
  try {
@@ -116,26 +116,24 @@ export function createPlanningExtraOps(runtime: AgentRuntime): OpDefinition[] {
116
116
  auth: 'write',
117
117
  schema: z.object({
118
118
  planId: z.string().describe('Plan ID to split tasks for'),
119
- tasks: z
120
- .array(
121
- z.object({
122
- title: z.string(),
123
- description: z.string(),
124
- dependsOn: z.array(z.string()).optional().describe('Task IDs this task depends on'),
125
- phase: z
126
- .string()
127
- .optional()
128
- .describe(
129
- 'Phase this task belongs to (e.g., "wave-1", "discovery", "implementation")',
130
- ),
131
- milestone: z
132
- .string()
133
- .optional()
134
- .describe('Milestone this task contributes to (e.g., "v1.0", "mvp", "beta")'),
135
- parentTaskId: z.string().optional().describe('Parent task ID for sub-task hierarchy'),
136
- }),
137
- )
138
- .describe('New task list with optional dependency references (task-1, task-2, etc.)'),
119
+ tasks: coerceArray(
120
+ z.object({
121
+ title: z.string(),
122
+ description: z.string(),
123
+ dependsOn: z.array(z.string()).optional().describe('Task IDs this task depends on'),
124
+ phase: z
125
+ .string()
126
+ .optional()
127
+ .describe(
128
+ 'Phase this task belongs to (e.g., "wave-1", "discovery", "implementation")',
129
+ ),
130
+ milestone: z
131
+ .string()
132
+ .optional()
133
+ .describe('Milestone this task contributes to (e.g., "v1.0", "mvp", "beta")'),
134
+ parentTaskId: z.string().optional().describe('Parent task ID for sub-task hierarchy'),
135
+ }),
136
+ ).describe('New task list with optional dependency references (task-1, task-2, etc.)'),
139
137
  }),
140
138
  handler: async (params) => {
141
139
  try {
@@ -8,6 +8,7 @@
8
8
  import { z } from 'zod';
9
9
  import type { OpDefinition } from '../facades/types.js';
10
10
  import type { AgentRuntime } from './types.js';
11
+ import { coerceArray } from './schema-helpers.js';
11
12
  import { parsePlaybookFromEntry, validatePlaybook } from '../vault/playbook.js';
12
13
  import {
13
14
  matchPlaybooks,
@@ -69,7 +70,7 @@ export function createPlaybookOps(runtime: AgentRuntime): OpDefinition[] {
69
70
  title: z.string(),
70
71
  domain: z.string(),
71
72
  description: z.string(),
72
- steps: z.array(
73
+ steps: coerceArray(
73
74
  z.object({
74
75
  title: z.string(),
75
76
  description: z.string(),
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Colocated unit tests for schema-helpers.ts — coerceArray Zod helper.
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { z } from 'zod';
7
+ import { coerceArray } from './schema-helpers.js';
8
+
9
+ describe('coerceArray', () => {
10
+ const schema = coerceArray(z.string());
11
+
12
+ it('passes through a native array unchanged', () => {
13
+ const result = schema.parse(['a', 'b', 'c']);
14
+ expect(result).toEqual(['a', 'b', 'c']);
15
+ });
16
+
17
+ it('coerces a JSON-stringified array', () => {
18
+ const result = schema.parse(JSON.stringify(['x', 'y']));
19
+ expect(result).toEqual(['x', 'y']);
20
+ });
21
+
22
+ it('rejects invalid JSON strings', () => {
23
+ expect(() => schema.parse('not-json')).toThrow();
24
+ });
25
+
26
+ it('rejects non-array JSON (object)', () => {
27
+ expect(() => schema.parse(JSON.stringify({ a: 1 }))).toThrow();
28
+ });
29
+
30
+ it('rejects non-array JSON (number)', () => {
31
+ expect(() => schema.parse(JSON.stringify(42))).toThrow();
32
+ });
33
+
34
+ it('works with complex item schemas', () => {
35
+ const complex = coerceArray(z.object({ id: z.string(), value: z.number() }));
36
+ const items = [{ id: 'a', value: 1 }];
37
+ expect(complex.parse(JSON.stringify(items))).toEqual(items);
38
+ expect(complex.parse(items)).toEqual(items);
39
+ });
40
+
41
+ it('still validates item types after coercion', () => {
42
+ // Array of strings schema should reject array of numbers
43
+ expect(() => schema.parse(JSON.stringify([1, 2, 3]))).toThrow();
44
+ });
45
+ });
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Wraps a Zod array schema so it also accepts a JSON-stringified array.
5
+ * MCP transports sometimes serialize array params as strings.
6
+ */
7
+ export function coerceArray<T extends z.ZodTypeAny>(itemSchema: T) {
8
+ return z.preprocess((val) => {
9
+ if (typeof val === 'string') {
10
+ try {
11
+ const parsed = JSON.parse(val);
12
+ if (Array.isArray(parsed)) return parsed;
13
+ } catch {
14
+ /* fall through to let Zod reject */
15
+ }
16
+ }
17
+ return val;
18
+ }, z.array(itemSchema));
19
+ }
@@ -10,6 +10,7 @@
10
10
  import { z } from 'zod';
11
11
  import type { OpDefinition } from '../facades/types.js';
12
12
  import type { AgentRuntime } from './types.js';
13
+ import { coerceArray } from './schema-helpers.js';
13
14
  import { GitVaultSync, type GitVaultSyncConfig } from '../vault/git-vault-sync.js';
14
15
  import type {
15
16
  IntelligenceEntry,
@@ -257,15 +258,13 @@ export function createSyncOps(runtime: AgentRuntime): OpDefinition[] {
257
258
  'Import an intelligence pack into the vault with content-hash dedup. Entries with duplicate content are skipped.',
258
259
  auth: 'write' as const,
259
260
  schema: z.object({
260
- bundles: z
261
- .array(
262
- z.object({
263
- domain: z.string(),
264
- version: z.string(),
265
- entries: z.array(z.record(z.unknown())),
266
- }),
267
- )
268
- .describe('Array of IntelligenceBundle objects to import'),
261
+ bundles: coerceArray(
262
+ z.object({
263
+ domain: z.string(),
264
+ version: z.string(),
265
+ entries: z.array(z.record(z.unknown())),
266
+ }),
267
+ ).describe('Array of IntelligenceBundle objects to import'),
269
268
  tier: z
270
269
  .enum(['agent', 'project', 'team'])
271
270
  .optional()
@@ -13,6 +13,7 @@ import { join, basename } from 'node:path';
13
13
  import type { OpDefinition } from '../facades/types.js';
14
14
  import type { IntelligenceEntry } from '../intelligence/types.js';
15
15
  import type { AgentRuntime } from './types.js';
16
+ import { coerceArray } from './schema-helpers.js';
16
17
 
17
18
  const entrySchema = z.object({
18
19
  id: z.string(),
@@ -115,7 +116,7 @@ export function createVaultExtraOps(runtime: AgentRuntime): OpDefinition[] {
115
116
  description: 'Add multiple vault entries at once. Uses upsert — existing IDs are updated.',
116
117
  auth: 'write',
117
118
  schema: z.object({
118
- entries: z.array(entrySchema),
119
+ entries: coerceArray(entrySchema),
119
120
  }),
120
121
  handler: async (params) => {
121
122
  const entries = params.entries as IntelligenceEntry[];
@@ -128,7 +129,7 @@ export function createVaultExtraOps(runtime: AgentRuntime): OpDefinition[] {
128
129
  description: 'Remove multiple vault entries by IDs in a single transaction.',
129
130
  auth: 'admin',
130
131
  schema: z.object({
131
- ids: z.array(z.string()),
132
+ ids: coerceArray(z.string()),
132
133
  }),
133
134
  handler: async (params) => {
134
135
  const ids = params.ids as string[];
@@ -177,7 +178,7 @@ export function createVaultExtraOps(runtime: AgentRuntime): OpDefinition[] {
177
178
  'Import vault entries from a JSON bundle. Uses upsert — existing IDs are updated, new IDs are inserted.',
178
179
  auth: 'write',
179
180
  schema: z.object({
180
- entries: z.array(entrySchema),
181
+ entries: coerceArray(entrySchema),
181
182
  }),
182
183
  handler: async (params) => {
183
184
  const entries = params.entries as IntelligenceEntry[];
@@ -198,7 +199,7 @@ export function createVaultExtraOps(runtime: AgentRuntime): OpDefinition[] {
198
199
  'Seed the vault from intelligence data. Idempotent — safe to call multiple times. Uses upsert.',
199
200
  auth: 'write',
200
201
  schema: z.object({
201
- entries: z.array(entrySchema),
202
+ entries: coerceArray(entrySchema),
202
203
  }),
203
204
  handler: async (params) => {
204
205
  const entries = params.entries as IntelligenceEntry[];