Skip to content

feat(logger): standardize output and fold the spinner into the logger#32

Merged
rgdevme merged 1 commit into
mainfrom
feat/standardize-logging
Jul 1, 2026
Merged

feat(logger): standardize output and fold the spinner into the logger#32
rgdevme merged 1 commit into
mainfrom
feat/standardize-logging

Conversation

@rgdevme

@rgdevme rgdevme commented Jul 1, 2026

Copy link
Copy Markdown
Owner

What

Standardizes all CLI output into a single reusable, domain-aware logging component and folds the spinner into it.

Every line now renders in one shape:

[domain]  message [status]
extra line(s)
  • Prefix [domain] — padded, dimmed, in the domain's color (docs=cyan, skills=magenta, rules=green, mcp=blue, hooks=yellow, agents=gray; supervisor/core lines use [agnos]).
  • Message — colored by level: white (info), green (success), yellow (warn), red (error), blue (debug).
  • Status — optional, italic + dimmed (e.g. changed).
  • Extra — optional detail lines, white (e.g. a file list).

How

  • New formatLog component + structured LogInput ({ message, status, extra }); plain strings stay valid.
  • ctx.logger is scoped per domain via withDomain across the run pipeline, CLI command dispatch, and init steps — so every line carries its prefix/color automatically. Removes now-redundant hand-rolled domain: prefixes and the inline metadata-shape block from the docs/rules warnings (the shape lives in the doc-authoring skill).
  • Spinner folded into the logger: log methods accept an optional waitFor promise (plus done) that animates a standardized, domain-colored spinner until it settles, then returns the resolved value. withSpinner/createSpinner are removed and all 8 call sites migrated; the hand-passed { quiet } is gone (the logger owns it).

Verification

  • pnpm typecheck — clean
  • pnpm test — 241/241 pass
  • pnpm lint — 0 errors

Spinners are no-ops in the non-TTY test env, so migrated sites behave exactly as before (await + return value); live spinner appearance is best eyeballed in a real terminal.

Notes

