refactor: rename prepare-for-commit to iterate

- Renamed prepare_logs to iterations
- Updated task manager to use new terminology
- Simplified CLI interface
- Added better TypeScript types
- Improved error handling

Task ID: 4d3f7a2e-8c1b-4f9d-b5e2-9c1d8f3a6b4d
This commit is contained in:
Smartsheet-JB-Brown 2025-04-12 17:34:00 -07:00
parent 589b159687
commit 61c9480b5b
7 changed files with 203 additions and 246 deletions

137
.roo/iterate-cli.ts Normal file
View file

@ -0,0 +1,137 @@
#!/usr/bin/env node
import { program } from "commander"
import { taskManager } from "./task-manager"
import { execSync } from "child_process"
program.name("iterate").description("CLI to manage task iterations")
program
.command("create <taskId>")
.description("Create a new iteration")
.requiredOption("-d, --description <description>", "Task description")
.action(async (taskId: string, options: { description: string }) => {
try {
await taskManager.createIteration(taskId, options.description)
console.log(`Created iteration: ${taskId}`)
} catch (error) {
console.error("Failed to create iteration:", error)
process.exit(1)
}
})
program
.command("list")
.description("List all iterations")
.action(async () => {
try {
const iterations = await taskManager.listIterations()
console.log("Available iterations:")
for (const taskId of iterations) {
const task = await taskManager.getIteration(taskId)
if (task) {
console.log(`- ${taskId}: ${task.description} (${task.current_state.status})`)
}
}
} catch (error) {
console.error("Failed to list iterations:", error)
process.exit(1)
}
})
program
.command("status <taskId>")
.description("Show iteration status")
.action(async (taskId: string) => {
try {
const task = await taskManager.getIteration(taskId)
if (!task) {
console.log("No such iteration")
return
}
console.log(`Iteration: ${task.task_id}`)
console.log(`Description: ${task.description}`)
console.log(`Status: ${task.current_state.status}`)
if (task.checkpoints.length > 0) {
console.log("\nCheckpoints:")
task.checkpoints.forEach((checkpoint, i) => {
console.log(`${i + 1}. ${checkpoint.description}`)
console.log(` Changes: ${checkpoint.changes.join(", ")}`)
console.log(` Timestamp: ${checkpoint.timestamp}`)
})
}
if (task.test_results) {
console.log("\nTest results:")
console.log(
`- Unit tests: ${task.test_results.unit_tests.passing} passing, ${task.test_results.unit_tests.failing} failing`,
)
console.log(`- Linting: ${task.test_results.linting}`)
console.log(`- Manual testing: ${task.test_results.manual_testing}`)
}
if (task.current_state.final_commit) {
console.log("\nCommit info:")
console.log(`- Hash: ${task.current_state.final_commit.hash}`)
console.log(`- Message: ${task.current_state.final_commit.message}`)
console.log("- Changes:")
task.current_state.final_commit.changes.forEach((change) => {
console.log(` * ${change}`)
})
}
} catch (error) {
console.error("Failed to get iteration status:", error)
process.exit(1)
}
})
program
.command("checkpoint <taskId>")
.description("Create a new checkpoint")
.requiredOption("-d, --description <description>", "Checkpoint description")
.requiredOption("-c, --component <component>", "Component being modified")
.requiredOption("--changes <changes...>", "List of changes")
.requiredOption("--risks <risks...>", "List of risks")
.requiredOption("--feedback <feedback...>", "Expected user feedback")
.action(async (taskId: string, options) => {
try {
const checkpoint = {
id: `checkpoint_${Date.now()}`,
description: options.description,
component: options.component,
changes: options.changes,
risks: options.risks,
expected_feedback: options.feedback,
timestamp: new Date().toISOString(),
}
await taskManager.addCheckpoint(taskId, checkpoint)
console.log(`Created checkpoint: ${checkpoint.id}`)
} catch (error) {
console.error("Failed to create checkpoint:", error)
process.exit(1)
}
})
program
.command("complete <taskId>")
.description("Complete an iteration with commit info")
.requiredOption("--message <message>", "Commit message")
.requiredOption("--changes <changes...>", "List of changes")
.action(async (taskId: string, options) => {
try {
const hash = execSync("git rev-parse HEAD").toString().trim()
await taskManager.completeIteration(taskId, {
hash,
message: options.message,
changes: options.changes,
})
console.log(`Completed iteration: ${taskId}`)
} catch (error) {
console.error("Failed to complete iteration:", error)
process.exit(1)
}
})
program.parse()

