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.
@@ -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
+ };
@@ -288,13 +288,39 @@ async function listSkills(projectRoot) {
288
288
  ? `(installed: ${isInstalled.version})`
289
289
  : `(v${skill.version})`;
290
290
 
291
- console.log(` ${status} ${skill.name.padEnd(15)} ${versionInfo}`);
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.32.0",
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
  };