@bybrawe/opencode-loop 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.2 - 2026-09-30
4
+
5
+ - Conform the native OpenCode 2 runtime to the migration-guide location/options boundary: project scope comes only from `ctx.location.directory`; plugin `ctx.options` can no longer masquerade as project location.
6
+ - Keep session/worktree isolation on public `session.get(...).location.directory` and fail setup/dispatch closed when project or session location cannot be established.
7
+ - Retain the official root/server/`./v2` `Plugin.define({ id, setup })` entrypoints, plural `plugins` installer migration, abortable event subscription cleanup, public command/session capabilities and separately isolated V1 compatibility.
8
+ - Keep undocumented `session.shell`, `session.compact` and `session.inbox` properties out of the V2 runtime; scheduled shell remains Loop-managed and automatic compaction remains host-owned.
9
+ - Add lifecycle regression coverage proving `options.directory` is not V2 location authority.
10
+ - Pair trusted publication with immutable OpenCode Goal 1.3.42 and re-run production tarballs plus the joint OpenCode 2.0.18 host matrix.
11
+
12
+ ## 0.6.1 - 2026-09-29
13
+
14
+ - Expose the root/server and `./v2` npm entrypoints through the official `@opencode/plugin` `Plugin.define({ id, setup })` contract while keeping V1 `server()` as a separate compatibility implementation.
15
+ - Ship `@opencode/plugin@^2.0.18` as a production dependency and lock its non-optional runtime peer closure for reproducible clean installs.
16
+ - Make native local-file installs use the same real V2 entrypoint: `plugins/opencode-loop/index.js` is the `Plugin.define()` wrapper and `native.js` is only its supporting generated runtime.
17
+ - Preserve configured package options while migrating `plugin` to `plugins`; remove old single-file local copies during native upgrades and uninstall.
18
+ - Extend Windows production-tarball smoke time only at the harness boundary so the larger V2 dependency graph can install without weakening runtime assertions.
19
+ - Pair trusted publication with OpenCode Goal 1.3.40 from its exact immutable source and re-run the installed npm pair on OpenCode 2.0.18.
20
+
3
21
  ## 0.6.0 - 2026-09-29
4
22
 
5
23
  - Fix #164: persisted core user-message events cannot abort compaction; trusted admission hooks alone may steer legacy turns.
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # OpenCode Loop
2
2
 
3
- ## Native OpenCode 2 (0.6.0)
3
+ ## Native OpenCode 2 (0.6.2)
4
+
5
+ The npm and local-file V2 entrypoints now use the official `@opencode/plugin` `Plugin.define()` contract. The local installer writes a discovered plugin package directory with the generated runtime as a supporting module, while explicit `--legacy-v1` remains a separate compatibility path.
4
6
 
5
7
  The installer now defaults to the standalone native V2 plugin. It needs no V1 SDK or legacy command files for a local installation. Existing npm registrations migrate from `plugin` to `plugins` without dropping object options. Use `--legacy-v1` explicitly for the compatibility installation.
6
8
 
7
9
  V2 prompt admission and durable inbox/execution/compaction events own scheduling. Core-generated user-role messages never authorize a session abort. An unfinished dedicated Goal reserves its session, including paused and handed-off states; Loop does not wake or replace that Goal. Soft iteration timeouts do not abort a native model/tool/compaction turn.
8
10
 
9
- Scheduled shell commands use bounded local child processes when OpenCode does not expose `session.shell` to plugins. Automatic compaction stays host-owned. **Scheduled compaction requires a host exposing `session.compact`; OpenCode 2.0.18's published plugin Context does not expose it, and those commands are rejected before job creation rather than silently ignored.** Use OpenCode's built-in manual/automatic compaction on that host.
11
+ Scheduled shell commands use Loop-managed bounded local child processes; the OpenCode 2 `ctx.shell` surface is a hook domain, not a command-execution API. Automatic compaction stays host-owned. **The public OpenCode 2 SessionDomain has no manual compaction action, so `/loop-compact`, `--compact`, and `--compact-every` are rejected before job creation instead of probing undocumented `session.compact` fields.** Use OpenCode's built-in manual/automatic compaction on V2.
10
12
 
11
13
  Prompt/command timers, watch and stop conditions, verification, preflight/postrun, notifications, checkpoints and diagnostic commands use the V2 runtime. Uncertain restart admissions pause for review rather than replaying potentially admitted work. The dedicated `@bybrawe/opencode-goal` plugin owns `/goal`; legacy Loop Goal records are preserved and are not silently reinterpreted as new Goal contracts.
12
14
 
@@ -15,7 +17,7 @@ Prompt/command timers, watch and stop conditions, verification, preflight/postru
15
17
 
16
18
  OpenCode Loop adds `/loop`, scheduled prompt/command/shell jobs, compact scheduling, verification/checkpoints, and the `opencode-loopd` background daemon.
17
19
 
18
- > **Current stable release: `0.5.38`.**
20
+ > **Current stable release: `0.6.2`.**
19
21
 
20
22
  ## Install or update
21
23
 
