@boyingliu01/xp-gate 0.32.0 → 0.32.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.
Files changed (49) hide show
  1. package/adapters/powershell.sh +33 -7
  2. package/gate-3.sh +54 -5
  3. package/hooks/gate-3.sh +54 -5
  4. package/hooks/lib/jscpd-run.sh +92 -0
  5. package/hooks/pre-commit +111 -56
  6. package/lib/mattpocock-license-verdict.json +1 -1
  7. package/mock-policy/AGENTS.md +3 -3
  8. package/mutation/AGENTS.md +3 -3
  9. package/package.json +1 -1
  10. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  11. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  12. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  13. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  14. package/plugins/dsh/README.md +51 -2
  15. package/plugins/dsh/client/client.js +760 -0
  16. package/plugins/dsh/lib/client.js +760 -0
  17. package/plugins/dsh/lib/delphi-settings.d.ts +52 -0
  18. package/plugins/dsh/lib/delphi-settings.d.ts.map +1 -0
  19. package/plugins/dsh/lib/delphi-settings.js +76 -0
  20. package/plugins/dsh/lib/hooks.d.ts +14 -1
  21. package/plugins/dsh/lib/hooks.d.ts.map +1 -1
  22. package/plugins/dsh/lib/hooks.js +44 -5
  23. package/plugins/dsh/lib/index.d.ts +63 -0
  24. package/plugins/dsh/lib/index.d.ts.map +1 -1
  25. package/plugins/dsh/lib/index.js +126 -21
  26. package/plugins/dsh/package.json +13 -3
  27. package/plugins/dsh/scripts/copy-client.cjs +32 -0
  28. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  29. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  30. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  31. package/plugins/dsh/src/client.test.ts +231 -0
  32. package/plugins/dsh/src/delphi-settings.test.ts +101 -0
  33. package/plugins/dsh/src/delphi-settings.ts +111 -0
  34. package/plugins/dsh/src/hooks.test.ts +48 -0
  35. package/plugins/dsh/src/hooks.ts +45 -5
  36. package/plugins/dsh/src/index.test.ts +3 -2
  37. package/plugins/dsh/src/index.ts +147 -25
  38. package/plugins/opencode/package.json +1 -1
  39. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  40. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  41. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  42. package/plugins/qoder/plugin.json +1 -1
  43. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  44. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  45. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  46. package/principles/AGENTS.md +3 -3
  47. package/skills/delphi-review/AGENTS.md +3 -3
  48. package/skills/sprint-flow/AGENTS.md +3 -3
  49. package/skills/test-specification-alignment/AGENTS.md +3 -3
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Delphi seats supplied through DSH's own settings document.
3
+ *
4
+ * `.delphi-config.json` remains the portable, cross-platform source that the skill,
5
+ * the CI validator, and the other plugins share. This module adds a SECOND source —
6
+ * the `delphi` section of the plugin's Config, editable from the Plugins page — and
7
+ * the rules that keep the two from disagreeing in a way that costs money:
8
+ *
9
+ * - the settings layer, when it has any seats, WINS (it is the more recent edit)
10
+ * - it must still satisfy every Delphi invariant (`validateSeats`) before a single
11
+ * request is made, because a form is a convenience and the host is the authority
12
+ * - an absent settings layer resolves to `undefined`, so an existing JSON user is
13
+ * never silently migrated to an empty panel
14
+ *
15
+ * @test REQ-DSH-017 Delphi seats are configurable from the DSH settings document
16
+ * @intent 把 DSH settings 里的 delphi 段解析成与 JSON 同构的席位,并在这条新路径上
17
+ * 重新执行三席/三模型互异/非占位符等不变量,避免表单绕过校验。
18
+ * @covers AC-DSH-017-01 AC-DSH-017-02 AC-DSH-017-03
19
+ */
20
+ import type { ExpertSeat } from "./delphi.js";
21
+ /** One seat as the settings document stores it. */
22
+ export interface DelphiSettingsSeat {
23
+ role: string;
24
+ model: string;
25
+ maxTokens?: number;
26
+ }
27
+ /** The `delphi` section of the plugin Config. */
28
+ export interface DelphiSettingsLayer {
29
+ baseUrl?: string;
30
+ apiKey?: string;
31
+ seats?: DelphiSettingsSeat[];
32
+ thresholdPercent?: number;
33
+ maxRounds?: number;
34
+ }
35
+ /** What {@link seatsFromSettings} produces: seats plus the transport they need. */
36
+ export interface SettingsReviewSetup {
37
+ readonly seats: readonly ExpertSeat[];
38
+ readonly baseUrl: string;
39
+ /** The still-unexpanded `api_key` expression; the caller resolves it. */
40
+ readonly apiKeyExpression: string;
41
+ readonly thresholdPercent: number;
42
+ readonly maxRounds: number;
43
+ }
44
+ /**
45
+ * Turn the settings layer into a review setup, or `undefined` when it holds no seats.
46
+ *
47
+ * Throws the same errors the JSON path throws, so a caller can report either source
48
+ * through one handler. `validateSeats` runs before any network call, which is what
49
+ * makes a misconfigured panel a configuration error rather than a provider failure.
50
+ */
51
+ export declare function seatsFromSettings(layer: DelphiSettingsLayer | undefined): SettingsReviewSetup | undefined;
52
+ //# sourceMappingURL=delphi-settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delphi-settings.d.ts","sourceRoot":"","sources":["../src/delphi-settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAI7C,mDAAmD;AACnD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,iDAAiD;AACjD,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAA;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,mFAAmF;AACnF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAA;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,yEAAyE;IACzE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AA6BD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,mBAAmB,GAAG,SAAS,GAAG,mBAAmB,GAAG,SAAS,CA0BzG"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Delphi seats supplied through DSH's own settings document.
3
+ *
4
+ * `.delphi-config.json` remains the portable, cross-platform source that the skill,
5
+ * the CI validator, and the other plugins share. This module adds a SECOND source —
6
+ * the `delphi` section of the plugin's Config, editable from the Plugins page — and
7
+ * the rules that keep the two from disagreeing in a way that costs money:
8
+ *
9
+ * - the settings layer, when it has any seats, WINS (it is the more recent edit)
10
+ * - it must still satisfy every Delphi invariant (`validateSeats`) before a single
11
+ * request is made, because a form is a convenience and the host is the authority
12
+ * - an absent settings layer resolves to `undefined`, so an existing JSON user is
13
+ * never silently migrated to an empty panel
14
+ *
15
+ * @test REQ-DSH-017 Delphi seats are configurable from the DSH settings document
16
+ * @intent 把 DSH settings 里的 delphi 段解析成与 JSON 同构的席位,并在这条新路径上
17
+ * 重新执行三席/三模型互异/非占位符等不变量,避免表单绕过校验。
18
+ * @covers AC-DSH-017-01 AC-DSH-017-02 AC-DSH-017-03
19
+ */
20
+ import { validateSeats } from "./delphi.js";
21
+ import { DelphiSetupError } from "./delphi-config.js";
22
+ /** Delphi's documented defaults, applied when the settings omit a consensus value. */
23
+ const DEFAULT_THRESHOLD_PERCENT = 90;
24
+ const DEFAULT_MAX_ROUNDS = 5;
25
+ /**
26
+ * Is the settings layer actually configured?
27
+ *
28
+ * Judged by seats alone: a half-filled form (a gateway typed but no seats yet) must
29
+ * not pre-empt the JSON file, or opening the page would quietly disable every
30
+ * existing `.delphi-config.json` review.
31
+ */
32
+ function isConfigured(layer) {
33
+ return Array.isArray(layer?.seats) && layer.seats.length > 0;
34
+ }
35
+ /** Reject a layer that names no gateway, rather than dialling a guess. */
36
+ function requireBaseUrl(layer) {
37
+ const baseUrl = (layer.baseUrl ?? "").trim();
38
+ if (baseUrl === "") {
39
+ throw new DelphiSetupError("The Delphi settings have seats but no base_url. Set the gateway URL, " +
40
+ "or clear the seats to fall back to .delphi-config.json.");
41
+ }
42
+ return baseUrl;
43
+ }
44
+ /**
45
+ * Turn the settings layer into a review setup, or `undefined` when it holds no seats.
46
+ *
47
+ * Throws the same errors the JSON path throws, so a caller can report either source
48
+ * through one handler. `validateSeats` runs before any network call, which is what
49
+ * makes a misconfigured panel a configuration error rather than a provider failure.
50
+ */
51
+ export function seatsFromSettings(layer) {
52
+ if (!isConfigured(layer))
53
+ return undefined;
54
+ // Narrowed by isConfigured; non-null assertions are avoided to keep strict TS honest.
55
+ const configured = layer;
56
+ const rawSeats = configured.seats ?? [];
57
+ const seats = rawSeats.map((seat, index) => ({
58
+ id: seat.role || `Expert ${String.fromCharCode(65 + index)}`,
59
+ role: seat.role || `expert${index}`,
60
+ // Trim: a model id pasted from a catalog picker can carry padding, and
61
+ // `" model-a"` is not the model that was intended.
62
+ model: (seat.model ?? "").trim(),
63
+ maxTokens: seat.maxTokens,
64
+ }));
65
+ // The invariants are the reason this module exists; they run before the
66
+ // transport is even built.
67
+ validateSeats(seats);
68
+ return {
69
+ seats,
70
+ baseUrl: requireBaseUrl(configured),
71
+ apiKeyExpression: configured.apiKey ?? "",
72
+ thresholdPercent: configured.thresholdPercent ?? DEFAULT_THRESHOLD_PERCENT,
73
+ maxRounds: configured.maxRounds ?? DEFAULT_MAX_ROUNDS,
74
+ };
75
+ }
76
+ //# sourceMappingURL=delphi-settings.js.map
@@ -23,8 +23,21 @@ export interface GuardDenial {
23
23
  readonly kind: "deny";
24
24
  readonly reason: string;
25
25
  }
26
- /** Does this tool modify files? */
26
+ /**
27
+ * Can this tool modify the workspace? Anything not proven read-only can.
28
+ *
29
+ * Used by the save-time principles check, which must fire only for the tools that
30
+ * actually write a source file — `bash` is excluded here even though the guard
31
+ * gates it, because the check needs a `file_path` argument to inspect.
32
+ */
27
33
  export declare function isEditTool(name: string): boolean;
34
+ /**
35
+ * Must this tool be gated by the sprint review guard?
36
+ *
37
+ * The inverse of {@link READ_ONLY_TOOLS}: an unrecognised tool is treated as
38
+ * capable of writing, which is what closes the `bash` hole.
39
+ */
40
+ export declare function mayModifyFiles(name: string): boolean;
28
41
  /**
29
42
  * Should a save-time check run for this path?
30
43
  *
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAYH,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAsBD,mCAAmC;AACnC,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAKrD;AAgBD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CA8B3F"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAoCH,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAsBD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAKrD;AAgBD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CA8B3F"}
@@ -20,8 +20,32 @@
20
20
  */
21
21
  import { existsSync, readFileSync } from "node:fs";
22
22
  import { join } from "node:path";
23
- /** Tools that modify files. Read-only tools are never gated. */
24
- const EDIT_TOOLS = new Set(["write", "edit", "str_replace_editor"]);
23
+ /**
24
+ * Tools that provably cannot modify a file. Everything else is gated.
25
+ *
26
+ * This is an ALLOWLIST on purpose. The guard used to name the write tools
27
+ * instead (`write`, `edit`, `str_replace_editor`), which let `bash` through —
28
+ * one shell command writes a file, so the guard was bypassable by the very tool
29
+ * a model reaches for first. A write-blacklist loses again on every new
30
+ * file-touching tool; a read-only allowlist loses only by being too strict,
31
+ * which surfaces as an over-eager denial rather than a silent hole.
32
+ */
33
+ const READ_ONLY_TOOLS = new Set([
34
+ "read",
35
+ "read_image",
36
+ "grep",
37
+ "glob",
38
+ "todo_write",
39
+ "ask_user_question",
40
+ "list_agents",
41
+ "present",
42
+ // XP-Gate's report tools only read: `xp-gate check/principles/arch` report to
43
+ // stdout, and the one file they ever write is a temp file list under the OS
44
+ // temp directory (gate-runner's --all path), never the workspace.
45
+ "gate-check",
46
+ "gate-principles",
47
+ "gate-arch",
48
+ ]);
25
49
  /** Where Sprint Flow records the review verdict. */
26
50
  const SPRINT_STATE_DIR = ".sprint-state";
27
51
  const VERDICT_FILE = "delphi-reviewed.json";
@@ -44,9 +68,24 @@ const CHECKABLE_EXTENSIONS = new Set([
44
68
  ".sh",
45
69
  ".ps1",
46
70
  ]);
47
- /** Does this tool modify files? */
71
+ /**
72
+ * Can this tool modify the workspace? Anything not proven read-only can.
73
+ *
74
+ * Used by the save-time principles check, which must fire only for the tools that
75
+ * actually write a source file — `bash` is excluded here even though the guard
76
+ * gates it, because the check needs a `file_path` argument to inspect.
77
+ */
48
78
  export function isEditTool(name) {
49
- return EDIT_TOOLS.has(name);
79
+ return name === "write" || name === "edit" || name === "str_replace_editor";
80
+ }
81
+ /**
82
+ * Must this tool be gated by the sprint review guard?
83
+ *
84
+ * The inverse of {@link READ_ONLY_TOOLS}: an unrecognised tool is treated as
85
+ * capable of writing, which is what closes the `bash` hole.
86
+ */
87
+ export function mayModifyFiles(name) {
88
+ return !READ_ONLY_TOOLS.has(name);
50
89
  }
51
90
  /**
52
91
  * Should a save-time check run for this path?
@@ -82,7 +121,7 @@ function readVerdict(root) {
82
121
  * Returns undefined to mean "allow" so the caller can `if (denial) return denial`.
83
122
  */
