@boyingliu01/xp-gate 0.35.10 → 0.35.12

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 (36) hide show
  1. package/hooks/pre-commit +131 -5
  2. package/lib/__tests__/coverage-scope.test.js +140 -0
  3. package/lib/__tests__/slo.test.js +89 -0
  4. package/lib/mattpocock-license-verdict.json +1 -1
  5. package/lib/retro.js +84 -0
  6. package/lib/slo.js +150 -0
  7. package/mock-policy/AGENTS.md +2 -2
  8. package/mutation/AGENTS.md +2 -2
  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 +2 -2
  12. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  13. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  14. package/plugins/dsh/lib/index.d.ts +21 -0
  15. package/plugins/dsh/lib/index.d.ts.map +1 -1
  16. package/plugins/dsh/lib/index.js +23 -5
  17. package/plugins/dsh/package.json +1 -1
  18. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  19. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  20. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  21. package/plugins/dsh/src/index.test.ts +18 -1
  22. package/plugins/dsh/src/index.ts +24 -5
  23. package/plugins/opencode/index.ts +8 -4
  24. package/plugins/opencode/package.json +1 -1
  25. package/plugins/opencode/semver.test.ts +64 -0
  26. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  27. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  28. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  29. package/plugins/qoder/plugin.json +1 -1
  30. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  31. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  32. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  33. package/principles/AGENTS.md +2 -2
  34. package/skills/delphi-review/AGENTS.md +2 -2
  35. package/skills/sprint-flow/AGENTS.md +2 -2
  36. package/skills/test-specification-alignment/AGENTS.md +2 -2
