Skip to content

Enhance CLI with project workflows, feature flags, and documentation updates#2

Merged
backslash-ux merged 20 commits intomainfrom
release/1.0
Mar 31, 2026
Merged

Enhance CLI with project workflows, feature flags, and documentation updates#2
backslash-ux merged 20 commits intomainfrom
release/1.0

Conversation

@backslash-ux
Copy link
Copy Markdown
Owner

@backslash-ux backslash-ux commented Mar 31, 2026

This pull request updates documentation, configuration, and scripts to clarify CLI usage, improve deployment compatibility guidance, and ensure correct references after a repository rename. The most important changes are grouped below.

Repository and Package Name Updates:

  • Updated all references from plane-cli to plane-cli-cli in URLs, badges, and install instructions to match the new repository name. This includes .github/ISSUE_TEMPLATE/config.yml, README.md, and install commands in SKILL.md. [1] [2] [3] [4]

CLI Usage and Configuration Documentation:

  • Expanded documentation in README.md and SKILL.md to clarify global and local CLI setup, current project selection, configuration precedence, and project context management. Added examples for new commands (plane init --local, plane projects use, etc.) and described how local and environment config is resolved. [1] [2]
  • Added detailed command notes and compatibility notes, including project-scoped feature handling, explicit compatibility errors for missing endpoints, and deployment-dependent features (pages, worklogs). [1] [2]

New and Updated Commands:

  • Documented new cleanup commands: plane labels delete and plane modules delete, and clarified argument expectations for commands like plane modules issues remove and plane members list. [1] [2] [3] [4] [5]
  • Added and updated usage examples throughout README.md and SKILL.md to reflect the latest command grammar and argument conventions, including use of @current and explicit HTML for descriptions. [1] [2] [3]

Deployment Compatibility Guidance:

  • Added a new "Known Deployment Compatibility" section to AGENTS.md and updated compatibility notes in README.md and SKILL.md to document how features like pages and worklogs may not be available on all Plane deployments, even if feature flags are set. [1] [2] [3]

Developer Tooling Improvements:

  • Improved .husky/pre-commit script to reliably locate the bun binary, ensuring pre-commit hooks work even when bun is not in the default PATH.

These updates ensure the CLI documentation is accurate, user-friendly, and robust against deployment differences, while also aligning all references and scripts with the new repository structure.

Summary by CodeRabbit

  • New Features

    • Local project config: plane init --local, plane . init, top-level plane . command
    • Current project management: plane projects use and plane projects current
    • New commands: plane labels delete, plane modules delete
    • Writes local project helpers into a repo .plane/ and updates AGENTS.md
  • Documentation

    • README/SKILL/CHANGELOG/RELEASING updated with install, init (-g), examples, command grammar, and compatibility notes
  • Improvements

    • Config discovery/precedence, host normalization, clearer user messages when features (pages/worklogs/intake/modules/cycles) are unavailable
    • CLI version bumped to 1.0.0
  • Chores

    • Added .plane/ to ignore list; package branding/metadata updated

…ssing

Users can now enter `plane.example.com` during init instead of requiring
the full `https://plane.example.com` URL. The normalizeHost helper trims
whitespace and prepends https:// when no scheme is detected.
Newer Plane deployments may use `intake_view` instead of `inbox_view`.
The CLI now checks both fields via `isProjectIntakeEnabled()` and falls
back transparently. Also fixes intake status codes: -2=pending,
-1=rejected, 0=snoozed.
…ssues

Plane API may return raw issue objects instead of join-table records
for cycle-issues and module-issues endpoints. Schemas now accept both
formats via union types (CycleIssueSchema, ModuleIssueSchema) so the
CLI handles either response shape gracefully.
Extracts payload types and a requestWithFallback helper into
src/issue-support.ts. Issue link and worklog commands now try
/work-items/ paths before falling back to legacy /issues/ paths,
so the CLI works across Plane API versions.

Also refreshes the issue after PATCH in update to ensure the
displayed state reflects the server response.
When a Plane deployment returns 404 for page endpoints, the CLI now
shows a clear message instead of a raw error. Wraps all page API calls
with mapPageAvailabilityError to detect and explain the situation.
The command lists workspace members, not project members. Updated the
help text to match the actual behavior.
The managed section generated by `plane init --local` now includes
CLI preference, @current selector guidance, PLANE_* override clearing,
and a small command pattern block. This helps AGENTS.md-aware AI clients
discover the CLI without manual documentation.
Adds Known Deployment Compatibility section to AGENTS.md documenting
pages, worklogs, feature gating, and missing CLI commands. Updates
README and SKILL with matching compatibility notes. Removes misplaced
Agent CLI Usage section from AGENTS.md (that guidance now lives in
the generated managed section instead).
Add plane labels delete and plane modules delete so cleanup can stay
inside the CLI. Both commands resolve targets from discoverable list
output using either exact names or UUIDs, keeping the flows friendly
for humans and agents.

Also adds focused Bun/MSW coverage for the new delete handlers and
brings labels command coverage up to the repo threshold.
Update the public docs and contributor guidance to reflect the new
plane labels delete and plane modules delete commands. Remove the
old compatibility note that told users to fall back to direct API
cleanup for those two operations.
Copilot AI review requested due to automatic review settings March 31, 2026 10:54
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: df21ed2c-1fb4-4e77-96a2-b7f6b1c7a25b

📥 Commits

Reviewing files that changed from the base of the PR and between 9a1f054 and 3eb7714.

📒 Files selected for processing (1)
  • src/app.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app.ts

📝 Walkthrough

Walkthrough

Adds multi-scope config discovery (env → local → global), introduces project-context snapshots and AGENTS.md, implements per-project feature gating and fallback request helpers, rewrites init/projects/labels/modules/local commands with scoped persistence, updates many CLI commands/schemas/helpers, and adjusts tests and docs for the renamed package identity.

Changes

