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.
Files changed (123) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/extensions/gsd/auto/loop.js +80 -0
  3. package/dist/resources/extensions/gsd/auto/phases.js +2 -2
  4. package/dist/resources/extensions/gsd/auto/session.js +6 -0
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +2 -0
  6. package/dist/resources/extensions/gsd/auto.js +28 -1
  7. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
  8. package/dist/resources/extensions/gsd/commands/catalog.js +32 -0
  9. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
  10. package/dist/resources/extensions/gsd/context-injector.js +74 -0
  11. package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
  12. package/dist/resources/extensions/gsd/custom-verification.js +145 -0
  13. package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
  14. package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
  15. package/dist/resources/extensions/gsd/definition-loader.js +352 -0
  16. package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
  17. package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
  18. package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
  19. package/dist/resources/extensions/gsd/engine-types.js +8 -0
  20. package/dist/resources/extensions/gsd/execution-policy.js +8 -0
  21. package/dist/resources/extensions/gsd/graph.js +225 -0
  22. package/dist/resources/extensions/gsd/run-manager.js +134 -0
  23. package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
  24. package/dist/resources/skills/create-workflow/SKILL.md +103 -0
  25. package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  26. package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
  27. package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  28. package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  29. package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  30. package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  31. package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  32. package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  33. package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  34. package/dist/web/standalone/.next/BUILD_ID +1 -1
  35. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  36. package/dist/web/standalone/.next/build-manifest.json +2 -2
  37. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  38. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  39. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.html +1 -1
  55. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  62. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  63. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  64. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  65. package/package.json +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -0
  68. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  69. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +5 -0
  70. package/src/resources/extensions/gsd/auto/loop.ts +91 -0
  71. package/src/resources/extensions/gsd/auto/phases.ts +2 -2
  72. package/src/resources/extensions/gsd/auto/session.ts +6 -0
  73. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  74. package/src/resources/extensions/gsd/auto.ts +31 -1
  75. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
  76. package/src/resources/extensions/gsd/commands/catalog.ts +32 -0
  77. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
  78. package/src/resources/extensions/gsd/context-injector.ts +100 -0
  79. package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
  80. package/src/resources/extensions/gsd/custom-verification.ts +180 -0
  81. package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
  82. package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
  83. package/src/resources/extensions/gsd/definition-loader.ts +462 -0
  84. package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
  85. package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
  86. package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
  87. package/src/resources/extensions/gsd/engine-types.ts +71 -0
  88. package/src/resources/extensions/gsd/execution-policy.ts +43 -0
  89. package/src/resources/extensions/gsd/graph.ts +312 -0
  90. package/src/resources/extensions/gsd/run-manager.ts +180 -0
  91. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +100 -118
  92. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -2
  93. package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
  94. package/src/resources/extensions/gsd/tests/captures.test.ts +12 -1
  95. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
  96. package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
  97. package/src/resources/extensions/gsd/tests/continue-here.test.ts +20 -20
  98. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
  99. package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
  100. package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
  101. package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
  102. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
  103. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
  104. package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
  105. package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
  106. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
  107. package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
  108. package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
  109. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
  110. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +195 -105
  111. package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
  112. package/src/resources/skills/create-workflow/SKILL.md +103 -0
  113. package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  114. package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
  115. package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  116. package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  117. package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  118. package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  119. package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  120. package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  121. package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  122. /package/dist/web/standalone/.next/static/{JBSIr4fSfHXs5g5x2ZBSC → K7GYOOPvQWX6TKYEKhODM}/_buildManifest.js +0 -0
  123. /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
+ }