package/lib/slo.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @test REQ-609 SLO 指标必须区分"达标"与"没有数据"
3
+ * @intent 指南 §15 要求首构 ≥90%、终构 ≥98%、首测 ≥85%、Flaky ≤0.5%、
4
+ * CI 无效触发 ≤20%、文件丢失 = 0。
5
+ *
6
+ * 这个模块的**全部价值**在于三态判定,而不是六行数字:
7
+ *
8
+ * PASS 有数据,且在目标内
9
+ * MISS 有数据,但在目标外 <- 唯一需要行动的
10
+ * UNKNOWN 数据不足,无从判断 <- 不是通过,也不是失败
11
+ *
12
+ * 为什么 UNKNOWN 必须独立存在:把"没有数据"折叠进 PASS 就是 #554 那一类
13
+ * 缺陷(门禁没跑却报通过);折叠进 MISS 则会让每一个新仓库一开始就六项
14
+ * 全红,于是所有人都学会忽略这个面板——面板一旦被忽略,它就和不存在一样。
15
+ *
16
+ * "文件丢失 = 0"尤其危险:它的目标值是 0,而没有数据时最自然的算法
17
+ * 恰好也产出 0。所以这一项的 UNKNOWN 判定必须先于数值比较,而不是
18
+ * 比较完之后再补一句注释。
19
+ *
20
+ * 每个 SLO 都带 `source`:没有指明数据来源的指标无法被核实,
21
+ * 也就无法被信任。
22
+ */
23
+ 'use strict';
24
+
25
+ /** 六项 SLO 及其目标。方向决定比较运算符,避免手写每个分支。 */
26
+ const SLO_DEFINITIONS = [
27
+ {
28
+ id: 'first_build_pass',
29
+ label: 'First-build pass rate',
30
+ target: 90,
31
+ unit: '%',
32
+ direction: 'min',
33
+ source: '.xp-gate/audit.jsonl + pre-commit reports',
34
+ describe: 'first attempt of each gate run passes without a retry',
35
+ },
36
+ {
37
+ id: 'final_build_pass',
38
+ label: 'Final-build pass rate',
39
+ target: 98,
40
+ unit: '%',
41
+ direction: 'min',
42
+ source: '.xp-gate/audit.jsonl (last attempt per gate per commit)',
43
+ describe: 'the attempt that actually lands passes',
44
+ },
45
+ {
46
+ id: 'first_test_pass',
47
+ label: 'First-test pass rate',
48
+ target: 85,
49
+ unit: '%',
50
+ direction: 'min',
51
+ source: '.xp-gate/audit.jsonl (Gate 5 first attempt)',
52
+ describe: 'test suite passes on its first run in a commit',
53
+ },
54
+ {
55
+ id: 'flaky_rate',
56
+ label: 'Flaky rate',
57
+ target: 0.5,
58
+ unit: '%',
59
+ direction: 'max',
60
+ source: '.xp-gate/audit.jsonl re-run records',
61
+ describe: 'a test that failed then passed on re-run without a code change',
62
+ },
63
+ {
64
+ id: 'ci_wasted_trigger',
65
+ label: 'CI wasted trigger rate',
66
+ target: 20,
67
+ unit: '%',
68
+ direction: 'max',
69
+ source: '.github workflow runs vs gate journal',
70
+ describe: 'a CI run triggered by a commit that the gates had already rejected',
71
+ },
72
+ {
73
+ id: 'file_loss',
74
+ label: 'File loss events',
75
+ target: 0,
76
+ unit: '',
77
+ direction: 'max',
78
+ source: 'vitest-worktree-guard + git status snapshots',
79
+ describe: 'tracked files disappearing during a run',
80
+ },
81
+ ];
82
+
83
+ const STATES = { PASS: 'PASS', MISS: 'MISS', UNKNOWN: 'UNKNOWN' };
84
+
85
+ /**
86
+ * Evaluate one SLO sample.
87
+ *
88
+ * `value === null` / `undefined` means "no measurement", which is a different
89
+ * claim from `value === 0`. Keeping them distinct is the whole point of this module.
90
+ */
91
+ function evaluateSlo(id, value, opts = {}) {
92
+ const def = SLO_DEFINITIONS.find((d) => d.id === id);
93
+ if (!def) throw new Error(`unknown SLO id: ${id}`);
94
+
95
+ if (value === null || value === undefined || Number.isNaN(value)) {
96
+ return {
97
+ ...def,
98
+ value: null,
99
+ state: STATES.UNKNOWN,
100
+ reason: opts.reason || 'no measurement available in the reporting window',
101
+ };
102
+ }
103
+
104
+ // File loss is the trap: its target is 0, and a naive max-direction comparison
105
+ // (`0 <= 0`) reports PASS for a window that observed nothing. Requiring an
106
+ // explicit sample count makes "we looked and saw none" different from "we did
107
+ // not look".
108
+ const samples = opts.samples;
109
+ if (def.id === 'file_loss' && (samples === undefined || samples === null || samples <= 0)) {
110
+ return {
111
+ ...def,
112
+ value: null,
113
+ state: STATES.UNKNOWN,
114
+ reason: 'no guard runs in the window -- absence of guards is not absence of loss',
115
+ };
116
+ }
117
+
118
+ const pass = def.direction === 'min' ? value >= def.target : value <= def.target;
119
+ return {
120
+ ...def,
121
+ value,
122
+ sampleCount: samples,
123
+ state: pass ? STATES.PASS : STATES.MISS,
124
+ reason: pass ? '' : `${value}${def.unit} vs target ${def.direction === 'min' ? '>=' : '<='} ${def.target}${def.unit}`,
125
+ };
126
+ }
127
+
128
+ /** Evaluate a whole batch of measurements (`{ id: value | {value,samples} }`). */
129
+ function evaluateAll(measurements = {}) {
130
+ return SLO_DEFINITIONS.map((def) => {
131
+ const raw = measurements[def.id];
132
+ if (raw && typeof raw === 'object') return evaluateSlo(def.id, raw.value, raw);
133
+ return evaluateSlo(def.id, raw);
134
+ });
135
+ }
136
+
137
+ /** Count rows per state -- what a dashboard badge and a retro line both need. */
138
+ function summariseStates(rows) {
139
+ const out = { PASS: 0, MISS: 0, UNKNOWN: 0, total: rows.length };
140
+ for (const r of rows) out[r.state] += 1;
141
+ return out;
142
+ }
143
+
144
+ module.exports = {
145
+ SLO_DEFINITIONS,
146
+ STATES,
147
+ evaluateSlo,
148
+ evaluateAll,
149
+ summariseStates,
150
+ };
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** 6c9bcb78
4
+ **Commit:** a92243d7
5
5
  **Branch:** main
