{"record":{"id":"2ef36ccfdb642131","repo":"ruvnet/ruflo","slug":"events-and-contexts-arrays-must-have-same-length","errorCode":null,"errorMessage":"Events and contexts arrays must have same length","messagePattern":"Events and contexts arrays must have same length","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/shared/src/hooks/executor.ts","lineNumber":247,"sourceCode":"      };\n    }\n  }\n\n  /**\n   * Execute multiple hooks in parallel\n   *\n   * @param events - Array of hook events\n   * @param contexts - Array of contexts (matched by index)\n   * @param options - Execution options\n   * @returns Array of aggregated results\n   */\n  async executeParallel(\n    events: HookEvent[],\n    contexts: HookContext[],\n    options: HookExecutionOptions = {}\n  ): Promise<AggregatedHookResult[]> {\n    if (events.length !== contexts.length) {\n      throw new Error('Events and contexts arrays must have same length');\n    }\n\n    const maxParallel = options.maxParallel || events.length;\n    const results: AggregatedHookResult[] = [];\n\n    // Execute in batches\n    for (let i = 0; i < events.length; i += maxParallel) {\n      const batch = events.slice(i, i + maxParallel);\n      const batchContexts = contexts.slice(i, i + maxParallel);\n\n      const batchResults = await Promise.allSettled(\n        batch.map((event, index) =>\n          this.execute(event, batchContexts[index], options)\n        )\n      );\n\n      for (const result of batchResults) {\n        if (result.status === 'fulfilled') {","sourceCodeStart":229,"sourceCodeEnd":265,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/shared/src/hooks/executor.ts#L229-L265","documentation":"ParallelHookExecutor.executeParallel(events, contexts, options) zips two arrays by index: events[i] is executed with contexts[i], and batches of size maxParallel are run with Promise.allSettled. It throws synchronously before any hook runs when the two arrays have different lengths, because there is no defensible pairing. This is a caller precondition violation, not a hook execution failure.","triggerScenarios":"Calling executeParallel(['pre-task','post-task'], [ctx]) after appending an event without its context; building events with a filter (e.g. only enabled hooks) while passing an unfiltered contexts array; reusing a cached contexts array after the events list changed; passing events with contexts.length === 0.","commonSituations":"Refactoring from execute() (single event + single context) to executeParallel() and forgetting the contexts array must grow too; dynamic event lists composed at runtime paired with a static one-element context list; copy-pasting a call site and editing one array but not the other.","solutions":["Derive contexts from events so lengths are structurally equal: const contexts = events.map(e => contextFor(e))","If one shared context applies to every event, pass events.map(() => sharedContext) instead of [sharedContext]","Add a dev-time assertion events.length === contexts.length at the construction site so the bug is caught where the arrays are built, not inside the executor","If the two lists legitimately differ, loop over execute(event, context, options) instead of executeParallel()"],"exampleFix":"// before\nconst events = enabledHooks.map(h => h.event);   // 3 entries\nconst contexts = [context];                        // 1 entry\nawait executor.executeParallel(events, contexts);   // throws\n\n// after\nconst events = enabledHooks.map(h => h.event);\nconst contexts = events.map(() => context);         // matched by index\nawait executor.executeParallel(events, contexts);","handlingStrategy":"validation","validationCode":"if (events.length !== contexts.length) {\n  throw new RangeError(\n    `executeParallel precondition failed: ${events.length} events vs ${contexts.length} contexts`\n  );\n}\nawait executor.executeParallel(events, contexts);","typeGuard":"function isIndexMatched<T, U>(events: T[], contexts: U[]): boolean {\n  return Array.isArray(events) && Array.isArray(contexts) && events.length === contexts.length;\n}","tryCatchPattern":"try {\n  await executor.executeParallel(events, contexts);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('same length')) {\n    // caller-side data bug: rebuild the arrays, do NOT retry with the same inputs\n    throw new Error(`hook batch malformed: ${events.length} events / ${contexts.length} contexts`);\n  }\n  throw e;\n}","preventionTips":["Derive contexts from events with .map() so the arrays cannot diverge","Keep a single array of { event, context } tuples and unzip only at the call boundary","Apply identical filters to both arrays, or filter the tuple array before unzipping"],"tags":["hooks","parallel-execution","array-mismatch","precondition"],"backgroundTag":"array-length-mismatch","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}