@bybrawe/opencode-loop 0.6.5 → 0.6.7

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased (main only)
4
+
5
+ No unreleased changes yet.
6
+
7
+ ## 0.6.7 - 2026-10-09
8
+
9
+ - Recover the native Loop+Goal coexistence canary's cold Windows server bootstrap with bounded read-only session polling and distinguish server startup hangs from actual autonomous-ownership conflicts.
10
+ - Enforce fail-closed corrupt Loop state reads and protect per-session writes across multiple processes on Windows and Linux, including dead-owner recovery.
11
+ - Support manual native V2 compaction through the public `session.compact` capability, verified by matching manual `inputID`, completion and terminal boundaries.
12
+ - Add read-only Windows Desktop command-registry diagnostics for Loop and Goal and parent-versus-child workspace comparisons; the separate GUI `command.updated` refresh is proposed upstream but not published.
13
+ - Align `/loop-help` with the current compaction behavior, rather than advertising the removed V2 limitation.
14
+
15
+ ## 0.6.6 - 2026-10-04
16
+
17
+ - Make a normal Loop install/update strictly Loop-only: it no longer installs, detects, refreshes, migrates, or modifies the dedicated `@bybrawe/opencode-goal` package.
18
+ - Omit Loop's legacy experimental `/loop-goal*` commands by default; retain them only behind explicit `--with-loop-goals` compatibility opt-in.
19
+ - Keep `--with-goals` as the explicit combined-install opt-in, while recommending the independently installable OpenCode Goal plugin for durable verified goals.
20
+ - Link Loop and Goal documentation directly so the two packages remain complementary without hidden installer coupling.
21
+
3
22
  ## 0.6.5 - 2026-10-02
4
23
 
5
24
  - Audit the native OpenCode 2 package contract against `@opencode/plugin@2.0.22` and pin the production SDK/runtime dependency to exact 2.0.22.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenCode Loop
2
2
 
3
- ## Native OpenCode 2 (0.6.5)
3
+ ## Native OpenCode 2 (0.6.7)
4
4
 
5
5
  The npm and V2 entrypoints use the official `@opencode/plugin` `Plugin.define()` contract. The installer now registers the exact `@bybrawe/opencode-loop` package in OpenCode's plural `plugins` configuration instead of copying a loose global plugin directory. OpenCode therefore resolves `@opencode/plugin` from Loop's own production dependency graph; explicit `--legacy-v1` remains a separate compatibility path.
6
6
 
@@ -8,7 +8,7 @@ The installer defaults to the native V2 package plugin. It needs no loose local
8
8
 
9
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.
10
10
 
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.
11
+ Automatic compaction remains host-owned. On OpenCode 2 versions exposing public `session.compact`, Loop's explicit native compaction is capability-gated and waits for its own manual `session.compaction.started` input ID, compaction completion, and a session terminal event. Older hosts reject unsupported compaction before scheduling.
12
12
 
13
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.
14
14
 
@@ -17,52 +17,60 @@ Prompt/command timers, watch and stop conditions, verification, preflight/postru
17
17
 
18
18
  OpenCode Loop adds `/loop`, scheduled prompt/command/shell jobs, compact scheduling, verification/checkpoints, and the `opencode-loopd` background daemon.
19
19
 
20
- > **Current stable release: `0.6.5`.**
20
+ > **Current stable release: `0.6.7`.**
21
21
 
22
22
  ## Install or update
23
23
 
24
24
  ### Recommended OpenCode 2 setup
25
25
 
26
- Install the dedicated Goal plugin first:
26
+ Install **both packages separately**:
27
27
 
28
28
  ```bash
29
+ npx -y @bybrawe/opencode-loop@latest
29
30
  npx -y @bybrawe/opencode-goal@latest
30
31
  ```
31
32
 
