Add bulk issue workflows and stable structured output#6
Conversation
- Add src/agent-skills.ts with agent registry and skill writing functions
- Modify plane init --local to prompt for agent skill installation
- Install skills to .{agent}/skills/plane-cli/SKILL.md following Vercel convention
- Remove SKILL.md import into AGENTS.md (skills stay in agent dirs only)
- Update README.md and CHANGELOG.md documentation
…xml options - Add jsonOption and xmlOption CLI option helpers - Add issueRef, normalizeIssueForJson, issueMutationResult helpers - Add issueUrl using host/workspace from user config
- Add normalizeArgv to reorder flags before positional args for known commands - Teach bin.ts to preprocess argv before passing to @effect/cli
- Add plane project context to print local .plane/project-context.json - Support --json output and cross-check against resolved project
- Add resolveDescriptionInput supporting --description, --from-file, and --stdin - Add findDuplicateCandidates with title exact-match and token-similarity modes
…N output) - Add --from-file and --stdin for long HTML descriptions - Add --dedupe to report possible duplicates before creating - Emit structured JSON for create/update when --json is passed
- Add plane issues bulk-create with --file, --dry-run, and shared defaults - Add plane issues bulk-update requiring ref per record - Support dedupe detection, validation, and JSON output
- cycles: list/create/update - intake: list/accept/reject - labels: list/create/delete - members: list - modules: list/create - pages: list/get/create/update - projects: list/current/use - states: list - stats: project/workspace aggregation
- argv normalization - help output coverage - issue agent description resolution and dedupe - bulk issue create/update - members and states JSON output - project agents - project context command
- Document bulk-create, bulk-update, project context - Document --from-file, --stdin, --dedupe - Document argument-order tolerance and enhanced JSON output
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughThis PR implements version 1.2.1 with comprehensive structured output support across all CLI commands, new bulk issue create/update workflows from JSON files, AI agent skill installation during local initialization, project context snapshot querying, and improved argv normalization for flexible command-line parsing. Changesv1.2.1 Feature Release
Sequence Diagram(s)sequenceDiagram
participant User as CLI User
participant bin as bin.ts
participant app as app.ts
participant argv as argv.ts
participant cmd as Command Handler
participant api as API
User->>bin: plane issue create --from-file desc.html --title "Bug" PROJ
bin->>app: isRootHelpRequest(argv)?
alt Is Root Help
app-->>User: render root help
else Normal Command
bin->>argv: normalizeArgv(argv)
argv-->>bin: normalized argv
bin->>cmd: cli(normalized)
cmd->>cmd: resolveDescriptionInput (file → HTML)
cmd->>cmd: findDuplicateCandidates
alt Duplicates Found
cmd-->>User: duplicate candidates (JSON or plain)
else Create New
cmd->>api: POST /issues/ {title, description_html}
api-->>cmd: created issue
cmd->>cmd: normalizeIssueForJson
cmd-->>User: {action: "created", issue, ref, url}
end
end
sequenceDiagram
participant User as CLI User
participant bin as bin.ts
participant cmd as issues-bulk handler
participant file as JSON file
participant api as API
User->>bin: plane issues bulk-create --file bulk.json --dry-run PROJ
bin->>cmd: issuesBulkCreateHandler(...)
cmd->>file: readBulkFile(bulk.json)
file-->>cmd: array of records
cmd->>cmd: per-record: buildCreatePayload
cmd->>cmd: applySharedIssueFields
cmd->>cmd: duplicateCandidates check
alt Dry Run
cmd-->>User: [dry_run, would_create true/false, errors]
else Real Execution
cmd->>api: POST /issues/ per record
api-->>cmd: created issues
cmd->>api: POST /cycles/attach, /modules/attach
cmd-->>User: [created, updated counts, per-record results]
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (9)
tests/agent-skills.test.ts (1)
168-175: ⚡ Quick winStrengthen this test assertion to catch real regressions.
The current check is too permissive and can pass even when behavior drifts; tighten it to assert expected packaged content in this repo context.
✅ Suggested test tightening
- it("returns null when SKILL.md does not exist", async () => { - // Mock the import to point to a non-existent file - const { readPackageSkillContent } = await import("`@/agent-skills`"); - const result = readPackageSkillContent(); - // This will return actual content since we're in the real repo - // The test validates the function works correctly - expect(typeof result === "string" || result === null).toBe(true); - }); + it("returns packaged SKILL.md content", async () => { + const { readPackageSkillContent } = await import("`@/agent-skills`"); + const result = readPackageSkillContent(); + expect(result).not.toBeNull(); + expect(result).toContain("# Plane CLI"); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/agent-skills.test.ts` around lines 168 - 175, The test uses readPackageSkillContent() but asserts too loosely; change the assertion to require either null or a real non-empty markdown string: call readPackageSkillContent(), then expect(result === null || (typeof result === "string" && result.length > 0 && /#\s+/.test(result))). This ensures the function returns meaningful packaged SKILL.md content in this repo context while still allowing null for absent files.tests/argv.test.ts (1)
4-50: 💤 Low valueConsider edge case coverage for argv normalization.
The current tests cover the main happy paths (list and create commands with options before positional args). Consider adding tests for edge cases to improve robustness:
- Multiple positional arguments (e.g.,
@current@another-project``)- No positional arguments (options only)
- Empty argv arrays
- The
--separator convention- Commands without normalization requirements
However, if these edge cases are intentionally out of scope for this PR, the current coverage is adequate for the advertised functionality.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/argv.test.ts` around lines 4 - 50, Add additional unit tests in tests/argv.test.ts for normalizeArgv to cover edge cases: add cases for multiple positional args (e.g., ["bun","plane","issues","list","`@current`","`@another`","--state","Todo"]), options-only inputs (no positional args), empty argv arrays, handling of the "--" separator (ensure arguments after "--" are treated as positional and not re-ordered), and a command that should not be normalized; use descriptive it() names and assertions similar to the existing tests to validate expected order for normalizeArgv.src/commands/intake.ts (2)
104-115: ⚡ Quick winConsider decoding the API response for consistency with other mutation commands.
The
intakeAccepthandler captures the raw API response without decoding it, while other mutation commands in the codebase (e.g., labels create, modules create) decode their responses before including them in the JSON output. Additionally, echoingintakeIdback to the caller is redundant since they provided it.Suggested approach
Define a schema for the intake PATCH response (or reuse
IntakeIssueSchemaif appropriate) and decode before returning:const raw = yield* api.patch( `projects/${id}/intake-issues/${mutationId}/`, { status: 1, }, ); +const intake = yield* decodeOrFail(IntakeIssueSchema, raw); if (jsonMode) { yield* Console.log( - JSON.stringify({ action: "accepted", intakeId, result: raw }, null, 2), + JSON.stringify({ action: "accepted", intake }, null, 2), ); return; }This pattern would match other mutation commands and eliminate the redundant
intakeIdfield.Based on learnings: "Keep Plane concepts explicit and close to the underlying API domain" - decoding responses ensures we're working with validated, typed entities rather than raw API payloads.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/intake.ts` around lines 104 - 115, The handler currently calls api.patch in intakeAccept and stores the unvalidated response in raw and echoes intakeId; instead, decode the PATCH response using the appropriate schema (reuse IntakeIssueSchema or define an IntakePatchResponse schema) after the api.patch call, replace raw with the validated/typed result, and return JSON with { action: "accepted", result: decoded } (remove the redundant intakeId echo); update the jsonMode branch that logs JSON.stringify to use the decoded object so the output matches other mutation commands like labels create and modules create.
143-154: ⚡ Quick winConsider decoding the API response for consistency with other mutation commands.
Same issue as
intakeAccept- the handler usesresult: rawinstead of decoding the response, and includes the redundantintakeIdfield.Suggested fix
const raw = yield* api.patch( `projects/${id}/intake-issues/${mutationId}/`, { status: -1, }, ); +const intake = yield* decodeOrFail(IntakeIssueSchema, raw); if (jsonMode) { yield* Console.log( - JSON.stringify({ action: "rejected", intakeId, result: raw }, null, 2), + JSON.stringify({ action: "rejected", intake }, null, 2), ); return; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/intake.ts` around lines 143 - 154, The handler currently logs the raw API response (variable raw) and includes a redundant intakeId field; update the intake reject flow to decode the API response the same way intakeAccept does: call the shared response decoder (the same helper used by intakeAccept) on raw and include the decoded object under result in the JSON output, and remove the redundant intakeId property from the JSON.stringify payload; keep the jsonMode conditional and Console.log usage but replace result: raw with result: decodedResponse (and drop intakeId) so output structure matches other mutation commands.src/commands/issue-sub.ts (1)
208-217: ⚡ Quick winConsider decoding the comment update response for consistency.
The
issueCommentUpdatehandler usesresult: rawwhile other mutation handlers in this same file (e.g.,issueLinkAddHandler,issueWorklogsAddHandler) decode their responses before including them in JSON output. For consistency, consider decoding the PATCH response using an appropriate schema.Suggested approach
const raw = yield* api.patch( `projects/${projectId}/issues/${issue.id}/comments/${commentId}/`, { comment_html: `<p>${escaped}</p>` }, ); +const comment = yield* decodeOrFail(CommentSchema, raw); if (jsonMode) { yield* Console.log( - JSON.stringify({ action: "updated", commentId, result: raw }, null, 2), + JSON.stringify({ action: "updated", comment }, null, 2), ); return; }This would match the pattern used by link add and worklogs add in this file.
As per coding guidelines: "Preserve shared
--jsonand--xmloutput behavior instead of inventing per-command machine formats" - using decoded entities consistently helps maintain a predictable output structure.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/issue-sub.ts` around lines 208 - 217, The JSON branch currently returns the raw PATCH response ("result: raw") from the api.patch call in the issueCommentUpdate handler; change it to decode the response using the same decoder/schema used by other mutation handlers (e.g., the issue/comment response decoder used by issueLinkAddHandler/issueWorklogsAddHandler) and return the decoded entity in the JSON output (replace result: raw with result: decodedComment or similar after calling the appropriate decode function on the api.patch response), preserving the existing jsonMode and Console.log behavior.src/commands/projects.ts (1)
123-133: 💤 Low valueConsider consistent output structure for JSON mode.
The
projectsCurrenthandler outputs two different JSON structures depending on whether the full project details are found:
- When not found:
{ project: key, source }- When found:
{ project: { id, identifier, name, ... }, source }This variance could complicate JSON parsing for consumers. Consider always including a consistent structure, e.g., always using
{ project: { identifier: key }, source }with optional additional fields, or using separate fields like{ projectKey: key, projectDetails: project | null, source }.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/projects.ts` around lines 123 - 133, The JSON output from the projectsCurrent handler is inconsistent between the "not found" branch (JSON.stringify({ project: key, source })) and the "found" branch (JSON.stringify({ project, source })); update the handler (look for projectsCurrent, variables key, project, source, jsonMode) to emit a single consistent shape such as JSON.stringify({ projectKey: key, projectDetails: project || null, source }) (or JSON.stringify({ project: { identifier: key, ...project }, source }) if you prefer nested), and replace both JSON.stringify calls so consumers always get the same structure.src/commands/issue.ts (1)
171-172: ⚡ Quick winReuse parsed project key instead of re-splitting
reffor JSON output.Line 262 re-parses the ref string (
split("-")) even though parsing already happened on Line 171. Reuse the parsed key to avoid drift from canonical ref parsing behavior.Based on learnings: Fix root causes instead of layering on ad hoc patches when practical.
Also applies to: 257-264
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/issue.ts` around lines 171 - 172, The code currently reparses the issue ref string (via split("-")) later instead of reusing the canonical parsed values; update the JSON/output logic to use the already-parsed projectId and seq returned by parseIssueRef (used when calling findIssueBySeq) rather than splitting ref again, and remove the ad-hoc ref.split("-") logic so all output uses the single source of truth from parseIssueRef (adjust any variables/fields built from the split to reference projectId and seq instead).src/commands/issues-bulk.ts (2)
278-290: ⚡ Quick winSkip the refresh GET in text mode.
outputBulkResults()ignoresresultunlessjsonModeis enabled, but this path always refetches the issue. That adds one extra GET per updated record with no user-visible change in the default mode.♻️ Suggested change
- const refreshed = yield* decodeOrFail( - IssueSchema, - yield* api.get(`projects/${projectId}/issues/${issue.id}/`), - ); + const refreshed = jsonMode + ? yield* decodeOrFail( + IssueSchema, + yield* api.get(`projects/${projectId}/issues/${issue.id}/`), + ) + : updated; results.push({ index, ref, action: "updated",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/issues-bulk.ts` around lines 278 - 290, The code always refetches the updated issue via decodeOrFail(api.get(...)) before pushing the result, which is unnecessary when jsonMode is false; change the logic in the update path so that you only call api.get and decodeOrFail (using IssueSchema) when jsonMode is true, otherwise skip the GET and pass the local updated object into issueMutationResult; update the block around the refreshed variable and the results.push call (referencing IssueSchema, api.get, issueMutationResult, refreshed, updated, and jsonMode) so the extra network request is avoided in non-JSON output mode.
127-145: ⚡ Quick winAvoid preloading every issue when
--dedupeis off.Line 130 fetches the full issue list even when duplicate detection is disabled, so the normal bulk-create path pays an extra project-wide GET for no benefit.
♻️ Suggested change
return Effect.gen(function* () { const records = yield* readBulkFile(file); const { key, id: projectId } = yield* resolveProject(project); - const existing = yield* loadIssues(projectId); + const existing = Option.isSome(dedupe) + ? yield* loadIssues(projectId) + : []; const results: PlannedResult[] = [];🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/issues-bulk.ts` around lines 127 - 145, The code always calls loadIssues(projectId) even when dedupe is not provided; change logic so loadIssues is only invoked when Option.isSome(dedupe). Specifically, remove the unconditional const existing = yield* loadIssues(projectId) and instead, after resolving project (yield* resolveProject(project)), conditionally call yield* loadIssues(projectId) only when Option.isSome(dedupe) and store it in a local (e.g., existing) used by duplicateCandidates; ensure duplicateCandidates(key, existing, title, dedupe.value) is only called when existing is available and when dedupe is Some, otherwise supply an empty array.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/commands/cycles.ts`:
- Around line 237-246: The success message after updating a cycle uses the
pre-update `resolved` values and can be stale; change the non-JSON log to use
the decoded `updated` cycle returned by `decodeOrFail` instead of `resolved`
(i.e., log `updated.name` and `updated.id`), ensuring the `api.patch` -> `raw`
-> `updated` flow is reflected in the Console.log call in the function where
`raw`, `updated`, `resolved`, `jsonMode`, `decodeOrFail`, and `CycleSchema` are
used.
In `@src/commands/issues-bulk.ts`:
- Around line 542-559: The output logic in outputBulkResults (handling
PlannedResult, jsonMode and Console.log) must be moved into the shared
output/format layers: extract the human-readable formatter into a new format
function (e.g., formatBulkResults(results: PlannedResult[])) in the shared
formatting module and expose a shared machine-readable serializer in the output
module (reusing the repo’s standard --json behavior) so the command only calls
the shared helpers; replace the local JSON and text rendering in
outputBulkResults with calls to the shared output.serialize/results formatter
and remove any direct Console.log formatting so the command remains focused on
planning/mutation.
- Around line 174-202: Wrap each per-record workflow (the sequence using
api.post/api.patch, decodeOrFail, attachCycleAndModule, and the optional api.get
refresh—see the code around api.post, created, attachCycleAndModule,
resultIssue, and issueMutationResult) in a try/catch so a single failure does
not abort the loop; on error push a results entry for that index with action
"failed" and a result containing the error message/details, then continue to the
next record, and ensure outputBulkResults(results) is still called after the
loop; apply the same try/catch pattern to the other bulk block that performs
updates (the block referenced by the reviewer where api.patch and refresh
happen).
In `@src/issue-agent.ts`:
- Around line 131-135: normalizeTitle currently strips anything not in [a-z0-9],
which removes non‑Latin characters; update normalizeTitle to be Unicode-aware by
first applying value.normalize('NFKD'), then remove combining marks (diacritics)
via a /\p{M}/gu replace, then replace any run of characters that are not Unicode
letters or numbers using /[^\p{L}\p{N}]+/gu with a single space, finally
.toLowerCase() and .trim(); this preserves CJK and other scripts while folding
diacritics for consistent dedupe.
- Around line 61-67: The current dedupe logic only fetches a single page via
api.get(`projects/${projectId}/issues/?order_by=sequence_id`) and then decodes
IssuesResponseSchema, so duplicates beyond the first page are missed; update the
code that builds candidates (the call sites around api.get, IssuesResponseSchema
decoding, and the mapping that uses normalizeTitle and results) to fetch and
aggregate all paginated pages (follow the API's pagination cursor/next token or
increment page parameters) before running the dedupe mapping, ensuring you
collect all results across pages; apply the same change to the similar block
referenced around the later section (the code around lines 88-93) so both places
iterate through pages until no more results and then run the existing
normalizeTitle-based duplicate detection.
In `@tests/project-context-command.test.ts`:
- Around line 85-93: The test currently accepts either JSON or plain-text output
which makes it flaky; update the test so it forces and asserts a single
deterministic format: change the command invocation in this test to request JSON
output explicitly (e.g., add the CLI flag --json or --format=json where the
command is run that produces output), remove the else text-branch, parse output
with JSON.parse(output) and keep the JSON assertions
(expect(parsed.project.identifier).toBe("ACME");
expect(parsed.helpers.labels.total).toBe(1)); ensure the test no longer branches
on output.trim().startsWith("{") so it deterministically validates the JSON
shape.
---
Nitpick comments:
In `@src/commands/intake.ts`:
- Around line 104-115: The handler currently calls api.patch in intakeAccept and
stores the unvalidated response in raw and echoes intakeId; instead, decode the
PATCH response using the appropriate schema (reuse IntakeIssueSchema or define
an IntakePatchResponse schema) after the api.patch call, replace raw with the
validated/typed result, and return JSON with { action: "accepted", result:
decoded } (remove the redundant intakeId echo); update the jsonMode branch that
logs JSON.stringify to use the decoded object so the output matches other
mutation commands like labels create and modules create.
- Around line 143-154: The handler currently logs the raw API response (variable
raw) and includes a redundant intakeId field; update the intake reject flow to
decode the API response the same way intakeAccept does: call the shared response
decoder (the same helper used by intakeAccept) on raw and include the decoded
object under result in the JSON output, and remove the redundant intakeId
property from the JSON.stringify payload; keep the jsonMode conditional and
Console.log usage but replace result: raw with result: decodedResponse (and drop
intakeId) so output structure matches other mutation commands.
In `@src/commands/issue-sub.ts`:
- Around line 208-217: The JSON branch currently returns the raw PATCH response
("result: raw") from the api.patch call in the issueCommentUpdate handler;
change it to decode the response using the same decoder/schema used by other
mutation handlers (e.g., the issue/comment response decoder used by
issueLinkAddHandler/issueWorklogsAddHandler) and return the decoded entity in
the JSON output (replace result: raw with result: decodedComment or similar
after calling the appropriate decode function on the api.patch response),
preserving the existing jsonMode and Console.log behavior.
In `@src/commands/issue.ts`:
- Around line 171-172: The code currently reparses the issue ref string (via
split("-")) later instead of reusing the canonical parsed values; update the
JSON/output logic to use the already-parsed projectId and seq returned by
parseIssueRef (used when calling findIssueBySeq) rather than splitting ref
again, and remove the ad-hoc ref.split("-") logic so all output uses the single
source of truth from parseIssueRef (adjust any variables/fields built from the
split to reference projectId and seq instead).
In `@src/commands/issues-bulk.ts`:
- Around line 278-290: The code always refetches the updated issue via
decodeOrFail(api.get(...)) before pushing the result, which is unnecessary when
jsonMode is false; change the logic in the update path so that you only call
api.get and decodeOrFail (using IssueSchema) when jsonMode is true, otherwise
skip the GET and pass the local updated object into issueMutationResult; update
the block around the refreshed variable and the results.push call (referencing
IssueSchema, api.get, issueMutationResult, refreshed, updated, and jsonMode) so
the extra network request is avoided in non-JSON output mode.
- Around line 127-145: The code always calls loadIssues(projectId) even when
dedupe is not provided; change logic so loadIssues is only invoked when
Option.isSome(dedupe). Specifically, remove the unconditional const existing =
yield* loadIssues(projectId) and instead, after resolving project (yield*
resolveProject(project)), conditionally call yield* loadIssues(projectId) only
when Option.isSome(dedupe) and store it in a local (e.g., existing) used by
duplicateCandidates; ensure duplicateCandidates(key, existing, title,
dedupe.value) is only called when existing is available and when dedupe is Some,
otherwise supply an empty array.
In `@src/commands/projects.ts`:
- Around line 123-133: The JSON output from the projectsCurrent handler is
inconsistent between the "not found" branch (JSON.stringify({ project: key,
source })) and the "found" branch (JSON.stringify({ project, source })); update
the handler (look for projectsCurrent, variables key, project, source, jsonMode)
to emit a single consistent shape such as JSON.stringify({ projectKey: key,
projectDetails: project || null, source }) (or JSON.stringify({ project: {
identifier: key, ...project }, source }) if you prefer nested), and replace both
JSON.stringify calls so consumers always get the same structure.
In `@tests/agent-skills.test.ts`:
- Around line 168-175: The test uses readPackageSkillContent() but asserts too
loosely; change the assertion to require either null or a real non-empty
markdown string: call readPackageSkillContent(), then expect(result === null ||
(typeof result === "string" && result.length > 0 && /#\s+/.test(result))). This
ensures the function returns meaningful packaged SKILL.md content in this repo
context while still allowing null for absent files.
In `@tests/argv.test.ts`:
- Around line 4-50: Add additional unit tests in tests/argv.test.ts for
normalizeArgv to cover edge cases: add cases for multiple positional args (e.g.,
["bun","plane","issues","list","`@current`","`@another`","--state","Todo"]),
options-only inputs (no positional args), empty argv arrays, handling of the
"--" separator (ensure arguments after "--" are treated as positional and not
re-ordered), and a command that should not be normalized; use descriptive it()
names and assertions similar to the existing tests to validate expected order
for normalizeArgv.
🪄 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: e05d5df7-2881-4fc6-ba7e-48013aa7d8bc
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (42)
.gitignore.husky/pre-commitAGENTS.mdCHANGELOG.mdREADME.mdSKILL.mdpackage.jsonsrc/agent-skills.tssrc/app.tssrc/argv.tssrc/bin.tssrc/commands/cycles.tssrc/commands/init.tssrc/commands/intake.tssrc/commands/issue-sub.tssrc/commands/issue.tssrc/commands/issues-bulk.tssrc/commands/issues.tssrc/commands/labels.tssrc/commands/members.tssrc/commands/modules.tssrc/commands/pages.tssrc/commands/project.tssrc/commands/projects.tssrc/commands/states.tssrc/commands/stats.tssrc/issue-agent.tssrc/output.tssrc/project-agents.tstests/agent-skills.test.tstests/app.test.tstests/argv.test.tstests/help-output.test.tstests/issue-agent.test.tstests/issue-commands.test.tstests/issues-bulk.test.tstests/json-output.test.tstests/members-states.test.tstests/project-agents.test.tstests/project-context-command.test.tstests/project-features.test.tstests/xml-output.test.ts
💤 Files with no reviewable changes (3)
- AGENTS.md
- src/project-agents.ts
- tests/project-features.test.ts
| const raw = yield* api.patch(`projects/${id}/cycles/${resolved.id}/`, body); | ||
| const updated = yield* decodeOrFail(CycleSchema, raw); | ||
| if (jsonMode) { | ||
| yield* Console.log( | ||
| JSON.stringify({ action: "updated", cycle: updated }, null, 2), | ||
| ); | ||
| return; | ||
| } | ||
| yield* Console.log(`Updated cycle: ${resolved.name} (${resolved.id})`); | ||
| }); |
There was a problem hiding this comment.
Use the decoded updated cycle in the success message.
After rename/update, the current message can print stale values from resolved instead of the post-update state.
✏️ Suggested fix
- yield* Console.log(`Updated cycle: ${resolved.name} (${resolved.id})`);
+ yield* Console.log(`Updated cycle: ${updated.name} (${updated.id})`);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/commands/cycles.ts` around lines 237 - 246, The success message after
updating a cycle uses the pre-update `resolved` values and can be stale; change
the non-JSON log to use the decoded `updated` cycle returned by `decodeOrFail`
instead of `resolved` (i.e., log `updated.name` and `updated.id`), ensuring the
`api.patch` -> `raw` -> `updated` flow is reflected in the Console.log call in
the function where `raw`, `updated`, `resolved`, `jsonMode`, `decodeOrFail`, and
`CycleSchema` are used.
| const raw = yield* api.post( | ||
| `projects/${projectId}/issues/`, | ||
| planned.right.body, | ||
| ); | ||
| const created = yield* decodeOrFail(IssueSchema, raw); | ||
| yield* attachCycleAndModule( | ||
| projectId, | ||
| created.id, | ||
| planned.right.cycle, | ||
| planned.right.module, | ||
| ); | ||
| const resultIssue = jsonMode | ||
| ? yield* decodeOrFail( | ||
| IssueSchema, | ||
| yield* api.get(`projects/${projectId}/issues/${created.id}/`), | ||
| ) | ||
| : created; | ||
| results.push({ | ||
| index, | ||
| title, | ||
| action: "created", | ||
| result: issueMutationResult({ | ||
| action: "created", | ||
| projectKey: key, | ||
| issue: resultIssue, | ||
| }), | ||
| }); | ||
| } | ||
| yield* outputBulkResults(results); |
There was a problem hiding this comment.
Don't let one failed mutation abort the whole batch.
Any api.post / api.patch, attachment, or refresh failure here escapes the loop, so later records never run and the accumulated results never reach outputBulkResults(). For bulk commands, these need per-record failure capture and continuation.
Also applies to: 264-293
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/commands/issues-bulk.ts` around lines 174 - 202, Wrap each per-record
workflow (the sequence using api.post/api.patch, decodeOrFail,
attachCycleAndModule, and the optional api.get refresh—see the code around
api.post, created, attachCycleAndModule, resultIssue, and issueMutationResult)
in a try/catch so a single failure does not abort the loop; on error push a
results entry for that index with action "failed" and a result containing the
error message/details, then continue to the next record, and ensure
outputBulkResults(results) is still called after the loop; apply the same
try/catch pattern to the other bulk block that performs updates (the block
referenced by the reviewer where api.patch and refresh happen).
| function outputBulkResults(results: PlannedResult[]) { | ||
| return Effect.gen(function* () { | ||
| if (jsonMode) { | ||
| yield* Console.log(JSON.stringify({ results }, null, 2)); | ||
| return; | ||
| } | ||
| yield* Console.log( | ||
| results | ||
| .map((result) => { | ||
| const subject = | ||
| result.ref ?? result.title ?? `item ${result.index + 1}`; | ||
| if (result.errors?.length) | ||
| return `${result.action} ${subject}: ${result.errors.join("; ")}`; | ||
| return `${result.action} ${subject}`; | ||
| }) | ||
| .join("\n"), | ||
| ); | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
Move bulk result rendering into the shared output/format layers.
This command is defining both its JSON serialization and its human-readable text locally, which is exactly the kind of output drift the repo guidelines are trying to prevent. Please push this formatter into the shared output helpers and keep the command focused on planning/mutation.
As per coding guidelines, "Keep human-readable formatting in src/format.ts and shared machine-readable output behavior in src/output.ts" and "Preserve shared --json and --xml output behavior instead of inventing per-command machine formats".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/commands/issues-bulk.ts` around lines 542 - 559, The output logic in
outputBulkResults (handling PlannedResult, jsonMode and Console.log) must be
moved into the shared output/format layers: extract the human-readable formatter
into a new format function (e.g., formatBulkResults(results: PlannedResult[]))
in the shared formatting module and expose a shared machine-readable serializer
in the output module (reusing the repo’s standard --json behavior) so the
command only calls the shared helpers; replace the local JSON and text rendering
in outputBulkResults with calls to the shared output.serialize/results formatter
and remove any direct Console.log formatting so the command remains focused on
planning/mutation.
| const raw = yield* api.get( | ||
| `projects/${projectId}/issues/?order_by=sequence_id`, | ||
| ); | ||
| const { results } = yield* decodeOrFail(IssuesResponseSchema, raw); | ||
| const normalizedTitle = normalizeTitle(title); | ||
| const candidates = results | ||
| .map((issue) => { |
There was a problem hiding this comment.
Dedupe currently checks only the first issues page.
Line 61 fetches a single page, so projects with many issues can miss duplicates beyond page 1. This makes --dedupe unreliable in larger projects.
Also applies to: 88-93
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/issue-agent.ts` around lines 61 - 67, The current dedupe logic only
fetches a single page via
api.get(`projects/${projectId}/issues/?order_by=sequence_id`) and then decodes
IssuesResponseSchema, so duplicates beyond the first page are missed; update the
code that builds candidates (the call sites around api.get, IssuesResponseSchema
decoding, and the mapping that uses normalizeTitle and results) to fetch and
aggregate all paginated pages (follow the API's pagination cursor/next token or
increment page parameters) before running the dedupe mapping, ensuring you
collect all results across pages; apply the same change to the similar block
referenced around the later section (the code around lines 88-93) so both places
iterate through pages until no more results and then run the existing
normalizeTitle-based duplicate detection.
| function normalizeTitle(value: string): string { | ||
| return value | ||
| .toLowerCase() | ||
| .replace(/[^a-z0-9]+/g, " ") | ||
| .trim(); |
There was a problem hiding this comment.
Title normalization is ASCII-only and breaks non-Latin dedupe matching.
Line 134 removes all non [a-z0-9] characters, so titles with accents/CJK/etc. can normalize poorly and produce false negatives/positives.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/issue-agent.ts` around lines 131 - 135, normalizeTitle currently strips
anything not in [a-z0-9], which removes non‑Latin characters; update
normalizeTitle to be Unicode-aware by first applying value.normalize('NFKD'),
then remove combining marks (diacritics) via a /\p{M}/gu replace, then replace
any run of characters that are not Unicode letters or numbers using
/[^\p{L}\p{N}]+/gu with a single space, finally .toLowerCase() and .trim(); this
preserves CJK and other scripts while folding diacritics for consistent dedupe.
| if (output.trim().startsWith("{")) { | ||
| const parsed = JSON.parse(output); | ||
| expect(parsed.project.identifier).toBe("ACME"); | ||
| expect(parsed.helpers.labels.total).toBe(1); | ||
| } else { | ||
| expect(output).toContain("ACME Acme"); | ||
| expect(output).toContain("cycles=enabled"); | ||
| expect(output).toContain("Labels: 1"); | ||
| } |
There was a problem hiding this comment.
Make this assertion deterministic instead of accepting both output formats.
This branch lets the test pass even if output mode leaks across tests and unexpectedly flips to JSON.
Suggested change
- if (output.trim().startsWith("{")) {
- const parsed = JSON.parse(output);
- expect(parsed.project.identifier).toBe("ACME");
- expect(parsed.helpers.labels.total).toBe(1);
- } else {
- expect(output).toContain("ACME Acme");
- expect(output).toContain("cycles=enabled");
- expect(output).toContain("Labels: 1");
- }
+ expect(output).toContain("ACME Acme");
+ expect(output).toContain("cycles=enabled");
+ expect(output).toContain("Labels: 1");Based on learnings: "Fix root causes instead of layering on ad hoc patches when practical."
📝 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.
| if (output.trim().startsWith("{")) { | |
| const parsed = JSON.parse(output); | |
| expect(parsed.project.identifier).toBe("ACME"); | |
| expect(parsed.helpers.labels.total).toBe(1); | |
| } else { | |
| expect(output).toContain("ACME Acme"); | |
| expect(output).toContain("cycles=enabled"); | |
| expect(output).toContain("Labels: 1"); | |
| } | |
| expect(output).toContain("ACME Acme"); | |
| expect(output).toContain("cycles=enabled"); | |
| expect(output).toContain("Labels: 1"); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/project-context-command.test.ts` around lines 85 - 93, The test
currently accepts either JSON or plain-text output which makes it flaky; update
the test so it forces and asserts a single deterministic format: change the
command invocation in this test to request JSON output explicitly (e.g., add the
CLI flag --json or --format=json where the command is run that produces output),
remove the else text-branch, parse output with JSON.parse(output) and keep the
JSON assertions (expect(parsed.project.identifier).toBe("ACME");
expect(parsed.helpers.labels.total).toBe(1)); ensure the test no longer branches
on output.trim().startsWith("{") so it deterministically validates the JSON
shape.
Summary
Improves the CLI for agent-driven sessions by reducing command-shape retries, adding batch issue workflows, and making structured output more discoverable and consistent.
Changes
plane issues bulk-createandplane issues bulk-updatewith JSON file input, shared defaults, dry-run validation, and report-only duplicate detection.--from-fileand--stdinfor issue create/update descriptions, plus opt-in--jsonoutput for create/update/bulk flows.ref,title,state_name,state_group,url) and makes--json/--xmlvisible in help for supported list/get commands.plane project contextto print the local.plane/project-context.jsonsnapshot directly from the CLI.Testing
Notes
--jsonis opt-in for mutate commands; human-readable output remains the default.bun.lockincludes an existing lockfile metadata change in the worktree.Summary by CodeRabbit
Release Notes
New Features
--json,--xmlflags) for list, get, create, and update commands across projects, issues, cycles, pages, states, labels, and modulesplane project contextcommand to display local project snapshot with feature flags and statistics--dry-run,--dedupeoptions, and JSON file input--from-fileand--stdinfor HTML descriptions--labelfiltering forplane issues listwith AND-logic across labelsplane init --localChanges
plane init --localenhanced skill installation promptingChores