@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.
Files changed (35) hide show
  1. package/adapters/gate-3.sh +94 -9
  2. package/adapters/gate-4.sh +27 -32
  3. package/adapters/gate-8.sh +32 -3
  4. package/adapters/gate-9.sh +14 -2
  5. package/lib/__tests__/doctor-delphi.test.js +114 -0
  6. package/lib/__tests__/doctor-tui-skill-freshness.test.js +74 -0
  7. package/lib/__tests__/update-skill.test.js +77 -0
  8. package/lib/doctor-tui.js +23 -2
  9. package/lib/doctor.js +28 -7
  10. package/lib/mattpocock-license-verdict.json +1 -1
  11. package/lib/update-skill.js +59 -3
  12. package/mock-policy/AGENTS.md +3 -3
  13. package/mutation/AGENTS.md +3 -3
  14. package/package.json +1 -1
  15. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  16. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  17. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  18. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  19. package/plugins/dsh/package.json +1 -1
  20. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  21. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  22. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  23. package/plugins/opencode/package.json +1 -1
  24. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  25. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  26. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  27. package/plugins/qoder/plugin.json +1 -1
  28. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  29. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  30. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  31. package/principles/AGENTS.md +3 -3
  32. package/scripts/sync-package-content.js +32 -1
  33. package/skills/delphi-review/AGENTS.md +3 -3
  34. package/skills/sprint-flow/AGENTS.md +3 -3
  35. package/skills/test-specification-alignment/AGENTS.md +3 -3
@@ -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
- if (!e || !e.file || !e.name) continue;
176
- known.set(norm(e.file) + "::" + e.name, Number(e.ccn) || 0);
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
- echo "⚠️ WARN - lizard produced no parsable results; complexity not verified"
535
- GATE_3_STATUS="WARN"
536
- # Consumed by the sourcing parent (pre-commit report JSON), not here.
537
- # shellcheck disable=SC2034
538
- GATE_3_REASON="lizard_output_unparsable"
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 ""
@@ -46,7 +46,14 @@ java_coverage_note() {
46
46
  esac
47
47
  done
48
48
  [ -n "$_has_java" ] || return 0
49
- echo "ℹ️ Java rule coverage: $JAVA_COVERAGE_EFFECTIVE/$JAVA_COVERAGE_TOTAL rules effective (inert on Java: $JAVA_COVERAGE_INERT -- see docs/java-principles-coverage.md)"
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
- # Fail-closed (#661), same rule as Gate 8 (#499): a gate that cannot run
169
- # must not report success.
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
- # This branch used to settle on SKIP, which the caller reads as
172
- # non-blocking -- so a checker that crashed on startup
173
- # (`Cannot find module 'C:\c\Users\...'`, the POSIX-path-to-native-node
174
- # bug) still printed "PASSED - Principles checker (no errors found)".
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
- # SKIP stays reserved for "language not applicable / tool not
180
- # installed", which is not what this is: the checker exists and failed.
181
- echo ""
182
- echo "❌ BLOCKED - Principles checker failed to execute (exit $PRINCIPLES_EXIT)"
183
- echo "This is a tool fault, NOT a clean result. A gate that cannot run must not pass."
184
- echo " Fix the checker, or set PRINCIPLES_SKIP_TOOL_FAULT=1 to downgrade to a warning."
185
- echo " (downgrade is audit-logged and never allowed on main/master)"
186
- GATE_4_STATUS="FAIL"
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
@@ -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
- GITLEAKS_OUTPUT=$($GITLEAKS_CMD git --staged --redact --no-banner $GITLEAKS_CONFIG --report-format=json --report-path="$GITLEAKS_REPORT" 2>&1)
65
- GITLEAKS_EXIT=$?
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
- if git diff --cached --name-only --diff-filter=ACM -z \
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
@@ -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
- 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)
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
- checks.push({ name: label(name), status: 'PASS', detail: 'Up to date' });
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
- const cfgPath = configPath || path.join(process.cwd(), '.delphi-config.json');
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('Delphi config (.delphi-config.json)', 'FAIL',
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('Delphi config (.delphi-config.json)', 'FAIL', `profile "${profileName}" not found`);
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('Delphi config (.delphi-config.json)', 'FAIL', problems.join('; '));
3442
+ add(cfgLabel, 'FAIL', problems.join('; '));
3426
3443
  } else {
3427
- add('Delphi config (.delphi-config.json)', 'PASS',
3428
- `profile "${profileName}": ${models.join(' / ')}`);
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('Delphi config (.delphi-config.json)', 'FAIL', `unparseable: ${err.message}`);
3507
+ add(cfgLabel, 'FAIL', `unparseable: ${err.message}`);
3487
3508
  }
3488
3509
  }
3489
3510
 
@@ -2,6 +2,6 @@
2
2
  "verdict": "PASS",
3
3
  "license_type": "MIT",
4
4
  "sha256": "0e7ac423bf2c6e223b7c5b156f8cf72da49d748e56a1641402c31f22ad07dbb5",
5
- "checked_at": "2026-10-31T16:53:19.066Z",
5
+ "checked_at": "2026-11-01T15:38:49.220Z",
6
6
  "source": "network"
7
7
  }
@@ -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 config = getConfig();
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 Object.keys(skills)) {
82
+ for (const skillName of names) {
27
83
  try {
28
84
  await updateSingleSkill(skillName, verbose);
29
85
  } catch (err) {
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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": "@boyingliu01/xp-gate",
3
- "version": "0.35.18",
3
+ "version": "0.35.19",
4
4
  "description": "AI-driven development workflow: 6 quality gates + Delphi review + Sprint Flow",
5
5
  "bin": {
6
6
  "xp-gate": "bin/xp-gate.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.35.18",
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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": "@boyingliu01/dsh-plugin-xp-gate",
3
- "version": "0.35.18",
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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": "@boyingliu01/opencode-plugin",
3
- "version": "0.35.18",
3
+ "version": "0.35.19",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "XP-Gate quality gates + AI workflow skills for OpenCode",
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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.18",
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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
  # PRINCIPLES CHECKER MODULE
2
2
 
3
- **Generated:** 2026-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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
- return copied + gateFiles.length;
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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-10-31
4
- **Commit:** 97e3c2ec
3
+ **Generated:** 2026-11-01
4
+ **Commit:** effea31d
5
5
  **Branch:** main
6
- **Version:** 0.35.18.0
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.