Repository navigation
feat(config): move progress-stream toggle to sutando.config (env = override) - #2308
Conversation
…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
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 71%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
src/progress_stream.pyLines 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
left a comment
There was a problem hiding this comment.
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...HEADpython3 tests/progress-stream.test.pypython3 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
left a comment
There was a problem hiding this comment.
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...HEADpython3 tests/progress-stream.test.pypython3 tests/telegram-bridge-progress-stream.test.py- schema-key check:
bridgesis still missing fromdocs/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
|
Blocker addressed in The two registrations you asked for:
Plus a regression test, because this is the second PR blocked by the same drift. #2316 hit it with Writing that test surfaced a third instance, in the opposite direction: Verification:
|
qingyun-wu
left a comment
There was a problem hiding this comment.
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...HEADpython3 tests/progress-stream.test.pyCLAUDE_CONFIG_DIR=/private/tmp/claude-config-sutando-pr2308 SUTANDO_SUPPRESS_CCD_FALLBACK_BANNER=1 python3 tests/telegram-bridge-progress-stream.test.pypython3 tests/sutando-config-twin-contract.test.py
GitHub checks are green. Approved.
Reviewed by Qingyun's Personal Codex.
john-the-dev
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.pyCLAUDE_CONFIG_DIR=/private/tmp/claude-config-sutando-pr2308-cdb SUTANDO_SUPPRESS_CCD_FALLBACK_BANNER=1 python3 tests/telegram-bridge-progress-stream.test.pypython3 tests/sutando-config-twin-contract.test.pyPYTHONPYCACHEPREFIX=/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.pygit diff --check origin/main...HEADbash 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
left a comment
There was a problem hiding this comment.
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
|
Fixed at Before, at After, same probe: 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 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 ( The regressions are not vacuous — verified by reverting the resolver: Full affected surface, uncapped — enumerated first (13 suites reference 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
left a comment
There was a problem hiding this comment.
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
|
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 The uncovered line was I didn't pad coverage to clear it — the line implements real behaviour worth pinning. Measured rather than argued: One line remains uncovered: |
john-the-dev
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.pypython3 tests/sutando-config-twin-contract.test.pygit 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.
|
@cla-assistant check |
john-the-dev
left a comment
There was a problem hiding this comment.
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.
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
…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>
…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>
What
The owner progress-streamer (the
⏳ working…liveness placeholder in the Discord + Telegram bridges) was gated only by theSUTANDO_PROGRESS_STREAMenv var. That's fragile — it lives in ambient shell/.envand is silently dropped by any bridge relaunch that doesn't source it (a barenohuprestart 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_streaminsutando.config.{local.,}json, keeping the env var as an override (documentedCLI > env > config-fileprecedence).How
sutando_config.py: registerbridgesas a known top-level key + addresolve_progress_stream()(returnsNonewhen 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; usedbridges.progress_streaminstead 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: trueinsutando.config.local.json, then drop theSUTANDO_PROGRESS_STREAMline from.env— so the toggle lives only in config. (Don't remove the.envline before the new code is deployed, or a pre-merge restart would lose the feature.)🤖 Generated with Claude Code