@boyingliu01/xp-gate 0.20.2 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/adapters/gate-9.sh +142 -60
  2. package/adapters/python.sh +73 -6
  3. package/bin/xp-gate.js +2 -2
  4. package/gate-10.sh +107 -64
  5. package/gate-9.sh +142 -59
  6. package/hooks/gate-10.sh +107 -64
  7. package/hooks/gate-9.sh +142 -59
  8. package/hooks/lib/coverage-total.sh +72 -0
  9. package/hooks/pre-commit +128 -44
  10. package/hooks/pre-push +32 -0
  11. package/lib/__tests__/arch.test.js +403 -0
  12. package/lib/__tests__/doctor-469-hook-env.test.js +200 -0
  13. package/lib/arch.js +336 -49
  14. package/lib/doctor.js +319 -13
  15. package/lib/mattpocock-license-verdict.json +1 -1
  16. package/mock-policy/AGENTS.md +4 -4
  17. package/mutation/AGENTS.md +4 -4
  18. package/package.json +1 -1
  19. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  20. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  21. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  22. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  23. package/plugins/dsh/package.json +1 -1
  24. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  25. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  26. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  27. package/plugins/dsh/src/command.test.ts +14 -2
  28. package/plugins/dsh/src/command.ts +8 -1
  29. package/plugins/dsh/src/index.test.ts +43 -26
  30. package/plugins/dsh/src/index.ts +164 -138
  31. package/plugins/opencode/README.md +5 -2
  32. package/plugins/opencode/index.ts +215 -164
  33. package/plugins/opencode/package.json +1 -1
  34. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  35. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  36. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  37. package/plugins/qoder/plugin.json +1 -1
  38. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  39. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  40. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  41. package/principles/AGENTS.md +4 -4
  42. package/skills/delphi-review/AGENTS.md +4 -4
  43. package/skills/sprint-flow/AGENTS.md +4 -4
  44. package/skills/test-specification-alignment/AGENTS.md +4 -4
package/lib/doctor.js CHANGED
@@ -1252,7 +1252,7 @@ async function checkAdapters(checks, mode, gitDirPromise) {
1252
1252
  * D) Env + CLI tools — fully independent, run IN PARALLEL
1253
1253
  * E) TUI + Skills — independent, run IN PARALLEL
1254
1254
  */
