@jenga-ai/agent 3.5.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 (136) hide show
  1. package/README.md +85 -78
  2. package/agents/developer.md +1 -1
  3. package/agents/scrum-master.md +20 -2
  4. package/agents/tester.md +3 -3
  5. package/hooks/on_session_end.sh +5 -5
  6. package/lib/generate-agent-context.js +2 -2
  7. package/lib/generate-copilot-hooks.js +1 -1
  8. package/lib/generate-skill-allow-list.js +79 -8
  9. package/lib/mirror.js +1 -1
  10. package/lib/postinstall-manifest.js +1 -1
  11. package/lib/skill-allow-list.json +2 -2
  12. package/mcp/help/index.js +8 -17
  13. package/mcp/help/scan.js +73 -0
  14. package/package.json +5 -1
  15. package/project/app/api/lib/resolve-project-root.js +1 -1
  16. package/project/app/api/parsers/knowledge-graph.js +100 -9
  17. package/project/app/api/routes/health.js +36 -0
  18. package/project/app/api/scripts/capture-snapshot.js +9 -6
  19. package/project/app/package.json +4 -0
  20. package/project/app/ui/dist/assets/index-BADc5mmH.css +1 -0
  21. package/project/app/ui/dist/assets/index-C3oiuli_.js +104 -0
  22. package/project/app/ui/dist/index.html +2 -2
  23. package/project/app/ui/package.json +4 -0
  24. package/project/app/ui/scripts/build-snapshot-html.cjs +63 -2
  25. package/scripts/acquire-concurrency-slot.sh +35 -5
  26. package/scripts/apply-j-prefix.sh +46 -5
  27. package/scripts/build-pages-site.sh +1 -1
  28. package/scripts/check-public-playbook-steps.sh +158 -52
  29. package/scripts/check-publicignore-match.sh +2 -2
  30. package/scripts/compute-deploy-reconcile.sh +5 -5
  31. package/scripts/delete-bare-skill-dirs.sh +329 -0
  32. package/scripts/generate-legacy-shipped-paths.js +2 -2
  33. package/scripts/idea_manager.sh +273 -3
  34. package/scripts/mark-deployed.sh +2 -2
  35. package/scripts/populate-knowledge-graph.entity-resolution.test.js +254 -0
  36. package/scripts/populate-knowledge-graph.js +213 -5
  37. package/scripts/populate-knowledge-graph.staleness.test.js +130 -0
  38. package/scripts/postinstall.js +1 -1
  39. package/scripts/release-concurrency-slot.sh +34 -4
  40. package/scripts/render-ranked-list.sh +270 -0
  41. package/scripts/repoint-dead-bare-path-prose.py +81 -0
  42. package/scripts/repoint-skill-refs.sh +539 -0
  43. package/scripts/rewrite-stale-skill-preambles.py +188 -0
  44. package/scripts/strip-polyfill-frontmatter.py +166 -0
  45. package/scripts/todo_manager.sh +16 -1
  46. package/scripts/validate-typed-object.sh +750 -0
  47. package/scripts/verify-legacy-seed-reconcile.sh +10 -10
  48. package/scripts/verify-postinstall-reconcile.sh +7 -7
  49. package/scripts/write-context-digest.sh +1 -1
  50. package/skills/j-brainstorm/SKILL.md +3 -4
  51. package/skills/j-btw/SKILL.md +3 -4
  52. package/skills/j-clearify/SKILL.md +5 -6
  53. package/skills/j-close-story/SKILL.md +11 -12
  54. package/skills/j-close-story/scripts/check-privatized.sh +4 -4
  55. package/skills/j-close-story/scripts/check-story-closeable.sh +11 -4
  56. package/skills/j-commit/SKILL.md +3 -4
  57. package/skills/j-continue/SKILL.md +5 -6
  58. package/skills/j-deep-dive/SKILL.md +3 -4
  59. package/skills/j-distribute/CONFIG_SCHEMA.md +82 -5
  60. package/skills/j-distribute/SKILL.md +3 -4
  61. package/skills/j-do/SKILL.md +100 -18
  62. package/skills/j-doc/SKILL.md +3 -4
  63. package/skills/j-doc-sync/SKILL.md +4 -4
  64. package/skills/j-dooo/SKILL.md +6 -15
  65. package/skills/j-error/SKILL.md +3 -4
  66. package/skills/j-evaluate/SKILL.md +3 -4
  67. package/skills/j-examplify/SKILL.md +3 -4
  68. package/skills/j-gitignore/SKILL.md +157 -0
  69. package/skills/j-gitignore/assets/jenga-paths.txt +50 -0
  70. package/skills/j-gitignore/scripts/_catalog.sh +105 -0
  71. package/skills/j-gitignore/scripts/audit-gitignore.sh +194 -0
  72. package/skills/j-gitignore/scripts/repair-gitignore.sh +226 -0
  73. package/skills/j-gitignore/scripts/untrack-jenga-files.sh +210 -0
  74. package/skills/j-help/SKILL.md +3 -4
  75. package/skills/j-idea/SKILL.md +80 -9
  76. package/skills/j-idea/assets/idea_template.md +1 -1
  77. package/skills/j-improve/SKILL.md +4 -5
  78. package/skills/j-init/SKILL.md +23 -14
  79. package/skills/j-init/assets/scope-thresholds_template.json +5 -2
  80. package/skills/j-init/scripts/apply-scaffold-visibility.sh +9 -7
  81. package/skills/j-init/scripts/init.sh +4 -4
  82. package/skills/j-jbp/SKILL.md +3 -4
  83. package/skills/j-lgtm/SKILL.md +3 -4
  84. package/skills/j-pi-plan/SKILL.md +3 -4
  85. package/skills/j-playbook/SKILL.md +1 -1
  86. package/skills/j-proceed/SKILL.md +3 -4
  87. package/skills/j-publish/SKILL.md +4 -5
  88. package/skills/j-publish/adapters/npm-ci.md +6 -1
  89. package/skills/j-publish/adapters/npm.md +1 -1
  90. package/skills/j-publish/scripts/generate_release_notes.sh +1 -1
  91. package/skills/j-publish/scripts/npm_stage_inspect.sh +61 -0
  92. package/skills/j-publish/scripts/run_gates.sh +1 -1
  93. package/skills/j-reconcile/SKILL.md +40 -7
  94. package/skills/j-reconcile/assets/report_format.md +11 -0
  95. package/skills/j-reconcile/scripts/detect-unlinked-code.sh +13 -13
  96. package/skills/j-reconcile-origin/SKILL.md +3 -4
  97. package/skills/j-redo/SKILL.md +4 -5
  98. package/skills/j-skillify/SKILL.md +3 -4
  99. package/skills/j-spinoff/SKILL.md +4 -5
  100. package/skills/j-status/SKILL.md +18 -4
  101. package/skills/j-todo/SKILL.md +44 -5
  102. package/skills/j-todo/scripts/argument-is-not-ranked-list.sh +92 -0
  103. package/skills/j-todo/scripts/argument-is-ranked-list.sh +78 -0
  104. package/skills/j-uncharted/SKILL.md +252 -13
  105. package/skills/j-uncharted/scripts/detect-dependencies.sh +80 -22
  106. package/skills/j-uncharted/scripts/detect-tests.sh +1 -1
  107. package/skills/j-uncharted/scripts/diff-since-baseline.sh +600 -0
  108. package/skills/j-uncharted/scripts/elicitation-state.sh +1 -1
  109. package/skills/j-uncharted/scripts/find-scan-baseline.sh +545 -0
  110. package/skills/j-uncharted/scripts/run-engine.sh +36 -2
  111. package/skills/j-uncharted/scripts/validate-proposed-items.sh +1 -1
  112. package/skills/j-uncharted/scripts/write-scan-record.sh +361 -0
  113. package/skills/j-wtf/SKILL.md +4 -5
  114. package/skills/jenga/SKILL.md +106 -11
  115. package/skills/jenga/playbooks/board-hygiene.json +32 -0
  116. package/skills/jenga/playbooks/schema.json +73 -6
  117. package/skills/jenga/playbooks/understand-then-commit.json +19 -0
  118. package/skills/jenga/scripts/load-nl-catalog.js +5 -2
  119. package/skills/jenga/scripts/load-nl-catalog.sh +1 -1
  120. package/skills/jenga/scripts/load-playbooks.sh +290 -5
  121. package/skills/jenga/scripts/match-playbook.sh +4 -4
  122. package/skills/jenga/scripts/run-playbook-step.sh +267 -1
  123. package/templates/SCRUM_BOARD_SCHEMA.md +14 -1
  124. package/templates/SKILL_TEMPLATE.md +12 -0
  125. package/templates/permission-levels/level-1-locked.json +1 -1
  126. package/templates/permission-levels/level-2-guarded.json +1 -1
  127. package/templates/permission-levels/level-3-standard.json +1 -1
  128. package/templates/permission-levels/level-4-elevated.json +2 -2
  129. package/templates/permission-levels/level-5-unrestricted.json +2 -2
  130. package/templates/playbook-types.json +40 -6
  131. package/project/app/ui/dist/assets/index-7fj-vllY.js +0 -104
  132. package/project/app/ui/dist/assets/index-CdK3Qrep.css +0 -1
  133. package/scripts/audit-twin-divergence.sh +0 -625
  134. package/scripts/generate-j-alias.sh +0 -333
  135. package/skills/j-dev-done/SKILL.md +0 -53
  136. package/skills/j-dev-done/scripts/classify-commit-outcome.sh +0 -114
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://jenga.local/schemas/jenga-playbook.schema.json",
4
4
  "title": "Jenga Multi-Skill Playbook",
