@jenga-ai/agent 1.2.4 → 2.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 +97 -91
- package/agents/developer.md +26 -7
- package/agents/scrum-master.md +57 -22
- package/agents/tester.md +68 -4
- package/hooks/on_session_end.sh +40 -1
- package/lib/generate-agent-context.js +18 -1
- package/lib/generate-copilot-instructions.js +18 -1
- package/lib/generate-skill-allow-list.js +191 -0
- package/lib/skill-allow-list.json +43 -0
- package/package.json +35 -20
- package/scripts/apply-j-prefix.sh +230 -0
- package/scripts/consume-context-digest.sh +103 -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/SKILL.md +1 -1
- package/skills/btw/SKILL.md +1 -1
- package/skills/clearify/SKILL.md +1 -1
- package/skills/close-story/SKILL.md +78 -6
- package/skills/close-story/scripts/check-privatized.sh +345 -0
- package/skills/commit/SKILL.md +12 -2
- package/skills/continue/SKILL.md +1 -1
- package/skills/deep-dive/SKILL.md +1 -1
- package/skills/dev-done/SKILL.md +46 -0
- package/skills/dev-done/scripts/classify-commit-outcome.sh +114 -0
- package/skills/distribute/SKILL.md +1 -1
- package/skills/do/SKILL.md +100 -10
- package/skills/doc/README.md +155 -0
- package/skills/doc/SKILL.md +43 -13
- package/skills/doc/authoring-notes.md +72 -0
- package/skills/doc/scripts/resolve_last_update.py +149 -0
- package/skills/doc-sync/SKILL.md +1 -1
- package/skills/dooo/SKILL.md +1 -1
- package/skills/error/SKILL.md +1 -1
- package/skills/evaluate/SKILL.md +1 -1
- package/skills/examplify/SKILL.md +1 -1
- package/skills/help/SKILL.md +1 -1
- package/skills/idea/SKILL.md +1 -1
- package/skills/improve/SKILL.md +1 -1
- package/skills/init/SKILL.md +8 -7
- package/skills/init/assets/scope-thresholds_template.json +7 -0
- package/skills/init/scripts/init.sh +6 -0
- package/skills/j-init/SKILL.md +168 -0
- package/skills/j-init/assets/.gitignore_template +15 -0
- package/skills/j-init/assets/PROJECT_SUMMARY_template.md +13 -0
- package/skills/j-init/assets/directory_structure.txt +14 -0
- package/skills/j-init/assets/scope-thresholds_template.json +7 -0
- package/skills/j-init/assets/strategy_stub_template.md +38 -0
- package/skills/j-init/assets/test-config_template.json +4 -0
- package/skills/j-init/assets/workflow_template.json +30 -0
- package/skills/j-init/scripts/apply-project-visibility.sh +176 -0
- package/skills/j-init/scripts/detect-existing-codebase.sh +166 -0
- package/skills/j-init/scripts/init.sh +116 -0
- package/skills/jbp/SKILL.md +1 -1
- 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/skills/lgtm/SKILL.md +1 -1
- package/skills/pi-plan/SKILL.md +1 -1
- package/skills/proceed/SKILL.md +1 -1
- package/skills/publish/SKILL.md +67 -1
- package/skills/publish/adapters/npm-ci.md +60 -4
- package/skills/publish/adapters/npm.md +18 -0
- package/skills/publish/assets/ci-contract.md +27 -0
- package/skills/publish/assets/publish.example.json +27 -0
- package/skills/publish/schemas/publish.schema.json +20 -0
- package/skills/publish/scripts/npm_ci_pipeline.sh +50 -1
- package/skills/publish/scripts/npm_stage_inspect.sh +829 -0
- package/skills/publish/scripts/npm_stage_pipeline.sh +427 -0
- package/skills/publish/scripts/publish_common.sh +16 -0
- package/skills/publish/scripts/show_history.sh +12 -5
- package/skills/publish/scripts/validate_npm_stage_env.sh +184 -0
- package/skills/publish/scripts/write_ledger_entry.sh +92 -2
- package/skills/reconcile/SKILL.md +122 -12
- package/skills/reconcile/assets/report_format.md +17 -0
- package/skills/reconcile/scripts/resolve-reconcile-scope.sh +489 -0
- package/skills/reconcile-origin/SKILL.md +1 -1
- package/skills/redo/SKILL.md +1 -1
- package/skills/skillify/SKILL.md +1 -1
- package/skills/spinoff/SKILL.md +1 -1
- package/skills/status/SKILL.md +1 -1
- package/skills/todo/SKILL.md +40 -3
- package/skills/todo/scripts/add_trivial_task.sh +216 -0
- package/skills/todo/scripts/update_story_tasks.py +87 -0
- package/skills/uncharted/SKILL.md +201 -22
- package/skills/uncharted/scripts/directory-triage.sh +342 -0
- package/skills/uncharted/scripts/elicitation-state.sh +457 -0
- package/skills/wtf/SKILL.md +1 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +90 -2
- package/templates/agent-context.md.tpl +47 -12
- package/templates/copilot-instructions.md.tpl +36 -9
- package/mcp/router/README.md +0 -19
- package/mcp/router/embedder.js +0 -23
- package/mcp/router/index.js +0 -204
- package/mcp/router/matcher.js +0 -87
- package/mcp/router/package-lock.json +0 -1048
- package/mcp/router/package.json +0 -11
- package/mcp/router/skill-index.js +0 -104
- package/skills/route/SKILL.md +0 -180
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/apply-j-prefix.sh — mechanical j: prefix rename for skill invocation
|
|
3
|
+
#
|
|
4
|
+
# Implements the mechanism decided in docs/skill-authoring.md's "Invocation Convention"
|
|
5
|
+
# section (E50_S01_T01):
|
|
6
|
+
#
|
|
7
|
+
# 1. Frontmatter-only rename. For every skills/<name>/SKILL.md, rewrites the
|
|
8
|
+
# frontmatter `name:` field from `<name>` to `j:<name>`. Directory names are
|
|
9
|
+
# left untouched — ":" is not a Windows-safe filename character, and Claude
|
|
10
|
+
# Code's native skill resolution is a literal-string match against the
|
|
11
|
+
# discovery-path directory name, independent of frontmatter content (see the
|
|
12
|
+
# docs section for the full investigation and citation).
|
|
13
|
+
# 2. Bare-form prose rewrite. For every agents/*.md file, rewrites bare
|
|
14
|
+
# "/<name>" invocation mentions in prose to "j:<name>", for every skill name
|
|
15
|
+
# discovered in step 1 — longest-name-first, boundary-safe (skill names
|
|
16
|
+
# contain hyphens, so a naive `\b` regex would misfire — e.g. it would
|
|
17
|
+
# falsely match "/doc" inside "/doc-sync"; this only rewrites a match when
|
|
18
|
+
# the character immediately before/after it is not itself an identifier
|
|
19
|
+
# character: letter, digit, "-", or ":").
|
|
20
|
+
#
|
|
21
|
+
# This script does NOT remove or disable the old bare "/<name>" form. The
|
|
22
|
+
# migration decision recorded alongside the mechanism above is "alias" — both
|
|
23
|
+
# forms keep resolving. This script only ever writes inside skills/ and
|
|
24
|
+
# agents/; it never touches .claude/ or .agents/ (generated mirrors — see
|
|
25
|
+
# CLAUDE.md's Source of Truth rule) and makes no other filesystem change.
|
|
26
|
+
#
|
|
27
|
+
# Usage:
|
|
28
|
+
# scripts/apply-j-prefix.sh [--dry-run] [--skills-only | --agents-only]
|
|
29
|
+
#
|
|
30
|
+
# --dry-run Print the full change list without writing anything.
|
|
31
|
+
# Exit code is 0 whether or not changes were found (unless
|
|
32
|
+
# --skills-only/--agents-only surface a hard error, e.g. a
|
|
33
|
+
# frontmatter `name:` that doesn't match its directory).
|
|
34
|
+
# --skills-only Only process skills/*/SKILL.md frontmatter (step 1).
|
|
35
|
+
# --agents-only Only process agents/*.md prose (step 2).
|
|
36
|
+
#
|
|
37
|
+
# Idempotent: already-migrated skills (name already starts with "j:") are
|
|
38
|
+
# silently skipped, not re-applied or reported as an error.
|
|
39
|
+
|
|
40
|
+
set -euo pipefail
|
|
41
|
+
|
|
42
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
43
|
+
# shellcheck source=lib/resolve-project-dir.sh
|
|
44
|
+
source "$SCRIPT_DIR/../lib/resolve-project-dir.sh"
|
|
45
|
+
|
|
46
|
+
PROJECT_DIR="$JENGA_PROJECT_DIR"
|
|
47
|
+
SKILLS_DIR="$PROJECT_DIR/skills"
|
|
48
|
+
AGENTS_DIR="$PROJECT_DIR/agents"
|
|
49
|
+
|
|
50
|
+
DRY_RUN=0
|
|
51
|
+
DO_SKILLS=1
|
|
52
|
+
DO_AGENTS=1
|
|
53
|
+
|
|
54
|
+
for arg in "$@"; do
|
|
55
|
+
case "$arg" in
|
|
56
|
+
--dry-run) DRY_RUN=1 ;;
|
|
57
|
+
--skills-only) DO_AGENTS=0 ;;
|
|
58
|
+
--agents-only) DO_SKILLS=0 ;;
|
|
59
|
+
-h|--help)
|
|
60
|
+
echo "Usage: $(basename "$0") [--dry-run] [--skills-only | --agents-only]"
|
|
61
|
+
exit 0
|
|
62
|
+
;;
|
|
63
|
+
*)
|
|
64
|
+
echo "❌ Unknown argument: $arg" >&2
|
|
65
|
+
echo "Usage: $(basename "$0") [--dry-run] [--skills-only | --agents-only]" >&2
|
|
66
|
+
exit 1
|
|
67
|
+
;;
|
|
68
|
+
esac
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
export DRY_RUN DO_SKILLS DO_AGENTS SKILLS_DIR AGENTS_DIR
|
|
72
|
+
|
|
73
|
+
python3 - <<'PY'
|
|
74
|
+
import os
|
|
75
|
+
import re
|
|
76
|
+
import sys
|
|
77
|
+
|
|
78
|
+
skills_dir = os.environ["SKILLS_DIR"]
|
|
79
|
+
agents_dir = os.environ["AGENTS_DIR"]
|
|
80
|
+
dry_run = os.environ["DRY_RUN"] == "1"
|
|
81
|
+
do_skills = os.environ["DO_SKILLS"] == "1"
|
|
82
|
+
do_agents = os.environ["DO_AGENTS"] == "1"
|
|
83
|
+
|
|
84
|
+
changes = []
|
|
85
|
+
errors = []
|
|
86
|
+
|
|
87
|
+
# ---- Step 1: discover skills + (optionally) rewrite frontmatter name: field ----
|
|
88
|
+
skill_names = [] # bare directory names, in sorted order
|
|
89
|
+
name_re = re.compile(r'^name:\s*(.+?)\s*$')
|
|
90
|
+
|
|
91
|
+
if os.path.isdir(skills_dir):
|
|
92
|
+
for entry in sorted(os.listdir(skills_dir)):
|
|
93
|
+
skill_path = os.path.join(skills_dir, entry, "SKILL.md")
|
|
94
|
+
if not os.path.isfile(skill_path):
|
|
95
|
+
continue # not a skill dir (e.g. skills/index/ has no SKILL.md)
|
|
96
|
+
|
|
97
|
+
with open(skill_path, "r", encoding="utf-8") as f:
|
|
98
|
+
lines = f.readlines()
|
|
99
|
+
|
|
100
|
+
if not lines or lines[0].strip() != "---":
|
|
101
|
+
errors.append(f"{skill_path}: no frontmatter block found")
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
close_idx = None
|
|
105
|
+
for i in range(1, len(lines)):
|
|
106
|
+
if lines[i].strip() == "---":
|
|
107
|
+
close_idx = i
|
|
108
|
+
break
|
|
109
|
+
if close_idx is None:
|
|
110
|
+
errors.append(f"{skill_path}: unterminated frontmatter block")
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
name_idx = None
|
|
114
|
+
current_name = None
|
|
115
|
+
for i in range(1, close_idx):
|
|
116
|
+
m = name_re.match(lines[i])
|
|
117
|
+
if m:
|
|
118
|
+
name_idx = i
|
|
119
|
+
current_name = m.group(1).strip('"\'')
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
if name_idx is None:
|
|
123
|
+
errors.append(f"{skill_path}: no 'name:' field in frontmatter")
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
skill_names.append(entry)
|
|
127
|
+
|
|
128
|
+
if current_name.startswith("j:"):
|
|
129
|
+
continue # already migrated — not an error, nothing to do
|
|
130
|
+
|
|
131
|
+
if current_name != entry:
|
|
132
|
+
errors.append(
|
|
133
|
+
f"{skill_path}: frontmatter name '{current_name}' does not match "
|
|
134
|
+
f"directory name '{entry}' — skipped for safety"
|
|
135
|
+
)
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if not do_skills:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
new_line = f"name: j:{entry}\n"
|
|
142
|
+
changes.append({
|
|
143
|
+
"type": "frontmatter",
|
|
144
|
+
"file": skill_path,
|
|
145
|
+
"before": lines[name_idx].rstrip("\n"),
|
|
146
|
+
"after": new_line.rstrip("\n"),
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
if not dry_run:
|
|
150
|
+
lines[name_idx] = new_line
|
|
151
|
+
with open(skill_path, "w", encoding="utf-8") as f:
|
|
152
|
+
f.writelines(lines)
|
|
153
|
+
elif do_skills:
|
|
154
|
+
errors.append(f"skills directory not found: {skills_dir}")
|
|
155
|
+
|
|
156
|
+
# ---- Step 2: rewrite bare "/<name>" prose mentions in agents/*.md ----
|
|
157
|
+
if do_agents and os.path.isdir(agents_dir):
|
|
158
|
+
# Longest name first so "/jenga-permission-level" is matched (and
|
|
159
|
+
# consumed) before the shorter "/jenga" gets a chance to.
|
|
160
|
+
ordered_names = sorted(skill_names, key=len, reverse=True)
|
|
161
|
+
boundary_chars = set(
|
|
162
|
+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-:"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
for entry in sorted(os.listdir(agents_dir)):
|
|
166
|
+
if not entry.endswith(".md"):
|
|
167
|
+
continue
|
|
168
|
+
agent_path = os.path.join(agents_dir, entry)
|
|
169
|
+
if not os.path.isfile(agent_path):
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
with open(agent_path, "r", encoding="utf-8") as f:
|
|
173
|
+
text = f.read()
|
|
174
|
+
|
|
175
|
+
file_changes = 0
|
|
176
|
+
|
|
177
|
+
for name in ordered_names:
|
|
178
|
+
pattern = re.compile(r"/" + re.escape(name))
|
|
179
|
+
matches = []
|
|
180
|
+
for m in pattern.finditer(text):
|
|
181
|
+
start, end = m.start(), m.end()
|
|
182
|
+
if start > 0 and text[start - 1] in boundary_chars:
|
|
183
|
+
continue # part of a longer token, e.g. "x/status"
|
|
184
|
+
if end < len(text) and text[end] in boundary_chars:
|
|
185
|
+
continue # e.g. "/doc" inside "/doc-sync"
|
|
186
|
+
matches.append((start, end))
|
|
187
|
+
for start, end in reversed(matches):
|
|
188
|
+
text = text[:start] + f"j:{name}" + text[end:]
|
|
189
|
+
file_changes += 1
|
|
190
|
+
|
|
191
|
+
if file_changes:
|
|
192
|
+
changes.append({
|
|
193
|
+
"type": "agent-prose",
|
|
194
|
+
"file": agent_path,
|
|
195
|
+
"count": file_changes,
|
|
196
|
+
})
|
|
197
|
+
if not dry_run:
|
|
198
|
+
with open(agent_path, "w", encoding="utf-8") as f:
|
|
199
|
+
f.write(text)
|
|
200
|
+
elif do_agents and not os.path.isdir(agents_dir):
|
|
201
|
+
errors.append(f"agents directory not found: {agents_dir}")
|
|
202
|
+
|
|
203
|
+
# ---- report ----
|
|
204
|
+
mode = "DRY RUN — no files written" if dry_run else "APPLIED"
|
|
205
|
+
print(f"apply-j-prefix.sh — {mode}")
|
|
206
|
+
print(f"Skills discovered: {len(skill_names)}")
|
|
207
|
+
print()
|
|
208
|
+
|
|
209
|
+
if not changes:
|
|
210
|
+
print("No changes.")
|
|
211
|
+
else:
|
|
212
|
+
for c in changes:
|
|
213
|
+
if c["type"] == "frontmatter":
|
|
214
|
+
print(f"[frontmatter] {c['file']}")
|
|
215
|
+
print(f" - {c['before']}")
|
|
216
|
+
print(f" + {c['after']}")
|
|
217
|
+
else:
|
|
218
|
+
print(f"[agent-prose] {c['file']} — {c['count']} occurrence(s) rewritten")
|
|
219
|
+
print()
|
|
220
|
+
print(f"Total: {len(changes)} change group(s)")
|
|
221
|
+
|
|
222
|
+
if errors:
|
|
223
|
+
print()
|
|
224
|
+
print("Errors / skipped:")
|
|
225
|
+
for e in errors:
|
|
226
|
+
print(f" ⚠ {e}")
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
|
|
229
|
+
sys.exit(0)
|
|
230
|
+
PY
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# scripts/consume-context-digest.sh
|
|
4
|
+
#
|
|
5
|
+
# Read-and-delete helper for the resolved_context digest handoff convention
|
|
6
|
+
# (E49_S01). The receiving subagent (the one whose sender object carries a
|
|
7
|
+
# resolved_context / resolved_context_path pointing at a file under
|
|
8
|
+
# project/queue/context/) calls this once, as part of reading its own sender
|
|
9
|
+
# object, to fetch the digest content and immediately mark the file consumed
|
|
10
|
+
# — mirroring project/queue/handoffs/'s single-use lifecycle rather than
|
|
11
|
+
# leaving cleanup to a convention every receiver has to remember to follow.
|
|
12
|
+
#
|
|
13
|
+
# Atomic claim: before reading, the target file is renamed to a
|
|
14
|
+
# `.claimed.$$` sibling in the same directory (same TOCTOU-safe pattern
|
|
15
|
+
# hooks/on_session_end.sh section 4 already uses when routing
|
|
16
|
+
# project/queue/handoffs/ files). `mv` between two paths on the same
|
|
17
|
+
# filesystem is a rename(2) syscall — the kernel guarantees exactly one
|
|
18
|
+
# concurrent claimer wins. If the rename fails, another process already
|
|
19
|
+
# consumed (or is consuming) this file; this invocation exits 3 without
|
|
20
|
+
# printing anything, rather than risking a double-read of a file that's
|
|
21
|
+
# about to disappear out from under it.
|
|
22
|
+
#
|
|
23
|
+
# This script does NOT sweep for abandoned digests nobody ever consumes —
|
|
24
|
+
# that backstop is scripts/sweep-stale-context-digests.sh, invoked from
|
|
25
|
+
# hooks/on_session_end.sh.
|
|
26
|
+
#
|
|
27
|
+
# Usage:
|
|
28
|
+
# consume-context-digest.sh <path-to-digest-file>
|
|
29
|
+
# consume-context-digest.sh --raw <path-to-digest-file>
|
|
30
|
+
#
|
|
31
|
+
# Options:
|
|
32
|
+
# --raw print only the digest field's text (not the full JSON envelope)
|
|
33
|
+
# -h, --help
|
|
34
|
+
#
|
|
35
|
+
# Output (stdout):
|
|
36
|
+
# the digest file's full JSON envelope (default), or just its `digest`
|
|
37
|
+
# field's text with --raw
|
|
38
|
+
#
|
|
39
|
+
# Exit codes:
|
|
40
|
+
# 0 digest read and file deleted
|
|
41
|
+
# 1 usage error
|
|
42
|
+
# 2 target file does not exist (already consumed, or never written)
|
|
43
|
+
# 3 target file was claimed by a concurrent consumer first
|
|
44
|
+
# 4 target file is not valid JSON / missing expected fields
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
set -euo pipefail
|
|
48
|
+
|
|
49
|
+
SELF="$(basename "$0")"
|
|
50
|
+
|
|
51
|
+
die() {
|
|
52
|
+
local code="$1"; shift
|
|
53
|
+
printf '%s: error: %s\n' "$SELF" "$*" >&2
|
|
54
|
+
exit "$code"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
usage() {
|
|
58
|
+
sed -n '/^# Usage:/,/^# Exit codes:/p' "$0" | sed 's/^# \{0,1\}//' >&2
|
|
59
|
+
exit 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
command -v jq >/dev/null 2>&1 || die 1 "jq is required but not found on PATH"
|
|
63
|
+
|
|
64
|
+
RAW=0
|
|
65
|
+
TARGET=""
|
|
66
|
+
|
|
67
|
+
while [ "$#" -gt 0 ]; do
|
|
68
|
+
case "$1" in
|
|
69
|
+
--raw)
|
|
70
|
+
RAW=1; shift ;;
|
|
71
|
+
-h|--help)
|
|
72
|
+
usage ;;
|
|
73
|
+
-*)
|
|
74
|
+
die 1 "unknown option: $1" ;;
|
|
75
|
+
*)
|
|
76
|
+
[ -z "$TARGET" ] || die 1 "unexpected extra argument: $1"
|
|
77
|
+
TARGET="$1"; shift ;;
|
|
78
|
+
esac
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
[ -n "$TARGET" ] || usage
|
|
82
|
+
[ -f "$TARGET" ] || die 2 "digest file not found: $TARGET (already consumed, or never written)"
|
|
83
|
+
|
|
84
|
+
CLAIMED_FILE="${TARGET}.claimed.$$"
|
|
85
|
+
if ! mv "$TARGET" "$CLAIMED_FILE" 2>/dev/null; then
|
|
86
|
+
die 3 "digest file '$TARGET' was claimed by a concurrent consumer — skipping"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Always attempt to remove the claimed temp file, whether jq succeeds or not,
|
|
90
|
+
# so a malformed digest can't leave a permanent orphan behind.
|
|
91
|
+
cleanup() {
|
|
92
|
+
rm -f "$CLAIMED_FILE" 2>/dev/null || true
|
|
93
|
+
}
|
|
94
|
+
trap cleanup EXIT
|
|
95
|
+
|
|
96
|
+
if [ "$RAW" -eq 1 ]; then
|
|
97
|
+
DIGEST_TEXT=$(jq -r '.digest // empty' "$CLAIMED_FILE" 2>/dev/null) || die 4 "'$TARGET' is not valid digest JSON"
|
|
98
|
+
[ -n "$DIGEST_TEXT" ] || die 4 "'$TARGET' has no 'digest' field"
|
|
99
|
+
printf '%s\n' "$DIGEST_TEXT"
|
|
100
|
+
else
|
|
101
|
+
jq -e '.' "$CLAIMED_FILE" >/dev/null 2>&1 || die 4 "'$TARGET' is not valid JSON"
|
|
102
|
+
cat "$CLAIMED_FILE"
|
|
103
|
+
fi
|
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
|