@@ -0,0 +1,60 @@
1
+ # OpenCode Loop 0.6.1
2
+
3
+ Official OpenCode 2 plugin-definition release, paired with OpenCode Goal 1.3.40.
4
+
5
+ ## V2 package and local entrypoints
6
+
7
+ The npm root/server entry and the explicit `./v2` entry now use
8
+ `@opencode/plugin` `Plugin.define({ id, setup })`. V1 `server()` compatibility
9
+ remains a separate implementation rather than an adapter that translates V1 hooks.
10
+
11
+ A native local install uses the same contract. It creates
12
+ `plugins/opencode-loop/index.js` as the real V2 entrypoint and keeps the generated
13
+ single-file runtime beside it as `native.js`, where it is a support module rather
14
+ than a second auto-discovered plugin. Upgrades remove the old root
15
+ `opencode-loop.ts`/JS copies; explicit `--legacy-v1` still installs the V1 path.
16
+
17
+ ## Runtime and packaging
18
+
19
+ `@opencode/plugin@^2.0.18` is a production dependency and its required runtime
20
+ peer closure is locked for reproducible npm installs. Clean Windows package smoke
21
+ keeps all previous assertions but permits six minutes for the larger V2 dependency
22
+ graph to install.
23
+
24
+ The native prompt, command, scheduling, session-location, process-tree, Goal
25
+ ownership, compaction-safety and `--until` control-state protections from 0.6.0
26
+ remain unchanged.
27
+
28
+ ## Paired Goal release
29
+
30
+ Goal 1.3.40 defines its server, native-only and TUI entries with the official V2
31
+ plugin APIs while preserving separate V1 compatibility. Loop publication is pinned
32
+ to Goal's immutable 1.3.40 source and refuses publication until that exact package
33
+ is visible on npm.
34
+
35
+ ## Publication and verification
36
+
37
+ The main-only OIDC publisher requires Windows/Linux installed-package gates plus
38
+ normal exact-main CI and Native V2 CI. It builds the declared Goal source, installs
39
+ the actual Loop and Goal tarballs together, and exercises them on OpenCode 2.0.18.
40
+
41
+ After publication, registry latest, gitHead, exports, bins and integrity are checked.
42
+ A fresh npm consumer then installs Loop 0.6.1 and Goal 1.3.40 and re-runs joint
43
+ installation/options/uninstall, real native commands and the two-turn
44
+ single-session daemon before the GitHub v0.6.1 tag/release is created.
45
+
46
+ ## Capability boundary
47
+
48
+ OpenCode 2.0.18's published plugin context still does not expose
49
+ `session.compact`; scheduled `/loop-compact` therefore rejects before durable job
50
+ creation while automatic compaction stays host-owned. Controlled-provider canaries
51
+ are functional verification, not live-model cost or endurance measurements.
52
+
53
+ Install the verified pair after registry publication:
54
+
55
+ ```sh
56
+ npx -y @bybrawe/opencode-goal@1.3.40
57
+ npx -y @bybrawe/opencode-loop@0.6.1 --loop-only --without-loop-goals
58
+ ```
59
+
60
+ Fully restart OpenCode after installation. Existing project state is preserved.
@@ -0,0 +1,54 @@
1
+ # OpenCode Loop 0.6.2
2
+
3
+ OpenCode 2 migration-guide conformance release, paired with OpenCode Goal 1.3.42.
4
+
5
+ ## Public V2 plugin context
6
+
7
+ The native runtime treats `ctx.location.directory` as the only plugin-instance
8
+ project location. `ctx.options` remains plugin configuration and is not accepted as
9
+ a project-directory fallback.
10
+
11
+ Per-session scope is verified through public `session.get` location data, so a
12
+ session from another worktree/project cannot borrow this plugin instance's state.
13
+
14
+ ## V2 plugin and installation contract
15
+
16
+ The npm root/server and explicit `./v2` entries use
17
+ `@opencode/plugin` `Plugin.define({ id, setup })`. Native installs use plural
18
+ `plugins` configuration and the V2 plugins layout. Explicit `--legacy-v1` remains
19
+ a separate compatibility path; V1 hooks and SDK request shapes are not the default
20
+ V2 runtime.
21
+
22
+ Native prompt/compaction hooks, command transforms and the event subscription are
23
+ registered through V2 domains and disposed on unload. The event stream is cancelled
24
+ with an abort signal before local resources and registrations are drained.
25
+
26
+ ## Capability boundary
27
+
28
+ Loop does not probe undocumented `session.shell`, `session.compact` or
29
+ `session.inbox` properties. Scheduled shell execution is a bounded Loop-managed
30
+ local process. Manual scheduled compaction remains unsupported on the OpenCode
31
+ 2.0.18 public plugin contract and is rejected before durable job creation; automatic
32
+ compaction remains host-owned.
33
+
34
+ ## Paired Goal release
35
+
36
+ Publication is pinned to OpenCode Goal 1.3.42 from immutable source
37
+ `1dd58abf1e7c08594c2085883e28acea3dbe0418` and refuses to continue until that
38
+ exact package is visible on npm.
39
+
40
+ ## Verification
41
+
42
+ The publisher requires Windows/Linux native regressions, exact-main CI and Native V2
43
+ CI, production tarball installation, joint Goal+Loop installation/options/uninstall,
44
+ real native commands and the two-turn single-session daemon on OpenCode 2.0.18.
45
+
46
+ Install the verified pair after registry publication:
47
+
48
+ ```sh
49
+ npx -y @bybrawe/opencode-goal@1.3.42
50
+ npx -y @bybrawe/opencode-loop@0.6.2 --loop-only --without-loop-goals
51
+ ```
52
+
53
+ Fully restart OpenCode after installation. Existing project Goal/Loop state and
54
+ user-owned files are preserved.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@bybrawe/opencode-loop",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Claude Code/Codex style /loop and experimental goal mode for OpenCode: heartbeat scheduler, idle-safe loops, scheduled commands, compact scheduling, verification, checkpoints, and persistent coding goals.",
5
5
  "type": "module",
6
- "main": "src/server.js",
6
+ "main": "src/plugin.js",
7
7
  "exports": {
8
- ".": "./src/server.js",
9
- "./server": "./src/server.js",
10
- "./v2": "./src/native.js",
8
+ ".": "./src/plugin.js",
9
+ "./server": "./src/plugin.js",
10
+ "./v2": "./src/v2.js",
11
11
  "./v1": "./src/index.js"
12
12
  },