Scoped to src/** + test/**. Unrelated in-progress working-tree edits (.docs/plans/prd.md — which currently has a descripti0on typo — .docs/index.md, AGENTS.md, package.json) were intentionally left out of this branch.

Introduce a reusable, domain-aware log-line component. Every line renders
as `[domain]  message [status]` with a dimmed domain-colored prefix, a
level-colored message (white/green/yellow/red/blue), an italic-dim status,
and white extra lines. Structured LogInput ({ message, status, extra })
replaces ad-hoc strings; plain strings stay valid.

- Scope ctx.logger per domain across the run pipeline, CLI command
  dispatch, and init steps via withDomain, so every line carries its
  [domain] prefix and color automatically (docs=cyan, skills=magenta,
  rules=green, mcp=blue, hooks=yellow, agents=gray; supervisor -> [agnos]).
- Fold the spinner in: log methods accept an optional waitFor promise
  (plus done) that animates a standardized, domain-colored spinner until
  it settles, then returns the resolved value. Remove withSpinner /
  createSpinner and migrate all call sites.
- Strip now-redundant domain-name prefixes from messages and drop the
  inline metadata-shape block from the docs/rules warnings (the shape
  lives in the doc-authoring skill).
@rgdevme rgdevme merged commit 21f7b37 into main Jul 1, 2026
2 checks passed
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

feat(logger): Standardize CLI output and fold spinner into domain-aware logger

✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Introduces a unified formatLog component rendering every line as [domain]  message [status]
 with domain-colored prefix, level-colored message, italic-dim status, and white extra lines.
• Adds LogInput / LogParts / LogTask types; logger methods now accept a waitFor promise that
 animates a domain-colored spinner and returns the resolved value, replacing the standalone
 withSpinner / createSpinner helpers.
• Scopes ctx.logger per domain via withDomain in the run pipeline, CLI command dispatch, and
 init steps so every line carries its [domain] prefix automatically.
• Migrates all 8 withSpinner call sites across agents, docs, mcp, rules, and skills
 domains; removes hand-rolled domain: prefixes from log messages.
• Strips the inline metadata-shape block from docs/rules warnings (shape now lives in the
 doc-authoring skill); updates tests to assert on structured LogParts instead of raw strings.
Diagram

graph TD
    CLI["cli.ts\nCommand dispatch"] --> withDomain
    Run["run.ts\nDomain runner"] --> withDomain
    InitSteps["init-steps.ts\nInit pipeline"] --> withDomain
    Watch["watch.ts\nFile watcher"] --> withDomain
    withDomain --> makeLogger
    createLogger --> makeLogger
    makeLogger --> Logger(["Logger\ninfo/warn/error/debug/success"])
    Logger -->|"LogInput → void"| emit["emit()\nformatLog → console"]  
    Logger -->|"LogTask → Promise"| spin["spin()\nstartSpinner + waitFor"]
    emit --> formatLog["formatLog()\nPure renderer"]
    spin --> startSpinner["startSpinner()\nDomain-colored spinner"]
    formatLog --> formatPrefix["formatPrefix()\n[domain] padded"]
    Domains["Domain modules\nagents/docs/rules/mcp/skills/hooks"] -->|"ctx.logger.info waitFor"| Logger
    subgraph Legend
      direction LR
      _mod["Module"] ~~~ _fn(["Function"]) ~~~ _ext["External caller"]
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep spinner separate, only standardize log format
  • ➕ Simpler Logger interface — no overloads or async methods
  • ➕ Spinner lifecycle remains explicit at call sites
  • ➖ Still requires threading quiet/domain into every withSpinner call
  • ➖ Two APIs to maintain instead of one
2. Use a dedicated LogContext/Sink abstraction
  • ➕ Cleaner separation between formatting and I/O
  • ➕ Easier to swap output targets in tests
  • ➖ More boilerplate; overkill for a CLI tool
  • ➖ Doesn't eliminate the spinner-threading problem

Recommendation: The PR's approach — folding the spinner into the logger via an overloaded waitFor method — is clean and eliminates the separate spinner API surface. The main alternative worth considering is keeping the spinner as a separate concern (e.g. a Spinner class or a standalone withSpinner wrapper) and only standardizing the log format. The PR's approach wins on ergonomics (one call site, automatic cleanup, typed return value) but the overloaded method signature (LogTask vs LogInput) is a subtle API contract that test stubs must handle correctly — the PR addresses this by switching tests to use createLogger({ quiet: true }) as the base. The chosen approach is sound.

Files changed (19) +396 / -253

Enhancement (3) +64 / -7
public.tsAdd DomainColor, LogParts, LogInput, LogTask types; overload Logger methods; add Domain.color +51/-5

Add DomainColor, LogParts, LogInput, LogTask types; overload Logger methods; add Domain.color

• Defines 'DomainColor' union, 'LogParts' structured payload, 'LogInput' (string | LogParts), and 'LogTask<T>' (LogParts + waitFor promise + optional done). Overloads every 'Logger' method with a 'LogTask<T>' → 'Promise<T>' signature alongside the existing 'LogInput' → 'void' signature. Adds optional 'color?: DomainColor' to the 'Domain' interface.

src/core/types/public.ts

run.tsScope ctx.logger per domain in runDomain via withDomain +5/-1

Scope ctx.logger per domain in runDomain via withDomain

• Calls 'withDomain(ctx.logger, dom.domain)' before invoking each domain's 'run', so all domain output automatically carries the correct '[domain]' prefix without any changes inside the domain itself.

src/core/run.ts

cli.tsScope logger to owning domain when dispatching CLI subcommands +8/-1

Scope logger to owning domain when dispatching CLI subcommands

• Builds a domain-scoped logger via 'withDomain' and injects it into 'CommandContext' before calling 'cmd.run', matching the prefix behavior of run-mode output.

src/core/cli.ts

Refactor (12) +308 / -221
logger.tsRewrite logger: add formatLog, domain-aware prefix, spinner-as-waitFor, withDomain +187/-95

Rewrite logger: add formatLog, domain-aware prefix, spinner-as-waitFor, withDomain

• Replaces the ad-hoc ANSI color map and 'paint()' helper with 'yoctocolors-cjs'. Introduces 'formatLog' (pure log-line renderer), 'formatPrefix' (padded domain prefix), and 'startSpinner' (internal, domain-colored). Adds 'makeLogger' backed by a 'WeakMap<Logger, LoggerState>' so 'withDomain' can derive a sibling logger sharing quiet/debug config but carrying a different domain prefix. Each log method now dispatches to either a synchronous 'emit' path or an async 'spin' path when 'waitFor' is present. Removes 'createSpinner', 'withSpinner', and the 'Spinner' interface.

src/core/logger.ts

index.tsExport new logger API; remove createSpinner/withSpinner/Spinner exports +5/-2

Export new logger API; remove createSpinner/withSpinner/Spinner exports

• Replaces 'createSpinner', 'withSpinner', and 'Spinner' exports with 'formatLog' and 'withDomain'. Adds 'DomainColor', 'LogInput', 'LogParts', and 'LogTask' to the public type re-exports.

src/core/index.ts

init-steps.tsScope logger to domain in runDomainInitSteps; remove domain-name prefixes from messages +12/-6

Scope logger to domain in runDomainInitSteps; remove domain-name prefixes from messages

• Creates a 'dctx' with a domain-scoped logger via 'withDomain' and passes it to all step callbacks. Removes the 'domain.id.' prefix from log messages since the prefix is now in the logger itself.

src/core/commands/init-steps.ts

watch.tsUse domain-scoped logger and structured LogInput in watch event handler +6/-3

Use domain-scoped logger and structured LogInput in watch event handler

• Creates a 'domLog' via 'withDomain' for each watched domain. Converts the file-change log line to a structured '{ message: rel, status: eventLabel(event) }' payload and simplifies the config-change message.

src/core/watch.ts

index.tsMigrate agents domain: replace createSpinner with logger.info waitFor; add color +8/-10

Migrate agents domain: replace createSpinner with logger.info waitFor; add color

• Removes the 'createSpinner' import and replaces the manual spinner loop with 'ctx.logger.info({ message, waitFor })' per adapter. Adds 'color: 'gray'' to 'agentsDomain'. Converts the unknown-agent warning to structured 'LogParts'.

src/domains/agents/index.ts

index.tsMigrate docs domain: replace withSpinner with logger.info waitFor; add color +6/-6

Migrate docs domain: replace withSpinner with logger.info waitFor; add color

• Removes 'withSpinner' import and replaces the spinner call with 'ctx.logger.info({ message, waitFor: compileDocsIndex(...) })'. Adds 'color: 'cyan'' to 'docsDomain'.

src/domains/docs/index.ts

compile.tsRemove inline metadataShape block from incomplete-files warning; use structured LogParts +6/-17

Remove inline metadataShape block from incomplete-files warning; use structured LogParts

• Deletes the 'metadataShape()' helper and the fenced-code block it produced in the warning. The warning now emits a structured '{ message, extra: [...files] }' payload; the frontmatter shape reference moves to the doc-authoring skill.

src/domains/docs/compile.ts

index.tsMigrate rules domain: replace withSpinner with logger.info waitFor; structured warnings; add color +15/-18

Migrate rules domain: replace withSpinner with logger.info waitFor; structured warnings; add color

• Removes 'withSpinner' import; replaces the spinner call with 'ctx.logger.info({ message, waitFor })'. Converts all warn/info calls to structured 'LogParts'. Adds 'color: 'green'' to 'rulesDomain'.

src/domains/rules/index.ts

index.tsMigrate mcp domain: replace withSpinner with logger.info waitFor; add color +13/-13

Migrate mcp domain: replace withSpinner with logger.info waitFor; add color

• Removes 'withSpinner' import; migrates two spinner call sites ('addFromRegistry' and the update command) to 'ctx.logger.info({ message, waitFor })'. Adds 'color: 'blue'' to 'mcpDomain'.

src/domains/mcp/index.ts

index.tsMigrate skills domain: replace withSpinner with logger.info waitFor; add color +33/-38

Migrate skills domain: replace withSpinner with logger.info waitFor; add color

• Removes 'withSpinner' import; migrates four spinner call sites (diagnose, add, update, run) to 'ctx.logger.info({ message, waitFor })'. Adds 'color: 'magenta'' to 'skillsDomain'.

src/domains/skills/index.ts

pipeline.tsConvert skills-pipeline drift warning to structured LogParts +4/-5

Convert skills-pipeline drift warning to structured LogParts

• Replaces the concatenated multi-line string warn with a structured '{ message, extra }' payload matching the new log format.

src/domains/skills/pipeline.ts

shared.tsConvert warn calls in mirrorRules/linkSkills to structured LogParts +13/-8

Convert warn calls in mirrorRules/linkSkills to structured LogParts

• Replaces three string-based 'ctx.logger.warn(...)' calls with structured '{ message, status }' / '{ message, extra }' payloads; removes hand-rolled 'rules:' / 'skills:' prefixes.

src/agents/adapters/shared.ts

Tests (3) +23 / -25
init-steps.test.tsUpdate init-steps test to match new dry-run log message format +1/-1

Update init-steps test to match new dry-run log message format

• Adjusts the assertion from ''agents.select = ...'' to ''select = ...'' to reflect the removal of the domain-name prefix from init-step log messages.

test/core/init-steps.test.ts

compile.test.tsUpdate compile test to assert on structured LogParts instead of raw string +7/-11

Update compile test to assert on structured LogParts instead of raw string

• Changes the warning capture type from 'string[]' to 'LogParts[]' and asserts on 'message' and 'extra' fields separately; removes assertions for the now-deleted metadata shape block.

test/docs/compile.test.ts

skills-pipeline.test.tsUpdate skills-pipeline test to use real logger + structured LogParts assertions +15/-13

Update skills-pipeline test to use real logger + structured LogParts assertions

• Replaces the hand-rolled stub logger with 'createLogger({ quiet: true })' (so 'info''s 'waitFor' path runs correctly) and overrides only 'warn' to capture 'LogParts'. Asserts on 'message' and 'extra' fields instead of a joined string.

test/domains/skills-pipeline.test.ts

Other (1) +1 / -0
index.tsAdd color: 'yellow' to hooksDomain +1/-0

Add color: 'yellow' to hooksDomain

• Single-line addition of 'color: 'yellow'' to the 'hooksDomain' definition so its log prefix renders in yellow.

src/domains/hooks/index.ts

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (2) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 64 rules

Grey Divider


Action required

1. Quiet still shows spinners 🐞 Bug ☼ Reliability
Description
In quiet mode, warn/error LogTask calls will still start an animated spinner because spinner
suppression is derived from suppressed(level), which returns false for warn/error even when quiet is
true. This can leak cursor-hide/show and frame output into CI/scripts where quiet previously
prevented spinner escape codes entirely.
Code

src/core/logger.ts[R149-173]

+  // Whether a level's persistent output is suppressed: quiet drops
+  // info/success/debug; debug additionally needs the debug flag. warn/error
+  // always print. (The awaited work in a `waitFor` runs regardless.)
+  const suppressed = (level: LogLevel): boolean => {
+    if (level === "debug") return state.quiet || !state.debugEnabled;
+    if (level === "info" || level === "success") return state.quiet;
+    return false;
+  };
+
  // Wipe any running spinner frame before each write so the log line lands on a
  // clean row; the spinner redraws below it on its next tick.
-  return {
-    info: quiet
-      ? noop
-      : (msg) => {
-          clearActiveLine();
-          console.log(msg);
-        },
-    success: quiet
-      ? noop
-      : (msg) => {
-          clearActiveLine();
-          console.log(paint("green", "✓ ") + msg);
-        },
-    warn(msg) {
-      clearActiveLine();
-      console.warn(paint("yellow", "! ") + msg);
-    },
-    error(msg) {
-      clearActiveLine();
-      console.error(paint("red", "✗ ") + msg);
-    },
-    debug: quiet
-      ? noop
-      : (msg) => {
-          if (!debugEnabled) return;
-          clearActiveLine();
-          console.error(paint("gray", "· " + msg));
-        },
+  const emit = (level: LogLevel, msg: LogInput): void => {
+    if (suppressed(level)) return;
+    clearActiveLine();
+    const line = formatLog(level, msg, fmt);
+    if (level === "warn") console.warn(line);
+    else if (level === "error" || level === "debug") console.error(line);
+    else console.log(line);
+  };
+
+  // `waitFor` path: spin (unless suppressed / non-TTY) until the promise settles,
+  // then stop; on success optionally print `done`, and always return the value.
+  const spin = async <T>(level: LogLevel, task: LogTask<T>): Promise<T> => {
+    const handle = startSpinner(level, task, fmt, suppressed(level));
+    try {
Relevance

⭐⭐⭐ High

PR27 asserts --quiet produces no output/escape codes; warn/error spinners would violate that intent.

PR-#27
PR-#32

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The logger uses suppressed(level) both to decide whether to print and whether to spin. But
suppressed() explicitly returns false for warn/error regardless of state.quiet, so the spinner
is enabled for warn/error even under quiet mode; startSpinner only disables when its quiet
argument is true.

src/core/logger.ts[149-173]
src/core/logger.ts[91-106]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`startSpinner()` is currently disabled using `suppressed(level)`, but `suppressed()` intentionally never suppresses warn/error even when `quiet` is true. As a result, `warn({waitFor})` / `error({waitFor})` will still animate a spinner and emit terminal control sequences in quiet mode.

## Issue Context
Historically, `quiet` was used specifically to keep scripts/CI free of escape codes. The new implementation should preserve that behavior by treating "spinner allowed" separately from "log line allowed".

## Fix Focus Areas
- src/core/logger.ts[149-184]
- src/core/logger.ts[99-123]

## Suggested fix
Introduce a dedicated `spinnerSuppressed(level)` (or similar) that returns `state.quiet || (level === "debug" && !state.debugEnabled)` and pass that to `startSpinner(...)` instead of `suppressed(level)`. Keep `suppressed(level)` controlling only whether a persistent log line prints.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Loose waitFor detection 🐞 Bug ☼ Reliability
Description
Logger methods choose spinner mode using only 'waitFor' in msg, so any object that happens to
contain a waitFor key (even undefined/non-Promise) will be treated as a LogTask and awaited. This
can unexpectedly change runtime behavior/return type and may mask malformed inputs by "awaiting"
non-Promises.
Code

src/core/logger.ts[R186-194]

+  // A single implementation backs both overloads: a `waitFor` payload takes the
+  // spinner path (returns a promise), everything else prints synchronously.
+  const make =
+    (level: LogLevel) =>
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    (msg: any): any => {
+      if (msg !== null && typeof msg === "object" && "waitFor" in msg) return spin(level, msg);
+      emit(level, msg);
+    };
Relevance

⭐⭐ Medium

No prior suggestions found about stricter waitFor discriminant checks; enforcement unclear.

PR-#32

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The implementation uses only property presence ("waitFor" in msg) to route into spin(). The
public type contract defines waitFor as a Promise, so routing arbitrary objects violates the
intended discriminant and can lead to awaiting invalid values.

src/core/logger.ts[186-194]
src/core/types/public.ts[167-176]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The logger decides between synchronous `emit()` and async `spin()` with `"waitFor" in msg`, which is a very permissive discriminator. It will treat arbitrary objects as tasks if they merely have a `waitFor` property.

## Issue Context
`LogTask.waitFor` is intended to be a `Promise<T>`. Runtime behavior should align with that contract and avoid accidental collisions.

## Fix Focus Areas
- src/core/logger.ts[186-194]
- src/core/types/public.ts[167-177]

## Suggested fix
Tighten the guard to require a thenable:
- `const w = (msg as any).waitFor; if (w && typeof w.then === "function") ...`
Optionally also ensure `typeof (msg as any).message === "string"` to avoid nonsensical spinner payloads.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Init logs can lose domain 🐞 Bug ◔ Observability
Description
Init-step logging now omits domain.id from messages and relies on withDomain() for domain
context via the prefix. But withDomain() returns the base logger unchanged when it wasn't created
by createLogger(), so custom/injected loggers will emit init-step lines with no domain identifier.
Code

src/core/commands/init-steps.ts[R26-45]

+  // Scope the logger (and the ctx handed to steps/callbacks) to this domain so
+  // every init line carries its `[domain]` prefix; the id in messages is then
+  // just the step (the domain is already in the prefix).
+  const dctx: ResolveContext = { ...ctx, logger: withDomain(ctx.logger, domain) };
+
  for (const step of steps) {
    try {
-      if (step.when && !(await step.when(ctx))) {
-        ctx.logger.debug(`skipping ${domain.id}.${step.id} (when predicate false)`);
+      if (step.when && !(await step.when(dctx))) {
+        dctx.logger.debug(`skipping ${step.id} (when predicate false)`);
        continue;
      }
-      const value = await resolveStepValue(step, opts, ctx);
+      const value = await resolveStepValue(step, opts, dctx);
      if (opts.dryRun) {
-        ctx.logger.info(`would: ${domain.id}.${step.id} = ${formatValue(value)}`);
+        dctx.logger.info(`would: set ${step.id} = ${formatValue(value)}`);
        continue;
      }
-      await invokeCallback(step, value, ctx);
+      await invokeCallback(step, value, dctx);
    } catch (err) {
-      ctx.logger.error(`${domain.id}.${step.id} failed: ${(err as Error).message}`);
+      dctx.logger.error({ message: `${step.id} failed`, status: (err as Error).message });
    }
Relevance

⭐⭐ Medium

No historical review evidence; PR32 design returns base logger unchanged for non-createLogger
loggers.

PR-#32

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Init-step messages are now rendered without domain.id, assuming the logger will add the domain
prefix. But withDomain() explicitly returns the base logger unchanged when it lacks internal
state, so injected/custom loggers won't get the prefix and the domain context is lost.

src/core/commands/init-steps.ts[26-45]
src/core/logger.ts[216-226]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`runDomainInitSteps()` logs only `step.id` (e.g. `would: set select = ...`) and depends on `withDomain()` to supply the domain prefix. However, `withDomain()` is explicitly a no-op for loggers not produced by `createLogger()`, so domain context can be lost.

## Issue Context
The codebase exposes `BuildContextOptions.logger?: Logger`, so consumers/tests may inject custom loggers. Those loggers will not get the `[domain]` prefix, and the new messages no longer include `domain.id`.

## Fix Focus Areas
- src/core/commands/init-steps.ts[26-45]
- src/core/logger.ts[216-226]

## Suggested fix
Either:
1) Re-include `domain.id` in init-step messages (e.g. `skipping ${domain.id}.${step.id} ...`, `would: set ${domain.id}.${step.id} = ...`, `${domain.id}.${step.id} failed`), or
2) Enhance `withDomain()` to wrap unknown loggers (no WeakMap state) by prefixing `LogInput.message` with `[domain]`/`domain:` so domain context is preserved even for injected loggers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. any lacks justification 📘 Rule violation ⚙ Maintainability
Description
any is used in the new logger implementation without an attached rationale comment, which violates
the requirement to justify each any escape hatch. This reduces type-safety and makes it unclear
why stricter typing isn't feasible here.
Code

src/core/logger.ts[R190-193]

+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    (msg: any): any => {
+      if (msg !== null && typeof msg === "object" && "waitFor" in msg) return spin(level, msg);
+      emit(level, msg);
Relevance

⭐ Low

PR32 merged any usage with only eslint-disable, no rationale comment; rule seems unenforced.

PR-#32

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 704843 requires a rationale comment immediately adjacent to each any usage. The
logger overload implementation introduces (msg: any): any and another msg: any in the wrapper
without an explanatory comment.

Rule 704843: Require justification comments for TypeScript any usage
src/core/logger.ts[188-194]
src/core/logger.ts[238-242]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
TypeScript `any` is used without a justification comment directly attached to the usage.

## Issue Context
The compliance rule requires each `any` to have a nearby comment explaining *why* `any` is required (lint-disable comments alone are not sufficient).

## Fix Focus Areas
- src/core/logger.ts[188-194]
- src/core/logger.ts[238-242]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. detail array not plural 📘 Rule violation ⚙ Maintainability
Description
A local array variable is named detail, which is not a plural noun despite holding multiple items.
This conflicts with the collection naming convention and can reduce readability.
Code

src/core/logger.ts[R58-59]

+  const detail = extra === undefined ? [] : Array.isArray(extra) ? extra : [extra];
+  for (const d of detail) line += `\n${colors.white(d)}`;
Relevance

⭐ Low

PR32 merged detail as array name; naming convention not enforced in practice.

PR-#32

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 704868 requires collection variables to be named with plural nouns. In formatLog,
detail is constructed as an array and iterated, but is named singular.

Rule 704868: Collection variables must be named with plural nouns
src/core/logger.ts[55-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A collection variable is named with a singular noun (`detail`) even though it holds multiple items.

## Issue Context
The naming convention requires collection variables to be plural nouns.

## Fix Focus Areas
- src/core/logger.ts[58-59]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/core/logger.ts
Comment on lines +149 to +173
// Whether a level's persistent output is suppressed: quiet drops
// info/success/debug; debug additionally needs the debug flag. warn/error
// always print. (The awaited work in a `waitFor` runs regardless.)
const suppressed = (level: LogLevel): boolean => {
if (level === "debug") return state.quiet || !state.debugEnabled;
if (level === "info" || level === "success") return state.quiet;
return false;
};

// Wipe any running spinner frame before each write so the log line lands on a
// clean row; the spinner redraws below it on its next tick.
return {
info: quiet
? noop
: (msg) => {
clearActiveLine();
console.log(msg);
},
success: quiet
? noop
: (msg) => {
clearActiveLine();
console.log(paint("green", "✓ ") + msg);
},
warn(msg) {
clearActiveLine();
console.warn(paint("yellow", "! ") + msg);
},
error(msg) {
clearActiveLine();
console.error(paint("red", "✗ ") + msg);
},
debug: quiet
? noop
: (msg) => {
if (!debugEnabled) return;
clearActiveLine();
console.error(paint("gray", "· " + msg));
},
const emit = (level: LogLevel, msg: LogInput): void => {
if (suppressed(level)) return;
clearActiveLine();
const line = formatLog(level, msg, fmt);
if (level === "warn") console.warn(line);
else if (level === "error" || level === "debug") console.error(line);
else console.log(line);
};

// `waitFor` path: spin (unless suppressed / non-TTY) until the promise settles,
// then stop; on success optionally print `done`, and always return the value.
const spin = async <T>(level: LogLevel, task: LogTask<T>): Promise<T> => {
const handle = startSpinner(level, task, fmt, suppressed(level));
try {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Quiet still shows spinners 🐞 Bug ☼ Reliability

In quiet mode, warn/error LogTask calls will still start an animated spinner because spinner
suppression is derived from suppressed(level), which returns false for warn/error even when quiet is
true. This can leak cursor-hide/show and frame output into CI/scripts where quiet previously
prevented spinner escape codes entirely.
Agent Prompt
## Issue description
`startSpinner()` is currently disabled using `suppressed(level)`, but `suppressed()` intentionally never suppresses warn/error even when `quiet` is true. As a result, `warn({waitFor})` / `error({waitFor})` will still animate a spinner and emit terminal control sequences in quiet mode.

## Issue Context
Historically, `quiet` was used specifically to keep scripts/CI free of escape codes. The new implementation should preserve that behavior by treating "spinner allowed" separately from "log line allowed".

## Fix Focus Areas
- src/core/logger.ts[149-184]
- src/core/logger.ts[99-123]

## Suggested fix
Introduce a dedicated `spinnerSuppressed(level)` (or similar) that returns `state.quiet || (level === "debug" && !state.debugEnabled)` and pass that to `startSpinner(...)` instead of `suppressed(level)`. Keep `suppressed(level)` controlling only whether a persistent log line prints.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/core/logger.ts
Comment on lines +186 to +194
// A single implementation backs both overloads: a `waitFor` payload takes the
// spinner path (returns a promise), everything else prints synchronously.
const make =
(level: LogLevel) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(msg: any): any => {
if (msg !== null && typeof msg === "object" && "waitFor" in msg) return spin(level, msg);
emit(level, msg);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Loose waitfor detection 🐞 Bug ☼ Reliability

Logger methods choose spinner mode using only 'waitFor' in msg, so any object that happens to
contain a waitFor key (even undefined/non-Promise) will be treated as a LogTask and awaited. This
can unexpectedly change runtime behavior/return type and may mask malformed inputs by "awaiting"
non-Promises.
Agent Prompt
## Issue description
The logger decides between synchronous `emit()` and async `spin()` with `"waitFor" in msg`, which is a very permissive discriminator. It will treat arbitrary objects as tasks if they merely have a `waitFor` property.

## Issue Context
`LogTask.waitFor` is intended to be a `Promise<T>`. Runtime behavior should align with that contract and avoid accidental collisions.

## Fix Focus Areas
- src/core/logger.ts[186-194]
- src/core/types/public.ts[167-177]

## Suggested fix
Tighten the guard to require a thenable:
- `const w = (msg as any).waitFor; if (w && typeof w.then === "function") ...`
Optionally also ensure `typeof (msg as any).message === "string"` to avoid nonsensical spinner payloads.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +26 to 45
// Scope the logger (and the ctx handed to steps/callbacks) to this domain so
// every init line carries its `[domain]` prefix; the id in messages is then
// just the step (the domain is already in the prefix).
const dctx: ResolveContext = { ...ctx, logger: withDomain(ctx.logger, domain) };

for (const step of steps) {
try {
if (step.when && !(await step.when(ctx))) {
ctx.logger.debug(`skipping ${domain.id}.${step.id} (when predicate false)`);
if (step.when && !(await step.when(dctx))) {
dctx.logger.debug(`skipping ${step.id} (when predicate false)`);
continue;
}
const value = await resolveStepValue(step, opts, ctx);
const value = await resolveStepValue(step, opts, dctx);
if (opts.dryRun) {
ctx.logger.info(`would: ${domain.id}.${step.id} = ${formatValue(value)}`);
dctx.logger.info(`would: set ${step.id} = ${formatValue(value)}`);
continue;
}
await invokeCallback(step, value, ctx);
await invokeCallback(step, value, dctx);
} catch (err) {
ctx.logger.error(`${domain.id}.${step.id} failed: ${(err as Error).message}`);
dctx.logger.error({ message: `${step.id} failed`, status: (err as Error).message });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Init logs can lose domain 🐞 Bug ◔ Observability

Init-step logging now omits domain.id from messages and relies on withDomain() for domain
context via the prefix. But withDomain() returns the base logger unchanged when it wasn't created
by createLogger(), so custom/injected loggers will emit init-step lines with no domain identifier.
Agent Prompt
## Issue description
`runDomainInitSteps()` logs only `step.id` (e.g. `would: set select = ...`) and depends on `withDomain()` to supply the domain prefix. However, `withDomain()` is explicitly a no-op for loggers not produced by `createLogger()`, so domain context can be lost.

## Issue Context
The codebase exposes `BuildContextOptions.logger?: Logger`, so consumers/tests may inject custom loggers. Those loggers will not get the `[domain]` prefix, and the new messages no longer include `domain.id`.

## Fix Focus Areas
- src/core/commands/init-steps.ts[26-45]
- src/core/logger.ts[216-226]

## Suggested fix
Either:
1) Re-include `domain.id` in init-step messages (e.g. `skipping ${domain.id}.${step.id} ...`, `would: set ${domain.id}.${step.id} = ...`, `${domain.id}.${step.id} failed`), or
2) Enhance `withDomain()` to wrap unknown loggers (no WeakMap state) by prefixing `LogInput.message` with `[domain]`/`domain:` so domain context is preserved even for injected loggers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant