@jenga-ai/agent 3.6.0 → 4.0.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 (79) hide show
  1. package/lib/generate-skill-allow-list.js +42 -5
  2. package/lib/skill-allow-list.json +1 -1
  3. package/package.json +1 -2
  4. package/project/app/api/lib/resolve-project-root.js +1 -1
  5. package/project/app/package.json +4 -0
  6. package/project/app/ui/dist/assets/index-BADc5mmH.css +1 -0
  7. package/project/app/ui/dist/assets/index-C3oiuli_.js +104 -0
  8. package/project/app/ui/dist/index.html +2 -2
  9. package/scripts/acquire-concurrency-slot.sh +34 -4
  10. package/scripts/apply-j-prefix.sh +1 -1
  11. package/scripts/delete-bare-skill-dirs.sh +1 -2
  12. package/scripts/idea_manager.sh +16 -1
  13. package/scripts/release-concurrency-slot.sh +34 -4
  14. package/scripts/render-ranked-list.sh +270 -0
  15. package/scripts/repoint-dead-bare-path-prose.py +81 -0
  16. package/scripts/rewrite-stale-skill-preambles.py +188 -0
  17. package/scripts/strip-polyfill-frontmatter.py +166 -0
  18. package/scripts/todo_manager.sh +16 -1
  19. package/scripts/validate-typed-object.sh +750 -0
  20. package/skills/j-brainstorm/SKILL.md +3 -4
  21. package/skills/j-btw/SKILL.md +3 -4
  22. package/skills/j-clearify/SKILL.md +3 -4
  23. package/skills/j-close-story/SKILL.md +11 -12
  24. package/skills/j-close-story/scripts/check-story-closeable.sh +11 -4
  25. package/skills/j-commit/SKILL.md +3 -4
  26. package/skills/j-continue/SKILL.md +5 -6
  27. package/skills/j-deep-dive/SKILL.md +3 -4
  28. package/skills/j-distribute/SKILL.md +3 -4
  29. package/skills/j-do/SKILL.md +13 -15
  30. package/skills/j-doc/SKILL.md +3 -4
  31. package/skills/j-doc-sync/SKILL.md +3 -4
  32. package/skills/j-dooo/SKILL.md +6 -15
  33. package/skills/j-error/SKILL.md +3 -4
  34. package/skills/j-evaluate/SKILL.md +3 -4
  35. package/skills/j-examplify/SKILL.md +3 -4
  36. package/skills/j-help/SKILL.md +3 -4
  37. package/skills/j-idea/SKILL.md +3 -4
  38. package/skills/j-improve/SKILL.md +3 -4
  39. package/skills/j-init/SKILL.md +20 -11
  40. package/skills/j-init/scripts/apply-scaffold-visibility.sh +8 -6
  41. package/skills/j-jbp/SKILL.md +3 -4
  42. package/skills/j-lgtm/SKILL.md +3 -4
  43. package/skills/j-pi-plan/SKILL.md +3 -4
  44. package/skills/j-proceed/SKILL.md +3 -4
  45. package/skills/j-publish/SKILL.md +3 -4
  46. package/skills/j-publish/scripts/run_gates.sh +1 -1
  47. package/skills/j-reconcile/SKILL.md +38 -5
  48. package/skills/j-reconcile/assets/report_format.md +11 -0
  49. package/skills/j-reconcile/scripts/detect-unlinked-code.sh +2 -2
  50. package/skills/j-reconcile-origin/SKILL.md +3 -4
  51. package/skills/j-redo/SKILL.md +3 -4
  52. package/skills/j-skillify/SKILL.md +3 -4
  53. package/skills/j-spinoff/SKILL.md +3 -4
  54. package/skills/j-status/SKILL.md +4 -5
  55. package/skills/j-todo/SKILL.md +42 -5
  56. package/skills/j-todo/scripts/argument-is-not-ranked-list.sh +92 -0
  57. package/skills/j-todo/scripts/argument-is-ranked-list.sh +78 -0
  58. package/skills/j-uncharted/SKILL.md +251 -12
  59. package/skills/j-uncharted/scripts/detect-dependencies.sh +79 -21
  60. package/skills/j-uncharted/scripts/diff-since-baseline.sh +600 -0
  61. package/skills/j-uncharted/scripts/find-scan-baseline.sh +545 -0
  62. package/skills/j-uncharted/scripts/run-engine.sh +36 -2
  63. package/skills/j-uncharted/scripts/write-scan-record.sh +361 -0
  64. package/skills/j-wtf/SKILL.md +3 -4
  65. package/skills/jenga/SKILL.md +69 -8
  66. package/skills/jenga/playbooks/schema.json +4 -4
  67. package/skills/jenga/scripts/load-nl-catalog.js +5 -2
  68. package/skills/jenga/scripts/load-playbooks.sh +289 -4
  69. package/skills/jenga/scripts/match-playbook.sh +4 -4
  70. package/skills/jenga/scripts/run-playbook-step.sh +267 -1
  71. package/templates/permission-levels/level-1-locked.json +1 -1
  72. package/templates/permission-levels/level-2-guarded.json +1 -1
  73. package/templates/permission-levels/level-3-standard.json +1 -1
  74. package/templates/permission-levels/level-4-elevated.json +1 -1
  75. package/templates/permission-levels/level-5-unrestricted.json +1 -1
  76. package/templates/playbook-types.json +6 -0
  77. package/project/app/ui/dist/assets/index-BVR_7Owg.css +0 -1
  78. package/project/app/ui/dist/assets/index-CtU2xLQm.js +0 -104
  79. package/scripts/audit-twin-divergence.sh +0 -693
