feat(cli): bundle agentic-CLI improvements (swamp-club#235)#1305
feat(cli): bundle agentic-CLI improvements (swamp-club#235)#1305
Conversation
…:json, --timeout, --repo-dir parity (swamp-club#235) Bundle 6 agentic-CLI ergonomics fixes (#235): 1. JSON-mode logger isolation — set parentSinks: 'override' on ["model","method","run"], ["workflow","run"], and ["logtape","meta"] category loggers in jsonMode so they cannot inherit the root sink. Clear logtape.meta's own sinks too. Closes the leak path identified in #235. 2. Single-emitter renderError — in jsonMode, write JSON to stdout exactly once and skip logger.fatal. Delete createJsonErrorSink (audit confirmed only 2 logger.fatal calls in src/, both inside renderError). Extend buildErrorJson with optional `code` field; UserError gains a code parameter. Updated existing integration tests to assert against the combined stdout+stderr stream rather than stderr alone. 3. --input :json suffix — keys ending :json (on the leaf segment of any dot-notation path) parse the value as JSON. Bypasses the @file shorthand. Hard error on parse failure. Test matrix covers flat array/object, nested keys, escape interactions, no-suffix fallback, parse failures, and precedence with --input-file. 4. --repo-dir on `model type search` and top-level `doctor` — closes the "Unknown option --repo-dir" failure mode for agentic flows that pass --repo-dir uniformly. 5. data gc --json — already non-interactive (gate at outputMode === 'log'); add an explicit JSON example to help text so agents can discover the bypass without reading source. 6. --timeout flag for `model method run` and `workflow run` — wires AbortController via libswamp's existing LibSwampContext.withTimeout(ms). Reuses libswamp's parseDuration; adds parseTimeout helper for second- level granularity. Surfaces SwampError code "cancelled" via the new buildErrorJson code field. Documented as cooperative cancellation — built-in models that don't honor AbortSignal are tracked separately (swamp-club#247). Design docs: - design/rendering.md gains a normative "JSON Mode Output Contract" section with four guarantees: stdout = single JSON document; stderr free for logs; errors emit { error, stack?, code? } on stdout with non-zero exit; no interactive prompts in JSON mode. - design/inputs.md gains a :json suffix section with examples. - swamp-model and swamp-workflow skills updated with :json examples. Regression test: - integration/json_isolation_test.ts asserts BOTH directions across version, data list, and model method run: --json stdout-only-JSON; log mode still emits. Out of scope, tracked separately: - swamp-club#246 — Reader-lock or lock-free read path for data list/get/search/query (original #235 item 5). - swamp-club#247 — Built-in models must honor AbortSignal so --timeout works in practice. - swamp-uat#185 — UAT (adversarial): --timeout cancellation must release model locks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
--timeoutdescription is too long (src/cli/commands/model_method_run.ts:137,src/cli/commands/workflow_run.ts:128). At ~220 characters, it'll produce multi-line wrapped text that breaks the visual alignment of the options table in--help. Compare to--driver: "Override execution driver (e.g. raw, docker)". Suggested trim:"Cancellation deadline (e.g. 30s, 5m, 1h). Cooperative — only honored by methods that check AbortSignal."The "long-running model methods that ignore the signal" caveat is already documented in
design/inputs.mdand the PR description. -
--repo-dirdescription ontype_searchis inconsistent (src/cli/commands/type_search.ts:84). Every other command that accepts--repo-diruses"Repository directory (env: SWAMP_REPO_DIR)". This one says"Repository directory (informational; type search does not require an initialized repo)"— dropping the env var hint and adding a parenthetical that reads as if the flag is ignored. Users scripting viaSWAMP_REPO_DIRwon't see the familiar pattern, and the word "informational" implies the flag does nothing. Consider:"Repository directory (env: SWAMP_REPO_DIR; not required for type search)". -
:jsonsuffix is not mentioned in--inputhelp text (model_method_run.ts,workflow_run.ts). The--inputdescription still says"Input values (key=value or JSON)"— the new:jsonsuffix is entirely invisible from--help. The PR added a discoverability example fordata gc --json(item 5); the same treatment would help item 3. Consider adding one example per command, e.g.:.example("Pass an array input", "swamp model method run my-model search --input 'keywords:json=[\"a\",\"b\"]'") -
Minor wording inconsistency in
--timeoutdescription between the two commands:model_method_run.tssays "...long-running model methods that ignore the signal will not be interrupted" whileworkflow_run.tssays "...steps whose model methods honor AbortSignal." Same concept, different length. Worth aligning once the description is trimmed per suggestion 1.
Verdict
PASS — no blocking issues. All five ergonomic improvements work correctly: JSON-mode logger isolation, single-emitter renderError, :json suffix, --repo-dir parity, data gc --json discoverability, and --timeout. Integration test coverage is thorough. The suggestions above are polish items that don't block merge.
CLI UX review (PR #1305 review feedback): - Trim --timeout description on model method run + workflow run from ~220 chars to ~100; align wording across both commands. - Restore familiar --repo-dir wording on type_search ("env: SWAMP_REPO_DIR; not required for type search") so users scripting via SWAMP_REPO_DIR see the standard pattern. - Add a --input :json discoverability example to model method run + workflow run help text (parallel treatment to the data gc --json example). Skill review: - swamp-data-query content score was 0.7999... (just below the 0.90 strict threshold from scripts/review_skills.ts) due to LLM-judge rounding. Address the two judge suggestions: add an "Empty results and errors" section covering empty matches, malformed CEL, missing-field semantics, and binary content; clarify the references/fields.md path. Score now 94%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
CLI UX Review
Blocking
-
--timeoutformat inconsistency —src/cli/commands/model_method_run.ts:140andworkflow_run.ts:127vsdatastore_sync.ts:102datastore syncalready ships--timeout <seconds:integer>where users pass a plain integer:swamp datastore sync --timeout 1800. The new--timeoutonmodel method runandworkflow runuses a different format:--timeout <duration:string>requiring a unit suffix (30s,5m,1h).A user who transfers their knowledge of
--timeoutfromdatastore syncand typesswamp model method run my-model exec --timeout 1800gets:Invalid duration format: "1800". Expected format like 1h, 1d, 7d, 1w, 1moThe error doesn't even mention
s(the unit they'd reach for first), becauseparseDurationpredates this feature. The two--timeoutconventions on two commands in the same binary will reliably trip users.Suggested fix (pick one):
- Option A (preferred): Update
duration_parser.ts:parseTimeoutto also accept a bare integer and treat it as seconds, so--timeout 1800works on all three commands. Update the help text to say"Cancellation deadline — seconds (e.g. 30, 1800) or duration string (e.g. 30s, 5m, 1h)."and add an example for bare seconds. - Option B: Change the new
--timeoutto<seconds:integer>matchingdatastore sync, and document30snotation as a convenience alias thatparseTimeoutaccepts by stripping thessuffix.
Either option closes the inconsistency; Option A is more ergonomic.
- Option A (preferred): Update
Suggestions
-
--timeouthelp text uses jargon —model_method_run.ts:141andworkflow_run.ts:128: "Cooperative — only honored by methods that check AbortSignal."AbortSignalis a V8/Deno implementation term; most CLI users won't know it. Consider "Cooperative — only honored by models that support cancellation (see swamp-club#247)." or simply dropping the parenthetical from the help text since the limitation is noted in the design docs. -
parseDurationerror hint doesn't lists— if a user somehow reaches theparseDurationfallback with an invalid string, the error says"Expected format like 1h, 1d, 7d, 1w, 1mo"— missings. Low impact givenparseTimeoutcatches thescase first, but worth a note induration_parser.tsthat the fallback error is inherited and incomplete.
Verdict
NEEDS CHANGES — --timeout on the two new commands uses a different format than the existing datastore sync --timeout, which will silently break user expectations for anyone who already uses that flag.
There was a problem hiding this comment.
Code Review
Well-structured bundle of agentic-CLI improvements. The JSON-mode logger isolation fix is architecturally sound — severing sink inheritance with parentSinks: "override" and consolidating fatal output into a single renderError emitter is the right design. The :json input suffix, --timeout flag, and --repo-dir parity changes are all clean CLI-layer additions with proper test coverage.
Blocking Issues
None.
Suggestions
-
Stale comment in
logger.ts:108-110— The comment says "otherwise a child logger emitting an info record would also emit through the root'sjsonErrorsink" but thejsonErrorsink was deleted in this PR. The root logger in JSON mode now hassinks: []. The comment should reference the previous bug rather than a current sink name. -
Missing unit test for
doctor.ts--repo-dir— Other commands that gained--repo-dirin this PR (e.g.type_search) got a corresponding test.doctor.tsdidn't, though its top-level action only shows help so the risk is minimal. -
Integration tests now check
stderr + stdout— The patternassertStringIncludes(result.stderr + result.stdout, ...)is correct given the single-emitter change, but it no longer asserts which stream the error appears on. Fine for content-checking tests, but if you later want to enforce the JSON Mode Output Contract at the integration level, consider separate assertions on stdout (structured JSON) vs stderr (log records).
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found. The code paths are well-structured and the changes are internally consistent.
Medium
-
src/cli/input_parser.ts:131-153—:jsonsuffix is a silent semantic change for keys containing literal:json.
If any existing model definition has input keys named likefoo:json, the command--input foo:json=barwould now attempt to parsebaras JSON instead of setting the keyfoo:jsonto the string"bar". This is a deliberate design choice and is unlikely in practice (nobody names keys*:json), but it is technically a breaking change to the input parsing contract with no opt-out.
Mitigation: Already mitigated by theleaf.length > jsonSuffix.lengthguard (a bare:jsonkey is correctly rejected). The remaining risk is real keys ending in:json— document this in a migration note if any extensions use such key names. -
src/cli/duration_parser.ts:33—parseInton extremely large numeric strings produces imprecise results.
parseTimeout("99999999999999999999s")→parseIntyields1e+20, then* 1000produces1e+23milliseconds.AbortSignal.timeout(1e+23)behavior is implementation-defined for values exceedingNumber.MAX_SAFE_INTEGER. A practical user would never type this, but an agent constructing the flag from user input could.
Impact: Theoretical — the timeout would just be "very large" rather than precise, effectively meaning "no timeout."
Low
-
src/cli/input_parser.ts:43-62—setNestedValuetraverses__proto__/constructorpaths (pre-existing).
--input __proto__.polluted=yeswould walk intoObject.prototypevia the__proto__getter and setObject.prototype.polluted = "yes". This is prototype pollution. However: (a) this is pre-existing code not introduced by this PR, (b) CLI argument control requires shell access, (c) the agentic use case is the only plausible vector and even then the pollution only affects the current short-lived process. Not blocking — pre-existing concern for a separate hardening PR. -
src/cli/commands/workflow_run.ts:295+303—flushModelLocks()can be called twice (pre-existing).
On the success path (line 295), ifflushModelLocks()throws, the catch block (line 303) calls it again. Whether double-release is safe depends on the lock implementation. Again, pre-existing and not introduced by this PR.
Verdict
PASS — The six bundled changes are well-implemented. The JSON isolation fix correctly severs sink inheritance. The :json suffix parser handles edge cases (empty leaf, nested keys, @file bypass). The parseTimeout function correctly delegates to parseDuration while adding second-level granularity. Error output now has a clean single-emitter contract in JSON mode. Integration tests are updated consistently. The --repo-dir additions are trivially correct (accepted but unused where not needed). No critical or high issues found.
…stency
The new --timeout on `model method run` and `workflow run` originally
required a unit-suffixed string ("30s", "5m", "1h"). But `swamp datastore
sync --timeout` already ships as `<seconds:integer>` — users who transfer
their knowledge type `--timeout 1800` and get a confusing parseDuration
error that doesn't even mention `s` as a valid unit.
Update parseTimeout to accept a bare integer as seconds (matching
datastore sync). Suffixed forms still work: 30s, 5m, 1h, 1d, 1w. Update
help text to call out both forms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
--inputdescription doesn't mention the:jsonsuffix (src/cli/commands/model_method_run.ts:94,workflow_run.ts:85). The current text is "Input values (key=value or JSON)" — this still reads as only two syntaxes. The new:jsonsuffix is covered by the example added in this PR, which is good for discoverability, but a user reading only the flag descriptions (e.g. viaswamp model method run --help | grep -A1 input) won't see the new form. Suggested description update: "Input values (key=value, key:json=, or JSON object)". -
No
--timeoutexample in help text (model_method_run.ts,workflow_run.ts). The flag description is clear and self-explanatory, but a concrete example (e.g.swamp model method run my-server deploy --input env=prod --timeout 5m) would make the feature more discoverable via--help. Low priority since the description includes inline examples. -
type_search --repo-diris accepted but has no functional effect (src/cli/commands/type_search.ts:83). The description "not required for type search" is good but doesn't make explicit that passing the flag is a no-op for this command. This is intentional (agentic flag-uniformity), but a user who passes--repo-dir /alt-repomight expect extensions from that repo to appear in results. Consider: "(accepted for flag uniformity with other commands; type search reads only the global extension catalog)".
Verdict
PASS — All six agentic-ergonomics fixes are clean from a user-facing perspective. The --timeout description honestly discloses cooperative cancellation semantics, data gc --json discoverability is improved, the :json suffix is shown in examples, and the JSON output contract is solid (error JSON on stdout, no log leaks). None of the three suggestions above block merge.
There was a problem hiding this comment.
Code Review
Well-structured bundle of agentic-CLI improvements. The changes follow clean DDD layering: parseTimeout and :json suffix parsing live correctly in the CLI layer, UserError.code is a natural domain-level extension that aligns with SwampError.code in libswamp, and the renderer error-propagation bridge (e.error.code → UserError constructor) is the right way to cross the libswamp→presentation boundary. All libswamp imports go through mod.ts. License headers are present on all new files. Test coverage is solid across unit and integration levels.
Blocking Issues
None.
Suggestions
-
Stale comment in
logger.ts:108-110— The comment still references "the root'sjsonErrorsink" butcreateJsonErrorSinkwas deleted and the root logger's sinks are now[]in JSON mode. TheparentSinks: 'override'is still correct behavior, but the comment's "why" is misleading. Consider updating to reflect the current design (preventing any future sink additions from leaking). -
workflow_run_test.tsmissing models barrel import — CLAUDE.md specifies CLI command tests should includeimport "../../domain/models/models.ts"(seedata_get_test.tsfor the canonical pattern). The newworkflow_run_test.tsomits it. This test works today because it only checks option names via dynamic import, but including the barrel import would be more consistent and future-proof. (Pre-existingmodel_method_run_test.tshas the same gap, which isn't this PR's fault.) -
Minor: unreachable negative-integer path in
parseTimeout— Theseconds <= 0guard afterparseInton a^\d+$-matched string can only trigger for"0"(since the regex excludes negative numbers). The guard is fine as defense-in-depth, just noting it's not a separate code path to worry about testing.
There was a problem hiding this comment.
Adversarial Review
Critical / High
No critical or high-severity issues found. The changes are well-structured and correctly implemented across all six features.
Medium
-
src/infrastructure/logging/logger.ts:109— Stale comment references deletedjsonErrorsink. The comment reads "otherwise a child logger emitting an info record would also emit through the root'sjsonErrorsink, polluting stderr with malformed JSON." ThejsonErrorsink was deleted in this PR. The explanation of whyparentSinks: 'override'is needed is still accurate, but referencing a non-existent sink could confuse future readers. Suggest rewording to: "otherwise a child logger emitting an info record would also inherit the root logger's sinks." -
src/cli/duration_parser.ts:40—parseTimeout("0")throws but the test only covers the0case, not the bare-integer overflow boundary.parseInt("999999999999999999", 10)returns1000000000000000000due to IEEE 754 precision loss.1000000000000000000 * 1000is1e21ms (~31,709 years). Not a realistic user input, but the function doc says "Returns milliseconds, always > 0" — a Number.isSafeInteger guard would be belt-and-suspenders. Not blocking — this is within normal CLI tolerance. -
Integration test assertions now check
stderr + stdoutinstead of juststderr. For exampleintegration/inputs_test.ts:234:assertStringIncludes(result.stderr + result.stdout, "must be one of"). While this correctly accommodates the new JSON-mode error-on-stdout path, it means the tests would also pass if log-mode errors accidentally moved from stderr to stdout. This reduces the diagnostic precision of the test suite. Not blocking — the JSON isolation integration test (json_isolation_test.ts) separately guards the channel contract.
Low
-
src/cli/input_parser.ts:138— A key named literally:json(e.g.:json=value) does NOT trigger JSON parsing because of theleaf.length > jsonSuffix.lengthguard (5 > 5 is false). This is correct and intentional, but undocumented. A user who types--input ':json=["a"]'gets the raw string["a"]stored under the key:json, which may be surprising. Not worth fixing — the edge case is extremely unlikely. -
src/cli/duration_parser.ts:52—"1.5s"is silently rejected by the/^(\d+)s$/regex (the dot is not\d). It falls through toparseDuration("1.5s")which also rejects it with a UserError about format. The error message fromparseDurationsays "Expected format like 1h, 1d, 7d" — it doesn't mentionsas valid, which may confuse users who are close to valid input. Minor UX gap. -
src/libswamp/context.ts:49—AbortSignal.timeout(ms)creates an internal timer with no cancellation handle. If--timeout 999999999is passed, the timer lives for the process lifetime. No real leak since the process ends, butAbortSignal.any()+AbortSignal.timeout()together keep references alive until both fire. Purely theoretical.
Verdict
PASS — Solid implementation across all six features. The JSON-mode logger isolation is correctly designed with parentSinks: 'override', the single-emitter renderError pattern is clean, the :json suffix parser handles edge cases well (nested keys, @file bypass, empty leaves), and parseTimeout correctly unifies the bare-integer and suffixed forms. Test coverage is thorough. The medium findings are cosmetic/diagnostic — none represent incorrect behavior.
Summary
Closes swamp-club#235.
Bundles 6 agentic-CLI ergonomics fixes that make swamp friendlier for AI-agent integrations and JSON-piping workflows.
What's in the bundle
parentSinks: "override"on the run/workflow/logtape-meta category loggers in jsonMode. Closes the leak path where info-level records could inherit root sinks under `--json`.Design + skills
Regression guard
Out of scope, tracked separately
Test plan
🤖 Generated with Claude Code