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.
- package/README.md +4 -0
- package/builtin-skills/browser/SKILL.md +42 -0
- package/dist/agent/claudeAdapter.js +2 -2
- package/dist/agent/cursorProvider.d.ts +10 -0
- package/dist/agent/cursorProvider.js +83 -8
- package/dist/agent/fakeAgent.d.ts +1 -0
- package/dist/agent/fakeAgent.js +13 -0
- package/dist/agent/piAdapter.d.ts +3 -24
- package/dist/agent/piAdapter.js +49 -47
- package/dist/agent/piSessionFactory.d.ts +38 -0
- package/dist/agent/piSessionFactory.js +60 -0
- package/dist/agent/port.d.ts +2 -0
- package/dist/agent/providerAuth.d.ts +7 -3
- package/dist/agent/providerAuth.js +5 -3
- package/dist/agent/streamLogRedact.d.ts +6 -0
- package/dist/agent/streamLogRedact.js +23 -1
- package/dist/browser/anchor.d.ts +27 -0
- package/dist/browser/anchor.js +127 -0
- package/dist/browser/auditSink.d.ts +38 -0
- package/dist/browser/auditSink.js +34 -0
- package/dist/browser/browserHost.d.ts +86 -0
- package/dist/browser/browserHost.js +7 -0
- package/dist/browser/browserSweep.d.ts +14 -0
- package/dist/browser/browserSweep.js +54 -0
- package/dist/browser/browserTeardown.d.ts +56 -0
- package/dist/browser/browserTeardown.js +235 -0
- package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -0
- package/dist/browser/fakeRemoteBrowserHost.js +34 -0
- package/dist/browser/gateHandoff.d.ts +25 -0
- package/dist/browser/gateHandoff.js +83 -0
- package/dist/browser/humanLogin.d.ts +18 -0
- package/dist/browser/humanLogin.js +34 -0
- package/dist/browser/keyProvider.d.ts +10 -0
- package/dist/browser/keyProvider.js +39 -0
- package/dist/browser/localBrowserHost.d.ts +10 -0
- package/dist/browser/localBrowserHost.js +152 -0
- package/dist/browser/localProfileLock.d.ts +2 -0
- package/dist/browser/localProfileLock.js +218 -0
- package/dist/browser/localProfileStore.d.ts +5 -0
- package/dist/browser/localProfileStore.js +91 -0
- package/dist/browser/loginCheck.d.ts +53 -0
- package/dist/browser/loginCheck.js +147 -0
- package/dist/browser/memoryProfileLock.d.ts +2 -0
- package/dist/browser/memoryProfileLock.js +53 -0
- package/dist/browser/memoryProfileStore.d.ts +2 -0
- package/dist/browser/memoryProfileStore.js +31 -0
- package/dist/browser/navigationAudit.d.ts +16 -0
- package/dist/browser/navigationAudit.js +53 -0
- package/dist/browser/persistedEnv.d.ts +3 -0
- package/dist/browser/persistedEnv.js +15 -0
- package/dist/browser/profileLock.d.ts +40 -0
- package/dist/browser/profileLock.js +10 -0
- package/dist/browser/profileStore.d.ts +23 -0
- package/dist/browser/profileStore.js +39 -0
- package/dist/browser/runLiveness.d.ts +13 -0
- package/dist/browser/runLiveness.js +39 -0
- package/dist/browser/sitePolicy.d.ts +10 -0
- package/dist/browser/sitePolicy.js +61 -0
- package/dist/browser/stageBrowserEnv.d.ts +22 -0
- package/dist/browser/stageBrowserEnv.js +147 -0
- package/dist/browser/stageProfileLock.d.ts +17 -0
- package/dist/browser/stageProfileLock.js +39 -0
- package/dist/cli/browserCommand.d.ts +44 -0
- package/dist/cli/browserCommand.js +472 -0
- package/dist/cli/doctorCommand.js +7 -0
- package/dist/cli/hostClient.d.ts +20 -0
- package/dist/cli/hostClient.js +36 -2
- package/dist/cli/initCommand.d.ts +1 -0
- package/dist/cli/initCommand.js +23 -7
- package/dist/cli/initTemplates.d.ts +5 -2
- package/dist/cli/initTemplates.js +25 -2
- package/dist/cli/providersCommand.d.ts +1 -1
- package/dist/cli/providersCommand.js +4 -4
- package/dist/cli/runsCommand.js +6 -2
- package/dist/cli/triggerCommand.d.ts +12 -0
- package/dist/cli/triggerCommand.js +183 -0
- package/dist/cli/validateCommand.d.ts +1 -1
- package/dist/cli/validateCommand.js +23 -6
- package/dist/cli.js +32 -3
- package/dist/config/browseCatalog.d.ts +1 -0
- package/dist/config/browseCatalog.js +5 -4
- package/dist/config/builtinSkills.d.ts +4 -0
- package/dist/config/builtinSkills.js +23 -0
- package/dist/config/catalogRelativePath.d.ts +1 -0
- package/dist/config/catalogRelativePath.js +10 -2
- package/dist/config/createTrigger.d.ts +27 -0
- package/dist/config/createTrigger.js +232 -0
- package/dist/config/draftPackage.d.ts +117 -0
- package/dist/config/draftPackage.js +550 -0
- package/dist/config/ensureCatalogScanRoot.d.ts +16 -0
- package/dist/config/ensureCatalogScanRoot.js +73 -0
- package/dist/config/hostConfig.d.ts +2 -0
- package/dist/config/hostConfig.js +27 -0
- package/dist/config/listSkills.d.ts +2 -2
- package/dist/config/listSkills.js +17 -3
- package/dist/config/loadOutcome.d.ts +6 -0
- package/dist/config/loadPipeline.js +16 -0
- package/dist/config/loadStage.js +7 -0
- package/dist/config/loadStageflowManifest.js +25 -0
- package/dist/config/loadTrigger.d.ts +9 -0
- package/dist/config/loadTrigger.js +120 -0
- package/dist/config/multiProjectCatalog.d.ts +2 -0
- package/dist/config/multiProjectCatalog.js +26 -18
- package/dist/config/normalizePipelineStageEntry.js +8 -1
- package/dist/config/parseCompletionContract.js +6 -0
- package/dist/config/pipelineStageKeys.js +1 -0
- package/dist/config/publishDraftPackage.d.ts +7 -0
- package/dist/config/publishDraftPackage.js +25 -0
- package/dist/config/resolveCatalogContext.js +1 -1
- package/dist/config/resolvePipelineDag.js +1 -1
- package/dist/config/scanCatalogPaths.d.ts +1 -1
- package/dist/config/scanCatalogPaths.js +6 -2
- package/dist/config/stageBrowser.d.ts +12 -0
- package/dist/config/stageBrowser.js +118 -0
- package/dist/config/triggerCatalog.d.ts +10 -0
- package/dist/config/triggerCatalog.js +37 -0
- package/dist/config/validateCatalog.d.ts +13 -3
- package/dist/config/validateCatalog.js +83 -2
- package/dist/config/validateCompletionContract.js +8 -0
- package/dist/envelope/preEmitChecks.d.ts +1 -1
- package/dist/envelope/preEmitChecks.js +8 -0
- package/dist/logging/redact.d.ts +4 -0
- package/dist/logging/redact.js +17 -0
- package/dist/mcp/catalogTools.js +43 -8
- package/dist/mcp/controlTools.js +3 -1
- package/dist/mcp/deps.d.ts +2 -0
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/tools.js +2 -0
- package/dist/mcp/triggerTools.d.ts +3 -0
- package/dist/mcp/triggerTools.js +176 -0
- package/dist/operatorAgent/docsRetrieval.d.ts +40 -0
- package/dist/operatorAgent/docsRetrieval.js +269 -0
- package/dist/operatorAgent/draftContext.d.ts +16 -0
- package/dist/operatorAgent/draftContext.js +68 -0
- package/dist/operatorAgent/fakeHost.d.ts +39 -0
- package/dist/operatorAgent/fakeHost.js +92 -0
- package/dist/operatorAgent/host.d.ts +40 -0
- package/dist/operatorAgent/host.js +179 -0
- package/dist/operatorAgent/index.d.ts +9 -0
- package/dist/operatorAgent/index.js +8 -0
- package/dist/operatorAgent/piModel.d.ts +80 -0
- package/dist/operatorAgent/piModel.js +528 -0
- package/dist/operatorAgent/profiles/workshopAuthor.d.ts +23 -0
- package/dist/operatorAgent/profiles/workshopAuthor.js +338 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +6 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +103 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +57 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +623 -0
- package/dist/operatorAgent/proposals.d.ts +32 -0
- package/dist/operatorAgent/proposals.js +144 -0
- package/dist/operatorAgent/types.d.ts +128 -0
- package/dist/operatorAgent/types.js +1 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/project/findManifestRoot.d.ts +3 -0
- package/dist/project/findManifestRoot.js +35 -0
- package/dist/project/resolveStageflowContext.d.ts +7 -0
- package/dist/project/resolveStageflowContext.js +44 -28
- package/dist/runstore/assertTmpdir.js +6 -2
- package/dist/runstore/diskUsage.d.ts +1 -1
- package/dist/runstore/diskUsage.js +2 -7
- package/dist/runstore/port.d.ts +28 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +90 -0
- package/dist/runstore/sqlite/migrations/008-triggers-table.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/008-triggers-table.js +19 -0
- package/dist/runstore/sqlite/migrations/009-trigger-next-run.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/009-trigger-next-run.js +19 -0
- package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.js +17 -0
- package/dist/runstore/sqlite/migrations/index.js +6 -0
- package/dist/runtime/answerResume.d.ts +1 -0
- package/dist/runtime/answerResume.js +3 -1
- package/dist/runtime/completionCheckRunner.d.ts +12 -1
- package/dist/runtime/completionCheckRunner.js +22 -0
- package/dist/runtime/credentialBinding.d.ts +7 -2
- package/dist/runtime/credentialBinding.js +20 -28
- package/dist/runtime/emailSource.d.ts +72 -0
- package/dist/runtime/emailSource.js +304 -0
- package/dist/runtime/githubPollSource.d.ts +86 -0
- package/dist/runtime/githubPollSource.js +284 -0
- package/dist/runtime/pipelineRunner.d.ts +4 -0
- package/dist/runtime/pipelineRunner.js +3 -0
- package/dist/runtime/pipelineScheduler.d.ts +2 -0
- package/dist/runtime/pipelineScheduler.js +86 -1
- package/dist/runtime/repositoryMaterialize.d.ts +1 -1
- package/dist/runtime/runManager.d.ts +11 -0
- package/dist/runtime/runManager.js +161 -3
- package/dist/runtime/runRetryCoordinator.d.ts +2 -0
- package/dist/runtime/runRetryCoordinator.js +1 -0
- package/dist/runtime/scheduleSource.d.ts +38 -0
- package/dist/runtime/scheduleSource.js +119 -0
- package/dist/runtime/settingsFile.d.ts +7 -2
- package/dist/runtime/settingsFile.js +66 -56
- package/dist/runtime/stageAttemptBootstrap.js +55 -3
- package/dist/runtime/stageEnvironment.js +1 -0
- package/dist/runtime/stageHitl.d.ts +1 -0
- package/dist/runtime/stageHitl.js +5 -1
- package/dist/runtime/stageProcessLauncher.d.ts +3 -0
- package/dist/runtime/stageProcessLauncher.js +52 -5
- package/dist/runtime/stageRunner.d.ts +5 -0
- package/dist/runtime/stageRunner.js +30 -6
- package/dist/runtime/stageStreamLog.d.ts +1 -0
- package/dist/runtime/stageStreamLog.js +2 -1
- package/dist/runtime/triggerCatalog.d.ts +6 -0
- package/dist/runtime/triggerCatalog.js +24 -0
- package/dist/runtime/triggerEventMatch.d.ts +2 -0
- package/dist/runtime/triggerEventMatch.js +6 -0
- package/dist/runtime/triggerPort.d.ts +14 -0
- package/dist/runtime/triggerPort.js +1 -0
- package/dist/runtime/triggerRunner.d.ts +17 -0
- package/dist/runtime/triggerRunner.js +89 -0
- package/dist/runtime/verifiedStageExecution.d.ts +7 -0
- package/dist/runtime/verifiedStageExecution.js +14 -0
- package/dist/runtime/webhookSignature.d.ts +1 -0
- package/dist/runtime/webhookSignature.js +12 -0
- package/dist/server/bootstrap.d.ts +6 -0
- package/dist/server/bootstrap.js +70 -0
- package/dist/server/controlToken.js +13 -0
- package/dist/server/createHttpHost.js +3 -0
- package/dist/server/healthSurfaces.js +31 -6
- package/dist/server/http.d.ts +15 -0
- package/dist/server/http.js +1251 -74
- package/dist/server/requestAuthContext.d.ts +1 -1
- package/dist/server/triggerRoutes.d.ts +15 -0
- package/dist/server/triggerRoutes.js +268 -0
- package/dist/tools/askOperator.d.ts +3 -2
- package/dist/types/completion.d.ts +3 -0
- package/dist/types/pipeline.d.ts +3 -1
- package/dist/types/preEmitCheck.d.ts +7 -0
- package/dist/types/stage.d.ts +14 -0
- package/dist/types/stageflowManifest.d.ts +3 -0
- package/dist/types/trigger.d.ts +18 -0
- package/dist/types/trigger.js +1 -0
- package/dist/ui/assets/{index-CSsDued1.css → index-Bv4p1aPK.css} +1 -1
- package/dist/ui/assets/index-p8Iou3zk.js +133 -0
- package/dist/ui/index.html +2 -2
- package/dist/workshop/autosave.d.ts +58 -0
- package/dist/workshop/autosave.js +222 -0
- package/dist/workshop/buildStore.d.ts +79 -0
- package/dist/workshop/buildStore.js +326 -0
- package/dist/workshop/chatTurn.d.ts +232 -0
- package/dist/workshop/chatTurn.js +475 -0
- package/dist/workshop/modelSettings.d.ts +18 -0
- package/dist/workshop/modelSettings.js +44 -0
- package/dist/workshop/sessionStore.d.ts +61 -0
- package/dist/workshop/sessionStore.js +254 -0
- package/dist/workshop/workshopPin.d.ts +72 -0
- package/dist/workshop/workshopPin.js +193 -0
- package/examples/README.md +2 -1
- package/examples/browser-session/README.md +77 -0
- package/examples/browser-session/browser-session.pipeline.yaml +50 -0
- package/examples/browser-session/browser-session.task.yaml +6 -0
- package/examples/browser-session/check-login.yaml +22 -0
- package/examples/browser-session/fixture-server.mjs +64 -0
- package/examples/browser-session/human-login.yaml +31 -0
- package/examples/browser-session/merge.yaml +36 -0
- package/examples/browser-session/work-a.yaml +41 -0
- package/examples/browser-session/work-b.yaml +41 -0
- package/examples/conditional-fork/decide.yaml +1 -1
- package/examples/github-release/README.md +17 -3
- package/examples/stage-mcp/README.md +3 -2
- package/examples/stageflow.yaml +51 -0
- package/package.json +9 -4
- package/skills/stageflow/references/control-surface.md +3 -1
- package/skills/stageflow-author/references/stage-prompt-template.md +4 -4
- package/skills/stageflow-run/SKILL.md +2 -0
- package/skills/stageflow-run/references/task-and-pipeline-selection.md +3 -1
- package/skills/stageflow-setup/SKILL.md +11 -0
- package/dist/ui/assets/index-B6hlSXXn.js +0 -120
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const DEFAULT_WORKSHOP_MODEL = "cursor/auto";
|
|
2
|
+
export type ResolveWorkshopModelInput = {
|
|
3
|
+
sessionOverride?: string | null;
|
|
4
|
+
settingsDefault?: string | null;
|
|
5
|
+
profileDefault?: string | null;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Effective Workshop chat model: session override → Settings default → profile fallback.
|
|
9
|
+
* Credentials stay on the shared Stageflow provider auth stack; this only picks a model id.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveWorkshopModel(input?: ResolveWorkshopModelInput): string;
|
|
12
|
+
export declare function parseWorkshopModel(value: unknown): string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Model selected when Workshop chat opens.
|
|
15
|
+
* stageflow.yaml `model` wins; a saved Settings workshop model is used only
|
|
16
|
+
* when the manifest has none.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readWorkshopOpenModel(cwd: string): Promise<string>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { globalModelFromManifest } from "../config/resolveModel.js";
|
|
2
|
+
import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
|
|
3
|
+
import { readFactorySettings } from "../runtime/settingsFile.js";
|
|
4
|
+
export const DEFAULT_WORKSHOP_MODEL = "cursor/auto";
|
|
5
|
+
/**
|
|
6
|
+
* Effective Workshop chat model: session override → Settings default → profile fallback.
|
|
7
|
+
* Credentials stay on the shared Stageflow provider auth stack; this only picks a model id.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveWorkshopModel(input = {}) {
|
|
10
|
+
const session = normalizeModelId(input.sessionOverride);
|
|
11
|
+
if (session)
|
|
12
|
+
return session;
|
|
13
|
+
const settings = normalizeModelId(input.settingsDefault);
|
|
14
|
+
if (settings)
|
|
15
|
+
return settings;
|
|
16
|
+
const profile = normalizeModelId(input.profileDefault);
|
|
17
|
+
if (profile)
|
|
18
|
+
return profile;
|
|
19
|
+
return DEFAULT_WORKSHOP_MODEL;
|
|
20
|
+
}
|
|
21
|
+
export function parseWorkshopModel(value) {
|
|
22
|
+
if (typeof value !== "string")
|
|
23
|
+
return undefined;
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Model selected when Workshop chat opens.
|
|
29
|
+
* stageflow.yaml `model` wins; a saved Settings workshop model is used only
|
|
30
|
+
* when the manifest has none.
|
|
31
|
+
*/
|
|
32
|
+
export async function readWorkshopOpenModel(cwd) {
|
|
33
|
+
const ctx = await resolveStageflowContext(cwd);
|
|
34
|
+
return resolveWorkshopModel({
|
|
35
|
+
settingsDefault: globalModelFromManifest(ctx.manifest) ??
|
|
36
|
+
readFactorySettings(cwd).workshopModel,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function normalizeModelId(value) {
|
|
40
|
+
if (typeof value !== "string")
|
|
41
|
+
return undefined;
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
44
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export declare const WORKSHOP_SESSION_TITLE_MAX_LENGTH = 80;
|
|
2
|
+
export type WorkshopSessionMessageRole = "assistant" | "user" | "system";
|
|
3
|
+
export type WorkshopSessionMessage = {
|
|
4
|
+
id: string;
|
|
5
|
+
role: WorkshopSessionMessageRole;
|
|
6
|
+
text: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
};
|
|
9
|
+
export type WorkshopSessionRecord = {
|
|
10
|
+
version: 1;
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
transcript: WorkshopSessionMessage[];
|
|
16
|
+
piSessionId: string | null;
|
|
17
|
+
activeBuildId?: string;
|
|
18
|
+
};
|
|
19
|
+
export type WorkshopSessionSummary = {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
activeBuildId?: string;
|
|
25
|
+
};
|
|
26
|
+
export type WorkshopSessionAppendMessage = {
|
|
27
|
+
id?: string;
|
|
28
|
+
role: WorkshopSessionMessageRole;
|
|
29
|
+
text: string;
|
|
30
|
+
createdAt?: string;
|
|
31
|
+
};
|
|
32
|
+
export type WorkshopSessionStoreErrorCode = "workshop_session_not_found";
|
|
33
|
+
export declare class WorkshopSessionStoreError extends Error {
|
|
34
|
+
readonly code: WorkshopSessionStoreErrorCode;
|
|
35
|
+
readonly sessionId?: string;
|
|
36
|
+
constructor(message: string, code: WorkshopSessionStoreErrorCode, sessionId?: string);
|
|
37
|
+
}
|
|
38
|
+
export declare function resolveWorkshopSessionStoreRoot(): string;
|
|
39
|
+
export declare function workshopSessionsDir(storeRoot: string): string;
|
|
40
|
+
export declare function workshopSessionDir(storeRoot: string, sessionId: string): string;
|
|
41
|
+
export declare function workshopSessionFilePath(storeRoot: string, sessionId: string): string;
|
|
42
|
+
export declare function workshopSessionPiFilePath(storeRoot: string, sessionId: string): string;
|
|
43
|
+
export declare function truncateWorkshopSessionTitle(text: string): string;
|
|
44
|
+
export declare function parseWorkshopSessionRecord(value: unknown): WorkshopSessionRecord | null;
|
|
45
|
+
export declare function readWorkshopSession(storeRoot: string, sessionId: string): WorkshopSessionRecord | null;
|
|
46
|
+
export declare function createWorkshopSession(storeRoot: string, options?: {
|
|
47
|
+
id?: string;
|
|
48
|
+
now?: Date;
|
|
49
|
+
piSessionId?: string | null;
|
|
50
|
+
}): WorkshopSessionRecord;
|
|
51
|
+
export declare function getWorkshopSession(storeRoot: string, sessionId: string): WorkshopSessionRecord;
|
|
52
|
+
export declare function listWorkshopSessions(storeRoot: string): WorkshopSessionSummary[];
|
|
53
|
+
export declare function appendWorkshopSessionMessages(storeRoot: string, sessionId: string, messages: readonly WorkshopSessionAppendMessage[], options?: {
|
|
54
|
+
now?: Date;
|
|
55
|
+
}): WorkshopSessionRecord;
|
|
56
|
+
export declare function updateWorkshopSessionPiSessionId(storeRoot: string, sessionId: string, piSessionId: string | null, options?: {
|
|
57
|
+
now?: Date;
|
|
58
|
+
}): WorkshopSessionRecord;
|
|
59
|
+
export declare function updateWorkshopSessionActiveBuildId(storeRoot: string, sessionId: string, activeBuildId: string | null, options?: {
|
|
60
|
+
now?: Date;
|
|
61
|
+
}): WorkshopSessionRecord;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
5
|
+
export const WORKSHOP_SESSION_TITLE_MAX_LENGTH = 80;
|
|
6
|
+
export class WorkshopSessionStoreError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
sessionId;
|
|
9
|
+
constructor(message, code, sessionId) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "WorkshopSessionStoreError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
if (sessionId !== undefined)
|
|
14
|
+
this.sessionId = sessionId;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function resolveWorkshopSessionStoreRoot() {
|
|
18
|
+
return globalStageflowHome();
|
|
19
|
+
}
|
|
20
|
+
export function workshopSessionsDir(storeRoot) {
|
|
21
|
+
return path.join(storeRoot, "workshop", "sessions");
|
|
22
|
+
}
|
|
23
|
+
export function workshopSessionDir(storeRoot, sessionId) {
|
|
24
|
+
return path.join(workshopSessionsDir(storeRoot), guardSessionId(sessionId));
|
|
25
|
+
}
|
|
26
|
+
export function workshopSessionFilePath(storeRoot, sessionId) {
|
|
27
|
+
return path.join(workshopSessionDir(storeRoot, sessionId), "session.json");
|
|
28
|
+
}
|
|
29
|
+
export function workshopSessionPiFilePath(storeRoot, sessionId) {
|
|
30
|
+
return path.join(workshopSessionDir(storeRoot, sessionId), "pi-session.jsonl");
|
|
31
|
+
}
|
|
32
|
+
function guardSessionId(sessionId) {
|
|
33
|
+
const trimmed = sessionId.trim();
|
|
34
|
+
if (!trimmed ||
|
|
35
|
+
trimmed.includes("/") ||
|
|
36
|
+
trimmed.includes("\\") ||
|
|
37
|
+
trimmed.includes("..") ||
|
|
38
|
+
trimmed === ".") {
|
|
39
|
+
throw new Error(`invalid workshop session id: ${sessionId}`);
|
|
40
|
+
}
|
|
41
|
+
return trimmed;
|
|
42
|
+
}
|
|
43
|
+
function isPlainObject(value) {
|
|
44
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
export function truncateWorkshopSessionTitle(text) {
|
|
47
|
+
const collapsed = text.trim().replace(/\s+/g, " ");
|
|
48
|
+
if (collapsed.length <= WORKSHOP_SESSION_TITLE_MAX_LENGTH)
|
|
49
|
+
return collapsed;
|
|
50
|
+
return collapsed.slice(0, WORKSHOP_SESSION_TITLE_MAX_LENGTH).trimEnd();
|
|
51
|
+
}
|
|
52
|
+
function parseMessage(value) {
|
|
53
|
+
if (!isPlainObject(value))
|
|
54
|
+
return null;
|
|
55
|
+
if (typeof value.id !== "string" || !value.id)
|
|
56
|
+
return null;
|
|
57
|
+
if (value.role !== "assistant" &&
|
|
58
|
+
value.role !== "user" &&
|
|
59
|
+
value.role !== "system") {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value.text !== "string")
|
|
63
|
+
return null;
|
|
64
|
+
if (typeof value.createdAt !== "string" || !value.createdAt)
|
|
65
|
+
return null;
|
|
66
|
+
return {
|
|
67
|
+
id: value.id,
|
|
68
|
+
role: value.role,
|
|
69
|
+
text: value.text,
|
|
70
|
+
createdAt: value.createdAt,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function parseWorkshopSessionRecord(value) {
|
|
74
|
+
if (!isPlainObject(value))
|
|
75
|
+
return null;
|
|
76
|
+
if (value.version !== 1)
|
|
77
|
+
return null;
|
|
78
|
+
if (typeof value.id !== "string" || !value.id)
|
|
79
|
+
return null;
|
|
80
|
+
if (typeof value.title !== "string")
|
|
81
|
+
return null;
|
|
82
|
+
if (typeof value.createdAt !== "string" || !value.createdAt)
|
|
83
|
+
return null;
|
|
84
|
+
if (typeof value.updatedAt !== "string" || !value.updatedAt)
|
|
85
|
+
return null;
|
|
86
|
+
if (!Array.isArray(value.transcript))
|
|
87
|
+
return null;
|
|
88
|
+
if (value.piSessionId !== null && typeof value.piSessionId !== "string") {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (value.activeBuildId !== undefined &&
|
|
92
|
+
value.activeBuildId !== null &&
|
|
93
|
+
typeof value.activeBuildId !== "string") {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const transcript = [];
|
|
97
|
+
for (const entry of value.transcript) {
|
|
98
|
+
const msg = parseMessage(entry);
|
|
99
|
+
if (!msg)
|
|
100
|
+
return null;
|
|
101
|
+
transcript.push(msg);
|
|
102
|
+
}
|
|
103
|
+
const activeBuildId = typeof value.activeBuildId === "string" ? value.activeBuildId.trim() : "";
|
|
104
|
+
return {
|
|
105
|
+
version: 1,
|
|
106
|
+
id: value.id,
|
|
107
|
+
title: value.title,
|
|
108
|
+
createdAt: value.createdAt,
|
|
109
|
+
updatedAt: value.updatedAt,
|
|
110
|
+
transcript,
|
|
111
|
+
piSessionId: value.piSessionId,
|
|
112
|
+
...(activeBuildId ? { activeBuildId } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function writeSession(storeRoot, record) {
|
|
116
|
+
const dir = workshopSessionDir(storeRoot, record.id);
|
|
117
|
+
mkdirSync(dir, { recursive: true });
|
|
118
|
+
const payload = {
|
|
119
|
+
version: 1,
|
|
120
|
+
id: record.id,
|
|
121
|
+
title: record.title,
|
|
122
|
+
createdAt: record.createdAt,
|
|
123
|
+
updatedAt: record.updatedAt,
|
|
124
|
+
transcript: record.transcript,
|
|
125
|
+
piSessionId: record.piSessionId,
|
|
126
|
+
...(record.activeBuildId ? { activeBuildId: record.activeBuildId } : {}),
|
|
127
|
+
};
|
|
128
|
+
writeFileSync(workshopSessionFilePath(storeRoot, record.id), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
129
|
+
}
|
|
130
|
+
export function readWorkshopSession(storeRoot, sessionId) {
|
|
131
|
+
let id;
|
|
132
|
+
try {
|
|
133
|
+
id = guardSessionId(sessionId);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const filePath = workshopSessionFilePath(storeRoot, id);
|
|
139
|
+
if (!existsSync(filePath))
|
|
140
|
+
return null;
|
|
141
|
+
try {
|
|
142
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
143
|
+
const parsed = parseWorkshopSessionRecord(raw);
|
|
144
|
+
if (!parsed || parsed.id !== id)
|
|
145
|
+
return null;
|
|
146
|
+
return parsed;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export function createWorkshopSession(storeRoot, options) {
|
|
153
|
+
const id = options?.id?.trim() || randomUUID();
|
|
154
|
+
guardSessionId(id);
|
|
155
|
+
if (readWorkshopSession(storeRoot, id)) {
|
|
156
|
+
throw new Error(`workshop session already exists: ${id}`);
|
|
157
|
+
}
|
|
158
|
+
const now = (options?.now ?? new Date()).toISOString();
|
|
159
|
+
const record = {
|
|
160
|
+
version: 1,
|
|
161
|
+
id,
|
|
162
|
+
title: "",
|
|
163
|
+
createdAt: now,
|
|
164
|
+
updatedAt: now,
|
|
165
|
+
transcript: [],
|
|
166
|
+
piSessionId: options?.piSessionId ?? null,
|
|
167
|
+
};
|
|
168
|
+
writeSession(storeRoot, record);
|
|
169
|
+
return record;
|
|
170
|
+
}
|
|
171
|
+
export function getWorkshopSession(storeRoot, sessionId) {
|
|
172
|
+
const record = readWorkshopSession(storeRoot, sessionId);
|
|
173
|
+
if (!record) {
|
|
174
|
+
throw new WorkshopSessionStoreError(`workshop_session_not_found: ${sessionId}`, "workshop_session_not_found", sessionId);
|
|
175
|
+
}
|
|
176
|
+
return record;
|
|
177
|
+
}
|
|
178
|
+
export function listWorkshopSessions(storeRoot) {
|
|
179
|
+
const dir = workshopSessionsDir(storeRoot);
|
|
180
|
+
if (!existsSync(dir))
|
|
181
|
+
return [];
|
|
182
|
+
const summaries = [];
|
|
183
|
+
for (const name of readdirSync(dir)) {
|
|
184
|
+
const record = readWorkshopSession(storeRoot, name);
|
|
185
|
+
if (!record)
|
|
186
|
+
continue;
|
|
187
|
+
summaries.push({
|
|
188
|
+
id: record.id,
|
|
189
|
+
title: record.title,
|
|
190
|
+
createdAt: record.createdAt,
|
|
191
|
+
updatedAt: record.updatedAt,
|
|
192
|
+
...(record.activeBuildId ? { activeBuildId: record.activeBuildId } : {}),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
summaries.sort((a, b) => {
|
|
196
|
+
if (a.updatedAt !== b.updatedAt) {
|
|
197
|
+
return a.updatedAt < b.updatedAt ? 1 : -1;
|
|
198
|
+
}
|
|
199
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
200
|
+
});
|
|
201
|
+
return summaries;
|
|
202
|
+
}
|
|
203
|
+
export function appendWorkshopSessionMessages(storeRoot, sessionId, messages, options) {
|
|
204
|
+
const record = getWorkshopSession(storeRoot, sessionId);
|
|
205
|
+
const now = (options?.now ?? new Date()).toISOString();
|
|
206
|
+
let title = record.title;
|
|
207
|
+
const appended = [];
|
|
208
|
+
for (const msg of messages) {
|
|
209
|
+
const entry = {
|
|
210
|
+
id: msg.id?.trim() || randomUUID(),
|
|
211
|
+
role: msg.role,
|
|
212
|
+
text: msg.text,
|
|
213
|
+
createdAt: msg.createdAt || now,
|
|
214
|
+
};
|
|
215
|
+
appended.push(entry);
|
|
216
|
+
if (!title && entry.role === "user" && entry.text.trim()) {
|
|
217
|
+
title = truncateWorkshopSessionTitle(entry.text);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const next = {
|
|
221
|
+
...record,
|
|
222
|
+
title,
|
|
223
|
+
updatedAt: now,
|
|
224
|
+
transcript: [...record.transcript, ...appended],
|
|
225
|
+
};
|
|
226
|
+
writeSession(storeRoot, next);
|
|
227
|
+
return next;
|
|
228
|
+
}
|
|
229
|
+
export function updateWorkshopSessionPiSessionId(storeRoot, sessionId, piSessionId, options) {
|
|
230
|
+
const record = getWorkshopSession(storeRoot, sessionId);
|
|
231
|
+
const next = {
|
|
232
|
+
...record,
|
|
233
|
+
piSessionId,
|
|
234
|
+
updatedAt: (options?.now ?? new Date()).toISOString(),
|
|
235
|
+
};
|
|
236
|
+
writeSession(storeRoot, next);
|
|
237
|
+
return next;
|
|
238
|
+
}
|
|
239
|
+
export function updateWorkshopSessionActiveBuildId(storeRoot, sessionId, activeBuildId, options) {
|
|
240
|
+
const record = getWorkshopSession(storeRoot, sessionId);
|
|
241
|
+
const trimmed = activeBuildId?.trim() ?? "";
|
|
242
|
+
const next = {
|
|
243
|
+
...record,
|
|
244
|
+
updatedAt: (options?.now ?? new Date()).toISOString(),
|
|
245
|
+
};
|
|
246
|
+
if (trimmed) {
|
|
247
|
+
next.activeBuildId = trimmed;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
delete next.activeBuildId;
|
|
251
|
+
}
|
|
252
|
+
writeSession(storeRoot, next);
|
|
253
|
+
return next;
|
|
254
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { DraftPackage } from "../config/draftPackage.js";
|
|
2
|
+
import type { OperatorAgentToolResult, WorkshopDraftContext } from "../operatorAgent/types.js";
|
|
3
|
+
import { type WorkshopBuildRecord } from "./buildStore.js";
|
|
4
|
+
export declare const WORKSHOP_UNLINKED_DRAFT_TOOL_ERROR = "No build is selected \u2014 draft tools cannot edit or save until a build is focused.";
|
|
5
|
+
export type WorkshopPointerChange = {
|
|
6
|
+
buildId: string;
|
|
7
|
+
draft: DraftPackage;
|
|
8
|
+
};
|
|
9
|
+
export type WorkshopPinRegistry = {
|
|
10
|
+
get(sessionId: string): {
|
|
11
|
+
getContext(): unknown;
|
|
12
|
+
setContext(next: unknown): void;
|
|
13
|
+
} | undefined;
|
|
14
|
+
getPinnedBuildId(sessionId: string): string | null;
|
|
15
|
+
setPinnedBuildId(sessionId: string, buildId: string | null): void;
|
|
16
|
+
getLiveTurn(sessionId: string): {
|
|
17
|
+
onPointerChange?: (frame: WorkshopPointerChange) => void;
|
|
18
|
+
} | undefined;
|
|
19
|
+
};
|
|
20
|
+
export declare function enqueueSessionWrite<T>(sessionId: string, fn: () => T): Promise<T>;
|
|
21
|
+
export declare function registerLiveWorkshopSession(sessionId: string, live: {
|
|
22
|
+
storeRoot: string;
|
|
23
|
+
registry: WorkshopPinRegistry;
|
|
24
|
+
}): void;
|
|
25
|
+
export declare function unregisterLiveWorkshopSession(sessionId: string): void;
|
|
26
|
+
export declare function resetWorkshopPinStateForTests(): void;
|
|
27
|
+
export declare function bindPinnedBuildContext(current: unknown, build: WorkshopBuildRecord, sessionId: string): WorkshopDraftContext;
|
|
28
|
+
export declare function resolveLiveWorkshopBinding(context: unknown): {
|
|
29
|
+
sessionId: string;
|
|
30
|
+
registry: WorkshopPinRegistry;
|
|
31
|
+
storeRoot: string;
|
|
32
|
+
} | null;
|
|
33
|
+
/**
|
|
34
|
+
* Draft tools call this. Outside a workshop chat turn it does nothing.
|
|
35
|
+
* An unlinked chat turn fails the tool and does not create a build.
|
|
36
|
+
*/
|
|
37
|
+
export declare function rejectUnlinkedWorkshopDraftTool(context: unknown, toolName: string): OperatorAgentToolResult | null;
|
|
38
|
+
/** Tied builds save to the stored path. Untitled builds keep the caller's destination. */
|
|
39
|
+
export declare function resolvePinnedSaveDestination(context: unknown): {
|
|
40
|
+
directory: string;
|
|
41
|
+
pipelineFilename: string;
|
|
42
|
+
} | null;
|
|
43
|
+
/** A successful save of an untitled build records the operator destination on that id. */
|
|
44
|
+
export declare function recordPinnedWorkshopSave(context: unknown, saved: {
|
|
45
|
+
directory: string;
|
|
46
|
+
pipelineFilename?: string;
|
|
47
|
+
projectRoot: string;
|
|
48
|
+
draft: DraftPackage;
|
|
49
|
+
}): void;
|
|
50
|
+
/**
|
|
51
|
+
* Create an untitled build and move this turn's pin onto it.
|
|
52
|
+
* Later edits in the turn persist on that id.
|
|
53
|
+
*/
|
|
54
|
+
export declare function pinWorkshopBuildOnCreate(input: {
|
|
55
|
+
sessionId: string;
|
|
56
|
+
draft: DraftPackage;
|
|
57
|
+
registry: WorkshopPinRegistry;
|
|
58
|
+
storeRoot: string;
|
|
59
|
+
}): WorkshopBuildRecord;
|
|
60
|
+
/**
|
|
61
|
+
* Focus a build. When this turn already has a pin, update the pointer only.
|
|
62
|
+
* When the turn started with none, move the pin and the host onto that build.
|
|
63
|
+
*/
|
|
64
|
+
export declare function focusWorkshopBuildPointer(input: {
|
|
65
|
+
sessionId: string;
|
|
66
|
+
buildId: string;
|
|
67
|
+
registry: WorkshopPinRegistry;
|
|
68
|
+
storeRoot: string;
|
|
69
|
+
}): {
|
|
70
|
+
pinMoved: boolean;
|
|
71
|
+
activeBuildId: string;
|
|
72
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { isWorkshopDraftContext } from "../operatorAgent/draftContext.js";
|
|
3
|
+
import { createWorkshopBuild, getWorkshopBuild, readWorkshopBuild, updateWorkshopBuild, } from "./buildStore.js";
|
|
4
|
+
import { updateWorkshopSessionActiveBuildId } from "./sessionStore.js";
|
|
5
|
+
export const WORKSHOP_UNLINKED_DRAFT_TOOL_ERROR = "No build is selected — draft tools cannot edit or save until a build is focused.";
|
|
6
|
+
const WORKSHOP_DRAFT_TOOL_NAMES = new Set([
|
|
7
|
+
"read_draft",
|
|
8
|
+
"validate_draft",
|
|
9
|
+
"create_pipeline",
|
|
10
|
+
"edit_pipeline",
|
|
11
|
+
"create_stage",
|
|
12
|
+
"edit_stage",
|
|
13
|
+
"create_task",
|
|
14
|
+
"edit_task",
|
|
15
|
+
"save",
|
|
16
|
+
"propose_draft",
|
|
17
|
+
]);
|
|
18
|
+
const sessionWriteTails = new Map();
|
|
19
|
+
const liveBySession = new Map();
|
|
20
|
+
export function enqueueSessionWrite(sessionId, fn) {
|
|
21
|
+
const prev = sessionWriteTails.get(sessionId) ?? Promise.resolve();
|
|
22
|
+
const run = prev.then(fn, fn);
|
|
23
|
+
sessionWriteTails.set(sessionId, run.then(() => undefined, () => undefined));
|
|
24
|
+
return run;
|
|
25
|
+
}
|
|
26
|
+
export function registerLiveWorkshopSession(sessionId, live) {
|
|
27
|
+
liveBySession.set(sessionId, live);
|
|
28
|
+
}
|
|
29
|
+
export function unregisterLiveWorkshopSession(sessionId) {
|
|
30
|
+
liveBySession.delete(sessionId);
|
|
31
|
+
}
|
|
32
|
+
export function resetWorkshopPinStateForTests() {
|
|
33
|
+
sessionWriteTails.clear();
|
|
34
|
+
liveBySession.clear();
|
|
35
|
+
}
|
|
36
|
+
function destinationFromRelativePath(relativePath) {
|
|
37
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
38
|
+
const pipelineFilename = path.posix.basename(normalized);
|
|
39
|
+
const dir = path.posix.dirname(normalized);
|
|
40
|
+
return {
|
|
41
|
+
directory: dir === "." ? "." : dir,
|
|
42
|
+
pipelineFilename,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function relativePathFromDestination(directory, pipelineFilename) {
|
|
46
|
+
const dir = directory.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
47
|
+
const file = path.posix.basename(pipelineFilename.trim().replace(/\\/g, "/"));
|
|
48
|
+
if (!dir || dir === ".")
|
|
49
|
+
return file;
|
|
50
|
+
return `${dir.replace(/\/+$/, "")}/${file}`;
|
|
51
|
+
}
|
|
52
|
+
export function bindPinnedBuildContext(current, build, sessionId) {
|
|
53
|
+
const base = isWorkshopDraftContext(current)
|
|
54
|
+
? { ...current }
|
|
55
|
+
: { draft: build.draft };
|
|
56
|
+
const next = {
|
|
57
|
+
...base,
|
|
58
|
+
draft: build.draft,
|
|
59
|
+
chatSessionId: sessionId,
|
|
60
|
+
buildId: build.id,
|
|
61
|
+
};
|
|
62
|
+
if (build.projectRoot)
|
|
63
|
+
next.projectRoot = build.projectRoot;
|
|
64
|
+
else
|
|
65
|
+
delete next.projectRoot;
|
|
66
|
+
if (build.projectRoot && build.relativePath) {
|
|
67
|
+
next.destination = destinationFromRelativePath(build.relativePath);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
delete next.destination;
|
|
71
|
+
}
|
|
72
|
+
return next;
|
|
73
|
+
}
|
|
74
|
+
function emitPointerChange(registry, sessionId, build) {
|
|
75
|
+
registry.getLiveTurn(sessionId)?.onPointerChange?.({
|
|
76
|
+
buildId: build.id,
|
|
77
|
+
draft: build.draft,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function movePinOntoBuild(registry, sessionId, build, storeRoot) {
|
|
81
|
+
const session = registry.get(sessionId);
|
|
82
|
+
if (session) {
|
|
83
|
+
session.setContext(bindPinnedBuildContext(session.getContext(), build, sessionId));
|
|
84
|
+
}
|
|
85
|
+
registry.setPinnedBuildId(sessionId, build.id);
|
|
86
|
+
enqueueSessionWrite(sessionId, () => {
|
|
87
|
+
updateWorkshopSessionActiveBuildId(storeRoot, sessionId, build.id);
|
|
88
|
+
});
|
|
89
|
+
emitPointerChange(registry, sessionId, build);
|
|
90
|
+
}
|
|
91
|
+
export function resolveLiveWorkshopBinding(context) {
|
|
92
|
+
if (!isWorkshopDraftContext(context) || !context.chatSessionId)
|
|
93
|
+
return null;
|
|
94
|
+
const live = liveBySession.get(context.chatSessionId);
|
|
95
|
+
if (!live)
|
|
96
|
+
return null;
|
|
97
|
+
return {
|
|
98
|
+
sessionId: context.chatSessionId,
|
|
99
|
+
registry: live.registry,
|
|
100
|
+
storeRoot: live.storeRoot,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Draft tools call this. Outside a workshop chat turn it does nothing.
|
|
105
|
+
* An unlinked chat turn fails the tool and does not create a build.
|
|
106
|
+
*/
|
|
107
|
+
export function rejectUnlinkedWorkshopDraftTool(context, toolName) {
|
|
108
|
+
if (!WORKSHOP_DRAFT_TOOL_NAMES.has(toolName))
|
|
109
|
+
return null;
|
|
110
|
+
if (!isWorkshopDraftContext(context) || !context.chatSessionId)
|
|
111
|
+
return null;
|
|
112
|
+
if (context.buildId)
|
|
113
|
+
return null;
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
content: null,
|
|
117
|
+
error: WORKSHOP_UNLINKED_DRAFT_TOOL_ERROR,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Tied builds save to the stored path. Untitled builds keep the caller's destination. */
|
|
121
|
+
export function resolvePinnedSaveDestination(context) {
|
|
122
|
+
if (!isWorkshopDraftContext(context) ||
|
|
123
|
+
!context.buildId ||
|
|
124
|
+
!context.chatSessionId) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const live = liveBySession.get(context.chatSessionId);
|
|
128
|
+
if (!live)
|
|
129
|
+
return null;
|
|
130
|
+
const build = readWorkshopBuild(live.storeRoot, context.buildId);
|
|
131
|
+
if (!build?.projectRoot || !build.relativePath)
|
|
132
|
+
return null;
|
|
133
|
+
return destinationFromRelativePath(build.relativePath);
|
|
134
|
+
}
|
|
135
|
+
/** A successful save of an untitled build records the operator destination on that id. */
|
|
136
|
+
export function recordPinnedWorkshopSave(context, saved) {
|
|
137
|
+
if (!isWorkshopDraftContext(context) ||
|
|
138
|
+
!context.buildId ||
|
|
139
|
+
!context.chatSessionId) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const live = liveBySession.get(context.chatSessionId);
|
|
143
|
+
if (!live)
|
|
144
|
+
return;
|
|
145
|
+
const build = readWorkshopBuild(live.storeRoot, context.buildId);
|
|
146
|
+
if (!build || (build.projectRoot && build.relativePath))
|
|
147
|
+
return;
|
|
148
|
+
const pipelineFilename = saved.pipelineFilename?.trim() ||
|
|
149
|
+
`${saved.draft.pipeline.id}.pipeline.yaml`;
|
|
150
|
+
updateWorkshopBuild(live.storeRoot, build.id, {
|
|
151
|
+
draft: saved.draft,
|
|
152
|
+
projectRoot: path.resolve(saved.projectRoot),
|
|
153
|
+
relativePath: relativePathFromDestination(saved.directory, pipelineFilename),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Create an untitled build and move this turn's pin onto it.
|
|
158
|
+
* Later edits in the turn persist on that id.
|
|
159
|
+
*/
|
|
160
|
+
export function pinWorkshopBuildOnCreate(input) {
|
|
161
|
+
const build = createWorkshopBuild(input.storeRoot, { draft: input.draft });
|
|
162
|
+
registerLiveWorkshopSession(input.sessionId, {
|
|
163
|
+
storeRoot: input.storeRoot,
|
|
164
|
+
registry: input.registry,
|
|
165
|
+
});
|
|
166
|
+
movePinOntoBuild(input.registry, input.sessionId, build, input.storeRoot);
|
|
167
|
+
return build;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Focus a build. When this turn already has a pin, update the pointer only.
|
|
171
|
+
* When the turn started with none, move the pin and the host onto that build.
|
|
172
|
+
*/
|
|
173
|
+
export function focusWorkshopBuildPointer(input) {
|
|
174
|
+
const build = getWorkshopBuild(input.storeRoot, input.buildId);
|
|
175
|
+
registerLiveWorkshopSession(input.sessionId, {
|
|
176
|
+
storeRoot: input.storeRoot,
|
|
177
|
+
registry: input.registry,
|
|
178
|
+
});
|
|
179
|
+
const pinned = input.registry.getPinnedBuildId(input.sessionId);
|
|
180
|
+
const turnLive = input.registry.getLiveTurn(input.sessionId);
|
|
181
|
+
const pinMoved = Boolean(turnLive) && !pinned;
|
|
182
|
+
if (pinMoved) {
|
|
183
|
+
movePinOntoBuild(input.registry, input.sessionId, build, input.storeRoot);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
enqueueSessionWrite(input.sessionId, () => {
|
|
187
|
+
updateWorkshopSessionActiveBuildId(input.storeRoot, input.sessionId, build.id);
|
|
188
|
+
});
|
|
189
|
+
if (turnLive)
|
|
190
|
+
emitPointerChange(input.registry, input.sessionId, build);
|
|
191
|
+
}
|
|
192
|
+
return { pinMoved, activeBuildId: build.id };
|
|
193
|
+
}
|
package/examples/README.md
CHANGED
|
@@ -30,6 +30,7 @@ More CI-focused examples will follow this pattern (prepare context → run pipel
|
|
|
30
30
|
| [generic-fan-in](generic-fan-in/) | Dummy diamond join; inspect keyed parent envelopes | `sf ui`, then `sf run` |
|
|
31
31
|
| [stage-mcp](stage-mcp/) | Stage MCP via project `.mcp.json` and a local echo fixture | `sf validate`, then `sf run` from git root |
|
|
32
32
|
| [playwright-mcp](playwright-mcp/) | One stage: open a page and save a PNG screenshot (Playwright MCP) | `sf validate`, then `sf run` from git root |
|
|
33
|
+
| [browser-session](browser-session/) | Saved browser login: check, human login only when logged out, then work; local fixture site | [README](browser-session/README.md), then `sf run` |
|
|
33
34
|
| [context7-mcp](context7-mcp/) | Three stages: resolve a library, fetch docs, write a brief (Context7 MCP) | `sf validate`, then `sf run` from git root |
|
|
34
35
|
| [feedback-loop](feedback-loop/) | Source-owned review loop (`continue` / `send_back`) | `sf validate`, then `sf run` |
|
|
35
36
|
| [feature-loop](feature-loop/) | Epic-to-PR; two Clone Chains + `{ type: loop }` | `sf validate`, then `sf run` |
|
|
@@ -46,7 +47,7 @@ More CI-focused examples will follow this pattern (prepare context → run pipel
|
|
|
46
47
|
|
|
47
48
|
Current wiring tours: [`route-wiring-smoke-test`](route-wiring-smoke-test/), [`route-if-tour`](route-if-tour/).
|
|
48
49
|
|
|
49
|
-
Browse scope is declared in repo-root [`stageflow.yaml`](../stageflow.yaml). **`sf ui` started from any subdirectory** still uses
|
|
50
|
+
Browse scope is declared in repo-root [`stageflow.yaml`](../stageflow.yaml). Cold Host (`sf ui` / `sf mcp`) also browses the packaged seeded root via [`examples/stageflow.yaml`](stageflow.yaml) (paths relative to `examples/`; wire `project_root: "examples"`). **`sf ui` started from any subdirectory** still uses the global durable root (`$STAGEFLOW_HOME`) for run state.
|
|
50
51
|
|
|
51
52
|
## Prerequisites (all examples)
|
|
52
53
|
|