5
- "description": "Schema for a single multi-skill playbook definition consumed by skills/jenga/scripts/load-playbooks.sh (E53_S02_T01). A playbook is a dedicated, versionable data file describing an ORDERED chain of skills that /jenga's natural-language branch may propose (as an editable, confirmable numbered list -- see skills/jenga/scripts/render-playbook-confirmation.sh, E53_S02_T03) when free-text intent spans more than one skill and does not cleanly resolve to a single one via skills/route/SKILL.md's Step 2 matching. This file itself (schema.json) is never treated as a playbook -- load-playbooks.sh explicitly excludes it by filename when scanning skills/jenga/playbooks/*.json.",
5
+ "description": "Schema for a single multi-skill playbook definition consumed by skills/jenga/scripts/load-playbooks.sh (E53_S02_T01). A playbook is a dedicated, versionable data file describing an ORDERED chain of skills that /jenga's natural-language branch may propose (as an editable, confirmable numbered list -- see skills/jenga/scripts/render-playbook-confirmation.sh, E53_S02_T03) when free-text intent spans more than one skill and does not cleanly resolve to a single one via skills/jenga/SKILL.md's inlined Skill Matching & Invocation Contract. This file itself (schema.json) is never treated as a playbook -- load-playbooks.sh explicitly excludes it by filename when scanning skills/jenga/playbooks/*.json.",
6
6
  "type": "object",
7
7
  "required": ["id", "name", "description", "keywords", "examples", "steps"],
8
8
  "additionalProperties": false,
@@ -18,25 +18,92 @@
18
18
  },
19
19
  "description": {
20
20
  "type": "string",
21
- "description": "One-sentence explanation of what this playbook accomplishes end-to-end, used as the lowest-priority match signal (description match, same as skills/route/SKILL.md's Step 2 Pass 3) when keywords/examples don't produce a confident match."
21
+ "description": "One-sentence explanation of what this playbook accomplishes end-to-end, used as the lowest-priority match signal (description match, same as skills/jenga/SKILL.md's inlined Skill Matching & Invocation Contract Pass 3) when keywords/examples don't produce a confident match."
22
22
  },
23
23
  "keywords": {
24
24
  "type": "array",
25
- "description": "Short phrases (1-3 words) for verbatim, case-insensitive keyword matching against the raw natural-language prompt -- the highest-priority match signal (Pass 1), mirroring skills/route/SKILL.md's Step 2 Pass 1 semantics exactly, but scoped to this playbook's catalog rather than the single-skill catalog.",
25
+ "description": "Short phrases (1-3 words) for verbatim, case-insensitive keyword matching against the raw natural-language prompt -- the highest-priority match signal (Pass 1), mirroring skills/jenga/SKILL.md's inlined Skill Matching & Invocation Contract Pass 1 semantics exactly, but scoped to this playbook's catalog rather than the single-skill catalog.",
26
26
  "items": { "type": "string" },
27
27
  "minItems": 1
28
28
  },
29
29
  "examples": {
30
30
  "type": "array",
31
- "description": "Natural-language example prompts a user might type that should resolve to this playbook. Used for the semantic similarity match (Pass 2), mirroring skills/route/SKILL.md's Step 2 Pass 2 semantics. At least one example must plausibly span the full breadth of this playbook's steps (not just its first step) so it is distinguishable from a plain single-skill match.",
31
+ "description": "Natural-language example prompts a user might type that should resolve to this playbook. Used for the semantic similarity match (Pass 2), mirroring skills/jenga/SKILL.md's inlined Skill Matching & Invocation Contract Pass 2 semantics. At least one example must plausibly span the full breadth of this playbook's steps (not just its first step) so it is distinguishable from a plain single-skill match.",
32
32
  "items": { "type": "string" },
33
33
  "minItems": 1
34
34
  },
35
35
  "steps": {
36
36
  "type": "array",
37
- "description": "Ordered list of canonical skill DIRECTORY names (the directory under skills/<dir>/SKILL.md, e.g. \"j-brainstorm\", not the \"j.brainstorm\" frontmatter/invocation form and not the retired bare \"brainstorm\") that make up this playbook's chain, in the exact execution order. Per E50_S10's canonical naming contract the canonical directory is skills/j-<name>/, so a step value carries the j- prefix; the three permanent exceptions (jenga, jenga-permission-level, index) keep their bare directory names and are written bare. Each entry MUST resolve to an existing skills/<dir>/SKILL.md at load time -- load-playbooks.sh skips (with a stderr warning) any playbook referencing a nonexistent skill rather than silently including a broken chain in the catalog.",
38
- "items": { "type": "string" },
37
+ "description": "Ordered list of steps making up this playbook's chain, in the exact execution order. A step is either a BARE STRING or a STEPOBJECT (E53_S03; composition added by E53_S05). A bare string is shorthand for {\"skill\": \"<name>\"} -- \"bare\" describes the JSON shape, never a bare skill NAME. A skill name is always a canonical skill DIRECTORY name (the directory under skills/<dir>/SKILL.md, e.g. \"j-brainstorm\", not the \"j.brainstorm\" frontmatter/invocation form and not the retired bare \"brainstorm\"); per E50_S10's canonical naming contract the canonical directory is skills/j-<name>/, so a step value carries the j- prefix, while the three permanent exceptions (jenga, jenga-permission-level, index) keep their bare directory names and are written bare. Each skill name MUST resolve to an existing skills/<dir>/SKILL.md at load time -- load-playbooks.sh skips (with a stderr warning) any playbook referencing a nonexistent skill rather than silently including a broken chain in the catalog. NOTE: this schema documents SHAPE only. The cross-step rules -- forward_from naming an EARLIER step whose skill declares a non-empty output_types, conditional.depends_on naming an EARLIER step, composition cycle/depth limits -- are not expressible here and are enforced by load-playbooks.sh, which remains the single source of truth for validation.",
38
+ "items": {
39
+ "oneOf": [
40
+ {
41
+ "type": "string",
42
+ "description": "Bare-string step: shorthand for {\"skill\": \"<name>\"}, unchanged original behavior. An all-bare-string playbook loads byte-for-byte the same as it did before E53_S03, and a bare-string step is never rewritten into object form in the loader's output."
43
+ },
44
+ { "$ref": "#/definitions/stepObject" }
45
+ ]
46
+ },
39
47
  "minItems": 2
40
48
  }
