Skip to content

feat(config): move progress-stream toggle to sutando.config (env = override) - #2308

Merged
john-the-dev merged 9 commits into
mainfrom
feat/progress-stream-config
Aug 1, 2026
Merged

john-the-dev merged 9 commits into
mainfrom
feat/progress-stream-config

Conversation

@sonichi

@sonichi sonichi commented Jul 25, 2026

Copy link
Copy Markdown
Owner

What

The owner progress-streamer (the ⏳ working… liveness placeholder in the Discord + Telegram bridges) was gated only by the SUTANDO_PROGRESS_STREAM env var. That's fragile — it lives in ambient shell/.env and is silently dropped by any bridge relaunch that doesn't source it (a bare nohup restart is exactly how it went dark on this fleet). Per owner: "config should be in the right place… we shouldn't add random env var all over the place."

This moves the durable home into bridges.progress_stream in sutando.config.{local.,}json, keeping the env var as an override (documented CLI > env > config-file precedence).

How

  • sutando_config.py: register bridges as a known top-level key + add resolve_progress_stream() (returns None when unset so the caller keeps its own default).
  • progress_stream.stream_enabled(): env (if set → strict ==1) → config → default OFF.
  • sutando.config.json: document the tracked default (false).

Testing

  • python3 tests/progress-stream.test.py — green, incl. new config-fallback + env-override cases.
  • python3 tests/telegram-bridge-progress-stream.test.py — 10/10, unaffected (env path preserved).

Note

Proposed key was discord.progressStream; used bridges.progress_stream instead because the flag gates both bridges and the schema is snake_case (vault.remote_url, interval_seconds). Rename on request.

Local cutover (after merge + bridge restart on new code)

Set bridges.progress_stream: true in sutando.config.local.json, then drop the SUTANDO_PROGRESS_STREAM line from .env — so the toggle lives only in config. (Don't remove the .env line before the new code is deployed, or a pre-merge restart would lose the feature.)

🤖 Generated with Claude Code

…erride)

The owner progress-streamer (Discord + Telegram bridges) was gated ONLY by the
SUTANDO_PROGRESS_STREAM env var — fragile: it lives in ambient shell/.env and is
silently dropped by any bridge relaunch that doesn't source it (e.g. a bare
nohup restart, which is exactly how it went dark on this fleet). Move the
durable home into `bridges.progress_stream` in sutando.config.{local.,}json,
keeping the env var as an override (documented CLI > env > config precedence).

- sutando_config: register `bridges` top-level key + resolve_progress_stream()
  (returns None when unset so caller keeps its own default)
- progress_stream.stream_enabled(): env (if set) -> config -> default OFF
- sutando.config.json: document the tracked default (false)
- tests: config-fallback + env-override cases (progress-stream.test.py green;
  telegram-bridge-progress-stream.test.py 10/10 unaffected)

Note: proposed key was `discord.progressStream`; used `bridges.progress_stream`
instead because the flag gates BOTH bridges and the config schema is snake_case
(vault.remote_url, interval_seconds). Rename on request.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
@github-actions

github-actions Bot commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 71%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • src/progress_stream.py (93.3%): Missing lines 67
  • src/sutando_config.py (100%)

Summary

  • Total: 32 lines
  • Missing: 1 line
  • Coverage: 96%

src/progress_stream.py

Lines 63-71

  63     try:
  64         import sys
  65         src_dir = str(Path(__file__).resolve().parent)
  66         if src_dir not in sys.path:
! 67             sys.path.insert(0, src_dir)
  68         import sutando_config  # type: ignore[import-untyped]
  69         val = sutando_config.resolve_progress_stream()
  70         if val is not None:
  71             return val

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed current head b8f0226.

Two blockers before this can land:

[P1] The new tracked bridges config key is only registered in the Python loader. sutando.config.json now contains bridges.progress_stream, but docs/sutando-config.schema.json still has additionalProperties: false with no bridges property, and the TypeScript twin's KNOWN_TOP_LEVEL_KEYS in src/sutando_config.ts still omits bridges. That makes the checked-in default config schema-invalid and causes TS config consumers to warn that the repo's own bridges key is unread. Please update the schema plus the TS config twin/tests so the durable config contract stays consistent across loaders.

[P2] CI is correctly failing the diff-coverage gate: the new fallback branches in src/progress_stream.py are uncovered (sys.path.insert and the config-import exception fallback). Please cover those branches or simplify the fallback so the 95% diff coverage gate is green.

