From 58febc158223e5347cabeea0eeebdeb18fc5a7c2 Mon Sep 17 00:00:00 2001 From: a Date: Fri, 28 Aug 2026 10:54:47 +0000 Subject: [PATCH] feat(mtmharness): add Modern Go guidelines skill --- packages/mtmharness/README.md | 4 +- packages/mtmharness/package.json | 3 +- .../resources/go-modern-guidelines/LICENSE | 201 ++++++++++++++++++ .../go-modern-guidelines/scripts/VERSION | 1 + .../go-modern-guidelines/scripts/run-tool.ps1 | 91 ++++++++ .../go-modern-guidelines/scripts/run-tool.sh | 68 ++++++ .../mtmharness/scripts/verify-package.mjs | 12 +- packages/mtmharness/src/client/index.test.ts | 4 + .../features/coding/client/MtmCodingCard.tsx | 2 + .../src/features/coding/client/controller.ts | 6 +- .../src/features/coding/client/locales.ts | 16 +- .../mtmharness/src/features/coding/index.ts | 31 +++ .../src/features/coding/modern-go.ts | 93 ++++++++ .../mtmharness/src/features/coding/types.ts | 4 + packages/mtmharness/src/index.ts | 2 + .../tests/coding-controller.test.mjs | 33 +++ .../mtmharness/tests/coding-plugin.test.mjs | 31 ++- 17 files changed, 593 insertions(+), 9 deletions(-) create mode 100644 packages/mtmharness/resources/go-modern-guidelines/LICENSE create mode 100644 packages/mtmharness/resources/go-modern-guidelines/scripts/VERSION create mode 100644 packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.ps1 create mode 100755 packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.sh create mode 100644 packages/mtmharness/src/features/coding/modern-go.ts diff --git a/packages/mtmharness/README.md b/packages/mtmharness/README.md index bcf982b..634ce41 100644 --- a/packages/mtmharness/README.md +++ b/packages/mtmharness/README.md @@ -2,11 +2,13 @@ `mtmharness` is one public npm package with one unified DSH plugin and two explicit client identities: -- **DSH Web plugin**: the package root and `./client` export use the official `dsh.client` lazy-CJS contract. One installation provides the MTM sidebar action, the Connect control panel, and the settings-controlled Codebase Memory/Ponytail coding features. +- **DSH Web plugin**: the package root and `./client` export use the official `dsh.client` lazy-CJS contract. One installation provides the MTM sidebar action, the Connect control panel, and the settings-controlled Codebase Memory/Modern Go/Ponytail coding features. - **Independent web client**: the package also publishes a BrowserHistory static app and a MemoryHistory script/embed entry. These artifacts own their React root, router, styles, and teardown and never load the local coding runtime. The DSH plugin is assembled from Connect and coding feature domains under one Host/Client lifecycle. Codebase Memory keeps its `codebase_memory` server namespace and `mcp__codebase_memory__*` tool names; Ponytail ships six skills inline, including `/ponytail` and its companion commands. The `mtm-coding` settings namespace remains the configuration contract inside the unified `mtmharness` package. +Modern Go Guidelines is enabled by default as the inline `use-modern-go` skill. The skill uses the bundled JetBrains `v0.1.1` wrapper to resolve guidance for the target project version. The wrapper installs its CLI only when a Go task runs it, caches the binary outside the project, and reports a missing Go toolchain instead of assuming the host is prepared. `modernGoCommand` can replace the bundled wrapper command; `modernGoEnabled` removes the skill from the DSH catalog. The redistributed wrapper and license live under `resources/go-modern-guidelines/` and remain Apache-2.0. + RTK is an optional coding feature in the same `mtm-coding` settings namespace. `rtkMode` defaults to `auto`: it transparently rewrites Bash calls when the DSH `tools/pre-record-input` capability is available and falls back to inline guidance when it is not. Explicit `rewrite` is strict and reports `unavailable` on older DSH runtimes instead of changing frozen tool inputs. The pinned RTK `v0.45.0` binary is resolved from an explicit `rtkCommand` or lazily installed under the DSH home with checksum verification; RTK telemetry, tracking, and tee output are disabled for plugin-managed runs. ## DSH Web Plugin diff --git a/packages/mtmharness/package.json b/packages/mtmharness/package.json index 867b65b..f4a0631 100644 --- a/packages/mtmharness/package.json +++ b/packages/mtmharness/package.json @@ -1,7 +1,7 @@ { "name": "mtmharness", "version": "0.5.5", - "description": "Unified DeepSeek Harness Web plugin with Connect, Codebase Memory, Ponytail, and independent static/embed clients.", + "description": "Unified DeepSeek Harness Web plugin with Connect, Codebase Memory, Modern Go, Ponytail, and independent static/embed clients.", "type": "module", "engines": { "node": ">=22.19.0", "pnpm": ">=11.7.0" }, "main": "./lib/index.js", @@ -41,6 +41,7 @@ "dist/standalone", "dist/embed", "dist/types/standalone", + "resources/go-modern-guidelines", "cordis.patch.yml", "scripts/profile-migration.mjs", "README.md", diff --git a/packages/mtmharness/resources/go-modern-guidelines/LICENSE b/packages/mtmharness/resources/go-modern-guidelines/LICENSE new file mode 100644 index 0000000..c364434 --- /dev/null +++ b/packages/mtmharness/resources/go-modern-guidelines/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2000-2026 JetBrains s.r.o. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/mtmharness/resources/go-modern-guidelines/scripts/VERSION b/packages/mtmharness/resources/go-modern-guidelines/scripts/VERSION new file mode 100644 index 0000000..8308b63 --- /dev/null +++ b/packages/mtmharness/resources/go-modern-guidelines/scripts/VERSION @@ -0,0 +1 @@ +v0.1.1 diff --git a/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.ps1 b/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.ps1 new file mode 100644 index 0000000..3cc0db7 --- /dev/null +++ b/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.ps1 @@ -0,0 +1,91 @@ +$ErrorActionPreference = "Stop" + +$cliVersion = (Get-Content -LiteralPath (Join-Path $PSScriptRoot "VERSION") -TotalCount 1).Trim() +$modulePath = "github.com/JetBrains/go-modern-guidelines" +$binaryName = "go-modern-guidelines.exe" + +if ($env:LOCALAPPDATA) { + $cacheRoot = Join-Path $env:LOCALAPPDATA "go-modern-guidelines" +} else { + Write-Error "go-modern-guidelines: LOCALAPPDATA must be set" + exit 1 +} + +# GO_MODERN_GUIDELINES_DEV runs the binary built by dev-install. +if ($env:GO_MODERN_GUIDELINES_DEV) { + $devBinary = Join-Path (Join-Path $cacheRoot "dev") $binaryName + if (-not (Test-Path -LiteralPath $devBinary -PathType Leaf)) { + Write-Error "go-modern-guidelines: GO_MODERN_GUIDELINES_DEV is set but no dev build found; run dev-install" + exit 1 + } + & $devBinary @args + exit $LASTEXITCODE +} + +$installDir = Join-Path $cacheRoot $cliVersion +$binaryPath = Join-Path $installDir $binaryName + +if (-not (Test-Path -LiteralPath $binaryPath -PathType Leaf)) { + if (-not (Get-Command go -ErrorAction SilentlyContinue)) { + Write-Error "go-modern-guidelines: Go toolchain is required to install $modulePath@$cliVersion" + exit 1 + } + + $tmpDir = "$installDir.tmp.$PID" + Remove-Item -LiteralPath $tmpDir -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null + + Write-Host "go-modern-guidelines: installing $modulePath@$cliVersion into $installDir" -ForegroundColor DarkGray + + try { + $previousGoBin = $env:GOBIN + $previousGoFlags = $env:GOFLAGS + $previousGoWork = $env:GOWORK + $previousCgoEnabled = $env:CGO_ENABLED + $env:GOBIN = $tmpDir + $env:GOFLAGS = "" + $env:GOWORK = "off" + $env:CGO_ENABLED = "0" + Push-Location -LiteralPath $tmpDir + try { + go install "$modulePath@$cliVersion" + } finally { + Pop-Location + $env:GOBIN = $previousGoBin + $env:GOFLAGS = $previousGoFlags + $env:GOWORK = $previousGoWork + $env:CGO_ENABLED = $previousCgoEnabled + } + + $tmpBinary = Join-Path $tmpDir $binaryName + if (-not (Test-Path -LiteralPath $tmpBinary -PathType Leaf)) { + Write-Error "go-modern-guidelines: go install did not produce $binaryName" + exit 1 + } + + $actualVersion = "" + try { + $actualVersion = (& $tmpBinary --version 2>$null) + } catch { + $actualVersion = "" + } + if ($actualVersion -ne $cliVersion) { + if (-not $actualVersion) { + $actualVersion = "unknown version" + } + Write-Error "go-modern-guidelines: installed $actualVersion, want $cliVersion" + exit 1 + } + + New-Item -ItemType Directory -Path $installDir -Force | Out-Null + $stagedBinary = "$binaryPath.tmp.$PID" + Move-Item -LiteralPath $tmpBinary -Destination $stagedBinary -Force + Move-Item -LiteralPath $stagedBinary -Destination $binaryPath -Force + Remove-Item -LiteralPath $tmpDir -Recurse -Force -ErrorAction SilentlyContinue + } finally { + Remove-Item -LiteralPath $tmpDir -Recurse -Force -ErrorAction SilentlyContinue + } +} + +& $binaryPath @args +exit $LASTEXITCODE diff --git a/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.sh b/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.sh new file mode 100755 index 0000000..a9c3a98 --- /dev/null +++ b/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env sh +set -eu + +script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)" + +cli_version="$(cat "${script_dir}/VERSION")" +module_path="github.com/JetBrains/go-modern-guidelines" +binary_name="go-modern-guidelines" + +if [ -n "${XDG_CACHE_HOME:-}" ]; then + cache_root="${XDG_CACHE_HOME}/go-modern-guidelines" +elif [ -n "${HOME:-}" ]; then + cache_root="${HOME}/.cache/go-modern-guidelines" +else + echo "go-modern-guidelines: HOME or XDG_CACHE_HOME must be set" >&2 + exit 1 +fi + +# GO_MODERN_GUIDELINES_DEV runs the binary built by make dev-install. +if [ -n "${GO_MODERN_GUIDELINES_DEV:-}" ]; then + dev_binary="${cache_root}/dev/${binary_name}" + if [ ! -x "${dev_binary}" ]; then + echo "go-modern-guidelines: GO_MODERN_GUIDELINES_DEV is set but no dev build found; run make dev-install" >&2 + exit 1 + fi + exec "${dev_binary}" "$@" +fi + +install_dir="${cache_root}/${cli_version}" +binary_path="${install_dir}/${binary_name}" + +if [ ! -x "${binary_path}" ]; then + if ! command -v go >/dev/null 2>&1; then + echo "go-modern-guidelines: Go toolchain is required to install ${module_path}@${cli_version}" >&2 + exit 1 + fi + + tmp_dir="${install_dir}.tmp.$$" + rm -rf "${tmp_dir}" + mkdir -p "${tmp_dir}" + trap 'rm -rf "${tmp_dir}"' EXIT HUP INT TERM + + echo "go-modern-guidelines: installing ${module_path}@${cli_version} into ${install_dir}" >&2 + + ( + cd "${tmp_dir}" + GOFLAGS= GOWORK=off CGO_ENABLED=0 GOBIN="${tmp_dir}" go install "${module_path}@${cli_version}" + ) + + tmp_binary="${tmp_dir}/${binary_name}" + if [ ! -x "${tmp_binary}" ]; then + echo "go-modern-guidelines: go install did not produce ${binary_name}" >&2 + exit 1 + fi + + actual_version="$("${tmp_binary}" --version 2>/dev/null || true)" + if [ "${actual_version}" != "${cli_version}" ]; then + echo "go-modern-guidelines: installed ${actual_version:-unknown version}, want ${cli_version}" >&2 + exit 1 + fi + + mkdir -p "${install_dir}" + mv "${tmp_binary}" "${binary_path}.tmp.$$" + mv "${binary_path}.tmp.$$" "${binary_path}" + rm -rf "${tmp_dir}" +fi + +exec "${binary_path}" "$@" diff --git a/packages/mtmharness/scripts/verify-package.mjs b/packages/mtmharness/scripts/verify-package.mjs index 9dba78a..3a2fb55 100644 --- a/packages/mtmharness/scripts/verify-package.mjs +++ b/packages/mtmharness/scripts/verify-package.mjs @@ -44,6 +44,10 @@ for (const path of [ "lib/types/index.d.ts", "lib/types/client/index.d.ts", "lib/types/features/coding/index.d.ts", + "resources/go-modern-guidelines/scripts/VERSION", + "resources/go-modern-guidelines/LICENSE", + "resources/go-modern-guidelines/scripts/run-tool.sh", + "resources/go-modern-guidelines/scripts/run-tool.ps1", "dist/standalone/index.html", "dist/standalone/config.js", "dist/embed/mtmharness.js", @@ -57,7 +61,7 @@ if (!patch.includes("id: mtmharness") || !patch.includes("name: mtmharness") || } const host = read("lib/index.js"); -for (const required of ["mtm-coding", "codebase_memory", "mtm-coding-ponytail", "mtm-coding-rtk", "RTK_VERSION"]) { +for (const required of ["mtm-coding", "codebase_memory", "mtm-coding-modern-go", "use-modern-go", "mtm-coding-ponytail", "mtm-coding-rtk", "RTK_VERSION"]) { if (!host.includes(required)) fail("Host artifact is missing coding feature: " + required); } @@ -68,6 +72,8 @@ for (const required of [ "mtm-coding", "mtm.coding", "ponytail", + "modernGoEnabled", + "Modern Go Guidelines", "rtkMode", "RTK", "shell.overlay", @@ -118,6 +124,10 @@ if (tarball !== undefined) { "package/lib/types/client/index.d.ts", "package/lib/types/index.d.ts", "package/lib/types/features/coding/index.d.ts", + "package/resources/go-modern-guidelines/scripts/VERSION", + "package/resources/go-modern-guidelines/LICENSE", + "package/resources/go-modern-guidelines/scripts/run-tool.sh", + "package/resources/go-modern-guidelines/scripts/run-tool.ps1", "package/dist/standalone/index.html", "package/dist/standalone/config.js", "package/dist/embed/mtmharness.js", diff --git a/packages/mtmharness/src/client/index.test.ts b/packages/mtmharness/src/client/index.test.ts index e9ff7c3..e21c52f 100644 --- a/packages/mtmharness/src/client/index.test.ts +++ b/packages/mtmharness/src/client/index.test.ts @@ -18,6 +18,8 @@ function clientBench(): { registered: Registered[]; cleanups: Array<() => void | value: { codebaseMemoryEnabled: false, codebaseMemoryAugmentHooks: true, + modernGoEnabled: true, + modernGoCommand: "", ponytailEnabled: true, ponytailMode: "full", ponytailSubagents: true, @@ -85,6 +87,8 @@ async function hostBench(): Promise<{ provided: Record; cleanup const settings = { codebaseMemoryEnabled: false, codebaseMemoryAugmentHooks: true, + modernGoEnabled: false, + modernGoCommand: "", ponytailEnabled: false, ponytailMode: "full", ponytailSubagents: true, diff --git a/packages/mtmharness/src/features/coding/client/MtmCodingCard.tsx b/packages/mtmharness/src/features/coding/client/MtmCodingCard.tsx index 9a04bad..e7c91a3 100644 --- a/packages/mtmharness/src/features/coding/client/MtmCodingCard.tsx +++ b/packages/mtmharness/src/features/coding/client/MtmCodingCard.tsx @@ -112,6 +112,8 @@ export function MtmCodingCard(props: MtmCodingCardProps) { {disabled ?

