@boyingliu01/xp-gate 0.31.0 → 0.31.2
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/adapter-common.sh +41 -3
- package/adapters/gate-3.sh +8 -1
- package/adapters/gate-8.sh +67 -6
- package/adapters/go.sh +18 -1
- package/adapters/iac.sh +18 -1
- package/adapters/powershell.sh +110 -7
- package/adapters/python.sh +18 -4
- package/gate-3.sh +10 -1
- package/gate-8.sh +67 -6
- package/hooks/adapter-common.sh +41 -3
- package/hooks/gate-3.sh +10 -1
- package/hooks/gate-8.sh +67 -6
- package/hooks/lib/coverage-total.sh +20 -5
- package/hooks/lib/gate-5c-alignment.sh +145 -0
- package/hooks/lib/jscpd-run.sh +40 -1
- package/hooks/lib/native-path.sh +106 -0
- package/hooks/lib/prepush-score.py +143 -0
- package/hooks/lib/sprint-gate-report.sh +23 -0
- package/hooks/lib/toolchain-path.sh +370 -0
- package/hooks/lib/validate-code-walkthrough.cjs +224 -23
- package/hooks/pre-commit +371 -163
- package/hooks/pre-push +192 -74
- package/hooks/sprint-gate.sh +14 -3
- package/lib/__tests__/check-alignment-cli.test.mjs +20 -3
- package/lib/__tests__/check-alignment-reasons.test.mjs +91 -0
- package/lib/__tests__/doctor-delphi.test.js +46 -0
- package/lib/__tests__/doctor-exec-retry.test.js +20 -10
- package/lib/__tests__/doctor-hook-signatures.test.js +65 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +12 -1
- package/lib/__tests__/doctor.test.js +7 -1
- package/lib/__tests__/hook-health-path.test.js +9 -1
- package/lib/__tests__/issue-566-sync-hooks-backup.test.js +221 -0
- package/lib/__tests__/issue-583-hook-deploy-log.test.js +121 -0
- package/lib/__tests__/issue-586-principles-cmdline.test.js +183 -0
- package/lib/__tests__/issue-589-check-all-gate3.test.js +190 -0
- package/lib/__tests__/issue-590-archlint-vacuous-pass.test.js +138 -0
- package/lib/__tests__/shared-paths-lazy-home.test.js +67 -0
- package/lib/arch.js +261 -44
- package/lib/check-alignment-runner.cjs +23 -0
- package/lib/check.js +4 -1
- package/lib/doctor.js +194 -49
- package/lib/gate-runner.js +265 -58
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +228 -6
- package/lib/shared-paths.js +53 -19
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/gremlins-runner.test.ts +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/delphi-review/SKILL.md +50 -0
- 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/delphi-review/SKILL.md +50 -0
- 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/delphi-config-env-diagnostics.test.ts +210 -0
- package/plugins/dsh/src/delphi-config.ts +151 -9
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +50 -0
- 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/delphi-review/SKILL.md +50 -0
- 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__/issue-587-java-member-count.test.ts +279 -0
- package/principles/__tests__/issue-588-lsp-java-false-positives.test.ts +213 -0
- package/principles/adapters/java.ts +127 -11
- package/principles/rules/__tests__/solid/issue-588-lsp-base-contract.test.ts +252 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +26 -22
- package/principles/rules/solid/lsp.ts +348 -23
- package/scripts/__tests__/sync-package-content.test.js +32 -1
- package/scripts/sync-package-content.js +6 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +50 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/sprint-gate.sh +14 -3
package/lib/shared-paths.js
CHANGED
|
@@ -14,13 +14,9 @@ const fs = require('fs');
|
|
|
14
14
|
* Resolve the user's home directory cross-platform.
|
|
15
15
|
* Fallback chain: HOME → USERPROFILE → os.homedir()
|
|
16
16
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, 'xp-gate.json');
|
|
21
|
-
const GLOBAL_HOOKS_DIR = path.join(CONFIG_DIR, 'hooks');
|
|
22
|
-
const GLOBAL_ADAPTERS_DIR = path.join(CONFIG_DIR, 'adapters');
|
|
23
|
-
const GLOBAL_MODULES_DIR = path.join(CONFIG_DIR, 'modules');
|
|
17
|
+
function resolveHomeDir() {
|
|
18
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
19
|
+
}
|
|
24
20
|
|
|
25
21
|
/**
|
|
26
22
|
* Detect which AI agent platform is currently in use.
|
|
@@ -28,11 +24,12 @@ const GLOBAL_MODULES_DIR = path.join(CONFIG_DIR, 'modules');
|
|
|
28
24
|
* @returns {'opencode' | 'claude-code' | 'qoder'}
|
|
29
25
|
*/
|
|
30
26
|
function detectPlatform() {
|
|
27
|
+
const homeDir = resolveHomeDir();
|
|
31
28
|
// Any ~/.qoder content counts as Qoder: a brand-new install has no skills/
|
|
32
29
|
// subdirectory yet, so a skills-only marker misdetects it as opencode and
|
|
33
30
|
// configureQoderDelphiAgents deploys zero Delphi agents (#424).
|
|
34
|
-
if (fs.existsSync(path.join(
|
|
35
|
-
if (fs.existsSync(path.join(
|
|
31
|
+
if (fs.existsSync(path.join(homeDir, '.qoder'))) return 'qoder';
|
|
32
|
+
if (fs.existsSync(path.join(homeDir, '.claude', 'skills'))) return 'claude-code';
|
|
36
33
|
return 'opencode';
|
|
37
34
|
}
|
|
38
35
|
|
|
@@ -49,23 +46,60 @@ function detectPlatform() {
|
|
|
49
46
|
* @returns {string} Path to the platform-specific git hooks template directory
|
|
50
47
|
*/
|
|
51
48
|
function getTemplateDir() {
|
|
49
|
+
const homeDir = resolveHomeDir();
|
|
52
50
|
const platform = detectPlatform();
|
|
53
51
|
const configDir = platform === 'claude-code' ? '.claude' :
|
|
54
52
|
platform === 'qoder' ? '.qoder' :
|
|
55
53
|
path.join('.config', 'opencode');
|
|
56
|
-
return path.join(
|
|
54
|
+
return path.join(homeDir, configDir, 'git-hooks-template');
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// The constants below are DERIVED FROM THE ENVIRONMENT, so they are computed
|
|
59
|
+
// per property access rather than once at require() time.
|
|
60
|
+
//
|
|
61
|
+
// They used to be plain `const`s evaluated at module load. That made the value
|
|
62
|
+
// depend on WHO LOADED THIS MODULE FIRST: detect-deps.js requires shared-paths
|
|
63
|
+
// at its own load, so any consumer loaded earlier froze the paths to whatever
|
|
64
|
+
// HOME was at that moment. In the doctor test suite that was the REAL home
|
|
65
|
+
// directory — detect-deps is loaded in beforeAll, before any test sets HOME —
|
|
66
|
+
// so doctor reported `Config file: Not found` for every fixture that had in
|
|
67
|
+
// fact written the config, and `--fix` returned 1 on a healthy installation.
|
|
68
|
+
//
|
|
69
|
+
// A getter keeps the existing `require`-and-destructure call sites working
|
|
70
|
+
// (module.exports.CONFIG_FILE still evaluates to a string), while making the
|
|
71
|
+
// value follow process.env.HOME.
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
function defineLazy(obj, name, compute) {
|
|
74
|
+
Object.defineProperty(obj, name, {
|
|
75
|
+
enumerable: true,
|
|
76
|
+
get: compute,
|
|
77
|
+
// The values are a pure function of the environment; caching them for the
|
|
78
|
+
// lifetime of the process would reintroduce the bug fixed here.
|
|
79
|
+
configurable: true,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const GLOBAL_DIRS = {
|
|
84
|
+
CONFIG_DIR: () => path.join(resolveHomeDir(), '.config', 'xp-gate'),
|
|
85
|
+
GLOBAL_HOOKS_DIR: () => path.join(GLOBAL_DIRS.CONFIG_DIR(), 'hooks'),
|
|
86
|
+
GLOBAL_ADAPTERS_DIR: () => path.join(GLOBAL_DIRS.CONFIG_DIR(), 'adapters'),
|
|
87
|
+
GLOBAL_MODULES_DIR: () => path.join(GLOBAL_DIRS.CONFIG_DIR(), 'modules'),
|
|
88
|
+
};
|
|
60
89
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
TEMPLATE_DIR,
|
|
66
|
-
GLOBAL_HOOKS_DIR,
|
|
67
|
-
GLOBAL_ADAPTERS_DIR,
|
|
68
|
-
GLOBAL_MODULES_DIR,
|
|
90
|
+
// Base name for the other derived paths.
|
|
91
|
+
GLOBAL_DIRS.CONFIG_FILE = () => path.join(GLOBAL_DIRS.CONFIG_DIR(), 'xp-gate.json');
|
|
92
|
+
|
|
93
|
+
const exportsObj = {
|
|
69
94
|
detectPlatform,
|
|
70
95
|
getTemplateDir,
|
|
96
|
+
resolveHomeDir,
|
|
71
97
|
};
|
|
98
|
+
|
|
99
|
+
defineLazy(exportsObj, 'HOME_DIR', () => resolveHomeDir());
|
|
100
|
+
for (const [name, compute] of Object.entries(GLOBAL_DIRS)) {
|
|
101
|
+
defineLazy(exportsObj, name, compute);
|
|
102
|
+
}
|
|
103
|
+
defineLazy(exportsObj, 'TEMPLATE_DIR', () => getTemplateDir());
|
|
104
|
+
|
|
105
|
+
module.exports = exportsObj;
|
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-25
|
|
4
|
+
**Commit:** 1ab53eff
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.31.
|
|
6
|
+
**Version:** 0.31.2.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-25
|
|
4
|
+
**Commit:** 1ab53eff
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.31.
|
|
6
|
+
**Version:** 0.31.2.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.
|
|
@@ -106,7 +106,7 @@ describe('GremlinsRunner', () => {
|
|
|
106
106
|
|
|
107
107
|
it('returns report=null (never throws) when the report lines are absent', async () => {
|
|
108
108
|
let closeCb: ((code: number | null) => void) | undefined;
|
|
109
|
-
vi.mocked(spawn).mockReturnValue(mockChild({ stdout: () => {}, close: cb => { closeCb = cb; } }));
|
|
109
|
+
vi.mocked(spawn).mockReturnValue(mockChild({ stdout: () => { /* no output: the stub emits nothing on stdout */ }, close: cb => { closeCb = cb; } }));
|
|
110
110
|
|
|
111
111
|
const runner = new GremlinsRunner();
|
|
112
112
|
const promise = runner.run({ files: ['src/foo.py'], timeoutMs: 5000, cwd: '/tmp' });
|
|
@@ -136,7 +136,7 @@ describe('GremlinsRunner', () => {
|
|
|
136
136
|
|
|
137
137
|
it('passes --gremlin-targets derived from the changed file directories', async () => {
|
|
138
138
|
let closeCb: ((code: number | null) => void) | undefined;
|
|
139
|
-
vi.mocked(spawn).mockReturnValue(mockChild({ stdout: () => {}, close: cb => { closeCb = cb; } }));
|
|
139
|
+
vi.mocked(spawn).mockReturnValue(mockChild({ stdout: () => { /* no output: the stub emits nothing on stdout */ }, close: cb => { closeCb = cb; } }));
|
|
140
140
|
|
|
141
141
|
const runner = new GremlinsRunner();
|
|
142
142
|
const promise = runner.run({ files: ['src/foo/bar.py', 'src/baz.py'], timeoutMs: 5000, cwd: '/tmp' });
|
|
@@ -152,7 +152,7 @@ describe('GremlinsRunner', () => {
|
|
|
152
152
|
|
|
153
153
|
it('reports timedOut when the child exceeds the deadline', async () => {
|
|
154
154
|
let closeCb: ((code: number | null) => void) | undefined;
|
|
155
|
-
const child = mockChild({ stdout: () => {}, close: cb => { closeCb = cb; } });
|
|
155
|
+
const child = mockChild({ stdout: () => { /* no output: the stub emits nothing on stdout */ }, close: cb => { closeCb = cb; } });
|
|
156
156
|
vi.mocked(spawn).mockReturnValue(child);
|
|
157
157
|
|
|
158
158
|
const runner = new GremlinsRunner();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.2",
|
|
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-25
|
|
4
|
+
**Commit:** 1ab53eff
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.31.
|
|
6
|
+
**Version:** 0.31.2.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.
|
|
@@ -392,6 +392,11 @@ delphi-review(
|
|
|
392
392
|
)
|
|
393
393
|
```
|
|
394
394
|
|
|
395
|
+
> **先确认该工具真的存在(#574)**:不是所有 DSH profile 都装了它。若当前会话的工具列表里
|
|
396
|
+
> 没有 `delphi-review`,说明插件未安装 —— 此时**不要**推断"那就用 subagent / workflow 吧"。
|
|
397
|
+
> 按下方【通道白名单】处理:要么装插件(`dsh plugin add @boyingliu01/dsh-plugin-xp-gate`),
|
|
398
|
+
> 要么走 external 脚本路径,要么 BLOCK。
|
|
399
|
+
|
|
395
400
|
该工具**直连外部 OpenAI 兼容网关**,每个席位绑定一个不同模型,因此不依赖宿主配置即可
|
|
396
401
|
满足不变量。它复用**同一份** `.delphi-config.json`,无需额外配置。
|
|
397
402
|
|
|
@@ -431,6 +436,51 @@ delphi-review(
|
|
|
431
436
|
- 降级证据落盘 `delphi-reviewed.json`:`channel`、`fallback_reason`(如 `dsh_subagent_model_selection_disabled`)、`probe_evidence`(①②③ 的实测摘录)。
|
|
432
437
|
- 两通道同不变量:三专家齐全、`requested_model` 互异、consensus ≥90%、**无单专家路径**。
|
|
433
438
|
- 可用性探测可用 `xp-gate doctor delphi` 复核(DSH 环境下含 `subagentModelSelection` 持久化设置检查)。
|
|
439
|
+
|
|
440
|
+
##### 通道白名单(default-deny,#574)
|
|
441
|
+
|
|
442
|
+
上面的双通道契约是**允许清单**,不是"除 subagent 外都能用"。**只有这两条通道合法**:
|
|
443
|
+
`subagent`(native,需通过 ①②③)与 external(插件工具 / `delphi-external-review.cjs`)。
|
|
444
|
+
|
|
445
|
+
**其余一切扇出机制一律禁止**,包括但不限于:
|
|
446
|
+
|
|
447
|
+
| 禁止的通道 | 为什么禁止 |
|
|
448
|
+
|---|---|
|
|
449
|
+
| `workflow` 工具的 `agent()` 扇出 | `agent()` 的 `provider`/`model` **不受** `allowedModels` 约束(`dsh-workflow-ptc` 里没有任何白名单校验,直达 `subagents.start`)。脚本里写什么模型就真的跑什么模型。 |
|
|
450
|
+
| 手写脚本 / 循环里逐个调 `subagent` 但省略 `model` | 缺 `model` 时子代理**静默继承宿主会话模型**,三席变一席。 |
|
|
451
|
+
| Agent Teams / `spawn_teammate` | 团队成员模型不可逐条指定,且 `TeamMemberView.model` 是输出字段、可能是继承值。 |
|
|
452
|
+
| 任何"从网关模型清单里挑三个"的做法 | 网关可能注册上百个模型,任意三个互异都满足旧不变量 —— 这正是 #574 的事故形态。 |
|
|
453
|
+
|
|
454
|
+
**推荐通道不可用时,必须 BLOCK 并如实报告,不得自行选一条"看起来能跑"的路径。**
|
|
455
|
+
判定句:*如果我正在用的派发机制不在上面两条之内,立即停止,输出 BLOCK 并说明缺什么。*
|
|
456
|
+
|
|
457
|
+
##### 席位解析(派发前强制步骤,#574)
|
|
458
|
+
|
|
459
|
+
模型 ID **只能来自 `.delphi-config.json`**,不得来自会话默认模型、网关模型清单、
|
|
460
|
+
记忆、或上一次评审的残留。派发前依次执行:
|
|
461
|
+
|
|
462
|
+
1. 读取 `<项目根>/.delphi-config.json`,取 `active_profile` 指向的 profile。
|
|
463
|
+
(查找顺序:项目根 → `~/.config/xp-gate/.delphi-config.json`;多份存在时以项目根为准。)
|
|
464
|
+
2. 解出 `architecture` / `technical` / `feasibility` 三个席位的 `model`,**去空白**。
|
|
465
|
+
3. **原样打印**这三个 ID 与 `active_profile`,作为本轮派发的凭据。
|
|
466
|
+
4. 只用这三个字面量派发;脚本里**不得出现配置中不存在的模型 ID**。
|
|
467
|
+
5. 三席位必须互异 —— 若配置本身有重复,这是配置错误,BLOCK,不要去"挑一个换掉"。
|
|
468
|
+
|
|
469
|
+
> 反例(2026-10-22 实测事故):某次 code-walkthrough 的 workflow 脚本硬编码了
|
|
470
|
+
> `s-deepseek-v4.1-flash` / `g-deepseek-v4-pro` / `doubao-seed-2.0-pro`,
|
|
471
|
+
> 而配置写的是三个 `local-*` 免费席位。三者互异、评审"成功"、Gate MW 判 APPROVED,
|
|
472
|
+
> 但付费模型被真实调用,而 `local-*` 只落了 1/3。16 次 workflow 运行中读取配置的次数为 **0**。
|
|
473
|
+
|
|
474
|
+
##### 执行后核验(#574)
|
|
475
|
+
|
|
476
|
+
评审结束后必须逐席位核对**实际解析到的模型**,不能只看请求值:
|
|
477
|
+
|
|
478
|
+
- 证据来源:子会话的 `request/header`(平台日志)或 external 脚本的回执 `resolved_model`。
|
|
479
|
+
- 三席 `requested_model` 必须**逐一等于**配置中同角色的席位(按角色对应,不是"三者在集合里")。
|
|
480
|
+
- 任一不匹配 → 本次评审**作废**,不得写入 `.code-walkthrough-result.json`,按 BLOCK 处理。
|
|
481
|
+
- Gate MW 会独立复核这一点(#574):存在 `.delphi-config.json` 时,
|
|
482
|
+
证据里每个 `requested_model` 必须等于该角色的配置席位,否则 pre-push 直接 BLOCK。
|
|
483
|
+
|
|
434
484
|
#### WorkBuddy 平台(Expert 团队模式)
|
|
435
485
|
|
|
436
486
|
WorkBuddy 侧以 Team 型专家团(示例:`delphi-review-team`,安装在 `~/.workbuddy/plugins/marketplaces/my-experts/plugins/`,创建方式见 [docs/platform-workbuddy.md](../../docs/platform-workbuddy.md))承载评审,触发本技能后按此路由执行:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-25
|
|
4
|
+
**Commit:** 1ab53eff
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.31.
|
|
6
|
+
**Version:** 0.31.2.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-25
|
|
4
|
+
**Commit:** 1ab53eff
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.31.
|
|
6
|
+
**Version:** 0.31.2.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.31.
|
|
3
|
+
"version": "0.31.2",
|
|
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-25
|
|
4
|
+
**Commit:** 1ab53eff
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.31.
|
|
6
|
+
**Version:** 0.31.2.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.
|
|
@@ -392,6 +392,11 @@ delphi-review(
|
|
|
392
392
|
)
|
|
393
393
|
```
|
|
394
394
|
|
|
395
|
+
> **先确认该工具真的存在(#574)**:不是所有 DSH profile 都装了它。若当前会话的工具列表里
|
|
396
|
+
> 没有 `delphi-review`,说明插件未安装 —— 此时**不要**推断"那就用 subagent / workflow 吧"。
|
|
397
|
+
> 按下方【通道白名单】处理:要么装插件(`dsh plugin add @boyingliu01/dsh-plugin-xp-gate`),
|
|
398
|
+
> 要么走 external 脚本路径,要么 BLOCK。
|
|
399
|
+
|
|
395
400
|
该工具**直连外部 OpenAI 兼容网关**,每个席位绑定一个不同模型,因此不依赖宿主配置即可
|
|
396
401
|
满足不变量。它复用**同一份** `.delphi-config.json`,无需额外配置。
|
|
397
402
|
|
|
@@ -431,6 +436,51 @@ delphi-review(
|
|
|
431
436
|
- 降级证据落盘 `delphi-reviewed.json`:`channel`、`fallback_reason`(如 `dsh_subagent_model_selection_disabled`)、`probe_evidence`(①②③ 的实测摘录)。
|
|
432
437
|
- 两通道同不变量:三专家齐全、`requested_model` 互异、consensus ≥90%、**无单专家路径**。
|
|
433
438
|
- 可用性探测可用 `xp-gate doctor delphi` 复核(DSH 环境下含 `subagentModelSelection` 持久化设置检查)。
|
|
439
|
+
|
|
440
|
+
##### 通道白名单(default-deny,#574)
|
|
441
|
+
|
|
442
|
+
上面的双通道契约是**允许清单**,不是"除 subagent 外都能用"。**只有这两条通道合法**:
|
|
443
|
+
`subagent`(native,需通过 ①②③)与 external(插件工具 / `delphi-external-review.cjs`)。
|
|
444
|
+
|
|
445
|
+
**其余一切扇出机制一律禁止**,包括但不限于:
|
|
446
|
+
|
|
447
|
+
| 禁止的通道 | 为什么禁止 |
|
|
448
|
+
|---|---|
|
|
449
|
+
| `workflow` 工具的 `agent()` 扇出 | `agent()` 的 `provider`/`model` **不受** `allowedModels` 约束(`dsh-workflow-ptc` 里没有任何白名单校验,直达 `subagents.start`)。脚本里写什么模型就真的跑什么模型。 |
|
|
450
|
+
| 手写脚本 / 循环里逐个调 `subagent` 但省略 `model` | 缺 `model` 时子代理**静默继承宿主会话模型**,三席变一席。 |
|
|
451
|
+
| Agent Teams / `spawn_teammate` | 团队成员模型不可逐条指定,且 `TeamMemberView.model` 是输出字段、可能是继承值。 |
|
|
452
|
+
| 任何"从网关模型清单里挑三个"的做法 | 网关可能注册上百个模型,任意三个互异都满足旧不变量 —— 这正是 #574 的事故形态。 |
|
|
453
|
+
|
|
454
|
+
**推荐通道不可用时,必须 BLOCK 并如实报告,不得自行选一条"看起来能跑"的路径。**
|
|
455
|
+
判定句:*如果我正在用的派发机制不在上面两条之内,立即停止,输出 BLOCK 并说明缺什么。*
|
|
456
|
+
|
|
457
|
+
##### 席位解析(派发前强制步骤,#574)
|
|
458
|
+
|
|
459
|
+
模型 ID **只能来自 `.delphi-config.json`**,不得来自会话默认模型、网关模型清单、
|
|
460
|
+
记忆、或上一次评审的残留。派发前依次执行:
|
|
461
|
+
|
|
462
|
+
1. 读取 `<项目根>/.delphi-config.json`,取 `active_profile` 指向的 profile。
|
|
463
|
+
(查找顺序:项目根 → `~/.config/xp-gate/.delphi-config.json`;多份存在时以项目根为准。)
|
|
464
|
+
2. 解出 `architecture` / `technical` / `feasibility` 三个席位的 `model`,**去空白**。
|
|
465
|
+
3. **原样打印**这三个 ID 与 `active_profile`,作为本轮派发的凭据。
|
|
466
|
+
4. 只用这三个字面量派发;脚本里**不得出现配置中不存在的模型 ID**。
|
|
467
|
+
5. 三席位必须互异 —— 若配置本身有重复,这是配置错误,BLOCK,不要去"挑一个换掉"。
|
|
468
|
+
|
|
469
|
+
> 反例(2026-10-22 实测事故):某次 code-walkthrough 的 workflow 脚本硬编码了
|
|
470
|
+
> `s-deepseek-v4.1-flash` / `g-deepseek-v4-pro` / `doubao-seed-2.0-pro`,
|
|
471
|
+
> 而配置写的是三个 `local-*` 免费席位。三者互异、评审"成功"、Gate MW 判 APPROVED,
|
|
472
|
+
> 但付费模型被真实调用,而 `local-*` 只落了 1/3。16 次 workflow 运行中读取配置的次数为 **0**。
|
|
473
|
+
|
|
474
|
+
##### 执行后核验(#574)
|
|
475
|
+
|
|
476
|
+
评审结束后必须逐席位核对**实际解析到的模型**,不能只看请求值:
|
|
477
|
+
|
|
478
|
+
- 证据来源:子会话的 `request/header`(平台日志)或 external 脚本的回执 `resolved_model`。
|
|
479
|
+
- 三席 `requested_model` 必须**逐一等于**配置中同角色的席位(按角色对应,不是"三者在集合里")。
|
|
480
|
+
- 任一不匹配 → 本次评审**作废**,不得写入 `.code-walkthrough-result.json`,按 BLOCK 处理。
|
|
481
|
+
- Gate MW 会独立复核这一点(#574):存在 `.delphi-config.json` 时,
|
|
482
|
+
证据里每个 `requested_model` 必须等于该角色的配置席位,否则 pre-push 直接 BLOCK。
|
|
483
|
+
|
|
434
484
|
#### WorkBuddy 平台(Expert 团队模式)
|
|
435
485
|
|
|
436
486
|
WorkBuddy 侧以 Team 型专家团(示例:`delphi-review-team`,安装在 `~/.workbuddy/plugins/marketplaces/my-experts/plugins/`,创建方式见 [docs/platform-workbuddy.md](../../docs/platform-workbuddy.md))承载评审,触发本技能后按此路由执行:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-25
|
|
4
|
+
**Commit:** 1ab53eff
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.31.
|
|
6
|
+
**Version:** 0.31.2.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-25
|
|
4
|
+
**Commit:** 1ab53eff
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.31.
|
|
6
|
+
**Version:** 0.31.2.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { DelphiSetupError, expandEnv, expandEnvDetailed, resolveConfig } from "./delphi-config.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @test REQ-DSH-016
|
|
6
|
+
* @intent #576:「变量未设置」和「变量已设置为空串」以及「变量已设置但当前进程没有
|
|
7
|
+
* 继承到」在旧实现里被渲染成同一句 `is not set in the environment`。
|
|
8
|
+
* 第一句是事实错误,第二句丢掉了最常见的真实原因(宿主进程环境陈旧),
|
|
9
|
+
* 把排查引向 key 本身、配置写法、网关权限——唯独不会想到重启宿主。
|
|
10
|
+
*
|
|
11
|
+
* 这组用例要求 expandEnv 不丢失这一区分,并要求报错文案把已知事实全部
|
|
12
|
+
* 说出来:变量名、占位符形状、以及 env 中是否存在大小写不同的同名变量
|
|
13
|
+
* (Windows 环境变量大小写不敏感,POSIX 敏感——这是真实的平台差异)。
|
|
14
|
+
*
|
|
15
|
+
* 旁路探测(读取注册表 / shell 启动文件)只能作为可选诊断:探测本身
|
|
16
|
+
* 出错必须静默降级为现有文案,绝不能让诊断成为新的失败点。
|
|
17
|
+
* @covers AC-DSH-016-04
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const baseConfig = {
|
|
21
|
+
active_profile: "whalecloud",
|
|
22
|
+
profiles: {
|
|
23
|
+
whalecloud: {
|
|
24
|
+
provider: "whalecloud",
|
|
25
|
+
providers: {
|
|
26
|
+
whalecloud: {
|
|
27
|
+
base_url: "https://lab.example.com/gpt-proxy",
|
|
28
|
+
api_key: "${WHALECLOUD_API_KEY}",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
experts: [
|
|
32
|
+
{ id: "Expert A", role: "architecture", model: "g-glm-5.3-flash", max_tokens: 32000 },
|
|
33
|
+
{ id: "Expert B", role: "technical", model: "g-qwen3.8-flash" },
|
|
34
|
+
{ id: "Expert C", role: "feasibility", model: "g-deepseek-flash" },
|
|
35
|
+
],
|
|
36
|
+
consensus: { threshold_percent: 90, max_review_rounds: 5 },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const configWith = (apiKey: string) => ({
|
|
42
|
+
...baseConfig,
|
|
43
|
+
profiles: {
|
|
44
|
+
whalecloud: {
|
|
45
|
+
...baseConfig.profiles.whalecloud,
|
|
46
|
+
providers: {
|
|
47
|
+
whalecloud: {
|
|
48
|
+
base_url: "https://lab.example.com/gpt-proxy",
|
|
49
|
+
api_key: apiKey,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/** Capture the error message a resolve call produces, or "" if it did not throw. */
|
|
57
|
+
function messageFrom(apiKey: string, env: Record<string, string>): string {
|
|
58
|
+
try {
|
|
59
|
+
resolveConfig(configWith(apiKey), env)
|
|
60
|
+
return ""
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return error instanceof DelphiSetupError ? error.message : `NOT-A-SETUP-ERROR: ${String(error)}`
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("expandEnvDetailed keeps the three outcomes apart (#576)", () => {
|
|
67
|
+
it("anti-vacuity: the detailed form exists and expands a present variable", () => {
|
|
68
|
+
// If this fails, every case below would pass against a stub.
|
|
69
|
+
const result = expandEnvDetailed("Bearer ${TOK}!", { TOK: "secret" })
|
|
70
|
+
expect(result.value).toBe("Bearer secret!")
|
|
71
|
+
expect(result.missing).toEqual([])
|
|
72
|
+
expect(result.empty).toEqual([])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("reports a variable that the environment does not define as missing", () => {
|
|
76
|
+
const result = expandEnvDetailed("${NOPE}", {})
|
|
77
|
+
expect(result.value).toBe("")
|
|
78
|
+
expect(result.missing).toEqual(["NOPE"])
|
|
79
|
+
expect(result.empty).toEqual([])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("reports a variable that is defined but empty as empty, not missing", () => {
|
|
83
|
+
// "" is a CONFIGURATION error (someone set it wrong). Absent is an
|
|
84
|
+
// ENVIRONMENT error (nobody set it). Conflating them sends the reader to the
|
|
85
|
+
// wrong file (#576).
|
|
86
|
+
const result = expandEnvDetailed("${TOK}", { TOK: "" })
|
|
87
|
+
expect(result.value).toBe("")
|
|
88
|
+
expect(result.missing).toEqual([])
|
|
89
|
+
expect(result.empty).toEqual(["TOK"])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("handles both placeholder spellings in one value", () => {
|
|
93
|
+
const result = expandEnvDetailed("{env:A}-${B}-${C}", { A: "x", B: "" })
|
|
94
|
+
expect(result.value).toBe("x--")
|
|
95
|
+
expect(result.missing).toEqual(["C"])
|
|
96
|
+
expect(result.empty).toEqual(["B"])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("does not repeat a variable referenced twice", () => {
|
|
100
|
+
const result = expandEnvDetailed("${DUP}${DUP}", {})
|
|
101
|
+
expect(result.missing).toEqual(["DUP"])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("expandEnv still returns just the string for existing callers", () => {
|
|
105
|
+
expect(expandEnv("${NOPE}", {})).toBe("")
|
|
106
|
+
expect(expandEnv("${TOK}", { TOK: "v" })).toBe("v")
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe("the missing-key message names the actual situation (#576)", () => {
|
|
111
|
+
it("says the variable is not set when it is absent", () => {
|
|
112
|
+
const message = messageFrom("${WHALECLOUD_API_KEY}", {})
|
|
113
|
+
expect(message).toContain("WHALECLOUD_API_KEY")
|
|
114
|
+
expect(message).toMatch(/not set/i)
|
|
115
|
+
// The remedy has to be reachable from the message alone.
|
|
116
|
+
expect(message).toMatch(/set|export|environment/i)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it("does NOT claim the variable is unset when it is set to an empty string", () => {
|
|
120
|
+
const message = messageFrom("${WHALECLOUD_API_KEY}", { WHALECLOUD_API_KEY: "" })
|
|
121
|
+
expect(message).toContain("WHALECLOUD_API_KEY")
|
|
122
|
+
// The false statement this issue is about.
|
|
123
|
+
expect(message).not.toMatch(/is not set in the environment/i)
|
|
124
|
+
expect(message).toMatch(/empty/i)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("names the placeholder spelling used in the config", () => {
|
|
128
|
+
// Which spelling appears in the config is the first thing a reader needs:
|
|
129
|
+
// `${VAR}` and `{env:VAR}` expand through different code paths, and a typo
|
|
130
|
+
// in the spelling looks exactly like an unset variable.
|
|
131
|
+
expect(messageFrom("${WHALECLOUD_API_KEY}", {})).toMatch(/\$\{WHALECLOUD_API_KEY\}/)
|
|
132
|
+
expect(messageFrom("{env:WHALECLOUD_API_KEY}", {})).toContain("{env:WHALECLOUD_API_KEY}")
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it("points at a same-name variable whose case differs", () => {
|
|
136
|
+
// Windows env vars are case-insensitive but a POSIX shell's are not, so a
|
|
137
|
+
// value can be present under a different case and silently not match. The
|
|
138
|
+
// message must surface the near-miss instead of reporting "not set".
|
|
139
|
+
const message = messageFrom("${WHALECLOUD_API_KEY}", { whalecloud_api_key: "k" })
|
|
140
|
+
expect(message).toContain("whalecloud_api_key")
|
|
141
|
+
expect(message).toMatch(/case/i)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it("says nothing about case when there is no near-miss", () => {
|
|
145
|
+
const message = messageFrom("${WHALECLOUD_API_KEY}", { OTHER: "x" })
|
|
146
|
+
expect(message).not.toMatch(/case/i)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it("never echoes a literal api_key that is present", () => {
|
|
150
|
+
// A literal key in the config is valid and resolves; it must not appear in
|
|
151
|
+
// any diagnostic produced for OTHER providers.
|
|
152
|
+
const secret = "sk-live-DO-NOT-LEAK-42"
|
|
153
|
+
const config = {
|
|
154
|
+
...baseConfig,
|
|
155
|
+
profiles: {
|
|
156
|
+
whalecloud: {
|
|
157
|
+
...baseConfig.profiles.whalecloud,
|
|
158
|
+
providers: {
|
|
159
|
+
// Declared first so a later failure has already seen this value.
|
|
160
|
+
literal: { base_url: "https://literal.example.com", api_key: secret },
|
|
161
|
+
whalecloud: { base_url: "https://lab.example.com/gpt-proxy", api_key: "${WHALECLOUD_API_KEY}" },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
let message = ""
|
|
167
|
+
try {
|
|
168
|
+
resolveConfig(config, {})
|
|
169
|
+
} catch (error) {
|
|
170
|
+
message = String(error)
|
|
171
|
+
}
|
|
172
|
+
expect(message).toContain("WHALECLOUD_API_KEY")
|
|
173
|
+
expect(message).not.toContain(secret)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it("names the literal-value case when api_key is an empty literal", () => {
|
|
177
|
+
// `api_key: ""` references no variable, so no variable should be blamed.
|
|
178
|
+
const message = messageFrom("", {})
|
|
179
|
+
expect(message).toMatch(/no api_key/i)
|
|
180
|
+
expect(message).not.toMatch(/WHALECLOUD_API_KEY/)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it("a provider with no api_key at all keeps its direct message", () => {
|
|
184
|
+
const message = messageFrom("", {})
|
|
185
|
+
expect(message).toMatch(/no api_key/i)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it("the diagnostic never turns into a failure of its own", () => {
|
|
189
|
+
// A hostile/odd environment must degrade to a plain message, not throw
|
|
190
|
+
// something that is not a DelphiSetupError.
|
|
191
|
+
const weird = new Proxy(
|
|
192
|
+
{},
|
|
193
|
+
{
|
|
194
|
+
get() {
|
|
195
|
+
throw new Error("environment read exploded")
|
|
196
|
+
},
|
|
197
|
+
ownKeys() {
|
|
198
|
+
throw new Error("environment enumeration exploded")
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
) as unknown as NodeJS.ProcessEnv
|
|
202
|
+
let thrown: unknown
|
|
203
|
+
try {
|
|
204
|
+
resolveConfig(baseConfig, weird)
|
|
205
|
+
} catch (error) {
|
|
206
|
+
thrown = error
|
|
207
|
+
}
|
|
208
|
+
expect(thrown).toBeInstanceOf(DelphiSetupError)
|
|
209
|
+
})
|
|
210
|
+
})
|