@boyingliu01/xp-gate 0.35.14 → 0.35.16
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/bin/xp-gate.js +67 -6
- package/gates/gate-9.ts +26 -4
- package/hooks/pre-commit +26 -9
- package/lib/__tests__/audit-cli-contract.test.ts +117 -0
- package/lib/__tests__/gate-coverage-hook-floor.test.js +94 -0
- package/lib/__tests__/gate-entry-provenance.test.js +71 -0
- package/lib/gate-coverage.js +46 -3
- package/lib/gate-runner.js +10 -2
- package/lib/mattpocock-license-verdict.json +1 -1
- 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/README.md +22 -1
- package/plugins/opencode/contract-v2.test.ts +125 -0
- package/plugins/opencode/exec-injection.test.ts +110 -0
- package/plugins/opencode/index.ts +39 -4
- package/plugins/opencode/package.json +2 -7
- 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/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/bin/xp-gate.js
CHANGED
|
@@ -617,16 +617,38 @@ function printAnomalies(a, since) {
|
|
|
617
617
|
* CI gate: exit 1 when the audit ledger carries anomalies (implausible
|
|
618
618
|
* durations or unattributed commits). This turns both telemetry bugs into a
|
|
619
619
|
* build failure instead of something a human has to notice in a chart.
|
|
620
|
+
*
|
|
621
|
+
* `--ledger <path>` grades that file instead of `<cwd>/.xp-gate/audit.jsonl`.
|
|
622
|
+
* CI needs it: the BATS suites run before this check and execute the real hook
|
|
623
|
+
* from the repo root, so the live ledger contains fixture rows that were never
|
|
624
|
+
* given a commit hash. Grading them as "unattributed commits" reports a
|
|
625
|
+
* test-isolation problem as a telemetry bug -- and the CI step's job is the
|
|
626
|
+
* committed ledger, not the scratch one the tests left behind.
|
|
620
627
|
*/
|
|
621
|
-
function handleAuditCheck(
|
|
628
|
+
function handleAuditCheck(args = []) {
|
|
622
629
|
try {
|
|
623
|
-
const
|
|
630
|
+
const ledgerIdx = args.indexOf('--ledger');
|
|
631
|
+
let repoRoot = process.cwd();
|
|
632
|
+
if (ledgerIdx !== -1 && args[ledgerIdx + 1]) {
|
|
633
|
+
// computeAnomalies() reads <repoRoot>/.xp-gate/audit.jsonl, so point it at
|
|
634
|
+
// a directory whose .xp-gate/audit.jsonl is the file we were asked about.
|
|
635
|
+
// A temp dir keeps the caller's tree untouched.
|
|
636
|
+
const ledgerPath = path.resolve(args[ledgerIdx + 1]);
|
|
637
|
+
if (!require('node:fs').existsSync(ledgerPath)) {
|
|
638
|
+
console.error(`Error: --ledger file not found: ${ledgerPath}`);
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
repoRoot = require('node:fs').mkdtempSync(path.join(require('node:os').tmpdir(), 'xpg-ledger-'));
|
|
642
|
+
require('node:fs').mkdirSync(path.join(repoRoot, '.xp-gate'), { recursive: true });
|
|
643
|
+
require('node:fs').copyFileSync(ledgerPath, path.join(repoRoot, '.xp-gate', 'audit.jsonl'));
|
|
644
|
+
}
|
|
624
645
|
// Scope to entries after the fix: historical anomalies are grandfathered
|
|
625
646
|
// (they cannot be re-measured). A future epoch/blank row fails the build.
|
|
626
|
-
const
|
|
627
|
-
|
|
647
|
+
const since = evalGateAuditModule('m.AUDIT_ANOMALY_SINCE');
|
|
648
|
+
const a = evalGateAuditModule(`m.computeAnomalies(${JSON.stringify(repoRoot)}, ${JSON.stringify(since)})`);
|
|
649
|
+
printAnomalies(a, since);
|
|
628
650
|
if (a.total > 0) {
|
|
629
|
-
console.error(`FAIL: ${a.total} audit anomal${a.total === 1 ? 'y' : 'ies'} found since ${
|
|
651
|
+
console.error(`FAIL: ${a.total} audit anomal${a.total === 1 ? 'y' : 'ies'} found since ${since} (must be 0).`);
|
|
630
652
|
process.exit(1);
|
|
631
653
|
}
|
|
632
654
|
console.log('OK: no new audit anomalies.');
|
|
@@ -665,6 +687,45 @@ function handleAuditRecord(args, auditPath) {
|
|
|
665
687
|
}
|
|
666
688
|
}
|
|
667
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Evaluate `lib/gate-audit.ts` under tsx and return whatever the little script
|
|
692
|
+
* writes to stdout, JSON-decoded.
|
|
693
|
+
*
|
|
694
|
+
* `gate-audit.ts` is TypeScript, and Node cannot `require()` a `.ts` file -- so
|
|
695
|
+
* every `xp-gate audit` subcommand used to die on
|
|
696
|
+
*
|
|
697
|
+
* Error checking audit health: require is not defined in ES module scope
|
|
698
|
+
*
|
|
699
|
+
* (that message is Node trying to interpret the .ts file as ESM). `--tail`,
|
|
700
|
+
* `--stats`, `--check` and `record` were ALL unusable, and it stayed invisible
|
|
701
|
+
* because nothing automated called them: CI's Gate 5c runs
|
|
702
|
+
* `xp-gate audit --check || npx tsx ... audit --check`, so the npx fallback is
|
|
703
|
+
* what actually ran and the broken path was never exercised.
|
|
704
|
+
*
|
|
705
|
+
* Per this package's own convention (AGENTS.md: these `.ts` lib files are
|
|
706
|
+
* "loaded via npx tsx on demand", never compiled at install time), evaluate the
|
|
707
|
+
* module in a child process under tsx. The caller supplies the expression, and
|
|
708
|
+
* all values travel as JSON so nothing is ever interpolated into code.
|
|
709
|
+
*
|
|
710
|
+
* @param {string} expr an expression evaluated with `m` bound to the module
|
|
711
|
+
* @returns {*} the JSON-decoded result
|
|
712
|
+
*/
|
|
713
|
+
function evalGateAuditModule(expr) {
|
|
714
|
+
const { execFileSync } = require('node:child_process');
|
|
715
|
+
const modulePath = path.join(__dirname, '..', 'lib', 'gate-audit.ts');
|
|
716
|
+
const tsxCli = path.join(__dirname, '..', '..', '..', 'node_modules', 'tsx', 'dist', 'cli.mjs');
|
|
717
|
+
const script = [
|
|
718
|
+
`import * as m from ${JSON.stringify(modulePath)};`,
|
|
719
|
+
`process.stdout.write(JSON.stringify(${expr}));`,
|
|
720
|
+
].join('\n');
|
|
721
|
+
const raw = execFileSync(process.execPath, [tsxCli, '--eval', script], {
|
|
722
|
+
cwd: process.cwd(),
|
|
723
|
+
encoding: 'utf8',
|
|
724
|
+
timeout: 120000,
|
|
725
|
+
});
|
|
726
|
+
return JSON.parse(raw);
|
|
727
|
+
}
|
|
728
|
+
|
|
668
729
|
function handleAudit(args) {
|
|
669
730
|
const auditPath = path.join(__dirname, '..', 'lib', 'gate-audit.ts');
|
|
670
731
|
|
|
@@ -679,7 +740,7 @@ function handleAudit(args) {
|
|
|
679
740
|
}
|
|
680
741
|
|
|
681
742
|
if (args.includes('--check')) {
|
|
682
|
-
handleAuditCheck(
|
|
743
|
+
handleAuditCheck(args);
|
|
683
744
|
return;
|
|
684
745
|
}
|
|
685
746
|
|
package/gates/gate-9.ts
CHANGED
|
@@ -10,6 +10,27 @@ import {
|
|
|
10
10
|
getChangedFiles, detectProjectLang,
|
|
11
11
|
} from './common';
|
|
12
12
|
|
|
13
|
+
// The semgrep ruleset, in ONE place (#640). It appears in the scan invocation and in
|
|
14
|
+
// two messages that tell the user which command to re-run by hand; three copies of a
|
|
15
|
+
// string that decides what gets scanned is a drift waiting to happen.
|
|
16
|
+
export const SEMGREP_RULESET = 'p/security-audit';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fixed flags every Gate 9 semgrep run carries (#640).
|
|
20
|
+
*
|
|
21
|
+
* `--metrics=off` is the important one. Semgrep's usage upload is pure overhead for
|
|
22
|
+
* a gate: it reports statistics to the registry and contributes nothing to the
|
|
23
|
+
* verdict. Measured on one file on this machine, twice each way: 11.7s / 17.2s with
|
|
24
|
+
* metrics, 4.7s / 4.9s without -- a 58-72% cut on a gate whose actual scan work is
|
|
25
|
+
* a fraction of a second (#640 measured 0.45s of work inside 30.8s of wall time).
|
|
26
|
+
* The privacy answer is the same shape: a quality gate should not phone home about
|
|
27
|
+
* the files it just read.
|
|
28
|
+
*
|
|
29
|
+
* `--disable-version-check` removes the second network round trip. Neither flag
|
|
30
|
+
* touches the JSON on stdout, which is what carries the verdict (#518).
|
|
31
|
+
*/
|
|
32
|
+
export const SEMGREP_EXTRA_ARGS = ['--disable-version-check', '--metrics=off'] as const;
|
|
33
|
+
|
|
13
34
|
export function runGate9(input: GateInput): GateOutput {
|
|
14
35
|
const startMs = Date.now();
|
|
15
36
|
const messages: string[] = [];
|
|
@@ -27,7 +48,7 @@ export function runGate9(input: GateInput): GateOutput {
|
|
|
27
48
|
if (!tool.available) {
|
|
28
49
|
messages.push(' ⚠️ WARN - semgrep not installed — SAST scanning unavailable');
|
|
29
50
|
messages.push(' Install: brew install semgrep (macOS) | pip install semgrep (Linux/Windows)');
|
|
30
|
-
messages.push(' Pre-cache rules: semgrep --config=
|
|
51
|
+
messages.push(' Pre-cache rules: semgrep --config=' + SEMGREP_RULESET);
|
|
31
52
|
messages.push(' Gate 9: SAST Security (WARN, semgrep not installed)');
|
|
32
53
|
recordAudit('gate-9', 'sast-security', 'WARN', 0, startMs);
|
|
33
54
|
return { status: 'WARN', messages, exitCode: 0 };
|
|
@@ -44,8 +65,9 @@ export function runGate9(input: GateInput): GateOutput {
|
|
|
44
65
|
return { status: 'SKIP', messages, exitCode: 0 };
|
|
45
66
|
}
|
|
46
67
|
|
|
47
|
-
// Run semgrep with JSON output
|
|
48
|
-
|
|
68
|
+
// Run semgrep with JSON output. Fixed overhead per run is the point of #640 --
|
|
69
|
+
// see SEMGREP_EXTRA_ARGS.
|
|
70
|
+
semgrepArgs.push('scan', `--config=${SEMGREP_RULESET}`, '--json', ...SEMGREP_EXTRA_ARGS, ...semgrepFiles);
|
|
49
71
|
|
|
50
72
|
const result = runTool(semgrepCmd, semgrepArgs, { cwd: input.cwd });
|
|
51
73
|
|
|
@@ -125,7 +147,7 @@ function renderSemgrepBlock(
|
|
|
125
147
|
messages.push(' ❌ BLOCKED — blocking-severity vulnerability found');
|
|
126
148
|
messages.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
127
149
|
messages.push(...findingLines);
|
|
128
|
-
messages.push(
|
|
150
|
+
messages.push(` Run 'semgrep scan --config=${SEMGREP_RULESET}' to review all findings.`);
|
|
129
151
|
recordAudit('gate-9', 'sast-security', 'FAIL', blocking, startMs);
|
|
130
152
|
return { status: 'FAIL', messages, exitCode: 1 };
|
|
131
153
|
}
|
package/hooks/pre-commit
CHANGED
|
@@ -851,18 +851,32 @@ done
|
|
|
851
851
|
has_version_change=false
|
|
852
852
|
has_changelog_change=false
|
|
853
853
|
|
|
854
|
+
# #640: a fork costs ~90ms on Windows (measured: 100 `git rev-parse` = 9456ms),
|
|
855
|
+
# and this exact query is re-run by later gates. Cache it once; consumers below
|
|
856
|
+
# read $XP_STAGED_ACM. Subprocess COUNT, not scan duration, is the bottleneck.
|
|
857
|
+
#
|
|
858
|
+
# Computed HERE, once, and outside the protected-branch test below. It used to
|
|
859
|
+
# live inside that test, which meant the variable was only ever assigned when
|
|
860
|
+
# committing to master/main/develop/trunk/mainline. On a feature branch -- where
|
|
861
|
+
# essentially all work happens, and where CI fixtures commit -- it stayed EMPTY,
|
|
862
|
+
# and every #640 consumer that asks "did this commit touch a source file?" then
|
|
863
|
+
# matched nothing:
|
|
864
|
+
# - Gate 6 skipped archlint on every feature-branch commit that DID change
|
|
865
|
+
# source ("no source files changed"), silently dropping the architecture
|
|
866
|
+
# check;
|
|
867
|
+
# - Gate 0's own hint fired as though nothing were staged.
|
|
868
|
+
# A cache that is only warm on some branches is not a cache, it is a branch-wide
|
|
869
|
+
# behaviour difference. Assign it before anything can branch.
|
|
870
|
+
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
871
|
+
XP_STAGED_ACM="$STAGED_FILES"
|
|
872
|
+
export XP_STAGED_ACM
|
|
873
|
+
|
|
854
874
|
if [ "$is_protected" = "true" ]; then
|
|
855
875
|
# Env var bypass: SKIP_VERSION_CHECK=1 skips Gate 0 entirely
|
|
856
876
|
# Useful for: SKIP_VERSION_CHECK=1 git commit -m "chore: ..."
|
|
857
877
|
if [ "${SKIP_VERSION_CHECK:-}" = "1" ]; then
|
|
858
878
|
echo "⏭️ SKIPPED - Gate 0: Version consistency (SKIP_VERSION_CHECK=1 env var)"
|
|
859
879
|
else
|
|
860
|
-
# Check if staged changes include VERSION or CHANGELOG.md
|
|
861
|
-
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
862
|
-
# #640: a fork costs ~90ms on Windows (measured: 100 `git rev-parse` = 9456ms),
|
|
863
|
-
# and this exact query is re-run by later gates. Cache it once; consumers below
|
|
864
|
-
# read $XP_STAGED_ACM. Subprocess COUNT, not scan duration, is the bottleneck.
|
|
865
|
-
XP_STAGED_ACM="$STAGED_FILES"
|
|
866
880
|
for file in $STAGED_FILES; do
|
|
867
881
|
case "$file" in
|
|
868
882
|
VERSION) has_version_change=true ;;
|
|
@@ -2450,7 +2464,7 @@ else
|
|
|
2450
2464
|
GATE_5_DELETED_TESTS=""
|
|
2451
2465
|
if declare -f classify_test_focus >/dev/null 2>&1; then
|
|
2452
2466
|
STAGED_TEST_FILES=$(printf '%s\n' "${XP_STAGED_ACM:-$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null)}" \
|
|
2453
|
-
| grep -E '\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py|go|java|kt|cs|scala|ps1)$|(^|/)(test_[^/]*\.py|[^/]*_test\.(py|go))$' || true)
|
|
2467
|
+
| filter_project_paths | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py|go|java|kt|cs|scala|ps1)$|(^|/)(test_[^/]*\.py|[^/]*_test\.(py|go))$' || true)
|
|
2454
2468
|
if [ -n "$STAGED_TEST_FILES" ]; then
|
|
2455
2469
|
while IFS= read -r _tf; do
|
|
2456
2470
|
[ -n "$_tf" ] || continue
|
|
@@ -2570,8 +2584,11 @@ else
|
|
|
2570
2584
|
# --- deleted test files: WARN ---
|
|
2571
2585
|
DELETED_LIST=$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/xp-deleted-$$")
|
|
2572
2586
|
ADDED_LIST=$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/xp-added-$$")
|
|
2573
|
-
|
|
2574
|
-
|
|
2587
|
+
# Routed through the same filter as every other staged-file scan (#649):
|
|
2588
|
+
# a deleted/added file under node_modules/ or vendor/ is not project code,
|
|
2589
|
+
# and treating it as one reports deletions that mean nothing.
|
|
2590
|
+
git diff --cached --name-only --diff-filter=D 2>/dev/null | filter_project_paths > "$DELETED_LIST" || true
|
|
2591
|
+
git diff --cached --name-only --diff-filter=A 2>/dev/null | filter_project_paths > "$ADDED_LIST" || true
|
|
2575
2592
|
GATE_5_DELETED_TESTS=$(classify_deleted_tests "$DELETED_LIST" "$ADDED_LIST" || true)
|
|
2576
2593
|
rm -f "$DELETED_LIST" "$ADDED_LIST"
|
|
2577
2594
|
if [ -n "$GATE_5_DELETED_TESTS" ]; then
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test audit CLI runs at all
|
|
3
|
+
* @intent The four `xp-gate audit` subcommands used to require() a .ts file, which
|
|
4
|
+
* Node cannot do, so every one of them died. This pins that they execute,
|
|
5
|
+
* that --check still FAILS on a real anomaly (so "it runs" is not mistaken
|
|
6
|
+
* for "it passes"), and that --ledger grades the file it was given.
|
|
7
|
+
* @covers AC-673-01, AC-673-02, AC-673-03
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const CLI = path.resolve(HERE, '..', '..', 'bin', 'xp-gate.js');
|
|
19
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..', '..', '..');
|
|
20
|
+
|
|
21
|
+
// The audit CLI builds a tsx child process on every call, so the first one pays
|
|
22
|
+
// cold-start cost; the per-test timeout has to clear that.
|
|
23
|
+
const TEST_TIMEOUT_MS = 200_000;
|
|
24
|
+
const CLI_TIMEOUT_MS = 180_000;
|
|
25
|
+
const SAMPLE_DURATION_MS = 1200;
|
|
26
|
+
|
|
27
|
+
let dir: string;
|
|
28
|
+
|
|
29
|
+
beforeAll(() => {
|
|
30
|
+
dir = mkdtempSync(join(tmpdir(), 'xpg-audit-cli-'));
|
|
31
|
+
});
|
|
32
|
+
afterAll(() => {
|
|
33
|
+
rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function runAudit(args: string[], cwd: string): ReturnType<typeof spawnSync> {
|
|
37
|
+
try {
|
|
38
|
+
return spawnSync(process.execPath, [CLI, 'audit', ...args], {
|
|
39
|
+
cwd,
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
timeout: CLI_TIMEOUT_MS,
|
|
42
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
// A spawn failure (missing binary, EACCES) must surface as itself, not as a
|
|
45
|
+
// mysterious "status was undefined" several assertions later.
|
|
46
|
+
throw new Error(`failed to run audit ${args.join(' ')}: ${(err as Error).message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ledgerWith(rows: object[]): string {
|
|
51
|
+
const file = join(dir, `ledger-${Math.random().toString(36).slice(2)}.jsonl`);
|
|
52
|
+
try {
|
|
53
|
+
writeFileSync(file, rows.map((r) => JSON.stringify(r)).join('\n') + '\n');
|
|
54
|
+
} catch (err) {
|
|
55
|
+
throw new Error(`failed to write ledger ${file}: ${(err as Error).message}`);
|
|
56
|
+
}
|
|
57
|
+
return file;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ATTRIBUTED = {
|
|
61
|
+
timestamp: '2026-12-01T00:00:00.000Z',
|
|
62
|
+
gate_id: 'gate-1',
|
|
63
|
+
gate_name: 'code-quality',
|
|
64
|
+
passed: true,
|
|
65
|
+
issues_found: 0,
|
|
66
|
+
duration_ms: SAMPLE_DURATION_MS,
|
|
67
|
+
trigger: 'commit',
|
|
68
|
+
commit_hash: 'abc123def456',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const UNATTRIBUTED = { ...ATTRIBUTED, commit_hash: '' };
|
|
72
|
+
|
|
73
|
+
describe('xp-gate audit CLI', () => {
|
|
74
|
+
it('AC-673-01: --check runs the TypeScript implementation instead of dying on require()', () => {
|
|
75
|
+
const ledger = ledgerWith([ATTRIBUTED]);
|
|
76
|
+
const r = runAudit(['--check', '--ledger', ledger], REPO_ROOT);
|
|
77
|
+
|
|
78
|
+
// The failure this guards: Node cannot require() a .ts file, so the command
|
|
79
|
+
// printed "require is not defined in ES module scope" and exited 1 no matter
|
|
80
|
+
// what the ledger held. Assert the command produced a verdict at all.
|
|
81
|
+
expect(r.stderr).not.toContain('require is not defined');
|
|
82
|
+
expect(r.stdout).toContain('Audit health');
|
|
83
|
+
expect(r.stdout).toContain('OK: no new audit anomalies.');
|
|
84
|
+
expect(r.status).toBe(0);
|
|
85
|
+
}, TEST_TIMEOUT_MS);
|
|
86
|
+
|
|
87
|
+
it('AC-673-02: an unattributed commit still fails the check', () => {
|
|
88
|
+
// Anti-vacuity for AC-673-01: proving the command runs must not be mistaken
|
|
89
|
+
// for proving it passes. A blank commit_hash is the exact defect the check
|
|
90
|
+
// exists to catch, so it has to exit non-zero.
|
|
91
|
+
const ledger = ledgerWith([ATTRIBUTED, UNATTRIBUTED]);
|
|
92
|
+
const r = runAudit(['--check', '--ledger', ledger], REPO_ROOT);
|
|
93
|
+
|
|
94
|
+
expect(r.status).not.toBe(0);
|
|
95
|
+
expect(r.stdout).toContain('missing commit : 1');
|
|
96
|
+
expect(r.stderr).toContain('FAIL: 1 audit anomaly');
|
|
97
|
+
}, TEST_TIMEOUT_MS);
|
|
98
|
+
|
|
99
|
+
it('AC-673-03: --ledger grades the named file, not the cwd ledger', () => {
|
|
100
|
+
// Run from a directory with no .xp-gate/ at all: without --ledger the check
|
|
101
|
+
// would scan 0 entries and pass, which would make the flag look harmless
|
|
102
|
+
// while testing nothing.
|
|
103
|
+
const emptyCwd = mkdtempSync(join(dir, 'cwd-'));
|
|
104
|
+
const ledger = ledgerWith([UNATTRIBUTED]);
|
|
105
|
+
const r = runAudit(['--check', '--ledger', ledger], emptyCwd);
|
|
106
|
+
|
|
107
|
+
expect(r.stdout).toContain('entries scanned : 1');
|
|
108
|
+
expect(r.status).not.toBe(0);
|
|
109
|
+
}, TEST_TIMEOUT_MS);
|
|
110
|
+
|
|
111
|
+
it('AC-673-03: --ledger rejects a missing file instead of scanning nothing', () => {
|
|
112
|
+
const r = runAudit(['--check', '--ledger', join(dir, 'does-not-exist.jsonl')], REPO_ROOT);
|
|
113
|
+
|
|
114
|
+
expect(r.status).not.toBe(0);
|
|
115
|
+
expect(r.stderr).toContain('--ledger file not found');
|
|
116
|
+
}, TEST_TIMEOUT_MS);
|
|
117
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-605
|
|
3
|
+
* @intent `gate-coverage` used to announce a COVERAGE THRESHOLD CONFLICT in this
|
|
4
|
+
* repo that did not exist: "pre-commit enforces 80% while vitest.config.ts
|
|
5
|
+
* enforces 70%".
|
|
6
|
+
*
|
|
7
|
+
* The hook was actually enforcing 70 — it RESOLVES the number at runtime
|
|
8
|
+
* from `vitest.config.ts` (#605's own fix). The reporter read the hook's
|
|
9
|
+
* SOURCE and matched the first `COVERAGE_THRESHOLD` number it could find,
|
|
10
|
+
* which is `${COVERAGE_THRESHOLD:-80}` inside a *log message*. That literal
|
|
11
|
+
* renders only when the variable is empty, which the defensive guard above
|
|
12
|
+
* it prevents.
|
|
13
|
+
*
|
|
14
|
+
* A false conflict is worse than no conflict: it sends someone to change a
|
|
15
|
+
* declaration that is already correct, and it teaches them to distrust the
|
|
16
|
+
* report. These tests pin the distinction between "a number appears in the
|
|
17
|
+
* hook file" and "the hook enforces that number".
|
|
18
|
+
* @covers AC-605-07
|
|
19
|
+
*/
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
|
|
26
|
+
const { readHookCoverageFloor } = require('../gate-coverage.js');
|
|
27
|
+
|
|
28
|
+
/** Write a throwaway repo root holding `githooks/pre-commit` with `body`. */
|
|
29
|
+
function repoWithHook(body) {
|
|
30
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'xpg-hookfloor-'));
|
|
31
|
+
fs.mkdirSync(path.join(root, 'githooks'), { recursive: true });
|
|
32
|
+
fs.writeFileSync(path.join(root, 'githooks', 'pre-commit'), body);
|
|
33
|
+
return root;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('pre-commit coverage floor 读取 (#605)', () => {
|
|
37
|
+
it('AC-605-07: a message template is not mistaken for the enforced floor', () => {
|
|
38
|
+
const root = repoWithHook([
|
|
39
|
+
'#!/usr/bin/env bash',
|
|
40
|
+
'echo "below ${COVERAGE_THRESHOLD:-80}% threshold"',
|
|
41
|
+
].join('\n'));
|
|
42
|
+
try {
|
|
43
|
+
// This is the exact line that produced the phantom 80.
|
|
44
|
+
expect(readHookCoverageFloor(root)).toBeNull();
|
|
45
|
+
} finally {
|
|
46
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('AC-605-07: the defensive guard fallback alone reports "no declared floor"', () => {
|
|
51
|
+
const root = repoWithHook([
|
|
52
|
+
'#!/usr/bin/env bash',
|
|
53
|
+
'if ! [[ "${COVERAGE_THRESHOLD:-}" =~ ^[0-9]+$ ]]; then',
|
|
54
|
+
' COVERAGE_THRESHOLD=80',
|
|
55
|
+
'fi',
|
|
56
|
+
].join('\n'));
|
|
57
|
+
try {
|
|
58
|
+
// The guard fires only when nothing was declared; when a project DOES declare
|
|
59
|
+
// a threshold the hook uses that, so reporting 80 here would invert reality.
|
|
60
|
+
expect(readHookCoverageFloor(root)).toBeNull();
|
|
61
|
+
} finally {
|
|
62
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('AC-605-07: a genuinely assigned non-default floor is reported', () => {
|
|
67
|
+
const root = repoWithHook([
|
|
68
|
+
'#!/usr/bin/env bash',
|
|
69
|
+
'COVERAGE_THRESHOLD=72',
|
|
70
|
+
'if [ "$pct" -lt "$COVERAGE_THRESHOLD" ]; then echo blocked; fi',
|
|
71
|
+
].join('\n'));
|
|
72
|
+
try {
|
|
73
|
+
expect(readHookCoverageFloor(root)).toBe(72);
|
|
74
|
+
} finally {
|
|
75
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('AC-605-07: a hook with no threshold at all reports null, not a default', () => {
|
|
80
|
+
const root = repoWithHook('#!/usr/bin/env bash\necho hello\n');
|
|
81
|
+
try {
|
|
82
|
+
expect(readHookCoverageFloor(root)).toBeNull();
|
|
83
|
+
} finally {
|
|
84
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('AC-605-07: the real repo hook does not claim a floor it does not enforce', () => {
|
|
89
|
+
const repoRoot = path.resolve(__dirname, '..', '..', '..', '..');
|
|
90
|
+
// The real hook resolves from vitest.config.ts, so it declares no literal.
|
|
91
|
+
// Asserting null here is the regression guard for the phantom-conflict report.
|
|
92
|
+
expect(readHookCoverageFloor(repoRoot)).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-672
|
|
3
|
+
* @intent 门禁的 TypeScript 实现必须**随包走**,不能随被检项目的 cwd 走。
|
|
4
|
+
*
|
|
5
|
+
* `findTsGateEntry` 曾把 `process.cwd()/src/gates/<gate>.ts` 也列进候选。
|
|
6
|
+
* 在开发者布局里这条永远轮不到(前三个候选先命中),所以在本仓跑任何
|
|
7
|
+
* 断言都是绿的——**这正是这个缺陷能活下来的原因**。必须在 npm 安装布局
|
|
8
|
+
* 下测:前三个候选都不存在,第四条的 cwd 候选才会浮上来。
|
|
9
|
+
*
|
|
10
|
+
* 后果不是"用错了文件",是**被检方决定门禁怎么判**:那个文件可以是
|
|
11
|
+
* `process.exit(0)`,于是任何一个自带该路径的项目都能让自己通过任何门禁。
|
|
12
|
+
* 一个能被被检对象改写的门禁不是门禁。
|
|
13
|
+
*
|
|
14
|
+
* 夹具把 `lib/` 复制到临时目录并**故意不放** `gates/` 兄弟目录,以此复现
|
|
15
|
+
* npm 包的布局;被检项目则种下一个 `src/gates/gate-9.ts`。
|
|
16
|
+
* @covers AC-672-01
|
|
17
|
+
*/
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { spawnSync } = require('node:child_process');
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const os = require('node:os');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
|
|
25
|
+
const LIB_DIR = path.resolve(__dirname, '..');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build an npm-install-shaped fixture and ask it to resolve gate-9.
|
|
29
|
+
*
|
|
30
|
+
* @returns {{ resolved: string, consumer: string, planted: string }} paths for assertions
|
|
31
|
+
*/
|
|
32
|
+
function resolveInNpmLayout() {
|
|
33
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'xpg-provenance-'));
|
|
34
|
+
// npm package layout: `lib/` exists, and there is NO `src/gates/` sibling three
|
|
35
|
+
// levels up (that is the developer layout, which always wins when it is present).
|
|
36
|
+
const pkg = path.join(root, 'node_modules', 'xp-gate');
|
|
37
|
+
fs.mkdirSync(path.join(pkg, 'lib'), { recursive: true });
|
|
38
|
+
fs.cpSync(LIB_DIR, path.join(pkg, 'lib'), { recursive: true });
|
|
39
|
+
|
|
40
|
+
// The party being checked: it plants its own gate implementation.
|
|
41
|
+
const consumer = path.join(root, 'consumer');
|
|
42
|
+
const planted = path.join(consumer, 'src', 'gates', 'gate-9.ts');
|
|
43
|
+
fs.mkdirSync(path.dirname(planted), { recursive: true });
|
|
44
|
+
fs.writeFileSync(planted, 'process.exit(0)\n');
|
|
45
|
+
|
|
46
|
+
const runner = path.join(pkg, 'lib', 'gate-runner.js');
|
|
47
|
+
const script = [
|
|
48
|
+
`const { findTsGateEntry } = require(${JSON.stringify(runner)});`,
|
|
49
|
+
'process.stdout.write(String(findTsGateEntry("gate-9")));',
|
|
50
|
+
].join('\n');
|
|
51
|
+
const result = spawnSync(process.execPath, ['-e', script], {
|
|
52
|
+
cwd: consumer,
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
timeout: 30000,
|
|
55
|
+
});
|
|
56
|
+
return { resolved: (result.stdout || '').trim(), consumer, planted, root };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('findTsGateEntry 不从消费项目解析实现 (#672)', () => {
|
|
60
|
+
it('AC-672-01: a project that plants src/gates/gate-9.ts cannot own the gate implementation', () => {
|
|
61
|
+
const { resolved, consumer, root } = resolveInNpmLayout();
|
|
62
|
+
try {
|
|
63
|
+
// Whatever it resolves to (an installed package normally has no gates/ dir at
|
|
64
|
+
// all, so `null` is fine), it must never be a file under the checked project:
|
|
65
|
+
// that file is data supplied by the party being checked.
|
|
66
|
+
expect(resolved.startsWith(consumer)).toBe(false);
|
|
67
|
+
} finally {
|
|
68
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
package/lib/gate-coverage.js
CHANGED
|
@@ -246,13 +246,55 @@ function readVitestThresholds(repoRoot) {
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* The floor the pre-commit hook applies, or null when the hook applies none.
|
|
251
|
+
*
|
|
252
|
+
* The hook RESOLVES its floor at runtime (`_resolve_coverage_threshold`) from the
|
|
253
|
+
* project's own declaration -- pyproject / .coveragerc / vitest.config.ts -- and
|
|
254
|
+
* only falls back to a literal in the defensive guard. So a literal found in the
|
|
255
|
+
* file is usually NOT the enforced number:
|
|
256
|
+
*
|
|
257
|
+
* - `${COVERAGE_THRESHOLD:-80}` is the default inside *log messages*; it renders
|
|
258
|
+
* only if the variable is empty, which the guard above prevents.
|
|
259
|
+
* - `COVERAGE_THRESHOLD=80` is the guard itself, reached only when nothing was
|
|
260
|
+
* declared.
|
|
261
|
+
*
|
|
262
|
+
* Matching either one as "the hook's floor" is how this report used to announce a
|
|
263
|
+
* 80-vs-70 CONFLICT that did not exist: the hook was enforcing 70 (read from
|
|
264
|
+
* vitest.config.ts) while the reporter read a message literal and compared the two.
|
|
265
|
+
* A false conflict is worse than no conflict -- it sends someone to change a
|
|
266
|
+
* declaration that was already correct (#605).
|
|
267
|
+
*
|
|
268
|
+
* Both literals are therefore treated as "no declared floor": the reporter then
|
|
269
|
+
* defers to the declaration, which is exactly what the hook does.
|
|
270
|
+
*/
|
|
250
271
|
function readHookCoverageFloor(repoRoot) {
|
|
251
272
|
try {
|
|
252
273
|
const raw = fs.readFileSync(path.join(repoRoot, 'githooks', 'pre-commit'), 'utf8');
|
|
253
274
|
if (!/COVERAGE_THRESHOLD/.test(raw)) return null;
|
|
254
|
-
|
|
255
|
-
|
|
275
|
+
|
|
276
|
+
// Strip shell message templates (`${VAR:-NN}`) before scanning: those are
|
|
277
|
+
// display defaults, not enforcement.
|
|
278
|
+
const withoutTemplates = raw.replace(/\$\{COVERAGE_THRESHOLD:-?\d+\}/g, '');
|
|
279
|
+
|
|
280
|
+
// Assignment forms only, and only outside the defensive guard. The guard is
|
|
281
|
+
// recognisable by its comment-free shape: `COVERAGE_THRESHOLD=NN` sitting
|
|
282
|
+
// directly after the `-z`/regex test that precedes it in every version.
|
|
283
|
+
const matches = [...withoutTemplates.matchAll(
|
|
284
|
+
/(?:^[ \t]*(?:export[ \t]+)?COVERAGE_THRESHOLD=(\d+)[ \t]*$)/gm,
|
|
285
|
+
)].map((m) => Number(m[1]));
|
|
286
|
+
if (matches.length === 0) return null;
|
|
287
|
+
|
|
288
|
+
// The runtime resolver writes `COVERAGE_THRESHOLD="$_rct_value"`, which the
|
|
289
|
+
// numeric pattern above deliberately does not match. Any remaining numeric
|
|
290
|
+
// assignment other than the documented 80 fallback is the real floor.
|
|
291
|
+
const nonFallback = matches.filter((value) => value !== 80);
|
|
292
|
+
if (nonFallback.length > 0) return nonFallback[nonFallback.length - 1];
|
|
293
|
+
|
|
294
|
+
// Only the guard's fallback remains. The guard exists for a variable that
|
|
295
|
+
// arrived empty; when a project declares a threshold it never fires. Report
|
|
296
|
+
// "no declared floor" so the reporter uses the declaration, as the hook does.
|
|
297
|
+
return null;
|
|
256
298
|
} catch {
|
|
257
299
|
return null;
|
|
258
300
|
}
|
|
@@ -457,5 +499,6 @@ module.exports = {
|
|
|
457
499
|
exitCodeFor,
|
|
458
500
|
readQualityStatus,
|
|
459
501
|
readBoyScoutDebt,
|
|
502
|
+
readHookCoverageFloor,
|
|
460
503
|
main,
|
|
461
504
|
};
|
package/lib/gate-runner.js
CHANGED
|
@@ -416,9 +416,17 @@ function findTsGateEntry(gateModule) {
|
|
|
416
416
|
path.resolve(__dirname, '..', 'gates', `${gateModule}.ts`),
|
|
417
417
|
// node_modules install
|
|
418
418
|
path.resolve(__dirname, '..', '..', 'src', 'gates', `${gateModule}.ts`),
|
|
419
|
-
// Repo root
|
|
420
|
-
path.resolve(process.cwd(), 'src', 'gates', `${gateModule}.ts`),
|
|
421
419
|
];
|
|
420
|
+
// #672: there used to be a fourth candidate, `process.cwd()/src/gates/<gate>.ts`.
|
|
421
|
+
// It never won in this repo -- the developer layouts above always matched first --
|
|
422
|
+
// so every test here stayed green while the candidate was live. In an npm INSTALL
|
|
423
|
+
// the first three do not exist, and it became the gate implementation: a project
|
|
424
|
+
// that happened to have `src/gates/gate-9.ts` could ship `process.exit(0)` and
|
|
425
|
+
// pass every gate, security ones included. A gate whose implementation is chosen
|
|
426
|
+
// by the party being checked is not a gate.
|
|
427
|
+
//
|
|
428
|
+
// The implementation travels with the package. When none of the three are present
|
|
429
|
+
// the gate falls back to the bash adapter (or SKIPs), which is the designed path.
|
|
422
430
|
for (const c of candidates) {
|
|
423
431
|
if (fs.existsSync(c)) return c;
|
|
424
432
|
}
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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.16",
|
|
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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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.16",
|
|
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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -62,9 +62,30 @@ Or via local path (development):
|
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
### OpenCode v2
|
|
66
|
+
|
|
67
|
+
v2 validates the plugin's default export against a schema requiring `id` plus
|
|
68
|
+
`effect` or `setup`. This package ships both keys from 0.35.15 onwards, so the
|
|
69
|
+
same published version loads under v1 and v2 — no separate build, no manifest
|
|
70
|
+
change on your side.
|
|
71
|
+
|
|
72
|
+
Register it the same way (v2 reads the same `plugin` array):
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"plugin": ["@boyingliu01/opencode-plugin"]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Do **not** register `@boyingliu01/opencode-plugin/tui`. The TUI panel was
|
|
81
|
+
abandoned in v0.13.0 and there is no `/tui` entry; v2's npm installer treats the
|
|
82
|
+
whole string as a package name, so it fails on every startup with
|
|
83
|
+
`NpmInstallFailedError`. The subpath export was removed in 0.35.15 for exactly
|
|
84
|
+
that reason.
|
|
85
|
+
|
|
65
86
|
## Requirements
|
|
66
87
|
|
|
67
|
-
- OpenCode v0.11+
|
|
88
|
+
- OpenCode v0.11+ (v2.1.4+ supported)
|
|
68
89
|
- One of:
|
|
69
90
|
- `xp-gate` CLI installed globally (`npm install -g @boyingliu01/xp-gate`) — **preferred**, or
|
|
70
91
|
- the xp-gate repo checked out locally with `src/principles/index.ts` reachable — **fallback** (the tool will shell out via `npx -y tsx`)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-639
|
|
3
|
+
* @intent OpenCode v2(2.1.4+)更换了插件模块契约。v1 的 `{ id, server }` 导出会被
|
|
4
|
+
* v2 直接拒绝:
|
|
5
|
+
*
|
|
6
|
+
* PluginModule.LoadError: Plugin must export a default definition with an id
|
|
7
|
+
* and an effect or setup function.
|
|
8
|
+
* (SchemaError(Missing key at ["default"]["effect"] / ["default"]["setup"]))
|
|
9
|
+
*
|
|
10
|
+
* 后果不是"少个功能",是整个插件不加载——`gate-check` / `gate-principles` /
|
|
11
|
+
* `gate-arch` 在 v2 会话里全部消失,而且每次启动都刷一遍 load error。
|
|
12
|
+
*
|
|
13
|
+
* 这些测试钉的是**契约形状**,不是实现细节:v2 会不会接受这个模块,
|
|
14
|
+
* 取决于 default 导出有没有 `id` 和 `effect`/`setup`。所以断言必须照着
|
|
15
|
+
* v2 的校验规则写,而不是照着我们的代码写。
|
|
16
|
+
*
|
|
17
|
+
* 同一个 issue 里还有第二个独立的坏点:`exports["./tui"]` 指向
|
|
18
|
+
* `./tui-plugin.tsx`,而仓库里**根本没有这个文件**。一个指向不存在文件的
|
|
19
|
+
* 导出会让 v2 的 npm 安装器在每次启动时失败——即使 server 侧修好了,
|
|
20
|
+
* TUI 子路径仍然会把整个安装拖挂。
|
|
21
|
+
* @covers AC-639-01, AC-639-02, AC-639-03
|
|
22
|
+
*/
|
|
23
|
+
"use strict"
|
|
24
|
+
|
|
25
|
+
import fs from "node:fs"
|
|
26
|
+
import path from "node:path"
|
|
27
|
+
import { fileURLToPath } from "node:url"
|
|
28
|
+
|
|
29
|
+
// A real function, used as a stand-in for the `setup` export when feeding the v2
|
|
30
|
+
// validator. Named rather than inline `() => {}` because eslint's
|
|
31
|
+
// no-empty-function fires on the inline form -- and a named no-op reads as
|
|
32
|
+
// "deliberately does nothing" instead of "someone forgot to fill this in".
|
|
33
|
+
const noop = (): void => undefined
|
|
34
|
+
|
|
35
|
+
const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PLUGIN_DIR, "package.json"), "utf8"))
|
|
37
|
+
|
|
38
|
+
describe("OpenCode v2 模块契约 (#639)", () => {
|
|
39
|
+
/**
|
|
40
|
+
* The v2 validator, reduced to what it actually checks. Reproduced here as a
|
|
41
|
+
* predicate so the test fails with a readable reason instead of a schema dump.
|
|
42
|
+
*/
|
|
43
|
+
function v2Accepts(moduleDefault) {
|
|
44
|
+
if (moduleDefault === null || typeof moduleDefault !== "object") {
|
|
45
|
+
return { ok: false, reason: "default export is not an object" }
|
|
46
|
+
}
|
|
47
|
+
if (typeof moduleDefault.id !== "string" || moduleDefault.id === "") {
|
|
48
|
+
return { ok: false, reason: 'missing "id"' }
|
|
49
|
+
}
|
|
50
|
+
const hasEffect = moduleDefault.effect !== undefined
|
|
51
|
+
const hasSetup = typeof moduleDefault.setup === "function"
|
|
52
|
+
if (!hasEffect && !hasSetup) {
|
|
53
|
+
return { ok: false, reason: 'missing "effect" and "setup"' }
|
|
54
|
+
}
|
|
55
|
+
return { ok: true }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
it("AC-639-01: the default export satisfies the v2 schema (id + effect|setup)", () => {
|
|
59
|
+
const source = fs.readFileSync(path.join(PLUGIN_DIR, "index.ts"), "utf8")
|
|
60
|
+
|
|
61
|
+
// Read the module shape from source rather than importing: the file is TS and
|
|
62
|
+
// this suite is CommonJS, and the thing under test is the SHAPE, which is
|
|
63
|
+
// visible in source. Importing would drag the whole plugin runtime in.
|
|
64
|
+
const moduleBlock = /const pluginModule\s*=\s*\{([\s\S]*?)\n\}/.exec(source)
|
|
65
|
+
expect(moduleBlock).not.toBeNull()
|
|
66
|
+
|
|
67
|
+
const body = moduleBlock[1]
|
|
68
|
+
const hasId = /\bid\s*:/.test(body)
|
|
69
|
+
const hasEffect = /\beffect\s*:/.test(body)
|
|
70
|
+
const hasSetup = /\bsetup\s*:/.test(body)
|
|
71
|
+
|
|
72
|
+
// Assert through the validator so a future reader sees WHY, not just which key.
|
|
73
|
+
const verdict = v2Accepts({
|
|
74
|
+
id: hasId ? "xp-gate" : undefined,
|
|
75
|
+
effect: hasEffect ? {} : undefined,
|
|
76
|
+
setup: hasSetup ? noop : undefined,
|
|
77
|
+
})
|
|
78
|
+
expect(verdict.reason).toBeUndefined()
|
|
79
|
+
expect(verdict.ok).toBe(true)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("AC-639-02: v1 compatibility is kept, not silently dropped", () => {
|
|
83
|
+
const source = fs.readFileSync(path.join(PLUGIN_DIR, "index.ts"), "utf8")
|
|
84
|
+
const moduleBlock = /const pluginModule\s*=\s*\{([\s\S]*?)\n\}/.exec(source)
|
|
85
|
+
expect(moduleBlock).not.toBeNull()
|
|
86
|
+
|
|
87
|
+
// v1 reads `server`. Dropping it would break every v1 user on upgrade -- a fix
|
|
88
|
+
// for v2 that breaks v1 is a migration, not a fix, and nobody asked for one.
|
|
89
|
+
expect(moduleBlock[1]).toMatch(/\bserver\s*:/)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('AC-639-03: every path in "exports" resolves to a real file', () => {
|
|
93
|
+
const exportsMap = pkg.exports || {}
|
|
94
|
+
const broken = []
|
|
95
|
+
|
|
96
|
+
for (const [subpath, entry] of Object.entries(exportsMap)) {
|
|
97
|
+
// Each entry is either a string or { types, import } -- flatten both shapes.
|
|
98
|
+
const targets = typeof entry === "string"
|
|
99
|
+
? [entry]
|
|
100
|
+
: Object.values(entry || {}).filter((v) => typeof v === "string")
|
|
101
|
+
|
|
102
|
+
for (const target of targets) {
|
|
103
|
+
const resolved = path.resolve(PLUGIN_DIR, target)
|
|
104
|
+
if (!fs.existsSync(resolved)) {
|
|
105
|
+
broken.push(`${subpath} -> ${target}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// A dangling export is worse than a missing one: npm accepts the manifest,
|
|
111
|
+
// node resolves the subpath to nothing, and the failure surfaces at the
|
|
112
|
+
// consumer's startup as an install error with no pointer back to this file.
|
|
113
|
+
expect(broken).toEqual([])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('AC-639-03: every path in "files" exists too', () => {
|
|
117
|
+
const listed = pkg.files || []
|
|
118
|
+
const missing = listed.filter((entry) => {
|
|
119
|
+
// Directory entries end with "/" and are allowed to match a directory.
|
|
120
|
+
const resolved = path.resolve(PLUGIN_DIR, entry.replace(/\/$/, ""))
|
|
121
|
+
return !fs.existsSync(resolved)
|
|
122
|
+
})
|
|
123
|
+
expect(missing).toEqual([])
|
|
124
|
+
})
|
|
125
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-639
|
|
3
|
+
* @intent `readCurrentSessionModel` 曾把调用方的 `sessionId` 直接插进 shell 字符串。
|
|
4
|
+
*
|
|
5
|
+
* `escapedId` 只转义了 SQL 单引号——那是 **SQL** 的关注点,不是 **shell** 的。
|
|
6
|
+
* 双引号、命令替换、反引号全部继续生效,所以一个构造过的 session id
|
|
7
|
+
* 就是在宿主上执行任意命令。
|
|
8
|
+
*
|
|
9
|
+
* 把它换成 `execFileSync(..., [dbPath, sql])` 之后,shell 根本不在调用路径上。
|
|
10
|
+
* 但这些测试钉的不是"用了哪个 API"(那是实现细节,可以被同名包装绕过),
|
|
11
|
+
* 而是**行为**:给定一个带 shell 元字符的 session id,那条注入的命令不能执行。
|
|
12
|
+
* 测试真的调用 sqlite3,所以它验证的是现实,不是代码形状。
|
|
13
|
+
* @covers AC-639-04
|
|
14
|
+
*/
|
|
15
|
+
"use strict"
|
|
16
|
+
|
|
17
|
+
import { execFileSync } from "node:child_process"
|
|
18
|
+
import fs from "node:fs"
|
|
19
|
+
import os from "node:os"
|
|
20
|
+
import path from "node:path"
|
|
21
|
+
import { fileURLToPath } from "node:url"
|
|
22
|
+
|
|
23
|
+
const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
24
|
+
const SOURCE = path.join(PLUGIN_DIR, "index.ts")
|
|
25
|
+
|
|
26
|
+
/** sqlite3 spawn deadline. Generous: CI disks are slow, and a timeout here would
|
|
27
|
+
* surface as a flaky security test rather than the signal we want. */
|
|
28
|
+
const SQLITE_TIMEOUT_MS = 20_000
|
|
29
|
+
|
|
30
|
+
/** Read a function body out of source; the module's TS runtime is not loadable here. */
|
|
31
|
+
function readFunctionBody(name: string): string {
|
|
32
|
+
// A throw here means the source shape moved. That is a real failure worth seeing,
|
|
33
|
+
// so it is re-thrown with the reason rather than swallowed into an empty string --
|
|
34
|
+
// an unreadable source must not read as "the function has no execSync".
|
|
35
|
+
let source: string
|
|
36
|
+
try {
|
|
37
|
+
source = fs.readFileSync(SOURCE, "utf8")
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new Error(`cannot read ${SOURCE}: ${(error as Error).message}`)
|
|
40
|
+
}
|
|
41
|
+
const start = source.indexOf(`function ${name}`)
|
|
42
|
+
expect(start).toBeGreaterThan(-1)
|
|
43
|
+
// Brace-count to the matching close so nested braces do not truncate the body.
|
|
44
|
+
let depth = 0
|
|
45
|
+
let index = source.indexOf("{", start)
|
|
46
|
+
const open = index
|
|
47
|
+
for (; index < source.length; index += 1) {
|
|
48
|
+
if (source[index] === "{") depth += 1
|
|
49
|
+
else if (source[index] === "}") {
|
|
50
|
+
depth -= 1
|
|
51
|
+
if (depth === 0) break
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return source.slice(open, index + 1)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Drop comments so an assertion about code cannot fail on the prose that documents
|
|
59
|
+
* the bug it forbids -- the function itself names `execSync` in its explanation.
|
|
60
|
+
*/
|
|
61
|
+
function stripComments(code: string): string {
|
|
62
|
+
return code
|
|
63
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
64
|
+
.split("\n")
|
|
65
|
+
.filter((line) => !line.trim().startsWith("//"))
|
|
66
|
+
.join("\n")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("session model reader does not go through a shell (#639)", () => {
|
|
70
|
+
it("AC-639-04: the function does not build a shell command string", () => {
|
|
71
|
+
const code = stripComments(readFunctionBody("readCurrentSessionModel"))
|
|
72
|
+
|
|
73
|
+
// execSync with an interpolated command is the exact shape that shipped the
|
|
74
|
+
// injection. If a future refactor reintroduces it, this fails before review.
|
|
75
|
+
expect(code).not.toMatch(/execSync\s*\(/)
|
|
76
|
+
expect(code).toMatch(/execFileSync\s*\(/)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("AC-639-04: a session id with shell metacharacters reaches sqlite as data", () => {
|
|
80
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "xpg-sqlite-"))
|
|
81
|
+
const dbPath = path.join(tmp, "session.db")
|
|
82
|
+
const marker = path.join(tmp, "pwned")
|
|
83
|
+
try {
|
|
84
|
+
// Real sqlite3, so the argv/shell boundary is exercised for real rather than
|
|
85
|
+
// modelled by a mock that could happily agree with a wrong implementation.
|
|
86
|
+
execFileSync("sqlite3", [dbPath, "CREATE TABLE session (id TEXT, model TEXT)"], {
|
|
87
|
+
timeout: SQLITE_TIMEOUT_MS,
|
|
88
|
+
})
|
|
89
|
+
const hostileId = `x"; touch '${marker}'; echo "`
|
|
90
|
+
const sqlDoubleQuote = "'"
|
|
91
|
+
const escapedId = hostileId.split("'").join(sqlDoubleQuote + sqlDoubleQuote)
|
|
92
|
+
execFileSync(
|
|
93
|
+
"sqlite3",
|
|
94
|
+
[dbPath, `INSERT INTO session VALUES ('${escapedId}', '{"providerID":"p","id":"m"}')`],
|
|
95
|
+
{ timeout: SQLITE_TIMEOUT_MS },
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// The payload is stored verbatim: that is the point. It reached sqlite as a
|
|
99
|
+
// data value, nothing executed, and the marker file was never created.
|
|
100
|
+
const rows = execFileSync("sqlite3", [dbPath, "SELECT count(*) FROM session"], {
|
|
101
|
+
encoding: "utf8",
|
|
102
|
+
timeout: SQLITE_TIMEOUT_MS,
|
|
103
|
+
}).trim()
|
|
104
|
+
expect(rows).toBe("1")
|
|
105
|
+
expect(fs.existsSync(marker)).toBe(false)
|
|
106
|
+
} finally {
|
|
107
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -470,11 +470,24 @@ function gateArchTool(directory: string) {
|
|
|
470
470
|
|
|
471
471
|
function readCurrentSessionModel(dbPath: string, sessionId: string): string {
|
|
472
472
|
try {
|
|
473
|
+
// execFileSync, NOT execSync (#639). `sessionId` comes from the caller's
|
|
474
|
+
// argument, and the previous form interpolated it into a shell string:
|
|
475
|
+
//
|
|
476
|
+
// `sqlite3 "${dbPath}" "SELECT model FROM session WHERE id = '${escapedId}'"`
|
|
477
|
+
//
|
|
478
|
+
// Escaping only the SQL single quote left every SHELL metacharacter live --
|
|
479
|
+
// a session id containing `"`, `$( )` or a backtick escaped the quoting and
|
|
480
|
+
// executed. Passing argv as an array removes the shell from the path
|
|
481
|
+
// entirely, so no escaping is needed or possible to get wrong.
|
|
482
|
+
//
|
|
483
|
+
// The SQL single quote is still escaped: that is a SQL concern, not a shell
|
|
484
|
+
// one, and it survives the move to argv unchanged.
|
|
473
485
|
const escapedId = sessionId.replace(/'/g, "''")
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
486
|
+
const sql = `SELECT model FROM session WHERE id = '${escapedId}'`
|
|
487
|
+
const currentRaw = execFileSync("sqlite3", [dbPath, sql], {
|
|
488
|
+
encoding: "utf8",
|
|
489
|
+
timeout: SQLITE_TIMEOUT_MS,
|
|
490
|
+
}).trim()
|
|
478
491
|
if (!currentRaw) return "unknown"
|
|
479
492
|
try {
|
|
480
493
|
const parsed = JSON.parse(currentRaw)
|
|
@@ -664,9 +677,31 @@ export const XpGatePlugin = async (input: OpenCodePluginInput) => {
|
|
|
664
677
|
}
|
|
665
678
|
}
|
|
666
679
|
|
|
680
|
+
/**
|
|
681
|
+
* The plugin module, shaped for BOTH OpenCode generations (#639).
|
|
682
|
+
*
|
|
683
|
+
* v1 read `server`. v2 validates the default export against a schema that
|
|
684
|
+
* requires `id` plus exactly one of `effect` / `setup`, and rejects anything
|
|
685
|
+
* else outright:
|
|
686
|
+
*
|
|
687
|
+
* PluginModule.LoadError: Plugin must export a default definition with an id
|
|
688
|
+
* and an effect or setup function.
|
|
689
|
+
* (cause: SchemaError(Missing key at ["default"]["effect"] / ["default"]["setup"]))
|
|
690
|
+
*
|
|
691
|
+
* The failure mode is total, not partial: the plugin does not load at all, so
|
|
692
|
+
* `gate-check` / `gate-principles` / `gate-arch` simply vanish from v2 sessions
|
|
693
|
+
* and every startup prints a load error.
|
|
694
|
+
*
|
|
695
|
+
* `setup` and the v1 `server` take the same input and return the same shape, so
|
|
696
|
+
* the same function serves both keys. Both are exported deliberately: dropping
|
|
697
|
+
* `server` would break every v1 user on upgrade, which is a migration nobody
|
|
698
|
+
* asked for. `pluginModule` is not frozen, so this stays a plain object literal
|
|
699
|
+
* that both runtimes can read.
|
|
700
|
+
*/
|
|
667
701
|
const pluginModule = {
|
|
668
702
|
id: "xp-gate",
|
|
669
703
|
server: XpGatePlugin,
|
|
704
|
+
setup: XpGatePlugin,
|
|
670
705
|
}
|
|
671
706
|
|
|
672
707
|
/**
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyingliu01/opencode-plugin",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
-
"description": "XP-Gate quality gates + AI workflow skills
|
|
6
|
+
"description": "XP-Gate quality gates + AI workflow skills for OpenCode",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/boyingliu01/xp-gate",
|
|
@@ -17,15 +17,10 @@
|
|
|
17
17
|
".": {
|
|
18
18
|
"import": "./index.ts",
|
|
19
19
|
"types": "./index.ts"
|
|
20
|
-
},
|
|
21
|
-
"./tui": {
|
|
22
|
-
"import": "./tui-plugin.tsx",
|
|
23
|
-
"types": "./tui-plugin.tsx"
|
|
24
20
|
}
|
|
25
21
|
},
|
|
26
22
|
"files": [
|
|
27
23
|
"index.ts",
|
|
28
|
-
"tui-plugin.ts",
|
|
29
24
|
"skills/",
|
|
30
25
|
"tsconfig.json",
|
|
31
26
|
"README.md"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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.16",
|
|
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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 3aa988e7
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.16.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|