@@ -181,6 +181,18 @@
181
181
  # stdout). If that was the last step, emits a completion report instead (see OUTPUT SCHEMA)
182
182
  # and removes the state file.
183
183
  #
184
+ # RUNTIME TYPE VERIFICATION (E62_S02_T01): when a value IS given, it is checked against the
185
+ # CURRENT step's declared `output_types` (read off that step's own SKILL.md), via
186
+ # `scripts/validate-typed-object.sh` -- normalizing first, then re-checking, before any
187
+ # failure is declared. A `{when, type}` conditional declaration conforms if it satisfies AT
188
+ # LEAST ONE branch (no classifier is ever run, no branch state is tracked). A value that
189
+ # still does not conform after normalize is NOT stored -- this call is silently redirected
190
+ # into the exact same halt behavior as `advance <state_file> failed "<note>"` (see below),
191
+ # with a note naming the step, the declared type(s) checked, and the raw pre-normalization
192
+ # value. A step with no declared `output_types` (including one this call cannot resolve a
193
+ # SKILL.md for) is completely unaffected: no lookup is even attempted beyond determining
194
+ # there is nothing to check.
195
+ #
184
196
  # run-playbook-step.sh advance <state_file> skipped
185
197
  # Records the CURRENT step as SKIPPED (a new `skipped` list, distinct from `completed`) and
186
198
  # advances the pointer exactly like `passed` -- a skipped step never halts the chain and is
@@ -597,6 +609,30 @@ elif [ "$SUBCOMMAND" = "advance" ]; then
597
609
 
598
610
  PROJECT_DIR="$(resolve_project_dir)"
599
611
 
612
+ # --- E62_S02_T01: resolve the jenga-agent PACKAGE root, needed ONLY to look up a completing
613
+ # step's declared `output_types` off its SKILL.md for runtime type verification. Mirrors
614
+ # load-playbooks.sh's own PKG_ROOT resolution (monorepo checkout vs. installed npm package) --
615
+ # including reusing JENGA_PLAYBOOKS_TEST_ROOT verbatim as the SAME test-injection override, rather
616
+ # than inventing a second env var for the same concept. Deliberately gated on
617
+ # `OUTCOME = passed && EXTRA non-empty`: those are the ONLY two conditions under which
618
+ # verification does anything at all (see the no-op cases in this task's board item), so a
619
+ # `skipped`/`failed` call, or a `passed` call with no typed-output value, never pays for this
620
+ # resolution at all -- "no lookup even attempted beyond determining there is nothing to check."
621
+ PKG_ROOT=""
622
+ if [ "$OUTCOME" = "passed" ] && [ -n "$EXTRA" ]; then
623
+ ADVANCE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
624
+ if [ -n "${JENGA_PLAYBOOKS_TEST_ROOT:-}" ]; then
625
+ PKG_ROOT="$JENGA_PLAYBOOKS_TEST_ROOT"
626
+ elif [ -d "$ADVANCE_SCRIPT_DIR/../../../templates" ]; then
627
+ PKG_ROOT="$(cd "$ADVANCE_SCRIPT_DIR/../../.." && pwd)"
628
+ elif [ -d "$PROJECT_DIR/node_modules/@jenga-ai/agent/templates" ]; then
629
+ PKG_ROOT="$PROJECT_DIR/node_modules/@jenga-ai/agent"
630
+ else
631
+ 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
632
+ exit 2
633
+ fi
634
+ fi
635
+
600
636
  PY_SCRIPT="$(mktemp -t run-playbook-step-advance-XXXXXX.py)"
