@williambeto/ai-workflow 2.7.0 → 2.8.0
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 +62 -0
- package/README.md +3 -2
- package/bin/ai-workflow.js +1032 -258
- package/bin/ai-workflow.js.map +1 -1
- package/chunk-AINIR25D.js +120 -0
- package/chunk-AY33SA5W.js +46 -0
- package/{evidence-validator-76ZQQYDU.js → chunk-FNT7DN3N.js} +22 -2
- package/{chunk-W4RTQWVQ.js → chunk-LI76KI7C.js} +977 -820
- package/{chunk-BDZPUAEX.js → chunk-Y4RLP6ZM.js} +11 -3
- package/chunk-YOBY5C72.js +76 -0
- package/core/index.d.ts +32 -1
- package/core/index.js +4 -2
- package/dist-assets/docs/cli-reference.md +47 -2
- package/dist-assets/schemas/approval-receipt.schema.json +35 -0
- package/dist-assets/schemas/evidence.schema.json +33 -0
- 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-IEHLAVZ6.js} +2 -2
|
@@ -407,6 +407,7 @@ var EvidenceCollector = class {
|
|
|
407
407
|
explicitApprovals;
|
|
408
408
|
visualDist;
|
|
409
409
|
port;
|
|
410
|
+
workflowEvidence;
|
|
410
411
|
constructor({
|
|
411
412
|
cwd,
|
|
412
413
|
maxLogLength = 2e3,
|
|
@@ -419,7 +420,8 @@ var EvidenceCollector = class {
|
|
|
419
420
|
userRequest = null,
|
|
420
421
|
explicitApprovals = [],
|
|
421
422
|
visualDist = null,
|
|
422
|
-
port = 8080
|
|
423
|
+
port = 8080,
|
|
424
|
+
workflowEvidence = null
|
|
423
425
|
}) {
|
|
424
426
|
this.cwd = cwd;
|
|
425
427
|
this.maxLogLength = maxLogLength;
|
|
@@ -433,6 +435,7 @@ var EvidenceCollector = class {
|
|
|
433
435
|
this.explicitApprovals = explicitApprovals;
|
|
434
436
|
this.visualDist = visualDist;
|
|
435
437
|
this.port = Number(port || 8080);
|
|
438
|
+
this.workflowEvidence = workflowEvidence;
|
|
436
439
|
}
|
|
437
440
|
async findUserRequest() {
|
|
438
441
|
if (this.userRequest) return this.userRequest;
|
|
@@ -607,7 +610,12 @@ var EvidenceCollector = class {
|
|
|
607
610
|
limitations,
|
|
608
611
|
deliveryDecision,
|
|
609
612
|
artifactFidelityCheck,
|
|
610
|
-
visualEvidence
|
|
613
|
+
visualEvidence,
|
|
614
|
+
workflowId: this.workflowEvidence?.workflowId || void 0,
|
|
615
|
+
baseSha: this.workflowEvidence?.baseSha || void 0,
|
|
616
|
+
artifactHashes: this.workflowEvidence?.artifactHashes || void 0,
|
|
617
|
+
phaseOrder: this.workflowEvidence?.phaseOrder || void 0,
|
|
618
|
+
confirmedActors: this.workflowEvidence?.confirmedActors || void 0
|
|
611
619
|
};
|
|
612
620
|
if (writeArtifact) {
|
|
613
621
|
await fs3.writeFile(path3.join(this.cwd, "EVIDENCE.json"), JSON.stringify(evidence, null, 2));
|
|
@@ -731,4 +739,4 @@ export {
|
|
|
731
739
|
EvidenceCollector,
|
|
732
740
|
ValidationPlanner
|
|
733
741
|
};
|
|
734
|
-
//# sourceMappingURL=chunk-
|
|
742
|
+
//# sourceMappingURL=chunk-Y4RLP6ZM.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/linting/skill-frontmatter-linter.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
function parseFrontmatter(content) {
|
|
6
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
7
|
+
if (!match) return {};
|
|
8
|
+
const lines = match[1].split("\n");
|
|
9
|
+
const data = {};
|
|
10
|
+
for (const line of lines) {
|
|
11
|
+
const colonIdx = line.indexOf(":");
|
|
12
|
+
if (colonIdx !== -1) {
|
|
13
|
+
const key = line.substring(0, colonIdx).trim();
|
|
14
|
+
const value = line.substring(colonIdx + 1).trim();
|
|
15
|
+
data[key] = value.replace(/^["'](.*)["']$/, "$1");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
function lintSkillFile(filePath) {
|
|
21
|
+
const errors = [];
|
|
22
|
+
if (!fs.existsSync(filePath)) return errors;
|
|
23
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
24
|
+
const frontmatter = parseFrontmatter(content);
|
|
25
|
+
if (!frontmatter.name) {
|
|
26
|
+
errors.push({ filePath, message: "Missing or empty 'name' field in frontmatter" });
|
|
27
|
+
}
|
|
28
|
+
if (!frontmatter.description) {
|
|
29
|
+
errors.push({ filePath, message: "Missing or empty 'description' field in frontmatter" });
|
|
30
|
+
}
|
|
31
|
+
return errors;
|
|
32
|
+
}
|
|
33
|
+
function findSkillDirs(baseDir) {
|
|
34
|
+
if (!fs.existsSync(baseDir)) return [];
|
|
35
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
36
|
+
return entries.filter((e) => e.isDirectory()).map((e) => path.join(baseDir, e.name));
|
|
37
|
+
}
|
|
38
|
+
function getDiscoverableSkillPaths(cwd) {
|
|
39
|
+
const pathsToSearch = [
|
|
40
|
+
path.join(cwd, ".agents/skills"),
|
|
41
|
+
path.join(cwd, ".ai-workflow/opencode/skills"),
|
|
42
|
+
// Installed by kit
|
|
43
|
+
path.join(cwd, "dist-assets/skills"),
|
|
44
|
+
// DevKit source
|
|
45
|
+
path.join(os.homedir(), ".gemini/config/skills"),
|
|
46
|
+
path.join(os.homedir(), ".gemini/antigravity-cli/builtin/skills")
|
|
47
|
+
];
|
|
48
|
+
const skillFiles = [];
|
|
49
|
+
for (const basePath of pathsToSearch) {
|
|
50
|
+
const dirs = findSkillDirs(basePath);
|
|
51
|
+
for (const dir of dirs) {
|
|
52
|
+
const skillFile = path.join(dir, "SKILL.md");
|
|
53
|
+
if (fs.existsSync(skillFile)) {
|
|
54
|
+
skillFiles.push(skillFile);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return skillFiles;
|
|
59
|
+
}
|
|
60
|
+
function runLinter(cwd) {
|
|
61
|
+
const files = getDiscoverableSkillPaths(cwd);
|
|
62
|
+
const allErrors = [];
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const fileErrors = lintSkillFile(file);
|
|
65
|
+
allErrors.push(...fileErrors);
|
|
66
|
+
}
|
|
67
|
+
return { errors: allErrors, filesScanned: files.length };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
parseFrontmatter,
|
|
72
|
+
lintSkillFile,
|
|
73
|
+
getDiscoverableSkillPaths,
|
|
74
|
+
runLinter
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=chunk-YOBY5C72.js.map
|
package/core/index.d.ts
CHANGED
|
@@ -133,6 +133,8 @@ declare class EvidenceLedger {
|
|
|
133
133
|
cwd: string;
|
|
134
134
|
workflowId: string;
|
|
135
135
|
events: LedgerEvent[];
|
|
136
|
+
private loadedPath;
|
|
137
|
+
private loadedContentHash;
|
|
136
138
|
constructor({ cwd, workflowId }?: {
|
|
137
139
|
cwd?: string;
|
|
138
140
|
workflowId?: string;
|
|
@@ -154,6 +156,7 @@ declare class EvidenceLedger {
|
|
|
154
156
|
* Saves the ledger events to a JSON file.
|
|
155
157
|
*/
|
|
156
158
|
save(filePath: string): Promise<void>;
|
|
159
|
+
load(filePath: string): Promise<boolean>;
|
|
157
160
|
verifyRoleConfinement(): Promise<RoleConfinementResult>;
|
|
158
161
|
}
|
|
159
162
|
|
|
@@ -161,6 +164,7 @@ interface InspectResult {
|
|
|
161
164
|
available: boolean;
|
|
162
165
|
supports: {
|
|
163
166
|
run: boolean;
|
|
167
|
+
server: boolean;
|
|
164
168
|
formatJson: boolean;
|
|
165
169
|
agent: boolean;
|
|
166
170
|
model: boolean;
|
|
@@ -171,6 +175,7 @@ interface ExecuteOptions {
|
|
|
171
175
|
model?: string | null;
|
|
172
176
|
readOnly?: boolean;
|
|
173
177
|
fastTrack?: boolean;
|
|
178
|
+
requireActorConfirmation?: boolean;
|
|
174
179
|
}
|
|
175
180
|
interface ExecuteResult {
|
|
176
181
|
success: boolean;
|
|
@@ -182,6 +187,8 @@ interface ExecuteResult {
|
|
|
182
187
|
runtimeAgentApplied?: string | null;
|
|
183
188
|
runtimeAgentSupported?: boolean;
|
|
184
189
|
runtimeAgentSelectionMode?: string;
|
|
190
|
+
runtimeActorConfirmation?: "confirmed" | "unavailable";
|
|
191
|
+
output?: string;
|
|
185
192
|
}
|
|
186
193
|
/**
|
|
187
194
|
* OpenCodeAdapter - Real runtime integration with the OpenCode CLI.
|
|
@@ -198,7 +205,7 @@ declare class OpenCodeAdapter {
|
|
|
198
205
|
/**
|
|
199
206
|
* Runs opencode with a prompt and options.
|
|
200
207
|
*/
|
|
201
|
-
execute(message: string, { agent, model, readOnly, fastTrack }?: ExecuteOptions): Promise<ExecuteResult>;
|
|
208
|
+
execute(message: string, { agent, model, readOnly, fastTrack, requireActorConfirmation }?: ExecuteOptions): Promise<ExecuteResult>;
|
|
202
209
|
}
|
|
203
210
|
|
|
204
211
|
interface DelegationControllerOptions {
|
|
@@ -206,6 +213,22 @@ interface DelegationControllerOptions {
|
|
|
206
213
|
ledger?: EvidenceLedger;
|
|
207
214
|
adapter?: OpenCodeAdapter;
|
|
208
215
|
}
|
|
216
|
+
interface PhaseArtifactResult {
|
|
217
|
+
artifactPath?: string;
|
|
218
|
+
artifactHash?: string;
|
|
219
|
+
[key: string]: unknown;
|
|
220
|
+
}
|
|
221
|
+
interface DelegationPhaseOptions {
|
|
222
|
+
phase: "specification" | "planning" | "implementation";
|
|
223
|
+
actor: "Nexus" | "Orion" | "Astra";
|
|
224
|
+
prompt: string;
|
|
225
|
+
readOnly: boolean;
|
|
226
|
+
requireExplicitActor?: boolean;
|
|
227
|
+
onOutput?: (output: string) => Promise<PhaseArtifactResult | void>;
|
|
228
|
+
}
|
|
229
|
+
interface DelegationPhaseResult extends ExecuteResult {
|
|
230
|
+
artifact?: PhaseArtifactResult;
|
|
231
|
+
}
|
|
209
232
|
/**
|
|
210
233
|
* DelegationController - Manages and logs coordination between Atlas, Astra, Sage, and Phoenix.
|
|
211
234
|
*/
|
|
@@ -219,6 +242,11 @@ declare class DelegationController {
|
|
|
219
242
|
* Logs routing decision by Atlas.
|
|
220
243
|
*/
|
|
221
244
|
route(naturalRequest: string, classification: RequestClassifierResult): Promise<void>;
|
|
245
|
+
/**
|
|
246
|
+
* Executes one high-risk phase and fails closed unless the runtime confirms
|
|
247
|
+
* the requested actor through machine-readable output.
|
|
248
|
+
*/
|
|
249
|
+
executePhase({ phase, actor, prompt, readOnly, requireExplicitActor, onOutput }: DelegationPhaseOptions): Promise<DelegationPhaseResult>;
|
|
222
250
|
/**
|
|
223
251
|
* Logs and executes the implementation process by Astra.
|
|
224
252
|
*/
|
|
@@ -426,11 +454,14 @@ interface SpecValidationResult {
|
|
|
426
454
|
reason?: string;
|
|
427
455
|
tier?: string;
|
|
428
456
|
}
|
|
457
|
+
type RequiredSpecStatus = "DRAFT" | "APPROVED";
|
|
429
458
|
/**
|
|
430
459
|
* SpecValidator - Enforces "No Spec, No Code" rule.
|
|
431
460
|
* Validates existence, structure, and approval status of specifications.
|
|
432
461
|
*/
|
|
433
462
|
declare class SpecValidator {
|
|
463
|
+
validateContent(content: string, requiredStatus?: RequiredSpecStatus): SpecValidationResult;
|
|
464
|
+
validateDraftContent(content: string): SpecValidationResult;
|
|
434
465
|
/**
|
|
435
466
|
* Validates a specification file.
|
|
436
467
|
*/
|
package/core/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
BranchGate,
|
|
3
2
|
DelegationController,
|
|
4
3
|
EvidenceLedger,
|
|
5
4
|
ExecutionPlanner,
|
|
@@ -10,10 +9,13 @@ import {
|
|
|
10
9
|
RequestClassifier,
|
|
11
10
|
SpecValidator,
|
|
12
11
|
WorkflowStateMachine
|
|
13
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-LI76KI7C.js";
|
|
14
13
|
import {
|
|
15
14
|
MergeGate
|
|
16
15
|
} from "../chunk-XW747GIG.js";
|
|
16
|
+
import {
|
|
17
|
+
BranchGate
|
|
18
|
+
} from "../chunk-AINIR25D.js";
|
|
17
19
|
import "../chunk-5WRI5ZAA.js";
|
|
18
20
|
export {
|
|
19
21
|
BranchGate,
|
|
@@ -75,19 +75,21 @@ Runs formal orchestration for an approved specification with branch safety, obse
|
|
|
75
75
|
|
|
76
76
|
**Synopsis:**
|
|
77
77
|
```bash
|
|
78
|
-
ai-workflow run --spec-path=<path>
|
|
78
|
+
ai-workflow run --spec-path=<path> [--approval-path=<path>]
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
**Description:**
|
|
82
|
-
Reads an approved specification file and orchestrates the full implementation workflow. Enforces branch gate safety, runs validation commands, applies bounded remediation (up to configured attempt limits), and generates a handoff summary.
|
|
82
|
+
Reads an approved specification file and orchestrates the full implementation workflow. Enforces branch gate safety, runs validation commands, applies bounded remediation (up to configured attempt limits), and generates a handoff summary. DEEP specifications also require a hash-bound approval receipt created after human review.
|
|
83
83
|
|
|
84
84
|
| Flag | Description |
|
|
85
85
|
| --- | --- |
|
|
86
86
|
| `--spec-path=<path>` | Path to the approved specification file (required) |
|
|
87
|
+
| `--approval-path=<path>` | Path to the approval receipt required for DEEP specifications |
|
|
87
88
|
|
|
88
89
|
**Example:**
|
|
89
90
|
```bash
|
|
90
91
|
ai-workflow run --spec-path=.ai-workflow/specs/feature-auth.md
|
|
92
|
+
ai-workflow run --spec-path=docs/workflows/auth/spec.md --approval-path=.ai-workflow/approvals/auth.json
|
|
91
93
|
```
|
|
92
94
|
|
|
93
95
|
---
|
|
@@ -196,6 +198,49 @@ ai-workflow clean --purge-agents # Remove everything including .agents/
|
|
|
196
198
|
|
|
197
199
|
---
|
|
198
200
|
|
|
201
|
+
## `ai-workflow skill create`
|
|
202
|
+
|
|
203
|
+
Creates a workspace-local skill scaffold.
|
|
204
|
+
|
|
205
|
+
**Synopsis:**
|
|
206
|
+
```bash
|
|
207
|
+
ai-workflow skill create <name> [--description=<description>] [--dry-run] [--force]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Description:**
|
|
211
|
+
Creates `.agents/skills/<name>/SKILL.md` with the required `name` and `description` YAML frontmatter. Skill names must use lowercase kebab-case. The command validates the destination before applying the branch gate and rejects paths or symbolic links detected outside the workspace.
|
|
212
|
+
|
|
213
|
+
| Flag | Description |
|
|
214
|
+
| --- | --- |
|
|
215
|
+
| `--description=<description>` | Search-friendly description written to the YAML frontmatter |
|
|
216
|
+
| `--dry-run` | Show the target and planned action without creating a branch or writing files |
|
|
217
|
+
| `--force` | Back up and overwrite an existing `SKILL.md`; preserves auxiliary files |
|
|
218
|
+
|
|
219
|
+
**Example:**
|
|
220
|
+
```bash
|
|
221
|
+
ai-workflow skill create database-migrations \
|
|
222
|
+
--description="Guidelines for safe database migrations"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The generated folder can also contain optional `scripts/`, `examples/`, and `references/` directories. Workspace-local skills are discovered automatically and take precedence over installed or global skills.
|
|
226
|
+
|
|
227
|
+
The containment checks protect against unsafe paths present when the command runs. As with other local filesystem tools, they do not defend against another process concurrently replacing validated directories while the write is in progress.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## `ai-workflow lint skills`
|
|
232
|
+
|
|
233
|
+
Checks the required YAML frontmatter of every discoverable `SKILL.md`.
|
|
234
|
+
|
|
235
|
+
**Synopsis:**
|
|
236
|
+
```bash
|
|
237
|
+
ai-workflow lint skills
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The command scans workspace-local, installed, development, and supported global skill locations. It exits with a non-zero status when a skill is missing a non-empty `name` or `description` field.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
199
244
|
## Public Statuses
|
|
200
245
|
|
|
201
246
|
All workflow commands that produce delivery summaries use these statuses:
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://github.com/williambeto/ai-workflow/schemas/approval-receipt.schema.json",
|
|
3
|
+
"title": "Human Approval Receipt",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": [
|
|
7
|
+
"workflowId",
|
|
8
|
+
"approvedSpecHash",
|
|
9
|
+
"approvedBy",
|
|
10
|
+
"approvedAt",
|
|
11
|
+
"authorizationSource"
|
|
12
|
+
],
|
|
13
|
+
"properties": {
|
|
14
|
+
"workflowId": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$"
|
|
17
|
+
},
|
|
18
|
+
"approvedSpecHash": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
21
|
+
},
|
|
22
|
+
"approvedBy": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"minLength": 1
|
|
25
|
+
},
|
|
26
|
+
"approvedAt": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"format": "date-time"
|
|
29
|
+
},
|
|
30
|
+
"authorizationSource": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"minLength": 1
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -104,6 +104,39 @@
|
|
|
104
104
|
"visualEvidence": {
|
|
105
105
|
"type": ["object", "null"],
|
|
106
106
|
"additionalProperties": true
|
|
107
|
+
},
|
|
108
|
+
"workflowId": {
|
|
109
|
+
"type": "string"
|
|
110
|
+
},
|
|
111
|
+
"baseSha": {
|
|
112
|
+
"type": "string"
|
|
113
|
+
},
|
|
114
|
+
"artifactHashes": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"additionalProperties": false,
|
|
117
|
+
"required": ["draft", "approved", "technicalPlan"],
|
|
118
|
+
"properties": {
|
|
119
|
+
"draft": { "type": ["string", "null"], "pattern": "^[a-f0-9]{64}$" },
|
|
120
|
+
"approved": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
121
|
+
"technicalPlan": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"phaseOrder": {
|
|
125
|
+
"type": "array",
|
|
126
|
+
"items": { "enum": ["specification", "planning", "implementation"] }
|
|
127
|
+
},
|
|
128
|
+
"confirmedActors": {
|
|
129
|
+
"type": "array",
|
|
130
|
+
"items": {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"additionalProperties": false,
|
|
133
|
+
"required": ["phase", "requested", "applied"],
|
|
134
|
+
"properties": {
|
|
135
|
+
"phase": { "enum": ["specification", "planning", "implementation"] },
|
|
136
|
+
"requested": { "enum": ["Nexus", "Orion", "Astra"] },
|
|
137
|
+
"applied": { "enum": ["Nexus", "Orion", "Astra"] }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
107
140
|
}
|
|
108
141
|
}
|
|
109
142
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williambeto/ai-workflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
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,7 +3,7 @@ import {
|
|
|
3
3
|
QualityGuard,
|
|
4
4
|
ValidationPlanner,
|
|
5
5
|
VisualVerifier
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-Y4RLP6ZM.js";
|
|
7
7
|
import "./chunk-XW747GIG.js";
|
|
8
8
|
import "./chunk-5WRI5ZAA.js";
|
|
9
9
|
|
|
@@ -86,4 +86,4 @@ async function runValidate({
|
|
|
86
86
|
export {
|
|
87
87
|
runValidate
|
|
88
88
|
};
|
|
89
|
-
//# sourceMappingURL=validate-
|
|
89
|
+
//# sourceMappingURL=validate-IEHLAVZ6.js.map
|