84
123
  export function delphiGuardDecision(root, toolName) {
85
- if (!isEditTool(toolName))
124
+ if (!mayModifyFiles(toolName))
86
125
  return undefined;
87
126
  if (!existsSync(join(root, SPRINT_STATE_DIR)))
88
127
  return undefined;
@@ -8,13 +8,76 @@ export declare const inject: string[];
8
8
  * Exported as a schema, not just a type: DSH validates against it and surfaces the
9
9
  * keys in the profile schema, so a user can discover and set them. A bare interface
10
10
  * is invisible to the harness.
11
+ *
12
+ * The `delphi` block is `.volatile()`: the settings page edits it live, and DSH
13
+ * refuses to write a path that is not beneath a volatile node. The two hook
14
+ * switches stay ordinary fields — they are read at mount and remounting a hook is
15
+ * what "off" is supposed to require.
11
16
  */
12
17
  export declare const Config: z<Schemastery.ObjectS<NoInfer<{
13
18
  principlesCheck: z<"auto" | "off", "auto" | "off", "defined">;
14
19
  delphiGuard: z<"auto" | "off", "auto" | "off", "defined">;
20
+ delphi: z<NoInfer<Schemastery.ObjectS<NoInfer<{
21
+ baseUrl: z<string, string, "defined">;
22
+ apiKey: z<string, string, "defined">;
23
+ seats: z<({
24
+ role?: string | null | undefined;
25
+ model?: string | null | undefined;
26
+ maxTokens?: number | null | undefined;
27
+ } & import("@deepseek-ai/cosmokit").Dict)[], Schemastery.ObjectT<NoInfer<{
28
+ role: z<string, string, "defined">;
29
+ model: z<string, string, "defined">;
30
+ maxTokens: z<number, number, "plain">;
31
+ }>>[], "defined">;
32
+ thresholdPercent: z<number, number, "plain">;
33
+ maxRounds: z<number, number, "plain">;
34
+ }>>>, NoInfer<Schemastery.ObjectT<NoInfer<{
35
+ baseUrl: z<string, string, "defined">;
36
+ apiKey: z<string, string, "defined">;
37
+ seats: z<({
38
+ role?: string | null | undefined;
39
+ model?: string | null | undefined;
40
+ maxTokens?: number | null | undefined;
41
+ } & import("@deepseek-ai/cosmokit").Dict)[], Schemastery.ObjectT<NoInfer<{
42
+ role: z<string, string, "defined">;
43
+ model: z<string, string, "defined">;
44
+ maxTokens: z<number, number, "plain">;
45
+ }>>[], "defined">;
46
+ thresholdPercent: z<number, number, "plain">;
47
+ maxRounds: z<number, number, "plain">;
48
+ }>>>, "volatile">;
15
49
  }>>, Schemastery.ObjectT<NoInfer<{
16
50
  principlesCheck: z<"auto" | "off", "auto" | "off", "defined">;
17
51
  delphiGuard: z<"auto" | "off", "auto" | "off", "defined">;
52
+ delphi: z<NoInfer<Schemastery.ObjectS<NoInfer<{
53
+ baseUrl: z<string, string, "defined">;
54
+ apiKey: z<string, string, "defined">;
55
+ seats: z<({
56
+ role?: string | null | undefined;
57
+ model?: string | null | undefined;
58
+ maxTokens?: number | null | undefined;
59
+ } & import("@deepseek-ai/cosmokit").Dict)[], Schemastery.ObjectT<NoInfer<{
60
+ role: z<string, string, "defined">;
61
+ model: z<string, string, "defined">;
62
+ maxTokens: z<number, number, "plain">;
63
+ }>>[], "defined">;
64
+ thresholdPercent: z<number, number, "plain">;
65
+ maxRounds: z<number, number, "plain">;
66
+ }>>>, NoInfer<Schemastery.ObjectT<NoInfer<{
67
+ baseUrl: z<string, string, "defined">;
68
+ apiKey: z<string, string, "defined">;
69
+ seats: z<({
70
+ role?: string | null | undefined;
71
+ model?: string | null | undefined;
72
+ maxTokens?: number | null | undefined;
73
+ } & import("@deepseek-ai/cosmokit").Dict)[], Schemastery.ObjectT<NoInfer<{
74
+ role: z<string, string, "defined">;
75
+ model: z<string, string, "defined">;
76
+ maxTokens: z<number, number, "plain">;
77
+ }>>[], "defined">;
78
+ thresholdPercent: z<number, number, "plain">;
79
+ maxRounds: z<number, number, "plain">;
80
+ }>>>, "volatile">;
18
81
  }>>, "plain">;
19
82
  export declare function apply(ctx: Context, config?: unknown): void;
20
83
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,CAAC,MAAM,0BAA0B,CAAA;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAgBlD,eAAO,MAAM,IAAI,iBAAiB,CAAA;AAGlC,eAAO,MAAM,MAAM,UAA+B,CAAA;AA0TlD;;;;;;GAMG;AACH,eAAO,MAAM,MAAM;;;;;;aAajB,CAAA;AAEF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,OAAY,GAAG,IAAI,CA6B9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,CAAC,MAAM,0BAA0B,CAAA;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AA0BlD,eAAO,MAAM,IAAI,iBAAiB,CAAA;AAGlC,eAAO,MAAM,MAAM,UAA+B,CAAA;AAsZlD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aA4BjB,CAAA;AAEF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,OAAY,GAAG,IAAI,CA6B9D"}
@@ -5,7 +5,9 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
5
5
  import z from "@deepseek-ai/schemastery";
6
6
  import { GATE_WHITELIST, buildCommand, detectDialect, resolveTarget } from "./command.js";
7
7
  import { runXpGate } from "./gate-runner.js";
8
- import { DelphiSetupError, listCredentialReferences, loadDelphiConfig, toDelphiOptions } from "./delphi-config.js";
8
+ import { DelphiSetupError, expandEnvDetailed, listCredentialReferences, loadDelphiConfig, toDelphiOptions, } from "./delphi-config.js";
9
+ import { DelphiConfigError } from "./delphi.js";
10
+ import { seatsFromSettings } from "./delphi-settings.js";
9
11
  import { formatOutcome, runReview } from "./delphi-run.js";
10
12
  import { buildEvidence } from "./evidence.js";
11
13
  import { readGitContext } from "./git-context.js";
@@ -166,40 +168,110 @@ async function writeWalkthroughEvidence(cwd, cfg, outcome) {
166
168
  return `\nEvidence: not written — ${error.message}`;
167
169
  }
168
170
  }
