@shahmarasy/prodo 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -97
- package/bin/prodo.cjs +6 -6
- package/dist/agents/agent-registry.d.ts +13 -0
- package/dist/agents/agent-registry.js +79 -0
- package/dist/agents/anthropic/index.d.ts +9 -0
- package/dist/agents/anthropic/index.js +55 -0
- package/dist/agents/base.d.ts +25 -0
- package/dist/agents/base.js +71 -0
- package/dist/agents/google/index.d.ts +9 -0
- package/dist/agents/google/index.js +53 -0
- package/dist/agents/mock/index.d.ts +11 -0
- package/dist/agents/mock/index.js +26 -0
- package/dist/agents/openai/index.d.ts +9 -0
- package/dist/agents/openai/index.js +57 -0
- package/dist/agents/system-prompts.d.ts +3 -0
- package/dist/agents/system-prompts.js +32 -0
- package/dist/cli/agent-command-installer.d.ts +4 -0
- package/dist/cli/agent-command-installer.js +148 -0
- package/dist/cli/agent-ids.d.ts +15 -0
- package/dist/cli/agent-ids.js +49 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +144 -0
- package/dist/cli/fix-tui.d.ts +4 -0
- package/dist/cli/fix-tui.js +79 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.js +467 -0
- package/dist/cli/init-tui.d.ts +23 -0
- package/dist/cli/init-tui.js +183 -0
- package/dist/cli/init.d.ts +12 -0
- package/dist/cli/init.js +335 -0
- package/dist/cli/normalize-interactive.d.ts +8 -0
- package/dist/cli/normalize-interactive.js +167 -0
- package/dist/cli/preset-loader.d.ts +4 -0
- package/dist/cli/preset-loader.js +210 -0
- package/dist/core/artifact-registry.d.ts +11 -0
- package/dist/core/artifact-registry.js +49 -0
- package/dist/core/artifacts.d.ts +10 -0
- package/dist/core/artifacts.js +892 -0
- package/dist/core/clean.d.ts +10 -0
- package/dist/core/clean.js +74 -0
- package/dist/core/consistency.d.ts +8 -0
- package/dist/core/consistency.js +328 -0
- package/dist/core/constants.d.ts +7 -0
- package/dist/core/constants.js +64 -0
- package/dist/core/errors.d.ts +3 -0
- package/dist/core/errors.js +10 -0
- package/dist/core/fix.d.ts +31 -0
- package/dist/core/fix.js +188 -0
- package/dist/core/hook-executor.d.ts +1 -0
- package/dist/core/hook-executor.js +175 -0
- package/dist/core/markdown.d.ts +16 -0
- package/dist/core/markdown.js +81 -0
- package/dist/core/normalize.d.ts +8 -0
- package/dist/core/normalize.js +125 -0
- package/dist/core/normalized-brief.d.ts +48 -0
- package/dist/core/normalized-brief.js +182 -0
- package/dist/core/output-index.d.ts +13 -0
- package/dist/core/output-index.js +55 -0
- package/dist/core/paths.d.ts +17 -0
- package/dist/core/paths.js +80 -0
- package/dist/core/project-config.d.ts +14 -0
- package/dist/core/project-config.js +69 -0
- package/dist/core/registry.d.ts +13 -0
- package/dist/core/registry.js +115 -0
- package/dist/core/settings.d.ts +8 -0
- package/dist/core/settings.js +43 -0
- package/dist/core/template-engine.d.ts +3 -0
- package/dist/core/template-engine.js +43 -0
- package/dist/core/template-resolver.d.ts +15 -0
- package/dist/core/template-resolver.js +46 -0
- package/dist/core/templates.d.ts +33 -0
- package/dist/core/templates.js +440 -0
- package/dist/core/terminology.d.ts +21 -0
- package/dist/core/terminology.js +143 -0
- package/dist/core/tracing.d.ts +21 -0
- package/dist/core/tracing.js +74 -0
- package/dist/core/types.d.ts +35 -0
- package/dist/core/types.js +5 -0
- package/dist/core/utils.d.ts +7 -0
- package/dist/core/utils.js +66 -0
- package/dist/core/validate.d.ts +10 -0
- package/dist/core/validate.js +226 -0
- package/dist/core/validator.d.ts +5 -0
- package/dist/core/validator.js +76 -0
- package/dist/core/version.d.ts +1 -0
- package/dist/core/version.js +30 -0
- package/dist/core/workflow-commands.d.ts +7 -0
- package/dist/core/workflow-commands.js +29 -0
- package/dist/i18n/en.json +45 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/index.js +63 -0
- package/dist/i18n/tr.json +45 -0
- package/dist/providers/index.d.ts +2 -1
- package/dist/providers/index.js +20 -6
- package/dist/providers/mock-provider.d.ts +1 -1
- package/dist/providers/mock-provider.js +12 -11
- package/dist/providers/openai-provider.d.ts +1 -1
- package/dist/providers/openai-provider.js +13 -13
- package/dist/skill-engine/context.d.ts +7 -0
- package/dist/skill-engine/context.js +76 -0
- package/dist/skill-engine/discovery.d.ts +2 -0
- package/dist/skill-engine/discovery.js +52 -0
- package/dist/skill-engine/graph.d.ts +4 -0
- package/dist/skill-engine/graph.js +114 -0
- package/dist/skill-engine/index.d.ts +11 -0
- package/dist/skill-engine/index.js +49 -0
- package/dist/skill-engine/pipeline.d.ts +9 -0
- package/dist/skill-engine/pipeline.js +84 -0
- package/dist/skill-engine/registry.d.ts +12 -0
- package/dist/skill-engine/registry.js +74 -0
- package/dist/skill-engine/types.d.ts +66 -0
- package/dist/skill-engine/types.js +2 -0
- package/dist/skill-engine/validator.d.ts +4 -0
- package/dist/skill-engine/validator.js +90 -0
- package/dist/skills/engine.d.ts +10 -0
- package/dist/skills/engine.js +75 -0
- package/dist/skills/fix-skill.d.ts +2 -0
- package/dist/skills/fix-skill.js +38 -0
- package/dist/skills/fix.d.ts +2 -0
- package/dist/skills/fix.js +41 -0
- package/dist/skills/generate-artifact-skill.d.ts +2 -0
- package/dist/skills/generate-artifact-skill.js +32 -0
- package/dist/skills/generate-artifact.d.ts +2 -0
- package/dist/skills/generate-artifact.js +42 -0
- package/dist/skills/generate-pipeline-skill.d.ts +2 -0
- package/dist/skills/generate-pipeline-skill.js +45 -0
- package/dist/skills/normalize-skill.d.ts +2 -0
- package/dist/skills/normalize-skill.js +29 -0
- package/dist/skills/normalize.d.ts +2 -0
- package/dist/skills/normalize.js +29 -0
- package/dist/skills/register-core.d.ts +2 -0
- package/dist/skills/register-core.js +21 -0
- package/dist/skills/types.d.ts +28 -0
- package/dist/skills/types.js +2 -0
- package/dist/skills/validate-skill.d.ts +2 -0
- package/dist/skills/validate-skill.js +29 -0
- package/dist/skills/validate.d.ts +2 -0
- package/dist/skills/validate.js +37 -0
- package/package.json +72 -45
- package/src/agents/agent-registry.ts +93 -0
- package/src/agents/anthropic/index.ts +86 -0
- package/src/agents/anthropic/manifest.json +7 -0
- package/src/agents/base.ts +77 -0
- package/src/agents/google/index.ts +79 -0
- package/src/agents/google/manifest.json +7 -0
- package/src/agents/mock/index.ts +32 -0
- package/src/agents/mock/manifest.json +7 -0
- package/src/agents/openai/index.ts +83 -0
- package/src/agents/openai/manifest.json +7 -0
- package/src/agents/system-prompts.ts +35 -0
- package/src/{agent-command-installer.ts → cli/agent-command-installer.ts} +164 -164
- package/src/{agents.ts → cli/agent-ids.ts} +58 -58
- package/src/{doctor.ts → cli/doctor.ts} +157 -137
- package/src/cli/fix-tui.ts +111 -0
- package/src/{cli.ts → cli/index.ts} +463 -410
- package/src/{init-tui.ts → cli/init-tui.ts} +49 -37
- package/src/{init.ts → cli/init.ts} +399 -398
- package/src/cli/normalize-interactive.ts +241 -0
- package/src/{preset-loader.ts → cli/preset-loader.ts} +237 -237
- package/src/{artifact-registry.ts → core/artifact-registry.ts} +69 -69
- package/src/{artifacts.ts → core/artifacts.ts} +1081 -1072
- package/src/core/clean.ts +88 -0
- package/src/{consistency.ts → core/consistency.ts} +374 -303
- package/src/{constants.ts → core/constants.ts} +72 -72
- package/src/{errors.ts → core/errors.ts} +7 -7
- package/src/core/fix.ts +253 -0
- package/src/{hook-executor.ts → core/hook-executor.ts} +196 -196
- package/src/{markdown.ts → core/markdown.ts} +93 -73
- package/src/{normalize.ts → core/normalize.ts} +145 -137
- package/src/{normalized-brief.ts → core/normalized-brief.ts} +227 -206
- package/src/{output-index.ts → core/output-index.ts} +59 -59
- package/src/{paths.ts → core/paths.ts} +75 -71
- package/src/{project-config.ts → core/project-config.ts} +78 -78
- package/src/{registry.ts → core/registry.ts} +119 -119
- package/src/{settings.ts → core/settings.ts} +8 -2
- package/src/core/template-engine.ts +45 -0
- package/src/{template-resolver.ts → core/template-resolver.ts} +54 -54
- package/src/{templates.ts → core/templates.ts} +452 -452
- package/src/core/terminology.ts +177 -0
- package/src/core/tracing.ts +110 -0
- package/src/{types.ts → core/types.ts} +46 -46
- package/src/{utils.ts → core/utils.ts} +64 -64
- package/src/{validate.ts → core/validate.ts} +252 -246
- package/src/{validator.ts → core/validator.ts} +92 -92
- package/src/{version.ts → core/version.ts} +24 -24
- package/src/{workflow-commands.ts → core/workflow-commands.ts} +32 -32
- package/src/i18n/en.json +45 -0
- package/src/i18n/index.ts +58 -0
- package/src/i18n/tr.json +45 -0
- package/src/providers/index.ts +29 -12
- package/src/providers/mock-provider.ts +200 -199
- package/src/providers/openai-provider.ts +88 -88
- package/src/skill-engine/context.ts +90 -0
- package/src/skill-engine/discovery.ts +57 -0
- package/src/skill-engine/graph.ts +136 -0
- package/src/skill-engine/index.ts +55 -0
- package/src/skill-engine/pipeline.ts +112 -0
- package/src/skill-engine/registry.ts +75 -0
- package/src/skill-engine/types.ts +81 -0
- package/src/skill-engine/validator.ts +135 -0
- package/src/skills/fix.ts +45 -0
- package/src/skills/generate-artifact.ts +48 -0
- package/src/skills/normalize.ts +32 -0
- package/src/skills/register-core.ts +27 -0
- package/src/skills/validate.ts +40 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { normalizedBriefPath } from "../core/paths";
|
|
2
|
+
import { getActiveArtifactPath } from "../core/output-index";
|
|
3
|
+
import { fileExists } from "../core/utils";
|
|
4
|
+
import type { ArtifactType } from "../core/types";
|
|
5
|
+
import type { PipelineState } from "./types";
|
|
6
|
+
|
|
7
|
+
export function createPipelineState(cwd: string): PipelineState {
|
|
8
|
+
return {
|
|
9
|
+
cwd,
|
|
10
|
+
normalizedBriefPath: undefined,
|
|
11
|
+
generatedArtifacts: new Map(),
|
|
12
|
+
validationResult: undefined,
|
|
13
|
+
custom: {},
|
|
14
|
+
startedAt: new Date().toISOString(),
|
|
15
|
+
completedSkills: []
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function hydrateStateFromDisk(
|
|
20
|
+
cwd: string,
|
|
21
|
+
state: PipelineState,
|
|
22
|
+
artifactTypes: ArtifactType[]
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const nbPath = normalizedBriefPath(cwd);
|
|
25
|
+
if (!state.normalizedBriefPath && (await fileExists(nbPath))) {
|
|
26
|
+
state.normalizedBriefPath = nbPath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const type of artifactTypes) {
|
|
30
|
+
if (!state.generatedArtifacts.has(type)) {
|
|
31
|
+
const active = await getActiveArtifactPath(cwd, type);
|
|
32
|
+
if (active) {
|
|
33
|
+
state.generatedArtifacts.set(type, active);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isOutputSatisfied(
|
|
40
|
+
outputName: string,
|
|
41
|
+
state: PipelineState,
|
|
42
|
+
artifactType?: string
|
|
43
|
+
): boolean {
|
|
44
|
+
if (outputName === "normalizedBriefPath") {
|
|
45
|
+
return typeof state.normalizedBriefPath === "string" && state.normalizedBriefPath.length > 0;
|
|
46
|
+
}
|
|
47
|
+
if (outputName === "artifactPath" && artifactType) {
|
|
48
|
+
return state.generatedArtifacts.has(artifactType);
|
|
49
|
+
}
|
|
50
|
+
if (outputName === "validationResult") {
|
|
51
|
+
return state.validationResult !== undefined;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function wireInputsFromState(
|
|
57
|
+
inputNames: string[],
|
|
58
|
+
state: PipelineState
|
|
59
|
+
): Record<string, unknown> {
|
|
60
|
+
const inputs: Record<string, unknown> = {};
|
|
61
|
+
for (const name of inputNames) {
|
|
62
|
+
if (name === "cwd") inputs.cwd = state.cwd;
|
|
63
|
+
else if (name === "normalizedBriefPath") inputs.normalizedBriefPath = state.normalizedBriefPath;
|
|
64
|
+
else if (name in state.custom) inputs[name] = state.custom[name];
|
|
65
|
+
}
|
|
66
|
+
return inputs;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function wireOutputsToState(
|
|
70
|
+
outputs: Record<string, unknown>,
|
|
71
|
+
state: PipelineState,
|
|
72
|
+
skillName: string
|
|
73
|
+
): void {
|
|
74
|
+
if ("normalizedBriefPath" in outputs && typeof outputs.normalizedBriefPath === "string") {
|
|
75
|
+
state.normalizedBriefPath = outputs.normalizedBriefPath;
|
|
76
|
+
}
|
|
77
|
+
if ("artifactPath" in outputs && typeof outputs.artifactPath === "string") {
|
|
78
|
+
const artifactType = skillName;
|
|
79
|
+
state.generatedArtifacts.set(artifactType, outputs.artifactPath);
|
|
80
|
+
}
|
|
81
|
+
if ("validationResult" in outputs && outputs.validationResult && typeof outputs.validationResult === "object") {
|
|
82
|
+
state.validationResult = outputs.validationResult as PipelineState["validationResult"];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const [key, value] of Object.entries(outputs)) {
|
|
86
|
+
if (!["normalizedBriefPath", "artifactPath", "validationResult"].includes(key)) {
|
|
87
|
+
state.custom[key] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { prodoPath } from "../core/paths";
|
|
4
|
+
import { fileExists } from "../core/utils";
|
|
5
|
+
import type { Skill, SkillManifest } from "./types";
|
|
6
|
+
|
|
7
|
+
function isValidManifest(obj: unknown): obj is SkillManifest {
|
|
8
|
+
if (!obj || typeof obj !== "object") return false;
|
|
9
|
+
const m = obj as Record<string, unknown>;
|
|
10
|
+
return (
|
|
11
|
+
typeof m.name === "string" &&
|
|
12
|
+
typeof m.version === "string" &&
|
|
13
|
+
typeof m.description === "string" &&
|
|
14
|
+
Array.isArray(m.depends_on) &&
|
|
15
|
+
Array.isArray(m.inputs) &&
|
|
16
|
+
Array.isArray(m.outputs)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function discoverSkills(
|
|
21
|
+
cwd: string,
|
|
22
|
+
log?: (message: string) => void
|
|
23
|
+
): Promise<Skill[]> {
|
|
24
|
+
const skillsDir = path.join(prodoPath(cwd), "skills");
|
|
25
|
+
if (!(await fileExists(skillsDir))) return [];
|
|
26
|
+
|
|
27
|
+
const entries = await fs.readdir(skillsDir);
|
|
28
|
+
const jsFiles = entries.filter((e) => e.endsWith(".js"));
|
|
29
|
+
const skills: Skill[] = [];
|
|
30
|
+
|
|
31
|
+
for (const file of jsFiles) {
|
|
32
|
+
const fullPath = path.resolve(skillsDir, file);
|
|
33
|
+
try {
|
|
34
|
+
const mod = require(fullPath) as Record<string, unknown>;
|
|
35
|
+
const manifest = mod.manifest;
|
|
36
|
+
const execute = mod.execute;
|
|
37
|
+
|
|
38
|
+
if (!isValidManifest(manifest)) {
|
|
39
|
+
log?.(`[Skill Discovery] Skipping ${file}: invalid or missing manifest`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof execute !== "function") {
|
|
44
|
+
log?.(`[Skill Discovery] Skipping ${file}: missing execute function`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
skills.push({ manifest, execute: execute as Skill["execute"] });
|
|
49
|
+
log?.(`[Skill Discovery] Loaded plugin: ${manifest.name} v${manifest.version}`);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
52
|
+
log?.(`[Skill Discovery] Failed to load ${file}: ${message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return skills;
|
|
57
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { UserError } from "../core/errors";
|
|
2
|
+
import type { ExecutionTier, SkillManifest } from "./types";
|
|
3
|
+
|
|
4
|
+
export function detectCycles(manifests: Map<string, SkillManifest>): string[][] | null {
|
|
5
|
+
const WHITE = 0;
|
|
6
|
+
const GRAY = 1;
|
|
7
|
+
const BLACK = 2;
|
|
8
|
+
const color = new Map<string, number>();
|
|
9
|
+
const cycles: string[][] = [];
|
|
10
|
+
|
|
11
|
+
for (const name of manifests.keys()) {
|
|
12
|
+
color.set(name, WHITE);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function dfs(node: string, path: string[]): void {
|
|
16
|
+
color.set(node, GRAY);
|
|
17
|
+
path.push(node);
|
|
18
|
+
|
|
19
|
+
const manifest = manifests.get(node);
|
|
20
|
+
if (manifest) {
|
|
21
|
+
for (const dep of manifest.depends_on) {
|
|
22
|
+
if (!manifests.has(dep)) continue;
|
|
23
|
+
const c = color.get(dep) ?? WHITE;
|
|
24
|
+
if (c === GRAY) {
|
|
25
|
+
const cycleStart = path.indexOf(dep);
|
|
26
|
+
cycles.push([...path.slice(cycleStart), dep]);
|
|
27
|
+
} else if (c === WHITE) {
|
|
28
|
+
dfs(dep, path);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
path.pop();
|
|
34
|
+
color.set(node, BLACK);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const name of manifests.keys()) {
|
|
38
|
+
if (color.get(name) === WHITE) {
|
|
39
|
+
dfs(name, []);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return cycles.length > 0 ? cycles : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function collectTransitiveDeps(
|
|
47
|
+
target: string,
|
|
48
|
+
manifests: Map<string, SkillManifest>,
|
|
49
|
+
collected: Set<string>
|
|
50
|
+
): void {
|
|
51
|
+
if (collected.has(target)) return;
|
|
52
|
+
collected.add(target);
|
|
53
|
+
const manifest = manifests.get(target);
|
|
54
|
+
if (!manifest) return;
|
|
55
|
+
for (const dep of manifest.depends_on) {
|
|
56
|
+
if (manifests.has(dep)) {
|
|
57
|
+
collectTransitiveDeps(dep, manifests, collected);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildExecutionPlan(
|
|
63
|
+
manifests: Map<string, SkillManifest>,
|
|
64
|
+
targetSkills: string[]
|
|
65
|
+
): ExecutionTier[] {
|
|
66
|
+
const needed = new Set<string>();
|
|
67
|
+
for (const target of targetSkills) {
|
|
68
|
+
collectTransitiveDeps(target, manifests, needed);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const filteredManifests = new Map<string, SkillManifest>();
|
|
72
|
+
for (const name of needed) {
|
|
73
|
+
const m = manifests.get(name);
|
|
74
|
+
if (m) filteredManifests.set(name, m);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const cycles = detectCycles(filteredManifests);
|
|
78
|
+
if (cycles) {
|
|
79
|
+
const cycleStr = cycles.map((c) => c.join(" → ")).join("; ");
|
|
80
|
+
throw new UserError(`Dependency cycle detected: ${cycleStr}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const inDegree = new Map<string, number>();
|
|
84
|
+
for (const name of filteredManifests.keys()) {
|
|
85
|
+
inDegree.set(name, 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const [, manifest] of filteredManifests) {
|
|
89
|
+
for (const dep of manifest.depends_on) {
|
|
90
|
+
if (filteredManifests.has(dep)) {
|
|
91
|
+
inDegree.set(manifest.name, (inDegree.get(manifest.name) ?? 0) + 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const tiers: ExecutionTier[] = [];
|
|
97
|
+
const remaining = new Set(filteredManifests.keys());
|
|
98
|
+
let tierNum = 0;
|
|
99
|
+
|
|
100
|
+
while (remaining.size > 0) {
|
|
101
|
+
const ready: string[] = [];
|
|
102
|
+
for (const name of remaining) {
|
|
103
|
+
if ((inDegree.get(name) ?? 0) === 0) {
|
|
104
|
+
ready.push(name);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (ready.length === 0) {
|
|
109
|
+
throw new UserError(`Cannot resolve execution order for: ${Array.from(remaining).join(", ")}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
ready.sort();
|
|
113
|
+
tiers.push({ tier: tierNum, skills: ready });
|
|
114
|
+
|
|
115
|
+
for (const name of ready) {
|
|
116
|
+
remaining.delete(name);
|
|
117
|
+
for (const [otherName, manifest] of filteredManifests) {
|
|
118
|
+
if (manifest.depends_on.includes(name) && remaining.has(otherName)) {
|
|
119
|
+
inDegree.set(otherName, (inDegree.get(otherName) ?? 0) - 1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
tierNum += 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return tiers;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getExecutionOrder(
|
|
131
|
+
manifests: Map<string, SkillManifest>,
|
|
132
|
+
targetSkills: string[]
|
|
133
|
+
): string[] {
|
|
134
|
+
const tiers = buildExecutionPlan(manifests, targetSkills);
|
|
135
|
+
return tiers.flatMap((t) => t.skills);
|
|
136
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { SkillRegistry } from "./registry";
|
|
2
|
+
import { SkillPipeline } from "./pipeline";
|
|
3
|
+
import { discoverSkills } from "./discovery";
|
|
4
|
+
import { registerCoreSkills } from "../skills/register-core";
|
|
5
|
+
import { createPipelineState, hydrateStateFromDisk } from "./context";
|
|
6
|
+
import { listArtifactTypes } from "../core/artifact-registry";
|
|
7
|
+
import type { PipelineState } from "./types";
|
|
8
|
+
|
|
9
|
+
export async function createEngine(
|
|
10
|
+
cwd: string,
|
|
11
|
+
log?: (message: string) => void
|
|
12
|
+
): Promise<SkillPipeline> {
|
|
13
|
+
const registry = new SkillRegistry();
|
|
14
|
+
|
|
15
|
+
await registerCoreSkills(registry, cwd);
|
|
16
|
+
|
|
17
|
+
const plugins = await discoverSkills(cwd, log);
|
|
18
|
+
for (const plugin of plugins) {
|
|
19
|
+
try {
|
|
20
|
+
registry.register(plugin);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
23
|
+
log?.(`[Skill Engine] Plugin registration failed: ${message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return new SkillPipeline(registry);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function createHydratedState(cwd: string): Promise<PipelineState> {
|
|
31
|
+
const state = createPipelineState(cwd);
|
|
32
|
+
const artifactTypes = await listArtifactTypes(cwd);
|
|
33
|
+
await hydrateStateFromDisk(cwd, state, artifactTypes);
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { SkillPipeline } from "./pipeline";
|
|
38
|
+
export { SkillRegistry } from "./registry";
|
|
39
|
+
export { createPipelineState, hydrateStateFromDisk } from "./context";
|
|
40
|
+
export { buildExecutionPlan, detectCycles, getExecutionOrder } from "./graph";
|
|
41
|
+
export { discoverSkills } from "./discovery";
|
|
42
|
+
export { validateInputs, validateOutputs, validateInputPaths } from "./validator";
|
|
43
|
+
export type {
|
|
44
|
+
Skill,
|
|
45
|
+
SkillManifest,
|
|
46
|
+
SkillInput,
|
|
47
|
+
SkillOutput,
|
|
48
|
+
SkillContext,
|
|
49
|
+
SkillExecuteFn,
|
|
50
|
+
PipelineState,
|
|
51
|
+
PipelineOptions,
|
|
52
|
+
ExecutionTier,
|
|
53
|
+
SkillError,
|
|
54
|
+
ProgressCallback
|
|
55
|
+
} from "./types";
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { UserError } from "../core/errors";
|
|
2
|
+
import { wireInputsFromState, wireOutputsToState } from "./context";
|
|
3
|
+
import { getExecutionOrder } from "./graph";
|
|
4
|
+
import type { SkillRegistry } from "./registry";
|
|
5
|
+
import type { PipelineOptions, PipelineState, SkillContext, SkillError } from "./types";
|
|
6
|
+
import { validateInputs, validateOutputs } from "./validator";
|
|
7
|
+
|
|
8
|
+
function createSkillError(
|
|
9
|
+
skillName: string,
|
|
10
|
+
phase: SkillError["phase"],
|
|
11
|
+
error: unknown
|
|
12
|
+
): SkillError {
|
|
13
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
15
|
+
return {
|
|
16
|
+
skillName,
|
|
17
|
+
phase,
|
|
18
|
+
message,
|
|
19
|
+
stack,
|
|
20
|
+
recoveryHints: [`Skill "${skillName}" failed during ${phase}. Check inputs and retry.`]
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class SkillPipeline {
|
|
25
|
+
constructor(private registry: SkillRegistry) {}
|
|
26
|
+
|
|
27
|
+
async runPipeline(
|
|
28
|
+
skillNames: string[],
|
|
29
|
+
state: PipelineState,
|
|
30
|
+
options: PipelineOptions = {}
|
|
31
|
+
): Promise<PipelineState> {
|
|
32
|
+
const { log = console.log, progress, agent } = options;
|
|
33
|
+
|
|
34
|
+
for (const name of skillNames) {
|
|
35
|
+
if (!this.registry.has(name)) {
|
|
36
|
+
const available = this.registry.list().map((s) => s.manifest.name).join(", ");
|
|
37
|
+
throw new UserError(`Unknown skill: "${name}". Available: ${available}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const manifests = new Map(
|
|
42
|
+
this.registry.list().map((s) => [s.manifest.name, s.manifest])
|
|
43
|
+
);
|
|
44
|
+
const executionOrder = getExecutionOrder(manifests, skillNames);
|
|
45
|
+
const totalSteps = executionOrder.length;
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < executionOrder.length; i++) {
|
|
48
|
+
const skillName = executionOrder[i];
|
|
49
|
+
|
|
50
|
+
if (state.completedSkills.includes(skillName)) {
|
|
51
|
+
log(`[Skip] ${skillName} — already completed`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const skill = this.registry.get(skillName);
|
|
56
|
+
if (!skill) continue;
|
|
57
|
+
|
|
58
|
+
progress?.(i + 1, totalSteps, `Running ${skillName}...`);
|
|
59
|
+
log(`[${i + 1}/${totalSteps}] ${skillName}`);
|
|
60
|
+
|
|
61
|
+
const inputNames = skill.manifest.inputs.map((inp) => inp.name);
|
|
62
|
+
const wiredInputs = wireInputsFromState(inputNames, state);
|
|
63
|
+
|
|
64
|
+
const inputError = validateInputs(skill.manifest, wiredInputs);
|
|
65
|
+
if (inputError) {
|
|
66
|
+
throw new UserError(
|
|
67
|
+
`Skill "${skillName}" input validation failed: ${inputError.message}\n` +
|
|
68
|
+
inputError.recoveryHints.join("\n")
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let outputs: Record<string, unknown>;
|
|
73
|
+
try {
|
|
74
|
+
const context: SkillContext = {
|
|
75
|
+
state,
|
|
76
|
+
progress: progress ?? (() => {}),
|
|
77
|
+
log,
|
|
78
|
+
agent
|
|
79
|
+
};
|
|
80
|
+
outputs = await skill.execute(context, wiredInputs);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const skillError = createSkillError(skillName, "execution", error);
|
|
83
|
+
throw new UserError(
|
|
84
|
+
`Skill "${skillName}" execution failed: ${skillError.message}\n` +
|
|
85
|
+
skillError.recoveryHints.join("\n")
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const outputError = validateOutputs(skill.manifest, outputs);
|
|
90
|
+
if (outputError) {
|
|
91
|
+
log(`[Warning] Skill "${skillName}" output validation: ${outputError.message}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
wireOutputsToState(outputs, state, skillName);
|
|
95
|
+
state.completedSkills.push(skillName);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return state;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async runSkill(
|
|
102
|
+
name: string,
|
|
103
|
+
state: PipelineState,
|
|
104
|
+
options: PipelineOptions = {}
|
|
105
|
+
): Promise<PipelineState> {
|
|
106
|
+
return this.runPipeline([name], state, options);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getRegistry(): SkillRegistry {
|
|
110
|
+
return this.registry;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { UserError } from "../core/errors";
|
|
2
|
+
import type { Skill, SkillManifest } from "./types";
|
|
3
|
+
|
|
4
|
+
function validateManifest(manifest: SkillManifest): string | null {
|
|
5
|
+
if (!manifest.name || typeof manifest.name !== "string") return "Skill name is required";
|
|
6
|
+
if (!manifest.version || typeof manifest.version !== "string") return "Skill version is required";
|
|
7
|
+
if (!manifest.description || typeof manifest.description !== "string") return "Skill description is required";
|
|
8
|
+
|
|
9
|
+
const validCategories = ["core", "artifact", "validation", "custom"];
|
|
10
|
+
if (!validCategories.includes(manifest.category)) {
|
|
11
|
+
return `Invalid category "${manifest.category}". Must be: ${validCategories.join(", ")}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!Array.isArray(manifest.depends_on)) return "depends_on must be an array";
|
|
15
|
+
if (!Array.isArray(manifest.inputs)) return "inputs must be an array";
|
|
16
|
+
if (!Array.isArray(manifest.outputs)) return "outputs must be an array";
|
|
17
|
+
|
|
18
|
+
const validTypes = ["string", "path", "boolean", "json", "number"];
|
|
19
|
+
for (const input of manifest.inputs) {
|
|
20
|
+
if (!input.name || typeof input.name !== "string") return "Input name is required";
|
|
21
|
+
if (!validTypes.includes(input.type)) return `Invalid input type "${input.type}" for "${input.name}"`;
|
|
22
|
+
}
|
|
23
|
+
for (const output of manifest.outputs) {
|
|
24
|
+
if (!output.name || typeof output.name !== "string") return "Output name is required";
|
|
25
|
+
if (!validTypes.includes(output.type)) return `Invalid output type "${output.type}" for "${output.name}"`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class SkillRegistry {
|
|
32
|
+
private skills = new Map<string, Skill>();
|
|
33
|
+
|
|
34
|
+
register(skill: Skill): void {
|
|
35
|
+
const error = validateManifest(skill.manifest);
|
|
36
|
+
if (error) {
|
|
37
|
+
throw new UserError(`Invalid skill manifest for "${skill.manifest?.name ?? "unknown"}": ${error}`);
|
|
38
|
+
}
|
|
39
|
+
if (typeof skill.execute !== "function") {
|
|
40
|
+
throw new UserError(`Skill "${skill.manifest.name}" must export an execute function`);
|
|
41
|
+
}
|
|
42
|
+
if (this.skills.has(skill.manifest.name)) {
|
|
43
|
+
throw new UserError(`Skill "${skill.manifest.name}" is already registered`);
|
|
44
|
+
}
|
|
45
|
+
this.skills.set(skill.manifest.name, skill);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get(name: string): Skill | undefined {
|
|
49
|
+
return this.skills.get(name);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
has(name: string): boolean {
|
|
53
|
+
return this.skills.has(name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
list(): Skill[] {
|
|
57
|
+
return Array.from(this.skills.values());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
listByCategory(category: string): Skill[] {
|
|
61
|
+
return this.list().filter((s) => s.manifest.category === category);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
listManifests(): SkillManifest[] {
|
|
65
|
+
return this.list().map((s) => s.manifest);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
unregister(name: string): boolean {
|
|
69
|
+
return this.skills.delete(name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
size(): number {
|
|
73
|
+
return this.skills.size;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ValidationIssue } from "../core/types";
|
|
2
|
+
|
|
3
|
+
export type SkillInputType = "string" | "path" | "boolean" | "json" | "number";
|
|
4
|
+
|
|
5
|
+
export type SkillInput = {
|
|
6
|
+
name: string;
|
|
7
|
+
type: SkillInputType;
|
|
8
|
+
required: boolean;
|
|
9
|
+
description?: string;
|
|
10
|
+
default?: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type SkillOutput = {
|
|
14
|
+
name: string;
|
|
15
|
+
type: SkillInputType;
|
|
16
|
+
description?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type SkillManifest = {
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
description: string;
|
|
23
|
+
category: "core" | "artifact" | "validation" | "custom";
|
|
24
|
+
depends_on: string[];
|
|
25
|
+
inputs: SkillInput[];
|
|
26
|
+
outputs: SkillOutput[];
|
|
27
|
+
tags?: string[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type PipelineState = {
|
|
31
|
+
cwd: string;
|
|
32
|
+
normalizedBriefPath?: string;
|
|
33
|
+
generatedArtifacts: Map<string, string>;
|
|
34
|
+
validationResult?: {
|
|
35
|
+
pass: boolean;
|
|
36
|
+
reportPath: string;
|
|
37
|
+
issues: ValidationIssue[];
|
|
38
|
+
};
|
|
39
|
+
custom: Record<string, unknown>;
|
|
40
|
+
startedAt: string;
|
|
41
|
+
completedSkills: string[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ProgressCallback = (step: number, total: number, message: string) => void;
|
|
45
|
+
|
|
46
|
+
export type SkillContext = {
|
|
47
|
+
state: PipelineState;
|
|
48
|
+
progress: ProgressCallback;
|
|
49
|
+
log: (message: string) => void;
|
|
50
|
+
agent?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type SkillExecuteFn = (
|
|
54
|
+
context: SkillContext,
|
|
55
|
+
inputs: Record<string, unknown>
|
|
56
|
+
) => Promise<Record<string, unknown>>;
|
|
57
|
+
|
|
58
|
+
export type Skill = {
|
|
59
|
+
manifest: SkillManifest;
|
|
60
|
+
execute: SkillExecuteFn;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type SkillError = {
|
|
64
|
+
skillName: string;
|
|
65
|
+
phase: "input_validation" | "execution" | "output_validation";
|
|
66
|
+
message: string;
|
|
67
|
+
inputContext?: Record<string, unknown>;
|
|
68
|
+
stack?: string;
|
|
69
|
+
recoveryHints: string[];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type ExecutionTier = {
|
|
73
|
+
tier: number;
|
|
74
|
+
skills: string[];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type PipelineOptions = {
|
|
78
|
+
log?: (message: string) => void;
|
|
79
|
+
progress?: ProgressCallback;
|
|
80
|
+
agent?: string;
|
|
81
|
+
};
|