View file

@ -51,6 +51,14 @@
],
"current_state": {
"status": "completed",
"summary": "Successfully removed unused YamlParser.ts with no regressions"
"summary": "Successfully removed unused YamlParser.ts with no regressions",
"final_commit": {
"hash": "589b1596",
"message": "refactor: remove unused YamlParser implementation",
"changes": [
"Removed src/services/package-manager/YamlParser.ts",
"All tests passing (1263 pass, 0 fail, 23 pending)"
]
}
}
}

View file

@ -1,15 +1,15 @@
{
"name": "roo-prepare",
"name": "roo-iterate",
"version": "1.0.0",
"description": "Prepare for commit task management system",
"description": "Iteration task management system",
"private": true,
"bin": {
"prepare": "./dist/prepare-cli.js"
"iterate": "./dist/iterate-cli.js"
},
"scripts": {
"build": "tsc",
"prepare": "npm run build",
"start": "node ./dist/prepare-cli.js"
"start": "node ./dist/iterate-cli.js"
},
"dependencies": {
"commander": "^11.1.0"

View file

@ -1,148 +0,0 @@
#!/usr/bin/env node
import { program } from "commander"
import { taskManager } from "./task-manager"
import * as fs from "fs/promises"
import * as path from "path"
import { execSync } from "child_process"
program.name("prepare").description("CLI to manage prepare for commit tasks")
program
.command("create <taskId>")
.description("Create a new task")
.requiredOption("-d, --description <description>", "Task description")
.action(async (taskId: string, options: { description: string }) => {
try {
const initialCommit = execSync("git rev-parse HEAD").toString().trim()
await taskManager.createTask(taskId, options.description, initialCommit)
console.log(`Created task: ${taskId}`)
} catch (error) {
console.error("Failed to create task:", error)
process.exit(1)
}
})
program
.command("switch <taskId>")
.description("Switch to a different task")
.action(async (taskId: string) => {
try {
await taskManager.switchTask(taskId)
const task = taskManager.getCurrentTask()
console.log(`Switched to task: ${taskId}`)
console.log("Current checkpoint:", task?.active_checkpoint)
if (task?.pending_decisions.length) {
console.log("\nPending decisions:")
task.pending_decisions.forEach((decision, i) => {
console.log(`${i + 1}. ${decision}`)
})
}
} catch (error) {
console.error("Failed to switch task:", error)
process.exit(1)
}
})
program
.command("list")
.description("List all tasks")
.action(async () => {
try {
const tasks = await taskManager.listTasks()
console.log("Available tasks:")
for (const taskId of tasks) {
const content = await fs.readFile(path.join(".roo", "prepare_logs", `${taskId}.json`), "utf-8")
const task = JSON.parse(content)
console.log(`- ${taskId}: ${task.description} (${task.status})`)
}
} catch (error) {
console.error("Failed to list tasks:", error)
process.exit(1)
}
})
program
.command("status")
.description("Show current task status")
.action(() => {
const task = taskManager.getCurrentTask()
if (!task) {
console.log("No active task")
return
}
console.log(`Current task: ${task.task_id}`)
console.log(`Description: ${task.description}`)
console.log(`Status: ${task.status}`)
console.log(`Active checkpoint: ${task.active_checkpoint}`)
if (task.pending_decisions.length) {
console.log("\nPending decisions:")
task.pending_decisions.forEach((decision, i) => {
console.log(`${i + 1}. ${decision}`)
})
}
console.log("\nTest results:")
console.log(
`- Unit tests: ${task.test_results.unit_tests.passing} passing, ${task.test_results.unit_tests.failing} failing`,
)
console.log(`- Linting: ${task.test_results.linting}`)
console.log(`- Manual testing: ${task.test_results.manual_testing}`)
console.log("\nRollback info:")
console.log(`Full rollback: ${task.rollback_info.full_rollback}`)
console.log("Partial rollbacks:")
Object.entries(task.rollback_info.partial_rollbacks).forEach(([name, command]) => {
console.log(`- ${name}: ${command}`)
})
})
program
.command("checkpoint")
.description("Create a new checkpoint")
.requiredOption("-d, --description <description>", "Checkpoint description")
.requiredOption("-c, --component <component>", "Component being modified")
.requiredOption("--changes <changes...>", "List of changes")
.requiredOption("--risks <risks...>", "List of risks")
.requiredOption("--feedback <feedback...>", "Expected user feedback")
.action(async (options) => {
try {
const commitHash = execSync("git rev-parse HEAD").toString().trim()
const task = taskManager.getCurrentTask()
if (!task) throw new Error("No active task")
const checkpoint = {
id: `${task.task_id}_${task.checkpoints.length + 1}`,
commit_hash: commitHash,
description: options.description,
component: options.component,
changes: options.changes,
risks: options.risks,
expected_feedback: options.feedback,
timestamp: new Date().toISOString(),
}
await taskManager.addCheckpoint(checkpoint)
console.log(`Created checkpoint: ${checkpoint.id}`)
} catch (error) {
console.error("Failed to create checkpoint:", error)
process.exit(1)
}
})
program
.command("decide <index>")
.description("Resolve a pending decision")
.action(async (index: string) => {
try {
const idx = parseInt(index, 10) - 1
await taskManager.resolvePendingDecision(idx)
console.log("Decision resolved")
} catch (error) {
console.error("Failed to resolve decision:", error)
process.exit(1)
}
})
program.parse()

View file

@ -3,7 +3,6 @@ import * as path from "path"
interface TaskCheckpoint {
id: string
commit_hash: string
description: string
component: string
changes: string[]
@ -14,14 +13,10 @@ interface TaskCheckpoint {
interface TaskContext {
task_id: string
active_checkpoint: string
status: "in_progress" | "completed" | "failed"
created_at: string
last_accessed: string
description: string
initial_commit: string
created_at: string
checkpoints: TaskCheckpoint[]
test_results: {
test_results?: {
unit_tests: {
passing: number
failing: number
@ -30,130 +25,95 @@ interface TaskContext {
linting: string
manual_testing: string
}
pending_decisions: string[]
rollback_info: {
full_rollback: string
partial_rollbacks: Record<string, string>
current_state: {
status: "in_progress" | "completed" | "failed"
summary?: string
final_commit?: {
hash: string
message: string
changes: string[]
}
}
}
class TaskManager {
private currentTaskId: string | null = null
private currentContext: TaskContext | null = null
private readonly logsDir = path.join(".roo", "prepare_logs")
private readonly iterationsDir = path.join(".roo", "iterations")
constructor() {
this.ensureLogsDirectory()
this.ensureIterationsDirectory()
}
private async ensureLogsDirectory() {
private async ensureIterationsDirectory() {
try {
await fs.mkdir(this.logsDir, { recursive: true })
await fs.mkdir(this.iterationsDir, { recursive: true })
} catch (error) {
console.error("Failed to create logs directory:", error)
console.error("Failed to create iterations directory:", error)
}
}
async switchTask(taskId: string): Promise<void> {
if (this.currentTaskId) {
await this.saveCurrentTask()
}
await this.loadTask(taskId)
}
private async saveCurrentTask(): Promise<void> {
if (!this.currentTaskId || !this.currentContext) return
this.currentContext.last_accessed = new Date().toISOString()
const logPath = this.getLogPath(this.currentTaskId)
await fs.writeFile(logPath, JSON.stringify(this.currentContext, null, 2), "utf-8")
}
private async loadTask(taskId: string): Promise<void> {
const logPath = this.getLogPath(taskId)
try {
const content = await fs.readFile(logPath, "utf-8")
this.currentContext = JSON.parse(content)
this.currentTaskId = taskId
} catch (error) {
console.error(`Failed to load task ${taskId}:`, error)
throw error
}
}
async createTask(taskId: string, description: string, initialCommit: string): Promise<void> {
async createIteration(taskId: string, description: string): Promise<void> {
const newTask: TaskContext = {
task_id: taskId,
active_checkpoint: "",
status: "in_progress",
created_at: new Date().toISOString(),
last_accessed: new Date().toISOString(),
description,
initial_commit: initialCommit,
created_at: new Date().toISOString(),
checkpoints: [],
test_results: {
unit_tests: { passing: 0, failing: 0, pending: 0 },
linting: "",
manual_testing: "",
},
pending_decisions: [],
rollback_info: {
full_rollback: `git reset --hard ${initialCommit}`,
partial_rollbacks: {},
current_state: {
status: "in_progress",
},
}
const logPath = this.getLogPath(taskId)
await fs.writeFile(logPath, JSON.stringify(newTask, null, 2), "utf-8")
this.currentTaskId = taskId
this.currentContext = newTask
}
async addCheckpoint(checkpoint: TaskCheckpoint): Promise<void> {
if (!this.currentContext) throw new Error("No active task")
async addCheckpoint(taskId: string, checkpoint: TaskCheckpoint): Promise<void> {
const logPath = this.getLogPath(taskId)
const content = await fs.readFile(logPath, "utf-8")
const task = JSON.parse(content) as TaskContext
this.currentContext.checkpoints.push(checkpoint)
this.currentContext.active_checkpoint = checkpoint.id
this.currentContext.rollback_info.partial_rollbacks[checkpoint.description.toLowerCase().replace(/\s+/g, "_")] =
`git checkout ${checkpoint.commit_hash}`
await this.saveCurrentTask()
task.checkpoints.push(checkpoint)
await fs.writeFile(logPath, JSON.stringify(task, null, 2), "utf-8")
}
async updateTestResults(results: TaskContext["test_results"]): Promise<void> {
if (!this.currentContext) throw new Error("No active task")
async updateTestResults(taskId: string, results: TaskContext["test_results"]): Promise<void> {
const logPath = this.getLogPath(taskId)
const content = await fs.readFile(logPath, "utf-8")
const task = JSON.parse(content) as TaskContext
this.currentContext.test_results = results
await this.saveCurrentTask()
task.test_results = results
await fs.writeFile(logPath, JSON.stringify(task, null, 2), "utf-8")
}
async addPendingDecision(decision: string): Promise<void> {
if (!this.currentContext) throw new Error("No active task")
async completeIteration(taskId: string, commitInfo: TaskContext["current_state"]["final_commit"]): Promise<void> {
const logPath = this.getLogPath(taskId)
const content = await fs.readFile(logPath, "utf-8")
const task = JSON.parse(content) as TaskContext
this.currentContext.pending_decisions.push(decision)
await this.saveCurrentTask()
task.current_state = {
status: "completed",
summary: `Successfully completed task with commit ${commitInfo.hash}`,
final_commit: commitInfo,
}
await fs.writeFile(logPath, JSON.stringify(task, null, 2), "utf-8")
}
async resolvePendingDecision(index: number): Promise<void> {
if (!this.currentContext) throw new Error("No active task")
this.currentContext.pending_decisions.splice(index, 1)
await this.saveCurrentTask()
}
getCurrentTask(): TaskContext | null {
return this.currentContext
async getIteration(taskId: string): Promise<TaskContext | null> {
try {
const logPath = this.getLogPath(taskId)
const content = await fs.readFile(logPath, "utf-8")
return JSON.parse(content) as TaskContext
} catch (error) {
return null
}
}
private getLogPath(taskId: string): string {
return path.join(this.logsDir, `${taskId}.json`)
return path.join(this.iterationsDir, `${taskId}.json`)
}
async listTasks(): Promise<string[]> {
const files = await fs.readdir(this.logsDir)
async listIterations(): Promise<string[]> {
const files = await fs.readdir(this.iterationsDir)
return files.filter((file) => file.endsWith(".json")).map((file) => file.replace(".json", ""))
}
}