@magnusekdahl/parallix 1.1.0 → 1.2.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 (68) hide show
  1. package/config/integration-pipelines.json +9 -0
  2. package/config/workflow.config.schema.json +12 -0
  3. package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
  4. package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
  5. package/docs/adr/0041-integration-pipeline-gates.md +1 -1
  6. package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
  7. package/docs/adr/index.md +1 -0
  8. package/docs/use-cases.md +44 -0
  9. package/lib/agents/agents.js +149 -100
  10. package/lib/agents/claude-telemetry.js +14 -10
  11. package/lib/agents/claude.js +32 -11
  12. package/lib/agents/codex-telemetry.js +32 -19
  13. package/lib/agents/codex.js +36 -11
  14. package/lib/agents/limit-hit.js +42 -25
  15. package/lib/agents/mistral-telemetry.js +1 -1
  16. package/lib/agents/mistral.js +13 -3
  17. package/lib/agents/opencode-export.js +11 -5
  18. package/lib/agents/opencode-telemetry.js +75 -90
  19. package/lib/agents/opencode.js +86 -29
  20. package/lib/agents/stage-telemetry.js +3 -7
  21. package/lib/commands/active.js +139 -94
  22. package/lib/commands/checkpoint.js +3 -1
  23. package/lib/commands/config.js +5 -3
  24. package/lib/commands/coverage-gate.js +17 -10
  25. package/lib/commands/diff.js +11 -5
  26. package/lib/commands/draft.js +95 -55
  27. package/lib/commands/handoff.js +253 -65
  28. package/lib/commands/integrate.js +288 -188
  29. package/lib/commands/mission-start.js +39 -34
  30. package/lib/commands/rebase.js +32 -21
  31. package/lib/commands/repair-handoff.js +21 -18
  32. package/lib/commands/resolve-conflict.js +5 -2
  33. package/lib/commands/review.js +1 -0
  34. package/lib/commands/setup-review.js +1 -0
  35. package/lib/commands/stats-backfill.js +50 -41
  36. package/lib/commands/stats.js +653 -204
  37. package/lib/commands/status.js +33 -28
  38. package/lib/core/fmt.js +58 -15
  39. package/lib/core/git.js +7 -5
  40. package/lib/core/gitignore.js +6 -4
  41. package/lib/core/mission-utils.js +161 -88
  42. package/lib/core/nels.js +199 -0
  43. package/lib/core/persistent-data-migration.js +55 -23
  44. package/lib/core/product-config.js +44 -23
  45. package/lib/core/runtime-matrix.js +15 -2
  46. package/lib/core/spawn-tee.js +40 -13
  47. package/lib/core/state-map.js +32 -13
  48. package/lib/core/storage.js +19 -5
  49. package/lib/core/subagent-limit.js +28 -0
  50. package/lib/core/verification.js +29 -12
  51. package/lib/review/rebase.js +11 -3
  52. package/lib/review/review-adapter.js +93 -6
  53. package/lib/review/review-artifacts.js +89 -23
  54. package/lib/review/review-commands.js +139 -45
  55. package/lib/review/review-events.js +103 -52
  56. package/lib/review/review-loop.js +117 -87
  57. package/lib/review/review-polling.js +25 -5
  58. package/lib/review/review-prompts.js +36 -5
  59. package/lib/review/review-state.js +39 -15
  60. package/lib/tools/backlog.js +162 -71
  61. package/lib/tools/forgejo.js +290 -129
  62. package/lib/tools/gatekeeper.js +15 -0
  63. package/lib/tools/redgreen.js +220 -0
  64. package/lib/tools/sessions.js +12 -5
  65. package/lib/tools/setup-review.js +170 -70
  66. package/package.json +11 -3
  67. package/prompts/draft.md +6 -0
  68. package/templates/mission-scaffold.md +1 -1
