@jenga-ai/agent 3.2.0 → 3.5.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 +52 -12
- package/agents/developer.md +16 -1
- package/agents/scrum-master.md +1 -0
- package/bin/jenga.js +10 -0
- package/lib/commands/dashboard.js +92 -0
- package/lib/skill-allow-list.json +6 -2
- package/package.json +21 -2
- package/project/app/api/lib/resolve-project-root.js +120 -0
- package/project/app/api/package.json +16 -0
- package/project/app/api/parsers/architecture.js +72 -0
- package/project/app/api/parsers/board.js +141 -0
- package/project/app/api/parsers/documentation.js +125 -0
- package/project/app/api/parsers/git-log.js +52 -0
- package/project/app/api/parsers/ideas.js +62 -0
- package/project/app/api/parsers/knowledge-graph.js +73 -0
- package/project/app/api/parsers/lib/markdown-dir-reader.js +163 -0
- package/project/app/api/parsers/rapports.js +148 -0
- package/project/app/api/parsers/todo.js +179 -0
- package/project/app/api/response.js +47 -0
- package/project/app/api/routes/architecture.js +23 -0
- package/project/app/api/routes/board.js +46 -0
- package/project/app/api/routes/documentation.js +24 -0
- package/project/app/api/routes/health.js +25 -0
- package/project/app/api/routes/history.js +55 -0
- package/project/app/api/routes/rapports.js +24 -0
- package/project/app/api/scripts/capture-snapshot.js +294 -0
- package/project/app/api/server.js +112 -0
- package/project/app/api/types.js +40 -0
- package/project/app/package.json +21 -0
- package/project/app/ui/dist/assets/index-7fj-vllY.js +104 -0
- package/project/app/ui/dist/assets/index-CdK3Qrep.css +1 -0
- package/project/app/ui/dist/index.html +13 -0
- package/project/app/ui/package.json +23 -0
- package/project/app/ui/scripts/build-snapshot-html.cjs +214 -0
- package/project/app/ui/scripts/dashboard-open.cjs +88 -0
- package/project/app/ui/scripts/dashboard-start.cjs +87 -0
- package/scripts/acquire-concurrency-slot.sh +220 -0
- package/scripts/compute-deploy-reconcile.sh +439 -0
- package/scripts/jenga-permission-level-switch.sh +19 -3
- package/scripts/mark-deployed.sh +532 -0
- package/scripts/populate-knowledge-graph.js +429 -0
- package/scripts/release-concurrency-slot.sh +129 -0
- package/scripts/validate-board.sh +60 -2
- package/scripts/verify-consumer-install.sh +470 -0
- package/skills/j-cloud-connect/SKILL.md +95 -0
- package/skills/j-cloud-connect/scripts/configure-backend.sh +267 -0
- package/skills/j-cloud-connect/scripts/install-rclone.sh +153 -0
- package/skills/j-dashboard/SKILL.md +144 -0
- package/skills/j-dashboard/scripts/launch.sh +121 -0
- package/skills/j-dashboard/scripts/resolve-app-dir.sh +164 -0
- package/skills/j-dashboard/scripts/snapshot.sh +267 -0
- package/skills/j-dashboard-share/SKILL.md +96 -0
- package/skills/j-dashboard-share/scripts/upload-snapshot.sh +173 -0
- package/skills/j-init/SKILL.md +52 -13
- package/skills/j-init/assets/.gitignore_template +1 -2
- package/skills/j-init/scripts/apply-scaffold-visibility.sh +192 -0
- package/skills/j-init/scripts/init.sh +19 -5
- package/skills/j-playbook/SKILL.md +12 -0
- package/skills/j-playbook-new/SKILL.md +155 -0
- package/skills/j-playbook-new/scripts/playbook-new.sh +332 -0
- package/skills/j-publish/scripts/npm_ci_pipeline.sh +6 -0
- package/skills/j-skillify/assets/init-new/assets/.gitignore_template +1 -2
- package/skills/j-uncharted/SKILL.md +54 -7
- package/skills/j-uncharted/assets/NODE_QUESTION_TEMPLATE.md +69 -0
- package/skills/j-uncharted/scripts/elicitation-state.sh +45 -7
- package/skills/jenga/scripts/load-nl-catalog.js +22 -6
- package/skills/jenga/scripts/load-playbooks.sh +146 -35
- package/templates/SCRUM_BOARD_SCHEMA.md +18 -6
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
NODE QUESTION TEMPLATE — /uncharted Convergence Loop.
|
|
3
|
+
|
|
4
|
+
Added by E40_S06_T03. Consumed by the Convergence Loop's Step 5
|
|
5
|
+
("Confirm/correct", skills/j-uncharted/SKILL.md) whenever risk-weighted
|
|
6
|
+
gating (Step 4) decides a finding needs a confirm prompt under
|
|
7
|
+
`verification_depth: shallow` or `moderate`. Never reached under `strict`
|
|
8
|
+
— Step 4's strict branch converges the node directly, without ever
|
|
9
|
+
reaching Step 5, so this template is simply not invoked in that case.
|
|
10
|
+
|
|
11
|
+
Two fixed variants, selected by node kind:
|
|
12
|
+
|
|
13
|
+
INTERNAL — the node represents the candidate/service itself (the thing
|
|
14
|
+
`/uncharted` is investigating).
|
|
15
|
+
EXTERNAL — the node represents something outside the candidate that the
|
|
16
|
+
traces surfaced: a dependency it calls, or a consumer that calls it.
|
|
17
|
+
|
|
18
|
+
This replaces a fully open-ended "propose understanding, ask the user to
|
|
19
|
+
confirm or correct it" prompt for any node that fits one of these two
|
|
20
|
+
kinds — which, per the Convergence Loop, is every node this flow drafts.
|
|
21
|
+
Wording is fixed and matches the brainstorm's agreed phrasing verbatim
|
|
22
|
+
(project/documentation/examples/uncharted-conversational-elicitation-procedure.md);
|
|
23
|
+
do not paraphrase it when presenting the prompt.
|
|
24
|
+
|
|
25
|
+
Usage: present the drafted node/edge summary first (per Step 3's draft),
|
|
26
|
+
then ask the questions below for the selected variant, then offer the
|
|
27
|
+
same confirm / correct-with-detail / defer-as-unconfirmed / other choice
|
|
28
|
+
Step 5 already documents. This file supplies the fixed *questions*; the
|
|
29
|
+
response mechanics (turn cap, converge call) are unchanged and live in
|
|
30
|
+
SKILL.md, not here.
|
|
31
|
+
-->
|
|
32
|
+
|
|
33
|
+
# Node Question Template
|
|
34
|
+
|
|
35
|
+
## Internal Node
|
|
36
|
+
|
|
37
|
+
Use when the node represents the candidate/service itself — the thing this investigation is about,
|
|
38
|
+
not something external to it.
|
|
39
|
+
|
|
40
|
+
1. What is this?
|
|
41
|
+
2. What does it do?
|
|
42
|
+
3. Who consumes it?
|
|
43
|
+
4. Other (describe below)
|
|
44
|
+
|
|
45
|
+
## External Node
|
|
46
|
+
|
|
47
|
+
Use when the node represents a dependency or consumer the traces surfaced outside the candidate —
|
|
48
|
+
something the service calls, or something that calls the service.
|
|
49
|
+
|
|
50
|
+
1. What is this?
|
|
51
|
+
2. What does it do?
|
|
52
|
+
3. Is the service producing to it, or consuming from it?
|
|
53
|
+
4. Who is the producer, and who is the consumer?
|
|
54
|
+
5. Other (describe below)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
After the selected question set, present the standard confirm/correct choice (per the Interaction
|
|
59
|
+
Pattern in `CLAUDE.md` and Convergence Loop Step 5):
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
1. Confirm the draft as accurate
|
|
63
|
+
2. Correct it — describe what's wrong
|
|
64
|
+
3. Defer — mark this node "unconfirmed" for now
|
|
65
|
+
4. Other (describe below)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Silence, a counter-question, or an ambiguous reply is not consent — re-ask, the same convention used
|
|
69
|
+
at every other confirmation gate in this skill.
|
|
@@ -53,7 +53,23 @@
|
|
|
53
53
|
# "nodes": {
|
|
54
54
|
# "<node-id>": { "turns": <int>, "status": "pending"|"converged"|"flagged", "note": "<text>" }
|
|
55
55
|
# },
|
|
56
|
-
# "checkpoint": {
|
|
56
|
+
# "checkpoint": {
|
|
57
|
+
# ...arbitrary, agent-defined fields, e.g. directory-triage results...
|
|
58
|
+
# "verification_depth": {
|
|
59
|
+
# // Recognized field (E40_S06_T01). Per-candidate Familiarity Check
|
|
60
|
+
# // answer from skills/j-uncharted/SKILL.md's Convergence Loop Step 1,
|
|
61
|
+
# // keyed by candidate/node id so a resumed elicitation can tell which
|
|
62
|
+
# // candidates already answered. Values are "shallow" | "moderate" |
|
|
63
|
+
# // "strict". Written and read by the agent driving /uncharted, same
|
|
64
|
+
# // as every other checkpoint field — this script assigns it no
|
|
65
|
+
# // special handling beyond the dict-merge behavior documented under
|
|
66
|
+
# // `checkpoint` below (which exists so that checking in one
|
|
67
|
+
# // candidate's depth never clobbers another's already recorded
|
|
68
|
+
# // here). Consumed by E40_S06_T02's risk-weighted gating; unread by
|
|
69
|
+
# // anything in this script or E40_S06_T01's own scope.
|
|
70
|
+
# "<candidate-id>": "shallow" | "moderate" | "strict"
|
|
71
|
+
# }
|
|
72
|
+
# }
|
|
57
73
|
# }
|
|
58
74
|
#
|
|
59
75
|
# ---------------------------------------------------------------------------
|
|
@@ -79,11 +95,22 @@
|
|
|
79
95
|
# the node's note (e.g. a one-line summary of what was confirmed).
|
|
80
96
|
#
|
|
81
97
|
# checkpoint --id ID --json FILE
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
98
|
+
# Merge the JSON object in FILE (or stdin when FILE is "-") into the
|
|
99
|
+
# state's top-level "checkpoint" field. New keys are added; existing
|
|
100
|
+
# keys are overwritten by the new value — EXCEPT when both the existing
|
|
101
|
+
# value and the new value for a given key are themselves JSON objects,
|
|
102
|
+
# in which case they are merged one level deep instead of one replacing
|
|
103
|
+
# the other (existing sub-keys are kept, new sub-keys are added,
|
|
104
|
+
# conflicting sub-keys take the new value). This one-level dict merge
|
|
105
|
+
# is what lets a map-shaped field addressed by its own sub-keys — e.g.
|
|
106
|
+
# "verification_depth", keyed per candidate id (E40_S06_T01) — accumulate
|
|
107
|
+
# entries across separate checkpoint calls instead of each call
|
|
108
|
+
# clobbering every entry a previous call wrote. Plain (non-dict)
|
|
109
|
+
# values — strings, numbers, lists, directory-triage's own arrays —
|
|
110
|
+
# still simply overwrite, exactly as before this addition. This is the
|
|
111
|
+
# generic "save progress" primitive — directory-triage results, draft
|
|
112
|
+
# node content, anything else the flow wants durable before it might
|
|
113
|
+
# pause.
|
|
87
114
|
#
|
|
88
115
|
# pause --id ID
|
|
89
116
|
# Set status "paused" and update "updated_at". The caller (the agent
|
|
@@ -422,7 +449,18 @@ elif subcommand == "checkpoint":
|
|
|
422
449
|
state = load()
|
|
423
450
|
payload = json.loads(checkpoint_json)
|
|
424
451
|
cp = state.setdefault("checkpoint", {})
|
|
425
|
-
|
|
452
|
+
# One-level-deep merge when both sides are dicts (E40_S06_T01) — lets a
|
|
453
|
+
# map-shaped field keyed by its own sub-keys (e.g. verification_depth,
|
|
454
|
+
# keyed per candidate id) accumulate entries across separate checkpoint
|
|
455
|
+
# calls instead of each call replacing the whole field. Anything else
|
|
456
|
+
# (strings, numbers, lists, or a dict landing on a non-dict/absent key)
|
|
457
|
+
# keeps the prior plain overwrite behavior.
|
|
458
|
+
for key, value in payload.items():
|
|
459
|
+
existing = cp.get(key)
|
|
460
|
+
if isinstance(existing, dict) and isinstance(value, dict):
|
|
461
|
+
existing.update(value)
|
|
462
|
+
else:
|
|
463
|
+
cp[key] = value
|
|
426
464
|
state["updated_at"] = now
|
|
427
465
|
atomic_write(state)
|
|
428
466
|
result = {"checkpoint_keys": list(payload.keys())}
|
|
@@ -11,10 +11,16 @@
|
|
|
11
11
|
* inventory (`readSkillAllowList()`, which reads the committed `lib/skill-allow-list.json`
|
|
12
12
|
* artifact) — this script does not independently re-scan `skills/` for a name list of its own,
|
|
13
13
|
* per E53_S01_T02's acceptance criteria and the drift lesson E41_S04 already documented for that
|
|
14
|
-
* generator.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* generator. That inventory holds bare identifiers (e.g. "brainstorm"), stripped of the `j.`
|
|
15
|
+
* frontmatter prefix — but per docs/skill-authoring.md's Canonical Naming Contract, the actual
|
|
16
|
+
* on-disk directory is `skills/j-<name>/`, except the three permanent exceptions (`jenga`,
|
|
17
|
+
* `jenga-permission-level`, `index`) which keep their bare directory name. For each name in the
|
|
18
|
+
* inventory, this script re-derives that canonical directory name, reads exactly one file —
|
|
19
|
+
* `skills/<dirName>/SKILL.md` — to populate the remaining catalog fields: `description`,
|
|
20
|
+
* `keywords`, `examples`, and `metadata.prefered_agent`, and emits `dirName` (not the bare
|
|
21
|
+
* identifier) as the catalog entry's `name` — callers like `/jenga`'s Skill invocation and
|
|
22
|
+
* `playbook-new.sh`'s `validate-skill` need the real, invokable directory name. These are the
|
|
23
|
+
* same fields `/route`'s Step 1 ("Discover Available Skills") collects.
|
|
18
24
|
*
|
|
19
25
|
* ---------------------------------------------------------------------------
|
|
20
26
|
* USAGE
|
|
@@ -61,6 +67,15 @@ import { readFileSync, existsSync } from "fs";
|
|
|
61
67
|
import { join } from "path";
|
|
62
68
|
import { pathToFileURL } from "url";
|
|
63
69
|
|
|
70
|
+
// The three permanent exceptions to the `j-<name>` canonical directory convention — see
|
|
71
|
+
// docs/skill-authoring.md's Canonical Naming Contract and scripts/audit-twin-divergence.sh's
|
|
72
|
+
// NEVER_TWINNED list, which this mirrors.
|
|
73
|
+
const NEVER_TWINNED = new Set(["jenga", "jenga-permission-level", "index"]);
|
|
74
|
+
|
|
75
|
+
function canonicalSkillDir(name) {
|
|
76
|
+
return NEVER_TWINNED.has(name) ? name : `j-${name}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
64
79
|
/**
|
|
65
80
|
* Parses YAML frontmatter from a SKILL.md's content into a plain object. This is a hand-rolled,
|
|
66
81
|
* intentionally minimal parser scoped to the small set of shapes SKILL.md frontmatter actually
|
|
@@ -161,7 +176,8 @@ async function main() {
|
|
|
161
176
|
|
|
162
177
|
const catalog = [];
|
|
163
178
|
for (const name of names) {
|
|
164
|
-
const
|
|
179
|
+
const dirName = canonicalSkillDir(name);
|
|
180
|
+
const skillMdPath = join(pkgRoot, "skills", dirName, "SKILL.md");
|
|
165
181
|
if (!existsSync(skillMdPath)) {
|
|
166
182
|
process.stderr.write(
|
|
167
183
|
`Warning: ${skillMdPath} not found for allow-listed skill '${name}' — skipped\n`
|
|
@@ -186,7 +202,7 @@ async function main() {
|
|
|
186
202
|
}
|
|
187
203
|
|
|
188
204
|
catalog.push({
|
|
189
|
-
name,
|
|
205
|
+
name: dirName,
|
|
190
206
|
description: fm.description,
|
|
191
207
|
keywords: Array.isArray(fm.keywords) ? fm.keywords : [],
|
|
192
208
|
examples: Array.isArray(fm.examples) ? fm.examples : [],
|
|
@@ -256,10 +256,30 @@
|
|
|
256
256
|
# ---------------------------------------------------------------------------
|
|
257
257
|
# DATA SOURCE
|
|
258
258
|
# ---------------------------------------------------------------------------
|
|
259
|
-
#
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
-
#
|
|
259
|
+
# TWO directories are scanned and MERGED into one catalog (E53_S09_T01):
|
|
260
|
+
#
|
|
261
|
+
# 1. `skills/jenga/playbooks/` (BUILTIN, framework-owned) — every `*.json` file directly under
|
|
262
|
+
# it, EXCLUDING `schema.json` (which documents the required shape — see that file's own
|
|
263
|
+
# header — but is never itself a playbook entry). See `schema.json` for the authoritative
|
|
264
|
+
# field list; this script's validation below is a runtime mirror of that schema, not a
|
|
265
|
+
# substitute for it.
|
|
266
|
+
#
|
|
267
|
+
# 2. `project/.playbooks/` (PROJECT, project-owned, resolved relative to this script's own
|
|
268
|
+
# `PROJECT_DIR` — see its resolution below, including the `JENGA_PLAYBOOKS_TEST_ROOT`
|
|
269
|
+
# override) — every `*.json` file directly under it, same `schema.json`-exclusion rule.
|
|
270
|
+
# A MISSING `project/.playbooks/` directory is a SILENT NO-OP: no warning, no error — a
|
|
271
|
+
# project with no custom playbooks is the common case, not an exceptional one.
|
|
272
|
+
#
|
|
273
|
+
# The builtin directory is scanned and locally validated FIRST, in full, before the project
|
|
274
|
+
# directory is touched at all. Every project-directory file is then checked for an `id` collision
|
|
275
|
+
# against the already-loaded builtin set: if a project playbook's basename/id matches a
|
|
276
|
+
# SUCCESSFULLY LOADED builtin playbook's id, the project playbook is SKIPPED — a stderr warning
|
|
277
|
+
# names BOTH the project file's path and the builtin file's path — and the builtin entry is the
|
|
278
|
+
# one that survives into the catalog. This is never a silent override in either direction. A
|
|
279
|
+
# project playbook whose id does not collide goes through the exact same local validation,
|
|
280
|
+
# composition resolution, and `forward_from`/`conditional` validation as a builtin one — no
|
|
281
|
+
# separate or weaker path. See "VALIDATION / SKIP CONDITIONS" below for the exact collision rule,
|
|
282
|
+
# and "OUTPUT SCHEMA" for the `source` field every catalog entry gains as a result of this merge.
|
|
263
283
|
#
|
|
264
284
|
# ---------------------------------------------------------------------------
|
|
265
285
|
# USAGE
|
|
@@ -273,8 +293,10 @@
|
|
|
273
293
|
#
|
|
274
294
|
# Additive sibling mode (E53_S06_T02), for direct-by-id lookup (`j.playbook <id>`,
|
|
275
295
|
# `skills/j-playbook/SKILL.md`, E53_S06_T03) — runs the SAME PASS 1-3 pipeline as the no-argument
|
|
276
|
-
# mode above (never a separate implementation),
|
|
277
|
-
# (
|
|
296
|
+
# mode above (never a separate implementation), against the SAME MERGED builtin+project catalog
|
|
297
|
+
# (E53_S09_T01) — an id may resolve from either source, and the returned `playbook` object carries
|
|
298
|
+
# the same `source` field a full-catalog entry would — then emits exactly ONE JSON object to
|
|
299
|
+
# stdout (never the full catalog, never warnings about OTHER playbooks) and exits 0:
|
|
278
300
|
#
|
|
279
301
|
# {"status": "valid", "playbook": {...}} -- <id> resolved to a real file and passed every
|
|
280
302
|
# validation pass; `playbook` has the same field
|
|
@@ -306,6 +328,11 @@
|
|
|
306
328
|
# asserting on candidate sets never targets the repository root. Never set this variable in a real
|
|
307
329
|
# invocation.
|
|
308
330
|
#
|
|
331
|
+
# Because the SAME override also becomes the PROJECT root (see "DATA SOURCE" above), a fixture
|
|
332
|
+
# wanting project-local playbook coverage places its files at
|
|
333
|
+
# `<value>/project/.playbooks/<id>.json` — no second, project-specific test variable is
|
|
334
|
+
# introduced (E53_S09_T01, `tests/load-playbooks-project-source.bats`).
|
|
335
|
+
#
|
|
309
336
|
# ---------------------------------------------------------------------------
|
|
310
337
|
# OUTPUT SCHEMA
|
|
311
338
|
# ---------------------------------------------------------------------------
|
|
@@ -318,11 +345,16 @@
|
|
|
318
345
|
# "description": "...",
|
|
319
346
|
# "keywords": ["..."],
|
|
320
347
|
# "examples": ["..."],
|
|
321
|
-
# "steps": ["j-brainstorm", "j-todo", "j-do", "j-dev-done", "j-mirror-public"]
|
|
348
|
+
# "steps": ["j-brainstorm", "j-todo", "j-do", "j-dev-done", "j-mirror-public"],
|
|
349
|
+
# "source": "builtin"
|
|
322
350
|
# },
|
|
323
351
|
# ...
|
|
324
352
|
# ]
|
|
325
353
|
#
|
|
354
|
+
# `source` (E53_S09_T01) is `"builtin"` for anything loaded from `skills/jenga/playbooks/`, or
|
|
355
|
+
# `"project"` for anything loaded from `project/.playbooks/` — present on EVERY catalog entry,
|
|
356
|
+
# regardless of source, additive to the pre-existing field shape.
|
|
357
|
+
#
|
|
326
358
|
# `steps` entries are emitted exactly as validated/resolved: a depth-1 bare string stays a bare
|
|
327
359
|
# string; a depth-1 StepObject is emitted as an object carrying only its recognized fields (`skill`
|
|
328
360
|
# XOR `playbook` — though by the time this is emitted, a `playbook`-type step has already been
|
|
@@ -347,6 +379,11 @@
|
|
|
347
379
|
# - `id` does not equal the filename's basename without `.json` -> skipped
|
|
348
380
|
# (prevents a playbook's identity from silently drifting from its
|
|
349
381
|
# file location)
|
|
382
|
+
# - A PROJECT playbook's id/basename matches a SUCCESSFULLY LOADED
|
|
383
|
+
# BUILTIN playbook's id (E53_S09_T01) -> project playbook
|
|
384
|
+
# skipped; stderr warning names BOTH the project file's path and the
|
|
385
|
+
# builtin file's path; the builtin entry is the one that survives into
|
|
386
|
+
# the catalog (never a silent override in either direction)
|
|
350
387
|
# - A `steps` entry is neither a string nor an object, or is an empty
|
|
351
388
|
# string -> skipped
|
|
352
389
|
# - A StepObject step carries BOTH `skill` and `playbook`, or NEITHER -> skipped
|
|
@@ -424,18 +461,30 @@ if [ -n "${JENGA_PLAYBOOKS_TEST_ROOT:-}" ]; then
|
|
|
424
461
|
# E53_S05_T01 to also cover playbook-config.json resolution). Never set in a real invocation.
|
|
425
462
|
PKG_ROOT="$JENGA_PLAYBOOKS_TEST_ROOT"
|
|
426
463
|
PROJECT_DIR="$JENGA_PLAYBOOKS_TEST_ROOT"
|
|
427
|
-
# Resolve the jenga-agent PACKAGE root (where the canonical skills/ tree actually lives) — same
|
|
428
|
-
# monorepo-checkout vs. installed-npm-package detection used by
|
|
429
|
-
# skills/jenga/scripts/load-nl-catalog.sh's PKG_ROOT resolution and skills/init/scripts/init.sh.
|
|
430
|
-
elif [ -d "$SCRIPT_DIR/../../../templates" ]; then
|
|
431
|
-
PKG_ROOT="$SCRIPT_DIR/../../.."
|
|
432
|
-
PROJECT_DIR="${JENGA_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || pwd)}}"
|
|
433
|
-
elif [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -d "${CLAUDE_PROJECT_DIR}/node_modules/@jenga-ai/agent/templates" ]; then
|
|
434
|
-
PKG_ROOT="${CLAUDE_PROJECT_DIR}/node_modules/@jenga-ai/agent"
|
|
435
|
-
PROJECT_DIR="${JENGA_PROJECT_DIR:-$CLAUDE_PROJECT_DIR}"
|
|
436
464
|
else
|
|
437
|
-
|
|
438
|
-
|
|
465
|
+
# Resolve JENGA_PROJECT_DIR the same way every other script in skills/jenga/scripts/ does.
|
|
466
|
+
if [ -f "$SCRIPT_DIR/../../../lib/resolve-project-dir.sh" ]; then
|
|
467
|
+
# shellcheck source=/dev/null
|
|
468
|
+
source "$SCRIPT_DIR/../../../lib/resolve-project-dir.sh"
|
|
469
|
+
elif [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
470
|
+
JENGA_PROJECT_DIR="$CLAUDE_PROJECT_DIR"
|
|
471
|
+
else
|
|
472
|
+
JENGA_PROJECT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
473
|
+
fi
|
|
474
|
+
|
|
475
|
+
# Resolve the jenga-agent PACKAGE root (where the canonical skills/ tree actually lives) — same
|
|
476
|
+
# monorepo-checkout vs. installed-npm-package detection used by
|
|
477
|
+
# skills/jenga/scripts/load-nl-catalog.sh's PKG_ROOT resolution and skills/init/scripts/init.sh.
|
|
478
|
+
if [ -d "$SCRIPT_DIR/../../../templates" ]; then
|
|
479
|
+
PKG_ROOT="$SCRIPT_DIR/../../.."
|
|
480
|
+
PROJECT_DIR="$JENGA_PROJECT_DIR"
|
|
481
|
+
elif [ -d "$JENGA_PROJECT_DIR/node_modules/@jenga-ai/agent/templates" ]; then
|
|
482
|
+
PKG_ROOT="$JENGA_PROJECT_DIR/node_modules/@jenga-ai/agent"
|
|
483
|
+
PROJECT_DIR="$JENGA_PROJECT_DIR"
|
|
484
|
+
else
|
|
485
|
+
echo "Error: could not locate the jenga-agent package root (templates/ not found via monorepo checkout or node_modules/@jenga-ai/agent)." >&2
|
|
486
|
+
exit 2
|
|
487
|
+
fi
|
|
439
488
|
fi
|
|
440
489
|
|
|
441
490
|
PLAYBOOKS_DIR="$PKG_ROOT/skills/jenga/playbooks"
|
|
@@ -467,6 +516,12 @@ project_dir = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] else None
|
|
|
467
516
|
mode = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else "catalog"
|
|
468
517
|
lookup_id = sys.argv[5] if len(sys.argv) > 5 else ""
|
|
469
518
|
|
|
519
|
+
# --- E53_S09_T01: project-local playbook source directory ------------------------------------
|
|
520
|
+
# `project/.playbooks/`, resolved relative to the SAME project_dir already threaded above (which
|
|
521
|
+
# already honors JENGA_PLAYBOOKS_TEST_ROOT -- see header "TESTING OVERRIDE"). A missing directory
|
|
522
|
+
# is a silent no-op, not an error -- see header "DATA SOURCE".
|
|
523
|
+
project_playbooks_dir = os.path.join(project_dir, "project", ".playbooks") if project_dir else None
|
|
524
|
+
|
|
470
525
|
REQUIRED_FIELDS = ["id", "name", "description", "keywords", "examples", "steps"]
|
|
471
526
|
LIST_FIELDS = ["keywords", "examples", "steps"]
|
|
472
527
|
|
|
@@ -673,7 +728,7 @@ def extract_output_types(skill_md_path):
|
|
|
673
728
|
|
|
674
729
|
|
|
675
730
|
try:
|
|
676
|
-
|
|
731
|
+
builtin_filenames = sorted(
|
|
677
732
|
f for f in os.listdir(playbooks_dir)
|
|
678
733
|
if f.endswith(".json") and f != "schema.json"
|
|
679
734
|
)
|
|
@@ -681,17 +736,41 @@ except OSError as e:
|
|
|
681
736
|
print(f"Error: could not list {playbooks_dir}: {e}", file=sys.stderr)
|
|
682
737
|
sys.exit(2)
|
|
683
738
|
|
|
739
|
+
# --- E53_S09_T01: project-local playbook directory listing --------------------------------------
|
|
740
|
+
# Non-fatal, unlike the builtin listing above: a missing directory (the common case -- most
|
|
741
|
+
# projects have no custom playbooks) or an OSError while listing it both yield an empty list,
|
|
742
|
+
# never a setup-error exit. See header "DATA SOURCE".
|
|
743
|
+
project_filenames = []
|
|
744
|
+
if project_playbooks_dir and os.path.isdir(project_playbooks_dir):
|
|
745
|
+
try:
|
|
746
|
+
project_filenames = sorted(
|
|
747
|
+
f for f in os.listdir(project_playbooks_dir)
|
|
748
|
+
if f.endswith(".json") and f != "schema.json"
|
|
749
|
+
)
|
|
750
|
+
except OSError as e:
|
|
751
|
+
print(f"Warning: could not list {project_playbooks_dir}: {e}", file=sys.stderr)
|
|
752
|
+
project_filenames = []
|
|
753
|
+
|
|
684
754
|
# --- PASS 1: local (non-composition) validation -------------------------------------------------
|
|
685
755
|
# Builds `raw[pid]` for every playbook that passes purely local validation (parse/shape/id-match/
|
|
686
756
|
# step-shape/skill-existence) -- independent of any OTHER playbook. Composition resolution (PASS
|
|
687
757
|
# 2, below) needs this map fully populated before it can recurse into a referenced playbook.
|
|
758
|
+
#
|
|
759
|
+
# As of E53_S09_T01, `raw`/`order` are populated from BOTH the builtin and project directories
|
|
760
|
+
# (builtin first, in full, then project -- see the id-collision check below). Every playbook run
|
|
761
|
+
# through `process_playbook_file()` carries a `source` tag ("builtin" or "project") in its `raw`
|
|
762
|
+
# entry, which flows through unchanged into the final catalog entry (PASS 3, below).
|
|
688
763
|
raw = {}
|
|
689
|
-
order = [] # preserves the original sorted-filename order
|
|
764
|
+
order = [] # preserves the original builtin-then-project, sorted-within-source filename order
|
|
690
765
|
|
|
691
|
-
for filename in filenames:
|
|
692
|
-
path = os.path.join(playbooks_dir, filename)
|
|
693
|
-
basename = filename[: -len(".json")]
|
|
694
766
|
|
|
767
|
+
def process_playbook_file(path, basename, source):
|
|
768
|
+
"""Runs PASS 1's local validation for one playbook file (identical logic regardless of which
|
|
769
|
+
source directory it came from -- see header 'DATA SOURCE': project playbooks go through the
|
|
770
|
+
exact same pipeline as builtin ones, never a separate or weaker path). On success, populates
|
|
771
|
+
`raw[basename]` (tagged with `source`) and appends to `order`. On failure, prints the same
|
|
772
|
+
stderr warning this script has always printed for that condition and records the reason in
|
|
773
|
+
`skip_reasons[basename]`."""
|
|
695
774
|
try:
|
|
696
775
|
with open(path, encoding="utf-8") as fh:
|
|
697
776
|
data = json.load(fh)
|
|
@@ -699,20 +778,20 @@ for filename in filenames:
|
|
|
699
778
|
reason = f"is not valid JSON ({e})"
|
|
700
779
|
print(f"Warning: {path} {reason} — skipped", file=sys.stderr)
|
|
701
780
|
skip_reasons[basename] = reason
|
|
702
|
-
|
|
781
|
+
return
|
|
703
782
|
|
|
704
783
|
if not isinstance(data, dict):
|
|
705
784
|
reason = "is not a JSON object"
|
|
706
785
|
print(f"Warning: {path} {reason} — skipped", file=sys.stderr)
|
|
707
786
|
skip_reasons[basename] = reason
|
|
708
|
-
|
|
787
|
+
return
|
|
709
788
|
|
|
710
789
|
missing = [f for f in REQUIRED_FIELDS if f not in data]
|
|
711
790
|
if missing:
|
|
712
791
|
reason = f"missing required field(s) {missing}"
|
|
713
792
|
print(f"Warning: {path} {reason} — skipped", file=sys.stderr)
|
|
714
793
|
skip_reasons[basename] = reason
|
|
715
|
-
|
|
794
|
+
return
|
|
716
795
|
|
|
717
796
|
bad_list = [
|
|
718
797
|
f for f in LIST_FIELDS
|
|
@@ -722,7 +801,7 @@ for filename in filenames:
|
|
|
722
801
|
reason = f"field(s) {bad_list} must be non-empty lists"
|
|
723
802
|
print(f"Warning: {path} {reason} — skipped", file=sys.stderr)
|
|
724
803
|
skip_reasons[basename] = reason
|
|
725
|
-
|
|
804
|
+
return
|
|
726
805
|
|
|
727
806
|
if data["id"] != basename:
|
|
728
807
|
reason = (
|
|
@@ -730,7 +809,7 @@ for filename in filenames:
|
|
|
730
809
|
)
|
|
731
810
|
print(f"Warning: {path} {reason} — skipped", file=sys.stderr)
|
|
732
811
|
skip_reasons[basename] = reason
|
|
733
|
-
|
|
812
|
+
return
|
|
734
813
|
|
|
735
814
|
# --- E53_S03_T01: StepObject shape acceptance + bare-string back-compat ---
|
|
736
815
|
normalized_steps = []
|
|
@@ -745,7 +824,7 @@ for filename in filenames:
|
|
|
745
824
|
if step_error:
|
|
746
825
|
print(f"Warning: {path} {step_error} — skipped", file=sys.stderr)
|
|
747
826
|
skip_reasons[basename] = step_error
|
|
748
|
-
|
|
827
|
+
return
|
|
749
828
|
|
|
750
829
|
# Existence check for skill-type steps only (bare string, or StepObject with `skill`).
|
|
751
830
|
# `playbook`-type steps have no single skill name (step_skill_name returns None for them) and
|
|
@@ -766,10 +845,11 @@ for filename in filenames:
|
|
|
766
845
|
)
|
|
767
846
|
print(f"Warning: {path} {reason} — playbook skipped", file=sys.stderr)
|
|
768
847
|
skip_reasons[basename] = reason
|
|
769
|
-
|
|
848
|
+
return
|
|
770
849
|
|
|
771
850
|
raw[basename] = {
|
|
772
851
|
"path": path,
|
|
852
|
+
"source": source,
|
|
773
853
|
"name": data["name"],
|
|
774
854
|
"description": data["description"],
|
|
775
855
|
"keywords": data["keywords"],
|
|
@@ -778,6 +858,36 @@ for filename in filenames:
|
|
|
778
858
|
}
|
|
779
859
|
order.append(basename)
|
|
780
860
|
|
|
861
|
+
|
|
862
|
+
for filename in builtin_filenames:
|
|
863
|
+
path = os.path.join(playbooks_dir, filename)
|
|
864
|
+
basename = filename[: -len(".json")]
|
|
865
|
+
process_playbook_file(path, basename, "builtin")
|
|
866
|
+
|
|
867
|
+
# --- E53_S09_T01: project playbooks, id-collision check against the already-loaded builtin set --
|
|
868
|
+
# Runs AFTER the builtin loop above has fully populated `raw`, so a collision check here is
|
|
869
|
+
# checking against every SUCCESSFULLY LOADED builtin playbook -- never a builtin file that itself
|
|
870
|
+
# failed validation (that basename never made it into `raw`, so a project playbook may legitimately
|
|
871
|
+
# claim that id instead). See header 'VALIDATION / SKIP CONDITIONS'.
|
|
872
|
+
for filename in project_filenames:
|
|
873
|
+
path = os.path.join(project_playbooks_dir, filename)
|
|
874
|
+
basename = filename[: -len(".json")]
|
|
875
|
+
|
|
876
|
+
if basename in raw:
|
|
877
|
+
builtin_path = raw[basename]["path"]
|
|
878
|
+
reason = (
|
|
879
|
+
f"id '{basename}' collides with a built-in playbook already loaded from "
|
|
880
|
+
f"{builtin_path} — the built-in entry is retained, never silently overridden"
|
|
881
|
+
)
|
|
882
|
+
print(
|
|
883
|
+
f"Warning: {path} {reason} (project file skipped)",
|
|
884
|
+
file=sys.stderr,
|
|
885
|
+
)
|
|
886
|
+
skip_reasons[basename] = reason
|
|
887
|
+
continue
|
|
888
|
+
|
|
889
|
+
process_playbook_file(path, basename, "project")
|
|
890
|
+
|
|
781
891
|
# --- PASS 2: composition resolution (E53_S05_T01) ------------------------------------------------
|
|
782
892
|
# Recursively resolves `{"playbook": "<id>"}` steps into a flat, fully-spliced step list. See
|
|
783
893
|
# header 'COMPOSITION RESOLUTION' for the full algorithm description; this is that algorithm.
|
|
@@ -987,6 +1097,7 @@ for pid in order:
|
|
|
987
1097
|
"keywords": entry["keywords"],
|
|
988
1098
|
"examples": entry["examples"],
|
|
989
1099
|
"steps": flattened_steps,
|
|
1100
|
+
"source": entry["source"],
|
|
990
1101
|
})
|
|
991
1102
|
|
|
992
1103
|
# --- E53_S06_T02: `lookup <id>` mode output ------------------------------------------------------
|
|
@@ -1001,11 +1112,11 @@ if mode == "lookup":
|
|
|
1001
1112
|
if result is None:
|
|
1002
1113
|
if lookup_id in skip_reasons:
|
|
1003
1114
|
result = {"status": "invalid", "reason": skip_reasons[lookup_id]}
|
|
1004
|
-
elif f"{lookup_id}.json" in
|
|
1005
|
-
# Defensive fallback -- should not normally happen, since every basename scanned
|
|
1006
|
-
#
|
|
1007
|
-
# this point. Guards against ever silently reporting
|
|
1008
|
-
# does exist on disk.
|
|
1115
|
+
elif f"{lookup_id}.json" in builtin_filenames or f"{lookup_id}.json" in project_filenames:
|
|
1116
|
+
# Defensive fallback -- should not normally happen, since every basename scanned from
|
|
1117
|
+
# EITHER source directory (E53_S09_T01) either lands in `catalog` (valid) or
|
|
1118
|
+
# `skip_reasons` (invalid) by this point. Guards against ever silently reporting
|
|
1119
|
+
# `not_found` for a file that does exist on disk.
|
|
1009
1120
|
result = {
|
|
1010
1121
|
"status": "invalid",
|
|
1011
1122
|
"reason": "failed validation (no specific reason captured)",
|
|
@@ -105,7 +105,9 @@ date_started:
|
|
|
105
105
|
date_completed:
|
|
106
106
|
dates_previously_completed: # comma-separated list, e.g. 2026-01-15, 2026-03-22
|
|
107
107
|
reopened_on: # comma-separated list, e.g. 2026-02-01, 2026-04-10
|
|
108
|
-
reopened_reason: #
|
|
108
|
+
reopened_reason: # YAML list, one entry per reopen cycle; see Reopen Tracking Fields
|
|
109
|
+
- "Scope expanded"
|
|
110
|
+
- "Bug found post-release"
|
|
109
111
|
docs: [] # optional list of repo-relative documentation paths, e.g. ["README.md", "docs/API.md"]
|
|
110
112
|
epic_scope_approval: false # set to true by the human operator only when any task in this epic has execution_scope: epic
|
|
111
113
|
provenance: # optional; only valid value is `backfilled` (epic reverse-engineered from pre-existing code by `/uncharted onboard`). Omit for normally-authored epics.
|
|
@@ -139,7 +141,9 @@ date_started:
|
|
|
139
141
|
date_completed:
|
|
140
142
|
dates_previously_completed: # comma-separated list, e.g. 2026-01-15, 2026-03-22
|
|
141
143
|
reopened_on: # comma-separated list, e.g. 2026-02-01, 2026-04-10
|
|
142
|
-
reopened_reason: #
|
|
144
|
+
reopened_reason: # YAML list, one entry per reopen cycle; see Reopen Tracking Fields
|
|
145
|
+
- "Scope expanded"
|
|
146
|
+
- "Bug found post-release"
|
|
143
147
|
docs: [] # optional list of repo-relative documentation paths, e.g. ["README.md", "docs/API.md"]
|
|
144
148
|
crucial_level: # optional; advisory | gated | locked; absence means no elevated caution
|
|
145
149
|
crucial_set_by: # required when crucial_level is set; user | scrum-master | <agent>-escalation
|
|
@@ -177,7 +181,9 @@ date_started:
|
|
|
177
181
|
date_completed:
|
|
178
182
|
dates_previously_completed: # comma-separated list, e.g. 2026-01-15, 2026-03-22
|
|
179
183
|
reopened_on: # comma-separated list, e.g. 2026-02-01, 2026-04-10
|
|
180
|
-
reopened_reason: #
|
|
184
|
+
reopened_reason: # YAML list, one entry per reopen cycle; see Reopen Tracking Fields
|
|
185
|
+
- "Scope expanded"
|
|
186
|
+
- "Bug found post-release"
|
|
181
187
|
assigned_to: developer | tester | scrum-master
|
|
182
188
|
docs: [] # optional list of repo-relative documentation paths, e.g. ["README.md", "docs/API.md"]
|
|
183
189
|
execution_scope: task # task | story | epic | inline | light; omit for legacy tasks (defaults to task)
|
|
@@ -209,7 +215,7 @@ crucial_declined_note: # required when crucial_declined: true; free-te
|
|
|
209
215
|
|
|
210
216
|
### Runtime-written task fields
|
|
211
217
|
|
|
212
|
-
These
|
|
218
|
+
These five fields are **not authored by hand**. They are appended to a task's frontmatter after execution and are absent from any task that has not yet run. They are listed here so that tooling — in particular `scripts/validate-board.sh` — recognises them as valid rather than unknown.
|
|
213
219
|
|
|
214
220
|
| Field | Written by | Meaning |
|
|
215
221
|
|---|---|---|
|
|
@@ -217,8 +223,9 @@ These four fields are **not authored by hand**. They are appended to a task's fr
|
|
|
217
223
|
| `actual_lines_delta` | `/close-story` | Net line delta for the task, from the same extraction |
|
|
218
224
|
| `scope_divergence_flag` | `/close-story` | Set when actual diff stats exceed the thresholds that justified the assigned `execution_scope` |
|
|
219
225
|
| `divergence_flag` | `/do` | Set to `true` by the intent-vs-diff check when a `needs_docs: false` task touched unregistered files |
|
|
226
|
+
| `date_deployed_prod` | `scripts/mark-deployed.sh` | ISO 8601 date (e.g. `2026-09-12`) written in the same locked write window a ticket is set to `status: Deployed to Prod`. Never written on a `Deployed to Stage`-only write. |
|
|
220
227
|
|
|
221
|
-
All
|
|
228
|
+
All five are advisory and non-blocking — they record evidence for later review and never change a task's Passed/Failed outcome. `date_deployed_prod` is allow-listed in `scripts/validate-board.sh`'s `ALLOWED_KEYS` for `epic`, `story`, and `task` (a new `DEPLOY_KEYS` group), even though in practice only tasks (and occasionally stories) are expected to ever carry it — `mark-deployed.sh` only ever writes it to a task/story board file, never to an epic.
|
|
222
229
|
|
|
223
230
|
> `task_changed_files` is **not** a frontmatter field despite the similar name. It lives in the bundle manifest at `project/queue/bundle-<E##_S##>.json`, keyed by task ID.
|
|
224
231
|
|
|
@@ -228,7 +235,12 @@ All four are advisory and non-blocking — they record evidence for later review
|
|
|
228
235
|
|
|
229
236
|
## Reopen Tracking Fields
|
|
230
237
|
|
|
231
|
-
**`dates_previously_completed`, `reopened_on`, `reopened_reason`** — These fields are **only populated when a previously completed item is being reopened and modified**. Leave them blank on first-run items.
|
|
238
|
+
**`dates_previously_completed`, `reopened_on`, `reopened_reason`** — These fields are **only populated when a previously completed item is being reopened and modified**. Leave them blank on first-run items. All three support multiple reopen cycles, but they are **not written the same way**:
|
|
239
|
+
|
|
240
|
+
- `dates_previously_completed` and `reopened_on` hold a plain comma-separated string, e.g. `reopened_on: 2026-02-01, 2026-04-10`. Bare dates need no quoting, so this parses fine.
|
|
241
|
+
- `reopened_reason` **must be a YAML list** (a block sequence, one `- "reason"` per line). It cannot be comma-separated, because reopen reasons are free text that routinely contains colons and commas and therefore has to be quoted — and a run of comma-separated quoted strings (`reopened_reason: "A", "B"`) is not valid YAML. This schema previously documented exactly that invalid form, and every board file that followed it became unparseable: the board parser skipped those files silently, so the board under-reported itself with no error surfaced anywhere. 14 files were repaired on 2026-09-13; do not reintroduce the comma-separated form here.
|
|
242
|
+
|
|
243
|
+
Note the resulting asymmetry is deliberate and load-bearing, not an oversight: the first two fields stay strings because migrating them would churn every existing file for no parsing benefit.
|
|
232
244
|
|
|
233
245
|
## Planning Fields
|
|
234
246
|
|