@jenga-ai/agent 3.6.0 → 4.1.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 (86) hide show
  1. package/README.md +32 -1
  2. package/lib/generate-skill-allow-list.js +42 -5
  3. package/lib/skill-allow-list.json +1 -1
  4. package/package.json +1 -2
  5. package/project/app/api/lib/resolve-project-root.js +1 -1
  6. package/project/app/api/scripts/capture-snapshot.js +12 -7
  7. package/project/app/package.json +4 -0
  8. package/project/app/ui/dist/assets/index-BADc5mmH.css +1 -0
  9. package/project/app/ui/dist/assets/index-DX2pfTAW.js +104 -0
  10. package/project/app/ui/dist/index.html +2 -2
  11. package/scripts/acquire-concurrency-slot.sh +34 -4
  12. package/scripts/apply-j-prefix.sh +1 -1
  13. package/scripts/delete-bare-skill-dirs.sh +1 -2
  14. package/scripts/idea_manager.sh +16 -1
  15. package/scripts/release-concurrency-slot.sh +34 -4
  16. package/scripts/render-ranked-list.sh +270 -0
  17. package/scripts/repoint-dead-bare-path-prose.py +81 -0
  18. package/scripts/rewrite-stale-skill-preambles.py +188 -0
  19. package/scripts/strip-polyfill-frontmatter.py +166 -0
  20. package/scripts/todo_manager.sh +16 -1
  21. package/scripts/validate-typed-object.sh +750 -0
  22. package/scripts/verify-postinstall-reconcile.sh +33 -7
  23. package/skills/index/scripts/board_index.py +29 -1
  24. package/skills/j-brainstorm/SKILL.md +3 -4
  25. package/skills/j-btw/SKILL.md +3 -4
  26. package/skills/j-clearify/SKILL.md +3 -4
  27. package/skills/j-close-story/SKILL.md +11 -12
  28. package/skills/j-close-story/scripts/check-story-closeable.sh +11 -4
  29. package/skills/j-commit/SKILL.md +3 -4
  30. package/skills/j-continue/SKILL.md +5 -6
  31. package/skills/j-deep-dive/SKILL.md +3 -4
  32. package/skills/j-distribute/SKILL.md +3 -4
  33. package/skills/j-do/SKILL.md +13 -15
  34. package/skills/j-doc/SKILL.md +3 -4
  35. package/skills/j-doc/scripts/resolve_last_update.py +27 -1
  36. package/skills/j-doc-sync/SKILL.md +3 -4
  37. package/skills/j-dooo/SKILL.md +6 -15
  38. package/skills/j-error/SKILL.md +3 -4
  39. package/skills/j-evaluate/SKILL.md +3 -4
  40. package/skills/j-examplify/SKILL.md +3 -4
  41. package/skills/j-help/SKILL.md +3 -4
  42. package/skills/j-idea/SKILL.md +3 -4
  43. package/skills/j-improve/SKILL.md +3 -4
  44. package/skills/j-init/SKILL.md +20 -11
  45. package/skills/j-init/scripts/apply-scaffold-visibility.sh +8 -6
  46. package/skills/j-jbp/SKILL.md +3 -4
  47. package/skills/j-lgtm/SKILL.md +3 -4
  48. package/skills/j-pi-plan/SKILL.md +3 -4
  49. package/skills/j-proceed/SKILL.md +3 -4
  50. package/skills/j-publish/SKILL.md +3 -4
  51. package/skills/j-publish/scripts/run_gates.sh +1 -1
  52. package/skills/j-reconcile/SKILL.md +38 -5
  53. package/skills/j-reconcile/assets/report_format.md +11 -0
  54. package/skills/j-reconcile/scripts/detect-unlinked-code.sh +2 -2
  55. package/skills/j-reconcile-origin/SKILL.md +3 -4
  56. package/skills/j-redo/SKILL.md +3 -4
  57. package/skills/j-skillify/SKILL.md +3 -4
  58. package/skills/j-spinoff/SKILL.md +3 -4
  59. package/skills/j-status/SKILL.md +4 -5
  60. package/skills/j-todo/SKILL.md +42 -5
  61. package/skills/j-todo/scripts/add_trivial_task.sh +12 -1
  62. package/skills/j-todo/scripts/argument-is-not-ranked-list.sh +92 -0
  63. package/skills/j-todo/scripts/argument-is-ranked-list.sh +78 -0
  64. package/skills/j-uncharted/SKILL.md +251 -12
  65. package/skills/j-uncharted/scripts/detect-dependencies.sh +79 -21
  66. package/skills/j-uncharted/scripts/diff-since-baseline.sh +600 -0
  67. package/skills/j-uncharted/scripts/find-scan-baseline.sh +545 -0
  68. package/skills/j-uncharted/scripts/run-engine.sh +36 -2
  69. package/skills/j-uncharted/scripts/write-scan-record.sh +361 -0
  70. package/skills/j-wtf/SKILL.md +3 -4
  71. package/skills/jenga/SKILL.md +106 -10
  72. package/skills/jenga/playbooks/schema.json +4 -4
  73. package/skills/jenga/scripts/enrich-nl-prompt.sh +225 -0
  74. package/skills/jenga/scripts/load-nl-catalog.js +6 -3
  75. package/skills/jenga/scripts/load-playbooks.sh +289 -4
  76. package/skills/jenga/scripts/match-playbook.sh +6 -5
  77. package/skills/jenga/scripts/run-playbook-step.sh +270 -3
  78. package/templates/permission-levels/level-1-locked.json +1 -1
  79. package/templates/permission-levels/level-2-guarded.json +1 -1
  80. package/templates/permission-levels/level-3-standard.json +1 -1
  81. package/templates/permission-levels/level-4-elevated.json +1 -1
  82. package/templates/permission-levels/level-5-unrestricted.json +1 -1
  83. package/templates/playbook-types.json +6 -0
  84. package/project/app/ui/dist/assets/index-BVR_7Owg.css +0 -1
  85. package/project/app/ui/dist/assets/index-CtU2xLQm.js +0 -104
  86. package/scripts/audit-twin-divergence.sh +0 -693
