ai-consultants 2.17.2 → 2.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +32 -1
- package/README.md +6 -3
- package/SKILL.md +6 -3
- package/docs/COST_RATES.md +1 -1
- package/docs/cost_rates.json +1 -1
- package/docs/releases/v2.18.0.md +63 -0
- package/docs/releases/v2.19.0.md +66 -0
- package/docs/releases/v2.19.1.md +44 -0
- package/docs/releases/v2.19.2.md +54 -0
- package/package.json +1 -1
- package/scripts/config.sh +30 -1
- package/scripts/consult_all.sh +104 -4
- package/scripts/doctor.sh +71 -0
- package/scripts/lib/common.sh +79 -3
- package/scripts/lib/costs.sh +66 -9
- package/scripts/test_suite.sh +38 -0
package/CLAUDE.md
CHANGED
|
@@ -6,7 +6,7 @@ AI Consultants is a multi-model AI deliberation system that queries up to 15 AI
|
|
|
6
6
|
|
|
7
7
|
**Self-Exclusion**: The invoking agent is automatically excluded from the panel to prevent self-consultation. Claude Code won't query Claude, Codex CLI won't query Codex, etc.
|
|
8
8
|
|
|
9
|
-
**Version**: 2.
|
|
9
|
+
**Version**: 2.19.2
|
|
10
10
|
|
|
11
11
|
## Distribution
|
|
12
12
|
|
|
@@ -826,6 +826,37 @@ curl -fsSL https://raw.githubusercontent.com/matteoscurati/ai-consultants/main/s
|
|
|
826
826
|
|
|
827
827
|
## Changelog
|
|
828
828
|
|
|
829
|
+
### v2.19.2
|
|
830
|
+
- **Cost tracking silently lost on a fresh install — fixed.** `lib/costs.sh::track_session_cost` wrote to `$COST_TRACKING_FILE` (`$XDG_DATA_HOME/ai-consultants/costs.json`, i.e. `~/.local/share/...`) without ever creating the parent directory. That dir doesn't exist until something makes it, and `costs.json` is the only artifact stored there — so on a fresh install every session's cost write failed with "No such file or directory" (swallowed under `set -e` in the caller) and cumulative cost tracking never accumulated. Fix: `mkdir -p "$(dirname "$COST_TRACKING_FILE")"` up front; on failure, `log_warn` + `return 0`.
|
|
831
|
+
- **Function split for lock discipline**: `track_session_cost` (outer) now owns dir creation + locking and delegates the read-modify-write to a new inner `_track_session_cost_update`, which runs with the lock held and never propagates failure. Rationale documented inline: the caller in `consult_all.sh` runs under `set -e` *after* every consultant has already been queried and billed, so a bookkeeping failure must degrade to a warning, never abort the run.
|
|
832
|
+
- **Concurrency**: the RMW of `costs.json` was unguarded, so parallel consultations could interleave and lose records or fail the `mv`. Added a portable `mkdir`-based lock (flock is unavailable on macOS) with a bounded wait (50 × 0.1s = 5s); if the lock stays busy it proceeds unlocked with a warning rather than blocking or aborting. Writes now go through `mktemp "${COST_TRACKING_FILE}.XXXXXX"` instead of a fixed `.tmp` sibling, so an unlocked concurrent writer can't clobber another run's temp file (lost record / failed `mv`).
|
|
833
|
+
- **Corrupt-file self-heal**: a corrupt `costs.json` (truncated write, interleaved update) previously failed every future `jq` update forever and never recovered. Now `jq empty` gates the file; on failure it's moved aside to `${COST_TRACKING_FILE}.corrupt` and reset, so tracking recovers on the next session.
|
|
834
|
+
- **Tests**: `scripts/test_suite.sh::test_cost_tracking_resilience` (5 assertions, wired into `main()`) — costs.json created under a missing nested parent dir (fresh-install path), second session accumulates on the existing file (0.25 → 0.75, both exact in binary float), a corrupt costs.json is reset with the new session recorded, and the corrupt original is preserved as `.corrupt`. Calls guarded with `|| true` so a regression surfaces as a FAIL assertion, not a `set -e` suite abort. 8 suites pass; the new assertions are green; shellcheck clean.
|
|
835
|
+
- **No behavioral change for existing installs** whose data dir already exists; the fix only affects the first-run/absent-dir, concurrent, and corrupt-file paths. Cost bookkeeping remains best-effort by design.
|
|
836
|
+
|
|
837
|
+
### v2.19.1
|
|
838
|
+
- Cleanup from `/code-review max` on v2.19.0 (the workflow ran degraded under rate limits but surfaced these once it completed; all confirmed inline):
|
|
839
|
+
- **Report-table mangling fixed**: a failure reason containing a `|` (e.g. a CLI error mentioning a piped command) broke the "Diagnosed Failures" markdown row. The new `lib/common.sh::render_diagnosed_failure <entry> [console|table]` escapes `|`→`\|` in table mode. (Not a security injection — the text is the user's own CLI stderr — but a real rendering bug.)
|
|
840
|
+
- **DRY**: that helper is now the single source of the `name|reason` decode; the console log (quorum FAILED branch) and the report table both call it, so a future delimiter/encoding change touches one place instead of two.
|
|
841
|
+
- **Health gate is cache-aware**: it no longer pings a consultant whose response is already cached (Round 1 would serve it free via `check_cache`) — it keeps the cached consultant without a billed probe. Avoids the opt-in gate defeating the semantic cache.
|
|
842
|
+
- **`ping_consultant` takes the already-lowercased id** (callers compute it for the out/err paths anyway), dropping a redundant `to_lower` fork per consultant on both call sites (`doctor --live`, health gate).
|
|
843
|
+
- **Documented** the health gate's inherent pre-Round-1 startup latency (serial with the run by definition; up to `HEALTH_GATE_TIMEOUT`) in `config.sh` — it's the cost of pruning up front; opt-in + tunable.
|
|
844
|
+
- Tests: `test_render_diagnosed_failure` (4 assertions incl. pipe-escaping); `test_ping_consultant` updated for the lowercased-id signature. 8 suites pass; shellcheck clean. Smoke-verified: report table renders correctly, health gate still prunes + the min-2 guard fires on the pruned count.
|
|
845
|
+
|
|
846
|
+
### v2.19.0
|
|
847
|
+
- **Quorum grading + "Diagnosed Failures" report** (the #1 pick from the `/workflows` CLI-reliability investigation — chosen for zero transport/billing risk over the CLI→API fallback, which the workflow's adversarial critique showed would emit persona-less confidence-5 stubs that poison voting). After the round-1 collect loop, `grade_quorum <success> <attempted> <min>` (new pure helper in `lib/common.sh`) classifies the run **MET / DEGRADED / FAILED** vs `QUORUM_MIN` (default 2). The report gains an `**Outcome**:` banner and a **## Diagnosed Failures** table that surfaces each dropped consultant's reason (the v2.18.0 `.err` capture) — so a panel that silently shrank to 2/7 is visibly DEGRADED instead of presenting as authoritative. `QUORUM_ACTION=stop` aborts below quorum; default `warn` continues with the banner. Failures are collected in `_surface_consultant_error` (DRY — same call that already logs the reason).
|
|
848
|
+
- **Health gate (`ENABLE_HEALTH_GATE`, opt-in)** — before the run, ping every selected consultant **in parallel** (new `lib/common.sh::ping_consultant`, `HEALTH_GATE_TIMEOUT` default 30s) and drop the non-responsive ones, so installed-but-unauthenticated CLIs (Cursor/Kimi/stale installs) are pruned up front and the quorum/budget checks see the genuinely-working panel. Script-less custom API agents (no `query_*.sh`) return code 2 and are kept (not probeable). Opt-in because it costs one extra tiny query per consultant; it prunes, it does not switch transport.
|
|
849
|
+
- **DRY**: `doctor --live` refactored to use the shared `ping_consultant`; both surfaces now share one probe implementation.
|
|
850
|
+
- **Explicitly NOT done** (per the workflow synthesis): CLI→API automatic fallback (persona-loss vote poisoning; Gemini already auto-resolves), cold-start retry-timeout escalation (stretches the whole round's worst case), warm-up calls, and the transport-abstraction circuit breaker (racy on-disk state, highest blast radius).
|
|
851
|
+
- **Tests**: `test_functions.sh` — `test_grade_quorum` (5 assertions: MET/DEGRADED/FAILED + boundaries) and `test_ping_consultant` (3: valid→0, no-output→fail, missing-script→2, via stub query scripts). 8 suites pass; shellcheck clean. End-to-end smoke verified: a forced CLI failure yields FAILED outcome + Diagnosed Failures table with the real reason, and the health gate prunes the dead consultant pre-run.
|
|
852
|
+
|
|
853
|
+
### v2.18.0
|
|
854
|
+
- **Failures are now diagnosable, not silent (Fix A).** `consult_all.sh` launched every consultant with `> /dev/null 2>&1` — discarding stderr — so a failed consultant produced only a bare `Failed`/`Empty response`, with no way to tell *not installed* vs *not authenticated* vs *transient* (this is exactly what led a real session to mis-attribute failures to a timeout). Now each consultant's stderr is captured to `$OUTPUT_DIR/<consultant>.err`, and on FAILED/EMPTY the run surfaces a one-line reason via the new `lib/common.sh::get_consultant_error_reason` helper (ANSI-stripped, prefers an explicit error line, drops orchestration status noise, falls back to "no error captured — CLI likely missing or not authenticated").
|
|
855
|
+
- **`run_query` now embeds the CLI's real error in its failure log.** The per-attempt CLI stderr (`${output_file}.err`, an unpredictable mktemp path the orchestrator can't locate) was only `log_debug`'d. Its first line is now appended to the `Error (code: N)` and `All N attempts failed` warnings, so the actual reason (e.g. `401 Unauthorized`, `command not found`) reaches the captured stderr and the surfaced message.
|
|
856
|
+
- **`doctor.sh --live` (Fix B): real ping per consultant.** The static checks only verify a CLI is installed (`--version`), so `doctor` reports a consultant healthy even when it errors at query time (unauthenticated) — a real source of "All systems healthy" while 3 consultants silently fail. `--live` sends a minimal real query to each *enabled* (and not self-excluded) consultant with a short timeout (`DOCTOR_LIVE_TIMEOUT`, default 45s), and reports ✓/✗ with the captured reason; failures become `doctor` issues (exit 1). Opt-in (costs one tiny query each). Standalone short-circuit mode like `--suggest-preset`.
|
|
857
|
+
- **Tests**: `test_functions.sh::test_get_consultant_error_reason` (5 assertions: explicit-error line, embedded-auth reason, orchestration-noise-not-mistaken, empty/missing file). 8 suites pass; shellcheck clean (CI invocation).
|
|
858
|
+
- **Not fixable in code (documented for users)**: the underlying CLI auth/install state, and cold-start/warm-up retry effects, are environment issues — `--live` *surfaces* them but can't authenticate a CLI for you.
|
|
859
|
+
|
|
829
860
|
### v2.17.2
|
|
830
861
|
- **SECURITY: revert v2.17.1's `allow_absolute=true` for context files — it opened a secret-exfiltration surface (caught in `/code-review max`).** v2.17.1 fixed the macOS `/private/tmp` drop by letting `build_context.sh` accept *any* absolute context path behind `validate_file_path`'s prefix-only blocklist (`/etc /root /var/log /proc /sys /dev`). That blocklist covers no home secrets, so a context arg of `~/.ssh/id_rsa`, `~/.aws/credentials`, `~/.netrc`, or `~/.config/gh/hosts.yml` was read verbatim and **sent to the external AI providers**. It also matched literal non-canonical prefixes, so `/private/etc/master.passwd` (the real `/etc` on macOS, via the same symlink aliasing) bypassed the `/etc` rule.
|
|
831
862
|
- **Fix (correct altitude — allowlist, not blocklist)**: context files are now accepted only when (a) relative and in-tree (`validate_file_path ... false` — rejects absolute and `..`), or (b) under a recognized temp root via the new `_is_temp_path` helper (`/tmp`, `/private/tmp`, `$TMPDIR`, and the `/private`-prefixed macOS alias of `$TMPDIR`; `..` rejected so a temp prefix can't traverse out). Still fixes the original macOS scratch-file regression (Claude Code writes `/private/tmp/...`) **without** widening to the whole filesystem. Verified: `/tmp`, `/private/tmp`, `$TMPDIR/...`, relative in-tree → accepted; `~/.ssh/id_rsa`, `/etc/passwd`, `/private/etc/master.passwd`, `/tmp/../etc/passwd` → rejected.
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# AI Consultants v2.
|
|
1
|
+
# AI Consultants v2.19.2
|
|
2
2
|
|
|
3
3
|
> **A harness for every question.** A panel of up to 15 frontier models that writes its own playbook per question — fan out, debate to convergence, cross-examine under adversarial review, or run a tournament — and checks its work before it reaches you.
|
|
4
4
|
|
|
5
|
-
[](https://github.com/matteoscurati/ai-consultants)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://docs.anthropic.com/en/docs/claude-code/skills)
|
|
8
8
|
[](https://github.com/matteoscurati/ai-consultants)
|
|
@@ -574,13 +574,16 @@ Full reference (~150 vars): [`references/configuration.md`](references/configura
|
|
|
574
574
|
Diagnose, suggest, and fix:
|
|
575
575
|
|
|
576
576
|
```bash
|
|
577
|
-
ai-consultants doctor # 22 health checks
|
|
577
|
+
ai-consultants doctor # 22 health checks (CLI installed?)
|
|
578
578
|
ai-consultants doctor --fix # Auto-fix common issues
|
|
579
579
|
ai-consultants doctor --json # JSON for automation
|
|
580
|
+
ai-consultants doctor --live # Real ping per consultant — catches installed-but-unauthenticated CLIs
|
|
580
581
|
ai-consultants doctor --suggest-config # Print recommended ENABLE_* based on detected CLIs
|
|
581
582
|
ai-consultants doctor --suggest-preset --question "..." # Recommend preset + strategy for a question
|
|
582
583
|
```
|
|
583
584
|
|
|
585
|
+
> When a consultant fails mid-consultation, the run surfaces the captured reason (e.g. `CLI not found`, `401 Unauthorized`) instead of a bare "Failed", so you can tell *not installed* from *not authenticated* from *transient*.
|
|
586
|
+
|
|
584
587
|
---
|
|
585
588
|
|
|
586
589
|
## How It Works
|
package/SKILL.md
CHANGED
|
@@ -5,10 +5,10 @@ license: MIT
|
|
|
5
5
|
compatibility: Requires bash, jq, and at least 2 AI CLI tools (agy, codex, vibe, etc.). macOS and Linux.
|
|
6
6
|
metadata:
|
|
7
7
|
author: matteoscurati
|
|
8
|
-
version: 2.
|
|
8
|
+
version: 2.19.2
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
# AI Consultants v2.
|
|
11
|
+
# AI Consultants v2.19.2 - AI Expert Panel
|
|
12
12
|
|
|
13
13
|
**A harness for every question.** Convene a panel of AI "consultants" for coding questions — and let the engine pick *how* they deliberate: a quick read, a convergence loop, an adversarial refutation gate, a tournament, or an exhaustive sweep. Each consultant has a **configurable persona** that shapes its analysis.
|
|
14
14
|
|
|
@@ -130,11 +130,14 @@ See [Full Configuration Reference](references/configuration.md) for all variable
|
|
|
130
130
|
Diagnose and fix issues:
|
|
131
131
|
|
|
132
132
|
```bash
|
|
133
|
-
./scripts/doctor.sh # Full check
|
|
133
|
+
./scripts/doctor.sh # Full check (CLI installed?)
|
|
134
134
|
./scripts/doctor.sh --fix # Auto-fix
|
|
135
135
|
./scripts/doctor.sh --json # JSON output
|
|
136
|
+
./scripts/doctor.sh --live # Real ping per consultant — catches installed-but-unauthenticated CLIs
|
|
136
137
|
```
|
|
137
138
|
|
|
139
|
+
When a consultant fails during a consultation, the run prints the captured reason (e.g. "CLI not found", "401 Unauthorized"). The run is also **graded** MET/DEGRADED/FAILED by how many responded (`QUORUM_MIN`), with a "Diagnosed Failures" section in the report. Set `ENABLE_HEALTH_GATE=true` to ping consultants and drop the dead ones *before* the run.
|
|
140
|
+
|
|
138
141
|
## Interpreting Results
|
|
139
142
|
|
|
140
143
|
| Scenario | Recommendation |
|
package/docs/COST_RATES.md
CHANGED
package/docs/cost_rates.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
2
|
+
"version": "2.19.2",
|
|
3
3
|
"updated": "2026-06-24",
|
|
4
4
|
"models": {
|
|
5
5
|
"_comment_units": "All rates are USD per 1,000 tokens (per-1K). lib/costs.sh computes cost as (token_count / 1000) * rate, so every value here MUST be a per-1K rate = (provider $/1M price) / 1000. Do NOT paste $/MTok figures directly.",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Release v2.18.0
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-06-26
|
|
4
|
+
**Type:** Minor — observability (diagnosable failures + live doctor)
|
|
5
|
+
**Previous:** v2.17.2
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- **When a consultant fails, you now see *why*** — "CLI not found", "401 Unauthorized", "Timeout", etc. — instead of a bare "Failed".
|
|
10
|
+
- **`doctor.sh --live`** sends a real ping to each enabled consultant, catching CLIs that are installed but not authenticated (which the static check reports as healthy).
|
|
11
|
+
|
|
12
|
+
## What's New
|
|
13
|
+
|
|
14
|
+
### Failures tell you the reason
|
|
15
|
+
|
|
16
|
+
Previously the orchestrator launched every consultant with its output **and stderr** sent to `/dev/null`, so a failing consultant produced only `Failed` / `Empty response` with no detail — making it impossible to distinguish *not installed* from *not authenticated* from a *transient* error without digging through temp files. (In a real session this led to a wrong diagnosis — blaming a timeout for what were actually auth failures.)
|
|
17
|
+
|
|
18
|
+
Now each consultant's stderr is captured, and on failure the run prints a one-line reason:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
✗ Cursor: Failed
|
|
22
|
+
↳ Cursor: [ERROR] Cursor CLI not found (command: agent)
|
|
23
|
+
✗ GLM: Failed
|
|
24
|
+
↳ GLM: All 2 attempts failed: 401 Unauthorized
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If nothing diagnostic was captured, it says so plainly ("no output and no error captured — CLI likely missing or not authenticated").
|
|
28
|
+
|
|
29
|
+
### `doctor.sh --live`
|
|
30
|
+
|
|
31
|
+
The normal `doctor.sh` only checks that a consultant's CLI is *installed* (`--version`). `--live` goes further: it sends a minimal real query to each **enabled** consultant and reports which actually respond.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
./scripts/doctor.sh --live
|
|
35
|
+
# ✓ Gemini: responded
|
|
36
|
+
# ✗ Cursor: no valid response — Cursor CLI not found (command: agent)
|
|
37
|
+
# ✗ Kimi: no valid response
|
|
38
|
+
# Live result: 1 responding, 2 failing
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
It's opt-in (one small query per consultant) and honors `DOCTOR_LIVE_TIMEOUT` (default 45s). Failures are reported as `doctor` issues (exit 1).
|
|
42
|
+
|
|
43
|
+
## Breaking Changes
|
|
44
|
+
|
|
45
|
+
None.
|
|
46
|
+
|
|
47
|
+
## Not fixable in code
|
|
48
|
+
|
|
49
|
+
`--live` and the surfaced reasons *diagnose* a consultant that's missing or unauthenticated — but installing/authenticating that CLI is up to you. Cold-start/warm-up retry effects are also environmental.
|
|
50
|
+
|
|
51
|
+
## Upgrade Guide
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cd ~/.claude/skills/ai-consultants && git pull # or: npx ai-consultants@latest install
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Commits
|
|
58
|
+
|
|
59
|
+
- `_______` feat: diagnosable consultant failures + doctor --live (observability)
|
|
60
|
+
|
|
61
|
+
## Contributors
|
|
62
|
+
|
|
63
|
+
- @matteoscurati
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Release v2.19.0
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-06-26
|
|
4
|
+
**Type:** Minor — reliability (quorum grading + health gate)
|
|
5
|
+
**Previous:** v2.18.0
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- **A consultation that quietly ran with fewer panelists no longer looks authoritative.** It's now graded **MET / DEGRADED / FAILED**, and the report lists exactly which consultants dropped and why.
|
|
10
|
+
- **Opt-in health gate** prunes non-responsive consultants *before* the run, so you don't spend the full consultation on CLIs that are installed but not authenticated.
|
|
11
|
+
|
|
12
|
+
These were chosen from a multi-agent investigation of the "flaky CLI" problem; the adversarial review explicitly steered us *away* from an automatic CLI→API fallback (it would emit persona-less, confidence-5 stubs that poison the vote).
|
|
13
|
+
|
|
14
|
+
## What's New
|
|
15
|
+
|
|
16
|
+
### Quorum grading + Diagnosed Failures
|
|
17
|
+
|
|
18
|
+
After the panel responds, the run is graded by how many consultants actually answered, relative to `QUORUM_MIN` (default 2):
|
|
19
|
+
|
|
20
|
+
- **MET** — everyone responded
|
|
21
|
+
- **DEGRADED** — some failed, but ≥ quorum
|
|
22
|
+
- **FAILED** — below quorum
|
|
23
|
+
|
|
24
|
+
The report now opens with an `Outcome:` line and, when anything dropped, a **Diagnosed Failures** table:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
**Outcome**: DEGRADED (5/7 responded, quorum=2)
|
|
28
|
+
|
|
29
|
+
## Diagnosed Failures
|
|
30
|
+
| Consultant | Reason |
|
|
31
|
+
|------------|--------|
|
|
32
|
+
| Cursor | Cursor CLI not found (command: agent) |
|
|
33
|
+
| Kimi | no output and no error captured — CLI likely missing or not authenticated |
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`QUORUM_ACTION=stop` aborts a below-quorum run; the default `warn` continues with the banner so nothing silently passes as a full-panel result.
|
|
37
|
+
|
|
38
|
+
### Health gate (opt-in)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
ENABLE_HEALTH_GATE=true ai-consultants "your question"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Pings every selected consultant **in parallel** with a tiny real query (`HEALTH_GATE_TIMEOUT`, default 30s) and drops the ones that don't respond before the real run starts — catching installed-but-unauthenticated CLIs and stale installs. It prunes; it does not switch transport. Custom API agents without a query script are kept (not probeable).
|
|
45
|
+
|
|
46
|
+
## Breaking Changes
|
|
47
|
+
|
|
48
|
+
None. Both features are additive; `QUORUM_ACTION` defaults to `warn` (banner only) and the health gate is off by default.
|
|
49
|
+
|
|
50
|
+
## Not included (deliberately)
|
|
51
|
+
|
|
52
|
+
The investigation rejected, for now: automatic CLI→API fallback (vote-poisoning persona loss), cold-start retry-timeout escalation (stretches the whole round's worst case), warm-up calls (extra billing/OAuth contention), and a transport-abstraction circuit breaker (racy on-disk state).
|
|
53
|
+
|
|
54
|
+
## Upgrade Guide
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cd ~/.claude/skills/ai-consultants && git pull # or: npx ai-consultants@latest install
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Commits
|
|
61
|
+
|
|
62
|
+
- `_______` feat: quorum grading + diagnosed-failures report + opt-in health gate
|
|
63
|
+
|
|
64
|
+
## Contributors
|
|
65
|
+
|
|
66
|
+
- @matteoscurati
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Release v2.19.1
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-06-27
|
|
4
|
+
**Type:** Patch — code-review cleanup for v2.19.0
|
|
5
|
+
**Previous:** v2.19.0
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- **Fixes the "Diagnosed Failures" report table breaking** when a consultant's error reason contains a `|`.
|
|
10
|
+
- **The opt-in health gate no longer wastes a ping** on consultants whose answer is already cached.
|
|
11
|
+
|
|
12
|
+
## What's New
|
|
13
|
+
|
|
14
|
+
### Report table escapes pipes
|
|
15
|
+
|
|
16
|
+
A failure reason containing a `|` (e.g. a CLI error that mentions a piped command) used to mangle the markdown row in the **Diagnosed Failures** table. The reason is now escaped (`|` → `\|`) in table mode. (This was a rendering bug, not a security issue — the text is your own CLI's stderr.)
|
|
17
|
+
|
|
18
|
+
### Cache-aware health gate
|
|
19
|
+
|
|
20
|
+
When `ENABLE_HEALTH_GATE=true` and semantic caching is on, a consultant whose response is already cached is kept **without** a ping — Round 1 would serve it for free, so probing it would defeat the cache. The gate only pings consultants it would actually run live.
|
|
21
|
+
|
|
22
|
+
### Internal cleanup (from `/code-review max`)
|
|
23
|
+
|
|
24
|
+
- A single `render_diagnosed_failure` helper now renders both the console log and the report table — the `name|reason` decode lives in one place.
|
|
25
|
+
- `ping_consultant` takes the already-lowercased consultant id (callers compute it anyway), removing a redundant `to_lower` fork per consultant.
|
|
26
|
+
- The health gate's inherent pre-run startup latency (it's serial with the consultation by design, up to `HEALTH_GATE_TIMEOUT`) is now documented in `config.sh`.
|
|
27
|
+
|
|
28
|
+
## Breaking Changes
|
|
29
|
+
|
|
30
|
+
None.
|
|
31
|
+
|
|
32
|
+
## Upgrade Guide
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd ~/.claude/skills/ai-consultants && git pull # or: npx ai-consultants@latest install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Commits
|
|
39
|
+
|
|
40
|
+
- `_______` fix: escape pipes in diagnosed-failures table + cache-aware health gate + review cleanup
|
|
41
|
+
|
|
42
|
+
## Contributors
|
|
43
|
+
|
|
44
|
+
- @matteoscurati
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Release v2.19.2
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-11
|
|
4
|
+
**Type:** Patch — cost-tracking resilience (fresh-install fix + concurrency + corruption self-heal)
|
|
5
|
+
**Previous:** v2.19.1
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- **Fresh-install cost tracking is fixed** — sessions no longer fail to record their cost the first time you run the tool.
|
|
10
|
+
- **A corrupt `costs.json` now self-heals** instead of permanently wedging every future cost update.
|
|
11
|
+
- **Concurrent consultations can't lose cost records** — the update is serialized and uses a unique temp file.
|
|
12
|
+
- **Cost bookkeeping can never abort a run** — every failure path degrades to a warning.
|
|
13
|
+
- No configuration changes; no impact on existing installs whose data dir already exists.
|
|
14
|
+
|
|
15
|
+
## What's New
|
|
16
|
+
|
|
17
|
+
### Cost tracking resilience (`lib/costs.sh`)
|
|
18
|
+
|
|
19
|
+
`track_session_cost` records each consultation's cost to `costs.json` under the XDG data dir (`~/.local/share/ai-consultants/`). Four problems are fixed:
|
|
20
|
+
|
|
21
|
+
| Problem | Before | After |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| **Fresh install** | Wrote to `costs.json` without creating its parent dir → every session failed with "No such file or directory"; cost tracking silently never accumulated | `mkdir -p` the parent dir on demand |
|
|
24
|
+
| **Corrupt file** | A malformed `costs.json` failed every future `jq` update, forever | Detected via `jq empty`, moved aside to `costs.json.corrupt`, and reset |
|
|
25
|
+
| **Concurrency** | Unguarded read-modify-write; parallel runs could interleave and lose records | Serialized with a portable `mkdir` lock (5s bounded wait, then proceeds unlocked) |
|
|
26
|
+
| **Temp file** | Fixed `.tmp` sibling shared across runs → clobbered writes / failed `mv` | Unique `mktemp` temp file per write |
|
|
27
|
+
|
|
28
|
+
All failure paths now log a warning and return success: cost bookkeeping is best-effort and can never abort a consultation. This matters because the caller in `consult_all.sh` runs under `set -e` *after* every consultant has already been queried and billed — a bookkeeping hiccup must not throw away the run's results.
|
|
29
|
+
|
|
30
|
+
The function is split into an outer `track_session_cost` (directory creation + lock management) and an inner `_track_session_cost_update` (the read-modify-write, run with the lock held).
|
|
31
|
+
|
|
32
|
+
### Tests
|
|
33
|
+
|
|
34
|
+
New `test_cost_tracking_resilience` in `scripts/test_suite.sh` (5 assertions, wired into `main()`) covers the fresh-install path (costs.json created under a missing nested parent dir), cross-session accumulation (0.25 → 0.75), and the corrupt-file self-heal including the `.corrupt` backup. All 8 suites pass; shellcheck clean.
|
|
35
|
+
|
|
36
|
+
## Breaking Changes
|
|
37
|
+
|
|
38
|
+
None. Backwards-compatible. Installs whose data dir already exists see no behavioral change.
|
|
39
|
+
|
|
40
|
+
## Upgrade Guide
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd ~/.claude/skills/ai-consultants && git pull # or: npx ai-consultants@latest install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
No configuration changes required.
|
|
47
|
+
|
|
48
|
+
## Commits
|
|
49
|
+
|
|
50
|
+
- `_______` fix(costs): harden track_session_cost against fresh-install, concurrency, and corruption
|
|
51
|
+
|
|
52
|
+
## Contributors
|
|
53
|
+
|
|
54
|
+
- @matteoscurati
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-consultants",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.2",
|
|
4
4
|
"description": "A harness for every question - a panel of up to 15 AI models that picks its own deliberation strategy per question (convergence, adversarial verification, tournament) and checks its work before answering",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ai-consultants": "bin/ai-consultants"
|
package/scripts/config.sh
CHANGED
|
@@ -574,6 +574,35 @@ CONVERGENCE_STALL_EPSILON="${CONVERGENCE_STALL_EPSILON:-5}"
|
|
|
574
574
|
# and runs anonymous peer review as a refutation gate before synthesis.
|
|
575
575
|
ENABLE_ADVERSARIAL_VERIFY="${ENABLE_ADVERSARIAL_VERIFY:-true}"
|
|
576
576
|
|
|
577
|
+
# =============================================================================
|
|
578
|
+
# QUORUM GRADING (v2.19.0)
|
|
579
|
+
# =============================================================================
|
|
580
|
+
# Grade the consultation outcome by the number of consultants that actually
|
|
581
|
+
# responded, so a run that silently shrank to a few panelists is reported as
|
|
582
|
+
# DEGRADED/FAILED instead of presenting as authoritative. Failures are listed
|
|
583
|
+
# with their diagnosed reason (from the v2.18.0 .err capture) in the report.
|
|
584
|
+
# Minimum successful responses below which the outcome is FAILED.
|
|
585
|
+
QUORUM_MIN="${QUORUM_MIN:-2}"
|
|
586
|
+
# warn (default: banner + continue) | stop (abort if below quorum)
|
|
587
|
+
QUORUM_ACTION="${QUORUM_ACTION:-warn}"
|
|
588
|
+
|
|
589
|
+
# =============================================================================
|
|
590
|
+
# HEALTH GATE (v2.19.0, opt-in)
|
|
591
|
+
# =============================================================================
|
|
592
|
+
# Before the consultation, send a cheap real "ping" query to each selected
|
|
593
|
+
# consultant in parallel and drop the non-responsive ones (installed-but-
|
|
594
|
+
# unauthenticated CLIs, stale installs), so the panel only spends the full run
|
|
595
|
+
# on consultants that actually work. Opt-in: it costs one tiny extra query per
|
|
596
|
+
# consultant. Prunes; it does not switch transport. Consultants whose response
|
|
597
|
+
# is already cached are kept WITHOUT a ping (cache-aware).
|
|
598
|
+
#
|
|
599
|
+
# TRADE-OFF: the gate runs BEFORE Round 1 and is serial with it (a pre-flight
|
|
600
|
+
# probe can't overlap the real run by definition), so it adds up to
|
|
601
|
+
# HEALTH_GATE_TIMEOUT of blocking startup latency when a consultant is slow/dead.
|
|
602
|
+
# That's the cost of pruning up front; keep it opt-in and tune the timeout.
|
|
603
|
+
ENABLE_HEALTH_GATE="${ENABLE_HEALTH_GATE:-false}"
|
|
604
|
+
HEALTH_GATE_TIMEOUT="${HEALTH_GATE_TIMEOUT:-30}"
|
|
605
|
+
|
|
577
606
|
# --- Debate Optimization ---
|
|
578
607
|
# Optimize debate rounds for token efficiency: skip debate when consensus is
|
|
579
608
|
# already high (confidence spread below DEBATE_CONFIDENCE_SPREAD_THRESHOLD).
|
|
@@ -858,4 +887,4 @@ EOF
|
|
|
858
887
|
# VERSION
|
|
859
888
|
# =============================================================================
|
|
860
889
|
|
|
861
|
-
AI_CONSULTANTS_VERSION="2.
|
|
890
|
+
AI_CONSULTANTS_VERSION="2.19.2"
|
package/scripts/consult_all.sh
CHANGED
|
@@ -369,6 +369,45 @@ else
|
|
|
369
369
|
_discover_custom_api_agents
|
|
370
370
|
fi
|
|
371
371
|
|
|
372
|
+
# --- Health Gate (v2.19.0, opt-in) ---
|
|
373
|
+
# Ping each selected consultant in parallel and drop the non-responsive ones
|
|
374
|
+
# (installed-but-unauthenticated CLIs, stale installs) BEFORE the real run, so
|
|
375
|
+
# the quorum/budget checks below see the genuinely-working panel. Opt-in: costs
|
|
376
|
+
# one tiny extra query per consultant. Prunes; does not switch transport.
|
|
377
|
+
if [[ "${ENABLE_HEALTH_GATE:-false}" == "true" && ${#SELECTED_CONSULTANTS[@]} -gt 0 ]]; then
|
|
378
|
+
HG_DIR=$(mktemp -d)
|
|
379
|
+
declare -a HG_PIDS=() HG_NAMES=() RESPONSIVE=()
|
|
380
|
+
for _c in "${SELECTED_CONSULTANTS[@]}"; do
|
|
381
|
+
_cl=$(to_lower "$_c")
|
|
382
|
+
# Cache-aware: a consultant whose response is already cached costs nothing
|
|
383
|
+
# in Round 1 (check_cache short-circuits), so keep it without a billed ping.
|
|
384
|
+
if is_cache_enabled && [[ -n "$(check_cache "$QUERY" "$QUESTION_CATEGORY" "$_cl" "$CONTEXT_FILE" 2>/dev/null || echo "")" ]]; then
|
|
385
|
+
RESPONSIVE+=("$_c")
|
|
386
|
+
log_debug " Health gate: $_c cached, kept (not pinged)"
|
|
387
|
+
continue
|
|
388
|
+
fi
|
|
389
|
+
( ping_consultant "$_cl" "$SCRIPT_DIR" "${HEALTH_GATE_TIMEOUT:-30}" "$HG_DIR/${_cl}.json" "$HG_DIR/${_cl}.err" ) &
|
|
390
|
+
HG_PIDS+=("$!"); HG_NAMES+=("$_c")
|
|
391
|
+
done
|
|
392
|
+
[[ ${#HG_PIDS[@]} -gt 0 ]] && log_info "Health gate: pinging ${#HG_PIDS[@]} consultants in parallel..."
|
|
393
|
+
for _i in "${!HG_PIDS[@]}"; do
|
|
394
|
+
if wait "${HG_PIDS[$_i]}" 2>/dev/null; then _rc=0; else _rc=$?; fi
|
|
395
|
+
_nm="${HG_NAMES[$_i]}"
|
|
396
|
+
if [[ $_rc -eq 0 || $_rc -eq 2 ]]; then
|
|
397
|
+
RESPONSIVE+=("$_nm")
|
|
398
|
+
[[ $_rc -eq 2 ]] && log_debug " Health gate: $_nm not probeable (no query script), kept"
|
|
399
|
+
else
|
|
400
|
+
_cl=$(to_lower "$_nm")
|
|
401
|
+
_reason=$(get_consultant_error_reason "$HG_DIR/${_cl}.err")
|
|
402
|
+
log_warn " Health gate dropped $_nm${_reason:+: $_reason}"
|
|
403
|
+
fi
|
|
404
|
+
done
|
|
405
|
+
rm -rf "$HG_DIR"
|
|
406
|
+
_hg_total=${#SELECTED_CONSULTANTS[@]}
|
|
407
|
+
SELECTED_CONSULTANTS=(${RESPONSIVE[@]+"${RESPONSIVE[@]}"})
|
|
408
|
+
log_info "Health gate: ${#SELECTED_CONSULTANTS[@]} of ${_hg_total} usable"
|
|
409
|
+
fi
|
|
410
|
+
|
|
372
411
|
if [[ ${#SELECTED_CONSULTANTS[@]} -eq 0 ]]; then
|
|
373
412
|
log_error "No consultant enabled or selected"
|
|
374
413
|
exit 1
|
|
@@ -421,6 +460,9 @@ declare -a CACHE_HITS=()
|
|
|
421
460
|
for consultant in "${SELECTED_CONSULTANTS[@]}"; do
|
|
422
461
|
consultant_lower=$(to_lower "$consultant")
|
|
423
462
|
local_output_file="$OUTPUT_DIR/${consultant_lower}.json"
|
|
463
|
+
# Capture each consultant's stderr so a failure (auth/CLI-missing/transient)
|
|
464
|
+
# can be surfaced instead of silently discarded (was `2>&1` to /dev/null).
|
|
465
|
+
local_err_file="$OUTPUT_DIR/${consultant_lower}.err"
|
|
424
466
|
OUTPUT_FILES+=("$local_output_file")
|
|
425
467
|
NAMES+=("$consultant")
|
|
426
468
|
|
|
@@ -456,21 +498,21 @@ for consultant in "${SELECTED_CONSULTANTS[@]}"; do
|
|
|
456
498
|
apply_launch_stagger
|
|
457
499
|
export "$model_var=$local_model"
|
|
458
500
|
"$query_script" "" "$CONTEXT_FILE" "$local_output_file"
|
|
459
|
-
) > /dev/null 2
|
|
501
|
+
) > /dev/null 2>"$local_err_file" &
|
|
460
502
|
PIDS+=($!)
|
|
461
503
|
update_progress "$consultant" 10 "running"
|
|
462
504
|
continue
|
|
463
505
|
fi
|
|
464
506
|
fi
|
|
465
507
|
# Standard launch (inherits current environment models)
|
|
466
|
-
( apply_launch_stagger; "$query_script" "" "$CONTEXT_FILE" "$local_output_file" ) > /dev/null 2
|
|
508
|
+
( apply_launch_stagger; "$query_script" "" "$CONTEXT_FILE" "$local_output_file" ) > /dev/null 2>"$local_err_file" &
|
|
467
509
|
else
|
|
468
510
|
# Fallback: custom API agent via generic API query
|
|
469
511
|
(
|
|
470
512
|
apply_launch_stagger
|
|
471
513
|
source "$SCRIPT_DIR/lib/api_query.sh"
|
|
472
514
|
run_api_consultant "$consultant" "" "$CONTEXT_FILE" "$local_output_file"
|
|
473
|
-
) > /dev/null 2
|
|
515
|
+
) > /dev/null 2>"$local_err_file" &
|
|
474
516
|
fi
|
|
475
517
|
|
|
476
518
|
PIDS+=($!)
|
|
@@ -482,10 +524,25 @@ if [[ ${#CACHE_HITS[@]} -gt 0 ]]; then
|
|
|
482
524
|
log_info "Cache hits: ${CACHE_HITS[*]}"
|
|
483
525
|
fi
|
|
484
526
|
|
|
527
|
+
# Surface the captured stderr reason for a consultant that produced no usable
|
|
528
|
+
# output, so the user sees WHY (auth error, CLI missing, transient) instead of a
|
|
529
|
+
# bare "Failed". Derives the .err path from the .json output path.
|
|
530
|
+
_surface_consultant_error() {
|
|
531
|
+
local cname="$1" out="$2"
|
|
532
|
+
local ef="${out%.json}.err"
|
|
533
|
+
local reason
|
|
534
|
+
reason=$(get_consultant_error_reason "$ef")
|
|
535
|
+
local detail="${reason:-no output and no error captured — CLI likely missing or not authenticated (run doctor.sh --live)}"
|
|
536
|
+
log_warn " ↳ ${cname}: ${detail}"
|
|
537
|
+
# Record for the report's "Diagnosed Failures" section (quorum grading, v2.19.0).
|
|
538
|
+
DIAGNOSED_FAILURES+=("${cname}|${detail}")
|
|
539
|
+
}
|
|
540
|
+
|
|
485
541
|
# --- Wait and Collect Results ---
|
|
486
542
|
log_info "Waiting for responses from ${#PIDS[@]} consultants..."
|
|
487
543
|
|
|
488
544
|
declare -a RESULTS=()
|
|
545
|
+
declare -a DIAGNOSED_FAILURES=()
|
|
489
546
|
SUCCESS_COUNT=0
|
|
490
547
|
|
|
491
548
|
for i in "${!PIDS[@]}"; do
|
|
@@ -521,11 +578,13 @@ for i in "${!PIDS[@]}"; do
|
|
|
521
578
|
fi
|
|
522
579
|
else
|
|
523
580
|
log_warn " $name: Empty response"
|
|
581
|
+
_surface_consultant_error "$name" "$output_file"
|
|
524
582
|
RESULTS+=("$name:EMPTY")
|
|
525
583
|
update_progress "$name" 100 "failed"
|
|
526
584
|
fi
|
|
527
585
|
else
|
|
528
586
|
log_error " $name: Failed"
|
|
587
|
+
_surface_consultant_error "$name" "$output_file"
|
|
529
588
|
RESULTS+=("$name:FAILED")
|
|
530
589
|
update_progress "$name" 100 "failed"
|
|
531
590
|
fi
|
|
@@ -533,6 +592,27 @@ done
|
|
|
533
592
|
|
|
534
593
|
echo "" >&2
|
|
535
594
|
|
|
595
|
+
# --- Quorum Grading (v2.19.0) ---
|
|
596
|
+
# Grade the run by how many consultants actually responded, so a silently-shrunk
|
|
597
|
+
# panel is reported as DEGRADED/FAILED instead of presenting as authoritative.
|
|
598
|
+
QUORUM_ATTEMPTED=${#PIDS[@]}
|
|
599
|
+
QUORUM_MIN_EFF="${QUORUM_MIN:-2}"
|
|
600
|
+
QUORUM_OUTCOME=$(grade_quorum "$SUCCESS_COUNT" "$QUORUM_ATTEMPTED" "$QUORUM_MIN_EFF")
|
|
601
|
+
|
|
602
|
+
if [[ "$QUORUM_OUTCOME" == "DEGRADED" ]]; then
|
|
603
|
+
log_warn "Quorum: DEGRADED — ${SUCCESS_COUNT}/${QUORUM_ATTEMPTED} consultants responded"
|
|
604
|
+
elif [[ "$QUORUM_OUTCOME" == "FAILED" ]]; then
|
|
605
|
+
log_error "Quorum: FAILED — only ${SUCCESS_COUNT}/${QUORUM_ATTEMPTED} responded (need >= ${QUORUM_MIN_EFF})"
|
|
606
|
+
if [[ ${#DIAGNOSED_FAILURES[@]} -gt 0 ]]; then
|
|
607
|
+
log_error "Diagnosed failures:"
|
|
608
|
+
for _df in "${DIAGNOSED_FAILURES[@]}"; do log_error "$(render_diagnosed_failure "$_df")"; done
|
|
609
|
+
fi
|
|
610
|
+
if [[ "${QUORUM_ACTION:-warn}" == "stop" ]]; then
|
|
611
|
+
log_error "Aborting below quorum (QUORUM_ACTION=stop). Set QUORUM_ACTION=warn to continue."
|
|
612
|
+
exit 1
|
|
613
|
+
fi
|
|
614
|
+
fi
|
|
615
|
+
|
|
536
616
|
# --- Budget Check: After Round 1 ---
|
|
537
617
|
CURRENT_COST=0
|
|
538
618
|
if [[ "$ENABLE_COST_TRACKING" == "true" && $SUCCESS_COUNT -gt 0 ]]; then
|
|
@@ -792,9 +872,29 @@ REPORT_FILE="$OUTPUT_DIR/report.md"
|
|
|
792
872
|
echo "**Directory**: $(pwd)"
|
|
793
873
|
echo "**Version**: AI Consultants v${AI_CONSULTANTS_VERSION}"
|
|
794
874
|
echo "**Category**: $QUESTION_CATEGORY"
|
|
795
|
-
echo "**
|
|
875
|
+
echo "**Outcome**: ${QUORUM_OUTCOME:-MET} (${SUCCESS_COUNT}/${QUORUM_ATTEMPTED:-${#PIDS[@]}} responded, quorum=${QUORUM_MIN_EFF:-2})"
|
|
796
876
|
echo "**Consensus**: ${CONSENSUS_SCORE}% ($CONSENSUS_LEVEL)"
|
|
797
877
|
echo ""
|
|
878
|
+
|
|
879
|
+
# Diagnosed failures (quorum grading, v2.19.0) — surface WHY consultants
|
|
880
|
+
# dropped, so a degraded panel is visible instead of silently authoritative.
|
|
881
|
+
if [[ ${#DIAGNOSED_FAILURES[@]} -gt 0 ]]; then
|
|
882
|
+
echo "## Diagnosed Failures"
|
|
883
|
+
echo ""
|
|
884
|
+
if [[ "${QUORUM_OUTCOME:-MET}" == "FAILED" ]]; then
|
|
885
|
+
echo "> ⚠️ Below quorum (${SUCCESS_COUNT}/${QUORUM_MIN_EFF:-2}). Treat the recommendation as low-confidence."
|
|
886
|
+
elif [[ "${QUORUM_OUTCOME:-MET}" == "DEGRADED" ]]; then
|
|
887
|
+
echo "> Some consultants did not respond — the panel ran with ${SUCCESS_COUNT}/${QUORUM_ATTEMPTED:-${#PIDS[@]}}."
|
|
888
|
+
fi
|
|
889
|
+
echo ""
|
|
890
|
+
echo "| Consultant | Reason |"
|
|
891
|
+
echo "|------------|--------|"
|
|
892
|
+
for _df in "${DIAGNOSED_FAILURES[@]}"; do
|
|
893
|
+
echo "$(render_diagnosed_failure "$_df" table)"
|
|
894
|
+
done
|
|
895
|
+
echo ""
|
|
896
|
+
fi
|
|
897
|
+
|
|
798
898
|
echo "---"
|
|
799
899
|
echo ""
|
|
800
900
|
|
package/scripts/doctor.sh
CHANGED
|
@@ -32,6 +32,7 @@ VERBOSE=false
|
|
|
32
32
|
QUICK_MODE=false
|
|
33
33
|
SUGGEST_CONFIG=false
|
|
34
34
|
SUGGEST_PRESET=false
|
|
35
|
+
LIVE_MODE=false
|
|
35
36
|
QUESTION=""
|
|
36
37
|
|
|
37
38
|
while [[ $# -gt 0 ]]; do
|
|
@@ -60,6 +61,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
60
61
|
SUGGEST_PRESET=true
|
|
61
62
|
shift
|
|
62
63
|
;;
|
|
64
|
+
--live)
|
|
65
|
+
LIVE_MODE=true
|
|
66
|
+
shift
|
|
67
|
+
;;
|
|
63
68
|
--question)
|
|
64
69
|
QUESTION="${2:-}"
|
|
65
70
|
shift 2
|
|
@@ -78,6 +83,9 @@ while [[ $# -gt 0 ]]; do
|
|
|
78
83
|
echo " --suggest-config Print recommended ENABLE_* configuration"
|
|
79
84
|
echo " --suggest-preset Recommend preset + strategy for a question"
|
|
80
85
|
echo " --question \"...\" Question text for --suggest-preset"
|
|
86
|
+
echo " --live Send a real ping query to each enabled consultant"
|
|
87
|
+
echo " (catches installed-but-unauthenticated CLIs that the"
|
|
88
|
+
echo " static check reports as healthy). Costs a tiny query each."
|
|
81
89
|
exit 0
|
|
82
90
|
;;
|
|
83
91
|
*)
|
|
@@ -1080,6 +1088,62 @@ suggest_preset() {
|
|
|
1080
1088
|
# MAIN
|
|
1081
1089
|
# =============================================================================
|
|
1082
1090
|
|
|
1091
|
+
# --- Live consultant ping (--live) ---------------------------------------------
|
|
1092
|
+
# The static checks above only verify a CLI is installed (`--version`), so they
|
|
1093
|
+
# report a consultant as healthy even when its CLI errors at query time (e.g.
|
|
1094
|
+
# not authenticated). This sends a real, minimal query to each ENABLED consultant
|
|
1095
|
+
# and reports pass/fail with the captured error reason. Opt-in (costs one tiny
|
|
1096
|
+
# query per consultant).
|
|
1097
|
+
check_live_consultants() {
|
|
1098
|
+
print_section "Live Consultant Check (real ping query)"
|
|
1099
|
+
|
|
1100
|
+
local timeout_s="${DOCTOR_LIVE_TIMEOUT:-45}"
|
|
1101
|
+
local tmpdir
|
|
1102
|
+
tmpdir=$(mktemp -d)
|
|
1103
|
+
|
|
1104
|
+
# name|enable-flag — mirrors consult_all's selection set.
|
|
1105
|
+
local entries=(
|
|
1106
|
+
"Gemini|ENABLE_GEMINI" "Codex|ENABLE_CODEX" "Mistral|ENABLE_MISTRAL"
|
|
1107
|
+
"Kilo|ENABLE_KILO" "Cursor|ENABLE_CURSOR" "Aider|ENABLE_AIDER"
|
|
1108
|
+
"Amp|ENABLE_AMP" "Kimi|ENABLE_KIMI" "Claude|ENABLE_CLAUDE"
|
|
1109
|
+
"Qwen3|ENABLE_QWEN3" "GLM|ENABLE_GLM" "Grok|ENABLE_GROK"
|
|
1110
|
+
"DeepSeek|ENABLE_DEEPSEEK" "MiniMax|ENABLE_MINIMAX" "Ollama|ENABLE_OLLAMA"
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
local live_pass=0 live_fail=0 e name flagvar lower out err reason rc
|
|
1114
|
+
for e in "${entries[@]}"; do
|
|
1115
|
+
name="${e%%|*}"; flagvar="${e##*|}"
|
|
1116
|
+
if [[ "${!flagvar:-false}" != "true" ]]; then
|
|
1117
|
+
[[ "$VERBOSE" == "true" ]] && _print " ○ $name: disabled"
|
|
1118
|
+
continue
|
|
1119
|
+
fi
|
|
1120
|
+
if should_skip_consultant "$name" 2>/dev/null; then
|
|
1121
|
+
_print " ○ $name: excluded (invoking agent)"
|
|
1122
|
+
continue
|
|
1123
|
+
fi
|
|
1124
|
+
lower=$(to_lower "$name")
|
|
1125
|
+
out="$tmpdir/${lower}.json"; err="$tmpdir/${lower}.err"
|
|
1126
|
+
if ping_consultant "$lower" "$SCRIPT_DIR" "$timeout_s" "$out" "$err"; then rc=0; else rc=$?; fi
|
|
1127
|
+
if [[ $rc -eq 2 ]]; then
|
|
1128
|
+
_print " ✗ $name: query script missing"
|
|
1129
|
+
add_issue "live" "$name query script missing" "reinstall the skill"
|
|
1130
|
+
check_fail; live_fail=$((live_fail + 1))
|
|
1131
|
+
elif [[ $rc -eq 0 ]]; then
|
|
1132
|
+
_print " ✓ $name: responded"
|
|
1133
|
+
check_pass; live_pass=$((live_pass + 1))
|
|
1134
|
+
else
|
|
1135
|
+
reason=$(get_consultant_error_reason "$err")
|
|
1136
|
+
_print " ✗ $name: no valid response${reason:+ — $reason}"
|
|
1137
|
+
add_issue "live" "$name failed live ping${reason:+: $reason}" "check that the $name CLI is installed and authenticated"
|
|
1138
|
+
check_fail; live_fail=$((live_fail + 1))
|
|
1139
|
+
fi
|
|
1140
|
+
done
|
|
1141
|
+
|
|
1142
|
+
rm -rf "$tmpdir"
|
|
1143
|
+
_print ""
|
|
1144
|
+
_print " Live result: ${live_pass} responding, ${live_fail} failing"
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1083
1147
|
main() {
|
|
1084
1148
|
print_header
|
|
1085
1149
|
check_dependencies
|
|
@@ -1112,4 +1176,11 @@ if [[ "$SUGGEST_PRESET" == "true" ]]; then
|
|
|
1112
1176
|
exit 0
|
|
1113
1177
|
fi
|
|
1114
1178
|
|
|
1179
|
+
if [[ "$LIVE_MODE" == "true" ]]; then
|
|
1180
|
+
print_header
|
|
1181
|
+
check_live_consultants
|
|
1182
|
+
print_summary
|
|
1183
|
+
[[ ${#ISSUES[@]} -gt 0 ]] && exit 1 || exit 0
|
|
1184
|
+
fi
|
|
1185
|
+
|
|
1115
1186
|
main
|
package/scripts/lib/common.sh
CHANGED
|
@@ -192,11 +192,17 @@ run_query() {
|
|
|
192
192
|
local error_msg=""
|
|
193
193
|
[[ -f "$error_file" ]] && error_msg=$(head -5 "$error_file" 2>/dev/null)
|
|
194
194
|
|
|
195
|
+
# First line of the CLI's own stderr — the actual reason (auth error,
|
|
196
|
+
# command not found, etc.). Kept for the final failure message so it
|
|
197
|
+
# reaches the orchestrator's captured stderr (the per-CLI error_file is a
|
|
198
|
+
# mktemp path the caller can't locate).
|
|
199
|
+
local error_first=""
|
|
200
|
+
[[ -n "$error_msg" ]] && error_first=$(printf '%s\n' "$error_msg" | grep -v '^[[:space:]]*$' | head -1 | cut -c1-200)
|
|
201
|
+
|
|
195
202
|
if [[ $exit_code -eq 124 ]]; then
|
|
196
203
|
log_warn "[$consultant_name] Timeout after ${timeout_seconds}s"
|
|
197
204
|
else
|
|
198
|
-
log_warn "[$consultant_name] Error (code: $exit_code)"
|
|
199
|
-
[[ -n "$error_msg" ]] && log_debug "Details: $error_msg"
|
|
205
|
+
log_warn "[$consultant_name] Error (code: $exit_code)${error_first:+: $error_first}"
|
|
200
206
|
fi
|
|
201
207
|
|
|
202
208
|
((attempt++)) || true
|
|
@@ -206,7 +212,7 @@ run_query() {
|
|
|
206
212
|
fi
|
|
207
213
|
done
|
|
208
214
|
|
|
209
|
-
log_error "[$consultant_name] All $MAX_RETRIES attempts failed"
|
|
215
|
+
log_error "[$consultant_name] All $MAX_RETRIES attempts failed${error_first:+: $error_first}"
|
|
210
216
|
return 1
|
|
211
217
|
}
|
|
212
218
|
|
|
@@ -923,6 +929,76 @@ build_error_response() {
|
|
|
923
929
|
# fallback-only: if the input already parses as JSON it is returned unchanged
|
|
924
930
|
# (a real fence makes the text invalid JSON, so the gate reliably detects it).
|
|
925
931
|
# Usage: cleaned=$(strip_json_fence "$raw")
|
|
932
|
+
# Extract a concise, ANSI-stripped failure reason from a consultant's captured
|
|
933
|
+
# stderr (.err) file, for surfacing WHY a consultant produced no output (auth
|
|
934
|
+
# error, CLI not installed, transient init failure, ...) instead of a bare
|
|
935
|
+
# "Failed". Prefers an explicit error-ish line; falls back to the last non-empty
|
|
936
|
+
# line. Returns empty string if the file is missing/empty.
|
|
937
|
+
# Usage: reason=$(get_consultant_error_reason "$err_file")
|
|
938
|
+
get_consultant_error_reason() {
|
|
939
|
+
local ef="$1"
|
|
940
|
+
[[ -s "$ef" ]] || return 0
|
|
941
|
+
local esc cleaned line
|
|
942
|
+
esc=$(printf '\033')
|
|
943
|
+
# Strip ANSI, blank lines, AND our own orchestration status logs first — those
|
|
944
|
+
# carry no failure info and would otherwise be mis-picked (e.g. the
|
|
945
|
+
# "Consulting X (timeout: 180s...)" header matches the 'timeout' pattern below).
|
|
946
|
+
cleaned=$(sed "s/${esc}\[[0-9;]*m//g" "$ef" 2>/dev/null \
|
|
947
|
+
| grep -v '^[[:space:]]*$' \
|
|
948
|
+
| grep -ivE '\[INFO\].*(Consulting|Response received|Waiting .* before|Attempt [0-9]|Querying)')
|
|
949
|
+
# Prefer a line that names an actual error; else the last remaining line.
|
|
950
|
+
line=$(printf '%s\n' "$cleaned" | grep -iE 'error|fail|not found|no such|denied|unauthor|invalid|timed? ?out|missing|command not found|exit code' | tail -1)
|
|
951
|
+
[[ -z "$line" ]] && line=$(printf '%s\n' "$cleaned" | tail -1)
|
|
952
|
+
printf '%s' "$line" | cut -c1-200
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
# Render one DIAGNOSED_FAILURES entry ("name|reason") for a given surface.
|
|
956
|
+
# Single source of the "name|reason" decode (used by both the console log and the
|
|
957
|
+
# report table), and the table mode escapes any '|' in the reason so a CLI error
|
|
958
|
+
# containing a pipe doesn't mangle the markdown row.
|
|
959
|
+
# Usage: render_diagnosed_failure "<name>|<reason>" [console|table]
|
|
960
|
+
render_diagnosed_failure() {
|
|
961
|
+
local entry="$1" mode="${2:-console}" name reason
|
|
962
|
+
name="${entry%%|*}"
|
|
963
|
+
reason="${entry#*|}"
|
|
964
|
+
if [[ "$mode" == "table" ]]; then
|
|
965
|
+
printf '| %s | %s |' "$name" "${reason//|/\\|}"
|
|
966
|
+
else
|
|
967
|
+
printf ' - %s: %s' "$name" "$reason"
|
|
968
|
+
fi
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
# Grade a consultation by how many consultants responded (v2.19.0).
|
|
972
|
+
# Usage: grade_quorum <success_count> <attempted_count> <min_quorum>
|
|
973
|
+
# Echoes: FAILED (< min) | DEGRADED (>= min but some failed) | MET (all responded)
|
|
974
|
+
grade_quorum() {
|
|
975
|
+
local success="$1" attempted="$2" min="$3"
|
|
976
|
+
if [[ "$success" -lt "$min" ]]; then
|
|
977
|
+
echo "FAILED"
|
|
978
|
+
elif [[ "$success" -lt "$attempted" ]]; then
|
|
979
|
+
echo "DEGRADED"
|
|
980
|
+
else
|
|
981
|
+
echo "MET"
|
|
982
|
+
fi
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
# Send a trivial real "ping" query to a consultant's query script and report
|
|
986
|
+
# whether it produced a structurally valid response. Shared by `doctor --live`
|
|
987
|
+
# and the consult_all health gate. Persona is disabled (we only need a parseable
|
|
988
|
+
# envelope, not a full analysis). Caller provides scratch out/err paths.
|
|
989
|
+
# <id> is the ALREADY-lowercased consultant id. Callers compute the lowercase
|
|
990
|
+
# form anyway (for the out/err paths), so we take it directly instead of forking
|
|
991
|
+
# to_lower again per consultant.
|
|
992
|
+
# Usage: ping_consultant <id> <scripts_dir> <timeout_s> <out_file> <err_file>
|
|
993
|
+
# Returns: 0 = responded, 1 = failed/empty, 2 = no query script (not probeable)
|
|
994
|
+
ping_consultant() {
|
|
995
|
+
local id="$1" scripts_dir="$2" timeout_s="$3" out="$4" err="$5"
|
|
996
|
+
local qs="$scripts_dir/query_${id}.sh"
|
|
997
|
+
[[ -x "$qs" ]] || return 2
|
|
998
|
+
ENABLE_PERSONA=false run_with_timeout "$timeout_s" "$qs" "Reply with exactly: OK" "" "$out" >/dev/null 2>"$err" || true
|
|
999
|
+
[[ -s "$out" ]] && jq -e '.response' "$out" >/dev/null 2>&1
|
|
1000
|
+
}
|
|
1001
|
+
|
|
926
1002
|
strip_json_fence() {
|
|
927
1003
|
local text="$1"
|
|
928
1004
|
if echo "$text" | jq -e '.' > /dev/null 2>&1; then
|
package/scripts/lib/costs.sh
CHANGED
|
@@ -365,23 +365,80 @@ COST_TRACKING_FILE="${COST_TRACKING_FILE:-${_AI_CONSULTANTS_XDG_DATA:-/tmp/ai_co
|
|
|
365
365
|
|
|
366
366
|
# Record session cost
|
|
367
367
|
# Usage: track_session_cost <session_id> <cost>
|
|
368
|
+
# Best-effort: cost tracking is bookkeeping. The caller in consult_all.sh runs
|
|
369
|
+
# under set -e *after* every consultant has already been queried and billed,
|
|
370
|
+
# so failures here degrade to a warning instead of aborting the run.
|
|
368
371
|
track_session_cost() {
|
|
369
372
|
local session_id="$1"
|
|
370
373
|
local cost="$2"
|
|
374
|
+
|
|
375
|
+
# The XDG data dir may not exist yet on a fresh install — costs.json is
|
|
376
|
+
# the only artifact stored there
|
|
377
|
+
if ! mkdir -p "$(dirname "$COST_TRACKING_FILE")" 2>/dev/null; then
|
|
378
|
+
log_warn "Cost tracking skipped: cannot create $(dirname "$COST_TRACKING_FILE")"
|
|
379
|
+
return 0
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
# Serialize the read-modify-write against concurrent consultations with a
|
|
383
|
+
# portable mkdir lock (flock is unavailable on macOS). Bounded wait, then
|
|
384
|
+
# proceed unlocked: bookkeeping must never block or abort the run.
|
|
385
|
+
local lock_dir="${COST_TRACKING_FILE}.lock" locked=false _i
|
|
386
|
+
for _i in {1..50}; do
|
|
387
|
+
if mkdir "$lock_dir" 2>/dev/null; then locked=true; break; fi
|
|
388
|
+
sleep 0.1
|
|
389
|
+
done
|
|
390
|
+
if [[ "$locked" != "true" ]]; then
|
|
391
|
+
log_warn "Cost tracking lock busy for 5s (stale ${lock_dir}?), proceeding unlocked"
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
_track_session_cost_update "$session_id" "$cost" || true
|
|
395
|
+
|
|
396
|
+
if [[ "$locked" == "true" ]]; then
|
|
397
|
+
rmdir "$lock_dir" 2>/dev/null || true
|
|
398
|
+
fi
|
|
399
|
+
return 0
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
# Inner update for track_session_cost — runs with the lock held.
|
|
403
|
+
# Never propagates failure; logs a warning and returns instead.
|
|
404
|
+
_track_session_cost_update() {
|
|
405
|
+
local session_id="$1"
|
|
406
|
+
local cost="$2"
|
|
371
407
|
local timestamp=$(date -Iseconds)
|
|
372
408
|
|
|
373
|
-
#
|
|
409
|
+
# A corrupt file (truncated write, interleaved concurrent update) would
|
|
410
|
+
# fail every future jq update and never self-heal: set it aside and reset
|
|
411
|
+
if [[ -f "$COST_TRACKING_FILE" ]]; then
|
|
412
|
+
if ! jq empty "$COST_TRACKING_FILE" 2>/dev/null; then
|
|
413
|
+
log_warn "Cost tracking file corrupt, resetting (backup: ${COST_TRACKING_FILE}.corrupt)"
|
|
414
|
+
mv -f "$COST_TRACKING_FILE" "${COST_TRACKING_FILE}.corrupt" 2>/dev/null || true
|
|
415
|
+
fi
|
|
416
|
+
fi
|
|
374
417
|
if [[ ! -f "$COST_TRACKING_FILE" ]]; then
|
|
375
|
-
echo '{"sessions": [], "total_cost": 0}' > "$COST_TRACKING_FILE"
|
|
418
|
+
if ! { echo '{"sessions": [], "total_cost": 0}' > "$COST_TRACKING_FILE"; } 2>/dev/null; then
|
|
419
|
+
log_warn "Cost tracking skipped: cannot write $COST_TRACKING_FILE"
|
|
420
|
+
return 0
|
|
421
|
+
fi
|
|
376
422
|
fi
|
|
377
423
|
|
|
378
|
-
#
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
424
|
+
# Unique temp file: even an unlocked writer must not share a fixed .tmp
|
|
425
|
+
# sibling with other runs (lost records, failed mv)
|
|
426
|
+
local tmp_file
|
|
427
|
+
if ! tmp_file=$(mktemp "${COST_TRACKING_FILE}.XXXXXX" 2>/dev/null); then
|
|
428
|
+
log_warn "Cost tracking skipped: cannot create temp file for $COST_TRACKING_FILE"
|
|
429
|
+
return 0
|
|
430
|
+
fi
|
|
431
|
+
if jq --arg id "$session_id" \
|
|
432
|
+
--arg cost "$cost" \
|
|
433
|
+
--arg ts "$timestamp" \
|
|
434
|
+
'.sessions += [{id: $id, cost: ($cost | tonumber), timestamp: $ts}] | .total_cost += ($cost | tonumber)' \
|
|
435
|
+
"$COST_TRACKING_FILE" > "$tmp_file" 2>/dev/null; then
|
|
436
|
+
mv -f "$tmp_file" "$COST_TRACKING_FILE" 2>/dev/null || rm -f "$tmp_file"
|
|
437
|
+
else
|
|
438
|
+
rm -f "$tmp_file"
|
|
439
|
+
log_warn "Cost tracking update failed for session $session_id"
|
|
440
|
+
fi
|
|
441
|
+
return 0
|
|
385
442
|
}
|
|
386
443
|
|
|
387
444
|
# Get total tracked cost
|
package/scripts/test_suite.sh
CHANGED
|
@@ -1011,6 +1011,43 @@ test_cost_per1k_contract() {
|
|
|
1011
1011
|
if [[ -n "$saved" ]]; then COST_RATES_FILE="$saved"; else unset COST_RATES_FILE; fi
|
|
1012
1012
|
}
|
|
1013
1013
|
|
|
1014
|
+
# Fresh-install regression: track_session_cost wrote to $COST_TRACKING_FILE
|
|
1015
|
+
# without creating its parent directory, so on a fresh install (XDG data dir
|
|
1016
|
+
# ~/.local/share/ai-consultants absent) every session ended with
|
|
1017
|
+
# "No such file or directory" and cumulative cost tracking was lost.
|
|
1018
|
+
# Also covers the corrupt-file self-heal path (a bad costs.json previously
|
|
1019
|
+
# wedged every future update).
|
|
1020
|
+
test_cost_tracking_resilience() {
|
|
1021
|
+
suite "costs.sh: track_session_cost resilience (fresh-install + corrupt-file)"
|
|
1022
|
+
|
|
1023
|
+
local saved="$COST_TRACKING_FILE"
|
|
1024
|
+
COST_TRACKING_FILE="$TEST_TMPDIR/cost_tracking/nonexistent/nested/costs.json"
|
|
1025
|
+
|
|
1026
|
+
# Calls are guarded with || true: a recurrence of the guarded bugs must
|
|
1027
|
+
# surface as FAIL assertions below, not abort the suite under set -e
|
|
1028
|
+
track_session_cost "test_session_1" "0.25" 2>/dev/null || true
|
|
1029
|
+
|
|
1030
|
+
# 0.25 and 0.5 are exact in binary floating point, so the jq sum is exact
|
|
1031
|
+
assert_exit_code_success "costs.json created under a missing parent dir" test -f "$COST_TRACKING_FILE"
|
|
1032
|
+
assert_equals "0.25" "$(get_total_tracked_cost)" "session cost recorded on fresh install"
|
|
1033
|
+
|
|
1034
|
+
# Second call must append, not re-initialize
|
|
1035
|
+
track_session_cost "test_session_2" "0.5" 2>/dev/null || true
|
|
1036
|
+
assert_equals "0.75" "$(get_total_tracked_cost)" "second session accumulates on existing file"
|
|
1037
|
+
|
|
1038
|
+
# A corrupt costs.json must be set aside and reset, not wedge every
|
|
1039
|
+
# future update. The dir is created explicitly: this block tests the
|
|
1040
|
+
# self-heal path, not dir creation, and the setup write must not be able
|
|
1041
|
+
# to abort the suite under set -e
|
|
1042
|
+
mkdir -p "$(dirname "$COST_TRACKING_FILE")"
|
|
1043
|
+
echo '{"sessions": [' > "$COST_TRACKING_FILE"
|
|
1044
|
+
track_session_cost "test_session_3" "0.5" 2>/dev/null || true
|
|
1045
|
+
assert_equals "0.5" "$(get_total_tracked_cost)" "corrupt costs.json is reset and the new session recorded"
|
|
1046
|
+
assert_exit_code_success "corrupt file preserved as .corrupt backup" test -f "${COST_TRACKING_FILE}.corrupt"
|
|
1047
|
+
|
|
1048
|
+
COST_TRACKING_FILE="$saved"
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1014
1051
|
# =============================================================================
|
|
1015
1052
|
# TESTS: costs.sh - Cost formatting
|
|
1016
1053
|
# =============================================================================
|
|
@@ -1616,6 +1653,7 @@ main() {
|
|
|
1616
1653
|
test_cost_rates
|
|
1617
1654
|
test_cost_estimation
|
|
1618
1655
|
test_cost_per1k_contract
|
|
1656
|
+
test_cost_tracking_resilience
|
|
1619
1657
|
test_cost_formatting
|
|
1620
1658
|
test_budget_checking
|
|
1621
1659
|
test_query_complexity
|