diff --git a/.ai_ops_mock_failure/events/runtime-events.ndjson b/.ai_ops_mock_failure/events/runtime-events.ndjson new file mode 100644 index 0000000..143e1c6 --- /dev/null +++ b/.ai_ops_mock_failure/events/runtime-events.ndjson @@ -0,0 +1,23 @@ +{"id":"e351954e-36c2-48e6-a116-a9e9ef4b58fe","timestamp":"2026-02-24T16:16:29.465Z","type":"session.started","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","message":"Pipeline session started.","metadata":{"entryNodeId":"product-intake"}} +{"id":"d63ca8bc-9026-4ece-a599-d96c4179f2d8","timestamp":"2026-02-24T16:16:30.002Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"product-intake","attempt":1,"message":"Node \"product-intake\" attempt 1 completed with status \"success\".","usage":{"tokenInput":84,"tokenOutput":59,"tokenTotal":143,"toolCalls":3,"durationMs":525,"costUsd":0.000286},"metadata":{"status":"success","executionContext":{"phase":"product-intake","modelConstraint":"claude-opus-4-6","allowedTools":["read_file","search","list_files"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"sequential","retrySpawned":false,"subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}} +{"id":"2b1a7fb2-6231-4f0a-9d98-a5fb7e465ccb","timestamp":"2026-02-24T16:16:30.002Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"product-intake","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}} +{"id":"8fee1767-4298-4f9b-a2da-7c23bb66297c","timestamp":"2026-02-24T16:16:30.918Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"task-roadmap","attempt":1,"message":"Node \"task-roadmap\" attempt 1 completed with status \"success\".","usage":{"tokenInput":151,"tokenOutput":106,"tokenTotal":257,"toolCalls":21,"durationMs":920,"costUsd":0.000514},"metadata":{"status":"success","executionContext":{"phase":"task-roadmap","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","list_tasks","get_task","create_task","update_task","create_subtask","update_subtask","set_task_dependencies","mcp__claude-task-master__read_file","mcp__claude-task-master__write_file","mcp__claude-task-master__search","mcp__claude-task-master__list_tasks","mcp__claude-task-master__get_task","mcp__claude-task-master__create_task","mcp__claude-task-master__update_task","mcp__claude-task-master__create_subtask","mcp__claude-task-master__update_subtask","mcp__claude-task-master__set_task_dependencies"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"sequential","retrySpawned":false,"fromNodeId":"product-intake","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}} +{"id":"460d36f6-6f42-4045-ae6e-dd71599e1eb5","timestamp":"2026-02-24T16:16:30.918Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"task-roadmap","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}} +{"id":"91e84375-30c3-4cd3-ac0c-83fcfdc600cf","timestamp":"2026-02-24T16:16:30.918Z","type":"domain.branch_merged","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"task-roadmap","attempt":1,"message":"Task \"product-intake\" merged into session base branch.","metadata":{"source":"pipeline"}} +{"id":"18f93340-58a6-452c-94df-8a13006fc0ad","timestamp":"2026-02-24T16:16:31.433Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"dev-impl-a","attempt":1,"message":"Node \"dev-impl-a\" attempt 1 completed with status \"success\".","usage":{"tokenInput":124,"tokenOutput":87,"tokenTotal":211,"toolCalls":9,"durationMs":505,"costUsd":0.000422},"metadata":{"status":"success","executionContext":{"phase":"dev-impl-a","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","bash","run_command","git_status","git_diff","npm_test"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"parallel","retrySpawned":false,"fromNodeId":"task-roadmap","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}} +{"id":"6deb1ca9-7d35-4f25-9ce1-03bde8d6676a","timestamp":"2026-02-24T16:16:31.433Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"dev-impl-a","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}} +{"id":"ac993371-13bc-4d85-b7f3-ba237976095e","timestamp":"2026-02-24T16:16:31.433Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"dev-impl-b","attempt":1,"message":"Node \"dev-impl-b\" attempt 1 completed with status \"success\".","usage":{"tokenInput":124,"tokenOutput":87,"tokenTotal":211,"toolCalls":9,"durationMs":505,"costUsd":0.000422},"metadata":{"status":"success","executionContext":{"phase":"dev-impl-b","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","bash","run_command","git_status","git_diff","npm_test"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"parallel","retrySpawned":false,"fromNodeId":"task-roadmap","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}} +{"id":"595457fa-942f-4211-b34b-a34a03810abf","timestamp":"2026-02-24T16:16:31.433Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"dev-impl-b","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}} +{"id":"5b4c50da-48ab-4463-97e9-b92123f9bca8","timestamp":"2026-02-24T16:16:31.831Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-b","attempt":1,"message":"Node \"qa-b\" attempt 1 completed with status \"success\".","usage":{"tokenInput":99,"tokenOutput":70,"tokenTotal":169,"toolCalls":8,"durationMs":380,"costUsd":0.000338},"metadata":{"status":"success","executionContext":{"phase":"qa-b","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","bash","run_command","npm_test","npm_run"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"parallel","retrySpawned":false,"fromNodeId":"dev-impl-b","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}} +{"id":"478d0234-271e-42e3-a1fc-579478073970","timestamp":"2026-02-24T16:16:31.831Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-b","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}} +{"id":"98644a3b-239b-481e-88a6-893f58e3cb15","timestamp":"2026-02-24T16:16:31.831Z","type":"domain.merge_conflict_unresolved","severity":"critical","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-b","attempt":1,"message":"Fatal merge error for task \"product-intake\".","metadata":{"source":"pipeline"}} +{"id":"222d496f-3155-41d1-8047-54d839097979","timestamp":"2026-02-24T16:16:31.831Z","type":"domain.task_blocked","severity":"critical","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-b","attempt":1,"message":"Task \"product-intake\" blocked due to fatal merge error.","metadata":{"source":"pipeline"}} +{"id":"08b2e0fd-1b5e-416d-b482-292d69bcaf20","timestamp":"2026-02-24T16:16:31.831Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-a","attempt":1,"message":"Node \"qa-a\" attempt 1 completed with status \"success\".","usage":{"tokenInput":99,"tokenOutput":70,"tokenTotal":169,"toolCalls":8,"durationMs":380,"costUsd":0.000338},"metadata":{"status":"success","executionContext":{"phase":"qa-a","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","bash","run_command","npm_test","npm_run"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"parallel","retrySpawned":false,"fromNodeId":"dev-impl-a","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}} +{"id":"dce67c2f-269d-422f-9acc-43edc4466492","timestamp":"2026-02-24T16:16:31.831Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-a","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}} +{"id":"115c4f9d-4f66-4965-8224-aebf3207522a","timestamp":"2026-02-24T16:16:31.831Z","type":"domain.branch_merged","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-a","attempt":1,"message":"Task \"product-intake\" merged into session base branch.","metadata":{"source":"pipeline"}} +{"id":"51ef601c-bcf9-4ec9-a8a9-c062901c20db","timestamp":"2026-02-24T16:16:32.474Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"merge-a","attempt":1,"message":"Node \"merge-a\" attempt 1 completed with status \"success\".","usage":{"tokenInput":93,"tokenOutput":66,"tokenTotal":159,"toolCalls":12,"durationMs":635,"costUsd":0.000318},"metadata":{"status":"success","executionContext":{"phase":"merge-a","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","bash","run_command","git","git_status","git_diff","git_add","git_commit","git_merge"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"sequential","retrySpawned":false,"fromNodeId":"qa-a","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}} +{"id":"99d4483e-34e2-4bc4-94e4-b010d0855bae","timestamp":"2026-02-24T16:16:32.474Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"merge-a","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}} +{"id":"2f6041ac-aed6-4d7a-93d9-23b13209d6c8","timestamp":"2026-02-24T16:16:33.124Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"merge-b","attempt":1,"message":"Node \"merge-b\" attempt 1 completed with status \"success\".","usage":{"tokenInput":93,"tokenOutput":66,"tokenTotal":159,"toolCalls":12,"durationMs":635,"costUsd":0.000318},"metadata":{"status":"success","executionContext":{"phase":"merge-b","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","bash","run_command","git","git_status","git_diff","git_add","git_commit","git_merge"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"sequential","retrySpawned":false,"fromNodeId":"qa-b","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}} +{"id":"fc6a654f-b52a-4879-bb69-830a459016a8","timestamp":"2026-02-24T16:16:33.124Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"merge-b","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}} +{"id":"35172289-3b25-415a-8355-1948f3efe3d5","timestamp":"2026-02-24T16:16:33.124Z","type":"domain.branch_merged","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"merge-b","attempt":1,"message":"Task \"product-intake\" merged into session base branch.","metadata":{"source":"pipeline"}} +{"id":"7c4b5477-89be-45ba-bc81-7fd2109d85d0","timestamp":"2026-02-24T16:16:33.125Z","type":"session.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","message":"Pipeline session completed with status \"success\".","metadata":{"status":"success","recordCount":8,"eventCount":13}} diff --git a/.ai_ops_mock_failure/manifests/structured-dev-workflow.json b/.ai_ops_mock_failure/manifests/structured-dev-workflow.json new file mode 100644 index 0000000..5722d16 --- /dev/null +++ b/.ai_ops_mock_failure/manifests/structured-dev-workflow.json @@ -0,0 +1,385 @@ +{ + "schemaVersion": "1", + "topologies": ["sequential", "parallel"], + "personas": [ + { + "id": "product_manager", + "displayName": "Product Manager", + "systemPromptTemplate": "You are the product manager for {{repo}}. Define clear project goals, user outcomes, non-goals, and acceptance criteria. Maintain a concise PRD-level summary in payload fields so downstream agents can execute independently. Emit a requirements_defined domain event when product requirements are clear enough for implementation planning.", + "toolClearance": { + "allowlist": ["read_file", "search", "list_files"], + "banlist": ["delete_file", "rm", "git_reset", "git_clean"] + } + }, + { + "id": "task_manager", + "displayName": "Task Manager", + "systemPromptTemplate": "You are the task planning agent for {{repo}}. Convert requirements into an implementation roadmap and dependency-aware task graph. Use claude-task-master tooling to create or update tasks, subtasks, and dependencies. Prioritize and surface undone + unblocked tasks first. Required output in payload.taskPlan.tasks: id, title, status, dependencies, subtasks, acceptanceCriteria, and ownerHint. Status values should align with pending/in_progress/blocked/done. Emit requirements_defined when clarifications materially update scope and emit tasks_planned when task graph updates are ready for coding lanes.", + "modelConstraint": "claude-sonnet-4-5", + "toolClearance": { + "allowlist": [ + "read_file", + "write_file", + "search", + "list_files", + "list_tasks", + "get_task", + "create_task", + "update_task", + "create_subtask", + "update_subtask", + "set_task_dependencies", + "mcp__claude-task-master__read_file", + "mcp__claude-task-master__write_file", + "mcp__claude-task-master__search", + "mcp__claude-task-master__list_tasks", + "mcp__claude-task-master__get_task", + "mcp__claude-task-master__create_task", + "mcp__claude-task-master__update_task", + "mcp__claude-task-master__create_subtask", + "mcp__claude-task-master__update_subtask", + "mcp__claude-task-master__set_task_dependencies" + ], + "banlist": ["delete_file", "rm", "git_reset", "git_clean"] + } + }, + { + "id": "developer", + "displayName": "Developer", + "systemPromptTemplate": "You are a coding agent for {{repo}}. Consume the task plan from handoff payload and select undone, unblocked tasks whose dependencies are satisfied. Keep changes scoped to your assigned lane/node and report completed task IDs. If requirements are unclear or blocked, return status validation_fail with a precise clarification request in payload and emit task_blocked. On successful implementation, emit code_committed and include changedFiles, completedTaskIds, and validation notes in payload.", + "modelConstraint": "claude-sonnet-4-5", + "toolClearance": { + "allowlist": [ + "read_file", + "write_file", + "search", + "list_files", + "bash", + "run_command", + "git_status", + "git_diff", + "npm_test" + ], + "banlist": ["delete_file", "rm", "git_reset", "git_clean", "git_push"] + } + }, + { + "id": "tester", + "displayName": "Tester", + "systemPromptTemplate": "You are the testing and validation agent for {{repo}}. Validate code and task acceptance criteria for your lane, prioritizing deterministic checks (tests, build, lint, targeted runtime checks). If validation fails, return status validation_fail with reproducible steps and concrete remediation notes. If validation passes, return success, include evidence in payload, and emit validation_passed.", + "modelConstraint": "claude-sonnet-4-5", + "toolClearance": { + "allowlist": [ + "read_file", + "write_file", + "search", + "list_files", + "bash", + "run_command", + "npm_test", + "npm_run" + ], + "banlist": ["delete_file", "rm", "git_reset", "git_clean", "git_push"] + } + }, + { + "id": "git_integrator", + "displayName": "Git Integrator", + "systemPromptTemplate": "You are the git integration agent for {{repo}}. Validate lane readiness for merge into the integration target and ensure the worktree is merge-ready. Run status checks and report risks clearly. Do not force risky merge strategies. On successful readiness checks emit branch_merged and include mergeCommit (or readiness marker), mergedBranch, and targetBranch in payload.", + "modelConstraint": "claude-sonnet-4-5", + "toolClearance": { + "allowlist": [ + "read_file", + "write_file", + "search", + "list_files", + "bash", + "run_command", + "git", + "git_status", + "git_diff", + "git_add", + "git_commit", + "git_merge" + ], + "banlist": ["delete_file", "rm", "git_reset", "git_clean", "git_push", "git_rebase"] + } + }, + { + "id": "conflict_resolver", + "displayName": "Conflict Resolver", + "systemPromptTemplate": "You are the merge conflict resolver for {{repo}}. Resolve conflict markers for the assigned task/worktree, run targeted validation checks, and return success only when conflicts are cleanly resolved. Include resolvedFiles and validation evidence in payload.", + "modelConstraint": "claude-sonnet-4-5", + "toolClearance": { + "allowlist": [ + "read_file", + "write_file", + "search", + "list_files", + "bash", + "run_command", + "git", + "git_status", + "git_diff", + "git_add", + "git_commit", + "npm_test", + "npm_run" + ], + "banlist": ["delete_file", "rm", "git_reset", "git_clean", "git_push", "git_rebase"] + } + } + ], + "relationships": [ + { + "parentPersonaId": "product_manager", + "childPersonaId": "task_manager", + "constraints": { + "maxDepth": 2, + "maxChildren": 1 + } + }, + { + "parentPersonaId": "task_manager", + "childPersonaId": "developer", + "constraints": { + "maxDepth": 4, + "maxChildren": 4 + } + }, + { + "parentPersonaId": "task_manager", + "childPersonaId": "tester", + "constraints": { + "maxDepth": 4, + "maxChildren": 4 + } + }, + { + "parentPersonaId": "task_manager", + "childPersonaId": "git_integrator", + "constraints": { + "maxDepth": 5, + "maxChildren": 2 + } + } + ], + "topologyConstraints": { + "maxDepth": 6, + "maxRetries": 0 + }, + "pipeline": { + "entryNodeId": "product-intake", + "nodes": [ + { + "id": "product-intake", + "actorId": "product_manager_actor", + "personaId": "product_manager" + }, + { + "id": "task-roadmap", + "actorId": "task_manager_actor", + "personaId": "task_manager" + }, + { + "id": "dev-impl-a", + "actorId": "developer_actor", + "personaId": "developer", + "topology": { + "kind": "parallel", + "blockId": "implementation-pass-1" + } + }, + { + "id": "dev-impl-b", + "actorId": "developer_actor", + "personaId": "developer", + "topology": { + "kind": "parallel", + "blockId": "implementation-pass-1" + } + }, + { + "id": "qa-a", + "actorId": "tester_actor", + "personaId": "tester", + "topology": { + "kind": "parallel", + "blockId": "validation-pass-1" + } + }, + { + "id": "qa-b", + "actorId": "tester_actor", + "personaId": "tester", + "topology": { + "kind": "parallel", + "blockId": "validation-pass-1" + } + }, + { + "id": "task-clarify-a", + "actorId": "task_manager_actor", + "personaId": "task_manager" + }, + { + "id": "task-clarify-b", + "actorId": "task_manager_actor", + "personaId": "task_manager" + }, + { + "id": "dev-rework-a", + "actorId": "developer_actor", + "personaId": "developer", + "topology": { + "kind": "parallel", + "blockId": "implementation-pass-2" + } + }, + { + "id": "dev-rework-b", + "actorId": "developer_actor", + "personaId": "developer", + "topology": { + "kind": "parallel", + "blockId": "implementation-pass-2" + } + }, + { + "id": "qa-rework-a", + "actorId": "tester_actor", + "personaId": "tester", + "topology": { + "kind": "parallel", + "blockId": "validation-pass-2" + } + }, + { + "id": "qa-rework-b", + "actorId": "tester_actor", + "personaId": "tester", + "topology": { + "kind": "parallel", + "blockId": "validation-pass-2" + } + }, + { + "id": "merge-a", + "actorId": "git_integrator_actor", + "personaId": "git_integrator" + }, + { + "id": "merge-b", + "actorId": "git_integrator_actor", + "personaId": "git_integrator" + }, + { + "id": "merge-conflict-resolve-a", + "actorId": "conflict_resolver_actor", + "personaId": "conflict_resolver" + }, + { + "id": "merge-conflict-resolve-b", + "actorId": "conflict_resolver_actor", + "personaId": "conflict_resolver" + } + ], + "edges": [ + { + "from": "product-intake", + "to": "task-roadmap", + "on": "success" + }, + { + "from": "task-roadmap", + "to": "dev-impl-a", + "on": "success" + }, + { + "from": "task-roadmap", + "to": "dev-impl-b", + "on": "success" + }, + { + "from": "dev-impl-a", + "to": "qa-a", + "on": "success" + }, + { + "from": "dev-impl-b", + "to": "qa-b", + "on": "success" + }, + { + "from": "dev-impl-a", + "to": "task-clarify-a", + "on": "validation_fail" + }, + { + "from": "dev-impl-b", + "to": "task-clarify-b", + "on": "validation_fail" + }, + { + "from": "qa-a", + "to": "dev-rework-a", + "on": "validation_fail" + }, + { + "from": "qa-b", + "to": "dev-rework-b", + "on": "validation_fail" + }, + { + "from": "task-clarify-a", + "to": "dev-rework-a", + "on": "success" + }, + { + "from": "task-clarify-b", + "to": "dev-rework-b", + "on": "success" + }, + { + "from": "dev-rework-a", + "to": "qa-rework-a", + "on": "success" + }, + { + "from": "dev-rework-b", + "to": "qa-rework-b", + "on": "success" + }, + { + "from": "qa-a", + "to": "merge-a", + "on": "success" + }, + { + "from": "qa-b", + "to": "merge-b", + "on": "success" + }, + { + "from": "qa-rework-a", + "to": "merge-a", + "on": "success" + }, + { + "from": "qa-rework-b", + "to": "merge-b", + "on": "success" + }, + { + "from": "merge-a", + "to": "merge-conflict-resolve-a", + "event": "merge_conflict_detected" + }, + { + "from": "merge-b", + "to": "merge-conflict-resolve-b", + "event": "merge_conflict_detected" + } + ] + } +} diff --git a/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/dev-impl-a.json b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/dev-impl-a.json new file mode 100644 index 0000000..e908434 --- /dev/null +++ b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/dev-impl-a.json @@ -0,0 +1,17 @@ +{ + "nodeId": "dev-impl-a", + "fromNodeId": "task-roadmap", + "payload": { + "prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.", + "summary": "Node task-roadmap completed in mock mode.", + "subtasks": [ + "build EcoSwap: Community Skill-Sharing Marketplace", + "A platform for neighbors to trade time and skills without using actual money", + "Feature 1: Skill Registry: Users list what they can teach (e" + ], + "taskId": "product-intake", + "worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake", + "mergeStatus": "merged" + }, + "createdAt": "2026-02-24T16:16:30.919Z" +} diff --git a/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/dev-impl-b.json b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/dev-impl-b.json new file mode 100644 index 0000000..a08a084 --- /dev/null +++ b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/dev-impl-b.json @@ -0,0 +1,17 @@ +{ + "nodeId": "dev-impl-b", + "fromNodeId": "task-roadmap", + "payload": { + "prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.", + "summary": "Node task-roadmap completed in mock mode.", + "subtasks": [ + "build EcoSwap: Community Skill-Sharing Marketplace", + "A platform for neighbors to trade time and skills without using actual money", + "Feature 1: Skill Registry: Users list what they can teach (e" + ], + "taskId": "product-intake", + "worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake", + "mergeStatus": "merged" + }, + "createdAt": "2026-02-24T16:16:30.919Z" +} diff --git a/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/merge-a.json b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/merge-a.json new file mode 100644 index 0000000..c1d4ad4 --- /dev/null +++ b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/merge-a.json @@ -0,0 +1,17 @@ +{ + "nodeId": "merge-a", + "fromNodeId": "qa-a", + "payload": { + "prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.", + "summary": "Node qa-a completed in mock mode.", + "subtasks": [ + "build EcoSwap: Community Skill-Sharing Marketplace", + "A platform for neighbors to trade time and skills without using actual money", + "Feature 1: Skill Registry: Users list what they can teach (e" + ], + "taskId": "product-intake", + "worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake", + "mergeStatus": "merged" + }, + "createdAt": "2026-02-24T16:16:31.832Z" +} diff --git a/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/merge-b.json b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/merge-b.json new file mode 100644 index 0000000..4505752 --- /dev/null +++ b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/merge-b.json @@ -0,0 +1,18 @@ +{ + "nodeId": "merge-b", + "fromNodeId": "qa-b", + "payload": { + "prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.", + "summary": "Node qa-b completed in mock mode.", + "subtasks": [ + "build EcoSwap: Community Skill-Sharing Marketplace", + "A platform for neighbors to trade time and skills without using actual money", + "Feature 1: Skill Registry: Users list what they can teach (e" + ], + "taskId": "product-intake", + "worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake", + "mergeStatus": "fatal_error", + "mergeError": "git -C /home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/base worktree remove --force /home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake failed: fatal: '/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake' is not a working tree" + }, + "createdAt": "2026-02-24T16:16:31.832Z" +} diff --git a/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/product-intake.json b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/product-intake.json new file mode 100644 index 0000000..d7a59b9 --- /dev/null +++ b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/product-intake.json @@ -0,0 +1,7 @@ +{ + "nodeId": "product-intake", + "payload": { + "prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned." + }, + "createdAt": "2026-02-24T16:16:29.466Z" +} diff --git a/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/qa-a.json b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/qa-a.json new file mode 100644 index 0000000..0d70902 --- /dev/null +++ b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/qa-a.json @@ -0,0 +1,17 @@ +{ + "nodeId": "qa-a", + "fromNodeId": "dev-impl-a", + "payload": { + "prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.", + "summary": "Node dev-impl-a completed in mock mode.", + "subtasks": [ + "build EcoSwap: Community Skill-Sharing Marketplace", + "A platform for neighbors to trade time and skills without using actual money", + "Feature 1: Skill Registry: Users list what they can teach (e" + ], + "taskId": "product-intake", + "worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake", + "mergeStatus": "merged" + }, + "createdAt": "2026-02-24T16:16:31.434Z" +} diff --git a/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/qa-b.json b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/qa-b.json new file mode 100644 index 0000000..421b2cd --- /dev/null +++ b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/qa-b.json @@ -0,0 +1,17 @@ +{ + "nodeId": "qa-b", + "fromNodeId": "dev-impl-b", + "payload": { + "prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.", + "summary": "Node dev-impl-b completed in mock mode.", + "subtasks": [ + "build EcoSwap: Community Skill-Sharing Marketplace", + "A platform for neighbors to trade time and skills without using actual money", + "Feature 1: Skill Registry: Users list what they can teach (e" + ], + "taskId": "product-intake", + "worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake", + "mergeStatus": "merged" + }, + "createdAt": "2026-02-24T16:16:31.434Z" +} diff --git a/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/task-roadmap.json b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/task-roadmap.json new file mode 100644 index 0000000..e9592b8 --- /dev/null +++ b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/handoffs/task-roadmap.json @@ -0,0 +1,16 @@ +{ + "nodeId": "task-roadmap", + "fromNodeId": "product-intake", + "payload": { + "prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.", + "summary": "Node product-intake completed in mock mode.", + "subtasks": [ + "build EcoSwap: Community Skill-Sharing Marketplace", + "A platform for neighbors to trade time and skills without using actual money", + "Feature 1: Skill Registry: Users list what they can teach (e" + ], + "taskId": "product-intake", + "worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake" + }, + "createdAt": "2026-02-24T16:16:30.002Z" +} diff --git a/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/project-context.json b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/project-context.json new file mode 100644 index 0000000..0de0b06 --- /dev/null +++ b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/project-context.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "globalFlags": {}, + "artifactPointers": { + "sessions/ui-session-mm0t56zh-079824be/last_completed_node": "merge-b", + "sessions/ui-session-mm0t56zh-079824be/last_attempt": "1", + "sessions/ui-session-mm0t56zh-079824be/final_state": "/home/zman/projects/ai_ops/.ai_ops/state/ui-session-mm0t56zh-079824be/state.json" + }, + "taskQueue": [ + { + "taskId": "product-intake", + "id": "product-intake", + "title": "product-intake", + "status": "merged", + "metadata": { + "mergeConflict": { + "attempts": 0, + "maxAttempts": 2, + "status": "merged", + "mergedAt": "2026-02-24T16:16:33.124Z" + } + } + } + ] +} diff --git a/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/session-metadata.json b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/session-metadata.json new file mode 100644 index 0000000..6a192a5 --- /dev/null +++ b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/session-metadata.json @@ -0,0 +1,8 @@ +{ + "sessionId": "ui-session-mm0t56zh-079824be", + "projectPath": "/home/zman/projects/ai_ops/.workspace/ecoswap", + "baseWorkspacePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/base", + "sessionStatus": "active", + "createdAt": "2026-02-24T16:16:12.702Z", + "updatedAt": "2026-02-24T16:16:12.702Z" +} diff --git a/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/state.json b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/state.json new file mode 100644 index 0000000..7aacdf9 --- /dev/null +++ b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/state.json @@ -0,0 +1,182 @@ +{ + "flags": { + "product-intake_completed": true, + "task-roadmap_completed": true, + "dev-impl-a_completed": true, + "dev-impl-b_completed": true, + "qa-b_completed": true, + "qa-a_completed": true, + "merge-a_completed": true, + "merge-b_completed": true + }, + "metadata": { + "project_context": { + "globalFlags": {}, + "artifactPointers": {}, + "taskQueue": [] + }, + "usage": { + "tokenInput": 93, + "tokenOutput": 66, + "durationMs": 635, + "costUsd": 0.000318, + "tokenTotal": 159, + "toolCalls": 12 + }, + "topologyHint": "manifest-default" + }, + "history": [ + { + "nodeId": "product-intake", + "event": "validation_passed", + "timestamp": "2026-02-24T16:16:30.000Z", + "data": { + "source": "actor", + "attempt": 1, + "summary": "Node completed successfully." + } + }, + { + "nodeId": "task-roadmap", + "event": "validation_passed", + "timestamp": "2026-02-24T16:16:30.905Z", + "data": { + "source": "actor", + "attempt": 1, + "summary": "Node completed successfully." + } + }, + { + "nodeId": "task-roadmap", + "event": "branch_merged", + "timestamp": "2026-02-24T16:16:30.918Z", + "data": { + "source": "pipeline", + "attempt": 1, + "summary": "Task \"product-intake\" merged into session base branch.", + "details": { + "taskId": "product-intake", + "worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake" + } + } + }, + { + "nodeId": "dev-impl-a", + "event": "validation_passed", + "timestamp": "2026-02-24T16:16:31.432Z", + "data": { + "source": "actor", + "attempt": 1, + "summary": "Node completed successfully." + } + }, + { + "nodeId": "dev-impl-b", + "event": "validation_passed", + "timestamp": "2026-02-24T16:16:31.432Z", + "data": { + "source": "actor", + "attempt": 1, + "summary": "Node completed successfully." + } + }, + { + "nodeId": "qa-b", + "event": "validation_passed", + "timestamp": "2026-02-24T16:16:31.817Z", + "data": { + "source": "actor", + "attempt": 1, + "summary": "Node completed successfully." + } + }, + { + "nodeId": "qa-b", + "event": "merge_conflict_unresolved", + "timestamp": "2026-02-24T16:16:31.831Z", + "data": { + "source": "pipeline", + "attempt": 1, + "summary": "Fatal merge error for task \"product-intake\".", + "details": { + "taskId": "product-intake", + "worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake", + "error": "git -C /home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/base worktree remove --force /home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake failed: fatal: '/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake' is not a working tree", + "mergeBase": "1650156ff0966f9071107c1e3ed4e51d02df3f24" + } + } + }, + { + "nodeId": "qa-b", + "event": "task_blocked", + "timestamp": "2026-02-24T16:16:31.831Z", + "data": { + "source": "pipeline", + "attempt": 1, + "summary": "Task \"product-intake\" blocked due to fatal merge error.", + "details": { + "taskId": "product-intake", + "error": "git -C /home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/base worktree remove --force /home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake failed: fatal: '/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake' is not a working tree" + } + } + }, + { + "nodeId": "qa-a", + "event": "validation_passed", + "timestamp": "2026-02-24T16:16:31.816Z", + "data": { + "source": "actor", + "attempt": 1, + "summary": "Node completed successfully." + } + }, + { + "nodeId": "qa-a", + "event": "branch_merged", + "timestamp": "2026-02-24T16:16:31.831Z", + "data": { + "source": "pipeline", + "attempt": 1, + "summary": "Task \"product-intake\" merged into session base branch.", + "details": { + "taskId": "product-intake", + "worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake" + } + } + }, + { + "nodeId": "merge-a", + "event": "validation_passed", + "timestamp": "2026-02-24T16:16:32.473Z", + "data": { + "source": "actor", + "attempt": 1, + "summary": "Node completed successfully." + } + }, + { + "nodeId": "merge-b", + "event": "validation_passed", + "timestamp": "2026-02-24T16:16:33.112Z", + "data": { + "source": "actor", + "attempt": 1, + "summary": "Node completed successfully." + } + }, + { + "nodeId": "merge-b", + "event": "branch_merged", + "timestamp": "2026-02-24T16:16:33.124Z", + "data": { + "source": "pipeline", + "attempt": 1, + "summary": "Task \"product-intake\" merged into session base branch.", + "details": { + "taskId": "product-intake", + "worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake" + } + } + } + ] +} diff --git a/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/ui-run-meta.json b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/ui-run-meta.json new file mode 100644 index 0000000..7f3935a --- /dev/null +++ b/.ai_ops_mock_failure/state/ui-session-mm0t56zh-079824be/ui-run-meta.json @@ -0,0 +1,10 @@ +{ + "runId": "eb9f2e22-d117-42fd-b03c-25a08127a3d8", + "sessionId": "ui-session-mm0t56zh-079824be", + "status": "success", + "startedAt": "2026-02-24T16:16:29.463Z", + "executionMode": "mock", + "provider": "claude", + "manifestPath": ".ai_ops/manifests/structured-dev-workflow.json", + "endedAt": "2026-02-24T16:16:33.125Z" +} diff --git a/.ai_ops_mock_failure/worktrees/ui-session-mm0t56zh-079824be/base/README.md b/.ai_ops_mock_failure/worktrees/ui-session-mm0t56zh-079824be/base/README.md new file mode 100644 index 0000000..34a055e --- /dev/null +++ b/.ai_ops_mock_failure/worktrees/ui-session-mm0t56zh-079824be/base/README.md @@ -0,0 +1 @@ +# ECOSWAP diff --git a/.env.example b/.env.example index d4334cf..2204842 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,7 @@ CLAUDE_CODE_OAUTH_TOKEN= ANTHROPIC_API_KEY= CLAUDE_MODEL= CLAUDE_CODE_PATH= +CLAUDE_MAX_TURNS=2 # Claude binary observability: off | stdout | file | both CLAUDE_OBSERVABILITY_MODE=off # CLAUDE_OBSERVABILITY_VERBOSITY: summary | full @@ -52,7 +53,7 @@ AGENT_PORT_LOCK_DIR=.ai_ops/locks/ports AGENT_DISCOVERY_FILE_RELATIVE_PATH=.agent-context/resources.json # Security middleware -# AGENT_SECURITY_VIOLATION_MODE: hard_abort | validation_fail +# AGENT_SECURITY_VIOLATION_MODE: hard_abort | validation_fail | dangerous_warn_only AGENT_SECURITY_VIOLATION_MODE=hard_abort AGENT_SECURITY_ALLOWED_BINARIES=git,npm,node,cat,ls,pwd,echo,bash,sh AGENT_SECURITY_COMMAND_TIMEOUT_MS=120000 diff --git a/README.md b/README.md index 451c308..53e93f8 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,9 @@ Provider mode notes: - `provider=codex` uses existing OpenAI/Codex auth settings (`OPENAI_AUTH_MODE`, `CODEX_API_KEY`, `OPENAI_API_KEY`). - `provider=claude` uses Claude auth resolution (`CLAUDE_CODE_OAUTH_TOKEN` preferred, otherwise `ANTHROPIC_API_KEY`, or existing Claude Code login state). - `CLAUDE_MODEL` should be a Claude model id/alias recognized by Claude Code (for example `claude-sonnet-4-6`); `anthropic/...` prefixes are normalized automatically. +- `CLAUDE_MAX_TURNS` controls the per-query Claude turn budget (default `2`). - Claude provider runs can emit Claude SDK/CLI internals to stdout and/or NDJSON with `CLAUDE_OBSERVABILITY_*` settings. +- UI session-mode provider runs execute directly in orchestration-assigned task/base worktrees; provider adapters do not allocate additional nested worktrees. ## Manifest Semantics @@ -271,6 +273,7 @@ jq -c 'select(.severity=="critical")' .ai_ops/events/runtime-events.ndjson - Pipeline behavior on `SecurityViolationError` is configurable: - `hard_abort` (default) - `validation_fail` (retry-unrolled remediation) + - `dangerous_warn_only` (logs violations and continues execution; high risk) ## Environment Variables @@ -285,6 +288,7 @@ jq -c 'select(.severity=="critical")' .ai_ops/events/runtime-events.ndjson - `ANTHROPIC_API_KEY` (used when `CLAUDE_CODE_OAUTH_TOKEN` is unset) - `CLAUDE_MODEL` - `CLAUDE_CODE_PATH` +- `CLAUDE_MAX_TURNS` (integer >= 1, defaults to `2`) - `CLAUDE_OBSERVABILITY_MODE` (`off`, `stdout`, `file`, or `both`) - `CLAUDE_OBSERVABILITY_VERBOSITY` (`summary` or `full`) - `CLAUDE_OBSERVABILITY_LOG_PATH` @@ -322,7 +326,7 @@ jq -c 'select(.severity=="critical")' .ai_ops/events/runtime-events.ndjson ### Security Middleware -- `AGENT_SECURITY_VIOLATION_MODE` (`hard_abort` or `validation_fail`) +- `AGENT_SECURITY_VIOLATION_MODE` (`hard_abort`, `validation_fail`, or `dangerous_warn_only`) - `AGENT_SECURITY_ALLOWED_BINARIES` - `AGENT_SECURITY_COMMAND_TIMEOUT_MS` - `AGENT_SECURITY_AUDIT_LOG_PATH` diff --git a/docs/orchestration-engine.md b/docs/orchestration-engine.md index ea2bc2b..a10e198 100644 --- a/docs/orchestration-engine.md +++ b/docs/orchestration-engine.md @@ -37,6 +37,11 @@ Before each actor invocation, orchestration resolves an immutable `ResolvedExecu This keeps orchestration policy resolution separate from executor enforcement. Executors do not need to parse manifests or MCP registry internals. +Worktree ownership invariant: + +- In UI session mode, orchestration/session lifecycle is the single owner of git worktree allocation. +- Provider adapters (Codex/Claude runtime wrappers) must execute inside `ResolvedExecutionContext.security.worktreePath` and must not provision independent worktrees. + ## Execution topology model - Pipeline graph execution is DAG-based with ready-node frontiers. diff --git a/docs/security-middleware.md b/docs/security-middleware.md index 04dd581..01e6303 100644 --- a/docs/security-middleware.md +++ b/docs/security-middleware.md @@ -30,6 +30,7 @@ This middleware provides a first-pass hardening layer for agent-executed shell c - `hard_abort` (default): fail fast and stop the pipeline. - `validation_fail`: map violation to retry-unrolled behavior so the actor can attempt a compliant alternative. +- `dangerous_warn_only`: emit security audit/runtime events but continue execution. This is intentionally unsafe and should only be used for temporary unblock/debug workflows. ## MCP integration diff --git a/src/agents/orchestration.ts b/src/agents/orchestration.ts index ebba1c8..611670f 100644 --- a/src/agents/orchestration.ts +++ b/src/agents/orchestration.ts @@ -26,6 +26,7 @@ import type { JsonObject } from "./types.js"; import { SessionWorktreeManager, type SessionMetadata } from "./session-lifecycle.js"; import { SecureCommandExecutor, + type SecurityViolationHandling, type SecurityAuditEvent, type SecurityAuditSink, SecurityRulesEngine, @@ -46,7 +47,7 @@ export type OrchestrationSettings = { maxRetries: number; maxChildren: number; mergeConflictMaxAttempts: number; - securityViolationHandling: "hard_abort" | "validation_fail"; + securityViolationHandling: SecurityViolationHandling; runtimeContext: Record; }; @@ -211,6 +212,9 @@ function createActorSecurityContext(input: { blockedEnvAssignments: ["AGENT_STATE_ROOT", "AGENT_PROJECT_CONTEXT_PATH"], }, auditSink, + { + violationHandling: input.settings.securityViolationHandling, + }, ); return { @@ -342,6 +346,7 @@ export class SchemaDrivenExecutionEngine { this.sessionWorktreeManager = new SessionWorktreeManager({ worktreeRoot: resolve(this.settings.workspaceRoot, this.config.provisioning.gitWorktree.rootDirectory), baseRef: this.config.provisioning.gitWorktree.baseRef, + targetPath: this.config.provisioning.gitWorktree.targetPath, }); this.actorExecutors = toExecutorMap(input.actorExecutors); @@ -426,7 +431,11 @@ export class SchemaDrivenExecutionEngine { }): Promise { const managerSessionId = `${input.sessionId}__pipeline`; const managerSession = this.manager.createSession(managerSessionId); - const workspaceRoot = input.sessionMetadata?.baseWorkspacePath ?? this.settings.workspaceRoot; + const workspaceRoot = input.sessionMetadata + ? this.sessionWorktreeManager.resolveWorkingDirectoryForWorktree( + input.sessionMetadata.baseWorkspacePath, + ) + : this.settings.workspaceRoot; const projectContextStore = input.sessionMetadata ? new FileSystemProjectContextStore({ filePath: resolveSessionProjectContextPath(this.settings.stateRoot, input.sessionId), @@ -531,6 +540,7 @@ export class SchemaDrivenExecutionEngine { return { taskId, + workingDirectory: ensured.taskWorkingDirectory, worktreePath: ensured.taskWorktreePath, statusAtStart, ...(existing?.metadata ? { metadata: existing.metadata } : {}), diff --git a/src/agents/pipeline.ts b/src/agents/pipeline.ts index 109dc5a..8a497a2 100644 --- a/src/agents/pipeline.ts +++ b/src/agents/pipeline.ts @@ -63,6 +63,7 @@ export type ActorExecutionResult = { export type ActorToolPermissionResult = | { behavior: "allow"; + updatedInput?: Record; toolUseID?: string; } | { @@ -171,6 +172,7 @@ export type ActorExecutionSecurityContext = { export type TaskExecutionResolution = { taskId: string; + workingDirectory: string; worktreePath: string; statusAtStart: string; metadata?: JsonObject; @@ -941,7 +943,7 @@ export class PipelineExecutor { node, toolClearance, prompt, - worktreePathOverride: taskResolution?.worktreePath, + worktreePathOverride: taskResolution?.workingDirectory, }); const result = await this.invokeActorExecutor({ @@ -970,6 +972,7 @@ export class PipelineExecutor { ...(taskResolution ? { taskId: taskResolution.taskId, + workingDirectory: taskResolution.workingDirectory, worktreePath: taskResolution.worktreePath, } : {}), @@ -1309,6 +1312,7 @@ export class PipelineExecutor { const createToolPermissionHandler = (): ActorToolPermissionHandler => this.createToolPermissionHandler({ allowedTools: executionContext.allowedTools, + violationMode: executionContext.security.violationMode, sessionId: input.sessionId, nodeId: input.nodeId, attempt: input.attempt, @@ -1326,6 +1330,7 @@ export class PipelineExecutor { private createToolPermissionHandler(input: { allowedTools: readonly string[]; + violationMode: SecurityViolationHandling; sessionId: string; nodeId: string; attempt: number; @@ -1340,7 +1345,7 @@ export class PipelineExecutor { attempt: input.attempt, }; - return async (toolName, _input, options) => { + return async (toolName, toolInput, options) => { const toolUseID = options.toolUseID; if (options.signal.aborted) { return { @@ -1358,11 +1363,28 @@ export class PipelineExecutor { caseInsensitiveLookup: caseInsensitiveAllowLookup, }); if (!allowMatch) { - rulesEngine?.assertToolInvocationAllowed({ - tool: candidates[0] ?? toolName, - toolClearance: toolPolicy, - context: toolAuditContext, - }); + if (rulesEngine) { + try { + rulesEngine.assertToolInvocationAllowed({ + tool: candidates[0] ?? toolName, + toolClearance: toolPolicy, + context: toolAuditContext, + }); + } catch (error) { + if ( + !(input.violationMode === "dangerous_warn_only" && error instanceof SecurityViolationError) + ) { + throw error; + } + } + } + if (input.violationMode === "dangerous_warn_only") { + return { + behavior: "allow", + updatedInput: toolInput, + ...(toolUseID ? { toolUseID } : {}), + }; + } return { behavior: "deny", message: `Tool "${toolName}" is not in the resolved execution allowlist.`, @@ -1379,6 +1401,7 @@ export class PipelineExecutor { return { behavior: "allow", + updatedInput: toolInput, ...(toolUseID ? { toolUseID } : {}), }; }; diff --git a/src/agents/session-lifecycle.ts b/src/agents/session-lifecycle.ts index 1904ef4..1cf064d 100644 --- a/src/agents/session-lifecycle.ts +++ b/src/agents/session-lifecycle.ts @@ -358,13 +358,16 @@ export class FileSystemSessionMetadataStore { export class SessionWorktreeManager { private readonly worktreeRoot: string; private readonly baseRef: string; + private readonly targetPath?: string; constructor(input: { worktreeRoot: string; baseRef: string; + targetPath?: string; }) { this.worktreeRoot = assertAbsolutePath(input.worktreeRoot, "worktreeRoot"); this.baseRef = assertNonEmptyString(input.baseRef, "baseRef"); + this.targetPath = normalizeWorktreeTargetPath(input.targetPath, "targetPath"); } resolveBaseWorkspacePath(sessionId: string): string { @@ -378,6 +381,11 @@ export class SessionWorktreeManager { return resolve(this.worktreeRoot, scopedSession, "tasks", scopedTask); } + resolveWorkingDirectoryForWorktree(worktreePath: string): string { + const normalizedWorktreePath = assertAbsolutePath(worktreePath, "worktreePath"); + return this.targetPath ? resolve(normalizedWorktreePath, this.targetPath) : normalizedWorktreePath; + } + private resolveBaseBranchName(sessionId: string): string { const scoped = sanitizeSegment(sessionId, "session"); return `ai-ops/${scoped}/base`; @@ -399,14 +407,13 @@ export class SessionWorktreeManager { await mkdir(dirname(baseWorkspacePath), { recursive: true }); - const alreadyExists = await pathExists(baseWorkspacePath); - if (alreadyExists) { - return; + if (!(await pathExists(baseWorkspacePath))) { + const repoRoot = await runGit(["-C", projectPath, "rev-parse", "--show-toplevel"]); + const branchName = this.resolveBaseBranchName(input.sessionId); + await runGit(["-C", repoRoot, "worktree", "add", "-B", branchName, baseWorkspacePath, this.baseRef]); } - const repoRoot = await runGit(["-C", projectPath, "rev-parse", "--show-toplevel"]); - const branchName = this.resolveBaseBranchName(input.sessionId); - await runGit(["-C", repoRoot, "worktree", "add", "-B", branchName, baseWorkspacePath, this.baseRef]); + await this.ensureWorktreeTargetPath(baseWorkspacePath); } async ensureTaskWorktree(input: { @@ -416,6 +423,7 @@ export class SessionWorktreeManager { existingWorktreePath?: string; }): Promise<{ taskWorktreePath: string; + taskWorkingDirectory: string; }> { const baseWorkspacePath = assertAbsolutePath(input.baseWorkspacePath, "baseWorkspacePath"); const maybeExisting = input.existingWorktreePath?.trim(); @@ -451,8 +459,10 @@ export class SessionWorktreeManager { if (addResult.exitCode !== 0) { const attachedAfterFailure = await this.findWorktreePathForBranch(baseWorkspacePath, branchName); if (attachedAfterFailure === worktreePath && (await pathExists(worktreePath))) { + const taskWorkingDirectory = await this.ensureWorktreeTargetPath(worktreePath); return { taskWorktreePath: worktreePath, + taskWorkingDirectory, }; } throw new Error( @@ -462,8 +472,10 @@ export class SessionWorktreeManager { } } + const taskWorkingDirectory = await this.ensureWorktreeTargetPath(worktreePath); return { taskWorktreePath: worktreePath, + taskWorkingDirectory, }; } @@ -780,4 +792,69 @@ export class SessionWorktreeManager { } return parseGitWorktreeRecords(result.stdout); } + + private async ensureWorktreeTargetPath(worktreePath: string): Promise { + if (this.targetPath) { + await runGit(["-C", worktreePath, "sparse-checkout", "init", "--cone"]); + await runGit(["-C", worktreePath, "sparse-checkout", "set", this.targetPath]); + } + + const workingDirectory = this.resolveWorkingDirectoryForWorktree(worktreePath); + let workingDirectoryStats; + try { + workingDirectoryStats = await stat(workingDirectory); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + if (this.targetPath) { + throw new Error( + `Configured worktree target path "${this.targetPath}" is not a directory in ref "${this.baseRef}".`, + ); + } + throw new Error(`Worktree path "${workingDirectory}" does not exist.`); + } + throw error; + } + + if (!workingDirectoryStats.isDirectory()) { + if (this.targetPath) { + throw new Error( + `Configured worktree target path "${this.targetPath}" is not a directory in ref "${this.baseRef}".`, + ); + } + throw new Error(`Worktree path "${workingDirectory}" is not a directory.`); + } + + return workingDirectory; + } +} + +function normalizeWorktreeTargetPath(value: string | undefined, key: string): string | undefined { + if (value === undefined) { + return undefined; + } + + const trimmed = value.trim(); + if (trimmed.length === 0) { + return undefined; + } + + const slashNormalized = trimmed.replace(/\\/g, "/"); + if (isAbsolute(slashNormalized) || /^[a-zA-Z]:\//.test(slashNormalized)) { + throw new Error(`${key} must be a relative path within the repository worktree.`); + } + + const normalizedSegments = slashNormalized + .split("/") + .map((segment) => segment.trim()) + .filter((segment) => segment.length > 0 && segment !== "."); + + if (normalizedSegments.some((segment) => segment === "..")) { + throw new Error(`${key} must not contain ".." path segments.`); + } + + if (normalizedSegments.length === 0) { + return undefined; + } + + return normalizedSegments.join("/"); } diff --git a/src/config.ts b/src/config.ts index 051c42f..785f6fe 100644 --- a/src/config.ts +++ b/src/config.ts @@ -16,6 +16,7 @@ export type ProviderRuntimeConfig = { anthropicApiKey?: string; claudeModel?: string; claudeCodePath?: string; + claudeMaxTurns: number; claudeObservability: ClaudeObservabilityRuntimeConfig; }; @@ -136,6 +137,8 @@ const DEFAULT_CLAUDE_OBSERVABILITY: ClaudeObservabilityRuntimeConfig = { debugLogPath: undefined, }; +const DEFAULT_CLAUDE_MAX_TURNS = 2; + function readOptionalString( env: NodeJS.ProcessEnv, key: string, @@ -401,6 +404,12 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Readonly { return out; } +function toCaseInsensitiveLookup(values: readonly string[]): Map { + const out = new Map(); + for (const value of values) { + const normalized = normalizeLookupToken(value); + if (!normalized || out.has(normalized)) { + continue; + } + out.set(normalized, value); + } + return out; +} + function toNow(): string { return new Date().toISOString(); } @@ -133,10 +150,14 @@ export class SecurityRulesEngine { private readonly blockedEnvAssignments: Set; private readonly worktreeRoot: string; private readonly protectedPaths: string[]; + private readonly violationHandling: SecurityViolationHandling; constructor( policy: ShellValidationPolicy, private readonly auditSink?: SecurityAuditSink, + options?: { + violationHandling?: SecurityViolationHandling; + }, ) { this.policy = parseShellValidationPolicy(policy); this.allowedBinaries = toToolSet(this.policy.allowedBinaries); @@ -144,6 +165,7 @@ export class SecurityRulesEngine { this.blockedEnvAssignments = toToolSet(this.policy.blockedEnvAssignments); this.worktreeRoot = resolve(this.policy.worktreeRoot); this.protectedPaths = this.policy.protectedPaths.map((path) => resolve(path)); + this.violationHandling = options?.violationHandling ?? "hard_abort"; } getPolicy(): ShellValidationPolicy { @@ -212,6 +234,15 @@ export class SecurityRulesEngine { code: error.code, details: error.details, }); + if (this.violationHandling === "dangerous_warn_only") { + return { + cwd: resolvedCwd, + parsed: { + commandCount: 0, + commands: [], + }, + }; + } throw error; } @@ -232,8 +263,11 @@ export class SecurityRulesEngine { }; }): void { const policy = parseToolClearancePolicy(input.toolClearance); + const normalizedTool = normalizeLookupToken(input.tool); + const banlistLookup = toCaseInsensitiveLookup(policy.banlist); + const allowlistLookup = toCaseInsensitiveLookup(policy.allowlist); - if (policy.banlist.includes(input.tool)) { + if (banlistLookup.has(normalizedTool)) { this.emit({ ...toAuditContext(input.context), type: "tool.invocation_blocked", @@ -252,7 +286,7 @@ export class SecurityRulesEngine { ); } - if (policy.allowlist.length > 0 && !policy.allowlist.includes(input.tool)) { + if (policy.allowlist.length > 0 && !allowlistLookup.has(normalizedTool)) { this.emit({ ...toAuditContext(input.context), type: "tool.invocation_blocked", @@ -280,13 +314,15 @@ export class SecurityRulesEngine { filterAllowedTools(tools: string[], toolClearance: ToolClearancePolicy): string[] { const policy = parseToolClearancePolicy(toolClearance); + const allowlistLookup = toCaseInsensitiveLookup(policy.allowlist); + const banlistLookup = toCaseInsensitiveLookup(policy.banlist); const allowedByAllowlist = policy.allowlist.length === 0 ? tools - : tools.filter((tool) => policy.allowlist.includes(tool)); + : tools.filter((tool) => allowlistLookup.has(normalizeLookupToken(tool))); - return allowedByAllowlist.filter((tool) => !policy.banlist.includes(tool)); + return allowedByAllowlist.filter((tool) => !banlistLookup.has(normalizeLookupToken(tool))); } private assertCwdBoundary(cwd: string): void { diff --git a/src/security/schemas.ts b/src/security/schemas.ts index d7a3437..a1e9662 100644 --- a/src/security/schemas.ts +++ b/src/security/schemas.ts @@ -157,11 +157,15 @@ export function parseParsedShellScript(input: unknown): ParsedShellScript { }; } -export type SecurityViolationHandling = "hard_abort" | "validation_fail"; +export type SecurityViolationHandling = + | "hard_abort" + | "validation_fail" + | "dangerous_warn_only"; export const securityViolationHandlingSchema = z.union([ z.literal("hard_abort"), z.literal("validation_fail"), + z.literal("dangerous_warn_only"), ]); export function parseSecurityViolationHandling(input: unknown): SecurityViolationHandling { diff --git a/src/ui/config-store.ts b/src/ui/config-store.ts index fd245a9..bdd3766 100644 --- a/src/ui/config-store.ts +++ b/src/ui/config-store.ts @@ -1,5 +1,6 @@ import { resolve } from "node:path"; import { loadConfig, type AppConfig } from "../config.js"; +import type { SecurityViolationHandling } from "../security/index.js"; import { parseEnvFile, writeEnvFileUpdates } from "./env-store.js"; export type RuntimeNotificationSettings = { @@ -9,7 +10,7 @@ export type RuntimeNotificationSettings = { }; export type SecurityPolicySettings = { - violationMode: "hard_abort" | "validation_fail"; + violationMode: SecurityViolationHandling; allowedBinaries: string[]; commandTimeoutMs: number; inheritedEnv: string[]; diff --git a/src/ui/provider-executor.ts b/src/ui/provider-executor.ts index c43ddf3..2633e5f 100644 --- a/src/ui/provider-executor.ts +++ b/src/ui/provider-executor.ts @@ -9,7 +9,6 @@ import { import { isDomainEventType, type DomainEventEmission } from "../agents/domain-events.js"; import type { ActorExecutionInput, ActorExecutionResult, ActorExecutor } from "../agents/pipeline.js"; import { isRecord, type JsonObject, type JsonValue } from "../agents/types.js"; -import { createSessionContext, type SessionContext } from "../examples/session-context.js"; import { ClaudeObservabilityLogger } from "./claude-observability.js"; export type RunProvider = "codex" | "claude"; @@ -17,7 +16,7 @@ export type RunProvider = "codex" | "claude"; export type ProviderRunRuntime = { provider: RunProvider; config: Readonly; - sessionContext: SessionContext; + sharedEnv: Record; claudeObservability: ClaudeObservabilityLogger; close: () => Promise; }; @@ -30,6 +29,16 @@ type ProviderUsage = { costUsd?: number; }; +function sanitizeEnv(input: Record): Record { + const output: Record = {}; + for (const [key, value] of Object.entries(input)) { + if (typeof value === "string") { + output[key] = value; + } + } + return output; +} + const ACTOR_RESPONSE_SCHEMA = { type: "object", additionalProperties: true, @@ -74,8 +83,6 @@ const CLAUDE_OUTPUT_FORMAT = { schema: ACTOR_RESPONSE_SCHEMA, } as const; -const CLAUDE_PROVIDER_MAX_TURNS = 2; - function toErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; @@ -83,6 +90,23 @@ function toErrorMessage(error: unknown): string { return String(error); } +export function resolveProviderWorkingDirectory(actorInput: ActorExecutionInput): string { + return actorInput.executionContext.security.worktreePath; +} + +export function buildProviderRuntimeEnv(input: { + runtime: ProviderRunRuntime; + actorInput: ActorExecutionInput; + includeClaudeAuth?: boolean; +}): Record { + const workingDirectory = resolveProviderWorkingDirectory(input.actorInput); + return sanitizeEnv({ + ...input.runtime.sharedEnv, + ...(input.includeClaudeAuth ? buildClaudeAuthEnv(input.runtime.config.provider) : {}), + AGENT_WORKTREE_PATH: workingDirectory, + }); +} + function toJsonValue(value: unknown): JsonValue { return JSON.parse(JSON.stringify(value)) as JsonValue; } @@ -367,6 +391,7 @@ async function runCodexActor(input: { const prompt = buildActorPrompt(actorInput); const startedAt = Date.now(); const apiKey = resolveOpenAiApiKey(runtime.config.provider); + const workingDirectory = resolveProviderWorkingDirectory(actorInput); const codex = new Codex({ ...(apiKey ? { apiKey } : {}), @@ -376,20 +401,21 @@ async function runCodexActor(input: { ...(actorInput.mcp.resolvedConfig.codexConfig ? { config: actorInput.mcp.resolvedConfig.codexConfig } : {}), - env: runtime.sessionContext.runtimeInjection.env, + env: buildProviderRuntimeEnv({ + runtime, + actorInput, + }), }); const thread = codex.startThread({ - workingDirectory: runtime.sessionContext.runtimeInjection.workingDirectory, + workingDirectory, skipGitRepoCheck: runtime.config.provider.codexSkipGitCheck, }); - const turn = await runtime.sessionContext.runInSession(() => - thread.run(prompt, { - signal: actorInput.signal, - outputSchema: ACTOR_RESPONSE_SCHEMA, - }), - ); + const turn = await thread.run(prompt, { + signal: actorInput.signal, + outputSchema: ACTOR_RESPONSE_SCHEMA, + }); const usage: ProviderUsage = { ...(turn.usage @@ -457,6 +483,7 @@ function buildClaudeOptions(input: { actorInput: ActorExecutionInput; }): Options { const { runtime, actorInput } = input; + const workingDirectory = resolveProviderWorkingDirectory(actorInput); const authOptionOverrides = runtime.config.provider.anthropicOauthToken ? { authToken: runtime.config.provider.anthropicOauthToken } @@ -465,14 +492,15 @@ function buildClaudeOptions(input: { return token ? { apiKey: token } : {}; })(); - const runtimeEnv = { - ...runtime.sessionContext.runtimeInjection.env, - ...buildClaudeAuthEnv(runtime.config.provider), - }; + const runtimeEnv = buildProviderRuntimeEnv({ + runtime, + actorInput, + includeClaudeAuth: true, + }); const traceContext = toClaudeTraceContext(actorInput); return { - maxTurns: CLAUDE_PROVIDER_MAX_TURNS, + maxTurns: runtime.config.provider.claudeMaxTurns, ...(runtime.config.provider.claudeModel ? { model: runtime.config.provider.claudeModel } : {}), @@ -484,7 +512,7 @@ function buildClaudeOptions(input: { ? { mcpServers: actorInput.mcp.resolvedConfig.claudeMcpServers as Options["mcpServers"] } : {}), canUseTool: actorInput.mcp.createClaudeCanUseTool(), - cwd: runtime.sessionContext.runtimeInjection.workingDirectory, + cwd: workingDirectory, env: runtimeEnv, ...runtime.claudeObservability.toOptionOverrides({ context: traceContext, @@ -507,8 +535,8 @@ async function runClaudeTurn(input: { context: traceContext, data: { ...(options.model ? { model: options.model } : {}), - maxTurns: options.maxTurns ?? CLAUDE_PROVIDER_MAX_TURNS, - cwd: input.runtime.sessionContext.runtimeInjection.workingDirectory, + maxTurns: options.maxTurns ?? input.runtime.config.provider.claudeMaxTurns, + ...(typeof options.cwd === "string" ? { cwd: options.cwd } : {}), }, }); @@ -605,13 +633,11 @@ async function runClaudeActor(input: { actorInput: ActorExecutionInput; }): Promise { const prompt = buildActorPrompt(input.actorInput); - const turn = await input.runtime.sessionContext.runInSession(() => - runClaudeTurn({ - runtime: input.runtime, - actorInput: input.actorInput, - prompt, - }), - ); + const turn = await runClaudeTurn({ + runtime: input.runtime, + actorInput: input.actorInput, + prompt, + }); const parsed = parseActorExecutionResultFromModelOutput({ rawText: turn.text, @@ -626,33 +652,21 @@ async function runClaudeActor(input: { export async function createProviderRunRuntime(input: { provider: RunProvider; - initialPrompt: string; config: Readonly; - projectPath: string; observabilityRootPath?: string; + baseEnv?: Record; }): Promise { - const sessionContext = await createSessionContext(input.provider, { - prompt: input.initialPrompt, - config: input.config, - workspaceRoot: input.projectPath, - }); const claudeObservability = new ClaudeObservabilityLogger({ - workspaceRoot: input.observabilityRootPath ?? input.projectPath, + workspaceRoot: input.observabilityRootPath ?? process.cwd(), config: input.config.provider.claudeObservability, }); return { provider: input.provider, config: input.config, - sessionContext, + sharedEnv: sanitizeEnv(input.baseEnv ?? process.env), claudeObservability, - close: async () => { - try { - await sessionContext.close(); - } finally { - await claudeObservability.close(); - } - }, + close: async () => claudeObservability.close(), }; } diff --git a/src/ui/public/index.html b/src/ui/public/index.html index 7a48404..23693e2 100644 --- a/src/ui/public/index.html +++ b/src/ui/public/index.html @@ -202,6 +202,7 @@