@syntesseraai/opencode-feature-factory 0.10.1 → 0.10.3
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 +14 -6
- package/agents/feature-factory.md +15 -3
- package/dist/tools/mini-loop.d.ts +4 -0
- package/dist/tools/mini-loop.js +318 -228
- package/dist/tools/pipeline.d.ts +4 -0
- package/dist/tools/pipeline.js +522 -434
- package/dist/tools/prompts.d.ts +1 -0
- package/dist/tools/prompts.js +16 -0
- package/dist/workflow/ci-runner.d.ts +22 -0
- package/dist/workflow/ci-runner.js +91 -0
- package/dist/workflow/fan-out.d.ts +27 -6
- package/dist/workflow/fan-out.js +40 -12
- package/dist/workflow/orchestrator.d.ts +2 -1
- package/dist/workflow/orchestrator.js +2 -1
- package/dist/workflow/run-isolation.d.ts +13 -0
- package/dist/workflow/run-isolation.js +77 -0
- package/package.json +1 -1
package/dist/tools/prompts.d.ts
CHANGED
|
@@ -21,4 +21,5 @@ export declare function reviewSynthesisPrompt(reviews: Array<{
|
|
|
21
21
|
export declare function documentPrompt(input: string): string;
|
|
22
22
|
export declare function docReviewPrompt(docUpdate: string): string;
|
|
23
23
|
export declare function miniBuildPrompt(requirements: string, reworkFeedback?: string): string;
|
|
24
|
+
export declare function ciFixPrompt(requirements: string, ciOutput: string): string;
|
|
24
25
|
export declare function miniReviewPrompt(implementationReport: string): string;
|
package/dist/tools/prompts.js
CHANGED
|
@@ -169,6 +169,22 @@ Requirements:
|
|
|
169
169
|
3. Run lint/typecheck/tests only for impacted scope.
|
|
170
170
|
4. Return a concise implementation report with changed files, tests run, and known open issues.`;
|
|
171
171
|
}
|
|
172
|
+
export function ciFixPrompt(requirements, ciOutput) {
|
|
173
|
+
return `The CI checks (ff-ci.sh) failed after your implementation. Fix the issues identified below.
|
|
174
|
+
|
|
175
|
+
Original requirements:
|
|
176
|
+
${requirements}
|
|
177
|
+
|
|
178
|
+
CI failure output:
|
|
179
|
+
\`\`\`
|
|
180
|
+
${ciOutput}
|
|
181
|
+
\`\`\`
|
|
182
|
+
|
|
183
|
+
Requirements:
|
|
184
|
+
1. Fix all failing checks (lint, typecheck, build, tests).
|
|
185
|
+
2. Do not remove or skip tests — fix the underlying issues.
|
|
186
|
+
3. Return a concise implementation report with changed files and what was fixed.`;
|
|
187
|
+
}
|
|
172
188
|
export function miniReviewPrompt(implementationReport) {
|
|
173
189
|
return `Review the latest mini-loop implementation output below.
|
|
174
190
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI runner utility for workflow tools.
|
|
3
|
+
*
|
|
4
|
+
* Executes `ff-ci.sh` in a subprocess and returns a structured result.
|
|
5
|
+
* Reuses sanitizeOutput / truncateOutput from stop-quality-gate.ts for
|
|
6
|
+
* consistent secret redaction and output trimming.
|
|
7
|
+
*/
|
|
8
|
+
export interface CIResult {
|
|
9
|
+
passed: boolean;
|
|
10
|
+
output: string;
|
|
11
|
+
timedOut: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Check whether `ff-ci.sh` exists at the given directory root.
|
|
15
|
+
* Never throws — returns `false` on any error.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ciScriptExists(directory: string): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Execute `ff-ci.sh` from the given directory and return a structured result.
|
|
20
|
+
* Never throws — returns a failure result on any error.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runCI(directory: string): Promise<CIResult>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI runner utility for workflow tools.
|
|
3
|
+
*
|
|
4
|
+
* Executes `ff-ci.sh` in a subprocess and returns a structured result.
|
|
5
|
+
* Reuses sanitizeOutput / truncateOutput from stop-quality-gate.ts for
|
|
6
|
+
* consistent secret redaction and output trimming.
|
|
7
|
+
*/
|
|
8
|
+
import { sanitizeOutput, truncateOutput } from '../stop-quality-gate.js';
|
|
9
|
+
const CI_TIMEOUT_MS = 300_000; // 5 minutes
|
|
10
|
+
/**
|
|
11
|
+
* Check whether `ff-ci.sh` exists at the given directory root.
|
|
12
|
+
* Never throws — returns `false` on any error.
|
|
13
|
+
*/
|
|
14
|
+
export async function ciScriptExists(directory) {
|
|
15
|
+
try {
|
|
16
|
+
const ciPath = `${directory}/ff-ci.sh`;
|
|
17
|
+
// eslint-disable-next-line no-undef
|
|
18
|
+
const proc = Bun.spawn(['test', '-f', ciPath], {
|
|
19
|
+
cwd: directory,
|
|
20
|
+
stdout: 'pipe',
|
|
21
|
+
stderr: 'pipe',
|
|
22
|
+
});
|
|
23
|
+
await proc.exited;
|
|
24
|
+
return proc.exitCode === 0;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Execute `ff-ci.sh` from the given directory and return a structured result.
|
|
32
|
+
* Never throws — returns a failure result on any error.
|
|
33
|
+
*/
|
|
34
|
+
export async function runCI(directory) {
|
|
35
|
+
try {
|
|
36
|
+
const ciPath = `${directory}/ff-ci.sh`;
|
|
37
|
+
// eslint-disable-next-line no-undef
|
|
38
|
+
const proc = Bun.spawn(['bash', ciPath], {
|
|
39
|
+
cwd: directory,
|
|
40
|
+
stdout: 'pipe',
|
|
41
|
+
stderr: 'pipe',
|
|
42
|
+
});
|
|
43
|
+
let timedOut = false;
|
|
44
|
+
let timeoutId = null;
|
|
45
|
+
let forceKillTimeoutId = null;
|
|
46
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
47
|
+
timeoutId = setTimeout(() => {
|
|
48
|
+
timedOut = true;
|
|
49
|
+
proc.kill('SIGTERM');
|
|
50
|
+
forceKillTimeoutId = setTimeout(() => {
|
|
51
|
+
try {
|
|
52
|
+
proc.kill('SIGKILL');
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Process already terminated
|
|
56
|
+
}
|
|
57
|
+
}, 5000);
|
|
58
|
+
resolve();
|
|
59
|
+
}, CI_TIMEOUT_MS);
|
|
60
|
+
});
|
|
61
|
+
await Promise.race([proc.exited, timeoutPromise]);
|
|
62
|
+
if (timeoutId)
|
|
63
|
+
clearTimeout(timeoutId);
|
|
64
|
+
if (forceKillTimeoutId)
|
|
65
|
+
clearTimeout(forceKillTimeoutId);
|
|
66
|
+
const stdout = await new Response(proc.stdout).text();
|
|
67
|
+
const stderr = await new Response(proc.stderr).text();
|
|
68
|
+
let output;
|
|
69
|
+
let passed;
|
|
70
|
+
if (timedOut) {
|
|
71
|
+
output =
|
|
72
|
+
`CI execution timed out after ${CI_TIMEOUT_MS / 1000} seconds\n\n${stdout}\n${stderr}`.trim();
|
|
73
|
+
passed = false;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
output = stdout + (stderr ? `\n${stderr}` : '');
|
|
77
|
+
passed = proc.exitCode === 0;
|
|
78
|
+
}
|
|
79
|
+
// Sanitize secrets and truncate for prompt safety
|
|
80
|
+
output = truncateOutput(sanitizeOutput(output));
|
|
81
|
+
return { passed, output, timedOut };
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
85
|
+
return {
|
|
86
|
+
passed: false,
|
|
87
|
+
output: `CI runner error: ${message}`,
|
|
88
|
+
timedOut: false,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -18,6 +18,9 @@ import type { NamedModel, ModelId } from './types.js';
|
|
|
18
18
|
export type Client = {
|
|
19
19
|
session: {
|
|
20
20
|
create(options: {
|
|
21
|
+
query?: {
|
|
22
|
+
directory?: string;
|
|
23
|
+
};
|
|
21
24
|
body?: {
|
|
22
25
|
parentID?: string;
|
|
23
26
|
title?: string;
|
|
@@ -28,6 +31,9 @@ export type Client = {
|
|
|
28
31
|
};
|
|
29
32
|
}>;
|
|
30
33
|
prompt(options: {
|
|
34
|
+
query?: {
|
|
35
|
+
directory?: string;
|
|
36
|
+
};
|
|
31
37
|
path: {
|
|
32
38
|
id: string;
|
|
33
39
|
};
|
|
@@ -53,6 +59,9 @@ export type Client = {
|
|
|
53
59
|
};
|
|
54
60
|
}>;
|
|
55
61
|
promptAsync(options: {
|
|
62
|
+
query?: {
|
|
63
|
+
directory?: string;
|
|
64
|
+
};
|
|
56
65
|
path: {
|
|
57
66
|
id: string;
|
|
58
67
|
};
|
|
@@ -100,6 +109,14 @@ export declare function extractText(parts: Array<{
|
|
|
100
109
|
text?: string;
|
|
101
110
|
[k: string]: unknown;
|
|
102
111
|
}>): string;
|
|
112
|
+
export interface SessionContext {
|
|
113
|
+
sessionId: string;
|
|
114
|
+
directory?: string;
|
|
115
|
+
}
|
|
116
|
+
export declare function createRunParentSession(client: Client, callerSessionId: string, options?: {
|
|
117
|
+
title?: string;
|
|
118
|
+
directory?: string;
|
|
119
|
+
}): Promise<SessionContext>;
|
|
103
120
|
export interface FanOutResult {
|
|
104
121
|
tag: string;
|
|
105
122
|
raw: string;
|
|
@@ -111,13 +128,13 @@ export interface FanOutResult {
|
|
|
111
128
|
* Each model's work runs in its own child session so none of the
|
|
112
129
|
* intermediate outputs pollute the parent context window.
|
|
113
130
|
*/
|
|
114
|
-
export declare function fanOut(client: Client,
|
|
131
|
+
export declare function fanOut(client: Client, context: SessionContext, models: readonly NamedModel[], buildPrompt: (tag: string) => string, agent?: string): Promise<FanOutResult[]>;
|
|
115
132
|
/**
|
|
116
133
|
* Prompt in an isolated child session and return the raw text response.
|
|
117
134
|
*
|
|
118
135
|
* This keeps the sub-step off the parent's context window.
|
|
119
136
|
*/
|
|
120
|
-
export declare function promptSession(client: Client,
|
|
137
|
+
export declare function promptSession(client: Client, context: SessionContext, prompt: string, options?: {
|
|
121
138
|
model?: ModelId;
|
|
122
139
|
agent?: string;
|
|
123
140
|
title?: string;
|
|
@@ -125,8 +142,12 @@ export declare function promptSession(client: Client, parentSessionId: string, p
|
|
|
125
142
|
/**
|
|
126
143
|
* Send a visible notification message to the parent session via `promptAsync`.
|
|
127
144
|
*
|
|
128
|
-
*
|
|
129
|
-
* without triggering an LLM turn.
|
|
130
|
-
*
|
|
145
|
+
* By default uses `noReply: true` so the message appears in the TUI as a chat
|
|
146
|
+
* message without triggering an LLM turn. Pass `noReply: false` for terminal
|
|
147
|
+
* messages (completion reports, errors) so the LLM can validate the outcome.
|
|
148
|
+
*
|
|
149
|
+
* Errors are swallowed — notification delivery must never break the pipeline.
|
|
131
150
|
*/
|
|
132
|
-
export declare function notifyParent(client: Client,
|
|
151
|
+
export declare function notifyParent(client: Client, context: SessionContext, agent: string | undefined, message: string, options?: {
|
|
152
|
+
noReply?: boolean;
|
|
153
|
+
}): Promise<void>;
|
package/dist/workflow/fan-out.js
CHANGED
|
@@ -19,6 +19,29 @@ export function extractText(parts) {
|
|
|
19
19
|
.map((p) => p.text)
|
|
20
20
|
.join('\n');
|
|
21
21
|
}
|
|
22
|
+
function withDirectory(directory) {
|
|
23
|
+
if (!directory) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return { directory };
|
|
27
|
+
}
|
|
28
|
+
export async function createRunParentSession(client, callerSessionId, options) {
|
|
29
|
+
const session = await client.session.create({
|
|
30
|
+
query: withDirectory(options?.directory),
|
|
31
|
+
body: {
|
|
32
|
+
parentID: callerSessionId,
|
|
33
|
+
title: options?.title,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const runParentId = session.data?.id;
|
|
37
|
+
if (!runParentId) {
|
|
38
|
+
throw new Error('Failed to create run parent session');
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
sessionId: runParentId,
|
|
42
|
+
directory: options?.directory,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
22
45
|
// ---------------------------------------------------------------------------
|
|
23
46
|
// Isolated session helper
|
|
24
47
|
// ---------------------------------------------------------------------------
|
|
@@ -28,11 +51,12 @@ export function extractText(parts) {
|
|
|
28
51
|
* The child session is parented to `parentSessionId` so it appears in the
|
|
29
52
|
* session tree but its messages do **not** pollute the parent context window.
|
|
30
53
|
*/
|
|
31
|
-
async function promptInChildSession(client,
|
|
54
|
+
async function promptInChildSession(client, context, prompt, options) {
|
|
32
55
|
// 1. Create an isolated child session
|
|
33
56
|
const session = await client.session.create({
|
|
57
|
+
query: withDirectory(context.directory),
|
|
34
58
|
body: {
|
|
35
|
-
parentID:
|
|
59
|
+
parentID: context.sessionId,
|
|
36
60
|
title: options?.title,
|
|
37
61
|
},
|
|
38
62
|
});
|
|
@@ -53,6 +77,7 @@ async function promptInChildSession(client, parentSessionId, prompt, options) {
|
|
|
53
77
|
body.agent = options.agent;
|
|
54
78
|
}
|
|
55
79
|
const response = await client.session.prompt({
|
|
80
|
+
query: withDirectory(context.directory),
|
|
56
81
|
path: { id: childId },
|
|
57
82
|
body,
|
|
58
83
|
});
|
|
@@ -66,9 +91,9 @@ async function promptInChildSession(client, parentSessionId, prompt, options) {
|
|
|
66
91
|
* Each model's work runs in its own child session so none of the
|
|
67
92
|
* intermediate outputs pollute the parent context window.
|
|
68
93
|
*/
|
|
69
|
-
export async function fanOut(client,
|
|
94
|
+
export async function fanOut(client, context, models, buildPrompt, agent) {
|
|
70
95
|
const results = await Promise.all(models.map(async (nm) => {
|
|
71
|
-
const raw = await promptInChildSession(client,
|
|
96
|
+
const raw = await promptInChildSession(client, context, buildPrompt(nm.tag), {
|
|
72
97
|
model: nm.model,
|
|
73
98
|
agent,
|
|
74
99
|
title: `ff-fanout-${nm.tag}`,
|
|
@@ -85,8 +110,8 @@ export async function fanOut(client, parentSessionId, models, buildPrompt, agent
|
|
|
85
110
|
*
|
|
86
111
|
* This keeps the sub-step off the parent's context window.
|
|
87
112
|
*/
|
|
88
|
-
export async function promptSession(client,
|
|
89
|
-
return promptInChildSession(client,
|
|
113
|
+
export async function promptSession(client, context, prompt, options) {
|
|
114
|
+
return promptInChildSession(client, context, prompt, options);
|
|
90
115
|
}
|
|
91
116
|
// ---------------------------------------------------------------------------
|
|
92
117
|
// Parent session notification helper
|
|
@@ -94,21 +119,24 @@ export async function promptSession(client, parentSessionId, prompt, options) {
|
|
|
94
119
|
/**
|
|
95
120
|
* Send a visible notification message to the parent session via `promptAsync`.
|
|
96
121
|
*
|
|
97
|
-
*
|
|
98
|
-
* without triggering an LLM turn.
|
|
99
|
-
*
|
|
122
|
+
* By default uses `noReply: true` so the message appears in the TUI as a chat
|
|
123
|
+
* message without triggering an LLM turn. Pass `noReply: false` for terminal
|
|
124
|
+
* messages (completion reports, errors) so the LLM can validate the outcome.
|
|
125
|
+
*
|
|
126
|
+
* Errors are swallowed — notification delivery must never break the pipeline.
|
|
100
127
|
*/
|
|
101
|
-
export async function notifyParent(client,
|
|
128
|
+
export async function notifyParent(client, context, agent, message, options) {
|
|
102
129
|
try {
|
|
103
130
|
const body = {
|
|
104
|
-
noReply: true,
|
|
131
|
+
noReply: options?.noReply ?? true,
|
|
105
132
|
parts: [{ type: 'text', text: message }],
|
|
106
133
|
};
|
|
107
134
|
if (agent) {
|
|
108
135
|
body.agent = agent;
|
|
109
136
|
}
|
|
110
137
|
await client.session.promptAsync({
|
|
111
|
-
|
|
138
|
+
query: withDirectory(context.directory),
|
|
139
|
+
path: { id: context.sessionId },
|
|
112
140
|
body,
|
|
113
141
|
});
|
|
114
142
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Re-exports the fan-out, gate, and type modules as a single convenient
|
|
5
5
|
* entry-point for tool implementations.
|
|
6
6
|
*/
|
|
7
|
-
export { fanOut, promptSession, extractText, notifyParent, type Client } from './fan-out.js';
|
|
7
|
+
export { fanOut, promptSession, extractText, notifyParent, createRunParentSession, type Client, type SessionContext, } from './fan-out.js';
|
|
8
8
|
export { evaluatePlanningGate, evaluateReviewGate, evaluateDocGate, evaluateMiniLoopImplGate, evaluateMiniLoopDocGate, } from './gate-evaluator.js';
|
|
9
|
+
export { runCI, ciScriptExists } from './ci-runner.js';
|
|
9
10
|
export * from './types.js';
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Re-exports the fan-out, gate, and type modules as a single convenient
|
|
5
5
|
* entry-point for tool implementations.
|
|
6
6
|
*/
|
|
7
|
-
export { fanOut, promptSession, extractText, notifyParent } from './fan-out.js';
|
|
7
|
+
export { fanOut, promptSession, extractText, notifyParent, createRunParentSession, } from './fan-out.js';
|
|
8
8
|
export { evaluatePlanningGate, evaluateReviewGate, evaluateDocGate, evaluateMiniLoopImplGate, evaluateMiniLoopDocGate, } from './gate-evaluator.js';
|
|
9
|
+
export { runCI, ciScriptExists } from './ci-runner.js';
|
|
9
10
|
export * from './types.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface WorktreeOptions {
|
|
2
|
+
enabled?: boolean;
|
|
3
|
+
parentDirectory?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface RunDirectoryContext {
|
|
6
|
+
runId: string;
|
|
7
|
+
runDirectory: string;
|
|
8
|
+
worktreeEnabled: boolean;
|
|
9
|
+
worktreePath?: string;
|
|
10
|
+
repoRoot?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveRunDirectory(toolTag: 'pipeline' | 'mini-loop', worktree: WorktreeOptions | undefined, cwd?: string): Promise<RunDirectoryContext>;
|
|
13
|
+
export declare function cleanupWorktree(ctx: RunDirectoryContext): Promise<void>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { basename, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
const UNSAFE_WORKTREE_PARENT_DIRS = new Set(['/', '/etc']);
|
|
5
|
+
function assertSafeParentDirectory(parentDirectory) {
|
|
6
|
+
const normalized = resolve(parentDirectory);
|
|
7
|
+
if (UNSAFE_WORKTREE_PARENT_DIRS.has(normalized)) {
|
|
8
|
+
throw new Error(`Unsafe worktree_parent_dir: ${parentDirectory}. Choose a dedicated writable path.`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function resolveParentDirectory(cwd, parentDirectory) {
|
|
12
|
+
// Trust model: worktree_parent_dir is caller-controlled and can point anywhere
|
|
13
|
+
// on the local filesystem. We allow flexible absolute/relative paths but reject
|
|
14
|
+
// obviously unsafe system locations where accidental cleanup would be dangerous.
|
|
15
|
+
if (!parentDirectory || parentDirectory.trim().length === 0) {
|
|
16
|
+
return join(cwd, '.feature-factory', 'worktrees');
|
|
17
|
+
}
|
|
18
|
+
const resolvedParent = isAbsolute(parentDirectory)
|
|
19
|
+
? parentDirectory
|
|
20
|
+
: resolve(cwd, parentDirectory);
|
|
21
|
+
assertSafeParentDirectory(resolvedParent);
|
|
22
|
+
return resolvedParent;
|
|
23
|
+
}
|
|
24
|
+
async function runGit(args, cwd) {
|
|
25
|
+
// eslint-disable-next-line no-undef
|
|
26
|
+
const proc = Bun.spawn(['git', ...args], {
|
|
27
|
+
cwd,
|
|
28
|
+
stdout: 'pipe',
|
|
29
|
+
stderr: 'pipe',
|
|
30
|
+
});
|
|
31
|
+
const [stdout, stderr] = await Promise.all([
|
|
32
|
+
new Response(proc.stdout).text(),
|
|
33
|
+
new Response(proc.stderr).text(),
|
|
34
|
+
proc.exited,
|
|
35
|
+
]);
|
|
36
|
+
if (proc.exitCode !== 0) {
|
|
37
|
+
const detail = stderr.trim() || stdout.trim() || `git ${args.join(' ')} failed`;
|
|
38
|
+
throw new Error(detail);
|
|
39
|
+
}
|
|
40
|
+
return stdout.trim();
|
|
41
|
+
}
|
|
42
|
+
export async function resolveRunDirectory(toolTag, worktree, cwd = process.cwd()) {
|
|
43
|
+
const runId = `${toolTag}-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
44
|
+
if (!worktree?.enabled) {
|
|
45
|
+
return {
|
|
46
|
+
runId,
|
|
47
|
+
runDirectory: cwd,
|
|
48
|
+
worktreeEnabled: false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const repoRoot = await runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
52
|
+
const head = await runGit(['rev-parse', '--verify', 'HEAD'], repoRoot);
|
|
53
|
+
const parentDirectory = resolveParentDirectory(repoRoot, worktree.parentDirectory);
|
|
54
|
+
await mkdir(parentDirectory, { recursive: true });
|
|
55
|
+
const repoName = basename(repoRoot);
|
|
56
|
+
const worktreePath = join(parentDirectory, `${repoName}-${runId}`);
|
|
57
|
+
await runGit(['worktree', 'add', '--detach', worktreePath, head], repoRoot);
|
|
58
|
+
return {
|
|
59
|
+
runId,
|
|
60
|
+
runDirectory: worktreePath,
|
|
61
|
+
worktreeEnabled: true,
|
|
62
|
+
worktreePath,
|
|
63
|
+
repoRoot,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export async function cleanupWorktree(ctx) {
|
|
67
|
+
if (!ctx.worktreeEnabled || !ctx.worktreePath) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const gitCwd = ctx.repoRoot ?? ctx.runDirectory;
|
|
71
|
+
try {
|
|
72
|
+
await runGit(['worktree', 'remove', '--force', ctx.worktreePath], gitCwd);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.warn('[feature-factory] Failed to cleanup run worktree:', error);
|
|
76
|
+
}
|
|
77
|
+
}
|
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.10.
|
|
4
|
+
"version": "0.10.3",
|
|
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",
|