diff --git a/.github/npm-release.json b/.github/npm-release.json index fbfe42d0..9ac48c9f 100644 --- a/.github/npm-release.json +++ b/.github/npm-release.json @@ -1,11 +1,11 @@ { "name": "@bybrawe/opencode-loop", - "version": "0.6.8", - "predecessor": "0.6.7", + "version": "0.6.9", + "predecessor": "0.6.8", "goal": { "version": "1.3.49", "sha": "0c834b06843d4f55aa3c6a1489d992adc70a5be4" }, - "requestedAt": "2026-10-13", - "reason": "User-authorized native exFAT lock compatibility patch, Goal 1.3.49 independently installed, Windows/Linux and immutable native-host release validation. Preserve existing lock ownership, fail-closed state, legacy/V2 separation and release integrity." + "requestedAt": "2026-10-19", + "reason": "Host-verified Markdown checklist completion on native OpenCode 2 and V1 compatibility, source/build parity, Goal 1.3.49 independently installed, strict Windows/Linux and real-host consumer gates. Preserve existing unlimited defaults, completion evidence integrity, plugin ownership and Desktop GUI uncertainty." } diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index ebebec87..b31c3f98 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -39,7 +39,10 @@ jobs: - run: git diff --exit-code -- src/index.js src/server.js src/native.js - run: npm run check - run: npm test - - run: npm run package:smoke + - name: Strict clean packaged npm consumer on Windows and Linux + env: + OPENCODE_PACKAGE_SMOKE_REQUIRE_INSTALL: '1' + run: npm run package:smoke real-native-pair: if: github.repository == 'ByBrawe/opencode-loop' && github.ref == 'refs/heads/main' @@ -82,7 +85,7 @@ jobs: mkdir -p "$RUNNER_TEMP/packed" "$RUNNER_TEMP/consumer" npm pack --pack-destination "$RUNNER_TEMP/packed" (cd companion-goal && npm pack --ignore-scripts --pack-destination "$RUNNER_TEMP/packed") - (cd "$RUNNER_TEMP/consumer" && npm init -y >/dev/null && npm install --omit=dev --omit=peer --ignore-scripts --no-audit --no-fund "$RUNNER_TEMP/packed/bybrawe-opencode-loop-0.6.8.tgz" "$RUNNER_TEMP/packed/bybrawe-opencode-goal-1.3.49.tgz") + (cd "$RUNNER_TEMP/consumer" && npm init -y >/dev/null && npm install --omit=dev --omit=peer --ignore-scripts --no-audit --no-fund "$RUNNER_TEMP/packed/bybrawe-opencode-loop-0.6.9.tgz" "$RUNNER_TEMP/packed/bybrawe-opencode-goal-1.3.49.tgz") npm install --prefix "$RUNNER_TEMP/native-host" --package-lock=false @opencode/cli@2.0.18 - name: Real OpenCode with both installed packages, command presets and daemon shell: bash @@ -159,7 +162,7 @@ jobs: # install-facing packument/CDN mirrors converge. A clean consumer # must still pass; retry with a fresh npm cache, never skip this gate. for attempt in {1..12}; do - if (cd "$VERIFY_DIR" && npm install --omit=dev --omit=peer --ignore-scripts --no-audit --no-fund --prefer-online --cache "$VERIFY_DIR/npm-cache-$attempt" @bybrawe/opencode-loop@0.6.8 @bybrawe/opencode-goal@1.3.49); then + if (cd "$VERIFY_DIR" && npm install --omit=dev --omit=peer --ignore-scripts --no-audit --no-fund --prefer-online --cache "$VERIFY_DIR/npm-cache-$attempt" @bybrawe/opencode-loop@0.6.9 @bybrawe/opencode-goal@1.3.49); then break fi if [ "$attempt" -eq 12 ]; then @@ -177,7 +180,7 @@ jobs: node "$VERIFY_DIR/node_modules/@bybrawe/opencode-loop/scripts/native-install-pair-test.mjs" node "$VERIFY_DIR/node_modules/@bybrawe/opencode-loop/scripts/opencode2-loop-canary.mjs" node "$VERIFY_DIR/node_modules/@bybrawe/opencode-loop/scripts/native-cli-host-canary.mjs" - echo 'PUBLISHED_NATIVE_PAIR_PASS Loop 0.6.8 + Goal 1.3.49 on OpenCode 2.0.18' + echo 'PUBLISHED_NATIVE_PAIR_PASS Loop 0.6.9 + Goal 1.3.49 on OpenCode 2.0.18' github-release: name: Create tag and GitHub release @@ -202,7 +205,7 @@ jobs: import assert from 'node:assert/strict' const base = 'https://api.github.com/repos/' + process.env.GITHUB_REPOSITORY const headers = { Authorization: `Bearer ${process.env.GH_TOKEN}`, Accept: 'application/vnd.github+json', 'Content-Type': 'application/json' } - const response = await fetch(base + '/git/ref/tags/v0.6.8', { headers }) + const response = await fetch(base + '/git/ref/tags/v0.6.9', { headers }) if (response.ok) { let object = (await response.json()).object if (object.type === 'tag') { @@ -213,11 +216,11 @@ jobs: assert.equal(object.sha, process.env.GITHUB_SHA, 'Existing release tag belongs to another source') } else { assert.equal(response.status, 404) - const created = await fetch(base + '/git/refs', { method: 'POST', headers, body: JSON.stringify({ ref: 'refs/tags/v0.6.8', sha: process.env.GITHUB_SHA }) }) + const created = await fetch(base + '/git/refs', { method: 'POST', headers, body: JSON.stringify({ ref: 'refs/tags/v0.6.9', sha: process.env.GITHUB_SHA }) }) assert.equal(created.status, 201) } JS - if ! gh release view v0.6.8 >/dev/null 2>&1; then + if ! gh release view v0.6.9 >/dev/null 2>&1; then git archive --format=zip --output=opencode-loop.zip "$COMMIT_SHA" - gh release create v0.6.8 opencode-loop.zip --title v0.6.8 --notes-file examples/OPENCODE2-0.6.8.md --verify-tag + gh release create v0.6.9 opencode-loop.zip --title v0.6.9 --notes-file examples/OPENCODE2-0.6.9.md --verify-tag fi diff --git a/CHANGELOG.md b/CHANGELOG.md index c37b4f78..f0ae06b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ No unreleased changes yet. +## 0.6.9 - 2026-10-19 + +- Fix #174: add opt-in `--until-checklist-complete` paired with `--progress-file TASK.md` so a host-verified Markdown checklist can stop Loop without an extra model iteration. The default plain idle Loop remains intentionally unlimited. +- Validate task files strictly (project-scoped regular file, bounded size, symlink and control-plane protection); ignore Markdown code fences, block quotes and commented checkbox examples. Missing/unsafe/unreadable/no-task files pause visibly rather than report completion. +- If `--verify` is configured, require successful host verification of the **exact SHA-256 checklist file contents**, never accepting a stale test success after the file changes. +- Preserve V1 and V2 semantics, update native help/status to clarify host-owned `/loop-stop` and `/loop-clear`, add regression tests and synchronize all shipped bundles. +- Clarify OpenCode 2 native package support and Windows Desktop GUI limitations; enhance two-server Desktop diagnostics already on main. Do not claim Windows GUI slash discovery is fixed. +- Keep dedicated Goal 1.3.49 separately installed and validated on real OpenCode 2 stable and pinned hosts. Track expanded macOS/platform canaries separately under #177. + ## 0.6.8 - 2026-10-13 - Fix #172: fall back to exclusive `wx` lock claims where exFAT/FAT or other volumes reject `fs.link()`, including the reported `EISDIR` error. diff --git a/README.md b/README.md index 7ce673ed..0b9cfbc1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OpenCode Loop -## Native OpenCode 2 (0.6.8) +## Native OpenCode 2 (0.6.9) 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. @@ -17,7 +17,7 @@ Prompt/command timers, watch and stop conditions, verification, preflight/postru OpenCode Loop adds `/loop`, scheduled prompt/command/shell jobs, compact scheduling, verification/checkpoints, and the `opencode-loopd` background daemon. -> **Current stable release: `0.6.8`.** +> **Release candidate: `0.6.9` (publishes only after verified release gates). Last published stable release: `0.6.8`.** ## Install or update @@ -143,9 +143,9 @@ Create a starter progress file first if needed: /loop-init ``` -### Stop when a Markdown checklist is complete (unreleased) +### Stop when a Markdown checklist is complete (0.6.9) -The native V2 source on this branch supports an **opt-in, host-checked** completion guard. It is **not part of the published 0.6.8 npm package** until a verified release ships: +OpenCode Loop 0.6.9 adds an **opt-in, host-checked** completion guard. It is **not part of 0.6.8**. Install 0.6.9 only after its npm release passes the verification gates: ```text /loop --progress-file TASK.md --until-checklist-complete --verify "npm test" finish the tasks in TASK.md diff --git a/docs/SCHEDULING.md b/docs/SCHEDULING.md index 24b43883..a7f18728 100644 --- a/docs/SCHEDULING.md +++ b/docs/SCHEDULING.md @@ -332,7 +332,7 @@ If the desired contract is to stop when the project is demonstrably complete, ma /loop --safe --ask-never --progress-file progress.md devam et bitene kadar; projeyi bitir ``` -## Opt-in checklist completion guard (unreleased on main) +## Opt-in checklist completion guard (0.6.9) The native V2 code under development adds `--until-checklist-complete` when `--progress-file` names the user-owned project Markdown checklist: @@ -344,7 +344,7 @@ The native V2 code under development adds `--until-checklist-complete` when `--p Users can stop any job directly with `/loop-stop` or `/loop-clear`. An assistant simply printing those strings in chat does **not** execute the host-native slash command; see [#175](https://github.com/ByBrawe/opencode-loop/issues/175). -This feature is **not shipped in npm 0.6.8**; verify the release notes of a later version before relying on it. +This feature is **not shipped in npm 0.6.8**. It is planned for verified Loop 0.6.9; install the published version only after its release gates pass. ## Stopping and limits diff --git a/examples/OPENCODE2-0.6.9.md b/examples/OPENCODE2-0.6.9.md new file mode 100644 index 00000000..ec683da8 --- /dev/null +++ b/examples/OPENCODE2-0.6.9.md @@ -0,0 +1,34 @@ +# OpenCode Loop 0.6.9 + +## Native OpenCode 2 checklist completion guard (#174) + +Loop now supports **opt-in host-verified checklist completion** for user-owned project Markdown files. This addresses the real user report in which all tasks in TASK.md were checked but an intentionally unlimited idle Loop kept running. + +Example: + +~~~text +/loop --progress-file TASK.md --until-checklist-complete --verify "npm test" finish remaining project work +~~~ + +The **--until-checklist-complete** flag is explicit: an ordinary **--progress-file** is context only and does **not** change the existing unlimited Loop continuation contract. At a safe scheduling boundary, the host reads the Markdown file and requires at least one task checkbox with **zero unchecked tasks**. It ignores fenced code/quoted/commented examples. Missing, unsafe, symlinked, outside-project, control-plane, empty/unreadable or oversized paths never authorize completion; Loop pauses visibly instead. + +When **--verify** is present, completion requires the host's successful verification recorded for the **exact SHA-256 content** of the checklist file. If TASK.md changes after a successful verification, the old result cannot stop the updated task. This is a file-content consistency check, **not** independent semantic evidence that untested work was correct. + +Both native V2 and explicit V1 compatibility runtimes use the guard. Deterministic parser/filesystem, safe idle prompt suppression, stale-verification, bundle parity and Loop+Goal coexistence tests are required release gates. + +## Native stop controls and OpenCode 2 diagnostics + +Invoke /loop-stop or /loop-clear as a **real user slash command**. An AI assistant merely printing /loop-clear does not invoke the host command. /loop-status shows the opt-in checklist state and actionable stop information for active jobs. + +Read-only Windows Desktop diagnostics can compare the CLI backend and an explicit localhost Desktop backend across two workspace directories, but the original Desktop GUI command discovery issue [#169](https://github.com/ByBrawe/opencode-loop/issues/169) is **not** claimed fixed until field replay. + +## Package installation and verification + +~~~text +npx -y @bybrawe/opencode-loop@0.6.9 +npx -y @bybrawe/opencode-goal@1.3.49 +~~~ + +Install packages separately; Loop never silently installs or updates dedicated Goal. Fully quit and restart OpenCode, verify /loop-doctor, /loop-status, /goal status, and a short bounded Loop run in the intended project directory. Only rely on the new flag after 0.6.9 is present in the npm registry and the full published-package consumer workflow has succeeded. If publication fails, do not treat the GitHub source commit as an npm release. + +This release does **not** claim all Windows Desktop/WSL/ARM64 GUI scenarios are solved. diff --git a/package-lock.json b/package-lock.json index 3a7d8a82..f7b94da3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bybrawe/opencode-loop", - "version": "0.6.8", + "version": "0.6.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bybrawe/opencode-loop", - "version": "0.6.8", + "version": "0.6.9", "license": "MIT", "bin": { "opencode-loop": "scripts/install-with-goals.mjs", diff --git a/package.json b/package.json index cfb2d27d..f76bfd1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bybrawe/opencode-loop", - "version": "0.6.8", + "version": "0.6.9", "description": "Idle-safe /loop scheduling and background automation for OpenCode, with optional legacy Loop Goal compatibility and a separately installable OpenCode Goal companion.", "type": "module", "main": "src/plugin.js", diff --git a/scripts/native-release.mjs b/scripts/native-release.mjs index 9e7ddb63..f5174ffe 100644 --- a/scripts/native-release.mjs +++ b/scripts/native-release.mjs @@ -7,11 +7,11 @@ export function validateRelease(request, pkg, env) { assert.equal(env.GITHUB_REPOSITORY, "ByBrawe/opencode-loop", "wrong release repository") assert.equal(env.GITHUB_REF, "refs/heads/main", "npm release must come from main") assert.match(env.GITHUB_SHA ?? "", /^[a-f0-9]{40}$/, "immutable release SHA required") - assert.equal(request?.version, "0.6.8", "release version is not authorized") + assert.equal(request?.version, "0.6.9", "release version is not authorized") assert.equal(request?.name, "@bybrawe/opencode-loop") assert.equal(pkg.name, request.name) assert.equal(pkg.version, request.version) - assert.equal(request.predecessor, "0.6.7") + assert.equal(request.predecessor, "0.6.8") assert.equal(request.goal?.version, "1.3.49") assert.match(request.goal?.sha ?? "", /^[a-f0-9]{40}$/) return request diff --git a/scripts/publish-workflow-test.mjs b/scripts/publish-workflow-test.mjs index 16ab04db..47ead5d6 100644 --- a/scripts/publish-workflow-test.mjs +++ b/scripts/publish-workflow-test.mjs @@ -20,6 +20,7 @@ assert.match(workflow, /needs:\s*\[native-regressions, real-native-pair\]/) assert.match(workflow, /node scripts\/native-release\.mjs ci/) assert.match(workflow, /git diff --exit-code -- src\/index.js src\/server.js src\/native.js/) assert.match(workflow, /npm run package:smoke/) +assert.match(workflow, /OPENCODE_PACKAGE_SMOKE_REQUIRE_INSTALL:\s*\x271\x27/, "Windows/Linux npm release gate must require a strict clean consumer install, never a tarball fallback") assert.match(workflow, /windows-latest/) assert.match(workflow, /Decide whether npm publish is needed/) assert.match(workflow, /steps\.npm-state\.outputs\.publish == 'true'/) @@ -48,14 +49,14 @@ assert.ok(workflow.includes('--prefer-online --cache "$VERIFY_DIR/npm-cache-$att assert.ok(workflow.includes("Published Loop+Goal clean-install validation did not converge"), "clean consumer must remain a hard release gate") const sha = "a".repeat(40) -const request = { name: "@bybrawe/opencode-loop", version: "0.6.8", predecessor: "0.6.7", goal: { version: "1.3.49", sha: "b".repeat(40) } } +const request = { name: "@bybrawe/opencode-loop", version: "0.6.9", predecessor: "0.6.8", goal: { version: "1.3.49", sha: "b".repeat(40) } } const pkg = { name: request.name, version: request.version } const env = { GITHUB_REPOSITORY: "ByBrawe/opencode-loop", GITHUB_REF: "refs/heads/main", GITHUB_SHA: sha } assert.strictEqual(validateRelease(request, pkg, env), request) for (const change of [{ GITHUB_REPOSITORY: "someone/fork" }, { GITHUB_REF: "refs/heads/release" }, { GITHUB_SHA: "main" }]) assert.throws(() => validateRelease(request, pkg, { ...env, ...change })) assert.throws(() => validateRelease({ ...request, version: "9.0.0" }, pkg, env)) assert.throws(() => validateRelease(request, { ...pkg, version: "0.6.5" }, env)) -assert.equal(releaseDecision(404, null, request, sha, "0.6.7"), true) +assert.equal(releaseDecision(404, null, request, sha, "0.6.8"), true) for (const status of [401, 403, 429, 500, 503]) assert.throws(() => releaseDecision(status, null, request, sha, "0.6.0")) assert.throws(() => releaseDecision(404, null, request, sha, "0.7.0")) const manifest = { ...pkg, gitHead: sha }