@shahmarasy/prodo 0.1.5 → 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.
Files changed (67) hide show
  1. package/dist/agents/system-prompts.js +12 -12
  2. package/dist/cli/agent-command-installer.js +18 -18
  3. package/dist/cli/doctor.js +2 -2
  4. package/dist/cli/index.js +32 -30
  5. package/dist/cli/init-tui.d.ts +2 -2
  6. package/dist/cli/init-tui.js +43 -36
  7. package/dist/cli/init.d.ts +1 -0
  8. package/dist/cli/init.js +2 -1
  9. package/dist/core/artifacts.js +72 -72
  10. package/dist/core/settings.d.ts +1 -0
  11. package/dist/core/settings.js +10 -2
  12. package/dist/core/templates.js +248 -248
  13. package/dist/i18n/en.json +45 -45
  14. package/dist/i18n/tr.json +45 -45
  15. package/dist/providers/mock-provider.js +5 -5
  16. package/dist/providers/openai-provider.js +12 -12
  17. package/dist/skill-engine/context.d.ts +7 -0
  18. package/dist/skill-engine/context.js +76 -0
  19. package/dist/skill-engine/discovery.d.ts +2 -0
  20. package/dist/skill-engine/discovery.js +52 -0
  21. package/dist/skill-engine/graph.d.ts +4 -0
  22. package/dist/skill-engine/graph.js +114 -0
  23. package/dist/skill-engine/index.d.ts +11 -0
  24. package/dist/skill-engine/index.js +49 -0
  25. package/dist/skill-engine/pipeline.d.ts +9 -0
  26. package/dist/skill-engine/pipeline.js +84 -0
  27. package/dist/skill-engine/registry.d.ts +12 -0
  28. package/dist/skill-engine/registry.js +74 -0
  29. package/dist/skill-engine/types.d.ts +66 -0
  30. package/dist/skill-engine/types.js +2 -0
  31. package/dist/skill-engine/validator.d.ts +4 -0
  32. package/dist/skill-engine/validator.js +90 -0
  33. package/dist/skills/fix.d.ts +2 -0
  34. package/dist/skills/fix.js +41 -0
  35. package/dist/skills/generate-artifact.d.ts +2 -0
  36. package/dist/skills/generate-artifact.js +42 -0
  37. package/dist/skills/normalize.d.ts +2 -0
  38. package/dist/skills/normalize.js +29 -0
  39. package/dist/skills/register-core.d.ts +2 -0
  40. package/dist/skills/register-core.js +21 -0
  41. package/dist/skills/validate.d.ts +2 -0
  42. package/dist/skills/validate.js +37 -0
  43. package/package.json +4 -6
  44. package/src/cli/doctor.ts +2 -2
  45. package/src/cli/index.ts +35 -31
  46. package/src/cli/init-tui.ts +220 -208
  47. package/src/cli/init.ts +3 -2
  48. package/src/core/settings.ts +41 -35
  49. package/src/skill-engine/context.ts +90 -0
  50. package/src/skill-engine/discovery.ts +57 -0
  51. package/src/skill-engine/graph.ts +136 -0
  52. package/src/skill-engine/index.ts +55 -0
  53. package/src/skill-engine/pipeline.ts +112 -0
  54. package/src/skill-engine/registry.ts +75 -0
  55. package/src/skill-engine/types.ts +81 -0
  56. package/src/skill-engine/validator.ts +135 -0
  57. package/src/skills/fix.ts +45 -0
  58. package/src/skills/generate-artifact.ts +48 -0
  59. package/src/skills/{normalize-skill.ts → normalize.ts} +15 -12
  60. package/src/skills/register-core.ts +27 -0
  61. package/src/skills/validate.ts +40 -0
  62. package/src/skills/engine.ts +0 -94
  63. package/src/skills/fix-skill.ts +0 -38
  64. package/src/skills/generate-artifact-skill.ts +0 -32
  65. package/src/skills/generate-pipeline-skill.ts +0 -49
  66. package/src/skills/types.ts +0 -36
  67. package/src/skills/validate-skill.ts +0 -29