{t("readOnly")}

: null} { props.edit("codebaseMemoryEnabled", String(value)); }} onReset={() => { props.resetField("codebaseMemoryEnabled"); }} /> { props.edit("codebaseMemoryAugmentHooks", String(value)); }} onReset={() => { props.resetField("codebaseMemoryAugmentHooks"); }} /> + { props.edit("modernGoEnabled", String(value)); }} onReset={() => { props.resetField("modernGoEnabled"); }} /> + { props.edit("modernGoCommand", value); }} onReset={() => { props.resetField("modernGoCommand"); }} /> { props.edit("ponytailEnabled", String(value)); }} onReset={() => { props.resetField("ponytailEnabled"); }} />
{fieldLabel(t, "ponytailMode", mode, () => { props.resetField("ponytailMode"); }, disabled)} diff --git a/packages/mtmharness/src/features/coding/client/controller.ts b/packages/mtmharness/src/features/coding/client/controller.ts index e424484..2a3fd59 100644 --- a/packages/mtmharness/src/features/coding/client/controller.ts +++ b/packages/mtmharness/src/features/coding/client/controller.ts @@ -8,6 +8,8 @@ export const RTK_MODE_VALUES: readonly RtkMode[] = ["off", "guidance", "auto", " const FIELD_NAMES = [ "codebaseMemoryEnabled", "codebaseMemoryAugmentHooks", + "modernGoEnabled", + "modernGoCommand", "ponytailEnabled", "ponytailMode", "ponytailSubagents", @@ -45,14 +47,14 @@ type StagedEdit = { readonly text: string; readonly clear: boolean }; type FieldValue = boolean | PonytailMode | RtkMode | string; function format(field: MtmCodingField, value: unknown): string { - if (field === "ponytailMode" || field === "rtkMode" || field === "rtkCommand") return typeof value === "string" ? value : ""; + if (field === "ponytailMode" || field === "rtkMode" || field === "rtkCommand" || field === "modernGoCommand") return typeof value === "string" ? value : ""; return typeof value === "boolean" ? String(value) : ""; } function parse(field: MtmCodingField, text: string): FieldValue | undefined { if (field === "ponytailMode") return MODE_VALUES.includes(text as PonytailMode) ? text as PonytailMode : undefined; if (field === "rtkMode") return RTK_MODE_VALUES.includes(text as RtkMode) ? text as RtkMode : undefined; - if (field === "rtkCommand") return text; + if (field === "rtkCommand" || field === "modernGoCommand") return text; if (text === "true" || text === "false") return text === "true"; return undefined; } diff --git a/packages/mtmharness/src/features/coding/client/locales.ts b/packages/mtmharness/src/features/coding/client/locales.ts index 70b0f2c..0265ae0 100644 --- a/packages/mtmharness/src/features/coding/client/locales.ts +++ b/packages/mtmharness/src/features/coding/client/locales.ts @@ -6,6 +6,10 @@ export type MtmCodingLocaleKey = | "codebaseMemoryEnabledHint" | "codebaseMemoryAugmentHooks" | "codebaseMemoryAugmentHooksHint" + | "modernGoEnabled" + | "modernGoEnabledHint" + | "modernGoCommand" + | "modernGoCommandHint" | "ponytailEnabled" | "ponytailEnabledHint" | "ponytailMode" @@ -40,11 +44,15 @@ export type MtmCodingLocaleKey = export const en: Record = { nav: "Coding", title: "MTM Coding", - description: "Codebase Memory and Ponytail coding assistance.", + description: "Codebase Memory, Modern Go, and Ponytail coding assistance.", codebaseMemoryEnabled: "Codebase Memory", codebaseMemoryEnabledHint: "Expose graph-first code discovery tools and guidance.", codebaseMemoryAugmentHooks: "Codebase Memory context augmentation", codebaseMemoryAugmentHooksHint: "Add bounded repository context around session and read/search events.", + modernGoEnabled: "Modern Go Guidelines", + modernGoEnabledHint: "Use JetBrains Modern Go guidance when working on Go code.", + modernGoCommand: "Modern Go wrapper override", + modernGoCommandHint: "Optional wrapper command; leave empty for the bundled on-demand wrapper.", ponytailEnabled: "Ponytail", ponytailEnabledHint: "Apply the lazy-senior-dev rules and bundled skills.", ponytailMode: "Ponytail intensity", @@ -80,11 +88,15 @@ export const en: Record = { export const zh: Record = { nav: "编程", title: "MTM 编程", - description: "统一配置 Codebase Memory 与 Ponytail 编程辅助。", + description: "统一配置 Codebase Memory、Modern Go 与 Ponytail 编程辅助。", codebaseMemoryEnabled: "Codebase Memory", codebaseMemoryEnabledHint: "启用图谱优先的代码发现工具和指导。", codebaseMemoryAugmentHooks: "Codebase Memory 上下文增强", codebaseMemoryAugmentHooksHint: "在会话和读/搜索事件周围加入有边界的仓库上下文。", + modernGoEnabled: "Modern Go Guidelines", + modernGoEnabledHint: "编写 Go 代码时使用 JetBrains Modern Go 指导。", + modernGoCommand: "Modern Go wrapper 覆盖", + modernGoCommandHint: "可选的 wrapper 命令;留空使用内置的按需 wrapper。", ponytailEnabled: "Ponytail", ponytailEnabledHint: "启用 lazy senior dev 规则和内置 skills。", ponytailMode: "Ponytail 强度", diff --git a/packages/mtmharness/src/features/coding/index.ts b/packages/mtmharness/src/features/coding/index.ts index f18cbfe..db33e28 100644 --- a/packages/mtmharness/src/features/coding/index.ts +++ b/packages/mtmharness/src/features/coding/index.ts @@ -1,6 +1,7 @@ import type { Context, Fiber } from "@deepseek-ai/cordis"; import { settingsNamespace } from "@deepseek-ai/dsh-settings"; import { apply as applyCodebaseMemory, type Config as CodebaseMemoryConfig } from "./codebase-memory.js"; +import { apply as applyModernGo } from "./modern-go.js"; import { apply as applyPonytail } from "./ponytail.js"; import { apply as applyRtk } from "./rtk.js"; import { @@ -12,6 +13,7 @@ import { export { buildMcpConfig, resolveConfig } from "./codebase-memory.js"; export { MtmCodingSettingsSchema, codebaseMemoryConfig } from "./types.js"; +export { MODERN_GO_RESOURCE_BASE, createModernGoSkill } from "./modern-go.js"; export { PONYTAIL_SKILLS } from "./ponytail-skills.js"; export { ensureRuntime, @@ -23,6 +25,7 @@ export { resolveWorkingDirectory, } from "./runtime.js"; export { apply as applyCodebaseMemory } from "./codebase-memory.js"; +export { apply as applyModernGo } from "./modern-go.js"; export { apply as applyPonytail } from "./ponytail.js"; export { apply as applyRtk } from "./rtk.js"; export type { MtmCodingConfig, MtmCodingSettings, PonytailMode, RtkMode } from "./types.js"; @@ -36,6 +39,12 @@ const CodebaseMemoryFeature = { apply: applyCodebaseMemory, }; +const ModernGoFeature = { + name: "mtm-coding-modern-go", + inject: ["skills"], + apply: applyModernGo, +}; + const PonytailFeature = { name: "mtm-coding-ponytail", inject: ["systemPrompt", "skills", "commands"], @@ -73,6 +82,13 @@ function ponytailKey(settings: MtmCodingSettings): string { }); } +function modernGoKey(settings: MtmCodingSettings): string { + return jsonKey({ + enabled: settings.modernGoEnabled, + command: settings.modernGoCommand, + }); +} + function rtkKey(settings: MtmCodingSettings): string { return jsonKey({ mode: settings.rtkMode, @@ -89,9 +105,11 @@ export async function apply(ctx: Context, rawConfig: MtmCodingConfig = {}): Prom { base: rawConfig }, ); let codebaseMemoryFiber: MountedFiber | undefined; + let modernGoFiber: MountedFiber | undefined; let ponytailFiber: MountedFiber | undefined; let rtkFiber: MountedFiber | undefined; let activeCodebaseKey = ""; + let activeModernGoKey = ""; let activePonytailKey = ""; let activeRtkKey = ""; let reconciling = Promise.resolve(); @@ -99,6 +117,7 @@ export async function apply(ctx: Context, rawConfig: MtmCodingConfig = {}): Prom const reconcile = async (next: MtmCodingSettings): Promise => { const nextCodebaseKey = codebaseKey(next); + const nextModernGoKey = modernGoKey(next); const nextPonytailKey = ponytailKey(next); const nextRtkKey = rtkKey(next); @@ -115,6 +134,16 @@ export async function apply(ctx: Context, rawConfig: MtmCodingConfig = {}): Prom activeCodebaseKey = nextCodebaseKey; } + if (nextModernGoKey !== activeModernGoKey) { + await dispose(modernGoFiber); + modernGoFiber = undefined; + activeModernGoKey = ""; + if (next.modernGoEnabled) { + modernGoFiber = await ctx.plugin(ModernGoFeature, { command: next.modernGoCommand }); + } + activeModernGoKey = nextModernGoKey; + } + if (nextPonytailKey !== activePonytailKey) { await dispose(ponytailFiber); ponytailFiber = undefined; @@ -159,9 +188,11 @@ export async function apply(ctx: Context, rawConfig: MtmCodingConfig = {}): Prom stopWatching(); await reconciling; await dispose(rtkFiber); + await dispose(modernGoFiber); await dispose(ponytailFiber); await dispose(codebaseMemoryFiber); rtkFiber = undefined; + modernGoFiber = undefined; ponytailFiber = undefined; codebaseMemoryFiber = undefined; }, "mtm-coding.lifecycle"); diff --git a/packages/mtmharness/src/features/coding/modern-go.ts b/packages/mtmharness/src/features/coding/modern-go.ts new file mode 100644 index 0000000..1c56c26 --- /dev/null +++ b/packages/mtmharness/src/features/coding/modern-go.ts @@ -0,0 +1,93 @@ +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { Context } from "@deepseek-ai/cordis"; +import type { SkillDefinition } from "@deepseek-ai/dsh-skill"; + +export const MODERN_GO_RESOURCE_BASE = resolve( + dirname(fileURLToPath(import.meta.url)), + "../resources/go-modern-guidelines", +); + +// Adapted from JetBrains/go-modern-guidelines v1.1.1; command paths are resolved for mtmharness. +const SKILL_CONTENT_TEMPLATE = [ + "# Modern Go Guidelines CLI", + "", + "Always write modern, idiomatic Go code. Use the Modern Go Guidelines CLI as the source of truth for modern Go idioms that may be newer than your knowledge cutoff.", + "", + "Command:", + "", + "- Linux or macOS: `__UNIX_COMMAND__`", + "- Windows PowerShell: `__WINDOWS_COMMAND__`", + "", + "First run and approvals:", + "", + "On first use, the wrapper installs the Modern Go Guidelines CLI in a local cache directory.", + "", + "Subcommands:", + "", + "- `list`", + "- `explain`", + "", + "Before editing Go code:", + "", + "1. Call the wrapper's `list` subcommand for the relevant Go file.", + "", + " Prefer passing the file you are about to edit:", + "", + " `__UNIX_COMMAND__ list --file-path path/to/file.go`", + "", + " On Windows, use the PowerShell wrapper with the same arguments.", + "", + " The CLI resolves the applicable Go version from `go.mod`, `go.work`, the local Go toolchain, or an explicit override.", + "", + "2. If the target Go version is already known, you may pass it directly:", + "", + " `__UNIX_COMMAND__ list --go-version 1.24`", + "", + "3. Read the complete list output before deciding which guidelines apply.", + "", + " The list output is ordered newest first. Read the full output because older supported guidelines may still apply.", + "", + " Do not pipe the output through head, tail, grep, sed, or any other truncating/filtering command. Important guidelines may otherwise be missed.", + "", + "4. Treat returned guidelines as authoritative for modern Go style choices in code you are editing.", + "", + " If a guideline applies, follow it even when nearby code or repository convention uses an older pattern. Skip it only when it would not compile, would change behavior, or clearly does not match the edited code. Before skipping a returned guideline that seems relevant, call the wrapper's `explain` subcommand for that guideline ID.", + "", + "Call `explain` only when a specific guideline may apply and you need the detailed explanation or examples. Request only the guideline IDs you intend to evaluate or apply:", + "", + "`__UNIX_COMMAND__ explain sync_waitgroup_go`", + "", + "Multiple guideline IDs may be requested as positional arguments:", + "", + "`__UNIX_COMMAND__ explain atomic_types errors_as_type`", + "", + "Do not call `explain` without guideline IDs. Use `list` first to discover the short guideline list for the target Go version, then call `explain` for the specific returned IDs.", +].join("\n"); + +function content(command: string): string { + const configured = command.trim(); + const unixCommand = configured || "sh \"" + join(MODERN_GO_RESOURCE_BASE, "scripts", "run-tool.sh") + "\""; + const windowsCommand = configured || "& '" + join(MODERN_GO_RESOURCE_BASE, "scripts", "run-tool.ps1") + "'"; + return SKILL_CONTENT_TEMPLATE + .replaceAll("__UNIX_COMMAND__", unixCommand) + .replaceAll("__WINDOWS_COMMAND__", windowsCommand); +} + +/** Build the inline Modern Go skill and its package-owned wrapper resources. */ +export function createModernGoSkill(command = ""): SkillDefinition { + return { + name: "use-modern-go", + description: "Use the Modern Go Guidelines CLI whenever writing, modifying, fixing, or refactoring Go code. Apply its version-specific guidance to generated changes.", + source: "runtime", + provider: "mtm-coding", + resourceBase: { kind: "directory", path: MODERN_GO_RESOURCE_BASE }, + invocation: { modelInvocable: true, userInvocable: true }, + content: content(command), + }; +} + +/** Register the inline Modern Go skill without installing its CLI at startup. */ +export function apply(ctx: Context, config: { command?: string } = {}): void { + ctx.skills.register(createModernGoSkill(config.command)); +} diff --git a/packages/mtmharness/src/features/coding/types.ts b/packages/mtmharness/src/features/coding/types.ts index 534ed84..8fc7402 100644 --- a/packages/mtmharness/src/features/coding/types.ts +++ b/packages/mtmharness/src/features/coding/types.ts @@ -7,6 +7,8 @@ export type RtkMode = "off" | "guidance" | "auto" | "rewrite"; export interface MtmCodingSettings { codebaseMemoryEnabled: boolean; codebaseMemoryAugmentHooks: boolean; + modernGoEnabled: boolean; + modernGoCommand: string; ponytailEnabled: boolean; ponytailMode: PonytailMode; ponytailSubagents: boolean; @@ -40,6 +42,8 @@ const Reconnect = z.object({ export const MtmCodingSettingsSchema: z = z.object({ codebaseMemoryEnabled: z.boolean().default(true), codebaseMemoryAugmentHooks: z.boolean().default(true), + modernGoEnabled: z.boolean().default(true), + modernGoCommand: z.string().default(""), ponytailEnabled: z.boolean().default(true), ponytailMode: z.union(["off", "lite", "full", "ultra"] as const).default("full"), ponytailSubagents: z.boolean().default(true), diff --git a/packages/mtmharness/src/index.ts b/packages/mtmharness/src/index.ts index d93e672..ab091af 100644 --- a/packages/mtmharness/src/index.ts +++ b/packages/mtmharness/src/index.ts @@ -4,6 +4,7 @@ import { apply as applyCodingHost } from "./features/coding/index.ts"; import { apply as applyConnectHost } from "./features/connect/index.ts"; export { buildMcpConfig, resolveConfig } from "./features/coding/index.ts"; +export { MODERN_GO_RESOURCE_BASE, createModernGoSkill } from "./features/coding/modern-go.ts"; export { PONYTAIL_SKILLS } from "./features/coding/ponytail-skills.ts"; export { ensureRuntime, @@ -16,6 +17,7 @@ export { } from "./features/coding/runtime.ts"; export { apply as applyCoding } from "./features/coding/index.ts"; export { apply as applyCodebaseMemory } from "./features/coding/codebase-memory.ts"; +export { apply as applyModernGo } from "./features/coding/modern-go.ts"; export { apply as applyPonytail } from "./features/coding/ponytail.ts"; export { apply as applyRtk } from "./features/coding/rtk.ts"; export { diff --git a/packages/mtmharness/tests/coding-controller.test.mjs b/packages/mtmharness/tests/coding-controller.test.mjs index 7dd780b..69b7b89 100644 --- a/packages/mtmharness/tests/coding-controller.test.mjs +++ b/packages/mtmharness/tests/coding-controller.test.mjs @@ -6,6 +6,8 @@ import { runInNewContext } from "node:vm"; const base = { codebaseMemoryEnabled: true, codebaseMemoryAugmentHooks: true, + modernGoEnabled: true, + modernGoCommand: "", ponytailEnabled: true, ponytailMode: "full", ponytailSubagents: true, @@ -173,6 +175,37 @@ test("client artifact settings card clears an overridden field after Host readba }); +test("client artifact settings card saves and resets Modern Go settings", async () => { + const fake = createScope(); + const client = loadClient(); + const mounted = createClientContext(fake.scope); + client.applyCoding(mounted.context); + const face = mounted.registrations[0].face; + + face.edit("modernGoEnabled", "false"); + face.edit("modernGoCommand", "/opt/go-modern-guidelines"); + face.save(); + await nextTurn(); + + let state = face.hooks.mtmCodingCard.getSnapshot(); + assert.equal(state.failed, false); + assert.equal(state.dirty, false); + assert.equal(fake.getSnapshot().user.modernGoEnabled, false); + assert.equal(fake.getSnapshot().user.modernGoCommand, "/opt/go-modern-guidelines"); + + face.resetField("modernGoCommand"); + face.resetField("modernGoEnabled"); + face.save(); + await nextTurn(); + state = face.hooks.mtmCodingCard.getSnapshot(); + assert.equal(state.fields.modernGoCommand.text, ""); + assert.equal(state.fields.modernGoCommand.overridden, false); + assert.equal(state.fields.modernGoEnabled.text, "true"); + assert.equal(state.fields.modernGoEnabled.overridden, false); + + for (const cleanup of mounted.cleanups.reverse()) await cleanup(); +}); + test("client artifact settings card saves and resets RTK settings", async () => { const fake = createScope(); const client = loadClient(); diff --git a/packages/mtmharness/tests/coding-plugin.test.mjs b/packages/mtmharness/tests/coding-plugin.test.mjs index 639f4f8..08eea78 100644 --- a/packages/mtmharness/tests/coding-plugin.test.mjs +++ b/packages/mtmharness/tests/coding-plugin.test.mjs @@ -1,4 +1,6 @@ import assert from "node:assert/strict"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; import test from "node:test"; import { applyCoding, @@ -12,6 +14,8 @@ import { const DEFAULT_SETTINGS = { codebaseMemoryEnabled: true, codebaseMemoryAugmentHooks: true, + modernGoEnabled: true, + modernGoCommand: "", ponytailEnabled: true, ponytailMode: "full", ponytailSubagents: true, @@ -287,11 +291,19 @@ test("Codebase Memory hooks become bounded DSH context messages", async () => { await fake.dispose(); }); -test("unified settings reconcile both domains and unregisters its watcher", async () => { +test("unified settings reconcile coding features and unregister the watcher", async () => { const fake = createContext({ codebaseMemoryEnabled: false, ponytailMode: "lite" }); await applyCoding(fake.context, {}); assert.equal(name, "mtmharness"); - assert.equal(fake.skills.length, 7); + assert.equal(fake.skills.length, 8); + const modernGo = fake.skills.find((skill) => skill.name === "use-modern-go"); + assert.ok(modernGo); + assert.equal(modernGo.invocation.modelInvocable, true); + assert.equal(modernGo.invocation.userInvocable, true); + assert.equal(existsSync(join(modernGo.resourceBase.path, "scripts", "run-tool.sh")), true); + assert.match(modernGo.content, /run-tool\.sh/); + assert.match(modernGo.content, /list --file-path/); + assert.equal(fake.spawnSpecs.length, 0); const autoSection = fake.sections.find((section) => section.name === "mtm-coding:rtk"); assert.ok(autoSection); assert.match(autoSection.text({}), /guidance is active/); @@ -305,6 +317,21 @@ test("unified settings reconcile both domains and unregisters its watcher", asyn assert.equal(fake.pluginCalls.length, callsBeforeDispose); }); +test("Modern Go can be disabled and accepts a wrapper override", async () => { + const disabled = createContext({ modernGoEnabled: false }); + await applyCoding(disabled.context, {}); + assert.equal(disabled.skills.some((skill) => skill.name === "use-modern-go"), false); + await disabled.dispose(); + + const configured = createContext({ modernGoCommand: "/opt/go-modern-guidelines" }); + await applyCoding(configured.context, {}); + const skill = configured.skills.find((item) => item.name === "use-modern-go"); + assert.ok(skill); + assert.match(skill.content, /\/opt\/go-modern-guidelines list --file-path/); + assert.equal(configured.spawnSpecs.length, 0); + await configured.dispose(); +}); + test("RTK stays honest when the DSH pre-record capability is absent", async () => { const fake = createContext(); await applyRtk(fake.context, { mode: "rewrite", autoInstall: false });