@jenga-ai/agent 1.3.0 → 3.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.
- package/README.md +93 -256
- package/agents/developer.md +9 -8
- package/agents/scrum-master.md +57 -23
- package/agents/tester.md +51 -5
- package/hooks/on_session_end.sh +13 -1
- package/lib/generate-agent-context.js +18 -1
- package/lib/generate-copilot-instructions.js +18 -1
- package/lib/generate-skill-allow-list.js +197 -0
- package/lib/skill-allow-list.json +42 -0
- package/package.json +17 -13
- package/scripts/apply-j-prefix.sh +243 -0
- package/scripts/consume-context-digest.sh +103 -0
- package/scripts/generate-j-alias.sh +333 -0
- package/scripts/postinstall.js +25 -0
- package/scripts/sweep-stale-context-digests.sh +132 -0
- package/scripts/validate-board.sh +5 -0
- package/scripts/write-context-digest.sh +230 -0
- package/skills/{brainstorm → j-brainstorm}/SKILL.md +9 -2
- package/skills/{btw → j-btw}/SKILL.md +9 -2
- package/skills/{clearify → j-clearify}/SKILL.md +9 -2
- package/skills/{close-story → j-close-story}/SKILL.md +92 -13
- package/skills/j-close-story/scripts/check-privatized.sh +345 -0
- package/skills/{close-story → j-close-story}/scripts/check-story-closeable.sh +1 -1
- package/skills/{close-story → j-close-story}/scripts/extract-task-diff-stats.sh +1 -1
- package/skills/{commit → j-commit}/SKILL.md +9 -2
- package/skills/j-continue/SKILL.md +36 -0
- package/skills/{deep-dive → j-deep-dive}/SKILL.md +9 -8
- package/skills/{dev-done → j-dev-done}/SKILL.md +11 -4
- package/skills/{dev-done → j-dev-done}/scripts/classify-commit-outcome.sh +4 -4
- package/skills/{distribute → j-distribute}/SKILL.md +17 -10
- package/skills/{distribute → j-distribute}/scripts/distribute-changes.sh +1 -1
- package/skills/{do → j-do}/SKILL.md +111 -14
- package/skills/j-doc/README.md +155 -0
- package/skills/{doc → j-doc}/SKILL.md +55 -18
- package/skills/j-doc/authoring-notes.md +72 -0
- package/skills/j-doc/scripts/resolve_last_update.py +149 -0
- package/skills/{doc-sync → j-doc-sync}/SKILL.md +9 -2
- package/skills/{dooo → j-dooo}/SKILL.md +9 -2
- package/skills/j-error/SKILL.md +36 -0
- package/skills/{evaluate → j-evaluate}/SKILL.md +9 -2
- package/skills/j-examplify/SKILL.md +49 -0
- package/skills/{help → j-help}/SKILL.md +9 -2
- package/skills/{idea → j-idea}/SKILL.md +10 -3
- package/skills/{idea → j-idea}/assets/idea_handoff_template.md +1 -1
- package/skills/{improve → j-improve}/SKILL.md +9 -2
- package/skills/{init → j-init}/SKILL.md +21 -8
- package/skills/j-init/assets/scope-thresholds_template.json +7 -0
- package/skills/j-jbp/SKILL.md +32 -0
- package/skills/j-lgtm/SKILL.md +28 -0
- package/skills/{pi-plan → j-pi-plan}/SKILL.md +10 -3
- package/skills/{proceed → j-proceed}/SKILL.md +9 -2
- package/skills/{publish → j-publish}/SKILL.md +47 -40
- package/skills/{publish → j-publish}/adapters/droplet.md +1 -1
- package/skills/{publish → j-publish}/adapters/mobile-ios.md +3 -3
- package/skills/{publish → j-publish}/adapters/npm-ci.md +29 -7
- package/skills/{publish → j-publish}/adapters/npm.md +8 -8
- package/skills/{publish → j-publish}/assets/ci-contract.md +2 -2
- package/skills/{publish → j-publish}/schemas/publish.schema.json +1 -1
- package/skills/{publish → j-publish}/scripts/npm_ci_pipeline.sh +21 -1
- package/skills/{publish → j-publish}/scripts/npm_stage_inspect.sh +34 -1
- package/skills/{publish → j-publish}/scripts/npm_stage_pipeline.sh +9 -4
- package/skills/{publish → j-publish}/scripts/publish_deploy.sh +4 -4
- package/skills/{publish → j-publish}/scripts/validate_npm_stage_env.sh +1 -1
- package/skills/{publish → j-publish}/wizards/droplet.md +1 -1
- package/skills/{publish → j-publish}/wizards/mobile-ios.md +1 -1
- package/skills/{publish → j-publish}/wizards/npm-ci.md +1 -1
- package/skills/{publish → j-publish}/wizards/npm.md +1 -1
- package/skills/{reconcile → j-reconcile}/SKILL.md +12 -5
- package/skills/{reconcile → j-reconcile}/scripts/detect-unlinked-code.sh +2 -2
- package/skills/{reconcile → j-reconcile}/scripts/resolve-reconcile-scope.sh +3 -3
- package/skills/{reconcile-origin → j-reconcile-origin}/SKILL.md +13 -6
- package/skills/{redo → j-redo}/SKILL.md +9 -2
- package/skills/{skillify → j-skillify}/SKILL.md +10 -3
- package/skills/{spinoff → j-spinoff}/SKILL.md +9 -2
- package/skills/{status → j-status}/SKILL.md +9 -2
- package/skills/j-todo/SKILL.md +92 -0
- package/skills/{todo → j-todo}/assets/todo_handoff_template.md +1 -1
- package/skills/j-todo/scripts/add_trivial_task.sh +216 -0
- package/skills/j-todo/scripts/update_story_tasks.py +87 -0
- package/skills/{uncharted → j-uncharted}/SKILL.md +35 -28
- package/skills/{uncharted → j-uncharted}/assets/UNDERSTANDING_DOC_TEMPLATE.md +2 -2
- package/skills/{uncharted → j-uncharted}/scripts/detect-dependencies.sh +1 -1
- package/skills/{uncharted → j-uncharted}/scripts/detect-tests.sh +1 -1
- package/skills/{uncharted → j-uncharted}/scripts/directory-triage.sh +3 -3
- package/skills/{uncharted → j-uncharted}/scripts/elicitation-state.sh +3 -3
- package/skills/{uncharted → j-uncharted}/scripts/enumerate-target.sh +1 -1
- package/skills/{uncharted → j-uncharted}/scripts/import-source.sh +1 -1
- package/skills/{uncharted → j-uncharted}/scripts/inspect-provenance.sh +1 -1
- package/skills/{uncharted → j-uncharted}/scripts/resolve-segment-target.sh +5 -5
- package/skills/{uncharted → j-uncharted}/scripts/run-engine.sh +1 -1
- package/skills/{uncharted → j-uncharted}/scripts/validate-proposed-items.sh +2 -2
- package/skills/{uncharted → j-uncharted}/scripts/write-backfilled-epics.sh +1 -1
- package/skills/j-wtf/SKILL.md +27 -0
- package/skills/jenga/SKILL.md +1 -1
- package/skills/jenga/scripts/render-confirmation.sh +55 -18
- package/skills/jenga-permission-level/SKILL.md +1 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +33 -2
- package/templates/agent-context.md.tpl +32 -9
- package/templates/copilot-instructions.md.tpl +66 -11
- package/skills/continue/SKILL.md +0 -29
- package/skills/error/SKILL.md +0 -29
- package/skills/examplify/SKILL.md +0 -42
- package/skills/init/assets/scope-thresholds_template.json +0 -7
- package/skills/jbp/SKILL.md +0 -25
- package/skills/lgtm/SKILL.md +0 -21
- package/skills/todo/SKILL.md +0 -48
- package/skills/wtf/SKILL.md +0 -20
- /package/skills/{close-story → j-close-story}/scripts/compute-scope-divergence.sh +0 -0
- /package/skills/{close-story → j-close-story}/scripts/extract-diff-stats.sh +0 -0
- /package/skills/{close-story → j-close-story}/scripts/update-task-frontmatter.sh +0 -0
- /package/skills/{commit → j-commit}/assets/user_instructions_template.md +0 -0
- /package/skills/{distribute → j-distribute}/CONFIG_SCHEMA.md +0 -0
- /package/skills/{distribute → j-distribute}/scripts/check-version.sh +0 -0
- /package/skills/{distribute → j-distribute}/scripts/commit-version-bump.sh +0 -0
- /package/skills/{do → j-do}/assets/intent-vs-diff-prompt.md +0 -0
- /package/skills/{do → j-do}/assets/sender_template.json +0 -0
- /package/skills/{doc → j-doc}/assets/path-objectives.yaml +0 -0
- /package/skills/{doc-sync → j-doc-sync}/assets/default_excludes.txt +0 -0
- /package/skills/{doc-sync → j-doc-sync}/assets/doc_targets.md +0 -0
- /package/skills/{evaluate → j-evaluate}/assets/evaluation_invokation_template.yml +0 -0
- /package/skills/{evaluate → j-evaluate}/assets/evaluation_rapport_template.md +0 -0
- /package/skills/{idea → j-idea}/assets/idea_template.md +0 -0
- /package/skills/{init → j-init}/assets/.gitignore_template +0 -0
- /package/skills/{init → j-init}/assets/PROJECT_SUMMARY_template.md +0 -0
- /package/skills/{init → j-init}/assets/directory_structure.txt +0 -0
- /package/skills/{init → j-init}/assets/strategy_stub_template.md +0 -0
- /package/skills/{init → j-init}/assets/test-config_template.json +0 -0
- /package/skills/{init → j-init}/assets/workflow_template.json +0 -0
- /package/skills/{init → j-init}/scripts/apply-project-visibility.sh +0 -0
- /package/skills/{init → j-init}/scripts/detect-existing-codebase.sh +0 -0
- /package/skills/{init → j-init}/scripts/init.sh +0 -0
- /package/skills/{pi-plan → j-pi-plan}/assets/epic.json +0 -0
- /package/skills/{pi-plan → j-pi-plan}/assets/story_template.md +0 -0
- /package/skills/{publish → j-publish}/assets/ExportOptions.plist.template +0 -0
- /package/skills/{publish → j-publish}/assets/ownership-matrix.md +0 -0
- /package/skills/{publish → j-publish}/assets/publish.example.json +0 -0
- /package/skills/{publish → j-publish}/assets/publish.example.npm-ci.json +0 -0
- /package/skills/{publish → j-publish}/assets/publish.example.npm.json +0 -0
- /package/skills/{publish → j-publish}/assets/secrets-guide.md +0 -0
- /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-minimal.json +0 -0
- /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-with-empty-secrets.json +0 -0
- /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-with-workflow-path.json +0 -0
- /package/skills/{publish → j-publish}/scripts/check_target_config.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/droplet_pipeline.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/finalize_changelog.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/generate_release_notes.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/ios_pipeline.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/npm_pipeline.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/publish_common.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/reconcile_tags.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/run_gates.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/setup_wizard.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/show_history.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/suggest_semver_bump.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/validate_config.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/validate_droplet_env.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/validate_ios_env.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/validate_npm_ci_env.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/validate_npm_env.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/write_ledger_entry.sh +0 -0
- /package/skills/{reconcile → j-reconcile}/assets/report_format.md +0 -0
- /package/skills/{reconcile-origin → j-reconcile-origin}/scripts/reconcile-origin.sh +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/SKILL.md +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/assets/.gitignore_template +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/assets/PROJECT_SUMMARY_template.md +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/assets/directory_structure.txt +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/assets/test-config_template.json +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/assets/workflow_template.json +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/scripts/init.sh +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-old/SKILL.md +0 -0
- /package/skills/{status → j-status}/assets/output_format.md +0 -0
- /package/skills/{todo → j-todo}/assets/todo_template.md +0 -0
- /package/skills/{uncharted → j-uncharted}/assets/SEGMENT_PROPOSAL_TEMPLATE.md +0 -0
- /package/skills/{uncharted → j-uncharted}/scripts/apply-subsystem-cap.sh +0 -0
- /package/skills/{uncharted → j-uncharted}/scripts/discover-subsystems.sh +0 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/generate-j-alias.sh — generate/sync a j-<name> polyfill alias skill directory
|
|
3
|
+
#
|
|
4
|
+
# Mechanizes the hand-maintained pattern that produced skills/j-init/ (E50_S04): a
|
|
5
|
+
# literal-directory-name duplicate of skills/<name>/ that gives a guaranteed-unshadowed
|
|
6
|
+
# way to reach a skill even when a host tool's own built-in command of the same name
|
|
7
|
+
# would otherwise shadow the bare /<name> form. Per CLAUDE.md's Skill Implementation
|
|
8
|
+
# Principle, this must not be repeated by hand across every skill — this script performs
|
|
9
|
+
# both the initial generation and any later resync-after-source-change as a single
|
|
10
|
+
# deterministic command (E50_S05_T01).
|
|
11
|
+
#
|
|
12
|
+
# For the given <skill-name>, this script:
|
|
13
|
+
# 1. Copies the full skills/<skill-name>/ tree into skills/j-<skill-name>/, overwriting
|
|
14
|
+
# the destination on re-run (the sync case).
|
|
15
|
+
# 2. Rewrites every self-referential "skills/<skill-name>/" path reference inside the
|
|
16
|
+
# copied SKILL.md and any copied scripts to "skills/j-<skill-name>/" — including the
|
|
17
|
+
# .claude/skills/<skill-name>/ and .agents/skills/<skill-name>/ mirrored-install
|
|
18
|
+
# fallback paths, which fall out of the same literal substring replace.
|
|
19
|
+
# 3. Rewrites the copied SKILL.md's frontmatter: name: j.<skill-name> -> name:
|
|
20
|
+
# j.j-<skill-name>; description: reframed as a polyfill alias; keywords: gets
|
|
21
|
+
# j-<skill-name> and polyfill appended (existing keywords preserved, list created
|
|
22
|
+
# if absent); examples: gets a "j-<skill-name>" example appended if the list exists.
|
|
23
|
+
# 4. Inserts a short, programmatically generated lockstep/duplicate-alias note near the
|
|
24
|
+
# top of the body.
|
|
25
|
+
#
|
|
26
|
+
# Idempotent: the target directory is always fully rebuilt from the source on every run
|
|
27
|
+
# (never incrementally patched), so two runs against an unchanged source produce a
|
|
28
|
+
# byte-identical result, and a source change is picked up in full on the next run.
|
|
29
|
+
#
|
|
30
|
+
# Exclusions:
|
|
31
|
+
# - Only operates on directories containing a SKILL.md (skips e.g. skills/index/,
|
|
32
|
+
# which has no SKILL.md and is not part of skill routing) — enforced as a hard error,
|
|
33
|
+
# not a silent no-op, so a typo'd or non-skill argument fails clearly.
|
|
34
|
+
# - Refuses to touch the skills/init/ <-> skills/j-init/ pair — that pair already
|
|
35
|
+
# exists, is hand-maintained, and is explicitly out of scope for this story.
|
|
36
|
+
# - Refuses to touch skills/jenga/ or skills/jenga-permission-level/ (and their would-be
|
|
37
|
+
# j-jenga/j-jenga-permission-level twins) — hard error, non-zero exit (E50_S06_T01).
|
|
38
|
+
# These are root orchestrator commands; their invocation surface must stay exactly
|
|
39
|
+
# /jenga (j.jenga) and /jenga-permission-level (j.jenga-permission-level), never a
|
|
40
|
+
# doubled j-jenga alias. A prior generic run of this generator produced exactly that
|
|
41
|
+
# unreachable doubled twin by mistake — see docs/skill-authoring.md's "j-<name>
|
|
42
|
+
# directory twins" Exclusions paragraph.
|
|
43
|
+
#
|
|
44
|
+
# This is a single-skill generator only. Running it across every skill in the repo is a
|
|
45
|
+
# separate task (E50_S05_T02) — deliberately not built here.
|
|
46
|
+
#
|
|
47
|
+
# Usage:
|
|
48
|
+
# scripts/generate-j-alias.sh <skill-name>
|
|
49
|
+
#
|
|
50
|
+
# Examples:
|
|
51
|
+
# scripts/generate-j-alias.sh status # generates/syncs skills/j-status/
|
|
52
|
+
# scripts/generate-j-alias.sh close-story # generates/syncs skills/j-close-story/
|
|
53
|
+
|
|
54
|
+
set -euo pipefail
|
|
55
|
+
|
|
56
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
57
|
+
# shellcheck source=lib/resolve-project-dir.sh
|
|
58
|
+
source "$SCRIPT_DIR/../lib/resolve-project-dir.sh"
|
|
59
|
+
|
|
60
|
+
PROJECT_DIR="$JENGA_PROJECT_DIR"
|
|
61
|
+
SKILLS_DIR="$PROJECT_DIR/skills"
|
|
62
|
+
|
|
63
|
+
usage() {
|
|
64
|
+
echo "Usage: $(basename "$0") <skill-name>" >&2
|
|
65
|
+
echo " Generates/syncs skills/j-<skill-name>/ as a polyfill alias of skills/<skill-name>/." >&2
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if [ "$#" -ne 1 ]; then
|
|
69
|
+
usage
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
case "$1" in
|
|
74
|
+
-h|--help)
|
|
75
|
+
usage
|
|
76
|
+
exit 0
|
|
77
|
+
;;
|
|
78
|
+
esac
|
|
79
|
+
|
|
80
|
+
SKILL_NAME="$1"
|
|
81
|
+
|
|
82
|
+
# Security guard (E50_S05_T01 rework — see
|
|
83
|
+
# project/rapports/problems/E50_S05_T01-generator-path-traversal.md): validate
|
|
84
|
+
# the skill-name argument BEFORE any path is constructed from it, let alone
|
|
85
|
+
# before anything destructive runs. An unsanitized argument containing '/' or
|
|
86
|
+
# '..' segments previously let SRC_DIR/TARGET_DIR (built by plain string
|
|
87
|
+
# concatenation below) resolve outside $SKILLS_DIR, and the destructive
|
|
88
|
+
# rmtree-then-copytree step ran before the one existing safety check
|
|
89
|
+
# (frontmatter name match) ever fired. This allowlist alone closes that vector
|
|
90
|
+
# — it forbids '/' and '.' entirely — and runs first, ahead of every other
|
|
91
|
+
# check including the init/j-init special-case below.
|
|
92
|
+
if [[ ! "$SKILL_NAME" =~ ^[a-z0-9][a-z0-9_-]*$ ]]; then
|
|
93
|
+
echo "generate-j-alias.sh: error: '$SKILL_NAME' is not a valid skill name — must match ^[a-z0-9][a-z0-9_-]*\$ (lowercase letters, digits, '-', '_' only, starting with a letter or digit). Refusing to construct a path from it." >&2
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
if [[ "$SKILL_NAME" == "init" || "$SKILL_NAME" == "j-init" ]]; then
|
|
98
|
+
echo "generate-j-alias.sh: refusing to touch '$SKILL_NAME' — the skills/init/ <-> skills/j-init/ pair already exists and is hand-maintained (out of scope for this generator, E50_S05). No-op." >&2
|
|
99
|
+
exit 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Hard exclusion (E50_S06_T01): jenga and jenga-permission-level are root orchestrator
|
|
103
|
+
# commands whose invocation surface must stay exactly /jenga (j.jenga) and
|
|
104
|
+
# /jenga-permission-level (j.jenga-permission-level) — never a doubled j-jenga alias.
|
|
105
|
+
# Unlike the init/j-init case above (a silent no-op, exit 0, because that pair is
|
|
106
|
+
# legitimately hand-maintained and pre-dates this generator), this is a hard error with a
|
|
107
|
+
# non-zero exit: a j-jenga/j-jenga-permission-level twin should never be generated at all,
|
|
108
|
+
# so a future accidental invocation must fail loudly rather than silently succeed as a
|
|
109
|
+
# no-op. See docs/skill-authoring.md's "j-<name> directory twins" Exclusions paragraph.
|
|
110
|
+
if [[ "$SKILL_NAME" == "jenga" || "$SKILL_NAME" == "j-jenga" || "$SKILL_NAME" == "jenga-permission-level" || "$SKILL_NAME" == "j-jenga-permission-level" ]]; then
|
|
111
|
+
echo "generate-j-alias.sh: error: refusing to generate a j-<name> twin for '$SKILL_NAME' — jenga and jenga-permission-level are root orchestrator commands and are hard-excluded from this generator (E50_S06_T01). Their invocation surface must stay exactly /jenga (j.jenga) and /jenga-permission-level (j.jenga-permission-level), never a doubled j-jenga alias. This is not a no-op — it is an intentional hard failure." >&2
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
SRC_DIR="$SKILLS_DIR/$SKILL_NAME"
|
|
116
|
+
SRC_SKILL_MD="$SRC_DIR/SKILL.md"
|
|
117
|
+
|
|
118
|
+
if [ ! -f "$SRC_SKILL_MD" ]; then
|
|
119
|
+
echo "generate-j-alias.sh: error: '$SRC_DIR' does not contain a SKILL.md — not a skill directory, refusing to generate skills/j-$SKILL_NAME/." >&2
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
TARGET_DIR="$SKILLS_DIR/j-$SKILL_NAME"
|
|
124
|
+
|
|
125
|
+
# Defense in depth (belt and suspenders on top of the allowlist above):
|
|
126
|
+
# resolve SRC_DIR/TARGET_DIR to absolute canonical paths and hard-fail unless
|
|
127
|
+
# each is confined to a direct child of $SKILLS_DIR. This protects against the
|
|
128
|
+
# allowlist regex being weakened in a future edit, or $SKILLS_DIR itself
|
|
129
|
+
# containing a symlink that could redirect an otherwise-validated path outside
|
|
130
|
+
# skills/. Both checks below run before the python heredoc, i.e. strictly
|
|
131
|
+
# before any rmtree/copytree call.
|
|
132
|
+
REAL_SKILLS_DIR="$(realpath "$SKILLS_DIR")"
|
|
133
|
+
REAL_SRC_DIR="$(realpath "$SRC_DIR")"
|
|
134
|
+
if [ "$(dirname "$REAL_SRC_DIR")" != "$REAL_SKILLS_DIR" ]; then
|
|
135
|
+
echo "generate-j-alias.sh: error: resolved source path '$REAL_SRC_DIR' escapes '$REAL_SKILLS_DIR' — refusing to proceed." >&2
|
|
136
|
+
exit 1
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# TARGET_DIR may not exist yet (first-run generation case), and this
|
|
140
|
+
# platform's realpath has no -m/--canonicalize-missing option, so it can't be
|
|
141
|
+
# realpath'd directly when absent. Its parent is always $SKILLS_DIR by
|
|
142
|
+
# construction (SKILL_NAME is a single path component, and the allowlist above
|
|
143
|
+
# already forbids '/' and '..' in it), so check the literal parent/basename
|
|
144
|
+
# shape here, and additionally realpath the target itself when it already
|
|
145
|
+
# exists (the resync case) to catch a symlink planted at the target that
|
|
146
|
+
# points outside skills/.
|
|
147
|
+
TARGET_BASENAME="$(basename "$TARGET_DIR")"
|
|
148
|
+
if [[ "$TARGET_BASENAME" == "." || "$TARGET_BASENAME" == ".." || "$TARGET_BASENAME" == */* ]]; then
|
|
149
|
+
echo "generate-j-alias.sh: error: computed target basename '$TARGET_BASENAME' is unsafe — refusing to proceed." >&2
|
|
150
|
+
exit 1
|
|
151
|
+
fi
|
|
152
|
+
if [ "$(dirname "$TARGET_DIR")" != "$SKILLS_DIR" ]; then
|
|
153
|
+
echo "generate-j-alias.sh: error: computed target path '$TARGET_DIR' does not resolve directly under '$SKILLS_DIR' — refusing to proceed." >&2
|
|
154
|
+
exit 1
|
|
155
|
+
fi
|
|
156
|
+
if [ -e "$TARGET_DIR" ] || [ -L "$TARGET_DIR" ]; then
|
|
157
|
+
REAL_TARGET_DIR="$(realpath "$TARGET_DIR")"
|
|
158
|
+
if [ "$(dirname "$REAL_TARGET_DIR")" != "$REAL_SKILLS_DIR" ]; then
|
|
159
|
+
echo "generate-j-alias.sh: error: resolved target path '$REAL_TARGET_DIR' escapes '$REAL_SKILLS_DIR' — refusing to proceed." >&2
|
|
160
|
+
exit 1
|
|
161
|
+
fi
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
export SKILL_NAME SRC_DIR TARGET_DIR
|
|
165
|
+
|
|
166
|
+
python3 - <<'PY'
|
|
167
|
+
import os
|
|
168
|
+
import re
|
|
169
|
+
import shutil
|
|
170
|
+
import sys
|
|
171
|
+
|
|
172
|
+
skill_name = os.environ["SKILL_NAME"]
|
|
173
|
+
src_dir = os.environ["SRC_DIR"]
|
|
174
|
+
target_dir = os.environ["TARGET_DIR"]
|
|
175
|
+
|
|
176
|
+
old_ref = f"skills/{skill_name}/"
|
|
177
|
+
new_ref = f"skills/j-{skill_name}/"
|
|
178
|
+
|
|
179
|
+
# ---- 1. Copy the full tree, overwriting the destination (the sync case) ----
|
|
180
|
+
if os.path.lexists(target_dir):
|
|
181
|
+
shutil.rmtree(target_dir)
|
|
182
|
+
shutil.copytree(src_dir, target_dir, symlinks=True)
|
|
183
|
+
|
|
184
|
+
# ---- 2. Rewrite self-referential paths in every copied file ----
|
|
185
|
+
for dirpath, _dirnames, filenames in os.walk(target_dir):
|
|
186
|
+
for fn in filenames:
|
|
187
|
+
fpath = os.path.join(dirpath, fn)
|
|
188
|
+
try:
|
|
189
|
+
with open(fpath, "r", encoding="utf-8") as fh:
|
|
190
|
+
content = fh.read()
|
|
191
|
+
except (UnicodeDecodeError, OSError):
|
|
192
|
+
continue # binary or unreadable — leave untouched
|
|
193
|
+
if old_ref in content:
|
|
194
|
+
with open(fpath, "w", encoding="utf-8") as fh:
|
|
195
|
+
fh.write(content.replace(old_ref, new_ref))
|
|
196
|
+
|
|
197
|
+
# ---- 3. Rewrite SKILL.md frontmatter + insert the lockstep note ----
|
|
198
|
+
target_skill_md = os.path.join(target_dir, "SKILL.md")
|
|
199
|
+
with open(target_skill_md, "r", encoding="utf-8") as fh:
|
|
200
|
+
lines = fh.readlines()
|
|
201
|
+
|
|
202
|
+
if not lines or lines[0].strip() != "---":
|
|
203
|
+
sys.exit(f"generate-j-alias.sh: error: {target_skill_md} has no opening '---' frontmatter marker")
|
|
204
|
+
|
|
205
|
+
close_idx = None
|
|
206
|
+
for idx in range(1, len(lines)):
|
|
207
|
+
if lines[idx].strip() == "---":
|
|
208
|
+
close_idx = idx
|
|
209
|
+
break
|
|
210
|
+
if close_idx is None:
|
|
211
|
+
sys.exit(f"generate-j-alias.sh: error: {target_skill_md} has no closing '---' frontmatter marker")
|
|
212
|
+
|
|
213
|
+
# Parse the frontmatter into an ordered list of top-level fields. This is the same
|
|
214
|
+
# informal line-oriented approach scripts/apply-j-prefix.sh already uses elsewhere in
|
|
215
|
+
# this repo for SKILL.md frontmatter — not a full YAML parser, consistent with how
|
|
216
|
+
# frontmatter is already handled here.
|
|
217
|
+
key_re = re.compile(r'^([A-Za-z_][\w-]*):(.*)$')
|
|
218
|
+
fields = []
|
|
219
|
+
i = 1
|
|
220
|
+
while i < close_idx:
|
|
221
|
+
line = lines[i]
|
|
222
|
+
m = key_re.match(line)
|
|
223
|
+
if not m:
|
|
224
|
+
i += 1
|
|
225
|
+
continue
|
|
226
|
+
block = [line]
|
|
227
|
+
j = i + 1
|
|
228
|
+
if m.group(2).strip() == "":
|
|
229
|
+
# possible nested block (list or mapping) — consume indented continuation lines
|
|
230
|
+
while j < close_idx and lines[j].startswith(" "):
|
|
231
|
+
block.append(lines[j])
|
|
232
|
+
j += 1
|
|
233
|
+
fields.append({"key": m.group(1), "lines": block})
|
|
234
|
+
i = j
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def find_field(key):
|
|
238
|
+
for f in fields:
|
|
239
|
+
if f["key"] == key:
|
|
240
|
+
return f
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
name_field = find_field("name")
|
|
245
|
+
desc_field = find_field("description")
|
|
246
|
+
|
|
247
|
+
if name_field is None or len(name_field["lines"]) != 1:
|
|
248
|
+
sys.exit(f"generate-j-alias.sh: error: {target_skill_md} has no single-line 'name:' field")
|
|
249
|
+
if desc_field is None or len(desc_field["lines"]) != 1:
|
|
250
|
+
sys.exit(f"generate-j-alias.sh: error: {target_skill_md} has no single-line 'description:' field")
|
|
251
|
+
|
|
252
|
+
current_name = key_re.match(name_field["lines"][0]).group(2).strip().strip('"\'')
|
|
253
|
+
# Accepts the current "j.<name>" form, the bare "<name>" form, and the legacy
|
|
254
|
+
# "j:<name>" form (pre-E50_S07_T01) so a twin can still be regenerated from a
|
|
255
|
+
# not-yet-migrated source instead of hard-aborting. Whatever the source carries,
|
|
256
|
+
# the twin is always emitted on the current "j." separator.
|
|
257
|
+
if current_name not in (f"j.{skill_name}", f"j:{skill_name}", skill_name):
|
|
258
|
+
sys.exit(
|
|
259
|
+
f"generate-j-alias.sh: error: frontmatter name '{current_name}' in {target_skill_md} "
|
|
260
|
+
f"does not match expected 'j.{skill_name}' (or bare '{skill_name}') — refusing to guess, aborting."
|
|
261
|
+
)
|
|
262
|
+
name_field["lines"][0] = f"name: j.j-{skill_name}\n"
|
|
263
|
+
|
|
264
|
+
original_desc = key_re.match(desc_field["lines"][0]).group(2).strip().strip('"\'').rstrip(".")
|
|
265
|
+
new_desc = (
|
|
266
|
+
f"Polyfill alias of the {skill_name} skill under a collision-safe directory name. "
|
|
267
|
+
f"Identical behavior to /{skill_name} — {original_desc}. "
|
|
268
|
+
f"Use when the bare /{skill_name} form is shadowed by another tool's own built-in "
|
|
269
|
+
f"command of the same name."
|
|
270
|
+
)
|
|
271
|
+
desc_field["lines"][0] = f"description: {new_desc}\n"
|
|
272
|
+
|
|
273
|
+
keywords_field = find_field("keywords")
|
|
274
|
+
if keywords_field is not None:
|
|
275
|
+
keywords_field["lines"].append(f" - j-{skill_name}\n")
|
|
276
|
+
keywords_field["lines"].append(" - polyfill\n")
|
|
277
|
+
else:
|
|
278
|
+
fields.append({
|
|
279
|
+
"key": "keywords",
|
|
280
|
+
"lines": ["keywords:\n", f" - j-{skill_name}\n", " - polyfill\n"],
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
examples_field = find_field("examples")
|
|
284
|
+
if examples_field is not None:
|
|
285
|
+
examples_field["lines"].append(f' - "j-{skill_name}"\n')
|
|
286
|
+
# if absent: per spec, do not fabricate an examples: list where none existed
|
|
287
|
+
|
|
288
|
+
new_frontmatter_lines = []
|
|
289
|
+
for f in fields:
|
|
290
|
+
new_frontmatter_lines.extend(f["lines"])
|
|
291
|
+
|
|
292
|
+
body_lines = lines[close_idx + 1:]
|
|
293
|
+
|
|
294
|
+
note_text = (
|
|
295
|
+
f"This skill is a literal-directory-name duplicate of `skills/{skill_name}/`. It exists so "
|
|
296
|
+
f"that `/j-{skill_name}` (and `j.j-{skill_name}`) give a guaranteed-unshadowed way to reach "
|
|
297
|
+
f"the same flow as `/{skill_name}`, even if a host tool's own built-in command of the same "
|
|
298
|
+
f"name would otherwise shadow or override the bare `/{skill_name}` alias (Claude Code's "
|
|
299
|
+
f"native skill resolution is a literal-string, directory-name-based match — see "
|
|
300
|
+
f"`docs/skill-authoring.md`'s \"Invocation Convention\").\n\n"
|
|
301
|
+
f"This file is generated/synced by `scripts/generate-j-alias.sh {skill_name}` from "
|
|
302
|
+
f"`skills/{skill_name}/SKILL.md` — do not hand-edit it; re-run the generator instead to "
|
|
303
|
+
f"pick up source changes."
|
|
304
|
+
)
|
|
305
|
+
note_block = []
|
|
306
|
+
for para in note_text.split("\n\n"):
|
|
307
|
+
note_block.append(para + "\n")
|
|
308
|
+
note_block.append("\n")
|
|
309
|
+
|
|
310
|
+
heading_idx = None
|
|
311
|
+
for idx, bl in enumerate(body_lines):
|
|
312
|
+
if bl.startswith("# "):
|
|
313
|
+
heading_idx = idx
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
if heading_idx is not None:
|
|
317
|
+
insert_at = heading_idx + 1
|
|
318
|
+
if insert_at < len(body_lines) and body_lines[insert_at].strip() == "":
|
|
319
|
+
insert_at += 1
|
|
320
|
+
prefix = body_lines[:insert_at]
|
|
321
|
+
if prefix and prefix[-1].strip() != "":
|
|
322
|
+
prefix = prefix + ["\n"]
|
|
323
|
+
new_body_lines = prefix + note_block + body_lines[insert_at:]
|
|
324
|
+
else:
|
|
325
|
+
new_body_lines = note_block + body_lines
|
|
326
|
+
|
|
327
|
+
new_lines = [lines[0]] + new_frontmatter_lines + [lines[close_idx]] + new_body_lines
|
|
328
|
+
|
|
329
|
+
with open(target_skill_md, "w", encoding="utf-8") as fh:
|
|
330
|
+
fh.writelines(new_lines)
|
|
331
|
+
|
|
332
|
+
print(f"generate-j-alias.sh: generated/synced skills/j-{skill_name}/ from skills/{skill_name}/")
|
|
333
|
+
PY
|
package/scripts/postinstall.js
CHANGED
|
@@ -33,6 +33,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
33
33
|
|
|
34
34
|
import { mirror } from '../lib/mirror.js';
|
|
35
35
|
import { generateCopilotInstructions } from '../lib/generate-copilot-instructions.js';
|
|
36
|
+
import { generateSkillAllowList } from '../lib/generate-skill-allow-list.js';
|
|
36
37
|
|
|
37
38
|
// ESM equivalent of __dirname
|
|
38
39
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -154,6 +155,30 @@ function main() {
|
|
|
154
155
|
totalSkipped += result.skipped.length;
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
// Regenerate the canonical skill allow-list (E50_S02) from the freshly mirrored
|
|
159
|
+
// `.agents/skills` BEFORE bootstrapping .github/copilot-instructions.md below — so a consumer
|
|
160
|
+
// install ships a correct, current allow-list from the very first `npm install`, not
|
|
161
|
+
// only after a later `/self-sync`. The output is written into this installed package's own
|
|
162
|
+
// `lib/` (not the consumer root — `lib/` is never duplicated into the consumer per this file's
|
|
163
|
+
// header comment), matching generateSkillAllowList's own default output location and the path
|
|
164
|
+
// the MCP router / native routing enforcement (E50_S02_T03/T04) read from at runtime.
|
|
165
|
+
//
|
|
166
|
+
// Ordering matters (E50_S02_T04): generateCopilotInstructions() below now renders an
|
|
167
|
+
// {{ALLOWED_SKILL_IDS}} block by reading this same lib/skill-allow-list.json path. Running
|
|
168
|
+
// this regeneration first means that render reflects the freshly-mirrored `.agents/skills`
|
|
169
|
+
// scan from this very postinstall run, not the stale artifact committed at publish time.
|
|
170
|
+
// Wrapped in a try/catch-and-warn pattern so a generation failure degrades gracefully
|
|
171
|
+
// (console warning) rather than failing the whole postinstall.
|
|
172
|
+
try {
|
|
173
|
+
const result = generateSkillAllowList(
|
|
174
|
+
path.join(consumerRoot, '.agents', 'skills'),
|
|
175
|
+
path.join(packageRoot, 'lib', 'skill-allow-list.json')
|
|
176
|
+
);
|
|
177
|
+
console.log(` ✓ lib/skill-allow-list.json regenerated (${result.skill_count} skills)`);
|
|
178
|
+
} catch (e) {
|
|
179
|
+
console.log(` ⚠ Could not regenerate lib/skill-allow-list.json — ${e.message}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
157
182
|
// Bootstrap .github/copilot-instructions.md unconditionally, right after the mirror step —
|
|
158
183
|
// no interactive prompts, since postinstall runs unattended during `npm install`. Without
|
|
159
184
|
// this, a consumer whose very first action is a Copilot slash command (before ever running
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# scripts/sweep-stale-context-digests.sh
|
|
4
|
+
#
|
|
5
|
+
# Staleness-cleanup backstop for project/queue/context/ digest files
|
|
6
|
+
# (E49_S01_T02). The PRIMARY cleanup path is scripts/consume-context-digest.sh,
|
|
7
|
+
# called by the receiving subagent once it has read its digest. This script
|
|
8
|
+
# exists for the cases that leaves uncovered:
|
|
9
|
+
# - a digest whose intended receiving session never starts (e.g. the
|
|
10
|
+
# dispatch it was written for was abandoned or superseded)
|
|
11
|
+
# - a receiver that reads the raw file directly instead of going through
|
|
12
|
+
# consume-context-digest.sh and forgets to delete it
|
|
13
|
+
#
|
|
14
|
+
# Invoked from hooks/on_session_end.sh on every session end (any agent), so
|
|
15
|
+
# it runs far more often than any single digest's realistic lifetime — a
|
|
16
|
+
# digest is meant to be consumed by the very next session dispatched for its
|
|
17
|
+
# task, not to sit for a full day. Age-based (mtime), not consumption-based:
|
|
18
|
+
# unlike project/queue/handoffs/ (routed and deleted deterministically, same
|
|
19
|
+
# hook invocation, by on_session_end.sh section 4), a digest's consumer is a
|
|
20
|
+
# LATER session that hasn't necessarily started yet when an unrelated
|
|
21
|
+
# session's SessionEnd hook fires — so this sweep must not delete a digest
|
|
22
|
+
# just because some other session ended in the meantime. The default
|
|
23
|
+
# threshold (24h) is deliberately generous for that reason.
|
|
24
|
+
#
|
|
25
|
+
# Usage:
|
|
26
|
+
# sweep-stale-context-digests.sh [--context-dir <dir>] [--max-age-seconds <n>]
|
|
27
|
+
#
|
|
28
|
+
# Options:
|
|
29
|
+
# --context-dir <dir> override project/queue/context (default:
|
|
30
|
+
# resolved via JENGA_PROJECT_DIR / git toplevel)
|
|
31
|
+
# --max-age-seconds <n> override the default 86400s (24h) threshold
|
|
32
|
+
# (env: CONTEXT_DIGEST_STALE_SECONDS)
|
|
33
|
+
# -h, --help
|
|
34
|
+
#
|
|
35
|
+
# Output (stdout): one line per file removed: "removed: <path> (age <n>s)"
|
|
36
|
+
#
|
|
37
|
+
# Exit codes:
|
|
38
|
+
# 0 ran successfully (including the common case of nothing to remove)
|
|
39
|
+
# 1 usage error
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
set -u
|
|
43
|
+
|
|
44
|
+
SELF="$(basename "$0")"
|
|
45
|
+
|
|
46
|
+
die() {
|
|
47
|
+
local code="$1"; shift
|
|
48
|
+
printf '%s: error: %s\n' "$SELF" "$*" >&2
|
|
49
|
+
exit "$code"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
usage() {
|
|
53
|
+
sed -n '/^# Usage:/,/^# Exit codes:/p' "$0" | sed 's/^# \{0,1\}//' >&2
|
|
54
|
+
exit 1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
resolve_project_dir() {
|
|
58
|
+
if [ -n "${JENGA_PROJECT_DIR:-}" ]; then
|
|
59
|
+
printf '%s\n' "$JENGA_PROJECT_DIR"
|
|
60
|
+
return 0
|
|
61
|
+
fi
|
|
62
|
+
if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
63
|
+
printf '%s\n' "$CLAUDE_PROJECT_DIR"
|
|
64
|
+
return 0
|
|
65
|
+
fi
|
|
66
|
+
git rev-parse --show-toplevel 2>/dev/null || pwd
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
CONTEXT_DIR=""
|
|
70
|
+
MAX_AGE_SECONDS="${CONTEXT_DIGEST_STALE_SECONDS:-86400}"
|
|
71
|
+
|
|
72
|
+
while [ "$#" -gt 0 ]; do
|
|
73
|
+
case "$1" in
|
|
74
|
+
--context-dir)
|
|
75
|
+
[ "$#" -ge 2 ] || die 1 "--context-dir requires a value"
|
|
76
|
+
CONTEXT_DIR="$2"; shift 2 ;;
|
|
77
|
+
--context-dir=*)
|
|
78
|
+
CONTEXT_DIR="${1#--context-dir=}"; shift ;;
|
|
79
|
+
--max-age-seconds)
|
|
80
|
+
[ "$#" -ge 2 ] || die 1 "--max-age-seconds requires a value"
|
|
81
|
+
MAX_AGE_SECONDS="$2"; shift 2 ;;
|
|
82
|
+
--max-age-seconds=*)
|
|
83
|
+
MAX_AGE_SECONDS="${1#--max-age-seconds=}"; shift ;;
|
|
84
|
+
-h|--help)
|
|
85
|
+
usage ;;
|
|
86
|
+
*)
|
|
87
|
+
die 1 "unknown argument: $1" ;;
|
|
88
|
+
esac
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
case "$MAX_AGE_SECONDS" in
|
|
92
|
+
''|*[!0-9]*) die 1 "--max-age-seconds must be a non-negative integer, got '$MAX_AGE_SECONDS'" ;;
|
|
93
|
+
esac
|
|
94
|
+
|
|
95
|
+
if [ -z "$CONTEXT_DIR" ]; then
|
|
96
|
+
PROJECT_DIR="$(resolve_project_dir)"
|
|
97
|
+
CONTEXT_DIR="$PROJECT_DIR/project/queue/context"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
[ -d "$CONTEXT_DIR" ] || exit 0
|
|
101
|
+
|
|
102
|
+
# Portable mtime lookup: GNU stat (Linux) uses -c, BSD stat (macOS) uses -f —
|
|
103
|
+
# same fallback pattern as scripts/with-lock.sh's lock_mtime_epoch().
|
|
104
|
+
file_mtime_epoch() {
|
|
105
|
+
stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
now_epoch() {
|
|
109
|
+
date +%s
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
NOW="$(now_epoch)"
|
|
113
|
+
|
|
114
|
+
for FILE in "$CONTEXT_DIR"/*.json; do
|
|
115
|
+
[ -e "$FILE" ] || continue
|
|
116
|
+
# Never sweep a file mid-claim by consume-context-digest.sh — that script
|
|
117
|
+
# renames its target to a .claimed.$$ sibling before reading it, and this
|
|
118
|
+
# glob (*.json) doesn't match that suffix, so in-flight claims are already
|
|
119
|
+
# excluded structurally. This comment documents that rather than adding a
|
|
120
|
+
# redundant runtime check.
|
|
121
|
+
|
|
122
|
+
MTIME="$(file_mtime_epoch "$FILE")" || continue
|
|
123
|
+
AGE=$(( NOW - MTIME ))
|
|
124
|
+
|
|
125
|
+
if [ "$AGE" -ge "$MAX_AGE_SECONDS" ]; then
|
|
126
|
+
if rm -f "$FILE" 2>/dev/null; then
|
|
127
|
+
echo "removed: $FILE (age ${AGE}s)"
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
done
|
|
131
|
+
|
|
132
|
+
exit 0
|