Skip to content

fix(workboard): filter archived cards by default in workboard list CLI (#94555)#95505

Closed
Pandah97 wants to merge 3 commits into
openclaw:mainfrom
Pandah97:fix/issue-94555-workboard-list-cli-filter-archived-cards
Closed

fix(workboard): filter archived cards by default in workboard list CLI (#94555)#95505
Pandah97 wants to merge 3 commits into
openclaw:mainfrom
Pandah97:fix/issue-94555-workboard-list-cli-filter-archived-cards

Conversation

@Pandah97

Copy link
Copy Markdown
Contributor

Summary

Problem: The openclaw workboard list CLI command does not filter soft-archived cards from its output. When a user archives a card through the workboard tool (which sets metadata.archivedAt), the card still appears in the default workboard list output. Meanwhile, the workboard_list MCP tool and API path correctly hide archived cards unless includeArchived: true is explicitly passed. This CLI/API inconsistency causes users to think archive operations failed — they archive a card, run workboard list, still see it, and assume nothing happened.

Solution: Add an --include-archived boolean flag to the workboard list CLI command (default false), and apply a filter that excludes cards with metadata.archivedAt set, matching the behavior of the API tool in extensions/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-archived option declaration via commander .option(), and inserted a three-line filter if (!options.includeArchived) cards = cards.filter((card) => !card.metadata?.archivedAt) before the existing status filter.
  • extensions/workboard/src/cli.test.ts (+50 lines): Added three new describe("registerWorkboardCli") test cases covering the default archived filter, the --include-archived opt-out, and JSON output path filtering.

What did NOT change:

  • API/tool path (extensions/workboard/src/tools.ts): Left unchanged — it already filters archived cards correctly at line 257.
  • Store layer (extensions/workboard/src/store.ts): Left unchanged — the list() method returns all cards; filtering is intentionally at the presentation layer.
  • Other CLI subcommands (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.
  • UI components (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.
  • Card type definitions and store schema: Left unchanged — archivedAt is an existing optional field on card.metadata that was already being set by archive operations, just not read by the CLI.

Root cause analysis

The CLI code path

The openclaw workboard list command is registered in extensions/workboard/src/cli.ts within the registerWorkboardCli function (exported at line 123). The list subcommand is configured at lines 128-140 with commander:

workboard
  .command("list")
  .description("List Workboard cards")
  .option("--board <id>", "Board id")
  .option("--status <status>", "Filter by status")
  .option("--json", "Print JSON", false)
  .action(async (options) => {
    let cards = await params.store.list({ boardId: options.board });
    if (options.status) {
      cards = cards.filter((card) => card.status === options.status);
    }
    writeCards(cards, options);
  });

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 on card.status, and only when --status is explicitly provided. There is no check on card.metadata?.archivedAt at all. This means any card that has been soft-archived (via the workboard_archive tool or direct store update) will appear in every workboard list invocation alongside active cards.

The API code path (reference implementation)

The workboard_list tool is defined in extensions/workboard/src/tools.ts around line 256:

const cards = (await store.list({ boardId }))
  .filter((card) => record.includeArchived === true || !card.metadata?.archivedAt)
  .filter((card) => !status || card.status === status)
  .filter((card) => !agentId || card.agentId === agentId)
  .filter((card) => !tenant || card.metadata?.automation?.tenant === tenant)
  .slice(0, limit)
  .map(summarizeCard);

The first filter after the store call is the archive check: record.includeArchived === true || !card.metadata?.archivedAt. This means:

  • By default (includeArchived is undefined/false), cards with archivedAt set are excluded.
  • When includeArchived: true is 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 archivedAt field is set on cards when the workboard archive tool is used (or when a direct store mutation sets metadata.archivedAt to 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 includeArchived feature 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 an filterArchived or includeArchived option on its list() 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 handler

The change touches only the list command registration block. The diff:

  1. Added option declaration: .option("--include-archived", "Include archived cards", false) — This registers a new boolean flag with commander. The third argument false sets the default value, so when the user does not pass --include-archived, options.includeArchived is false. When they do pass it, commander sets it to true. The option description "Include archived cards" follows the same terse style as the existing options ("Board id", "Filter by status", "Print JSON").

  2. Added filter before status filter:

    if (!options.includeArchived) {
      cards = cards.filter((card) => !card.metadata?.archivedAt);
    }

    This filters out archived cards by default. The filter uses optional chaining (card.metadata?.archivedAt) to safely handle cards where metadata is undefined (which should not happen in practice but is good defensive coding). The negated condition !card.metadata?.archivedAt evaluates to:

    • true when archivedAt is undefined (never archived)
    • true when archivedAt is null (explicitly set to null)
    • false when archivedAt is 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.

  3. Expanded the action handler type signature to include includeArchived?: boolean in 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 coverage

Three test cases were added following the existing test patterns. The tests use the same captureStdout helper, WorkboardStore with createMemoryStore(), and createProgram() setup as the existing tests:

Test 1: "hides archived cards by default in list output"

  • Creates two cards: one active (status "todo") and one that will be archived.
  • Archives the second card by calling store.update(card.id, { metadata: { archivedAt: Date.now() } }). This directly sets the archivedAt field on the card's metadata, simulating what the archive tool does.
  • Runs workboard list and captures stdout.
  • Asserts that the active card title appears in the output.
  • Asserts that the archived card title does NOT appear in the output.
  • This test validates the core fix: default behavior hides archived cards.

Test 2: "includes archived cards with --include-archived flag"

  • Same setup as test 1 (one active, one archived).
  • Runs workboard list --include-archived instead.
  • Asserts that BOTH card titles appear in the output.
  • This test validates the opt-out mechanism works correctly.

Test 3: "filters archived cards in JSON output by default"

  • Creates a single card and archives it.
  • Runs workboard list --json and captures stdout.
  • Parses the JSON output and asserts parsed.cards is an empty array (toHaveLength(0)).
  • This test validates that the JSON formatting path (which uses writeCardswriteJson) also applies the filter. The writeCards function is shared between text and JSON paths, so filtering before calling writeCards ensures both paths are covered.

The tests use Date.now() for the archivedAt timestamp, 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

Test Type What it covers
Default archived filter Unit (integration with store) Core fix: archived cards excluded from plain text list
--include-archived flag Unit (integration with store) Opt-out path: all cards shown with flag
JSON output filter Unit (integration with store) JSON formatting path also applies filter
Redacts claim tokens Existing (unchanged) Regression: claim token redaction still works
Gateway dispatch fallback Existing (unchanged) Regression: dispatch logic unchanged
Ambiguous prefix rejection Existing (unchanged) Regression: card lookup unchanged

All 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 list CLI command now filters out soft-archived cards by default, matching the behavior of the workboard_list API 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 commit 37a4b565ea (June 21, 2026). Node.js version matches the project's runtime requirement.

Exact steps or command run after this patch:

node scripts/run-vitest.mjs extensions/workboard/src/cli.test.ts

After-fix evidence:

$ node scripts/run-vitest.mjs extensions/workboard/src/cli.test.ts
[test] starting test/vitest/vitest.extensions.config.ts

 RUN  v4.1.8 /home/.../fix/issue-94555

 ✓ registerWorkboardCli > hides archived cards by default in list output
 ✓ registerWorkboardCli > includes archived cards with --include-archived flag
 ✓ registerWorkboardCli > filters archived cards in JSON output by default
 ✓ registerWorkboardCli > redacts claim tokens from card JSON output
 ✓ registerWorkboardCli > does not fall back to local dispatch for explicit gateway targets
 ✓ registerWorkboardCli > does not fall back to local dispatch for configured remote gateways
 ✓ registerWorkboardCli > rejects ambiguous card id prefixes

 Test Files  1 passed (1)
      Tests  7 passed (7)
   Start at  15:04:26
   Duration  15.84s (transform 12.28s, setup 2.04s, import 13.26s, tests 72ms)

Observed result after the fix: All 7 tests pass (4 existing + 3 new). The three new tests specifically confirm:

  • Default workboard list output excludes archived cards
  • workboard list --include-archived shows all cards including archived
  • workboard list --json also filters archived cards by default

What 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

Risk Level Mitigation
Regression on existing list behavior Low The change is purely additive — it introduces a new filter that is applied before the existing status filter. All 4 existing tests continue to pass unchanged. The filter logic (!card.metadata?.archivedAt) is a single-line expression with optional chaining that is safe for all card states.
Breaking scripts or aliases that parse CLI output Low Users and scripts that depend on seeing archived cards in the default output can pass --include-archived to restore the previous behavior. The flag is explicitly documented in the --help output. The --json flag behavior is also consistent — archived cards are filtered from JSON output by default, and --include-archived --json includes them.
Performance impact on list command None The filter is a single O(n) pass over the in-memory array returned by store.list(), identical in complexity to the existing status filter. For realistic card counts (typically tens to low hundreds), the overhead is negligible. The store layer is not touched.
Missed configuration variant (gateway RPC list) Low The gateway RPC path (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-archived opt-out flag.

How is that risk mitigated?: The filter pattern is identical to the workboard_list tool at extensions/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-archived flag is a standard commander CLI option that follows the same pattern as the existing --board, --status, and --json options. 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#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
@clawsweeper

clawsweeper Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

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
Relationship: superseded
Canonical: #94562
Summary: This PR and the canonical PR target the same Workboard CLI archived-card visibility bug; the canonical PR is older, cleanly mergeable, proof-positive, documented, and JSON-preserving.

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 details

Best 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 workboard list filters only by status, while workboard_list and /workboard list already hide archived cards by default.

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:

  • Current CLI mismatch: Current main workboard list reads all cards, optionally filters by status, and writes the result without checking metadata.archivedAt, so the linked bug remains real on main. (extensions/workboard/src/cli.ts:135, 52251261ca0f)
  • JSON output contract: Current docs describe --json as printing the full card list as machine JSON; this is the compatibility contract the canonical PR preserves and this branch changes. Public docs: docs/cli/workboard.md. (docs/cli/workboard.md:57, 52251261ca0f)
  • Sibling list behavior: The Workboard tool already exposes includeArchived and hides archived cards unless that option is true, while the slash command also hides archived cards by default. (extensions/workboard/src/tools.ts:237, 52251261ca0f)
  • Current PR changes JSON too: This PR filters cards before writeCards, so the filter affects both compact text and --json; its added test asserts archived cards disappear from JSON by default. (extensions/workboard/src/cli.ts:144, b6961da6aa5e)
  • Canonical PR viability: The older candidate PR is open, non-draft, maintainer-editable, REST reports mergeable: true and mergeable_state: clean, and labels include proof: sufficient and ready-for-maintainer-review. (66ba5f43be9d)
  • Canonical PR preserves JSON: The canonical PR filters archived cards only when output is not JSON, documents --include-archived, and adds a test that archived cards remain present in JSON output by default. (extensions/workboard/src/cli.ts:146, 66ba5f43be9d)

Likely related people:

  • steipete: The Workboard CLI and Workboard tool surfaces involved in this mismatch appear to date to commits authored by Peter Steinberger. (role: feature-history owner; confidence: high; commits: f8b566b9c1df, 61031d1b1cec; files: extensions/workboard/src/cli.ts, extensions/workboard/src/tools.ts, extensions/workboard/src/command.ts)
  • vincentkoc: Current-main blame attributes the relevant Workboard CLI, tool, slash command, and docs snapshot to a recent grafted commit by Vincent Koc. (role: recent area contributor; confidence: medium; commits: 00f8b10567ff; files: extensions/workboard/src/cli.ts, extensions/workboard/src/tools.ts, extensions/workboard/src/command.ts)
  • ZengWen-DT: This account authored the older proof-sufficient PR that now appears to be the safer canonical landing path for the same issue. (role: canonical candidate author; confidence: medium; commits: 66ba5f43be9d; files: extensions/workboard/src/cli.ts, extensions/workboard/src/cli.test.ts, docs/cli/workboard.md)

Codex review notes: model internal, reasoning high; reviewed against 52251261ca0f.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. labels Jun 21, 2026
Pandah97 added 2 commits June 22, 2026 22:36
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

clawsweeper Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

@clawsweeper clawsweeper Bot closed this Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P2 Normal backlog priority with limited blast radius. plugin: workboard rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: S status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: openclaw workboard list CLI does not filter archived cards (CLI/API inconsistency)

1 participant