@syntesseraai/opencode-feature-factory 0.2.13 → 0.2.14
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/agents/building.md +66 -16
- package/agents/planning.md +59 -15
- package/agents/reviewing.md +47 -16
- package/dist/agent-context.d.ts +54 -0
- package/dist/agent-context.js +273 -0
- package/dist/agent-management-tools.d.ts +5 -0
- package/dist/agent-management-tools.js +117 -0
- package/dist/feature-factory-setup.d.ts +9 -0
- package/dist/feature-factory-setup.js +84 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +31 -4
- package/dist/output.test.js +1 -1
- package/dist/quality-gate-config.test.js +1 -1
- package/dist/stop-quality-gate.js +3 -3
- package/dist/stop-quality-gate.test.js +1 -1
- package/dist/uuid.d.ts +13 -0
- package/dist/uuid.js +22 -0
- package/package.json +1 -1
- package/skills/ff-delegation/SKILL.md +313 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin';
|
|
2
|
+
import { listActiveAgents, findAgentFiles, findAgentFilesById, findAllAgentFiles, readAgentContextById, } from './agent-context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create agent management tools for the plugin
|
|
5
|
+
*/
|
|
6
|
+
export function createAgentManagementTools(input) {
|
|
7
|
+
const { $ } = input;
|
|
8
|
+
return {
|
|
9
|
+
tool: {
|
|
10
|
+
'ff-agents-current': tool({
|
|
11
|
+
description: 'List all currently active Feature Factory agents with their UUIDs and status',
|
|
12
|
+
args: {
|
|
13
|
+
sessionID: tool.schema.string().optional().describe('Filter by specific session ID'),
|
|
14
|
+
agent: tool.schema
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Filter by agent type (e.g., planning, research)'),
|
|
18
|
+
},
|
|
19
|
+
execute: async (args) => {
|
|
20
|
+
try {
|
|
21
|
+
const agents = await listActiveAgents(input, args.sessionID, args.agent);
|
|
22
|
+
if (agents.length === 0) {
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
count: 0,
|
|
25
|
+
message: 'No active agents found',
|
|
26
|
+
agents: [],
|
|
27
|
+
}, null, 2);
|
|
28
|
+
}
|
|
29
|
+
return JSON.stringify({
|
|
30
|
+
count: agents.length,
|
|
31
|
+
agents: agents.map((a) => ({
|
|
32
|
+
id: a.id,
|
|
33
|
+
agent: a.agent,
|
|
34
|
+
title: a.title,
|
|
35
|
+
folder: a.folder,
|
|
36
|
+
status: a.status,
|
|
37
|
+
started: a.started,
|
|
38
|
+
session: a.session,
|
|
39
|
+
parent: a.parent || null,
|
|
40
|
+
delegated_to: a.delegated_to || [],
|
|
41
|
+
})),
|
|
42
|
+
}, null, 2);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return JSON.stringify({
|
|
46
|
+
error: `Failed to list agents: ${error}`,
|
|
47
|
+
}, null, 2);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
'ff-agents-clear': tool({
|
|
52
|
+
description: 'Clear agent context files. Can clear all, or filter by session, agent type, or specific UUID',
|
|
53
|
+
args: {
|
|
54
|
+
sessionID: tool.schema
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('Clear only agents for specific session'),
|
|
58
|
+
agent: tool.schema.string().optional().describe('Clear only specific agent type'),
|
|
59
|
+
id: tool.schema.string().optional().describe('Clear specific agent by UUID'),
|
|
60
|
+
},
|
|
61
|
+
execute: async (args) => {
|
|
62
|
+
try {
|
|
63
|
+
let files = [];
|
|
64
|
+
if (args.id) {
|
|
65
|
+
files = await findAgentFilesById(input, args.id);
|
|
66
|
+
}
|
|
67
|
+
else if (args.agent && args.sessionID) {
|
|
68
|
+
files = await findAgentFiles(input, args.agent, args.sessionID);
|
|
69
|
+
}
|
|
70
|
+
else if (args.sessionID) {
|
|
71
|
+
files = await findAgentFiles(input, undefined, args.sessionID);
|
|
72
|
+
}
|
|
73
|
+
else if (args.agent) {
|
|
74
|
+
files = await findAgentFiles(input, args.agent);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
files = await findAllAgentFiles(input);
|
|
78
|
+
}
|
|
79
|
+
if (files.length === 0) {
|
|
80
|
+
return 'No agent context files found to clear.';
|
|
81
|
+
}
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
try {
|
|
84
|
+
await $ `rm ${file}`.quiet();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Continue even if one file fails
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return `Cleared ${files.length} agent context file(s)`;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return `Error clearing agents: ${error}`;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
'ff-agents-show': tool({
|
|
98
|
+
description: 'Show detailed context for a specific agent by UUID',
|
|
99
|
+
args: {
|
|
100
|
+
id: tool.schema.string().describe('Agent UUID to show details for'),
|
|
101
|
+
},
|
|
102
|
+
execute: async (args) => {
|
|
103
|
+
try {
|
|
104
|
+
const agentContext = await readAgentContextById(input, args.id);
|
|
105
|
+
if (!agentContext) {
|
|
106
|
+
return `Agent with ID "${args.id}" not found`;
|
|
107
|
+
}
|
|
108
|
+
return JSON.stringify(agentContext, null, 2);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return `Error reading agent: ${error}`;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
|
+
type BunShell = any;
|
|
3
|
+
/**
|
|
4
|
+
* Initialize the Feature Factory directory structure.
|
|
5
|
+
* Creates .feature-factory/ and .feature-factory/agents/ directories.
|
|
6
|
+
* Migrates management/ci.sh to .feature-factory/ci.sh if it exists.
|
|
7
|
+
*/
|
|
8
|
+
export declare function initializeFeatureFactory(input: PluginInput, $: BunShell): Promise<void>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const SERVICE_NAME = 'feature-factory';
|
|
2
|
+
/**
|
|
3
|
+
* Log a message using the OpenCode client's structured logging.
|
|
4
|
+
* Silently fails if logging is unavailable.
|
|
5
|
+
*/
|
|
6
|
+
async function log(client, level, message, extra) {
|
|
7
|
+
try {
|
|
8
|
+
await client.app.log({
|
|
9
|
+
body: {
|
|
10
|
+
service: SERVICE_NAME,
|
|
11
|
+
level,
|
|
12
|
+
message,
|
|
13
|
+
extra,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Logging failure should not affect plugin operation
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Initialize the Feature Factory directory structure.
|
|
23
|
+
* Creates .feature-factory/ and .feature-factory/agents/ directories.
|
|
24
|
+
* Migrates management/ci.sh to .feature-factory/ci.sh if it exists.
|
|
25
|
+
*/
|
|
26
|
+
export async function initializeFeatureFactory(input, $) {
|
|
27
|
+
const { directory, client } = input;
|
|
28
|
+
const featureFactoryDir = `${directory}/.feature-factory`;
|
|
29
|
+
const agentsDir = `${featureFactoryDir}/agents`;
|
|
30
|
+
// Create directories if they don't exist
|
|
31
|
+
try {
|
|
32
|
+
await $ `mkdir -p ${agentsDir}`.quiet();
|
|
33
|
+
await log(client, 'debug', 'feature-factory-directories-created', {
|
|
34
|
+
featureFactoryDir,
|
|
35
|
+
agentsDir,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
await log(client, 'warn', 'feature-factory-directory-creation-failed', {
|
|
40
|
+
error: String(error),
|
|
41
|
+
});
|
|
42
|
+
// Continue even if directory creation fails - it might already exist
|
|
43
|
+
}
|
|
44
|
+
// Check for CI script migration
|
|
45
|
+
const oldCiPath = `${directory}/management/ci.sh`;
|
|
46
|
+
const newCiPath = `${featureFactoryDir}/ci.sh`;
|
|
47
|
+
try {
|
|
48
|
+
// Check if old CI exists
|
|
49
|
+
await $ `test -f ${oldCiPath}`.quiet();
|
|
50
|
+
// Old exists, check if new already exists
|
|
51
|
+
try {
|
|
52
|
+
await $ `test -f ${newCiPath}`.quiet();
|
|
53
|
+
// Both exist - migration already done or user has both
|
|
54
|
+
await log(client, 'debug', 'ci-sh-both-locations-exist', {
|
|
55
|
+
oldPath: oldCiPath,
|
|
56
|
+
newPath: newCiPath,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Old exists, new doesn't - migrate
|
|
61
|
+
try {
|
|
62
|
+
await $ `cp ${oldCiPath} ${newCiPath}`.quiet();
|
|
63
|
+
await $ `rm ${oldCiPath}`.quiet();
|
|
64
|
+
await log(client, 'info', 'ci-sh-migrated', {
|
|
65
|
+
from: oldCiPath,
|
|
66
|
+
to: newCiPath,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (migrateError) {
|
|
70
|
+
await log(client, 'error', 'ci-sh-migration-failed', {
|
|
71
|
+
from: oldCiPath,
|
|
72
|
+
to: newCiPath,
|
|
73
|
+
error: String(migrateError),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Old doesn't exist, nothing to migrate
|
|
80
|
+
await log(client, 'debug', 'ci-sh-no-migration-needed', {
|
|
81
|
+
oldPath: oldCiPath,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,11 +6,16 @@ import type { Plugin } from '@opencode-ai/plugin';
|
|
|
6
6
|
*
|
|
7
7
|
* Behavior:
|
|
8
8
|
* - Runs quality gate on session.idle (when agent finishes working)
|
|
9
|
-
* - Executes
|
|
9
|
+
* - Executes .feature-factory/ci.sh directly (no LLM involvement)
|
|
10
10
|
* - On fast feedback: "Quality gate is running, please stand-by for results ..."
|
|
11
11
|
* - On success: "Quality gate passed"
|
|
12
12
|
* - On failure: passes full CI output to LLM for fix instructions
|
|
13
|
-
* - If
|
|
13
|
+
* - If .feature-factory/ci.sh does not exist, quality gate does not run
|
|
14
|
+
*
|
|
15
|
+
* Feature Factory Context System:
|
|
16
|
+
* - Creates .feature-factory/ directory structure on startup
|
|
17
|
+
* - Migrates management/ci.sh to .feature-factory/ci.sh if present
|
|
18
|
+
* - Provides tools for agent context tracking (ff-agents-current, ff-agents-clear, ff-agents-show)
|
|
14
19
|
*/
|
|
15
20
|
export declare const StopQualityGatePlugin: Plugin;
|
|
16
21
|
export default StopQualityGatePlugin;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createQualityGateHooks } from './stop-quality-gate.js';
|
|
2
|
+
import { initializeFeatureFactory } from './feature-factory-setup.js';
|
|
3
|
+
import { createAgentManagementTools } from './agent-management-tools.js';
|
|
2
4
|
const SERVICE_NAME = 'feature-factory';
|
|
3
5
|
/**
|
|
4
6
|
* Log a message using the OpenCode client's structured logging.
|
|
@@ -36,19 +38,33 @@ function resolveRootDir(input) {
|
|
|
36
38
|
*
|
|
37
39
|
* Behavior:
|
|
38
40
|
* - Runs quality gate on session.idle (when agent finishes working)
|
|
39
|
-
* - Executes
|
|
41
|
+
* - Executes .feature-factory/ci.sh directly (no LLM involvement)
|
|
40
42
|
* - On fast feedback: "Quality gate is running, please stand-by for results ..."
|
|
41
43
|
* - On success: "Quality gate passed"
|
|
42
44
|
* - On failure: passes full CI output to LLM for fix instructions
|
|
43
|
-
* - If
|
|
45
|
+
* - If .feature-factory/ci.sh does not exist, quality gate does not run
|
|
46
|
+
*
|
|
47
|
+
* Feature Factory Context System:
|
|
48
|
+
* - Creates .feature-factory/ directory structure on startup
|
|
49
|
+
* - Migrates management/ci.sh to .feature-factory/ci.sh if present
|
|
50
|
+
* - Provides tools for agent context tracking (ff-agents-current, ff-agents-clear, ff-agents-show)
|
|
44
51
|
*/
|
|
45
52
|
export const StopQualityGatePlugin = async (input) => {
|
|
46
|
-
const { worktree, directory, client } = input;
|
|
53
|
+
const { worktree, directory, client, $ } = input;
|
|
47
54
|
const rootDir = resolveRootDir({ worktree, directory });
|
|
48
|
-
// Skip
|
|
55
|
+
// Skip if no valid directory (e.g., global config with no project)
|
|
49
56
|
if (!rootDir || rootDir === '' || rootDir === '/') {
|
|
50
57
|
return {};
|
|
51
58
|
}
|
|
59
|
+
// Initialize Feature Factory directory structure
|
|
60
|
+
try {
|
|
61
|
+
await initializeFeatureFactory(input, $);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
await log(client, 'error', 'feature-factory-init-error', {
|
|
65
|
+
error: String(error),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
52
68
|
// Create quality gate hooks
|
|
53
69
|
let qualityGateHooks = {};
|
|
54
70
|
try {
|
|
@@ -59,8 +75,19 @@ export const StopQualityGatePlugin = async (input) => {
|
|
|
59
75
|
error: String(error),
|
|
60
76
|
});
|
|
61
77
|
}
|
|
78
|
+
// Create agent management tools
|
|
79
|
+
let agentManagementHooks = {};
|
|
80
|
+
try {
|
|
81
|
+
agentManagementHooks = createAgentManagementTools(input);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
await log(client, 'error', 'agent-management.init-error', {
|
|
85
|
+
error: String(error),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
62
88
|
return {
|
|
63
89
|
...qualityGateHooks,
|
|
90
|
+
...agentManagementHooks,
|
|
64
91
|
};
|
|
65
92
|
};
|
|
66
93
|
// Default export for OpenCode plugin discovery
|
package/dist/output.test.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - extractErrorLines: extracts error-like lines from command output
|
|
6
6
|
* - tailLines: returns last N lines with truncation notice
|
|
7
7
|
*/
|
|
8
|
-
import { extractErrorLines, tailLines } from './output
|
|
8
|
+
import { extractErrorLines, tailLines } from './output';
|
|
9
9
|
describe('extractErrorLines', () => {
|
|
10
10
|
it('should return empty array for empty input', () => {
|
|
11
11
|
expect(extractErrorLines('', 10)).toEqual([]);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - hasConfiguredCommands: checks if explicit commands are configured
|
|
7
7
|
* - DEFAULT_QUALITY_GATE: verify default values
|
|
8
8
|
*/
|
|
9
|
-
import { mergeQualityGateConfig, hasConfiguredCommands, DEFAULT_QUALITY_GATE, } from './quality-gate-config
|
|
9
|
+
import { mergeQualityGateConfig, hasConfiguredCommands, DEFAULT_QUALITY_GATE, } from './quality-gate-config';
|
|
10
10
|
describe('DEFAULT_QUALITY_GATE', () => {
|
|
11
11
|
it('should have correct default values', () => {
|
|
12
12
|
expect(DEFAULT_QUALITY_GATE.steps).toEqual(['lint', 'build', 'test']);
|
|
@@ -158,7 +158,7 @@ export async function createQualityGateHooks(input) {
|
|
|
158
158
|
const { client, $, directory } = input;
|
|
159
159
|
async function ciShExists() {
|
|
160
160
|
try {
|
|
161
|
-
await $ `test -f ${directory}/
|
|
161
|
+
await $ `test -f ${directory}/.feature-factory/ci.sh`.quiet();
|
|
162
162
|
return true;
|
|
163
163
|
}
|
|
164
164
|
catch {
|
|
@@ -213,7 +213,7 @@ export async function createQualityGateHooks(input) {
|
|
|
213
213
|
let ciOutput = '';
|
|
214
214
|
let ciPassed = false;
|
|
215
215
|
let timedOut = false;
|
|
216
|
-
const ciPath = `${directory}/
|
|
216
|
+
const ciPath = `${directory}/.feature-factory/ci.sh`;
|
|
217
217
|
// eslint-disable-next-line no-undef
|
|
218
218
|
const proc = Bun.spawn(['bash', ciPath], {
|
|
219
219
|
cwd: directory,
|
|
@@ -274,7 +274,7 @@ export async function createQualityGateHooks(input) {
|
|
|
274
274
|
|
|
275
275
|
**Important:** Do not interrupt your current task. Add "Fix quality gate failures" to your todo list and continue with what you were doing. Address the quality gate issues after completing your current task.
|
|
276
276
|
|
|
277
|
-
If the failure details are missing or truncated, run "
|
|
277
|
+
If the failure details are missing or truncated, run ".feature-factory/ci.sh" to get the full output.`;
|
|
278
278
|
const message = timedOut
|
|
279
279
|
? `⏱️ Quality gate timed out\n\nThe CI execution exceeded the ${CI_TIMEOUT_MS / 1000} second timeout. The build may be hanging or taking too long.\n\n\`\`\`\n${sanitizedOutput}\n\`\`\`${instructions}`
|
|
280
280
|
: `❌ Quality gate failed\n\nThe CI checks did not pass. Please review the output below and fix the issues:\n\n\`\`\`\n${sanitizedOutput}\n\`\`\`${instructions}`;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - sanitizeOutput: redacts secrets from CI output
|
|
6
6
|
* - isSessionReadOnly: determines if session has write permissions
|
|
7
7
|
*/
|
|
8
|
-
import { sanitizeOutput, truncateOutput } from './stop-quality-gate
|
|
8
|
+
import { sanitizeOutput, truncateOutput } from './stop-quality-gate';
|
|
9
9
|
function isSessionReadOnly(permission) {
|
|
10
10
|
if (!permission)
|
|
11
11
|
return false;
|
package/dist/uuid.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UUID v4 generator
|
|
3
|
+
* Generates random UUIDs for agent instance tracking
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Generate a random UUID v4
|
|
7
|
+
* Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateUUID(): string;
|
|
10
|
+
/**
|
|
11
|
+
* Validate if a string is a valid UUID v4
|
|
12
|
+
*/
|
|
13
|
+
export declare function isValidUUID(str: string): boolean;
|
package/dist/uuid.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UUID v4 generator
|
|
3
|
+
* Generates random UUIDs for agent instance tracking
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Generate a random UUID v4
|
|
7
|
+
* Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
8
|
+
*/
|
|
9
|
+
export function generateUUID() {
|
|
10
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
11
|
+
const r = (Math.random() * 16) | 0;
|
|
12
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
13
|
+
return v.toString(16);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Validate if a string is a valid UUID v4
|
|
18
|
+
*/
|
|
19
|
+
export function isValidUUID(str) {
|
|
20
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
21
|
+
return uuidRegex.test(str);
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@syntesseraai/opencode-feature-factory",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.14",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenCode plugin for Feature Factory agents - provides sub-agents and skills for validation, review, security, and architecture assessment",
|
|
7
7
|
"license": "MIT",
|