@jenga-ai/agent 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +28 -10
  2. package/agents/scrum-master.md +75 -0
  3. package/mcp/router/embedder.js +1 -1
  4. package/mcp/training_runner/index.js +239 -0
  5. package/mcp/training_runner/package-lock.json +1065 -0
  6. package/mcp/training_runner/package.json +15 -0
  7. package/package.json +13 -11
  8. package/skills/close-story/SKILL.md +203 -0
  9. package/skills/close-story/scripts/check-story-closeable.sh +195 -0
  10. package/skills/close-story/scripts/compute-scope-divergence.sh +128 -0
  11. package/skills/close-story/scripts/extract-diff-stats.sh +48 -0
  12. package/skills/close-story/scripts/extract-task-diff-stats.sh +97 -0
  13. package/skills/close-story/scripts/update-task-frontmatter.sh +103 -0
  14. package/skills/commit/SKILL.md +18 -0
  15. package/skills/distribute/CONFIG_SCHEMA.md +90 -0
  16. package/skills/distribute/SKILL.md +173 -0
  17. package/skills/distribute/scripts/check-version.sh +74 -0
  18. package/skills/distribute/scripts/commit-version-bump.sh +108 -0
  19. package/skills/distribute/scripts/distribute-changes.sh +381 -0
  20. package/skills/do/SKILL.md +314 -0
  21. package/skills/do/assets/intent-vs-diff-prompt.md +69 -0
  22. package/skills/doc/assets/path-objectives.yaml +13 -0
  23. package/skills/init/SKILL.md +4 -3
  24. package/skills/init/assets/strategy_stub_template.md +38 -0
  25. package/skills/init/scripts/init.sh +6 -1
  26. package/skills/jenga/SKILL.md +51 -2
  27. package/skills/strategy/SKILL.md +312 -0
  28. package/templates/SCRUM_BOARD_SCHEMA.md +49 -0
  29. package/skills/train/SKILL.md +0 -116
  30. package/skills/train/assets/dashboard-templates/classifiers.html +0 -106
  31. package/skills/train/assets/dashboard-templates/nlp.html +0 -102
  32. package/skills/train/assets/dashboard-templates/transformers.html +0 -98
  33. package/skills/train/assets/results-parsers/__init__.py +0 -9
  34. package/skills/train/assets/results-parsers/classifiers.py +0 -84
  35. package/skills/train/assets/results-parsers/nlp.py +0 -88
  36. package/skills/train/assets/results-parsers/reporter.py +0 -154
  37. package/skills/train/assets/results-parsers/transformers.py +0 -120
  38. package/skills/train/train_cli.py +0 -786
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "training-runner-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server that validates and runs ML training jobs via bash start.sh",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "start": "node index.js"
9
+ },
10
+ "dependencies": {
11
+ "@modelcontextprotocol/sdk": "^1.10.1",
12
+ "js-yaml": "^4.1.0",
13
+ "zod": "^3.23.8"
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jenga-ai/agent",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "JengaAgent — agentic project management CLI",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -26,22 +26,24 @@
26
26
  "templates/",
27
27
  "bin/",
28
28
  "lib/",
29
- "mcp/execute-ticket/",
30
- "mcp/help/",
31
- "mcp/router/index.js",
32
- "mcp/router/matcher.js",
33
- "mcp/router/embedder.js",
34
- "mcp/router/skill-index.js",
35
- "mcp/router/package.json",
36
- "mcp/router/package-lock.json",
37
- "mcp/router/README.md",
29
+ "mcp/",
38
30
  "README.md",
39
31
  "LICENSE"
40
32
  ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/samwelmunga/jenga-npm"
36
+ },
41
37
  "keywords": [],
42
38
  "author": "",
43
39
  "license": "MIT",
44
40
  "dependencies": {
45
- "@xenova/transformers": "^2.17.2"
41
+ "@huggingface/transformers": "^4.2.0"
42
+ },
43
+ "overrides": {
44
+ "sharp": "^0.34.5"
45
+ },
46
+ "comments": {
47
+ "audit": "sharp <0.35.0 and adm-zip <0.6.0 are transitive deps of @huggingface/transformers with no upstream fix available. Not exploitable in this context: only text feature-extraction pipeline is used — no image processing or ZIP handling at application boundary. Review when @huggingface/transformers ships a patched release."
46
48
  }
47
49
  }
