merge: integrate origin/dev into modular-enforcement branch

Resolves all merge conflicts, preserving our split module structure
while integrating all dev changes:
- Custom agent summaries support (parseRegisteredAgentSummaries)
- Background notification queue (enqueueNotificationForParent)
- Atlas shared git-worktree module (collectGitDiffStats, formatFileChanges)
- Ralph-loop withTimeout + DEFAULT_API_TIMEOUT=5000
- Session recovery assistant_prefill_unsupported error type
- Atlas agentOverrides forwarding
- Config handler plan model demotion (buildPlanDemoteConfig)
- Delegate-task agentOverrides, promptSyncWithModelSuggestionRetry, variant
- LSP init timeout + stale init detection
- isPlanFamily function + task-continuation-enforcer hook
- Handoff command
This commit is contained in:
YeonGyu-Kim
2026-02-08 17:34:47 +09:00
74 changed files with 4016 additions and 654 deletions
-133
View File
@@ -1,133 +0,0 @@
import { execSync } from "node:child_process"
interface GitFileStat {
path: string
added: number
removed: number
status: "modified" | "added" | "deleted"
}
export function getGitDiffStats(directory: string): GitFileStat[] {
try {
const statusOutput = execSync("git status --porcelain", {
cwd: directory,
encoding: "utf-8",
timeout: 5000,
stdio: ["pipe", "pipe", "pipe"],
}).trim()
if (!statusOutput) return []
const statusMap = new Map<string, "modified" | "added" | "deleted">()
const untrackedFiles: string[] = []
for (const line of statusOutput.split("\n")) {
if (!line) continue
const status = line.substring(0, 2).trim()
const filePath = line.substring(3)
if (status === "??") {
statusMap.set(filePath, "added")
untrackedFiles.push(filePath)
} else if (status === "A") {
statusMap.set(filePath, "added")
} else if (status === "D") {
statusMap.set(filePath, "deleted")
} else {
statusMap.set(filePath, "modified")
}
}
const output = execSync("git diff --numstat HEAD", {
cwd: directory,
encoding: "utf-8",
timeout: 5000,
stdio: ["pipe", "pipe", "pipe"],
}).trim()
const stats: GitFileStat[] = []
const trackedPaths = new Set<string>()
if (output) {
for (const line of output.split("\n")) {
const parts = line.split("\t")
if (parts.length < 3) continue
const [addedStr, removedStr, path] = parts
const added = addedStr === "-" ? 0 : parseInt(addedStr, 10)
const removed = removedStr === "-" ? 0 : parseInt(removedStr, 10)
trackedPaths.add(path)
stats.push({
path,
added,
removed,
status: statusMap.get(path) ?? "modified",
})
}
}
for (const filePath of untrackedFiles) {
if (trackedPaths.has(filePath)) continue
try {
const content = execSync(`wc -l < "${filePath}"`, {
cwd: directory,
encoding: "utf-8",
timeout: 3000,
stdio: ["pipe", "pipe", "pipe"],
}).trim()
const lineCount = parseInt(content, 10) || 0
stats.push({ path: filePath, added: lineCount, removed: 0, status: "added" })
} catch {
stats.push({ path: filePath, added: 0, removed: 0, status: "added" })
}
}
return stats
} catch {
return []
}
}
export function formatFileChanges(stats: GitFileStat[], notepadPath?: string): string {
if (stats.length === 0) return "[FILE CHANGES SUMMARY]\nNo file changes detected.\n"
const modified = stats.filter((s) => s.status === "modified")
const added = stats.filter((s) => s.status === "added")
const deleted = stats.filter((s) => s.status === "deleted")
const lines: string[] = ["[FILE CHANGES SUMMARY]"]
if (modified.length > 0) {
lines.push("Modified files:")
for (const f of modified) {
lines.push(` ${f.path} (+${f.added}, -${f.removed})`)
}
lines.push("")
}
if (added.length > 0) {
lines.push("Created files:")
for (const f of added) {
lines.push(` ${f.path} (+${f.added})`)
}
lines.push("")
}
if (deleted.length > 0) {
lines.push("Deleted files:")
for (const f of deleted) {
lines.push(` ${f.path} (-${f.removed})`)
}
lines.push("")
}
if (notepadPath) {
const notepadStat = stats.find((s) => s.path.includes("notepad") || s.path.includes(".sisyphus"))
if (notepadStat) {
lines.push("[NOTEPAD UPDATED]")
lines.push(` ${notepadStat.path} (+${notepadStat.added})`)
lines.push("")
}
}
return lines.join("\n")
}
+2 -2
View File
@@ -2,9 +2,9 @@ import type { PluginInput } from "@opencode-ai/plugin"
import { appendSessionId, getPlanProgress, readBoulderState } from "../../features/boulder-state"
import { log } from "../../shared/logger"
import { isCallerOrchestrator } from "../../shared/session-utils"
import { collectGitDiffStats, formatFileChanges } from "../../shared/git-worktree"
import { HOOK_NAME } from "./hook-name"
import { DIRECT_WORK_REMINDER } from "./system-reminder-templates"
import { formatFileChanges, getGitDiffStats } from "./git-diff-stats"
import { isSisyphusPath } from "./sisyphus-path"
import { extractSessionIdFromOutput } from "./subagent-session-id"
import { buildOrchestratorReminder, buildStandaloneVerificationReminder } from "./verification-reminders"
@@ -57,7 +57,7 @@ export function createToolExecuteAfterHandler(input: {
}
if (toolOutput.output && typeof toolOutput.output === "string") {
const gitStats = getGitDiffStats(ctx.directory)
const gitStats = collectGitDiffStats(ctx.directory)
const fileChanges = formatFileChanges(gitStats)
const subagentSessionId = extractSessionIdFromOutput(toolOutput.output)
+2
View File
@@ -1,3 +1,4 @@
import type { AgentOverrides } from "../../config"
import type { BackgroundManager } from "../../features/background-agent"
export type ModelInfo = { providerID: string; modelID: string }
@@ -6,6 +7,7 @@ export interface AtlasHookOptions {
directory: string
backgroundManager?: BackgroundManager
isContinuationStopped?: (sessionID: string) => boolean
agentOverrides?: AgentOverrides
}
export interface ToolExecuteAfterInput {