@boyingliu01/xp-gate 0.27.6 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/adapter-common.sh +0 -0
  2. package/adapters/cpp.sh +0 -0
  3. package/adapters/dart.sh +0 -0
  4. package/adapters/flutter.sh +0 -0
  5. package/adapters/gate-9.sh +13 -2
  6. package/adapters/go.sh +0 -0
  7. package/adapters/iac.sh +0 -0
  8. package/adapters/java.sh +0 -0
  9. package/adapters/kotlin.sh +0 -0
  10. package/adapters/objectivec.sh +0 -0
  11. package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
  12. package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
  13. package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
  14. package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
  15. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
  16. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
  17. package/adapters/powershell.sh +0 -0
  18. package/adapters/python.sh +0 -0
  19. package/adapters/shell.sh +0 -0
  20. package/adapters/swift.sh +0 -0
  21. package/adapters/typescript.sh +0 -0
  22. package/bin/xp-gate.js +0 -0
  23. package/build-integrity/gate-10.ts +290 -263
  24. package/gate-10.sh +25 -6
  25. package/gate-12-file-hygiene.sh +35 -1
  26. package/gate-3.sh +78 -2
  27. package/gate-4.sh +0 -0
  28. package/gate-7.sh +0 -0
  29. package/gate-8.sh +0 -0
  30. package/gate-9.sh +13 -2
  31. package/hooks/adapter-common.sh +0 -0
  32. package/hooks/gate-10.sh +25 -6
  33. package/hooks/gate-12-file-hygiene.sh +35 -1
  34. package/hooks/gate-3.sh +78 -2
  35. package/hooks/gate-4.sh +0 -0
  36. package/hooks/gate-7.sh +0 -0
  37. package/hooks/gate-8.sh +0 -0
  38. package/hooks/gate-9.sh +13 -2
  39. package/hooks/lib/audit-sanitize.sh +51 -0
  40. package/hooks/lib/ccn-ratchet.sh +183 -0
  41. package/hooks/lib/docs-only.sh +49 -0
  42. package/hooks/post-merge +0 -0
  43. package/hooks/pre-commit +74 -15
  44. package/hooks/pre-push +36 -5
  45. package/hooks/sprint-gate.sh +0 -0
  46. package/lib/__tests__/sprint-audit.test.js +207 -207
  47. package/lib/gate-audit.ts +9 -4
  48. package/lib/mattpocock-license-verdict.json +1 -1
  49. package/lib/retro.js +27 -6
  50. package/lib/sprint-audit.js +235 -235
  51. package/mock-policy/AGENTS.md +3 -3
  52. package/mutation/AGENTS.md +3 -3
  53. package/package.json +1 -1
  54. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  55. package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
  56. package/plugins/claude-code/bin/xp-gate-check +0 -0
  57. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  58. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  59. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  60. package/plugins/dsh/package.json +1 -1
  61. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  62. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  63. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  64. package/plugins/opencode/package.json +1 -1
  65. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  66. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  67. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  68. package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
  69. package/plugins/qoder/bin/xp-gate-check +0 -0
  70. package/plugins/qoder/plugin.json +1 -1
  71. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  72. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  73. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  74. package/principles/AGENTS.md +3 -3
  75. package/principles/__tests__/many-exports-reexport.test.ts +40 -0
  76. package/principles/adapters/typescript.ts +61 -30
  77. package/principles/boy-scout.ts +22 -17
  78. package/scripts/delphi-external-review.cjs +195 -132
  79. package/skills/delphi-review/AGENTS.md +3 -3
  80. package/skills/sprint-flow/AGENTS.md +3 -3
  81. package/skills/test-specification-alignment/AGENTS.md +3 -3
  82. package/sprint-gate.sh +0 -0
  83. package/plugins/dsh/lib/command.d.ts +0 -32
  84. package/plugins/dsh/lib/command.d.ts.map +0 -1
  85. package/plugins/dsh/lib/command.js +0 -81
  86. package/plugins/dsh/lib/delphi-config.d.ts +0 -75
  87. package/plugins/dsh/lib/delphi-config.d.ts.map +0 -1
  88. package/plugins/dsh/lib/delphi-config.js +0 -183
  89. package/plugins/dsh/lib/delphi-run.d.ts +0 -24
  90. package/plugins/dsh/lib/delphi-run.d.ts.map +0 -1
  91. package/plugins/dsh/lib/delphi-run.js +0 -92
  92. package/plugins/dsh/lib/delphi.d.ts +0 -88
  93. package/plugins/dsh/lib/delphi.d.ts.map +0 -1
  94. package/plugins/dsh/lib/delphi.js +0 -161
  95. package/plugins/dsh/lib/dialect.d.ts +0 -24
  96. package/plugins/dsh/lib/dialect.d.ts.map +0 -1
  97. package/plugins/dsh/lib/dialect.js +0 -36
  98. package/plugins/dsh/lib/gate-runner.d.ts +0 -18
  99. package/plugins/dsh/lib/gate-runner.d.ts.map +0 -1
  100. package/plugins/dsh/lib/gate-runner.js +0 -46
  101. package/plugins/dsh/lib/index.d.ts +0 -5
  102. package/plugins/dsh/lib/index.d.ts.map +0 -1
  103. package/plugins/dsh/lib/index.js +0 -162
  104. package/plugins/dsh/node_modules/.vite/vitest/results.json +0 -1
