stacktrace-cli 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/PKG-INFO +1 -1
- stacktrace_cli-0.2.2/docs/adrs/0028-the-console-shows-what-it-placed.md +108 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/INDEX.md +1 -0
- stacktrace_cli-0.2.2/docs/releases/v0.2.2.md +76 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/specs/monitor.md +25 -9
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/pyproject.toml +1 -1
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/__init__.py +1 -1
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/render.py +6 -2
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/site/app.js +48 -46
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/site/index.html +0 -2
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/monitor/feed_harness.mjs +62 -12
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/monitor/test_feed_model.py +55 -31
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/monitor/test_render.py +19 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/monitor/test_site_assets.py +8 -4
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/uv.lock +1 -1
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/.agents/skills/release-stacktrace/SKILL.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/.claude/settings.json +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/.claude/skills/release-stacktrace/SKILL.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/.codex/hooks.json +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/.github/workflows/autofix.yml +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/.github/workflows/ci.yml +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/.github/workflows/claude.yml +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/.github/workflows/publish-pypi.yml +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/.gitignore +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/AGENTS.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/CLAUDE.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/README.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0001-session-telemetry-as-input.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0002-session-collection-in-openaidr.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0003-runtime-edges.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0004-three-stage-detector.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0005-detection-family-and-report-assembly.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0006-trust-boundary-and-detection-upload.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0007-proprietary-package-on-open-dependencies.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0008-console-in-separate-demo-package.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0010-detection-severity-and-confidence-ladders.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0011-verdict-cache.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0012-observation-evidence-kinds-and-transport.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0013-rule-catalogue-triage-and-per-rule-context.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0014-declared-project-mapping.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0015-narrow-the-security-catalogue.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0016-stall-grouping-is-session-wide.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0017-a-declined-repeat-is-a-stall.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0018-remote-sync-config-and-facade-consumption.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0019-delegated-commands-are-openaca-objects.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0020-openaca-consumption-boundary.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0021-two-command-kinds.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0022-detection-scope-is-a-catalogue-column.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0023-sync-detect-collects-and-does-not-escalate.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0024-the-upload-carries-observations.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0025-a-denied-call-is-activity-never-an-invocation.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0026-the-console-becomes-a-product-surface.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/0027-a-finding-may-name-what-it-could-not-place.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/HOOK-PROMPT.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/adrs/TEMPLATE.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/cutover-openaca-remote.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/plans/002-session-input.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/plans/003-correlation.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/plans/004-detector.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/plans/005-remote-sync.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/plans/006-cli-composition.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/plans/007-detection-upload.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/plans/008-monitor.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/releases/v0.0.1.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/releases/v0.1.0.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/releases/v0.2.0.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/releases/v0.2.1.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/specs/aidr.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/specs/cli-composition.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/specs/correlation.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/specs/detection-upload.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/specs/detector.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/specs/remote-sync.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/docs/specs/session-input.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/scripts/git-hooks/pre-push +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/scripts/install-hooks.sh +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/__main__.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/analysis.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/cli.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/correlate/__init__.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/correlate/acquire.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/correlate/composition.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/correlate/join.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/correlate/observed.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/correlate/orchestrate.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/correlate/project_map.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/correlate/record.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/correlate/render.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/__init__.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/analyzer.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/cache.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/deterministic.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/finding.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/markers.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/priors.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/prompts/__init__.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/prompts/v1/exclusions.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/prompts/v1/framing.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/prompts/v1/stacktrace-deceptive-completion.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/prompts/v1/stacktrace-injected-instruction-followed.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/prompts/v1/stacktrace-intent-drift.md +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/reasoning.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/render.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/rules.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/run.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/secrets.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/detector/verdict.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/__init__.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/escalate.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/server.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/site/fonts/OFL.txt +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/site/fonts/dm-mono-400-latin.woff2 +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/site/fonts/dm-mono-500-latin.woff2 +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/site/fonts/dm-sans-latin.woff2 +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/site/styles.css +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/state.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/verdicts.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/monitor/watch.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/remote/__init__.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/remote/cli.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/remote/client.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/remote/config.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/remote/detect_payload.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/remote/payload.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/remote/policy.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/remote/redact.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/remote/spool.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/remote/sync.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/remote/sync_detect.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/remote/upload_contract.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/sessions/__init__.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/sessions/access.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/sessions/outcome.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/sessions/protocols.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/src/stacktrace_cli/sessions/render.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/__init__.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/agent_bom_fixture.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/detector_session_fixture.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/monitor/__init__.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/monitor/test_escalation_gates.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/monitor/test_server.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/monitor/test_state.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/monitor/test_verdicts.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/monitor/test_watch.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/__init__.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/helpers.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_cli.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_client.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_config.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_detect_activity.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_detect_cli.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_detect_client.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_detect_contract_is_exhaustive.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_detect_gate_matches_the_cloud.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_detect_layers_compose.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_detect_payload_reaches_the_cloud_model.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_detect_redaction.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_detect_spool.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_detect_upload_contract.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_detect_wire_payload.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_facade_contract.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_payload.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_policy.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_redact_payload.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_seam.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_spool.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_sync.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_sync_detect.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/remote/test_upload_contract.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_acquire.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_agent_instance_id.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_analysis.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_bom_shape.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_call_status_conventions.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_cli.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_composition.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_correlate_orchestrate.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_correlate_render.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_correlated_session.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detection_components.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detection_subject.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_analyzer_contract.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_analyzer_live.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_cache.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_deterministic.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_end_to_end.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_finding.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_liveness.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_markers.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_priors.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_reasoning.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_render.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_run.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_secrets.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_detector_verdict.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_fingerprint_covers_what_the_analyzer_reads.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_help_sections.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_join.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_monitor_cli.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_observed.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_openaca_contract.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_openaidr_contract.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_option_arity_matches_the_binary.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_project_map.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_readme_examples_are_real_output.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_release_readiness.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_seam_boundary.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_sessions_access.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_sessions_end_to_end.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_sessions_protocol_typing.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_sessions_protocols.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_sessions_render.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_shell_separators_match_the_shell.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_untrusted_content_never_travels.py +0 -0
- {stacktrace_cli-0.2.1 → stacktrace_cli-0.2.2}/tests/test_verification_subcommands_match_the_binary.py +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 0028
|
|
3
|
+
title: The console shows what it placed, and states no coverage
|
|
4
|
+
status: accepted
|
|
5
|
+
date: 2026-09-08
|
|
6
|
+
supersedes: null
|
|
7
|
+
superseded-by: null
|
|
8
|
+
amends: null
|
|
9
|
+
amended-by: null
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Context
|
|
13
|
+
|
|
14
|
+
A reader opened `stacktrace monitor` on their own machine and asked why the
|
|
15
|
+
busiest row on the page was red. The row was the grouped built-in row for one
|
|
16
|
+
project — 4,467 calls over five days — and it carried `⚠ 16 refusals`. All 16
|
|
17
|
+
were `user-rejected` in the transcript, and their recorded reasons were the
|
|
18
|
+
Bash gate rather than a person: 29 `contains multiple operations`, 28 `requires
|
|
19
|
+
approval`, 6 `changes directory before running git`, 3 `brace with quote
|
|
20
|
+
character (expansion obfuscation)`, 2 `parser aborted`, 1 `contains
|
|
21
|
+
for_statement` across the project's 70 denials. Nothing reached a tool.
|
|
22
|
+
|
|
23
|
+
`sessions/outcome.py` already states the rule: *a denied call reached nothing,
|
|
24
|
+
so it is evidence of nothing* — written after six sites had each grown their own
|
|
25
|
+
`status == "rejected"` test. The page took that rule for its counters and then
|
|
26
|
+
painted the row red anyway, which reports the permission prompt as the
|
|
27
|
+
incident. ADR-0025 left this open deliberately: its Consequences name "a second
|
|
28
|
+
counter every activity read surface must decide whether to show".
|
|
29
|
+
|
|
30
|
+
The same page led with `⚠ unresolved` rows, an `Unresolved` row in the overview
|
|
31
|
+
table, a legend line, and a `6,867/7,055 placed` header fraction. `unresolved`
|
|
32
|
+
is a fact about the *record*, with four causes the reader cannot distinguish and
|
|
33
|
+
none of which they can act on: absent from the composition, no composition for
|
|
34
|
+
that agent kind, an ambiguous match, or a matched component whose type the
|
|
35
|
+
category map does not know. The row names no component — `classify_call`
|
|
36
|
+
returns no name for it by design (ADR-0027 admits a name at one position, and an
|
|
37
|
+
activity row is not one) — so what reached the reader was "something happened
|
|
38
|
+
and nothing here claims it", set in the page's warning colour.
|
|
39
|
+
|
|
40
|
+
The rule that would have made such a call actionable, `unsanctioned-mcp-tool-use`,
|
|
41
|
+
was withdrawn by ADR-0015. So no live rule is about an unplaceable call, and the
|
|
42
|
+
page was warning about something it could not then explain.
|
|
43
|
+
|
|
44
|
+
## Decision
|
|
45
|
+
|
|
46
|
+
The console renders only activity it could place, and states no coverage
|
|
47
|
+
figure. Rows whose category is `unresolved` are dropped at the page boundary;
|
|
48
|
+
the `N/M placed` fraction and its `placing…` placeholder are gone from the
|
|
49
|
+
header, which now counts sessions and the actions it actually shows; a refusal
|
|
50
|
+
is counted in a row's `×N` and carries no badge and no colour; the
|
|
51
|
+
no-composition banner is gone, leaving the two banners that report a *reader*
|
|
52
|
+
which came back with nothing.
|
|
53
|
+
|
|
54
|
+
The snapshot contract does not change. `_activity` still emits unresolved rows,
|
|
55
|
+
`summary.coverage` and each session's `coverage` are still computed and still
|
|
56
|
+
sent, `activity_for_upload` and the `denied` counter are untouched, and every
|
|
57
|
+
detector rule reads what it always read. `stacktrace correlate` remains the
|
|
58
|
+
surface where coverage and unplaceability are the subject; ADR-0026's clause
|
|
59
|
+
holds, because a page that shows less has still invented nothing.
|
|
60
|
+
|
|
61
|
+
## Alternatives considered
|
|
62
|
+
|
|
63
|
+
- **Filter in `_activity`, server-side** — rejected. The snapshot is also the
|
|
64
|
+
`/api/state` contract and the thing `tests/monitor/test_render.py` holds; a
|
|
65
|
+
page decision that narrows the record makes the two disagree, and the next
|
|
66
|
+
surface built on the snapshot would inherit a filter it never asked for.
|
|
67
|
+
- **Keep the rows, drop only the warning framing** — rejected by the person the
|
|
68
|
+
page is for. Neutral wording does not give the row a subject: with no
|
|
69
|
+
component name there is nothing for a reader to look up, so a quiet row is
|
|
70
|
+
the same non-answer in a calmer colour.
|
|
71
|
+
- **Keep the coverage fraction** — rejected. The gap between numerator and
|
|
72
|
+
denominator *is* the count of rows the page no longer shows, so the header
|
|
73
|
+
restated by arithmetic exactly what the feed had stopped saying.
|
|
74
|
+
- **Keep the red on refusals alone** — rejected. It is the page asserting the
|
|
75
|
+
opposite of `outcome.py`'s rule about the same call, and the measured reasons
|
|
76
|
+
above are a guardrail working, not an endpoint under attack.
|
|
77
|
+
- **Reword the no-composition banner rather than remove it** — rejected after
|
|
78
|
+
trying it. "No Agent BOM found, so its components are not identified" is the
|
|
79
|
+
same claim one step back, and it sends a reader looking for rows that are no
|
|
80
|
+
longer there.
|
|
81
|
+
|
|
82
|
+
## Consequences
|
|
83
|
+
|
|
84
|
+
The page stops answering one question it used to answer badly: how much of the
|
|
85
|
+
window it could not attribute. A reader who wants that runs `stacktrace
|
|
86
|
+
correlate`, where the figure is the subject and the four causes are named
|
|
87
|
+
separately; the fleet still receives it, because the upload path is untouched.
|
|
88
|
+
|
|
89
|
+
The costs are real and worth naming. An agent kind with no Agent BOM now shows
|
|
90
|
+
fewer categories in the overview table and says nothing about why — the built-in
|
|
91
|
+
preview rows still appear, so the page does not go blank, but the absence is
|
|
92
|
+
silent. A responder cannot see from the console that an MCP call went to a
|
|
93
|
+
server no scan can find. And a denied-heavy endpoint — ADR-0025's own example of
|
|
94
|
+
a fact visible on no surface — is still visible on none.
|
|
95
|
+
|
|
96
|
+
**Watch for:** a second page-side filter added the same way, without an ADR,
|
|
97
|
+
because this one made it look routine; and the header regaining any derived
|
|
98
|
+
ratio, which is the shape this decision removed.
|
|
99
|
+
|
|
100
|
+
## When to revisit
|
|
101
|
+
|
|
102
|
+
If ADR-0015's `unsanctioned-mcp-tool-use` returns, revisit immediately: its
|
|
103
|
+
evidence is exactly the rows this decision hides, and a finding whose subject
|
|
104
|
+
cannot be seen on the page it is reported on is worse than either state alone.
|
|
105
|
+
Also if the console gains an affordance for *why something is missing* — a
|
|
106
|
+
per-kind detail view, say — which is where the coverage figure and the four
|
|
107
|
+
causes belong, or if a remote surface starts reading the local snapshot and
|
|
108
|
+
assuming the page's filtering rather than the record's contents.
|
|
@@ -60,3 +60,4 @@ supersedes anything yet.
|
|
|
60
60
|
- [0026](0026-the-console-becomes-a-product-surface.md) — **The console becomes a product surface, served by the engine.** Supersedes ADR-0008 on its own stated condition: the engine came first, 0.1.0 shipped, and the data is only useful if a person can follow it. `stacktrace monitor` serves a loopback page from in-process calls, so the wheel now carries a `site/` asset — the cost 0008 refused, accepted here explicitly. What survives from 0008 is the clause that matters: the console renders and never computes, so a finding on the page is one `detect` would print. Read before adding anything to the console that the detector does not produce.
|
|
61
61
|
- [0025](0025-a-denied-call-is-activity-never-an-invocation.md) — **A denied call is activity, and never an invocation.** ADR-0017 makes a declined repeat a stall while `outcome.py` keeps a denied call out of every count, so a denied-only session produced a finding with no activity and the Cloud rejected the whole run; a separate `denied` counter resolves it without either rule bending, and coverage keeps summing `invocations` alone. Read before changing what `activity[]` counts, or before folding `denied` into a coverage-like ratio.
|
|
62
62
|
- [0027](0027-a-finding-may-name-what-it-could-not-place.md) — **A finding may name the component it could not place.** Narrows ADR-0006 constraint 3 for one field: an unplaced subject carries the name the session addressed it by, never as an identity. Read before removing a name from a finding on trust-boundary grounds, or before widening this to a second field.
|
|
63
|
+
- [0028](0028-the-console-shows-what-it-placed.md) — **The console shows what it placed, and states no coverage.** Unplaceable rows, the `N/M placed` header fraction and the red on refusals are gone from `stacktrace monitor`: an unplaced row names no component, and `outcome.py`'s rule about a denied call makes reddening one the page contradicting its own counters. The snapshot contract, the upload and every rule are unchanged. Read before adding a page-side filter, before restoring a coverage ratio to the console, or if `unsanctioned-mcp-tool-use` returns.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# 0.2.2 — what the console shows
|
|
2
|
+
|
|
3
|
+
`stacktrace monitor` shipped in 0.2.0 with two kinds of warning on it, and this
|
|
4
|
+
release removes both. Neither pointed at something a reader could act on: one
|
|
5
|
+
marked a call the correlator could not attribute, which names no component by
|
|
6
|
+
design, and the other marked a call a permission prompt had already blocked.
|
|
7
|
+
The engine is untouched — no rule, payload field, command or output shape
|
|
8
|
+
moves — and the whole delta is what one page renders.
|
|
9
|
+
|
|
10
|
+
## Highlights
|
|
11
|
+
|
|
12
|
+
- **A blocked call is no longer an alarm.** A refusal is counted in its row's
|
|
13
|
+
`×N` and says nothing more: no `⚠ N refusals` badge, no red row. On the
|
|
14
|
+
machine that prompted this, 16 declined `Bash` calls out of 4,467 turned the
|
|
15
|
+
busiest row on the page red — and all 16 were the Bash gate refusing a
|
|
16
|
+
compound command, a `cd` before `git`, or an obfuscated brace. Nothing
|
|
17
|
+
reached a tool. `sessions/outcome.py` has held since 0.1.0 that a denied call
|
|
18
|
+
reached nothing, so it is evidence of nothing; the page took that rule for
|
|
19
|
+
its counters and then reddened the row anyway, which reports the permission
|
|
20
|
+
prompt as the incident. ADR-0025 left this to each surface deliberately, and
|
|
21
|
+
the console now answers it.
|
|
22
|
+
|
|
23
|
+
- **The console shows only activity it could place.** The `⚠ unresolved` rows,
|
|
24
|
+
the `Unresolved` row in the overview table and the legend line describing
|
|
25
|
+
them are gone. An unplaced row carries no component name — that is a design
|
|
26
|
+
constraint, not an omission (ADR-0027 admits a name at one position, and an
|
|
27
|
+
activity row is not one) — so what reached the reader was "something happened
|
|
28
|
+
and nothing here claims it", with four causes behind it they could not
|
|
29
|
+
distinguish and none they could act on. The rule that would have made such a
|
|
30
|
+
call actionable was withdrawn by ADR-0015, so nothing on the page could
|
|
31
|
+
explain the warning it was showing.
|
|
32
|
+
|
|
33
|
+
- **The header states no coverage figure.** `6,867/7,055 placed` and the
|
|
34
|
+
`placing…` placeholder it used while correlation ran are both gone: the gap
|
|
35
|
+
between those two numbers *is* the count of rows the page withholds, so the
|
|
36
|
+
header restated by arithmetic exactly what the rows had stopped saying. It
|
|
37
|
+
now reports sessions and the actions it actually shows — previously it read
|
|
38
|
+
`summary.actions`, the total over every call in the window, and so claimed
|
|
39
|
+
more actions than the feed below it could account for.
|
|
40
|
+
|
|
41
|
+
- **A legend line that described nothing.** `✓ resolved to something you
|
|
42
|
+
installed` is removed. The page has never drawn that checkmark: the glyph
|
|
43
|
+
appeared in the legend and in no row, summary or card.
|
|
44
|
+
|
|
45
|
+
- **The decision is recorded rather than implied.** ADR-0028 states it, lists
|
|
46
|
+
the five alternatives and is explicit about the costs — an agent kind with no
|
|
47
|
+
Agent BOM is now silent on the page, and ADR-0025's denied-heavy endpoint is
|
|
48
|
+
still visible on no surface. `docs/specs/monitor.md` said the opposite in four
|
|
49
|
+
places and now describes what ships.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
`uv tool install stacktrace-cli==0.2.2` or `pip install stacktrace-cli==0.2.2`.
|
|
54
|
+
|
|
55
|
+
## Compatibility
|
|
56
|
+
|
|
57
|
+
Nothing to do, and nothing outside the console changes. `stacktrace detect`,
|
|
58
|
+
`stacktrace correlate` and `stacktrace sync detect` produce byte-identical
|
|
59
|
+
output to 0.2.1, the upload payload is unchanged, and every detector rule reads
|
|
60
|
+
what it always read. No finding is suppressed — ADR-0026's clause that the
|
|
61
|
+
console renders and never judges still holds, and a page that shows fewer
|
|
62
|
+
activity rows invents nothing.
|
|
63
|
+
|
|
64
|
+
The `/api/state` document is also unchanged: it still carries the unplaced
|
|
65
|
+
activity rows and both coverage figures. The filter is applied in the page, so
|
|
66
|
+
anything reading that endpoint sees the same record 0.2.1 served.
|
|
67
|
+
|
|
68
|
+
Two things you will notice if you relied on them. Coverage is no longer on the
|
|
69
|
+
console — run `stacktrace correlate`, where it is the subject and the four
|
|
70
|
+
causes of an unplaced call are named separately. And an agent kind with no
|
|
71
|
+
Agent BOM no longer raises a banner: its built-in rows still appear, so the
|
|
72
|
+
page does not go blank, but the absence is reported by `correlate` rather than
|
|
73
|
+
on screen.
|
|
74
|
+
|
|
75
|
+
Still pre-alpha, with no back-compat hedging: interfaces can change between
|
|
76
|
+
releases without a deprecation window.
|
|
@@ -141,8 +141,11 @@ An unplaced stage is honest in two specific ways, and both are load-bearing:
|
|
|
141
141
|
property over the whole built-in vocabulary, not just the names a fixture
|
|
142
142
|
uses.
|
|
143
143
|
- **Coverage is `None`, not `0/0`.** Correlation computes coverage, so an
|
|
144
|
-
unplaced stage does not have it
|
|
145
|
-
|
|
144
|
+
unplaced stage does not have it, and the snapshot says so rather than
|
|
145
|
+
claiming a figure. The page no longer renders one either way (ADR-0028): the
|
|
146
|
+
header counts sessions and the actions it shows, and the fraction — whose gap
|
|
147
|
+
was the count of rows the page withholds — is gone, along with the `placing…`
|
|
148
|
+
placeholder that stood in for it.
|
|
146
149
|
|
|
147
150
|
Batches are then judged **newest first**, because someone watching a machine
|
|
148
151
|
cares about what just happened and should not scroll for it. The last stage
|
|
@@ -185,10 +188,18 @@ name".** Routine built-ins — Bash, Read, Edit — are the constant background
|
|
|
185
188
|
every session: 5,795 of one three-day window's 7,065 calls. They collapse **per
|
|
186
189
|
project** into one counted row for the agent, which is what makes `Claude Code ·
|
|
187
190
|
working — files, commands & searches · ×1,719` a line a person reads instead of
|
|
188
|
-
1,719 lines they do not. Skills, MCP servers, sub-agents
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
1,719 lines they do not. Skills, MCP servers, sub-agents and commands
|
|
192
|
+
are rare and consequential, so each **call** keeps its own row, keyed by span
|
|
193
|
+
and never coalesced: two runs of one skill are two things that happened. A
|
|
194
|
+
three-day window is ~66 rows.
|
|
195
|
+
|
|
196
|
+
**A call correlation could not place gets no row at all** (ADR-0028). It names
|
|
197
|
+
no component — `classify_call` returns no name for one by design — so the row
|
|
198
|
+
could only say that something happened and nothing here claims it, which is not
|
|
199
|
+
a thing a reader can act on, and the rule that would have made it actionable
|
|
200
|
+
was withdrawn by ADR-0015. The rows are dropped at the page boundary and
|
|
201
|
+
nowhere else: `_activity` still emits them, and `stacktrace correlate` still
|
|
202
|
+
reports them with each of the four causes named.
|
|
192
203
|
|
|
193
204
|
**The summary's "Most used" column names the top three, and says what it left
|
|
194
205
|
out.** A truncated list beside a total reads as a breakdown, and then the
|
|
@@ -207,8 +218,13 @@ exactly the terms the component name is: an ambiguous match names no alias,
|
|
|
207
218
|
because printing the first candidate's configured name over the second's is the
|
|
208
219
|
narrowing correlation refuses to do.
|
|
209
220
|
|
|
210
|
-
A refusal
|
|
211
|
-
of its own
|
|
221
|
+
A refusal is counted in the row's `×N` and says nothing more (ADR-0028). It
|
|
222
|
+
took neither a row of its own — `Bash ×3 denied` beside `Bash ×38` was half the
|
|
223
|
+
noise — nor a badge and a colour, which is the page asserting the opposite of
|
|
224
|
+
`outcome.py`'s rule about the same call: a denied call reached nothing, so it is
|
|
225
|
+
evidence of nothing. On the machine that prompted this, 16 declined `Bash` calls
|
|
226
|
+
out of 4,467 turned the busiest row on the page red, and all 16 were the Bash
|
|
227
|
+
gate refusing a compound command.
|
|
212
228
|
|
|
213
229
|
**Every panel is additive.** A stage re-sends every session and every finding
|
|
214
230
|
the stages before it carried, so a render that replaced the columns threw away
|
|
@@ -399,7 +415,7 @@ only on click, so the ceiling is visible before it is hit.
|
|
|
399
415
|
| No sessions on this machine | An empty page that **says so**. A blank column reads as a broken renderer |
|
|
400
416
|
| A reader failed before correlation (`collection_failures`) | Rendered as a banner. A run that never got to look must not read as a clean one |
|
|
401
417
|
| A session with no agent kind | Counted and named as unplaceable, never rendered under an invented kind |
|
|
402
|
-
| A session with no composition |
|
|
418
|
+
| A session with no composition | Its calls are read, and the page shows what it can place them to. Since ADR-0028 there is no banner and no coverage figure: the built-in preview rows still appear, and the absence itself is reported by `stacktrace correlate` rather than by the console |
|
|
403
419
|
| The port is taken | Refuse with the port named, rather than silently choosing another and printing a URL the user did not expect |
|
|
404
420
|
|
|
405
421
|
## Files
|
|
@@ -176,6 +176,12 @@ def _banners(analysis: Analysis) -> list[str]:
|
|
|
176
176
|
A blank column and a clean run render identically, which is the one
|
|
177
177
|
confusion this page refuses: a reader who cannot tell "nothing happened"
|
|
178
178
|
from "nothing could be read" has been told nothing.
|
|
179
|
+
|
|
180
|
+
Both banners are about a *reader* that came back with nothing. A kind with
|
|
181
|
+
no composition is not one of those and no longer appears: the calls it
|
|
182
|
+
accounts for are read, and the page shows what it can place them to.
|
|
183
|
+
`view.without_composition` is still reported by `stacktrace correlate`,
|
|
184
|
+
where the record rather than the console is the subject.
|
|
179
185
|
"""
|
|
180
186
|
banners: list[str] = []
|
|
181
187
|
for failure in analysis.run.collection_failures:
|
|
@@ -183,8 +189,6 @@ def _banners(analysis: Analysis) -> list[str]:
|
|
|
183
189
|
anonymous = len(analysis.view.kind_anonymous)
|
|
184
190
|
if anonymous:
|
|
185
191
|
banners.append(f"{anonymous} session(s) skipped: no agent kind to attribute them to")
|
|
186
|
-
for kind in analysis.view.without_composition:
|
|
187
|
-
banners.append(f"{kind}: no composition, so every call reads as unresolved")
|
|
188
192
|
return banners
|
|
189
193
|
|
|
190
194
|
|
|
@@ -145,16 +145,20 @@ function renderBanners(banners) {
|
|
|
145
145
|
|
|
146
146
|
function renderCounts(state) {
|
|
147
147
|
const summary = state.summary || {};
|
|
148
|
-
/*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
148
|
+
/* Sessions and actions, and no coverage fraction. `6,867/7,055 placed` is
|
|
149
|
+
* the count of what this page leaves off, stated: the gap between the two
|
|
150
|
+
* numbers is exactly the unplaceable rows, so the header reinstated by
|
|
151
|
+
* arithmetic what the rows below had stopped saying. The snapshot still
|
|
152
|
+
* carries it — `stacktrace correlate` is where coverage is the subject.
|
|
153
|
+
*
|
|
154
|
+
* Actions are counted off the rows this page keeps rather than read from
|
|
155
|
+
* `summary.actions`, which is the server's total over every call in the
|
|
156
|
+
* window. Read straight through, the header claimed more actions than the
|
|
157
|
+
* feed below it could account for. */
|
|
158
|
+
const actions = eventsFrom(state.sessions || []).length;
|
|
154
159
|
text(el("live-counts"),
|
|
155
160
|
" — " + plural(summary.sessions || 0, "session") +
|
|
156
|
-
" · " + plural(
|
|
157
|
-
" · " + (coverage ? coverage.resolved + "/" + coverage.total + " placed" : "placing…"));
|
|
161
|
+
" · " + plural(actions, "action"));
|
|
158
162
|
}
|
|
159
163
|
|
|
160
164
|
/* --- the demo's vocabulary ----------------------------------------------- */
|
|
@@ -209,7 +213,6 @@ const RULE_PLAIN = {
|
|
|
209
213
|
"stacktrace-credential-egress": "A password or token was sent to an outside service.",
|
|
210
214
|
"stacktrace-capability-crossing": "Private data was read, then sent outside.",
|
|
211
215
|
"stacktrace-intent-drift": "It acted on something nobody asked about.",
|
|
212
|
-
"stacktrace-unsanctioned-mcp-tool-use": "A tool ran that isn't in your installed set.",
|
|
213
216
|
"stacktrace-guardrail-modification": "A session changed the agent's own rules or settings.",
|
|
214
217
|
"stacktrace-injection-marker": "Injected-instruction markers were found in what the agent read.",
|
|
215
218
|
"stacktrace-destructive-action": "A destructive action was taken (deleting or overwriting).",
|
|
@@ -227,20 +230,32 @@ const KIND_TAG = {
|
|
|
227
230
|
subagent: ["sub-agent", "kt-sub"],
|
|
228
231
|
mcp: ["mcp", "kt-conn"],
|
|
229
232
|
command: ["command", "kt-cmd"],
|
|
230
|
-
unresolved: ["⚠ unresolved", "kt-warn"],
|
|
231
233
|
};
|
|
232
234
|
|
|
233
235
|
/* Plural, because every one of these labels a count. */
|
|
234
236
|
const KIND_LABEL = {
|
|
235
237
|
mcp: "MCP servers", skill: "Skills", command: "Commands",
|
|
236
|
-
subagent: "Sub-agents", tool: "Built-in tools",
|
|
238
|
+
subagent: "Sub-agents", tool: "Built-in tools",
|
|
237
239
|
};
|
|
238
240
|
|
|
239
241
|
/* Most consequential first, rather than by count: sorted by count the
|
|
240
242
|
* built-ins win every window and push the MCP servers — the only category
|
|
241
243
|
* describing a call that left the machine — under a number three orders of
|
|
242
244
|
* magnitude larger. */
|
|
243
|
-
const KIND_ORDER = ["mcp", "skill", "command", "subagent", "tool"
|
|
245
|
+
const KIND_ORDER = ["mcp", "skill", "command", "subagent", "tool"];
|
|
246
|
+
|
|
247
|
+
/* The category the correlator gives a call it could not place. Every surface
|
|
248
|
+
* on this page drops these rows: an unplaceable call names no component, so
|
|
249
|
+
* the row could only say that something happened and nothing here claims it,
|
|
250
|
+
* which is not a thing a reader can act on. The count is still computed
|
|
251
|
+
* server-side and still reaches the detector — this is a decision about the
|
|
252
|
+
* page, not about the record. */
|
|
253
|
+
const UNPLACEABLE = "unresolved";
|
|
254
|
+
|
|
255
|
+
/* Whether the snapshot row is one of those. */
|
|
256
|
+
function unplaceable(row) {
|
|
257
|
+
return row.category === UNPLACEABLE;
|
|
258
|
+
}
|
|
244
259
|
const TOP_PER_KIND = 3;
|
|
245
260
|
|
|
246
261
|
const SEVERITY_WORD = { critical: "Serious", high: "High", medium: "Medium", low: "Low" };
|
|
@@ -294,10 +309,13 @@ function evParts(e) {
|
|
|
294
309
|
provenance: "a link to an outside service",
|
|
295
310
|
};
|
|
296
311
|
}
|
|
312
|
+
/* A category this build has never heard of. Named as itself rather than
|
|
313
|
+
* described: anything else this function said about it would be invented,
|
|
314
|
+
* and the one category it used to describe no longer reaches the page. */
|
|
297
315
|
return {
|
|
298
316
|
name: e.name,
|
|
299
|
-
action: "used
|
|
300
|
-
provenance: "
|
|
317
|
+
action: "used " + (e.tool || "a tool"),
|
|
318
|
+
provenance: e.kind ? "category " + e.kind : "",
|
|
301
319
|
};
|
|
302
320
|
}
|
|
303
321
|
|
|
@@ -321,6 +339,7 @@ function eventsFrom(sessions) {
|
|
|
321
339
|
const activity = session.activity || [];
|
|
322
340
|
for (let index = 0; index < activity.length; index += 1) {
|
|
323
341
|
const row = activity[index];
|
|
342
|
+
if (unplaceable(row)) continue;
|
|
324
343
|
events.push({
|
|
325
344
|
id: row.span,
|
|
326
345
|
kind: row.category,
|
|
@@ -336,11 +355,6 @@ function eventsFrom(sessions) {
|
|
|
336
355
|
* the skills a reader had just run sat under MCP calls from half a day
|
|
337
356
|
* earlier. Named for what it is, because it is not the start. */
|
|
338
357
|
at: session.last_active || session.started_at,
|
|
339
|
-
/* Both worth a reader's attention; only the first is a claim about
|
|
340
|
-
* attribution. Correlation keeps a refused call's resolution, so the
|
|
341
|
-
* row beside the label names the component it would have reached. */
|
|
342
|
-
unplaced: row.category === "unresolved",
|
|
343
|
-
refused: row.status === "denied",
|
|
344
358
|
order: index,
|
|
345
359
|
});
|
|
346
360
|
}
|
|
@@ -356,7 +370,8 @@ function compositionOf(sessions) {
|
|
|
356
370
|
const seen = new Set();
|
|
357
371
|
for (const session of sessions) {
|
|
358
372
|
for (const row of session.activity || []) {
|
|
359
|
-
|
|
373
|
+
if (unplaceable(row)) continue;
|
|
374
|
+
const type = row.component_type || row.category;
|
|
360
375
|
const key = type + "\n" + (row.component_identity || row.component_name || row.tool_name);
|
|
361
376
|
if (!type || seen.has(key)) continue;
|
|
362
377
|
seen.add(key);
|
|
@@ -372,6 +387,7 @@ function activitySummaryOf(sessions) {
|
|
|
372
387
|
const named = {};
|
|
373
388
|
for (const session of sessions) {
|
|
374
389
|
for (const row of session.activity || []) {
|
|
390
|
+
if (unplaceable(row)) continue;
|
|
375
391
|
byKind[row.category] = (byKind[row.category] || 0) + 1;
|
|
376
392
|
if (!named[row.category]) named[row.category] = new Map();
|
|
377
393
|
const label = entryLabel(row);
|
|
@@ -531,9 +547,9 @@ let revealTimer = null;
|
|
|
531
547
|
* Routine built-ins (Bash, Read, Edit) are the constant background of every
|
|
532
548
|
* session: 5,795 of one window's 7,065 calls. They collapse **per project**
|
|
533
549
|
* into one counting row for the agent. Notable events — skills, MCP servers,
|
|
534
|
-
* sub-agents
|
|
535
|
-
*
|
|
536
|
-
*
|
|
550
|
+
* sub-agents and commands — are rare and important, so each gets its own row,
|
|
551
|
+
* keyed by span and never coalesced: two runs of `superpowers:brainstorming`
|
|
552
|
+
* are two things that happened.
|
|
537
553
|
*
|
|
538
554
|
* Grouping by tool name instead gave `Bash ×38 / Read ×16 / Bash ×3 / Edit
|
|
539
555
|
* ×20` per session, with every skill and connector buried under it. */
|
|
@@ -609,14 +625,6 @@ function countAgain(entry, e) {
|
|
|
609
625
|
entry.timeEl.textContent = fmtRange(entry.first, entry.last);
|
|
610
626
|
}
|
|
611
627
|
}
|
|
612
|
-
/* A refusal is worth seeing and was not worth a row of its own: `Bash ×3
|
|
613
|
-
* denied` beside `Bash ×38` was half the noise. It rides the row instead. */
|
|
614
|
-
if (e.refused) {
|
|
615
|
-
entry.refused += 1;
|
|
616
|
-
entry.node.classList.add("warn");
|
|
617
|
-
entry.warnEl.hidden = false;
|
|
618
|
-
entry.warnEl.textContent = "⚠ " + plural(entry.refused, "refusal");
|
|
619
|
-
}
|
|
620
628
|
/* Animated where it sits, never hoisted. Hoisting the most recently counted
|
|
621
629
|
* row would rank a busy week-old session above a quiet one from this
|
|
622
630
|
* morning: during a backfill, "counted just now" is a fact about the drain
|
|
@@ -656,14 +664,19 @@ function place(entry) {
|
|
|
656
664
|
|
|
657
665
|
function newRow(e, signature) {
|
|
658
666
|
const parts = evParts(e);
|
|
659
|
-
|
|
667
|
+
/* A category this build has never heard of takes its own name and the
|
|
668
|
+
* neutral tag: nothing on this page knows enough about it to warn. */
|
|
669
|
+
const kind = KIND_TAG[e.kind] || [e.kind || "", "kt-tool"];
|
|
660
670
|
/* The placeholder goes as soon as there is a real row — otherwise it rides
|
|
661
671
|
* the list and counts against the trim. */
|
|
662
672
|
if (!feedOrder.length) activityFeed.innerHTML = "";
|
|
663
673
|
|
|
674
|
+
/* No row this feed builds is reddened. The two sources of it were a call
|
|
675
|
+
* the correlator could not place — which no longer reaches the feed — and a
|
|
676
|
+
* refusal, which `outcome.py` is explicit is a guardrail working as
|
|
677
|
+
* intended: painting it red reports the permission prompt as the incident. */
|
|
664
678
|
const node = document.createElement("div");
|
|
665
679
|
node.className = "ev fresh";
|
|
666
|
-
if (e.unplaced || e.refused) node.classList.add("warn");
|
|
667
680
|
|
|
668
681
|
const countEl = element("span", "evcount", "");
|
|
669
682
|
countEl.hidden = true;
|
|
@@ -674,16 +687,6 @@ function newRow(e, signature) {
|
|
|
674
687
|
first.appendChild(element("span", "evact", parts.action));
|
|
675
688
|
first.appendChild(countEl);
|
|
676
689
|
|
|
677
|
-
const warnEl = element("span", "ev-warn", "");
|
|
678
|
-
warnEl.hidden = true;
|
|
679
|
-
if (e.unplaced) {
|
|
680
|
-
warnEl.hidden = false;
|
|
681
|
-
warnEl.textContent = "unplaced";
|
|
682
|
-
} else if (e.refused) {
|
|
683
|
-
warnEl.hidden = false;
|
|
684
|
-
warnEl.textContent = e.kind === "tool" ? "⚠ 1 refusal" : "refused";
|
|
685
|
-
}
|
|
686
|
-
|
|
687
690
|
const lastEl = element("span", "evlast", "");
|
|
688
691
|
const timeEl = element("span", "ev-time", fmtAgo(e.at));
|
|
689
692
|
|
|
@@ -692,15 +695,14 @@ function newRow(e, signature) {
|
|
|
692
695
|
second.appendChild(element("span", "ev-prov", parts.provenance));
|
|
693
696
|
second.appendChild(lastEl);
|
|
694
697
|
second.appendChild(timeEl);
|
|
695
|
-
second.appendChild(warnEl);
|
|
696
698
|
|
|
697
699
|
node.appendChild(first);
|
|
698
700
|
node.appendChild(second);
|
|
699
701
|
|
|
700
702
|
const started = Date.parse(e.at);
|
|
701
703
|
const entry = {
|
|
702
|
-
node: node, countEl: countEl,
|
|
703
|
-
count: 1,
|
|
704
|
+
node: node, countEl: countEl, lastEl: lastEl, timeEl: timeEl,
|
|
705
|
+
count: 1, key: keyOf(e), signature: signature,
|
|
704
706
|
first: Number.isNaN(started) ? Infinity : started,
|
|
705
707
|
last: Number.isNaN(started) ? -Infinity : started,
|
|
706
708
|
};
|
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
|
|
75
75
|
<div class="legend" id="live-legend">
|
|
76
76
|
<span><b class="lg-conn">MCP</b> = a link to an outside service</span>
|
|
77
|
-
<span><b class="lg-ok">✓</b> resolved to something you installed</span>
|
|
78
|
-
<span><b class="lg-warn">⚠</b> unresolved — nothing on this machine claims it</span>
|
|
79
77
|
</div>
|
|
80
78
|
|
|
81
79
|
<div class="logs">
|
|
@@ -290,9 +290,11 @@ const scenarios = {
|
|
|
290
290
|
};
|
|
291
291
|
},
|
|
292
292
|
|
|
293
|
-
/* A
|
|
294
|
-
*
|
|
295
|
-
|
|
293
|
+
/* A refusal is the permission prompt working, not an incident: `outcome.py`
|
|
294
|
+
* refuses to treat a denied call as evidence of anything, and the page must
|
|
295
|
+
* not treat it as an alarm either. It is counted with the rest of the
|
|
296
|
+
* project's built-ins, and that is all the row says about it. */
|
|
297
|
+
a_refusal_is_counted_on_the_row_and_never_marked() {
|
|
296
298
|
const feed = instance();
|
|
297
299
|
feed.renderActivity([session("a", [
|
|
298
300
|
...repeat(10, {}),
|
|
@@ -318,10 +320,11 @@ const scenarios = {
|
|
|
318
320
|
return { name: row.name, text: row.text, kind: row.kind };
|
|
319
321
|
},
|
|
320
322
|
|
|
321
|
-
/*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
|
|
323
|
+
/* A call correlation could not place names no component, so there is nothing
|
|
324
|
+
* on the row for a reader to act on: it is left off the feed rather than
|
|
325
|
+
* shown as a warning. The denied MCP call beside it *is* placed, so it
|
|
326
|
+
* stays — and stays unmarked. */
|
|
327
|
+
an_unresolved_call_never_reaches_the_feed() {
|
|
325
328
|
const feed = instance();
|
|
326
329
|
feed.renderActivity([session("a", [
|
|
327
330
|
mcp("support", "open_ticket", { status: "denied" }),
|
|
@@ -329,8 +332,27 @@ const scenarios = {
|
|
|
329
332
|
])]);
|
|
330
333
|
feed.tick(10);
|
|
331
334
|
const rows = feed.rows();
|
|
332
|
-
return {
|
|
333
|
-
(r) => r.
|
|
335
|
+
return {
|
|
336
|
+
names: rows.map((r) => r.name),
|
|
337
|
+
labels: rows.map((r) => r.label),
|
|
338
|
+
warned: rows.map((r) => r.className.split(" ").includes("warn")),
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
/* Nothing this feed builds is painted red. Refusals and unplaceable calls
|
|
343
|
+
* were the only two sources of it, and asked about every row rather than the
|
|
344
|
+
* two that carried it: a third source would otherwise arrive unnoticed. */
|
|
345
|
+
no_row_the_feed_builds_is_reddened() {
|
|
346
|
+
const feed = instance();
|
|
347
|
+
feed.renderActivity([session("a", [
|
|
348
|
+
...repeat(6, {}),
|
|
349
|
+
...repeat(2, { status: "denied" }),
|
|
350
|
+
mcp("support", "open_ticket", { status: "denied" }),
|
|
351
|
+
skill("code-review", { status: "denied" }),
|
|
352
|
+
call({ category: "unresolved", component_name: null, tool_name: "mystery_tool" }),
|
|
353
|
+
])]);
|
|
354
|
+
feed.tick(40);
|
|
355
|
+
return { classes: feed.rows().map((r) => r.className) };
|
|
334
356
|
},
|
|
335
357
|
|
|
336
358
|
/* Pacing: distinct rows arrive one tick at a time. */
|
|
@@ -509,6 +531,26 @@ const scenarios = {
|
|
|
509
531
|
return { count: feed.rows()[0].count, time: cell("ev-time"), last: cell("evlast") };
|
|
510
532
|
},
|
|
511
533
|
|
|
534
|
+
/* The header counts what the page shows. `summary.actions` is the server's
|
|
535
|
+
* own total and includes the calls this page leaves off, so reading it left
|
|
536
|
+
* the header claiming more actions than the feed could account for. */
|
|
537
|
+
the_header_counts_only_the_actions_the_page_shows() {
|
|
538
|
+
const feed = instance();
|
|
539
|
+
feed.render({
|
|
540
|
+
summary: { sessions: 1, actions: 5, coverage: { total: 5, resolved: 3 } },
|
|
541
|
+
sessions: [session("a", [
|
|
542
|
+
skill("code-review"),
|
|
543
|
+
mcp("github", "create_pull_request"),
|
|
544
|
+
call({}),
|
|
545
|
+
call({ category: "unresolved", component_name: null, tool_name: "one" }),
|
|
546
|
+
call({ category: "unresolved", component_name: null, tool_name: "two" }),
|
|
547
|
+
])],
|
|
548
|
+
alerts: [], banners: [], escalate: {},
|
|
549
|
+
});
|
|
550
|
+
feed.tick(10);
|
|
551
|
+
return { header: feed.dom("live-counts").textContent };
|
|
552
|
+
},
|
|
553
|
+
|
|
512
554
|
/* The alias is the name in the reader's own `.mcp.json`; the package name is
|
|
513
555
|
* what got installed under it. Only one is the word they recognise. */
|
|
514
556
|
a_row_leads_with_the_alias_not_the_package_name() {
|
|
@@ -707,11 +749,19 @@ const scenarios = {
|
|
|
707
749
|
/* Coverage is what correlation computes, and correlation is the slow part.
|
|
708
750
|
* The header therefore has a real session and action count a second before
|
|
709
751
|
* it has a coverage fraction. */
|
|
710
|
-
|
|
711
|
-
const feed = instance();
|
|
752
|
+
the_header_never_shows_a_coverage_fraction() {
|
|
753
|
+
const feed = instance();
|
|
754
|
+
/* The rows are built rather than asserted through `summary.actions`: the
|
|
755
|
+
* header counts what the page shows, so a fixture that carries the total
|
|
756
|
+
* and no rows tests the fixture. 82 sessions holding 6,995 calls between
|
|
757
|
+
* them, which is the window this was measured on. */
|
|
758
|
+
const sessions = [];
|
|
759
|
+
for (let i = 0; i < 82; i += 1) {
|
|
760
|
+
sessions.push(session("s" + i, repeat(i === 81 ? 6995 - 81 * 85 : 85, {})));
|
|
761
|
+
}
|
|
712
762
|
const state = (coverage) => ({
|
|
713
763
|
summary: { sessions: 82, actions: 6995, coverage },
|
|
714
|
-
sessions:
|
|
764
|
+
sessions: sessions, alerts: [], banners: [], escalate: {},
|
|
715
765
|
});
|
|
716
766
|
feed.render(state(null));
|
|
717
767
|
const during = feed.dom("live-counts").textContent;
|