stageflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/agent/activity.d.ts +76 -0
- package/dist/agent/activity.js +89 -0
- package/dist/agent/activityObserver.d.ts +17 -0
- package/dist/agent/activityObserver.js +66 -0
- package/dist/agent/cursorExtension.d.ts +12 -0
- package/dist/agent/cursorExtension.js +88 -0
- package/dist/agent/cursorProvider.d.ts +14 -0
- package/dist/agent/cursorProvider.js +139 -0
- package/dist/agent/fakeAgent.d.ts +29 -0
- package/dist/agent/fakeAgent.js +295 -0
- package/dist/agent/oauthSessionManager.d.ts +33 -0
- package/dist/agent/oauthSessionManager.js +214 -0
- package/dist/agent/piAdapter.d.ts +128 -0
- package/dist/agent/piAdapter.js +967 -0
- package/dist/agent/port.d.ts +63 -0
- package/dist/agent/port.js +46 -0
- package/dist/agent/providerAuth.d.ts +101 -0
- package/dist/agent/providerAuth.js +282 -0
- package/dist/agent/providerSupport.d.ts +28 -0
- package/dist/agent/providerSupport.js +19 -0
- package/dist/cli/providersCommand.d.ts +11 -0
- package/dist/cli/providersCommand.js +275 -0
- package/dist/cli/terminalAuthInteraction.d.ts +8 -0
- package/dist/cli/terminalAuthInteraction.js +65 -0
- package/dist/cli/terminalSecret.d.ts +2 -0
- package/dist/cli/terminalSecret.js +67 -0
- package/dist/cli/validateCommand.d.ts +11 -0
- package/dist/cli/validateCommand.js +81 -0
- package/dist/cli/validateOutput.d.ts +8 -0
- package/dist/cli/validateOutput.js +75 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +273 -0
- package/dist/config/createPipeline.d.ts +31 -0
- package/dist/config/createPipeline.js +255 -0
- package/dist/config/createStage.d.ts +25 -0
- package/dist/config/createStage.js +181 -0
- package/dist/config/listConfig.d.ts +33 -0
- package/dist/config/listConfig.js +195 -0
- package/dist/config/listExtensions.d.ts +26 -0
- package/dist/config/listExtensions.js +50 -0
- package/dist/config/listSkills.d.ts +29 -0
- package/dist/config/listSkills.js +55 -0
- package/dist/config/loadOutcome.d.ts +27 -0
- package/dist/config/loadOutcome.js +6 -0
- package/dist/config/loadPipeline.d.ts +25 -0
- package/dist/config/loadPipeline.js +196 -0
- package/dist/config/loadStage.d.ts +4 -0
- package/dist/config/loadStage.js +127 -0
- package/dist/config/loadTask.d.ts +7 -0
- package/dist/config/loadTask.js +69 -0
- package/dist/config/readYamlObject.d.ts +1 -0
- package/dist/config/readYamlObject.js +5 -0
- package/dist/config/resolvePipelineDag.d.ts +12 -0
- package/dist/config/resolvePipelineDag.js +282 -0
- package/dist/config/validateCatalog.d.ts +36 -0
- package/dist/config/validateCatalog.js +322 -0
- package/dist/envelope/check.d.ts +3 -0
- package/dist/envelope/check.js +52 -0
- package/dist/envelope/payloadSchema.d.ts +10 -0
- package/dist/envelope/payloadSchema.js +91 -0
- package/dist/hitl/qaTrail.d.ts +31 -0
- package/dist/hitl/qaTrail.js +65 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +7 -0
- package/dist/mcp/projectRun.d.ts +37 -0
- package/dist/mcp/projectRun.js +57 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +46 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +26 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.js +96 -0
- package/dist/package-meta.d.ts +1 -0
- package/dist/package-meta.js +1 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -0
- package/dist/prompt/priorEnvelope.js +5 -0
- package/dist/runstore/catalog.d.ts +8 -0
- package/dist/runstore/catalog.js +16 -0
- package/dist/runstore/createStore.d.ts +15 -0
- package/dist/runstore/createStore.js +23 -0
- package/dist/runstore/disk/DiskRunStore.d.ts +30 -0
- package/dist/runstore/disk/DiskRunStore.js +238 -0
- package/dist/runstore/disk/catalogHelpers.d.ts +11 -0
- package/dist/runstore/disk/catalogHelpers.js +102 -0
- package/dist/runstore/layout.d.ts +22 -0
- package/dist/runstore/layout.js +58 -0
- package/dist/runstore/paths.d.ts +7 -0
- package/dist/runstore/paths.js +60 -0
- package/dist/runstore/pipelineDagSnapshot.d.ts +5 -0
- package/dist/runstore/pipelineDagSnapshot.js +43 -0
- package/dist/runstore/port.d.ts +146 -0
- package/dist/runstore/port.js +28 -0
- package/dist/runstore/runProjection.d.ts +6 -0
- package/dist/runstore/runProjection.js +102 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +48 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +460 -0
- package/dist/runstore/sqlite/migrateFromDisk.d.ts +5 -0
- package/dist/runstore/sqlite/migrateFromDisk.js +108 -0
- package/dist/runstore/sqlite/schema.d.ts +1 -0
- package/dist/runstore/sqlite/schema.js +59 -0
- package/dist/runstore/stageExecution.d.ts +13 -0
- package/dist/runstore/stageExecution.js +22 -0
- package/dist/runstore/stageSnapshot.d.ts +2 -0
- package/dist/runstore/stageSnapshot.js +46 -0
- package/dist/runstore/syntheticStageSnapshot.d.ts +2 -0
- package/dist/runstore/syntheticStageSnapshot.js +10 -0
- package/dist/runstore/trackProjection.d.ts +7 -0
- package/dist/runstore/trackProjection.js +94 -0
- package/dist/runstore/workspaceLayout.d.ts +31 -0
- package/dist/runstore/workspaceLayout.js +172 -0
- package/dist/runtime/answerResume.d.ts +16 -0
- package/dist/runtime/answerResume.js +22 -0
- package/dist/runtime/credentialBinding.d.ts +16 -0
- package/dist/runtime/credentialBinding.js +80 -0
- package/dist/runtime/dagTraversal.d.ts +3 -0
- package/dist/runtime/dagTraversal.js +26 -0
- package/dist/runtime/envelopeRouting.d.ts +20 -0
- package/dist/runtime/envelopeRouting.js +59 -0
- package/dist/runtime/hitlSeams.d.ts +38 -0
- package/dist/runtime/hitlSeams.js +2 -0
- package/dist/runtime/pipelineRunner.d.ts +77 -0
- package/dist/runtime/pipelineRunner.js +141 -0
- package/dist/runtime/pipelineScheduler.d.ts +90 -0
- package/dist/runtime/pipelineScheduler.js +494 -0
- package/dist/runtime/pipelineValidationError.d.ts +6 -0
- package/dist/runtime/pipelineValidationError.js +8 -0
- package/dist/runtime/resumeReconstruct.d.ts +33 -0
- package/dist/runtime/resumeReconstruct.js +144 -0
- package/dist/runtime/runManager.d.ts +129 -0
- package/dist/runtime/runManager.js +818 -0
- package/dist/runtime/runRetryCoordinator.d.ts +85 -0
- package/dist/runtime/runRetryCoordinator.js +372 -0
- package/dist/runtime/settingsFile.d.ts +17 -0
- package/dist/runtime/settingsFile.js +88 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +40 -0
- package/dist/runtime/stageAttemptBootstrap.js +82 -0
- package/dist/runtime/stageAttemptContext.d.ts +11 -0
- package/dist/runtime/stageAttemptContext.js +26 -0
- package/dist/runtime/stageConcurrency.d.ts +10 -0
- package/dist/runtime/stageConcurrency.js +51 -0
- package/dist/runtime/stageHitl.d.ts +95 -0
- package/dist/runtime/stageHitl.js +198 -0
- package/dist/runtime/stageProcessLauncher.d.ts +45 -0
- package/dist/runtime/stageProcessLauncher.js +199 -0
- package/dist/runtime/stageRecovery.d.ts +10 -0
- package/dist/runtime/stageRecovery.js +43 -0
- package/dist/runtime/stageRoots.d.ts +22 -0
- package/dist/runtime/stageRoots.js +93 -0
- package/dist/runtime/stageRunner.d.ts +59 -0
- package/dist/runtime/stageRunner.js +221 -0
- package/dist/runtime/stageWorker.d.ts +6 -0
- package/dist/runtime/stageWorker.js +109 -0
- package/dist/runtime/stageWorkerProtocol.d.ts +27 -0
- package/dist/runtime/stageWorkerProtocol.js +16 -0
- package/dist/runtime/taskInput.d.ts +15 -0
- package/dist/runtime/taskInput.js +38 -0
- package/dist/server/http.d.ts +31 -0
- package/dist/server/http.js +561 -0
- package/dist/server/providerRoutes.d.ts +15 -0
- package/dist/server/providerRoutes.js +186 -0
- package/dist/tools/askOperator.d.ts +185 -0
- package/dist/tools/askOperator.js +409 -0
- package/dist/tools/emitStageEnvelope.d.ts +42 -0
- package/dist/tools/emitStageEnvelope.js +61 -0
- package/dist/tools/writeStageArtifact.d.ts +26 -0
- package/dist/tools/writeStageArtifact.js +103 -0
- package/dist/types/envelope.d.ts +12 -0
- package/dist/types/envelope.js +6 -0
- package/dist/types/pipeline.d.ts +25 -0
- package/dist/types/pipeline.js +1 -0
- package/dist/types/stage.d.ts +12 -0
- package/dist/types/stage.js +6 -0
- package/dist/types/task.d.ts +7 -0
- package/dist/types/task.js +1 -0
- package/dist/ui/assets/index-Cry0Tpfx.js +118 -0
- package/dist/ui/assets/index-DCsDopak.css +1 -0
- package/dist/ui/index.html +21 -0
- package/package.json +72 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { newRunId, runWorkspaceDir, runsDir } from "../paths.js";
|
|
4
|
+
import { agentDir, artifactsDir, attemptAgentDir, attemptArtifactsDir, attemptEnvelopePath, attemptLogPath, envelopePath, stageDir, } from "../workspaceLayout.js";
|
|
5
|
+
import { projectRunDetail, orderStageSnapshots } from "../runProjection.js";
|
|
6
|
+
import { buildStageSnapshotFromStore } from "../stageSnapshot.js";
|
|
7
|
+
import { buildRunSummaryFromDisk, fileExists, listStageIdsFromDisk, readRunMetaFromDisk, readStageLogFromDisk, taskIdFromCopy, } from "./catalogHelpers.js";
|
|
8
|
+
const ensuredStageDirs = new Set();
|
|
9
|
+
const EXECUTIONS_FILE = "executions.json";
|
|
10
|
+
async function readStageLogFile(file) {
|
|
11
|
+
if (!(await fileExists(file)))
|
|
12
|
+
return [];
|
|
13
|
+
const text = await readFile(file, "utf8");
|
|
14
|
+
return text
|
|
15
|
+
.split("\n")
|
|
16
|
+
.map((line) => line.trim())
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.map((line) => JSON.parse(line));
|
|
19
|
+
}
|
|
20
|
+
async function readExecutionsFromDisk(workspaceDir, stageId) {
|
|
21
|
+
const file = path.join(workspaceDir, "stages", stageId, EXECUTIONS_FILE);
|
|
22
|
+
if (!(await fileExists(file)))
|
|
23
|
+
return [];
|
|
24
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
25
|
+
return [...parsed].sort((a, b) => a.attempt - b.attempt);
|
|
26
|
+
}
|
|
27
|
+
async function writeExecutionsToDisk(workspaceDir, stageId, executions) {
|
|
28
|
+
const file = path.join(workspaceDir, "stages", stageId, EXECUTIONS_FILE);
|
|
29
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
30
|
+
const sorted = [...executions].sort((a, b) => a.attempt - b.attempt);
|
|
31
|
+
await writeFile(file, `${JSON.stringify(sorted, null, 2)}\n`);
|
|
32
|
+
}
|
|
33
|
+
export class DiskRunStore {
|
|
34
|
+
storeRoot;
|
|
35
|
+
constructor(storeRoot) {
|
|
36
|
+
this.storeRoot = storeRoot;
|
|
37
|
+
}
|
|
38
|
+
getWorkspaceDir(runId) {
|
|
39
|
+
return runWorkspaceDir(this.storeRoot, runId);
|
|
40
|
+
}
|
|
41
|
+
async createRun(input) {
|
|
42
|
+
const runId = newRunId();
|
|
43
|
+
const workspaceDir = this.getWorkspaceDir(runId);
|
|
44
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
45
|
+
const now = new Date().toISOString();
|
|
46
|
+
const meta = {
|
|
47
|
+
run_id: runId,
|
|
48
|
+
pipeline_id: input.pipelineId,
|
|
49
|
+
created_at: now,
|
|
50
|
+
status: "running",
|
|
51
|
+
task_id: input.taskId,
|
|
52
|
+
updated_at: now,
|
|
53
|
+
...(input.checkoutRoot !== undefined
|
|
54
|
+
? { checkout_root: input.checkoutRoot }
|
|
55
|
+
: {}),
|
|
56
|
+
...(input.pipelineDag ? { pipeline_dag: input.pipelineDag } : {}),
|
|
57
|
+
};
|
|
58
|
+
await Promise.all([
|
|
59
|
+
writeFile(path.join(workspaceDir, "meta.json"), `${JSON.stringify(meta, null, 2)}\n`),
|
|
60
|
+
writeFile(path.join(workspaceDir, "task.copy.yaml"), input.taskYaml),
|
|
61
|
+
]);
|
|
62
|
+
return { runId, workspaceDir };
|
|
63
|
+
}
|
|
64
|
+
async updateRunStatus(runId, status) {
|
|
65
|
+
const workspaceDir = this.getWorkspaceDir(runId);
|
|
66
|
+
const meta = await readRunMetaFromDisk(workspaceDir);
|
|
67
|
+
meta.status = status;
|
|
68
|
+
meta.updated_at = new Date().toISOString();
|
|
69
|
+
await writeFile(path.join(workspaceDir, "meta.json"), `${JSON.stringify(meta, null, 2)}\n`);
|
|
70
|
+
}
|
|
71
|
+
async readRunMeta(runId) {
|
|
72
|
+
return readRunMetaFromDisk(this.getWorkspaceDir(runId));
|
|
73
|
+
}
|
|
74
|
+
async readTaskYaml(runId) {
|
|
75
|
+
return readFile(path.join(this.getWorkspaceDir(runId), "task.copy.yaml"), "utf8");
|
|
76
|
+
}
|
|
77
|
+
async ensureStageWorkspace(runId, stageId) {
|
|
78
|
+
const workspaceDir = this.getWorkspaceDir(runId);
|
|
79
|
+
const dir = stageDir(workspaceDir, stageId);
|
|
80
|
+
if (!ensuredStageDirs.has(dir)) {
|
|
81
|
+
await mkdir(artifactsDir(workspaceDir, stageId), { recursive: true });
|
|
82
|
+
await mkdir(agentDir(workspaceDir, stageId), { recursive: true });
|
|
83
|
+
ensuredStageDirs.add(dir);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async ensureAttemptWorkspace(runId, stageId, attempt) {
|
|
87
|
+
const workspaceDir = this.getWorkspaceDir(runId);
|
|
88
|
+
const key = `${runId}:${stageId}:${attempt}`;
|
|
89
|
+
if (!ensuredStageDirs.has(key)) {
|
|
90
|
+
await mkdir(attemptArtifactsDir(workspaceDir, stageId, attempt), {
|
|
91
|
+
recursive: true,
|
|
92
|
+
});
|
|
93
|
+
await mkdir(attemptAgentDir(workspaceDir, stageId, attempt), {
|
|
94
|
+
recursive: true,
|
|
95
|
+
});
|
|
96
|
+
ensuredStageDirs.add(key);
|
|
97
|
+
}
|
|
98
|
+
if (attempt === 1) {
|
|
99
|
+
await this.ensureStageWorkspace(runId, stageId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async createStageExecution(runId, stageId) {
|
|
103
|
+
const workspaceDir = this.getWorkspaceDir(runId);
|
|
104
|
+
await this.ensureStageWorkspace(runId, stageId);
|
|
105
|
+
const executions = await readExecutionsFromDisk(workspaceDir, stageId);
|
|
106
|
+
const attempt = executions.length > 0 ? Math.max(...executions.map((e) => e.attempt)) + 1 : 1;
|
|
107
|
+
const execution = {
|
|
108
|
+
run_id: runId,
|
|
109
|
+
stage_id: stageId,
|
|
110
|
+
attempt,
|
|
111
|
+
status: "pending",
|
|
112
|
+
envelope: null,
|
|
113
|
+
};
|
|
114
|
+
executions.push(execution);
|
|
115
|
+
await writeExecutionsToDisk(workspaceDir, stageId, executions);
|
|
116
|
+
return execution;
|
|
117
|
+
}
|
|
118
|
+
async listStageExecutions(runId, stageId) {
|
|
119
|
+
return readExecutionsFromDisk(this.getWorkspaceDir(runId), stageId);
|
|
120
|
+
}
|
|
121
|
+
async getLatestStageExecution(runId, stageId) {
|
|
122
|
+
const executions = await this.listStageExecutions(runId, stageId);
|
|
123
|
+
return executions.length > 0 ? executions[executions.length - 1] : null;
|
|
124
|
+
}
|
|
125
|
+
async countStageAttempts(runId, stageId) {
|
|
126
|
+
const executions = await this.listStageExecutions(runId, stageId);
|
|
127
|
+
return executions.length;
|
|
128
|
+
}
|
|
129
|
+
async getStageExecution(runId, stageId, attempt) {
|
|
130
|
+
const executions = await this.listStageExecutions(runId, stageId);
|
|
131
|
+
const found = executions.find((e) => e.attempt === attempt);
|
|
132
|
+
if (!found) {
|
|
133
|
+
throw new Error(`Stage execution not found: ${runId}/${stageId} attempt ${attempt}`);
|
|
134
|
+
}
|
|
135
|
+
return found;
|
|
136
|
+
}
|
|
137
|
+
async updateStageExecution(runId, stageId, attempt, patch) {
|
|
138
|
+
const workspaceDir = this.getWorkspaceDir(runId);
|
|
139
|
+
const executions = await readExecutionsFromDisk(workspaceDir, stageId);
|
|
140
|
+
const index = executions.findIndex((e) => e.attempt === attempt);
|
|
141
|
+
if (index < 0) {
|
|
142
|
+
throw new Error(`Stage execution not found: ${runId}/${stageId} attempt ${attempt}`);
|
|
143
|
+
}
|
|
144
|
+
const current = executions[index];
|
|
145
|
+
executions[index] = {
|
|
146
|
+
...current,
|
|
147
|
+
...(patch.status !== undefined ? { status: patch.status } : {}),
|
|
148
|
+
...(patch.started_at !== undefined ? { started_at: patch.started_at } : {}),
|
|
149
|
+
...(patch.finished_at !== undefined ? { finished_at: patch.finished_at } : {}),
|
|
150
|
+
...(patch.envelope !== undefined ? { envelope: patch.envelope } : {}),
|
|
151
|
+
};
|
|
152
|
+
await writeExecutionsToDisk(workspaceDir, stageId, executions);
|
|
153
|
+
}
|
|
154
|
+
async writeEnvelope(runId, stageId, envelope, options) {
|
|
155
|
+
const attempt = options?.attempt ?? 1;
|
|
156
|
+
await this.ensureStageWorkspace(runId, stageId);
|
|
157
|
+
if (attempt >= 2) {
|
|
158
|
+
await this.ensureAttemptWorkspace(runId, stageId, attempt);
|
|
159
|
+
}
|
|
160
|
+
const workspaceDir = this.getWorkspaceDir(runId);
|
|
161
|
+
const file = attempt >= 2
|
|
162
|
+
? attemptEnvelopePath(workspaceDir, stageId, attempt)
|
|
163
|
+
: envelopePath(workspaceDir, stageId);
|
|
164
|
+
await writeFile(file, `${JSON.stringify(envelope, null, 2)}\n`);
|
|
165
|
+
try {
|
|
166
|
+
await this.updateStageExecution(runId, stageId, attempt, { envelope });
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// no execution row yet
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async readEnvelope(runId, stageId) {
|
|
173
|
+
const file = envelopePath(this.getWorkspaceDir(runId), stageId);
|
|
174
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
175
|
+
}
|
|
176
|
+
async appendStageEvent(runId, stageId, event, options) {
|
|
177
|
+
const attempt = options?.attempt ?? 1;
|
|
178
|
+
await this.ensureAttemptWorkspace(runId, stageId, attempt);
|
|
179
|
+
const file = attemptLogPath(this.getWorkspaceDir(runId), stageId, attempt);
|
|
180
|
+
const line = `${JSON.stringify({ ...event, at: new Date().toISOString() })}\n`;
|
|
181
|
+
await appendFile(file, line);
|
|
182
|
+
}
|
|
183
|
+
async listStageEvents(runId, stageId, attempt) {
|
|
184
|
+
const workspaceDir = this.getWorkspaceDir(runId);
|
|
185
|
+
if (attempt != null) {
|
|
186
|
+
return readStageLogFile(attemptLogPath(workspaceDir, stageId, attempt));
|
|
187
|
+
}
|
|
188
|
+
const events = [];
|
|
189
|
+
events.push(...(await readStageLogFromDisk(workspaceDir, stageId)));
|
|
190
|
+
const attemptsDir = path.join(workspaceDir, "stages", stageId, "attempts");
|
|
191
|
+
if (await fileExists(attemptsDir)) {
|
|
192
|
+
const entries = await readdir(attemptsDir, { withFileTypes: true });
|
|
193
|
+
const attemptNums = entries
|
|
194
|
+
.filter((e) => e.isDirectory())
|
|
195
|
+
.map((e) => Number.parseInt(e.name, 10))
|
|
196
|
+
.filter((n) => Number.isFinite(n) && n >= 2)
|
|
197
|
+
.sort((a, b) => a - b);
|
|
198
|
+
for (const n of attemptNums) {
|
|
199
|
+
events.push(...(await readStageLogFile(attemptLogPath(workspaceDir, stageId, n))));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return events.sort((a, b) => (a.at ?? "").localeCompare(b.at ?? ""));
|
|
203
|
+
}
|
|
204
|
+
async listRuns() {
|
|
205
|
+
const root = runsDir(this.storeRoot);
|
|
206
|
+
if (!(await fileExists(root)))
|
|
207
|
+
return [];
|
|
208
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
209
|
+
const summaries = [];
|
|
210
|
+
for (const entry of entries) {
|
|
211
|
+
if (!entry.isDirectory())
|
|
212
|
+
continue;
|
|
213
|
+
try {
|
|
214
|
+
summaries.push(await buildRunSummaryFromDisk(path.join(root, entry.name)));
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// skip malformed run folders
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
summaries.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
221
|
+
return summaries;
|
|
222
|
+
}
|
|
223
|
+
async readRun(runId) {
|
|
224
|
+
const workspaceDir = this.getWorkspaceDir(runId);
|
|
225
|
+
if (!(await fileExists(path.join(workspaceDir, "meta.json")))) {
|
|
226
|
+
throw new Error(`Run not found: ${runId}`);
|
|
227
|
+
}
|
|
228
|
+
const meta = await readRunMetaFromDisk(workspaceDir);
|
|
229
|
+
const task_yaml = await readFile(path.join(workspaceDir, "task.copy.yaml"), "utf8");
|
|
230
|
+
const stageIds = await listStageIdsFromDisk(workspaceDir, meta);
|
|
231
|
+
const loaded = await Promise.all(stageIds.map((id) => buildStageSnapshotFromStore(this, runId, id, workspaceDir)));
|
|
232
|
+
const stages = meta.pipeline_dag?.stage_ids?.length
|
|
233
|
+
? orderStageSnapshots(meta.pipeline_dag.stage_ids, loaded)
|
|
234
|
+
: loaded;
|
|
235
|
+
const task_id = await taskIdFromCopy(workspaceDir, meta);
|
|
236
|
+
return projectRunDetail({ ...meta, task_id }, stages, task_yaml, meta.pipeline_dag ?? null);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type RunDetail, type RunMeta, type RunSummary, type StageLogEvent, type StageSnapshot } from "../port.js";
|
|
2
|
+
import { listArtifactNames } from "../workspaceLayout.js";
|
|
3
|
+
export { listArtifactNames };
|
|
4
|
+
export declare function fileExists(filePath: string): Promise<boolean>;
|
|
5
|
+
export declare function readStageLogFromDisk(workspaceDir: string, stageId: string): Promise<StageLogEvent[]>;
|
|
6
|
+
export declare function loadStageSnapshotFromDisk(workspaceDir: string, stageId: string, store?: import("../port.js").RunStore, runId?: string): Promise<StageSnapshot>;
|
|
7
|
+
export declare function listStageIdsFromDisk(workspaceDir: string, meta?: RunMeta): Promise<string[]>;
|
|
8
|
+
export declare function taskIdFromCopy(workspaceDir: string, meta: RunMeta): Promise<string | undefined>;
|
|
9
|
+
export declare function readRunMetaFromDisk(workspaceDir: string): Promise<RunMeta>;
|
|
10
|
+
export declare function buildRunSummaryFromDisk(workspaceDir: string): Promise<RunSummary>;
|
|
11
|
+
export declare function buildRunDetailFromDisk(workspaceDir: string): Promise<RunDetail>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readdir, readFile, access } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadTaskFromYaml } from "../../config/loadTask.js";
|
|
4
|
+
import { derivePendingPrompt } from "../../hitl/qaTrail.js";
|
|
5
|
+
import { stageStatusFromEvents, } from "../port.js";
|
|
6
|
+
import { projectRunDetail, projectRunSummary, orderStageSnapshots } from "../runProjection.js";
|
|
7
|
+
import { buildStageSnapshotFromStore } from "../stageSnapshot.js";
|
|
8
|
+
import { envelopePath, listArtifactNames, stageLogPath, } from "../workspaceLayout.js";
|
|
9
|
+
export { listArtifactNames };
|
|
10
|
+
export async function fileExists(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
await access(filePath);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function readStageLogFromDisk(workspaceDir, stageId) {
|
|
20
|
+
const file = stageLogPath(workspaceDir, stageId);
|
|
21
|
+
if (!(await fileExists(file)))
|
|
22
|
+
return [];
|
|
23
|
+
const text = await readFile(file, "utf8");
|
|
24
|
+
return text
|
|
25
|
+
.split("\n")
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.map((line) => JSON.parse(line));
|
|
29
|
+
}
|
|
30
|
+
export async function loadStageSnapshotFromDisk(workspaceDir, stageId, store, runId) {
|
|
31
|
+
if (store !== undefined && runId !== undefined) {
|
|
32
|
+
return buildStageSnapshotFromStore(store, runId, stageId, workspaceDir);
|
|
33
|
+
}
|
|
34
|
+
const events = await readStageLogFromDisk(workspaceDir, stageId);
|
|
35
|
+
const envFile = envelopePath(workspaceDir, stageId);
|
|
36
|
+
let envelope = null;
|
|
37
|
+
if (await fileExists(envFile)) {
|
|
38
|
+
envelope = JSON.parse(await readFile(envFile, "utf8"));
|
|
39
|
+
}
|
|
40
|
+
const artifacts = await listArtifactNames(workspaceDir, stageId, 1);
|
|
41
|
+
const last_at = events.length > 0 ? events[events.length - 1]?.at : undefined;
|
|
42
|
+
const pending = derivePendingPrompt(events);
|
|
43
|
+
return {
|
|
44
|
+
stage_id: stageId,
|
|
45
|
+
status: stageStatusFromEvents(events),
|
|
46
|
+
events,
|
|
47
|
+
envelope,
|
|
48
|
+
artifacts,
|
|
49
|
+
last_at,
|
|
50
|
+
attempt_count: 1,
|
|
51
|
+
...(pending ? { pending_prompt: pending } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export async function listStageIdsFromDisk(workspaceDir, meta) {
|
|
55
|
+
if (meta?.pipeline_dag?.stage_ids?.length) {
|
|
56
|
+
return [...meta.pipeline_dag.stage_ids];
|
|
57
|
+
}
|
|
58
|
+
const stagesPath = path.join(workspaceDir, "stages");
|
|
59
|
+
if (!(await fileExists(stagesPath)))
|
|
60
|
+
return [];
|
|
61
|
+
const entries = await readdir(stagesPath, { withFileTypes: true });
|
|
62
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
63
|
+
}
|
|
64
|
+
export async function taskIdFromCopy(workspaceDir, meta) {
|
|
65
|
+
if (meta.task_id)
|
|
66
|
+
return meta.task_id;
|
|
67
|
+
const taskPath = path.join(workspaceDir, "task.copy.yaml");
|
|
68
|
+
if (!(await fileExists(taskPath)))
|
|
69
|
+
return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const task = loadTaskFromYaml(await readFile(taskPath, "utf8"), "task.copy.yaml");
|
|
72
|
+
return task.id;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function readRunMetaFromDisk(workspaceDir) {
|
|
79
|
+
const file = path.join(workspaceDir, "meta.json");
|
|
80
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
81
|
+
}
|
|
82
|
+
export async function buildRunSummaryFromDisk(workspaceDir) {
|
|
83
|
+
const meta = await readRunMetaFromDisk(workspaceDir);
|
|
84
|
+
const stageIds = await listStageIdsFromDisk(workspaceDir, meta);
|
|
85
|
+
const loaded = await Promise.all(stageIds.map((id) => loadStageSnapshotFromDisk(workspaceDir, id)));
|
|
86
|
+
const stages = meta.pipeline_dag?.stage_ids?.length
|
|
87
|
+
? orderStageSnapshots(meta.pipeline_dag.stage_ids, loaded)
|
|
88
|
+
: loaded;
|
|
89
|
+
const task_id = await taskIdFromCopy(workspaceDir, meta);
|
|
90
|
+
return projectRunSummary({ ...meta, task_id }, stages);
|
|
91
|
+
}
|
|
92
|
+
export async function buildRunDetailFromDisk(workspaceDir) {
|
|
93
|
+
const meta = await readRunMetaFromDisk(workspaceDir);
|
|
94
|
+
const task_yaml = await readFile(path.join(workspaceDir, "task.copy.yaml"), "utf8");
|
|
95
|
+
const stageIds = await listStageIdsFromDisk(workspaceDir, meta);
|
|
96
|
+
const loaded = await Promise.all(stageIds.map((id) => loadStageSnapshotFromDisk(workspaceDir, id)));
|
|
97
|
+
const stages = meta.pipeline_dag?.stage_ids?.length
|
|
98
|
+
? orderStageSnapshots(meta.pipeline_dag.stage_ids, loaded)
|
|
99
|
+
: loaded;
|
|
100
|
+
const task_id = await taskIdFromCopy(workspaceDir, meta);
|
|
101
|
+
return projectRunDetail({ ...meta, task_id }, stages, task_yaml, meta.pipeline_dag ?? null);
|
|
102
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StageEnvelope } from "../types/envelope.js";
|
|
2
|
+
import type { RunMeta, RunStatus } from "./port.js";
|
|
3
|
+
export type { RunMeta, RunStatus } from "./port.js";
|
|
4
|
+
export { stageDir } from "./paths.js";
|
|
5
|
+
export declare function runsRoot(rootDir: string): string;
|
|
6
|
+
export declare function runDirFor(rootDir: string, runId: string): string;
|
|
7
|
+
export declare function createRun(options: {
|
|
8
|
+
rootDir: string;
|
|
9
|
+
pipelineId: string;
|
|
10
|
+
taskYaml: string;
|
|
11
|
+
taskId?: string;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
runId: string;
|
|
14
|
+
runDir: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function writeRunMeta(runDir: string, meta: RunMeta): Promise<void>;
|
|
17
|
+
export declare function readRunMeta(runDir: string): Promise<RunMeta>;
|
|
18
|
+
export declare function updateRunStatus(runDir: string, status: RunStatus): Promise<void>;
|
|
19
|
+
export declare function ensureStageDirs(runDir: string, stageId: string): Promise<string>;
|
|
20
|
+
export declare function writeEnvelope(runDir: string, stageId: string, envelope: StageEnvelope): Promise<string>;
|
|
21
|
+
export declare function appendStageLog(runDir: string, stageId: string, event: Record<string, unknown>): Promise<void>;
|
|
22
|
+
export declare function readEnvelope(runDir: string, stageId: string): Promise<StageEnvelope>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Prefer createRunStore() + RunStore. Thin disk helpers for legacy callers.
|
|
3
|
+
*/
|
|
4
|
+
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { DiskRunStore } from "./disk/DiskRunStore.js";
|
|
7
|
+
import { storeRootFor } from "./paths.js";
|
|
8
|
+
export { stageDir } from "./paths.js";
|
|
9
|
+
function disk(rootDir) {
|
|
10
|
+
return new DiskRunStore(storeRootFor(rootDir));
|
|
11
|
+
}
|
|
12
|
+
export function runsRoot(rootDir) {
|
|
13
|
+
return path.join(storeRootFor(rootDir), "runs");
|
|
14
|
+
}
|
|
15
|
+
export function runDirFor(rootDir, runId) {
|
|
16
|
+
return path.join(runsRoot(rootDir), runId);
|
|
17
|
+
}
|
|
18
|
+
export async function createRun(options) {
|
|
19
|
+
const created = await disk(options.rootDir).createRun({
|
|
20
|
+
pipelineId: options.pipelineId,
|
|
21
|
+
taskYaml: options.taskYaml,
|
|
22
|
+
taskId: options.taskId,
|
|
23
|
+
});
|
|
24
|
+
return { runId: created.runId, runDir: created.workspaceDir };
|
|
25
|
+
}
|
|
26
|
+
export async function writeRunMeta(runDir, meta) {
|
|
27
|
+
await writeFile(path.join(runDir, "meta.json"), `${JSON.stringify(meta, null, 2)}\n`);
|
|
28
|
+
}
|
|
29
|
+
export async function readRunMeta(runDir) {
|
|
30
|
+
return JSON.parse(await readFile(path.join(runDir, "meta.json"), "utf8"));
|
|
31
|
+
}
|
|
32
|
+
export async function updateRunStatus(runDir, status) {
|
|
33
|
+
const meta = await readRunMeta(runDir);
|
|
34
|
+
meta.status = status;
|
|
35
|
+
meta.updated_at = new Date().toISOString();
|
|
36
|
+
await writeRunMeta(runDir, meta);
|
|
37
|
+
}
|
|
38
|
+
export async function ensureStageDirs(runDir, stageId) {
|
|
39
|
+
const dir = path.join(runDir, "stages", stageId);
|
|
40
|
+
await mkdir(path.join(dir, "artifacts"), { recursive: true });
|
|
41
|
+
return dir;
|
|
42
|
+
}
|
|
43
|
+
export async function writeEnvelope(runDir, stageId, envelope) {
|
|
44
|
+
const dir = await ensureStageDirs(runDir, stageId);
|
|
45
|
+
const file = path.join(dir, "envelope.json");
|
|
46
|
+
await writeFile(file, `${JSON.stringify(envelope, null, 2)}\n`);
|
|
47
|
+
return file;
|
|
48
|
+
}
|
|
49
|
+
export async function appendStageLog(runDir, stageId, event) {
|
|
50
|
+
const dir = await ensureStageDirs(runDir, stageId);
|
|
51
|
+
const file = path.join(dir, "log.jsonl");
|
|
52
|
+
const line = `${JSON.stringify({ ...event, at: new Date().toISOString() })}\n`;
|
|
53
|
+
await appendFile(file, line);
|
|
54
|
+
}
|
|
55
|
+
export async function readEnvelope(runDir, stageId) {
|
|
56
|
+
const file = path.join(runDir, "stages", stageId, "envelope.json");
|
|
57
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
58
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function newRunId(): string;
|
|
2
|
+
/** Root for factory state under a project: `<rootDir>/.stageflow`. */
|
|
3
|
+
export declare function storeRootFor(rootDir: string): string;
|
|
4
|
+
export declare function migrateLegacyStoreRoot(rootDir: string): void;
|
|
5
|
+
export declare function runsDir(storeRoot: string): string;
|
|
6
|
+
export declare function runWorkspaceDir(storeRoot: string, runId: string): string;
|
|
7
|
+
export declare function stageDir(workspaceDir: string, stageId: string): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { readdirSync, renameSync, rmdirSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export function newRunId() {
|
|
5
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
6
|
+
return `${stamp}-${randomBytes(3).toString("hex")}`;
|
|
7
|
+
}
|
|
8
|
+
/** Root for factory state under a project: `<rootDir>/.stageflow`. */
|
|
9
|
+
export function storeRootFor(rootDir) {
|
|
10
|
+
return path.join(rootDir, ".stageflow");
|
|
11
|
+
}
|
|
12
|
+
function directoryState(dirPath) {
|
|
13
|
+
try {
|
|
14
|
+
const st = statSync(dirPath);
|
|
15
|
+
if (!st.isDirectory())
|
|
16
|
+
return "other";
|
|
17
|
+
return readdirSync(dirPath).length === 0 ? "empty" : "populated";
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
if (err.code === "ENOENT")
|
|
21
|
+
return "missing";
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function migrateLegacyStoreRoot(rootDir) {
|
|
26
|
+
const newPath = storeRootFor(rootDir);
|
|
27
|
+
const oldPath = path.join(rootDir, ".software-factory");
|
|
28
|
+
const newState = directoryState(newPath);
|
|
29
|
+
if (newState === "populated") {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const oldState = directoryState(oldPath);
|
|
33
|
+
if (oldState !== "empty" && oldState !== "populated") {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (newState === "empty") {
|
|
37
|
+
rmdirSync(newPath);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
renameSync(oldPath, newPath);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const code = err.code;
|
|
44
|
+
if (code === "ENOENT" &&
|
|
45
|
+
directoryState(oldPath) === "missing" &&
|
|
46
|
+
directoryState(newPath) === "populated") {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function runsDir(storeRoot) {
|
|
53
|
+
return path.join(storeRoot, "runs");
|
|
54
|
+
}
|
|
55
|
+
export function runWorkspaceDir(storeRoot, runId) {
|
|
56
|
+
return path.join(runsDir(storeRoot), runId);
|
|
57
|
+
}
|
|
58
|
+
export function stageDir(workspaceDir, stageId) {
|
|
59
|
+
return path.join(workspaceDir, "stages", stageId);
|
|
60
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LoadedPipeline } from "../types/pipeline.js";
|
|
2
|
+
import type { RunPipelineDagSnapshot } from "./port.js";
|
|
3
|
+
export declare function buildPipelineDagSnapshotFromLoaded(loaded: LoadedPipeline): RunPipelineDagSnapshot;
|
|
4
|
+
export declare function linearCompatDagSnapshot(stageIds: string[]): RunPipelineDagSnapshot;
|
|
5
|
+
export declare function parsePipelineDagSnapshot(raw: unknown): RunPipelineDagSnapshot | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function buildPipelineDagSnapshotFromLoaded(loaded) {
|
|
2
|
+
const gate_kinds = {};
|
|
3
|
+
for (const stage of loaded.stages) {
|
|
4
|
+
if (stage.gate_kinds?.length) {
|
|
5
|
+
gate_kinds[stage.id] = stage.gate_kinds;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
return {
|
|
9
|
+
stage_ids: [...loaded.pipeline.stages],
|
|
10
|
+
nodes: loaded.dag.nodes,
|
|
11
|
+
roots: loaded.dag.roots,
|
|
12
|
+
childrenOf: loaded.dag.childrenOf,
|
|
13
|
+
...(Object.keys(gate_kinds).length > 0 ? { gate_kinds } : {}),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function linearCompatDagSnapshot(stageIds) {
|
|
17
|
+
const nodes = stageIds.map((id, index) => ({
|
|
18
|
+
id,
|
|
19
|
+
needs: index === 0 ? null : stageIds[index - 1],
|
|
20
|
+
ancestors: index === 0 ? [] : stageIds.slice(0, index),
|
|
21
|
+
stageIndex: index,
|
|
22
|
+
}));
|
|
23
|
+
const childrenOf = {};
|
|
24
|
+
for (let i = 1; i < stageIds.length; i++) {
|
|
25
|
+
const parent = stageIds[i - 1];
|
|
26
|
+
const child = stageIds[i];
|
|
27
|
+
childrenOf[parent] = [...(childrenOf[parent] ?? []), child];
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
stage_ids: [...stageIds],
|
|
31
|
+
nodes,
|
|
32
|
+
roots: stageIds.length > 0 ? [stageIds[0]] : [],
|
|
33
|
+
childrenOf,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function parsePipelineDagSnapshot(raw) {
|
|
37
|
+
if (!raw || typeof raw !== "object")
|
|
38
|
+
return null;
|
|
39
|
+
const value = raw;
|
|
40
|
+
if (!Array.isArray(value.stage_ids) || !Array.isArray(value.nodes))
|
|
41
|
+
return null;
|
|
42
|
+
return raw;
|
|
43
|
+
}
|