From 75497b5b540076717744fdcda9a44faaf523bf4e Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sat, 9 May 2026 15:49:25 -0700 Subject: [PATCH 1/4] [luv-336] fix: route OpenCode project pages by encoded cwd, not basename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align `ProjectFolder.name` for OpenCode projects with the URL-slug encoding every other CLI uses (`encodeFolderName(worktree)`), replacing the prior `proj.name`/`basename(worktree)` fallback. Fixes a 404 on the project page for OpenCode-only sessions (e.g. /project/dev-purge → 404) while session URLs already worked via `encodeCwdForUrl(cwd)`. As a side benefit, OpenCode projects now merge with their Claude/Codex/Copilot/ Cursor/Pi/Gemini siblings into one row in `mergeProjectFolders`. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 3 +++ __tests__/lib/opencode-projects.test.ts | 7 ++++--- lib/opencode-projects.ts | 16 +++++++++------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17e2a1cc..7df0180a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Fixes +- `lib/opencode-projects.ts`: align `ProjectFolder.name` for OpenCode projects with the URL-slug encoding every other CLI uses (`encodeFolderName(worktree)`, e.g. `-home-nivedit-dev-purge`), replacing the previous `proj.name`/`basename(worktree)` fallback chain that produced bare names like `dev-purge`. The `[name]` route handler resolves OpenCode sessions via `encodeFolderName(p.worktree) === name` in `getOpenCodeSessionsByEncodedName`, and the Projects-list link in `app/components/project-list.tsx:330` is built from `folder.name` — so an OpenCode-only project at `/home/nivedit/dev-purge` was emitting `/project/dev-purge`, which 404'd, while the session-row URL built elsewhere via `encodeCwdForUrl(cwd)` correctly used `-home-nivedit-dev-purge` and worked. Side benefit: `mergeProjectFolders` (`lib/projects.ts:107`) keys by `f.name`, so OpenCode projects now merge with their Claude / Codex / Copilot / Cursor / Pi / Gemini siblings into a single row with multiple CLI badges instead of appearing as two separate rows. Display via `decodeFolderName(folder.name)` in the Projects table now shows the full path (`/home/nivedit/dev-purge`) instead of the prior `dev/purge`, matching every other CLI. Three test assertions in `__tests__/lib/opencode-projects.test.ts` updated to pin the encoded form (`-repo`, `-other`); existing `getOpenCodeSessionsByEncodedName` tests already asserted the correct contract and didn't need changes (#336). + ## 0.0.10-beta.9 — 2026-05-09 ### Features diff --git a/__tests__/lib/opencode-projects.test.ts b/__tests__/lib/opencode-projects.test.ts index d7c12191..92583741 100644 --- a/__tests__/lib/opencode-projects.test.ts +++ b/__tests__/lib/opencode-projects.test.ts @@ -69,11 +69,12 @@ describe("getOpenCodeProjects", () => { const projects = await getOpenCodeProjects(); expect(projects).toHaveLength(2); // Newest first — p1 has time_updated=200, p2 has 50. - expect(projects[0].name).toBe("repo"); // basename(/repo) — name was null + // `name` is the URL slug = encodeFolderName(worktree) — matches every other CLI. + expect(projects[0].name).toBe("-repo"); expect(projects[0].path).toBe("/repo"); expect(projects[0].cli).toEqual(["opencode"]); expect(projects[0].lastModified.getTime()).toBe(200); - expect(projects[1].name).toBe("Other Project"); + expect(projects[1].name).toBe("-other"); expect(projects[1].lastModified.getTime()).toBe(50); }); @@ -86,7 +87,7 @@ describe("getOpenCodeProjects", () => { ]); const projects = await getOpenCodeProjects(); expect(projects).toHaveLength(1); - expect(projects[0].name).toBe("Empty Project"); + expect(projects[0].name).toBe("-repo"); expect(projects[0].lastModified.getTime()).toBe(10); }); diff --git a/lib/opencode-projects.ts b/lib/opencode-projects.ts index 30e7ea26..b45106b1 100644 --- a/lib/opencode-projects.ts +++ b/lib/opencode-projects.ts @@ -22,7 +22,6 @@ * https://opencode.ai/docs/plugins/ (plugin model context) */ import { execFileSync } from "node:child_process"; -import { basename } from "node:path"; import { encodeFolderName } from "./paths"; import type { ProjectFolder, SessionFile } from "./projects"; import { runtimeCache } from "./runtime-cache"; @@ -91,10 +90,11 @@ function readProjectRows(): OpenCodeProjectRow[] | null { /** * Group sessions by `project_id` and produce one ProjectFolder per project. - * The folder name comes from `project.name` when set, else `basename(worktree)`, - * else the project_id (last-resort). `lastModified` is the max session - * `time_updated` for that project (or the project's own time_updated if no - * sessions exist yet). + * The folder name is `encodeFolderName(worktree)` (matches every other CLI's + * URL-slug encoding so the dashboard's `/project/[name]` route resolves), or + * the project_id when no worktree is recorded. `lastModified` is the max + * session `time_updated` for that project (or the project's own time_updated + * if no sessions exist yet). */ export async function getOpenCodeProjects(): Promise { const sessions = readSessionRows(); @@ -122,13 +122,15 @@ export async function getOpenCodeProjects(): Promise { // Emit one ProjectFolder per project that has at least one session OR a // project row (covers projects opencode knows about but hasn't run yet). + // `name` is the dashboard's URL slug — must be `encodeFolderName(cwd)` to + // match every other CLI (and the resolver in `getOpenCodeSessionsByEncodedName`). const seen = new Set(); const out: ProjectFolder[] = []; for (const [projectId, group] of groups) { seen.add(projectId); const proj = projectMap.get(projectId); const worktree = proj?.worktree ?? group.rows[0]?.directory ?? null; - const name = proj?.name?.trim() || (worktree ? basename(worktree) : projectId); + const name = worktree ? encodeFolderName(worktree) : projectId; const path = worktree ?? ""; const lastModified = new Date(Math.max(group.latest, proj?.time_updated ?? 0)); out.push({ @@ -143,7 +145,7 @@ export async function getOpenCodeProjects(): Promise { for (const p of projects ?? []) { if (seen.has(p.id)) continue; const worktree = p.worktree ?? ""; - const name = p.name?.trim() || (worktree ? basename(worktree) : p.id); + const name = worktree ? encodeFolderName(worktree) : p.id; const lastModified = new Date(p.time_updated); out.push({ name, From 3284b8e395625ffec725c4a1ee115cadfa24c0a0 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sat, 9 May 2026 16:08:26 -0700 Subject: [PATCH 2/4] [luv-336] feat: drop `## Unreleased`; ship every PR release-ready MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switches the changelog convention so every feature PR's entry lands directly under a versioned `## ` section (matching package.json's current version + today's date). The `## Unreleased` heading is gone — feature PRs already ship release-ready, so a separate cut PR is no longer required for the changelog rename. - `.failproofai/policies/workflow-policies.mjs`: - new `release-prep-check` policy fires on `gh pr create` and instructs the agent to put entries under a dated versioned heading, creating that heading above the previous version's section if needed - `changelog-check` updated to drop the `## Unreleased` reference - `CHANGELOG.md`: drop the empty `## Unreleased` heading; this PR's entry now sits directly under `## 0.0.10-beta.10 — 2026-05-09` - `CLAUDE.md`: update the Changelog section to describe the new no-`Unreleased` flow Version bumps in `package.json` remain reserved for `luv-cut-*` PRs by the existing `block-version-bumps` policy. Co-Authored-By: Claude Opus 4.7 --- .failproofai/policies/workflow-policies.mjs | 29 ++++++++++++++++++--- CHANGELOG.md | 5 ++-- CLAUDE.md | 11 +++++--- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/.failproofai/policies/workflow-policies.mjs b/.failproofai/policies/workflow-policies.mjs index 80e874dd..cae90798 100644 --- a/.failproofai/policies/workflow-policies.mjs +++ b/.failproofai/policies/workflow-policies.mjs @@ -16,9 +16,10 @@ customPolicies.add({ if (/git\s+commit/.test(cmd)) { return instruct( "Check whether CHANGELOG.md needs an update for this commit. " + - "Every PR must include an entry under the `## Unreleased` section. " + - "Use the appropriate subsection: Features, Fixes, Docs, or Dependencies.\n" + - "Check the version in package.json and ensure the changelog entry matches the current version." + "Every PR must include an entry under the current `## ` section " + + "(matching `version` in package.json + today's date). If that section does not exist yet, " + + "create it above the previous version's section — there is no `## Unreleased` section. " + + "Use the appropriate subsection: Features, Fixes, Docs, or Dependencies." ); } return allow(); @@ -61,3 +62,25 @@ customPolicies.add({ return allow(); }, }); + +// On `gh pr create`, instruct the agent to ensure CHANGELOG entries live +// under a versioned `## ` section so the PR ships +// release-ready. There is no `## Unreleased` section. +customPolicies.add({ + name: "release-prep-check", + description: + "On `gh pr create`, instruct the agent to ensure CHANGELOG entries are under a versioned `## ` section", + match: { events: ["PreToolUse"] }, + fn: async (ctx) => { + if (ctx.toolName !== "Bash") return allow(); + const cmd = String(ctx.toolInput?.command ?? ""); + if (!/\bgh\s+pr\s+create\b/.test(cmd)) return allow(); + return instruct( + "Before creating the PR, ensure CHANGELOG.md entries land under a versioned section so the PR ships release-ready:\n" + + " 1. Read `version` from package.json (e.g. `0.0.10-beta.10`).\n" + + " 2. Ensure your changelog entries are under a `## ` heading. If that heading does not exist yet, create it above the previous version's section. There is NO `## Unreleased` section — entries always go under a dated, versioned heading.\n" + + " 3. If you are on a `luv-cut-X.Y.Z` branch, the cut PR handles version bump itself.\n" + + " 4. Do NOT bump `package.json`'s `version` outside of `luv-cut-*` branches — that is enforced by `block-version-bumps`." + ); + }, +}); diff --git a/CHANGELOG.md b/CHANGELOG.md index 7df0180a..9cc94227 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # Changelog -## Unreleased +## 0.0.10-beta.10 — 2026-05-09 ### Fixes -- `lib/opencode-projects.ts`: align `ProjectFolder.name` for OpenCode projects with the URL-slug encoding every other CLI uses (`encodeFolderName(worktree)`, e.g. `-home-nivedit-dev-purge`), replacing the previous `proj.name`/`basename(worktree)` fallback chain that produced bare names like `dev-purge`. The `[name]` route handler resolves OpenCode sessions via `encodeFolderName(p.worktree) === name` in `getOpenCodeSessionsByEncodedName`, and the Projects-list link in `app/components/project-list.tsx:330` is built from `folder.name` — so an OpenCode-only project at `/home/nivedit/dev-purge` was emitting `/project/dev-purge`, which 404'd, while the session-row URL built elsewhere via `encodeCwdForUrl(cwd)` correctly used `-home-nivedit-dev-purge` and worked. Side benefit: `mergeProjectFolders` (`lib/projects.ts:107`) keys by `f.name`, so OpenCode projects now merge with their Claude / Codex / Copilot / Cursor / Pi / Gemini siblings into a single row with multiple CLI badges instead of appearing as two separate rows. Display via `decodeFolderName(folder.name)` in the Projects table now shows the full path (`/home/nivedit/dev-purge`) instead of the prior `dev/purge`, matching every other CLI. Three test assertions in `__tests__/lib/opencode-projects.test.ts` updated to pin the encoded form (`-repo`, `-other`); existing `getOpenCodeSessionsByEncodedName` tests already asserted the correct contract and didn't need changes (#336). +- `lib/opencode-projects.ts`: align `ProjectFolder.name` for OpenCode projects with the URL-slug encoding every other CLI uses (`encodeFolderName(worktree)`, e.g. `-home-nivedit-dev-purge`), replacing the previous `proj.name`/`basename(worktree)` fallback chain that produced bare names like `dev-purge`. The `[name]` route handler resolves OpenCode sessions via `encodeFolderName(p.worktree) === name` in `getOpenCodeSessionsByEncodedName`, and the Projects-list link in `app/components/project-list.tsx:330` is built from `folder.name` — so an OpenCode-only project at `/home/nivedit/dev-purge` was emitting `/project/dev-purge`, which 404'd, while the session-row URL built elsewhere via `encodeCwdForUrl(cwd)` correctly used `-home-nivedit-dev-purge` and worked. Side benefit: `mergeProjectFolders` (`lib/projects.ts:107`) keys by `f.name`, so OpenCode projects now merge with their Claude / Codex / Copilot / Cursor / Pi / Gemini siblings into a single row with multiple CLI badges instead of appearing as two separate rows. Display via `decodeFolderName(folder.name)` in the Projects table now shows the full path (`/home/nivedit/dev-purge`) instead of the prior `dev/purge`, matching every other CLI. Three test assertions in `__tests__/lib/opencode-projects.test.ts` updated to pin the encoded form (`-repo`, `-other`); existing `getOpenCodeSessionsByEncodedName` tests already asserted the correct contract and didn't need changes (#335). +- `.failproofai/policies/workflow-policies.mjs`: new `release-prep-check` policy fires on `gh pr create` and instructs the agent to ensure CHANGELOG entries land under a versioned `## ` heading (creating that heading above the previous version's section if needed) so each merged feature PR ships release-ready. The existing `changelog-check` policy is updated to match — the `## Unreleased` section is gone; entries always go under a dated, versioned heading. Version bumps in `package.json` remain reserved for `luv-cut-*` PRs by the existing `block-version-bumps` policy. ## 0.0.10-beta.9 — 2026-05-09 diff --git a/CLAUDE.md b/CLAUDE.md index e1d68415..95872b29 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -425,7 +425,13 @@ examples/ Sample custom policy files ## Changelog Every PR **must** include an update to `CHANGELOG.md`. Add your entry under the -`## Unreleased` section at the top. Use the appropriate subsection: +current `## ` section at the top, where `` matches +`version` in `package.json` and `` is today's date. If that section +does not exist yet, create it above the previous version's section. There is **no** +`## Unreleased` section — entries always go under a dated, versioned heading, so +each feature PR ships release-ready. + +Use the appropriate subsection: - **Features** for new functionality - **Fixes** for bug fixes @@ -433,8 +439,7 @@ Every PR **must** include an update to `CHANGELOG.md`. Add your entry under the - **Dependencies** for dependency bumps Each entry should be a single line: a short description followed by the PR number -(e.g. `- Add foo support (#123)`). When a release is cut, the `Unreleased` section gets -renamed to the version and date, and a fresh `## Unreleased` heading is added. +(e.g. `- Add foo support (#123)`). ## Version bumps From c1e68c96735bc5dd3916dd12f9f95e8f748f9780 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sat, 9 May 2026 16:10:27 -0700 Subject: [PATCH 3/4] [luv-336] fix: anchor workflow-policy regexes to command boundaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `release-prep-check`, `changelog-check`, `docs-check`, and `pr-description-check` policies used unanchored regexes against the Bash command string, so the literal phrases (`gh pr create`, `git commit`, `git push`) matched anywhere — including inside HEREDOC bodies or quoted arguments. Concrete failure: editing a PR body that contained the words "gh pr create" inside backticks fired the new `release-prep-check` policy on the `gh pr edit` invocation. Extracts a shared `matchesCommand(cmd, verbPattern)` helper that anchors the verb-phrase match to a command boundary (`^`, `;`, `&&`, `||`, `|`, or newline). All four policies route through it. Co-Authored-By: Claude Opus 4.7 --- .failproofai/policies/workflow-policies.mjs | 21 +++++++++++++++++---- CHANGELOG.md | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.failproofai/policies/workflow-policies.mjs b/.failproofai/policies/workflow-policies.mjs index cae90798..374d57d8 100644 --- a/.failproofai/policies/workflow-policies.mjs +++ b/.failproofai/policies/workflow-policies.mjs @@ -5,6 +5,19 @@ */ import { customPolicies, allow, instruct } from "failproofai"; +/** + * Match `` only when it appears at a command boundary (start of + * string, `;`, `&&`, `||`, `|`, or newline). Avoids false-positive matches + * when the literal phrase appears inside a HEREDOC or a quoted argument + * (e.g. `gh pr edit --body "...gh pr create..."` would previously trigger + * `release-prep-check` because the regex matched anywhere in the string). + */ +function matchesCommand(cmd, verbPhrasePattern) { + return new RegExp( + String.raw`(?:^|[;\n|]|&&|\|\|)\s*` + verbPhrasePattern + String.raw`\b`, + ).test(cmd); +} + // Remind to update CHANGELOG before committing customPolicies.add({ name: "changelog-check", @@ -13,7 +26,7 @@ customPolicies.add({ fn: async (ctx) => { if (ctx.toolName !== "Bash") return allow(); const cmd = String(ctx.toolInput?.command ?? ""); - if (/git\s+commit/.test(cmd)) { + if (matchesCommand(cmd, String.raw`git\s+commit`)) { return instruct( "Check whether CHANGELOG.md needs an update for this commit. " + "Every PR must include an entry under the current `## ` section " + @@ -34,7 +47,7 @@ customPolicies.add({ fn: async (ctx) => { if (ctx.toolName !== "Bash") return allow(); const cmd = String(ctx.toolInput?.command ?? ""); - if (/git\s+commit/.test(cmd)) { + if (matchesCommand(cmd, String.raw`git\s+commit`)) { return instruct( "Check whether documentation needs updating for this change. " + "Consider: docs/*.mdx files, README.md, and examples/ directory. " + @@ -53,7 +66,7 @@ customPolicies.add({ fn: async (ctx) => { if (ctx.toolName !== "Bash") return allow(); const cmd = String(ctx.toolInput?.command ?? ""); - if (/git\s+push/.test(cmd)) { + if (matchesCommand(cmd, String.raw`git\s+push`)) { return instruct( "After pushing, check if there is an open PR for this branch. " + "If so, update the PR description to reflect the latest changes." @@ -74,7 +87,7 @@ customPolicies.add({ fn: async (ctx) => { if (ctx.toolName !== "Bash") return allow(); const cmd = String(ctx.toolInput?.command ?? ""); - if (!/\bgh\s+pr\s+create\b/.test(cmd)) return allow(); + if (!matchesCommand(cmd, String.raw`gh\s+pr\s+create`)) return allow(); return instruct( "Before creating the PR, ensure CHANGELOG.md entries land under a versioned section so the PR ships release-ready:\n" + " 1. Read `version` from package.json (e.g. `0.0.10-beta.10`).\n" + diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cc94227..7dcd28fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixes - `lib/opencode-projects.ts`: align `ProjectFolder.name` for OpenCode projects with the URL-slug encoding every other CLI uses (`encodeFolderName(worktree)`, e.g. `-home-nivedit-dev-purge`), replacing the previous `proj.name`/`basename(worktree)` fallback chain that produced bare names like `dev-purge`. The `[name]` route handler resolves OpenCode sessions via `encodeFolderName(p.worktree) === name` in `getOpenCodeSessionsByEncodedName`, and the Projects-list link in `app/components/project-list.tsx:330` is built from `folder.name` — so an OpenCode-only project at `/home/nivedit/dev-purge` was emitting `/project/dev-purge`, which 404'd, while the session-row URL built elsewhere via `encodeCwdForUrl(cwd)` correctly used `-home-nivedit-dev-purge` and worked. Side benefit: `mergeProjectFolders` (`lib/projects.ts:107`) keys by `f.name`, so OpenCode projects now merge with their Claude / Codex / Copilot / Cursor / Pi / Gemini siblings into a single row with multiple CLI badges instead of appearing as two separate rows. Display via `decodeFolderName(folder.name)` in the Projects table now shows the full path (`/home/nivedit/dev-purge`) instead of the prior `dev/purge`, matching every other CLI. Three test assertions in `__tests__/lib/opencode-projects.test.ts` updated to pin the encoded form (`-repo`, `-other`); existing `getOpenCodeSessionsByEncodedName` tests already asserted the correct contract and didn't need changes (#335). -- `.failproofai/policies/workflow-policies.mjs`: new `release-prep-check` policy fires on `gh pr create` and instructs the agent to ensure CHANGELOG entries land under a versioned `## ` heading (creating that heading above the previous version's section if needed) so each merged feature PR ships release-ready. The existing `changelog-check` policy is updated to match — the `## Unreleased` section is gone; entries always go under a dated, versioned heading. Version bumps in `package.json` remain reserved for `luv-cut-*` PRs by the existing `block-version-bumps` policy. +- `.failproofai/policies/workflow-policies.mjs`: new `release-prep-check` policy fires on `gh pr create` and instructs the agent to ensure CHANGELOG entries land under a versioned `## ` heading (creating that heading above the previous version's section if needed) so each merged feature PR ships release-ready. The existing `changelog-check` policy is updated to match — the `## Unreleased` section is gone; entries always go under a dated, versioned heading. Version bumps in `package.json` remain reserved for `luv-cut-*` PRs by the existing `block-version-bumps` policy. All four policies in this file (`changelog-check`, `docs-check`, `pr-description-check`, `release-prep-check`) now route through a shared `matchesCommand` helper that anchors verb-phrase matches to a command boundary (`^`, `;`, `&&`, `||`, `|`, or newline), so the literal phrases `git commit` / `git push` / `gh pr create` no longer false-positive when they appear inside HEREDOC bodies or quoted arguments (e.g. `gh pr edit --body "...gh pr create..."` no longer triggers `release-prep-check`). ## 0.0.10-beta.9 — 2026-05-09 From d3bc7738aac02dc07d809e8a016c2fd923c9e647 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sat, 9 May 2026 16:15:30 -0700 Subject: [PATCH 4/4] [luv-336] docs: trim 0.0.10-beta.10 changelog entries to one-liners Per CLAUDE.md:425-435 the changelog convention is one short line per entry with the PR number; the prior verbose paragraph form has been trimmed to match. Implementation detail lives in commit messages and the PR description. CodeRabbit flagged the previous verbosity on PR #335. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dcd28fd..00e6d746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,8 @@ ## 0.0.10-beta.10 — 2026-05-09 ### Fixes -- `lib/opencode-projects.ts`: align `ProjectFolder.name` for OpenCode projects with the URL-slug encoding every other CLI uses (`encodeFolderName(worktree)`, e.g. `-home-nivedit-dev-purge`), replacing the previous `proj.name`/`basename(worktree)` fallback chain that produced bare names like `dev-purge`. The `[name]` route handler resolves OpenCode sessions via `encodeFolderName(p.worktree) === name` in `getOpenCodeSessionsByEncodedName`, and the Projects-list link in `app/components/project-list.tsx:330` is built from `folder.name` — so an OpenCode-only project at `/home/nivedit/dev-purge` was emitting `/project/dev-purge`, which 404'd, while the session-row URL built elsewhere via `encodeCwdForUrl(cwd)` correctly used `-home-nivedit-dev-purge` and worked. Side benefit: `mergeProjectFolders` (`lib/projects.ts:107`) keys by `f.name`, so OpenCode projects now merge with their Claude / Codex / Copilot / Cursor / Pi / Gemini siblings into a single row with multiple CLI badges instead of appearing as two separate rows. Display via `decodeFolderName(folder.name)` in the Projects table now shows the full path (`/home/nivedit/dev-purge`) instead of the prior `dev/purge`, matching every other CLI. Three test assertions in `__tests__/lib/opencode-projects.test.ts` updated to pin the encoded form (`-repo`, `-other`); existing `getOpenCodeSessionsByEncodedName` tests already asserted the correct contract and didn't need changes (#335). -- `.failproofai/policies/workflow-policies.mjs`: new `release-prep-check` policy fires on `gh pr create` and instructs the agent to ensure CHANGELOG entries land under a versioned `## ` heading (creating that heading above the previous version's section if needed) so each merged feature PR ships release-ready. The existing `changelog-check` policy is updated to match — the `## Unreleased` section is gone; entries always go under a dated, versioned heading. Version bumps in `package.json` remain reserved for `luv-cut-*` PRs by the existing `block-version-bumps` policy. All four policies in this file (`changelog-check`, `docs-check`, `pr-description-check`, `release-prep-check`) now route through a shared `matchesCommand` helper that anchors verb-phrase matches to a command boundary (`^`, `;`, `&&`, `||`, `|`, or newline), so the literal phrases `git commit` / `git push` / `gh pr create` no longer false-positive when they appear inside HEREDOC bodies or quoted arguments (e.g. `gh pr edit --body "...gh pr create..."` no longer triggers `release-prep-check`). +- Route OpenCode project pages by encoded cwd (`encodeFolderName(worktree)`) instead of opencode's project name / basename, fixing the dashboard `/project/` 404 for OpenCode-only sessions and merging same-cwd OpenCode + other-CLI rows on the Projects page (#335). +- `.failproofai/policies/workflow-policies.mjs`: drop the `## Unreleased` section; new `release-prep-check` policy + updated `changelog-check` instruct the agent to put entries under a dated `## ` heading so each PR ships release-ready, and all four workflow policies now anchor command-phrase matches to shell boundaries to avoid false-positives from HEREDOC bodies (#335). ## 0.0.10-beta.9 — 2026-05-09