@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.
- package/README.md +7 -3
- package/agents/developer.md +82 -2
- package/agents/scrum-master.md +140 -21
- package/agents/tester.md +90 -8
- package/hooks/on_session_end.sh +171 -20
- package/package.json +1 -1
- package/scripts/check-permission-level.sh +107 -0
- package/scripts/check-publicignore-match.sh +122 -0
- package/scripts/check-worktree-liveness.sh +193 -0
- package/scripts/generate-rapport-manifest.sh +43 -0
- package/scripts/idea_manager.sh +47 -0
- package/scripts/install-worktree-commit-guard.sh +134 -0
- package/scripts/jenga-permission-level-switch.sh +109 -0
- package/scripts/smoke-harness.sh +139 -0
- package/scripts/validate-board.sh +62 -0
- package/scripts/with-lock.sh +158 -0
- package/scripts/worktree-remove-guard.sh +204 -0
- package/skills/clearify/SKILL.md +52 -0
- package/skills/commit/SKILL.md +13 -4
- package/skills/distribute/CONFIG_SCHEMA.md +60 -2
- package/skills/do/SKILL.md +48 -11
- package/skills/doc-sync/SKILL.md +16 -0
- package/skills/doc-sync/assets/doc_targets.md +11 -0
- package/skills/idea/SKILL.md +56 -0
- package/skills/idea/assets/idea_handoff_template.md +26 -0
- package/skills/idea/assets/idea_template.md +3 -0
- package/skills/init/SKILL.md +100 -7
- package/skills/init/assets/directory_structure.txt +1 -0
- package/skills/init/assets/workflow_template.json +1 -1
- package/skills/init/scripts/apply-project-visibility.sh +176 -0
- package/skills/init/scripts/detect-existing-codebase.sh +166 -0
- package/skills/init/scripts/init.sh +30 -1
- package/skills/jenga/SKILL.md +160 -17
- package/skills/jenga/scripts/board-scan.sh +238 -0
- package/skills/jenga/scripts/cascade-resolve.sh +297 -0
- package/skills/jenga/scripts/render-confirmation.sh +679 -0
- package/skills/jenga/scripts/render-picker.sh +439 -0
- package/skills/jenga/scripts/resolve-id.sh +367 -0
- package/skills/jenga-permission-level/SKILL.md +81 -0
- package/skills/proceed/SKILL.md +1 -1
- package/skills/publish/SKILL.md +8 -5
- package/skills/publish/assets/ci-contract.md +2 -2
- package/skills/publish/assets/ownership-matrix.md +1 -1
- package/skills/publish/scripts/finalize_changelog.sh +115 -0
- package/skills/publish/scripts/generate_release_notes.sh +475 -28
- package/skills/publish/scripts/npm_ci_pipeline.sh +44 -6
- package/skills/publish/scripts/publish_deploy.sh +38 -8
- package/skills/publish/scripts/run_gates.sh +2 -2
- package/skills/reconcile/SKILL.md +117 -5
- package/skills/reconcile/scripts/detect-unlinked-code.sh +741 -0
- package/skills/skillify/assets/init-new/assets/directory_structure.txt +5 -1
- package/skills/spinoff/SKILL.md +12 -7
- package/skills/todo/SKILL.md +2 -0
- package/skills/uncharted/SKILL.md +711 -0
- package/skills/uncharted/assets/SEGMENT_PROPOSAL_TEMPLATE.md +129 -0
- package/skills/uncharted/assets/UNDERSTANDING_DOC_TEMPLATE.md +160 -0
- package/skills/uncharted/scripts/apply-subsystem-cap.sh +573 -0
- package/skills/uncharted/scripts/detect-dependencies.sh +732 -0
- package/skills/uncharted/scripts/detect-tests.sh +553 -0
- package/skills/uncharted/scripts/discover-subsystems.sh +1029 -0
- package/skills/uncharted/scripts/enumerate-target.sh +470 -0
- package/skills/uncharted/scripts/import-source.sh +517 -0
- package/skills/uncharted/scripts/inspect-provenance.sh +573 -0
- package/skills/uncharted/scripts/resolve-segment-target.sh +640 -0
- package/skills/uncharted/scripts/run-engine.sh +655 -0
- package/skills/uncharted/scripts/validate-proposed-items.sh +125 -0
- package/skills/uncharted/scripts/write-backfilled-epics.sh +498 -0
- package/skills/wtf/SKILL.md +20 -0
- package/templates/CHANGELOG_TEMPLATE.md +13 -0
- package/templates/PROBLEM_RAPPORT_TEMPLATE.md +4 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +157 -10
- package/templates/permission-levels/README.md +73 -0
- package/templates/permission-levels/level-1-locked.json +71 -0
- package/templates/permission-levels/level-2-guarded.json +64 -0
- package/templates/permission-levels/level-3-standard.json +62 -0
- package/templates/permission-levels/level-4-elevated.json +60 -0
- package/templates/permission-levels/level-5-unrestricted.json +58 -0
- package/skills/convert/SKILL.md +0 -124
- package/skills/convert/convert_cli.py +0 -235
- package/skills/convert/tests/sample.csv +0 -4
- package/skills/convert/tests/sample.json +0 -5
- package/skills/convert/tests/sample.jsonl +0 -3
- package/skills/convert/tests/sample.yaml +0 -18
- package/skills/convert/tests/sample_obj.csv +0 -2
- package/skills/convert/tests/sample_obj.json +0 -9
- package/skills/mirror-public/SKILL.md +0 -237
- package/skills/mirror-public/assets/config.json +0 -5
- package/skills/mirror-public/scripts/mirror.sh +0 -374
- package/skills/self-sync/SKILL.md +0 -73
- package/skills/self-sync/scripts/run.js +0 -136
- package/skills/strategy/SKILL.md +0 -312
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# skills/uncharted/scripts/validate-proposed-items.sh
|
|
4
|
+
#
|
|
5
|
+
# Post-write gate for `/uncharted segment` Step 7. Given the board files a
|
|
6
|
+
# confirmed proposal just wrote, it answers one mechanical question: do these
|
|
7
|
+
# files pass the validators the rest of the project already enforces?
|
|
8
|
+
#
|
|
9
|
+
# It validates. It NEVER writes, moves, or repairs a board file, and it has no
|
|
10
|
+
# opinion about content — only about format. Deciding what to do with a failure
|
|
11
|
+
# (fix and re-run, or roll the write back) is agent judgement and lives in
|
|
12
|
+
# `skills/uncharted/SKILL.md`.
|
|
13
|
+
#
|
|
14
|
+
# Usage: validate-proposed-items.sh <board-file> [more-files...]
|
|
15
|
+
#
|
|
16
|
+
# It reuses the two existing validators rather than reimplementing them:
|
|
17
|
+
#
|
|
18
|
+
# scripts/validate-board.sh frontmatter — run against EVERY supplied
|
|
19
|
+
# file (epic, story, and task alike)
|
|
20
|
+
# scripts/validate-story-format.sh body sections — run against every supplied
|
|
21
|
+
# STORY file only
|
|
22
|
+
#
|
|
23
|
+
# A file is treated as a story when its frontmatter `id` matches E##_S##. That
|
|
24
|
+
# is the same classification `validate-board.sh` makes, and it reads the file
|
|
25
|
+
# rather than trusting the path, so a story that landed in the wrong directory
|
|
26
|
+
# is still checked as a story.
|
|
27
|
+
#
|
|
28
|
+
# EVERY FAILURE IS REPORTED. The script does not stop at the first bad file: a
|
|
29
|
+
# half-checked board is the same problem as a half-written one, and an agent
|
|
30
|
+
# deciding whether to roll back needs the whole list, not the first line of it.
|
|
31
|
+
# Both validators run under `set -euo pipefail`, so each call here is guarded
|
|
32
|
+
# (`|| rc=$?`) — an unguarded call would abort this script on the first failure
|
|
33
|
+
# and silently under-report the rest.
|
|
34
|
+
#
|
|
35
|
+
# `validate-story-format.sh` exit codes are reported rather than flattened to
|
|
36
|
+
# "failed", because they name the defect: 2 = no `## Acceptance Criteria`,
|
|
37
|
+
# 3 = no `## Definition of Done`, 4 = a Definition of Done with no `- [ ]`
|
|
38
|
+
# checkboxes.
|
|
39
|
+
#
|
|
40
|
+
# Exit 0 every supplied file passed every applicable validator
|
|
41
|
+
# 1 at least one file failed; each failure is printed
|
|
42
|
+
# 2 usage error, or a required validator is missing
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
set -euo pipefail
|
|
46
|
+
|
|
47
|
+
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
|
48
|
+
REPO_ROOT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)
|
|
49
|
+
[ -n "$REPO_ROOT" ] || REPO_ROOT=$(cd -- "$SCRIPT_DIR/../../.." && pwd -P)
|
|
50
|
+
|
|
51
|
+
BOARD_VALIDATOR="$REPO_ROOT/scripts/validate-board.sh"
|
|
52
|
+
STORY_VALIDATOR="$REPO_ROOT/scripts/validate-story-format.sh"
|
|
53
|
+
|
|
54
|
+
usage() {
|
|
55
|
+
echo "Usage: $(basename "$0") <board-file> [more-files...]" >&2
|
|
56
|
+
echo " Validates newly written epic/story/task files. Exits 1 listing every failure." >&2
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case "${1:-}" in
|
|
60
|
+
-h|--help) usage; exit 0 ;;
|
|
61
|
+
"") echo "❌ no board files supplied" >&2; usage; exit 2 ;;
|
|
62
|
+
esac
|
|
63
|
+
|
|
64
|
+
# Checked for executability, not just existence. A present-but-non-executable validator would
|
|
65
|
+
# otherwise return 126 and be recorded as an ordinary per-file failure — and since SKILL.md reads
|
|
66
|
+
# exit 1 as "repair or roll the whole write back", a stripped exec bit would roll back a board
|
|
67
|
+
# write that was perfectly valid. A broken checkout must not read as a bad proposal.
|
|
68
|
+
for v in "$BOARD_VALIDATOR" "$STORY_VALIDATOR"; do
|
|
69
|
+
if [ ! -f "$v" ]; then
|
|
70
|
+
echo "❌ required validator not found: $v" >&2
|
|
71
|
+
exit 2
|
|
72
|
+
fi
|
|
73
|
+
if [ ! -x "$v" ]; then
|
|
74
|
+
echo "❌ required validator is not executable: $v" >&2
|
|
75
|
+
exit 2
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
# Frontmatter `id` of a board file; empty when the frontmatter is absent or malformed
|
|
80
|
+
# (in which case validate-board.sh has already reported it as a failure).
|
|
81
|
+
item_id() {
|
|
82
|
+
awk '
|
|
83
|
+
NR == 1 { if ($0 !~ /^---[[:space:]]*$/) exit; next }
|
|
84
|
+
/^---[[:space:]]*$/ { exit }
|
|
85
|
+
/^id:[[:space:]]*/ {
|
|
86
|
+
sub(/^id:[[:space:]]*/, ""); sub(/[[:space:]]*#.*$/, "")
|
|
87
|
+
gsub(/"/, ""); gsub(/'"'"'/, ""); sub(/[[:space:]]+$/, "")
|
|
88
|
+
print; exit
|
|
89
|
+
}
|
|
90
|
+
' "$1" 2>/dev/null || true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Runs one validator against one file, recording a failure instead of aborting.
|
|
94
|
+
FAILURES=()
|
|
95
|
+
check() {
|
|
96
|
+
local label="$1" validator="$2" file="$3" rc=0 out=""
|
|
97
|
+
out=$("$validator" "$file" 2>&1) || rc=$?
|
|
98
|
+
if [ "$rc" -ne 0 ]; then
|
|
99
|
+
FAILURES+=("❌ $file — $label ($(basename "$validator") exit $rc)
|
|
100
|
+
$(printf '%s\n' "$out" | sed 's/^/ /')")
|
|
101
|
+
fi
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
CHECKED=0 STORIES=0
|
|
105
|
+
for file in "$@"; do
|
|
106
|
+
CHECKED=$((CHECKED + 1))
|
|
107
|
+
if [ ! -f "$file" ] || [ ! -r "$file" ]; then
|
|
108
|
+
FAILURES+=("❌ $file — file not found or not readable")
|
|
109
|
+
continue
|
|
110
|
+
fi
|
|
111
|
+
check "board frontmatter" "$BOARD_VALIDATOR" "$file"
|
|
112
|
+
if [[ "$(item_id "$file")" =~ ^E[0-9]{2}_S[0-9]{2}$ ]]; then
|
|
113
|
+
STORIES=$((STORIES + 1))
|
|
114
|
+
check "story format" "$STORY_VALIDATOR" "$file"
|
|
115
|
+
fi
|
|
116
|
+
done
|
|
117
|
+
|
|
118
|
+
if [ "${#FAILURES[@]}" -gt 0 ]; then
|
|
119
|
+
echo "❌ ${#FAILURES[@]} validation failure(s) across $CHECKED file(s):" >&2
|
|
120
|
+
printf '%s\n' "${FAILURES[@]}" >&2
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
echo "✅ $CHECKED board file(s) valid ($STORIES story file(s) additionally format-checked)"
|
|
125
|
+
exit 0
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# write-backfilled-epics.sh — turn apply-subsystem-cap.sh's kept subsystem list into board epics
|
|
3
|
+
#
|
|
4
|
+
# Usage: write-backfilled-epics.sh [options] [<cap-report.json>]
|
|
5
|
+
# apply-subsystem-cap.sh ... | write-backfilled-epics.sh [options]
|
|
6
|
+
# write-backfilled-epics.sh --help
|
|
7
|
+
#
|
|
8
|
+
# The last mile of `/uncharted onboard`: apply-subsystem-cap.sh (E40_S04_T03) decides WHICH
|
|
9
|
+
# discovered subsystems become epics. This script is the ONLY thing that actually writes those
|
|
10
|
+
# epics to the board. It performs no discovery, no scoring, and no cap decision of its own —
|
|
11
|
+
# it consumes apply-subsystem-cap.sh's JSON output (its "kept" array) and renders one epic file
|
|
12
|
+
# per entry, following the Epic format in templates/SCRUM_BOARD_SCHEMA.md exactly.
|
|
13
|
+
#
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# THE HARD CONSTRAINT
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# `onboard` mode must NEVER modify, move, rename, delete, or restructure the consumer's
|
|
18
|
+
# application code. Its entire output surface is project/board/, project/rapports/analysis/,
|
|
19
|
+
# and project/PROJECT_SUMMARY.md. This script is the one that actually touches disk in the
|
|
20
|
+
# epic-generation step, so it is where that guarantee is made STRUCTURAL rather than only
|
|
21
|
+
# documented in skills/uncharted/SKILL.md:
|
|
22
|
+
#
|
|
23
|
+
# - The epics directory (--epics-dir, default <repo-root>/project/board/epics) and the
|
|
24
|
+
# --json-out path, if given, are both canonicalised and checked BEFORE any file is written.
|
|
25
|
+
# Either one resolving outside <repo-root>/project/ is a hard failure — exit 3, nothing
|
|
26
|
+
# written — not a warning.
|
|
27
|
+
# - The script never reads, deletes, or touches anything under the analysed codebase itself.
|
|
28
|
+
# Its only inputs are apply-subsystem-cap.sh's JSON (an already-computed decision) and the
|
|
29
|
+
# existing project/board/epics/ directory (to avoid ID collisions).
|
|
30
|
+
#
|
|
31
|
+
# There is deliberately no flag to point --epics-dir or --json-out outside project/ and have the
|
|
32
|
+
# guard look the other way. If a caller needs epics written somewhere else, that is a different
|
|
33
|
+
# script's problem, not an override of this one's.
|
|
34
|
+
#
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# INPUT
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# apply-subsystem-cap.sh's JSON report, from a file argument or stdin (`-`, or no argument). The
|
|
39
|
+
# field consumed is `kept` — an array of {rank, path, absolute_path, score, files, lines}. This
|
|
40
|
+
# is deliberately the SAME field set apply-subsystem-cap.sh's own header documents; nothing here
|
|
41
|
+
# assumes a richer shape (discover-subsystems.sh's fuller per-candidate fields — manifests,
|
|
42
|
+
# test_paths, doc_paths, signals — are NOT present in `kept` and are not depended on). Each
|
|
43
|
+
# generated epic's Purpose section says so explicitly, so nobody reading a backfilled epic
|
|
44
|
+
# mistakes a coarse discovery signal for a deep investigation.
|
|
45
|
+
#
|
|
46
|
+
# `root` / `root_absolute`, when present, label which codebase was onboarded.
|
|
47
|
+
#
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# OPTIONS
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# --epics-dir DIR Where epic files are written. Default: <repo-root>/project/board/epics.
|
|
52
|
+
# Must resolve under <repo-root>/project/ or the run is refused.
|
|
53
|
+
# --json-out FILE Also write this script's JSON summary to FILE. stdout gets it regardless.
|
|
54
|
+
# Must resolve under <repo-root>/project/ or the run is refused.
|
|
55
|
+
# --dry-run Compute IDs and render epic content, but write nothing. Still validates and
|
|
56
|
+
# reports the write-path guard as it would apply to a real run.
|
|
57
|
+
# --label TEXT Human label for the analysed codebase, used in each epic's Purpose section.
|
|
58
|
+
# Default: the report's own `root` (or `root_absolute`'s basename).
|
|
59
|
+
# -h, --help Show this help and exit 0.
|
|
60
|
+
#
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# EPIC ID CONTINUATION
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Epic IDs must continue the existing board's numbering, never collide with an epic already on
|
|
65
|
+
# it. The next free ID is one past the HIGHEST `E##` found in filenames under BOTH --epics-dir
|
|
66
|
+
# and the canonical <repo-root>/project/board/epics (the same directory in the common case;
|
|
67
|
+
# checking both means a --epics-dir override used for a dry-run/test pass can never later collide
|
|
68
|
+
# with the real board). IDs are assigned to `kept` entries in array order (rank order).
|
|
69
|
+
#
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# OUTPUT (stdout, JSON)
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# {
|
|
74
|
+
# "script": "write-backfilled-epics.sh",
|
|
75
|
+
# "version": 1,
|
|
76
|
+
# "epics_dir": "<absolute path>",
|
|
77
|
+
# "dry_run": <bool>,
|
|
78
|
+
# "epic_count": <int>,
|
|
79
|
+
# "epics": [ { "id", "path", "title", "source_path", "rank", "score", "files", "lines" }, ... ],
|
|
80
|
+
# "notices": [ "<non-fatal diagnostic>", ... ]
|
|
81
|
+
# }
|
|
82
|
+
#
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# EXIT CODES
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# 0 — success (including a `kept` array of length zero: 0 epics is a valid outcome)
|
|
87
|
+
# 1 — usage error: unknown flag, missing value
|
|
88
|
+
# 2 — input error: report missing/unreadable, unparseable JSON, or no `kept` array
|
|
89
|
+
# 3 — write-path guard: --epics-dir or --json-out resolved outside <repo-root>/project/
|
|
90
|
+
# 4 — write failure: an epic file could not be written, or an existing epic file could not be
|
|
91
|
+
# read while computing ID continuation
|
|
92
|
+
#
|
|
93
|
+
# Examples:
|
|
94
|
+
# discover-subsystems.sh . | apply-subsystem-cap.sh --rapport "$DOC" | write-backfilled-epics.sh
|
|
95
|
+
# apply-subsystem-cap.sh --json-out cap.json ... && write-backfilled-epics.sh cap.json
|
|
96
|
+
# write-backfilled-epics.sh --dry-run --epics-dir project/rapports/analysis/scratch-epics cap.json
|
|
97
|
+
#
|
|
98
|
+
# Requires: bash, git, python3. jq is NOT required — JSON is parsed and emitted by python3.
|
|
99
|
+
|
|
100
|
+
set -euo pipefail
|
|
101
|
+
|
|
102
|
+
EPICS_DIR=""
|
|
103
|
+
JSON_OUT=""
|
|
104
|
+
DRY_RUN=0
|
|
105
|
+
LABEL=""
|
|
106
|
+
INPUT=""
|
|
107
|
+
|
|
108
|
+
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
|
109
|
+
|
|
110
|
+
usage() {
|
|
111
|
+
cat <<EOF
|
|
112
|
+
Usage: $(basename "$0") [options] [<cap-report.json>]
|
|
113
|
+
|
|
114
|
+
Render one backfilled epic file per apply-subsystem-cap.sh "kept" entry, under
|
|
115
|
+
<repo-root>/project/board/epics by default. Refuses to write anywhere outside
|
|
116
|
+
<repo-root>/project/ — that guard cannot be overridden by flag.
|
|
117
|
+
|
|
118
|
+
Arguments:
|
|
119
|
+
<cap-report.json> apply-subsystem-cap.sh JSON output. Omit, or pass "-", to read stdin.
|
|
120
|
+
|
|
121
|
+
Options:
|
|
122
|
+
--epics-dir DIR Output directory for epic files (default: <repo-root>/project/board/epics)
|
|
123
|
+
--json-out FILE Also write this script's JSON summary to FILE
|
|
124
|
+
--dry-run Compute and print, but write no epic files
|
|
125
|
+
--label TEXT Human label for the analysed codebase (default: the report's own root)
|
|
126
|
+
-h, --help Show this help and exit
|
|
127
|
+
|
|
128
|
+
Exit codes: 0 success, 1 usage error, 2 input error, 3 write-path guard, 4 write failure.
|
|
129
|
+
EOF
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
die_usage() {
|
|
133
|
+
echo "Error: $1" >&2
|
|
134
|
+
echo >&2
|
|
135
|
+
usage >&2
|
|
136
|
+
exit 1
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
require_value() {
|
|
140
|
+
# require_value <flag> <remaining-arg-count>
|
|
141
|
+
[ "$2" -ge 2 ] || die_usage "$1 requires a value"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# Argument parsing
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
while [ "$#" -gt 0 ]; do
|
|
149
|
+
case "$1" in
|
|
150
|
+
--epics-dir) require_value "--epics-dir" "$#"; EPICS_DIR="$2"; shift 2 ;;
|
|
151
|
+
--epics-dir=*) EPICS_DIR="${1#*=}"; shift ;;
|
|
152
|
+
--json-out) require_value "--json-out" "$#"; JSON_OUT="$2"; shift 2 ;;
|
|
153
|
+
--json-out=*) JSON_OUT="${1#*=}"; shift ;;
|
|
154
|
+
--label) require_value "--label" "$#"; LABEL="$2"; shift 2 ;;
|
|
155
|
+
--label=*) LABEL="${1#*=}"; shift ;;
|
|
156
|
+
--dry-run) DRY_RUN=1; shift ;;
|
|
157
|
+
-h|--help) usage; exit 0 ;;
|
|
158
|
+
--)
|
|
159
|
+
shift
|
|
160
|
+
[ "$#" -le 1 ] || die_usage "at most one input report is accepted"
|
|
161
|
+
[ "$#" -eq 0 ] || INPUT="$1"
|
|
162
|
+
break ;;
|
|
163
|
+
-)
|
|
164
|
+
INPUT="-"; shift ;;
|
|
165
|
+
-*)
|
|
166
|
+
die_usage "unknown option \"$1\"" ;;
|
|
167
|
+
*)
|
|
168
|
+
[ -z "$INPUT" ] || die_usage "at most one input report is accepted (got \"$INPUT\" and \"$1\")"
|
|
169
|
+
INPUT="$1"; shift ;;
|
|
170
|
+
esac
|
|
171
|
+
done
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Input resolution
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
if [ -z "$INPUT" ] || [ "$INPUT" = "-" ]; then
|
|
178
|
+
INPUT="-"
|
|
179
|
+
else
|
|
180
|
+
if [ ! -e "$INPUT" ]; then
|
|
181
|
+
echo "Error: input report does not exist: $INPUT" >&2
|
|
182
|
+
echo " Expected apply-subsystem-cap.sh JSON output." >&2
|
|
183
|
+
exit 2
|
|
184
|
+
fi
|
|
185
|
+
if [ ! -f "$INPUT" ] || [ ! -r "$INPUT" ]; then
|
|
186
|
+
echo "Error: input report is not a readable file: $INPUT" >&2
|
|
187
|
+
exit 2
|
|
188
|
+
fi
|
|
189
|
+
INPUT=$(cd -- "$(dirname -- "$INPUT")" && pwd -P)/$(basename -- "$INPUT")
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Repo root and write-path guard — anchored on THIS SCRIPT, not on the analysed root, matching
|
|
194
|
+
# apply-subsystem-cap.sh's convention: the board belongs to the project that owns the engine.
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
REPO_ROOT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)
|
|
198
|
+
[ -n "$REPO_ROOT" ] || REPO_ROOT="$(pwd -P)"
|
|
199
|
+
|
|
200
|
+
PROJECT_DIR="$REPO_ROOT/project"
|
|
201
|
+
mkdir -p "$PROJECT_DIR" 2>/dev/null || {
|
|
202
|
+
echo "Error: could not create project directory: $PROJECT_DIR" >&2
|
|
203
|
+
exit 4
|
|
204
|
+
}
|
|
205
|
+
PROJECT_DIR_REAL=$(cd -- "$PROJECT_DIR" && pwd -P)
|
|
206
|
+
|
|
207
|
+
CANONICAL_EPICS_DIR="$PROJECT_DIR_REAL/board/epics"
|
|
208
|
+
|
|
209
|
+
[ -n "$EPICS_DIR" ] || EPICS_DIR="$CANONICAL_EPICS_DIR"
|
|
210
|
+
|
|
211
|
+
# resolve_path_no_mkdir <path> — canonicalise <path> WITHOUT creating anything on disk. Existing
|
|
212
|
+
# ancestor components have their symlinks resolved (via Python's os.path.realpath, which is safe
|
|
213
|
+
# to call on a path that does not fully exist — it resolves what it can and appends the rest
|
|
214
|
+
# literally); a not-yet-existing tail is normalised lexically (".."/"." collapsed) so relative
|
|
215
|
+
# escapes like "project/../src" are still caught even before "project/" exists.
|
|
216
|
+
resolve_path_no_mkdir() {
|
|
217
|
+
python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$1"
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# guard_under_project_dir <path> <human-label> — refuse, loudly and BEFORE ANY FILESYSTEM WRITE,
|
|
221
|
+
# if <path> does not resolve under PROJECT_DIR_REAL. This is the one guard the task exists to
|
|
222
|
+
# make structural rather than aspirational, so the order matters as much as the check itself:
|
|
223
|
+
# resolution happens first and touches nothing; `mkdir -p` runs only after the path has already
|
|
224
|
+
# been accepted. An earlier version of this guard called `mkdir -p` before the check and so
|
|
225
|
+
# created an (empty) directory outside project/ on every rejected path, even under --dry-run —
|
|
226
|
+
# exactly the kind of write this guard exists to prevent. No flag combination may reintroduce
|
|
227
|
+
# that ordering.
|
|
228
|
+
guard_under_project_dir() {
|
|
229
|
+
local raw="$1" label="$2" real
|
|
230
|
+
real=$(resolve_path_no_mkdir "$raw")
|
|
231
|
+
case "$real" in
|
|
232
|
+
"$PROJECT_DIR_REAL"/*|"$PROJECT_DIR_REAL")
|
|
233
|
+
;;
|
|
234
|
+
*)
|
|
235
|
+
echo "Error: refusing to write $label outside project/." >&2
|
|
236
|
+
echo " Resolved path: $real" >&2
|
|
237
|
+
echo " Allowed root: $PROJECT_DIR_REAL" >&2
|
|
238
|
+
echo " onboard mode's entire output surface is project/board/, project/rapports/analysis/," >&2
|
|
239
|
+
echo " and project/PROJECT_SUMMARY.md — it must never write anywhere else, including any" >&2
|
|
240
|
+
echo " location that could be mistaken for the consumer's application code." >&2
|
|
241
|
+
echo " Nothing was created on disk for this rejected path." >&2
|
|
242
|
+
exit 3
|
|
243
|
+
;;
|
|
244
|
+
esac
|
|
245
|
+
mkdir -p "$real" 2>/dev/null || {
|
|
246
|
+
echo "Error: could not create $label: $real" >&2
|
|
247
|
+
exit 4
|
|
248
|
+
}
|
|
249
|
+
printf '%s\n' "$real"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
EPICS_DIR_REAL=$(guard_under_project_dir "$EPICS_DIR" "the epics directory")
|
|
253
|
+
|
|
254
|
+
if [ -n "$JSON_OUT" ]; then
|
|
255
|
+
JSON_OUT_DIR=$(dirname -- "$JSON_OUT")
|
|
256
|
+
JSON_OUT_DIR_REAL=$(guard_under_project_dir "$JSON_OUT_DIR" "the --json-out directory")
|
|
257
|
+
JSON_OUT="$JSON_OUT_DIR_REAL/$(basename -- "$JSON_OUT")"
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
ISO_DATE=$(date -u +%Y-%m-%d)
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
# Render — the python source is captured into a variable and run with `python3 -c`, exactly as
|
|
264
|
+
# apply-subsystem-cap.sh does, because this script must be able to read stdin itself
|
|
265
|
+
# (`... | write-backfilled-epics.sh`), which a `python3 - <<PY` heredoc would occupy.
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
PY_SRC=$(cat <<'PY'
|
|
269
|
+
import json
|
|
270
|
+
import os
|
|
271
|
+
import re
|
|
272
|
+
import sys
|
|
273
|
+
|
|
274
|
+
(INPUT, EPICS_DIR, CANONICAL_EPICS_DIR, JSON_OUT, DRY_RUN_S, LABEL, TODAY) = sys.argv[1:8]
|
|
275
|
+
DRY_RUN = DRY_RUN_S == "1"
|
|
276
|
+
|
|
277
|
+
notices = []
|
|
278
|
+
|
|
279
|
+
# --- read apply-subsystem-cap.sh's report ---------------------------------------------------
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
if INPUT == "-":
|
|
283
|
+
raw = sys.stdin.read()
|
|
284
|
+
origin = "stdin"
|
|
285
|
+
else:
|
|
286
|
+
with open(INPUT, "r", encoding="utf-8") as fh:
|
|
287
|
+
raw = fh.read()
|
|
288
|
+
origin = INPUT
|
|
289
|
+
except OSError as exc:
|
|
290
|
+
sys.stderr.write("Error: could not read input report: %s\n" % exc)
|
|
291
|
+
sys.exit(2)
|
|
292
|
+
|
|
293
|
+
if not raw.strip():
|
|
294
|
+
sys.stderr.write("Error: input report is empty (%s).\n" % origin)
|
|
295
|
+
sys.stderr.write(" Expected apply-subsystem-cap.sh JSON output.\n")
|
|
296
|
+
sys.exit(2)
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
report = json.loads(raw)
|
|
300
|
+
except ValueError as exc:
|
|
301
|
+
sys.stderr.write("Error: input report is not valid JSON (%s): %s\n" % (origin, exc))
|
|
302
|
+
sys.exit(2)
|
|
303
|
+
|
|
304
|
+
if not isinstance(report, dict) or not isinstance(report.get("kept"), list):
|
|
305
|
+
sys.stderr.write("Error: input report has no \"kept\" array (%s).\n" % origin)
|
|
306
|
+
sys.stderr.write(" This does not look like apply-subsystem-cap.sh output. Pipe\n")
|
|
307
|
+
sys.stderr.write(" discover-subsystems.sh's output through apply-subsystem-cap.sh first.\n")
|
|
308
|
+
sys.exit(2)
|
|
309
|
+
|
|
310
|
+
produced_by = report.get("script")
|
|
311
|
+
if produced_by and produced_by != "apply-subsystem-cap.sh":
|
|
312
|
+
notices.append(
|
|
313
|
+
"Input reports itself as \"%s\" rather than apply-subsystem-cap.sh; proceeding on the "
|
|
314
|
+
"strength of its \"kept\" array." % produced_by)
|
|
315
|
+
|
|
316
|
+
kept = [c for c in report["kept"] if isinstance(c, dict)]
|
|
317
|
+
root = report.get("root") or report.get("root_absolute") or "(unknown root)"
|
|
318
|
+
root_absolute = report.get("root_absolute") or ""
|
|
319
|
+
if LABEL:
|
|
320
|
+
label = LABEL
|
|
321
|
+
elif root not in (".", "", "./"):
|
|
322
|
+
label = root
|
|
323
|
+
else:
|
|
324
|
+
label = os.path.basename(root_absolute.rstrip("/")) or root
|
|
325
|
+
|
|
326
|
+
# --- epic ID continuation -------------------------------------------------------------------
|
|
327
|
+
# Scan BOTH the target epics dir and the canonical project/board/epics for the highest existing
|
|
328
|
+
# E## so a scratch --epics-dir used for a dry run can never later collide with the real board.
|
|
329
|
+
|
|
330
|
+
EPIC_FILENAME_RE = re.compile(r"^E(\d{2,})_.+\.md$")
|
|
331
|
+
|
|
332
|
+
def highest_epic_number(directory):
|
|
333
|
+
highest = 0
|
|
334
|
+
try:
|
|
335
|
+
names = os.listdir(directory)
|
|
336
|
+
except OSError:
|
|
337
|
+
return 0
|
|
338
|
+
for name in names:
|
|
339
|
+
m = EPIC_FILENAME_RE.match(name)
|
|
340
|
+
if m:
|
|
341
|
+
highest = max(highest, int(m.group(1)))
|
|
342
|
+
return highest
|
|
343
|
+
|
|
344
|
+
next_id = 1 + max(highest_epic_number(EPICS_DIR), highest_epic_number(CANONICAL_EPICS_DIR))
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def slugify(text):
|
|
348
|
+
s = re.sub(r"[^A-Za-z0-9]+", "-", str(text)).strip("-").lower()
|
|
349
|
+
return (s or "subsystem")[:48]
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def humanize_path(path):
|
|
353
|
+
parts = [p for p in re.split(r"[\\/]+", path) if p and p not in (".", "..")]
|
|
354
|
+
if not parts:
|
|
355
|
+
return path or "(root)"
|
|
356
|
+
words = []
|
|
357
|
+
for part in parts:
|
|
358
|
+
words.append(" ".join(w.capitalize() for w in re.split(r"[-_]+", part) if w) or part)
|
|
359
|
+
return " / ".join(words)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def fmt_score(v):
|
|
363
|
+
return ("%.2f" % v) if isinstance(v, (int, float)) else "unknown"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def fmt_count(v):
|
|
367
|
+
return ("%d" % v) if isinstance(v, int) else "unknown"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
epics = []
|
|
371
|
+
|
|
372
|
+
for entry in kept:
|
|
373
|
+
path = entry.get("path") or "(unknown path)"
|
|
374
|
+
rank = entry.get("rank")
|
|
375
|
+
score = entry.get("score")
|
|
376
|
+
files = entry.get("files")
|
|
377
|
+
lines = entry.get("lines")
|
|
378
|
+
|
|
379
|
+
epic_id = "E%02d" % next_id
|
|
380
|
+
next_id += 1
|
|
381
|
+
|
|
382
|
+
title = humanize_path(path)
|
|
383
|
+
slug = slugify(path.replace("/", "-").replace("\\", "-"))
|
|
384
|
+
filename = "%s_%s.md" % (epic_id, slug)
|
|
385
|
+
dest_path = os.path.join(EPICS_DIR, filename)
|
|
386
|
+
|
|
387
|
+
body = []
|
|
388
|
+
body.append("---")
|
|
389
|
+
body.append("id: %s" % epic_id)
|
|
390
|
+
body.append("title: %s" % title)
|
|
391
|
+
body.append("status: Pending")
|
|
392
|
+
body.append("date_created: %s" % TODAY)
|
|
393
|
+
body.append("date_started:")
|
|
394
|
+
body.append("date_completed:")
|
|
395
|
+
body.append("dates_previously_completed:")
|
|
396
|
+
body.append("reopened_on:")
|
|
397
|
+
body.append("reopened_reason:")
|
|
398
|
+
body.append("docs: []")
|
|
399
|
+
body.append("epic_scope_approval: false")
|
|
400
|
+
body.append("provenance: backfilled")
|
|
401
|
+
body.append("stories: []")
|
|
402
|
+
body.append("---")
|
|
403
|
+
body.append("")
|
|
404
|
+
body.append("# Epic: %s" % title)
|
|
405
|
+
body.append("")
|
|
406
|
+
body.append("## Purpose")
|
|
407
|
+
body.append(
|
|
408
|
+
"`%s` (in `%s`) was identified by `/uncharted onboard`'s coarse subsystem discovery pass "
|
|
409
|
+
"as an existing, cohesive subsystem of this codebase with no prior Jenga board "
|
|
410
|
+
"provenance. This epic exists to bring it under board provenance — understanding what it "
|
|
411
|
+
"already does and integrating it with the rest of the project's board — not to build new "
|
|
412
|
+
"functionality. See the `provenance: backfilled` field above." % (path, label))
|
|
413
|
+
body.append("")
|
|
414
|
+
body.append(
|
|
415
|
+
"**Discovery evidence (from `discover-subsystems.sh` via `apply-subsystem-cap.sh`):**")
|
|
416
|
+
body.append("- Path: `%s`" % path)
|
|
417
|
+
body.append("- Discovery rank: %s (score %s/100)" % (fmt_count(rank), fmt_score(score)))
|
|
418
|
+
body.append("- Size: %s files, %s lines" % (fmt_count(files), fmt_count(lines)))
|
|
419
|
+
body.append("")
|
|
420
|
+
body.append(
|
|
421
|
+
"This evidence is coarse by design — `onboard`'s cap step carries only path, rank, "
|
|
422
|
+
"score, and file/line counts, not the fuller per-subsystem signals (manifests, test "
|
|
423
|
+
"coverage, documentation) that `discover-subsystems.sh` computes internally. Run "
|
|
424
|
+
"`/uncharted segment %s` for a full understanding document (purpose, structure, "
|
|
425
|
+
"dependencies, existing tests, risk areas) before breaking this epic into stories." % path)
|
|
426
|
+
body.append("")
|
|
427
|
+
body.append("## Definition of Done")
|
|
428
|
+
body.append(
|
|
429
|
+
"- [ ] An `/uncharted segment` understanding document exists for `%s`, covering its "
|
|
430
|
+
"purpose, structure, dependencies, and existing test coverage" % path)
|
|
431
|
+
body.append(
|
|
432
|
+
"- [ ] Any risk areas or undocumented behaviour surfaced by that document are captured "
|
|
433
|
+
"as follow-up stories or tasks under this epic")
|
|
434
|
+
body.append(
|
|
435
|
+
"- [ ] This epic's stories and tasks describe understanding and integration work — "
|
|
436
|
+
"documenting behaviour, closing test gaps, establishing board provenance — and not "
|
|
437
|
+
"original construction; an incomplete-looking checklist here does not mean unbuilt "
|
|
438
|
+
"functionality")
|
|
439
|
+
body.append("- [ ] This subsystem's application code is unchanged by the act of backfilling this epic")
|
|
440
|
+
body.append("")
|
|
441
|
+
|
|
442
|
+
content = "\n".join(body)
|
|
443
|
+
|
|
444
|
+
if not DRY_RUN:
|
|
445
|
+
if os.path.exists(dest_path):
|
|
446
|
+
sys.stderr.write("Error: refusing to overwrite existing epic file: %s\n" % dest_path)
|
|
447
|
+
sys.exit(4)
|
|
448
|
+
try:
|
|
449
|
+
with open(dest_path, "w", encoding="utf-8") as fh:
|
|
450
|
+
fh.write(content)
|
|
451
|
+
except OSError as exc:
|
|
452
|
+
sys.stderr.write("Error: could not write epic file %s: %s\n" % (dest_path, exc))
|
|
453
|
+
sys.exit(4)
|
|
454
|
+
|
|
455
|
+
epics.append({
|
|
456
|
+
"id": epic_id,
|
|
457
|
+
"path": dest_path,
|
|
458
|
+
"title": title,
|
|
459
|
+
"source_path": path,
|
|
460
|
+
"rank": rank,
|
|
461
|
+
"score": score,
|
|
462
|
+
"files": files,
|
|
463
|
+
"lines": lines,
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
result = {
|
|
467
|
+
"script": "write-backfilled-epics.sh",
|
|
468
|
+
"version": 1,
|
|
469
|
+
"epics_dir": EPICS_DIR,
|
|
470
|
+
"dry_run": DRY_RUN,
|
|
471
|
+
"epic_count": len(epics),
|
|
472
|
+
"epics": epics,
|
|
473
|
+
"notices": notices,
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if JSON_OUT:
|
|
477
|
+
try:
|
|
478
|
+
with open(JSON_OUT, "w", encoding="utf-8") as fh:
|
|
479
|
+
json.dump(result, fh, indent=2, ensure_ascii=False)
|
|
480
|
+
fh.write("\n")
|
|
481
|
+
except OSError as exc:
|
|
482
|
+
sys.stderr.write("Error: could not write --json-out file: %s\n" % exc)
|
|
483
|
+
sys.exit(4)
|
|
484
|
+
|
|
485
|
+
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
|
|
486
|
+
sys.stdout.write("\n")
|
|
487
|
+
|
|
488
|
+
if DRY_RUN:
|
|
489
|
+
sys.stderr.write("Notice: --dry-run — %d epic file(s) computed but not written.\n" % len(epics))
|
|
490
|
+
else:
|
|
491
|
+
sys.stderr.write("Notice: wrote %d backfilled epic file(s) to %s\n" % (len(epics), EPICS_DIR))
|
|
492
|
+
for n in notices:
|
|
493
|
+
sys.stderr.write("Notice: %s\n" % n)
|
|
494
|
+
PY
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
python3 -c "$PY_SRC" \
|
|
498
|
+
"$INPUT" "$EPICS_DIR_REAL" "$CANONICAL_EPICS_DIR" "$JSON_OUT" "$DRY_RUN" "$LABEL" "$ISO_DATE"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wtf
|
|
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
|
+
keywords:
|
|
5
|
+
- wtf
|
|
6
|
+
- confused
|
|
7
|
+
- huh
|
|
8
|
+
- what does this mean
|
|
9
|
+
- I'm lost
|
|
10
|
+
examples:
|
|
11
|
+
- "wtf"
|
|
12
|
+
- "wtf does this mean"
|
|
13
|
+
- "wtf is going on here"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# WTF — Alias of /clearify
|
|
17
|
+
|
|
18
|
+
## Instructions
|
|
19
|
+
|
|
20
|
+
`/wtf` is an alias of `/clearify`. Follow `skills/clearify/SKILL.md` in full — do not duplicate or reimplement its ambiguity-detection logic here. Read that file's `## Instructions` section and execute it exactly as written, using whatever prompt or conversation context is attached to this `/wtf` invocation.
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
**Related Epic:** <Epic name or N/A>
|
|
6
6
|
**Related Story:** <Story name or N/A>
|
|
7
7
|
**Related Task:** <Task name or N/A>
|
|
8
|
-
**Type:** `conflict` | `implementation_blocker` | `security_concern` | `test_failure` | `analysis`
|
|
8
|
+
**Type:** `conflict` | `implementation_blocker` | `security_concern` | `test_failure` | `analysis` | `crucial_escalation`
|
|
9
|
+
|
|
10
|
+
> For `crucial_escalation`: the **Related Epic/Story/Task** field above must name the specific target item's ID (`E##`, `E##_S##`, or `E##_S##_T##`) whose `crucial_level` is being escalated — no separate field is used for this.
|
|
9
11
|
|
|
10
12
|
---
|
|
11
13
|
|
|
@@ -44,6 +46,7 @@ A detailed explanation of the issue.
|
|
|
44
46
|
- **Implementation blocker:** describe what failed and why
|
|
45
47
|
- **Test failure:** describe which tests failed, what was expected, and what was observed
|
|
46
48
|
- **Analysis:** describe the analysis scope, methodology, and findings
|
|
49
|
+
- **Crucial escalation:** describe what was discovered during implementation or testing, why it changes the target item's risk profile enough to warrant raising its `crucial_level`, and the concrete, checkable fact backing that claim (a specific file/path, an exact error message, a reproduction count, or a quantifiable impact — see `templates/SCRUM_BOARD_SCHEMA.md`'s Rapport Types section for the full concrete-reason requirement). A subjective statement alone (e.g. "this seems risky") is not sufficient.
|
|
47
50
|
|
|
48
51
|
---
|
|
49
52
|
|