32
- Then install Loop by itself and disable Loop's older experimental Goal command files:
33
+ This is the recommended setup. The two installers stay independent:
34
+
35
+ - the Loop command installs or updates **OpenCode Loop only**;
36
+ - the Goal command installs or updates the dedicated **OpenCode Goal** plugin;
37
+ - Loop does **not** install, detect, refresh, migrate, or otherwise modify `@bybrawe/opencode-goal` during a normal install/update;
38
+ - Loop omits/removes its older experimental `/loop-goal*` command files by default.
39
+
40
+ OpenCode Goal owns `/goal`, durable objective state, evidence, verification, restart recovery, and semantic completion. See [OpenCode Goal](https://github.com/ByBrawe/opencode-goal).
41
+
42
+ A combined `--with-goals` path remains only as a compatibility/convenience option; the recommended setup is the two separate commands above:
43
+
44
+ ```bash
45
+ npx -y @bybrawe/opencode-loop@latest --with-goals
46
+ ```
47
+
48
+ If you explicitly need Loop's **legacy experimental** `/loop-goal*` commands for compatibility, opt in with:
33
49
 
34
50
  ```bash
35
- npx -y @bybrawe/opencode-loop@latest --loop-only --without-loop-goals
51
+ npx -y @bybrawe/opencode-loop@latest --with-loop-goals
36
52
  ```
37
53
 
38
- This is the recommended split for OpenCode 2:
54
+ Those legacy commands are not recommended for new setups; prefer the dedicated Goal plugin instead.
39
55
 
40
- - **OpenCode Goal** owns `/goal`, durable outcome-driven work, evidence, verification, recovery, and semantic completion.
41
- - **OpenCode Loop** owns `/loop`, scheduling, repeated prompts/commands/shell work, and `opencode-loopd`.
42
- - `--loop-only` prevents the Loop installer from installing/updating the Goal companion.
43
- - `--without-loop-goals` removes only Loop's older experimental `/loop-goal*` command files; it does not remove the dedicated OpenCode Goal plugin.
56
+ `--loop-only` remains as an explicit alias for the new default Loop-only behavior, and `--without-loop-goals` remains as a compatibility alias for the new default clean command surface.
44
57
 
45
- Run the same two commands again to update both packages. Loop 0.6.4 also canonicalizes valid legacy `plugin` entries into the native `plugins` list; earlier 0.6.1/0.6.2 loose global V2 installs are removed and replaced by the pinned published package. Then **fully restart OpenCode** and verify:
58
+ Run the relevant install command again to update that package. Then **fully restart OpenCode** and verify:
46
59
 
47
60
  ```text
48
- /goal status
49
61
  /loop-help
50
62
  /loop-doctor
51
63
  ```
52
64
 
53
- Convenience alternative: let the Loop installer also install/update the dedicated Goal companion:
65
+ If you installed Goal too:
54
66
 
55
- ```bash
56
- npx -y @bybrawe/opencode-loop@latest --with-goals --without-loop-goals
67
+ ```text
68
+ /goal status
57
69
  ```
58
70
 
59
- For new OpenCode 2 installations, the explicit two-command setup above is preferred because ownership is clear and each package can be updated independently.
71
+ If both native commands work in terminal TUI but are missing from Windows Desktop, follow the [Windows Desktop command-registry diagnostic guide](docs/OPENCODE2-DESKTOP-WINDOWS.md). Do not replace native commands with legacy Markdown bridges.
60
72
 
61
- Install only Loop without touching Goal:
62
-
63
- ```bash
64
- npx -y @bybrawe/opencode-loop@latest --loop-only
65
- ```
73
+ On a version containing the diagnostic, run `npx -y @bybrawe/opencode-loop@latest --desktop-diagnostics --workspace "C:\\path\\to\\your\\project"` to check both commands against the currently registered *local* V2 service without printing its credentials. This does not install or modify Goal or Loop.
66
74
 
67
75
  Global npm alternative:
68
76
 
@@ -84,7 +92,7 @@ opencode-loop --uninstall
84
92
  npm uninstall -g @bybrawe/opencode-loop
85
93
  ```
86
94
 
87
- Project Loop state under `.opencode/opencode-loop/` is intentionally preserved by uninstall.
95
+ Loop uninstall never removes the dedicated Goal plugin. Project Loop state under `.opencode/opencode-loop/` is intentionally preserved by uninstall.
88
96
 
89
97
  ## The mental model
90
98
 
@@ -374,7 +382,7 @@ Recommended choices are to finish/pause the Goal or use a separate session. Adva
374
382
 
375
383
  That escape hatch can create competing autonomous turns; use it only when that is intentional.
376
384
 
377
- Loop's older experimental `/loop-goal*` commands remain available for compatibility unless installed with `--without-loop-goals`. For new strongly verified Goal work, prefer:
385
+ Loop's older experimental `/loop-goal*` commands are omitted by default. They remain available only as an explicit compatibility opt-in with `--with-loop-goals`. For new strongly verified Goal work, use the separate [OpenCode Goal](https://github.com/ByBrawe/opencode-goal) plugin:
378
386
 
379
387
  ```bash
380
388
  npx -y @bybrawe/opencode-goal@latest
@@ -0,0 +1,48 @@
1
+ # OpenCode Loop 0.6.6
2
+
3
+ Loop-only installation contract patch, paired for coexistence validation with OpenCode Goal 1.3.47.
4
+
5
+ ## Default installation
6
+
7
+ A normal Loop install/update now touches **Loop only**:
8
+
9
+ ```sh
10
+ npx -y @bybrawe/opencode-loop@0.6.6
11
+ ```
12
+
13
+ It does not install, detect, refresh, migrate, or modify the dedicated OpenCode Goal package. Loop's older experimental `/loop-goal*` command files are omitted by default.
14
+
15
+ For durable outcome-driven work, install Goal independently:
16
+
17
+ ```sh
18
+ npx -y @bybrawe/opencode-goal@1.3.47
19
+ ```
20
+
21
+ Or explicitly opt into a combined install:
22
+
23
+ ```sh
24
+ npx -y @bybrawe/opencode-loop@0.6.6 --with-goals
25
+ ```
26
+
27
+ Legacy experimental Loop Goal commands remain available only when explicitly requested:
28
+
29
+ ```sh
30
+ npx -y @bybrawe/opencode-loop@0.6.6 --with-loop-goals
31
+ ```
32
+
33
+ ## Validation contract
34
+
35
+ The release remains gated by Ubuntu/Windows regressions, production package smoke, the real OpenCode 2 adapter, exact OpenCode 2.0.18 compatibility, current-host coexistence, and immutable Goal companion source verification.
36
+
37
+ After installation, fully restart OpenCode and verify:
38
+
39
+ ```text
40
+ /loop-help
41
+ /loop-doctor
42
+ ```
43
+
44
+ If Goal is installed too:
45
+
46
+ ```text
47
+ /goal status
48
+ ```
@@ -0,0 +1,26 @@
1
+ # OpenCode Loop 0.6.7
2
+
3
+ OpenCode 2 native reliability and package parity patch, tested alongside **separately installed** OpenCode Goal 1.3.48.
4
+
5
+ ## Changes
6
+
7
+ - Protect per-session Loop state across multiple OpenCode processes with file locks, fail-closed corrupt-file reads, crash/dead-owner lock recovery and atomic writes on Windows/Linux.
8
+ - Restore manual native compaction with the public OpenCode 2 `session.compact` capability, requiring its exact manual compaction input ID, completion and terminal. Reject unsupported hosts without corrupting scheduled work.
9
+ - Provide read-only Windows Desktop native `/loop` and `/goal` command-catalog diagnostics and explicit project-directory comparisons. This is **not** a claim that Windows Desktop GUI autocomplete is fixed upstream.
10
+ - Bound Loop+Goal Windows cold-server startup probes and separate unresponsive host bootstrap from real Loop/Goal ownership conflicts.
11
+ - Align native `/loop-help` with the current V2 compaction contract.
12
+
13
+ ## Install independently
14
+
15
+ ```powershell
16
+ npx -y @bybrawe/opencode-loop@0.6.7
17
+ npx -y @bybrawe/opencode-goal@1.3.48
18
+ ```
19
+
20
+ A normal Loop install touches Loop only; it never installs or updates the dedicated Goal package. Legacy `/loop-goal*` commands stay opt-in, and existing state is preserved.
21
+
22
+ ## Verification
23
+
24
+ When npm confirms publication, fully restart OpenCode and run `/loop-help`, `/loop-doctor`, `/goal status` in terminal TUI. For Windows Desktop use the [read-only diagnostic](../docs/OPENCODE2-DESKTOP-WINDOWS.md) without adding Markdown slash-command bridges.
25
+
26
+ Required release gates: immutable Goal source, Windows/Ubuntu package regressions, exact-main CI, installed-pair real host and clean npm consumer checks. GitHub tag and release creation require separate repository authorization, which may fail independently of verified npm publication.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bybrawe/opencode-loop",
3
- "version": "0.6.5",
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.",
3
+ "version": "0.6.7",
4
+ "description": "Idle-safe /loop scheduling and background automation for OpenCode, with optional legacy Loop Goal compatibility and a separately installable OpenCode Goal companion.",
5
5
  "type": "module",
6
6
  "main": "src/plugin.js",
7
7
  "exports": {
@@ -24,8 +24,8 @@
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
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
- "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",
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/state-process-test.mjs && node --check scripts/state-integrity-test.mjs && node --check scripts/install-test.mjs && node --check scripts/desktop-command-diagnostics.mjs && node --check scripts/desktop-command-diagnostics-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-goal-bootstrap.mjs && node --check scripts/host-goal-bootstrap-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
+ "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/state-process-test.mjs && node scripts/state-integrity-test.mjs && node scripts/install-test.mjs && node --test scripts/desktop-command-diagnostics-test.mjs && node scripts/goal-companion-test.mjs && node scripts/loopd-test.mjs && node scripts/smoke-test.mjs && node --test scripts/host-goal-bootstrap-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",
31
31
  "pack:zip": "node scripts/make-zip.mjs",
@@ -337,7 +337,7 @@ async function testActionRoutingAndSafety() {
337
337
  try {
338
338
  await h.command("loop-shell", "0s --safe npm run format")
339
339
  await h.command("loop-now", "shell")
340
- await delay(25)
340
+ await waitForValue(() => h.records.shells[0], 2_000)
341
341
  assert.equal(h.records.shells.length, 1)
342
342
  assert.equal(h.records.shells[0].command, "npm run format", "safe mode must allow a harmless format script")
343
343
  } finally {
@@ -403,8 +403,8 @@ async function testActionRoutingAndSafety() {
403
403
  try {
404
404
  await h.command("loop-command", "0s /custom-command alpha beta")
405
405
  await h.command("loop-now", "command")
406
- await delay(25)
407
- assert.deepEqual(h.records.commands[0], {
406
+ const dispatched = await waitForValue(() => h.records.commands[0], 2_000)
407
+ assert.deepEqual(dispatched, {
408
408
  command: "custom-command",
409
409
  arguments: "alpha beta",
410
410
  agent: "build",
@@ -0,0 +1,92 @@
1
+ import assert from "node:assert/strict"
2
+ import { createServer } from "node:http"
3
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"
4
+ import { tmpdir } from "node:os"
5
+ import path from "node:path"
6
+ import { spawnSync } from "node:child_process"
7
+ import { fileURLToPath } from "node:url"
8
+ import test from "node:test"
9
+ import { diagnose } from "./desktop-command-diagnostics.mjs"
10
+
11
+ test("Windows Desktop native catalog probe checks both commands but never returns credentials", async () => {
12
+ const home = await mkdtemp(path.join(tmpdir(), "loop-desktop-diagnostic-"))
13
+ const workspace = path.join(home, "project with spaces")
14
+ const desktopWorkspace = path.join(home, "desktop parent project")
15
+ const cfg = path.join(home, ".config", "opencode")
16
+ const state = path.join(home, ".local", "state", "opencode")
17
+ const calls = []
18
+ const password = "private-diagnostic-password"
19
+ const server = createServer((req, res) => {
20
+ calls.push({ url: req.url, directory: req.headers["x-opencode-directory"], auth: req.headers.authorization })
21
+ res.setHeader("Content-Type", "application/json")
22
+ if (req.url === "/api/info") return res.end(JSON.stringify({ pid: process.pid, version: "2.0.22" }))
23
+ if (req.url === "/api/command") {
24
+ const native = req.headers["x-opencode-directory"] === desktopWorkspace
25
+ ? [{ name: "init" }] : [{ name: "loop" }, { name: "goal" }, { name: "loop-help" }]
26
+ return res.end(JSON.stringify(native))
27
+ }
28
+ res.writeHead(404).end("{}")
29
+ })
30
+ try {
31
+ await mkdir(cfg, { recursive: true })
32
+ await mkdir(state, { recursive: true })
33
+ await mkdir(workspace, { recursive: true })
34
+ await mkdir(desktopWorkspace, { recursive: true })
35
+ await writeFile(path.join(cfg, "opencode.json"), JSON.stringify({
36
+ plugins: ["@bybrawe/opencode-loop@0.6.6", "@bybrawe/opencode-goal@1.3.47"],
37
+ secrets: password,
38
+ }))
39
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve))
40
+ const url = "http://127.0.0.1:" + server.address().port
41
+ await writeFile(path.join(state, "service.json"), JSON.stringify({
42
+ url, pid: process.pid, version: "2.0.22", password,
43
+ }))
44
+ const result = await diagnose({ home, workspace, env: {} })
45
+ assert.equal(result.service.status, "ready")
46
+ assert.deepEqual(result.service.commands, { loop: true, goal: true, count: 3 })
47
+ assert.deepEqual(result.config.pins.loop, ["@bybrawe/opencode-loop@0.6.6"])
48
+ assert.deepEqual(result.config.pins.goal, ["@bybrawe/opencode-goal@1.3.47"])
49
+ assert.ok(calls.every((c) => c.directory === workspace))
50
+ assert.ok(calls.every((c) => c.auth === "Basic " + Buffer.from("opencode:" + password).toString("base64")))
51
+ assert.ok(!JSON.stringify(result).includes(password), "no secret may appear in diagnostic output")
52
+ const beforeCompare = calls.length
53
+ const compared = await diagnose({ home, workspace, compareWorkspace: desktopWorkspace, env: {} })
54
+ assert.equal(compared.comparison.workspace, desktopWorkspace)
55
+ assert.equal(compared.comparison.status, "ready")
56
+ assert.deepEqual(compared.comparison.commands, { loop: false, goal: false, count: 1 })
57
+ assert.equal(compared.comparison.matchesPrimary, false, "Desktop parent and terminal child must be comparable")
58
+ assert.ok(calls.slice(beforeCompare).some((entry) =>
59
+ entry.url === "/api/command" && entry.directory === desktopWorkspace &&
60
+ entry.auth === "Basic " + Buffer.from("opencode:" + password).toString("base64")
61
+ ), "comparison must use the same authorized local server and the requested Desktop project location")
62
+ assert.ok(!JSON.stringify(compared).includes(password), "comparison never exposes credentials")
63
+ const same = await diagnose({ home, workspace, compareWorkspace: workspace, env: {} })
64
+ assert.equal(same.comparison, undefined, "identical workspace needs no redundant catalog probe")
65
+ const bad = await diagnose({
66
+ home, workspace, env: {}, server: "https://example.com", fetchImpl: () => { throw new Error("must not contact remote") },
67
+ })
68
+ assert.equal(bad.service.status, "refused-nonlocal-or-invalid-url")
69
+ await writeFile(path.join(state, "service.json"), JSON.stringify({ url, pid: process.pid + 5, password }))
70
+ const stale = await diagnose({ home, workspace, env: {} })
71
+ assert.equal(stale.service.status, "stale-service-registration")
72
+ } finally {
73
+ await new Promise((resolve) => server.close(resolve))
74
+ await rm(home, { recursive: true, force: true })
75
+ }
76
+ })
77
+
78
+ test("installer desktop diagnosis is read-only and does not invoke Loop or Goal installers", async () => {
79
+ const home = await mkdtemp(path.join(tmpdir(), "loop-desktop-installer-"))
80
+ try {
81
+ const installer = fileURLToPath(new URL("./install-with-goals.mjs", import.meta.url))
82
+ const result = spawnSync(process.execPath, [installer, "--desktop-diagnostics", "--workspace", home, "--json"], {
83
+ encoding: "utf8", timeout: 15_000, env: { ...process.env, OPENCODE_CONFIG_DIR: path.join(home, "no-config"), XDG_STATE_HOME: path.join(home, "no-state") },
84
+ })
85
+ assert.equal(result.status, 0, result.stderr)
86
+ const report = JSON.parse(result.stdout)
87
+ assert.equal(report.service.status, "registration-not-found")
88
+ assert.equal(report.workspace, home)
89
+ assert.equal(await readFile(path.join(home, "no-config", "opencode.json"), "utf8").then(() => true, () => false), false)
90
+ assert.ok(!result.stdout.includes("Installed/updated"), "installer must not run in diagnosis mode")
91
+ } finally { await rm(home, { recursive: true, force: true }) }
92
+ })
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+ // Read-only diagnosis for a Windows OpenCode Desktop/TUI command mismatch.
3
+ // Never publish service.json, config content, authorization or response bodies.
4
+ import { readFile, stat } from "node:fs/promises"
5
+ import { homedir } from "node:os"
6
+ import path from "node:path"
7
+ import process from "node:process"
8
+ import { fileURLToPath } from "node:url"
9
+
10
+ const packageNames = Object.freeze(["loop", "goal"])
11
+ const allowHosts = new Set(["localhost", "127.0.0.1", "[::1]"])
12
+ const knownConfigs = ["opencode.json", "opencode.jsonc", "config.json", "config.jsonc"]
13
+
14
+ async function readSmall(file) {
15
+ try {
16
+ const info = await stat(file)
17
+ if (!info.isFile() || info.size > 1024 * 1024) return undefined
18
+ return await readFile(file, "utf8")
19
+ } catch (error) {
20
+ if (error?.code === "ENOENT") return undefined
21
+ throw error
22
+ }
23
+ }
24
+
25
+ function localUrl(raw) {
26
+ if (typeof raw !== "string") return
27
+ let url
28
+ try { url = new URL(raw) } catch { return }
29
+ if (!["http:", "https:"].includes(url.protocol)) return
30
+ if (!allowHosts.has(url.hostname.toLowerCase())) return
31
+ if (url.username || url.password) return
32
+ if ((url.pathname && url.pathname !== "/") || url.search || url.hash) return
33
+ return url.origin
34
+ }
35
+
36
+ function presentPins(raw) {
37
+ const result = {}
38
+ for (const name of packageNames) {
39
+ const prefix = "@bybrawe/opencode-" + name + "@"
40
+ const hits = raw.match(new RegExp(prefix.replace(/[.*+?^$\\{\\}()|[\\]\\\\]/g, "\\$&") + "[0-9]+\\.[0-9]+\\.[0-9]+", "g")) ?? []
41
+ result[name] = [...new Set(hits)].sort()
42
+ }
43
+ return result
44
+ }
45
+
46
+ async function configPins(env, home) {
47
+ const configHome = env.OPENCODE_CONFIG_DIR
48
+ ?? path.join(env.XDG_CONFIG_HOME ?? path.join(home, ".config"), "opencode")
49
+ const files = []
50
+ const pins = { loop: [], goal: [] }
51
+ for (const name of knownConfigs) {
52
+ const file = path.join(configHome, name)
53
+ const value = await readSmall(file)
54
+ if (value === undefined) continue
55
+ files.push(file)
56
+ const found = presentPins(value)
57
+ for (const kind of packageNames) pins[kind].push(...found[kind])
58
+ }
59
+ for (const kind of packageNames) pins[kind] = [...new Set(pins[kind])].sort()
60
+ return { directory: configHome, files, pins }
61
+ }
62
+
63
+ function commandsFrom(json) {
64
+ const body = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : undefined
65
+ if (!body) return undefined
66
+ return new Set(body.flatMap((item) => typeof item?.name === "string" ? [item.name] : []))
67
+ }
68
+
69
+ async function request(fetchImpl, base, pathname, headers) {
70
+ try {
71
+ const response = await fetchImpl(base + pathname, {
72
+ method: "GET",
73
+ headers,
74
+ signal: AbortSignal.timeout(5_000),
75
+ })
76
+ if (![200, 201].includes(response.status)) return { status: response.status }
77
+ const json = await response.json()
78
+ return { status: response.status, json }
79
+ } catch (error) {
80
+ return { error: error?.name === "TimeoutError" ? "timeout" : "connection-failed" }
81
+ }
82
+ }
83
+
84
+ async function catalog(fetchImpl, origin, headers) {
85
+ let response = await request(fetchImpl, origin, "/api/command", headers)
86
+ if (response.status === 404) response = await request(fetchImpl, origin, "/command", headers)
87
+ return response
88
+ }
89
+
90
+ export async function diagnose({
91
+ env = process.env, home = homedir(), workspace = process.cwd(),
92
+ compareWorkspace, server, serviceFile, fetchImpl = fetch,
93
+ } = {}) {
94
+ const cwd = path.resolve(workspace)
95
+ const comparisonDirectory = typeof compareWorkspace === "string" && compareWorkspace.trim()
96
+ ? path.resolve(compareWorkspace) : undefined
97
+ const config = await configPins(env, home)
98
+ const registeredFile = serviceFile ?? path.join(
99
+ env.XDG_STATE_HOME ?? path.join(home, ".local", "state"), "opencode", "service.json",
100
+ )
101
+ const output = { workspace: cwd, config, service: { source: server ? "explicit" : "registration", status: "unavailable" } }
102
+ let url = server
103
+ let password = env.OPENCODE_PASSWORD ?? env.OPENCODE_SERVER_PASSWORD
104
+ let version
105
+ let pid
106
+ if (!server) {
107
+ const raw = await readSmall(registeredFile)
108
+ if (raw === undefined) {
109
+ output.service.status = "registration-not-found"
110
+ return output
111
+ }
112
+ let info
113
+ try { info = JSON.parse(raw) }
114
+ catch {
115
+ output.service.status = "registration-invalid"
116
+ return output
117
+ }
118
+ url = info?.url
119
+ password = info?.password
120
+ version = typeof info?.version === "string" ? info.version : undefined
121
+ pid = Number.isInteger(info?.pid) ? info.pid : undefined
122
+ }
123
+ const origin = localUrl(url)
124
+ if (!origin) {
125
+ output.service.status = "refused-nonlocal-or-invalid-url"
126
+ return output
127
+ }
128
+ output.service.host = new URL(origin).host
129
+ if (version) output.service.registeredVersion = version
130
+ const headers = { "x-opencode-directory": cwd, accept: "application/json" }
131
+ if (typeof password === "string" && password.length) {
132
+ headers.authorization = "Basic " + Buffer.from("opencode:" + password).toString("base64")
133
+ }
134
+ const info = await request(fetchImpl, origin, "/api/info", headers)
135
+ if (info.error) {
136
+ output.service.status = info.error
137
+ return output
138
+ }
139
+ if (info.status === 401 || info.status === 403) {
140
+ output.service.status = "authorization-failed"
141
+ return output
142
+ }
143
+ if (info.status === 200 && info.json) {
144
+ const hostPID = info.json?.pid
145
+ if (pid !== undefined && hostPID !== undefined && pid !== hostPID) {
146
+ output.service.status = "stale-service-registration"
147
+ return output
148
+ }
149
+ if (typeof info.json.version === "string") output.service.actualVersion = info.json.version
150
+ }
151
+ const list = await catalog(fetchImpl, origin, headers)
152
+ if (list.error) {
153
+ output.service.status = list.error
154
+ return output
155
+ }
156
+ if (list.status === 401 || list.status === 403) {
157
+ output.service.status = "authorization-failed"
158
+ return output
159
+ }
160
+ const names = commandsFrom(list.json)
161
+ if (!names) {
162
+ output.service.status = "command-catalog-unavailable"
163
+ output.service.httpStatus = list.status
164
+ return output
165
+ }
166
+ output.service.status = "ready"
167
+ output.service.commands = { loop: names.has("loop"), goal: names.has("goal"), count: names.size }
168
+ if (comparisonDirectory && comparisonDirectory !== cwd) {
169
+ const comparisonHeaders = { ...headers, "x-opencode-directory": comparisonDirectory }
170
+ const compared = await catalog(fetchImpl, origin, comparisonHeaders)
171
+ const comparedNames = commandsFrom(compared.json)
172
+ output.comparison = {
173
+ workspace: comparisonDirectory,
174
+ status: comparedNames ? "ready" : compared.error || (
175
+ compared.status === 401 || compared.status === 403 ? "authorization-failed" : "command-catalog-unavailable"
176
+ ),
177
+ ...(comparedNames ? {
178
+ commands: { loop: comparedNames.has("loop"), goal: comparedNames.has("goal"), count: comparedNames.size },
179
+ matchesPrimary: comparedNames.has("loop") === names.has("loop") && comparedNames.has("goal") === names.has("goal"),
180
+ } : {}),
181
+ }
182
+ }
183
+ return output
184
+ }
185
+
186
+ export async function runDesktopDiagnosticCli(args = process.argv.slice(2), options = {}) {
187
+ let workspace = process.cwd(), compareWorkspace, server, json = false
188
+ for (let i = 0; i < args.length; i++) {
189
+ const arg = args[i]
190
+ if (arg === "--help" || arg === "-h") {
191
+ console.log("Usage: opencode-loop --desktop-diagnostics [--workspace PATH] [--compare-workspace PATH] [--server LOCAL_URL] [--json]")
192
+ console.log("Read-only check of both native /loop and /goal registrations; never prints credentials.")
193
+ return 0
194
+ }
195
+ if (arg === "--json") { json = true; continue }
196
+ if (arg === "--workspace" || arg === "--compare-workspace" || arg === "--server") {
197
+ if (!args[i + 1]) { console.error("Missing value for " + arg); return 2 }
198
+ if (arg === "--workspace") workspace = args[++i]
199
+ else if (arg === "--compare-workspace") compareWorkspace = args[++i]
200
+ else server = args[++i]
201
+ continue
202
+ }
203
+ console.error("Unknown Desktop diagnostic option: " + arg)
204
+ return 2
205
+ }
206
+ const result = await diagnose({ ...options, workspace, compareWorkspace, server })
207
+ if (json) console.log(JSON.stringify(result, null, 2))
208
+ else {
209
+ console.log("OpenCode Desktop native command diagnosis (read-only)")
210
+ console.log("Workspace: " + result.workspace)
211
+ console.log("Global config: " + result.config.directory)
212
+ for (const kind of packageNames)
213
+ console.log("Configured " + kind + " spec: " + (result.config.pins[kind].join(", ") || "not found (text scan)"))
214
+ console.log("Local service: " + result.service.status + (result.service.registeredVersion ? " (" + result.service.registeredVersion + ")" : ""))
215
+ if (result.service.commands)
216
+ console.log("Server /loop: " + result.service.commands.loop + " | /goal: " + result.service.commands.goal)
217
+ if (result.comparison) {
218
+ console.log("Compared workspace: " + result.comparison.workspace)
219
+ console.log("Compared catalog: " + result.comparison.status)
220
+ if (result.comparison.commands) {
221
+ console.log("Compared /loop: " + result.comparison.commands.loop + " | /goal: " + result.comparison.commands.goal)
222
+ console.log("Same native command visibility across directories: " + result.comparison.matchesPrimary)
223
+ }
224
+ }
225
+ if (!result.service.commands)
226
+ console.log("No confirmed local server catalog. Desktop may use a different connection or service.")
227
+ console.log("This checks one local service and directory; it cannot prove which connection Desktop currently uses.")
228
+ }
229
+ return 0
230
+ }
231
+
232
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
233
+ runDesktopDiagnosticCli().then((code) => { process.exitCode = code }).catch((error) => {
234
+ console.error("Desktop diagnostic failed: " + (error?.code || error?.name || "unknown error"))
235
+ process.exitCode = 1
236
+ })
237
+ }