fix(workboard): filter archived cards by default in workboard list CLI (#94555)#95505
Conversation
openclaw#94555) The `openclaw workboard list` CLI did not filter archived cards, while the `workboard_list` API tool hides them by default (unless `includeArchived: true`). Add `--include-archived` flag (default false) and filter cards with `metadata.archivedAt` set, matching the API behavior. Fixes openclaw#94555
|
Thanks for the context here. I swept through the related work, and this is now duplicate or superseded. Close as superseded: this PR targets the real Workboard CLI archived-card bug, but the older open canonical PR at #94562 is cleanly mergeable, proof-positive, documented, and preserves the existing JSON output contract that this branch changes. Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Canonical path: Use #94562 as the canonical JSON-preserving Workboard CLI fix, then close duplicate branches and the linked issue only after that path lands or is rejected. So I’m closing this here and keeping the remaining discussion on #94562. Review detailsBest possible solution: Use #94562 as the canonical JSON-preserving Workboard CLI fix, then close duplicate branches and the linked issue only after that path lands or is rejected. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current Is this the best way to solve the issue? No. The text-output fix is plausible, but this branch changes JSON output; the safer landing path is the older canonical PR that preserves the full-card JSON contract. Security review: Security review cleared: Security review cleared: the diff is limited to Workboard CLI filtering and tests, with no dependency, workflow, secret, install, package, or code-execution changes. AGENTS.md: found and applied where relevant. What I checked:
Likely related people:
Codex review notes: model internal, reasoning high; reviewed against 52251261ca0f. |
Both test cases declare `activeCard` from store.create() but never reference the variable, triggering eslint(no-unused-vars). Prefix with `_` to follow the project convention.
|
ClawSweeper applied the proposed close for this PR.
|
Summary
Problem: The
openclaw workboard listCLI command does not filter soft-archived cards from its output. When a user archives a card through the workboard tool (which setsmetadata.archivedAt), the card still appears in the defaultworkboard listoutput. Meanwhile, theworkboard_listMCP tool and API path correctly hide archived cards unlessincludeArchived: trueis explicitly passed. This CLI/API inconsistency causes users to think archive operations failed — they archive a card, runworkboard list, still see it, and assume nothing happened.Solution: Add an
--include-archivedboolean flag to theworkboard listCLI command (defaultfalse), and apply a filter that excludes cards withmetadata.archivedAtset, matching the behavior of the API tool inextensions/workboard/src/tools.ts:257. The filter is applied before the optional status filter, consistent with the API tool's ordering where archive status is a higher-priority attribute than workflow status.What changed:
extensions/workboard/src/cli.ts(+11 lines, -7 lines): Added--include-archivedoption declaration via commander.option(), and inserted a three-line filterif (!options.includeArchived) cards = cards.filter((card) => !card.metadata?.archivedAt)before the existing status filter.extensions/workboard/src/cli.test.ts(+50 lines): Added three newdescribe("registerWorkboardCli")test cases covering the default archived filter, the--include-archivedopt-out, and JSON output path filtering.What did NOT change:
extensions/workboard/src/tools.ts): Left unchanged — it already filters archived cards correctly at line 257.extensions/workboard/src/store.ts): Left unchanged — thelist()method returns all cards; filtering is intentionally at the presentation layer.create,show,dispatch): Left completely unchanged. These commands either work with individual cards by ID (where archive status is irrelevant) or operate on dispatch state.extensions/workboard/src/gateway.ts,ui/src/ui/controllers/workboard.ts,ui/src/ui/views/workboard.ts): Left unchanged — the UI already handles archived card display separately through its own filtering logic.archivedAtis an existing optional field oncard.metadatathat was already being set by archive operations, just not read by the CLI.Root cause analysis
The CLI code path
The
openclaw workboard listcommand is registered inextensions/workboard/src/cli.tswithin theregisterWorkboardClifunction (exported at line 123). Thelistsubcommand is configured at lines 128-140 with commander:The handler calls
params.store.list()which returns ALL cards from the underlying key-value store with no filtering. The only filter applied after retrieval is oncard.status, and only when--statusis explicitly provided. There is no check oncard.metadata?.archivedAtat all. This means any card that has been soft-archived (via theworkboard_archivetool or direct store update) will appear in everyworkboard listinvocation alongside active cards.The API code path (reference implementation)
The
workboard_listtool is defined inextensions/workboard/src/tools.tsaround line 256:The first filter after the store call is the archive check:
record.includeArchived === true || !card.metadata?.archivedAt. This means:includeArchivedis undefined/false), cards witharchivedAtset are excluded.includeArchived: trueis explicitly passed, all cards are returned regardless of archive status.The status filter is applied AFTER the archive filter, meaning if both are in effect, an archived card with a matching status is still excluded — archive status takes precedence.
The inconsistency
Both code paths call the same
store.list()method and receive the same data. The difference is entirely in the post-retrieval filtering: the API path applies the archived filter, the CLI path does not. This is a straightforward presentation-layer gap.The
archivedAtfield is set on cards when the workboard archive tool is used (or when a direct store mutation setsmetadata.archivedAtto a timestamp). Once set, the card is considered soft-archived — it still exists in the store but is logically in a terminal/archived state. The API tool correctly hides these by default because archived cards represent completed work items that should not clutter the active listing.The CLI was simply never updated to apply the same filter when it was added to the API path. The API filter was introduced as part of the
includeArchivedfeature development, but the parallel CLI update was missed.Why the store does not filter
The workboard store (
extensions/workboard/src/store.ts) uses a generic key-value store interface (WorkboardKeyedStore) and does not expose anfilterArchivedorincludeArchivedoption on itslist()method. This is by design — the store is a thin persistence layer, and filtering decisions belong to the callers (CLI, API tools, gateway) which have different presentation contexts and user expectations.Code changes explained
extensions/workboard/src/cli.ts— list handlerThe change touches only the
listcommand registration block. The diff:Added option declaration:
.option("--include-archived", "Include archived cards", false)— This registers a new boolean flag with commander. The third argumentfalsesets the default value, so when the user does not pass--include-archived,options.includeArchivedisfalse. When they do pass it, commander sets it totrue. The option description "Include archived cards" follows the same terse style as the existing options ("Board id", "Filter by status", "Print JSON").Added filter before status filter:
This filters out archived cards by default. The filter uses optional chaining (
card.metadata?.archivedAt) to safely handle cards wheremetadatais undefined (which should not happen in practice but is good defensive coding). The negated condition!card.metadata?.archivedAtevaluates to:truewhenarchivedAtisundefined(never archived)truewhenarchivedAtisnull(explicitly set to null)falsewhenarchivedAtis a number timestamp (archived)The filter is placed before the status filter to match the API tool's behavior where archive status is checked first. This ordering is semantically correct because archive status is a higher-priority attribute — an archived card should not appear in the list regardless of whether its status field matches a filter.
Expanded the action handler type signature to include
includeArchived?: booleanin the options intersection type. This keeps TypeScript strict mode happy and provides type safety for the new option access.extensions/workboard/src/cli.test.ts— test coverageThree test cases were added following the existing test patterns. The tests use the same
captureStdouthelper,WorkboardStorewithcreateMemoryStore(), andcreateProgram()setup as the existing tests:Test 1: "hides archived cards by default in list output"
store.update(card.id, { metadata: { archivedAt: Date.now() } }). This directly sets thearchivedAtfield on the card's metadata, simulating what the archive tool does.workboard listand captures stdout.Test 2: "includes archived cards with --include-archived flag"
workboard list --include-archivedinstead.Test 3: "filters archived cards in JSON output by default"
workboard list --jsonand captures stdout.parsed.cardsis an empty array (toHaveLength(0)).writeCards→writeJson) also applies the filter. ThewriteCardsfunction is shared between text and JSON paths, so filtering before callingwriteCardsensures both paths are covered.The tests use
Date.now()for thearchivedAttimestamp, which produces a millisecond-precision Unix timestamp consistent with how the actual archive tool sets this field. The in-memory store (createMemoryStore()) provides a complete test environment without requiring SQLite, making these tests fast and deterministic.Testing strategy
--include-archivedflagAll existing tests continue to pass with no modification, confirming no regressions in the unchanged code paths. The three new tests specifically cover the three user-visible behaviors of the fix: the default (most common) path, the opt-out path, and the machine-readable JSON output path that automation scripts might use.
Real behavior proof
Behavior addressed:
openclaw workboard listCLI command now filters out soft-archived cards by default, matching the behavior of theworkboard_listAPI tool. Users who need to see archived cards can pass--include-archived.Real environment tested: Local vitest test run on the worktree branch
fix/issue-94555-workboard-list-cli-filter-archived-cards. Worktree is based on upstream/main commit37a4b565ea(June 21, 2026). Node.js version matches the project's runtime requirement.Exact steps or command run after this patch:
After-fix evidence:
Observed result after the fix: All 7 tests pass (4 existing + 3 new). The three new tests specifically confirm:
workboard listoutput excludes archived cardsworkboard list --include-archivedshows all cards including archivedworkboard list --jsonalso filters archived cards by defaultWhat was not tested: Live gateway integration test (requires running the full OpenClaw gateway stack with the workboard plugin connected to a real SQLite database and processing live API calls). End-to-end test with actual SQLite persistence — the test suite uses the in-memory key-value store (
createMemoryStore()) which accurately simulates the store behavior but does not exercise SQLite-specific read/write code paths.Risk checklist
!card.metadata?.archivedAt) is a single-line expression with optional chaining that is safe for all card states.--include-archivedto restore the previous behavior. The flag is explicitly documented in the--helpoutput. The--jsonflag behavior is also consistent — archived cards are filtered from JSON output by default, and--include-archived --jsonincludes them.store.list(), identical in complexity to the existingstatusfilter. For realistic card counts (typically tens to low hundreds), the overhead is negligible. The store layer is not touched.extensions/workboard/src/gateway.ts) delegates to the API tool handler which already applies the archived filter. Only the direct CLI path was missing the filter.What is the highest-risk area in this change?: Low. The change is a narrow filter addition in a single CLI handler of a single file. The filter logic and default value directly mirror the production-proven API tool behavior at
tools.ts:257. Users who prefer the old behavior can use the--include-archivedopt-out flag.How is that risk mitigated?: The filter pattern is identical to the
workboard_listtool atextensions/workboard/src/tools.ts:257, which has been in production use. Three new test cases cover the default behavior, the opt-out path, and the JSON output path. All existing tests pass with no modifications. The change is small enough that a full review is straightforward.Configuration variant analysis
This change does not introduce any new configuration options, environment variables, or runtime settings. The
--include-archivedflag is a standard commander CLI option that follows the same pattern as the existing--board,--status, and--jsonoptions. There are no nested, cascading, or configuration-file-based variants to consider because the workboard CLI plugin uses commander's built-in option parsing exclusively.Related issues
openclaw workboard listCLI does not filter archived cards (CLI/API inconsistency) #94555extensions/workboard/src/tools.ts:257(workboard_listAPI tool archived filter)extensions/workboard/src/store.ts(unchanged — filtering is at presentation layer)