This commit is contained in:
2026-02-24 18:57:20 -05:00
parent 45374a033b
commit 7727612ce9
36 changed files with 1331 additions and 70 deletions

View 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}}

View 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"
}
]
}
}

View File

@@ -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 users \"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"
}

View File

@@ -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 users \"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"
}

View File

@@ -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 users \"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"
}

View File

@@ -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 users \"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"
}

View File

@@ -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 users \"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"
}

View File

@@ -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 users \"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"
}

View File

@@ -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 users \"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"
}

View File

@@ -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 users \"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"
}

View File

@@ -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"
}
}
}
]
}

View File

@@ -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"
}

View File

@@ -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"
}
}
}
]
}

View File

@@ -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"
}

View File

@@ -0,0 +1 @@
# ECOSWAP

View File

@@ -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

View File

@@ -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`

View File

@@ -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.

View File

@@ -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

View File

@@ -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 } : {}),

View File

@@ -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 } : {}),
};
};

View File

@@ -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("/");
}

View File

@@ -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(

View File

@@ -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 }

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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[];

View File

@@ -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(),
};
}

View File

@@ -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>

View File

@@ -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,
});
}

View File

@@ -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" }),

View File

@@ -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-"));

View File

@@ -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);

View File

@@ -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();
}
});

View File

@@ -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"],
);
});

View File

@@ -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");
});