@boyingliu01/xp-gate 0.31.3 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/java.sh +256 -4
- package/adapters/powershell.sh +45 -6
- package/bin/xp-gate.js +8 -2
- package/build-integrity/__tests__/gate-10.test.ts +88 -1
- package/build-integrity/gate-10.ts +51 -16
- package/dsh.bundle.patch.yml +15 -0
- package/gate-13-compiler-strictness.sh +1031 -0
- package/gate-3.sh +48 -13
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +48 -13
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +9 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +269 -13
- package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
- package/lib/__tests__/gate-runner.test.js +29 -2
- package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
- package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
- package/lib/__tests__/shared-utils.test.js +21 -0
- package/lib/check.js +6 -2
- package/lib/detect-deps.js +42 -10
- package/lib/doctor-tui.js +111 -21
- package/lib/gate-runner.js +111 -37
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +48 -6
- package/lib/shared-utils.js +8 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/gate-m3.ts +57 -23
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
- package/mutation/__tests__/test-filter-parity.test.ts +5 -2
- package/mutation/gate-m.ts +30 -24
- package/mutation/runners/mutmut-runner.ts +80 -30
- package/mutation/runners/pitest-runner.ts +60 -42
- package/package.json +14 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/lib/command.d.ts +38 -0
- package/plugins/dsh/lib/command.d.ts.map +1 -0
- package/plugins/dsh/lib/command.js +87 -0
- package/plugins/dsh/lib/credentials.d.ts +39 -0
- package/plugins/dsh/lib/credentials.d.ts.map +1 -0
- package/plugins/dsh/lib/credentials.js +57 -0
- package/plugins/dsh/lib/delphi-config.d.ts +129 -0
- package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-config.js +369 -0
- package/plugins/dsh/lib/delphi-run.d.ts +24 -0
- package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-run.js +113 -0
- package/plugins/dsh/lib/delphi.d.ts +94 -0
- package/plugins/dsh/lib/delphi.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi.js +207 -0
- package/plugins/dsh/lib/dialect.d.ts +24 -0
- package/plugins/dsh/lib/dialect.d.ts.map +1 -0
- package/plugins/dsh/lib/dialect.js +36 -0
- package/plugins/dsh/lib/evidence.d.ts +70 -0
- package/plugins/dsh/lib/evidence.d.ts.map +1 -0
- package/plugins/dsh/lib/evidence.js +92 -0
- package/plugins/dsh/lib/gate-runner.d.ts +49 -0
- package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
- package/plugins/dsh/lib/gate-runner.js +95 -0
- package/plugins/dsh/lib/git-context.d.ts +26 -0
- package/plugins/dsh/lib/git-context.d.ts.map +1 -0
- package/plugins/dsh/lib/git-context.js +30 -0
- package/plugins/dsh/lib/hooks.d.ts +42 -0
- package/plugins/dsh/lib/hooks.d.ts.map +1 -0
- package/plugins/dsh/lib/hooks.js +113 -0
- package/plugins/dsh/lib/index.d.ts +20 -0
- package/plugins/dsh/lib/index.d.ts.map +1 -0
- package/plugins/dsh/lib/index.js +430 -0
- package/plugins/dsh/lib/skill-provider.d.ts +15 -0
- package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
- package/plugins/dsh/lib/skill-provider.js +205 -0
- package/plugins/dsh/lib/sprint.d.ts +60 -0
- package/plugins/dsh/lib/sprint.d.ts.map +1 -0
- package/plugins/dsh/lib/sprint.js +92 -0
- package/plugins/dsh/package.json +14 -8
- package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/command-single-form.test.ts +67 -0
- package/plugins/dsh/src/command.test.ts +21 -17
- package/plugins/dsh/src/command.ts +19 -21
- package/plugins/dsh/src/compat.test.ts +200 -0
- package/plugins/dsh/src/contract.test.ts +7 -1
- package/plugins/dsh/src/credentials.test.ts +93 -0
- package/plugins/dsh/src/credentials.ts +71 -0
- package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
- package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
- package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
- package/plugins/dsh/src/delphi-config.ts +146 -53
- package/plugins/dsh/src/delphi-run.ts +27 -4
- package/plugins/dsh/src/delphi.ts +133 -65
- package/plugins/dsh/src/evidence.test.ts +74 -0
- package/plugins/dsh/src/evidence.ts +141 -0
- package/plugins/dsh/src/gate-runner.test.ts +14 -6
- package/plugins/dsh/src/gate-runner.ts +68 -5
- package/plugins/dsh/src/git-context.test.ts +39 -0
- package/plugins/dsh/src/git-context.ts +38 -0
- package/plugins/dsh/src/hooks.test.ts +87 -0
- package/plugins/dsh/src/hooks.ts +126 -0
- package/plugins/dsh/src/index.test.ts +63 -6
- package/plugins/dsh/src/index.ts +340 -13
- package/plugins/dsh/src/skill-provider.test.ts +194 -0
- package/plugins/dsh/src/skill-provider.ts +231 -0
- package/plugins/dsh/src/sprint.test.ts +64 -0
- package/plugins/dsh/src/sprint.ts +122 -0
- package/plugins/dsh/src/tri-state.test.ts +123 -0
- package/plugins/opencode/index.ts +192 -58
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/analyze-read-failure.test.ts +70 -0
- package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
- package/principles/__tests__/baseline-prune.test.ts +61 -0
- package/principles/__tests__/cli-files-separators.test.ts +69 -0
- package/principles/__tests__/config-merge-warnings.test.ts +148 -0
- package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
- package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
- package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
- package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
- package/principles/adapters/cpp.ts +102 -50
- package/principles/adapters/java.ts +22 -3
- package/principles/adapters/typescript.ts +548 -178
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- package/principles/baseline.ts +58 -70
- package/principles/config.ts +56 -35
- package/principles/index.ts +122 -40
- package/principles/rules/clean-code/deep-nesting.ts +36 -22
- package/principles/rules/clean-code/unused-imports.ts +30 -19
- package/principles/rules/solid/dip.ts +30 -20
- package/principles/rules/solid/lsp.ts +17 -2
- package/principles/rules/solid/ocp.ts +33 -23
- package/principles/rules/solid/srp.ts +65 -23
- package/scripts/delphi-external-review.cjs +25 -2
- package/scripts/sync-package-content.js +28 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
5
|
+
import z from "@deepseek-ai/schemastery";
|
|
6
|
+
import { GATE_WHITELIST, buildCommand, detectDialect, resolveTarget } from "./command.js";
|
|
7
|
+
import { runXpGate } from "./gate-runner.js";
|
|
8
|
+
import { DelphiSetupError, listCredentialReferences, loadDelphiConfig, toDelphiOptions } from "./delphi-config.js";
|
|
9
|
+
import { formatOutcome, runReview } from "./delphi-run.js";
|
|
10
|
+
import { buildEvidence } from "./evidence.js";
|
|
11
|
+
import { readGitContext } from "./git-context.js";
|
|
12
|
+
import { delphiGuardDecision, isCheckablePath, isEditTool } from "./hooks.js";
|
|
13
|
+
import { PHASE_STATUSES, SPRINT_SUBCOMMANDS, buildSprintCommand } from "./sprint.js";
|
|
14
|
+
import { createSkillProvider } from "./skill-provider.js";
|
|
15
|
+
import { createCredentialResolver } from "./credentials.js";
|
|
16
|
+
export const name = "tool-xp-gate";
|
|
17
|
+
// "skills" is required: the bundled skill provider registers during apply(), and
|
|
18
|
+
// without it ctx.skills may not be mounted yet when this plugin starts.
|
|
19
|
+
export const inject = ["tools", "shell", "skills"];
|
|
20
|
+
/**
|
|
21
|
+
* Absolute path to the skills directory shipped alongside this module.
|
|
22
|
+
*
|
|
23
|
+
* Resolved from `import.meta.url` rather than `process.cwd()`: after an npm
|
|
24
|
+
* install the plugin lives in the profile's node_modules, so the working
|
|
25
|
+
* directory says nothing about where its assets are. This is the same
|
|
26
|
+
* resolution the official `@deepseek-ai/dsh-skill-office` uses.
|
|
27
|
+
*/
|
|
28
|
+
function bundledSkillsRoot() {
|
|
29
|
+
return fileURLToPath(new URL("../skills/", import.meta.url));
|
|
30
|
+
}
|
|
31
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
32
|
+
// Multi-round reviews over large inputs are slow; the transport applies its
|
|
33
|
+
// own per-request timeout on top of this ceiling.
|
|
34
|
+
const DELPHI_TIMEOUT_MS = 900_000;
|
|
35
|
+
/** The document Gate MW reads at push time. */
|
|
36
|
+
const EVIDENCE_FILE = ".code-walkthrough-result.json";
|
|
37
|
+
/**
|
|
38
|
+
* Files already reported on in this process.
|
|
39
|
+
*
|
|
40
|
+
* Server-lifetime rather than session-lifetime on purpose: the addon outlives any
|
|
41
|
+
* one session, and re-reporting the same unchanged file to a second session would
|
|
42
|
+
* spend a subprocess to repeat something already said.
|
|
43
|
+
*/
|
|
44
|
+
const checkedThisSession = new Set();
|
|
45
|
+
// Boy Scout (#447): gate-check's `gates` parameter nests one level too deep
|
|
46
|
+
// inline (parameters → gates → items), so the schema lives at module scope.
|
|
47
|
+
const GATES_PARAMETER = {
|
|
48
|
+
type: "array",
|
|
49
|
+
items: { type: "string", enum: GATE_WHITELIST },
|
|
50
|
+
description: "Optional gate subset to run (e.g. ['principles', 'arch']).",
|
|
51
|
+
};
|
|
52
|
+
// Boy Scout (#447): apply() used to inline four register(defineTool({...}))
|
|
53
|
+
// blocks (149 lines, 5 brace levels). Each tool now lives in its own factory;
|
|
54
|
+
// apply() is pure wiring.
|
|
55
|
+
function gateCheckTool(shell, dialect) {
|
|
56
|
+
return defineTool({
|
|
57
|
+
name: "gate-check",
|
|
58
|
+
description: "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.",
|
|
59
|
+
parameters: {
|
|
60
|
+
path: {
|
|
61
|
+
type: "string",
|
|
62
|
+
required: true,
|
|
63
|
+
description: "File or directory path to check (absolute or relative to the workspace).",
|
|
64
|
+
},
|
|
65
|
+
gates: GATES_PARAMETER,
|
|
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: "check", target, gates: args.gates, dialect });
|
|
76
|
+
return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function gatePrinciplesTool(shell, dialect) {
|
|
81
|
+
return defineTool({
|
|
82
|
+
name: "gate-principles",
|
|
83
|
+
description: "Run the 14 Clean Code / SOLID rules (xp-gate principles, Gate 4) on a file or directory and return the principles report.",
|
|
84
|
+
parameters: {
|
|
85
|
+
path: {
|
|
86
|
+
type: "string",
|
|
87
|
+
required: true,
|
|
88
|
+
description: "File or directory path to check (absolute or relative to the workspace).",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
output: {
|
|
92
|
+
schema: { type: "string" },
|
|
93
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
94
|
+
},
|
|
95
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
96
|
+
async execute(args, exec) {
|
|
97
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
98
|
+
const target = resolveTarget(args.path, cwd);
|
|
99
|
+
const command = buildCommand({ subcommand: "principles", target, dialect });
|
|
100
|
+
return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function gateArchTool(shell, dialect) {
|
|
105
|
+
return defineTool({
|
|
106
|
+
name: "gate-arch",
|
|
107
|
+
description: "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).",
|
|
108
|
+
parameters: {
|
|
109
|
+
config: {
|
|
110
|
+
type: "string",
|
|
111
|
+
description: "Optional architecture config file (opts into archlint --config; the default built-in-rules path needs none).",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
output: {
|
|
115
|
+
schema: { type: "string" },
|
|
116
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
117
|
+
},
|
|
118
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
119
|
+
async execute(args, exec) {
|
|
120
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
121
|
+
const command = buildCommand({ subcommand: "arch", config: args.config, dialect });
|
|
122
|
+
return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// Delphi multi-model cross-review.
|
|
127
|
+
//
|
|
128
|
+
// DSH's own subagents cannot do this: they inherit one host-configured model,
|
|
129
|
+
// so three "experts" would be three instances of the same model, and Delphi's
|
|
130
|
+
// value rests entirely on the reviewers being DIFFERENT models. This tool
|
|
131
|
+
// therefore calls an external OpenAI-compatible provider directly, with one
|
|
132
|
+
// distinct model per seat.
|
|
133
|
+
// Boy Scout (#447): the review body used to sit inline in execute() at 5 brace
|
|
134
|
+
// levels (defineTool → execute → try → runReview options). Module scope keeps
|
|
135
|
+
// the try/catch semantics identical while dropping two levels.
|
|
136
|
+
/**
|
|
137
|
+
* Write .code-walkthrough-result.json for Gate MW, or explain why it cannot.
|
|
138
|
+
*
|
|
139
|
+
* Returns a suffix for the tool's output rather than throwing: the review itself
|
|
140
|
+
* succeeded, and losing its text because evidence could not be written would be a
|
|
141
|
+
* worse outcome than a clear note that the push gate will not be satisfied.
|
|
142
|
+
*/
|
|
143
|
+
async function writeWalkthroughEvidence(cwd, cfg, outcome) {
|
|
144
|
+
try {
|
|
145
|
+
const lastRound = outcome.rounds[outcome.rounds.length - 1];
|
|
146
|
+
const seats = lastRound.verdicts.map((v) => ({
|
|
147
|
+
role: v.role,
|
|
148
|
+
model: v.requestedModel,
|
|
149
|
+
resolved: v.resolvedModel ?? "",
|
|
150
|
+
}));
|
|
151
|
+
const git = readGitContext(cwd);
|
|
152
|
+
const evidence = buildEvidence({
|
|
153
|
+
head: git.head,
|
|
154
|
+
branch: git.branch,
|
|
155
|
+
seats,
|
|
156
|
+
verdict: outcome.consensus ? "APPROVED" : "REQUEST_CHANGES",
|
|
157
|
+
now: new Date(),
|
|
158
|
+
baseUrl: cfg.baseUrl,
|
|
159
|
+
consensusRatio: outcome.finalRatio,
|
|
160
|
+
});
|
|
161
|
+
const target = join(cwd, EVIDENCE_FILE);
|
|
162
|
+
await writeFile(target, `${JSON.stringify(evidence, null, 2)}\n`, "utf8");
|
|
163
|
+
return `\nEvidence: ${EVIDENCE_FILE} written for ${evidence.commit.slice(0, 8)}.`;
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
return `\nEvidence: not written — ${error.message}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function runDelphiReview(subject, request) {
|
|
170
|
+
const { addressed, maxRounds, cwd, resolveApiKey, signal, writeEvidence } = request;
|
|
171
|
+
try {
|
|
172
|
+
// Resolve credentials BEFORE loading the config: the loader is synchronous and
|
|
173
|
+
// throws as soon as a referenced variable has no value. Reading the reference
|
|
174
|
+
// names first turns "the DSH process never inherited WHALECLOUD_API_KEY" from a
|
|
175
|
+
// hard failure into a lookup in the credential store.
|
|
176
|
+
const apiKeyOverrides = {};
|
|
177
|
+
for (const name of listCredentialReferences(cwd)) {
|
|
178
|
+
const value = await resolveApiKey(name);
|
|
179
|
+
if (value !== undefined && value.length > 0)
|
|
180
|
+
apiKeyOverrides[name] = value;
|
|
181
|
+
}
|
|
182
|
+
const cfg = loadDelphiConfig(cwd, process.env, undefined, apiKeyOverrides);
|
|
183
|
+
const options = toDelphiOptions(cfg);
|
|
184
|
+
const outcome = await runReview({ ...options, maxRounds: maxRounds ?? cfg.maxRounds }, subject, addressed, signal);
|
|
185
|
+
const seats = cfg.seats.map((s) => s.model).join(", ");
|
|
186
|
+
const suffix = writeEvidence
|
|
187
|
+
? await writeWalkthroughEvidence(cwd, cfg, outcome)
|
|
188
|
+
: "";
|
|
189
|
+
return `${formatOutcome(outcome)}\n\nProvider: ${cfg.provider}\nSeats: ${seats}${suffix}`;
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
// Config problems are the common failure; report them as guidance
|
|
193
|
+
// rather than a stack trace.
|
|
194
|
+
if (e instanceof DelphiSetupError)
|
|
195
|
+
return `Delphi review not started: ${e.message}`;
|
|
196
|
+
throw e;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function delphiReviewTool(resolveApiKey) {
|
|
200
|
+
return defineTool({
|
|
201
|
+
name: "delphi-review",
|
|
202
|
+
description: "Run a Delphi cross-review of a subject (a diff, a design, a spec) using three DISTINCT models from an external OpenAI-compatible provider configured in .delphi-config.json. Round 1 is anonymous and concurrent; later rounds feed back the aggregate until the approval ratio reaches the configured threshold (default 90%) or the round cap (default 5). Use this whenever a change needs independent multi-model review rather than a single model's opinion.",
|
|
203
|
+
parameters: {
|
|
204
|
+
subject: {
|
|
205
|
+
type: "string",
|
|
206
|
+
required: true,
|
|
207
|
+
description: "The material to review, inline. For a diff, pass the diff text (or a summarized form if very large).",
|
|
208
|
+
},
|
|
209
|
+
addressed: {
|
|
210
|
+
type: "string",
|
|
211
|
+
description: "Optional description of what changed since a previous round, fed back to later rounds so reviewers can revise their position.",
|
|
212
|
+
},
|
|
213
|
+
maxRounds: {
|
|
214
|
+
type: "number",
|
|
215
|
+
description: "Override the configured round cap (Delphi default is 5).",
|
|
216
|
+
},
|
|
217
|
+
cwd: {
|
|
218
|
+
type: "string",
|
|
219
|
+
description: "Project root containing .delphi-config.json (defaults to the session cwd).",
|
|
220
|
+
},
|
|
221
|
+
writeEvidence: {
|
|
222
|
+
type: "boolean",
|
|
223
|
+
description: "Write .code-walkthrough-result.json for the Gate MW pre-push check. Off by default because it is a file side effect; requires an APPROVED review.",
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
output: {
|
|
227
|
+
schema: { type: "string" },
|
|
228
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
229
|
+
},
|
|
230
|
+
timeoutMs: DELPHI_TIMEOUT_MS,
|
|
231
|
+
async execute(args, exec) {
|
|
232
|
+
const cwd = args.cwd ?? exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
233
|
+
return runDelphiReview(args.subject, {
|
|
234
|
+
addressed: args.addressed ?? "",
|
|
235
|
+
maxRounds: args.maxRounds,
|
|
236
|
+
cwd,
|
|
237
|
+
resolveApiKey,
|
|
238
|
+
signal: exec.signal,
|
|
239
|
+
writeEvidence: args.writeEvidence ?? false,
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Sprint-flow state commands.
|
|
246
|
+
*
|
|
247
|
+
* The sprint-flow skill tells the model to run these by name; without a tool the
|
|
248
|
+
* instruction was unfollowable. Deliberately narrow: the subcommand and the
|
|
249
|
+
* phase-transition status are enumerated, and paths are quoted, so this does not
|
|
250
|
+
* become a passthrough to the whole CLI.
|
|
251
|
+
*/
|
|
252
|
+
function sprintTool(shell, dialect) {
|
|
253
|
+
return defineTool({
|
|
254
|
+
name: "sprint",
|
|
255
|
+
description: "Inspect and advance XP-Gate Sprint Flow state: sprint-status, phase-transition, retro, ui-review, sprint-init, sprint-audit, next-sprint.",
|
|
256
|
+
parameters: {
|
|
257
|
+
subcommand: {
|
|
258
|
+
type: "string",
|
|
259
|
+
required: true,
|
|
260
|
+
description: `One of: ${SPRINT_SUBCOMMANDS.join(", ")}.`,
|
|
261
|
+
// Enumerated rather than free text: the schema rejects anything else before
|
|
262
|
+
// the value could ever reach a shell.
|
|
263
|
+
enum: SPRINT_SUBCOMMANDS.slice(),
|
|
264
|
+
},
|
|
265
|
+
phase: { type: "string", description: "phase-transition only: phase number, 1-6." },
|
|
266
|
+
status: {
|
|
267
|
+
type: "string",
|
|
268
|
+
description: "phase-transition only: phase status.",
|
|
269
|
+
enum: PHASE_STATUSES.slice(),
|
|
270
|
+
},
|
|
271
|
+
dir: { type: "string", description: "Project directory (absolute or relative)." },
|
|
272
|
+
json: { type: "boolean", description: "Request JSON output where supported." },
|
|
273
|
+
days: { type: "number", description: "retro only: days to look back." },
|
|
274
|
+
all: { type: "boolean", description: "retro only: cover every sprint." },
|
|
275
|
+
},
|
|
276
|
+
output: {
|
|
277
|
+
schema: { type: "string" },
|
|
278
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
279
|
+
},
|
|
280
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
281
|
+
async execute(args, exec) {
|
|
282
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
283
|
+
const command = buildSprintCommand(args.subcommand, args, dialect);
|
|
284
|
+
return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS);
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Runtime schema for the keys above.
|
|
290
|
+
*
|
|
291
|
+
* Exported as a schema, not just a type: DSH validates against it and surfaces the
|
|
292
|
+
* keys in the profile schema, so a user can discover and set them. A bare interface
|
|
293
|
+
* is invisible to the harness.
|
|
294
|
+
*/
|
|
295
|
+
export const Config = z.object({
|
|
296
|
+
principlesCheck: z
|
|
297
|
+
.union([z.const("auto"), z.const("off")])
|
|
298
|
+
.default("auto")
|
|
299
|
+
.description("Save-time principles check: auto = on for source files, once per file; off = do not register the hook."),
|
|
300
|
+
delphiGuard: z
|
|
301
|
+
.union([z.const("auto"), z.const("off")])
|
|
302
|
+
.default("auto")
|
|
303
|
+
.description("Sprint write guard: auto = deny edits until delphi-review is APPROVED; off = do not register the hook."),
|
|
304
|
+
});
|
|
305
|
+
export function apply(ctx, config = {}) {
|
|
306
|
+
const cfg = config;
|
|
307
|
+
// DSH runs a POSIX shell off-Windows and PowerShell on Windows; generating the
|
|
308
|
+
// wrong dialect makes every tool fail to parse rather than degrade gracefully.
|
|
309
|
+
const dialect = detectDialect();
|
|
310
|
+
const shell = ctx.shell;
|
|
311
|
+
// ctx.credentials is optional: outside DSH there is no store, and the resolver
|
|
312
|
+
// then reads the process environment directly. Inside DSH this is what makes
|
|
313
|
+
// delphi-review work on a machine whose key lives in the credential store rather
|
|
314
|
+
// than in the host process's inherited environment.
|
|
315
|
+
//
|
|
316
|
+
// The cast is needed because the service is attached by a peer package's module
|
|
317
|
+
// augmentation, which only applies when that package is in the compilation; a
|
|
318
|
+
// consumer may install this plugin without it.
|
|
319
|
+
const credentials = ctx.credentials;
|
|
320
|
+
const resolveApiKey = createCredentialResolver(credentials, process.env);
|
|
321
|
+
ctx.tools.register(gateCheckTool(shell, dialect));
|
|
322
|
+
ctx.tools.register(gatePrinciplesTool(shell, dialect));
|
|
323
|
+
ctx.tools.register(gateArchTool(shell, dialect));
|
|
324
|
+
ctx.tools.register(delphiReviewTool(resolveApiKey));
|
|
325
|
+
ctx.tools.register(sprintTool(shell, dialect));
|
|
326
|
+
// Publishing the 12 bundled skills is what makes them visible to the model.
|
|
327
|
+
// They ship in the tarball either way, but without a provider no discovery
|
|
328
|
+
// root points at the plugin directory and the model never sees them — the v1
|
|
329
|
+
// bug this provider exists to fix.
|
|
330
|
+
ctx.skills.registerProvider(() => createSkillProvider(bundledSkillsRoot()));
|
|
331
|
+
registerHooks(ctx, shell, dialect, cfg);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Attach the advisory principles result to an accepted tool result.
|
|
335
|
+
*
|
|
336
|
+
* Best effort by contract: a throwing post-execute fails the tool call, which is a
|
|
337
|
+
* much worse outcome than skipping one advisory check, so nothing here propagates.
|
|
338
|
+
*/
|
|
339
|
+
async function enrichWithPrinciples(shell, dialect, exec, result, next) {
|
|
340
|
+
try {
|
|
341
|
+
const note = await principlesNote(shell, dialect, exec, result);
|
|
342
|
+
if (note === undefined)
|
|
343
|
+
return next();
|
|
344
|
+
const accepted = await next();
|
|
345
|
+
if (accepted.kind !== "accept" || accepted.value !== undefined)
|
|
346
|
+
return accepted;
|
|
347
|
+
// Appended to the tool's own output rather than as a separate message: the model
|
|
348
|
+
// reads it immediately, with no dependency on message identity.
|
|
349
|
+
return {
|
|
350
|
+
...accepted,
|
|
351
|
+
content: [...(accepted.content ?? []), { type: "text", text: note }],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return next();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Register the pre/post-execute hooks.
|
|
360
|
+
*
|
|
361
|
+
* Both default to on and are individually switchable, because each is a policy a
|
|
362
|
+
* repository may reasonably reject: the guard blocks writes, and the save check
|
|
363
|
+
* costs a subprocess per file.
|
|
364
|
+
*/
|
|
365
|
+
function registerHooks(ctx, shell, dialect, cfg) {
|
|
366
|
+
if (cfg.delphiGuard !== "off") {
|
|
367
|
+
ctx.on("tools/pre-execute", async (exec, next) => {
|
|
368
|
+
const cwd = sessionCwd(exec) ?? process.cwd();
|
|
369
|
+
// Deny before dispatch so the edit never reaches the filesystem.
|
|
370
|
+
return delphiGuardDecision(cwd, exec.name) ?? next();
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (cfg.principlesCheck !== "off") {
|
|
374
|
+
ctx.on("tools/post-execute", (exec, result, next) => enrichWithPrinciples(shell, dialect, exec, result, next));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/** The session cwd, if the execution carries one. */
|
|
378
|
+
function sessionCwd(exec) {
|
|
379
|
+
return exec.agent?.session?.header?.cwd;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Advisory principles result for a just-written file, or undefined to stay silent.
|
|
383
|
+
*
|
|
384
|
+
* Cost is the whole design here. `xp-gate principles` is a Node CLI with a
|
|
385
|
+
* 200-400ms cold start, and an editing session makes dozens of edits, so two
|
|
386
|
+
* filters cut nearly all of the calls:
|
|
387
|
+
*
|
|
388
|
+
* - extension allowlist: the engine covers source files, not .md/.json/.yaml,
|
|
389
|
+
* which are a large share of the edits in repositories that use xp-gate
|
|
390
|
+
* - per-process dedupe: the model often edits the same file ten times in a row, and
|
|
391
|
+
* those later edits are it fixing what the first check reported
|
|
392
|
+
*
|
|
393
|
+
* Anything unexpected returns undefined. Blocking the tool call to report an
|
|
394
|
+
* advisory violation would be far worse than missing the report.
|
|
395
|
+
*/
|
|
396
|
+
async function principlesNote(shell, dialect, exec, result) {
|
|
397
|
+
if (!isEditTool(exec.name) || result.isError)
|
|
398
|
+
return undefined;
|
|
399
|
+
const path = writtenPath(exec.arguments);
|
|
400
|
+
if (path === undefined || !isCheckablePath(path))
|
|
401
|
+
return undefined;
|
|
402
|
+
const cwd = sessionCwd(exec) ?? process.cwd();
|
|
403
|
+
const absolute = resolveTarget(path, cwd);
|
|
404
|
+
if (checkedThisSession.has(absolute))
|
|
405
|
+
return undefined;
|
|
406
|
+
checkedThisSession.add(absolute);
|
|
407
|
+
try {
|
|
408
|
+
// No caller signal reaches a post-execute hook; the hook's own ceiling is enough,
|
|
409
|
+
// and it is the shell that enforces it.
|
|
410
|
+
const report = await runXpGate(shell, AbortSignal.timeout(DEFAULT_TIMEOUT_MS), buildCommand({ subcommand: "principles", target: absolute, dialect }), cwd, DEFAULT_TIMEOUT_MS);
|
|
411
|
+
return `[XP-Gate] principles for ${path}:\n${report}`;
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* The file a write/edit touched.
|
|
419
|
+
*
|
|
420
|
+
* Both shapes appear across DSH's file tools, so both are read rather than assuming
|
|
421
|
+
* one: `file_path` from the fs tools, `path` from str-replace-editor.
|
|
422
|
+
*/
|
|
423
|
+
function writtenPath(args) {
|
|
424
|
+
if (typeof args !== "object" || args === null)
|
|
425
|
+
return undefined;
|
|
426
|
+
const record = args;
|
|
427
|
+
const candidate = record.file_path ?? record.path;
|
|
428
|
+
return typeof candidate === "string" && candidate.length > 0 ? candidate : undefined;
|
|
429
|
+
}
|
|
430
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SkillCandidate, SkillProvider } from "@deepseek-ai/dsh-skill";
|
|
2
|
+
/**
|
|
3
|
+
* Build the DSH skill candidates for every skill bundled under `root`.
|
|
4
|
+
* Exposed for tests and for callers that need the catalog without a provider.
|
|
5
|
+
*/
|
|
6
|
+
export declare function listBundledSkills(root: string): SkillCandidate[];
|
|
7
|
+
/**
|
|
8
|
+
* Create the XP-Gate skill provider for a given skills root.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the shape of the official `@deepseek-ai/dsh-skill-office` provider:
|
|
11
|
+
* candidates are scanned once, bodies are re-read on every `get()` so editing a
|
|
12
|
+
* skill takes effect without restarting the host.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createSkillProvider(root: string): SkillProvider;
|
|
15
|
+
//# sourceMappingURL=skill-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-provider.d.ts","sourceRoot":"","sources":["../src/skill-provider.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,cAAc,EAGd,aAAa,EACd,MAAM,wBAAwB,CAAA;AA8I/B;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE,CAahE;AA6CD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAS/D"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { BUNDLED_SKILL_RANK } from "@deepseek-ai/dsh-skill";
|
|
5
|
+
/**
|
|
6
|
+
* Provider label surfaced to the model alongside each skill.
|
|
7
|
+
* Matches the plugin name so a loaded skill is attributable.
|
|
8
|
+
*/
|
|
9
|
+
const PROVIDER_NAME = "xp-gate";
|
|
10
|
+
/** Files a skill directory must contain to be loadable. */
|
|
11
|
+
const SKILL_FILE = "SKILL.md";
|
|
12
|
+
/** The `---` line that opens and closes a SKILL.md YAML frontmatter block. */
|
|
13
|
+
const FRONTMATTER_FENCE = "---";
|
|
14
|
+
/** Offset just past the opening fence, and past the closing fence plus its newline. */
|
|
15
|
+
const FRONTMATTER_START = FRONTMATTER_FENCE.length;
|
|
16
|
+
const FRONTMATTER_END_OFFSET = FRONTMATTER_FENCE.length + 1;
|
|
17
|
+
/**
|
|
18
|
+
* Unfold a YAML block scalar (`key: >` / `key: |`) body into one line.
|
|
19
|
+
* Folded blocks join on spaces, literal blocks on newlines.
|
|
20
|
+
*/
|
|
21
|
+
function unfoldBlock(lines, literal) {
|
|
22
|
+
const trimmed = lines.map((line) => line.trim());
|
|
23
|
+
if (literal)
|
|
24
|
+
return trimmed.filter((line) => line.length > 0).join("\n");
|
|
25
|
+
return trimmed.filter((line) => line.length > 0).join(" ");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Extract `name` and `description` from a SKILL.md frontmatter block.
|
|
29
|
+
*
|
|
30
|
+
* Two description shapes ship in this repository: folded blocks
|
|
31
|
+
* (`description: >`) used by delphi-review and sprint-flow, and one-line quoted
|
|
32
|
+
* or plain values used by the rest. A parser handling only one shape yields an
|
|
33
|
+
* empty description for the other, which makes those skills unroutable — the
|
|
34
|
+
* model never picks a skill it cannot see described.
|
|
35
|
+
*/
|
|
36
|
+
function parseSkill(raw, file) {
|
|
37
|
+
if (!raw.startsWith(FRONTMATTER_FENCE)) {
|
|
38
|
+
throw new Error(`${file}: missing YAML frontmatter`);
|
|
39
|
+
}
|
|
40
|
+
const end = raw.indexOf(`\n${FRONTMATTER_FENCE}`, FRONTMATTER_START);
|
|
41
|
+
if (end === -1) {
|
|
42
|
+
throw new Error(`${file}: unterminated YAML frontmatter`);
|
|
43
|
+
}
|
|
44
|
+
const frontmatter = raw.slice(FRONTMATTER_START, end);
|
|
45
|
+
const content = raw.slice(end + FRONTMATTER_END_OFFSET).replace(/^\r?\n/, "");
|
|
46
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
47
|
+
let name = "";
|
|
48
|
+
let description = "";
|
|
49
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
50
|
+
const line = lines[index] ?? "";
|
|
51
|
+
const keyed = /^([A-Za-z_][A-Za-z0-9_-]*):(.*)$/.exec(line);
|
|
52
|
+
if (!keyed)
|
|
53
|
+
continue;
|
|
54
|
+
const key = keyed[1];
|
|
55
|
+
const rest = (keyed[2] ?? "").trim();
|
|
56
|
+
// Collect the indented continuation lines of a block scalar.
|
|
57
|
+
const blockMatch = /^([>|])([+-]?)$/.exec(rest);
|
|
58
|
+
if (blockMatch) {
|
|
59
|
+
const block = [];
|
|
60
|
+
let cursor = index + 1;
|
|
61
|
+
while (cursor < lines.length) {
|
|
62
|
+
const next = lines[cursor] ?? "";
|
|
63
|
+
if (next.trim().length > 0 && !/^\s/.test(next))
|
|
64
|
+
break;
|
|
65
|
+
block.push(next);
|
|
66
|
+
cursor += 1;
|
|
67
|
+
}
|
|
68
|
+
index = cursor - 1;
|
|
69
|
+
const value = unfoldBlock(block, blockMatch[1] === "|");
|
|
70
|
+
if (key === "name")
|
|
71
|
+
name = value;
|
|
72
|
+
if (key === "description")
|
|
73
|
+
description = value;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (key !== "name" && key !== "description")
|
|
77
|
+
continue;
|
|
78
|
+
// Strip a matching pair of surrounding quotes without touching inner ones.
|
|
79
|
+
const unquoted = /^(['"])(.*)\1$/.exec(rest)?.[2] ?? rest;
|
|
80
|
+
if (key === "name")
|
|
81
|
+
name = unquoted;
|
|
82
|
+
if (key === "description")
|
|
83
|
+
description = unquoted;
|
|
84
|
+
}
|
|
85
|
+
if (!name)
|
|
86
|
+
throw new Error(`${file}: frontmatter carries no name`);
|
|
87
|
+
if (!description)
|
|
88
|
+
throw new Error(`${file}: frontmatter carries no description`);
|
|
89
|
+
return { name, description, content };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Scan `root` for skill directories without loading their bodies.
|
|
93
|
+
*
|
|
94
|
+
* The catalog is fixed at package build time, so scanning once at apply() is
|
|
95
|
+
* enough; bodies stay on disk and are read per `get()`. A missing root yields
|
|
96
|
+
* an empty catalog rather than throwing: a plugin whose skills directory was
|
|
97
|
+
* stripped during packaging must still load its tools.
|
|
98
|
+
*/
|
|
99
|
+
function scanSkills(root) {
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = readdirSync(root, { withFileTypes: true })
|
|
103
|
+
.filter((entry) => entry.isDirectory())
|
|
104
|
+
.map((entry) => entry.name);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const listed = [];
|
|
110
|
+
for (const entry of entries.sort()) {
|
|
111
|
+
const directory = join(root, entry);
|
|
112
|
+
const file = join(directory, SKILL_FILE);
|
|
113
|
+
let raw;
|
|
114
|
+
try {
|
|
115
|
+
raw = readFileSync(file, "utf8");
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// A directory without SKILL.md is not a skill; skip it silently so a
|
|
119
|
+
// stray file in the package cannot break the whole catalog.
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const parsed = parseSkill(raw, file);
|
|
123
|
+
listed.push({
|
|
124
|
+
name: parsed.name,
|
|
125
|
+
description: parsed.description,
|
|
126
|
+
directory,
|
|
127
|
+
file,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return listed;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Build the DSH skill candidates for every skill bundled under `root`.
|
|
134
|
+
* Exposed for tests and for callers that need the catalog without a provider.
|
|
135
|
+
*/
|
|
136
|
+
export function listBundledSkills(root) {
|
|
137
|
+
return scanSkills(root).map((skill) => ({
|
|
138
|
+
name: skill.name,
|
|
139
|
+
description: skill.description,
|
|
140
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
141
|
+
provider: PROVIDER_NAME,
|
|
142
|
+
source: "bundled",
|
|
143
|
+
rank: BUNDLED_SKILL_RANK,
|
|
144
|
+
// 8 of the 12 skills reference references/*.md and templates/*; without a
|
|
145
|
+
// directory base those relative paths have nothing to resolve against.
|
|
146
|
+
resourceBase: { kind: "directory", path: skill.directory },
|
|
147
|
+
locator: skill.file,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Load one candidate's body from disk.
|
|
152
|
+
*
|
|
153
|
+
* Separated from the provider literal so the lookup/read/decorate steps sit at the
|
|
154
|
+
* same nesting level instead of inside an object method's promise chain.
|
|
155
|
+
*/
|
|
156
|
+
async function loadSkill(candidate, options, byName) {
|
|
157
|
+
// A candidate carries the absolute SKILL.md path it was listed from, so loading
|
|
158
|
+
// works even if the scanned catalog is stale; fall back to the catalog for
|
|
159
|
+
// hand-built candidates in tests.
|
|
160
|
+
const locator = typeof candidate.locator === "string" ? candidate.locator : "";
|
|
161
|
+
const target = locator.length > 0 ? locator : byName.get(candidate.name)?.file;
|
|
162
|
+
if (!target)
|
|
163
|
+
return undefined;
|
|
164
|
+
let raw;
|
|
165
|
+
try {
|
|
166
|
+
raw = await readFile(target, { encoding: "utf8", signal: options.signal });
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// A skill deleted after listing is no longer loadable; the registry treats
|
|
170
|
+
// undefined as "gone" and invalidates its cached entry.
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const parsed = parseSkill(raw, target);
|
|
174
|
+
// Built field by field rather than by spreading and dropping the extras: `rank`
|
|
175
|
+
// and `locator` describe the catalog entry and are not part of a definition, and
|
|
176
|
+
// naming what belongs here keeps that boundary visible.
|
|
177
|
+
return {
|
|
178
|
+
name: candidate.name,
|
|
179
|
+
description: candidate.description,
|
|
180
|
+
invocation: candidate.invocation,
|
|
181
|
+
source: candidate.source,
|
|
182
|
+
provider: candidate.provider,
|
|
183
|
+
path: candidate.path,
|
|
184
|
+
resourceBase: candidate.resourceBase,
|
|
185
|
+
whenToUse: candidate.whenToUse,
|
|
186
|
+
content: parsed.content,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Create the XP-Gate skill provider for a given skills root.
|
|
191
|
+
*
|
|
192
|
+
* Mirrors the shape of the official `@deepseek-ai/dsh-skill-office` provider:
|
|
193
|
+
* candidates are scanned once, bodies are re-read on every `get()` so editing a
|
|
194
|
+
* skill takes effect without restarting the host.
|
|
195
|
+
*/
|
|
196
|
+
export function createSkillProvider(root) {
|
|
197
|
+
const byName = new Map(scanSkills(root).map((skill) => [skill.name, skill]));
|
|
198
|
+
const candidates = listBundledSkills(root);
|
|
199
|
+
return {
|
|
200
|
+
name: PROVIDER_NAME,
|
|
201
|
+
list: () => Promise.resolve(candidates),
|
|
202
|
+
get: (candidate, options) => loadSkill(candidate, options, byName),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=skill-provider.js.map
|