@open-product-primer/cli 0.7.1 → 0.10.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.
@@ -272,6 +272,7 @@ exports.CLAUDE_SKILLS = {
272
272
  'oprim-criteria': criteriaSkill(),
273
273
  'oprim-review': reviewSkill(),
274
274
  'oprim-archive': archiveSkill(),
275
+ 'oprim-sequence': oprimSequenceSkill(),
275
276
  };
276
277
  // ─── Claude command wrappers (thin, invoke skill) ────────────────────────────
277
278
  exports.CLAUDE_COMMANDS = {
@@ -289,7 +290,7 @@ exports.CURSOR_SKILLS = {
289
290
  // ─── Cursor command files (full inline — no Skill tool in Cursor) ────────────
290
291
  exports.CURSOR_COMMANDS = {
291
292
  'oprim-promote.md': cursorWrapper('oprim-promote', 'Promote a prioritized bet to an OpenSpec change', promoteContent()),
292
- 'oprim-sequence.md': cursorWrapper('oprim-sequence', 'Validate and update the primer sequencing board', sequenceContent()),
293
+ 'oprim-sequence.md': cursorWrapper('oprim-sequence', 'Validate and update the primer sequencing board', sequenceInlineContent()),
293
294
  'oprim-pdr.md': cursorWrapper('oprim-pdr', 'Create a new Product Decision Record with auto-assigned ID', pdrInlineContent()),
294
295
  'oprim-bet.md': cursorWrapper('oprim-bet', 'Create a new bet decision and register it on the sequencing board', betInlineContent()),
295
296
  'oprim-criteria.md': cursorWrapper('oprim-criteria', 'Create or append to a criteria.yaml contract for a bet', criteriaInlineContent()),
@@ -327,6 +328,8 @@ description: Create a new Product Decision Record in oprim/decisions/ with auto-
327
328
 
328
329
  Create a new Product Decision Record (PDR) in \`oprim/decisions/\`.
329
330
 
331
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
332
+
330
333
  ## Steps
331
334
 
332
335
  ### 1. Get the decision title
@@ -387,6 +390,8 @@ description: Create a new bet directory and bet-decision artifact in oprim/bets/
387
390
 
388
391
  Create a new bet in \`oprim/bets/\` and register it on the sequencing board.
389
392
 
393
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
394
+
390
395
  ## Steps
391
396
 
392
397
  ### 1. Get the bet title
@@ -406,7 +411,10 @@ After receiving the title, validate: if fewer than 4 words OR fewer than 25 char
406
411
  - If "y": proceed with the original title
407
412
 
408
413
  ### 2. Assign the next BET ID
409
- Scan \`oprim/bets/\` for directories matching \`BET-(\\d+)\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none.
414
+ Scan both \`oprim/bets/\` and \`oprim/bets/archived/\` for directories whose names match \`BET-(\\d+)(-[^/]*)?\` (handles both \`BET-NNN/\` and \`BET-NNN-<slug>/\`). Extract the numeric part from each match. Assign max+1, zero-padded to 3 digits. Default \`001\` if none found in either location.
415
+
416
+ ### 2b. Derive the slug
417
+ From the bet title: lowercase all characters, replace any character that is not a letter or digit with a hyphen, collapse consecutive hyphens to one, strip leading/trailing hyphens, truncate to 40 characters at the last hyphen boundary. This becomes \`<slug>\`. Example: "Add title slugs to bet directories for scannability" → \`add-title-slugs-to-bet-dirs-for\`.
410
418
 
411
419
  ### 3. Check sequence.yaml exists
412
420
  If \`oprim/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
@@ -414,7 +422,7 @@ If \`oprim/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
414
422
  ### 4. Gather content
415
423
  Ask: Decision (Build now / Defer / Kill, default Build now), Owner, Review date (YYYY-MM-DD), Why now, Alternatives considered, Expected outcomes (metric: baseline → target in timeframe), Kill criteria / rollback trigger, PDR links (optional).
416
424
 
417
- ### 5. Write oprim/bets/BET-NNN/bet-decision.md
425
+ ### 5. Write oprim/bets/BET-NNN-<slug>/bet-decision.md
418
426
  \`\`\`
419
427
  # Decision: BET-NNN <title>
420
428
  <!-- Naming tip: verb + object [for context] — e.g. "Improve bet naming for scannability" not "Naming" -->
@@ -454,7 +462,7 @@ Read → parse YAML → append → write back (2-space indentation):
454
462
 
455
463
  ### 7. Prompt for optional discovery scaffolding
456
464
  Ask: "Do you want to scaffold a discovery.md now? (y/N)"
457
- - If "y": write \`oprim/bets/BET-NNN/discovery.md\` from the discovery template (same structure as \`oprim/templates/discovery.md\`).
465
+ - If "y": write \`oprim/bets/BET-NNN-<slug>/discovery.md\` from the discovery template (same structure as \`oprim/templates/discovery.md\`).
458
466
  - If "n" or Enter: skip silently.
459
467
 
460
468
  ### 8. Report what was created
@@ -468,6 +476,8 @@ description: Create or append to a criteria.yaml contract for a bet, with struct
468
476
 
469
477
  Create or append to \`oprim/bets/BET-NNN/criteria.yaml\`.
470
478
 
479
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
480
+
471
481
  ## Steps
472
482
 
473
483
  ### 1. Identify the bet
@@ -516,11 +526,13 @@ If not: create with \`metrics:\` list.
516
526
  function archiveSkill() {
517
527
  return `---
518
528
  name: oprim-archive
519
- description: Archive a completed bet — moves it to oprim/bets/archived/BET-NNN/ and removes its sequence.yaml entry
529
+ description: Archive a completed bet — moves it to oprim/bets/archived/ and removes its sequence.yaml entry
520
530
  ---
521
531
 
522
532
  Archive a completed bet by moving it to \`oprim/bets/archived/\` and removing it from \`sequence.yaml\`.
523
533
 
534
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
535
+
524
536
  ## Steps
525
537
 
526
538
  ### 1. Get the bet ID
@@ -531,11 +543,19 @@ If not provided, ask: "Which bet ID would you like to archive? (e.g., BET-005)"
531
543
 
532
544
  Normalize the input: accept \`bet-005\`, \`005\`, \`5\`, or \`BET-005\` — always treat as \`BET-NNN\` zero-padded to 3 digits.
533
545
 
534
- ### 2. Check the bet directory exists
546
+ ### 2. Resolve the bet directory
547
+
548
+ Look for the bet directory in \`oprim/bets/\` using two patterns:
549
+ 1. Exact match: \`oprim/bets/BET-NNN/\` (legacy non-slug format)
550
+ 2. Slug variant: any directory starting with \`BET-NNN-\` (e.g., \`BET-NNN-<slug>/\`)
535
551
 
536
- Check whether \`oprim/bets/BET-NNN/\` exists.
552
+ Use whichever pattern matches. Call this the **resolved directory name**.
553
+
554
+ If multiple directories match (e.g., both \`BET-NNN/\` and \`BET-NNN-slug/\` exist):
555
+ - Report: "Ambiguous: found multiple directories for BET-NNN: [list them]. Please archive manually."
556
+ - Stop.
537
557
 
538
- If not found:
558
+ If neither pattern matches:
539
559
  - Report: "Bet BET-NNN was not found in oprim/bets/. Nothing was changed."
540
560
  - Stop.
541
561
 
@@ -565,9 +585,9 @@ Create the archive subfolder if it doesn't exist:
565
585
  mkdir -p oprim/bets/archived
566
586
  \`\`\`
567
587
 
568
- Move the directory:
588
+ Move the resolved directory:
569
589
  \`\`\`bash
570
- mv oprim/bets/BET-NNN oprim/bets/archived/BET-NNN
590
+ mv oprim/bets/<resolved-dir> oprim/bets/archived/<resolved-dir>
571
591
  \`\`\`
572
592
 
573
593
  ### 5. Remove the bet entry from sequence.yaml
@@ -580,7 +600,7 @@ Read \`oprim/sequence.yaml\`, parse it, and remove the entry with \`id: BET-NNN\
580
600
  ## Bet Archived
581
601
 
582
602
  **Bet:** BET-NNN
583
- **Archived to:** oprim/bets/archived/BET-NNN/
603
+ **Archived to:** oprim/bets/archived/<resolved-dir>/
584
604
  **Removed from sequence.yaml:** ✓
585
605
 
586
606
  The bet is preserved in full at the archive location.
@@ -617,6 +637,97 @@ List each match on its own line, then return — the invoking skill continues to
617
637
  If no PDRs match: exit silently — produce no output.
618
638
  `;
619
639
  }
640
+ function oprimSequenceSkill() {
641
+ return `---
642
+ name: oprim-sequence
643
+ description: Validate and update the primer sequencing board — triage mode computes board health and surfaces specific suggestions; seeded mode targets a specific context
644
+ ---
645
+
646
+ Manage the primer sequencing board in \`oprim/sequence.yaml\`.
647
+
648
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
649
+
650
+ ## Entry modes
651
+
652
+ **Triage mode** — no intention provided: reads board state, computes health, surfaces specific actionable suggestions.
653
+ **Seeded mode** — context pre-provided (e.g. from a lifecycle hook): skips full triage and targets the most relevant move for that context.
654
+
655
+ ## Steps
656
+
657
+ ### 1. Determine mode
658
+
659
+ If lifecycle context was provided as an argument or pre-seeded in the conversation (e.g. \`bet-created\`, \`bet-promoted\`, or a specific bet ID was just archived), enter **Seeded mode** — go to Step 2B.
660
+
661
+ Otherwise, enter **Triage mode** — go to Step 2A.
662
+
663
+ ### 2A. Triage mode — compute board health
664
+
665
+ Read \`oprim/sequence.yaml\`. Compute:
666
+
667
+ - **WIP utilization**: count entries in \`now\` vs \`wip_limits.now\`
668
+ - **WIP violations**: entries in \`now\` that exceed the WIP limit
669
+ - **Blocked Now bets**: entries in \`now\` whose \`blocked_by\` list contains a bet ID still present in any active lane (now/next/later/backlog)
670
+ - **Ready-to-pull bets**: entries in \`next\` whose \`blocked_by\` list is empty or all resolved (each blocked_by ID is absent from all active lanes)
671
+ - **PDR gaps**: entries in any lane whose \`requires_pdrs\` list contains a PDR ID not found in \`oprim/decisions/\`
672
+
673
+ Surface **ranked suggestions**, most urgent first:
674
+ 1. WIP violations → name each excess bet, suggest deferring to \`next\` or \`later\`
675
+ 2. Blocked Now bets → name the bet and its unresolved blocker, suggest deferring until blocker resolves
676
+ 3. Open Now slot (count < \`wip_limits.now\`) with a ready Next bet → name a specific bet to pull into \`now\`
677
+ 4. PDR gaps → name the bet and missing PDR, suggest creating it first
678
+
679
+ Each suggestion must name the exact bet ID, current lane, target lane, and reason.
680
+
681
+ If the board is healthy (no violations, no ready moves): report current WIP utilization and state the board is healthy. Offer to move something anyway if the user wants.
682
+
683
+ After surfacing suggestions, ask: "Which move would you like to make?"
684
+
685
+ ### 2B. Seeded mode — target specific context
686
+
687
+ Use the provided context to jump to the most relevant suggestion:
688
+
689
+ - \`bet-created\`: A new bet just landed in backlog. Check if \`now\` has an open slot and \`next\` has a ready bet to pull. If so, suggest the specific bet to pull. Otherwise, confirm the new bet is in backlog and the board looks healthy.
690
+ - \`bet-promoted\`: A bet was just promoted to an OpenSpec change. Verify the bet is still correctly sequenced and surface any resequencing action the promotion warrants.
691
+ - Bet archived (e.g. "BET-011 was archived"): Check if \`now\` dropped below \`wip_limits.now\`. If so, find the most ready bet in \`next\` and suggest pulling it.
692
+
693
+ If no relevant move is found for the provided context, fall back to Triage mode (Step 2A).
694
+
695
+ ### 3. Validate the requested move
696
+
697
+ Before executing any move, verify all three constraints:
698
+
699
+ **WIP limit**: Moving a bet to \`now\` must not push the count above \`wip_limits.now\`. If it would, explain the violation and suggest deferring an existing \`now\` bet first.
700
+
701
+ **Blocker resolution**: Moving a bet to \`now\` requires its \`blocked_by\` list to be empty or all resolved (IDs absent from all active lanes). If unresolved, name each unresolved blocker.
702
+
703
+ **PDR preconditions**: Moving a bet to \`now\` requires all \`requires_pdrs\` entries to exist as files in \`oprim/decisions/\`. If any are missing, name them and suggest creating them first.
704
+
705
+ If the move is invalid: explain which constraint failed and suggest the nearest valid alternative. Do not proceed to Step 4.
706
+
707
+ ### 4. Preview and confirm
708
+
709
+ Show the exact YAML change before writing. Name the entry that will move: its \`id\`, \`title\`, source lane, and target lane.
710
+
711
+ \`\`\`
712
+ Before: BET-005 is in next (now: 1/2 slots filled)
713
+ After: BET-005 moves to now (now: 2/2 slots filled)
714
+ \`\`\`
715
+
716
+ Ask: "Apply this change? (y/N)"
717
+ - If "n" or Enter: stop, no changes made.
718
+ - If "y": proceed to Step 5.
719
+
720
+ ### 5. Write sequence.yaml
721
+
722
+ Read \`oprim/sequence.yaml\`. Remove the bet entry from its current lane. Insert it into the target lane. Write back with 2-space indentation. Do not modify any other entries.
723
+
724
+ ### 6. Regenerate view
725
+
726
+ Run \`node oprim/scripts/generate-sequence-view.js\` from the project root to update \`oprim/sequence-view.md\`.
727
+
728
+ ### 7. Report what was done
729
+ `;
730
+ }
620
731
  function archiveCommandContent() {
621
732
  return `Use the Skill tool to invoke the \`oprim-archive\` skill.`;
622
733
  }
@@ -628,6 +739,8 @@ description: Create a KPI review artifact for a completed bet, pre-filled from c
628
739
 
629
740
  Create a KPI review in \`oprim/reviews/\`.
630
741
 
742
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
743
+
631
744
  ## Steps
632
745
 
633
746
  ### 1. Identify the bet
@@ -703,54 +816,88 @@ function hooksConfig(framework) {
703
816
  }, null, 2) + '\n');
704
817
  }
705
818
  const ON_PROMPT_SUBMIT_HOOK = `#!/usr/bin/env bash
706
- # UserPromptSubmit hook: detects archive slash commands and sets a pending flag.
819
+ # UserPromptSubmit hook: detects lifecycle slash commands and sets pending flags.
707
820
 
708
- config_file=".claude/hooks/config.json"
709
- flag_file=".claude/hooks/.archive-pending"
821
+ archive_flag=".claude/hooks/.archive-pending"
822
+ nudge_flag=".claude/hooks/.sequence-nudge"
710
823
 
711
- framework="openspec"
712
- if [ -f "$config_file" ]; then
713
- framework=$(python3 -c "import sys,json; print(json.load(open('$config_file')).get('framework','openspec'))" 2>/dev/null || echo "openspec")
714
- fi
824
+ input=$(cat)
715
825
 
716
- prompt=$(cat | python3 -c "import sys,json; print(json.load(sys.stdin).get('prompt',''))" 2>/dev/null || true)
826
+ # Archive co-archival detection
827
+ if printf '%s' "$input" | grep -qE '"prompt"[[:space:]]*:[[:space:]]*"[^"]*(opsx:archive|openspec-archive-change)'; then
828
+ prompt=$(printf '%s' "$input" | grep -oE '"prompt"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"prompt"[[:space:]]*:[[:space:]]*"//;s/"$//')
829
+ arg=$(printf '%s' "$prompt" | sed 's|^[[:space:]]*/[^[:space:]]* *||' | awk '{print $1}' | sed 's|^@||' | sed 's|.*/changes/||' | sed 's|/$||' | xargs 2>/dev/null || true)
830
+ printf '%s' "$arg" > "$archive_flag"
831
+ fi
717
832
 
718
- [ -z "$prompt" ] && exit 0
833
+ # Bet creation detection
834
+ if printf '%s' "$input" | grep -qE '"prompt"[[:space:]]*:[[:space:]]*"[^"]*oprim:bet'; then
835
+ printf 'bet-created' > "$nudge_flag"
836
+ fi
719
837
 
720
- if [ "$framework" = "openspec" ]; then
721
- if echo "$prompt" | grep -qE '^[[:space:]]*/(opsx:archive|openspec-archive-change)([[:space:]]|$)'; then
722
- arg=$(echo "$prompt" | sed 's|^[[:space:]]*/[^[:space:]]* *||' | sed 's|^@||' | sed 's|.*/changes/||' | sed 's|/$||' | xargs 2>/dev/null || true)
723
- echo "$arg" > "$flag_file"
724
- fi
838
+ # Bet promotion detection
839
+ if printf '%s' "$input" | grep -qE '"prompt"[[:space:]]*:[[:space:]]*"[^"]*oprim:promote'; then
840
+ printf 'bet-promoted' > "$nudge_flag"
725
841
  fi
726
842
  `;
727
843
  const ON_STOP_HOOK = `#!/usr/bin/env bash
728
- # Stop hook: if an archive command was detected, find the linked bet and prompt co-archival.
844
+ # Stop hook: archive co-archival coordination and sequencing nudges.
729
845
 
730
- flag_file=".claude/hooks/.archive-pending"
731
- [ -f "$flag_file" ] || exit 0
846
+ archive_flag=".claude/hooks/.archive-pending"
847
+ nudge_flag=".claude/hooks/.sequence-nudge"
732
848
 
733
- change=$(tr -d '[:space:]' < "$flag_file")
734
- rm -f "$flag_file"
735
-
736
- if [ -z "$change" ]; then
737
- latest=$(ls openspec/changes/archive/ 2>/dev/null | sort -r | head -1)
738
- [ -z "$latest" ] && exit 0
739
- change=$(echo "$latest" | sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}-//')
740
- fi
849
+ # --- Archive co-archival block ---
850
+ if [ -f "$archive_flag" ]; then
851
+ change=$(tr -d '[:space:]' < "$archive_flag")
741
852
 
742
- [ -z "$change" ] && exit 0
853
+ if [ -z "$change" ]; then
854
+ latest=$(ls openspec/changes/archive/ 2>/dev/null | sort -r | head -1)
855
+ [ -n "$latest" ] && change=$(echo "$latest" | sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}-//')
856
+ fi
743
857
 
744
- archive_dir=$(ls openspec/changes/archive/ 2>/dev/null | grep -F "$change" | sort -r | head -1)
745
- [ -z "$archive_dir" ] && exit 0
858
+ if [ -n "$change" ]; then
859
+ archive_dir=$(ls openspec/changes/archive/ 2>/dev/null | grep -F "$change" | sort -r | head -1)
860
+ if [ -n "$archive_dir" ]; then
861
+ rm -f "$archive_flag"
862
+ proposal="openspec/changes/archive/$archive_dir/proposal.md"
863
+ if [ -f "$proposal" ]; then
864
+ bet_id=$(grep -oE 'BET-[0-9]+' "$proposal" | head -1)
865
+ if [ -n "$bet_id" ]; then
866
+ printf '{"decision":"block","reason":"The openspec change '''%s''' was just archived. Its proposal.md references %s. Please invoke \`/oprim:archive %s\` to co-archive the linked bet."}\\n' "$change" "$bet_id" "$bet_id"
867
+ exit 0
868
+ fi
869
+ fi
870
+ fi
871
+ fi
872
+ fi
746
873
 
747
- proposal="openspec/changes/archive/$archive_dir/proposal.md"
748
- [ -f "$proposal" ] || exit 0
874
+ # --- Sequencing nudge from lifecycle event ---
875
+ if [ -f "$nudge_flag" ]; then
876
+ context=$(cat "$nudge_flag")
877
+ rm -f "$nudge_flag"
878
+
879
+ case "$context" in
880
+ bet-created)
881
+ printf '\\n💡 A new bet was added to your backlog. Run \`/oprim:sequence\` to check if it should be pulled into Now or Next.\\n'
882
+ ;;
883
+ bet-promoted)
884
+ printf '\\n💡 A bet was promoted to an OpenSpec change. Run \`/oprim:sequence\` to verify the board reflects this.\\n'
885
+ ;;
886
+ esac
887
+ fi
749
888
 
750
- bet_id=$(grep -oE 'BET-[0-9]+' "$proposal" | head -1)
751
- [ -z "$bet_id" ] && exit 0
889
+ # --- Open Now slot check ---
890
+ if [ -f "oprim/sequence.yaml" ]; then
891
+ wip_limit=$(awk '/^wip_limits:/{in_wip=1} in_wip && /^ now:/{print $2; exit} /^[^ ]/{in_wip=0}' oprim/sequence.yaml 2>/dev/null)
892
+ now_count=$(awk '/^now:/{in_now=1; next} in_now && /^[^ ]/{in_now=0} in_now && /^ - id:/{count++} END{print count+0}' oprim/sequence.yaml 2>/dev/null)
893
+ next_count=$(awk '/^next:/{in_next=1; next} in_next && /^[^ ]/{in_next=0} in_next && /^ - id:/{count++} END{print count+0}' oprim/sequence.yaml 2>/dev/null)
752
894
 
753
- printf '{"decision":"block","reason":"The openspec change '\''%s'\'' was just archived. Its proposal.md references %s. Please invoke \`/oprim:archive %s\` to co-archive the linked bet."}\\n' "$change" "$bet_id" "$bet_id"
895
+ if [ -n "$wip_limit" ] && [ -n "$now_count" ] && [ -n "$next_count" ]; then
896
+ if [ "$now_count" -lt "$wip_limit" ] 2>/dev/null && [ "$next_count" -gt 0 ] 2>/dev/null; then
897
+ printf '\\n💡 Now lane has capacity (%s/%s). Run \`/oprim:sequence\` to pull something from Next.\\n' "$now_count" "$wip_limit"
898
+ fi
899
+ fi
900
+ fi
754
901
  `;
755
902
  // Merge UserPromptSubmit + Stop hooks into .claude/settings.json without clobbering existing entries.
756
903
  // Also removes the legacy PostToolUse/Skill hook from on-skill-archive.sh if present.
@@ -810,7 +957,7 @@ function mergeClaudeSettingsHooks(claudeDir) {
810
957
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
811
958
  console.log(chalk_1.default.green('✓') + ' .claude/settings.json (UserPromptSubmit + Stop hooks registered)');
812
959
  }
813
- // ─── Legacy content (promote / sequence remain inline) ───────────────────────
960
+ // ─── Legacy content (promote remains inline; sequence now delegates to skill)
814
961
  function promoteContent() {
815
962
  return `
816
963
  Promote a prioritized bet to an OpenSpec change and link criteria contracts.
@@ -840,6 +987,9 @@ Promote a prioritized bet to an OpenSpec change and link criteria contracts.
840
987
  `;
841
988
  }
842
989
  function sequenceContent() {
990
+ return `Use the Skill tool to invoke the \`oprim-sequence\` skill.`;
991
+ }
992
+ function sequenceInlineContent() {
843
993
  return `
844
994
  Validate the primer sequencing board and suggest rebalancing if needed.
845
995
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-product-primer/cli",
3
- "version": "0.7.1",
3
+ "version": "0.10.0",
4
4
  "description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
5
5
  "keywords": [
6
6
  "product",