Cohort / File(s) Summary
Repository metadata & docs
\.github/ISSUE_TEMPLATE/config.yml, package.json, README.md, SKILL.md, CHANGELOG.md, AGENTS.md, docs/RELEASING.md
Rebranded package/repo references to @backslash-ux/plane-cli / plane-cli-cli; updated install/CI/docs, added compatibility/command notes, changelog entries for label/module delete, and release preflight/post-release steps.
Git tooling & CI
.husky/pre-commit, scripts/check-coverage.ts, .gitignore
Pre-commit hook now resolves bun path and invokes coverage via resolved runtime; coverage script uses current Node (process.execPath); .gitignore adds .plane/ and broadens .vscode/ ignore.
Config discovery & API integration
src/user-config.ts (new), src/api.ts
New multi-scope config APIs (env/local/global) with normalization, read/write (0o600), getConfig/getConfigDetails; src/api.ts now imports getConfig, expands init guidance, and tightens JSON sanitization.
Project context & agents
src/project-context.ts (new), src/project-agents.ts (new), AGENTS.md
New ProjectContextSnapshot types and builders, writer utilities for project-context.json, and upsert-managed block writer for AGENTS.md.
CLI root & local/init command
src/app.ts, src/commands/init.ts, src/commands/local.ts (new)
Adds top-level local command; init rewritten to support --global/--local, interactive prompts, remote project fetch, scoped persistence, project-context snapshot and AGENTS helper generation; exports initHandler and localInit. CLI version bumped to 1.0.0.
Project commands & persistence
src/commands/projects.ts, tests/projects.test.ts
Adds projects use/projects current, write-scope logic (--local/--global), projects list marks current project, and tests verifying persistence and scope behavior.
Command arg defaults & feature gating
src/commands/* (issues, cycles, states, modules, pages, intake, labels, members, pages modifications)
Many list commands now accept optional PROJECT (default ""), support @current, and call requireProjectFeature() before feature-dependent API calls; added labels delete and modules delete; output formatting and endpoint usage adjusted.
Feature resolution & helpers
src/resolve.ts
Adds project-detail caching, feature-key typing, getProjectFeatureDetails(), requireProjectFeature(), enhanced resolveProject() (handles ., @current, @default), and resolveLabel/resolveModule helpers.
Issue support & fallback requests
src/issue-support.ts (new), src/commands/issue.ts
New payload interfaces, requestWithFallback() for alternate REST path probing on 404, and helper path generators (issueLinkPaths, issueWorklogPaths); issue handlers use fallback and re-GET-after-PATCH pattern.
Schemas & decoding updates
src/config.ts, tests/new-schemas*.ts
Adds ProjectDetail/Estimate/EstimatePoint schemas; converts Module/Cycle issue schemas into unions to accept relation or raw payload shapes; tests updated to cover both shapes.
Local init artifacts & tests
src/project-context.ts, src/project-agents.ts, tests/project-features.test.ts, tests/user-config.test.ts
Local init writes project-context and AGENTS.md; tests cover feature gating, artifact generation, AGENTS.md preservation, and user-config precedence/normalization.
Tests: fixtures, endpoints & invocation shifts
tests/*.ts (many files)
Widespread test updates: added PROJECT_DETAIL fixtures/MSW handlers, switched many issue-link/worklog mocks to /work-items/:id/links/ with worklog fallbacks, normalized env var access to dot-notation, invoked *Handler exports directly in JSON/XML tests, and adjusted expected shapes for raw vs relation issue payloads.
Output formatting
src/output.ts
Minor refactor using template literals for XML assembly (no behavioral change).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as "plane CLI"
    participant ConfigMgr as "Config Manager"
    participant LocalFS as "Local Filesystem"
    participant API as "Plane API"

    User->>CLI: plane init --local
    CLI->>ConfigMgr: initHandler(global=false, local=true)
    ConfigMgr->>LocalFS: readLocalStoredConfig()
    ConfigMgr->>User: prompt(host, workspace, token, default project)
    User-->>ConfigMgr: answers
    ConfigMgr->>API: GET /projects, /states, /labels, /estimates
    API-->>ConfigMgr: project detail + metadata
    ConfigMgr->>LocalFS: writeLocalStoredConfig(.plane/config.json)
    ConfigMgr->>LocalFS: writeLocalProjectContextSnapshot(.plane/project-context.json)
    ConfigMgr->>LocalFS: writeLocalProjectAgentsFile(AGENTS.md)
    CLI-->>User: print feature flags and saved locations
Loading
sequenceDiagram
    participant User
    participant CLI as "plane CLI"
    participant Resolver as "Config & Feature Resolver"
    participant API as "Plane API"
    participant Handler as "Command Handler"

    User->>CLI: plane cycles list
    CLI->>Handler: cyclesListHandler(project="")
    Handler->>Resolver: resolveProject("") -> "@current"
    Resolver->>Resolver: consult env/local/global config
    Resolver-->>Handler: resolved project id
    Handler->>Resolver: requireProjectFeature(projectId, "cycle_view")
    Resolver->>API: GET /projects/{id}
    API-->>Resolver: ProjectDetail with cycle_view flag
    alt cycle_view enabled
        Resolver-->>Handler: allowed
        Handler->>API: GET /projects/{id}/cycles/
        API-->>Handler: cycles list
        Handler-->>User: formatted cycles output
    else cycle_view disabled
        Resolver-->>Handler: denied (error)
        Handler-->>User: feature-disabled error with hint
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through configs, near and far,

Saved projects in a neat little jar.
Fallbacks and gates keep chaos away,
AGENTS.md hums where snapshots stay,
A CLI that settles in — hip‑hop hooray! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: enhancing CLI with project workflows, feature flags, and documentation updates, which aligns with the substantial refactoring across commands, configuration, and documentation files.
Description check ✅ Passed The PR description comprehensively covers all major changes with clear groupings, includes specific file references, and explains user-facing outcomes. However, it lacks explicit validation checklist completion status and missing Docs section confirmation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/1.0

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the Plane CLI’s workflow around project selection and configuration layering (env/global/local), adds project feature-gating with clearer compatibility errors, introduces cleanup and project-management commands, and updates docs/tooling to match the renamed package/repo and new command grammar.

Changes:

  • Added global + nearest-local config resolution (.plane/config.json) with a persisted “current project” (PLANE_PROJECT, plane projects use/current) and richer init flows (including . init alias + local project context snapshot/AGENTS section).
  • Added project feature gating (cycles/modules/pages/intake) and API-compatibility fallbacks for evolving endpoints (work-items links/worklogs, raw issue payload variants).
  • Added new commands (labels delete, modules delete) and updated docs/tooling (repo rename references, Husky/Bun reliability, releasing notes).

Reviewed changes

Copilot reviewed 52 out of 53 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/xml-output.test.ts Updates XML mode tests for new handlers, endpoints, and project-detail mocking.
tests/user-config.test.ts Adds tests for config layering (env/local/global) and host normalization.
tests/schemas.test.ts Adjusts schema imports/order to match config exports.
tests/resolve.test.ts Extends resolve tests for PLANE_PROJECT/@current behaviors.
tests/projects.test.ts Adds coverage for projects use/current/list behaviors and persistence scope.
tests/project-features.test.ts Adds tests for project feature flags and local init context/AGENTS updates.
tests/pages.test.ts Adds project detail mocking and explicit “pages unavailable” error test.
tests/output.test.ts Minor cleanup of output unit test imports.
tests/new-schemas2.test.ts Updates schema tests for cycle-issue payload variants (legacy vs raw).
tests/new-schemas.test.ts Updates schema tests for module-issue payload variants (legacy vs raw).
tests/modules.test.ts Adds tests for modules delete and raw module-issue payload listing.
tests/labels.test.ts Adds tests for labels delete plus list/create coverage.
tests/json-output.test.ts Updates JSON mode tests for new handlers/endpoints and payload variants.
tests/issue-links.test.ts Updates link endpoint tests to new work-items/.../links primary route.
tests/issue-comments-worklogs.test.ts Adds worklogs endpoint fallback coverage (work-items 404 -> issues worklogs).
tests/issue-commands.test.ts Extends issue command tests (blank project uses saved current project, extra GET).
tests/issue-activity.test.ts Minor import/env-var normalization adjustments in activity tests.
tests/intake.test.ts Updates intake payloads/status expectations and mutation endpoint ID usage.
tests/helpers/mock-api.ts Normalizes MSW import ordering.
tests/format.test.ts Minor import ordering cleanup.
tests/cycles-extended.test.ts Adds project detail mocking and legacy cycle-issue join payload coverage.
tests/api.test.ts Updates env-var access style and import ordering.
src/user-config.ts Introduces global+local config storage, discovery, normalization, and source tracking.
src/resolve.ts Adds default-project resolution, project detail caching, feature gating, label/module resolvers.
src/project-context.ts Generates/writes .plane/project-context.json snapshot for states/labels/estimates/features.
src/project-agents.ts Creates/updates managed AGENTS.md section for local project context guidance.
src/output.ts Small XML string-formatting adjustments.
src/issue-support.ts Adds request fallback helper + link/worklog path sets for API compatibility.
src/config.ts Adds ProjectDetail/Estimate schemas and unions for raw vs relation payload variants.
src/commands/states.ts Allows omitting PROJECT (uses saved current project).
src/commands/projects.ts Adds projects current and projects use with local/global persistence behavior.
src/commands/pages.ts Adds feature gating + maps 404s to explicit “pages unavailable” compatibility errors.
src/commands/modules.ts Adds feature gating, modules delete, and raw module-issue payload handling.
src/commands/members.ts Minor import ordering cleanup.
src/commands/local.ts Adds plane . command group for local-only workflows (plane . init).
src/commands/labels.ts Adds labels delete plus refactors list/create into handlers and local-default support.
src/commands/issues.ts Allows omitting PROJECT (uses saved current project) and updates help text.
src/commands/issue.ts Adds endpoint fallbacks for links/worklogs; refreshes issue after update.
src/commands/intake.ts Adds feature gating, resolves mutation ID, and allows omitting PROJECT.
src/commands/init.ts Major init overhaul: scope selection, layering, project selection, local snapshot + AGENTS update.
src/commands/cycles.ts Adds feature gating, omittable project argument, and raw cycle-issue payload handling.
src/app.ts Updates CLI help/docs to reflect new config layering and commands (including plane . init).
src/api.ts Switches config source to user-config and improves missing-config error messaging.
SKILL.md Updates package name/install instructions + expands CLI/config/current-project and compatibility docs.
scripts/check-coverage.ts Uses process.execPath for running bun test --coverage more robustly.
README.md Updates repo/package references and expands setup/config/current-project/compatibility guidance.
package.json Renames published package + updates repository/bugs/homepage URLs.
docs/RELEASING.md Adds maintainer setup and preflight/postflight release guidance.
CHANGELOG.md Updates package identity notes and records new commands/docs validation.
AGENTS.md Adds known deployment compatibility notes and includes a managed local context section.
.husky/pre-commit Improves Bun discovery and runs repo scripts via resolved Bun path.
.gitignore Ignores .plane/ and broadens .vscode/ ignore.
.github/ISSUE_TEMPLATE/config.yml Updates repository links to renamed repo.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/commands/pages.ts Outdated
Comment on lines +112 to +115
const raw = yield* mapPageAvailabilityError(
api.get(`projects/${id}/pages/${pageId}/`),
`Project pages are not available for ${key} on this Plane instance or API version.`,
);
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapPageAvailabilityError converts any HTTP 404 into a "pages are not available" compatibility error. For handlers that target a specific page ID (e.g. get/update/delete), a legitimate "page not found" 404 would be incorrectly reported as an API-availability problem. Consider only mapping 404s to the compatibility message when you can confirm the endpoint is missing (e.g. probe the list endpoint on 404), otherwise preserve a not-found error for the specific page.

Suggested change
const raw = yield* mapPageAvailabilityError(
api.get(`projects/${id}/pages/${pageId}/`),
`Project pages are not available for ${key} on this Plane instance or API version.`,
);
const raw = yield* api.get(`projects/${id}/pages/${pageId}/`);

Copilot uses AI. Check for mistakes.
Comment thread src/commands/issue.ts Outdated
Comment on lines 432 to 436
yield* requestWithFallback(
issueLinkPaths(projectId, issue.id).map((path) => `${path}${linkId}/`),
(path) => api.delete(path),
`Issue links are not available for ${ref} on this Plane instance or API version.`,
);
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requestWithFallback treats any HTTP 404 as "try the next endpoint" and ultimately returns the generic compatibility error. In issueLinkRemoveHandler, that means a real "link ID not found" 404 (on a supported endpoint) will be reported as "Issue links are not available…". Consider handling delete differently (e.g. determine the correct base links endpoint via list/probe first, then delete once, so a missing link surfaces as a not-found error).

Suggested change
yield* requestWithFallback(
issueLinkPaths(projectId, issue.id).map((path) => `${path}${linkId}/`),
(path) => api.delete(path),
`Issue links are not available for ${ref} on this Plane instance or API version.`,
);
// First, resolve the correct base path for issue links via a safe probe.
const basePath = yield* requestWithFallback(
issueLinkPaths(projectId, issue.id),
(path) => api.get(path).pipe(Effect.as(path)),
`Issue links are not available for ${ref} on this Plane instance or API version.`,
);
// Then, perform the delete once against the resolved base path so a missing
// link ID results in a proper not-found error instead of a fallback message.
yield* api.delete(`${basePath}${linkId}/`);

Copilot uses AI. Check for mistakes.
Comment thread src/commands/init.ts Outdated
Comment on lines +450 to +454
yield* Console.log(
`\n${scope === "global" ? "Global" : "Local"} config saved to ${savePath}`,
);
yield* Console.log(
` Host: ${describeValue(scope, savedHost, normalizedHost)}`,
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For local init, the displayed host uses the raw savedHost when set, but the persisted value is normalized by writeLocalStoredConfig (via cleanConfig/normalizeHost). This can print a host value that doesn’t match what was actually written (e.g. missing scheme or trailing slash). Consider normalizing savedHost for display (or showing both input and normalized value) so the output reflects the stored config accurately.

Suggested change
yield* Console.log(
`\n${scope === "global" ? "Global" : "Local"} config saved to ${savePath}`,
);
yield* Console.log(
` Host: ${describeValue(scope, savedHost, normalizedHost)}`,
const hostNormalizedForDisplay =
scope === "global"
? normalizedHost
: savedHost
? normalizeHost(savedHost)
: normalizedHost;
yield* Console.log(
`\n${scope === "global" ? "Global" : "Local"} config saved to ${savePath}`,
);
yield* Console.log(
` Host: ${describeValue(scope, savedHost, hostNormalizedForDisplay)}`,

Copilot uses AI. Check for mistakes.
Comment thread src/commands/intake.ts
Comment on lines +16 to 23
// Intake status codes: -2=pending, -1=rejected, 0=snoozed, 1=accepted, 2=duplicate
const STATUS_LABELS: Record<number, string> = {
[-2]: "rejected",
[-1]: "snoozed",
[0]: "pending",
[1]: "accepted",
[2]: "duplicate",
[-2]: "pending",
[-1]: "rejected",
0: "snoozed",
1: "accepted",
2: "duplicate",
};
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With STATUS_LABELS now mapping 0 to snoozed, the existing list formatting logic later in this file that does i.status ?? 0 will label any intake item with an omitted status as snoozed. Consider updating the formatter to treat undefined status as unknown (or handle it separately) rather than defaulting to 0 now that 0 is not a safe default.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
scripts/check-coverage.ts (1)

13-20: ⚠️ Potential issue | 🟠 Major

Fail closed when coverage output is unparseable.

At line 19, the script exits with code 0 if the coverage regex fails to match, allowing the pre-commit hook to silently pass even when coverage metrics cannot be verified. This is inconsistent with the threshold validation at line 41, which correctly exits with code 1 on failure, and creates a quality gate bypass. The parse failure should fail the hook.

Proposed fix
   const coverageMatch = output.match(/All files\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|/)
   if (!coverageMatch) {
-    console.log("\n⚠️  Could not parse coverage output")
-    process.exit(0)
+    console.error("\n❌ Could not parse coverage output; failing coverage check")
+    process.exit(1)
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check-coverage.ts` around lines 13 - 20, The script currently treats
an unparseable coverage output as success; change the behavior so the process
fails instead. In scripts/check-coverage.ts, after running the test via execSync
and when checking coverageMatch (the regex result from output.match(...)),
replace the process.exit(0) path with process.exit(1) (and keep or enhance the
console.log("\n⚠️  Could not parse coverage output") message) so that an
unparsable coverage report causes the hook to fail consistently with the later
threshold validation logic.
src/commands/pages.ts (1)

180-184: ⚠️ Potential issue | 🟡 Minor

Early return missing for validation failure.

When neither --name nor --description is provided, Effect.fail is called but execution continues. The generator will throw, but an explicit return makes intent clearer.

🔧 Suggested fix
 	return Effect.gen(function* () {
 		if (Option.isNone(name) && Option.isNone(description)) {
-			yield* Effect.fail(new Error("provide at least --name or --description"));
+			return yield* Effect.fail(new Error("provide at least --name or --description"));
 		}
 		const { key, id } = yield* resolveProject(project);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/pages.ts` around lines 180 - 184, The generator started in
Effect.gen currently calls Effect.fail when both Option.isNone(name) and
Option.isNone(description) but does not explicitly stop execution; update the
generator inside Effect.gen so that after yielding Effect.fail(new
Error("provide at least --name or --description")) you immediately return (i.e.,
return yield* Effect.fail(...) or return after the yield) to prevent further
execution; locate the failing check using Option.isNone(name),
Option.isNone(description), and the surrounding Effect.gen function to apply the
change.
src/resolve.ts (1)

137-149: ⚠️ Potential issue | 🟡 Minor

Use the resolved project key in the not-found error.

For ".", "@current", and "@default", the current message reports the selector instead of the configured project that actually missed. That makes stale config harder to diagnose.

Suggested fix
-						`Unknown project: ${identifier}. Known: ${Object.keys(map).join(", ")}`,
+						`Unknown project: ${key}. Known: ${Object.keys(map).join(", ")}`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/resolve.ts` around lines 137 - 149, The not-found error in resolveProject
uses the original selector (identifier) instead of the resolved project key,
which hides the actual configured project name; update the Effect.fail error
message in resolveProject (where key is computed via
getConfiguredProject(identifier).toUpperCase()) to reference key (the resolved
project name) and list Object.keys(map) so the error shows the missing
configured project and available keys.
🧹 Nitpick comments (6)
.husky/pre-commit (1)

10-16: Add typecheck to pre-commit quality gates.

The hook currently runs file-size and coverage checks but skips TypeScript typechecking. Consider adding "$BUN" run typecheck before coverage.

Proposed update
 # Check file sizes
 echo "Checking file sizes..."
 "$BUN" scripts/check-file-size.ts

+# Type safety gate
+echo "Checking types..."
+"$BUN" run typecheck
+
 # Check test coverage
 echo "Checking test coverage..."
 "$BUN" scripts/check-coverage.ts

Based on learnings: Run the narrowest relevant tests plus bun run typecheck.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.husky/pre-commit around lines 10 - 16, Add a TypeScript typecheck step to
the pre-commit hook by invoking the project's Bun typecheck script before
running coverage: insert a call to run the `typecheck` script (e.g., `"$BUN" run
typecheck`) in the pre-commit sequence between the file-size check (`"$BUN"
scripts/check-file-size.ts`) and the coverage check (`"$BUN"
scripts/check-coverage.ts`) so that `typecheck` runs as a quality gate prior to
coverage.
src/project-context.ts (1)

166-176: Redundant chmodSync call.

The mode: 0o600 option in writeFileSync already sets the file permissions. The subsequent chmodSync call is unnecessary.

♻️ Proposed fix
 export function writeLocalProjectContextSnapshot(
 	snapshot: ProjectContextSnapshot,
 	cwd = process.cwd(),
 ): void {
 	const filePath = getLocalProjectContextFilePath(cwd);
 	fs.mkdirSync(path.dirname(filePath), { recursive: true });
 	fs.writeFileSync(filePath, `${JSON.stringify(snapshot, null, 2)}\n`, {
 		mode: 0o600,
 	});
-	fs.chmodSync(filePath, 0o600);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/project-context.ts` around lines 166 - 176, In
writeLocalProjectContextSnapshot remove the redundant fs.chmodSync(filePath,
0o600) because fs.writeFileSync already sets permissions via the mode: 0o600
option; update the function by deleting the chmodSync call so permissions are
only set via writeFileSync (ensure getLocalProjectContextFilePath and the
preceding fs.mkdirSync call remain unchanged).
src/issue-support.ts (1)

26-28: Consider making error detection more robust.

The 404 detection relies on the exact error message format ^HTTP 404:. This coupling to the API module's error string format could break silently if the message format changes.

Consider exporting a typed error class or error code from the API module for more reliable detection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/issue-support.ts` around lines 26 - 28, The current
isNotFoundError(error: Error) uses a fragile regex against error.message;
instead export a typed error (e.g., class NotFoundError extends Error) or a
stable error code (e.g., error.code === 'NOT_FOUND') from the API module and
update isNotFoundError to detect that (prefer using instanceof NotFoundError,
falling back to a code check for compatibility). Modify the API to throw or
attach the stable type/code, then change the isNotFoundError function to check
for that exported symbol (NotFoundError or error.code) rather than matching
message text.
package.json (1)

2-2: Note: Package name vs repository name inconsistency.

The package is named @backslash-ux/plane-cli but the repository URLs reference plane-cli-cli. This asymmetry may cause confusion for contributors looking for the source repository. If intentional, consider documenting the naming convention in README.

Also applies to: 36-41

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 2, The package name "@backslash-ux/plane-cli" in
package.json is inconsistent with the repository references that use
"plane-cli-cli"; update the package.json "name" field to match the canonical
repository name (or update the repository fields to match the package name) and
add a short note in the README explaining the naming convention if the mismatch
is intentional; specifically check and align the "name" value
"@backslash-ux/plane-cli" and the repository URL/fields that currently contain
"plane-cli-cli" (and any other occurrences) so they are consistent.
SKILL.md (1)

274-278: Clarify module-issue removal identifier in docs.

The comment mentions "use the identifier returned by plane modules issues list" which is helpful, but consider making the distinction between issue ref (PROJ-29) and module-issue ID even clearer for agents that might conflate them.

📝 Suggested clarification
-plane modules issues remove PROJ <module-id> <module-issue-id>  # use the identifier returned by `plane modules issues list`
+plane modules issues remove PROJ <module-id> <module-issue-id>  # module-issue-id is the join record UUID from `plane modules issues list`, NOT an issue ref like PROJ-29
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SKILL.md` around lines 274 - 278, The docs currently risk confusing the issue
reference (e.g., PROJ-29) with the module-issue identifier returned by the CLI;
update the SKILL.md lines around the module commands (specifically the examples
for plane modules issues list, add, and remove) to explicitly state that plane
modules issues list returns a distinct module-issue ID (not the issue key), show
a concrete example of both values (e.g., "issue ref: PROJ-29" vs
"module-issue-id: 12345" or the actual format returned), and revise the plane
modules issues remove example to use that module-issue-id placeholder and a
short clarifying sentence like "use the module-issue-id returned by plane
modules issues list (not the issue key like PROJ-29)."
README.md (1)

64-71: Add language specifier to fenced code block.

Static analysis flagged this code block as missing a language specifier. Since these are environment variables, bash or shell would be appropriate.

📝 Suggested fix
-```
+```bash
 PLANE_API_TOKEN=...
 PLANE_HOST=https://plane.so
 PLANE_WORKSPACE=myworkspace
 PLANE_PROJECT=PROJ               # optional saved-project override
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @README.md around lines 64 - 71, The fenced code block showing environment
variables (e.g., lines with PLANE_API_TOKEN, PLANE_HOST, PLANE_WORKSPACE,
PLANE_PROJECT) in README.md should include a language specifier for static
analysis; update the opening fence to use an appropriate specifier such as
"bash" or "shell" (e.g., ```bash) so the block is marked correctly without
changing the block contents.


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @src/commands/init.ts:

  • Around line 323-327: The readline interface created via
    readline.createInterface (rl) can remain open if resolveProjectSelection throws,
    so update the code to ensure rl.close() is always called: wrap the interactive
    prompts and calls to resolveProjectSelection(...) (and any other logic that may
    throw between where rl is created and previous rl.close() calls) inside a
    try/finally and call rl.close() in the finally block (or use a try/catch/finally
    if you need to handle errors), ensuring rl is closed whether
    resolveProjectSelection succeeds or throws.
  • Around line 470-534: The branch only runs when savedDefaultProject is set, so
    when the user inherits a global current project (savedDefaultProject is
    undefined) the local helper files are skipped; update the logic in the scope ===
    "local" block to pick a selectedProject from either savedDefaultProject or the
    inherited current project identifier (e.g., use savedDefaultProject ??
    currentProjectIdentifier) before resolving projectsResult, then call
    fetchLocalProjectHelperForConfig(...) for that selectedProject and proceed to
    run writeLocalProjectContextSnapshot(...), writeLocalProjectAgentsFile(...),
    getLocalProjectContextFilePath(), getLocalAgentsFilePath(), and
    summarizeProjectFeatures(...) exactly as done now so the helper files are
    generated even when the project was inherited rather than re-selected.

In @src/commands/modules.ts:

  • Around line 133-141: The list output branch that handles raw issue payloads
    (the if ("sequence_id" in mi) branch inside the results.map call) currently
    returns only ${key}-${seq} ${mi.name}, which omits any removable identifier;
    update that branch to append a discoverable identifier so consumers can run
    modules issues remove <id> — e.g. return ${key}-${seq} ${mi.name} (${mi.id ?? mi.issue_id ?? mi.uuid}) or otherwise detect and include the unique id field
    present on mi; ensure you only render the parentheses if an identifier exists.

In @src/config.ts:

  • Around line 76-94: The current isProjectIntakeEnabled function uses the
    nullish coalescing operator (project.inbox_view ?? project.intake_view) which
    treats false as a terminal value and prevents intake_view from acting as an
    alias; update isProjectIntakeEnabled to return true if either inbox_view or
    intake_view is truthy (e.g., use logical OR on booleans) so a payload like {
    inbox_view: false, intake_view: true } yields true; modify the implementation of
    isProjectIntakeEnabled (and confirm ProjectDetailSchema fields inbox_view and
    intake_view remain optional) to evaluate as (Boolean(project.inbox_view) ||
    Boolean(project.intake_view)) or equivalent.

In @src/user-config.ts:

  • Around line 192-200: The code uses the nullish coalescing operator so empty
    environment variables (e.g., PLANE_API_TOKEN or PLANE_HOST captured as
    envToken/envHost) incorrectly win over stored configs; change the selection to
    treat empty strings as unset by normalizing envToken/envHost to undefined when
    they are "" (or only whitespace) before applying precedence for token and host
    (the host still passed through normalizeHost); update the logic around token,
    host and any similar blocks (also the block referenced at lines 214-242) to use
    these normalized env values so stored localConfig/globalConfig are chosen when
    env vars are blank.
  • Around line 187-200: getConfigDetails currently merges token, host, and
    workspace independently across env/local/global which can mix scopes; change it
    so the connection settings are chosen from a single scope: inspect scopes in
    precedence order (env, localConfigFile, globalConfig) to find which scope
    supplies the token first and then pick host and workspace only from that same
    scope (use DEFAULT_HOST only if that chosen scope lacks host), otherwise if no
    token is found fall back to the first scope that supplies host/workspace but do
    not combine values from different scopes; update the logic around token, host,
    and envWorkspace/envProject resolution in getConfigDetails (and keep using
    normalizeHost) to enforce same-scope selection rather than per-field merging.

In @tests/user-config.test.ts:

  • Around line 6-18: The test setup deletes PLANЕ_* env vars but never restores
    originals, causing state leakage; capture the original values of
    PLANЕ_API_TOKEN, PLANЕ_HOST, PLANЕ_WORKSPACE, and PLANЕ_PROJECT at top (similar
    to ORIGINAL_HOME), and in afterEach restore process.env.PLANE_API_TOKEN,
    process.env.PLANE_HOST, process.env.PLANE_WORKSPACE, and
    process.env.PLANE_PROJECT to their saved originals (or delete them if original
    was undefined); update the beforeEach/afterEach blocks in
    tests/user-config.test.ts to use these saved variables alongside ORIGINAL_HOME
    and ORIGINAL_CWD.

Outside diff comments:
In @scripts/check-coverage.ts:

  • Around line 13-20: The script currently treats an unparseable coverage output
    as success; change the behavior so the process fails instead. In
    scripts/check-coverage.ts, after running the test via execSync and when checking
    coverageMatch (the regex result from output.match(...)), replace the
    process.exit(0) path with process.exit(1) (and keep or enhance the
    console.log("\n⚠️ Could not parse coverage output") message) so that an
    unparsable coverage report causes the hook to fail consistently with the later
    threshold validation logic.

In @src/commands/pages.ts:

  • Around line 180-184: The generator started in Effect.gen currently calls
    Effect.fail when both Option.isNone(name) and Option.isNone(description) but
    does not explicitly stop execution; update the generator inside Effect.gen so
    that after yielding Effect.fail(new Error("provide at least --name or
    --description")) you immediately return (i.e., return yield* Effect.fail(...) or
    return after the yield) to prevent further execution; locate the failing check
    using Option.isNone(name), Option.isNone(description), and the surrounding
    Effect.gen function to apply the change.

In @src/resolve.ts:

  • Around line 137-149: The not-found error in resolveProject uses the original
    selector (identifier) instead of the resolved project key, which hides the
    actual configured project name; update the Effect.fail error message in
    resolveProject (where key is computed via
    getConfiguredProject(identifier).toUpperCase()) to reference key (the resolved
    project name) and list Object.keys(map) so the error shows the missing
    configured project and available keys.

Nitpick comments:
In @.husky/pre-commit:

  • Around line 10-16: Add a TypeScript typecheck step to the pre-commit hook by
    invoking the project's Bun typecheck script before running coverage: insert a
    call to run the typecheck script (e.g., "$BUN" run typecheck) in the
    pre-commit sequence between the file-size check ("$BUN" scripts/check-file-size.ts) and the coverage check ("$BUN" scripts/check-coverage.ts) so that typecheck runs as a quality gate prior to
    coverage.

In @package.json:

  • Line 2: The package name "@backslash-ux/plane-cli" in package.json is
    inconsistent with the repository references that use "plane-cli-cli"; update the
    package.json "name" field to match the canonical repository name (or update the
    repository fields to match the package name) and add a short note in the README
    explaining the naming convention if the mismatch is intentional; specifically
    check and align the "name" value "@backslash-ux/plane-cli" and the repository
    URL/fields that currently contain "plane-cli-cli" (and any other occurrences) so
    they are consistent.

In @README.md:

  • Around line 64-71: The fenced code block showing environment variables (e.g.,
    lines with PLANE_API_TOKEN, PLANE_HOST, PLANE_WORKSPACE, PLANE_PROJECT) in
    README.md should include a language specifier for static analysis; update the
    opening fence to use an appropriate specifier such as "bash" or "shell" (e.g.,

In `@SKILL.md`:
- Around line 274-278: The docs currently risk confusing the issue reference
(e.g., PROJ-29) with the module-issue identifier returned by the CLI; update the
SKILL.md lines around the module commands (specifically the examples for plane
modules issues list, add, and remove) to explicitly state that plane modules
issues list returns a distinct module-issue ID (not the issue key), show a
concrete example of both values (e.g., "issue ref: PROJ-29" vs "module-issue-id:
12345" or the actual format returned), and revise the plane modules issues
remove example to use that module-issue-id placeholder and a short clarifying
sentence like "use the module-issue-id returned by plane modules issues list
(not the issue key like PROJ-29)."

In `@src/issue-support.ts`:
- Around line 26-28: The current isNotFoundError(error: Error) uses a fragile
regex against error.message; instead export a typed error (e.g., class
NotFoundError extends Error) or a stable error code (e.g., error.code ===
'NOT_FOUND') from the API module and update isNotFoundError to detect that
(prefer using instanceof NotFoundError, falling back to a code check for
compatibility). Modify the API to throw or attach the stable type/code, then
change the isNotFoundError function to check for that exported symbol
(NotFoundError or error.code) rather than matching message text.

In `@src/project-context.ts`:
- Around line 166-176: In writeLocalProjectContextSnapshot remove the redundant
fs.chmodSync(filePath, 0o600) because fs.writeFileSync already sets permissions
via the mode: 0o600 option; update the function by deleting the chmodSync call
so permissions are only set via writeFileSync (ensure
getLocalProjectContextFilePath and the preceding fs.mkdirSync call remain
unchanged).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 132d4f35-2bb8-42e6-9be8-f7da1ce4375a

📥 Commits

Reviewing files that changed from the base of the PR and between 6d92b59 and d8e0845.

📒 Files selected for processing (53)
  • .github/ISSUE_TEMPLATE/config.yml
  • .gitignore
  • .husky/pre-commit
  • AGENTS.md
  • CHANGELOG.md
  • README.md
  • SKILL.md
  • docs/RELEASING.md
  • package.json
  • scripts/check-coverage.ts
  • src/api.ts
  • src/app.ts
  • src/commands/cycles.ts
  • src/commands/init.ts
  • src/commands/intake.ts
  • src/commands/issue.ts
  • src/commands/issues.ts
  • src/commands/labels.ts
  • src/commands/local.ts
  • src/commands/members.ts
  • src/commands/modules.ts
  • src/commands/pages.ts
  • src/commands/projects.ts
  • src/commands/states.ts
  • src/config.ts
  • src/issue-support.ts
  • src/output.ts
  • src/project-agents.ts
  • src/project-context.ts
  • src/resolve.ts
  • src/user-config.ts
  • tests/api.test.ts
  • tests/cycles-extended.test.ts
  • tests/format.test.ts
  • tests/helpers/mock-api.ts
  • tests/intake.test.ts
  • tests/issue-activity.test.ts
  • tests/issue-commands.test.ts
  • tests/issue-comments-worklogs.test.ts
  • tests/issue-links.test.ts
  • tests/json-output.test.ts
  • tests/labels.test.ts
  • tests/modules.test.ts
  • tests/new-schemas.test.ts
  • tests/new-schemas2.test.ts
  • tests/output.test.ts
  • tests/pages.test.ts
  • tests/project-features.test.ts
  • tests/projects.test.ts
  • tests/resolve.test.ts
  • tests/schemas.test.ts
  • tests/user-config.test.ts
  • tests/xml-output.test.ts

Comment thread src/commands/init.ts
Comment thread src/commands/init.ts Outdated
Comment thread src/commands/modules.ts
Comment on lines 133 to +141
const lines = results.map((mi) => {
if (mi.issue_detail) {
if ("issue_detail" in mi && mi.issue_detail) {
const seq = String(mi.issue_detail.sequence_id).padStart(3, " ");
return `${key}-${seq} ${mi.issue_detail.name} (${mi.id})`;
}
if ("sequence_id" in mi) {
const seq = String(mi.sequence_id).padStart(3, " ");
return `${key}-${seq} ${mi.name}`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep a removable identifier in the raw-issue branch.

When module-issues comes back as raw issue payloads, this branch prints only PROJ-123 Name. That leaves plane modules issues remove ... <module-issue-id> with nothing discoverable to consume, even though the help text says the identifier comes from modules issues list.

💡 Minimal fix
 		if ("sequence_id" in mi) {
 			const seq = String(mi.sequence_id).padStart(3, " ");
-			return `${key}-${seq}  ${mi.name}`;
+			return `${key}-${seq}  ${mi.name}  (${mi.id})`;
 		}

As per coding guidelines, "Prefer discoverable workflows: if a command depends on UUIDs or join IDs, expose a list or get path to help obtain them."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/modules.ts` around lines 133 - 141, The list output branch that
handles raw issue payloads (the `if ("sequence_id" in mi)` branch inside the
`results.map` call) currently returns only `${key}-${seq}  ${mi.name}`, which
omits any removable identifier; update that branch to append a discoverable
identifier so consumers can run `modules issues remove <id>` — e.g. return
`${key}-${seq}  ${mi.name}  (${mi.id ?? mi.issue_id ?? mi.uuid})` or otherwise
detect and include the unique id field present on `mi`; ensure you only render
the parentheses if an identifier exists.

Comment thread src/config.ts
Comment thread src/user-config.ts
Comment on lines +187 to +200
export function getConfigDetails(cwd = process.cwd()): PlaneConfigDetails {
const globalConfig = readGlobalStoredConfig();
const localConfigFile = findNearestLocalConfigFilePath(cwd);
const localConfig = localConfigFile ? readConfigFile(localConfigFile) : {};

const envToken = process.env.PLANE_API_TOKEN;
const envHost = process.env.PLANE_HOST;
const envWorkspace = process.env.PLANE_WORKSPACE;
const envProject = process.env.PLANE_PROJECT;

const token = envToken ?? localConfig.token ?? globalConfig.token ?? "";
const host = normalizeHost(
envHost ?? localConfig.host ?? globalConfig.host ?? DEFAULT_HOST,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don't compose connection settings across scopes.

This per-field merge lets a repo-local .plane/config.json override host while token falls through from env/global config. Running the CLI in an untrusted repo could then send a real API token to an attacker-controlled Plane URL.

Resolve host, workspace, and token from a single scope, or refuse to use a lower-precedence host unless the token comes from that same scope.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/user-config.ts` around lines 187 - 200, getConfigDetails currently merges
token, host, and workspace independently across env/local/global which can mix
scopes; change it so the connection settings are chosen from a single scope:
inspect scopes in precedence order (env, localConfigFile, globalConfig) to find
which scope supplies the token first and then pick host and workspace only from
that same scope (use DEFAULT_HOST only if that chosen scope lacks host),
otherwise if no token is found fall back to the first scope that supplies
host/workspace but do not combine values from different scopes; update the logic
around token, host, and envWorkspace/envProject resolution in getConfigDetails
(and keep using normalizeHost) to enforce same-scope selection rather than
per-field merging.

Comment thread src/user-config.ts Outdated
Comment thread tests/user-config.test.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/commands/intake.ts (1)

25-40: Consider caching intake issues to avoid redundant API calls.

resolveIntakeMutationId fetches all intake issues to find one by ID. For accept/reject operations immediately after list, this duplicates the API call. For high-volume projects this could add latency.

This is acceptable for now, but if performance becomes a concern, consider short-lived in-memory caching or accepting the mutation ID directly from list output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/intake.ts` around lines 25 - 40, resolveIntakeMutationId
currently issues an API request every time it runs, causing duplicate fetches
for back-to-back operations like accept/reject after list; add a short-lived
in-memory cache keyed by projectId (or accept an optionally-provided mutationId)
so list results can be reused: modify resolveIntakeMutationId to first check a
cache map (e.g., a simple Map<string, IntakeIssuesResponse> or Map<string,
record>) before calling api.get, populate the cache when list is called (or when
resolveIntakeMutationId fetches), and expire entries after a small TTL or when
list is explicitly refreshed; reference resolveIntakeMutationId, list, accept,
reject to wire the cache usage and ensure behavior falls back to the existing
api.get when no cached entry exists.
src/user-config.ts (1)

77-87: Redundant chmodSync call after writeFileSync.

The mode: 0o600 in writeFileSync options already sets file permissions. The subsequent chmodSync is unnecessary.

♻️ Suggested fix
 function writeConfigFile(filePath: string, config: StoredPlaneConfig): void {
 	fs.mkdirSync(path.dirname(filePath), { recursive: true });
 	fs.writeFileSync(
 		filePath,
 		`${JSON.stringify(cleanConfig(config), null, 2)}\n`,
 		{
 			mode: 0o600,
 		},
 	);
-	fs.chmodSync(filePath, 0o600);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/user-config.ts` around lines 77 - 87, In writeConfigFile remove the
redundant fs.chmodSync call: rely on fs.writeFileSync(filePath, ..., { mode:
0o600 }) to set permissions and delete the fs.chmodSync(filePath, 0o600) line to
avoid the duplicate operation; keep fs.mkdirSync and the writeFileSync call and
ensure cleanConfig(config) remains used when serializing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/init.ts`:
- Around line 489-498: The code uses forbidden non-null assertions on
projectsResult (projectsResult!._tag and projectsResult!.right); change the
selectedProject assignment to safely handle a possibly undefined projectsResult
by checking projectsResult?. _tag === "Right" (or using an isRight helper) and
then using projectsResult?.right?.find(...) so you only access .right when
projectsResult is present and Right, ensuring no non-null assertions remain;
update the logic around activeDefaultProject/scope as needed to preserve the
original behavior.

---

Nitpick comments:
In `@src/commands/intake.ts`:
- Around line 25-40: resolveIntakeMutationId currently issues an API request
every time it runs, causing duplicate fetches for back-to-back operations like
accept/reject after list; add a short-lived in-memory cache keyed by projectId
(or accept an optionally-provided mutationId) so list results can be reused:
modify resolveIntakeMutationId to first check a cache map (e.g., a simple
Map<string, IntakeIssuesResponse> or Map<string, record>) before calling
api.get, populate the cache when list is called (or when resolveIntakeMutationId
fetches), and expire entries after a small TTL or when list is explicitly
refreshed; reference resolveIntakeMutationId, list, accept, reject to wire the
cache usage and ensure behavior falls back to the existing api.get when no
cached entry exists.

In `@src/user-config.ts`:
- Around line 77-87: In writeConfigFile remove the redundant fs.chmodSync call:
rely on fs.writeFileSync(filePath, ..., { mode: 0o600 }) to set permissions and
delete the fs.chmodSync(filePath, 0o600) line to avoid the duplicate operation;
keep fs.mkdirSync and the writeFileSync call and ensure cleanConfig(config)
remains used when serializing.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9aac4ba5-3154-48bc-9cd2-b84bf052b0dc

📥 Commits

Reviewing files that changed from the base of the PR and between d8e0845 and 7794ca4.

📒 Files selected for processing (8)
  • src/commands/init.ts
  • src/commands/intake.ts
  • src/commands/issue.ts
  • src/commands/modules.ts
  • src/commands/pages.ts
  • src/config.ts
  • src/user-config.ts
  • tests/user-config.test.ts
✅ Files skipped from review due to trivial changes (1)
  • tests/user-config.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/commands/issue.ts
  • src/commands/pages.ts
  • src/config.ts

Comment thread src/commands/init.ts Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Autofix skipped. No unresolved CodeRabbit review comments with fix instructions found.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/commands/pages.ts (2)

67-68: Extract the repeated availability message string to one helper/constant.

The same message template is duplicated; centralizing avoids drift when wording changes later.

♻️ Proposed tidy-up
+function pageUnavailableMessage(projectKey: string): string {
+	return `Project pages are not available for ${projectKey} on this Plane instance or API version.`;
+}
+
 function mapPageAvailabilityError<A>(
 	effect: Effect.Effect<A, Error>,
 	message: string,
 ): Effect.Effect<A, Error> {
@@
 		const raw = yield* mapPageAvailabilityError(
 			api.get(`projects/${id}/pages/`),
-			`Project pages are not available for ${key} on this Plane instance or API version.`,
+			pageUnavailableMessage(key),
 		);
@@
 		const raw = yield* mapPageAvailabilityError(
 			api.post(`projects/${id}/pages/`, body),
-			`Project pages are not available for ${key} on this Plane instance or API version.`,
+			pageUnavailableMessage(key),
 		);

Also applies to: 148-149

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/pages.ts` around lines 67 - 68, Extract the duplicated message
template into a single helper so both call sites reuse it: add a constant like
PROJECT_PAGES_UNAVAILABLE = (key: string) => `Project pages are not available
for ${key} on this Plane instance or API version.` near the top of the pages
command file, then replace the two literal usages (the duplicated template
strings in src/commands/pages.ts) with calls to PROJECT_PAGES_UNAVAILABLE(key);
update imports/exports if needed so the helper is visible to both locations.

41-57: Use structured HTTP status checks instead of parsing error text.

isNotFoundError depends on the exact "HTTP 404:" message format in the error string. If src/api.ts changes the message format, this silent breaks. Consider adding a structured error type or status field to the API layer (e.g., a custom ApiError class with a status property) so status checks can match on a concrete field rather than a fragile regex pattern. This same pattern exists in src/issue-support.ts, so a unified solution across the codebase would improve maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/pages.ts` around lines 41 - 57, The current isNotFoundError
implementation relies on parsing error.message which is fragile; introduce a
structured error type (e.g., ApiError with a numeric status property) in the API
layer and update isNotFoundError to test error as ApiError and check
error.status === 404 (instead of regex), then update mapPageAvailabilityError to
rely on the new isNotFoundError behavior; apply the same pattern used in
src/issue-support.ts so both modules check a concrete ApiError.status rather
than error.message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/init.ts`:
- Around line 478-486: The current check uses (savedDefaultProject ??
effective.defaultProject).trim() which throws if both are undefined; change to
compute a local project variable (e.g., const project = savedDefaultProject ??
effective.defaultProject) and guard using optional chaining or explicit falsy
check (e.g., if (project?.trim()) { ... }) before calling trim; then pass the
original savedDefaultProject and effective.defaultProject into describeValue and
Console.log exactly as before (referencing savedDefaultProject,
effective.defaultProject, scope, describeValue, and Console.log) so you avoid
calling .trim() on undefined.

---

Nitpick comments:
In `@src/commands/pages.ts`:
- Around line 67-68: Extract the duplicated message template into a single
helper so both call sites reuse it: add a constant like
PROJECT_PAGES_UNAVAILABLE = (key: string) => `Project pages are not available
for ${key} on this Plane instance or API version.` near the top of the pages
command file, then replace the two literal usages (the duplicated template
strings in src/commands/pages.ts) with calls to PROJECT_PAGES_UNAVAILABLE(key);
update imports/exports if needed so the helper is visible to both locations.
- Around line 41-57: The current isNotFoundError implementation relies on
parsing error.message which is fragile; introduce a structured error type (e.g.,
ApiError with a numeric status property) in the API layer and update
isNotFoundError to test error as ApiError and check error.status === 404
(instead of regex), then update mapPageAvailabilityError to rely on the new
isNotFoundError behavior; apply the same pattern used in src/issue-support.ts so
both modules check a concrete ApiError.status rather than error.message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0524098b-68f7-42d5-9df8-0758da2c6989

📥 Commits

Reviewing files that changed from the base of the PR and between 7794ca4 and 9a1f054.

📒 Files selected for processing (2)
  • src/commands/init.ts
  • src/commands/pages.ts

Comment thread src/commands/init.ts
Comment on lines +478 to +486
if ((savedDefaultProject ?? effective.defaultProject).trim()) {
yield* Console.log(
` Project: ${describeValue(
scope,
savedDefaultProject,
savedDefaultProject ?? effective.defaultProject,
)}`,
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

TypeError when no default project is configured.

If both savedDefaultProject and effective.defaultProject are undefined, calling .trim() on the result of the nullish coalescing operator throws a TypeError. This can happen when running plane init on a fresh setup with no project configured.

🐛 Proposed fix using optional chaining
-		if ((savedDefaultProject ?? effective.defaultProject).trim()) {
+		if ((savedDefaultProject ?? effective.defaultProject)?.trim()) {
 			yield* Console.log(
 				`  Project:   ${describeValue(
 					scope,
 					savedDefaultProject,
-					savedDefaultProject ?? effective.defaultProject,
+					savedDefaultProject ?? effective.defaultProject ?? "",
 				)}`,
 			);
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ((savedDefaultProject ?? effective.defaultProject).trim()) {
yield* Console.log(
` Project: ${describeValue(
scope,
savedDefaultProject,
savedDefaultProject ?? effective.defaultProject,
)}`,
);
}
if ((savedDefaultProject ?? effective.defaultProject)?.trim()) {
yield* Console.log(
` Project: ${describeValue(
scope,
savedDefaultProject,
savedDefaultProject ?? effective.defaultProject ?? "",
)}`,
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/init.ts` around lines 478 - 486, The current check uses
(savedDefaultProject ?? effective.defaultProject).trim() which throws if both
are undefined; change to compute a local project variable (e.g., const project =
savedDefaultProject ?? effective.defaultProject) and guard using optional
chaining or explicit falsy check (e.g., if (project?.trim()) { ... }) before
calling trim; then pass the original savedDefaultProject and
effective.defaultProject into describeValue and Console.log exactly as before
(referencing savedDefaultProject, effective.defaultProject, scope,
describeValue, and Console.log) so you avoid calling .trim() on undefined.

@backslash-ux backslash-ux merged commit 7b83c13 into main Mar 31, 2026
4 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Apr 2, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants