From ae59e74a0c465afa1376d0bd86e2b2bd03610d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:26:45 +0300 Subject: [PATCH 01/16] test: cover installed TypeScript plugin shape --- .github/workflows/ci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4447c13e..d4cd3930 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,12 @@ jobs: - name: Verify Bun can load the plugin run: bun -e "await import('./src/index.js')" + - name: Regression #2 - verify local .ts plugin loads in Bun + run: | + node -e "require('node:fs').copyFileSync('src/index.js', 'src/__opencode_loop_local_ci.ts')" + bun -e "await import('./src/__opencode_loop_local_ci.ts')" + node -e "require('node:fs').rmSync('src/__opencode_loop_local_ci.ts', { force: true })" + opencode-peer-compat: runs-on: ubuntu-latest strategy: @@ -75,3 +81,9 @@ jobs: - name: Verify Bun can load with this OpenCode plugin run: bun -e "await import('./src/index.js')" + + - name: Regression #2 - verify local .ts plugin loads with this OpenCode plugin + run: | + node -e "require('node:fs').copyFileSync('src/index.js', 'src/__opencode_loop_local_ci.ts')" + bun -e "await import('./src/__opencode_loop_local_ci.ts')" + node -e "require('node:fs').rmSync('src/__opencode_loop_local_ci.ts', { force: true })" From 933a6353716a8c7ebf3df600767742caa0584c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:27:14 +0300 Subject: [PATCH 02/16] chore: stage Goal Mode README update --- .github/workflows/apply-community-docs-pr.yml | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 .github/workflows/apply-community-docs-pr.yml diff --git a/.github/workflows/apply-community-docs-pr.yml b/.github/workflows/apply-community-docs-pr.yml new file mode 100644 index 00000000..c75dc2dc --- /dev/null +++ b/.github/workflows/apply-community-docs-pr.yml @@ -0,0 +1,111 @@ +name: Apply community regression docs + +on: + pull_request: + branches: + - main + +permissions: + contents: write + +jobs: + apply: + if: github.head_ref == 'chore/community-regressions-goal-readme' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref }} + + - name: Highlight Goal Mode in README + shell: bash + run: | + python - <<'PY' + from pathlib import Path + + path = Path("README.md") + text = path.read_text() + + def replace_once(old, new): + global text + count = text.count(old) + if count != 1: + raise SystemExit(f"expected one README match, found {count}: {old[:80]!r}") + text = text.replace(old, new, 1) + + replace_once( + '''**Claude Code style auto-continue for OpenCode.** + +OpenCode Loop adds a practical `/loop` command and `opencode-loopd` daemon to OpenCode so an agent can keep working after each idle turn instead of waiting for you to type “continue” again. +''', + '''**Claude Code style auto-continue + persistent Goal Mode for OpenCode.** + +OpenCode Loop adds a practical `/loop` command and `opencode-loopd` daemon to OpenCode so an agent can keep working after each idle turn instead of waiting for you to type “continue” again. It also includes experimental `/loop-goal` for outcome-driven work that keeps pursuing a goal across idle turns until it is proven complete, blocked, paused, or stopped by safety limits. +''', + ) + + replace_once( + '''Repository: **ByBrawe/opencode-loop** +NPM package name: **@bybrawe/opencode-loop** + +### Scheduler heartbeat note +''', + '''Repository: **ByBrawe/opencode-loop** +NPM package name: **@bybrawe/opencode-loop** + +## Goal Mode: pursue an outcome, not a timer + +Normal loops answer “what should run again?” **Goal Mode answers “what result should the agent keep pursuing until it is actually done?”** + +```text +/loop-goal --check "npm test" --complete-when-checks-pass fix the failing tests without weakening coverage +``` + +Goal Mode is experimental, but it is designed as a first-class workflow: + +- **Persistent objective:** the goal survives across idle turns instead of relying on a fixed timer. +- **Evidence-gated completion:** the agent cannot finish with a generic “done”; completion needs concrete evidence. +- **Verification gates:** `--check`, acceptance criteria, and `--complete-when-checks-pass` can define what “finished” means. +- **Safety against runaway work:** user interruption, no-progress guards, max turns, and max runtime can pause or stop the goal. +- **Inspectable progress:** Goal Mode records progress/evidence and can write a goal report under `.opencode/opencode-loop/goals/`. + +Use `/loop` for recurring or timer-driven work. Use `/loop-goal` when you care about an **outcome** and want OpenCode to keep working toward it until the result is verified or genuinely blocked. + +See [Goal Mode: persistent objectives](#goal-mode-persistent-objectives) for the full command guide and examples. + +### Scheduler heartbeat note +''', + ) + + replace_once("### The 6 examples most people need", "### The examples most people need") + replace_once("### Experimental Goal Mode", "### Goal Mode: persistent objectives") + replace_once( + '''- **Experimental Goal Mode** with `/loop-goal`, goal status, goal tools, acceptance criteria, and verification checks. +''', + '''- **Experimental Goal Mode** with persistent objectives, evidence-gated completion, acceptance criteria, verification checks, no-progress guards, and goal reports. +''', + ) + replace_once( + '''- OpenCode auto continue +- OpenCode continue automatically +''', + '''- OpenCode auto continue +- OpenCode Goal Mode +- OpenCode persistent goal +- Codex `/goal` style workflow for OpenCode +- OpenCode continue automatically +''', + ) + + path.write_text(text) + PY + + - name: Commit README update + shell: bash + run: | + rm .github/workflows/apply-community-docs-pr.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add README.md .github/workflows/apply-community-docs-pr.yml + git commit -m "docs: make Goal Mode a headline workflow" + git push origin HEAD:${{ github.head_ref }} From a8e3de7ab7a8c3e41c7e25c13ac3e9506ef1478e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:28:19 +0300 Subject: [PATCH 03/16] ci: let docs patch run on branch push --- .github/workflows/apply-community-docs-pr.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/apply-community-docs-pr.yml b/.github/workflows/apply-community-docs-pr.yml index c75dc2dc..799cb9ad 100644 --- a/.github/workflows/apply-community-docs-pr.yml +++ b/.github/workflows/apply-community-docs-pr.yml @@ -1,6 +1,9 @@ name: Apply community regression docs on: + push: + branches: + - chore/community-regressions-goal-readme pull_request: branches: - main @@ -10,12 +13,14 @@ permissions: jobs: apply: - if: github.head_ref == 'chore/community-regressions-goal-readme' + if: github.head_ref == 'chore/community-regressions-goal-readme' || github.ref == 'refs/heads/chore/community-regressions-goal-readme' runs-on: ubuntu-latest + env: + TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} steps: - uses: actions/checkout@v6 with: - ref: ${{ github.head_ref }} + ref: ${{ github.head_ref || github.ref_name }} - name: Highlight Goal Mode in README shell: bash @@ -108,4 +113,4 @@ See [Goal Mode: persistent objectives](#goal-mode-persistent-objectives) for the git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add README.md .github/workflows/apply-community-docs-pr.yml git commit -m "docs: make Goal Mode a headline workflow" - git push origin HEAD:${{ github.head_ref }} + git push origin HEAD:${TARGET_BRANCH} From 4820b632b425480614fc1b3199ef3aa04ddc8cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:31:19 +0300 Subject: [PATCH 04/16] test: cover Goal Mode check auto-completion --- scripts/smoke-test.mjs | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index 59e781cd..af0ea1e8 100644 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -124,6 +124,32 @@ try { await hooks["command.execute.before"]({ command: "loop-clear", sessionID, arguments: "" }, { parts: [] }) + const promptCountBeforeAutoGoal = prompts.length + await hooks["command.execute.before"]({ + command: "loop-goal", + sessionID, + arguments: "--no-now --check \"node -e process.exitCode=0\" --complete-when-checks-pass make the configured checks pass", + }, { parts: [] }) + await hooks["command.execute.before"]({ command: "loop-now", sessionID, arguments: "goal" }, { parts: [] }) + assert.equal( + await waitFor(() => prompts.length === promptCountBeforeAutoGoal + 1), + true, + "a Goal Mode check-gated run must start when forced", + ) + await hooks.event({ event: { type: "session.idle", properties: { sessionID } } }) + let autoCompletedState + for (let attempt = 0; attempt < 100; attempt++) { + autoCompletedState = JSON.parse(await fs.readFile(stateFile, "utf8")) + if (autoCompletedState.jobs[0]?.goalStatus === "completed") break + await new Promise((resolve) => setTimeout(resolve, 10)) + } + assert.equal(autoCompletedState.jobs[0].goalStatus, "completed", "--complete-when-checks-pass must complete the goal after successful configured checks") + assert.equal(autoCompletedState.jobs[0].paused, true) + assert.equal(autoCompletedState.jobs[0].enabled, false) + assert.ok(autoCompletedState.jobs[0].lastGoalChecks?.every((item) => item.code === 0), "auto-completed Goal Mode must persist passing check evidence") + + await hooks["command.execute.before"]({ command: "loop-clear", sessionID, arguments: "" }, { parts: [] }) + const promptCountBeforeBackgroundTool = prompts.length await hooks["tool.execute.before"]({ tool: "bash", sessionID, callID: "call_background" }, { args: {} }) await hooks["command.execute.before"]({ @@ -146,7 +172,7 @@ try { const childSessionID = "ses_background_child" liveStatuses.set(childSessionID, "busy") - await hooks.event({ event: { type: "session.created", properties: { info: { id: childSessionID, parentID: sessionID } } } }) + await hooks.event({ event: { type: "session.created", properties: { info: { id: childSessionID, parentID: sessionID } } }) await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "busy" } } } }) const promptCountBeforeBackgroundChild = prompts.length await hooks["command.execute.before"]({ @@ -158,7 +184,7 @@ try { assert.equal(prompts.length, promptCountBeforeBackgroundChild, "a running background child session must keep its parent loop busy") liveStatuses.set(childSessionID, "idle") - await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "idle" } } } }) + await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "idle" } } }) await hooks.event({ event: { type: "session.idle", properties: { sessionID } } }) assert.equal( await waitFor(() => prompts.length === promptCountBeforeBackgroundChild + 1, 5_000), @@ -175,4 +201,4 @@ try { } finally { await hooks?.dispose?.() await fs.rm(directory, { recursive: true, force: true }) -} +} \ No newline at end of file From b85a8f405ec1d143c77533eccef4aaab4f526535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:31:30 +0300 Subject: [PATCH 05/16] ci: label community regression gates clearly --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4cd3930..4b9a9def 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: - name: Verify Bun can load the plugin run: bun -e "await import('./src/index.js')" - - name: Regression #2 - verify local .ts plugin loads in Bun + - name: "Regression #2 - verify local .ts plugin loads in Bun" run: | node -e "require('node:fs').copyFileSync('src/index.js', 'src/__opencode_loop_local_ci.ts')" bun -e "await import('./src/__opencode_loop_local_ci.ts')" @@ -82,7 +82,7 @@ jobs: - name: Verify Bun can load with this OpenCode plugin run: bun -e "await import('./src/index.js')" - - name: Regression #2 - verify local .ts plugin loads with this OpenCode plugin + - name: "Regression #2 - verify local .ts plugin loads with this OpenCode plugin" run: | node -e "require('node:fs').copyFileSync('src/index.js', 'src/__opencode_loop_local_ci.ts')" bun -e "await import('./src/__opencode_loop_local_ci.ts')" From 543727677ccd048443a34bc6e152c809bc0d96c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:32:35 +0300 Subject: [PATCH 06/16] ci: stage Goal Mode README update in PR --- .github/workflows/ci.yml | 106 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b9a9def..5c3ee1a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,3 +87,109 @@ jobs: node -e "require('node:fs').copyFileSync('src/index.js', 'src/__opencode_loop_local_ci.ts')" bun -e "await import('./src/__opencode_loop_local_ci.ts')" node -e "require('node:fs').rmSync('src/__opencode_loop_local_ci.ts', { force: true })" + + apply-goal-docs: + if: github.event_name == 'pull_request' && github.head_ref == 'chore/community-regressions-goal-readme' + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref }} + + - name: Highlight Goal Mode in README + shell: bash + run: | + python - <<'PY' + from pathlib import Path + + path = Path("README.md") + text = path.read_text() + + def replace_once(old, new): + global text + count = text.count(old) + if count != 1: + raise SystemExit(f"expected one README match, found {count}: {old[:80]!r}") + text = text.replace(old, new, 1) + + replace_once( + '''**Claude Code style auto-continue for OpenCode.** + +OpenCode Loop adds a practical `/loop` command and `opencode-loopd` daemon to OpenCode so an agent can keep working after each idle turn instead of waiting for you to type “continue” again. +''', + '''**Claude Code style auto-continue + persistent Goal Mode for OpenCode.** + +OpenCode Loop adds a practical `/loop` command and `opencode-loopd` daemon to OpenCode so an agent can keep working after each idle turn instead of waiting for you to type “continue” again. It also includes experimental `/loop-goal` for outcome-driven work that keeps pursuing a goal across idle turns until it is proven complete, blocked, paused, or stopped by safety limits. +''', + ) + + replace_once( + '''Repository: **ByBrawe/opencode-loop** +NPM package name: **@bybrawe/opencode-loop** + +### Scheduler heartbeat note +''', + '''Repository: **ByBrawe/opencode-loop** +NPM package name: **@bybrawe/opencode-loop** + +## Goal Mode: pursue an outcome, not a timer + +Normal loops answer “what should run again?” **Goal Mode answers “what result should the agent keep pursuing until it is actually done?”** + +```text +/loop-goal --check "npm test" --complete-when-checks-pass fix the failing tests without weakening coverage +``` + +Goal Mode is experimental, but it is designed as a first-class workflow: + +- **Persistent objective:** the goal survives across idle turns instead of relying on a fixed timer. +- **Evidence-gated completion:** the agent cannot finish with a generic “done”; completion needs concrete evidence. +- **Verification gates:** `--check`, acceptance criteria, and `--complete-when-checks-pass` can define what “finished” means. +- **Safety against runaway work:** user interruption, no-progress guards, max turns, and max runtime can pause or stop the goal. +- **Inspectable progress:** Goal Mode records progress/evidence and can write a goal report under `.opencode/opencode-loop/goals/`. + +Use `/loop` for recurring or timer-driven work. Use `/loop-goal` when you care about an **outcome** and want OpenCode to keep working toward it until the result is verified or genuinely blocked. + +See [Goal Mode: persistent objectives](#goal-mode-persistent-objectives) for the full command guide and examples. + +### Scheduler heartbeat note +''', + ) + + replace_once("### The 6 examples most people need", "### The examples most people need") + replace_once("### Experimental Goal Mode", "### Goal Mode: persistent objectives") + replace_once( + '''- **Experimental Goal Mode** with `/loop-goal`, goal status, goal tools, acceptance criteria, and verification checks. +''', + '''- **Experimental Goal Mode** with persistent objectives, evidence-gated completion, acceptance criteria, verification checks, no-progress guards, and goal reports. +''', + ) + replace_once( + '''- OpenCode auto continue +- OpenCode continue automatically +''', + '''- OpenCode auto continue +- OpenCode Goal Mode +- OpenCode persistent goal +- Codex `/goal` style workflow for OpenCode +- OpenCode continue automatically +''', + ) + + path.write_text(text) + PY + + - name: Commit README update + shell: bash + run: | + if git diff --quiet -- README.md; then + echo "README already updated" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add README.md + git commit -m "docs: make Goal Mode a headline workflow" + git push origin HEAD:${{ github.head_ref }} From 5a3ebe162130175fae2b015b314f725344aa182c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:36:09 +0300 Subject: [PATCH 07/16] ci: keep community regression gates permanent --- .github/workflows/ci.yml | 106 --------------------------------------- 1 file changed, 106 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c3ee1a8..4b9a9def 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,109 +87,3 @@ jobs: node -e "require('node:fs').copyFileSync('src/index.js', 'src/__opencode_loop_local_ci.ts')" bun -e "await import('./src/__opencode_loop_local_ci.ts')" node -e "require('node:fs').rmSync('src/__opencode_loop_local_ci.ts', { force: true })" - - apply-goal-docs: - if: github.event_name == 'pull_request' && github.head_ref == 'chore/community-regressions-goal-readme' - permissions: - contents: write - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ github.head_ref }} - - - name: Highlight Goal Mode in README - shell: bash - run: | - python - <<'PY' - from pathlib import Path - - path = Path("README.md") - text = path.read_text() - - def replace_once(old, new): - global text - count = text.count(old) - if count != 1: - raise SystemExit(f"expected one README match, found {count}: {old[:80]!r}") - text = text.replace(old, new, 1) - - replace_once( - '''**Claude Code style auto-continue for OpenCode.** - -OpenCode Loop adds a practical `/loop` command and `opencode-loopd` daemon to OpenCode so an agent can keep working after each idle turn instead of waiting for you to type “continue” again. -''', - '''**Claude Code style auto-continue + persistent Goal Mode for OpenCode.** - -OpenCode Loop adds a practical `/loop` command and `opencode-loopd` daemon to OpenCode so an agent can keep working after each idle turn instead of waiting for you to type “continue” again. It also includes experimental `/loop-goal` for outcome-driven work that keeps pursuing a goal across idle turns until it is proven complete, blocked, paused, or stopped by safety limits. -''', - ) - - replace_once( - '''Repository: **ByBrawe/opencode-loop** -NPM package name: **@bybrawe/opencode-loop** - -### Scheduler heartbeat note -''', - '''Repository: **ByBrawe/opencode-loop** -NPM package name: **@bybrawe/opencode-loop** - -## Goal Mode: pursue an outcome, not a timer - -Normal loops answer “what should run again?” **Goal Mode answers “what result should the agent keep pursuing until it is actually done?”** - -```text -/loop-goal --check "npm test" --complete-when-checks-pass fix the failing tests without weakening coverage -``` - -Goal Mode is experimental, but it is designed as a first-class workflow: - -- **Persistent objective:** the goal survives across idle turns instead of relying on a fixed timer. -- **Evidence-gated completion:** the agent cannot finish with a generic “done”; completion needs concrete evidence. -- **Verification gates:** `--check`, acceptance criteria, and `--complete-when-checks-pass` can define what “finished” means. -- **Safety against runaway work:** user interruption, no-progress guards, max turns, and max runtime can pause or stop the goal. -- **Inspectable progress:** Goal Mode records progress/evidence and can write a goal report under `.opencode/opencode-loop/goals/`. - -Use `/loop` for recurring or timer-driven work. Use `/loop-goal` when you care about an **outcome** and want OpenCode to keep working toward it until the result is verified or genuinely blocked. - -See [Goal Mode: persistent objectives](#goal-mode-persistent-objectives) for the full command guide and examples. - -### Scheduler heartbeat note -''', - ) - - replace_once("### The 6 examples most people need", "### The examples most people need") - replace_once("### Experimental Goal Mode", "### Goal Mode: persistent objectives") - replace_once( - '''- **Experimental Goal Mode** with `/loop-goal`, goal status, goal tools, acceptance criteria, and verification checks. -''', - '''- **Experimental Goal Mode** with persistent objectives, evidence-gated completion, acceptance criteria, verification checks, no-progress guards, and goal reports. -''', - ) - replace_once( - '''- OpenCode auto continue -- OpenCode continue automatically -''', - '''- OpenCode auto continue -- OpenCode Goal Mode -- OpenCode persistent goal -- Codex `/goal` style workflow for OpenCode -- OpenCode continue automatically -''', - ) - - path.write_text(text) - PY - - - name: Commit README update - shell: bash - run: | - if git diff --quiet -- README.md; then - echo "README already updated" - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add README.md - git commit -m "docs: make Goal Mode a headline workflow" - git push origin HEAD:${{ github.head_ref }} From 6a7bba361da33f8bca3a8d407407420f7d23aef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:36:27 +0300 Subject: [PATCH 08/16] chore: remove temporary docs workflow --- .github/workflows/apply-community-docs-pr.yml | 116 ------------------ 1 file changed, 116 deletions(-) delete mode 100644 .github/workflows/apply-community-docs-pr.yml diff --git a/.github/workflows/apply-community-docs-pr.yml b/.github/workflows/apply-community-docs-pr.yml deleted file mode 100644 index 799cb9ad..00000000 --- a/.github/workflows/apply-community-docs-pr.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: Apply community regression docs - -on: - push: - branches: - - chore/community-regressions-goal-readme - pull_request: - branches: - - main - -permissions: - contents: write - -jobs: - apply: - if: github.head_ref == 'chore/community-regressions-goal-readme' || github.ref == 'refs/heads/chore/community-regressions-goal-readme' - runs-on: ubuntu-latest - env: - TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ github.head_ref || github.ref_name }} - - - name: Highlight Goal Mode in README - shell: bash - run: | - python - <<'PY' - from pathlib import Path - - path = Path("README.md") - text = path.read_text() - - def replace_once(old, new): - global text - count = text.count(old) - if count != 1: - raise SystemExit(f"expected one README match, found {count}: {old[:80]!r}") - text = text.replace(old, new, 1) - - replace_once( - '''**Claude Code style auto-continue for OpenCode.** - -OpenCode Loop adds a practical `/loop` command and `opencode-loopd` daemon to OpenCode so an agent can keep working after each idle turn instead of waiting for you to type “continue” again. -''', - '''**Claude Code style auto-continue + persistent Goal Mode for OpenCode.** - -OpenCode Loop adds a practical `/loop` command and `opencode-loopd` daemon to OpenCode so an agent can keep working after each idle turn instead of waiting for you to type “continue” again. It also includes experimental `/loop-goal` for outcome-driven work that keeps pursuing a goal across idle turns until it is proven complete, blocked, paused, or stopped by safety limits. -''', - ) - - replace_once( - '''Repository: **ByBrawe/opencode-loop** -NPM package name: **@bybrawe/opencode-loop** - -### Scheduler heartbeat note -''', - '''Repository: **ByBrawe/opencode-loop** -NPM package name: **@bybrawe/opencode-loop** - -## Goal Mode: pursue an outcome, not a timer - -Normal loops answer “what should run again?” **Goal Mode answers “what result should the agent keep pursuing until it is actually done?”** - -```text -/loop-goal --check "npm test" --complete-when-checks-pass fix the failing tests without weakening coverage -``` - -Goal Mode is experimental, but it is designed as a first-class workflow: - -- **Persistent objective:** the goal survives across idle turns instead of relying on a fixed timer. -- **Evidence-gated completion:** the agent cannot finish with a generic “done”; completion needs concrete evidence. -- **Verification gates:** `--check`, acceptance criteria, and `--complete-when-checks-pass` can define what “finished” means. -- **Safety against runaway work:** user interruption, no-progress guards, max turns, and max runtime can pause or stop the goal. -- **Inspectable progress:** Goal Mode records progress/evidence and can write a goal report under `.opencode/opencode-loop/goals/`. - -Use `/loop` for recurring or timer-driven work. Use `/loop-goal` when you care about an **outcome** and want OpenCode to keep working toward it until the result is verified or genuinely blocked. - -See [Goal Mode: persistent objectives](#goal-mode-persistent-objectives) for the full command guide and examples. - -### Scheduler heartbeat note -''', - ) - - replace_once("### The 6 examples most people need", "### The examples most people need") - replace_once("### Experimental Goal Mode", "### Goal Mode: persistent objectives") - replace_once( - '''- **Experimental Goal Mode** with `/loop-goal`, goal status, goal tools, acceptance criteria, and verification checks. -''', - '''- **Experimental Goal Mode** with persistent objectives, evidence-gated completion, acceptance criteria, verification checks, no-progress guards, and goal reports. -''', - ) - replace_once( - '''- OpenCode auto continue -- OpenCode continue automatically -''', - '''- OpenCode auto continue -- OpenCode Goal Mode -- OpenCode persistent goal -- Codex `/goal` style workflow for OpenCode -- OpenCode continue automatically -''', - ) - - path.write_text(text) - PY - - - name: Commit README update - shell: bash - run: | - rm .github/workflows/apply-community-docs-pr.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add README.md .github/workflows/apply-community-docs-pr.yml - git commit -m "docs: make Goal Mode a headline workflow" - git push origin HEAD:${TARGET_BRANCH} From 04c2858615391fac23ca70120c923a3156647bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:37:34 +0300 Subject: [PATCH 09/16] test: fix Goal Mode regression syntax --- scripts/smoke-test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index af0ea1e8..190c9a06 100644 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -172,7 +172,7 @@ try { const childSessionID = "ses_background_child" liveStatuses.set(childSessionID, "busy") - await hooks.event({ event: { type: "session.created", properties: { info: { id: childSessionID, parentID: sessionID } } }) + await hooks.event({ event: { type: "session.created", properties: { info: { id: childSessionID, parentID: sessionID } } } }) await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "busy" } } } }) const promptCountBeforeBackgroundChild = prompts.length await hooks["command.execute.before"]({ From 19e422d9595baa3e5b9c147c5acb595f576f576b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:39:27 +0300 Subject: [PATCH 10/16] test: restore background idle event shape --- scripts/smoke-test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index 190c9a06..746f64b9 100644 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -184,7 +184,7 @@ try { assert.equal(prompts.length, promptCountBeforeBackgroundChild, "a running background child session must keep its parent loop busy") liveStatuses.set(childSessionID, "idle") - await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "idle" } } }) + await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "idle" } } } }) await hooks.event({ event: { type: "session.idle", properties: { sessionID } } }) assert.equal( await waitFor(() => prompts.length === promptCountBeforeBackgroundChild + 1, 5_000), @@ -201,4 +201,4 @@ try { } finally { await hooks?.dispose?.() await fs.rm(directory, { recursive: true, force: true }) -} \ No newline at end of file +} From b10a6bbd899b6582d03d55a9583bc696a0baa59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:41:19 +0300 Subject: [PATCH 11/16] test: model completed goal idle lifecycle --- scripts/smoke-test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index 746f64b9..61b93688 100644 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -122,6 +122,7 @@ try { assert.equal(completedState.jobs[0].paused, true) assert.equal(completedState.jobs[0].enabled, false) + await hooks.event({ event: { type: "session.idle", properties: { sessionID } } }) await hooks["command.execute.before"]({ command: "loop-clear", sessionID, arguments: "" }, { parts: [] }) const promptCountBeforeAutoGoal = prompts.length From 91a878e93bf8a8931050314f35a06faeb90047c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:42:28 +0300 Subject: [PATCH 12/16] test: wait for Goal Mode run token before idle --- scripts/smoke-test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index 61b93688..cc15a82a 100644 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -137,6 +137,7 @@ try { true, "a Goal Mode check-gated run must start when forced", ) + await new Promise((resolve) => setTimeout(resolve, 25)) await hooks.event({ event: { type: "session.idle", properties: { sessionID } } }) let autoCompletedState for (let attempt = 0; attempt < 100; attempt++) { From 06d6d167439e713885ffd693277ec42e333dc327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:45:57 +0300 Subject: [PATCH 13/16] test: allow Goal Mode idle debounce to finish --- scripts/smoke-test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index cc15a82a..23d410df 100644 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -140,7 +140,7 @@ try { await new Promise((resolve) => setTimeout(resolve, 25)) await hooks.event({ event: { type: "session.idle", properties: { sessionID } } }) let autoCompletedState - for (let attempt = 0; attempt < 100; attempt++) { + for (let attempt = 0; attempt < 400; attempt++) { autoCompletedState = JSON.parse(await fs.readFile(stateFile, "utf8")) if (autoCompletedState.jobs[0]?.goalStatus === "completed") break await new Promise((resolve) => setTimeout(resolve, 10)) @@ -186,7 +186,7 @@ try { assert.equal(prompts.length, promptCountBeforeBackgroundChild, "a running background child session must keep its parent loop busy") liveStatuses.set(childSessionID, "idle") - await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "idle" } } } }) + await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "idle" } } }) await hooks.event({ event: { type: "session.idle", properties: { sessionID } } }) assert.equal( await waitFor(() => prompts.length === promptCountBeforeBackgroundChild + 1, 5_000), From 507c291c40d309b185e044e917525e3ecc5c96ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:47:11 +0300 Subject: [PATCH 14/16] chore: stage precise smoke regression fix --- .github/workflows/fix-pr12-smoke.yml | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/fix-pr12-smoke.yml diff --git a/.github/workflows/fix-pr12-smoke.yml b/.github/workflows/fix-pr12-smoke.yml new file mode 100644 index 00000000..43ca7fdd --- /dev/null +++ b/.github/workflows/fix-pr12-smoke.yml @@ -0,0 +1,41 @@ +name: Fix PR12 smoke regression + +on: + pull_request: + branches: + - main + +permissions: + contents: write + +jobs: + fix: + if: github.head_ref == 'chore/community-regressions-goal-readme' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref }} + - name: Restore unchanged background regression line + shell: bash + run: | + python - <<'PY' + from pathlib import Path + p = Path('scripts/smoke-test.mjs') + text = p.read_text() + old = ' await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "idle" } } })\n' + new = ' await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "idle" } } } })\n' + if text.count(old) != 1: + raise SystemExit(f'expected one broken child-idle line, found {text.count(old)}') + p.write_text(text.replace(old, new, 1)) + PY + node --check scripts/smoke-test.mjs + - name: Commit precise fix + shell: bash + run: | + rm .github/workflows/fix-pr12-smoke.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add scripts/smoke-test.mjs .github/workflows/fix-pr12-smoke.yml + git commit -m "test: restore background regression event shape" + git push origin HEAD:${{ github.head_ref }} From bd1ff9a8bbe3e6c2a02383e1af666e137257654d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:47:28 +0000 Subject: [PATCH 15/16] test: restore background regression event shape --- .github/workflows/fix-pr12-smoke.yml | 41 ---------------------------- scripts/smoke-test.mjs | 2 +- 2 files changed, 1 insertion(+), 42 deletions(-) delete mode 100644 .github/workflows/fix-pr12-smoke.yml diff --git a/.github/workflows/fix-pr12-smoke.yml b/.github/workflows/fix-pr12-smoke.yml deleted file mode 100644 index 43ca7fdd..00000000 --- a/.github/workflows/fix-pr12-smoke.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Fix PR12 smoke regression - -on: - pull_request: - branches: - - main - -permissions: - contents: write - -jobs: - fix: - if: github.head_ref == 'chore/community-regressions-goal-readme' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ github.head_ref }} - - name: Restore unchanged background regression line - shell: bash - run: | - python - <<'PY' - from pathlib import Path - p = Path('scripts/smoke-test.mjs') - text = p.read_text() - old = ' await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "idle" } } })\n' - new = ' await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "idle" } } } })\n' - if text.count(old) != 1: - raise SystemExit(f'expected one broken child-idle line, found {text.count(old)}') - p.write_text(text.replace(old, new, 1)) - PY - node --check scripts/smoke-test.mjs - - name: Commit precise fix - shell: bash - run: | - rm .github/workflows/fix-pr12-smoke.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add scripts/smoke-test.mjs .github/workflows/fix-pr12-smoke.yml - git commit -m "test: restore background regression event shape" - git push origin HEAD:${{ github.head_ref }} diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index 23d410df..f45bffce 100644 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -186,7 +186,7 @@ try { assert.equal(prompts.length, promptCountBeforeBackgroundChild, "a running background child session must keep its parent loop busy") liveStatuses.set(childSessionID, "idle") - await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "idle" } } }) + await hooks.event({ event: { type: "session.status", properties: { sessionID: childSessionID, status: { type: "idle" } } } }) await hooks.event({ event: { type: "session.idle", properties: { sessionID } } }) assert.equal( await waitFor(() => prompts.length === promptCountBeforeBackgroundChild + 1, 5_000), From efb7d753e5775b2ff5c506c325e64d74e86831cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 20:48:20 +0300 Subject: [PATCH 16/16] test: document community regression gate --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b9a9def..1b81aefc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,7 @@ jobs: - name: Verify Bun can load the plugin run: bun -e "await import('./src/index.js')" + # Issue #2 happened in the installer-created .ts plugin shape, not the source .js file. - name: "Regression #2 - verify local .ts plugin loads in Bun" run: | node -e "require('node:fs').copyFileSync('src/index.js', 'src/__opencode_loop_local_ci.ts')"