feat(boulder-state): add platform-prefixed session ids
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -20,6 +20,7 @@ export {
|
|||||||
getWorkByPlanName,
|
getWorkByPlanName,
|
||||||
getWorkForSession,
|
getWorkForSession,
|
||||||
getWorkResumeOptions,
|
getWorkResumeOptions,
|
||||||
|
normalizeSessionId,
|
||||||
readBoulderState,
|
readBoulderState,
|
||||||
resolveBoulderPlanPath,
|
resolveBoulderPlanPath,
|
||||||
resolveBoulderPlanPathForWork,
|
resolveBoulderPlanPathForWork,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export { getBoulderFilePath, resolveBoulderPlanPath, resolveBoulderPlanPathForWork } from "./path"
|
export { getBoulderFilePath, resolveBoulderPlanPath, resolveBoulderPlanPathForWork } from "./path"
|
||||||
export { findPrometheusPlans, getPlanName, getPlanProgress } from "./plan-progress"
|
export { findPrometheusPlans, getPlanName, getPlanProgress } from "./plan-progress"
|
||||||
|
export { normalizeSessionId } from "./shared"
|
||||||
export {
|
export {
|
||||||
getActiveWorks,
|
getActiveWorks,
|
||||||
getBoulderWorks,
|
getBoulderWorks,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from "node:fs"
|
|||||||
import type { BoulderState, BoulderWorkResumeOption, BoulderWorkState, TaskSessionState } from "../types"
|
import type { BoulderState, BoulderWorkResumeOption, BoulderWorkState, TaskSessionState } from "../types"
|
||||||
import { getBoulderFilePath, resolveBoulderPlanPathForWork } from "./path"
|
import { getBoulderFilePath, resolveBoulderPlanPathForWork } from "./path"
|
||||||
import { getPlanProgress } from "./plan-progress"
|
import { getPlanProgress } from "./plan-progress"
|
||||||
import { buildWorkFromMirror, isValidWorkStatus, parseIsoToMs, projectWorkToMirror, selectMirrorWork } from "./shared"
|
import { buildWorkFromMirror, isValidWorkStatus, normalizeSessionId, parseIsoToMs, projectWorkToMirror, selectMirrorWork } from "./shared"
|
||||||
|
|
||||||
export function readBoulderState(directory: string): BoulderState | null {
|
export function readBoulderState(directory: string): BoulderState | null {
|
||||||
const filePath = getBoulderFilePath(directory)
|
const filePath = getBoulderFilePath(directory)
|
||||||
@@ -33,8 +33,9 @@ export function readBoulderState(directory: string): BoulderState | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeState(state: Record<string, unknown>): void {
|
function normalizeState(state: Record<string, unknown>): void {
|
||||||
|
normalizeSessionFields(state)
|
||||||
|
|
||||||
const sessionIds = Array.isArray(state.session_ids) ? state.session_ids : []
|
const sessionIds = Array.isArray(state.session_ids) ? state.session_ids : []
|
||||||
state.session_ids = sessionIds
|
|
||||||
|
|
||||||
const sessionOrigins = state.session_origins && typeof state.session_origins === "object" && !Array.isArray(state.session_origins)
|
const sessionOrigins = state.session_origins && typeof state.session_origins === "object" && !Array.isArray(state.session_origins)
|
||||||
? (state.session_origins as Record<string, unknown>)
|
? (state.session_origins as Record<string, unknown>)
|
||||||
@@ -55,6 +56,38 @@ function normalizeState(state: Record<string, unknown>): void {
|
|||||||
if (!state.task_sessions || typeof state.task_sessions !== "object" || Array.isArray(state.task_sessions)) {
|
if (!state.task_sessions || typeof state.task_sessions !== "object" || Array.isArray(state.task_sessions)) {
|
||||||
state.task_sessions = {}
|
state.task_sessions = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalizeWorkSessionFields(state.works)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSessionFields(target: Record<string, unknown>): void {
|
||||||
|
const sessionIds = Array.isArray(target.session_ids)
|
||||||
|
? target.session_ids.filter((sessionId): sessionId is string => typeof sessionId === "string").map((sessionId) => normalizeSessionId(sessionId))
|
||||||
|
: []
|
||||||
|
target.session_ids = sessionIds
|
||||||
|
|
||||||
|
const sessionOrigins = target.session_origins && typeof target.session_origins === "object" && !Array.isArray(target.session_origins)
|
||||||
|
? normalizeSessionOrigins(target.session_origins as Record<string, unknown>)
|
||||||
|
: {}
|
||||||
|
target.session_origins = sessionOrigins
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSessionOrigins(sessionOrigins: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(sessionOrigins).map(([sessionId, origin]) => [normalizeSessionId(sessionId), origin]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWorkSessionFields(works: unknown): void {
|
||||||
|
if (!works || typeof works !== "object" || Array.isArray(works)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const work of Object.values(works)) {
|
||||||
|
if (work && typeof work === "object" && !Array.isArray(work)) {
|
||||||
|
normalizeSessionFields(work as Record<string, unknown>)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBoulderWorks(state: BoulderState): BoulderWorkState[] {
|
export function getBoulderWorks(state: BoulderState): BoulderWorkState[] {
|
||||||
@@ -113,15 +146,16 @@ export function getWorkForSession(directory: string, sessionId: string): Boulder
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedSessionId = normalizeSessionId(sessionId)
|
||||||
const works = getBoulderWorks(state)
|
const works = getBoulderWorks(state)
|
||||||
.filter((work) => work.session_ids.includes(sessionId))
|
.filter((work) => work.session_ids.includes(normalizedSessionId))
|
||||||
.sort((left, right) => (parseIsoToMs(right.updated_at ?? right.started_at) ?? 0) - (parseIsoToMs(left.updated_at ?? left.started_at) ?? 0))
|
.sort((left, right) => (parseIsoToMs(right.updated_at ?? right.started_at) ?? 0) - (parseIsoToMs(left.updated_at ?? left.started_at) ?? 0))
|
||||||
|
|
||||||
if (works.length > 0) {
|
if (works.length > 0) {
|
||||||
return works[0] ?? null
|
return works[0] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.session_ids.includes(sessionId) ? buildWorkFromMirror(state) : null
|
return state.session_ids.includes(normalizedSessionId) ? buildWorkFromMirror(state) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkResumeOptions(directory: string): BoulderWorkResumeOption[] {
|
export function getWorkResumeOptions(directory: string): BoulderWorkResumeOption[] {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { BoulderSessionOrigin, BoulderState, BoulderWorkState } from "../types"
|
import type { BoulderSessionOrigin, BoulderState, BoulderWorkState } from "../types"
|
||||||
import { getBoulderWorks, readBoulderState } from "./read-state"
|
import { getBoulderWorks, readBoulderState } from "./read-state"
|
||||||
import { nowIsoString, projectWorkToMirror } from "./shared"
|
import { normalizeSessionId, nowIsoString, projectWorkToMirror } from "./shared"
|
||||||
import { writeBoulderState } from "./write-state"
|
import { writeBoulderState } from "./write-state"
|
||||||
|
|
||||||
export function appendSessionId(
|
export function appendSessionId(
|
||||||
@@ -8,9 +8,10 @@ export function appendSessionId(
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
origin: "direct" | "appended" = "direct",
|
origin: "direct" | "appended" = "direct",
|
||||||
): BoulderState | null {
|
): BoulderState | null {
|
||||||
|
const normalizedSessionId = normalizeSessionId(sessionId)
|
||||||
const activeWorkId = readBoulderState(directory)?.active_work_id
|
const activeWorkId = readBoulderState(directory)?.active_work_id
|
||||||
if (activeWorkId) {
|
if (activeWorkId) {
|
||||||
return appendSessionIdForWork(directory, activeWorkId, sessionId, origin)
|
return appendSessionIdForWork(directory, activeWorkId, normalizedSessionId, origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = readBoulderState(directory)
|
const state = readBoulderState(directory)
|
||||||
@@ -22,15 +23,15 @@ export function appendSessionId(
|
|||||||
state.session_origins = {}
|
state.session_origins = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.session_ids?.includes(sessionId)) {
|
if (!state.session_ids?.includes(normalizedSessionId)) {
|
||||||
if (!Array.isArray(state.session_ids)) {
|
if (!Array.isArray(state.session_ids)) {
|
||||||
state.session_ids = []
|
state.session_ids = []
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalSessionIds = [...state.session_ids]
|
const originalSessionIds = [...state.session_ids]
|
||||||
const originalSessionOrigins = { ...state.session_origins }
|
const originalSessionOrigins = { ...state.session_origins }
|
||||||
state.session_ids.push(sessionId)
|
state.session_ids.push(normalizedSessionId)
|
||||||
state.session_origins[sessionId] = origin
|
state.session_origins[normalizedSessionId] = origin
|
||||||
if (writeBoulderState(directory, state)) {
|
if (writeBoulderState(directory, state)) {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
@@ -40,8 +41,8 @@ export function appendSessionId(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.session_origins[sessionId]) {
|
if (!state.session_origins[normalizedSessionId]) {
|
||||||
state.session_origins[sessionId] = origin
|
state.session_origins[normalizedSessionId] = origin
|
||||||
if (!writeBoulderState(directory, state)) {
|
if (!writeBoulderState(directory, state)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -56,6 +57,7 @@ export function appendSessionIdForWork(
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
origin: BoulderSessionOrigin = "direct",
|
origin: BoulderSessionOrigin = "direct",
|
||||||
): BoulderState | null {
|
): BoulderState | null {
|
||||||
|
const normalizedSessionId = normalizeSessionId(sessionId)
|
||||||
const state = readBoulderState(directory)
|
const state = readBoulderState(directory)
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return null
|
return null
|
||||||
@@ -69,10 +71,10 @@ export function appendSessionIdForWork(
|
|||||||
|
|
||||||
const updatedWork: BoulderWorkState = {
|
const updatedWork: BoulderWorkState = {
|
||||||
...targetWork,
|
...targetWork,
|
||||||
session_ids: targetWork.session_ids.includes(sessionId)
|
session_ids: targetWork.session_ids.includes(normalizedSessionId)
|
||||||
? [...targetWork.session_ids]
|
? [...targetWork.session_ids]
|
||||||
: [...targetWork.session_ids, sessionId],
|
: [...targetWork.session_ids, normalizedSessionId],
|
||||||
session_origins: { ...(targetWork.session_origins ?? {}), [sessionId]: origin },
|
session_origins: { ...(targetWork.session_origins ?? {}), [normalizedSessionId]: origin },
|
||||||
updated_at: nowIsoString(),
|
updated_at: nowIsoString(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ import type { BoulderState, BoulderWorkState, BoulderWorkStatus } from "../types
|
|||||||
|
|
||||||
export const RESERVED_KEYS = new Set(["__proto__", "prototype", "constructor"])
|
export const RESERVED_KEYS = new Set(["__proto__", "prototype", "constructor"])
|
||||||
|
|
||||||
|
type SessionPlatform = "codex" | "opencode"
|
||||||
|
|
||||||
|
const SESSION_ID_PREFIX_PATTERN = /^(codex|opencode):/
|
||||||
|
|
||||||
|
export function normalizeSessionId(sessionId: string, platform: SessionPlatform = "opencode"): string {
|
||||||
|
if (SESSION_ID_PREFIX_PATTERN.test(sessionId)) {
|
||||||
|
return sessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${platform}:${sessionId}`
|
||||||
|
}
|
||||||
|
|
||||||
export function nowIsoString(): string {
|
export function nowIsoString(): string {
|
||||||
return new Date().toISOString()
|
return new Date().toISOString()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { BoulderState, BoulderWorkState, TaskSessionState } from "../types"
|
import type { BoulderState, BoulderWorkState, TaskSessionState } from "../types"
|
||||||
import { getBoulderWorks, readBoulderState } from "./read-state"
|
import { getBoulderWorks, readBoulderState } from "./read-state"
|
||||||
import { getElapsedMs, nowIsoString, projectWorkToMirror, RESERVED_KEYS } from "./shared"
|
import { getElapsedMs, normalizeSessionId, nowIsoString, projectWorkToMirror, RESERVED_KEYS } from "./shared"
|
||||||
import { writeBoulderState } from "./write-state"
|
import { writeBoulderState } from "./write-state"
|
||||||
|
|
||||||
export function upsertTaskSessionState(
|
export function upsertTaskSessionState(
|
||||||
@@ -24,12 +24,13 @@ export function upsertTaskSessionState(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedSessionId = normalizeSessionId(input.sessionId)
|
||||||
const taskSessions = state.task_sessions ?? {}
|
const taskSessions = state.task_sessions ?? {}
|
||||||
taskSessions[input.taskKey] = {
|
taskSessions[input.taskKey] = {
|
||||||
task_key: input.taskKey,
|
task_key: input.taskKey,
|
||||||
task_label: input.taskLabel,
|
task_label: input.taskLabel,
|
||||||
task_title: input.taskTitle,
|
task_title: input.taskTitle,
|
||||||
session_id: input.sessionId,
|
session_id: normalizedSessionId,
|
||||||
...(input.agent !== undefined ? { agent: input.agent } : {}),
|
...(input.agent !== undefined ? { agent: input.agent } : {}),
|
||||||
...(input.category !== undefined ? { category: input.category } : {}),
|
...(input.category !== undefined ? { category: input.category } : {}),
|
||||||
updated_at: nowIsoString(),
|
updated_at: nowIsoString(),
|
||||||
@@ -66,12 +67,13 @@ export function upsertTaskSessionStateForWork(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedSessionId = normalizeSessionId(input.sessionId)
|
||||||
const previousTaskSession = targetWork.task_sessions?.[input.taskKey]
|
const previousTaskSession = targetWork.task_sessions?.[input.taskKey]
|
||||||
const nextTaskSession: TaskSessionState = {
|
const nextTaskSession: TaskSessionState = {
|
||||||
task_key: input.taskKey,
|
task_key: input.taskKey,
|
||||||
task_label: input.taskLabel,
|
task_label: input.taskLabel,
|
||||||
task_title: input.taskTitle,
|
task_title: input.taskTitle,
|
||||||
session_id: input.sessionId,
|
session_id: normalizedSessionId,
|
||||||
...(input.agent !== undefined ? { agent: input.agent } : {}),
|
...(input.agent !== undefined ? { agent: input.agent } : {}),
|
||||||
...(input.category !== undefined ? { category: input.category } : {}),
|
...(input.category !== undefined ? { category: input.category } : {}),
|
||||||
...(previousTaskSession?.started_at !== undefined ? { started_at: previousTaskSession.started_at } : {}),
|
...(previousTaskSession?.started_at !== undefined ? { started_at: previousTaskSession.started_at } : {}),
|
||||||
@@ -116,7 +118,10 @@ export function startTaskTimer(
|
|||||||
startedAt?: string
|
startedAt?: string
|
||||||
},
|
},
|
||||||
): BoulderState | null {
|
): BoulderState | null {
|
||||||
const nextState = upsertTaskSessionStateForWork(directory, workId, input)
|
const nextState = upsertTaskSessionStateForWork(directory, workId, {
|
||||||
|
...input,
|
||||||
|
sessionId: normalizeSessionId(input.sessionId),
|
||||||
|
})
|
||||||
if (!nextState) {
|
if (!nextState) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { BoulderState, BoulderWorkState } from "../types"
|
|||||||
import { getBoulderFilePath } from "./path"
|
import { getBoulderFilePath } from "./path"
|
||||||
import { getPlanName } from "./plan-progress"
|
import { getPlanName } from "./plan-progress"
|
||||||
import { getBoulderWorks, readBoulderState } from "./read-state"
|
import { getBoulderWorks, readBoulderState } from "./read-state"
|
||||||
import { getElapsedMs, nowIsoString, projectWorkToMirror } from "./shared"
|
import { getElapsedMs, normalizeSessionId, nowIsoString, projectWorkToMirror } from "./shared"
|
||||||
|
|
||||||
export function writeBoulderState(directory: string, state: BoulderState): boolean {
|
export function writeBoulderState(directory: string, state: BoulderState): boolean {
|
||||||
const filePath = getBoulderFilePath(directory)
|
const filePath = getBoulderFilePath(directory)
|
||||||
@@ -67,6 +67,7 @@ export function generateWorkId(planName: string): string {
|
|||||||
|
|
||||||
export function createBoulderState(planPath: string, sessionId: string, agent?: string, worktreePath?: string): BoulderState {
|
export function createBoulderState(planPath: string, sessionId: string, agent?: string, worktreePath?: string): BoulderState {
|
||||||
const startedAt = nowIsoString()
|
const startedAt = nowIsoString()
|
||||||
|
const normalizedSessionId = normalizeSessionId(sessionId)
|
||||||
const workId = generateWorkId(getPlanName(planPath))
|
const workId = generateWorkId(getPlanName(planPath))
|
||||||
const work: BoulderWorkState = {
|
const work: BoulderWorkState = {
|
||||||
work_id: workId,
|
work_id: workId,
|
||||||
@@ -75,8 +76,8 @@ export function createBoulderState(planPath: string, sessionId: string, agent?:
|
|||||||
status: "active",
|
status: "active",
|
||||||
started_at: startedAt,
|
started_at: startedAt,
|
||||||
updated_at: startedAt,
|
updated_at: startedAt,
|
||||||
session_ids: [sessionId],
|
session_ids: [normalizedSessionId],
|
||||||
session_origins: { [sessionId]: "direct" },
|
session_origins: { [normalizedSessionId]: "direct" },
|
||||||
...(agent !== undefined ? { agent } : {}),
|
...(agent !== undefined ? { agent } : {}),
|
||||||
...(worktreePath !== undefined ? { worktree_path: worktreePath } : {}),
|
...(worktreePath !== undefined ? { worktree_path: worktreePath } : {}),
|
||||||
task_sessions: {},
|
task_sessions: {},
|
||||||
@@ -90,8 +91,8 @@ export function createBoulderState(planPath: string, sessionId: string, agent?:
|
|||||||
started_at: startedAt,
|
started_at: startedAt,
|
||||||
status: "active",
|
status: "active",
|
||||||
updated_at: startedAt,
|
updated_at: startedAt,
|
||||||
session_ids: [sessionId],
|
session_ids: [normalizedSessionId],
|
||||||
session_origins: { [sessionId]: "direct" },
|
session_origins: { [normalizedSessionId]: "direct" },
|
||||||
plan_name: getPlanName(planPath),
|
plan_name: getPlanName(planPath),
|
||||||
task_sessions: {},
|
task_sessions: {},
|
||||||
...(agent !== undefined ? { agent } : {}),
|
...(agent !== undefined ? { agent } : {}),
|
||||||
@@ -132,6 +133,7 @@ export function addBoulderWork(
|
|||||||
|
|
||||||
const workId = generateWorkId(getPlanName(input.planPath))
|
const workId = generateWorkId(getPlanName(input.planPath))
|
||||||
const startedAt = input.startedAt ?? nowIsoString()
|
const startedAt = input.startedAt ?? nowIsoString()
|
||||||
|
const normalizedSessionId = normalizeSessionId(input.sessionId)
|
||||||
const nextWork: BoulderWorkState = {
|
const nextWork: BoulderWorkState = {
|
||||||
work_id: workId,
|
work_id: workId,
|
||||||
active_plan: input.planPath,
|
active_plan: input.planPath,
|
||||||
@@ -139,8 +141,8 @@ export function addBoulderWork(
|
|||||||
status: "active",
|
status: "active",
|
||||||
started_at: startedAt,
|
started_at: startedAt,
|
||||||
updated_at: startedAt,
|
updated_at: startedAt,
|
||||||
session_ids: [input.sessionId],
|
session_ids: [normalizedSessionId],
|
||||||
session_origins: { [input.sessionId]: "direct" },
|
session_origins: { [normalizedSessionId]: "direct" },
|
||||||
...(input.agent !== undefined ? { agent: input.agent } : {}),
|
...(input.agent !== undefined ? { agent: input.agent } : {}),
|
||||||
...(input.worktreePath !== undefined ? { worktree_path: input.worktreePath } : {}),
|
...(input.worktreePath !== undefined ? { worktree_path: input.worktreePath } : {}),
|
||||||
task_sessions: {},
|
task_sessions: {},
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/// <reference path="../../../bun-test.d.ts" />
|
||||||
|
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import * as boulderState from "../src"
|
||||||
|
|
||||||
|
describe("normalizeSessionId", () => {
|
||||||
|
test("#given a bare id #when normalized without a platform #then opencode is used by default", () => {
|
||||||
|
// given
|
||||||
|
expect(typeof boulderState.normalizeSessionId).toBe("function")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const normalized = boulderState.normalizeSessionId("sess_abc")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(normalized).toBe("opencode:sess_abc")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given a bare id and codex platform #when normalized #then codex is used", () => {
|
||||||
|
// given
|
||||||
|
expect(typeof boulderState.normalizeSessionId).toBe("function")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const normalized = boulderState.normalizeSessionId("sess_abc", "codex")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(normalized).toBe("codex:sess_abc")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given an opencode-prefixed id #when normalized #then the id is unchanged", () => {
|
||||||
|
// given
|
||||||
|
const input = "opencode:sess_abc"
|
||||||
|
expect(typeof boulderState.normalizeSessionId).toBe("function")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const normalized = boulderState.normalizeSessionId(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(normalized).toBe(input)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given a codex-prefixed id and opencode platform #when normalized #then the existing prefix wins", () => {
|
||||||
|
// given
|
||||||
|
expect(typeof boulderState.normalizeSessionId).toBe("function")
|
||||||
|
|
||||||
|
|
||||||
|
// when
|
||||||
|
const normalized = boulderState.normalizeSessionId("codex:sess_abc", "opencode")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(normalized).toBe("codex:sess_abc")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given an empty id #when normalized #then opencode empty id is preserved", () => {
|
||||||
|
// given
|
||||||
|
expect(typeof boulderState.normalizeSessionId).toBe("function")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const normalized = boulderState.normalizeSessionId("")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(normalized).toBe("opencode:")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
getPlanProgress,
|
getPlanProgress,
|
||||||
getPlanName,
|
getPlanName,
|
||||||
createBoulderState,
|
createBoulderState,
|
||||||
findPrometheusPlans,
|
|
||||||
getTaskSessionState,
|
getTaskSessionState,
|
||||||
resolveBoulderPlanPath,
|
resolveBoulderPlanPath,
|
||||||
resolveBoulderPlanPathForWork,
|
resolveBoulderPlanPathForWork,
|
||||||
@@ -74,7 +73,7 @@ describe("boulder-state", () => {
|
|||||||
expect(writeSucceeded).toBe(true)
|
expect(writeSucceeded).toBe(true)
|
||||||
expect(roundTripState?.active_plan).toBe(legacyRawState.active_plan)
|
expect(roundTripState?.active_plan).toBe(legacyRawState.active_plan)
|
||||||
expect(roundTripState?.started_at).toBe(legacyRawState.started_at)
|
expect(roundTripState?.started_at).toBe(legacyRawState.started_at)
|
||||||
expect(roundTripState?.session_ids).toEqual(legacyRawState.session_ids)
|
expect(roundTripState?.session_ids).toEqual(["opencode:legacy-session"])
|
||||||
expect(roundTripState?.plan_name).toBe(legacyRawState.plan_name)
|
expect(roundTripState?.plan_name).toBe(legacyRawState.plan_name)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -172,7 +171,7 @@ describe("boulder-state", () => {
|
|||||||
const result = readBoulderState(TEST_DIR)
|
const result = readBoulderState(TEST_DIR)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result?.session_origins).toEqual({ "session-1": "direct" })
|
expect(result?.session_origins).toEqual({ "opencode:session-1": "direct" })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should keep missing origins empty when multiple sessions are tracked", () => {
|
test("should keep missing origins empty when multiple sessions are tracked", () => {
|
||||||
@@ -207,7 +206,7 @@ describe("boulder-state", () => {
|
|||||||
// then
|
// then
|
||||||
expect(result).not.toBeNull()
|
expect(result).not.toBeNull()
|
||||||
expect(result?.active_plan).toBe("/path/to/plan.md")
|
expect(result?.active_plan).toBe("/path/to/plan.md")
|
||||||
expect(result?.session_ids).toEqual(["session-1", "session-2"])
|
expect(result?.session_ids).toEqual(["opencode:session-1", "opencode:session-2"])
|
||||||
expect(result?.plan_name).toBe("my-plan")
|
expect(result?.plan_name).toBe("my-plan")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -267,7 +266,7 @@ describe("boulder-state", () => {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result).not.toBeNull()
|
expect(result).not.toBeNull()
|
||||||
expect(result?.session_ids).toEqual(["session-1", "session-2"])
|
expect(result?.session_ids).toEqual(["opencode:session-1", "opencode:session-2"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should not duplicate existing session id", () => {
|
test("should not duplicate existing session id", () => {
|
||||||
@@ -285,7 +284,7 @@ describe("boulder-state", () => {
|
|||||||
const result = readBoulderState(TEST_DIR)
|
const result = readBoulderState(TEST_DIR)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result?.session_ids).toEqual(["session-1"])
|
expect(result?.session_ids).toEqual(["opencode:session-1"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should return null when no state exists", () => {
|
test("should return null when no state exists", () => {
|
||||||
@@ -310,7 +309,7 @@ describe("boulder-state", () => {
|
|||||||
|
|
||||||
//#then - should not crash and should contain the new session
|
//#then - should not crash and should contain the new session
|
||||||
expect(result).not.toBeNull()
|
expect(result).not.toBeNull()
|
||||||
expect(result!.session_ids).toContain("ses-new")
|
expect(result!.session_ids).toContain("opencode:ses-new")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should persist appended session origin when provided", () => {
|
test("should persist appended session origin when provided", () => {
|
||||||
@@ -328,8 +327,8 @@ describe("boulder-state", () => {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result?.session_origins).toEqual({
|
expect(result?.session_origins).toEqual({
|
||||||
"session-1": "direct",
|
"opencode:session-1": "direct",
|
||||||
"session-2": "appended",
|
"opencode:session-2": "appended",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -387,7 +386,7 @@ describe("boulder-state", () => {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result).not.toBeNull()
|
expect(result).not.toBeNull()
|
||||||
expect(result?.session_id).toBe("ses_task_123")
|
expect(result?.session_id).toBe("opencode:ses_task_123")
|
||||||
expect(result?.task_title).toBe("Implement auth flow")
|
expect(result?.task_title).toBe("Implement auth flow")
|
||||||
expect(result?.agent).toBe("sisyphus-junior")
|
expect(result?.agent).toBe("sisyphus-junior")
|
||||||
expect(result?.category).toBe("deep")
|
expect(result?.category).toBe("deep")
|
||||||
@@ -422,7 +421,7 @@ describe("boulder-state", () => {
|
|||||||
const result = getTaskSessionState(TEST_DIR, "todo:1")
|
const result = getTaskSessionState(TEST_DIR, "todo:1")
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result?.session_id).toBe("ses_new")
|
expect(result?.session_id).toBe("opencode:ses_new")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -542,7 +541,7 @@ describe("boulder-state", () => {
|
|||||||
// then
|
// then
|
||||||
expect(updated).not.toBeNull()
|
expect(updated).not.toBeNull()
|
||||||
const taskSession = updated?.works?.[workId]?.task_sessions?.["todo:1"]
|
const taskSession = updated?.works?.[workId]?.task_sessions?.["todo:1"]
|
||||||
expect(taskSession?.session_id).toBe("task-session-b")
|
expect(taskSession?.session_id).toBe("opencode:task-session-b")
|
||||||
expect(taskSession?.started_at).toBe("2026-01-01T00:00:00.000Z")
|
expect(taskSession?.started_at).toBe("2026-01-01T00:00:00.000Z")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -993,7 +992,7 @@ describe("boulder-state", () => {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
expect(state.active_plan).toBe(planPath)
|
expect(state.active_plan).toBe(planPath)
|
||||||
expect(state.session_ids).toEqual([sessionId])
|
expect(state.session_ids).toEqual(["opencode:ses-abc123"])
|
||||||
expect(state.plan_name).toBe("auth-refactor")
|
expect(state.plan_name).toBe("auth-refactor")
|
||||||
expect(state.started_at).toBeDefined()
|
expect(state.started_at).toBeDefined()
|
||||||
})
|
})
|
||||||
@@ -1010,7 +1009,7 @@ describe("boulder-state", () => {
|
|||||||
//#then - state should include the agent field
|
//#then - state should include the agent field
|
||||||
expect(state.agent).toBe("atlas")
|
expect(state.agent).toBe("atlas")
|
||||||
expect(state.active_plan).toBe(planPath)
|
expect(state.active_plan).toBe(planPath)
|
||||||
expect(state.session_ids).toEqual([sessionId])
|
expect(state.session_ids).toEqual(["opencode:ses-xyz789"])
|
||||||
expect(state.plan_name).toBe("feature")
|
expect(state.plan_name).toBe("feature")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1023,7 +1022,7 @@ describe("boulder-state", () => {
|
|||||||
const state = createBoulderState(planPath, sessionId)
|
const state = createBoulderState(planPath, sessionId)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(state.session_origins).toEqual({ [sessionId]: "direct" })
|
expect(state.session_origins).toEqual({ "opencode:ses-origin": "direct" })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should allow agent to be undefined", () => {
|
test("should allow agent to be undefined", () => {
|
||||||
@@ -1080,4 +1079,79 @@ describe("boulder-state", () => {
|
|||||||
expect(resolvedPath).toBe(planPath)
|
expect(resolvedPath).toBe(planPath)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("platform-prefixed session ids", () => {
|
||||||
|
test("#given a fresh state with raw session id #when read back #then opencode prefix is stored", () => {
|
||||||
|
// given
|
||||||
|
const planPath = join(TEST_DIR, ".omo", "plans", "raw-session.md")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const state = createBoulderState(planPath, "raw-sess", "atlas", undefined)
|
||||||
|
writeBoulderState(TEST_DIR, state)
|
||||||
|
const readBack = readBoulderState(TEST_DIR)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(readBack?.session_ids).toEqual(["opencode:raw-sess"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given a fresh state with codex session id #when read back #then codex prefix is preserved", () => {
|
||||||
|
// given
|
||||||
|
const planPath = join(TEST_DIR, ".omo", "plans", "codex-session.md")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const state = createBoulderState(planPath, "codex:raw-sess", "atlas", undefined)
|
||||||
|
writeBoulderState(TEST_DIR, state)
|
||||||
|
const readBack = readBoulderState(TEST_DIR)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(readBack?.session_ids).toEqual(["codex:raw-sess"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given a legacy boulder file with bare session id #when read #then opencode prefix is migrated", () => {
|
||||||
|
// given
|
||||||
|
const boulderFile = join(OMO_DIR, "boulder.json")
|
||||||
|
writeFileSync(boulderFile, JSON.stringify({
|
||||||
|
active_plan: "/path/to/legacy.md",
|
||||||
|
started_at: "2026-01-01T00:00:00Z",
|
||||||
|
session_ids: ["legacy-bare-id"],
|
||||||
|
plan_name: "legacy",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// when
|
||||||
|
const state = readBoulderState(TEST_DIR)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(state?.session_ids).toEqual(["opencode:legacy-bare-id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given existing prefixed session #when appending raw session #then appended id receives opencode prefix", () => {
|
||||||
|
// given
|
||||||
|
writeBoulderState(TEST_DIR, {
|
||||||
|
active_plan: "/path/to/plan.md",
|
||||||
|
started_at: "2026-01-01T00:00:00Z",
|
||||||
|
session_ids: ["opencode:first"],
|
||||||
|
plan_name: "plan",
|
||||||
|
})
|
||||||
|
|
||||||
|
// when
|
||||||
|
appendSessionId(TEST_DIR, "another-raw")
|
||||||
|
const state = readBoulderState(TEST_DIR)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(state?.session_ids).toEqual(["opencode:first", "opencode:another-raw"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given stored work with prefixed session #when looking up by raw id #then matching work is returned", () => {
|
||||||
|
// given
|
||||||
|
const planPath = join(TEST_DIR, ".omo", "plans", "lookup.md")
|
||||||
|
const state = createBoulderState(planPath, "opencode:raw-id", "atlas", undefined)
|
||||||
|
writeBoulderState(TEST_DIR, state)
|
||||||
|
|
||||||
|
// when
|
||||||
|
const work = getWorkForSession(TEST_DIR, "raw-id")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(work?.session_ids).toEqual(["opencode:raw-id"])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export {
|
|||||||
getWorkByPlanName,
|
getWorkByPlanName,
|
||||||
getWorkForSession,
|
getWorkForSession,
|
||||||
getWorkResumeOptions,
|
getWorkResumeOptions,
|
||||||
|
normalizeSessionId,
|
||||||
readBoulderState,
|
readBoulderState,
|
||||||
resolveBoulderPlanPath,
|
resolveBoulderPlanPath,
|
||||||
resolveBoulderPlanPathForWork,
|
resolveBoulderPlanPathForWork,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import { normalizeSessionId } from "../../features/boulder-state"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
import { HOOK_NAME } from "./hook-name"
|
import { HOOK_NAME } from "./hook-name"
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ export async function isSessionInBoulderLineage(input: {
|
|||||||
sessionID: string
|
sessionID: string
|
||||||
boulderSessionIDs: string[]
|
boulderSessionIDs: string[]
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
|
const normalizedBoulderSessionIDs = input.boulderSessionIDs.map((sessionID) => normalizeSessionId(sessionID))
|
||||||
const visitedSessionIDs = new Set<string>()
|
const visitedSessionIDs = new Set<string>()
|
||||||
let currentSessionID = input.sessionID
|
let currentSessionID = input.sessionID
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ export async function isSessionInBoulderLineage(input: {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.boulderSessionIDs.includes(parentSessionID)) {
|
if (normalizedBoulderSessionIDs.includes(normalizeSessionId(parentSessionID))) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getPlanProgress,
|
getPlanProgress,
|
||||||
getWorkForSession,
|
getWorkForSession,
|
||||||
getTaskSessionState,
|
getTaskSessionState,
|
||||||
|
normalizeSessionId,
|
||||||
readBoulderState,
|
readBoulderState,
|
||||||
readCurrentTopLevelTask,
|
readCurrentTopLevelTask,
|
||||||
resolveBoulderPlanPath,
|
resolveBoulderPlanPath,
|
||||||
@@ -77,6 +78,7 @@ async function injectContinuation(input: {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const currentBoulder = readBoulderState(input.ctx.directory)
|
const currentBoulder = readBoulderState(input.ctx.directory)
|
||||||
|
const normalizedSessionID = normalizeSessionId(input.sessionID)
|
||||||
const currentPlanPath = currentBoulder
|
const currentPlanPath = currentBoulder
|
||||||
? resolveBoulderPlanPath(input.ctx.directory, currentBoulder)
|
? resolveBoulderPlanPath(input.ctx.directory, currentBoulder)
|
||||||
: null
|
: null
|
||||||
@@ -95,7 +97,7 @@ async function injectContinuation(input: {
|
|||||||
const canContinueSession = await canContinueTrackedBoulderSession({
|
const canContinueSession = await canContinueTrackedBoulderSession({
|
||||||
client: input.ctx.client,
|
client: input.ctx.client,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
sessionOrigin: currentBoulder.session_origins?.[input.sessionID],
|
sessionOrigin: currentBoulder.session_origins?.[normalizedSessionID],
|
||||||
boulderSessionIDs: currentBoulder.session_ids,
|
boulderSessionIDs: currentBoulder.session_ids,
|
||||||
requiredAgent: currentBoulder.agent,
|
requiredAgent: currentBoulder.agent,
|
||||||
})
|
})
|
||||||
@@ -192,7 +194,8 @@ function scheduleRetry(input: {
|
|||||||
|
|
||||||
const currentBoulder = readBoulderState(ctx.directory)
|
const currentBoulder = readBoulderState(ctx.directory)
|
||||||
if (!currentBoulder) return
|
if (!currentBoulder) return
|
||||||
if (!currentBoulder.session_ids?.includes(sessionID)) return
|
const normalizedSessionID = normalizeSessionId(sessionID)
|
||||||
|
if (!currentBoulder.session_ids?.includes(normalizedSessionID)) return
|
||||||
|
|
||||||
const currentProgress = getPlanProgress(resolveBoulderPlanPath(ctx.directory, currentBoulder))
|
const currentProgress = getPlanProgress(resolveBoulderPlanPath(ctx.directory, currentBoulder))
|
||||||
if (currentProgress.isComplete) return
|
if (currentProgress.isComplete) return
|
||||||
@@ -200,7 +203,7 @@ function scheduleRetry(input: {
|
|||||||
const canContinueSession = await canContinueTrackedBoulderSession({
|
const canContinueSession = await canContinueTrackedBoulderSession({
|
||||||
client: ctx.client,
|
client: ctx.client,
|
||||||
sessionID,
|
sessionID,
|
||||||
sessionOrigin: currentBoulder.session_origins?.[sessionID],
|
sessionOrigin: currentBoulder.session_origins?.[normalizedSessionID],
|
||||||
boulderSessionIDs: currentBoulder.session_ids,
|
boulderSessionIDs: currentBoulder.session_ids,
|
||||||
requiredAgent: currentBoulder.agent,
|
requiredAgent: currentBoulder.agent,
|
||||||
})
|
})
|
||||||
@@ -230,6 +233,7 @@ export async function handleAtlasSessionIdle(input: {
|
|||||||
sessionID: string
|
sessionID: string
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { ctx, options, getState, sessionID } = input
|
const { ctx, options, getState, sessionID } = input
|
||||||
|
const normalizedSessionID = normalizeSessionId(sessionID)
|
||||||
const sessionState = getState(sessionID)
|
const sessionState = getState(sessionID)
|
||||||
|
|
||||||
log(`[${HOOK_NAME}] session.idle`, { sessionID })
|
log(`[${HOOK_NAME}] session.idle`, { sessionID })
|
||||||
@@ -358,7 +362,7 @@ export async function handleAtlasSessionIdle(input: {
|
|||||||
const canContinueSession = await canContinueTrackedBoulderSession({
|
const canContinueSession = await canContinueTrackedBoulderSession({
|
||||||
client: ctx.client,
|
client: ctx.client,
|
||||||
sessionID,
|
sessionID,
|
||||||
sessionOrigin: boulderState.session_origins?.[sessionID],
|
sessionOrigin: boulderState.session_origins?.[normalizedSessionID],
|
||||||
boulderSessionIDs: boulderState.session_ids,
|
boulderSessionIDs: boulderState.session_ids,
|
||||||
requiredAgent: boulderState.agent,
|
requiredAgent: boulderState.agent,
|
||||||
})
|
})
|
||||||
@@ -477,7 +481,14 @@ async function canContinueTrackedBoulderSession(input: {
|
|||||||
boulderSessionIDs: string[]
|
boulderSessionIDs: string[]
|
||||||
requiredAgent?: string
|
requiredAgent?: string
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
const ancestorSessionIDs = input.boulderSessionIDs.filter((trackedSessionID) => trackedSessionID !== input.sessionID)
|
const normalizedSessionID = normalizeSessionId(input.sessionID)
|
||||||
|
if (input.sessionOrigin === "direct") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const ancestorSessionIDs = input.boulderSessionIDs
|
||||||
|
.map((sessionID) => normalizeSessionId(sessionID))
|
||||||
|
.filter((trackedSessionID) => trackedSessionID !== normalizedSessionID)
|
||||||
if (ancestorSessionIDs.length === 0) {
|
if (ancestorSessionIDs.length === 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -487,10 +498,6 @@ async function canContinueTrackedBoulderSession(input: {
|
|||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
boulderSessionIDs: ancestorSessionIDs,
|
boulderSessionIDs: ancestorSessionIDs,
|
||||||
})
|
})
|
||||||
if (input.sessionOrigin === "direct") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTrackedDescendant) {
|
if (!isTrackedDescendant) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ describe("start-work hook", () => {
|
|||||||
let omoDir: string
|
let omoDir: string
|
||||||
|
|
||||||
function createMockPluginInput() {
|
function createMockPluginInput() {
|
||||||
return {
|
return unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as Parameters<typeof createStartWorkHook>[0]
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStartWorkPrompt(options?: {
|
function createStartWorkPrompt(options?: {
|
||||||
@@ -190,7 +190,7 @@ You are starting a Sisyphus work session.
|
|||||||
writeFileSync(planAPath, "# Plan A\n- [ ] Task 1")
|
writeFileSync(planAPath, "# Plan A\n- [ ] Task 1")
|
||||||
writeFileSync(planBPath, "# Plan B\n- [ ] Task 2")
|
writeFileSync(planBPath, "# Plan B\n- [ ] Task 2")
|
||||||
|
|
||||||
const hook = createStartWorkHook({
|
const hook = createStartWorkHook(unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -207,7 +207,7 @@ You are starting a Sisyphus work session.
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as Parameters<typeof createStartWorkHook>[0])
|
}))
|
||||||
const output = {
|
const output = {
|
||||||
parts: [{ type: "text", text: createStartWorkPrompt() }],
|
parts: [{ type: "text", text: createStartWorkPrompt() }],
|
||||||
}
|
}
|
||||||
@@ -245,7 +245,7 @@ You are starting a Sisyphus work session.
|
|||||||
plan_name: "old-plan",
|
plan_name: "old-plan",
|
||||||
})
|
})
|
||||||
|
|
||||||
const hook = createStartWorkHook({
|
const hook = createStartWorkHook(unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -262,7 +262,7 @@ You are starting a Sisyphus work session.
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as Parameters<typeof createStartWorkHook>[0])
|
}))
|
||||||
const output = {
|
const output = {
|
||||||
parts: [{ type: "text", text: createStartWorkPrompt() }],
|
parts: [{ type: "text", text: createStartWorkPrompt() }],
|
||||||
}
|
}
|
||||||
@@ -291,7 +291,7 @@ You are starting a Sisyphus work session.
|
|||||||
writeFileSync(planAPath, "# Plan A\n- [ ] Task A")
|
writeFileSync(planAPath, "# Plan A\n- [ ] Task A")
|
||||||
writeFileSync(planBPath, "# Plan B\n- [ ] Task B")
|
writeFileSync(planBPath, "# Plan B\n- [ ] Task B")
|
||||||
|
|
||||||
const hook = createStartWorkHook({
|
const hook = createStartWorkHook(unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -313,7 +313,7 @@ You are starting a Sisyphus work session.
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as Parameters<typeof createStartWorkHook>[0])
|
}))
|
||||||
const output = {
|
const output = {
|
||||||
parts: [{ type: "text", text: createStartWorkPrompt() }],
|
parts: [{ type: "text", text: createStartWorkPrompt() }],
|
||||||
}
|
}
|
||||||
@@ -899,6 +899,7 @@ You are starting a Sisyphus work session.
|
|||||||
session: {
|
session: {
|
||||||
promptAsync: promptAsyncMock,
|
promptAsync: promptAsyncMock,
|
||||||
prompt: async (_request: unknown) => undefined,
|
prompt: async (_request: unknown) => undefined,
|
||||||
|
get: async () => ({ data: {} }),
|
||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -916,7 +917,7 @@ You are starting a Sisyphus work session.
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
expect(output.message.agent).toBe("atlas")
|
expect(output.message.agent).toBe("atlas")
|
||||||
expect(readBoulderState(testDir)?.session_ids).toContain("session-123")
|
expect(readBoulderState(testDir)?.session_ids).toContain("opencode:session-123")
|
||||||
expect(readBoulderState(testDir)?.agent).toBe("atlas")
|
expect(readBoulderState(testDir)?.agent).toBe("atlas")
|
||||||
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
|
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
|
||||||
promptAsyncMock.mockRestore()
|
promptAsyncMock.mockRestore()
|
||||||
@@ -968,6 +969,7 @@ You are starting a Sisyphus work session.
|
|||||||
session: {
|
session: {
|
||||||
promptAsync: promptAsyncMock,
|
promptAsync: promptAsyncMock,
|
||||||
prompt: async (_request: unknown) => undefined,
|
prompt: async (_request: unknown) => undefined,
|
||||||
|
get: async () => ({ data: {} }),
|
||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1006,7 +1008,7 @@ You are starting a Sisyphus work session.
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
expect(output.message.agent).toBe("atlas")
|
expect(output.message.agent).toBe("atlas")
|
||||||
expect(readBoulderState(testDir)?.session_ids).toContain("session-123")
|
expect(readBoulderState(testDir)?.session_ids).toContain("opencode:session-123")
|
||||||
expect(readBoulderState(testDir)?.agent).toBe("atlas")
|
expect(readBoulderState(testDir)?.agent).toBe("atlas")
|
||||||
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
|
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1022,7 +1024,8 @@ You are starting a Sisyphus work session.
|
|||||||
let detectSpy: ReturnType<typeof spyOn>
|
let detectSpy: ReturnType<typeof spyOn>
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
detectSpy = spyOn(worktreeDetector, "detectWorktreePath").mockReturnValue(null)
|
detectSpy = spyOn(worktreeDetector, "detectWorktreePath")
|
||||||
|
detectSpy.mockReturnValue(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -1138,7 +1141,7 @@ You are starting a Sisyphus work session.
|
|||||||
// then - boulder reflects updated worktree and new session appended
|
// then - boulder reflects updated worktree and new session appended
|
||||||
const state = readBoulderState(testDir)
|
const state = readBoulderState(testDir)
|
||||||
expect(state?.worktree_path).toBe("/new/wt")
|
expect(state?.worktree_path).toBe("/new/wt")
|
||||||
expect(state?.session_ids).toContain("session-456")
|
expect(state?.session_ids).toContain("opencode:session-456")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should show existing worktree on resume when no --worktree flag", async () => {
|
test("should show existing worktree on resume when no --worktree flag", async () => {
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
|
import { describe, expect, test, beforeEach, afterEach } from "bun:test"
|
||||||
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||||
|
import { join } from "node:path"
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
|
import { randomUUID } from "node:crypto"
|
||||||
|
import { readBoulderState, clearBoulderState } from "../../features/boulder-state"
|
||||||
|
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||||
|
import { createStartWorkHook } from "./start-work-hook"
|
||||||
|
|
||||||
|
describe("start-work hook platform session ids", () => {
|
||||||
|
let testDir: string
|
||||||
|
|
||||||
|
function createStartWorkPrompt(): string {
|
||||||
|
return `<command-instruction>
|
||||||
|
You are starting a Sisyphus work session.
|
||||||
|
</command-instruction>
|
||||||
|
|
||||||
|
<session-context></session-context>`
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testDir = join(tmpdir(), `start-work-hook-session-prefix-${randomUUID()}`)
|
||||||
|
mkdirSync(join(testDir, ".omo", "plans"), { recursive: true })
|
||||||
|
clearBoulderState(testDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clearBoulderState(testDir)
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given raw chat session id #when processing start-work template #then boulder stores opencode-prefixed id", async () => {
|
||||||
|
// given
|
||||||
|
writeFileSync(join(testDir, ".omo", "plans", "work.md"), "# Work\n- [ ] First task\n")
|
||||||
|
const hook = createStartWorkHook(unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
|
||||||
|
directory: testDir,
|
||||||
|
client: {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({ data: [] }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
const output = {
|
||||||
|
parts: [{ type: "text", text: createStartWorkPrompt() }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook["chat.message"]({ sessionID: "raw-sess" }, output)
|
||||||
|
const state = readBoulderState(testDir)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(state?.session_ids).toEqual(["opencode:raw-sess"])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
import { statSync } from "node:fs"
|
|
||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import {
|
import {
|
||||||
readBoulderState,
|
readBoulderState,
|
||||||
writeBoulderState,
|
|
||||||
appendSessionId,
|
|
||||||
findPrometheusPlans,
|
findPrometheusPlans,
|
||||||
getPlanProgress,
|
normalizeSessionId,
|
||||||
createBoulderState,
|
|
||||||
getPlanName,
|
|
||||||
clearBoulderState,
|
|
||||||
} from "../../features/boulder-state"
|
} from "../../features/boulder-state"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
import {
|
import {
|
||||||
@@ -89,7 +83,7 @@ export function createStartWorkHook(ctx: PluginInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const existingState = readBoulderState(ctx.directory)
|
const existingState = readBoulderState(ctx.directory)
|
||||||
const sessionId = input.sessionID
|
const sessionId = normalizeSessionId(input.sessionID, "opencode")
|
||||||
const timestamp = new Date().toISOString()
|
const timestamp = new Date().toISOString()
|
||||||
|
|
||||||
const { planName: explicitPlanName, explicitWorktreePath } = parseUserRequest(promptText)
|
const { planName: explicitPlanName, explicitWorktreePath } = parseUserRequest(promptText)
|
||||||
|
|||||||
Reference in New Issue
Block a user