@@ -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
+ };
@@ -0,0 +1,135 @@
1
+ import { fileExists } from "../core/utils";
2
+ import type { SkillError, SkillManifest } from "./types";
3
+
4
+ function makeError(
5
+ skillName: string,
6
+ phase: SkillError["phase"],
7
+ message: string,
8
+ hints: string[] = []
9
+ ): SkillError {
10
+ return {
11
+ skillName,
12
+ phase,
13
+ message,
14
+ recoveryHints: hints
15
+ };
16
+ }
17
+
18
+ function checkType(value: unknown, expectedType: string, fieldName: string): string | null {
19
+ switch (expectedType) {
20
+ case "string":
21
+ if (typeof value !== "string") return `"${fieldName}" must be a string, got ${typeof value}`;
22
+ return null;
23
+
24
+ case "path":
25
+ if (typeof value !== "string") return `"${fieldName}" must be a path (string), got ${typeof value}`;
26
+ return null;
27
+
28
+ case "boolean":
29
+ if (typeof value !== "boolean") return `"${fieldName}" must be a boolean, got ${typeof value}`;
30
+ return null;
31
+
32
+ case "number":
33
+ if (typeof value !== "number" || !Number.isFinite(value)) {
34
+ return `"${fieldName}" must be a finite number, got ${typeof value}`;
35
+ }
36
+ return null;
37
+
38
+ case "json":
39
+ if (typeof value === "string") {
40
+ try {
41
+ JSON.parse(value);
42
+ } catch {
43
+ return `"${fieldName}" must be valid JSON string`;
44
+ }
45
+ }
46
+ return null;
47
+
48
+ default:
49
+ return null;
50
+ }
51
+ }
52
+
53
+ export function validateInputs(
54
+ manifest: SkillManifest,
55
+ inputs: Record<string, unknown>
56
+ ): SkillError | null {
57
+ for (const input of manifest.inputs) {
58
+ const value = inputs[input.name];
59
+
60
+ if (input.required && (value === undefined || value === null)) {
61
+ if (input.default !== undefined) continue;
62
+ return makeError(
63
+ manifest.name,
64
+ "input_validation",
65
+ `Required input "${input.name}" is missing`,
66
+ [`Provide "${input.name}" (${input.type}): ${input.description ?? ""}`]
67
+ );
68
+ }
69
+
70
+ if (value === undefined || value === null) continue;
71
+
72
+ const typeError = checkType(value, input.type, input.name);
73
+ if (typeError) {
74
+ return makeError(
75
+ manifest.name,
76
+ "input_validation",
77
+ typeError,
78
+ [`Expected type: ${input.type}`]
79
+ );
80
+ }
81
+ }
82
+
83
+ return null;
84
+ }
85
+
86
+ export async function validateInputPaths(
87
+ manifest: SkillManifest,
88
+ inputs: Record<string, unknown>
89
+ ): Promise<SkillError | null> {
90
+ for (const input of manifest.inputs) {
91
+ if (input.type !== "path") continue;
92
+ const value = inputs[input.name];
93
+ if (typeof value !== "string") continue;
94
+
95
+ if (input.required && !(await fileExists(value))) {
96
+ return makeError(
97
+ manifest.name,
98
+ "input_validation",
99
+ `Path "${input.name}" does not exist: ${value}`,
100
+ [`Ensure the file or directory exists: ${value}`]
101
+ );
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+
107
+ export function validateOutputs(
108
+ manifest: SkillManifest,
109
+ outputs: Record<string, unknown>
110
+ ): SkillError | null {
111
+ for (const output of manifest.outputs) {
112
+ const value = outputs[output.name];
113
+
114
+ if (value === undefined || value === null) {
115
+ return makeError(
116
+ manifest.name,
117
+ "output_validation",
118
+ `Expected output "${output.name}" was not produced`,
119
+ [`Skill "${manifest.name}" should return "${output.name}" (${output.type})`]
120
+ );
121
+ }
122
+
123
+ const typeError = checkType(value, output.type, output.name);
124
+ if (typeError) {
125
+ return makeError(
126
+ manifest.name,
127
+ "output_validation",
128
+ typeError,
129
+ [`Skill "${manifest.name}" returned wrong type for "${output.name}"`]
130
+ );
131
+ }
132
+ }
133
+
134
+ return null;
135
+ }
@@ -0,0 +1,45 @@
1
+ import { runFix } from "../core/fix";
2
+ import type { Skill } from "../skill-engine/types";
3
+
4
+ export const fixSkill: Skill = {
5
+ manifest: {
6
+ name: "fix",
7
+ version: "2.0.0",
8
+ description: "Auto-regenerate failing artifacts with backup, proposal, and validation",
9
+ category: "validation",
10
+ depends_on: [],
11
+ inputs: [
12
+ { name: "cwd", type: "path", required: true, description: "Project working directory" }
13
+ ],
14
+ outputs: [
15
+ { name: "validationResult", type: "json", description: "Final validation result after fix" }
16
+ ],
17
+ tags: ["fix", "repair", "validation"]
18
+ },
19
+
20
+ async execute(context, inputs) {
21
+ const cwd = (inputs.cwd as string) ?? context.state.cwd;
22
+ context.progress(1, 3, "Running fix pipeline...");
23
+
24
+ const result = await runFix({
25
+ cwd,
26
+ agent: context.agent,
27
+ log: context.log
28
+ });
29
+
30
+ context.progress(3, 3, result.finalPass ? "Fix complete." : "Fix applied, issues remain.");
31
+
32
+ context.state.validationResult = result.finalPass
33
+ ? { pass: true, reportPath: result.reportPath, issues: [] }
34
+ : undefined;
35
+
36
+ return {
37
+ validationResult: {
38
+ pass: result.finalPass,
39
+ reportPath: result.reportPath,
40
+ applied: result.applied,
41
+ targetCount: result.proposal.targets.length
42
+ }
43
+ };
44
+ }
45
+ };