stageflow 0.27.0 → 0.31.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 (270) hide show
  1. package/README.md +4 -0
  2. package/builtin-skills/browser/SKILL.md +42 -0
  3. package/dist/agent/claudeAdapter.js +2 -2
  4. package/dist/agent/cursorProvider.d.ts +10 -0
  5. package/dist/agent/cursorProvider.js +83 -8
  6. package/dist/agent/fakeAgent.d.ts +1 -0
  7. package/dist/agent/fakeAgent.js +13 -0
  8. package/dist/agent/piAdapter.d.ts +3 -24
  9. package/dist/agent/piAdapter.js +49 -47
  10. package/dist/agent/piSessionFactory.d.ts +38 -0
  11. package/dist/agent/piSessionFactory.js +60 -0
  12. package/dist/agent/port.d.ts +2 -0
  13. package/dist/agent/providerAuth.d.ts +7 -3
  14. package/dist/agent/providerAuth.js +5 -3
  15. package/dist/agent/streamLogRedact.d.ts +6 -0
  16. package/dist/agent/streamLogRedact.js +23 -1
  17. package/dist/browser/anchor.d.ts +27 -0
  18. package/dist/browser/anchor.js +127 -0
  19. package/dist/browser/auditSink.d.ts +38 -0
  20. package/dist/browser/auditSink.js +34 -0
  21. package/dist/browser/browserHost.d.ts +86 -0
  22. package/dist/browser/browserHost.js +7 -0
  23. package/dist/browser/browserSweep.d.ts +14 -0
  24. package/dist/browser/browserSweep.js +54 -0
  25. package/dist/browser/browserTeardown.d.ts +56 -0
  26. package/dist/browser/browserTeardown.js +235 -0
  27. package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -0
  28. package/dist/browser/fakeRemoteBrowserHost.js +34 -0
  29. package/dist/browser/gateHandoff.d.ts +25 -0
  30. package/dist/browser/gateHandoff.js +83 -0
  31. package/dist/browser/humanLogin.d.ts +18 -0
  32. package/dist/browser/humanLogin.js +34 -0
  33. package/dist/browser/keyProvider.d.ts +10 -0
  34. package/dist/browser/keyProvider.js +39 -0
  35. package/dist/browser/localBrowserHost.d.ts +10 -0
  36. package/dist/browser/localBrowserHost.js +152 -0
  37. package/dist/browser/localProfileLock.d.ts +2 -0
  38. package/dist/browser/localProfileLock.js +218 -0
  39. package/dist/browser/localProfileStore.d.ts +5 -0
  40. package/dist/browser/localProfileStore.js +91 -0
  41. package/dist/browser/loginCheck.d.ts +53 -0
  42. package/dist/browser/loginCheck.js +147 -0
  43. package/dist/browser/memoryProfileLock.d.ts +2 -0
  44. package/dist/browser/memoryProfileLock.js +53 -0
  45. package/dist/browser/memoryProfileStore.d.ts +2 -0
  46. package/dist/browser/memoryProfileStore.js +31 -0
  47. package/dist/browser/navigationAudit.d.ts +16 -0
  48. package/dist/browser/navigationAudit.js +53 -0
  49. package/dist/browser/persistedEnv.d.ts +3 -0
  50. package/dist/browser/persistedEnv.js +15 -0
  51. package/dist/browser/profileLock.d.ts +40 -0
  52. package/dist/browser/profileLock.js +10 -0
  53. package/dist/browser/profileStore.d.ts +23 -0
  54. package/dist/browser/profileStore.js +39 -0
  55. package/dist/browser/runLiveness.d.ts +13 -0
  56. package/dist/browser/runLiveness.js +39 -0
  57. package/dist/browser/sitePolicy.d.ts +10 -0
  58. package/dist/browser/sitePolicy.js +61 -0
  59. package/dist/browser/stageBrowserEnv.d.ts +22 -0
  60. package/dist/browser/stageBrowserEnv.js +147 -0
  61. package/dist/browser/stageProfileLock.d.ts +17 -0
  62. package/dist/browser/stageProfileLock.js +39 -0
  63. package/dist/cli/browserCommand.d.ts +44 -0
  64. package/dist/cli/browserCommand.js +472 -0
  65. package/dist/cli/doctorCommand.js +7 -0
  66. package/dist/cli/hostClient.d.ts +20 -0
  67. package/dist/cli/hostClient.js +36 -2
  68. package/dist/cli/initCommand.d.ts +1 -0
  69. package/dist/cli/initCommand.js +23 -7
  70. package/dist/cli/initTemplates.d.ts +5 -2
  71. package/dist/cli/initTemplates.js +25 -2
  72. package/dist/cli/providersCommand.d.ts +1 -1
  73. package/dist/cli/providersCommand.js +4 -4
  74. package/dist/cli/runsCommand.js +6 -2
  75. package/dist/cli/triggerCommand.d.ts +12 -0
  76. package/dist/cli/triggerCommand.js +183 -0
  77. package/dist/cli/validateCommand.d.ts +1 -1
  78. package/dist/cli/validateCommand.js +23 -6
  79. package/dist/cli.js +32 -3
  80. package/dist/config/browseCatalog.d.ts +1 -0
  81. package/dist/config/browseCatalog.js +5 -4
  82. package/dist/config/builtinSkills.d.ts +4 -0
  83. package/dist/config/builtinSkills.js +23 -0
  84. package/dist/config/catalogRelativePath.d.ts +1 -0
  85. package/dist/config/catalogRelativePath.js +10 -2
  86. package/dist/config/createTrigger.d.ts +27 -0
  87. package/dist/config/createTrigger.js +232 -0
  88. package/dist/config/draftPackage.d.ts +117 -0
  89. package/dist/config/draftPackage.js +550 -0
  90. package/dist/config/ensureCatalogScanRoot.d.ts +16 -0
  91. package/dist/config/ensureCatalogScanRoot.js +73 -0
  92. package/dist/config/hostConfig.d.ts +2 -0
  93. package/dist/config/hostConfig.js +27 -0
  94. package/dist/config/listSkills.d.ts +2 -2
  95. package/dist/config/listSkills.js +17 -3
  96. package/dist/config/loadOutcome.d.ts +6 -0
  97. package/dist/config/loadPipeline.js +16 -0
  98. package/dist/config/loadStage.js +7 -0
  99. package/dist/config/loadStageflowManifest.js +25 -0
  100. package/dist/config/loadTrigger.d.ts +9 -0
  101. package/dist/config/loadTrigger.js +120 -0
  102. package/dist/config/multiProjectCatalog.d.ts +2 -0
  103. package/dist/config/multiProjectCatalog.js +26 -18
  104. package/dist/config/normalizePipelineStageEntry.js +8 -1
  105. package/dist/config/parseCompletionContract.js +6 -0
  106. package/dist/config/pipelineStageKeys.js +1 -0
  107. package/dist/config/publishDraftPackage.d.ts +7 -0
  108. package/dist/config/publishDraftPackage.js +25 -0
  109. package/dist/config/resolveCatalogContext.js +1 -1
  110. package/dist/config/resolvePipelineDag.js +1 -1
  111. package/dist/config/scanCatalogPaths.d.ts +1 -1
  112. package/dist/config/scanCatalogPaths.js +6 -2
  113. package/dist/config/stageBrowser.d.ts +12 -0
  114. package/dist/config/stageBrowser.js +118 -0
  115. package/dist/config/triggerCatalog.d.ts +10 -0
  116. package/dist/config/triggerCatalog.js +37 -0
  117. package/dist/config/validateCatalog.d.ts +13 -3
  118. package/dist/config/validateCatalog.js +83 -2
  119. package/dist/config/validateCompletionContract.js +8 -0
  120. package/dist/envelope/preEmitChecks.d.ts +1 -1
  121. package/dist/envelope/preEmitChecks.js +8 -0
  122. package/dist/logging/redact.d.ts +4 -0
  123. package/dist/logging/redact.js +17 -0
  124. package/dist/mcp/catalogTools.js +43 -8
  125. package/dist/mcp/controlTools.js +3 -1
  126. package/dist/mcp/deps.d.ts +2 -0
  127. package/dist/mcp/server.js +1 -0
  128. package/dist/mcp/tools.js +2 -0
  129. package/dist/mcp/triggerTools.d.ts +3 -0
  130. package/dist/mcp/triggerTools.js +176 -0
  131. package/dist/operatorAgent/docsRetrieval.d.ts +40 -0
  132. package/dist/operatorAgent/docsRetrieval.js +269 -0
  133. package/dist/operatorAgent/draftContext.d.ts +16 -0
  134. package/dist/operatorAgent/draftContext.js +68 -0
  135. package/dist/operatorAgent/fakeHost.d.ts +39 -0
  136. package/dist/operatorAgent/fakeHost.js +92 -0
  137. package/dist/operatorAgent/host.d.ts +40 -0
  138. package/dist/operatorAgent/host.js +179 -0
  139. package/dist/operatorAgent/index.d.ts +9 -0
  140. package/dist/operatorAgent/index.js +8 -0
  141. package/dist/operatorAgent/piModel.d.ts +80 -0
  142. package/dist/operatorAgent/piModel.js +528 -0
  143. package/dist/operatorAgent/profiles/workshopAuthor.d.ts +23 -0
  144. package/dist/operatorAgent/profiles/workshopAuthor.js +338 -0
  145. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +6 -0
  146. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +103 -0
  147. package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +57 -0
  148. package/dist/operatorAgent/profiles/workshopAuthorTools.js +623 -0
  149. package/dist/operatorAgent/proposals.d.ts +32 -0
  150. package/dist/operatorAgent/proposals.js +144 -0
  151. package/dist/operatorAgent/types.d.ts +128 -0
  152. package/dist/operatorAgent/types.js +1 -0
  153. package/dist/package-meta.d.ts +1 -1
  154. package/dist/package-meta.js +1 -1
  155. package/dist/project/findManifestRoot.d.ts +3 -0
  156. package/dist/project/findManifestRoot.js +35 -0
  157. package/dist/project/resolveStageflowContext.d.ts +7 -0
  158. package/dist/project/resolveStageflowContext.js +44 -28
  159. package/dist/runstore/assertTmpdir.js +6 -2
  160. package/dist/runstore/diskUsage.d.ts +1 -1
  161. package/dist/runstore/diskUsage.js +2 -7
  162. package/dist/runstore/port.d.ts +28 -0
  163. package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -1
  164. package/dist/runstore/sqlite/SqliteRunStore.js +90 -0
  165. package/dist/runstore/sqlite/migrations/008-triggers-table.d.ts +7 -0
  166. package/dist/runstore/sqlite/migrations/008-triggers-table.js +19 -0
  167. package/dist/runstore/sqlite/migrations/009-trigger-next-run.d.ts +7 -0
  168. package/dist/runstore/sqlite/migrations/009-trigger-next-run.js +19 -0
  169. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.d.ts +7 -0
  170. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.js +17 -0
  171. package/dist/runstore/sqlite/migrations/index.js +6 -0
  172. package/dist/runtime/answerResume.d.ts +1 -0
  173. package/dist/runtime/answerResume.js +3 -1
  174. package/dist/runtime/completionCheckRunner.d.ts +12 -1
  175. package/dist/runtime/completionCheckRunner.js +22 -0
  176. package/dist/runtime/credentialBinding.d.ts +7 -2
  177. package/dist/runtime/credentialBinding.js +20 -28
  178. package/dist/runtime/emailSource.d.ts +72 -0
  179. package/dist/runtime/emailSource.js +304 -0
  180. package/dist/runtime/githubPollSource.d.ts +86 -0
  181. package/dist/runtime/githubPollSource.js +284 -0
  182. package/dist/runtime/pipelineRunner.d.ts +4 -0
  183. package/dist/runtime/pipelineRunner.js +3 -0
  184. package/dist/runtime/pipelineScheduler.d.ts +2 -0
  185. package/dist/runtime/pipelineScheduler.js +86 -1
  186. package/dist/runtime/repositoryMaterialize.d.ts +1 -1
  187. package/dist/runtime/runManager.d.ts +11 -0
  188. package/dist/runtime/runManager.js +161 -3
  189. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  190. package/dist/runtime/runRetryCoordinator.js +1 -0
  191. package/dist/runtime/scheduleSource.d.ts +38 -0
  192. package/dist/runtime/scheduleSource.js +119 -0
  193. package/dist/runtime/settingsFile.d.ts +7 -2
  194. package/dist/runtime/settingsFile.js +66 -56
  195. package/dist/runtime/stageAttemptBootstrap.js +55 -3
  196. package/dist/runtime/stageEnvironment.js +1 -0
  197. package/dist/runtime/stageHitl.d.ts +1 -0
  198. package/dist/runtime/stageHitl.js +5 -1
  199. package/dist/runtime/stageProcessLauncher.d.ts +3 -0
  200. package/dist/runtime/stageProcessLauncher.js +52 -5
  201. package/dist/runtime/stageRunner.d.ts +5 -0
  202. package/dist/runtime/stageRunner.js +30 -6
  203. package/dist/runtime/stageStreamLog.d.ts +1 -0
  204. package/dist/runtime/stageStreamLog.js +2 -1
  205. package/dist/runtime/triggerCatalog.d.ts +6 -0
  206. package/dist/runtime/triggerCatalog.js +24 -0
  207. package/dist/runtime/triggerEventMatch.d.ts +2 -0
  208. package/dist/runtime/triggerEventMatch.js +6 -0
  209. package/dist/runtime/triggerPort.d.ts +14 -0
  210. package/dist/runtime/triggerPort.js +1 -0
  211. package/dist/runtime/triggerRunner.d.ts +17 -0
  212. package/dist/runtime/triggerRunner.js +89 -0
  213. package/dist/runtime/verifiedStageExecution.d.ts +7 -0
  214. package/dist/runtime/verifiedStageExecution.js +14 -0
  215. package/dist/runtime/webhookSignature.d.ts +1 -0
  216. package/dist/runtime/webhookSignature.js +12 -0
  217. package/dist/server/bootstrap.d.ts +6 -0
  218. package/dist/server/bootstrap.js +70 -0
  219. package/dist/server/controlToken.js +13 -0
  220. package/dist/server/createHttpHost.js +3 -0
  221. package/dist/server/healthSurfaces.js +31 -6
  222. package/dist/server/http.d.ts +15 -0
  223. package/dist/server/http.js +1251 -74
  224. package/dist/server/requestAuthContext.d.ts +1 -1
  225. package/dist/server/triggerRoutes.d.ts +15 -0
  226. package/dist/server/triggerRoutes.js +268 -0
  227. package/dist/tools/askOperator.d.ts +3 -2
  228. package/dist/types/completion.d.ts +3 -0
  229. package/dist/types/pipeline.d.ts +3 -1
  230. package/dist/types/preEmitCheck.d.ts +7 -0
  231. package/dist/types/stage.d.ts +14 -0
  232. package/dist/types/stageflowManifest.d.ts +3 -0
  233. package/dist/types/trigger.d.ts +18 -0
  234. package/dist/types/trigger.js +1 -0
  235. package/dist/ui/assets/{index-CSsDued1.css → index-Bv4p1aPK.css} +1 -1
  236. package/dist/ui/assets/index-p8Iou3zk.js +133 -0
  237. package/dist/ui/index.html +2 -2
  238. package/dist/workshop/autosave.d.ts +58 -0
  239. package/dist/workshop/autosave.js +222 -0
  240. package/dist/workshop/buildStore.d.ts +79 -0
  241. package/dist/workshop/buildStore.js +326 -0
  242. package/dist/workshop/chatTurn.d.ts +232 -0
  243. package/dist/workshop/chatTurn.js +475 -0
  244. package/dist/workshop/modelSettings.d.ts +18 -0
  245. package/dist/workshop/modelSettings.js +44 -0
  246. package/dist/workshop/sessionStore.d.ts +61 -0
  247. package/dist/workshop/sessionStore.js +254 -0
  248. package/dist/workshop/workshopPin.d.ts +72 -0
  249. package/dist/workshop/workshopPin.js +193 -0
  250. package/examples/README.md +2 -1
  251. package/examples/browser-session/README.md +77 -0
  252. package/examples/browser-session/browser-session.pipeline.yaml +50 -0
  253. package/examples/browser-session/browser-session.task.yaml +6 -0
  254. package/examples/browser-session/check-login.yaml +22 -0
  255. package/examples/browser-session/fixture-server.mjs +64 -0
  256. package/examples/browser-session/human-login.yaml +31 -0
  257. package/examples/browser-session/merge.yaml +36 -0
  258. package/examples/browser-session/work-a.yaml +41 -0
  259. package/examples/browser-session/work-b.yaml +41 -0
  260. package/examples/conditional-fork/decide.yaml +1 -1
  261. package/examples/github-release/README.md +17 -3
  262. package/examples/stage-mcp/README.md +3 -2
  263. package/examples/stageflow.yaml +51 -0
  264. package/package.json +9 -4
  265. package/skills/stageflow/references/control-surface.md +3 -1
  266. package/skills/stageflow-author/references/stage-prompt-template.md +4 -4
  267. package/skills/stageflow-run/SKILL.md +2 -0
  268. package/skills/stageflow-run/references/task-and-pipeline-selection.md +3 -1
  269. package/skills/stageflow-setup/SKILL.md +11 -0
  270. package/dist/ui/assets/index-B6hlSXXn.js +0 -120
@@ -0,0 +1,7 @@
1
+ import type Database from "better-sqlite3";
2
+ export declare const MIGRATION_008: {
3
+ version: number;
4
+ name: string;
5
+ minStageflowVersion: string;
6
+ up(db: Database.Database): void;
7
+ };
@@ -0,0 +1,19 @@
1
+ import { PACKAGE_VERSION } from "../../../package-meta.js";
2
+ export const MIGRATION_008 = {
3
+ version: 8,
4
+ name: "008_triggers_table",
5
+ minStageflowVersion: PACKAGE_VERSION,
6
+ up(db) {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS triggers (
9
+ id TEXT PRIMARY KEY NOT NULL,
10
+ definition_ref TEXT NOT NULL,
11
+ enabled INTEGER NOT NULL DEFAULT 1,
12
+ last_fired_at TEXT,
13
+ last_run_id TEXT,
14
+ created_at TEXT NOT NULL,
15
+ updated_at TEXT NOT NULL
16
+ );
17
+ `);
18
+ },
19
+ };
@@ -0,0 +1,7 @@
1
+ import type Database from "better-sqlite3";
2
+ export declare const MIGRATION_009: {
3
+ version: number;
4
+ name: string;
5
+ minStageflowVersion: string;
6
+ up(db: Database.Database): void;
7
+ };
@@ -0,0 +1,19 @@
1
+ import { PACKAGE_VERSION } from "../../../package-meta.js";
2
+ function addNextRunAtColumn(db) {
3
+ try {
4
+ db.exec(`ALTER TABLE triggers ADD COLUMN next_run_at TEXT`);
5
+ }
6
+ catch (err) {
7
+ const message = err instanceof Error ? err.message : String(err);
8
+ if (!/duplicate column name/i.test(message))
9
+ throw err;
10
+ }
11
+ }
12
+ export const MIGRATION_009 = {
13
+ version: 9,
14
+ name: "009_trigger_next_run",
15
+ minStageflowVersion: PACKAGE_VERSION,
16
+ up(db) {
17
+ addNextRunAtColumn(db);
18
+ },
19
+ };
@@ -0,0 +1,7 @@
1
+ import type Database from "better-sqlite3";
2
+ export declare const MIGRATION_010: {
3
+ version: number;
4
+ name: string;
5
+ minStageflowVersion: string;
6
+ up(db: Database.Database): void;
7
+ };
@@ -0,0 +1,17 @@
1
+ import { PACKAGE_VERSION } from "../../../package-meta.js";
2
+ export const MIGRATION_010 = {
3
+ version: 10,
4
+ name: "010_trigger_adapter_state",
5
+ minStageflowVersion: PACKAGE_VERSION,
6
+ up(db) {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS trigger_adapter_state (
9
+ trigger_id TEXT NOT NULL,
10
+ key TEXT NOT NULL,
11
+ value TEXT NOT NULL,
12
+ updated_at TEXT NOT NULL,
13
+ PRIMARY KEY (trigger_id, key)
14
+ );
15
+ `);
16
+ },
17
+ };
@@ -6,6 +6,9 @@ import { MIGRATION_004 } from "./004-auto-resume-count.js";
6
6
  import { MIGRATION_005 } from "./005-config-origins.js";
