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.
- 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 +15 -15
- 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 +15 -15
- 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/package.json +1 -1
- package/pkg/package.json +1 -1
- 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/bundled-workflow-defs.test.ts +180 -0
- 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/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/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/{EnGUNqHeGbE0tuuUkTJVA → PXrI5DoWsm7rwAVnEU2rD}/_buildManifest.js +0 -0
- /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 };
|