49
+ },
50
+ "definitions": {
51
+ "stepObject": {
52
+ "type": "object",
53
+ "description": "A StepObject step (E53_S03_T01). Carries EXACTLY ONE of the mutually exclusive target fields `skill` or `playbook` -- a step naming both, or naming neither, is rejected by load-playbooks.sh.",
54
+ "additionalProperties": false,
55
+ "oneOf": [
56
+ { "required": ["skill"], "not": { "required": ["playbook"] } },
57
+ { "required": ["playbook"], "not": { "required": ["skill"] } }
58
+ ],
59
+ "properties": {
60
+ "skill": {
61
+ "type": "string",
62
+ "description": "Invokes a single skill, same as a bare string. Value is a canonical skill directory name (see the `steps` description above)."
63
+ },
64
+ "playbook": {
65
+ "type": "string",
66
+ "description": "Composes another playbook by ID (E53_S05). Resolved and flattened in place at load time, so the emitted catalog's `steps` array never contains a raw playbook-type entry. Existence validation, cycle detection, and a configurable nesting-depth limit (project/configs/playbook-config.json's max_composition_depth, default 3) all apply."
67
+ },
68
+ "instruction": {
69
+ "type": "string",
70
+ "description": "Static natural-language text appended to this step's own invocation message."
71
+ },
72
+ "forward_from": {
73
+ "type": "string",
74
+ "description": "Names an EARLIER step in the same playbook (by that step's skill name) whose typed output becomes this step's actual invocation input. The named source's SKILL.md must declare a non-empty output_types -- a skill declaring nothing can never be a forward source. Transparent across composition boundaries, since composition resolution runs before this validation."
75
+ },
76
+ "resolve": {
77
+ "type": "string",
78
+ "description": "Natural-language instructions for reshaping/filtering/type-bridging the value forwarded into this step (e.g. \"pick the first three items\"). A step carrying both `resolve` and `playbook` is rejected at load time: resolve may never pre-authorize a downstream confirmation gate (E53_S07's safety review, permanently rejected)."
79
+ },
80
+ "conditional": {
81
+ "type": "object",
82
+ "description": "Runs this step only when the predicate holds against a named earlier step's captured output; otherwise the step is marked `skipped` and the chain continues without halting (E53_S04_T02). Unlike forward_from, this does NOT require the depended-on step to declare output_types.",
83
+ "additionalProperties": false,
84
+ "required": ["depends_on", "predicate"],
85
+ "properties": {
86
+ "depends_on": {
87
+ "type": "string",
88
+ "description": "Names an EARLIER skill step in this playbook whose captured output the predicate is evaluated against.",
89
+ "minLength": 1
90
+ },
91
+ "predicate": {
92
+ "type": "string",
93
+ "description": "The recognized predicate grammar, whose single source of truth is skills/jenga/scripts/run-playbook-step.sh: non_empty | empty | equals:<value> | not_equals:<value>.",
94
+ "pattern": "^(non_empty|empty|equals:.+|not_equals:.+)$"
95
+ }
96
+ }
97
+ },
98
+ "version": {
99
+ "type": "string",
100
+ "description": "RESERVED, currently a no-op. Accepted and passed through unchanged; not acted upon by anything yet. Exists so a future schema revision has a place to declare itself without a retroactive migration of every existing playbook."
101
+ },
102
+ "schema_version": {
103
+ "type": "string",
104
+ "description": "Alternate key name for `version`. Same reserved no-op semantics."
105
+ }
106
+ }
107
+ }
41
108
  }
42
109
  }
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "understand-then-commit",
3
+ "name": "Understand Then Build",
4
+ "description": "Investigates existing or unfamiliar code first, then runs the full idea-to-commit pipeline on top of what was learned -- a composed chain built from the canonical idea-to-committed playbook, terminating at the commit.",
5
+ "keywords": [
6
+ "understand then build",
7
+ "investigate then build",
8
+ "explore then implement",
9
+ "onboard and build",
10
+ "learn the codebase first"
11
+ ],
12
+ "examples": [
13
+ "help me understand this codebase first, then plan a change here, build it, and commit it",
14
+ "investigate this existing code, then take an idea through planning, implementation, and a commit",
15
+ "look into what's already here, then brainstorm a fix, build it, and commit the work",
16
+ "onboard me to this project and then walk a new idea from planning through to a committed change"
17
+ ],
18
+ "steps": ["j-uncharted", {"playbook": "idea-to-committed"}]
19
+ }
@@ -68,8 +68,11 @@ import { join } from "path";
68
68
  import { pathToFileURL } from "url";
69
69
 
70
70
  // The three permanent exceptions to the `j-<name>` canonical directory convention — see
71
- // docs/skill-authoring.md's Canonical Naming Contract and scripts/audit-twin-divergence.sh's
72
- // NEVER_TWINNED list, which this mirrors.
71
+ // docs/skill-authoring.md's Canonical Naming Contract, and scripts/repoint-skill-refs.sh's
72
+ // REPOINT_SKILL_REFS_EXCEPTIONS list, which this mirrors. (It previously mirrored the
73
+ // twin-divergence audit script's NEVER_TWINNED tuple; that script was deleted when
74
+ // E42_S07 retired the twin-parity gate, so both this comment and the cross-check in
75
+ // tests/load-nl-catalog-twin-resolution.bats were repointed at the surviving list.)
73
76
  const NEVER_TWINNED = new Set(["jenga", "jenga-permission-level", "index"]);
74
77
 
