a
This commit is contained in:
23
.ai_ops_mock_failure/events/runtime-events.ndjson
Normal file
23
.ai_ops_mock_failure/events/runtime-events.ndjson
Normal file
@@ -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}}
|
||||
385
.ai_ops_mock_failure/manifests/structured-dev-workflow.json
Normal file
385
.ai_ops_mock_failure/manifests/structured-dev-workflow.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# ECOSWAP
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<string, string | number | boolean>;
|
||||
};
|
||||
|
||||
@@ -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<PipelineRunSummary> {
|
||||
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 } : {}),
|
||||
|
||||
@@ -63,6 +63,7 @@ export type ActorExecutionResult = {
|
||||
export type ActorToolPermissionResult =
|
||||
| {
|
||||
behavior: "allow";
|
||||
updatedInput?: Record<string, unknown>;
|
||||
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 } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<string> {
|
||||
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("/");
|
||||
}
|
||||
|
||||
@@ -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<AppCo
|
||||
anthropicApiKey,
|
||||
claudeModel: normalizeClaudeModel(readOptionalString(env, "CLAUDE_MODEL")),
|
||||
claudeCodePath: readOptionalString(env, "CLAUDE_CODE_PATH"),
|
||||
claudeMaxTurns: readIntegerWithBounds(
|
||||
env,
|
||||
"CLAUDE_MAX_TURNS",
|
||||
DEFAULT_CLAUDE_MAX_TURNS,
|
||||
{ min: 1 },
|
||||
),
|
||||
claudeObservability: {
|
||||
mode: parseClaudeObservabilityMode(
|
||||
readStringWithFallback(
|
||||
|
||||
@@ -19,7 +19,7 @@ function requiredPrompt(argv: string[]): string {
|
||||
|
||||
function buildOptions(config = getConfig()): Options {
|
||||
return {
|
||||
maxTurns: 1,
|
||||
maxTurns: config.provider.claudeMaxTurns,
|
||||
...(config.provider.claudeModel ? { model: config.provider.claudeModel } : {}),
|
||||
...(config.provider.claudeCodePath
|
||||
? { pathToClaudeCodeExecutable: config.provider.claudeCodePath }
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import {
|
||||
parseShellValidationPolicy,
|
||||
parseToolClearancePolicy,
|
||||
type SecurityViolationHandling,
|
||||
type ShellValidationPolicy,
|
||||
type ToolClearancePolicy,
|
||||
} from "./schemas.js";
|
||||
@@ -62,6 +63,10 @@ function normalizeToken(value: string): string {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function normalizeLookupToken(value: string): string {
|
||||
return normalizeToken(value).toLowerCase();
|
||||
}
|
||||
|
||||
function hasPathTraversalSegment(token: string): boolean {
|
||||
const normalized = token.replaceAll("\\", "/");
|
||||
if (normalized === ".." || normalized.startsWith("../") || normalized.endsWith("/..")) {
|
||||
@@ -100,6 +105,18 @@ function toToolSet(values: readonly string[]): Set<string> {
|
||||
return out;
|
||||
}
|
||||
|
||||
function toCaseInsensitiveLookup(values: readonly string[]): Map<string, string> {
|
||||
const out = new Map<string, string>();
|
||||
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<string>;
|
||||
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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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<AppConfig>;
|
||||
sessionContext: SessionContext;
|
||||
sharedEnv: Record<string, string>;
|
||||
claudeObservability: ClaudeObservabilityLogger;
|
||||
close: () => Promise<void>;
|
||||
};
|
||||
@@ -30,6 +29,16 @@ type ProviderUsage = {
|
||||
costUsd?: number;
|
||||
};
|
||||
|
||||
function sanitizeEnv(input: Record<string, string | undefined>): Record<string, string> {
|
||||
const output: Record<string, string> = {};
|
||||
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<string, string> {
|
||||
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<ActorExecutionResult> {
|
||||
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<AppConfig>;
|
||||
projectPath: string;
|
||||
observabilityRootPath?: string;
|
||||
baseEnv?: Record<string, string | undefined>;
|
||||
}): Promise<ProviderRunRuntime> {
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
<select id="cfg-security-mode">
|
||||
<option value="hard_abort">hard_abort</option>
|
||||
<option value="validation_fail">validation_fail</option>
|
||||
<option value="dangerous_warn_only">dangerous_warn_only</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
|
||||
@@ -359,6 +359,7 @@ export class UiRunService {
|
||||
worktreeManager: new SessionWorktreeManager({
|
||||
worktreeRoot: paths.worktreeRoot,
|
||||
baseRef: config.provisioning.gitWorktree.baseRef,
|
||||
targetPath: config.provisioning.gitWorktree.targetPath,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -485,10 +486,9 @@ export class UiRunService {
|
||||
if (executionMode === "provider") {
|
||||
providerRuntime = await createProviderRunRuntime({
|
||||
provider,
|
||||
initialPrompt: input.prompt,
|
||||
config,
|
||||
projectPath: session?.baseWorkspacePath ?? this.workspaceRoot,
|
||||
observabilityRootPath: this.workspaceRoot,
|
||||
baseEnv: process.env,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ test("loads defaults and freezes config", () => {
|
||||
"session.failed",
|
||||
]);
|
||||
assert.equal(config.provider.openAiAuthMode, "auto");
|
||||
assert.equal(config.provider.claudeMaxTurns, 2);
|
||||
assert.equal(config.provider.claudeObservability.mode, "off");
|
||||
assert.equal(config.provider.claudeObservability.verbosity, "summary");
|
||||
assert.equal(config.provider.claudeObservability.logPath, ".ai_ops/events/claude-trace.ndjson");
|
||||
@@ -55,6 +56,11 @@ test("validates security violation mode", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("loads dangerous_warn_only security violation mode", () => {
|
||||
const config = loadConfig({ AGENT_SECURITY_VIOLATION_MODE: "dangerous_warn_only" });
|
||||
assert.equal(config.security.violationHandling, "dangerous_warn_only");
|
||||
});
|
||||
|
||||
test("validates runtime discord severity mode", () => {
|
||||
assert.throws(
|
||||
() => loadConfig({ AGENT_RUNTIME_DISCORD_MIN_SEVERITY: "verbose" }),
|
||||
@@ -69,6 +75,13 @@ test("validates claude observability mode", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("validates CLAUDE_MAX_TURNS bounds", () => {
|
||||
assert.throws(
|
||||
() => loadConfig({ CLAUDE_MAX_TURNS: "0" }),
|
||||
/CLAUDE_MAX_TURNS must be an integer >= 1/,
|
||||
);
|
||||
});
|
||||
|
||||
test("validates claude observability verbosity", () => {
|
||||
assert.throws(
|
||||
() => loadConfig({ CLAUDE_OBSERVABILITY_VERBOSITY: "verbose" }),
|
||||
|
||||
@@ -380,6 +380,7 @@ test("injects resolved mcp/helpers and enforces Claude tool gate in actor execut
|
||||
);
|
||||
assert.deepEqual(allow, {
|
||||
behavior: "allow",
|
||||
updatedInput: {},
|
||||
toolUseID: "allow-1",
|
||||
});
|
||||
|
||||
@@ -997,6 +998,7 @@ test("createClaudeCanUseTool accepts tool casing differences from providers", as
|
||||
});
|
||||
assert.deepEqual(allow, {
|
||||
behavior: "allow",
|
||||
updatedInput: {},
|
||||
toolUseID: "allow-bash",
|
||||
});
|
||||
|
||||
@@ -1020,6 +1022,88 @@ test("createClaudeCanUseTool accepts tool casing differences from providers", as
|
||||
assert.equal(result.status, "success");
|
||||
});
|
||||
|
||||
test("dangerous_warn_only allows tool use outside persona allowlist", async () => {
|
||||
const workspaceRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-workspace-"));
|
||||
const stateRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-session-state-"));
|
||||
const projectContextPath = resolve(stateRoot, "project-context.json");
|
||||
|
||||
const manifest = {
|
||||
schemaVersion: "1",
|
||||
topologies: ["sequential"],
|
||||
personas: [
|
||||
{
|
||||
id: "reader",
|
||||
displayName: "Reader",
|
||||
systemPromptTemplate: "Reader",
|
||||
toolClearance: {
|
||||
allowlist: ["read_file"],
|
||||
banlist: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
relationships: [],
|
||||
topologyConstraints: {
|
||||
maxDepth: 2,
|
||||
maxRetries: 0,
|
||||
},
|
||||
pipeline: {
|
||||
entryNodeId: "warn-node",
|
||||
nodes: [
|
||||
{
|
||||
id: "warn-node",
|
||||
actorId: "warn_actor",
|
||||
personaId: "reader",
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
},
|
||||
} as const;
|
||||
|
||||
const engine = new SchemaDrivenExecutionEngine({
|
||||
manifest,
|
||||
settings: {
|
||||
workspaceRoot,
|
||||
stateRoot,
|
||||
projectContextPath,
|
||||
maxChildren: 1,
|
||||
maxDepth: 2,
|
||||
maxRetries: 0,
|
||||
securityViolationHandling: "dangerous_warn_only",
|
||||
runtimeContext: {},
|
||||
},
|
||||
actorExecutors: {
|
||||
warn_actor: async (input) => {
|
||||
const canUseTool = input.mcp.createClaudeCanUseTool();
|
||||
const allow = await canUseTool("Bash", {}, {
|
||||
signal: new AbortController().signal,
|
||||
toolUseID: "allow-bash-warn",
|
||||
});
|
||||
assert.deepEqual(allow, {
|
||||
behavior: "allow",
|
||||
updatedInput: {},
|
||||
toolUseID: "allow-bash-warn",
|
||||
});
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
payload: {
|
||||
ok: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await engine.runSession({
|
||||
sessionId: "session-dangerous-warn-only",
|
||||
initialPayload: {
|
||||
task: "verify warn-only bypass",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, "success");
|
||||
});
|
||||
|
||||
test("hard-aborts pipeline on security violations by default", async () => {
|
||||
const workspaceRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-workspace-"));
|
||||
const stateRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-session-state-"));
|
||||
|
||||
@@ -160,6 +160,7 @@ test("runClaudePrompt wires auth env, stream parsing, and output", async () => {
|
||||
ANTHROPIC_API_KEY: "legacy-api-key",
|
||||
CLAUDE_MODEL: "claude-sonnet-4-6",
|
||||
CLAUDE_CODE_PATH: "/usr/local/bin/claude",
|
||||
CLAUDE_MAX_TURNS: "5",
|
||||
});
|
||||
|
||||
let closed = false;
|
||||
@@ -229,6 +230,7 @@ test("runClaudePrompt wires auth env, stream parsing, and output", async () => {
|
||||
assert.equal(queryInput?.prompt, "augmented prompt");
|
||||
assert.equal(queryInput?.options?.model, "claude-sonnet-4-6");
|
||||
assert.equal(queryInput?.options?.pathToClaudeCodeExecutable, "/usr/local/bin/claude");
|
||||
assert.equal(queryInput?.options?.maxTurns, 5);
|
||||
assert.equal(queryInput?.options?.cwd, "/tmp/claude-worktree");
|
||||
assert.equal(queryInput?.options?.authToken, "oauth-token");
|
||||
assert.deepEqual(queryInput?.options?.mcpServers, sessionContext.mcp.claudeMcpServers);
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseActorExecutionResultFromModelOutput } from "../src/ui/provider-executor.js";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import type { ActorExecutionInput } from "../src/agents/pipeline.js";
|
||||
import {
|
||||
buildProviderRuntimeEnv,
|
||||
createProviderRunRuntime,
|
||||
parseActorExecutionResultFromModelOutput,
|
||||
resolveProviderWorkingDirectory,
|
||||
type ProviderRunRuntime,
|
||||
} from "../src/ui/provider-executor.js";
|
||||
|
||||
test("parseActorExecutionResultFromModelOutput parses strict JSON payload", () => {
|
||||
const parsed = parseActorExecutionResultFromModelOutput({
|
||||
@@ -64,3 +75,71 @@ test("parseActorExecutionResultFromModelOutput falls back when response is not J
|
||||
assert.equal(parsed.status, "success");
|
||||
assert.equal(parsed.payload?.assistantResponse, "Implemented update successfully.");
|
||||
});
|
||||
|
||||
test("resolveProviderWorkingDirectory reads cwd from actor execution context", () => {
|
||||
const actorInput = {
|
||||
executionContext: {
|
||||
security: {
|
||||
worktreePath: "/tmp/session/tasks/product-intake",
|
||||
},
|
||||
},
|
||||
} as unknown as ActorExecutionInput;
|
||||
|
||||
assert.equal(
|
||||
resolveProviderWorkingDirectory(actorInput),
|
||||
"/tmp/session/tasks/product-intake",
|
||||
);
|
||||
});
|
||||
|
||||
test("buildProviderRuntimeEnv scopes AGENT_WORKTREE_PATH to actor worktree and filters undefined auth", () => {
|
||||
const config = loadConfig({
|
||||
CLAUDE_CODE_OAUTH_TOKEN: "oauth-token",
|
||||
});
|
||||
const runtime = {
|
||||
provider: "claude",
|
||||
config,
|
||||
sharedEnv: {
|
||||
PATH: "/usr/bin",
|
||||
KEEP_ME: "1",
|
||||
},
|
||||
claudeObservability: {} as ProviderRunRuntime["claudeObservability"],
|
||||
close: async () => {},
|
||||
} satisfies ProviderRunRuntime;
|
||||
const actorInput = {
|
||||
executionContext: {
|
||||
security: {
|
||||
worktreePath: "/tmp/session/tasks/product-intake",
|
||||
},
|
||||
},
|
||||
} as unknown as ActorExecutionInput;
|
||||
|
||||
const env = buildProviderRuntimeEnv({
|
||||
runtime,
|
||||
actorInput,
|
||||
includeClaudeAuth: true,
|
||||
});
|
||||
|
||||
assert.equal(env.AGENT_WORKTREE_PATH, "/tmp/session/tasks/product-intake");
|
||||
assert.equal(env.CLAUDE_CODE_OAUTH_TOKEN, "oauth-token");
|
||||
assert.equal("ANTHROPIC_API_KEY" in env, false);
|
||||
assert.equal(env.KEEP_ME, "1");
|
||||
});
|
||||
|
||||
test("createProviderRunRuntime does not require session context provisioning", async () => {
|
||||
const observabilityRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-provider-runtime-"));
|
||||
const runtime = await createProviderRunRuntime({
|
||||
provider: "claude",
|
||||
config: loadConfig({}),
|
||||
observabilityRootPath: observabilityRoot,
|
||||
baseEnv: {
|
||||
PATH: "/usr/bin",
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
assert.equal(runtime.provider, "claude");
|
||||
assert.equal(runtime.sharedEnv.PATH, "/usr/bin");
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -111,6 +111,42 @@ test("rules engine enforces binary allowlist, tool policy, and path boundaries",
|
||||
);
|
||||
});
|
||||
|
||||
test("rules engine dangerous_warn_only logs but does not block violating shell commands", async () => {
|
||||
const worktreeRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-security-warn-worktree-"));
|
||||
const stateRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-security-warn-state-"));
|
||||
const projectContextPath = resolve(stateRoot, "project-context.json");
|
||||
|
||||
const rules = new SecurityRulesEngine(
|
||||
{
|
||||
allowedBinaries: ["git"],
|
||||
worktreeRoot,
|
||||
protectedPaths: [stateRoot, projectContextPath],
|
||||
requireCwdWithinWorktree: true,
|
||||
rejectRelativePathTraversal: true,
|
||||
enforcePathBoundaryOnArguments: true,
|
||||
allowedEnvAssignments: [],
|
||||
blockedEnvAssignments: [],
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
violationHandling: "dangerous_warn_only",
|
||||
},
|
||||
);
|
||||
|
||||
const validated = await rules.validateShellCommand({
|
||||
command: "unauthorized_bin --version",
|
||||
cwd: worktreeRoot,
|
||||
toolClearance: {
|
||||
allowlist: ["git"],
|
||||
banlist: [],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(validated.cwd, worktreeRoot);
|
||||
assert.equal(validated.parsed.commandCount, 0);
|
||||
assert.deepEqual(validated.parsed.commands, []);
|
||||
});
|
||||
|
||||
test("secure executor runs with explicit env policy", async () => {
|
||||
const worktreeRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-security-exec-"));
|
||||
|
||||
@@ -193,3 +229,47 @@ test("rules engine carries session context in tool audit events", () => {
|
||||
assert.equal(allowedEvent.nodeId, "node-ctx");
|
||||
assert.equal(allowedEvent.attempt, 2);
|
||||
});
|
||||
|
||||
test("rules engine applies tool clearance matching case-insensitively", () => {
|
||||
const rules = new SecurityRulesEngine({
|
||||
allowedBinaries: ["git"],
|
||||
worktreeRoot: "/tmp",
|
||||
protectedPaths: [],
|
||||
requireCwdWithinWorktree: true,
|
||||
rejectRelativePathTraversal: true,
|
||||
enforcePathBoundaryOnArguments: true,
|
||||
allowedEnvAssignments: [],
|
||||
blockedEnvAssignments: [],
|
||||
});
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
rules.assertToolInvocationAllowed({
|
||||
tool: "Bash",
|
||||
toolClearance: {
|
||||
allowlist: ["bash", "glob"],
|
||||
banlist: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
rules.assertToolInvocationAllowed({
|
||||
tool: "Glob",
|
||||
toolClearance: {
|
||||
allowlist: ["bash", "glob"],
|
||||
banlist: ["GLOB"],
|
||||
},
|
||||
}),
|
||||
(error: unknown) =>
|
||||
error instanceof SecurityViolationError && error.code === "TOOL_BANNED",
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
rules.filterAllowedTools(["Bash", "Glob", "Read"], {
|
||||
allowlist: ["bash", "glob"],
|
||||
banlist: ["gLoB"],
|
||||
}),
|
||||
["Bash"],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -228,3 +228,60 @@ test("session worktree manager recreates a task worktree after stale metadata pr
|
||||
const stats = await stat(recreatedTaskWorktreePath);
|
||||
assert.equal(stats.isDirectory(), true);
|
||||
});
|
||||
|
||||
test("session worktree manager applies target path sparse checkout and task working directory", async () => {
|
||||
const root = await mkdtemp(resolve(tmpdir(), "ai-ops-session-worktree-target-"));
|
||||
const projectPath = resolve(root, "project");
|
||||
const worktreeRoot = resolve(root, "worktrees");
|
||||
|
||||
await mkdir(resolve(projectPath, "app", "src"), { recursive: true });
|
||||
await mkdir(resolve(projectPath, "infra"), { recursive: true });
|
||||
await git(["init", projectPath]);
|
||||
await git(["-C", projectPath, "config", "user.name", "AI Ops"]);
|
||||
await git(["-C", projectPath, "config", "user.email", "ai-ops@example.local"]);
|
||||
await writeFile(resolve(projectPath, "app", "src", "index.ts"), "export const app = true;\n", "utf8");
|
||||
await writeFile(resolve(projectPath, "infra", "notes.txt"), "infra\n", "utf8");
|
||||
await git(["-C", projectPath, "add", "."]);
|
||||
await git(["-C", projectPath, "commit", "-m", "initial commit"]);
|
||||
|
||||
const manager = new SessionWorktreeManager({
|
||||
worktreeRoot,
|
||||
baseRef: "HEAD",
|
||||
targetPath: "app",
|
||||
});
|
||||
|
||||
const sessionId = "session-target-1";
|
||||
const baseWorkspacePath = manager.resolveBaseWorkspacePath(sessionId);
|
||||
await manager.initializeSessionBaseWorkspace({
|
||||
sessionId,
|
||||
projectPath,
|
||||
baseWorkspacePath,
|
||||
});
|
||||
|
||||
const baseWorkingDirectory = manager.resolveWorkingDirectoryForWorktree(baseWorkspacePath);
|
||||
assert.equal(baseWorkingDirectory, resolve(baseWorkspacePath, "app"));
|
||||
const baseWorkingStats = await stat(baseWorkingDirectory);
|
||||
assert.equal(baseWorkingStats.isDirectory(), true);
|
||||
await assert.rejects(() => stat(resolve(baseWorkspacePath, "infra")), {
|
||||
code: "ENOENT",
|
||||
});
|
||||
|
||||
const ensured = await manager.ensureTaskWorktree({
|
||||
sessionId,
|
||||
taskId: "task-target-1",
|
||||
baseWorkspacePath,
|
||||
});
|
||||
assert.equal(ensured.taskWorkingDirectory, resolve(ensured.taskWorktreePath, "app"));
|
||||
|
||||
await writeFile(resolve(ensured.taskWorkingDirectory, "src", "feature.ts"), "export const feature = true;\n", "utf8");
|
||||
|
||||
const mergeOutcome = await manager.mergeTaskIntoBase({
|
||||
taskId: "task-target-1",
|
||||
baseWorkspacePath,
|
||||
taskWorktreePath: ensured.taskWorktreePath,
|
||||
});
|
||||
assert.equal(mergeOutcome.kind, "success");
|
||||
|
||||
const merged = await readFile(resolve(baseWorkingDirectory, "src", "feature.ts"), "utf8");
|
||||
assert.equal(merged, "export const feature = true;\n");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user