@monster-orange/den 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/LICENSE +21 -0
- package/README.md +166 -2
- package/bin/den +61 -0
- package/docs/distributions.md +256 -0
- package/make/demo.mk +82 -0
- package/make/den.mk +80 -0
- package/make/journeys.mk +57 -0
- package/make/local_dev.mk +154 -0
- package/make/security.mk +59 -0
- package/make/test.mk +405 -0
- package/package.json +104 -4
- package/scripts/auth-configure.ts +415 -0
- package/scripts/heal-config.ts +92 -0
- package/scripts/jsonl-playwright-reporter.js +70 -0
- package/scripts/manifest-read.cjs +91 -0
- package/scripts/setup-1password.ts +570 -0
- package/scripts/setup-secrets.ts +149 -0
- package/scripts/view-results.js +156 -0
- package/service/install.sh +112 -0
- package/src/pillars/api/fixtures/index.ts +128 -0
- package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
- package/src/pillars/api/global-setup.ts +103 -0
- package/src/pillars/api/playwright.config.ts +105 -0
- package/src/pillars/api/schemas/example-api.json +110 -0
- package/src/pillars/api/tests/health/health.spec.ts +19 -0
- package/src/pillars/api/tests/health/manifest.yml +6 -0
- package/src/pillars/browser/fixtures/index.ts +232 -0
- package/src/pillars/browser/fixtures/package.json +10 -0
- package/src/pillars/browser/global-setup.ts +140 -0
- package/src/pillars/browser/global-teardown.ts +48 -0
- package/src/pillars/browser/playwright.config.ts +212 -0
- package/src/pillars/browser/setup.ts +219 -0
- package/src/pillars/journeys/app-config.ts +237 -0
- package/src/pillars/journeys/discover.ts +160 -0
- package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
- package/src/pillars/journeys/observability/events.ts +288 -0
- package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
- package/src/pillars/journeys/report/friction.ts +304 -0
- package/src/pillars/journeys/report/junit.ts +105 -0
- package/src/pillars/journeys/report/render.ts +273 -0
- package/src/pillars/journeys/report/results.ts +323 -0
- package/src/pillars/journeys/run.ts +673 -0
- package/src/pillars/journeys/runner.ts +1025 -0
- package/src/pillars/journeys/sandbox.ts +406 -0
- package/src/pillars/journeys/spec/loader.ts +152 -0
- package/src/pillars/journeys/spec/schema.ts +208 -0
- package/src/pillars/journeys/teardown.ts +98 -0
- package/src/pillars/journeys/tools/browser-session.ts +219 -0
- package/src/pillars/journeys/tools/browser-tool.ts +914 -0
- package/src/pillars/journeys/tools/http-tool.ts +564 -0
- package/src/pillars/journeys/tools/ledger.ts +278 -0
- package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
- package/src/pillars/journeys/verify/assertions.ts +502 -0
- package/src/pillars/journeys/verify/verdict.ts +71 -0
- package/src/pillars/perf/build-report.ts +36 -0
- package/src/pillars/perf/k6.config.js +112 -0
- package/src/pillars/perf/run.ts +345 -0
- package/src/pillars/perf/scenarios/load-profile.js +97 -0
- package/src/pillars/perf/staging.ts +73 -0
- package/src/pillars/security/discover.ts +108 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +729 -0
- package/src/pillars/smoke/admin-probe.ts +68 -0
- package/src/pillars/smoke/base-url.ts +56 -0
- package/src/pillars/smoke/cleanup.ts +8 -0
- package/src/pillars/smoke/discover.ts +237 -0
- package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
- package/src/pillars/smoke/fixture-manager.ts +217 -0
- package/src/pillars/smoke/fixtures.ts +468 -0
- package/src/pillars/smoke/global-setup-all.ts +262 -0
- package/src/pillars/smoke/global-setup-global.ts +262 -0
- package/src/pillars/smoke/global-setup.ts +273 -0
- package/src/pillars/smoke/global-teardown-all.ts +8 -0
- package/src/pillars/smoke/global-teardown-global.ts +8 -0
- package/src/pillars/smoke/global-teardown.ts +139 -0
- package/src/pillars/smoke/playwright-all.config.ts +147 -0
- package/src/pillars/smoke/playwright-global.config.ts +141 -0
- package/src/pillars/smoke/playwright.config.ts +162 -0
- package/src/pillars/smoke/setup-env.ts +132 -0
- package/src/pillars/smoke/setup-global.ts +126 -0
- package/src/pillars/smoke/setup.ts +115 -0
- package/src/pillars/smoke/smoke-repo.ts +16 -0
- package/src/pillars/smoke/staging.ts +49 -0
- package/src/pillars/suites/run.ts +147 -0
- package/src/scripts/configure-aws-profiles.ts +114 -0
- package/src/scripts/register-repos.ts +307 -0
- package/src/scripts/setup-default-workspace.ts +203 -0
- package/src/shared/applications.ts +138 -0
- package/src/shared/auth/api-token.ts +416 -0
- package/src/shared/auth/auth0-helper.ts +284 -0
- package/src/shared/auth/headless-login.ts +265 -0
- package/src/shared/auth/jwt-helper.ts +186 -0
- package/src/shared/auth/sudo-vault.ts +69 -0
- package/src/shared/auth/test-accounts.ts +98 -0
- package/src/shared/auth/token-injection.ts +184 -0
- package/src/shared/auth/token-providers/auth0.ts +80 -0
- package/src/shared/auth/token-providers/index.ts +182 -0
- package/src/shared/auth/token-refresh.ts +273 -0
- package/src/shared/banner.ts +15 -0
- package/src/shared/brand.ts +79 -0
- package/src/shared/cli-tabs.ts +131 -0
- package/src/shared/constants.ts +128 -0
- package/src/shared/den-config.ts +566 -0
- package/src/shared/dev-metrics/emit.ts +354 -0
- package/src/shared/edge/config.ts +229 -0
- package/src/shared/edge/render.ts +221 -0
- package/src/shared/edge/runtime.ts +268 -0
- package/src/shared/edge/types.ts +62 -0
- package/src/shared/engine.ts +67 -0
- package/src/shared/environment/aws-profiles.ts +49 -0
- package/src/shared/environment/env-selector.ts +161 -0
- package/src/shared/environment/playwright-env.ts +82 -0
- package/src/shared/environment-manager/catalog.ts +77 -0
- package/src/shared/environment-manager/clone-guard.ts +163 -0
- package/src/shared/environment-manager/dag.ts +286 -0
- package/src/shared/environment-manager/default-workspace.ts +182 -0
- package/src/shared/environment-manager/derivation/alias.ts +90 -0
- package/src/shared/environment-manager/derivation/cache.ts +97 -0
- package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
- package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
- package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
- package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
- package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
- package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
- package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
- package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
- package/src/shared/environment-manager/derivation/scan.ts +160 -0
- package/src/shared/environment-manager/derivation/text.ts +40 -0
- package/src/shared/environment-manager/derivation/types.ts +69 -0
- package/src/shared/environment-manager/health.ts +589 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +78 -0
- package/src/shared/environment-manager/log-tail.ts +212 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +334 -0
- package/src/shared/environment-manager/readiness.ts +93 -0
- package/src/shared/environment-manager/register-local.ts +72 -0
- package/src/shared/environment-manager/secrets-check.ts +260 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1588 -0
- package/src/shared/environment-manager/up-plan.ts +302 -0
- package/src/shared/environment-manager/workspace-manager.ts +905 -0
- package/src/shared/environment-manager/workspaces.ts +860 -0
- package/src/shared/manifest/deps.ts +27 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/read-manifest.ts +160 -0
- package/src/shared/manifest/schema.ts +299 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-manifest.ts +140 -0
- package/src/shared/manifest/workspace-manifests.ts +92 -0
- package/src/shared/open-url.ts +122 -0
- package/src/shared/org.ts +44 -0
- package/src/shared/paths.ts +178 -0
- package/src/shared/perf/optional-fields.ts +105 -0
- package/src/shared/perf/report-normalizer.ts +404 -0
- package/src/shared/perf/report-paths.ts +19 -0
- package/src/shared/perf/report-store.ts +110 -0
- package/src/shared/perf/report-types.ts +70 -0
- package/src/shared/perf/test-paths.ts +25 -0
- package/src/shared/process/log-rotation.ts +147 -0
- package/src/shared/process/spawn-streaming.ts +229 -0
- package/src/shared/product.ts +92 -0
- package/src/shared/profile.ts +141 -0
- package/src/shared/repo-kind.ts +32 -0
- package/src/shared/reporting/jsonl-reporter.ts +131 -0
- package/src/shared/reporting/slack-reporter.ts +340 -0
- package/src/shared/results/result-reader.ts +266 -0
- package/src/shared/results/result-writer.ts +168 -0
- package/src/shared/secrets/doppler.ts +126 -0
- package/src/shared/secrets/env.ts +48 -0
- package/src/shared/secrets/index.ts +197 -0
- package/src/shared/secrets/keys.ts +33 -0
- package/src/shared/secrets/onepassword.ts +75 -0
- package/src/shared/secrets/types.ts +52 -0
- package/src/shared/service-url.ts +119 -0
- package/src/shared/smoke/branch-resolver.ts +141 -0
- package/src/shared/smoke/git-remote.ts +70 -0
- package/src/shared/smoke/repo-cloner.ts +151 -0
- package/src/shared/stacks/config.ts +137 -0
- package/src/shared/stacks/manifest-commands.ts +88 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/types.ts +53 -0
- package/src/shared/terminal/tabs.ts +212 -0
- package/src/shared/test-discovery/module-scanner.ts +569 -0
- package/src/shared/types/index.ts +678 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +116 -0
- package/src/ui/cli/config-command.ts +68 -0
- package/src/ui/cli/create-main.ts +9 -0
- package/src/ui/cli/create.ts +360 -0
- package/src/ui/cli/edge-command.ts +91 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +524 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3656 -0
- package/src/ui/cli/init.ts +855 -0
- package/src/ui/cli/invocation-target.ts +175 -0
- package/src/ui/cli/logs-command.ts +104 -0
- package/src/ui/cli/module-selection.ts +31 -0
- package/src/ui/cli/prompts.ts +767 -0
- package/src/ui/cli/results-viewer.ts +436 -0
- package/src/ui/cli/secrets-preflight.ts +91 -0
- package/src/ui/cli/stack-command.ts +144 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1312 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/server/changelog-parser.ts +94 -0
- package/src/ui/server/daemon-reaper.ts +129 -0
- package/src/ui/server/editor-manager.ts +301 -0
- package/src/ui/server/github-service.ts +223 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3236 -0
- package/src/ui/server/log-history.ts +452 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +494 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/header.js +1371 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +7405 -0
- package/src/ui/server/public/logs.html +1489 -0
- package/src/ui/server/public/perf-report.js +359 -0
- package/src/ui/server/public/release.html +2111 -0
- package/src/ui/server/public/services.html +1098 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/release-deploy-session.ts +246 -0
- package/src/ui/server/release-service.ts +1125 -0
- package/src/ui/server/sudo-store.ts +111 -0
- package/src/ui/server/workspace-routes.ts +2154 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journeys observability event bus — the single structured timeline of what
|
|
3
|
+
* a journey did, feeding three sinks from one source:
|
|
4
|
+
* 1. an in-memory list (the report reads it for the timeline + binding
|
|
5
|
+
* summary + denial list),
|
|
6
|
+
* 2. `events.jsonl` next to the transcript (post-mortem forensics),
|
|
7
|
+
* 3. an optional condensed live line to stdout (JOURNEYS_VERBOSE) so a
|
|
8
|
+
* stuck/looping run is obvious WHILE it runs, not after `report:`.
|
|
9
|
+
*
|
|
10
|
+
* The transcript.jsonl remains the raw SDK stream; events.jsonl is the
|
|
11
|
+
* harness's own interpretation — tool calls with results, ledger bindings
|
|
12
|
+
* (the `{<type>_id}` question answered directly), network interception hits,
|
|
13
|
+
* capability/policy denials, agent errors, and turn/token progress.
|
|
14
|
+
*
|
|
15
|
+
* Data-minimization (mirrors ledger.ts): events carry ONLY method +
|
|
16
|
+
* sanitized path + status + extracted id + tool name + a short human
|
|
17
|
+
* summary. Never bodies, headers, cookies, or credential material. The
|
|
18
|
+
* caller still passes secret values so the JSONL writer fails closed if one
|
|
19
|
+
* ever appears (defense in depth alongside TranscriptScrubber).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as fs from "fs";
|
|
23
|
+
import * as path from "path";
|
|
24
|
+
import type { HttpMethod, LedgerSource } from "../tools/ledger.js";
|
|
25
|
+
|
|
26
|
+
/** Monotonic-ish wall clock; injectable for deterministic tests. */
|
|
27
|
+
export type Clock = () => number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One structured harness event. `kind` discriminates; every event carries a
|
|
31
|
+
* `seq` (emission order) and `tMs` (ms since bus creation) stamped by the
|
|
32
|
+
* bus, so consumers never compute timing themselves.
|
|
33
|
+
*/
|
|
34
|
+
export type JourneyEvent = { seq: number; tMs: number } & JourneyEventBody;
|
|
35
|
+
|
|
36
|
+
export type JourneyEventBody =
|
|
37
|
+
| {
|
|
38
|
+
kind: "run_started";
|
|
39
|
+
journey: string;
|
|
40
|
+
env: string;
|
|
41
|
+
surface: string;
|
|
42
|
+
model: string;
|
|
43
|
+
}
|
|
44
|
+
| { kind: "turn_started"; turn: number }
|
|
45
|
+
| {
|
|
46
|
+
kind: "turn_ended";
|
|
47
|
+
turn: number;
|
|
48
|
+
/** Cumulative total tokens if the runtime reported usage this turn. */
|
|
49
|
+
totalTokens?: number;
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
kind: "tool_call";
|
|
53
|
+
/** Tool wire name, e.g. "browser_navigate", "http_request". */
|
|
54
|
+
tool: string;
|
|
55
|
+
/** SDK call id, for correlating start/end if both are emitted. */
|
|
56
|
+
callId?: string;
|
|
57
|
+
status: "running" | "completed" | "error";
|
|
58
|
+
/** One-line, secret-free summary of args (e.g. `GET /api/v1/items`). */
|
|
59
|
+
summary?: string;
|
|
60
|
+
/** One-line, secret-free summary of the result (status, error code). */
|
|
61
|
+
result?: string;
|
|
62
|
+
}
|
|
63
|
+
| { kind: "thinking"; text: string }
|
|
64
|
+
| { kind: "assistant_text"; text: string }
|
|
65
|
+
| {
|
|
66
|
+
kind: "ledger_binding";
|
|
67
|
+
resourceType: string;
|
|
68
|
+
id: string;
|
|
69
|
+
method: HttpMethod;
|
|
70
|
+
pathValue: string;
|
|
71
|
+
status: number;
|
|
72
|
+
source: LedgerSource;
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
kind: "unmanaged_mutation";
|
|
76
|
+
method: HttpMethod;
|
|
77
|
+
pathValue: string;
|
|
78
|
+
status: number;
|
|
79
|
+
source: LedgerSource;
|
|
80
|
+
}
|
|
81
|
+
| { kind: "interception_error"; message: string }
|
|
82
|
+
| {
|
|
83
|
+
kind: "snapshot";
|
|
84
|
+
action: string;
|
|
85
|
+
/** null when suppressed/failed — the reason is in `note`. */
|
|
86
|
+
pathValue: string | null;
|
|
87
|
+
note?: string;
|
|
88
|
+
}
|
|
89
|
+
| {
|
|
90
|
+
kind: "agent_error";
|
|
91
|
+
/** SDK RunResult.error.message (secret-redacted by the caller). */
|
|
92
|
+
message: string;
|
|
93
|
+
code?: string;
|
|
94
|
+
}
|
|
95
|
+
| { kind: "run_ended"; failureKind?: string; durationMs: number };
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The surface the runner depends on — lets tests inject a recording fake
|
|
99
|
+
* without a filesystem. `JourneyEventBus` is the production implementation.
|
|
100
|
+
*/
|
|
101
|
+
export interface JourneyEventSink {
|
|
102
|
+
emit(body: JourneyEventBody): void;
|
|
103
|
+
addSecret(value: string): void;
|
|
104
|
+
readonly events: readonly JourneyEvent[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Sink config; all optional so tests can run the bus headless-silent. */
|
|
108
|
+
export interface JourneyEventBusOptions {
|
|
109
|
+
/** Absolute path for the JSONL sink; when omitted, nothing is written. */
|
|
110
|
+
filePath?: string;
|
|
111
|
+
/** Emit a condensed line per event to this writer (stdout when verbose). */
|
|
112
|
+
liveWriter?: (line: string) => void;
|
|
113
|
+
/** Secret values redacted before any event is written/printed. */
|
|
114
|
+
secrets?: readonly string[];
|
|
115
|
+
clock?: Clock;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A live secret slipped into an event — fail the run loudly (never write). */
|
|
119
|
+
export class EventLeakError extends Error {
|
|
120
|
+
constructor() {
|
|
121
|
+
super(
|
|
122
|
+
"Journey event persist blocked: an event matched a live secret value. " +
|
|
123
|
+
"The run fails closed (failure_kind=infra_error).",
|
|
124
|
+
);
|
|
125
|
+
this.name = "EventLeakError";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Collects, persists, and (optionally) live-prints structured journey
|
|
131
|
+
* events. Single-threaded append; the JSONL write is synchronous so an
|
|
132
|
+
* event is durable the instant it is emitted (a crash mid-run still leaves
|
|
133
|
+
* a partial timeline on disk).
|
|
134
|
+
*/
|
|
135
|
+
export class JourneyEventBus {
|
|
136
|
+
private readonly recorded: JourneyEvent[] = [];
|
|
137
|
+
private readonly filePath?: string;
|
|
138
|
+
private readonly liveWriter?: (line: string) => void;
|
|
139
|
+
private readonly secrets: string[];
|
|
140
|
+
private readonly clock: Clock;
|
|
141
|
+
private readonly startedAt: number;
|
|
142
|
+
private seqCounter = 0;
|
|
143
|
+
|
|
144
|
+
constructor(options: JourneyEventBusOptions = {}) {
|
|
145
|
+
this.filePath = options.filePath;
|
|
146
|
+
this.liveWriter = options.liveWriter;
|
|
147
|
+
this.secrets = (options.secrets ?? []).filter((v) => v.length > 0);
|
|
148
|
+
this.clock = options.clock ?? Date.now;
|
|
149
|
+
this.startedAt = this.clock();
|
|
150
|
+
if (this.filePath !== undefined) {
|
|
151
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Events in emission order (report timeline reads this). */
|
|
156
|
+
get events(): readonly JourneyEvent[] {
|
|
157
|
+
return this.recorded;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Add live secret values to redact from future events. The runner learns
|
|
162
|
+
* the session token only after provisioning, so the bus is constructed
|
|
163
|
+
* with the Cursor key and the token is added once derived.
|
|
164
|
+
*/
|
|
165
|
+
addSecret(value: string): void {
|
|
166
|
+
if (value.length > 0 && !this.secrets.includes(value)) {
|
|
167
|
+
this.secrets.push(value);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private redact(text: string): string {
|
|
172
|
+
let out = text;
|
|
173
|
+
for (const secret of this.secrets) {
|
|
174
|
+
out = out.split(secret).join("[REDACTED]");
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Emit one event: stamp seq + tMs, redact strings, record, persist, and
|
|
181
|
+
* (if verbose) live-print. Throws EventLeakError if a raw secret survives
|
|
182
|
+
* redaction anywhere in the serialized event.
|
|
183
|
+
*/
|
|
184
|
+
emit(body: JourneyEventBody): void {
|
|
185
|
+
const redactedBody = redactEventStrings(body, (t) => this.redact(t));
|
|
186
|
+
const event: JourneyEvent = {
|
|
187
|
+
seq: this.seqCounter++,
|
|
188
|
+
tMs: this.clock() - this.startedAt,
|
|
189
|
+
...redactedBody,
|
|
190
|
+
};
|
|
191
|
+
const line = JSON.stringify(event);
|
|
192
|
+
for (const secret of this.secrets) {
|
|
193
|
+
if (line.includes(secret)) {
|
|
194
|
+
throw new EventLeakError();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
this.recorded.push(event);
|
|
198
|
+
if (this.filePath !== undefined) {
|
|
199
|
+
fs.appendFileSync(this.filePath, `${line}\n`);
|
|
200
|
+
}
|
|
201
|
+
this.liveWriter?.(formatLiveLine(event));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Map every user-supplied string field of an event through `fn` (redaction).
|
|
207
|
+
* Kept exhaustive-by-construction: only known string-bearing fields are
|
|
208
|
+
* touched, numbers/enums pass through.
|
|
209
|
+
*/
|
|
210
|
+
function redactEventStrings(
|
|
211
|
+
body: JourneyEventBody,
|
|
212
|
+
fn: (t: string) => string,
|
|
213
|
+
): JourneyEventBody {
|
|
214
|
+
switch (body.kind) {
|
|
215
|
+
case "tool_call":
|
|
216
|
+
return {
|
|
217
|
+
...body,
|
|
218
|
+
summary: body.summary === undefined ? undefined : fn(body.summary),
|
|
219
|
+
result: body.result === undefined ? undefined : fn(body.result),
|
|
220
|
+
};
|
|
221
|
+
case "thinking":
|
|
222
|
+
case "assistant_text":
|
|
223
|
+
return { ...body, text: fn(body.text) };
|
|
224
|
+
case "interception_error":
|
|
225
|
+
return { ...body, message: fn(body.message) };
|
|
226
|
+
case "agent_error":
|
|
227
|
+
return { ...body, message: fn(body.message) };
|
|
228
|
+
default:
|
|
229
|
+
return body;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** ANSI-free, one-line condensed rendering for the live/verbose stream. */
|
|
234
|
+
export function formatLiveLine(event: JourneyEvent): string {
|
|
235
|
+
const t = `${(event.tMs / 1000).toFixed(1)}s`;
|
|
236
|
+
switch (event.kind) {
|
|
237
|
+
case "run_started":
|
|
238
|
+
return ` [${t}] ▶ ${event.journey} (${event.surface}) model=${event.model}`;
|
|
239
|
+
case "turn_started":
|
|
240
|
+
return ` [${t}] turn ${event.turn}`;
|
|
241
|
+
case "turn_ended":
|
|
242
|
+
return ` [${t}] turn ${event.turn} ended${
|
|
243
|
+
event.totalTokens !== undefined ? ` (${event.totalTokens} tok)` : ""
|
|
244
|
+
}`;
|
|
245
|
+
case "tool_call": {
|
|
246
|
+
const icon =
|
|
247
|
+
event.status === "completed"
|
|
248
|
+
? "✓"
|
|
249
|
+
: event.status === "error"
|
|
250
|
+
? "✗"
|
|
251
|
+
: "…";
|
|
252
|
+
const parts = [` [${t}] ${icon} ${event.tool}`];
|
|
253
|
+
if (event.summary) parts.push(event.summary);
|
|
254
|
+
if (event.result) parts.push(`→ ${event.result}`);
|
|
255
|
+
return parts.join(" ");
|
|
256
|
+
}
|
|
257
|
+
case "thinking":
|
|
258
|
+
return ` [${t}] 💭 ${truncate(event.text, 100)}`;
|
|
259
|
+
case "assistant_text":
|
|
260
|
+
return ` [${t}] 🗣 ${truncate(event.text, 100)}`;
|
|
261
|
+
case "ledger_binding":
|
|
262
|
+
return ` [${t}] 🔗 bound ${event.resourceType}=${event.id} (${event.method} ${event.pathValue} → ${event.status})`;
|
|
263
|
+
case "unmanaged_mutation":
|
|
264
|
+
return ` [${t}] ⚠ unmanaged ${event.method} ${event.pathValue} → ${event.status}`;
|
|
265
|
+
case "interception_error":
|
|
266
|
+
return ` [${t}] ⚠ interception error: ${truncate(event.message, 120)}`;
|
|
267
|
+
case "snapshot":
|
|
268
|
+
return ` [${t}] 📸 ${event.action}${
|
|
269
|
+
event.pathValue === null
|
|
270
|
+
? ` (no frame${event.note ? `: ${event.note}` : ""})`
|
|
271
|
+
: ""
|
|
272
|
+
}`;
|
|
273
|
+
case "agent_error":
|
|
274
|
+
return ` [${t}] ✗ agent error${event.code ? ` [${event.code}]` : ""}: ${truncate(event.message, 200)}`;
|
|
275
|
+
case "run_ended":
|
|
276
|
+
return ` [${t}] ■ ended${event.failureKind ? ` (${event.failureKind})` : " (ok)"} in ${event.durationMs}ms`;
|
|
277
|
+
default: {
|
|
278
|
+
// Exhaustiveness guard: a new event kind must extend this switch.
|
|
279
|
+
const _never: never = event;
|
|
280
|
+
return String(_never);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function truncate(text: string, max: number): string {
|
|
286
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
287
|
+
return flat.length <= max ? flat : `${flat.slice(0, max - 1)}…`;
|
|
288
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure SDK-stream → JourneyEvent mapper. The runner streams opaque SDK
|
|
3
|
+
* messages; this module turns the interesting ones into condensed,
|
|
4
|
+
* secret-free harness events (tool calls with results, thinking,
|
|
5
|
+
* assistant text, per-turn usage). Kept pure + injectable-free so it is
|
|
6
|
+
* exhaustively unit-testable without an SDK or a live agent.
|
|
7
|
+
*
|
|
8
|
+
* Only the subset the operator cares about is mapped; system/request/task
|
|
9
|
+
* bookkeeping messages produce no events (the raw line is still in
|
|
10
|
+
* transcript.jsonl). Tool args/results are summarized to a single line —
|
|
11
|
+
* never the full payload — so events.jsonl stays a timeline, not a body
|
|
12
|
+
* dump (see events.ts data-minimization contract).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { SDKMessage } from "@cursor/sdk";
|
|
16
|
+
import type { JourneyEventBody } from "./events.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Stream state threaded across messages. The SDK streams assistant output
|
|
20
|
+
* one TOKEN per `assistant` message (verified on a staging transcript:
|
|
21
|
+
* 771 assistant frames for a handful of logical replies — "I'll", " run",
|
|
22
|
+
* " the", …). Emitting an event per frame produced 771 fake turns + 763
|
|
23
|
+
* one-word assistant_text lines and buried the real tool timeline. So we
|
|
24
|
+
* BUFFER consecutive text deltas here and flush a single coalesced
|
|
25
|
+
* assistant_text when a non-text signal (tool call / thinking / usage /
|
|
26
|
+
* end-of-stream) arrives. `turn` increments per usage-reported round, the
|
|
27
|
+
* only honest turn boundary the stream exposes.
|
|
28
|
+
*/
|
|
29
|
+
export interface MapperState {
|
|
30
|
+
turn: number;
|
|
31
|
+
/** Accumulated assistant text since the last flush. */
|
|
32
|
+
textBuffer: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createMapperState(): MapperState {
|
|
36
|
+
return { turn: 0, textBuffer: "" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Flush any buffered assistant text as one coalesced event. Called by the
|
|
41
|
+
* mapper before emitting a non-text event, and by the runner at end-of-stream
|
|
42
|
+
* so a trailing reply is not lost.
|
|
43
|
+
*/
|
|
44
|
+
export function flushAssistantText(state: MapperState): JourneyEventBody[] {
|
|
45
|
+
const text = state.textBuffer.trim();
|
|
46
|
+
state.textBuffer = "";
|
|
47
|
+
return text.length > 0 ? [{ kind: "assistant_text", text }] : [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Map one SDK message to zero or more journey events, mutating `state`.
|
|
52
|
+
* Returns [] for messages with no operator signal (the buffer may still be
|
|
53
|
+
* mutated). The runner MUST call flushAssistantText(state) after the stream
|
|
54
|
+
* ends to emit any trailing buffered reply.
|
|
55
|
+
*/
|
|
56
|
+
export function sdkMessageToEvents(
|
|
57
|
+
message: SDKMessage,
|
|
58
|
+
state: MapperState,
|
|
59
|
+
): JourneyEventBody[] {
|
|
60
|
+
switch (message.type) {
|
|
61
|
+
case "assistant":
|
|
62
|
+
return mapAssistant(message, state);
|
|
63
|
+
case "tool_call": {
|
|
64
|
+
// Custom tools are routed through the SDK's MCP bridge, so the wire
|
|
65
|
+
// `name` is the literal "mcp" and the real tool + args are nested under
|
|
66
|
+
// args.{toolName,args}. Unwrap so the timeline shows `browser_navigate`,
|
|
67
|
+
// not `mcp` (verified against a staging transcript, 2026-07-16).
|
|
68
|
+
const { tool, args } = unwrapMcp(message.name, message.args);
|
|
69
|
+
return [
|
|
70
|
+
...flushAssistantText(state),
|
|
71
|
+
{
|
|
72
|
+
kind: "tool_call",
|
|
73
|
+
tool,
|
|
74
|
+
callId: message.call_id,
|
|
75
|
+
status: message.status,
|
|
76
|
+
summary: summarizeArgs(tool, args),
|
|
77
|
+
result: summarizeResult(message.status, message.result),
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
case "thinking":
|
|
82
|
+
return message.text.trim().length > 0
|
|
83
|
+
? [
|
|
84
|
+
...flushAssistantText(state),
|
|
85
|
+
{ kind: "thinking", text: message.text },
|
|
86
|
+
]
|
|
87
|
+
: [];
|
|
88
|
+
case "usage":
|
|
89
|
+
// A usage message is the SDK's real turn boundary — flush the reply,
|
|
90
|
+
// count the turn, and report cumulative tokens.
|
|
91
|
+
state.turn += 1;
|
|
92
|
+
return [
|
|
93
|
+
...flushAssistantText(state),
|
|
94
|
+
{
|
|
95
|
+
kind: "turn_ended",
|
|
96
|
+
turn: state.turn,
|
|
97
|
+
totalTokens: message.usage.totalTokens,
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
default:
|
|
101
|
+
// system / user / status / request / task — no timeline signal.
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handle one streamed assistant frame. Text blocks are buffered (coalesced
|
|
108
|
+
* across the many one-token frames); an inline tool_use block flushes the
|
|
109
|
+
* buffer and emits a running tool_call (the SDK also emits a standalone
|
|
110
|
+
* `tool_call` line for execution status, so this is the intent marker).
|
|
111
|
+
*/
|
|
112
|
+
function mapAssistant(
|
|
113
|
+
message: Extract<SDKMessage, { type: "assistant" }>,
|
|
114
|
+
state: MapperState,
|
|
115
|
+
): JourneyEventBody[] {
|
|
116
|
+
// Defensive: a malformed/partial assistant frame (missing message.content)
|
|
117
|
+
// must never crash the stream loop — the raw line is still in
|
|
118
|
+
// transcript.jsonl; we just skip block-level events for it.
|
|
119
|
+
const content = message.message?.content;
|
|
120
|
+
if (!Array.isArray(content)) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const events: JourneyEventBody[] = [];
|
|
125
|
+
for (const block of content) {
|
|
126
|
+
if (block.type === "text") {
|
|
127
|
+
state.textBuffer += block.text;
|
|
128
|
+
} else {
|
|
129
|
+
events.push(...flushAssistantText(state));
|
|
130
|
+
const { tool, args } = unwrapMcp(block.name, block.input);
|
|
131
|
+
events.push({
|
|
132
|
+
kind: "tool_call",
|
|
133
|
+
tool,
|
|
134
|
+
callId: block.id,
|
|
135
|
+
status: "running",
|
|
136
|
+
summary: summarizeArgs(tool, args),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return events;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Unwrap the SDK's MCP tool envelope. Custom tools surface on the stream as
|
|
145
|
+
* `name:"mcp"` with `args:{providerIdentifier, toolName, args}`; return the
|
|
146
|
+
* inner tool name + its real args. Non-MCP calls pass through unchanged.
|
|
147
|
+
*/
|
|
148
|
+
export function unwrapMcp(
|
|
149
|
+
name: string,
|
|
150
|
+
args: unknown,
|
|
151
|
+
): { tool: string; args: unknown } {
|
|
152
|
+
if (name !== "mcp" || typeof args !== "object" || args === null) {
|
|
153
|
+
return { tool: name, args };
|
|
154
|
+
}
|
|
155
|
+
const bag = args as Record<string, unknown>;
|
|
156
|
+
const toolName = typeof bag.toolName === "string" ? bag.toolName : "mcp";
|
|
157
|
+
return { tool: toolName, args: bag.args };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Peel the MCP result envelope to the tool's own payload. Custom-tool
|
|
162
|
+
* results arrive as `{ status, value: { content: [{ text: { text } }],
|
|
163
|
+
* isError } }`; return either the inner text (browser tools) or the parsed
|
|
164
|
+
* JSON payload (http_request returns a JSON string) so summarizeResult can
|
|
165
|
+
* read status codes / error codes. Non-MCP results pass through.
|
|
166
|
+
*/
|
|
167
|
+
function unwrapMcpResult(result: unknown): {
|
|
168
|
+
payload: unknown;
|
|
169
|
+
isError: boolean;
|
|
170
|
+
} {
|
|
171
|
+
if (typeof result !== "object" || result === null) {
|
|
172
|
+
return { payload: result, isError: false };
|
|
173
|
+
}
|
|
174
|
+
const bag = result as Record<string, unknown>;
|
|
175
|
+
const value = bag.value;
|
|
176
|
+
if (typeof value !== "object" || value === null) {
|
|
177
|
+
return { payload: result, isError: false };
|
|
178
|
+
}
|
|
179
|
+
const valueBag = value as Record<string, unknown>;
|
|
180
|
+
const isError = valueBag.isError === true;
|
|
181
|
+
const content = valueBag.content;
|
|
182
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
183
|
+
return { payload: result, isError };
|
|
184
|
+
}
|
|
185
|
+
const first = content[0] as { text?: { text?: unknown } } | undefined;
|
|
186
|
+
const text = first?.text?.text;
|
|
187
|
+
if (typeof text !== "string") {
|
|
188
|
+
return { payload: result, isError };
|
|
189
|
+
}
|
|
190
|
+
// http_request returns a JSON string; browser tools return prose.
|
|
191
|
+
try {
|
|
192
|
+
return { payload: JSON.parse(text), isError };
|
|
193
|
+
} catch {
|
|
194
|
+
return { payload: text, isError };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build a one-line, secret-free summary of tool args. For the HTTP tool we
|
|
200
|
+
* surface `METHOD /sanitized/path`; for browser tools the target
|
|
201
|
+
* URL/selector; everything else falls back to the arg keys so we never dump
|
|
202
|
+
* a full payload (which could carry a body/token) into the timeline.
|
|
203
|
+
*/
|
|
204
|
+
export function summarizeArgs(tool: string, args: unknown): string | undefined {
|
|
205
|
+
if (typeof args !== "object" || args === null) return undefined;
|
|
206
|
+
const bag = args as Record<string, unknown>;
|
|
207
|
+
|
|
208
|
+
if (tool === "http_request" || tool === "journeys_http_request") {
|
|
209
|
+
const method =
|
|
210
|
+
typeof bag.method === "string" ? bag.method.toUpperCase() : "GET";
|
|
211
|
+
const target =
|
|
212
|
+
typeof bag.path === "string"
|
|
213
|
+
? bag.path
|
|
214
|
+
: typeof bag.url === "string"
|
|
215
|
+
? bag.url
|
|
216
|
+
: "";
|
|
217
|
+
return `${method} ${sanitizeForSummary(target)}`.trim();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (tool.startsWith("browser_")) {
|
|
221
|
+
if (typeof bag.url === "string") return sanitizeForSummary(bag.url);
|
|
222
|
+
if (typeof bag.selector === "string") return `selector=${bag.selector}`;
|
|
223
|
+
if (typeof bag.text === "string") return `text="${truncate(bag.text, 40)}"`;
|
|
224
|
+
if (typeof bag.action === "string") return bag.action;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const keys = Object.keys(bag);
|
|
228
|
+
return keys.length > 0 ? `{${keys.join(", ")}}` : undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Summarize a tool result to one line. HTTP results expose their status
|
|
233
|
+
* code and any structured error code; browser results expose the leading
|
|
234
|
+
* line of their page summary; errors expose the error code/message. Never
|
|
235
|
+
* the full body.
|
|
236
|
+
*/
|
|
237
|
+
export function summarizeResult(
|
|
238
|
+
status: "running" | "completed" | "error",
|
|
239
|
+
rawResult: unknown,
|
|
240
|
+
): string | undefined {
|
|
241
|
+
if (status === "running") return undefined;
|
|
242
|
+
// Peel the MCP envelope (custom tools) → the tool's own payload + isError.
|
|
243
|
+
const { payload: result, isError } = unwrapMcpResult(rawResult);
|
|
244
|
+
if (result === undefined || result === null) {
|
|
245
|
+
return status === "error" || isError ? "ERROR" : undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (typeof result === "string") {
|
|
249
|
+
return withErrorPrefix(truncate(firstLine(result), 120), isError);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (typeof result === "object") {
|
|
253
|
+
const bag = result as Record<string, unknown>;
|
|
254
|
+
// Scoped-tool error envelope: { error: { code, message } }.
|
|
255
|
+
if (typeof bag.error === "object" && bag.error !== null) {
|
|
256
|
+
const err = bag.error as Record<string, unknown>;
|
|
257
|
+
const code = typeof err.code === "string" ? err.code : "ERROR";
|
|
258
|
+
const msg = typeof err.message === "string" ? ` ${err.message}` : "";
|
|
259
|
+
return truncate(`${code}${msg}`, 120);
|
|
260
|
+
}
|
|
261
|
+
if (typeof bag.error === "string") {
|
|
262
|
+
return truncate(bag.error, 120);
|
|
263
|
+
}
|
|
264
|
+
// HTTP payload: { status, ... }.
|
|
265
|
+
if (typeof bag.status === "number") {
|
|
266
|
+
return `HTTP ${bag.status}`;
|
|
267
|
+
}
|
|
268
|
+
// Browser summary: { summary: "URL: ...\nTitle: ..." }.
|
|
269
|
+
if (typeof bag.summary === "string") {
|
|
270
|
+
return truncate(firstLine(bag.summary), 120);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return status === "error" ? "error" : "done";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Strip query/fragment/origin from a URL-ish string for the summary line. */
|
|
277
|
+
function sanitizeForSummary(pathOrUrl: string): string {
|
|
278
|
+
if (pathOrUrl.length === 0) return "";
|
|
279
|
+
try {
|
|
280
|
+
const url = new URL(pathOrUrl, "http://placeholder.invalid");
|
|
281
|
+
return url.pathname;
|
|
282
|
+
} catch {
|
|
283
|
+
// Not URL-shaped — return as-is minus any query tail.
|
|
284
|
+
return pathOrUrl.split("?")[0] ?? pathOrUrl;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Prefix a summary with "ERROR " when the tool flagged an error — but only
|
|
290
|
+
* if the text doesn't already begin with "ERROR" (browser-tool failures
|
|
291
|
+
* already start with "ERROR ACTION_FAILED: …", which double-prefixed to the
|
|
292
|
+
* nonsensical "ERROR ERROR ACTION_FAILED" on the first staging run).
|
|
293
|
+
*/
|
|
294
|
+
function withErrorPrefix(text: string, isError: boolean): string {
|
|
295
|
+
if (!isError) return text;
|
|
296
|
+
return /^error\b/i.test(text) ? text : `ERROR ${text}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function firstLine(text: string): string {
|
|
300
|
+
return text.split("\n")[0] ?? text;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function truncate(text: string, max: number): string {
|
|
304
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
305
|
+
return flat.length <= max ? flat : `${flat.slice(0, max - 1)}…`;
|
|
306
|
+
}
|