@jenga-ai/agent 1.0.1 → 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 +10 -7
- package/agents/developer.md +82 -2
- package/agents/scrum-master.md +215 -21
- package/agents/tester.md +90 -8
- package/hooks/on_session_end.sh +171 -20
- package/mcp/router/embedder.js +1 -1
- package/mcp/training_runner/index.js +239 -0
- package/mcp/training_runner/package-lock.json +1065 -0
- package/mcp/training_runner/package.json +15 -0
- package/package.json +14 -16
- 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/close-story/SKILL.md +203 -0
- package/skills/close-story/scripts/check-story-closeable.sh +195 -0
- package/skills/close-story/scripts/compute-scope-divergence.sh +128 -0
- package/skills/close-story/scripts/extract-diff-stats.sh +48 -0
- package/skills/close-story/scripts/extract-task-diff-stats.sh +97 -0
- package/skills/close-story/scripts/update-task-frontmatter.sh +103 -0
- package/skills/commit/SKILL.md +30 -3
- package/skills/distribute/CONFIG_SCHEMA.md +148 -0
- package/skills/distribute/SKILL.md +173 -0
- package/skills/distribute/scripts/check-version.sh +74 -0
- package/skills/distribute/scripts/commit-version-bump.sh +108 -0
- package/skills/distribute/scripts/distribute-changes.sh +381 -0
- package/skills/do/SKILL.md +352 -1
- package/skills/do/assets/intent-vs-diff-prompt.md +69 -0
- package/skills/doc/assets/path-objectives.yaml +13 -0
- 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 +101 -7
- package/skills/init/assets/directory_structure.txt +1 -0
- package/skills/init/assets/strategy_stub_template.md +38 -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 +35 -1
- package/skills/jenga/SKILL.md +206 -14
- 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 +206 -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/train/SKILL.md +0 -116
- package/skills/train/assets/dashboard-templates/classifiers.html +0 -106
- package/skills/train/assets/dashboard-templates/nlp.html +0 -102
- package/skills/train/assets/dashboard-templates/transformers.html +0 -98
- package/skills/train/assets/results-parsers/__init__.py +0 -9
- package/skills/train/assets/results-parsers/classifiers.py +0 -84
- package/skills/train/assets/results-parsers/nlp.py +0 -88
- package/skills/train/assets/results-parsers/reporter.py +0 -154
- package/skills/train/assets/results-parsers/transformers.py +0 -120
- package/skills/train/train_cli.py +0 -786
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/install-worktree-commit-guard.sh — install a branch-guard pre-commit
|
|
3
|
+
# hook into a given git worktree.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# scripts/install-worktree-commit-guard.sh <worktree-path> <branch-name>
|
|
7
|
+
#
|
|
8
|
+
# Writes a `pre-commit` hook that rejects any commit attempted while HEAD is
|
|
9
|
+
# on a branch other than <branch-name>. This exists because a tester once
|
|
10
|
+
# committed its verification of a task to `main` instead of the branch it was
|
|
11
|
+
# actually verifying, from inside the worktree created for that task —
|
|
12
|
+
# nothing mechanically prevented it (see E37's Purpose / E37_S02). Written
|
|
13
|
+
# rules alone have a documented track record of drifting in this project
|
|
14
|
+
# (E17 rollup note: two of its own stories were marked complete without the
|
|
15
|
+
# underlying change landing), so this is a real guard, not just a documented
|
|
16
|
+
# rule.
|
|
17
|
+
#
|
|
18
|
+
# IMPORTANT — why this does NOT just write to `git rev-parse --git-path
|
|
19
|
+
# hooks`: that path resolves to the *shared* `hooks` directory common to the
|
|
20
|
+
# main repository and every one of its linked worktrees (verified against
|
|
21
|
+
# git 2.39: `git -C <any-worktree> rev-parse --git-path hooks` returns the
|
|
22
|
+
# same common path for every worktree of the same repo, including the main
|
|
23
|
+
# one). A naive install there would mean each new worktree's guard silently
|
|
24
|
+
# clobbers every other worktree's guard (only the most-recently-created
|
|
25
|
+
# worktree's branch would ever be accepted), and every *other* worktree would
|
|
26
|
+
# then have its commits blocked outright, since the current branch would
|
|
27
|
+
# never match the last-installed expected branch. That is worse than doing
|
|
28
|
+
# nothing: it defeats the isolation this task exists to provide.
|
|
29
|
+
#
|
|
30
|
+
# The actual fix is git's per-worktree config file
|
|
31
|
+
# (`$GIT_DIR/worktrees/<name>/config.worktree`, gated behind the
|
|
32
|
+
# `extensions.worktreeConfig` repo extension) combined with the `core.hooksPath`
|
|
33
|
+
# setting, which git DOES resolve per-worktree when scoped that way. This
|
|
34
|
+
# script:
|
|
35
|
+
# 1. enables `extensions.worktreeConfig` on the repo if not already set
|
|
36
|
+
# (idempotent, additive, does not disturb any existing config value —
|
|
37
|
+
# it only enables the *possibility* of per-worktree config overrides)
|
|
38
|
+
# 2. resolves this worktree's own private git-dir via
|
|
39
|
+
# `git -C <worktree-path> rev-parse --absolute-git-dir` (this IS
|
|
40
|
+
# genuinely private per worktree — `.git/worktrees/<name>` for a linked
|
|
41
|
+
# worktree, verified distinct per worktree in the same test)
|
|
42
|
+
# 3. points `core.hooksPath`, set with `git config --worktree` (so it lands
|
|
43
|
+
# in that worktree's own config.worktree file, not the shared config),
|
|
44
|
+
# at a private `hooks-guard` directory under that private git-dir
|
|
45
|
+
# 4. writes the branch-guard `pre-commit` hook there
|
|
46
|
+
#
|
|
47
|
+
# This was manually verified end-to-end against a scratch repo with two
|
|
48
|
+
# worktrees: installing the guard for worktree A blocked wrong-branch commits
|
|
49
|
+
# in A and had zero effect on worktree B or the main repo, both of which kept
|
|
50
|
+
# using the shared default hooks directory untouched.
|
|
51
|
+
#
|
|
52
|
+
# This script is intended to be called automatically at the end of the
|
|
53
|
+
# `WorktreeCreate` hook in the canonical root `settings.json`, immediately
|
|
54
|
+
# after `git worktree add "$DIR" -b "$NAME"` — see that file for the wiring.
|
|
55
|
+
# It can also be run manually against any existing worktree, and is safe to
|
|
56
|
+
# re-run against the same worktree (idempotent — simply overwrites the hook).
|
|
57
|
+
#
|
|
58
|
+
# Exit codes:
|
|
59
|
+
# 0 hook installed successfully
|
|
60
|
+
# 1 invalid usage (wrong argument count)
|
|
61
|
+
# 2 <worktree-path> is not a valid git working tree
|
|
62
|
+
|
|
63
|
+
set -u
|
|
64
|
+
|
|
65
|
+
usage() {
|
|
66
|
+
echo "Usage: $0 <worktree-path> <branch-name>" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
[ "$#" -eq 2 ] || usage
|
|
71
|
+
WORKTREE_PATH="$1"
|
|
72
|
+
BRANCH_NAME="$2"
|
|
73
|
+
|
|
74
|
+
[ -n "$WORKTREE_PATH" ] || usage
|
|
75
|
+
[ -n "$BRANCH_NAME" ] || usage
|
|
76
|
+
|
|
77
|
+
if ! git -C "$WORKTREE_PATH" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
78
|
+
echo "install-worktree-commit-guard: '$WORKTREE_PATH' is not a valid git working tree" >&2
|
|
79
|
+
exit 2
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Enable per-worktree config overrides on the repo, if not already enabled.
|
|
83
|
+
# This is a one-time, repo-wide, additive flag — it does not move or change
|
|
84
|
+
# any existing config value, it only makes `git config --worktree` (used
|
|
85
|
+
# below) actually take effect instead of silently writing to a file git
|
|
86
|
+
# never reads.
|
|
87
|
+
if [ "$(git -C "$WORKTREE_PATH" config --get extensions.worktreeConfig 2>/dev/null)" != "true" ]; then
|
|
88
|
+
git -C "$WORKTREE_PATH" config extensions.worktreeConfig true
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# This worktree's own private git-dir. For a linked worktree this resolves to
|
|
92
|
+
# `.git/worktrees/<name>` (private to this worktree); for the main working
|
|
93
|
+
# tree it resolves to the common `.git` dir itself. Either way it is safe to
|
|
94
|
+
# scope a private hooks directory under it.
|
|
95
|
+
PRIVATE_GIT_DIR="$(git -C "$WORKTREE_PATH" rev-parse --absolute-git-dir)"
|
|
96
|
+
|
|
97
|
+
# A distinctly-named directory (not the default `hooks`) so this never
|
|
98
|
+
# collides with, or gets confused for, whatever the shared default hooks
|
|
99
|
+
# directory may already contain.
|
|
100
|
+
HOOKS_DIR="$PRIVATE_GIT_DIR/hooks-guard"
|
|
101
|
+
mkdir -p "$HOOKS_DIR"
|
|
102
|
+
|
|
103
|
+
# Scope the override to THIS worktree only, via its private config.worktree
|
|
104
|
+
# file — not the shared/common config, which would affect every worktree.
|
|
105
|
+
git -C "$WORKTREE_PATH" config --worktree core.hooksPath "$HOOKS_DIR"
|
|
106
|
+
|
|
107
|
+
HOOK_FILE="$HOOKS_DIR/pre-commit"
|
|
108
|
+
|
|
109
|
+
# The expected branch name is baked into the generated hook as a literal
|
|
110
|
+
# comparison value (it is fixed at install time — the branch this worktree
|
|
111
|
+
# was created for). The *actual* branch is always re-checked live, at commit
|
|
112
|
+
# time, via `git rev-parse --abbrev-ref HEAD`.
|
|
113
|
+
cat > "$HOOK_FILE" <<EOF
|
|
114
|
+
#!/usr/bin/env bash
|
|
115
|
+
# Installed by scripts/install-worktree-commit-guard.sh — do not edit by hand.
|
|
116
|
+
# Rejects any commit made while this worktree is not checked out on its
|
|
117
|
+
# assigned branch: '$BRANCH_NAME'.
|
|
118
|
+
set -u
|
|
119
|
+
|
|
120
|
+
expected_branch="$BRANCH_NAME"
|
|
121
|
+
current_branch="\$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
|
|
122
|
+
|
|
123
|
+
if [ "\$current_branch" != "\$expected_branch" ]; then
|
|
124
|
+
echo "pre-commit: commit blocked — this worktree is assigned to branch '\$expected_branch' but HEAD is currently on '\$current_branch'." >&2
|
|
125
|
+
echo "pre-commit: verification/implementation commits for this worktree must land on '\$expected_branch', never on any other branch (including main)." >&2
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
exit 0
|
|
130
|
+
EOF
|
|
131
|
+
|
|
132
|
+
chmod +x "$HOOK_FILE"
|
|
133
|
+
|
|
134
|
+
echo "install-worktree-commit-guard: installed branch guard for '$BRANCH_NAME' at $HOOK_FILE (core.hooksPath scoped to this worktree only)"
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/jenga-permission-level-switch.sh — switch the current session's
|
|
3
|
+
# 5-tier permission level by copying the matching template into
|
|
4
|
+
# .claude/settings.json and .agents/settings.json, and recording the level
|
|
5
|
+
# in .jenga-permission-level.json at the repo root.
|
|
6
|
+
#
|
|
7
|
+
# Usage: bash scripts/jenga-permission-level-switch.sh <n>
|
|
8
|
+
# <n> must be an integer 1-5.
|
|
9
|
+
#
|
|
10
|
+
# Behavior:
|
|
11
|
+
# - Templates live at templates/permission-levels/level-<n>-<name>.json
|
|
12
|
+
# and are COMPLETE settings.json documents (defaultMode, env,
|
|
13
|
+
# permissions, hooks). The copy is a whole-file overwrite of the
|
|
14
|
+
# destination, not a merge — this intentionally drops any extra
|
|
15
|
+
# top-level blocks (e.g. autoMode) present in the destination but
|
|
16
|
+
# absent from the template. autoMode is never used at any permission
|
|
17
|
+
# level (see PROJECT_SUMMARY.md, epic E33).
|
|
18
|
+
# - Both destination writes use a temp-file-then-atomic-rename pattern.
|
|
19
|
+
# - .jenga-permission-level.json is only written after BOTH settings.json
|
|
20
|
+
# copies succeed. If the .claude/ copy succeeds but the .agents/ copy
|
|
21
|
+
# fails, the script fails loud (no rollback of the .claude/ copy, no
|
|
22
|
+
# write to .jenga-permission-level.json) and instructs the user to
|
|
23
|
+
# re-run — re-running with the same <n> is idempotent and self-heals
|
|
24
|
+
# the inconsistency, since every copy is an unconditional overwrite.
|
|
25
|
+
# - Invalid input (missing/extra args, non-integer, out of range 1-5)
|
|
26
|
+
# exits non-zero and makes zero file changes.
|
|
27
|
+
#
|
|
28
|
+
# Exit codes:
|
|
29
|
+
# 0 Success — all three files updated consistently.
|
|
30
|
+
# 1 Invalid usage/argument — no files changed.
|
|
31
|
+
# 2 Mapped template file missing — no files changed.
|
|
32
|
+
# 3 Failed to copy template to .claude/settings.json — no files changed.
|
|
33
|
+
# 4 Failed to copy template to .agents/settings.json — .claude/settings.json
|
|
34
|
+
# WAS updated (inconsistent state); re-run the script to repair.
|
|
35
|
+
# 5 Failed to write .jenga-permission-level.json — both settings.json
|
|
36
|
+
# files WERE updated (inconsistent state); re-run the script to repair.
|
|
37
|
+
|
|
38
|
+
set -euo pipefail
|
|
39
|
+
|
|
40
|
+
usage() {
|
|
41
|
+
echo "Usage: $(basename "$0") <n> (n must be an integer 1-5)" >&2
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if [[ $# -ne 1 ]]; then
|
|
45
|
+
echo "Error: expected exactly one argument, got $#." >&2
|
|
46
|
+
usage
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
LEVEL="$1"
|
|
51
|
+
|
|
52
|
+
if ! [[ "$LEVEL" =~ ^[1-5]$ ]]; then
|
|
53
|
+
echo "Error: permission level must be an integer between 1 and 5, got '$LEVEL'." >&2
|
|
54
|
+
usage
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
case "$LEVEL" in
|
|
59
|
+
1) NAME="locked" ;;
|
|
60
|
+
2) NAME="guarded" ;;
|
|
61
|
+
3) NAME="standard" ;;
|
|
62
|
+
4) NAME="elevated" ;;
|
|
63
|
+
5) NAME="unrestricted" ;;
|
|
64
|
+
esac
|
|
65
|
+
|
|
66
|
+
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
67
|
+
TEMPLATE="$REPO_ROOT/templates/permission-levels/level-${LEVEL}-${NAME}.json"
|
|
68
|
+
CLAUDE_SETTINGS="$REPO_ROOT/.claude/settings.json"
|
|
69
|
+
AGENTS_SETTINGS="$REPO_ROOT/.agents/settings.json"
|
|
70
|
+
LEVEL_FILE="$REPO_ROOT/.jenga-permission-level.json"
|
|
71
|
+
|
|
72
|
+
if [[ ! -f "$TEMPLATE" ]]; then
|
|
73
|
+
echo "Error: template file not found for level $LEVEL: $TEMPLATE" >&2
|
|
74
|
+
exit 2
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Whole-file overwrite of destination via temp-file + atomic rename.
|
|
78
|
+
copy_template() {
|
|
79
|
+
local dest="$1"
|
|
80
|
+
local tmp="${dest}.tmp"
|
|
81
|
+
mkdir -p "$(dirname "$dest")"
|
|
82
|
+
cp "$TEMPLATE" "$tmp" && mv "$tmp" "$dest"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if ! copy_template "$CLAUDE_SETTINGS"; then
|
|
86
|
+
echo "Error: failed to copy template to $CLAUDE_SETTINGS. No files changed." >&2
|
|
87
|
+
rm -f "${CLAUDE_SETTINGS}.tmp"
|
|
88
|
+
exit 3
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
if ! copy_template "$AGENTS_SETTINGS"; then
|
|
92
|
+
echo "Error: failed to copy template to $AGENTS_SETTINGS." >&2
|
|
93
|
+
echo "State is now INCONSISTENT: $CLAUDE_SETTINGS was updated to level $LEVEL but $AGENTS_SETTINGS was not." >&2
|
|
94
|
+
echo ".jenga-permission-level.json was NOT updated. Re-run this script to repair (it is idempotent)." >&2
|
|
95
|
+
rm -f "${AGENTS_SETTINGS}.tmp"
|
|
96
|
+
exit 4
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Only write the metadata file after both settings.json copies succeeded.
|
|
100
|
+
LEVEL_TMP="${LEVEL_FILE}.tmp"
|
|
101
|
+
if ! jq -n --argjson n "$LEVEL" '{"session_level": $n}' > "$LEVEL_TMP" || ! mv "$LEVEL_TMP" "$LEVEL_FILE"; then
|
|
102
|
+
echo "Error: failed to write $LEVEL_FILE." >&2
|
|
103
|
+
echo "State is now INCONSISTENT: both settings.json files were updated to level $LEVEL but .jenga-permission-level.json was not. Re-run this script to repair (it is idempotent)." >&2
|
|
104
|
+
rm -f "$LEVEL_TMP"
|
|
105
|
+
exit 5
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
echo "Switched to level $LEVEL ($NAME)."
|
|
109
|
+
exit 0
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-harness.sh — Per-repository smoke test harness for inline-scoped task execution
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# bash scripts/smoke-harness.sh [changed_file ...]
|
|
6
|
+
#
|
|
7
|
+
# If changed files are supplied as positional arguments, those are used directly.
|
|
8
|
+
# If no arguments are given, the harness infers changed files from:
|
|
9
|
+
# git diff --name-only HEAD
|
|
10
|
+
#
|
|
11
|
+
# Discovery algorithm (in priority order):
|
|
12
|
+
# 1. scripts/smoke.sh — if present, delegate and return its exit code
|
|
13
|
+
# 2. npm test — if package.json has a .scripts.test entry and jq is available
|
|
14
|
+
# 3. pytest — if pytest is in PATH and setup.py/pyproject.toml exists
|
|
15
|
+
# 4. go test ./... — if go.mod exists
|
|
16
|
+
# 5. No runner found — pass if all changed files are config/docs; fail otherwise
|
|
17
|
+
#
|
|
18
|
+
# Exit codes:
|
|
19
|
+
# 0 — smoke test passed (or structural-only change with no runner available)
|
|
20
|
+
# 1 — smoke test failed (or logic files changed with no runner available)
|
|
21
|
+
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Resolve repo root
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Collect changed files
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Accept changed files as positional arguments; if none given, infer from git.
|
|
33
|
+
# Uses a portable loop rather than mapfile/readarray (bash 3.x safe for macOS).
|
|
34
|
+
if [[ $# -gt 0 ]]; then
|
|
35
|
+
CHANGED_FILES=("$@")
|
|
36
|
+
else
|
|
37
|
+
CHANGED_FILES=()
|
|
38
|
+
while IFS= read -r line; do
|
|
39
|
+
[[ -n "$line" ]] && CHANGED_FILES+=("$line")
|
|
40
|
+
done < <(git diff --name-only HEAD 2>/dev/null || true)
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Step 1: Delegate to scripts/smoke.sh if it exists
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
if [[ -f "$REPO_ROOT/scripts/smoke.sh" ]]; then
|
|
47
|
+
echo "INFO: Found scripts/smoke.sh — delegating to project smoke test."
|
|
48
|
+
bash "$REPO_ROOT/scripts/smoke.sh"
|
|
49
|
+
exit $?
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Step 2: Infer npm test
|
|
54
|
+
# Requires: package.json with a non-null .scripts.test field, and jq in PATH.
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
if [[ -f "$REPO_ROOT/package.json" ]]; then
|
|
57
|
+
if command -v jq > /dev/null 2>&1; then
|
|
58
|
+
if jq -e '.scripts.test // empty' "$REPO_ROOT/package.json" > /dev/null 2>&1; then
|
|
59
|
+
echo "INFO: Detected npm project with test script — running npm test."
|
|
60
|
+
cd "$REPO_ROOT" && npm test
|
|
61
|
+
exit $?
|
|
62
|
+
else
|
|
63
|
+
echo "INFO: package.json found but no .scripts.test entry — skipping npm test inference."
|
|
64
|
+
fi
|
|
65
|
+
else
|
|
66
|
+
echo "INFO: package.json found but jq is not available — skipping npm test inference."
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Step 3: Infer pytest
|
|
72
|
+
# Requires: pytest in PATH, and setup.py or pyproject.toml at repo root.
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
if command -v pytest > /dev/null 2>&1; then
|
|
75
|
+
if [[ -f "$REPO_ROOT/setup.py" || -f "$REPO_ROOT/pyproject.toml" ]]; then
|
|
76
|
+
echo "INFO: Detected Python project — running pytest."
|
|
77
|
+
cd "$REPO_ROOT" && pytest
|
|
78
|
+
exit $?
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Step 4: Infer go test
|
|
84
|
+
# Requires: go.mod at repo root.
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
if [[ -f "$REPO_ROOT/go.mod" ]]; then
|
|
87
|
+
echo "INFO: Detected Go project — running go test ./..."
|
|
88
|
+
cd "$REPO_ROOT" && go test ./...
|
|
89
|
+
exit $?
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Step 5: No test runner found — classify changed files
|
|
94
|
+
#
|
|
95
|
+
# Files are considered config/documentation if they match one of:
|
|
96
|
+
# - Extension: .json .yaml .yml .md .txt .toml .ini .cfg
|
|
97
|
+
# - Path prefix: docs/ project/ templates/
|
|
98
|
+
# - Shell scripts in scripts/: scripts/*.sh (structural scaffolding, not logic)
|
|
99
|
+
# All other files are treated as logic/source files requiring a real test.
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
CONFIG_DOC_PATTERN='\.(json|yaml|yml|md|txt|toml|ini|cfg)$'
|
|
102
|
+
STRUCTURAL_SCRIPT_PATTERN='^scripts/[^/]+\.sh$'
|
|
103
|
+
STRUCTURAL_PATH_PATTERN='^(docs|project|templates)/'
|
|
104
|
+
|
|
105
|
+
NON_CONFIG_FILES=()
|
|
106
|
+
for f in "${CHANGED_FILES[@]}"; do
|
|
107
|
+
is_config=false
|
|
108
|
+
|
|
109
|
+
# Check extension pattern
|
|
110
|
+
if echo "$f" | grep -qE "$CONFIG_DOC_PATTERN"; then
|
|
111
|
+
is_config=true
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Check structural script pattern (scripts/*.sh)
|
|
115
|
+
if echo "$f" | grep -qE "$STRUCTURAL_SCRIPT_PATTERN"; then
|
|
116
|
+
is_config=true
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# Check structural path prefix
|
|
120
|
+
if echo "$f" | grep -qE "$STRUCTURAL_PATH_PATTERN"; then
|
|
121
|
+
is_config=true
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
if [[ "$is_config" == "false" ]]; then
|
|
125
|
+
NON_CONFIG_FILES+=("$f")
|
|
126
|
+
fi
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
if [[ ${#NON_CONFIG_FILES[@]} -gt 0 ]]; then
|
|
130
|
+
echo "ERROR: No smoke test runner found and the task touches logic/source files:" >&2
|
|
131
|
+
for f in "${NON_CONFIG_FILES[@]}"; do
|
|
132
|
+
echo " - $f" >&2
|
|
133
|
+
done
|
|
134
|
+
echo "Add a test runner (scripts/smoke.sh, npm test, pytest, or go test) before marking this task complete." >&2
|
|
135
|
+
exit 1
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
echo "INFO: No test runner discoverable; all changed files are config/docs/structural. Smoke test passes."
|
|
139
|
+
exit 0
|
|
@@ -27,18 +27,76 @@ STATUS_VALUES = {
|
|
|
27
27
|
"Done",
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
# E39 tiered item-level caution/escalation. crucial_level is OPTIONAL — absence means no
|
|
31
|
+
# elevated caution. When present it must be one of these three values (see
|
|
32
|
+
# templates/SCRUM_BOARD_SCHEMA.md). crucial_set_by / crucial_note are free-text fields that are
|
|
33
|
+
# only recognised (not enum-validated) here; their "required when crucial_level is set" rule is
|
|
34
|
+
# an authoring discipline documented in the schema, not mechanically enforced by this script —
|
|
35
|
+
# the same treatment given to scope_rationale's "required when execution_scope is set" rule.
|
|
36
|
+
CRUCIAL_LEVEL_VALUES = {
|
|
37
|
+
"advisory",
|
|
38
|
+
"gated",
|
|
39
|
+
"locked",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Base keys shared by every item type, plus the E32 adaptive-execution-scope fields.
|
|
43
|
+
#
|
|
44
|
+
# The execution-scope fields are all OPTIONAL — a board file that omits them is valid and is
|
|
45
|
+
# treated as execution_scope: task / needs_docs: true (see the backward-compatibility rule in
|
|
46
|
+
# templates/SCRUM_BOARD_SCHEMA.md). They are listed here so that files which DO carry them are
|
|
47
|
+
# not rejected as unknown.
|
|
48
|
+
#
|
|
49
|
+
# Two groups, distinguished by who writes them:
|
|
50
|
+
# - assigned at breakdown time by the scrum-master (execution_scope … epic_scope_approval)
|
|
51
|
+
# - written at runtime by /close-story and /do (actual_files_changed … divergence_flag)
|
|
52
|
+
# Runtime fields appear only after a task has executed, so most task files will lack them.
|
|
53
|
+
# task_changed_files is deliberately absent: it lives in the bundle manifest JSON
|
|
54
|
+
# (project/queue/bundle-<E##_S##>.json), not in task frontmatter.
|
|
55
|
+
EXECUTION_SCOPE_KEYS = {
|
|
56
|
+
"execution_scope", "needs_docs", "scope_rationale",
|
|
57
|
+
"jenga_assigned", "override_justification", "epic_scope_approval",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
CLOSE_STORY_KEYS = {
|
|
61
|
+
"actual_files_changed", "actual_lines_delta", "scope_divergence_flag", "divergence_flag",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# E39 tiered item-level caution/escalation fields. OPTIONAL and story/task-only — epics do not
|
|
65
|
+
# carry these; an epic's risk gating is already handled by epic_scope_approval. See
|
|
66
|
+
# templates/SCRUM_BOARD_SCHEMA.md.
|
|
67
|
+
#
|
|
68
|
+
# crucial_declined / crucial_declined_note (E39_S02_T03) are a separate optional pair recording
|
|
69
|
+
# that a scrum-master-proposed crucial_level was explicitly declined by the user for this item —
|
|
70
|
+
# the durable decline-tracking mechanism that stops the breakdown step from re-proposing on a
|
|
71
|
+
# later pass. Like crucial_set_by / crucial_note, they are free-text/recognised-not-enum-validated
|
|
72
|
+
# here; their "required when crucial_declined: true" rule is an authoring discipline documented in
|
|
73
|
+
# the schema, not mechanically enforced by this script.
|
|
74
|
+
CRUCIAL_KEYS = {
|
|
75
|
+
"crucial_level", "crucial_set_by", "crucial_note",
|
|
76
|
+
"crucial_declined", "crucial_declined_note",
|
|
77
|
+
}
|
|
78
|
+
|
|
30
79
|
ALLOWED_KEYS = {
|
|
31
80
|
"epic": {
|
|
32
81
|
"id", "title", "status", "date_created", "date_started", "date_completed",
|
|
33
82
|
"dates_previously_completed", "reopened_on", "reopened_reason", "stories", "docs",
|
|
83
|
+
"epic_scope_approval",
|
|
84
|
+
# OPTIONAL. Marks how the epic came to exist; only written by `/uncharted onboard`
|
|
85
|
+
# (value: backfilled). Absence means the epic was authored normally, so every existing
|
|
86
|
+
# epic that omits it stays valid. See templates/SCRUM_BOARD_SCHEMA.md.
|
|
87
|
+
"provenance",
|
|
34
88
|
},
|
|
35
89
|
"story": {
|
|
36
90
|
"id", "epic_id", "title", "status", "date_created", "date_started", "date_completed",
|
|
37
91
|
"dates_previously_completed", "reopened_on", "reopened_reason", "tasks", "docs",
|
|
92
|
+
"priority", "depends_on",
|
|
93
|
+
*CRUCIAL_KEYS,
|
|
38
94
|
},
|
|
39
95
|
"task": {
|
|
40
96
|
"id", "story_id", "epic_id", "title", "status", "date_created", "date_started", "date_completed",
|
|
41
97
|
"dates_previously_completed", "reopened_on", "reopened_reason", "assigned_to", "docs",
|
|
98
|
+
"depends_on",
|
|
99
|
+
*EXECUTION_SCOPE_KEYS, *CLOSE_STORY_KEYS, *CRUCIAL_KEYS,
|
|
42
100
|
},
|
|
43
101
|
}
|
|
44
102
|
|
|
@@ -152,6 +210,10 @@ def validate_file(source: Path):
|
|
|
152
210
|
if status and status not in STATUS_VALUES:
|
|
153
211
|
raise ValueError(f"{source}: invalid status '{status}'")
|
|
154
212
|
|
|
213
|
+
crucial_level = data.get("crucial_level")
|
|
214
|
+
if crucial_level and crucial_level not in CRUCIAL_LEVEL_VALUES:
|
|
215
|
+
raise ValueError(f"{source}: invalid crucial_level '{crucial_level}'")
|
|
216
|
+
|
|
155
217
|
docs = data.get("docs", None)
|
|
156
218
|
if docs is not None:
|
|
157
219
|
validate_docs(source, docs)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/with-lock.sh — atomic, cross-platform exclusive file lock wrapper
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# scripts/with-lock.sh <target-file> -- <command> [args...]
|
|
6
|
+
#
|
|
7
|
+
# Acquires an exclusive lock keyed to <target-file> before running <command>,
|
|
8
|
+
# and always releases the lock afterward — on success, on failure, and on
|
|
9
|
+
# signal (INT/TERM sent to this script are forwarded to the wrapped command,
|
|
10
|
+
# which is then waited on before the lock is released — see the signal
|
|
11
|
+
# handling note further down). The wrapped command's exit code is preserved
|
|
12
|
+
# as this script's own exit code.
|
|
13
|
+
#
|
|
14
|
+
# This replaces the purely advisory "check for a .lock file, wait, retry,
|
|
15
|
+
# then create/delete it yourself" convention previously described in
|
|
16
|
+
# templates/SCRUM_BOARD_SCHEMA.md. That convention relied on every caller
|
|
17
|
+
# implementing check-wait-retry-cleanup correctly, including on error paths,
|
|
18
|
+
# with nothing to mechanically stop two writers from both deciding the lock
|
|
19
|
+
# is free at the same time. This script enforces exclusivity instead.
|
|
20
|
+
#
|
|
21
|
+
# Lock primitive: `mkdir` on a lock directory, NOT `flock`. `flock` is a
|
|
22
|
+
# Linux-only (util-linux) utility and is not present by default on macOS/BSD
|
|
23
|
+
# (this repo runs on Darwin). POSIX `mkdir` is atomic on every platform this
|
|
24
|
+
# repo targets: the kernel guarantees that when multiple processes race to
|
|
25
|
+
# create the same directory, exactly one succeeds and every other call fails
|
|
26
|
+
# with EEXIST. That atomicity — not any cooperative check beforehand — is
|
|
27
|
+
# what makes this a real mutual-exclusion lock instead of advisory prose.
|
|
28
|
+
#
|
|
29
|
+
# Environment overrides (all optional):
|
|
30
|
+
# WITH_LOCK_TIMEOUT_SECONDS Max time to wait for a held lock (default: 30)
|
|
31
|
+
# WITH_LOCK_POLL_SECONDS Poll interval while waiting (default: 0.2)
|
|
32
|
+
# WITH_LOCK_STALE_SECONDS Age after which a still-held lock is treated
|
|
33
|
+
# as abandoned (crashed holder) and reclaimed
|
|
34
|
+
# (default: 60)
|
|
35
|
+
#
|
|
36
|
+
# Exit codes:
|
|
37
|
+
# 0 lock acquired, command ran, exit code is the command's own
|
|
38
|
+
# 1 invalid usage
|
|
39
|
+
# 2 could not acquire the lock within the timeout — the command was
|
|
40
|
+
# NEVER run (fail safe; no partial or silently-clobbered write)
|
|
41
|
+
# * otherwise, the wrapped command's own exit code
|
|
42
|
+
|
|
43
|
+
set -u
|
|
44
|
+
|
|
45
|
+
usage() {
|
|
46
|
+
echo "Usage: $0 <target-file> -- <command> [args...]" >&2
|
|
47
|
+
exit 1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[ "$#" -ge 1 ] || usage
|
|
51
|
+
TARGET_FILE="$1"
|
|
52
|
+
shift
|
|
53
|
+
|
|
54
|
+
[ "${1:-}" = "--" ] || usage
|
|
55
|
+
shift
|
|
56
|
+
|
|
57
|
+
[ "$#" -ge 1 ] || usage
|
|
58
|
+
|
|
59
|
+
TIMEOUT_SECONDS="${WITH_LOCK_TIMEOUT_SECONDS:-30}"
|
|
60
|
+
POLL_SECONDS="${WITH_LOCK_POLL_SECONDS:-0.2}"
|
|
61
|
+
STALE_SECONDS="${WITH_LOCK_STALE_SECONDS:-60}"
|
|
62
|
+
|
|
63
|
+
LOCK_DIR="${TARGET_FILE}.lock.d"
|
|
64
|
+
LOCK_PID_FILE="${LOCK_DIR}/pid"
|
|
65
|
+
|
|
66
|
+
now_epoch() {
|
|
67
|
+
date +%s
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Portable mtime lookup: GNU stat (Linux) uses -c, BSD stat (macOS) uses -f.
|
|
71
|
+
# Try GNU form first, fall back to BSD form. Echoes nothing (and returns
|
|
72
|
+
# non-zero) if the directory vanished in the meantime.
|
|
73
|
+
lock_mtime_epoch() {
|
|
74
|
+
stat -c %Y "$LOCK_DIR" 2>/dev/null || stat -f %m "$LOCK_DIR" 2>/dev/null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# If the held lock looks abandoned (older than STALE_SECONDS), reclaim it.
|
|
78
|
+
# This does NOT grant the lock by itself — it only clears the way. The next
|
|
79
|
+
# mkdir attempt in the polling loop is still the sole arbiter of who
|
|
80
|
+
# proceeds, so simultaneous reclaim attempts by multiple waiters never let
|
|
81
|
+
# more than one of them through.
|
|
82
|
+
maybe_reclaim_stale_lock() {
|
|
83
|
+
local mtime now age
|
|
84
|
+
mtime="$(lock_mtime_epoch)" || return 0
|
|
85
|
+
now="$(now_epoch)"
|
|
86
|
+
age=$(( now - mtime ))
|
|
87
|
+
if [ "$age" -ge "$STALE_SECONDS" ]; then
|
|
88
|
+
echo "with-lock: reclaiming stale lock '$LOCK_DIR' (age ${age}s >= ${STALE_SECONDS}s)" >&2
|
|
89
|
+
rm -rf "$LOCK_DIR" 2>/dev/null
|
|
90
|
+
fi
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
acquire_lock() {
|
|
94
|
+
local start now elapsed
|
|
95
|
+
start="$(now_epoch)"
|
|
96
|
+
while true; do
|
|
97
|
+
if mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
98
|
+
echo "$$" > "$LOCK_PID_FILE" 2>/dev/null || true
|
|
99
|
+
return 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
maybe_reclaim_stale_lock
|
|
103
|
+
|
|
104
|
+
now="$(now_epoch)"
|
|
105
|
+
elapsed=$(( now - start ))
|
|
106
|
+
if [ "$elapsed" -ge "$TIMEOUT_SECONDS" ]; then
|
|
107
|
+
return 1
|
|
108
|
+
fi
|
|
109
|
+
sleep "$POLL_SECONDS" 2>/dev/null || sleep 1
|
|
110
|
+
done
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
release_lock() {
|
|
114
|
+
rm -rf "$LOCK_DIR" 2>/dev/null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if ! acquire_lock; then
|
|
118
|
+
echo "with-lock: failed to acquire lock on '$TARGET_FILE' within ${TIMEOUT_SECONDS}s (lock dir: $LOCK_DIR) — command was not run" >&2
|
|
119
|
+
exit 2
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# Always release the lock on exit, regardless of how the wrapped command
|
|
123
|
+
# (or this script itself) terminates.
|
|
124
|
+
trap release_lock EXIT
|
|
125
|
+
|
|
126
|
+
# Signal handling note: the wrapped command is run in the background and
|
|
127
|
+
# waited on explicitly (rather than exec'd directly in the foreground) so
|
|
128
|
+
# that INT/TERM delivered to this script are handled promptly. Bash defers
|
|
129
|
+
# non-EXIT traps until a foreground command completes, so a script that
|
|
130
|
+
# instead ran `"$@"` directly in the foreground would not react to a signal
|
|
131
|
+
# sent to its own PID until the wrapped command finished on its own —
|
|
132
|
+
# release would then only happen once the command exited naturally (or, in
|
|
133
|
+
# the worst case, once WITH_LOCK_STALE_SECONDS elapsed and another waiter
|
|
134
|
+
# reclaimed the lock). Backgrounding + `wait` avoids that: the trap fires as
|
|
135
|
+
# soon as the signal arrives, forwards it to the child, waits for the child
|
|
136
|
+
# to actually exit, then this script exits (triggering the EXIT trap above,
|
|
137
|
+
# which releases the lock).
|
|
138
|
+
CHILD_PID=""
|
|
139
|
+
|
|
140
|
+
forward_signal_and_exit() {
|
|
141
|
+
local sig="$1" exit_code="$2"
|
|
142
|
+
if [ -n "$CHILD_PID" ]; then
|
|
143
|
+
kill -s "$sig" "$CHILD_PID" 2>/dev/null
|
|
144
|
+
wait "$CHILD_PID" 2>/dev/null
|
|
145
|
+
fi
|
|
146
|
+
exit "$exit_code"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
trap 'forward_signal_and_exit TERM 143' TERM
|
|
150
|
+
trap 'forward_signal_and_exit INT 130' INT
|
|
151
|
+
|
|
152
|
+
"$@" &
|
|
153
|
+
CHILD_PID=$!
|
|
154
|
+
wait "$CHILD_PID"
|
|
155
|
+
status=$?
|
|
156
|
+
CHILD_PID=""
|
|
157
|
+
|
|
158
|
+
exit "$status"
|