Focused validation:

  • git diff --check origin/main...HEAD
  • python3 tests/progress-stream.test.py
  • python3 tests/telegram-bridge-progress-stream.test.py

Reviewed by Qingyun's Personal Codex.

…cept path

The diff-coverage gate flagged the new config surface: the prior config-fallback
tests MOCKED sutando_config.resolve_progress_stream, so its real body (and the
except branch in stream_enabled) were uncovered. Add:
- direct resolve_progress_stream() cases over a real temp config (True/False/None),
- an except-branch case (config reader raising -> safe OFF).
All green; no behavior change.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed current head b670d74.

Thanks for the quick coverage follow-up. The new Python tests cover the stream_enabled() config fallback and config-read exception path, and the focused local checks pass.

The remaining blocker is still the cross-loader config contract: sutando.config.json now includes top-level bridges, but docs/sutando-config.schema.json still has additionalProperties: false without a bridges property, and src/sutando_config.ts still omits bridges from KNOWN_TOP_LEVEL_KEYS. A quick schema-key check on this head still reports ['bridges'] as present in the default config but absent from the schema. Please add the schema entry and update the TypeScript config twin/tests so the checked-in default config is valid and TS consumers do not warn about the repo's own key.

Focused validation:

  • git diff --check origin/main...HEAD
  • python3 tests/progress-stream.test.py
  • python3 tests/telegram-bridge-progress-stream.test.py
  • schema-key check: bridges is still missing from docs/sutando-config.schema.json

Reviewed by Qingyun's Personal Codex.

…ion test)

Addresses the remaining CHANGES_REQUESTED blocker on #2308.

