@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.
@@ -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-BDZPUAEX.js.map
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-W4RTQWVQ.js";
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
  }
@@ -0,0 +1,8 @@
1
+ import {
2
+ EvidenceValidator
3
+ } from "./chunk-FNT7DN3N.js";
4
+ import "./chunk-5WRI5ZAA.js";
5
+ export {
6
+ EvidenceValidator
7
+ };
8
+ //# sourceMappingURL=evidence-validator-HS3NTWAB.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williambeto/ai-workflow",
3
- "version": "2.7.0",
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-BDZPUAEX.js";
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-A46WUBVZ.js.map
89
+ //# sourceMappingURL=validate-IEHLAVZ6.js.map