@jenga-ai/agent 1.3.0 → 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.
Files changed (76) hide show
  1. package/README.md +97 -92
  2. package/agents/developer.md +9 -8
  3. package/agents/scrum-master.md +57 -23
  4. package/agents/tester.md +51 -5
  5. package/hooks/on_session_end.sh +13 -1
  6. package/lib/generate-agent-context.js +18 -1
  7. package/lib/generate-copilot-instructions.js +18 -1
  8. package/lib/generate-skill-allow-list.js +191 -0
  9. package/lib/skill-allow-list.json +43 -0
  10. package/package.json +18 -4
  11. package/scripts/apply-j-prefix.sh +230 -0
  12. package/scripts/consume-context-digest.sh +103 -0
  13. package/scripts/postinstall.js +25 -0
  14. package/scripts/sweep-stale-context-digests.sh +132 -0
  15. package/scripts/validate-board.sh +5 -0
  16. package/scripts/write-context-digest.sh +230 -0
  17. package/skills/brainstorm/SKILL.md +1 -1
  18. package/skills/btw/SKILL.md +1 -1
  19. package/skills/clearify/SKILL.md +1 -1
  20. package/skills/close-story/SKILL.md +78 -6
  21. package/skills/close-story/scripts/check-privatized.sh +345 -0
  22. package/skills/commit/SKILL.md +1 -1
  23. package/skills/continue/SKILL.md +1 -1
  24. package/skills/deep-dive/SKILL.md +1 -1
  25. package/skills/dev-done/SKILL.md +1 -1
  26. package/skills/distribute/SKILL.md +1 -1
  27. package/skills/do/SKILL.md +100 -10
  28. package/skills/doc/README.md +155 -0
  29. package/skills/doc/SKILL.md +43 -13
  30. package/skills/doc/authoring-notes.md +72 -0
  31. package/skills/doc/scripts/resolve_last_update.py +149 -0
  32. package/skills/doc-sync/SKILL.md +1 -1
  33. package/skills/dooo/SKILL.md +1 -1
  34. package/skills/error/SKILL.md +1 -1
  35. package/skills/evaluate/SKILL.md +1 -1
  36. package/skills/examplify/SKILL.md +1 -1
  37. package/skills/help/SKILL.md +1 -1
  38. package/skills/idea/SKILL.md +1 -1
  39. package/skills/improve/SKILL.md +1 -1
  40. package/skills/init/SKILL.md +1 -1
  41. package/skills/init/assets/scope-thresholds_template.json +3 -3
  42. package/skills/j-init/SKILL.md +168 -0
  43. package/skills/j-init/assets/.gitignore_template +15 -0
  44. package/skills/j-init/assets/PROJECT_SUMMARY_template.md +13 -0
  45. package/skills/j-init/assets/directory_structure.txt +14 -0
  46. package/skills/j-init/assets/scope-thresholds_template.json +7 -0
  47. package/skills/j-init/assets/strategy_stub_template.md +38 -0
  48. package/skills/j-init/assets/test-config_template.json +4 -0
  49. package/skills/j-init/assets/workflow_template.json +30 -0
  50. package/skills/j-init/scripts/apply-project-visibility.sh +176 -0
  51. package/skills/j-init/scripts/detect-existing-codebase.sh +166 -0
  52. package/skills/j-init/scripts/init.sh +116 -0
  53. package/skills/jbp/SKILL.md +1 -1
  54. package/skills/jenga/SKILL.md +1 -1
  55. package/skills/jenga/scripts/render-confirmation.sh +55 -18
  56. package/skills/jenga-permission-level/SKILL.md +1 -1
  57. package/skills/lgtm/SKILL.md +1 -1
  58. package/skills/pi-plan/SKILL.md +1 -1
  59. package/skills/proceed/SKILL.md +1 -1
  60. package/skills/publish/SKILL.md +1 -1
  61. package/skills/publish/adapters/npm-ci.md +26 -4
  62. package/skills/publish/scripts/npm_ci_pipeline.sh +21 -1
  63. package/skills/reconcile/SKILL.md +1 -1
  64. package/skills/reconcile-origin/SKILL.md +1 -1
  65. package/skills/redo/SKILL.md +1 -1
  66. package/skills/skillify/SKILL.md +1 -1
  67. package/skills/spinoff/SKILL.md +1 -1
  68. package/skills/status/SKILL.md +1 -1
  69. package/skills/todo/SKILL.md +40 -3
  70. package/skills/todo/scripts/add_trivial_task.sh +216 -0
  71. package/skills/todo/scripts/update_story_tasks.py +87 -0
  72. package/skills/uncharted/SKILL.md +1 -1
  73. package/skills/wtf/SKILL.md +1 -1
  74. package/templates/SCRUM_BOARD_SCHEMA.md +33 -2
  75. package/templates/agent-context.md.tpl +32 -9
  76. package/templates/copilot-instructions.md.tpl +25 -8
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env bash
2
+ # skills/todo/scripts/add_trivial_task.sh
3
+ #
4
+ # Creates a fully-formed task board file for a `/todo --trivial` mission,
5
+ # forced to execution_scope: inline unconditionally, and registers it both
6
+ # in its parent story's tasks: frontmatter list and in project/todo.md.
7
+ #
8
+ # This script performs only the MECHANICAL half of --trivial: templating the
9
+ # task file, assigning the next task number, and wiring it into the board.
10
+ # The JUDGMENT half — estimating file/line counts and deciding what
11
+ # execution_scope the normal heuristic in agents/scrum-master.md would have
12
+ # assigned (computed_tier) — happens in the calling agent's instructions
13
+ # (skills/todo/SKILL.md), per the Skill Implementation Principle: a step that
14
+ # requires reasoning about branching logic, new dependencies, etc. does not
15
+ # belong in a script. This script never reads project/configs/scope-thresholds.json
16
+ # or recomputes computed_tier itself — it only records what it was told.
17
+ #
18
+ # Usage:
19
+ # add_trivial_task.sh --story <E##_S##> --title "<title>" \
20
+ # --description "<description>" --criteria "<c1>|<c2>|..." \
21
+ # --computed-tier <inline|task|story> --est-files <N> --est-lines <M> \
22
+ # [--prerequisites "<text>"]
23
+ #
24
+ # Requires an ALREADY-EXISTING parent story file (--trivial does not create
25
+ # stories or epics — resolve/create those first via the normal /todo flow).
26
+ #
27
+ # On success, prints the new task ID (e.g. E32_S14_T05) to stdout and exits 0.
28
+
29
+ set -euo pipefail
30
+
31
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
32
+ cd "$REPO_ROOT"
33
+
34
+ TASKS_DIR="project/board/tasks"
35
+ STORIES_DIR="project/board/stories"
36
+ UPDATE_STORY_SCRIPT="skills/todo/scripts/update_story_tasks.py"
37
+
38
+ story_id=""
39
+ title=""
40
+ description=""
41
+ criteria=""
42
+ computed_tier=""
43
+ est_files=""
44
+ est_lines=""
45
+ prerequisites="None."
46
+
47
+ usage() {
48
+ cat >&2 <<EOF
49
+ Usage: $0 --story <E##_S##> --title "<title>" --description "<description>" \\
50
+ --criteria "<c1>|<c2>|..." --computed-tier <inline|task|story> \\
51
+ --est-files <N> --est-lines <M> [--prerequisites "<text>"]
52
+ EOF
53
+ exit 1
54
+ }
55
+
56
+ while [ $# -gt 0 ]; do
57
+ case "$1" in
58
+ --story) story_id="$2"; shift 2 ;;
59
+ --title) title="$2"; shift 2 ;;
60
+ --description) description="$2"; shift 2 ;;
61
+ --criteria) criteria="$2"; shift 2 ;;
62
+ --computed-tier) computed_tier="$2"; shift 2 ;;
63
+ --est-files) est_files="$2"; shift 2 ;;
64
+ --est-lines) est_lines="$2"; shift 2 ;;
65
+ --prerequisites) prerequisites="$2"; shift 2 ;;
66
+ -h|--help) usage ;;
67
+ *) echo "Error: unknown argument '$1'" >&2; usage ;;
68
+ esac
69
+ done
70
+
71
+ for req_name in story_id title description criteria computed_tier est_files est_lines; do
72
+ if [ -z "${!req_name}" ]; then
73
+ echo "Error: --${req_name//_/-} is required" >&2
74
+ exit 1
75
+ fi
76
+ done
77
+
78
+ if ! [[ "$story_id" =~ ^E[0-9]+_S[0-9]+$ ]]; then
79
+ echo "Error: --story must look like E##_S## (got '$story_id')" >&2
80
+ exit 1
81
+ fi
82
+
83
+ case "$computed_tier" in
84
+ inline|task|story) ;;
85
+ *) echo "Error: --computed-tier must be one of: inline, task, story (got '$computed_tier')" >&2; exit 1 ;;
86
+ esac
87
+
88
+ case "$est_files" in
89
+ ''|*[!0-9]*) echo "Error: --est-files must be a non-negative integer (got '$est_files')" >&2; exit 1 ;;
90
+ esac
91
+ case "$est_lines" in
92
+ ''|*[!0-9]*) echo "Error: --est-lines must be a non-negative integer (got '$est_lines')" >&2; exit 1 ;;
93
+ esac
94
+
95
+ epic_id="${story_id%%_S*}"
96
+
97
+ # Locate the parent story file. --trivial requires it to already exist.
98
+ story_file=""
99
+ for f in "${STORIES_DIR}/${story_id}_"*.md; do
100
+ if [ -f "$f" ]; then
101
+ story_file="$f"
102
+ break
103
+ fi
104
+ done
105
+ if [ -z "$story_file" ]; then
106
+ echo "Error: no story file found for $story_id under $STORIES_DIR/. --trivial requires an already-resolved story — create it via the normal /todo flow first." >&2
107
+ exit 1
108
+ fi
109
+
110
+ if [ ! -f "$UPDATE_STORY_SCRIPT" ]; then
111
+ echo "Error: $UPDATE_STORY_SCRIPT not found." >&2
112
+ exit 1
113
+ fi
114
+
115
+ # Determine the next task number for this story by scanning existing task files.
116
+ max_num=0
117
+ shopt -s nullglob
118
+ for f in "${TASKS_DIR}/${story_id}_T"*.md; do
119
+ base="$(basename "$f")"
120
+ num="$(printf '%s' "$base" | sed -nE "s/^${story_id}_T([0-9]+)_.*/\1/p")"
121
+ if [ -n "$num" ]; then
122
+ num=$((10#$num))
123
+ if [ "$num" -gt "$max_num" ]; then
124
+ max_num=$num
125
+ fi
126
+ fi
127
+ done
128
+ shopt -u nullglob
129
+ next_num=$((max_num + 1))
130
+ next_num_padded="$(printf '%02d' "$next_num")"
131
+ task_id="${story_id}_T${next_num_padded}"
132
+
133
+ shopt -s nullglob
134
+ existing_matches=("${TASKS_DIR}/${task_id}_"*.md)
135
+ shopt -u nullglob
136
+ if [ "${#existing_matches[@]}" -gt 0 ]; then
137
+ echo "Error: a task file for $task_id already exists (ID collision). Aborting." >&2
138
+ exit 1
139
+ fi
140
+
141
+ # Slugify the title for the filename.
142
+ slug="$(printf '%s' "$title" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')"
143
+ slug="${slug:0:60}"
144
+ slug="${slug%-}"
145
+ if [ -z "$slug" ]; then
146
+ slug="trivial-task"
147
+ fi
148
+
149
+ today="$(date -u +%Y-%m-%d)"
150
+
151
+ scope_rationale="forced inline via --trivial; computed scope would have been '${computed_tier}' — estimated ${est_files} files, ~${est_lines} lines"
152
+ override_justification="/todo --trivial invoked by user on ${today}; execution_scope forced to 'inline', overriding the heuristic's computed '${computed_tier}' tier (see scope_rationale)."
153
+
154
+ # Build the Acceptance Criteria block from the pipe-separated --criteria value.
155
+ criteria_block=""
156
+ IFS='|' read -ra crit_arr <<< "$criteria"
157
+ for c in "${crit_arr[@]}"; do
158
+ c_trimmed="$(printf '%s' "$c" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
159
+ if [ -n "$c_trimmed" ]; then
160
+ criteria_block+="- [ ] ${c_trimmed}"$'\n'
161
+ fi
162
+ done
163
+ if [ -z "$criteria_block" ]; then
164
+ echo "Error: --criteria produced no usable acceptance criteria" >&2
165
+ exit 1
166
+ fi
167
+
168
+ task_file="${TASKS_DIR}/${task_id}_${slug}.md"
169
+
170
+ cat > "$task_file" <<EOF
171
+ ---
172
+ id: ${task_id}
173
+ story_id: ${story_id}
174
+ epic_id: ${epic_id}
175
+ title: ${title}
176
+ status: Pending
177
+ date_created: ${today}
178
+ date_started:
179
+ date_completed:
180
+ dates_previously_completed:
181
+ reopened_on:
182
+ reopened_reason:
183
+ assigned_to: developer
184
+ docs: []
185
+ execution_scope: inline
186
+ needs_docs: false
187
+ scope_rationale: "${scope_rationale}"
188
+ jenga_assigned: false
189
+ override_justification: "${override_justification}"
190
+ ---
191
+
192
+ # Task: ${title}
193
+
194
+ ## Description
195
+ ${description}
196
+
197
+ ## Prerequisites
198
+ ${prerequisites}
199
+
200
+ ## Acceptance Criteria
201
+ ${criteria_block}
202
+ EOF
203
+
204
+ echo "Wrote task file: ${task_file}"
205
+
206
+ # Register the new task under its parent story's tasks: frontmatter list.
207
+ if ! scripts/with-lock.sh "$story_file" -- python3 "$UPDATE_STORY_SCRIPT" "$story_file" "$task_id"; then
208
+ echo "Error: failed to register ${task_id} in ${story_file}'s tasks: list (lock timeout or write failure)." >&2
209
+ exit 1
210
+ fi
211
+
212
+ # Register the todo entry using the FULL task ID, so /do routes straight into
213
+ # its Inline Execution Path without a redundant breakdown pass.
214
+ bash scripts/todo_manager.sh add "${title}: ${task_id}"
215
+
216
+ echo "${task_id}"
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ skills/todo/scripts/update_story_tasks.py
4
+
5
+ Mechanically inserts a task ID into a story board file's `tasks:` frontmatter
6
+ list, if it is not already present. Idempotent (a re-run with the same
7
+ task_id is a no-op, exit 0).
8
+
9
+ This is a pure text/frontmatter edit and does no board-wide validation beyond
10
+ "frontmatter block exists" — it is intended to be invoked already wrapped in
11
+ scripts/with-lock.sh, keyed to the story file, per templates/SCRUM_BOARD_SCHEMA.md's
12
+ File Locking section (board frontmatter writes must go through the lock wrapper).
13
+
14
+ Usage:
15
+ python3 skills/todo/scripts/update_story_tasks.py <story-file> <task-id>
16
+
17
+ Exit codes:
18
+ 0 success (task_id inserted, or already present)
19
+ 1 usage error
20
+ 2 story file has no parseable frontmatter block
21
+ """
22
+ import sys
23
+
24
+
25
+ def main() -> int:
26
+ if len(sys.argv) != 3:
27
+ print("Usage: update_story_tasks.py <story-file> <task-id>", file=sys.stderr)
28
+ return 1
29
+
30
+ path, task_id = sys.argv[1], sys.argv[2]
31
+
32
+ with open(path, "r", encoding="utf-8") as f:
33
+ content = f.read()
34
+
35
+ parts = content.split("---", 2)
36
+ if len(parts) < 3:
37
+ print(f"Error: {path} has no valid '---' frontmatter block", file=sys.stderr)
38
+ return 2
39
+
40
+ _, frontmatter, rest = parts
41
+ lines = frontmatter.split("\n")
42
+ task_line = f" - {task_id}"
43
+
44
+ if any(line.strip() == task_line.strip() for line in lines):
45
+ print(f"No-op: {task_id} already present in {path}'s tasks: list")
46
+ return 0
47
+
48
+ idx = None
49
+ inline_empty_idx = None
50
+ for i, line in enumerate(lines):
51
+ stripped = line.strip()
52
+ if stripped == "tasks:":
53
+ idx = i
54
+ break
55
+ if stripped == "tasks: []":
56
+ inline_empty_idx = i
57
+ break
58
+
59
+ if inline_empty_idx is not None:
60
+ lines[inline_empty_idx] = "tasks:"
61
+ lines.insert(inline_empty_idx + 1, task_line)
62
+ elif idx is not None:
63
+ j = idx + 1
64
+ while j < len(lines) and lines[j].strip().startswith("- "):
65
+ j += 1
66
+ lines.insert(j, task_line)
67
+ else:
68
+ # No tasks: key found at all — append one at the end of the frontmatter,
69
+ # trimming any single trailing blank line first so formatting stays tidy.
70
+ if lines and lines[-1].strip() == "":
71
+ lines.pop()
72
+ lines.append("tasks:")
73
+ lines.append(task_line)
74
+ lines.append("")
75
+
76
+ new_frontmatter = "\n".join(lines)
77
+ new_content = "---" + new_frontmatter + "---" + rest
78
+
79
+ with open(path, "w", encoding="utf-8") as f:
80
+ f.write(new_content)
81
+
82
+ print(f"Updated {path}: added {task_id} to tasks:")
83
+ return 0
84
+
85
+
86
+ if __name__ == "__main__":
87
+ sys.exit(main())
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: uncharted
2
+ name: j:uncharted
3
3
  description: Investigate code that has no Jenga board provenance — a foreign file, an external source being pulled in, or an entire pre-existing codebase — and give it a consistent understanding document plus proper board representation.
4
4
  metadata:
5
5
  prefered_agent: scrum-master
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: wtf
2
+ name: j:wtf
3
3
  description: Alias of /clearify — clarifies ambiguous, dense, or under-specified prompts and conversation on request. This folder exists only so the `/wtf` slash command resolves to a skill; behaviour is identical to `/clearify`.
4
4
  keywords:
5
5
  - wtf
@@ -67,9 +67,28 @@ All status fields must use one of the following exact strings:
67
67
  | `Blocked` | Cannot proceed; human intervention required |
68
68
  | `Backlog` | Epic-level only; queued but not yet prioritized for work |
69
69
  | `Done` | Epic-level only; all child stories/tasks closed out |
70
+ | `Merged` | Set after a successful `/self-sync` run's file diff shows the ticket's recorded files were touched |
71
+ | `Publicized` | Set after a successful `/mirror-public` run's file diff shows the ticket's recorded files were touched |
72
+ | `Privatized` | Set at ticket-close time via a static `.publicignore` blocklist membership check (no run dependency) |
73
+ | `Deployed to Stage` | Set when the public `jenga-npm` repo's CI tags a `vX.Y.Z-stage` tag that resolves back (via the `Source-Commit:` trailer) to this ticket's commit |
74
+ | `Deployed to Prod` | Set when the public `jenga-npm` repo's CI tags a `vX.Y.Z` (prod) tag that resolves back to this ticket's commit |
70
75
 
71
76
  Only the **tester agent** may write status values to story and task files. Only the **scrum master** may write status values to epic files and may update story status as part of rollup.
72
77
 
78
+ All five statuses above are **script-set, never agent-judged** — no agent decides when a ticket becomes `Merged`, `Publicized`, `Privatized`, `Deployed to Stage`, or `Deployed to Prod`; a deterministic script observation sets them, per the mechanisms described below.
79
+
80
+ ### Static vs. Reactive Status Setting
81
+
82
+ `Privatized` is set **statically**: at ticket-close time, a script checks whether the ticket's recorded files match the `.publicignore` blocklist. This check has no dependency on any particular run having occurred — it is a pure membership test.
83
+
84
+ The other four — `Merged`, `Publicized`, `Deployed to Stage`, `Deployed to Prod` — are set **reactively**: a script observes the outcome of a specific run (a `/self-sync` or `/mirror-public` file diff, or a public-repo CI tag event) and sets the status only when that run's evidence confirms the ticket was affected. Absent a qualifying run, the status is not set.
85
+
86
+ ### Publicized / Privatized / Deployed Lifecycle Relationship
87
+
88
+ `Publicized` and `Privatized` are **mutually exclusive** — a ticket is one or the other, never both. A ticket's files either pass the `.publicignore` blocklist check (making it eligible for `Publicized`) or match it (making it `Privatized`); it cannot satisfy both conditions at once.
89
+
90
+ Only **`Publicized`** tickets are eligible to progress further down the deploy lifecycle, from `Deployed to Stage` to `Deployed to Prod`. A `Privatized` ticket's files never reach the public `jenga-npm` repo, so it can never acquire a CI tag there and therefore can never reach either deploy status.
91
+
73
92
  ---
74
93
 
75
94
  ## File Formats
@@ -161,7 +180,7 @@ reopened_on: # comma-separated list, e.g. 2026-02-01, 2026-04-10
161
180
  reopened_reason: # comma-separated list, e.g. "Scope expanded", "Bug found post-release"
162
181
  assigned_to: developer | tester | scrum-master
163
182
  docs: [] # optional list of repo-relative documentation paths, e.g. ["README.md", "docs/API.md"]
164
- execution_scope: task # task | story | epic | inline; omit for legacy tasks (defaults to task)
183
+ execution_scope: task # task | story | epic | inline | light; omit for legacy tasks (defaults to task)
165
184
  needs_docs: true # boolean; omit for legacy tasks (defaults to true)
166
185
  scope_rationale: "" # required when execution_scope is set; must contain a numeric/file-count claim
167
186
  jenga_assigned: true # boolean; true = machine-assigned, false = human override
@@ -315,13 +334,14 @@ level.
315
334
  These six fields control the execution footprint of a task within the `/jenga` and `/do` workflows. They are **optional** — omitting all six is valid and equivalent to `execution_scope: task` / `needs_docs: true`.
316
335
 
317
336
  **`execution_scope`**
318
- - Valid values: `task` | `story` | `epic` | `inline`
337
+ - Valid values: `task` | `story` | `epic` | `inline` | `light`
319
338
  - When required: optional; omit for legacy tasks (runtime default: `task`)
320
339
  - Description: defines how broadly this task's implementation touches the codebase.
321
340
  - `task` — standard single-task scope (default)
322
341
  - `story` — task may touch files across multiple tasks in the same story
323
342
  - `epic` — task may touch files across stories; requires `epic_scope_approval: true` on the parent epic
324
343
  - `inline` — trivial change (e.g. config tweak, comment, schema doc); no execution plan or summary document is needed
344
+ - `light` — sits between `inline` and `task` in scope: a single developer subagent pass with no worktree, self-verified via `scripts/smoke-harness.sh` in lieu of a separate tester invocation; if the smoke harness fails, execution falls back to `task` scope
325
345
 
326
346
  **`needs_docs`**
327
347
  - Valid values: `true` | `false`
@@ -528,6 +548,7 @@ Each file is written by an agent as the **last action** of its session, and is s
528
548
  | `worktree` | developer, tester | Absolute path |
529
549
  | `paths` | developer, tester | Commit SHAs |
530
550
  | `rapport_file`| tester only | Path to rapport if status is failed/error |
551
+ | `resolved_context` | all, optional | Digest of context the sending agent already resolved; see below |
531
552
  | `date` | all | ISO 8601 UTC |
532
553
 
533
554
  **Status values per agent:**
@@ -535,6 +556,16 @@ Each file is written by an agent as the **last action** of its session, and is s
535
556
  - `developer`: `implementation_complete`
536
557
  - `tester`: `passed`, `passed_with_remarks`, `failed`, `error`
537
558
 
559
+ **`resolved_context` — digest, not a dump (E49).** An optional field a sending agent populates with a short digest of conclusions it already reached while navigating source documents (e.g. which schema fields apply, which skill precedent governs, which decisions are already made) — so the receiving subagent doesn't have to cold-re-read the same files from scratch. It must stay under a size cap of roughly 100 lines (a few hundred tokens), mirroring `scope_rationale`'s "must contain a measurable claim" discipline: a `resolved_context` value that is a raw file dump or exceeds the cap is not valid. The digest is a starting point only — it never restricts the receiving agent from reading full source files when the digest is insufficient or needs verification. The digest body itself lives in a per-task file at `project/queue/context/<agent>-<session_id>-<task_id>.json`, following the same unique-path, single-use, session-scoped convention as `handoffs/` above (not a shared, clobber-prone slot); this handoff's `resolved_context` field holds a reference to (or the inline content of) that file.
560
+
561
+ **`project/queue/context/` — physical digest files (E49_S01_T02).** The directory itself is kept via `.gitkeep`; individual digest files (`*.json`) are git-ignored for the same reason `handoffs/*.json` is — a committed one can no longer be told apart from a live pending digest by inspection alone. Three scripts implement the convention end to end:
562
+
563
+ - `scripts/write-context-digest.sh` — the sending agent's write path. Takes `--agent`, `--session-id`, `--task-id`, and digest content (`--content`, `--content-file`, or stdin); enforces the ~100-line cap above by **rejecting** (not truncating) an oversized digest, since a silently-truncated digest could cut off mid-thought and mislead the receiver — the sender is the only party that actually knows what's safe to cut. Writes atomically (tmp file in the same directory, then `mv`) and prints the resulting absolute path to stdout for the caller to place in the handoff's `resolved_context` field.
564
+ - `scripts/consume-context-digest.sh <path>` — the receiving agent's read path. Atomically claims the file (rename to a `.claimed.$$` sibling, same TOCTOU-safe pattern `on_session_end.sh` section 4 uses for `handoffs/`), prints its content (full JSON envelope, or just the `digest` field with `--raw`), and deletes it — single-use, like `handoffs/`.
565
+ - `scripts/sweep-stale-context-digests.sh` — an age-based backstop (default 24h, overridable), invoked from `hooks/on_session_end.sh` on every session end regardless of agent, for a digest whose intended receiver never calls the consume script (abandoned dispatch, or a receiver that read the raw file directly and forgot to clean up). Age-based rather than routed-and-deleted-immediately like `handoffs/`, because a digest's consumer is a later session that may not have started yet when some unrelated session's `SessionEnd` hook fires.
566
+
567
+ Populating `resolved_context` when dispatching (scrum-master → developer, developer → tester) is sibling task E49_S01_T03 — not yet wired into `agents/scrum-master.md` or `agents/developer.md` as of this writing. The physical convention above is usable standalone in the meantime.
568
+
538
569
 
539
570
 
540
571
  Located at `project/configs/workflow.json`. Scaffolded by `/init` and owned by the scrum master.
@@ -10,20 +10,30 @@ This project uses **Jenga** — a skill-based AI agent framework. Jenga organise
10
10
  ### How Jenga Works
11
11
 
12
12
  - Each **skill** is a self-contained instruction set stored under `{{SKILL_DISCOVERY_PATH}}<skill-name>/`.
13
- - Skills are invoked by typing `/skill-name` in the chat prompt.
13
+ - Skills are invoked by typing `j:skill-name` in the chat prompt (e.g. `j:status`, `j:commit`). The
14
+ older bare `/skill-name` form (e.g. `/status`, `/commit`) is a **permanent alias** — it keeps
15
+ resolving indefinitely, with no deprecation warning and no removal planned — so treat a message in
16
+ either form as the exact same invocation.
14
17
  - The active project directory is available via the `JENGA_PROJECT_DIR` environment variable. **Use `JENGA_PROJECT_DIR` — not `CLAUDE_PROJECT_DIR` or any other agent-specific variable** — as the canonical path to the project folder.
15
18
 
16
19
  ### Skill Routing
17
20
 
18
- If you are Claude Code, `/skill-name` is a native harness-level mechanism: the harness itself
19
- intercepts the literal command and loads the skill for you, independent of anything written here. If
20
- you are any other agent (Codex, or a generic `AGENTS.md` consumer) with no equivalent native
21
- interception, you depend entirely on the instructions below to know what "invoking a skill" concretely
22
- means. Do not improvise a plausible-sounding response instead of following these steps — that is the
23
- exact failure this section exists to prevent.
21
+ If you are Claude Code, both `j:skill-name` (the canonical form) and the older bare `/skill-name`
22
+ (a permanent alias) are native harness-level mechanisms: the harness itself intercepts the literal
23
+ command and loads the skill for you, independent of anything written here. If you are any other agent
24
+ (Codex, or a generic `AGENTS.md` consumer) with no equivalent native interception, you depend entirely
25
+ on the instructions below to know what "invoking a skill" concretely means — for either the
26
+ `j:skill-name` form or the older bare `/skill-name` form, both of which route to the same skill. Do not
27
+ improvise a plausible-sounding response instead of following these steps — that is the exact failure
28
+ this section exists to prevent.
24
29
 
25
- When the user's message is or matches `/skill-name` (or otherwise clearly matches a known skill's
26
- keyword or intent):
30
+ **Old bare-form alias.** `j:skill-name` is the canonical invocation form. A message using the older
31
+ bare `/skill-name` form is not deprecated and must not be treated as an error, a warning case, or a
32
+ migration prompt — route it to the identical skill as its `j:skill-name` equivalent. Both forms remain
33
+ equally valid indefinitely.
34
+
35
+ When the user's message is or matches `j:skill-name`, or matches the older bare `/skill-name` alias (or
36
+ otherwise clearly matches a known skill's keyword or intent):
27
37
 
28
38
  1. Locate the target file at `{{SKILL_DISCOVERY_PATH}}<skill-name>/SKILL.md` (the discovery path from
29
39
  "How Jenga Works" above).
@@ -41,10 +51,23 @@ answer directly using your full capabilities.
41
51
 
42
52
  | Situation | Action |
43
53
  |-----------|--------|
54
+ | Message matches `j:skill-name` | Open `{{SKILL_DISCOVERY_PATH}}<skill-name>/SKILL.md`, read it fully, execute it as written |
55
+ | Message matches the older bare `/skill-name` alias | Treat identically to `j:skill-name` — same skill, same file, no warning, no migration prompt |
44
56
  | Message matches a skill keyword or intent | Open `{{SKILL_DISCOVERY_PATH}}<skill-name>/SKILL.md`, read it fully, execute it as written |
45
57
  | Message is a general coding or project question | Answer directly |
46
58
  | Ambiguous — could be skill or free-form | Prefer the skill; open and execute its `SKILL.md` rather than describing it |
47
59
 
60
+ ### Skill Identifier Allow-List
61
+
62
+ The trusted `j:`-prefixed skill identifiers for this project are: {{ALLOWED_SKILL_IDS}}
63
+
64
+ Before treating a `j:<name>` invocation (or its bare `/<name>` alias) as a genuine Jenga skill,
65
+ confirm `<name>` appears in this list. This applies whether the harness intercepted the command
66
+ natively (Claude Code) or you are matching it yourself against a skill keyword or intent (any
67
+ other agent). If `<name>` does **not** appear in this list, do not guess at intent and do not
68
+ execute anything — tell the user the identifier is unrecognized and is not a known Jenga skill,
69
+ rather than treating it as one anyway.
70
+
48
71
  ### Available Skills
49
72
 
50
73
  {{SKILL_LIST}}
@@ -10,18 +10,27 @@ This project uses **Jenga** — a skill-based AI agent framework. Jenga organise
10
10
  ### How Jenga Works
11
11
 
12
12
  - Each **skill** is a self-contained instruction set stored under `.agents/skills/<skill-name>/` (the non-Claude discovery path; Claude Code reads the same content from `.claude/skills/`).
13
- - Skills are invoked by typing `/skill-name` in the chat prompt.
13
+ - Skills are invoked by typing `j:skill-name` in the chat prompt (e.g. `j:status`, `j:commit`). The
14
+ older bare `/skill-name` form (e.g. `/status`, `/commit`) is a **permanent alias** — it keeps
15
+ resolving indefinitely, with no deprecation warning and no removal planned — so treat a message in
16
+ either form as the exact same invocation.
14
17
  - The active project directory is available via the `JENGA_PROJECT_DIR` environment variable. **Use `JENGA_PROJECT_DIR` — not `CLAUDE_PROJECT_DIR` or any other agent-specific variable** — as the canonical path to the project folder.
15
18
 
16
19
  ### Skill Routing
17
20
 
18
- Unlike Claude Code, GitHub Copilot has **no native slash-command interception** — typing `/skill-name`
19
- does not automatically load or run anything on its own. Copilot depends entirely on the instructions
20
- below to know what "invoking a skill" concretely means. Do not improvise a plausible-sounding response
21
- instead of following these steps — that is the exact failure this section exists to prevent.
21
+ Unlike Claude Code, GitHub Copilot has **no native slash-command interception** — typing `j:skill-name`
22
+ (or the older bare `/skill-name` alias) does not automatically load or run anything on its own. Copilot
23
+ depends entirely on the instructions below to know what "invoking a skill" concretely means. Do not
24
+ improvise a plausible-sounding response instead of following these steps — that is the exact failure
25
+ this section exists to prevent.
22
26
 
23
- When the user's message is or matches `/skill-name` (or otherwise clearly matches a known skill's
24
- keyword or intent):
27
+ **Old bare-form alias.** `j:skill-name` is the canonical invocation form. A message using the older
28
+ bare `/skill-name` form is not deprecated and must not be treated as an error, a warning case, or a
29
+ migration prompt — route it to the identical skill as its `j:skill-name` equivalent. Both forms remain
30
+ equally valid indefinitely.
31
+
32
+ When the user's message is or matches `j:skill-name`, or matches the older bare `/skill-name` alias (or
33
+ otherwise clearly matches a known skill's keyword or intent):
25
34
 
26
35
  1. Locate the target file at `.agents/skills/<skill-name>/SKILL.md` (the discovery path from "How
27
36
  Jenga Works" above).
@@ -37,9 +46,17 @@ answer directly using your full capabilities.
37
46
 
38
47
  #### Routing decision table
39
48
 
49
+ Before acting on any row below that opens a `SKILL.md` file, first check the identifier against
50
+ the trusted allow-list: {{ALLOWED_SKILL_IDS}}. Since Copilot/Codex has no native interception for
51
+ `j:skill-name`, this prose-level check is the entire enforcement mechanism — there is no runtime
52
+ guard behind it.
53
+
40
54
  | Situation | Action |
41
55
  |-----------|--------|
42
- | Message matches a skill keyword or intent | Open `.agents/skills/<skill-name>/SKILL.md`, read it fully, execute it as written |
56
+ | Message matches `j:skill-name` and `skill-name` is in the allow-list | Open `.agents/skills/<skill-name>/SKILL.md`, read it fully, execute it as written |
57
+ | Message matches the older bare `/skill-name` alias and `skill-name` is in the allow-list | Treat identically to `j:skill-name` — same skill, same file, no warning, no migration prompt |
58
+ | Message matches a skill keyword or intent and the matched skill is in the allow-list | Open `.agents/skills/<skill-name>/SKILL.md`, read it fully, execute it as written |
59
+ | Message matches `j:skill-name` or `/skill-name`, but `skill-name` is **not** in the allow-list | Do not open or execute anything — tell the user the identifier is unrecognized and is not a known Jenga skill |
43
60
  | Message is a general coding or project question | Answer directly |
44
61
  | Ambiguous — could be skill or free-form | Prefer the skill; open and execute its `SKILL.md` rather than describing it |
45
62