75
78
  function canonicalSkillDir(name) {
@@ -46,7 +46,7 @@ fi
46
46
 
47
47
  # Resolve the jenga-agent PACKAGE root (where lib/generate-skill-allow-list.js and the canonical
48
48
  # skills/ tree actually live) — same monorepo-checkout vs. installed-npm-package detection used
49
- # by skills/init/scripts/init.sh's PKG_ROOT resolution.
49
+ # by skills/j-init/scripts/init.sh's PKG_ROOT resolution.
50
50
  if [ -d "$SCRIPT_DIR/../../../templates" ]; then
51
51
  PKG_ROOT="$SCRIPT_DIR/../../.."
52
52
  elif [ -d "$JENGA_PROJECT_DIR/node_modules/@jenga-ai/agent/templates" ]; then
@@ -130,6 +130,84 @@
130
130
  # anywhere in this loop. `conditional.depends_on` works identically, by the same mechanism.
131
131
  #
132
132
  # ---------------------------------------------------------------------------
133
+ # TYPE REGISTRY (E62_S01_T04)
134
+ # ---------------------------------------------------------------------------
135
+ # The canonical type vocabulary this script validates against is `templates/playbook-types.json`
136
+ # (E53_S03_T02, converted into a map of type descriptors by E62_S01_T01), resolved as
137
+ # `<PKG_ROOT>/templates/playbook-types.json` — the SAME `PKG_ROOT` that already resolves
138
+ # `PLAYBOOKS_DIR`/`SKILLS_DIR` above, so the `JENGA_PLAYBOOKS_TEST_ROOT` override (see "TESTING
139
+ # OVERRIDE") redirects it too, and a fixture tree may supply its own vocabulary. The vocabulary is
140
+ # ALWAYS read from that file's `types` map — this script never hardcodes a type name, and adding a
141
+ # type to the registry is a data-only edit that requires no change here (see that file's own
142
+ # `_comment`, "EXTENDING").
143
+ #
144
+ # Two load-time checks are driven from it. Both are about DECLARATIONS ONLY — what a SKILL.md
145
+ # claims — never about an actual runtime VALUE. Verifying a value against its declared type is a
146
+ # separate concern owned by `scripts/validate-typed-object.sh` (E62_S01_T02) and consumed at
147
+ # runtime by `run-playbook-step.sh` (E62_S02); this script never calls it and never sees a value.
148
+ #
149
+ # CHECK 1 — REGISTRY VOCABULARY. For EVERY skill named by a step in the final flattened step
150
+ # list (bare-string steps included, not only StepObjects, and not only `forward_from` sources),
151
+ # every type value the skill declares in its `output_types` or `input_types` frontmatter must be
152
+ # a key in the registry's `types` map. An unrecognized value (e.g. `output_types: banana`) skips
153
+ # the whole playbook with a stderr warning naming the offending value, the field it came from,
154
+ # and — for the `{when, type}` list form — the branch it came from. This check is INDEPENDENT of
155
+ # `forward_from`: it has repo-wide value on a playbook with no forward edges at all.
156
+ #
157
+ # CHECK 2 — OUTPUT/INPUT COMPATIBILITY. Where a step declares `forward_from`, the SOURCE's
158
+ # declared `output_types` must be accepted by the CONSUMER's (i.e. this step's own skill's)
159
+ # declared `input_types`. This runs AFTER the existing declaredness and Blocker-1 structural
160
+ # checks above, so a source that declares nothing, or declares a malformed `{when, type}` entry,
161
+ # is still rejected by its own pre-existing message rather than by this one.
162
+ #
163
+ # THE ALL-BRANCHES RULE — both sides. Either side's declaration may be a single static type string
164
+ # or a list of `{when, type}` branches. This check never needs to know which branch will actually
165
+ # fire on either side, and never executes a classifier script to find out:
166
+ #
167
+ # Compatible IFF every type the SOURCE could produce is accepted under EVERY CONSUMER branch.
168
+ # Equivalently: the set of source types must be a subset of the INTERSECTION of the consumer's
169
+ # accepted types.
170
+ #
171
+ # The source half of that rule (every source branch must produce a type the consumer accepts; any
172
+ # branch that does not rejects the playbook, naming the offending branch) is the rule as originally
173
+ # specified in E53. The CONSUMER half — what a conditional consumer, one whose own `input_types` is
174
+ # a `{when, type}` list, means for compatibility — was left open by E62_S01_T03 and is RATIFIED
175
+ # HERE (2026-09-19, E62_S01_T04) as the conservative dual stated above.
176
+ #
177
+ # Rationale: which consumer branch applies at runtime is no more knowable at load time than which
178
+ # source branch applies. The only answer that is safe regardless of BOTH is the one that holds
179
+ # under both universally. It stays fully deterministic and executes no classifier script. No
180
+ # skill in this repository declares a conditional `input_types` today, so the ruling costs
181
+ # nothing now — it exists to close the ambiguity before it can bite.
182
+ #
183
+ # Mechanically, each consumer branch accepts exactly one type, so the intersection is `{t}` when
184
+ # every branch declares the same `t`, and EMPTY when the branches disagree. An empty intersection
185
+ # rejects every forward into that consumer. That is the intended conservative outcome of the
186
+ # ruling, not an accident of the implementation.
187
+ #
188
+ # BACKWARD COMPATIBILITY — BINDING. A consumer that declares NO `input_types` at all retains
189
+ # today's behavior EXACTLY: the forward is allowed on the existing source-declaredness check alone,
190
+ # and Check 2 short-circuits before any comparison is made. The ABSENCE of a declaration is never a
191
+ # violation. Adding the input side must not, and does not, retroactively break a single existing
192
+ # playbook. (`docs/skill-authoring.md`'s `input_types` section states the same guarantee from the
193
+ # skill author's side.)
194
+ #
195
+ # `text` IS NOT A WILDCARD. `text` carries `"verify": null` in the registry because prose has no
196
+ # checkable shape, but it is a specific type in this compatibility lattice — never an `any`. It is
197
+ # neither an `output_types` that satisfies every `input_types` nor an `input_types` that accepts
198
+ # every `output_types`. Treating it as a universal acceptor would make every text-declaring skill
199
+ # compatible with everything and render this check decorative, which is precisely what E53_S11's
200
+ # honesty-over-breadth policy exists to prevent. Nothing in this script special-cases it.
201
+ #
202
+ # MISSING REGISTRY — DELIBERATE FAIL-OPEN. If the registry file is absent or unparseable, CHECK 1
203
+ # is a silent no-op (no warning, no rejection); Check 2 is unaffected, since comparing declared
204
+ # type NAMES needs no vocabulary. This matches this script's existing convention for an absent
205
+ # optional input (a missing `project/.playbooks/` directory, a missing `playbook-config.json`), and
206
+ # it is unreachable in a real invocation: PKG_ROOT detection is itself keyed on `templates/`
207
+ # existing. It exists so a fixture tree under `JENGA_PLAYBOOKS_TEST_ROOT` may supply its own
208
+ # registry, or deliberately supply none.
209
+ #
210
+ # ---------------------------------------------------------------------------
133
211
  # CONDITIONAL RESOLUTION (E53_S04_T02)
134
212
  # ---------------------------------------------------------------------------
135
213
  # A step's `conditional: {"depends_on": "<name>", "predicate": "<predicate>"}` is validated at load
@@ -418,6 +496,20 @@
418
496
  # - A `conditional`'s `predicate` does not match the recognized grammar
419
497
  # (`non_empty`/`empty`/`equals:<value>`/`not_equals:<value>`, defined
420
498
  # in `run-playbook-step.sh`'s own header) (E53_S04_T02) -> skipped
499
+ # - ANY step's skill declares an `output_types`/`input_types` value
500
+ # that is not a key in `templates/playbook-types.json`'s `types`
501
+ # map (Check 1) (E62_S01_T04) -> skipped
502
+ # (applies to every skill-type step, bare string included -- not only
503
+ # `forward_from` sources; a silent no-op if the registry file itself is
504
+ # missing/unparseable, see "TYPE REGISTRY" above)
505
+ # - A `forward_from` source declares an `output_types` the consumer step's
506
+ # own `input_types` does not accept under EVERY consumer branch
507
+ # (Check 2, the all-branches rule) (E62_S01_T04) -> skipped
508
+ # (a consumer declaring NO `input_types` is never a violation -- the
509
+ # forward is allowed on the source-declaredness check alone, exactly as
510
+ # before this task)
511
+ # - A consumer's `input_types` list carries an entry missing `when`/`type`
512
+ # (E62_S01_T04) -> skipped
421
513
  #
422
514
  # ---------------------------------------------------------------------------
423
515
  # EXIT CODES
@@ -474,7 +566,7 @@ else
474
566
 
475
567
  # Resolve the jenga-agent PACKAGE root (where the canonical skills/ tree actually lives) — same
476
568
  # monorepo-checkout vs. installed-npm-package detection used by
477
- # skills/jenga/scripts/load-nl-catalog.sh's PKG_ROOT resolution and skills/init/scripts/init.sh.
569
+ # skills/jenga/scripts/load-nl-catalog.sh's PKG_ROOT resolution and skills/j-init/scripts/init.sh.
478
570
  if [ -d "$SCRIPT_DIR/../../../templates" ]; then
479
571
  PKG_ROOT="$SCRIPT_DIR/../../.."
480
572
  PROJECT_DIR="$JENGA_PROJECT_DIR"
@@ -515,6 +607,11 @@ project_dir = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] else None
515
607
  # --- E53_S06_T02: additive `lookup <id>` CLI mode --------------------------------------------
