gsd-pi 2.41.0-dev.cac69f9 → 2.42.0-dev.97e9e30

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 (115) hide show
  1. package/dist/resources/extensions/gsd/auto/loop.js +80 -0
  2. package/dist/resources/extensions/gsd/auto/phases.js +2 -2
  3. package/dist/resources/extensions/gsd/auto/session.js +6 -0
  4. package/dist/resources/extensions/gsd/auto-dashboard.js +2 -0
  5. package/dist/resources/extensions/gsd/auto.js +28 -1
  6. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
  7. package/dist/resources/extensions/gsd/commands/catalog.js +32 -0
  8. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
  9. package/dist/resources/extensions/gsd/context-injector.js +74 -0
  10. package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
  11. package/dist/resources/extensions/gsd/custom-verification.js +145 -0
  12. package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
  14. package/dist/resources/extensions/gsd/definition-loader.js +352 -0
  15. package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
  16. package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
  17. package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
  18. package/dist/resources/extensions/gsd/engine-types.js +8 -0
  19. package/dist/resources/extensions/gsd/execution-policy.js +8 -0
  20. package/dist/resources/extensions/gsd/graph.js +225 -0
  21. package/dist/resources/extensions/gsd/run-manager.js +134 -0
  22. package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
  23. package/dist/resources/skills/create-workflow/SKILL.md +103 -0
  24. package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  25. package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
  26. package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  27. package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  28. package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  29. package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  30. package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  31. package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  32. package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  33. package/dist/web/standalone/.next/BUILD_ID +1 -1
  34. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  35. package/dist/web/standalone/.next/build-manifest.json +2 -2
  36. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  37. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  38. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.html +1 -1
  54. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  61. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  62. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  63. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  64. package/package.json +1 -1
  65. package/packages/pi-coding-agent/package.json +1 -1
  66. package/pkg/package.json +1 -1
  67. package/src/resources/extensions/gsd/auto/loop.ts +91 -0
  68. package/src/resources/extensions/gsd/auto/phases.ts +2 -2
  69. package/src/resources/extensions/gsd/auto/session.ts +6 -0
  70. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  71. package/src/resources/extensions/gsd/auto.ts +31 -1
  72. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
  73. package/src/resources/extensions/gsd/commands/catalog.ts +32 -0
  74. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
  75. package/src/resources/extensions/gsd/context-injector.ts +100 -0
  76. package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
  77. package/src/resources/extensions/gsd/custom-verification.ts +180 -0
  78. package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
  79. package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
  80. package/src/resources/extensions/gsd/definition-loader.ts +462 -0
  81. package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
  82. package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
  83. package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
  84. package/src/resources/extensions/gsd/engine-types.ts +71 -0
  85. package/src/resources/extensions/gsd/execution-policy.ts +43 -0
  86. package/src/resources/extensions/gsd/graph.ts +312 -0
  87. package/src/resources/extensions/gsd/run-manager.ts +180 -0
  88. package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
  89. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
  90. package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
  91. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
  92. package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
  93. package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
  94. package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
  95. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
  96. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
  97. package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
  98. package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
  99. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
  100. package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
  101. package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
  102. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
  103. package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
  104. package/src/resources/skills/create-workflow/SKILL.md +103 -0
  105. package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  106. package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
  107. package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  108. package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  109. package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  110. package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  111. package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  112. package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  113. package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  114. /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → PXrI5DoWsm7rwAVnEU2rD}/_buildManifest.js +0 -0
  115. /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → PXrI5DoWsm7rwAVnEU2rD}/_ssgManifest.js +0 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * dev-execution-policy.ts — DevExecutionPolicy implementation.
3
+ *
4
+ * Stub policy for the dev engine. All methods return safe defaults.
5
+ * Real verification/closeout continues running through phases.ts via LoopDeps.
6
+ * Wiring this policy into the loop is S04's responsibility.
7
+ */
8
+
9
+ import type { ExecutionPolicy } from "./execution-policy.js";
10
+ import type { RecoveryAction, CloseoutResult } from "./engine-types.js";
11
+
12
+ export class DevExecutionPolicy implements ExecutionPolicy {
13
+ async prepareWorkspace(
14
+ _basePath: string,
15
+ _milestoneId: string,
16
+ ): Promise<void> {
17
+ // no-op — workspace preparation handled by existing GSD logic
18
+ }
19
+
20
+ async selectModel(
21
+ _unitType: string,
22
+ _unitId: string,
23
+ _context: { basePath: string },
24
+ ): Promise<{ tier: string; modelDowngraded: boolean } | null> {
25
+ return null; // use default model selection
26
+ }
27
+
28
+ async verify(
29
+ _unitType: string,
30
+ _unitId: string,
31
+ _context: { basePath: string },
32
+ ): Promise<"continue" | "retry" | "pause"> {
33
+ return "continue";
34
+ }
35
+
36
+ async recover(
37
+ _unitType: string,
38
+ _unitId: string,
39
+ _context: { basePath: string },
40
+ ): Promise<RecoveryAction> {
41
+ return { outcome: "retry" };
42
+ }
43
+
44
+ async closeout(
45
+ _unitType: string,
46
+ _unitId: string,
47
+ _context: { basePath: string; startedAt: number },
48
+ ): Promise<CloseoutResult> {
49
+ return { committed: false, artifacts: [] };
50
+ }
51
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * dev-workflow-engine.ts — DevWorkflowEngine implementation.
3
+ *
4
+ * Implements WorkflowEngine by delegating to existing GSD state derivation
5
+ * and dispatch logic. This is the "dev" engine — it wraps the current GSD
6
+ * auto-mode behavior behind the engine-polymorphic interface.
7
+ */
8
+
9
+ import type { WorkflowEngine } from "./workflow-engine.js";
10
+ import type {
11
+ EngineState,
12
+ EngineDispatchAction,
13
+ CompletedStep,
14
+ ReconcileResult,
15
+ DisplayMetadata,
16
+ } from "./engine-types.js";
17
+ import type { GSDState } from "./types.js";
18
+ import type { DispatchAction, DispatchContext } from "./auto-dispatch.js";
19
+
20
+ import { deriveState } from "./state.js";
21
+ import { resolveDispatch } from "./auto-dispatch.js";
22
+ import { loadEffectiveGSDPreferences } from "./preferences.js";
23
+
24
+ // ─── Bridge: DispatchAction → EngineDispatchAction ────────────────────────
25
+
26
+ /**
27
+ * Map a GSD-specific DispatchAction (which carries `matchedRule`, `unitType`,
28
+ * etc.) to the engine-generic EngineDispatchAction discriminated union.
29
+ *
30
+ * Exported for unit testing.
31
+ */
32
+ export function bridgeDispatchAction(da: DispatchAction): EngineDispatchAction {
33
+ switch (da.action) {
34
+ case "dispatch":
35
+ return {
36
+ action: "dispatch",
37
+ step: {
38
+ unitType: da.unitType,
39
+ unitId: da.unitId,
40
+ prompt: da.prompt,
41
+ },
42
+ };
43
+ case "stop":
44
+ return {
45
+ action: "stop",
46
+ reason: da.reason,
47
+ level: da.level,
48
+ };
49
+ case "skip":
50
+ return { action: "skip" };
51
+ }
52
+ }
53
+
54
+ // ─── DevWorkflowEngine ───────────────────────────────────────────────────
55
+
56
+ export class DevWorkflowEngine implements WorkflowEngine {
57
+ readonly engineId = "dev" as const;
58
+
59
+ async deriveState(basePath: string): Promise<EngineState> {
60
+ const gsd: GSDState = await deriveState(basePath);
61
+ return {
62
+ phase: gsd.phase,
63
+ currentMilestoneId: gsd.activeMilestone?.id ?? null,
64
+ activeSliceId: gsd.activeSlice?.id ?? null,
65
+ activeTaskId: gsd.activeTask?.id ?? null,
66
+ isComplete: gsd.phase === "complete",
67
+ raw: gsd,
68
+ };
69
+ }
70
+
71
+ async resolveDispatch(
72
+ state: EngineState,
73
+ context: { basePath: string },
74
+ ): Promise<EngineDispatchAction> {
75
+ const gsd = state.raw as GSDState;
76
+ const mid = gsd.activeMilestone?.id ?? "";
77
+ const midTitle = gsd.activeMilestone?.title ?? "";
78
+ const loaded = loadEffectiveGSDPreferences();
79
+ const prefs = loaded?.preferences ?? undefined;
80
+
81
+ const dispatchCtx: DispatchContext = {
82
+ basePath: context.basePath,
83
+ mid,
84
+ midTitle,
85
+ state: gsd,
86
+ prefs,
87
+ };
88
+
89
+ const result = await resolveDispatch(dispatchCtx);
90
+ return bridgeDispatchAction(result);
91
+ }
92
+
93
+ async reconcile(
94
+ state: EngineState,
95
+ _completedStep: CompletedStep,
96
+ ): Promise<ReconcileResult> {
97
+ return {
98
+ outcome: state.isComplete ? "milestone-complete" : "continue",
99
+ };
100
+ }
101
+
102
+ getDisplayMetadata(state: EngineState): DisplayMetadata {
103
+ return {
104
+ engineLabel: "GSD Dev",
105
+ currentPhase: state.phase,
106
+ progressSummary: `${state.currentMilestoneId ?? "no milestone"} / ${state.activeSliceId ?? "—"} / ${state.activeTaskId ?? "—"}`,
107
+ stepCount: null,
108
+ };
109
+ }
110
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * engine-resolver.ts — Route sessions to engine/policy pairs.
3
+ *
4
+ * Routes `null` and `"dev"` engine IDs to the DevWorkflowEngine/DevExecutionPolicy
5
+ * pair. Any other non-null engine ID is treated as a custom workflow engine that
6
+ * reads its state from an `activeRunDir`. Respects `GSD_ENGINE_BYPASS=1` kill
7
+ * switch to skip the engine layer entirely.
8
+ */
9
+
10
+ import type { WorkflowEngine } from "./workflow-engine.js";
11
+ import type { ExecutionPolicy } from "./execution-policy.js";
12
+ import { DevWorkflowEngine } from "./dev-workflow-engine.js";
13
+ import { DevExecutionPolicy } from "./dev-execution-policy.js";
14
+ import { CustomWorkflowEngine } from "./custom-workflow-engine.js";
15
+ import { CustomExecutionPolicy } from "./custom-execution-policy.js";
16
+
17
+ /** A resolved engine + policy pair ready for the auto-loop. */
18
+ export interface ResolvedEngine {
19
+ engine: WorkflowEngine;
20
+ policy: ExecutionPolicy;
21
+ }
22
+
23
+ /**
24
+ * Resolve an engine/policy pair for the given session.
25
+ *
26
+ * - `null` or `"dev"` → DevWorkflowEngine + DevExecutionPolicy
27
+ * - any other non-null ID → CustomWorkflowEngine(activeRunDir) + CustomExecutionPolicy()
28
+ * (requires activeRunDir to be a non-empty string)
29
+ *
30
+ * Note: `GSD_ENGINE_BYPASS=1` is checked in autoLoop before calling this function.
31
+ */
32
+ export function resolveEngine(
33
+ session: { activeEngineId: string | null; activeRunDir?: string | null },
34
+ ): ResolvedEngine {
35
+ const { activeEngineId, activeRunDir } = session;
36
+
37
+ if (activeEngineId === null || activeEngineId === "dev") {
38
+ return {
39
+ engine: new DevWorkflowEngine(),
40
+ policy: new DevExecutionPolicy(),
41
+ };
42
+ }
43
+
44
+ // Any non-null, non-"dev" engine ID is a custom workflow engine.
45
+ // activeRunDir is required — the engine reads GRAPH.yaml from it.
46
+ if (!activeRunDir || typeof activeRunDir !== "string") {
47
+ throw new Error(
48
+ `Custom engine "${activeEngineId}" requires activeRunDir to be a non-empty string, ` +
49
+ `got: ${JSON.stringify(activeRunDir)}`,
50
+ );
51
+ }
52
+
53
+ return {
54
+ engine: new CustomWorkflowEngine(activeRunDir),
55
+ policy: new CustomExecutionPolicy(activeRunDir),
56
+ };
57
+ }
@@ -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 };