@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.
Files changed (117) hide show
  1. package/README.md +10 -7
  2. package/agents/developer.md +82 -2
  3. package/agents/scrum-master.md +215 -21
  4. package/agents/tester.md +90 -8
  5. package/hooks/on_session_end.sh +171 -20
  6. package/mcp/router/embedder.js +1 -1
  7. package/mcp/training_runner/index.js +239 -0
  8. package/mcp/training_runner/package-lock.json +1065 -0
  9. package/mcp/training_runner/package.json +15 -0
  10. package/package.json +14 -16
  11. package/scripts/check-permission-level.sh +107 -0
  12. package/scripts/check-publicignore-match.sh +122 -0
  13. package/scripts/check-worktree-liveness.sh +193 -0
  14. package/scripts/generate-rapport-manifest.sh +43 -0
  15. package/scripts/idea_manager.sh +47 -0
  16. package/scripts/install-worktree-commit-guard.sh +134 -0
  17. package/scripts/jenga-permission-level-switch.sh +109 -0
  18. package/scripts/smoke-harness.sh +139 -0
  19. package/scripts/validate-board.sh +62 -0
  20. package/scripts/with-lock.sh +158 -0
  21. package/scripts/worktree-remove-guard.sh +204 -0
  22. package/skills/clearify/SKILL.md +52 -0
  23. package/skills/close-story/SKILL.md +203 -0
  24. package/skills/close-story/scripts/check-story-closeable.sh +195 -0
  25. package/skills/close-story/scripts/compute-scope-divergence.sh +128 -0
  26. package/skills/close-story/scripts/extract-diff-stats.sh +48 -0
  27. package/skills/close-story/scripts/extract-task-diff-stats.sh +97 -0
  28. package/skills/close-story/scripts/update-task-frontmatter.sh +103 -0
  29. package/skills/commit/SKILL.md +30 -3
  30. package/skills/distribute/CONFIG_SCHEMA.md +148 -0
  31. package/skills/distribute/SKILL.md +173 -0
  32. package/skills/distribute/scripts/check-version.sh +74 -0
  33. package/skills/distribute/scripts/commit-version-bump.sh +108 -0
  34. package/skills/distribute/scripts/distribute-changes.sh +381 -0
  35. package/skills/do/SKILL.md +352 -1
  36. package/skills/do/assets/intent-vs-diff-prompt.md +69 -0
  37. package/skills/doc/assets/path-objectives.yaml +13 -0
  38. package/skills/doc-sync/SKILL.md +16 -0
  39. package/skills/doc-sync/assets/doc_targets.md +11 -0
  40. package/skills/idea/SKILL.md +56 -0
  41. package/skills/idea/assets/idea_handoff_template.md +26 -0
  42. package/skills/idea/assets/idea_template.md +3 -0
  43. package/skills/init/SKILL.md +101 -7
  44. package/skills/init/assets/directory_structure.txt +1 -0
  45. package/skills/init/assets/strategy_stub_template.md +38 -0
  46. package/skills/init/assets/workflow_template.json +1 -1
  47. package/skills/init/scripts/apply-project-visibility.sh +176 -0
  48. package/skills/init/scripts/detect-existing-codebase.sh +166 -0
  49. package/skills/init/scripts/init.sh +35 -1
  50. package/skills/jenga/SKILL.md +206 -14
  51. package/skills/jenga/scripts/board-scan.sh +238 -0
  52. package/skills/jenga/scripts/cascade-resolve.sh +297 -0
  53. package/skills/jenga/scripts/render-confirmation.sh +679 -0
  54. package/skills/jenga/scripts/render-picker.sh +439 -0
  55. package/skills/jenga/scripts/resolve-id.sh +367 -0
  56. package/skills/jenga-permission-level/SKILL.md +81 -0
  57. package/skills/proceed/SKILL.md +1 -1
  58. package/skills/publish/SKILL.md +8 -5
  59. package/skills/publish/assets/ci-contract.md +2 -2
  60. package/skills/publish/assets/ownership-matrix.md +1 -1
  61. package/skills/publish/scripts/finalize_changelog.sh +115 -0
  62. package/skills/publish/scripts/generate_release_notes.sh +475 -28
  63. package/skills/publish/scripts/npm_ci_pipeline.sh +44 -6
  64. package/skills/publish/scripts/publish_deploy.sh +38 -8
  65. package/skills/publish/scripts/run_gates.sh +2 -2
  66. package/skills/reconcile/SKILL.md +117 -5
  67. package/skills/reconcile/scripts/detect-unlinked-code.sh +741 -0
  68. package/skills/skillify/assets/init-new/assets/directory_structure.txt +5 -1
  69. package/skills/spinoff/SKILL.md +12 -7
  70. package/skills/todo/SKILL.md +2 -0
  71. package/skills/uncharted/SKILL.md +711 -0
  72. package/skills/uncharted/assets/SEGMENT_PROPOSAL_TEMPLATE.md +129 -0
  73. package/skills/uncharted/assets/UNDERSTANDING_DOC_TEMPLATE.md +160 -0
  74. package/skills/uncharted/scripts/apply-subsystem-cap.sh +573 -0
  75. package/skills/uncharted/scripts/detect-dependencies.sh +732 -0
  76. package/skills/uncharted/scripts/detect-tests.sh +553 -0
  77. package/skills/uncharted/scripts/discover-subsystems.sh +1029 -0
  78. package/skills/uncharted/scripts/enumerate-target.sh +470 -0
  79. package/skills/uncharted/scripts/import-source.sh +517 -0
  80. package/skills/uncharted/scripts/inspect-provenance.sh +573 -0
  81. package/skills/uncharted/scripts/resolve-segment-target.sh +640 -0
  82. package/skills/uncharted/scripts/run-engine.sh +655 -0
  83. package/skills/uncharted/scripts/validate-proposed-items.sh +125 -0
  84. package/skills/uncharted/scripts/write-backfilled-epics.sh +498 -0
  85. package/skills/wtf/SKILL.md +20 -0
  86. package/templates/CHANGELOG_TEMPLATE.md +13 -0
  87. package/templates/PROBLEM_RAPPORT_TEMPLATE.md +4 -1
  88. package/templates/SCRUM_BOARD_SCHEMA.md +206 -10
  89. package/templates/permission-levels/README.md +73 -0
  90. package/templates/permission-levels/level-1-locked.json +71 -0
  91. package/templates/permission-levels/level-2-guarded.json +64 -0
  92. package/templates/permission-levels/level-3-standard.json +62 -0
  93. package/templates/permission-levels/level-4-elevated.json +60 -0
  94. package/templates/permission-levels/level-5-unrestricted.json +58 -0
  95. package/skills/convert/SKILL.md +0 -124
  96. package/skills/convert/convert_cli.py +0 -235
  97. package/skills/convert/tests/sample.csv +0 -4
  98. package/skills/convert/tests/sample.json +0 -5
  99. package/skills/convert/tests/sample.jsonl +0 -3
  100. package/skills/convert/tests/sample.yaml +0 -18
  101. package/skills/convert/tests/sample_obj.csv +0 -2
  102. package/skills/convert/tests/sample_obj.json +0 -9
  103. package/skills/mirror-public/SKILL.md +0 -237
  104. package/skills/mirror-public/assets/config.json +0 -5
  105. package/skills/mirror-public/scripts/mirror.sh +0 -374
  106. package/skills/self-sync/SKILL.md +0 -73
  107. package/skills/self-sync/scripts/run.js +0 -136
  108. package/skills/train/SKILL.md +0 -116
  109. package/skills/train/assets/dashboard-templates/classifiers.html +0 -106
  110. package/skills/train/assets/dashboard-templates/nlp.html +0 -102
  111. package/skills/train/assets/dashboard-templates/transformers.html +0 -98
  112. package/skills/train/assets/results-parsers/__init__.py +0 -9
  113. package/skills/train/assets/results-parsers/classifiers.py +0 -84
  114. package/skills/train/assets/results-parsers/nlp.py +0 -88
  115. package/skills/train/assets/results-parsers/reporter.py +0 -154
  116. package/skills/train/assets/results-parsers/transformers.py +0 -120
  117. 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"