@aiwg/cli 2026.8.0 → 2026.8.2
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/README.md +33 -0
- package/agentic/code/providers/capability-matrix.yaml +511 -0
- package/agentic/code/providers/model-capabilities.v1.json +120 -0
- package/agentic/code/providers/model-catalog.v1.json +96 -0
- package/agentic/code/providers/model-policy-evaluations.v1.json +50 -0
- package/agentic/code/providers/premium-model-allowlist.v1.json +36 -0
- package/bin/aiwg.mjs +14 -10
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/cli.js +2 -0
- package/dist/src/artifacts/types.js +4 -0
- package/dist/src/auth/client.js +209 -0
- package/dist/src/auth/config.js +38 -0
- package/dist/src/auth/credential-store.js +141 -0
- package/dist/src/auth/resource-credentials.js +25 -0
- package/dist/src/auth/types.js +2 -0
- package/dist/src/channel/manager.mjs +5 -5
- package/dist/src/cli/handlers/auth.js +125 -0
- package/dist/src/cli/handlers/help.js +1 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/install.js +42 -4
- package/dist/src/cli/handlers/marketplace.js +375 -122
- package/dist/src/cli/handlers/resource-versions.js +2 -0
- package/dist/src/cli/handlers/sessions.js +23 -5
- package/dist/src/cli/handlers/subcommands.js +10 -1
- package/dist/src/cli/handlers/use.js +342 -43
- package/dist/src/config/gitignore.js +1 -0
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/marketplace/exchange.js +602 -0
- package/dist/src/marketplace/provenance-types.js +19 -0
- package/dist/src/marketplace/provenance.js +834 -0
- package/dist/src/memory/canonical-context.js +342 -0
- package/dist/src/memory/context-pack.js +282 -0
- package/dist/src/memory/index.js +4 -0
- package/dist/src/memory/intake.js +118 -0
- package/dist/src/packages/adapters/git.js +79 -29
- package/dist/src/packages/package-discovery.js +81 -0
- package/dist/src/packages/package-registry.js +2 -0
- package/dist/src/packages/registry.js +119 -20
- package/dist/src/resources/resolver.js +1 -0
- package/dist/src/resources/web-release.d.ts +3 -1
- package/dist/src/resources/web-release.js +14 -6
- package/dist/src/serve/agentic-sandbox-fleet-client.js +213 -0
- package/dist/src/serve/fleet-mission-conductor.js +293 -0
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/output-registration.js +338 -0
- package/dist/src/sessions/promotion.js +73 -2
- package/dist/src/sessions/repository.js +2 -1
- package/dist/src/update/notifier.mjs +13 -2
- package/package.json +8 -1
- package/tools/_resolve-impl.mjs +74 -0
- package/tools/agents/deploy-agents.mjs +962 -0
- package/tools/agents/providers/base.mjs +2954 -0
- package/tools/agents/providers/claude.mjs +711 -0
- package/tools/agents/providers/codex.mjs +699 -0
- package/tools/agents/providers/copilot.mjs +659 -0
- package/tools/agents/providers/cursor.mjs +714 -0
- package/tools/agents/providers/factory.mjs +1130 -0
- package/tools/agents/providers/hermes.mjs +663 -0
- package/tools/agents/providers/hook-capabilities.mjs +85 -0
- package/tools/agents/providers/model-role.mjs +56 -0
- package/tools/agents/providers/openclaw-translator.mjs +348 -0
- package/tools/agents/providers/openclaw.mjs +680 -0
- package/tools/agents/providers/opencode.mjs +675 -0
- package/tools/agents/providers/openhuman.mjs +292 -0
- package/tools/agents/providers/warp.mjs +413 -0
- package/tools/agents/providers/windsurf.mjs +748 -0
- package/tools/commands/deploy-prompts-codex.mjs +336 -0
- package/tools/plugin/package-plugins.mjs +1013 -0
- package/tools/skills/deploy-skills-codex.mjs +571 -0
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory AI Provider
|
|
3
|
+
*
|
|
4
|
+
* Deploys agents as "droids" in Factory AI format. Factory uses a different
|
|
5
|
+
* frontmatter structure with kebab-case names and mapped tools.
|
|
6
|
+
*
|
|
7
|
+
* Deployment paths:
|
|
8
|
+
* - Agents: .factory/droids/
|
|
9
|
+
* - Commands: .factory/commands/
|
|
10
|
+
* - Skills: .factory/skills/
|
|
11
|
+
* - Rules: .factory/rules/
|
|
12
|
+
*
|
|
13
|
+
* Special features:
|
|
14
|
+
* - Transforms agent names to kebab-case
|
|
15
|
+
* - Maps Claude tools to Factory equivalents
|
|
16
|
+
* - Enables custom droids in ~/.factory/settings.json
|
|
17
|
+
* - Creates/updates AGENTS.md from template
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import realFs from 'fs';
|
|
21
|
+
import { createRequire } from 'module';
|
|
22
|
+
const _require = createRequire(import.meta.url);
|
|
23
|
+
const staticModelCatalog = _require('../../../agentic/code/providers/model-catalog.v1.json');
|
|
24
|
+
let fs;
|
|
25
|
+
try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; }
|
|
26
|
+
import path from 'path';
|
|
27
|
+
import {
|
|
28
|
+
ensureDir,
|
|
29
|
+
listMdFiles,
|
|
30
|
+
listMdFilesRecursive,
|
|
31
|
+
writeFile,
|
|
32
|
+
deployFiles,
|
|
33
|
+
toKebabCase,
|
|
34
|
+
stripJsonComments,
|
|
35
|
+
createAgentsMdFromTemplate,
|
|
36
|
+
initializeFrameworkWorkspace,
|
|
37
|
+
getAddonAgentFiles,
|
|
38
|
+
getAddonCommandFiles,
|
|
39
|
+
getAddonRuleFiles,
|
|
40
|
+
getAddonSkillDirs,
|
|
41
|
+
listSkillDirs,
|
|
42
|
+
deploySkillDir,
|
|
43
|
+
deploySkillsWithKernelRouting,
|
|
44
|
+
isKernelSkill,
|
|
45
|
+
pruneStaleAiwgSkills,
|
|
46
|
+
computeAllKernelNames,
|
|
47
|
+
normalizeDeploymentMode,
|
|
48
|
+
collectFrameworkArtifacts,
|
|
49
|
+
listOnDemandRuleFiles,
|
|
50
|
+
writeOnDemandRuleIndex,
|
|
51
|
+
cleanupOldRuleFiles,
|
|
52
|
+
loadRuntimeModelCatalog,
|
|
53
|
+
filterCommandsAgainstSkills,
|
|
54
|
+
deploySoulCompanions
|
|
55
|
+
} from './base.mjs';
|
|
56
|
+
const modelCatalog = loadRuntimeModelCatalog(staticModelCatalog);
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Provider Configuration
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
export const name = 'factory';
|
|
63
|
+
export const aliases = [];
|
|
64
|
+
|
|
65
|
+
export const paths = {
|
|
66
|
+
agents: '.factory/droids/',
|
|
67
|
+
commands: '.factory/commands/',
|
|
68
|
+
// Skills sequestered under .factory/.aiwg/skills/ — index-driven discovery (#1212).
|
|
69
|
+
skills: '.factory/.aiwg/skills/',
|
|
70
|
+
rules: '.factory/rules/'
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Kernel skills (always-loaded) deploy to the platform-native dir.
|
|
74
|
+
export const kernelSkillsPath = '.factory/skills/';
|
|
75
|
+
|
|
76
|
+
export const support = {
|
|
77
|
+
agents: 'native',
|
|
78
|
+
commands: 'native',
|
|
79
|
+
skills: 'native',
|
|
80
|
+
rules: 'conventional'
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const capabilities = {
|
|
84
|
+
skills: true,
|
|
85
|
+
rules: true,
|
|
86
|
+
aggregatedOutput: false,
|
|
87
|
+
yamlFormat: false
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Tool Mapping
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Map Claude Code tools to Factory AI equivalents
|
|
96
|
+
*/
|
|
97
|
+
export function mapToolsToFactory(toolsString, agentName) {
|
|
98
|
+
// Default comprehensive tool set if no tools specified
|
|
99
|
+
if (!toolsString) {
|
|
100
|
+
return ["Read", "LS", "Grep", "Glob", "Edit", "Create", "Execute", "Task", "TodoWrite", "WebSearch", "FetchUrl"];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Parse tools (comma-separated or array format)
|
|
104
|
+
let originalTools = [];
|
|
105
|
+
if (toolsString.startsWith('[')) {
|
|
106
|
+
try {
|
|
107
|
+
originalTools = JSON.parse(toolsString);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
originalTools = toolsString.replace(/[\[\]"']/g, '').split(/[,\s]+/).filter(Boolean);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
originalTools = toolsString.split(/[,\s]+/).filter(Boolean);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Tool mapping: Claude Code → Factory
|
|
116
|
+
const toolMap = {
|
|
117
|
+
'Bash': 'Execute',
|
|
118
|
+
'Write': 'Create', // Will add Edit too
|
|
119
|
+
'WebFetch': 'FetchUrl',
|
|
120
|
+
'Read': 'Read',
|
|
121
|
+
'Grep': 'Grep',
|
|
122
|
+
'Glob': 'Glob',
|
|
123
|
+
'LS': 'LS'
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const factoryTools = new Set();
|
|
127
|
+
|
|
128
|
+
// Map original tools
|
|
129
|
+
for (const tool of originalTools) {
|
|
130
|
+
// MultiEdit maps to Edit + ApplyPatch in Factory
|
|
131
|
+
if (tool === 'MultiEdit') {
|
|
132
|
+
factoryTools.add('Edit');
|
|
133
|
+
factoryTools.add('ApplyPatch');
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const mapped = toolMap[tool] || tool;
|
|
138
|
+
factoryTools.add(mapped);
|
|
139
|
+
|
|
140
|
+
// If Write is present, add both Create and Edit
|
|
141
|
+
if (tool === 'Write') {
|
|
142
|
+
factoryTools.add('Create');
|
|
143
|
+
factoryTools.add('Edit');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Orchestration agents need Task tool for invoking subagents
|
|
148
|
+
const orchestrationAgents = [
|
|
149
|
+
'executive-orchestrator',
|
|
150
|
+
'intake-coordinator',
|
|
151
|
+
'documentation-synthesizer',
|
|
152
|
+
'project-manager',
|
|
153
|
+
'deployment-manager',
|
|
154
|
+
'test-architect',
|
|
155
|
+
'architecture-designer',
|
|
156
|
+
'requirements-analyst',
|
|
157
|
+
'security-architect',
|
|
158
|
+
'technical-writer'
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const normalizedName = (agentName || '').toLowerCase().replace(/\s+/g, '-');
|
|
162
|
+
if (orchestrationAgents.some(oa => normalizedName.includes(oa))) {
|
|
163
|
+
factoryTools.add('Task');
|
|
164
|
+
factoryTools.add('TodoWrite');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Add web tools if WebFetch was present
|
|
168
|
+
if (originalTools.includes('WebFetch')) {
|
|
169
|
+
factoryTools.add('FetchUrl');
|
|
170
|
+
factoryTools.add('WebSearch');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Convert to sorted array for consistency
|
|
174
|
+
return Array.from(factoryTools).sort();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// Model Mapping
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
// Default Factory models (fallback if config not loaded)
|
|
182
|
+
const DEFAULT_FACTORY_MODELS = {
|
|
183
|
+
reasoning: modelCatalog.providers.factory.roles.reasoning.id,
|
|
184
|
+
coding: modelCatalog.providers.factory.roles.coding.id,
|
|
185
|
+
efficiency: modelCatalog.providers.factory.roles.efficiency.id
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Sonnet-tier agents that need higher reasoningEffort than the default "medium".
|
|
190
|
+
* These are deploy-time overrides — source agent files remain provider-agnostic.
|
|
191
|
+
*
|
|
192
|
+
* Rationale: these roles perform analysis, review, or verification where
|
|
193
|
+
* thoroughness is critical despite running on the cost-efficient sonnet tier.
|
|
194
|
+
*/
|
|
195
|
+
const REASONING_EFFORT_OVERRIDES = {
|
|
196
|
+
// Review & analysis — must be thorough
|
|
197
|
+
'code-reviewer': 'high',
|
|
198
|
+
'security-auditor': 'high',
|
|
199
|
+
'test-architect': 'high',
|
|
200
|
+
'requirements-reviewer': 'high',
|
|
201
|
+
'api-designer': 'high',
|
|
202
|
+
'regression-analyst': 'high',
|
|
203
|
+
'compliance-checker': 'high',
|
|
204
|
+
'privacy-officer': 'high',
|
|
205
|
+
'reliability-engineer': 'high',
|
|
206
|
+
'migration-planner': 'high',
|
|
207
|
+
'incident-responder': 'high',
|
|
208
|
+
// Grounding agents — verification accuracy is paramount
|
|
209
|
+
'security-grounding-agent': 'high',
|
|
210
|
+
'compliance-grounding-agent': 'high',
|
|
211
|
+
'technology-grounding-agent': 'high',
|
|
212
|
+
'performance-grounding-agent': 'high',
|
|
213
|
+
// Lightweight roles — mechanical, not analytical
|
|
214
|
+
'documentation-archivist': 'low',
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Map model tier to Factory reasoningEffort level.
|
|
219
|
+
*
|
|
220
|
+
* Priority: agent-specific override > agent frontmatter > models.json per-tier > default by model tier.
|
|
221
|
+
* Valid levels: off, low, medium, high.
|
|
222
|
+
*/
|
|
223
|
+
export function mapReasoningEffort(originalModel, modelsConfig, frontmatterEffort, agentName) {
|
|
224
|
+
// Explicit frontmatter override wins (source-level)
|
|
225
|
+
if (frontmatterEffort && ['off', 'low', 'medium', 'high'].includes(frontmatterEffort)) {
|
|
226
|
+
return frontmatterEffort;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Agent-specific deploy-time override (Factory provider policy)
|
|
230
|
+
const normalizedName = (agentName || '').toLowerCase().replace(/\s+/g, '-');
|
|
231
|
+
if (REASONING_EFFORT_OVERRIDES[normalizedName]) {
|
|
232
|
+
return REASONING_EFFORT_OVERRIDES[normalizedName];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const factoryConfig = modelsConfig?.factory || {};
|
|
236
|
+
const clean = (originalModel || 'sonnet').toLowerCase().replace(/['"]/g, '');
|
|
237
|
+
|
|
238
|
+
// Check models.json for per-tier reasoningEffort
|
|
239
|
+
if (/opus/i.test(clean) && factoryConfig.reasoning?.reasoningEffort) {
|
|
240
|
+
return factoryConfig.reasoning.reasoningEffort;
|
|
241
|
+
}
|
|
242
|
+
if (/haiku/i.test(clean) && factoryConfig.efficiency?.reasoningEffort) {
|
|
243
|
+
return factoryConfig.efficiency.reasoningEffort;
|
|
244
|
+
}
|
|
245
|
+
if (factoryConfig.coding?.reasoningEffort) {
|
|
246
|
+
return factoryConfig.coding.reasoningEffort;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Default mapping by model tier
|
|
250
|
+
if (/opus/i.test(clean)) return 'high';
|
|
251
|
+
if (/haiku/i.test(clean)) return 'low';
|
|
252
|
+
return 'medium';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Map model shorthand to Factory AI format
|
|
257
|
+
*/
|
|
258
|
+
export function mapModel(originalModel, modelCfg, modelsConfig) {
|
|
259
|
+
// Safe access to nested config with fallbacks
|
|
260
|
+
const factoryConfig = modelsConfig?.factory || {};
|
|
261
|
+
const defaultReasoning = factoryConfig.reasoning?.model || DEFAULT_FACTORY_MODELS.reasoning;
|
|
262
|
+
const defaultCoding = factoryConfig.coding?.model || DEFAULT_FACTORY_MODELS.coding;
|
|
263
|
+
const defaultEfficiency = factoryConfig.efficiency?.model || DEFAULT_FACTORY_MODELS.efficiency;
|
|
264
|
+
|
|
265
|
+
// Handle override models first
|
|
266
|
+
if (modelCfg?.reasoningModel || modelCfg?.codingModel || modelCfg?.efficiencyModel) {
|
|
267
|
+
const clean = (originalModel || 'sonnet').toLowerCase().replace(/['"]/g, '');
|
|
268
|
+
if (/opus/i.test(clean)) return modelCfg.reasoningModel || defaultReasoning;
|
|
269
|
+
if (/haiku/i.test(clean)) return modelCfg.efficiencyModel || defaultEfficiency;
|
|
270
|
+
return modelCfg.codingModel || defaultCoding;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Prefer factory-specific shorthand over shared shorthand
|
|
274
|
+
const factoryModels = modelsConfig?.factory_shorthand || {
|
|
275
|
+
'opus': defaultReasoning,
|
|
276
|
+
'sonnet': defaultCoding,
|
|
277
|
+
'haiku': defaultEfficiency,
|
|
278
|
+
'inherit': 'inherit'
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const clean = (originalModel || 'sonnet').toLowerCase().replace(/['"]/g, '');
|
|
282
|
+
for (const [key, value] of Object.entries(factoryModels)) {
|
|
283
|
+
if (clean.includes(key)) return value;
|
|
284
|
+
}
|
|
285
|
+
if (/opus/i.test(clean)) return defaultReasoning;
|
|
286
|
+
if (/haiku/i.test(clean)) return defaultEfficiency;
|
|
287
|
+
if (/sonnet/i.test(clean)) return defaultCoding;
|
|
288
|
+
|
|
289
|
+
return defaultCoding; // default
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// Content Transformation
|
|
294
|
+
// ============================================================================
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Transform AIWG agent to Factory droid format
|
|
298
|
+
*/
|
|
299
|
+
export function transformAgent(srcPath, content, opts) {
|
|
300
|
+
const { modelsConfig = {} } = opts;
|
|
301
|
+
|
|
302
|
+
// Parse existing frontmatter
|
|
303
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
304
|
+
if (!fmMatch) return content;
|
|
305
|
+
|
|
306
|
+
const [, frontmatter, body] = fmMatch;
|
|
307
|
+
|
|
308
|
+
// Extract metadata
|
|
309
|
+
const rawName = frontmatter.match(/name:\s*(.+)/)?.[1]?.trim();
|
|
310
|
+
const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim();
|
|
311
|
+
const modelMatch = frontmatter.match(/model:\s*(.+)/)?.[1]?.trim();
|
|
312
|
+
const toolsMatch = frontmatter.match(/tools:\s*(.+)/)?.[1]?.trim();
|
|
313
|
+
const effortMatch = frontmatter.match(/reasoningEffort:\s*(.+)/)?.[1]?.trim();
|
|
314
|
+
|
|
315
|
+
// Convert name to kebab-case for Factory compatibility
|
|
316
|
+
const name = toKebabCase(rawName);
|
|
317
|
+
|
|
318
|
+
// Map model to Factory format
|
|
319
|
+
const factoryModel = mapModel(modelMatch, opts, modelsConfig);
|
|
320
|
+
|
|
321
|
+
// Map reasoning effort based on model tier, with agent-specific overrides
|
|
322
|
+
const reasoningEffort = mapReasoningEffort(modelMatch, modelsConfig, effortMatch, name);
|
|
323
|
+
|
|
324
|
+
// Map tools to Factory equivalents
|
|
325
|
+
const factoryTools = mapToolsToFactory(toolsMatch, name);
|
|
326
|
+
|
|
327
|
+
// Generate Factory droid frontmatter
|
|
328
|
+
const factoryFrontmatter = `---
|
|
329
|
+
name: ${name}
|
|
330
|
+
description: ${description || 'AIWG SDLC agent'}
|
|
331
|
+
model: ${factoryModel}
|
|
332
|
+
reasoningEffort: ${reasoningEffort}
|
|
333
|
+
tools: ${JSON.stringify(factoryTools)}
|
|
334
|
+
---`;
|
|
335
|
+
|
|
336
|
+
return `${factoryFrontmatter}\n\n${body.trim()}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Map a skill commandHint.allowedTools string to Factory equivalents.
|
|
341
|
+
*
|
|
342
|
+
* Skill `allowedTools` differs from agent `tools` — it may carry Claude
|
|
343
|
+
* Code's allowlist syntax (e.g. `Bash(git *, gh *)`, `Bash(npm:*)`) where
|
|
344
|
+
* the parentheses contain commas that must NOT be treated as token
|
|
345
|
+
* separators. This tokenizer respects parentheses, then maps each token's
|
|
346
|
+
* identifier head:
|
|
347
|
+
*
|
|
348
|
+
* Bash(git *, gh *) → Execute(git *, gh *)
|
|
349
|
+
* Bash → Execute
|
|
350
|
+
* Write → Create (Factory pairs Create+Edit but for hint-only
|
|
351
|
+
* use we keep the head unique)
|
|
352
|
+
* MultiEdit → Edit
|
|
353
|
+
* mcp__gitea__* → mcp__gitea__* (passthrough)
|
|
354
|
+
*
|
|
355
|
+
* Returns a comma-separated string suitable for re-insertion into SKILL.md.
|
|
356
|
+
*/
|
|
357
|
+
export function mapAllowedToolsString(allowedTools) {
|
|
358
|
+
if (!allowedTools) return '';
|
|
359
|
+
const tokens = [];
|
|
360
|
+
let buf = '';
|
|
361
|
+
let depth = 0;
|
|
362
|
+
for (const ch of allowedTools) {
|
|
363
|
+
if (ch === '(') { depth += 1; buf += ch; continue; }
|
|
364
|
+
if (ch === ')') { depth = Math.max(0, depth - 1); buf += ch; continue; }
|
|
365
|
+
if (ch === ',' && depth === 0) {
|
|
366
|
+
if (buf.trim()) tokens.push(buf.trim());
|
|
367
|
+
buf = '';
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
buf += ch;
|
|
371
|
+
}
|
|
372
|
+
if (buf.trim()) tokens.push(buf.trim());
|
|
373
|
+
|
|
374
|
+
const headMap = {
|
|
375
|
+
Bash: 'Execute',
|
|
376
|
+
Write: 'Create',
|
|
377
|
+
MultiEdit: 'Edit',
|
|
378
|
+
WebFetch: 'FetchUrl'
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const mapped = tokens.map(tok => {
|
|
382
|
+
const m = tok.match(/^([A-Za-z_][\w]*)(.*)$/);
|
|
383
|
+
if (!m) return tok;
|
|
384
|
+
const [, head, rest] = m;
|
|
385
|
+
const newHead = headMap[head] || head;
|
|
386
|
+
return `${newHead}${rest}`;
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Deduplicate while preserving order
|
|
390
|
+
const seen = new Set();
|
|
391
|
+
const out = [];
|
|
392
|
+
for (const t of mapped) {
|
|
393
|
+
if (!seen.has(t)) { seen.add(t); out.push(t); }
|
|
394
|
+
}
|
|
395
|
+
return out.join(', ');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Transform SKILL.md frontmatter for Factory.
|
|
400
|
+
*
|
|
401
|
+
* Skills declare optional `commandHint:` metadata describing how the skill
|
|
402
|
+
* should be invoked as a slash command. The hint carries Claude-native tool
|
|
403
|
+
* names and Claude model shorthand, both of which are wrong on Factory:
|
|
404
|
+
*
|
|
405
|
+
* commandHint:
|
|
406
|
+
* allowedTools: 'WebSearch, Read, Write, Bash' ← Bash/Write/MultiEdit
|
|
407
|
+
* model: sonnet ← bare shorthand
|
|
408
|
+
*
|
|
409
|
+
* This transform rewrites the indented allowedTools and model fields so
|
|
410
|
+
* the deployed SKILL.md is consistent with how agent droids are transformed
|
|
411
|
+
* by transformAgent() — Bash → Execute, Write → Create+Edit, MultiEdit →
|
|
412
|
+
* Edit+ApplyPatch, sonnet → claude-sonnet-4-x, etc.
|
|
413
|
+
*
|
|
414
|
+
* Issue: #1056 (upstream #102)
|
|
415
|
+
*/
|
|
416
|
+
export function transformSkillFrontmatter(content, opts) {
|
|
417
|
+
const { modelsConfig = {} } = opts || {};
|
|
418
|
+
|
|
419
|
+
// Match frontmatter, allowing an optional leading HTML comment.
|
|
420
|
+
const fmMatch = content.match(/^((?:<!--[\s\S]*?-->\s*)?)---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
421
|
+
if (!fmMatch) return content;
|
|
422
|
+
|
|
423
|
+
const [, prefix, frontmatter, body] = fmMatch;
|
|
424
|
+
|
|
425
|
+
// Step 1: filter top-level keys to the Factory-recognized allowlist (#102).
|
|
426
|
+
// Anything else is AIWG-internal metadata or Claude Code-specific and would
|
|
427
|
+
// confuse Factory's skill loader. Block-aware parsing preserves multi-line
|
|
428
|
+
// values like the `commandHint:` map.
|
|
429
|
+
const filtered = filterFactorySkillFrontmatter(frontmatter);
|
|
430
|
+
|
|
431
|
+
// Step 2: rewrite the still-Claude-flavored bits inside `commandHint:` —
|
|
432
|
+
// tool name remap and model shorthand → Factory model id.
|
|
433
|
+
let updated = filtered;
|
|
434
|
+
updated = updated.replace(
|
|
435
|
+
/^(\s+allowedTools:\s*)(['"]?)([^\n'"]+)\2/m,
|
|
436
|
+
(_match, lead, quote, raw) => {
|
|
437
|
+
const mapped = mapAllowedToolsString(raw);
|
|
438
|
+
const q = quote || "'";
|
|
439
|
+
return `${lead}${q}${mapped}${q}`;
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
updated = updated.replace(
|
|
443
|
+
/^(\s+model:\s*)(['"]?)([^\s\n'"]+)\2/m,
|
|
444
|
+
(_match, lead, quote, raw) => {
|
|
445
|
+
const mapped = mapModel(raw, opts, modelsConfig);
|
|
446
|
+
const q = quote;
|
|
447
|
+
return `${lead}${q}${mapped}${q}`;
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
if (updated === frontmatter) return content;
|
|
452
|
+
return `${prefix}---\n${updated}\n---\n${body}`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Top-level frontmatter keys Factory recognizes for skills.
|
|
457
|
+
*
|
|
458
|
+
* The list comes from Factory's skill loader contract — anything not in it is
|
|
459
|
+
* either AIWG-internal (`namespace`, `platforms`, `memory`, `category`,
|
|
460
|
+
* `orchestration`, `subagent-optimized`, `version`) or a Claude Code-only
|
|
461
|
+
* concept (`tools`, `model` at the top level — Factory handles those via
|
|
462
|
+
* `commandHint:` for skills).
|
|
463
|
+
*
|
|
464
|
+
* Issue: #102 — Factory provider must strip non-Factory fields, not pass them through.
|
|
465
|
+
*/
|
|
466
|
+
const FACTORY_SKILL_TOP_LEVEL_KEYS = new Set([
|
|
467
|
+
'name',
|
|
468
|
+
'description',
|
|
469
|
+
'user-invocable',
|
|
470
|
+
'disable-model-invocation',
|
|
471
|
+
'commandHint',
|
|
472
|
+
]);
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Filter a YAML frontmatter string down to Factory-recognized top-level keys.
|
|
476
|
+
*
|
|
477
|
+
* Preserves the order and exact line content of kept blocks (no YAML round-trip),
|
|
478
|
+
* including indented continuation lines. Block boundaries are detected by
|
|
479
|
+
* unindented `key:` lines.
|
|
480
|
+
*/
|
|
481
|
+
export function filterFactorySkillFrontmatter(frontmatter) {
|
|
482
|
+
const lines = frontmatter.split('\n');
|
|
483
|
+
const kept = [];
|
|
484
|
+
let keepingCurrent = true;
|
|
485
|
+
|
|
486
|
+
for (const line of lines) {
|
|
487
|
+
const topKeyMatch = line.match(/^([A-Za-z_][\w-]*)\s*:/);
|
|
488
|
+
if (topKeyMatch) {
|
|
489
|
+
// Start of a new top-level block — decide whether to keep it.
|
|
490
|
+
keepingCurrent = FACTORY_SKILL_TOP_LEVEL_KEYS.has(topKeyMatch[1]);
|
|
491
|
+
if (keepingCurrent) kept.push(line);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
// Continuation of the current block (indented value, list item, blank line).
|
|
495
|
+
if (keepingCurrent) kept.push(line);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return kept.join('\n');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Transform command for Factory
|
|
503
|
+
* Commands use similar format to agents
|
|
504
|
+
*/
|
|
505
|
+
export function transformCommand(srcPath, content, opts) {
|
|
506
|
+
// Commands are simpler - just basic frontmatter transformation
|
|
507
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
508
|
+
if (!fmMatch) return content;
|
|
509
|
+
|
|
510
|
+
const [, frontmatter, body] = fmMatch;
|
|
511
|
+
|
|
512
|
+
// Extract metadata
|
|
513
|
+
const rawName = frontmatter.match(/name:\s*(.+)/)?.[1]?.trim();
|
|
514
|
+
const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim();
|
|
515
|
+
const args = frontmatter.match(/args:\s*(.+)/)?.[1]?.trim();
|
|
516
|
+
const argumentHint = frontmatter.match(/argument-hint:\s*(.+)/)?.[1]?.trim();
|
|
517
|
+
|
|
518
|
+
// Convert name to kebab-case
|
|
519
|
+
const name = toKebabCase(rawName) || path.basename(srcPath, '.md');
|
|
520
|
+
|
|
521
|
+
// Build Factory command frontmatter
|
|
522
|
+
let factoryFrontmatter = `---
|
|
523
|
+
name: ${name}
|
|
524
|
+
description: ${description || 'AIWG command'}
|
|
525
|
+
argument-hint: ${argumentHint || '<task-description>'}`;
|
|
526
|
+
|
|
527
|
+
if (args) {
|
|
528
|
+
factoryFrontmatter += `\nargs: ${args}`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
factoryFrontmatter += '\n---';
|
|
532
|
+
|
|
533
|
+
// Prepend $ARGUMENTS so Factory passes user input into the prompt body.
|
|
534
|
+
// Factory silently drops anything typed after the command name if $ARGUMENTS
|
|
535
|
+
// is not present. This is a deploy-time transform only — source files are unchanged.
|
|
536
|
+
const bodyWithArgs = `$ARGUMENTS\n\n${body.trim()}`;
|
|
537
|
+
|
|
538
|
+
return `${factoryFrontmatter}\n\n${bodyWithArgs}`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ============================================================================
|
|
542
|
+
// Deployment Functions
|
|
543
|
+
// ============================================================================
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Deploy agents to .factory/droids/
|
|
547
|
+
*/
|
|
548
|
+
export function deployAgents(agentFiles, targetDir, opts) {
|
|
549
|
+
const destDir = path.join(targetDir, paths.agents);
|
|
550
|
+
ensureDir(destDir, opts.dryRun);
|
|
551
|
+
return deployFiles(agentFiles, destDir, { ...opts, injectPlatform: true }, transformAgent);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Deploy commands to .factory/commands/
|
|
556
|
+
*/
|
|
557
|
+
export function deployCommands(commandFiles, targetDir, opts) {
|
|
558
|
+
const destDir = path.join(targetDir, paths.commands);
|
|
559
|
+
ensureDir(destDir, opts.dryRun);
|
|
560
|
+
return deployFiles(commandFiles, destDir, opts, transformCommand);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Deploy skills with kernel-vs-standard routing (#1212/#1216).
|
|
565
|
+
* - kernel skills → .factory/skills/ (platform-native, always-loaded)
|
|
566
|
+
* - standard → .factory/.aiwg/skills/ (index-discoverable)
|
|
567
|
+
*/
|
|
568
|
+
export function deploySkills(skillDirs, targetDir, opts) {
|
|
569
|
+
const standardDestDir = path.join(targetDir, paths.skills);
|
|
570
|
+
const kernelDestDir = path.join(targetDir, kernelSkillsPath);
|
|
571
|
+
const skillOpts = { ...opts, transformSkillMd: transformSkillFrontmatter };
|
|
572
|
+
deploySkillsWithKernelRouting(skillDirs, standardDestDir, kernelDestDir, skillOpts);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Deploy rules to .factory/rules/
|
|
577
|
+
*/
|
|
578
|
+
export function deployRules(ruleFiles, targetDir, opts) {
|
|
579
|
+
const destDir = path.join(targetDir, paths.rules);
|
|
580
|
+
ensureDir(destDir, opts.dryRun);
|
|
581
|
+
cleanupOldRuleFiles(destDir, opts);
|
|
582
|
+
return deployFiles(ruleFiles, destDir, opts, transformAgent);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ============================================================================
|
|
586
|
+
// AGENTS.md
|
|
587
|
+
// ============================================================================
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Create/update AGENTS.md from Factory template
|
|
591
|
+
*/
|
|
592
|
+
export function createAgentsMd(target, srcRoot, dryRun) {
|
|
593
|
+
createAgentsMdFromTemplate(target, srcRoot, 'factory/AGENTS.md.aiwg-template', dryRun);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ============================================================================
|
|
597
|
+
// Factory Settings
|
|
598
|
+
// ============================================================================
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Enable custom droids in Factory settings.json
|
|
602
|
+
*/
|
|
603
|
+
export function enableFactoryCustomDroids(dryRun) {
|
|
604
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
605
|
+
if (!homeDir) {
|
|
606
|
+
console.warn('Could not determine home directory, skipping Factory settings configuration');
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const settingsDir = path.join(homeDir, '.factory');
|
|
611
|
+
const settingsPath = path.join(settingsDir, 'settings.json');
|
|
612
|
+
|
|
613
|
+
let settings = {};
|
|
614
|
+
let originalContent = '';
|
|
615
|
+
let hasExistingFile = false;
|
|
616
|
+
|
|
617
|
+
// Read existing settings if present
|
|
618
|
+
if (fs.existsSync(settingsPath)) {
|
|
619
|
+
hasExistingFile = true;
|
|
620
|
+
try {
|
|
621
|
+
originalContent = fs.readFileSync(settingsPath, 'utf8');
|
|
622
|
+
// Strip JSONC comments before parsing
|
|
623
|
+
const jsonContent = stripJsonComments(originalContent);
|
|
624
|
+
settings = JSON.parse(jsonContent);
|
|
625
|
+
} catch (err) {
|
|
626
|
+
console.warn(`Warning: Could not parse existing Factory settings.json: ${err.message}`);
|
|
627
|
+
console.warn('Will add enableCustomDroids setting using text manipulation to preserve file...');
|
|
628
|
+
|
|
629
|
+
// Try to add the setting via text manipulation
|
|
630
|
+
if (originalContent.includes('"enableCustomDroids"')) {
|
|
631
|
+
if (originalContent.includes('"enableCustomDroids": true') ||
|
|
632
|
+
originalContent.includes('"enableCustomDroids":true')) {
|
|
633
|
+
console.log('Factory Custom Droids already enabled in settings');
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
// Replace false with true
|
|
637
|
+
if (!dryRun) {
|
|
638
|
+
const updatedContent = originalContent.replace(
|
|
639
|
+
/"enableCustomDroids"\s*:\s*false/,
|
|
640
|
+
'"enableCustomDroids": true'
|
|
641
|
+
);
|
|
642
|
+
fs.writeFileSync(settingsPath, updatedContent, 'utf8');
|
|
643
|
+
console.log(`Enabled Custom Droids in Factory settings: ${settingsPath}`);
|
|
644
|
+
} else {
|
|
645
|
+
console.log(`[dry-run] Would enable Custom Droids in ${settingsPath}`);
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Setting doesn't exist, add it after the first {
|
|
651
|
+
if (!dryRun) {
|
|
652
|
+
const insertPoint = originalContent.indexOf('{') + 1;
|
|
653
|
+
const updatedContent =
|
|
654
|
+
originalContent.slice(0, insertPoint) +
|
|
655
|
+
'\n "enableCustomDroids": true,' +
|
|
656
|
+
originalContent.slice(insertPoint);
|
|
657
|
+
fs.writeFileSync(settingsPath, updatedContent, 'utf8');
|
|
658
|
+
console.log(`Enabled Custom Droids in Factory settings: ${settingsPath}`);
|
|
659
|
+
} else {
|
|
660
|
+
console.log(`[dry-run] Would enable Custom Droids in ${settingsPath}`);
|
|
661
|
+
}
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Check if Custom Droids already enabled
|
|
667
|
+
if (settings.enableCustomDroids === true) {
|
|
668
|
+
console.log('Factory Custom Droids already enabled in settings');
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Enable Custom Droids
|
|
673
|
+
settings.enableCustomDroids = true;
|
|
674
|
+
|
|
675
|
+
if (dryRun) {
|
|
676
|
+
console.log(`[dry-run] Would enable Custom Droids in ${settingsPath}`);
|
|
677
|
+
console.log(`[dry-run] New setting: enableCustomDroids: true`);
|
|
678
|
+
} else {
|
|
679
|
+
// Ensure settings directory exists
|
|
680
|
+
if (!fs.existsSync(settingsDir)) {
|
|
681
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (hasExistingFile && originalContent.includes('//')) {
|
|
685
|
+
// File has comments - use text manipulation to preserve them
|
|
686
|
+
if (originalContent.includes('"enableCustomDroids"')) {
|
|
687
|
+
const updatedContent = originalContent.replace(
|
|
688
|
+
/"enableCustomDroids"\s*:\s*false/,
|
|
689
|
+
'"enableCustomDroids": true'
|
|
690
|
+
);
|
|
691
|
+
fs.writeFileSync(settingsPath, updatedContent, 'utf8');
|
|
692
|
+
} else {
|
|
693
|
+
const insertPoint = originalContent.indexOf('{') + 1;
|
|
694
|
+
const updatedContent =
|
|
695
|
+
originalContent.slice(0, insertPoint) +
|
|
696
|
+
'\n "enableCustomDroids": true,' +
|
|
697
|
+
originalContent.slice(insertPoint);
|
|
698
|
+
fs.writeFileSync(settingsPath, updatedContent, 'utf8');
|
|
699
|
+
}
|
|
700
|
+
} else {
|
|
701
|
+
// No comments or new file - safe to use JSON.stringify
|
|
702
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
703
|
+
}
|
|
704
|
+
console.log(`Enabled Custom Droids in Factory settings: ${settingsPath}`);
|
|
705
|
+
console.log('Note: You may need to restart droid for this setting to take effect');
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// ============================================================================
|
|
710
|
+
// Factory Hooks
|
|
711
|
+
// ============================================================================
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Deploy SessionStart hook for AIWG pre-flight checks.
|
|
715
|
+
*
|
|
716
|
+
* Writes a SessionStart hook to ~/.factory/settings.json that runs
|
|
717
|
+
* `aiwg sync --dry-run --quiet` on every new session. Preserves
|
|
718
|
+
* existing hooks and settings.
|
|
719
|
+
*/
|
|
720
|
+
export function deployFactoryHooks(dryRun) {
|
|
721
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
722
|
+
if (!homeDir) {
|
|
723
|
+
console.warn('Could not determine home directory, skipping Factory hooks configuration');
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const settingsDir = path.join(homeDir, '.factory');
|
|
728
|
+
const settingsPath = path.join(settingsDir, 'settings.json');
|
|
729
|
+
|
|
730
|
+
let settings = {};
|
|
731
|
+
let originalContent = '';
|
|
732
|
+
let hasExistingFile = false;
|
|
733
|
+
|
|
734
|
+
// Read existing settings if present
|
|
735
|
+
if (fs.existsSync(settingsPath)) {
|
|
736
|
+
hasExistingFile = true;
|
|
737
|
+
try {
|
|
738
|
+
originalContent = fs.readFileSync(settingsPath, 'utf8');
|
|
739
|
+
const jsonContent = stripJsonComments(originalContent);
|
|
740
|
+
settings = JSON.parse(jsonContent);
|
|
741
|
+
} catch (err) {
|
|
742
|
+
console.warn(`Warning: Could not parse Factory settings.json for hooks: ${err.message}`);
|
|
743
|
+
console.warn('Skipping hook deployment to avoid corrupting settings file.');
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Define the AIWG pre-flight hook
|
|
749
|
+
const aiwgHook = {
|
|
750
|
+
type: 'command',
|
|
751
|
+
command: 'aiwg sync --dry-run --quiet'
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
const aiwgMatcher = {
|
|
755
|
+
matcher: '*',
|
|
756
|
+
hooks: [aiwgHook]
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
// Initialize hooks structure if missing
|
|
760
|
+
if (!settings.hooks) {
|
|
761
|
+
settings.hooks = {};
|
|
762
|
+
}
|
|
763
|
+
if (!settings.hooks.SessionStart) {
|
|
764
|
+
settings.hooks.SessionStart = [];
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Check if AIWG hook already exists (by command string)
|
|
768
|
+
const alreadyInstalled = settings.hooks.SessionStart.some(entry =>
|
|
769
|
+
entry.hooks?.some(h => h.command && h.command.includes('aiwg sync'))
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
if (alreadyInstalled) {
|
|
773
|
+
console.log('Factory SessionStart hook already installed for AIWG pre-flight');
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Add the hook
|
|
778
|
+
settings.hooks.SessionStart.push(aiwgMatcher);
|
|
779
|
+
|
|
780
|
+
if (dryRun) {
|
|
781
|
+
console.log(`[dry-run] Would add SessionStart hook to ${settingsPath}`);
|
|
782
|
+
console.log(`[dry-run] Hook: aiwg sync --dry-run --quiet`);
|
|
783
|
+
} else {
|
|
784
|
+
if (!fs.existsSync(settingsDir)) {
|
|
785
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Since we've successfully parsed the JSON, safe to write back
|
|
789
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
790
|
+
console.log(`Deployed SessionStart hook to ${settingsPath}`);
|
|
791
|
+
console.log('Hook: aiwg sync --dry-run --quiet (runs on every new Factory session)');
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ============================================================================
|
|
796
|
+
// Post-Deployment
|
|
797
|
+
// ============================================================================
|
|
798
|
+
|
|
799
|
+
export async function postDeploy(targetDir, opts) {
|
|
800
|
+
// Initialize framework workspace structure
|
|
801
|
+
initializeFrameworkWorkspace(targetDir, opts.mode, opts.dryRun, opts.srcRoot);
|
|
802
|
+
|
|
803
|
+
// Enable custom droids in Factory settings
|
|
804
|
+
enableFactoryCustomDroids(opts.dryRun);
|
|
805
|
+
|
|
806
|
+
// Deploy SessionStart hook for AIWG pre-flight checks
|
|
807
|
+
deployFactoryHooks(opts.dryRun);
|
|
808
|
+
|
|
809
|
+
// Create/update AGENTS.md if requested
|
|
810
|
+
if (opts.createAgentsMd) {
|
|
811
|
+
createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ============================================================================
|
|
816
|
+
// Factory Plugin Bundle
|
|
817
|
+
// ============================================================================
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Generate a .factory-plugin/ bundle for distributing AIWG as a Factory plugin.
|
|
821
|
+
*
|
|
822
|
+
* Factory plugins use the structure:
|
|
823
|
+
* .factory-plugin/
|
|
824
|
+
* plugin.json — manifest (name, version, description, contents)
|
|
825
|
+
* droids/ — agent definitions
|
|
826
|
+
* commands/ — command definitions
|
|
827
|
+
* skills/ — skill definitions
|
|
828
|
+
* rules/ — rule definitions
|
|
829
|
+
* hooks.json — hook configuration
|
|
830
|
+
*
|
|
831
|
+
* Invoke via: aiwg use sdlc --provider factory --as-plugin
|
|
832
|
+
*/
|
|
833
|
+
export function generatePluginBundle(targetDir, opts) {
|
|
834
|
+
const pluginDir = path.join(targetDir, '.factory-plugin');
|
|
835
|
+
const { dryRun, version } = opts;
|
|
836
|
+
|
|
837
|
+
// Read package.json for version info
|
|
838
|
+
let pkgVersion = version || 'unknown';
|
|
839
|
+
if (!version) {
|
|
840
|
+
try {
|
|
841
|
+
const pkgPath = path.join(opts.srcRoot, 'package.json');
|
|
842
|
+
if (fs.existsSync(pkgPath)) {
|
|
843
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
844
|
+
pkgVersion = pkg.version || 'unknown';
|
|
845
|
+
}
|
|
846
|
+
} catch (_) { /* use default */ }
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Count deployed artifacts
|
|
850
|
+
const droidDir = path.join(targetDir, paths.agents);
|
|
851
|
+
const cmdDir = path.join(targetDir, paths.commands);
|
|
852
|
+
const skillDir = path.join(targetDir, paths.skills);
|
|
853
|
+
const ruleDir = path.join(targetDir, paths.rules);
|
|
854
|
+
|
|
855
|
+
const countFiles = (dir, ext) => {
|
|
856
|
+
try {
|
|
857
|
+
return fs.readdirSync(dir).filter(f => f.endsWith(ext || '.md')).length;
|
|
858
|
+
} catch (_) { return 0; }
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
const droidCount = countFiles(droidDir);
|
|
862
|
+
const commandCount = countFiles(cmdDir);
|
|
863
|
+
const ruleCount = countFiles(ruleDir);
|
|
864
|
+
let skillCount = 0;
|
|
865
|
+
try {
|
|
866
|
+
skillCount = fs.readdirSync(skillDir).filter(f =>
|
|
867
|
+
fs.statSync(path.join(skillDir, f)).isDirectory()
|
|
868
|
+
).length;
|
|
869
|
+
} catch (_) { /* 0 */ }
|
|
870
|
+
|
|
871
|
+
// Build plugin manifest
|
|
872
|
+
const manifest = {
|
|
873
|
+
name: 'aiwg-sdlc',
|
|
874
|
+
version: pkgVersion,
|
|
875
|
+
description: `AIWG SDLC Framework v${pkgVersion} — ${droidCount} droids, ${commandCount} commands, ${skillCount} skills, ${ruleCount} rules. Phase-based SDLC workflows with specialized agents for architecture, security, testing, and deployment.`,
|
|
876
|
+
author: {
|
|
877
|
+
name: 'AIWG Contributors',
|
|
878
|
+
email: 'support@aiwg.io'
|
|
879
|
+
},
|
|
880
|
+
homepage: 'https://aiwg.io',
|
|
881
|
+
repository: 'https://github.com/jmagly/aiwg',
|
|
882
|
+
license: 'MIT',
|
|
883
|
+
contents: {
|
|
884
|
+
droids: droidCount,
|
|
885
|
+
commands: commandCount,
|
|
886
|
+
skills: skillCount,
|
|
887
|
+
rules: ruleCount
|
|
888
|
+
},
|
|
889
|
+
hooks: {
|
|
890
|
+
SessionStart: [
|
|
891
|
+
{
|
|
892
|
+
matcher: '*',
|
|
893
|
+
hooks: [
|
|
894
|
+
{
|
|
895
|
+
type: 'command',
|
|
896
|
+
command: 'aiwg sync --dry-run --quiet'
|
|
897
|
+
}
|
|
898
|
+
]
|
|
899
|
+
}
|
|
900
|
+
]
|
|
901
|
+
},
|
|
902
|
+
keywords: [
|
|
903
|
+
'sdlc', 'software-development', 'project-management',
|
|
904
|
+
'security', 'testing', 'architecture', 'devops', 'aiwg'
|
|
905
|
+
]
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
if (dryRun) {
|
|
909
|
+
console.log(`[dry-run] Would create ${pluginDir}/plugin.json`);
|
|
910
|
+
console.log(`[dry-run] Plugin: aiwg-sdlc v${pkgVersion} (${droidCount} droids, ${commandCount} commands, ${skillCount} skills, ${ruleCount} rules)`);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
ensureDir(pluginDir, false);
|
|
915
|
+
fs.writeFileSync(
|
|
916
|
+
path.join(pluginDir, 'plugin.json'),
|
|
917
|
+
JSON.stringify(manifest, null, 2) + '\n',
|
|
918
|
+
'utf8'
|
|
919
|
+
);
|
|
920
|
+
console.log(`Generated Factory plugin manifest: ${pluginDir}/plugin.json`);
|
|
921
|
+
console.log(`Plugin: aiwg-sdlc v${pkgVersion} (${droidCount} droids, ${commandCount} commands, ${skillCount} skills, ${ruleCount} rules)`);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// ============================================================================
|
|
925
|
+
// File Extension
|
|
926
|
+
// ============================================================================
|
|
927
|
+
|
|
928
|
+
export function getFileExtension(type) {
|
|
929
|
+
return '.md';
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// ============================================================================
|
|
933
|
+
// Main Deploy Function
|
|
934
|
+
// ============================================================================
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Main deployment function for Factory provider
|
|
938
|
+
*/
|
|
939
|
+
export async function deploy(opts) {
|
|
940
|
+
const {
|
|
941
|
+
srcRoot,
|
|
942
|
+
target,
|
|
943
|
+
mode,
|
|
944
|
+
deployCommands: shouldDeployCommands,
|
|
945
|
+
deploySkills: shouldDeploySkills,
|
|
946
|
+
deployRules: shouldDeployRules,
|
|
947
|
+
commandsOnly,
|
|
948
|
+
skillsOnly,
|
|
949
|
+
rulesOnly,
|
|
950
|
+
dryRun,
|
|
951
|
+
createAgentsMd: shouldCreateAgentsMd
|
|
952
|
+
} = opts;
|
|
953
|
+
|
|
954
|
+
console.log(`\n=== Factory AI Provider ===`);
|
|
955
|
+
console.log(`Target: ${target}`);
|
|
956
|
+
console.log(`Mode: ${mode}`);
|
|
957
|
+
|
|
958
|
+
// Collect source files based on mode
|
|
959
|
+
const agentFiles = [];
|
|
960
|
+
const commandFiles = [];
|
|
961
|
+
const ruleFiles = [];
|
|
962
|
+
const skillDirs = [];
|
|
963
|
+
const normalizedMode = normalizeDeploymentMode(mode);
|
|
964
|
+
|
|
965
|
+
// Check for addon-style directory structure (direct agents/, commands/,
|
|
966
|
+
// skills/, rules/ subdirs). Handles deployment when --source points at a
|
|
967
|
+
// project-local bundle (.aiwg/extensions/<name>/) rather than $AIWG_ROOT.
|
|
968
|
+
// Mirrors the reference implementation in claude.mjs (#124).
|
|
969
|
+
const isAddonSource = fs.existsSync(path.join(srcRoot, 'agents')) ||
|
|
970
|
+
fs.existsSync(path.join(srcRoot, 'commands')) ||
|
|
971
|
+
fs.existsSync(path.join(srcRoot, 'skills')) ||
|
|
972
|
+
fs.existsSync(path.join(srcRoot, 'rules'));
|
|
973
|
+
|
|
974
|
+
if (isAddonSource) {
|
|
975
|
+
const addonAgentsDir = path.join(srcRoot, 'agents');
|
|
976
|
+
if (fs.existsSync(addonAgentsDir)) {
|
|
977
|
+
agentFiles.push(...listMdFiles(addonAgentsDir));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (shouldDeployCommands || commandsOnly) {
|
|
981
|
+
const addonCommandsDir = path.join(srcRoot, 'commands');
|
|
982
|
+
if (fs.existsSync(addonCommandsDir)) {
|
|
983
|
+
commandFiles.push(...listMdFiles(addonCommandsDir));
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (shouldDeploySkills || skillsOnly) {
|
|
988
|
+
const addonSkillsDir = path.join(srcRoot, 'skills');
|
|
989
|
+
if (fs.existsSync(addonSkillsDir)) {
|
|
990
|
+
skillDirs.push(...listSkillDirs(addonSkillsDir));
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (shouldDeployRules || rulesOnly) {
|
|
995
|
+
const addonRulesDir = path.join(srcRoot, 'rules');
|
|
996
|
+
if (fs.existsSync(addonRulesDir)) {
|
|
997
|
+
ruleFiles.push(...listMdFiles(addonRulesDir));
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// All addons (dynamically discovered)
|
|
1003
|
+
if (normalizedMode === 'general' || normalizedMode === 'sdlc' || normalizedMode === 'both' || normalizedMode === 'all') {
|
|
1004
|
+
agentFiles.push(...getAddonAgentFiles(srcRoot));
|
|
1005
|
+
|
|
1006
|
+
if (shouldDeployCommands || commandsOnly) {
|
|
1007
|
+
commandFiles.push(...getAddonCommandFiles(srcRoot));
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (shouldDeployRules || rulesOnly) {
|
|
1011
|
+
ruleFiles.push(...getAddonRuleFiles(srcRoot));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (shouldDeploySkills || skillsOnly) {
|
|
1015
|
+
skillDirs.push(...getAddonSkillDirs(srcRoot));
|
|
1016
|
+
|
|
1017
|
+
// Holistic post-deploy cleanup of stale AIWG-managed kernel
|
|
1018
|
+
// skills (renamed/removed sources). Uses the global kernel set
|
|
1019
|
+
// (computeAllKernelNames walks all source frameworks/addons),
|
|
1020
|
+
// not just this-call's skillDirs, because aiwg use invokes
|
|
1021
|
+
// deploy-agents.mjs multiple times.
|
|
1022
|
+
{
|
|
1023
|
+
const _kernelDestDir = path.isAbsolute(kernelSkillsPath)
|
|
1024
|
+
? kernelSkillsPath
|
|
1025
|
+
: path.join(target, kernelSkillsPath);
|
|
1026
|
+
pruneStaleAiwgSkills(_kernelDestDir, computeAllKernelNames(srcRoot), opts);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const frameworkArtifacts = collectFrameworkArtifacts(srcRoot, normalizedMode, {
|
|
1032
|
+
includeAgents: true,
|
|
1033
|
+
includeCommands: shouldDeployCommands || commandsOnly,
|
|
1034
|
+
includeSkills: shouldDeploySkills || skillsOnly,
|
|
1035
|
+
includeRules: shouldDeployRules || rulesOnly,
|
|
1036
|
+
recursiveCommands: true,
|
|
1037
|
+
consolidatedSdlcRules: true
|
|
1038
|
+
});
|
|
1039
|
+
agentFiles.push(...frameworkArtifacts.agents);
|
|
1040
|
+
const soulFiles = [...(frameworkArtifacts.souls || [])];
|
|
1041
|
+
commandFiles.push(...frameworkArtifacts.commands);
|
|
1042
|
+
skillDirs.push(...frameworkArtifacts.skills);
|
|
1043
|
+
ruleFiles.push(...frameworkArtifacts.rules);
|
|
1044
|
+
|
|
1045
|
+
// Deploy based on flags
|
|
1046
|
+
if (!commandsOnly && !skillsOnly && !rulesOnly) {
|
|
1047
|
+
console.log(`\nDeploying ${agentFiles.length} agents as droids...`);
|
|
1048
|
+
deployAgents(agentFiles, target, opts);
|
|
1049
|
+
|
|
1050
|
+
// Deploy soul companion files alongside agents
|
|
1051
|
+
if (soulFiles.length > 0) {
|
|
1052
|
+
const destDir = path.join(target, paths.agents);
|
|
1053
|
+
console.log(`\nDeploying ${soulFiles.length} soul files...`);
|
|
1054
|
+
deploySoulCompanions(soulFiles, destDir, opts);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Filter commands that collide with skills (skills take precedence)
|
|
1059
|
+
const filteredCommands = (shouldDeploySkills || skillsOnly)
|
|
1060
|
+
? filterCommandsAgainstSkills(commandFiles, skillDirs)
|
|
1061
|
+
: commandFiles;
|
|
1062
|
+
|
|
1063
|
+
if (shouldDeployCommands || commandsOnly) {
|
|
1064
|
+
console.log(`\nDeploying ${filteredCommands.length} commands...`);
|
|
1065
|
+
deployCommands(filteredCommands, target, opts);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (shouldDeploySkills || skillsOnly) {
|
|
1069
|
+
console.log(`\nDeploying ${skillDirs.length} skills...`);
|
|
1070
|
+
deploySkills(skillDirs, target, opts);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (shouldDeployRules || rulesOnly) {
|
|
1074
|
+
console.log(`\nDeploying ${ruleFiles.length} rules...`);
|
|
1075
|
+
deployRules(ruleFiles, target, opts);
|
|
1076
|
+
|
|
1077
|
+
// On-demand index (#1675): list the MEDIUM/LOW rules tier-gated out of the
|
|
1078
|
+
// always-on set so agents can fetch them via `aiwg show rule`.
|
|
1079
|
+
const onDemandCount = writeOnDemandRuleIndex(
|
|
1080
|
+
path.join(target, paths.rules),
|
|
1081
|
+
listOnDemandRuleFiles(srcRoot),
|
|
1082
|
+
opts,
|
|
1083
|
+
);
|
|
1084
|
+
if (onDemandCount > 0) {
|
|
1085
|
+
console.log(` On-demand rules (not inlined): ${onDemandCount} → RULES-ONDEMAND.md`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Post-deployment
|
|
1090
|
+
await postDeploy(target, { ...opts, createAgentsMd: shouldCreateAgentsMd });
|
|
1091
|
+
|
|
1092
|
+
// Generate Factory plugin bundle if requested
|
|
1093
|
+
if (opts.asPlugin) {
|
|
1094
|
+
console.log('\nGenerating Factory plugin bundle...');
|
|
1095
|
+
generatePluginBundle(target, opts);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
console.log('\n=== Factory deployment complete ===\n');
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// ============================================================================
|
|
1102
|
+
// Default Export
|
|
1103
|
+
// ============================================================================
|
|
1104
|
+
|
|
1105
|
+
export default {
|
|
1106
|
+
name,
|
|
1107
|
+
aliases,
|
|
1108
|
+
paths,
|
|
1109
|
+
kernelSkillsPath,
|
|
1110
|
+
support,
|
|
1111
|
+
capabilities,
|
|
1112
|
+
transformAgent,
|
|
1113
|
+
transformCommand,
|
|
1114
|
+
transformSkillFrontmatter,
|
|
1115
|
+
filterFactorySkillFrontmatter,
|
|
1116
|
+
mapModel,
|
|
1117
|
+
mapReasoningEffort,
|
|
1118
|
+
mapToolsToFactory,
|
|
1119
|
+
deployAgents,
|
|
1120
|
+
deployCommands,
|
|
1121
|
+
deploySkills,
|
|
1122
|
+
deployRules,
|
|
1123
|
+
createAgentsMd,
|
|
1124
|
+
enableFactoryCustomDroids,
|
|
1125
|
+
deployFactoryHooks,
|
|
1126
|
+
generatePluginBundle,
|
|
1127
|
+
postDeploy,
|
|
1128
|
+
getFileExtension,
|
|
1129
|
+
deploy
|
|
1130
|
+
};
|