Repository navigation
fix: 只有解释器产物的提交不算成功; 保留 agent 日志; run --tasks #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { Command } from 'commander'; | |
| import { | ||
| analyzeCodebase, | ||
| analyzeCodebaseNative, | ||
| ignoreStateDir, | ||
| UnifiedScheduler, | ||
| TaskSolverManager, | ||
| DistributedScheduler, | ||
|
|
@@ -75,11 +76,25 @@ export async function runFullAnalysis(): Promise<void> { | |
| } | ||
|
|
||
| // Step 1: analyze the codebase and get tasks | ||
| console.log('Analyzing the codebase...'); | ||
| const tasks: Task[] = | ||
| mode === 'docker' | ||
| ? await analyzeCodebase(config, gitRemoteUrl) | ||
| : await analyzeCodebaseNative(config, process.cwd()); | ||
| if (mode !== 'docker') ignoreStateDir(process.cwd()); | ||
| const tasksFile = process.env.FSC_TASKS_FILE; | ||
| let tasks: Task[]; | ||
| if (tasksFile) { | ||
| // Skip analysis: run exactly the tasks in this JSON file (an array of Task objects). | ||
| try { | ||
| tasks = JSON.parse(fs.readFileSync(tasksFile, 'utf-8')); | ||
| } catch (e) { | ||
| throw new Error(`Cannot read tasks from ${tasksFile}: ${e instanceof Error ? e.message : String(e)}`); | ||
| } | ||
| if (!Array.isArray(tasks) || tasks.length === 0) throw new Error(`${tasksFile} must contain a non-empty JSON array of tasks.`); | ||
| tasks = tasks.map((t, i) => ({ ...t, priority: t.priority ?? 1, ID: t.ID ?? `task-${i + 1}` })); | ||
|
Comment on lines
+89
to
+90
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate each task before execution. A file containing
|
||
| } else { | ||
| console.log('Analyzing the codebase...'); | ||
| tasks = | ||
| mode === 'docker' | ||
| ? await analyzeCodebase(config, gitRemoteUrl) | ||
| : await analyzeCodebaseNative(config, process.cwd()); | ||
| } | ||
| console.log(`Found ${tasks.length} task(s):`); | ||
| for (const t of tasks) console.log(` - [P${t.priority}] ${t.title}`); | ||
|
|
||
|
|
@@ -179,13 +194,17 @@ export function createCLI(): Command { | |
| .command('run') | ||
| .description('Run the full self-coding analysis and task execution') | ||
| .option('-c, --config <path>', 'Path to configuration file') | ||
| .option('--tasks <file>', 'Run the tasks in this JSON file instead of analyzing the codebase') | ||
| .option('--mode <mode>', 'Execution mode: worktree (native git worktree), bare, or docker (optional)', 'worktree') | ||
| .action(async (options) => { | ||
| try { | ||
| // If config option is provided, add it to process.argv for the main function to pick up | ||
| if (options.config) { | ||
| process.argv.push('--config', options.config); | ||
| } | ||
| if (options.tasks) { | ||
| process.env.FSC_TASKS_FILE = options.tasks; | ||
| } | ||
| if (options.mode) { | ||
| process.env.FSC_EXECUTION_MODE = options.mode; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,14 @@ import type { SalaciaPlugin } from "../salacia/plugin"; | |
|
|
||
| const FSC_AUTHOR = "FSC Worker <fsc@full-self-coding>"; | ||
|
|
||
| /** Never auto-committed: FSC state and build/interpreter byproducts that agents leave behind. */ | ||
| const AUTO_COMMIT_EXCLUDES = [ | ||
| ":(exclude).fsc", | ||
| ":(exclude)**/__pycache__/**", | ||
| ":(exclude)**/*.pyc", | ||
| ":(exclude)**/node_modules/**", | ||
| ]; | ||
|
|
||
| export class LocalExecution implements ExecutionStrategy { | ||
| private worktreeManager: WorktreeManager; | ||
| private running: Map<string, Subprocess> = new Map(); | ||
|
|
@@ -155,6 +163,28 @@ export class LocalExecution implements ExecutionStrategy { | |
| } : undefined, | ||
| }; | ||
|
|
||
| // Keep the full agent output: the worktree (and its .fsc/) is removed after the run. | ||
| try { | ||
|
Comment on lines
+166
to
+167
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Save the full log on Salacia rejection. If Salacia blocks a task,
|
||
| const logDir = path.resolve(unit.repoPath, ".fsc", "logs"); | ||
| fs.mkdirSync(logDir, { recursive: true }); | ||
|
Comment on lines
+168
to
+169
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the exported Useful? React with 👍 / 👎. |
||
| const header = `# exit=${exitCode} timedOut=${timedOut} spawnError=${spawnError ?? ""}`; | ||
| const shown = cmd.map(c => (c.length > 80 ? `${c.slice(0, 80)}...` : c)).join(" "); | ||
| const body = [ | ||
| header, | ||
| `# cmd: ${shown}`, | ||
| "", | ||
| "## stdout", | ||
| stdoutChunks.join("\n"), | ||
| "", | ||
| "## stderr", | ||
| stderrChunks.join("\n"), | ||
| "", | ||
| ].join("\n"); | ||
| fs.writeFileSync(path.resolve(logDir, `${unit.id}.log`), body); | ||
| } catch { | ||
| // logging must never fail a run | ||
| } | ||
|
|
||
| // 8. Write .fsc/result.json | ||
| fs.writeFileSync( | ||
| path.resolve(fscDir, "result.json"), | ||
|
|
@@ -191,10 +221,10 @@ export class LocalExecution implements ExecutionStrategy { | |
| baseBranch: string, | ||
| ): string | null { | ||
| // Check for uncommitted changes (exclude .fsc/ which is FSC internal) | ||
| const status = gitExec(["status", "--porcelain", "--", ":(exclude).fsc"], worktreePath); | ||
| const status = gitExec(["status", "--porcelain", "--", ".", ...AUTO_COMMIT_EXCLUDES], worktreePath); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject artifact-only commits made by the agent. These exclusions do not apply to commits the agent has already made. If the agent commits only a
|
||
| if (status.stdout.trim()) { | ||
| // Auto-commit (add everything except .fsc/) | ||
| gitExec(["add", "-A", "--", ":(exclude).fsc"], worktreePath); | ||
| gitExec(["add", "-A", "--", ".", ...AUTO_COMMIT_EXCLUDES], worktreePath); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep already-staged artifacts out of automatic commits. If an agent stages a
|
||
| const commitResult = gitExec( | ||
| ["commit", "--author", FSC_AUTHOR, "-m", `auto: ${taskId} completion`], | ||
| worktreePath, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -86,6 +86,35 @@ describe("LocalExecution", () => { | |||||||||||||||||||||||||||||||||||||||||||
| expect(result.filesChanged).toContain("auto_commit_test.txt"); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| test("interpreter byproducts alone are not a result", async () => { | ||||||||||||||||||||||||||||||||||||||||||||
| const shell = process.platform === "win32" ? "bash" : "/bin/sh"; | ||||||||||||||||||||||||||||||||||||||||||||
| const unit: WorkUnit = { | ||||||||||||||||||||||||||||||||||||||||||||
| id: "local-test-pyc-only", | ||||||||||||||||||||||||||||||||||||||||||||
| repoPath: testRepo, | ||||||||||||||||||||||||||||||||||||||||||||
| baseBranch: "main", | ||||||||||||||||||||||||||||||||||||||||||||
| taskPrompt: "test", | ||||||||||||||||||||||||||||||||||||||||||||
| agentConfig: { | ||||||||||||||||||||||||||||||||||||||||||||
| type: "custom", | ||||||||||||||||||||||||||||||||||||||||||||
| costTier: "free", | ||||||||||||||||||||||||||||||||||||||||||||
| memoryMB: 50, | ||||||||||||||||||||||||||||||||||||||||||||
| command: shell, | ||||||||||||||||||||||||||||||||||||||||||||
| buildArgs: () => ["-c", "mkdir -p __pycache__ && echo x > __pycache__/a.cpython-312.pyc"], | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| constraints: { timeoutMs: 10_000 }, | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const result = await execution.execute(unit); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(result.commitHash).toBeNull(); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(result.status).toBe("failure"); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(result.error?.message).toContain("no changes"); | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+101
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔎 Supported by static analysis
Isolate the bytecode-only test from global Git ignore rules.
Suggested fix
Committable suggestion
|
||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| test("agent output is kept in the repo's .fsc/logs", async () => { | ||||||||||||||||||||||||||||||||||||||||||||
| const log = path.resolve(testRepo, ".fsc", "logs", "local-test-pyc-only.log"); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(fs.existsSync(log)).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(fs.readFileSync(log, "utf-8")).toContain("# exit=0"); | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+112
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make the log test independent of the preceding test. This test never calls 🧰 Tools
|
||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| test("handles timeout", async () => { | ||||||||||||||||||||||||||||||||||||||||||||
| const shell = process.platform === "win32" ? "bash" : "/bin/sh"; | ||||||||||||||||||||||||||||||||||||||||||||
| const unit: WorkUnit = { | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a tasks file mixes explicit and omitted IDs, the generated IDs can collide with valid supplied ones: an explicit
task-2on the first entry and no ID on the second entry both becometask-2.TaskStateStore.create()overwrites the first task's state, and both executions target the same branch and worktree. With concurrent execution,WorktreeManager.create()can even force-remove the first task's active worktree while its branch has no commits ahead of the base. Reserve supplied IDs before generating defaults and reject duplicate IDs before scheduling.Useful? React with 👍 / 👎.