wogiflow 2.32.0 → 2.33.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/.claude/docs/claude-code-compatibility.md +51 -0
- package/.claude/docs/scheduled-mode.md +213 -0
- package/.claude/docs/skill-portability.md +190 -0
- package/.claude/rules/alternative-hook-args-exec-form.md +6 -0
- package/.claude/settings.json +2 -1
- package/.claude/skills/_template/skill.md +1 -0
- package/.claude/skills/conventional-commit/knowledge/examples.md +65 -0
- package/.claude/skills/conventional-commit/skill.md +76 -0
- package/bin/flow +16 -0
- package/lib/scheduled-mode.js +377 -0
- package/lib/skill-export-agentskills.js +211 -0
- package/lib/skill-export-claude-plugin.js +143 -0
- package/lib/skill-portability.js +324 -0
- package/lib/skill-registry.js +32 -2
- package/package.json +2 -2
- package/scripts/flow +8 -0
- package/scripts/flow-config-defaults.js +20 -0
- package/scripts/flow-schedule.js +469 -0
- package/scripts/flow-scheduled-runner.js +614 -0
- package/scripts/flow-skill-export.js +334 -0
- package/scripts/hooks/adapters/claude-code.js +12 -1
- package/scripts/hooks/core/git-safety-gate.js +92 -20
- package/scripts/hooks/core/long-input-enforcement.js +139 -4
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Skill Exporter — Claude Code plugin format (Phase 1B — wf-0342fc33)
|
|
5
|
+
*
|
|
6
|
+
* Produces a Claude Code plugin manifest + file list suitable for the
|
|
7
|
+
* `claude plugin tag` distribution path (Claude Code 2.1.118+).
|
|
8
|
+
*
|
|
9
|
+
* Layout produced (the file map this module returns; `flow skill export`
|
|
10
|
+
* is responsible for actually writing these to disk):
|
|
11
|
+
*
|
|
12
|
+
* .claude-plugin/plugin.json (root manifest)
|
|
13
|
+
* skills/<name>/SKILL.md (the skill itself)
|
|
14
|
+
* skills/<name>/<...other files> (knowledge/, templates/, etc.)
|
|
15
|
+
*
|
|
16
|
+
* The plugin.json shape mirrors what shipping Claude Code plugins use
|
|
17
|
+
* (see e.g., the official Figma plugin's `.claude-plugin/plugin.json`):
|
|
18
|
+
*
|
|
19
|
+
* {
|
|
20
|
+
* name: string, // plugin identifier
|
|
21
|
+
* description: string,
|
|
22
|
+
* version: string,
|
|
23
|
+
* author: { name: string } | string,
|
|
24
|
+
* license: string // optional
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* Like the agentskills exporter, this module assumes the caller has already
|
|
28
|
+
* run the portability checker. We don't double-verify.
|
|
29
|
+
*
|
|
30
|
+
* @module lib/skill-export-claude-plugin
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
const fs = require('node:fs');
|
|
36
|
+
const path = require('node:path');
|
|
37
|
+
|
|
38
|
+
const { parseFrontmatter } = require('./skill-portability');
|
|
39
|
+
const { listBundleFiles } = require('./skill-export-agentskills');
|
|
40
|
+
|
|
41
|
+
const DEFAULT_LICENSE = 'MIT';
|
|
42
|
+
const DEFAULT_AUTHOR = 'wogiflow';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read a file safely, returning empty string on failure.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} filePath
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
function safeReadFile(filePath) {
|
|
51
|
+
try {
|
|
52
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
53
|
+
} catch (_err) {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Export a skill to Claude Code plugin format.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} skillDir - Absolute path to skill directory
|
|
62
|
+
* @param {Object} [opts]
|
|
63
|
+
* @param {string} [opts.name] - Override plugin name (default: from frontmatter or dir basename)
|
|
64
|
+
* @param {string} [opts.version] - Override version (default: from frontmatter or 0.0.0)
|
|
65
|
+
* @param {string} [opts.author] - Override author name (default: DEFAULT_AUTHOR)
|
|
66
|
+
* @returns {{manifest: Object, files: Array<{path: string, content: string}>, skillMdPath: string|null}}
|
|
67
|
+
*/
|
|
68
|
+
function exportToClaudePlugin(skillDir, opts = {}) {
|
|
69
|
+
if (typeof skillDir !== 'string' || !skillDir) {
|
|
70
|
+
throw new Error('exportToClaudePlugin: skillDir must be a non-empty string');
|
|
71
|
+
}
|
|
72
|
+
if (!fs.existsSync(skillDir) || !fs.statSync(skillDir).isDirectory()) {
|
|
73
|
+
throw new Error(`exportToClaudePlugin: not a directory: ${skillDir}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Locate skill.md (we tolerate case variants; output is normalized to SKILL.md
|
|
77
|
+
// which is the prevailing convention in shipping Claude Code plugins).
|
|
78
|
+
let skillMdPath = null;
|
|
79
|
+
for (const candidate of ['skill.md', 'SKILL.md', 'Skill.md']) {
|
|
80
|
+
const p = path.join(skillDir, candidate);
|
|
81
|
+
if (fs.existsSync(p)) {
|
|
82
|
+
skillMdPath = p;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let frontmatter = {};
|
|
88
|
+
if (skillMdPath) {
|
|
89
|
+
frontmatter = parseFrontmatter(safeReadFile(skillMdPath));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const name = opts.name ?? frontmatter.name ?? path.basename(skillDir);
|
|
93
|
+
const version = opts.version ?? frontmatter.version ?? '0.0.0';
|
|
94
|
+
const description = frontmatter.description ?? '';
|
|
95
|
+
const license = frontmatter.license ?? DEFAULT_LICENSE;
|
|
96
|
+
const author = opts.author ?? DEFAULT_AUTHOR;
|
|
97
|
+
|
|
98
|
+
// Plugin manifest — matches the .claude-plugin/plugin.json shape used by
|
|
99
|
+
// shipping Claude Code plugins. Keeping it tight: name/description/version/
|
|
100
|
+
// author/license. Future plugin fields (commands, hooks, mcpServers) are
|
|
101
|
+
// additive — leave room but don't speculatively fill them in.
|
|
102
|
+
const manifest = {
|
|
103
|
+
name,
|
|
104
|
+
description,
|
|
105
|
+
version,
|
|
106
|
+
author: { name: author },
|
|
107
|
+
license,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Build file bundle. The Claude Code plugin layout puts the skill under
|
|
111
|
+
// `skills/<name>/...`, so we re-root every file under that prefix.
|
|
112
|
+
// skill.md is normalized to SKILL.md in the destination.
|
|
113
|
+
const absFiles = listBundleFiles(skillDir);
|
|
114
|
+
const files = [
|
|
115
|
+
// The plugin.json manifest itself
|
|
116
|
+
{
|
|
117
|
+
path: '.claude-plugin/plugin.json',
|
|
118
|
+
content: JSON.stringify(manifest, null, 2) + '\n',
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
for (const abs of absFiles) {
|
|
123
|
+
const relRaw = path.relative(skillDir, abs) || path.basename(abs);
|
|
124
|
+
const rel = relRaw.split(path.sep).join('/');
|
|
125
|
+
let destName = rel;
|
|
126
|
+
|
|
127
|
+
// Normalize skill.md → SKILL.md at the skill root only.
|
|
128
|
+
if (rel === 'skill.md' || rel === 'Skill.md') {
|
|
129
|
+
destName = 'SKILL.md';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
files.push({
|
|
133
|
+
path: `skills/${name}/${destName}`,
|
|
134
|
+
content: safeReadFile(abs),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { manifest, files, skillMdPath };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
exportToClaudePlugin,
|
|
143
|
+
};
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Skill Portability Checker (Phase 1B — wf-0342fc33)
|
|
5
|
+
*
|
|
6
|
+
* Determines whether a skill is portable to the broader Claude Code / agentskills.io
|
|
7
|
+
* ecosystem. A "portable" skill MUST NOT reference WogiFlow-specific paths, state files,
|
|
8
|
+
* imports, or slash-command invocations — because those won't exist outside this project.
|
|
9
|
+
*
|
|
10
|
+
* Fail-loud: every blocker is cited with `path:line`. Callers MUST refuse export when
|
|
11
|
+
* `portable === false`.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const { assessSkillPortability } = require('./skill-portability');
|
|
15
|
+
* const result = assessSkillPortability('/path/to/.claude/skills/commit');
|
|
16
|
+
* // result = { portable: true, blockers: [], manifest: {...}, scannedFiles: [...] }
|
|
17
|
+
*
|
|
18
|
+
* @module lib/skill-portability
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Blocker patterns
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Each entry: { pattern: RegExp, label: string }
|
|
30
|
+
//
|
|
31
|
+
// The pattern is matched line-by-line against every text file under the skill
|
|
32
|
+
// directory. If any line matches any pattern, the skill is non-portable.
|
|
33
|
+
//
|
|
34
|
+
// NOTE on scoping: we deliberately use *substring* matches on canonical WogiFlow
|
|
35
|
+
// path strings rather than language-level imports, because skill content is
|
|
36
|
+
// mostly Markdown. A reference like ".workflow/state/ready.json" in a how-to
|
|
37
|
+
// guide is just as project-locking as a literal `require('./scripts/flow-utils')`.
|
|
38
|
+
|
|
39
|
+
const BLOCKER_PATTERNS = [
|
|
40
|
+
// State-file references
|
|
41
|
+
{ pattern: /\.workflow\//, label: 'wogiflow-state-path (.workflow/)' },
|
|
42
|
+
{ pattern: /\bwogiflow-cloud\b/, label: 'wogiflow-cloud reference' },
|
|
43
|
+
{ pattern: /\bready\.json\b/, label: 'ready.json reference' },
|
|
44
|
+
{ pattern: /\bfeedback-patterns\.md\b/, label: 'feedback-patterns.md reference' },
|
|
45
|
+
{ pattern: /\bdecisions\.md\b/, label: 'decisions.md reference' },
|
|
46
|
+
{ pattern: /\bapp-map\.md\b/, label: 'app-map.md reference' },
|
|
47
|
+
{ pattern: /\bfunction-map\.md\b/, label: 'function-map.md reference' },
|
|
48
|
+
{ pattern: /\bapi-map\.md\b/, label: 'api-map.md reference' },
|
|
49
|
+
// Imports / requires of WogiFlow modules
|
|
50
|
+
{ pattern: /\bflow-utils\b/, label: 'flow-utils import/reference' },
|
|
51
|
+
{ pattern: /require\(['"][^'"]*\/scripts\/flow[-/]/, label: 'WogiFlow scripts/ require()' },
|
|
52
|
+
{ pattern: /from\s+['"][^'"]*\/scripts\/flow[-/]/, label: 'WogiFlow scripts/ import' },
|
|
53
|
+
// Slash-command invocations (any /wogi-* with a word char after)
|
|
54
|
+
{ pattern: /\/wogi-[a-z][a-z0-9-]*/i, label: '/wogi-* slash command' },
|
|
55
|
+
// Shell invocations of the local flow CLI
|
|
56
|
+
{ pattern: /\.\/scripts\/flow\b/, label: 'local ./scripts/flow CLI call' },
|
|
57
|
+
{ pattern: /\bflow\s+(?:wogi-|skill\s+|story\s+|start\s+|status\b|ready\b|finalize\b)/, label: 'flow CLI subcommand specific to WogiFlow' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// File extensions we scan for blockers.
|
|
61
|
+
const SCAN_EXTENSIONS = new Set(['.md', '.markdown', '.txt', '.yaml', '.yml', '.json', '.js', '.ts', '.sh']);
|
|
62
|
+
|
|
63
|
+
// Max file size to scan (defense against accidentally enormous fixtures).
|
|
64
|
+
const MAX_SCAN_BYTES = 1 * 1024 * 1024; // 1 MiB
|
|
65
|
+
|
|
66
|
+
// Max files to scan (defense against deeply nested fixture trees).
|
|
67
|
+
const MAX_SCAN_FILES = 200;
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Frontmatter parser (kept local — same shape as flow-skill-freshness.js)
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse YAML frontmatter from a skill.md file. Handles colons in values
|
|
77
|
+
* by splitting on the first colon only. Blocks prototype pollution keys.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} content - File content
|
|
80
|
+
* @returns {Object} Parsed frontmatter key-value pairs (empty object on miss)
|
|
81
|
+
*/
|
|
82
|
+
function parseFrontmatter(content) {
|
|
83
|
+
if (typeof content !== 'string') return {};
|
|
84
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
85
|
+
if (!match) return {};
|
|
86
|
+
|
|
87
|
+
const result = {};
|
|
88
|
+
for (const rawLine of match[1].split(/\r?\n/)) {
|
|
89
|
+
const line = rawLine.replace(/\s+$/, '');
|
|
90
|
+
if (!line || line.startsWith('#')) continue;
|
|
91
|
+
// Skip list-item lines (we don't parse arrays here — caller pulls those via dedicated logic)
|
|
92
|
+
if (line.startsWith('- ')) continue;
|
|
93
|
+
const colonIdx = line.indexOf(':');
|
|
94
|
+
if (colonIdx === -1) continue;
|
|
95
|
+
const key = line.slice(0, colonIdx).trim();
|
|
96
|
+
if (!key || DANGEROUS_KEYS.has(key)) continue;
|
|
97
|
+
const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, '');
|
|
98
|
+
result[key] = value;
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Directory walker
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Walk a directory and return absolute paths of files we should scan.
|
|
109
|
+
* Skips hidden dirs (except the skill root itself), node_modules, large files.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} rootDir - Absolute path to skill directory
|
|
112
|
+
* @returns {string[]} Absolute file paths, capped at MAX_SCAN_FILES
|
|
113
|
+
*/
|
|
114
|
+
function listScanFiles(rootDir) {
|
|
115
|
+
const out = [];
|
|
116
|
+
const stack = [rootDir];
|
|
117
|
+
|
|
118
|
+
while (stack.length > 0 && out.length < MAX_SCAN_FILES) {
|
|
119
|
+
const dir = stack.pop();
|
|
120
|
+
let entries;
|
|
121
|
+
try {
|
|
122
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
123
|
+
} catch (_err) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
const full = path.join(dir, entry.name);
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
130
|
+
stack.push(full);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!entry.isFile()) continue;
|
|
134
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
135
|
+
if (!SCAN_EXTENSIONS.has(ext)) continue;
|
|
136
|
+
let stat;
|
|
137
|
+
try {
|
|
138
|
+
stat = fs.statSync(full);
|
|
139
|
+
} catch (_err) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (stat.size > MAX_SCAN_BYTES) continue;
|
|
143
|
+
out.push(full);
|
|
144
|
+
if (out.length >= MAX_SCAN_FILES) break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Core: assessSkillPortability
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Assess whether a skill directory is portable to non-WogiFlow consumers.
|
|
156
|
+
*
|
|
157
|
+
* Algorithm:
|
|
158
|
+
* 1. Validate the directory exists and has a `skill.md` (or SKILL.md).
|
|
159
|
+
* 2. Parse the skill.md frontmatter (used by exporters for manifest fields).
|
|
160
|
+
* 3. Walk the directory, scanning every text-y file line by line.
|
|
161
|
+
* 4. For each line, test all BLOCKER_PATTERNS; collect citations.
|
|
162
|
+
* 5. portable = blockers.length === 0.
|
|
163
|
+
*
|
|
164
|
+
* If the frontmatter declares `portable: false` explicitly, that wins even
|
|
165
|
+
* when the content scan finds nothing — the skill author is signaling an
|
|
166
|
+
* implicit-dependency the scanner can't detect (e.g., relies on a project
|
|
167
|
+
* convention that isn't a literal path string).
|
|
168
|
+
*
|
|
169
|
+
* If the frontmatter declares `portable: true` but the scanner finds blockers,
|
|
170
|
+
* the scanner wins — fail-loud is the priority.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} skillDir - Absolute path to skill directory
|
|
173
|
+
* @param {Object} [opts]
|
|
174
|
+
* @param {RegExp[]} [opts.extraPatterns] - Additional blocker patterns
|
|
175
|
+
* @returns {{portable: boolean, blockers: Array<{file: string, line: number, match: string, label: string}>, manifest: Object, scannedFiles: string[], skillMdPath: string|null}}
|
|
176
|
+
*/
|
|
177
|
+
function assessSkillPortability(skillDir, opts = {}) {
|
|
178
|
+
if (typeof skillDir !== 'string' || !skillDir) {
|
|
179
|
+
return {
|
|
180
|
+
portable: false,
|
|
181
|
+
blockers: [{ file: '<input>', line: 0, match: '', label: 'invalid skill directory (empty path)' }],
|
|
182
|
+
manifest: {},
|
|
183
|
+
scannedFiles: [],
|
|
184
|
+
skillMdPath: null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let stat;
|
|
189
|
+
try {
|
|
190
|
+
stat = fs.statSync(skillDir);
|
|
191
|
+
} catch (_err) {
|
|
192
|
+
return {
|
|
193
|
+
portable: false,
|
|
194
|
+
blockers: [{ file: skillDir, line: 0, match: '', label: 'skill directory does not exist' }],
|
|
195
|
+
manifest: {},
|
|
196
|
+
scannedFiles: [],
|
|
197
|
+
skillMdPath: null,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (!stat.isDirectory()) {
|
|
201
|
+
return {
|
|
202
|
+
portable: false,
|
|
203
|
+
blockers: [{ file: skillDir, line: 0, match: '', label: 'skill path is not a directory' }],
|
|
204
|
+
manifest: {},
|
|
205
|
+
scannedFiles: [],
|
|
206
|
+
skillMdPath: null,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Locate skill.md (case variants accepted)
|
|
211
|
+
let skillMdPath = null;
|
|
212
|
+
for (const candidate of ['skill.md', 'SKILL.md', 'Skill.md']) {
|
|
213
|
+
const p = path.join(skillDir, candidate);
|
|
214
|
+
if (fs.existsSync(p)) {
|
|
215
|
+
skillMdPath = p;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let manifest = {};
|
|
221
|
+
if (skillMdPath) {
|
|
222
|
+
try {
|
|
223
|
+
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
224
|
+
manifest = parseFrontmatter(content);
|
|
225
|
+
} catch (_err) {
|
|
226
|
+
// Continue; missing manifest is itself a portability concern handled below.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const blockers = [];
|
|
231
|
+
if (!skillMdPath) {
|
|
232
|
+
blockers.push({
|
|
233
|
+
file: skillDir,
|
|
234
|
+
line: 0,
|
|
235
|
+
match: '',
|
|
236
|
+
label: 'skill.md not found at skill root',
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Explicit author declaration: portable: false short-circuits scanning.
|
|
241
|
+
const declaredPortable = typeof manifest.portable === 'string'
|
|
242
|
+
? manifest.portable.toLowerCase() === 'true'
|
|
243
|
+
: null;
|
|
244
|
+
if (declaredPortable === false) {
|
|
245
|
+
blockers.push({
|
|
246
|
+
file: skillMdPath ?? skillDir,
|
|
247
|
+
line: 0,
|
|
248
|
+
match: 'portable: false',
|
|
249
|
+
label: 'manifest declares portable: false',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Compose pattern list: builtin + extras.
|
|
254
|
+
const patterns = [...BLOCKER_PATTERNS];
|
|
255
|
+
if (Array.isArray(opts.extraPatterns)) {
|
|
256
|
+
for (const p of opts.extraPatterns) {
|
|
257
|
+
if (p instanceof RegExp) {
|
|
258
|
+
patterns.push({ pattern: p, label: `custom: ${p.source}` });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const files = listScanFiles(skillDir);
|
|
264
|
+
for (const file of files) {
|
|
265
|
+
let content;
|
|
266
|
+
try {
|
|
267
|
+
content = fs.readFileSync(file, 'utf-8');
|
|
268
|
+
} catch (_err) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const rel = path.relative(skillDir, file) || path.basename(file);
|
|
272
|
+
const lines = content.split(/\r?\n/);
|
|
273
|
+
for (let i = 0; i < lines.length; i++) {
|
|
274
|
+
const line = lines[i];
|
|
275
|
+
for (const { pattern, label } of patterns) {
|
|
276
|
+
const match = line.match(pattern);
|
|
277
|
+
if (match) {
|
|
278
|
+
blockers.push({
|
|
279
|
+
file: rel,
|
|
280
|
+
line: i + 1,
|
|
281
|
+
match: match[0],
|
|
282
|
+
label,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
portable: blockers.length === 0,
|
|
291
|
+
blockers,
|
|
292
|
+
manifest,
|
|
293
|
+
scannedFiles: files.map((f) => path.relative(skillDir, f) || path.basename(f)),
|
|
294
|
+
skillMdPath,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Format a blockers array as a human-readable report. Useful for CLI output.
|
|
300
|
+
*
|
|
301
|
+
* @param {Array} blockers - Output of assessSkillPortability().blockers
|
|
302
|
+
* @returns {string} Multi-line summary
|
|
303
|
+
*/
|
|
304
|
+
function formatBlockers(blockers) {
|
|
305
|
+
if (!Array.isArray(blockers) || blockers.length === 0) {
|
|
306
|
+
return 'No portability blockers found.';
|
|
307
|
+
}
|
|
308
|
+
const lines = [`Found ${blockers.length} portability blocker(s):`];
|
|
309
|
+
for (const b of blockers) {
|
|
310
|
+
const where = `${b.file}:${b.line}`;
|
|
311
|
+
const detail = b.match ? ` — "${b.match}"` : '';
|
|
312
|
+
lines.push(` - [${b.label}] ${where}${detail}`);
|
|
313
|
+
}
|
|
314
|
+
return lines.join('\n');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = {
|
|
318
|
+
assessSkillPortability,
|
|
319
|
+
formatBlockers,
|
|
320
|
+
parseFrontmatter,
|
|
321
|
+
// Exposed for tests
|
|
322
|
+
BLOCKER_PATTERNS,
|
|
323
|
+
SCAN_EXTENSIONS,
|
|
324
|
+
};
|
package/lib/skill-registry.js
CHANGED
|
@@ -288,13 +288,39 @@ async function listSkills(projectRoot) {
|
|
|
288
288
|
? `(installed: ${isInstalled.version})`
|
|
289
289
|
: `(v${skill.version})`;
|
|
290
290
|
|
|
291
|
-
|
|
291
|
+
// Phase 1B: surface portability field on listed skills. We treat the
|
|
292
|
+
// field as a boolean tri-state — true/false from the manifest, undefined
|
|
293
|
+
// when missing (renders as no tag). Default-on-display is `false` when
|
|
294
|
+
// missing per Phase 1B spec.
|
|
295
|
+
const portableSource = isInstalled ?? skill;
|
|
296
|
+
const portable = readPortableField(portableSource);
|
|
297
|
+
const portableTag = portable === true ? ' [portable]' : '';
|
|
298
|
+
|
|
299
|
+
console.log(` ${status} ${skill.name.padEnd(15)} ${versionInfo}${portableTag}`);
|
|
292
300
|
console.log(` ${skill.description}`);
|
|
293
301
|
}
|
|
294
302
|
|
|
295
303
|
console.log('\nUse `flow skill add <name>` to install a skill');
|
|
296
304
|
}
|
|
297
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Read the `portable` flag from a skill manifest, defaulting to false.
|
|
308
|
+
*
|
|
309
|
+
* Manifest values may arrive as boolean (JSON manifest) or string (YAML
|
|
310
|
+
* frontmatter copied through). Anything other than literal-true is false.
|
|
311
|
+
* Phase 1B (wf-0342fc33).
|
|
312
|
+
*
|
|
313
|
+
* @param {Object} manifest - Skill manifest object
|
|
314
|
+
* @returns {boolean} portable flag, default false
|
|
315
|
+
*/
|
|
316
|
+
function readPortableField(manifest) {
|
|
317
|
+
if (!manifest || typeof manifest !== 'object') return false;
|
|
318
|
+
const v = manifest.portable;
|
|
319
|
+
if (v === true) return true;
|
|
320
|
+
if (typeof v === 'string' && v.toLowerCase() === 'true') return true;
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
298
324
|
/**
|
|
299
325
|
* Install a skill
|
|
300
326
|
* @param {string} skillName - Skill name
|
|
@@ -353,8 +379,12 @@ async function addSkill(skillName, projectRoot, options) {
|
|
|
353
379
|
}
|
|
354
380
|
|
|
355
381
|
// Write manifest (safe - we control the filename)
|
|
382
|
+
// Phase 1B: explicitly persist `portable` as boolean (default false). Upstream
|
|
383
|
+
// manifests may omit the field or set it as a string; we normalize here so
|
|
384
|
+
// downstream tools and `flow skill list` see a consistent shape.
|
|
356
385
|
const localManifest = {
|
|
357
386
|
...manifest,
|
|
387
|
+
portable: readPortableField(manifest),
|
|
358
388
|
installedAt: new Date().toISOString(),
|
|
359
389
|
installedVersion: manifest.version
|
|
360
390
|
};
|
|
@@ -523,4 +553,4 @@ async function skill(args) {
|
|
|
523
553
|
}
|
|
524
554
|
}
|
|
525
555
|
|
|
526
|
-
module.exports = { skill };
|
|
556
|
+
module.exports = { skill, readPortableField };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wogiflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.33.0",
|
|
4
4
|
"description": "AI-powered development workflow management system with multi-model support",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"flow": "./scripts/flow",
|
|
13
|
-
"test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-audit-gates.test.js tests/flow-standards-hook-three-layer.test.js tests/flow-correction-detector-reconcile.test.js tests/flow-correction-backfill.test.js tests/flow-audit-gates-feature-output-health.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js tests/flow-orchestrate-corrections.test.js tests/flow-source-fidelity.test.js tests/flow-hooks-long-input-enforcement.test.js tests/workspace-channel-tracking.test.js tests/flow-hooks-deletion-log.test.js tests/flow-task-boundary-reset.test.js tests/flow-deferral-gate.test.js tests/flow-research-required-gate.test.js tests/flow-standards-forbidden-patterns.test.js tests/flow-hooks-architect-required-gate.test.js tests/flow-architect-runs.test.js tests/flow-installer-forbidden-patterns.test.js tests/flow-deferral-classifier-ai.test.js tests/flow-no-defer-policy.test.js tests/flow-self-adversary-loop.test.js tests/flow-impl-question-classifier.test.js tests/flow-hooks-self-adversary-gate.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
|
|
13
|
+
"test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-audit-gates.test.js tests/flow-standards-hook-three-layer.test.js tests/flow-correction-detector-reconcile.test.js tests/flow-correction-backfill.test.js tests/flow-audit-gates-feature-output-health.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js tests/flow-orchestrate-corrections.test.js tests/flow-source-fidelity.test.js tests/flow-hooks-long-input-enforcement.test.js tests/workspace-channel-tracking.test.js tests/flow-hooks-deletion-log.test.js tests/flow-task-boundary-reset.test.js tests/flow-deferral-gate.test.js tests/flow-research-required-gate.test.js tests/flow-standards-forbidden-patterns.test.js tests/flow-hooks-architect-required-gate.test.js tests/flow-architect-runs.test.js tests/flow-installer-forbidden-patterns.test.js tests/flow-deferral-classifier-ai.test.js tests/flow-no-defer-policy.test.js tests/flow-self-adversary-loop.test.js tests/flow-impl-question-classifier.test.js tests/flow-hooks-self-adversary-gate.test.js tests/flow-scheduled-runner.test.js tests/flow-schedule-cli.test.js tests/flow-skill-portability.test.js tests/flow-skill-export.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
|
|
14
14
|
"test:syntax": "find scripts/ lib/ -name '*.js' -not -path '*/node_modules/*' -exec node --check {} +",
|
|
15
15
|
"lint": "eslint scripts/ lib/ tests/",
|
|
16
16
|
"lint:ci": "eslint scripts/ lib/ tests/ --max-warnings 0",
|
package/scripts/flow
CHANGED
|
@@ -123,6 +123,7 @@ show_help() {
|
|
|
123
123
|
echo " skill remove <name> Remove installed skill"
|
|
124
124
|
echo " skill update [name] Update skill(s)"
|
|
125
125
|
echo " skill info <name> Show skill details"
|
|
126
|
+
echo " skill export <name> Export portable skill (agentskills@v1 | claude-plugin)"
|
|
126
127
|
echo " correct Capture a correction/learning"
|
|
127
128
|
echo " correct \"desc\" Quick mode with description"
|
|
128
129
|
echo " correct list List recent corrections"
|
|
@@ -696,6 +697,10 @@ case "${1:-}" in
|
|
|
696
697
|
# List skills from registry
|
|
697
698
|
node -e "require('$SCRIPT_DIR/../lib/skill-registry').skill(['list'])"
|
|
698
699
|
;;
|
|
700
|
+
export)
|
|
701
|
+
# Phase 1B (wf-0342fc33): portable skill export to agentskills.io / claude-plugin formats
|
|
702
|
+
node "$SCRIPT_DIR/flow-skill-export.js" "${@:3}"
|
|
703
|
+
;;
|
|
699
704
|
propose|patch|promote|reject|archive|pending)
|
|
700
705
|
# Agent proposal CLI (staged, user-approved at session-end)
|
|
701
706
|
node "$SCRIPT_DIR/flow-skill-manage.js" "${@:2}"
|
|
@@ -715,6 +720,9 @@ case "${1:-}" in
|
|
|
715
720
|
echo " update [name] Update skill(s)"
|
|
716
721
|
echo " info <name> Show skill details"
|
|
717
722
|
echo ""
|
|
723
|
+
echo "Portable Export (Phase 1B):"
|
|
724
|
+
echo " export <name> [--format=agentskills@v1|claude-plugin] [--out=<dir>]"
|
|
725
|
+
echo ""
|
|
718
726
|
echo "Agent Proposals (staged, user-approved at session-end):"
|
|
719
727
|
echo " propose --name <n> --content <f> Stage new skill"
|
|
720
728
|
echo " patch --name <n> --content <f> Stage edit to existing skill"
|
|
@@ -1242,6 +1242,26 @@ const CONFIG_DEFAULTS = {
|
|
|
1242
1242
|
}
|
|
1243
1243
|
},
|
|
1244
1244
|
|
|
1245
|
+
// --- Scheduled / Background Mode (Phase 1A — wf-b211a076) ---
|
|
1246
|
+
// Default OFF; enable per-project via `.workflow/config.json`.
|
|
1247
|
+
// Headless runner at `scripts/flow-scheduled-runner.js`; GH workflow at
|
|
1248
|
+
// `.github/workflows/wogi-scheduled.yml`. CLI installer: `flow schedule install`.
|
|
1249
|
+
// Hard invariants (also enforced by the runner): operates only on the default
|
|
1250
|
+
// branch in a temp worktree; never `git push origin master`, never
|
|
1251
|
+
// `gh pr merge`, never writes to `.workflow/state/decisions.md`.
|
|
1252
|
+
scheduledMode: {
|
|
1253
|
+
enabled: false,
|
|
1254
|
+
dailyTokenBudget: 5000000,
|
|
1255
|
+
perJobModel: {
|
|
1256
|
+
'nightly-regression': 'haiku',
|
|
1257
|
+
'weekly-audit': 'sonnet',
|
|
1258
|
+
'weekly-digest': 'sonnet',
|
|
1259
|
+
'per-pr-review': 'opus'
|
|
1260
|
+
},
|
|
1261
|
+
dryRun: false,
|
|
1262
|
+
jobs: ['nightly-regression', 'weekly-audit', 'weekly-digest', 'per-pr-review']
|
|
1263
|
+
},
|
|
1264
|
+
|
|
1245
1265
|
// --- Workflow Steps ---
|
|
1246
1266
|
workflowSteps: WORKFLOW_STEP_DEFAULTS
|
|
1247
1267
|
};
|