@jenga-ai/agent 1.2.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +97 -91
  2. package/agents/developer.md +26 -7
  3. package/agents/scrum-master.md +57 -22
  4. package/agents/tester.md +68 -4
  5. package/hooks/on_session_end.sh +40 -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 +35 -20
  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 +12 -2
  23. package/skills/continue/SKILL.md +1 -1
  24. package/skills/deep-dive/SKILL.md +1 -1
  25. package/skills/dev-done/SKILL.md +46 -0
  26. package/skills/dev-done/scripts/classify-commit-outcome.sh +114 -0
  27. package/skills/distribute/SKILL.md +1 -1
  28. package/skills/do/SKILL.md +100 -10
  29. package/skills/doc/README.md +155 -0
  30. package/skills/doc/SKILL.md +43 -13
  31. package/skills/doc/authoring-notes.md +72 -0
  32. package/skills/doc/scripts/resolve_last_update.py +149 -0
  33. package/skills/doc-sync/SKILL.md +1 -1
  34. package/skills/dooo/SKILL.md +1 -1
  35. package/skills/error/SKILL.md +1 -1
  36. package/skills/evaluate/SKILL.md +1 -1
  37. package/skills/examplify/SKILL.md +1 -1
  38. package/skills/help/SKILL.md +1 -1
  39. package/skills/idea/SKILL.md +1 -1
  40. package/skills/improve/SKILL.md +1 -1
  41. package/skills/init/SKILL.md +8 -7
  42. package/skills/init/assets/scope-thresholds_template.json +7 -0
  43. package/skills/init/scripts/init.sh +6 -0
  44. package/skills/j-init/SKILL.md +168 -0
  45. package/skills/j-init/assets/.gitignore_template +15 -0
  46. package/skills/j-init/assets/PROJECT_SUMMARY_template.md +13 -0
  47. package/skills/j-init/assets/directory_structure.txt +14 -0
  48. package/skills/j-init/assets/scope-thresholds_template.json +7 -0
  49. package/skills/j-init/assets/strategy_stub_template.md +38 -0
  50. package/skills/j-init/assets/test-config_template.json +4 -0
  51. package/skills/j-init/assets/workflow_template.json +30 -0
  52. package/skills/j-init/scripts/apply-project-visibility.sh +176 -0
  53. package/skills/j-init/scripts/detect-existing-codebase.sh +166 -0
  54. package/skills/j-init/scripts/init.sh +116 -0
  55. package/skills/jbp/SKILL.md +1 -1
  56. package/skills/jenga/SKILL.md +1 -1
  57. package/skills/jenga/scripts/render-confirmation.sh +55 -18
  58. package/skills/jenga-permission-level/SKILL.md +1 -1
  59. package/skills/lgtm/SKILL.md +1 -1
  60. package/skills/pi-plan/SKILL.md +1 -1
  61. package/skills/proceed/SKILL.md +1 -1
  62. package/skills/publish/SKILL.md +67 -1
  63. package/skills/publish/adapters/npm-ci.md +60 -4
  64. package/skills/publish/adapters/npm.md +18 -0
  65. package/skills/publish/assets/ci-contract.md +27 -0
  66. package/skills/publish/assets/publish.example.json +27 -0
  67. package/skills/publish/schemas/publish.schema.json +20 -0
  68. package/skills/publish/scripts/npm_ci_pipeline.sh +50 -1
  69. package/skills/publish/scripts/npm_stage_inspect.sh +829 -0
  70. package/skills/publish/scripts/npm_stage_pipeline.sh +427 -0
  71. package/skills/publish/scripts/publish_common.sh +16 -0
  72. package/skills/publish/scripts/show_history.sh +12 -5
  73. package/skills/publish/scripts/validate_npm_stage_env.sh +184 -0
  74. package/skills/publish/scripts/write_ledger_entry.sh +92 -2
  75. package/skills/reconcile/SKILL.md +122 -12
  76. package/skills/reconcile/assets/report_format.md +17 -0
  77. package/skills/reconcile/scripts/resolve-reconcile-scope.sh +489 -0
  78. package/skills/reconcile-origin/SKILL.md +1 -1
  79. package/skills/redo/SKILL.md +1 -1
  80. package/skills/skillify/SKILL.md +1 -1
  81. package/skills/spinoff/SKILL.md +1 -1
  82. package/skills/status/SKILL.md +1 -1
  83. package/skills/todo/SKILL.md +40 -3
  84. package/skills/todo/scripts/add_trivial_task.sh +216 -0
  85. package/skills/todo/scripts/update_story_tasks.py +87 -0
  86. package/skills/uncharted/SKILL.md +201 -22
  87. package/skills/uncharted/scripts/directory-triage.sh +342 -0
  88. package/skills/uncharted/scripts/elicitation-state.sh +457 -0
  89. package/skills/wtf/SKILL.md +1 -1
  90. package/templates/SCRUM_BOARD_SCHEMA.md +90 -2
  91. package/templates/agent-context.md.tpl +47 -12
  92. package/templates/copilot-instructions.md.tpl +36 -9
  93. package/mcp/router/README.md +0 -19
  94. package/mcp/router/embedder.js +0 -23
  95. package/mcp/router/index.js +0 -204
  96. package/mcp/router/matcher.js +0 -87
  97. package/mcp/router/package-lock.json +0 -1048
  98. package/mcp/router/package.json +0 -11
  99. package/mcp/router/skill-index.js +0 -104
  100. package/skills/route/SKILL.md +0 -180
