@jenga-ai/agent 3.0.0 → 3.1.1
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.
- package/README.md +10 -3
- package/bin/jenga.js +10 -0
- package/hooks/copilot_session_end.sh +7 -3
- package/hooks/prompt_router_helper.js +17 -5
- package/lib/commands/doctor.js +351 -0
- package/lib/commands/init.js +16 -0
- package/lib/generate-copilot-hooks.js +116 -0
- package/lib/legacy-shipped-paths.json +336 -0
- package/lib/postinstall-manifest.js +469 -0
- package/lib/skill-allow-list.json +1 -1
- package/package.json +2 -1
- package/scripts/build-pages-site.sh +268 -0
- package/scripts/generate-legacy-shipped-paths.js +248 -0
- package/scripts/postinstall.js +205 -2
- package/scripts/verify-legacy-seed-reconcile.sh +254 -0
- package/scripts/verify-postinstall-reconcile.sh +392 -0
- package/skills/j-uncharted/scripts/elicitation-state.sh +15 -1
- package/skills/jenga/SKILL.md +39 -7
- package/skills/jenga/playbooks/brainstorm-to-mirror.json +22 -0
- package/skills/jenga/playbooks/schema.json +42 -0
- package/skills/jenga/scripts/detect-nl-intent.sh +179 -0
- package/skills/jenga/scripts/load-nl-catalog.js +206 -0
- package/skills/jenga/scripts/load-nl-catalog.sh +65 -0
- package/skills/jenga/scripts/load-playbooks.sh +194 -0
- package/skills/jenga/scripts/match-playbook.sh +262 -0
- package/skills/jenga/scripts/render-playbook-confirmation.sh +363 -0
- package/skills/jenga/scripts/run-playbook-step.sh +273 -0
- package/templates/copilot-instructions.md.tpl +32 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/build-pages-site.sh
|
|
3
|
+
#
|
|
4
|
+
# Deterministically (re)generates the wiki-derived pages of the GitHub Pages
|
|
5
|
+
# documentation site (docs/*.md) from the existing wiki mirror under
|
|
6
|
+
# project/.wiki/. This is the sync mechanism behind E41_S13_T01's
|
|
7
|
+
# source-of-truth decision ("restructure from the wiki, keep both in sync"):
|
|
8
|
+
# project/.wiki/ stays the canonical, doc-sync-maintained content; this
|
|
9
|
+
# script is the deterministic transform from that content into the
|
|
10
|
+
# multi-page, navigable Pages site.
|
|
11
|
+
#
|
|
12
|
+
# WHEN TO RE-RUN: after `j.doc-sync` (or any manual edit) updates
|
|
13
|
+
# project/.wiki/documentation.md, project/.wiki/intro-guide.md, or
|
|
14
|
+
# project/.wiki/concepts/*.md, re-run this script to refresh the Pages site
|
|
15
|
+
# so it doesn't silently drift from the wiki the way README.md and the wiki
|
|
16
|
+
# itself have drifted from each other before (see skills/doc-sync/SKILL.md
|
|
17
|
+
# for the doc-sync side of this convention).
|
|
18
|
+
#
|
|
19
|
+
# WHAT THIS SCRIPT DOES NOT TOUCH:
|
|
20
|
+
# - docs/_config.yml, docs/index.md — hand-authored site structure/config,
|
|
21
|
+
# not derived from wiki content. Edit these directly if the page set or
|
|
22
|
+
# nav changes.
|
|
23
|
+
# - docs/README.md — maintainer-facing decision record, not a rendered
|
|
24
|
+
# Jekyll page and not wiki-derived content.
|
|
25
|
+
#
|
|
26
|
+
# WHAT THIS SCRIPT REGENERATES (always overwritten, never hand-edit these):
|
|
27
|
+
# - docs/getting-started.md <- project/.wiki/intro-guide.md
|
|
28
|
+
# - docs/concepts.md <- project/.wiki/concepts/*.md (concatenated)
|
|
29
|
+
# - docs/skills.md <- project/.wiki/documentation.md "## Skills"
|
|
30
|
+
# - docs/agents.md <- project/.wiki/documentation.md "## Agents"
|
|
31
|
+
# - docs/hooks.md <- project/.wiki/documentation.md "## Hooks"
|
|
32
|
+
# - docs/mcp-tools.md <- project/.wiki/documentation.md "## MCP Tools"
|
|
33
|
+
# - docs/reference.md <- project/.wiki/documentation.md
|
|
34
|
+
# "## Directory Structure" +
|
|
35
|
+
# "## Agent Communication Contract"
|
|
36
|
+
# (plus a short hand-authored index blurb
|
|
37
|
+
# linking to skills/agents/hooks/mcp-tools)
|
|
38
|
+
#
|
|
39
|
+
# KNOWN LIMITATION: this is a structural transform, not a fact-checker. It
|
|
40
|
+
# ports project/.wiki/documentation.md's content as-is, including whatever
|
|
41
|
+
# is currently stale in that file (e.g. it documents ~30 skills under the
|
|
42
|
+
# old bare `/<name>` invocation form and does not yet list every skill in
|
|
43
|
+
# skills/, including the `j-<name>` twins — a pre-existing wiki staleness
|
|
44
|
+
# gap, not something introduced by this script). Fixing the wiki's own
|
|
45
|
+
# staleness is doc-sync's job, not this script's — this script only keeps
|
|
46
|
+
# the Pages site faithful to whatever the wiki currently says.
|
|
47
|
+
#
|
|
48
|
+
# HEADING-STABILITY ASSUMPTION: section extraction below is keyed to exact
|
|
49
|
+
# top-level ("## ") heading text in documentation.md (Agents, Skills, MCP
|
|
50
|
+
# Tools, Hooks, Directory Structure, Agent Communication Contract). If any
|
|
51
|
+
# of those headings are renamed, update the `case` statement in
|
|
52
|
+
# extract_sections() to match.
|
|
53
|
+
#
|
|
54
|
+
# Usage: scripts/build-pages-site.sh
|
|
55
|
+
# Safe to re-run any number of times — every run fully overwrites its
|
|
56
|
+
# output files from the current wiki content (no partial/incremental state).
|
|
57
|
+
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
|
|
60
|
+
# shellcheck source=lib/resolve-project-dir.sh disable=SC1091
|
|
61
|
+
source "$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh"
|
|
62
|
+
|
|
63
|
+
REPO_ROOT="$JENGA_PROJECT_DIR"
|
|
64
|
+
WIKI_DIR="$REPO_ROOT/project/.wiki"
|
|
65
|
+
DOCS_DIR="$REPO_ROOT/docs"
|
|
66
|
+
DOC_MD="$WIKI_DIR/documentation.md"
|
|
67
|
+
INTRO_MD="$WIKI_DIR/intro-guide.md"
|
|
68
|
+
CONCEPTS_DIR="$WIKI_DIR/concepts"
|
|
69
|
+
|
|
70
|
+
for f in "$DOC_MD" "$INTRO_MD"; do
|
|
71
|
+
if [ ! -f "$f" ]; then
|
|
72
|
+
echo "build-pages-site.sh: required source file not found: $f" >&2
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
done
|
|
76
|
+
if [ ! -d "$CONCEPTS_DIR" ]; then
|
|
77
|
+
echo "build-pages-site.sh: required source directory not found: $CONCEPTS_DIR" >&2
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
WORK="$(mktemp -d)"
|
|
82
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
83
|
+
|
|
84
|
+
# --- 1. Split documentation.md into per-section scratch files -------------
|
|
85
|
+
# A line can only belong to one target section at a time; the generic
|
|
86
|
+
# "any other ## heading" rule resets `section` to "" so unrelated top-level
|
|
87
|
+
# sections (e.g. "## Table of Contents") are dropped rather than bleeding
|
|
88
|
+
# into whichever named section preceded them.
|
|
89
|
+
awk -v work="$WORK" '
|
|
90
|
+
/^## Agents$/ { section = "agents"; next }
|
|
91
|
+
/^## Skills$/ { section = "skills"; next }
|
|
92
|
+
/^## MCP Tools$/ { section = "mcp-tools"; next }
|
|
93
|
+
/^## Hooks$/ { section = "hooks"; next }
|
|
94
|
+
/^## Directory Structure$/ { section = "directory-structure"; next }
|
|
95
|
+
/^## Agent Communication Contract$/ { section = "contract"; next }
|
|
96
|
+
/^## / { section = "" }
|
|
97
|
+
{
|
|
98
|
+
if (section != "") {
|
|
99
|
+
print >> (work "/section-" section ".md")
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
' "$DOC_MD"
|
|
103
|
+
|
|
104
|
+
for s in agents skills mcp-tools hooks directory-structure contract; do
|
|
105
|
+
[ -f "$WORK/section-$s.md" ] || touch "$WORK/section-$s.md"
|
|
106
|
+
done
|
|
107
|
+
|
|
108
|
+
write_page() {
|
|
109
|
+
# write_page <output-path> <title> <permalink> <body-file>
|
|
110
|
+
local out="$1" title="$2" permalink="$3" body="$4"
|
|
111
|
+
{
|
|
112
|
+
printf -- '---\n'
|
|
113
|
+
printf 'layout: page\n'
|
|
114
|
+
printf 'title: %s\n' "$title"
|
|
115
|
+
printf 'permalink: %s\n' "$permalink"
|
|
116
|
+
printf -- '---\n\n'
|
|
117
|
+
cat "$body"
|
|
118
|
+
} > "$out"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# --- 2. docs/agents.md, docs/hooks.md, docs/mcp-tools.md -------------------
|
|
122
|
+
# Straight ports — no internal markdown links were found in these sections
|
|
123
|
+
# of documentation.md, so no link rewriting is needed beyond the front
|
|
124
|
+
# matter wrapper.
|
|
125
|
+
write_page "$DOCS_DIR/agents.md" "Agents" "/agents.html" "$WORK/section-agents.md"
|
|
126
|
+
write_page "$DOCS_DIR/hooks.md" "Hooks" "/hooks.html" "$WORK/section-hooks.md"
|
|
127
|
+
write_page "$DOCS_DIR/mcp-tools.md" "MCP Tools" "/mcp-tools.html" "$WORK/section-mcp-tools.md"
|
|
128
|
+
|
|
129
|
+
# --- 3. docs/skills.md ------------------------------------------------------
|
|
130
|
+
# Single reference page (matches the story's own suggested top-level
|
|
131
|
+
# category list: Getting Started / Concepts / Skills reference / Agents /
|
|
132
|
+
# Hooks / MCP Tools — "Skills reference" is one page, not one page per
|
|
133
|
+
# skill). Category (### ) and per-skill (#### ) structure is preserved
|
|
134
|
+
# as-is from the source.
|
|
135
|
+
write_page "$DOCS_DIR/skills.md" "Skills Reference" "/skills.html" "$WORK/section-skills.md"
|
|
136
|
+
|
|
137
|
+
# --- 4. docs/reference.md ---------------------------------------------------
|
|
138
|
+
# Combines the two remaining documentation.md sections (Directory Structure,
|
|
139
|
+
# Agent Communication Contract) behind a short hand-authored index blurb
|
|
140
|
+
# that links out to the pages built above — this replaces documentation.md's
|
|
141
|
+
# original role as the single "full reference" entry point, now that its
|
|
142
|
+
# content is split across multiple pages.
|
|
143
|
+
{
|
|
144
|
+
cat <<'EOF'
|
|
145
|
+
## Full Reference Index
|
|
146
|
+
|
|
147
|
+
This page is the entry point into the full reference material, split across
|
|
148
|
+
several pages so nothing requires scrolling through one giant file:
|
|
149
|
+
|
|
150
|
+
- **[Skills Reference](./skills.md)** — every skill, grouped by Setup &
|
|
151
|
+
Planning, Execution, Status & Review, and Committing & Maintenance.
|
|
152
|
+
- **[Agents](./agents.md)** — Scrum Master, Developer, Tester: roles,
|
|
153
|
+
ownership, and responsibilities.
|
|
154
|
+
- **[Hooks](./hooks.md)** — session lifecycle hooks and what they run.
|
|
155
|
+
- **[MCP Tools](./mcp-tools.md)** — Model Context Protocol tools available
|
|
156
|
+
in a Claude Code session.
|
|
157
|
+
|
|
158
|
+
The rest of this page covers the repo's directory structure and the
|
|
159
|
+
inter-agent communication contract (the typed "sender object" every agent
|
|
160
|
+
call carries).
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
EOF
|
|
165
|
+
cat "$WORK/section-directory-structure.md"
|
|
166
|
+
printf '\n---\n\n'
|
|
167
|
+
cat "$WORK/section-contract.md"
|
|
168
|
+
} > "$WORK/reference-body.md"
|
|
169
|
+
write_page "$DOCS_DIR/reference.md" "Reference" "/reference.html" "$WORK/reference-body.md"
|
|
170
|
+
|
|
171
|
+
# --- 5. docs/getting-started.md ---------------------------------------------
|
|
172
|
+
# Port of intro-guide.md verbatim (drop the leading H1 — front matter
|
|
173
|
+
# supplies the page title instead), with its two links into the
|
|
174
|
+
# now-restructured reference/concepts pages rewritten.
|
|
175
|
+
tail -n +2 "$INTRO_MD" > "$WORK/intro-body.md"
|
|
176
|
+
# NOTE on delimiter choice: sed's `s<delim>pattern<delim>replacement<delim>`
|
|
177
|
+
# breaks if the replacement text itself contains the delimiter character.
|
|
178
|
+
# Several replacements below contain a literal "#" (anchor fragments), so
|
|
179
|
+
# "#" cannot be used as the delimiter here — "|" is used instead, since
|
|
180
|
+
# none of these paths/anchors contain a literal "|".
|
|
181
|
+
sed -i.bak \
|
|
182
|
+
-e 's|\[documentation\.md\](\./documentation\.md)|[reference.md](./reference.md)|g' \
|
|
183
|
+
-e 's|(\./concepts/role-separation\.md)|(./concepts.md#role-separation)|g' \
|
|
184
|
+
-e 's|(\./concepts/board-hierarchy\.md)|(./concepts.md#board-hierarchy)|g' \
|
|
185
|
+
-e 's|(\./concepts/session-continuity\.md)|(./concepts.md#session-continuity)|g' \
|
|
186
|
+
-e 's|(\./concepts/first-feature\.md)|(./concepts.md#your-first-feature)|g' \
|
|
187
|
+
-e 's|(\./concepts/multi-session-work\.md)|(./concepts.md#working-across-sessions)|g' \
|
|
188
|
+
-e 's|(\./concepts/mid-flow-capture\.md)|(./concepts.md#capturing-mid-flow-ideas)|g' \
|
|
189
|
+
-e 's|(\./concepts/parallel-tasks\.md)|(./concepts.md#parallel-tasks)|g' \
|
|
190
|
+
"$WORK/intro-body.md"
|
|
191
|
+
rm -f "$WORK/intro-body.md.bak"
|
|
192
|
+
write_page "$DOCS_DIR/getting-started.md" "Getting Started" "/getting-started.html" "$WORK/intro-body.md"
|
|
193
|
+
|
|
194
|
+
# --- 6. docs/concepts.md -----------------------------------------------------
|
|
195
|
+
# Concatenates all 7 project/.wiki/concepts/*.md files into one page:
|
|
196
|
+
# - each file's H1 becomes an H2 section heading (slug listed below must
|
|
197
|
+
# stay in sync with each file's actual title text — GitHub Pages/kramdown
|
|
198
|
+
# slugifies headings to lowercase-hyphenated automatically)
|
|
199
|
+
# - all other headings are demoted one level (## -> ###, ### -> ####)
|
|
200
|
+
# - sibling/parent links are rewritten to point within the merged page
|
|
201
|
+
# and at the sibling getting-started.md / reference.md pages
|
|
202
|
+
#
|
|
203
|
+
{
|
|
204
|
+
cat <<'EOF'
|
|
205
|
+
## Concepts
|
|
206
|
+
|
|
207
|
+
The ideas behind Jenga AI's structure, and the how-tos for using it day to
|
|
208
|
+
day. Jump to any section:
|
|
209
|
+
|
|
210
|
+
- [Role Separation](#role-separation)
|
|
211
|
+
- [Board Hierarchy](#board-hierarchy)
|
|
212
|
+
- [Session Continuity](#session-continuity)
|
|
213
|
+
- [Your First Feature](#your-first-feature)
|
|
214
|
+
- [Working Across Sessions](#working-across-sessions)
|
|
215
|
+
- [Capturing Mid-Flow Ideas](#capturing-mid-flow-ideas)
|
|
216
|
+
- [Parallel Tasks](#parallel-tasks)
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
EOF
|
|
221
|
+
} > "$WORK/concepts-body.md"
|
|
222
|
+
|
|
223
|
+
append_concept() {
|
|
224
|
+
# append_concept <filename-stem> <section-title>
|
|
225
|
+
local stem="$1"
|
|
226
|
+
local title="$2"
|
|
227
|
+
local src="$CONCEPTS_DIR/$stem.md"
|
|
228
|
+
{
|
|
229
|
+
printf '## %s\n\n' "$title"
|
|
230
|
+
# NOTE on delimiter choice: same reasoning as the getting-started block
|
|
231
|
+
# above — replacements here contain a literal "#" anchor character, so
|
|
232
|
+
# "|" is used as the sed delimiter instead of "#".
|
|
233
|
+
#
|
|
234
|
+
# NOTE on heading demotion: a naive two-pass "## -> ###" then
|
|
235
|
+
# "### -> ####" sed would double-demote lines that were originally
|
|
236
|
+
# "## " (they'd match the first rule, becoming "### ", and then ALSO
|
|
237
|
+
# match the second rule on the same pass, becoming "#### " — wrong).
|
|
238
|
+
# The single extended-regex rule below captures the existing run of
|
|
239
|
+
# 2-3 "#" characters and prepends exactly one more, so each line is
|
|
240
|
+
# demoted exactly once regardless of its original level.
|
|
241
|
+
tail -n +2 "$src" \
|
|
242
|
+
| sed \
|
|
243
|
+
-e 's|\[documentation\.md\](\.\./documentation\.md)|[reference.md](./reference.md)|g' \
|
|
244
|
+
-e 's|(\.\./intro-guide\.md)|(./getting-started.md)|g' \
|
|
245
|
+
-e 's|(\.\./documentation\.md)|(./reference.md)|g' \
|
|
246
|
+
-e 's|(\./role-separation\.md)|(#role-separation)|g' \
|
|
247
|
+
-e 's|(\./board-hierarchy\.md)|(#board-hierarchy)|g' \
|
|
248
|
+
-e 's|(\./session-continuity\.md)|(#session-continuity)|g' \
|
|
249
|
+
-e 's|(\./first-feature\.md)|(#your-first-feature)|g' \
|
|
250
|
+
-e 's|(\./multi-session-work\.md)|(#working-across-sessions)|g' \
|
|
251
|
+
-e 's|(\./mid-flow-capture\.md)|(#capturing-mid-flow-ideas)|g' \
|
|
252
|
+
-e 's|(\./parallel-tasks\.md)|(#parallel-tasks)|g' \
|
|
253
|
+
| sed -E 's/^(#{2,3}) /#\1 /'
|
|
254
|
+
printf '\n---\n\n'
|
|
255
|
+
} >> "$WORK/concepts-body.md"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
append_concept "role-separation" "Role Separation"
|
|
259
|
+
append_concept "board-hierarchy" "Board Hierarchy"
|
|
260
|
+
append_concept "session-continuity" "Session Continuity"
|
|
261
|
+
append_concept "first-feature" "Your First Feature"
|
|
262
|
+
append_concept "multi-session-work" "Working Across Sessions"
|
|
263
|
+
append_concept "mid-flow-capture" "Capturing Mid-Flow Ideas"
|
|
264
|
+
append_concept "parallel-tasks" "Parallel Tasks"
|
|
265
|
+
|
|
266
|
+
write_page "$DOCS_DIR/concepts.md" "Concepts" "/concepts.html" "$WORK/concepts-body.md"
|
|
267
|
+
|
|
268
|
+
echo "build-pages-site.sh: regenerated docs/{getting-started,concepts,skills,agents,hooks,mcp-tools,reference}.md from project/.wiki/*"
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/generate-legacy-shipped-paths.js — legacy shipped-path list generator (E26_S08_T03)
|
|
4
|
+
*
|
|
5
|
+
* Why this exists
|
|
6
|
+
* ────────────────
|
|
7
|
+
* `lib/postinstall-manifest.js`'s delete reconciliation (E26_S08_T01) is purely forward-looking:
|
|
8
|
+
* a consumer already installed before any manifest existed can never have their pre-existing
|
|
9
|
+
* orphans cleaned up, because the first manifest a fixed version ever writes for them records
|
|
10
|
+
* only what THAT run mirrored. This script produces the static list this package ships so the
|
|
11
|
+
* FIRST manifest a consumer ever gets can instead be *seeded* with paths known to have shipped in
|
|
12
|
+
* some real prior published version — see `seedFromLegacyPaths` in `lib/postinstall-manifest.js`
|
|
13
|
+
* and `scripts/postinstall.js`'s `no-prior-manifest` branch for how the seed is consumed.
|
|
14
|
+
*
|
|
15
|
+
* Why not git tags
|
|
16
|
+
* ─────────────────
|
|
17
|
+
* The task's design note allows deriving the list "from git tags" as an alternative to
|
|
18
|
+
* publish-time derivation. Checked and rejected for this repo specifically: this repo's local
|
|
19
|
+
* tags (`v0.0.1`, `v55.0.1`, `last-self-sync`) do not correspond to the real npm publish history
|
|
20
|
+
* at all — `npm view @jenga-ai/agent versions --json` shows the real, disconnected sequence
|
|
21
|
+
* (1.0.0 through 3.0.0 as of this writing). Reconstructing shipped paths from local git tags in
|
|
22
|
+
* this repo would silently produce a list bearing no relation to what was actually published.
|
|
23
|
+
*
|
|
24
|
+
* Two modes
|
|
25
|
+
* ─────────
|
|
26
|
+
* --bootstrap Fetches EVERY version `npm view <package> versions --json` currently lists from
|
|
27
|
+
* the real registry, `npm pack`s each one into a throwaway temp dir, and unions
|
|
28
|
+
* the `skills/`+`agents/` entries inside every tarball. Network-dependent. This is
|
|
29
|
+
* how the real historical shipped-path set is captured — including versions whose
|
|
30
|
+
* git history is not reliably reconstructable locally (verified true here). Meant
|
|
31
|
+
* to be run manually / rarely — a one-off backfill, or an occasional resync — NOT
|
|
32
|
+
* wired into the automatic per-publish flow (see incremental mode below for that).
|
|
33
|
+
*
|
|
34
|
+
* (default) Incremental, no network: reads whatever is already at the output path (if any)
|
|
35
|
+
* and unions it with the paths CURRENTLY on disk under this repo's own `skills/`
|
|
36
|
+
* and `agents/` directories — i.e. "what this release is about to ship" folds into
|
|
37
|
+
* the running cumulative record. This is the mode wired into the publish pipeline
|
|
38
|
+
* (`skills/publish/scripts/npm_pipeline.sh` / `npm_ci_pipeline.sh`, via the
|
|
39
|
+
* `generate:legacy-paths` npm script) so every future publish keeps the list
|
|
40
|
+
* current with zero network dependency and zero publish-time registry flakiness.
|
|
41
|
+
*
|
|
42
|
+
* IMPORTANT — this mode's first-ever run is NOT a substitute for --bootstrap: if
|
|
43
|
+
* the output file doesn't exist yet, incremental mode unions an EMPTY existing set
|
|
44
|
+
* with whatever's on disk in THIS repo's tree right now. That happens to currently
|
|
45
|
+
* equal the real historical union (this repo's working tree is a superset of every
|
|
46
|
+
* published version's file list, as of the 2026-09-07 verification below) — but
|
|
47
|
+
* that is a coincidence of this repo's current state, not a guarantee the mode
|
|
48
|
+
* itself provides. The very first generation of the shipped artifact MUST use
|
|
49
|
+
* --bootstrap so the baseline is verified against the real registry, not assumed.
|
|
50
|
+
*
|
|
51
|
+
* Output
|
|
52
|
+
* ──────
|
|
53
|
+
* `lib/legacy-shipped-paths.json` (ships automatically — `lib/` is already in package.json's
|
|
54
|
+
* `files` allow-list, no change needed there):
|
|
55
|
+
*
|
|
56
|
+
* {
|
|
57
|
+
* "generated_at": "<ISO 8601>",
|
|
58
|
+
* "package": "@jenga-ai/agent",
|
|
59
|
+
* "source": "bootstrap-from-registry+incremental" | "incremental",
|
|
60
|
+
* "paths": ["agents/developer.md", "skills/do/SKILL.md", ...]
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* `paths` are relative to a mirror root, POSIX-separated, deduped and sorted — matching the same
|
|
64
|
+
* shape convention `lib/postinstall-manifest.js`'s own manifest uses, for consistency.
|
|
65
|
+
*
|
|
66
|
+
* The generation step is regenerated automatically as part of the publish flow (incremental mode),
|
|
67
|
+
* per this task's AC — it is NOT hand-maintained, so it cannot silently go stale across releases.
|
|
68
|
+
*
|
|
69
|
+
* ESM, Node built-ins only — matches lib/postinstall-manifest.js and lib/mirror.js. `npm` itself is
|
|
70
|
+
* shelled out to (via `execFileSync`) only in `--bootstrap` mode.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
import fs from 'node:fs';
|
|
74
|
+
import os from 'node:os';
|
|
75
|
+
import path from 'node:path';
|
|
76
|
+
import { execFileSync } from 'node:child_process';
|
|
77
|
+
import { fileURLToPath } from 'node:url';
|
|
78
|
+
|
|
79
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
80
|
+
const REPO_ROOT = path.join(__dirname, '..');
|
|
81
|
+
|
|
82
|
+
export const DEFAULT_OUTPUT_PATH = path.join(REPO_ROOT, 'lib', 'legacy-shipped-paths.json');
|
|
83
|
+
export const DEFAULT_PACKAGE_NAME = '@jenga-ai/agent';
|
|
84
|
+
|
|
85
|
+
/** Discovery-bound directories mirrored into a consumer's .agents/ and .claude/ (see docs/distribution.md §1). */
|
|
86
|
+
const COPY_SET = ['skills', 'agents'];
|
|
87
|
+
|
|
88
|
+
// ── walk a real directory tree ──────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
function walkDir(base, dir, out) {
|
|
91
|
+
let entries;
|
|
92
|
+
try {
|
|
93
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
94
|
+
} catch (_) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const abs = path.join(dir, entry.name);
|
|
99
|
+
if (entry.isDirectory()) {
|
|
100
|
+
walkDir(base, abs, out);
|
|
101
|
+
} else if (entry.isFile()) {
|
|
102
|
+
out.push(path.relative(base, abs).split(path.sep).join('/'));
|
|
103
|
+
}
|
|
104
|
+
// symlinks intentionally ignored — matches lib/mirror.js's own walk behavior.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Relative POSIX paths this repo's CURRENT `skills/` + `agents/` trees would ship, i.e. exactly
|
|
110
|
+
* what a fresh install of the version about to be published would mirror.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} repoRoot
|
|
113
|
+
* @returns {string[]} sorted, deduped
|
|
114
|
+
*/
|
|
115
|
+
export function currentShippedPaths(repoRoot = REPO_ROOT) {
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const entry of COPY_SET) {
|
|
118
|
+
const dir = path.join(repoRoot, entry);
|
|
119
|
+
if (fs.existsSync(dir)) walkDir(repoRoot, dir, out);
|
|
120
|
+
}
|
|
121
|
+
return [...new Set(out)].sort();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── read existing output (if any) ───────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
function readExistingPaths(outputPath) {
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
|
129
|
+
return Array.isArray(parsed.paths) ? parsed.paths.filter((p) => typeof p === 'string') : [];
|
|
130
|
+
} catch (_) {
|
|
131
|
+
return []; // absent, unreadable, or corrupt — start from an empty cumulative set
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── bootstrap from the real npm registry ────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fetch every currently-listed published version of `packageName`, `npm pack` each into a
|
|
139
|
+
* throwaway temp dir, and union the `skills/`+`agents/` entries found inside every tarball.
|
|
140
|
+
* Network-dependent — intended for manual/rare use (a one-off backfill or occasional resync),
|
|
141
|
+
* never called by the automatic per-publish (incremental) path.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} packageName
|
|
144
|
+
* @returns {string[]} sorted, deduped relative POSIX paths
|
|
145
|
+
*/
|
|
146
|
+
export function bootstrapFromRegistry(packageName = DEFAULT_PACKAGE_NAME) {
|
|
147
|
+
const versionsRaw = execFileSync('npm', ['view', packageName, 'versions', '--json'], {
|
|
148
|
+
encoding: 'utf8',
|
|
149
|
+
});
|
|
150
|
+
const versions = JSON.parse(versionsRaw);
|
|
151
|
+
const all = new Set();
|
|
152
|
+
|
|
153
|
+
for (const version of versions) {
|
|
154
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'jenga-legacy-bootstrap-'));
|
|
155
|
+
try {
|
|
156
|
+
execFileSync('npm', ['pack', `${packageName}@${version}`, '--silent'], { cwd: tmp, stdio: 'ignore' });
|
|
157
|
+
const tarball = fs.readdirSync(tmp).find((f) => f.endsWith('.tgz'));
|
|
158
|
+
if (!tarball) continue;
|
|
159
|
+
const listing = execFileSync('tar', ['-tzf', path.join(tmp, tarball)], { encoding: 'utf8' });
|
|
160
|
+
for (const line of listing.split('\n')) {
|
|
161
|
+
const m = line.match(/^package\/(skills|agents)\/(.+)$/);
|
|
162
|
+
if (m && !line.endsWith('/')) all.add(`${m[1]}/${m[2]}`);
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return [...all].sort();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── generate ─────────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {object} [opts]
|
|
176
|
+
* @param {string} [opts.outputPath] Default: lib/legacy-shipped-paths.json
|
|
177
|
+
* @param {string} [opts.packageName] Default: @jenga-ai/agent
|
|
178
|
+
* @param {boolean} [opts.bootstrap] Default: false (incremental, no network)
|
|
179
|
+
* @param {string} [opts.repoRoot] Default: this repo's own root
|
|
180
|
+
* @returns {{written: boolean, path: string, count: number, source: string}}
|
|
181
|
+
*/
|
|
182
|
+
export function generate({
|
|
183
|
+
outputPath = DEFAULT_OUTPUT_PATH,
|
|
184
|
+
packageName = DEFAULT_PACKAGE_NAME,
|
|
185
|
+
bootstrap = false,
|
|
186
|
+
repoRoot = REPO_ROOT,
|
|
187
|
+
} = {}) {
|
|
188
|
+
const existing = readExistingPaths(outputPath);
|
|
189
|
+
let paths;
|
|
190
|
+
let source;
|
|
191
|
+
|
|
192
|
+
if (bootstrap) {
|
|
193
|
+
const registryPaths = bootstrapFromRegistry(packageName);
|
|
194
|
+
paths = [...new Set([...registryPaths, ...existing])].sort();
|
|
195
|
+
source = 'bootstrap-from-registry+incremental';
|
|
196
|
+
} else {
|
|
197
|
+
const current = currentShippedPaths(repoRoot);
|
|
198
|
+
paths = [...new Set([...existing, ...current])].sort();
|
|
199
|
+
source = 'incremental';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const artifact = {
|
|
203
|
+
generated_at: new Date().toISOString(),
|
|
204
|
+
package: packageName,
|
|
205
|
+
source,
|
|
206
|
+
paths,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
210
|
+
fs.writeFileSync(outputPath, JSON.stringify(artifact, null, 2) + '\n', 'utf8');
|
|
211
|
+
|
|
212
|
+
return { written: true, path: outputPath, count: paths.length, source };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Read the shipped legacy-paths artifact's `paths` array. Fail-toward-doing-nothing: any read or
|
|
217
|
+
* parse failure returns `[]` rather than throwing — a missing/corrupt legacy-paths file must
|
|
218
|
+
* never abort or degrade an unattended `npm install`, mirroring `readManifest`'s own posture in
|
|
219
|
+
* `lib/postinstall-manifest.js`.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} artifactPath
|
|
222
|
+
* @returns {string[]}
|
|
223
|
+
*/
|
|
224
|
+
export function readLegacyShippedPaths(artifactPath = DEFAULT_OUTPUT_PATH) {
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
|
|
227
|
+
if (!Array.isArray(parsed.paths)) return [];
|
|
228
|
+
return parsed.paths.filter((p) => typeof p === 'string' && p.length > 0);
|
|
229
|
+
} catch (_) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── CLI guard ────────────────────────────────────────────────────────────────
|
|
235
|
+
// node scripts/generate-legacy-shipped-paths.js [--bootstrap] [--package <name>] [outputPath]
|
|
236
|
+
|
|
237
|
+
const invokedPath = process.argv[1] ? fs.realpathSync(process.argv[1]) : null;
|
|
238
|
+
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
239
|
+
const argv = process.argv.slice(2);
|
|
240
|
+
const bootstrap = argv.includes('--bootstrap');
|
|
241
|
+
const pkgFlagIndex = argv.indexOf('--package');
|
|
242
|
+
const packageName = pkgFlagIndex !== -1 ? argv[pkgFlagIndex + 1] : DEFAULT_PACKAGE_NAME;
|
|
243
|
+
const positional = argv.filter((a, i) => a !== '--bootstrap' && i !== pkgFlagIndex && i !== pkgFlagIndex + 1 && !a.startsWith('--'));
|
|
244
|
+
const outputPath = positional[0] || DEFAULT_OUTPUT_PATH;
|
|
245
|
+
|
|
246
|
+
const result = generate({ outputPath, packageName, bootstrap });
|
|
247
|
+
console.log(`✓ ${result.path} (${result.count} paths, ${result.source})`);
|
|
248
|
+
}
|