Skip to content

Commit 3703ff0

Browse files
NiveditJainclaude
andauthored
[luv-336] fix: route OpenCode project pages by encoded cwd; drop ## Unreleased from changelog (#335)
* [luv-336] fix: route OpenCode project pages by encoded cwd, not basename 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 <noreply@anthropic.com> * [luv-336] feat: drop `## Unreleased`; ship every PR release-ready Switches the changelog convention so every feature PR's entry lands directly under a versioned `## <version> — <YYYY-MM-DD>` 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 <noreply@anthropic.com> * [luv-336] fix: anchor workflow-policy regexes to command boundaries 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 <noreply@anthropic.com> * [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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 4f41a97 commit 3703ff0

5 files changed

Lines changed: 68 additions & 20 deletions

File tree

.failproofai/policies/workflow-policies.mjs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@
55
*/
66
import { customPolicies, allow, instruct } from "failproofai";
77

8+
/**
9+
* Match `<verb-phrase>` only when it appears at a command boundary (start of
10+
* string, `;`, `&&`, `||`, `|`, or newline). Avoids false-positive matches
11+
* when the literal phrase appears inside a HEREDOC or a quoted argument
12+
* (e.g. `gh pr edit --body "...gh pr create..."` would previously trigger
13+
* `release-prep-check` because the regex matched anywhere in the string).
14+
*/
15+
function matchesCommand(cmd, verbPhrasePattern) {
16+
return new RegExp(
17+
String.raw`(?:^|[;\n|]|&&|\|\|)\s*` + verbPhrasePattern + String.raw`\b`,
18+
).test(cmd);
19+
}
20+
821
// Remind to update CHANGELOG before committing
922
customPolicies.add({
1023
name: "changelog-check",
@@ -13,12 +26,13 @@ customPolicies.add({
1326
fn: async (ctx) => {
1427
if (ctx.toolName !== "Bash") return allow();
1528
const cmd = String(ctx.toolInput?.command ?? "");
16-
if (/git\s+commit/.test(cmd)) {
29+
if (matchesCommand(cmd, String.raw`git\s+commit`)) {
1730
return instruct(
1831
"Check whether CHANGELOG.md needs an update for this commit. " +
19-
"Every PR must include an entry under the `## Unreleased` section. " +
20-
"Use the appropriate subsection: Features, Fixes, Docs, or Dependencies.\n" +
21-
"Check the version in package.json and ensure the changelog entry matches the current version."
32+
"Every PR must include an entry under the current `## <version> — <YYYY-MM-DD>` section " +
33+
"(matching `version` in package.json + today's date). If that section does not exist yet, " +
34+
"create it above the previous version's section — there is no `## Unreleased` section. " +
35+
"Use the appropriate subsection: Features, Fixes, Docs, or Dependencies."
2236
);
2337
}
2438
return allow();
@@ -33,7 +47,7 @@ customPolicies.add({
3347
fn: async (ctx) => {
3448
if (ctx.toolName !== "Bash") return allow();
3549
const cmd = String(ctx.toolInput?.command ?? "");
36-
if (/git\s+commit/.test(cmd)) {
50+
if (matchesCommand(cmd, String.raw`git\s+commit`)) {
3751
return instruct(
3852
"Check whether documentation needs updating for this change. " +
3953
"Consider: docs/*.mdx files, README.md, and examples/ directory. " +
@@ -52,7 +66,7 @@ customPolicies.add({
5266
fn: async (ctx) => {
5367
if (ctx.toolName !== "Bash") return allow();
5468
const cmd = String(ctx.toolInput?.command ?? "");
55-
if (/git\s+push/.test(cmd)) {
69+
if (matchesCommand(cmd, String.raw`git\s+push`)) {
5670
return instruct(
5771
"After pushing, check if there is an open PR for this branch. " +
5872
"If so, update the PR description to reflect the latest changes."
@@ -61,3 +75,25 @@ customPolicies.add({
6175
return allow();
6276
},
6377
});
78+
79+
// On `gh pr create`, instruct the agent to ensure CHANGELOG entries live
80+
// under a versioned `## <version> — <date>` section so the PR ships
81+
// release-ready. There is no `## Unreleased` section.
82+
customPolicies.add({
83+
name: "release-prep-check",
84+
description:
85+
"On `gh pr create`, instruct the agent to ensure CHANGELOG entries are under a versioned `## <version> — <date>` section",
86+
match: { events: ["PreToolUse"] },
87+
fn: async (ctx) => {
88+
if (ctx.toolName !== "Bash") return allow();
89+
const cmd = String(ctx.toolInput?.command ?? "");
90+
if (!matchesCommand(cmd, String.raw`gh\s+pr\s+create`)) return allow();
91+
return instruct(
92+
"Before creating the PR, ensure CHANGELOG.md entries land under a versioned section so the PR ships release-ready:\n" +
93+
" 1. Read `version` from package.json (e.g. `0.0.10-beta.10`).\n" +
94+
" 2. Ensure your changelog entries are under a `## <version> — <today's date in YYYY-MM-DD>` 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" +
95+
" 3. If you are on a `luv-cut-X.Y.Z` branch, the cut PR handles version bump itself.\n" +
96+
" 4. Do NOT bump `package.json`'s `version` outside of `luv-cut-*` branches — that is enforced by `block-version-bumps`."
97+
);
98+
},
99+
});

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Changelog
22

3-
## Unreleased
3+
## 0.0.10-beta.10 — 2026-05-09
4+
5+
### Fixes
6+
- Route OpenCode project pages by encoded cwd (`encodeFolderName(worktree)`) instead of opencode's project name / basename, fixing the dashboard `/project/<slug>` 404 for OpenCode-only sessions and merging same-cwd OpenCode + other-CLI rows on the Projects page (#335).
7+
- `.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 `## <version> — <YYYY-MM-DD>` 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).
48

59
## 0.0.10-beta.9 — 2026-05-09
610

CLAUDE.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -425,16 +425,21 @@ examples/ Sample custom policy files
425425
## Changelog
426426

427427
Every PR **must** include an update to `CHANGELOG.md`. Add your entry under the
428-
`## Unreleased` section at the top. Use the appropriate subsection:
428+
current `## <version> — <YYYY-MM-DD>` section at the top, where `<version>` matches
429+
`version` in `package.json` and `<YYYY-MM-DD>` is today's date. If that section
430+
does not exist yet, create it above the previous version's section. There is **no**
431+
`## Unreleased` section — entries always go under a dated, versioned heading, so
432+
each feature PR ships release-ready.
433+
434+
Use the appropriate subsection:
429435

430436
- **Features** for new functionality
431437
- **Fixes** for bug fixes
432438
- **Docs** for documentation-only changes
433439
- **Dependencies** for dependency bumps
434440

435441
Each entry should be a single line: a short description followed by the PR number
436-
(e.g. `- Add foo support (#123)`). When a release is cut, the `Unreleased` section gets
437-
renamed to the version and date, and a fresh `## Unreleased` heading is added.
442+
(e.g. `- Add foo support (#123)`).
438443

439444
## Version bumps
440445

__tests__/lib/opencode-projects.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,12 @@ describe("getOpenCodeProjects", () => {
6969
const projects = await getOpenCodeProjects();
7070
expect(projects).toHaveLength(2);
7171
// Newest first — p1 has time_updated=200, p2 has 50.
72-
expect(projects[0].name).toBe("repo"); // basename(/repo) — name was null
72+
// `name` is the URL slug = encodeFolderName(worktree) — matches every other CLI.
73+
expect(projects[0].name).toBe("-repo");
7374
expect(projects[0].path).toBe("/repo");
7475
expect(projects[0].cli).toEqual(["opencode"]);
7576
expect(projects[0].lastModified.getTime()).toBe(200);
76-
expect(projects[1].name).toBe("Other Project");
77+
expect(projects[1].name).toBe("-other");
7778
expect(projects[1].lastModified.getTime()).toBe(50);
7879
});
7980

@@ -86,7 +87,7 @@ describe("getOpenCodeProjects", () => {
8687
]);
8788
const projects = await getOpenCodeProjects();
8889
expect(projects).toHaveLength(1);
89-
expect(projects[0].name).toBe("Empty Project");
90+
expect(projects[0].name).toBe("-repo");
9091
expect(projects[0].lastModified.getTime()).toBe(10);
9192
});
9293

lib/opencode-projects.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
* https://opencode.ai/docs/plugins/ (plugin model context)
2323
*/
2424
import { execFileSync } from "node:child_process";
25-
import { basename } from "node:path";
2625
import { encodeFolderName } from "./paths";
2726
import type { ProjectFolder, SessionFile } from "./projects";
2827
import { runtimeCache } from "./runtime-cache";
@@ -91,10 +90,11 @@ function readProjectRows(): OpenCodeProjectRow[] | null {
9190

9291
/**
9392
* Group sessions by `project_id` and produce one ProjectFolder per project.
94-
* The folder name comes from `project.name` when set, else `basename(worktree)`,
95-
* else the project_id (last-resort). `lastModified` is the max session
96-
* `time_updated` for that project (or the project's own time_updated if no
97-
* sessions exist yet).
93+
* The folder name is `encodeFolderName(worktree)` (matches every other CLI's
94+
* URL-slug encoding so the dashboard's `/project/[name]` route resolves), or
95+
* the project_id when no worktree is recorded. `lastModified` is the max
96+
* session `time_updated` for that project (or the project's own time_updated
97+
* if no sessions exist yet).
9898
*/
9999
export async function getOpenCodeProjects(): Promise<ProjectFolder[]> {
100100
const sessions = readSessionRows();
@@ -122,13 +122,15 @@ export async function getOpenCodeProjects(): Promise<ProjectFolder[]> {
122122

123123
// Emit one ProjectFolder per project that has at least one session OR a
124124
// project row (covers projects opencode knows about but hasn't run yet).
125+
// `name` is the dashboard's URL slug — must be `encodeFolderName(cwd)` to
126+
// match every other CLI (and the resolver in `getOpenCodeSessionsByEncodedName`).
125127
const seen = new Set<string>();
126128
const out: ProjectFolder[] = [];
127129
for (const [projectId, group] of groups) {
128130
seen.add(projectId);
129131
const proj = projectMap.get(projectId);
130132
const worktree = proj?.worktree ?? group.rows[0]?.directory ?? null;
131-
const name = proj?.name?.trim() || (worktree ? basename(worktree) : projectId);
133+
const name = worktree ? encodeFolderName(worktree) : projectId;
132134
const path = worktree ?? "";
133135
const lastModified = new Date(Math.max(group.latest, proj?.time_updated ?? 0));
134136
out.push({
@@ -143,7 +145,7 @@ export async function getOpenCodeProjects(): Promise<ProjectFolder[]> {
143145
for (const p of projects ?? []) {
144146
if (seen.has(p.id)) continue;
145147
const worktree = p.worktree ?? "";
146-
const name = p.name?.trim() || (worktree ? basename(worktree) : p.id);
148+
const name = worktree ? encodeFolderName(worktree) : p.id;
147149
const lastModified = new Date(p.time_updated);
148150
out.push({
149151
name,

0 commit comments

Comments
 (0)