7
7
  import { MIGRATION_006 } from "./006-pipeline-body-and-caller.js";
8
8
  import { MIGRATION_007 } from "./007-projects-registry.js";
9
+ import { MIGRATION_008 } from "./008-triggers-table.js";
10
+ import { MIGRATION_009 } from "./009-trigger-next-run.js";
11
+ import { MIGRATION_010 } from "./010-trigger-adapter-state.js";
9
12
  const DEFAULT_MIGRATIONS = [
10
13
  MIGRATION_001,
11
14
  MIGRATION_002,
@@ -14,6 +17,9 @@ const DEFAULT_MIGRATIONS = [
14
17
  MIGRATION_005,
15
18
  MIGRATION_006,
16
19
  MIGRATION_007,
20
+ MIGRATION_008,
21
+ MIGRATION_009,
22
+ MIGRATION_010,
17
23
  ];
18
24
  export const CURRENT_SCHEMA_VERSION = DEFAULT_MIGRATIONS[DEFAULT_MIGRATIONS.length - 1].version;
19
25
  function throwStoreSchemaTooNew(db, version, maxKnown) {
@@ -4,6 +4,7 @@ export declare const WAIT_WITHOUT_WORKER_DISPATCH = "stage entered wait without
4
4
  export type AnswerResumeAdapterResult = {
5
5
  ok: boolean;
6
6
  reason?: string;
7
+ verification?: "failed_retrying";
7
8
  };
8
9
  export declare function orchestrateAnswerResume(options: {
9
10
  prefixMode: "live" | "restart";
@@ -18,5 +18,7 @@ export async function orchestrateAnswerResume(options) {
18
18
  status: 500,
19
19
  };
20
20
  }
21
- return { ok: true };
21
+ return outcome.verification !== undefined
22
+ ? { ok: true, verification: outcome.verification }
23
+ : { ok: true };
22
24
  }
@@ -72,6 +72,12 @@ export type CompletionCheckRunnerInput = {
72
72
  /** Why the runtime could not establish the required pre-stage baseline. */
73
73
  checkoutError?: string;
74
74
  checklistAttestations?: readonly ChecklistAttestation[];
75
+ /** Re-runs the Host login check against the stage's own browser session. */
76
+ browserLogin?: () => Promise<{
77
+ state: "logged_in" | "logged_out" | "unknown";
78
+ logged_in: boolean | null;
79
+ url: string;
80
+ }>;
75
81
  onCheckStart?: (check: CompletionCheck) => Promise<void> | void;
76
82
  onCheckComplete?: (result: CompletionCheckResult) => Promise<void> | void;
77
83
  };
@@ -118,7 +124,12 @@ export type ChecklistCheckEvidence = {
118
124
  missing_items: string[];
119
125
  unexpected_items: string[];
120
126
  };
121
- export type CompletionCheckEvidence = CommandCheckEvidence | ArtifactCheckEvidence | PayloadSchemaCheckEvidence | GateCheckEvidence | CheckoutChangesCheckEvidence | ChecklistCheckEvidence;
127
+ export type BrowserLoginCheckEvidence = {
128
+ kind: "browser_login";
129
+ state?: "logged_in" | "logged_out" | "unknown";
130
+ url?: string;
131
+ };
132
+ export type CompletionCheckEvidence = BrowserLoginCheckEvidence | CommandCheckEvidence | ArtifactCheckEvidence | PayloadSchemaCheckEvidence | GateCheckEvidence | CheckoutChangesCheckEvidence | ChecklistCheckEvidence;
122
133
  export type CompletionCheckResult = {
123
134
  check_id: string;
124
135
  check_type: CompletionCheck["type"];
@@ -364,6 +364,26 @@ function runChecklistCheck(check, input) {
364
364
  }
365
365
  return result(check, "passed", evidence);
366
366
  }
367
+ async function runBrowserLoginCheck(check, input) {
368
+ const empty = { kind: "browser_login" };
369
+ if (input.browserLogin === undefined) {
370
+ return result(check, "error", empty, "browser_login check requires a browser session");
371
+ }
372
+ try {
373
+ const observed = await input.browserLogin();
374
+ const evidence = {
375
+ kind: "browser_login",
376
+ state: observed.state,
377
+ url: observed.url,
378
+ };
379
+ return observed.logged_in === true
380
+ ? result(check, "passed", evidence)
381
+ : result(check, "failed", evidence, `the Host login check still sees a ${observed.state === "unknown" ? "session it cannot confirm" : "logged-out session"}`);
382
+ }
383
+ catch (error) {
384
+ return result(check, "error", empty, error instanceof Error ? error.message : String(error));
385
+ }
386
+ }
367
387
  async function runCheck(check, input) {
368
388
  switch (check.type) {
369
389
  case "command":
@@ -378,6 +398,8 @@ async function runCheck(check, input) {
378
398
  return runCheckoutChangesCheck(check, input);
379
399
  case "checklist":
380
400
  return runChecklistCheck(check, input);
401
+ case "browser_login":
402
+ return runBrowserLoginCheck(check, input);
381
403
  }
382
404
  }
383
405
  /**
@@ -7,12 +7,17 @@ export type CredentialBinding = {
7
7
  provisional: boolean;
8
8
  };
9
9
  export type ResolveCredentialBindingOptions = {
10
- piHomeAuthPath?: string;
10
+ agentAuthPath?: string;
11
11
  };
12
12
  export declare function sfOwnedAgentDir(): string;
13
13
  export declare function sfOwnedAuthPath(): string;
14
+ export declare const STAGEFLOW_AGENT_AUTH_PATH_ENV = "STAGEFLOW_AGENT_AUTH_PATH";
15
+ /** @deprecated Use STAGEFLOW_AGENT_AUTH_PATH_ENV. */
16
+ export declare const PI_HOME_AUTH_PATH_ENV = "STAGEFLOW_AGENT_AUTH_PATH";
17
+ export declare function stageflowAgentAuthPath(override?: string): string;
18
+ /** @deprecated Use stageflowAgentAuthPath */
14
19
  export declare function piHomeAuthPath(override?: string): string;
15
20
  export declare function isUsableAuthFile(authPath: string): boolean;
16
21
  export declare function ensureSfOwnedAuthStore(): string;
17
- export declare function resolveCredentialBinding(ctx: ProjectContext | string, options?: ResolveCredentialBindingOptions): CredentialBinding;
22
+ export declare function resolveCredentialBinding(ctx: ProjectContext | string, _options?: ResolveCredentialBindingOptions): CredentialBinding;
18
23
  export { readCredentialSourceFromFile, writeCredentialSourceToFile, } from "./settingsFile.js";
@@ -1,5 +1,4 @@
1
1
  import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
3
  import { ensureGlobalHome } from "../project/globalHome.js";
5
4
  import { resolveProjectContext, } from "../project/resolveProjectContext.js";
@@ -11,9 +10,23 @@ export function sfOwnedAgentDir() {
11
10
  export function sfOwnedAuthPath() {
12
11
  return path.join(sfOwnedAgentDir(), "auth.json");
13
12
  }
13
+ export const STAGEFLOW_AGENT_AUTH_PATH_ENV = "STAGEFLOW_AGENT_AUTH_PATH";
14
+ /** @deprecated Use STAGEFLOW_AGENT_AUTH_PATH_ENV. */
15
+ export const PI_HOME_AUTH_PATH_ENV = STAGEFLOW_AGENT_AUTH_PATH_ENV;
16
+ export function stageflowAgentAuthPath(override) {
17
+ if (override !== undefined) {
18
+ return path.resolve(override);
19
+ }
20
+ const fromEnv = process.env[STAGEFLOW_AGENT_AUTH_PATH_ENV]?.trim() ||
21
+ process.env.STAGEFLOW_PI_HOME_AUTH_PATH?.trim();
22
+ if (fromEnv) {
23
+ return path.resolve(fromEnv);
24
+ }
25
+ return sfOwnedAuthPath();
26
+ }
27
+ /** @deprecated Use stageflowAgentAuthPath */
14
28
  export function piHomeAuthPath(override) {
15
- return (override ??
16
- path.join(os.homedir(), ".pi", "agent", "auth.json"));
29
+ return stageflowAgentAuthPath(override);
17
30
  }
18
31
  export function isUsableAuthFile(authPath) {
19
32
  if (!existsSync(authPath))
@@ -40,35 +53,14 @@ export function ensureSfOwnedAuthStore() {
40
53
  }
41
54
  return authPath;
42
55
  }
43
- export function resolveCredentialBinding(ctx, options = {}) {
56
+ export function resolveCredentialBinding(ctx, _options = {}) {
44
57
  const projectCtx = typeof ctx === "string" ? resolveProjectContext(ctx) : ctx;
45
- const piHome = piHomeAuthPath(options.piHomeAuthPath);
46
58
  const persisted = readCredentialSourceFromContext(projectCtx);
47
- if (persisted !== undefined) {
48
- if (persisted === "sf_owned") {
49
- return {
50
- source: "sf_owned",
51
- authPath: ensureSfOwnedAuthStore(),
52
- provisional: false,
53
- };
54
- }
55
- return {
56
- source: "pi_home",
57
- authPath: piHome,
58
- provisional: false,
59
- };
60
- }
61
- if (isUsableAuthFile(piHome)) {
62
- return {
63
- source: "pi_home",
64
- authPath: piHome,
65
- provisional: true,
66
- };
67
- }
59
+ const authPath = ensureSfOwnedAuthStore();
68
60
  return {
69
61
  source: "sf_owned",
70
- authPath: ensureSfOwnedAuthStore(),
71
- provisional: true,
62
+ authPath,
63
+ provisional: persisted === undefined,
72
64
  };
73
65
  }
74
66
  export { readCredentialSourceFromFile, writeCredentialSourceToFile, } from "./settingsFile.js";
@@ -0,0 +1,72 @@
1
+ import type { RunStore } from "../runstore/port.js";
2
+ import type { TriggerFireEvent, TriggerSourcePort } from "./triggerPort.js";
3
+ export declare const DEFAULT_EMAIL_RECONNECT_BASE_DELAY_MS = 1000;
4
+ export declare const DEFAULT_EMAIL_RECONNECT_MAX_DELAY_MS = 30000;
5
+ export declare const EMAIL_TEXT_EXCERPT_MAX_CHARS = 2000;
6
+ export type NormalizedEmailMessage = {
7
+ uid: number;
8
+ from: string;
9
+ subject: string;
10
+ date: string;
11
+ text: string;
12
+ };
13
+ export type EmailClientConfig = {
14
+ host: string;
15
+ port: number;
16
+ user: string;
17
+ password: string;
18
+ };
19
+ /**
20
+ * Narrow, test-friendly seam over "one IMAP/IDLE mailbox connection". The
21
+ * default implementation adapts `imapflow`'s `ImapFlow`; tests inject a fake
22
+ * so no real mailbox is ever contacted.
23
+ */
24
+ export type EmailImapClient = {
25
+ connect(): Promise<{
26
+ uidNext: number;
27
+ }>;
28
+ idle(): void;
29
+ fetchNewMessages(sinceUid: number): Promise<NormalizedEmailMessage[]>;
30
+ close(): Promise<void>;
31
+ onExists(listener: () => void): void;
32
+ onClose(listener: () => void): void;
33
+ onError(listener: (err: Error) => void): void;
34
+ };
35
+ export type EmailSourceOptions = {
36
+ store: RunStore;
37
+ cwd?: string;
38
+ logError?: (message: string) => void;
39
+ env?: NodeJS.ProcessEnv;
40
+ /** Injectable client factory so tests never hit a real mailbox. */
41
+ createEmailClient?: (config: EmailClientConfig) => EmailImapClient;
42
+ reconnectBaseDelayMs?: number;
43
+ reconnectMaxDelayMs?: number;
44
+ };
45
+ /**
46
+ * `TriggerSourcePort` adapter for `event`-kind triggers whose `event.source`
47
+ * starts with `email.`. Unlike the poll-based adapters this one is push-based:
48
+ * on `start()` it groups triggers by distinct mailbox (`host`+`port`+`user`),
49
+ * opens exactly one real IMAP/IDLE connection per mailbox regardless of how
50
+ * many triggers watch it, and reacts to the server's new-message
51
+ * notification instead of ticking on an interval. A dropped connection
52
+ * reconnects with bounded exponential backoff rather than going dark.
53
+ */
54
+ export declare class EmailSource implements TriggerSourcePort {
55
+ private readonly store;
56
+ private readonly cwd;
57
+ private readonly logError;
58
+ private readonly env;
59
+ private readonly createEmailClient;
60
+ private readonly reconnectBaseDelayMs;
61
+ private readonly reconnectMaxDelayMs;
62
+ private readonly connections;
63
+ private stopped;
64
+ constructor(options: EmailSourceOptions);
65
+ start(onFire: (event: TriggerFireEvent) => Promise<void>): Promise<void>;
66
+ stop(): Promise<void>;
67
+ private discover;
68
+ private connectMailbox;
69
+ private scheduleReconnect;
70
+ private processNewMail;
71
+ private fireSafely;
72
+ }
@@ -0,0 +1,304 @@
1
+ import path from "node:path";
2
+ import { ImapFlow } from "imapflow";
3
+ import { getCatalogScanPaths } from "../config/browseCatalog.js";
4
+ import { loadTriggerOutcome } from "../config/loadTrigger.js";
5
+ import { catalogContextFromStageflow } from "../config/resolveCatalogContext.js";
6
+ import { readSecretFromEnvOrFile } from "../config/secretFromEnvOrFile.js";
7
+ import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
8
+ import { matchesEventFilter } from "./triggerEventMatch.js";
9
+ export const DEFAULT_EMAIL_RECONNECT_BASE_DELAY_MS = 1_000;
10
+ export const DEFAULT_EMAIL_RECONNECT_MAX_DELAY_MS = 30_000;
11
+ export const EMAIL_TEXT_EXCERPT_MAX_CHARS = 2_000;
12
+ function defaultCreateEmailClient(config) {
13
+ const client = new ImapFlow({
14
+ host: config.host,
15
+ port: config.port,
16
+ secure: config.port === 993 || config.port === 465,
17
+ auth: { user: config.user, pass: config.password },
18
+ logger: false,
19
+ });
20
+ return {
21
+ async connect() {
22
+ await client.connect();
23
+ const mailbox = await client.mailboxOpen("INBOX");
24
+ return { uidNext: mailbox.uidNext };
25
+ },
26
+ idle() {
27
+ void client.idle().catch(() => {
28
+ // Connection drop surfaces via the 'close'/'error' events below.
29
+ });
30
+ },
31
+ async fetchNewMessages(sinceUid) {
32
+ if (sinceUid <= 0)
33
+ return [];
34
+ const results = [];
35
+ for await (const msg of client.fetch(`${sinceUid + 1}:*`, { uid: true, envelope: true, bodyParts: ["TEXT"] }, { uid: true })) {
36
+ if (msg.uid <= sinceUid)
37
+ continue;
38
+ const fromAddr = msg.envelope?.from?.[0];
39
+ const from = fromAddr?.address ?? fromAddr?.name ?? "";
40
+ const subject = msg.envelope?.subject ?? "";
41
+ const rawDate = msg.envelope?.date;
42
+ const date = rawDate instanceof Date ? rawDate.toISOString() : String(rawDate ?? "");
43
+ const text = msg.bodyParts?.get("TEXT")?.toString("utf8").slice(0, EMAIL_TEXT_EXCERPT_MAX_CHARS) ?? "";
44
+ results.push({ uid: msg.uid, from, subject, date, text });
45
+ }
46
+ return results;
47
+ },
48
+ async close() {
49
+ try {
50
+ await client.logout();
51
+ }
52
+ catch {
53
+ client.close();
54
+ }
55
+ },
56
+ onExists(listener) {
57
+ client.on("exists", () => listener());
58
+ },
59
+ onClose(listener) {
60
+ client.on("close", () => listener());
61
+ },
62
+ onError(listener) {
63
+ client.on("error", (err) => listener(err));
64
+ },
65
+ };
66
+ }
67
+ function parseMailboxConfig(definition) {
68
+ const config = definition.event?.config;
69
+ const host = config?.host;
70
+ const port = config?.port;
71
+ const user = config?.user;
72
+ const secretRef = config?.secretRef;
73
+ if (typeof host !== "string" ||
74
+ host.length === 0 ||
75
+ typeof port !== "number" ||
76
+ !Number.isFinite(port) ||
77
+ typeof user !== "string" ||
78
+ user.length === 0 ||
79
+ typeof secretRef !== "string" ||
80
+ secretRef.length === 0) {
81
+ return undefined;
82
+ }
83
+ return { host, port, user, secretRef };
84
+ }
85
+ function mailboxKeyFor(config) {
86
+ return `email:${config.host}:${config.port}:${config.user}`;
87
+ }
88
+ function errMsg(err) {
89
+ return err instanceof Error ? err.message : String(err);
90
+ }
91
+ /**
92
+ * `TriggerSourcePort` adapter for `event`-kind triggers whose `event.source`
93
+ * starts with `email.`. Unlike the poll-based adapters this one is push-based:
94
+ * on `start()` it groups triggers by distinct mailbox (`host`+`port`+`user`),
95
+ * opens exactly one real IMAP/IDLE connection per mailbox regardless of how
96
+ * many triggers watch it, and reacts to the server's new-message
97
+ * notification instead of ticking on an interval. A dropped connection
98
+ * reconnects with bounded exponential backoff rather than going dark.
99
+ */
100
+ export class EmailSource {
101
+ store;
102
+ cwd;
103
+ logError;
104
+ env;
105
+ createEmailClient;
106
+ reconnectBaseDelayMs;
107
+ reconnectMaxDelayMs;
108
+ connections = new Map();
109
+ stopped = false;
110
+ constructor(options) {
111
+ this.store = options.store;
112
+ this.cwd = options.cwd ?? process.cwd();
113
+ this.logError =
114
+ options.logError ??
115
+ ((message) => {
116
+ console.error(message);
117
+ });
118
+ this.env = options.env ?? process.env;
119
+ this.createEmailClient = options.createEmailClient ?? defaultCreateEmailClient;
120
+ this.reconnectBaseDelayMs = options.reconnectBaseDelayMs ?? DEFAULT_EMAIL_RECONNECT_BASE_DELAY_MS;
121
+ this.reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_EMAIL_RECONNECT_MAX_DELAY_MS;
122
+ }
123
+ async start(onFire) {
124
+ this.stopped = false;
125
+ const ctx = catalogContextFromStageflow(await resolveStageflowContext(this.cwd));
126
+ const scanPaths = await getCatalogScanPaths(ctx);
127
+ if (!scanPaths)
128
+ return;
129
+ const projectRoot = ctx.projectRoot ?? undefined;
130
+ const groups = await this.discover(scanPaths.triggerPaths);
131
+ for (const [mailboxKey, group] of groups) {
132
+ for (const match of group.matches) {
133
+ const definitionRef = projectRoot !== undefined
134
+ ? path.relative(projectRoot, match.path).replace(/\\/g, "/")
135
+ : match.path;
136
+ await this.store.upsertTrigger({
137
+ id: match.definition.id,
138
+ definitionRef,
139
+ enabled: true,
140
+ });
141
+ }
142
+ await this.connectMailbox(mailboxKey, group, onFire);
143
+ }
144
+ }
145
+ async stop() {
146
+ this.stopped = true;
147
+ for (const [mailboxKey, conn] of this.connections) {
148
+ conn.stopping = true;
149
+ if (conn.reconnectTimer !== undefined) {
150
+ clearTimeout(conn.reconnectTimer);
151
+ conn.reconnectTimer = undefined;
152
+ }
153
+ if (conn.client) {
154
+ try {
155
+ await conn.client.close();
156
+ }
157
+ catch (err) {
158
+ this.logError(`email: closing mailbox "${mailboxKey}" failed: ${errMsg(err)}`);
159
+ }
160
+ }
161
+ }
162
+ this.connections.clear();
163
+ }
164
+ async discover(triggerPaths) {
165
+ const groups = new Map();
166
+ for (const triggerPath of triggerPaths) {
167
+ const outcome = await loadTriggerOutcome(triggerPath);
168
+ if (!outcome.ok)
169
+ continue;
170
+ const definition = outcome.value;
171
+ if (definition.kind !== "event" || !definition.enabled)
172
+ continue;
173
+ const source = definition.event?.source;
174
+ if (typeof source !== "string" || !source.startsWith("email."))
175
+ continue;
176
+ const config = parseMailboxConfig(definition);
177
+ if (!config) {
178
+ this.logError(`email: trigger "${definition.id}" has incomplete event.config ` +
179
+ `(expected host, port, user, secretRef); skipping`);
180
+ continue;
181
+ }
182
+ const mailboxKey = mailboxKeyFor(config);
183
+ const group = groups.get(mailboxKey) ?? { config, matches: [] };
184
+ group.matches.push({ path: triggerPath, definition });
185
+ groups.set(mailboxKey, group);
186
+ }
187
+ return groups;
188
+ }
189
+ async connectMailbox(mailboxKey, group, onFire) {
190
+ let conn = this.connections.get(mailboxKey);
191
+ if (!conn) {
192
+ conn = { group, onFire, reconnectAttempt: 0, stopping: false };
193
+ this.connections.set(mailboxKey, conn);
194
+ }
195
+ if (conn.stopping || this.stopped)
196
+ return;
197
+ let password;
198
+ try {
199
+ password = readSecretFromEnvOrFile(this.env, group.config.secretRef);
200
+ }
201
+ catch (err) {
202
+ this.logError(`email: failed to read secret "${group.config.secretRef}" for mailbox "${mailboxKey}": ${errMsg(err)}`);
203
+ return;
204
+ }
205
+ if (password === undefined) {
206
+ this.logError(`email: secret "${group.config.secretRef}" is not set for mailbox "${mailboxKey}"; skipping`);
207
+ return;
208
+ }
209
+ const client = this.createEmailClient({
210
+ host: group.config.host,
211
+ port: group.config.port,
212
+ user: group.config.user,
213
+ password,
214
+ });
215
+ conn.client = client;
216
+ client.onError((err) => {
217
+ this.logError(`email: mailbox "${mailboxKey}" error: ${errMsg(err)}`);
218
+ });
219
+ client.onClose(() => {
220
+ const current = this.connections.get(mailboxKey);
221
+ if (this.stopped || !current || current.stopping)
222
+ return;
223
+ this.scheduleReconnect(mailboxKey);
224
+ });
225
+ client.onExists(() => {
226
+ void this.processNewMail(mailboxKey).catch((err) => {
227
+ this.logError(`email: processing new mail for mailbox "${mailboxKey}" failed: ${errMsg(err)}`);
228
+ });
229
+ });
230
+ try {
231
+ const { uidNext } = await client.connect();
232
+ const existing = await this.store.getTriggerAdapterState(mailboxKey, "uid");
233
+ if (existing === null) {
234
+ await this.store.setTriggerAdapterState(mailboxKey, "uid", String(Math.max(uidNext - 1, 0)));
235
+ }
236
+ client.idle();
237
+ conn.reconnectAttempt = 0;
238
+ }
239
+ catch (err) {
240
+ this.logError(`email: connecting to mailbox "${mailboxKey}" failed: ${errMsg(err)}`);
241
+ this.scheduleReconnect(mailboxKey);
242
+ }
243
+ }
244
+ scheduleReconnect(mailboxKey) {
245
+ const conn = this.connections.get(mailboxKey);
246
+ if (!conn || conn.stopping || this.stopped)
247
+ return;
248
+ const attempt = conn.reconnectAttempt;
249
+ const delay = Math.min(this.reconnectBaseDelayMs * 2 ** attempt, this.reconnectMaxDelayMs);
250
+ conn.reconnectAttempt = attempt + 1;
251
+ conn.reconnectTimer = setTimeout(() => {
252
+ conn.reconnectTimer = undefined;
253
+ void this.connectMailbox(mailboxKey, conn.group, conn.onFire);
254
+ }, delay).unref();
255
+ }
256
+ async processNewMail(mailboxKey) {
257
+ const conn = this.connections.get(mailboxKey);
258
+ if (!conn?.client)
259
+ return;
260
+ const watermarkRaw = await this.store.getTriggerAdapterState(mailboxKey, "uid");
261
+ const sinceUid = watermarkRaw !== null ? Number(watermarkRaw) : 0;
262
+ const messages = await conn.client.fetchNewMessages(sinceUid);
263
+ if (messages.length === 0)
264
+ return;
265
+ let maxUid = sinceUid;
266
+ for (const message of messages) {
267
+ if (message.uid <= sinceUid)
268
+ continue;
269
+ if (message.uid > maxUid)
270
+ maxUid = message.uid;
271
+ const summary = {
272
+ from: message.from,
273
+ subject: message.subject,
274
+ date: message.date,
275
+ text: message.text,
276
+ };
277
+ for (const { definition } of conn.group.matches) {
278
+ if (!matchesEventFilter(definition.event?.match, summary))
279
+ continue;
280
+ await this.fireSafely(conn.onFire, definition, message, summary);
281
+ }
282
+ }
283
+ if (maxUid > sinceUid) {
284
+ await this.store.setTriggerAdapterState(mailboxKey, "uid", String(maxUid));
285
+ }
286
+ }
287
+ async fireSafely(onFire, definition, message, summary) {
288
+ try {
289
+ if (definition.task !== undefined) {
290
+ await onFire({ triggerId: definition.id });
291
+ return;
292
+ }
293
+ const task = {
294
+ id: `${definition.id}-email-${message.uid}`,
295
+ goal: `Handle email: ${message.subject}`,
296
+ input: summary,
297
+ };
298
+ await onFire({ triggerId: definition.id, task });
299
+ }
300
+ catch (err) {
301
+ this.logError(`email: firing trigger "${definition.id}" failed: ${errMsg(err)}`);
302
+ }
303
+ }
304
+ }