@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.
Files changed (231) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -2
  4. package/bin/den +61 -0
  5. package/docs/distributions.md +256 -0
  6. package/make/demo.mk +82 -0
  7. package/make/den.mk +80 -0
  8. package/make/journeys.mk +57 -0
  9. package/make/local_dev.mk +154 -0
  10. package/make/security.mk +59 -0
  11. package/make/test.mk +405 -0
  12. package/package.json +104 -4
  13. package/scripts/auth-configure.ts +415 -0
  14. package/scripts/heal-config.ts +92 -0
  15. package/scripts/jsonl-playwright-reporter.js +70 -0
  16. package/scripts/manifest-read.cjs +91 -0
  17. package/scripts/setup-1password.ts +570 -0
  18. package/scripts/setup-secrets.ts +149 -0
  19. package/scripts/view-results.js +156 -0
  20. package/service/install.sh +112 -0
  21. package/src/pillars/api/fixtures/index.ts +128 -0
  22. package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
  23. package/src/pillars/api/global-setup.ts +103 -0
  24. package/src/pillars/api/playwright.config.ts +105 -0
  25. package/src/pillars/api/schemas/example-api.json +110 -0
  26. package/src/pillars/api/tests/health/health.spec.ts +19 -0
  27. package/src/pillars/api/tests/health/manifest.yml +6 -0
  28. package/src/pillars/browser/fixtures/index.ts +232 -0
  29. package/src/pillars/browser/fixtures/package.json +10 -0
  30. package/src/pillars/browser/global-setup.ts +140 -0
  31. package/src/pillars/browser/global-teardown.ts +48 -0
  32. package/src/pillars/browser/playwright.config.ts +212 -0
  33. package/src/pillars/browser/setup.ts +219 -0
  34. package/src/pillars/journeys/app-config.ts +237 -0
  35. package/src/pillars/journeys/discover.ts +160 -0
  36. package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
  37. package/src/pillars/journeys/observability/events.ts +288 -0
  38. package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
  39. package/src/pillars/journeys/report/friction.ts +304 -0
  40. package/src/pillars/journeys/report/junit.ts +105 -0
  41. package/src/pillars/journeys/report/render.ts +273 -0
  42. package/src/pillars/journeys/report/results.ts +323 -0
  43. package/src/pillars/journeys/run.ts +673 -0
  44. package/src/pillars/journeys/runner.ts +1025 -0
  45. package/src/pillars/journeys/sandbox.ts +406 -0
  46. package/src/pillars/journeys/spec/loader.ts +152 -0
  47. package/src/pillars/journeys/spec/schema.ts +208 -0
  48. package/src/pillars/journeys/teardown.ts +98 -0
  49. package/src/pillars/journeys/tools/browser-session.ts +219 -0
  50. package/src/pillars/journeys/tools/browser-tool.ts +914 -0
  51. package/src/pillars/journeys/tools/http-tool.ts +564 -0
  52. package/src/pillars/journeys/tools/ledger.ts +278 -0
  53. package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
  54. package/src/pillars/journeys/verify/assertions.ts +502 -0
  55. package/src/pillars/journeys/verify/verdict.ts +71 -0
  56. package/src/pillars/perf/build-report.ts +36 -0
  57. package/src/pillars/perf/k6.config.js +112 -0
  58. package/src/pillars/perf/run.ts +345 -0
  59. package/src/pillars/perf/scenarios/load-profile.js +97 -0
  60. package/src/pillars/perf/staging.ts +73 -0
  61. package/src/pillars/security/discover.ts +108 -0
  62. package/src/pillars/security/findings.ts +151 -0
  63. package/src/pillars/security/plan.ts +355 -0
  64. package/src/pillars/security/run.ts +729 -0
  65. package/src/pillars/smoke/admin-probe.ts +68 -0
  66. package/src/pillars/smoke/base-url.ts +56 -0
  67. package/src/pillars/smoke/cleanup.ts +8 -0
  68. package/src/pillars/smoke/discover.ts +237 -0
  69. package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
  70. package/src/pillars/smoke/fixture-manager.ts +217 -0
  71. package/src/pillars/smoke/fixtures.ts +468 -0
  72. package/src/pillars/smoke/global-setup-all.ts +262 -0
  73. package/src/pillars/smoke/global-setup-global.ts +262 -0
  74. package/src/pillars/smoke/global-setup.ts +273 -0
  75. package/src/pillars/smoke/global-teardown-all.ts +8 -0
  76. package/src/pillars/smoke/global-teardown-global.ts +8 -0
  77. package/src/pillars/smoke/global-teardown.ts +139 -0
  78. package/src/pillars/smoke/playwright-all.config.ts +147 -0
  79. package/src/pillars/smoke/playwright-global.config.ts +141 -0
  80. package/src/pillars/smoke/playwright.config.ts +162 -0
  81. package/src/pillars/smoke/setup-env.ts +132 -0
  82. package/src/pillars/smoke/setup-global.ts +126 -0
  83. package/src/pillars/smoke/setup.ts +115 -0
  84. package/src/pillars/smoke/smoke-repo.ts +16 -0
  85. package/src/pillars/smoke/staging.ts +49 -0
  86. package/src/pillars/suites/run.ts +147 -0
  87. package/src/scripts/configure-aws-profiles.ts +114 -0
  88. package/src/scripts/register-repos.ts +307 -0
  89. package/src/scripts/setup-default-workspace.ts +203 -0
  90. package/src/shared/applications.ts +138 -0
  91. package/src/shared/auth/api-token.ts +416 -0
  92. package/src/shared/auth/auth0-helper.ts +284 -0
  93. package/src/shared/auth/headless-login.ts +265 -0
  94. package/src/shared/auth/jwt-helper.ts +186 -0
  95. package/src/shared/auth/sudo-vault.ts +69 -0
  96. package/src/shared/auth/test-accounts.ts +98 -0
  97. package/src/shared/auth/token-injection.ts +184 -0
  98. package/src/shared/auth/token-providers/auth0.ts +80 -0
  99. package/src/shared/auth/token-providers/index.ts +182 -0
  100. package/src/shared/auth/token-refresh.ts +273 -0
  101. package/src/shared/banner.ts +15 -0
  102. package/src/shared/brand.ts +79 -0
  103. package/src/shared/cli-tabs.ts +131 -0
  104. package/src/shared/constants.ts +128 -0
  105. package/src/shared/den-config.ts +566 -0
  106. package/src/shared/dev-metrics/emit.ts +354 -0
  107. package/src/shared/edge/config.ts +229 -0
  108. package/src/shared/edge/render.ts +221 -0
  109. package/src/shared/edge/runtime.ts +268 -0
  110. package/src/shared/edge/types.ts +62 -0
  111. package/src/shared/engine.ts +67 -0
  112. package/src/shared/environment/aws-profiles.ts +49 -0
  113. package/src/shared/environment/env-selector.ts +161 -0
  114. package/src/shared/environment/playwright-env.ts +82 -0
  115. package/src/shared/environment-manager/catalog.ts +77 -0
  116. package/src/shared/environment-manager/clone-guard.ts +163 -0
  117. package/src/shared/environment-manager/dag.ts +286 -0
  118. package/src/shared/environment-manager/default-workspace.ts +182 -0
  119. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  120. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  121. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  122. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  123. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  124. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  125. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  126. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  127. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  128. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  129. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  130. package/src/shared/environment-manager/derivation/text.ts +40 -0
  131. package/src/shared/environment-manager/derivation/types.ts +69 -0
  132. package/src/shared/environment-manager/health.ts +589 -0
  133. package/src/shared/environment-manager/index.ts +6 -0
  134. package/src/shared/environment-manager/load-applications.ts +78 -0
  135. package/src/shared/environment-manager/log-tail.ts +212 -0
  136. package/src/shared/environment-manager/make-targets.ts +78 -0
  137. package/src/shared/environment-manager/opctl.ts +334 -0
  138. package/src/shared/environment-manager/readiness.ts +93 -0
  139. package/src/shared/environment-manager/register-local.ts +72 -0
  140. package/src/shared/environment-manager/secrets-check.ts +260 -0
  141. package/src/shared/environment-manager/service-env.ts +110 -0
  142. package/src/shared/environment-manager/service-runner.ts +1588 -0
  143. package/src/shared/environment-manager/up-plan.ts +302 -0
  144. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  145. package/src/shared/environment-manager/workspaces.ts +860 -0
  146. package/src/shared/manifest/deps.ts +27 -0
  147. package/src/shared/manifest/migrate-manifest.ts +101 -0
  148. package/src/shared/manifest/read-manifest.ts +160 -0
  149. package/src/shared/manifest/schema.ts +299 -0
  150. package/src/shared/manifest/service-links.ts +93 -0
  151. package/src/shared/manifest/validate-manifest.ts +140 -0
  152. package/src/shared/manifest/workspace-manifests.ts +92 -0
  153. package/src/shared/open-url.ts +122 -0
  154. package/src/shared/org.ts +44 -0
  155. package/src/shared/paths.ts +178 -0
  156. package/src/shared/perf/optional-fields.ts +105 -0
  157. package/src/shared/perf/report-normalizer.ts +404 -0
  158. package/src/shared/perf/report-paths.ts +19 -0
  159. package/src/shared/perf/report-store.ts +110 -0
  160. package/src/shared/perf/report-types.ts +70 -0
  161. package/src/shared/perf/test-paths.ts +25 -0
  162. package/src/shared/process/log-rotation.ts +147 -0
  163. package/src/shared/process/spawn-streaming.ts +229 -0
  164. package/src/shared/product.ts +92 -0
  165. package/src/shared/profile.ts +141 -0
  166. package/src/shared/repo-kind.ts +32 -0
  167. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  168. package/src/shared/reporting/slack-reporter.ts +340 -0
  169. package/src/shared/results/result-reader.ts +266 -0
  170. package/src/shared/results/result-writer.ts +168 -0
  171. package/src/shared/secrets/doppler.ts +126 -0
  172. package/src/shared/secrets/env.ts +48 -0
  173. package/src/shared/secrets/index.ts +197 -0
  174. package/src/shared/secrets/keys.ts +33 -0
  175. package/src/shared/secrets/onepassword.ts +75 -0
  176. package/src/shared/secrets/types.ts +52 -0
  177. package/src/shared/service-url.ts +119 -0
  178. package/src/shared/smoke/branch-resolver.ts +141 -0
  179. package/src/shared/smoke/git-remote.ts +70 -0
  180. package/src/shared/smoke/repo-cloner.ts +151 -0
  181. package/src/shared/stacks/config.ts +137 -0
  182. package/src/shared/stacks/manifest-commands.ts +88 -0
  183. package/src/shared/stacks/render.ts +253 -0
  184. package/src/shared/stacks/runtime.ts +334 -0
  185. package/src/shared/stacks/types.ts +53 -0
  186. package/src/shared/terminal/tabs.ts +212 -0
  187. package/src/shared/test-discovery/module-scanner.ts +569 -0
  188. package/src/shared/types/index.ts +678 -0
  189. package/src/shared/ui-marker.ts +19 -0
  190. package/src/ui/cli/applications-command.ts +116 -0
  191. package/src/ui/cli/config-command.ts +68 -0
  192. package/src/ui/cli/create-main.ts +9 -0
  193. package/src/ui/cli/create.ts +360 -0
  194. package/src/ui/cli/edge-command.ts +91 -0
  195. package/src/ui/cli/flag-parser.ts +246 -0
  196. package/src/ui/cli/graph-command.ts +524 -0
  197. package/src/ui/cli/health-display.ts +63 -0
  198. package/src/ui/cli/index.ts +3656 -0
  199. package/src/ui/cli/init.ts +855 -0
  200. package/src/ui/cli/invocation-target.ts +175 -0
  201. package/src/ui/cli/logs-command.ts +104 -0
  202. package/src/ui/cli/module-selection.ts +31 -0
  203. package/src/ui/cli/prompts.ts +767 -0
  204. package/src/ui/cli/results-viewer.ts +436 -0
  205. package/src/ui/cli/secrets-preflight.ts +91 -0
  206. package/src/ui/cli/stack-command.ts +144 -0
  207. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  208. package/src/ui/cli/test-runner.ts +1312 -0
  209. package/src/ui/cli/web-command.ts +1073 -0
  210. package/src/ui/server/changelog-parser.ts +94 -0
  211. package/src/ui/server/daemon-reaper.ts +129 -0
  212. package/src/ui/server/editor-manager.ts +301 -0
  213. package/src/ui/server/github-service.ts +223 -0
  214. package/src/ui/server/http-helpers.ts +98 -0
  215. package/src/ui/server/index.ts +3236 -0
  216. package/src/ui/server/log-history.ts +452 -0
  217. package/src/ui/server/perf-report.ts +42 -0
  218. package/src/ui/server/public/environments.html +494 -0
  219. package/src/ui/server/public/favicon.ico +0 -0
  220. package/src/ui/server/public/header.js +1371 -0
  221. package/src/ui/server/public/index.html +2595 -0
  222. package/src/ui/server/public/local-dev.html +7405 -0
  223. package/src/ui/server/public/logs.html +1489 -0
  224. package/src/ui/server/public/perf-report.js +359 -0
  225. package/src/ui/server/public/release.html +2111 -0
  226. package/src/ui/server/public/services.html +1098 -0
  227. package/src/ui/server/public/workspace-picker.js +69 -0
  228. package/src/ui/server/release-deploy-session.ts +246 -0
  229. package/src/ui/server/release-service.ts +1125 -0
  230. package/src/ui/server/sudo-store.ts +111 -0
  231. package/src/ui/server/workspace-routes.ts +2154 -0
@@ -0,0 +1,673 @@
1
+ /**
2
+ * Journeys CLI entrypoint (plan: jrn-pilot-journeys packet).
3
+ *
4
+ * npx tsx src/pillars/journeys/run.ts --env=<env> [--journey=<repo/name>]
5
+ *
6
+ * `--journey` accepts a repo-qualified "<repo>/<name>" (exact) or a bare
7
+ * spec name; bare names duplicated across repos are refused loudly.
8
+ *
9
+ * The FIRST executable surface of the journeys pillar — every library
10
+ * packet (runner, verifier, reporting, teardown) already landed, and this
11
+ * module only orchestrates them:
12
+ *
13
+ * - `--env` is REQUIRED and exactly one environment per invocation
14
+ * (Approach §Explicit environment targeting; the make targets add the
15
+ * command-line-origin guard on top).
16
+ * - Discovery is manifest-driven via discoverJourneySpecs() — specs live
17
+ * in registered repos (the app's own repo), never a den-local dir. Specs whose
18
+ * `environments` allowlist excludes the target env are skipped with a
19
+ * logged reason; zero runnable specs exits with the distinct loud code
20
+ * {@link EXIT_NO_SPECS} — never silent green.
21
+ * - Per journey, the STRICT order is runJourney → verify → collect report
22
+ * → teardown (in finally, strictly AFTER the verification probes so they
23
+ * observe created state) → finalize report (teardown warnings folded
24
+ * in). Reporting is guaranteed in the same finally on every outcome.
25
+ * - Sequential in v1; exit codes: 0 all passed / 1 infra error / 2 any
26
+ * journey failed / 3 no specs discovered.
27
+ * - In-process transcript secret scan: the secrets captured for the run
28
+ * are compared FROM MEMORY against the persisted transcript artifacts —
29
+ * they never pass through argv or a child process. A hit fails the
30
+ * journey as infra_error (belt and suspenders over the runner's
31
+ * persist-path scrubber).
32
+ */
33
+
34
+ import * as fs from "fs";
35
+ import * as path from "path";
36
+ import type { Environment } from "../../shared/types/index.js";
37
+ import {
38
+ discoverJourneySpecs,
39
+ type DiscoveredJourneySpec,
40
+ } from "./discover.js";
41
+ import { extractFrictionPoints } from "./report/friction.js";
42
+ import {
43
+ collectJourneyReport,
44
+ finalizeJourneyReport,
45
+ type CollectedJourneyReport,
46
+ } from "./report/results.js";
47
+ import {
48
+ writeJourneysJUnitXml,
49
+ type JourneysJUnitCase,
50
+ } from "./report/junit.js";
51
+ import {
52
+ runJourney,
53
+ type RunnerDeps,
54
+ type RunnerEnvConfigLike,
55
+ type RunOutcome,
56
+ } from "./runner.js";
57
+ import type { JourneyPersonaRole, JourneySpec } from "./spec/schema.js";
58
+ import type { JourneysSessionTokenConfig } from "./app-config.js";
59
+ import type { CapturedSecrets } from "./sandbox.js";
60
+ import type { SessionApiToken } from "./tools/browser-session.js";
61
+ import { teardownLedger, type TeardownReport } from "./teardown.js";
62
+ import { allowedBaseUrlsFromEnv } from "./tools/http-tool.js";
63
+ import type { RunLedger } from "./tools/ledger.js";
64
+ import {
65
+ buildVerifierProbeClient,
66
+ type VerifierProbeClient,
67
+ type VerifierProbeClientOptions,
68
+ } from "./verify/assertions.js";
69
+ import { verifyJourney, type Verdict } from "./verify/verdict.js";
70
+
71
+ import { product } from "../../shared/product.js";
72
+ // ── Exit codes ───────────────────────────────────────────────────────────────
73
+
74
+ export const EXIT_ALL_PASSED = 0;
75
+ export const EXIT_INFRA_ERROR = 1;
76
+ export const EXIT_JOURNEY_FAILED = 2;
77
+ /** Zero specs discovered/runnable — loud and distinct, never silent green. */
78
+ export const EXIT_NO_SPECS = 3;
79
+
80
+ // ── CLI args ─────────────────────────────────────────────────────────────────
81
+
82
+ const KNOWN_ENVIRONMENTS: readonly Environment[] = [
83
+ "local",
84
+ "dev",
85
+ "sandbox",
86
+ "staging",
87
+ "beta",
88
+ "prod",
89
+ "production",
90
+ ];
91
+
92
+ /** Operator/usage mistake — maps to {@link EXIT_INFRA_ERROR}. */
93
+ export class CliUsageError extends Error {
94
+ constructor(message: string) {
95
+ super(message);
96
+ this.name = "CliUsageError";
97
+ }
98
+ }
99
+
100
+ export interface ParsedCliArgs {
101
+ env: Environment;
102
+ journey?: string;
103
+ }
104
+
105
+ /**
106
+ * Parse `--env=<env> [--journey=<name>]`. `--env` is required and must
107
+ * appear exactly once (one environment per invocation); unknown arguments
108
+ * are rejected loudly rather than ignored.
109
+ */
110
+ export function parseCliArgs(argv: readonly string[]): ParsedCliArgs {
111
+ const envValues: string[] = [];
112
+ let journey: string | undefined;
113
+
114
+ for (const arg of argv) {
115
+ if (arg.startsWith("--env=")) {
116
+ envValues.push(arg.slice("--env=".length));
117
+ } else if (arg.startsWith("--journey=")) {
118
+ journey = arg.slice("--journey=".length);
119
+ } else {
120
+ throw new CliUsageError(
121
+ `Unrecognized argument "${arg}". ` +
122
+ "Usage: run.ts --env=<env> [--journey=<name>]",
123
+ );
124
+ }
125
+ }
126
+
127
+ if (envValues.length === 0) {
128
+ throw new CliUsageError(
129
+ "--env is required (exactly one environment per invocation). " +
130
+ "Usage: run.ts --env=<env> [--journey=<name>]",
131
+ );
132
+ }
133
+ if (envValues.length > 1) {
134
+ throw new CliUsageError(
135
+ "Exactly one --env per invocation — journeys never fan out across " +
136
+ `environments (got: ${envValues.join(", ")}).`,
137
+ );
138
+ }
139
+
140
+ const env = envValues[0];
141
+ if (!(KNOWN_ENVIRONMENTS as readonly string[]).includes(env)) {
142
+ throw new CliUsageError(
143
+ `Unknown environment "${env}". Known: ${KNOWN_ENVIRONMENTS.join(", ")}.`,
144
+ );
145
+ }
146
+
147
+ const parsed: ParsedCliArgs = { env: env as Environment };
148
+ if (journey !== undefined) {
149
+ parsed.journey = journey;
150
+ }
151
+ return parsed;
152
+ }
153
+
154
+ // ── Secret scan ──────────────────────────────────────────────────────────────
155
+
156
+ /**
157
+ * In-process scan: does `contents` contain any live secret value? Values
158
+ * are compared from memory — never serialized to argv, a temp file, or a
159
+ * child process. Empty values are ignored (they would match everything).
160
+ */
161
+ export function transcriptContainsSecret(
162
+ contents: string,
163
+ secrets: readonly string[],
164
+ ): boolean {
165
+ return secrets.some(
166
+ (secret) => secret.length > 0 && contents.includes(secret),
167
+ );
168
+ }
169
+
170
+ // ── Injectable dependencies ──────────────────────────────────────────────────
171
+
172
+ export interface EntrypointDeps {
173
+ discoverSpecs: () => DiscoveredJourneySpec[];
174
+ loadEnvironment: (env: Environment) => Promise<RunnerEnvConfigLike>;
175
+ /**
176
+ * The persona-secret source (Cursor key only). The entrypoint passes a
177
+ * caching wrapper of this into runJourney's deps so the captured value
178
+ * remains available — in memory only — for the transcript secret scan.
179
+ */
180
+ captureSecrets: (
181
+ role: JourneyPersonaRole,
182
+ env: Environment,
183
+ ) => Promise<CapturedSecrets>;
184
+ /**
185
+ * Derive the API assertion token + session headers from the browser
186
+ * session. The entrypoint passes a caching wrapper into runJourney's deps
187
+ * so the SAME real-user token + headers back the verifier probe client and
188
+ * the teardown client — one credential for navigation and assertions.
189
+ */
190
+ deriveApiToken: (
191
+ authFilePath: string,
192
+ env: Environment,
193
+ sessionToken?: JourneysSessionTokenConfig,
194
+ ) => Promise<SessionApiToken>;
195
+ runJourney: (
196
+ spec: JourneySpec,
197
+ env: Environment,
198
+ deps: Partial<RunnerDeps>,
199
+ ) => Promise<RunOutcome>;
200
+ buildProbeClient: (
201
+ options: VerifierProbeClientOptions,
202
+ ) => VerifierProbeClient;
203
+ verifyJourney: typeof verifyJourney;
204
+ teardownLedger: typeof teardownLedger;
205
+ collectReport: typeof collectJourneyReport;
206
+ finalizeReport: (
207
+ collected: CollectedJourneyReport,
208
+ teardown: TeardownReport | undefined,
209
+ ) => ReturnType<typeof finalizeJourneyReport>;
210
+ writeJUnit: (cases: readonly JourneysJUnitCase[]) => Promise<string>;
211
+ extractFriction: typeof extractFrictionPoints;
212
+ /** Read a text file, or undefined when it does not exist. */
213
+ readFileIfExists: (filePath: string) => string | undefined;
214
+ log: (message: string) => void;
215
+ logError: (message: string) => void;
216
+ }
217
+
218
+ function defaultReadFileIfExists(filePath: string): string | undefined {
219
+ if (!fs.existsSync(filePath)) return undefined;
220
+ return fs.readFileSync(filePath, "utf8");
221
+ }
222
+
223
+ /** Production wiring — every collaborator is the real den machinery. */
224
+ export function defaultEntrypointDeps(): EntrypointDeps {
225
+ return {
226
+ discoverSpecs: () => discoverJourneySpecs(),
227
+ loadEnvironment: async (env) => {
228
+ const { envSelector } =
229
+ await import("../../shared/environment/env-selector.js");
230
+ return envSelector.loadEnvironment(env);
231
+ },
232
+ captureSecrets: async (role, env) => {
233
+ const { captureSecrets } = await import("./sandbox.js");
234
+ return captureSecrets(role, env);
235
+ },
236
+ deriveApiToken: async (authFilePath, env, sessionToken) => {
237
+ const { deriveApiTokenFromSession } =
238
+ await import("./tools/browser-session.js");
239
+ return deriveApiTokenFromSession(authFilePath, env, sessionToken);
240
+ },
241
+ runJourney,
242
+ buildProbeClient: buildVerifierProbeClient,
243
+ verifyJourney,
244
+ teardownLedger,
245
+ collectReport: collectJourneyReport,
246
+ finalizeReport: (collected, teardown) =>
247
+ finalizeJourneyReport(collected, teardown),
248
+ writeJUnit: (cases) => writeJourneysJUnitXml(cases),
249
+ extractFriction: extractFrictionPoints,
250
+ readFileIfExists: defaultReadFileIfExists,
251
+ log: (message) => console.log(message),
252
+ logError: (message) => console.error(message),
253
+ };
254
+ }
255
+
256
+ // ── Per-journey execution ────────────────────────────────────────────────────
257
+
258
+ type JourneyStatus = "passed" | "failed" | "infra";
259
+
260
+ interface JourneyExecution {
261
+ status: JourneyStatus;
262
+ junitCase?: JourneysJUnitCase;
263
+ }
264
+
265
+ /** Parse the persisted transcript JSONL into lines for friction analysis. */
266
+ function parseTranscriptLines(contents: string | undefined): unknown[] {
267
+ if (contents === undefined) return [];
268
+ const lines: unknown[] = [];
269
+ for (const line of contents.split("\n")) {
270
+ if (line.trim().length === 0) continue;
271
+ try {
272
+ lines.push(JSON.parse(line));
273
+ } catch {
274
+ // Non-JSON lines carry no structured signal; friction skips them.
275
+ }
276
+ }
277
+ return lines;
278
+ }
279
+
280
+ function statusOf(verdict: Verdict): JourneyStatus {
281
+ if (verdict.passed) return "passed";
282
+ return verdict.failure_kind === "infra_error" ? "infra" : "failed";
283
+ }
284
+
285
+ async function executeJourney(
286
+ discovered: DiscoveredJourneySpec,
287
+ env: Environment,
288
+ envConfig: RunnerEnvConfigLike,
289
+ deps: EntrypointDeps,
290
+ ): Promise<JourneyExecution> {
291
+ const { spec } = discovered;
292
+ deps.log(`▶ journey ${spec.name} (${spec.surface}) — env=${env}`);
293
+
294
+ // Held in memory for verify/teardown/scan; populated by the caching
295
+ // wrappers the runner invokes during its provisioning phase.
296
+ let secrets: CapturedSecrets | undefined;
297
+ let sessionToken: SessionApiToken | undefined;
298
+ const cachingCaptureSecrets = async (
299
+ role: JourneyPersonaRole,
300
+ targetEnv: Environment,
301
+ ): Promise<CapturedSecrets> => {
302
+ secrets = await deps.captureSecrets(role, targetEnv);
303
+ return secrets;
304
+ };
305
+ // The browser-session-derived API token + headers. Cached
306
+ // here so the verifier probe client and teardown client reuse the SAME
307
+ // real-user credential the agent navigated with.
308
+ const cachingDeriveApiToken = async (
309
+ authFilePath: string,
310
+ targetEnv: Environment,
311
+ sessionTokenConfig?: JourneysSessionTokenConfig,
312
+ ): Promise<SessionApiToken> => {
313
+ sessionToken = await deps.deriveApiToken(
314
+ authFilePath,
315
+ targetEnv,
316
+ sessionTokenConfig,
317
+ );
318
+ return sessionToken;
319
+ };
320
+
321
+ let outcome: RunOutcome | undefined;
322
+ let verdict: Verdict | undefined;
323
+ let collected: CollectedJourneyReport | undefined;
324
+ let junitCase: JourneysJUnitCase | undefined;
325
+ let crashed: Error | undefined;
326
+
327
+ const buildClient = (): VerifierProbeClient =>
328
+ deps.buildProbeClient({
329
+ allowedBaseUrls: allowedBaseUrlsFromEnv(envConfig),
330
+ tokenProvider: async () => {
331
+ if (sessionToken === undefined) {
332
+ throw new Error(
333
+ "No session API token available — runJourney never derived one",
334
+ );
335
+ }
336
+ return sessionToken.apiToken;
337
+ },
338
+ headersProvider: async () => {
339
+ if (sessionToken === undefined) {
340
+ throw new Error(
341
+ "No session headers available — runJourney never derived them",
342
+ );
343
+ }
344
+ return sessionToken.headers;
345
+ },
346
+ });
347
+
348
+ try {
349
+ outcome = await deps.runJourney(spec, env, {
350
+ captureSecrets: cachingCaptureSecrets,
351
+ deriveApiToken: cachingDeriveApiToken,
352
+ app: discovered.app,
353
+ });
354
+
355
+ // Post-ledger runner failures come back as outcomes carrying the
356
+ // ledger (review finding 8) — loud in the log, and the normal
357
+ // verify → report → teardown pipeline below still runs.
358
+ if (outcome.error !== undefined) {
359
+ deps.logError(
360
+ `✖ journey ${spec.name} run error (${outcome.failure_kind ?? "unknown"}): ${outcome.error}`,
361
+ );
362
+ }
363
+
364
+ // Verification probes run strictly BEFORE teardown so they observe
365
+ // the created state; even a failed run gets probed for diagnostics.
366
+ verdict = await deps.verifyJourney(
367
+ spec.success.assertions,
368
+ outcome,
369
+ buildClient(),
370
+ );
371
+
372
+ // In-process secret scan over the persisted transcript artifacts
373
+ // (JSONL + rendered markdown), secrets compared from memory.
374
+ const transcriptContents = deps.readFileIfExists(outcome.transcriptPath);
375
+ const renderedContents = deps.readFileIfExists(
376
+ path.join(path.dirname(outcome.transcriptPath), "transcript.md"),
377
+ );
378
+ // The event log is scrubbed at emit time by the bus, but scan it here
379
+ // too — defense in depth alongside the transcript scan (same failure
380
+ // mode: a live secret in a persisted artifact fails the run closed).
381
+ const eventsContents = deps.readFileIfExists(outcome.eventsPath);
382
+ const secretValues = [secrets?.cursorApiKey, sessionToken?.apiToken].filter(
383
+ (value): value is string => value !== undefined,
384
+ );
385
+ const leaked = [transcriptContents, renderedContents, eventsContents].some(
386
+ (contents) =>
387
+ contents !== undefined &&
388
+ transcriptContainsSecret(contents, secretValues),
389
+ );
390
+ if (leaked) {
391
+ deps.logError(
392
+ `✖ journey ${spec.name}: a live secret value was found in the ` +
393
+ "persisted transcript — failing closed as infra_error.",
394
+ );
395
+ outcome.failure_kind = "infra_error";
396
+ verdict = { ...verdict, passed: false, failure_kind: "infra_error" };
397
+ }
398
+
399
+ const friction = deps.extractFriction({
400
+ transcript: parseTranscriptLines(transcriptContents),
401
+ snapshotIndex: outcome.snapshotIndex,
402
+ budgets: spec.budgets,
403
+ });
404
+
405
+ collected = deps.collectReport({
406
+ spec,
407
+ env,
408
+ outcome,
409
+ verdict,
410
+ frictionPoints: friction,
411
+ });
412
+ } catch (error) {
413
+ crashed = error instanceof Error ? error : new Error(String(error));
414
+ deps.logError(`✖ journey ${spec.name} crashed: ${crashed.message}`);
415
+ } finally {
416
+ // Teardown strictly AFTER verification; a run that never produced an
417
+ // outcome has no ledger to tear down (and nothing persisted to report).
418
+ let teardown: TeardownReport | undefined;
419
+ // Teardown needs the session-derived token/headers to authenticate its
420
+ // DELETE probes; skip cleanly if the run never got that far.
421
+ if (outcome !== undefined && sessionToken !== undefined) {
422
+ try {
423
+ teardown = await deps.teardownLedger(
424
+ outcome.ledger as RunLedger,
425
+ async (request) => {
426
+ const response = await buildClient()({
427
+ method: request.method as
428
+ | "GET"
429
+ | "POST"
430
+ | "PUT"
431
+ | "PATCH"
432
+ | "DELETE",
433
+ path: request.path,
434
+ });
435
+ return { status: response.status };
436
+ },
437
+ );
438
+ } catch (error) {
439
+ deps.logError(
440
+ `✖ journey ${spec.name} teardown crashed: ` +
441
+ `${error instanceof Error ? error.message : String(error)}`,
442
+ );
443
+ crashed ??= error instanceof Error ? error : new Error(String(error));
444
+ }
445
+ }
446
+
447
+ // Reporting guaranteed on every outcome: when the pipeline crashed
448
+ // after producing an outcome, persist a failed infra report anyway.
449
+ if (outcome !== undefined) {
450
+ if (collected === undefined) {
451
+ verdict = {
452
+ passed: false,
453
+ failure_kind: outcome.failure_kind ?? "infra_error",
454
+ results: [],
455
+ };
456
+ collected = deps.collectReport({
457
+ spec,
458
+ env,
459
+ outcome,
460
+ verdict,
461
+ frictionPoints: [],
462
+ });
463
+ }
464
+ try {
465
+ const finalized = await deps.finalizeReport(collected, teardown);
466
+ junitCase = finalized.junitCase;
467
+ deps.log(` report: ${finalized.reportPath}`);
468
+ // Point operators at the structured timeline for post-mortem; the
469
+ // report embeds it too, but the raw JSONL is the forensic source.
470
+ if (outcome !== undefined) {
471
+ deps.log(` events: ${outcome.eventsPath}`);
472
+ }
473
+ } catch (error) {
474
+ deps.logError(
475
+ `✖ journey ${spec.name} report finalization failed: ` +
476
+ `${error instanceof Error ? error.message : String(error)}`,
477
+ );
478
+ crashed ??= error instanceof Error ? error : new Error(String(error));
479
+ }
480
+ }
481
+ }
482
+
483
+ if (crashed !== undefined || verdict === undefined) {
484
+ const execution: JourneyExecution = { status: "infra" };
485
+ if (junitCase !== undefined) execution.junitCase = junitCase;
486
+ return execution;
487
+ }
488
+
489
+ const status = statusOf(verdict);
490
+ deps.log(
491
+ status === "passed"
492
+ ? `✔ journey ${spec.name} passed`
493
+ : `✖ journey ${spec.name} ${status === "infra" ? "hit an infra error" : "failed"}` +
494
+ (verdict.failure_kind !== undefined
495
+ ? ` (failure_kind=${verdict.failure_kind})`
496
+ : ""),
497
+ );
498
+ const execution: JourneyExecution = { status };
499
+ if (junitCase !== undefined) execution.junitCase = junitCase;
500
+ return execution;
501
+ }
502
+
503
+ // ── CLI orchestration ────────────────────────────────────────────────────────
504
+
505
+ /**
506
+ * Run the journeys pillar once against one environment. Returns the
507
+ * process exit code (see the EXIT_* constants).
508
+ */
509
+ export async function runJourneysCli(
510
+ argv: readonly string[],
511
+ deps: EntrypointDeps = defaultEntrypointDeps(),
512
+ ): Promise<number> {
513
+ let args: ParsedCliArgs;
514
+ try {
515
+ args = parseCliArgs(argv);
516
+ } catch (error) {
517
+ deps.logError(error instanceof Error ? error.message : String(error));
518
+ return EXIT_INFRA_ERROR;
519
+ }
520
+
521
+ let discovered: DiscoveredJourneySpec[];
522
+ try {
523
+ discovered = deps.discoverSpecs();
524
+ } catch (error) {
525
+ deps.logError(
526
+ `Journey spec discovery failed: ` +
527
+ `${error instanceof Error ? error.message : String(error)}`,
528
+ );
529
+ return EXIT_INFRA_ERROR;
530
+ }
531
+
532
+ // Repo-qualified filtering (review finding 10): --journey accepts
533
+ // "<repo>/<name>" (exact) or a bare name — bare names must be
534
+ // unambiguous across repos, else the run refuses loudly instead of
535
+ // executing an arbitrary duplicate.
536
+ if (args.journey !== undefined) {
537
+ const requested = args.journey;
538
+ const slash = requested.indexOf("/");
539
+ if (slash !== -1) {
540
+ const repo = requested.slice(0, slash);
541
+ const name = requested.slice(slash + 1);
542
+ discovered = discovered.filter(
543
+ (entry) => entry.repoName === repo && entry.spec.name === name,
544
+ );
545
+ if (discovered.length === 0) {
546
+ deps.logError(
547
+ `No discovered journey "${name}" in repo "${repo}". Check the ` +
548
+ "spec name and that the repo is registered (make register-repos).",
549
+ );
550
+ return EXIT_NO_SPECS;
551
+ }
552
+ } else {
553
+ const matches = discovered.filter(
554
+ (entry) => entry.spec.name === requested,
555
+ );
556
+ if (matches.length === 0) {
557
+ deps.logError(
558
+ `No discovered journey named "${requested}". Check the spec name ` +
559
+ "and that its repo is registered (make register-repos).",
560
+ );
561
+ return EXIT_NO_SPECS;
562
+ }
563
+ const repos = [...new Set(matches.map((entry) => entry.repoName))];
564
+ if (repos.length > 1) {
565
+ deps.logError(
566
+ `Journey name "${requested}" is ambiguous — declared by repos ` +
567
+ `[${repos.join(", ")}]. Qualify it as <repo>/<name> ` +
568
+ `(e.g. --journey=${repos[0]}/${requested}).`,
569
+ );
570
+ return EXIT_INFRA_ERROR;
571
+ }
572
+ discovered = matches;
573
+ }
574
+ }
575
+
576
+ if (discovered.length === 0) {
577
+ deps.logError(
578
+ "No journey specs discovered across registered repos. Register the " +
579
+ `spec-hosting repo (make register-repos) and check its ${product().manifestYml} ` +
580
+ "journeys: block — zero specs is never a pass.",
581
+ );
582
+ return EXIT_NO_SPECS;
583
+ }
584
+
585
+ const runnable: DiscoveredJourneySpec[] = [];
586
+ for (const entry of discovered) {
587
+ if (entry.spec.environments.includes(args.env)) {
588
+ runnable.push(entry);
589
+ } else {
590
+ deps.log(
591
+ `↷ skipping ${entry.spec.name}: environments allowlist ` +
592
+ `[${entry.spec.environments.join(", ")}] excludes "${args.env}"`,
593
+ );
594
+ }
595
+ }
596
+
597
+ if (runnable.length === 0) {
598
+ deps.logError(
599
+ `0 of ${discovered.length} discovered journeys allow env "${args.env}" ` +
600
+ "— nothing ran (never silent green).",
601
+ );
602
+ return EXIT_NO_SPECS;
603
+ }
604
+
605
+ let envConfig: RunnerEnvConfigLike;
606
+ try {
607
+ envConfig = await deps.loadEnvironment(args.env);
608
+ } catch (error) {
609
+ deps.logError(
610
+ `Failed to load environment "${args.env}": ` +
611
+ `${error instanceof Error ? error.message : String(error)}`,
612
+ );
613
+ return EXIT_INFRA_ERROR;
614
+ }
615
+
616
+ // Sequential in v1 — one agent, one browser context at a time.
617
+ const executions: JourneyExecution[] = [];
618
+ for (const entry of runnable) {
619
+ executions.push(await executeJourney(entry, args.env, envConfig, deps));
620
+ }
621
+
622
+ const junitCases = executions.flatMap((execution) =>
623
+ execution.junitCase !== undefined ? [execution.junitCase] : [],
624
+ );
625
+ if (junitCases.length > 0) {
626
+ try {
627
+ const junitPath = await deps.writeJUnit(junitCases);
628
+ deps.log(`JUnit: ${junitPath}`);
629
+ } catch (error) {
630
+ deps.logError(
631
+ `Failed to write the JUnit report: ` +
632
+ `${error instanceof Error ? error.message : String(error)}`,
633
+ );
634
+ return EXIT_INFRA_ERROR;
635
+ }
636
+ }
637
+
638
+ const infra = executions.filter((e) => e.status === "infra").length;
639
+ const failed = executions.filter((e) => e.status === "failed").length;
640
+ const passedCount = executions.length - infra - failed;
641
+ deps.log(
642
+ `journeys: ${passedCount} passed, ${failed} failed, ${infra} infra ` +
643
+ `error(s) — env=${args.env}`,
644
+ );
645
+
646
+ // Infra errors dominate: the result set is unreliable, not merely red.
647
+ if (infra > 0) return EXIT_INFRA_ERROR;
648
+ if (failed > 0) return EXIT_JOURNEY_FAILED;
649
+ return EXIT_ALL_PASSED;
650
+ }
651
+
652
+ // ── Main-module gate ─────────────────────────────────────────────────────────
653
+
654
+ // No import.meta here: ts-jest compiles this module to CJS for the unit
655
+ // tests, where import.meta is a syntax error. Matching argv[1] against this
656
+ // module's stable repo path keeps the gate inert under jest (argv[1] is the
657
+ // jest binary) while firing under `npx tsx src/pillars/journeys/run.ts`.
658
+ const isMainModule = /[\\/]pillars[\\/]journeys[\\/]run\.(ts|js)$/.test(
659
+ process.argv[1] ?? "",
660
+ );
661
+
662
+ if (isMainModule) {
663
+ runJourneysCli(process.argv.slice(2))
664
+ .then((exitCode) => {
665
+ process.exitCode = exitCode;
666
+ })
667
+ .catch((error: unknown) => {
668
+ console.error(
669
+ error instanceof Error ? (error.stack ?? error.message) : String(error),
670
+ );
671
+ process.exitCode = EXIT_INFRA_ERROR;
672
+ });
673
+ }