@@ -1,183 +0,0 @@
1
- /**
2
- * Delphi configuration loading for the DSH plugin.
3
- *
4
- * Reads the same `.delphi-config.json` the xp-gate skill already uses, so a
5
- * project configures its reviewers once and both the skill and the DSH tool
6
- * agree on who they are.
7
- *
8
- * @test REQ-DSH-016
9
- * @intent 复用项目既有的 .delphi-config.json,把 provider/api_key/专家席位解析成
10
- * DSH 工具可用的结构,并对 ${ENV} 占位符与缺失密钥给出可操作的报错。
11
- * @covers AC-DSH-016-02
12
- */
13
- import { readFileSync } from "node:fs";
14
- import { homedir } from "node:os";
15
- import { join, resolve } from "node:path";
16
- import { validateSeats } from "./delphi.js";
17
- /** Raised when config on disk cannot be turned into a usable review setup. */
18
- export class DelphiSetupError extends Error {
19
- }
20
- /**
21
- * Expand `${VAR}` and `{env:VAR}` placeholders from the environment.
22
- * Supports the two spellings already present in xp-gate config files.
23
- */
24
- export function expandEnv(value, env = process.env) {
25
- return value
26
- .replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => env[name] ?? "")
27
- .replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => env[name] ?? "");
28
- }
29
- /** Pick the active profile, falling back to the top level. */
30
- function selectProfile(cfg) {
31
- const active = cfg.active_profile;
32
- if (active && cfg.profiles?.[active])
33
- return cfg.profiles[active];
34
- if (cfg.profiles && Object.keys(cfg.profiles).length > 0) {
35
- throw new DelphiSetupError(`No active_profile set. Available: ${Object.keys(cfg.profiles).join(", ")}`);
36
- }
37
- return cfg;
38
- }
39
- function seatFromRaw(raw, index, fallbackRole) {
40
- const model = raw.model ?? raw.requested_model ?? "";
41
- return {
42
- id: raw.id ?? raw.name ?? `Expert ${String.fromCharCode(65 + index)}`,
43
- role: raw.role ?? fallbackRole,
44
- model,
45
- maxTokens: raw.max_tokens ?? raw.maxTokens,
46
- };
47
- }
48
- /** Normalize either experts shape into an ordered list with roles attached. */
49
- function normalizeExperts(experts) {
50
- if (!experts)
51
- return [];
52
- if (Array.isArray(experts)) {
53
- return experts.map((raw, i) => ({ raw, role: raw.role ?? `expert${i}` }));
54
- }
55
- // Object form: the KEY is the role, which is how xp-gate writes it.
56
- return Object.entries(experts).map(([role, raw]) => ({ raw, role }));
57
- }
58
- /** Resolve every referenced provider's base_url + api_key, failing loudly. */
59
- function resolveProviders(profile, needed, env) {
60
- if (needed.size === 0) {
61
- throw new DelphiSetupError('No provider configured. Set "provider" on the profile or on each expert.');
62
- }
63
- const providerTable = profile.providers ?? {};
64
- const resolved = new Map();
65
- for (const name of needed) {
66
- const entry = providerTable[name];
67
- if (!entry) {
68
- throw new DelphiSetupError(`Provider "${name}" is not defined under "providers". ` +
69
- `Known: ${Object.keys(providerTable).join(", ") || "(none)"}`);
70
- }
71
- const baseUrl = entry.base_url ?? entry.baseUrl ?? "";
72
- if (!baseUrl)
73
- throw new DelphiSetupError(`Provider "${name}" has no base_url.`);
74
- const rawKey = entry.api_key ?? entry.apiKey ?? "";
75
- const apiKey = expandEnv(rawKey, env);
76
- if (!apiKey) {
77
- // Name the variable rather than echoing config, which may hold a literal key.
78
- const varName = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/.exec(rawKey)?.[1] ??
79
- /\{env:([A-Za-z_][A-Za-z0-9_]*)\}/.exec(rawKey)?.[1];
80
- throw new DelphiSetupError(varName
81
- ? `API key variable ${varName} is not set in the environment.`
82
- : `Provider "${name}" has no api_key.`);
83
- }
84
- resolved.set(name, { baseUrl, apiKey });
85
- }
86
- return resolved;
87
- }
88
- /**
89
- * Turn raw config into a ready-to-run review setup.
90
- * Fails loudly and specifically: a missing key or an unexpanded placeholder is
91
- * the most common way a Delphi run silently degrades.
92
- */
93
- export function resolveConfig(cfg, env = process.env) {
94
- const profile = selectProfile(cfg);
95
- const normalized = normalizeExperts(profile.experts);
96
- // An expert may name its own provider; otherwise there must be a profile-wide
97
- // default. Resolve base_url/api_key per provider so mixed setups work.
98
- const providerNames = new Set(normalized.map((e) => e.raw.provider ?? profile.provider).filter((p) => !!p));
99
- const resolvedProviders = resolveProviders(profile, providerNames, env);
100
- const seats = normalized.map((e, i) => seatFromRaw(e.raw, i, e.role));
101
- // Surface an unexpanded placeholder as a config error, not a provider 404.
102
- for (const s of seats) {
103
- if (s.model.includes("${") || s.model.includes("{env:")) {
104
- throw new DelphiSetupError(`Expert "${s.id}" model "${s.model}" is an unexpanded placeholder.`);
105
- }
106
- }
107
- validateSeats(seats);
108
- return buildResult(profile, normalized, resolvedProviders, seats, cfg);
109
- }
110
- /** Assemble the final result after seats and providers validated. */
111
- function buildResult(profile, normalized, resolvedProviders, seats, cfg) {
112
- // All seats share one gateway in the supported setup; require agreement so a
113
- // mixed list cannot silently send one seat's key to another host.
114
- const chosen = resolvedProviders.get(normalized[0].raw.provider ?? profile.provider ?? "");
115
- if (!chosen)
116
- throw new DelphiSetupError("Unable to resolve a provider for the first expert.");
117
- for (const e of normalized) {
118
- const name = e.raw.provider ?? profile.provider ?? "";
119
- const other = resolvedProviders.get(name);
120
- if (!other || other.baseUrl !== chosen.baseUrl) {
121
- throw new DelphiSetupError(`All experts must share one gateway base_url; "${name}" differs. ` +
122
- `Split the review or align the providers.`);
123
- }
124
- }
125
- // consensus may sit on the profile or at the document root; both occur.
126
- const consensus = profile.consensus ?? cfg.consensus;
127
- return {
128
- seats,
129
- baseUrl: chosen.baseUrl,
130
- apiKey: chosen.apiKey,
131
- thresholdPercent: consensus?.threshold_percent ?? 90,
132
- maxRounds: consensus?.max_review_rounds ?? 5,
133
- provider: normalized[0].raw.provider ?? profile.provider ?? "",
134
- };
135
- }
136
- /** Read and resolve `.delphi-config.json` from a project root. */
137
- export function loadDelphiConfig(cwd, env = process.env, globalPath = join(homedir(), ".config", "xp-gate", ".delphi-config.json")) {
138
- // Global fallback (AC-DSH-016-03): the machine-wide config at
139
- // ~/.config/xp-gate/.delphi-config.json is the single source of truth for
140
- // every project. Without it, each repo needed its own copy — and reading a
141
- // stale or differently-priced config from another project's directory caused
142
- // real money loss (2026-10-03: an expensive overseas model burned the daily
143
- // quota). A project-local config still wins, so per-project overrides work.
144
- const path = resolve(cwd, ".delphi-config.json");
145
- let raw;
146
- try {
147
- raw = readFileSync(path, "utf8");
148
- }
149
- catch (projectError) {
150
- // Fall back ONLY when the project file is genuinely absent (ENOENT); any
151
- // other read error must surface rather than silently route this run to
152
- // another config. Same semantics as delphi-external-review.cjs, which
153
- // gates the fallback on existsSync (walkthrough R8 MA-01).
154
- if (projectError?.code !== "ENOENT") {
155
- throw new DelphiSetupError(`Cannot read ${path}: ${projectError.message}`);
156
- }
157
- try {
158
- raw = readFileSync(globalPath, "utf8");
159
- }
160
- catch {
161
- throw new DelphiSetupError(`No .delphi-config.json found at ${path} or the global location ${globalPath}.`);
162
- }
163
- }
164
- let parsed;
165
- try {
166
- parsed = JSON.parse(raw);
167
- }
168
- catch (e) {
169
- throw new DelphiSetupError(`.delphi-config.json is not valid JSON: ${e.message}`);
170
- }
171
- return resolveConfig(parsed, env);
172
- }
173
- /** Build the transport options a round needs from a resolved config. */
174
- export function toDelphiOptions(cfg) {
175
- return {
176
- baseUrl: cfg.baseUrl,
177
- apiKey: cfg.apiKey,
178
- seats: cfg.seats,
179
- thresholdPercent: cfg.thresholdPercent,
180
- maxRounds: cfg.maxRounds,
181
- };
182
- }
183
- //# sourceMappingURL=delphi-config.js.map
@@ -1,24 +0,0 @@
1
- /**
2
- * Delphi review orchestration: iterate rounds until consensus or the cap.
3
- *
4
- * @test REQ-DSH-016
5
- * @intent 编排多轮交叉评审:首轮匿名独立评审,后续轮次反馈上一轮汇总,
6
- * 直到达到共识阈值(默认 90%)或触及轮次上限(默认 5),
7
- * 并把结果整理成可写成 .code-walkthrough-result.json 的证据。
8
- * @covers AC-DSH-016-03
9
- */
10
- import type { DelphiOptions, RoundResult } from "./delphi.js";
11
- export interface ReviewOutcome {
12
- readonly rounds: readonly RoundResult[];
13
- readonly consensus: boolean;
14
- readonly finalRatio: number;
15
- readonly totalRounds: number;
16
- }
17
- /**
18
- * Run a full review. Round 1 is anonymous; each later round sees the aggregate
19
- * plus whatever the caller says was addressed, then re-votes.
20
- */
21
- export declare function runReview(opts: DelphiOptions, subject: string, addressed?: string): Promise<ReviewOutcome>;
22
- /** Human-readable transcript for the tool result. */
23
- export declare function formatOutcome(outcome: ReviewOutcome): string;
24
- //# sourceMappingURL=delphi-run.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"delphi-run.d.ts","sourceRoot":"","sources":["../src/delphi-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAc,WAAW,EAAE,MAAM,aAAa,CAAA;AAOzE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAA;IACvC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AA0CD;;;GAGG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,MAAM,EACf,SAAS,SAAK,GACb,OAAO,CAAC,aAAa,CAAC,CAoCxB;AAED,qDAAqD;AACrD,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAiB5D"}
@@ -1,92 +0,0 @@
1
- /**
2
- * Delphi review orchestration: iterate rounds until consensus or the cap.
3
- *
4
- * @test REQ-DSH-016
5
- * @intent 编排多轮交叉评审:首轮匿名独立评审,后续轮次反馈上一轮汇总,
6
- * 直到达到共识阈值(默认 90%)或触及轮次上限(默认 5),
7
- * 并把结果整理成可写成 .code-walkthrough-result.json 的证据。
8
- * @covers AC-DSH-016-03
9
- */
10
- import { runRound } from "./delphi.js";
11
- /** Mirrors delphi.ts; declared here so orchestrators need not import internals. */
12
- const DEFAULT_THRESHOLD_PERCENT = 90;
13
- const DEFAULT_MAX_ROUNDS = 5;
14
- /** Anonymous first round: each seat answers the same question with no peers. */
15
- function roundOnePrompt(seat, subject) {
16
- return (`You are the ${seat.role.toUpperCase()} expert. Review the material below INDEPENDENTLY. ` +
17
- `Do not assume other reviewers exist or agree with you.\n\n${subject}\n\n` +
18
- `Respond with <=300 words under the headings: "### Findings" (a markdown table of ` +
19
- `ID | Severity | Description | Location | Fix, or a single NONE row), then "### VERDICT" ` +
20
- `followed by exactly APPROVED or REQUEST_CHANGES, then "### CONFIDENCE" followed by an ` +
21
- `integer 1-10. Approve only if you would personally ship this.`);
22
- }
23
- /**
24
- * Later rounds: feed back the aggregate so seats can revise their position.
25
- * Individual reports stay anonymous -- only the tally and the addressed items
26
- * are shared, which is what keeps the method honest.
27
- */
28
- function laterRoundPrompt(seat, subject, previous, addressed) {
29
- const tally = previous.verdicts
30
- .map((v) => `- ${v.role}: ${v.verdict}`)
31
- .join("\n");
32
- return (`You are the ${seat.role.toUpperCase()} expert. This is ROUND ${previous.round + 1} of a ` +
33
- `Delphi consensus review. Individual peer reports are anonymous; only this aggregate is shared:\n\n` +
34
- `${tally}\nApprovals: ${previous.approvals}/${previous.validCount} ` +
35
- `(threshold ${DEFAULT_THRESHOLD_PERCENT}%).\n\n` +
36
- `${addressed ? `What changed since the last round:\n${addressed}\n\n` : ""}` +
37
- `Re-examine the material and revise or hold your position.\n\n${subject}\n\n` +
38
- `Respond with <=300 words: "### Round assessment" (2-3 sentences), "### Remaining blocking ` +
39
- `issues" (table, or a single NONE row), "### VERDICT" followed by exactly APPROVED or ` +
40
- `REQUEST_CHANGES, then "### CONFIDENCE" and an integer 1-10.`);
41
- }
42
- /**
43
- * Run a full review. Round 1 is anonymous; each later round sees the aggregate
44
- * plus whatever the caller says was addressed, then re-votes.
45
- */
46
- export async function runReview(opts, subject, addressed = "") {
47
- const threshold = opts.thresholdPercent ?? DEFAULT_THRESHOLD_PERCENT;
48
- const maxRounds = opts.maxRounds ?? DEFAULT_MAX_ROUNDS;
49
- const rounds = [];
50
- for (let n = 1; n <= maxRounds; n++) {
51
- const previous = rounds[rounds.length - 1];
52
- const result = previous
53
- ? await runRound(opts, n, "You are a rigorous senior code reviewer.", (seat) => laterRoundPrompt(seat, subject, previous, addressed))
54
- : await runRound(opts, 1, "You are a rigorous senior code reviewer.", (seat) => roundOnePrompt(seat, subject));
55
- rounds.push(result);
56
- // Threshold is a percentage of the whole panel, so a round with a malformed
57
- // report cannot reach consensus even if every valid seat approved.
58
- if (result.approvals / opts.seats.length * 100 >= threshold) {
59
- return {
60
- rounds,
61
- consensus: true,
62
- finalRatio: result.approvals / opts.seats.length,
63
- totalRounds: n,
64
- };
65
- }
66
- }
67
- const last = rounds[rounds.length - 1];
68
- return {
69
- rounds,
70
- consensus: false,
71
- finalRatio: last ? last.approvals / opts.seats.length : 0,
72
- totalRounds: rounds.length,
73
- };
74
- }
75
- /** Human-readable transcript for the tool result. */
76
- export function formatOutcome(outcome) {
77
- const lines = [];
78
- for (const r of outcome.rounds) {
79
- lines.push(`## Round ${r.round} — approvals ${r.approvals}/${r.validCount} valid`);
80
- for (const v of r.verdicts) {
81
- const model = v.resolvedModel ? `${v.requestedModel} -> ${v.resolvedModel}` : v.requestedModel;
82
- const note = v.error ? ` (${v.error})` : "";
83
- lines.push(`- [${v.role}] ${v.id} (${model}): ${v.verdict} confidence=${v.confidence}${note}`);
84
- }
85
- lines.push("");
86
- }
87
- lines.push(outcome.consensus
88
- ? `CONSENSUS REACHED: ${(outcome.finalRatio * 100).toFixed(0)}% after ${outcome.totalRounds} round(s).`
89
- : `NO CONSENSUS: ${(outcome.finalRatio * 100).toFixed(0)}% after ${outcome.totalRounds} round(s) (cap reached).`);
90
- return lines.join("\n");
91
- }
92
- //# sourceMappingURL=delphi-run.js.map
@@ -1,88 +0,0 @@
1
- /**
2
- * Delphi cross-review over an external OpenAI-compatible provider.
3
- *
4
- * This is the codified form of the manual run that produced PR #456: three
5
- * DISTINCT models, each asked the same question independently and anonymously,
6
- * iterated until the approval ratio clears the consensus threshold.
7
- *
8
- * Why a plugin tool rather than a shell one-liner: the DSH tool plane is where
9
- * a session actually lives, so the mechanism has to be callable from there.
10
- * DSH's own `tool-subagent` cannot serve this purpose -- subagents inherit one
11
- * host-configured model, so they cannot guarantee three distinct models, and
12
- * Delphi's whole value rests on that distinctness.
13
- *
14
- * @test REQ-DSH-016
15
- * @intent 用外部 provider 的多个不同模型做匿名交叉评审并判定共识,
16
- * 把「多模型交叉评审」固化成 DSH 可调用的机制而非一次性脚本。
17
- * @covers AC-DSH-016-01
18
- */
19
- /** One expert seat. `model` is the id sent to the provider. */
20
- export interface ExpertSeat {
21
- readonly id: string;
22
- readonly role: string;
23
- readonly model: string;
24
- /** Reasoning models burn hidden tokens, so seats need independent budgets. */
25
- readonly maxTokens?: number;
26
- }
27
- /** A single expert's parsed verdict for one round. */
28
- export interface ExpertVerdict {
29
- readonly id: string;
30
- readonly role: string;
31
- readonly requestedModel: string;
32
- readonly resolvedModel: string | null;
33
- readonly verdict: "APPROVED" | "REQUEST_CHANGES" | "INVALID_NO_VERDICT" | "ERROR";
34
- readonly confidence: number;
35
- /** Only well-formed verdicts may count toward consensus. */
36
- readonly counted: boolean;
37
- readonly report: string;
38
- readonly error?: string;
39
- }
40
- /** Aggregated outcome of one round. */
41
- export interface RoundResult {
42
- readonly round: number;
43
- readonly verdicts: readonly ExpertVerdict[];
44
- readonly validCount: number;
45
- readonly approvals: number;
46
- readonly consensusRatio: number;
47
- readonly reachedConsensus: boolean;
48
- }
49
- export interface DelphiOptions {
50
- /** OpenAI-compatible base URL, e.g. https://host/gpt-proxy */
51
- readonly baseUrl: string;
52
- /** Bearer token. Callers read this from the environment; never hardcode it. */
53
- readonly apiKey: string;
54
- readonly seats: readonly ExpertSeat[];
55
- /** Percent, 0-100. Delphi default is 90. */
56
- readonly thresholdPercent?: number;
57
- readonly maxRounds?: number;
58
- /** Per-request ceiling; the caller owns the overall timeout. */
59
- readonly timeoutMs?: number;
60
- readonly fetchImpl?: typeof fetch;
61
- }
62
- /** Raised when the configured seats cannot satisfy the Delphi invariants. */
63
- export declare class DelphiConfigError extends Error {
64
- }
65
- /**
66
- * Reject a seat configuration that cannot produce a valid Delphi review.
67
- * Checked BEFORE any network call so a misconfiguration never looks like a
68
- * provider failure.
69
- */
70
- export declare function validateSeats(seats: readonly ExpertSeat[]): void;
71
- /**
72
- * Parse an expert report into a verdict.
73
- *
74
- * The verdict token usually sits on the line AFTER a "### VERDICT" heading, so
75
- * a same-line `VERDICT: X` pattern misses every real report. A report with no
76
- * recognisable verdict is INVALID, never a guess: inferring approval from prose
77
- * would let the gate pass on an assumption.
78
- */
79
- export declare function parseVerdict(text: string): {
80
- verdict: ExpertVerdict["verdict"];
81
- confidence: number;
82
- };
83
- /**
84
- * Run ONE round. All seats are asked concurrently and never see each other's
85
- * output, which is what makes the round anonymous.
86
- */
87
- export declare function runRound(opts: DelphiOptions, round: number, system: string, buildPrompt: (seat: ExpertSeat) => string): Promise<RoundResult>;
88
- //# sourceMappingURL=delphi.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"delphi.d.ts","sourceRoot":"","sources":["../src/delphi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,sDAAsD;AACtD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,iBAAiB,GAAG,oBAAoB,GAAG,OAAO,CAAA;IACjF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,uCAAuC;AACvC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAA;IAC3C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAA;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAA;IACrC,4CAA4C;IAC5C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,gEAAgE;IAChE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CAClC;AASD,6EAA6E;AAC7E,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAE/C;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI,CAoBhE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG;IAC1C,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC,CAAA;IACjC,UAAU,EAAE,MAAM,CAAA;CACnB,CAWA;AA6DD;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,aAAa,EACnB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,MAAM,GACxC,OAAO,CAAC,WAAW,CAAC,CAmDtB"}
@@ -1,161 +0,0 @@
1
- /**
2
- * Delphi cross-review over an external OpenAI-compatible provider.
3
- *
4
- * This is the codified form of the manual run that produced PR #456: three
5
- * DISTINCT models, each asked the same question independently and anonymously,
6
- * iterated until the approval ratio clears the consensus threshold.
7
- *
8
- * Why a plugin tool rather than a shell one-liner: the DSH tool plane is where
9
- * a session actually lives, so the mechanism has to be callable from there.
10
- * DSH's own `tool-subagent` cannot serve this purpose -- subagents inherit one
11
- * host-configured model, so they cannot guarantee three distinct models, and
12
- * Delphi's whole value rests on that distinctness.
13
- *
14
- * @test REQ-DSH-016
15
- * @intent 用外部 provider 的多个不同模型做匿名交叉评审并判定共识,
16
- * 把「多模型交叉评审」固化成 DSH 可调用的机制而非一次性脚本。
17
- * @covers AC-DSH-016-01
18
- */
19
- /** Delphi's defining invariant: exactly three seats and a 90% approval bar. */
20
- const REQUIRED_EXPERTS = 3;
21
- const DEFAULT_THRESHOLD_PERCENT = 90;
22
- const DEFAULT_MAX_TOKENS = 8000;
23
- const DEFAULT_TIMEOUT_MS = 180_000;
24
- /** Raised when the configured seats cannot satisfy the Delphi invariants. */
25
- export class DelphiConfigError extends Error {
26
- }
27
- /**
28
- * Reject a seat configuration that cannot produce a valid Delphi review.
29
- * Checked BEFORE any network call so a misconfiguration never looks like a
30
- * provider failure.
31
- */
32
- export function validateSeats(seats) {
33
- if (seats.length !== REQUIRED_EXPERTS) {
34
- throw new DelphiConfigError(`Delphi requires exactly ${REQUIRED_EXPERTS} experts, got ${seats.length}.`);
35
- }
36
- for (const s of seats) {
37
- if (!s.model || s.model.trim() === "") {
38
- throw new DelphiConfigError(`Expert "${s.id}" has no model (a local fallback cannot count).`);
39
- }
40
- if (s.model.includes("${")) {
41
- throw new DelphiConfigError(`Expert "${s.id}" model "${s.model}" looks like an unexpanded env var.`);
42
- }
43
- }
44
- const distinct = new Set(seats.map((s) => s.model.trim()));
45
- if (distinct.size !== REQUIRED_EXPERTS) {
46
- throw new DelphiConfigError(`All ${REQUIRED_EXPERTS} experts must use distinct models; got: ${[...distinct].join(", ")}.`);
47
- }
48
- }
49
- /**
50
- * Parse an expert report into a verdict.
51
- *
52
- * The verdict token usually sits on the line AFTER a "### VERDICT" heading, so
53
- * a same-line `VERDICT: X` pattern misses every real report. A report with no
54
- * recognisable verdict is INVALID, never a guess: inferring approval from prose
55
- * would let the gate pass on an assumption.
56
- */
57
- export function parseVerdict(text) {
58
- const section = /VERDICT[\s\S]{0,120}?\b(APPROVED|REQUEST_CHANGES|PASS_WITH_CAVEATS|BLOCKED)\b/i.exec(text);
59
- if (!section)
60
- return { verdict: "INVALID_NO_VERDICT", confidence: 0 };
61
- const raw = section[1].toUpperCase();
62
- // PASS_WITH_CAVEATS / BLOCKED are not approvals under the 90% rule.
63
- const verdict = raw === "APPROVED" ? "APPROVED" : "REQUEST_CHANGES";
64
- const c = /CONFIDENCE[\s\S]{0,40}?(\d{1,2})/i.exec(text);
65
- const confidence = c ? Math.min(10, Math.max(0, Number(c[1]))) : 0;
66
- return { verdict, confidence };
67
- }
68
- /** One provider call. Throws on transport failure or empty content. */
69
- async function chatOnce(opts, seat, system, user) {
70
- const doFetch = opts.fetchImpl ?? fetch;
71
- const controller = new AbortController();
72
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
73
- try {
74
- const res = await doFetch(`${opts.baseUrl.replace(/\/+$/, "")}/v1/chat/completions`, {
75
- method: "POST",
76
- headers: {
77
- Authorization: `Bearer ${opts.apiKey}`,
78
- "Content-Type": "application/json",
79
- },
80
- body: JSON.stringify({
81
- model: seat.model,
82
- messages: [
83
- { role: "system", content: system },
84
- { role: "user", content: user },
85
- ],
86
- max_tokens: seat.maxTokens ?? DEFAULT_MAX_TOKENS,
87
- temperature: 0.2,
88
- }),
89
- signal: controller.signal,
90
- });
91
- if (!res.ok) {
92
- const body = await res.text().catch(() => "");
93
- throw new Error(`HTTP ${res.status}: ${body.slice(0, 200)}`);
94
- }
95
- const json = (await res.json());
96
- const content = json.choices?.[0]?.message?.content ?? "";
97
- const finishReason = json.choices?.[0]?.finish_reason ?? "unknown";
98
- // Empty content is a real failure -- typically a reasoning model that spent
99
- // its whole budget on hidden tokens. Never let it pass as an empty review.
100
- if (content.trim() === "") {
101
- throw new Error(`empty content (finish_reason=${finishReason}, max_tokens=${seat.maxTokens ?? DEFAULT_MAX_TOKENS}); ` +
102
- `raise max_tokens if this is a reasoning model`);
103
- }
104
- return { content, resolvedModel: json.model ?? null, finishReason };
105
- }
106
- finally {
107
- clearTimeout(timer);
108
- }
109
- }
110
- /**
111
- * Run ONE round. All seats are asked concurrently and never see each other's
112
- * output, which is what makes the round anonymous.
113
- */
114
- export async function runRound(opts, round, system, buildPrompt) {
115
- const settled = await Promise.allSettled(opts.seats.map(async (seat) => {
116
- const r = await chatOnce(opts, seat, system, buildPrompt(seat));
117
- return { seat, ...r };
118
- }));
119
- const verdicts = settled.map((s, i) => {
120
- const seat = opts.seats[i];
121
- if (s.status !== "fulfilled") {
122
- return {
123
- id: seat.id,
124
- role: seat.role,
125
- requestedModel: seat.model,
126
- resolvedModel: null,
127
- verdict: "ERROR",
128
- confidence: 0,
129
- counted: false,
130
- report: "",
131
- error: s.reason instanceof Error ? s.reason.message : String(s.reason),
132
- };
133
- }
134
- const { verdict, confidence } = parseVerdict(s.value.content);
135
- return {
136
- id: seat.id,
137
- role: seat.role,
138
- requestedModel: seat.model,
139
- resolvedModel: s.value.resolvedModel,
140
- verdict,
141
- confidence,
142
- counted: verdict !== "INVALID_NO_VERDICT",
143
- report: s.value.content,
144
- };
145
- });
146
- const counted = verdicts.filter((v) => v.counted);
147
- const approvals = counted.filter((v) => v.verdict === "APPROVED").length;
148
- // Consensus is measured over VALID responses: a malformed report is not a
149
- // dissent, but it cannot be counted as agreement either.
150
- const ratio = counted.length === 0 ? 0 : approvals / counted.length;
151
- const threshold = opts.thresholdPercent ?? DEFAULT_THRESHOLD_PERCENT;
152
- return {
153
- round,
154
- verdicts,
155
- validCount: counted.length,
156
- approvals,
157
- consensusRatio: ratio,
158
- reachedConsensus: counted.length === REQUIRED_EXPERTS && ratio * 100 >= threshold,
159
- };
160
- }
161
- //# sourceMappingURL=delphi.js.map
@@ -1,24 +0,0 @@
1
- /**
2
- * Shell dialect selection and quoting.
3
- *
4
- * DSH chooses its shell backend per platform: `dsh-base` disables
5
- * `bash-sandbox`/`tool-bash` and enables `pwsh-sandbox`/`tool-pwsh` on win32.
6
- * A command string written for the wrong dialect fails to parse, so callers
7
- * must pick the dialect to match the executor rather than assume POSIX.
8
- */
9
- export type ShellDialect = "posix" | "powershell";
10
- /** Quote one argument for the target dialect. */
11
- export declare function shellQuote(value: string, dialect: ShellDialect): string;
12
- /**
13
- * POSIX single-quote shell escaping: `'` becomes `'\''`.
14
- * @deprecated Use {@link shellQuote} with an explicit dialect.
15
- */
16
- export declare function shq(value: string): string;
17
- /**
18
- * Pick the dialect for the current platform.
19
- *
20
- * `process.platform` is the reliable signal available inside a tool body; it
21
- * mirrors exactly where DSH enables each sandbox/executor pair.
22
- */
23
- export declare function detectDialect(platform?: NodeJS.Platform): ShellDialect;
24
- //# sourceMappingURL=dialect.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../src/dialect.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,YAAY,CAAA;AAEjD,iDAAiD;AACjD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,CASvE;AAED;;;GAGG;AACH,wBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,YAAY,CAExF"}
@@ -1,36 +0,0 @@
1
- /**
2
- * Shell dialect selection and quoting.
3
- *
4
- * DSH chooses its shell backend per platform: `dsh-base` disables
5
- * `bash-sandbox`/`tool-bash` and enables `pwsh-sandbox`/`tool-pwsh` on win32.
6
- * A command string written for the wrong dialect fails to parse, so callers
7
- * must pick the dialect to match the executor rather than assume POSIX.
8
- */
9
- /** Quote one argument for the target dialect. */
10
- export function shellQuote(value, dialect) {
11
- if (dialect === "posix") {
12
- // POSIX single-quote escaping: `'` becomes `'\''`.
13
- return `'${value.replace(/'/g, "'\\''")}'`;
14
- }
15
- // PowerShell single-quoted strings escape an embedded quote by doubling it.
16
- // Backticks and `$(…)` are literal inside single quotes, so no further
17
- // escaping is needed to keep them inert.
18
- return `'${value.replace(/'/g, "''")}'`;
19
- }
20
- /**
21
- * POSIX single-quote shell escaping: `'` becomes `'\''`.
22
- * @deprecated Use {@link shellQuote} with an explicit dialect.
23
- */
24
- export function shq(value) {
25
- return shellQuote(value, "posix");
26
- }
27
- /**
28
- * Pick the dialect for the current platform.
29
- *
30
- * `process.platform` is the reliable signal available inside a tool body; it
31
- * mirrors exactly where DSH enables each sandbox/executor pair.
32
- */
33
- export function detectDialect(platform = process.platform) {
34
- return platform === "win32" ? "powershell" : "posix";
35
- }
36
- //# sourceMappingURL=dialect.js.map
@@ -1,18 +0,0 @@
1
- import type { ShellExecRequest, ShellExecSpec, ShellRunResult } from "@deepseek-ai/dsh-shell";
2
- /**
3
- * Structural view of `ctx.shell` so the runner is testable with a fake shell
4
- * without importing the abstract `ShellExecutor` service.
5
- */
6
- export interface XpGateShell {
7
- resolve(request: ShellExecRequest): ShellExecSpec;
8
- run(spec: ShellExecSpec): Promise<ShellRunResult>;
9
- }
10
- /** Render a completed shell run as a single model-facing text block. */
11
- export declare function renderGateResult(result: ShellRunResult): string;
12
- /**
13
- * Run an xp-gate command through the harness shell seam. Nonzero exits, timeouts,
14
- * and sandbox denials resolve (and render) rather than reject; only cancellation
15
- * (the caller's AbortSignal fired first) throws an AbortError with code ABORTED.
16
- */
17
- export declare function runXpGate(shell: XpGateShell, signal: AbortSignal, command: string, workdir: string, timeoutMs: number): Promise<string>;
18
- //# sourceMappingURL=gate-runner.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gate-runner.d.ts","sourceRoot":"","sources":["../src/gate-runner.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAE7F;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,aAAa,CAAA;IACjD,GAAG,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;CAClD;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAqB/D;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CASjB"}