@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.
- package/README.md +28 -10
- package/agents/scrum-master.md +75 -0
- package/mcp/router/embedder.js +1 -1
- package/mcp/training_runner/index.js +239 -0
- package/mcp/training_runner/package-lock.json +1065 -0
- package/mcp/training_runner/package.json +15 -0
- package/package.json +13 -11
- package/skills/close-story/SKILL.md +203 -0
- package/skills/close-story/scripts/check-story-closeable.sh +195 -0
- package/skills/close-story/scripts/compute-scope-divergence.sh +128 -0
- package/skills/close-story/scripts/extract-diff-stats.sh +48 -0
- package/skills/close-story/scripts/extract-task-diff-stats.sh +97 -0
- package/skills/close-story/scripts/update-task-frontmatter.sh +103 -0
- package/skills/commit/SKILL.md +18 -0
- package/skills/distribute/CONFIG_SCHEMA.md +90 -0
- package/skills/distribute/SKILL.md +173 -0
- package/skills/distribute/scripts/check-version.sh +74 -0
- package/skills/distribute/scripts/commit-version-bump.sh +108 -0
- package/skills/distribute/scripts/distribute-changes.sh +381 -0
- package/skills/do/SKILL.md +314 -0
- package/skills/do/assets/intent-vs-diff-prompt.md +69 -0
- package/skills/doc/assets/path-objectives.yaml +13 -0
- package/skills/init/SKILL.md +4 -3
- package/skills/init/assets/strategy_stub_template.md +38 -0
- package/skills/init/scripts/init.sh +6 -1
- package/skills/jenga/SKILL.md +51 -2
- package/skills/strategy/SKILL.md +312 -0
- package/templates/SCRUM_BOARD_SCHEMA.md +49 -0
- package/skills/train/SKILL.md +0 -116
- package/skills/train/assets/dashboard-templates/classifiers.html +0 -106
- package/skills/train/assets/dashboard-templates/nlp.html +0 -102
- package/skills/train/assets/dashboard-templates/transformers.html +0 -98
- package/skills/train/assets/results-parsers/__init__.py +0 -9
- package/skills/train/assets/results-parsers/classifiers.py +0 -84
- package/skills/train/assets/results-parsers/nlp.py +0 -88
- package/skills/train/assets/results-parsers/reporter.py +0 -154
- package/skills/train/assets/results-parsers/transformers.py +0 -120
- package/skills/train/train_cli.py +0 -786
|
@@ -0,0 +1,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.
|
|
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/
|
|
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
|
-
"@
|
|
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"
|