601
637
  trap 'rm -f "$PY_SCRIPT"' EXIT
602
638
 
@@ -604,7 +640,9 @@ elif [ "$SUBCOMMAND" = "advance" ]; then
604
640
  import json
605
641
  import os
606
642
  import re
643
+ import subprocess
607
644
  import sys
645
+ import tempfile
608
646
  from datetime import datetime, timezone
609
647
 
610
648
  state_file_path = sys.argv[1]
@@ -615,6 +653,221 @@ outcome = sys.argv[2]
615
653
  extra = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] != "" else None
616
654
  note = extra
617
655
  project_dir = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] != "" else None
656
+ # E62_S02_T01: the jenga-agent package root, resolved (or hard-errored) by the bash wrapper above,
657
+ # ONLY when outcome == "passed" and extra is non-empty. Empty string in every other case -- exactly
658
+ # the cases where verification is a documented no-op, so this is never even looked at.
659
+ pkg_root = sys.argv[5] if len(sys.argv) > 5 and sys.argv[5] != "" else None
660
+
661
+ # ---------------------------------------------------------------------------------------------
662
+ # E62_S02_T01: runtime type verification at the "passed" chokepoint
663
+ # ---------------------------------------------------------------------------------------------
664
+ # `extract_output_types`/`declared_type_entries` below are a DELIBERATE, DOCUMENTED duplication of
665
+ # skills/jenga/scripts/load-playbooks.sh's own `extract_types_field`/`extract_output_types`/
666
+ # `declared_type_entries` (see that script's "TYPE REGISTRY" section, around line 788 as of
667
+ # E62_S01_T04). The task considered factoring this into a shared, importable module, but every
668
+ # python body in both scripts is a throwaway file generated into a fresh `mktemp` path per
669
+ # invocation (see the `cat > "$PY_SCRIPT" <<'PY'` pattern this whole file uses) -- there is no
670
+ # existing precedent in this repo for a shared importable python module fed to that pattern, and
671
+ # building one purely for a ~30-line, rarely-changing frontmatter parser was judged riskier than a
672
+ # small, clearly-labelled duplicate. If this parser ever needs a THIRD copy, that is the signal to
673
+ # extract it for real; until then, a change to one copy's parsing rules should always be checked
674
+ # against the other by anyone touching either.
675
+ _FRONTMATTER_RE = re.compile(r'^---\r?\n(.*?)\r?\n---', re.DOTALL)
676
+
677
+
678
+ def extract_output_types(skill_md_path):
679
+ """Best-effort extraction of the `output_types` frontmatter field from a SKILL.md. Returns
680
+ None if the file/field is missing or unparseable, a `str` for the single-static-type form, or
681
+ a `list[dict]` for the `{when, type}` list form. Mirrors load-playbooks.sh's
682
+ `extract_types_field(path, "output_types")` exactly -- see the module-level note above."""
683
+ try:
684
+ with open(skill_md_path, encoding="utf-8") as fh:
685
+ content = fh.read()
686
+ except OSError:
687
+ return None
688
+
689
+ fm_match = _FRONTMATTER_RE.match(content)
690
+ if not fm_match:
691
+ return None
692
+
693
+ fm_lines = fm_match.group(1).splitlines()
694
+
695
+ for i, line in enumerate(fm_lines):
696
+ key_match = re.match(r'^output_types:\s*(.*)$', line)
697
+ if not key_match:
698
+ continue
699
+
700
+ rest = key_match.group(1).strip()
701
+ if rest:
702
+ return rest.strip('"\'')
703
+
704
+ # Block/list form: gather subsequent, more-indented lines into a list of dicts.
705
+ items = []
706
+ current = {}
707
+ j = i + 1
708
+ while j < len(fm_lines):
709
+ raw_line = fm_lines[j]
710
+ if not raw_line.strip():
711
+ j += 1
712
+ continue
713
+ if not raw_line[0].isspace():
714
+ break # a new top-level frontmatter key ends this block
715
+
716
+ stripped = raw_line.strip()
717
+ item_match = re.match(r'^-\s*(.*)$', stripped)
718
+ if item_match:
719
+ if current:
720
+ items.append(current)
721
+ current = {}
722
+ remainder = item_match.group(1)
723
+ kv = re.match(r'^([a-zA-Z_]+):\s*(.*)$', remainder) if remainder else None
724
+ if kv:
725
+ current[kv.group(1)] = kv.group(2).strip().strip('"\'')
726
+ else:
727
+ kv = re.match(r'^([a-zA-Z_]+):\s*(.*)$', stripped)
728
+ if kv:
729
+ current[kv.group(1)] = kv.group(2).strip().strip('"\'')
730
+ j += 1
731
+
732
+ if current:
733
+ items.append(current)
734
+ return items if items else None
735
+
736
+ return None
737
+
738
+
739
+ def declared_type_entries(value):
740
+ """Normalize an `output_types` value into `[(type_or_None, when_or_None), ...]`. Mirrors
741
+ load-playbooks.sh's `declared_type_entries` exactly -- see the module-level note above."""
742
+ if not value:
743
+ return []
744
+ if isinstance(value, str):
745
+ return [(value, None)]
746
+ if isinstance(value, list):
747
+ entries = []
748
+ for item in value:
749
+ if isinstance(item, dict):
750
+ entries.append((item.get("type") or None, item.get("when") or None))
751
+ else:
752
+ entries.append((None, None))
753
+ return entries
754
+ return []
755
+
756
+
757
+ def verify_typed_output(step_name, raw_value, pkg_root_dir):
758
+ """The E62_S02_T01 chokepoint check itself.
759
+
760
+ Returns `(failure_note_or_None, resolved_value)`:
761
+ - `(None, value)` -- conforms (or nothing declared/checkable -- a no-op). `value` is the
762
+ raw value unless a declared branch needed normalization to conform,
763
+ in which case it is that branch's normalized value.
764
+ - `(note, None)` -- every declared branch is non-conforming even after normalize. `note`
765
+ names the step, the declared type(s) checked, and the raw
766
+ pre-normalization value, for the existing `failed` -> `halted` path.
767
+
768
+ The conditional `{when, type}` rule (ratified at epic level, not re-derived here): the value
769
+ conforms if it satisfies AT LEAST ONE declared branch. No classifier script is ever run and no
770
+ branch state is tracked -- every declared type is checked against the SAME captured value.
771
+ """
772
+ skill_md_path = os.path.join(pkg_root_dir, "skills", step_name, "SKILL.md")
773
+ if not os.path.isfile(skill_md_path):
774
+ # No resolvable skill dir (e.g. a synthetic/test step name, or a stale board reference) --
775
+ # nothing to check against. Not this chokepoint's problem to diagnose further.
776
+ return None, raw_value
777
+
778
+ declared = extract_output_types(skill_md_path)
779
+ entries = [e for e in declared_type_entries(declared) if e[0] is not None]
780
+ if not entries:
781
+ # No declared output_types (or only malformed entries, which is a load-time concern this
782
+ # chokepoint does not re-litigate) -- a documented no-op, exactly as today.
783
+ return None, raw_value
784
+
785
+ validator = os.path.join(pkg_root_dir, "scripts", "validate-typed-object.sh")
786
+ if not os.path.isfile(validator):
787
+ print(
788
+ f"Error: type registry validator not found at {validator} -- cannot verify step "
789
+ f"'{step_name}'s declared output_types.",
790
+ file=sys.stderr,
791
+ )
792
+ sys.exit(2)
793
+
794
+ types_checked = []
795
+ branch_reasons = []
796
+ normalized_fallback = None # first branch that conforms only after normalize
797
+
798
+ for type_name, when_val in entries:
799
+ if type_name not in types_checked:
800
+ types_checked.append(type_name)
801
+
802
+ tmp_path = None
803
+ try:
804
+ with tempfile.NamedTemporaryFile(
805
+ mode="w", encoding="utf-8", delete=False, prefix="run-playbook-step-verify-"
806
+ ) as tmp:
807
+ tmp.write(raw_value)
808
+ tmp_path = tmp.name
809
+ proc = subprocess.run(
810
+ [validator, "--value-file", tmp_path, type_name],
811
+ capture_output=True,
812
+ text=True,
813
+ )
814
+ finally:
815
+ if tmp_path is not None:
816
+ try:
817
+ os.unlink(tmp_path)
818
+ except OSError:
819
+ pass
820
+
821
+ if proc.returncode == 0:
822
+ # Conforms as given, under this branch alone -- the strongest possible verdict.
823
+ # "At least one branch accepts it" is satisfied immediately; stop checking further
824
+ # branches and never report this as normalized.
825
+ return None, raw_value
826
+ if proc.returncode == 2:
827
+ if normalized_fallback is None:
828
+ try:
829
+ payload = json.loads(proc.stdout)
830
+ normalized_fallback = payload.get("value")
831
+ except (json.JSONDecodeError, AttributeError):
832
+ normalized_fallback = raw_value
833
+ continue
834
+ if proc.returncode == 3:
835
+ reason = None
836
+ try:
837
+ payload = json.loads(proc.stdout)
838
+ reason = payload.get("reason")
839
+ except (json.JSONDecodeError, AttributeError):
840
+ reason = None
841
+ branch = f" (when='{when_val}')" if when_val else ""
842
+ branch_reasons.append(f"type '{type_name}'{branch}: {reason or 'does not conform'}")
843
+ continue
844
+ # Exit 4 (unknown type), 5 (environment), 6 (registry contract violation) are
845
+ # ENVIRONMENT-level failures, never value judgments -- E62_S01_T02's own validator
846
+ # contract is explicit that these must not be conflated with non-conforming. A properly
847
+ # loaded chain should never reach an unknown/malformed type here (load-playbooks.sh's own
848
+ # load-time check, E62_S01_T04, already rejects that at confirmation time) -- if it
849
+ # happens anyway, this is a broken environment, not a playbook-author's bad value, so it
850
+ # is NOT routed through the failed/halted board mechanism.
851
+ print(
852
+ f"Error: scripts/validate-typed-object.sh reported an environment-level failure "
853
+ f"(exit {proc.returncode}) verifying step '{step_name}'s declared type '{type_name}': "
854
+ f"{proc.stderr.strip() or proc.stdout.strip()}",
855
+ file=sys.stderr,
856
+ )
857
+ sys.exit(2)
858
+
859
+ if normalized_fallback is not None:
860
+ # No branch conformed as given, but at least one conforms after its own normalize.
861
+ return None, normalized_fallback
862
+
863
+ # No declared branch accepts the value, as given or normalized.
864
+ types_list = ", ".join(types_checked)
865
+ detail = "; ".join(branch_reasons) if branch_reasons else "no declared branch accepts it"
866
+ failure_note = (
867
+ f"step '{step_name}' produced a value that does not conform to its declared "
868
+ f"output_types [{types_list}] (raw value: '{raw_value}'); {detail}"
869
+ )
870
+ return failure_note, None
618
871
 
