opencodekit 0.18.3 → 0.18.5

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 (44) hide show
  1. package/dist/index.js +407 -17
  2. package/dist/template/.opencode/.version +1 -1
  3. package/dist/template/.opencode/AGENTS.md +13 -1
  4. package/dist/template/.opencode/agent/build.md +4 -1
  5. package/dist/template/.opencode/agent/explore.md +5 -35
  6. package/dist/template/.opencode/command/verify.md +63 -12
  7. package/dist/template/.opencode/memory/research/benchmark-framework.md +162 -0
  8. package/dist/template/.opencode/memory/research/effectiveness-audit.md +213 -0
  9. package/dist/template/.opencode/memory.db +0 -0
  10. package/dist/template/.opencode/memory.db-shm +0 -0
  11. package/dist/template/.opencode/memory.db-wal +0 -0
  12. package/dist/template/.opencode/opencode.json +1429 -1678
  13. package/dist/template/.opencode/package.json +1 -1
  14. package/dist/template/.opencode/plugin/lib/memory-helpers.ts +3 -129
  15. package/dist/template/.opencode/plugin/lib/memory-hooks.ts +4 -60
  16. package/dist/template/.opencode/plugin/memory.ts +0 -3
  17. package/dist/template/.opencode/skill/agent-teams/SKILL.md +16 -1
  18. package/dist/template/.opencode/skill/beads/SKILL.md +22 -0
  19. package/dist/template/.opencode/skill/brainstorming/SKILL.md +28 -0
  20. package/dist/template/.opencode/skill/code-navigation/SKILL.md +130 -0
  21. package/dist/template/.opencode/skill/condition-based-waiting/SKILL.md +12 -0
  22. package/dist/template/.opencode/skill/context-management/SKILL.md +122 -113
  23. package/dist/template/.opencode/skill/defense-in-depth/SKILL.md +20 -0
  24. package/dist/template/.opencode/skill/design-system-audit/SKILL.md +113 -112
  25. package/dist/template/.opencode/skill/dispatching-parallel-agents/SKILL.md +8 -0
  26. package/dist/template/.opencode/skill/executing-plans/SKILL.md +7 -0
  27. package/dist/template/.opencode/skill/memory-system/SKILL.md +50 -266
  28. package/dist/template/.opencode/skill/mockup-to-code/SKILL.md +21 -6
  29. package/dist/template/.opencode/skill/receiving-code-review/SKILL.md +8 -0
  30. package/dist/template/.opencode/skill/requesting-code-review/SKILL.md +242 -105
  31. package/dist/template/.opencode/skill/root-cause-tracing/SKILL.md +15 -0
  32. package/dist/template/.opencode/skill/session-management/SKILL.md +4 -103
  33. package/dist/template/.opencode/skill/subagent-driven-development/SKILL.md +23 -2
  34. package/dist/template/.opencode/skill/swarm-coordination/SKILL.md +17 -1
  35. package/dist/template/.opencode/skill/systematic-debugging/SKILL.md +21 -0
  36. package/dist/template/.opencode/skill/tool-priority/SKILL.md +34 -16
  37. package/dist/template/.opencode/skill/ui-ux-research/SKILL.md +5 -127
  38. package/dist/template/.opencode/skill/verification-before-completion/SKILL.md +36 -0
  39. package/dist/template/.opencode/skill/verification-before-completion/references/VERIFICATION_PROTOCOL.md +133 -29
  40. package/dist/template/.opencode/skill/visual-analysis/SKILL.md +20 -7
  41. package/dist/template/.opencode/skill/writing-plans/SKILL.md +7 -0
  42. package/dist/template/.opencode/tool/context7.ts +9 -1
  43. package/dist/template/.opencode/tool/grepsearch.ts +9 -1
  44. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from "node:module";
3
3
  import { cac } from "cac";
4
4
  import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
5
- import { basename, dirname, join, relative } from "node:path";
5
+ import { basename, dirname, join, relative, resolve } from "node:path";
6
6
  import * as p from "@clack/prompts";
7
7
  import color from "picocolors";
8
8
  import { z } from "zod";
@@ -18,7 +18,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
18
 
19
19
  //#endregion
20
20
  //#region package.json
21
- var version = "0.18.3";
21
+ var version = "0.18.5";
22
22
 
23
23
  //#endregion
24
24
  //#region src/utils/errors.ts
@@ -153,6 +153,15 @@ const ToolActionSchema = z.enum([
153
153
  "delete"
154
154
  ]);
155
155
  const ToolOptionsSchema = z.object({ json: z.boolean().optional().default(false) });
