@jenga-ai/agent 1.1.0 → 1.1.1

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 (91) hide show
  1. package/README.md +7 -3
  2. package/agents/developer.md +82 -2
  3. package/agents/scrum-master.md +140 -21
  4. package/agents/tester.md +90 -8
  5. package/hooks/on_session_end.sh +171 -20
  6. package/package.json +1 -1
  7. package/scripts/check-permission-level.sh +107 -0
  8. package/scripts/check-publicignore-match.sh +122 -0
  9. package/scripts/check-worktree-liveness.sh +193 -0
  10. package/scripts/generate-rapport-manifest.sh +43 -0
  11. package/scripts/idea_manager.sh +47 -0
  12. package/scripts/install-worktree-commit-guard.sh +134 -0
  13. package/scripts/jenga-permission-level-switch.sh +109 -0
  14. package/scripts/smoke-harness.sh +139 -0
  15. package/scripts/validate-board.sh +62 -0
  16. package/scripts/with-lock.sh +158 -0
  17. package/scripts/worktree-remove-guard.sh +204 -0
  18. package/skills/clearify/SKILL.md +52 -0
  19. package/skills/commit/SKILL.md +13 -4
  20. package/skills/distribute/CONFIG_SCHEMA.md +60 -2
  21. package/skills/do/SKILL.md +48 -11
  22. package/skills/doc-sync/SKILL.md +16 -0
  23. package/skills/doc-sync/assets/doc_targets.md +11 -0
  24. package/skills/idea/SKILL.md +56 -0
  25. package/skills/idea/assets/idea_handoff_template.md +26 -0
  26. package/skills/idea/assets/idea_template.md +3 -0
  27. package/skills/init/SKILL.md +100 -7
  28. package/skills/init/assets/directory_structure.txt +1 -0
  29. package/skills/init/assets/workflow_template.json +1 -1
  30. package/skills/init/scripts/apply-project-visibility.sh +176 -0
  31. package/skills/init/scripts/detect-existing-codebase.sh +166 -0
  32. package/skills/init/scripts/init.sh +30 -1
  33. package/skills/jenga/SKILL.md +160 -17
  34. package/skills/jenga/scripts/board-scan.sh +238 -0
  35. package/skills/jenga/scripts/cascade-resolve.sh +297 -0
  36. package/skills/jenga/scripts/render-confirmation.sh +679 -0
  37. package/skills/jenga/scripts/render-picker.sh +439 -0
  38. package/skills/jenga/scripts/resolve-id.sh +367 -0
  39. package/skills/jenga-permission-level/SKILL.md +81 -0
  40. package/skills/proceed/SKILL.md +1 -1
  41. package/skills/publish/SKILL.md +8 -5
  42. package/skills/publish/assets/ci-contract.md +2 -2
  43. package/skills/publish/assets/ownership-matrix.md +1 -1
  44. package/skills/publish/scripts/finalize_changelog.sh +115 -0
  45. package/skills/publish/scripts/generate_release_notes.sh +475 -28
  46. package/skills/publish/scripts/npm_ci_pipeline.sh +44 -6
  47. package/skills/publish/scripts/publish_deploy.sh +38 -8
  48. package/skills/publish/scripts/run_gates.sh +2 -2
  49. package/skills/reconcile/SKILL.md +117 -5
  50. package/skills/reconcile/scripts/detect-unlinked-code.sh +741 -0
  51. package/skills/skillify/assets/init-new/assets/directory_structure.txt +5 -1
  52. package/skills/spinoff/SKILL.md +12 -7
  53. package/skills/todo/SKILL.md +2 -0
  54. package/skills/uncharted/SKILL.md +711 -0
  55. package/skills/uncharted/assets/SEGMENT_PROPOSAL_TEMPLATE.md +129 -0
  56. package/skills/uncharted/assets/UNDERSTANDING_DOC_TEMPLATE.md +160 -0
  57. package/skills/uncharted/scripts/apply-subsystem-cap.sh +573 -0
  58. package/skills/uncharted/scripts/detect-dependencies.sh +732 -0
  59. package/skills/uncharted/scripts/detect-tests.sh +553 -0
  60. package/skills/uncharted/scripts/discover-subsystems.sh +1029 -0
  61. package/skills/uncharted/scripts/enumerate-target.sh +470 -0
  62. package/skills/uncharted/scripts/import-source.sh +517 -0
  63. package/skills/uncharted/scripts/inspect-provenance.sh +573 -0
  64. package/skills/uncharted/scripts/resolve-segment-target.sh +640 -0
  65. package/skills/uncharted/scripts/run-engine.sh +655 -0
  66. package/skills/uncharted/scripts/validate-proposed-items.sh +125 -0
  67. package/skills/uncharted/scripts/write-backfilled-epics.sh +498 -0
  68. package/skills/wtf/SKILL.md +20 -0
  69. package/templates/CHANGELOG_TEMPLATE.md +13 -0
  70. package/templates/PROBLEM_RAPPORT_TEMPLATE.md +4 -1
  71. package/templates/SCRUM_BOARD_SCHEMA.md +157 -10
  72. package/templates/permission-levels/README.md +73 -0
  73. package/templates/permission-levels/level-1-locked.json +71 -0
  74. package/templates/permission-levels/level-2-guarded.json +64 -0
  75. package/templates/permission-levels/level-3-standard.json +62 -0
  76. package/templates/permission-levels/level-4-elevated.json +60 -0
  77. package/templates/permission-levels/level-5-unrestricted.json +58 -0
  78. package/skills/convert/SKILL.md +0 -124
  79. package/skills/convert/convert_cli.py +0 -235
  80. package/skills/convert/tests/sample.csv +0 -4
  81. package/skills/convert/tests/sample.json +0 -5
  82. package/skills/convert/tests/sample.jsonl +0 -3
  83. package/skills/convert/tests/sample.yaml +0 -18
  84. package/skills/convert/tests/sample_obj.csv +0 -2
  85. package/skills/convert/tests/sample_obj.json +0 -9
  86. package/skills/mirror-public/SKILL.md +0 -237
  87. package/skills/mirror-public/assets/config.json +0 -5
  88. package/skills/mirror-public/scripts/mirror.sh +0 -374
  89. package/skills/self-sync/SKILL.md +0 -73
  90. package/skills/self-sync/scripts/run.js +0 -136
  91. package/skills/strategy/SKILL.md +0 -312
