@boyingliu01/xp-gate 0.31.4 → 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.
Files changed (144) hide show
  1. package/adapters/java.sh +256 -4
  2. package/adapters/powershell.sh +45 -6
  3. package/bin/xp-gate.js +8 -2
  4. package/build-integrity/__tests__/gate-10.test.ts +88 -1
  5. package/build-integrity/gate-10.ts +51 -16
  6. package/dsh.bundle.patch.yml +15 -0
  7. package/gate-13-compiler-strictness.sh +1031 -0
  8. package/gate-3.sh +48 -13
  9. package/hooks/gate-13-compiler-strictness.sh +1031 -0
  10. package/hooks/gate-3.sh +48 -13
  11. package/hooks/lib/coverage-total.sh +18 -2
  12. package/hooks/lib/java-test-selection.sh +251 -0
  13. package/hooks/lib/jscpd-run.sh +9 -0
  14. package/hooks/lib/validate-code-walkthrough.cjs +186 -29
  15. package/hooks/pre-commit +149 -2
  16. package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
  17. package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
  18. package/lib/__tests__/gate-runner.test.js +29 -2
  19. package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
  20. package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
  21. package/lib/__tests__/shared-utils.test.js +21 -0
  22. package/lib/check.js +6 -2
  23. package/lib/detect-deps.js +42 -10
  24. package/lib/doctor-tui.js +111 -21
  25. package/lib/gate-runner.js +111 -37
  26. package/lib/mattpocock-license-verdict.json +1 -1
  27. package/lib/principles.js +48 -6
  28. package/lib/shared-utils.js +8 -0
  29. package/mock-policy/AGENTS.md +3 -3
  30. package/mock-policy/gate-m3.ts +57 -23
  31. package/mutation/AGENTS.md +3 -3
  32. package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
  33. package/mutation/__tests__/test-filter-parity.test.ts +5 -2
  34. package/mutation/gate-m.ts +30 -24
  35. package/mutation/runners/mutmut-runner.ts +80 -30
  36. package/mutation/runners/pitest-runner.ts +60 -42
  37. package/package.json +14 -2
  38. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  39. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  40. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  41. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  42. package/plugins/dsh/lib/command.d.ts +38 -0
  43. package/plugins/dsh/lib/command.d.ts.map +1 -0
  44. package/plugins/dsh/lib/command.js +87 -0
  45. package/plugins/dsh/lib/credentials.d.ts +39 -0
  46. package/plugins/dsh/lib/credentials.d.ts.map +1 -0
  47. package/plugins/dsh/lib/credentials.js +57 -0
  48. package/plugins/dsh/lib/delphi-config.d.ts +129 -0
  49. package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
  50. package/plugins/dsh/lib/delphi-config.js +369 -0
  51. package/plugins/dsh/lib/delphi-run.d.ts +24 -0
  52. package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
  53. package/plugins/dsh/lib/delphi-run.js +113 -0
  54. package/plugins/dsh/lib/delphi.d.ts +94 -0
  55. package/plugins/dsh/lib/delphi.d.ts.map +1 -0
  56. package/plugins/dsh/lib/delphi.js +207 -0
  57. package/plugins/dsh/lib/dialect.d.ts +24 -0
  58. package/plugins/dsh/lib/dialect.d.ts.map +1 -0
  59. package/plugins/dsh/lib/dialect.js +36 -0
  60. package/plugins/dsh/lib/evidence.d.ts +70 -0
  61. package/plugins/dsh/lib/evidence.d.ts.map +1 -0
  62. package/plugins/dsh/lib/evidence.js +92 -0
  63. package/plugins/dsh/lib/gate-runner.d.ts +49 -0
  64. package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
  65. package/plugins/dsh/lib/gate-runner.js +95 -0
  66. package/plugins/dsh/lib/git-context.d.ts +26 -0
  67. package/plugins/dsh/lib/git-context.d.ts.map +1 -0
  68. package/plugins/dsh/lib/git-context.js +30 -0
  69. package/plugins/dsh/lib/hooks.d.ts +42 -0
  70. package/plugins/dsh/lib/hooks.d.ts.map +1 -0
  71. package/plugins/dsh/lib/hooks.js +113 -0
  72. package/plugins/dsh/lib/index.d.ts +20 -0
  73. package/plugins/dsh/lib/index.d.ts.map +1 -0
  74. package/plugins/dsh/lib/index.js +430 -0
  75. package/plugins/dsh/lib/skill-provider.d.ts +15 -0
  76. package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
  77. package/plugins/dsh/lib/skill-provider.js +205 -0
  78. package/plugins/dsh/lib/sprint.d.ts +60 -0
  79. package/plugins/dsh/lib/sprint.d.ts.map +1 -0
  80. package/plugins/dsh/lib/sprint.js +92 -0
  81. package/plugins/dsh/package.json +14 -8
  82. package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
  83. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  84. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  85. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  86. package/plugins/dsh/src/command-single-form.test.ts +67 -0
  87. package/plugins/dsh/src/command.test.ts +21 -17
  88. package/plugins/dsh/src/command.ts +19 -21
  89. package/plugins/dsh/src/compat.test.ts +200 -0
  90. package/plugins/dsh/src/contract.test.ts +7 -1
  91. package/plugins/dsh/src/credentials.test.ts +93 -0
  92. package/plugins/dsh/src/credentials.ts +71 -0
  93. package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
  94. package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
  95. package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
  96. package/plugins/dsh/src/delphi-config.ts +146 -53
  97. package/plugins/dsh/src/delphi-run.ts +27 -4
  98. package/plugins/dsh/src/delphi.ts +133 -65
  99. package/plugins/dsh/src/evidence.test.ts +74 -0
  100. package/plugins/dsh/src/evidence.ts +141 -0
  101. package/plugins/dsh/src/gate-runner.test.ts +14 -6
  102. package/plugins/dsh/src/gate-runner.ts +68 -5
  103. package/plugins/dsh/src/git-context.test.ts +39 -0
  104. package/plugins/dsh/src/git-context.ts +38 -0
  105. package/plugins/dsh/src/hooks.test.ts +87 -0
  106. package/plugins/dsh/src/hooks.ts +126 -0
  107. package/plugins/dsh/src/index.test.ts +63 -6
  108. package/plugins/dsh/src/index.ts +340 -13
  109. package/plugins/dsh/src/skill-provider.test.ts +194 -0
  110. package/plugins/dsh/src/skill-provider.ts +231 -0
  111. package/plugins/dsh/src/sprint.test.ts +64 -0
  112. package/plugins/dsh/src/sprint.ts +122 -0
  113. package/plugins/dsh/src/tri-state.test.ts +123 -0
  114. package/plugins/opencode/index.ts +192 -58
  115. package/plugins/opencode/package.json +1 -1
  116. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  117. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  118. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  119. package/plugins/qoder/plugin.json +1 -1
  120. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  121. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  122. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  123. package/principles/AGENTS.md +3 -3
  124. package/principles/__tests__/analyze-read-failure.test.ts +70 -0
  125. package/principles/__tests__/baseline-prune.test.ts +61 -0
  126. package/principles/__tests__/cli-files-separators.test.ts +69 -0
  127. package/principles/__tests__/config-merge-warnings.test.ts +148 -0
  128. package/principles/adapters/java.ts +22 -3
  129. package/principles/adapters/typescript.ts +70 -29
  130. package/principles/analyzer.ts +36 -3
  131. package/principles/baseline-prune.ts +19 -7
  132. package/principles/config.ts +56 -35
  133. package/principles/index.ts +122 -40
  134. package/principles/rules/clean-code/deep-nesting.ts +36 -22
  135. package/principles/rules/clean-code/unused-imports.ts +30 -19
  136. package/principles/rules/solid/dip.ts +30 -20
  137. package/principles/rules/solid/lsp.ts +17 -2
  138. package/principles/rules/solid/ocp.ts +33 -23
  139. package/principles/rules/solid/srp.ts +65 -23
  140. package/scripts/delphi-external-review.cjs +25 -2
  141. package/scripts/sync-package-content.js +28 -1
  142. package/skills/delphi-review/AGENTS.md +3 -3
  143. package/skills/sprint-flow/AGENTS.md +3 -3
  144. package/skills/test-specification-alignment/AGENTS.md +3 -3
