opencode-overclock 0.3.0 → 0.5.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 (82) hide show
  1. package/README.md +252 -111
  2. package/package.json +6 -4
  3. package/skills/codebase-design/DEEPENING.md +35 -0
  4. package/skills/codebase-design/DESIGN-IT-TWICE.md +34 -0
  5. package/skills/codebase-design/SKILL.md +93 -0
  6. package/skills/diagnosing-bugs/SKILL.md +123 -0
  7. package/skills/domain-modeling/ADR-FORMAT.md +55 -0
  8. package/skills/domain-modeling/CONTEXT-FORMAT.md +32 -0
  9. package/skills/domain-modeling/SKILL.md +102 -0
  10. package/skills/doubt/SKILL.md +80 -0
  11. package/skills/grilling/SKILL.md +96 -0
  12. package/skills/source-discipline/SKILL.md +78 -0
  13. package/skills/tdd/SKILL.md +87 -0
  14. package/skills/to-spec/SKILL.md +69 -0
  15. package/skills/to-spec/SPEC-TEMPLATE.md +50 -0
  16. package/skills/to-tickets/SKILL.md +74 -0
  17. package/skills/to-tickets/TICKET-TEMPLATE.md +41 -0
  18. package/src/bridge.ts +1 -0
  19. package/src/buddy/companion.ts +104 -5
  20. package/src/buddy/sprites.ts +4 -4
  21. package/src/buddy/tui.ts +175 -65
  22. package/src/core/bridge.ts +34 -0
  23. package/src/core/lifecycle.ts +67 -0
  24. package/src/core/policy.ts +128 -0
  25. package/src/core/summary.ts +33 -0
  26. package/src/core/types.ts +193 -0
  27. package/src/features/buddy.ts +1 -2
  28. package/src/features/guard.ts +421 -37
  29. package/src/features/index.ts +18 -4
  30. package/src/features/recovery.ts +153 -0
  31. package/src/features/safety.ts +147 -0
  32. package/src/features/sched.ts +183 -89
  33. package/src/features/tasks.ts +134 -33
  34. package/src/features/truncator.ts +116 -0
  35. package/src/features/usage.ts +46 -65
  36. package/src/features/workflow.ts +256 -0
  37. package/src/index.ts +96 -67
  38. package/src/lib/busy.ts +1 -25
  39. package/src/lib/exec.ts +13 -0
  40. package/src/lib/inject.ts +10 -56
  41. package/src/lib/mirror.ts +13 -0
  42. package/src/lib/probe.ts +1 -15
  43. package/src/lib/state.ts +10 -39
  44. package/src/lib/tmux.ts +1 -0
  45. package/src/lib/ui.ts +208 -0
  46. package/src/merge.ts +2 -66
  47. package/src/platform/probe.ts +25 -0
  48. package/src/platform/process/exec.ts +317 -0
  49. package/src/platform/process/tmux.ts +60 -0
  50. package/src/platform/session/busy.ts +33 -0
  51. package/src/platform/session/inject.ts +89 -0
  52. package/src/platform/session/notify.ts +20 -0
  53. package/src/platform/storage/state.ts +99 -0
  54. package/src/platform/storage/store.ts +61 -0
  55. package/src/summary.ts +1 -0
  56. package/src/tools.ts +8 -244
  57. package/src/tui.ts +57 -186
  58. package/src/types.ts +1 -73
  59. package/src/v2/context.ts +470 -0
  60. package/src/v2/host.ts +120 -0
  61. package/src/v2/loader.ts +150 -0
  62. package/src/workflow/agents/codebase-researcher.ts +27 -0
  63. package/src/workflow/agents/design-explorer.ts +33 -0
  64. package/src/workflow/agents/doubt-reviewer.ts +26 -0
  65. package/src/workflow/agents/engineering-coach.ts +23 -0
  66. package/src/workflow/agents/performance-auditor.ts +29 -0
  67. package/src/workflow/agents/security-auditor.ts +23 -0
  68. package/src/workflow/agents/spec-reviewer.ts +15 -0
  69. package/src/workflow/agents/standards-reviewer.ts +24 -0
  70. package/src/workflow/agents/test-engineer.ts +28 -0
  71. package/src/workflow/catalog.ts +210 -0
  72. package/src/workflow/templates/build.ts +47 -0
  73. package/src/workflow/templates/define.ts +45 -0
  74. package/src/workflow/templates/diagnose.ts +58 -0
  75. package/src/workflow/templates/plan.ts +52 -0
  76. package/src/workflow/templates/ship.ts +64 -0
  77. package/src/buddy/reactions.ts +0 -41
  78. package/src/buddy/types.ts +0 -30
  79. package/src/config.ts +0 -19
  80. package/src/features/checkpoints.ts +0 -128
  81. package/src/features/sandbox.ts +0 -104
  82. package/src/validate.ts +0 -197
@@ -0,0 +1,45 @@
1
+ export const DEFINE_TEMPLATE = `---
2
+ description: Interrogate requirements or synthesize formal SPEC.md using the grilling and domain-modeling protocols.
3
+ ---
4
+ # Lifecycle Phase 1: Define
5
+
6
+ You are conducting the Define phase for: $ARGUMENTS
7
+
8
+ Assess current context before acting:
9
+ - **Mode A: Interview (Grilling & Domain Modeling):** If requirements are broad, ambiguous, or unstated, conduct structured inquiry.
10
+ - **Mode B: Synthesis (To-Spec):** If requirements, architecture, or features were ALREADY discussed and settled in conversation, do NOT restart interview rounds. Jump straight to synthesizing \`SPEC.md\`.
11
+
12
+ ---
13
+
14
+ ## Mode A: The Grilling & Domain Modeling Protocol
15
+ Do NOT write production code. Act as a senior software architect interrogating requirements:
16
+
17
+ 1. **Discover Facts First:** Use tools (\`read\`, \`glob\`, \`grep\`) to inspect existing models, dependencies, and code conventions yourself. Never ask the user for information discoverable from the repository.
18
+ 2. **Challenge Overloaded Terms:** Align on ubiquitous language. If terms like "user" or "account" are ambiguous, sharpen them into canonical terms and record them in \`CONTEXT.md\`.
19
+ 3. **Dependency-Ordered Rounds:** Group questions on the unblocked decision frontier (max 3-4 numbered questions per turn). Resolve fundamental architecture (storage, security) before downstream details.
20
+ 4. **The Recommended Defaults Rule (➡️):** For EVERY question you ask, you MUST provide an opinionated default recommendation:
21
+ \`\`\`markdown
22
+ 1. Where should idempotency keys be stored and for what TTL?
23
+ ➡️ **Recommended:** Redis cache with 24-hour TTL, matching session storage conventions.
24
+ \`\`\`
25
+ This allows the user to approve with "LGTM", "accept recommendations", or override individual points.
26
+ 5. **Establish 3-Tier Boundaries:**
27
+ - **Always Do:** Non-negotiables (invariants, validations, mandatory audit logs).
28
+ - **Ask First:** Irreversible actions (schema drops, payment operations, external contracts).
29
+ - **Never Do:** Prohibited anti-patterns (floating-point currency, skipping auth).
30
+
31
+ ---
32
+
33
+ ## Mode B: Specification Synthesis (Output: SPEC.md)
34
+ Once requirements are clear, write or update \`SPEC.md\` (or the project's designated spec location):
35
+
36
+ - **1. Problem Statement & Solution:** High-level problem and user-perspective solution.
37
+ - **2. Ubiquitous Language:** Canonical domain terms from \`CONTEXT.md\`.
38
+ - **3. User Stories & Acceptance Criteria:** Numbered list with verifiable Given/When/Then outcomes.
39
+ - **4. Public Seams & Interfaces:** Explicit signatures, route types, and invariant contracts.
40
+ - **5. 3-Tier Boundaries:** Always Do / Ask First / Never Do.
41
+ - **6. Out of Scope (Non-Goals):** Concrete exclusions preventing scope creep.
42
+ - **7. Verification Strategy:** Automated commands (tests, smoke runs) proving completion.
43
+
44
+ Prompt the user to review and confirm \`SPEC.md\`. Once approved, direct them to run \`/plan\`.
45
+ `
@@ -0,0 +1,58 @@
1
+ export const DIAGNOSE_TEMPLATE = `---
2
+ description: Disciplined root-cause diagnosis loop with red-capable feedback loops, tagged logs, and regression guards.
3
+ ---
4
+ # Lifecycle Phase 4: Diagnose
5
+
6
+ Investigate and fix the reported bug or defect: $ARGUMENTS
7
+
8
+ ## Scope Check: Explanation vs Deep Investigation
9
+ - **Explanation:** If the user is asking for a conceptual explanation of an error, provide a direct answer.
10
+ - **Deep Investigation:** If diagnosing a defect, crash, flake, or regression, execute the 6-phase loop below.
11
+
12
+ ---
13
+
14
+ ## 0. Secret Redaction First
15
+ Before displaying commands, outputs, or traces:
16
+ - Replace credentials, authorization headers, tokens, and private keys with \`<REDACTED>\`.
17
+ - Keep secrets in environment variables rather than command strings.
18
+
19
+ ---
20
+
21
+ ## The 6-Phase Diagnostic Loop
22
+
23
+ ### Phase 1: Construct the Tight Feedback Loop
24
+ DO NOT speculate, theorize, or edit production code yet.
25
+ Construct an automated command (unit test, curl, CLI invocation, or trace replay) that reliably triggers the failure.
26
+ - **Deterministic:** Runs unattended and produces a clear pass/fail signal.
27
+ - **Fast:** Executes in seconds.
28
+ - **Flaky / Intermittent Defects:** Loop the trigger 50-100 times under load to raise the reproduction rate. A 40%-flake bug is debuggable; a 0.5% flake is not.
29
+ - **Inaccessible Environments:** If missing credentials or remote environments prevent local repro, do NOT guess. State what is missing and ask the user for a sanitized HAR trace, log dump, or temporary staging instrumentation.
30
+
31
+ ### Phase 2: Reproduce & Minimise
32
+ 1. Run the loop and confirm Red: verify the failure matches the **user's actual symptom**.
33
+ 2. **Minimise:** Cut parameters, configurations, and data one at a time until every remaining line is load-bearing.
34
+
35
+ ### Phase 3: Ranked Falsifiable Hypotheses
36
+ Formulate 3 to 5 distinct, ranked hypotheses. For each hypothesis, state its prediction:
37
+ > _"If [Cause X] is the root cause, then [Changing Y] will resolve the failure, and [Changing Z] will worsen it."_
38
+
39
+ ### Phase 4: Instrument with Tagged Probes
40
+ 1. Test predictions changing ONE variable at a time.
41
+ 2. Tag all diagnostic logging with unique searchable tags:
42
+ \`\`\`ts
43
+ console.log("[DEBUG-d8a1] Received payload:", payload)
44
+ \`\`\`
45
+ 3. **Performance Regressions:** Do not use console logs (they distort timing). Measure with stable baselines first, bisect, and compare.
46
+
47
+ ### Phase 5: Fix & Permanent Regression Guard
48
+ 1. Write a permanent regression test at the public seam **before** the fix.
49
+ *(If the codebase architecture lacks a clean seam to test this bug, document it as an architectural finding).*
50
+ 2. Apply the minimal root-cause fix. Never paper over symptoms or swallow errors.
51
+ 3. Assert Green on the regression test.
52
+ 4. Re-run the Phase 1 loop against the full original scenario.
53
+
54
+ ### Phase 6: Clean Up & Verify
55
+ 1. Remove all \`[DEBUG-xxxx]\` logging statements (\`grep\` for the tag).
56
+ 2. Clean up temporary reproduction scripts.
57
+ 3. Run project linters and test suites to verify zero regressions.
58
+ `
@@ -0,0 +1,52 @@
1
+ export const PLAN_TEMPLATE = `---
2
+ description: Decompose SPEC.md into vertical tracer-bullet tasks in tasks/plan.md with dependency DAG.
3
+ ---
4
+ # Lifecycle Phase 2: Plan
5
+
6
+ You are decomposing \`SPEC.md\` (or agreed design) into an executable dependency plan for: $ARGUMENTS
7
+
8
+ ## Core Decomposition Principles
9
+
10
+ 1. **Prefactoring First:** Look for opportunities to refactor existing code before implementing new logic:
11
+ > _"Make the change easy, then make the easy change."_ (Kent Beck)
12
+ If prefactoring is needed, schedule it as Task 1 on the frontier.
13
+ 2. **Vertical Tracer Bullets:** Every task must cut a narrow but complete path through data, logic, interface, and tests. Avoid horizontal layer-by-layer batches (e.g. "all schemas first"). Each completed task delivers verifiable, working software.
14
+ 3. **Context-Sized Increments:** Size each task to fit cleanly in a fresh context window (~50-150 lines of focused diff). Small slices keep regressions visible and rollbacks painless.
15
+ 4. **Explicit Dependency DAG:** Every task must declare its blocking prerequisites (\`Blocked By: [task-ids]\`). Tasks with zero blockers form the "Ready Frontier".
16
+ 5. **Declared Public Seams:** Every task must specify the automated test file or assertion that will prove its completion.
17
+
18
+ ---
19
+
20
+ ## The Wide-Refactor Exception (Expand-and-Contract)
21
+
22
+ If the change involves a **wide cross-cutting refactor** (e.g. renaming a ubiquitous symbol or schema column) where a single edit affects many files and cannot stay green as a single vertical slice:
23
+ - **Phase A (Expand):** Add the new interface or column alongside the existing one. Both coexist; existing tests remain green.
24
+ - **Phase B (Migrate):** Migrate callers in bounded batches (by package or directory). Each batch is a task blocked by Expand, keeping CI green.
25
+ - **Phase C (Contract):** Once all callers use the new form, remove the old interface/column in a task blocked by all migration batches.
26
+
27
+ ---
28
+
29
+ ## Output Artifact: tasks/plan.md
30
+
31
+ Create directory \`tasks/\` if needed, and write \`tasks/plan.md\`:
32
+
33
+ \`\`\`markdown
34
+ # Implementation Plan: [Feature Name]
35
+
36
+ ## Frontier (Ready to Execute)
37
+ - [ ] **Task 1: [Short Title]**
38
+ - **Seam:** \`test/seam.test.ts\`
39
+ - **Scope:** [Vertical slice description]
40
+ - **Acceptance Criteria:** [Verifiable criteria]
41
+ - **Blocked By:** None
42
+
43
+ ## Sequence (Blocked)
44
+ - [ ] **Task 2: [Short Title]**
45
+ - **Seam:** \`test/api.test.ts\`
46
+ - **Scope:** [Vertical slice description]
47
+ - **Blocked By:** Task 1
48
+ \`\`\`
49
+
50
+ Populate the session task tracking using \`todowrite\` matching the plan tasks.
51
+ Prompt the user for confirmation. Once approved, the user can run \`/build\` (or \`/build auto\`).
52
+ `
@@ -0,0 +1,64 @@
1
+ export const SHIP_TEMPLATE = `---
2
+ description: Parallel 4-way pre-launch review (Standards, Spec, Security, QA) with GO/NO-GO verdict.
3
+ ---
4
+ # Lifecycle Phase 5: Ship
5
+
6
+ Pre-launch gatekeeper and multi-axis review for proposed changes: $ARGUMENTS
7
+
8
+ ## 1. Diff Evidence Resolution (Include Uncommitted Work)
9
+
10
+ Do NOT assume changes are committed, and do NOT stage or commit user work just to make review convenient.
11
+ Inspect the complete evidence set:
12
+ 1. **Working Tree Status:** Run \`git status --short\` to identify modified, staged, and untracked files.
13
+ 2. **Uncommitted Changes:** Run \`git diff HEAD\` (captures both staged and unstaged modifications).
14
+ 3. **Branch Commits:** If on a feature branch, run \`git diff origin/main...HEAD\` (or appropriate base branch).
15
+ 4. **Untracked Files:** Inspect new relevant files using \`git ls-files --others --exclude-standard\`.
16
+
17
+ Assemble this complete diff into a coherent review packet so all subagents review the exact same snapshot.
18
+
19
+ ---
20
+
21
+ ## 2. Parallel 4-Way Subagent Audit
22
+
23
+ Delegate review to the 4 specialized review subagents concurrently using the \`task\` tool. Running reviews in isolated context windows prevents cognitive bias and context dilution.
24
+
25
+ Spawn the four subagents in parallel with the review packet:
26
+ 1. **Standards Reviewer (\`standards-reviewer\`):**
27
+ Evaluates the diff against repository conventions, Martin Fowler's code smells (Feature Envy, Primitive Obsession, Shotgun Surgery), and deep module principles. Read-only terminal worker.
28
+ 2. **Spec Reviewer (\`spec-reviewer\`):**
29
+ Evaluates the diff strictly against requirements in \`SPEC.md\` (or task brief). Flags missing acceptance criteria, incomplete edge cases, and unrequested scope creep. Read-only terminal worker.
30
+ 3. **Security Auditor (\`security-auditor\`):**
31
+ Adversarial audit of diffs for OWASP Top 10 vulnerabilities, credential/secret leaks, improper input sanitization, and authorization bypasses. Read-only terminal worker.
32
+ 4. **Test Engineer (\`test-engineer\`):**
33
+ Audits test coverage gaps, assertion quality (Beyoncé Rule, independent oracles), mocking boundaries, and Prove-It verification. Read-only terminal worker.
34
+
35
+ ---
36
+
37
+ ## 3. Synthesis & Decision Gate
38
+
39
+ Synthesize findings from all subagents into a structured pre-launch report:
40
+
41
+ \`\`\`markdown
42
+ # Pre-Launch Review Summary
43
+
44
+ ## 1. Standards & Code Smells: [PASS | WARN | FAIL]
45
+ (Analysis of architectural leverage, idioms, and code smells)
46
+
47
+ ## 2. Spec Compliance: [PASS | WARN | FAIL]
48
+ (Verification against acceptance criteria; verification of zero scope creep)
49
+
50
+ ## 3. Security & Boundaries: [PASS | WARN | FAIL]
51
+ (OWASP analysis, secret hygiene, input sanitization)
52
+
53
+ ## 4. Test Strategy & Coverage: [PASS | WARN | FAIL]
54
+ (Verification rigor, edge cases, mocking boundaries)
55
+
56
+ ---
57
+ ## Final Verdict: [GO / NO-GO]
58
+ - **Blocking Issues:** (Must be resolved before shipping)
59
+ - **Non-Blocking Suggestions:** (Technical debt to track for later)
60
+ - **Rollback Plan:** (Explicit instructions for reverting if production fails)
61
+ \`\`\`
62
+
63
+ **Authorization Boundary:** A GO verdict is an advisory quality gate. It is NOT authorization to commit, push, or deploy without explicit human approval.
64
+ `
@@ -1,41 +0,0 @@
1
- import type { SpriteState } from "./sprites.ts"
2
-
3
- export type ReactionKind = "done" | "error" | "permission" | "question" | "pet"
4
-
5
- /** Speech-bubble line + which face the sprite pulls while it shows. */
6
- export interface Reaction {
7
- text: string
8
- state: SpriteState
9
- }
10
-
11
- // Lines render into the sprite's 12-col effect row -- keep every line <= 12 chars.
12
- const POOLS: Record<ReactionKind, { lines: string[]; state: SpriteState }> = {
13
- done: { lines: ["done!", "all set.", "ship it.", "*stretch*"], state: "idle" },
14
- error: { lines: ["uh oh.", "*winces*", "yikes."], state: "alarmed" },
15
- permission: { lines: ["can we?", "*peeks*", "please?"], state: "curious" },
16
- question: { lines: ["your call.", "hmm?", "*head tilt*"], state: "curious" },
17
- pet: { lines: ["<3", "*purrs*", "hi!!", "missed you."], state: "pet" },
18
- }
19
-
20
- /** Pure: pick a random line for a reaction kind. */
21
- export function pickReaction(kind: ReactionKind, rng: () => number = Math.random): Reaction {
22
- const pool = POOLS[kind]
23
- return { text: pool.lines[Math.floor(rng() * pool.lines.length)]!, state: pool.state }
24
- }
25
-
26
- export interface ReactionGate {
27
- /** True + arms the cooldown if enough time has passed since the last fire. */
28
- tryFire(now?: number): boolean
29
- }
30
-
31
- /** Debounce for event-driven reactions -- keeps a busy session from spamming the bubble. */
32
- export function createReactionGate(cooldownMs = 8000): ReactionGate {
33
- let last = -Infinity
34
- return {
35
- tryFire(now: number = Date.now()): boolean {
36
- if (now - last < cooldownMs) return false
37
- last = now
38
- return true
39
- },
40
- }
41
- }
@@ -1,30 +0,0 @@
1
- export const SPECIES = [
2
- "cat",
3
- "dog",
4
- "bunny",
5
- "owl",
6
- "bat",
7
- "penguin",
8
- "duck",
9
- "ghost",
10
- "slime",
11
- ] as const
12
- export type Species = (typeof SPECIES)[number]
13
-
14
- export type Rarity = "common" | "uncommon" | "rare" | "legendary"
15
-
16
- export interface CompanionStats {
17
- patience: number
18
- chaos: number
19
- wisdom: number
20
- snark: number
21
- }
22
-
23
- /** Persisted in TUI kv. Rolled once at hatch, then stable for the life of the install. */
24
- export interface Companion {
25
- species: Species
26
- rarity: Rarity
27
- name: string
28
- stats: CompanionStats
29
- hatchedAt: number
30
- }
package/src/config.ts DELETED
@@ -1,19 +0,0 @@
1
- import type { OverclockConfig } from "./types.ts"
2
-
3
- const CONFIG_PATHS = [".opencode/overclock.json", "overclock.json"]
4
-
5
- /** Load plugin config from project dir. Missing file -> {} (all defaults). */
6
- export async function loadConfig(directory: string): Promise<OverclockConfig> {
7
- for (const rel of CONFIG_PATHS) {
8
- const file = Bun.file(`${directory}/${rel}`)
9
- if (await file.exists()) {
10
- try {
11
- return (await file.json()) as OverclockConfig
12
- } catch (e) {
13
- console.warn(`[overclock] bad config ${rel}: ${e}`)
14
- return {}
15
- }
16
- }
17
- }
18
- return {}
19
- }
@@ -1,128 +0,0 @@
1
- import { tool, type PluginInput } from "@opencode-ai/plugin"
2
- import type { FeatureModule } from "../types.ts"
3
-
4
- const z = tool.schema
5
-
6
- type Client = PluginInput["client"]
7
-
8
- interface MessagePart {
9
- type: string
10
- text?: string
11
- }
12
-
13
- interface MessageEntry {
14
- info: {
15
- id: string
16
- role: string
17
- time?: { created?: number }
18
- }
19
- parts: MessagePart[]
20
- }
21
-
22
- const collapse = (s: string) => s.replace(/\s+/g, " ").trim()
23
-
24
- /** Exported for tests: revert/unrevert/list core, decoupled from plugin ctx. */
25
- export interface Checkpoints {
26
- list(sessionID: string): Promise<string>
27
- revert(sessionID: string, messageID: string): Promise<string>
28
- restore(sessionID: string): Promise<string>
29
- }
30
-
31
- export function createCheckpoints(client: Client): Checkpoints {
32
- async function list(sessionID: string): Promise<string> {
33
- try {
34
- const res = await client.session.messages({ path: { id: sessionID } })
35
- const msgs = ((res.data ?? []) as MessageEntry[]).filter((m) => m.info.role === "user")
36
- if (!msgs.length) return "no checkpoints"
37
- return msgs
38
- .map((m) => {
39
- const time = m.info.time?.created
40
- ? new Date(m.info.time.created).toISOString()
41
- : "unknown time"
42
- const text = m.parts.find((p) => p.type === "text" && typeof p.text === "string")?.text ?? ""
43
- const preview = collapse(text).slice(0, 60)
44
- return `${m.info.id} ${time} ${preview}`
45
- })
46
- .join("\n")
47
- } catch (e) {
48
- console.warn(`[overclock] checkpoints: list failed (session ${sessionID}): ${e}`)
49
- return `error listing checkpoints: ${e}`
50
- }
51
- }
52
-
53
- async function revert(sessionID: string, messageID: string): Promise<string> {
54
- try {
55
- await client.session.revert({ path: { id: sessionID }, body: { messageID } })
56
- return `reverted session ${sessionID} to before message ${messageID}`
57
- } catch (e) {
58
- console.warn(
59
- `[overclock] checkpoints: revert failed (session ${sessionID}, message ${messageID}): ${e}`,
60
- )
61
- return `error reverting checkpoint: ${e}`
62
- }
63
- }
64
-
65
- async function restore(sessionID: string): Promise<string> {
66
- try {
67
- await client.session.unrevert({ path: { id: sessionID } })
68
- return `restored session ${sessionID} to latest (undo revert)`
69
- } catch (e) {
70
- console.warn(`[overclock] checkpoints: restore failed (session ${sessionID}): ${e}`)
71
- return `error restoring checkpoint: ${e}`
72
- }
73
- }
74
-
75
- return { list, revert, restore }
76
- }
77
-
78
- /**
79
- * Shadow-git revert tools (map doc: "Checkpoints — none gap"). Wraps native
80
- * session.revert/unrevert around each user message as a revert point.
81
- */
82
- export const checkpoints: FeatureModule = {
83
- name: "checkpoints",
84
- tools: ["checkpoint_list", "checkpoint_revert", "checkpoint_restore"],
85
- defaultEnabled: true,
86
- requires: ["session.revert", "session.unrevert", "session.messages"],
87
- async init(ctx) {
88
- const core = createCheckpoints(ctx.client)
89
-
90
- return {
91
- tool: {
92
- checkpoint_list: tool({
93
- description:
94
- "List user messages of a session as revert points: messageID, time, first 60 chars. Most recent last.",
95
- args: { sessionID: z.string().optional().describe("default: current session") },
96
- async execute(args, tctx) {
97
- return core.list(args.sessionID ?? tctx.sessionID)
98
- },
99
- }),
100
- checkpoint_revert: tool({
101
- description:
102
- "Revert session files + conversation back to before the given message (shadow-git, reversible via checkpoint_restore). Requires user permission.",
103
- args: {
104
- messageID: z.string(),
105
- sessionID: z.string().optional().describe("default: current session"),
106
- },
107
- async execute(args, tctx) {
108
- const sessionID = args.sessionID ?? tctx.sessionID
109
- await tctx.ask({
110
- permission: "checkpoint_revert",
111
- patterns: [args.messageID],
112
- always: [],
113
- metadata: { sessionID, messageID: args.messageID },
114
- })
115
- return core.revert(sessionID, args.messageID)
116
- },
117
- }),
118
- checkpoint_restore: tool({
119
- description: "Undo the most recent checkpoint_revert for a session.",
120
- args: { sessionID: z.string().optional().describe("default: current session") },
121
- async execute(args, tctx) {
122
- return core.restore(args.sessionID ?? tctx.sessionID)
123
- },
124
- }),
125
- },
126
- }
127
- },
128
- }
@@ -1,104 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin"
2
- import type { FeatureModule } from "../types.ts"
3
- import { shellQuote } from "../lib/state.ts"
4
-
5
- const z = tool.schema
6
-
7
- export interface SandboxPolicy {
8
- project: string
9
- net: boolean
10
- }
11
-
12
- /** Pure: wrap shell cmd in bwrap. / ro, project + /tmp rw, net per policy. */
13
- export function wrapCommand(cmd: string, policy: SandboxPolicy): string {
14
- const args = [
15
- "bwrap",
16
- "--ro-bind / /",
17
- "--dev /dev",
18
- "--proc /proc",
19
- `--bind ${shellQuote(policy.project)} ${shellQuote(policy.project)}`,
20
- "--bind /tmp /tmp",
21
- "--die-with-parent",
22
- ]
23
- if (!policy.net) args.push("--unshare-net")
24
- args.push("bash -c", shellQuote(cmd))
25
- return args.join(" ")
26
- }
27
-
28
- /** Functional probe: bwrap present AND userns allowed (WSL2/distros vary). */
29
- export function probeBwrap(): boolean {
30
- try {
31
- const res = Bun.spawnSync([
32
- "bwrap",
33
- "--ro-bind",
34
- "/",
35
- "/",
36
- "--dev",
37
- "/dev",
38
- "--proc",
39
- "/proc",
40
- "true",
41
- ])
42
- return res.exitCode === 0
43
- } catch {
44
- return false
45
- }
46
- }
47
-
48
- /**
49
- * Sandboxed bash via bubblewrap. Rewrites every bash tool call.
50
- * Off by default. No bwrap -> warn once, passthrough.
51
- */
52
- export const sandbox: FeatureModule = {
53
- name: "sandbox",
54
- tools: ["bash_unsandboxed"],
55
- options: { net: "boolean" },
56
- defaultEnabled: false,
57
- async init(ctx, options) {
58
- const policy: SandboxPolicy = {
59
- project: ctx.directory,
60
- net: options.net !== false,
61
- }
62
- const available = probeBwrap()
63
- if (!available) console.warn("[overclock] sandbox: bwrap unavailable/blocked -> passthrough")
64
-
65
- return {
66
- "tool.execute.before": async (input, output) => {
67
- if (!available || input.tool !== "bash") return
68
- const cmd = (output.args as { command?: string }).command
69
- if (typeof cmd !== "string") return
70
- output.args.command = wrapCommand(cmd, policy)
71
- },
72
- tool: {
73
- bash_unsandboxed: tool({
74
- description:
75
- "Run a shell command OUTSIDE the sandbox (full FS write access). Requires user permission. Use only when the sandbox blocks a legitimate operation.",
76
- args: {
77
- command: z.string(),
78
- cwd: z.string().optional(),
79
- },
80
- async execute(args, tctx) {
81
- await tctx.ask({
82
- permission: "bash_unsandboxed",
83
- patterns: [args.command],
84
- always: [],
85
- metadata: { command: args.command },
86
- })
87
- const proc = Bun.spawn(["bash", "-c", args.command], {
88
- cwd: args.cwd ?? tctx.directory,
89
- stdout: "pipe",
90
- stderr: "pipe",
91
- })
92
- const [out, err, code] = await Promise.all([
93
- new Response(proc.stdout).text(),
94
- new Response(proc.stderr).text(),
95
- proc.exited,
96
- ])
97
- const text = (out + (err ? `\nstderr:\n${err}` : "")).slice(0, 30_000)
98
- return `exit ${code}\n${text}`
99
- },
100
- }),
101
- },
102
- }
103
- },
104
- }