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,24 @@
|
|
|
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
|
+
export class DevExecutionPolicy {
|
|
9
|
+
async prepareWorkspace(_basePath, _milestoneId) {
|
|
10
|
+
// no-op — workspace preparation handled by existing GSD logic
|
|
11
|
+
}
|
|
12
|
+
async selectModel(_unitType, _unitId, _context) {
|
|
13
|
+
return null; // use default model selection
|
|
14
|
+
}
|
|
15
|
+
async verify(_unitType, _unitId, _context) {
|
|
16
|
+
return "continue";
|
|
17
|
+
}
|
|
18
|
+
async recover(_unitType, _unitId, _context) {
|
|
19
|
+
return { outcome: "retry" };
|
|
20
|
+
}
|
|
21
|
+
async closeout(_unitType, _unitId, _context) {
|
|
22
|
+
return { committed: false, artifacts: [] };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
import { deriveState } from "./state.js";
|
|
9
|
+
import { resolveDispatch } from "./auto-dispatch.js";
|
|
10
|
+
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
11
|
+
// ─── Bridge: DispatchAction → EngineDispatchAction ────────────────────────
|
|
12
|
+
/**
|
|
13
|
+
* Map a GSD-specific DispatchAction (which carries `matchedRule`, `unitType`,
|
|
14
|
+
* etc.) to the engine-generic EngineDispatchAction discriminated union.
|
|
15
|
+
*
|
|
16
|
+
* Exported for unit testing.
|
|
17
|
+
*/
|
|
18
|
+
export function bridgeDispatchAction(da) {
|
|
19
|
+
switch (da.action) {
|
|
20
|
+
case "dispatch":
|
|
21
|
+
return {
|
|
22
|
+
action: "dispatch",
|
|
23
|
+
step: {
|
|
24
|
+
unitType: da.unitType,
|
|
25
|
+
unitId: da.unitId,
|
|
26
|
+
prompt: da.prompt,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
case "stop":
|
|
30
|
+
return {
|
|
31
|
+
action: "stop",
|
|
32
|
+
reason: da.reason,
|
|
33
|
+
level: da.level,
|
|
34
|
+
};
|
|
35
|
+
case "skip":
|
|
36
|
+
return { action: "skip" };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// ─── DevWorkflowEngine ───────────────────────────────────────────────────
|
|
40
|
+
export class DevWorkflowEngine {
|
|
41
|
+
engineId = "dev";
|
|
42
|
+
async deriveState(basePath) {
|
|
43
|
+
const gsd = await deriveState(basePath);
|
|
44
|
+
return {
|
|
45
|
+
phase: gsd.phase,
|
|
46
|
+
currentMilestoneId: gsd.activeMilestone?.id ?? null,
|
|
47
|
+
activeSliceId: gsd.activeSlice?.id ?? null,
|
|
48
|
+
activeTaskId: gsd.activeTask?.id ?? null,
|
|
49
|
+
isComplete: gsd.phase === "complete",
|
|
50
|
+
raw: gsd,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async resolveDispatch(state, context) {
|
|
54
|
+
const gsd = state.raw;
|
|
55
|
+
const mid = gsd.activeMilestone?.id ?? "";
|
|
56
|
+
const midTitle = gsd.activeMilestone?.title ?? "";
|
|
57
|
+
const loaded = loadEffectiveGSDPreferences();
|
|
58
|
+
const prefs = loaded?.preferences ?? undefined;
|
|
59
|
+
const dispatchCtx = {
|
|
60
|
+
basePath: context.basePath,
|
|
61
|
+
mid,
|
|
62
|
+
midTitle,
|
|
63
|
+
state: gsd,
|
|
64
|
+
prefs,
|
|
65
|
+
};
|
|
66
|
+
const result = await resolveDispatch(dispatchCtx);
|
|
67
|
+
return bridgeDispatchAction(result);
|
|
68
|
+
}
|
|
69
|
+
async reconcile(state, _completedStep) {
|
|
70
|
+
return {
|
|
71
|
+
outcome: state.isComplete ? "milestone-complete" : "continue",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
getDisplayMetadata(state) {
|
|
75
|
+
return {
|
|
76
|
+
engineLabel: "GSD Dev",
|
|
77
|
+
currentPhase: state.phase,
|
|
78
|
+
progressSummary: `${state.currentMilestoneId ?? "no milestone"} / ${state.activeSliceId ?? "—"} / ${state.activeTaskId ?? "—"}`,
|
|
79
|
+
stepCount: null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
import { DevWorkflowEngine } from "./dev-workflow-engine.js";
|
|
10
|
+
import { DevExecutionPolicy } from "./dev-execution-policy.js";
|
|
11
|
+
import { CustomWorkflowEngine } from "./custom-workflow-engine.js";
|
|
12
|
+
import { CustomExecutionPolicy } from "./custom-execution-policy.js";
|
|
13
|
+
/**
|
|
14
|
+
* Resolve an engine/policy pair for the given session.
|
|
15
|
+
*
|
|
16
|
+
* - `null` or `"dev"` → DevWorkflowEngine + DevExecutionPolicy
|
|
17
|
+
* - any other non-null ID → CustomWorkflowEngine(activeRunDir) + CustomExecutionPolicy()
|
|
18
|
+
* (requires activeRunDir to be a non-empty string)
|
|
19
|
+
*
|
|
20
|
+
* Note: `GSD_ENGINE_BYPASS=1` is checked in autoLoop before calling this function.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveEngine(session) {
|
|
23
|
+
const { activeEngineId, activeRunDir } = session;
|
|
24
|
+
if (activeEngineId === null || activeEngineId === "dev") {
|
|
25
|
+
return {
|
|
26
|
+
engine: new DevWorkflowEngine(),
|
|
27
|
+
policy: new DevExecutionPolicy(),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Any non-null, non-"dev" engine ID is a custom workflow engine.
|
|
31
|
+
// activeRunDir is required — the engine reads GRAPH.yaml from it.
|
|
32
|
+
if (!activeRunDir || typeof activeRunDir !== "string") {
|
|
33
|
+
throw new Error(`Custom engine "${activeEngineId}" requires activeRunDir to be a non-empty string, ` +
|
|
34
|
+
`got: ${JSON.stringify(activeRunDir)}`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
engine: new CustomWorkflowEngine(activeRunDir),
|
|
38
|
+
policy: new CustomExecutionPolicy(activeRunDir),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
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
|
+
import { parse, stringify } from "yaml";
|
|
18
|
+
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
// ─── YAML schema mapping ─────────────────────────────────────────────────
|
|
21
|
+
const GRAPH_FILENAME = "GRAPH.yaml";
|
|
22
|
+
// ─── Functions ───────────────────────────────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* Read and parse GRAPH.yaml from a run directory.
|
|
25
|
+
*
|
|
26
|
+
* @param runDir — directory containing GRAPH.yaml
|
|
27
|
+
* @returns Parsed workflow graph
|
|
28
|
+
* @throws Error if file doesn't exist or YAML is malformed
|
|
29
|
+
*/
|
|
30
|
+
export function readGraph(runDir) {
|
|
31
|
+
const filePath = join(runDir, GRAPH_FILENAME);
|
|
32
|
+
if (!existsSync(filePath)) {
|
|
33
|
+
throw new Error(`GRAPH.yaml not found: ${filePath}`);
|
|
34
|
+
}
|
|
35
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
36
|
+
const yaml = parse(raw);
|
|
37
|
+
if (!yaml?.steps || !Array.isArray(yaml.steps)) {
|
|
38
|
+
throw new Error(`Invalid GRAPH.yaml: missing or invalid 'steps' array in ${filePath}`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
steps: yaml.steps.map((s) => ({
|
|
42
|
+
id: s.id,
|
|
43
|
+
title: s.title,
|
|
44
|
+
status: s.status,
|
|
45
|
+
prompt: s.prompt,
|
|
46
|
+
dependsOn: s.depends_on ?? [],
|
|
47
|
+
...(s.parent_step_id != null ? { parentStepId: s.parent_step_id } : {}),
|
|
48
|
+
...(s.started_at != null ? { startedAt: s.started_at } : {}),
|
|
49
|
+
...(s.finished_at != null ? { finishedAt: s.finished_at } : {}),
|
|
50
|
+
})),
|
|
51
|
+
metadata: {
|
|
52
|
+
name: yaml.metadata?.name ?? "unnamed",
|
|
53
|
+
createdAt: yaml.metadata?.created_at ?? new Date().toISOString(),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Write a workflow graph to GRAPH.yaml in a run directory.
|
|
59
|
+
* Creates the directory if it doesn't exist. Write is atomic (write + rename).
|
|
60
|
+
*
|
|
61
|
+
* @param runDir — directory to write GRAPH.yaml into
|
|
62
|
+
* @param graph — the workflow graph to serialize
|
|
63
|
+
*/
|
|
64
|
+
export function writeGraph(runDir, graph) {
|
|
65
|
+
if (!existsSync(runDir)) {
|
|
66
|
+
mkdirSync(runDir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
const yamlData = {
|
|
69
|
+
steps: graph.steps.map((s) => ({
|
|
70
|
+
id: s.id,
|
|
71
|
+
title: s.title,
|
|
72
|
+
status: s.status,
|
|
73
|
+
prompt: s.prompt,
|
|
74
|
+
depends_on: s.dependsOn.length > 0 ? s.dependsOn : undefined,
|
|
75
|
+
parent_step_id: s.parentStepId ?? undefined,
|
|
76
|
+
started_at: s.startedAt ?? undefined,
|
|
77
|
+
finished_at: s.finishedAt ?? undefined,
|
|
78
|
+
})),
|
|
79
|
+
metadata: {
|
|
80
|
+
name: graph.metadata.name,
|
|
81
|
+
created_at: graph.metadata.createdAt,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const filePath = join(runDir, GRAPH_FILENAME);
|
|
85
|
+
const tmpPath = filePath + ".tmp";
|
|
86
|
+
const content = stringify(yamlData);
|
|
87
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
88
|
+
// Atomic rename for crash safety
|
|
89
|
+
renameSync(tmpPath, filePath);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get the next pending step whose dependencies are all complete.
|
|
93
|
+
*
|
|
94
|
+
* Returns the first step (in array order) with status "pending" where
|
|
95
|
+
* every step in its `dependsOn` list has status "complete".
|
|
96
|
+
*
|
|
97
|
+
* @param graph — the workflow graph to query
|
|
98
|
+
* @returns The next dispatchable step, or null if none available
|
|
99
|
+
*/
|
|
100
|
+
export function getNextPendingStep(graph) {
|
|
101
|
+
const statusMap = new Map(graph.steps.map((s) => [s.id, s.status]));
|
|
102
|
+
for (const step of graph.steps) {
|
|
103
|
+
if (step.status !== "pending")
|
|
104
|
+
continue;
|
|
105
|
+
const depsComplete = step.dependsOn.every((depId) => statusMap.get(depId) === "complete");
|
|
106
|
+
if (depsComplete)
|
|
107
|
+
return step;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Return a new graph with the specified step marked as "complete".
|
|
113
|
+
* Immutable — does not mutate the input graph.
|
|
114
|
+
*
|
|
115
|
+
* @param graph — the current workflow graph
|
|
116
|
+
* @param stepId — ID of the step to mark complete
|
|
117
|
+
* @returns New graph with the step's status set to "complete"
|
|
118
|
+
* @throws Error if stepId is not found in the graph
|
|
119
|
+
*/
|
|
120
|
+
export function markStepComplete(graph, stepId) {
|
|
121
|
+
const found = graph.steps.some((s) => s.id === stepId);
|
|
122
|
+
if (!found) {
|
|
123
|
+
throw new Error(`Step not found: ${stepId}`);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
...graph,
|
|
127
|
+
steps: graph.steps.map((s) => s.id === stepId
|
|
128
|
+
? { ...s, status: "complete", finishedAt: new Date().toISOString() }
|
|
129
|
+
: s),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// ─── Iteration expansion ─────────────────────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* Expand an iterate step into concrete instances. Pure and deterministic —
|
|
135
|
+
* identical inputs always produce identical output.
|
|
136
|
+
*
|
|
137
|
+
* Given a parent step with status "pending" and an array of matched items,
|
|
138
|
+
* creates one instance step per item, marks the parent as "expanded", and
|
|
139
|
+
* rewrites any downstream dependsOn references from the parent ID to the
|
|
140
|
+
* full set of instance IDs.
|
|
141
|
+
*
|
|
142
|
+
* @param graph — the current workflow graph (not mutated)
|
|
143
|
+
* @param stepId — ID of the iterate step to expand
|
|
144
|
+
* @param items — matched items from the source artifact
|
|
145
|
+
* @param promptTemplate — template with {{item}} placeholders
|
|
146
|
+
* @returns New WorkflowGraph with instances inserted and deps rewritten
|
|
147
|
+
* @throws Error if stepId not found or step is not pending
|
|
148
|
+
*/
|
|
149
|
+
export function expandIteration(graph, stepId, items, promptTemplate) {
|
|
150
|
+
const parentIndex = graph.steps.findIndex((s) => s.id === stepId);
|
|
151
|
+
if (parentIndex === -1) {
|
|
152
|
+
throw new Error(`expandIteration: step not found: ${stepId}`);
|
|
153
|
+
}
|
|
154
|
+
const parentStep = graph.steps[parentIndex];
|
|
155
|
+
if (parentStep.status !== "pending") {
|
|
156
|
+
throw new Error(`expandIteration: step "${stepId}" has status "${parentStep.status}", expected "pending"`);
|
|
157
|
+
}
|
|
158
|
+
// Create instance steps
|
|
159
|
+
const instanceIds = [];
|
|
160
|
+
const instances = items.map((item, i) => {
|
|
161
|
+
const instanceId = `${stepId}--${String(i + 1).padStart(3, "0")}`;
|
|
162
|
+
instanceIds.push(instanceId);
|
|
163
|
+
return {
|
|
164
|
+
id: instanceId,
|
|
165
|
+
title: `${parentStep.title}: ${item}`,
|
|
166
|
+
status: "pending",
|
|
167
|
+
prompt: promptTemplate.replace(/\{\{item\}\}/g, () => item),
|
|
168
|
+
dependsOn: [...parentStep.dependsOn],
|
|
169
|
+
parentStepId: stepId,
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
// Build new steps array: copy everything, mark parent as expanded,
|
|
173
|
+
// insert instances right after the parent, rewrite downstream deps.
|
|
174
|
+
const newSteps = [];
|
|
175
|
+
for (let i = 0; i < graph.steps.length; i++) {
|
|
176
|
+
if (i === parentIndex) {
|
|
177
|
+
// Mark parent as expanded
|
|
178
|
+
newSteps.push({ ...parentStep, status: "expanded" });
|
|
179
|
+
// Insert instances immediately after parent
|
|
180
|
+
newSteps.push(...instances);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const step = graph.steps[i];
|
|
184
|
+
// Rewrite dependsOn: replace parent ID with all instance IDs
|
|
185
|
+
const hasDep = step.dependsOn.includes(stepId);
|
|
186
|
+
if (hasDep) {
|
|
187
|
+
const rewritten = step.dependsOn.flatMap((dep) => dep === stepId ? instanceIds : [dep]);
|
|
188
|
+
newSteps.push({ ...step, dependsOn: rewritten });
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
newSteps.push(step);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
...graph,
|
|
197
|
+
steps: newSteps,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// ─── Definition → Graph conversion ──────────────────────────────────────
|
|
201
|
+
/**
|
|
202
|
+
* Convert a parsed WorkflowDefinition into a WorkflowGraph with all
|
|
203
|
+
* steps in "pending" status. Used by run-manager to generate the initial
|
|
204
|
+
* GRAPH.yaml for a new run.
|
|
205
|
+
*
|
|
206
|
+
* @param def — a validated WorkflowDefinition from definition-loader
|
|
207
|
+
* @returns WorkflowGraph with pending steps and metadata from the definition
|
|
208
|
+
*/
|
|
209
|
+
export function initializeGraph(def) {
|
|
210
|
+
return {
|
|
211
|
+
steps: def.steps.map((s) => ({
|
|
212
|
+
id: s.id,
|
|
213
|
+
title: s.name,
|
|
214
|
+
status: "pending",
|
|
215
|
+
prompt: s.prompt,
|
|
216
|
+
dependsOn: s.requires ?? [],
|
|
217
|
+
})),
|
|
218
|
+
metadata: {
|
|
219
|
+
name: def.name,
|
|
220
|
+
createdAt: new Date().toISOString(),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** @deprecated Use initializeGraph instead. Kept for backward compatibility. */
|
|
225
|
+
export { initializeGraph as graphFromDefinition };
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
import { mkdirSync, writeFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { stringify } from "yaml";
|
|
18
|
+
import { loadDefinition, substituteParams } from "./definition-loader.js";
|
|
19
|
+
import { initializeGraph, writeGraph, readGraph } from "./graph.js";
|
|
20
|
+
// ─── Constants ───────────────────────────────────────────────────────────
|
|
21
|
+
const RUNS_DIR = "workflow-runs";
|
|
22
|
+
const DEFS_DIR = "workflow-defs";
|
|
23
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Generate a filesystem-safe timestamp: `YYYY-MM-DDTHH-MM-SS`.
|
|
26
|
+
* Replaces colons with hyphens so the string is safe as a directory name
|
|
27
|
+
* on all platforms (Windows forbids colons in paths).
|
|
28
|
+
*/
|
|
29
|
+
function makeTimestamp(date = new Date()) {
|
|
30
|
+
return date.toISOString().replace(/:/g, "-").replace(/\.\d{3}Z$/, "");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Derive overall status from a graph's step statuses.
|
|
34
|
+
*/
|
|
35
|
+
function deriveStatus(graph) {
|
|
36
|
+
const hasActive = graph.steps.some((s) => s.status === "active");
|
|
37
|
+
const allDone = graph.steps.every((s) => s.status === "complete" || s.status === "expanded");
|
|
38
|
+
if (allDone)
|
|
39
|
+
return "complete";
|
|
40
|
+
if (hasActive)
|
|
41
|
+
return "running";
|
|
42
|
+
return "pending";
|
|
43
|
+
}
|
|
44
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Create a new isolated run directory for a workflow definition.
|
|
47
|
+
*
|
|
48
|
+
* 1. Loads the definition from `<basePath>/.gsd/workflow-defs/<defName>.yaml`
|
|
49
|
+
* 2. Applies parameter substitution if overrides are provided
|
|
50
|
+
* 3. Creates `<basePath>/.gsd/workflow-runs/<defName>/<timestamp>/`
|
|
51
|
+
* 4. Writes frozen DEFINITION.yaml, initialized GRAPH.yaml, and optional PARAMS.json
|
|
52
|
+
*
|
|
53
|
+
* @param basePath — project root directory
|
|
54
|
+
* @param defName — definition filename (without .yaml extension)
|
|
55
|
+
* @param overrides — optional parameter overrides (merged with definition defaults)
|
|
56
|
+
* @returns Full path to the created run directory
|
|
57
|
+
* @throws Error if the definition file doesn't exist or is invalid
|
|
58
|
+
*/
|
|
59
|
+
export function createRun(basePath, defName, overrides) {
|
|
60
|
+
const defsDir = join(basePath, ".gsd", DEFS_DIR);
|
|
61
|
+
// Load and validate the definition
|
|
62
|
+
const rawDef = loadDefinition(defsDir, defName);
|
|
63
|
+
// Apply parameter substitution if overrides provided
|
|
64
|
+
const def = overrides
|
|
65
|
+
? substituteParams(rawDef, overrides)
|
|
66
|
+
: substituteParams(rawDef); // still resolve default params if any
|
|
67
|
+
// Create the run directory
|
|
68
|
+
const timestamp = makeTimestamp();
|
|
69
|
+
const runDir = join(basePath, ".gsd", RUNS_DIR, defName, timestamp);
|
|
70
|
+
mkdirSync(runDir, { recursive: true });
|
|
71
|
+
// Freeze the definition as DEFINITION.yaml
|
|
72
|
+
writeFileSync(join(runDir, "DEFINITION.yaml"), stringify(def), "utf-8");
|
|
73
|
+
// Initialize and write GRAPH.yaml
|
|
74
|
+
const graph = initializeGraph(def);
|
|
75
|
+
writeGraph(runDir, graph);
|
|
76
|
+
// Write PARAMS.json if overrides were provided
|
|
77
|
+
if (overrides && Object.keys(overrides).length > 0) {
|
|
78
|
+
writeFileSync(join(runDir, "PARAMS.json"), JSON.stringify(overrides, null, 2), "utf-8");
|
|
79
|
+
}
|
|
80
|
+
return runDir;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* List existing workflow runs with metadata.
|
|
84
|
+
*
|
|
85
|
+
* Scans `<basePath>/.gsd/workflow-runs/` for run directories. Each run's
|
|
86
|
+
* GRAPH.yaml is read to derive step counts and overall status.
|
|
87
|
+
*
|
|
88
|
+
* @param basePath — project root directory
|
|
89
|
+
* @param defName — optional filter: only list runs for this definition name
|
|
90
|
+
* @returns Array of run metadata, sorted newest-first within each definition
|
|
91
|
+
*/
|
|
92
|
+
export function listRuns(basePath, defName) {
|
|
93
|
+
const runsRoot = join(basePath, ".gsd", RUNS_DIR);
|
|
94
|
+
if (!existsSync(runsRoot))
|
|
95
|
+
return [];
|
|
96
|
+
const results = [];
|
|
97
|
+
// Get workflow name directories
|
|
98
|
+
const nameDirs = defName ? [defName] : readdirSync(runsRoot).filter((entry) => {
|
|
99
|
+
const full = join(runsRoot, entry);
|
|
100
|
+
return statSync(full).isDirectory();
|
|
101
|
+
});
|
|
102
|
+
for (const name of nameDirs) {
|
|
103
|
+
const nameDir = join(runsRoot, name);
|
|
104
|
+
if (!existsSync(nameDir))
|
|
105
|
+
continue;
|
|
106
|
+
const timestamps = readdirSync(nameDir).filter((entry) => {
|
|
107
|
+
const full = join(nameDir, entry);
|
|
108
|
+
return statSync(full).isDirectory();
|
|
109
|
+
});
|
|
110
|
+
// Sort newest-first (ISO strings sort lexicographically)
|
|
111
|
+
timestamps.sort().reverse();
|
|
112
|
+
for (const ts of timestamps) {
|
|
113
|
+
const runDir = join(nameDir, ts);
|
|
114
|
+
try {
|
|
115
|
+
const graph = readGraph(runDir);
|
|
116
|
+
const total = graph.steps.length;
|
|
117
|
+
const completed = graph.steps.filter((s) => s.status === "complete").length;
|
|
118
|
+
const pending = graph.steps.filter((s) => s.status === "pending").length;
|
|
119
|
+
const active = graph.steps.filter((s) => s.status === "active").length;
|
|
120
|
+
results.push({
|
|
121
|
+
name,
|
|
122
|
+
timestamp: ts,
|
|
123
|
+
runDir,
|
|
124
|
+
steps: { total, completed, pending, active },
|
|
125
|
+
status: deriveStatus(graph),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Skip runs with invalid/missing GRAPH.yaml
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-workflow
|
|
3
|
+
description: Conversational guide for creating valid YAML workflow definitions. Use when asked to "create a workflow", "new workflow definition", "build a workflow", "workflow YAML", "define workflow steps", or "workflow from template".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<essential_principles>
|
|
7
|
+
You are a workflow definition author. You help users create valid V1 YAML workflow definitions that the GSD workflow engine can execute.
|
|
8
|
+
|
|
9
|
+
**V1 Schema Basics:**
|
|
10
|
+
|
|
11
|
+
- Every definition requires `version: 1`, a non-empty `name`, and at least one step in `steps[]`.
|
|
12
|
+
- Optional top-level fields: `description` (string), `params` (key-value defaults for `{{ key }}` substitution).
|
|
13
|
+
- Each step requires: `id` (unique string), `name` (non-empty string), `prompt` (non-empty string).
|
|
14
|
+
- Each step optionally has: `requires` or `depends_on` (array of step IDs), `produces` (array of artifact paths), `context_from` (array of step IDs), `verify` (verification policy object), `iterate` (fan-out config object).
|
|
15
|
+
- YAML uses **snake_case** keys: `depends_on`, `context_from`. The engine converts to camelCase internally.
|
|
16
|
+
|
|
17
|
+
**Validation Rules:**
|
|
18
|
+
|
|
19
|
+
- Step IDs must be unique across the workflow.
|
|
20
|
+
- Dependencies (`requires`/`depends_on`) must reference existing step IDs — no dangling refs.
|
|
21
|
+
- A step cannot depend on itself.
|
|
22
|
+
- The dependency graph must be acyclic (no circular dependencies).
|
|
23
|
+
- `produces` paths must not contain `..` (path traversal rejected).
|
|
24
|
+
- `iterate.source` must not contain `..` (path traversal rejected).
|
|
25
|
+
- `iterate.pattern` must be a valid regex with at least one capture group.
|
|
26
|
+
|
|
27
|
+
**Four Verification Policies:**
|
|
28
|
+
|
|
29
|
+
1. `content-heuristic` — Checks artifact content. Optional: `minSize` (number), `pattern` (string).
|
|
30
|
+
2. `shell-command` — Runs a shell command. Required: `command` (non-empty string).
|
|
31
|
+
3. `prompt-verify` — Asks an LLM to verify. Required: `prompt` (non-empty string).
|
|
32
|
+
4. `human-review` — Pauses for human approval. No extra fields required.
|
|
33
|
+
|
|
34
|
+
**Parameter Substitution:**
|
|
35
|
+
|
|
36
|
+
- Define defaults in top-level `params: { key: "default_value" }`.
|
|
37
|
+
- Use `{{ key }}` placeholders in step prompts — the engine replaces them at runtime.
|
|
38
|
+
- CLI overrides take precedence over definition defaults.
|
|
39
|
+
- Parameter values must not contain `..` (path traversal guard).
|
|
40
|
+
- Any unresolved `{{ key }}` after substitution causes an error.
|
|
41
|
+
|
|
42
|
+
**Path Traversal Guard:**
|
|
43
|
+
|
|
44
|
+
- The engine rejects any `produces` path or `iterate.source` containing `..`.
|
|
45
|
+
- Parameter values are also checked for `..` during substitution.
|
|
46
|
+
|
|
47
|
+
**Output Location:**
|
|
48
|
+
|
|
49
|
+
- Finished definitions go in `.gsd/workflow-defs/<name>.yaml`.
|
|
50
|
+
- After writing, tell the user to validate with `/gsd workflow validate <name>`.
|
|
51
|
+
</essential_principles>
|
|
52
|
+
|
|
53
|
+
<routing>
|
|
54
|
+
Determine the user's intent and route to the appropriate workflow:
|
|
55
|
+
|
|
56
|
+
**"I want to create a workflow from scratch" / "new workflow" / "build a workflow":**
|
|
57
|
+
→ Read `workflows/create-from-scratch.md` and follow it.
|
|
58
|
+
|
|
59
|
+
**"I want to start from a template" / "from an example" / "customize a template":**
|
|
60
|
+
→ Read `workflows/create-from-template.md` and follow it.
|
|
61
|
+
|
|
62
|
+
**"Help me understand the schema" / "what fields are available?":**
|
|
63
|
+
→ Read `references/yaml-schema-v1.md` and explain the relevant parts.
|
|
64
|
+
|
|
65
|
+
**"How does verification work?" / "verify policies":**
|
|
66
|
+
→ Read `references/verification-policies.md` and explain.
|
|
67
|
+
|
|
68
|
+
**"How do I use context_from / iterate / params?":**
|
|
69
|
+
→ Read `references/feature-patterns.md` and explain the relevant feature.
|
|
70
|
+
|
|
71
|
+
**If intent is unclear, ask one clarifying question:**
|
|
72
|
+
- "Do you want to create a workflow from scratch, or start from an existing template?"
|
|
73
|
+
- Then route based on the answer.
|
|
74
|
+
</routing>
|
|
75
|
+
|
|
76
|
+
<reference_index>
|
|
77
|
+
Read these files when you need detailed schema knowledge during workflow authoring:
|
|
78
|
+
|
|
79
|
+
- `references/yaml-schema-v1.md` — Complete field-by-field V1 schema reference. Read when you need to explain any field's type, constraints, or defaults.
|
|
80
|
+
- `references/verification-policies.md` — All four verify policies with complete YAML examples. Read when helping the user choose or configure verification for a step.
|
|
81
|
+
- `references/feature-patterns.md` — Usage patterns for `context_from`, `iterate`, and `params` with complete YAML examples. Read when the user wants context chaining, fan-out iteration, or parameterized workflows.
|
|
82
|
+
</reference_index>
|
|
83
|
+
|
|
84
|
+
<templates_index>
|
|
85
|
+
Available templates in `templates/`:
|
|
86
|
+
|
|
87
|
+
- `workflow-definition.yaml` — Blank scaffold with all fields shown as comments. Copy and fill for a quick start.
|
|
88
|
+
- `blog-post-pipeline.yaml` — Linear chain with params and content-heuristic verification.
|
|
89
|
+
- `code-audit.yaml` — Iterate-based fan-out with shell-command verification.
|
|
90
|
+
- `release-checklist.yaml` — Diamond dependency graph with human-review verification.
|
|
91
|
+
</templates_index>
|
|
92
|
+
|
|
93
|
+
<output_conventions>
|
|
94
|
+
When assembling the final YAML:
|
|
95
|
+
|
|
96
|
+
1. Use 2-space indentation consistently.
|
|
97
|
+
2. Quote string values that contain special YAML characters (`:`, `{`, `}`, `[`, `]`, `#`).
|
|
98
|
+
3. Always include `version: 1` as the first field.
|
|
99
|
+
4. Order top-level fields: `version`, `name`, `description`, `params`, `steps`.
|
|
100
|
+
5. Order step fields: `id`, `name`, `prompt`, `requires`, `produces`, `context_from`, `verify`, `iterate`.
|
|
101
|
+
6. Write the file to `.gsd/workflow-defs/<name>.yaml`.
|
|
102
|
+
7. After writing, tell the user: "Run `/gsd workflow validate <name>` to check the definition."
|
|
103
|
+
</output_conventions>
|