@@ -0,0 +1,457 @@
1
+ #!/usr/bin/env bash
2
+ # ---------------------------------------------------------------------------
3
+ # skills/uncharted/scripts/elicitation-state.sh
4
+ #
5
+ # Deterministic persistence + turn-cap counting for `/uncharted`'s
6
+ # conversational convergence loop (E20_S08_T03) — the "propose understanding,
7
+ # confirm/correct" cycle that runs during `onboard`'s default (non-`--legacy`)
8
+ # flow and `segment --mode investigate`.
9
+ #
10
+ # Two problems this script exists to solve mechanically rather than leave to
11
+ # agent memory across a long, possibly multi-session conversation:
12
+ #
13
+ # 1. HARD TURN CAP (solution-assessment-uncharted-interactive-elicitation.md,
14
+ # Problem 10). A convergence loop with no termination bound can run
15
+ # forever on a genuinely hard case. `turn` increments a per-node
16
+ # counter and exits 3 — not 0 — the instant the cap is reached, so the
17
+ # caller gets an unmissable, mechanical stop signal instead of having to
18
+ # remember to compare numbers itself.
19
+ #
20
+ # 2. MULTI-SESSION PERSISTENCE (same document, Problem 11). A whole-
21
+ # codebase onboard conversation can span many sessions. `init` /
22
+ # `checkpoint` / `pause` / `complete` / `list-paused` give the
23
+ # conversation a durable, resumable state file, checkpointed after each
24
+ # converged node (per that problem's RECOMMENDED solution) rather than
25
+ # only at the very end.
26
+ #
27
+ # This script performs NO judgement — it does not decide what to ask, what
28
+ # counts as high-impact, or when understanding has actually converged. It
29
+ # only counts turns, tracks status, and persists whatever the agent asks it
30
+ # to persist. All of that judgement lives in skills/uncharted/SKILL.md's
31
+ # Convergence Loop subsection.
32
+ #
33
+ # ---------------------------------------------------------------------------
34
+ # STATE FILE
35
+ # ---------------------------------------------------------------------------
36
+ # project/queue/elicitation-state/<id>.json — one file per elicitation run
37
+ # (a whole `onboard` pass, or a single `segment --mode investigate` target).
38
+ # This directory is git-ignored (mirrors project/queue/handoffs/ — see
39
+ # templates/SCRUM_BOARD_SCHEMA.md): it is session-scratch resumability data,
40
+ # never a durable artifact. The durable output of a converged elicitation is
41
+ # the graph write (project/knowledge-graph/graph.json, per the stub schema at
42
+ # project/knowledge-graph/STUB_SCHEMA.md) and the `[ARCH]`-tagged board item —
43
+ # both written by the agent driving the flow, never by this script.
44
+ #
45
+ # Shape:
46
+ # {
47
+ # "id": "<id>",
48
+ # "target": "<free text, e.g. the investigated path or description>",
49
+ # "cap": <int>, // hard turn cap, default 5
50
+ # "status": "in_progress" | "paused" | "complete",
51
+ # "created_at": "<ISO 8601 UTC>",
52
+ # "updated_at": "<ISO 8601 UTC>",
53
+ # "nodes": {
54
+ # "<node-id>": { "turns": <int>, "status": "pending"|"converged"|"flagged", "note": "<text>" }
55
+ # },
56
+ # "checkpoint": { ...arbitrary, agent-defined fields, e.g. directory-triage results... }
57
+ # }
58
+ #
59
+ # ---------------------------------------------------------------------------
60
+ # SUBCOMMANDS
61
+ # ---------------------------------------------------------------------------
62
+ # init --id ID [--target TEXT] [--cap N]
63
+ # Create the state file if it does not already exist (default cap 5,
64
+ # per the solution assessment's "3-5 rounds" recommendation). Idempotent
65
+ # — calling init again on an existing id returns the existing state
66
+ # unchanged rather than resetting it, so a resumed session can call
67
+ # init unconditionally without wiping progress.
68
+ #
69
+ # turn --id ID --node NODE
70
+ # Increment NODE's turn counter by one. Prints
71
+ # {"turns": N, "cap": C, "cap_reached": true|false}. EXITS 3 (not 0)
72
+ # when the increment reaches the cap — the node's status is also set to
73
+ # "flagged" in the state file at that point, so the cap event is
74
+ # durable, not just a transient exit code the caller might not act on.
75
+ #
76
+ # converge --id ID --node NODE [--note TEXT]
77
+ # Mark NODE's status "converged", independent of whether the cap was
78
+ # ever hit (a node can converge on turn 1). Optional TEXT is stored as
79
+ # the node's note (e.g. a one-line summary of what was confirmed).
80
+ #
81
+ # checkpoint --id ID --json FILE
82
+ # Shallow-merge the JSON object in FILE (or stdin when FILE is "-")
83
+ # into the state's top-level "checkpoint" field. New keys are added;
84
+ # existing keys are overwritten by the new value. This is the generic
85
+ # "save progress" primitive — directory-triage results, draft node
86
+ # content, anything else the flow wants durable before it might pause.
87
+ #
88
+ # pause --id ID
89
+ # Set status "paused" and update "updated_at". The caller (the agent
90
+ # driving `/uncharted`) is responsible for also writing a scrum-master
91
+ # SessionEnd handoff with status "elicitation_paused" and this state
92
+ # file's path, per skills/uncharted/SKILL.md's Multi-Session
93
+ # Persistence subsection — this script only updates the state file
94
+ # itself, it does not write handoffs.
95
+ #
96
+ # complete --id ID
97
+ # Set status "complete" and update "updated_at". A completed
98
+ # elicitation's state file is left on disk (not deleted) as an audit
99
+ # trail of what was asked and confirmed; nothing currently prunes it.
100
+ #
101
+ # status --id ID
102
+ # Read-only. Prints the current state file, pretty-printed.
103
+ #
104
+ # list-paused
105
+ # Read-only. Prints a JSON array of every state file currently
106
+ # "status": "paused" — {"id", "state_file", "target", "updated_at"} per
107
+ # entry — for a resuming session (or on_session_end.sh's routing logic)
108
+ # to discover what is waiting to be picked back up.
109
+ #
110
+ # ---------------------------------------------------------------------------
111
+ # CONCURRENCY
112
+ # ---------------------------------------------------------------------------
113
+ # Every mutating subcommand (init/turn/converge/checkpoint/pause/complete)
114
+ # wraps its read-modify-write in scripts/with-lock.sh, keyed to the target
115
+ # state file — the same atomic mkdir-based lock already used for board files
116
+ # and events.json (see hooks/on_session_end.sh and
117
+ # templates/SCRUM_BOARD_SCHEMA.md's File Locking section), not a new
118
+ # concurrency mechanism. The write itself is atomic (temp file in the same
119
+ # directory, then `mv`), matching the pattern in hooks/on_session_end.sh's
120
+ # own events.json append.
121
+ #
122
+ # ---------------------------------------------------------------------------
123
+ # EXIT CODES
124
+ # ---------------------------------------------------------------------------
125
+ # 0 — success
126
+ # 1 — usage error: unknown subcommand/flag, missing required argument
127
+ # 2 — input error: state file missing for a subcommand that requires it
128
+ # (everything except init/list-paused), or --json input unreadable/
129
+ # not a JSON object
130
+ # 3 — turn cap reached (ONLY for `turn`; the increment still happened and
131
+ # was persisted — this is a signal to stop looping, not a failure)
132
+ # 4 — write failure: could not acquire the lock, or the atomic write failed
133
+ #
134
+ # Examples:
135
+ # elicitation-state.sh init --id onboard-2026-09-01 --target . --cap 5
136
+ # elicitation-state.sh turn --id onboard-2026-09-01 --node billing-worker
137
+ # elicitation-state.sh converge --id onboard-2026-09-01 --node billing-worker --note "confirmed: reconciles ledger entries"
138
+ # echo '{"directory_triage": {...}}' | elicitation-state.sh checkpoint --id onboard-2026-09-01 --json -
139
+ # elicitation-state.sh pause --id onboard-2026-09-01
140
+ # elicitation-state.sh list-paused
141
+ #
142
+ # Requires: bash, python3, git (to resolve the repo root).
143
+
144
+ set -euo pipefail
145
+
146
+ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
147
+ REPO_ROOT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)
148
+
149
+ if [ -z "$REPO_ROOT" ]; then
150
+ echo "elicitation-state.sh: could not resolve repository root from $SCRIPT_DIR (not inside a git work tree?)" >&2
151
+ exit 2
152
+ fi
153
+
154
+ WITH_LOCK="$REPO_ROOT/scripts/with-lock.sh"
155
+ STATE_DIR="$REPO_ROOT/project/queue/elicitation-state"
156
+ DEFAULT_CAP=5
157
+
158
+ mkdir -p "$STATE_DIR"
159
+
160
+ usage() {
161
+ cat <<'EOF'
162
+ Usage: elicitation-state.sh <subcommand> [options]
163
+
164
+ Subcommands:
165
+ init --id ID [--target TEXT] [--cap N]
166
+ turn --id ID --node NODE
167
+ converge --id ID --node NODE [--note TEXT]
168
+ checkpoint --id ID --json FILE|-
169
+ pause --id ID
170
+ complete --id ID
171
+ status --id ID
172
+ list-paused
173
+
174
+ See this script's own header comment for full semantics of each subcommand.
175
+
176
+ Exit codes: 0 success, 1 usage error, 2 input error, 3 turn cap reached
177
+ (only for `turn`), 4 write failure.
178
+ EOF
179
+ }
180
+
181
+ if [ "$#" -eq 0 ]; then
182
+ usage
183
+ exit 1
184
+ fi
185
+
186
+ SUBCOMMAND="$1"
187
+ shift
188
+
189
+ if [ "$SUBCOMMAND" = "-h" ] || [ "$SUBCOMMAND" = "--help" ]; then
190
+ usage
191
+ exit 0
192
+ fi
193
+
194
+ ID=""
195
+ NODE=""
196
+ TARGET=""
197
+ CAP="$DEFAULT_CAP"
198
+ NOTE=""
199
+ JSON_ARG=""
200
+
201
+ while [ "$#" -gt 0 ]; do
202
+ case "$1" in
203
+ --id)
204
+ [ "$#" -ge 2 ] || { usage; exit 1; }
205
+ ID="$2"; shift 2 ;;
206
+ --node)
207
+ [ "$#" -ge 2 ] || { usage; exit 1; }
208
+ NODE="$2"; shift 2 ;;
209
+ --target)
210
+ [ "$#" -ge 2 ] || { usage; exit 1; }
211
+ TARGET="$2"; shift 2 ;;
212
+ --cap)
213
+ [ "$#" -ge 2 ] || { usage; exit 1; }
214
+ CAP="$2"; shift 2 ;;
215
+ --note)
216
+ [ "$#" -ge 2 ] || { usage; exit 1; }
217
+ NOTE="$2"; shift 2 ;;
218
+ --json)
219
+ [ "$#" -ge 2 ] || { usage; exit 1; }
220
+ JSON_ARG="$2"; shift 2 ;;
221
+ -h|--help)
222
+ usage; exit 0 ;;
223
+ *)
224
+ echo "elicitation-state.sh: unknown argument '$1'" >&2
225
+ exit 1 ;;
226
+ esac
227
+ done
228
+
229
+ case "$SUBCOMMAND" in
230
+ init|turn|converge|checkpoint|pause|complete|status)
231
+ if [ -z "$ID" ]; then
232
+ echo "elicitation-state.sh: --id is required for '$SUBCOMMAND'" >&2
233
+ exit 1
234
+ fi
235
+ ;;
236
+ list-paused)
237
+ ;;
238
+ *)
239
+ echo "elicitation-state.sh: unknown subcommand '$SUBCOMMAND'" >&2
240
+ exit 1
241
+ ;;
242
+ esac
243
+
244
+ if [ "$SUBCOMMAND" = "turn" ] || [ "$SUBCOMMAND" = "converge" ]; then
245
+ if [ -z "$NODE" ]; then
246
+ echo "elicitation-state.sh: --node is required for '$SUBCOMMAND'" >&2
247
+ exit 1
248
+ fi
249
+ fi
250
+
251
+ if [ "$SUBCOMMAND" = "checkpoint" ] && [ -z "$JSON_ARG" ]; then
252
+ echo "elicitation-state.sh: --json is required for 'checkpoint'" >&2
253
+ exit 1
254
+ fi
255
+
256
+ if [ -n "$ID" ]; then
257
+ STATE_FILE="$STATE_DIR/${ID}.json"
258
+ fi
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # Read-only subcommands — no lock needed, nothing is mutated.
262
+ # ---------------------------------------------------------------------------
263
+
264
+ if [ "$SUBCOMMAND" = "status" ]; then
265
+ if [ ! -f "$STATE_FILE" ]; then
266
+ echo "elicitation-state.sh: no state file for id '$ID' ($STATE_FILE)" >&2
267
+ exit 2
268
+ fi
269
+ python3 -m json.tool "$STATE_FILE"
270
+ exit 0
271
+ fi
272
+
273
+ if [ "$SUBCOMMAND" = "list-paused" ]; then
274
+ python3 -c '
275
+ import json, glob, os, sys
276
+
277
+ state_dir = sys.argv[1]
278
+ paused = []
279
+ for path in sorted(glob.glob(os.path.join(state_dir, "*.json"))):
280
+ try:
281
+ with open(path) as f:
282
+ data = json.load(f)
283
+ except (OSError, ValueError):
284
+ continue
285
+ if data.get("status") == "paused":
286
+ paused.append({
287
+ "id": data.get("id", os.path.splitext(os.path.basename(path))[0]),
288
+ "state_file": path,
289
+ "target": data.get("target", ""),
290
+ "updated_at": data.get("updated_at", ""),
291
+ })
292
+ print(json.dumps(paused, indent=2))
293
+ ' "$STATE_DIR"
294
+ exit 0
295
+ fi
296
+
297
+ # ---------------------------------------------------------------------------
298
+ # Mutating subcommands — everything below goes through with-lock.sh.
299
+ # ---------------------------------------------------------------------------
300
+
301
+ if [ ! -f "$WITH_LOCK" ]; then
302
+ echo "elicitation-state.sh: scripts/with-lock.sh not found at $WITH_LOCK" >&2
303
+ exit 4
304
+ fi
305
+
306
+ if [ "$SUBCOMMAND" != "init" ] && [ ! -f "$STATE_FILE" ]; then
307
+ echo "elicitation-state.sh: no state file for id '$ID' ($STATE_FILE) — run 'init' first" >&2
308
+ exit 2
309
+ fi
310
+
311
+ # checkpoint's JSON payload is read here (outside the lock) so a bad/missing
312
+ # file fails fast with exit 2 before ever touching the lock.
313
+ CHECKPOINT_JSON="{}"
314
+ if [ "$SUBCOMMAND" = "checkpoint" ]; then
315
+ if [ "$JSON_ARG" = "-" ]; then
316
+ CHECKPOINT_JSON=$(cat)
317
+ else
318
+ if [ ! -f "$JSON_ARG" ]; then
319
+ echo "elicitation-state.sh: --json file '$JSON_ARG' does not exist" >&2
320
+ exit 2
321
+ fi
322
+ CHECKPOINT_JSON=$(cat "$JSON_ARG")
323
+ fi
324
+ if ! echo "$CHECKPOINT_JSON" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert isinstance(d, dict)' 2>/dev/null; then
325
+ echo "elicitation-state.sh: --json payload is not a JSON object" >&2
326
+ exit 2
327
+ fi
328
+ fi
329
+
330
+ # The update logic is captured as a standalone python3 script and run via
331
+ # `python3 -c` with every value passed as a positional argv entry (never
332
+ # interpolated into the script text), mirroring the same avoid-fragile-
333
+ # quoting convention hooks/on_session_end.sh already uses for its own
334
+ # events.json read-modify-write.
335
+ UPDATE_SCRIPT=$(cat <<'PYEOF'
336
+ import json, os, sys, tempfile, datetime
337
+
338
+ state_file, subcommand, elicitation_id, target, cap_s, node, note, checkpoint_json = sys.argv[1:9]
339
+
340
+ now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
341
+
342
+ def load():
343
+ with open(state_file) as f:
344
+ return json.load(f)
345
+
346
+ def atomic_write(data):
347
+ dir_name = os.path.dirname(state_file)
348
+ fd, tmp_path = tempfile.mkstemp(prefix=".elicitation_tmp.", dir=dir_name)
349
+ try:
350
+ with os.fdopen(fd, "w") as f:
351
+ json.dump(data, f, indent=2)
352
+ f.write("\n")
353
+ os.replace(tmp_path, state_file)
354
+ except Exception:
355
+ if os.path.exists(tmp_path):
356
+ os.remove(tmp_path)
357
+ raise
358
+
359
+ result = {}
360
+ exit_code = 0
361
+
362
+ if subcommand == "init":
363
+ if os.path.exists(state_file):
364
+ state = load()
365
+ result = {"created": False, "state": state}
366
+ else:
367
+ cap = int(cap_s)
368
+ state = {
369
+ "id": elicitation_id,
370
+ "target": target,
371
+ "cap": cap,
372
+ "status": "in_progress",
373
+ "created_at": now,
374
+ "updated_at": now,
375
+ "nodes": {},
376
+ "checkpoint": {},
377
+ }
378
+ atomic_write(state)
379
+ result = {"created": True, "state": state}
380
+
381
+ elif subcommand == "turn":
382
+ state = load()
383
+ nodes = state.setdefault("nodes", {})
384
+ entry = nodes.setdefault(node, {"turns": 0, "status": "pending", "note": ""})
385
+ entry["turns"] = entry.get("turns", 0) + 1
386
+ cap = state.get("cap", int(cap_s))
387
+ cap_reached = entry["turns"] >= cap
388
+ if cap_reached:
389
+ entry["status"] = "flagged"
390
+ state["updated_at"] = now
391
+ atomic_write(state)
392
+ result = {"turns": entry["turns"], "cap": cap, "cap_reached": cap_reached}
393
+ if cap_reached:
394
+ exit_code = 3
395
+
396
+ elif subcommand == "converge":
397
+ state = load()
398
+ nodes = state.setdefault("nodes", {})
399
+ entry = nodes.setdefault(node, {"turns": 0, "status": "pending", "note": ""})
400
+ entry["status"] = "converged"
401
+ if note:
402
+ entry["note"] = note
403
+ state["updated_at"] = now
404
+ atomic_write(state)
405
+ result = {"node": node, "status": "converged", "turns": entry.get("turns", 0)}
406
+
407
+ elif subcommand == "checkpoint":
408
+ state = load()
409
+ payload = json.loads(checkpoint_json)
410
+ cp = state.setdefault("checkpoint", {})
411
+ cp.update(payload)
412
+ state["updated_at"] = now
413
+ atomic_write(state)
414
+ result = {"checkpoint_keys": list(payload.keys())}
415
+
416
+ elif subcommand == "pause":
417
+ state = load()
418
+ state["status"] = "paused"
419
+ state["updated_at"] = now
420
+ atomic_write(state)
421
+ result = {"status": "paused"}
422
+
423
+ elif subcommand == "complete":
424
+ state = load()
425
+ state["status"] = "complete"
426
+ state["updated_at"] = now
427
+ atomic_write(state)
428
+ result = {"status": "complete"}
429
+
430
+ else:
431
+ sys.stderr.write("elicitation-state.sh: internal error — unhandled subcommand '%s'\n" % subcommand)
432
+ sys.exit(1)
433
+
434
+ print(json.dumps(result, indent=2))
435
+ sys.exit(exit_code)
436
+ PYEOF
437
+ )
438
+
439
+ set +e
440
+ # NOTE: unlike `bash -c`, `python3 -c CODE arg1 arg2...` sets sys.argv[0] to
441
+ # the literal string "-c" (not the first following argument) — there is no
442
+ # python3 equivalent of bash's "$0 placeholder" convention. sys.argv[1:9]
443
+ # below therefore lines up directly with the positional arguments given here,
444
+ # with no placeholder needed.
445
+ "$WITH_LOCK" "$STATE_FILE" -- python3 -c "$UPDATE_SCRIPT" \
446
+ "$STATE_FILE" "$SUBCOMMAND" "$ID" "$TARGET" "$CAP" "$NODE" "$NOTE" "$CHECKPOINT_JSON"
447
+ STATUS=$?
448
+ set -e
449
+
450
+ if [ "$STATUS" -eq 2 ]; then
451
+ # with-lock.sh's own "could not acquire the lock" exit code — remap to
452
+ # this script's write-failure code so callers have one code (4), not two,
453
+ # to check for "the mutation did not happen".
454
+ exit 4
455
+ fi
456
+
457
+ exit "$STATUS"
@@ -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
@@ -254,18 +273,75 @@ construction**. Agents and humans reading the board should not treat a backfille
254
273
  Definition of Done as a build plan, and should not assume that an incomplete-looking backfilled
