gsd-pi 2.41.0-dev.3557dc4 → 2.41.0-dev.5a170d0
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 +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +80 -0
- package/dist/resources/extensions/gsd/auto/phases.js +2 -2
- package/dist/resources/extensions/gsd/auto/session.js +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +2 -0
- package/dist/resources/extensions/gsd/auto.js +28 -1
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
- package/dist/resources/extensions/gsd/commands/catalog.js +32 -0
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
- package/dist/resources/extensions/gsd/context-injector.js +74 -0
- package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
- package/dist/resources/extensions/gsd/custom-verification.js +145 -0
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
- package/dist/resources/extensions/gsd/definition-loader.js +352 -0
- package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
- package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
- package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
- package/dist/resources/extensions/gsd/engine-types.js +8 -0
- package/dist/resources/extensions/gsd/execution-policy.js +8 -0
- package/dist/resources/extensions/gsd/graph.js +225 -0
- package/dist/resources/extensions/gsd/run-manager.js +134 -0
- package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
- package/dist/resources/skills/create-workflow/SKILL.md +103 -0
- package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +5 -0
- package/src/resources/extensions/gsd/auto/loop.ts +91 -0
- package/src/resources/extensions/gsd/auto/phases.ts +2 -2
- package/src/resources/extensions/gsd/auto/session.ts +6 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/src/resources/extensions/gsd/auto.ts +31 -1
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
- package/src/resources/extensions/gsd/commands/catalog.ts +32 -0
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
- package/src/resources/extensions/gsd/context-injector.ts +100 -0
- package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
- package/src/resources/extensions/gsd/custom-verification.ts +180 -0
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
- package/src/resources/extensions/gsd/definition-loader.ts +462 -0
- package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
- package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
- package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
- package/src/resources/extensions/gsd/engine-types.ts +71 -0
- package/src/resources/extensions/gsd/execution-policy.ts +43 -0
- package/src/resources/extensions/gsd/graph.ts +312 -0
- package/src/resources/extensions/gsd/run-manager.ts +180 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +100 -118
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/captures.test.ts +12 -1
- package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +20 -20
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
- package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
- package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
- package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
- package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
- package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
- package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
- package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
- package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
- package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +195 -105
- package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
- package/src/resources/skills/create-workflow/SKILL.md +103 -0
- package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- /package/dist/web/standalone/.next/static/{JBSIr4fSfHXs5g5x2ZBSC → K7GYOOPvQWX6TKYEKhODM}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{JBSIr4fSfHXs5g5x2ZBSC → K7GYOOPvQWX6TKYEKhODM}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine-types.ts — Engine-polymorphic type contracts.
|
|
3
|
+
*
|
|
4
|
+
* LEAF NODE: This file must have ZERO imports from any GSD module.
|
|
5
|
+
* Only `node:` imports are permitted. All engine/policy interfaces
|
|
6
|
+
* depend on these types; nothing here depends on GSD internals.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Snapshot of engine state at a point in time. */
|
|
10
|
+
export interface EngineState {
|
|
11
|
+
phase: string;
|
|
12
|
+
currentMilestoneId: string | null;
|
|
13
|
+
activeSliceId: string | null;
|
|
14
|
+
activeTaskId: string | null;
|
|
15
|
+
isComplete: boolean;
|
|
16
|
+
/** Opaque engine-specific state — never narrowed to a GSD-specific type. */
|
|
17
|
+
raw: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A unit of work the engine wants the agent to execute. */
|
|
21
|
+
export interface StepContract {
|
|
22
|
+
unitType: string;
|
|
23
|
+
unitId: string;
|
|
24
|
+
prompt: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** UI-facing metadata for progress display. */
|
|
28
|
+
export interface DisplayMetadata {
|
|
29
|
+
engineLabel: string;
|
|
30
|
+
currentPhase: string;
|
|
31
|
+
progressSummary: string;
|
|
32
|
+
stepCount: { completed: number; total: number } | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Discriminated union: what the engine tells the loop to do next.
|
|
37
|
+
*
|
|
38
|
+
* - `dispatch` — execute a step
|
|
39
|
+
* - `stop` — halt the loop with a reason and severity
|
|
40
|
+
* - `skip` — nothing to do right now, advance without executing
|
|
41
|
+
*/
|
|
42
|
+
export type EngineDispatchAction =
|
|
43
|
+
| { action: "dispatch"; step: StepContract }
|
|
44
|
+
| { action: "stop"; reason: string; level: "info" | "warning" | "error" }
|
|
45
|
+
| { action: "skip" };
|
|
46
|
+
|
|
47
|
+
/** Outcome of reconciling state after a step completes. */
|
|
48
|
+
export interface ReconcileResult {
|
|
49
|
+
outcome: "continue" | "milestone-complete" | "pause" | "stop";
|
|
50
|
+
reason?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Recovery strategy when a step fails. */
|
|
54
|
+
export interface RecoveryAction {
|
|
55
|
+
outcome: "retry" | "skip" | "stop" | "pause";
|
|
56
|
+
reason?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Result of closing out a completed unit. */
|
|
60
|
+
export interface CloseoutResult {
|
|
61
|
+
committed: boolean;
|
|
62
|
+
artifacts: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Record of a completed execution step. */
|
|
66
|
+
export interface CompletedStep {
|
|
67
|
+
unitType: string;
|
|
68
|
+
unitId: string;
|
|
69
|
+
startedAt: number;
|
|
70
|
+
finishedAt: number;
|
|
71
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* execution-policy.ts — ExecutionPolicy interface.
|
|
3
|
+
*
|
|
4
|
+
* Defines the policy layer that governs model selection, verification,
|
|
5
|
+
* recovery, and closeout for each execution step. Imports only from
|
|
6
|
+
* the leaf-node engine-types.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { RecoveryAction, CloseoutResult } from "./engine-types.js";
|
|
10
|
+
|
|
11
|
+
/** Policy governing how each step is executed, verified, and closed out. */
|
|
12
|
+
export interface ExecutionPolicy {
|
|
13
|
+
/** Prepare the workspace before a milestone begins (e.g. worktree setup). */
|
|
14
|
+
prepareWorkspace(basePath: string, milestoneId: string): Promise<void>;
|
|
15
|
+
|
|
16
|
+
/** Select the model tier for a given unit. Returns null to use defaults. */
|
|
17
|
+
selectModel(
|
|
18
|
+
unitType: string,
|
|
19
|
+
unitId: string,
|
|
20
|
+
context: { basePath: string },
|
|
21
|
+
): Promise<{ tier: string; modelDowngraded: boolean } | null>;
|
|
22
|
+
|
|
23
|
+
/** Verify unit output. Returns disposition for the loop. */
|
|
24
|
+
verify(
|
|
25
|
+
unitType: string,
|
|
26
|
+
unitId: string,
|
|
27
|
+
context: { basePath: string },
|
|
28
|
+
): Promise<"continue" | "retry" | "pause">;
|
|
29
|
+
|
|
30
|
+
/** Determine recovery action when a unit fails. */
|
|
31
|
+
recover(
|
|
32
|
+
unitType: string,
|
|
33
|
+
unitId: string,
|
|
34
|
+
context: { basePath: string },
|
|
35
|
+
): Promise<RecoveryAction>;
|
|
36
|
+
|
|
37
|
+
/** Close out a completed unit (commit, snapshot, artifact capture). */
|
|
38
|
+
closeout(
|
|
39
|
+
unitType: string,
|
|
40
|
+
unitId: string,
|
|
41
|
+
context: { basePath: string; startedAt: number },
|
|
42
|
+
): Promise<CloseoutResult>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graph.ts — Pure data module for GRAPH.yaml workflow step tracking.
|
|
3
|
+
*
|
|
4
|
+
* Provides types and functions for reading, writing, and querying the
|
|
5
|
+
* step graph that drives CustomWorkflowEngine. Zero engine dependencies.
|
|
6
|
+
*
|
|
7
|
+
* GRAPH.yaml lives in a run directory and tracks step statuses
|
|
8
|
+
* (pending → active → complete) with optional dependency edges.
|
|
9
|
+
*
|
|
10
|
+
* Observability:
|
|
11
|
+
* - readGraph/writeGraph use YAML on disk — human-readable, diffable,
|
|
12
|
+
* inspectable with `cat` or any YAML viewer.
|
|
13
|
+
* - Each GraphStep has status, startedAt, finishedAt fields visible in GRAPH.yaml.
|
|
14
|
+
* - writeGraph uses atomic write (tmp + rename) for crash safety.
|
|
15
|
+
* - All operations are immutable — callers always get a new graph object.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { parse, stringify } from "yaml";
|
|
19
|
+
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import type { WorkflowDefinition } from "./definition-loader.js";
|
|
22
|
+
|
|
23
|
+
// ─── Types ───────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface GraphStep {
|
|
26
|
+
/** Unique step identifier within the workflow. */
|
|
27
|
+
id: string;
|
|
28
|
+
/** Human-readable step title. */
|
|
29
|
+
title: string;
|
|
30
|
+
/** Current status: pending → active → complete → expanded (iterate parent). */
|
|
31
|
+
status: "pending" | "active" | "complete" | "expanded";
|
|
32
|
+
/** The prompt to dispatch for this step. */
|
|
33
|
+
prompt: string;
|
|
34
|
+
/** IDs of steps that must be "complete" before this step can run. */
|
|
35
|
+
dependsOn: string[];
|
|
36
|
+
/** For iteration instances: ID of the parent step that was expanded. */
|
|
37
|
+
parentStepId?: string;
|
|
38
|
+
/** ISO timestamp when the step started executing. */
|
|
39
|
+
startedAt?: string;
|
|
40
|
+
/** ISO timestamp when the step finished executing. */
|
|
41
|
+
finishedAt?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WorkflowGraph {
|
|
45
|
+
/** Ordered list of steps in the workflow. */
|
|
46
|
+
steps: GraphStep[];
|
|
47
|
+
/** Workflow metadata. */
|
|
48
|
+
metadata: {
|
|
49
|
+
name: string;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── YAML schema mapping ─────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
const GRAPH_FILENAME = "GRAPH.yaml";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Internal YAML shape — uses snake_case for YAML keys.
|
|
60
|
+
* Converted to/from the camelCase TypeScript types on read/write.
|
|
61
|
+
*/
|
|
62
|
+
interface YamlStep {
|
|
63
|
+
id: string;
|
|
64
|
+
title: string;
|
|
65
|
+
status: string;
|
|
66
|
+
prompt: string;
|
|
67
|
+
depends_on?: string[];
|
|
68
|
+
parent_step_id?: string;
|
|
69
|
+
started_at?: string;
|
|
70
|
+
finished_at?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface YamlGraph {
|
|
74
|
+
steps: YamlStep[];
|
|
75
|
+
metadata: { name: string; created_at: string };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Functions ───────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read and parse GRAPH.yaml from a run directory.
|
|
82
|
+
*
|
|
83
|
+
* @param runDir — directory containing GRAPH.yaml
|
|
84
|
+
* @returns Parsed workflow graph
|
|
85
|
+
* @throws Error if file doesn't exist or YAML is malformed
|
|
86
|
+
*/
|
|
87
|
+
export function readGraph(runDir: string): WorkflowGraph {
|
|
88
|
+
const filePath = join(runDir, GRAPH_FILENAME);
|
|
89
|
+
if (!existsSync(filePath)) {
|
|
90
|
+
throw new Error(`GRAPH.yaml not found: ${filePath}`);
|
|
91
|
+
}
|
|
92
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
93
|
+
const yaml = parse(raw) as YamlGraph;
|
|
94
|
+
|
|
95
|
+
if (!yaml?.steps || !Array.isArray(yaml.steps)) {
|
|
96
|
+
throw new Error(`Invalid GRAPH.yaml: missing or invalid 'steps' array in ${filePath}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
steps: yaml.steps.map((s) => ({
|
|
101
|
+
id: s.id,
|
|
102
|
+
title: s.title,
|
|
103
|
+
status: s.status as GraphStep["status"],
|
|
104
|
+
prompt: s.prompt,
|
|
105
|
+
dependsOn: s.depends_on ?? [],
|
|
106
|
+
...(s.parent_step_id != null ? { parentStepId: s.parent_step_id } : {}),
|
|
107
|
+
...(s.started_at != null ? { startedAt: s.started_at } : {}),
|
|
108
|
+
...(s.finished_at != null ? { finishedAt: s.finished_at } : {}),
|
|
109
|
+
})),
|
|
110
|
+
metadata: {
|
|
111
|
+
name: yaml.metadata?.name ?? "unnamed",
|
|
112
|
+
createdAt: yaml.metadata?.created_at ?? new Date().toISOString(),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Write a workflow graph to GRAPH.yaml in a run directory.
|
|
119
|
+
* Creates the directory if it doesn't exist. Write is atomic (write + rename).
|
|
120
|
+
*
|
|
121
|
+
* @param runDir — directory to write GRAPH.yaml into
|
|
122
|
+
* @param graph — the workflow graph to serialize
|
|
123
|
+
*/
|
|
124
|
+
export function writeGraph(runDir: string, graph: WorkflowGraph): void {
|
|
125
|
+
if (!existsSync(runDir)) {
|
|
126
|
+
mkdirSync(runDir, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const yamlData: YamlGraph = {
|
|
130
|
+
steps: graph.steps.map((s) => ({
|
|
131
|
+
id: s.id,
|
|
132
|
+
title: s.title,
|
|
133
|
+
status: s.status,
|
|
134
|
+
prompt: s.prompt,
|
|
135
|
+
depends_on: s.dependsOn.length > 0 ? s.dependsOn : undefined,
|
|
136
|
+
parent_step_id: s.parentStepId ?? undefined,
|
|
137
|
+
started_at: s.startedAt ?? undefined,
|
|
138
|
+
finished_at: s.finishedAt ?? undefined,
|
|
139
|
+
})) as YamlStep[],
|
|
140
|
+
metadata: {
|
|
141
|
+
name: graph.metadata.name,
|
|
142
|
+
created_at: graph.metadata.createdAt,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const filePath = join(runDir, GRAPH_FILENAME);
|
|
147
|
+
const tmpPath = filePath + ".tmp";
|
|
148
|
+
const content = stringify(yamlData);
|
|
149
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
150
|
+
// Atomic rename for crash safety
|
|
151
|
+
renameSync(tmpPath, filePath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get the next pending step whose dependencies are all complete.
|
|
156
|
+
*
|
|
157
|
+
* Returns the first step (in array order) with status "pending" where
|
|
158
|
+
* every step in its `dependsOn` list has status "complete".
|
|
159
|
+
*
|
|
160
|
+
* @param graph — the workflow graph to query
|
|
161
|
+
* @returns The next dispatchable step, or null if none available
|
|
162
|
+
*/
|
|
163
|
+
export function getNextPendingStep(graph: WorkflowGraph): GraphStep | null {
|
|
164
|
+
const statusMap = new Map(graph.steps.map((s) => [s.id, s.status]));
|
|
165
|
+
|
|
166
|
+
for (const step of graph.steps) {
|
|
167
|
+
if (step.status !== "pending") continue;
|
|
168
|
+
const depsComplete = step.dependsOn.every(
|
|
169
|
+
(depId) => statusMap.get(depId) === "complete",
|
|
170
|
+
);
|
|
171
|
+
if (depsComplete) return step;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Return a new graph with the specified step marked as "complete".
|
|
179
|
+
* Immutable — does not mutate the input graph.
|
|
180
|
+
*
|
|
181
|
+
* @param graph — the current workflow graph
|
|
182
|
+
* @param stepId — ID of the step to mark complete
|
|
183
|
+
* @returns New graph with the step's status set to "complete"
|
|
184
|
+
* @throws Error if stepId is not found in the graph
|
|
185
|
+
*/
|
|
186
|
+
export function markStepComplete(
|
|
187
|
+
graph: WorkflowGraph,
|
|
188
|
+
stepId: string,
|
|
189
|
+
): WorkflowGraph {
|
|
190
|
+
const found = graph.steps.some((s) => s.id === stepId);
|
|
191
|
+
if (!found) {
|
|
192
|
+
throw new Error(`Step not found: ${stepId}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
...graph,
|
|
197
|
+
steps: graph.steps.map((s) =>
|
|
198
|
+
s.id === stepId
|
|
199
|
+
? { ...s, status: "complete" as const, finishedAt: new Date().toISOString() }
|
|
200
|
+
: s,
|
|
201
|
+
),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Iteration expansion ─────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Expand an iterate step into concrete instances. Pure and deterministic —
|
|
209
|
+
* identical inputs always produce identical output.
|
|
210
|
+
*
|
|
211
|
+
* Given a parent step with status "pending" and an array of matched items,
|
|
212
|
+
* creates one instance step per item, marks the parent as "expanded", and
|
|
213
|
+
* rewrites any downstream dependsOn references from the parent ID to the
|
|
214
|
+
* full set of instance IDs.
|
|
215
|
+
*
|
|
216
|
+
* @param graph — the current workflow graph (not mutated)
|
|
217
|
+
* @param stepId — ID of the iterate step to expand
|
|
218
|
+
* @param items — matched items from the source artifact
|
|
219
|
+
* @param promptTemplate — template with {{item}} placeholders
|
|
220
|
+
* @returns New WorkflowGraph with instances inserted and deps rewritten
|
|
221
|
+
* @throws Error if stepId not found or step is not pending
|
|
222
|
+
*/
|
|
223
|
+
export function expandIteration(
|
|
224
|
+
graph: WorkflowGraph,
|
|
225
|
+
stepId: string,
|
|
226
|
+
items: string[],
|
|
227
|
+
promptTemplate: string,
|
|
228
|
+
): WorkflowGraph {
|
|
229
|
+
const parentIndex = graph.steps.findIndex((s) => s.id === stepId);
|
|
230
|
+
if (parentIndex === -1) {
|
|
231
|
+
throw new Error(`expandIteration: step not found: ${stepId}`);
|
|
232
|
+
}
|
|
233
|
+
const parentStep = graph.steps[parentIndex];
|
|
234
|
+
if (parentStep.status !== "pending") {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`expandIteration: step "${stepId}" has status "${parentStep.status}", expected "pending"`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Create instance steps
|
|
241
|
+
const instanceIds: string[] = [];
|
|
242
|
+
const instances: GraphStep[] = items.map((item, i) => {
|
|
243
|
+
const instanceId = `${stepId}--${String(i + 1).padStart(3, "0")}`;
|
|
244
|
+
instanceIds.push(instanceId);
|
|
245
|
+
return {
|
|
246
|
+
id: instanceId,
|
|
247
|
+
title: `${parentStep.title}: ${item}`,
|
|
248
|
+
status: "pending" as const,
|
|
249
|
+
prompt: promptTemplate.replace(/\{\{item\}\}/g, () => item),
|
|
250
|
+
dependsOn: [...parentStep.dependsOn],
|
|
251
|
+
parentStepId: stepId,
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Build new steps array: copy everything, mark parent as expanded,
|
|
256
|
+
// insert instances right after the parent, rewrite downstream deps.
|
|
257
|
+
const newSteps: GraphStep[] = [];
|
|
258
|
+
for (let i = 0; i < graph.steps.length; i++) {
|
|
259
|
+
if (i === parentIndex) {
|
|
260
|
+
// Mark parent as expanded
|
|
261
|
+
newSteps.push({ ...parentStep, status: "expanded" as const });
|
|
262
|
+
// Insert instances immediately after parent
|
|
263
|
+
newSteps.push(...instances);
|
|
264
|
+
} else {
|
|
265
|
+
const step = graph.steps[i];
|
|
266
|
+
// Rewrite dependsOn: replace parent ID with all instance IDs
|
|
267
|
+
const hasDep = step.dependsOn.includes(stepId);
|
|
268
|
+
if (hasDep) {
|
|
269
|
+
const rewritten = step.dependsOn.flatMap((dep) =>
|
|
270
|
+
dep === stepId ? instanceIds : [dep],
|
|
271
|
+
);
|
|
272
|
+
newSteps.push({ ...step, dependsOn: rewritten });
|
|
273
|
+
} else {
|
|
274
|
+
newSteps.push(step);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
...graph,
|
|
281
|
+
steps: newSteps,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ─── Definition → Graph conversion ──────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Convert a parsed WorkflowDefinition into a WorkflowGraph with all
|
|
289
|
+
* steps in "pending" status. Used by run-manager to generate the initial
|
|
290
|
+
* GRAPH.yaml for a new run.
|
|
291
|
+
*
|
|
292
|
+
* @param def — a validated WorkflowDefinition from definition-loader
|
|
293
|
+
* @returns WorkflowGraph with pending steps and metadata from the definition
|
|
294
|
+
*/
|
|
295
|
+
export function initializeGraph(def: WorkflowDefinition): WorkflowGraph {
|
|
296
|
+
return {
|
|
297
|
+
steps: def.steps.map((s) => ({
|
|
298
|
+
id: s.id,
|
|
299
|
+
title: s.name,
|
|
300
|
+
status: "pending" as const,
|
|
301
|
+
prompt: s.prompt,
|
|
302
|
+
dependsOn: s.requires ?? [],
|
|
303
|
+
})),
|
|
304
|
+
metadata: {
|
|
305
|
+
name: def.name,
|
|
306
|
+
createdAt: new Date().toISOString(),
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** @deprecated Use initializeGraph instead. Kept for backward compatibility. */
|
|
312
|
+
export { initializeGraph as graphFromDefinition };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-manager.ts — Create and list isolated workflow run directories.
|
|
3
|
+
*
|
|
4
|
+
* Each run lives under `.gsd/workflow-runs/<name>/<timestamp>/` and contains:
|
|
5
|
+
* - DEFINITION.yaml — frozen snapshot of the workflow definition at run-creation time
|
|
6
|
+
* - GRAPH.yaml — initialized step graph with all steps pending
|
|
7
|
+
* - PARAMS.json — (optional) parameter overrides used for this run
|
|
8
|
+
*
|
|
9
|
+
* Observability:
|
|
10
|
+
* - All run state is on disk in human-readable YAML/JSON — inspectable with cat/less.
|
|
11
|
+
* - `listRuns()` returns structured metadata including step counts and overall status.
|
|
12
|
+
* - Timestamp directory names are filesystem-safe (ISO with hyphens replacing colons).
|
|
13
|
+
* - Errors include the full path context for diagnosis.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdirSync, writeFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { stringify } from "yaml";
|
|
19
|
+
import { loadDefinition, substituteParams } from "./definition-loader.js";
|
|
20
|
+
import { initializeGraph, writeGraph, readGraph } from "./graph.js";
|
|
21
|
+
import type { WorkflowDefinition } from "./definition-loader.js";
|
|
22
|
+
import type { WorkflowGraph } from "./graph.js";
|
|
23
|
+
|
|
24
|
+
// ─── Types ───────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export interface RunMetadata {
|
|
27
|
+
/** Workflow definition name. */
|
|
28
|
+
name: string;
|
|
29
|
+
/** Filesystem-safe timestamp string used as dir name. */
|
|
30
|
+
timestamp: string;
|
|
31
|
+
/** Full path to the run directory. */
|
|
32
|
+
runDir: string;
|
|
33
|
+
/** Step counts derived from GRAPH.yaml. */
|
|
34
|
+
steps: { total: number; completed: number; pending: number; active: number };
|
|
35
|
+
/** Overall status derived from step states. */
|
|
36
|
+
status: "pending" | "running" | "complete";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Constants ───────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const RUNS_DIR = "workflow-runs";
|
|
42
|
+
const DEFS_DIR = "workflow-defs";
|
|
43
|
+
|
|
44
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate a filesystem-safe timestamp: `YYYY-MM-DDTHH-MM-SS`.
|
|
48
|
+
* Replaces colons with hyphens so the string is safe as a directory name
|
|
49
|
+
* on all platforms (Windows forbids colons in paths).
|
|
50
|
+
*/
|
|
51
|
+
function makeTimestamp(date: Date = new Date()): string {
|
|
52
|
+
return date.toISOString().replace(/:/g, "-").replace(/\.\d{3}Z$/, "");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Derive overall status from a graph's step statuses.
|
|
57
|
+
*/
|
|
58
|
+
function deriveStatus(graph: WorkflowGraph): "pending" | "running" | "complete" {
|
|
59
|
+
const hasActive = graph.steps.some((s) => s.status === "active");
|
|
60
|
+
const allDone = graph.steps.every(
|
|
61
|
+
(s) => s.status === "complete" || s.status === "expanded",
|
|
62
|
+
);
|
|
63
|
+
if (allDone) return "complete";
|
|
64
|
+
if (hasActive) return "running";
|
|
65
|
+
return "pending";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a new isolated run directory for a workflow definition.
|
|
72
|
+
*
|
|
73
|
+
* 1. Loads the definition from `<basePath>/.gsd/workflow-defs/<defName>.yaml`
|
|
74
|
+
* 2. Applies parameter substitution if overrides are provided
|
|
75
|
+
* 3. Creates `<basePath>/.gsd/workflow-runs/<defName>/<timestamp>/`
|
|
76
|
+
* 4. Writes frozen DEFINITION.yaml, initialized GRAPH.yaml, and optional PARAMS.json
|
|
77
|
+
*
|
|
78
|
+
* @param basePath — project root directory
|
|
79
|
+
* @param defName — definition filename (without .yaml extension)
|
|
80
|
+
* @param overrides — optional parameter overrides (merged with definition defaults)
|
|
81
|
+
* @returns Full path to the created run directory
|
|
82
|
+
* @throws Error if the definition file doesn't exist or is invalid
|
|
83
|
+
*/
|
|
84
|
+
export function createRun(
|
|
85
|
+
basePath: string,
|
|
86
|
+
defName: string,
|
|
87
|
+
overrides?: Record<string, string>,
|
|
88
|
+
): string {
|
|
89
|
+
const defsDir = join(basePath, ".gsd", DEFS_DIR);
|
|
90
|
+
|
|
91
|
+
// Load and validate the definition
|
|
92
|
+
const rawDef = loadDefinition(defsDir, defName);
|
|
93
|
+
|
|
94
|
+
// Apply parameter substitution if overrides provided
|
|
95
|
+
const def: WorkflowDefinition = overrides
|
|
96
|
+
? substituteParams(rawDef, overrides)
|
|
97
|
+
: substituteParams(rawDef); // still resolve default params if any
|
|
98
|
+
|
|
99
|
+
// Create the run directory
|
|
100
|
+
const timestamp = makeTimestamp();
|
|
101
|
+
const runDir = join(basePath, ".gsd", RUNS_DIR, defName, timestamp);
|
|
102
|
+
mkdirSync(runDir, { recursive: true });
|
|
103
|
+
|
|
104
|
+
// Freeze the definition as DEFINITION.yaml
|
|
105
|
+
writeFileSync(join(runDir, "DEFINITION.yaml"), stringify(def), "utf-8");
|
|
106
|
+
|
|
107
|
+
// Initialize and write GRAPH.yaml
|
|
108
|
+
const graph = initializeGraph(def);
|
|
109
|
+
writeGraph(runDir, graph);
|
|
110
|
+
|
|
111
|
+
// Write PARAMS.json if overrides were provided
|
|
112
|
+
if (overrides && Object.keys(overrides).length > 0) {
|
|
113
|
+
writeFileSync(
|
|
114
|
+
join(runDir, "PARAMS.json"),
|
|
115
|
+
JSON.stringify(overrides, null, 2),
|
|
116
|
+
"utf-8",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return runDir;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* List existing workflow runs with metadata.
|
|
125
|
+
*
|
|
126
|
+
* Scans `<basePath>/.gsd/workflow-runs/` for run directories. Each run's
|
|
127
|
+
* GRAPH.yaml is read to derive step counts and overall status.
|
|
128
|
+
*
|
|
129
|
+
* @param basePath — project root directory
|
|
130
|
+
* @param defName — optional filter: only list runs for this definition name
|
|
131
|
+
* @returns Array of run metadata, sorted newest-first within each definition
|
|
132
|
+
*/
|
|
133
|
+
export function listRuns(basePath: string, defName?: string): RunMetadata[] {
|
|
134
|
+
const runsRoot = join(basePath, ".gsd", RUNS_DIR);
|
|
135
|
+
if (!existsSync(runsRoot)) return [];
|
|
136
|
+
|
|
137
|
+
const results: RunMetadata[] = [];
|
|
138
|
+
|
|
139
|
+
// Get workflow name directories
|
|
140
|
+
const nameDirs = defName ? [defName] : readdirSync(runsRoot).filter((entry) => {
|
|
141
|
+
const full = join(runsRoot, entry);
|
|
142
|
+
return statSync(full).isDirectory();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
for (const name of nameDirs) {
|
|
146
|
+
const nameDir = join(runsRoot, name);
|
|
147
|
+
if (!existsSync(nameDir)) continue;
|
|
148
|
+
|
|
149
|
+
const timestamps = readdirSync(nameDir).filter((entry) => {
|
|
150
|
+
const full = join(nameDir, entry);
|
|
151
|
+
return statSync(full).isDirectory();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Sort newest-first (ISO strings sort lexicographically)
|
|
155
|
+
timestamps.sort().reverse();
|
|
156
|
+
|
|
157
|
+
for (const ts of timestamps) {
|
|
158
|
+
const runDir = join(nameDir, ts);
|
|
159
|
+
try {
|
|
160
|
+
const graph = readGraph(runDir);
|
|
161
|
+
const total = graph.steps.length;
|
|
162
|
+
const completed = graph.steps.filter((s) => s.status === "complete").length;
|
|
163
|
+
const pending = graph.steps.filter((s) => s.status === "pending").length;
|
|
164
|
+
const active = graph.steps.filter((s) => s.status === "active").length;
|
|
165
|
+
|
|
166
|
+
results.push({
|
|
167
|
+
name,
|
|
168
|
+
timestamp: ts,
|
|
169
|
+
runDir,
|
|
170
|
+
steps: { total, completed, pending, active },
|
|
171
|
+
status: deriveStatus(graph),
|
|
172
|
+
});
|
|
173
|
+
} catch {
|
|
174
|
+
// Skip runs with invalid/missing GRAPH.yaml
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return results;
|
|
180
|
+
}
|