@@ -0,0 +1,94 @@
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
+ /**
61
+ * Caller cancellation. Aborting it stops the in-flight request immediately and
62
+ * raises TOOL_ABORTED, matching the three gate tools. Without this a cancelled
63
+ * review kept spending money on requests the user had already abandoned.
64
+ */
65
+ readonly signal?: AbortSignal;
66
+ readonly fetchImpl?: typeof fetch;
67
+ }
68
+ /** Raised when the configured seats cannot satisfy the Delphi invariants. */
69
+ export declare class DelphiConfigError extends Error {
70
+ }
71
+ /**
72
+ * Reject a seat configuration that cannot produce a valid Delphi review.
73
+ * Checked BEFORE any network call so a misconfiguration never looks like a
74
+ * provider failure.
75
+ */
76
+ export declare function validateSeats(seats: readonly ExpertSeat[]): void;
77
+ /**
78
+ * Parse an expert report into a verdict.
79
+ *
80
+ * The verdict token usually sits on the line AFTER a "### VERDICT" heading, so
81
+ * a same-line `VERDICT: X` pattern misses every real report. A report with no
82
+ * recognisable verdict is INVALID, never a guess: inferring approval from prose
83
+ * would let the gate pass on an assumption.
84
+ */
85
+ export declare function parseVerdict(text: string): {
86
+ verdict: ExpertVerdict["verdict"];
87
+ confidence: number;
88
+ };
89
+ /**
90
+ * Run ONE round. All seats are asked concurrently and never see each other's
91
+ * output, which is what makes the round anonymous.
92
+ */
93
+ export declare function runRound(opts: DelphiOptions, round: number, system: string, buildPrompt: (seat: ExpertSeat) => string): Promise<RoundResult>;
94
+ //# sourceMappingURL=delphi.d.ts.map
@@ -0,0 +1 @@
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;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CAClC;AAeD,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;AA0ID;;;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,CA8BtB"}
@@ -0,0 +1,207 @@
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
+ /**
25
+ * Sampling temperature. Low on purpose: reviewers must apply the rubric the same
26
+ * way every round, and creativity in a verdict is noise, not signal.
27
+ */
28
+ const REVIEW_TEMPERATURE = 0.2;
29
+ /** Raised when the configured seats cannot satisfy the Delphi invariants. */
30
+ export class DelphiConfigError extends Error {
31
+ }
32
+ /**
33
+ * Reject a seat configuration that cannot produce a valid Delphi review.
34
+ * Checked BEFORE any network call so a misconfiguration never looks like a
35
+ * provider failure.
36
+ */
37
+ export function validateSeats(seats) {
38
+ if (seats.length !== REQUIRED_EXPERTS) {
39
+ throw new DelphiConfigError(`Delphi requires exactly ${REQUIRED_EXPERTS} experts, got ${seats.length}.`);
40
+ }
41
+ for (const s of seats) {
42
+ if (!s.model || s.model.trim() === "") {
43
+ throw new DelphiConfigError(`Expert "${s.id}" has no model (a local fallback cannot count).`);
44
+ }
45
+ if (s.model.includes("${")) {
46
+ throw new DelphiConfigError(`Expert "${s.id}" model "${s.model}" looks like an unexpanded env var.`);
47
+ }
48
+ }
49
+ const distinct = new Set(seats.map((s) => s.model.trim()));
50
+ if (distinct.size !== REQUIRED_EXPERTS) {
51
+ throw new DelphiConfigError(`All ${REQUIRED_EXPERTS} experts must use distinct models; got: ${[...distinct].join(", ")}.`);
52
+ }
53
+ }
54
+ /**
55
+ * Parse an expert report into a verdict.
56
+ *
57
+ * The verdict token usually sits on the line AFTER a "### VERDICT" heading, so
58
+ * a same-line `VERDICT: X` pattern misses every real report. A report with no
59
+ * recognisable verdict is INVALID, never a guess: inferring approval from prose
60
+ * would let the gate pass on an assumption.
61
+ */
62
+ export function parseVerdict(text) {
63
+ const section = /VERDICT[\s\S]{0,120}?\b(APPROVED|REQUEST_CHANGES|PASS_WITH_CAVEATS|BLOCKED)\b/i.exec(text);
64
+ if (!section)
65
+ return { verdict: "INVALID_NO_VERDICT", confidence: 0 };
66
+ const raw = section[1].toUpperCase();
67
+ // PASS_WITH_CAVEATS / BLOCKED are not approvals under the 90% rule.
68
+ const verdict = raw === "APPROVED" ? "APPROVED" : "REQUEST_CHANGES";
69
+ const c = /CONFIDENCE[\s\S]{0,40}?(\d{1,2})/i.exec(text);
70
+ const confidence = c ? Math.min(10, Math.max(0, Number(c[1]))) : 0;
71
+ return { verdict, confidence };
72
+ }
73
+ /** Cleanup for a case where no listener was attached, so there is nothing to undo. */
74
+ const NO_CLEANUP = () => undefined;
75
+ /**
76
+ * Bridge caller cancellation into the controller that also owns the timeout.
77
+ * Bridging rather than racing two signals means one abort path, so the reason a
78
+ * request stopped is never ambiguous.
79
+ */
80
+ function linkAbort(controller, callerSignal) {
81
+ if (!callerSignal)
82
+ return NO_CLEANUP;
83
+ const forwardAbort = () => controller.abort();
84
+ if (callerSignal.aborted) {
85
+ controller.abort();
86
+ return NO_CLEANUP;
87
+ }
88
+ callerSignal.addEventListener("abort", forwardAbort, { once: true });
89
+ return () => callerSignal.removeEventListener("abort", forwardAbort);
90
+ }
91
+ /** Extract the reviewer's text, or explain why there isn't any. */
92
+ function readContent(json, maxTokens) {
93
+ const content = json.choices?.[0]?.message?.content ?? "";
94
+ const finishReason = json.choices?.[0]?.finish_reason ?? "unknown";
95
+ // Empty content is a real failure -- typically a reasoning model that spent its
96
+ // whole budget on hidden tokens. Never let it pass as an empty review.
97
+ if (content.trim() === "") {
98
+ throw new Error(`empty content (finish_reason=${finishReason}, max_tokens=${maxTokens}); ` +
99
+ `raise max_tokens if this is a reasoning model`);
100
+ }
101
+ return { content, resolvedModel: json.model ?? null, finishReason };
102
+ }
103
+ /** Reject a non-2xx answer, keeping the body short enough to read in a verdict. */
104
+ async function ensureOk(res) {
105
+ if (res.ok)
106
+ return;
107
+ const body = await res.text().catch(() => "");
108
+ throw new Error(`HTTP ${res.status}: ${body.slice(0, 200)}`);
109
+ }
110
+ /** Build the chat-completions request for one seat. */
111
+ function buildRequest(opts, seat, system, user, maxTokens, signal) {
112
+ return {
113
+ method: "POST",
114
+ headers: {
115
+ Authorization: `Bearer ${opts.apiKey}`,
116
+ "Content-Type": "application/json",
117
+ },
118
+ body: JSON.stringify({
119
+ model: seat.model,
120
+ messages: [
121
+ { role: "system", content: system },
122
+ { role: "user", content: user },
123
+ ],
124
+ max_tokens: maxTokens,
125
+ temperature: REVIEW_TEMPERATURE,
126
+ }),
127
+ signal,
128
+ };
129
+ }
130
+ /** One provider call. Throws on transport failure or empty content. */
131
+ async function chatOnce(opts, seat, system, user) {
132
+ const doFetch = opts.fetchImpl ?? fetch;
133
+ const controller = new AbortController();
134
+ const maxTokens = seat.maxTokens ?? DEFAULT_MAX_TOKENS;
135
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
136
+ const unlink = linkAbort(controller, opts.signal);
137
+ const url = `${opts.baseUrl.replace(/\/+$/, "")}/v1/chat/completions`;
138
+ try {
139
+ const request = buildRequest(opts, seat, system, user, maxTokens, controller.signal);
140
+ const res = await doFetch(url, request);
141
+ await ensureOk(res);
142
+ return readContent((await res.json()), maxTokens);
143
+ }
144
+ finally {
145
+ clearTimeout(timer);
146
+ unlink();
147
+ }
148
+ }
149
+ /** A seat that failed to answer. Its silence is never counted as agreement. */
150
+ function failedVerdict(seat, reason) {
151
+ return {
152
+ id: seat.id,
153
+ role: seat.role,
154
+ requestedModel: seat.model,
155
+ resolvedModel: null,
156
+ verdict: "ERROR",
157
+ confidence: 0,
158
+ counted: false,
159
+ report: "",
160
+ error: reason instanceof Error ? reason.message : String(reason),
161
+ };
162
+ }
163
+ /** A seat that answered: parse its verdict and keep the report as evidence. */
164
+ function answeredVerdict(seat, answer) {
165
+ const { verdict, confidence } = parseVerdict(answer.content);
166
+ return {
167
+ id: seat.id,
168
+ role: seat.role,
169
+ requestedModel: seat.model,
170
+ resolvedModel: answer.resolvedModel,
171
+ verdict,
172
+ confidence,
173
+ counted: verdict !== "INVALID_NO_VERDICT",
174
+ report: answer.content,
175
+ };
176
+ }
177
+ /**
178
+ * Run ONE round. All seats are asked concurrently and never see each other's
179
+ * output, which is what makes the round anonymous.
180
+ */
181
+ export async function runRound(opts, round, system, buildPrompt) {
182
+ const settled = await Promise.allSettled(opts.seats.map(async (seat) => {
183
+ const r = await chatOnce(opts, seat, system, buildPrompt(seat));
184
+ return { seat, ...r };
185
+ }));
186
+ const verdicts = settled.map((s, i) => {
187
+ const seat = opts.seats[i];
188
+ return s.status === "fulfilled"
189
+ ? answeredVerdict(seat, s.value)
190
+ : failedVerdict(seat, s.reason);
191
+ });
192
+ const counted = verdicts.filter((v) => v.counted);
193
+ const approvals = counted.filter((v) => v.verdict === "APPROVED").length;
194
+ // Consensus is measured over VALID responses: a malformed report is not a
195
+ // dissent, but it cannot be counted as agreement either.
196
+ const ratio = counted.length === 0 ? 0 : approvals / counted.length;
197
+ const threshold = opts.thresholdPercent ?? DEFAULT_THRESHOLD_PERCENT;
198
+ return {
199
+ round,
200
+ verdicts,
201
+ validCount: counted.length,
202
+ approvals,
203
+ consensusRatio: ratio,
204
+ reachedConsensus: counted.length === REQUIRED_EXPERTS && ratio * 100 >= threshold,
205
+ };
206
+ }
207
+ //# sourceMappingURL=delphi.js.map
@@ -0,0 +1,24 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,36 @@
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
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Gate MW walkthrough evidence.
3
+ *
4
+ * @test REQ-DSH-012 delphi-review can produce Gate MW evidence
5
+ * @intent Build the .code-walkthrough-result.json document that the pre-push
6
+ * validator accepts, from a review the plugin actually ran.
7
+ * @covers AC-DSH-012-01
8
+ *
9
+ * Every field here mirrors a check in githooks/lib/validate-code-walkthrough.cjs.
10
+ * Two of those checks are easy to misread and are pinned by test:
11
+ *
12
+ * - result_type belongs to each EXPERT record. Putting it at the top level looks
13
+ * right, passes no check, and fails with a message that never mentions placement.
14
+ * - requested_model comes from the configured seat, never from the gateway's
15
+ * response. The gateway answers with its own naming (Qwen3_8-flash-next for a seat
16
+ * configured as local-qwen3.8-flash), so using the response tripped #574.
17
+ */
18
+ /** The document Gate MW reads. Field names are the validator's, not ours. */
19
+ export interface WalkthroughEvidence {
20
+ readonly commit: string;
21
+ readonly branch: string;
22
+ readonly timestamp: string;
23
+ readonly expires: string;
24
+ readonly consensus_ratio: number;
25
+ readonly verdict: "APPROVED";
26
+ readonly experts: readonly EvidenceExpert[];
27
+ }
28
+ export interface EvidenceExpert {
29
+ readonly role: string;
30
+ readonly requested_model: string;
31
+ readonly resolved_model: string;
32
+ /** Per-expert, not top-level. The validator reads it here. */
33
+ readonly result_type: "delphi_expert_result";
34
+ readonly channel: "external" | "local";
35
+ readonly verdict: "APPROVED";
36
+ }
37
+ /** One seat as observed by the review: what we asked for, and what answered. */
38
+ export interface SeatOutcome {
39
+ readonly role: string;
40
+ /** The model configured for this role. Becomes requested_model. */
41
+ readonly model: string;
42
+ /** The model the gateway reported. Becomes resolved_model. */
43
+ readonly resolved: string;
44
+ }
45
+ export interface BuildEvidenceInput {
46
+ readonly head: string;
47
+ readonly branch: string;
48
+ readonly seats: readonly SeatOutcome[];
49
+ readonly verdict: string;
50
+ readonly now: Date;
51
+ /** Gateway URL, used to label provenance. */
52
+ readonly baseUrl?: string;
53
+ /** Observed agreement. Defaults to unanimous, which is what a 3-seat APPROVED run is. */
54
+ readonly consensusRatio?: number;
55
+ }
56
+ /**
57
+ * Label a gateway as local or external.
58
+ *
59
+ * Only loopback counts as local: the validator uses this field as provenance, so a
60
+ * private-network host is still external — nothing about it is verifiable from here.
61
+ */
62
+ export declare function evidenceChannel(baseUrl: string): "local" | "external";
63
+ /**
64
+ * Build the evidence document.
65
+ *
66
+ * Refuses on anything the validator would reject rather than emitting a document
67
+ * that fails at push time, where the cause is much harder to see.
68
+ */
69
+ export declare function buildEvidence(input: BuildEvidenceInput): WalkthroughEvidence;
70
+ //# sourceMappingURL=evidence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../src/evidence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;IAC5B,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAA;CAC5C;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,EAAE,sBAAsB,CAAA;IAC5C,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAA;IACtC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAC7B;AAED,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAA;IAClB,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,yFAAyF;IACzF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CACjC;AAWD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,CAKrE;AAwBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,mBAAmB,CA8B5E"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Gate MW walkthrough evidence.
3
+ *
4
+ * @test REQ-DSH-012 delphi-review can produce Gate MW evidence
5
+ * @intent Build the .code-walkthrough-result.json document that the pre-push
6
+ * validator accepts, from a review the plugin actually ran.
7
+ * @covers AC-DSH-012-01
8
+ *
9
+ * Every field here mirrors a check in githooks/lib/validate-code-walkthrough.cjs.
10
+ * Two of those checks are easy to misread and are pinned by test:
11
+ *
12
+ * - result_type belongs to each EXPERT record. Putting it at the top level looks
13
+ * right, passes no check, and fails with a message that never mentions placement.
14
+ * - requested_model comes from the configured seat, never from the gateway's
15
+ * response. The gateway answers with its own naming (Qwen3_8-flash-next for a seat
16
+ * configured as local-qwen3.8-flash), so using the response tripped #574.
17
+ */
18
+ /** The validator requires exactly three seats. */
19
+ const REQUIRED_SEATS = 3;
20
+ /** Evidence is valid for exactly this long; the validator checks the delta. */
21
+ const VALIDITY_MS = 3_600_000;
22
+ /** The validator rejects anything below this. */
23
+ const MIN_CONSENSUS = 0.9;
24
+ /**
25
+ * Label a gateway as local or external.
26
+ *
27
+ * Only loopback counts as local: the validator uses this field as provenance, so a
28
+ * private-network host is still external — nothing about it is verifiable from here.
29
+ */
30
+ export function evidenceChannel(baseUrl) {
31
+ const lowered = baseUrl.toLowerCase();
32
+ return lowered.includes("localhost") || lowered.includes("127.0.0.1") || lowered.includes("[::1]")
33
+ ? "local"
34
+ : "external";
35
+ }
36
+ /** Refuse to emit a document the validator would reject, and say which field. */
37
+ function assertSeatsUsable(seats) {
38
+ if (seats.length !== REQUIRED_SEATS) {
39
+ throw new Error(`evidence needs exactly ${REQUIRED_SEATS} seats; got ${seats.length}`);
40
+ }
41
+ const requested = new Set();
42
+ const resolved = new Set();
43
+ for (const seat of seats) {
44
+ if (seat.resolved.trim() === "") {
45
+ throw new Error(`seat "${seat.role}" has no resolved_model; a failed seat cannot produce evidence`);
46
+ }
47
+ // Both are checked case-insensitively by some validator paths, so assert on the
48
+ // stricter reading: duplicated IDs mean one model answered twice.
49
+ if (requested.has(seat.model.trim()))
50
+ throw new Error(`duplicate requested_model "${seat.model}"`);
51
+ if (resolved.has(seat.resolved.trim().toLowerCase())) {
52
+ throw new Error(`duplicate resolved_model "${seat.resolved}"`);
53
+ }
54
+ requested.add(seat.model.trim());
55
+ resolved.add(seat.resolved.trim().toLowerCase());
56
+ }
57
+ }
58
+ /**
59
+ * Build the evidence document.
60
+ *
61
+ * Refuses on anything the validator would reject rather than emitting a document
62
+ * that fails at push time, where the cause is much harder to see.
63
+ */
64
+ export function buildEvidence(input) {
65
+ if (input.verdict !== "APPROVED") {
66
+ throw new Error(`evidence requires an APPROVED review; got ${input.verdict}`);
67
+ }
68
+ assertSeatsUsable(input.seats);
69
+ const ratio = input.consensusRatio ?? 1;
70
+ if (ratio < MIN_CONSENSUS || ratio > 1) {
71
+ throw new Error(`consensus_ratio must be ${MIN_CONSENSUS} through 1; got ${ratio}`);
72
+ }
73
+ const channel = evidenceChannel(input.baseUrl ?? "");
74
+ const expires = new Date(input.now.getTime() + VALIDITY_MS);
75
+ return {
76
+ commit: input.head,
77
+ branch: input.branch,
78
+ timestamp: input.now.toISOString(),
79
+ expires: expires.toISOString(),
80
+ consensus_ratio: ratio,
81
+ verdict: "APPROVED",
82
+ experts: input.seats.map((seat) => ({
83
+ role: seat.role,
84
+ requested_model: seat.model,
85
+ resolved_model: seat.resolved,
86
+ result_type: "delphi_expert_result",
87
+ channel,
88
+ verdict: "APPROVED",
89
+ })),
90
+ };
91
+ }
92
+ //# sourceMappingURL=evidence.js.map
@@ -0,0 +1,49 @@
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
+ * The shape tracks the 0.2.0-rc.2 seam: `execute()` returns a handle whose
7
+ * `result()` settles into the run result. Earlier 0.1.x exposed a single `run()`,
8
+ * which is why a stale devDependency made this compile while the real runtime
9
+ * had already moved on.
10
+ */
11
+ export interface XpGateShell {
12
+ resolve(request: ShellExecRequest): ShellExecSpec;
13
+ execute(spec: ShellExecSpec): Promise<{
14
+ result(): Promise<ShellRunResult>;
15
+ }>;
16
+ }
17
+ /** The three verdicts a gate run can produce. There is deliberately no fourth. */
18
+ export type GateVerdict = "PASS" | "GATE FAILED" | "INDETERMINATE";
19
+ /**
20
+ * Classify a completed shell run.
21
+ *
22
+ * The order below is load-bearing and must not be rearranged:
23
+ *
24
+ * 1. timeout / truncation -> INDETERMINATE (a killed or clipped run yields no verdict)
25
+ * 2. exit 0 with no stdout -> INDETERMINATE (the command succeeded but said nothing)
26
+ * 3. exit 0 with stdout -> PASS
27
+ * 4. otherwise -> GATE FAILED (nonzero with output) or INDETERMINATE (silent)
28
+ *
29
+ * Step 1 must precede step 3: a timed-out process can still report exit 0 from
30
+ * the state it held when the deadline killed it, so testing `exitCode === 0`
31
+ * first would render a timeout as a pass. That is the exact defect this project
32
+ * fixed in Gate 8 and Gate 10, and the plugin must not reintroduce it.
33
+ */
34
+ export declare function classifyGateRun(result: ShellRunResult): GateVerdict;
35
+ /**
36
+ * Render a completed shell run as model-facing text.
37
+ *
38
+ * Every run resolves to exactly one of three verdicts. A run that could not
39
+ * establish a verdict says INDETERMINATE and states explicitly that it is not a
40
+ * pass, because an empty output read as a green check is worse than no output.
41
+ */
42
+ export declare function renderGateResult(result: ShellRunResult): string;
43
+ /**
44
+ * Run an xp-gate command through the harness shell seam. Nonzero exits, timeouts,
45
+ * and sandbox denials resolve (and render) rather than reject; only cancellation
46
+ * (the caller's AbortSignal fired first) throws an AbortError with code ABORTED.
47
+ */
48
+ export declare function runXpGate(shell: XpGateShell, signal: AbortSignal, command: string, workdir: string, timeoutMs: number): Promise<string>;
49
+ //# sourceMappingURL=gate-runner.d.ts.map
@@ -0,0 +1 @@
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;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,aAAa,CAAA;IACjD,OAAO,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC;QAAE,MAAM,IAAI,OAAO,CAAC,cAAc,CAAC,CAAA;KAAE,CAAC,CAAA;CAC7E;AAED,kFAAkF;AAClF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,aAAa,GAAG,eAAe,CAAA;AAElE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,WAAW,CAUnE;AA+BD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAa/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,CAUjB"}