@@ -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
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env bash
2
+ # compute-scope-divergence.sh — Compute scope_divergence_flag for a task
3
+ #
4
+ # Usage: compute-scope-divergence.sh <execution_scope> <actual_files_changed> <actual_lines_delta>
5
+ #
6
+ # Reads project/configs/scope-thresholds.json (resolved from git root) and compares
7
+ # the supplied diff stats against the thresholds that apply to <execution_scope>.
8
+ #
9
+ # Scope logic:
10
+ # inline — divergence if actual_files_changed > inline_max_files
11
+ # OR actual_lines_delta > inline_max_lines
12
+ # story — divergence if actual_files_changed > story_max_files
13
+ # task — no automatic threshold: always false
14
+ # epic — no automatic threshold: always false
15
+ # (any other value) — treated as task; always false
16
+ #
17
+ # Prints "true" or "false" on stdout and exits 0.
18
+ # If the config file is missing or cannot be parsed, prints "false" (non-blocking),
19
+ # emits a warning to stderr, and exits 0.
20
+ #
21
+ # Exit codes:
22
+ # 0 — success (including graceful degradation when config is absent/malformed)
23
+ # 1 — invalid argument count
24
+
25
+ set -euo pipefail
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Argument validation
29
+ # ---------------------------------------------------------------------------
30
+
31
+ if [[ $# -ne 3 ]]; then
32
+ echo "Usage: $(basename "$0") <execution_scope> <actual_files_changed> <actual_lines_delta>" >&2
33
+ echo " Example: $(basename "$0") inline 2 35" >&2
34
+ exit 1
35
+ fi
36
+
37
+ EXECUTION_SCOPE="${1}"
38
+ ACTUAL_FILES="${2}"
39
+ ACTUAL_LINES="${3}"
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Short-circuit: task and epic scopes never flag divergence
43
+ # ---------------------------------------------------------------------------
44
+
45
+ if [[ "$EXECUTION_SCOPE" == "task" || "$EXECUTION_SCOPE" == "epic" ]]; then
46
+ echo "false"
47
+ exit 0
48
+ fi
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Locate the config file (relative to git root)
52
+ # ---------------------------------------------------------------------------
53
+
54
+ GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true)
55
+
56
+ if [[ -z "$GIT_ROOT" ]]; then
57
+ echo "WARNING: Not inside a git repository; cannot locate scope-thresholds.json. Skipping divergence flag." >&2
58
+ echo "false"
59
+ exit 0
60
+ fi
61
+
62
+ CONFIG_FILE="${GIT_ROOT}/project/configs/scope-thresholds.json"
63
+
64
+ if [[ ! -f "$CONFIG_FILE" ]]; then
65
+ echo "WARNING: scope-thresholds.json not found at '${CONFIG_FILE}'. Skipping divergence flag." >&2
66
+ echo "false"
67
+ exit 0
68
+ fi
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Parse thresholds (jq preferred, plain shell fallback)
72
+ # ---------------------------------------------------------------------------
73
+
74
+ INLINE_MAX_FILES=""
75
+ INLINE_MAX_LINES=""
76
+ STORY_MAX_FILES=""
77
+
78
+ if command -v jq >/dev/null 2>&1; then
79
+ INLINE_MAX_FILES=$(jq -r '.inline_max_files // empty' "$CONFIG_FILE" 2>/dev/null || true)
80
+ INLINE_MAX_LINES=$(jq -r '.inline_max_lines // empty' "$CONFIG_FILE" 2>/dev/null || true)
81
+ STORY_MAX_FILES=$(jq -r '.story_max_files // empty' "$CONFIG_FILE" 2>/dev/null || true)
82
+ else
83
+ # Plain shell fallback: grep for the numeric value on the matching key line
84
+ INLINE_MAX_FILES=$(grep '"inline_max_files"' "$CONFIG_FILE" | grep -o '[0-9][0-9]*' | head -1 || true)
85
+ INLINE_MAX_LINES=$(grep '"inline_max_lines"' "$CONFIG_FILE" | grep -o '[0-9][0-9]*' | head -1 || true)
86
+ STORY_MAX_FILES=$(grep '"story_max_files"' "$CONFIG_FILE" | grep -o '[0-9][0-9]*' | head -1 || true)
87
+ fi
88
+
89
+ # Validate that all required thresholds were parsed
90
+ if [[ -z "$INLINE_MAX_FILES" || -z "$INLINE_MAX_LINES" || -z "$STORY_MAX_FILES" ]]; then
91
+ echo "WARNING: scope-thresholds.json is malformed or missing required keys (inline_max_files, inline_max_lines, story_max_files). Skipping divergence flag." >&2
92
+ echo "false"
93
+ exit 0
94
+ fi
95
+
96
+ # Ensure values are integers
97
+ if ! [[ "$INLINE_MAX_FILES" =~ ^[0-9]+$ && "$INLINE_MAX_LINES" =~ ^[0-9]+$ && "$STORY_MAX_FILES" =~ ^[0-9]+$ ]]; then
98
+ echo "WARNING: Non-integer threshold value found in scope-thresholds.json. Skipping divergence flag." >&2
99
+ echo "false"
100
+ exit 0
101
+ fi
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Apply per-scope divergence logic
105
+ # ---------------------------------------------------------------------------
106
+
107
+ case "$EXECUTION_SCOPE" in
108
+ inline)
109
+ if [[ "$ACTUAL_FILES" -gt "$INLINE_MAX_FILES" ]] || [[ "$ACTUAL_LINES" -gt "$INLINE_MAX_LINES" ]]; then
110
+ echo "true"
111
+ else
112
+ echo "false"
113
+ fi
114
+ ;;
115
+ story)
116
+ if [[ "$ACTUAL_FILES" -gt "$STORY_MAX_FILES" ]]; then
117
+ echo "true"
118
+ else
119
+ echo "false"
120
+ fi
121
+ ;;
122
+ *)
123
+ # Unknown scope — treat conservatively as no divergence
124
+ echo "false"
125
+ ;;
126
+ esac
127
+
128
+ exit 0
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ STORY_ID="${1:-}"
5
+ DATE_STARTED="${2:-}"
6
+ DATE_COMPLETED="${3:-}"
7
+
8
+ if [[ -z "$STORY_ID" ]]; then
9
+ echo "Usage: extract-diff-stats.sh <story-id> [<date_started> <date_completed>]" >&2
10
+ exit 1
11
+ fi
12
+
13
+ # Primary query — EST pattern match
14
+ # Collect all --stat output for commits matching the story ID pattern
15
+ STAT_OUTPUT=$(git log --all --stat --no-merges --grep="$STORY_ID" --pretty=format:"COMMIT:%H %s" 2>/dev/null || true)
16
+
17
+ # Parse insertions and deletions from --stat lines
18
+ # --stat lines look like: " 3 files changed, 45 insertions(+), 12 deletions(-)"
19
+ # Using portable grep (BSD and GNU compatible) instead of grep -oP
20
+ DIFF_ADDED=$(echo "$STAT_OUTPUT" | (grep -o '[0-9]* insertion' || true) | awk '{sum+=$1} END {print sum+0}')
21
+ DIFF_REMOVED=$(echo "$STAT_OUTPUT" | (grep -o '[0-9]* deletion' || true) | awk '{sum+=$1} END {print sum+0}')
22
+ COMMITS_MATCHED=$(git log --all --no-merges --grep="$STORY_ID" --pretty=format:"%H" 2>/dev/null | wc -l | tr -d ' ')
23
+
24
+ # Secondary query — date-window unmatched count
25
+ if [[ -n "$DATE_STARTED" && -n "$DATE_COMPLETED" ]]; then
26
+ ALL_IN_WINDOW=$(git log --all --no-merges \
27
+ --since="$DATE_STARTED" \
28
+ --until="${DATE_COMPLETED}T23:59:59" \
29
+ --pretty=format:"%H" 2>/dev/null || true)
30
+
31
+ MATCHED_HASHES=$(git log --all --no-merges --grep="$STORY_ID" --pretty=format:"%H" 2>/dev/null || true)
32
+
33
+ COMMITS_UNMATCHED=0
34
+ while IFS= read -r hash; do
35
+ [[ -z "$hash" ]] && continue
36
+ if ! echo "$MATCHED_HASHES" | grep -q "^$hash$"; then
37
+ COMMITS_UNMATCHED=$((COMMITS_UNMATCHED + 1))
38
+ fi
39
+ done <<< "$ALL_IN_WINDOW"
40
+ else
41
+ COMMITS_UNMATCHED="unknown"
42
+ fi
43
+
44
+ # Output structured results
45
+ echo "diff_added: $DIFF_ADDED"
46
+ echo "diff_removed: $DIFF_REMOVED"
47
+ echo "commits_matched: $COMMITS_MATCHED"
48
+ echo "commits_unmatched_in_window: $COMMITS_UNMATCHED"