7cce0ad230
notepad-write-guard: - The hook was created by create-tool-guard-hooks but tool-execute-before never invoked it, so the guard was inert. - It also only matched .sisyphus/notepads, missing the current .omo/notepads layout introduced by the workspace migration. - Add the dispatch call alongside writeExistingFileGuard, and extend NOTEPAD_ROOTS to cover both paths via normalize() + sep. New integration test pins the wire and the .omo block; the existing unit test now asserts both paths. start-work session-plan-affinity: - PLAN_PATH_PATTERN only matched .sisyphus/plans, so sessions referring to plans under .omo/plans returned null and start-work missed the current session's own plan. - Extend the regex to .(sisyphus|omo)/plans and add findPrometheusPlans in packages/boulder-state to scan both directories during the transition. New regression test pins .omo/plans matching; legacy .sisyphus/plans coverage preserved.
109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
import { isAbsolute, resolve } from "node:path"
|
|
import type { PluginInput } from "@opencode-ai/plugin"
|
|
import { normalizeSDKResponse } from "../../shared"
|
|
import { log } from "../../shared/logger"
|
|
|
|
const PLAN_PATH_PATTERN = /[A-Za-z0-9_./\\:-]*\.(?:sisyphus|omo)[\\/]plans[\\/][A-Za-z0-9._/\\-]+\.md/gi
|
|
|
|
interface SessionMessagePart {
|
|
text?: string
|
|
output?: string
|
|
input?: Record<string, unknown>
|
|
}
|
|
|
|
interface SessionMessage {
|
|
parts?: SessionMessagePart[]
|
|
}
|
|
|
|
function normalizePlanPath(directory: string, candidate: string): string {
|
|
const trimmedCandidate = candidate.trim().replace(/^["'`]+|["'`]+$/g, "")
|
|
if (isAbsolute(trimmedCandidate) || /^[A-Za-z]:[\\/]/.test(trimmedCandidate)) {
|
|
return resolve(trimmedCandidate)
|
|
}
|
|
|
|
return resolve(directory, trimmedCandidate)
|
|
}
|
|
|
|
function extractPlanPathsFromText(directory: string, text: string): string[] {
|
|
const matches = text.match(PLAN_PATH_PATTERN) ?? []
|
|
return matches.map((match) => normalizePlanPath(directory, match))
|
|
}
|
|
|
|
function extractPlanPathsFromValue(directory: string, value: unknown): string[] {
|
|
if (typeof value === "string") {
|
|
return extractPlanPathsFromText(directory, value)
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
return value.flatMap((item) => extractPlanPathsFromValue(directory, item))
|
|
}
|
|
|
|
if (value && typeof value === "object") {
|
|
return Object.values(value).flatMap((item) => extractPlanPathsFromValue(directory, item))
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
function extractPlanPathsFromInput(directory: string, input: Record<string, unknown> | undefined): string[] {
|
|
if (!input) {
|
|
return []
|
|
}
|
|
|
|
const nestedCandidates = Object.entries(input)
|
|
.filter(([key]) => key !== "filePath" && key !== "path" && key !== "file")
|
|
.flatMap(([, value]) => extractPlanPathsFromValue(directory, value))
|
|
|
|
const directCandidates = [input.filePath, input.path, input.file]
|
|
.filter((value): value is string => typeof value === "string")
|
|
.flatMap((value) => extractPlanPathsFromText(directory, value))
|
|
|
|
return [...new Set([...nestedCandidates, ...directCandidates])]
|
|
}
|
|
|
|
export async function findRecentSessionPlanPath(input: {
|
|
client: PluginInput["client"]
|
|
directory: string
|
|
sessionID: string
|
|
availablePlans: string[]
|
|
}): Promise<string | null> {
|
|
if (typeof input.client.session?.messages !== "function") {
|
|
return null
|
|
}
|
|
|
|
const availablePlans = new Set(input.availablePlans.map((planPath) => resolve(planPath)))
|
|
if (availablePlans.size === 0) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const response = await input.client.session.messages({ path: { id: input.sessionID } })
|
|
const messages = normalizeSDKResponse(response, [] as SessionMessage[])
|
|
|
|
for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
|
|
const parts = messages[messageIndex]?.parts ?? []
|
|
|
|
for (let partIndex = parts.length - 1; partIndex >= 0; partIndex -= 1) {
|
|
const part = parts[partIndex]
|
|
const planCandidates = [
|
|
...extractPlanPathsFromText(input.directory, part.text ?? ""),
|
|
...extractPlanPathsFromText(input.directory, part.output ?? ""),
|
|
...extractPlanPathsFromInput(input.directory, part.input),
|
|
]
|
|
|
|
const matchedPlan = planCandidates.find((planPath) => availablePlans.has(resolve(planPath)))
|
|
if (matchedPlan) {
|
|
return resolve(matchedPlan)
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
log("[start-work] Failed to inspect session history for preferred plan", {
|
|
sessionID: input.sessionID,
|
|
error: String(error),
|
|
})
|
|
}
|
|
|
|
return null
|
|
}
|