`sutando.config.json` gained a top-level `bridges`, but neither twin knew about
it: `docs/sutando-config.schema.json` declares `additionalProperties: false`
without a `bridges` property (so the repo's own checked-in default config did
not validate against the repo's own schema), and `src/sutando_config.ts` omitted
it from KNOWN_TOP_LEVEL_KEYS (so every TS consumer warned "has top-level keys
the loader does not read: 'bridges'" on first config load). Both are registered
here.

Also adds tests/sutando-config-twin-contract.test.py, because this is the SECOND
PR blocked by the same drift — #2316 hit it with `health_check`, #2308 with
`bridges`. Twice is a missing guardrail, not coincidence: sutando_config.ts
documents itself as the Python twin but nothing enforced that, and the schema had
no consistency check at all. The test asserts every tracked config key is known to
the TS twin AND declared in the schema, and that the schema stays closed.

Writing it immediately surfaced a third, unreported instance in the other
direction: `migrate` was accepted by the TS loader but absent from the schema, so
any user who actually set it got a config that failed validation. Fixed in the
same contract rather than left as a known break — it is the same defect qingyun
asked to close, not new scope.

Verified:
- tests/sutando-config-twin-contract.test.py 5/5, and it genuinely catches drift:
  removing 'bridges' from the TS set again -> exit 1; restored -> exit 0
- tests/progress-stream.test.py, tests/telegram-bridge-progress-stream.test.py green
- default config key-check against the schema passes
- src/sutando_config.ts still imports under node --experimental-strip-types
- git diff --check clean

Stand: Echo Act IV Pro
@sonichi

sonichi commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Blocker addressed in 3eaa772e.

The two registrations you asked for:

  • docs/sutando-config.schema.json — added a bridges property. The schema declares additionalProperties: false, so without it the repo's own checked-in sutando.config.json did not validate against the repo's own schema.
  • src/sutando_config.ts — added bridges to KNOWN_TOP_LEVEL_KEYS, so TS consumers stop warning on first config load.

Plus a regression test, because this is the second PR blocked by the same drift. #2316 hit it with health_check; this one with bridges. sutando_config.ts documents itself as the Python twin but nothing enforced that, and the schema had no consistency check at all. tests/sutando-config-twin-contract.test.py now asserts every tracked config key is known to the TS twin and declared in the schema, and that the schema stays closed.

Writing that test surfaced a third instance, in the opposite direction: migrate was accepted by the TS loader but absent from the schema — so any user who actually set it got a config that failed validation. Fixed in the same commit. Flagging it explicitly since it wasn't in the review: it's the same cross-loader contract, not new scope, and leaving a known break to keep the diff pure seemed worse.

Verification:

  • tests/sutando-config-twin-contract.test.py — 5/5, and it genuinely catches drift: removing bridges from the TS set again → exit 1; restored → exit 0
  • tests/progress-stream.test.py, tests/telegram-bridge-progress-stream.test.py — green
  • default config key-check against the schema — passes
  • src/sutando_config.ts still imports under node --experimental-strip-types
  • git diff --check — clean

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed head 3eaa772e74b25c3ef724ff6ab9029439da335bbc.

The previous blockers are fixed. The durable bridges.progress_stream key is now represented in the Python loader, TypeScript config twin, checked-in default config, and JSON schema; the new config-contract test also catches future TS/schema drift. The progress-stream flag still preserves the expected precedence: non-empty SUTANDO_PROGRESS_STREAM wins, otherwise config is consulted, otherwise the feature defaults off.

Focused checks passed:

  • git diff --check origin/main...HEAD
  • python3 tests/progress-stream.test.py
  • CLAUDE_CONFIG_DIR=/private/tmp/claude-config-sutando-pr2308 SUTANDO_SUPPRESS_CCD_FALLBACK_BANNER=1 python3 tests/telegram-bridge-progress-stream.test.py
  • python3 tests/sutando-config-twin-contract.test.py

GitHub checks are green. Approved.

Reviewed by Qingyun's Personal Codex.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved exact head 3eaa772e74b25c3ef724ff6ab9029439da335bbc.

The prior cross-loader blocker is resolved: bridges.progress_stream is recognized by the Python loader, TypeScript twin, tracked default, and closed JSON schema. Env =0/=1 retains override precedence, unset falls through to config, unreadable config fails safely off, and the tracked default remains boolean false.

Independent validation passed on exact content plus a conflict-free current-main cumulative tree: progress-stream suite, Telegram progress integration (10/10), config twin/schema contract, Python compilation, diff hygiene, and REVIEW.md path gate. Worst-case disruption is unexpectedly enabling owner-channel placeholders or breaking existing env-controlled installs; default-off plus retained strict env precedence and bridge regressions mitigate both.

Code-ready. The branch is 102 main commits behind, so it is not merge-ready until a real refresh, fresh exact-head CI/CLA, and another qualifying current-head maintainer approval.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved refreshed exact head cdb5076e48dee79716bbc7721e6970233fb620e9.

This is a merge-only refresh with parents 3eaa772e plus current main 043a60bf; it is now 4 commits ahead and 0 behind. The progress-stream config contract remains intact across Python, TypeScript, the tracked default, and the closed JSON schema, with strict env override precedence and safe default-off behavior.

Fresh exact-head validation passed: progress-stream suite, Telegram progress integration (10/10), config twin/schema contract, Python compilation, diff hygiene, and REVIEW.md path gate. Worst-case unexpected placeholder activation remains mitigated by default-off, explicit boolean config, strict env override, and owner-tier bridge tests.

Code-ready. Do not merge until all refreshed-head CI/CLA completes green and a second qualifying maintainer records a formal approval on this refreshed head.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Changes requested on exact head cdb5076e48dee79716bbc7721e6970233fb620e9.

[P1] src/sutando_config.py:431 coerces every non-null bridges.progress_stream value with bool(val), so malformed-but-common disable values enable the feature. Exact-head temp-config repro:

{"progress_stream":"false"}             -> True
{"progress_stream":{"enabled":false}}   -> True

The loader explicitly does not enforce the JSON schema at runtime, so the schema's boolean declaration does not protect direct/local config edits. This violates the resolver's Optional[bool] contract and the PR's safe default-off guarantee: a user trying to disable placeholders can instead turn on owner-channel progress messages. Please accept only a real JSON boolean; treat any other type as unset/off (and ideally diagnose it), with regressions for string/object values.

The merge-only refresh itself is clean: the three topic commits are unchanged by range-diff; progress-stream, Telegram integration (10/10), and config-twin suites pass; Python compilation, diff hygiene, REVIEW.md path gate, current CI, and CLA pass. Worst-case disruption is unexpected owner-channel placeholder activation from an invalid local value. Not merge-ready: changes requested.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed refreshed exact head cdb5076e48dee79716bbc7721e6970233fb620e9.

No blockers found. This refresh keeps the progress-stream config contract intact across the Python resolver, TypeScript config twin, checked-in default config, and closed JSON schema. Env override precedence still wins over config, and unreadable config still falls back to the safe default-off behavior.

Focused checks passed locally:

  • python3 tests/progress-stream.test.py
  • CLAUDE_CONFIG_DIR=/private/tmp/claude-config-sutando-pr2308-cdb SUTANDO_SUPPRESS_CCD_FALLBACK_BANNER=1 python3 tests/telegram-bridge-progress-stream.test.py
  • python3 tests/sutando-config-twin-contract.test.py
  • PYTHONPYCACHEPREFIX=/private/tmp/ag2-pycache-2308-cdb python3 -m py_compile src/progress_stream.py src/sutando_config.py tests/progress-stream.test.py tests/sutando-config-twin-contract.test.py
  • git diff --check origin/main...HEAD
  • bash scripts/review-checks.sh --diff /private/tmp/pr2308-cdb5076.diff

GitHub checks are green on this head. Approved.

Reviewed by Qingyun's Personal Codex.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Changes requested on exact head cdb5076e48dee79716bbc7721e6970233fb620e9; this replaces my post-refresh approval after independently reproducing Qingyun's new current-head finding.

P1 blocker — src/sutando_config.py:431 violates the Optional[bool]/default-off contract by coercing arbitrary JSON values with bool(val). Exact-head real-loader results:

"false"             => True
{"enabled": false}  => True
1                   => True

The runtime loader is intentionally schema-lenient, so the JSON schema does not protect manually edited sutando.config.local.json. A user attempting to disable the feature with a common string or object shape instead enables owner-channel progress placeholders. Please return a value only when val is an actual JSON boolean (non-booleans should be unset/off, ideally with a diagnostic) and add regressions for string/object/numeric values.

The existing focused suite, Telegram integration 10/10, config-twin contract, Python compilation, diff hygiene, added-line path scan, CI, and CLA all pass, but the suite covers only valid booleans/unset and misses the activated malformed-config path. Worst-case disruption is unexpected owner-channel messages from a purportedly disabled opt-in feature. Not merge-ready: changes requested.

`resolve_progress_stream` coerced any non-null value with `bool(val)`, so the
two shapes users actually type when DISABLING a feature both enabled it:

    {"progress_stream": "false"}           -> True
    {"progress_stream": {"enabled": false}} -> True

The runtime loader is deliberately schema-lenient, so the JSON schema's
boolean declaration does not protect a hand-edited sutando.config.local.json
— the guard has to live in the resolver. Now only a real bool is honored;
anything else is treated as unset (caller default, which is OFF) and draws a
one-time stderr diagnostic, matching the module's existing warn convention.

Regressions cover the whole type axis (str/dict/list/int/float, truthy AND
falsy of each) rather than only the three reported values, since the property
is "non-bool => unset" and the original defect was a truthiness bug that a
falsy-only fixture would not have caught.

Verified: progress-stream suite 114 pass / 0 fail; reverting the resolver
produces 22 discriminating failures, so the regressions are not vacuous.
All 13 suites referencing sutando_config or progress_stream are green.

Reported independently by qingyun-wu and john-the-dev on #2308.

Stand: Echo Act IV Pro
@sonichi

sonichi commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

Fixed at 8be3fea0. You both reproduced the same defect independently and the repro was exact — thank you, that made this unambiguous.

Before, at cdb5076e, through the real loader (not a reimplementation):

true                 -> True
false                -> False
"false"              -> True   <-- enables a feature the user tried to disable
{"enabled": false}   -> True   <-- same
"no"                 -> True   <-- same
1                    -> True
0                    -> False
[]                   -> False

After, same probe:

true                 -> True   warned=False
false                -> False  warned=False
"false"              -> None   warned=True
{"enabled": false}   -> None   warned=True
"no"                 -> None   warned=True
1                    -> None   warned=True
0                    -> None   warned=True
[]                   -> None   warned=True
absent key           -> None   warned=False

Only a real JSON boolean is honored; everything else is treated as unset, so the caller's own default (OFF) applies. Non-booleans draw a one-time stderr diagnostic using the module's existing _..._WARN_PRINTED convention, reset via _reset_cache_for_tests(). isinstance(val, bool) correctly rejects 0/1 since bool is a subclass of int — noted inline so it doesn't look accidental.

I went slightly wider than the three values you named. The property is "non-bool ⇒ unset", so a fixture pinning only str/dict/int would stay green if a future branch special-cased lists or floats. The regressions cover str/dict/list/int/float, truthy and falsy of each — the falsy half matters because the original defect was a truthiness bug, and a falsy-only fixture ("", {}, [], 0) would have passed against the broken code and proven nothing.

The regressions are not vacuous — verified by reverting the resolver:

with the fix:      114 PASS   0 FAIL
resolver reverted:  58 PASS  22 FAIL   <- all 22 are the new cases
restored:          114 PASS   0 FAIL

Full affected surface, uncapped — enumerated first (13 suites reference sutando_config or progress_stream), then all 13 run: 13/13 green, including sutando-config-twin-contract.test.py. git diff --check clean; scripts/review-checks.sh PASS (hardcoded-paths clean).

The branch is still behind current main. I'd rather refresh it once you're satisfied with this change than rebase now and invalidate the head you're reviewing.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved exact head 8be3fea; this replaces my stale cdb5076 change request.

The P1 truthiness blocker is resolved. Independent real-loader reproduction returns None plus a diagnostic for string "false", object {"enabled": false}, and integer 1; real False/True remain False/True without warnings. The parent cdb5076 reproduces all three malformed values as True. The full type-axis regression covers truthy and falsy strings, objects, lists, integers, and float input.

Reviewer-run exact-head checks pass: progress-stream suite, Telegram integration 10/10, Python/TypeScript/schema config-twin contract, Python compilation, diff hygiene, and REVIEW.md hardcoded-path scan. The activated path preserves strict env override, explicit config booleans, safe config-read failure, owner-only streaming, and default OFF.

Worst-case disruption is malformed local config unexpectedly enabling owner-channel progress placeholders; real-boolean validation plus safe-off fallback and a one-time diagnostic mitigate it.

Code-ready, but not merge-ready: the branch is BEHIND, clean-install and diff coverage are still pending, and it needs a second qualifying current-head maintainer approval after refresh/fresh gates.

The diff-coverage gate failed my previous commit at 93% (need 95%): every
case in the new type-axis loop calls `_reset_cache_for_tests()`, so the
already-warned early return in `_warn_progress_stream_type` never executed.

Rather than pad coverage, this asserts the behaviour that line implements:
the resolver is called per bridge message, so a per-call warning would spam
the log for as long as the bad value sits in config. Three consecutive reads
with no reset must produce exactly one diagnostic.

Measured, not argued: sutando_config.py line 245 goes hits=0 -> hits=1, and
diff-cover over the same 32-line diff CI used goes 93% -> 96%, with
src/sutando_config.py at 100%.

Suite 116 pass / 0 fail. The one line still uncovered is
src/progress_stream.py:67 (`sys.path.insert` when src is not already on the
path) — it predates this commit and cannot execute under a loader that has
already imported from src/.

Stand: Echo Act IV Pro
@sonichi

sonichi commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

Correction to my previous comment: I reported the suites green without checking the PR's own CI, and the diff-coverage gate failed at 93% on 8be3fea0. My fault — local suite runs don't exercise that gate. Fixed at 4c0a2328.

The uncovered line was src/sutando_config.py:245, the already-warned early return in _warn_progress_stream_type. Every case in my type-axis loop calls _reset_cache_for_tests(), so that branch never ran.

I didn't pad coverage to clear it — the line implements real behaviour worth pinning. resolve_progress_stream is called per bridge message, so a per-call warning would spam the log for as long as the bad value sits in config. The new check does three consecutive bad reads with no reset and asserts exactly one diagnostic.

Measured rather than argued:

sutando_config.py:245   hits=0  ->  hits=1
diff-cover (same 32-line diff CI used)   93%  ->  96%
src/sutando_config.py                    94.1% -> 100%
suite                                    116 pass / 0 fail

One line remains uncovered: src/progress_stream.py:67 — the sys.path.insert(0, src_dir) inside the defensive lazy-import. It predates this commit and cannot execute under a loader that has already imported from src/, which is every path the suite can construct. At 1 missing of 32 the gate is satisfied at 96%, so I've left it rather than contort a test around it. Say the word if you'd rather it were covered or the fallback simplified — that was your original [P2] on this PR, so I'd rather you decide than assume.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed exact head 4c0a2328 after the coverage fix.

The delta is test-only and exercises the real behavior that was previously uncovered: three consecutive malformed config reads without a cache reset emit the diagnostic exactly once. Reviewer-run exact-head tests/progress-stream.test.py, Telegram progress integration (10/10), the cross-loader config contract, Python compilation, cumulative diff hygiene, and the REVIEW.md hardcoded-path scan all pass. GitHub diff coverage is now green at this head. Worst-case user disruption remains bounded and improved: malformed values fail closed to the caller default (OFF), and the one-time guard prevents recurring bridge-log spam.

APPROVE for the code at 4c0a2328. Not merge-ready yet: the branch is BEHIND, clean-install is still running, and it requires a second qualifying current-head maintainer approval after refresh/fresh gates.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed exact head ad6ef34f6d7c24195c52306c1567ab6acaa34bd4 and the full config/progress-stream path.

The prior truthiness blocker is resolved: only real JSON booleans enable/disable the stream, malformed values fail closed with the one-time diagnostic, and Python/TypeScript/schema/default twins remain aligned. Reviewer-run tests/progress-stream.test.py passes, the cumulative path scan passes, and exact-head CI is green. The final commit is a current-main merge with no new PR-owned behavior.

Approved on this head. The branch is now behind again, so merge still requires a refresh and fresh exact-head gates/approval as branch protection dictates.

… and stand

main added `stand` (per-clone Stand: trailer value) to _KNOWN_TOP_LEVEL_KEYS
while this branch added `bridges` (read by resolve_progress_stream at
sutando_config.py:443). Purely additive — the set only decides which top-level
keys are recognized, so dropping either resurrects a spurious "unknown key"
warning for anyone using it.

Verified by behaviour, with a control that proves the check can fire:
  - bogus key                        -> WARNS
  - bridges + stand together         -> silent
  - resolve_progress_stream()        -> True  (this PR's feature still works)

tests/sutando-config-tsx-resolution.test.sh fails here; it also fails at
origin/main with this change absent — pre-existing and environment-dependent.

Stand: Echo Act IV Mini

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed refreshed head 1284924.

No blocking findings in this PR's progress-stream config surface. The branch now keeps bridges aligned across the Python loader, TypeScript known-key set, and strict JSON schema, and the added contract test covers the checked-in config/schema/TS path that previously drifted. Focused checks passed: git diff --check, tests/progress-stream.test.py, tests/telegram-bridge-progress-stream.test.py, and tests/sutando-config-twin-contract.test.py.

I also noticed the pre-existing stand top-level key is Python-only on current main-derived heads; #2486 addresses that separate drift. It is not introduced by this PR, so I am not holding this progress-stream fix on it.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed exact head 128492488a590c30e067c0b566a4e59e4b49e531.

No blocking findings. The merge refresh preserves both the PR's bridges key and main's stand key in the Python loader, while the progress-stream resolver still accepts only real JSON booleans and keeps invalid values safely OFF with a one-time diagnostic. That bounds the worst case: a malformed local config cannot silently enable owner-channel progress messages.

Reviewer-run verification passed:

  • python3 tests/progress-stream.test.py
  • python3 tests/sutando-config-twin-contract.test.py
  • git diff --check
  • the REVIEW.md hardcoded-path gate

All exact-head CI and CLA checks are green. Code-ready and approved from this review lane; refresh the behind branch and satisfy the repository's full approval gate before merge.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed exact refreshed head c0884475af99a1d97050d11a7a7086fd71014404 against current main badd2a64.

The refreshed merge preserves the exact seven-file topic patch (54e51ce8 stable patch ID). Current main still makes progress streaming env-only; this head adds the durable bridges.progress_stream config home while preserving a non-empty SUTANDO_PROGRESS_STREAM environment override and default OFF.

Reviewer-run evidence: tests/progress-stream.test.py passes the complete env/config precedence, malformed-type, one-time diagnostic, owner-tier, placeholder, outage, and real Discord bridge-wiring controls; tests/sutando-config-twin-contract.test.py passes Python/TypeScript/schema key parity; Python compilation, diff hygiene, and REVIEW.md hardcoded-path scan pass. I traced both Discord and Telegram call sites through the shared stream_enabled() resolver.

Worst-case disruption is unwanted owner-channel progress messages. Mitigations are explicit: tracked default remains false, missing or unreadable config fails OFF, non-boolean values are rejected with a one-time diagnostic instead of truthy coercion, SUTANDO_PROGRESS_STREAM=0 remains an immediate kill switch, and team/other tiers remain excluded. There is no state migration or destructive action.

Ready to merge once this exact head has every required refreshed CI/coverage/CLA gate green and GitHub reports CLEAN/MERGEABLE.

@john-the-dev
john-the-dev merged commit 4c0c340 into main Aug 1, 2026
18 checks passed
@john-the-dev
john-the-dev deleted the feat/progress-stream-config branch August 1, 2026 22:56
sonichi added a commit that referenced this pull request Aug 1, 2026
Both conflicts were additive, not opposed: this branch adds the `stand`
top-level config key; `main` added `bridges` (#2308) in the same two places.
Resolved by keeping BOTH on each side.

  src/sutando_config.ts            KNOWN_TOP_LEVEL_KEYS gains `bridges` + `stand`
  docs/sutando-config.schema.json  rebuilt from main`s JSON with `stand` appended,
                                   so the file is valid by construction rather
                                   than hand-patched around the markers

Verified the property this PR exists to establish still holds after the merge --
all three surfaces agree on the same 8 keys:

  python  bridges claude_sutando_config_dir core core_config_dirs migrate stand vault workspace
  ts      same 8
  schema  same 8

  tests/sutando-config-twin-contract.test.py  PASS
  tests/sutando-config.test.py                PASS

Stand: Echo Act IV Mini
john-the-dev added a commit that referenced this pull request Aug 3, 2026
…it warns + fails schema (#2486)

* fix(config): `stand` was known to the Python loader only, so setting it warns + fails schema

#2484 added `stand` to src/sutando_config.py's _KNOWN_TOP_LEVEL_KEYS without the
TS twin or the JSON schema. `stand` is per-clone identity by design, so it lives
in sutando.config.local.json — meaning anyone who actually uses what #2484
shipped hits both gaps.

Before/after, measured on the two commits:

  PARENT (origin/main)  TS knows stand=False  schema declares stand=False
                        -> every TS service warns "top-level keys the loader
                           does not read: 'stand'", and the config fails
                           validation because the schema is additionalProperties:false
  HEAD (this fix)       TS knows stand=True   schema declares stand=True
                        -> clean

This is the same drift class the repo has now hit three times (`health_check`
#2316, `bridges` #2308, `migrate` caught 2026-07-30). The structural guard for
it is the cross-loader contract test proposed in #2308; that test is scoped to
keys present in the TRACKED sutando.config.json, so it would NOT have caught
`stand` — a per-clone key never appears there. Noting the gap rather than
widening this fix: closing it is #2308's concern, not this one's.

Scope note: docs/sutando-config.schema.json is also missing `migrate`,
`bridges`, and `health_check`. Those are pre-existing and belong to their own
PRs (#2308 covers `bridges`); this change is limited to the regression #2484
introduced.

Not run locally: repo typecheck (no node_modules in this worktree) — CI covers it.

Stand: Echo Act IV Mini

* fix(config): `migrate` had the same defect as `stand` — close the axis, not the instance

Sutando-Pro's review of this PR: `migrate` is real (`sutando_config.py:59`,
`sutando_config.ts:46`) but absent from the schema, which is
`additionalProperties: false`. Setting it hard-fails validation for anyone
whose editor honours the schema, while both loaders accept it — the exact
asymmetry this branch is named for. Fixing only `stand` left the axis open.

`migrate` is NOT a dead key. I checked before writing the schema entry, because
a wrong shape is worse than a missing one:

  scripts/sutando-config.sh:118  load_config().get('migrate', {}).get('stale_hosts', [])
  scripts/sutando-config.sh:131  load_config().get('migrate', {}).get('skip_skills', [])
  scripts/sync-workspace.sh:1285,1352  consume both via those subcommands

So the entry describes the shape the consumers actually read: two string arrays,
defaulting empty, per-clone (sutando.config.local.json), config not secret.

The second half is the part that stops this recurring. Nothing asserted the three
declarations agree, which is why #2484 shipped two-of-three and why `migrate` was
still adrift after this PR fixed `stand`. `tests/config-known-keys-parity.test.py`
asserts python == ts == schema, and CI's `find tests -name '*.test.py'` picks it up
with no wiring.

Evidence, run at this branch's prior head e46aa53 and at HEAD:

  BEFORE  python(7) ts(7) schema(6)
          FAIL — MISSING from the schema: ['migrate']  exit=1
  AFTER   python(7) ts(7) schema(7)
          PASS — all three declarations agree.        exit=0

The test fails loudly if it cannot locate a declaration rather than returning an
empty set — two empty sets compare equal, and the check would pass while measuring
nothing.

Co-authored-by: Sutando-Pro <4250911+sonichi@users.noreply.github.com>
Stand: Echo Act IV Mini

* test(config): add an extraction floor so the parity guard cannot go vacuous

Follow-up to the parity test, answering qingyun-wu's "preferably close the class
with extraction floors" on this PR.

Equality across three extractors is non-vacuous for the drift case, but it has one
blind spot it cannot cover on its own: if every extractor narrowed the SAME way,
three shrunken sets still compare equal and the test passes while measuring almost
nothing. MIN_KEYS is a floor (5, against a real count of 7) rather than an exact
count, so adding a config key never breaks it and only catastrophic narrowing fires.

Negative controls, run at this head — each must fail, and does:

  drift (schema loses `migrate`)      exit=1  MISSING from the schema: ['migrate']
  extraction narrows (ts -> 2 keys)   exit=1  below the MIN_KEYS=5 extraction floor
  declaration renamed (not found)     exit=1  could not locate KNOWN_TOP_LEVEL_KEYS
  restored, re-run clean              exit=0  PASS

The third case is why the extractor raises instead of returning an empty set: an
empty set compares equal to another empty set, so a silent parse failure would have
read as agreement.

Stand: Echo Act IV Mini

* docs(config): restore the four escaped-unicode descriptions this PR rewrote

Review blocker on 9ca86d7: alongside the `stand` key, this PR rewrote four
unrelated `description` strings in docs/sutando-config.schema.json from escaped
`—` / `⏳` / `…` to literal characters. Semantically equivalent
JSON, but it expands a single-concern config-key fix and adds conflict surface
in a central schema.

Reverts exactly those four lines. Verified the change is presentation-only:
`json.load` of this tree and of 9ca86d7 compare equal.

Contribution against the merge base (654e4fe) is now what the PR body claimed:

  docs/sutando-config.schema.json        |   4 ++
  src/sutando_config.ts                  |   1 +
  tests/config-known-keys-parity.test.py | 111 +++++++++
  3 files changed, 116 insertions(+)

config-known-keys-parity and sutando-config-twin-contract both pass (8/8/8 with
`stand` declared); schema parses; `git diff --check` and the REVIEW.md path scan
are clean.

Stand: Echo Act IV Pro

---------

Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
john-the-dev added a commit that referenced this pull request Aug 26, 2026
…type (#3405)

* fix(config): the two loaders disagree on a config block of the wrong type

`load_config` validates that the top level is a JSON object, then every
consumer assumes each *block* is one too. Nothing checks that, and the two
twins fail differently on the same file:

    sutando.config.local.json:  {"workspace": "/tmp/ws"}

Python  -> AttributeError: 'str' object has no attribute 'get'
TS      -> resolveWorkspace() = <repo>/workspace

The Python side dies inside `(cfg.get("workspace") or {}).get("path")` — `or
{}` cannot help, the string is truthy — and it dies at *module import* of any
consumer, so `health-check.py` and the core exit on a traceback that never
names the config file. The TS side reads `?.path` off the string, gets
undefined, and silently falls through to the baked-in default. It discards the
valid `workspace.path` from `sutando.config.json` as well, because deep-merge
replaced the object with the string. So the Node services and the Python core
end up in different workspaces with no error anywhere.

Both loaders now reject it where the top-level check already lives, naming the
file, the key, the type found, and the expected shape:

    sutando config: <path>/sutando.config.local.json key 'workspace' must be a
    JSON object, got str '/tmp/ws'. Did you mean {"workspace": {...}}?

The guarded set is exactly the schema's `"type": "object"` properties, so this
enforces at read time what `docs/sutando-config.schema.json` already declares;
`stand` (string) and `core_config_dirs` (array) are untouched.

The twin-contract test gains the parity check that keeps the two sets and the
schema from drifting — the same guardrail #2316 and #2308 established for
KNOWN_TOP_LEVEL_KEYS, which drifted twice before it was made structural.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(config): treat a null top-level block as absent, not as a type error

qingyun-wu measured the gap: {"vault": null} loads on BOTH loaders today, and
every consumer already spells absence that way — `(cfg.get('vault') or {})` in
six places, an isinstance guard in workspace_layout.py. The guard this PR added
turned that working shape into a fatal error in _load_json / loadJsonFile, i.e.
at startup for every service.

That is not the broken-quietly -> broken-loudly trade the rest of the PR makes.
For scalars and arrays the Python core is already dead at import, so failing
loudly is strictly better. A null block is not broken today, and the population
carrying one lives in gitignored per-clone sutando.config.local.json files that
no in-repo grep can see (REVIEW.md lesson 5).

Both twins now skip null and keep rejecting scalars and arrays. Tests drive the
loader across null / scalar / array / object; reverting just the null arm fails
the null case and leaves the other 46 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
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.

3 participants