@@ -20,8 +20,9 @@
20
20
  # metadata — see CONDITIONALS below) -> get the first step to invoke.
21
21
  # 2. `should-skip <state_file>` -> deterministically decide whether the CURRENT step should run.
22
22
  # - `{"skip": true, ...}` -> do NOT invoke the step; call `advance <state_file> skipped`.
23
- # - `{"skip": false, ...}` -> invoke the step (as `/route`'s Step 6 already does for a single
24
- # matched skill), then call `advance <state_file> passed ["<typed-output-value>"]` (step
23
+ # - `{"skip": false, ...}` -> invoke the step (as the Skill Matching & Invocation Contract's
24
+ # Invoke rule already does for a single matched skill), then call
25
+ # `advance <state_file> passed ["<typed-output-value>"]` (step
25
26
  # succeeded) or `advance <state_file> failed [note]` (step failed).
26
27
  # 3. Any of the three `advance` outcomes returns the next step, a "complete" signal, or (on
27
28
  # failure) a halt report.
@@ -181,6 +182,18 @@
181
182
  # stdout). If that was the last step, emits a completion report instead (see OUTPUT SCHEMA)
182
183
  # and removes the state file.
183
184
  #
185
+ # RUNTIME TYPE VERIFICATION (E62_S02_T01): when a value IS given, it is checked against the
186
+ # CURRENT step's declared `output_types` (read off that step's own SKILL.md), via
187
+ # `scripts/validate-typed-object.sh` -- normalizing first, then re-checking, before any
188
+ # failure is declared. A `{when, type}` conditional declaration conforms if it satisfies AT
189
+ # LEAST ONE branch (no classifier is ever run, no branch state is tracked). A value that
190
+ # still does not conform after normalize is NOT stored -- this call is silently redirected
191
+ # into the exact same halt behavior as `advance <state_file> failed "<note>"` (see below),
192
+ # with a note naming the step, the declared type(s) checked, and the raw pre-normalization
193
+ # value. A step with no declared `output_types` (including one this call cannot resolve a
194
+ # SKILL.md for) is completely unaffected: no lookup is even attempted beyond determining
195
+ # there is nothing to check.
196
+ #
184
197
  # run-playbook-step.sh advance <state_file> skipped
185
198
  # Records the CURRENT step as SKIPPED (a new `skipped` list, distinct from `completed`) and
186
199
  # advances the pointer exactly like `passed` -- a skipped step never halts the chain and is
@@ -597,6 +610,30 @@ elif [ "$SUBCOMMAND" = "advance" ]; then
597
610
 
598
611
  PROJECT_DIR="$(resolve_project_dir)"
599
612
 
613
+ # --- E62_S02_T01: resolve the jenga-agent PACKAGE root, needed ONLY to look up a completing
614
+ # step's declared `output_types` off its SKILL.md for runtime type verification. Mirrors
615
+ # load-playbooks.sh's own PKG_ROOT resolution (monorepo checkout vs. installed npm package) --
616
+ # including reusing JENGA_PLAYBOOKS_TEST_ROOT verbatim as the SAME test-injection override, rather
617
+ # than inventing a second env var for the same concept. Deliberately gated on
618
+ # `OUTCOME = passed && EXTRA non-empty`: those are the ONLY two conditions under which
619
+ # verification does anything at all (see the no-op cases in this task's board item), so a
620
+ # `skipped`/`failed` call, or a `passed` call with no typed-output value, never pays for this
621
+ # resolution at all -- "no lookup even attempted beyond determining there is nothing to check."
622
+ PKG_ROOT=""
623
+ if [ "$OUTCOME" = "passed" ] && [ -n "$EXTRA" ]; then
624
+ ADVANCE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
625
+ if [ -n "${JENGA_PLAYBOOKS_TEST_ROOT:-}" ]; then
626
+ PKG_ROOT="$JENGA_PLAYBOOKS_TEST_ROOT"
627
+ elif [ -d "$ADVANCE_SCRIPT_DIR/../../../templates" ]; then
628
+ PKG_ROOT="$(cd "$ADVANCE_SCRIPT_DIR/../../.." && pwd)"
629
+ elif [ -d "$PROJECT_DIR/node_modules/@jenga-ai/agent/templates" ]; then
630
+ PKG_ROOT="$PROJECT_DIR/node_modules/@jenga-ai/agent"
631
+ else
632
+ echo "Error: could not locate the jenga-agent package root (templates/ not found via monorepo checkout or node_modules/@jenga-ai/agent) -- cannot resolve output_types for step verification." >&2
633
+ exit 2
634
+ fi
635
+ fi
636
+
600
637
  PY_SCRIPT="$(mktemp -t run-playbook-step-advance-XXXXXX.py)"
601
638
  trap 'rm -f "$PY_SCRIPT"' EXIT
602
639
 
@@ -604,7 +641,9 @@ elif [ "$SUBCOMMAND" = "advance" ]; then
604
641
  import json