516
608
  mode = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else "catalog"
517
609
  lookup_id = sys.argv[5] if len(sys.argv) > 5 else ""
610
+ # --- E62_S01_T04: the jenga-agent PACKAGE root, where `templates/playbook-types.json` lives ---
611
+ # Already resolved by the bash wrapper above (and already honoring JENGA_PLAYBOOKS_TEST_ROOT) --
612
+ # threaded in here rather than re-derived, so there is exactly one PKG_ROOT resolution in this
613
+ # script. See header "TYPE REGISTRY".
614
+ pkg_root = sys.argv[6] if len(sys.argv) > 6 and sys.argv[6] else None
518
615
 
519
616
  # --- E53_S09_T01: project-local playbook source directory ------------------------------------
520
617
  # `project/.playbooks/`, resolved relative to the SAME project_dir already threaded above (which
@@ -568,6 +665,32 @@ def load_max_composition_depth(proj_dir):
568
665
 
569
666
  MAX_COMPOSITION_DEPTH = load_max_composition_depth(project_dir)
570
667
 
668
+
669
+ # --- E62_S01_T04: the canonical type vocabulary -------------------------------------------------
670
+ def load_type_registry(root):
671
+ """Read the set of known type names from `<root>/templates/playbook-types.json`'s `types` map.
672
+
673
+ Returns None when the registry is missing or unparseable, which makes the vocabulary check a
674
+ deliberate silent no-op -- see header 'TYPE REGISTRY' ("MISSING REGISTRY"). The vocabulary is
675
+ ALWAYS read from that file; no type name is ever hardcoded here, so adding a type to the
676
+ registry is a data-only edit requiring no change to this script.
677
+ """
678
+ if not root:
679
+ return None
680
+ registry_path = os.path.join(root, "templates", "playbook-types.json")
681
+ try:
682
+ with open(registry_path, encoding="utf-8") as fh:
683
+ registry = json.load(fh)
684
+ except (OSError, ValueError):
685
+ return None
686
+ types = registry.get("types") if isinstance(registry, dict) else None
687
+ if not isinstance(types, dict):
688
+ return None
689
+ return set(types.keys())
690
+
691
+
692
+ TYPE_REGISTRY = load_type_registry(pkg_root)
693
+
571
694
  catalog = []
572
695
  # --- E53_S06_T02: per-basename skip-reason capture -------------------------------------------
573
696
  # Threaded through PASS 1, PASS 2 (resolve_playbook), and PASS 3 below -- whenever a playbook
@@ -662,8 +785,11 @@ def step_skill_name(step):
662
785
  _FRONTMATTER_RE = re.compile(r'^---\r?\n(.*?)\r?\n---', re.DOTALL)
663
786
 
664
787
 
665
- def extract_output_types(skill_md_path):
666
- """Best-effort extraction of the `output_types` frontmatter field from a SKILL.md.
788
+ def extract_types_field(skill_md_path, field):
789
+ """Best-effort extraction of a type-declaration frontmatter field from a SKILL.md.
790
+
791
+ `field` is `output_types` or `input_types` (E62_S01_T04) -- both take the SAME two shapes (see
792
+ docs/skill-authoring.md), so they are read by this one parser rather than two copies of it.
667
793
 
