diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index df1cd730..6b5724f5 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ { "name": "oddyssey", "description": "Observability-Driven Development for CLI coding agents: OTel expert agent, run-observation agent, verification pass, local Grafana stack via MCP", - "version": "1.12.2", + "version": "1.13.0", "category": "Observability", "source": "./marketplace/oddyssey" } diff --git a/CHANGELOG.md b/CHANGELOG.md index b37351b3..377c2a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## [1.13.0] - 2026-09-19 + +### 🚀 Features + +- *(harness)* One observation depth - drop quick, drop the depth field, older reports' depth ignored (#620) +- *(mcp)* Environment-scoped stack_config - env-stack keys next to stack keys, an optional environment field selects the effective entry (#621) +- *(mcp)* Grafana persists the gcx context the runs use - stack_config.grafana.context, proved by the one config check (#622) +- *(bench)* Launch-llms-benchmark runs each model twice and keeps the better run for the table (#634) + +### 🐛 Bug Fixes + +- *(ci)* Pin every job's runs-on to ubuntu-26.04 - the ubuntu-latest label migrates to Ubuntu 26.04 from 2026-10-19 (#616) + +### 📚 Documentation + +- *(agents)* A separate reviewer sub-agent checks every branch before its PR, fixes loop back until green (#611) +- *(odd)* Verify oddyssey-mcp after the environment-scoped stack_config and the grafana context - clear the loop's warning (#624) +- *(readme)* The APM install and update commands install the latest 1.x release (#626) +- *(readme)* The APM commands install and update to the latest release (#627) +- *(readme)* The APM update command names the package, the release range stays in the manifest (#628) + +### ⚙️ Miscellaneous Tasks + +- *(ci)* Odd-status job on oddyssey-actions v1.3.0 - one minimum oddyssey version checked by every setup, the verdict read from the package only (#613) +- *(bench)* Llms-benchmark row for z-ai/glm-5.3-flashx on opencode (#630) +- *(harness)* Test-plugin-harnessing defaults to deepseek/deepseek-v4.1-flash (#632) ## [1.12.2] - 2026-09-17 ### 🚀 Features diff --git a/apm.yml b/apm.yml index 06ed6867..c1062b14 100644 --- a/apm.yml +++ b/apm.yml @@ -1,5 +1,5 @@ name: oddyssey -version: 1.12.2 +version: 1.13.0 license: MIT description: Observability-Driven Development for CLI coding agents displayName: Oddyssey @@ -27,7 +27,7 @@ dependencies: args: - "--refresh-package" - "oddyssey-mcp" - - "oddyssey-mcp==1.12.2" + - "oddyssey-mcp==1.13.0" # `apm pack` compiles this block into the native marketplace manifests # (.claude-plugin/marketplace.json for Claude Code / Copilot CLI / Kimi @@ -45,5 +45,5 @@ marketplace: - name: oddyssey description: "Observability-Driven Development for CLI coding agents: OTel expert agent, run-observation agent, verification pass, local Grafana stack via MCP" source: ./marketplace/oddyssey - version: 1.12.2 + version: 1.13.0 category: Observability diff --git a/marketplace/oddyssey/.claude-plugin/plugin.json b/marketplace/oddyssey/.claude-plugin/plugin.json index 88d758da..20e61be4 100644 --- a/marketplace/oddyssey/.claude-plugin/plugin.json +++ b/marketplace/oddyssey/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "oddyssey", - "version": "1.12.2", + "version": "1.13.0", "description": "Observability-Driven Development for CLI coding agents", "author": { "name": "using-system", diff --git a/marketplace/oddyssey/.mcp.json b/marketplace/oddyssey/.mcp.json index 930fed8e..9e352024 100644 --- a/marketplace/oddyssey/.mcp.json +++ b/marketplace/oddyssey/.mcp.json @@ -3,7 +3,7 @@ "oddyssey": { "type": "stdio", "command": "uvx", - "args": ["--refresh-package", "oddyssey-mcp", "oddyssey-mcp==1.12.2"] + "args": ["--refresh-package", "oddyssey-mcp", "oddyssey-mcp==1.13.0"] } } } diff --git a/marketplace/oddyssey/agents/observe-run.agent.md b/marketplace/oddyssey/agents/observe-run.agent.md index 20f9d195..3b40d417 100644 --- a/marketplace/oddyssey/agents/observe-run.agent.md +++ b/marketplace/oddyssey/agents/observe-run.agent.md @@ -124,20 +124,11 @@ the report. minutes spent waiting belong to the run record's `Watch:` line. - **Focus** — performance, errors, correctness, cost/cardinality, a named endpoint, or a full sweep (default: full sweep). -- **Depth** — `quick` or `full`: how far the mission goes, the second - axis next to `mode` (who generates the traffic) — the two compose. - `full` is the whole protocol below; `quick` is the bounded version - the **Depth** section defines. Default when the mission block is - silent: `full`. The callers resolve it: `/odd-observe` from the - user's phrasing (asking only when it carries no signal), `/odd-verify` - from the baseline's `depth` frontmatter (`quick` when an observation - baseline predates the field, stated as defaulted; `full` for an - instrumentation baseline, whose presence rulings span every signal). - The existing `focus` is the intermediate dial and applies at both - depths. - **Preflight** — optional: the `Preflight:` handoff block the caller's - `backend-configuration` `## Check` closed with (stack, backend, - reference read, CLI and context, target values, connection proof + `backend-configuration` `## Check` closed with (stack, the configured + environment, backend, + reference read, CLI and context, the `stack_config` entry the values + came from and the values, connection proof with its UTC). It is **conversation-scope**: section 1 restates the stack and backend, never the block — a real tenant, workspace or site name, a GUID, a login, a home-directory path, a value persisted @@ -340,17 +331,14 @@ run record. decide: a script running them concurrently answers in seconds what spelling them out call by call costs in minutes, and its synthesis is what the later steps read. Absent such a script, probe each signal - yourself (at `quick` depth, for the signals the mission queries — the - Depth section — the others are neither probed nor reported absent): + yourself: - **traces** — a search scoped to the service returns traces; - **metrics** — the service's own series/dimensions exist (discovery, not liveness probes: on push-based pipelines an absent scrape-style `up` series proves nothing); - **logs** — a stream or index carries the service. - If **no** signal carries a named service, stop (at `quick` depth, - only after probing the unqueried signals once — the Depth section): - report which signals are + If **no** signal carries a named service, stop: report which signals are absent, whether the process is reachable at all — and on the local stack, whether the service's configured export endpoint matches the effective ports (`odd_config_get`): a divergence is the likely cause, @@ -412,71 +400,20 @@ run record. report" in section 1 and fall back to the within-run baseline. When the mission hands you a **baseline environment** to compare - against (verify missions), that comparison is yours: matching, carry + against (verify missions), or the preflight handoff carries a + **configured** one (the `environment=` of its `Preflight:` line, + other than `none`), that comparison is yours: matching, carry on; diverging, **stop hard** — no verdict is ever ruled across - environments. Name both values (baseline `prod`, detected `uat`) and + environments, and an entry persisted for one environment never + answers for another. Name both values (baseline `prod`, detected + `uat`; configured `prod`, detected `dev`) and recommend rerunning against the baseline's environment, or observing - the detected one as a new baseline. A baseline carrying no + the detected one as a new baseline — for a configured one, + retargeting the configuration, or observing the detected environment + as its own. A baseline carrying no environment (an instrumentation report has none by design) skips the check — record the detected environment fresh. -## Depth - -`full` is everything this file says. `quick` bounds it — a one-question -mission answered under five minutes on the local stack (a target, not -a contract), never a cheaper way to write a full report: - -- **Signals** — always: traces, plus the metrics the per-operation - table comes from — the span-derived ones when the backend derives - them, otherwise the service's own, and from the spans themselves when - neither exists, said in section 1. Then whatever the focus touches - (`errors` → the logs too; a profile question → the profiles). The - others are **not queried**: section 5 says - `not queried (quick): logs, profiles` — a statement about the - mission, never a gap of the service — and, on a backend that cannot - serve a signal, `not served: profiles` next to it; the service - preflight (Setup step 3) covers the queried signals only. -- **Stops and the environment are never ruled on the subset.** Before - a "no telemetry" stop (Setup step 3) or an `unknown` environment - (Setup step 4), probe the unqueried signals once: a service silent - on traces but alive on metrics or logs is reported as such ("no - traces in the window; metrics present, not analyzed (quick) — rerun - at `full` to rule the service silent"), never handed to the - `otel-instrumentation-expert` agent off an unprobed subset — and - `environment` is a durable frontmatter value the recall matches on, - so it is detected on every signal, at both depths. -- **Exemplars** — one trace per operation: the worst-duration one, - through the bounded search below (one p99 predicate, one fallback - list); a p50 or error exemplar only when the question is about it. -- **Cross-confirmation** — only for the anomalies you report as - `confirmed`; the rest stay single-signal, marked `suspected` with - the probe that would confirm them, and that is the expected shape - at this depth. -- **Report** — the seven headings stay (the recall reads by section - number), and section 8 with them on a custom stack, complete at - both depths. Sections 1, 2 and 7 are complete. Section 3 is the ranked - table only, no detail per row — a verify or re-measure keeps its - baseline-ruling table above it whole, one row per baseline finding, - `not ruled (quick)` where the queried signals could not rule. Sections 4 and 6 are one line each; - section 5 is its `not queried (quick)` line, then one bullet per gap - the queried signals showed. Section 7 carries the checks this run - measured, and only those: a quick report is a legal baseline for a - later verify, on exactly what it measured. -- **Verify at quick depth** — rule on every check, anomaly and gap of - the baseline that the queried signals can rule; every other item - reads `not ruled (quick)`, and the headline counts them - (`N of M checks ruled`). Never guess a ruling from an unqueried - signal. The other way round — a `full` replay of a `quick` - baseline — rules every check the baseline carries and says the - baseline's coverage was quick: the check count is the baseline's, - not the protocol's. - -Everything else — the clean base, the flush wait, the discover-first -rule, evidence over adjectives, the no-secrets rule, the frontmatter -— is identical at both depths. `depth` is a frontmatter field: the -persistence skill records it, the recall reads it (a full mission never -takes a quick report as its baseline without saying so). - ## Investigation The backend's reference file in `observability-cli-guides` carries the @@ -673,8 +610,7 @@ Then go from aggregates to explanations: - **Exemplars** — for each operation that matters, fetch three traces: one p50-representative, the worst-duration one, and an error one if errors - exist (at `quick` depth, the worst-duration one only — the Depth - section). When the backend's reference ships this whole step as a + exist. When the backend's reference ships this whole step as a script — one that ranks a service's operations from the window's traces, picks their p50 and worst exemplars, fetches them concurrently and summarises them — that invocation is the step, searches and fetches @@ -754,9 +690,7 @@ Then go from aggregates to explanations: - **Cross-signal** — a slow trace names the span, the span's window narrows the metric query, the trace ID filters the logs. Every anomaly ends up either cross-confirmed in a second signal or explicitly labeled - single-signal (at `quick` depth, only the anomalies you report as - `confirmed` are cross-confirmed; the others are `suspected` by - construction). + single-signal. ## When `gen_ai.*` spans exist in the window @@ -814,12 +748,6 @@ rest of section 2. loop; content on span attributes where the mission said it must stay off is a finding for section 3, not a gap. -At `quick` depth the per-model table comes from the traces the run -already reads; the agent-loop reading runs only when the focus asks for -it, and when it does not, section 5's not-queried statement names it -with the signals — `not queried (quick): ..., the agent loop` — a -statement about the mission, never a gap of the service. - ## What the run reports about a custom stack A custom stack is authored and fixed by one prompt, @@ -861,15 +789,14 @@ The report file is the persistence script's: `odd-memory`'s flag surface included, in its `## The script owns the format` — read that section, never `--help` (it answers nothing the section does not) — and run it with the run's values (the services, the stack, the -detected environment, the mode, the depth, the window, the run name, +detected environment, the mode, the window, the run name, the replayed report, the identity, and `--custom-stack` when the handoff names a custom stack). It prints the path of the file it wrote, then the file's body: the title, a `` for the one-line headline, then the seven headings — eight on a custom stack — each followed by a ``. Write that body, filled, to a **draft file of -your own** with your file tool — the sections in this order (at -`quick` depth, in the collapsed shape the Depth section gives sections -3 to 6; section 8 complete at both depths), every `` replaced, +your own** with your file tool — the sections in this order, every +`` replaced, the headings kept — never open, read or edit the report file itself, never rewrite its frontmatter — then run the reference's `persist --body ` on the path: it writes the @@ -884,7 +811,6 @@ What each of the sections carries is stated once, in that reference's `## The body` — `new` prints it after the skeleton, so it is read there, at report time, never earlier and never from the file: the Investigation above is what fills them, -the Depth section is what collapses sections 3 to 6 at `quick` depth, and the section above is what fills section 8 on a custom stack. ## Rules @@ -917,9 +843,6 @@ and the section above is what fills section 8 on a custom stack. finding against the baseline, never a value in your report. - Every anomaly is either cross-confirmed in a second signal or explicitly labeled single-signal. -- The depth bounds how far you look, never how honestly you report: - a quick report says what it did not query and did not rule, in the - words the Depth section fixes, so nobody mistakes it for a full one. - A load generator's own telemetry is never a named service: when k6's OpenTelemetry output lands in the store (`service_name="k6"` on the local stack), it is a bonus signal to cross-confirm the target's @@ -942,10 +865,7 @@ and the section above is what fills section 8 on a custom stack. step 5) — never once per query, never once per request batch. - Before returning the report, self-check: every named service was preflighted; all four signals were queried or their absence recorded in - section 5 — at `quick` depth, queried or listed as - `not queried (quick)` there; the depth appears in section 1 and in - the frontmatter, and a quick verify counts the items it did not rule; - in a verify or re-measure, section 3 opens with one ruling row per + section 5; in a verify or re-measure, section 3 opens with one ruling row per baseline finding, none missing, each keyed by the baseline's own id, and this run's own findings carry new identifiers; every table row and every finding carries its query and diff --git a/marketplace/oddyssey/com.github.copilot/agents/observe-run.agent.md b/marketplace/oddyssey/com.github.copilot/agents/observe-run.agent.md index 20f9d195..3b40d417 100644 --- a/marketplace/oddyssey/com.github.copilot/agents/observe-run.agent.md +++ b/marketplace/oddyssey/com.github.copilot/agents/observe-run.agent.md @@ -124,20 +124,11 @@ the report. minutes spent waiting belong to the run record's `Watch:` line. - **Focus** — performance, errors, correctness, cost/cardinality, a named endpoint, or a full sweep (default: full sweep). -- **Depth** — `quick` or `full`: how far the mission goes, the second - axis next to `mode` (who generates the traffic) — the two compose. - `full` is the whole protocol below; `quick` is the bounded version - the **Depth** section defines. Default when the mission block is - silent: `full`. The callers resolve it: `/odd-observe` from the - user's phrasing (asking only when it carries no signal), `/odd-verify` - from the baseline's `depth` frontmatter (`quick` when an observation - baseline predates the field, stated as defaulted; `full` for an - instrumentation baseline, whose presence rulings span every signal). - The existing `focus` is the intermediate dial and applies at both - depths. - **Preflight** — optional: the `Preflight:` handoff block the caller's - `backend-configuration` `## Check` closed with (stack, backend, - reference read, CLI and context, target values, connection proof + `backend-configuration` `## Check` closed with (stack, the configured + environment, backend, + reference read, CLI and context, the `stack_config` entry the values + came from and the values, connection proof with its UTC). It is **conversation-scope**: section 1 restates the stack and backend, never the block — a real tenant, workspace or site name, a GUID, a login, a home-directory path, a value persisted @@ -340,17 +331,14 @@ run record. decide: a script running them concurrently answers in seconds what spelling them out call by call costs in minutes, and its synthesis is what the later steps read. Absent such a script, probe each signal - yourself (at `quick` depth, for the signals the mission queries — the - Depth section — the others are neither probed nor reported absent): + yourself: - **traces** — a search scoped to the service returns traces; - **metrics** — the service's own series/dimensions exist (discovery, not liveness probes: on push-based pipelines an absent scrape-style `up` series proves nothing); - **logs** — a stream or index carries the service. - If **no** signal carries a named service, stop (at `quick` depth, - only after probing the unqueried signals once — the Depth section): - report which signals are + If **no** signal carries a named service, stop: report which signals are absent, whether the process is reachable at all — and on the local stack, whether the service's configured export endpoint matches the effective ports (`odd_config_get`): a divergence is the likely cause, @@ -412,71 +400,20 @@ run record. report" in section 1 and fall back to the within-run baseline. When the mission hands you a **baseline environment** to compare - against (verify missions), that comparison is yours: matching, carry + against (verify missions), or the preflight handoff carries a + **configured** one (the `environment=` of its `Preflight:` line, + other than `none`), that comparison is yours: matching, carry on; diverging, **stop hard** — no verdict is ever ruled across - environments. Name both values (baseline `prod`, detected `uat`) and + environments, and an entry persisted for one environment never + answers for another. Name both values (baseline `prod`, detected + `uat`; configured `prod`, detected `dev`) and recommend rerunning against the baseline's environment, or observing - the detected one as a new baseline. A baseline carrying no + the detected one as a new baseline — for a configured one, + retargeting the configuration, or observing the detected environment + as its own. A baseline carrying no environment (an instrumentation report has none by design) skips the check — record the detected environment fresh. -## Depth - -`full` is everything this file says. `quick` bounds it — a one-question -mission answered under five minutes on the local stack (a target, not -a contract), never a cheaper way to write a full report: - -- **Signals** — always: traces, plus the metrics the per-operation - table comes from — the span-derived ones when the backend derives - them, otherwise the service's own, and from the spans themselves when - neither exists, said in section 1. Then whatever the focus touches - (`errors` → the logs too; a profile question → the profiles). The - others are **not queried**: section 5 says - `not queried (quick): logs, profiles` — a statement about the - mission, never a gap of the service — and, on a backend that cannot - serve a signal, `not served: profiles` next to it; the service - preflight (Setup step 3) covers the queried signals only. -- **Stops and the environment are never ruled on the subset.** Before - a "no telemetry" stop (Setup step 3) or an `unknown` environment - (Setup step 4), probe the unqueried signals once: a service silent - on traces but alive on metrics or logs is reported as such ("no - traces in the window; metrics present, not analyzed (quick) — rerun - at `full` to rule the service silent"), never handed to the - `otel-instrumentation-expert` agent off an unprobed subset — and - `environment` is a durable frontmatter value the recall matches on, - so it is detected on every signal, at both depths. -- **Exemplars** — one trace per operation: the worst-duration one, - through the bounded search below (one p99 predicate, one fallback - list); a p50 or error exemplar only when the question is about it. -- **Cross-confirmation** — only for the anomalies you report as - `confirmed`; the rest stay single-signal, marked `suspected` with - the probe that would confirm them, and that is the expected shape - at this depth. -- **Report** — the seven headings stay (the recall reads by section - number), and section 8 with them on a custom stack, complete at - both depths. Sections 1, 2 and 7 are complete. Section 3 is the ranked - table only, no detail per row — a verify or re-measure keeps its - baseline-ruling table above it whole, one row per baseline finding, - `not ruled (quick)` where the queried signals could not rule. Sections 4 and 6 are one line each; - section 5 is its `not queried (quick)` line, then one bullet per gap - the queried signals showed. Section 7 carries the checks this run - measured, and only those: a quick report is a legal baseline for a - later verify, on exactly what it measured. -- **Verify at quick depth** — rule on every check, anomaly and gap of - the baseline that the queried signals can rule; every other item - reads `not ruled (quick)`, and the headline counts them - (`N of M checks ruled`). Never guess a ruling from an unqueried - signal. The other way round — a `full` replay of a `quick` - baseline — rules every check the baseline carries and says the - baseline's coverage was quick: the check count is the baseline's, - not the protocol's. - -Everything else — the clean base, the flush wait, the discover-first -rule, evidence over adjectives, the no-secrets rule, the frontmatter -— is identical at both depths. `depth` is a frontmatter field: the -persistence skill records it, the recall reads it (a full mission never -takes a quick report as its baseline without saying so). - ## Investigation The backend's reference file in `observability-cli-guides` carries the @@ -673,8 +610,7 @@ Then go from aggregates to explanations: - **Exemplars** — for each operation that matters, fetch three traces: one p50-representative, the worst-duration one, and an error one if errors - exist (at `quick` depth, the worst-duration one only — the Depth - section). When the backend's reference ships this whole step as a + exist. When the backend's reference ships this whole step as a script — one that ranks a service's operations from the window's traces, picks their p50 and worst exemplars, fetches them concurrently and summarises them — that invocation is the step, searches and fetches @@ -754,9 +690,7 @@ Then go from aggregates to explanations: - **Cross-signal** — a slow trace names the span, the span's window narrows the metric query, the trace ID filters the logs. Every anomaly ends up either cross-confirmed in a second signal or explicitly labeled - single-signal (at `quick` depth, only the anomalies you report as - `confirmed` are cross-confirmed; the others are `suspected` by - construction). + single-signal. ## When `gen_ai.*` spans exist in the window @@ -814,12 +748,6 @@ rest of section 2. loop; content on span attributes where the mission said it must stay off is a finding for section 3, not a gap. -At `quick` depth the per-model table comes from the traces the run -already reads; the agent-loop reading runs only when the focus asks for -it, and when it does not, section 5's not-queried statement names it -with the signals — `not queried (quick): ..., the agent loop` — a -statement about the mission, never a gap of the service. - ## What the run reports about a custom stack A custom stack is authored and fixed by one prompt, @@ -861,15 +789,14 @@ The report file is the persistence script's: `odd-memory`'s flag surface included, in its `## The script owns the format` — read that section, never `--help` (it answers nothing the section does not) — and run it with the run's values (the services, the stack, the -detected environment, the mode, the depth, the window, the run name, +detected environment, the mode, the window, the run name, the replayed report, the identity, and `--custom-stack` when the handoff names a custom stack). It prints the path of the file it wrote, then the file's body: the title, a `` for the one-line headline, then the seven headings — eight on a custom stack — each followed by a ``. Write that body, filled, to a **draft file of -your own** with your file tool — the sections in this order (at -`quick` depth, in the collapsed shape the Depth section gives sections -3 to 6; section 8 complete at both depths), every `` replaced, +your own** with your file tool — the sections in this order, every +`` replaced, the headings kept — never open, read or edit the report file itself, never rewrite its frontmatter — then run the reference's `persist --body ` on the path: it writes the @@ -884,7 +811,6 @@ What each of the sections carries is stated once, in that reference's `## The body` — `new` prints it after the skeleton, so it is read there, at report time, never earlier and never from the file: the Investigation above is what fills them, -the Depth section is what collapses sections 3 to 6 at `quick` depth, and the section above is what fills section 8 on a custom stack. ## Rules @@ -917,9 +843,6 @@ and the section above is what fills section 8 on a custom stack. finding against the baseline, never a value in your report. - Every anomaly is either cross-confirmed in a second signal or explicitly labeled single-signal. -- The depth bounds how far you look, never how honestly you report: - a quick report says what it did not query and did not rule, in the - words the Depth section fixes, so nobody mistakes it for a full one. - A load generator's own telemetry is never a named service: when k6's OpenTelemetry output lands in the store (`service_name="k6"` on the local stack), it is a bonus signal to cross-confirm the target's @@ -942,10 +865,7 @@ and the section above is what fills section 8 on a custom stack. step 5) — never once per query, never once per request batch. - Before returning the report, self-check: every named service was preflighted; all four signals were queried or their absence recorded in - section 5 — at `quick` depth, queried or listed as - `not queried (quick)` there; the depth appears in section 1 and in - the frontmatter, and a quick verify counts the items it did not rule; - in a verify or re-measure, section 3 opens with one ruling row per + section 5; in a verify or re-measure, section 3 opens with one ruling row per baseline finding, none missing, each keyed by the baseline's own id, and this run's own findings carry new identifiers; every table row and every finding carries its query and diff --git a/marketplace/oddyssey/com.github.copilot/commands/odd-config.md b/marketplace/oddyssey/com.github.copilot/commands/odd-config.md index 664aa3b3..5696a2b5 100644 --- a/marketplace/oddyssey/com.github.copilot/commands/odd-config.md +++ b/marketplace/oddyssey/com.github.copilot/commands/odd-config.md @@ -1,6 +1,6 @@ --- -description: "Display the current oddyssey backend configuration - configured stack, targeted instance, connection proof - then offer to change it: pick a backend from the full list, built-in or custom, and route the switch to the backend-configuration skill's Switch; persists a stack's targeting values. A custom stack is written by /odd-instrument-stack, never here" -argument-hint: "switch to (local | grafana | datadog | dynatrace | azure-monitor | cloudwatch | a custom stack) | persist for | clear for " +description: "Display the current oddyssey backend configuration - configured stack and environment, targeted instance, connection proof - then offer to change it: pick a backend from the full list, built-in or custom, and route the switch to the backend-configuration skill's Switch; targets a deployment environment; persists a stack's targeting values, per environment when one is named. A custom stack is written by /odd-instrument-stack, never here" +argument-hint: "switch to (local | grafana | datadog | dynatrace | azure-monitor | cloudwatch | a custom stack) [in ] | target | clear the environment | persist for [in ] | clear for [in ]" --- Answer "where do my missions point?" - and let the user change the @@ -10,17 +10,25 @@ picks a change. - Arguments: $ARGUMENTS - Expected fields (optional, free-form): a target backend (`switch to datadog`, `use the local stack`, `switch to seq` for a - custom stack the repository carries), a targeting value to + custom stack the repository carries), a deployment environment to + target or clear (`target prod`, `clear the environment`), a targeting + value to persist or clear (`persist workspace `, - `clear the workspace for azure-monitor`), an explicit local-port + `clear the workspace for azure-monitor`) - in an environment when + one is named (`switch to cloudwatch in prod`, `persist log group + for cloudwatch in prod`: the value lands in that environment's + entry of the stack, `prod-cloudwatch`), an explicit local-port change (`set the local Grafana port to 3001`). A request to create, complete, link or fix a custom stack is `/odd-instrument-stack`'s (the last section says so). No arguments = display first. -When the arguments already name a target backend or a persist or clear +When the arguments already name a target backend, an environment, or a +persist or clear request, skip the display-first flow and route straight to the `backend-configuration` skill's `## Switch` - it owns those entries: a named -backend runs the full switch, a bare targeting value enters at its +backend runs the full switch (with the environment, when one is +named), `target ` and `clear the environment` are its +switch step's `environment` write, a bare targeting value enters at its `stack_config` step and stands alone, and a clear is the same step's null write. The verification it ends with produces the display anyway, so nothing is lost by skipping ahead. A request to create, complete, @@ -37,15 +45,21 @@ result embeds the reset outcome). With no arguments, in this order: 1. **Display.** Run the `backend-configuration` skill's `## Check` for the - configured stack: the effective configuration in that backend's own + configured stack: the stack and the configured environment (none + when unset; on the local stack the environment is `local` by + construction and the field inert), the effective configuration in + that backend's own display shape (the `## Configuration display` section of its - `observability-cli-guides` reference), which instance, tenant, + `observability-cli-guides` reference) - the entry the pair resolves + to, named - which instance, tenant, or site the runs will hit, and the connection proof. Surface any `invalid_ignored` field `odd_config_get` reports - the stored value was tolerated but ignored, and only the user can say what they meant. Name the effect per field: a `local.*` port fell back to its default, a `stack_config` dotted name was simply dropped - nothing defaults it, - so it now reads as not persisted; a `stack` value no longer accepted + so it now reads as not persisted; an `environment` value was dropped + - the runs read the stack's plain entry until `target ` + sets it again; a `stack` value no longer accepted (a built-in the package removed, a custom stack whose declaration is gone) fell back to the default stack - say which stack the runs now hit, and offer the switch, or `/odd-instrument-stack create a stack @@ -57,14 +71,18 @@ With no arguments, in this order: observed repository carries (`.odd/observability-stacks/*/guide.md`, one line each, marked `custom`), with the current one marked, and the line "or `/odd-instrument-stack create a stack ` for a - backend not listed". Anything the user + backend not listed" - and, on a remote stack, "or `target + ` to read the values persisted for another + environment". Anything the user picks goes to the `backend-configuration` skill's `## Switch`, which owns the switch end to end: CLI presence preflight with a guided install offer, the contract check for a custom stack, the persisted - switch, the per-stack `stack_config` values, and the re-verification + switch, the environment, the `stack_config` values per stack and per + environment, and the re-verification through its `## Check`. -Displaying never writes configuration - not the stack, not a +Displaying never writes configuration - not the stack, not the +environment, not a `stack_config` value, not a port. A user who only wanted to look ends this prompt with exactly the configuration they started with. diff --git a/marketplace/oddyssey/com.github.copilot/commands/odd-observe.md b/marketplace/oddyssey/com.github.copilot/commands/odd-observe.md index 073d4ffa..beac4b4b 100644 --- a/marketplace/oddyssey/com.github.copilot/commands/odd-observe.md +++ b/marketplace/oddyssey/com.github.copilot/commands/odd-observe.md @@ -1,6 +1,6 @@ --- description: Observe a running service through its telemetry (local stack or remote backend) and get the plan-ready observation report -argument-hint: " [on ] [drive | observe | post-hoc] [quick | full] [run ] [window] [focus] [expectations]" +argument-hint: " [on ] [in ] [drive | observe | post-hoc] [run ] [window] [focus] [expectations]" --- Invoke the `observe-run` agent. It owns the whole method and the report @@ -32,8 +32,13 @@ backend's own configuration. no row is a custom stack's name when `.odd/observability-stacks//guide.md` exists in the observed repository - `` is the directory's name, the phrasing lowercased and kebab-cased ("my stack seq" is `seq`). A - phrasing on neither is a deployment-environment expectation ("on - prod"), never a switch - see below; when it reads as a stack's name + phrasing on neither is a deployment environment ("on prod", "in + prod"), never a stack switch: it is persisted the way a named stack + is - `odd_config_set {"environment": ""}`, kebab-case; on the + local stack the field is inert, persist nothing there - so the next + run starts from it and the preflight resolves that environment's + entry; and it stays an expectation for the detection - see below. + When it reads as a stack's name rather than a location word, list that directory and ask before reading it as an environment (a misspelled custom name is not an environment). A named stack is persisted so the next run starts @@ -71,20 +76,6 @@ backend's own configuration. account and no configuration), otherwise follow that reference's non-interactive path for the platform or hand the remaining steps to the user and stop. -4. Resolve the **depth** — `quick` or `full`, how far the mission goes - (the agent's Depth section) — from the arguments when they carry it, - on the user's phrasing in any language, the way step 1 resolves a - stack from the **Also called** column: "quick", "fast", "simple - report", "just check that ...", "a first look" resolve to `quick`; - "audit", "full sweep", "complete", "before the SDD wave" resolve to - `full` — examples, not a closed list. A resolved depth is stated in - the preflight's display and in the mission block, never asked again. - Only when the arguments carry no depth signal, ask the user — with - the host's structured-question tool when it has one (Claude Code: - `AskUserQuestion`) — `quick` first and marked recommended, and carry - the answer; when the service is missing too (the rule below), one - ask carries both questions, never two in a row. A service-less - discovery question (below) has no depth. Build the mission block from the arguments below; a field the arguments do not carry is left out - the agent applies its own default, and its @@ -103,8 +94,7 @@ file is not read here: - Arguments: $ARGUMENTS - Expected fields (any order, free-form): service name(s), stack (defaults to the configured one - the preflight resolved it), mode - (drive / observe / post-hoc), depth (quick / full - the preflight - resolved or asked it), benchmark, window, focus, baseline + (drive / observe / post-hoc), benchmark, window, focus, baseline expectations. - `benchmark` names a stored k6 benchmark - its directory name under `.odd/benchmarks/` or that path. `run .odd/benchmarks//` means @@ -115,10 +105,13 @@ file is not read here: arguments name one. - The deployment environment is not a mission field: the agent detects it from the telemetry (`deployment.environment.name`) and records it - - never pass one, never guess one here. When the arguments name one - ("on prod", "on uat"), it is neither the stack nor a mission input: - carry it into the baseline expectations, so the agent compares it - against the environment it detects and flags a divergence. + never pass one as a field, never guess one here. When the arguments + name one ("on prod", "in uat"), it is neither the stack nor a mission + input: step 1 persisted it, the preflight handoff carries it (the + `environment=` of its `Preflight:` line), and the agent compares the + environment it detects against it, stopping hard on a divergence - + carry it into the baseline expectations too, as the expectation it + is. - If no service name can be determined - no argument names one and no benchmark manifest supplies one - and the ask is an observation mission, ask for it before invoking the agent. A service-less diff --git a/marketplace/oddyssey/com.github.copilot/commands/odd-verify.md b/marketplace/oddyssey/com.github.copilot/commands/odd-verify.md index 7658ca08..32592d94 100644 --- a/marketplace/oddyssey/com.github.copilot/commands/odd-verify.md +++ b/marketplace/oddyssey/com.github.copilot/commands/odd-verify.md @@ -1,6 +1,6 @@ --- description: Verify that a fix or an implemented instrumentation plan landed - replay a stored report's protocol (observation or instrumentation report) and get the full observation report, carrying the verdict on everything it recorded - measurements, anomalies, telemetry gaps, or planned signals now present -argument-hint: "[ | my last report] [full | quick]" +argument-hint: "[ | my last report]" --- Invoke the `observe-run` agent. It owns the whole method and the report @@ -14,20 +14,20 @@ an **instrumentation report** (presence rulings on planned signals). `.odd/otel-instrumentation-reports/`, enough of a run name to find it, or constraints on the newest-first resolution ("my last report for checkout", "my last prod report", "my last report on seq": a - service, a deployment environment, a stack) - a depth ("full - verify", "quick check"), the carve-out "verify that verification's - own protocol", how the caller frames the mission (a verification, or + service, a deployment environment, a stack) - the carve-out "verify + that verification's own protocol", how the caller frames the mission + (a verification, or "nothing changed, re-measure"), and, when the report's stack is remote, the access material the agent will need. **Resolve the baseline first**, with the `odd-memory` skill's `observe-run-report` reference, `## Resolving a replay` - its -`baseline` command, the arguments' report, constraints and depth passed -as flags, nothing else. It settles what the inputs fix: the resolved +`baseline` command, the arguments' report and constraints passed as +flags, nothing else. It settles what the inputs fix: the resolved report, the baseline one hop away (or none under the carve-out), the `verifies` value, the services, the stack, the environment, the -execution mode the `verifies` chain reaches, the depth and its reason, -the revision, the benchmark the record names, the recorded target, and +execution mode the `verifies` chain reaches, the revision, the +benchmark the record names, the recorded target, and whether a drive needs the user's confirmation. An `ask:` line is a question only the user can answer - which report, which original, which mode - put it to them verbatim and stop until they answer; never @@ -36,12 +36,17 @@ guess past it. `nothing to verify` ends the mission. Preflight next - in the main conversation, before any dispatch, in this order: -1. **The stack.** The `stack` line is the contract being replayed. - When it disagrees with the configured stack (`odd_config_get`), +1. **The stack and the environment.** The `stack` and `environment` + lines are the contract being replayed. + When the pair disagrees with the configured one (`odd_config_get`'s + `stack` and `environment`), say so and **follow the report** - a verify run replays the - baseline's stack, never silently retargets the current one, and + baseline's pair, never silently retargets the current one, and never rewrites the configuration: the divergence is stated, not - persisted. That `stack` may be a custom name - a value on no row of + persisted; the entry the replay reads is the report's pair's, which + step 3's `## Check` resolves itself when the pair is not the + configured one (`effective` answers for the configured pair only), + and says so. That `stack` may be a custom name - a value on no row of `builtin-stacks.md`: step 3's `## Check` resolves it from `.odd/observability-stacks//guide.md` in this clone and, when that directory is absent, stops with the `observability-stack` @@ -83,10 +88,7 @@ this order: confirmation - k6 needs no account and no configuration), otherwise follow that reference's non-interactive path for the platform or hand the remaining steps to the user and stop. -5. **The depth.** State the `depth` line in the conversation, reason - included, before anything is dispatched - the run record must say - when the depth was defaulted. -6. **Verification or re-measure.** The same reference's `boundary` +5. **Verification or re-measure.** The same reference's `boundary` command on the baseline: `verification` when a runtime entry, an uncommitted change to one, or the benchmark the record names moved since the baseline's revision - the mission tests a fix; @@ -126,7 +128,7 @@ Then build the mission block: mode (never inferred from whether the report records a scenario or a benchmark - an `observe`-mode report backed by a stored benchmark records a replayable protocol nobody authorized this run to drive), - the depth, the benchmark when one is named, the baseline's path - + the benchmark when one is named, the baseline's path - the agent uses it as the recalled baseline - and its environment: the agent detects the environment of its own run, compares it against the one handed over, and owns the hard stop when the two @@ -136,7 +138,7 @@ Then build the mission block: - remote drive: the confirmation step 2 obtained is what authorizes a `drive` mission on a remote stack - state that it was given, and for what; -- verification or re-measure, from step 6, with what changed; the run +- verification or re-measure, from step 5, with what changed; the run persists through the reference's `new --mode verify` (or `--mode re-measure`) `--verifies ` - the script names the file and writes the frontmatter, this block restates none of it; diff --git a/marketplace/oddyssey/com.github.copilot/hooks/scripts/check_report_frontmatter.py b/marketplace/oddyssey/com.github.copilot/hooks/scripts/check_report_frontmatter.py index 7492c28d..f94a546b 100644 --- a/marketplace/oddyssey/com.github.copilot/hooks/scripts/check_report_frontmatter.py +++ b/marketplace/oddyssey/com.github.copilot/hooks/scripts/check_report_frontmatter.py @@ -8,7 +8,7 @@ wrote a file: when the file is a report under ``.odd/observe-run-reports/`` or ``.odd/otel-instrumentation-reports/`` it checks the filename shape, the required fields of the kind present -and non-empty, ``mode`` and ``depth`` among their values, ``window`` as +and non-empty, ``mode`` among its values, ``window`` as ``/`` in UTC with the end after the start, ``date`` matching the filename's date, ``run_name`` matching the filename's slug, and on a ``verify`` or ``re-measure`` report a ``verifies`` naming a stored @@ -19,11 +19,9 @@ The checker is the one the ``get-status`` skill runs over the stored history (``check_report`` in its ``odd_status.py``), copied here: a hook script imports nothing outside itself, and a test runs both over -one fixture set so the two copies cannot drift. Two intended -differences: ``depth`` - the status reads a report without it as a -legacy file that predates the field; a report being written now has no -such excuse - and an unreadable file - the status lists it as a -violation (``unreadable: ``); the hook fails open on it, the +one fixture set so the two copies cannot drift. One intended +difference: an unreadable file - the status lists it as a violation +(``unreadable: ``); the hook fails open on it, the filename-shape problem included, since a file it cannot read is not a report it can judge. @@ -206,7 +204,7 @@ def split_top_level(text: str, sep: str = ",") -> list[str]: the beginning of an item or right after a mapping colon - so an apostrophe inside a bare word is just a character. """ - parts, buf, depth, quote = [], [], 0, None + parts, buf, nesting, quote = [], [], 0, None for ch in text: if quote: buf.append(ch) @@ -216,10 +214,10 @@ def split_top_level(text: str, sep: str = ",") -> list[str]: if ch in ("'", '"') and scalar_can_start(buf): quote = ch elif ch in "[{": - depth += 1 + nesting += 1 elif ch in "]}": - depth -= 1 - if ch == sep and depth == 0: + nesting -= 1 + if ch == sep and nesting == 0: parts.append("".join(buf)) buf = [] else: @@ -343,15 +341,10 @@ def read_report(path: Path, kind: str) -> dict: DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") OBSERVATION_MODES_ALL = ("drive", "observe", "post-hoc", "verify", "re-measure") REPLAY_MODES = ("verify", "re-measure") -DEPTHS = ("quick", "full") def check_report(report: dict, stored_names: set[str], root: Path) -> list[str]: - """What the report lacks against the memory contract's frontmatter. - - The one line that differs from get-status's copy: ``depth`` is - required - a report written now does not predate the field. - """ + """What the report lacks against the memory contract's frontmatter.""" problems: list[str] = [] name = Path(report["path"]).name match = REPORT_NAME_RE.match(name) @@ -391,11 +384,6 @@ def scalar(key: str) -> str | None: problems.append( f"mode {mode!r} is not one of {list(OBSERVATION_MODES_ALL)}" ) - depth = fm.get("depth") - if depth is None: - problems.append("depth absent") - elif str(depth) not in DEPTHS: - problems.append(f"depth {str(depth)!r} is not one of {list(DEPTHS)}") window = values.get("window") if window is not None: wm = WINDOW_RE.match(window) diff --git a/marketplace/oddyssey/commands/odd-config.md b/marketplace/oddyssey/commands/odd-config.md index 664aa3b3..5696a2b5 100644 --- a/marketplace/oddyssey/commands/odd-config.md +++ b/marketplace/oddyssey/commands/odd-config.md @@ -1,6 +1,6 @@ --- -description: "Display the current oddyssey backend configuration - configured stack, targeted instance, connection proof - then offer to change it: pick a backend from the full list, built-in or custom, and route the switch to the backend-configuration skill's Switch; persists a stack's targeting values. A custom stack is written by /odd-instrument-stack, never here" -argument-hint: "switch to (local | grafana | datadog | dynatrace | azure-monitor | cloudwatch | a custom stack) | persist for | clear for " +description: "Display the current oddyssey backend configuration - configured stack and environment, targeted instance, connection proof - then offer to change it: pick a backend from the full list, built-in or custom, and route the switch to the backend-configuration skill's Switch; targets a deployment environment; persists a stack's targeting values, per environment when one is named. A custom stack is written by /odd-instrument-stack, never here" +argument-hint: "switch to (local | grafana | datadog | dynatrace | azure-monitor | cloudwatch | a custom stack) [in ] | target | clear the environment | persist for [in ] | clear for [in ]" --- Answer "where do my missions point?" - and let the user change the @@ -10,17 +10,25 @@ picks a change. - Arguments: $ARGUMENTS - Expected fields (optional, free-form): a target backend (`switch to datadog`, `use the local stack`, `switch to seq` for a - custom stack the repository carries), a targeting value to + custom stack the repository carries), a deployment environment to + target or clear (`target prod`, `clear the environment`), a targeting + value to persist or clear (`persist workspace `, - `clear the workspace for azure-monitor`), an explicit local-port + `clear the workspace for azure-monitor`) - in an environment when + one is named (`switch to cloudwatch in prod`, `persist log group + for cloudwatch in prod`: the value lands in that environment's + entry of the stack, `prod-cloudwatch`), an explicit local-port change (`set the local Grafana port to 3001`). A request to create, complete, link or fix a custom stack is `/odd-instrument-stack`'s (the last section says so). No arguments = display first. -When the arguments already name a target backend or a persist or clear +When the arguments already name a target backend, an environment, or a +persist or clear request, skip the display-first flow and route straight to the `backend-configuration` skill's `## Switch` - it owns those entries: a named -backend runs the full switch, a bare targeting value enters at its +backend runs the full switch (with the environment, when one is +named), `target ` and `clear the environment` are its +switch step's `environment` write, a bare targeting value enters at its `stack_config` step and stands alone, and a clear is the same step's null write. The verification it ends with produces the display anyway, so nothing is lost by skipping ahead. A request to create, complete, @@ -37,15 +45,21 @@ result embeds the reset outcome). With no arguments, in this order: 1. **Display.** Run the `backend-configuration` skill's `## Check` for the - configured stack: the effective configuration in that backend's own + configured stack: the stack and the configured environment (none + when unset; on the local stack the environment is `local` by + construction and the field inert), the effective configuration in + that backend's own display shape (the `## Configuration display` section of its - `observability-cli-guides` reference), which instance, tenant, + `observability-cli-guides` reference) - the entry the pair resolves + to, named - which instance, tenant, or site the runs will hit, and the connection proof. Surface any `invalid_ignored` field `odd_config_get` reports - the stored value was tolerated but ignored, and only the user can say what they meant. Name the effect per field: a `local.*` port fell back to its default, a `stack_config` dotted name was simply dropped - nothing defaults it, - so it now reads as not persisted; a `stack` value no longer accepted + so it now reads as not persisted; an `environment` value was dropped + - the runs read the stack's plain entry until `target ` + sets it again; a `stack` value no longer accepted (a built-in the package removed, a custom stack whose declaration is gone) fell back to the default stack - say which stack the runs now hit, and offer the switch, or `/odd-instrument-stack create a stack @@ -57,14 +71,18 @@ With no arguments, in this order: observed repository carries (`.odd/observability-stacks/*/guide.md`, one line each, marked `custom`), with the current one marked, and the line "or `/odd-instrument-stack create a stack ` for a - backend not listed". Anything the user + backend not listed" - and, on a remote stack, "or `target + ` to read the values persisted for another + environment". Anything the user picks goes to the `backend-configuration` skill's `## Switch`, which owns the switch end to end: CLI presence preflight with a guided install offer, the contract check for a custom stack, the persisted - switch, the per-stack `stack_config` values, and the re-verification + switch, the environment, the `stack_config` values per stack and per + environment, and the re-verification through its `## Check`. -Displaying never writes configuration - not the stack, not a +Displaying never writes configuration - not the stack, not the +environment, not a `stack_config` value, not a port. A user who only wanted to look ends this prompt with exactly the configuration they started with. diff --git a/marketplace/oddyssey/commands/odd-observe.md b/marketplace/oddyssey/commands/odd-observe.md index 073d4ffa..beac4b4b 100644 --- a/marketplace/oddyssey/commands/odd-observe.md +++ b/marketplace/oddyssey/commands/odd-observe.md @@ -1,6 +1,6 @@ --- description: Observe a running service through its telemetry (local stack or remote backend) and get the plan-ready observation report -argument-hint: " [on ] [drive | observe | post-hoc] [quick | full] [run ] [window] [focus] [expectations]" +argument-hint: " [on ] [in ] [drive | observe | post-hoc] [run ] [window] [focus] [expectations]" --- Invoke the `observe-run` agent. It owns the whole method and the report @@ -32,8 +32,13 @@ backend's own configuration. no row is a custom stack's name when `.odd/observability-stacks//guide.md` exists in the observed repository - `` is the directory's name, the phrasing lowercased and kebab-cased ("my stack seq" is `seq`). A - phrasing on neither is a deployment-environment expectation ("on - prod"), never a switch - see below; when it reads as a stack's name + phrasing on neither is a deployment environment ("on prod", "in + prod"), never a stack switch: it is persisted the way a named stack + is - `odd_config_set {"environment": ""}`, kebab-case; on the + local stack the field is inert, persist nothing there - so the next + run starts from it and the preflight resolves that environment's + entry; and it stays an expectation for the detection - see below. + When it reads as a stack's name rather than a location word, list that directory and ask before reading it as an environment (a misspelled custom name is not an environment). A named stack is persisted so the next run starts @@ -71,20 +76,6 @@ backend's own configuration. account and no configuration), otherwise follow that reference's non-interactive path for the platform or hand the remaining steps to the user and stop. -4. Resolve the **depth** — `quick` or `full`, how far the mission goes - (the agent's Depth section) — from the arguments when they carry it, - on the user's phrasing in any language, the way step 1 resolves a - stack from the **Also called** column: "quick", "fast", "simple - report", "just check that ...", "a first look" resolve to `quick`; - "audit", "full sweep", "complete", "before the SDD wave" resolve to - `full` — examples, not a closed list. A resolved depth is stated in - the preflight's display and in the mission block, never asked again. - Only when the arguments carry no depth signal, ask the user — with - the host's structured-question tool when it has one (Claude Code: - `AskUserQuestion`) — `quick` first and marked recommended, and carry - the answer; when the service is missing too (the rule below), one - ask carries both questions, never two in a row. A service-less - discovery question (below) has no depth. Build the mission block from the arguments below; a field the arguments do not carry is left out - the agent applies its own default, and its @@ -103,8 +94,7 @@ file is not read here: - Arguments: $ARGUMENTS - Expected fields (any order, free-form): service name(s), stack (defaults to the configured one - the preflight resolved it), mode - (drive / observe / post-hoc), depth (quick / full - the preflight - resolved or asked it), benchmark, window, focus, baseline + (drive / observe / post-hoc), benchmark, window, focus, baseline expectations. - `benchmark` names a stored k6 benchmark - its directory name under `.odd/benchmarks/` or that path. `run .odd/benchmarks//` means @@ -115,10 +105,13 @@ file is not read here: arguments name one. - The deployment environment is not a mission field: the agent detects it from the telemetry (`deployment.environment.name`) and records it - - never pass one, never guess one here. When the arguments name one - ("on prod", "on uat"), it is neither the stack nor a mission input: - carry it into the baseline expectations, so the agent compares it - against the environment it detects and flags a divergence. + never pass one as a field, never guess one here. When the arguments + name one ("on prod", "in uat"), it is neither the stack nor a mission + input: step 1 persisted it, the preflight handoff carries it (the + `environment=` of its `Preflight:` line), and the agent compares the + environment it detects against it, stopping hard on a divergence - + carry it into the baseline expectations too, as the expectation it + is. - If no service name can be determined - no argument names one and no benchmark manifest supplies one - and the ask is an observation mission, ask for it before invoking the agent. A service-less diff --git a/marketplace/oddyssey/commands/odd-verify.md b/marketplace/oddyssey/commands/odd-verify.md index 7658ca08..32592d94 100644 --- a/marketplace/oddyssey/commands/odd-verify.md +++ b/marketplace/oddyssey/commands/odd-verify.md @@ -1,6 +1,6 @@ --- description: Verify that a fix or an implemented instrumentation plan landed - replay a stored report's protocol (observation or instrumentation report) and get the full observation report, carrying the verdict on everything it recorded - measurements, anomalies, telemetry gaps, or planned signals now present -argument-hint: "[ | my last report] [full | quick]" +argument-hint: "[ | my last report]" --- Invoke the `observe-run` agent. It owns the whole method and the report @@ -14,20 +14,20 @@ an **instrumentation report** (presence rulings on planned signals). `.odd/otel-instrumentation-reports/`, enough of a run name to find it, or constraints on the newest-first resolution ("my last report for checkout", "my last prod report", "my last report on seq": a - service, a deployment environment, a stack) - a depth ("full - verify", "quick check"), the carve-out "verify that verification's - own protocol", how the caller frames the mission (a verification, or + service, a deployment environment, a stack) - the carve-out "verify + that verification's own protocol", how the caller frames the mission + (a verification, or "nothing changed, re-measure"), and, when the report's stack is remote, the access material the agent will need. **Resolve the baseline first**, with the `odd-memory` skill's `observe-run-report` reference, `## Resolving a replay` - its -`baseline` command, the arguments' report, constraints and depth passed -as flags, nothing else. It settles what the inputs fix: the resolved +`baseline` command, the arguments' report and constraints passed as +flags, nothing else. It settles what the inputs fix: the resolved report, the baseline one hop away (or none under the carve-out), the `verifies` value, the services, the stack, the environment, the -execution mode the `verifies` chain reaches, the depth and its reason, -the revision, the benchmark the record names, the recorded target, and +execution mode the `verifies` chain reaches, the revision, the +benchmark the record names, the recorded target, and whether a drive needs the user's confirmation. An `ask:` line is a question only the user can answer - which report, which original, which mode - put it to them verbatim and stop until they answer; never @@ -36,12 +36,17 @@ guess past it. `nothing to verify` ends the mission. Preflight next - in the main conversation, before any dispatch, in this order: -1. **The stack.** The `stack` line is the contract being replayed. - When it disagrees with the configured stack (`odd_config_get`), +1. **The stack and the environment.** The `stack` and `environment` + lines are the contract being replayed. + When the pair disagrees with the configured one (`odd_config_get`'s + `stack` and `environment`), say so and **follow the report** - a verify run replays the - baseline's stack, never silently retargets the current one, and + baseline's pair, never silently retargets the current one, and never rewrites the configuration: the divergence is stated, not - persisted. That `stack` may be a custom name - a value on no row of + persisted; the entry the replay reads is the report's pair's, which + step 3's `## Check` resolves itself when the pair is not the + configured one (`effective` answers for the configured pair only), + and says so. That `stack` may be a custom name - a value on no row of `builtin-stacks.md`: step 3's `## Check` resolves it from `.odd/observability-stacks//guide.md` in this clone and, when that directory is absent, stops with the `observability-stack` @@ -83,10 +88,7 @@ this order: confirmation - k6 needs no account and no configuration), otherwise follow that reference's non-interactive path for the platform or hand the remaining steps to the user and stop. -5. **The depth.** State the `depth` line in the conversation, reason - included, before anything is dispatched - the run record must say - when the depth was defaulted. -6. **Verification or re-measure.** The same reference's `boundary` +5. **Verification or re-measure.** The same reference's `boundary` command on the baseline: `verification` when a runtime entry, an uncommitted change to one, or the benchmark the record names moved since the baseline's revision - the mission tests a fix; @@ -126,7 +128,7 @@ Then build the mission block: mode (never inferred from whether the report records a scenario or a benchmark - an `observe`-mode report backed by a stored benchmark records a replayable protocol nobody authorized this run to drive), - the depth, the benchmark when one is named, the baseline's path - + the benchmark when one is named, the baseline's path - the agent uses it as the recalled baseline - and its environment: the agent detects the environment of its own run, compares it against the one handed over, and owns the hard stop when the two @@ -136,7 +138,7 @@ Then build the mission block: - remote drive: the confirmation step 2 obtained is what authorizes a `drive` mission on a remote stack - state that it was given, and for what; -- verification or re-measure, from step 6, with what changed; the run +- verification or re-measure, from step 5, with what changed; the run persists through the reference's `new --mode verify` (or `--mode re-measure`) `--verifies ` - the script names the file and writes the frontmatter, this block restates none of it; diff --git a/marketplace/oddyssey/hooks/scripts/check_report_frontmatter.py b/marketplace/oddyssey/hooks/scripts/check_report_frontmatter.py index 7492c28d..f94a546b 100644 --- a/marketplace/oddyssey/hooks/scripts/check_report_frontmatter.py +++ b/marketplace/oddyssey/hooks/scripts/check_report_frontmatter.py @@ -8,7 +8,7 @@ wrote a file: when the file is a report under ``.odd/observe-run-reports/`` or ``.odd/otel-instrumentation-reports/`` it checks the filename shape, the required fields of the kind present -and non-empty, ``mode`` and ``depth`` among their values, ``window`` as +and non-empty, ``mode`` among its values, ``window`` as ``/`` in UTC with the end after the start, ``date`` matching the filename's date, ``run_name`` matching the filename's slug, and on a ``verify`` or ``re-measure`` report a ``verifies`` naming a stored @@ -19,11 +19,9 @@ The checker is the one the ``get-status`` skill runs over the stored history (``check_report`` in its ``odd_status.py``), copied here: a hook script imports nothing outside itself, and a test runs both over -one fixture set so the two copies cannot drift. Two intended -differences: ``depth`` - the status reads a report without it as a -legacy file that predates the field; a report being written now has no -such excuse - and an unreadable file - the status lists it as a -violation (``unreadable: ``); the hook fails open on it, the +one fixture set so the two copies cannot drift. One intended +difference: an unreadable file - the status lists it as a violation +(``unreadable: ``); the hook fails open on it, the filename-shape problem included, since a file it cannot read is not a report it can judge. @@ -206,7 +204,7 @@ def split_top_level(text: str, sep: str = ",") -> list[str]: the beginning of an item or right after a mapping colon - so an apostrophe inside a bare word is just a character. """ - parts, buf, depth, quote = [], [], 0, None + parts, buf, nesting, quote = [], [], 0, None for ch in text: if quote: buf.append(ch) @@ -216,10 +214,10 @@ def split_top_level(text: str, sep: str = ",") -> list[str]: if ch in ("'", '"') and scalar_can_start(buf): quote = ch elif ch in "[{": - depth += 1 + nesting += 1 elif ch in "]}": - depth -= 1 - if ch == sep and depth == 0: + nesting -= 1 + if ch == sep and nesting == 0: parts.append("".join(buf)) buf = [] else: @@ -343,15 +341,10 @@ def read_report(path: Path, kind: str) -> dict: DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") OBSERVATION_MODES_ALL = ("drive", "observe", "post-hoc", "verify", "re-measure") REPLAY_MODES = ("verify", "re-measure") -DEPTHS = ("quick", "full") def check_report(report: dict, stored_names: set[str], root: Path) -> list[str]: - """What the report lacks against the memory contract's frontmatter. - - The one line that differs from get-status's copy: ``depth`` is - required - a report written now does not predate the field. - """ + """What the report lacks against the memory contract's frontmatter.""" problems: list[str] = [] name = Path(report["path"]).name match = REPORT_NAME_RE.match(name) @@ -391,11 +384,6 @@ def scalar(key: str) -> str | None: problems.append( f"mode {mode!r} is not one of {list(OBSERVATION_MODES_ALL)}" ) - depth = fm.get("depth") - if depth is None: - problems.append("depth absent") - elif str(depth) not in DEPTHS: - problems.append(f"depth {str(depth)!r} is not one of {list(DEPTHS)}") window = values.get("window") if window is not None: wm = WINDOW_RE.match(window) diff --git a/marketplace/oddyssey/mcp.json b/marketplace/oddyssey/mcp.json index daf9eccb..3c70bb03 100644 --- a/marketplace/oddyssey/mcp.json +++ b/marketplace/oddyssey/mcp.json @@ -4,7 +4,7 @@ "oddyssey": { "type": "stdio", "command": "uvx", - "args": ["--refresh-package", "oddyssey-mcp", "oddyssey-mcp==1.12.2"] + "args": ["--refresh-package", "oddyssey-mcp", "oddyssey-mcp==1.13.0"] } } } diff --git a/marketplace/oddyssey/plugin.json b/marketplace/oddyssey/plugin.json index 406ead48..43cf3a4c 100644 --- a/marketplace/oddyssey/plugin.json +++ b/marketplace/oddyssey/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "oddyssey", - "version": "1.12.2", + "version": "1.13.0", "description": "Observability-Driven Development for CLI coding agents", "author": { "name": "using-system", diff --git a/marketplace/oddyssey/skills/backend-configuration/SKILL.md b/marketplace/oddyssey/skills/backend-configuration/SKILL.md index 646b8f64..2553ea2d 100644 --- a/marketplace/oddyssey/skills/backend-configuration/SKILL.md +++ b/marketplace/oddyssey/skills/backend-configuration/SKILL.md @@ -10,8 +10,10 @@ interactive auth (OAuth device codes and SSO browser logins stall subagents — observed). This skill resolves the configured stack, shows which instance the runs will hit, proves the CLI is connected, and guides the user when it is not (`## Check`); and it owns the write — -the switch to another backend and the targeting values persisted for -it (`## Switch`). A switch is a write, and a write deserves a preflight: +the switch to another backend, the deployment environment the missions +target, and the targeting values persisted for +it — per environment when one is named or configured (`## Switch`). A +switch is a write, and a write deserves a preflight: a stack persisted for a CLI that is not on the machine turns every later mission into the same discovery, so the switch checks the binary first, writes second, and ends in `## Check`'s proof. @@ -89,12 +91,16 @@ the rest of this section is for. `odd_config_get` names the configured stack — a value `builtin-stacks.md` lists, or a custom stack whose declaration sits -under `custom` in the same result. When the mission or the -instructions name a different one, they win — whether the switch +under `custom` in the same result — and the configured `environment` +(null when none is set; inert on the local stack, which is the `local` +environment by construction). When the mission or the +instructions name a different one — a stack, an environment, or both +— they win — whether the switch persists is the **caller's call**: `odd-observe` persists it with `odd_config_set` so the next run starts from it; `odd-verify` states the divergence and does not persist (the stored report is the contract -it replays). Open the stack's row in `builtin-stacks.md` and, from it, +it replays — its stack and its environment alike). Open the stack's +row in `builtin-stacks.md` and, from it, the stack's reference file; a name on no row is a custom stack, its reference `.odd/observability-stacks//guide.md` in the observed repository (absent too: the error of `## Switch`'s step 1) — or, when @@ -124,8 +130,17 @@ below — display, probe, guidance — assumes a binary that runs. Then follow the reference's `### Display` — it says exactly what to display for that backend and where each value comes from, including -the persisted `stack_config` values from `odd_config_get` — that -stack's entry only, never the other stacks' values. Show that +the persisted `stack_config` values — the **effective entry** only, +never another stack's or another environment's. For the configured +pair, `odd_config_get`'s `effective` block resolves it: +`stack_config_key` names the entry (`-` when one +is persisted for the configured environment, the plain `` +otherwise — whole, never a merge of the two) and its `stack_config` is +the values; read them there, never composed from `stack_config` by +hand. For a replay's other pair (`odd-verify`, step 1) `effective` +does not apply: resolve the entry yourself from `stack_config` — +`-` when that key is present, else `` — and +say so in the display, next to the divergence. Show that configuration to the user **as-is, no confirmation needed** — it is informative: which instance, tenant, or site the queries are about to hit is exactly what a user wants to see before a run, and what catches a @@ -174,10 +189,10 @@ re-proves the connection — it reads the reference's other sections only: ```text -Preflight: stack=, backend= (custom)"> +Preflight: stack=, environment=, backend= (custom)"> Reference: ; read: CLI binary, Configuration display CLI: [, at when not on PATH]; context: -Target: +Target: entry=; Proof: -> , at Machine: ``` @@ -190,7 +205,11 @@ The script prints the line; the caller copies it, never composes it. The `Target:` line carries what the Display showed — the real targeting values the agent's queries need — and nothing more: never a -credential. The block is **conversation-scope**: a real tenant, +credential. Its `entry=` names the `stack_config` key the values came +from, and the `Preflight:` line's `environment=` the environment the +agent compares its detection against: it reads both there and never +re-reads the configuration for them. The block is +**conversation-scope**: a real tenant, workspace, subscription or site name, a GUID, a login, a path under a home directory all identify a real environment, and the agent's report is a committed file — section 1 restates the stack and backend, never @@ -287,6 +306,16 @@ run — `python3` missing, or an install that dropped the skill's `scripts/` — the switch stops there too, saying which of the two: an unchecked stack is not persisted, and the fix is the user's. +An **environment switch** — "target prod", "clear the environment" — +is a write of the `environment` field and nothing else: +`odd_config_set {"environment": ""}` (kebab-case, never +`unknown`), or `{"environment": null}` to clear it. It selects which +of the configured stack's entries the missions read (`## Check` step +2's effective entry) and changes no `stack_config` value; on the local +stack it is inert. A switch that names both ("switch to cloudwatch in +prod") writes both fields in the one call. Like every path it ends at +verification (step 5). + The switch alone touches nothing else: it does not boot, reset, or stop the local stack container. A `stack_reset` block appears in the result **only** when the same call also changed a local host port, because a @@ -313,8 +342,15 @@ server): a value under another name is rejected there, and the reference's `### What stack_config holds` is where the list is explained. -Write with `odd_config_set {"stack_config": {"": {...}}}`. The -payload is merged into that stack's entry and every other stack's entry +Write with `odd_config_set {"stack_config": {"": {...}}}` — or, +when the request names an environment ("... for in prod") or +one is configured (`odd_config_get`'s `environment`; the local stack +excepted, it takes no prefix), under the prefixed key: +`{"stack_config": {"-": {...}}}`. The key carries +the environment, the fields stay the stack's, and the entry is whole: +what the missions read for that pair is that entry alone, never the +plain one underneath it. The +payload is merged into that entry and every other entry is left untouched, so a one-value correction is a one-value call. Values are flat scalars (string, number, boolean) and nothing else: identifiers, names, regions, GUIDs, group names. **Never a secret** — no password, no @@ -328,7 +364,8 @@ A `stack_config` write never boots or resets the stack container. that key (deleting the last one leaves the present-but-empty entry — "not configured", the normal state), and `{"stack_config": {"": null}}` removes the stack's entry -entirely. A deletion never boots or resets the container either, and it +entirely — an environment's under its prefixed key, the same way. A +deletion never boots or resets the container either, and it is the tool-surface answer to "clear the for " — never hand-edit the file. @@ -342,7 +379,9 @@ A **persist-only** request — targeting values named, no switch asked ("persist workspace `` for " while the configured stack is another) — enters the switch here and stands alone. Resolve the target stack (step 1) for validation only, so the values land in the right -entry and are read against the right reference; **skip the switch +entry — the environment's, when one is named or configured, under the +prefixed key above — and are read against the right reference; +**skip the switch persist** (step 3) entirely; write the values as above; end at verification (step 5) as every path does. The configured stack is left exactly as it was — naming a backend to persist values for is not diff --git a/marketplace/oddyssey/skills/get-status/SKILL.md b/marketplace/oddyssey/skills/get-status/SKILL.md index 942c0558..da7b7620 100644 --- a/marketplace/oddyssey/skills/get-status/SKILL.md +++ b/marketplace/oddyssey/skills/get-status/SKILL.md @@ -104,8 +104,7 @@ renderings of the same rules: **Judgment needed** lists everything the rules deferred: a ruling whose wording states no state or two, verifications that disagree, a -verification stating no verdict, a quick verification that ruled only -part of its items, a boundary the files cannot settle (tree entries +verification stating no verdict, a boundary the files cannot settle (tree entries the anchor cannot classify, an entry present on one side only, a commit-date boundary with commits since), a ruling on an id its chain does not define (the same finding, or a homonym) unless the decisions @@ -114,8 +113,7 @@ judgment, persisted — or its own chain already ruled it (a regression claimed from outside stays listed), a verification that states a verdict yet keys none of its rulings by a finding of its chain (with the hint, never the guess, when its keys are the -baseline's ids with an `F` added or dropped), a quick report's -gaps section opening with its not-queried list, a section not lifted +baseline's ids with an `F` added or dropped), a section not lifted (a gap's text shown up to the cap is a note under the gaps table, never a deferral), an unreadable report, a malformed frontmatter value, a skipped ledger row — except a classification row naming an entry @@ -202,14 +200,10 @@ status. must not split a chain — but the row's evidence states it whenever the last report names another repository than the store's, or the store holds reports of several. Per lineage: last - observation (date, `stack`, `environment`, mode, `depth` — `full` - when the frontmatter has none — `workload` when present), last - verification (`mode: verify` reports — their `verifies` value names - what they replayed) with its verdict from the report body — a - `depth: quick` verification renders its coverage - (`PASS (quick, 3 of 5 ruled)`) and satisfies "verified" only for - the items it ruled, never for the service as a whole; a - verification's presence rulings satisfy "verified" only for the + observation (date, `stack`, `environment`, mode, `workload` when + present), last verification (`mode: verify` reports — their + `verifies` value names what they replayed) with its verdict from the + report body — a verification's presence rulings satisfy "verified" only for the items ruled `closed`, `present, unattributed` closes nothing — and the chain as the files tell it: observed -> fixed -> verified. A `mode: re-measure` report is an @@ -279,8 +273,7 @@ status. verification's rulings, and the decisions ledger: open, fixed-and-verified, regressed, or declined, with severity — the burn-down of the loop's backlog. A finding no verification ever ruled - on stays open, whatever a commit message claims — `not ruled (quick)` - in a quick verification is not a ruling — unless the + on stays open, whatever a commit message claims, unless the decisions ledger declines it. Cross-reference every finding against `.odd/decisions.md` on the key that ledger uses: ` / bool: return mode_of(report) == "verify" -def is_quick(report: dict) -> bool: - return str(report["frontmatter"].get("depth")).lower() == "quick" - - def readable(facts: dict) -> list[dict]: return [r for r in facts["reports"] if "unreadable" not in r] @@ -181,17 +170,6 @@ def rulings_of(report: dict) -> list[dict]: return [f for f in report.get("findings", []) if f["ruling"]] -def quick_coverage(report: dict) -> tuple[int, int] | None: - """(ruled, total) for a quick verification, from its own rows.""" - if not is_quick(report): - return None - rulings = rulings_of(report) - if not rulings: - return None - not_ruled = sum(NOT_RULED_RE.search(r["ruling"]) is not None for r in rulings) - return len(rulings) - not_ruled, len(rulings) - - def verdict_label(report: dict) -> str: """A verification's verdict: its own word first, else its rulings counted.""" verdict_text = " ".join(report.get("verdict_lines") or []) @@ -210,25 +188,15 @@ def verdict_label(report: dict) -> str: passed, total = int(counted.group(1)), int(counted.group(2)) label = f"{'PASS' if passed >= total else 'FAIL'} ({passed}/{total})" if label: - coverage = QUICK_COVERAGE_RE.search(text) - if coverage: - label += f" (quick, {coverage.group(1)} of {coverage.group(2)} ruled)" break if label is None: - rulings = [ - r for r in rulings_of(report) if not NOT_RULED_RE.search(r["ruling"]) - ] + rulings = rulings_of(report) if not rulings: return "no verdict stated" closed = sum( classify_ruling(r["ruling"]) == "fixed-and-verified" for r in rulings ) label = f"{closed} of {len(rulings)} rulings closed" - coverage = quick_coverage(report) - if coverage and "(quick" not in label: - label += f" (quick, {coverage[0]} of {coverage[1]} ruled)" - elif is_quick(report) and "(quick" not in label: - label += " (quick)" return label @@ -771,19 +739,10 @@ def screen_lines(facts: dict) -> list[str]: ) head = facts["head"] or {} repositories = repositories_of(facts) - invariant = facts.get("invariant") or { - "checked": 0, - "violations": [], - "legacy": [], - } + invariant = facts.get("invariant") or {"checked": 0, "violations": []} violations = invariant["violations"] - legacy = invariant.get("legacy", []) if not violations and not skipped and not skipped_classes: - status = ( - f"clean ({invariant['checked']} of {invariant['checked']}" - + (f"; {len(legacy)} predate `depth`, read as full" if legacy else "") - + ")" - ) + status = f"clean ({invariant['checked']} of {invariant['checked']})" else: problems = ( [f"{Path(v['path']).name} - {p}" for v in violations for p in v["problems"]] @@ -801,7 +760,6 @@ def screen_lines(facts: dict) -> list[str]: status = ( f"{plural(len(violations), 'violation')}, " f"{plural(len(skipped) + len(skipped_classes), 'ledger row')} skipped" - + (f", {len(legacy)} predate `depth`" if legacy else "") + ": " + "; ".join(problems) ) @@ -1111,25 +1069,6 @@ def boundary(report: dict) -> dict: return {"changed": None, "evidence": "no boundary: the report is not committed"} -def unruled_by_quick(report: dict, by_name: dict[str, dict]) -> tuple[str, int] | None: - """(baseline, count) of a baseline's findings a quick verification left unruled.""" - if not (is_verify(report) and is_quick(report)): - return None - targets = targets_of(report, by_name) - if not targets: - return None - base = targets[0] - # a row reading ``not ruled (quick)`` is the contract's way of saying the - # finding was left unruled: it names the finding, it does not rule it - ruled = { - row["id"] - for row in rulings_of(report) - if not NOT_RULED_RE.search(row["ruling"]) - } - unruled = [f for f in own_findings(base) if f["id"] not in ruled] - return (name_of(base), len(unruled)) if unruled else None - - def parse_day(text: str | None) -> date | None: try: return date.fromisoformat(str(text)[:10]) @@ -1193,7 +1132,6 @@ def recommendations( else (parse_day(today) or datetime.now(timezone.utc).date()) ) by_target = verifications_of(facts) - by_name = {name_of(r): r for r in readable(facts)} out = [] for label, line in lineages(facts).items(): last = line[-1] @@ -1225,18 +1163,10 @@ def recommendations( if is_verify(last): evidence.append(f"verdict {verdict_label(last)}") evidence.append(bound["evidence"]) - unruled = unruled_by_quick(last, by_name) unread = unread_baseline_rulings(last, facts, ruled) - if unruled or unread: + if unread: action = "judgment needed" - if unruled: - evidence.append( - f"{unruled[1]} finding(s) of {unruled[0]} unruled by the quick " - "verification: verified only for the items it ruled, never for " - "the service" - ) - if unread: - evidence.append(unread) + evidence.append(unread) elif bound["changed"] is None: action = "judgment needed" elif bound["changed"]: @@ -1273,17 +1203,17 @@ def gap_section(report: dict) -> dict | None: def top_level_chars(text: str) -> Iterator[tuple[int, str]]: """The characters of ``text`` outside backticks and parentheses, with their index.""" - depth, quoted = 0, False + nesting, quoted = 0, False for i, ch in enumerate(text): if ch == "`": quoted = not quoted elif quoted: continue elif ch == "(": - depth += 1 + nesting += 1 elif ch == ")": - depth = max(depth - 1, 0) - elif depth == 0: + nesting = max(nesting - 1, 0) + elif nesting == 0: yield i, ch @@ -1376,19 +1306,6 @@ def gap_items(section: dict) -> list[str]: return gap_items_by_shape(section)[0] -def is_not_queried_item(item: str) -> bool: - """A not-queried item hides its gaps - unless the list it opens with is ``none``.""" - return ( - NOT_QUERIED_RE.match(item) is not None - and NOT_QUERIED_NONE_RE.match(item) is None - ) - - -def is_not_queried_none(item: str) -> bool: - """The ``Not queried (): none`` statement on its own: never a gap.""" - return NOT_QUERIED_NONE_RE.match(item) is not None and "`" not in item - - def is_no_gap_statement(item: str) -> bool: """A no-gap / no-handoff statement on its own - one carrying a query or a fate marker may name a gap after its opening words, and settles nothing.""" @@ -1408,23 +1325,6 @@ def newest_observations(facts: dict) -> dict[str, dict]: return out -def mixed_not_queried(facts: dict) -> list[str]: - """Reports whose gaps section opens an item with its not-queried list: - the item is not a gap, and the gaps it carries cannot be told apart - - unless the section states it carries none.""" - out = [] - for newest in newest_observations(facts).values(): - section = gap_section(newest) - if section is None or section["text"] is None: - continue - items = gap_items(section) - if any(is_no_gap_statement(item) for item in items): - continue - if any(is_not_queried_item(item) for item in items): - out.append(name_of(newest)) - return out - - def lift_losses(facts: dict) -> dict[str, dict]: """Per report whose gaps section the lift cut, what it took: the bullets it dropped whole, the bullets it cut at their tail, and whether the prose @@ -1454,11 +1354,6 @@ def lift_losses(facts: dict) -> dict[str, dict]: def gap_rows(facts: dict) -> list[dict]: """The newest observation of each lineage, its telemetry-gaps section as recorded. - A ``Not queried ()`` item is a statement about that mission, - never a gap, at either depth: a ``none`` list on its own is skipped, - a list of signals is dropped whole and deferred - unless the section - states it carries no gap. - ``truncated`` says the lift took something from the section's text - what it took is the lift's to state (``lift_losses``); ``cut`` carries an item's whole length when the row caps it, else 0; ``paragraph`` @@ -1480,23 +1375,12 @@ def gap_rows(facts: dict) -> list[dict]: } ) continue - quick = is_quick(newest) - before = len(rows) - dropped = no_gap = False items, structured = gap_items_by_shape(section) paragraph = len(items) == 1 and not structured for item in items: - if not item or is_not_queried_none(item): - continue - if NO_GAP_RE.match(item): - no_gap = no_gap or is_no_gap_statement(item) - continue - if is_not_queried_item(item): - dropped = True + if not item or NO_GAP_RE.match(item): continue gap, capped = cap(item, MAX_GAP_LENGTH) - if quick: - gap = f"(quick report) {gap}" rows.append( { "lineage": label, @@ -1507,17 +1391,6 @@ def gap_rows(facts: dict) -> list[dict]: "paragraph": paragraph, } ) - if dropped and not no_gap and len(rows) == before: - rows.append( - { - "lineage": label, - "gap": f"{'(quick report) ' if quick else ''}{MIXED_GAPS}", - "recorded_by": name_of(newest), - "truncated": False, - "cut": 0, - "paragraph": False, - } - ) return rows @@ -1609,17 +1482,12 @@ def inventory_lines(facts: dict) -> list[str]: def invariant_section(facts: dict) -> list[str]: """The memory invariant: never a failure - the store is append-only.""" - invariant = facts.get("invariant") or { - "checked": 0, - "violations": [], - "legacy": [], - } + invariant = facts.get("invariant") or {"checked": 0, "violations": []} ledger = facts["ledger"] skipped = [r for r in ledger["rows"] if r["status"] == "skipped"] classifications = facts.get("classifications") or {"present": False, "rows": []} skipped_classes = [r for r in classifications["rows"] if r["status"] == "skipped"] checked = invariant["checked"] - legacy = invariant.get("legacy", []) clean = checked - len(invariant["violations"]) out = [ "## Memory invariant", @@ -1631,14 +1499,6 @@ def invariant_section(facts: dict) -> list[str]: if invariant["violations"] else " - every stored report carries the contract's frontmatter" ) - + ( - f"; {len(legacy)} predate the `depth` field and read as full" - f" ({', '.join(Path(p).name for p in legacy[:3])}" - + (f", +{len(legacy) - 3} more" if len(legacy) > 3 else "") - + ")" - if legacy - else "" - ) ), ( f"- Decisions: {len(skipped)} row(s) skipped" @@ -1715,10 +1575,7 @@ def state_rows(facts: dict) -> list[list[str]]: elif last_obs: fm = last_obs["frontmatter"] workload = f", workload {fm.get('workload')}" if fm.get("workload") else "" - last_obs_cell = ( - f"{date_of(last_obs)} {fm.get('run_name')} ({mode_of(last_obs)}, " - f"depth {fm.get('depth') or 'full'}{workload})" - ) + last_obs_cell = f"{date_of(last_obs)} {fm.get('run_name')} ({mode_of(last_obs)}{workload})" else: last_obs_cell = "none in observe mode" last_ver_cell = ( @@ -1819,12 +1676,6 @@ def render( ) elif label.startswith("no verdict stated"): verdicts.append(f"{name_of(report)} states two verdicts {label[18:]}") - coverage = quick_coverage(report) - if coverage and coverage[0] < coverage[1]: - verdicts.append( - f"quick verification {name_of(report)} ruled {coverage[0]} of " - f"{coverage[1]}: verified only for those items, never for the service" - ) rows = finding_rows(facts) problems, ruled_keys = apply_rulings(rows, ruled) @@ -1889,11 +1740,6 @@ def render( f"{plural(count, 'gap')} of {name} shown up to {MAX_GAP_LENGTH} " "characters; the body carries the whole" ) - for name in mixed_not_queried(facts): - gap_notes.append( - f"section 5 of {name} mixes a not-queried list with its gaps: open the body " - "for the gaps it carries" - ) recs = recommendations(facts, today, ruled=ruled_keys) boundaries: list[str] = [] for r in recs: @@ -2045,8 +1891,6 @@ def full_sections( ] if cap_notes: out += [f"- {note}" for note in cap_notes] + [""] - elif mixed_not_queried(facts): - out += ["No gap listed by rule - see Judgment needed.", ""] else: out += ["No gap recorded.", ""] diff --git a/marketplace/oddyssey/skills/get-status/scripts/odd_status.py b/marketplace/oddyssey/skills/get-status/scripts/odd_status.py index f56f9686..9db9f0d9 100644 --- a/marketplace/oddyssey/skills/get-status/scripts/odd_status.py +++ b/marketplace/oddyssey/skills/get-status/scripts/odd_status.py @@ -46,7 +46,6 @@ import odd_report from odd_report import ( LEDGER_PATH, - LEGACY_PREFIX, MAX_FINDING_TITLE, MEMORY_PATHS, OBSERVATION_MODES, @@ -551,23 +550,16 @@ def load_ledger(root: Path, reports: list[dict]) -> dict: def check_invariant(root: Path, reports: list[dict]) -> dict: - """Every stored report checked; a report whose only problem is a field - it predates is listed as legacy, not as a violation - the contract reads - it as full, and nothing can ever change an append-only file.""" + """Every stored report checked against the contract's frontmatter.""" stored = {Path(r["path"]).name for r in reports if r["kind"] == "observation"} violations = [] - legacy = [] for report in reports: problems = check_report(report, stored, root) - if not problems: - continue - if all(p.startswith(LEGACY_PREFIX) for p in problems): - legacy.append(report["path"]) - continue - violations.append( - {"path": report["path"], "kind": report["kind"], "problems": problems} - ) - return {"checked": len(reports), "violations": violations, "legacy": legacy} + if problems: + violations.append( + {"path": report["path"], "kind": report["kind"], "problems": problems} + ) + return {"checked": len(reports), "violations": violations} # --- the fact sheet ----------------------------------------------------------- diff --git a/marketplace/oddyssey/skills/observability-cli-guides/references/CONTRACT.md b/marketplace/oddyssey/skills/observability-cli-guides/references/CONTRACT.md index 18c4f5d2..4a83a374 100644 --- a/marketplace/oddyssey/skills/observability-cli-guides/references/CONTRACT.md +++ b/marketplace/oddyssey/skills/observability-cli-guides/references/CONTRACT.md @@ -56,7 +56,10 @@ agents like any other, never by the preflight or the switch. for a switch to this stack. - **`## What to persist`** — read by the switch only: - **`### What stack_config holds`**: the field list the switch persists - for this stack (never a credential); + for this stack (never a credential) — per stack, never per + environment: a `stack_config` key is `` or + `-` (`local` takes no prefix), and a prefixed + entry accepts exactly the stack's fields — said here once; - **`### Where each value comes from`**: the command or console path that yields each field; - **`### What to ask the user`**: what the switch asks when a field diff --git a/marketplace/oddyssey/skills/observability-cli-guides/references/azure-monitor.md b/marketplace/oddyssey/skills/observability-cli-guides/references/azure-monitor.md index acaea850..cad87a57 100644 --- a/marketplace/oddyssey/skills/observability-cli-guides/references/azure-monitor.md +++ b/marketplace/oddyssey/skills/observability-cli-guides/references/azure-monitor.md @@ -66,8 +66,9 @@ preflight handoff carries (the `package-layout` skill's `--help` has nothing to add and the files have nothing to read. A run that lacks a shape of the work records it in its report's `## 8. Stack friction`, never a wrapper. -Common to every script: the targeting values come from -`stack_config.azure-monitor` and are passed as flags - `--app +Common to every script: the targeting values come from the +`stack_config` entry the preflight resolved (the handoff's `Target:` +line names the key and carries the values) and are passed as flags - `--app ` (the component's appId GUID, never with `-g` beside it, never with `--subscription`: the data plane needs neither), `--workspace ` (the Log Analytics customer ID GUID), @@ -544,8 +545,11 @@ From `az account show` (the CLI's own context, printed by `check`): the active subscription (name and id), the tenant id, the identity's type; never `user.name` (a login name), never a token. -From `stack_config.azure-monitor` (persisted through -`odd_config_set`, read with `odd_config_get`), shown next to its field: +From the `stack_config` entry the preflight resolved - for the +configured pair `odd_config_get`'s `effective.stack_config`, its +`effective.stack_config_key` naming it: `-azure-monitor` +when one is persisted for the configured environment, `azure-monitor` +otherwise - shown next to its field: - `subscription` - the subscription the missions query, when pinned separately from the CLI's active one; say it plainly when it differs @@ -560,7 +564,7 @@ From `stack_config.azure-monitor` (persisted through takes), not its resource name. A field the user did not persist reads "not persisted - the mission will -ask"; an empty `stack_config.azure-monitor` (`{}`) is all four +ask"; an empty resolved entry (`{}`) is all four unset, a valid state. `app_insights_app` unset is the one exception to that neutral wording - a **named degradation**: diff --git a/marketplace/oddyssey/skills/observability-cli-guides/references/cloudwatch.md b/marketplace/oddyssey/skills/observability-cli-guides/references/cloudwatch.md index 11f7302a..10709660 100644 --- a/marketplace/oddyssey/skills/observability-cli-guides/references/cloudwatch.md +++ b/marketplace/oddyssey/skills/observability-cli-guides/references/cloudwatch.md @@ -80,8 +80,9 @@ surface: `--help` has nothing to add and the files have nothing to read. A run that lacks a shape of the work records it in its report's `## 8. Stack friction`, never a wrapper. -Common to every script: the targeting values come from -`stack_config.cloudwatch` and are passed as flags - `--profile +Common to every script: the targeting values come from the +`stack_config` entry the preflight resolved (the handoff's `Target:` +line names the key and carries the values) and are passed as flags - `--profile --region ` on every script, `--log-group ` (the application logs group), `--metrics-log-group ` (the EMF group), `--xray-group ` (an X-Ray group name, where the @@ -609,7 +610,7 @@ report's stack-friction section. Two sources, labelled per line - the CLI's effective credentials and the persisted targeting values. -**If `stack_config.cloudwatch.profile` is persisted, run every +**If the resolved entry persists `profile`, run every command below (display and connection proof alike) with `--profile `** - a bare call answers for whatever profile happens to resolve without a flag, which on an SSO setup with no `default` is @@ -634,7 +635,11 @@ Token has expired and refresh failed`, exit 255) while still printing a partial table - the same auth failure the connection proof diagnoses, one step earlier. Carry on to the proof's expired-token guidance. -From `stack_config.cloudwatch` (per `odd_config_get`): +From the `stack_config` entry the preflight resolved - for the +configured pair `odd_config_get`'s `effective.stack_config`, its +`effective.stack_config_key` naming it: `-cloudwatch` +when one is persisted for the configured environment, `cloudwatch` +otherwise - shown next to its key: - `region` - the region the mission queries, when pinned separately from the CLI's effective one. @@ -651,7 +656,7 @@ From `stack_config.cloudwatch` (per `odd_config_get`): when persisted; the default group otherwise. Every field the user did not persist is listed as "not persisted - the -mission will ask", and a present-but-empty `stack_config.cloudwatch` +mission will ask", and a present-but-empty resolved entry (`{}`) means exactly that for all of them: a valid state, not an error. Call out a persisted `region` that differs from the CLI's effective one - the query targets the persisted value. diff --git a/marketplace/oddyssey/skills/observability-cli-guides/references/datadog.md b/marketplace/oddyssey/skills/observability-cli-guides/references/datadog.md index fea9d983..224a448f 100644 --- a/marketplace/oddyssey/skills/observability-cli-guides/references/datadog.md +++ b/marketplace/oddyssey/skills/observability-cli-guides/references/datadog.md @@ -106,7 +106,8 @@ queries will hit. partial or masked value. See the setup section earlier in this file for the priority order between the three and for the site list. -`stack_config.datadog` is expected **empty** — the CLI session already +The `stack_config` entry the preflight resolved (`datadog`, or an +`-datadog` entry) is expected **empty** — the CLI session already names the site and org. Present-and-empty (`{}`) or missing both display as "nothing persisted — the Pup session is the source". diff --git a/marketplace/oddyssey/skills/observability-cli-guides/references/dynatrace.md b/marketplace/oddyssey/skills/observability-cli-guides/references/dynatrace.md index fd753df3..6a4fdc38 100644 --- a/marketplace/oddyssey/skills/observability-cli-guides/references/dynatrace.md +++ b/marketplace/oddyssey/skills/observability-cli-guides/references/dynatrace.md @@ -104,7 +104,8 @@ DQL queries will run against. vs token contexts, how a context is created); this section owns only what to display. -`stack_config.dynatrace` is expected **empty** — the dtctl context +The `stack_config` entry the preflight resolved (`dynatrace`, or an +`-dynatrace` entry) is expected **empty** — the dtctl context already names the environment. Present-and-empty (`{}`) or missing both display as "nothing persisted — the dtctl context is the source". diff --git a/marketplace/oddyssey/skills/observability-cli-guides/references/grafana.md b/marketplace/oddyssey/skills/observability-cli-guides/references/grafana.md index 70a377df..0803869f 100644 --- a/marketplace/oddyssey/skills/observability-cli-guides/references/grafana.md +++ b/marketplace/oddyssey/skills/observability-cli-guides/references/grafana.md @@ -27,7 +27,7 @@ identical either way. | Topic | Link | What to do with it | | --- | --- | --- | | Install | [installation.md](https://raw.githubusercontent.com/grafana/gcx/main/docs/sources/installation.md) | Quick-install script (`curl \| sh`), Homebrew (`brew install gcx`, or the `grafana/grafana/gcx` tap to build from source), prebuilt binaries, `go install github.com/grafana/gcx/cmd/gcx@latest`. Pick one method only — running two leaves two binaries on `PATH`; use `which -a gcx` to find duplicates. Homebrew installs avoid the macOS Gatekeeper `killed: 9` issue that manually downloaded binaries hit. | -| Configure / contexts | [configuration.md](https://raw.githubusercontent.com/grafana/gcx/main/docs/sources/configuration.md) | How `gcx` layers config (system → user `$HOME/.config/gcx/config.yaml` → repo `.gcx.yaml`), the four auth methods (OAuth, service-account token, basic auth, mTLS), and `gcx config set/check/view/list-contexts/use-context`. Use this to define a named context per Grafana instance (`stacks..grafana.server`, `.org-id` for on-prem, `.token`/`.user`+`.password`) and switch with `gcx config use-context`. Run `gcx config check` (optionally `--context `) as a connectivity/auth gate. | +| Configure / contexts | [configuration.md](https://raw.githubusercontent.com/grafana/gcx/main/docs/sources/configuration.md) | How `gcx` layers config (system → user `$HOME/.config/gcx/config.yaml` → repo `.gcx.yaml`), the four auth methods (OAuth, service-account token, basic auth, mTLS), and `gcx config set/check/view/list-contexts/use-context`. Use this to define a named context per Grafana instance (`stacks..grafana.server`, `.org-id` for on-prem, `.token`/`.user`+`.password`) and switch with `gcx config use-context`. Run `gcx config check --context ` as a connectivity/auth gate. | | Configuration file schema | [reference/configuration](https://raw.githubusercontent.com/grafana/gcx/main/docs/reference/configuration/index.md) | Full annotated YAML schema: `stacks`, `cloud`, `contexts`, `contexts..datasources.` (default datasource UID per signal), `diagnostics`. Consult when hand-editing a config/`.gcx.yaml` file instead of using `gcx config set`, or to see exactly which fields a given auth method needs. | | Environment variables | [reference/environment-variables](https://raw.githubusercontent.com/grafana/gcx/main/docs/reference/environment-variables/index.md) | `GRAFANA_SERVER`, `GRAFANA_ORG_ID`, `GRAFANA_STACK_ID`, `GRAFANA_TOKEN`, `GRAFANA_USER`/`GRAFANA_PASSWORD`, `GRAFANA_TLS_*`, `GRAFANA_CLOUD_TOKEN`, `GCX_TELEMETRY`, `GCX_AUTO_APPROVE`. Use for CI/non-interactive runs — env vars override the selected context in memory and are never persisted. Minimum for a working call: `GRAFANA_SERVER` + `GRAFANA_ORG_ID` (on-prem) plus one credential var. | | `gcx login` | [gcx_login.md](https://raw.githubusercontent.com/grafana/gcx/main/docs/reference/cli/gcx_login.md) | Interactive/non-interactive auth: `--oauth` (Cloud, browser-based, works in agent mode), `--token` (service-account token, Cloud or on-prem), `--cloud-token` (Cloud platform API), `--yes` to skip prompts. `gcx login prod --server https://.grafana.net` for Cloud; `gcx login local --server http://localhost:3000 --token ` for self-hosted/on-prem. | @@ -43,20 +43,26 @@ Remote missions only: on the local stack the context is the python3 /observability-cli-guides/scripts/grafana-context.py [--stack ] [--json] ``` -Whole surface: `--stack` (default: the user's current context), `--json`. +Whole surface: `--stack` (the context the preflight handoff's `context:` +line names — the persisted `stack_config` `context` when the entry +carries one; omitted otherwise, and the user's current context is the +target), `--json`. Prints the `export GCX_CONFIG=…` line to put in front of every later call, the context, and the four datasource UIDs (marked when a UID is not a context default: gcx then resolves it from the stack). Exit 0 = proved with [`config check --context`](https://raw.githubusercontent.com/grafana/gcx/main/docs/reference/cli/gcx_config_check.md); exit 1 = the message names the fix, which is the user's, never the mission's. -- Target is the user's current context (the usual case): the user's +- Target is the user's current context (the usual case — no context + persisted, or the persisted one is the current one): the user's file is used in place, nothing copied, nothing written — a keychain-bound credential answers only from the file it was bound to, and no default is needed (verified 2026-09-08 on Cloud: four signals - answered with `loki` and `prometheus` the only defaults set). Fix on + answered with `loki` and `prometheus` the only defaults set; + re-verified 2026-09-19 with `--stack `). Fix on exit 1: `gcx login `. -- Target is another context: the user's file is copied to a session path +- Target is another context (a persisted context that is not the + current one): the user's file is copied to a session path (one per stack and session), the copy gets that context and the default UID per signal from [`datasources list`](https://raw.githubusercontent.com/grafana/gcx/main/docs/reference/cli/gcx_datasources_list.md). Fix on exit 1 ("the keychain reference does not match this config @@ -529,19 +535,27 @@ instrumentation gap; the series carrying one are `grafana` is a **remote** Grafana; the gcx context is what says which instance, so the display is the context, not an invented value. +- The `stack_config` entry the preflight resolved (`grafana`, or an + `-grafana` entry) holds one field, `context`: show it + when set, "not persisted — the active context is used" otherwise + (present-and-empty `{}` and missing both read so, and both are a + normal state, not a gap). - `gcx config list-contexts` — every configured context, with the active one marked. -- `gcx config view` — the active context's `grafana.server` (the - instance the queries will hit) and its `org-id` when set. Show the - server URL and org; never echo a token, password, or any other - credential field the view prints. - -`stack_config.grafana` is expected **empty** — the gcx context already -names the instance, and duplicating it in the global configuration only -creates a second truth to drift. Present-and-empty (`{}`) or missing -both display as "nothing persisted — the gcx context is the source". -If values are stored there anyway, show them as-is and say the gcx -context still wins for targeting. +- `gcx config view --context --minify` — that context's + `grafana.server` (the instance the queries will hit) and its + `stack-id` or `org-id`; `` is the persisted name, the + active context's otherwise. `--minify` is what restricts the output + to the one context: plain `gcx config view` dumps the whole file, and + `--context` alone does not restrict it (verified 2026-09-19 on 1.2.0 + with two contexts). Show the server URL and the org or stack id; + never echo a token, password, or any other credential field (1.2.0 + prints `**REDACTED**` in their place; never add `--raw`, which prints + them). +- A `view` that exits 1 with `context not found` (verified 2026-09-19) + is the targeting failure met one step early: show the persisted + name, say gcx does not know that context, and take + `### Connection proof`'s targeting routing without running the proof. List any `invalid_ignored` dotted names `odd_config_get` returned as degradations: the stored value was invalid and was dropped. @@ -550,14 +564,38 @@ not persisted — nothing silently took its place. ### Connection proof -`gcx config check` (add `--context ` when proving a context other -than the active one). Success on the active context = connected. No -context configured for a remote instance is not automatically an +**One call**: `gcx config check --context ` — the persisted +`context` when set, the active context's name (the one +`list-contexts` marks) otherwise. Always with `--context`: without it, +`check` proves every configured context and exits 1 when any of them +fails (verified 2026-09-19 on 1.2.0). Read in two parts: + +- **Targeting** — the context resolves. A name gcx does not know exits + 1 with `context not found` (a `gcx.error` line; verified + 2026-09-19): a wrong persisted value, never a "CLI not configured" + error — route to `backend-configuration`'s `## Switch` persist-only + path **once** for a corrected name; a corrected name that fails too + stops with the report, never a guess. +- **Connectivity** — the instance answers. Exit 0 with `Connectivity: + online` = connected (verified 2026-09-19, on the active context and + on a context that was not the active one). Exit 1 with + `Configuration: valid` and a failed `Connectivity:` line is the + user's to fix — a network error names the server (verified + 2026-09-19), an expired or rejected credential is `gcx login + `, guided and never run on the user's behalf (that shape + unverified 2026-09-19). `Configuration:` failing names the missing + field of the context (a `stack-id` or `org-id`; verified + 2026-09-19): the context's own setup, the user's to complete. + +Never two calls: the display shows, the proof proves. No context +configured for a remote instance is not automatically an authentication problem: offer `odd_config_set {"stack": "local"}` first if the user meant the local stack. ### Change-request phrasing +- "persist gcx context for grafana" +- "clear the grafana context" - "switch gcx to context " - "change backend to local" @@ -565,47 +603,46 @@ if the user meant the local stack. ### What stack_config holds -**Nothing.** `stack_config.grafana` is expected to stay empty, and an -empty entry is the correct final state of a switch to `grafana`, not an -unfinished one. +One field, `context`: the **name** of the gcx context the runs use — +`{"stack_config": {"grafana": {"context": ""}}}`, or under an +`-grafana` entry. Absent, the runs use the user's active +context, and an empty entry is a correct final state of a switch to +`grafana`, not an unfinished one. -The reason is that gcx is a **context-bearing** CLI: the active context -already names the instance (`grafana.server`), the org (`org-id`) or -Cloud stack, and the default datasource UID per signal. Copying any of -that into the global configuration creates a second truth that drifts -the first time the user runs `gcx config use-context` — and the gcx -context wins for targeting regardless, so the copy would be wrong -without being consulted. +The name is a pointer to gcx's own truth, never a copy of it: the +server (`grafana.server`), the org or Cloud stack id, the credential +and the default datasource UID per signal stay in the context, read at +use time. Copying any of them here would create a second truth that +drifts, and the context wins for targeting regardless; a stale name +fails loudly at the proof (`context not found`) instead of silently +targeting the wrong instance. `grafana` always means a **remote** Grafana. The local stack is the separate `local` value, with its own reference. ### Where each value comes from -From the gcx context, read at use time and never mirrored here: +- `context` — `gcx config list-contexts`: the configured contexts, the + active one marked; the value is one of the names, as printed. -- `gcx config list-contexts` — the configured contexts, active one - marked. -- `gcx config view` — the active context's server URL, org, and - datasource defaults. - -Whichever credential the context uses (a service-account token, basic -auth, OAuth, mTLS) lives in gcx's own configuration. It is referred to -by name in any display and never written into `stack_config`. +Everything else stays in gcx, read at use time and never mirrored here +— `gcx config view --context --minify` shows that context's +server URL, org or stack id, and datasource defaults. Whichever +credential the context uses (a service-account token, basic auth, +OAuth, mTLS) lives in gcx's own configuration: referred to by name in +any display, never written into `stack_config`. ### What to ask the user -**Nothing about targeting.** Do not ask for the instance URL, the org, -the stack id, or the datasource UIDs — asking implies they should be -stored, and they should not be. - -The one thing worth raising, and only when the user has more than one -context or none active, is which gcx context the runs should use — and -the fix for that lives in gcx (`gcx config use-context `), not in -this configuration. If no context points at a remote instance at all, -offer the alternative before anything else: the user may have meant the -local stack, and that is `odd_config_set {"stack": "local"}`. - -Leave `stack_config.grafana` alone. If values are already stored there -from an earlier run, do not add to them and say plainly that the gcx -context is what the missions will target. +Which context the runs should use — only when `gcx config +list-contexts` shows several and none is persisted for the entry. +"None, use the active context" is a valid answer that writes nothing. +With one context, or one already persisted, ask nothing. If no context +points at a remote instance at all, offer the alternative before +anything else: the user may have meant the local stack, and that is +`odd_config_set {"stack": "local"}`. + +Never the instance URL, the org, the stack id, or the datasource UIDs — +asking implies they should be stored, and they should not be. Stray +keys an earlier run or a hand edit left under the entry are listed in +`invalid_ignored` and ignored: do not add to them. diff --git a/marketplace/oddyssey/skills/observability-cli-guides/scripts/cloudwatch_aws.py b/marketplace/oddyssey/skills/observability-cli-guides/scripts/cloudwatch_aws.py index 4e1b3cf1..620d5ca4 100644 --- a/marketplace/oddyssey/skills/observability-cli-guides/scripts/cloudwatch_aws.py +++ b/marketplace/oddyssey/skills/observability-cli-guides/scripts/cloudwatch_aws.py @@ -613,25 +613,30 @@ def add_targeting( metrics_log_group: bool = False, required_groups: bool = True, ): + # The flags carry the resolved stack_config entry's values - the + # entry the preflight resolved for the configured stack and + # environment, named on the handoff's Target: line. ap.add_argument( "--profile", required=True, - help="the aws CLI profile (stack_config.cloudwatch.profile)", + help="the aws CLI profile (the resolved stack_config entry's profile)", ) ap.add_argument( - "--region", required=True, help="the region (stack_config.cloudwatch.region)" + "--region", + required=True, + help="the region (the resolved stack_config entry's region)", ) if log_group: ap.add_argument( "--log-group", required=required_groups, - help="the application logs group (stack_config.cloudwatch.log_group)", + help="the application logs group (the resolved entry's log_group)", ) if metrics_log_group: ap.add_argument( "--metrics-log-group", required=required_groups, - help="the EMF group (stack_config.cloudwatch.metrics_log_group)", + help="the EMF group (the resolved entry's metrics_log_group)", ) diff --git a/marketplace/oddyssey/skills/odd-memory/SKILL.md b/marketplace/oddyssey/skills/odd-memory/SKILL.md index b1eb3777..26eab1db 100644 --- a/marketplace/oddyssey/skills/odd-memory/SKILL.md +++ b/marketplace/oddyssey/skills/odd-memory/SKILL.md @@ -98,7 +98,7 @@ recalled by name, each ledger is one file; their references own that: with the mission's scope as flags — the kind's reference says which. **The invocation may travel with the mission**: a caller's preflight that already resolved the scope writes it into the mission block as - `Recall: python3 /scripts/odd_recall.py --repo --service [--service ...] --stack --depth ` + `Recall: python3 /scripts/odd_recall.py --repo --service [--service ...] --stack ` (the observation form — one `--service` per name, `--env` left for the agent to append once it has detected the environment; a benchmark is recalled by `--kind benchmark --service ` alone, an @@ -106,7 +106,7 @@ recalled by name, each ledger is one file; their references own that: and the agent runs that line as is — never re-derived, never a flag guessed. The whole surface is `--repo`, `--kind ` (default `observation`), `--service`, - `--stack`, `--env`, `--depth`, `--mode` (repeatable) and `--project` + `--stack`, `--env`, `--mode` (repeatable) and `--project` — nothing else: `--help` has nothing to add and the file has nothing to read. The script reads every stored frontmatter (a benchmark's manifest) in Python and prints the matches newest first, one line each; a @@ -125,8 +125,8 @@ recalled by name, each ledger is one file; their references own that: whole** — the kind's reference names the sections a mission needs, and the command that prints them when it ships one. - A replay's baseline is not recalled but resolved — the report the - caller named or the newest, its one-hop source, the mode and the - depth to replay, and whether the code changed since it — by the + caller named or the newest, its one-hop source, the mode to replay, + and whether the code changed since it — by the report script's `baseline` and `boundary` commands, stated in the observation reference's `## Resolving a replay`; the caller runs them before the dispatch and the mission block carries their lines. diff --git a/marketplace/oddyssey/skills/odd-memory/references/benchmark.md b/marketplace/oddyssey/skills/odd-memory/references/benchmark.md index 8928591e..4dd016ab 100644 --- a/marketplace/oddyssey/skills/odd-memory/references/benchmark.md +++ b/marketplace/oddyssey/skills/odd-memory/references/benchmark.md @@ -29,7 +29,7 @@ It prints one line per stored benchmark, tab-separated: `name` (the directory), `service`, `test_type`, `executor` (the manifest's `profile.executor`), `authored`. It reads the manifests in Python, so none of them reaches the conversation. Drop `--service` to list the -whole store; `--stack`, `--env`, `--mode`, `--depth` and `--project` +whole store; `--stack`, `--env`, `--mode` and `--project` belong to the report kinds and are refused here. Unlike a report recall, the **whole listing** is the answer, not its diff --git a/marketplace/oddyssey/skills/odd-memory/references/observe-run-report.md b/marketplace/oddyssey/skills/odd-memory/references/observe-run-report.md index ff229c62..4c50f75c 100644 --- a/marketplace/oddyssey/skills/odd-memory/references/observe-run-report.md +++ b/marketplace/oddyssey/skills/odd-memory/references/observe-run-report.md @@ -13,7 +13,7 @@ the next `odd_stack_reset` — when in doubt, record the number. ```bash python3 /scripts/odd_report.py new [--repo ] \ --service [--service ...] --stack --env \ - --mode --depth \ + --mode \ --window / | --from --to --run-name \ [--verifies ] [--workload ] [--instance = ...] \ [--process-restarted ...] [--repository ] \ @@ -25,7 +25,7 @@ python3 /scripts/odd_report.py synthesis python3 /scripts/odd_report.py show python3 /scripts/odd_report.py baseline [--repo ] \ [] [--service ]... [--stack ] \ - [--env ] [--depth ] [--own-protocol] + [--env ] [--own-protocol] python3 /scripts/odd_report.py boundary [--repo ] \ [--runtime ]... [--non-runtime ]... ``` @@ -100,12 +100,10 @@ The frontmatter mirrors the run **as it executed**, defaults applied — `.odd/otel-instrumentation-reports/` for an instrumentation baseline — and records `--mode verify` when a fix is under test, `--mode re-measure` when the code is unchanged since the baseline's - `revision`. The run name and the depth are inherited from the baseline - when the flags are omitted (`quick` when an observation baseline - predates the field; `full` for an instrumentation baseline); a replay - knowingly run on another stack says so in section 1. -- `--depth` is how far the mission went (the agent's Depth section); - `--workload` when the input shaped the run (a different workload is a + `revision`. The run name is inherited from the baseline when the flag + is omitted; a replay knowingly run on another stack says so in + section 1. +- `--workload` when the input shaped the run (a different workload is a new run, not a note); `--instance` and `--process-restarted` pin the process the numbers belong to (`run-scenario`'s `run-identity.md`) — cumulative queries in the protocol are qualified by that identity. @@ -119,12 +117,11 @@ The frontmatter mirrors the run **as it executed**, defaults applied — Seven numbered sections — an eighth on a custom stack — read by number by the recall, the status and `show`. What each carries is the calling agent's judgment, stated here beside the format it fills and read at report time — its Investigation -gathers the evidence, its Depth section collapses sections 3 to 6 at -`quick` depth. Three shapes in it are the script's, never yours to -vary: section 3's ruling table on a replay (`new` pre-fills it, +gathers the evidence. Three shapes in it are the script's, never yours +to vary: section 3's ruling table on a replay (`new` pre-fills it, `check` wants one row per baseline finding), section 5's -`- — — ` bullets and its not-queried line, and -section 2's `### GenAI` heading: +`- — — ` bullets, and section 2's `### GenAI` +heading: 1. **Mission and run record** — the mission as understood (services, stack and backend, mode, window, focus, expectations) and every @@ -233,8 +230,7 @@ section 2's `### GenAI` heading: is the key `.odd/decisions.md` names a finding by, and the only thing that ties your ruling to it. **Verdict** is `fixed`, `still present` or `worse` — a nuance goes after the word (`still present, - reduced`) — or `not ruled (quick)` for a baseline finding the - queried signals could not rule. A ruling written anywhere else — in + reduced`). A ruling written anywhere else — in prose, in a row of the ranked table, under an id you renumbered — is a ruling no reader can key to the baseline: the finding stays open in the loop's burn-down however plainly your report calls it fixed. @@ -252,14 +248,14 @@ section 2's `### GenAI` heading: ~52 to ~2 per request") and the query that will prove it landed. 5. **Telemetry gaps** — what the service should emit but does not: missing latency histograms, logs without trace IDs, absent database or - downstream spans, missing resource attributes. The `not queried - ()` statement, when the section carries one (the agent's Depth section), - is its own first line, never spliced into a gap; then one bullet per - gap — `- — - — ` — the fate ruled against the baseline (`new` - when no baseline carries the gap, `not ruled (quick)` when a quick - replay left it unqueried) and the discovery query that came back - empty as evidence; never several gaps in one paragraph. When gaps + downstream spans, missing resource attributes. One bullet per gap — + `- — — ` — the + fate ruled against the baseline (`new` when no baseline carries the + gap) and the discovery query that came back empty as evidence; never + several gaps in one paragraph. A signal the backend cannot serve (a + backend fact, shipped by the reference's discovery script) is said as + `not served: ` on its own line before the bullets — a + statement about the backend, never a gap of the service. When gaps dominate the picture, add a one-line handoff to the `otel-instrumentation-expert` agent. 6. **Decisions the spec must settle** — the open questions telemetry cannot @@ -355,9 +351,7 @@ verify`, a report it cannot read, a repository it cannot compare). for the original. The mode is the baseline's execution mode, or the first the `verifies` chain reaches - an instrumentation report at its end is `drive`; a chain reaching none is an `ask:` for the mode. - The depth: `--depth`, else the baseline's field, else `quick` for an - observation baseline that predates it and `full` for an - instrumentation one. A drive needs the user's confirmation when the + A drive needs the user's confirmation when the stack or the record's base URL is not local. Its `verifies` line is what the replay's `new --verifies` takes. - `boundary ` decides **verification or @@ -380,20 +374,18 @@ verify`, a report it cannot read, a repository it cannot compare). ## Recall: reading the memory 1. `python3 /scripts/odd_recall.py --repo - [--service ]... --stack --env - --depth ` — `--service` repeated per service, `--mode` to + [--service ]... --stack --env ` + — `--service` repeated per service, `--mode` to restrict to one mode, `--env` omitted while the environment is provisional. It lists `.odd/observe-run-reports/` newest first, one tab-separated line per match: filename, kind, services, stack, - environment, mode, depth, `verifies`, `workload`, `repository` (`-` + environment, mode, `verifies`, `workload`, `repository` (`-` when absent); a flagged report is named on stderr, matched or not. 2. A report matches on intersecting `services`, the same `stack` and the detected `environment` (`unknown` matches only `unknown`, with a warning; a provisional environment matches on services and stack alone, pending re-confirmation). A differing `workload` is kept and - warned about. A `full` mission's baseline is the newest `full` (or - depth-less) match, the skipped newer quick ones named on stderr and - in section 1; a `quick` mission takes either depth. + warned about. 3. The first line is the baseline, read **by section, never whole**: `read --sections 1,2,3,7 --record` — section 1's scenario record and replay notes, section 2's numbers and deltas (its GenAI diff --git a/marketplace/oddyssey/skills/odd-memory/scripts/odd_recall.py b/marketplace/oddyssey/skills/odd-memory/scripts/odd_recall.py index e5b75e4e..f11c4e60 100644 --- a/marketplace/oddyssey/skills/odd-memory/scripts/odd_recall.py +++ b/marketplace/oddyssey/skills/odd-memory/scripts/odd_recall.py @@ -13,7 +13,7 @@ stdout carries the matches only, one per line, tab-separated. A report kind prints - filename kind services|project stack environment mode depth verifies workload repository + filename kind services|project stack environment mode verifies workload repository (``-`` for an absent value; a plan carries its ``project`` in the third column and ``-`` in the observation-only ones). A benchmark is a @@ -29,15 +29,15 @@ stderr carries what is not a match: a report the memory contract's frontmatter checks flag, or a benchmark whose manifest a recall cannot -read (listed all the same, never skipped silently), a newer quick -report a full mission skips, a scope matching nothing and what exists -instead, an absent store. Exit 0 in every one of those cases - a first +read (listed all the same, never skipped silently), a scope matching +nothing and what exists instead, an absent store. Exit 0 in every one +of those cases - a first run is normal; 2 on a usage error or outside a git repository. python3 odd_recall.py [--repo PATH] [--kind observation|instrumentation|benchmark] [--service S ...] [--stack S] [--env E] - [--depth quick|full] [--mode M ...] [--project P] + [--mode M ...] [--project P] """ from __future__ import annotations @@ -54,8 +54,6 @@ sys.path.insert(0, str(Path(__file__).resolve().parent)) from odd_report import ( DATE_RE, - DEPTHS, - LEGACY_PREFIX, as_list, check_report, parse_value, @@ -77,7 +75,6 @@ "stack", "environment", "mode", - "depth", "verifies", "workload", "repository", @@ -102,13 +99,8 @@ def git_root(path: Path) -> Path: def check(report: dict, stored_names: set[str], root: Path) -> list[str]: """What the report lacks against the frontmatter contract - the checks - get-status's memory invariant makes, minus the legacy note on an absent - depth (it reads as full, and nothing can change an append-only file).""" - return [ - p - for p in check_report(report, stored_names, root) - if not p.startswith(LEGACY_PREFIX) - ] + get-status's memory invariant makes.""" + return check_report(report, stored_names, root) # --- the frontmatter, read as the contract writes it --------------------------------- @@ -160,7 +152,6 @@ def line_of(report: dict) -> str: cell(fm.get("stack")), "-" if plan else cell(fm.get("environment")), "-" if plan else cell(fm.get("mode")), - "-" if plan else cell(fm.get("depth")), "-" if plan else cell(fm.get("verifies")), "-" if plan else cell(fm.get("workload")), cell(fm.get("repository")), @@ -175,7 +166,6 @@ def describe(scope: dict) -> str: ("stack", scope["stack"]), ("environment", scope["environment"]), ("mode", ", ".join(scope["modes"])), - ("depth", scope["depth"]), ("project", scope["project"]), ): if value: @@ -197,23 +187,6 @@ def recall(root: Path, kind: str, scope: dict) -> tuple[list[str], list[str]]: # field the scope matches on must never hide the report silently problems = {r["name"]: check(r, stored, root) for r in reports} matched = [r for r in reports if matches(r, scope)] - if kind == "observation" and scope["depth"] == "full": - kept, skipped = [], [] - for r in matched: - depth = r["frontmatter"].get("depth") - if depth is not None and str(depth) != "full": - if not kept: # ahead of the baseline: name the skip - skipped.append(r["name"]) - continue - kept.append(r) - if kept: - err.extend(f"newer quick report skipped: {name}" for name in skipped) - elif skipped: - err.append( - f"no full match; {len(skipped)} quick report(s) skipped: " - + ", ".join(skipped) - ) - matched = kept matched_names = {r["name"] for r in matched} for r in matched: out.append(line_of(r)) @@ -478,30 +451,24 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument( "--env", help="the detected environment; omit while provisional" ) - parser.add_argument("--depth", choices=DEPTHS, help="the mission's depth") parser.add_argument("--mode", action="append", default=[], help="repeatable") parser.add_argument( "--project", help="the scope a plan must cover (instrumentation)" ) args = parser.parse_args(argv) - if args.kind == "instrumentation" and ( - args.service or args.env or args.mode or args.depth - ): - parser.error( - "--service, --env, --mode and --depth apply to observation reports only" - ) + if args.kind == "instrumentation" and (args.service or args.env or args.mode): + parser.error("--service, --env and --mode apply to observation reports only") if args.kind != "instrumentation" and args.project: parser.error("--project applies to instrumentation reports only") - if args.kind == "benchmark" and (args.stack or args.env or args.mode or args.depth): + if args.kind == "benchmark" and (args.stack or args.env or args.mode): parser.error( - "--stack, --env, --mode and --depth apply to the report kinds only;" + "--stack, --env and --mode apply to the report kinds only;" " a benchmark is recalled by --service and by name" ) scope = { "services": args.service, "stack": args.stack, "environment": args.env, - "depth": args.depth, "modes": args.mode, "project": args.project, } diff --git a/marketplace/oddyssey/skills/odd-memory/scripts/odd_report.py b/marketplace/oddyssey/skills/odd-memory/scripts/odd_report.py index d1a50acb..ebf02897 100644 --- a/marketplace/oddyssey/skills/odd-memory/scripts/odd_report.py +++ b/marketplace/oddyssey/skills/odd-memory/scripts/odd_report.py @@ -20,7 +20,7 @@ read by one module. odd_report.py new --service S [--service S ...] --stack S --env E - --mode M --depth D --window START/END | --from START --to END + --mode M --window START/END | --from START --to END --run-name SLUG (prints the path, then the skeleton to fill) [--verifies FILE] [--workload W] [--instance K=V ...] @@ -35,8 +35,8 @@ odd_report.py show PATH odd_report.py persist PATH [--body DRAFT] [--no-commit] odd_report.py baseline [TARGET] [--service S ...] [--stack S] [--env E] - [--depth D] [--own-protocol] [--repo PATH] - (a replay's baseline, mode and depth; exit 3 with an + [--own-protocol] [--repo PATH] + (a replay's baseline and mode; exit 3 with an ``ask:`` line when only the user can settle it) odd_report.py boundary PATH [--runtime NAME ...] [--non-runtime NAME ...] [--repo PATH] (verification, re-measure or undecidable - exit 3) @@ -74,7 +74,6 @@ REPORT_FILE_RE = re.compile(r"\d{4}-\d{2}-\d{2}-\d{4}-[a-z0-9][a-z0-9-]*\.md") OBSERVATION_MODES = ("drive", "observe", "post-hoc", "verify", "re-measure") REPLAY_MODES = ("verify", "re-measure") -DEPTHS = ("quick", "full") PREFIXES = {"verify": "verify-", "re-measure": "remeasure-"} SUBJECTS = { "verify": "verification report", @@ -204,7 +203,6 @@ def body_contract() -> str: "stack", "environment", "mode", - "depth", "window", "run_name", "date", @@ -217,12 +215,11 @@ def body_contract() -> str: "process_restarted", "stack_friction", ) -VERDICTS = ("fixed", "still present", "worse", "not ruled (quick)") -FATES = ("filled", "still missing", "new", "not ruled (quick)") +VERDICTS = ("fixed", "still present", "worse") +FATES = ("filled", "still missing", "new") RULING_HEADER = ["#", "Baseline finding", "Verdict", "Evidence"] PLACEHOLDER = "" PLACEHOLDER_RE = re.compile(r"]*>") -LEGACY_PREFIX = "depth absent (predates" MAX_FINDING_TITLE = 80 MAX_ROWS = 10 # the synthesis's cap per table, the rest behind "+N more" MAX_LINE = 200 @@ -244,22 +241,18 @@ def body_contract() -> str: r"^\W*(?:recalled baseline|previous report|baseline report|baseline)\b", re.IGNORECASE, ) -BASELINE_NOTE_RE = re.compile( - r"provisional|baseline .*dropped|newer quick report skipped", re.IGNORECASE -) +BASELINE_NOTE_RE = re.compile(r"provisional|baseline .*dropped", re.IGNORECASE) DELTA_RE = re.compile( r"^\W*deltas?\b|against the (?:recalled )?baseline|vs\.? (?:the )?baseline" r"|^\W*[^:—]{2,80}(?::|—)\s*\**(?:improved|regressed|unchanged|new)\b", re.IGNORECASE, ) -NOT_QUERIED_RE = re.compile(r"^\W*not queried\b", re.IGNORECASE) NONE_RE = re.compile(r"^\W*none\b", re.IGNORECASE) GAP_SPLIT = " — " SEVERE_RE = re.compile(r"\b(?:high|critical)\b", re.IGNORECASE) CONFIRMED_RE = re.compile(r"^\W*confirmed", re.IGNORECASE) PASS_RE = re.compile(r"\bpass", re.IGNORECASE) FAIL_RE = re.compile(r"\bfail", re.IGNORECASE) -NOT_RULED_RE = re.compile(r"not ruled", re.IGNORECASE) class Refusal(Exception): @@ -280,7 +273,7 @@ def split_top_level(text: str, sep: str = ",") -> list[str]: the beginning of an item or right after a mapping colon - so an apostrophe inside a bare word is just a character. """ - parts, buf, depth, quote = [], [], 0, None + parts, buf, nesting, quote = [], [], 0, None for ch in text: if quote: buf.append(ch) @@ -290,10 +283,10 @@ def split_top_level(text: str, sep: str = ",") -> list[str]: if ch in ("'", '"') and scalar_can_start(buf): quote = ch elif ch in "[{": - depth += 1 + nesting += 1 elif ch in "]}": - depth -= 1 - if ch == sep and depth == 0: + nesting -= 1 + if ch == sep and nesting == 0: parts.append("".join(buf)) buf = [] else: @@ -1108,15 +1101,8 @@ def read_report(path: Path, kind: str) -> dict: } -def check_report( - report: dict, stored_names: set[str], root: Path, written_now: bool = False -) -> list[str]: - """What the report lacks against the memory contract's frontmatter. - - ``written_now`` is the write-time reading: ``depth`` is required. The - status reads a stored report without it as a legacy file that - predates the field (the problem starts with ``LEGACY_PREFIX``). - """ +def check_report(report: dict, stored_names: set[str], root: Path) -> list[str]: + """What the report lacks against the memory contract's frontmatter.""" problems: list[str] = [] name = Path(report.get("path") or report["name"]).name match = REPORT_NAME_RE.match(name) @@ -1152,15 +1138,6 @@ def scalar(key: str) -> str | None: mode = values.get("mode") if mode is not None and mode not in OBSERVATION_MODES: problems.append(f"mode {mode!r} is not one of {list(OBSERVATION_MODES)}") - depth = fm.get("depth") - if depth is None: - problems.append( - "depth absent" - if written_now - else "depth absent (predates the field: reads as full)" - ) - elif str(depth) not in DEPTHS: - problems.append(f"depth {str(depth)!r} is not one of {list(DEPTHS)}") window = values.get("window") if window is not None: wm = WINDOW_RE.match(window) @@ -1483,7 +1460,7 @@ def check_rulings(sections: list[dict], baseline: list[dict]) -> list[str]: return problems -def check_file(path: Path, written_now: bool = False, body: bool = False) -> list[str]: +def check_file(path: Path, body: bool = False) -> list[str]: """The report's problems: the hook's frontmatter and filename rules, and with ``body`` the write-time body rules. An empty list is a report the contract accepts; a file that is no report is a refusal.""" @@ -1503,7 +1480,7 @@ def check_file(path: Path, written_now: bool = False, body: bool = False) -> lis else set() ) root = path.parents[2] - problems = check_report(report, stored, root, written_now=written_now) + problems = check_report(report, stored, root) if body: problems.extend(check_body(report, root)) return problems @@ -1545,7 +1522,7 @@ def gap_bullets(baseline: list[dict]) -> list[tuple[str, str]]: return [] found = [] for item in items(current["lines"]): - if not item.startswith("- ") or NOT_QUERIED_RE.match(item[2:]): + if not item.startswith("- "): continue parts = item[2:].split(GAP_SPLIT) if len(parts) >= 3: @@ -1644,7 +1621,6 @@ def new_instrumentation_report(args: argparse.Namespace) -> tuple[Path, str, lis ("--service", args.service), ("--env", args.env), ("--mode", args.mode), - ("--depth", args.depth), ("--window", args.window), ("--from/--to", args.start or args.end), ("--verifies", args.verifies), @@ -1746,8 +1722,6 @@ def new_report(args: argparse.Namespace) -> tuple[Path, str, list[str]]: raise Refusal( f"--mode is one of {', '.join(OBSERVATION_MODES)}, not {args.mode!r}" ) - if args.depth is not None and args.depth not in DEPTHS: - raise Refusal(f"--depth is one of {', '.join(DEPTHS)}, not {args.depth!r}") # the two instants a query script printed (--from START --to END) are # the window as recorded, pasted as they are: never recomputed by hand if args.window and (args.start or args.end): @@ -1783,7 +1757,6 @@ def new_report(args: argparse.Namespace) -> tuple[Path, str, list[str]]: baseline_sections: list[dict] | None = None baseline_kind = None run_name = args.run_name - depth = args.depth if replay: target = baseline_path(root, args.verifies) if not target.is_file(): @@ -1800,25 +1773,8 @@ def new_report(args: argparse.Namespace) -> tuple[Path, str, list[str]]: if not run_name: raise Refusal("the baseline carries no run_name; pass --run-name") notes.append(f"run_name {run_name} inherited from the baseline") - if depth is None: - if baseline_kind == "instrumentation": - depth = "full" - notes.append( - "depth full: an instrumentation baseline replays every signal" - ) - elif base_fm.get("depth") is None: - depth = "quick" - notes.append( - "depth quick: the baseline predates the field (it ran full; a " - "replay of it is quick unless the caller says full)" - ) - else: - depth = str(base_fm["depth"]) - notes.append(f"depth {depth} inherited from the baseline") if baseline_kind == "observation": baseline_sections = raw_sections(base["body"]) - if depth is None: - raise Refusal("--depth is required (quick or full)") if run_name is None: raise Refusal("--run-name is required") if not SLUG_RE.match(run_name): @@ -1851,7 +1807,6 @@ def new_report(args: argparse.Namespace) -> tuple[Path, str, list[str]]: "stack": args.stack, "environment": args.env, "mode": args.mode, - "depth": depth, "window": args.window, "run_name": run_name, "date": date, @@ -2076,7 +2031,6 @@ def synthesis_data(text: str, kind: str | None = None) -> dict: ], "mode": mode, "replay": replay, - "quick": str(fm.get("depth")) == "quick", "baseline_lines": [], "baseline_name": None, "no_baseline": False, @@ -2084,7 +2038,6 @@ def synthesis_data(text: str, kind: str | None = None) -> dict: "checks": [], "rulings": [], "findings": [], - "not_queried": None, "gaps": [], "decisions": [], "decisions_none": None, @@ -2158,15 +2111,7 @@ def synthesis_data(text: str, kind: str | None = None) -> dict: five = section(sections, 5) if five is not None: for item in items(five["lines"]): - if NOT_QUERIED_RE.match(item) and data["not_queried"] is None: - data["not_queried"] = item - elif ( - item.startswith("- ") - and NOT_QUERIED_RE.match(item[2:]) - and data["not_queried"] is None - ): - data["not_queried"] = item[2:] - elif item.startswith("- "): + if item.startswith("- "): data["gaps"].append(item[2:]) six = section(sections, 6) if six is not None: @@ -2263,10 +2208,8 @@ def synthesis_text(data: dict) -> str: else ["(no findings table in section 3)"] ) out.append("--- section 5: telemetry gaps") - if data["not_queried"]: - out.append(data["not_queried"]) out += [f"- {g}" for g in data["gaps"]] - if not data["not_queried"] and not data["gaps"]: + if not data["gaps"]: out.append("(no gap bullet in section 5)") out.append("--- section 6: open decisions") if data["decisions"]: @@ -2300,7 +2243,7 @@ def locate(path: Path) -> tuple[Path | None, str]: return root, rel.as_posix() -# --- a replay's preflight: the baseline, the mode, the depth, the boundary ----------- +# --- a replay's preflight: the baseline, the mode, the boundary ---------------------- def stored_reports(root: Path) -> list[dict]: @@ -2540,20 +2483,6 @@ def walk_mode(root: Path, baseline: dict) -> tuple[str, str]: current, hops = nxt, hops + 1 -def replay_depth(baseline: dict, override: str | None) -> tuple[str, str]: - if override: - return override, "the argument" - if baseline["kind"] == "instrumentation": - return "full", "an instrumentation baseline replays every signal" - depth = baseline["frontmatter"].get("depth") - if depth is None: - return "quick", ( - "the baseline predates the depth field (it ran full); say `full verify` " - "to replay at the protocol it ran" - ) - return str(depth), "the baseline's depth field" - - def recorded_target(body: str) -> str | None: """The record's base URL, when it recorded one (``n/a`` is none).""" record = scenario_record(body) or "" @@ -2575,7 +2504,6 @@ def baseline_facts(root: Path, args: argparse.Namespace) -> dict: resolved = resolve_report(root, args.target, args.service, args.stack, args.env) baseline, how = hop_to_baseline(root, resolved, args.own_protocol) mode, mode_why = walk_mode(root, baseline) - depth, depth_why = replay_depth(baseline, args.depth) fm = baseline["frontmatter"] sections = raw_sections(baseline["body"]) benchmarks = [m["path"] for m in benchmark_mentions(sections, baseline["body"])] @@ -2606,8 +2534,6 @@ def baseline_facts(root: Path, args: argparse.Namespace) -> dict: ), "mode": mode, "mode_why": mode_why, - "depth": depth, - "depth_why": depth_why, "revision": fm.get("revision"), "benchmarks": benchmarks, "target": target, @@ -2631,7 +2557,6 @@ def render_baseline(facts: dict) -> str: else str(env or "none") ), f"mode: {facts['mode']} ({facts['mode_why']})", - f"depth: {facts['depth']} ({facts['depth_why']})", f"revision: {facts['revision'] or 'none'}", f"benchmark: {', '.join(facts['benchmarks']) or 'none named'}", f"target: {facts['target'] or 'not recorded'}", @@ -2924,21 +2849,10 @@ def plural(count: int, noun: str, nouns: str | None = None) -> str: return f"{count} {noun if count == 1 else (nouns or noun + 's')}" -def verdict_counts(rows: list[list[str]]) -> tuple[int, int, int]: +def verdict_counts(rows: list[list[str]]) -> tuple[int, int]: passed = sum(bool(PASS_RE.search(r[3])) and not FAIL_RE.search(r[3]) for r in rows) failed = sum(bool(FAIL_RE.search(r[3])) for r in rows) - unruled = sum(bool(NOT_RULED_RE.search(r[3])) for r in rows) - return passed, failed, unruled - - -def not_queried_summary(line: str | None) -> str | None: - if not line: - return None - match = re.match(r"^\W*not queried \([^)]*\):\s*([^—.;]+)", line, re.IGNORECASE) - if not match: - return "some signals not queried" - names = match.group(1).strip().rstrip(",") - return f"{names} not queried" + return passed, failed def dominant_approach(approaches: dict) -> str: @@ -3042,21 +2956,19 @@ def render_headline(data: dict) -> str: gaps = len(data["gaps"]) mode = data["mode"] if mode == "verify": - passed, failed, unruled = verdict_counts(data["checks"]) + passed, failed = verdict_counts(data["checks"]) total = len(data["checks"]) if total: text = f"{'FAIL' if failed else 'PASS'} — {passed}/{total} checks passed" if failed: text = f"FAIL — {failed}/{total} checks red" - if unruled: - text += f", {unruled} not ruled (quick)" else: text = ( f"verify — {plural(len(data['rulings']), 'baseline finding')} ruled, " "no check table" ) elif mode == "re-measure": - passed, failed, unruled = verdict_counts(data["checks"]) + passed, failed = verdict_counts(data["checks"]) total = len(data["checks"]) text = ( f"{'drift' if failed else 'no drift'} — {passed}/{total} checks within range" @@ -3072,9 +2984,6 @@ def render_headline(data: dict) -> str: text += f", vs baseline {data['baseline_name']}" elif data["no_baseline"]: text += ", no previous report" - if data["quick"]: - summary = not_queried_summary(data["not_queried"]) - text = f"quick — {text}" + (f", {summary}" if summary else "") return f"**{text}**" @@ -3092,7 +3001,6 @@ def render_show(data: dict, rel: str, commit: str | None) -> str: ("services", ", ".join(as_list(fm.get("services")))), ("stack", fm.get("stack")), ("mode", fm.get("mode")), - ("depth", fm.get("depth") or "full"), ("window", fm.get("window")), ("environment", fm.get("environment")), ] @@ -3137,15 +3045,13 @@ def render_show(data: dict, rel: str, commit: str | None) -> str: ] out += table_lines(["Severity", "Confidence", "Finding"], rows, MAX_ROWS) out.append("") - if data["not_queried"]: - out.append(cap(data["not_queried"], MAX_LINE)[0]) if data["gaps"]: out.append("Telemetry gaps:") for gap in data["gaps"][:MAX_ROWS]: out.append(f"- {gap_line(gap)}") if len(data["gaps"]) > MAX_ROWS: out.append(f"+{len(data['gaps']) - MAX_ROWS} more in the report") - if data["not_queried"] or data["gaps"]: + if data["gaps"]: out.append("") count = len(data["decisions"]) out.append(f"Decisions the spec must settle: {count}") @@ -3181,11 +3087,9 @@ def render_show(data: dict, rel: str, commit: str | None) -> str: def next_action(data: dict) -> str: mode = data["mode"] if mode == "verify": - _, failed, unruled = verdict_counts(data["checks"]) + _, failed = verdict_counts(data["checks"]) if failed: return "back to the fix plan - the red checks name what did not land; replay the protocol with /odd-verify once it does." - if unruled: - return "replay the protocol with /odd-verify at full depth to rule what this quick run left unruled." return "nothing left to verify from this replay; the next observation when the loop's cadence is due." if mode == "re-measure": return "no fix was under test; build the fix plan from the baseline report, then replay its protocol with /odd-verify." @@ -3225,8 +3129,8 @@ def splice_body(path: Path, draft: Path) -> list[str]: counted = recount_friction(path) if counted is not None: notes.append(f"{FRICTION_KEY}: {counted} (section {FRICTION_NUMBER} recounted)") - frontmatter_problems = check_file(path, written_now=True) - problems = check_file(path, written_now=True, body=True) + frontmatter_problems = check_file(path) + problems = check_file(path, body=True) if problems: # the file keeps what new wrote - a replay's pre-filled rulings and # gaps included - and the draft is what the run fixes; a problem of @@ -3249,7 +3153,7 @@ def persist( """The return value's lines (stdout) and the notes (stderr).""" spliced = splice_body(path, body) if body is not None else [] # a spliced draft was checked as it landed; a file persisted as it is is checked here - problems = [] if body is not None else check_file(path, written_now=True, body=True) + problems = [] if body is not None else check_file(path, body=True) if problems: raise Refusal( "the report does not follow the memory contract - fix it before " @@ -3367,11 +3271,6 @@ def main(argv: list[str] | None = None) -> int: metavar="MODE", help="drive | observe | post-hoc | verify | re-measure", ) - p.add_argument( - "--depth", - metavar="DEPTH", - help="quick | full (a replay inherits the baseline's)", - ) p.add_argument( "--window", metavar="START/END", @@ -3446,14 +3345,13 @@ def main(argv: list[str] | None = None) -> int: p = sub.add_parser( "baseline", - help="a replay's baseline, verifies value, mode and depth (exit 3: ask)", + help="a replay's baseline, verifies value and mode (exit 3: ask)", ) p.add_argument("target", nargs="?", help="a report path, or enough of a run name") p.add_argument("--repo", default=".", help="a path inside the repository") p.add_argument("--service", action="append", default=[], help="repeatable") p.add_argument("--stack") p.add_argument("--env", help="the deployment environment the baseline ran on") - p.add_argument("--depth", choices=DEPTHS, help="the argument's depth, which wins") p.add_argument( "--own-protocol", action="store_true", @@ -3528,7 +3426,7 @@ def main(argv: list[str] | None = None) -> int: if not path.is_file(): raise Refusal(f"no such file: {path}") if args.command == "check": - problems = check_file(path, written_now=True, body=True) + problems = check_file(path, body=True) for problem in problems: print(f"{path.name}: {problem}", file=sys.stderr) if problems: diff --git a/src/mcp-server/pyproject.toml b/src/mcp-server/pyproject.toml index ac815f9d..c8737b03 100644 --- a/src/mcp-server/pyproject.toml +++ b/src/mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oddyssey-mcp" -version = "1.12.2" +version = "1.13.0" description = "Oddyssey MCP server: pilots the local observability stack for CLI coding agents" requires-python = ">=3.12" license = "MIT" diff --git a/src/mcp-server/uv.lock b/src/mcp-server/uv.lock index e758ce32..ccaaedc5 100644 --- a/src/mcp-server/uv.lock +++ b/src/mcp-server/uv.lock @@ -506,7 +506,7 @@ wheels = [ [[package]] name = "oddyssey-mcp" -version = "1.12.2" +version = "1.13.0" source = { editable = "." } dependencies = [ { name = "httpx" },