605
642
  import os
606
643
  import re
644
+ import subprocess
607
645
  import sys
646
+ import tempfile
608
647
  from datetime import datetime, timezone
609
648
 
610
649
  state_file_path = sys.argv[1]
@@ -615,6 +654,221 @@ outcome = sys.argv[2]
615
654
  extra = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] != "" else None
616
655
  note = extra
617
656
  project_dir = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] != "" else None
657
+ # E62_S02_T01: the jenga-agent package root, resolved (or hard-errored) by the bash wrapper above,
658
+ # ONLY when outcome == "passed" and extra is non-empty. Empty string in every other case -- exactly
659
+ # the cases where verification is a documented no-op, so this is never even looked at.
660
+ pkg_root = sys.argv[5] if len(sys.argv) > 5 and sys.argv[5] != "" else None
661
+
662
+ # ---------------------------------------------------------------------------------------------
663
+ # E62_S02_T01: runtime type verification at the "passed" chokepoint
664
+ # ---------------------------------------------------------------------------------------------
665
+ # `extract_output_types`/`declared_type_entries` below are a DELIBERATE, DOCUMENTED duplication of
666
+ # skills/jenga/scripts/load-playbooks.sh's own `extract_types_field`/`extract_output_types`/
667
+ # `declared_type_entries` (see that script's "TYPE REGISTRY" section, around line 788 as of
668
+ # E62_S01_T04). The task considered factoring this into a shared, importable module, but every
669
+ # python body in both scripts is a throwaway file generated into a fresh `mktemp` path per
670
+ # invocation (see the `cat > "$PY_SCRIPT" <<'PY'` pattern this whole file uses) -- there is no
671
+ # existing precedent in this repo for a shared importable python module fed to that pattern, and
672
+ # building one purely for a ~30-line, rarely-changing frontmatter parser was judged riskier than a
673
+ # small, clearly-labelled duplicate. If this parser ever needs a THIRD copy, that is the signal to
674
+ # extract it for real; until then, a change to one copy's parsing rules should always be checked
675
+ # against the other by anyone touching either.
676
+ _FRONTMATTER_RE = re.compile(r'^---\r?\n(.*?)\r?\n---', re.DOTALL)
677
+
678
+
679
+ def extract_output_types(skill_md_path):
680
+ """Best-effort extraction of the `output_types` frontmatter field from a SKILL.md. Returns
681
+ None if the file/field is missing or unparseable, a `str` for the single-static-type form, or
682
+ a `list[dict]` for the `{when, type}` list form. Mirrors load-playbooks.sh's
683
+ `extract_types_field(path, "output_types")` exactly -- see the module-level note above."""
684
+ try:
685
+ with open(skill_md_path, encoding="utf-8") as fh:
686
+ content = fh.read()
687
+ except OSError:
688
+ return None
689
+
690
+ fm_match = _FRONTMATTER_RE.match(content)
691
+ if not fm_match:
692
+ return None
693
+
694
+ fm_lines = fm_match.group(1).splitlines()
695
+
696
+ for i, line in enumerate(fm_lines):
697
+ key_match = re.match(r'^output_types:\s*(.*)$', line)
698
+ if not key_match:
699
+ continue
700
+
701
+ rest = key_match.group(1).strip()
702
+ if rest:
703
+ return rest.strip('"\'')
704
+
705
+ # Block/list form: gather subsequent, more-indented lines into a list of dicts.
706
+ items = []
707
+ current = {}
708
+ j = i + 1
709
+ while j < len(fm_lines):
710
+ raw_line = fm_lines[j]
711
+ if not raw_line.strip():
712
+ j += 1
713
+ continue
714
+ if not raw_line[0].isspace():
715
+ break # a new top-level frontmatter key ends this block
716
+
717
+ stripped = raw_line.strip()
718
+ item_match = re.match(r'^-\s*(.*)$', stripped)
719
+ if item_match:
720
+ if current:
721
+ items.append(current)
722
+ current = {}
723
+ remainder = item_match.group(1)
724
+ kv = re.match(r'^([a-zA-Z_]+):\s*(.*)$', remainder) if remainder else None
725
+ if kv:
726
+ current[kv.group(1)] = kv.group(2).strip().strip('"\'')
727
+ else:
728
+ kv = re.match(r'^([a-zA-Z_]+):\s*(.*)$', stripped)
729
+ if kv:
730
+ current[kv.group(1)] = kv.group(2).strip().strip('"\'')
731
+ j += 1
732
+
733
+ if current:
734
+ items.append(current)
735
+ return items if items else None
736
+
737
+ return None
738
+
739
+
740
+ def declared_type_entries(value):
741
+ """Normalize an `output_types` value into `[(type_or_None, when_or_None), ...]`. Mirrors
742
+ load-playbooks.sh's `declared_type_entries` exactly -- see the module-level note above."""
743
+ if not value:
744
+ return []
745
+ if isinstance(value, str):
746
+ return [(value, None)]
747
+ if isinstance(value, list):
748
+ entries = []
749
+ for item in value:
750
+ if isinstance(item, dict):
751
+ entries.append((item.get("type") or None, item.get("when") or None))
752
+ else:
753
+ entries.append((None, None))
754
+ return entries
755
+ return []
756
+
757
+
758
+ def verify_typed_output(step_name, raw_value, pkg_root_dir):
759
+ """The E62_S02_T01 chokepoint check itself.
760
+
761
+ Returns `(failure_note_or_None, resolved_value)`:
762
+ - `(None, value)` -- conforms (or nothing declared/checkable -- a no-op). `value` is the
763
+ raw value unless a declared branch needed normalization to conform,
764
+ in which case it is that branch's normalized value.
765
+ - `(note, None)` -- every declared branch is non-conforming even after normalize. `note`
766
+ names the step, the declared type(s) checked, and the raw
767
+ pre-normalization value, for the existing `failed` -> `halted` path.
768
+
769
+ The conditional `{when, type}` rule (ratified at epic level, not re-derived here): the value
770
+ conforms if it satisfies AT LEAST ONE declared branch. No classifier script is ever run and no
771
+ branch state is tracked -- every declared type is checked against the SAME captured value.
772
+ """
773
+ skill_md_path = os.path.join(pkg_root_dir, "skills", step_name, "SKILL.md")
774
+ if not os.path.isfile(skill_md_path):
775
+ # No resolvable skill dir (e.g. a synthetic/test step name, or a stale board reference) --
776
+ # nothing to check against. Not this chokepoint's problem to diagnose further.
777
+ return None, raw_value
778
+
779
+ declared = extract_output_types(skill_md_path)
780
+ entries = [e for e in declared_type_entries(declared) if e[0] is not None]
781
+ if not entries:
782
+ # No declared output_types (or only malformed entries, which is a load-time concern this
783
+ # chokepoint does not re-litigate) -- a documented no-op, exactly as today.
784
+ return None, raw_value
785
+
786
+ validator = os.path.join(pkg_root_dir, "scripts", "validate-typed-object.sh")
787
+ if not os.path.isfile(validator):
788
+ print(
789
+ f"Error: type registry validator not found at {validator} -- cannot verify step "
790
+ f"'{step_name}'s declared output_types.",
791
+ file=sys.stderr,
792
+ )
793
+ sys.exit(2)
794
+
795
+ types_checked = []
796
+ branch_reasons = []
797
+ normalized_fallback = None # first branch that conforms only after normalize
798
+
799
+ for type_name, when_val in entries:
800
+ if type_name not in types_checked:
801
+ types_checked.append(type_name)
802
+
803
+ tmp_path = None
804
+ try:
805
+ with tempfile.NamedTemporaryFile(
806
+ mode="w", encoding="utf-8", delete=False, prefix="run-playbook-step-verify-"
807
+ ) as tmp:
808
+ tmp.write(raw_value)
809
+ tmp_path = tmp.name
810
+ proc = subprocess.run(
811
+ [validator, "--value-file", tmp_path, type_name],
812
+ capture_output=True,
813
+ text=True,
814
+ )
815
+ finally:
816
+ if tmp_path is not None:
817
+ try:
818
+ os.unlink(tmp_path)
819
+ except OSError:
820
+ pass
821
+
822
+ if proc.returncode == 0:
823
+ # Conforms as given, under this branch alone -- the strongest possible verdict.
824
+ # "At least one branch accepts it" is satisfied immediately; stop checking further
825
+ # branches and never report this as normalized.
826
+ return None, raw_value
827
+ if proc.returncode == 2:
828
+ if normalized_fallback is None:
829
+ try:
830
+ payload = json.loads(proc.stdout)
831
+ normalized_fallback = payload.get("value")
832
+ except (json.JSONDecodeError, AttributeError):
833
+ normalized_fallback = raw_value
834
+ continue
835
+ if proc.returncode == 3:
836
+ reason = None
837
+ try:
838
+ payload = json.loads(proc.stdout)
839
+ reason = payload.get("reason")
840
+ except (json.JSONDecodeError, AttributeError):
841
+ reason = None
842
+ branch = f" (when='{when_val}')" if when_val else ""
843
+ branch_reasons.append(f"type '{type_name}'{branch}: {reason or 'does not conform'}")
844
+ continue
845
+ # Exit 4 (unknown type), 5 (environment), 6 (registry contract violation) are
846
+ # ENVIRONMENT-level failures, never value judgments -- E62_S01_T02's own validator
847
+ # contract is explicit that these must not be conflated with non-conforming. A properly
848
+ # loaded chain should never reach an unknown/malformed type here (load-playbooks.sh's own
849
+ # load-time check, E62_S01_T04, already rejects that at confirmation time) -- if it
850
+ # happens anyway, this is a broken environment, not a playbook-author's bad value, so it
851
+ # is NOT routed through the failed/halted board mechanism.
852
+ print(
853
+ f"Error: scripts/validate-typed-object.sh reported an environment-level failure "
854
+ f"(exit {proc.returncode}) verifying step '{step_name}'s declared type '{type_name}': "
855
+ f"{proc.stderr.strip() or proc.stdout.strip()}",
856
+ file=sys.stderr,
857
+ )
858
+ sys.exit(2)
859
+
860
+ if normalized_fallback is not None:
861
+ # No branch conformed as given, but at least one conforms after its own normalize.
862
+ return None, normalized_fallback
863
+
864
+ # No declared branch accepts the value, as given or normalized.
865
+ types_list = ", ".join(types_checked)
866
+ detail = "; ".join(branch_reasons) if branch_reasons else "no declared branch accepts it"
867
+ failure_note = (
868
+ f"step '{step_name}' produced a value that does not conform to its declared "
869
+ f"output_types [{types_list}] (raw value: '{raw_value}'); {detail}"
870
+ )
871
+ return failure_note, None
618
872
 
