@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.
Files changed (151) hide show
  1. package/adapters/java.sh +256 -4
  2. package/adapters/powershell.sh +45 -6
  3. package/bin/xp-gate.js +8 -2
  4. package/build-integrity/__tests__/gate-10.test.ts +88 -1
  5. package/build-integrity/gate-10.ts +51 -16
  6. package/dsh.bundle.patch.yml +15 -0
  7. package/gate-13-compiler-strictness.sh +1031 -0
  8. package/gate-3.sh +48 -13
  9. package/hooks/gate-13-compiler-strictness.sh +1031 -0
  10. package/hooks/gate-3.sh +48 -13
  11. package/hooks/lib/coverage-total.sh +18 -2
  12. package/hooks/lib/java-test-selection.sh +251 -0
  13. package/hooks/lib/jscpd-run.sh +9 -0
  14. package/hooks/lib/validate-code-walkthrough.cjs +186 -29
  15. package/hooks/pre-commit +269 -13
  16. package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
  17. package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
  18. package/lib/__tests__/gate-runner.test.js +29 -2
  19. package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
  20. package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
  21. package/lib/__tests__/shared-utils.test.js +21 -0
  22. package/lib/check.js +6 -2
  23. package/lib/detect-deps.js +42 -10
  24. package/lib/doctor-tui.js +111 -21
  25. package/lib/gate-runner.js +111 -37
  26. package/lib/mattpocock-license-verdict.json +1 -1
  27. package/lib/principles.js +48 -6
  28. package/lib/shared-utils.js +8 -0
  29. package/mock-policy/AGENTS.md +3 -3
  30. package/mock-policy/gate-m3.ts +57 -23
  31. package/mutation/AGENTS.md +3 -3
  32. package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
  33. package/mutation/__tests__/test-filter-parity.test.ts +5 -2
  34. package/mutation/gate-m.ts +30 -24
  35. package/mutation/runners/mutmut-runner.ts +80 -30
  36. package/mutation/runners/pitest-runner.ts +60 -42
  37. package/package.json +14 -2
  38. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  39. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  40. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  41. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  42. package/plugins/dsh/lib/command.d.ts +38 -0
  43. package/plugins/dsh/lib/command.d.ts.map +1 -0
  44. package/plugins/dsh/lib/command.js +87 -0
  45. package/plugins/dsh/lib/credentials.d.ts +39 -0
  46. package/plugins/dsh/lib/credentials.d.ts.map +1 -0
  47. package/plugins/dsh/lib/credentials.js +57 -0
  48. package/plugins/dsh/lib/delphi-config.d.ts +129 -0
  49. package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
  50. package/plugins/dsh/lib/delphi-config.js +369 -0
  51. package/plugins/dsh/lib/delphi-run.d.ts +24 -0
  52. package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
  53. package/plugins/dsh/lib/delphi-run.js +113 -0
  54. package/plugins/dsh/lib/delphi.d.ts +94 -0
  55. package/plugins/dsh/lib/delphi.d.ts.map +1 -0
  56. package/plugins/dsh/lib/delphi.js +207 -0
  57. package/plugins/dsh/lib/dialect.d.ts +24 -0
  58. package/plugins/dsh/lib/dialect.d.ts.map +1 -0
  59. package/plugins/dsh/lib/dialect.js +36 -0
  60. package/plugins/dsh/lib/evidence.d.ts +70 -0
  61. package/plugins/dsh/lib/evidence.d.ts.map +1 -0
  62. package/plugins/dsh/lib/evidence.js +92 -0
  63. package/plugins/dsh/lib/gate-runner.d.ts +49 -0
  64. package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
  65. package/plugins/dsh/lib/gate-runner.js +95 -0
  66. package/plugins/dsh/lib/git-context.d.ts +26 -0
  67. package/plugins/dsh/lib/git-context.d.ts.map +1 -0
  68. package/plugins/dsh/lib/git-context.js +30 -0
  69. package/plugins/dsh/lib/hooks.d.ts +42 -0
  70. package/plugins/dsh/lib/hooks.d.ts.map +1 -0
  71. package/plugins/dsh/lib/hooks.js +113 -0
  72. package/plugins/dsh/lib/index.d.ts +20 -0
  73. package/plugins/dsh/lib/index.d.ts.map +1 -0
  74. package/plugins/dsh/lib/index.js +430 -0
  75. package/plugins/dsh/lib/skill-provider.d.ts +15 -0
  76. package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
  77. package/plugins/dsh/lib/skill-provider.js +205 -0
  78. package/plugins/dsh/lib/sprint.d.ts +60 -0
  79. package/plugins/dsh/lib/sprint.d.ts.map +1 -0
  80. package/plugins/dsh/lib/sprint.js +92 -0
  81. package/plugins/dsh/package.json +14 -8
  82. package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
  83. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  84. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  85. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  86. package/plugins/dsh/src/command-single-form.test.ts +67 -0
  87. package/plugins/dsh/src/command.test.ts +21 -17
  88. package/plugins/dsh/src/command.ts +19 -21
  89. package/plugins/dsh/src/compat.test.ts +200 -0
  90. package/plugins/dsh/src/contract.test.ts +7 -1
  91. package/plugins/dsh/src/credentials.test.ts +93 -0
  92. package/plugins/dsh/src/credentials.ts +71 -0
  93. package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
  94. package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
  95. package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
  96. package/plugins/dsh/src/delphi-config.ts +146 -53
  97. package/plugins/dsh/src/delphi-run.ts +27 -4
  98. package/plugins/dsh/src/delphi.ts +133 -65
  99. package/plugins/dsh/src/evidence.test.ts +74 -0
  100. package/plugins/dsh/src/evidence.ts +141 -0
  101. package/plugins/dsh/src/gate-runner.test.ts +14 -6
  102. package/plugins/dsh/src/gate-runner.ts +68 -5
  103. package/plugins/dsh/src/git-context.test.ts +39 -0
  104. package/plugins/dsh/src/git-context.ts +38 -0
  105. package/plugins/dsh/src/hooks.test.ts +87 -0
  106. package/plugins/dsh/src/hooks.ts +126 -0
  107. package/plugins/dsh/src/index.test.ts +63 -6
  108. package/plugins/dsh/src/index.ts +340 -13
  109. package/plugins/dsh/src/skill-provider.test.ts +194 -0
  110. package/plugins/dsh/src/skill-provider.ts +231 -0
  111. package/plugins/dsh/src/sprint.test.ts +64 -0
  112. package/plugins/dsh/src/sprint.ts +122 -0
  113. package/plugins/dsh/src/tri-state.test.ts +123 -0
  114. package/plugins/opencode/index.ts +192 -58
  115. package/plugins/opencode/package.json +1 -1
  116. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  117. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  118. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  119. package/plugins/qoder/plugin.json +1 -1
  120. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  121. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  122. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  123. package/principles/AGENTS.md +3 -3
  124. package/principles/__tests__/analyze-read-failure.test.ts +70 -0
  125. package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
  126. package/principles/__tests__/baseline-prune.test.ts +61 -0
  127. package/principles/__tests__/cli-files-separators.test.ts +69 -0
  128. package/principles/__tests__/config-merge-warnings.test.ts +148 -0
  129. package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
  130. package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
  131. package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
  132. package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
  133. package/principles/adapters/cpp.ts +102 -50
  134. package/principles/adapters/java.ts +22 -3
  135. package/principles/adapters/typescript.ts +548 -178
  136. package/principles/analyzer.ts +36 -3
  137. package/principles/baseline-prune.ts +19 -7
  138. package/principles/baseline.ts +58 -70
  139. package/principles/config.ts +56 -35
  140. package/principles/index.ts +122 -40
  141. package/principles/rules/clean-code/deep-nesting.ts +36 -22
  142. package/principles/rules/clean-code/unused-imports.ts +30 -19
  143. package/principles/rules/solid/dip.ts +30 -20
  144. package/principles/rules/solid/lsp.ts +17 -2
  145. package/principles/rules/solid/ocp.ts +33 -23
  146. package/principles/rules/solid/srp.ts +65 -23
  147. package/scripts/delphi-external-review.cjs +25 -2
  148. package/scripts/sync-package-content.js +28 -1
  149. package/skills/delphi-review/AGENTS.md +3 -3
  150. package/skills/sprint-flow/AGENTS.md +3 -3
  151. package/skills/test-specification-alignment/AGENTS.md +3 -3
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Sprint-flow subcommands the model may run.
3
+ *
4
+ * @test REQ-DSH-013 sprint-flow subcommands are callable from the model
5
+ * @intent Expose the sprint state commands the sprint-flow skill instructs the model
6
+ * to run, without opening a passthrough to the whole CLI.
7
+ * @covers AC-DSH-013-01
8
+ *
9
+ * The skill references these commands by name, so before this module existed a model
10
+ * following the skill could read the instruction and had no way to carry it out.
11
+ *
12
+ * The whitelist is the security boundary, not a convenience: a passthrough tool would
13
+ * turn one tool call into arbitrary CLI invocation, which is a far larger surface than
14
+ * the four gate tools it sits beside. Enumerating both the subcommand and the
15
+ * phase-transition status keeps every user-influenced token inside a known set.
16
+ */
17
+ import { type ShellDialect } from "./dialect.js";
18
+ /**
19
+ * Subcommands in scope. Deliberately excludes anything destructive or
20
+ * environment-mutating (`uninstall`, `init`, `migrate`, `setup-global`) even though
21
+ * they are the same CLI.
22
+ */
23
+ export declare const SPRINT_SUBCOMMANDS: readonly ["sprint-status", "phase-transition", "retro", "ui-review", "sprint-init", "sprint-audit", "next-sprint"];
24
+ export type SprintSubcommand = (typeof SPRINT_SUBCOMMANDS)[number];
25
+ /** The only status values `phase-transition` documents. */
26
+ export declare const PHASE_STATUSES: readonly ["in_progress", "completed", "skipped", "failed", "paused"];
27
+ export type PhaseStatus = (typeof PHASE_STATUSES)[number];
28
+ /** Options a caller may pass. Every field is either enumerated or quoted. */
29
+ export interface SprintCommandOptions {
30
+ /** phase-transition phase number, 1-6. */
31
+ readonly phase?: string;
32
+ /** phase-transition status. */
33
+ readonly status?: string;
34
+ /** Project directory, quoted as a single argument. */
35
+ readonly dir?: string;
36
+ /** Emit JSON where the subcommand supports it. */
37
+ readonly json?: boolean;
38
+ /** `retro`: look back N days. */
39
+ readonly days?: number;
40
+ /** `retro`: cover every sprint. */
41
+ readonly all?: boolean;
42
+ }
43
+ /** Type guard for the subcommand whitelist. */
44
+ export declare function isSprintSubcommand(value: string): boolean;
45
+ /**
46
+ * Validate a subcommand, throwing rather than returning a sentinel.
47
+ *
48
+ * A caller that ignores a boolean would fall through to "run nothing" or, worse, to a
49
+ * default subcommand; throwing makes the rejection impossible to overlook.
50
+ */
51
+ export declare function assertSprintSubcommand(value: string): asserts value is SprintSubcommand;
52
+ /**
53
+ * Build the command line for a whitelisted subcommand.
54
+ *
55
+ * User-influenced values reach the shell in exactly two ways: enumerated membership
56
+ * (subcommand, phase, status) or shell quoting (paths). Nothing is interpolated raw,
57
+ * which is what keeps this from becoming the injection surface a passthrough would be.
58
+ */
59
+ export declare function buildSprintCommand(subcommand: string, options?: SprintCommandOptions, dialect?: ShellDialect): string;
60
+ //# sourceMappingURL=sprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sprint.d.ts","sourceRoot":"","sources":["../src/sprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,KAAK,YAAY,EAAc,MAAM,cAAc,CAAA;AAE5D;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,oHAQrB,CAAA;AAEV,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAA;AAElE,2DAA2D;AAC3D,eAAO,MAAM,cAAc,sEAAuE,CAAA;AAElG,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzD,6EAA6E;AAC7E,MAAM,WAAW,oBAAoB;IACnC,0CAA0C;IAC1C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,+BAA+B;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,sDAAsD;IACtD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,kDAAkD;IAClD,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,iCAAiC;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,mCAAmC;IACnC,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CACvB;AAKD,+CAA+C;AAC/C,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,IAAI,gBAAgB,CAMvF;AA2BD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,oBAAyB,EAClC,OAAO,GAAE,YAAsB,GAC9B,MAAM,CAMR"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Sprint-flow subcommands the model may run.
3
+ *
4
+ * @test REQ-DSH-013 sprint-flow subcommands are callable from the model
5
+ * @intent Expose the sprint state commands the sprint-flow skill instructs the model
6
+ * to run, without opening a passthrough to the whole CLI.
7
+ * @covers AC-DSH-013-01
8
+ *
9
+ * The skill references these commands by name, so before this module existed a model
10
+ * following the skill could read the instruction and had no way to carry it out.
11
+ *
12
+ * The whitelist is the security boundary, not a convenience: a passthrough tool would
13
+ * turn one tool call into arbitrary CLI invocation, which is a far larger surface than
14
+ * the four gate tools it sits beside. Enumerating both the subcommand and the
15
+ * phase-transition status keeps every user-influenced token inside a known set.
16
+ */
17
+ import { shellQuote } from "./dialect.js";
18
+ /**
19
+ * Subcommands in scope. Deliberately excludes anything destructive or
20
+ * environment-mutating (`uninstall`, `init`, `migrate`, `setup-global`) even though
21
+ * they are the same CLI.
22
+ */
23
+ export const SPRINT_SUBCOMMANDS = [
24
+ "sprint-status",
25
+ "phase-transition",
26
+ "retro",
27
+ "ui-review",
28
+ "sprint-init",
29
+ "sprint-audit",
30
+ "next-sprint",
31
+ ];
32
+ /** The only status values `phase-transition` documents. */
33
+ export const PHASE_STATUSES = ["in_progress", "completed", "skipped", "failed", "paused"];
34
+ /** Phase numbers the CLI accepts. */
35
+ const PHASE_NUMBERS = ["1", "2", "3", "4", "5", "6"];
36
+ /** Type guard for the subcommand whitelist. */
37
+ export function isSprintSubcommand(value) {
38
+ return SPRINT_SUBCOMMANDS.includes(value);
39
+ }
40
+ /**
41
+ * Validate a subcommand, throwing rather than returning a sentinel.
42
+ *
43
+ * A caller that ignores a boolean would fall through to "run nothing" or, worse, to a
44
+ * default subcommand; throwing makes the rejection impossible to overlook.
45
+ */
46
+ export function assertSprintSubcommand(value) {
47
+ if (!isSprintSubcommand(value)) {
48
+ throw new Error(`Unknown sprint subcommand "${value}". Allowed: ${SPRINT_SUBCOMMANDS.join(", ")}`);
49
+ }
50
+ }
51
+ /** Validate the two positional arguments phase-transition requires. */
52
+ function phaseTokens(options) {
53
+ const phase = options.phase ?? "";
54
+ if (!PHASE_NUMBERS.includes(phase)) {
55
+ throw new Error(`phase must be one of ${PHASE_NUMBERS.join(", ")}; got "${phase}"`);
56
+ }
57
+ const status = options.status ?? "";
58
+ if (!PHASE_STATUSES.includes(status)) {
59
+ throw new Error(`status must be one of ${PHASE_STATUSES.join(", ")}; got "${status}"`);
60
+ }
61
+ return [phase, status];
62
+ }
63
+ /** Append the flags shared by several subcommands. */
64
+ function sharedFlags(options, dialect) {
65
+ const flags = [];
66
+ if (options.json)
67
+ flags.push("--json");
68
+ if (options.all)
69
+ flags.push("--all");
70
+ if (options.days !== undefined && Number.isInteger(options.days) && options.days > 0) {
71
+ flags.push("--days", String(options.days));
72
+ }
73
+ if (options.dir !== undefined)
74
+ flags.push("--dir", shellQuote(options.dir, dialect));
75
+ return flags;
76
+ }
77
+ /**
78
+ * Build the command line for a whitelisted subcommand.
79
+ *
80
+ * User-influenced values reach the shell in exactly two ways: enumerated membership
81
+ * (subcommand, phase, status) or shell quoting (paths). Nothing is interpolated raw,
82
+ * which is what keeps this from becoming the injection surface a passthrough would be.
83
+ */
84
+ export function buildSprintCommand(subcommand, options = {}, dialect = "posix") {
85
+ assertSprintSubcommand(subcommand);
86
+ const tokens = ["xp-gate", subcommand];
87
+ if (subcommand === "phase-transition")
88
+ tokens.push(...phaseTokens(options));
89
+ tokens.push(...sharedFlags(options, dialect));
90
+ return tokens.join(" ");
91
+ }
92
+ //# sourceMappingURL=sprint.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/dsh-plugin-xp-gate",
3
- "version": "0.31.3",
3
+ "version": "0.32.0",
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",
@@ -42,15 +42,21 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@deepseek-ai/cordis": "^4.0.1",
45
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
46
- "@deepseek-ai/dsh-shell": "^0.1.1-rc.2",
47
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.2"
45
+ "@deepseek-ai/dsh-credentials": ">=0.2.0-rc.2 <0.3.0-0",
46
+ "@deepseek-ai/dsh-llm": ">=0.2.0-rc.2 <0.3.0-0",
47
+ "@deepseek-ai/dsh-shell": ">=0.2.0-rc.2 <0.3.0-0",
48
+ "@deepseek-ai/dsh-skill": ">=0.2.0-rc.2 <0.3.0-0",
49
+ "@deepseek-ai/dsh-tools": ">=0.2.0-rc.2 <0.3.0-0",
50
+ "@deepseek-ai/schemastery": "^3.18.5"
48
51
  },
