diff --git a/.opc/config/company_orgs/org_corporate_config.yaml b/.opc/config/company_orgs/org_corporate_config.yaml index 3d617a3..f6f1b52 100644 --- a/.opc/config/company_orgs/org_corporate_config.yaml +++ b/.opc/config/company_orgs/org_corporate_config.yaml @@ -70,6 +70,8 @@ roles: - web_search - web_fetch - shell_exec + - file_write + - file_edit preferred_external_agent: null prompt_refs: - Focus on technical feasibility, architecture quality, and engineering best practices. @@ -113,6 +115,8 @@ roles: - browser_wait_for - browser_scroll - browser_take_screenshot + - file_write + - file_edit preferred_external_agent: null prompt_refs: - Optimize for audience fit, brand consistency, and content quality. @@ -157,6 +161,8 @@ roles: - browser_wait_for - browser_scroll - browser_take_screenshot + - file_write + - file_edit preferred_external_agent: null prompt_refs: - Ensure operational efficiency, process compliance, and delivery quality. @@ -205,6 +211,7 @@ roles: - browser_select_option - browser_take_screenshot - browser_close + - file_edit preferred_external_agent: claude_code prompt_refs: - 'Run data acquisition in four phases: Discover, Verify, Prepare, Report.' @@ -440,6 +447,7 @@ roles: - browser_scroll - browser_select_option - browser_take_screenshot + - file_edit preferred_external_agent: null prompt_refs: - Test rigorously. Reject unclear or unsafe outputs. diff --git a/.opc/config/company_orgs/org_game-development-studio_config.yaml b/.opc/config/company_orgs/org_game-development-studio_config.yaml index 3d28e73..1877b95 100644 --- a/.opc/config/company_orgs/org_game-development-studio_config.yaml +++ b/.opc/config/company_orgs/org_game-development-studio_config.yaml @@ -32,6 +32,8 @@ roles: - todo_read - web_search - web_fetch + - file_write + - file_edit preferred_external_agent: codex prompt_refs: - You are the Studio Director. You are the owner-facing final decision role for @@ -79,6 +81,8 @@ roles: - todo_read - web_search - web_fetch + - file_write + - file_edit preferred_external_agent: claude_code prompt_refs: - You are the Game Designer. Turn the user's request into a buildable gameplay brief diff --git a/.opc/config/company_orgs/org_research-report-studio_config.yaml b/.opc/config/company_orgs/org_research-report-studio_config.yaml index fc9bdf1..885dfe3 100644 --- a/.opc/config/company_orgs/org_research-report-studio_config.yaml +++ b/.opc/config/company_orgs/org_research-report-studio_config.yaml @@ -31,6 +31,8 @@ roles: - todo_read - web_search - web_fetch + - file_write + - file_edit preferred_external_agent: claude_code prompt_refs: - You are the Chief Analyst — the owner-facing final decision role for a lean research @@ -81,6 +83,8 @@ roles: - todo_read - web_search - web_fetch + - file_write + - file_edit preferred_external_agent: claude_code prompt_refs: - You are the Researcher. Your job is to conduct thorough, evidence-based research diff --git a/.opc/config/company_orgs/org_vc-investment-firm_config.yaml b/.opc/config/company_orgs/org_vc-investment-firm_config.yaml index c3b96f2..43e2273 100644 --- a/.opc/config/company_orgs/org_vc-investment-firm_config.yaml +++ b/.opc/config/company_orgs/org_vc-investment-firm_config.yaml @@ -31,6 +31,8 @@ roles: - todo_read - web_search - web_fetch + - file_write + - file_edit preferred_external_agent: null prompt_refs: - 'You are the Managing Partner. Defines the investment mandate, target stage, sector @@ -83,6 +85,8 @@ roles: - todo_read - web_search - web_fetch + - file_write + - file_edit preferred_external_agent: null prompt_refs: - 'You are the Investment Director. Translates the user request into an investment @@ -137,6 +141,8 @@ roles: - browser_wait_for - browser_scroll - browser_take_screenshot + - file_write + - file_edit preferred_external_agent: null prompt_refs: - You are the Sector Analyst. Evaluates whether the target sector can compound over @@ -181,6 +187,8 @@ roles: - browser_wait_for - browser_scroll - browser_take_screenshot + - file_write + - file_edit preferred_external_agent: null prompt_refs: - You are the Market Researcher. Quantifies TAM/SAM/SOM, buyer budgets, growth rates, @@ -226,6 +234,8 @@ roles: - browser_wait_for - browser_scroll - browser_take_screenshot + - file_write + - file_edit preferred_external_agent: null prompt_refs: - You are the Startup Scout. Builds a broad candidate pool with founding date, headquarters, @@ -274,6 +284,8 @@ roles: - browser_wait_for - browser_scroll - browser_take_screenshot + - file_write + - file_edit preferred_external_agent: null prompt_refs: - You are the Competitive Analyst. Maps incumbents, startup competitors, substitute @@ -318,6 +330,8 @@ roles: - browser_wait_for - browser_scroll - browser_take_screenshot + - file_write + - file_edit preferred_external_agent: null prompt_refs: - You are the News & Signal Analyst. Tracks recent market signals, customer wins, @@ -360,6 +374,8 @@ roles: - todo_read - web_search - web_fetch + - file_write + - file_edit preferred_external_agent: null prompt_refs: - 'You are the Due Diligence Lead. Turns the shortlist into a diligence plan, coordinates @@ -460,6 +476,8 @@ roles: - browser_wait_for - browser_scroll - browser_take_screenshot + - file_write + - file_edit preferred_external_agent: claude_code prompt_refs: - You are the Business DD Analyst. Evaluates target customers, sales motion, pricing @@ -552,6 +570,8 @@ roles: - browser_wait_for - browser_scroll - browser_take_screenshot + - file_write + - file_edit preferred_external_agent: null prompt_refs: - You are the Risk & Legal Analyst. Finds privacy, open-source license, supply-chain, @@ -595,6 +615,8 @@ roles: - todo_read - web_search - web_fetch + - file_write + - file_edit preferred_external_agent: null prompt_refs: - 'You are the Investment Committee Chair. Runs structured bull-case and bear-case @@ -644,6 +666,8 @@ roles: - browser_wait_for - browser_scroll - browser_take_screenshot + - file_write + - file_edit preferred_external_agent: null prompt_refs: - You are the Bull Case Reviewer. Builds the strongest evidence-backed case for @@ -688,6 +712,8 @@ roles: - browser_wait_for - browser_scroll - browser_take_screenshot + - file_write + - file_edit preferred_external_agent: null prompt_refs: - You are the Bear Case Reviewer. Builds the strongest evidence-backed case against @@ -726,6 +752,8 @@ roles: - todo_read - web_search - web_fetch + - file_write + - file_edit preferred_external_agent: null prompt_refs: - You are the Final Decision Reviewer. Reconciles bull and bear cases, ranks companies, @@ -768,6 +796,8 @@ roles: - todo_read - web_search - web_fetch + - file_write + - file_edit preferred_external_agent: null prompt_refs: - 'You are the Report & Delivery Lead. Owns final deliverables: investment memo, diff --git a/opc/engine.py b/opc/engine.py index 2696acc..574273b 100644 --- a/opc/engine.py +++ b/opc/engine.py @@ -9953,8 +9953,16 @@ class OPCEngine: else: resolved = False wait = dict(task.metadata.get("peer_wait", {})) - if wait.get("kind") == "meeting": + wait_kind = str(wait.get("kind") or "") + if wait_kind == "meeting": resolved = await self.communication.resolve_task_meeting_wait(task) + elif wait_kind == "comms_blocking" or not wait: + # Comms-blocking (and orphaned) waits resolve from durable + # inbox files owned by the company dispatcher's per-tick + # unpark. Re-enter the runtime and let it converge: it + # either releases the park or re-parks and re-checkpoints + # consistently. + resolved = True else: resolved = await self.communication.resolve_task_peer_wait(task) if not resolved: diff --git a/opc/layer2_organization/communication.py b/opc/layer2_organization/communication.py index 6196a21..a2789b1 100644 --- a/opc/layer2_organization/communication.py +++ b/opc/layer2_organization/communication.py @@ -1989,6 +1989,11 @@ class CommunicationManager: wait = dict(task.metadata.get("peer_wait", {})) if not wait: return False + if str(wait.get("kind") or "") == "comms_blocking": + # File-comms blocking waits are owned by the company + # dispatcher's per-tick unpark; resolving them here would flip + # task.status without the work-item phase and strand the run. + return False if wait.get("kind") != "meeting": msg_id = wait.get("message_id") reply = await self.store.get_latest_reply(msg_id) if msg_id else None diff --git a/opc/layer2_organization/company_mode.py b/opc/layer2_organization/company_mode.py index 8b69dc4..87dd656 100644 --- a/opc/layer2_organization/company_mode.py +++ b/opc/layer2_organization/company_mode.py @@ -4236,6 +4236,21 @@ class CompanyWorkItemExecutor: ) ] or list(self._active_tasks) self._active_tasks = tasks + # Consumer half of `_park_for_blocking_comms`: blocking + # replies arrive as durable inbox files, so each tick checks + # parked tasks and releases the ones whose replies are all + # present. In-flight tasks are skipped — their coroutine + # still owns the Task object and a late save_task would + # clobber the transition. + in_flight_task_ids = { + claimed.id for _member, claimed in active_work_item_tasks.values() + } + for parked in tasks: + if ( + parked.status == TaskStatus.AWAITING_PEER + and parked.id not in in_flight_task_ids + ): + await self._try_unpark_blocking_comms(parked) await self.runtime.refresh_inbox_state(tasks) work_items = await self._load_delegation_work_items(tasks) work_items = await self._refresh_ready_work_items(work_items, tasks=tasks) @@ -9225,7 +9240,13 @@ class CompanyWorkItemExecutor: if task.status != TaskStatus.AWAITING_PEER: return False peer_wait = dict(task.metadata.get("peer_wait", {}) or {}) - if str(peer_wait.get("kind") or "") != "comms_blocking": + wait_kind = str(peer_wait.get("kind") or "") + # An empty kind is an orphaned wait (e.g. a legacy resolver popped + # `peer_wait` while the work item stayed WAITING_FOR_PEER); those + # are recoverable from the durable comms state below. Waits with a + # different explicit kind (meeting, message-id) have their own + # resolvers. + if wait_kind and wait_kind != "comms_blocking": return False try: from opc.layer2_organization import comms as _comms @@ -9240,7 +9261,11 @@ class CompanyWorkItemExecutor: return False blocking_ids = list(peer_wait.get("blocking_message_ids", []) or []) if not blocking_ids: - # Nothing to wait for — defensively unpark. + # No recorded ids (orphaned or empty wait): fall back to the + # park predicate itself — any unanswered blocking outbox + # message keeps the task parked, none means release. + if _comms.find_unresolved_blocking_outbox(layout, role_id): + return False task.metadata = dict(task.metadata) task.metadata.pop("peer_wait", None) await transition_work_item_from_task( diff --git a/opc/layer2_organization/company_runtime_profiles.py b/opc/layer2_organization/company_runtime_profiles.py index 4c3da54..8607f23 100644 --- a/opc/layer2_organization/company_runtime_profiles.py +++ b/opc/layer2_organization/company_runtime_profiles.py @@ -40,6 +40,8 @@ _BROWSER_EXECUTION_TOOLS = [ _CORPORATE_COORDINATION_TOOLS = [ "file_read", + "file_write", + "file_edit", "file_search", "list_dir", "todo_write", @@ -49,8 +51,6 @@ _CORPORATE_COORDINATION_TOOLS = [ _CORPORATE_BOOTSTRAP_TOOLS = [ *_CORPORATE_COORDINATION_TOOLS, "shell_exec", - "file_write", - "file_edit", ] _CORPORATE_WEB_COORDINATION_TOOLS = [ @@ -76,6 +76,7 @@ _CORPORATE_EXECUTION_TOOLS = [ _CORPORATE_QA_TOOLS = [ "file_read", "file_write", + "file_edit", "file_search", "list_dir", "shell_exec", @@ -105,6 +106,7 @@ _CORPORATE_DATA_ACQUISITION_TOOLS = [ "shell_exec", "file_read", "file_write", + "file_edit", "file_search", "list_dir", "web_search", diff --git a/opc/layer3_agent/native_agent.py b/opc/layer3_agent/native_agent.py index 5591178..99db7aa 100644 --- a/opc/layer3_agent/native_agent.py +++ b/opc/layer3_agent/native_agent.py @@ -121,10 +121,11 @@ _TASK_MODE_ORCHESTRATION = """ work or context isolation when that improves the result. """ +# file_write / file_edit are deliberately NOT blocked: coordination turns +# produce in-context content (briefs, matrices, review notes) that must be +# persistable to the workspace, or it gets trapped in blocking DM hand-offs. _MULTI_TEAM_COORDINATION_NATIVE_TOOL_BLOCKLIST = { "shell_exec", - "file_write", - "file_edit", "apply_patch", "python_exec", "web_search", diff --git a/opc/plugins/office_ui/ws_handler.py b/opc/plugins/office_ui/ws_handler.py index 391983b..a374e74 100644 --- a/opc/plugins/office_ui/ws_handler.py +++ b/opc/plugins/office_ui/ws_handler.py @@ -8476,6 +8476,16 @@ class WSHandler: for item in list(pause_request.get("required_fields", []) or []) if str(item).strip() ] + resume_hint = str(pause_request.get("resume_hint", "") or "").strip() + if not resume_hint and "blocked by autonomy policy" in f"{prompt} {summary}".lower(): + # This park came from a tool-approval timeout. The approval card + # posted earlier stays pending and clickable indefinitely, so point + # the user at it instead of leaving typed input as the only path. + resume_hint = ( + "Tip: the tool-approval card above is still active — choose an option " + "there (e.g. Approve) to grant the permission and resume this task " + "automatically. Reply here only to give different instructions." + ) return { "checkpoint_type": "task_user_input", @@ -8489,7 +8499,7 @@ class WSHandler: "input_questions": input_questions, "required_fields": required_fields, "context_note": str(pause_request.get("context_note", "") or "").strip(), - "resume_hint": str(pause_request.get("resume_hint", "") or "").strip(), + "resume_hint": resume_hint, "requesting_role_id": str( payload.get("requesting_role_id") or pause_request.get("requesting_role_id") or "" ).strip(), diff --git a/tests/test_company_collaboration.py b/tests/test_company_collaboration.py index fadc180..26b0286 100644 --- a/tests/test_company_collaboration.py +++ b/tests/test_company_collaboration.py @@ -640,7 +640,8 @@ class CompanyCollaborationTests(unittest.IsolatedAsyncioTestCase): self.assertIn("acquisition_specialist", role_by_id) self.assertEqual(role_by_id["acquisition_specialist"].reports_to, "coo") self.assertIn("acquisition_specialist", role_by_id["coo"].can_spawn) - self.assertNotIn("file_edit", role_by_id["acquisition_specialist"].tools) + # Every company role can author files (file_write/file_edit baseline). + self.assertIn("file_edit", role_by_id["acquisition_specialist"].tools) def test_data_acquisition_turn_type_stays_execute_even_with_audit_language(self) -> None: builder = CompanyRuntimeSpecBuilder(DummyOrgEngine()) diff --git a/tests/test_task_mode_contract.py b/tests/test_task_mode_contract.py index c1c6353..159b64b 100644 --- a/tests/test_task_mode_contract.py +++ b/tests/test_task_mode_contract.py @@ -463,8 +463,10 @@ class TaskModeNativeAgentTests(unittest.IsolatedAsyncioTestCase): self.assertIn("file_read", allowed) self.assertIn("delegate_work", allowed) self.assertIn("manager_board_read", allowed) + # file_write stays available: coordination turns must be able to + # persist in-context content instead of trapping it in DM hand-offs. + self.assertIn("file_write", allowed) self.assertNotIn("shell_exec", allowed) - self.assertNotIn("file_write", allowed) self.assertNotIn("web_search", allowed) self.assertNotIn("agent_spawn", allowed) self.assertNotIn("agent_wait", allowed)