171
+ /**
172
+ * Build the review options from whichever source is configured.
173
+ *
174
+ * The settings page wins when it holds seats: it is the more recent edit, and a
175
+ * user who just saved three models expects the next run to use them. When it holds
176
+ * none, `.delphi-config.json` applies exactly as before — which is what keeps every
177
+ * existing project working without opening the page.
178
+ *
179
+ * Both paths end in the same `validateSeats`, so neither can produce a panel the
180
+ * skill would reject.
181
+ */
182
+ async function reviewOptions(request, resolveApiKey) {
183
+ const fromSettings = seatsFromSettings(request.settings);
184
+ if (fromSettings) {
185
+ // The seat list is already validated; only the key still has to be resolved,
186
+ // and only the credential store (not this process's environment) knows it.
187
+ const key = await resolveExpressionKey(fromSettings.apiKeyExpression, resolveApiKey);
188
+ return {
189
+ options: {
190
+ baseUrl: fromSettings.baseUrl,
191
+ apiKey: key,
192
+ seats: fromSettings.seats,
193
+ thresholdPercent: fromSettings.thresholdPercent,
194
+ maxRounds: request.maxRounds ?? fromSettings.maxRounds,
195
+ },
196
+ provider: "settings",
197
+ described: fromSettings.seats.map((s) => s.model).join(", "),
198
+ };
199
+ }
200
+ // Resolve credentials BEFORE loading the config: the loader is synchronous and
201
+ // throws as soon as a referenced variable has no value. Reading the reference
202
+ // names first turns "the DSH process never inherited WHALECLOUD_API_KEY" from a
203
+ // hard failure into a lookup in the credential store.
204
+ const apiKeyOverrides = {};
205
+ for (const name of listCredentialReferences(request.cwd)) {
206
+ const value = await resolveApiKey(name);
207
+ if (value !== undefined && value.length > 0)
208
+ apiKeyOverrides[name] = value;
209
+ }
210
+ const cfg = loadDelphiConfig(request.cwd, process.env, undefined, apiKeyOverrides);
211
+ const options = toDelphiOptions(cfg);
212
+ return {
213
+ options: { ...options, maxRounds: request.maxRounds ?? cfg.maxRounds },
214
+ provider: cfg.provider,
215
+ described: cfg.seats.map((s) => s.model).join(", "),
216
+ cfg,
217
+ };
218
+ }
219
+ /**
220
+ * Resolve a settings-supplied `api_key` expression to a usable token.
221
+ *
222
+ * Accepts the same two placeholder spellings the JSON loader accepts. A literal
223
+ * value is rejected rather than used: a bearer token typed into a settings field is
224
+ * a credential written to a profile file in plain text.
225
+ */
226
+ async function resolveExpressionKey(expression, resolveApiKey) {
227
+ const placeholder = findPlaceholderName(expression);
228
+ if (placeholder === undefined) {
229
+ if (expression.trim() !== "") {
230
+ throw new DelphiSetupError("The Delphi settings hold a literal api key. Use a credential reference instead, " +
231
+ 'e.g. "${WHALECLOUD_API_KEY}", so the token never lands in the profile file.');
232
+ }
233
+ throw new DelphiSetupError('The Delphi settings have no api key. Set it to "${YOUR_KEY_VAR}".');
234
+ }
235
+ const fromStore = await resolveApiKey(placeholder);
236
+ if (fromStore !== undefined && fromStore.length > 0)
237
+ return fromStore;
238
+ const fromEnv = expandEnvDetailed(expression, process.env);
239
+ if (fromEnv.value.trim() !== "")
240
+ return fromEnv.value;
241
+ throw new DelphiSetupError(`API key variable ${placeholder} has no value. Set it, or store it in DSH's credential store.`);
242
+ }
243
+ /** The first `${VAR}` / `{env:VAR}` name in a config value, or undefined. */
244
+ function findPlaceholderName(value) {
245
+ const shell = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/.exec(value);
246
+ if (shell)
247
+ return shell[1];
248
+ const envStyle = /\{env:([A-Za-z_][A-Za-z0-9_]*)\}/.exec(value);
249
+ return envStyle ? envStyle[1] : undefined;
250
+ }
169
251
  async function runDelphiReview(subject, request) {
170
- const { addressed, maxRounds, cwd, resolveApiKey, signal, writeEvidence } = request;
252
+ const { addressed, cwd, resolveApiKey, signal, writeEvidence } = request;
171
253
  try {
172
- // Resolve credentials BEFORE loading the config: the loader is synchronous and
173
- // throws as soon as a referenced variable has no value. Reading the reference
174
- // names first turns "the DSH process never inherited WHALECLOUD_API_KEY" from a
175
- // hard failure into a lookup in the credential store.
176
- const apiKeyOverrides = {};
177
- for (const name of listCredentialReferences(cwd)) {
178
- const value = await resolveApiKey(name);
179
- if (value !== undefined && value.length > 0)
180
- apiKeyOverrides[name] = value;
181
- }
182
- const cfg = loadDelphiConfig(cwd, process.env, undefined, apiKeyOverrides);
183
- const options = toDelphiOptions(cfg);
184
- const outcome = await runReview({ ...options, maxRounds: maxRounds ?? cfg.maxRounds }, subject, addressed, signal);
185
- const seats = cfg.seats.map((s) => s.model).join(", ");
186
- const suffix = writeEvidence
254
+ const { options, provider, described, cfg } = await reviewOptions(request, resolveApiKey);
255
+ const outcome = await runReview(options, subject, addressed, signal);
256
+ const suffix = writeEvidence && cfg !== undefined
187
257
  ? await writeWalkthroughEvidence(cwd, cfg, outcome)
188
258
  : "";
189
- return `${formatOutcome(outcome)}\n\nProvider: ${cfg.provider}\nSeats: ${seats}${suffix}`;
259
+ return `${formatOutcome(outcome)}\n\nProvider: ${provider}\nSeats: ${described}${suffix}`;
190
260
  }
191
261
  catch (e) {
192
262
  // Config problems are the common failure; report them as guidance
193
263
  // rather than a stack trace.
194
264
  if (e instanceof DelphiSetupError)
195
265
  return `Delphi review not started: ${e.message}`;
266
+ if (e instanceof DelphiConfigError)
267
+ return `Delphi review not started: ${e.message}`;
196
268
  throw e;
197
269
  }
198
270
  }