255
274
  epic represents unbuilt functionality.
256
275
 
276
+ ## Board Item Tag Conventions
277
+
278
+ Two bracketed title-tag conventions mark board items whose nature differs from ordinary delivery
279
+ work: `[SPIKE]` (bounded research) and `[ARCH]` (durable architectural inventory). Both are
280
+ **title-text conventions, not frontmatter fields** — there is no `tag:` key; the tag is written
281
+ directly into the item's `title` (e.g. `title: "[SPIKE] Security Section"`) and is not validated or
282
+ enforced by `scripts/validate-board.sh`, which treats `title` as free text. Using either tag is
283
+ advisory: it signals intent to readers of the board, `/status` output, and rollup logic, but nothing
284
+ currently gates on its presence.
285
+
286
+ ### `[SPIKE]` — Bounded Research
287
+
288
+ **Scope:** story and task level only.
289
+
290
+ **Meaning:** a time-boxed research or exploration effort whose output is a decision, a design note,
291
+ or an answered question — **not** shippable implementation code. Existing usage (`E06_S03_spike-editable-board.md`,
292
+ `E08_S02_spike-security-section.md`) follows a consistent shape:
293
+
294
+ - Bracketed title: `title: "[SPIKE] <Topic>"`.
295
+ - A `## Spike Questions to Answer` section (or equivalent) in place of, or alongside, ordinary
296
+ Acceptance Criteria.
297
+ - A Definition of Done line stating that no implementation code is produced by the spike itself —
298
+ only findings, a design note, or a recommendation that a follow-up story/task will act on.
299
+
300
+ `[SPIKE]` predates this document; this section formalizes an existing informal convention rather
301
+ than introducing new behavior.
302
+
303
+ ### `[ARCH]` — Durable Architectural Inventory
304
+
305
+ **Scope:** epic, story, and task level. This is the **first tag extended to epic level** — `[SPIKE]`
306
+ has never applied above story/task.
307
+
308
+ **Meaning:** durable architectural-inventory record-keeping — capturing how existing or
309
+ newly-understood code is structured, so the record persists as a lasting reference — as distinct
310
+ from `[SPIKE]`'s bounded, time-boxed research meaning. `[ARCH]`-tagged items are not "temporary
311
+ until answered" the way a spike is; they are the durable output itself (e.g. graph nodes/edges,
312
+ architecture documentation) and are not expected to be superseded by a subsequent non-`[ARCH]` item
313
+ the way a spike's findings feed into a normal follow-up.
314
+
315
+ Introduced for E20_S08's conversational architecture elicitation flow (`/uncharted` integration),
316
+ where generated board items record architectural understanding of code rather than proposing new
317
+ delivery work, at a scale (potentially a whole investigated subsystem) that can reach epic level.
318
+
319
+ **Explicitly distinct from `[SPIKE]`:**
320
+
321
+ | | `[SPIKE]` | `[ARCH]` |
322
+ |---|---|---|
323
+ | Nature | Bounded, time-boxed research | Durable architectural record |
324
+ | Valid levels | Story, task | Epic, story, task |
325
+ | Typical DoD | "No implementation code produced" | Graph nodes/edges written, or architecture documented |
326
+ | Lifecycle | Findings feed a follow-up item | The record itself is the lasting artifact |
327
+
328
+ Do not use `[ARCH]` and `[SPIKE]` interchangeably or on the same item — pick whichever meaning
329
+ actually applies. An epic can only ever be `[ARCH]` (or untagged); `[SPIKE]` is not valid at epic
330
+ level.
331
+
257
332
  ## Execution Scope Fields (Task)
