@boyingliu01/xp-gate 0.31.3 → 0.32.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.
- package/adapters/java.sh +256 -4
- package/adapters/powershell.sh +45 -6
- package/bin/xp-gate.js +8 -2
- package/build-integrity/__tests__/gate-10.test.ts +88 -1
- package/build-integrity/gate-10.ts +51 -16
- package/dsh.bundle.patch.yml +15 -0
- package/gate-13-compiler-strictness.sh +1031 -0
- package/gate-3.sh +48 -13
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +48 -13
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +9 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +269 -13
- package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
- package/lib/__tests__/gate-runner.test.js +29 -2
- package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
- package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
- package/lib/__tests__/shared-utils.test.js +21 -0
- package/lib/check.js +6 -2
- package/lib/detect-deps.js +42 -10
- package/lib/doctor-tui.js +111 -21
- package/lib/gate-runner.js +111 -37
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +48 -6
- package/lib/shared-utils.js +8 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/gate-m3.ts +57 -23
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
- package/mutation/__tests__/test-filter-parity.test.ts +5 -2
- package/mutation/gate-m.ts +30 -24
- package/mutation/runners/mutmut-runner.ts +80 -30
- package/mutation/runners/pitest-runner.ts +60 -42
- package/package.json +14 -2
- 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/lib/command.d.ts +38 -0
- package/plugins/dsh/lib/command.d.ts.map +1 -0
- package/plugins/dsh/lib/command.js +87 -0
- package/plugins/dsh/lib/credentials.d.ts +39 -0
- package/plugins/dsh/lib/credentials.d.ts.map +1 -0
- package/plugins/dsh/lib/credentials.js +57 -0
- package/plugins/dsh/lib/delphi-config.d.ts +129 -0
- package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-config.js +369 -0
- package/plugins/dsh/lib/delphi-run.d.ts +24 -0
- package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-run.js +113 -0
- package/plugins/dsh/lib/delphi.d.ts +94 -0
- package/plugins/dsh/lib/delphi.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi.js +207 -0
- package/plugins/dsh/lib/dialect.d.ts +24 -0
- package/plugins/dsh/lib/dialect.d.ts.map +1 -0
- package/plugins/dsh/lib/dialect.js +36 -0
- package/plugins/dsh/lib/evidence.d.ts +70 -0
- package/plugins/dsh/lib/evidence.d.ts.map +1 -0
- package/plugins/dsh/lib/evidence.js +92 -0
- package/plugins/dsh/lib/gate-runner.d.ts +49 -0
- package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
- package/plugins/dsh/lib/gate-runner.js +95 -0
- package/plugins/dsh/lib/git-context.d.ts +26 -0
- package/plugins/dsh/lib/git-context.d.ts.map +1 -0
- package/plugins/dsh/lib/git-context.js +30 -0
- package/plugins/dsh/lib/hooks.d.ts +42 -0
- package/plugins/dsh/lib/hooks.d.ts.map +1 -0
- package/plugins/dsh/lib/hooks.js +113 -0
- package/plugins/dsh/lib/index.d.ts +20 -0
- package/plugins/dsh/lib/index.d.ts.map +1 -0
- package/plugins/dsh/lib/index.js +430 -0
- package/plugins/dsh/lib/skill-provider.d.ts +15 -0
- package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
- package/plugins/dsh/lib/skill-provider.js +205 -0
- package/plugins/dsh/lib/sprint.d.ts +60 -0
- package/plugins/dsh/lib/sprint.d.ts.map +1 -0
- package/plugins/dsh/lib/sprint.js +92 -0
- package/plugins/dsh/package.json +14 -8
- package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
- 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/dsh/src/command-single-form.test.ts +67 -0
- package/plugins/dsh/src/command.test.ts +21 -17
- package/plugins/dsh/src/command.ts +19 -21
- package/plugins/dsh/src/compat.test.ts +200 -0
- package/plugins/dsh/src/contract.test.ts +7 -1
- package/plugins/dsh/src/credentials.test.ts +93 -0
- package/plugins/dsh/src/credentials.ts +71 -0
- package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
- package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
- package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
- package/plugins/dsh/src/delphi-config.ts +146 -53
- package/plugins/dsh/src/delphi-run.ts +27 -4
- package/plugins/dsh/src/delphi.ts +133 -65
- package/plugins/dsh/src/evidence.test.ts +74 -0
- package/plugins/dsh/src/evidence.ts +141 -0
- package/plugins/dsh/src/gate-runner.test.ts +14 -6
- package/plugins/dsh/src/gate-runner.ts +68 -5
- package/plugins/dsh/src/git-context.test.ts +39 -0
- package/plugins/dsh/src/git-context.ts +38 -0
- package/plugins/dsh/src/hooks.test.ts +87 -0
- package/plugins/dsh/src/hooks.ts +126 -0
- package/plugins/dsh/src/index.test.ts +63 -6
- package/plugins/dsh/src/index.ts +340 -13
- package/plugins/dsh/src/skill-provider.test.ts +194 -0
- package/plugins/dsh/src/skill-provider.ts +231 -0
- package/plugins/dsh/src/sprint.test.ts +64 -0
- package/plugins/dsh/src/sprint.ts +122 -0
- package/plugins/dsh/src/tri-state.test.ts +123 -0
- package/plugins/opencode/index.ts +192 -58
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/analyze-read-failure.test.ts +70 -0
- package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
- package/principles/__tests__/baseline-prune.test.ts +61 -0
- package/principles/__tests__/cli-files-separators.test.ts +69 -0
- package/principles/__tests__/config-merge-warnings.test.ts +148 -0
- package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
- package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
- package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
- package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
- package/principles/adapters/cpp.ts +102 -50
- package/principles/adapters/java.ts +22 -3
- package/principles/adapters/typescript.ts +548 -178
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- package/principles/baseline.ts +58 -70
- package/principles/config.ts +56 -35
- package/principles/index.ts +122 -40
- package/principles/rules/clean-code/deep-nesting.ts +36 -22
- package/principles/rules/clean-code/unused-imports.ts +30 -19
- package/principles/rules/solid/dip.ts +30 -20
- package/principles/rules/solid/lsp.ts +17 -2
- package/principles/rules/solid/ocp.ts +33 -23
- package/principles/rules/solid/srp.ts +65 -23
- package/scripts/delphi-external-review.cjs +25 -2
- package/scripts/sync-package-content.js +28 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-DSH-011 tri-state gate rendering
|
|
3
|
+
* @intent Verify renderGateResult classifies every shell outcome into exactly one of
|
|
4
|
+
* PASS / GATE FAILED / INDETERMINATE, following the documented decision order,
|
|
5
|
+
* and that no ambiguous outcome can be read as a pass.
|
|
6
|
+
* @covers AC-DSH-011-01 AC-DSH-011-02 AC-DSH-011-03 AC-DSH-011-04 AC-DSH-011-05 AC-DSH-011-06
|
|
7
|
+
*
|
|
8
|
+
* Why the ordering matters: a timed-out process can still report exit 0 from its
|
|
9
|
+
* last state before the kill, so checking `exit === 0` first would turn a timeout
|
|
10
|
+
* into a green check. The same mistake in Gate 8 and Gate 10 is exactly what this
|
|
11
|
+
* project kept re-fixing — a gate that could not run must never report success.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from "vitest"
|
|
15
|
+
import type { ShellRunResult } from "@deepseek-ai/dsh-shell"
|
|
16
|
+
import { renderGateResult } from "./gate-runner.js"
|
|
17
|
+
|
|
18
|
+
/** The shell's convention for "command not found" — no verdict, no reason. */
|
|
19
|
+
const COMMAND_NOT_FOUND = 127;
|
|
20
|
+
|
|
21
|
+
/** A non-zero count a real gate prints when it finds violations. */
|
|
22
|
+
const VIOLATIONS_FOUND = 3;
|
|
23
|
+
|
|
24
|
+
function makeResult(over: Partial<ShellRunResult> = {}): ShellRunResult {
|
|
25
|
+
return {
|
|
26
|
+
exitCode: 0,
|
|
27
|
+
stdout: { text: "", truncated: false },
|
|
28
|
+
stderr: { text: "", truncated: false },
|
|
29
|
+
timedOut: false,
|
|
30
|
+
timeoutMs: 1000,
|
|
31
|
+
signal: null,
|
|
32
|
+
aborted: false,
|
|
33
|
+
...over,
|
|
34
|
+
} as ShellRunResult
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("renderGateResult tri-state", () => {
|
|
38
|
+
it("reports PASS on exit 0 with non-empty stdout", () => {
|
|
39
|
+
const out = renderGateResult(makeResult({ stdout: { text: "all gates ok", truncated: false } }))
|
|
40
|
+
expect(out).toContain("[XP-Gate] PASS")
|
|
41
|
+
expect(out).toContain("all gates ok")
|
|
42
|
+
expect(out).not.toContain("INDETERMINATE")
|
|
43
|
+
expect(out).not.toContain("GATE FAILED")
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("reports INDETERMINATE on exit 0 with empty stdout", () => {
|
|
47
|
+
// The command succeeded but said nothing — that is not a pass.
|
|
48
|
+
const out = renderGateResult(makeResult({ stdout: { text: " \n", truncated: false } }))
|
|
49
|
+
expect(out).toContain("INDETERMINATE")
|
|
50
|
+
expect(out).toContain("NOT a pass")
|
|
51
|
+
expect(out).not.toContain("[XP-Gate] PASS")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("reports GATE FAILED on nonzero exit with output", () => {
|
|
55
|
+
const out = renderGateResult(
|
|
56
|
+
makeResult({
|
|
57
|
+
exitCode: VIOLATIONS_FOUND,
|
|
58
|
+
stdout: { text: `${VIOLATIONS_FOUND} violations found`, truncated: false },
|
|
59
|
+
}),
|
|
60
|
+
)
|
|
61
|
+
expect(out).toContain("GATE FAILED")
|
|
62
|
+
expect(out).toContain(`exit code: ${VIOLATIONS_FOUND}`)
|
|
63
|
+
expect(out).toContain(`${VIOLATIONS_FOUND} violations found`)
|
|
64
|
+
expect(out).not.toContain("INDETERMINATE")
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("reports GATE FAILED on nonzero exit with only stderr", () => {
|
|
68
|
+
const out = renderGateResult(
|
|
69
|
+
makeResult({
|
|
70
|
+
exitCode: 1,
|
|
71
|
+
stderr: { text: "adapter crashed", truncated: false },
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
expect(out).toContain("GATE FAILED")
|
|
75
|
+
expect(out).toContain("adapter crashed")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("reports INDETERMINATE on nonzero exit with no output at all", () => {
|
|
79
|
+
// Typical command-not-found: neither a verdict nor a reason.
|
|
80
|
+
const out = renderGateResult(makeResult({ exitCode: COMMAND_NOT_FOUND }))
|
|
81
|
+
expect(out).toContain("INDETERMINATE")
|
|
82
|
+
expect(out).not.toContain("GATE FAILED")
|
|
83
|
+
expect(out).not.toContain("[XP-Gate] PASS")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("reports INDETERMINATE on timeout even when exit code is 0", () => {
|
|
87
|
+
// The ordering trap: exit 0 must not win over timedOut.
|
|
88
|
+
const out = renderGateResult(
|
|
89
|
+
makeResult({
|
|
90
|
+
exitCode: 0,
|
|
91
|
+
timedOut: true,
|
|
92
|
+
stdout: { text: "partial output", truncated: false },
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
expect(out).toContain("INDETERMINATE")
|
|
96
|
+
expect(out).toContain("timed out")
|
|
97
|
+
expect(out).not.toContain("[XP-Gate] PASS")
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("reports INDETERMINATE when stdout was truncated", () => {
|
|
101
|
+
const out = renderGateResult(
|
|
102
|
+
makeResult({
|
|
103
|
+
stdout: { text: "head of the report", truncated: true, spillPath: "/tmp/spill.log" },
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
expect(out).toContain("INDETERMINATE")
|
|
107
|
+
expect(out).not.toContain("[XP-Gate] PASS")
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("never emits PASS anywhere in an ambiguous result", () => {
|
|
111
|
+
const ambiguous = [
|
|
112
|
+
makeResult({ exitCode: 0 }),
|
|
113
|
+
makeResult({ exitCode: COMMAND_NOT_FOUND }),
|
|
114
|
+
makeResult({ timedOut: true, exitCode: 0 }),
|
|
115
|
+
makeResult({ stdout: { text: "", truncated: true } }),
|
|
116
|
+
]
|
|
117
|
+
for (const result of ambiguous) {
|
|
118
|
+
const out = renderGateResult(result)
|
|
119
|
+
// "NOT a pass" is allowed; a bare verdict line is not.
|
|
120
|
+
expect(out).not.toMatch(/\[XP-Gate\]\s+PASS/)
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
})
|
|
@@ -98,51 +98,113 @@ function getLocalXpGateVersion(): string | null {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Decide whether an upgrade is worth attempting, without performing one.
|
|
103
|
+
*
|
|
104
|
+
* Split out of `checkXpGateUpdate` (CCN 14). That function interleaved five
|
|
105
|
+
* "return noop for this reason" branches with the install itself, so the
|
|
106
|
+
* conditions that STOP an upgrade and the act of upgrading were one readable
|
|
107
|
+
* blob. Separating them means the decision can be read (and tested) on its own.
|
|
108
|
+
*/
|
|
109
|
+
type UpgradeDecision =
|
|
110
|
+
| { kind: "noop"; localVersion: string | null; remoteVersion: string | null }
|
|
111
|
+
| { kind: "install"; localVersion: string; remoteVersion: string }
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Is the cached answer still a truthful description of this install?
|
|
115
|
+
*
|
|
116
|
+
* A cache records the version it was computed FOR. After a manual upgrade the
|
|
117
|
+
* local version differs, so the cache describes a different install and must be
|
|
118
|
+
* re-derived -- otherwise a user who upgrades by hand is never checked again.
|
|
119
|
+
*/
|
|
120
|
+
function cacheStillDescribes(
|
|
121
|
+
cached: { remoteVersion: string; localVersion?: string; status?: string } | null,
|
|
122
|
+
localVersion: string | null,
|
|
123
|
+
): cached is { remoteVersion: string; localVersion: string; status: "current" } {
|
|
124
|
+
if (!cached) return false
|
|
125
|
+
if (cached.status !== "current") return false
|
|
126
|
+
if (!cached.remoteVersion || !localVersion) return false
|
|
127
|
+
return cached.localVersion === localVersion
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function decideUpgrade(): Promise<UpgradeDecision> {
|
|
103
131
|
const localVersion = getLocalXpGateVersion()
|
|
132
|
+
const cached = readCache(XP_GATE_CACHE_FILE)
|
|
104
133
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (cached?.status === "current" && cached.remoteVersion && localVersion && cached.localVersion === localVersion) {
|
|
108
|
-
return { action: "noop", localVersion, remoteVersion: cached.remoteVersion }
|
|
134
|
+
if (cacheStillDescribes(cached, localVersion)) {
|
|
135
|
+
return { kind: "noop", localVersion, remoteVersion: cached.remoteVersion }
|
|
109
136
|
}
|
|
110
137
|
|
|
111
|
-
|
|
112
|
-
// or cache has no "current" status, we need to re-check remote
|
|
113
|
-
if (!localVersion) return { action: "noop", localVersion: null, remoteVersion: null }
|
|
138
|
+
if (!localVersion) return { kind: "noop", localVersion: null, remoteVersion: null }
|
|
114
139
|
|
|
115
140
|
const remoteVersion = await fetchNpmLatestVersion(XP_GATE_REGISTRY_URL)
|
|
116
|
-
if (!remoteVersion) return {
|
|
117
|
-
|
|
118
|
-
|
|
141
|
+
if (!remoteVersion) return { kind: "noop", localVersion, remoteVersion: null }
|
|
142
|
+
|
|
143
|
+
const upToDate = !semverLt(localVersion, remoteVersion)
|
|
144
|
+
// The opt-out is checked AFTER the version comparison so that recording
|
|
145
|
+
// "current" is still correct for an opted-out user; skipping it would make the
|
|
146
|
+
// next run re-query the registry every time.
|
|
147
|
+
const optedOut = readXpGateConfig()?.autoUpgrade === false
|
|
148
|
+
if (upToDate || optedOut) {
|
|
119
149
|
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion, status: "current" })
|
|
120
|
-
return {
|
|
150
|
+
return { kind: "noop", localVersion, remoteVersion }
|
|
121
151
|
}
|
|
122
152
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (config?.autoUpgrade === false) {
|
|
126
|
-
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion, status: "current" })
|
|
127
|
-
return { action: "noop", localVersion, remoteVersion }
|
|
128
|
-
}
|
|
153
|
+
return { kind: "install", localVersion, remoteVersion }
|
|
154
|
+
}
|
|
129
155
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Run `npm install -g` for one version.
|
|
158
|
+
*
|
|
159
|
+
* Resolves with the exit code rather than throwing on a non-zero exit: a failed
|
|
160
|
+
* install is a reported condition ("error"), not an exception the caller has to
|
|
161
|
+
* guess the meaning of. A spawn that could not start at all still rejects -- that
|
|
162
|
+
* is a different failure (no npm on PATH) and the caller must see it.
|
|
163
|
+
*
|
|
164
|
+
* The spawn call is guarded as well as listening for the 'error' event: both are
|
|
165
|
+
* reachable (the event covers an async ENOENT, the guard covers a synchronous
|
|
166
|
+
* throw such as an invalid argument), and an unhandled one here would escape into
|
|
167
|
+
* the host's chat loop.
|
|
168
|
+
*/
|
|
169
|
+
function installGlobally(version: string): Promise<number | null> {
|
|
170
|
+
return new Promise<number | null>((resolve, reject) => {
|
|
171
|
+
let child;
|
|
172
|
+
try {
|
|
173
|
+
child = spawn("npm", ["install", "-g", `${XP_GATE_NPM_PKG}@${version}`], {
|
|
134
174
|
stdio: "pipe",
|
|
135
175
|
shell: true, // Required on Windows: npm is a .cmd file
|
|
136
176
|
timeout: 120_000,
|
|
137
177
|
})
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
178
|
+
} catch (err) {
|
|
179
|
+
reject(err)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
child.on("close", (code) => resolve(code))
|
|
183
|
+
child.on("error", (err) => reject(err))
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function checkXpGateUpdate(): Promise<UpgradeResult> {
|
|
188
|
+
const decision = await decideUpgrade()
|
|
189
|
+
if (decision.kind === "noop") {
|
|
190
|
+
return {
|
|
191
|
+
action: "noop",
|
|
192
|
+
localVersion: decision.localVersion,
|
|
193
|
+
remoteVersion: decision.remoteVersion,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const { localVersion, remoteVersion } = decision
|
|
198
|
+
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion })
|
|
199
|
+
try {
|
|
200
|
+
const exitCode = await installGlobally(remoteVersion)
|
|
201
|
+
if (exitCode !== 0) {
|
|
202
|
+
return { action: "error", localVersion, remoteVersion, error: `npm install exited with code ${exitCode}` }
|
|
144
203
|
}
|
|
145
|
-
|
|
204
|
+
// Record the NEW version as local: the install succeeded, so the next run
|
|
205
|
+
// must not offer the same upgrade again.
|
|
206
|
+
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion: remoteVersion, remoteVersion, status: "current" })
|
|
207
|
+
return { action: "upgraded", localVersion, remoteVersion }
|
|
146
208
|
} catch (err) {
|
|
147
209
|
const msg = err instanceof Error ? err.message : String(err)
|
|
148
210
|
return { action: "error", localVersion, remoteVersion, error: msg }
|
|
@@ -160,26 +222,36 @@ function readXpGateConfig(): { autoUpgrade?: boolean } | null {
|
|
|
160
222
|
}
|
|
161
223
|
// ── OpenCode plugin version check (notification only) ──
|
|
162
224
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (cached?.status === "current" && cached.remoteVersion) return
|
|
166
|
-
|
|
167
|
-
let localVersion = ""
|
|
225
|
+
/** The plugin's own installed version, or null when it cannot be read. */
|
|
226
|
+
function readInstalledPluginVersion(pluginDir: string): string | null {
|
|
168
227
|
try {
|
|
169
228
|
const pkg = JSON.parse(readFileSync(join(pluginDir, "package.json"), "utf8"))
|
|
170
|
-
|
|
229
|
+
return pkg.version || null
|
|
171
230
|
} catch {
|
|
172
|
-
return
|
|
231
|
+
return null
|
|
173
232
|
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function checkPluginUpdate(pluginDir: string): Promise<void> {
|
|
236
|
+
const cached = readCache(OPENCODE_CACHE_FILE)
|
|
237
|
+
if (cached?.status === "current" && cached.remoteVersion) return
|
|
238
|
+
|
|
239
|
+
const localVersion = readInstalledPluginVersion(pluginDir)
|
|
240
|
+
if (!localVersion) return
|
|
174
241
|
|
|
175
242
|
const remoteVersion = await fetchNpmLatestVersion(OPENCODE_PLUGIN_REGISTRY)
|
|
176
243
|
if (!remoteVersion) return
|
|
177
244
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
245
|
+
// The presence or absence of `status: "current"` is the whole message: with it
|
|
246
|
+
// the caller stays quiet, without it a notice is owed. It used to be expressed
|
|
247
|
+
// as an if/else-if that re-tested `remoteVersion && localVersion` in both arms,
|
|
248
|
+
// even though both were already known non-empty by this point.
|
|
249
|
+
const upToDate = !semverLt(localVersion, remoteVersion)
|
|
250
|
+
const entry: { ts: number; localVersion: string; remoteVersion: string; status?: string } = {
|
|
251
|
+
ts: Date.now(), localVersion, remoteVersion,
|
|
182
252
|
}
|
|
253
|
+
if (upToDate) entry.status = "current"
|
|
254
|
+
writeCache(OPENCODE_CACHE_FILE, entry)
|
|
183
255
|
}
|
|
184
256
|
|
|
185
257
|
// ── TUI upgrade notice ──
|
|
@@ -432,39 +504,75 @@ function writeSessionModel(dbPath: string, sessionId: string, modelJson: string)
|
|
|
432
504
|
// Boy Scout (#447): the tool's execute() body inlined 40+ lines at 5 brace
|
|
433
505
|
// levels (tool → execute → try → JSON.stringify options). The logic lives at
|
|
434
506
|
// module scope; execute() is pure wiring.
|
|
435
|
-
|
|
507
|
+
/**
|
|
508
|
+
* The config a session-model reload needs, or the reason it cannot proceed.
|
|
509
|
+
*
|
|
510
|
+
* Split out of `reloadSessionModel` (CCN 14): the function was six preconditions
|
|
511
|
+
* plus one action, and each precondition owns a distinct diagnostic. Keeping them
|
|
512
|
+
* as one linear chain made the failure paths hard to read and impossible to test
|
|
513
|
+
* individually; returning a discriminated result lets the caller stay flat.
|
|
514
|
+
*/
|
|
515
|
+
type ReloadInputs =
|
|
516
|
+
| { ok: true; dbPath: string; providerID: string; modelID: string }
|
|
517
|
+
| { ok: false; reason: string }
|
|
518
|
+
|
|
519
|
+
function readReloadInputs(): ReloadInputs {
|
|
436
520
|
const dbPath = join(homedir(), ".local", "share", "opencode", "opencode.db")
|
|
437
|
-
const configDir = join(homedir(), ".config", "opencode")
|
|
438
|
-
const omoConfigPath = join(configDir, "oh-my-openagent.json")
|
|
439
|
-
|
|
440
521
|
if (!existsSync(dbPath)) {
|
|
441
|
-
return "Session reload failed: OpenCode database not found at " + dbPath
|
|
522
|
+
return { ok: false, reason: "Session reload failed: OpenCode database not found at " + dbPath }
|
|
442
523
|
}
|
|
524
|
+
|
|
525
|
+
const omoConfigPath = join(homedir(), ".config", "opencode", "oh-my-openagent.json")
|
|
443
526
|
if (!existsSync(omoConfigPath)) {
|
|
444
|
-
return "Session reload failed: oh-my-openagent.json not found at " + omoConfigPath
|
|
527
|
+
return { ok: false, reason: "Session reload failed: oh-my-openagent.json not found at " + omoConfigPath }
|
|
445
528
|
}
|
|
446
529
|
|
|
447
|
-
|
|
530
|
+
const parsed = parseOmoConfig(omoConfigPath)
|
|
531
|
+
if (typeof parsed === "string") return { ok: false, reason: parsed }
|
|
532
|
+
|
|
533
|
+
const model = readSisyphusModel(parsed)
|
|
534
|
+
if (typeof model === "string" && model.startsWith("Session reload failed")) {
|
|
535
|
+
return { ok: false, reason: model }
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const slashIdx = (model as string).indexOf("/")
|
|
539
|
+
if (slashIdx === -1) {
|
|
540
|
+
return { ok: false, reason: `Session reload failed: model "${model}" is not in provider/model format` }
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
ok: true,
|
|
545
|
+
dbPath,
|
|
546
|
+
providerID: (model as string).substring(0, slashIdx),
|
|
547
|
+
modelID: (model as string).substring(slashIdx + 1),
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** The parsed config object, or the failure message. */
|
|
552
|
+
function parseOmoConfig(path: string): Record<string, unknown> | string {
|
|
448
553
|
try {
|
|
449
|
-
|
|
554
|
+
return JSON.parse(readFileSync(path, "utf8"))
|
|
450
555
|
} catch (err) {
|
|
451
556
|
const msg = err instanceof Error ? err.message : String(err)
|
|
452
557
|
return "Session reload failed: could not parse oh-my-openagent.json: " + msg
|
|
453
558
|
}
|
|
559
|
+
}
|
|
454
560
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
561
|
+
/** The configured sisyphus model id, or the failure message. */
|
|
562
|
+
function readSisyphusModel(config: Record<string, unknown>): string {
|
|
563
|
+
const agents = config?.["agents"] as Record<string, { model?: string }> | undefined
|
|
564
|
+
const model = agents?.["sisyphus"]?.["model"]
|
|
565
|
+
if (!model || typeof model !== "string") {
|
|
458
566
|
return "Session reload failed: could not find sisyphus.model in oh-my-openagent.json"
|
|
459
567
|
}
|
|
568
|
+
return model
|
|
569
|
+
}
|
|
460
570
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
const providerID = sisyphusModel.substring(0, slashIdx)
|
|
466
|
-
const modelID = sisyphusModel.substring(slashIdx + 1)
|
|
571
|
+
function reloadSessionModel(sessionId: string): string {
|
|
572
|
+
const inputs = readReloadInputs()
|
|
573
|
+
if (!inputs.ok) return inputs.reason
|
|
467
574
|
|
|
575
|
+
const { dbPath, providerID, modelID } = inputs
|
|
468
576
|
const oldModel = readCurrentSessionModel(dbPath, sessionId)
|
|
469
577
|
try {
|
|
470
578
|
writeSessionModel(dbPath, sessionId, JSON.stringify({ id: modelID, providerID }))
|
|
@@ -557,4 +665,30 @@ const pluginModule = {
|
|
|
557
665
|
server: XpGatePlugin,
|
|
558
666
|
}
|
|
559
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Internals exposed for the test suite.
|
|
670
|
+
*
|
|
671
|
+
* This module had 560 lines and 27 functions with no tests at all, so nothing
|
|
672
|
+
* could be changed safely -- including the CCN=48 update callback. The pure
|
|
673
|
+
* helpers are re-exported here so their behaviour can be pinned (REQ-625)
|
|
674
|
+
* before and after the refactor that brings the complexity down.
|
|
675
|
+
*
|
|
676
|
+
* Not part of the plugin's public surface: the host loads `default`, and nothing
|
|
677
|
+
* here is a tool or a hook.
|
|
678
|
+
*/
|
|
679
|
+
export const __test = {
|
|
680
|
+
semverLt,
|
|
681
|
+
readCache,
|
|
682
|
+
writeCache,
|
|
683
|
+
extractTitleFromText,
|
|
684
|
+
getLocalXpGateVersion,
|
|
685
|
+
checkXpGateUpdate,
|
|
686
|
+
checkPluginUpdate,
|
|
687
|
+
runBackgroundUpdates,
|
|
688
|
+
reloadSessionModel,
|
|
689
|
+
renameSession,
|
|
690
|
+
XP_GATE_CACHE_FILE,
|
|
691
|
+
OPENCODE_CACHE_FILE,
|
|
692
|
+
}
|
|
693
|
+
|
|
560
694
|
export default pluginModule
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.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": "xp-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
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-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.0.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-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.0.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.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-623
|
|
3
|
+
* @intent A file the checker could not read must not be silently rounded down to
|
|
4
|
+
* "no violations". Before this, `analyze` caught the read failure into
|
|
5
|
+
* `result.errors` and `main()` never looked at that array, so the run
|
|
6
|
+
* exited 0 with a clean report. That is how CI's SARIF step archived a
|
|
7
|
+
* successful-looking report while its log said
|
|
8
|
+
* `Analysis failed: Could not read file: <blob>` -- a report about fewer
|
|
9
|
+
* files than were asked for, presented as a report about all of them.
|
|
10
|
+
*
|
|
11
|
+
* The contract kept here: an unreadable file is RECORDED (one entry, not
|
|
12
|
+
* one per rule) and `main()` refuses to call the run clean.
|
|
13
|
+
* @covers AC-623-04, AC-623-05
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { analyze, getAdapterForFile } from '../analyzer';
|
|
21
|
+
import { getAllRules } from '../index';
|
|
22
|
+
|
|
23
|
+
/** A directory named `*.ts`: resolves a TS adapter, then cannot be read. */
|
|
24
|
+
function unreadableTsPath(): string {
|
|
25
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'xp-analyze-'));
|
|
26
|
+
const asDir = path.join(dir, 'broken.ts');
|
|
27
|
+
fs.mkdirSync(asDir);
|
|
28
|
+
return asDir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('AC-623-04: an unreadable file is recorded, not swallowed', () => {
|
|
32
|
+
it('puts the read failure in result.errors', async () => {
|
|
33
|
+
const result = await analyze([unreadableTsPath()], getAllRules(), getAdapterForFile);
|
|
34
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('names the file that could not be read', async () => {
|
|
38
|
+
const target = unreadableTsPath();
|
|
39
|
+
const result = await analyze([target], getAllRules(), getAdapterForFile);
|
|
40
|
+
expect(result.errors.join('\n')).toContain('broken.ts');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('records one entry per file, not one per rule', async () => {
|
|
44
|
+
// The unreadable file is touched by every enabled rule; the failure is a
|
|
45
|
+
// property of the file, so the operator must not see it repeated per rule.
|
|
46
|
+
const result = await analyze([unreadableTsPath()], getAllRules(), getAdapterForFile);
|
|
47
|
+
expect(result.errors.length).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not report the unreadable file as examined-and-clean', async () => {
|
|
51
|
+
const result = await analyze([unreadableTsPath()], getAllRules(), getAdapterForFile);
|
|
52
|
+
// No violations could have been found in a file that was never read.
|
|
53
|
+
expect(result.violations).toHaveLength(0);
|
|
54
|
+
// ...so the only honest signal is the error, and it must be present.
|
|
55
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('AC-623-05: a healthy run records no errors', () => {
|
|
60
|
+
it('leaves errors empty when every file reads', async () => {
|
|
61
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'xp-analyze-ok-'));
|
|
62
|
+
const good = path.join(dir, 'good.ts');
|
|
63
|
+
fs.writeFileSync(good, 'export const value = 1;\n', 'utf8');
|
|
64
|
+
|
|
65
|
+
const result = await analyze([good], getAllRules(), getAdapterForFile);
|
|
66
|
+
|
|
67
|
+
expect(result.errors).toHaveLength(0);
|
|
68
|
+
expect(result.summary.filesChecked).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
});
|