@@ -93,7 +93,7 @@ const VALID_VERDICTS = Object.freeze([
93
93
  */
94
94
  function reviewEventsDir(slug, rootDir = process.cwd()) {
95
95
  const missionDir = findMissionDir(slug, rootDir);
96
- if (!missionDir) return null;
96
+ if (!missionDir) {return null;}
97
97
  const dir = path.join(missionDir, 'review-events');
98
98
  if (!fs.existsSync(dir)) {
99
99
  fs.mkdirSync(dir, { recursive: true });
@@ -118,9 +118,9 @@ function reviewEventsDir(slug, rootDir = process.cwd()) {
118
118
  * @param {string} [rootDir] - Root directory to resolve from
119
119
  * @returns {string|null} - Absolute file path, or null if mission dir not found
120
120
  */
121
- function eventFilePath(slug, eventType, round, actor, timestamp = null, rootDir = process.cwd()) {
121
+ function eventFilePath(slug, eventType, round, actor, /** @type {string|null} */ timestamp = null, rootDir = process.cwd()) {
122
122
  const eventsDir = reviewEventsDir(slug, rootDir);
123
- if (!eventsDir) return null;
123
+ if (!eventsDir) {return null;}
124
124
 
125
125
  const ts = timestamp || generateEventTimestamp();
126
126
  const sanitizedActor = sanitizeFilename(actor);
@@ -161,7 +161,7 @@ function generateEventTimestamp() {
161
161
  * @returns {string} - Filesystem-safe string
162
162
  */
163
163
  function sanitizeFilename(str) {
164
- if (!str) return 'unknown';
164
+ if (!str) {return 'unknown';}
165
165
  return String(str)
166
166
  .toLowerCase()
167
167
  // Replace @ and . with empty string first (common in emails/agent names)
@@ -179,8 +179,8 @@ function sanitizeFilename(str) {
179
179
  * @param {string} eventType - The type to validate
180
180
  * @returns {boolean} - True if valid
181
181
  */
182
- function isValidEventType(eventType) {
183
- return ALL_EVENT_TYPES.includes(eventType);
182
+ function isValidEventType(/** @type {string} */ eventType) {
183
+ return ALL_EVENT_TYPES.includes(/** @type {typeof ALL_EVENT_TYPES[number]} */ (eventType));
184
184
  }
185
185
 
186
186
  /**
@@ -206,8 +206,8 @@ function isValidVerdict(verdict) {
206
206
  * @param {string} eventType - The event type
207
207
  * @returns {boolean} - True if mirrored
208
208
  */
209
- function shouldMirrorToProvider(eventType) {
210
- return MIRRORED_EVENT_TYPES.has(eventType);
209
+ function shouldMirrorToProvider(/** @type {string} */ eventType) {
210
+ return MIRRORED_EVENT_TYPES.has(/** @type {typeof MIRRORED_EVENT_TYPES extends Set<infer T> ? T : string} */ (eventType));
211
211
  }
212
212
 
213
213
  // -------- Event Structure --------
@@ -221,8 +221,15 @@ function shouldMirrorToProvider(eventType) {
221
221
  * @param {Object} [metadata] - Additional metadata (round, phase, actor, etc.)
222
222
  * @returns {Object} - Normalized event structure
223
223
  */
224
+ /**
225
+ * @param {string} content
226
+ * @param {string} eventType
227
+ * @param {{[key: string]: any}} [metadata]
228
+ * @returns {{eventType: string, timestamp: string, content: string}}
229
+ */
224
230
  function normalizeEventContent(content, eventType, metadata = {}) {
225
- const payload = { ...metadata };
231
+ /** @type {{eventType: string, timestamp: string, content: string, [key: string]: any}} */
232
+ const payload = { eventType, timestamp: '', content, ...metadata };
226
233
 
227
234
  // Ensure we have the base metadata
228
235
  payload.eventType = eventType;
@@ -241,6 +248,10 @@ function normalizeEventContent(content, eventType, metadata = {}) {
241
248
  * @param {Object} event - Normalized event structure
242
249
  * @returns {string} - Frontmatter block
243
250
  */
251
+ /**
252
+ * @param {{eventType: string, timestamp: string, round?: number, phase?: string, actor?: string, slug?: string, disposition?: string, verdict?: string, fixedItems?: string[], pushedBackItems?: string[], parkedItems?: string[], blockedReason?: string, followUpReference?: string}} event
253
+ * @returns {string}
254
+ */
244
255
  function buildEventFrontmatter(event) {
245
256
  const lines = [
246
257
  '---',
@@ -319,7 +330,7 @@ function hasWorkflowFooter(body) {
319
330
  * @param {Object} comment - Comment object with body field
320
331
  * @returns {string|null} - 'human_note' if human, null if workflow-generated
321
332
  */
322
- function classifyComment(comment) {
333
+ function classifyComment(/** @type {{[key: string]: any}} */ comment) {
323
334
  const body = comment.body || '';
324
335
  // Workflow-generated comments have the metadata footer
325
336
  if (hasWorkflowFooter(body)) {
@@ -349,6 +360,12 @@ function classifyComment(comment) {
349
360
  * @param {Function} [options.error] - Error function
350
361
  * @returns {Object} - { ok: boolean, created: Array, skipped: Array }
351
362
  */
363
+ /**
364
+ * @param {string} slug
365
+ * @param {string} actor
366
+ * @param {{getCommentsFn?: Function, createEventFn?: Function, readTokenFn?: Function, reviewIdentity?: string|null, forgejoUser?: string|null, worktree?: string, log?: Function, error?: Function}} [options]
367
+ * @returns {Promise<{ok: boolean, created: Array<any>, skipped: Array<any>, error?: string}>}
368
+ */
352
369
  async function consumeHumanNotes(slug, actor, options = {}) {
353
370
  const {
354
371
  getCommentsFn,
@@ -440,9 +457,13 @@ async function consumeHumanNotes(slug, actor, options = {}) {
440
457
  * @param {Object} event - Normalized event structure
441
458
  * @returns {string} - Complete file content
442
459
  */
460
+ /**
461
+ * @param {{content?: string, round?: number, phase?: string, slug?: string, eventType?: string, timestamp?: string}} event
462
+ * @returns {string}
463
+ */
443
464
  function renderEventFile(event) {
444
- const frontmatter = buildEventFrontmatter(event);
445
- const content = event.content || '';
465
+ const frontmatter = buildEventFrontmatter(/** @type {{eventType: string, timestamp: string, content?: string, round?: number, phase?: string, slug?: string}} */ (event));
466
+ const content = /** @type {string} */ (event.content || '');
446
467
 
447
468
  // If the content already has a metadata footer, don't add another
448
469
  const hasFooter = /\n\n---\n`\[workflow-round:\d+, workflow-phase:[^\]]+\]`/.test(content);
@@ -453,7 +474,7 @@ function renderEventFile(event) {
453
474
 
454
475
  // Add footer if we have round and phase
455
476
  const footer = event.round !== undefined && event.phase !== undefined
456
- ? buildEventFooter(event.slug, event.round, event.phase)
477
+ ? buildEventFooter(/** @type {string} */ (event.slug), event.round, event.phase)
457
478
  : '';
458
479
 
459
480
  return `${frontmatter}\n\n${content}${footer}`;
@@ -473,9 +494,9 @@ function renderEventFile(event) {
473
494
  * @param {string} [params.actor] - Agent family or 'human'
474
495
  * @param {string} [params.disposition] - For implementer disposition events
475
496
  * @param {string} [params.verdict] - For reviewer outcome events
476
- * @param {Array} [params.fixedItems] - For implementer round summary
477
- * @param {Array} [params.pushedBackItems] - For implementer round summary
478
- * @param {Array} [params.parkedItems] - For implementer round summary
497
+ * @param {Array<any>} [params.fixedItems] - For implementer round summary
498
+ * @param {Array<any>} [params.pushedBackItems] - For implementer round summary
499
+ * @param {Array<any>} [params.parkedItems] - For implementer round summary
479
500
  * @param {string} [params.blockedReason] - For BLOCKED events
480
501
  * @param {string} [params.followUpReference] - For PARKED_FOLLOWUP events
481
502
  * @param {string} [params.timestamp] - Custom timestamp
@@ -485,6 +506,13 @@ function renderEventFile(event) {
485
506
  * @param {Function} [options.gitFn] - Git function override for testing
486
507
  * @returns {Object} - { ok: boolean, path: string|null, error: string|null }
487
508
  */
509
+ /**
510
+ * @param {string} slug
511
+ * @param {string} eventType
512
+ * @param {{content: string, round?: number, phase?: string, actor?: string, disposition?: string, verdict?: string, fixedItems?: Array<any>, pushedBackItems?: Array<any>, parkedItems?: Array<any>, blockedReason?: string, followUpReference?: string, timestamp?: string}} params
513
+ * @param {{skipGit?: boolean, gitFn?: Function, log?: Function, error?: Function, worktree?: string, allowMissingRequiredFields?: boolean}} [options]
514
+ * @returns {{ok: boolean, path: string|null, error?: string|null, event?: Object}}
515
+ */
488
516
  function createEvent(slug, eventType, params, options = {}) {
489
517
  const {
490
518
  skipGit = false,
@@ -605,6 +633,14 @@ function createEvent(slug, eventType, params, options = {}) {
605
633
  * @param {Object} [options] - Options (same as createEvent)
606
634
  * @returns {Object} - { ok: boolean, path: string|null, importedFrom: string|null, error: string|null }
607
635
  */
636
+ /**
637
+ * @param {string} slug
638
+ * @param {string} artifactName
639
+ * @param {string} eventType
640
+ * @param {{round?: number, phase?: string, actor?: string}} [params]
641
+ * @param {{tmpDir?: string, log?: Function, error?: Function, worktree?: string}} [options]
642
+ * @returns {{ok: boolean, path: string|null, importedFrom: string|null, event?: Object, skipped?: boolean, legacyPath?: string, error?: string}}
643
+ */
608
644
  function importLegacyArtifact(slug, artifactName, eventType, params = {}, options = {}) {
609
645
  const {
610
646
  tmpDir = os.tmpdir(),
@@ -612,8 +648,7 @@ function importLegacyArtifact(slug, artifactName, eventType, params = {}, option
612
648
  error = fmt.log.plainError,
613
649
  worktree
614
650
  } = options;
615
-
616
- const rootDir = worktree || resolveWorktree(slug) || process.cwd();
651
+
617
652
  const legacyPath = legacyArtifactPath(slug, artifactName, tmpDir);
618
653
 
619
654
  // Check if legacy file exists
@@ -626,9 +661,9 @@ function importLegacyArtifact(slug, artifactName, eventType, params = {}, option
626
661
  let content;
627
662
  try {
628
663
  content = fs.readFileSync(legacyPath, 'utf8');
629
- } catch (err) {
630
- error(fmt.status('FAIL', `Failed to read legacy artifact: ${err.message}`));
631
- return { ok: false, path: null, importedFrom: legacyPath, error: err.message };
664
+ } catch (/** @type {unknown} */ err) {
665
+ error(fmt.status('FAIL', `Failed to read legacy artifact: ${/** @type {Error} */(err).message}`));
666
+ return { ok: false, path: null, importedFrom: legacyPath, error: /** @type {Error} */ (err).message };
632
667
  }
633
668
 
634
669
  // Create the normalized event
@@ -637,14 +672,14 @@ function importLegacyArtifact(slug, artifactName, eventType, params = {}, option
637
672
  content,
638
673
  ...params,
639
674
  // Default actor from artifact name
640
- actor: params.actor || inferActorFromArtifact(artifactName)
641
- }, { ...options, worktree: rootDir, allowMissingRequiredFields: true });
675
+ actor: params.actor || (/** @type {Function} */ (inferActorFromArtifact))()
676
+ }, { ...options, worktree: worktree || undefined, allowMissingRequiredFields: true });
642
677
 
643
678
  if (!result.ok) {
644
- return { ok: false, path: null, importedFrom: legacyPath, error: result.error };
679
+ return { ok: false, path: null, importedFrom: legacyPath, error: /** @type {string | undefined} */ (result.error) };
645
680
  }
646
681
 
647
- log(fmt.status('PASS', `Imported legacy artifact ${artifactName} to ${path.basename(result.path)}`));
682
+ log(fmt.status('PASS', `Imported legacy artifact ${artifactName} to ${path.basename(/** @type {string} */ (result.path))}`));
648
683
 
649
684
  return {
650
685
  ok: true,
@@ -659,7 +694,7 @@ function importLegacyArtifact(slug, artifactName, eventType, params = {}, option
659
694
  * Infer actor from legacy artifact name (best guess).
660
695
  * In practice, the actor should be passed explicitly from the workflow context.
661
696
  */
662
- function inferActorFromArtifact(artifactName) {
697
+ function inferActorFromArtifact() {
663
698
  // Can't reliably infer from artifact name alone
664
699
  return 'unknown';
665
700
  }
@@ -687,10 +722,14 @@ const LEGACY_ARTIFACT_TO_EVENT_TYPE = Object.freeze({
687
722
  * @param {Object} [options] - Options
688
723
  * @returns {Object} - { ok: boolean, imported: Array, errors: Array }
689
724
  */
725
+ /**
726
+ * @param {string} slug
727
+ * @param {{tmpDir?: string, error?: Function, worktree?: string, log?: Function}} [options]
728
+ * @returns {{ok: boolean, imported: Array<any>, errors: Array<any>}}
729
+ */
690
730
  function importAllLegacyArtifacts(slug, options = {}) {
691
731
  const {
692
732
  tmpDir = os.tmpdir(),
693
- log = fmt.log.plain,
694
733
  error = fmt.log.plainError,
695
734
  worktree
696
735
  } = options;
@@ -705,6 +744,7 @@ function importAllLegacyArtifacts(slug, options = {}) {
705
744
  const phase = state ? state.phase : 'reviewing';
706
745
 
707
746
  // Track which legacy files exist
747
+ /** @type {{[key: string]: string|null}} */
708
748
  const legacyFiles = {};
709
749
  for (const artifactName of Object.keys(LEGACY_ARTIFACT_TO_EVENT_TYPE)) {
710
750
  const legacyPath = legacyArtifactPath(slug, artifactName, tmpDir);
@@ -724,14 +764,15 @@ function importAllLegacyArtifacts(slug, options = {}) {
724
764
  if (fs.existsSync(verdictPath)) {
725
765
  verdictContent = fs.readFileSync(verdictPath, 'utf8').trim();
726
766
  }
727
- } catch (err) {
728
- error(fmt.status('WARN', `Failed to read verdict file: ${err.message}`));
767
+ } catch (/** @type {unknown} */ err) {
768
+ error(fmt.status('WARN', `Failed to read verdict file: ${/** @type {Error} */(err).message}`));
729
769
  }
730
770
  }
731
771
 
732
772
  // Import review-outcome.md with verdict as metadata (only include verdict if valid)
773
+ /** @type {{round: number, phase: string, actor: string, verdict?: string}} */
733
774
  const outcomeParams = { round, phase, actor: 'legacy' };
734
- if (verdictContent && isValidVerdict(verdictContent)) {
775
+ if (verdictContent && isValidVerdict(/** @type {string} */ (verdictContent))) {
735
776
  outcomeParams.verdict = verdictContent;
736
777
  }
737
778
 
@@ -768,12 +809,13 @@ function importAllLegacyArtifacts(slug, options = {}) {
768
809
  let verdictContent = null;
769
810
  try {
770
811
  verdictContent = fs.readFileSync(verdictPath, 'utf8').trim();
771
- } catch (err) {
772
- error(fmt.status('WARN', `Failed to read verdict file: ${err.message}`));
812
+ } catch (/** @type {unknown} */ err) {
813
+ error(fmt.status('WARN', `Failed to read verdict file: ${/** @type {Error} */(err).message}`));
773
814
  }
774
815
 
816
+ /** @type {{round: number, phase: string, actor: string, verdict?: string}} */
775
817
  const outcomeParams = { round, phase, actor: 'legacy' };
776
- if (verdictContent && isValidVerdict(verdictContent)) {
818
+ if (verdictContent && isValidVerdict(/** @type {string} */ (verdictContent))) {
777
819
  outcomeParams.verdict = verdictContent;
778
820
  }
779
821
 
@@ -797,16 +839,16 @@ function importAllLegacyArtifacts(slug, options = {}) {
797
839
  continue;
798
840
  }
799
841
 
800
- const result = importLegacyArtifact(slug, artifactName, LEGACY_ARTIFACT_TO_EVENT_TYPE[artifactName],
842
+ const result = importLegacyArtifact(slug, artifactName, /** @type {string} */ (LEGACY_ARTIFACT_TO_EVENT_TYPE[/** @type {keyof typeof LEGACY_ARTIFACT_TO_EVENT_TYPE} */ (artifactName)]),
801
843
  { round, phase, actor: 'legacy' },
802
844
  { ...options, worktree: rootDir });
803
845
 
804
846
  if (result.ok) {
805
847
  if (result.path) {
806
- imported.push({ artifactName, eventType: LEGACY_ARTIFACT_TO_EVENT_TYPE[artifactName], path: result.path, legacyPath: result.legacyPath });
848
+ imported.push({ artifactName, eventType: LEGACY_ARTIFACT_TO_EVENT_TYPE[/** @type {keyof typeof LEGACY_ARTIFACT_TO_EVENT_TYPE} */ (artifactName)], path: result.path, legacyPath: result.legacyPath });
807
849
  }
808
850
  } else {
809
- errors.push({ artifactName, eventType: LEGACY_ARTIFACT_TO_EVENT_TYPE[artifactName], error: result.error });
851
+ errors.push({ artifactName, eventType: LEGACY_ARTIFACT_TO_EVENT_TYPE[/** @type {keyof typeof LEGACY_ARTIFACT_TO_EVENT_TYPE} */ (artifactName)], error: result.error });
810
852
  }
811
853
  }
812
854
 
@@ -823,14 +865,18 @@ function importAllLegacyArtifacts(slug, options = {}) {
823
865
  * @param {string} [options.rootDir] - Root directory
824
866
  * @param {Function} [options.readdirSync] - readdirSync override for testing
825
867
  * @param {Function} [options.readFileSync] - readFileSync override for testing
826
- * @returns {Array} - Array of parsed event objects
868
+ * @returns {Array<any>} - Array of parsed event objects
869
+ */
870
+ /**
871
+ * @param {string} slug
872
+ * @param {{rootDir?: string, readdirSync?: Function, readFileSync?: Function, error?: Function}} [options]
873
+ * @returns {Array<any>}
827
874
  */
828
875
  function readAllEvents(slug, options = {}) {
829
876
  const {
830
877
  rootDir = process.cwd(),
831
878
  readdirSync = fs.readdirSync,
832
879
  readFileSync = fs.readFileSync,
833
- log = fmt.log.plain,
834
880
  error = fmt.log.plainError
835
881
  } = options;
836
882
 
@@ -844,21 +890,21 @@ function readAllEvents(slug, options = {}) {
844
890
 
845
891
  try {
846
892
  files = readdirSync(eventsDir);
847
- } catch (err) {
848
- error(fmt.status('WARN', `Failed to read review-events directory: ${err.message}`));
893
+ } catch (/** @type {unknown} */ err) {
894
+ error(fmt.status('WARN', `Failed to read review-events directory: ${/** @type {Error} */(err).message}`));
849
895
  return [];
850
896
  }
851
897
 
852
898
  for (const filename of files) {
853
- if (!filename.endsWith('.md')) continue;
899
+ if (!filename.endsWith('.md')) {continue;}
854
900
 
855
901
  const filePath = path.join(eventsDir, filename);
856
902
  let content;
857
903
 
858
904
  try {
859
905
  content = readFileSync(filePath, 'utf8');
860
- } catch (err) {
861
- error(fmt.status('WARN', `Failed to read event file ${filename}: ${err.message}`));
906
+ } catch (/** @type {unknown} */ err) {
907
+ error(fmt.status('WARN', `Failed to read event file ${filename}: ${/** @type {Error} */(err).message}`));
862
908
  continue;
863
909
  }
864
910
 
@@ -886,6 +932,11 @@ function readAllEvents(slug, options = {}) {
886
932
  * @param {string} filePath - File path (for fallback metadata)
887
933
  * @returns {Object|null} - Parsed event or null
888
934
  */
935
+ /**
936
+ * @param {string} content
937
+ * @param {string} filePath
938
+ * @returns {{[key: string]: any}|null}
939
+ */
889
940
  function parseEventFile(content, filePath) {
890
941
  // Try to parse frontmatter
891
942
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
@@ -900,9 +951,9 @@ function parseEventFile(content, filePath) {
900
951
  // Parse YAML-like frontmatter
901
952
  const lines = frontmatterText.split('\n');
902
953
  for (const line of lines) {
903
- if (!line.trim()) continue;
954
+ if (!line.trim()) {continue;}
904
955
  const colonIdx = line.indexOf(':');
905
- if (colonIdx === -1) continue;
956
+ if (colonIdx === -1) {continue;}
906
957
 
907
958
  const key = line.slice(0, colonIdx).trim().toLowerCase();
908
959
  const value = line.slice(colonIdx + 1).trim();
@@ -910,21 +961,21 @@ function parseEventFile(content, filePath) {
910
961
  // Handle quoted strings
911
962
  if ((value.startsWith('"') && value.endsWith('"')) ||
912
963
  (value.startsWith("'") && value.endsWith("'"))) {
913
- metadata[key] = value.slice(1, -1);
964
+ /** @type {any} */ (metadata)[key] = value.slice(1, -1);
914
965
  }
915
966
  // Handle JSON arrays
916
967
  else if (value.startsWith('[') && value.endsWith(']')) {
917
968
  try {
918
- metadata[key] = JSON.parse(value);
969
+ /** @type {any} */ (metadata)[key] = JSON.parse(value);
919
970
  } catch (_) {
920
- metadata[key] = value;
971
+ /** @type {any} */ (metadata)[key] = value;
921
972
  }
922
973
  }
923
974
  // Handle booleans and numbers
924
- else if (value === 'true') metadata[key] = true;
925
- else if (value === 'false') metadata[key] = false;
926
- else if (!isNaN(value)) metadata[key] = Number(value);
927
- else metadata[key] = value;
975
+ else if (value === 'true') {/** @type {any} */ (metadata)[key] = true;}
976
+ else if (value === 'false') {/** @type {any} */ (metadata)[key] = false;}
977
+ else if (value !== '' && !isNaN(Number(value))) {/** @type {any} */ (metadata)[key] = Number(value);}
978
+ else {/** @type {any} */ (metadata)[key] = value;}
928
979
  }
929
980
  }
930
981