6
- **Version:** 0.35.10.0
6
+ **Version:** 0.35.12.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** 6c9bcb78
4
+ **Commit:** a92243d7
5
5
  **Branch:** main
6
- **Version:** 0.35.10.0
6
+ **Version:** 0.35.12.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/xp-gate",
3
- "version": "0.35.10",
3
+ "version": "0.35.12",
4
4
  "description": "AI-driven development workflow: 6 quality gates + Delphi review + Sprint Flow",
5
5
  "bin": {
6
6
  "xp-gate": "bin/xp-gate.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.35.10",
3
+ "version": "0.35.12",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
6
6
  "author": {
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** 6c9bcb78
4
+ **Commit:** a92243d7
5
5
  **Branch:** main
6
- **Version:** 0.35.10.0
6
+ **Version:** 0.35.12.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
3
  **Generated:** 2026-10-30
4
- **Commit:** 6c9bcb78
4
+ **Commit:** a92243d7
5
5
  **Branch:** main
6
- **Version:** 0.35.10.0
6
+ **Version:** 0.35.12.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
3
  **Generated:** 2026-10-30
4
- **Commit:** 6c9bcb78
4
+ **Commit:** a92243d7
5
5
  **Branch:** main
6
- **Version:** 0.35.10.0
6
+ **Version:** 0.35.12.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,5 +1,26 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import type { Context } from "@deepseek-ai/cordis";
3
+ /**
4
+ * Renders a tool's string result as plain text.
5
+ *
6
+ * Extracted because five tools had written this identical closure inline, which
7
+ * left five separate uncovered functions in the coverage report for one line of
8
+ * behaviour (#562). One definition can be tested once and reads as what it is: a
9
+ * shared output convention, not five coincidences.
10
+ *
11
+ * Generic in the argument type on purpose. `defineTool` types `render` as
12
+ * `(args: <this tool's parameters>, value: string) => ContentBlock[]`, so a
13
+ * `(_args: unknown, ...)` implementation is NOT assignable to it -- `unknown` is
14
+ * the wrong direction for a parameter. Declaring it as `A` lets every call site
15
+ * infer its own argument shape while the return type stays a plain text block.
16
+ *
17
+ * This mismatch was caught by `tsc` in the publish job, not by the test suite:
18
+ * vitest transpiles without type-checking. A green test run is not a green build.
19
+ */
20
+ export declare const renderText: <A>(_args: A, value: string) => {
21
+ type: "text";
22
+ text: string;
23
+ }[];
3
24
  export declare const name = "tool-xp-gate";
4
25
  export declare const inject: string[];
5
26
  /**
@@ -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;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,CAgC9D"}
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;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,OAAO,CAAC,EAAE,OAAO,MAAM;;;GAA6C,CAAA;AAElG,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,CAgC9D"}
@@ -15,6 +15,24 @@ import { delphiGuardDecision, isCheckablePath, isEditTool } from "./hooks.js";
15
15
  import { PHASE_STATUSES, SPRINT_SUBCOMMANDS, buildSprintCommand } from "./sprint.js";
16
16
  import { createSkillProvider } from "./skill-provider.js";
17
17
  import { createCredentialResolver } from "./credentials.js";
18
+ /**
19
+ * Renders a tool's string result as plain text.
20
+ *
21
+ * Extracted because five tools had written this identical closure inline, which
22
+ * left five separate uncovered functions in the coverage report for one line of
23
+ * behaviour (#562). One definition can be tested once and reads as what it is: a
24
+ * shared output convention, not five coincidences.
25
+ *
26
+ * Generic in the argument type on purpose. `defineTool` types `render` as
27
+ * `(args: <this tool's parameters>, value: string) => ContentBlock[]`, so a
28
+ * `(_args: unknown, ...)` implementation is NOT assignable to it -- `unknown` is
29
+ * the wrong direction for a parameter. Declaring it as `A` lets every call site
30
+ * infer its own argument shape while the return type stays a plain text block.
31
+ *
32
+ * This mismatch was caught by `tsc` in the publish job, not by the test suite:
33
+ * vitest transpiles without type-checking. A green test run is not a green build.
34
+ */
35
+ export const renderText = (_args, value) => [{ type: "text", text: value }];
18
36
  export const name = "tool-xp-gate";
19
37
  // "skills" is required: the bundled skill provider registers during apply(), and
20
38
  // without it ctx.skills may not be mounted yet when this plugin starts.
@@ -68,7 +86,7 @@ function gateCheckTool(shell, dialect) {
68
86
  },
69
87
  output: {
70
88
  schema: { type: "string" },
71
- render: (_args, value) => [{ type: "text", text: value }],
89
+ render: renderText,
72
90
  },
73
91
  timeoutMs: DEFAULT_TIMEOUT_MS,
74
92
  async execute(args, exec) {
@@ -92,7 +110,7 @@ function gatePrinciplesTool(shell, dialect) {
92
110
  },
93
111
  output: {
94
112
  schema: { type: "string" },
95
- render: (_args, value) => [{ type: "text", text: value }],
113
+ render: renderText,
96
114
  },
97
115
  timeoutMs: DEFAULT_TIMEOUT_MS,
98
116
  async execute(args, exec) {
@@ -115,7 +133,7 @@ function gateArchTool(shell, dialect) {
115
133
  },
116
134
  output: {
117
135
  schema: { type: "string" },
118
- render: (_args, value) => [{ type: "text", text: value }],
136
+ render: renderText,
119
137
  },
120
138
  timeoutMs: DEFAULT_TIMEOUT_MS,
121
139
  async execute(args, exec) {
@@ -297,7 +315,7 @@ function delphiReviewTool(resolveApiKey, settings) {
297
315
  },
298
316
  output: {
299
317
  schema: { type: "string" },
300
- render: (_args, value) => [{ type: "text", text: value }],
318
+ render: renderText,
301
319
  },
302
320
  timeoutMs: DELPHI_TIMEOUT_MS,
303
321
  async execute(args, exec) {
@@ -348,7 +366,7 @@ function sprintTool(shell, dialect) {
348
366
  },
349
367
  output: {
350
368
  schema: { type: "string" },
351
- render: (_args, value) => [{ type: "text", text: value }],
369
+ render: renderText,
352
370
  },
353
371
  timeoutMs: DEFAULT_TIMEOUT_MS,
354
372
  async execute(args, exec) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/dsh-plugin-xp-gate",
3
- "version": "0.35.10",
3
+ "version": "0.35.12",
4
4
  "description": "XP-Gate deterministic quality gates + AI workflow skills as a DeepSeek Harness (DSH) native plugin: gate-check / gate-principles / gate-arch tools + 12 SKILL.md skills",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** 6c9bcb78
4
+ **Commit:** a92243d7
5
5
  **Branch:** main
6
- **Version:** 0.35.10.0
6
+ **Version:** 0.35.12.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
3
  **Generated:** 2026-10-30
4
- **Commit:** 6c9bcb78
4
+ **Commit:** a92243d7
5
5
  **Branch:** main
6
- **Version:** 0.35.10.0
6
+ **Version:** 0.35.12.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
3
  **Generated:** 2026-10-30
4
- **Commit:** 6c9bcb78
4
+ **Commit:** a92243d7
5
5
  **Branch:** main
6
- **Version:** 0.35.10.0
6
+ **Version:** 0.35.12.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest"
2
2
  import type { Context } from "@deepseek-ai/cordis"
3
3
  import type { ShellExecRequest, ShellExecSpec, ShellRunResult } from "@deepseek-ai/dsh-shell"
4
4
  import type { SkillProvider, SkillProviderControl, SkillProviderObservation } from "@deepseek-ai/dsh-skill"
5
- import { apply, inject } from "./index.js"
5
+ import { apply, inject, renderText } from "./index.js"
6
6
 
7
7
  type ProviderFactory = (control: SkillProviderControl) => SkillProvider
8
8
 
@@ -169,4 +169,21 @@ describe("apply", () => {
169
169
  await tool.execute({ config: "custom.yaml" }, { signal: new AbortController().signal })
170
170
  expect(requests[1]?.command).toContain("xp-gate arch --use-config 'custom.yaml'")
171
171
  })
172
+
173
+ /**
174
+ * @test REQ-562
175
+ * @intent 5 个工具曾各自内联一份完全相同的 render 闭包,在覆盖率报告里表现为 5 个
176
+ * 未覆盖函数,而实际只有一行行为(#562)。提取为共享常量后只需测一次,
177
+ * 也让"共享的输出约定"这件事在代码里显形,而不是 5 个巧合。
178
+ * @covers AC-562-04
179
+ */
180
+ it("renderText wraps a tool result as a single text part", () => {
181
+ expect(renderText(undefined, "hello")).toEqual([{ type: "text", text: "hello" }])
182
+ })
183
+
184
+ it("renderText keeps empty output as an empty text part, not a dropped one", () => {
185
+ // A tool that legitimately produced nothing must still render one part; returning
186
+ // [] would look to the host like the tool never answered.
187
+ expect(renderText({ path: "x" }, "")).toEqual([{ type: "text", text: "" }])
188
+ })
172
189
  })
@@ -32,6 +32,25 @@ import { createSkillProvider } from "./skill-provider.js"
32
32
  import type { ApiKeyResolver } from "./credentials.js"
33
33
  import { createCredentialResolver } from "./credentials.js"
34
34
 
35
+ /**
36
+ * Renders a tool's string result as plain text.
37
+ *
38
+ * Extracted because five tools had written this identical closure inline, which
39
+ * left five separate uncovered functions in the coverage report for one line of
40
+ * behaviour (#562). One definition can be tested once and reads as what it is: a
41
+ * shared output convention, not five coincidences.
42
+ *
43
+ * Generic in the argument type on purpose. `defineTool` types `render` as
44
+ * `(args: <this tool's parameters>, value: string) => ContentBlock[]`, so a
45
+ * `(_args: unknown, ...)` implementation is NOT assignable to it -- `unknown` is
46
+ * the wrong direction for a parameter. Declaring it as `A` lets every call site
47
+ * infer its own argument shape while the return type stays a plain text block.
48
+ *
49
+ * This mismatch was caught by `tsc` in the publish job, not by the test suite:
50
+ * vitest transpiles without type-checking. A green test run is not a green build.
51
+ */
52
+ export const renderText = <A>(_args: A, value: string) => [{ type: "text" as const, text: value }]
53
+
35
54
  export const name = "tool-xp-gate"
36
55
  // "skills" is required: the bundled skill provider registers during apply(), and
37
56
  // without it ctx.skills may not be mounted yet when this plugin starts.
@@ -92,7 +111,7 @@ function gateCheckTool(shell: Context["shell"], dialect: ShellDialect) {
92
111
  },
93
112
  output: {
94
113
  schema: { type: "string" },
95
- render: (_args, value) => [{ type: "text", text: value }],
114
+ render: renderText,
96
115
  },
97
116
  timeoutMs: DEFAULT_TIMEOUT_MS,
98
117
  async execute(args, exec) {
@@ -118,7 +137,7 @@ function gatePrinciplesTool(shell: Context["shell"], dialect: ShellDialect) {
118
137
  },
119
138
  output: {
120
139
  schema: { type: "string" },
121
- render: (_args, value) => [{ type: "text", text: value }],
140
+ render: renderText,
122
141
  },
123
142
  timeoutMs: DEFAULT_TIMEOUT_MS,
124
143
  async execute(args, exec) {
@@ -144,7 +163,7 @@ function gateArchTool(shell: Context["shell"], dialect: ShellDialect) {
144
163
  },
145
164
  output: {
146
165
  schema: { type: "string" },
147
- render: (_args, value) => [{ type: "text", text: value }],
166
+ render: renderText,
148
167
  },
149
168
  timeoutMs: DEFAULT_TIMEOUT_MS,
150
169
  async execute(args, exec) {
@@ -352,7 +371,7 @@ function delphiReviewTool(resolveApiKey: ApiKeyResolver, settings: DelphiSetting
352
371
  },
353
372
  output: {
354
373
  schema: { type: "string" },
355
- render: (_args, value) => [{ type: "text", text: value }],
374
+ render: renderText,
356
375
  },
357
376
  timeoutMs: DELPHI_TIMEOUT_MS,
358
377
  async execute(args, exec) {
@@ -405,7 +424,7 @@ function sprintTool(shell: Context["shell"], dialect: ShellDialect) {
405
424
  },
406
425
  output: {
407
426
  schema: { type: "string" },
408
- render: (_args, value) => [{ type: "text", text: value }],
427
+ render: renderText,
409
428
  },
410
429
  timeoutMs: DEFAULT_TIMEOUT_MS,
411
430
  async execute(args, exec) {
@@ -1,6 +1,6 @@
1
1
  import { tool } from "@opencode-ai/plugin"
2
2
  import { z } from "zod"
3
- import { exec, execSync, spawn } from "node:child_process"
3
+ import { exec, execFileSync, execSync, spawn } from "node:child_process"
4
4
  import { promisify } from "node:util"
5
5
  import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs"
6
6
  import { join } from "node:path"
@@ -345,7 +345,7 @@ function generateSessionTitle(dbPath: string, sessionId: string): string {
345
345
  ORDER BY m.time_created DESC
346
346
  LIMIT 10
347
347
  `
348
- const raw = execSync(`sqlite3 "${dbPath}" "${sql}"`, {
348
+ const raw = execFileSync("sqlite3", [dbPath, sql], {
349
349
  encoding: "utf8",
350
350
  timeout: 5000,
351
351
  })
@@ -492,7 +492,11 @@ function writeSessionModel(dbPath: string, sessionId: string, modelJson: string)
492
492
  const escapedModel = modelJson.replace(/'/g, "''")
493
493
  const sql = `UPDATE session SET model = '${escapedModel}', time_updated = ${Date.now()} WHERE id = '${escapedId}'`
494
494
  try {
495
- execSync(`sqlite3 "${dbPath}" "${sql}"`, { timeout: SQLITE_TIMEOUT_MS })
495
+ // execFileSync, NOT execSync: `dbPath` was interpolated into a shell string with
496
+ // only double quotes for protection, so a path containing `"` or `$()` broke out
497
+ // into shell (semgrep security-audit flagged it as a command-injection sink).
498
+ // Passing the arguments as an array removes the shell from the picture entirely.
499
+ execFileSync("sqlite3", [dbPath, sql], { timeout: SQLITE_TIMEOUT_MS })
496
500
  } catch (err) {
497
501
  // Contract is to propagate: the caller renders the failure as
498
502
  // "Session reload failed: <message>". Wrapped so the IO sits inside
@@ -616,7 +620,7 @@ function renameSession(sessionId: string, newTitle?: string): string {
616
620
  const escapedId = sessionId.replace(/'/g, "''")
617
621
  const sql = `UPDATE session SET title = '${escapedTitle}', time_updated = ${Date.now()} WHERE id = '${escapedId}'`
618
622
  try {
619
- execSync(`sqlite3 "${dbPath}" "${sql}"`, { timeout: SQLITE_TIMEOUT_MS })
623
+ execFileSync("sqlite3", [dbPath, sql], { timeout: SQLITE_TIMEOUT_MS })
620
624
  return `Session renamed to: "${trimmed}"`
621
625
  } catch (err) {
622
626
  const msg = err instanceof Error ? err.message : String(err)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/opencode-plugin",
3
- "version": "0.35.10",
3
+ "version": "0.35.12",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "XP-Gate quality gates + AI workflow skills + Sprint Flow TUI sidebar for OpenCode",
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @test REQ-562 插件版本比较必须正确,否则升级提示会撒谎
3
+ * @intent `plugins/opencode/index.ts` 有 4 个 export,函数覆盖率 50%,且**没有任何
4
+ * 测试文件**。它不是可选的样板:`semverLt` 决定"要不要提示用户升级",
5
+ * 在 3 处被调用(第 143/249/289 行)。
6
+ *
7
+ * 一个错的 `semverLt` 不会崩溃,只会让提示永远说"已是最新"或永远提示升级——
8
+ * 两种都是"看起来在工作"。这类函数正是最该被钉住的:纯、无副作用、
9
+ * 影响用户可见的行为。
10
+ *
11
+ * 这里只测导出的纯函数,不碰网络和文件系统——测试不该依赖 npm registry
12
+ * 是否在线(那会让 CI 在断网时红,而问题不在代码里)。
13
+ * @covers AC-562-03
14
+ */
15
+ 'use strict';
16
+
17
+ const path = require('node:path');
18
+ const { fileURLToPath } = require('node:url');
19
+
20
+ const PLUGIN_ENTRY = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'index.ts');
21
+
22
+ describe('opencode 插件版本比较 (#562)', () => {
23
+ let mod;
24
+
25
+ beforeAll(async () => {
26
+ // The plugin is TypeScript with an npm dependency on @opencode-ai/plugin, which
27
+ // is only present in that workspace. Import through tsx so the test itself stays
28
+ // runnable from the root suite.
29
+ mod = await import(PLUGIN_ENTRY);
30
+ });
31
+
32
+ it('AC-562-03: 暴露了纯函数测试钩子', () => {
33
+ expect(mod.__test).toBeDefined();
34
+ expect(typeof mod.__test.semverLt).toBe('function');
35
+ });
36
+
37
+ it('AC-562-03: 主版本更高时判定为更小', () => {
38
+ expect(mod.__test.semverLt('0.35.10', '0.36.0')).toBe(true);
39
+ expect(mod.__test.semverLt('0.36.0', '0.35.10')).toBe(false);
40
+ });
41
+
42
+ it('AC-562-03: patch 与 minor 的位数差异不被误判', () => {
43
+ // This is the case a naive string comparison gets wrong: "0.9.0" < "0.10.0"
44
+ // lexicographically is 9 < 1 == false. Numeric segment compare must win.
45
+ expect(mod.__test.semverLt('0.9.0', '0.10.0')).toBe(true);
46
+ expect(mod.__test.semverLt('0.10.0', '0.9.0')).toBe(false);
47
+ });
48
+
49
+ it('AC-562-03: 前导 v 与缺失段被正确处理', () => {
50
+ expect(mod.__test.semverLt('v0.35.10', '0.35.11')).toBe(true);
51
+ // "0.35" is treated as 0.35.0, so it is LESS than 0.35.1 -- solid? No: it is
52
+ // equal to 0.35.0, and 0.35 < 0.35.1. Asserting the documented behaviour keeps
53
+ // a future refactor from silently changing missing-segment semantics.
54
+ expect(mod.__test.semverLt('0.35', '0.35.1')).toBe(true);
55
+ expect(mod.__test.semverLt('0.35.1', '0.35')).toBe(false);
56
+ });
57
+
58
+ it('AC-562-03: 相等版本不判定为更小', () => {
59
+ // `upToDate = !semverLt(...)` -- if equal versions compared as "less", every
60
+ // run would tell the user an update is available when there is none.
61
+ expect(mod.__test.semverLt('0.35.11', '0.35.11')).toBe(false);
62
+ expect(mod.__test.semverLt('v0.35.11', '0.35.11')).toBe(false);
63
+ });
64
+ });
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** 6c9bcb78
4
+ **Commit:** a92243d7
5
5
  **Branch:** main
6
- **Version:** 0.35.10.0
6
+ **Version:** 0.35.12.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
3
  **Generated:** 2026-10-30
4
- **Commit:** 6c9bcb78
4
+ **Commit:** a92243d7
5
5
  **Branch:** main
6
- **Version:** 0.35.10.0
6
+ **Version:** 0.35.12.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
3
  **Generated:** 2026-10-30
4
- **Commit:** 6c9bcb78
4
+ **Commit:** a92243d7
5
5
  **Branch:** main
6
- **Version:** 0.35.10.0
6
+ **Version:** 0.35.12.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.35.10",
3
+ "version": "0.35.12",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Qoder. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
6
6
  "author": {