@boyingliu01/xp-gate 0.30.2 → 0.30.3
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/adapters/iac.sh +45 -4
- package/adapters/java.sh +82 -16
- package/adapters/powershell.sh +70 -9
- package/gate-10.sh +32 -2
- package/gate-3.sh +17 -0
- package/gate-4.sh +12 -0
- package/gate-7.sh +4 -0
- package/hooks/gate-10.sh +32 -2
- package/hooks/gate-3.sh +17 -0
- package/hooks/gate-4.sh +12 -0
- package/hooks/gate-7.sh +4 -0
- package/hooks/hook-module-resolution.sh +14 -0
- package/hooks/lib/coverage-total.sh +1 -1
- package/hooks/pre-commit +138 -17
- package/hooks/pre-push +97 -6
- package/hooks/sprint-gate.sh +81 -2
- package/lib/__tests__/hook-health.test.js +85 -0
- package/lib/coverage-total.sh +92 -0
- package/lib/doctor.js +44 -0
- package/lib/hook-health.js +121 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
- package/sprint-gate.sh +81 -2
package/hooks/sprint-gate.sh
CHANGED
|
@@ -181,6 +181,65 @@ read_verdict() {
|
|
|
181
181
|
json_extract "$APPROVED_FILE" '.verdict // ""' || echo ""
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
# --- Helper: sprint age in days, or empty if unknown ---
|
|
185
|
+
# Reads status + started_at from sprint-state.json. #551: the gate judged only
|
|
186
|
+
# `phase` and the delphi verdict, so a state file left at status=in_progress
|
|
187
|
+
# reported "sprint phase 6 validated" on unrelated commits for as long as
|
|
188
|
+
# nobody fixed it. An advancing sprint and an abandoned one were
|
|
189
|
+
# indistinguishable -- worse than SKIP, because SKIP is honest and PASS lies.
|
|
190
|
+
read_sprint_status() {
|
|
191
|
+
if [ ! -f "$SPRINT_STATE_JSON" ] || [ "$JSON_PARSER" = "none" ]; then
|
|
192
|
+
echo ""
|
|
193
|
+
return
|
|
194
|
+
fi
|
|
195
|
+
json_validate "$SPRINT_STATE_JSON" || { echo ""; return; }
|
|
196
|
+
json_extract "$SPRINT_STATE_JSON" '.status // ""' 2>/dev/null || echo ""
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
read_sprint_age_days() {
|
|
200
|
+
if [ ! -f "$SPRINT_STATE_JSON" ] || [ "$JSON_PARSER" = "none" ]; then
|
|
201
|
+
echo ""
|
|
202
|
+
return
|
|
203
|
+
fi
|
|
204
|
+
json_validate "$SPRINT_STATE_JSON" || { echo ""; return; }
|
|
205
|
+
local started
|
|
206
|
+
started=$(json_extract "$SPRINT_STATE_JSON" '.started_at // ""' 2>/dev/null || echo "")
|
|
207
|
+
# Older state files may carry updated_at only; either is the last sign of life.
|
|
208
|
+
if [ -z "$started" ] || [ "$started" = "null" ]; then
|
|
209
|
+
started=$(json_extract "$SPRINT_STATE_JSON" '.updated_at // ""' 2>/dev/null || echo "")
|
|
210
|
+
fi
|
|
211
|
+
if [ -z "$started" ] || [ "$started" = "null" ]; then
|
|
212
|
+
echo ""
|
|
213
|
+
return
|
|
214
|
+
fi
|
|
215
|
+
# Portable ISO-8601 -> epoch. GNU date first, BSD date second, node last; if
|
|
216
|
+
# all fail the age is unknown and the caller must not invent one.
|
|
217
|
+
local epoch=""
|
|
218
|
+
epoch=$(date -u -d "$started" +%s 2>/dev/null || true)
|
|
219
|
+
if [ -z "$epoch" ]; then
|
|
220
|
+
epoch=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$started" +%s 2>/dev/null || true)
|
|
221
|
+
fi
|
|
222
|
+
if [ -z "$epoch" ] && command -v node >/dev/null 2>&1; then
|
|
223
|
+
epoch=$(node -e "const t=Date.parse(process.argv[1]);if(Number.isFinite(t))process.stdout.write(String(Math.floor(t/1000)))" "$started" 2>/dev/null || true)
|
|
224
|
+
fi
|
|
225
|
+
if [ -z "$epoch" ]; then
|
|
226
|
+
echo ""
|
|
227
|
+
return
|
|
228
|
+
fi
|
|
229
|
+
local now
|
|
230
|
+
now=$(date -u +%s)
|
|
231
|
+
local age=$(( (now - epoch) / 86400 ))
|
|
232
|
+
[ "$age" -lt 0 ] && age=0
|
|
233
|
+
echo "$age"
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Threshold in days beyond which an in_progress sprint is considered abandoned.
|
|
237
|
+
# Configurable because the right number is project-specific: a weekly cadence
|
|
238
|
+
# wants 14, a monthly one wants more. Default 14 = two full weeks without
|
|
239
|
+
# progress: enough that a sprint paused over a holiday is not called dead, tight
|
|
240
|
+
# enough that the reported 12-day case is caught within two days of it firing.
|
|
241
|
+
SPRINT_STALE_DAYS="${SPRINT_STALE_DAYS:-14}"
|
|
242
|
+
|
|
184
243
|
# --- Pre-commit mode ---
|
|
185
244
|
if [ "$MODE" = "pre-commit" ]; then
|
|
186
245
|
PHASE=$(read_phase)
|
|
@@ -222,8 +281,28 @@ if [ "$MODE" = "pre-commit" ]; then
|
|
|
222
281
|
exit 1
|
|
223
282
|
fi
|
|
224
283
|
|
|
225
|
-
#
|
|
226
|
-
|
|
284
|
+
# #551: staleness. A sprint left at status=in_progress past the threshold is
|
|
285
|
+
# abandoned, not validated -- reporting PASS for it means the gate cannot
|
|
286
|
+
# distinguish an advancing sprint from a forgotten one, and it injects
|
|
287
|
+
# ancient phase_history into every unrelated commit's audit trail. SKIP is
|
|
288
|
+
# the honest answer: the gate declines to certify a sprint nobody is
|
|
289
|
+
# driving, without blocking work that has nothing to do with it.
|
|
290
|
+
SPRINT_STATUS=$(read_sprint_status)
|
|
291
|
+
SPRINT_AGE_DAYS=$(read_sprint_age_days)
|
|
292
|
+
if [ "$SPRINT_STATUS" = "in_progress" ] && [ -n "$SPRINT_AGE_DAYS" ] \
|
|
293
|
+
&& [ "$SPRINT_AGE_DAYS" -ge "$SPRINT_STALE_DAYS" ] 2>/dev/null; then
|
|
294
|
+
echo "{\"decision\":\"skip\",\"reason\":\"STALE SPRINT: status=in_progress for ${SPRINT_AGE_DAYS} days (threshold ${SPRINT_STALE_DAYS}) — not certifying it. Finish it (sprint-flow close) or clear .sprint-state/sprint-state.json\"}"
|
|
295
|
+
exit 0
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
# All checks passed. Name the age so "validated" is not an unconditional
|
|
299
|
+
# claim: a 29-day-old sprint under a 30-day threshold is a very different
|
|
300
|
+
# situation from one opened today, and the log should say which it is.
|
|
301
|
+
if [ -n "$SPRINT_AGE_DAYS" ]; then
|
|
302
|
+
echo "{\"decision\":\"allow\",\"message\":\"delphi-review APPROVED, sprint phase ${PHASE} validated (sprint open ${SPRINT_AGE_DAYS} day(s), staleness threshold ${SPRINT_STALE_DAYS})\"}"
|
|
303
|
+
else
|
|
304
|
+
echo "{\"decision\":\"allow\",\"message\":\"delphi-review APPROVED, sprint phase ${PHASE} validated (sprint age unknown — started_at unreadable)\"}"
|
|
305
|
+
fi
|
|
227
306
|
exit 0
|
|
228
307
|
fi
|
|
229
308
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-546 hook health: line endings and bash parseability
|
|
3
|
+
* @intent Verify doctor can tell a hook that merely EXISTS from a hook git can
|
|
4
|
+
* actually run. #546: a pre-commit rewritten with CRLF line endings
|
|
5
|
+
* failed every commit while `xp-gate doctor` reported it healthy,
|
|
6
|
+
* because the check only asked whether the file was there.
|
|
7
|
+
* @covers AC-05, AC-08
|
|
8
|
+
*/
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
const { inspectHookFile } = require('../hook-health');
|
|
14
|
+
|
|
15
|
+
let TMP_DIR;
|
|
16
|
+
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'hook-health-'));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
fs.rmSync(TMP_DIR, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function write(name, content) {
|
|
26
|
+
const p = path.join(TMP_DIR, name);
|
|
27
|
+
fs.writeFileSync(p, content);
|
|
28
|
+
return p;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('inspectHookFile', () => {
|
|
32
|
+
test('a healthy LF hook reports ok', () => {
|
|
33
|
+
const p = write('good.sh', '#!/bin/bash\nfoo() {\n echo hi\n}\nfoo\n');
|
|
34
|
+
const r = inspectHookFile(p);
|
|
35
|
+
expect(r.ok).toBe(true);
|
|
36
|
+
expect(r.crlfLines).toBe(0);
|
|
37
|
+
expect(r.problems).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('an all-CRLF hook is reported with a CRLF count', () => {
|
|
41
|
+
const p = write('crlf.sh', '#!/bin/bash\r\nfoo() {\r\n echo hi\r\n}\r\nfoo\r\n');
|
|
42
|
+
const r = inspectHookFile(p);
|
|
43
|
+
expect(r.ok).toBe(false);
|
|
44
|
+
expect(r.crlfLines).toBe(5);
|
|
45
|
+
expect(r.problems.join(' ')).toMatch(/CRLF/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('a single stray CRLF is enough to fail', () => {
|
|
49
|
+
// One bad line is all bash needs to choke on, and it is the case an
|
|
50
|
+
// "is the file mostly fine" heuristic would wave through.
|
|
51
|
+
const p = write('one.sh', '#!/bin/bash\necho one\necho two\r\necho three\n');
|
|
52
|
+
const r = inspectHookFile(p);
|
|
53
|
+
expect(r.ok).toBe(false);
|
|
54
|
+
expect(r.crlfLines).toBe(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('CRLF in the middle of a multi-line construct is flagged', () => {
|
|
58
|
+
// The exact shape from #546: the \r lands inside a construct bash is
|
|
59
|
+
// mid-way through parsing, so the reported line number drifts far from
|
|
60
|
+
// the real cause.
|
|
61
|
+
const p = write('mid.sh', 'case "$x" in\r\n a) echo a ;;\r\nesac\n');
|
|
62
|
+
const r = inspectHookFile(p);
|
|
63
|
+
expect(r.ok).toBe(false);
|
|
64
|
+
expect(r.crlfLines).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('a syntax error with clean line endings is reported as unparseable', () => {
|
|
68
|
+
const p = write('bad.sh', '#!/bin/bash\nif [ 1 -eq 1 ]; then\n echo x\n');
|
|
69
|
+
const r = inspectHookFile(p);
|
|
70
|
+
expect(r.ok).toBe(false);
|
|
71
|
+
expect(r.problems.join(' ')).toMatch(/not parseable|syntax/i);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('a binary-looking file does not crash the inspector', () => {
|
|
75
|
+
const p = path.join(TMP_DIR, 'bin.sh');
|
|
76
|
+
fs.writeFileSync(p, Buffer.from([0x00, 0xff, 0xfe, 0x0a]));
|
|
77
|
+
expect(() => inspectHookFile(p)).not.toThrow();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('a missing file reports missing rather than throwing', () => {
|
|
81
|
+
const r = inspectHookFile(path.join(TMP_DIR, 'nope.sh'));
|
|
82
|
+
expect(r.ok).toBe(false);
|
|
83
|
+
expect(r.problems.join(' ')).toMatch(/missing/i);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Gate 5's Python coverage parsing and threshold verdict (#523).
|
|
3
|
+
#
|
|
4
|
+
# Sourced by githooks/pre-commit; kept in its own file so the contract is
|
|
5
|
+
# unit-testable without executing the hook
|
|
6
|
+
# (githooks/__tests__/issue-523-python-coverage-parse.test.bats).
|
|
7
|
+
#
|
|
8
|
+
# The defect this file exists for: `coverage report --format=total` prints a
|
|
9
|
+
# BARE number ("97"), but the hook piped it through `grep -o '[0-9]*%'` — a
|
|
10
|
+
# pattern that requires a literal percent sign — so COVERAGE_PERCENT was always
|
|
11
|
+
# empty and the 80% threshold branch never executed. The gate answered "Could
|
|
12
|
+
# not determine" forever: a silent no-op, not a verdict.
|
|
13
|
+
|
|
14
|
+
# Parse the raw output of `coverage report --format=total` (stdin) and echo the
|
|
15
|
+
# integer total percentage — or nothing when the input carries no number.
|
|
16
|
+
#
|
|
17
|
+
# coverage.py 7.x --format=total prints a bare number; other tools (and older
|
|
18
|
+
# forms) append a percent sign. Only a line that IS a bare number (optionally
|
|
19
|
+
# %-suffixed) is accepted — a line that merely CONTAINS a number ("WARNING: 2
|
|
20
|
+
# files skipped") must not be mistaken for the total, so the whole line is
|
|
21
|
+
# matched, not the first token. A decimal part is truncated (floor — the
|
|
22
|
+
# conservative direction for a threshold gate, the same convention as the Java
|
|
23
|
+
# path's ${VAR%.*}). Inputs without any bare-number line (empty, "No data to
|
|
24
|
+
# report.", warnings) produce NO output so the caller takes its
|
|
25
|
+
# cannot-determine path: a wrong number is worse than none.
|
|
26
|
+
parse_coverage_total() {
|
|
27
|
+
awk '
|
|
28
|
+
{
|
|
29
|
+
line = $0
|
|
30
|
+
sub(/%$/, "", line)
|
|
31
|
+
gsub(/^[ \t]+|[ \t]+$/, "", line)
|
|
32
|
+
if (line ~ /^[0-9]+(\.[0-9]+)?$/) {
|
|
33
|
+
sub(/\.[0-9]+$/, "", line)
|
|
34
|
+
print line
|
|
35
|
+
exit
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Gate 5 Stage 2 Python coverage enforcement — the hook's python case body,
|
|
42
|
+
# extracted so the mock-PATH test exercises the real control flow. Echoes the
|
|
43
|
+
# verdict lines and returns 1 on BLOCK (the caller turns that into `exit 1`).
|
|
44
|
+
#
|
|
45
|
+
# COVERAGE_PERCENT is deliberately NOT declared local: the hook's quality
|
|
46
|
+
# report reads it after the Stage-2 loop (last writer wins across languages),
|
|
47
|
+
# exactly as the inline branch did.
|
|
48
|
+
enforce_python_coverage() {
|
|
49
|
+
# Critical-1 (#399): when the test run was skipped (PY_STRATEGY=skip, set in
|
|
50
|
+
# pre-commit before this Stage-2 function), refuse to consume a stale
|
|
51
|
+
# .coverage left by an earlier subset run — its low % is meaningless for a
|
|
52
|
+
# commit that changed no Python and must not BLOCK a docs-only change. This
|
|
53
|
+
# mirrors the TS branch's TESTS_SKIPPED guard.
|
|
54
|
+
if [ "${PY_TEST_SKIPPED:-false}" = "true" ]; then
|
|
55
|
+
echo "ℹ️ Test run was skipped — skipping Python coverage check."
|
|
56
|
+
return 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
if ! command -v coverage &> /dev/null; then
|
|
60
|
+
echo "⚠️ coverage.py not available, cannot enforce Python coverage threshold."
|
|
61
|
+
return 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
COVERAGE_PERCENT=""
|
|
65
|
+
if [ -f ".coverage" ]; then
|
|
66
|
+
COVERAGE_PERCENT=$(coverage report --format=total 2>/dev/null | parse_coverage_total) || true
|
|
67
|
+
fi
|
|
68
|
+
if [ -z "$COVERAGE_PERCENT" ] && [ -f "coverage/.coverage" ]; then
|
|
69
|
+
COVERAGE_PERCENT=$(coverage report --format=total -i coverage 2>/dev/null | parse_coverage_total) || true
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
if [ -n "$COVERAGE_PERCENT" ]; then
|
|
73
|
+
if [ "$COVERAGE_PERCENT" -lt "${COVERAGE_THRESHOLD:-80}" ]; then
|
|
74
|
+
# #399: a subset run (smart test selection) measures only the tested
|
|
75
|
+
# files, so its global total is meaningless as a verdict — the full
|
|
76
|
+
# suite may well be >=80% even when the subset is not. Warn exactly as
|
|
77
|
+
# the TypeScript branch does (#286) instead of blocking. Full runs keep
|
|
78
|
+
# the hard BLOCK below.
|
|
79
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
80
|
+
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
81
|
+
echo " Run the full suite for a real Python coverage check."
|
|
82
|
+
return 0
|
|
83
|
+
fi
|
|
84
|
+
echo "❌ BLOCKED - Python coverage ${COVERAGE_PERCENT}% below 80% threshold"
|
|
85
|
+
return 1
|
|
86
|
+
fi
|
|
87
|
+
echo "Python coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
88
|
+
else
|
|
89
|
+
echo "⚠️ Could not determine Python coverage percentage. Ensure pytest-cov or coverage.py data exists."
|
|
90
|
+
fi
|
|
91
|
+
return 0
|
|
92
|
+
}
|
package/lib/doctor.js
CHANGED
|
@@ -18,6 +18,14 @@ const { GATE_CLI_TOOLS, checkCliTool, getToolInstallCmd, checkBash } = require('
|
|
|
18
18
|
// #505: the single source of truth for module resolution order. The hook reads a
|
|
19
19
|
// generated shell projection of this same module.
|
|
20
20
|
const { RESOLUTION_ORDER } = require('./hook-module-resolution.js');
|
|
21
|
+
const { inspectHookFile } = require('./hook-health.js');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Files above this size get a `bash -n` probe in addition to the CRLF scan.
|
|
25
|
+
* Real xp-gate hooks are 40 KB (pre-push) to 190 KB (pre-commit); test fixtures
|
|
26
|
+
* and stubs are a few hundred bytes. 4 KB separates them comfortably.
|
|
27
|
+
*/
|
|
28
|
+
const PARSE_CHECK_MIN_BYTES = 4096;
|
|
21
29
|
const {
|
|
22
30
|
diagnoseTuiRegistration, diagnoseInstalledSkills,
|
|
23
31
|
fixTuiRegistration, ensureTuiRegistration, readTuiJson,
|
|
@@ -1256,10 +1264,46 @@ function checkSingleHook(hooksDir, name, signature, label, checks) {
|
|
|
1256
1264
|
checks.push({ name: `${label}: ${name}`, status: 'FAIL', detail: 'Missing or not xp-gate' });
|
|
1257
1265
|
return 1;
|
|
1258
1266
|
}
|
|
1267
|
+
// Existence and signature are not enough (#546): a hook rewritten with CRLF
|
|
1268
|
+
// line endings, or truncated mid-write, satisfies both and still cannot run.
|
|
1269
|
+
// Reporting it as healthy is worse than reporting nothing, because the
|
|
1270
|
+
// operator stops looking.
|
|
1271
|
+
//
|
|
1272
|
+
// The CRLF scan is pure and cheap; the `bash -n` probe spawns a process, so it
|
|
1273
|
+
// is skipped for files under the size floor below — test fixtures and stub
|
|
1274
|
+
// hooks are not the things that break here, and spawning bash per stub made
|
|
1275
|
+
// doctor slower than its own test timeout.
|
|
1276
|
+
const health = inspectHookFile(hookPath, { parseCheck: shouldParseCheck(hookPath) });
|
|
1277
|
+
if (!health.ok) {
|
|
1278
|
+
checks.push({
|
|
1279
|
+
name: `${label}: ${name}`,
|
|
1280
|
+
status: 'FAIL',
|
|
1281
|
+
detail: health.problems.join('; '),
|
|
1282
|
+
});
|
|
1283
|
+
return 1;
|
|
1284
|
+
}
|
|
1259
1285
|
checks.push({ name: `${label}: ${name}`, status: 'PASS', detail: hookPath });
|
|
1260
1286
|
return 0;
|
|
1261
1287
|
}
|
|
1262
1288
|
|
|
1289
|
+
/**
|
|
1290
|
+
* Should the `bash -n` probe run for this hook?
|
|
1291
|
+
*
|
|
1292
|
+
* Measured on a real install: a 190 KB pre-commit takes ~90 ms to parse, but a
|
|
1293
|
+
* dozen stubs in a test fixture still add up to seconds of process spawns. The
|
|
1294
|
+
* CRLF scan catches the #546 failure without spawning anything, so the probe is
|
|
1295
|
+
* reserved for files large enough to be a real hook.
|
|
1296
|
+
* @param {string} hookPath - Hook path
|
|
1297
|
+
* @returns {boolean} Whether to run the parse check
|
|
1298
|
+
*/
|
|
1299
|
+
function shouldParseCheck(hookPath) {
|
|
1300
|
+
try {
|
|
1301
|
+
return fs.statSync(hookPath).size > PARSE_CHECK_MIN_BYTES;
|
|
1302
|
+
} catch {
|
|
1303
|
+
return false;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1263
1307
|
/**
|
|
1264
1308
|
* Expected gate scripts that should be present in the adapters directory.
|
|
1265
1309
|
* gate-5.sh and gate-6.sh are inline in pre-commit, so they are NOT expected here.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook file health inspection (#546).
|
|
3
|
+
*
|
|
4
|
+
* `xp-gate doctor` used to answer one question about an installed hook: does the
|
|
5
|
+
* file exist and does it carry the xp-gate signature. A hook can satisfy both
|
|
6
|
+
* and still be unrunnable — #546 was a pre-commit that had been rewritten with
|
|
7
|
+
* CRLF line endings, which fails every commit while doctor reported ✓.
|
|
8
|
+
*
|
|
9
|
+
* Two independent failure modes are checked here:
|
|
10
|
+
* - CRLF line endings: git runs hooks through bash, and a stray \r makes the
|
|
11
|
+
* parser choke at a line number far from the real cause (the reported line
|
|
12
|
+
* is wherever the multi-line construct that swallowed the \r began).
|
|
13
|
+
* - Unparseable syntax: a truncated or mangled write leaves a hook that
|
|
14
|
+
* exists but cannot run.
|
|
15
|
+
*
|
|
16
|
+
* Both are reported per file rather than as a single boolean, because the
|
|
17
|
+
* operator needs to know which one happened: CRLF has a mechanical fix, a
|
|
18
|
+
* syntax error does not.
|
|
19
|
+
*/
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { spawnSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Locate a bash binary without assuming it is on PATH under that exact name.
|
|
26
|
+
* @returns {string|null} Path or bare command name, null when none found
|
|
27
|
+
*/
|
|
28
|
+
function findBash() {
|
|
29
|
+
const candidates = ['bash'];
|
|
30
|
+
if (process.platform === 'win32') {
|
|
31
|
+
// Git for Windows ships bash outside PATH far more often than inside it.
|
|
32
|
+
const pf = process.env.ProgramFiles || 'C:\\Program Files';
|
|
33
|
+
const pf86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
|
34
|
+
const local = process.env.LOCALAPPDATA;
|
|
35
|
+
if (local) candidates.push(path.join(local, 'Programs', 'Git', 'bin', 'bash.exe'));
|
|
36
|
+
candidates.push(path.join(pf, 'Git', 'bin', 'bash.exe'));
|
|
37
|
+
candidates.push(path.join(pf86, 'Git', 'bin', 'bash.exe'));
|
|
38
|
+
candidates.push(path.join(pf, 'Git', 'usr', 'bin', 'bash.exe'));
|
|
39
|
+
}
|
|
40
|
+
for (const c of candidates) {
|
|
41
|
+
if (path.isAbsolute(c)) {
|
|
42
|
+
if (fs.existsSync(c)) return c;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const probe = spawnSync(c, ['--version'], { encoding: 'utf8', timeout: 10000 });
|
|
46
|
+
if (!probe.error && probe.status === 0) return c;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Count lines that end with CRLF.
|
|
53
|
+
* @param {string} text - File content
|
|
54
|
+
* @returns {number} Number of CRLF-terminated lines
|
|
55
|
+
*/
|
|
56
|
+
function countCrlfLines(text) {
|
|
57
|
+
const matches = text.match(/\r\n/g);
|
|
58
|
+
return matches ? matches.length : 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ask bash whether it can parse the file (`bash -n`).
|
|
63
|
+
* @param {string} filePath - Hook to check
|
|
64
|
+
* @param {string} bashBin - Resolved bash binary
|
|
65
|
+
* @returns {{checked: boolean, ok: boolean, detail: string}} Parse verdict
|
|
66
|
+
*/
|
|
67
|
+
function checkParseable(filePath, bashBin) {
|
|
68
|
+
if (!bashBin) return { checked: false, ok: true, detail: 'bash not found; parse check skipped' };
|
|
69
|
+
const res = spawnSync(bashBin, ['-n', filePath], { encoding: 'utf8', timeout: 30000 });
|
|
70
|
+
if (res.error) return { checked: false, ok: true, detail: `bash -n could not run: ${res.error.message}` };
|
|
71
|
+
if (res.status === 0) return { checked: true, ok: true, detail: 'parses' };
|
|
72
|
+
// bash reports the first syntax error it hits. Under CRLF that line number is
|
|
73
|
+
// unreliable, which is exactly why the CRLF count is reported alongside.
|
|
74
|
+
const firstLine = String(res.stderr || '').split('\n').find(l => l.trim()) || `exit ${res.status}`;
|
|
75
|
+
return { checked: true, ok: false, detail: firstLine.trim() };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Inspect one hook file for the conditions that make git unable to run it.
|
|
80
|
+
* @param {string} filePath - Hook path
|
|
81
|
+
* @param {{bashBin?: string|null, parseCheck?: boolean}} [options] - Overrides
|
|
82
|
+
* @returns {{ok: boolean, crlfLines: number, parseable: boolean, problems: string[]}}
|
|
83
|
+
*/
|
|
84
|
+
function inspectHookFile(filePath, options = {}) {
|
|
85
|
+
const problems = [];
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(filePath)) {
|
|
88
|
+
return { ok: false, crlfLines: 0, parseable: false, problems: [`hook file is missing: ${filePath}`] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let text = '';
|
|
92
|
+
try {
|
|
93
|
+
text = fs.readFileSync(filePath, 'latin1');
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return { ok: false, crlfLines: 0, parseable: false, problems: [`hook file is unreadable: ${err.message}`] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const crlfLines = countCrlfLines(text);
|
|
99
|
+
if (crlfLines > 0) {
|
|
100
|
+
problems.push(
|
|
101
|
+
`hook has CRLF line endings (${crlfLines} line(s)) — bash will fail to parse it; ` +
|
|
102
|
+
'run `tr -d "\\r" < hook > hook.fixed && mv hook.fixed hook`'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// A CRLF hit already answers the question this probe exists to answer, and
|
|
107
|
+
// the probe is the expensive half. Callers can also opt out for stubs.
|
|
108
|
+
if (crlfLines > 0 || options.parseCheck === false) {
|
|
109
|
+
return { ok: problems.length === 0, crlfLines, parseable: crlfLines === 0, problems };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const bashBin = options.bashBin !== undefined ? options.bashBin : findBash();
|
|
113
|
+
const parsed = checkParseable(filePath, bashBin);
|
|
114
|
+
if (!parsed.ok) {
|
|
115
|
+
problems.push(`hook is not parseable by bash: ${parsed.detail}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { ok: problems.length === 0, crlfLines, parseable: parsed.ok, problems };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { inspectHookFile, findBash, countCrlfLines };
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-20
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 03f6d10a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
|
package/mutation/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MUTATION KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-20
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 03f6d10a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.3",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-20
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 03f6d10a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-20
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 03f6d10a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-20
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 03f6d10a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/plugins/dsh/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyingliu01/dsh-plugin-xp-gate",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.3",
|
|
4
4
|
"description": "XP-Gate deterministic quality gates + AI workflow skills as a DeepSeek Harness (DSH) native plugin: gate-check / gate-principles / gate-arch tools + 12 SKILL.md skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-20
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 03f6d10a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-20
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 03f6d10a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-20
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 03f6d10a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-20
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 03f6d10a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-20
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 03f6d10a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-20
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 03f6d10a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.3",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Qoder. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|