199
- function delphiReviewTool(resolveApiKey) {
271
+ function delphiReviewTool(resolveApiKey, settings) {
200
272
  return defineTool({
201
273
  name: "delphi-review",
202
- description: "Run a Delphi cross-review of a subject (a diff, a design, a spec) using three DISTINCT models from an external OpenAI-compatible provider configured in .delphi-config.json. Round 1 is anonymous and concurrent; later rounds feed back the aggregate until the approval ratio reaches the configured threshold (default 90%) or the round cap (default 5). Use this whenever a change needs independent multi-model review rather than a single model's opinion.",
274
+ description: "Run a Delphi cross-review of a subject (a diff, a design, a spec) using three DISTINCT models from an external OpenAI-compatible provider. The seats come from the plugin's settings page (Plugins -> xp-gate -> Configure) when it holds any, otherwise from .delphi-config.json. Round 1 is anonymous and concurrent; later rounds feed back the aggregate until the approval ratio reaches the configured threshold (default 90%) or the round cap (default 5). Use this whenever a change needs independent multi-model review rather than a single model's opinion.",
203
275
  parameters: {
204
276
  subject: {
205
277
  type: "string",
@@ -237,6 +309,7 @@ function delphiReviewTool(resolveApiKey) {
237
309
  resolveApiKey,
238
310
  signal: exec.signal,
239
311
  writeEvidence: args.writeEvidence ?? false,
312
+ settings,
240
313
  });
241
314
  },
242
315
  });
@@ -285,12 +358,31 @@ function sprintTool(shell, dialect) {
285
358
  },
286
359
  });
287
360
  }
361
+ /**
362
+ * One Delphi seat, as the settings page stores it.
363
+ *
364
+ * `model` is a free string rather than an enum: the catalog the page offers comes
365
+ * from the Host's configured providers, which this schema cannot enumerate, and a
366
+ * model id that is not in the catalog yet must still be accepted so an operator can
367
+ * pin a gateway alias. The invariants that matter are checked by
368
+ * `seatsFromSettings` on every run, not by the schema.
369
+ */
370
+ const DelphiSeatSchema = z.object({
371
+ role: z.string().default(""),
372
+ model: z.string().default("").description("Model id sent to the gateway, e.g. g-glm-5.3-flash."),
373
+ maxTokens: z.number().min(0).description("Per-seat token budget; raise it for reasoning models."),
374
+ });
288
375
  /**
289
376
  * Runtime schema for the keys above.
290
377
  *
291
378
  * Exported as a schema, not just a type: DSH validates against it and surfaces the
292
379
  * keys in the profile schema, so a user can discover and set them. A bare interface
293
380
  * is invisible to the harness.
381
+ *
382
+ * The `delphi` block is `.volatile()`: the settings page edits it live, and DSH
383
+ * refuses to write a path that is not beneath a volatile node. The two hook
384
+ * switches stay ordinary fields — they are read at mount and remounting a hook is
385
+ * what "off" is supposed to require.
294
386
  */
