@williambeto/ai-workflow 2.7.1 → 2.8.1
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/CHANGELOG.md +69 -0
- package/README.md +3 -2
- package/bin/ai-workflow.js +1203 -301
- package/bin/ai-workflow.js.map +1 -1
- package/{chunk-W4RTQWVQ.js → chunk-4FI5ODAM.js} +879 -759
- package/chunk-AINIR25D.js +120 -0
- package/chunk-AY33SA5W.js +46 -0
- package/{evidence-validator-76ZQQYDU.js → chunk-FNT7DN3N.js} +22 -2
- package/{chunk-XW747GIG.js → chunk-H7GIKXFO.js} +339 -14
- package/{chunk-BDZPUAEX.js → chunk-LD7EHAU2.js} +352 -39
- package/chunk-YOBY5C72.js +76 -0
- package/core/index.d.ts +51 -1
- package/core/index.js +5 -3
- package/dist-assets/agents/atlas.md +25 -6
- package/dist-assets/commands/atlas.md +7 -2
- package/dist-assets/docs/QUICKSTART.md +10 -1
- package/dist-assets/docs/cli-reference.md +47 -2
- package/dist-assets/docs/compatibility/provider-usage.md +16 -1
- package/dist-assets/docs/compatibility/runtime-matrix.md +13 -2
- package/dist-assets/docs/policies/06-FINAL_EVIDENCE_CONTRACT.md +11 -1
- package/dist-assets/docs/policies/SKILLS_COMMON_GOVERNANCE.md +16 -0
- package/dist-assets/schemas/README.md +4 -0
- package/dist-assets/schemas/approval-receipt.schema.json +35 -0
- package/dist-assets/schemas/evidence.schema.json +93 -1
- package/evidence-validator-HS3NTWAB.js +8 -0
- package/package.json +17 -2
- package/skill-4MEGJ3DO.js +211 -0
- package/skill-frontmatter-linter-FMJADOK4.js +14 -0
- package/{validate-A46WUBVZ.js → validate-F3ZH63LI.js} +3 -3
|
@@ -35,6 +35,34 @@
|
|
|
35
35
|
"type": "string"
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
+
"deliveryChangedFiles": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": {
|
|
41
|
+
"type": "string"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"mutationOwner": {
|
|
45
|
+
"type": ["string", "null"],
|
|
46
|
+
"enum": ["Astra", "Phoenix", "ControlPlane", null]
|
|
47
|
+
},
|
|
48
|
+
"capabilitiesUsed": {
|
|
49
|
+
"type": "array",
|
|
50
|
+
"items": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"deliveryMode": {
|
|
55
|
+
"enum": ["workspace", "answer-only"]
|
|
56
|
+
},
|
|
57
|
+
"deliveryOutcome": {
|
|
58
|
+
"enum": ["CHANGED", "ALREADY_SATISFIED", "READ_ONLY_RESULT", "PREVIEW_ONLY", "NEEDS_CLARIFICATION", "NOT_DELIVERED"]
|
|
59
|
+
},
|
|
60
|
+
"testAction": {
|
|
61
|
+
"enum": ["reuse", "extend", "add", "none"]
|
|
62
|
+
},
|
|
63
|
+
"testReason": {
|
|
64
|
+
"type": "string"
|
|
65
|
+
},
|
|
38
66
|
"status": {
|
|
39
67
|
"enum": ["COMPLETED", "COMPLETED_WITH_NOTES", "BLOCKED"]
|
|
40
68
|
},
|
|
@@ -104,6 +132,70 @@
|
|
|
104
132
|
"visualEvidence": {
|
|
105
133
|
"type": ["object", "null"],
|
|
106
134
|
"additionalProperties": true
|
|
135
|
+
},
|
|
136
|
+
"workflowId": {
|
|
137
|
+
"type": "string"
|
|
138
|
+
},
|
|
139
|
+
"baseSha": {
|
|
140
|
+
"type": "string"
|
|
141
|
+
},
|
|
142
|
+
"artifactHashes": {
|
|
143
|
+
"type": "object",
|
|
144
|
+
"additionalProperties": false,
|
|
145
|
+
"required": ["draft", "approved", "technicalPlan"],
|
|
146
|
+
"properties": {
|
|
147
|
+
"draft": { "type": ["string", "null"], "pattern": "^[a-f0-9]{64}$" },
|
|
148
|
+
"approved": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
149
|
+
"technicalPlan": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"phaseOrder": {
|
|
153
|
+
"type": "array",
|
|
154
|
+
"items": { "enum": ["specification", "planning", "implementation"] }
|
|
155
|
+
},
|
|
156
|
+
"confirmedActors": {
|
|
157
|
+
"type": "array",
|
|
158
|
+
"items": {
|
|
159
|
+
"type": "object",
|
|
160
|
+
"additionalProperties": false,
|
|
161
|
+
"required": ["phase", "requested", "applied"],
|
|
162
|
+
"properties": {
|
|
163
|
+
"phase": { "enum": ["specification", "planning", "implementation"] },
|
|
164
|
+
"requested": { "enum": ["Nexus", "Orion", "Astra"] },
|
|
165
|
+
"applied": { "enum": ["Nexus", "Orion", "Astra"] }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"allOf": [
|
|
171
|
+
{
|
|
172
|
+
"if": {
|
|
173
|
+
"properties": {
|
|
174
|
+
"deliveryMode": { "const": "workspace" },
|
|
175
|
+
"deliveryOutcome": { "const": "CHANGED" }
|
|
176
|
+
},
|
|
177
|
+
"required": ["deliveryMode", "deliveryOutcome"]
|
|
178
|
+
},
|
|
179
|
+
"then": {
|
|
180
|
+
"required": ["mutationOwner", "commands"],
|
|
181
|
+
"properties": {
|
|
182
|
+
"mutationOwner": {
|
|
183
|
+
"enum": ["Astra", "Phoenix"]
|
|
184
|
+
},
|
|
185
|
+
"commands": {
|
|
186
|
+
"type": "array",
|
|
187
|
+
"minItems": 1,
|
|
188
|
+
"contains": {
|
|
189
|
+
"type": "object",
|
|
190
|
+
"required": ["kind", "status"],
|
|
191
|
+
"properties": {
|
|
192
|
+
"kind": { "not": { "const": "other" } },
|
|
193
|
+
"status": { "enum": ["PASS", "PASS_WITH_NOTES"] }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
107
199
|
}
|
|
108
|
-
|
|
200
|
+
]
|
|
109
201
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williambeto/ai-workflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"description": "AI Workflow Kit — OpenCode-first software delivery workflow with agents, commands, skills, validation, and evidence",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "José Willams",
|
|
@@ -13,6 +13,18 @@
|
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=20.11"
|
|
15
15
|
},
|
|
16
|
+
"devEngines": {
|
|
17
|
+
"runtime": {
|
|
18
|
+
"name": "node",
|
|
19
|
+
"version": ">=22.14.0 <23",
|
|
20
|
+
"onFail": "error"
|
|
21
|
+
},
|
|
22
|
+
"packageManager": {
|
|
23
|
+
"name": "npm",
|
|
24
|
+
"version": ">=10",
|
|
25
|
+
"onFail": "error"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
16
28
|
"keywords": [
|
|
17
29
|
"ai-workflow",
|
|
18
30
|
"opencode",
|
|
@@ -41,5 +53,8 @@
|
|
|
41
53
|
"README.md",
|
|
42
54
|
"LICENSE",
|
|
43
55
|
"CHANGELOG.md"
|
|
44
|
-
]
|
|
56
|
+
],
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@opencode-ai/sdk": "1.17.18"
|
|
59
|
+
}
|
|
45
60
|
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createManagedBackup
|
|
3
|
+
} from "./chunk-AY33SA5W.js";
|
|
4
|
+
import {
|
|
5
|
+
BranchGate
|
|
6
|
+
} from "./chunk-AINIR25D.js";
|
|
7
|
+
import {
|
|
8
|
+
lintSkillFile
|
|
9
|
+
} from "./chunk-YOBY5C72.js";
|
|
10
|
+
import "./chunk-5WRI5ZAA.js";
|
|
11
|
+
|
|
12
|
+
// src/cli/commands/skill.ts
|
|
13
|
+
import fs from "fs/promises";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { randomUUID } from "crypto";
|
|
16
|
+
var SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
17
|
+
var MAX_SKILL_NAME_LENGTH = 64;
|
|
18
|
+
var BACKUP_ROOT = ".ai-workflow-backups";
|
|
19
|
+
function requireDescription(value) {
|
|
20
|
+
if (value === void 0 || value.trim().length === 0) {
|
|
21
|
+
throw new Error("--description requires a non-empty value");
|
|
22
|
+
}
|
|
23
|
+
return value.trim();
|
|
24
|
+
}
|
|
25
|
+
function parseSkillCreateArgs(args) {
|
|
26
|
+
const positionals = [];
|
|
27
|
+
let description;
|
|
28
|
+
let descriptionSeen = false;
|
|
29
|
+
let force = false;
|
|
30
|
+
let dryRun = false;
|
|
31
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
32
|
+
const arg = args[index];
|
|
33
|
+
if (arg === "--force") {
|
|
34
|
+
force = true;
|
|
35
|
+
} else if (arg === "--dry-run") {
|
|
36
|
+
dryRun = true;
|
|
37
|
+
} else if (arg === "--description") {
|
|
38
|
+
if (descriptionSeen) throw new Error("--description may only be provided once");
|
|
39
|
+
const value = args[index + 1];
|
|
40
|
+
if (value?.startsWith("--")) throw new Error("--description requires a non-empty value");
|
|
41
|
+
description = requireDescription(value);
|
|
42
|
+
descriptionSeen = true;
|
|
43
|
+
index += 1;
|
|
44
|
+
} else if (arg.startsWith("--description=")) {
|
|
45
|
+
if (descriptionSeen) throw new Error("--description may only be provided once");
|
|
46
|
+
description = requireDescription(arg.slice("--description=".length));
|
|
47
|
+
descriptionSeen = true;
|
|
48
|
+
} else if (arg.startsWith("-")) {
|
|
49
|
+
throw new Error(`Unknown option for skill create: ${arg}`);
|
|
50
|
+
} else {
|
|
51
|
+
positionals.push(arg);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (positionals.length > 1) {
|
|
55
|
+
throw new Error(`Unexpected positional arguments: ${positionals.slice(1).join(" ")}`);
|
|
56
|
+
}
|
|
57
|
+
return { name: positionals[0], description, force, dryRun };
|
|
58
|
+
}
|
|
59
|
+
function isWithin(parent, candidate) {
|
|
60
|
+
const relative = path.relative(parent, candidate);
|
|
61
|
+
return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
62
|
+
}
|
|
63
|
+
function assertWithinWorkspace(workspaceRoot, candidate, label) {
|
|
64
|
+
if (!isWithin(workspaceRoot, candidate)) {
|
|
65
|
+
throw new Error(`${label} resolves outside the workspace: ${candidate}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function lstatIfExists(targetPath) {
|
|
69
|
+
try {
|
|
70
|
+
return await fs.lstat(targetPath);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error?.code === "ENOENT") return void 0;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function validateExistingDirectory(targetPath, workspaceRoot, label, rejectSymlink = false) {
|
|
77
|
+
const entry = await lstatIfExists(targetPath);
|
|
78
|
+
if (!entry) return false;
|
|
79
|
+
if (rejectSymlink && entry.isSymbolicLink()) {
|
|
80
|
+
throw new Error(`${label} must not be a symbolic link: ${targetPath}`);
|
|
81
|
+
}
|
|
82
|
+
let resolved;
|
|
83
|
+
try {
|
|
84
|
+
resolved = await fs.realpath(targetPath);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(`Cannot resolve ${label}: ${error.message}`);
|
|
87
|
+
}
|
|
88
|
+
assertWithinWorkspace(workspaceRoot, resolved, label);
|
|
89
|
+
const resolvedEntry = await fs.stat(targetPath);
|
|
90
|
+
if (!resolvedEntry.isDirectory()) {
|
|
91
|
+
throw new Error(`${label} must be a directory: ${targetPath}`);
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
async function validateExistingSkillFile(skillFile) {
|
|
96
|
+
const entry = await lstatIfExists(skillFile);
|
|
97
|
+
if (!entry) return false;
|
|
98
|
+
if (entry.isSymbolicLink()) {
|
|
99
|
+
throw new Error(`SKILL.md must not be a symbolic link: ${skillFile}`);
|
|
100
|
+
}
|
|
101
|
+
if (!entry.isFile()) {
|
|
102
|
+
throw new Error(`SKILL.md must be a regular file: ${skillFile}`);
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
function validateSkillName(name) {
|
|
107
|
+
if (name.length > MAX_SKILL_NAME_LENGTH || !SKILL_NAME_PATTERN.test(name)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Invalid skill name '${name}'. Use lowercase kebab-case with at most ${MAX_SKILL_NAME_LENGTH} characters (for example: database-migrations).`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function buildSkillContent(name, description) {
|
|
114
|
+
const safeDescription = description || `Provides guidelines and instructions for ${name}`;
|
|
115
|
+
const readableName = name.split("-").map((part) => `${part[0].toUpperCase()}${part.slice(1)}`).join(" ");
|
|
116
|
+
return `---
|
|
117
|
+
name: ${JSON.stringify(name)}
|
|
118
|
+
description: ${JSON.stringify(safeDescription)}
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
# ${readableName}
|
|
122
|
+
|
|
123
|
+
## Context
|
|
124
|
+
|
|
125
|
+
Describe when this skill should be used.
|
|
126
|
+
|
|
127
|
+
## Instructions
|
|
128
|
+
|
|
129
|
+
- Add concrete, actionable instructions.
|
|
130
|
+
- Document constraints and prohibited actions.
|
|
131
|
+
|
|
132
|
+
## Validation
|
|
133
|
+
|
|
134
|
+
- Define the commands or checks required before completion.
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
async function runSkillCreate(options) {
|
|
138
|
+
const { cwd, name, description, force = false, dryRun = false } = options;
|
|
139
|
+
if (!name) {
|
|
140
|
+
throw new Error('Skill name is required. Usage: ai-workflow skill create <name> [--description="..."] [--dry-run] [--force]');
|
|
141
|
+
}
|
|
142
|
+
validateSkillName(name);
|
|
143
|
+
const normalizedDescription = description === void 0 ? void 0 : requireDescription(description);
|
|
144
|
+
const workspaceRoot = await fs.realpath(cwd);
|
|
145
|
+
const agentsDir = path.join(workspaceRoot, ".agents");
|
|
146
|
+
const skillsDir = path.join(agentsDir, "skills");
|
|
147
|
+
const skillDir = path.join(skillsDir, name);
|
|
148
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
149
|
+
assertWithinWorkspace(workspaceRoot, skillFile, "Skill destination");
|
|
150
|
+
await validateExistingDirectory(agentsDir, workspaceRoot, ".agents directory");
|
|
151
|
+
await validateExistingDirectory(skillsDir, workspaceRoot, "Skills directory");
|
|
152
|
+
await validateExistingDirectory(skillDir, workspaceRoot, `Skill directory '${name}'`, true);
|
|
153
|
+
const skillExists = await validateExistingSkillFile(skillFile);
|
|
154
|
+
if (skillExists && !force) {
|
|
155
|
+
throw new Error(`Skill '${name}' already exists at ${skillFile}. Use --force to overwrite.`);
|
|
156
|
+
}
|
|
157
|
+
const content = buildSkillContent(name, normalizedDescription);
|
|
158
|
+
const branchGate = new BranchGate({ cwd: workspaceRoot });
|
|
159
|
+
const gateResult = branchGate.check("", { taskSlug: `skill-${name}`, readOnly: dryRun });
|
|
160
|
+
if (gateResult.blocked || gateResult.branch === "unknown") {
|
|
161
|
+
throw new Error(`[GATE BLOCKED] ${gateResult.reason || "Git is unavailable or the current branch could not be determined."}`);
|
|
162
|
+
}
|
|
163
|
+
if (dryRun) {
|
|
164
|
+
console.log(`[DRY RUN] Would ${skillExists ? "overwrite" : "create"} skill '${name}' at ${skillFile}`);
|
|
165
|
+
return { status: "dry-run", skillName: name, skillFile, branch: gateResult.branch };
|
|
166
|
+
}
|
|
167
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
168
|
+
await validateExistingDirectory(agentsDir, workspaceRoot, ".agents directory");
|
|
169
|
+
await validateExistingDirectory(skillsDir, workspaceRoot, "Skills directory");
|
|
170
|
+
await validateExistingDirectory(skillDir, workspaceRoot, `Skill directory '${name}'`, true);
|
|
171
|
+
let backupPath;
|
|
172
|
+
if (skillExists) {
|
|
173
|
+
await validateExistingSkillFile(skillFile);
|
|
174
|
+
const backupRoot = path.join(workspaceRoot, BACKUP_ROOT);
|
|
175
|
+
await validateExistingDirectory(backupRoot, workspaceRoot, "Backup directory");
|
|
176
|
+
backupPath = await createManagedBackup(skillFile, { cwd: workspaceRoot, backupRoot: BACKUP_ROOT });
|
|
177
|
+
assertWithinWorkspace(workspaceRoot, await fs.realpath(backupPath), "Backup file");
|
|
178
|
+
await validateExistingDirectory(skillDir, workspaceRoot, `Skill directory '${name}'`, true);
|
|
179
|
+
await validateExistingSkillFile(skillFile);
|
|
180
|
+
const stagedFile = path.join(skillDir, `.SKILL.md.${randomUUID()}.tmp`);
|
|
181
|
+
try {
|
|
182
|
+
await fs.writeFile(stagedFile, content, { encoding: "utf8", flag: "wx" });
|
|
183
|
+
await validateExistingDirectory(skillDir, workspaceRoot, `Skill directory '${name}'`, true);
|
|
184
|
+
await fs.rename(stagedFile, skillFile);
|
|
185
|
+
} finally {
|
|
186
|
+
await fs.rm(stagedFile, { force: true }).catch(() => {
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
try {
|
|
191
|
+
await fs.writeFile(skillFile, content, { encoding: "utf8", flag: "wx" });
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error?.code === "EEXIST") {
|
|
194
|
+
throw new Error(`Skill '${name}' already exists at ${skillFile}. Use --force to overwrite.`);
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const lintErrors = lintSkillFile(skillFile);
|
|
200
|
+
if (lintErrors.length > 0) {
|
|
201
|
+
throw new Error(`Generated skill failed frontmatter lint: ${lintErrors.map((error) => error.message).join("; ")}`);
|
|
202
|
+
}
|
|
203
|
+
const status = skillExists ? "overwritten" : "created";
|
|
204
|
+
console.log(`${status === "created" ? "Created" : "Overwrote"} skill '${name}' at ${skillFile}`);
|
|
205
|
+
return { status, skillName: name, skillFile, backupPath, branch: gateResult.branch };
|
|
206
|
+
}
|
|
207
|
+
export {
|
|
208
|
+
parseSkillCreateArgs,
|
|
209
|
+
runSkillCreate
|
|
210
|
+
};
|
|
211
|
+
//# sourceMappingURL=skill-4MEGJ3DO.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDiscoverableSkillPaths,
|
|
3
|
+
lintSkillFile,
|
|
4
|
+
parseFrontmatter,
|
|
5
|
+
runLinter
|
|
6
|
+
} from "./chunk-YOBY5C72.js";
|
|
7
|
+
import "./chunk-5WRI5ZAA.js";
|
|
8
|
+
export {
|
|
9
|
+
getDiscoverableSkillPaths,
|
|
10
|
+
lintSkillFile,
|
|
11
|
+
parseFrontmatter,
|
|
12
|
+
runLinter
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=skill-frontmatter-linter-FMJADOK4.js.map
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
QualityGuard,
|
|
4
4
|
ValidationPlanner,
|
|
5
5
|
VisualVerifier
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-LD7EHAU2.js";
|
|
7
|
+
import "./chunk-H7GIKXFO.js";
|
|
8
8
|
import "./chunk-5WRI5ZAA.js";
|
|
9
9
|
|
|
10
10
|
// src/cli/commands/validate.ts
|
|
@@ -86,4 +86,4 @@ async function runValidate({
|
|
|
86
86
|
export {
|
|
87
87
|
runValidate
|
|
88
88
|
};
|
|
89
|
-
//# sourceMappingURL=validate-
|
|
89
|
+
//# sourceMappingURL=validate-F3ZH63LI.js.map
|