1255
- async function diagnoseAsync() {
1255
+ async function diagnoseAsync(refreshEnv = false) {
1256
1256
  const checks = [];
1257
1257
  let issues = 0;
1258
1258
 
@@ -1295,7 +1295,12 @@ async function diagnoseAsync() {
1295
1295
 
1296
1296
  // Group C: Environment checks + CLI tools — fully parallel
1297
1297
  const envPromise = checkEnv(checks);
1298
- const cliToolsPromise = diagnoseCliToolsAsync(checks);
1298
+ // #469: the tool probes run in the hook environment; --refresh-env swaps in
1299
+ // the registry PATH so "installed but invisible" separates from "not
1300
+ // installed". The staleness check runs always: it is what makes a stale
1301
+ // terminal's green verdict honest.
1302
+ const cliToolsPromise = diagnoseCliToolsAsync(checks, { refreshEnv });
1303
+ const stalenessPromise = diagnosePathStaleness(checks);
1299
1304
 
1300
1305
  // Group D: TUI + Skills (independent)
1301
1306
  const tuiPromise = diagnoseTuiRegistration(checks);
@@ -1320,6 +1325,7 @@ async function diagnoseAsync() {
1320
1325
  adaptersPromise,
1321
1326
  envPromise,
1322
1327
  cliToolsPromise,
1328
+ stalenessPromise,
1323
1329
  tuiPromise,
1324
1330
  skillsPromise,
1325
1331
  driftPromise,
@@ -1335,23 +1341,273 @@ async function diagnoseAsync() {
1335
1341
  return { checks, issues };
1336
1342
  }
1337
1343
 
1344
+ /**
1345
+ * #469 layer 1: the hook environment is the authoritative visibility test.
1346
+ *
1347
+ * git executes hooks through `bash -c` (Git Bash on Windows, /bin/sh on
1348
+ * POSIX), so whether a gate tool is callable is decided by `command -v`
1349
+ * inside THAT shell — not by `where` in the doctor's cmd.exe, and not by the
1350
+ * doctor's own inherited PATH. Probing any other environment answers a
1351
+ * different question than the one the hook asks, which is exactly how
1352
+ * doctor came to report "healthy" on machines where every hook failed with
1353
+ * "command not found" (#469).
1354
+ *
1355
+ * @param {string} toolName
1356
+ * @param {string|null} bashPath — resolved bash (checkBash), null when absent
1357
+ * @param {NodeJS.ProcessEnv} [env] — environment override (--refresh-env)
1358
+ * @returns {Promise<string|null>} resolved path (bash's POSIX form on Windows) or null
1359
+ */
1360
+ async function probeToolInHookEnv(toolName, bashPath, env) {
1361
+ const isWindows = process.platform === 'win32';
1362
+ // Interpolation guard (walkthrough FC-04): toolName goes into a shell
1363
+ // command string verbatim. Callers pass internal constants today, but this
1364
+ // function is exported — anything outside the tool-name character set is
1365
+ // rejected fail-closed rather than executed.
1366
+ if (!/^[A-Za-z0-9._-]+$/.test(toolName)) return null;
1367
+ const shell = isWindows ? (bashPath || 'bash') : '/bin/sh';
1368
+ const inner = `command -v ${toolName}`;
1369
+ const cmd = isWindows ? `"${shell}" -c "${inner}"` : `${shell} -c '${inner}'`;
1370
+ const result = await execWithTimeout(cmd, { stdio: ['ignore', 'pipe', 'pipe'], env });
1371
+ if (!result || !result.stdout) return null;
1372
+ const first = result.stdout.split('\n')[0].trim();
1373
+ return first || null;
1374
+ }
1375
+
1376
+ /**
1377
+ * Split a Windows PATH-style list into entries. Accepts the raw string the
1378
+ * registry stores (`;`-separated) or an already-split array.
1379
+ */
1380
+ function splitPathEntries(rawPath) {
1381
+ if (Array.isArray(rawPath)) return rawPath.filter(Boolean);
1382
+ return String(rawPath || '').split(';').map((s) => s.trim()).filter(Boolean);
1383
+ }
1384
+
1385
+ /** Normalize a directory for set comparison: trailing separators, slashes, case. */
1386
+ function normalizeDirEntry(entry) {
1387
+ return String(entry || '')
1388
+ .replace(/[\\/]+$/, '')
1389
+ .replace(/\//g, '\\')
1390
+ .toLowerCase();
1391
+ }
1392
+
1393
+ /**
1394
+ * Registry PATH entries the current process cannot see (#469 layer 2).
1395
+ *
1396
+ * On Windows the registry value and the process environment block are two
1397
+ * copies of PATH; anything written to the registry after this process
1398
+ * started is invisible to it (and to every hook it spawns). Such an entry
1399
+ * means "installed, but this terminal is too old to see it" — the false-green
1400
+ * class this issue exists to kill.
1401
+ */
1402
+ function findStalePathEntries(processPath, registryEntries) {
1403
+ const visible = new Set(splitPathEntries(processPath).map(normalizeDirEntry));
1404
+ const stale = [];
1405
+ for (const entry of splitPathEntries(registryEntries)) {
1406
+ const norm = normalizeDirEntry(entry);
1407
+ if (!norm || visible.has(norm)) continue;
1408
+ if (stale.some((s) => normalizeDirEntry(s) === norm)) continue;
1409
+ stale.push(entry);
1410
+ }
1411
+ return stale;
1412
+ }
1413
+
1414
+ /**
1415
+ * The process PATH extended with the registry entries it cannot see.
1416
+ *
1417
+ * Process PATH FIRST, registry-only entries appended: the purpose is not to
1418
+ * simulate a new terminal's resolution order but to make freshly-installed
1419
+ * tools VISIBLE to the probes without disturbing the resolution of the
1420
+ * environment this doctor run is diagnosing. Registry-first was tried and is
1421
+ * wrong: it re-resolves `bash` itself (System32's WSL stub can win), and the
1422
+ * probe layer dies with the shell it meant to use. Resolution-order fidelity
1423
+ * is irrelevant here — a stale entry is already reported by the PATH
1424
+ * freshness check, and `command -v` only needs the directory to be present.
1425
+ */
1426
+ function buildRefreshedPath(processPath, registry) {
1427
+ const ordered = [
1428
+ ...splitPathEntries(processPath),
1429
+ ...findStalePathEntries(processPath, registry && registry.machine),
1430
+ ...findStalePathEntries(processPath, registry && registry.user),
1431
+ ];
1432
+ const out = [];
1433
+ const seen = new Set();
1434
+ for (const entry of ordered) {
1435
+ const norm = normalizeDirEntry(entry);
1436
+ if (!norm || seen.has(norm)) continue;
1437
+ seen.add(norm);
1438
+ out.push(entry);
1439
+ }
1440
+ return out.join(';');
1441
+ }
1442
+
1443
+ /**
1444
+ * Extract the Path value from `reg query <key> /v Path` output:
1445
+ * " Path REG_EXPAND_SZ C:\a;C:\b"
1446
+ * Returns null when the value line is absent (bad key, wrong value name).
1447
+ */
1448
+ function parseRegQueryPath(stdout) {
1449
+ const line = String(stdout || '')
1450
+ .split(/\r?\n/)
1451
+ .find((l) => /^\s+Path\s+REG_[A-Z_]+\s+\S/.test(l));
1452
+ if (!line) return null;
1453
+ return line.replace(/^\s+Path\s+REG_[A-Z_]+\s+/, '').trim();
1454
+ }
1455
+
1456
+ async function queryRegistryPathValue(key) {
1457
+ const result = await execWithTimeout(`reg query "${key}" /v Path`, {
1458
+ stdio: ['ignore', 'pipe', 'pipe'],
1459
+ });
1460
+ if (!result) return { entries: [], error: `reg query failed for ${key}` };
1461
+ const raw = parseRegQueryPath(result.stdout);
1462
+ if (!raw) return { entries: [], error: `no Path value under ${key}` };
1463
+ return { entries: splitPathEntries(raw), error: null };
1464
+ }
1465
+
1466
+ /**
1467
+ * The persistent PATH from the registry — what a NEW terminal would see.
1468
+ * Fail-closed: an unreadable hive returns `error`, and the caller WARNs
1469
+ * instead of claiming a freshness it cannot know.
1470
+ */
1471
+ async function readRegistryPath() {
1472
+ if (process.platform !== 'win32') {
1473
+ return { supported: false, user: [], machine: [], userError: null, machineError: null, error: null };
1474
+ }
1475
+ const user = await queryRegistryPathValue('HKCU\\Environment');
1476
+ const machine = await queryRegistryPathValue(
1477
+ 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment'
1478
+ );
1479
+ // `error` is the combined view for callers that need a single boolean
1480
+ // ("could I read everything?"); userError/machineError carry the halves
1481
+ // separately so staleness can use partial evidence (walkthrough FC-07).
1482
+ return {
1483
+ supported: true,
1484
+ user: user.entries,
1485
+ machine: machine.entries,
1486
+ userError: user.error,
1487
+ machineError: machine.error,
1488
+ error: user.error || machine.error || null,
1489
+ };
1490
+ }
1491
+
1492
+ /**
1493
+ * #469 layer 2: compare the process PATH against the registry.
1494
+ *
1495
+ * A registry entry the process cannot see means "installed, but this
1496
+ * terminal is too old to see it" — so a "Not found" verdict for a tool that
1497
+ * lives under such an entry is a false claim, and a "✓ All checks passed"
1498
+ * over a stale terminal is a false green. WARN, not FAIL: the machine may
1499
+ * be perfectly healthy; only this terminal is stale, and the remedy is a
1500
+ * new terminal (or `xp-gate doctor --refresh-env`).
1501
+ */
1502
+ async function diagnosePathStaleness(checks, options = {}) {
1503
+ if (process.platform !== 'win32') return 0;
1504
+ const registry = options.registry || (await readRegistryPath());
1505
+ // Per-half fail-closed (walkthrough FC-07): a readable hive is still
1506
+ // evidence — discarding it because the OTHER half failed threw away partial
1507
+ // facts. Each half is compared independently; an unreadable half is
1508
+ // reported as an unknown, never as a PASS.
1509
+ const halves = [
1510
+ { label: 'User', entries: registry.user, error: registry.userError },
1511
+ { label: 'Machine', entries: registry.machine, error: registry.machineError },
1512
+ ];
1513
+ const usable = halves.filter((h) => !h.error);
1514
+ const unreadable = halves.filter((h) => h.error);
1515
+
1516
+ let status = 'PASS';
1517
+ const parts = [];
1518
+ let stale;
1519
+ if (usable.length === 0) {
1520
+ // Fail closed: no readable hive is not evidence of freshness.
1521
+ checks.push({
1522
+ name: 'PATH freshness',
1523
+ status: 'WARN',
1524
+ detail: `Could not read the registry PATH (${halves.map((h) => `${h.label}: ${h.error}`).join('; ')}) — staleness unknown. After installing a tool, open a new terminal before trusting any "not found" verdict.`,
1525
+ });
1526
+ return 0;
1527
+ }
1528
+ stale = [
1529
+ ...usable.flatMap((h) => findStalePathEntries(process.env.PATH || '', h.entries)),
1530
+ ];
1531
+ if (unreadable.length > 0) {
1532
+ status = 'WARN';
1533
+ parts.push(`${unreadable.map((h) => `${h.label} hive unreadable (${h.error}), staleness unknown there`).join('; ')}`);
1534
+ }
1535
+ if (stale.length === 0) {
1536
+ parts.unshift(`process PATH matches the readable registry hive${usable.length === 2 ? 's' : ''} (${usable.map((h) => h.label).join(' + ')})`);
1537
+ checks.push({
1538
+ name: 'PATH freshness',
1539
+ status,
1540
+ detail: parts.join(' — '),
1541
+ });
1542
+ return 0;
1543
+ }
1544
+ status = 'WARN';
1545
+ // Which gate tools does THIS terminal lose to staleness? A tool visible in
1546
+ // the hook env only under the registry PATH is "installed but invisible" —
1547
+ // never "not installed".
1548
+ const bash = checkBash();
1549
+ const bashPath = bash.ok ? bash.path : null;
1550
+ const refreshedPath = buildRefreshedPath(process.env.PATH || '', registry);
1551
+ const invisible = [];
1552
+ if (bashPath) {
1553
+ await Promise.all(GATE_CLI_TOOLS.map(async (entry) => {
1554
+ const now = await probeToolInHookEnv(entry.tool, bashPath);
1555
+ if (now) return;
1556
+ const refreshed = await probeToolInHookEnv(entry.tool, bashPath, {
1557
+ ...process.env,
1558
+ PATH: refreshedPath,
1559
+ });
1560
+ if (refreshed) invisible.push(`${entry.tool} → ${refreshed}`);
1561
+ }));
1562
+ }
1563
+ const preview = stale.length <= 3
1564
+ ? stale.join(' ; ')
1565
+ : `${stale.slice(0, 3).join(' ; ')} ; …+${stale.length - 3} more`;
1566
+ parts.unshift(`PATH is stale — ${stale.length} registry entr${stale.length === 1 ? 'y' : 'ies'} invisible to this process (${preview})`);
1567
+ checks.push({
1568
+ name: 'PATH freshness',
1569
+ status: 'WARN',
1570
+ detail: parts.join(' — ')
1571
+ + '. A tool installed after this terminal opened needs a NEW terminal — or re-run: xp-gate doctor --refresh-env'
1572
+ + (invisible.length ? ` — installed-but-invisible gate tools: ${invisible.join(', ')}` : ''),
1573
+ });
1574
+ return 0;
1575
+ }
1576
+
1338
1577
  /**
1339
1578
  * Async CLI tools check — runs each tool detection with a 3-second timeout
1340
1579
  * using async exec. Tools are checked IN PARALLEL via Promise.allSettled.
1341
1580
  * This mirrors checkCliTool logic but uses async exec for parallelization.
1342
1581
  *
1582
+ * #469: the probe environment is the HOOK environment (`bash -c`), and
1583
+ * `options.refreshEnv` re-probes against the registry PATH — what a freshly
1584
+ * opened terminal would see — so "installed but invisible" is distinguishable
1585
+ * from "not installed".
1586
+ *
1343
1587
  * @param {Array} checks
1588
+ * @param {{refreshEnv?: boolean}} [options]
1344
1589
  * @returns {Promise<number>} issue count
1345
1590
  */
1346
- async function diagnoseCliToolsAsync(checks) {
1591
+ async function diagnoseCliToolsAsync(checks, options = {}) {
1347
1592
  let issues = 0;
1348
1593
  const platform = process.platform;
1349
1594
  const isWindows = platform === 'win32';
1350
1595
 
1596
+ const bash = checkBash();
1597
+ const bashPath = bash.ok ? bash.path : null;
1598
+
1599
+ let probeEnv;
1600
+ if (options.refreshEnv && isWindows) {
1601
+ const registry = await readRegistryPath();
1602
+ if (!registry.error) {
1603
+ probeEnv = { ...process.env, PATH: buildRefreshedPath(process.env.PATH || '', registry) };
1604
+ }
1605
+ }
1606
+
1351
1607
  // Build parallel checks for each tool
1352
1608
  const toolChecks = GATE_CLI_TOOLS.map(async (entry) => {
1353
1609
  const toolName = entry.tool;
1354
- const result = await detectCliToolAsync(toolName, isWindows);
1610
+ const result = await detectCliToolAsync(toolName, isWindows, { bashPath, env: probeEnv });
1355
1611
  return { entry, toolName, result };
1356
1612
  });
1357
1613
 
@@ -1388,13 +1644,33 @@ async function diagnoseCliToolsAsync(checks) {
1388
1644
  * Async CLI tool detection — mirrors checkCliTool but uses async exec
1389
1645
  * with a hard 3-second timeout per subprocess call.
1390
1646
  *
1647
+ * #469: when bash is available the hook environment (`bash -c 'command -v'`)
1648
+ * is AUTHORITATIVE and there is no fall-through: a tool the hook cannot see
1649
+ * must not be reported available, however visible it is to `where` or to a
1650
+ * hardcoded local path. The where/which, direct-exec and local-path tiers
1651
+ * remain only for the no-bash case, where hooks cannot run at all
1652
+ * (checkEnv already FAILs Environment: Bash there).
1653
+ *
1391
1654
  * @param {string} toolName
1392
1655
  * @param {boolean} isWindows
1656
+ * @param {{bashPath?: string|null, env?: NodeJS.ProcessEnv}} [options]
1393
1657
  * @returns {Promise<{available: boolean, version?: string, path?: string}>}
1394
1658
  */
1395
- async function detectCliToolAsync(toolName, isWindows) {
1659
+ async function detectCliToolAsync(toolName, isWindows, options = {}) {
1660
+ const bashPath = options.bashPath || null;
1661
+ const env = options.env || undefined;
1662
+
1663
+ if (bashPath) {
1664
+ const hookEnvPath = await probeToolInHookEnv(toolName, bashPath, env);
1665
+ if (hookEnvPath) {
1666
+ const version = await getVersionAsync(hookEnvPath, isWindows, { bashPath, env });
1667
+ return { available: true, path: hookEnvPath, version };
1668
+ }
1669
+ return { available: false };
1670
+ }
1671
+
1396
1672
  const shell = isWindows ? 'cmd.exe' : '/bin/sh';
1397
- const execOpts = { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], shell, timeout: EXEC_TIMEOUT_MS };
1673
+ const execOpts = { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], shell, timeout: EXEC_TIMEOUT_MS, env };
1398
1674
 
1399
1675
  // Step 1: Locate tool via which/where (async, 3s timeout)
1400
1676
  const locator = isWindows ? 'where' : 'which';
@@ -1431,12 +1707,29 @@ async function detectCliToolAsync(toolName, isWindows) {
1431
1707
 
1432
1708
  /**
1433
1709
  * Get version string from a tool path using async exec with 3s timeout.
1434
- */
1435
- async function getVersionAsync(execPath, isWindows) {
1710
+ *
1711
+ * #469: a path resolved inside Git Bash is a POSIX path (`/c/...`), which
1712
+ * cmd.exe cannot execute — so the version probe stays in the shell that
1713
+ * resolved the path instead of failing with a misleading ENOENT.
1714
+ */
1715
+ async function getVersionAsync(execPath, isWindows, options = {}) {
1716
+ const bashPath = options.bashPath || null;
1717
+ const env = options.env || undefined;
1718
+ // POSIX path test: a Git Bash-resolved path starts with '/', a Windows path
1719
+ // never does (drive letter or UNC). Enumerating prefixes (usr/, opt/, /c/)
1720
+ // was tried and leaks: /bin, /sbin, and every mount point outside the list
1721
+ // fall through to cmd.exe, which cannot run them (walkthrough FC-05).
1722
+ if (isWindows && bashPath && execPath.startsWith('/')) {
1723
+ const result = await execWithTimeout(
1724
+ `"${bashPath}" -c "${execPath} --version 2>/dev/null || ${execPath} -v 2>/dev/null"`,
1725
+ { stdio: ['ignore', 'pipe', 'pipe'], env }
1726
+ );
1727
+ return result && result.stdout ? result.stdout.split('\n')[0] : undefined;
1728
+ }
1436
1729
  const fallbackCmd = isWindows
1437
1730
  ? `"${execPath}" --version`
1438
1731
  : `"${execPath}" --version 2>/dev/null || "${execPath}" -v 2>/dev/null`;
1439
- const result = await execWithTimeout(fallbackCmd);
1732
+ const result = await execWithTimeout(fallbackCmd, { env });
1440
1733
  return result && result.stdout ? result.stdout.split('\n')[0] : undefined;
1441
1734
  }
1442
1735
 
@@ -2064,7 +2357,7 @@ function diagnoseLanguageTools() {
2064
2357
  // #488: an unrecognised flag used to be silently ignored — the npm release
2065
2358
  // that predates --sync-hooks made "doctor --sync-hooks" a silent no-op and the
2066
2359
  // user believed a sync had happened. Fail loudly instead.
2067
- const KNOWN_FLAGS = ['--fix', '--sync-hooks', '--force', '--install-tools', '--json', '--format'];
2360
+ const KNOWN_FLAGS = ['--fix', '--sync-hooks', '--force', '--install-tools', '--json', '--format', '--refresh-env'];
2068
2361
 
2069
2362
  /**
2070
2363
  * The accepted flag surface of `doctor`, as a pure function.
@@ -2097,6 +2390,10 @@ function doctorFlagContract(args) {
2097
2390
  // Installing a toolchain changes the machine, not the repository, so it is
2098
2391
  // never a side effect of --fix (#502). --fix reports the missing tools.
2099
2392
  const installToolsFlag = args.includes('--install-tools');
2393
+ // #469: re-probe CLI tools against the registry PATH (what a freshly opened
2394
+ // terminal would see) instead of this process's possibly-stale snapshot.
2395
+ // Read-only: it changes the probe environment, never the machine.
2396
+ const refreshEnv = args.includes('--refresh-env');
2100
2397
  const jsonMode = args.includes('--json') || (args.includes('--format') && formatValue === 'json');
2101
2398
  if (args.includes('--format') && !['json', 'text'].includes(formatValue)) {
2102
2399
  errors.push(`Unknown --format value: ${formatValue || '(none)'} — expected json or text`);
@@ -2118,11 +2415,11 @@ function doctorFlagContract(args) {
2118
2415
  errors.push('--install-tools is only read by --fix, and it is the only flag that installs toolchains onto this machine');
2119
2416
  errors.push('Pass --fix --install-tools, or drop --install-tools');
2120
2417
  }
2121
- return { errors, fixMode, syncHooks, force, installToolsFlag, jsonMode };
2418
+ return { errors, fixMode, syncHooks, force, installToolsFlag, jsonMode, refreshEnv };
2122
2419
  }
2123
2420
 
2124
2421
  async function doctor(args) {
2125
- const { errors, fixMode, syncHooks, force, installToolsFlag, jsonMode } = doctorFlagContract(args);
2422
+ const { errors, fixMode, syncHooks, force, installToolsFlag, jsonMode, refreshEnv } = doctorFlagContract(args);
2126
2423
  if (errors.length > 0) {
2127
2424
  for (const error of errors) console.error(error);
2128
2425
  return 1;
@@ -2206,7 +2503,7 @@ async function doctor(args) {
2206
2503
 
2207
2504
  // Global timeout wrapper — ensures doctor completes within GLOBAL_DIAGNOSIS_TIMEOUT_MS
2208
2505
  // even if network/subprocess checks stack up (fix #348: 43s on Windows).
2209
- const diagnosisPromise = diagnoseAsync();
2506
+ const diagnosisPromise = diagnoseAsync(refreshEnv);
2210
2507
  let diagnosisTimer = null;
2211
2508
  const timeoutPromise = new Promise((resolve) => {
2212
2509
  diagnosisTimer = setTimeout(() => resolve({ checks: [{ name: 'Diagnosis', status: 'WARN', detail: `Timed out after ${GLOBAL_DIAGNOSIS_TIMEOUT_MS / 1000}s — some checks incomplete` }], issues: 0, timedOut: true }), GLOBAL_DIAGNOSIS_TIMEOUT_MS);
@@ -2375,4 +2672,13 @@ module.exports = {
2375
2672
  checkSpawnCapability,
2376
2673
  execWithTimeout,
2377
2674
  EBUSY_ATTRIBUTION,
2675
+ probeToolInHookEnv,
2676
+ detectCliToolAsync,
2677
+ splitPathEntries,
2678
+ normalizeDirEntry,
2679
+ findStalePathEntries,
2680
+ buildRefreshedPath,
2681
+ parseRegQueryPath,
2682
+ readRegistryPath,
2683
+ diagnosePathStaleness,
2378
2684
  };
@@ -2,6 +2,6 @@
2
2
  "verdict": "PASS",
3
3
  "license_type": "MIT",
4
4
  "sha256": "0e7ac423bf2c6e223b7c5b156f8cf72da49d748e56a1641402c31f22ad07dbb5",
5
- "checked_at": "2026-10-12T10:19:27.219Z",
5
+ "checked_at": "2026-10-14T01:56:49.591Z",
6
6
  "source": "network"
7
7
  }
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-12
4
- **Commit:** unknown
5
- **Branch:** unknown
6
- **Version:** 0.20.2.0
3
+ **Generated:** 2026-10-13
4
+ **Commit:** 05a08fcf
5
+ **Branch:** main
6
+ **Version:** 0.21.0.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-12
4
- **Commit:** unknown
5
- **Branch:** unknown
6
- **Version:** 0.20.2.0
3
+ **Generated:** 2026-10-13
4
+ **Commit:** 05a08fcf
5
+ **Branch:** main
6
+ **Version:** 0.21.0.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.20.2",
3
+ "version": "0.22.0",
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.20.2",
3
+ "version": "0.21.0",
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-12
4
- **Commit:** unknown
5
- **Branch:** unknown
6
- **Version:** 0.20.2.0
3
+ **Generated:** 2026-10-13
4
+ **Commit:** 05a08fcf
5
+ **Branch:** main
6
+ **Version:** 0.21.0.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-12
4
- **Commit:** unknown
5
- **Branch:** unknown
6
- **Version:** 0.20.2.0
3
+ **Generated:** 2026-10-13
4
+ **Commit:** 05a08fcf
5
+ **Branch:** main
6
+ **Version:** 0.21.0.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-12
4
- **Commit:** unknown
5
- **Branch:** unknown
6
- **Version:** 0.20.2.0
3
+ **Generated:** 2026-10-13
4
+ **Commit:** 05a08fcf
5
+ **Branch:** main
6
+ **Version:** 0.21.0.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.20.2",
3
+ "version": "0.21.0",
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-12
4
- **Commit:** unknown
5
- **Branch:** unknown
6
- **Version:** 0.20.2.0
3
+ **Generated:** 2026-10-13
4
+ **Commit:** 05a08fcf
5
+ **Branch:** main
6
+ **Version:** 0.21.0.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-12
4
- **Commit:** unknown
5
- **Branch:** unknown
6
- **Version:** 0.20.2.0
3
+ **Generated:** 2026-10-13
4
+ **Commit:** 05a08fcf
5
+ **Branch:** main
6
+ **Version:** 0.21.0.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-12
4
- **Commit:** unknown
5
- **Branch:** unknown
6
- **Version:** 0.20.2.0
3
+ **Generated:** 2026-10-13
4
+ **Commit:** 05a08fcf
5
+ **Branch:** main
6
+ **Version:** 0.21.0.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -68,8 +68,20 @@ describe("buildCommand", () => {
68
68
  )
69
69
  })
70
70
 
71
- it("builds gate-arch with the default config", () => {
72
- expect(buildCommand({ subcommand: "arch" })).toContain("xp-gate arch --config 'architecture.yaml'")
71
+ // #447: the default path uses archlint's built-in rules — no --config.
72
+ // Forcing the old default (`--config architecture.yaml`) hit the pre-0.16
73
+ // schema that archlint itself rejects, so the flag is gone from the default.
74
+ it("builds gate-arch without a config by default", () => {
75
+ const cmd = buildCommand({ subcommand: "arch" })
76
+ expect(cmd).toContain("xp-gate arch")
77
+ expect(cmd).not.toContain("--config")
78
+ expect(cmd).not.toContain("--use-config")
79
+ })
80
+
81
+ it("forwards an explicit config via the opt-in --use-config flag", () => {
82
+ expect(buildCommand({ subcommand: "arch", config: "custom.yaml" })).toContain(
83
+ "xp-gate arch --use-config 'custom.yaml'",
84
+ )
73
85
  })
74
86
 
75
87
  /**
@@ -62,7 +62,14 @@ function buildInner(options: BuildCommandOptions, dialect: ShellDialect): string
62
62
  if (target !== undefined) tokens.push(q(target))
63
63
  return tokens.join(" ")
64
64
  }
65
- return ["xp-gate", "arch", "--config", q(config ?? "architecture.yaml")].join(" ")
65
+ // subcommand === "arch" — the only remaining member of the union.
66
+ // #447: the default path runs the pinned local @archlinter/cli with its
67
+ // built-in rules — no config needed. Forcing `--config architecture.yaml`
68
+ // (the old default) hit the pre-0.16 schema that archlint itself rejects.
69
+ // Only forward an explicitly provided config, via the opt-in --use-config.
70
+ const tokens = ["xp-gate", "arch"]
71
+ if (config !== undefined) tokens.push("--use-config", q(config))
72
+ return tokens.join(" ")
66
73
  }
67
74
 
68
75
  /**