619
873
  # --- E53_S04_T05: artifact persistence + redaction (persisted copy only -- see header) ---
620
874
  _ABS_PATH_RE = re.compile(r'(/[^\s"\']+)')
@@ -693,6 +947,19 @@ if idx >= len(steps):
693
947
  current_step = steps[idx]
694
948
  state.setdefault("skipped", [])
695
949
 
950
+ # E62_S02_T01: verify a "passed" step's typed-output value against its declared output_types
951
+ # BEFORE the outcome dispatch below, so a non-conforming value can redirect into the EXISTING
952
+ # `outcome == "failed"` branch immediately after -- no second failure mechanism is introduced.
953
+ # Both no-op cases (no declared output_types; no typed-output value at all) leave `outcome` and
954
+ # `extra` untouched, so this is byte-for-byte the pre-existing behavior for them.
955
+ if outcome == "passed" and extra is not None:
956
+ verify_failure_note, resolved_value = verify_typed_output(current_step, extra, pkg_root)
957
+ if verify_failure_note is not None:
958
+ outcome = "failed"
959
+ note = verify_failure_note
960
+ else:
961
+ extra = resolved_value
962
+
696
963
  if outcome == "failed":
697
964
  state["halted"] = True
698
965
  state["failed_step"] = current_step
@@ -757,7 +1024,7 @@ print(json.dumps({
757
1024
  print(f"STATE_FILE: {state_file_path}", file=sys.stderr)
758
1025
  PY
759
1026
 
760
- python3 "$PY_SCRIPT" "$STATE_FILE" "$OUTCOME" "$EXTRA" "$PROJECT_DIR"
1027
+ python3 "$PY_SCRIPT" "$STATE_FILE" "$OUTCOME" "$EXTRA" "$PROJECT_DIR" "$PKG_ROOT"
761
1028
  exit $?
762
1029
 
763
1030
  else
@@ -41,7 +41,7 @@
41
41
  "hooks": [
42
42
  {
43
43
  "type": "command",
44
- "command": "NAME=$(jq -r '.name')\n. \"$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh\"\nDIR=\"$JENGA_PROJECT_DIR/.claude/worktrees/$NAME\"\ngit worktree add \"$DIR\" -b \"$NAME\" 2>&1\necho \"$DIR\"\n"
44
+ "command": "NAME=$(jq -r '.name')\n. \"$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh\"\nDIR=\"$JENGA_PROJECT_DIR/.claude/worktrees/$NAME\"\ngit worktree add \"$DIR\" -b \"$NAME\" 2>&1\n\"$(git rev-parse --show-toplevel)/scripts/install-worktree-commit-guard.sh\" \"$DIR\" \"$NAME\" 1>&2\necho \"$DIR\"\n"
45
45
  }
46
46
  ]
47
47
  }
@@ -34,7 +34,7 @@
34
34
  "hooks": [
35
35
  {
36
36
  "type": "command",
37
- "command": "NAME=$(jq -r '.name')\n. \"$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh\"\nDIR=\"$JENGA_PROJECT_DIR/.claude/worktrees/$NAME\"\ngit worktree add \"$DIR\" -b \"$NAME\" 2>&1\necho \"$DIR\"\n"
37
+ "command": "NAME=$(jq -r '.name')\n. \"$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh\"\nDIR=\"$JENGA_PROJECT_DIR/.claude/worktrees/$NAME\"\ngit worktree add \"$DIR\" -b \"$NAME\" 2>&1\n\"$(git rev-parse --show-toplevel)/scripts/install-worktree-commit-guard.sh\" \"$DIR\" \"$NAME\" 1>&2\necho \"$DIR\"\n"
38
38
  }
39
39
  ]
40
40
  }
@@ -32,7 +32,7 @@
32
32
  "hooks": [
33
33
  {
34
34
  "type": "command",
35
- "command": "NAME=$(jq -r '.name')\n. \"$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh\"\nDIR=\"$JENGA_PROJECT_DIR/.claude/worktrees/$NAME\"\ngit worktree add \"$DIR\" -b \"$NAME\" 2>&1\necho \"$DIR\"\n"
35
+ "command": "NAME=$(jq -r '.name')\n. \"$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh\"\nDIR=\"$JENGA_PROJECT_DIR/.claude/worktrees/$NAME\"\ngit worktree add \"$DIR\" -b \"$NAME\" 2>&1\n\"$(git rev-parse --show-toplevel)/scripts/install-worktree-commit-guard.sh\" \"$DIR\" \"$NAME\" 1>&2\necho \"$DIR\"\n"
36
36
  }
37
37
  ]
38
38
  }
@@ -30,7 +30,7 @@
30
30
  "hooks": [
31
31
  {
32
32
  "type": "command",
33
- "command": "NAME=$(jq -r '.name')\n. \"$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh\"\nDIR=\"$JENGA_PROJECT_DIR/.claude/worktrees/$NAME\"\ngit worktree add \"$DIR\" -b \"$NAME\" 2>&1\necho \"$DIR\"\n"
33
+ "command": "NAME=$(jq -r '.name')\n. \"$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh\"\nDIR=\"$JENGA_PROJECT_DIR/.claude/worktrees/$NAME\"\ngit worktree add \"$DIR\" -b \"$NAME\" 2>&1\n\"$(git rev-parse --show-toplevel)/scripts/install-worktree-commit-guard.sh\" \"$DIR\" \"$NAME\" 1>&2\necho \"$DIR\"\n"
34
34
  }
35
35
  ]
36
36
  }
@@ -28,7 +28,7 @@
28
28
  "hooks": [
29
29
  {
30
30
  "type": "command",
31
- "command": "NAME=$(jq -r '.name')\n. \"$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh\"\nDIR=\"$JENGA_PROJECT_DIR/.claude/worktrees/$NAME\"\ngit worktree add \"$DIR\" -b \"$NAME\" 2>&1\necho \"$DIR\"\n"
31
+ "command": "NAME=$(jq -r '.name')\n. \"$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh\"\nDIR=\"$JENGA_PROJECT_DIR/.claude/worktrees/$NAME\"\ngit worktree add \"$DIR\" -b \"$NAME\" 2>&1\n\"$(git rev-parse --show-toplevel)/scripts/install-worktree-commit-guard.sh\" \"$DIR\" \"$NAME\" 1>&2\necho \"$DIR\"\n"
32
32
  }
33
33
  ]
