Problem / Goal
slashdo v3.25.0 (the pinned submodule at lib/slashdo, commit 1f70b7e) added a
per-reviewer iteration cap: any --review-with slot may be suffixed ~max=<n> —
--review-with claude~max=2,ollama~max=1,codex~max=3 — budgeting how many
review → fix → re-review cycles that one reviewer runs. Before this, the local
agents (codex/claude/agy/grok) and ollama had hardcoded 3-iteration caps
with no way to move them, which makes a slow local model expensive to include in a
CoS Review Loop.
PortOS emits --review-with for every CoS agent task but has no way to express a
cap, so every local reviewer still burns up to 3 full cycles. The goal: expose a
per-reviewer max-rounds control in the task-add form (and the surfaces that share
the same picker) and thread it into the emitted flag.
Context
The ~opt (non-blocking) suffix — shipped in the same slashdo grammar slot — is the
exact precedent to mirror; ~max=<n> is a sibling suffix that chains with it in
either order (ollama~opt~max=1 ≡ ollama~max=1~opt), and slashdo's canonical
storage order is ~opt first, then ~max=<n>.
Existing ~opt plumbing, end to end:
server/lib/cosValidation.js:117 normalizeOptionalReviewers — normalizes the
token list (keyed slug or @user), aliases gemini→antigravity, dedupes.
server/lib/cosValidation.js:149 resolveOptionalReviewers — task-over-default
precedence (an explicitly empty array overrides the Code Review Defaults).
server/lib/cosValidation.js:163 markOptional — appends the suffix to a token.
server/lib/cosValidation.js:221 buildReviewersCsv — prompt {reviewers} CSV.
server/lib/cosValidation.js:244 buildReviewWithArgs — the flag string.
server/lib/cosValidation.js codeReviewSettingsSchema (optionalReviewers
field) and the task-metadata sanitizer (optionalReviewers branch, which KEEPS an
explicitly empty array).
- Consumers:
server/services/agentPromptBuilder.js (:400, :1053, :1352, :1475,
:1580, :1750), server/services/agentWorktreeCleanup.js (:46, :232, :298, :320),
server/services/cosTaskGenerator.js (:332, :369, :2202), server/services/codeReview.js
(:70, :132, :145), server/routes/cosTaskRoutes.js:130, server/services/prWatcher.js:332.
- Client:
client/src/components/cos/ReviewerPicker.jsx:82 (renderOptToggle, the
per-chip badge), client/src/components/cos/TaskAddForm.jsx (:26, :307, :556),
client/src/components/cos/tabs/schedule/GlobalConfigControls.jsx,
client/src/components/providers/CodeReviewDefaultsPanel.jsx,
client/src/hooks/useCodeReviewDefaults.jsx, client/src/components/apps/SlashDoRunDrawer.jsx.
The keyed-map storage precedent already in the codebase is
metadata.reviewLoopReviewerModels (server/services/agentPromptBuilder.js:413) —
a reviewer-keyed object threaded into the review-loop prompt.
PortOS does not use slashdo's --review-iterations flag anywhere (grep is empty), so
~max=<n> is the only cap surface that needs building.
Proposed approach
Mirror the ~opt plumbing with a per-reviewer numeric cap, stored as a
token-keyed map rather than a list.
- Schema/storage — add
reviewerMaxRounds: Record<token, number> where token
is an emitted --review-with token (a REVIEWER_VALUES slug or @user,
matching normalizeOptionalReviewers' identity rule). Add a
normalizeReviewerMaxRounds(map) + resolveReviewerMaxRounds(taskMap, defaultMap)
pair in server/lib/cosValidation.js next to their optionalReviewers siblings:
drop unknown tokens, coerce to a non-negative integer, drop non-integers /
negatives, and drop entries with no cap so absent ≠ 0 (slashdo reads 0 as
"loop until clean", so a dropped key and 0 must not collapse).
- Emission — extend
markOptional into a markSuffixes(token, optSet, maxMap)
that appends ~opt then ~max=<n> in slashdo's canonical order, and use it from
both buildReviewersCsv and buildReviewWithArgs.
- Settings — add the field to
codeReviewSettingsSchema (.strict(), so it
must be declared) and to the task-metadata sanitizer, keeping the same
"explicitly empty overrides the default" semantics optionalReviewers has.
- Threading — pass
reviewerMaxRounds through every call site that already
carries optionalReviewers (list above), including the review-loop follow-up
spawner in agentWorktreeCleanup.js and codeReview.js's resolver.
- UI — in
ReviewerPicker.jsx, render a compact numeric cap control on each
reviewer/username chip beside the ~opt badge (blank = slashdo's built-in
default; 0 = loop until clean, labeled as such). Since the picker is shared, the
control lands in TaskAddForm, GlobalConfigControls, CodeReviewDefaultsPanel,
and SlashDoRunDrawer at once. Clean up the map entry when a reviewer/username is
removed, exactly as withoutToken does for optionalReviewers.
- Tests — extend
server/lib/validation.test.js, server/routes/cos.test.js,
server/services/agentPromptBuilder.test.js, server/services/cosTaskGenerator.test.js,
server/services/codeReview.test.js, and client/src/components/cos/ReviewerPicker.test.jsx
alongside their existing optionalReviewers cases.
Acceptance criteria
Out of scope
- Surfacing slashdo's
--review-iterations flag (PortOS uses none of it today; the
per-entry suffix supersedes it for every reviewer type).
- Rendering slashdo's new
capped status or its Iterations column back into the
PortOS Review Loop UI — a separate follow-up if wanted.
- Fixing the pre-existing quirk that PortOS emits
lmstudio (and antigravity
rather than agy) into --review-with, which are outside slashdo's accepted slug
set. A ~max on those tokens is inert for the same pre-existing reason.
--review-mode parallel, where slashdo ignores ~max entirely.
Problem / Goal
slashdo v3.25.0 (the pinned submodule at
lib/slashdo, commit1f70b7e) added aper-reviewer iteration cap: any
--review-withslot may be suffixed~max=<n>—--review-with claude~max=2,ollama~max=1,codex~max=3— budgeting how manyreview → fix → re-review cycles that one reviewer runs. Before this, the local
agents (
codex/claude/agy/grok) andollamahad hardcoded 3-iteration capswith no way to move them, which makes a slow local model expensive to include in a
CoS Review Loop.
PortOS emits
--review-withfor every CoS agent task but has no way to express acap, so every local reviewer still burns up to 3 full cycles. The goal: expose a
per-reviewer max-rounds control in the task-add form (and the surfaces that share
the same picker) and thread it into the emitted flag.
Context
The
~opt(non-blocking) suffix — shipped in the same slashdo grammar slot — is theexact precedent to mirror;
~max=<n>is a sibling suffix that chains with it ineither order (
ollama~opt~max=1≡ollama~max=1~opt), and slashdo's canonicalstorage order is
~optfirst, then~max=<n>.Existing
~optplumbing, end to end:server/lib/cosValidation.js:117normalizeOptionalReviewers— normalizes thetoken list (keyed slug or
@user), aliasesgemini→antigravity, dedupes.server/lib/cosValidation.js:149resolveOptionalReviewers— task-over-defaultprecedence (an explicitly empty array overrides the Code Review Defaults).
server/lib/cosValidation.js:163markOptional— appends the suffix to a token.server/lib/cosValidation.js:221buildReviewersCsv— prompt{reviewers}CSV.server/lib/cosValidation.js:244buildReviewWithArgs— the flag string.server/lib/cosValidation.jscodeReviewSettingsSchema(optionalReviewersfield) and the task-metadata sanitizer (
optionalReviewersbranch, which KEEPS anexplicitly empty array).
server/services/agentPromptBuilder.js(:400, :1053, :1352, :1475,:1580, :1750),
server/services/agentWorktreeCleanup.js(:46, :232, :298, :320),server/services/cosTaskGenerator.js(:332, :369, :2202),server/services/codeReview.js(:70, :132, :145),
server/routes/cosTaskRoutes.js:130,server/services/prWatcher.js:332.client/src/components/cos/ReviewerPicker.jsx:82(renderOptToggle, theper-chip badge),
client/src/components/cos/TaskAddForm.jsx(:26, :307, :556),client/src/components/cos/tabs/schedule/GlobalConfigControls.jsx,client/src/components/providers/CodeReviewDefaultsPanel.jsx,client/src/hooks/useCodeReviewDefaults.jsx,client/src/components/apps/SlashDoRunDrawer.jsx.The keyed-map storage precedent already in the codebase is
metadata.reviewLoopReviewerModels(server/services/agentPromptBuilder.js:413) —a reviewer-keyed object threaded into the review-loop prompt.
PortOS does not use slashdo's
--review-iterationsflag anywhere (grep is empty), so~max=<n>is the only cap surface that needs building.Proposed approach
Mirror the
~optplumbing with a per-reviewer numeric cap, stored as atoken-keyed map rather than a list.
reviewerMaxRounds: Record<token, number>wheretokenis an emitted
--review-withtoken (aREVIEWER_VALUESslug or@user,matching
normalizeOptionalReviewers' identity rule). Add anormalizeReviewerMaxRounds(map)+resolveReviewerMaxRounds(taskMap, defaultMap)pair in
server/lib/cosValidation.jsnext to theiroptionalReviewerssiblings:drop unknown tokens, coerce to a non-negative integer, drop non-integers /
negatives, and drop entries with no cap so absent ≠
0(slashdo reads0as"loop until clean", so a dropped key and
0must not collapse).markOptionalinto amarkSuffixes(token, optSet, maxMap)that appends
~optthen~max=<n>in slashdo's canonical order, and use it fromboth
buildReviewersCsvandbuildReviewWithArgs.codeReviewSettingsSchema(.strict(), so itmust be declared) and to the task-metadata sanitizer, keeping the same
"explicitly empty overrides the default" semantics
optionalReviewershas.reviewerMaxRoundsthrough every call site that alreadycarries
optionalReviewers(list above), including the review-loop follow-upspawner in
agentWorktreeCleanup.jsandcodeReview.js's resolver.ReviewerPicker.jsx, render a compact numeric cap control on eachreviewer/username chip beside the
~optbadge (blank = slashdo's built-indefault;
0= loop until clean, labeled as such). Since the picker is shared, thecontrol lands in
TaskAddForm,GlobalConfigControls,CodeReviewDefaultsPanel,and
SlashDoRunDrawerat once. Clean up the map entry when a reviewer/username isremoved, exactly as
withoutTokendoes foroptionalReviewers.server/lib/validation.test.js,server/routes/cos.test.js,server/services/agentPromptBuilder.test.js,server/services/cosTaskGenerator.test.js,server/services/codeReview.test.js, andclient/src/components/cos/ReviewerPicker.test.jsxalongside their existing
optionalReviewerscases.Acceptance criteria
reviewer and per
@usernamereviewer, persisted in task metadata.buildReviewWithArgsemits<token>~max=<n>, and<token>~opt~max=<n>whenboth are set (
~optfirst), for exactly the tokens that carry a cap.buildReviewersCsvemits the same suffixes so the{reviewers}promptplaceholder matches the flag string.
0round-trips as~max=0and is distinguishable from "no cap set"(which emits no suffix).
rather than reaching the flag string.
explicitly empty one) overrides them — same precedence as
optionalReviewers.the picker control;
cd server && npm testandcd client && npm testpass.Out of scope
--review-iterationsflag (PortOS uses none of it today; theper-entry suffix supersedes it for every reviewer type).
cappedstatus or its Iterations column back into thePortOS Review Loop UI — a separate follow-up if wanted.
lmstudio(andantigravityrather than
agy) into--review-with, which are outside slashdo's accepted slugset. A
~maxon those tokens is inert for the same pre-existing reason.--review-mode parallel, where slashdo ignores~maxentirely.