13
13
  "files": [
@@ -23,8 +23,8 @@
23
23
  "scripts": {
24
24
  "build:plugin": "bun build src/source/v1.js --outfile=src/index.js --target=bun --format=esm --external=@opencode-ai/plugin/tool && bun build src/source/server.js --outfile=src/server.js --target=bun --format=esm --external=@opencode-ai/plugin/tool && bun build src/source/native.js --outfile=src/native.js --target=bun --format=esm",
25
25
  "build:plugin:npm": "npm run build:plugin",
26
- "prepack": "node --check src/index.js && node --check src/server.js && node --check src/native.js",
27
- "check": "node --check src/source/v1.js && node --check src/source/core/args.js && node --check src/source/core/continuation.js && node --check src/source/core/schedule-syntax.js && node --check src/source/core/state.js && node --check src/source/core/jobs.js && node --check src/source/core/process.js && node --check src/source/opencode/sdk.js && node --check src/source/opencode/session-context.js && node --check src/source/opencode/command-router.js && node --check src/source/opencode/goal-commands.js && node --check src/source/opencode/loop-commands.js && node --check src/source/opencode/loop-registration.js && node --check src/source/runtime/session-activity.js && node --check src/source/runtime/session-status.js && node --check src/source/runtime/schedule-policy.js && node --check src/source/runtime/scheduler-diagnostics.js && node --check src/source/runtime/companion-goal.js && node --check src/source/runtime/loop-diagnostics.js && node --check src/source/runtime/compaction.js && node --check src/source/runtime/action-dispatch.js && node --check src/source/runtime/network-recovery.js && node --check src/source/runtime/empty-turn.js && node --check src/source/runtime/terminal-guard.js && node --check src/source/runtime/run-finalization.js && node --check src/source/runtime/run-admission.js && node --check src/source/runtime/executor.js && node --check src/source/runtime/scheduler.js && node --check src/source/runtime/goal-prompt.js && node --check src/source/runtime/goal-report.js && node --check src/source/runtime/goal-evidence.js && node --check src/source/runtime/goal-runtime.js && node --check src/source/runtime/goal-policy.js && node --check src/source/runtime/goal-steering.js && node --check src/source/runtime/job-workspace.js && node --check src/source/opencode2/prompt-runtime.js && node --check src/source/opencode2/diagnostics.js && node --check src/source/opencode2/logging.js && node --check src/source/server.js && node --check src/source/legacy-v1.js && node --check src/index.js && node --check src/server.js && node --check scripts/install-node.mjs && node --check scripts/install-with-goals.mjs && node --check scripts/loopd.mjs && node --check scripts/pulse-check.mjs && node --check scripts/install-test.mjs && node --check scripts/goal-companion-test.mjs && node --check scripts/loopd-test.mjs && node --check scripts/smoke-test.mjs && node --check scripts/host-adapter-contract-test.mjs && node --check scripts/server-entry-test.mjs && node --check scripts/command-router-test.mjs && node --check scripts/goal-command-handlers-test.mjs && node --check scripts/loop-command-handlers-test.mjs && node --check scripts/loop-registration-test.mjs && node --check scripts/schedule-syntax-test.mjs && node --check scripts/schedule-policy-test.mjs && node --check scripts/session-activity-test.mjs && node --check scripts/session-status-test.mjs && node --check scripts/session-status-idle-recovery-test.mjs && node --check scripts/session-error-recovery-test.mjs && node --check scripts/empty-turn-recovery-test.mjs && node --check scripts/network-recovery-test.mjs && node --check scripts/compaction-runtime-test.mjs && node --check scripts/executor-runtime-test.mjs && node --check scripts/scheduler-runtime-test.mjs && node --check scripts/goal-runtime-test.mjs && node --check scripts/goal-policy-test.mjs && node --check scripts/goal-steering-test.mjs && node --check scripts/job-workspace-test.mjs && node --check scripts/v2-prompt-runtime-test.mjs && node --check scripts/v2-prompt-interval-test.mjs && node --check scripts/v2-command-runtime-test.mjs && node --check scripts/v2-command-adapter-test.mjs && node --check scripts/v2-diagnostics-test.mjs && node --check scripts/v2-logging-test.mjs && node --check scripts/comprehensive-watchdog.mjs && node --check scripts/comprehensive-test.mjs && node --check scripts/host-loop-canary.mjs && node --check scripts/host-goal-steering-canary.mjs && node --check scripts/publish-workflow-test.mjs && node --check scripts/goal-compaction-safety-test.mjs && node --check src/source/native.js && node --check src/source/opencode2/native-plugin.js && node --check src/source/opencode2/native-runtime.js && node --check src/source/opencode2/native-policy.js && node --check src/native.js && node --check scripts/native-entry-test.mjs && node --check scripts/package-native-smoke.mjs && node --check src/v1.js && node --check scripts/native-cli-io-test.mjs && node --check scripts/native-lifecycle-test.mjs && node --check scripts/native-plugin-lifetime-test.mjs && node --check src/source/core/progress.js && node --check scripts/native-presets-test.mjs && node --check scripts/control-plane-until-test.mjs && node --check src/source/opencode2/session-location.js && node --check scripts/native-session-location-test.mjs",
26
+ "prepack": "node --check src/plugin.js && node --check src/v2.js && node --check src/index.js && node --check src/server.js && node --check src/native.js",
27
+ "check": "node --check src/source/v1.js && node --check src/source/core/args.js && node --check src/source/core/continuation.js && node --check src/source/core/schedule-syntax.js && node --check src/source/core/state.js && node --check src/source/core/jobs.js && node --check src/source/core/process.js && node --check src/source/opencode/sdk.js && node --check src/source/opencode/session-context.js && node --check src/source/opencode/command-router.js && node --check src/source/opencode/goal-commands.js && node --check src/source/opencode/loop-commands.js && node --check src/source/opencode/loop-registration.js && node --check src/source/runtime/session-activity.js && node --check src/source/runtime/session-status.js && node --check src/source/runtime/schedule-policy.js && node --check src/source/runtime/scheduler-diagnostics.js && node --check src/source/runtime/companion-goal.js && node --check src/source/runtime/loop-diagnostics.js && node --check src/source/runtime/compaction.js && node --check src/source/runtime/action-dispatch.js && node --check src/source/runtime/network-recovery.js && node --check src/source/runtime/empty-turn.js && node --check src/source/runtime/terminal-guard.js && node --check src/source/runtime/run-finalization.js && node --check src/source/runtime/run-admission.js && node --check src/source/runtime/executor.js && node --check src/source/runtime/scheduler.js && node --check src/source/runtime/goal-prompt.js && node --check src/source/runtime/goal-report.js && node --check src/source/runtime/goal-evidence.js && node --check src/source/runtime/goal-runtime.js && node --check src/source/runtime/goal-policy.js && node --check src/source/runtime/goal-steering.js && node --check src/source/runtime/job-workspace.js && node --check src/source/opencode2/prompt-runtime.js && node --check src/source/opencode2/diagnostics.js && node --check src/source/opencode2/logging.js && node --check src/source/server.js && node --check src/source/legacy-v1.js && node --check src/index.js && node --check src/server.js && node --check scripts/install-node.mjs && node --check scripts/install-with-goals.mjs && node --check scripts/loopd.mjs && node --check scripts/pulse-check.mjs && node --check scripts/install-test.mjs && node --check scripts/goal-companion-test.mjs && node --check scripts/loopd-test.mjs && node --check scripts/smoke-test.mjs && node --check scripts/host-adapter-contract-test.mjs && node --check scripts/server-entry-test.mjs && node --check scripts/command-router-test.mjs && node --check scripts/goal-command-handlers-test.mjs && node --check scripts/loop-command-handlers-test.mjs && node --check scripts/loop-registration-test.mjs && node --check scripts/schedule-syntax-test.mjs && node --check scripts/schedule-policy-test.mjs && node --check scripts/session-activity-test.mjs && node --check scripts/session-status-test.mjs && node --check scripts/session-status-idle-recovery-test.mjs && node --check scripts/session-error-recovery-test.mjs && node --check scripts/empty-turn-recovery-test.mjs && node --check scripts/network-recovery-test.mjs && node --check scripts/compaction-runtime-test.mjs && node --check scripts/executor-runtime-test.mjs && node --check scripts/scheduler-runtime-test.mjs && node --check scripts/goal-runtime-test.mjs && node --check scripts/goal-policy-test.mjs && node --check scripts/goal-steering-test.mjs && node --check scripts/job-workspace-test.mjs && node --check scripts/v2-prompt-runtime-test.mjs && node --check scripts/v2-prompt-interval-test.mjs && node --check scripts/v2-command-runtime-test.mjs && node --check scripts/v2-command-adapter-test.mjs && node --check scripts/v2-diagnostics-test.mjs && node --check scripts/v2-logging-test.mjs && node --check scripts/comprehensive-watchdog.mjs && node --check scripts/comprehensive-test.mjs && node --check scripts/host-loop-canary.mjs && node --check scripts/host-goal-steering-canary.mjs && node --check scripts/publish-workflow-test.mjs && node --check scripts/goal-compaction-safety-test.mjs && node --check src/source/native.js && node --check src/source/opencode2/native-plugin.js && node --check src/source/opencode2/native-runtime.js && node --check src/source/opencode2/native-policy.js && node --check src/native.js && node --check scripts/native-entry-test.mjs && node --check scripts/package-native-smoke.mjs && node --check src/v1.js && node --check scripts/native-cli-io-test.mjs && node --check scripts/native-lifecycle-test.mjs && node --check scripts/native-plugin-lifetime-test.mjs && node --check src/source/core/progress.js && node --check scripts/native-presets-test.mjs && node --check scripts/control-plane-until-test.mjs && node --check src/source/opencode2/session-location.js && node --check scripts/native-session-location-test.mjs && node --check src/plugin.js && node --check src/v2.js",
28
28
  "test": "node --test scripts/native-session-location-test.mjs && node --test scripts/control-plane-until-test.mjs && node --test scripts/native-presets-test.mjs && node --test scripts/native-plugin-lifetime-test.mjs && node --test scripts/native-cli-io-test.mjs scripts/native-lifecycle-test.mjs && node scripts/native-entry-test.mjs && node scripts/native-coexistence-test.mjs && node scripts/native-shell-test.mjs && node scripts/native-install-test.mjs && node scripts/native-v2-test.mjs && node scripts/goal-compaction-safety-test.mjs && node scripts/publish-workflow-test.mjs && node scripts/server-entry-test.mjs && node scripts/command-router-test.mjs && node scripts/goal-command-handlers-test.mjs && node scripts/loop-command-handlers-test.mjs && node scripts/loop-registration-test.mjs && node scripts/schedule-syntax-test.mjs && node scripts/schedule-policy-test.mjs && node scripts/session-activity-test.mjs && node scripts/session-status-test.mjs && node scripts/session-status-idle-recovery-test.mjs && node scripts/session-error-recovery-test.mjs && node scripts/empty-turn-recovery-test.mjs && node scripts/network-recovery-test.mjs && node scripts/compaction-runtime-test.mjs && node scripts/action-dispatch-test.mjs && node scripts/run-finalization-test.mjs && node scripts/run-admission-test.mjs && node scripts/executor-runtime-test.mjs && node scripts/scheduler-runtime-test.mjs && node scripts/goal-runtime-test.mjs && node scripts/goal-policy-test.mjs && node scripts/goal-steering-test.mjs && node scripts/job-workspace-test.mjs && node scripts/v2-prompt-runtime-test.mjs && node scripts/v2-prompt-interval-test.mjs && node scripts/v2-command-runtime-test.mjs && node scripts/v2-command-adapter-test.mjs && node scripts/v2-diagnostics-test.mjs && node scripts/v2-logging-test.mjs && node scripts/install-test.mjs && node scripts/goal-companion-test.mjs && node scripts/loopd-test.mjs && node scripts/smoke-test.mjs && node scripts/host-adapter-contract-test.mjs && node scripts/comprehensive-watchdog.mjs",
29
29
  "canary:host": "node scripts/host-loop-canary.mjs && node scripts/host-goal-steering-canary.mjs",
30
30
  "install:global": "node scripts/install-with-goals.mjs",
@@ -73,5 +73,8 @@
73
73
  },
74
74
  "publishConfig": {
75
75
  "access": "public"
76
+ },
77
+ "dependencies": {
78
+ "@opencode/plugin": "^2.0.18"
76
79
  }
77
80
  }
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url"
8
8
  const root = dirname(dirname(fileURLToPath(import.meta.url)))
