@boyingliu01/xp-gate 0.20.2 → 0.22.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 (44) hide show
  1. package/adapters/gate-9.sh +142 -60
  2. package/adapters/python.sh +73 -6
  3. package/bin/xp-gate.js +2 -2
  4. package/gate-10.sh +107 -64
  5. package/gate-9.sh +142 -59
  6. package/hooks/gate-10.sh +107 -64
  7. package/hooks/gate-9.sh +142 -59
  8. package/hooks/lib/coverage-total.sh +72 -0
  9. package/hooks/pre-commit +128 -44
  10. package/hooks/pre-push +32 -0
  11. package/lib/__tests__/arch.test.js +403 -0
  12. package/lib/__tests__/doctor-469-hook-env.test.js +200 -0
  13. package/lib/arch.js +336 -49
  14. package/lib/doctor.js +319 -13
  15. package/lib/mattpocock-license-verdict.json +1 -1
  16. package/mock-policy/AGENTS.md +4 -4
  17. package/mutation/AGENTS.md +4 -4
  18. package/package.json +1 -1
  19. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  20. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  21. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  22. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  23. package/plugins/dsh/package.json +1 -1
  24. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  25. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  26. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  27. package/plugins/dsh/src/command.test.ts +14 -2
  28. package/plugins/dsh/src/command.ts +8 -1
  29. package/plugins/dsh/src/index.test.ts +43 -26
  30. package/plugins/dsh/src/index.ts +164 -138
  31. package/plugins/opencode/README.md +5 -2
  32. package/plugins/opencode/index.ts +215 -164
  33. package/plugins/opencode/package.json +1 -1
  34. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  35. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  36. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  37. package/plugins/qoder/plugin.json +1 -1
  38. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  39. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  40. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  41. package/principles/AGENTS.md +4 -4
  42. package/skills/delphi-review/AGENTS.md +4 -4
  43. package/skills/sprint-flow/AGENTS.md +4 -4
  44. package/skills/test-specification-alignment/AGENTS.md +4 -4
@@ -8,34 +8,50 @@ interface CapturedTool {
8
8
  execute: (args: unknown, exec: unknown) => Promise<unknown>
9
9
  }
10
10
 