@@ -0,0 +1,573 @@
1
+ #!/usr/bin/env bash
2
+ # apply-subsystem-cap.sh — bound the number of backfilled epics `onboard` produces, and make
3
+ # every subsystem it drops durable and auditable
4
+ #
5
+ # Usage: apply-subsystem-cap.sh [options] [<report.json>]
6
+ # discover-subsystems.sh <root> | apply-subsystem-cap.sh [options]
7
+ # apply-subsystem-cap.sh --help
8
+ #
9
+ # `onboard` mode backfills a board for a codebase that was never built through Jenga. Left
10
+ # unbounded it would emit one epic per discovered subsystem, and adopting Jenga into a large
11
+ # repository would flood the board with dozens of epics nobody asked for. The CAP is that bound.
12
+ #
13
+ # The cap is the easy half. The hard requirement — the reason this is its own script rather than
14
+ # three lines inside the epic generator — is that the cap is NEVER SILENT:
15
+ #
16
+ # A user who onboards a 40-subsystem repo and gets 8 epics must be able to find out, LATER
17
+ # AND ON DISK, which 32 subsystems were not turned into epics, and why.
18
+ #
19
+ # A chat message does not satisfy that. It scrolls away, it is not in the repository, and the
20
+ # person who inherits the board six months later never saw it. So every run writes a
21
+ # human-readable Subsystem Cap section into an analysis rapport under project/rapports/analysis/,
22
+ # naming every dropped subsystem individually. There is deliberately NO flag to suppress that
23
+ # write and no "… and 32 more" elision in it: either would reintroduce exactly the silent
24
+ # truncation this script exists to prevent.
25
+ #
26
+ # This script performs NO discovery of its own. It consumes discover-subsystems.sh's ranked
27
+ # output and contributes only the cap decision and its record. It is read-only against the
28
+ # analysed codebase — its only writes are the rapport and, on request, the JSON report.
29
+ #
30
+ # ---------------------------------------------------------------------------
31
+ # INPUT
32
+ # ---------------------------------------------------------------------------
33
+ # discover-subsystems.sh's JSON report, from a file argument or from stdin (`-`, or no argument).
34
+ # The fields consumed are `candidates[].rank`, `.path`, `.score`, `.files`, `.lines`, plus the
35
+ # report's `root` / `root_absolute` for labelling. Anything else is passed through untouched.
36
+ #
37
+ # The candidate array arrives already ranked (score descending, then path ascending). This script
38
+ # SLICES it; it does not re-rank. Re-sorting here would silently disagree with the discoverer the
39
+ # moment its tie-break changed. If the array order and the `rank` fields disagree — which only
40
+ # happens if something edited the report in between — a notice says so and array order wins,
41
+ # because array order is what the discoverer's own consumers see.
42
+ #
43
+ # ---------------------------------------------------------------------------
44
+ # OPTIONS
45
+ # ---------------------------------------------------------------------------
46
+ # --cap N Maximum number of subsystems that become backfilled epics. Default 8,
47
+ # which is the figure `onboard`'s own worked example assumes. Must be >= 1:
48
+ # a cap of 0 or below is a USAGE ERROR, not an empty run, because "produce no
49
+ # epics at all" is never what a caller meant and must fail loudly.
50
+ # --rapport FILE Append the Subsystem Cap section to this existing analysis document — the
51
+ # one run-engine.sh --mode onboard already wrote for this codebase. The normal
52
+ # onboard flow: the cap record belongs with the analysis that produced it.
53
+ # --out-dir DIR Where a STANDALONE cap record is written when --rapport is not given.
54
+ # Default <repo-root>/project/rapports/analysis.
55
+ # --json-out FILE Also write this script's JSON report to a file. stdout gets it regardless.
56
+ # --label TEXT Human label for the analysed codebase in the rapport. Default: the report's
57
+ # own `root`.
58
+ # -h, --help Show this help and exit 0.
59
+ #
60
+ # ---------------------------------------------------------------------------
61
+ # OUTPUT (stdout, JSON)
62
+ # ---------------------------------------------------------------------------
63
+ # {
64
+ # "script": "apply-subsystem-cap.sh",
65
+ # "version": 1,
66
+ # "cap": <int>, // the cap in effect
67
+ # "root": "<from the input report>",
68
+ # "root_absolute": "<from the input report>",
69
+ # "candidate_count": <int>, // how many candidates were offered
70
+ # "kept_count": <int>,
71
+ # "dropped_count": <int>,
72
+ # "capped": <bool>, // true when the cap actually removed something
73
+ # "rapport": "<absolute path of the document carrying the drop record>",
74
+ # "rapport_mode": "appended" | "created",
75
+ # "kept": [ { "rank", "path", "absolute_path", "score", "files", "lines" }, ... ],
76
+ # "dropped": [ { "rank", "path", "absolute_path", "score", "files", "lines", "reason" }, ... ],
77
+ # "notices": [ "<non-fatal diagnostic>", ... ]
78
+ # }
79
+ #
80
+ # Every `dropped` entry carries an explicit `reason` string — "below cap of N" — so a consumer
81
+ # never has to infer why an entry is in that array.
82
+ #
83
+ # When the candidate count is at or below the cap, `dropped` is an EMPTY ARRAY and the exit code
84
+ # is 0. That is a normal, successful run, not a degenerate one. The rapport section is still
85
+ # written in that case: "the cap was applied and removed nothing" is itself a fact worth being
86
+ # able to look up, and always writing it means the record's absence is unambiguous evidence that
87
+ # this script never ran.
88
+ #
89
+ # ---------------------------------------------------------------------------
90
+ # EXIT CODES
91
+ # ---------------------------------------------------------------------------
92
+ # 0 — success (including the nothing-was-dropped case)
93
+ # 1 — usage error: unknown flag, missing value, non-integer cap, or a cap of 0 or below
94
+ # 2 — input error: report file missing/unreadable, unparseable JSON, or not a
95
+ # discover-subsystems.sh report with a candidates array
96
+ # 4 — write failure: the rapport could not be written. Deliberately FATAL. Exiting 0 with a
97
+ # clean kept/dropped JSON and no durable record would be a silent truncation wearing a
98
+ # different hat, so the destination is pre-flighted before any work and a failure to
99
+ # record the drop fails the whole run.
100
+ #
101
+ # Examples:
102
+ # discover-subsystems.sh . | apply-subsystem-cap.sh
103
+ # discover-subsystems.sh . > s.json && apply-subsystem-cap.sh --cap 12 s.json
104
+ # apply-subsystem-cap.sh --rapport project/rapports/analysis/uncharted-onboard-app-….md s.json
105
+ #
106
+ # Requires: bash, python3. jq is NOT required — JSON is parsed and emitted by python3.
107
+
108
+ set -euo pipefail
109
+
110
+ DEFAULT_CAP=8
111
+
112
+ CAP="$DEFAULT_CAP"
113
+ RAPPORT=""
114
+ OUT_DIR=""
115
+ JSON_OUT=""
116
+ LABEL=""
117
+ INPUT=""
118
+
119
+ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
120
+
121
+ usage() {
122
+ cat <<EOF
123
+ Usage: $(basename "$0") [options] [<report.json>]
124
+
125
+ Apply the onboard epic cap to discover-subsystems.sh output. Emits JSON with explicit "kept" and
126
+ "dropped" arrays, and always records every dropped subsystem BY NAME in a human-readable
127
+ Subsystem Cap section of an analysis rapport. Dropped subsystems are never silently truncated.
128
+
129
+ Arguments:
130
+ <report.json> discover-subsystems.sh output. Omit, or pass "-", to read stdin.
131
+
132
+ Options:
133
+ --cap N Max subsystems that become epics (default: $DEFAULT_CAP, minimum: 1)
134
+ --rapport FILE Append the cap record to this existing analysis document
135
+ --out-dir DIR Directory for a standalone cap record (default: <repo-root>/project/rapports/analysis)
136
+ --json-out FILE Also write this script's JSON report to FILE
137
+ --label TEXT Human label for the codebase in the rapport (default: the report's root)
138
+ -h, --help Show this help and exit
139
+
140
+ Exit codes: 0 success, 1 usage error, 2 input error, 4 rapport write failure.
141
+ EOF
142
+ }
143
+
144
+ die_usage() {
145
+ echo "Error: $1" >&2
146
+ echo >&2
147
+ usage >&2
148
+ exit 1
149
+ }
150
+
151
+ require_value() {
152
+ # require_value <flag> <remaining-arg-count>
153
+ [ "$2" -ge 2 ] || die_usage "$1 requires a value"
154
+ }
155
+
156
+ validate_cap() {
157
+ # A cap of 0 or below is an ERROR, never an empty run. Validated here, before the report is even
158
+ # read, so the message can name the value actually received rather than the run failing later
159
+ # with an inexplicably empty result set.
160
+ if ! [[ "$1" =~ ^-?[0-9]+$ ]]; then
161
+ die_usage "--cap requires an integer, got \"$1\""
162
+ fi
163
+ if [ "$1" -lt 1 ]; then
164
+ echo "Error: --cap must be a positive integer, got \"$1\"." >&2
165
+ echo " A cap of 0 or below would mean \"produce no backfilled epics at all\", which is" >&2
166
+ echo " never a meaningful onboard run - so it is rejected rather than silently dropping" >&2
167
+ echo " every discovered subsystem. Pass --cap 1 or higher." >&2
168
+ echo >&2
169
+ usage >&2
170
+ exit 1
171
+ fi
172
+ }
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Argument parsing
176
+ # ---------------------------------------------------------------------------
177
+
178
+ while [ "$#" -gt 0 ]; do
179
+ case "$1" in
180
+ --cap) require_value "--cap" "$#"; CAP="$2"; shift 2 ;;
181
+ --cap=*) CAP="${1#*=}"; shift ;;
182
+ --rapport) require_value "--rapport" "$#"; RAPPORT="$2"; shift 2 ;;
183
+ --rapport=*) RAPPORT="${1#*=}"; shift ;;
184
+ --out-dir) require_value "--out-dir" "$#"; OUT_DIR="$2"; shift 2 ;;
185
+ --out-dir=*) OUT_DIR="${1#*=}"; shift ;;
186
+ --json-out) require_value "--json-out" "$#"; JSON_OUT="$2"; shift 2 ;;
187
+ --json-out=*) JSON_OUT="${1#*=}"; shift ;;
188
+ --label) require_value "--label" "$#"; LABEL="$2"; shift 2 ;;
189
+ --label=*) LABEL="${1#*=}"; shift ;;
190
+ -h|--help) usage; exit 0 ;;
191
+ --)
192
+ shift
193
+ [ "$#" -le 1 ] || die_usage "at most one input report is accepted"
194
+ [ "$#" -eq 0 ] || INPUT="$1"
195
+ break ;;
196
+ -)
197
+ INPUT="-"; shift ;;
198
+ -*)
199
+ die_usage "unknown option \"$1\"" ;;
200
+ *)
201
+ [ -z "$INPUT" ] || die_usage "at most one input report is accepted (got \"$INPUT\" and \"$1\")"
202
+ INPUT="$1"; shift ;;
203
+ esac
204
+ done
205
+
206
+ validate_cap "$CAP"
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # Input resolution
210
+ # ---------------------------------------------------------------------------
211
+
212
+ if [ -z "$INPUT" ] || [ "$INPUT" = "-" ]; then
213
+ INPUT="-"
214
+ else
215
+ if [ ! -e "$INPUT" ]; then
216
+ echo "Error: input report does not exist: $INPUT" >&2
217
+ echo " Expected discover-subsystems.sh JSON output." >&2
218
+ exit 2
219
+ fi
220
+ if [ ! -f "$INPUT" ] || [ ! -r "$INPUT" ]; then
221
+ echo "Error: input report is not a readable file: $INPUT" >&2
222
+ exit 2
223
+ fi
224
+ INPUT=$(cd -- "$(dirname -- "$INPUT")" && pwd -P)/$(basename -- "$INPUT")
225
+ fi
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # Output destinations — pre-flighted BEFORE any work
229
+ # ---------------------------------------------------------------------------
230
+ # Same fail-fast contract as run-engine.sh: an unwritable destination must surface with nothing
231
+ # done, rather than after a report has been emitted. Here it matters more than usual, because the
232
+ # rapport IS the audit trail; a run that reports drops it never recorded is the failure mode this
233
+ # script exists to rule out.
234
+ #
235
+ # Anchored on THIS SCRIPT, not on the analysed root: the cap record belongs to the project that
236
+ # owns the engine, even when onboarding a codebase that lives elsewhere.
237
+
238
+ REPO_ROOT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)
239
+ [ -n "$REPO_ROOT" ] || REPO_ROOT="$(pwd -P)"
240
+
241
+ if [ -n "$RAPPORT" ]; then
242
+ if [ ! -f "$RAPPORT" ]; then
243
+ echo "Error: --rapport document does not exist: $RAPPORT" >&2
244
+ echo " Pass the analysis document run-engine.sh --mode onboard wrote, or omit" >&2
245
+ echo " --rapport to have a standalone cap record created instead." >&2
246
+ exit 4
247
+ fi
248
+ if [ ! -w "$RAPPORT" ]; then
249
+ echo "Error: --rapport document is not writable: $RAPPORT" >&2
250
+ exit 4
251
+ fi
252
+ RAPPORT=$(cd -- "$(dirname -- "$RAPPORT")" && pwd -P)/$(basename -- "$RAPPORT")
253
+ else
254
+ [ -n "$OUT_DIR" ] || OUT_DIR="$REPO_ROOT/project/rapports/analysis"
255
+ mkdir -p "$OUT_DIR" 2>/dev/null || {
256
+ echo "Error: could not create output directory: $OUT_DIR" >&2
257
+ exit 4
258
+ }
259
+ [ -w "$OUT_DIR" ] || { echo "Error: output directory is not writable: $OUT_DIR" >&2; exit 4; }
260
+ OUT_DIR=$(cd -- "$OUT_DIR" && pwd -P)
261
+ fi
262
+
263
+ if [ -n "$JSON_OUT" ]; then
264
+ JSON_OUT_DIR=$(dirname -- "$JSON_OUT")
265
+ [ -d "$JSON_OUT_DIR" ] || { echo "Error: --json-out directory does not exist: $JSON_OUT_DIR" >&2; exit 4; }
266
+ [ -w "$JSON_OUT_DIR" ] || { echo "Error: --json-out directory is not writable: $JSON_OUT_DIR" >&2; exit 4; }
267
+ JSON_OUT=$(cd -- "$JSON_OUT_DIR" && pwd -P)/$(basename -- "$JSON_OUT")
268
+ fi
269
+
270
+ ISO_TS=$(date -u +%Y%m%dT%H%M%SZ)
271
+
272
+ # ---------------------------------------------------------------------------
273
+ # Apply the cap and record it
274
+ # ---------------------------------------------------------------------------
275
+
276
+ # The python source is captured into a variable and run with `python3 -c`, exactly as
277
+ # run-engine.sh does. A `python3 - <<PY` heredoc would occupy python's stdin, and this script has
278
+ # to be able to READ stdin (`discover-subsystems.sh . | apply-subsystem-cap.sh`), so the heredoc
279
+ # form is not available here.
280
+ PY_SRC=$(cat <<'PY'
281
+ import json
282
+ import os
283
+ import re
284
+ import sys
285
+
286
+ INPUT, CAP_S, RAPPORT, OUT_DIR, JSON_OUT, LABEL, TS, DEFAULT_CAP_S = sys.argv[1:9]
287
+ CAP = int(CAP_S)
288
+
289
+ notices = []
290
+
291
+ # --- read the discoverer's report ---------------------------------------------------------------
292
+
293
+ try:
294
+ if INPUT == "-":
295
+ raw = sys.stdin.read()
296
+ origin = "stdin"
297
+ else:
298
+ with open(INPUT, "r", encoding="utf-8") as fh:
299
+ raw = fh.read()
300
+ origin = INPUT
301
+ except OSError as exc:
302
+ sys.stderr.write("Error: could not read input report: %s\n" % exc)
303
+ sys.exit(2)
304
+
305
+ if not raw.strip():
306
+ sys.stderr.write("Error: input report is empty (%s).\n" % origin)
307
+ sys.stderr.write(" Expected discover-subsystems.sh JSON output.\n")
308
+ sys.exit(2)
309
+
310
+ try:
311
+ report = json.loads(raw)
312
+ except ValueError as exc:
313
+ sys.stderr.write("Error: input report is not valid JSON (%s): %s\n" % (origin, exc))
314
+ sys.stderr.write(" Expected discover-subsystems.sh JSON output.\n")
315
+ sys.exit(2)
316
+
317
+ if not isinstance(report, dict) or not isinstance(report.get("candidates"), list):
318
+ sys.stderr.write("Error: input report has no \"candidates\" array (%s).\n" % origin)
319
+ sys.stderr.write(" This does not look like discover-subsystems.sh output.\n")
320
+ sys.exit(2)
321
+
322
+ produced_by = report.get("script")
323
+ if produced_by and produced_by != "discover-subsystems.sh":
324
+ notices.append(
325
+ "Input reports itself as \"%s\" rather than discover-subsystems.sh; proceeding on the "
326
+ "strength of its candidates array." % produced_by)
327
+
328
+ candidates = report.get("candidates")
329
+ root = report.get("root") or report.get("root_absolute") or "(unknown root)"
330
+ root_absolute = report.get("root_absolute") or ""
331
+ # "." is what the discoverer reports when the analysed root IS the repo root, and it is a useless
332
+ # label in a document somebody reads months later. Fall back to the directory's real name.
333
+ if LABEL:
334
+ label = LABEL
335
+ elif root not in (".", "", "./"):
336
+ label = root
337
+ else:
338
+ label = os.path.basename(root_absolute.rstrip("/")) or root
339
+
340
+ # The array arrives ranked. Slicing it -- rather than re-sorting -- keeps this script from quietly
341
+ # disagreeing with the discoverer's own tie-break. A disagreement is reported, never repaired.
342
+ ranks = [c.get("rank") for c in candidates if isinstance(c, dict)]
343
+ if ranks == sorted(r for r in ranks if isinstance(r, int)) and None not in ranks:
344
+ pass
345
+ else:
346
+ notices.append(
347
+ "The candidates array is not in ascending rank order. Array order was used for the cap, "
348
+ "because that is the order the discoverer's consumers see; the report was not reordered.")
349
+
350
+
351
+ def field(c, name, default=None):
352
+ return c.get(name, default) if isinstance(c, dict) else default
353
+
354
+
355
+ def entry(c, reason=None):
356
+ out = {
357
+ "rank": field(c, "rank"),
358
+ "path": field(c, "path"),
359
+ "absolute_path": field(c, "absolute_path"),
360
+ "score": field(c, "score"),
361
+ "files": field(c, "files"),
362
+ "lines": field(c, "lines"),
363
+ }
364
+ if reason is not None:
365
+ out["reason"] = reason
366
+ return out
367
+
368
+
369
+ # --- the cap ------------------------------------------------------------------------------------
370
+ # The whole arithmetic, in two lines. It is small on purpose: the volume of this script is the
371
+ # RECORD of the decision, not the decision.
372
+
373
+ reason = "below cap of %d" % CAP
374
+ kept = [entry(c) for c in candidates[:CAP]]
375
+ dropped = [entry(c, reason) for c in candidates[CAP:]]
376
+ capped = bool(dropped)
377
+
378
+ # --- the drop record ----------------------------------------------------------------------------
379
+
380
+ # The --rapport document is read HERE, before the section is assembled, not at write time. Any
381
+ # notice raised by inspecting it has to be able to reach the section text -- and section text that
382
+ # has already been joined cannot be amended. Getting this order wrong is what made the durable
383
+ # record LESS complete than the ephemeral stderr it exists to outlive, which inverts the whole
384
+ # design intent of this script.
385
+ existing = None
386
+ if RAPPORT:
387
+ try:
388
+ with open(RAPPORT, "r", encoding="utf-8") as fh:
389
+ existing = fh.read()
390
+ except OSError as exc:
391
+ sys.stderr.write("Error: could not read --rapport document: %s\n" % exc)
392
+ sys.exit(4)
393
+ if "## Subsystem Cap" in existing:
394
+ notices.append(
395
+ "The rapport already carried a Subsystem Cap section; this run's record was appended "
396
+ "after it rather than replacing it, so earlier cap decisions stay auditable.")
397
+
398
+
399
+ def fmt_score(v):
400
+ return ("%.2f" % v) if isinstance(v, (int, float)) else "—"
401
+
402
+
403
+ def fmt_int(v):
404
+ return ("%d" % v) if isinstance(v, int) else "—"
405
+
406
+
407
+ def cell(v):
408
+ # Pipes inside a cell would break the table; nothing else needs escaping in a path.
409
+ return str(v if v not in (None, "") else "—").replace("|", "\\|")
410
+
411
+
412
+ def table(rows, with_reason):
413
+ head = "| Rank | Score | Subsystem | Files | Lines |"
414
+ rule = "|---:|---:|---|---:|---:|"
415
+ if with_reason:
416
+ head += " Reason |"
417
+ rule += "---|"
418
+ out = [head, rule]
419
+ for r in rows:
420
+ line = "| %s | %s | `%s` | %s | %s |" % (
421
+ fmt_int(r["rank"]), fmt_score(r["score"]), cell(r["path"]),
422
+ fmt_int(r["files"]), fmt_int(r["lines"]))
423
+ if with_reason:
424
+ line += " %s |" % cell(r.get("reason"))
425
+ out.append(line)
426
+ return out
427
+
428
+
429
+ section = []
430
+ section.append("## Subsystem Cap")
431
+ section.append("")
432
+ section.append("| | |")
433
+ section.append("|---|---|")
434
+ section.append("| Codebase | `%s` |" % cell(label))
435
+ section.append("| Cap in effect | %d |" % CAP)
436
+ section.append("| Subsystems discovered | %d |" % len(candidates))
437
+ section.append("| Kept — became backfilled epics | %d |" % len(kept))
438
+ section.append("| Dropped — did **not** become epics | %d |" % len(dropped))
439
+ section.append("| Recorded | %s |" % TS)
440
+ section.append("")
441
+
442
+ if capped:
443
+ section.append(
444
+ "`onboard` caps how many backfilled epics it creates so that adopting Jenga into a large "
445
+ "codebase does not flood the board. **%d of the %d subsystems discovered here were not "
446
+ "turned into epics.** They are all named below — the cap is never applied silently, and "
447
+ "this list is never elided."
448
+ % (len(dropped), len(candidates)))
449
+ else:
450
+ section.append(
451
+ "All %d discovered subsystem(s) fit within the cap of %d, so nothing was dropped. This "
452
+ "section is written on every capped run, including this one, so that its absence means "
453
+ "the cap was never applied — not that it happened to drop nothing."
454
+ % (len(candidates), CAP))
455
+ section.append("")
456
+
457
+ section.append("### Kept — backfilled as epics")
458
+ section.append("")
459
+ if kept:
460
+ section.extend(table(kept, with_reason=False))
461
+ else:
462
+ section.append("_No subsystems were discovered, so none were kept._")
463
+ section.append("")
464
+
465
+ section.append("### Dropped — not backfilled")
466
+ section.append("")
467
+ if dropped:
468
+ # Every dropped subsystem is named. No truncation, no "… and N more": eliding this list is
469
+ # precisely the silent truncation the cap record exists to prevent.
470
+ section.extend(table(dropped, with_reason=True))
471
+ section.append("")
472
+ section.append(
473
+ "These subsystems still exist in the codebase — they simply have no backfilled epic. To "
474
+ "adopt one, either re-run `onboard` with a higher `--cap`, or bring it onto the board "
475
+ "individually with `/uncharted segment <path>`.")
476
+ else:
477
+ section.append("_None. Every discovered subsystem is on the board._")
478
+ section.append("")
479
+
480
+ if notices:
481
+ section.append("### Notices")
482
+ section.append("")
483
+ for n in notices:
484
+ section.append("- %s" % n)
485
+ section.append("")
486
+
487
+ section_text = "\n".join(section).rstrip("\n") + "\n"
488
+
489
+ # --- write it -----------------------------------------------------------------------------------
490
+
491
+
492
+ def slugify(text):
493
+ s = re.sub(r"[^A-Za-z0-9]+", "-", str(text)).strip("-").lower()
494
+ return (s or "codebase")[:48]
495
+
496
+
497
+ if RAPPORT:
498
+ # `existing` was read above, before the section was assembled.
499
+ body = existing.rstrip("\n") + "\n\n---\n\n" + section_text
500
+ try:
501
+ with open(RAPPORT, "w", encoding="utf-8") as fh:
502
+ fh.write(body)
503
+ except OSError as exc:
504
+ sys.stderr.write("Error: could not write --rapport document: %s\n" % exc)
505
+ sys.exit(4)
506
+ rapport_path = RAPPORT
507
+ rapport_mode = "appended"
508
+ else:
509
+ base = slugify(os.path.basename(root_absolute.rstrip("/")) or root)
510
+ stem = "uncharted-onboard-cap-%s-%s" % (base, TS)
511
+ # Collision suffix rather than overwrite, matching run-engine.sh: an existing cap record is
512
+ # evidence of an earlier decision and must not be destroyed by a later one.
513
+ path = os.path.join(OUT_DIR, stem + ".md")
514
+ n = 2
515
+ while os.path.exists(path):
516
+ path = os.path.join(OUT_DIR, "%s-%d.md" % (stem, n))
517
+ n += 1
518
+ header = "# Onboard Subsystem Cap — %s\n\n" % label
519
+ header += ("_Generated by `apply-subsystem-cap.sh` for `/uncharted onboard`. This document is "
520
+ "the durable record of which discovered subsystems became backfilled epics and "
521
+ "which did not._\n\n")
522
+ try:
523
+ with open(path, "w", encoding="utf-8") as fh:
524
+ fh.write(header + section_text)
525
+ except OSError as exc:
526
+ sys.stderr.write("Error: could not write cap record: %s\n" % exc)
527
+ sys.exit(4)
528
+ rapport_path = path
529
+ rapport_mode = "created"
530
+
531
+ # --- report -------------------------------------------------------------------------------------
532
+
533
+ result = {
534
+ "script": "apply-subsystem-cap.sh",
535
+ "version": 1,
536
+ "cap": CAP,
537
+ "default_cap": int(DEFAULT_CAP_S),
538
+ "root": root,
539
+ "root_absolute": root_absolute or None,
540
+ "candidate_count": len(candidates),
541
+ "kept_count": len(kept),
542
+ "dropped_count": len(dropped),
543
+ "capped": capped,
544
+ "rapport": rapport_path,
545
+ "rapport_mode": rapport_mode,
546
+ "kept": kept,
547
+ "dropped": dropped,
548
+ "notices": notices,
549
+ }
550
+
551
+ if JSON_OUT:
552
+ try:
553
+ with open(JSON_OUT, "w", encoding="utf-8") as fh:
554
+ json.dump(result, fh, indent=2, ensure_ascii=False)
555
+ fh.write("\n")
556
+ except OSError as exc:
557
+ sys.stderr.write("Error: could not write --json-out file: %s\n" % exc)
558
+ sys.exit(4)
559
+
560
+ json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
561
+ sys.stdout.write("\n")
562
+
563
+ if capped:
564
+ sys.stderr.write(
565
+ "Notice: %d of %d discovered subsystem(s) were dropped by the cap of %d. Every one is "
566
+ "named in %s\n" % (len(dropped), len(candidates), CAP, rapport_path))
567
+ for n in notices:
568
+ sys.stderr.write("Notice: %s\n" % n)
569
+ PY
570
+ )
571
+
572
+ python3 -c "$PY_SRC" \
573
+ "$INPUT" "$CAP" "$RAPPORT" "$OUT_DIR" "$JSON_OUT" "$LABEL" "$ISO_TS" "$DEFAULT_CAP"