Skip to content

feat(#51): add --verbose flag for debug output - #143

Open
aperson30 wants to merge 1 commit into
medic:mainfrom
aperson30:feat/51-verbose-flag
Open

feat(#51): add --verbose flag for debug output#143
aperson30 wants to merge 1 commit into
medic:mainfrom
aperson30:feat/51-verbose-flag

Conversation

@aperson30

Copy link
Copy Markdown

Summary

Closes #51.

Adds a --verbose / -v flag to the full-workflow CLI (npm run full) that emits detailed debug output to stderr. Normal stdout output is unchanged when the flag is absent.

npm run full tickets/my-ticket.md --verbose

What it logs

At the CLI boundary: verbose-mode notice, cht-core path, parsed ticket (title/type/domain), configured model, development options, full-workflow timing, and a completion summary. All payloads are redacted + truncated before writing.

Design

  • src/utils/debug-logger.ts — small reusable logger:
    • writes to stderr (never pollutes stdout / piped results)
    • redacts secrets: sk-ant- / sk- keys, Bearer + Authorization headers, and api_key/token/secret/password key-value pairs (linear-time patterns, no ReDoS)
    • truncates long payloads (LLM prompt/response sized), reporting original length
    • no-op when disabled; sink + clock injectable for tests
  • src/cli/full-args.ts — position-independent flag/ticket parsing, split out so it is unit-testable without importing the CLI entry (which runs the workflow). Also fixes a latent bug where a flag in the first positional slot was treated as the ticket path.
  • src/cli/full.ts — wires the flag in and guards the auto-run with require.main === module.

Scope note

This covers the flag, stderr routing, redaction/truncation, and CLI-level lifecycle + timing logging. Deeper per-LLM-call prompt/response/token logging (listed aspirationally in the issue) is a natural follow-up that can reuse this same logger — kept out here to keep the change focused and the diff reviewable.

Acceptance criteria

  • --verbose flag implemented
  • Debug output goes to stderr (not stdout)
  • Normal output unchanged without flag
  • Sensitive data (API keys) NOT logged — redaction, unit-tested per secret class
  • Documentation updated (README + CLI header)

Test plan

  • npm run build — clean
  • npm run lint — clean
  • npm test — 1032 passing (+21 new: 15 logger, 6 arg-parsing)
  • npm run test:coverage — all floors pass; debug-logger.ts 96% stmts, full-args.ts 100%

…low CLI

Add a `--verbose` / `-v` flag to `npm run full` that emits detailed debug
output to stderr: configuration, parsed ticket, model, development options,
and phase timing. Normal stdout output is unchanged when the flag is absent.

- New reusable `src/utils/debug-logger.ts`: a small logger that writes to
  stderr, redacts secrets (sk-ant-/sk- keys, Bearer/Authorization headers,
  api_key/token/secret/password key-values), and truncates long payloads.
  Sink and clock are injectable for testing.
- New `src/cli/full-args.ts`: position-independent flag/ticket-path parsing,
  split from the CLI entry so it is unit-testable without running the workflow.
- Wire the flag into `src/cli/full.ts`; guard the auto-run with
  `require.main === module` so importing it in tests has no side effects.
- Docs: README section + updated CLI header/usage.

Tests: redaction (each secret class), truncation, logger enable/disable,
JSON/circular payloads, injected-clock timing, and arg parsing.

@sugat009 sugat009 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice piece of work. The logger's shape is genuinely good: injectable sink and clock, redaction ordered before truncation, a shared no-op for the disabled path, and splitting parseCliArgs into its own module specifically so it is testable without importing the CLI entrypoint. The require.main === module guard is the right instinct too. 21 new tests, suite at 1022 from a 1001 baseline, tsc and eslint clean.

One blocking problem, and it is the invocation rather than the code: npm run full <ticket> --verbose, the only form the README documents, does not enable verbose mode. --verbose is npm's own flag, so npm consumes it instead of forwarding it, and -v makes npm print its version and never run the workflow at all. I verified both with an argv probe rather than inferring it; details inline. The parser is fine, so this is a one-line docs fix plus a decision about -v.

The rest is redaction hardening. I attacked the five patterns with a corpus of real key shapes, and the one that matters for this repo is sk-: the rule written for OpenAI-style keys omits - and _ from its character class, so sk-or-v1-... (OpenRouter, which is in this repo's .env.example and drives the memory pipeline) and sk-proj-... both pass straight through.

Worth being fair about severity on all of that: none of the current debug.log call sites carry a secret. They log a filesystem path, ticket title/type/domain, a model name, developmentOptions, and two booleans. So today these are defence-in-depth gaps, not live leaks. They become real the moment the logger is wired into LLM prompt/response logging, which your scope note says is next, and that is exactly why I would rather fix them now than after.

On scope: "Closes #51" will auto-close the issue while most of its "what to log" list is still unimplemented. Your scope note is clear about that, so this is just a suggestion to change it to "Refs #51" and leave the ticket open for the wiring.

Comment thread README.md
it is safe to share. Normal stdout output is unchanged.

```bash
npm run full tickets/my-ticket.md --verbose

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

issue (blocking): this is the only documented way to use the feature, and it does not enable verbose mode. --verbose collides with npm's own flag, so npm consumes it and never forwards it to the script. I verified it with an argv probe in a worktree:

$ npm run <script> tickets/x.md --verbose
npm verbose cli /home/.../node .../npm     <- npm turned ITS OWN verbose logging on
npm verbose title npm run <script> tickets/x.md    <- flag stripped from the script's args

$ npm run <script> tickets/x.md -- --verbose
argv: ["tickets/x.md","--verbose"]           <- works, needs the -- separator

The irony is that it looks like it worked, because npm starts printing its own verbose output.

-v is worse. npm reads it as --version:

$ npm run <script> -v
11.12.1        <- printed npm's version, never ran the script

Suggest documenting npm run full <ticket> -- --verbose, and either dropping -v or noting it cannot work through npm run. parseCliArgs itself is fine; this is purely the invocation.

Comment thread README.md

### Verbose debug output

Pass `--verbose` (or `-v`) to the full workflow CLI to emit detailed debug

@sugat009 sugat009 Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

issue (non-blocking): same collision applies to the -v alias mentioned here. Also, "secrets are redacted" is unqualified: the redactor covers sk-ant- keys, some sk- keys, Authorization/Bearer headers, and api_key/token/secret/password pairs. It does not cover GitHub PATs, AWS keys, JWTs, or credentials embedded in connection strings, and it does not redact the log label at all (see the note on debug-logger.ts:106). Worth softening, since "safe to share" is the sort of line someone acts on.

Comment thread src/utils/debug-logger.ts
// Anthropic-style API keys: sk-ant-...
.replace(/sk-ant-[A-Za-z0-9_-]{6,}/g, 'sk-ant-***REDACTED***')
// Other `sk-` prefixed keys (e.g. OpenAI): sk-<long token>
.replace(/sk-[A-Za-z0-9]{16,}/g, 'sk-***REDACTED***')

@sugat009 sugat009 Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

issue (non-blocking): this rule is documented as covering "Other sk- prefixed keys (e.g. OpenAI)" but its character class omits - and _, unlike the sk-ant- rule directly above it which includes both. So the modern key shapes pass through untouched. Verified against the patterns directly:

sk-proj-AbC123def456GHI789jkl012MNO       -> unredacted (OpenAI, current format)
sk-or-v1-abc123def456ghi789jkl           -> unredacted (OpenRouter)
sk-ant-api03-AbC123_xy-ZZ99001122334455  -> redacted correctly

OpenRouter matters here specifically: OPENROUTER_API_KEY is in this repo's .env.example and drives the memory pipeline. Adding _- to the class fixes all three.

Comment thread src/utils/debug-logger.ts
.replace(/Bearer\s+[A-Z0-9._-]{8,}/gi, 'Bearer ***REDACTED***')
// key/value pairs: apiKey / api_key / token / secret / password
.replace(
/("?(?:api[_-]?key|apikey|token|secret|password)"?\s*[:=]\s*)("?)([^"\s,}]{4,})\2/gi,

@sugat009 sugat009 Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

issue (non-blocking): the \2 backreference makes this fail open on the values most likely to need redacting. Because group 2 captures the opening quote and must match again at the end, a quoted secret containing a space, comma or brace does not match at all, so it gets zero redaction rather than partial. An unquoted value stops at the first space. Suggest dropping the backreference and handling quoted and unquoted forms as separate alternatives.

Comment thread src/utils/debug-logger.ts
// Authorization headers: redact the whole credential (scheme + token), so
// both "Bearer <jwt>" and "Basic <base64>" forms are covered. Runs before
// the standalone Bearer rule so the credential is redacted exactly once.
.replace(/(authorization"?\s*[:=]\s*"?)[^"\n,}]{4,}/gi, '$1***REDACTED***')

@sugat009 sugat009 Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

issue (non-blocking): this rule (and the key-name rule below) operates on the raw string, but formatData feeds it JSON.stringify output, where a nested object serialised inside a string value has its quotes escaped as \". The "? in the pattern does not match \", so secrets one level deep in a stringified-JSON-inside-JSON payload are missed. Worth a test with a doubly-encoded payload, since LLM request/response objects often carry one.

Comment thread src/utils/debug-logger.ts
export function createDebugLogger(options: DebugLoggerOptions): DebugLogger {
if (!options.enabled) return NOOP_LOGGER;
const maxLength = options.maxLength ?? DEFAULT_MAX_LENGTH;
const sink = options.sink ?? ((line: string) => process.stderr.write(line));

@sugat009 sugat009 Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

issue (non-blocking): no test proves output goes to stderr. Every spec injects the sink option, so switching this default to process.stdout.write would leave the suite green, and stderr-routing is the module's first stated design goal. One test asserting the default sink writes to process.stderr would lock it in.

Comment thread src/utils/debug-logger.ts
const now = options.now ?? Date.now;

const log = (label: string, data?: unknown): void => {
sink(`[debug] ${label}${renderSuffix(data, maxLength)}\n`);

@sugat009 sugat009 Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

issue (non-blocking): the label is interpolated raw, so it is neither redacted nor truncated, while the module docstring says sensitive data is redacted "before it is ever written" and the README calls the output safe to share. A caller doing debug.log(request to ${url}) with a credentialed URL leaks it. Cheap fix: run the label through redactSensitive too.

Comment thread src/utils/debug-logger.ts
/** Truncate long text, appending a marker with the original length. */
export function truncate(text: string, maxLength: number = DEFAULT_MAX_LENGTH): string {
if (text.length <= maxLength) return text;
return `${text.slice(0, maxLength)}… (${text.length} chars total)`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nitpick: the (N chars total) marker reports the post-redaction length, since truncate runs after redactSensitive, so the number misstates the real payload size whenever anything was redacted. Minor, but the marker exists precisely to tell you what you lost.

Comment thread src/cli/full.ts
ticket,
developmentOptions
);
stopTimer();

@sugat009 sugat009 Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion (non-blocking): stopTimer() sits on the success path only, so a workflow that throws logs no duration, which is the run where timing matters most. try { ... } finally { stopTimer(); } would cover both.

Comment thread src/cli/full-args.ts
*/
export function parseCliArgs(argv: string[]): CliArgs {
const args = argv.slice(2);
const verbose = args.includes('--verbose') || args.includes('-v');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nitpick: args.find(a => !a.startsWith('-')) takes the first non-dash token as the ticket, so a near-miss spelling like --verbos is silently ignored (verbose just stays off), and any future flag taking a separate value would have its value swallowed as the ticket path. Fine for today's single boolean flag; worth a comment noting the assumption.

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

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

Add --verbose flag for debug output

2 participants