11
- function makeCtx(stdoutText: string) {
12
- const captured: unknown[] = []
13
- const requests: ShellExecRequest[] = []
14
- const shell = {
11
+ // Boy Scout (#447): the inline shell mock pushed makeCtx to 5 brace levels.
12
+ // The spec/result builders keep every function at ≤4.
13
+ function makeSpec(req: ShellExecRequest): ShellExecSpec {
14
+ return {
15
+ command: req.command,
16
+ workdir: req.workdir ?? "/workspace",
17
+ timeoutMs: req.timeoutMs ?? 1000,
18
+ stdoutMaxBytes: 1_000_000,
19
+ signal: req.signal,
20
+ sandboxPolicy: undefined,
21
+ }
22
+ }
23
+
24
+ function makeRunResult(stdoutText: string): ShellRunResult {
25
+ return {
26
+ exitCode: 0,
27
+ signal: null,
28
+ timedOut: false,
29
+ aborted: false,
30
+ timeoutMs: 1000,
31
+ stdout: { text: stdoutText, truncated: false },
32
+ stderr: { text: "", truncated: false },
33
+ }
34
+ }
35
+
36
+ function makeShell(stdoutText: string, requests: ShellExecRequest[]) {
37
+ return {
15
38
  resolve(req: ShellExecRequest): ShellExecSpec {
16
39
  requests.push(req)
17
- return {
18
- command: req.command,
19
- workdir: req.workdir ?? "/workspace",
20
- timeoutMs: req.timeoutMs ?? 1000,
21
- stdoutMaxBytes: 1_000_000,
22
- signal: req.signal,
23
- sandboxPolicy: undefined,
24
- }
40
+ return makeSpec(req)
25
41
  },
26
42
  run(): Promise<ShellRunResult> {
27
- return Promise.resolve({
28
- exitCode: 0,
29
- signal: null,
30
- timedOut: false,
31
- aborted: false,
32
- timeoutMs: 1000,
33
- stdout: { text: stdoutText, truncated: false },
34
- stderr: { text: "", truncated: false },
35
- })
43
+ return Promise.resolve(makeRunResult(stdoutText))
36
44
  },
37
45
  }
38
- const ctx = { tools: { register: (def: unknown) => captured.push(def) }, shell } as unknown as Context
46
+ }
47
+
48
+ function makeCtx(stdoutText: string) {
49
+ const captured: unknown[] = []
50
+ const requests: ShellExecRequest[] = []
51
+ const ctx = {
52
+ tools: { register: (def: unknown) => captured.push(def) },
53
+ shell: makeShell(stdoutText, requests),
54
+ } as unknown as Context
39
55
  return { ctx, captured, requests }
40
56
  }
41
57
 
@@ -82,16 +98,17 @@ describe("apply", () => {
82
98
 
83
99
  /**
84
100
  * @test REQ-DSH-004
85
- * @intent 验证 gate-arch 工具执行生成 `xp-gate arch --config <config>`(缺省 architecture.yaml)
101
+ * @intent 验证 gate-arch 工具执行生成 `xp-gate arch`(默认不带 --config,#447),显式 config 走 --use-config
86
102
  * @covers AC-DSH-004-01
87
103
  */
88
- it("gate-arch execute builds `xp-gate arch` with default and custom config", async () => {
104
+ it("gate-arch execute builds `xp-gate arch` with built-in rules by default", async () => {
89
105
  const { ctx, captured, requests } = makeCtx("arch report")
90
106
  apply(ctx)
91
107
  const tool = captured.find((c) => (c as CapturedTool).name === "gate-arch") as CapturedTool
92
108
  await tool.execute({}, { signal: new AbortController().signal })
93
- expect(requests[0]?.command).toContain("xp-gate arch --config 'architecture.yaml'")
109
+ expect(requests[0]?.command).toContain("xp-gate arch")
110
+ expect(requests[0]?.command).not.toContain("--config")
94
111
  await tool.execute({ config: "custom.yaml" }, { signal: new AbortController().signal })
95
- expect(requests[1]?.command).toContain("xp-gate arch --config 'custom.yaml'")
112
+ expect(requests[1]?.command).toContain("xp-gate arch --use-config 'custom.yaml'")
96
113
  })
97
114
  })
@@ -1,5 +1,6 @@
1
1
  import { defineTool } from "@deepseek-ai/dsh-tools"
2
2
  import type { Context } from "@deepseek-ai/cordis"
3
+ import type { ShellDialect } from "./command.js"
3
4
  import { GATE_WHITELIST, buildCommand, detectDialect, resolveTarget } from "./command.js"
4
5
  import { runXpGate } from "./gate-runner.js"
5
6
  import { DelphiSetupError, loadDelphiConfig, toDelphiOptions } from "./delphi-config.js"
@@ -9,152 +10,177 @@ export const name = "tool-xp-gate"
9
10
  export const inject = ["tools", "shell"]
10
11
 
11
12
  const DEFAULT_TIMEOUT_MS = 120_000
13
+ // Multi-round reviews over large inputs are slow; the transport applies its
14
+ // own per-request timeout on top of this ceiling.
15
+ const DELPHI_TIMEOUT_MS = 900_000
12
16
 
13
- export function apply(ctx: Context, _config: unknown = {}): void {
14
- const shell = ctx.shell
15
- // DSH runs a POSIX shell off-Windows and PowerShell on Windows; generating the
16
- // wrong dialect makes every tool fail to parse rather than degrade gracefully.
17
- const dialect = detectDialect()
17
+ // Boy Scout (#447): gate-check's `gates` parameter nests one level too deep
18
+ // inline (parameters → gates → items), so the schema lives at module scope.
19
+ const GATES_PARAMETER = {
20
+ type: "array",
21
+ items: { type: "string", enum: GATE_WHITELIST },
22
+ description: "Optional gate subset to run (e.g. ['principles', 'arch']).",
23
+ } as const
18
24
 
19
- ctx.tools.register(
20
- defineTool({
21
- name: "gate-check",
22
- description:
23
- "Run XP-Gate quality gates (xp-gate check) on a file or directory and return the gate report. Prefers the global xp-gate CLI and degrades to an actionable install hint when it is absent.",
24
- parameters: {
25
- path: {
26
- type: "string",
27
- required: true,
28
- description: "File or directory path to check (absolute or relative to the workspace).",
29
- },
30
- gates: {
31
- type: "array",
32
- items: { type: "string", enum: GATE_WHITELIST },
33
- description: "Optional gate subset to run (e.g. ['principles', 'arch']).",
34
- },
25
+ // Boy Scout (#447): apply() used to inline four register(defineTool({...}))
26
+ // blocks (149 lines, 5 brace levels). Each tool now lives in its own factory;
27
+ // apply() is pure wiring.
28
+ function gateCheckTool(shell: Context["shell"], dialect: ShellDialect) {
29
+ return defineTool({
30
+ name: "gate-check",
31
+ description:
32
+ "Run XP-Gate quality gates (xp-gate check) on a file or directory and return the gate report. Prefers the global xp-gate CLI and degrades to an actionable install hint when it is absent.",
33
+ parameters: {
34
+ path: {
35
+ type: "string",
36
+ required: true,
37
+ description: "File or directory path to check (absolute or relative to the workspace).",
35
38
  },
36
- output: {
37
- schema: { type: "string" },
38
- render: (_args, value) => [{ type: "text", text: value }],
39
- },
40
- timeoutMs: DEFAULT_TIMEOUT_MS,
41
- async execute(args, exec) {
42
- const cwd = exec.agent?.session?.header?.cwd ?? process.cwd()
43
- const target = resolveTarget(args.path, cwd)
44
- const command = buildCommand({ subcommand: "check", target, gates: args.gates, dialect })
45
- return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS)
46
- },
47
- }),
48
- )
39
+ gates: GATES_PARAMETER,
40
+ },
41
+ output: {
42
+ schema: { type: "string" },
43
+ render: (_args, value) => [{ type: "text", text: value }],
44
+ },
45
+ timeoutMs: DEFAULT_TIMEOUT_MS,
46
+ async execute(args, exec) {
47
+ const cwd = exec.agent?.session?.header?.cwd ?? process.cwd()
48
+ const target = resolveTarget(args.path, cwd)
49
+ const command = buildCommand({ subcommand: "check", target, gates: args.gates, dialect })
50
+ return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS)
51
+ },
52
+ })
53
+ }
49
54
 
50
- ctx.tools.register(
51
- defineTool({
52
- name: "gate-principles",
53
- description:
54
- "Run the 14 Clean Code / SOLID rules (xp-gate principles, Gate 4) on a file or directory and return the principles report.",
55
- parameters: {
56
- path: {
57
- type: "string",
58
- required: true,
59
- description: "File or directory path to check (absolute or relative to the workspace).",
60
- },
61
- },
62
- output: {
63
- schema: { type: "string" },
64
- render: (_args, value) => [{ type: "text", text: value }],
55
+ function gatePrinciplesTool(shell: Context["shell"], dialect: ShellDialect) {
56
+ return defineTool({
57
+ name: "gate-principles",
58
+ description:
59
+ "Run the 14 Clean Code / SOLID rules (xp-gate principles, Gate 4) on a file or directory and return the principles report.",
60
+ parameters: {
61
+ path: {
62
+ type: "string",
63
+ required: true,
64
+ description: "File or directory path to check (absolute or relative to the workspace).",
65
65
  },
66
- timeoutMs: DEFAULT_TIMEOUT_MS,
67
- async execute(args, exec) {
68
- const cwd = exec.agent?.session?.header?.cwd ?? process.cwd()
69
- const target = resolveTarget(args.path, cwd)
70
- const command = buildCommand({ subcommand: "principles", target, dialect })
71
- return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS)
72
- },
73
- }),
74
- )
66
+ },
67
+ output: {
68
+ schema: { type: "string" },
69
+ render: (_args, value) => [{ type: "text", text: value }],
70
+ },
71
+ timeoutMs: DEFAULT_TIMEOUT_MS,
72
+ async execute(args, exec) {
73
+ const cwd = exec.agent?.session?.header?.cwd ?? process.cwd()
74
+ const target = resolveTarget(args.path, cwd)
75
+ const command = buildCommand({ subcommand: "principles", target, dialect })
76
+ return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS)
77
+ },
78
+ })
79
+ }
75
80
 
76
- ctx.tools.register(
77
- defineTool({
78
- name: "gate-arch",
79
- description:
80
- "Run architecture validation (xp-gate arch, Gate 6) using the given config file and return the architecture report. Defaults to architecture.yaml.",
81
- parameters: {
82
- config: {
83
- type: "string",
84
- description: "Architecture config file path (defaults to architecture.yaml).",
85
- },
86
- },
87
- output: {
88
- schema: { type: "string" },
89
- render: (_args, value) => [{ type: "text", text: value }],
81
+ function gateArchTool(shell: Context["shell"], dialect: ShellDialect) {
82
+ return defineTool({
83
+ name: "gate-arch",
84
+ description:
85
+ "Run architecture validation (xp-gate arch, Gate 6) with the pinned local @archlinter/cli built-in rules and baseline ratchet, and return the architecture report. Pass a config file only to opt into archlint's own --config path (pre-0.16 schemas are rejected by archlint itself).",
86
+ parameters: {
87
+ config: {
88
+ type: "string",
89
+ description:
90
+ "Optional architecture config file (opts into archlint --config; the default built-in-rules path needs none).",
90
91
  },
91
- timeoutMs: DEFAULT_TIMEOUT_MS,
92
- async execute(args, exec) {
93
- const cwd = exec.agent?.session?.header?.cwd ?? process.cwd()
94
- const command = buildCommand({ subcommand: "arch", config: args.config, dialect })
95
- return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS)
96
- },
97
- }),
98
- )
92
+ },
93
+ output: {
94
+ schema: { type: "string" },
95
+ render: (_args, value) => [{ type: "text", text: value }],
96
+ },
97
+ timeoutMs: DEFAULT_TIMEOUT_MS,
98
+ async execute(args, exec) {
99
+ const cwd = exec.agent?.session?.header?.cwd ?? process.cwd()
100
+ const command = buildCommand({ subcommand: "arch", config: args.config, dialect })
101
+ return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS)
102
+ },
103
+ })
104
+ }
105
+
106
+ // Delphi multi-model cross-review.
107
+ //
108
+ // DSH's own subagents cannot do this: they inherit one host-configured model,
109
+ // so three "experts" would be three instances of the same model, and Delphi's
110
+ // value rests entirely on the reviewers being DIFFERENT models. This tool
111
+ // therefore calls an external OpenAI-compatible provider directly, with one
112
+ // distinct model per seat.
113
+ // Boy Scout (#447): the review body used to sit inline in execute() at 5 brace
114
+ // levels (defineTool → execute → try → runReview options). Module scope keeps
115
+ // the try/catch semantics identical while dropping two levels.
116
+ async function runDelphiReview(
117
+ subject: string,
118
+ addressed: string,
119
+ maxRounds: number | undefined,
120
+ cwd: string,
121
+ ): Promise<string> {
122
+ try {
123
+ const cfg = loadDelphiConfig(cwd)
124
+ const outcome = await runReview(
125
+ { ...toDelphiOptions(cfg), maxRounds: maxRounds ?? cfg.maxRounds },
126
+ subject,
127
+ addressed,
128
+ )
129
+ const seats = cfg.seats.map((s) => s.model).join(", ")
130
+ return `${formatOutcome(outcome)}\n\nProvider: ${cfg.provider}\nSeats: ${seats}`
131
+ } catch (e) {
132
+ // Config problems are the common failure; report them as guidance
133
+ // rather than a stack trace.
134
+ if (e instanceof DelphiSetupError) return `Delphi review not started: ${e.message}`
135
+ throw e
136
+ }
137
+ }
99
138
 
100
- // Delphi multi-model cross-review.
101
- //
102
- // DSH's own subagents cannot do this: they inherit one host-configured model,
103
- // so three "experts" would be three instances of the same model, and Delphi's
104
- // value rests entirely on the reviewers being DIFFERENT models. This tool
105
- // therefore calls an external OpenAI-compatible provider directly, with one
106
- // distinct model per seat.
107
- ctx.tools.register(
108
- defineTool({
109
- name: "delphi-review",
110
- description:
111
- "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.",
112
- parameters: {
113
- subject: {
114
- type: "string",
115
- required: true,
116
- description:
117
- "The material to review, inline. For a diff, pass the diff text (or a summarized form if very large).",
118
- },
119
- addressed: {
120
- type: "string",
121
- description:
122
- "Optional description of what changed since a previous round, fed back to later rounds so reviewers can revise their position.",
123
- },
124
- maxRounds: {
125
- type: "number",
126
- description: "Override the configured round cap (Delphi default is 5).",
127
- },
128
- cwd: {
129
- type: "string",
130
- description: "Project root containing .delphi-config.json (defaults to the session cwd).",
131
- },
139
+ function delphiReviewTool() {
140
+ return defineTool({
141
+ name: "delphi-review",
142
+ description:
143
+ "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.",
144
+ parameters: {
145
+ subject: {
146
+ type: "string",
147
+ required: true,
148
+ description:
149
+ "The material to review, inline. For a diff, pass the diff text (or a summarized form if very large).",
150
+ },
151
+ addressed: {
152
+ type: "string",
153
+ description:
154
+ "Optional description of what changed since a previous round, fed back to later rounds so reviewers can revise their position.",
132
155
  },
133
- output: {
134
- schema: { type: "string" },
135
- render: (_args, value) => [{ type: "text", text: value }],
156
+ maxRounds: {
157
+ type: "number",
158
+ description: "Override the configured round cap (Delphi default is 5).",
136
159
  },
137
- // Multi-round reviews over large inputs are slow; the transport applies
138
- // its own per-request timeout on top of this ceiling.
139
- timeoutMs: 900_000,
140
- async execute(args, exec) {
141
- const cwd = args.cwd ?? exec.agent?.session?.header?.cwd ?? process.cwd()
142
- try {
143
- const cfg = loadDelphiConfig(cwd)
144
- const outcome = await runReview(
145
- { ...toDelphiOptions(cfg), maxRounds: args.maxRounds ?? cfg.maxRounds },
146
- args.subject,
147
- args.addressed ?? "",
148
- )
149
- const seats = cfg.seats.map((s) => s.model).join(", ")
150
- return `${formatOutcome(outcome)}\n\nProvider: ${cfg.provider}\nSeats: ${seats}`
151
- } catch (e) {
152
- // Config problems are the common failure; report them as guidance
153
- // rather than a stack trace.
154
- if (e instanceof DelphiSetupError) return `Delphi review not started: ${e.message}`
155
- throw e
156
- }
160
+ cwd: {
161
+ type: "string",
162
+ description: "Project root containing .delphi-config.json (defaults to the session cwd).",
157
163
  },
158
- }),
159
- )
160
- }
164
+ },
165
+ output: {
166
+ schema: { type: "string" },
167
+ render: (_args, value) => [{ type: "text", text: value }],
168
+ },
169
+ timeoutMs: DELPHI_TIMEOUT_MS,
170
+ async execute(args, exec) {
171
+ const cwd = args.cwd ?? exec.agent?.session?.header?.cwd ?? process.cwd()
172
+ return runDelphiReview(args.subject, args.addressed ?? "", args.maxRounds, cwd)
173
+ },
174
+ })
175
+ }
176
+
177
+ export function apply(ctx: Context, _config: unknown = {}): void {
178
+ // DSH runs a POSIX shell off-Windows and PowerShell on Windows; generating the
179
+ // wrong dialect makes every tool fail to parse rather than degrade gracefully.
180
+ const dialect = detectDialect()
181
+ const shell = ctx.shell
182
+ ctx.tools.register(gateCheckTool(shell, dialect))
183
+ ctx.tools.register(gatePrinciplesTool(shell, dialect))
184
+ ctx.tools.register(gateArchTool(shell, dialect))
185
+ ctx.tools.register(delphiReviewTool())
186
+ }
@@ -9,7 +9,8 @@ OpenCode plugin exposing xp-gate quality gates and AI workflow skills.
9
9
  - **gate-principles** ⇄ `xp-gate principles <path>`: Run Clean Code + SOLID
10
10
  principles checker (Gate 4 standalone).
11
11
  - **gate-arch** ⇄ `xp-gate arch`: Run architecture validation (Gate 6
12
- standalone, layer boundary checks).
12
+ standalone) with the pinned local @archlinter/cli built-in rules and baseline
13
+ ratchet; an optional config file opts into archlint's own `--config` path.
13
14
  - **session-reload-model**: Reload session model from config. After switching
14
15
  provider configs (opencode.json + oh-my-openagent.json via switch-coding-plan.sh),
15
16
  updates the session model in OpenCode DB to match the new config, so restarted
@@ -67,7 +68,9 @@ Or via local path (development):
67
68
  - One of:
68
69
  - `xp-gate` CLI installed globally (`npm install -g @boyingliu01/xp-gate`) — **preferred**, or
69
70
  - the xp-gate repo checked out locally with `src/principles/index.ts` reachable — **fallback** (the tool will shell out via `npx -y tsx`)
70
- - `architecture.yaml` in repo root (for `gate-arch` only)
71
+ - `@archlinter/cli` installed locally or on PATH (for `gate-arch` only; the
72
+ default built-in-rules path needs no `architecture.yaml` — passing one opts
73
+ into archlint's own `--config` schema validation, which rejects pre-0.16 files)
71
74
 
72
75
  ## Graceful Degradation
73
76