@jenga-ai/agent 3.2.0 → 3.4.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-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/jenga/scripts/load-nl-catalog.js +22 -6
- package/skills/jenga/scripts/load-playbooks.sh +123 -24
- package/templates/SCRUM_BOARD_SCHEMA.md +18 -6
|
@@ -191,6 +191,9 @@ jobs:
|
|
|
191
191
|
- name: Install dependencies
|
|
192
192
|
run: npm ci
|
|
193
193
|
|
|
194
|
+
- name: Install project/app workspace dependencies (E29_S05_T01)
|
|
195
|
+
run: npm ci --prefix project/app
|
|
196
|
+
|
|
194
197
|
- name: Regenerate lib/legacy-shipped-paths.json (E26_S08_T03)
|
|
195
198
|
run: node scripts/generate-legacy-shipped-paths.js || echo "::warning::legacy-shipped-paths generation failed; publishing without an updated list"
|
|
196
199
|
|
|
@@ -224,6 +227,9 @@ jobs:
|
|
|
224
227
|
- name: Install dependencies
|
|
225
228
|
run: npm ci
|
|
226
229
|
|
|
230
|
+
- name: Install project/app workspace dependencies (E29_S05_T01)
|
|
231
|
+
run: npm ci --prefix project/app
|
|
232
|
+
|
|
227
233
|
- name: Stage to npm
|
|
228
234
|
run: npm stage publish --provenance --access ${NPM_ACCESS} --tag ${DIST_TAG}
|
|
229
235
|
|
|
@@ -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
|
|
@@ -467,6 +504,12 @@ project_dir = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] else None
|
|
|
467
504
|
mode = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else "catalog"
|
|
468
505
|
lookup_id = sys.argv[5] if len(sys.argv) > 5 else ""
|
|
469
506
|
|
|
507
|
+
# --- E53_S09_T01: project-local playbook source directory ------------------------------------
|
|
508
|
+
# `project/.playbooks/`, resolved relative to the SAME project_dir already threaded above (which
|
|
509
|
+
# already honors JENGA_PLAYBOOKS_TEST_ROOT -- see header "TESTING OVERRIDE"). A missing directory
|
|
510
|
+
# is a silent no-op, not an error -- see header "DATA SOURCE".
|
|
511
|
+
project_playbooks_dir = os.path.join(project_dir, "project", ".playbooks") if project_dir else None
|
|
512
|
+
|
|
470
513
|
REQUIRED_FIELDS = ["id", "name", "description", "keywords", "examples", "steps"]
|
|
471
514
|
LIST_FIELDS = ["keywords", "examples", "steps"]
|
|
472
515
|
|
|
@@ -673,7 +716,7 @@ def extract_output_types(skill_md_path):
|
|
|
673
716
|
|
|
674
717
|
|
|
675
718
|
try:
|
|
676
|
-
|
|
719
|
+
builtin_filenames = sorted(
|
|
677
720
|
f for f in os.listdir(playbooks_dir)
|
|
678
721
|
if f.endswith(".json") and f != "schema.json"
|
|
679
722
|
)
|
|
@@ -681,17 +724,41 @@ except OSError as e:
|
|
|
681
724
|
print(f"Error: could not list {playbooks_dir}: {e}", file=sys.stderr)
|
|
682
725
|
sys.exit(2)
|
|
683
726
|
|
|
727
|
+
# --- E53_S09_T01: project-local playbook directory listing --------------------------------------
|
|
728
|
+
# Non-fatal, unlike the builtin listing above: a missing directory (the common case -- most
|
|
729
|
+
# projects have no custom playbooks) or an OSError while listing it both yield an empty list,
|
|
730
|
+
# never a setup-error exit. See header "DATA SOURCE".
|
|
731
|
+
project_filenames = []
|
|
732
|
+
if project_playbooks_dir and os.path.isdir(project_playbooks_dir):
|
|
733
|
+
try:
|
|
734
|
+
project_filenames = sorted(
|
|
735
|
+
f for f in os.listdir(project_playbooks_dir)
|
|
736
|
+
if f.endswith(".json") and f != "schema.json"
|
|
737
|
+
)
|
|
738
|
+
except OSError as e:
|
|
739
|
+
print(f"Warning: could not list {project_playbooks_dir}: {e}", file=sys.stderr)
|
|
740
|
+
project_filenames = []
|
|
741
|
+
|
|
684
742
|
# --- PASS 1: local (non-composition) validation -------------------------------------------------
|
|
685
743
|
# Builds `raw[pid]` for every playbook that passes purely local validation (parse/shape/id-match/
|
|
686
744
|
# step-shape/skill-existence) -- independent of any OTHER playbook. Composition resolution (PASS
|
|
687
745
|
# 2, below) needs this map fully populated before it can recurse into a referenced playbook.
|
|
746
|
+
#
|
|
747
|
+
# As of E53_S09_T01, `raw`/`order` are populated from BOTH the builtin and project directories
|
|
748
|
+
# (builtin first, in full, then project -- see the id-collision check below). Every playbook run
|
|
749
|
+
# through `process_playbook_file()` carries a `source` tag ("builtin" or "project") in its `raw`
|
|
750
|
+
# entry, which flows through unchanged into the final catalog entry (PASS 3, below).
|
|
688
751
|
raw = {}
|
|
689
|
-
order = [] # preserves the original sorted-filename order
|
|
752
|
+
order = [] # preserves the original builtin-then-project, sorted-within-source filename order
|
|
690
753
|
|
|
691
|
-
for filename in filenames:
|
|
692
|
-
path = os.path.join(playbooks_dir, filename)
|
|
693
|
-
basename = filename[: -len(".json")]
|
|
694
754
|
|
|
755
|
+
def process_playbook_file(path, basename, source):
|
|
756
|
+
"""Runs PASS 1's local validation for one playbook file (identical logic regardless of which
|
|
757
|
+
source directory it came from -- see header 'DATA SOURCE': project playbooks go through the
|
|
758
|
+
exact same pipeline as builtin ones, never a separate or weaker path). On success, populates
|
|
759
|
+
`raw[basename]` (tagged with `source`) and appends to `order`. On failure, prints the same
|
|
760
|
+
stderr warning this script has always printed for that condition and records the reason in
|
|
761
|
+
`skip_reasons[basename]`."""
|
|
695
762
|
try:
|
|
696
763
|
with open(path, encoding="utf-8") as fh:
|
|
697
764
|
data = json.load(fh)
|
|
@@ -699,20 +766,20 @@ for filename in filenames:
|
|
|
699
766
|
reason = f"is not valid JSON ({e})"
|
|
700
767
|
print(f"Warning: {path} {reason} — skipped", file=sys.stderr)
|
|
701
768
|
skip_reasons[basename] = reason
|
|
702
|
-
|
|
769
|
+
return
|
|
703
770
|
|
|
704
771
|
if not isinstance(data, dict):
|
|
705
772
|
reason = "is not a JSON object"
|
|
706
773
|
print(f"Warning: {path} {reason} — skipped", file=sys.stderr)
|
|
707
774
|
skip_reasons[basename] = reason
|
|
708
|
-
|
|
775
|
+
return
|
|
709
776
|
|
|
710
777
|
missing = [f for f in REQUIRED_FIELDS if f not in data]
|
|
711
778
|
if missing:
|
|
712
779
|
reason = f"missing required field(s) {missing}"
|
|
713
780
|
print(f"Warning: {path} {reason} — skipped", file=sys.stderr)
|
|
714
781
|
skip_reasons[basename] = reason
|
|
715
|
-
|
|
782
|
+
return
|
|
716
783
|
|
|
717
784
|
bad_list = [
|
|
718
785
|
f for f in LIST_FIELDS
|
|
@@ -722,7 +789,7 @@ for filename in filenames:
|
|
|
722
789
|
reason = f"field(s) {bad_list} must be non-empty lists"
|
|
723
790
|
print(f"Warning: {path} {reason} — skipped", file=sys.stderr)
|
|
724
791
|
skip_reasons[basename] = reason
|
|
725
|
-
|
|
792
|
+
return
|
|
726
793
|
|
|
727
794
|
if data["id"] != basename:
|
|
728
795
|
reason = (
|
|
@@ -730,7 +797,7 @@ for filename in filenames:
|
|
|
730
797
|
)
|
|
731
798
|
print(f"Warning: {path} {reason} — skipped", file=sys.stderr)
|
|
732
799
|
skip_reasons[basename] = reason
|
|
733
|
-
|
|
800
|
+
return
|
|
734
801
|
|
|
735
802
|
# --- E53_S03_T01: StepObject shape acceptance + bare-string back-compat ---
|
|
736
803
|
normalized_steps = []
|
|
@@ -745,7 +812,7 @@ for filename in filenames:
|
|
|
745
812
|
if step_error:
|
|
746
813
|
print(f"Warning: {path} {step_error} — skipped", file=sys.stderr)
|
|
747
814
|
skip_reasons[basename] = step_error
|
|
748
|
-
|
|
815
|
+
return
|
|
749
816
|
|
|
750
817
|
# Existence check for skill-type steps only (bare string, or StepObject with `skill`).
|
|
751
818
|
# `playbook`-type steps have no single skill name (step_skill_name returns None for them) and
|
|
@@ -766,10 +833,11 @@ for filename in filenames:
|
|
|
766
833
|
)
|
|
767
834
|
print(f"Warning: {path} {reason} — playbook skipped", file=sys.stderr)
|
|
768
835
|
skip_reasons[basename] = reason
|
|
769
|
-
|
|
836
|
+
return
|
|
770
837
|
|
|
771
838
|
raw[basename] = {
|
|
772
839
|
"path": path,
|
|
840
|
+
"source": source,
|
|
773
841
|
"name": data["name"],
|
|
774
842
|
"description": data["description"],
|
|
775
843
|
"keywords": data["keywords"],
|
|
@@ -778,6 +846,36 @@ for filename in filenames:
|
|
|
778
846
|
}
|
|
779
847
|
order.append(basename)
|
|
780
848
|
|
|
849
|
+
|
|
850
|
+
for filename in builtin_filenames:
|
|
851
|
+
path = os.path.join(playbooks_dir, filename)
|
|
852
|
+
basename = filename[: -len(".json")]
|
|
853
|
+
process_playbook_file(path, basename, "builtin")
|
|
854
|
+
|
|
855
|
+
# --- E53_S09_T01: project playbooks, id-collision check against the already-loaded builtin set --
|
|
856
|
+
# Runs AFTER the builtin loop above has fully populated `raw`, so a collision check here is
|
|
857
|
+
# checking against every SUCCESSFULLY LOADED builtin playbook -- never a builtin file that itself
|
|
858
|
+
# failed validation (that basename never made it into `raw`, so a project playbook may legitimately
|
|
859
|
+
# claim that id instead). See header 'VALIDATION / SKIP CONDITIONS'.
|
|
860
|
+
for filename in project_filenames:
|
|
861
|
+
path = os.path.join(project_playbooks_dir, filename)
|
|
862
|
+
basename = filename[: -len(".json")]
|
|
863
|
+
|
|
864
|
+
if basename in raw:
|
|
865
|
+
builtin_path = raw[basename]["path"]
|
|
866
|
+
reason = (
|
|
867
|
+
f"id '{basename}' collides with a built-in playbook already loaded from "
|
|
868
|
+
f"{builtin_path} — the built-in entry is retained, never silently overridden"
|
|
869
|
+
)
|
|
870
|
+
print(
|
|
871
|
+
f"Warning: {path} {reason} (project file skipped)",
|
|
872
|
+
file=sys.stderr,
|
|
873
|
+
)
|
|
874
|
+
skip_reasons[basename] = reason
|
|
875
|
+
continue
|
|
876
|
+
|
|
877
|
+
process_playbook_file(path, basename, "project")
|
|
878
|
+
|
|
781
879
|
# --- PASS 2: composition resolution (E53_S05_T01) ------------------------------------------------
|
|
782
880
|
# Recursively resolves `{"playbook": "<id>"}` steps into a flat, fully-spliced step list. See
|
|
783
881
|
# header 'COMPOSITION RESOLUTION' for the full algorithm description; this is that algorithm.
|
|
@@ -987,6 +1085,7 @@ for pid in order:
|
|
|
987
1085
|
"keywords": entry["keywords"],
|
|
988
1086
|
"examples": entry["examples"],
|
|
989
1087
|
"steps": flattened_steps,
|
|
1088
|
+
"source": entry["source"],
|
|
990
1089
|
})
|
|
991
1090
|
|
|
992
1091
|
# --- E53_S06_T02: `lookup <id>` mode output ------------------------------------------------------
|
|
@@ -1001,11 +1100,11 @@ if mode == "lookup":
|
|
|
1001
1100
|
if result is None:
|
|
1002
1101
|
if lookup_id in skip_reasons:
|
|
1003
1102
|
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.
|
|
1103
|
+
elif f"{lookup_id}.json" in builtin_filenames or f"{lookup_id}.json" in project_filenames:
|
|
1104
|
+
# Defensive fallback -- should not normally happen, since every basename scanned from
|
|
1105
|
+
# EITHER source directory (E53_S09_T01) either lands in `catalog` (valid) or
|
|
1106
|
+
# `skip_reasons` (invalid) by this point. Guards against ever silently reporting
|
|
1107
|
+
# `not_found` for a file that does exist on disk.
|
|
1009
1108
|
result = {
|
|
1010
1109
|
"status": "invalid",
|
|
1011
1110
|
"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
|
|