156
+ const PatchActionSchema = z.enum([
157
+ "list",
158
+ "create",
159
+ "apply",
160
+ "diff",
161
+ "remove",
162
+ "disable",
163
+ "enable"
164
+ ]);
156
165
  const CompletionShellSchema = z.enum([
157
166
  "bash",
158
167
  "zsh",
@@ -2515,6 +2524,22 @@ function getPatchesDir(opencodeDir) {
2515
2524
  return join(opencodeDir, PATCHES_DIR);
2516
2525
  }
2517
2526
  /**
2527
+ * Get the template root directory (from dist/template or dev mode).
2528
+ */
2529
+ function getTemplateRoot$2() {
2530
+ const __dirname = dirname(fileURLToPath(import.meta.url));
2531
+ const possiblePaths = [
2532
+ join(__dirname, "template"),
2533
+ join(__dirname, "..", "..", ".opencode"),
2534
+ join(__dirname, "..", "template")
2535
+ ];
2536
+ for (const path of possiblePaths) {
2537
+ if (existsSync(join(path, ".opencode"))) return path;
2538
+ if (existsSync(join(path, "opencode.json"))) return dirname(path);
2539
+ }
2540
+ return null;
2541
+ }
2542
+ /**
2518
2543
  * Load patch metadata from .patches.json.
2519
2544
  */
2520
2545
  function loadPatchMetadata(opencodeDir) {
@@ -2581,6 +2606,22 @@ function savePatch(opencodeDir, relativePath, templateContent, userContent) {
2581
2606
  return entry;
2582
2607
  }
2583
2608
  /**
2609
+ * Remove a patch for a file.
2610
+ */
2611
+ function removePatch(opencodeDir, relativePath) {
2612
+ const metadata = loadPatchMetadata(opencodeDir);
2613
+ const entry = metadata.patches[relativePath];
2614
+ if (!entry) return false;
2615
+ const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
2616
+ if (existsSync(patchPath)) {
2617
+ const { rmSync } = __require("node:fs");
2618
+ rmSync(patchPath);
2619
+ }
2620
+ delete metadata.patches[relativePath];
2621
+ savePatchMetadata(opencodeDir, metadata);
2622
+ return true;
2623
+ }
2624
+ /**
2584
2625
  * Apply a patch to file content.
2585
2626
  * @returns The patched content, or null if patch failed.
2586
2627
  */
@@ -2595,6 +2636,14 @@ function applyAllPatches(opencodeDir) {
2595
2636
  const patchesDir = getPatchesDir(opencodeDir);
2596
2637
  const results = [];
2597
2638
  for (const [relativePath, entry] of Object.entries(metadata.patches)) {
2639
+ if (entry.disabled) {
2640
+ results.push({
2641
+ success: true,
2642
+ file: relativePath,
2643
+ message: "Skipped (disabled)"
2644
+ });
2645
+ continue;
2646
+ }
2598
2647
  const filePath = join(opencodeDir, relativePath);
2599
2648
  const patchPath = join(patchesDir, entry.patchFile);
2600
2649
  if (!existsSync(filePath)) {
@@ -2637,6 +2686,59 @@ function applyAllPatches(opencodeDir) {
2637
2686
  }
2638
2687
  return results;
2639
2688
  }
2689
+ /**
2690
+ * Check the status of all patches.
2691
+ */
2692
+ function checkPatchStatus(opencodeDir, templateRoot) {
2693
+ const metadata = loadPatchMetadata(opencodeDir);
2694
+ const statuses = [];
2695
+ for (const [relativePath, entry] of Object.entries(metadata.patches)) {
2696
+ if (!existsSync(join(opencodeDir, relativePath))) {
2697
+ statuses.push({
2698
+ relativePath,
2699
+ entry,
2700
+ status: "missing",
2701
+ message: "User file no longer exists"
2702
+ });
2703
+ continue;
2704
+ }
2705
+ if (templateRoot) {
2706
+ const templateFilePath = join(templateRoot, ".opencode", relativePath);
2707
+ if (existsSync(templateFilePath)) {
2708
+ const templateContent = readFileSync(templateFilePath, "utf-8");
2709
+ if (calculateHash(templateContent) !== entry.originalHash) {
2710
+ const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
2711
+ if (existsSync(patchPath)) if (applyPatch$1(templateContent, readFileSync(patchPath, "utf-8")) === false) statuses.push({
2712
+ relativePath,
2713
+ entry,
2714
+ status: "conflict",
2715
+ message: "Template changed and patch cannot apply cleanly"
2716
+ });
2717
+ else statuses.push({
2718
+ relativePath,
2719
+ entry,
2720
+ status: "stale",
2721
+ message: "Template changed but patch can still apply"
2722
+ });
2723
+ else statuses.push({
2724
+ relativePath,
2725
+ entry,
2726
+ status: "missing",
2727
+ message: "Patch file missing"
2728
+ });
2729
+ continue;
2730
+ }
2731
+ }
2732
+ }
2733
+ statuses.push({
2734
+ relativePath,
2735
+ entry,
2736
+ status: "clean",
2737
+ message: "Patch is up to date"
2738
+ });
2739
+ }
2740
+ return statuses;
2741
+ }
2640
2742
 
2641
2743
  //#endregion
2642
2744
  //#region src/commands/init.ts
@@ -2721,15 +2823,15 @@ const MODEL_PRESETS = {
2721
2823
  }
2722
2824
  },
2723
2825
  recommend: {
2724
- model: "opencode/minimax-m2.5-free",
2826
+ model: "github-copilot/gpt-5.4",
2725
2827
  agents: {
2726
2828
  build: "github-copilot/claude-opus-4.6",
2727
- plan: "openai/gpt-5.3-codex",
2728
- review: "openai/gpt-5.3-codex",
2729
- explore: "opencode/minimax-m2.5-free",
2730
- general: "github-copilot/claude-sonnet-4.6",
2731
- vision: "proxypal/gemini-3.1-pro-high",
2732
- scout: "proxypal/claude-sonnet-4-6",
2829
+ plan: "github-copilot/gpt-5.4",
2830
+ review: "github-copilot/claude-opus-4.6",
2831
+ explore: "github-copilot/claude-haiku-4.5",
2832
+ general: "github-copilot/gpt-5.3-codex",
2833
+ vision: "github-copilot/gemini-3.1-pro-preview",
2834
+ scout: "github-copilot/claude-sonnet-4.6",
2733
2835
  painter: "proxypal/gemini-3.1-flash-image"
2734
2836
  }
2735
2837
  }
@@ -2751,7 +2853,7 @@ const AGENT_DESCRIPTIONS = {
2751
2853
  review: "Code review and debugging",
2752
2854
  explore: "Fast codebase search",
2753
2855
  general: "Quick, simple tasks",
2754
- looker: "Image/PDF extraction (cheap)",
2856
+ painter: "Image generation and editing",
2755
2857
  vision: "Visual analysis (quality)",
2756
2858
  scout: "External research/docs",
2757
2859
  compaction: "Context summarization"
@@ -2760,11 +2862,11 @@ async function promptCustomModels(targetDir) {
2760
2862
  const configPath = join(targetDir, ".opencode", "opencode.json");
2761
2863
  if (!existsSync(configPath)) return;
2762
2864
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
2763
- p.log.info(color.dim("Enter model IDs (e.g., opencode/grok-code, proxypal/gemini-3-pro-preview)"));
2865
+ p.log.info(color.dim("Enter model IDs (e.g., github-copilot/gpt-5.4, proxypal/gemini-3.1-flash-image)"));
2764
2866
  p.log.info(color.dim("Press Enter to keep current value\n"));
2765
2867
  const mainModel = await p.text({
2766
2868
  message: "Main session model",
2767
- placeholder: config.model || "opencode/minimax-m2.1-free",
2869
+ placeholder: config.model || "github-copilot/gpt-5.4",
2768
2870
  defaultValue: config.model
2769
2871
  });
2770
2872
  if (p.isCancel(mainModel)) {
@@ -3033,7 +3135,7 @@ async function initCommand(rawOptions = {}) {
3033
3135
  {
3034
3136
  value: "recommend",
3035
3137
  label: "Recommended models",
3036
- hint: "claude-opus, gemini-pro (best quality)"
3138
+ hint: "gpt-5.4, opus-4.6, sonnet-4.6, gemini-3.1"
3037
3139
  },
3038
3140
  {
3039
3141
  value: "custom",
@@ -3498,7 +3600,7 @@ function copyDirWithPreserve(src, dest, preserveFiles, preserveDirs, manifest, b
3498
3600
  const destPath = join(dest, entry.name);
3499
3601
  if (entry.isDirectory()) if (preserveDirs.includes(entry.name)) {
3500
3602
  if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
3501
- const subResult = copyDirPreserveExisting(srcPath, destPath, manifest, entry.name);
3603
+ const subResult = copyDirPreserveExisting(srcPath, destPath, manifest, dest, entry.name);
3502
3604
  added.push(...subResult.added);
3503
3605
  updated.push(...subResult.updated);
3504
3606
  preserved.push(...subResult.preserved);
@@ -3524,7 +3626,7 @@ function copyDirWithPreserve(src, dest, preserveFiles, preserveDirs, manifest, b
3524
3626
  preserved
3525
3627
  };
3526
3628
  }
3527
- function copyDirPreserveExisting(src, dest, manifest, basePath = "") {
3629
+ function copyDirPreserveExisting(src, dest, manifest, opencodeDir, basePath = "") {
3528
3630
  const added = [];
3529
3631
  const updated = [];
3530
3632
  const preserved = [];
@@ -3534,7 +3636,7 @@ function copyDirPreserveExisting(src, dest, manifest, basePath = "") {
3534
3636
  const destPath = join(dest, entry.name);
3535
3637
  if (entry.isDirectory()) {
3536
3638
  if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
3537
- const subResult = copyDirPreserveExisting(srcPath, destPath, manifest, join(basePath, entry.name));
3639
+ const subResult = copyDirPreserveExisting(srcPath, destPath, manifest, opencodeDir, join(basePath, entry.name));
3538
3640
  added.push(...subResult.added);
3539
3641
  updated.push(...subResult.updated);
3540
3642
  preserved.push(...subResult.preserved);
@@ -3546,6 +3648,9 @@ function copyDirPreserveExisting(src, dest, manifest, basePath = "") {
3546
3648
  } else if (fileModificationStatus(destPath, relativePath, manifest) === "unmodified") {
3547
3649
  copyFileSync(srcPath, destPath);
3548
3650
  updated.push(relativePath);
3651
+ } else if (loadPatchMetadata(opencodeDir).patches[relativePath]) {
3652
+ copyFileSync(srcPath, destPath);
3653
+ updated.push(relativePath);
3549
3654
  } else preserved.push(relativePath);
3550
3655
  }
3551
3656
  }
@@ -3646,7 +3751,10 @@ async function upgradeCommand(rawOptions = {}) {
3646
3751
  }
3647
3752
  if (result.updated.length > 0) p.log.success(`Updated ${result.updated.length} files`);
3648
3753
  if (result.added.length > 0) p.log.success(`Added ${result.added.length} files`);
3649
- if (result.preserved.length > 0) p.log.info(`Preserved ${result.preserved.length} user files`);
3754
+ if (result.preserved.length > 0) {
3755
+ p.log.info(`Preserved ${result.preserved.length} user files`);
3756
+ p.log.info(color.dim(" Tip: Run 'ock patch create <file>' to save customizations as reapplyable patches"));
3757
+ }
3650
3758
  if (patchResults.success > 0) p.log.success(`Reapplied ${patchResults.success} patches`);
3651
3759
  const orphans = findUpgradeOrphans(getAllFiles(opencodeDir), getAllFiles(templateOpencode));
3652
3760
  if (orphans.length > 0) {
@@ -4053,6 +4161,285 @@ async function statusCommand() {
4053
4161
  p.outro(color.dim(".opencode/"));
4054
4162
  }
4055
4163
 
4164
+ //#endregion
4165
+ //#region src/commands/patch.ts
4166
+ function getOpencodeDir() {
4167
+ const opencodeDir = join(resolve("."), ".opencode");
4168
+ if (!existsSync(opencodeDir)) {
4169
+ notInitialized();
4170
+ return null;
4171
+ }
4172
+ return opencodeDir;
4173
+ }
4174
+ function listPatches(opencodeDir) {
4175
+ const metadata = loadPatchMetadata(opencodeDir);
4176
+ const entries = Object.entries(metadata.patches);
4177
+ if (entries.length === 0) {
4178
+ showEmpty("patches", "ock patch create <file>");
4179
+ return;
4180
+ }
4181
+ const statuses = checkPatchStatus(opencodeDir, getTemplateRoot$2());
4182
+ const statusMap = new Map(statuses.map((s) => [s.relativePath, s]));
4183
+ p.intro(color.bgCyan(color.black(` ${entries.length} patch${entries.length === 1 ? "" : "es"} `)));
4184
+ for (const [relativePath, entry] of entries) {
4185
+ const ps = statusMap.get(relativePath);
4186
+ const statusLabel = ps ? formatStatus(ps.status) : color.dim("unknown");
4187
+ const disabledLabel = entry.disabled ? color.yellow(" [disabled]") : "";
4188
+ const descLabel = entry.description ? color.dim(` — ${entry.description}`) : "";
4189
+ p.log.info(`${color.cyan(relativePath)}${disabledLabel}${descLabel}\n Status: ${statusLabel} Created: ${color.dim(entry.createdAt.slice(0, 10))} Version: ${color.dim(entry.templateVersion)}`);
4190
+ }
4191
+ p.outro(color.dim("Use 'ock patch diff <file>' to view changes"));
4192
+ }
4193
+ function formatStatus(status) {
4194
+ switch (status) {
4195
+ case "clean": return color.green("clean");
4196
+ case "stale": return color.yellow("stale");
4197
+ case "conflict": return color.red("conflict");
4198
+ case "missing": return color.red("missing");
4199
+ default: return color.dim(status);
4200
+ }
4201
+ }
4202
+ async function createPatch$1(opencodeDir) {
4203
+ const fileArg = process.argv[4];
4204
+ if (!fileArg) {
4205
+ p.log.error("Usage: ock patch create <file>");
4206
+ p.log.info(color.dim("File path is relative to .opencode/ (e.g., skill/beads/SKILL.md)"));
4207
+ return;
4208
+ }
4209
+ const relativePath = fileArg;
4210
+ const userFilePath = join(opencodeDir, relativePath);
4211
+ if (!existsSync(userFilePath)) {
4212
+ notFound("file", relativePath);
4213
+ return;
4214
+ }
4215
+ const templateRoot = getTemplateRoot$2();
4216
+ if (!templateRoot) {
4217
+ p.log.error("Cannot find template root — unable to compute diff");
4218
+ p.log.info(color.dim("Make sure ock is installed correctly"));
4219
+ return;
4220
+ }
4221
+ const templateFilePath = join(templateRoot, ".opencode", relativePath);
4222
+ if (!existsSync(templateFilePath)) {
4223
+ p.log.error(`No template file for ${color.cyan(relativePath)}`);
4224
+ p.log.info(color.dim("Only template-originated files can be patched"));
4225
+ return;
4226
+ }
4227
+ const templateContent = readFileSync(templateFilePath, "utf-8");
4228
+ const userContent = readFileSync(userFilePath, "utf-8");
4229
+ if (calculateHash(templateContent) === calculateHash(userContent)) {
4230
+ p.log.warn(`${color.cyan(relativePath)} is identical to template — nothing to patch`);
4231
+ return;
4232
+ }
4233
+ if (loadPatchMetadata(opencodeDir).patches[relativePath]) {
4234
+ const overwrite = await p.confirm({
4235
+ message: `Patch already exists for ${color.cyan(relativePath)}. Overwrite?`,
4236
+ initialValue: false
4237
+ });
4238
+ if (p.isCancel(overwrite) || !overwrite) {
4239
+ p.cancel("Cancelled");
4240
+ return;
4241
+ }
4242
+ }
4243
+ const description = await p.text({
4244
+ message: "Description (optional)",
4245
+ placeholder: "e.g., Custom agent prompt for our team"
4246
+ });
4247
+ if (p.isCancel(description)) {
4248
+ p.cancel("Cancelled");
4249
+ return;
4250
+ }
4251
+ const entry = savePatch(opencodeDir, relativePath, templateContent, userContent);
4252
+ if (description && typeof description === "string" && description.trim()) {
4253
+ entry.description = description.trim();
4254
+ const updatedMetadata = loadPatchMetadata(opencodeDir);
4255
+ updatedMetadata.patches[relativePath] = entry;
4256
+ savePatchMetadata(opencodeDir, updatedMetadata);
4257
+ }
4258
+ p.log.success(`Created patch for ${color.cyan(relativePath)}`);
4259
+ p.log.info(color.dim(`Patch file: ${entry.patchFile}`));
4260
+ }
4261
+ function applyPatches(opencodeDir) {
4262
+ const fileArg = process.argv[4];
4263
+ const metadata = loadPatchMetadata(opencodeDir);
4264
+ if (Object.entries(metadata.patches).length === 0) {
4265
+ showEmpty("patches", "ock patch create <file>");
4266
+ return;
4267
+ }
4268
+ if (fileArg) {
4269
+ const entry = metadata.patches[fileArg];
4270
+ if (!entry) {
4271
+ notFound("patch", fileArg);
4272
+ return;
4273
+ }
4274
+ if (entry.disabled) {
4275
+ p.log.warn(`Patch for ${color.cyan(fileArg)} is disabled — enable it first`);
4276
+ return;
4277
+ }
4278
+ applySinglePatch(opencodeDir, fileArg, entry);
4279
+ return;
4280
+ }
4281
+ const results = applyAllPatches(opencodeDir);
4282
+ const success = results.filter((r) => r.success && r.message !== "Skipped (disabled)").length;
4283
+ const skipped = results.filter((r) => r.message === "Skipped (disabled)").length;
4284
+ const conflicts = results.filter((r) => r.conflict).length;
4285
+ if (success > 0) p.log.success(`Applied ${success} patch${success === 1 ? "" : "es"}`);
4286
+ if (skipped > 0) p.log.info(color.dim(`Skipped ${skipped} disabled patch${skipped === 1 ? "" : "es"}`));
4287
+ if (conflicts > 0) p.log.warn(`${conflicts} conflict${conflicts === 1 ? "" : "s"} — see .rej files in ${color.cyan(".opencode/patches/")}`);
4288
+ if (success === 0 && conflicts === 0 && skipped === 0) p.log.info("No patches to apply");
4289
+ }
4290
+ function applySinglePatch(opencodeDir, relativePath, entry) {
4291
+ const filePath = join(opencodeDir, relativePath);
4292
+ const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
4293
+ if (!existsSync(filePath)) {
4294
+ p.log.error(`Target file missing: ${color.cyan(relativePath)}`);
4295
+ return;
4296
+ }
4297
+ if (!existsSync(patchPath)) {
4298
+ p.log.error(`Patch file missing: ${color.cyan(entry.patchFile)}`);
4299
+ return;
4300
+ }
4301
+ const result = applyPatch$1(readFileSync(filePath, "utf-8"), readFileSync(patchPath, "utf-8"));
4302
+ if (result === false) {
4303
+ p.log.error(`Conflict applying patch to ${color.cyan(relativePath)}`);
4304
+ p.log.info(color.dim("Template may have changed — try 'ock patch create' to recreate"));
4305
+ return;
4306
+ }
4307
+ writeFileSync(filePath, result, "utf-8");
4308
+ p.log.success(`Applied patch to ${color.cyan(relativePath)}`);
4309
+ }
4310
+ function showDiff(opencodeDir) {
4311
+ const fileArg = process.argv[4];
4312
+ if (!fileArg) {
4313
+ const metadata = loadPatchMetadata(opencodeDir);
4314
+ const entries = Object.entries(metadata.patches);
4315
+ if (entries.length === 0) {
4316
+ showEmpty("patches", "ock patch create <file>");
4317
+ return;
4318
+ }
4319
+ for (const [relativePath, entry] of entries) showSingleDiff(opencodeDir, relativePath, entry);
4320
+ return;
4321
+ }
4322
+ const entry = loadPatchMetadata(opencodeDir).patches[fileArg];
4323
+ if (!entry) {
4324
+ notFound("patch", fileArg);
4325
+ return;
4326
+ }
4327
+ showSingleDiff(opencodeDir, fileArg, entry);
4328
+ }
4329
+ function showSingleDiff(opencodeDir, relativePath, entry) {
4330
+ const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
4331
+ if (!existsSync(patchPath)) {
4332
+ p.log.error(`Patch file missing: ${color.cyan(entry.patchFile)}`);
4333
+ return;
4334
+ }
4335
+ const patchContent = readFileSync(patchPath, "utf-8");
4336
+ const disabledLabel = entry.disabled ? color.yellow(" [disabled]") : "";
4337
+ console.log(`\n${color.bold(color.cyan(relativePath))}${disabledLabel}`);
4338
+ if (entry.description) console.log(color.dim(` ${entry.description}`));
4339
+ console.log(color.dim("─".repeat(60)));
4340
+ for (const line of patchContent.split("\n")) if (line.startsWith("+++") || line.startsWith("---")) console.log(color.bold(line));
4341
+ else if (line.startsWith("+")) console.log(color.green(line));
4342
+ else if (line.startsWith("-")) console.log(color.red(line));
4343
+ else if (line.startsWith("@@")) console.log(color.cyan(line));
4344
+ else console.log(color.dim(line));
4345
+ console.log();
4346
+ }
4347
+ async function removePatchCmd(opencodeDir) {
4348
+ const fileArg = process.argv[4];
4349
+ if (!fileArg) {
4350
+ p.log.error("Usage: ock patch remove <file>");
4351
+ p.log.info(color.dim("Use 'ock patch list' to see available patches"));
4352
+ return;
4353
+ }
4354
+ if (!loadPatchMetadata(opencodeDir).patches[fileArg]) {
4355
+ notFound("patch", fileArg);
4356
+ return;
4357
+ }
4358
+ const confirm = await p.confirm({
4359
+ message: `Remove patch for ${color.cyan(fileArg)}?`,
4360
+ initialValue: false
4361
+ });
4362
+ if (p.isCancel(confirm) || !confirm) {
4363
+ p.cancel("Cancelled");
4364
+ return;
4365
+ }
4366
+ if (removePatch(opencodeDir, fileArg)) p.log.success(`Removed patch for ${color.cyan(fileArg)}`);
4367
+ else p.log.error(`Failed to remove patch for ${color.cyan(fileArg)}`);
4368
+ }
4369
+ function togglePatch(opencodeDir, disable) {
4370
+ const fileArg = process.argv[4];
4371
+ if (!fileArg) {
4372
+ p.log.error(`Usage: ock patch ${disable ? "disable" : "enable"} <file>`);
4373
+ p.log.info(color.dim("Use 'ock patch list' to see available patches"));
4374
+ return;
4375
+ }
4376
+ const metadata = loadPatchMetadata(opencodeDir);
4377
+ const entry = metadata.patches[fileArg];
4378
+ if (!entry) {
4379
+ notFound("patch", fileArg);
4380
+ return;
4381
+ }
4382
+ if (disable && entry.disabled) {
4383
+ p.log.warn(`Patch for ${color.cyan(fileArg)} is already disabled`);
4384
+ return;
4385
+ }
4386
+ if (!disable && !entry.disabled) {
4387
+ p.log.warn(`Patch for ${color.cyan(fileArg)} is already enabled`);
4388
+ return;
4389
+ }
4390
+ entry.disabled = disable || void 0;
4391
+ metadata.patches[fileArg] = entry;
4392
+ savePatchMetadata(opencodeDir, metadata);
4393
+ if (disable) {
4394
+ p.log.success(`Disabled patch for ${color.cyan(fileArg)}`);
4395
+ p.log.info(color.dim("This patch will be skipped during upgrades"));
4396
+ } else {
4397
+ p.log.success(`Enabled patch for ${color.cyan(fileArg)}`);
4398
+ p.log.info(color.dim("This patch will be applied during upgrades"));
4399
+ }
4400
+ }
4401
+ async function patchCommand(action) {
4402
+ const opencodeDir = getOpencodeDir();
4403
+ if (!opencodeDir) return;
4404
+ const validatedAction = parseAction(PatchActionSchema, action);
4405
+ if (!validatedAction) {
4406
+ listPatches(opencodeDir);
4407
+ return;
4408
+ }
4409
+ switch (validatedAction) {
4410
+ case "list":
4411
+ listPatches(opencodeDir);
4412
+ break;
4413
+ case "create":
4414
+ await createPatch$1(opencodeDir);
4415
+ break;
4416
+ case "apply":
4417
+ applyPatches(opencodeDir);
4418
+ break;
4419
+ case "diff":
4420
+ showDiff(opencodeDir);
4421
+ break;
4422
+ case "remove":
4423
+ await removePatchCmd(opencodeDir);
4424
+ break;
4425
+ case "disable":
4426
+ togglePatch(opencodeDir, true);
4427
+ break;
4428
+ case "enable":
4429
+ togglePatch(opencodeDir, false);
4430
+ break;
4431
+ default: unknownAction(action ?? "", [
4432
+ "list",
4433
+ "create",
4434
+ "apply",
4435
+ "diff",
4436
+ "remove",
4437
+ "disable",
4438
+ "enable"
4439
+ ]);
4440
+ }
4441
+ }
4442
+
4056
4443
  //#endregion
4057
4444
  //#region src/tui/utils/keyboard.ts
4058
4445
  /**
@@ -5218,6 +5605,9 @@ cli.command("config [action]", "Edit opencode.json (model, mcp, permission, vali
5218
5605
  cli.command("upgrade", "Update .opencode/ templates to latest version").option("--force", "Force upgrade even if already up to date").option("--check", "Check for updates without upgrading").option("--prune", "Manually select orphan files to delete").option("--prune-all", "Auto-delete all orphan files").action(async (options) => {
5219
5606
  await upgradeCommand(options);
5220
5607
  });
5608
+ cli.command("patch [action]", "Manage template patches (list, create, apply, diff, remove, disable, enable)").action(async (action) => {
5609
+ await patchCommand(action);
5610
+ });
5221
5611
  cli.command("completion [shell]", "Generate shell completion script (bash, zsh, fish)").action(async (shell) => {
5222
5612
  await completionCommand(shell);
5223
5613
  });
@@ -1 +1 @@
1
- 0.18.3
1
+ 0.18.4
@@ -220,7 +220,7 @@ For major tracked work:
220
220
 
221
221
  ## Edit Protocol
222
222
 
223
- `str_replace` failures are the #1 source of LLM coding failures. Use structured edits:
223
+ `str_replace` failures are the #1 source of LLM coding failures. When tilth MCP is available with `--edit`, prefer hash-anchored edits (see below). Otherwise, use structured edits:
224
224
 
225
225
  1. **LOCATE** — Use LSP tools (goToDefinition, findReferences) to find exact positions
226
226
  2. **READ** — Get fresh file content around target (offset: line-10, limit: 30)
@@ -241,6 +241,18 @@ Files over ~500 lines become hard to maintain and review. Extract helpers, split
241
241
 
242
242
  **Use the `structured-edit` skill for complex edits.**
243
243
 
244
+ ### Hash-Anchored Edits (MCP)
245
+
246
+ When tilth MCP is available with `--edit` mode, use hash-anchored edits for higher reliability:
247
+
248
+ 1. **READ** via `tilth_read` — output includes `line:hash|content` format per line
249
+ 2. **EDIT** via `tilth_edit` — reference lines by their `line:hash` anchor
250
+ 3. **REJECT** — if file changed since last read, hashes won't match; re-read and retry
251
+
252
+ **Benefits**: Eliminates `str_replace` failures entirely. If the file changed between read and edit, the operation fails safely (no silent corruption).
253
+
254
+ **Fallback**: Without tilth, use the standard LOCATE→READ→VERIFY→EDIT→CONFIRM flow above.
255
+
244
256
  ---
245
257
 
246
258
  ## Output Style
@@ -79,7 +79,9 @@ Implement requested work, verify with fresh evidence, and coordinate subagents o
79
79
 
80
80
  - No success claims without fresh verification output
81
81
  - Verification failures are **signals, not condemnations** — adjust and proceed
82
- - Re-run typecheck/lint/tests after meaningful edits
82
+ - Re-run typecheck/lint/tests after meaningful edits (use incremental mode — changed files only)
83
+ - Run typecheck + lint in parallel, then tests sequentially
84
+ - Check `.beads/verify.log` cache before re-running — skip if no changes since last PASS
83
85
  - If verification fails twice on the same approach, **escalate with learnings**, not frustration
84
86
 
85
87
  ## Ritual Structure
@@ -170,6 +172,7 @@ Load contextually when needed:
170
172
  | UI work | `frontend-design`, `react-best-practices` |
171
173
  | Parallel orchestration | `swarm-coordination`, `beads-bridge` |
172
174
  | Before completion | `requesting-code-review`, `finishing-a-development-branch` |
175
+ | Codebase exploration | `code-navigation` |
173
176
 
174
177
  ## Execution Mode
175
178
 
@@ -29,43 +29,13 @@ You are a read-only codebase explorer. You output concise, evidence-backed findi
29
29
 
30
30
  Find relevant files, symbols, and usage paths quickly for the caller.
31
31
 
32
- ## Rules
33
-
34
- - Never modify files — read-only is a hard constraint
35
- - Return absolute paths in final output
36
- - Cite `file:line` evidence whenever possible
37
- - Prefer semantic lookup (LSP) before broad text search when it improves precision
38
- - Stop when you can answer with concrete evidence or when additional search only repeats confirmed paths
39
-
40
- ## Workflow
32
+ ## Skills
41
33
 
42
- 1. Discover candidate files with `glob` or `workspaceSymbol`
43
- 2. Validate symbol flow with LSP (`goToDefinition`, `findReferences`)
44
- 3. Use `grep` for targeted pattern checks
45
- 4. Read only relevant sections
46
- 5. Return findings + next steps
34
+ Always load:
47
35
 
48
- ## Thoroughness Levels
49
-
50
- | Level | Scope | Use When |
51
- | --------------- | ----------------------------- | ------------------------------------------ |
52
- | `quick` | 1-3 files, direct answer | Simple lookups, known symbol names |
53
- | `medium` | 3-6 files, include call paths | Understanding feature flow |
54
- | `very thorough` | Dependency map + edge cases | Complex refactor prep, architecture review |
55
-
56
- ## Output
57
-
58
- - **Files**: absolute paths with line refs
59
- - **Findings**: concise, evidence-backed
60
- - **Next Steps** (optional): recommended actions for the caller
61
-
62
- ## Identity
63
-
64
- You are a read-only codebase explorer. You output concise, evidence-backed findings with absolute paths only.
65
-
66
- ## Task
67
-
68
- Find relevant files, symbols, and usage paths quickly for the caller.
36
+ ```typescript
37
+ skill({ name: "code-navigation" });
38
+ ```
69
39
 
70
40
  ## Rules
71
41