@jenga-ai/agent 3.5.0 → 4.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.
- package/README.md +85 -78
- package/agents/developer.md +1 -1
- package/agents/scrum-master.md +20 -2
- package/agents/tester.md +3 -3
- package/hooks/on_session_end.sh +5 -5
- package/lib/generate-agent-context.js +2 -2
- package/lib/generate-copilot-hooks.js +1 -1
- package/lib/generate-skill-allow-list.js +79 -8
- package/lib/mirror.js +1 -1
- package/lib/postinstall-manifest.js +1 -1
- package/lib/skill-allow-list.json +2 -2
- package/mcp/help/index.js +8 -17
- package/mcp/help/scan.js +73 -0
- package/package.json +5 -1
- package/project/app/api/lib/resolve-project-root.js +1 -1
- package/project/app/api/parsers/knowledge-graph.js +100 -9
- package/project/app/api/routes/health.js +36 -0
- package/project/app/api/scripts/capture-snapshot.js +9 -6
- package/project/app/package.json +4 -0
- package/project/app/ui/dist/assets/index-BADc5mmH.css +1 -0
- package/project/app/ui/dist/assets/index-C3oiuli_.js +104 -0
- package/project/app/ui/dist/index.html +2 -2
- package/project/app/ui/package.json +4 -0
- package/project/app/ui/scripts/build-snapshot-html.cjs +63 -2
- package/scripts/acquire-concurrency-slot.sh +35 -5
- package/scripts/apply-j-prefix.sh +46 -5
- package/scripts/build-pages-site.sh +1 -1
- package/scripts/check-public-playbook-steps.sh +158 -52
- package/scripts/check-publicignore-match.sh +2 -2
- package/scripts/compute-deploy-reconcile.sh +5 -5
- package/scripts/delete-bare-skill-dirs.sh +329 -0
- package/scripts/generate-legacy-shipped-paths.js +2 -2
- package/scripts/idea_manager.sh +273 -3
- package/scripts/mark-deployed.sh +2 -2
- package/scripts/populate-knowledge-graph.entity-resolution.test.js +254 -0
- package/scripts/populate-knowledge-graph.js +213 -5
- package/scripts/populate-knowledge-graph.staleness.test.js +130 -0
- package/scripts/postinstall.js +1 -1
- package/scripts/release-concurrency-slot.sh +34 -4
- package/scripts/render-ranked-list.sh +270 -0
- package/scripts/repoint-dead-bare-path-prose.py +81 -0
- package/scripts/repoint-skill-refs.sh +539 -0
- package/scripts/rewrite-stale-skill-preambles.py +188 -0
- package/scripts/strip-polyfill-frontmatter.py +166 -0
- package/scripts/todo_manager.sh +16 -1
- package/scripts/validate-typed-object.sh +750 -0
- package/scripts/verify-legacy-seed-reconcile.sh +10 -10
- package/scripts/verify-postinstall-reconcile.sh +7 -7
- package/scripts/write-context-digest.sh +1 -1
- package/skills/j-brainstorm/SKILL.md +3 -4
- package/skills/j-btw/SKILL.md +3 -4
- package/skills/j-clearify/SKILL.md +5 -6
- package/skills/j-close-story/SKILL.md +11 -12
- package/skills/j-close-story/scripts/check-privatized.sh +4 -4
- package/skills/j-close-story/scripts/check-story-closeable.sh +11 -4
- package/skills/j-commit/SKILL.md +3 -4
- package/skills/j-continue/SKILL.md +5 -6
- package/skills/j-deep-dive/SKILL.md +3 -4
- package/skills/j-distribute/CONFIG_SCHEMA.md +82 -5
- package/skills/j-distribute/SKILL.md +3 -4
- package/skills/j-do/SKILL.md +100 -18
- package/skills/j-doc/SKILL.md +3 -4
- package/skills/j-doc-sync/SKILL.md +4 -4
- package/skills/j-dooo/SKILL.md +6 -15
- package/skills/j-error/SKILL.md +3 -4
- package/skills/j-evaluate/SKILL.md +3 -4
- package/skills/j-examplify/SKILL.md +3 -4
- package/skills/j-gitignore/SKILL.md +157 -0
- package/skills/j-gitignore/assets/jenga-paths.txt +50 -0
- package/skills/j-gitignore/scripts/_catalog.sh +105 -0
- package/skills/j-gitignore/scripts/audit-gitignore.sh +194 -0
- package/skills/j-gitignore/scripts/repair-gitignore.sh +226 -0
- package/skills/j-gitignore/scripts/untrack-jenga-files.sh +210 -0
- package/skills/j-help/SKILL.md +3 -4
- package/skills/j-idea/SKILL.md +80 -9
- package/skills/j-idea/assets/idea_template.md +1 -1
- package/skills/j-improve/SKILL.md +4 -5
- package/skills/j-init/SKILL.md +23 -14
- package/skills/j-init/assets/scope-thresholds_template.json +5 -2
- package/skills/j-init/scripts/apply-scaffold-visibility.sh +9 -7
- package/skills/j-init/scripts/init.sh +4 -4
- package/skills/j-jbp/SKILL.md +3 -4
- package/skills/j-lgtm/SKILL.md +3 -4
- package/skills/j-pi-plan/SKILL.md +3 -4
- package/skills/j-playbook/SKILL.md +1 -1
- package/skills/j-proceed/SKILL.md +3 -4
- package/skills/j-publish/SKILL.md +4 -5
- package/skills/j-publish/adapters/npm-ci.md +6 -1
- package/skills/j-publish/adapters/npm.md +1 -1
- package/skills/j-publish/scripts/generate_release_notes.sh +1 -1
- package/skills/j-publish/scripts/npm_stage_inspect.sh +61 -0
- package/skills/j-publish/scripts/run_gates.sh +1 -1
- package/skills/j-reconcile/SKILL.md +40 -7
- package/skills/j-reconcile/assets/report_format.md +11 -0
- package/skills/j-reconcile/scripts/detect-unlinked-code.sh +13 -13
- package/skills/j-reconcile-origin/SKILL.md +3 -4
- package/skills/j-redo/SKILL.md +4 -5
- package/skills/j-skillify/SKILL.md +3 -4
- package/skills/j-spinoff/SKILL.md +4 -5
- package/skills/j-status/SKILL.md +18 -4
- package/skills/j-todo/SKILL.md +44 -5
- package/skills/j-todo/scripts/argument-is-not-ranked-list.sh +92 -0
- package/skills/j-todo/scripts/argument-is-ranked-list.sh +78 -0
- package/skills/j-uncharted/SKILL.md +252 -13
- package/skills/j-uncharted/scripts/detect-dependencies.sh +80 -22
- package/skills/j-uncharted/scripts/detect-tests.sh +1 -1
- package/skills/j-uncharted/scripts/diff-since-baseline.sh +600 -0
- package/skills/j-uncharted/scripts/elicitation-state.sh +1 -1
- package/skills/j-uncharted/scripts/find-scan-baseline.sh +545 -0
- package/skills/j-uncharted/scripts/run-engine.sh +36 -2
- package/skills/j-uncharted/scripts/validate-proposed-items.sh +1 -1
- package/skills/j-uncharted/scripts/write-scan-record.sh +361 -0
- package/skills/j-wtf/SKILL.md +4 -5
- package/skills/jenga/SKILL.md +106 -11
- package/skills/jenga/playbooks/board-hygiene.json +32 -0
- package/skills/jenga/playbooks/schema.json +73 -6
- package/skills/jenga/playbooks/understand-then-commit.json +19 -0
- package/skills/jenga/scripts/load-nl-catalog.js +5 -2
- package/skills/jenga/scripts/load-nl-catalog.sh +1 -1
- package/skills/jenga/scripts/load-playbooks.sh +290 -5
- package/skills/jenga/scripts/match-playbook.sh +4 -4
- package/skills/jenga/scripts/run-playbook-step.sh +267 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +14 -1
- package/templates/SKILL_TEMPLATE.md +12 -0
- package/templates/permission-levels/level-1-locked.json +1 -1
- package/templates/permission-levels/level-2-guarded.json +1 -1
- package/templates/permission-levels/level-3-standard.json +1 -1
- package/templates/permission-levels/level-4-elevated.json +2 -2
- package/templates/permission-levels/level-5-unrestricted.json +2 -2
- package/templates/playbook-types.json +40 -6
- package/project/app/ui/dist/assets/index-7fj-vllY.js +0 -104
- package/project/app/ui/dist/assets/index-CdK3Qrep.css +0 -1
- package/scripts/audit-twin-divergence.sh +0 -625
- package/scripts/generate-j-alias.sh +0 -333
- package/skills/j-dev-done/SKILL.md +0 -53
- package/skills/j-dev-done/scripts/classify-commit-outcome.sh +0 -114
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# diff-since-baseline.sh — classify candidates as unchanged/changed/new/removed since a baseline
|
|
3
|
+
#
|
|
4
|
+
# Usage: diff-since-baseline.sh [options] [<baseline.json>]
|
|
5
|
+
# find-scan-baseline.sh <root> | diff-since-baseline.sh [options]
|
|
6
|
+
# diff-since-baseline.sh --help
|
|
7
|
+
#
|
|
8
|
+
# `refresh` mode (E40_S07_T04, not yet implemented) needs to know what actually changed since a
|
|
9
|
+
# baseline before it decides what to do about it. This script is the "what changed" step, and
|
|
10
|
+
# only that step — it does not locate the baseline (find-scan-baseline.sh, E40_S07_T02, already
|
|
11
|
+
# implemented) and does not decide what `refresh` does with the classified result (that belongs
|
|
12
|
+
# in skills/j-uncharted/SKILL.md's refresh mode section, E40_S07_T04). It is read-only: it never
|
|
13
|
+
# writes to the analysed codebase or the board.
|
|
14
|
+
#
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# INPUT
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# BASELINE (required): find-scan-baseline.sh's JSON output, from a file argument or from stdin
|
|
19
|
+
# (`-`, or no argument) — the same input contract write-scan-record.sh / apply-subsystem-cap.sh
|
|
20
|
+
# use. Two shapes are accepted, matching find-scan-baseline.sh's own two success tiers exactly
|
|
21
|
+
# (read that script's header comment for the authoritative description):
|
|
22
|
+
#
|
|
23
|
+
# Tier 1 ("source": "scan-record") — has a `candidates[]` array ({path, files, lines} each),
|
|
24
|
+
# `baseline_commit`, and `root`. Both `new` and `removed` are computed by set-difference
|
|
25
|
+
# against the fresh discovery run below.
|
|
26
|
+
#
|
|
27
|
+
# Tier 2 ("source": "inferred") — has NO `candidates[]` array at all (only `baseline_commit`,
|
|
28
|
+
# `root`, and `inferred_from[]`). There is nothing to diff the fresh candidate set's
|
|
29
|
+
# MEMBERSHIP against, so `new` and `removed` are NOT computed for a Tier 2 baseline — both
|
|
30
|
+
# stay empty arrays by construction, always, never an error. Every fresh-discovery candidate
|
|
31
|
+
# is instead classified as EITHER `unchanged` or `changed`, via the identical git-diff-since-
|
|
32
|
+
# `baseline_commit` check Tier 1 uses. This is documented, intentional behaviour, not a
|
|
33
|
+
# degraded fallback — see the task's own acceptance criteria (E40_S07_T03).
|
|
34
|
+
#
|
|
35
|
+
# A Tier 3 "no baseline" result (find-scan-baseline.sh exit 3) is the CALLER's problem, not this
|
|
36
|
+
# script's — this script always receives an already-found baseline. Passing it one anyway (e.g. a
|
|
37
|
+
# hand-built JSON object with neither shape) is an input error (exit 2).
|
|
38
|
+
#
|
|
39
|
+
# FRESH CANDIDATE SET: by default this script invokes discover-subsystems.sh itself (no extra
|
|
40
|
+
# flags — a plain default-options pass) against the baseline's own `root` field, resolved
|
|
41
|
+
# relative to the engine repo root (see REPO ANCHORING below). Pass `--fresh FILE` (or `--fresh -`
|
|
42
|
+
# for stdin) to supply an already-computed discover-subsystems.sh report instead — useful when a
|
|
43
|
+
# caller already ran discovery once in the same pass and wants to reuse it rather than pay for a
|
|
44
|
+
# second walk. Only ONE of `<baseline.json>` / `--fresh` may be stdin (`-`) at a time; asking for
|
|
45
|
+
# both is a usage error.
|
|
46
|
+
#
|
|
47
|
+
# Candidates are matched between baseline and fresh discovery BY `path` ONLY — never by `rank` or
|
|
48
|
+
# `score`. apply-subsystem-cap.sh's `kept`/`dropped` entries recompute rank/score fresh every run,
|
|
49
|
+
# and those can shift between runs with zero material change underneath, so they are never a
|
|
50
|
+
# valid diff key. A directory rename is deliberately NOT detected as a rename — it classifies as
|
|
51
|
+
# a `removed` (old path) plus a `new` (new path). Rename detection is out of scope.
|
|
52
|
+
#
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# REPO ANCHORING
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# `REPO_ROOT` is resolved from THIS SCRIPT's own location (`git -C <script-dir> rev-parse
|
|
57
|
+
# --show-toplevel`), exactly like every sibling script (find-scan-baseline.sh,
|
|
58
|
+
# write-scan-record.sh, apply-subsystem-cap.sh) — the engine repo owns this lookup even when the
|
|
59
|
+
# analysed root lives elsewhere. `baseline_commit` is understood to be a commit reachable from
|
|
60
|
+
# this same repo's history (write-scan-record.sh writes it as the ENGINE repo's HEAD, not the
|
|
61
|
+
# analysed root's — see that script's own header), so the git-diff check below always runs
|
|
62
|
+
# against REPO_ROOT. The baseline's `root` field is repo-relative to REPO_ROOT; a candidate's own
|
|
63
|
+
# `path` field is relative to `root`. The full repo-relative path used for both `discover-
|
|
64
|
+
# subsystems.sh`'s target directory and the `git diff` path is therefore built by joining the two
|
|
65
|
+
# (see FULL PATH RESOLUTION below).
|
|
66
|
+
#
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# CLASSIFICATION
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# unchanged — same path present in both candidate sets, and
|
|
71
|
+
# `git diff --name-only <baseline_commit>..HEAD -- <full-path>` is EMPTY.
|
|
72
|
+
# changed — same path present in both candidate sets, and that git diff is NON-EMPTY.
|
|
73
|
+
# new — a candidate path present in the fresh discovery run with no baseline counterpart.
|
|
74
|
+
# Tier 2: always empty.
|
|
75
|
+
# removed — a candidate path present in the baseline with no fresh-run counterpart.
|
|
76
|
+
# Tier 2: always empty.
|
|
77
|
+
#
|
|
78
|
+
# Two edge cases in the git-diff check itself, both handled explicitly rather than silently:
|
|
79
|
+
# - `baseline_commit` is `null` (write-scan-record.sh's own documented case: the engine repo had
|
|
80
|
+
# zero commits at scan time). There is nothing to diff against, so EVERY matched/fresh
|
|
81
|
+
# candidate is conservatively classified `changed` (never guessed `unchanged`), with a notice
|
|
82
|
+
# explaining why.
|
|
83
|
+
# - `baseline_commit` does not resolve to a real commit in this repo's history (e.g. squashed or
|
|
84
|
+
# rebased away since the baseline was captured). Checked ONCE up front via
|
|
85
|
+
# `git cat-file -e <sha>^{commit}`; on failure this is an INPUT error (exit 2) — the baseline
|
|
86
|
+
# itself is stale/invalid, not something this script can safely guess around per-candidate.
|
|
87
|
+
#
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# FULL PATH RESOLUTION
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Both `root` (from the baseline) and each candidate's `path` are normalized the same way
|
|
92
|
+
# find-scan-baseline.sh's own `norm()` does: strip a trailing slash; collapse "", ".", "./" to the
|
|
93
|
+
# canonical "." meaning "the whole repository" / "the whole root". The full repo-relative path is:
|
|
94
|
+
# root == "." -> candidate path (or "." if the candidate path is also ".")
|
|
95
|
+
# candidate path == "." -> root itself
|
|
96
|
+
# otherwise -> "<root>/<candidate path>"
|
|
97
|
+
#
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# OPTIONS
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# --fresh FILE Use this discover-subsystems.sh JSON report as the fresh
|
|
102
|
+
# candidate set instead of running discover-subsystems.sh.
|
|
103
|
+
# File path, or "-" to read from stdin.
|
|
104
|
+
# --root ROOT Override the root passed to discover-subsystems.sh when
|
|
105
|
+
# --fresh is not given. Default: the baseline's own `root` field.
|
|
106
|
+
# --discover-subsystems-bin PATH Path to discover-subsystems.sh. Default: the sibling script
|
|
107
|
+
# in this same directory. Override for testing.
|
|
108
|
+
# --json-out FILE Also write the same JSON this script prints on stdout to this
|
|
109
|
+
# exact path. Not written unless requested. Pre-flighted before
|
|
110
|
+
# any real work, same fail-fast contract as every sibling script.
|
|
111
|
+
# -h, --help Show this help and exit 0.
|
|
112
|
+
#
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# OUTPUT
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# stdout : on success, a single JSON object, one line, nothing else — so
|
|
117
|
+
# `RESULT=$(diff-since-baseline.sh ...)` is safe. Shape:
|
|
118
|
+
#
|
|
119
|
+
# {
|
|
120
|
+
# "script": "diff-since-baseline.sh",
|
|
121
|
+
# "version": 1,
|
|
122
|
+
# "root": "<repo-relative root, from the baseline>",
|
|
123
|
+
# "baseline_source": "scan-record" | "inferred",
|
|
124
|
+
# "baseline_commit": "<short SHA>" | null,
|
|
125
|
+
# "unchanged": [ { "path", "files", "lines" }, ... ],
|
|
126
|
+
# "changed": [ { "path", "files", "lines", "changed_files": [ "...", ... ] }, ... ],
|
|
127
|
+
# "new": [ { "path", "files", "lines" }, ... ], // Tier 2: always []
|
|
128
|
+
# "removed": [ { "path", "files", "lines" }, ... ], // Tier 2: always []
|
|
129
|
+
# "unchanged_count": <int>, "changed_count": <int>, "new_count": <int>, "removed_count": <int>,
|
|
130
|
+
# "notices": [ "<non-fatal diagnostic>", ... ]
|
|
131
|
+
# }
|
|
132
|
+
#
|
|
133
|
+
# `files`/`lines` come from whichever side actually has them for that entry (the fresh
|
|
134
|
+
# discovery run for `unchanged`/`changed`/`new`, the baseline for `removed`). Every entry
|
|
135
|
+
# carries at minimum `path`. `changed_files` is the (capped at 50) list of repo-relative
|
|
136
|
+
# paths `git diff --name-only` reported under that candidate, for auditability — never
|
|
137
|
+
# silently dropped, matching this script family's house style.
|
|
138
|
+
#
|
|
139
|
+
# stderr : notices and diagnostics only, never document/record content.
|
|
140
|
+
#
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# EXIT CODES
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# 0 — success; classification computed and printed as JSON on stdout
|
|
145
|
+
# 1 — usage error: unknown flag, missing value, more than one positional baseline argument,
|
|
146
|
+
# both the baseline and --fresh set to stdin at once
|
|
147
|
+
# 2 — input error: baseline JSON missing/unreadable/unparseable or not a recognised
|
|
148
|
+
# find-scan-baseline.sh shape; --fresh JSON missing/unreadable/unparseable or not a
|
|
149
|
+
# discover-subsystems.sh report (no "candidates" array); discover-subsystems.sh itself
|
|
150
|
+
# failed or produced unparseable output; baseline_commit does not resolve to a real commit
|
|
151
|
+
# in this repo's history
|
|
152
|
+
# 4 — write failure: --json-out's directory could not be found or is not writable
|
|
153
|
+
#
|
|
154
|
+
# NOTE: `3` is deliberately NOT reused here. It is reserved by find-scan-baseline.sh for its own
|
|
155
|
+
# "no baseline found" case, and that is a distinct concern from anything this script does — this
|
|
156
|
+
# script always receives an already-found baseline as input.
|
|
157
|
+
#
|
|
158
|
+
# Examples:
|
|
159
|
+
# find-scan-baseline.sh skills/j-uncharted | diff-since-baseline.sh
|
|
160
|
+
# diff-since-baseline.sh /tmp/baseline.json
|
|
161
|
+
# discover-subsystems.sh skills/j-uncharted > /tmp/fresh.json
|
|
162
|
+
# find-scan-baseline.sh skills/j-uncharted | diff-since-baseline.sh --fresh /tmp/fresh.json
|
|
163
|
+
#
|
|
164
|
+
# Requires: bash, git, python3. jq is NOT required — JSON is parsed and emitted by python3.
|
|
165
|
+
|
|
166
|
+
set -euo pipefail
|
|
167
|
+
|
|
168
|
+
FRESH=""
|
|
169
|
+
ROOT_OVERRIDE=""
|
|
170
|
+
DISCOVER_BIN=""
|
|
171
|
+
JSON_OUT=""
|
|
172
|
+
BASELINE_INPUT=""
|
|
173
|
+
|
|
174
|
+
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
|
175
|
+
|
|
176
|
+
usage() {
|
|
177
|
+
cat <<EOF
|
|
178
|
+
Usage: $(basename "$0") [options] [<baseline.json>]
|
|
179
|
+
|
|
180
|
+
Classify each candidate as unchanged/changed/new/removed since a baseline: consumes
|
|
181
|
+
find-scan-baseline.sh's JSON output (Tier 1 "scan-record" or Tier 2 "inferred") plus a fresh
|
|
182
|
+
discover-subsystems.sh run, matches candidates by path (never rank/score), and reports which
|
|
183
|
+
paths are unchanged, changed (via git diff since baseline_commit), newly appeared, or removed.
|
|
184
|
+
Tier 2 baselines have no candidates[] to diff membership against, so new/removed always stay
|
|
185
|
+
empty arrays for them -- documented, intentional behaviour, not an error.
|
|
186
|
+
|
|
187
|
+
Arguments:
|
|
188
|
+
<baseline.json> find-scan-baseline.sh output. Omit, or pass "-", to read stdin.
|
|
189
|
+
|
|
190
|
+
Options:
|
|
191
|
+
--fresh FILE Use this discover-subsystems.sh report as the fresh candidate
|
|
192
|
+
set instead of running discover-subsystems.sh. "-" for stdin.
|
|
193
|
+
--root ROOT Root to pass to discover-subsystems.sh when --fresh is not
|
|
194
|
+
given (default: the baseline's own "root" field)
|
|
195
|
+
--discover-subsystems-bin PATH Path to discover-subsystems.sh (default: sibling script)
|
|
196
|
+
--json-out FILE Also write the result JSON to this exact path
|
|
197
|
+
-h, --help Show this help and exit
|
|
198
|
+
|
|
199
|
+
Exit codes: 0 success, 1 usage error, 2 input error, 4 write failure. (3 is reserved by
|
|
200
|
+
find-scan-baseline.sh and is never reused here.)
|
|
201
|
+
EOF
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
die_usage() {
|
|
205
|
+
echo "Error: $1" >&2
|
|
206
|
+
echo >&2
|
|
207
|
+
usage >&2
|
|
208
|
+
exit 1
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
require_value() {
|
|
212
|
+
# require_value <flag> <remaining-arg-count>
|
|
213
|
+
[ "$2" -ge 2 ] || die_usage "$1 requires a value"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# Argument parsing
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
while [ "$#" -gt 0 ]; do
|
|
221
|
+
case "$1" in
|
|
222
|
+
--fresh) require_value "--fresh" "$#"; FRESH="$2"; shift 2 ;;
|
|
223
|
+
--fresh=*) FRESH="${1#*=}"; shift ;;
|
|
224
|
+
--root) require_value "--root" "$#"; ROOT_OVERRIDE="$2"; shift 2 ;;
|
|
225
|
+
--root=*) ROOT_OVERRIDE="${1#*=}"; shift ;;
|
|
226
|
+
--discover-subsystems-bin) require_value "--discover-subsystems-bin" "$#"; DISCOVER_BIN="$2"; shift 2 ;;
|
|
227
|
+
--discover-subsystems-bin=*) DISCOVER_BIN="${1#*=}"; shift ;;
|
|
228
|
+
--json-out) require_value "--json-out" "$#"; JSON_OUT="$2"; shift 2 ;;
|
|
229
|
+
--json-out=*) JSON_OUT="${1#*=}"; shift ;;
|
|
230
|
+
-h|--help) usage; exit 0 ;;
|
|
231
|
+
--)
|
|
232
|
+
shift
|
|
233
|
+
[ "$#" -le 1 ] || die_usage "at most one <baseline.json> argument is accepted"
|
|
234
|
+
[ "$#" -eq 0 ] || BASELINE_INPUT="$1"
|
|
235
|
+
break ;;
|
|
236
|
+
-)
|
|
237
|
+
BASELINE_INPUT="-"; shift ;;
|
|
238
|
+
-*)
|
|
239
|
+
die_usage "unknown option \"$1\"" ;;
|
|
240
|
+
*)
|
|
241
|
+
[ -z "$BASELINE_INPUT" ] || die_usage "at most one <baseline.json> argument is accepted (got \"$BASELINE_INPUT\" and \"$1\")"
|
|
242
|
+
BASELINE_INPUT="$1"; shift ;;
|
|
243
|
+
esac
|
|
244
|
+
done
|
|
245
|
+
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
# Input resolution
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
if [ -z "$BASELINE_INPUT" ] || [ "$BASELINE_INPUT" = "-" ]; then
|
|
251
|
+
BASELINE_INPUT="-"
|
|
252
|
+
else
|
|
253
|
+
if [ ! -e "$BASELINE_INPUT" ]; then
|
|
254
|
+
echo "Error: baseline input does not exist: $BASELINE_INPUT" >&2
|
|
255
|
+
echo " Expected find-scan-baseline.sh JSON output." >&2
|
|
256
|
+
exit 2
|
|
257
|
+
fi
|
|
258
|
+
if [ ! -f "$BASELINE_INPUT" ] || [ ! -r "$BASELINE_INPUT" ]; then
|
|
259
|
+
echo "Error: baseline input is not a readable file: $BASELINE_INPUT" >&2
|
|
260
|
+
exit 2
|
|
261
|
+
fi
|
|
262
|
+
BASELINE_INPUT=$(cd -- "$(dirname -- "$BASELINE_INPUT")" && pwd -P)/$(basename -- "$BASELINE_INPUT")
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
if [ -n "$FRESH" ] && [ "$FRESH" != "-" ]; then
|
|
266
|
+
if [ ! -e "$FRESH" ]; then
|
|
267
|
+
echo "Error: --fresh input does not exist: $FRESH" >&2
|
|
268
|
+
exit 2
|
|
269
|
+
fi
|
|
270
|
+
if [ ! -f "$FRESH" ] || [ ! -r "$FRESH" ]; then
|
|
271
|
+
echo "Error: --fresh input is not a readable file: $FRESH" >&2
|
|
272
|
+
exit 2
|
|
273
|
+
fi
|
|
274
|
+
FRESH=$(cd -- "$(dirname -- "$FRESH")" && pwd -P)/$(basename -- "$FRESH")
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
if [ "$BASELINE_INPUT" = "-" ] && [ "$FRESH" = "-" ]; then
|
|
278
|
+
die_usage "only one of <baseline.json> / --fresh may be stdin (\"-\") at a time"
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
# Repo anchoring & output pre-flight — before any real work, same fail-fast contract as every
|
|
283
|
+
# sibling script.
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
REPO_ROOT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)
|
|
287
|
+
[ -n "$REPO_ROOT" ] || REPO_ROOT="$(pwd -P)"
|
|
288
|
+
|
|
289
|
+
if [ -z "$DISCOVER_BIN" ]; then
|
|
290
|
+
DISCOVER_BIN="$SCRIPT_DIR/discover-subsystems.sh"
|
|
291
|
+
fi
|
|
292
|
+
if [ -z "$FRESH" ]; then
|
|
293
|
+
if [ ! -x "$DISCOVER_BIN" ]; then
|
|
294
|
+
echo "Error: discover-subsystems.sh not found or not executable: $DISCOVER_BIN" >&2
|
|
295
|
+
echo " Pass --fresh to supply an already-computed report instead." >&2
|
|
296
|
+
exit 2
|
|
297
|
+
fi
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
if [ -n "$JSON_OUT" ]; then
|
|
301
|
+
JSON_OUT_DIR=$(dirname -- "$JSON_OUT")
|
|
302
|
+
[ -d "$JSON_OUT_DIR" ] || { echo "Error: --json-out directory does not exist: $JSON_OUT_DIR" >&2; exit 4; }
|
|
303
|
+
[ -w "$JSON_OUT_DIR" ] || { echo "Error: --json-out directory is not writable: $JSON_OUT_DIR" >&2; exit 4; }
|
|
304
|
+
JSON_OUT=$(cd -- "$JSON_OUT_DIR" && pwd -P)/$(basename -- "$JSON_OUT")
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
# Read the baseline, obtain the fresh candidate set (running discover-subsystems.sh ourselves
|
|
309
|
+
# unless --fresh was given), classify, emit.
|
|
310
|
+
#
|
|
311
|
+
# Captured into a variable and run with `python3 -c`, exactly as find-scan-baseline.sh /
|
|
312
|
+
# write-scan-record.sh / apply-subsystem-cap.sh do: a `python3 - <<PY` heredoc would occupy
|
|
313
|
+
# python's stdin, and this script must itself be able to read stdin for the baseline.
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
PY_SRC=$(cat <<'PY'
|
|
317
|
+
import json
|
|
318
|
+
import os
|
|
319
|
+
import subprocess
|
|
320
|
+
import sys
|
|
321
|
+
|
|
322
|
+
(REPO_ROOT, BASELINE_INPUT, FRESH, ROOT_OVERRIDE, DISCOVER_BIN, JSON_OUT) = sys.argv[1:7]
|
|
323
|
+
|
|
324
|
+
notices = []
|
|
325
|
+
CHANGED_FILES_CAP = 50
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def norm(path):
|
|
329
|
+
"""Same normalization as find-scan-baseline.sh's own norm(): strip a trailing slash,
|
|
330
|
+
collapse "", ".", "./" to the canonical "." meaning 'the whole repository'."""
|
|
331
|
+
p = (path or "").strip()
|
|
332
|
+
if p in ("", ".", "./"):
|
|
333
|
+
return "."
|
|
334
|
+
return p.rstrip("/")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def full_path(root, cand_path):
|
|
338
|
+
root = norm(root)
|
|
339
|
+
cand_path = norm(cand_path)
|
|
340
|
+
if root == ".":
|
|
341
|
+
return cand_path
|
|
342
|
+
if cand_path == ".":
|
|
343
|
+
return root
|
|
344
|
+
return root + "/" + cand_path
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# --- read a JSON document from a file path or "-" (stdin) -------------------------------------
|
|
348
|
+
|
|
349
|
+
def read_json(source, label):
|
|
350
|
+
try:
|
|
351
|
+
if source == "-":
|
|
352
|
+
raw = sys.stdin.read()
|
|
353
|
+
origin = "stdin"
|
|
354
|
+
else:
|
|
355
|
+
with open(source, "r", encoding="utf-8") as fh:
|
|
356
|
+
raw = fh.read()
|
|
357
|
+
origin = source
|
|
358
|
+
except OSError as exc:
|
|
359
|
+
sys.stderr.write("Error: could not read %s: %s\n" % (label, exc))
|
|
360
|
+
sys.exit(2)
|
|
361
|
+
|
|
362
|
+
if not raw.strip():
|
|
363
|
+
sys.stderr.write("Error: %s is empty (%s).\n" % (label, origin))
|
|
364
|
+
sys.exit(2)
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
doc = json.loads(raw)
|
|
368
|
+
except ValueError as exc:
|
|
369
|
+
sys.stderr.write("Error: %s is not valid JSON (%s): %s\n" % (label, origin, exc))
|
|
370
|
+
sys.exit(2)
|
|
371
|
+
|
|
372
|
+
if not isinstance(doc, dict):
|
|
373
|
+
sys.stderr.write("Error: %s did not contain a JSON object (%s).\n" % (label, origin))
|
|
374
|
+
sys.exit(2)
|
|
375
|
+
|
|
376
|
+
return doc
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# --- baseline -----------------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
baseline = read_json(BASELINE_INPUT, "baseline input")
|
|
382
|
+
|
|
383
|
+
source = baseline.get("source")
|
|
384
|
+
if source not in ("scan-record", "inferred"):
|
|
385
|
+
sys.stderr.write(
|
|
386
|
+
"Error: baseline input is not a recognised find-scan-baseline.sh shape "
|
|
387
|
+
"(expected \"source\": \"scan-record\" or \"inferred\", got %r).\n" % (source,)
|
|
388
|
+
)
|
|
389
|
+
sys.exit(2)
|
|
390
|
+
|
|
391
|
+
root = norm(baseline.get("root", "."))
|
|
392
|
+
baseline_commit = baseline.get("baseline_commit")
|
|
393
|
+
|
|
394
|
+
if source == "scan-record":
|
|
395
|
+
baseline_candidates = baseline.get("candidates")
|
|
396
|
+
if not isinstance(baseline_candidates, list):
|
|
397
|
+
sys.stderr.write(
|
|
398
|
+
"Error: Tier 1 (\"source\": \"scan-record\") baseline is missing its "
|
|
399
|
+
"\"candidates\" array.\n"
|
|
400
|
+
)
|
|
401
|
+
sys.exit(2)
|
|
402
|
+
else:
|
|
403
|
+
# Tier 2 ("inferred"): no candidates[] by design -- new/removed are never computed for it.
|
|
404
|
+
baseline_candidates = None
|
|
405
|
+
notices.append(
|
|
406
|
+
"Tier 2 (\"inferred\") baseline has no candidates[] -- \"new\" and \"removed\" cannot "
|
|
407
|
+
"be computed and are reported as empty arrays; every fresh candidate is classified "
|
|
408
|
+
"unchanged/changed only."
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
baseline_by_path = {}
|
|
412
|
+
if baseline_candidates is not None:
|
|
413
|
+
for entry in baseline_candidates:
|
|
414
|
+
if not isinstance(entry, dict) or "path" not in entry:
|
|
415
|
+
notices.append("skipped a baseline candidate entry with no \"path\": %r" % (entry,))
|
|
416
|
+
continue
|
|
417
|
+
baseline_by_path[norm(entry["path"])] = entry
|
|
418
|
+
|
|
419
|
+
# --- fresh candidate set --------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
if FRESH:
|
|
422
|
+
fresh_report = read_json(FRESH, "--fresh input")
|
|
423
|
+
else:
|
|
424
|
+
target_root = ROOT_OVERRIDE if ROOT_OVERRIDE else root
|
|
425
|
+
if norm(target_root) == ".":
|
|
426
|
+
discover_target = REPO_ROOT
|
|
427
|
+
else:
|
|
428
|
+
discover_target = os.path.join(REPO_ROOT, norm(target_root))
|
|
429
|
+
try:
|
|
430
|
+
proc = subprocess.run(
|
|
431
|
+
[DISCOVER_BIN, discover_target],
|
|
432
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True,
|
|
433
|
+
)
|
|
434
|
+
except OSError as exc:
|
|
435
|
+
sys.stderr.write("Error: could not run discover-subsystems.sh: %s\n" % exc)
|
|
436
|
+
sys.exit(2)
|
|
437
|
+
if proc.returncode != 0:
|
|
438
|
+
sys.stderr.write(
|
|
439
|
+
"Error: discover-subsystems.sh exited %d for %r:\n%s\n"
|
|
440
|
+
% (proc.returncode, discover_target, proc.stderr)
|
|
441
|
+
)
|
|
442
|
+
sys.exit(2)
|
|
443
|
+
if proc.stderr:
|
|
444
|
+
for line in proc.stderr.splitlines():
|
|
445
|
+
notices.append("discover-subsystems.sh: %s" % line)
|
|
446
|
+
try:
|
|
447
|
+
fresh_report = json.loads(proc.stdout)
|
|
448
|
+
except ValueError as exc:
|
|
449
|
+
sys.stderr.write(
|
|
450
|
+
"Error: discover-subsystems.sh produced unparseable JSON for %r: %s\n"
|
|
451
|
+
% (discover_target, exc)
|
|
452
|
+
)
|
|
453
|
+
sys.exit(2)
|
|
454
|
+
if not isinstance(fresh_report, dict):
|
|
455
|
+
sys.stderr.write(
|
|
456
|
+
"Error: discover-subsystems.sh did not produce a JSON object for %r.\n"
|
|
457
|
+
% (discover_target,)
|
|
458
|
+
)
|
|
459
|
+
sys.exit(2)
|
|
460
|
+
|
|
461
|
+
fresh_candidates = fresh_report.get("candidates")
|
|
462
|
+
if not isinstance(fresh_candidates, list):
|
|
463
|
+
sys.stderr.write(
|
|
464
|
+
"Error: fresh discovery report is missing its \"candidates\" array "
|
|
465
|
+
"(not a discover-subsystems.sh report).\n"
|
|
466
|
+
)
|
|
467
|
+
sys.exit(2)
|
|
468
|
+
|
|
469
|
+
fresh_by_path = {}
|
|
470
|
+
for entry in fresh_candidates:
|
|
471
|
+
if not isinstance(entry, dict) or "path" not in entry:
|
|
472
|
+
notices.append("skipped a fresh candidate entry with no \"path\": %r" % (entry,))
|
|
473
|
+
continue
|
|
474
|
+
fresh_by_path[norm(entry["path"])] = entry
|
|
475
|
+
|
|
476
|
+
# --- resolve baseline_commit once, up front ------------------------------------------------
|
|
477
|
+
|
|
478
|
+
no_commit_available = False
|
|
479
|
+
if baseline_commit is None:
|
|
480
|
+
no_commit_available = True
|
|
481
|
+
notices.append(
|
|
482
|
+
"baseline_commit is null (the engine repo had no commits at scan time, per "
|
|
483
|
+
"write-scan-record.sh's own documented null case) -- git diff cannot be computed; "
|
|
484
|
+
"every matched/fresh candidate is conservatively classified \"changed\"."
|
|
485
|
+
)
|
|
486
|
+
else:
|
|
487
|
+
check = subprocess.run(
|
|
488
|
+
["git", "-C", REPO_ROOT, "cat-file", "-e", "%s^{commit}" % baseline_commit],
|
|
489
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
490
|
+
)
|
|
491
|
+
if check.returncode != 0:
|
|
492
|
+
sys.stderr.write(
|
|
493
|
+
"Error: baseline_commit %r does not resolve to a commit in this repository's "
|
|
494
|
+
"history (rebased/squashed away since the baseline was captured?).\n" % (baseline_commit,)
|
|
495
|
+
)
|
|
496
|
+
sys.exit(2)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def git_diff_touched(path):
|
|
500
|
+
"""Returns the (possibly empty) list of repo-relative files git reports touched under
|
|
501
|
+
`path` since baseline_commit..HEAD. Only called when a real baseline_commit is available."""
|
|
502
|
+
proc = subprocess.run(
|
|
503
|
+
["git", "-C", REPO_ROOT, "diff", "--name-only", "%s..HEAD" % baseline_commit, "--", path],
|
|
504
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True,
|
|
505
|
+
)
|
|
506
|
+
if proc.returncode != 0:
|
|
507
|
+
notices.append(
|
|
508
|
+
"git diff failed for path %r (%s); conservatively classified \"changed\"" % (path, proc.stderr.strip())
|
|
509
|
+
)
|
|
510
|
+
return None # signals "treat as changed, could not verify"
|
|
511
|
+
return [line.strip() for line in proc.stdout.splitlines() if line.strip()]
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def classify(cand_path, source_entry):
|
|
515
|
+
"""Returns (bucket, changed_files_or_None) for a matched (present in both, or Tier-2-only)
|
|
516
|
+
candidate path."""
|
|
517
|
+
if no_commit_available:
|
|
518
|
+
return "changed", None
|
|
519
|
+
path = full_path(root, cand_path)
|
|
520
|
+
touched = git_diff_touched(path)
|
|
521
|
+
if touched is None:
|
|
522
|
+
return "changed", None
|
|
523
|
+
if touched:
|
|
524
|
+
return "changed", touched[:CHANGED_FILES_CAP]
|
|
525
|
+
return "unchanged", None
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
unchanged = []
|
|
529
|
+
changed = []
|
|
530
|
+
new = []
|
|
531
|
+
removed = []
|
|
532
|
+
|
|
533
|
+
if baseline_candidates is not None:
|
|
534
|
+
# Tier 1: full unchanged/changed/new/removed classification.
|
|
535
|
+
all_paths = sorted(set(baseline_by_path) | set(fresh_by_path))
|
|
536
|
+
for p in all_paths:
|
|
537
|
+
in_baseline = p in baseline_by_path
|
|
538
|
+
in_fresh = p in fresh_by_path
|
|
539
|
+
if in_baseline and in_fresh:
|
|
540
|
+
entry = dict(fresh_by_path[p])
|
|
541
|
+
bucket, touched = classify(p, entry)
|
|
542
|
+
if bucket == "unchanged":
|
|
543
|
+
unchanged.append({"path": p, "files": entry.get("files"), "lines": entry.get("lines")})
|
|
544
|
+
else:
|
|
545
|
+
out = {"path": p, "files": entry.get("files"), "lines": entry.get("lines")}
|
|
546
|
+
out["changed_files"] = touched if touched is not None else []
|
|
547
|
+
changed.append(out)
|
|
548
|
+
elif in_fresh:
|
|
549
|
+
entry = fresh_by_path[p]
|
|
550
|
+
new.append({"path": p, "files": entry.get("files"), "lines": entry.get("lines")})
|
|
551
|
+
else:
|
|
552
|
+
entry = baseline_by_path[p]
|
|
553
|
+
removed.append({"path": p, "files": entry.get("files"), "lines": entry.get("lines")})
|
|
554
|
+
else:
|
|
555
|
+
# Tier 2: only unchanged/changed, over the fresh candidate set.
|
|
556
|
+
for p in sorted(fresh_by_path):
|
|
557
|
+
entry = fresh_by_path[p]
|
|
558
|
+
bucket, touched = classify(p, entry)
|
|
559
|
+
if bucket == "unchanged":
|
|
560
|
+
unchanged.append({"path": p, "files": entry.get("files"), "lines": entry.get("lines")})
|
|
561
|
+
else:
|
|
562
|
+
out = {"path": p, "files": entry.get("files"), "lines": entry.get("lines")}
|
|
563
|
+
out["changed_files"] = touched if touched is not None else []
|
|
564
|
+
changed.append(out)
|
|
565
|
+
|
|
566
|
+
result = {
|
|
567
|
+
"script": "diff-since-baseline.sh",
|
|
568
|
+
"version": 1,
|
|
569
|
+
"root": root,
|
|
570
|
+
"baseline_source": source,
|
|
571
|
+
"baseline_commit": baseline_commit,
|
|
572
|
+
"unchanged": unchanged,
|
|
573
|
+
"changed": changed,
|
|
574
|
+
"new": new,
|
|
575
|
+
"removed": removed,
|
|
576
|
+
"unchanged_count": len(unchanged),
|
|
577
|
+
"changed_count": len(changed),
|
|
578
|
+
"new_count": len(new),
|
|
579
|
+
"removed_count": len(removed),
|
|
580
|
+
"notices": notices,
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
body = json.dumps(result, ensure_ascii=False)
|
|
584
|
+
|
|
585
|
+
if JSON_OUT:
|
|
586
|
+
try:
|
|
587
|
+
with open(JSON_OUT, "w", encoding="utf-8") as fh:
|
|
588
|
+
fh.write(json.dumps(result, indent=2, ensure_ascii=False) + "\n")
|
|
589
|
+
except OSError as exc:
|
|
590
|
+
sys.stderr.write("Error: could not write --json-out file: %s\n" % exc)
|
|
591
|
+
sys.exit(4)
|
|
592
|
+
|
|
593
|
+
for n in notices:
|
|
594
|
+
sys.stderr.write("Notice: %s\n" % n)
|
|
595
|
+
|
|
596
|
+
print(body)
|
|
597
|
+
PY
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
python3 -c "$PY_SRC" "$REPO_ROOT" "$BASELINE_INPUT" "$FRESH" "$ROOT_OVERRIDE" "$DISCOVER_BIN" "$JSON_OUT"
|
|
@@ -183,7 +183,7 @@ fi
|
|
|
183
183
|
# and .agents/ — scripts/ (which owns with-lock.sh) is never copied there, so
|
|
184
184
|
# this script — itself shipped under skills/j-uncharted/scripts/ and mirrored
|
|
185
185
|
# alongside it — cannot assume "$REPO_ROOT/scripts/with-lock.sh" exists.
|
|
186
|
-
# Mirrors skills/init/scripts/init.sh's PKG_ROOT fallback: prefer a monorepo
|
|
186
|
+
# Mirrors skills/j-init/scripts/init.sh's PKG_ROOT fallback: prefer a monorepo
|
|
187
187
|
# checkout's sibling scripts/ dir, else fall back to the installed npm
|
|
188
188
|
# package under node_modules/@jenga-ai/agent.
|
|
189
189
|
if [ -f "$SCRIPT_DIR/../../../scripts/with-lock.sh" ]; then
|