34
34
  }
@@ -31,6 +31,12 @@
31
31
  "trim",
32
32
  "drop_empty"
33
33
  ]
34
+ },
35
+ "ranked_list": {
36
+ "verify": {
37
+ "per_line": "^[0-9]+\\. [^[:space:]]+ — .+$"
38
+ },
39
+ "normalize": []
34
40
  }
35
41
  }
36
42
  }
@@ -1 +0,0 @@
1
- .loading-spinner{padding:2rem;text-align:center;color:#888;font-size:1rem}.error-message{padding:1rem;background:#fef2f2;border:1px solid #fca5a5;border-radius:4px;color:#b91c1c}.history-tab h2{margin-bottom:1rem}.history-empty{color:#888;padding:2rem;text-align:center}.history-list{list-style:none;padding:0;margin:0;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.history-entry{display:flex;align-items:center;gap:.75rem;padding:.65rem 1rem;cursor:pointer;border-bottom:1px solid #f3f4f6;transition:background .12s}.history-entry:last-child{border-bottom:none}.history-entry:hover,.history-entry:focus{background:#f0f7ff;outline:none}.history-entry.selected{background:#dbeafe}.history-badge{flex-shrink:0;font-size:.75rem;font-weight:600;padding:.2rem .55rem;border-radius:999px;white-space:nowrap}.badge-commit{background:#dbeafe;color:#1d4ed8}.badge-rapport{background:#dcfce7;color:#15803d}.history-subject{flex:1;font-size:.9rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#1f2937}.history-date{flex-shrink:0;font-size:.78rem;color:#9ca3af}.panel-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000040;z-index:100}.detail-panel{position:fixed;top:0;right:0;bottom:0;width:min(480px,100vw);background:#fff;box-shadow:-4px 0 24px #00000026;z-index:101;overflow-y:auto;padding:1.5rem;animation:slideIn .2s ease-out}@keyframes slideIn{0%{transform:translate(100%)}to{transform:translate(0)}}.panel-close{float:right;background:none;border:none;font-size:1.25rem;cursor:pointer;color:#6b7280;padding:.25rem;line-height:1}.panel-close:hover{color:#111827}.panel-title{margin:0 0 1rem;font-size:1.1rem;color:#1f2937}.panel-meta{display:grid;grid-template-columns:auto 1fr;gap:.3rem 1rem;font-size:.85rem;margin-bottom:1rem}.panel-meta dt{font-weight:600;color:#6b7280;white-space:nowrap}.panel-meta dd{margin:0;color:#1f2937;word-break:break-all}.panel-subject{font-size:.95rem;color:#1f2937}.panel-body{font-size:.82rem;white-space:pre-wrap;background:#f9fafb;border:1px solid #e5e7eb;border-radius:4px;padding:.75rem;max-height:40vh;overflow-y:auto}.panel-empty{color:#9ca3af;font-style:italic}.markdown-body{font-size:.9rem;line-height:1.6;color:#1f2937}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4{margin:1em 0 .4em;line-height:1.3}.markdown-body p{margin:.5em 0}.markdown-body code{background:#f3f4f6;padding:.1em .35em;border-radius:3px;font-size:.85em}.markdown-body pre{background:#f3f4f6;padding:.75rem;border-radius:4px;overflow-x:auto;font-size:.82em}.markdown-body pre code{background:none;padding:0}.markdown-body blockquote{border-left:3px solid #d1d5db;margin:.75em 0;padding:.25em 1em;color:#6b7280}.markdown-body ul,.markdown-body ol{padding-left:1.5em;margin:.5em 0}@media (max-width: 480px){.history-entry{flex-wrap:wrap;row-gap:var(--space-xs)}.history-subject{flex-basis:100%;order:3;white-space:normal}.detail-panel{box-sizing:border-box;padding:var(--space-md)}.panel-close{padding:var(--space-sm);font-size:1.5rem;display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px}.panel-meta{grid-template-columns:1fr;gap:.15rem 0}.panel-meta dt{margin-top:var(--space-xs)}.markdown-body code{overflow-wrap:break-word;word-break:break-word}.markdown-body table{display:block;max-width:100%;overflow-x:auto}}.board-view{display:flex;flex-direction:column;gap:1.5rem}.epic-card{background:#fff;border:1px solid #e5e7eb;border-radius:6px;padding:1rem}.epic-header{display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem}.epic-title{font-size:1.1rem;font-weight:600;margin:0;cursor:pointer}.epic-id{color:#6b7280;font-size:.85rem}.stories-list{display:flex;flex-direction:column;gap:.75rem;padding-left:1rem}.story-card{background:#f9fafb;border:1px solid #e5e7eb;border-radius:4px;padding:.75rem}.story-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.story-title{font-size:1rem;font-weight:500;margin:0;cursor:pointer}.story-id{color:#6b7280;font-size:.8rem}.tasks-list{display:flex;flex-direction:column;gap:.4rem;padding-left:1rem}.task-card{background:#fff;border:1px solid #e5e7eb;border-radius:3px;padding:.5rem .75rem;display:flex;align-items:center;gap:.5rem}.task-title{font-size:.9rem;flex:1;cursor:pointer}.task-id{color:#9ca3af;font-size:.75rem}.task-assigned{color:#6b7280;font-size:.8rem}.status-badge{padding:.2rem .5rem;border-radius:9999px;font-size:.75rem;font-weight:500}.badge-pending{background:#f3f4f6;color:#374151}.badge-running{background:#dbeafe;color:#1d4ed8}.badge-in-progress{background:#ffedd5;color:#c2410c}.badge-passed{background:#dcfce7;color:#15803d}.badge-passed-remarks{background:#fff;color:#15803d;border:1px solid #dcfce7}.badge-done{background:#dbeafe;color:#1d4ed8}.badge-failed{background:#fee2e2;color:#b91c1c}.empty-state{padding:2rem;text-align:center;color:#6b7280}.status-filter{display:flex;flex-wrap:wrap;gap:.75rem;padding:.75rem 1rem;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px}.status-filter-option{display:flex;align-items:center;gap:.35rem;font-size:.85rem;color:#374151;cursor:pointer}@media (max-width: 480px){.epic-header,.story-header{flex-wrap:wrap;row-gap:var(--space-xs)}.epic-title,.story-title{flex-basis:100%}.task-card{flex-wrap:wrap;row-gap:var(--space-xs)}.task-card .task-title{flex-basis:100%}.stories-list,.tasks-list{padding-left:var(--space-sm)}}.kanban-board{display:flex;align-items:flex-start;gap:.75rem;overflow-x:auto;padding-bottom:.5rem}.kanban-column{flex:0 0 220px;width:220px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:.6rem;display:flex;flex-direction:column;gap:.5rem}.kanban-column-empty{flex:0 0 140px;width:140px;opacity:.6}.kanban-column-header{display:flex;align-items:center;justify-content:space-between;gap:.4rem}.kanban-column-title{font-size:.8rem;font-weight:600;color:#374151;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kanban-column-count{font-size:.7rem;color:#6b7280;background:#e5e7eb;border-radius:9999px;padding:.05rem .4rem}.kanban-column-empty-state{font-size:.75rem;color:#9ca3af;text-align:center;padding:.5rem 0}.kanban-column-items{display:flex;flex-direction:column;gap:.5rem}.kanban-card{background:#fff;border:1px solid #e5e7eb;border-radius:4px;padding:.5rem .6rem;display:flex;flex-direction:column;gap:.3rem}.kanban-card-header{display:flex;align-items:center;justify-content:space-between;gap:.4rem}.kanban-card-type{font-size:.65rem;text-transform:uppercase;letter-spacing:.03em;color:#9ca3af}.kanban-card-id{font-size:.7rem;color:#9ca3af}.kanban-card-title{font-size:.85rem;color:#111827;cursor:pointer}.kanban-card-queued{border-style:dashed;border-color:#c4b5fd;background:#fdfcff}.kanban-card-footer{display:flex;align-items:center;justify-content:space-between;gap:.4rem}.kanban-card-queued-marker{font-size:.65rem;text-transform:uppercase;letter-spacing:.03em;color:#6d28d9;background:#ede9fe;border:1px solid #ddd6fe;border-radius:9999px;padding:.05rem .4rem;white-space:nowrap;cursor:help}@media (max-width: 480px){.kanban-board{position:relative;gap:var(--space-sm);scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch}.kanban-column{flex:0 0 85%;width:85%;scroll-snap-align:start;scroll-snap-stop:always}.kanban-column-empty{flex:0 0 140px;width:140px}.kanban-board:after{content:"";position:sticky;top:0;right:0;flex:0 0 var(--space-xl);align-self:stretch;background:linear-gradient(to right,transparent,rgba(249,250,251,.95));pointer-events:none}}.architecture-tab{overflow-x:hidden}.architecture-tab h2{margin-bottom:1rem}.arch-empty{color:#9ca3af;font-style:italic}.arch-section{margin-bottom:2rem}.arch-section h3{font-size:1rem;font-weight:600;color:#374151;margin-bottom:.75rem;border-bottom:1px solid #e5e7eb;padding-bottom:.35rem}.tech-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.75rem}.tech-card{background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:.85rem 1rem;display:flex;flex-direction:column;gap:.3rem;box-shadow:0 1px 3px #0000000d}.tech-name{font-weight:600;font-size:.9rem;color:#1f2937}.tech-description{font-size:.78rem;color:#6b7280}.dep-table{width:100%;border-collapse:collapse;font-size:.875rem}.dep-table th{text-align:left;padding:.5rem .75rem;background:#f9fafb;border:1px solid #e5e7eb;font-weight:600;color:#374151}.dep-table td{padding:.45rem .75rem;border:1px solid #f3f4f6;color:#1f2937}.dep-table tr:hover td{background:#f9fafb}.dep-name{font-family:monospace}.dep-version{color:#6b7280;font-family:monospace}.dep-badge{display:inline-block;font-size:.7rem;font-weight:600;padding:.18rem .55rem;border-radius:999px;white-space:nowrap}.dep-runtime{background:#dbeafe;color:#1d4ed8}.dep-dev{background:#f3f4f6;color:#4b5563}.dep-table-wrap{overflow-x:auto}.sad-map-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;margin-bottom:.6rem}.sad-type-chip{--chip-color: #94a3b8;display:inline-flex;align-items:center;gap:.35rem;font-size:.75rem;font-weight:600;color:#374151;background:#fff;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .65rem;cursor:pointer;line-height:1.2}.sad-type-chip.is-collapsed{background:#f3f4f6;border-color:var(--chip-color)}.sad-type-chip-dot{width:8px;height:8px;border-radius:50%;background:var(--chip-color);flex-shrink:0}.sad-reset-view-btn{font-size:.75rem;font-weight:600;color:#374151;background:#fff;border:1px solid #e5e7eb;border-radius:6px;padding:.25rem .65rem;cursor:pointer;margin-left:auto}.sad-map-scroll{width:100%;min-height:320px;overflow:hidden;-webkit-overflow-scrolling:touch;border:1px solid var(--border, #e2e8f0);border-radius:8px;background:var(--bg-secondary, #f8fafc)}.sad-map-svg{display:block;width:100%;cursor:grab}.sad-map-svg:active{cursor:grabbing}.sad-empty{color:var(--text-muted, #94a3b8);font-style:italic}.sad-coverage-indicator{font-size:.78rem;color:#6b7280;margin:0 0 .5rem}.sad-coverage-shown{font-weight:600;color:#374151}.sad-coverage-hidden{color:#9ca3af}.sad-coverage-stale{color:#b45309;font-weight:600}.sad-empty-state{display:flex;flex-direction:column;gap:.35rem}.sad-empty-detail{font-size:.8rem;color:#9ca3af;margin:0}.sad-ghost-legend{display:flex;align-items:center;gap:.4rem;font-size:.75rem;color:#9ca3af;margin:0 0 .6rem}.sad-ghost-legend-swatch{display:inline-block;width:12px;height:12px;flex-shrink:0;border-radius:3px;background:#94a3b8;opacity:.4;border:1.5px dashed #1f2937}@media (max-width: 480px){.architecture-tab{padding-bottom:var(--space-sm)}.arch-section{margin-bottom:var(--space-lg)}.arch-section h3{padding-bottom:var(--space-xs)}.tech-grid{gap:var(--space-sm)}.tech-card{padding:var(--space-sm) var(--space-md)}.sad-map-scroll{min-height:180px}}.entry-list-empty{color:#888;padding:2rem;text-align:center}.entry-list{list-style:none;padding:0;margin:0;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.entry-list-item{display:flex;align-items:center;gap:.75rem;padding:.65rem 1rem;cursor:pointer;border-bottom:1px solid #f3f4f6;transition:background .12s}.entry-list-item:last-child{border-bottom:none}.entry-list-item:hover,.entry-list-item:focus{background:#f0f7ff;outline:none}.entry-list-item.selected{background:#dbeafe}.entry-badge{flex-shrink:0;font-size:.75rem;font-weight:600;padding:.2rem .55rem;border-radius:999px;white-space:nowrap;text-transform:capitalize;background:#ede9fe;color:#6d28d9}.entry-title{flex:1;font-size:.9rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#1f2937}.entry-date{flex-shrink:0;font-size:.78rem;color:#9ca3af}@media (max-width: 480px){.entry-list-item{flex-wrap:wrap;row-gap:var(--space-xs)}.entry-title{flex-basis:100%;order:3;white-space:normal}}.pagination-controls{display:flex;align-items:center;justify-content:center;gap:var(--space-md);margin-top:var(--space-md)}.pagination-btn{padding:.4rem .9rem;font-size:.85rem;border:1px solid #e5e7eb;border-radius:6px;background:#fff;color:#1f2937;cursor:pointer}.pagination-btn:hover:not(:disabled){background:#f0f7ff;border-color:#93c5fd}.pagination-btn:disabled{opacity:.45;cursor:not-allowed}.pagination-status{font-size:.82rem;color:#6b7280;white-space:nowrap}@media (max-width: 480px){.pagination-controls{gap:var(--space-sm)}.pagination-btn{padding:.5rem .85rem;font-size:.9rem}.pagination-status{font-size:.78rem}}.rapports-search-input{display:block;width:100%;box-sizing:border-box;padding:.55rem .75rem;margin-bottom:var(--space-md);font-size:.9rem;border:1px solid #e5e7eb;border-radius:6px;color:#1f2937}.rapports-search-input:focus{outline:none;border-color:#93c5fd;box-shadow:0 0 0 2px #3b82f640}@media (max-width: 480px){.rapports-search-input{font-size:1rem;padding:.65rem .75rem}}:root{--space-xs: .25rem;--space-sm: .5rem;--space-md: 1rem;--space-lg: 1.5rem;--space-xl: 2rem;--breakpoint-mobile: 480px;--breakpoint-tablet: 768px}body{font-family:sans-serif;margin:0;background:#f5f5f5}.app{max-width:1200px;margin:0 auto;padding:1rem}.tabs{display:flex;gap:0;border-bottom:1px solid #ddd}.tab-btn{padding:.6rem 1.2rem;border:none;background:transparent;cursor:pointer;font-size:1rem;color:#555;border-bottom:2px solid transparent}.tab-btn.active{color:#2563eb;border-bottom-color:#2563eb;font-weight:600}.tab-content{padding:1rem 0}@media (max-width: 480px){.app{width:100%;box-sizing:border-box;padding:var(--space-sm)}.tabs{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch}.tab-btn{flex:0 0 auto;padding:var(--space-sm) var(--space-md)}}