258
333
 
259
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`.
260
335
 
261
336
  **`execution_scope`**
262
- - Valid values: `task` | `story` | `epic` | `inline`
337
+ - Valid values: `task` | `story` | `epic` | `inline` | `light`
263
338
  - When required: optional; omit for legacy tasks (runtime default: `task`)
264
339
  - Description: defines how broadly this task's implementation touches the codebase.
265
340
  - `task` — standard single-task scope (default)
266
341
  - `story` — task may touch files across multiple tasks in the same story
267
342
  - `epic` — task may touch files across stories; requires `epic_scope_approval: true` on the parent epic
268
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
269
345
 
270
346
  **`needs_docs`**
271
347
  - Valid values: `true` | `false`
@@ -429,6 +505,7 @@ A `crucial_escalation` rapport must also name the target item's ID (`E##`, `E##_
429
505
  | `rapport_review` | on_session_end.sh | New problem rapport(s) detected; create backlog items or mark Failed |
430
506
  | `status_review` | on_session_end.sh | Session ended; review board for stale statuses |
431
507
  | `story_rollup` | tester / on_session_end.sh | All tasks under story complete; check rollup |
508
+ | `elicitation_resume` | on_session_end.sh (from a scrum-master `elicitation_paused` handoff) | A `/uncharted` conversational architecture elicitation (E20_S08_T03) paused mid-run; resume it from the state file persisted by `skills/uncharted/scripts/elicitation-state.sh` |
432
509
 
433
510
  ### `developer_triggers.jsonl` — processed by developer at session start
434
511
 
@@ -471,6 +548,7 @@ Each file is written by an agent as the **last action** of its session, and is s
471
548
  | `worktree` | developer, tester | Absolute path |
472
549
  | `paths` | developer, tester | Commit SHAs |
473
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 |
474
552
  | `date` | all | ISO 8601 UTC |
475
553
 
476
554
  **Status values per agent:**
@@ -478,6 +556,16 @@ Each file is written by an agent as the **last action** of its session, and is s
478
556
  - `developer`: `implementation_complete`
479
557
  - `tester`: `passed`, `passed_with_remarks`, `failed`, `error`
480
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
+
481
569
 
482
570
 
483
571
  Located at `project/configs/workflow.json`. Scaffolded by `/init` and owned by the scrum master.