@boyingliu01/xp-gate 0.18.4 → 0.19.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.
- package/adapter-common.sh +27 -0
- package/adapters/powershell.sh +85 -33
- package/adapters/typescript.sh +8 -8
- package/build-integrity/__tests__/import-resolver.test.ts +22 -0
- package/build-integrity/__tests__/orchestrator.test.ts +1 -2
- package/build-integrity/gate-10.ts +7 -0
- package/gate-12-file-hygiene.sh +230 -0
- package/hooks/adapter-common.sh +27 -0
- package/hooks/gate-10.sh +105 -0
- package/hooks/lib/validate-code-walkthrough.cjs +125 -0
- package/hooks/pre-commit +130 -33
- package/hooks/pre-push +15 -91
- package/lib/__tests__/check-version.test.js +67 -46
- package/lib/__tests__/doctor.test.js +7 -6
- package/lib/__tests__/gate-runner.test.js +267 -0
- package/lib/__tests__/init.test.js +32 -0
- package/lib/__tests__/npm-package.test.js +13 -0
- package/lib/__tests__/phase-transition.test.js +604 -12
- package/lib/__tests__/sprint-discovery.test.ts +0 -1
- package/lib/__tests__/sprint-status-rework.test.js +100 -1
- package/lib/__tests__/update-hooks.test.js +143 -10
- package/lib/__tests__/upgrade-exec.test.js +73 -45
- package/lib/__tests__/upgrade-skill-sync.test.js +1 -1
- package/lib/__tests__/upgrade.test.js +12 -11
- package/lib/doctor.js +2 -1
- package/lib/gate-runner.js +68 -23
- package/lib/init.js +34 -5
- package/lib/install-skill.js +1 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/phase-transition.js +203 -23
- package/lib/shared-paths.js +2 -0
- package/lib/sprint-status.js +15 -4
- package/lib/test-alignment.ts +3 -13
- package/lib/update-hooks.js +129 -15
- package/mock-policy/AGENTS.md +5 -5
- package/mock-policy/__tests__/gate-m3.test.ts +0 -5
- package/mock-policy/__tests__/scope-scanner.test.ts +8 -2
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/mutmut-runner.test.ts +443 -2
- package/mutation/__tests__/update-baseline.test.ts +0 -2
- package/mutation/runners/mutmut-runner.ts +67 -17
- package/package.json +9 -3
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/clipboard-vision/SKILL.md +47 -0
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
- package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
- package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.sh +161 -0
- package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
- package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
- package/plugins/claude-code/skills/delphi-review/.delphi-config.json.example +24 -27
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +13 -12
- package/plugins/claude-code/skills/delphi-review/INSTALL.md +15 -17
- package/plugins/claude-code/skills/delphi-review/SKILL.md +30 -29
- package/plugins/claude-code/skills/delphi-review/opencode.json.delphi.example +6 -10
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +33 -49
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +66 -54
- package/plugins/claude-code/skills/delphi-review/references/round-templates.md +7 -7
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +5 -5
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +37 -35
- package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/phase-1-prep.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +24 -14
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +6 -6
- package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/README.md +25 -0
- package/plugins/dsh/cordis.patch.yml +7 -0
- package/plugins/dsh/package.json +70 -0
- package/plugins/dsh/scripts/prepack.cjs +82 -0
- package/plugins/dsh/skills/admin-template-guidelines/SKILL.md +113 -0
- package/plugins/dsh/skills/admin-template-guidelines/references/rule-1.md +91 -0
- package/plugins/dsh/skills/admin-template-guidelines/references/rule-2.md +155 -0
- package/plugins/dsh/skills/admin-template-guidelines/references/rule-3.md +70 -0
- package/plugins/dsh/skills/admin-template-guidelines/references/rule-4.md +154 -0
- package/plugins/dsh/skills/admin-template-guidelines/references/rule-5.md +126 -0
- package/plugins/dsh/skills/admin-template-guidelines/references/rule-6.md +180 -0
- package/plugins/dsh/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/dsh/skills/clipboard-vision/SKILL.md +47 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
- package/plugins/dsh/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
- package/plugins/dsh/skills/clipboard-vision/describe-clipboard.sh +161 -0
- package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
- package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
- package/plugins/dsh/skills/delphi-review/.delphi-config.json.example +70 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +67 -0
- package/plugins/dsh/skills/delphi-review/INSTALL.md +150 -0
- package/plugins/dsh/skills/delphi-review/SKILL.md +559 -0
- package/plugins/dsh/skills/delphi-review/hooks.md +19 -0
- package/plugins/dsh/skills/delphi-review/opencode.json.delphi.example +52 -0
- package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +470 -0
- package/plugins/dsh/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
- package/plugins/dsh/skills/delphi-review/references/requirements.md +402 -0
- package/plugins/dsh/skills/delphi-review/references/round-templates.md +134 -0
- package/plugins/dsh/skills/delphi-review/scripts/verify-consensus.sh +43 -0
- package/plugins/dsh/skills/delphi-review/tools.md +29 -0
- package/plugins/dsh/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/dsh/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/dsh/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/dsh/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/dsh/skills/grilling/SKILL.md +12 -0
- package/plugins/dsh/skills/improve-codebase-architecture/SKILL.md +156 -0
- package/plugins/dsh/skills/ralph-loop/SKILL.md +198 -0
- package/plugins/dsh/skills/ralph-loop/references/components.md +113 -0
- package/plugins/dsh/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
- package/plugins/dsh/skills/ralph-loop/references/workflow-details.md +99 -0
- package/plugins/dsh/skills/ralph-loop/templates/progress-log.md +36 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +103 -0
- package/plugins/dsh/skills/sprint-flow/SKILL.md +411 -0
- package/plugins/dsh/skills/sprint-flow/__tests__/sprint-flow.test.cjs +457 -0
- package/plugins/dsh/skills/sprint-flow/hooks.md +18 -0
- package/plugins/dsh/skills/sprint-flow/references/components/memory.md +87 -0
- package/plugins/dsh/skills/sprint-flow/references/components/middleware.md +75 -0
- package/plugins/dsh/skills/sprint-flow/references/components/skill-invocations.md +105 -0
- package/plugins/dsh/skills/sprint-flow/references/components/system-prompt.md +27 -0
- package/plugins/dsh/skills/sprint-flow/references/components/tool-descriptions.md +95 -0
- package/plugins/dsh/skills/sprint-flow/references/force-levels.md +204 -0
- package/plugins/dsh/skills/sprint-flow/references/orchestration-rules.md +236 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-1-prep.md +294 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +273 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-3-build.md +348 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-4-verify.md +225 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-5-ship.md +222 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-6-close.md +266 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-overview.md +486 -0
- package/plugins/dsh/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
- package/plugins/dsh/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
- package/plugins/dsh/skills/sprint-flow/templates/auto-estimate-output-template.md +110 -0
- package/plugins/dsh/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
- package/plugins/dsh/skills/sprint-flow/templates/pain-document-template.md +115 -0
- package/plugins/dsh/skills/sprint-flow/templates/sprint-progress-template.md +140 -0
- package/plugins/dsh/skills/sprint-flow/templates/sprint-summary-template.md +121 -0
- package/plugins/dsh/skills/sprint-flow/tools.md +29 -0
- package/plugins/dsh/skills/test-driven-development/SKILL.md +371 -0
- package/plugins/dsh/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +56 -0
- package/plugins/dsh/skills/test-specification-alignment/SKILL.md +205 -0
- package/plugins/dsh/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
- package/plugins/dsh/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
- package/plugins/dsh/skills/test-specification-alignment/references/specification-format.md +348 -0
- package/plugins/dsh/skills/to-issues/SKILL.md +277 -0
- package/plugins/dsh/src/command.test.ts +102 -0
- package/plugins/dsh/src/command.ts +75 -0
- package/plugins/dsh/src/contract.test.ts +62 -0
- package/plugins/dsh/src/gate-runner.test.ts +100 -0
- package/plugins/dsh/src/gate-runner.ts +58 -0
- package/plugins/dsh/src/index.test.ts +97 -0
- package/plugins/dsh/src/index.ts +94 -0
- package/plugins/dsh/src/skills.test.ts +58 -0
- package/plugins/dsh/tsconfig.build.json +13 -0
- package/plugins/dsh/tsconfig.json +19 -0
- package/plugins/dsh/vitest.config.ts +8 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/scripts/prepack.cjs +1 -0
- package/plugins/opencode/skills/clipboard-vision/SKILL.md +47 -0
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
- package/plugins/opencode/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
- package/plugins/opencode/skills/clipboard-vision/describe-clipboard.sh +161 -0
- package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
- package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
- package/plugins/opencode/skills/delphi-review/.delphi-config.json.example +24 -27
- package/plugins/opencode/skills/delphi-review/AGENTS.md +13 -12
- package/plugins/opencode/skills/delphi-review/INSTALL.md +15 -17
- package/plugins/opencode/skills/delphi-review/SKILL.md +30 -29
- package/plugins/opencode/skills/delphi-review/opencode.json.delphi.example +6 -10
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +33 -49
- package/plugins/opencode/skills/delphi-review/references/requirements.md +66 -54
- package/plugins/opencode/skills/delphi-review/references/round-templates.md +7 -7
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +5 -5
- package/plugins/opencode/skills/sprint-flow/SKILL.md +4 -4
- package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +2 -2
- package/plugins/opencode/skills/sprint-flow/references/force-levels.md +37 -35
- package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/phase-1-prep.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +24 -14
- package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +6 -6
- package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/clipboard-vision/SKILL.md +47 -0
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/SKILL.md +47 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/scripts/clipboard-vision.sh +137 -0
- package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
- package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
- package/plugins/qoder/skills/delphi-review/.delphi-config.json.example +3 -11
- package/plugins/qoder/skills/delphi-review/AGENTS.md +15 -17
- package/plugins/qoder/skills/delphi-review/INSTALL.md +12 -12
- package/plugins/qoder/skills/delphi-review/SKILL.md +58 -25
- package/plugins/qoder/skills/delphi-review/opencode.json.delphi.example +1 -1
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +33 -49
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +5 -5
- package/plugins/qoder/skills/sprint-flow/SKILL.md +2 -2
- package/plugins/qoder/skills/sprint-flow/references/force-levels.md +38 -35
- package/plugins/qoder/skills/sprint-flow/references/phase-1-prep.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +48 -6
- package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +5 -6
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/scripts/__tests__/sync-package-content.test.js +30 -0
- package/scripts/delphi-external-review.cjs +194 -63
- package/scripts/sync-package-content.js +9 -2
- package/skills/clipboard-vision/SKILL.md +47 -0
- package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
- package/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
- package/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
- package/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
- package/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
- package/skills/clipboard-vision/describe-clipboard.sh +161 -0
- package/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
- package/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
- package/skills/delphi-review/.delphi-config.json.example +24 -27
- package/skills/delphi-review/AGENTS.md +13 -12
- package/skills/delphi-review/INSTALL.md +15 -17
- package/skills/delphi-review/SKILL.md +30 -29
- package/skills/delphi-review/opencode.json.delphi.example +6 -10
- package/skills/delphi-review/references/code-walkthrough.md +33 -49
- package/skills/delphi-review/references/requirements.md +66 -54
- package/skills/delphi-review/references/round-templates.md +7 -7
- package/skills/sprint-flow/AGENTS.md +5 -5
- package/skills/sprint-flow/SKILL.md +4 -4
- package/skills/sprint-flow/references/components/skill-invocations.md +2 -2
- package/skills/sprint-flow/references/force-levels.md +37 -35
- package/skills/sprint-flow/references/orchestration-rules.md +1 -1
- package/skills/sprint-flow/references/phase-1-prep.md +1 -1
- package/skills/sprint-flow/references/phase-2-design.md +24 -14
- package/skills/sprint-flow/references/phase-4-verify.md +6 -6
- package/skills/sprint-flow/references/phase-overview.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/sprint-gate.sh +291 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
FALLBACK_MESSAGE,
|
|
4
|
+
GATE_WHITELIST,
|
|
5
|
+
buildCommand,
|
|
6
|
+
isGateAllowed,
|
|
7
|
+
resolveTarget,
|
|
8
|
+
shq,
|
|
9
|
+
} from "./command.js"
|
|
10
|
+
|
|
11
|
+
describe("shq (POSIX single-quote escaping)", () => {
|
|
12
|
+
it("wraps a plain value in single quotes", () => {
|
|
13
|
+
expect(shq("abc")).toBe("'abc'")
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("escapes embedded single quotes with the quote-close idiom", () => {
|
|
17
|
+
expect(shq("a'b")).toBe("'a'\\''b'")
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @test REQ-DSH-009
|
|
22
|
+
* @intent 验证 shq() 对命令替换/反引号等 shell 元字符做 POSIX 单引号转义,防止命令注入
|
|
23
|
+
* @covers AC-DSH-009-01
|
|
24
|
+
*/
|
|
25
|
+
it("neutralizes shell metacharacters by quoting them literally", () => {
|
|
26
|
+
const payload = "$(touch /tmp/pwned); `id`"
|
|
27
|
+
expect(shq(payload)).toBe("'$(touch /tmp/pwned); `id`'")
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe("isGateAllowed (enum whitelist)", () => {
|
|
32
|
+
it("accepts whitelisted gate ids", () => {
|
|
33
|
+
expect(isGateAllowed("principles")).toBe(true)
|
|
34
|
+
expect(isGateAllowed("arch")).toBe(true)
|
|
35
|
+
expect(isGateAllowed("duplicates")).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("rejects unknown or injected gate ids", () => {
|
|
39
|
+
expect(isGateAllowed("$(rm -rf)")).toBe(false)
|
|
40
|
+
expect(isGateAllowed("0")).toBe(false)
|
|
41
|
+
expect(isGateAllowed("")).toBe(false)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe("buildCommand", () => {
|
|
46
|
+
it("builds a guarded gate-check command with target + gates", () => {
|
|
47
|
+
const cmd = buildCommand({ subcommand: "check", target: "src", gates: ["principles", "arch"] })
|
|
48
|
+
expect(cmd).toContain("command -v xp-gate")
|
|
49
|
+
expect(cmd).toContain("xp-gate check 'src' --gates 'principles,arch'")
|
|
50
|
+
expect(cmd).toContain(shq(FALLBACK_MESSAGE))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("drops non-whitelisted gates before building the command", () => {
|
|
54
|
+
const cmd = buildCommand({ subcommand: "check", target: "src", gates: ["principles", "bogus$(id)"] })
|
|
55
|
+
expect(cmd).toContain("xp-gate check 'src' --gates 'principles'")
|
|
56
|
+
expect(cmd).not.toContain("bogus")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("escapes a hostile target path", () => {
|
|
60
|
+
const cmd = buildCommand({ subcommand: "check", target: "a'b$(id)" })
|
|
61
|
+
expect(cmd).toContain("xp-gate check 'a'\\''b$(id)'")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("builds gate-principles", () => {
|
|
65
|
+
expect(buildCommand({ subcommand: "principles", target: "src/x.ts" })).toContain(
|
|
66
|
+
"xp-gate principles 'src/x.ts'",
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("builds gate-arch with the default config", () => {
|
|
71
|
+
expect(buildCommand({ subcommand: "arch" })).toContain("xp-gate arch --config 'architecture.yaml'")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @test REQ-DSH-005
|
|
76
|
+
* @intent 验证 xp-gate CLI 缺失时 buildCommand 走 else 分支输出优雅降级安装提示,而非抛错或返回门禁失败
|
|
77
|
+
* @covers AC-DSH-005-01
|
|
78
|
+
*/
|
|
79
|
+
it("emits the graceful-degradation fallback when xp-gate is absent", () => {
|
|
80
|
+
const cmd = buildCommand({ subcommand: "check", target: "src" })
|
|
81
|
+
expect(cmd).toContain("else printf")
|
|
82
|
+
expect(cmd).toContain(FALLBACK_MESSAGE)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe("resolveTarget", () => {
|
|
87
|
+
it("keeps absolute paths unchanged", () => {
|
|
88
|
+
expect(resolveTarget("/tmp/x", "/workspace")).toBe("/tmp/x")
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("resolves relative paths against the session cwd", () => {
|
|
92
|
+
expect(resolveTarget("src", "/workspace")).toBe("/workspace/src")
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe("GATE_WHITELIST", () => {
|
|
97
|
+
it("contains only known xp-gate gate aliases", () => {
|
|
98
|
+
expect(GATE_WHITELIST).toContain("principles")
|
|
99
|
+
expect(GATE_WHITELIST).toContain("arch")
|
|
100
|
+
expect(GATE_WHITELIST).toContain("secrets")
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path"
|
|
2
|
+
|
|
3
|
+
/** Shown when the global `xp-gate` CLI is not on PATH. */
|
|
4
|
+
export const FALLBACK_MESSAGE =
|
|
5
|
+
"[XP-Gate] xp-gate CLI not installed. Install it with: npm install -g @boyingliu01/xp-gate"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Allowlist of xp-gate gate aliases that are safe to invoke standalone
|
|
9
|
+
* (non-preCommitOnly gates + common aliases). Anything outside this set is
|
|
10
|
+
* rejected before reaching the shell.
|
|
11
|
+
*/
|
|
12
|
+
export const GATE_WHITELIST = [
|
|
13
|
+
"duplicates",
|
|
14
|
+
"complexity",
|
|
15
|
+
"principles",
|
|
16
|
+
"arch",
|
|
17
|
+
"architecture",
|
|
18
|
+
"iac",
|
|
19
|
+
"secrets",
|
|
20
|
+
"sast",
|
|
21
|
+
] as const
|
|
22
|
+
|
|
23
|
+
export type GateId = (typeof GATE_WHITELIST)[number]
|
|
24
|
+
|
|
25
|
+
/** POSIX single-quote shell escaping: `'` becomes `'\''`. */
|
|
26
|
+
export function shq(value: string): string {
|
|
27
|
+
return `'${value.replace(/'/g, "'\\''")}'`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isGateAllowed(gate: string): boolean {
|
|
31
|
+
return (GATE_WHITELIST as readonly string[]).includes(gate)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BuildCommandOptions {
|
|
35
|
+
subcommand: "check" | "principles" | "arch"
|
|
36
|
+
target?: string
|
|
37
|
+
gates?: readonly string[]
|
|
38
|
+
config?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildInner(options: BuildCommandOptions): string {
|
|
42
|
+
const { subcommand, target, gates, config } = options
|
|
43
|
+
if (subcommand === "check") {
|
|
44
|
+
const tokens = ["xp-gate", "check"]
|
|
45
|
+
if (target !== undefined) tokens.push(shq(target))
|
|
46
|
+
if (gates !== undefined && gates.length > 0) {
|
|
47
|
+
// Defense-in-depth: the JSON-schema enum already rejects unknown gate ids,
|
|
48
|
+
// but we also whitelist here so a non-whitelisted value can never reach the shell.
|
|
49
|
+
const allowed = gates.filter(isGateAllowed)
|
|
50
|
+
if (allowed.length > 0) tokens.push("--gates", shq(allowed.join(",")))
|
|
51
|
+
}
|
|
52
|
+
return tokens.join(" ")
|
|
53
|
+
}
|
|
54
|
+
if (subcommand === "principles") {
|
|
55
|
+
const tokens = ["xp-gate", "principles"]
|
|
56
|
+
if (target !== undefined) tokens.push(shq(target))
|
|
57
|
+
return tokens.join(" ")
|
|
58
|
+
}
|
|
59
|
+
return ["xp-gate", "arch", "--config", shq(config ?? "architecture.yaml")].join(" ")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build a single guarded shell command: runs `xp-gate …` when the global CLI
|
|
64
|
+
* is present, otherwise prints the graceful-degradation hint. A single
|
|
65
|
+
* `if command -v` avoids a separate probe/run race.
|
|
66
|
+
*/
|
|
67
|
+
export function buildCommand(options: BuildCommandOptions): string {
|
|
68
|
+
const inner = buildInner(options)
|
|
69
|
+
return `if command -v xp-gate >/dev/null 2>&1; then ${inner}; else printf '%s\\n' ${shq(FALLBACK_MESSAGE)}; fi`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Resolve a model-supplied path against the session cwd; absolutes pass through. */
|
|
73
|
+
export function resolveTarget(rawPath: string, cwd: string): string {
|
|
74
|
+
return isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath)
|
|
75
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { join, resolve } from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
|
|
6
|
+
const PKG_ROOT = resolve(fileURLToPath(new URL("..", import.meta.url)))
|
|
7
|
+
|
|
8
|
+
interface PkgShape {
|
|
9
|
+
main?: string
|
|
10
|
+
files?: string[]
|
|
11
|
+
scripts?: { prepack?: string }
|
|
12
|
+
publishConfig?: { access?: string }
|
|
13
|
+
dsh?: { bundle?: { patch?: string } }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readPkg(): PkgShape {
|
|
17
|
+
return JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")) as PkgShape
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("packaging contract", () => {
|
|
21
|
+
/**
|
|
22
|
+
* @test REQ-DSH-001
|
|
23
|
+
* @intent 验证 package.json 声明 dsh.bundle.patch 且 cordis.patch.yml 插入 id=tool-xp-gate + 包名
|
|
24
|
+
* @covers AC-DSH-001-01
|
|
25
|
+
*/
|
|
26
|
+
it("declares dsh.bundle.patch and a cordis patch entry", () => {
|
|
27
|
+
expect(readPkg().dsh?.bundle?.patch).toBe("./cordis.patch.yml")
|
|
28
|
+
const patch = readFileSync(join(PKG_ROOT, "cordis.patch.yml"), "utf8")
|
|
29
|
+
expect(patch).toContain("id: tool-xp-gate")
|
|
30
|
+
expect(patch).toContain("@boyingliu01/dsh-plugin-xp-gate")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @test REQ-DSH-010
|
|
35
|
+
* @intent 验证打包契约:main/exports 指向 lib/index.js,files 白名单,prepack 构建,publishConfig.access=public
|
|
36
|
+
* @covers AC-DSH-010-01
|
|
37
|
+
*/
|
|
38
|
+
it("satisfies the npm packaging contract", () => {
|
|
39
|
+
const pkg = readPkg()
|
|
40
|
+
expect(pkg.main).toBe("./lib/index.js")
|
|
41
|
+
expect(pkg.files).toContain("lib/")
|
|
42
|
+
expect(pkg.files).toContain("cordis.patch.yml")
|
|
43
|
+
expect(pkg.files).toContain("skills/")
|
|
44
|
+
expect(pkg.scripts?.prepack).toContain("npm run build")
|
|
45
|
+
expect(pkg.publishConfig?.access).toBe("public")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @test REQ-DSH-008
|
|
50
|
+
* @intent 验证 TDD 契约:所有测试文件携带 @test/@intent/@covers 注解,覆盖三工具 + fallback + 取消/超时 + shq 转义
|
|
51
|
+
* @covers AC-DSH-008-01
|
|
52
|
+
*/
|
|
53
|
+
it("every test file carries @test/@intent/@covers annotations", () => {
|
|
54
|
+
const files = ["command.test.ts", "gate-runner.test.ts", "index.test.ts", "skills.test.ts"]
|
|
55
|
+
for (const f of files) {
|
|
56
|
+
const content = readFileSync(join(PKG_ROOT, "src", f), "utf8")
|
|
57
|
+
expect(content, `${f}: @test`).toMatch(/@test\s+REQ-\S+/)
|
|
58
|
+
expect(content, `${f}: @intent`).toMatch(/@intent\s+/)
|
|
59
|
+
expect(content, `${f}: @covers`).toMatch(/@covers\s+AC-\S+/)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import type { ShellExecRequest, ShellRunResult } from "@deepseek-ai/dsh-shell"
|
|
3
|
+
import { renderGateResult, runXpGate, type XpGateShell } from "./gate-runner.js"
|
|
4
|
+
|
|
5
|
+
function result(over: Partial<ShellRunResult> = {}): ShellRunResult {
|
|
6
|
+
return {
|
|
7
|
+
exitCode: 0,
|
|
8
|
+
signal: null,
|
|
9
|
+
timedOut: false,
|
|
10
|
+
aborted: false,
|
|
11
|
+
timeoutMs: 1000,
|
|
12
|
+
stdout: { text: "ok", truncated: false },
|
|
13
|
+
stderr: { text: "", truncated: false },
|
|
14
|
+
...over,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeShell(finalResult: ShellRunResult): { shell: XpGateShell; requests: ShellExecRequest[] } {
|
|
19
|
+
const requests: ShellExecRequest[] = []
|
|
20
|
+
const shell: XpGateShell = {
|
|
21
|
+
resolve(req) {
|
|
22
|
+
requests.push(req)
|
|
23
|
+
return {
|
|
24
|
+
command: req.command,
|
|
25
|
+
workdir: req.workdir ?? "/workspace",
|
|
26
|
+
timeoutMs: req.timeoutMs ?? 1000,
|
|
27
|
+
stdoutMaxBytes: 1_000_000,
|
|
28
|
+
signal: req.signal,
|
|
29
|
+
sandboxPolicy: undefined,
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
run() {
|
|
33
|
+
return Promise.resolve(finalResult)
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
return { shell, requests }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("renderGateResult", () => {
|
|
40
|
+
it("renders stdout text", () => {
|
|
41
|
+
expect(renderGateResult(result())).toBe("ok")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("appends [exit code: N] on non-zero exit", () => {
|
|
45
|
+
const r = renderGateResult(result({ exitCode: 2, stdout: { text: "boom", truncated: false } }))
|
|
46
|
+
expect(r).toContain("boom")
|
|
47
|
+
expect(r).toContain("[exit code: 2]")
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("marks a timeout without throwing", () => {
|
|
51
|
+
const r = renderGateResult(result({ timedOut: true, exitCode: null, stdout: { text: "", truncated: false } }))
|
|
52
|
+
expect(r).toContain("[timed out after 1000ms]")
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("marks a sandbox denial", () => {
|
|
56
|
+
const r = renderGateResult(result({ sandbox: { mode: "workspace-write", denied: true } }))
|
|
57
|
+
expect(r).toContain("denied")
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("includes stderr and truncation markers", () => {
|
|
61
|
+
const r = renderGateResult(
|
|
62
|
+
result({
|
|
63
|
+
stderr: { text: "err", truncated: false },
|
|
64
|
+
stdout: { text: "tail", truncated: true, spillPath: "/tmp/full" },
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
expect(r).toContain("err")
|
|
68
|
+
expect(r).toContain("truncated")
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe("runXpGate", () => {
|
|
73
|
+
it("passes command/workdir/timeoutMs/signal to the shell", async () => {
|
|
74
|
+
const signal = new AbortController().signal
|
|
75
|
+
const { shell, requests } = makeShell(result())
|
|
76
|
+
await runXpGate(shell, signal, "xp-gate check 'src'", "/workspace", 5000)
|
|
77
|
+
expect(requests[0]?.command).toBe("xp-gate check 'src'")
|
|
78
|
+
expect(requests[0]?.workdir).toBe("/workspace")
|
|
79
|
+
expect(requests[0]?.timeoutMs).toBe(5000)
|
|
80
|
+
expect(requests[0]?.signal).toBe(signal)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @test REQ-DSH-007
|
|
85
|
+
* @intent 验证取消与超时分离:result.aborted 抛 code=ABORTED 的 AbortError(不悬挂),result.timedOut 仅渲染标记不抛错
|
|
86
|
+
* @covers AC-DSH-007-01
|
|
87
|
+
*/
|
|
88
|
+
it("throws an AbortError (code ABORTED) only on cancellation, not on timeout", async () => {
|
|
89
|
+
const { shell } = makeShell(result({ aborted: true, exitCode: null }))
|
|
90
|
+
await expect(runXpGate(shell, new AbortController().signal, "cmd", "/w", 1000)).rejects.toMatchObject({
|
|
91
|
+
name: "AbortError",
|
|
92
|
+
code: "ABORTED",
|
|
93
|
+
})
|
|
94
|
+
// timeout resolves (render marker) rather than throwing
|
|
95
|
+
const timeoutShell = makeShell(result({ timedOut: true, exitCode: null, stdout: { text: "", truncated: false } })).shell
|
|
96
|
+
await expect(runXpGate(timeoutShell, new AbortController().signal, "cmd", "/w", 1000)).resolves.toContain(
|
|
97
|
+
"[timed out after 1000ms]",
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { TOOL_ABORTED } from "@deepseek-ai/dsh-tools"
|
|
2
|
+
import { HarnessError } from "@deepseek-ai/dsh-llm"
|
|
3
|
+
import type { ShellExecRequest, ShellExecSpec, ShellRunResult } from "@deepseek-ai/dsh-shell"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Structural view of `ctx.shell` so the runner is testable with a fake shell
|
|
7
|
+
* without importing the abstract `ShellExecutor` service.
|
|
8
|
+
*/
|
|
9
|
+
export interface XpGateShell {
|
|
10
|
+
resolve(request: ShellExecRequest): ShellExecSpec
|
|
11
|
+
run(spec: ShellExecSpec): Promise<ShellRunResult>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Render a completed shell run as a single model-facing text block. */
|
|
15
|
+
export function renderGateResult(result: ShellRunResult): string {
|
|
16
|
+
const lines: string[] = []
|
|
17
|
+
const stdout = result.stdout.text.trim()
|
|
18
|
+
if (stdout.length > 0) lines.push(stdout)
|
|
19
|
+
const stderr = result.stderr.text.trim()
|
|
20
|
+
if (stderr.length > 0) lines.push(`[stderr] ${stderr}`)
|
|
21
|
+
if (result.timedOut) lines.push(`[timed out after ${result.timeoutMs}ms]`)
|
|
22
|
+
if (result.exitCode !== null && result.exitCode !== 0) lines.push(`[exit code: ${result.exitCode}]`)
|
|
23
|
+
if (result.signal !== null) lines.push(`[signal: ${result.signal}]`)
|
|
24
|
+
if (result.sandbox?.denied === true) {
|
|
25
|
+
lines.push(`[sandbox: file access denied under ${result.sandbox.mode} mode]`)
|
|
26
|
+
}
|
|
27
|
+
if (result.stdout.truncated) {
|
|
28
|
+
const spill = result.stdout.spillPath !== undefined ? `, full output: ${result.stdout.spillPath}` : ""
|
|
29
|
+
lines.push(`[stdout truncated${spill}]`)
|
|
30
|
+
}
|
|
31
|
+
if (result.stderr.truncated) {
|
|
32
|
+
const spill = result.stderr.spillPath !== undefined ? `, full output: ${result.stderr.spillPath}` : ""
|
|
33
|
+
lines.push(`[stderr truncated${spill}]`)
|
|
34
|
+
}
|
|
35
|
+
return lines.length > 0 ? lines.join("\n") : "[XP-Gate] command completed (no output)."
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Run an xp-gate command through the harness shell seam. Nonzero exits, timeouts,
|
|
40
|
+
* and sandbox denials resolve (and render) rather than reject; only cancellation
|
|
41
|
+
* (the caller's AbortSignal fired first) throws an AbortError with code ABORTED.
|
|
42
|
+
*/
|
|
43
|
+
export async function runXpGate(
|
|
44
|
+
shell: XpGateShell,
|
|
45
|
+
signal: AbortSignal,
|
|
46
|
+
command: string,
|
|
47
|
+
workdir: string,
|
|
48
|
+
timeoutMs: number,
|
|
49
|
+
): Promise<string> {
|
|
50
|
+
const spec = shell.resolve({ command, workdir, timeoutMs, signal })
|
|
51
|
+
const result = await shell.run(spec)
|
|
52
|
+
if (result.aborted) {
|
|
53
|
+
const error = new HarnessError("tool call aborted", TOOL_ABORTED)
|
|
54
|
+
error.name = "AbortError"
|
|
55
|
+
throw error
|
|
56
|
+
}
|
|
57
|
+
return renderGateResult(result)
|
|
58
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import type { Context } from "@deepseek-ai/cordis"
|
|
3
|
+
import type { ShellExecRequest, ShellExecSpec, ShellRunResult } from "@deepseek-ai/dsh-shell"
|
|
4
|
+
import { apply } from "./index.js"
|
|
5
|
+
|
|
6
|
+
interface CapturedTool {
|
|
7
|
+
name: string
|
|
8
|
+
execute: (args: unknown, exec: unknown) => Promise<unknown>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function makeCtx(stdoutText: string) {
|
|
12
|
+
const captured: unknown[] = []
|
|
13
|
+
const requests: ShellExecRequest[] = []
|
|
14
|
+
const shell = {
|
|
15
|
+
resolve(req: ShellExecRequest): ShellExecSpec {
|
|
16
|
+
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
|
+
}
|
|
25
|
+
},
|
|
26
|
+
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
|
+
})
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
const ctx = { tools: { register: (def: unknown) => captured.push(def) }, shell } as unknown as Context
|
|
39
|
+
return { ctx, captured, requests }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("apply", () => {
|
|
43
|
+
it("registers all three tools", () => {
|
|
44
|
+
const { ctx, captured } = makeCtx("ok")
|
|
45
|
+
apply(ctx)
|
|
46
|
+
const names = captured.map((c) => (c as CapturedTool).name)
|
|
47
|
+
expect(names).toContain("gate-check")
|
|
48
|
+
expect(names).toContain("gate-principles")
|
|
49
|
+
expect(names).toContain("gate-arch")
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @test REQ-DSH-002
|
|
54
|
+
* @intent 验证 gate-check 工具注册(path required / gates optional)并端到端接线 args→命令→shell
|
|
55
|
+
* @covers AC-DSH-002-01
|
|
56
|
+
*/
|
|
57
|
+
it("gate-check execute builds the guarded command and returns rendered stdout", async () => {
|
|
58
|
+
const { ctx, captured, requests } = makeCtx("gate report")
|
|
59
|
+
apply(ctx)
|
|
60
|
+
const gateCheck = captured.find((c) => (c as CapturedTool).name === "gate-check") as CapturedTool
|
|
61
|
+
const out = await gateCheck.execute(
|
|
62
|
+
{ path: "/repo/src", gates: ["principles"] },
|
|
63
|
+
{ signal: new AbortController().signal },
|
|
64
|
+
)
|
|
65
|
+
expect(out).toBe("gate report")
|
|
66
|
+
expect(requests[0]?.command).toContain("xp-gate check '/repo/src' --gates 'principles'")
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @test REQ-DSH-003
|
|
71
|
+
* @intent 验证 gate-principles 工具执行生成 `xp-gate principles <path>` 守卫命令
|
|
72
|
+
* @covers AC-DSH-003-01
|
|
73
|
+
*/
|
|
74
|
+
it("gate-principles execute builds `xp-gate principles <path>`", async () => {
|
|
75
|
+
const { ctx, captured, requests } = makeCtx("principles report")
|
|
76
|
+
apply(ctx)
|
|
77
|
+
const tool = captured.find((c) => (c as CapturedTool).name === "gate-principles") as CapturedTool
|
|
78
|
+
const out = await tool.execute({ path: "/repo/src/app.ts" }, { signal: new AbortController().signal })
|
|
79
|
+
expect(out).toBe("principles report")
|
|
80
|
+
expect(requests[0]?.command).toContain("xp-gate principles '/repo/src/app.ts'")
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @test REQ-DSH-004
|
|
85
|
+
* @intent 验证 gate-arch 工具执行生成 `xp-gate arch --config <config>`(缺省 architecture.yaml)
|
|
86
|
+
* @covers AC-DSH-004-01
|
|
87
|
+
*/
|
|
88
|
+
it("gate-arch execute builds `xp-gate arch` with default and custom config", async () => {
|
|
89
|
+
const { ctx, captured, requests } = makeCtx("arch report")
|
|
90
|
+
apply(ctx)
|
|
91
|
+
const tool = captured.find((c) => (c as CapturedTool).name === "gate-arch") as CapturedTool
|
|
92
|
+
await tool.execute({}, { signal: new AbortController().signal })
|
|
93
|
+
expect(requests[0]?.command).toContain("xp-gate arch --config 'architecture.yaml'")
|
|
94
|
+
await tool.execute({ config: "custom.yaml" }, { signal: new AbortController().signal })
|
|
95
|
+
expect(requests[1]?.command).toContain("xp-gate arch --config 'custom.yaml'")
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { defineTool } from "@deepseek-ai/dsh-tools"
|
|
2
|
+
import type { Context } from "@deepseek-ai/cordis"
|
|
3
|
+
import { GATE_WHITELIST, buildCommand, resolveTarget } from "./command.js"
|
|
4
|
+
import { runXpGate } from "./gate-runner.js"
|
|
5
|
+
|
|
6
|
+
export const name = "tool-xp-gate"
|
|
7
|
+
export const inject = ["tools", "shell"]
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 120_000
|
|
10
|
+
|
|
11
|
+
export function apply(ctx: Context, _config: unknown = {}): void {
|
|
12
|
+
const shell = ctx.shell
|
|
13
|
+
|
|
14
|
+
ctx.tools.register(
|
|
15
|
+
defineTool({
|
|
16
|
+
name: "gate-check",
|
|
17
|
+
description:
|
|
18
|
+
"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.",
|
|
19
|
+
parameters: {
|
|
20
|
+
path: {
|
|
21
|
+
type: "string",
|
|
22
|
+
required: true,
|
|
23
|
+
description: "File or directory path to check (absolute or relative to the workspace).",
|
|
24
|
+
},
|
|
25
|
+
gates: {
|
|
26
|
+
type: "array",
|
|
27
|
+
items: { type: "string", enum: GATE_WHITELIST },
|
|
28
|
+
description: "Optional gate subset to run (e.g. ['principles', 'arch']).",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
output: {
|
|
32
|
+
schema: { type: "string" },
|
|
33
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
34
|
+
},
|
|
35
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
36
|
+
async execute(args, exec) {
|
|
37
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd()
|
|
38
|
+
const target = resolveTarget(args.path, cwd)
|
|
39
|
+
const command = buildCommand({ subcommand: "check", target, gates: args.gates })
|
|
40
|
+
return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS)
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
ctx.tools.register(
|
|
46
|
+
defineTool({
|
|
47
|
+
name: "gate-principles",
|
|
48
|
+
description:
|
|
49
|
+
"Run the 14 Clean Code / SOLID rules (xp-gate principles, Gate 4) on a file or directory and return the principles report.",
|
|
50
|
+
parameters: {
|
|
51
|
+
path: {
|
|
52
|
+
type: "string",
|
|
53
|
+
required: true,
|
|
54
|
+
description: "File or directory path to check (absolute or relative to the workspace).",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
output: {
|
|
58
|
+
schema: { type: "string" },
|
|
59
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
60
|
+
},
|
|
61
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
62
|
+
async execute(args, exec) {
|
|
63
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd()
|
|
64
|
+
const target = resolveTarget(args.path, cwd)
|
|
65
|
+
const command = buildCommand({ subcommand: "principles", target })
|
|
66
|
+
return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS)
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
ctx.tools.register(
|
|
72
|
+
defineTool({
|
|
73
|
+
name: "gate-arch",
|
|
74
|
+
description:
|
|
75
|
+
"Run architecture validation (xp-gate arch, Gate 6) using the given config file and return the architecture report. Defaults to architecture.yaml.",
|
|
76
|
+
parameters: {
|
|
77
|
+
config: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "Architecture config file path (defaults to architecture.yaml).",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
output: {
|
|
83
|
+
schema: { type: "string" },
|
|
84
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
85
|
+
},
|
|
86
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
87
|
+
async execute(args, exec) {
|
|
88
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd()
|
|
89
|
+
const command = buildCommand({ subcommand: "arch", config: args.config })
|
|
90
|
+
return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS)
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs"
|
|
3
|
+
import { join, resolve } from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
import { parse } from "yaml"
|
|
6
|
+
|
|
7
|
+
/** Mirrors `SKILL_NAME` in @deepseek-ai/dsh-skill. */
|
|
8
|
+
const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
9
|
+
|
|
10
|
+
const EXPECTED_SKILLS = [
|
|
11
|
+
"admin-template-guidelines",
|
|
12
|
+
"batch-grill-me",
|
|
13
|
+
"delphi-review",
|
|
14
|
+
"domain-modeling",
|
|
15
|
+
"grilling",
|
|
16
|
+
"grill-with-docs",
|
|
17
|
+
"improve-codebase-architecture",
|
|
18
|
+
"ralph-loop",
|
|
19
|
+
"sprint-flow",
|
|
20
|
+
"test-driven-development",
|
|
21
|
+
"test-specification-alignment",
|
|
22
|
+
"to-issues",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const SKILLS_DIR = resolve(fileURLToPath(new URL("..", import.meta.url)), "skills")
|
|
26
|
+
|
|
27
|
+
function readFrontmatter(file: string): Record<string, unknown> {
|
|
28
|
+
const raw = readFileSync(file, "utf8")
|
|
29
|
+
const m = raw.match(/^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n/)
|
|
30
|
+
if (!m) throw new Error(`${file}: missing YAML frontmatter`)
|
|
31
|
+
return parse(m[1]) as Record<string, unknown>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("bundled skills", () => {
|
|
35
|
+
it("ships all 12 skills as directories", () => {
|
|
36
|
+
expect(existsSync(SKILLS_DIR)).toBe(true)
|
|
37
|
+
const dirs = readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
38
|
+
.filter((d) => d.isDirectory())
|
|
39
|
+
.map((d) => d.name)
|
|
40
|
+
.sort()
|
|
41
|
+
expect(dirs).toEqual([...EXPECTED_SKILLS].sort())
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @test REQ-DSH-006
|
|
46
|
+
* @intent 验证随包 12 个 skills 目录里各 SKILL.md 的 frontmatter 满足 DSH 契约(name kebab-case + description 非空)
|
|
47
|
+
* @covers AC-DSH-006-01
|
|
48
|
+
*/
|
|
49
|
+
it("every SKILL.md satisfies the DSH frontmatter contract", () => {
|
|
50
|
+
for (const name of EXPECTED_SKILLS) {
|
|
51
|
+
const fm = readFrontmatter(join(SKILLS_DIR, name, "SKILL.md"))
|
|
52
|
+
expect(fm.name, `${name}: frontmatter.name`).toBeTypeOf("string")
|
|
53
|
+
expect(SKILL_NAME.test(fm.name as string), `${name}: kebab-case name`).toBe(true)
|
|
54
|
+
expect(fm.description, `${name}: frontmatter.description`).toBeTypeOf("string")
|
|
55
|
+
expect((fm.description as string).trim().length, `${name}: non-empty description`).toBeGreaterThan(0)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": false,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"declarationMap": true,
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"outDir": "lib",
|
|
9
|
+
"rootDir": "src"
|
|
10
|
+
},
|
|
11
|
+
"include": ["src"],
|
|
12
|
+
"exclude": ["node_modules", "lib", "src/**/*.test.ts"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noImplicitAny": true,
|
|
9
|
+
"noUnusedLocals": true,
|
|
10
|
+
"noUnusedParameters": true,
|
|
11
|
+
"noImplicitReturns": true,
|
|
12
|
+
"noFallthroughCasesInSwitch": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"esModuleInterop": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"noEmit": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src"]
|
|
19
|
+
}
|