9
9
  const config = process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode")
10
10
  const pluginDir = join(config, "plugins")
11
+ const nativePluginDir = join(pluginDir, "opencode-loop")
11
12
  const commandDir = join(config, "commands")
12
13
  const agentDir = join(config, "agents")
13
14
  const packagePath = join(config, "package.json")
@@ -343,6 +344,7 @@ async function uninstall() {
343
344
  for (const plan of plans) if (plan.updated !== plan.source) await writeFile(plan.target, plan.updated, "utf8")
344
345
  await rm(join(pluginDir, "opencode-loop.ts"), { force: true })
345
346
  await rm(join(pluginDir, "opencode-loop.js"), { force: true })
347
+ await rm(nativePluginDir, { recursive: true, force: true })
346
348
  await removePackagedFiles(join(root, "commands"), commandDir)
347
349
  await removePackagedFiles(join(root, "agents"), agentDir)
348
350
  const changed = plans.filter((plan) => plan.source !== plan.updated).length
@@ -358,10 +360,21 @@ async function installOrUpdate() {
358
360
  if (packageConfig.configured) {
359
361
  await rm(join(pluginDir, "opencode-loop.ts"), { force: true })
360
362
  await rm(join(pluginDir, "opencode-loop.js"), { force: true })
363
+ await rm(nativePluginDir, { recursive: true, force: true })
364
+ } else if (legacyV1) {
365
+ await ensureDependency()
366
+ await rm(nativePluginDir, { recursive: true, force: true })
367
+ await copyFile(join(root, "src", "index.js"), join(pluginDir, "opencode-loop.ts"))
368
+ await rm(join(pluginDir, "opencode-loop.js"), { force: true })
361
369
  } else {
362
- if (legacyV1) await ensureDependency()
363
- await copyFile(join(root, "src", legacyV1 ? "index.js" : "native.js"), join(pluginDir, "opencode-loop.ts"))
370
+ // V2 local plugins use the same Plugin.define entrypoint as the npm package.
371
+ // Keep the generated native runtime beside it as a supporting module instead
372
+ // of auto-discovering that implementation object as a second plugin.
373
+ await rm(join(pluginDir, "opencode-loop.ts"), { force: true })
364
374
  await rm(join(pluginDir, "opencode-loop.js"), { force: true })
375
+ await mkdir(nativePluginDir, { recursive: true })
376
+ await copyFile(join(root, "src", "v2.js"), join(nativePluginDir, "index.js"))
377
+ await copyFile(join(root, "src", "native.js"), join(nativePluginDir, "native.js"))
365
378
  }
366
379
  if (legacyV1) {
367
380
  await mkdir(commandDir, { recursive: true })
@@ -375,7 +388,7 @@ async function installOrUpdate() {
375
388
  if (packageConfig.configured) {
376
389
  const pinResult = packageConfig.updatedFiles.length ? `pinned the config entry to ${packageSpec}` : `the config entry is already pinned to ${packageSpec}`
377
390
  console.log(`OpenCode Loop is already configured as a package in ${config}; ${pinResult} and removed the duplicate local plugin copy.`)
378
- } else console.log(`Installed OpenCode Loop plugin to ${config}`)
391
+ } else console.log(`Installed OpenCode Loop plugin to ${legacyV1 ? join(pluginDir, "opencode-loop.ts") : nativePluginDir}`)
379
392
  if (legacyV1) {
380
393
  console.log(`Installed ${packageName} commands to ${commandDir}`)
381
394
  console.log(`Installed ${packageName} local command agent to ${agentDir}`)
@@ -27,8 +27,13 @@ try {
27
27
  const fresh = await config("fresh")
28
28
  run(fresh)
29
29
  const native = await readFile(path.join(root, "src/native.js"), "utf8")
30
- assert.equal(await readFile(path.join(fresh, "plugins/opencode-loop.ts"), "utf8"), native)
30
+ const v2 = await readFile(path.join(root, "src/v2.js"), "utf8")
31
+ assert.equal(await readFile(path.join(fresh, "plugins/opencode-loop/index.js"), "utf8"), v2)
32
+ assert.equal(await readFile(path.join(fresh, "plugins/opencode-loop/native.js"), "utf8"), native)
33
+ assert.match(v2, /from\s+["']@opencode\/plugin["']/)
34
+ assert.match(v2, /Plugin\.define\s*\(/)
31
35
  assert.doesNotMatch(native, /(?:from\s*|import\s*\()["']@opencode-ai\/plugin/)
36
+ assert.equal(await exists(path.join(fresh, "plugins/opencode-loop.ts")), false)
32
37
  assert.equal(await exists(path.join(fresh, "package.json")), false)
33
38
  assert.equal(await exists(path.join(fresh, "commands/loop.md")), false)
34
39
  assert.equal(await exists(path.join(fresh, "agents/opencode-loop-local.md")), false)
@@ -43,7 +48,7 @@ try {
43
48
  const before = await readFile(path.join(obj, "opencode.json"), "utf8")
44
49
  run(obj)
45
50
  assert.equal(await readFile(path.join(obj, "opencode.json"), "utf8"), before)
46
- assert.equal(await exists(path.join(obj, "plugins/opencode-loop.ts")), false)
51
+ assert.equal(await exists(path.join(obj, "plugins/opencode-loop")), false)
47
52
  cases++
48
53
 
49
54
  const both = await config("both", { plugin: ["legacy-unrelated", "@bybrawe/opencode-loop@old"], plugins: [options, "native-unrelated"] })
@@ -83,7 +88,9 @@ try {
83
88
  run(upgrade)
84
89
  assert.equal(await exists(path.join(upgrade, "commands/loop.md")), false)
85
90
  assert.equal(await readFile(path.join(upgrade, "commands/loop-help.md"), "utf8"), "This is a custom user command; keep it.")
86
- assert.equal(await readFile(path.join(upgrade, "plugins/opencode-loop.ts"), "utf8"), native)
91
+ assert.equal(await readFile(path.join(upgrade, "plugins/opencode-loop/index.js"), "utf8"), v2)
92
+ assert.equal(await readFile(path.join(upgrade, "plugins/opencode-loop/native.js"), "utf8"), native)
93
+ assert.equal(await exists(path.join(upgrade, "plugins/opencode-loop.ts")), false)
87
94
  cases++
88
95
 
89
96
  await mkdir(path.join(obj, ".opencode/opencode-loop"), { recursive: true })
@@ -19,6 +19,7 @@ async function fixture({ failDispose = false, failSubscribe = false } = {}) {
19
19
  const ctx = {
20
20
  location: { directory }, options: {},
21
21
  session: {
22
+ get: async ({ sessionID }) => ({ id: sessionID, location: { directory } }),
22
23
  prompt: async () => { throw new Error("cleanup must not prompt or abort a native session") },
23
24
  hook: async name => registration(name),
24
25
  },
@@ -46,6 +47,18 @@ async function bounded(promise) {
46
47
  finally { clearTimeout(timer) }
47
48
  }
48
49
 
50
+ test("native setup never treats plugin options.directory as location authority", async () => {
51
+ const host = await fixture()
52
+ try {
53
+ host.ctx.location = {}
54
+ host.ctx.options.directory = "/not-a-v2-location"
55
+ await assert.rejects(
56
+ plugin.setup(host.ctx),
57
+ /requires a project directory/i,
58
+ )
59
+ } finally { host.release(); await host.remove() }
60
+ })
61
+
49
62
  test("native unload aborts a pending public event stream and shares cleanup", async () => {
50
63
  const host = await fixture()
51
64
  let dispose
@@ -17,6 +17,7 @@ function fixture(directory, { failCleanup = false } = {}) {
17
17
  const ctx = {
18
18
  location: { directory }, options: {}, app: { version: "2.0.18" },
19
19
  session: {
20
+ get: async ({ sessionID }) => ({ id: sessionID, location: { directory } }),
20
21
  prompt: async () => { throw new Error("cleanup must not dispatch a prompt") },
21
22
  hook: async (name) => registration(name),
22
23
  },
@@ -47,6 +48,18 @@ async function bounded(promise) {
47
48
  } finally { clearTimeout(timer) }
48
49
  }
49
50
 
51
+ test("native Loop fails setup when the public session.get location capability is unavailable", async () => {
52
+ const directory = await mkdtemp(path.join(os.tmpdir(), "loop-v2-no-session-get-"))
53
+ const host = fixture(directory)
54
+ delete host.ctx.session.get
55
+ try {
56
+ await assert.rejects(plugin.setup(host.ctx), /requires session\.get/)
57
+ } finally {
58
+ host.end()
59
+ await rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
60
+ }
61
+ })
62
+
50
63
  test("native Loop aborts an idle subscription before awaiting iterator return", async () => {
51
64
  const directory = await mkdtemp(path.join(os.tmpdir(), "loop-v2-lifetime-"))
52
65
  const host = fixture(directory)
@@ -87,6 +100,26 @@ test("native Loop cleanup is shared and drains all registrations after an error"
87
100
  }
88
101
  })
89
102
 
103
+ test("native Loop never probes undocumented SessionDomain shell compact or inbox fields", async () => {
104
+ const directory = await mkdtemp(path.join(os.tmpdir(), "loop-v2-public-session-"))
105
+ const host = fixture(directory)
106
+ let dispose
107
+ try {
108
+ for (const field of ["shell", "compact", "inbox"]) {
109
+ Object.defineProperty(host.ctx.session, field, {
110
+ configurable: true,
111
+ get() { throw new Error(`undocumented session.${field} was accessed`) },
112
+ })
113
+ }
114
+ dispose = await plugin.setup(host.ctx)
115
+ assert.equal(typeof dispose, "function")
116
+ } finally {
117
+ host.end()
118
+ await dispose?.().catch(() => {})
119
+ await rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
120
+ }
121
+ })
122
+
90
123
  test("native Loop uses the same public plugin identity as the package entry", () => {
91
124
  assert.equal(plugin.id, "@bybrawe/opencode-loop", "native entry must share canonical plugin identity")
92
125
  })
@@ -9,9 +9,9 @@ import { presetDefaults } from "../src/source/core/jobs.js"
9
9
  import { normalizeLoopScheduleArgs } from "../src/source/core/schedule-syntax.js"
10
10
  import { readState } from "../src/source/core/state.js"
11
11
 
12
- const PRESETS = ["loop-dev", "loop-testfix", "loop-compact", "loop-progress", "loop-safe-dev", "loop-command", "loop-cmd", "loop-prompt", "loop-ask", "loop-shell"]
12
+ const PRESETS = ["loop-dev", "loop-testfix", "loop-progress", "loop-safe-dev", "loop-command", "loop-cmd", "loop-prompt", "loop-ask", "loop-shell"]
13
13
  const SESSION = "ses_native_presets"
14
- async function fixture({ compact = true } = {}) {
14
+ async function fixture() {
15
15
  const temp = await mkdtemp(path.join(os.tmpdir(), "loop-native-presets-"))
16
16
  const directory = path.join(temp, "project with spaces")
17
17
  await mkdir(directory)
@@ -27,7 +27,6 @@ async function fixture({ compact = true } = {}) {
27
27
  command: async () => { throw new Error("a deferred/dry-run preset must not execute a slash command") },
28
28
  wait: async () => {},
29
29
  shell: async request => { shellCalls.push(request); return { id: request.id, status: "exited", exit: 0 } },
30
- ...(compact ? { compact: async () => { throw new Error("a deferred/dry-run preset must not compact") } } : {}),
31
30
  },
32
31
  command: { transform: async edit => edit({ add(definition) {
33
32
  assert.equal(commands.has(definition.name), false, `duplicate command ${definition.name}`)
@@ -54,7 +53,7 @@ for (const name of PRESETS) {
54
53
  const action = ["loop-command", "loop-cmd"].includes(name) ? "/review project"
55
54
  : name === "loop-shell" ? "npm test"
56
55
  : ["loop-prompt", "loop-ask"].includes(name) ? "did you run tests and tsc --noEmit?" : ""
57
- const text = name === "loop-compact" ? "--dry-run" : `every 1h --dry-run --max-runs 2 ${action}`.trim()
56
+ const text = `every 1h --dry-run --max-runs 2 ${action}`.trim()
58
57
  const normalized = normalizeLoopScheduleArgs(text, presetDefaults(name))
59
58
  assert.equal(normalized.ok, true, normalized.error)
60
59
  const expected = parseLoopArgs(normalized.args, normalized.defaults)
@@ -64,7 +63,7 @@ for (const name of PRESETS) {
64
63
  const state = await host.state()
65
64
  assert.equal(state.jobs.length, 1)
66
65
  for (const field of fields) assert.deepEqual(state.jobs[0][field], expected.job[field], `${name}: ${field}`)
67
- assert.equal(state.jobs[0].intervalMs, name === "loop-compact" ? 12000000 : 3600000, "explicit duration cannot remain part of the task text")
66
+ assert.equal(state.jobs[0].intervalMs, 3600000, "explicit duration cannot remain part of the task text")
68
67
  assert.equal(state.jobs[0].immediate, false)
69
68
  assert.equal(state.jobs[0].scheduleMode, normalized.scheduleMode)
70
69
  assert.equal(state.jobs[0].scheduleSyntax, normalized.scheduleSyntax)
@@ -98,21 +97,28 @@ test("an immediate native development preset enters the existing dry-run safety
98
97
  } finally { await host.dispose() }
99
98
  })
100
99
 
101
- test("zero-delay deferred shell preset is scheduled without an unrelated model turn", async () => {
100
+ test("zero-delay deferred shell preset uses Loop-managed local execution without an unrelated model turn", async () => {
102
101
  const host = await fixture()
103
102
  try {
104
- await host.execute("loop-shell", "0s echo safe --max-runs 1")
105
- const deadline = Date.now() + 3000
106
- while (Date.now() < deadline && (await host.state()).jobs[0]?.v2Run) await new Promise(resolve => setTimeout(resolve, 10))
103
+ const marker = path.join(host.directory, "shell-ran.txt")
104
+ await writeFile(path.join(host.directory, "shell-test.cjs"), 'require("fs").writeFileSync("shell-ran.txt", "ok")')
105
+ const command = `"${process.execPath}" shell-test.cjs`
106
+ await host.execute("loop-shell", `0s ${command} --max-runs 1`)
107
+ const deadline = Date.now() + 5000
107
108
  while (Date.now() < deadline && (await host.state()).jobs[0]?.runCount !== 1) await new Promise(resolve => setTimeout(resolve, 10))
109
+ while (Date.now() < deadline) {
110
+ try { if ((await readFile(marker, "utf8")) === "ok") break } catch {}
111
+ await new Promise(resolve => setTimeout(resolve, 10))
112
+ }
108
113
  assert.equal((await host.state()).jobs[0].runCount, 1)
109
- assert.equal(host.shellCalls.length, 1)
114
+ assert.equal(await readFile(marker, "utf8"), "ok")
115
+ assert.equal(host.shellCalls.length, 0, "V2 must not call undocumented session.shell")
110
116
  assert.deepEqual(host.prompts, [])
111
117
  } finally { await host.dispose() }
112
118
  })
113
119
 
114
- test("unavailable native compaction rejects its preset before job persistence", async () => {
115
- const host = await fixture({ compact: false })
120
+ test("native compaction rejects its preset before job persistence because V2 exposes no manual compaction action", async () => {
121
+ const host = await fixture()
116
122
  try {
117
123
  await assert.rejects(host.execute("loop-compact", "--dry-run"), /native compaction capability/)
118
124
  assert.deepEqual((await host.state()).jobs, [])
@@ -7,12 +7,12 @@ export function validateRelease(request, pkg, env) {
7
7
  assert.equal(env.GITHUB_REPOSITORY, "ByBrawe/opencode-loop", "wrong release repository")
8
8
  assert.equal(env.GITHUB_REF, "refs/heads/main", "npm release must come from main")
9
9
  assert.match(env.GITHUB_SHA ?? "", /^[a-f0-9]{40}$/, "immutable release SHA required")
10
- assert.equal(request?.version, "0.6.0", "release version is not authorized")
10
+ assert.equal(request?.version, "0.6.2", "release version is not authorized")
11
11
  assert.equal(request?.name, "@bybrawe/opencode-loop")
12
12
  assert.equal(pkg.name, request.name)
13
13
  assert.equal(pkg.version, request.version)
14
- assert.equal(request.predecessor, "0.5.38")
15
- assert.equal(request.goal?.version, "1.3.39")
14
+ assert.equal(request.predecessor, "0.6.1")
15
+ assert.equal(request.goal?.version, "1.3.42")
16
16
  assert.match(request.goal?.sha ?? "", /^[a-f0-9]{40}$/)
17
17
  return request
18
18
  }
@@ -183,6 +183,16 @@ await test("preflight failure does not consume a run", async (f) => {
183
183
  assert.equal(f.prompts.length, 0)
184
184
  }, { runShellCommand: async () => ({ code: 3, stdout: "", stderr: "not ready" }) })
185
185
 
186
+ await test("native slash command dispatch uses OpenCode 2 command and arguments fields", async (f) => {
187
+ await f.add("0s --command /review --staged --max-runs 1")
188
+ await f.wake()
189
+ assert.equal(f.commands.length, 1)
190
+ assert.equal(f.commands[0].command, "review")
191
+ assert.equal(f.commands[0].arguments, "--staged")
192
+ assert.equal("name" in f.commands[0], false)
193
+ assert.equal("text" in f.commands[0], false)
194
+ })
195
+
186
196
  await test("safe guard covers verification commands too", async (f) => {
187
197
  await f.add('0s task --safe --verify "git reset --hard" --max-runs 1')
188
198
  await f.wake()
@@ -9,7 +9,7 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
9
9
  const npmCLI = process.env.npm_execpath
10
10
  assert.ok(npmCLI, "run this through npm run package:smoke")
11
11
  function run(command, args, cwd) {
12
- const result = spawnSync(command, args, { cwd, encoding: "utf8", timeout: 180_000, maxBuffer: 8 * 1024 * 1024, windowsHide: true })
12
+ const result = spawnSync(command, args, { cwd, encoding: "utf8", timeout: 360_000, maxBuffer: 8 * 1024 * 1024, windowsHide: true })
13
13
  if (result.error) throw result.error
14
14
  assert.equal(result.status, 0, [result.stdout, result.stderr].filter(Boolean).join("\n"))
15
15
  return result.stdout
@@ -19,7 +19,7 @@ try {
19
19
  const packed = JSON.parse(run(process.execPath, [npmCLI, "pack", root, "--json", "--ignore-scripts"], temp))
20
20
  assert.equal(packed.length, 1)
21
21
  const files = new Set(packed[0].files.map((entry) => entry.path))
22
- for (const entry of ["src/server.js", "src/native.js", "src/v1.js", "src/index.js"]) assert.ok(files.has(entry), `missing ${entry}`)
22
+ for (const entry of ["src/plugin.js", "src/v2.js", "src/server.js", "src/native.js", "src/v1.js", "src/index.js"]) assert.ok(files.has(entry), `missing ${entry}`)
23
23
  const consumer = path.join(temp, "consumer with spaces")
24
24
  await mkdir(consumer)
25
25
  await writeFile(path.join(consumer, "package.json"), JSON.stringify({ private: true, type: "module" }))
@@ -41,15 +41,15 @@ const publisher = workflow.slice(workflow.indexOf(" publish:\n"), workflow.inde
41
41
  assert.doesNotMatch(publisher, /contents:\s*write/, "OIDC publisher must not gain repository mutation privileges")
42
42
 
43
43
  const sha = "a".repeat(40)
44
- const request = { name: "@bybrawe/opencode-loop", version: "0.6.0", predecessor: "0.5.38", goal: { version: "1.3.39", sha: "b".repeat(40) } }
44
+ const request = { name: "@bybrawe/opencode-loop", version: "0.6.2", predecessor: "0.6.1", goal: { version: "1.3.42", sha: "b".repeat(40) } }
45
45
  const pkg = { name: request.name, version: request.version }
46
46
  const env = { GITHUB_REPOSITORY: "ByBrawe/opencode-loop", GITHUB_REF: "refs/heads/main", GITHUB_SHA: sha }
47
47
  assert.strictEqual(validateRelease(request, pkg, env), request)
48
48
  for (const change of [{ GITHUB_REPOSITORY: "someone/fork" }, { GITHUB_REF: "refs/heads/release" }, { GITHUB_SHA: "main" }]) assert.throws(() => validateRelease(request, pkg, { ...env, ...change }))
49
49
  assert.throws(() => validateRelease({ ...request, version: "9.0.0" }, pkg, env))
50
- assert.throws(() => validateRelease(request, { ...pkg, version: "0.5.38" }, env))
51
- assert.equal(releaseDecision(404, null, request, sha, "0.5.38"), true)
52
- for (const status of [401, 403, 429, 500, 503]) assert.throws(() => releaseDecision(status, null, request, sha, "0.5.38"))
50
+ assert.throws(() => validateRelease(request, { ...pkg, version: "0.6.1" }, env))
51
+ assert.equal(releaseDecision(404, null, request, sha, "0.6.1"), true)
52
+ for (const status of [401, 403, 429, 500, 503]) assert.throws(() => releaseDecision(status, null, request, sha, "0.6.0"))
53
53
  assert.throws(() => releaseDecision(404, null, request, sha, "0.7.0"))
54
54
  const manifest = { ...pkg, gitHead: sha }
55
55
  assert.equal(releaseDecision(200, manifest, request, sha), false)
package/src/native.js CHANGED
@@ -1862,7 +1862,7 @@ ${await policy.buildPrompt(scope.directory, job)}`, delivery: "queue", metadata:
1862
1862
  request = { sessionID: scope.sessionID, id: id(), command: String(job.action).replace(/^[!$]\s*/, ""), timeoutMs: job.timeoutMs };
1863
1863
  else {
1864
1864
  const [name, text] = splitFirst(String(job.action).replace(/^\/+/, ""));
1865
- request = { sessionID: scope.sessionID, name, text: text || "", delivery: "queue" };
1865
+ request = { sessionID: scope.sessionID, command: name, arguments: text || "", delivery: "queue" };
1866
1866
  }
1867
1867
  if (!isCurrent())
1868
1868
  return result({ reason: "foreground-or-compaction" });
@@ -2963,8 +2963,8 @@ Presets: /loop-dev, /loop-testfix, /loop-progress, /loop-safe-dev, /loop-ask, /l
2963
2963
  /loop-init [progress.md] creates a missing project-local progress file without overwriting existing data.
2964
2964
  Controls: /loop-now, /loop-pause, /loop-resume, /loop-stop, /loop-remove, /loop-clear, /loop-status, /loop-export.
2965
2965
  Preflight, postrun, notifications, stop files, completion markers, runtime/failure/run limits, branches and checkpoints are supported.
2966
- Scheduled shell commands run as bounded local child processes when the plugin context has no session.shell.
2967
- Scheduled/manual compaction requires session.compact on the host; unavailable capability is rejected before creating a job. Native automatic compaction is always left to OpenCode.
2966
+ Scheduled shell commands run as bounded local child processes managed by Loop; OpenCode 2's ctx.shell surface is a hook API, not a shell-execution method.
2967
+ The public OpenCode 2 plugin API does not expose manual session compaction. /loop-compact, --compact and --compact-every are rejected before job creation on V2; native automatic compaction remains host-owned and is observed through the compaction hook.
2968
2968
  An unfinished dedicated Goal reserves its session; Loop will not override it.
2969
2969
  --timeout pauses future iterations without aborting the current native model/tool/compaction operation.
2970
2970
  --safe is a command heuristic, not a sandbox. --dry-run does not dispatch or run hooks.
@@ -2982,11 +2982,11 @@ async function cleanup(value) {
2982
2982
  var OpenCodeLoopNativePlugin = Object.freeze({
2983
2983
  id: "@bybrawe/opencode-loop",
2984
2984
  async setup(ctx) {
2985
- for (const [label, fn] of [["session.prompt", ctx?.session?.prompt], ["session.hook", ctx?.session?.hook], ["event.subscribe", ctx?.event?.subscribe], ["command.transform", ctx?.command?.transform]]) {
2985
+ for (const [label, fn] of [["session.get", ctx?.session?.get], ["session.prompt", ctx?.session?.prompt], ["session.hook", ctx?.session?.hook], ["event.subscribe", ctx?.event?.subscribe], ["command.transform", ctx?.command?.transform]]) {
2986
2986
  if (typeof fn !== "function")
2987
2987
  throw new Error(`Native OpenCode Loop requires ${label}; use OpenCode 2.0.18 or newer.`);
2988
2988
  }
2989
- const directory = String(ctx.location?.directory || ctx.options?.directory || "").trim();
2989
+ const directory = String(ctx.location?.directory || "").trim();
2990
2990
  if (!directory)
2991
2991
  throw new Error("Native OpenCode Loop requires a project directory");
2992
2992
  const ownsSession = createNativeSessionLocationGuard({
@@ -3010,12 +3010,9 @@ var OpenCodeLoopNativePlugin = Object.freeze({
3010
3010
  prompt,
3011
3011
  command: typeof ctx.session.command === "function" ? (request) => ctx.session.command(request) : undefined,
3012
3012
  wait: typeof ctx.session.wait === "function" ? (request) => ctx.session.wait(request) : undefined,
3013
- compact: typeof ctx.session.compact === "function" ? (request) => ctx.session.compact({ ...request, delivery: "queue" }) : undefined,
3014
- shell: typeof ctx.session.shell === "function" ? (request) => ctx.session.shell(request) : (request) => shellHost.dispatch(request),
3015
- cancel: typeof ctx.session.inbox?.cancel === "function" ? async (request) => {
3016
- await ctx.session.inbox.cancel(request);
3017
- return false;
3018
- } : undefined,
3013
+ compact: undefined,
3014
+ shell: (request) => shellHost.dispatch(request),
3015
+ cancel: undefined,
3019
3016
  onError: (error) => {
3020
3017
  appendLoopLog(directory, "v2-native-error", { message: String(error?.message || error) }).catch(() => {});
3021
3018
  }
package/src/plugin.js ADDED
@@ -0,0 +1,19 @@
1
+ import { Plugin } from "@opencode/plugin"
2
+ import bundled from "./server.js"
3
+
4
+ export * from "./server.js"
5
+
6
+ const native = Plugin.define({
7
+ id: "@bybrawe/opencode-loop",
8
+ setup: (context) => bundled.setup(context),
9
+ })
10
+
11
+ export const OpenCodeLoopPluginModule = Object.freeze({
12
+ ...native,
13
+
14
+ // OpenCode 1.x compatibility remains a separate implementation. V2 uses
15
+ // id/setup; V1 1.18.29+ uses server().
16
+ server: bundled.server,
17
+ })
18
+
19
+ export default OpenCodeLoopPluginModule
package/src/server.js CHANGED
@@ -1862,7 +1862,7 @@ ${await policy.buildPrompt(scope.directory, job)}`, delivery: "queue", metadata:
1862
1862
  request = { sessionID: scope.sessionID, id: id(), command: String(job.action).replace(/^[!$]\s*/, ""), timeoutMs: job.timeoutMs };
1863
1863
  else {
1864
1864
  const [name, text] = splitFirst(String(job.action).replace(/^\/+/, ""));
1865
- request = { sessionID: scope.sessionID, name, text: text || "", delivery: "queue" };
1865
+ request = { sessionID: scope.sessionID, command: name, arguments: text || "", delivery: "queue" };
1866
1866
  }
1867
1867
  if (!isCurrent())
1868
1868
  return result({ reason: "foreground-or-compaction" });
@@ -2963,8 +2963,8 @@ Presets: /loop-dev, /loop-testfix, /loop-progress, /loop-safe-dev, /loop-ask, /l
2963
2963
  /loop-init [progress.md] creates a missing project-local progress file without overwriting existing data.
2964
2964
  Controls: /loop-now, /loop-pause, /loop-resume, /loop-stop, /loop-remove, /loop-clear, /loop-status, /loop-export.
2965
2965
  Preflight, postrun, notifications, stop files, completion markers, runtime/failure/run limits, branches and checkpoints are supported.
2966
- Scheduled shell commands run as bounded local child processes when the plugin context has no session.shell.
2967
- Scheduled/manual compaction requires session.compact on the host; unavailable capability is rejected before creating a job. Native automatic compaction is always left to OpenCode.
2966
+ Scheduled shell commands run as bounded local child processes managed by Loop; OpenCode 2's ctx.shell surface is a hook API, not a shell-execution method.
2967
+ The public OpenCode 2 plugin API does not expose manual session compaction. /loop-compact, --compact and --compact-every are rejected before job creation on V2; native automatic compaction remains host-owned and is observed through the compaction hook.
2968
2968
  An unfinished dedicated Goal reserves its session; Loop will not override it.
2969
2969
  --timeout pauses future iterations without aborting the current native model/tool/compaction operation.
2970
2970
  --safe is a command heuristic, not a sandbox. --dry-run does not dispatch or run hooks.
@@ -2982,11 +2982,11 @@ async function cleanup(value) {
2982
2982
  var OpenCodeLoopNativePlugin = Object.freeze({
2983
2983
  id: "@bybrawe/opencode-loop",
2984
2984
  async setup(ctx) {
2985
- for (const [label, fn] of [["session.prompt", ctx?.session?.prompt], ["session.hook", ctx?.session?.hook], ["event.subscribe", ctx?.event?.subscribe], ["command.transform", ctx?.command?.transform]]) {
2985
+ for (const [label, fn] of [["session.get", ctx?.session?.get], ["session.prompt", ctx?.session?.prompt], ["session.hook", ctx?.session?.hook], ["event.subscribe", ctx?.event?.subscribe], ["command.transform", ctx?.command?.transform]]) {
2986
2986
  if (typeof fn !== "function")
2987
2987
  throw new Error(`Native OpenCode Loop requires ${label}; use OpenCode 2.0.18 or newer.`);
2988
2988
  }
2989
- const directory = String(ctx.location?.directory || ctx.options?.directory || "").trim();
2989
+ const directory = String(ctx.location?.directory || "").trim();
2990
2990
  if (!directory)
2991
2991
  throw new Error("Native OpenCode Loop requires a project directory");
2992
2992
  const ownsSession = createNativeSessionLocationGuard({
@@ -3010,12 +3010,9 @@ var OpenCodeLoopNativePlugin = Object.freeze({
3010
3010
  prompt,
3011
3011
  command: typeof ctx.session.command === "function" ? (request) => ctx.session.command(request) : undefined,
3012
3012
  wait: typeof ctx.session.wait === "function" ? (request) => ctx.session.wait(request) : undefined,
3013
- compact: typeof ctx.session.compact === "function" ? (request) => ctx.session.compact({ ...request, delivery: "queue" }) : undefined,
3014
- shell: typeof ctx.session.shell === "function" ? (request) => ctx.session.shell(request) : (request) => shellHost.dispatch(request),
3015
- cancel: typeof ctx.session.inbox?.cancel === "function" ? async (request) => {
3016
- await ctx.session.inbox.cancel(request);
3017
- return false;
3018
- } : undefined,
3013
+ compact: undefined,
3014
+ shell: (request) => shellHost.dispatch(request),
3015
+ cancel: undefined,
3019
3016
  onError: (error) => {
3020
3017
  appendLoopLog(directory, "v2-native-error", { message: String(error?.message || error) }).catch(() => {});
3021
3018
  }
@@ -20,8 +20,8 @@ Presets: /loop-dev, /loop-testfix, /loop-progress, /loop-safe-dev, /loop-ask, /l
20
20
  /loop-init [progress.md] creates a missing project-local progress file without overwriting existing data.
21
21
  Controls: /loop-now, /loop-pause, /loop-resume, /loop-stop, /loop-remove, /loop-clear, /loop-status, /loop-export.
22
22
  Preflight, postrun, notifications, stop files, completion markers, runtime/failure/run limits, branches and checkpoints are supported.
23
- Scheduled shell commands run as bounded local child processes when the plugin context has no session.shell.
24
- Scheduled/manual compaction requires session.compact on the host; unavailable capability is rejected before creating a job. Native automatic compaction is always left to OpenCode.
23
+ Scheduled shell commands run as bounded local child processes managed by Loop; OpenCode 2's ctx.shell surface is a hook API, not a shell-execution method.
24
+ The public OpenCode 2 plugin API does not expose manual session compaction. /loop-compact, --compact and --compact-every are rejected before job creation on V2; native automatic compaction remains host-owned and is observed through the compaction hook.
25
25
  An unfinished dedicated Goal reserves its session; Loop will not override it.
26
26
  --timeout pauses future iterations without aborting the current native model/tool/compaction operation.
27
27
  --safe is a command heuristic, not a sandbox. --dry-run does not dispatch or run hooks.
@@ -38,10 +38,10 @@ async function cleanup(value) {
38
38
  export const OpenCodeLoopNativePlugin = Object.freeze({
39
39
  id: "@bybrawe/opencode-loop",
40
40
  async setup(ctx) {
41
- for (const [label, fn] of [["session.prompt", ctx?.session?.prompt], ["session.hook", ctx?.session?.hook], ["event.subscribe", ctx?.event?.subscribe], ["command.transform", ctx?.command?.transform]]) {
41
+ for (const [label, fn] of [["session.get", ctx?.session?.get], ["session.prompt", ctx?.session?.prompt], ["session.hook", ctx?.session?.hook], ["event.subscribe", ctx?.event?.subscribe], ["command.transform", ctx?.command?.transform]]) {
42
42
  if (typeof fn !== "function") throw new Error(`Native OpenCode Loop requires ${label}; use OpenCode 2.0.18 or newer.`)
43
43
  }
44
- const directory = String(ctx.location?.directory || ctx.options?.directory || "").trim()
44
+ const directory = String(ctx.location?.directory || "").trim()
45
45
  if (!directory) throw new Error("Native OpenCode Loop requires a project directory")
46
46
  const ownsSession = createNativeSessionLocationGuard({
47
47
  directory, workspaceID: ctx.location?.workspaceID,
@@ -61,11 +61,13 @@ export const OpenCodeLoopNativePlugin = Object.freeze({
61
61
  prompt,
62
62
  command: typeof ctx.session.command === "function" ? (request) => ctx.session.command(request) : undefined,
63
63
  wait: typeof ctx.session.wait === "function" ? (request) => ctx.session.wait(request) : undefined,
64
- compact: typeof ctx.session.compact === "function" ? (request) => ctx.session.compact({ ...request, delivery: "queue" }) : undefined,
65
- shell: typeof ctx.session.shell === "function" ? (request) => ctx.session.shell(request) : (request) => shellHost.dispatch(request),
66
- // cancel() returns void. The durable inbox.cancelled event, not this
67
- // response alone, proves that an undelivered input can be refunded.
68
- cancel: typeof ctx.session.inbox?.cancel === "function" ? async (request) => { await ctx.session.inbox.cancel(request); return false } : undefined,
64
+ // OpenCode 2 exposes ctx.shell as a hook domain, not an execution method,
65
+ // and SessionDomain has no public compact/inbox-cancel action. Keep those
66
+ // undocumented capabilities out of the V2 runtime instead of probing
67
+ // private host fields that may disappear between releases.
68
+ compact: undefined,
69
+ shell: (request) => shellHost.dispatch(request),
70
+ cancel: undefined,
69
71
  onError: (error) => { void appendLoopLog(directory, "v2-native-error", { message: String(error?.message || error) }).catch(() => {}) },
70
72
  })
71
73
  const bridge = createOpenCode2EventBridge({ directory, allowInboxCommands: false, onEvent: async (event) => {
@@ -166,7 +166,7 @@ export function createNativeLoopRuntime(options = {}) {
166
166
  else if (kind === "shell") request = { sessionID: scope.sessionID, id: id(), command: String(job.action).replace(/^[!$]\s*/, ""), timeoutMs: job.timeoutMs }
167
167
  else {
168
168
  const [name, text] = splitFirst(String(job.action).replace(/^\/+/, ""))
169
- request = { sessionID: scope.sessionID, name, text: text || "", delivery: "queue" }
169
+ request = { sessionID: scope.sessionID, command: name, arguments: text || "", delivery: "queue" }
170
170
  }
171
171
  if (!isCurrent()) return result({ reason: "foreground-or-compaction" })
172
172
  if (job.dryRun) {
package/src/v2.js ADDED
@@ -0,0 +1,10 @@
1
+ import { Plugin } from "@opencode/plugin"
2
+ import bundled from "./native.js"
3
+
4
+ const native = Plugin.define({
5
+ id: "@bybrawe/opencode-loop",
6
+ setup: (context) => bundled.setup(context),
7
+ })
8
+
9
+ export const OpenCodeLoopNativePlugin = native
10
+ export default native