49
52
  "devDependencies": {
50
- "@deepseek-ai/cordis": "4.0.1",
51
- "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
52
- "@deepseek-ai/dsh-shell": "0.1.1-rc.2",
53
- "@deepseek-ai/dsh-tools": "0.1.1-rc.2",
53
+ "@deepseek-ai/cordis": "4.0.4",
54
+ "@deepseek-ai/dsh-credentials": "0.2.0-rc.2",
55
+ "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
56
+ "@deepseek-ai/dsh-shell": "0.2.0-rc.2",
57
+ "@deepseek-ai/dsh-skill": "0.2.0-rc.2",
58
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
59
+ "@deepseek-ai/schemastery": "3.18.5",
54
60
  "@types/node": "^25.6.0",
55
61
  "typescript": "^5.9.3",
56
62
  "vitest": "^1.6.1",
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @test REQ-DSH-006
3
+ * @intent INV-2 end-to-end: drive DSH's REAL SkillRegistry (not a fake), mount the
4
+ * xp-gate plugin through its own apply(), and assert the registry surfaces
5
+ * all 12 bundled skills with non-empty bodies and resolvable resource bases.
6
+ * @covers AC-DSH-006-04
7
+ *
8
+ * Why this exists: every unit test asserts the provider's internal contract. None
9
+ * prove the registry actually receives and ranks the skills. v1 shipped 65 files
10
+ * and passed its own tests while the model saw nothing, so this script uses
11
+ * @deepseek-ai/dsh-skill's SkillRegistry itself as the consumer.
12
+ *
13
+ * Usage: node scripts/verify-skill-provider.mjs <pluginDir>
14
+ */
15
+
16
+ import { pathToFileURL } from "node:url"
17
+ import { join } from "node:path"
18
+
19
+ const EXPECTED = 12
20
+
21
+ /**
22
+ * Build a real Cordis Context and mount the DSH SkillRegistry service on it.
23
+ *
24
+ * SkillRegistry extends cordis' Service, whose constructor reflects into the
25
+ * owning context — a plain object literal cannot host it. Cordis also rejects
26
+ * assigning undeclared properties, so the tools/shell seams are installed by a
27
+ * small plugin that declares them the way a real DSH service would.
28
+ */
29
+ async function makeContext(dshRoot, SkillRegistry) {
30
+ const cordis = await import(
31
+ pathToFileURL(join(dshRoot, "@deepseek-ai", "cordis", "lib", "index.js")).href
32
+ )
33
+ const ctx = new cordis.Context()
34
+ const tools = []
35
+
36
+ const seams = {
37
+ name: "verify-seams",
38
+ apply(scope) {
39
+ scope.provide("tools", { register: (tool) => tools.push(tool) })
40
+ scope.provide("shell", {
41
+ resolve: (request) => ({ ...request }),
42
+ execute: () => Promise.resolve({ result: () => Promise.resolve({}) }),
43
+ })
44
+ },
45
+ }
46
+
47
+ ctx.plugin(seams)
48
+ // SkillRegistry is a Service; mounting it provides ctx.skills.
49
+ await ctx.plugin(SkillRegistry)
50
+ return { ctx, tools }
51
+ }
52
+
53
+ export async function verifySkillProvider(pluginDir, dshRoot) {
54
+ const skillModule = await import(
55
+ pathToFileURL(join(dshRoot, "@deepseek-ai", "dsh-skill", "lib", "index.js")).href
56
+ )
57
+ const { SkillRegistry } = skillModule
58
+
59
+ const plugin = await import(pathToFileURL(join(pluginDir, "lib", "index.js")).href)
60
+ const { ctx, tools } = await makeContext(dshRoot, SkillRegistry)
61
+
62
+ const failures = []
63
+ if (!Array.isArray(plugin.inject) || !plugin.inject.includes("skills")) {
64
+ failures.push(`inject does not declare skills: ${JSON.stringify(plugin.inject)}`)
65
+ }
66
+
67
+ plugin.apply(ctx)
68
+
69
+ // Registry-level view: exactly what a model-facing consumer would read.
70
+ const summaries = await ctx.skills.list({ cwd: pluginDir })
71
+ const ours = summaries.filter((s) => s.provider === "xp-gate")
72
+ if (ours.length !== EXPECTED) {
73
+ failures.push(`registry lists ${ours.length} xp-gate skills, expected ${EXPECTED}`)
74
+ }
75
+
76
+ const loaded = []
77
+ for (const summary of ours) {
78
+ const definition = await ctx.skills.get(summary.name, { cwd: pluginDir })
79
+ if (!definition) {
80
+ failures.push(`${summary.name}: not loadable through the registry`)
81
+ continue
82
+ }
83
+ if (!definition.content || definition.content.trim().length === 0) {
84
+ failures.push(`${summary.name}: empty body`)
85
+ continue
86
+ }
87
+ if (definition.resourceBase?.kind !== "directory") {
88
+ failures.push(`${summary.name}: resourceBase is ${JSON.stringify(definition.resourceBase)}`)
89
+ }
90
+ loaded.push({ name: summary.name, bytes: definition.content.length })
91
+ }
92
+
93
+ return {
94
+ ok: failures.length === 0,
95
+ reason: failures.join("; "),
96
+ tools: tools.map((t) => t?.name).filter(Boolean),
97
+ count: ours.length,
98
+ loaded,
99
+ }
100
+ }
101
+
102
+ const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
103
+ if (isMain) {
104
+ const pluginDir = process.argv[2]
105
+ const dshRoot = process.argv[3] ?? join(pluginDir, "node_modules")
106
+ if (!pluginDir) {
107
+ console.error("usage: node scripts/verify-skill-provider.mjs <pluginDir> [dshNodeModulesRoot]")
108
+ process.exit(2)
109
+ }
110
+ const result = await verifySkillProvider(pluginDir, dshRoot)
111
+ console.log(JSON.stringify(result, null, 2))
112
+ process.exit(result.ok ? 0 : 1)
113
+ }
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-26
4
- **Commit:** 7a6d9971
3
+ **Generated:** 2026-10-28
4
+ **Commit:** 0f58c300
5
5
  **Branch:** main
6
- **Version:** 0.31.3.0
6
+ **Version:** 0.32.0.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-26
4
- **Commit:** 7a6d9971
3
+ **Generated:** 2026-10-28
4
+ **Commit:** 0f58c300
5
5
  **Branch:** main
6
- **Version:** 0.31.3.0
6
+ **Version:** 0.32.0.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-26
4
- **Commit:** 7a6d9971
3
+ **Generated:** 2026-10-28
4
+ **Commit:** 0f58c300
5
5
  **Branch:** main
6
- **Version:** 0.31.3.0
6
+ **Version:** 0.32.0.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @test REQ-DSH-011 single-form command construction
3
+ * @intent Verify buildCommand emits one unconditional xp-gate invocation per dialect,
4
+ * appends an explicit exit-code echo, and no longer branches on whether the
5
+ * CLI resolves — so "installed but broken" can never look like a pass.
6
+ * @covers AC-DSH-011-07 AC-DSH-011-08
7
+ *
8
+ * Why: the old form probed with `command -v` / `Get-Command` and printed a hint on
9
+ * the else branch while leaving the exit code at 0. A CLI that resolved but then
10
+ * failed produced empty stdout with exit 0, which the model read as green.
11
+ */
12
+
13
+ import { describe, it, expect } from "vitest"
14
+ import { buildCommand, GATE_WHITELIST, isGateAllowed } from "./command.js"
15
+
16
+ describe("buildCommand single form", () => {
17
+ it("never branches on CLI presence in POSIX form", () => {
18
+ const command = buildCommand({ subcommand: "check", target: "src", dialect: "posix" })
19
+ expect(command).not.toContain("command -v")
20
+ expect(command).not.toContain("if ")
21
+ expect(command.trimStart().startsWith("xp-gate")).toBe(true)
22
+ })
23
+
24
+ it("never branches on CLI presence in PowerShell form", () => {
25
+ const command = buildCommand({ subcommand: "check", target: "src", dialect: "powershell" })
26
+ expect(command).not.toContain("Get-Command")
27
+ expect(command.trimStart().startsWith("xp-gate")).toBe(true)
28
+ })
29
+
30
+ it("echoes the exit status so a missing CLI becomes a nonzero exit", () => {
31
+ const posix = buildCommand({ subcommand: "principles", dialect: "posix" })
32
+ expect(posix).toContain("exit:")
33
+ expect(posix).toContain("2>&1")
34
+
35
+ const powershell = buildCommand({ subcommand: "principles", dialect: "powershell" })
36
+ expect(powershell).toContain("LASTEXITCODE")
37
+ })
38
+
39
+ it("keeps quoting model-controlled arguments in both dialects", () => {
40
+ const posix = buildCommand({ subcommand: "check", target: "a b; rm -rf /", dialect: "posix" })
41
+ expect(posix).toContain("'a b; rm -rf /'")
42
+
43
+ const powershell = buildCommand({
44
+ subcommand: "check",
45
+ target: "a b; rm -rf /",
46
+ dialect: "powershell",
47
+ })
48
+ expect(powershell).toContain("'a b; rm -rf /'")
49
+ })
50
+
51
+ it("filters gates through the allowlist before they reach the shell", () => {
52
+ const command = buildCommand({
53
+ subcommand: "check",
54
+ target: "src",
55
+ gates: ["principles", "not-a-gate", "arch"],
56
+ dialect: "posix",
57
+ })
58
+ expect(command).toContain("principles,arch")
59
+ expect(command).not.toContain("not-a-gate")
60
+ })
61
+
62
+ it("knows which gate ids are allowed", () => {
63
+ expect(isGateAllowed("principles")).toBe(true)
64
+ expect(isGateAllowed("nope")).toBe(false)
65
+ expect(GATE_WHITELIST.length).toBeGreaterThan(0)
66
+ })
67
+ })
@@ -1,7 +1,6 @@
1
1
  import { describe, it, expect } from "vitest"
2
2
  import { resolve } from "node:path"
3
3
  import {
4
- FALLBACK_MESSAGE,
5
4
  GATE_WHITELIST,
6
5
  buildCommand,
7
6
  isGateAllowed,
@@ -44,11 +43,12 @@ describe("isGateAllowed (enum whitelist)", () => {
44
43
  })
45
44
 
46
45
  describe("buildCommand", () => {
47
- it("builds a guarded gate-check command with target + gates", () => {
46
+ it("builds an unconditional gate-check command with target + gates", () => {
48
47
  const cmd = buildCommand({ subcommand: "check", target: "src", gates: ["principles", "arch"] })
49
- expect(cmd).toContain("command -v xp-gate")
48
+ // No presence probe: the command runs regardless and reports its own status.
50
49
  expect(cmd).toContain("xp-gate check 'src' --gates 'principles,arch'")
51
- expect(cmd).toContain(shq(FALLBACK_MESSAGE))
50
+ expect(cmd).toContain("[XP-Gate] exit:")
51
+ expect(cmd).not.toContain("command -v")
52
52
  })
53
53
 
54
54
  it("drops non-whitelisted gates before building the command", () => {
@@ -86,13 +86,16 @@ describe("buildCommand", () => {
86
86
 
87
87
  /**
88
88
  * @test REQ-DSH-005
89
- * @intent 验证 xp-gate CLI 缺失时 buildCommand 走 else 分支输出优雅降级安装提示,而非抛错或返回门禁失败
89
+ * @intent 验证 xp-gate CLI 缺失时不再走 else 分支伪装成功:命令无条件执行并回显退出码,
90
+ * 由渲染层把 command-not-found 判为 INDETERMINATE 并附安装提示。
91
+ * 优雅降级保留(不抛错、不崩),但不再静默返回 exit 0。
90
92
  * @covers AC-DSH-005-01
91
93
  */
92
- it("emits the graceful-degradation fallback when xp-gate is absent", () => {
94
+ it("degrades through the exit code instead of a fake success branch", () => {
93
95
  const cmd = buildCommand({ subcommand: "check", target: "src" })
94
- expect(cmd).toContain("else printf")
95
- expect(cmd).toContain(FALLBACK_MESSAGE)
96
+ // No else branch: absence becomes a real nonzero exit the renderer can see.
97
+ expect(cmd).not.toContain("else printf")
98
+ expect(cmd).toContain("[XP-Gate] exit:")
96
99
  })
97
100
  })
98
101
 
@@ -122,32 +125,32 @@ describe("GATE_WHITELIST", () => {
122
125
 
123
126
  /**
124
127
  * @test REQ-DSH-010
125
- * @intent 验证 PowerShell 方言下生成的命令使用 Get-Command/Write-Output 而非 POSIX 的
126
- * command -v/printf,避免 DSH 在 win32(pwsh executor)下因语法错误导致三个工具全部不可用
128
+ * @intent 验证 PowerShell 方言下生成的命令使用 Write-Output/$LASTEXITCODE 而非 POSIX 的
129
+ * printf/$?,避免 DSH 在 win32(pwsh executor)下因语法错误导致三个工具全部不可用
127
130
  * @covers AC-DSH-010-01
128
131
  */
129
132
  describe("PowerShell dialect (regression: DSH win32 uses the pwsh executor)", () => {
130
- it("uses Get-Command instead of command -v", () => {
133
+ it("reports the exit code with $LASTEXITCODE instead of POSIX $?", () => {
131
134
  const cmd = buildCommand({ subcommand: "principles", target: "src/x.ts", dialect: "powershell" })
132
- expect(cmd).toContain("Get-Command xp-gate -ErrorAction SilentlyContinue")
135
+ expect(cmd).toContain("$LASTEXITCODE")
133
136
  expect(cmd).not.toContain("command -v")
137
+ expect(cmd).not.toContain("Get-Command")
134
138
  })
135
139
 
136
- it("uses Write-Output instead of printf for the fallback hint", () => {
140
+ it("uses Write-Output instead of printf for the exit report", () => {
137
141
  const cmd = buildCommand({ subcommand: "check", target: "src", dialect: "powershell" })
138
142
  expect(cmd).toContain("Write-Output")
139
143
  expect(cmd).not.toContain("printf")
140
- expect(cmd).toContain(FALLBACK_MESSAGE)
141
144
  })
142
145
 
143
146
  it("never emits POSIX shell syntax that PowerShell cannot parse", () => {
144
147
  const cmd = buildCommand({ subcommand: "check", target: "src", gates: ["principles"], dialect: "powershell" })
145
148
  // `/dev/null` and `; then … ; fi` are the constructs that produced
146
- // "if语句中的'if'后缺少\"(\"" under the pwsh executor.
149
+ // "if语句中的'if'后缺少\"(\"" under the pwsh executor. `2>&1` IS valid in
150
+ // PowerShell and is used deliberately to merge stderr into the report.
147
151
  expect(cmd).not.toContain("/dev/null")
148
152
  expect(cmd).not.toContain("then ")
149
153
  expect(cmd).not.toContain("; fi")
150
- expect(cmd).not.toContain("2>&1")
151
154
  })
152
155
 
153
156
  it("preserves the xp-gate invocation and gate subset", () => {
@@ -173,6 +176,7 @@ describe("PowerShell dialect (regression: DSH win32 uses the pwsh executor)", ()
173
176
 
174
177
  it("defaults to the POSIX dialect when none is given (backwards compatible)", () => {
175
178
  const cmd = buildCommand({ subcommand: "arch" })
176
- expect(cmd).toContain("command -v xp-gate")
179
+ expect(cmd).toContain("printf")
180
+ expect(cmd).not.toContain("Write-Output")
177
181
  })
178
182
  })
@@ -5,10 +5,6 @@ import { type ShellDialect, shellQuote } from "./dialect.js"
5
5
  // primitives themselves live in ./dialect.ts.
6
6
  export { type ShellDialect, detectDialect, shellQuote, shq } from "./dialect.js"
7
7
 
8
- /** Shown when the global `xp-gate` CLI is not on PATH. */
9
- export const FALLBACK_MESSAGE =
10
- "[XP-Gate] xp-gate CLI not installed. Install it with: npm install -g @boyingliu01/xp-gate"
11
-
12
8
  /**
13
9
  * Allowlist of xp-gate gate aliases that are safe to invoke standalone
14
10
  * (non-preCommitOnly gates + common aliases). Anything outside this set is
@@ -73,29 +69,31 @@ function buildInner(options: BuildCommandOptions, dialect: ShellDialect): string
73
69
  }
74
70
 
75
71
  /**
76
- * Build a single guarded shell command: runs `xp-gate …` when the global CLI
77
- * is present, otherwise prints the graceful-degradation hint. A single
78
- * presence check avoids a separate probe/run race.
72
+ * Build a single unconditional shell command.
73
+ *
74
+ * There is deliberately no presence probe. An earlier version wrapped the call
75
+ * in `if command -v xp-gate …; then …; else print hint; fi`, which left the exit
76
+ * code at 0 on the else branch — so a CLI that resolved but then failed produced
77
+ * empty stdout with exit 0, and the model read that as a pass.
78
+ *
79
+ * Running unconditionally turns "not installed" into an ordinary command-not-found
80
+ * (nonzero exit, empty output), which renderGateResult classifies as
81
+ * INDETERMINATE and pairs with an install hint. It also removes the branch that
82
+ * could not distinguish "absent" from "present but broken".
79
83
  */
80
84
  export function buildCommand(options: BuildCommandOptions): string {
81
85
  const dialect = options.dialect ?? "posix"
82
86
  const inner = buildInner(options, dialect)
83
87
  if (dialect === "posix") {
84
- return `if command -v xp-gate >/dev/null 2>&1; then ${inner}; else printf '%s\\n' ${shellQuote(
85
- FALLBACK_MESSAGE,
86
- dialect,
87
- )}; fi`
88
+ // Merge stderr into stdout so a failure that only writes to stderr still
89
+ // reaches the renderer, then echo the status so the exit code survives the
90
+ // pipeline.
91
+ return `${inner} 2>&1; printf '\\n[XP-Gate] exit:%s\\n' "$?"`
88
92
  }
89
- // PowerShell: `Get-Command` resolves the .cmd/.ps1 shims npm writes to
90
- // %APPDATA%\npm. Those shims are invisible to a Git-Bash PATH lookup, so this
91
- // branch is more reliable than `command -v` on Windows.
92
- //
93
- // The hint branch leaves $LASTEXITCODE untouched at 0, matching the POSIX
94
- // branch's `printf` behaviour.
95
- return `if (Get-Command xp-gate -ErrorAction SilentlyContinue) { ${inner} } else { Write-Output ${shellQuote(
96
- FALLBACK_MESSAGE,
97
- dialect,
98
- )} }`
93
+ // PowerShell: merge streams and report $LASTEXITCODE explicitly. The
94
+ // .cmd/.ps1 shims npm writes under %APPDATA%\npm are visible here but not to a
95
+ // Git-Bash PATH lookup, which is why Windows uses this dialect.
96
+ return `${inner} 2>&1; Write-Output "[XP-Gate] exit:$LASTEXITCODE"`
99
97
  }
100
98
 
101
99
  /** Resolve a model-supplied path against the session cwd; absolutes pass through. */