@boyingliu01/xp-gate 0.35.18 → 0.35.19
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/gate-3.sh +94 -9
- package/adapters/gate-4.sh +27 -32
- package/adapters/gate-8.sh +32 -3
- package/adapters/gate-9.sh +14 -2
- package/lib/__tests__/doctor-delphi.test.js +114 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +74 -0
- package/lib/__tests__/update-skill.test.js +77 -0
- package/lib/doctor-tui.js +23 -2
- package/lib/doctor.js +28 -7
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/update-skill.js +59 -3
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/scripts/sync-package-content.js +32 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/adapters/gate-3.sh
CHANGED
|
@@ -172,8 +172,19 @@ elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
|
172
172
|
const entries = baseline.functions || baseline.entries || baseline;
|
|
173
173
|
if (Array.isArray(entries)) {
|
|
174
174
|
for (const e of entries) {
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
// #632: accept the key the baseline WRITER actually emits.
|
|
176
|
+
// tools/Update-ComplexityBaseline.ps1 (the tool this gate
|
|
177
|
+
// points at in its own fix-it message) writes "function",
|
|
178
|
+
// while this reader only knew "name" -- so every entry was
|
|
179
|
+
// dropped, "known" stayed empty, and every function above
|
|
180
|
+
// the threshold became a brand-new function. The ratchet
|
|
181
|
+
// could never pass. Both spellings are read: this is a
|
|
182
|
+
// reader, and the writer vocabulary is not ours to
|
|
183
|
+
// redefine from here.
|
|
184
|
+
if (!e || !e.file) continue;
|
|
185
|
+
const fnName = e.name || e.function;
|
|
186
|
+
if (!fnName) continue;
|
|
187
|
+
known.set(norm(e.file) + "::" + fnName, Number(e.ccn) || 0);
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
190
|
// The measurement emits ABSOLUTE paths (pwsh resolved them), while
|
|
@@ -450,6 +461,52 @@ else
|
|
|
450
461
|
GATE_3_DETAIL="mode=ratchet verifier=MISSING"
|
|
451
462
|
exit 1
|
|
452
463
|
fi
|
|
464
|
+
RATCHET_LIB="$GATE_DIR/lib/ccn-ratchet.sh"
|
|
465
|
+
[ -f "$RATCHET_LIB" ] || RATCHET_LIB="$(git rev-parse --show-toplevel 2>/dev/null)/githooks/lib/ccn-ratchet.sh"
|
|
466
|
+
# #617: grade exactly the population the baseline was written from.
|
|
467
|
+
#
|
|
468
|
+
# The comment below has always said so, but only as a comment: a repo whose
|
|
469
|
+
# baseline came from a narrower scan (a Java-only ccn-report, say) had its
|
|
470
|
+
# vendored minified JS graded anyway, and --csv-verified then disagreed with
|
|
471
|
+
# lizard on single-line code and fail-closed with "could not be
|
|
472
|
+
# span-verified". That BLOCK has no repair: the file has no baseline entry
|
|
473
|
+
# to ratchet, so there is nothing the user could add to clear it.
|
|
474
|
+
#
|
|
475
|
+
# The population filter runs BEFORE verification on purpose. Domain
|
|
476
|
+
# restriction is decidable from the path alone, so asking the expensive
|
|
477
|
+
# span re-derivation about a file that will not be graded anyway only
|
|
478
|
+
# manufactures failures for code the ratchet never claims to cover.
|
|
479
|
+
if [ -f "$RATCHET_LIB" ]; then
|
|
480
|
+
# shellcheck source=/dev/null
|
|
481
|
+
. "$RATCHET_LIB" 2>/dev/null || true
|
|
482
|
+
if declare -F ccn_file_in_population >/dev/null 2>&1; then
|
|
483
|
+
_cc_pop_kept=""
|
|
484
|
+
_cc_pop_skipped=""
|
|
485
|
+
while IFS= read -r _cc_row; do
|
|
486
|
+
[ -n "$_cc_row" ] || continue
|
|
487
|
+
_cc_row_path=$(printf '%s' "$_cc_row" | awk -F, '{print $1}')
|
|
488
|
+
if ccn_file_in_population ".ccn-baseline.json" "$_cc_row_path"; then
|
|
489
|
+
_cc_pop_kept="${_cc_pop_kept}${_cc_row}
|
|
490
|
+
"
|
|
491
|
+
else
|
|
492
|
+
_cc_pop_skipped="${_cc_pop_skipped}${_cc_row_path}
|
|
493
|
+
"
|
|
494
|
+
fi
|
|
495
|
+
done <<CC_POP_EOF
|
|
496
|
+
$CC_CSV
|
|
497
|
+
CC_POP_EOF
|
|
498
|
+
CC_CSV=$(printf '%s' "$_cc_pop_kept")
|
|
499
|
+
if [ -n "$_cc_pop_skipped" ]; then
|
|
500
|
+
_cc_pop_skip_n=$(printf '%s' "$_cc_pop_skipped" | grep -c . || true)
|
|
501
|
+
echo "ℹ️ Gate 3: ${_cc_pop_skip_n:-0} file(s) outside the baseline population — not graded, not ratcheted:"
|
|
502
|
+
printf '%s' "$_cc_pop_skipped" | sed 's/^/ /' | sed -n '1,5p'
|
|
503
|
+
if [ "${_cc_pop_skip_n:-0}" -gt 5 ]; then
|
|
504
|
+
echo " ... and $((_cc_pop_skip_n - 5)) more"
|
|
505
|
+
fi
|
|
506
|
+
echo " (declare or widen the population in .ccn-baseline.json to grade them)"
|
|
507
|
+
fi
|
|
508
|
+
fi
|
|
509
|
+
fi
|
|
453
510
|
if [ -f "$CC_REPORT" ] && command -v node >/dev/null 2>&1; then
|
|
454
511
|
_cc_verify_failed=0
|
|
455
512
|
CC_CSV_VERIFIED=$(printf '%s\n' "$CC_CSV" | node "$CC_REPORT" --csv-verified batch --root "$(git rev-parse --show-toplevel 2>/dev/null)") || _cc_verify_failed=1
|
|
@@ -460,8 +517,6 @@ else
|
|
|
460
517
|
fi
|
|
461
518
|
CC_CSV="$CC_CSV_VERIFIED"
|
|
462
519
|
fi
|
|
463
|
-
RATCHET_LIB="$GATE_DIR/lib/ccn-ratchet.sh"
|
|
464
|
-
[ -f "$RATCHET_LIB" ] || RATCHET_LIB="$(git rev-parse --show-toplevel 2>/dev/null)/githooks/lib/ccn-ratchet.sh"
|
|
465
520
|
# Fail closed when the ratchet library is missing. Sourcing a nonexistent
|
|
466
521
|
# file only prints "No such file or directory" and leaves
|
|
467
522
|
# evaluate_ccn_ratchet undefined; the verdict then came back empty, every
|
|
@@ -529,13 +584,43 @@ else
|
|
|
529
584
|
# this, a crashing or mis-invoked lizard yields 0 warnings and the gate
|
|
530
585
|
# reports PASSED on zero analysed functions -- the same silent-success
|
|
531
586
|
# failure this issue is about.
|
|
587
|
+
#
|
|
588
|
+
# #666: "no rows" has TWO causes and they must not share a verdict. A
|
|
589
|
+
# module of pure constants legitimately has zero functions, and lizard
|
|
590
|
+
# emits no row for it at all (measured: `lizard pure_data.py --csv` prints
|
|
591
|
+
# nothing and exits 0). That was reported as `lizard_output_unparsable`
|
|
592
|
+
# /WARN, which is the message for a BROKEN lizard -- so a healthy run on a
|
|
593
|
+
# data-only diff was indistinguishable from a tooling fault. The two are
|
|
594
|
+
# separated by a POSITIVE probe: ask lizard to describe a fixture known to
|
|
595
|
+
# contain a function. If it produces rows, lizard works and the input
|
|
596
|
+
# simply had no functions (SKIP, `no_functions_in_input`); if it does not,
|
|
597
|
+
# the tool really is broken (WARN, `lizard_output_unparsable`).
|
|
532
598
|
if ! printf '%s\n' "$CC_CSV_FLAT" | awk -F, 'NF > 1 && $2 ~ /^[0-9]+$/ { found = 1; exit }
|
|
533
599
|
END { exit(found ? 0 : 1) }'; then
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
600
|
+
_lizard_probe_dir=$(mktemp -d 2>/dev/null || echo "${TMPDIR:-/tmp}")
|
|
601
|
+
_lizard_probe_file="$_lizard_probe_dir/xp-gate-lizard-probe.py"
|
|
602
|
+
printf 'def _xp_gate_probe(x):\n if x:\n return 1\n return 0\n' > "$_lizard_probe_file" 2>/dev/null
|
|
603
|
+
_lizard_probe_rows=0
|
|
604
|
+
if [ -f "$_lizard_probe_file" ]; then
|
|
605
|
+
_lizard_probe_rows=$(printf '%s\n' "$_lizard_probe_file" \
|
|
606
|
+
| lizard_batches --csv 2>/dev/null \
|
|
607
|
+
| awk -F, 'NF > 1 && $2 ~ /^[0-9]+$/ { c++ } END { print c + 0 }')
|
|
608
|
+
fi
|
|
609
|
+
rm -rf "$_lizard_probe_dir" 2>/dev/null || true
|
|
610
|
+
if [ "${_lizard_probe_rows:-0}" -gt 0 ]; then
|
|
611
|
+
echo "⏭️ SKIPPED - Complexity: the changed source files define no functions (pure data module)."
|
|
612
|
+
echo " Verified: lizard parsed a probe fixture normally, so this is an empty result set, not a broken tool."
|
|
613
|
+
GATE_3_STATUS="SKIP"
|
|
614
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
615
|
+
# shellcheck disable=SC2034
|
|
616
|
+
GATE_3_REASON="no_functions_in_input"
|
|
617
|
+
else
|
|
618
|
+
echo "⚠️ WARN - lizard produced no parsable results; complexity not verified"
|
|
619
|
+
GATE_3_STATUS="WARN"
|
|
620
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
621
|
+
# shellcheck disable=SC2034
|
|
622
|
+
GATE_3_REASON="lizard_output_unparsable"
|
|
623
|
+
fi
|
|
539
624
|
elif [ "$CC_WARNINGS" -gt 0 ]; then
|
|
540
625
|
echo "$CC_OUTPUT"
|
|
541
626
|
echo ""
|
package/adapters/gate-4.sh
CHANGED
|
@@ -46,7 +46,14 @@ java_coverage_note() {
|
|
|
46
46
|
esac
|
|
47
47
|
done
|
|
48
48
|
[ -n "$_has_java" ] || return 0
|
|
49
|
-
|
|
49
|
+
# #628: `docs/` is not in the npm package's `files` list, so pointing a user at
|
|
50
|
+
# `docs/java-principles-coverage.md` was a dead path in every installed project --
|
|
51
|
+
# the note told them to read a file that does not exist on their machine. Cite the
|
|
52
|
+
# upstream URL instead, and keep the local path only when the doc is actually
|
|
53
|
+
# present (i.e. when running inside this repo).
|
|
54
|
+
local _where="https://github.com/boyingliu01/xp-gate/blob/main/docs/java-principles-coverage.md"
|
|
55
|
+
[ -f "docs/java-principles-coverage.md" ] && _where="docs/java-principles-coverage.md"
|
|
56
|
+
echo "ℹ️ Java rule coverage: $JAVA_COVERAGE_EFFECTIVE/$JAVA_COVERAGE_TOTAL rules effective (inert on Java: $JAVA_COVERAGE_INERT -- see $_where)"
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
2>&1 echo ""
|
|
@@ -165,42 +172,30 @@ else
|
|
|
165
172
|
rm -f "$PRINCIPLES_STDERR"
|
|
166
173
|
|
|
167
174
|
if [ "$PRINCIPLES_EXIT" -ge 2 ]; then
|
|
168
|
-
#
|
|
169
|
-
#
|
|
175
|
+
# #661: exit>=2 means the checker did not RUN (crash, module not found,
|
|
176
|
+
# bad invocation). This used to record SKIP, and pre-commit treats SKIP
|
|
177
|
+
# as non-blocking, so a tool failure was absorbed into the green light --
|
|
178
|
+
# the reproduction printed a module-not-found stack trace and then
|
|
179
|
+
# "✅ PASSED - Principles checker (no errors found)".
|
|
170
180
|
#
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
# The path conversion fixed that one cause; any other way of breaking
|
|
176
|
-
# the launch (missing tsx, corrupt module, OOM) lands here too, and all
|
|
177
|
-
# of them would have been absorbed into a green light.
|
|
181
|
+
# The distinction the gate must keep:
|
|
182
|
+
# - tool NOT INSTALLED -> SKIP is honest; nothing is being claimed.
|
|
183
|
+
# - tool PRESENT, CRASH -> WARN. Not a pass (nothing was checked) and
|
|
184
|
+
# not a silent skip (the user must know their code went unaudited).
|
|
178
185
|
#
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
echo "
|
|
184
|
-
echo "
|
|
185
|
-
echo "
|
|
186
|
-
|
|
186
|
+
# WARN rather than FAIL on purpose: the commit itself is not necessarily
|
|
187
|
+
# bad, the TOOL is broken, and blocking every commit until node/tsx is
|
|
188
|
+
# repaired would blame the change for an environment fault. What must
|
|
189
|
+
# never happen is the green light.
|
|
190
|
+
echo "⚠️ Principles checker did not run (exit ${PRINCIPLES_EXIT}) — this code is NOT verified."
|
|
191
|
+
echo " Gate 4 cannot claim a pass for a check that never executed."
|
|
192
|
+
echo " Fix the checker invocation above (or run 'npx xp-gate check principles .')."
|
|
193
|
+
echo "⏭️ WARN - Principles check (checker execution error)"
|
|
194
|
+
GATE_4_STATUS="WARN"
|
|
195
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
187
196
|
# shellcheck disable=SC2034
|
|
188
197
|
GATE_4_REASON="checker_execution_error"
|
|
189
198
|
rm -f "$PRINCIPLES_JSON"
|
|
190
|
-
if [ "${PRINCIPLES_SKIP_TOOL_FAULT:-0}" = "1" ]; then
|
|
191
|
-
echo "⚠️ ESCAPE VALVE: PRINCIPLES_SKIP_TOOL_FAULT=1 — downgrading to SKIP"
|
|
192
|
-
# Same asymmetry as Gate 5a: a downgrade is tolerable on a feature
|
|
193
|
-
# branch, never on main/master, where it would green-light the trunk.
|
|
194
|
-
_g4_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
|
195
|
-
if [ "$_g4_branch" = "main" ] || [ "$_g4_branch" = "master" ]; then
|
|
196
|
-
echo "❌ ESCAPE VALVE BLOCKED: PRINCIPLES_SKIP_TOOL_FAULT not allowed on $_g4_branch"
|
|
197
|
-
GATE_4_STATUS="FAIL"
|
|
198
|
-
exit 1
|
|
199
|
-
fi
|
|
200
|
-
GATE_4_STATUS="SKIP"
|
|
201
|
-
else
|
|
202
|
-
exit 1
|
|
203
|
-
fi
|
|
204
199
|
else
|
|
205
200
|
# Check severity levels. The reporter emits JSON.stringify(out, null, 2),
|
|
206
201
|
# i.e. `"severity": "warning"` WITH a space; tolerate any whitespace so a
|
package/adapters/gate-8.sh
CHANGED
|
@@ -59,10 +59,24 @@ fi
|
|
|
59
59
|
# revisions and reports "0 commits scanned" on a pre-commit run -- so a staged
|
|
60
60
|
# secret was never seen and Gate 8 always passed. `git --staged` scans
|
|
61
61
|
# `git diff --cached`, which is exactly the pre-commit payload (#449).
|
|
62
|
+
#
|
|
63
|
+
# #616: `--all` is a different question, and the index is the wrong place to ask
|
|
64
|
+
# it. Under `xp-gate check . --all` the user asked "is the whole tree clean", the
|
|
65
|
+
# index is usually empty, `git --staged` legitimately found nothing, and that
|
|
66
|
+
# nothing was counted as PASSED -- a secret sitting in a tracked file was
|
|
67
|
+
# invisible to the one command meant to find it. The runner already exports
|
|
68
|
+
# GATE_SCOPE (#589); this fragment just has to read it. `dir --no-git` scans the
|
|
69
|
+
# filesystem so tracked-but-unmodified files are covered.
|
|
62
70
|
GITLEAKS_REPORT="${TMPDIR:-/tmp}/gitleaks-report.json"
|
|
63
71
|
rm -f "$GITLEAKS_REPORT"
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
if [ "${GATE_SCOPE:-diff}" = "all" ]; then
|
|
73
|
+
echo " Scope: --all (scanning the working tree, not the index)"
|
|
74
|
+
GITLEAKS_OUTPUT=$($GITLEAKS_CMD dir --no-git --redact --no-banner $GITLEAKS_CONFIG --report-format=json --report-path="$GITLEAKS_REPORT" 2>&1)
|
|
75
|
+
GITLEAKS_EXIT=$?
|
|
76
|
+
else
|
|
77
|
+
GITLEAKS_OUTPUT=$($GITLEAKS_CMD git --staged --redact --no-banner $GITLEAKS_CONFIG --report-format=json --report-path="$GITLEAKS_REPORT" 2>&1)
|
|
78
|
+
GITLEAKS_EXIT=$?
|
|
79
|
+
fi
|
|
66
80
|
|
|
67
81
|
if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
68
82
|
# Decide SKIP from the REPORT ARTIFACT, not from log wording. Parsing stdout
|
|
@@ -183,7 +197,22 @@ fi
|
|
|
183
197
|
# if-condition (errexit-exempt); a non-zero hook exit lands in the else branch.
|
|
184
198
|
if command -v detect-secrets-hook >/dev/null 2>&1 && [ -f ".secrets.baseline" ]; then
|
|
185
199
|
DS_TMP="$(dirname "$(mktemp -u)")/ds-output.$$"
|
|
186
|
-
|
|
200
|
+
# #616: same scope split as the gitleaks call above. Under `--all` the index is
|
|
201
|
+
# usually empty, so piping `git diff --cached -z` into xargs fed the hook zero
|
|
202
|
+
# files and it reported success without reading anything.
|
|
203
|
+
if [ "${GATE_SCOPE:-diff}" = "all" ]; then
|
|
204
|
+
if git ls-files -z | xargs -0 detect-secrets-hook --baseline .secrets.baseline > "$DS_TMP" 2>&1; then
|
|
205
|
+
echo " ✅ detect-secrets PASSED - no new secrets beyond baseline (scope: --all)."
|
|
206
|
+
else
|
|
207
|
+
cat "$DS_TMP"
|
|
208
|
+
echo ""
|
|
209
|
+
echo " ❌ BLOCKED - detect-secrets found secrets not in .secrets.baseline."
|
|
210
|
+
echo " If these are false positives, regenerate the baseline (cross-platform):"
|
|
211
|
+
echo " detect-secrets scan --update .secrets.baseline && git add .secrets.baseline"
|
|
212
|
+
rm -f "$DS_TMP"
|
|
213
|
+
exit 1
|
|
214
|
+
fi
|
|
215
|
+
elif git diff --cached --name-only --diff-filter=ACM -z \
|
|
187
216
|
| xargs -0 detect-secrets-hook --baseline .secrets.baseline > "$DS_TMP" 2>&1; then
|
|
188
217
|
echo " ✅ detect-secrets PASSED - no new secrets beyond baseline."
|
|
189
218
|
else
|
package/adapters/gate-9.sh
CHANGED
|
@@ -32,8 +32,20 @@ if [ -z "$SEMGREP_CMD" ]; then
|
|
|
32
32
|
echo " Gate 9: SAST Security (WARN, semgrep not installed)"
|
|
33
33
|
GATE_9_STATUS="WARN"
|
|
34
34
|
else
|
|
35
|
-
# Get staged files filtered to Semgrep-supported languages
|
|
36
|
-
|
|
35
|
+
# Get staged files filtered to Semgrep-supported languages.
|
|
36
|
+
#
|
|
37
|
+
# #616: under `--all` the index is usually empty, so the staged diff yielded no
|
|
38
|
+
# files and the gate reported SKIPPED -- "no supported language files changed"
|
|
39
|
+
# -- for a run whose whole purpose was to scan the tree. Same fail-open shape
|
|
40
|
+
# #589 fixed in Gate 3, one layer down: the CLI says the tree was checked and
|
|
41
|
+
# the fragment checks a diff that does not exist. The runner exports GATE_SCOPE;
|
|
42
|
+
# read it and enumerate tracked files instead.
|
|
43
|
+
if [ "${GATE_SCOPE:-diff}" = "all" ]; then
|
|
44
|
+
echo " Scope: --all (scanning tracked files, not the index)"
|
|
45
|
+
SEMGREP_FILES=$(git ls-files 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift)$' || true)
|
|
46
|
+
else
|
|
47
|
+
SEMGREP_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift)$' || true)
|
|
48
|
+
fi
|
|
37
49
|
|
|
38
50
|
if [ -z "$SEMGREP_FILES" ]; then
|
|
39
51
|
echo " ⏭️ SKIPPED - SAST (no supported language files changed)"
|
|
@@ -337,3 +337,117 @@ describe('doctor delphi (#414 REQ-3/REQ-6)', () => {
|
|
|
337
337
|
}
|
|
338
338
|
});
|
|
339
339
|
});
|
|
340
|
+
|
|
341
|
+
// ── #684: the documented global-config fallback ──────────────────────────────
|
|
342
|
+
// SKILL.md tells users to keep ONE machine-level config at
|
|
343
|
+
// `~/.config/xp-gate/.delphi-config.json` and carry no repo-local copy. The
|
|
344
|
+
// runner resolves that fallback, so a review works; the doctor did not, and
|
|
345
|
+
// reported FAIL on a layout its own documentation recommends. Worse, the FAIL
|
|
346
|
+
// detail told the user to copy the example into the project — which manufactures
|
|
347
|
+
// exactly the drifted duplicate that #541 added a shadow warning for.
|
|
348
|
+
//
|
|
349
|
+
// The distinction this batch pins: "no config anywhere" is still a FAIL
|
|
350
|
+
// (that is a genuinely broken install); "no project config but a valid global
|
|
351
|
+
// one" is not.
|
|
352
|
+
describe('doctor delphi #684 global config fallback', () => {
|
|
353
|
+
let tmpDirs;
|
|
354
|
+
let tmpHome;
|
|
355
|
+
let originalHome;
|
|
356
|
+
|
|
357
|
+
beforeEach(() => {
|
|
358
|
+
tmpDirs = makeTempDirs(['doctor-684-home']);
|
|
359
|
+
tmpHome = tmpDirs['doctor-684-home'];
|
|
360
|
+
originalHome = process.env.HOME;
|
|
361
|
+
process.env.HOME = tmpHome;
|
|
362
|
+
fs.mkdirSync(path.join(tmpHome, '.qoder'), { recursive: true });
|
|
363
|
+
resetLibModules(vi, LIB_MODULES);
|
|
364
|
+
muteConsole(vi);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
afterEach(() => {
|
|
368
|
+
if (originalHome === undefined) delete process.env.HOME; else process.env.HOME = originalHome;
|
|
369
|
+
removeTempDirs(tmpDirs);
|
|
370
|
+
vi.restoreAllMocks();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Local copy: the suite above keeps its own writeConfig inside its describe
|
|
374
|
+
// scope, so this block cannot reach it.
|
|
375
|
+
function writeConfig(configPath) {
|
|
376
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
377
|
+
active_profile: 'default',
|
|
378
|
+
profiles: {
|
|
379
|
+
default: {
|
|
380
|
+
providers: {
|
|
381
|
+
gw: { base_url: 'https://gw.example/v1', api_key: 'k' },
|
|
382
|
+
},
|
|
383
|
+
experts: {
|
|
384
|
+
architecture: { provider: 'gw', model: 'model-a' },
|
|
385
|
+
technical: { provider: 'gw', model: 'model-b' },
|
|
386
|
+
feasibility: { provider: 'gw', model: 'model-c' },
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
}));
|
|
391
|
+
return configPath;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function writeGlobalConfig() {
|
|
395
|
+
const dir = path.join(tmpHome, '.config', 'xp-gate');
|
|
396
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
397
|
+
return writeConfig(path.join(dir, '.delphi-config.json'));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function captureLogs() {
|
|
401
|
+
const logs = [];
|
|
402
|
+
const spy = vi.spyOn(console, 'log').mockImplementation((...args) => logs.push(args.join(' ')));
|
|
403
|
+
return { logs, restore: () => spy.mockRestore() };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
it('AC-684-01: a valid global config with no project copy PASSes instead of failing', async () => {
|
|
407
|
+
const globalPath = writeGlobalConfig();
|
|
408
|
+
const project = path.join(tmpHome, 'a-project-with-no-config');
|
|
409
|
+
fs.mkdirSync(project, { recursive: true });
|
|
410
|
+
const { logs, restore } = captureLogs();
|
|
411
|
+
const { doctorDelphi } = require('../doctor');
|
|
412
|
+
await doctorDelphi({ projectDir: project, globalConfigPath: globalPath, npmLatest: null });
|
|
413
|
+
restore();
|
|
414
|
+
const cfgLine = logs.find((l) => l.includes('Delphi config'));
|
|
415
|
+
expect(cfgLine).toBeDefined();
|
|
416
|
+
expect(cfgLine).not.toMatch(/FAIL/);
|
|
417
|
+
expect(cfgLine).toMatch(/global/i);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('AC-684-02: no config in either scope still FAILs (fallback must not mask a broken install)', async () => {
|
|
421
|
+
const project = path.join(tmpHome, 'bare-project');
|
|
422
|
+
fs.mkdirSync(project, { recursive: true });
|
|
423
|
+
const { logs, restore } = captureLogs();
|
|
424
|
+
const { doctorDelphi } = require('../doctor');
|
|
425
|
+
const code = await doctorDelphi({
|
|
426
|
+
projectDir: project,
|
|
427
|
+
globalConfigPath: path.join(tmpHome, 'no-such-global.json'),
|
|
428
|
+
npmLatest: null,
|
|
429
|
+
});
|
|
430
|
+
restore();
|
|
431
|
+
expect(code).toBe(1);
|
|
432
|
+
// The rendered line carries a ✗ marker, not the literal word FAIL.
|
|
433
|
+
const cfgLine = logs.find((l) => l.includes('Delphi config (.delphi-config.json)'));
|
|
434
|
+
expect(cfgLine).toBeDefined();
|
|
435
|
+
expect(cfgLine).toContain('✗');
|
|
436
|
+
expect(cfgLine).toMatch(/not found/);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('AC-684-03: a project config still wins over the global one', async () => {
|
|
440
|
+
const globalPath = writeGlobalConfig();
|
|
441
|
+
const project = path.join(tmpHome, 'project-with-own-config');
|
|
442
|
+
fs.mkdirSync(project, { recursive: true });
|
|
443
|
+
const localPath = writeConfig(path.join(project, '.delphi-config.json'));
|
|
444
|
+
const { logs, restore } = captureLogs();
|
|
445
|
+
const { doctorDelphi } = require('../doctor');
|
|
446
|
+
await doctorDelphi({ projectDir: project, globalConfigPath: globalPath, npmLatest: null });
|
|
447
|
+
restore();
|
|
448
|
+
const cfgLine = logs.find((l) => l.includes('Delphi config'));
|
|
449
|
+
expect(cfgLine).toBeDefined();
|
|
450
|
+
expect(cfgLine).toContain(localPath);
|
|
451
|
+
expect(cfgLine).not.toMatch(/global/i);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
@@ -294,3 +294,77 @@ describe('doctor-tui installed skills enumerate the filesystem (#635)', () => {
|
|
|
294
294
|
expect(row.status).toBe('PASS');
|
|
295
295
|
});
|
|
296
296
|
});
|
|
297
|
+
|
|
298
|
+
// ── #681: equal version is not proof of equal content ────────────────────────
|
|
299
|
+
// The freshness check short-circuits on `compareSemver(...) === 0` and answers
|
|
300
|
+
// "Up to date" without ever reading the bodies. A skill that gained a whole
|
|
301
|
+
// chapter without a version bump therefore reported healthy while the installed
|
|
302
|
+
// copy was ~20k characters behind — and `update-skill` agreed with it.
|
|
303
|
+
//
|
|
304
|
+
// The version number is a claim the publisher makes; the content is the fact.
|
|
305
|
+
// When the claim says "same" the fact still has to be checked.
|
|
306
|
+
describe('doctor-tui skill freshness #681 equal version, different content', () => {
|
|
307
|
+
let tmpHome;
|
|
308
|
+
let originalHome;
|
|
309
|
+
const bundledSkillsDir = path.join(__dirname, '..', '..', 'skills');
|
|
310
|
+
|
|
311
|
+
beforeEach(() => {
|
|
312
|
+
originalHome = process.env.HOME;
|
|
313
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-fresh681-'));
|
|
314
|
+
process.env.HOME = tmpHome;
|
|
315
|
+
vi.resetModules();
|
|
316
|
+
delete require.cache[require.resolve('../doctor-tui')];
|
|
317
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
afterEach(() => {
|
|
321
|
+
process.env.HOME = originalHome;
|
|
322
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
323
|
+
vi.restoreAllMocks();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
function installSkill(name, content) {
|
|
327
|
+
const dir = path.join(tmpHome, '.config', 'opencode', 'skills', name);
|
|
328
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
329
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), content);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function runFreshness(name) {
|
|
333
|
+
const { diagnoseInstalledSkills } = require('../doctor-tui');
|
|
334
|
+
const checks = [];
|
|
335
|
+
const issues = diagnoseInstalledSkills({ installedSkills: { [name]: {} } }, checks);
|
|
336
|
+
return { issues, check: checks.find((c) => c.name === `Skill: ${name}`) };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
it('AC-681-01: same version but a missing chapter is WARN, not "Up to date"', () => {
|
|
340
|
+
const bundled = fs.readFileSync(path.join(bundledSkillsDir, 'delphi-review', 'SKILL.md'), 'utf8');
|
|
341
|
+
// Keep the frontmatter (version included), drop a large slice of the body.
|
|
342
|
+
const idx = bundled.indexOf('\n---', 4);
|
|
343
|
+
const head = bundled.slice(0, idx + 4);
|
|
344
|
+
const body = bundled.slice(idx + 4);
|
|
345
|
+
const truncated = head + body.slice(0, Math.floor(body.length / 2));
|
|
346
|
+
installSkill('delphi-review', truncated);
|
|
347
|
+
|
|
348
|
+
const { issues, check } = runFreshness('delphi-review');
|
|
349
|
+
expect(check).toBeDefined();
|
|
350
|
+
expect(check.detail).not.toMatch(/Up to date/);
|
|
351
|
+
expect(check.status).toBe('WARN');
|
|
352
|
+
expect(issues).toBe(1);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('AC-681-02: same version and same content is still "Up to date" (no false alarm)', () => {
|
|
356
|
+
const bundled = fs.readFileSync(path.join(bundledSkillsDir, 'delphi-review', 'SKILL.md'), 'utf8');
|
|
357
|
+
installSkill('delphi-review', bundled);
|
|
358
|
+
const { issues, check } = runFreshness('delphi-review');
|
|
359
|
+
expect(check.detail).toMatch(/Up to date/);
|
|
360
|
+
expect(issues).toBe(0);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('AC-681-03: line endings alone still do not count as a difference', () => {
|
|
364
|
+
const bundled = fs.readFileSync(path.join(bundledSkillsDir, 'delphi-review', 'SKILL.md'), 'utf8');
|
|
365
|
+
installSkill('delphi-review', bundled.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n'));
|
|
366
|
+
const { issues, check } = runFreshness('delphi-review');
|
|
367
|
+
expect(check.detail).toMatch(/Up to date/);
|
|
368
|
+
expect(issues).toBe(0);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
@@ -288,3 +288,80 @@ describe('update-skill', () => {
|
|
|
288
288
|
});
|
|
289
289
|
});
|
|
290
290
|
});
|
|
291
|
+
|
|
292
|
+
// ── #682: `--all` must enumerate what is on disk, not the config registry ────
|
|
293
|
+
// `xp-gate init` / `setup-global` deploy skills to disk but never write
|
|
294
|
+
// `config.installedSkills`, so `--all` iterated an always-empty object: it
|
|
295
|
+
// printed "Updating all skills..." and exited 0 having touched nothing. doctor
|
|
296
|
+
// solved the identical problem in #635 by enumerating the filesystem; this
|
|
297
|
+
// block pins the same contract for update-skill, plus the honesty requirement
|
|
298
|
+
// that a run which updates nothing must not look like a successful one.
|
|
299
|
+
describe('update-skill #682 filesystem enumeration', () => {
|
|
300
|
+
let tmpHome;
|
|
301
|
+
let originalHome;
|
|
302
|
+
let installSkillMock;
|
|
303
|
+
let logSpy;
|
|
304
|
+
let errorSpy;
|
|
305
|
+
|
|
306
|
+
beforeEach(() => {
|
|
307
|
+
originalHome = process.env.HOME;
|
|
308
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-up682-'));
|
|
309
|
+
process.env.HOME = tmpHome;
|
|
310
|
+
vi.resetModules();
|
|
311
|
+
delete require.cache[require.resolve('../update-skill')];
|
|
312
|
+
delete require.cache[require.resolve('../install-skill.js')];
|
|
313
|
+
installSkillMock = vi.fn().mockResolvedValue(0);
|
|
314
|
+
const p = require.resolve('../install-skill.js');
|
|
315
|
+
require.cache[p] = { id: p, filename: p, loaded: true, exports: { installSkill: installSkillMock } };
|
|
316
|
+
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
317
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
afterEach(() => {
|
|
321
|
+
process.env.HOME = originalHome;
|
|
322
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
323
|
+
delete require.cache[require.resolve('../install-skill.js')];
|
|
324
|
+
delete require.cache[require.resolve('../update-skill')];
|
|
325
|
+
vi.restoreAllMocks();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
function deploySkill(platform, name) {
|
|
329
|
+
const dir = path.join(tmpHome, '.config', platform, 'skills', name);
|
|
330
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
331
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), 'old content');
|
|
332
|
+
return dir;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
it('AC-682-01: --all updates skills found on disk even when config.installedSkills is absent', async () => {
|
|
336
|
+
deploySkill('opencode', 'sprint-flow');
|
|
337
|
+
deploySkill('opencode', 'delphi-review');
|
|
338
|
+
const { updateSkill } = require('../update-skill');
|
|
339
|
+
const code = await updateSkill(null, { all: true });
|
|
340
|
+
expect(code).toBe(0);
|
|
341
|
+
const names = installSkillMock.mock.calls.map((c) => c[0]);
|
|
342
|
+
expect(names).toContain('sprint-flow');
|
|
343
|
+
expect(names).toContain('delphi-review');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('AC-682-02: --all does not update the same skill twice for the same platform', async () => {
|
|
347
|
+
deploySkill('opencode', 'sprint-flow');
|
|
348
|
+
fs.mkdirSync(path.join(tmpHome, '.config', 'xp-gate'), { recursive: true });
|
|
349
|
+
fs.writeFileSync(
|
|
350
|
+
path.join(tmpHome, '.config', 'xp-gate', 'xp-gate.json'),
|
|
351
|
+
JSON.stringify({ installedSkills: { 'sprint-flow': { version: '1.0.0' } } })
|
|
352
|
+
);
|
|
353
|
+
const { updateSkill } = require('../update-skill');
|
|
354
|
+
await updateSkill(null, { all: true });
|
|
355
|
+
const sprintCalls = installSkillMock.mock.calls.filter((c) => c[0] === 'sprint-flow');
|
|
356
|
+
expect(sprintCalls.length).toBe(1);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('AC-682-03: --all with nothing installed says so instead of silently succeeding', async () => {
|
|
360
|
+
const { updateSkill } = require('../update-skill');
|
|
361
|
+
const code = await updateSkill(null, { all: true });
|
|
362
|
+
const said = [...logSpy.mock.calls, ...errorSpy.mock.calls].map((c) => c.join(' ')).join('\n');
|
|
363
|
+
expect(said).toMatch(/nothing to update|no skills installed|not installed/i);
|
|
364
|
+
expect(installSkillMock).not.toHaveBeenCalled();
|
|
365
|
+
expect(code).toBe(0);
|
|
366
|
+
});
|
|
367
|
+
});
|
package/lib/doctor-tui.js
CHANGED
|
@@ -280,8 +280,29 @@ function diagnoseSkillsInDir(skillNames, userSkillsDir, bundledSkillsDir, checks
|
|
|
280
280
|
});
|
|
281
281
|
issues++;
|
|
282
282
|
} else {
|
|
283
|
-
// cmp >= 0: installed version is same or newer than bundled
|
|
284
|
-
|
|
283
|
+
// cmp >= 0: installed version is same or newer than bundled.
|
|
284
|
+
//
|
|
285
|
+
// #681: an EQUAL version is a claim, not a fact. A skill that gained a
|
|
286
|
+
// whole chapter without a version bump reported "Up to date" while the
|
|
287
|
+
// installed copy was ~20k characters behind, and `update-skill` agreed.
|
|
288
|
+
// So when the versions tie, still compare the bodies — and do NOT
|
|
289
|
+
// prescribe `update-skill` on a difference: the installed copy may be
|
|
290
|
+
// NEWER than main (the same asymmetry the versionless branch below
|
|
291
|
+
// already respects), so the advice is "compare manually".
|
|
292
|
+
const sameBody = sameContentIgnoringLineEndings(
|
|
293
|
+
withoutVersionLine(bundledContent),
|
|
294
|
+
withoutVersionLine(userContent)
|
|
295
|
+
);
|
|
296
|
+
if (!sameBody) {
|
|
297
|
+
checks.push({
|
|
298
|
+
name: label(name),
|
|
299
|
+
status: 'WARN',
|
|
300
|
+
detail: `Content differs at the same version ${userVersion} — the version was not bumped when the skill changed; compare manually before running update-skill`,
|
|
301
|
+
});
|
|
302
|
+
issues++;
|
|
303
|
+
} else {
|
|
304
|
+
checks.push({ name: label(name), status: 'PASS', detail: 'Up to date' });
|
|
305
|
+
}
|
|
285
306
|
}
|
|
286
307
|
} else {
|
|
287
308
|
// No version in at least one file — compare content, but not byte-for-byte:
|
package/lib/doctor.js
CHANGED
|
@@ -3394,9 +3394,26 @@ async function doctorDelphi(options = {}) {
|
|
|
3394
3394
|
|
|
3395
3395
|
// 5. Config ping: parseable, three distinct callable models. A duplicate
|
|
3396
3396
|
// model here is a review that will BLOCK later — say so now.
|
|
3397
|
-
|
|
3397
|
+
//
|
|
3398
|
+
// #684: resolve the SAME way the runner does. A project-local config wins; a
|
|
3399
|
+
// machine-level one under XDG_CONFIG_HOME (or ~/.config) is the documented
|
|
3400
|
+
// fallback, and a review keeps working on it. Checking only `cwd` reported
|
|
3401
|
+
// FAIL on the layout SKILL.md recommends and told the user to copy the example
|
|
3402
|
+
// into the project — manufacturing the drifted duplicate #541 warns about.
|
|
3403
|
+
//
|
|
3404
|
+
// An explicit `configPath` is an ANSWER, not a hint: a caller that names a
|
|
3405
|
+
// file means that file, so it disables the fallback. Otherwise a test (or a
|
|
3406
|
+
// caller) pinning an absent path silently reads the developer's real global
|
|
3407
|
+
// config and reports PASS — the same vacuous-pass shape this batch is fixing.
|
|
3408
|
+
const projectCfgPath = configPath
|
|
3409
|
+
|| path.join(options.projectDir || process.cwd(), '.delphi-config.json');
|
|
3410
|
+
const usingGlobalConfig = !configPath
|
|
3411
|
+
&& !fs.existsSync(projectCfgPath)
|
|
3412
|
+
&& fs.existsSync(globalConfigPath);
|
|
3413
|
+
const cfgPath = usingGlobalConfig ? globalConfigPath : projectCfgPath;
|
|
3414
|
+
const cfgLabel = 'Delphi config (.delphi-config.json)';
|
|
3398
3415
|
if (!fs.existsSync(cfgPath)) {
|
|
3399
|
-
add(
|
|
3416
|
+
add(cfgLabel, 'FAIL',
|
|
3400
3417
|
`${cfgPath} not found (copy .delphi-config.json.example from the skill directory)`);
|
|
3401
3418
|
} else {
|
|
3402
3419
|
try {
|
|
@@ -3404,7 +3421,7 @@ async function doctorDelphi(options = {}) {
|
|
|
3404
3421
|
const profileName = raw.active_profile || 'default';
|
|
3405
3422
|
const profile = raw.profiles && raw.profiles[profileName];
|
|
3406
3423
|
if (!profile) {
|
|
3407
|
-
add(
|
|
3424
|
+
add(cfgLabel, 'FAIL', `profile "${profileName}" not found`);
|
|
3408
3425
|
} else {
|
|
3409
3426
|
const experts = profile.experts || {};
|
|
3410
3427
|
const models = roles.map((r) => experts[r] && experts[r].model);
|
|
@@ -3422,10 +3439,14 @@ async function doctorDelphi(options = {}) {
|
|
|
3422
3439
|
if (missingRoles.length > 0) problems.push(`missing experts: ${missingRoles.join(', ')}`);
|
|
3423
3440
|
if (duplicates.length > 0) problems.push(`duplicate models: ${[...new Set(duplicates)].join(', ')}`);
|
|
3424
3441
|
if (badProviders.length > 0) problems.push(`unusable provider binding: ${badProviders.join(', ')}`);
|
|
3425
|
-
add(
|
|
3442
|
+
add(cfgLabel, 'FAIL', problems.join('; '));
|
|
3426
3443
|
} else {
|
|
3427
|
-
|
|
3428
|
-
|
|
3444
|
+
// Always name the file that was actually read. Without it a user with
|
|
3445
|
+
// both scopes present cannot tell which one the review will use — the
|
|
3446
|
+
// exact ambiguity that made #684 hard to see.
|
|
3447
|
+
const scopeNote = usingGlobalConfig ? ` [global: ${cfgPath}]` : ` [project: ${cfgPath}]`;
|
|
3448
|
+
add(cfgLabel, 'PASS',
|
|
3449
|
+
`profile "${profileName}": ${models.join(' / ')}${scopeNote}`);
|
|
3429
3450
|
// #574: non-local seats are reported, not blocked. The local-only rule
|
|
3430
3451
|
// is an operator preference (the 2026-10-03 quota burn), so doctor
|
|
3431
3452
|
// surfaces the cost exposure without failing a deliberately hosted
|
|
@@ -3483,7 +3504,7 @@ async function doctorDelphi(options = {}) {
|
|
|
3483
3504
|
}
|
|
3484
3505
|
}
|
|
3485
3506
|
} catch (err) {
|
|
3486
|
-
add(
|
|
3507
|
+
add(cfgLabel, 'FAIL', `unparseable: ${err.message}`);
|
|
3487
3508
|
}
|
|
3488
3509
|
}
|
|
3489
3510
|
|
package/lib/update-skill.js
CHANGED
|
@@ -7,6 +7,57 @@ const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
|
7
7
|
|
|
8
8
|
const CONFIG_DIR = path.join(HOME, '.config', 'xp-gate');
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The skill names that are actually deployed on this machine.
|
|
12
|
+
*
|
|
13
|
+
* `xp-gate init` / `setup-global` copy skills onto disk but never write
|
|
14
|
+
* `config.installedSkills`, so the config registry is empty on a fully
|
|
15
|
+
* deployed machine. Iterating it made `update-skill --all` print
|
|
16
|
+
* "Updating all skills..." and exit 0 having touched nothing (#682) -- the
|
|
17
|
+
* same defect #635 fixed for `doctor`, with the same fix: ask the filesystem.
|
|
18
|
+
*
|
|
19
|
+
* The explicit registry is still unioned in, so a skill that is merely
|
|
20
|
+
* registered (or was installed on a platform this scan cannot see) keeps
|
|
21
|
+
* working for callers that rely on it.
|
|
22
|
+
*
|
|
23
|
+
* @returns {string[]} de-duplicated skill names, in a stable order
|
|
24
|
+
*/
|
|
25
|
+
function installedSkillNames() {
|
|
26
|
+
const names = new Set();
|
|
27
|
+
const config = getConfig();
|
|
28
|
+
for (const name of Object.keys(config.installedSkills || {})) names.add(name);
|
|
29
|
+
|
|
30
|
+
let platforms = [];
|
|
31
|
+
try {
|
|
32
|
+
platforms = Object.keys(require('./detect-deps.js').PLATFORM_PROFILES);
|
|
33
|
+
} catch {
|
|
34
|
+
// detect-deps unavailable: fall back to the registry only rather than
|
|
35
|
+
// reporting "nothing installed" for a machine that has skills on disk.
|
|
36
|
+
platforms = [];
|
|
37
|
+
}
|
|
38
|
+
const detect = require('./detect-deps.js');
|
|
39
|
+
for (const platform of platforms) {
|
|
40
|
+
let dir;
|
|
41
|
+
try {
|
|
42
|
+
dir = detect.getSkillsDirs(platform)[0];
|
|
43
|
+
} catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
49
|
+
} catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (!entry.isDirectory()) continue;
|
|
54
|
+
// A skill is `<dir>/<name>/SKILL.md` — the shape install-skill writes.
|
|
55
|
+
if (fs.existsSync(path.join(dir, entry.name, 'SKILL.md'))) names.add(entry.name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return [...names].sort();
|
|
59
|
+
}
|
|
60
|
+
|
|
10
61
|
function handleCheckMode() {
|
|
11
62
|
console.log('Checking for updates...');
|
|
12
63
|
const config = getConfig();
|
|
@@ -19,11 +70,16 @@ function handleCheckMode() {
|
|
|
19
70
|
}
|
|
20
71
|
|
|
21
72
|
async function handleAllMode(verbose) {
|
|
22
|
-
const
|
|
23
|
-
const skills = config.installedSkills || {};
|
|
73
|
+
const names = installedSkillNames();
|
|
24
74
|
console.log('Updating all skills...');
|
|
75
|
+
// #635/#682: an empty run must say so. Exiting 0 after "Updating all skills..."
|
|
76
|
+
// with no further output reads as success, and was how this stayed broken.
|
|
77
|
+
if (names.length === 0) {
|
|
78
|
+
console.log('No skills installed — nothing to update.');
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
25
81
|
let hasErrors = false;
|
|
26
|
-
for (const skillName of
|
|
82
|
+
for (const skillName of names) {
|
|
27
83
|
try {
|
|
28
84
|
await updateSingleSkill(skillName, verbose);
|
|
29
85
|
} catch (err) {
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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.35.
|
|
3
|
+
"version": "0.35.19",
|
|
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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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.35.
|
|
3
|
+
"version": "0.35.19",
|
|
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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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.35.
|
|
3
|
+
"version": "0.35.19",
|
|
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": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/principles/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# PRINCIPLES CHECKER MODULE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
|
|
@@ -205,7 +205,38 @@ function syncAdapters() {
|
|
|
205
205
|
if (gateFiles.length > 0) {
|
|
206
206
|
console.error(`[sync] gate scripts: ${gateFiles.join(', ')}`);
|
|
207
207
|
}
|
|
208
|
-
|
|
208
|
+
|
|
209
|
+
// #683: `githooks/adapters/` carries a handful of the same gate scripts,
|
|
210
|
+
// because `resolveGateScript()` falls back to `<adapterDir>/gate-<n>.sh` in a
|
|
211
|
+
// TS-less install, and #532 requires the gate-9 verdict block to stay
|
|
212
|
+
// byte-identical across those copies. Four of them had silently lagged the
|
|
213
|
+
// canonical gates (gate-3.sh by 4953 bytes, missing the #632 fix). Nothing
|
|
214
|
+
// failed only because the package-root copy is written later and wins.
|
|
215
|
+
//
|
|
216
|
+
// Re-mirror them here so the invariant is maintained, not merely observed.
|
|
217
|
+
// The names are taken FROM the adapters directory: this only refreshes copies
|
|
218
|
+
// that already exist, it never adds a gate script to `adapters/` that was not
|
|
219
|
+
// deliberately placed there.
|
|
220
|
+
const adaptersRoot = path.join(githooksDir, 'adapters');
|
|
221
|
+
let refreshed = 0;
|
|
222
|
+
let adaptersNames = [];
|
|
223
|
+
try {
|
|
224
|
+
adaptersNames = fs.readdirSync(adaptersRoot);
|
|
225
|
+
} catch {
|
|
226
|
+
adaptersNames = [];
|
|
227
|
+
}
|
|
228
|
+
for (const f of adaptersNames) {
|
|
229
|
+
const isGateScript = f.endsWith('.sh') && (f.startsWith('gate-') || f === 'sprint-gate.sh');
|
|
230
|
+
const canonical = path.join(githooksDir, f);
|
|
231
|
+
if (!isGateScript || !fs.existsSync(canonical)) continue;
|
|
232
|
+
const mirror = path.join(adaptersRoot, f);
|
|
233
|
+
const a = fs.readFileSync(canonical);
|
|
234
|
+
if (fs.existsSync(mirror) && fs.readFileSync(mirror).equals(a)) continue;
|
|
235
|
+
fs.copyFileSync(canonical, mirror);
|
|
236
|
+
console.error(`[sync] refreshed stale adapter gate mirror: ${f}`);
|
|
237
|
+
refreshed += 1;
|
|
238
|
+
}
|
|
239
|
+
return copied + gateFiles.length + refreshed;
|
|
209
240
|
}
|
|
210
241
|
|
|
211
242
|
function syncHooks() {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.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
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-11-01
|
|
4
|
+
**Commit:** effea31d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.19.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|