diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md b/agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md index 9b0ec64..8d1000e 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md @@ -77,7 +77,7 @@ Dispatcher-child re-entry boundary: | `local-G01`–`local-G06` | Pi `iop/ornith:35b`, thinking high | | `local-G07`–`local-G08` | KST `[07:00,23:00)` agy `Gemini 3.6 Flash (Medium)`; `[23:00,07:00)` Pi `iop/laguna-s:2.1` | | `local-G09`–`local-G10` | Claude `claude-opus-4-8`, effort xhigh | -| `cloud-G01`–`cloud-G02` | agy `Gemini 3.6 Flash (Low)` | +| `cloud-G01`–`cloud-G02` | Codex `gpt-5.3-codex-spark`, reasoning xhigh → on quota/failover agy `Gemini 3.6 Flash (Low)` → Claude `claude-haiku-4-5`, effort xhigh | | `cloud-G03`–`cloud-G04` | agy `Gemini 3.6 Flash (Medium)` | | `cloud-G05`–`cloud-G06` | agy `Gemini 3.6 Flash (High)` | | `cloud-G07`–`cloud-G08` | Claude `claude-opus-4-8`, effort xhigh | @@ -234,7 +234,7 @@ When recovering a KST-night `local-G07`–`local-G08` Laguna locator or a termin - Archive `WORK_LOG.md` as `work_log_N.log` only after the final task review process exits, the dispatcher appends `FINISH`, and a complete scan finds no active/running task in that group. Accept the log at either the active group path or the verified completed single-task archive; do not impose either location contract on common plan/code-review. 3. **Escalate and recover context.** - - Escalate `agy -> Claude -> Codex` or `Claude -> Codex` only on terminal provider error events or stderr evidence of context/output limits, provider quota/rate limits, unavailable models, or confirmed provider transport errors. For AGY, accept top-level `error`, `fatal`, `request.failed`, or `turn.failed` events; failed/rejected status with a top-level error/code; stderr; or strong `RESOURCE_EXHAUSTED`, HTTP 429, quota, or rate-limit evidence in `agy-cli.log`. For Claude, classify a `rate_limit_event` with `rate_limit_info.status=rejected`, an error `result` with `api_error_status=429` or `error=rate_limit`, or a `You've hit your session limit · resets ...` terminal diagnostic as `provider-quota`. Never escalate from an assistant message, source text, tool/test output, or a plain quota-configuration string in an AGY log. + - For `cloud-G01`–`cloud-G02`, fail over in the pinned candidate order `gpt-5.3-codex-spark -> Gemini 3.6 Flash (Low) -> claude-haiku-4-5`. For single-candidate routes, escalate `agy -> Claude -> Codex` or `Claude -> Codex`. Apply either transition only on terminal provider error events or stderr evidence of context/output limits, provider quota/rate limits, unavailable models, or confirmed provider transport errors. For AGY, accept top-level `error`, `fatal`, `request.failed`, or `turn.failed` events; failed/rejected status with a top-level error/code; stderr; or strong `RESOURCE_EXHAUSTED`, HTTP 429, quota, or rate-limit evidence in `agy-cli.log`. For Claude, classify a `rate_limit_event` with `rate_limit_info.status=rejected`, an error `result` with `api_error_status=429` or `error=rate_limit`, or a `You've hit your session limit · resets ...` terminal diagnostic as `provider-quota`. Never escalate from an assistant message, source text, tool/test output, or a plain quota-configuration string in an AGY log. - Target Codex `gpt-5.6-terra` with reasoning `high` when escalating from Claude to Codex. - If Codex returns the same error, retry in a fresh Codex session using the locator while preserving the previous Codex model/reasoning and sharing the same stage's 10-consecutive-failure limit. Continue dispatching other tasks during recovery. - When current source reads a locator blocked 10 times as `generic-error` by older dispatcher source, collapse those 10 failures into one terminal error and clear only that task's blocker only if all 10 terminal-evidence records for the same task/plan/role/source/execution target reclassify to the same escalatable error. Include `stream.log` and the attempt's `agy-cli.log` for AGY. Do not adjust automatically when any history is missing or mixed, or when the locator dispatcher source hash equals the current source hash. Dry-run must display this escalation recovery and next model without writing state. Live execution must choose the higher target from the locator's actual failed target, not the initial PLAN route, inherit locator context, and restore the same escalation target and locator from persisted reclassification metadata after immediate restart. diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py index f951970..6028d62 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py @@ -44,6 +44,12 @@ AGY_GEMINI_HIGH = RouteTarget( ) PI_LAGUNA = RouteTarget("pi", "iop/laguna-s:2.1", "local_model", True) CLAUDE_OPUS = RouteTarget("claude", "claude-opus-4-8", "cloud_model", False) +CLAUDE_HAIKU_XHIGH = RouteTarget( + "claude", "claude-haiku-4-5", "cloud_model", False +) +CODEX_SPARK_XHIGH = RouteTarget( + "codex", "gpt-5.3-codex-spark", "cloud_model", False +) CODEX_SOL_XHIGH = RouteTarget("codex", "gpt-5.6-sol", "cloud_model", False) CODEX_TERRA_HIGH = RouteTarget("codex", "gpt-5.6-terra", "cloud_model", False) @@ -55,6 +61,8 @@ CANONICAL_TARGETS = ( AGY_GEMINI_HIGH, PI_LAGUNA, CLAUDE_OPUS, + CLAUDE_HAIKU_XHIGH, + CODEX_SPARK_XHIGH, CODEX_SOL_XHIGH, CODEX_TERRA_HIGH, ) @@ -180,23 +188,27 @@ def select_policy( ) if grade <= 2: - target = AGY_GEMINI_LOW + candidates = ( + CODEX_SPARK_XHIGH, + AGY_GEMINI_LOW, + CLAUDE_HAIKU_XHIGH, + ) rule_id = "worker-cloud-g01-g02" - reason_code = "cloud_gemini_low_grade" + reason_code = "cloud_spark_priority_grade" elif grade <= 4: - target = AGY_GEMINI_MEDIUM + candidates = (AGY_GEMINI_MEDIUM,) rule_id = "worker-cloud-g03-g04" reason_code = "cloud_gemini_medium_grade" elif grade <= 6: - target = AGY_GEMINI_HIGH + candidates = (AGY_GEMINI_HIGH,) rule_id = "worker-cloud-g05-g06" reason_code = "cloud_gemini_high_grade" elif grade <= 8: - target = CLAUDE_OPUS + candidates = (CLAUDE_OPUS,) rule_id = "worker-cloud-g07-g08" reason_code = "cloud_opus_grade" else: - target = CODEX_SOL_XHIGH + candidates = (CODEX_SOL_XHIGH,) rule_id = "worker-cloud-g09-g10" reason_code = "cloud_codex_grade" return PolicyDecision( @@ -204,5 +216,5 @@ def select_policy( policy_priority=30, reason_codes=(reason_code,), time_window="not_applicable", - candidates=(target,), + candidates=candidates, ) diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py index cc1d362..d243345 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py @@ -3106,13 +3106,25 @@ class ReviewControlTest(unittest.TestCase): "gpt-5.6-sol", "codex/gpt-5.6-sol xhigh", ) + spark = dispatch.AgentSpec( + "codex", + "gpt-5.3-codex-spark", + "codex/gpt-5.3-codex-spark xhigh", + ) claude = dispatch.AgentSpec( "claude", "claude-opus-4-8", "claude/claude-opus-4-8 xhigh", ) + haiku = dispatch.AgentSpec( + "claude", + "claude-haiku-4-5", + "claude/claude-haiku-4-5 xhigh", + ) self.assertEqual(dispatch.effective_reasoning_effort(codex), "xhigh") + self.assertEqual(dispatch.effective_reasoning_effort(spark), "xhigh") self.assertEqual(dispatch.effective_reasoning_effort(claude), "xhigh") + self.assertEqual(dispatch.effective_reasoning_effort(haiku), "xhigh") def test_classifies_provider_tunnel_connection_refusal(self): provider_line = ( @@ -7878,6 +7890,112 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas locator.write_text(json.dumps(record), encoding="utf-8") return locator + async def test_cloud_g01_g02_quota_failover_runs_spark_gemini_haiku(self): + daytime = datetime( + 2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9)) + ) + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + (workspace / ".git").mkdir() + task = self.make_task(workspace, lane="cloud", grade=1) + store = dispatch.StateStore(workspace) + selector = dispatch._selector_module() + + def unknown_quota_probe(*args, **kwargs): + adapter = kwargs["adapter"] + target = kwargs["target"] + return { + "schema_version": "1.0", + "snapshot_id": f"unknown-{adapter}-{target}", + "source": "iop-node quota-probe", + "checked_at": kwargs["checked_at"].isoformat(), + "targets": [ + { + "adapter": adapter, + "target": target, + "status": "unknown", + } + ], + "required_caps": [], + "reason_codes": ["checker_error"], + } + + try: + with mock.patch.object( + selector, + "probe_candidate_quota", + side_effect=unknown_quota_probe, + ): + _, initial_spec = dispatch.persisted_execution_decision( + store, + task, + stage="worker", + evaluated_at=daytime, + ) + specs = { + "codex": initial_spec, + "agy": dispatch.AgentSpec( + "agy", + "Gemini 3.6 Flash (Low)", + "agy/Gemini 3.6 Flash (Low)", + ), + "claude": dispatch.AgentSpec( + "claude", + "claude-haiku-4-5", + "claude/claude-haiku-4-5 xhigh", + ), + } + locators = { + cli: self.make_attempt_locator(workspace, task, spec) + for cli, spec in specs.items() + } + invoked_specs = [] + + async def mock_invoke(*args, **kwargs): + spec = args[4] + invoked_specs.append(spec) + if spec.cli == "claude": + return 0, None, locators[spec.cli] + return 1, "provider-quota", locators[spec.cli] + + with ( + mock.patch.object(dispatch, "invoke", new=mock_invoke), + mock.patch.object( + dispatch.asyncio, + "sleep", + new=mock.AsyncMock(), + ), + ): + success, final_locator = await dispatch.run_escalating( + workspace, + store, + task, + "worker", + initial_spec, + ) + + self.assertTrue(success) + self.assertEqual(final_locator, locators["claude"]) + self.assertEqual( + [(spec.cli, spec.model) for spec in invoked_specs], + [ + ("codex", "gpt-5.3-codex-spark"), + ("agy", "Gemini 3.6 Flash (Low)"), + ("claude", "claude-haiku-4-5"), + ], + ) + decision = store.task_state(task)["execution_decisions"]["worker"] + self.assertEqual( + decision["used_candidates"], + [ + {"adapter": "codex", "target": "gpt-5.3-codex-spark"}, + {"adapter": "agy", "target": "Gemini 3.6 Flash (Low)"}, + {"adapter": "claude", "target": "claude-haiku-4-5"}, + ], + ) + finally: + store.close() + async def test_invalid_logical_context_does_not_commit_or_promote(self): daytime = datetime(2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))) diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py index 417f617..cefa9ea 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py @@ -68,7 +68,7 @@ class ExecutionTargetPolicyTests(unittest.TestCase): }, "cloud": { **{ - grade: ("agy", "Gemini 3.6 Flash (Low)", False) + grade: ("codex", "gpt-5.3-codex-spark", False) for grade in range(1, 3) }, **{ @@ -103,6 +103,28 @@ class ExecutionTargetPolicyTests(unittest.TestCase): route, ) + def test_cloud_g01_g02_uses_ordered_spark_gemini_haiku_candidates(self): + for grade in (1, 2): + with self.subTest(grade=grade): + decision = policy.select_policy( + stage="worker", + lane="cloud", + grade=grade, + evaluated_at=at_utc(3), + ) + self.assertEqual( + decision.candidates, + ( + policy.CODEX_SPARK_XHIGH, + policy.AGY_GEMINI_LOW, + policy.CLAUDE_HAIKU_XHIGH, + ), + ) + self.assertEqual( + decision.reason_codes, + ("cloud_spark_priority_grade",), + ) + def test_review_matrix_is_fixed_to_codex(self): for lane in ("local", "cloud"): for grade in range(1, 11): @@ -166,6 +188,8 @@ class ExecutionTargetPolicyTests(unittest.TestCase): (policy.AGY_GEMINI_MEDIUM, policy.CLAUDE_OPUS), (policy.AGY_GEMINI_HIGH, policy.CLAUDE_OPUS), (policy.CLAUDE_OPUS, policy.CODEX_TERRA_HIGH), + (policy.CLAUDE_HAIKU_XHIGH, None), + (policy.CODEX_SPARK_XHIGH, None), (policy.CODEX_SOL_XHIGH, None), (policy.CODEX_TERRA_HIGH, None), (policy.PI_ORNITH, None), @@ -203,6 +227,14 @@ class ExecutionTargetPolicyTests(unittest.TestCase): policy.CLAUDE_OPUS, policy.QuotaProbeSpec("claude", "claude-opus-4-8", ("overall",)), ), + ( + policy.CLAUDE_HAIKU_XHIGH, + policy.QuotaProbeSpec("claude", "claude-haiku-4-5", ("overall",)), + ), + ( + policy.CODEX_SPARK_XHIGH, + policy.QuotaProbeSpec("codex", "gpt-5.3-codex-spark", ("overall",)), + ), ( policy.CODEX_SOL_XHIGH, policy.QuotaProbeSpec("codex", "gpt-5.6-sol", ("overall",)), diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py index f44816d..91a1038 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py @@ -376,8 +376,8 @@ class SelectorRouteMatrixTests(unittest.TestCase): 10: ("claude", "claude-opus-4-8", "cloud_model", False), }, "cloud": { - 1: ("agy", "Gemini 3.6 Flash (Low)", "cloud_model", False), - 2: ("agy", "Gemini 3.6 Flash (Low)", "cloud_model", False), + 1: ("codex", "gpt-5.3-codex-spark", "cloud_model", False), + 2: ("codex", "gpt-5.3-codex-spark", "cloud_model", False), 3: ("agy", "Gemini 3.6 Flash (Medium)", "cloud_model", False), 4: ("agy", "Gemini 3.6 Flash (Medium)", "cloud_model", False), 5: ("agy", "Gemini 3.6 Flash (High)", "cloud_model", False), @@ -1063,6 +1063,69 @@ class SelectorIdentityAndQuotaRoundtripTests(unittest.TestCase): class SelectorFailoverContractTests(unittest.TestCase): + def test_cloud_g01_g02_quota_failover_follows_spark_gemini_haiku_order(self): + with TemporaryDirectory() as tmp: + task_file = write_task_file(Path(tmp), "PLAN", "cloud", 1) + initial = selector.select_execution_target( + task_file, + evaluated_at=kst(12), + quota_probe_command="missing-probe", + ) + gemini = selector.select_execution_target( + task_file, + evaluated_at=kst(12), + transition="failover", + prior_decision=initial, + failure_class="provider-quota", + quota_probe_command="missing-probe", + ) + haiku = selector.select_execution_target( + task_file, + evaluated_at=kst(12), + transition="failover", + prior_decision=gemini, + failure_class="provider-quota", + quota_probe_command="missing-probe", + ) + + self.assertEqual( + [ + (candidate["adapter"], candidate["target"]) + for candidate in initial["candidates"] + ], + [ + ("codex", "gpt-5.3-codex-spark"), + ("agy", "Gemini 3.6 Flash (Low)"), + ("claude", "claude-haiku-4-5"), + ], + ) + self.assertEqual( + (gemini["selected"]["adapter"], gemini["selected"]["target"]), + ("agy", "Gemini 3.6 Flash (Low)"), + ) + self.assertEqual( + (haiku["selected"]["adapter"], haiku["selected"]["target"]), + ("claude", "claude-haiku-4-5"), + ) + self.assertEqual( + haiku["used_candidates"], + [ + {"adapter": "codex", "target": "gpt-5.3-codex-spark"}, + {"adapter": "agy", "target": "Gemini 3.6 Flash (Low)"}, + {"adapter": "claude", "target": "claude-haiku-4-5"}, + ], + ) + with self.assertRaises(selector.SelectorInputError) as exhausted: + selector.select_execution_target( + task_file, + evaluated_at=kst(12), + transition="failover", + prior_decision=haiku, + failure_class="provider-quota", + quota_probe_command="missing-probe", + ) + self.assertEqual(exhausted.exception.code, "no_failover_candidate") + def test_qualified_failover_uses_only_unused_eligible_candidate(self): with TemporaryDirectory() as tmp: task_file = write_task_file(Path(tmp), "PLAN", "local", 8) diff --git a/docs/agent-development-workflow-cost-quality-report.md b/docs/agent-development-workflow-cost-quality-report.md index 84a8017..546967e 100644 --- a/docs/agent-development-workflow-cost-quality-report.md +++ b/docs/agent-development-workflow-cost-quality-report.md @@ -4,6 +4,8 @@ > **하이브리드 Plan/Review는 검증 완료된 Opus 바이브코딩 대비 비용을 23% 절감하고, Opus-only Plan/Review 대비 40% 절감하면서 검증 품질은 99% 수준을 유지한다.** +`m-iop-agent-cli-runtime` 완료 작업을 추가 분석한 결과, G08 이하 local worker는 최종 구현 가치의 **약 30%**에 기여한 것으로 추정된다. 합리적인 추정 범위는 **25~35%**다. 이 값은 최종 PASS를 만든 worker가 아니라 실제 구현 루프, G등급, PLAN 범위량과 최종 산출물 유지도를 함께 반영한다. + | 비교 기준 | 기준 비용 | 하이브리드 비용 | 비용 절감률 | 절감 ROI¹ | 품질 유지율 | |---|---:|---:|---:|---:|---:| | 검증 완료된 Opus 바이브코딩 | 195 | **150** | **23%** | **30%** | **100%** | @@ -73,7 +75,84 @@ Opus 바이브코딩은 구현, 검토, 수정 과정에서 Opus가 반복적으 즉, 품질을 결정하는 독립 검증은 유지하면서 가장 많은 토큰이 발생하는 실행 구간의 평균 단가를 낮추는 것이 비용 우위의 핵심이다. -## 산정 근거 +## IOP Agent CLI Runtime local 기여도 사례 + +### 측정 대상과 판정 기준 + +측정 스냅샷은 2026년 7월 30일 현재 `m-iop-agent-cli-runtime` archive의 완료 작업 22개다. `complete.log`의 Loop History에 기록된 실제 구현·리뷰 루프만 세었으며, archive에는 남았지만 실행 또는 리뷰되지 않은 PLAN stub은 제외했다. + +- `local-G01`~`local-G08`: local 구현 +- 모든 `cloud-*`: cloud 구현 +- `local-G09`~`local-G10`: 이름과 무관하게 cloud 구현 +- 공식 Code Review: 항상 cloud이므로 local 구현 기여에서 제외 +- 최종 PASS worker: 마지막 보정 주체일 뿐이므로 전체 기여도 판정에 사용하지 않음 + +이번 스냅샷에는 실행된 `local-G09`~`local-G10`이 없어서 해당 재분류가 측정값을 바꾸지는 않았다. + +### 모델 카탈로그 + +이 보고서의 local 기여도와 향후 ROI 비교에는 아래 표준 모델 카탈로그를 사용한다. 특히 `local-G07`~`local-G08`은 시간대별 대체 모델이 아니라 **Pi `iop/laguna-s:2.1`**을 기준으로 정규화한다. + +| Route | 실행 구분 | 기준 모델 | 설정 | +|---|---|---|---| +| `local-G01`~`local-G06` | Local | Pi `iop/ornith:35b` | thinking high | +| `local-G07`~`local-G08` | Local | Pi `iop/laguna-s:2.1` | Laguna 기준 | +| `local-G09`~`local-G10` | Cloud | Claude `claude-opus-4-8` | effort xhigh | +| `cloud-G01`~`cloud-G02` | Cloud | Gemini 3.6 Flash | Low | +| `cloud-G03`~`cloud-G04` | Cloud | Gemini 3.6 Flash | Medium | +| `cloud-G05`~`cloud-G06` | Cloud | Gemini 3.6 Flash | High | +| `cloud-G07`~`cloud-G08` | Cloud | Claude `claude-opus-4-8` | effort xhigh | +| `cloud-G09`~`cloud-G10` | Cloud | Codex `gpt-5.6-sol` | reasoning xhigh | +| 모든 공식 Code Review | Cloud | Codex `gpt-5.6-sol` | reasoning xhigh | + +이 카탈로그는 등급별 실행 비용과 local 대체 효과를 비교하기 위한 표준선이다. 개별 과거 실행의 일시적인 failover나 승격 target은 별도 실제 비용 로그가 있을 때만 재무 ROI에 반영한다. + +### 측정 결과 + +| 측정 지표 | Local | 전체 | Local 비중 | 해석 | +|---|---:|---:|---:|---| +| 실제 리뷰된 구현 루프 | 20 | 65 | **30.8%** | local worker가 수행한 구현 회차 | +| G등급 가중치 | 111 | 448 | **24.8%** | G01=1, G10=10으로 둔 난도 가중치 | +| PLAN 구현 범위량 | 3,258줄 | 13,115줄 | **24.8%** | 실제 리뷰된 PLAN 문서 크기를 범위량 proxy로 사용 | +| Local 참여 완료 작업 | 15 | 22 | **68.2%** | local 산출물이 한 번 이상 포함된 완료 작업 | +| 산출물 유지도 반영 종합 추정 | - | - | **약 30%** | 합리적 범위 **25~35%** | + +구현 루프, G등급, PLAN 범위량의 단순 중심은 약 27%다. 여기에 local이 먼저 만든 package, schema, lifecycle, command와 journal 기반이 최종 코드에 유지된 사례를 반영해 대표값을 30%로 잡았다. 반대로 cloud가 핵심 불변식이나 저장 구조를 크게 확장한 사례도 있으므로 35%를 상한으로 본다. + +PLAN 문서 줄 수는 실제 코드 LOC나 토큰 비용이 아니다. 서로 다른 경로의 측정값이 25~31% 구간에 모이는지 확인하기 위한 보조 proxy로만 사용한다. + +### 최종 산출물 유지도 + +| 유지도 | 대표 작업 | 판단 근거 | +|---|---|---| +| 높음 | `14_cli_config_fixtures`, `15_host_lifecycle`, `16_bootstrap_composition`, `17_project_log_records` | local이 핵심 파일·타입·동작을 만들었고 최종 cloud 작업은 좁은 검증 또는 edge case 보정이었다. | +| 중간 이상 | `07_target_policy`, `18_cli_command_tree`, `19_cli_binary_contract`, `20_project_log_journal` | local 구조가 유지됐지만 cloud가 중요한 정확성·경계 조건을 추가했다. | +| 중간 | `01_common_runtime_node_bridge`, `06_config_registry`, `10_state_recovery`, `21_project_log_sink` | local 보정이 유지됐으나 초기 또는 최종 핵심 변경에서 cloud 비중도 컸다. | +| 낮음 | `05_contract_boundary`, `09_workflow_evidence`, `13_agent_domain` | local 범위가 evidence 중심이거나 후속 cloud에서 책임 경계와 구현 범위가 크게 재정리됐다. | + +대표적인 높은 유지 사례는 다음과 같다. + +- `15_host_lifecycle`: local이 production deadlock을 해결했고 [최종 cloud PLAN](../agent-task/archive/2026/07/m-iop-agent-cli-runtime/15+13_host_lifecycle/plan_cloud_G03_2.log)은 production code를 바꾸지 않고 cleanup 중 terminal status를 확인하는 테스트만 보강했다. +- `16_bootstrap_composition`: local이 bootstrap package와 typed-nil·identity 처리를 구현했고 [최종 cloud PLAN](../agent-task/archive/2026/07/m-iop-agent-cli-runtime/16+13,15_bootstrap_composition/plan_cloud_G03_2.log)은 `Name()` 중복 호출과 회귀 테스트를 좁게 보정했다. +- `17_project_log_records`: local이 record schema와 archive manifest를 구현했고 [최종 cloud PLAN](../agent-task/archive/2026/07/m-iop-agent-cli-runtime/17+13_project_log_records/plan_cloud_G02_3.log)은 sealed quota identity의 길이·민감정보 검증을 추가했다. + +반대로 [21_project_log_sink의 최종 cloud PLAN](../agent-task/archive/2026/07/m-iop-agent-cli-runtime/21+13,17,20_project_log_sink/plan_cloud_G10_3.log)은 project-wide replay index와 atomic batch CAS를 추가한 구조적 보정이었다. 이처럼 cloud가 핵심 구조를 확장한 작업을 함께 반영했기 때문에 local 기여도를 30%보다 과도하게 높이지 않았다. + +### 해석과 운영 기준 + +이 사례에서 local은 독립적인 최종 해결자가 아니라, cloud Plan/Review의 검증 품질을 유지하면서 전체 구현 가치의 약 3분의 1을 흡수하는 실행 계층으로 기능했다. 따라서 최종 PASS가 local인 작업 비율만으로 ROI를 평가하면 local이 만든 기반 코드와 후속 cloud의 좁은 보정을 구분하지 못해 기여도를 크게 과소평가한다. + +운영 지표로는 다음 값을 사용한다. + +> **`m-iop-agent-cli-runtime` local 기여도: 대표값 30%, 합리적 범위 25~35%** + +이 정도의 기여도는 **운영상 의미 있는 ROI가 나온 수준**으로 평가한다. 이미 확보한 local 장비를 활용해 추가 비용이 전력과 운영비 중심으로 제한된다는 전제에서, cloud Plan/Review 품질을 유지하면서 전체 구현 가치의 약 30%를 local로 대체한 결과는 충분히 긍정적이다. 일회성 비용 절감 실험을 넘어 local 실행 계층을 계속 운영하고 측정할 근거가 되는 수치다. + +이 값은 재무상 실제 비용 절감률과 동일하지 않다. 비용 ROI로 전환할 때는 회피한 cloud worker 비용에서 local 전력·장비 상각·운영비와 local worker 결함으로 발생한 재작업비만 차감한다. PLAN 누락이나 리뷰에서 새로 확장된 범위는 local worker 실패 비용으로 귀속하지 않는다. + +현재 archive에는 worker 종료 시점의 독립적인 Git tree나 전체 patch가 없으므로 정확한 line survival 비율은 계산할 수 없다. 향후에는 worker 시작·종료 tree SHA와 최종 PASS tree SHA를 남겨 `base → local → final` 3-way 비교로 이 추정치를 교정한다. + +## 기존 ROI 산정 근거 측정 대상은 `m-agent-readable-repository-refactor`의 완료 작업 1~11번이며, 12번 작업은 제외했다.