@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,204 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/worktree-remove-guard.sh — hard-block-by-default liveness
3
+ # precondition for `git worktree remove`, wired into the `WorktreeRemove`
4
+ # hook in (root, canonical) settings.json.
5
+ #
6
+ # Why this exists: see project/board/epics/E37_inter-agent-completion-signaling.md
7
+ # and project/board/stories/E37_S03_worktree-remove-liveness-precondition.md.
8
+ # `git worktree remove --force` previously ran unconditionally as soon as
9
+ # the WorktreeRemove hook fired, with no precondition of any kind — this is
10
+ # how a 2026-08-25 incident's orphaned shell polling loops got their cwd
11
+ # deleted out from under them. This script now runs
12
+ # scripts/check-worktree-liveness.sh (E37_S03_T01) first, and only proceeds
13
+ # to `git worktree remove --force` if that check reports the path clear (or
14
+ # an explicit override is set).
15
+ #
16
+ # Invocation modes:
17
+ #
18
+ # 1. Hook mode (no positional worktree-path argument): reads a JSON
19
+ # payload from stdin and extracts `.worktree_path` (required) and an
20
+ # optional `.force_ignore_liveness` boolean field. This is how the
21
+ # WorktreeRemove hook actually invokes this script — matching the
22
+ # pre-existing `jq -r '.worktree_path'` convention this script
23
+ # replaces. Note: the native harness's WorktreeRemove hook JSON
24
+ # schema is outside this repo's control (its `ExitWorktree` tool
25
+ # exposes only `action`/`discard_changes`, no pass-through custom
26
+ # field), so `.force_ignore_liveness` on the payload is supported for
27
+ # any caller that *does* control the JSON directly (e.g. a script
28
+ # invoking this guard outside the native tool), but is not the primary
29
+ # override mechanism for the native hook path — see the environment
30
+ # variable below for that.
31
+ #
32
+ # 2. CLI mode (direct/manual invocation — used for this script's own
33
+ # testing and any other scripted use outside the hook):
34
+ # scripts/worktree-remove-guard.sh [--force-ignore-liveness] <worktree-path>
35
+ #
36
+ # Override (works in both modes): set the environment variable
37
+ # WORKTREE_REMOVE_FORCE_IGNORE_LIVENESS=1
38
+ # immediately before invoking this script (directly, or before whatever
39
+ # triggers the WorktreeRemove hook). Accepted truthy values: 1, true, TRUE,
40
+ # True, yes, YES. This is the one lever guaranteed to work regardless of
41
+ # invocation mode or what JSON the calling harness does or doesn't pass
42
+ # through. It must be set explicitly on each invocation — it is never read
43
+ # from a file, never cached, and never a default; every invocation re-reads
44
+ # the environment fresh.
45
+ #
46
+ # Decision logic:
47
+ # - If an override is set (CLI flag, JSON field, or env var): skip the
48
+ # liveness check, print a visible bypass notice to stderr, and run
49
+ # `git worktree remove --force <path>`.
50
+ # - Otherwise, run scripts/check-worktree-liveness.sh <path>. ANY non-zero
51
+ # exit blocks removal by default — not just exit 1 ("live process
52
+ # found"), but also exit 2/3/4 (usage error / invalid path / liveness
53
+ # indeterminate), since none of those represent "the path is
54
+ # affirmatively clear," consistent with that script's own fail-closed
55
+ # design. `git worktree remove` is never invoked in this branch. The
56
+ # liveness check's own PID/command output (or error message) is
57
+ # surfaced to the caller, plus a note on how to override.
58
+ # - On a clear (exit 0) result: proceed to `git worktree remove --force
59
+ # <path>` exactly as the old unconditional one-liner did, with no extra
60
+ # output — the "clear worktree" case must behave exactly as before,
61
+ # with no added friction.
62
+ #
63
+ # Exit codes:
64
+ # * whatever `git worktree remove --force <path>` itself exits with, if
65
+ # removal proceeded (clear path, or override set)
66
+ # 1 blocked — liveness check reported non-zero and no override was set;
67
+ # git worktree remove was NOT run
68
+ # 2 usage error — no worktree path resolvable from CLI args or stdin
69
+ # JSON, or an unrecognized flag
70
+
71
+ set -u
72
+
73
+ SCRIPT_NAME="worktree-remove-guard.sh"
74
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)"
75
+ LIVENESS_SCRIPT="$SCRIPT_DIR/check-worktree-liveness.sh"
76
+
77
+ # Resolve a safe anchor directory to relocate this process's own cwd into
78
+ # further below (see the "Caller-cwd self-detection hardening" block after
79
+ # argument parsing) — the main repository's working-tree root, derived from
80
+ # the shared git common dir so it resolves correctly regardless of which
81
+ # worktree this script's own cwd currently happens to be in. Must be
82
+ # computed now, from the ORIGINAL cwd, since `git rev-parse` depends on
83
+ # being inside a git working tree.
84
+ GIT_COMMON_DIR_RAW="$(git rev-parse --git-common-dir 2>/dev/null)"
85
+ SAFE_ANCHOR=""
86
+ if [ -n "$GIT_COMMON_DIR_RAW" ]; then
87
+ SAFE_ANCHOR="$(cd "$(dirname "$GIT_COMMON_DIR_RAW")" >/dev/null 2>&1 && pwd -P)"
88
+ fi
89
+
90
+ FORCE_OVERRIDE=0
91
+ WORKTREE_PATH=""
92
+
93
+ # --- Parse CLI-style args, if any. ---
94
+ while [ "$#" -gt 0 ]; do
95
+ case "$1" in
96
+ --force-ignore-liveness)
97
+ FORCE_OVERRIDE=1
98
+ shift
99
+ ;;
100
+ --)
101
+ shift
102
+ break
103
+ ;;
104
+ -*)
105
+ echo "$SCRIPT_NAME: unknown flag '$1'" >&2
106
+ exit 2
107
+ ;;
108
+ *)
109
+ break
110
+ ;;
111
+ esac
112
+ done
113
+ if [ "$#" -ge 1 ]; then
114
+ WORKTREE_PATH="$1"
115
+ fi
116
+
117
+ # --- Hook mode: no positional worktree path given, so read the JSON
118
+ # payload the WorktreeRemove hook receives on stdin. ---
119
+ if [ -z "$WORKTREE_PATH" ]; then
120
+ if [ -t 0 ]; then
121
+ echo "$SCRIPT_NAME: no worktree path given (expected a positional argument, or JSON on stdin with a 'worktree_path' field)" >&2
122
+ exit 2
123
+ fi
124
+ PAYLOAD="$(cat)"
125
+ WORKTREE_PATH="$(printf '%s' "$PAYLOAD" | jq -r '.worktree_path // empty' 2>/dev/null)"
126
+ if [ "$FORCE_OVERRIDE" -eq 0 ]; then
127
+ PAYLOAD_OVERRIDE="$(printf '%s' "$PAYLOAD" | jq -r 'if .force_ignore_liveness == true then "true" else "false" end' 2>/dev/null)"
128
+ [ "$PAYLOAD_OVERRIDE" = "true" ] && FORCE_OVERRIDE=1
129
+ fi
130
+ fi
131
+
132
+ if [ -z "$WORKTREE_PATH" ]; then
133
+ echo "$SCRIPT_NAME: no worktree path provided (expected a positional argument, or a 'worktree_path' field on stdin JSON)" >&2
134
+ exit 2
135
+ fi
136
+
137
+ # Resolve WORKTREE_PATH to an absolute, symlink-resolved path NOW, while
138
+ # still in the original cwd — we're about to relocate this process's own
139
+ # cwd away (hardening below), and a relative path would no longer resolve
140
+ # correctly against the caller's original cwd afterward. (In practice the
141
+ # hook always supplies an absolute path — see WorktreeCreate's own
142
+ # `DIR="$JENGA_PROJECT_DIR/.claude/worktrees/$NAME"` construction — this
143
+ # also makes relative paths work correctly for CLI-mode/manual use.) If
144
+ # resolution fails (path does not exist), fall through with the original,
145
+ # unresolved string; check-worktree-liveness.sh and/or `git worktree
146
+ # remove` will report the appropriate error themselves.
147
+ WORKTREE_PATH_RESOLVED="$(cd "$WORKTREE_PATH" 2>/dev/null && pwd -P)"
148
+ [ -n "$WORKTREE_PATH_RESOLVED" ] && WORKTREE_PATH="$WORKTREE_PATH_RESOLVED"
149
+
150
+ # --- Environment override — checked last so it can force the bypass
151
+ # regardless of how the path was supplied. ---
152
+ case "${WORKTREE_REMOVE_FORCE_IGNORE_LIVENESS:-}" in
153
+ 1|true|TRUE|True|yes|YES)
154
+ FORCE_OVERRIDE=1
155
+ ;;
156
+ esac
157
+
158
+ # Caller-cwd self-detection hardening (this script's own layer of it — see
159
+ # the matching comment block in check-worktree-liveness.sh for the
160
+ # underlying check script's own layer, which protects only its own spawned
161
+ # subprocesses). THIS script is what actually invokes that check below —
162
+ # if this script's own process was itself launched with a cwd already
163
+ # inside the worktree being removed (a real, expected scenario: the
164
+ # WorktreeRemove hook may fire from a session whose cwd is inside the
165
+ # worktree about to be removed, right before cleanup), this script's own
166
+ # PID would itself be "a live process rooted in" that path at the moment
167
+ # the check runs — check-worktree-liveness.sh's $$-exclusion only ever
168
+ # covers its own PID, not its caller's. Relocating to the main repo root
169
+ # (resolved above, before WORKTREE_PATH is used any further) — rather than
170
+ # to `/` — means both the liveness check AND the eventual `git worktree
171
+ # remove` below keep working normally (git needs to run from inside some
172
+ # working tree), while still guaranteeing neither this script's own process
173
+ # nor the check script it spawns as a child can be cwd'd inside the target,
174
+ # regardless of what cwd the original caller had. Falls back to `/` only if
175
+ # the repo-root anchor could not be resolved (a degenerate case; `git
176
+ # worktree remove` itself will then fail with its own clear error).
177
+ # (Residual, out-of-our-control limitation: if some ancestor further up the
178
+ # process chain — e.g. the harness's own hook-runner shell, if it stays
179
+ # resident rather than exec'ing straight into this script — independently
180
+ # keeps its own cwd inside the target and stays alive while waiting on this
181
+ # script, that ancestor is a separate process this script cannot
182
+ # retroactively relocate; the courtesy $$-style exclusion pattern used
183
+ # throughout these two scripts only ever covers processes each script
184
+ # itself controls.)
185
+ cd "${SAFE_ANCHOR:-/}" 2>/dev/null || true
186
+
187
+ if [ "$FORCE_OVERRIDE" -eq 1 ]; then
188
+ echo "$SCRIPT_NAME: liveness check explicitly bypassed (--force-ignore-liveness / WORKTREE_REMOVE_FORCE_IGNORE_LIVENESS) for '$WORKTREE_PATH' — proceeding to remove without checking for live processes." >&2
189
+ git worktree remove --force "$WORKTREE_PATH"
190
+ exit $?
191
+ fi
192
+
193
+ LIVENESS_OUTPUT="$("$LIVENESS_SCRIPT" "$WORKTREE_PATH" 2>&1)"
194
+ LIVENESS_EXIT=$?
195
+
196
+ if [ "$LIVENESS_EXIT" -eq 0 ]; then
197
+ git worktree remove --force "$WORKTREE_PATH"
198
+ exit $?
199
+ fi
200
+
201
+ echo "$SCRIPT_NAME: BLOCKED — not removing '$WORKTREE_PATH' (liveness check exited $LIVENESS_EXIT):" >&2
202
+ [ -n "$LIVENESS_OUTPUT" ] && echo "$LIVENESS_OUTPUT" >&2
203
+ echo "$SCRIPT_NAME: git worktree remove was NOT run. If this is expected (e.g. a known, accepted background process), re-run with an explicit override: pass --force-ignore-liveness (CLI mode), a true 'force_ignore_liveness' field on the input JSON (hook mode), or set WORKTREE_REMOVE_FORCE_IGNORE_LIVENESS=1 in the environment (either mode)." >&2
204
+ exit 1
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: clearify
3
+ description: Clarifies ambiguous, dense, or under-specified prompts and conversation on request — inspects an attached prompt or falls back to the current conversation and surfaces plain-language clarifications with examples.
4
+ keywords:
5
+ - clarify this
6
+ - clarify
7
+ - what do you mean
8
+ - I don't understand
9
+ - wtf
10
+ - explain that again
11
+ - can you simplify
12
+ examples:
13
+ - "clarify this"
14
+ - "clearify the last message"
15
+ - "wtf does this mean"
16
+ - "I don't understand what you're asking me to do here"
17
+ alias: wtf
18
+ ---
19
+
20
+ # Clearify — Ambiguity Clarification
21
+
22
+ > **Note on the `alias: wtf` frontmatter field:** this repo has no runtime mechanism that reads an `alias` key to route slash commands — no existing `SKILL.md` implements one, and `project/configs/workflow.json` has no alias registry. The field here documents the intended relationship only. `/wtf` is made invocable as a working alias by the companion skill folder at `skills/wtf/SKILL.md`, which delegates to these same instructions.
23
+
24
+ ## Instructions
25
+
26
+ 1. **Determine the target content.**
27
+ - If the user attached a prompt, file, or pasted block of text with this invocation, treat that as the target content.
28
+ - Otherwise, fall back to the most recent user message, plus any surrounding conversation context needed to make sense of it (e.g. the message it's replying to, an earlier instruction it references).
29
+ - Note: this is the only fallback distinction that matters — do not ask the user which content to clarify; infer it from what's available.
30
+
31
+ 2. **Identify ambiguous or dense formulations** in the target content. Look for:
32
+ - Jargon or domain-specific terms used without definition
33
+ - Unclear pronouns or references ("it", "that", "this one" — where the referent isn't obvious)
34
+ - Compound asks (multiple distinct requests bundled into one sentence)
35
+ - Unstated assumptions (the request depends on context the reader doesn't have)
36
+ - Vague quantifiers or qualifiers ("soon", "a bit", "some of them") where precision matters
37
+ - Overloaded or dense sentences that pack too much meaning into too little structure
38
+
39
+ 3. **For each ambiguous item found, output a structured block** with:
40
+ - **Plain-language clarification** — what it most likely means, stated simply and directly
41
+ - **Simplified restatement** — the original phrasing rewritten in plain terms
42
+ - **Additional context** — relevant background the user may be missing (why this term/reference matters, what it typically implies)
43
+ - **Example(s)** — a concrete worked example, included only where it would actually help clarify the specific ambiguity — skip this sub-section for an item if an example wouldn't add value, rather than manufacturing a weak one
44
+
45
+ 4. **Format the output so it's scannable.** Use a header or bold label per ambiguous item (e.g. `### 1. "the usual setup"` or `**1. "the usual setup"**`) — never collapse multiple ambiguous items into a single unstructured paragraph. If there are multiple items, number them in the order they appear in the source content.
46
+
47
+ 5. **Handle the zero-ambiguity case explicitly.** If nothing in the target content is actually ambiguous, dense, or under-specified, say so plainly — e.g. "Nothing here looks ambiguous — the request is clear as written." Do not manufacture findings just to have something to report.
48
+
49
+ ## Examples
50
+ - `/clearify` — Clarify the most recent user message / relevant conversation context
51
+ - `/clearify <pasted text>` — Clarify the attached text directly
52
+ - `/wtf` — Alias for `/clearify`, invoked via the companion skill in `skills/wtf/`
@@ -0,0 +1,203 @@
1
+ ---
2
+ name: close-story
3
+ description: Close a story by verifying all tasks are in terminal state, extracting actual diff stats per task, computing scope divergence flags, and writing closure metadata to task frontmatter.
4
+ keywords:
5
+ - close story
6
+ - close
7
+ - finish story
8
+ - complete story
9
+ - story done
10
+ examples:
11
+ - "close story E17_S06"
12
+ - "/close-story E32_S06"
13
+ - "mark story E12_S03 as done"
14
+ ---
15
+
16
+ # close-story — Close a Story
17
+
18
+ ## Purpose
19
+
20
+ `/close-story <story-id>` performs the final closure steps for a story:
21
+
22
+ 1. Verifies all tasks in the story are in a terminal state (Passed or Done).
23
+ 2. Extracts actual `git diff --stat` metrics for each task from the commit history.
24
+ 3. Writes `actual_files_changed`, `actual_lines_delta`, and `scope_divergence_flag` to each task's frontmatter.
25
+ 4. Updates the story's frontmatter `status` to `Done` and records `date_completed`.
26
+
27
+ ---
28
+
29
+ ## Instructions
30
+
31
+ ### Step 1 — Verify story is closeable
32
+
33
+ Run the guard script:
34
+
35
+ ```bash
36
+ bash skills/close-story/scripts/check-story-closeable.sh <story-id>
37
+ ```
38
+
39
+ - If it exits with code 0 and prints `CLOSEABLE`, continue.
40
+ - If it exits with code 1, halt and report the blocking tasks to the user. Do not proceed.
41
+
42
+ ---
43
+
44
+ ### Step 2 — Extract per-task diff stats and compute scope divergence
45
+
46
+ **At the start of this step**, read `project/configs/scope-thresholds.json` to check that it
47
+ exists and is parseable. If it is missing or malformed, log a warning and continue — divergence
48
+ flags will be skipped (non-blocking). The `compute-scope-divergence.sh` script handles this
49
+ gracefully by always printing `false` on error.
50
+
51
+ For **each task ID** listed in the story's `tasks:` frontmatter array:
52
+
53
+ 1. **Find the task file** at `project/board/tasks/<task-id>_*.md`.
54
+
55
+ 2. **Extract commit SHA(s)** for this task:
56
+ ```bash
57
+ git log --all --no-merges --grep="<task-id>" --pretty=format:"%H" 2>/dev/null
58
+ ```
59
+ This matches commits whose message contains the EST task identifier (e.g. `E32_S06_T01`).
60
+
61
+ 3. **Run per-task diff stat extraction:**
62
+ ```bash
63
+ bash skills/close-story/scripts/extract-task-diff-stats.sh <task-id>
64
+ ```
65
+ The script outputs:
66
+ ```
67
+ actual_files_changed: <N>
68
+ actual_lines_delta: <N>
69
+ ```
70
+ Parse these values from stdout.
71
+
72
+ 4. **Write stats to task frontmatter:**
73
+ ```bash
74
+ bash skills/close-story/scripts/update-task-frontmatter.sh \
75
+ "project/board/tasks/<task-id>_*.md" \
76
+ actual_files_changed <N>
77
+
78
+ bash skills/close-story/scripts/update-task-frontmatter.sh \
79
+ "project/board/tasks/<task-id>_*.md" \
80
+ actual_lines_delta <N>
81
+ ```
82
+
83
+ 5. **Compute and write scope divergence flag:**
84
+
85
+ Read the task's `execution_scope` from its frontmatter:
86
+ ```bash
87
+ EXECUTION_SCOPE=$(grep '^execution_scope:' "project/board/tasks/<task-id>_*.md" \
88
+ | head -1 | awk '{print $2}')
89
+ ```
90
+
91
+ If `execution_scope` is empty or absent, treat it as `task` (no divergence).
92
+
93
+ Run the divergence computation:
94
+ ```bash
95
+ DIVERGENCE_FLAG=$(bash skills/close-story/scripts/compute-scope-divergence.sh \
96
+ "<execution_scope>" <actual_files_changed> <actual_lines_delta>)
97
+ ```
98
+
99
+ Write the result to task frontmatter:
100
+ ```bash
101
+ bash skills/close-story/scripts/update-task-frontmatter.sh \
102
+ "project/board/tasks/<task-id>_*.md" \
103
+ scope_divergence_flag "$DIVERGENCE_FLAG"
104
+ ```
105
+
106
+ Track diverging tasks in a running list for the final report (Step 5).
107
+
108
+ 6. **Skip tasks with no matched commits** — If `actual_files_changed` is `0`
109
+ and `actual_lines_delta` is `0`, it likely means the task predates this
110
+ feature or uses an untracked commit. Write the `0` values anyway so the
111
+ field is present but do not treat this as an error. Run divergence
112
+ computation with `0 0` — this will return `false` for all scopes.
113
+
114
+ ---
115
+
116
+ ### Step 3 — Bundle task attribution
117
+
118
+ A **bundle task** is one where a single commit message contains multiple task
119
+ IDs (e.g. `task(E32_S06_T01, E32_S06_T02): ...`). In this case:
120
+
121
+ - Each task matched by `git log --grep` will independently resolve to the same
122
+ commit SHA, and each will receive the **full bundle stats** as its
123
+ `actual_files_changed` and `actual_lines_delta` values.
124
+ - This is the **"full credit"** attribution model: each task is credited with
125
+ the total work of the bundle commit since the individual contribution cannot
126
+ be mechanically separated.
127
+ - An alternative **proportional split** (divide total stats by number of tasks
128
+ in the bundle) is mathematically cleaner but requires detecting the bundle
129
+ size at query time, which is not supported by the current scripts.
130
+ - The full-credit model is the default. Future work may introduce proportional
131
+ splitting if telemetry shows significant over-counting in bundle scenarios.
132
+
133
+ > Note for tester: bundle attribution means the sum of `actual_lines_delta`
134
+ > across tasks may exceed the true total for stories where bundle commits were
135
+ > used.
136
+
137
+ ---
138
+
139
+ ### Step 4 — Update story frontmatter
140
+
141
+ After all tasks have been updated, set the following fields in the story's
142
+ frontmatter:
143
+
144
+ - `status: Done`
145
+ - `date_completed: <YYYY-MM-DD>` (today's date in UTC)
146
+
147
+ Use `update-task-frontmatter.sh` targeting the story file, or edit directly.
148
+
149
+ ---
150
+
151
+ ### Step 5 — Report
152
+
153
+ Print a summary to the user:
154
+
155
+ ```
156
+ Story <story-id> closed.
157
+ Tasks updated with actual diff stats:
158
+ <task-id>: files_changed=<N>, lines_delta=<N>, scope_divergence_flag=<true|false>
159
+ ...
160
+ ```
161
+
162
+ If any task had `0` for both fields, note it as potentially untracked:
163
+ ```
164
+ <task-id>: files_changed=0, lines_delta=0 (no EST-tagged commit found)
165
+ ```
166
+
167
+ #### Scope Divergence
168
+
169
+ Include a "Scope Divergence" section listing every task whose `scope_divergence_flag` was set
170
+ to `true`. If no tasks diverged, note that explicitly.
171
+
172
+ ```
173
+ Scope Divergence:
174
+ <task-id> [scope=<execution_scope>]: files_changed=<N>, lines_delta=<N> — exceeded threshold
175
+ ...
176
+ ```
177
+
178
+ Or, if none diverged:
179
+ ```
180
+ Scope Divergence: none
181
+ ```
182
+
183
+ The divergence thresholds applied are those from `project/configs/scope-thresholds.json`:
184
+ - `inline` scope: max_files=<inline_max_files>, max_lines=<inline_max_lines>
185
+ - `story` scope: max_files=<story_max_files>
186
+ - `task` / `epic` scope: no threshold (always false)
187
+
188
+ If the config was missing or malformed, note that divergence flags were skipped:
189
+ ```
190
+ Scope Divergence: skipped (scope-thresholds.json missing or malformed)
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Scripts Reference
196
+
197
+ | Script | Purpose |
198
+ |--------|---------|
199
+ | `scripts/check-story-closeable.sh <story-id>` | Guard: verify all tasks in terminal state |
200
+ | `scripts/extract-diff-stats.sh <story-id>` | Story-level aggregate diff stats (for story frontmatter) |
201
+ | `scripts/extract-task-diff-stats.sh <task-id>` | Per-task diff stats from EST-tagged commits |
202
+ | `scripts/update-task-frontmatter.sh <file> <key> <value>` | Write/update a YAML field in task or story frontmatter |
203
+ | `scripts/compute-scope-divergence.sh <execution_scope> <actual_files_changed> <actual_lines_delta>` | Compute scope divergence flag: prints `true` or `false` |
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env bash
2
+ # check-story-closeable.sh — Last-task detection guard for /close-story
3
+ #
4
+ # Usage: bash check-story-closeable.sh <story-id>
5
+ #
6
+ # Exits 0 and prints "CLOSEABLE" to stdout when all tasks in the story's
7
+ # `tasks:` frontmatter list have status Passed or Done.
8
+ # Exits 1 and prints a descriptive message to stderr for any other outcome.
9
+ #
10
+ # This script always re-reads task files from disk — never uses cached state.
11
+
12
+ set -euo pipefail
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Helpers
16
+ # ---------------------------------------------------------------------------
17
+
18
+ usage() {
19
+ echo "Usage: $(basename "$0") <story-id>" >&2
20
+ echo " Example: $(basename "$0") E17_S06" >&2
21
+ exit 1
22
+ }
23
+
24
+ # Locate the project root relative to this script's location.
25
+ # Script lives at: skills/close-story/scripts/check-story-closeable.sh
26
+ # Project root is three levels up (skills/ → repo root).
27
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
29
+ STORIES_DIR="$PROJECT_ROOT/project/board/stories"
30
+ TASKS_DIR="$PROJECT_ROOT/project/board/tasks"
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Argument validation
34
+ # ---------------------------------------------------------------------------
35
+
36
+ if [[ $# -ne 1 ]]; then
37
+ usage
38
+ fi
39
+
40
+ STORY_ID="$1"
41
+
42
+ # Basic sanity check on the story ID format (E##_S##)
43
+ if [[ ! "$STORY_ID" =~ ^E[0-9]{2}_S[0-9]{2}$ ]]; then
44
+ echo "ERROR: Invalid story ID format '$STORY_ID'. Expected format: E##_S## (e.g. E17_S06)" >&2
45
+ exit 1
46
+ fi
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Step 1: Locate story file
50
+ # ---------------------------------------------------------------------------
51
+
52
+ # Use a glob — story slug may vary, so match on ID prefix only.
53
+ story_file=""
54
+ while IFS= read -r -d '' candidate; do
55
+ story_file="$candidate"
56
+ break
57
+ done < <(find "$STORIES_DIR" -maxdepth 1 -name "${STORY_ID}_*.md" -print0 2>/dev/null)
58
+
59
+ if [[ -z "$story_file" ]]; then
60
+ echo "ERROR: Story file not found for '$STORY_ID' in $STORIES_DIR" >&2
61
+ exit 1
62
+ fi
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Step 2: Extract tasks: list from story frontmatter
66
+ # ---------------------------------------------------------------------------
67
+ # Frontmatter is the YAML block between the first pair of --- delimiters.
68
+ # The tasks: field is a YAML list of task IDs, e.g.:
69
+ # tasks:
70
+ # - E17_S06_T01
71
+ # - E17_S06_T02
72
+
73
+ extract_tasks() {
74
+ local file="$1"
75
+ local in_frontmatter=0
76
+ local in_tasks=0
77
+ local found_any=0
78
+
79
+ while IFS= read -r line; do
80
+ # Detect frontmatter boundaries
81
+ if [[ "$line" == "---" ]]; then
82
+ if [[ $in_frontmatter -eq 0 ]]; then
83
+ in_frontmatter=1
84
+ continue
85
+ else
86
+ # Closing --- ends the frontmatter block
87
+ break
88
+ fi
89
+ fi
90
+
91
+ # Only process lines inside frontmatter
92
+ if [[ $in_frontmatter -eq 0 ]]; then
93
+ continue
94
+ fi
95
+
96
+ # Detect the start of the tasks: key
97
+ if [[ "$line" =~ ^tasks:[[:space:]]*$ ]]; then
98
+ in_tasks=1
99
+ continue
100
+ fi
101
+
102
+ # If we are collecting tasks, look for list items
103
+ if [[ $in_tasks -eq 1 ]]; then
104
+ # A list item starts with optional whitespace then "- "
105
+ if [[ "$line" =~ ^[[:space:]]*-[[:space:]]+([A-Za-z0-9_]+) ]]; then
106
+ echo "${BASH_REMATCH[1]}"
107
+ found_any=1
108
+ elif [[ "$line" =~ ^[^[:space:]] ]]; then
109
+ # A non-indented, non-list line means we've left the tasks block
110
+ break
111
+ fi
112
+ fi
113
+ done < "$file"
114
+ }
115
+
116
+ mapfile -t TASK_IDS < <(extract_tasks "$story_file")
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Step 3: Validate tasks: list is present and non-empty
120
+ # ---------------------------------------------------------------------------
121
+
122
+ if [[ ${#TASK_IDS[@]} -eq 0 ]]; then
123
+ echo "ERROR: No tasks found in story frontmatter for '$STORY_ID' — cannot determine close state" >&2
124
+ exit 1
125
+ fi
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Step 4: Check each task's status
129
+ # ---------------------------------------------------------------------------
130
+
131
+ all_closeable=1
132
+
133
+ for task_id in "${TASK_IDS[@]}"; do
134
+ # 4a. Locate the task file (always re-read from disk)
135
+ task_file=""
136
+ while IFS= read -r -d '' candidate; do
137
+ task_file="$candidate"
138
+ break
139
+ done < <(find "$TASKS_DIR" -maxdepth 1 -name "${task_id}_*.md" -print0 2>/dev/null)
140
+
141
+ if [[ -z "$task_file" ]]; then
142
+ echo "ERROR: Task file not found for '$task_id' — treating as open" >&2
143
+ all_closeable=0
144
+ continue
145
+ fi
146
+
147
+ # 4b/4c. Extract status: field from the task's frontmatter
148
+ task_status=""
149
+ in_fm=0
150
+ while IFS= read -r line; do
151
+ if [[ "$line" == "---" ]]; then
152
+ if [[ $in_fm -eq 0 ]]; then
153
+ in_fm=1
154
+ continue
155
+ else
156
+ break
157
+ fi
158
+ fi
159
+ if [[ $in_fm -eq 1 && "$line" =~ ^status:[[:space:]]*(.+)$ ]]; then
160
+ task_status="${BASH_REMATCH[1]}"
161
+ # Trim surrounding quotes if present
162
+ task_status="${task_status%\"}"
163
+ task_status="${task_status#\"}"
164
+ task_status="${task_status%\'}"
165
+ task_status="${task_status#\'}"
166
+ break
167
+ fi
168
+ done < "$task_file"
169
+
170
+ # 4d. Evaluate status
171
+ case "$task_status" in
172
+ Passed|Done)
173
+ # Task is in a terminal state — OK
174
+ ;;
175
+ "")
176
+ echo "OPEN: '$task_id' has no status field — treating as open" >&2
177
+ all_closeable=0
178
+ ;;
179
+ *)
180
+ echo "OPEN: '$task_id' has status '$task_status'" >&2
181
+ all_closeable=0
182
+ ;;
183
+ esac
184
+ done
185
+
186
+ # ---------------------------------------------------------------------------
187
+ # Step 5: Report result
188
+ # ---------------------------------------------------------------------------
189
+
190
+ if [[ $all_closeable -eq 1 ]]; then
191
+ echo "CLOSEABLE"
192
+ exit 0
193
+ else
194
+ exit 1
195
+ fi