295
387
  export const Config = z.object({
296
388
  principlesCheck: z
@@ -301,6 +393,19 @@ export const Config = z.object({
301
393
  .union([z.const("auto"), z.const("off")])
302
394
  .default("auto")
303
395
  .description("Sprint write guard: auto = deny edits until delphi-review is APPROVED; off = do not register the hook."),
396
+ delphi: z
397
+ .object({
398
+ baseUrl: z.string().default("").description("OpenAI-compatible gateway, e.g. https://host/gpt-proxy."),
399
+ apiKey: z
400
+ .string()
401
+ .default("")
402
+ .description("Credential reference such as ${WHALECLOUD_API_KEY}; never a literal key."),
403
+ seats: z.array(DelphiSeatSchema).default([]).description("The three Delphi reviewers."),
404
+ thresholdPercent: z.number().min(0).max(100).description("Approval bar; 3 seats at 90% means 3/3."),
405
+ maxRounds: z.number().min(1).max(20).description("Round cap before the review reports NO CONSENSUS."),
406
+ })
407
+ .volatile()
408
+ .description("Delphi seats edited on the Plugins page. Leave seats empty to use .delphi-config.json."),
304
409
  });
305
410
  export function apply(ctx, config = {}) {
306
411
  const cfg = config;
@@ -321,7 +426,7 @@ export function apply(ctx, config = {}) {
321
426
  ctx.tools.register(gateCheckTool(shell, dialect));
322
427
  ctx.tools.register(gatePrinciplesTool(shell, dialect));
323
428
  ctx.tools.register(gateArchTool(shell, dialect));
324
- ctx.tools.register(delphiReviewTool(resolveApiKey));
429
+ ctx.tools.register(delphiReviewTool(resolveApiKey, cfg.delphi));
325
430
  ctx.tools.register(sprintTool(shell, dialect));
326
431
  // Publishing the 12 bundled skills is what makes them visible to the model.
327
432
  // They ship in the tarball either way, but without a provider no discovery
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/dsh-plugin-xp-gate",
3
- "version": "0.32.0",
3
+ "version": "0.32.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",
@@ -11,7 +11,10 @@
11
11
  "import": "./lib/index.js",
12
12
  "default": "./lib/index.js"
13
13
  },
14
- "./package.json": "./package.json"
14
+ "./package.json": "./package.json",
15
+ "./client": {
16
+ "default": "./lib/client.js"
17
+ }
15
18
  },
16
19
  "repository": {
17
20
  "type": "git",
@@ -21,6 +24,13 @@
21
24
  "dsh": {
22
25
  "bundle": {
23
26
  "patch": "./cordis.patch.yml"
27
+ },
28
+ "client": {
29
+ "inject": [
30
+ "@deepseek-ai/dsh-client-locale",
31
+ "@deepseek-ai/dsh-client-ui-settings"
32
+ ],
33
+ "platform": "web"
24
34
  }
25
35
  },
26
36
  "publishConfig": {
@@ -34,7 +44,7 @@
34
44
  "README.md"
35
45
  ],
36
46
  "scripts": {
37
- "build": "tsc -p tsconfig.build.json",
47
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-client.cjs",
38
48
  "check": "tsc -p tsconfig.json",
39
49
  "test": "vitest run",
40
50
  "prepare-skills": "node scripts/prepack.cjs",
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * copy-client.cjs — put the hand-authored browser bundle where the package ships it.
6
+ *
7
+ * `client/client.js` is written by hand in the DSH client-module format
8
+ * (`window.__ModuleLoader__.load`), so it must NOT pass through tsc: it is not a
9
+ * TypeScript module and it references a browser global. The build therefore
10
+ * compiles `src/` to `lib/` and this step adds `lib/client.js` alongside.
11
+ *
12
+ * Run by the `build` script, so `npm run build` and the release both produce the
13
+ * file the `dsh.client` declaration promises.
14
+ */
15
+
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+
19
+ const PLUGIN_ROOT = path.resolve(__dirname, '..');
20
+ const SRC = path.join(PLUGIN_ROOT, 'client', 'client.js');
21
+ const OUT = path.join(PLUGIN_ROOT, 'lib', 'client.js');
22
+
23
+ if (!fs.existsSync(SRC)) {
24
+ console.error(`[client] ERROR: browser bundle missing at ${SRC}`);
25
+ process.exit(1);
26
+ }
27
+
28
+ fs.mkdirSync(path.dirname(OUT), { recursive: true });
29
+ fs.copyFileSync(SRC, OUT);
30
+
31
+ const rel = (p) => path.relative(PLUGIN_ROOT, p).split(path.sep).join('/');
32
+ console.log(`[client] ${rel(SRC)} -> ${rel(OUT)}`);
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-28
4
- **Commit:** 0f58c300
3
+ **Generated:** 2026-10-30
4
+ **Commit:** 78786c3f
5
5
  **Branch:** main
6
- **Version:** 0.32.0.0
6
+ **Version:** 0.32.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.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-28
4
- **Commit:** 0f58c300
3
+ **Generated:** 2026-10-30
4
+ **Commit:** 78786c3f
5
5
  **Branch:** main
6
- **Version:** 0.32.0.0
6
+ **Version:** 0.32.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-28
4
- **Commit:** 0f58c300
3
+ **Generated:** 2026-10-30
4
+ **Commit:** 78786c3f
5
5
  **Branch:** main
6
- **Version:** 0.32.0.0
6
+ **Version:** 0.32.2.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.