619
872
  # --- E53_S04_T05: artifact persistence + redaction (persisted copy only -- see header) ---
620
873
  _ABS_PATH_RE = re.compile(r'(/[^\s"\']+)')
@@ -693,6 +946,19 @@ if idx >= len(steps):
693
946
  current_step = steps[idx]
694
947
  state.setdefault("skipped", [])
695
948
 
949
+ # E62_S02_T01: verify a "passed" step's typed-output value against its declared output_types
950
+ # BEFORE the outcome dispatch below, so a non-conforming value can redirect into the EXISTING
951
+ # `outcome == "failed"` branch immediately after -- no second failure mechanism is introduced.
952
+ # Both no-op cases (no declared output_types; no typed-output value at all) leave `outcome` and
953
+ # `extra` untouched, so this is byte-for-byte the pre-existing behavior for them.
954
+ if outcome == "passed" and extra is not None:
955
+ verify_failure_note, resolved_value = verify_typed_output(current_step, extra, pkg_root)
956
+ if verify_failure_note is not None:
957
+ outcome = "failed"
958
+ note = verify_failure_note
959
+ else:
960
+ extra = resolved_value
961
+
696
962
  if outcome == "failed":
697
963
  state["halted"] = True
698
964
  state["failed_step"] = current_step
@@ -757,7 +1023,7 @@ print(json.dumps({
757
1023
  print(f"STATE_FILE: {state_file_path}", file=sys.stderr)
758
1024
  PY
759
1025
 
760
- python3 "$PY_SCRIPT" "$STATE_FILE" "$OUTCOME" "$EXTRA" "$PROJECT_DIR"
1026
+ python3 "$PY_SCRIPT" "$STATE_FILE" "$OUTCOME" "$EXTRA" "$PROJECT_DIR" "$PKG_ROOT"
761
1027
  exit $?
762
1028
 
763
1029
  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)}}