cleargate 0.10.0 → 0.11.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/CHANGELOG.md +20 -0
- package/README.md +11 -1
- package/dist/MANIFEST.json +40 -26
- package/dist/chunk-HZPJ5QX4.js +459 -0
- package/dist/chunk-HZPJ5QX4.js.map +1 -0
- package/dist/cli.cjs +419 -202
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +387 -513
- package/dist/cli.js.map +1 -1
- package/dist/lib/lifecycle-reconcile.cjs +497 -0
- package/dist/lib/lifecycle-reconcile.cjs.map +1 -0
- package/dist/lib/lifecycle-reconcile.d.cts +136 -0
- package/dist/lib/lifecycle-reconcile.d.ts +136 -0
- package/dist/lib/lifecycle-reconcile.js +20 -0
- package/dist/lib/lifecycle-reconcile.js.map +1 -0
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +55 -2
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +22 -0
- package/dist/templates/cleargate-planning/.claude/agents/devops.md +249 -0
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +41 -0
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +44 -8
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +21 -0
- package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +12 -1
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +21 -1
- package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +200 -29
- package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +160 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +41 -9
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +98 -16
- package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +3 -3
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +86 -10
- package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +173 -87
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +150 -22
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +32 -8
- package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +12 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +3 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +3 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +3 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +3 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +1 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +8 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +3 -0
- package/dist/templates/cleargate-planning/CLAUDE.md +3 -1
- package/dist/templates/cleargate-planning/MANIFEST.json +40 -26
- package/package.json +8 -5
- package/templates/cleargate-planning/.claude/agents/architect.md +55 -2
- package/templates/cleargate-planning/.claude/agents/developer.md +22 -0
- package/templates/cleargate-planning/.claude/agents/devops.md +249 -0
- package/templates/cleargate-planning/.claude/agents/qa.md +41 -0
- package/templates/cleargate-planning/.claude/agents/reporter.md +44 -8
- package/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +21 -0
- package/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +12 -1
- package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +21 -1
- package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +200 -29
- package/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +160 -0
- package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +41 -9
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +98 -16
- package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +3 -3
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +86 -10
- package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +173 -87
- package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +150 -22
- package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
- package/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +32 -8
- package/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +12 -1
- package/templates/cleargate-planning/.cleargate/templates/Bug.md +3 -0
- package/templates/cleargate-planning/.cleargate/templates/CR.md +3 -0
- package/templates/cleargate-planning/.cleargate/templates/epic.md +3 -0
- package/templates/cleargate-planning/.cleargate/templates/hotfix.md +3 -0
- package/templates/cleargate-planning/.cleargate/templates/initiative.md +1 -1
- package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +8 -0
- package/templates/cleargate-planning/.cleargate/templates/story.md +3 -0
- package/templates/cleargate-planning/CLAUDE.md +3 -1
- package/templates/cleargate-planning/MANIFEST.json +40 -26
|
@@ -1,123 +1,209 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# run_script.sh —
|
|
3
|
-
# structured
|
|
4
|
-
#
|
|
5
|
-
#
|
|
2
|
+
# run_script.sh — Arbitrary-command wrapper that captures stdout/stderr independently
|
|
3
|
+
# and writes a structured JSON incident file on non-zero exit.
|
|
4
|
+
#
|
|
5
|
+
# Interface: bash run_script.sh <command> [args...]
|
|
6
|
+
# <command> — any executable on PATH (e.g. node, bash, sh, true, false)
|
|
7
|
+
# [args...] — forwarded to the command unchanged
|
|
8
|
+
#
|
|
9
|
+
# Example (node script):
|
|
10
|
+
# bash run_script.sh node .cleargate/scripts/update_state.mjs STORY-01 Done
|
|
11
|
+
# Example (bash script):
|
|
12
|
+
# bash run_script.sh bash .cleargate/scripts/pre_gate_runner.sh qa .worktrees/X sprint/S-01
|
|
13
|
+
#
|
|
14
|
+
# On success (exit 0): stdout+stderr are passed through; no incident file written.
|
|
15
|
+
# On failure (exit ≠ 0): stdout+stderr are passed through AND a JSON incident is
|
|
16
|
+
# written to .cleargate/sprint-runs/<active-sprint>/.script-incidents/<ts>-<hash>.json
|
|
17
|
+
#
|
|
18
|
+
# Self-exemption: if RUN_SCRIPT_ACTIVE=1 is already set, the wrapper is already
|
|
19
|
+
# running — do not nest. Execute the command directly to avoid infinite recursion.
|
|
20
|
+
# This guard implements the self-exempt contract documented in SKILL.md §C.x.
|
|
21
|
+
#
|
|
22
|
+
# Env vars read:
|
|
23
|
+
# ORCHESTRATOR_PROJECT_DIR — project root override (falls back to CLAUDE_PROJECT_DIR,
|
|
24
|
+
# then to git rev-parse --show-toplevel, then script dir ancestor)
|
|
25
|
+
# AGENT_TYPE — populates incident JSON agent_type field (null if empty)
|
|
26
|
+
# WORK_ITEM_ID — populates incident JSON work_item_id field (null if empty)
|
|
27
|
+
# RUN_SCRIPT_ACTIVE — self-exemption guard (set to 1 by this wrapper)
|
|
28
|
+
|
|
6
29
|
set -euo pipefail
|
|
7
30
|
|
|
8
|
-
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Self-exemption guard — do not wrap recursively
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
if [[ "${RUN_SCRIPT_ACTIVE:-}" == "1" ]]; then
|
|
35
|
+
# Already inside a wrapper invocation; pass through directly, no JSON capture
|
|
36
|
+
exec "$@"
|
|
37
|
+
fi
|
|
9
38
|
|
|
10
39
|
# ---------------------------------------------------------------------------
|
|
11
40
|
# Usage guard
|
|
12
41
|
# ---------------------------------------------------------------------------
|
|
13
42
|
if [[ $# -lt 1 ]]; then
|
|
14
|
-
echo "Usage: run_script.sh <
|
|
43
|
+
echo "Usage: bash run_script.sh <command> [args...]" >&2
|
|
15
44
|
exit 2
|
|
16
45
|
fi
|
|
17
46
|
|
|
18
|
-
SCRIPT_NAME="$1"
|
|
19
|
-
shift
|
|
20
|
-
SCRIPT_ARGS=()
|
|
21
|
-
if [[ $# -gt 0 ]]; then
|
|
22
|
-
SCRIPT_ARGS=("$@")
|
|
23
|
-
fi
|
|
24
|
-
|
|
25
47
|
# ---------------------------------------------------------------------------
|
|
26
|
-
# Resolve
|
|
48
|
+
# Resolve project root for incident file path
|
|
27
49
|
# ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
_resolve_project_root() {
|
|
51
|
+
# Priority: ORCHESTRATOR_PROJECT_DIR → CLAUDE_PROJECT_DIR → git toplevel → script dir ancestor
|
|
52
|
+
if [[ -n "${ORCHESTRATOR_PROJECT_DIR:-}" ]]; then
|
|
53
|
+
echo "$ORCHESTRATOR_PROJECT_DIR"
|
|
54
|
+
return
|
|
55
|
+
fi
|
|
56
|
+
if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
|
|
57
|
+
echo "$CLAUDE_PROJECT_DIR"
|
|
58
|
+
return
|
|
59
|
+
fi
|
|
60
|
+
if _root=$(git rev-parse --show-toplevel 2>/dev/null); then
|
|
61
|
+
echo "$_root"
|
|
62
|
+
return
|
|
63
|
+
fi
|
|
64
|
+
# Fallback: assume run_script.sh lives in <repo>/.cleargate/scripts/
|
|
65
|
+
echo "$(cd "$(dirname "${BASH_SOURCE[0]}")" && cd ../.. && pwd)"
|
|
66
|
+
}
|
|
33
67
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
EXT="${SCRIPT_NAME##*.}"
|
|
38
|
-
case "$EXT" in
|
|
39
|
-
mjs) RUNNER="node" ;;
|
|
40
|
-
sh) RUNNER="bash" ;;
|
|
41
|
-
*)
|
|
42
|
-
echo "unsupported extension: .${EXT}" >&2
|
|
43
|
-
exit 2
|
|
44
|
-
;;
|
|
45
|
-
esac
|
|
68
|
+
PROJECT_ROOT="$(_resolve_project_root)"
|
|
69
|
+
SPRINT_RUNS_DIR="${PROJECT_ROOT}/.cleargate/sprint-runs"
|
|
70
|
+
ACTIVE_FILE="${SPRINT_RUNS_DIR}/.active"
|
|
46
71
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
# Determine sprint incident dir
|
|
73
|
+
if [[ -f "$ACTIVE_FILE" ]]; then
|
|
74
|
+
SPRINT_ID="$(cat "$ACTIVE_FILE" | tr -d '[:space:]')"
|
|
75
|
+
INCIDENTS_DIR="${SPRINT_RUNS_DIR}/${SPRINT_ID}/.script-incidents"
|
|
76
|
+
else
|
|
77
|
+
# No active sprint sentinel → write to _off-sprint bucket
|
|
78
|
+
INCIDENTS_DIR="${SPRINT_RUNS_DIR}/_off-sprint/.script-incidents"
|
|
53
79
|
fi
|
|
54
80
|
|
|
55
81
|
# ---------------------------------------------------------------------------
|
|
56
82
|
# Capture stdout + stderr to temp files
|
|
57
83
|
# ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
trap 'rm -f "$
|
|
84
|
+
STDOUT_TMP="$(mktemp)"
|
|
85
|
+
STDERR_TMP="$(mktemp)"
|
|
86
|
+
trap 'rm -f "$STDOUT_TMP" "$STDERR_TMP"' EXIT
|
|
87
|
+
|
|
88
|
+
# Mark self as active before running the wrapped command
|
|
89
|
+
export RUN_SCRIPT_ACTIVE=1
|
|
61
90
|
|
|
62
91
|
EXIT_CODE=0
|
|
63
|
-
|
|
64
|
-
"$RUNNER" "$SCRIPT_PATH" "${SCRIPT_ARGS[@]}" > "$STDOUT_FILE" 2> "$STDERR_FILE" || EXIT_CODE=$?
|
|
65
|
-
else
|
|
66
|
-
"$RUNNER" "$SCRIPT_PATH" > "$STDOUT_FILE" 2> "$STDERR_FILE" || EXIT_CODE=$?
|
|
67
|
-
fi
|
|
92
|
+
"$@" >"$STDOUT_TMP" 2>"$STDERR_TMP" || EXIT_CODE=$?
|
|
68
93
|
|
|
69
94
|
# ---------------------------------------------------------------------------
|
|
70
|
-
#
|
|
95
|
+
# Always pass through stdout + stderr to the caller
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
cat "$STDOUT_TMP"
|
|
98
|
+
cat "$STDERR_TMP" >&2
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# On success: nothing more to do
|
|
71
102
|
# ---------------------------------------------------------------------------
|
|
72
103
|
if [[ $EXIT_CODE -eq 0 ]]; then
|
|
73
|
-
cat "$STDOUT_FILE"
|
|
74
|
-
cat "$STDERR_FILE" >&2
|
|
75
104
|
exit 0
|
|
76
105
|
fi
|
|
77
106
|
|
|
78
107
|
# ---------------------------------------------------------------------------
|
|
79
|
-
# On failure:
|
|
108
|
+
# On failure: write structured JSON incident file
|
|
80
109
|
# ---------------------------------------------------------------------------
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
MAX_BYTES=4096
|
|
111
|
+
TRUNCATION_SUFFIX="... [truncated]"
|
|
112
|
+
|
|
113
|
+
_truncate_stream() {
|
|
114
|
+
# Byte-correct truncation: use head -c for POSIX byte-count (not bash ${var:0:N}
|
|
115
|
+
# which is char-index — wrong for UTF-8 multi-byte input).
|
|
116
|
+
# Trade-off: output may end with a partial multi-byte char boundary; downstream
|
|
117
|
+
# Node.js JSON.stringify escapes the partial sequence, producing valid JSON.
|
|
118
|
+
local file="$1"
|
|
119
|
+
local file_bytes
|
|
120
|
+
file_bytes="$(wc -c < "$file")"
|
|
121
|
+
if [[ $file_bytes -le $MAX_BYTES ]]; then
|
|
122
|
+
# File fits within budget — emit as-is (no suffix)
|
|
123
|
+
head -c "$MAX_BYTES" "$file"
|
|
124
|
+
else
|
|
125
|
+
# Truncation occurred — emit first MAX_BYTES bytes then TRUNCATION_SUFFIX
|
|
126
|
+
printf '%s' "$(head -c "$MAX_BYTES" "$file")${TRUNCATION_SUFFIX}"
|
|
127
|
+
fi
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
STDOUT_CAPTURED="$(_truncate_stream "$STDOUT_TMP")"
|
|
131
|
+
STDERR_CAPTURED="$(_truncate_stream "$STDERR_TMP")"
|
|
132
|
+
|
|
133
|
+
# Build filename: <ts>-<hash>.json
|
|
134
|
+
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
135
|
+
TS_FILE="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
136
|
+
# Hash the full command string (first arg + all args)
|
|
137
|
+
HASH="$(printf '%s' "$*" | shasum -a 1 | cut -c1-12)"
|
|
138
|
+
INCIDENT_FILE="${INCIDENTS_DIR}/${TS_FILE}-${HASH}.json"
|
|
139
|
+
|
|
140
|
+
# Ensure incident directory exists
|
|
141
|
+
mkdir -p "$INCIDENTS_DIR"
|
|
142
|
+
|
|
143
|
+
# Collect context
|
|
144
|
+
COMMAND="$1"
|
|
145
|
+
shift
|
|
146
|
+
ARGS_JSON="["
|
|
147
|
+
FIRST=1
|
|
148
|
+
for ARG in "$@"; do
|
|
149
|
+
if [[ $FIRST -eq 1 ]]; then
|
|
150
|
+
FIRST=0
|
|
151
|
+
else
|
|
152
|
+
ARGS_JSON="${ARGS_JSON},"
|
|
153
|
+
fi
|
|
154
|
+
# JSON-escape the arg: replace \ with \\, " with \", newlines with \n
|
|
155
|
+
ARG_ESCAPED="${ARG//\\/\\\\}"
|
|
156
|
+
ARG_ESCAPED="${ARG_ESCAPED//\"/\\\"}"
|
|
157
|
+
ARG_ESCAPED="${ARG_ESCAPED//$'\n'/\\n}"
|
|
158
|
+
ARGS_JSON="${ARGS_JSON}\"${ARG_ESCAPED}\""
|
|
159
|
+
done
|
|
160
|
+
ARGS_JSON="${ARGS_JSON}]"
|
|
161
|
+
|
|
162
|
+
CWD="$(pwd)"
|
|
163
|
+
|
|
164
|
+
# Encode agent_type + work_item_id (null if empty)
|
|
165
|
+
AGENT_TYPE_VAL="${AGENT_TYPE:-}"
|
|
166
|
+
WORK_ITEM_ID_VAL="${WORK_ITEM_ID:-}"
|
|
167
|
+
|
|
168
|
+
if [[ -z "$AGENT_TYPE_VAL" ]]; then
|
|
169
|
+
AGENT_TYPE_JSON="null"
|
|
170
|
+
else
|
|
171
|
+
AGENT_TYPE_JSON="\"${AGENT_TYPE_VAL}\""
|
|
106
172
|
fi
|
|
107
173
|
|
|
174
|
+
if [[ -z "$WORK_ITEM_ID_VAL" ]]; then
|
|
175
|
+
WORK_ITEM_ID_JSON="null"
|
|
176
|
+
else
|
|
177
|
+
WORK_ITEM_ID_JSON="\"${WORK_ITEM_ID_VAL}\""
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# JSON-encode captured streams (escape backslash, double-quote, and newlines)
|
|
181
|
+
_json_str() {
|
|
182
|
+
local s="$1"
|
|
183
|
+
s="${s//\\/\\\\}"
|
|
184
|
+
s="${s//\"/\\\"}"
|
|
185
|
+
s="${s//$'\n'/\\n}"
|
|
186
|
+
s="${s//$'\r'/\\r}"
|
|
187
|
+
printf '%s' "$s"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
STDOUT_JSON="$(_json_str "$STDOUT_CAPTURED")"
|
|
191
|
+
STDERR_JSON="$(_json_str "$STDERR_CAPTURED")"
|
|
192
|
+
COMMAND_JSON="$(_json_str "$COMMAND")"
|
|
193
|
+
CWD_JSON="$(_json_str "$CWD")"
|
|
194
|
+
|
|
195
|
+
cat > "$INCIDENT_FILE" <<JSON
|
|
108
196
|
{
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
echo "## End Incident"
|
|
121
|
-
} >&2
|
|
197
|
+
"ts": "${TS}",
|
|
198
|
+
"command": "${COMMAND_JSON}",
|
|
199
|
+
"args": ${ARGS_JSON},
|
|
200
|
+
"cwd": "${CWD_JSON}",
|
|
201
|
+
"exit_code": ${EXIT_CODE},
|
|
202
|
+
"stdout": "${STDOUT_JSON}",
|
|
203
|
+
"stderr": "${STDERR_JSON}",
|
|
204
|
+
"agent_type": ${AGENT_TYPE_JSON},
|
|
205
|
+
"work_item_id": ${WORK_ITEM_ID_JSON}
|
|
206
|
+
}
|
|
207
|
+
JSON
|
|
122
208
|
|
|
123
209
|
exit $EXIT_CODE
|
|
@@ -131,8 +131,73 @@ function atomicWrite(filePath, content) {
|
|
|
131
131
|
fs.renameSync(tmpFile, filePath);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Read all ledger entries from a token-ledger.jsonl file.
|
|
136
|
+
* @param {string} ledgerPath
|
|
137
|
+
* @returns {{ work_item_id: string, agent_type: string, session_id?: string, sprint_id?: string }[]}
|
|
138
|
+
*/
|
|
139
|
+
function readLedgerEntries(ledgerPath) {
|
|
140
|
+
if (!fs.existsSync(ledgerPath)) return [];
|
|
141
|
+
const lines = fs.readFileSync(ledgerPath, 'utf8').split('\n').filter(l => l.trim());
|
|
142
|
+
const entries = [];
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
try {
|
|
145
|
+
const entry = JSON.parse(line);
|
|
146
|
+
if (entry.work_item_id && entry.agent_type) {
|
|
147
|
+
entries.push({
|
|
148
|
+
work_item_id: entry.work_item_id,
|
|
149
|
+
agent_type: entry.agent_type,
|
|
150
|
+
session_id: entry.session_id ?? '',
|
|
151
|
+
sprint_id: entry.sprint_id ?? '',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
} catch { /* skip malformed lines */ }
|
|
155
|
+
}
|
|
156
|
+
return entries;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a (work_item_id|agent_type) bucket is a session-attribution artifact.
|
|
161
|
+
*
|
|
162
|
+
* Session-shared filter (CR-056): When multiple Architect (or other agent) dispatches run
|
|
163
|
+
* within the same session, the token ledger attributes them all to the same bucket keyed
|
|
164
|
+
* by the first-merged work_item_id. The canonical example is "CR-045 × architect" in
|
|
165
|
+
* SPRINT-23/SPRINT-24 — all 17 entries share the SAME session_id 48aa90c9-..., making
|
|
166
|
+
* them one session mis-attributed as 17 distinct repeats.
|
|
167
|
+
*
|
|
168
|
+
* Rule: session-attribution artifact if ALL entries with a known session_id share the
|
|
169
|
+
* SAME session_id (i.e., exactly 1 distinct session across all entries). When multiple
|
|
170
|
+
* distinct sessions are present, there is genuine independent repetition.
|
|
171
|
+
*
|
|
172
|
+
* Known false-positive class: "CR-045 × architect" — 17 entries, 1 session UUID.
|
|
173
|
+
*
|
|
174
|
+
* @param {{ session_id?: string }[]} entries all entries for this bucket (across sprints)
|
|
175
|
+
* @returns {boolean} true if bucket should be filtered as session-shared artifact
|
|
176
|
+
*/
|
|
177
|
+
function isSessionShared(entries) {
|
|
178
|
+
if (entries.length < 3) return false;
|
|
179
|
+
const distinctSessions = new Set(
|
|
180
|
+
entries.map(e => e.session_id ?? '').filter(s => s !== '')
|
|
181
|
+
);
|
|
182
|
+
// If exactly 1 distinct session accounts for all entries, this is a session-attribution artifact
|
|
183
|
+
return distinctSessions.size === 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
134
186
|
/**
|
|
135
187
|
* Scan token-ledger.jsonl and FLASHCARD.md for skill creation candidates.
|
|
188
|
+
*
|
|
189
|
+
* Heuristic (CR-056 tightened):
|
|
190
|
+
* 1. Session-shared filter: if ≥2 of ≥3 entries for a bucket share the same
|
|
191
|
+
* session_id → skip (token-attribution artifact, not a real recurring pattern).
|
|
192
|
+
* Known false-positive class: "CR-045 × architect" from SPRINT-23/SPRINT-24 —
|
|
193
|
+
* all 17 entries share session_id 48aa90c9-... (session-shared).
|
|
194
|
+
* 2. Cross-sprint aggregation: collect entries from the current sprint's ledger PLUS
|
|
195
|
+
* prior sprints' ledgers (via CLEARGATE_SPRINT_RUNS_DIR). Count ≥3× total across
|
|
196
|
+
* ≥2 distinct sprints.
|
|
197
|
+
* 3. Cross-sprint dedup: if the candidate's hash already appears in any prior sprint's
|
|
198
|
+
* improvement-suggestions.md → surface as seen_in: [...] instead of re-flagging.
|
|
199
|
+
* 4. Threshold raised to "≥3× across ≥2 distinct sprints AND not session-shared".
|
|
200
|
+
*
|
|
136
201
|
* Appends or replaces the "## Skill Creation Candidates" section in improvement-suggestions.md.
|
|
137
202
|
* @param {string} sprintId
|
|
138
203
|
* @param {string} sprintDir
|
|
@@ -144,24 +209,72 @@ function scanSkillCandidates(sprintId, sprintDir, suggestionsFile) {
|
|
|
144
209
|
: path.join(REPO_ROOT, '.cleargate', 'FLASHCARD.md');
|
|
145
210
|
const ledgerPath = path.join(sprintDir, 'token-ledger.jsonl');
|
|
146
211
|
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
212
|
+
// Resolve sprint-runs root (used for cross-sprint lookback)
|
|
213
|
+
const sprintRunsDir = process.env.CLEARGATE_SPRINT_RUNS_DIR
|
|
214
|
+
? path.resolve(process.env.CLEARGATE_SPRINT_RUNS_DIR)
|
|
215
|
+
: path.dirname(sprintDir);
|
|
216
|
+
|
|
217
|
+
// ── Step 1: collect all ledger entries across current + prior sprints ─────────
|
|
218
|
+
// Collect from current sprint
|
|
219
|
+
const currentEntries = readLedgerEntries(ledgerPath);
|
|
220
|
+
|
|
221
|
+
// Collect from prior sprints (all sprint dirs except current)
|
|
222
|
+
const allPriorEntries = [];
|
|
223
|
+
const priorSuggestionsContents = [];
|
|
224
|
+
if (fs.existsSync(sprintRunsDir)) {
|
|
225
|
+
let priorDirs;
|
|
226
|
+
try {
|
|
227
|
+
priorDirs = fs.readdirSync(sprintRunsDir)
|
|
228
|
+
.filter(name => name !== sprintId && !name.startsWith('.'))
|
|
229
|
+
.map(name => path.join(sprintRunsDir, name))
|
|
230
|
+
.filter(p => {
|
|
231
|
+
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
232
|
+
});
|
|
233
|
+
} catch { priorDirs = []; }
|
|
234
|
+
|
|
235
|
+
for (const priorDir of priorDirs) {
|
|
236
|
+
const priorLedger = path.join(priorDir, 'token-ledger.jsonl');
|
|
237
|
+
const entries = readLedgerEntries(priorLedger);
|
|
238
|
+
allPriorEntries.push(...entries);
|
|
239
|
+
|
|
240
|
+
// Collect prior improvement-suggestions.md for cross-sprint dedup
|
|
241
|
+
const priorSuggFile = path.join(priorDir, 'improvement-suggestions.md');
|
|
242
|
+
if (fs.existsSync(priorSuggFile)) {
|
|
243
|
+
try {
|
|
244
|
+
priorSuggestionsContents.push(fs.readFileSync(priorSuggFile, 'utf8'));
|
|
245
|
+
} catch { /* skip unreadable */ }
|
|
246
|
+
}
|
|
160
247
|
}
|
|
161
248
|
}
|
|
162
249
|
|
|
163
|
-
//
|
|
164
|
-
|
|
250
|
+
// ── Step 2: build cross-sprint bucket map ────────────────────────────────────
|
|
251
|
+
// Map: key (work_item_id|agent_type) → { entries: [...], sprintIds: Set<string> }
|
|
252
|
+
/** @type {Map<string, { entries: { session_id?: string, sprint_id?: string }[], sprintIds: Set<string> }>} */
|
|
253
|
+
const buckets = new Map();
|
|
254
|
+
|
|
255
|
+
for (const e of currentEntries) {
|
|
256
|
+
const key = `${e.work_item_id}|${e.agent_type}`;
|
|
257
|
+
if (!buckets.has(key)) buckets.set(key, { entries: [], sprintIds: new Set() });
|
|
258
|
+
const b = buckets.get(key);
|
|
259
|
+
b.entries.push(e);
|
|
260
|
+
b.sprintIds.add(sprintId);
|
|
261
|
+
}
|
|
262
|
+
for (const e of allPriorEntries) {
|
|
263
|
+
const key = `${e.work_item_id}|${e.agent_type}`;
|
|
264
|
+
if (!buckets.has(key)) buckets.set(key, { entries: [], sprintIds: new Set() });
|
|
265
|
+
const b = buckets.get(key);
|
|
266
|
+
b.entries.push(e);
|
|
267
|
+
if (e.sprint_id) b.sprintIds.add(e.sprint_id);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Step 3: apply heuristic filters ──────────────────────────────────────────
|
|
271
|
+
// Threshold: ≥3× total AND ≥2 distinct sprints AND NOT session-shared
|
|
272
|
+
const repeatedTuples = [...buckets.entries()].filter(([, b]) => {
|
|
273
|
+
if (b.entries.length < 3) return false;
|
|
274
|
+
if (b.sprintIds.size < 2) return false;
|
|
275
|
+
if (isSessionShared(b.entries)) return false;
|
|
276
|
+
return true;
|
|
277
|
+
});
|
|
165
278
|
|
|
166
279
|
// Grep FLASHCARD.md for "also do" patterns
|
|
167
280
|
const alsoDoMatches = [];
|
|
@@ -175,21 +288,35 @@ function scanSkillCandidates(sprintId, sprintDir, suggestionsFile) {
|
|
|
175
288
|
}
|
|
176
289
|
}
|
|
177
290
|
|
|
178
|
-
// Read existing suggestions file content
|
|
291
|
+
// Read existing suggestions file content (current sprint)
|
|
179
292
|
let existingContent = fs.existsSync(suggestionsFile)
|
|
180
293
|
? fs.readFileSync(suggestionsFile, 'utf8')
|
|
181
294
|
: `# Improvement Suggestions — ${sprintId}\n\nGenerated by \`suggest_improvements.mjs\`. Append-only; IDs are stable.\nVocabulary: Templates | Handoffs | Skills | Process | Tooling\n\n---\n\n`;
|
|
182
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Check if a hash already appears in current sprint's suggestions OR any prior sprint's.
|
|
298
|
+
* @param {string} hash
|
|
299
|
+
* @returns {boolean}
|
|
300
|
+
*/
|
|
301
|
+
function hashAlreadySeen(hash) {
|
|
302
|
+
const marker = `<!-- hash:${hash} -->`;
|
|
303
|
+
if (existingContent.includes(marker)) return true;
|
|
304
|
+
for (const priorContent of priorSuggestionsContents) {
|
|
305
|
+
if (priorContent.includes(marker)) return true;
|
|
306
|
+
}
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
183
310
|
// Build the candidates
|
|
184
311
|
const candidates = [];
|
|
185
312
|
let candN = 1;
|
|
186
|
-
for (const [key] of repeatedTuples) {
|
|
313
|
+
for (const [key, bucket] of repeatedTuples) {
|
|
187
314
|
const [workItemId, agentType] = key.split('|');
|
|
188
315
|
const candId = `CAND-${sprintId}-S${String(candN).padStart(2, '0')}`;
|
|
189
316
|
const hashKey = `skill|${key}`;
|
|
190
317
|
const hash = stableHash(hashKey);
|
|
191
|
-
if (!
|
|
192
|
-
candidates.push({ candId, hash, workItemId, agentType, source: 'ledger' });
|
|
318
|
+
if (!hashAlreadySeen(hash)) {
|
|
319
|
+
candidates.push({ candId, hash, workItemId, agentType, source: 'ledger', sprintIds: bucket.sprintIds });
|
|
193
320
|
candN++;
|
|
194
321
|
}
|
|
195
322
|
}
|
|
@@ -197,8 +324,8 @@ function scanSkillCandidates(sprintId, sprintDir, suggestionsFile) {
|
|
|
197
324
|
const candId = `CAND-${sprintId}-S${String(candN).padStart(2, '0')}`;
|
|
198
325
|
const hashKey = `skill|flashcard|${line.slice(0, 60)}`;
|
|
199
326
|
const hash = stableHash(hashKey);
|
|
200
|
-
if (!
|
|
201
|
-
candidates.push({ candId, hash, workItemId: null, agentType: null, source: 'flashcard', line });
|
|
327
|
+
if (!hashAlreadySeen(hash)) {
|
|
328
|
+
candidates.push({ candId, hash, workItemId: null, agentType: null, source: 'flashcard', line, sprintIds: new Set() });
|
|
202
329
|
candN++;
|
|
203
330
|
}
|
|
204
331
|
}
|
|
@@ -220,7 +347,8 @@ function scanSkillCandidates(sprintId, sprintDir, suggestionsFile) {
|
|
|
220
347
|
sectionLines.push(`<!-- hash:${c.hash} -->`);
|
|
221
348
|
sectionLines.push('');
|
|
222
349
|
if (c.source === 'ledger') {
|
|
223
|
-
|
|
350
|
+
const sprintList = c.sprintIds ? [...c.sprintIds].sort().join(', ') : sprintId;
|
|
351
|
+
sectionLines.push(`**Pattern detected:** ${c.workItemId} × ${c.agentType} repeated ≥3× across ≥2 distinct sprints (${sprintList})`);
|
|
224
352
|
} else {
|
|
225
353
|
sectionLines.push(`**Pattern detected:** "also do" pattern in FLASHCARD.md`);
|
|
226
354
|
sectionLines.push(`**Source line:** \`${c.line}\``);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Strategy: grep-based. Creates a synthetic dev-report fixture with
|
|
6
6
|
# flashcards_flagged, simulates a mock worktree-creation step, and asserts
|
|
7
|
-
# the gate contract documented in protocol §
|
|
7
|
+
# the gate contract documented in protocol §18 and the agent output-shape
|
|
8
8
|
# blocks in developer.md + qa.md.
|
|
9
9
|
#
|
|
10
10
|
# Usage: bash .cleargate/scripts/test/test_flashcard_gate.sh
|
|
@@ -30,14 +30,14 @@ fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); }
|
|
|
30
30
|
# And upon approval each card is appended to .cleargate/FLASHCARD.md
|
|
31
31
|
# And only then does worktree creation proceed
|
|
32
32
|
#
|
|
33
|
-
# This script validates the contract (protocol §
|
|
33
|
+
# This script validates the contract (protocol §18 + output-shape fields)
|
|
34
34
|
# that makes the scenario enforceable. The gate logic itself is orchestrator-
|
|
35
35
|
# out-of-band (v1: informational; v2: mandatory). The test therefore checks:
|
|
36
36
|
# (a) developer.md output-shape contains flashcards_flagged field
|
|
37
37
|
# (b) qa.md output-shape contains flashcards_flagged field
|
|
38
|
-
# (c) protocol.md §
|
|
39
|
-
# (d) §
|
|
40
|
-
# (e) §
|
|
38
|
+
# (c) protocol.md §18 exists with the correct heading
|
|
39
|
+
# (d) §18 specifies the approve/reject processing rule
|
|
40
|
+
# (e) §18 specifies the worktree creation gate
|
|
41
41
|
# (f) live vs mirror diff is empty for all three files
|
|
42
42
|
|
|
43
43
|
DEV_MD="$REPO_ROOT/.claude/agents/developer.md"
|
|
@@ -62,25 +62,25 @@ else
|
|
|
62
62
|
fail "qa.md missing flashcards_flagged field in output-shape"
|
|
63
63
|
fi
|
|
64
64
|
|
|
65
|
-
# (c) protocol.md §
|
|
65
|
+
# (c) protocol.md §18 heading exists
|
|
66
66
|
if grep -q "^## 18. Immediate Flashcard Gate (v2)" "$PROTOCOL_MD"; then
|
|
67
67
|
pass "protocol.md contains ## 18. Immediate Flashcard Gate (v2)"
|
|
68
68
|
else
|
|
69
69
|
fail "protocol.md missing ## 18. Immediate Flashcard Gate (v2)"
|
|
70
70
|
fi
|
|
71
71
|
|
|
72
|
-
# (d) §
|
|
72
|
+
# (d) §18 specifies approve + reject processing
|
|
73
73
|
if grep -q "Approve" "$PROTOCOL_MD" && grep -q "Reject" "$PROTOCOL_MD"; then
|
|
74
|
-
pass "protocol §
|
|
74
|
+
pass "protocol §18 documents Approve/Reject processing rule"
|
|
75
75
|
else
|
|
76
|
-
fail "protocol §
|
|
76
|
+
fail "protocol §18 missing Approve/Reject processing rule"
|
|
77
77
|
fi
|
|
78
78
|
|
|
79
|
-
# (e) §
|
|
79
|
+
# (e) §18 specifies the worktree creation gate
|
|
80
80
|
if grep -q "Worktree creation gate\|worktree creation gate\|MUST NOT.*worktree\|worktree.*MUST NOT" "$PROTOCOL_MD"; then
|
|
81
|
-
pass "protocol §
|
|
81
|
+
pass "protocol §18 documents worktree creation gate"
|
|
82
82
|
else
|
|
83
|
-
fail "protocol §
|
|
83
|
+
fail "protocol §18 missing worktree creation gate rule"
|
|
84
84
|
fi
|
|
85
85
|
|
|
86
86
|
# (f) qa.md says QA list is additive to Developer's
|
|
@@ -90,18 +90,18 @@ else
|
|
|
90
90
|
fail "qa.md missing note that flashcards_flagged is additive"
|
|
91
91
|
fi
|
|
92
92
|
|
|
93
|
-
# (g) developer.md references protocol §
|
|
94
|
-
if grep -q "protocol §
|
|
95
|
-
pass "developer.md references protocol §
|
|
93
|
+
# (g) developer.md references protocol §18
|
|
94
|
+
if grep -q "protocol §18\|§18" "$DEV_MD"; then
|
|
95
|
+
pass "developer.md references protocol §18"
|
|
96
96
|
else
|
|
97
|
-
fail "developer.md does not reference protocol §
|
|
97
|
+
fail "developer.md does not reference protocol §18"
|
|
98
98
|
fi
|
|
99
99
|
|
|
100
|
-
# (h) qa.md references protocol §
|
|
101
|
-
if grep -q "protocol §
|
|
102
|
-
pass "qa.md references protocol §
|
|
100
|
+
# (h) qa.md references protocol §18
|
|
101
|
+
if grep -q "protocol §18\|§18" "$QA_MD"; then
|
|
102
|
+
pass "qa.md references protocol §18"
|
|
103
103
|
else
|
|
104
|
-
fail "qa.md does not reference protocol §
|
|
104
|
+
fail "qa.md does not reference protocol §18"
|
|
105
105
|
fi
|
|
106
106
|
|
|
107
107
|
# (i) three-surface diff: live developer.md vs mirror
|