@jwillert/forgeflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +119 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +11 -0
- package/dist/cli/init.js +198 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/lock.d.ts +4 -0
- package/dist/cli/lock.js +55 -0
- package/dist/cli/lock.js.map +1 -0
- package/dist/cli/templates/Dockerfile +38 -0
- package/dist/cli/templates/build-image.sh +11 -0
- package/dist/core/capabilities.d.ts +75 -0
- package/dist/core/capabilities.js +2 -0
- package/dist/core/capabilities.js.map +1 -0
- package/dist/core/config.d.ts +38 -0
- package/dist/core/config.js +25 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/engine.d.ts +44 -0
- package/dist/core/engine.js +225 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/state.d.ts +42 -0
- package/dist/core/state.js +8 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/types.d.ts +141 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/workflow.d.ts +72 -0
- package/dist/core/workflow.js +38 -0
- package/dist/core/workflow.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/process/index.d.ts +14 -0
- package/dist/process/index.js +46 -0
- package/dist/process/index.js.map +1 -0
- package/dist/providers/github/index.d.ts +81 -0
- package/dist/providers/github/index.js +271 -0
- package/dist/providers/github/index.js.map +1 -0
- package/dist/providers/gitlab/index.d.ts +79 -0
- package/dist/providers/gitlab/index.js +263 -0
- package/dist/providers/gitlab/index.js.map +1 -0
- package/dist/state/sqlite/index.d.ts +2 -0
- package/dist/state/sqlite/index.js +138 -0
- package/dist/state/sqlite/index.js.map +1 -0
- package/dist/workflows/implement.d.ts +14 -0
- package/dist/workflows/implement.js +154 -0
- package/dist/workflows/implement.js.map +1 -0
- package/dist/workflows/index.d.ts +9 -0
- package/dist/workflows/index.js +5 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/prompts/implement.md +43 -0
- package/dist/workflows/prompts/review/extraction.md +21 -0
- package/dist/workflows/prompts/review/prompt.md +49 -0
- package/dist/workflows/prompts/update-branch/extraction.md +13 -0
- package/dist/workflows/prompts/update-branch/prompt.md +41 -0
- package/dist/workflows/review.d.ts +14 -0
- package/dist/workflows/review.js +134 -0
- package/dist/workflows/review.js.map +1 -0
- package/dist/workflows/shared/git.d.ts +17 -0
- package/dist/workflows/shared/git.js +28 -0
- package/dist/workflows/shared/git.js.map +1 -0
- package/dist/workflows/shared/lifecycle.d.ts +21 -0
- package/dist/workflows/shared/lifecycle.js +29 -0
- package/dist/workflows/shared/lifecycle.js.map +1 -0
- package/dist/workflows/shared/review-output.d.ts +44 -0
- package/dist/workflows/shared/review-output.js +100 -0
- package/dist/workflows/shared/review-output.js.map +1 -0
- package/dist/workflows/shared/sandcastle.d.ts +26 -0
- package/dist/workflows/shared/sandcastle.js +112 -0
- package/dist/workflows/shared/sandcastle.js.map +1 -0
- package/dist/workflows/shared/schema.d.ts +4 -0
- package/dist/workflows/shared/schema.js +17 -0
- package/dist/workflows/shared/schema.js.map +1 -0
- package/dist/workflows/update-branch.d.ts +12 -0
- package/dist/workflows/update-branch.js +148 -0
- package/dist/workflows/update-branch.js.map +1 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jan Willert
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Forgeflow
|
|
2
|
+
|
|
3
|
+
Forgeflow externalizes repository workflows that would otherwise live inside GitHub Actions or GitLab CI. It polls enabled targets, turns matching labels/comments into durable Agent Commands, and lets a worker run user-defined TypeScript workflows on your own machine or internal infrastructure.
|
|
4
|
+
|
|
5
|
+
The workflow can call anything: Sandcastle scripts, shell commands, local tools, Hermes, build scripts, or provider CLIs.
|
|
6
|
+
|
|
7
|
+
## MVP commands
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
forgeflow poll --config forgeflow.config.ts --max-events 100
|
|
11
|
+
forgeflow worker --config forgeflow.config.ts --parallel 3
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`poll` is one-shot and intended for cron/systemd. `worker` claims queued commands and runs workflows concurrently. `--parallel` defaults to `3`; `--limit` defaults to the same value.
|
|
15
|
+
|
|
16
|
+
## Scripted invocation
|
|
17
|
+
|
|
18
|
+
You can call Forgeflow from your own `run.ts` instead of using the CLI directly:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createEnvReader, createGateway } from "@jwillert/forgeflow"
|
|
22
|
+
import configFactory from "./.forgeflow/forgeflow.config"
|
|
23
|
+
|
|
24
|
+
const config = await configFactory({ env: createEnvReader() })
|
|
25
|
+
const gateway = createGateway(config)
|
|
26
|
+
|
|
27
|
+
const result = await gateway.runOnce({
|
|
28
|
+
maxEvents: 100,
|
|
29
|
+
parallel: 3,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
console.log(result)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then run once:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm run forgeflow:once
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or drain until idle:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm run forgeflow:drain
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Config shape
|
|
48
|
+
|
|
49
|
+
See `examples/forgeflow.config.ts` and `examples/run.ts` for fuller examples.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { defineConfig, defineWorkflow, labelAdded, Match, runProcessOrThrow } from "@jwillert/forgeflow"
|
|
53
|
+
import { github } from "forgeflow/github"
|
|
54
|
+
import { sqliteState } from "forgeflow/sqlite"
|
|
55
|
+
|
|
56
|
+
const implement = defineWorkflow("implement", {
|
|
57
|
+
match: ({ event }) => labelAdded(event, "agent:implement") ? Match.accept() : Match.ignore(),
|
|
58
|
+
run: async ({ command, workTracker }) => {
|
|
59
|
+
await workTracker.addLabel(command.workTarget, "agent:running")
|
|
60
|
+
await workTracker.removeLabel(command.workTarget, "agent:implement")
|
|
61
|
+
|
|
62
|
+
await runProcessOrThrow({
|
|
63
|
+
command: "npx",
|
|
64
|
+
args: ["tsx", ".sandcastle/agent-workflows/implement/implement.ts"],
|
|
65
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
66
|
+
env: {
|
|
67
|
+
ISSUE_NUMBER: command.workTarget.id,
|
|
68
|
+
ISSUE_TITLE: command.title,
|
|
69
|
+
BRANCH: `agent/issue-${command.workTarget.id}`,
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
await workTracker.removeLabel(command.workTarget, "agent:running")
|
|
74
|
+
await workTracker.addLabel(command.workTarget, "agent:done")
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
export default defineConfig(({ env }) => {
|
|
79
|
+
const gh = github({ token: env.required("GITHUB_TOKEN") })
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
state: sqliteState(env.optional("FORGEFLOW_DB", "./forgeflow.db")),
|
|
83
|
+
enabledTargets: [
|
|
84
|
+
gh.repo("owner/repo", { workflows: [implement] }),
|
|
85
|
+
],
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Design docs
|
|
91
|
+
|
|
92
|
+
- `CONTEXT.md` — domain glossary
|
|
93
|
+
- `docs/architecture.md` — architecture overview
|
|
94
|
+
- `docs/adr/` — architectural decisions
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { config as loadEnv } from "dotenv";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
6
|
+
import { tsImport } from "tsx/esm/api";
|
|
7
|
+
import { createGateway } from "../core/engine.js";
|
|
8
|
+
import { createEnvReader } from "../core/config.js";
|
|
9
|
+
import { withLock, withLockBlocking } from "./lock.js";
|
|
10
|
+
import { runInit } from "./init.js";
|
|
11
|
+
async function loadConfig(path) {
|
|
12
|
+
const resolved = resolve(path);
|
|
13
|
+
const mod = extname(resolved) === ".ts"
|
|
14
|
+
? await tsImport(resolved, import.meta.url)
|
|
15
|
+
: await import(pathToFileURL(resolved).href);
|
|
16
|
+
const exported = mod.default?.default ?? mod.default;
|
|
17
|
+
if (typeof exported === "function")
|
|
18
|
+
return await exported({ env: createEnvReader() });
|
|
19
|
+
return exported;
|
|
20
|
+
}
|
|
21
|
+
function loadEnvFile(configPath, envPath) {
|
|
22
|
+
loadEnv({ path: envPath ?? join(dirname(resolve(configPath)), ".env") });
|
|
23
|
+
}
|
|
24
|
+
const program = new Command()
|
|
25
|
+
.name("forgeflow")
|
|
26
|
+
.description("Agent workflow gateway")
|
|
27
|
+
.option("--config <path>", "config file", "forgeflow.config.ts");
|
|
28
|
+
program.command("poll")
|
|
29
|
+
.option("--max-events <number>", "maximum events/rechecks to process", v => Number(v), 100)
|
|
30
|
+
.action(async (opts) => {
|
|
31
|
+
const configPath = program.opts().config;
|
|
32
|
+
const config = await loadConfig(configPath);
|
|
33
|
+
const result = await createGateway(config).poll({ maxEvents: opts.maxEvents });
|
|
34
|
+
console.log(JSON.stringify(result, null, 2));
|
|
35
|
+
});
|
|
36
|
+
program.command("worker")
|
|
37
|
+
.option("--limit <number>", "maximum commands to claim; defaults to --parallel", v => Number(v))
|
|
38
|
+
.option("--parallel <number>", "maximum commands to run concurrently", v => Number(v), 3)
|
|
39
|
+
.action(async (opts) => {
|
|
40
|
+
const configPath = program.opts().config;
|
|
41
|
+
const config = await loadConfig(configPath);
|
|
42
|
+
const result = await createGateway(config).worker({ limit: opts.limit, parallel: opts.parallel });
|
|
43
|
+
console.log(JSON.stringify(result, null, 2));
|
|
44
|
+
});
|
|
45
|
+
program.command("run")
|
|
46
|
+
.description("Poll once and process everything currently queued")
|
|
47
|
+
.option("--env <path>", "path to .env file (default: <config dir>/.env)")
|
|
48
|
+
.option("--max-events <number>", "maximum events/rechecks to process", v => Number(v), 100)
|
|
49
|
+
.option("--limit <number>", "maximum commands to claim; defaults to --parallel", v => Number(v))
|
|
50
|
+
.option("--parallel <number>", "maximum commands to run concurrently", v => Number(v), 3)
|
|
51
|
+
.option("--lock <path>", "lock file path (default: <config dir>/forgeflow.lock)")
|
|
52
|
+
.action(async (opts) => {
|
|
53
|
+
const configPath = program.opts().config;
|
|
54
|
+
loadEnvFile(configPath, opts.env);
|
|
55
|
+
const lockPath = opts.lock ?? join(dirname(resolve(configPath)), "forgeflow.lock");
|
|
56
|
+
const result = await withLock(lockPath, async () => {
|
|
57
|
+
const config = await loadConfig(configPath);
|
|
58
|
+
return createGateway(config).runOnce({ maxEvents: opts.maxEvents, parallel: opts.parallel, limit: opts.limit });
|
|
59
|
+
});
|
|
60
|
+
console.log(JSON.stringify(result, null, 2));
|
|
61
|
+
});
|
|
62
|
+
program.command("drain")
|
|
63
|
+
.description("Run repeatedly until there is nothing left to do")
|
|
64
|
+
.option("--env <path>", "path to .env file (default: <config dir>/.env)")
|
|
65
|
+
.option("--max-events <number>", "maximum events/rechecks to process per iteration", v => Number(v), 100)
|
|
66
|
+
.option("--limit <number>", "maximum commands to claim per iteration; defaults to --parallel", v => Number(v))
|
|
67
|
+
.option("--parallel <number>", "maximum commands to run concurrently", v => Number(v), 3)
|
|
68
|
+
.option("--max-iterations <number>", "stop after this many iterations even if work remains", v => Number(v), 20)
|
|
69
|
+
.option("--lock <path>", "lock file path (default: <config dir>/forgeflow.lock)")
|
|
70
|
+
.action(async (opts) => {
|
|
71
|
+
const configPath = program.opts().config;
|
|
72
|
+
loadEnvFile(configPath, opts.env);
|
|
73
|
+
const lockPath = opts.lock ?? join(dirname(resolve(configPath)), "forgeflow.lock");
|
|
74
|
+
await withLockBlocking(lockPath, async () => {
|
|
75
|
+
const config = await loadConfig(configPath);
|
|
76
|
+
const gateway = createGateway(config);
|
|
77
|
+
for (let iteration = 1; iteration <= opts.maxIterations; iteration++) {
|
|
78
|
+
console.log(`\n=== Forgeflow drain iteration ${iteration}/${opts.maxIterations} ===`);
|
|
79
|
+
const result = await gateway.runOnce({ maxEvents: opts.maxEvents, parallel: opts.parallel, limit: opts.limit });
|
|
80
|
+
console.log(JSON.stringify(result, null, 2));
|
|
81
|
+
if (result.poll.events === 0 && result.poll.commands === 0 && result.worker.processed === 0) {
|
|
82
|
+
console.log("Forgeflow idle.");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
console.log(`Forgeflow drain stopped after ${opts.maxIterations} iteration(s).`);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
program.command("init")
|
|
90
|
+
.description("Scaffold a forgeflow config, prompts, and sandbox image for a new project")
|
|
91
|
+
.requiredOption("--provider <provider>", "github or gitlab")
|
|
92
|
+
.requiredOption("--repo <repo>", "repository to watch, e.g. owner/repo (github) or group/project (gitlab)")
|
|
93
|
+
.option("--dir <path>", "directory to scaffold into", ".forgeflow")
|
|
94
|
+
.option("--base-branch <name>", "base branch (default: current git branch, else main)")
|
|
95
|
+
.option("--force", "overwrite existing files", false)
|
|
96
|
+
.action(async (opts) => {
|
|
97
|
+
if (opts.provider !== "github" && opts.provider !== "gitlab") {
|
|
98
|
+
console.error(`Unknown provider "${opts.provider}" — expected "github" or "gitlab"`);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const { written, skipped } = runInit({
|
|
103
|
+
provider: opts.provider,
|
|
104
|
+
repo: opts.repo,
|
|
105
|
+
dir: opts.dir,
|
|
106
|
+
baseBranch: opts.baseBranch,
|
|
107
|
+
force: opts.force,
|
|
108
|
+
});
|
|
109
|
+
for (const path of written)
|
|
110
|
+
console.log(`created ${path}`);
|
|
111
|
+
for (const path of skipped)
|
|
112
|
+
console.log(`skipped ${path} (already exists, use --force to overwrite)`);
|
|
113
|
+
console.log(`\nNext steps:
|
|
114
|
+
1. cp ${opts.dir}/.env.example ${opts.dir}/.env and fill in your access token
|
|
115
|
+
2. bash ${opts.dir}/build-image.sh
|
|
116
|
+
3. npx forgeflow run --config ${opts.dir}/forgeflow.config.ts`);
|
|
117
|
+
});
|
|
118
|
+
await program.parseAsync(process.argv);
|
|
119
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, appendFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const bundledPromptsDir = join(here, "../workflows/prompts");
|
|
7
|
+
const templatesDir = join(here, "templates");
|
|
8
|
+
function detectBaseBranch() {
|
|
9
|
+
try {
|
|
10
|
+
return execFileSync("git", ["symbolic-ref", "--short", "HEAD"], { encoding: "utf8" }).trim() || "main";
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return "main";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function writeFile(path, content, force, written, skipped) {
|
|
17
|
+
if (existsSync(path) && !force) {
|
|
18
|
+
skipped.push(path);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
22
|
+
writeFileSync(path, content);
|
|
23
|
+
written.push(path);
|
|
24
|
+
}
|
|
25
|
+
function copyFile(from, to, force, written, skipped) {
|
|
26
|
+
if (existsSync(to) && !force) {
|
|
27
|
+
skipped.push(to);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
31
|
+
cpSync(from, to);
|
|
32
|
+
written.push(to);
|
|
33
|
+
}
|
|
34
|
+
function configTemplate(options, dir) {
|
|
35
|
+
const providerImport = options.provider === "github"
|
|
36
|
+
? `import { github } from "@jwillert/forgeflow/github"`
|
|
37
|
+
: `import { gitlab } from "@jwillert/forgeflow/gitlab"`;
|
|
38
|
+
const targetSetup = options.provider === "github"
|
|
39
|
+
? ` const gh = github({ token: env.required("GH_TOKEN") })
|
|
40
|
+
const target = gh.repo(env.required("FORGEFLOW_GITHUB_REPO"), {
|
|
41
|
+
baseBranch: env.optional("FORGEFLOW_BASE_REF", "${options.baseBranch}"),
|
|
42
|
+
workflows: [implement, review, updateBranch],
|
|
43
|
+
})`
|
|
44
|
+
: ` const gl = gitlab({
|
|
45
|
+
token: env.required("GITLAB_TOKEN"),
|
|
46
|
+
baseUrl: env.optional("FORGEFLOW_GITLAB_BASE_URL", "https://gitlab.com"),
|
|
47
|
+
})
|
|
48
|
+
const target = gl.project(env.required("FORGEFLOW_GITLAB_PROJECT"), {
|
|
49
|
+
baseBranch: env.optional("FORGEFLOW_BASE_REF", "${options.baseBranch}"),
|
|
50
|
+
workflows: [implement, review, updateBranch],
|
|
51
|
+
})`;
|
|
52
|
+
return `import { readFileSync } from "node:fs"
|
|
53
|
+
import { dirname, join } from "node:path"
|
|
54
|
+
import { fileURLToPath } from "node:url"
|
|
55
|
+
import { defineConfig } from "@jwillert/forgeflow"
|
|
56
|
+
${providerImport}
|
|
57
|
+
import { sqliteState } from "@jwillert/forgeflow/sqlite"
|
|
58
|
+
import { createImplementWorkflow, createReviewWorkflow, createUpdateBranchWorkflow } from "@jwillert/forgeflow/workflows"
|
|
59
|
+
|
|
60
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
61
|
+
|
|
62
|
+
// Prompts below were copied from forgeflow/workflows by \`forgeflow init\` — edit
|
|
63
|
+
// freely, or delete the promptFile/extractionPrompt overrides to fall back to
|
|
64
|
+
// the package's bundled defaults.
|
|
65
|
+
const implement = createImplementWorkflow({
|
|
66
|
+
promptFile: join(here, "prompts/implement.md"),
|
|
67
|
+
})
|
|
68
|
+
const review = createReviewWorkflow({
|
|
69
|
+
promptFile: join(here, "prompts/review/prompt.md"),
|
|
70
|
+
extractionPrompt: readFileSync(join(here, "prompts/review/extraction.md"), "utf8"),
|
|
71
|
+
})
|
|
72
|
+
const updateBranch = createUpdateBranchWorkflow({
|
|
73
|
+
promptFile: join(here, "prompts/update-branch/prompt.md"),
|
|
74
|
+
extractionPrompt: readFileSync(join(here, "prompts/update-branch/extraction.md"), "utf8"),
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
export default defineConfig(({ env }) => {
|
|
78
|
+
${targetSetup}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
state: sqliteState(env.optional("FORGEFLOW_DB", "${dir}/forgeflow.db")),
|
|
82
|
+
defaultDeferInterval: env.optional("FORGEFLOW_DEFAULT_DEFER_INTERVAL", "15m"),
|
|
83
|
+
enabledTargets: [target],
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
function envExampleTemplate(options, dir) {
|
|
89
|
+
const providerLines = options.provider === "github"
|
|
90
|
+
? `# GitHub access token. Needs repo contents/issues/pull-request permissions.
|
|
91
|
+
GH_TOKEN=
|
|
92
|
+
|
|
93
|
+
# Repository this config watches, e.g. "owner/repo".
|
|
94
|
+
FORGEFLOW_GITHUB_REPO=${options.repo}`
|
|
95
|
+
: `# GitLab access token. Needs api scope on the target project.
|
|
96
|
+
GITLAB_TOKEN=
|
|
97
|
+
|
|
98
|
+
# Self-managed GitLab instance base URL, if not gitlab.com.
|
|
99
|
+
FORGEFLOW_GITLAB_BASE_URL=https://gitlab.com
|
|
100
|
+
|
|
101
|
+
# Project this config watches, e.g. "group/project".
|
|
102
|
+
FORGEFLOW_GITLAB_PROJECT=${options.repo}`;
|
|
103
|
+
return `${providerLines}
|
|
104
|
+
|
|
105
|
+
# Base branch used for implementation work branches.
|
|
106
|
+
FORGEFLOW_BASE_REF=${options.baseBranch}
|
|
107
|
+
|
|
108
|
+
# SQLite database path for Forgeflow state.
|
|
109
|
+
FORGEFLOW_DB=${dir}/forgeflow.db
|
|
110
|
+
|
|
111
|
+
# Max provider events/deferred commands handled per run.
|
|
112
|
+
FORGEFLOW_MAX_EVENTS=100
|
|
113
|
+
|
|
114
|
+
# Number of queued commands run concurrently.
|
|
115
|
+
FORGEFLOW_PARALLEL=3
|
|
116
|
+
|
|
117
|
+
# Optional cap for commands claimed in one worker run. Empty means defaults to FORGEFLOW_PARALLEL.
|
|
118
|
+
FORGEFLOW_LIMIT=
|
|
119
|
+
|
|
120
|
+
# Podman image used by the Sandcastle sandbox provider. Build it with:
|
|
121
|
+
# bash ${dir}/build-image.sh
|
|
122
|
+
FORGEFLOW_SANDBOX_IMAGE=forgeflow-agent
|
|
123
|
+
FORGEFLOW_SANDBOX_DOCKERFILE=${dir}/Dockerfile
|
|
124
|
+
FORGEFLOW_SANDBOX_BUILD_CONTEXT=.
|
|
125
|
+
|
|
126
|
+
# Optional command executed inside the sandbox after creation, before the agent runs.
|
|
127
|
+
# Example: npm ci
|
|
128
|
+
FORGEFLOW_SANDBOX_PREFLIGHT=
|
|
129
|
+
|
|
130
|
+
# Optional output directory used by workflows.
|
|
131
|
+
FORGEFLOW_OUTPUT_DIR=/tmp/forgeflow-output
|
|
132
|
+
|
|
133
|
+
# Pi agent model settings.
|
|
134
|
+
PI_MODEL=openai-codex/gpt-5.5
|
|
135
|
+
PI_THINKING=low
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
function ensureGitignore(root, dir, written) {
|
|
139
|
+
const gitignorePath = join(root, ".gitignore");
|
|
140
|
+
const relDir = relative(root, dir) || dir;
|
|
141
|
+
const lines = [
|
|
142
|
+
`${relDir}/*.db`,
|
|
143
|
+
`${relDir}/*.db-shm`,
|
|
144
|
+
`${relDir}/*.db-wal`,
|
|
145
|
+
`${relDir}/*.lock`,
|
|
146
|
+
`${relDir}/.env`,
|
|
147
|
+
`!${relDir}/.env.example`,
|
|
148
|
+
];
|
|
149
|
+
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : "";
|
|
150
|
+
const missing = lines.filter(line => !existing.includes(line));
|
|
151
|
+
if (missing.length === 0)
|
|
152
|
+
return;
|
|
153
|
+
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
154
|
+
appendFileSync(gitignorePath, `${separator}${existing.length > 0 ? "\n" : ""}# Forgeflow\n${missing.join("\n")}\n`);
|
|
155
|
+
written.push(gitignorePath);
|
|
156
|
+
}
|
|
157
|
+
function mergePackageJsonScripts(root, dir, written, skipped) {
|
|
158
|
+
const pkgPath = join(root, "package.json");
|
|
159
|
+
if (!existsSync(pkgPath))
|
|
160
|
+
return;
|
|
161
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
162
|
+
pkg.scripts ??= {};
|
|
163
|
+
const desired = {
|
|
164
|
+
"forgeflow:once": `forgeflow run --config ${dir}/forgeflow.config.ts`,
|
|
165
|
+
"forgeflow:drain": `forgeflow drain --config ${dir}/forgeflow.config.ts`,
|
|
166
|
+
};
|
|
167
|
+
let changed = false;
|
|
168
|
+
for (const [key, value] of Object.entries(desired)) {
|
|
169
|
+
if (pkg.scripts[key]) {
|
|
170
|
+
skipped.push(`package.json#scripts.${key}`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
pkg.scripts[key] = value;
|
|
174
|
+
changed = true;
|
|
175
|
+
}
|
|
176
|
+
if (changed) {
|
|
177
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
178
|
+
written.push(pkgPath);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export function runInit(options) {
|
|
182
|
+
const root = process.cwd();
|
|
183
|
+
const dir = resolve(root, options.dir);
|
|
184
|
+
const baseBranch = options.baseBranch ?? detectBaseBranch();
|
|
185
|
+
const written = [];
|
|
186
|
+
const skipped = [];
|
|
187
|
+
writeFile(join(dir, "forgeflow.config.ts"), configTemplate({ provider: options.provider, repo: options.repo, baseBranch }, options.dir), options.force, written, skipped);
|
|
188
|
+
writeFile(join(dir, ".env.example"), envExampleTemplate({ provider: options.provider, repo: options.repo, baseBranch }, options.dir), options.force, written, skipped);
|
|
189
|
+
for (const promptFile of ["implement.md", "review/prompt.md", "review/extraction.md", "update-branch/prompt.md", "update-branch/extraction.md"]) {
|
|
190
|
+
copyFile(join(bundledPromptsDir, promptFile), join(dir, "prompts", promptFile), options.force, written, skipped);
|
|
191
|
+
}
|
|
192
|
+
copyFile(join(templatesDir, "Dockerfile"), join(dir, "Dockerfile"), options.force, written, skipped);
|
|
193
|
+
copyFile(join(templatesDir, "build-image.sh"), join(dir, "build-image.sh"), options.force, written, skipped);
|
|
194
|
+
ensureGitignore(root, dir, written);
|
|
195
|
+
mergePackageJsonScripts(root, options.dir, written, skipped);
|
|
196
|
+
return { written, skipped };
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Runs `fn` under an advisory PID-file lock at `lockPath`. Fails fast if already held. */
|
|
2
|
+
export declare function withLock<T>(lockPath: string, fn: () => Promise<T>): Promise<T>;
|
|
3
|
+
/** Same as withLock, but waits (polling) for the lock to free up instead of failing immediately. */
|
|
4
|
+
export declare function withLockBlocking<T>(lockPath: string, fn: () => Promise<T>, timeoutMs?: number): Promise<T>;
|
package/dist/cli/lock.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
function isProcessAlive(pid) {
|
|
4
|
+
try {
|
|
5
|
+
process.kill(pid, 0);
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function tryAcquire(lockPath) {
|
|
13
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
14
|
+
if (existsSync(lockPath)) {
|
|
15
|
+
const pid = Number(readFileSync(lockPath, "utf8").trim());
|
|
16
|
+
if (pid && isProcessAlive(pid))
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
writeFileSync(lockPath, String(process.pid));
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
/** Runs `fn` under an advisory PID-file lock at `lockPath`. Fails fast if already held. */
|
|
23
|
+
export async function withLock(lockPath, fn) {
|
|
24
|
+
if (!tryAcquire(lockPath)) {
|
|
25
|
+
throw new Error(`Another forgeflow run holds the lock at ${lockPath}. Skipping.`);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return await fn();
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
try {
|
|
32
|
+
rmSync(lockPath);
|
|
33
|
+
}
|
|
34
|
+
catch { /* already gone */ }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Same as withLock, but waits (polling) for the lock to free up instead of failing immediately. */
|
|
38
|
+
export async function withLockBlocking(lockPath, fn, timeoutMs = 10 * 60_000) {
|
|
39
|
+
const deadline = Date.now() + timeoutMs;
|
|
40
|
+
while (!tryAcquire(lockPath)) {
|
|
41
|
+
if (Date.now() > deadline)
|
|
42
|
+
throw new Error(`Timed out waiting for the lock at ${lockPath}`);
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return await fn();
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
try {
|
|
50
|
+
rmSync(lockPath);
|
|
51
|
+
}
|
|
52
|
+
catch { /* already gone */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/cli/lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACpF,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,SAAS,cAAc,CAAC,GAAW;IACjC,IAAI,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QAAC,OAAO,IAAI,CAAA;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,KAAK,CAAA;IAAC,CAAC;AAClE,CAAC;AAED,SAAS,UAAU,CAAC,QAAgB;IAClC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACjD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;QACzD,IAAI,GAAG,IAAI,cAAc,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAA;IAC9C,CAAC;IACD,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAA;IAC5C,OAAO,IAAI,CAAA;AACb,CAAC;AAED,2FAA2F;AAC3F,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAI,QAAgB,EAAE,EAAoB;IACtE,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,2CAA2C,QAAQ,aAAa,CAAC,CAAA;IACnF,CAAC;IACD,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,EAAE,CAAA;IACnB,CAAC;YAAS,CAAC;QACT,IAAI,CAAC;YAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,kBAAkB,CAAC,CAAC;IACvD,CAAC;AACH,CAAC;AAED,oGAAoG;AACpG,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAI,QAAgB,EAAE,EAAoB,EAAE,SAAS,GAAG,EAAE,GAAG,MAAM;IACvG,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAA;IACvC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,QAAQ,EAAE,CAAC,CAAA;QAC3F,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAA;IACxD,CAAC;IACD,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,EAAE,CAAA;IACnB,CAAC;YAAS,CAAC;QACT,IAAI,CAAC;YAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,kBAAkB,CAAC,CAAC;IACvD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
FROM node:22-bookworm
|
|
2
|
+
|
|
3
|
+
# Install system dependencies
|
|
4
|
+
RUN apt-get update && apt-get install -y \
|
|
5
|
+
git \
|
|
6
|
+
curl \
|
|
7
|
+
jq \
|
|
8
|
+
unzip \
|
|
9
|
+
ca-certificates \
|
|
10
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
11
|
+
|
|
12
|
+
# Install GitHub CLI
|
|
13
|
+
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
|
14
|
+
| dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
|
15
|
+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
|
16
|
+
| tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
|
17
|
+
&& apt-get update && apt-get install -y gh \
|
|
18
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
19
|
+
|
|
20
|
+
# Build-args for UID/GID alignment.
|
|
21
|
+
ARG AGENT_UID=1000
|
|
22
|
+
ARG AGENT_GID=1000
|
|
23
|
+
|
|
24
|
+
RUN groupmod -o -g $AGENT_GID node \
|
|
25
|
+
&& usermod -o -u $AGENT_UID -g $AGENT_GID -d /home/agent -m -l agent node
|
|
26
|
+
|
|
27
|
+
USER ${AGENT_UID}:${AGENT_GID}
|
|
28
|
+
WORKDIR /home/agent
|
|
29
|
+
|
|
30
|
+
ENV NPM_CONFIG_PREFIX=/home/agent/.npm-global
|
|
31
|
+
ENV PATH=/home/agent/.local/bin:/home/agent/.npm-global/bin:$PATH
|
|
32
|
+
|
|
33
|
+
# Install Pi Agent. Runtime auth is mounted from the host at ~/.pi/agent.
|
|
34
|
+
RUN npm install -g @earendil-works/pi-coding-agent
|
|
35
|
+
RUN mkdir -p /home/agent/.pi/agent \
|
|
36
|
+
&& echo '{"defaultProvider":"openai-codex","defaultModel":"gpt-5.5","theme":"dark"}' > /home/agent/.pi/agent/settings.json
|
|
37
|
+
|
|
38
|
+
ENTRYPOINT ["sleep", "infinity"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
image_name="${FORGEFLOW_SANDBOX_IMAGE:-forgeflow-agent}"
|
|
5
|
+
context_dir="${FORGEFLOW_SANDBOX_BUILD_CONTEXT:-.}"
|
|
6
|
+
dockerfile="${FORGEFLOW_SANDBOX_DOCKERFILE:-.forgeflow/Dockerfile}"
|
|
7
|
+
|
|
8
|
+
podman build \
|
|
9
|
+
-t "$image_name" \
|
|
10
|
+
-f "$dockerfile" \
|
|
11
|
+
"$context_dir"
|