668
794
  Returns None if the file/field is missing or unparseable, a `str` for the single-static-type
669
795
  form, or a `list[dict]` for the `{when, type}` list form. This is a small, targeted parser for
@@ -684,7 +810,7 @@ def extract_output_types(skill_md_path):
684
810
  fm_lines = fm_match.group(1).splitlines()
685
811
 
686
812
  for i, line in enumerate(fm_lines):
687
- key_match = re.match(r'^output_types:\s*(.*)$', line)
813
+ key_match = re.match(r'^%s:\s*(.*)$' % re.escape(field), line)
688
814
  if not key_match:
689
815
  continue
690
816
 
@@ -727,6 +853,104 @@ def extract_output_types(skill_md_path):
727
853
  return None
728
854
 
729
855
 
856
+ def extract_output_types(skill_md_path):
857
+ """The `output_types` half of extract_types_field -- kept as a named wrapper so every
858
+ pre-existing call site (E53_S03_T03/T04) reads exactly as it did before E62_S01_T04."""
859
+ return extract_types_field(skill_md_path, "output_types")
860
+
861
+
862
+ def extract_input_types(skill_md_path):
863
+ """The `input_types` half of extract_types_field (E62_S01_T04)."""
864
+ return extract_types_field(skill_md_path, "input_types")
865
+
866
+
867
+ # --- E62_S01_T04: shared normalization of either declaration shape ------------------------------
868
+ def declared_type_entries(value):
869
+ """Normalize an `output_types`/`input_types` value into `[(type_or_None, when_or_None), ...]`.
870
+
871
+ A single static type string yields ONE entry whose `when` is None; a `{when, type}` list yields
872
+ one entry per branch. A malformed list entry (not an object, or missing `type`) yields a
873
+ `(None, when_or_None)` entry rather than being dropped, so a caller can tell "no branches" from
874
+ "a branch this function could not read". Both checks in header 'TYPE REGISTRY' consume this, so
875
+ neither re-derives the two shapes.
876
+ """
877
+ if not value:
878
+ return []
879
+ if isinstance(value, str):
880
+ return [(value, None)]
881
+ if isinstance(value, list):
882
+ entries = []
883
+ for item in value:
884
+ if isinstance(item, dict):
885
+ entries.append((item.get("type") or None, item.get("when") or None))
886
+ else:
887
+ entries.append((None, None))
888
+ return entries
889
+ return []
890
+
891
+
892
+ def validate_declared_vocabulary(idx, skill_name, skill_md_path, registry):
893
+ """CHECK 1 -- every type value this skill declares must be a key in the registry's `types` map.
894
+
895
+ Returns an error string naming the offending value (and its branch, for the list form), or None.
896
+ A None `registry` never reaches here (the caller skips the check entirely -- see header
897
+ 'TYPE REGISTRY', "MISSING REGISTRY").
898
+ """
899
+ known = ", ".join(sorted(registry)) if registry else "<none>"
900
+ for field in ("output_types", "input_types"):
901
+ declared = extract_types_field(skill_md_path, field)
902
+ for type_name, when_val in declared_type_entries(declared):
903
+ if type_name is None:
904
+ # A malformed `{when, type}` entry. Deliberately NOT this check's business: the
905
+ # pre-existing Blocker-1 structural check (E53_S03_T04) already owns that rejection
906
+ # for a forward source, with its own message.
907
+ continue
908
+ if type_name not in registry:
909
+ branch = " (branch when='%s')" % when_val if when_val else ""
910
+ return (
911
+ f"step {idx} skill '{skill_name}' declares {field} '{type_name}'{branch}, "
912
+ f"which is not a type in templates/playbook-types.json "
913
+ f"(known types: {known})"
914
+ )
915
+ return None
916
+
917
+
918
+ def accepted_input_types(value):
919
+ """The CONSUMER side of the all-branches rule (E62_S01_T04, ratified 2026-09-19).
920
+
921
+ Returns `(accepted_set_or_None, error_or_None)`:
922
+
923
+ - `(None, None)` -- the consumer declares NO `input_types`. This is the BINDING backward-
924
+ compatible case: the caller must allow the forward on the source-
925
+ declaredness check alone, exactly as before this task. It is distinct
926
+ from `(set(), None)` ("declares branches that agree on nothing"), and
927
+ conflating the two would be precisely the retroactive break the task
928
+ forbids.
929
+ - `(set, None)` -- the set of types accepted under EVERY declared branch, i.e. the
930
+ INTERSECTION. A single static type yields `{t}`. A `{when, type}` list
931
+ yields `{t}` when every branch declares the same `t`, and an EMPTY set
932
+ when they disagree -- an empty set rejects every forward, which is the
933
+ intended conservative outcome of the ruling (see header 'TYPE REGISTRY').
934
+ - `(None, str)` -- a malformed declaration; the string is the reason.
935
+ """
936
+ if not value:
937
+ return None, None
938
+ if isinstance(value, str):
939
+ return {value}, None
940
+ if not isinstance(value, list):
941
+ return None, "declares a malformed input_types (neither a type string nor a list)"
942
+
943
+ accepted = None
944
+ for item in value:
945
+ if not isinstance(item, dict) or not item.get("when") or not item.get("type"):
946
+ return None, "declares a malformed input_types entry (missing 'when' or 'type')"
947
+ branch_set = {item["type"]}
948
+ accepted = branch_set if accepted is None else (accepted & branch_set)
949
+ if accepted is None:
950
+ return None, "declares an empty input_types list"
951
+ return accepted, None
952
+
953
+
730
954
  try:
731
955
  builtin_filenames = sorted(
732
956
  f for f in os.listdir(playbooks_dir)
@@ -1014,7 +1238,26 @@ for pid in order:
1014
1238
  continue
1015
1239
 
1016
1240
  validation_error = None
1241
+
1242
+ # --- E62_S01_T04 CHECK 1: registry vocabulary validation (see header 'TYPE REGISTRY') -------
1243
+ # Its own loop, deliberately separate from the forward_from/conditional loop below: that loop
1244
+ # skips every non-dict step, and this check must cover BARE-STRING steps too -- it is keyed on
1245
+ # the step's resolved skill name, not on the step carrying any particular field. A None
1246
+ # registry (missing/unparseable file) makes the whole check a silent no-op.
1247
+ if TYPE_REGISTRY is not None:
1248
+ for idx, step in enumerate(flattened_steps):
1249
+ step_name = step_skill_name(step)
1250
+ if not step_name:
1251
+ continue
1252
+ validation_error = validate_declared_vocabulary(
1253
+ idx, step_name, os.path.join(skills_dir, step_name, "SKILL.md"), TYPE_REGISTRY
1254
+ )
1255
+ if validation_error:
1256
+ break
1257
+
1017
1258
  for idx, step in enumerate(flattened_steps):
1259
+ if validation_error:
1260
+ break
1018
1261
  if not isinstance(step, dict):
1019
1262
  continue
1020
1263
 
@@ -1085,6 +1328,48 @@ for pid in order:
1085
1328
  if validation_error:
1086
1329
  break
1087
1330
 
1331
+ # --- E62_S01_T04 CHECK 2: output/input compatibility under the all-branches rule -------
1332
+ # Runs AFTER the two checks above on purpose: a source that declares nothing, or declares
1333
+ # a malformed {when, type} entry, is still rejected by its own pre-existing message, never
1334
+ # by this one. See header 'TYPE REGISTRY'.
1335
+ consumer_name = step_skill_name(step)
1336
+ consumer_input_val = (
1337
+ extract_input_types(os.path.join(skills_dir, consumer_name, "SKILL.md"))
1338
+ if consumer_name
1339
+ else None
1340
+ )
1341
+ accepted, accepted_error = accepted_input_types(consumer_input_val)
1342
+ if accepted_error:
1343
+ validation_error = f"step {idx} forward_from consumer '{consumer_name}' {accepted_error}"
1344
+ break
1345
+ if accepted is not None:
1346
+ # `accepted is None` is the BINDING backward-compatible path: a consumer declaring no
1347
+ # `input_types` is allowed on the source-declaredness check alone, exactly as before
1348
+ # this task. Nothing below runs for it.
1349
+ offending = next(
1350
+ (
1351
+ (type_val, when_val)
1352
+ for type_val, when_val in declared_type_entries(output_types_val)
1353
+ if type_val is not None and type_val not in accepted
1354
+ ),
1355
+ None,
1356
+ )
1357
+ if offending:
1358
+ offending_type, offending_when = offending
1359
+ branch_desc = (
1360
+ f"output_types branch when='{offending_when}'"
1361
+ if offending_when
1362
+ else "static output_types"
1363
+ )
1364
+ accepted_desc = ", ".join(sorted(accepted)) if accepted else "<none>"
1365
+ validation_error = (
1366
+ f"step {idx} 'forward_from' source '{source_name}' {branch_desc} produces "
1367
+ f"type '{offending_type}', which consumer '{consumer_name}' does not accept "
1368
+ f"under every input_types branch (accepted under all branches: "
1369
+ f"{accepted_desc})"
1370
+ )
1371
+ break
1372
+
1088
1373
  if validation_error:
1089
1374
  print(f"Warning: {path} {validation_error} — skipped", file=sys.stderr)
1090
1375
  skip_reasons[pid] = validation_error
@@ -1129,5 +1414,5 @@ if mode == "lookup":
1129
1414
  print(json.dumps(catalog, indent=2))
1130
1415
  PY
1131
1416
 
1132
- python3 "$PY_SCRIPT" "$PLAYBOOKS_DIR" "$SKILLS_DIR" "$PROJECT_DIR" "$MODE" "$LOOKUP_ID"
1417
+ python3 "$PY_SCRIPT" "$PLAYBOOKS_DIR" "$SKILLS_DIR" "$PROJECT_DIR" "$MODE" "$LOOKUP_ID" "$PKG_ROOT"
1133
1418
  exit $?
@@ -3,10 +3,10 @@
3
3
  # skills/jenga/scripts/match-playbook.sh
4
4
  #
5
5
  # Deterministic PLAYBOOK matcher for `/jenga`'s natural-language branch (E53_S02_T02). Runs the
6
- # same three-pass matching *philosophy* as `skills/route/SKILL.md`'s Step 2 (keyword ->
7
- # example similarity -> description), but scoped to the playbook catalog produced by
8
- # `load-playbooks.sh` (E53_S02_T01) instead of the single-skill catalog `load-nl-catalog.sh`
9
- # produces for `/route`/`/jenga`'s existing single-skill matching.
6
+ # same three-pass matching *philosophy* as `skills/jenga/SKILL.md`'s inlined Skill Matching &
7
+ # Invocation Contract (keyword -> example similarity -> description), but scoped to the playbook
8
+ # catalog produced by `load-playbooks.sh` (E53_S02_T01) instead of the single-skill catalog
9
+ # `load-nl-catalog.sh` produces for `/route`/`/jenga`'s existing single-skill matching.
10
10
  #
11
11
  # ---------------------------------------------------------------------------
12
12
  # THIS IS A FALLBACK — READ BEFORE WIRING (E53_S02_T04)