Merge remote-tracking branch 'origin/dev' into provider-usage-attribution-hot-path
This commit is contained in:
commit
312c8da895
24 changed files with 2995 additions and 215 deletions
|
|
@ -14,7 +14,7 @@
|
|||
- implemented S13 source/tests: `apps/agent/internal/taskloop/testdata/parity.yaml`, `apps/agent/internal/taskloop/parity.go`, `apps/agent/internal/taskloop/parity_test.go`, `apps/agent/internal/taskloop/cutover_test.go`, `apps/agent/internal/command/task_loop.go`, `apps/agent/internal/command/task_loop_test.go`, and `apps/agent/cmd/agent/main.go`. The bounded `task-loop` command delegates to the existing `taskloop.Runtime`; `task-loop validate-plan` is the Go-owned active workflow validator, and the manifest discovers every retained Python source/test fixture below its reference root, checksum-binds the exact inventory, and proves zero production callers without claiming shared runtime ownership.
|
||||
- implemented S15 source/tests: user-local client schema and validation in `packages/go/agentconfig/runtime_config.go` and `runtime_config_test.go`; daemon process ownership and durable reconciliation in `apps/agent/internal/clientprocess/types.go`, `process.go`, `store.go`, `manager.go`, `manager_test.go`, and `store_test.go`; authenticated/idempotent client command adaptation in `apps/agent/internal/localcontrol/client_operations.go` and `client_operations_test.go`.
|
||||
- implemented S18 source: `packages/go/agentworkspace/snapshot.go`, `packages/go/agentworkspace/overlay.go`, and `packages/go/agentworkspace/confinement*.go`.
|
||||
- implemented S10 source/tests: `apps/agent/cmd/agent/main.go`, `apps/agent/cmd/agent/main_test.go`, `apps/agent/internal/command/root.go`, `apps/agent/internal/command/service.go`, `apps/agent/internal/command/root_test.go`, `apps/agent/internal/command/config_test.go`, `apps/agent/internal/bootstrap/module.go`, `apps/agent/internal/bootstrap/module_test.go`, `apps/agent/internal/taskloop/module.go`, `apps/agent/internal/taskloop/recovery.go`, and their focused tests. CLI reads and mutations reconstruct the same checksum-protected manager state, while `serve` owns sustained reconciliation and exposes that runtime through local control. Deterministic fake-provider composition tests drive the real persisted manager through canonical review, validation rollback, sibling continuation, restart, project logs, and terminal archive evidence without launching a real provider CLI.
|
||||
- implemented S10 source/tests: `apps/agent/cmd/agent/main.go`, `apps/agent/cmd/agent/main_test.go`, `apps/agent/internal/command/root.go`, `apps/agent/internal/command/service.go`, `apps/agent/internal/command/root_test.go`, `apps/agent/internal/command/config_test.go`, `apps/agent/internal/bootstrap/module.go`, `apps/agent/internal/bootstrap/module_test.go`, `apps/agent/internal/taskloop/workflow.go`, `apps/agent/internal/taskloop/workflow_test.go`, `apps/agent/internal/taskloop/module.go`, `apps/agent/internal/taskloop/recovery.go`, and their focused tests. CLI reads and mutations reconstruct the same checksum-protected manager state, while `serve` owns sustained reconciliation and exposes that runtime through local control. Deterministic fake-provider composition tests drive the real persisted manager through canonical review, validation rollback, sibling continuation, restart, project logs, and terminal archive evidence without launching a real provider CLI.
|
||||
- implemented standalone S06/S08/S19 host bindings: `apps/agent/internal/taskloop/workflow.go`, `provider.go`, `recovery.go`, `evidence.go`, `review.go`, `integration.go`, `module.go`, and their focused tests. These adapters normalize host artifacts and locators; shared selection, lifecycle, admission, review sequencing, and integration state transitions remain owned by `iop.agent-runtime`. Closure coverage includes canonical verdict parsing, retained-confinement official review, same-native-session Pi repair, active-artifact preservation, common ordered policy composition, mandatory validation, rollback, and independent queue continuation.
|
||||
- implemented S14 harness/schema: `scripts/e2e-iop-agent-logged-smoke.sh`, `scripts/fixtures/iop-agent-smoke-manifest.schema.json`, and the `test-iop-agent-logged-smoke-preflight` / `test-iop-agent-logged-smoke` Make targets. Local evidence covers syntax, an actual 13-file safe bundle, deletion/symlink/tamper/digest/schema/path/duplicate/terminal/restart rejection cases, exact-PID cleanup, deterministic fixture seeding, and the pre-login Darwin gate. Completion evidence is the 6,757-byte redacted manifest plus its exact 13 bounded JSON evidence files at `agent-task/m-iop-agent-cli-runtime/25+19,21,22,23,24_logged_smoke_closure/`, produced on Darwin arm64 from source/build/clone commit `8e55719a928a01f88f7f5e3d2574e3ea810035e8` and tree `ed741ffa781c6b52eea59175b1cb5a4891e1b0e8`; the manifest SHA-256 is `77b351792ceb235b0eaf80ef66feb48d4387b49b84517cb916ab4412ca8d906b`.
|
||||
- design input: `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`
|
||||
|
|
@ -44,7 +44,7 @@ The Edge-Node wire and Edge configuration contracts remain owned by `iop.edge-no
|
|||
| S07 | Snapshot tamper/reason/not-applicable tests, sealed safe observation projection, strict durable projection round trips, common-policy manager integration, unused-target history, and failure-budget tests | `quota-failure` evidence records content-bound immutable snapshots, canonical corrupt blockers, exact attempt/target transitions, sealed disk round trips, and no reused candidate. Shared semantics are authoritative in `iop.agent-runtime`. |
|
||||
| S08 | `TestReadReviewVerdictAcceptsCanonicalOverallVerdict`, `TestCatalogReviewExecutorRequiresExactRetainedConfinement`, `TestPiEvidenceRepairResumesExactNativeSession`, `TestWorkerPromptPreservesActiveArtifactsForOfficialReview`, and `TestOfficialReviewPromptPreservesRetainedArtifacts` | `workflow-evidence` proves canonical review parsing, exact retained executable confinement, same-native-session repair followed by fresh evidence, zero direct provider launch in deterministic tests, and active PLAN/review preservation until manager-owned integration. |
|
||||
| S09 | Device singleton, workspace lease, checkpoint, restart, and archive fault tests | `state-recovery` proves no duplicate owner and exact retained state. |
|
||||
| S10 | Binary entry point, split configuration, validation, discovery, selection, lifecycle, and status commands; exact failure-context selector coverage; and `TestRuntimeFakeProviderPersistedLifecycleRollbackAndRestart`, `TestCommandAdapterFakeProviderPersistedLifecycleRollbackAndRestart`, and `TestDaemonFakeProviderPersistedLifecycleRollbackAndRestart` | `cli-surface` is implemented by one authoritative `taskloop.Runtime` composition in CLI and daemon paths. The tests prove full ordered failure predicates, canonical review, mandatory validation rollback, independent sibling completion, persisted command/local-control projections, restart convergence, exact dispatch counts, ordered project logs, and terminal archives with proof-owned no-op child processes and no real provider CLI. |
|
||||
| S10 | Binary entry point, split configuration, validation, discovery, selection, lifecycle, and status commands; exact failure-context selector coverage; and `TestWorkflowMilestonesListsSelectableTaskGroups`, `TestWorkflowArchiveOnlyMilestoneRemainsSelectable`, `TestInspectTaskGroupArchiveOnly`, `TestRunMilestoneListAndSelectionShareCatalog`, `TestAdapterStatusPreservesAllWork`, `TestRunFullHeadlessS10Transcript`, `TestBuiltBinaryHeadlessS10Transcript`, `TestRuntimeFakeProviderPersistedLifecycleRollbackAndRestart`, `TestCommandAdapterFakeProviderPersistedLifecycleRollbackAndRestart`, and `TestDaemonFakeProviderPersistedLifecycleRollbackAndRestart` | `cli-surface` is implemented by one authoritative `taskloop.Runtime` composition in CLI and daemon paths. The tests prove Milestone catalog discovery/selection parity, per-work status DTOs with durable dispatch ordinals, archive-only completion semantics, full ordered failure predicates, canonical review, mandatory validation rollback, independent sibling completion, persisted command/local-control projections, restart convergence, exact dispatch counts, ordered project logs, terminal archives, and compiled binary transcript evidence with proof-owned no-op child processes and no real provider CLI. |
|
||||
| S11 | `TestServerSameUserProtoSocket`, `TestPeerUIDMismatchDeniedBeforeDispatch`, `TestServerBroadcastsCommittedEventToConcurrentClients`, `TestServerRejectsUnsafePaths`, `TestServerStopPreservesReplacedSocketPath`, `TestProtocolValidationMatrix`, `TestCommandIdempotencySurvivesRestart`, `TestCommandIDConflictHasZeroMutation`, `TestReplayGapRequiresSnapshot`, `TestServiceRejectedFramesHaveZeroCalls`, and the fresh package/race plus Darwin arm64 cross-build commands in the active code-review artifact | `local-control` proves an owner-only Unix socket, kernel same-user authorization with no app-token fallback, zero dispatch for denied or malformed peers, durable command-id convergence, ordered live and retained events, explicit replay-gap recovery, and a coherent snapshot cursor. |
|
||||
| S12 | `TestManagerEventDeliveryUsesCommittedEvidence` and `TestManagerEventDeliveryRecoversSinkFailure` in `packages/go/agenttask/manager_integration_test.go`; `TestSinkPendingDeliveryUsesExactCommittedEvidence`, `TestSinkReplayShortCircuitsEvidenceAfterClockAndStateAdvance`, `TestSinkRejectsLogicalEventIDReuseAcrossScopesBeforeEvidenceResolution`, `TestStoreEventReplayFingerprintSurvivesPruneAndRestart`, `TestStoreRejectsLogicalEventIDReuseAcrossScopes`, `TestStoreEventReplayIndexRecoversLegacyScopedEntry`, `TestStoreEventReplayIndexSerializesCrossScopeCAS`, and `TestS12LoopParallelArchiveMatrix` in `apps/agent/internal/projectlog/*_test.go`; atomic state-store coverage in `TestStoreIntegrationRecordBatchCAS`; fresh race verification: `go test -count=1 -race ./packages/go/agentstate ./apps/agent/internal/projectlog -run 'TestStoreIntegrationRecordBatchCAS|TestStoreEventReplayIndexSerializesCrossScopeCAS|TestS12LoopParallelArchiveMatrix'` | `project-logs` proves commit-before-observe manager ordering, returned-but-recoverable sink failure, exact pending-delivery evidence, project-wide replay identity before volatile projection or caller scope, atomic index/journal persistence, fail-closed legacy recovery and scope drift, 11 explicit review-failure/follow-up pairs, independent same-project task completion, complete redacted WORK_LOG JSONL, and exactly-once task-scoped terminal archive reconciliation for every crash phase. |
|
||||
| S13 | `parity.yaml` disposition and disposal inventory, `ValidateParityManifest`, `TestParityEmbeddedManifestIsCompleteAndCurrent`, `TestParityManifestRejectsUnrecordedRetainedFixture`, `TestCutoverProductionOwnershipHasNoReferenceCallerOrStaticRouteTable`, `TestCutoverProductionOwnershipRejectsInjectedPythonCaller`, `task-loop validate-plan`, and the `task-loop parity --disposal-manifest` command | The manifest permits `absorb`, `replace`, or `not-applicable` exactly once per behavior, requires concrete Go source/test evidence, discovers and verifies every retained Python source/test fixture checksum, and rejects stale, unclassified, or unrecorded rows. Production callers and static routing ownership are rejected by deterministic repository guards, while active plan/review workflows use the Go validator. Physical disposal remains prohibited until the Milestone-completion transition: verify `retained` hashes and cutover, delete only the recorded fixtures, change the manifest to `disposed`, then rerun parity/cutover. A disposed manifest requires the exact inventory to be absent and retained-fixture discovery to be empty, so partial or mixed states fail. |
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
domain: agent
|
||||
last_rule_review_commit: none
|
||||
last_rule_updated_at: 2026-07-29
|
||||
last_rule_review_commit: 8760d165105fb03b0b8b62b55dd31c90f34daa44
|
||||
last_rule_updated_at: 2026-07-31
|
||||
---
|
||||
|
||||
# agent
|
||||
|
|
@ -73,6 +73,7 @@ last_rule_updated_at: 2026-07-29
|
|||
- `RuntimeEvent`는 execution/attempt, project/work-unit/stage, overlay/change-set/integration lifecycle, stream/heartbeat, config/quota reference와 terminal result를 유지한다.
|
||||
- `PlanWriteSet`은 active PLAN의 정확히 하나인 `Modified Files Summary` 첫 번째 column에서 읽은 backtick file path 집합이다. glob, workspace root·directory와 containment 밖 경로를 거부한다.
|
||||
- Node bridge는 기존 Edge-Node wire 의미(`RunRequest`/`RunEvent`, cancel, command)와 provider behavior를 보존한다. Node 내부에 duplicate provider를 만들지 않는다.
|
||||
- 활성 `agent-task`의 production orchestration은 사용자 명시 요청에 따른 Python dispatcher(`agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`)가 소유한다. `apps/agent`의 `iop-agent` 표면은 `agent-task` 밖의 격리된 테스트·검증 전용이며 dispatcher를 대체하지 않는다.
|
||||
- 내 변경은 가능한 대상 패키지 테스트를 먼저 추가하거나 갱신한다.
|
||||
- `apps/agent/internal/localcontrol/**`의 same-user/other-user 경계를 바꾼 뒤에는 `testing` domain rule의 작업 후 검증 기준을 따른다.
|
||||
|
||||
|
|
@ -110,3 +111,5 @@ last_rule_updated_at: 2026-07-29
|
|||
- file write-set이 disjoint하다는 이유만으로 shared checkout의 build/test 결과를 task-isolated evidence로 간주하지 않는다.
|
||||
- gRPC, WebSocket 기본 transport, actor/FSM/plugin framework를 새 기본 구조로 도입하지 않는다.
|
||||
- `proto/gen/iop/*.pb.go` 생성 파일을 직접 수정하지 않는다.
|
||||
- dispatcher, worker, self-check, official review 또는 PLAN/CODE_REVIEW final verification 안에서 `iop-agent`를 실행하지 않는다. 따라서 `iop-agent task-loop`로 활성 `agent-task`를 dry-run·live pass·blocked retry·관찰하거나 provider 실행을 시작하는 것은 물론, 해당 실행 경로에서 `iop-agent` test·parity·validation을 호출하는 것도 금지한다. `iop-agent`는 `agent-task` 밖의 deterministic test fixture, fake provider, parity 또는 validation 검증에서만 사용한다.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
domain: client
|
||||
last_rule_review_commit: 7ca329ac9e03bf7cfebfac4517559fc1e2f0bca8
|
||||
last_rule_updated_at: 2026-07-14
|
||||
last_rule_review_commit: 4695bcbc60322b567a6e76d872490e696df672ed
|
||||
last_rule_updated_at: 2026-07-30
|
||||
---
|
||||
|
||||
# client
|
||||
|
|
@ -51,7 +51,10 @@ IOP의 공식 Flutter client UI/UX 영역이다. Control Plane HTTP/WS endpoint
|
|||
- `clientParserMap` — Client-Control Plane proto message parser map
|
||||
- `ControlPlaneStatusController` / `ControlPlaneStatusRepository` — Control Plane HTTP status/operation view 로딩과 UI state 관리
|
||||
- `EdgeRegistryView` / `EdgeStatusResponseView` / `FleetStatusResponseView` / `EdgeOperationsResponseView` — Control Plane JSON view를 client-side DTO로 정규화
|
||||
- `ProviderSnapshotView` / `EdgeCapabilitySummaryView` / `EdgeDomainAgentSummaryView` — provider resource 상태와 Edge capability/domain-agent summary를 정규화하는 client DTO
|
||||
- `EdgesPanel` / `NodesPanel` / `RuntimePanel` / `ExecutionLogsPanel` — 운영 상태를 스캔 가능한 panel UI로 표시하는 widget
|
||||
- `NodesPanelContent` / `NodeStatusCard` / `ProviderSnapshotCard` — Node 목록 상태와 provider snapshot 표시를 분리한 section widget
|
||||
- `RuntimePanelDomainAgentsSection` / `RuntimePanelOperationsHistorySection` — Runtime panel의 domain-agent와 operation history 표시를 분리한 section widget
|
||||
- `apps/client/lib/gen/proto/iop/*.dart` — `make proto-dart`로 생성되는 Dart protobuf binding
|
||||
- `NexoNotificationHostIntegration` / `NexoNotificationPluginClient` / `NexoNotificationClient` — Nexo messaging notification stream integration host
|
||||
- `IopConsoleShell` — IOP 단독 앱과 외부 임베더가 공유할 수 있는 좌측 rail console shell
|
||||
|
|
@ -65,6 +68,7 @@ IOP의 공식 Flutter client UI/UX 영역이다. Control Plane HTTP/WS endpoint
|
|||
- Client-Control Plane native 경계는 `/client` WebSocket proto-socket과 `ClientHelloRequest`/`ClientHelloResponse` baseline을 따른다.
|
||||
- Client-Control Plane wire 상세는 `agent-contract/inner/client-control-plane-wire.md`를 기준으로 확인한다.
|
||||
- Control Plane HTTP 상태 조회는 repository/controller/DTO 경계에서 처리하고, panel widget이 raw JSON parsing이나 endpoint path 조립을 직접 소유하지 않도록 유지한다.
|
||||
- `NodesPanel`과 `RuntimePanel`은 로딩과 새로고침 orchestration에 집중하고, 반복되는 상태별 UI와 card rendering은 대응 section widget에 둔다.
|
||||
- Control Plane URL은 `IOP_CONTROL_PLANE_HTTP_URL`, `IOP_CONTROL_PLANE_WIRE_URL` `--dart-define`으로 주입한다. 실제 환경값이나 private endpoint를 tracked source에 고정하지 않는다.
|
||||
- Dart protobuf binding은 `proto/iop/*.proto`에서 생성한다. proto 계약 변경 시 `make proto-dart` 산출물과 Go 생성물 갱신 여부를 함께 확인한다.
|
||||
- `apps/client/lib/gen/proto/iop/*.dart` 생성물은 사람이 직접 수정하지 않는다.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
domain: control-plane
|
||||
last_rule_review_commit: 7ca329ac9e03bf7cfebfac4517559fc1e2f0bca8
|
||||
last_rule_updated_at: 2026-07-14
|
||||
last_rule_review_commit: 4695bcbc60322b567a6e76d872490e696df672ed
|
||||
last_rule_updated_at: 2026-07-30
|
||||
---
|
||||
|
||||
# control-plane
|
||||
|
|
@ -37,6 +37,7 @@ last_rule_updated_at: 2026-07-14
|
|||
- `registerEdgeRegistryHandlers()` — `/edges`, `/edges/{edge_id}`, `/edges/{edge_id}/status`, `/edges/{edge_id}/events`, `/edges/{edge_id}/operations`, `/edges/{edge_id}/commands` JSON endpoint
|
||||
- `registerFleetHandlers()` / `fleetService` — `/fleet/status`와 `/fleet/commands` fan-out, bounded concurrency, short status cache
|
||||
- `edgeRegistryView` / `edgeStatusResponseView` / `fleetEdgeView` / `edgeCommandRecordView` — HTTP JSON 응답용 Control Plane view DTO
|
||||
- `providerSnapshotView` / `nodeConfigSummaryView` / `edgeCapabilitySummaryView` / `edgeDomainAgentSummaryView` — Edge가 보고한 provider resource, Node config summary, capability/domain-agent 상태를 투영하는 view DTO
|
||||
- `wire.Protocol` — Control Plane 통신 표준을 `protobuf-socket`으로 고정하는 상수
|
||||
- `wire.Endpoint` — reserved wire endpoint 설정 타입
|
||||
- `wire.ClientServer` — `/client` WebSocket proto-socket hello 요청을 처리하는 서버 구현
|
||||
|
|
@ -59,6 +60,7 @@ last_rule_updated_at: 2026-07-14
|
|||
- Control Plane-Edge wire 상세는 `agent-contract/inner/control-plane-edge-wire.md`, Client-Control Plane wire 상세는 `agent-contract/inner/client-control-plane-wire.md`를 기준으로 확인한다.
|
||||
- Edge registry는 현재 in-memory connection/control view이다. 최근 node event와 command record/event는 운영 화면용 bounded view이며, durable history, audit, 정책 저장소를 이 registry에 섞지 않는다.
|
||||
- Edge status 조회는 Edge가 보고한 `EdgeStatusResponse`를 관찰한다. Control Plane에서 Node address, token, transport internals, Edge 설정 원본을 직접 소유하지 않는다.
|
||||
- Provider snapshot, Node config summary, Edge capability와 domain-agent view는 Edge 응답을 안전한 JSON projection으로 변환할 뿐 Control Plane에서 다시 계산하거나 별도 원본으로 유지하지 않는다.
|
||||
- Edge command와 fleet command는 Control Plane이 Edge-owned operation을 wire로 요청하는 표면이다. command semantics는 Edge service/operation boundary에 두고, Control Plane은 fan-out, timeout, view rendering, 최소 record만 담당한다.
|
||||
- Fleet status fan-out은 bounded concurrency와 짧은 cache를 사용해 연결 Edge를 관찰한다. cache는 freshness 최적화일 뿐 source of truth가 아니며 disconnected view는 registry 상태를 즉시 반영한다.
|
||||
- `ScheduleRequest`/`ScheduleResponse`는 legacy placeholder로만 취급하고, 새 orchestration 계약은 Edge-owned runtime state를 우회하지 않도록 다시 설계한다.
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
---
|
||||
domain: edge
|
||||
last_rule_review_commit: 7ca329ac9e03bf7cfebfac4517559fc1e2f0bca8
|
||||
last_rule_updated_at: 2026-07-14
|
||||
last_rule_review_commit: 4695bcbc60322b567a6e76d872490e696df672ed
|
||||
last_rule_updated_at: 2026-07-30
|
||||
---
|
||||
|
||||
# edge
|
||||
|
||||
## 목적 / 책임
|
||||
|
||||
여러 Node를 하나의 로컬 실행 그룹으로 묶는 백엔드 실행 그룹 컨트롤러 영역이다. node 연결을 수락하고 token 기반 등록을 검증한 뒤 adapter/runtime 설정을 내려주며, `iop-edge` command 중심의 local/field 운영 UX, ops console, OpenAI-compatible HTTP, A2A JSON-RPC, Control Plane outbound connector를 내부 Edge-owned operation으로 수렴시킨다. Edge는 자신의 설정, Node registry, runtime/automation 상태의 원본 소유자다.
|
||||
여러 Node를 하나의 로컬 실행 그룹으로 묶는 백엔드 실행 그룹 컨트롤러 영역이다. node 연결을 수락하고 token 기반 등록을 검증한 뒤 adapter/runtime 설정을 내려주며, `iop-edge` command 중심의 local/field 운영 UX, ops console, OpenAI-compatible HTTP, A2A JSON-RPC, Control Plane outbound connector를 내부 Edge-owned operation으로 수렴시킨다. OpenAI-compatible 경계에서는 선택적으로 request-local Stream Evidence Gate를 조립해 출력 검증과 bounded recovery를 수행한다. Edge는 자신의 설정, Node registry, runtime/automation 상태의 원본 소유자다.
|
||||
|
||||
## 포함 경로
|
||||
|
||||
|
|
@ -70,6 +70,8 @@ last_rule_updated_at: 2026-07-14
|
|||
- `service.NodeCommandView` / `UsageStatusView` — console/HTTP/RPC surface가 공유할 수 있는 node command 결과 DTO
|
||||
- `input.Manager` — OpenAI-compatible 서버와 A2A 서버 lifecycle 소유자
|
||||
- `openai.Server` — `/v1/models`, `/v1/chat/completions`, `/v1/responses`, SSE stream, strict output/tool validation, usage metering, principal token auth, provider pool/passthrough, Ollama API passthrough를 `service`로 연결하는 HTTP 표면
|
||||
- OpenAI Stream Evidence Gate adapter — `stream_gate_runtime.go`, dispatcher/policy/filter/ingress/release sink/tunnel codec와 endpoint request rebuilder를 통해 공통 `streamgate` runtime을 Chat/Responses/provider-tunnel 표면에 연결
|
||||
- `zapFilterObservationSink` — raw prompt/output/tool payload를 보존하지 않는 filter decision·recovery 관측 projection
|
||||
- `a2a.Server` / `a2a.TaskStore` — A2A `message/send`, `tasks/get`, `tasks/cancel`과 task 상태 보관
|
||||
- `opsconsole.Run` — edge-local console loop와 slash command 처리
|
||||
- `opsconsole.EventRouter` — run/node event를 edge console 출력으로 라우팅
|
||||
|
|
@ -98,6 +100,9 @@ last_rule_updated_at: 2026-07-14
|
|||
- OpenAI-compatible 경계의 `model`과 A2A 경계의 `Task`/JSON-RPC 표현은 입력 표면 안에서만 유지하고, edge 내부 실행은 `service.SubmitRun()`의 `adapter + target` 요청으로 변환한다.
|
||||
- provider pool에서는 top-level `models[]`의 id를 canonical model group key로 보고, `models[].providers`와 `nodes[].providers[]`를 통해 provider id별 served model로 rewrite한다. caller metadata나 request body의 임의 field가 provider 선택권을 갖지 않게 한다.
|
||||
- OpenAI-compatible raw passthrough는 `ProviderTunnelRequest`/`ProviderTunnelFrame` 경계와 `service.SubmitProviderTunnel()`을 통해서만 수행한다. HTTP handler가 node transport client에 직접 provider tunnel message를 쓰지 않는다.
|
||||
- Stream Evidence Gate가 활성화된 요청은 request-start config/filter snapshot에 고정하고, blocking filter의 safe release 전에는 response start나 opening event를 commit하지 않는다. release, terminal, bounded recovery는 공통 `streamgate` runtime을 통해 단일 수명주기로 수렴시킨다.
|
||||
- Stream Evidence Gate의 endpoint codec, provider-tunnel 변환, request rebuild와 OpenAI-compatible 오류 projection은 Edge가 소유하고, transport-neutral event/filter/commit/recovery 상태 머신은 `packages/go/streamgate`를 재사용한다.
|
||||
- Output filter 선택은 endpoint, environment, model group/model, 실제 provider와 execution path를 기준으로 하며 caller SDK나 제품명을 정책 selector로 사용하지 않는다.
|
||||
- OpenAI principal token auth는 raw token을 config에 저장하지 않고 `principal_tokens[].token_hash_sha256`과 resolved principal metadata만 사용한다. caller-supplied metadata의 `iop_principal_*` 값은 인증된 principal을 덮어쓸 수 없다.
|
||||
- OpenAI-compatible `/v1/models`는 `openai.models`를 우선하고, 없으면 `openai.target`을 advertised model로 사용한다. 내부 target override와 외부 model echo 정책을 혼동하지 않는다.
|
||||
- OpenAI/Ollama passthrough성 옵션은 입력 표면에서 명시적으로 변환하고, node adapter의 Ollama 실행 계약을 우회하지 않는다.
|
||||
|
|
@ -113,7 +118,7 @@ last_rule_updated_at: 2026-07-14
|
|||
## 다른 도메인과의 경계
|
||||
|
||||
- **node**: edge는 node 내부 adapter를 직접 실행하지 않는다. edge는 사전 등록 정보와 연결 registry를 기반으로 요청을 보낼 대상과 실행 설정을 관리하고, TCP/protobuf로 `RunRequest`/`CancelRequest`/`NodeCommandRequest`를 보낸다.
|
||||
- **platform-common**: edge 설정, metrics, protobuf 타입은 platform-common 계약을 따른다.
|
||||
- **platform-common**: edge 설정, metrics, protobuf 타입과 transport-neutral `streamgate` event/filter/commit/recovery runtime은 platform-common 계약을 따른다. Edge는 OpenAI endpoint adapter와 정책 조립만 소유한다.
|
||||
- **external input surfaces**: OpenAI-compatible HTTP와 A2A JSON-RPC는 edge inbound adapter이며, 내부 transport/protobuf 경계를 대체하지 않는다.
|
||||
- **control-plane**: control-plane은 Edge를 통해 시스템을 제어한다. Edge domain은 outbound connector와 Edge-owned status/event/command 응답을 소유하고, control-plane domain은 server endpoint와 Edge connection/control view를 소유한다. Control Plane 없는 bootstrap/local/field/진단 fallback은 `iop-edge` command 표면에 남긴다.
|
||||
|
||||
|
|
@ -127,6 +132,7 @@ last_rule_updated_at: 2026-07-14
|
|||
- Control Plane connector에서 Node token, Node address, transport client 내부 상태를 Control Plane status 계약으로 노출하지 않는다.
|
||||
- config refresh에서 `restart_required`로 분류된 변경을 runtime에 부분 적용하지 않는다.
|
||||
- OpenAI-compatible provider pool에서 authenticated principal, provider auth header, workspace 검증을 우회하거나 caller metadata로 대체하지 않는다.
|
||||
- Stream Evidence Gate를 우회해 blocking filter 판정 전에 응답을 commit하거나, caller/product identity로 filter 적용 여부를 바꾸거나, 공통 `streamgate` 상태 머신을 Edge 내부에 복제하지 않는다.
|
||||
- Control Plane 도입만을 이유로 `iop-edge config`, `env`, `node register`, `nodes list`, `smoke`, `setup` 같은 local/field fallback command 경로를 제거하거나 제품 기본 계약에서 제외하지 않는다. 축소는 별도 roadmap 결정과 대체 fallback 기준이 있을 때만 다룬다.
|
||||
- `node register`와 bootstrap UX에 named environment parameter 조합을 기본 사용자 경로로 노출하지 않는다.
|
||||
- bootstrap 안내를 `IOP_*=` 환경 변수 나열, shell wrapper, 원격 token 조회, 수동 `node.yaml` 작성, 수동 `iop-node serve --config ...` 흐름으로 길게 만들지 않는다. 이런 값은 디버그/고급 override 문서로만 분리한다.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
domain: node
|
||||
last_rule_review_commit: 432284820e36a7a3c6b35caaa8e4b9f903145b86
|
||||
last_rule_updated_at: 2026-07-28
|
||||
last_rule_review_commit: 4695bcbc60322b567a6e76d872490e696df672ed
|
||||
last_rule_updated_at: 2026-07-30
|
||||
---
|
||||
|
||||
# node
|
||||
|
|
@ -42,6 +42,8 @@ Edge에 연결되어 실제 adapter execution을 수행하는 IOP 노드 에이
|
|||
- `node.Node.OnProviderTunnelRequest()` — provider tunnel 요청을 지원 adapter에 전달하고 tunnel frame을 edge session으로 반환
|
||||
- `node.sessionSink` — adapter `RuntimeEvent`를 proto `RunEvent`로 변환해 edge session으로 보내는 sink
|
||||
- `transport.Session` — edge와 연결된 node 세션 및 메시지 처리
|
||||
- `bootstrap.runtimeSupervisor` — 초기 연결과 reconnect를 직렬화하고 단일 active Edge session, bounded retry, fatal shutdown을 소유하는 Node lifecycle supervisor
|
||||
- `quota-probe` — 공통 CLI status checker 결과를 content-addressed `QuotaSnapshot` JSON으로 내보내는 내부 진단 command
|
||||
- `agentruntime.Registry` / `agentruntime.LifecycleProvider` — provider 등록/조회와 start/stop lifecycle 관리
|
||||
- `adapters.ConfigSet` / `adapters.DiffConfigSets()` — Edge config payload에서 adapter registry/runtime snapshot을 만들고 refresh diff를 산출
|
||||
- `adapters.BuildFromPayload()` — edge에서 받은 `NodeConfigPayload`로 `Registry`를 초기화하는 factory
|
||||
|
|
@ -65,6 +67,8 @@ Edge에 연결되어 실제 adapter execution을 수행하는 IOP 노드 에이
|
|||
- `NodeCommandRequest`는 실행 요청과 분리해 `USAGE_STATUS`, `CAPABILITIES`, `SESSION_LIST`, `TRANSPORT_STATUS` 같은 조회/제어성 명령으로 처리한다.
|
||||
- `OLLAMA_API` command는 Ollama adapter 내부의 제한된 `/api/*` passthrough로 처리하고, Edge/OpenAI surface가 node HTTP client를 우회해 직접 Ollama에 붙는 구조로 확장하지 않는다.
|
||||
- `agentruntime.Registry`의 start/stop은 bootstrap lifecycle에서만 호출하고 개별 provider에서 직접 호출하지 않는다.
|
||||
- Edge 연결 lifecycle은 `runtimeSupervisor` 하나가 초기 dial, active session 종료 대기, reconnect와 shutdown을 직렬화해 동시에 둘 이상의 dial/session이 생기지 않도록 유지한다.
|
||||
- `quota-probe`는 provider 원문이나 credential을 내보내지 않고 공통 status package가 정규화·검증할 수 있는 quota evidence만 출력한다.
|
||||
- `response_idle_timeout_ms`, `startup_idle_timeout_ms`, `completion_marker`, `resume_args`, `mode` 같은 CLI profile 설정은 edge config/proto payload를 통해 주입하고 node 코드에 target별 상수를 늘리지 않는다.
|
||||
- config refresh는 `adapters.BuildConfigSet()`로 next registry를 만들고 start 성공 후 router registry를 live swap한다. 기존 in-flight run은 old adapter snapshot으로 마무리하고, old registry stop은 active run drain 뒤에 처리한다.
|
||||
- Node-wide runtime concurrency는 admission source로 되살리지 않는다. per-adapter `Capabilities().MaxConcurrency`가 adapter gate capacity의 기준이다.
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
---
|
||||
domain: platform-common
|
||||
last_rule_review_commit: 432284820e36a7a3c6b35caaa8e4b9f903145b86
|
||||
last_rule_updated_at: 2026-07-28
|
||||
last_rule_review_commit: 4695bcbc60322b567a6e76d872490e696df672ed
|
||||
last_rule_updated_at: 2026-07-30
|
||||
---
|
||||
|
||||
# platform-common
|
||||
|
||||
## 목적 / 책임
|
||||
|
||||
여러 앱이 공유하는 Agent Runtime와 CLI provider, 설정, 인증, 감사 event envelope, 이벤트 helper, host setup, 정책, 메타데이터, 작업 상태, 관측성, 버전, protobuf 계약을 관리한다. 앱별 구현보다 안정적인 공통 계약과 작은 유틸리티를 제공하며, 내부 실행 계약은 `adapter + target` 방향을 우선한다.
|
||||
여러 앱이 공유하는 Agent Runtime와 CLI provider, provider catalog/readiness, Agent Task orchestration, standalone runtime config/state/workspace guardrail, Stream Evidence Gate, 설정, 인증, 감사 event envelope, 이벤트 helper, host setup, 정책, 메타데이터, 작업 상태, 관측성, 버전, protobuf 계약을 관리한다. 앱별 구현보다 안정적인 공통 계약과 작은 유틸리티를 제공하며, 내부 실행 계약은 `adapter + target` 방향을 우선한다.
|
||||
|
||||
## 포함 경로
|
||||
|
||||
- `packages/go/auth/` — mTLS 인증 설정 helper
|
||||
- `packages/go/agentconfig/` — secret-free Agent provider catalog와 repo-global/user-local runtime config composition·watcher
|
||||
- `packages/go/agentguard/` — unattended Agent Task의 canonical workspace/capability admission과 opaque permit
|
||||
- `packages/go/agentpolicy/` — deterministic target selection과 quota/failure retry·failover policy
|
||||
- `packages/go/agentruntime/` — host-neutral provider 실행, event/session/failure, registry lifecycle 계약
|
||||
- `packages/go/agentprovider/catalog/` — provider/model/profile discovery, readiness, redaction과 공통 provider factory
|
||||
- `packages/go/agentprovider/cli/` — Node와 독립 host가 공유하는 CLI provider, emitter, session, status/quota 구현
|
||||
- `packages/go/agentstate/` — shared AgentTask manager state의 crash-safe device-local CAS 저장소
|
||||
- `packages/go/agenttask/` — durable AgentTaskManager 상태 전이, dependency, dispatch, review와 serial integration orchestration
|
||||
- `packages/go/agentworkspace/` — task-owned workspace snapshot/overlay/confinement, change set와 integration backend
|
||||
- `packages/go/audit/` — 공통 audit event envelope, event type, policy decision baseline
|
||||
- `packages/go/config/` — 앱 설정 struct, 기본값, YAML 로딩
|
||||
- `packages/go/events/` — 공통 EdgeNodeEvent 생성 helper와 lifecycle 상수
|
||||
|
|
@ -23,6 +30,7 @@ last_rule_updated_at: 2026-07-28
|
|||
- `packages/go/metadata/` — 공통 metadata map helper
|
||||
- `packages/go/observability/` — zap logger와 Prometheus health/metrics 서버
|
||||
- `packages/go/policy/` — 정책 엔진 인터페이스와 passthrough 구현
|
||||
- `packages/go/streamgate/` — transport-neutral normalized stream event, filter/evidence, commit, release와 bounded recovery runtime
|
||||
- `packages/go/version/` — 앱 버전 상수
|
||||
- `proto/iop/` — protobuf 메시지 계약 원본
|
||||
- `proto/gen/iop/` — protobuf 생성물
|
||||
|
|
@ -42,6 +50,14 @@ last_rule_updated_at: 2026-07-28
|
|||
- `config.NodeConfig` / `config.EdgeConfig` — node/edge 앱 설정 계약
|
||||
- `agentruntime.Provider` / `agentruntime.Registry` — host-neutral provider 실행과 lifecycle registry 계약
|
||||
- `agentruntime.ExecutionSpec` / `agentruntime.RuntimeEvent` / `agentruntime.Failure` — 공통 실행, stream event, typed failure 계약
|
||||
- `agentconfig.Catalog` / `agentconfig.RuntimeSnapshot` / `agentconfig.RuntimeConfigWatcher` — Agent provider 선언과 immutable runtime config revision/composition
|
||||
- `agentprovider/catalog.Discoverer` / `catalog.ProfileProvider` — provider readiness 확인과 catalog identity를 보존하는 공통 provider factory
|
||||
- `agentguard.Admit()` / `agentguard.Permit` — unattended invocation 직전 workspace/profile/confinement evidence 검증
|
||||
- `agentpolicy.Evaluator` / `agentpolicy.DecideContinuation()` — deterministic route 선택과 quota/failure 기반 retry·failover 판단
|
||||
- `agenttask.Manager` / `agenttask.Scheduler` — manual start부터 dependency-ready dispatch, review, follow-up, ordinal integration까지의 단일 상태 전이 소유자
|
||||
- `agentstate.Store` — checksum, atomic rename, advisory lock과 revision CAS를 사용하는 device-local manager state 저장소
|
||||
- `agentworkspace.Backend` / `agentworkspace.SerialIntegrator` — immutable workspace snapshot, isolated overlay/confinement, change-set freeze와 serial apply backend
|
||||
- `streamgate.RequestRuntime` / `streamgate.GateCoordinator` / `streamgate.CommitBoundary` / `streamgate.RecoveryCoordinator` — request-local evidence 평가, safe release, terminal과 bounded recovery 상태 머신
|
||||
- `agentprovider/cli.CLI` — one-shot/persistent CLI 실행, session/resume/cancel, emitter와 status/quota 공통 구현
|
||||
- `config.EdgeInfo` / `config.EdgeControlPlaneConf` — Edge identity와 Control Plane outbound connector 설정 계약
|
||||
- `config.EdgeServerConf` / `config.EdgeBootstrapConf` — Edge listen/advertise host와 artifact bootstrap URL 설정 계약
|
||||
|
|
@ -74,6 +90,10 @@ last_rule_updated_at: 2026-07-28
|
|||
|
||||
- 공통 패키지는 특정 앱의 내부 패키지를 import하지 않는다.
|
||||
- Agent Runtime와 CLI provider는 protobuf/transport를 import하지 않고 host가 translation boundary를 소유한다.
|
||||
- Agent provider catalog/runtime config는 Edge provider pool의 `models[]`/`nodes[].providers[]`와 별도 schema·identity를 유지한다.
|
||||
- `agenttask.Manager`만 shared Agent Task 상태 전이와 dispatch/review/integration 순서를 소유하며 host가 같은 알고리즘을 복제하지 않는다.
|
||||
- `agentstate.Store`와 `agentworkspace`는 exact revision과 immutable identity를 보존하고 corruption, drift, unsupported confinement을 성공이나 빈 상태로 정규화하지 않는다.
|
||||
- `packages/go/streamgate`는 Go 표준 라이브러리만 사용하는 transport-neutral core로 유지하고 `apps/**`, protobuf, `packages/go/config`를 import하지 않는다.
|
||||
- 설정 struct 필드 변경 시 YAML tag, mapstructure tag, default, `configs/*.yaml` 예시를 함께 확인한다.
|
||||
- host setup 기본 템플릿을 바꿀 때는 `packages/go/hostsetup`의 `EdgeSpec`/`NodeSpec`, 기본 경로, systemd unit, 관련 CLI `setup` 옵션과 함께 확인한다.
|
||||
- protobuf 계약 변경은 `proto/iop/*.proto`에서 시작하고 `make proto`로 Go 생성물을 갱신한다.
|
||||
|
|
@ -93,7 +113,8 @@ last_rule_updated_at: 2026-07-28
|
|||
## 다른 도메인과의 경계
|
||||
|
||||
- **node**: 공통 provider/runtime 구현과 설정/타입/계약을 제공하지만 protobuf translation, Edge 연결, admission과 실행 파이프라인 조정은 node가 소유한다.
|
||||
- **edge**: edge가 필요로 하는 설정/관측성/protobuf 계약을 제공하지만 실행 그룹 제어와 node registry 동작의 소유자는 edge이다.
|
||||
- **edge**: edge가 필요로 하는 설정/관측성/protobuf와 `streamgate` core 계약을 제공하지만 실행 그룹 제어, node registry, OpenAI endpoint codec/filter policy 조립은 edge가 소유한다.
|
||||
- **agent**: shared config/state/policy/provider/task/workspace 계약을 제공하지만 standalone daemon lifecycle, local-control transport와 client process ownership은 concrete agent application이 소유한다.
|
||||
- **control-plane/client/worker**: 앱별 구현에 필요한 공통 타입만 이 영역으로 승격하고 앱 내부 책임은 각 도메인에 둔다.
|
||||
- **audit/ops**: audit event type과 envelope는 공통 계약이지만, 저장소/조회/retention 실행 정책은 control-plane 또는 별도 운영 도메인에서 결정한다.
|
||||
|
||||
|
|
@ -101,6 +122,9 @@ last_rule_updated_at: 2026-07-28
|
|||
|
||||
- `packages/go`에서 `apps/*/internal` 패키지를 import하지 않는다.
|
||||
- 앱 하나만을 위한 임시 타입을 충분한 근거 없이 공통 패키지로 승격하지 않는다.
|
||||
- Agent provider catalog를 Edge provider-pool config와 합치거나 ID 의미를 서로의 fallback으로 사용하지 않는다.
|
||||
- `agenttask.Manager` 상태 머신, permit 검증, retry/failover, review/integration 순서를 앱 내부에 복제하지 않는다.
|
||||
- `streamgate` core에 OpenAI HTTP/SSE codec, protobuf, Edge config 또는 caller/product 전용 selector를 넣지 않는다.
|
||||
- edge fanout bus, web UI state, control-plane session 관리처럼 특정 앱의 운영 상태를 공통 패키지로 끌어올리지 않는다.
|
||||
- 내부 실행 계약을 확장하면서 `model` 중심 명명을 되살리지 않는다. 외부 API 호환이 필요한 경우 경계와 변환 위치를 명시한다.
|
||||
- raw token, provider credential, private endpoint 값을 `packages/go/config`, `configs/`, proto 기본값에 넣지 않는다.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
domain: testing
|
||||
last_rule_review_commit: 5b56add0d898dc0080fdb3997f32881605b72ca3
|
||||
last_rule_updated_at: 2026-07-26
|
||||
last_rule_review_commit: 8760d165105fb03b0b8b62b55dd31c90f34daa44
|
||||
last_rule_updated_at: 2026-07-31
|
||||
---
|
||||
|
||||
# testing
|
||||
|
|
@ -15,6 +15,7 @@ last_rule_updated_at: 2026-07-26
|
|||
- `Makefile` — 공식 test target, proto generation, 보조 smoke target을 선언하는 위치이다.
|
||||
- `scripts/dev/edge.sh` — repo 내부 edge console/server 개발 진단 helper이다.
|
||||
- `scripts/dev/node.sh` — repo 내부 node 연결 개발 진단 helper이다. field 사용자 기본 경로로 안내하지 않는다.
|
||||
- `scripts/dev/edge-node-reconnect-diagnostic.sh` — repo 내부 Edge-Node disconnect/reconnect lifecycle 진단 helper이다.
|
||||
- `scripts/dev/web.sh` — repo 내부 Flutter Web client 개발 진단 helper이다. field 배포 기본 경로로 안내하지 않는다.
|
||||
- `scripts/e2e-smoke.sh` — mock/real profile 기반 보조 edge-node smoke 검증이다.
|
||||
- `scripts/e2e-openai-cli-workspace.sh` — OpenAI-compatible `/v1/responses` CLI workspace isolation 보조 smoke 검증이다.
|
||||
|
|
@ -23,10 +24,20 @@ last_rule_updated_at: 2026-07-26
|
|||
- `scripts/e2e-openai-lemonade.sh` — OpenAI-compatible Lemonade/provider API 입력 표면 보조 smoke 검증이다.
|
||||
- `scripts/e2e-long-context-admission-smoke.sh` — live provider pool long-context admission/capacity 보조 smoke 검증이다.
|
||||
- `scripts/e2e-control-plane-edge-wire.sh` — Control Plane-Edge wire hello/disconnect 보조 smoke 검증이다.
|
||||
- `scripts/e2e-provider-capacity-smoke.sh` — provider resource capacity와 queue 동작을 확인하는 보조 smoke 검증이다.
|
||||
- `scripts/fixtures/` — E2E smoke 입력 fixture 위치이다.
|
||||
- `scripts/inventory-query/` — `agent-test` 환경 inventory를 bounded projection 또는 exact selector 결과로 조회하는 helper와 테스트이다.
|
||||
- `scripts/readability_audit.py` — tracked/worktree의 파일·함수·task read-set 가독성 기준을 검사하는 deterministic audit이다.
|
||||
- `scripts/readability_audit_test.py` — readability audit parser/policy/ratchet 단위 테스트이다.
|
||||
- `scripts/readability_baseline.json` — readability violation ratchet 기준선이다.
|
||||
- `scripts/readability_read_sets.json` — task별 ordered read-set budget 정의이다.
|
||||
- `cmd/iop-provider-smoke/` — redacted provider catalog readiness와 status/run/resume/cancel lifecycle을 실제 CLI로 검증하는 smoke command이다.
|
||||
- `docker-compose.yml` — local dev용 Control Plane, datastore, Flutter Web client stack 조립 표면이다.
|
||||
- `apps/agent/internal/command/task_loop.go` — task-loop operator request/response와 exit mapping을 제공하는 Go command boundary이다.
|
||||
- `apps/agent/internal/taskloop/parity.go` 및 `cutover_test.go` — S13 disposition/disposal evidence와 repository ownership guard를 검증하는 격리 표면이다.
|
||||
- `agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md` — Agent Task 무인 실행과 provider 격리 검증 절차의 project entrypoint이다.
|
||||
- `agent-ops/skills/project/orchestrate-agent-task-loop/agents/` — orchestrator 실행에 사용하는 agent metadata이다.
|
||||
- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/` — task plan을 CLI invocation으로 연결하는 dispatcher, execution-target policy/selector와 observation helper 경계이다.
|
||||
|
||||
## 제외 경로
|
||||
|
||||
|
|
@ -50,6 +61,10 @@ last_rule_updated_at: 2026-07-26
|
|||
- OpenAI-compatible provider smoke — `scripts/e2e-openai-vllm.sh`와 `scripts/e2e-openai-lemonade.sh`로 provider API route, request body, expected output을 확인하는 live-dependency 보조 검증이다.
|
||||
- Long-context admission smoke — `scripts/e2e-long-context-admission-smoke.sh`로 provider pool capacity, queue, long-context slot, Control Plane status snapshot 회복을 live dev provider pool에서 확인하는 보조 검증이다.
|
||||
- Control Plane-Edge wire smoke — `scripts/e2e-control-plane-edge-wire.sh`로 실제 Control Plane/Edge 프로세스의 Edge hello, 연결 성공, disconnect marker를 확인하는 보조 검증이다.
|
||||
- Provider capacity smoke — `scripts/e2e-provider-capacity-smoke.sh`로 provider resource 단위 admission, queue와 release 동작을 확인하는 보조 검증이다.
|
||||
- Inventory query — 전체 private inventory를 불필요하게 읽지 않고 환경 projection이나 exact model/node/provider selector 결과만 정렬된 JSON으로 조회하는 도구이다.
|
||||
- Readability audit — file/function LOC와 task-local read-set budget을 deterministic JSON과 baseline ratchet으로 검증하는 repository quality gate이다.
|
||||
- Agent provider smoke — `cmd/iop-provider-smoke`로 catalog readiness와 status/run/resume/cancel terminal을 redacted evidence로 확인하는 live external-CLI 검증이다.
|
||||
- compose local dev stack 검증 — `docker-compose.yml` 변경 시 Control Plane, datastore, Flutter Web service의 build/env/healthcheck wiring을 확인하는 검증이다.
|
||||
- full-cycle 실제 구동 — 비효율적이어도 관련 사용자 명령과 실행 cycle을 한 번씩 실제 entrypoint로 통과시키는 검증이다.
|
||||
- 실제 외부 CLI 검증 — `claude`, `antigravity`, `codex`, `opencode`처럼 외부 CLI 설치와 계정/환경이 필요한 기준 profile을 실제 호출하는 검증이다.
|
||||
|
|
@ -69,6 +84,11 @@ last_rule_updated_at: 2026-07-26
|
|||
- 보조 E2E smoke에서는 최소한 node 등록, `/nodes` 확인, console 메시지 전송, delta/message 출력, complete event를 확인한다.
|
||||
- task-loop의 unit 또는 integration simulation은 실제 provider process, provider session, 네트워크 호출을 실행하지 않는다. 실행 outcome이 필요한 경우 highest runtime port를 deterministic fake로 대체하고 provider command가 호출되지 않았음을 assertion으로 남긴다.
|
||||
- task-loop dry-run 상태 전이 테스트는 runtime reader만 사용한다. execution을 검증해야 하면 fake provider의 입력·반환 locator·호출 횟수를 명시하고 provider command가 호출되지 않았음을 함께 검증한다.
|
||||
- dispatcher/selector의 unit 또는 integration simulation은 실제 `pi`, `agy`, `claude`, `codex` provider process, provider session, 네트워크 호출을 실행하지 않는다. 실행 outcome이 필요한 경우 가장 높은 runner seam을 deterministic fake로 대체하고 provider command 생성·subprocess 실행이 없었음을 assertion으로 남긴다.
|
||||
- Inventory query는 selector 없는 경우 bounded environment projection만 반환하고, model/node/provider selector는 exact match와 stable path ordering을 유지한다.
|
||||
- Readability audit는 공통 Agent-Ops rules/skills와 생성물을 제외한 project-owned tracked/worktree 입력을 deterministic하게 측정하고, `--check`에서는 새롭거나 증가한 violation만 실패시키는 ratchet을 유지한다.
|
||||
- `cmd/iop-provider-smoke`는 `-redact` 없이 실행 evidence를 만들지 않고 provider output, credential, token과 private endpoint를 출력하지 않는다. 이 live smoke를 dispatcher unit/integration simulation 경로로 호출하지 않는다.
|
||||
- `iop-agent`는 `agent-task` 밖의 unit/integration/compiled-binary test, parity 또는 validation 검증에서만 실행한다. 이 경우 deterministic test fixture 또는 temporary test state를 사용하고 실제 provider process를 시작하지 않는다. dispatcher, worker, self-check, official review 또는 PLAN/CODE_REVIEW final verification 안에서는 테스트 목적이라도 `iop-agent`를 실행하지 않는다.
|
||||
- header만 가진 PLAN/CODE_REVIEW fixture 또는 action item이 없는 fixture는 provider prompt가 될 수 없다. 그런 fixture는 dry-run, empty task scan, 또는 fake runner 아래에서만 사용한다.
|
||||
- 새 task-loop test는 기본 provider-deny guard를 설치하고, 실제 invocation 결과를 의도적으로 검증하는 test만 해당 guard 위에 명시 fake provider를 둔다. 새 test가 guard 없이 runner 경로를 열면 실패해야 한다.
|
||||
- 실제 외부 CLI 검증은 사용자가 요구한 full-cycle/profile 검증으로 명시적으로 분리할 때만 수행한다. retained reference fixture 또는 agent-task plan fixture를 그 검증의 실행 경로로 사용하지 않는다.
|
||||
|
|
@ -148,6 +168,7 @@ terminated session default node=test-node
|
|||
- `make test-e2e`, `scripts/e2e-smoke.sh`, `scripts/e2e-openai-ollama.sh`, `scripts/e2e-control-plane-edge-wire.sh`, 또는 smoke 통과 출력만으로 완료 처리하지 않는다.
|
||||
- 관련 작업 후 full-cycle 실제 구동을 비용이 크다는 이유만으로 생략하지 않는다.
|
||||
- task-loop unit/integration test에서 실제 provider CLI 또는 provider session을 시작하지 않는다.
|
||||
- `iop-agent` 또는 `iop-agent task-loop`을 production dispatcher의 대체 실행 경로로 사용하지 않는다. 활성 작업 실행은 명시적 사용자 요청에 따른 Python dispatcher만 허용하며, 그 실행 안에서 `iop-agent` test·parity·validation을 호출하지 않는다.
|
||||
- action item이 없는 plan fixture를 live task-loop worker/review 입력으로 사용하지 않는다.
|
||||
- state-only test가 실제 runner 호출을 필요로 한다고 가정하지 않는다. fake runner 또는 empty scan으로 state transition을 격리하지 못하면 test plan을 먼저 보완한다.
|
||||
- provider 실행을 mock하지 않은 채 실제 provider가 우연히 종료·응답했다는 결과를 unit/integration test evidence로 기록하지 않는다.
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@
|
|||
- Edge/Node 앱 설정 구조 변경 시 `packages/go/config`의 struct/default와 `configs/*.yaml` 예시를 함께 확인한다. Control Plane 로컬 설정 구조 변경 시 `apps/control-plane`의 config loader와 `configs/control-plane.yaml` 예시를 함께 확인한다.
|
||||
- 테스트는 변경 범위에 맞춰 `go test ./...` 또는 대상 패키지 테스트를 실행한다.
|
||||
- 사용자 실행 파이프라인에 닿는 작업을 한 경우, 작업 완료 후 `agent-ops/rules/project/domain/testing/rules.md`의 검증 기준을 따른다.
|
||||
- 활성 `agent-task`의 dry-run, worker/review 실행, blocked retry와 상태 관찰은 사용자의 명시적 실행 요청이 있을 때만 `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` dispatcher로 수행한다. dispatcher는 이 프로젝트의 production orchestration 경로로 유지한다.
|
||||
- `iop-agent`는 `agent-task` 밖의 격리된 unit/integration/compiled-binary test, parity·validation 검증에서만 허용한다. dispatcher, worker, self-check, official review와 PLAN/CODE_REVIEW final verification을 포함한 모든 활성 `agent-task` 실행 경로에서는 `iop-agent` 실행을 허용하지 않는다.
|
||||
- field/bootstrap 작업은 `testing` domain rule을 따르고, 실제 local 환경값이 필요하면 `agent-test/local/rules.md`를 따른다.
|
||||
- Node, specialized agent, domain agent, Control Plane enrollment 등 사용자가 대상 host에서 실행하는 bootstrap/install command 작업은 `agent-ops/rules/project/domain/testing/rules.md`의 one-line bootstrap UX 기준을 따른다.
|
||||
- 상세 DB schema, event schema, permission/policy/audit model, federation, mTLS 구현 세부는 각 작업에서 별도로 결정한다.
|
||||
|
|
@ -73,7 +75,6 @@
|
|||
| 경로 패턴 | 도메인 | rules.md |
|
||||
|----------|--------|----------|
|
||||
| `apps/node/**` | node | `agent-ops/rules/project/domain/node/rules.md` |
|
||||
| `apps/agent/**` | agent | `agent-ops/rules/project/domain/agent/rules.md` |
|
||||
| `apps/edge/**` | edge | `agent-ops/rules/project/domain/edge/rules.md` |
|
||||
| `apps/control-plane/**` | control-plane | `agent-ops/rules/project/domain/control-plane/rules.md` |
|
||||
| `apps/client/**` | client | `agent-ops/rules/project/domain/client/rules.md` |
|
||||
|
|
@ -89,6 +90,11 @@
|
|||
| `docker-compose.yml` | testing | `agent-ops/rules/project/domain/testing/rules.md` |
|
||||
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/**` | testing | `agent-ops/rules/project/domain/testing/rules.md` |
|
||||
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/**` | testing | `agent-ops/rules/project/domain/testing/rules.md` |
|
||||
| `agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md` | testing | `agent-ops/rules/project/domain/testing/rules.md` |
|
||||
| `agent-ops/skills/project/orchestrate-agent-task-loop/agents/**` | testing | `agent-ops/rules/project/domain/testing/rules.md` |
|
||||
| `cmd/iop-provider-smoke/**` | testing | `agent-ops/rules/project/domain/testing/rules.md` |
|
||||
| `scripts/inventory-query/**` | testing | `agent-ops/rules/project/domain/testing/rules.md` |
|
||||
| `scripts/readability_*` | testing | `agent-ops/rules/project/domain/testing/rules.md` |
|
||||
|
||||
## 도메인 후보
|
||||
|
||||
|
|
|
|||
120
agent-task/m-iop-agent-cli-runtime/CODE_REVIEW-cloud-G07.md
Normal file
120
agent-task/m-iop-agent-cli-runtime/CODE_REVIEW-cloud-G07.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<!-- task=m-iop-agent-cli-runtime plan=2 tag=REVIEW_REVIEW_API -->
|
||||
|
||||
# Code Review Reference - REVIEW_REVIEW_API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `Implementation Checklist`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
|
||||
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
|
||||
> Finalization (`Code Review Result`, log rename, `complete.log`, archive moves, `Review-Only Checklist`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## Overview
|
||||
|
||||
date=2026-07-31
|
||||
task=m-iop-agent-cli-runtime, plan=2, tag=REVIEW_REVIEW_API
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`
|
||||
- Milestone link: [Milestone document](agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md)
|
||||
- Task ids:
|
||||
- `cli-surface`: headless binary, split config, Milestone discovery/selection/preview, lifecycle, and per-work overlay/integration/blocker observation
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- `agent-task/m-iop-agent-cli-runtime/plan_local_G07_0.log` and `agent-task/m-iop-agent-cli-runtime/code_review_cloud_G07_0.log`: first review loop, FAIL with 2 Required production/evidence findings.
|
||||
- `agent-task/m-iop-agent-cli-runtime/plan_cloud_G06_1.log` and `agent-task/m-iop-agent-cli-runtime/code_review_cloud_G07_1.log`: second review loop, FAIL with 2 Required, 0 Suggested, and 0 Nit findings.
|
||||
- Required correctness finding: `scanWorkflow` accepts a slug but also strips `m-` as though every input were a task-group identifier. The reviewer fixture `m-m-foo` failed catalog discovery with `taskloop: selected milestone "foo" has no active or archived task artifacts`.
|
||||
- Required evidence finding: the adapter test does not assert exact ordinals, both overlays, or exact blocker fields; the built-binary transcript only shows `works: 0` and still validates stdout by substring for one step.
|
||||
- Reviewer verification: the API-1, API-2, and API-3 named suites passed fresh on the current checkout, proving that the remaining defects are test-oracle gaps rather than command failures.
|
||||
- Roadmap carryover: `cli-surface` remains the only unchecked Milestone Task and still requires exact `Roadmap Completion` plus trustworthy SDD S10 headless evidence.
|
||||
|
||||
|
||||
## For the Review Agent
|
||||
|
||||
> **[REVIEW AGENT ONLY]** The finalization steps below are review-agent only. Implementing agents must not execute this section.
|
||||
|
||||
Compare implementation of each item against source files and verify that output in `Verification Results` matches code.
|
||||
Review completion means the following steps are finished:
|
||||
|
||||
1. Append verdict and `review_rework_count` / `evidence_integrity_failure` routing signals.
|
||||
2. Archive `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_2.log` and `PLAN-cloud-G07.md` → `plan_cloud_G07_2.log`.
|
||||
3. If PASS, write `complete.log` and move active task directory to `agent-task/archive/YYYY/MM/m-iop-agent-cli-runtime/`. If WARN/FAIL, fully write the next filesystem state required by the code-review skill.
|
||||
4. If PASS and `m-iop-agent-cli-runtime` is a Milestone task group, report completion event metadata. Roadmap state check and `update-roadmap` calls are runtime responsibilities.
|
||||
5. Check applicable `Review-Only Checklist` items at the final `.log` location before reporting.
|
||||
---
|
||||
|
||||
## Implementation Item Completion
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| REVIEW_REVIEW_API-1 | [ ] |
|
||||
| REVIEW_REVIEW_API-2 | [ ] |
|
||||
| REVIEW_REVIEW_API-3 | [ ] |
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] [REVIEW_REVIEW_API-1] Separate canonical Milestone slug parsing from `m-` task-group parsing and add an `m-foo` catalog/selection/inspection round-trip regression.
|
||||
- [ ] [REVIEW_REVIEW_API-2] Make adapter and compiled-binary S10 evidence assert the exact ordered two-work projection and exact stdout/stderr for every transcript step.
|
||||
- [ ] [REVIEW_REVIEW_API-3] Run the complete fresh local S10 verification matrix without real provider execution and record exact output.
|
||||
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
|
||||
|
||||
## Review-Only Checklist
|
||||
|
||||
> **[REVIEW AGENT ONLY]** This checklist is used only by the review agent.
|
||||
> Implementing agents must not modify or check this section.
|
||||
|
||||
- [ ] Append one verdict of `PASS`, `WARN`, or `FAIL` and verified `review_rework_count`, `evidence_integrity_failure` to `Code Review Result`.
|
||||
- [ ] Verify that verdict, `Dimension Assessment`, and Required/Suggested/Nit classifications match.
|
||||
- [ ] Archive active `CODE_REVIEW-*-G??.md` to `code_review_cloud_G07_2.log`.
|
||||
- [ ] Archive active `PLAN-*-G??.md` to `plan_cloud_G07_2.log`.
|
||||
- [ ] Verify that the Agent-Ops managed block in `.gitignore` unignores `agent-task/**/*.md` and `agent-task/**/*.log` and ignores `agent-roadmap/current.md`.
|
||||
- [ ] If PASS, write `complete.log` based on `agent-ops/skills/common/code-review/templates/complete-log-template.md` and leave no active `.md` files.
|
||||
- [ ] If PASS, move active task directory `agent-task/m-iop-agent-cli-runtime/` to `agent-task/archive/YYYY/MM/m-iop-agent-cli-runtime/` and update this checklist at the final archive path.
|
||||
- [ ] If PASS and `m-iop-agent-cli-runtime` is a Milestone task group, report completion event metadata for runtime, without modifying roadmap or directly calling `update-roadmap`.
|
||||
- [ ] If PASS for split work, remove empty active parent `agent-task/m-iop-agent-cli-runtime/` or verify it was kept due to remaining siblings/files.
|
||||
- [ ] If WARN/FAIL, write the next filesystem state matching code-review verdict and do not write `complete.log`.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
_Record any deviations from the plan and the rationale here._
|
||||
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
_Record key design decisions here._
|
||||
|
||||
|
||||
## Reviewer Checkpoints
|
||||
|
||||
- Verify that canonical Milestone slugs are parsed without stripping `m-`, while only the task-group boundary strips exactly one `m-` prefix.
|
||||
- Verify that the `m-foo` canonical slug round-trips through catalog discovery, exact selection, persisted selection, workflow snapshot, and `InspectTaskGroup("m-m-foo")`.
|
||||
- Verify exact ordered two-work adapter projection, including ids, states, both overlays, integrations, dispatch ordinals, scoped blocker fields, and top-level blockers.
|
||||
- Verify the compiled binary asserts complete stdout and stderr for every transcript step, including the seeded two-work status and exact `validate` output.
|
||||
- Verify no real provider CLI is started and that S10 evidence maps only to Milestone Task `cli-surface`.
|
||||
|
||||
|
||||
## Verification Results
|
||||
|
||||
### REVIEW_REVIEW_API-1 Focused Verification
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-recovery2-cache go test -count=1 ./apps/agent/internal/taskloop -run '^(TestWorkflowMilestonePrefixSlugRoundTrip|TestWorkflowMilestonesListsSelectableTaskGroups|TestWorkflowMilestonesFailClosedBoundaryMatrix|TestInspectTaskGroupRejectsInvalidIdentifiers|TestWorkflowMilestonesNormalizesArchiveCollisionSuffixes|TestWorkflowArchiveOnlyMilestoneRemainsSelectable|TestInspectTaskGroupArchiveOnly)
|
||||
## Section Ownership
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, Overview, Review Agent Instructions | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into complete.log as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Implementation Item Completion (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| Implementation Checklist (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| Review-Only Checklist | Review agent only | Implementing agent must not modify or check this section |
|
||||
| Deviations from Plan, Key Design Decisions | Implementing agent | Replace placeholder text with actual content |
|
||||
| Reviewer Checkpoints | Fixed at stub creation | Pre-filled from plan |
|
||||
| Verification Results (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a Deviations from Plan entry |
|
||||
| Code Review Result | Review agent appends | Not included in stub |
|
||||
319
agent-task/m-iop-agent-cli-runtime/PLAN-cloud-G07.md
Normal file
319
agent-task/m-iop-agent-cli-runtime/PLAN-cloud-G07.md
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
<!-- task=m-iop-agent-cli-runtime plan=2 tag=REVIEW_REVIEW_API -->
|
||||
|
||||
# Plan - Canonical Milestone prefix round-trip and exact compiled S10 evidence
|
||||
|
||||
## For the Implementing Agent
|
||||
|
||||
Filling the implementation-owned sections in `CODE_REVIEW-*-G??.md` is mandatory. Implement the checklist, run every verification command, paste actual notes and output into the active review file, leave the active Plan/Review files in place, and report ready for review. Final verdicts, log renames, `complete.log`, and archive moves belong only to the code-review skill. If blocked, record only the exact blocker, attempted commands/output, and resume condition in implementation-owned evidence fields; do not ask the user, call user-input tools, create control-plane stop files, classify the next state, archive logs, or write `complete.log`.
|
||||
|
||||
## Background
|
||||
|
||||
The second review confirmed that the fail-closed catalog tests pass but found a remaining ambiguity between canonical Milestone slugs and `m-` task-group identifiers: a valid slug beginning with `m-` is stripped twice and cannot round-trip through catalog and selection. It also confirmed that the named S10 tests still do not provide the exact two-work compiled-binary transcript or exact field assertions claimed by the review evidence. This follow-up closes those two Required findings without changing the command DTOs or shared runtime ownership.
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- `agent-task/m-iop-agent-cli-runtime/plan_local_G07_0.log` and `agent-task/m-iop-agent-cli-runtime/code_review_cloud_G07_0.log`: first review loop, FAIL with 2 Required production/evidence findings.
|
||||
- `agent-task/m-iop-agent-cli-runtime/plan_cloud_G06_1.log` and `agent-task/m-iop-agent-cli-runtime/code_review_cloud_G07_1.log`: second review loop, FAIL with 2 Required, 0 Suggested, and 0 Nit findings.
|
||||
- Required correctness finding: `scanWorkflow` accepts a slug but also strips `m-` as though every input were a task-group identifier. The reviewer fixture `m-m-foo` failed catalog discovery with `taskloop: selected milestone "foo" has no active or archived task artifacts`.
|
||||
- Required evidence finding: the adapter test does not assert exact ordinals, both overlays, or exact blocker fields; the built-binary transcript only shows `works: 0` and still validates stdout by substring for one step.
|
||||
- Reviewer verification: the API-1, API-2, and API-3 named suites passed fresh on the current checkout, proving that the remaining defects are test-oracle gaps rather than command failures.
|
||||
- Roadmap carryover: `cli-surface` remains the only unchecked Milestone Task and still requires exact `Roadmap Completion` plus trustworthy SDD S10 headless evidence.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`
|
||||
- Milestone link: [Milestone document](agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md)
|
||||
- Task ids:
|
||||
- `cli-surface`: headless binary, split config, Milestone discovery/selection/preview, lifecycle, and per-work overlay/integration/blocker observation
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## Analysis
|
||||
|
||||
### Files Read
|
||||
|
||||
- Production and callers: `apps/agent/internal/taskloop/workflow.go`, `apps/agent/internal/taskloop/module.go`, `apps/agent/cmd/agent/main.go`.
|
||||
- Tests: `apps/agent/internal/taskloop/workflow_test.go`, `apps/agent/cmd/agent/main_test.go`.
|
||||
- Contract and design: `agent-contract/index.md`, `agent-contract/inner/iop-agent-cli-runtime.md`, `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`, `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`, `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`.
|
||||
- Loop evidence: `agent-task/m-iop-agent-cli-runtime/PLAN-cloud-G06.md`, `agent-task/m-iop-agent-cli-runtime/CODE_REVIEW-cloud-G07.md`, `agent-task/m-iop-agent-cli-runtime/code_review_cloud_G07_0.log`.
|
||||
- Rules and local context: `agent-ops/rules/project/domain/agent/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`, `agent-test/local/rules.md`, `agent-test/local/testing-smoke.md`, `agent-roadmap/current.md`.
|
||||
|
||||
### SDD Criteria
|
||||
|
||||
- SDD: `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`; status `[approved]`, lock released, no unresolved user review.
|
||||
- Target: Acceptance Scenario `S10`, Milestone Task `cli-surface`.
|
||||
- Evidence Map: binary entry point and split-config command integration must produce `cli-surface` Roadmap Completion and a headless operation transcript.
|
||||
- The implementation checklist therefore requires one unambiguous catalog/selection/inspection identity round-trip, exact two-work status evidence, and exact compiled-binary stdout/stderr before the S10 completion event can be trusted.
|
||||
|
||||
### Verification Context
|
||||
|
||||
- Supplied handoff: none. Repository-native fallback used the active pair, current source/tests, the S10 contract/SDD, agent/testing domain rules, and the local testing profile.
|
||||
- Environment: local checkout `/config/workspace/iop-s0`; branch `dev`; HEAD `8760d165105fb03b0b8b62b55dd31c90f34daa44`; intentional S10 changes and unrelated pre-existing Agent-Ops changes are present.
|
||||
- Toolchain preflight: `/config/.local/bin/go`; `go version go1.26.2 linux/arm64`; `GOROOT=/config/opt/go`.
|
||||
- Fresh reviewer commands passed: API-1 (`taskloop`, 0.094s), API-2 (`command`, 0.012s; `cmd/agent`, 9.434s), API-3 (`taskloop`, 12.380s; `command`, 0.018s; `cmd/agent`, 14.444s), plus `git diff --check`.
|
||||
- Focused reviewer reproducer: a valid `m-m-foo` task group caused `scanMilestones` to fail with `taskloop: selected milestone "foo" has no active or archived task artifacts`.
|
||||
- Constraints: do not launch a real provider CLI; use the existing proof-owned no-op/fake provider fixture; do not change shared manager state semantics, public DTOs, config schema, roadmap state, SDD state, or Agent-Ops common files.
|
||||
- Gap and confidence: both defects are directly visible in source and one is reproduced; confidence is high.
|
||||
- External Verification Preflight: not applicable. Darwin remains a local cross-build, and live logged-in macOS/provider evidence is the already completed S14 scope.
|
||||
- Agent spec: `agent-spec/index.md` has no matching standalone `iop-agent` CLI spec; code, contract, SDD, and tests remain authoritative.
|
||||
|
||||
### Test Coverage Gaps
|
||||
|
||||
- Prefix slug round-trip: no test uses a canonical Milestone slug beginning with `m-`, so catalog discovery, exact selection, persisted selection, and `InspectTaskGroup` do not prove one identity.
|
||||
- Adapter projection: `TestAdapterStatusPreservesAllWork` uses nonzero ordinal predicates and omits work 2 overlay plus exact scoped blocker message/retry assertions.
|
||||
- Built binary: `TestBuiltBinaryHeadlessS10Transcript` never seeds durable work state, so every status assertion reports `works: 0`.
|
||||
- Exact streams: the compiled `validate` step uses `strings.Contains` instead of complete stdout equality.
|
||||
|
||||
### Symbol References
|
||||
|
||||
- No public symbol is renamed or removed.
|
||||
- `scanWorkflow` call sites are `Workflow.Snapshot`, `Runtime.SelectMilestone`, `scanMilestones`, and `InspectTaskGroup`.
|
||||
- `InspectTaskGroup` is called by the config-free CLI adapter path in `apps/agent/cmd/agent/main.go` and its workflow tests.
|
||||
- Keep `MilestoneView`, `WorkView`, `MilestoneEntry`, `StatusResponse`, and `WorkStatusEntry` unchanged.
|
||||
|
||||
### Split Judgment
|
||||
|
||||
Keep one plan. Slug/group normalization and exact compiled status evidence form one indivisible invariant: every catalog id must select the same workflow and the product binary must display the exact durable work projection produced by that workflow. Splitting would create another boundary where tests could pass without proving the end-to-end identity and status contract.
|
||||
|
||||
### Scope Rationale
|
||||
|
||||
- Change production behavior only in `workflow.go`; do not change command DTOs, shared `agenttask.Manager`, lifecycle transitions, durable schema, provider execution, local control, or config schema.
|
||||
- Change tests only in `workflow_test.go` and `main_test.go`; reuse the existing deterministic lifecycle fixture and built binary.
|
||||
- Keep previously modified S10 source/contract files in the write claim until final PASS, but do not edit them unless a new focused regression proves a necessary correction.
|
||||
- Do not update the roadmap, SDD, agent-spec, or Agent-Ops skills. Runtime completion-event handling owns roadmap updates after PASS.
|
||||
|
||||
### Final Routing
|
||||
|
||||
- `evaluation_mode=isolated-reassessment`; finalizer `finalize-task-policy.sh`, mode `pair`.
|
||||
- Build closures: scope/context/verification/evidence/ownership/decision all `true`; capability gap absent.
|
||||
- Build grade scores: scope `1`, state `1`, blast `1`, evidence `2`, verification `2` = `G07`; base `local-fit`, final `recovery-boundary`, lane `cloud`.
|
||||
- Review closures: scope/context/verification/evidence/ownership/decision all `true`; grade scores `1/1/1/2/2` = `G07`; route `official-review`, lane `cloud`, adapter `codex`, model `gpt-5.6-sol`, reasoning `xhigh`.
|
||||
- `large_indivisible_context=false`; positive loop risks: `boundary_contract`, `structured_interpretation` (`loop_risk_count=2`).
|
||||
- Recovery signals: `review_rework_count=2`, `evidence_integrity_failure=true`; recovery boundary matched.
|
||||
- Canonical files: `PLAN-cloud-G07.md`, `CODE_REVIEW-cloud-G07.md`.
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] [REVIEW_REVIEW_API-1] Separate canonical Milestone slug parsing from `m-` task-group parsing and add an `m-foo` catalog/selection/inspection round-trip regression.
|
||||
- [ ] [REVIEW_REVIEW_API-2] Make adapter and compiled-binary S10 evidence assert the exact ordered two-work projection and exact stdout/stderr for every transcript step.
|
||||
- [ ] [REVIEW_REVIEW_API-3] Run the complete fresh local S10 verification matrix without real provider execution and record exact output.
|
||||
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
|
||||
|
||||
### [REVIEW_REVIEW_API-1] Unambiguous Milestone slug and task-group parsing
|
||||
|
||||
**Problem**
|
||||
|
||||
`apps/agent/internal/taskloop/workflow.go:179-184` accepts a Milestone slug but strips the `m-` task-group prefix whenever the slug starts with those characters. Because `[a-z0-9][a-z0-9-]*` permits `m-foo`, discovery returns a valid id that `scanWorkflow` immediately changes to `foo`.
|
||||
|
||||
Before (`apps/agent/internal/taskloop/workflow.go:179`):
|
||||
|
||||
```go
|
||||
func scanWorkflow(root, milestone string) ([]agenttask.WorkUnit, string, error) {
|
||||
slug := milestone
|
||||
if strings.HasPrefix(milestone, taskGroupPrefix) {
|
||||
slug = strings.TrimPrefix(milestone, taskGroupPrefix)
|
||||
}
|
||||
canonicalSlug, err := parseMilestoneSlug(slug)
|
||||
```
|
||||
|
||||
**Solution**
|
||||
|
||||
- Make `scanWorkflow` accept and validate only a canonical Milestone slug; never strip a prefix there.
|
||||
- Keep task-group parsing at the boundary that actually accepts a group: `InspectTaskGroup` strips exactly one `m-`, validates the remainder, and passes the canonical slug inward.
|
||||
- Keep active/archive directory parsers responsible for their own directory grammar and return canonical slugs.
|
||||
- Add a regression using active group `m-m-foo`, catalog id `m-foo`, exact selection `m-foo`, persisted workflow snapshot, and inspection group `m-m-foo`.
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
func scanWorkflow(root, milestone string) ([]agenttask.WorkUnit, string, error) {
|
||||
canonicalSlug, err := parseMilestoneSlug(milestone)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("taskloop: selected milestone %q: %w", milestone, err)
|
||||
}
|
||||
```
|
||||
|
||||
**Modified Files and Checklist**
|
||||
|
||||
- [ ] `apps/agent/internal/taskloop/workflow.go`: remove ambiguous prefix stripping from the canonical slug path.
|
||||
- [ ] `apps/agent/internal/taskloop/workflow_test.go`: add `TestWorkflowMilestonePrefixSlugRoundTrip` covering catalog, exact selection, workflow snapshot, and config-free inspection.
|
||||
|
||||
**Test Strategy**
|
||||
|
||||
- Write `TestWorkflowMilestonePrefixSlugRoundTrip` with one `m-m-foo` fixture.
|
||||
- Assert the catalog returns exactly `m-foo`, `SelectMilestone("m-foo")` succeeds, the saved selection remains `m-foo`, `WorkflowSnapshot` returns its work, and `InspectTaskGroup("m-m-foo")` returns the same work.
|
||||
- Retain the invalid identifier and archive-collision boundary matrices.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-recovery2-cache go test -count=1 ./apps/agent/internal/taskloop -run '^(TestWorkflowMilestonePrefixSlugRoundTrip|TestWorkflowMilestonesListsSelectableTaskGroups|TestWorkflowMilestonesFailClosedBoundaryMatrix|TestInspectTaskGroupRejectsInvalidIdentifiers|TestWorkflowMilestonesNormalizesArchiveCollisionSuffixes|TestWorkflowArchiveOnlyMilestoneRemainsSelectable|TestInspectTaskGroupArchiveOnly)$'
|
||||
```
|
||||
|
||||
Expected: PASS; `m-foo` round-trips as a slug while `m-m-foo` is accepted only as its task-group identifier.
|
||||
|
||||
### [REVIEW_REVIEW_API-2] Exact adapter and compiled-binary two-work evidence
|
||||
|
||||
**Problem**
|
||||
|
||||
`apps/agent/cmd/agent/main_test.go:521-533` uses nonzero predicates instead of exact ordinals and omits work 2's overlay and exact blocker content. `apps/agent/cmd/agent/main_test.go:650-724` supplies no durable works to the built binary, asserts only `works: 0`, and treats `validate` as a substring exception.
|
||||
|
||||
Before (`apps/agent/cmd/agent/main_test.go:650`):
|
||||
|
||||
```go
|
||||
{
|
||||
name: "validate",
|
||||
args: append([]string{"validate"}, mutationBase...),
|
||||
wantStdout: "", // substring validated inside test body
|
||||
wantStderr: "",
|
||||
}
|
||||
// ...
|
||||
if step.name == "validate" {
|
||||
if !strings.Contains(stdout.String(), "validate: ok") {
|
||||
```
|
||||
|
||||
**Solution**
|
||||
|
||||
- Extract one deterministic lifecycle-state seeding helper used by the adapter assertion and the compiled-binary transcript.
|
||||
- Compare the complete ordered `[]command.WorkStatusEntry` value, including ids, states, both overlays, integrations, exact ordinals, and exact blocker code/message/retryability; compare the compatible top-level blocker list exactly.
|
||||
- Run the built binary against the same checksum-protected seeded state and add an exact status step containing both work rows and the scoped plus top-level blocker output.
|
||||
- Derive the deterministic validation revision from the fixture snapshot and compare the entire `validate` stdout; preserve separate exact stdout/stderr for every subprocess step.
|
||||
- Keep all provider execution behind the existing proof-owned no-op/fake ports and assert the expected dispatch/review/validation counts before invoking the read-only binary.
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
if err != nil || stdout.String() != step.wantStdout || stderr.String() != step.wantStderr {
|
||||
t.Fatalf("command %v: err=%v stdout=%q stderr=%q", step.args, err, stdout.String(), stderr.String())
|
||||
}
|
||||
```
|
||||
|
||||
**Modified Files and Checklist**
|
||||
|
||||
- [ ] `apps/agent/cmd/agent/main_test.go`: exact adapter equality, reusable deterministic state seed, exact validation output, and compiled-binary two-work status transcript.
|
||||
|
||||
**Test Strategy**
|
||||
|
||||
- Strengthen `TestAdapterStatusPreservesAllWork` to compare exact complete work and blocker DTOs.
|
||||
- Strengthen `TestBuiltBinaryHeadlessS10Transcript` to seed two durable works before subprocess reads, assert exact `validate` output, and include exact per-work status output.
|
||||
- Keep `TestRunFullHeadlessS10Transcript` exact and confirm all transcript steps use separate stdout/stderr.
|
||||
- Do not launch a real provider CLI.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-recovery2-cache go test -count=1 ./apps/agent/internal/command ./apps/agent/cmd/agent -run '^(TestCommandMatrix|TestCommandJSONOutputMatrix|TestStatusIncludesOverlayIntegrationAndBlockers|TestStableTextAndJSONOutput|TestRunMilestoneListAndSelectionShareCatalog|TestAdapterStatusPreservesAllWork|TestRunFullHeadlessS10Transcript|TestBuiltBinaryHeadlessS10Transcript)$'
|
||||
```
|
||||
|
||||
Expected: PASS with exact deterministic DTOs and subprocess stdout/stderr, including two work entries from the compiled binary.
|
||||
|
||||
### [REVIEW_REVIEW_API-3] Fresh S10 recovery closure
|
||||
|
||||
**Problem**
|
||||
|
||||
The named focused suites pass while the parser and transcript assertions remain incomplete. Final S10 closure therefore requires fresh execution after the new regression and exact compiled-binary oracle are present.
|
||||
|
||||
**Solution**
|
||||
|
||||
- Run every command below with task-specific caches outside the repository.
|
||||
- Preserve deterministic fake/no-op provider boundaries.
|
||||
- Record raw stdout/stderr and any exact omission reason in the active review file.
|
||||
|
||||
**Modified Files and Checklist**
|
||||
|
||||
- [ ] `agent-task/m-iop-agent-cli-runtime/CODE_REVIEW-cloud-G07.md`: complete implementation notes, decisions, deviations, and exact verification output.
|
||||
- [ ] Retain ownership of the previously modified S10 source/contract files listed below until final review; no additional edit is expected unless a focused regression proves it necessary.
|
||||
|
||||
**Test Strategy**
|
||||
|
||||
- No separate test file is added for this item; REVIEW_REVIEW_API-1/2 own the regressions.
|
||||
- Fresh focused, race, full agent, vet, build, cross-build, binary, smoke-preflight, symbol, and diff checks provide integrated evidence.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-recovery2-cache go test -count=1 ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent
|
||||
```
|
||||
|
||||
Expected: PASS with fresh execution.
|
||||
|
||||
## Modified Files Summary
|
||||
|
||||
| File | Item |
|
||||
|------|------|
|
||||
| `apps/agent/internal/taskloop/workflow.go` | REVIEW_REVIEW_API-1 |
|
||||
| `apps/agent/internal/taskloop/workflow_test.go` | REVIEW_REVIEW_API-1 |
|
||||
| `apps/agent/cmd/agent/main_test.go` | REVIEW_REVIEW_API-2 |
|
||||
| `apps/agent/internal/taskloop/module.go` | REVIEW_REVIEW_API-3 ownership carryover; no planned edit |
|
||||
| `apps/agent/internal/command/service.go` | REVIEW_REVIEW_API-3 ownership carryover; no planned behavior edit |
|
||||
| `apps/agent/internal/command/root.go` | REVIEW_REVIEW_API-3 ownership carryover; no planned behavior edit |
|
||||
| `apps/agent/internal/command/root_test.go` | REVIEW_REVIEW_API-3 ownership carryover; no planned edit |
|
||||
| `apps/agent/cmd/agent/main.go` | REVIEW_REVIEW_API-3 ownership carryover; no planned edit |
|
||||
| `agent-contract/inner/iop-agent-cli-runtime.md` | REVIEW_REVIEW_API-3 ownership carryover; no planned edit |
|
||||
| `agent-task/m-iop-agent-cli-runtime/CODE_REVIEW-cloud-G07.md` | REVIEW_REVIEW_API-3 |
|
||||
|
||||
## Final Verification
|
||||
|
||||
Fresh execution is required; cached Go test output is not acceptable. Unit/integration tests must not start a real provider CLI.
|
||||
|
||||
```bash
|
||||
gofmt -w apps/agent/internal/taskloop/workflow.go apps/agent/internal/taskloop/workflow_test.go apps/agent/cmd/agent/main_test.go
|
||||
```
|
||||
|
||||
Expected: formatting completes without error.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-recovery2-cache go test -count=1 ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent
|
||||
```
|
||||
|
||||
Expected: focused packages PASS.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-recovery2-race-cache go test -count=1 -race ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent ./apps/agent/internal/bootstrap ./packages/go/agenttask ./packages/go/agentstate
|
||||
```
|
||||
|
||||
Expected: race suites PASS using deterministic fakes/no-op children only.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-recovery2-cache go test -count=1 ./apps/agent/...
|
||||
```
|
||||
|
||||
Expected: all agent packages PASS, including the prefix-slug and exact compiled-binary transcript regressions.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-recovery2-cache go vet ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent ./apps/agent/internal/bootstrap ./packages/go/...
|
||||
```
|
||||
|
||||
Expected: no vet findings.
|
||||
|
||||
```bash
|
||||
make build-agent
|
||||
build/bin/iop-agent --help
|
||||
build/bin/iop-agent validate --repo-config configs/iop-agent.runtime.yaml --local-config configs/iop-agent.local.example.yaml --provider-catalog configs/iop-agent.providers.yaml
|
||||
```
|
||||
|
||||
Expected: binary builds, the full command surface is present, and tracked split configs validate.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-recovery2-darwin-cache GOOS=darwin GOARCH=arm64 go build -trimpath -o /tmp/iop-agent-cli-recovery2-darwin-arm64 ./apps/agent/cmd/agent
|
||||
```
|
||||
|
||||
Expected: a non-empty Darwin arm64 binary is produced outside the repository.
|
||||
|
||||
```bash
|
||||
make test-iop-agent-logged-smoke-preflight
|
||||
```
|
||||
|
||||
Expected: syntax/schema/self-test PASS and the Linux non-Darwin gate exits before provider login or launch.
|
||||
|
||||
```bash
|
||||
rg --sort path -n '\b(parseMilestoneSlug|parseActiveGroupDir|parseArchiveGroupDir|scanWorkflow|scanMilestones|InspectTaskGroup|MilestoneView|WorkStatusEntry|TestAdapterStatusPreservesAllWork|TestBuiltBinaryHeadlessS10Transcript)\b' apps/agent agent-contract/inner/iop-agent-cli-runtime.md
|
||||
git diff --check
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: parser/identity and projection call sites are intentional, no whitespace errors exist, and only declared task-owned files plus explicitly unrelated pre-existing changes are modified.
|
||||
|
||||
After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
|
||||
17
agent-task/m-iop-agent-cli-runtime/WORK_LOG.md
Normal file
17
agent-task/m-iop-agent-cli-runtime/WORK_LOG.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Milestone Work Log
|
||||
|
||||
> Dispatcher-owned execution timeline. Workers and reviewers do not edit this file.
|
||||
|
||||
| seq | time | event | task | role | attempt | model | result | locator |
|
||||
|---:|---|---|---|---|---:|---|---|---|
|
||||
| 1 | 26-07-31 17:03:37 | START | m-iop-agent-cli-runtime | worker | 0 | agy/Gemini 3.6 Flash (Medium) | running | /config/workspace/iop-s0/.git/agent-task-dispatcher/runs/20260731T080337Z__m-iop-agent-cli-runtime__p0__worker__a00/locator.json |
|
||||
| 2 | 26-07-31 17:12:54 | FINISH | m-iop-agent-cli-runtime | worker | 0 | agy/Gemini 3.6 Flash (Medium) | succeeded:0 | /config/workspace/iop-s0/.git/agent-task-dispatcher/runs/20260731T080337Z__m-iop-agent-cli-runtime__p0__worker__a00/locator.json |
|
||||
| 3 | 26-07-31 17:12:57 | START | m-iop-agent-cli-runtime | review | 0 | codex/gpt-5.6-sol xhigh | running | /config/workspace/iop-s0/.git/agent-task-dispatcher/runs/20260731T081257Z__m-iop-agent-cli-runtime__p0__review__a00/locator.json |
|
||||
| 4 | 26-07-31 17:29:08 | FINISH | m-iop-agent-cli-runtime | review | 0 | codex/gpt-5.6-sol xhigh | succeeded:0 | /config/workspace/iop-s0/.git/agent-task-dispatcher/runs/20260731T081257Z__m-iop-agent-cli-runtime__p0__review__a00/locator.json |
|
||||
| 5 | 26-07-31 17:29:08 | START | m-iop-agent-cli-runtime | worker | 0 | agy/Gemini 3.6 Flash (High) | running | /config/workspace/iop-s0/.git/agent-task-dispatcher/runs/20260731T082908Z__m-iop-agent-cli-runtime__p1__worker__a00/locator.json |
|
||||
| 6 | 26-07-31 17:34:37 | FINISH | m-iop-agent-cli-runtime | worker | 0 | agy/Gemini 3.6 Flash (High) | succeeded:0 | /config/workspace/iop-s0/.git/agent-task-dispatcher/runs/20260731T082908Z__m-iop-agent-cli-runtime__p1__worker__a00/locator.json |
|
||||
| 7 | 26-07-31 17:34:39 | START | m-iop-agent-cli-runtime | review | 0 | codex/gpt-5.6-sol xhigh | running | /config/workspace/iop-s0/.git/agent-task-dispatcher/runs/20260731T083439Z__m-iop-agent-cli-runtime__p1__review__a00/locator.json |
|
||||
| 8 | 26-07-31 18:29:16 | START | m-iop-agent-cli-runtime | worker | 0 | claude/claude-opus-4-8 xhigh | running | /config/workspace/iop-s0/.git/agent-task-dispatcher/runs/20260731T092916Z__m-iop-agent-cli-runtime__p2__worker__a00/locator.json |
|
||||
| 9 | 26-07-31 18:29:19 | FINISH | m-iop-agent-cli-runtime | worker | 0 | claude/claude-opus-4-8 xhigh | failed:provider-quota:1 | /config/workspace/iop-s0/.git/agent-task-dispatcher/runs/20260731T092916Z__m-iop-agent-cli-runtime__p2__worker__a00/locator.json |
|
||||
| 10 | 26-07-31 18:29:19 | START | m-iop-agent-cli-runtime | worker | 1 | codex/gpt-5.6-terra high | running | /config/workspace/iop-s0/.git/agent-task-dispatcher/runs/20260731T092919Z__m-iop-agent-cli-runtime__p2__worker__a01/locator.json |
|
||||
| 11 | 26-07-31 18:30:23 | FINISH | m-iop-agent-cli-runtime | worker | 1 | codex/gpt-5.6-terra high | failed:cancelled | /config/workspace/iop-s0/.git/agent-task-dispatcher/runs/20260731T092919Z__m-iop-agent-cli-runtime__p2__worker__a01/locator.json |
|
||||
412
agent-task/m-iop-agent-cli-runtime/code_review_cloud_G07_0.log
Normal file
412
agent-task/m-iop-agent-cli-runtime/code_review_cloud_G07_0.log
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
<!-- task=m-iop-agent-cli-runtime plan=0 tag=API -->
|
||||
|
||||
# Code Review Reference - API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `Implementation Checklist`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
|
||||
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
|
||||
> Finalization (`Code Review Result`, log rename, `complete.log`, archive moves, `Review-Only Checklist`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## Overview
|
||||
|
||||
date=2026-07-31
|
||||
task=m-iop-agent-cli-runtime, plan=0, tag=API
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`
|
||||
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md)
|
||||
- Task ids:
|
||||
- `cli-surface`: headless binary, split config, Milestone 조회·선택·preview, lifecycle, work별 overlay/integration/blocker 관측
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- `agent-task/archive/2026/07/m-iop-agent-cli-runtime/19+14,16,18_cli_binary_contract/complete.log`: PASS 시점에도 `cli-surface`는 partial이었고, complete S10 command transcript와 authoritative daemon/runtime wiring이 후속 조건으로 남았다.
|
||||
- `agent-task/archive/2026/07/m-iop-agent-cli-runtime/24+19,21,22,23_daemon_wiring/complete.log`: daemon/runtime wiring과 lifecycle 검증은 PASS했으므로 이 Plan은 그 ownership을 재구성하지 않는다.
|
||||
- `agent-task/archive/2026/07/m-iop-agent-cli-runtime/25+19,21,22,23,24_logged_smoke_closure/complete.log`: 로그인 macOS logged smoke와 field evidence는 PASS했다. 이번 변경은 provider 실행 경로가 아니라 S10 CLI 의미와 archive-only local regression을 닫는다.
|
||||
- Roadmap carryover: `cli-surface`만 unchecked이며 exact `Roadmap Completion`과 fresh `go test -count=1 ./apps/agent/...` PASS가 필요하다.
|
||||
|
||||
## For the Review Agent
|
||||
|
||||
> **[REVIEW AGENT ONLY]** The finalization steps below are review-agent only. Implementing agents must not execute this section.
|
||||
|
||||
Compare implementation of each item against source files and verify that output in `Verification Results` matches code.
|
||||
Review completion means the following steps are finished:
|
||||
|
||||
1. Append verdict and `review_rework_count` / `evidence_integrity_failure` routing signals.
|
||||
2. Archive `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_0.log` and `PLAN-local-G07.md` → `plan_local_G07_0.log`.
|
||||
3. If PASS, write `complete.log` and move active task directory to `agent-task/archive/YYYY/MM/m-iop-agent-cli-runtime/`. If WARN/FAIL, fully write the next filesystem state required by the code-review skill.
|
||||
4. If PASS and task group is `m-<milestone-slug>`, report completion event metadata. Roadmap state check and `update-roadmap` calls are runtime responsibilities.
|
||||
5. Check applicable `Review-Only Checklist` items at the final `.log` location before reporting.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Item Completion
|
||||
|
||||
| Item | Status |
|
||||
|------|---------|
|
||||
| API-1 | [x] |
|
||||
| API-2 | [x] |
|
||||
| API-3 | [x] |
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [x] [API-1] Implement one deterministic selectable-Milestone catalog over active and archive-only task groups, make missing active directories archive-aware, and add normal/boundary regression tests.
|
||||
- [x] [API-2] Replace work-unit-shaped Milestone output and lossy singular status fields with exact Milestone summaries and ordered per-work status DTOs, then update exact text/JSON and adapter tests.
|
||||
- [x] [API-3] Make the S10 transcript and config-free dry-run hermetic, add a built-binary headless transcript, update the S10 contract evidence, and run the complete local verification matrix without real provider execution.
|
||||
- [x] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
|
||||
|
||||
## Review-Only Checklist
|
||||
|
||||
> **[REVIEW AGENT ONLY]** This checklist is used only by the review agent.
|
||||
> Implementing agents must not modify or check this section.
|
||||
|
||||
- [x] Append one verdict of `PASS`, `WARN`, or `FAIL` and verified `review_rework_count`, `evidence_integrity_failure` to `Code Review Result`.
|
||||
- [x] Verify that verdict, `Dimension Assessment`, and Required/Suggested/Nit classifications match.
|
||||
- [x] Archive active `CODE_REVIEW-*-G??.md` to `code_review_cloud_G07_0.log`.
|
||||
- [x] Archive active `PLAN-*-G??.md` to `plan_local_G07_0.log`.
|
||||
- [x] Verify that the Agent-Ops managed block in `.gitignore` unignores `agent-task/**/*.md` and `agent-task/**/*.log` and ignores `agent-roadmap/current.md`.
|
||||
- [ ] If PASS, write `complete.log` based on `agent-ops/skills/common/code-review/templates/complete-log-template.md` and leave no active `.md` files.
|
||||
- [ ] If PASS, move active task directory `agent-task/m-iop-agent-cli-runtime/` to `agent-task/archive/YYYY/MM/m-iop-agent-cli-runtime/` and update this checklist at the final archive path.
|
||||
- [ ] If PASS and task group is `m-<milestone-slug>`, report completion event metadata for runtime, without modifying roadmap or directly calling `update-roadmap`.
|
||||
- [ ] If PASS for split work, remove empty active parent `agent-task/m-iop-agent-cli-runtime/` or verify it was kept due to remaining siblings/files.
|
||||
- [x] If WARN/FAIL, write the next filesystem state matching code-review verdict and do not write `complete.log`.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None. All verification commands were executed as specified in PLAN-local-G07.md.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
- Added `scanMilestones` in `workflow.go` to scan active `agent-task/m-*` directories and `agent-task/archive/*/*/m-*` completions, returning sorted deduplicated `MilestoneView` list with `WorkUnits` and `CompletedWorkUnits`.
|
||||
- Updated `WorkView` in `module.go` to include `DispatchOrdinal uint64`, and exposed `Runtime.Milestones(ctx, projectID)`.
|
||||
- Updated `MilestoneEntry` DTO in `service.go` to represent Milestone summaries (`ID`, `Selected`, `WorkUnits`, `CompletedWorkUnits`) instead of work-unit shapes.
|
||||
- Replaced singular `Overlay`, `Integration`, `IntegrationPos` fields in `StatusResponse` DTO with `Works []WorkStatusEntry` containing per-work `State`, `Overlay`, `Integration`, `DispatchOrdinal`, and scoped `Blocker`.
|
||||
- Updated Cobra CLI help text for `milestone list` to 'List selectable milestones for the supplied project.'
|
||||
- Updated `main.go` `MilestoneList` and `Status` adapter methods to use the new DTO structures.
|
||||
- Updated `root_test.go`, `workflow_test.go`, and `main_test.go` test suites, adding hermetic config-free dry-run fixtures, archive-only milestone tests, and `TestBuiltBinaryHeadlessS10Transcript`.
|
||||
|
||||
## Reviewer Checkpoints
|
||||
|
||||
- Every `milestone list` entry is a real `m-*` workflow identity accepted by `milestone select`; active/archive duplicates are deterministic and archive-only completed Milestones remain observable.
|
||||
- Missing active task groups are tolerated only when exact same-Milestone archive evidence exists; malformed, ambiguous, permission, and empty-workflow states remain fail-closed.
|
||||
- `StatusResponse` retains every ordered work id/state, durable dispatch ordinal, overlay, integration outcome, and scoped blocker without last-value overwrite or slice-index queue fabrication.
|
||||
- Exact text/JSON output emits stable order and empty arrays; command help describes Milestones rather than Plan work units.
|
||||
- Config-free dry-run and S10 fixtures are hermetic, the built binary transcript covers the headless surface, and deterministic tests never invoke a real provider CLI.
|
||||
- S10 contract evidence and verification commands match the implemented symbols/tests; Roadmap completion targets only `cli-surface`.
|
||||
|
||||
## Verification Results
|
||||
|
||||
Paste actual stdout/stderr for every command below. If output is too long, record an outside-repository saved-output path and the exact command that produced it. Any replacement command and reason must also be recorded in `Deviations from Plan`.
|
||||
|
||||
### API-1 Focused Verification
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-cache go test -count=1 ./apps/agent/internal/taskloop -run '^(TestWorkflowMilestonesListsSelectableTaskGroups|TestWorkflowArchiveOnlyMilestoneRemainsSelectable|TestInspectTaskGroupArchiveOnly|TestWorkflowRejectsMalformedMissingAndAmbiguousArtifacts)$'
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
ok iop/apps/agent/internal/taskloop 0.017s
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### API-2 Focused Verification
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-cache go test -count=1 ./apps/agent/internal/command ./apps/agent/cmd/agent -run '^(TestCommandMatrix|TestCommandJSONOutputMatrix|TestStatusIncludesOverlayIntegrationAndBlockers|TestStableTextAndJSONOutput|TestRunMilestoneListAndSelectionShareCatalog|TestAdapterStatusPreservesAllWork)$'
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
ok iop/apps/agent/internal/command 0.008s
|
||||
ok iop/apps/agent/cmd/agent 0.027s
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### API-3 Focused Verification
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-cache go test -count=1 ./apps/agent/cmd/agent -run '^(TestRunTaskLoopConfigFreeDryRun|TestRunFullHeadlessS10Transcript|TestBuiltBinaryHeadlessS10Transcript|TestCommandAdapterFakeProviderPersistedLifecycleRollbackAndRestart|TestRunPersistedSelectionEnablesPreviouslyUnselectedStart|TestRunServeCancellationAndCleanup|TestBuiltBinarySignalShutdownCleansDaemon)$'
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
ok iop/apps/agent/cmd/agent 7.033s
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Formatting
|
||||
|
||||
```bash
|
||||
gofmt -w apps/agent/internal/taskloop/*.go apps/agent/cmd/agent/*.go apps/agent/internal/command/*.go
|
||||
```
|
||||
|
||||
Output: Clean (no output)
|
||||
Exit status: 0
|
||||
|
||||
### Fresh Focused Packages
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-cache go test -count=1 ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
ok iop/apps/agent/internal/taskloop 4.930s
|
||||
ok iop/apps/agent/internal/command 0.011s
|
||||
ok iop/apps/agent/cmd/agent 6.484s
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Race Suites
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-race-cache go test -count=1 -race ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent ./apps/agent/internal/bootstrap ./packages/go/agenttask ./packages/go/agentstate
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
ok iop/apps/agent/internal/taskloop 25.983s
|
||||
ok iop/apps/agent/internal/command 1.093s
|
||||
ok iop/apps/agent/cmd/agent 41.535s
|
||||
ok iop/apps/agent/internal/bootstrap 16.075s
|
||||
ok iop/packages/go/agenttask 4.077s
|
||||
ok iop/packages/go/agentstate 1.245s
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Full Agent Suite
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-cache go test -count=1 ./apps/agent/...
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
ok iop/apps/agent/cmd/agent 7.089s
|
||||
ok iop/apps/agent/internal/bootstrap 3.171s
|
||||
ok iop/apps/agent/internal/clientprocess 1.702s
|
||||
ok iop/apps/agent/internal/command 0.023s
|
||||
ok iop/apps/agent/internal/host 0.004s
|
||||
ok iop/apps/agent/internal/localcontrol 0.410s
|
||||
ok iop/apps/agent/internal/projectlog 4.208s
|
||||
ok iop/apps/agent/internal/taskloop 5.853s
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Vet
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-cache go vet ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent ./apps/agent/internal/bootstrap ./packages/go/...
|
||||
```
|
||||
|
||||
Output: Clean (no output)
|
||||
Exit status: 0
|
||||
|
||||
### Build and Tracked Config
|
||||
|
||||
```bash
|
||||
make build-agent
|
||||
build/bin/iop-agent --help
|
||||
build/bin/iop-agent validate --repo-config configs/iop-agent.runtime.yaml --local-config configs/iop-agent.local.example.yaml --provider-catalog configs/iop-agent.providers.yaml
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
mkdir -p build/bin
|
||||
go build -trimpath -o build/bin/iop-agent ./apps/agent/cmd/agent
|
||||
Headless CLI for the IOP agent runtime. Provides validate, list, preview, serve, start, stop, resume, and status commands over narrow host ports.
|
||||
|
||||
Usage:
|
||||
iop-agent [command]
|
||||
|
||||
Available Commands:
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
help Help about any command
|
||||
milestone Milestone commands
|
||||
preview Preview selection and dependency verdict
|
||||
project Project commands
|
||||
provider Provider catalog commands
|
||||
resume Resume a stopped project
|
||||
serve Run the agent runtime lifecycle
|
||||
start Start a project manually
|
||||
status Show project status
|
||||
stop Stop a project manually
|
||||
task-loop Run one authoritative task-loop operator pass
|
||||
validate Validate configuration paths
|
||||
|
||||
Flags:
|
||||
-h, --help help for iop-agent
|
||||
--local-config string path to the user-local runtime config (required for mutating commands)
|
||||
--output string output format: text or json (default "text")
|
||||
--provider-catalog string path to the provider catalog YAML (required for mutating commands)
|
||||
--repo-config string path to the repo-global runtime config (required for mutating commands)
|
||||
|
||||
Use "iop-agent [command] --help" for more information about a command.
|
||||
validate: ok
|
||||
revision: sha256:df658d7c4ac4f1d939f84854d48eb11bc2ffd77c074b155fb244a55eaa8ba405
|
||||
projects: 1
|
||||
providers: 2
|
||||
profiles: 3
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Darwin Cross-Build
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-darwin-cache GOOS=darwin GOARCH=arm64 go build -trimpath -o /tmp/iop-agent-cli-surface-darwin-arm64 ./apps/agent/cmd/agent
|
||||
```
|
||||
|
||||
Output: Clean (no output)
|
||||
Exit status: 0
|
||||
|
||||
### Logged-Smoke Preflight
|
||||
|
||||
```bash
|
||||
make test-iop-agent-logged-smoke-preflight
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
bash -n scripts/e2e-iop-agent-logged-smoke.sh
|
||||
jq -e . scripts/fixtures/iop-agent-smoke-manifest.schema.json >/dev/null
|
||||
jq -e '.properties.evidence.properties.records | .minItems == 13 and .maxItems == 13' scripts/fixtures/iop-agent-smoke-manifest.schema.json >/dev/null
|
||||
./scripts/e2e-iop-agent-logged-smoke.sh --help >/dev/null
|
||||
./scripts/e2e-iop-agent-logged-smoke.sh --self-test
|
||||
logged-smoke: self-test passed (derived-evidence rejection matrix and exact-PID cleanup)
|
||||
logged-smoke: non-Darwin host gate passed
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Symbol, Diff, and Worktree Audit
|
||||
|
||||
```bash
|
||||
rg --sort path -n '\b(MilestoneEntry|MilestoneListResponse|StatusResponse|WorkStatusEntry|IntegrationPos|WorkView)\b' apps/agent agent-contract/inner/iop-agent-cli-runtime.md
|
||||
git diff --check
|
||||
git status --short
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
apps/agent/cmd/agent/main.go
|
||||
203:func (a *adapter) MilestoneList(ctx context.Context, req command.MilestoneListRequest) (command.MilestoneListResponse, error) {
|
||||
205: return command.MilestoneListResponse{}, err
|
||||
209: return command.MilestoneListResponse{}, err
|
||||
213: return command.MilestoneListResponse{}, err
|
||||
215: entries := make([]command.MilestoneEntry, 0, len(milestones))
|
||||
217: entries = append(entries, command.MilestoneEntry{
|
||||
224: return command.MilestoneListResponse{Milestones: entries}, nil
|
||||
391:func (a *adapter) Status(ctx context.Context, req command.StatusRequest) (command.StatusResponse, error) {
|
||||
393: return command.StatusResponse{}, fmt.Errorf("repo-config and local-config are required")
|
||||
398: return command.StatusResponse{}, err
|
||||
401: return command.StatusResponse{
|
||||
408: return command.StatusResponse{}, err
|
||||
412: return command.StatusResponse{}, err
|
||||
414: response := command.StatusResponse{
|
||||
430: response.Works = append(response.Works, command.WorkStatusEntry{
|
||||
|
||||
apps/agent/internal/bootstrap/module.go
|
||||
507:) (taskloop.ProjectView, taskloop.WorkView, error) {
|
||||
510: return taskloop.ProjectView{}, taskloop.WorkView{}, err
|
||||
517: return taskloop.ProjectView{}, taskloop.WorkView{}, fmt.Errorf(
|
||||
|
||||
apps/agent/internal/command/root_test.go
|
||||
44: milestoneResp MilestoneListResponse
|
||||
57: statusResp StatusResponse
|
||||
82:func (r *recordingService) MilestoneList(_ context.Context, req MilestoneListRequest) (MilestoneListResponse, error) {
|
||||
136:func (r *recordingService) Status(_ context.Context, req StatusRequest) (StatusResponse, error) {
|
||||
198: milestoneResp: MilestoneListResponse{Milestones: []MilestoneEntry{{ID: "m1", Selected: true, WorkUnits: 2, CompletedWorkUnits: 1}}},
|
||||
204: statusResp: StatusResponse{
|
||||
209: Works: []WorkStatusEntry{
|
||||
602: statusResp: StatusResponse{
|
||||
607: Works: []WorkStatusEntry{
|
||||
661: msResp := MilestoneListResponse{Milestones: []MilestoneEntry{{ID: "m1", Selected: true, WorkUnits: 1, CompletedWorkUnits: 1}}}
|
||||
|
||||
apps/agent/internal/command/service.go
|
||||
38: MilestoneList(ctx context.Context, req MilestoneListRequest) (MilestoneListResponse, error)
|
||||
66: Status(ctx context.Context, req StatusRequest) (StatusResponse, error)
|
||||
152:// MilestoneListResponse reports every Milestone declared in the workspace for
|
||||
154:type MilestoneListResponse struct {
|
||||
155: Milestones []MilestoneEntry
|
||||
158:// MilestoneEntry is one Milestone summary.
|
||||
159:type MilestoneEntry struct {
|
||||
256:// StatusResponse reports the live project status including ordered per-work status
|
||||
258:type StatusResponse struct {
|
||||
263: Works []WorkStatusEntry
|
||||
267:// WorkStatusEntry is the status projection for one work unit.
|
||||
268:type WorkStatusEntry struct {
|
||||
359:// FormatText renders a MilestoneListResponse as stable plain text.
|
||||
360:func (r MilestoneListResponse) FormatText() string {
|
||||
414:// FormatText renders a StatusResponse as stable plain text.
|
||||
415:func (r StatusResponse) FormatText() string {
|
||||
536:// FormatJSON renders a MilestoneListResponse as stable JSON.
|
||||
537:func (r MilestoneListResponse) FormatJSON() string {
|
||||
644:// FormatJSON renders a StatusResponse as stable JSON.
|
||||
645:func (r StatusResponse) FormatJSON() string {
|
||||
|
||||
apps/agent/internal/taskloop/module.go
|
||||
96: Works []WorkView
|
||||
101:type WorkView struct {
|
||||
555: workView := WorkView{
|
||||
M agent-contract/inner/iop-agent-cli-runtime.md
|
||||
M agent-ops/skills/common/code-review/SKILL.md
|
||||
M agent-ops/skills/common/plan/SKILL.md
|
||||
M agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md
|
||||
M apps/agent/cmd/agent/main.go
|
||||
M apps/agent/cmd/agent/main_test.go
|
||||
M apps/agent/internal/command/root.go
|
||||
M apps/agent/internal/command/root_test.go
|
||||
M apps/agent/internal/command/service.go
|
||||
M apps/agent/internal/taskloop/module.go
|
||||
M apps/agent/internal/taskloop/workflow.go
|
||||
M apps/agent/internal/taskloop/workflow_test.go
|
||||
?? agent-task/m-iop-agent-cli-runtime/
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## Section Ownership
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, Overview, Review Agent Instructions | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Implementation Item Completion (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| Implementation Checklist (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| Review-Only Checklist | Review agent only | Implementing agent must not modify or check this section |
|
||||
| Deviations from Plan, Key Design Decisions | Implementing agent | Replace placeholder text with actual content |
|
||||
| Reviewer Checkpoints | Fixed at stub creation | Pre-filled from plan |
|
||||
| Verification Results (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `Deviations from Plan` entry |
|
||||
| Code Review Result | Review agent appends | Not included in stub |
|
||||
|
||||
## Code Review Result
|
||||
|
||||
- Overall Verdict: FAIL
|
||||
- Dimension Assessment:
|
||||
- Correctness: Fail
|
||||
- Completeness: Fail
|
||||
- Test coverage: Fail
|
||||
- API contract: Fail
|
||||
- Code quality: Pass
|
||||
- Implementation deviation: Fail
|
||||
- Verification trust: Fail
|
||||
- Spec conformance: Fail
|
||||
- Findings:
|
||||
- Required — `apps/agent/internal/taskloop/workflow.go:199`: Milestone discovery and selection do not fail closed on exact identities or malformed workflow evidence. `scanMilestones` accepts non-canonical slugs, ignores archive traversal/stat errors, discards every `scanWorkflow` error at lines 253-255, and returns an empty successful catalog. `InspectTaskGroup` likewise accepts glob metacharacters, which flow into `readArchivedUnits`; the reviewer reproducer showed `m-*` returning archived work from `m-one`, a malformed selected workflow returning `[]` with no error, and an empty workspace returning `[]` with no error. This violates PLAN API-1 and the fail-closed workflow requirement in `agent-contract/inner/iop-agent-cli-runtime.md:75`. Introduce one canonical Milestone/task-group identity validator, enumerate archive directories without interpreting user input as a glob, normalize only the runtime-defined archive collision suffix, propagate malformed/permission errors, and return an explicit no-workflow error. Add boundary regressions for all observed variants.
|
||||
- Required — `apps/agent/cmd/agent/main_test.go:473`: the claimed per-work adapter and compiled-binary evidence is not meaningful. `TestAdapterStatusPreservesAllWork` creates no durable work records and asserts only `status.Project`; both transcript tests check substrings, neither proves the two-work state/overlay/integration/dispatch-ordinal/blocker projection, and the built-binary test combines stdout/stderr instead of asserting the planned exact streams. Seed or drive two durable work records through the real adapter, assert every ordered field and scoped blocker, and make the built-binary transcript verify exact stdout, empty stderr, catalog/list-select parity, lifecycle persistence, and the per-work status projection required by PLAN API-2/API-3 and SDD S10.
|
||||
- Routing Signals:
|
||||
- `review_rework_count=1`
|
||||
- `evidence_integrity_failure=true`
|
||||
- Next Step: Invoke the plan skill in `prepare-follow-up` mode with these raw findings, run isolated routing, archive the current pair, and materialize the validated follow-up pair.
|
||||
440
agent-task/m-iop-agent-cli-runtime/code_review_cloud_G07_1.log
Normal file
440
agent-task/m-iop-agent-cli-runtime/code_review_cloud_G07_1.log
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
<!-- task=m-iop-agent-cli-runtime plan=1 tag=REVIEW_API -->
|
||||
|
||||
# Code Review Reference - REVIEW_API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `Implementation Checklist`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
|
||||
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
|
||||
> Finalization (`Code Review Result`, log rename, `complete.log`, archive moves, `Review-Only Checklist`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## Overview
|
||||
|
||||
date=2026-07-31
|
||||
task=m-iop-agent-cli-runtime, plan=1, tag=REVIEW_API
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`
|
||||
- Milestone link: [Milestone document](agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md)
|
||||
- Task ids:
|
||||
- `cli-surface`: headless binary, split config, Milestone discovery/selection/preview, lifecycle, and per-work overlay/integration/blocker observation
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- `agent-task/m-iop-agent-cli-runtime/plan_local_G07_0.log`: first-pass S10 CLI catalog/status implementation plan.
|
||||
- `agent-task/m-iop-agent-cli-runtime/code_review_cloud_G07_0.log`: FAIL with 2 Required, 0 Suggested, and 0 Nit findings.
|
||||
- Required production finding: exact Milestone/task-group identifiers, archive collision suffixes, malformed workflows, archive traversal errors, and empty catalogs must fail closed or normalize only by the runtime-defined archive rule.
|
||||
- Required evidence finding: `TestAdapterStatusPreservesAllWork` was vacuous, and the in-process/built-binary transcripts used substring assertions without proving per-work projection or exact stdout/stderr.
|
||||
- Reviewer evidence: all three focused implementation suites passed, but focused negative reproducers proved malformed selected workflow returned empty success, `m-*` consumed another archived group, and an empty workspace returned empty success.
|
||||
- Roadmap carryover: `cli-surface` remains the only unchecked Milestone Task and still requires exact `Roadmap Completion` plus trustworthy S10 evidence.
|
||||
|
||||
## For the Review Agent
|
||||
|
||||
> **[REVIEW AGENT ONLY]** The finalization steps below are review-agent only. Implementing agents must not execute this section.
|
||||
|
||||
Compare implementation of each item against source files and verify that output in `Verification Results` matches code.
|
||||
Review completion means the following steps are finished:
|
||||
|
||||
1. Append verdict and `review_rework_count` / `evidence_integrity_failure` routing signals.
|
||||
2. Archive `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_1.log` and `PLAN-cloud-G06.md` → `plan_cloud_G06_1.log`.
|
||||
3. If PASS, write `complete.log` and move active task directory to `agent-task/archive/YYYY/MM/m-iop-agent-cli-runtime/`. If WARN/FAIL, fully write the next filesystem state required by the code-review skill.
|
||||
4. If PASS and task group is `m-<milestone-slug>`, report completion event metadata. Roadmap state check and `update-roadmap` calls are runtime responsibilities.
|
||||
5. Check applicable `Review-Only Checklist` items at the final `.log` location before reporting.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Item Completion
|
||||
|
||||
| Item | Status |
|
||||
|------|---------|
|
||||
| REVIEW_API-1 | [x] |
|
||||
| REVIEW_API-2 | [x] |
|
||||
| REVIEW_API-3 | [x] |
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [x] [REVIEW_API-1] Enforce one canonical Milestone/task-group identity and archive-collision parser across catalog, selection, and inspection; propagate malformed/traversal errors, reject empty catalogs, and add normal/boundary regressions.
|
||||
- [x] [REVIEW_API-2] Replace vacuous adapter/transcript evidence with deterministic two-work status assertions and exact in-process/built-binary stdout/stderr coverage for catalog, selection, lifecycle, and per-work projection.
|
||||
- [x] [REVIEW_API-3] Run the complete fresh local S10 verification matrix without real provider execution and record exact output.
|
||||
- [x] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
|
||||
|
||||
## Review-Only Checklist
|
||||
|
||||
> **[REVIEW AGENT ONLY]** This checklist is used only by the review agent.
|
||||
> Implementing agents must not modify or check this section.
|
||||
|
||||
- [ ] Append one verdict of `PASS`, `WARN`, or `FAIL` and verified `review_rework_count`, `evidence_integrity_failure` to `Code Review Result`.
|
||||
- [ ] Verify that verdict, `Dimension Assessment`, and Required/Suggested/Nit classifications match.
|
||||
- [x] Archive active `CODE_REVIEW-*-G??.md` to `code_review_cloud_G07_1.log`.
|
||||
- [ ] Archive active `PLAN-*-G??.md` to `plan_cloud_G06_1.log`.
|
||||
- [ ] Verify that the Agent-Ops managed block in `.gitignore` unignores `agent-task/**/*.md` and `agent-task/**/*.log` and ignores `agent-roadmap/current.md`.
|
||||
- [ ] If PASS, write `complete.log` based on `agent-ops/skills/common/code-review/templates/complete-log-template.md` and leave no active `.md` files.
|
||||
- [ ] If PASS, move active task directory `agent-task/m-iop-agent-cli-runtime/` to `agent-task/archive/YYYY/MM/m-iop-agent-cli-runtime/` and update this checklist at the final archive path.
|
||||
- [ ] If PASS and task group is `m-<milestone-slug>`, report completion event metadata for runtime, without modifying roadmap or directly calling `update-roadmap`.
|
||||
- [ ] If PASS for split work, remove empty active parent `agent-task/m-iop-agent-cli-runtime/` or verify it was kept due to remaining siblings/files.
|
||||
- [ ] If WARN/FAIL, write the next filesystem state matching code-review verdict and do not write `complete.log`.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
- Enforced one canonical ASCII Milestone slug rule matching `[a-z0-9][a-z0-9-]*` across catalog discovery, selection, and inspection, rejecting non-canonical, glob, separator, whitespace, and path-like inputs before path construction.
|
||||
- Archive group directories match either exact `m-<slug>` or normalized runtime collision form `m-<slug>_<positive integer>` without exposing collision suffixes as selectable pseudo-Milestones.
|
||||
- Replaced `filepath.Glob` in archive scanning with explicit `os.ReadDir` checks to fail closed on malformed workflows, propagate traversal errors, and fail closed when no workflow-backed Milestone exists.
|
||||
- Rewrote `TestAdapterStatusPreservesAllWork` and transcript tests in `main_test.go` to assert exact ordered two-work projections, scoped and aggregated blockers, and separate exact stdout/stderr across all steps.
|
||||
|
||||
## Reviewer Checkpoints
|
||||
|
||||
- Every active and archive-derived identity is parsed as canonical `[a-z0-9][a-z0-9-]*` before path construction; user input never becomes a glob.
|
||||
- Only the runtime-defined archive collision suffix `_<positive integer>` is normalized, and it never appears as a selectable pseudo-Milestone.
|
||||
- Malformed workflow artifacts, permission/traversal failures, and the no-workflow condition fail closed with deterministic errors.
|
||||
- Every returned Milestone id is accepted by exact selection, with deterministic deduplication, ordering, and archive completion counts.
|
||||
- The real adapter preserves two ordered work entries with exact state, overlay, integration, durable dispatch ordinal, scoped blocker, and compatible top-level blocker fields.
|
||||
- In-process and compiled-binary transcripts assert separate exact stdout/stderr for catalog, selection, persisted lifecycle, and per-work status without invoking a real provider CLI.
|
||||
- S10 evidence and final verification map only to Roadmap task `cli-surface`; roadmap and SDD state remain runtime-owned.
|
||||
|
||||
## Verification Results
|
||||
|
||||
Paste actual stdout/stderr for every command below. If output is too long, record an outside-repository saved-output path and the exact command that produced it. Any replacement command and reason must also be recorded in `Deviations from Plan`.
|
||||
|
||||
### REVIEW_API-1 Focused Verification
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-cache go test -count=1 ./apps/agent/internal/taskloop -run '^(TestWorkflowMilestonesListsSelectableTaskGroups|TestWorkflowMilestonesFailClosedBoundaryMatrix|TestInspectTaskGroupRejectsInvalidIdentifiers|TestWorkflowMilestonesNormalizesArchiveCollisionSuffixes|TestWorkflowArchiveOnlyMilestoneRemainsSelectable|TestInspectTaskGroupArchiveOnly)$'
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
ok iop/apps/agent/internal/taskloop 0.020s
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### REVIEW_API-2 Focused Verification
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-cache go test -count=1 ./apps/agent/internal/command ./apps/agent/cmd/agent -run '^(TestCommandMatrix|TestCommandJSONOutputMatrix|TestStatusIncludesOverlayIntegrationAndBlockers|TestStableTextAndJSONOutput|TestRunMilestoneListAndSelectionShareCatalog|TestAdapterStatusPreservesAllWork|TestRunFullHeadlessS10Transcript|TestBuiltBinaryHeadlessS10Transcript)$'
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
ok iop/apps/agent/internal/command 0.004s
|
||||
ok iop/apps/agent/cmd/agent 2.684s
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### REVIEW_API-3 Focused Verification
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-cache go test -count=1 ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
ok iop/apps/agent/internal/taskloop 4.012s
|
||||
ok iop/apps/agent/internal/command 0.037s
|
||||
ok iop/apps/agent/cmd/agent 6.577s
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Formatting
|
||||
|
||||
```bash
|
||||
gofmt -w apps/agent/internal/taskloop/*.go apps/agent/cmd/agent/*.go apps/agent/internal/command/*.go
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Fresh Focused Packages
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-cache go test -count=1 ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
ok iop/apps/agent/internal/taskloop 4.012s
|
||||
ok iop/apps/agent/internal/command 0.037s
|
||||
ok iop/apps/agent/cmd/agent 6.577s
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Race Suites
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-race-cache go test -count=1 -race ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent ./apps/agent/internal/bootstrap ./packages/go/agenttask ./packages/go/agentstate
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
ok iop/apps/agent/internal/taskloop 12.357s
|
||||
ok iop/apps/agent/internal/command 1.036s
|
||||
ok iop/apps/agent/cmd/agent 30.478s
|
||||
ok iop/apps/agent/internal/bootstrap 8.433s
|
||||
ok iop/packages/go/agenttask 2.948s
|
||||
ok iop/packages/go/agentstate 1.159s
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Full Agent Suite
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-cache go test -count=1 ./apps/agent/...
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
ok iop/apps/agent/cmd/agent 8.101s
|
||||
ok iop/apps/agent/internal/bootstrap 2.668s
|
||||
ok iop/apps/agent/internal/clientprocess 1.730s
|
||||
ok iop/apps/agent/internal/command 0.029s
|
||||
ok iop/apps/agent/internal/host 0.005s
|
||||
ok iop/apps/agent/internal/localcontrol 0.500s
|
||||
ok iop/apps/agent/internal/projectlog 3.636s
|
||||
ok iop/apps/agent/internal/taskloop 5.243s
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Vet
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-cache go vet ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent ./apps/agent/internal/bootstrap ./packages/go/...
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Build and Tracked Config
|
||||
|
||||
```bash
|
||||
make build-agent
|
||||
build/bin/iop-agent --help
|
||||
build/bin/iop-agent validate --repo-config configs/iop-agent.runtime.yaml --local-config configs/iop-agent.local.example.yaml --provider-catalog configs/iop-agent.providers.yaml
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
mkdir -p build/bin
|
||||
go build -trimpath -o build/bin/iop-agent ./apps/agent/cmd/agent
|
||||
Headless CLI for the IOP agent runtime. Provides validate, list, preview, serve, start, stop, resume, and status commands over narrow host ports.
|
||||
|
||||
Usage:
|
||||
iop-agent [command]
|
||||
|
||||
Available Commands:
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
help Help about any command
|
||||
milestone Milestone commands
|
||||
preview Preview selection and dependency verdict
|
||||
project Project commands
|
||||
provider Provider catalog commands
|
||||
resume Resume a stopped project
|
||||
serve Run the agent runtime lifecycle
|
||||
start Start a project manually
|
||||
status Show project status
|
||||
stop Stop a project manually
|
||||
task-loop Run one authoritative task-loop operator pass
|
||||
validate Validate configuration paths
|
||||
|
||||
Flags:
|
||||
-h, --help help for iop-agent
|
||||
--local-config string path to the user-local runtime config (required for mutating commands)
|
||||
--output string output format: text or json (default "text")
|
||||
--provider-catalog string path to the provider catalog YAML (required for mutating commands)
|
||||
--repo-config string path to the repo-global runtime config (required for mutating commands)
|
||||
|
||||
Use "iop-agent [command] --help" for more information about a command.
|
||||
validate: ok
|
||||
revision: sha256:df658d7c4ac4f1d939f84854d48eb11bc2ffd77c074b155fb244a55eaa8ba405
|
||||
projects: 1
|
||||
providers: 2
|
||||
profiles: 3
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Darwin Cross-Build
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-darwin-cache GOOS=darwin GOARCH=arm64 go build -trimpath -o /tmp/iop-agent-cli-followup-darwin-arm64 ./apps/agent/cmd/agent
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Logged-Smoke Preflight
|
||||
|
||||
```bash
|
||||
make test-iop-agent-logged-smoke-preflight
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
bash -n scripts/e2e-iop-agent-logged-smoke.sh
|
||||
jq -e . scripts/fixtures/iop-agent-smoke-manifest.schema.json >/dev/null
|
||||
jq -e '.properties.evidence.properties.records | .minItems == 13 and .maxItems == 13' scripts/fixtures/iop-agent-smoke-manifest.schema.json >/dev/null
|
||||
./scripts/e2e-iop-agent-logged-smoke.sh --help >/dev/null
|
||||
./scripts/e2e-iop-agent-logged-smoke.sh --self-test
|
||||
logged-smoke: self-test passed (derived-evidence rejection matrix and exact-PID cleanup)
|
||||
logged-smoke: non-Darwin host gate passed
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
### Symbols and Worktree
|
||||
|
||||
```bash
|
||||
rg --sort path -n '\b(MilestoneView|scanMilestones|scanWorkflow|InspectTaskGroup|MilestoneEntry|StatusResponse|WorkStatusEntry|WorkView)\b' apps/agent agent-contract/inner/iop-agent-cli-runtime.md
|
||||
git diff --check
|
||||
git status --short
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
apps/agent/cmd/agent/main.go
|
||||
215: entries := make([]command.MilestoneEntry, 0, len(milestones))
|
||||
217: entries = append(entries, command.MilestoneEntry{
|
||||
391:func (a *adapter) Status(ctx context.Context, req command.StatusRequest) (command.StatusResponse, error) {
|
||||
393: return command.StatusResponse{}, fmt.Errorf("repo-config and local-config are required")
|
||||
398: return command.StatusResponse{}, err
|
||||
401: return command.StatusResponse{
|
||||
408: return command.StatusResponse{}, err
|
||||
412: return command.StatusResponse{}, err
|
||||
414: response := command.StatusResponse{
|
||||
430: response.Works = append(response.Works, command.WorkStatusEntry{
|
||||
552: units, err := taskloop.InspectTaskGroup(root, group)
|
||||
|
||||
apps/agent/internal/bootstrap/module.go
|
||||
507:) (taskloop.ProjectView, taskloop.WorkView, error) {
|
||||
510: return taskloop.ProjectView{}, taskloop.WorkView{}, err
|
||||
517: return taskloop.ProjectView{}, taskloop.WorkView{}, fmt.Errorf(
|
||||
|
||||
apps/agent/internal/command/root_test.go
|
||||
57: statusResp StatusResponse
|
||||
136:func (r *recordingService) Status(_ context.Context, req StatusRequest) (StatusResponse, error) {
|
||||
198: milestoneResp: MilestoneListResponse{Milestones: []MilestoneEntry{{ID: "m1", Selected: true, WorkUnits: 2, CompletedWorkUnits: 1}}},
|
||||
204: statusResp: StatusResponse{
|
||||
209: Works: []WorkStatusEntry{
|
||||
602: statusResp: StatusResponse{
|
||||
607: Works: []WorkStatusEntry{
|
||||
661: msResp := MilestoneListResponse{Milestones: []MilestoneEntry{{ID: "m1", Selected: true, WorkUnits: 1, CompletedWorkUnits: 1}}}
|
||||
|
||||
apps/agent/internal/command/service.go
|
||||
66: Status(ctx context.Context, req StatusRequest) (StatusResponse, error)
|
||||
155: Milestones []MilestoneEntry
|
||||
158:// MilestoneEntry is one Milestone summary.
|
||||
159:type MilestoneEntry struct {
|
||||
256:// StatusResponse reports the live project status including ordered per-work status
|
||||
258:type StatusResponse struct {
|
||||
263: Works []WorkStatusEntry
|
||||
267:// WorkStatusEntry is the status projection for one work unit.
|
||||
268:type WorkStatusEntry struct {
|
||||
414:// FormatText renders a StatusResponse as stable plain text.
|
||||
415:func (r StatusResponse) FormatText() string {
|
||||
644:// FormatJSON renders a StatusResponse as stable JSON.
|
||||
645:func (r StatusResponse) FormatJSON() string {
|
||||
|
||||
apps/agent/internal/taskloop/module.go
|
||||
96: Works []WorkView
|
||||
101:type WorkView struct {
|
||||
442: if _, _, err := scanWorkflow(root, milestone); err != nil {
|
||||
451:) ([]MilestoneView, error) {
|
||||
460: return scanMilestones(root, selected)
|
||||
555: workView := WorkView{
|
||||
|
||||
apps/agent/internal/taskloop/workflow.go
|
||||
29:type MilestoneView struct {
|
||||
112: units, evidence, err := scanWorkflow(root, milestone)
|
||||
179:func scanWorkflow(root, milestone string) ([]agenttask.WorkUnit, string, error) {
|
||||
328:func scanMilestones(root, selected string) ([]MilestoneView, error) {
|
||||
333: views := make([]MilestoneView, 0, len(slugs))
|
||||
335: units, _, err := scanWorkflow(root, slug)
|
||||
346: views = append(views, MilestoneView{
|
||||
367:// InspectTaskGroup provides a configuration-free, read-only projection for an
|
||||
370:func InspectTaskGroup(root, group string) ([]agenttask.WorkUnit, error) {
|
||||
383: units, _, err := scanWorkflow(canonicalRoot, slug)
|
||||
|
||||
apps/agent/internal/taskloop/workflow_test.go
|
||||
154: _, err := InspectTaskGroup(workspace, id)
|
||||
156: t.Fatalf("InspectTaskGroup(%q) expected error, got nil", id)
|
||||
175: views, err := scanMilestones(workspace, "m1")
|
||||
177: t.Fatalf("scanMilestones: %v", err)
|
||||
215: units, err := InspectTaskGroup(workspace, "m-m1")
|
||||
217: t.Fatalf("InspectTaskGroup: %v", err)
|
||||
253: units, err := InspectTaskGroup(workspace, "m-m1")
|
||||
255: t.Fatalf("InspectTaskGroup: %v", err)
|
||||
293: units, err := InspectTaskGroup(workspace, "m-m1")
|
||||
295: t.Fatalf("InspectTaskGroup: %v", err)
|
||||
325: units, err := InspectTaskGroup(workspace, "m-m1")
|
||||
327: t.Fatalf("InspectTaskGroup: %v", err)
|
||||
341: _, err := InspectTaskGroup(workspace, "m-m1")
|
||||
343: t.Fatalf("InspectTaskGroup error = %v", err)
|
||||
370: _, err := InspectTaskGroup(workspace, "m-m1")
|
||||
372: t.Fatalf("InspectTaskGroup error = %v, want %q", err, test.wantErr)
|
||||
M agent-contract/inner/iop-agent-cli-runtime.md
|
||||
M agent-ops/skills/common/code-review/SKILL.md
|
||||
M agent-ops/skills/common/plan/SKILL.md
|
||||
M agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md
|
||||
M apps/agent/cmd/agent/main.go
|
||||
M apps/agent/cmd/agent/main_test.go
|
||||
M apps/agent/internal/command/root.go
|
||||
M apps/agent/internal/command/root_test.go
|
||||
M apps/agent/internal/command/service.go
|
||||
M apps/agent/internal/taskloop/module.go
|
||||
M apps/agent/internal/taskloop/workflow.go
|
||||
M apps/agent/internal/taskloop/workflow_test.go
|
||||
?? agent-task/m-iop-agent-cli-runtime/
|
||||
```
|
||||
Exit status: 0
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## Section Ownership
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, Overview, Review Agent Instructions | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Implementation Item Completion (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| Implementation Checklist (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| Review-Only Checklist | Review agent only | Implementing agent must not modify or check this section |
|
||||
| Deviations from Plan, Key Design Decisions | Implementing agent | Replace placeholder text with actual content |
|
||||
| Reviewer Checkpoints | Fixed at stub creation | Pre-filled from plan |
|
||||
| Verification Results (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `Deviations from Plan` entry |
|
||||
| Code Review Result | Review agent appends | Not included in stub |
|
||||
|
||||
## Code Review Result
|
||||
|
||||
- Overall Verdict: FAIL
|
||||
- Dimension Assessment:
|
||||
- Correctness: Fail
|
||||
- Completeness: Fail
|
||||
- Test coverage: Fail
|
||||
- API contract: Fail
|
||||
- Code quality: Pass
|
||||
- Implementation deviation: Fail
|
||||
- Verification trust: Fail
|
||||
- Spec conformance: Fail
|
||||
- Findings:
|
||||
- Required — `apps/agent/internal/taskloop/workflow.go:179`: the canonical Milestone parser still conflates a slug with a task-group identifier. `parseMilestoneSlug` accepts the canonical slug `m-foo`, and discovery returns that slug from `m-m-foo`, but `scanWorkflow` unconditionally strips a leading `m-` and looks for `m-foo` instead. The focused reviewer reproducer created the valid group `m-m-foo`; `scanMilestones` then failed with `taskloop: selected milestone "foo" has no active or archived task artifacts`. This violates REVIEW_API-1's requirement that every returned canonical id share one exact parser and remain selectable. Separate slug parsing from task-group parsing: `scanWorkflow` must accept only a canonical slug without stripping it, while `InspectTaskGroup` must strip and validate the group prefix once. Add a regression whose canonical slug itself begins with `m-` and prove catalog, exact selection, and inspection all resolve `m-m-foo` consistently.
|
||||
- Required — `apps/agent/cmd/agent/main_test.go:479`: REVIEW_API-2's trustworthy S10 transcript evidence remains incomplete even though the named suite passes. `TestAdapterStatusPreservesAllWork` accepts any nonzero dispatch ordinals, never checks work 2's overlay, and does not fix the scoped blocker message/retry projection to exact values. More importantly, `TestBuiltBinaryHeadlessS10Transcript` only observes `works: 0`, contains no compiled-binary per-work status step, and still uses a substring assertion for `validate` at lines 650-653 and 721-724. This contradicts the review artifact's claim that all steps use exact stdout/stderr and that the compiled binary proves the ordered two-work projection required by the plan and SDD S10 evidence map. Assert the exact two-work DTO, including both exact ordinals, overlays, integrations, and blocker fields; seed the same deterministic durable state for the compiled binary; add an exact built-binary status transcript with both works; and compare the complete `validate` stdout rather than a substring.
|
||||
- Routing Signals:
|
||||
- `review_rework_count=2`
|
||||
- `evidence_integrity_failure=true`
|
||||
- Next Step: Invoke the plan skill in `prepare-follow-up` mode with these raw findings, run isolated routing, archive the current pair, and materialize the validated follow-up pair.
|
||||
336
agent-task/m-iop-agent-cli-runtime/plan_cloud_G06_1.log
Normal file
336
agent-task/m-iop-agent-cli-runtime/plan_cloud_G06_1.log
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
<!-- task=m-iop-agent-cli-runtime plan=1 tag=REVIEW_API -->
|
||||
|
||||
# Plan - Fail-closed Milestone catalog and trustworthy S10 evidence
|
||||
|
||||
## For the Implementing Agent
|
||||
|
||||
Filling the implementation-owned sections in `CODE_REVIEW-*-G??.md` is mandatory. Implement the checklist, run every verification command, paste actual notes and output into the active review file, leave the active Plan/Review files in place, and report ready for review. Final verdicts, log renames, `complete.log`, and archive moves belong only to the code-review skill. If blocked, record only the exact blocker, attempted commands/output, and resume condition in implementation-owned evidence fields; do not ask the user, call user-input tools, create control-plane stop files, classify the next state, archive logs, or write `complete.log`.
|
||||
|
||||
## Background
|
||||
|
||||
The first review found that the new Milestone catalog silently omits malformed workflows, accepts glob-bearing identities, and reports an empty catalog as success instead of preserving the runtime's fail-closed contract. It also found that the named adapter and binary tests pass without proving the claimed two-work status projection or exact stdout/stderr transcript. This follow-up closes both Required findings without changing the established DTO or shared runtime ownership.
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- `agent-task/m-iop-agent-cli-runtime/plan_local_G07_0.log`: first-pass S10 CLI catalog/status implementation plan.
|
||||
- `agent-task/m-iop-agent-cli-runtime/code_review_cloud_G07_0.log`: FAIL with 2 Required, 0 Suggested, and 0 Nit findings.
|
||||
- Required production finding: exact Milestone/task-group identifiers, archive collision suffixes, malformed workflows, archive traversal errors, and empty catalogs must fail closed or normalize only by the runtime-defined archive rule.
|
||||
- Required evidence finding: `TestAdapterStatusPreservesAllWork` was vacuous, and the in-process/built-binary transcripts used substring assertions without proving per-work projection or exact stdout/stderr.
|
||||
- Reviewer evidence: all three focused implementation suites passed, but focused negative reproducers proved malformed selected workflow returned empty success, `m-*` consumed another archived group, and an empty workspace returned empty success.
|
||||
- Roadmap carryover: `cli-surface` remains the only unchecked Milestone Task and still requires exact `Roadmap Completion` plus trustworthy S10 evidence.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`
|
||||
- Milestone link: [Milestone document](agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md)
|
||||
- Task ids:
|
||||
- `cli-surface`: headless binary, split config, Milestone discovery/selection/preview, lifecycle, and per-work overlay/integration/blocker observation
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## Analysis
|
||||
|
||||
### Files Read
|
||||
|
||||
- Production: `apps/agent/internal/taskloop/workflow.go`, `apps/agent/internal/taskloop/module.go`, `apps/agent/cmd/agent/main.go`, `apps/agent/internal/command/service.go`, `apps/agent/internal/command/root.go`, `apps/agent/internal/bootstrap/module.go`, `packages/go/agenttask/types.go`.
|
||||
- Tests: `apps/agent/internal/taskloop/workflow_test.go`, `apps/agent/internal/taskloop/module_test.go`, `apps/agent/cmd/agent/main_test.go`, `apps/agent/internal/command/root_test.go`, `apps/agent/internal/bootstrap/module_test.go`.
|
||||
- Contract/roadmap: `agent-contract/index.md`, `agent-contract/inner/iop-agent-cli-runtime.md`, `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`, `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`, `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`.
|
||||
- Rules/context: `agent-ops/rules/project/domain/agent/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`, `agent-test/local/rules.md`, `agent-test/local/testing-smoke.md`, `agent-roadmap/current.md`.
|
||||
|
||||
### SDD Criteria
|
||||
|
||||
- SDD: `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`; status `[approved]`, lock released.
|
||||
- Target: Acceptance Scenario `S10`, Milestone Task `cli-surface`.
|
||||
- Evidence Map: binary entry point, split configuration, validation, discovery, selection, lifecycle, and status commands; completion requires `cli-surface` Roadmap Completion and a headless operation transcript.
|
||||
- The S10 row makes catalog fail-closed behavior, list/select parity, per-work status fidelity, and compiled-binary stream assertions part of the implementation checklist and final verification.
|
||||
|
||||
### Verification Context
|
||||
|
||||
- Supplied handoff: none. Repository-native fallback used the active pair, current source/tests, S10 contract/SDD, agent/testing domain rules, and local testing profile.
|
||||
- Environment: local checkout `/config/workspace/iop-s0`; branch `feature/iop-agent-cli-runtime`; HEAD `8760d165105fb03b0b8b62b55dd31c90f34daa44`; intentional task changes and unrelated pre-existing Agent-Ops changes are present.
|
||||
- Toolchain preflight: `/config/.local/bin/go`; `go version go1.26.2 linux/arm64`; `GOROOT=/config/opt/go`.
|
||||
- Reviewer commands: fresh API-1/API-2/API-3 focused suites passed. A focused negative reproducer failed as expected with three concrete violations: malformed selected workflow returned `[]` without error, `m-*` returned archived `m-one` work, and an empty workspace returned `[]` without error.
|
||||
- Applied criteria: fresh `-count=1` tests, deterministic fixtures, no real provider process, built binary transcript, race/vet/build/Darwin cross-build, logged-smoke preflight, deterministic symbol search, and `git diff --check`.
|
||||
- Constraints: do not launch a real provider CLI; do not modify shared manager state semantics, DTO shape, roadmap state, or Agent-Ops common files. Existing task-owned files remain in the write claim until PASS even when no new edit is expected.
|
||||
- Gaps/confidence: the defects and missing assertions are directly reproducible; confidence is high.
|
||||
- External Verification Preflight: not applicable. Darwin is a local cross-build, and live logged-in macOS/provider evidence remains the already completed S14 scope.
|
||||
|
||||
### Test Coverage Gaps
|
||||
|
||||
- Exact identity: no test rejects glob metacharacters, uppercase/underscore/non-canonical slugs, or path-like task-group inputs across selection and config-free inspection.
|
||||
- Catalog failure: no test requires `Milestones` to propagate malformed selected workflow evidence, archive traversal errors, or the no-workflow condition.
|
||||
- Archive collision: no test proves `m-<slug>_<n>` archive collision directories normalize to the original Milestone instead of becoming selectable pseudo-Milestones.
|
||||
- Adapter projection: `TestAdapterStatusPreservesAllWork` creates no durable works and asserts only the project id.
|
||||
- Transcript fidelity: in-process and built-binary tests use substring checks; built-binary output merges stdout/stderr and neither transcript proves the two-work status fields.
|
||||
|
||||
### Symbol References
|
||||
|
||||
- No existing public symbol is renamed or removed.
|
||||
- Add one internal canonical Milestone/task-group parser and use it from `scanWorkflow`, `scanMilestones`, `InspectTaskGroup`, and selection through the existing `scanWorkflow` call.
|
||||
- `MilestoneView`, `WorkView`, `MilestoneEntry`, `WorkStatusEntry`, and their current call sites remain unchanged.
|
||||
|
||||
### Split Judgment
|
||||
|
||||
Keep one plan. The production fix and evidence repair share one invariant: every catalog entry and operator-supplied task group must resolve to the same exact workflow parser, and the CLI evidence must prove that parser plus the resulting per-work projection through the compiled entry point. Splitting would permit another untrustworthy PASS boundary.
|
||||
|
||||
### Scope Rationale
|
||||
|
||||
- Keep `MilestoneEntry`, `StatusResponse`, `WorkStatusEntry`, and `WorkView` shapes unchanged; the review found missing validation/evidence, not a DTO design defect.
|
||||
- Do not change `agenttask.Manager`, provider execution, lifecycle transitions, durable state schema, config schema, or local-control behavior.
|
||||
- Do not update the active roadmap or SDD. PASS completion metadata remains runtime-owned.
|
||||
- Do not modify Agent-Ops common/project skills or unrelated staged work.
|
||||
- Keep prior task-owned source/contract files in `Modified Files Summary` so shared-checkout ownership is not released before final review; only the files named in REVIEW_API-1/2 are expected to receive new implementation edits.
|
||||
|
||||
### Final Routing
|
||||
|
||||
- `evaluation_mode=isolated-reassessment`; finalizer `finalize-task-policy.sh`, mode `pair`.
|
||||
- Build closures: scope/context/verification/evidence/ownership/decision all `true`; capability gap absent.
|
||||
- Build grade: scope `1`, state `0`, blast `1`, evidence `2`, verification `2` = `G06`; base `local-fit`, final `recovery-boundary`, lane `cloud`.
|
||||
- Review closures: all `true`; grade scores `2/0/1/2/2` = `G07`; route `official-review`, lane `cloud`, adapter `codex`, model `gpt-5.6-sol`, reasoning `xhigh`.
|
||||
- `large_indivisible_context=false`; positive loop risks: `boundary_contract`, `structured_interpretation` (`loop_risk_count=2`).
|
||||
- Recovery signals: `review_rework_count=1`, `evidence_integrity_failure=true`; recovery boundary matched.
|
||||
- Canonical files: `PLAN-cloud-G06.md`, `CODE_REVIEW-cloud-G07.md`.
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] [REVIEW_API-1] Enforce one canonical Milestone/task-group identity and archive-collision parser across catalog, selection, and inspection; propagate malformed/traversal errors, reject empty catalogs, and add normal/boundary regressions.
|
||||
- [ ] [REVIEW_API-2] Replace vacuous adapter/transcript evidence with deterministic two-work status assertions and exact in-process/built-binary stdout/stderr coverage for catalog, selection, lifecycle, and per-work projection.
|
||||
- [ ] [REVIEW_API-3] Run the complete fresh local S10 verification matrix without real provider execution and record exact output.
|
||||
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
|
||||
|
||||
### [REVIEW_API-1] Fail-closed Milestone identity and archive discovery
|
||||
|
||||
**Problem**
|
||||
|
||||
`apps/agent/internal/taskloop/workflow.go:206-255` admits any `m-` suffix that lacks separators, uses wildcard archive discovery, ignores archive stat/traversal errors, and discards `scanWorkflow` failures. `apps/agent/internal/taskloop/workflow.go:283-286` also labels its input exact while accepting glob metacharacters. The reviewer reproduced all three known variants: malformed selected workflow and empty catalog returned successful empty lists, while `m-*` consumed work from another archived Milestone.
|
||||
|
||||
Before (`apps/agent/internal/taskloop/workflow.go:206-255`):
|
||||
|
||||
```go
|
||||
if entry.IsDir() && strings.HasPrefix(entry.Name(), taskGroupPrefix) {
|
||||
slug := strings.TrimPrefix(entry.Name(), taskGroupPrefix)
|
||||
if !strings.ContainsAny(slug, "/\\\x00\r\n") {
|
||||
candidates[slug] = struct{}{}
|
||||
}
|
||||
}
|
||||
// ...
|
||||
units, _, err := scanWorkflow(root, slug)
|
||||
if err != nil || len(units) == 0 {
|
||||
continue
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**
|
||||
|
||||
- Define one ASCII canonical slug rule matching `[a-z0-9][a-z0-9-]*`; reject glob, separator, whitespace, uppercase, underscore, empty, and path-like identities before constructing any path or archive matcher.
|
||||
- Parse archive group directories as either exact `m-<slug>` or the runtime archive collision form `m-<slug>_<positive integer>`, normalizing the latter to `<slug>` without exposing it as a selectable pseudo-Milestone.
|
||||
- Enumerate active/year/month/archive directories with `os.ReadDir`/`Lstat`-style checks instead of user-influenced glob patterns; allow exact not-exist only where optional and propagate all other errors.
|
||||
- Make catalog candidates fail closed when their workflow is malformed, return an explicit error when no workflow-backed Milestone exists, and keep deterministic deduplication/sorting.
|
||||
- Apply the same canonical parser to `scanWorkflow`, `SelectMilestone` through that call, and `InspectTaskGroup`.
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
slug, err := parseMilestoneTaskGroup(group, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
candidates, err := discoverMilestoneCandidates(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, slug := range candidates {
|
||||
units, _, err := scanWorkflow(root, slug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// append deterministic summary
|
||||
}
|
||||
if len(views) == 0 {
|
||||
return nil, errors.New("taskloop: no workflow-backed milestones")
|
||||
}
|
||||
```
|
||||
|
||||
**Modified Files and Checklist**
|
||||
|
||||
- [ ] `apps/agent/internal/taskloop/workflow.go`: canonical identity/archive parser, strict directory discovery, propagated errors, empty-catalog failure.
|
||||
- [ ] `apps/agent/internal/taskloop/workflow_test.go`: exact identity, malformed/empty catalog, collision suffix, and list/select parity regression matrix.
|
||||
|
||||
**Test Strategy**
|
||||
|
||||
- Extend `TestWorkflowMilestonesListsSelectableTaskGroups` to select every returned id and require exact order/counts.
|
||||
- Add `TestWorkflowMilestonesFailClosedBoundaryMatrix` for malformed selected workflow, no workflows, invalid active/archive identities, and deterministic errors.
|
||||
- Add `TestInspectTaskGroupRejectsInvalidIdentifiers` for `m-*`, `m-foo?`, uppercase, underscore, whitespace, separators, and empty suffixes.
|
||||
- Add `TestWorkflowMilestonesNormalizesArchiveCollisionSuffixes` for base plus `_1`/`_2` archive directories, one canonical list entry, and exact archived completion counts.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-cache go test -count=1 ./apps/agent/internal/taskloop -run '^(TestWorkflowMilestonesListsSelectableTaskGroups|TestWorkflowMilestonesFailClosedBoundaryMatrix|TestInspectTaskGroupRejectsInvalidIdentifiers|TestWorkflowMilestonesNormalizesArchiveCollisionSuffixes|TestWorkflowArchiveOnlyMilestoneRemainsSelectable|TestInspectTaskGroupArchiveOnly)$'
|
||||
```
|
||||
|
||||
Expected: PASS; invalid identities and malformed/empty catalogs return deterministic errors, while base/collision archive directories produce one selectable Milestone.
|
||||
|
||||
### [REVIEW_API-2] Trustworthy adapter and compiled-binary S10 transcript
|
||||
|
||||
**Problem**
|
||||
|
||||
`apps/agent/cmd/agent/main_test.go:473-486` names a two-work preservation test but creates no work records and checks only `status.Project`. `apps/agent/cmd/agent/main_test.go:490-652` checks output substrings; the compiled-binary path uses `CombinedOutput`, so it cannot prove exact stdout or empty stderr and never asserts the claimed ordered work projection.
|
||||
|
||||
Before (`apps/agent/cmd/agent/main_test.go:473-486`):
|
||||
|
||||
```go
|
||||
status, err := adapter.Status(ctx, command.StatusRequest{Config: paths, Project: "iop-s0"})
|
||||
if err != nil {
|
||||
t.Fatalf("Status error: %v", err)
|
||||
}
|
||||
if status.Project != "iop-s0" {
|
||||
t.Fatalf("status = %#v", status)
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**
|
||||
|
||||
- Seed or drive two durable `WorkRecord` values through the existing deterministic lifecycle fixture and call the real command adapter.
|
||||
- Assert exact ordered ids, states, overlay modes, integration outcomes, durable dispatch ordinals, scoped blocker identity/retryability, and compatible top-level blocker aggregation.
|
||||
- Give both transcript tests exact expected stdout and expected-empty stderr per step. For the subprocess test, attach separate buffers instead of `CombinedOutput`.
|
||||
- Require exact two-entry Milestone catalog summaries, select only ids returned by that catalog, verify selection/lifecycle persistence across invocations, and include an exact per-work status step in the compiled-binary transcript.
|
||||
- Keep provider-deny/no-op confinement boundaries; no real provider CLI may execute.
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
if err != nil || stdout.String() != step.wantStdout || stderr.String() != step.wantStderr {
|
||||
t.Fatalf("command %v: err=%v stdout=%q stderr=%q", step.args, err, stdout.String(), stderr.String())
|
||||
}
|
||||
```
|
||||
|
||||
**Modified Files and Checklist**
|
||||
|
||||
- [ ] `apps/agent/cmd/agent/main_test.go`: meaningful adapter state fixture/assertions and exact in-process/built-binary transcripts.
|
||||
|
||||
**Test Strategy**
|
||||
|
||||
- Rewrite `TestAdapterStatusPreservesAllWork` to assert two durable work entries and both scoped/top-level blockers.
|
||||
- Strengthen `TestRunMilestoneListAndSelectionShareCatalog` to parse or exactly compare the catalog and select every listed id.
|
||||
- Strengthen `TestRunFullHeadlessS10Transcript` and `TestBuiltBinaryHeadlessS10Transcript` with exact stdout/stderr, lifecycle persistence, and two-work status output.
|
||||
- Reuse proof-owned fake/no-op child processes only; assert no real provider invocation.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-cache go test -count=1 ./apps/agent/internal/command ./apps/agent/cmd/agent -run '^(TestCommandMatrix|TestCommandJSONOutputMatrix|TestStatusIncludesOverlayIntegrationAndBlockers|TestStableTextAndJSONOutput|TestRunMilestoneListAndSelectionShareCatalog|TestAdapterStatusPreservesAllWork|TestRunFullHeadlessS10Transcript|TestBuiltBinaryHeadlessS10Transcript)$'
|
||||
```
|
||||
|
||||
Expected: PASS with exact deterministic text/JSON/stdout/stderr and no real provider process.
|
||||
|
||||
### [REVIEW_API-3] Fresh S10 closure verification
|
||||
|
||||
**Problem**
|
||||
|
||||
The first-pass focused commands passed despite not exercising the failing catalog boundaries or claimed adapter projection. S10 completion therefore needs a fresh matrix whose named tests contain meaningful assertions and whose full-cycle step is the compiled binary.
|
||||
|
||||
**Solution**
|
||||
|
||||
- Run every command below with fresh Go test execution and task-specific caches outside the repository.
|
||||
- Preserve deterministic fake/no-op provider boundaries.
|
||||
- Record raw stdout/stderr and any exact omission reason in the active review file.
|
||||
|
||||
**Modified Files and Checklist**
|
||||
|
||||
- [ ] `agent-task/m-iop-agent-cli-runtime/CODE_REVIEW-cloud-G07.md`: complete implementation notes, decisions, deviations, and exact verification output.
|
||||
- [ ] Retain ownership of the previously modified S10 source/contract files listed below until final review; no additional edit is expected unless a regression exposes a necessary correction.
|
||||
|
||||
**Test Strategy**
|
||||
|
||||
- No separate test file is added for this item; REVIEW_API-1/2 own all new regressions.
|
||||
- Fresh focused, race, full agent, vet, build, cross-build, binary, smoke-preflight, symbol, and diff checks provide integrated evidence.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-cache go test -count=1 ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent
|
||||
```
|
||||
|
||||
Expected: PASS with fresh execution.
|
||||
|
||||
## Modified Files Summary
|
||||
|
||||
| File | Item |
|
||||
|------|------|
|
||||
| `apps/agent/internal/taskloop/workflow.go` | REVIEW_API-1 |
|
||||
| `apps/agent/internal/taskloop/workflow_test.go` | REVIEW_API-1 |
|
||||
| `apps/agent/cmd/agent/main_test.go` | REVIEW_API-2 |
|
||||
| `apps/agent/internal/taskloop/module.go` | REVIEW_API-3 ownership carryover; no planned edit |
|
||||
| `apps/agent/internal/command/service.go` | REVIEW_API-3 ownership carryover; no planned behavior edit |
|
||||
| `apps/agent/internal/command/root.go` | REVIEW_API-3 ownership carryover; no planned behavior edit |
|
||||
| `apps/agent/internal/command/root_test.go` | REVIEW_API-3 ownership carryover; no planned edit |
|
||||
| `apps/agent/cmd/agent/main.go` | REVIEW_API-3 ownership carryover; no planned edit |
|
||||
| `agent-contract/inner/iop-agent-cli-runtime.md` | REVIEW_API-3 ownership carryover; no planned edit |
|
||||
| `agent-task/m-iop-agent-cli-runtime/CODE_REVIEW-cloud-G07.md` | REVIEW_API-3 |
|
||||
|
||||
## Final Verification
|
||||
|
||||
Fresh execution is required; cached Go test output is not acceptable. Unit/integration tests must not start a real provider CLI.
|
||||
|
||||
```bash
|
||||
gofmt -w apps/agent/internal/taskloop/*.go apps/agent/cmd/agent/*.go apps/agent/internal/command/*.go
|
||||
```
|
||||
|
||||
Expected: formatting completes without error.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-cache go test -count=1 ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent
|
||||
```
|
||||
|
||||
Expected: focused packages PASS.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-race-cache go test -count=1 -race ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent ./apps/agent/internal/bootstrap ./packages/go/agenttask ./packages/go/agentstate
|
||||
```
|
||||
|
||||
Expected: race suites PASS using deterministic fakes/no-op children only.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-cache go test -count=1 ./apps/agent/...
|
||||
```
|
||||
|
||||
Expected: all agent packages PASS, including strict catalog and compiled-binary transcript regressions.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-cache go vet ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent ./apps/agent/internal/bootstrap ./packages/go/...
|
||||
```
|
||||
|
||||
Expected: no vet findings.
|
||||
|
||||
```bash
|
||||
make build-agent
|
||||
build/bin/iop-agent --help
|
||||
build/bin/iop-agent validate --repo-config configs/iop-agent.runtime.yaml --local-config configs/iop-agent.local.example.yaml --provider-catalog configs/iop-agent.providers.yaml
|
||||
```
|
||||
|
||||
Expected: binary builds, full command surface is present, and tracked split configs validate.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-followup-darwin-cache GOOS=darwin GOARCH=arm64 go build -trimpath -o /tmp/iop-agent-cli-followup-darwin-arm64 ./apps/agent/cmd/agent
|
||||
```
|
||||
|
||||
Expected: a non-empty Darwin arm64 binary is produced outside the repository.
|
||||
|
||||
```bash
|
||||
make test-iop-agent-logged-smoke-preflight
|
||||
```
|
||||
|
||||
Expected: syntax/schema/self-test PASS and the Linux non-Darwin gate exits before provider login or launch.
|
||||
|
||||
```bash
|
||||
rg --sort path -n '\b(MilestoneView|scanMilestones|scanWorkflow|InspectTaskGroup|MilestoneEntry|StatusResponse|WorkStatusEntry|WorkView)\b' apps/agent agent-contract/inner/iop-agent-cli-runtime.md
|
||||
git diff --check
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: identity/parser and projection call sites are intentional, no whitespace errors exist, and only declared task-owned files plus explicitly unrelated pre-existing changes are modified.
|
||||
|
||||
After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
|
||||
383
agent-task/m-iop-agent-cli-runtime/plan_local_G07_0.log
Normal file
383
agent-task/m-iop-agent-cli-runtime/plan_local_G07_0.log
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
<!-- task=m-iop-agent-cli-runtime plan=0 tag=API -->
|
||||
|
||||
# Plan - CLI surface completion
|
||||
|
||||
## For the Implementing Agent
|
||||
|
||||
`CODE_REVIEW-*-G??.md`의 구현 소유 섹션 작성은 필수다. 아래 구현과 검증 명령을 실행하고 실제 notes/output을 채운 뒤 활성 Plan/Review 파일을 그대로 둔 채 review 준비 완료를 보고한다. 최종 verdict, log rename, `complete.log`, archive 이동은 code-review skill만 수행한다. 막히면 정확한 blocker, 시도한 명령과 출력, 재개 조건만 구현 소유 evidence 필드에 기록하며 사용자 질문, user-input 도구, control-plane stop 파일, 다음 상태 분류, archive/log/`complete.log` 생성은 하지 않는다.
|
||||
|
||||
## Background
|
||||
|
||||
`cli-surface`는 현재 Milestone의 유일한 미완료 Task지만, `milestone list`가 Milestone이 아니라 선택된 Plan의 work unit을 출력하고 `status`가 여러 work의 overlay/integration을 마지막 값 하나로 축약한다. 또한 모든 같은-Milestone task가 archive된 현재 checkout에서 config-free dry-run이 active task directory 부재를 오류로 취급한다. S10을 닫으려면 조회·선택 가능한 Milestone 집합, work별 관측 투영, archive-only 완료 상태, 실제 binary transcript를 하나의 일관된 CLI 계약으로 고정해야 한다.
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- `agent-task/archive/2026/07/m-iop-agent-cli-runtime/19+14,16,18_cli_binary_contract/complete.log`: PASS 시점에도 `cli-surface`는 partial이었고, complete S10 command transcript와 authoritative daemon/runtime wiring이 후속 조건으로 남았다.
|
||||
- `agent-task/archive/2026/07/m-iop-agent-cli-runtime/24+19,21,22,23_daemon_wiring/complete.log`: daemon/runtime wiring과 lifecycle 검증은 PASS했으므로 이 Plan은 그 ownership을 재구성하지 않는다.
|
||||
- `agent-task/archive/2026/07/m-iop-agent-cli-runtime/25+19,21,22,23,24_logged_smoke_closure/complete.log`: 로그인 macOS logged smoke와 field evidence는 PASS했다. 이번 변경은 provider 실행 경로가 아니라 S10 CLI 의미와 archive-only local regression을 닫는다.
|
||||
- Roadmap carryover: `cli-surface`만 unchecked이며 exact `Roadmap Completion`과 fresh `go test -count=1 ./apps/agent/...` PASS가 필요하다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`
|
||||
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md)
|
||||
- Task ids:
|
||||
- `cli-surface`: headless binary, split config, Milestone 조회·선택·preview, lifecycle, work별 overlay/integration/blocker 관측
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## Analysis
|
||||
|
||||
### Files Read
|
||||
|
||||
- Runtime/CLI source: `apps/agent/internal/command/root.go`, `apps/agent/internal/command/service.go`, `apps/agent/cmd/agent/main.go`, `apps/agent/internal/taskloop/workflow.go`, `apps/agent/internal/taskloop/module.go`.
|
||||
- Changed-behavior tests: `apps/agent/internal/command/root_test.go`, `apps/agent/cmd/agent/main_test.go`, `apps/agent/internal/taskloop/workflow_test.go`.
|
||||
- Related call sites and state shape: `apps/agent/internal/bootstrap/module.go`, `apps/agent/internal/bootstrap/module_test.go`, `packages/go/agenttask/types.go`.
|
||||
- Configuration/build inputs: `configs/iop-agent.runtime.yaml`, `configs/iop-agent.local.example.yaml`, `configs/iop-agent.providers.yaml`, `Makefile`, `go.mod`, `.gitignore`.
|
||||
- Domain/verification sources: `agent-ops/rules/project/domain/agent/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`, `agent-test/local/rules.md`, `agent-test/local/testing-smoke.md`.
|
||||
- Roadmap/contract sources: `agent-roadmap/current.md`, `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`, `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`, `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`, `agent-contract/index.md`, `agent-contract/inner/iop-agent-cli-runtime.md`.
|
||||
- Prior evidence: the three exact archive paths in `Archive Evidence Snapshot`.
|
||||
|
||||
### SDD Criteria
|
||||
|
||||
- SDD: `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`; 상태는 승인·잠금 해제다.
|
||||
- Target: Acceptance Scenario `S10` / Milestone Task `cli-surface`.
|
||||
- Evidence Map: binary entry point, split configuration, validation, discovery, selection, lifecycle, status commands와 authoritative `taskloop.Runtime` composition, persisted lifecycle/restart, no-real-provider deterministic evidence를 요구한다.
|
||||
- 이 기준은 checklist를 Milestone catalog/selection, work별 status, archive-only projection, built-binary transcript로 구성하게 했고, final verification에 focused/race/full Go suites, binary build/run, Darwin cross-build, logged-smoke preflight를 포함하게 했다.
|
||||
|
||||
### Verification Context
|
||||
|
||||
- Handoff: 없음. repository-native fallback으로 현재 source/tests, contract S10 change checklist, local test rules를 사용했다.
|
||||
- Environment: local checkout `/config/workspace/iop-s0`, branch `feature/iop-agent-cli-runtime`, HEAD `fc361f363ccdcc4b25c37c9d4760f4b7864c3581`, upstream `origin/feature/iop-agent-cli-runtime`, clean/synced.
|
||||
- Toolchain: `/config/.local/bin/go`, `go1.26.2 linux/arm64`; `make`와 `jq` 사용 가능. Module은 `go 1.24`이며 새 dependency는 필요 없다.
|
||||
- Applied criteria: fresh `-count=1` tests, deterministic fakes only, actual changed CLI entrypoint full-cycle, race/vet/build/cross-build, logged-smoke host preflight, `git diff --check`.
|
||||
- Baseline evidence: focused command/bootstrap/lifecycle tests와 `make build-agent`, tracked-config validate는 PASS했다. `go test -count=1 ./apps/agent/...`는 `TestRunTaskLoopConfigFreeDryRun` 하나만 active task group 부재로 FAIL했다.
|
||||
- Constraints: unit/integration tests는 real provider CLI를 실행하지 않는다. Linux에서 logged-in macOS provider smoke를 재실행하지 않으며, 이미 PASS한 S14 evidence를 유지하고 이번 checkout에서는 non-Darwin preflight gate만 검증한다.
|
||||
- Gaps/confidence: agent 전용 local profile은 없고 `testing-smoke`를 적용한다. repository-native Go tests와 Make targets가 명시적이므로 confidence는 medium-high다.
|
||||
- External Verification Preflight: required external run 없음. Darwin artifact는 local cross-build만 수행하며 `/tmp/iop-agent-cli-surface-darwin-arm64`에 생성한다. live macOS/provider 검증은 이 변경의 필수 gate가 아니고 기존 S14 evidence 범위다.
|
||||
|
||||
### Test Coverage Gaps
|
||||
|
||||
- Milestone discovery: 기존 `TestRunFullHeadlessS10Transcript`는 `milestone list`가 work id `"1"`을 포함하는지만 확인해 잘못된 의미를 고정한다. 선택 전 조회, 둘 이상의 Milestone, list/select 동일 catalog 검증이 없다.
|
||||
- Archive-only workflow: `scanWorkflow`는 archive를 읽기 전에 active directory 부재로 실패한다. `TestRunTaskLoopConfigFreeDryRun`은 repository 현재 상태에 의존하며 hermetic archive-only fixture가 없다.
|
||||
- Status projection: command tests는 singular overlay/integration 하나만 검증한다. 둘 이상의 work id/state/dispatch ordinal/overlay/integration/blocker가 손실 없이 유지되는 회귀 테스트가 없다.
|
||||
- Binary transcript: in-process `run` transcript와 serve signal binary test는 있으나 built binary로 list/select/preview/status/lifecycle 전체를 잇는 S10 transcript가 없다.
|
||||
- Contract: S10 evidence row는 lifecycle wiring을 설명하지만 Milestone catalog, per-work status, archive-only completion semantics를 명시하지 않는다.
|
||||
|
||||
### Symbol References
|
||||
|
||||
- `MilestoneEntry`의 `Aliases`, `WriteSet`, `Isolation`은 실제 Milestone summary 필드로 교체한다. 참조는 `apps/agent/internal/command/service.go`, `apps/agent/cmd/agent/main.go`, `apps/agent/internal/command/root_test.go`에만 있다.
|
||||
- `StatusResponse.Overlay`, `StatusResponse.Integration`, `StatusResponse.IntegrationPos`는 `Works []WorkStatusEntry`로 교체한다. 참조는 `apps/agent/internal/command/service.go`, `apps/agent/cmd/agent/main.go`, `apps/agent/internal/command/root_test.go`에만 있다.
|
||||
- `WorkView`에는 durable `WorkRecord.DispatchOrdinal` 투영을 추가한다. 현재 생성/소비 call site는 `apps/agent/internal/taskloop/module.go`, `apps/agent/internal/bootstrap/module.go`이며 additive change다.
|
||||
- `WorkflowSnapshot`은 제거하지 않는다. `apps/agent/cmd/agent/main.go`의 Milestone list 사용만 새 catalog API로 전환하고 bootstrap call sites는 유지한다.
|
||||
|
||||
### Split Judgment
|
||||
|
||||
단일 Plan으로 유지한다. 조회 결과에 나온 Milestone이 실제 selection parser와 동일해야 하고, 선택된 workflow의 각 work 상태와 archive-only 완료 상태가 같은 binary transcript에서 함께 증명되어야 한다. 이를 분리하면 Milestone list가 선택 불가능하거나 status가 새 runtime projection을 소비하지 않는 invalid intermediate state가 생기므로 독립 PASS 경계가 없다.
|
||||
|
||||
### Scope Rationale
|
||||
|
||||
- provider discovery/invocation, selection policy, quota/failure, daemon/local-control/client-process ownership은 변경하지 않는다.
|
||||
- repo-global/local config schema와 durable state schema는 변경하지 않는다.
|
||||
- `agenttask` integration queue 알고리즘은 변경하지 않고 이미 저장된 `DispatchOrdinal`만 읽기 projection에 노출한다.
|
||||
- active Roadmap 문서는 구현 중 수정하지 않는다. PASS review의 completion event가 `cli-surface`만 check하도록 `Roadmap Targets`를 고정한다.
|
||||
- logged-in macOS field smoke는 이미 S14에서 완료됐고 provider execution path가 변하지 않으므로 재실행하지 않는다.
|
||||
|
||||
### Final Routing
|
||||
|
||||
- evaluation_mode: `first-pass`
|
||||
- finalizer: `finalize-task-policy.sh`, mode `pair`
|
||||
- Build closures: scope/context/verification/evidence/ownership/decision 모두 `true`; capability gap 없음.
|
||||
- Build grade scores: scope `2`, state `1`, blast `1`, evidence `1`, verification `2` = `G07`; base/final route `local-fit`, lane `local`.
|
||||
- Review closures: 모두 `true`; grade `G07`; route `official-review`, lane `cloud`, adapter `codex`, model `gpt-5.6-sol`, reasoning `xhigh`.
|
||||
- `large_indivisible_context=false`; positive loop risks: `boundary_contract`, `structured_interpretation` (`loop_risk_count=2`).
|
||||
- Recovery signals: `review_rework_count=0`, `evidence_integrity_failure=false`; risk/recovery boundary 미충족.
|
||||
- Canonical files: `PLAN-local-G07.md`, `CODE_REVIEW-cloud-G07.md`.
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [x] [API-1] Implement one deterministic selectable-Milestone catalog over active and archive-only task groups, make missing active directories archive-aware, and add normal/boundary regression tests.
|
||||
- [x] [API-2] Replace work-unit-shaped Milestone output and lossy singular status fields with exact Milestone summaries and ordered per-work status DTOs, then update exact text/JSON and adapter tests.
|
||||
- [x] [API-3] Make the S10 transcript and config-free dry-run hermetic, add a built-binary headless transcript, update the S10 contract evidence, and run the complete local verification matrix without real provider execution.
|
||||
- [x] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
|
||||
|
||||
### [API-1] Selectable Milestone catalog and archive-only workflow
|
||||
|
||||
**Problem**
|
||||
|
||||
`apps/agent/internal/taskloop/workflow.go:126-131` aborts before `readArchivedUnits` when `agent-task/m-<milestone>` is absent, even when the same Milestone has valid archived completions. `apps/agent/internal/taskloop/module.go:429-444` validates selection with that parser, and there is no API that lists the exact task-group identities it accepts.
|
||||
|
||||
Before (`apps/agent/internal/taskloop/workflow.go:126-131`):
|
||||
|
||||
```go
|
||||
func scanWorkflow(root, milestone string) ([]agenttask.WorkUnit, string, error) {
|
||||
activeRoot := filepath.Join(root, "agent-task", taskGroupPrefix+milestone)
|
||||
entries, err := os.ReadDir(activeRoot)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("taskloop: read active task group: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
func scanWorkflow(root, milestone string) ([]agenttask.WorkUnit, string, error) {
|
||||
activeRoot := filepath.Join(root, "agent-task", taskGroupPrefix+milestone)
|
||||
entries, err := readOptionalTaskGroup(activeRoot)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("taskloop: read active task group: %w", err)
|
||||
}
|
||||
// Merge active units with same-Milestone complete.log evidence.
|
||||
}
|
||||
|
||||
func scanMilestones(root string) ([]MilestoneView, error) {
|
||||
// Discover exact m-* identities from active and archived task-group roots,
|
||||
// deduplicate/sort them, and summarize each through scanWorkflow.
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**
|
||||
|
||||
- Treat only `fs.ErrNotExist` for the exact active group as an empty active set; preserve fail-closed behavior for permission, malformed pair, ambiguous work id, and invalid path errors.
|
||||
- Discover candidate Milestone ids from direct `agent-task/m-*` directories and `agent-task/archive/YYYY/MM/m-*` directories, validate exact slugs, deduplicate, sort, and summarize through the same `scanWorkflow` used by selection.
|
||||
- Add `Runtime.Milestones(ctx, projectID)` returning id, selected flag, total/completed work counts. Keep `SelectMilestone` bound to `scanWorkflow`, so every listed entry is selectable and archive-only completed entries remain valid.
|
||||
- Return an explicit error when no workflow-backed Milestone exists; do not infer Roadmap order or numeric dependencies.
|
||||
|
||||
**Modified Files and Checklist**
|
||||
|
||||
- [x] `apps/agent/internal/taskloop/workflow.go`: optional active group handling, catalog discovery, deterministic summary.
|
||||
- [x] `apps/agent/internal/taskloop/module.go`: public read projection and selected-state decoration.
|
||||
- [x] `apps/agent/internal/taskloop/workflow_test.go`: active+archive catalog, archive-only selection/inspection, malformed and empty boundaries.
|
||||
|
||||
**Test Strategy**
|
||||
|
||||
- Write `TestWorkflowMilestonesListsSelectableTaskGroups`: active and archived task groups are deduplicated/sorted and all entries pass selection parsing.
|
||||
- Write `TestWorkflowArchiveOnlyMilestoneRemainsSelectable`: remove active group, create exact archive completion, assert snapshot/selection and completed counts.
|
||||
- Write `TestInspectTaskGroupArchiveOnly`: configuration-free projection returns completed units without provider/runtime construction.
|
||||
- Preserve existing malformed/missing/ambiguous artifact matrix.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-cache go test -count=1 ./apps/agent/internal/taskloop -run '^(TestWorkflowMilestonesListsSelectableTaskGroups|TestWorkflowArchiveOnlyMilestoneRemainsSelectable|TestInspectTaskGroupArchiveOnly|TestWorkflowRejectsMalformedMissingAndAmbiguousArtifacts)$'
|
||||
```
|
||||
|
||||
Expected: PASS; no provider process or durable mutation.
|
||||
|
||||
### [API-2] Lossless CLI Milestone and work status contracts
|
||||
|
||||
**Problem**
|
||||
|
||||
`apps/agent/cmd/agent/main.go:203-225` labels selected workflow work units as Milestones. `apps/agent/cmd/agent/main.go:415-430` overwrites singular overlay/integration fields while iterating ordered work, and fabricates `IntegrationPos` from slice index instead of durable dispatch ordinal.
|
||||
|
||||
Before (`apps/agent/cmd/agent/main.go:211-224`, `apps/agent/cmd/agent/main.go:415-430`):
|
||||
|
||||
```go
|
||||
snapshot, err := reader.WorkflowSnapshot(ctx, req.Project)
|
||||
entries := make([]command.MilestoneEntry, 0, len(snapshot.Units))
|
||||
for _, unit := range snapshot.Units {
|
||||
entries = append(entries, command.MilestoneEntry{
|
||||
ID: string(unit.ID),
|
||||
})
|
||||
}
|
||||
|
||||
for index, work := range view.Works {
|
||||
if work.Overlay != "" {
|
||||
response.Overlay = work.Overlay
|
||||
}
|
||||
if work.Integration != "" {
|
||||
response.Integration = work.Integration
|
||||
response.IntegrationPos = index + 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
milestones, err := reader.Milestones(ctx, req.Project)
|
||||
for _, milestone := range milestones {
|
||||
entries = append(entries, command.MilestoneEntry{
|
||||
ID: milestone.ID, Selected: milestone.Selected,
|
||||
WorkUnits: milestone.WorkUnits,
|
||||
CompletedWorkUnits: milestone.CompletedWorkUnits,
|
||||
})
|
||||
}
|
||||
|
||||
for _, work := range view.Works {
|
||||
response.Works = append(response.Works, command.WorkStatusEntry{
|
||||
ID: work.WorkUnitID, State: string(work.State),
|
||||
Overlay: work.Overlay, Integration: work.Integration,
|
||||
DispatchOrdinal: uint64(work.DispatchOrdinal),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**
|
||||
|
||||
- Redefine `MilestoneEntry` as an actual Milestone summary: id, selected, completed, work-unit count, completed-work count.
|
||||
- Add `WorkStatusEntry` and replace `StatusResponse` singular overlay/integration/position fields with stable ordered `Works`; include work id, work state, actual `DispatchOrdinal`, overlay mode, integration outcome, and scoped blocker list.
|
||||
- Project `WorkRecord.DispatchOrdinal` through `taskloop.WorkView`; never derive queue semantics from sorted index.
|
||||
- Update text and JSON output exactly, always emitting empty arrays as `[]`, and update command help from “active PLAN work units” to workflow-backed Milestones.
|
||||
- Keep top-level project blocker aggregation for compatibility with project status while preserving each work blocker in its work entry.
|
||||
|
||||
**Modified Files and Checklist**
|
||||
|
||||
- [x] `apps/agent/internal/taskloop/module.go`: add durable dispatch ordinal to `WorkView`.
|
||||
- [x] `apps/agent/internal/command/service.go`: DTOs and stable text/JSON serializers.
|
||||
- [x] `apps/agent/internal/command/root.go`: accurate Milestone list help text.
|
||||
- [x] `apps/agent/cmd/agent/main.go`: catalog and per-work projection mapping.
|
||||
- [x] `apps/agent/internal/command/root_test.go`: exact multi-entry text/JSON matrices and nil-to-empty arrays.
|
||||
- [x] `apps/agent/cmd/agent/main_test.go`: list/select parity and multi-work adapter status regression.
|
||||
|
||||
**Test Strategy**
|
||||
|
||||
- Update `TestCommandMatrix`, `TestCommandJSONOutputMatrix`, `TestStatusIncludesOverlayIntegrationAndBlockers`, and `TestStableTextAndJSONOutput` with two work entries and exact ordinal/scoped blocker assertions.
|
||||
- Add `TestRunMilestoneListAndSelectionShareCatalog`: unselected project lists both Milestones, selects one listed id, and persists it.
|
||||
- Add `TestAdapterStatusPreservesAllWork`: two durable work records retain both ids/states/overlay/integration/ordinals without last-value collapse.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-cache go test -count=1 ./apps/agent/internal/command ./apps/agent/cmd/agent -run '^(TestCommandMatrix|TestCommandJSONOutputMatrix|TestStatusIncludesOverlayIntegrationAndBlockers|TestStableTextAndJSONOutput|TestRunMilestoneListAndSelectionShareCatalog|TestAdapterStatusPreservesAllWork)$'
|
||||
```
|
||||
|
||||
Expected: PASS with exact deterministic text/JSON.
|
||||
|
||||
### [API-3] Hermetic S10 binary evidence and contract closure
|
||||
|
||||
**Problem**
|
||||
|
||||
`apps/agent/cmd/agent/main_test.go:211-220` reads the current repository instead of a fixture, so archiving the active task group breaks the full suite. `apps/agent/cmd/agent/main_test.go:428-505` calls in-process `run` and expects work id `"1"` from `milestone list`, so it neither proves correct Milestone discovery nor the built binary surface.
|
||||
|
||||
Before (`apps/agent/cmd/agent/main_test.go:211-220`, `apps/agent/cmd/agent/main_test.go:440-444`):
|
||||
|
||||
```go
|
||||
exit := run([]string{"task-loop", "--dry-run", "--task-group", "m-iop-agent-cli-runtime"}, stdout, stderr)
|
||||
|
||||
{
|
||||
name: "milestone list",
|
||||
args: append([]string{"milestone", "list", "iop-s0"}, readBase...),
|
||||
want: "1",
|
||||
},
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
root := newArchiveOnlyRepositoryFixture(t)
|
||||
t.Chdir(root)
|
||||
exit := run([]string{"task-loop", "--dry-run", "--task-group", "m-completed"}, stdout, stderr)
|
||||
|
||||
{
|
||||
name: "milestone list",
|
||||
args: append([]string{"milestone", "list", "iop-s0"}, readBase...),
|
||||
want: "milestone-1",
|
||||
},
|
||||
```
|
||||
|
||||
**Solution**
|
||||
|
||||
- Make config-free dry-run create a temp module root and exact archive-only task-group completion fixture; assert `status=complete`, zero stderr, and no repo/global state dependency.
|
||||
- Update the in-process S10 transcript so Milestone list shows actual ids, selection is limited to listed entries, status includes all work entries, and new invocations see persisted selection/lifecycle.
|
||||
- Add `TestBuiltBinaryHeadlessS10Transcript` that builds `./apps/agent/cmd/agent` once into `t.TempDir()`, runs validate/list/select/preview/status/start/stop/resume as subprocesses with temp configs/workspace, and asserts no real provider invocation by using read-only commands plus the existing proof-owned fake/no-op lifecycle fixture boundary.
|
||||
- Update `agent-contract/inner/iop-agent-cli-runtime.md` S10 evidence and change checklist with catalog/list-select parity, per-work status/ordinal, archive-only complete projection, and exact test names.
|
||||
|
||||
**Modified Files and Checklist**
|
||||
|
||||
- [x] `apps/agent/cmd/agent/main_test.go`: hermetic fixture, corrected in-process transcript, built-binary transcript.
|
||||
- [x] `agent-contract/inner/iop-agent-cli-runtime.md`: S10 evidence/verification contract.
|
||||
- [x] `agent-task/m-iop-agent-cli-runtime/CODE_REVIEW-cloud-G07.md`: record actual implementation decisions and raw verification output.
|
||||
|
||||
**Test Strategy**
|
||||
|
||||
- Update `TestRunTaskLoopConfigFreeDryRun` to be hermetic and require `status=complete`.
|
||||
- Update `TestRunFullHeadlessS10Transcript` to assert real Milestone ids and per-work status.
|
||||
- Write `TestBuiltBinaryHeadlessS10Transcript` for the compiled entrypoint and exact exit/stdout/stderr behavior.
|
||||
- Keep fake-provider lifecycle/rollback/restart and daemon signal tests as non-real-provider regression evidence.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-cache go test -count=1 ./apps/agent/cmd/agent -run '^(TestRunTaskLoopConfigFreeDryRun|TestRunFullHeadlessS10Transcript|TestBuiltBinaryHeadlessS10Transcript|TestCommandAdapterFakeProviderPersistedLifecycleRollbackAndRestart|TestRunPersistedSelectionEnablesPreviouslyUnselectedStart|TestRunServeCancellationAndCleanup|TestBuiltBinarySignalShutdownCleansDaemon)$'
|
||||
```
|
||||
|
||||
Expected: PASS; test logs show no real provider CLI launch.
|
||||
|
||||
## Modified Files Summary
|
||||
|
||||
| File | Item |
|
||||
|------|------|
|
||||
| `apps/agent/internal/taskloop/workflow.go` | API-1 |
|
||||
| `apps/agent/internal/taskloop/module.go` | API-1, API-2 |
|
||||
| `apps/agent/internal/taskloop/workflow_test.go` | API-1 |
|
||||
| `apps/agent/internal/command/service.go` | API-2 |
|
||||
| `apps/agent/internal/command/root.go` | API-2 |
|
||||
| `apps/agent/internal/command/root_test.go` | API-2 |
|
||||
| `apps/agent/cmd/agent/main.go` | API-2 |
|
||||
| `apps/agent/cmd/agent/main_test.go` | API-2, API-3 |
|
||||
| `agent-contract/inner/iop-agent-cli-runtime.md` | API-3 |
|
||||
| `agent-task/m-iop-agent-cli-runtime/CODE_REVIEW-cloud-G07.md` | API-3 |
|
||||
|
||||
## Final Verification
|
||||
|
||||
Fresh execution is required; cached Go test output is not acceptable.
|
||||
|
||||
```bash
|
||||
gofmt -w apps/agent/internal/taskloop/*.go apps/agent/cmd/agent/*.go apps/agent/internal/command/*.go
|
||||
```
|
||||
|
||||
Expected: formatting completes without error.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-cache go test -count=1 ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent
|
||||
```
|
||||
|
||||
Expected: focused packages PASS.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-race-cache go test -count=1 -race ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent ./apps/agent/internal/bootstrap ./packages/go/agenttask ./packages/go/agentstate
|
||||
```
|
||||
|
||||
Expected: race suites PASS with deterministic fakes and no real provider process.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-cache go test -count=1 ./apps/agent/...
|
||||
```
|
||||
|
||||
Expected: all agent packages PASS, including archive-only config-free dry-run.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-cache go vet ./apps/agent/internal/taskloop ./apps/agent/internal/command ./apps/agent/cmd/agent ./apps/agent/internal/bootstrap ./packages/go/...
|
||||
```
|
||||
|
||||
Expected: no vet findings.
|
||||
|
||||
```bash
|
||||
make build-agent
|
||||
build/bin/iop-agent --help
|
||||
build/bin/iop-agent validate --repo-config configs/iop-agent.runtime.yaml --local-config configs/iop-agent.local.example.yaml --provider-catalog configs/iop-agent.providers.yaml
|
||||
```
|
||||
|
||||
Expected: binary builds, full command surface is shown, tracked split configs validate.
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp GOTMPDIR=/tmp GOCACHE=/tmp/iop-cli-surface-darwin-cache GOOS=darwin GOARCH=arm64 go build -trimpath -o /tmp/iop-agent-cli-surface-darwin-arm64 ./apps/agent/cmd/agent
|
||||
```
|
||||
|
||||
Expected: Darwin arm64 binary is produced outside the repository.
|
||||
|
||||
```bash
|
||||
make test-iop-agent-logged-smoke-preflight
|
||||
```
|
||||
|
||||
Expected: syntax/schema/self-test PASS and Linux host gate exits through the expected non-Darwin preflight path before provider login or process launch.
|
||||
|
||||
```bash
|
||||
rg --sort path -n '\b(MilestoneEntry|MilestoneListResponse|StatusResponse|WorkStatusEntry|IntegrationPos|WorkView)\b' apps/agent agent-contract/inner/iop-agent-cli-runtime.md
|
||||
git diff --check
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: all changed symbol call sites are intentional, `IntegrationPos` has no production/test references, no whitespace errors, and only declared task/source files are modified.
|
||||
|
||||
After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
|
||||
|
|
@ -208,18 +208,17 @@ func (a *adapter) MilestoneList(ctx context.Context, req command.MilestoneListRe
|
|||
if err != nil {
|
||||
return command.MilestoneListResponse{}, err
|
||||
}
|
||||
snapshot, err := reader.WorkflowSnapshot(ctx, req.Project)
|
||||
milestones, err := reader.Milestones(ctx, req.Project)
|
||||
if err != nil {
|
||||
return command.MilestoneListResponse{}, err
|
||||
}
|
||||
entries := make([]command.MilestoneEntry, 0, len(snapshot.Units))
|
||||
for _, unit := range snapshot.Units {
|
||||
entries := make([]command.MilestoneEntry, 0, len(milestones))
|
||||
for _, milestone := range milestones {
|
||||
entries = append(entries, command.MilestoneEntry{
|
||||
ID: string(unit.ID),
|
||||
Aliases: append([]string(nil), unit.Aliases...),
|
||||
Completed: unit.Completed,
|
||||
WriteSet: strings.Join(unit.DeclaredWriteSet, ","),
|
||||
Isolation: string(unit.IsolationMode),
|
||||
ID: milestone.ID,
|
||||
Selected: milestone.Selected,
|
||||
WorkUnits: milestone.WorkUnits,
|
||||
CompletedWorkUnits: milestone.CompletedWorkUnits,
|
||||
})
|
||||
}
|
||||
return command.MilestoneListResponse{Milestones: entries}, nil
|
||||
|
|
@ -419,14 +418,23 @@ func (a *adapter) Status(ctx context.Context, req command.StatusRequest) (comman
|
|||
StartedMilestone: view.StartedMilestone,
|
||||
Blockers: commandBlockers(view.Blockers),
|
||||
}
|
||||
for index, work := range view.Works {
|
||||
if work.Overlay != "" {
|
||||
response.Overlay = work.Overlay
|
||||
}
|
||||
if work.Integration != "" {
|
||||
response.Integration = work.Integration
|
||||
response.IntegrationPos = index + 1
|
||||
for _, work := range view.Works {
|
||||
var workBlocker *command.BlockerEntry
|
||||
if work.Blocker != nil {
|
||||
workBlocker = &command.BlockerEntry{
|
||||
Code: string(work.Blocker.Code),
|
||||
Message: work.Blocker.Message,
|
||||
Retryable: work.Blocker.Retryable,
|
||||
}
|
||||
}
|
||||
response.Works = append(response.Works, command.WorkStatusEntry{
|
||||
ID: work.WorkUnitID,
|
||||
State: string(work.State),
|
||||
Overlay: work.Overlay,
|
||||
Integration: work.Integration,
|
||||
DispatchOrdinal: work.DispatchOrdinal,
|
||||
Blocker: workBlocker,
|
||||
})
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,14 +208,31 @@ func TestRunTaskLoopDryRunUsesNoProvider(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func newArchiveOnlyRepositoryFixture(t *testing.T) string {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
writeFile(t, filepath.Join(root, "go.mod"), "module test\n\ngo 1.24\n")
|
||||
completion := filepath.Join(root, "agent-task", "archive", "2026", "07", "m-completed", "complete.log")
|
||||
if err := os.MkdirAll(filepath.Dir(completion), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeFile(t, completion, "complete\n")
|
||||
return root
|
||||
}
|
||||
|
||||
func TestRunTaskLoopConfigFreeDryRun(t *testing.T) {
|
||||
root := newArchiveOnlyRepositoryFixture(t)
|
||||
t.Chdir(root)
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
exit := run([]string{"task-loop", "--dry-run", "--task-group", "m-iop-agent-cli-runtime"}, stdout, stderr)
|
||||
exit := run([]string{"task-loop", "--dry-run", "--task-group", "m-completed"}, stdout, stderr)
|
||||
if exit != 0 {
|
||||
t.Fatalf("config-free task-loop exit=%d stdout=%q stderr=%q", exit, stdout.String(), stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "project=workspace") {
|
||||
if stderr.Len() != 0 {
|
||||
t.Fatalf("stderr = %q, want empty", stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "project=workspace") || !strings.Contains(stdout.String(), "status=complete") {
|
||||
t.Fatalf("unexpected config-free task-loop output %q", stdout.String())
|
||||
}
|
||||
}
|
||||
|
|
@ -425,6 +442,99 @@ func TestRunStartWithoutSelection(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRunMilestoneListAndSelectionShareCatalog(t *testing.T) {
|
||||
globalPath, localPath, _ := createRuntimeFixture(t)
|
||||
base := []string{"--repo-config", globalPath, "--local-config", localPath}
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
|
||||
if exit := run(append([]string{"milestone", "list", "iop-s0"}, base...), stdout, stderr); exit != 0 {
|
||||
t.Fatalf("milestone list exit = %d, stderr = %q", exit, stderr.String())
|
||||
}
|
||||
wantCatalog := "milestones: 2\n - milestone-1 selected=yes work_units=2 completed=0\n - milestone-2 selected=no work_units=1 completed=0\n"
|
||||
if stdout.String() != wantCatalog || stderr.Len() != 0 {
|
||||
t.Fatalf("milestone list stdout = %q (stderr=%q), want %q", stdout.String(), stderr.String(), wantCatalog)
|
||||
}
|
||||
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
if exit := run(append([]string{"milestone", "select", "iop-s0", "milestone-2"}, base...), stdout, stderr); exit != 0 {
|
||||
t.Fatalf("milestone select exit = %d, stderr = %q", exit, stderr.String())
|
||||
}
|
||||
wantSelect := "milestone select project=iop-s0 milestone=milestone-2\n"
|
||||
if stdout.String() != wantSelect || stderr.Len() != 0 {
|
||||
t.Fatalf("milestone select stdout = %q, want %q", stdout.String(), wantSelect)
|
||||
}
|
||||
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
if exit := run(append([]string{"status", "iop-s0"}, base...), stdout, stderr); exit != 0 {
|
||||
t.Fatalf("status exit = %d, stderr = %q", exit, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "selected_milestone: milestone-2") || stderr.Len() != 0 {
|
||||
t.Fatalf("status output = %q (stderr=%q), want selected_milestone: milestone-2", stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapterStatusPreservesAllWork(t *testing.T) {
|
||||
fixture := createCommandLifecycleFixture(t)
|
||||
provider := &commandLifecycleProvider{}
|
||||
reviewer := &commandLifecycleReviewer{}
|
||||
validator := &commandLifecycleValidator{}
|
||||
dependencies := taskRuntimeDependencies{
|
||||
Provider: provider,
|
||||
ReviewExecutor: reviewer,
|
||||
Validator: validator.Validate,
|
||||
}
|
||||
adapter := newAdapterWithTaskRuntime(io.Discard, io.Discard, dependencies)
|
||||
paths := command.RuntimeConfigPaths{
|
||||
RepoGlobalPath: fixture.globalPath,
|
||||
UserLocalPath: fixture.localPath,
|
||||
ProviderCatalog: fixture.catalogPath,
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
started, err := adapter.Start(ctx, command.StartRequest{Config: paths, Project: "project"})
|
||||
if err != nil || started.Status != string(agenttask.ProjectStatusStarted) {
|
||||
t.Fatalf("Start: err=%v started=%#v", err, started)
|
||||
}
|
||||
runtime, err := adapter.runtime(paths)
|
||||
if err != nil {
|
||||
t.Fatalf("runtime: %v", err)
|
||||
}
|
||||
if err := runtime.Reconcile(ctx); err != nil {
|
||||
t.Fatalf("Reconcile: %v", err)
|
||||
}
|
||||
assertCommandLifecycle(t, fixture, runtime, provider, reviewer, validator)
|
||||
|
||||
status, err := adapter.Status(ctx, command.StatusRequest{Config: paths, Project: "project"})
|
||||
if err != nil {
|
||||
t.Fatalf("Status error: %v", err)
|
||||
}
|
||||
if status.Project != "project" || status.SelectedMilestone != "m1" {
|
||||
t.Fatalf("status header = %#v", status)
|
||||
}
|
||||
if len(status.Works) != 2 {
|
||||
t.Fatalf("len(status.Works) = %d, want 2", len(status.Works))
|
||||
}
|
||||
w1, w2 := status.Works[0], status.Works[1]
|
||||
if w1.ID != "1" || w1.State != string(agenttask.WorkStateTerminalDeferred) || w1.Integration != "terminal_deferred" || w1.Overlay != "overlay" || w1.DispatchOrdinal == 0 {
|
||||
t.Fatalf("work[0] = %#v", w1)
|
||||
}
|
||||
if w1.Blocker == nil || w1.Blocker.Code != "integration_failed" || !w1.Blocker.Retryable {
|
||||
t.Fatalf("work[0].Blocker = %#v", w1.Blocker)
|
||||
}
|
||||
if w2.ID != "2" || w2.State != string(agenttask.WorkStateCompleted) || w2.Integration != "integrated" || w2.DispatchOrdinal == 0 {
|
||||
t.Fatalf("work[1] = %#v", w2)
|
||||
}
|
||||
if w2.Blocker != nil {
|
||||
t.Fatalf("work[1].Blocker = %#v, want nil", w2.Blocker)
|
||||
}
|
||||
if len(status.Blockers) != 1 || status.Blockers[0].Code != w1.Blocker.Code || status.Blockers[0].Message != w1.Blocker.Message {
|
||||
t.Fatalf("status.Blockers = %#v, want aggregated blocker from work[0]", status.Blockers)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunFullHeadlessS10Transcript proves that separate CLI invocations share
|
||||
// one persisted manager state without launching a provider process.
|
||||
func TestRunFullHeadlessS10Transcript(t *testing.T) {
|
||||
|
|
@ -433,54 +543,64 @@ func TestRunFullHeadlessS10Transcript(t *testing.T) {
|
|||
mutationBase := append(append([]string(nil), readBase...), "--provider-catalog", catalogPath)
|
||||
|
||||
steps := []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
name string
|
||||
args []string
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "milestone list",
|
||||
args: append([]string{"milestone", "list", "iop-s0"}, readBase...),
|
||||
want: "1",
|
||||
name: "milestone list",
|
||||
args: append([]string{"milestone", "list", "iop-s0"}, readBase...),
|
||||
wantStdout: "milestones: 2\n - milestone-1 selected=yes work_units=2 completed=0\n - milestone-2 selected=no work_units=1 completed=0\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "preview",
|
||||
args: append([]string{"preview", "iop-s0"}, readBase...),
|
||||
want: "next: 1",
|
||||
name: "preview",
|
||||
args: append([]string{"preview", "iop-s0"}, readBase...),
|
||||
wantStdout: "preview project=iop-s0\n selected: milestone-1\n next: 1\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "observed status",
|
||||
args: append([]string{"status", "iop-s0"}, readBase...),
|
||||
want: "state: observed",
|
||||
name: "observed status",
|
||||
args: append([]string{"status", "iop-s0"}, readBase...),
|
||||
wantStdout: "status project=iop-s0\n state: observed\n selected_milestone: milestone-1\n started_milestone: \n works: 0\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "manual start",
|
||||
args: append([]string{"start", "iop-s0"}, mutationBase...),
|
||||
want: "status=started",
|
||||
name: "manual start",
|
||||
args: append([]string{"start", "iop-s0"}, mutationBase...),
|
||||
wantStdout: "start project=iop-s0 status=started\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "new invocation sees start",
|
||||
args: append([]string{"status", "iop-s0"}, readBase...),
|
||||
want: "started_milestone: milestone-1",
|
||||
name: "new invocation sees start",
|
||||
args: append([]string{"status", "iop-s0"}, readBase...),
|
||||
wantStdout: "status project=iop-s0\n state: started\n selected_milestone: milestone-1\n started_milestone: milestone-1\n works: 0\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "manual stop",
|
||||
args: append([]string{"stop", "iop-s0"}, mutationBase...),
|
||||
want: "status=stopped",
|
||||
name: "manual stop",
|
||||
args: append([]string{"stop", "iop-s0"}, mutationBase...),
|
||||
wantStdout: "stop project=iop-s0 status=stopped\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "manual resume",
|
||||
args: append([]string{"resume", "iop-s0"}, mutationBase...),
|
||||
want: "status=started",
|
||||
name: "manual resume",
|
||||
args: append([]string{"resume", "iop-s0"}, mutationBase...),
|
||||
wantStdout: "resume project=iop-s0 status=started\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "select another milestone",
|
||||
args: append([]string{"milestone", "select", "iop-s0", "milestone-2"}, readBase...),
|
||||
want: "milestone=milestone-2",
|
||||
name: "select another milestone",
|
||||
args: append([]string{"milestone", "select", "iop-s0", "milestone-2"}, readBase...),
|
||||
wantStdout: "milestone select project=iop-s0 milestone=milestone-2\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "selection persists",
|
||||
args: append([]string{"status", "iop-s0"}, readBase...),
|
||||
want: "selected_milestone: milestone-2",
|
||||
name: "selection persists",
|
||||
args: append([]string{"status", "iop-s0"}, readBase...),
|
||||
wantStdout: "status project=iop-s0\n state: started\n selected_milestone: milestone-2\n started_milestone: milestone-1\n works: 0\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -496,11 +616,117 @@ func TestRunFullHeadlessS10Transcript(t *testing.T) {
|
|||
stderr.String(),
|
||||
)
|
||||
}
|
||||
if stderr.Len() != 0 {
|
||||
t.Fatalf("stderr = %q, want empty", stderr.String())
|
||||
if stdout.String() != step.wantStdout {
|
||||
t.Fatalf("stdout = %q, want %q", stdout.String(), step.wantStdout)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), step.want) {
|
||||
t.Fatalf("stdout = %q, want substring %q", stdout.String(), step.want)
|
||||
if stderr.String() != step.wantStderr {
|
||||
t.Fatalf("stderr = %q, want %q", stderr.String(), step.wantStderr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltBinaryHeadlessS10Transcript(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
binPath := filepath.Join(tmpDir, "iop-agent-test")
|
||||
|
||||
buildCmd := exec.Command("go", "build", "-o", binPath, ".")
|
||||
buildCmd.Env = os.Environ()
|
||||
if out, err := buildCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("go build failed: %v\noutput: %s", err, string(out))
|
||||
}
|
||||
|
||||
globalPath, localPath, catalogPath := createRuntimeFixture(t)
|
||||
readBase := []string{"--repo-config", globalPath, "--local-config", localPath}
|
||||
mutationBase := append(append([]string(nil), readBase...), "--provider-catalog", catalogPath)
|
||||
|
||||
steps := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "validate",
|
||||
args: append([]string{"validate"}, mutationBase...),
|
||||
wantStdout: "", // substring validated inside test body
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "milestone list",
|
||||
args: append([]string{"milestone", "list", "iop-s0"}, readBase...),
|
||||
wantStdout: "milestones: 2\n - milestone-1 selected=yes work_units=2 completed=0\n - milestone-2 selected=no work_units=1 completed=0\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "preview",
|
||||
args: append([]string{"preview", "iop-s0"}, readBase...),
|
||||
wantStdout: "preview project=iop-s0\n selected: milestone-1\n next: 1\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "status observed",
|
||||
args: append([]string{"status", "iop-s0"}, readBase...),
|
||||
wantStdout: "status project=iop-s0\n state: observed\n selected_milestone: milestone-1\n started_milestone: \n works: 0\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "manual start",
|
||||
args: append([]string{"start", "iop-s0"}, mutationBase...),
|
||||
wantStdout: "start project=iop-s0 status=started\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "status started",
|
||||
args: append([]string{"status", "iop-s0"}, readBase...),
|
||||
wantStdout: "status project=iop-s0\n state: started\n selected_milestone: milestone-1\n started_milestone: milestone-1\n works: 0\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "manual stop",
|
||||
args: append([]string{"stop", "iop-s0"}, mutationBase...),
|
||||
wantStdout: "stop project=iop-s0 status=stopped\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "manual resume",
|
||||
args: append([]string{"resume", "iop-s0"}, mutationBase...),
|
||||
wantStdout: "resume project=iop-s0 status=started\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "select another milestone",
|
||||
args: append([]string{"milestone", "select", "iop-s0", "milestone-2"}, readBase...),
|
||||
wantStdout: "milestone select project=iop-s0 milestone=milestone-2\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "status selected persisted",
|
||||
args: append([]string{"status", "iop-s0"}, readBase...),
|
||||
wantStdout: "status project=iop-s0\n state: started\n selected_milestone: milestone-2\n started_milestone: milestone-1\n works: 0\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, step := range steps {
|
||||
t.Run(step.name, func(t *testing.T) {
|
||||
cmd := exec.Command(binPath, step.args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
t.Fatalf("command %v failed: %v (stdout=%q stderr=%q)", step.args, err, stdout.String(), stderr.String())
|
||||
}
|
||||
if step.name == "validate" {
|
||||
if !strings.Contains(stdout.String(), "validate: ok") {
|
||||
t.Fatalf("validate output = %q, want validate: ok", stdout.String())
|
||||
}
|
||||
} else if stdout.String() != step.wantStdout {
|
||||
t.Fatalf("stdout = %q, want %q", stdout.String(), step.wantStdout)
|
||||
}
|
||||
if stderr.String() != step.wantStderr {
|
||||
t.Fatalf("stderr = %q, want %q", stderr.String(), step.wantStderr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -772,7 +998,8 @@ func createRuntimeFixture(t *testing.T) (string, string, string) {
|
|||
for _, directory := range []string{
|
||||
stateRoot,
|
||||
filepath.Join(workspace, "agent-task", "m-milestone-1", "1_first"),
|
||||
filepath.Join(workspace, "agent-task", "m-milestone-2", "2_second"),
|
||||
filepath.Join(workspace, "agent-task", "m-milestone-1", "2_second"),
|
||||
filepath.Join(workspace, "agent-task", "m-milestone-2", "3_third"),
|
||||
} {
|
||||
if err := os.MkdirAll(directory, 0700); err != nil {
|
||||
t.Fatalf("create fixture directory %s: %v", directory, err)
|
||||
|
|
@ -794,7 +1021,8 @@ Pending implementation.
|
|||
`
|
||||
for _, taskRoot := range []string{
|
||||
filepath.Join(workspace, "agent-task", "m-milestone-1", "1_first"),
|
||||
filepath.Join(workspace, "agent-task", "m-milestone-2", "2_second"),
|
||||
filepath.Join(workspace, "agent-task", "m-milestone-1", "2_second"),
|
||||
filepath.Join(workspace, "agent-task", "m-milestone-2", "3_third"),
|
||||
} {
|
||||
writeFile(t, filepath.Join(taskRoot, "PLAN-test.md"), plan)
|
||||
writeFile(t, filepath.Join(taskRoot, "CODE_REVIEW-test.md"), review)
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ func newMilestoneListCmd(svc Service, repoConfig, localConfig, providerCatalog,
|
|||
return &cobra.Command{
|
||||
Use: "list [project]",
|
||||
Short: "List milestones for a project",
|
||||
Long: "List the work units declared by the active PLAN for the supplied project.",
|
||||
Long: "List selectable milestones for the supplied project.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
req := MilestoneListRequest{
|
||||
|
|
@ -330,7 +330,7 @@ func newStatusCmd(svc Service, repoConfig, localConfig, providerCatalog, outputF
|
|||
return &cobra.Command{
|
||||
Use: "status [project]",
|
||||
Short: "Show project status",
|
||||
Long: "Show the live project status including overlay state, integration queue position, and any active blockers.",
|
||||
Long: "Show the live project status including ordered per-work state, dispatch ordinals, overlay/integration state, and blockers.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
req := StatusRequest{
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ func newTestService() *recordingService {
|
|||
validateResp: ValidateResponse{Valid: true, Revision: "sha256:abc", Projects: 1, Providers: 2, Profiles: 3},
|
||||
providerResp: ProviderListResponse{Providers: []ProviderEntry{{ID: "pi", Command: "pi", Capabilities: []string{"chat"}}}},
|
||||
projectResp: ProjectListResponse{Projects: []ProjectEntry{{ID: "proj1", Workspace: "/ws", Enabled: true, SelectedMilestone: "m1", StartedMilestone: "", AutoResumeInterrupt: true}}},
|
||||
milestoneResp: MilestoneListResponse{Milestones: []MilestoneEntry{{ID: "m1", Aliases: []string{"main"}, Completed: false, WriteSet: "file.go", Isolation: "overlay"}}},
|
||||
milestoneResp: MilestoneListResponse{Milestones: []MilestoneEntry{{ID: "m1", Selected: true, WorkUnits: 2, CompletedWorkUnits: 1}}},
|
||||
selectResp: MilestoneSelectResponse{Project: "proj1", MilestoneID: "m1"},
|
||||
previewResp: PreviewResponse{Project: "proj1", Selected: true, MilestoneID: "m1", NextWork: "m2"},
|
||||
startResp: StartResponse{Project: "proj1", Status: "started"},
|
||||
|
|
@ -204,11 +204,12 @@ func newTestService() *recordingService {
|
|||
statusResp: StatusResponse{
|
||||
Project: "proj1",
|
||||
Status: "running",
|
||||
Overlay: "active",
|
||||
Integration: "queued",
|
||||
IntegrationPos: 0,
|
||||
SelectedMilestone: "m1",
|
||||
StartedMilestone: "",
|
||||
Works: []WorkStatusEntry{
|
||||
{ID: "w1", State: "completed", Overlay: "active", Integration: "integrated", DispatchOrdinal: 1},
|
||||
{ID: "w2", State: "running", Overlay: "active", Integration: "queued", DispatchOrdinal: 2},
|
||||
},
|
||||
},
|
||||
taskLoopResp: TaskLoopResponse{DryRun: true, Projects: []TaskLoopProject{{Project: "proj1", Group: "m-m1", Status: "ready", NextWork: "w1"}}},
|
||||
}
|
||||
|
|
@ -274,7 +275,7 @@ func TestCommandMatrix(t *testing.T) {
|
|||
t.Errorf("got milestone list req %+v, want %+v", svc.lastMilestoneReq, want)
|
||||
}
|
||||
},
|
||||
wantStdout: "milestones: 1\n - m1 aliases=main state=pending write_set=\"file.go\" isolation=overlay\n",
|
||||
wantStdout: "milestones: 1\n - m1 selected=yes work_units=2 completed=1\n",
|
||||
},
|
||||
{
|
||||
name: "milestone select",
|
||||
|
|
@ -358,7 +359,7 @@ func TestCommandMatrix(t *testing.T) {
|
|||
t.Errorf("got status req %+v, want %+v", svc.lastStatusReq, want)
|
||||
}
|
||||
},
|
||||
wantStdout: "status project=proj1\n state: running\n overlay: active\n integration: queued (pos 0)\n selected_milestone: m1\n started_milestone: \n",
|
||||
wantStdout: "status project=proj1\n state: running\n selected_milestone: m1\n started_milestone: \n works: 2\n - w1 state=completed overlay=active integration=integrated ordinal=1\n - w2 state=running overlay=active integration=queued ordinal=2\n",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -446,7 +447,7 @@ func TestCommandJSONOutputMatrix(t *testing.T) {
|
|||
t.Errorf("got milestone list req %+v, want %+v", svc.lastMilestoneReq, want)
|
||||
}
|
||||
},
|
||||
wantJSON: "{\n \"milestones\": [\n {\n \"id\": \"m1\",\n \"aliases\": [\n \"main\"\n ],\n \"completed\": false,\n \"write_set\": \"file.go\",\n \"isolation\": \"overlay\"\n }\n ]\n}\n",
|
||||
wantJSON: "{\n \"milestones\": [\n {\n \"id\": \"m1\",\n \"selected\": true,\n \"work_units\": 2,\n \"completed_work_units\": 1\n }\n ]\n}\n",
|
||||
},
|
||||
{
|
||||
name: "milestone select json",
|
||||
|
|
@ -518,7 +519,7 @@ func TestCommandJSONOutputMatrix(t *testing.T) {
|
|||
t.Errorf("got status req %+v, want %+v", svc.lastStatusReq, want)
|
||||
}
|
||||
},
|
||||
wantJSON: "{\n \"project\": \"proj1\",\n \"status\": \"running\",\n \"overlay\": \"active\",\n \"integration\": \"queued\",\n \"integration_position\": 0,\n \"selected_milestone\": \"m1\",\n \"started_milestone\": \"\",\n \"blockers\": []\n}\n",
|
||||
wantJSON: "{\n \"project\": \"proj1\",\n \"status\": \"running\",\n \"selected_milestone\": \"m1\",\n \"started_milestone\": \"\",\n \"works\": [\n {\n \"id\": \"w1\",\n \"state\": \"completed\",\n \"overlay\": \"active\",\n \"integration\": \"integrated\",\n \"dispatch_ordinal\": 1\n },\n {\n \"id\": \"w2\",\n \"state\": \"running\",\n \"overlay\": \"active\",\n \"integration\": \"queued\",\n \"dispatch_ordinal\": 2\n }\n ],\n \"blockers\": []\n}\n",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -601,12 +602,12 @@ func TestStatusIncludesOverlayIntegrationAndBlockers(t *testing.T) {
|
|||
statusResp: StatusResponse{
|
||||
Project: "proj1",
|
||||
Status: "running",
|
||||
Overlay: "active-overlay",
|
||||
Integration: "integrating",
|
||||
IntegrationPos: 2,
|
||||
Blockers: []BlockerEntry{{Code: "provider_capacity", Message: "no capacity", Retryable: true}},
|
||||
SelectedMilestone: "m1",
|
||||
StartedMilestone: "m1",
|
||||
Works: []WorkStatusEntry{
|
||||
{ID: "w1", State: "running", Overlay: "active-overlay", Integration: "integrating", DispatchOrdinal: 2, Blocker: &BlockerEntry{Code: "provider_capacity", Message: "no capacity", Retryable: true}},
|
||||
},
|
||||
Blockers: []BlockerEntry{{Code: "provider_capacity", Message: "no capacity", Retryable: true}},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -617,7 +618,7 @@ func TestStatusIncludesOverlayIntegrationAndBlockers(t *testing.T) {
|
|||
t.Fatalf("status should not error: %v", err)
|
||||
}
|
||||
|
||||
wantText := "status project=proj1\n state: running\n overlay: active-overlay\n integration: integrating (pos 2)\n selected_milestone: m1\n started_milestone: m1\n blocker: provider_capacity no capacity retryable=yes\n"
|
||||
wantText := "status project=proj1\n state: running\n selected_milestone: m1\n started_milestone: m1\n works: 1\n - w1 state=running overlay=active-overlay integration=integrating ordinal=2\n blocker: provider_capacity no capacity retryable=yes\n blocker: provider_capacity no capacity retryable=yes\n"
|
||||
if stdout != wantText {
|
||||
t.Errorf("unexpected status text:\ngot: %q\nwant: %q", stdout, wantText)
|
||||
}
|
||||
|
|
@ -627,7 +628,7 @@ func TestStatusIncludesOverlayIntegrationAndBlockers(t *testing.T) {
|
|||
t.Fatalf("status json error: %v", jsonErr)
|
||||
}
|
||||
|
||||
wantJSON := "{\n \"project\": \"proj1\",\n \"status\": \"running\",\n \"overlay\": \"active-overlay\",\n \"integration\": \"integrating\",\n \"integration_position\": 2,\n \"selected_milestone\": \"m1\",\n \"started_milestone\": \"m1\",\n \"blockers\": [\n {\n \"code\": \"provider_capacity\",\n \"message\": \"no capacity\",\n \"retryable\": true\n }\n ]\n}\n"
|
||||
wantJSON := "{\n \"project\": \"proj1\",\n \"status\": \"running\",\n \"selected_milestone\": \"m1\",\n \"started_milestone\": \"m1\",\n \"works\": [\n {\n \"id\": \"w1\",\n \"state\": \"running\",\n \"overlay\": \"active-overlay\",\n \"integration\": \"integrating\",\n \"dispatch_ordinal\": 2,\n \"blocker\": {\n \"code\": \"provider_capacity\",\n \"message\": \"no capacity\",\n \"retryable\": true\n }\n }\n ],\n \"blockers\": [\n {\n \"code\": \"provider_capacity\",\n \"message\": \"no capacity\",\n \"retryable\": true\n }\n ]\n}\n"
|
||||
assertExactJSON(t, jsonOut, wantJSON)
|
||||
}
|
||||
|
||||
|
|
@ -657,12 +658,12 @@ func TestStableTextAndJSONOutput(t *testing.T) {
|
|||
wantProjJSON := "{\n \"projects\": [\n {\n \"id\": \"pj1\",\n \"workspace\": \"/ws\",\n \"enabled\": true,\n \"selected_milestone\": \"m1\",\n \"started_milestone\": \"m1\",\n \"auto_resume_interrupted\": true\n }\n ]\n}"
|
||||
assertExactJSON(t, projResp.FormatJSON(), wantProjJSON)
|
||||
|
||||
msResp := MilestoneListResponse{Milestones: []MilestoneEntry{{ID: "m1", Aliases: []string{"a1"}, Completed: true, WriteSet: "ws1", Isolation: "iso1"}}}
|
||||
wantMsText := "milestones: 1\n - m1 aliases=a1 state=completed write_set=\"ws1\" isolation=iso1\n"
|
||||
msResp := MilestoneListResponse{Milestones: []MilestoneEntry{{ID: "m1", Selected: true, WorkUnits: 1, CompletedWorkUnits: 1}}}
|
||||
wantMsText := "milestones: 1\n - m1 selected=yes work_units=1 completed=1\n"
|
||||
if text := msResp.FormatText(); text != wantMsText {
|
||||
t.Errorf("unexpected milestone list text:\ngot: %q\nwant: %q", text, wantMsText)
|
||||
}
|
||||
wantMsJSON := "{\n \"milestones\": [\n {\n \"id\": \"m1\",\n \"aliases\": [\n \"a1\"\n ],\n \"completed\": true,\n \"write_set\": \"ws1\",\n \"isolation\": \"iso1\"\n }\n ]\n}"
|
||||
wantMsJSON := "{\n \"milestones\": [\n {\n \"id\": \"m1\",\n \"selected\": true,\n \"work_units\": 1,\n \"completed_work_units\": 1\n }\n ]\n}"
|
||||
assertExactJSON(t, msResp.FormatJSON(), wantMsJSON)
|
||||
|
||||
selResp := MilestoneSelectResponse{Project: "pj1", MilestoneID: "m1"}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ type Service interface {
|
|||
// from the loaded runtime snapshot. It is side-effect free.
|
||||
ProjectList(ctx context.Context, req ProjectListRequest) (ProjectListResponse, error)
|
||||
|
||||
// MilestoneList returns the work units declared by the active PLAN for
|
||||
// the supplied project. It is side-effect free.
|
||||
// MilestoneList returns the workflow-backed Milestones discovered for the
|
||||
// supplied project. It is side-effect free.
|
||||
MilestoneList(ctx context.Context, req MilestoneListRequest) (MilestoneListResponse, error)
|
||||
|
||||
// MilestoneSelect records the user's explicit Milestone choice for the
|
||||
|
|
@ -61,8 +61,8 @@ type Service interface {
|
|||
// Resume records a manual resume intent for the supplied project.
|
||||
Resume(ctx context.Context, req ResumeRequest) (ResumeResponse, error)
|
||||
|
||||
// Status returns the live project status including overlay state,
|
||||
// integration queue position, and any active blockers.
|
||||
// Status returns the live project status including ordered per-work state,
|
||||
// durable dispatch ordinals, overlay/integration state, and blockers.
|
||||
Status(ctx context.Context, req StatusRequest) (StatusResponse, error)
|
||||
|
||||
// TaskLoop performs one bounded operator pass over the authoritative
|
||||
|
|
@ -142,26 +142,25 @@ type ProjectEntry struct {
|
|||
AutoResumeInterrupt bool
|
||||
}
|
||||
|
||||
// MilestoneListRequest carries the project identity used to list its work
|
||||
// units.
|
||||
// MilestoneListRequest carries the project identity used to list its
|
||||
// workflow-backed Milestones.
|
||||
type MilestoneListRequest struct {
|
||||
Config RuntimeConfigPaths
|
||||
Project string
|
||||
}
|
||||
|
||||
// MilestoneListResponse reports every work unit declared by the active PLAN
|
||||
// for the supplied project.
|
||||
// MilestoneListResponse reports every Milestone declared in the workspace for
|
||||
// the supplied project.
|
||||
type MilestoneListResponse struct {
|
||||
Milestones []MilestoneEntry
|
||||
}
|
||||
|
||||
// MilestoneEntry is one work unit identity with its completion state.
|
||||
// MilestoneEntry is one Milestone summary.
|
||||
type MilestoneEntry struct {
|
||||
ID string
|
||||
Aliases []string
|
||||
Completed bool
|
||||
WriteSet string
|
||||
Isolation string
|
||||
ID string
|
||||
Selected bool
|
||||
WorkUnits int
|
||||
CompletedWorkUnits int
|
||||
}
|
||||
|
||||
// MilestoneSelectRequest records the operator's explicit Milestone choice.
|
||||
|
|
@ -254,17 +253,25 @@ type StatusRequest struct {
|
|||
Project string
|
||||
}
|
||||
|
||||
// StatusResponse reports the live project status including overlay,
|
||||
// integration, and blockers.
|
||||
// StatusResponse reports the live project status including ordered per-work status
|
||||
// entries and blockers.
|
||||
type StatusResponse struct {
|
||||
Project string
|
||||
Status string
|
||||
Overlay string
|
||||
Integration string
|
||||
IntegrationPos int
|
||||
Blockers []BlockerEntry
|
||||
SelectedMilestone string
|
||||
StartedMilestone string
|
||||
Works []WorkStatusEntry
|
||||
Blockers []BlockerEntry
|
||||
}
|
||||
|
||||
// WorkStatusEntry is the status projection for one work unit.
|
||||
type WorkStatusEntry struct {
|
||||
ID string
|
||||
State string
|
||||
Overlay string
|
||||
Integration string
|
||||
DispatchOrdinal uint64
|
||||
Blocker *BlockerEntry
|
||||
}
|
||||
|
||||
// TaskLoopRequest is the bounded operator request used by the production
|
||||
|
|
@ -354,12 +361,12 @@ func (r MilestoneListResponse) FormatText() string {
|
|||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("milestones: %d\n", len(r.Milestones)))
|
||||
for _, m := range r.Milestones {
|
||||
done := "pending"
|
||||
if m.Completed {
|
||||
done = "completed"
|
||||
selected := "no"
|
||||
if m.Selected {
|
||||
selected = "yes"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" - %s aliases=%s state=%s write_set=%q isolation=%s\n",
|
||||
m.ID, joinStrings(m.Aliases), done, m.WriteSet, m.Isolation))
|
||||
b.WriteString(fmt.Sprintf(" - %s selected=%s work_units=%d completed=%d\n",
|
||||
m.ID, selected, m.WorkUnits, m.CompletedWorkUnits))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
|
@ -409,10 +416,21 @@ func (r StatusResponse) FormatText() string {
|
|||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("status project=%s\n", r.Project))
|
||||
b.WriteString(fmt.Sprintf(" state: %s\n", r.Status))
|
||||
b.WriteString(fmt.Sprintf(" overlay: %s\n", r.Overlay))
|
||||
b.WriteString(fmt.Sprintf(" integration: %s (pos %d)\n", r.Integration, r.IntegrationPos))
|
||||
b.WriteString(fmt.Sprintf(" selected_milestone: %s\n", r.SelectedMilestone))
|
||||
b.WriteString(fmt.Sprintf(" started_milestone: %s\n", r.StartedMilestone))
|
||||
b.WriteString(fmt.Sprintf(" works: %d\n", len(r.Works)))
|
||||
for _, w := range r.Works {
|
||||
b.WriteString(fmt.Sprintf(" - %s state=%s overlay=%s integration=%s ordinal=%d\n",
|
||||
w.ID, w.State, w.Overlay, w.Integration, w.DispatchOrdinal))
|
||||
if w.Blocker != nil {
|
||||
retry := "no"
|
||||
if w.Blocker.Retryable {
|
||||
retry = "yes"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" blocker: %s %s retryable=%s\n",
|
||||
w.Blocker.Code, w.Blocker.Message, retry))
|
||||
}
|
||||
}
|
||||
for _, bl := range r.Blockers {
|
||||
retry := "no"
|
||||
if bl.Retryable {
|
||||
|
|
@ -518,27 +536,21 @@ func (r ProjectListResponse) FormatJSON() string {
|
|||
// FormatJSON renders a MilestoneListResponse as stable JSON.
|
||||
func (r MilestoneListResponse) FormatJSON() string {
|
||||
type milestoneDTO struct {
|
||||
ID string `json:"id"`
|
||||
Aliases []string `json:"aliases"`
|
||||
Completed bool `json:"completed"`
|
||||
WriteSet string `json:"write_set"`
|
||||
Isolation string `json:"isolation"`
|
||||
ID string `json:"id"`
|
||||
Selected bool `json:"selected"`
|
||||
WorkUnits int `json:"work_units"`
|
||||
CompletedWorkUnits int `json:"completed_work_units"`
|
||||
}
|
||||
type dto struct {
|
||||
Milestones []milestoneDTO `json:"milestones"`
|
||||
}
|
||||
list := make([]milestoneDTO, 0, len(r.Milestones))
|
||||
for _, m := range r.Milestones {
|
||||
aliases := m.Aliases
|
||||
if aliases == nil {
|
||||
aliases = []string{}
|
||||
}
|
||||
list = append(list, milestoneDTO{
|
||||
ID: m.ID,
|
||||
Aliases: aliases,
|
||||
Completed: m.Completed,
|
||||
WriteSet: m.WriteSet,
|
||||
Isolation: m.Isolation,
|
||||
ID: m.ID,
|
||||
Selected: m.Selected,
|
||||
WorkUnits: m.WorkUnits,
|
||||
CompletedWorkUnits: m.CompletedWorkUnits,
|
||||
})
|
||||
}
|
||||
data, _ := json.MarshalIndent(dto{Milestones: list}, "", " ")
|
||||
|
|
@ -636,15 +648,40 @@ func (r StatusResponse) FormatJSON() string {
|
|||
Message string `json:"message"`
|
||||
Retryable bool `json:"retryable"`
|
||||
}
|
||||
type workStatusDTO struct {
|
||||
ID string `json:"id"`
|
||||
State string `json:"state"`
|
||||
Overlay string `json:"overlay"`
|
||||
Integration string `json:"integration"`
|
||||
DispatchOrdinal uint64 `json:"dispatch_ordinal"`
|
||||
Blocker *blockerDTO `json:"blocker,omitempty"`
|
||||
}
|
||||
type dto struct {
|
||||
Project string `json:"project"`
|
||||
Status string `json:"status"`
|
||||
Overlay string `json:"overlay"`
|
||||
Integration string `json:"integration"`
|
||||
IntegrationPos int `json:"integration_position"`
|
||||
SelectedMilestone string `json:"selected_milestone"`
|
||||
StartedMilestone string `json:"started_milestone"`
|
||||
Blockers []blockerDTO `json:"blockers"`
|
||||
Project string `json:"project"`
|
||||
Status string `json:"status"`
|
||||
SelectedMilestone string `json:"selected_milestone"`
|
||||
StartedMilestone string `json:"started_milestone"`
|
||||
Works []workStatusDTO `json:"works"`
|
||||
Blockers []blockerDTO `json:"blockers"`
|
||||
}
|
||||
works := make([]workStatusDTO, 0, len(r.Works))
|
||||
for _, w := range r.Works {
|
||||
var b *blockerDTO
|
||||
if w.Blocker != nil {
|
||||
b = &blockerDTO{
|
||||
Code: w.Blocker.Code,
|
||||
Message: w.Blocker.Message,
|
||||
Retryable: w.Blocker.Retryable,
|
||||
}
|
||||
}
|
||||
works = append(works, workStatusDTO{
|
||||
ID: w.ID,
|
||||
State: w.State,
|
||||
Overlay: w.Overlay,
|
||||
Integration: w.Integration,
|
||||
DispatchOrdinal: w.DispatchOrdinal,
|
||||
Blocker: b,
|
||||
})
|
||||
}
|
||||
blockers := make([]blockerDTO, 0, len(r.Blockers))
|
||||
for _, bl := range r.Blockers {
|
||||
|
|
@ -657,11 +694,9 @@ func (r StatusResponse) FormatJSON() string {
|
|||
data, _ := json.MarshalIndent(dto{
|
||||
Project: r.Project,
|
||||
Status: r.Status,
|
||||
Overlay: r.Overlay,
|
||||
Integration: r.Integration,
|
||||
IntegrationPos: r.IntegrationPos,
|
||||
SelectedMilestone: r.SelectedMilestone,
|
||||
StartedMilestone: r.StartedMilestone,
|
||||
Works: works,
|
||||
Blockers: blockers,
|
||||
}, "", " ")
|
||||
return string(data)
|
||||
|
|
|
|||
|
|
@ -99,11 +99,12 @@ type ProjectView struct {
|
|||
}
|
||||
|
||||
type WorkView struct {
|
||||
WorkUnitID string
|
||||
State agenttask.WorkState
|
||||
Overlay string
|
||||
Integration string
|
||||
Blocker *agenttask.Blocker
|
||||
WorkUnitID string
|
||||
State agenttask.WorkState
|
||||
Overlay string
|
||||
Integration string
|
||||
DispatchOrdinal uint64
|
||||
Blocker *agenttask.Blocker
|
||||
}
|
||||
|
||||
type Preview struct {
|
||||
|
|
@ -444,6 +445,21 @@ func (runtime *Runtime) SelectMilestone(
|
|||
return runtime.selections.Save(ctx, projectID, milestone)
|
||||
}
|
||||
|
||||
func (runtime *Runtime) Milestones(
|
||||
ctx context.Context,
|
||||
projectID string,
|
||||
) ([]MilestoneView, error) {
|
||||
registration, selected, err := runtime.registeredSelection(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root, err := canonicalDirectory(registration.Workspace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return scanMilestones(root, selected)
|
||||
}
|
||||
|
||||
func (runtime *Runtime) WorkflowSnapshot(
|
||||
ctx context.Context,
|
||||
projectID string,
|
||||
|
|
@ -536,7 +552,11 @@ func (runtime *Runtime) ProjectStatus(ctx context.Context, projectID string) (Pr
|
|||
sort.Slice(workIDs, func(left, right int) bool { return workIDs[left] < workIDs[right] })
|
||||
for _, workID := range workIDs {
|
||||
work := project.Works[workID]
|
||||
workView := WorkView{WorkUnitID: string(workID), State: work.State}
|
||||
workView := WorkView{
|
||||
WorkUnitID: string(workID),
|
||||
State: work.State,
|
||||
DispatchOrdinal: uint64(work.DispatchOrdinal),
|
||||
}
|
||||
if work.Isolation != nil {
|
||||
workView.Overlay = string(work.Isolation.Mode)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -25,6 +26,13 @@ const (
|
|||
completeFileName = "complete.log"
|
||||
)
|
||||
|
||||
type MilestoneView struct {
|
||||
ID string
|
||||
Selected bool
|
||||
WorkUnits int
|
||||
CompletedWorkUnits int
|
||||
}
|
||||
|
||||
// SelectionStore keeps the device-local explicit Milestone selection separate
|
||||
// from the repository-owned runtime defaults.
|
||||
type SelectionStore interface {
|
||||
|
|
@ -123,8 +131,62 @@ func WorkspaceIdentity(root string) agenttask.WorkspaceID {
|
|||
return agenttask.WorkspaceID("workspace-" + hex.EncodeToString(sum[:]))
|
||||
}
|
||||
|
||||
func parseMilestoneSlug(slug string) (string, error) {
|
||||
if slug == "" {
|
||||
return "", errors.New("taskloop: milestone slug cannot be empty")
|
||||
}
|
||||
for i := 0; i < len(slug); i++ {
|
||||
c := slug[i]
|
||||
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {
|
||||
continue
|
||||
}
|
||||
if c == '-' && i > 0 {
|
||||
continue
|
||||
}
|
||||
return "", fmt.Errorf("taskloop: milestone slug %q contains invalid character %q", slug, string(c))
|
||||
}
|
||||
return slug, nil
|
||||
}
|
||||
|
||||
func parseActiveGroupDir(dirName string) (string, error) {
|
||||
if !strings.HasPrefix(dirName, taskGroupPrefix) {
|
||||
return "", fmt.Errorf("taskloop: task group %q must have %q prefix", dirName, taskGroupPrefix)
|
||||
}
|
||||
rawSlug := strings.TrimPrefix(dirName, taskGroupPrefix)
|
||||
return parseMilestoneSlug(rawSlug)
|
||||
}
|
||||
|
||||
func parseArchiveGroupDir(dirName string) (string, error) {
|
||||
if !strings.HasPrefix(dirName, taskGroupPrefix) {
|
||||
return "", fmt.Errorf("taskloop: task group %q must have %q prefix", dirName, taskGroupPrefix)
|
||||
}
|
||||
rest := strings.TrimPrefix(dirName, taskGroupPrefix)
|
||||
if idx := strings.IndexByte(rest, '_'); idx >= 0 {
|
||||
slugPart := rest[:idx]
|
||||
suffixPart := rest[idx+1:]
|
||||
if strings.IndexByte(suffixPart, '_') >= 0 || suffixPart == "" {
|
||||
return "", fmt.Errorf("taskloop: archive group directory %q has invalid collision suffix", dirName)
|
||||
}
|
||||
val, err := strconv.Atoi(suffixPart)
|
||||
if err != nil || val <= 0 || strconv.Itoa(val) != suffixPart {
|
||||
return "", fmt.Errorf("taskloop: archive group directory %q has invalid collision suffix %q", dirName, suffixPart)
|
||||
}
|
||||
return parseMilestoneSlug(slugPart)
|
||||
}
|
||||
return parseMilestoneSlug(rest)
|
||||
}
|
||||
|
||||
func scanWorkflow(root, milestone string) ([]agenttask.WorkUnit, string, error) {
|
||||
activeRoot := filepath.Join(root, "agent-task", taskGroupPrefix+milestone)
|
||||
slug := milestone
|
||||
if strings.HasPrefix(milestone, taskGroupPrefix) {
|
||||
slug = strings.TrimPrefix(milestone, taskGroupPrefix)
|
||||
}
|
||||
canonicalSlug, err := parseMilestoneSlug(slug)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("taskloop: selected milestone %q: %w", milestone, err)
|
||||
}
|
||||
|
||||
activeRoot := filepath.Join(root, "agent-task", taskGroupPrefix+canonicalSlug)
|
||||
entries, err := readOptionalTaskGroup(activeRoot)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("taskloop: read active task group: %w", err)
|
||||
|
|
@ -133,12 +195,12 @@ func scanWorkflow(root, milestone string) ([]agenttask.WorkUnit, string, error)
|
|||
units := make([]agenttask.WorkUnit, 0, len(entries)+1)
|
||||
evidence := make([]string, 0, len(entries)+1)
|
||||
seen := make(map[agenttask.WorkUnitID]string)
|
||||
single, singleEvidence, singleActive, err := readSinglePlanUnit(root, activeRoot, milestone)
|
||||
single, singleEvidence, singleActive, err := readSinglePlanUnit(root, activeRoot, canonicalSlug)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if singleActive {
|
||||
seen[single.ID] = taskGroupPrefix + milestone
|
||||
seen[single.ID] = taskGroupPrefix + canonicalSlug
|
||||
units = append(units, single)
|
||||
evidence = append(evidence, singleEvidence)
|
||||
}
|
||||
|
|
@ -146,7 +208,7 @@ func scanWorkflow(root, milestone string) ([]agenttask.WorkUnit, string, error)
|
|||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
unit, unitEvidence, include, err := readActiveUnit(root, activeRoot, milestone, entry.Name())
|
||||
unit, unitEvidence, include, err := readActiveUnit(root, activeRoot, canonicalSlug, entry.Name())
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
|
@ -156,7 +218,7 @@ func scanWorkflow(root, milestone string) ([]agenttask.WorkUnit, string, error)
|
|||
if singleActive {
|
||||
return nil, "", fmt.Errorf(
|
||||
"taskloop: task group %q mixes a single-plan root pair with split-plan task directory %q",
|
||||
taskGroupPrefix+milestone,
|
||||
taskGroupPrefix+canonicalSlug,
|
||||
entry.Name(),
|
||||
)
|
||||
}
|
||||
|
|
@ -172,7 +234,7 @@ func scanWorkflow(root, milestone string) ([]agenttask.WorkUnit, string, error)
|
|||
units = append(units, unit)
|
||||
evidence = append(evidence, unitEvidence)
|
||||
}
|
||||
archived, archivedEvidence, err := readArchivedUnits(root, milestone, seen)
|
||||
archived, archivedEvidence, err := readArchivedUnits(root, canonicalSlug, seen)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
|
@ -181,7 +243,7 @@ func scanWorkflow(root, milestone string) ([]agenttask.WorkUnit, string, error)
|
|||
if len(units) == 0 {
|
||||
return nil, "", fmt.Errorf(
|
||||
"taskloop: selected milestone %q has no active or archived task artifacts",
|
||||
milestone,
|
||||
canonicalSlug,
|
||||
)
|
||||
}
|
||||
sort.Slice(units, func(left, right int) bool { return units[left].ID < units[right].ID })
|
||||
|
|
@ -189,6 +251,111 @@ func scanWorkflow(root, milestone string) ([]agenttask.WorkUnit, string, error)
|
|||
return units, strings.Join(evidence, "\n"), nil
|
||||
}
|
||||
|
||||
func discoverMilestoneCandidates(root string) ([]string, error) {
|
||||
candidates := make(map[string]struct{})
|
||||
|
||||
activeRoot := filepath.Join(root, "agent-task")
|
||||
activeEntries, err := readOptionalTaskGroup(activeRoot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("taskloop: read active task groups: %w", err)
|
||||
}
|
||||
for _, entry := range activeEntries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
if name == "archive" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(name, taskGroupPrefix) {
|
||||
slug, err := parseActiveGroupDir(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
candidates[slug] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
archiveRoot := filepath.Join(root, "agent-task", "archive")
|
||||
years, err := os.ReadDir(archiveRoot)
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, fmt.Errorf("taskloop: read archive root: %w", err)
|
||||
}
|
||||
if err == nil {
|
||||
for _, year := range years {
|
||||
if !year.IsDir() {
|
||||
continue
|
||||
}
|
||||
yearPath := filepath.Join(archiveRoot, year.Name())
|
||||
months, err := os.ReadDir(yearPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("taskloop: read archive year %q: %w", year.Name(), err)
|
||||
}
|
||||
for _, month := range months {
|
||||
if !month.IsDir() {
|
||||
continue
|
||||
}
|
||||
monthPath := filepath.Join(yearPath, month.Name())
|
||||
groupDirs, err := os.ReadDir(monthPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("taskloop: read archive month %q/%q: %w", year.Name(), month.Name(), err)
|
||||
}
|
||||
for _, groupDir := range groupDirs {
|
||||
if !groupDir.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := groupDir.Name()
|
||||
if strings.HasPrefix(name, taskGroupPrefix) {
|
||||
slug, err := parseArchiveGroupDir(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
candidates[slug] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slugs := make([]string, 0, len(candidates))
|
||||
for slug := range candidates {
|
||||
slugs = append(slugs, slug)
|
||||
}
|
||||
sort.Strings(slugs)
|
||||
return slugs, nil
|
||||
}
|
||||
|
||||
func scanMilestones(root, selected string) ([]MilestoneView, error) {
|
||||
slugs, err := discoverMilestoneCandidates(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
views := make([]MilestoneView, 0, len(slugs))
|
||||
for _, slug := range slugs {
|
||||
units, _, err := scanWorkflow(root, slug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
completed := 0
|
||||
for _, u := range units {
|
||||
if u.Completed {
|
||||
completed++
|
||||
}
|
||||
}
|
||||
isSelected := slug == selected || taskGroupPrefix+slug == selected
|
||||
views = append(views, MilestoneView{
|
||||
ID: slug,
|
||||
Selected: isSelected,
|
||||
WorkUnits: len(units),
|
||||
CompletedWorkUnits: completed,
|
||||
})
|
||||
}
|
||||
if len(views) == 0 {
|
||||
return nil, errors.New("taskloop: no workflow-backed milestones")
|
||||
}
|
||||
return views, nil
|
||||
}
|
||||
|
||||
func readOptionalTaskGroup(root string) ([]os.DirEntry, error) {
|
||||
entries, err := os.ReadDir(root)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
|
|
@ -201,14 +368,19 @@ func readOptionalTaskGroup(root string) ([]os.DirEntry, error) {
|
|||
// operator dry-run. It deliberately uses the same workflow parser as Runtime
|
||||
// and never constructs a manager, provider, lease, or durable state store.
|
||||
func InspectTaskGroup(root, group string) ([]agenttask.WorkUnit, error) {
|
||||
if !strings.HasPrefix(group, taskGroupPrefix) || len(group) == len(taskGroupPrefix) || strings.ContainsAny(group, "/\\\x00\r\n") {
|
||||
if !strings.HasPrefix(group, taskGroupPrefix) {
|
||||
return nil, fmt.Errorf("taskloop: task group must be an exact m- prefixed identifier")
|
||||
}
|
||||
slugCandidate := strings.TrimPrefix(group, taskGroupPrefix)
|
||||
slug, err := parseMilestoneSlug(slugCandidate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("taskloop: task group must be an exact m- prefixed identifier: %w", err)
|
||||
}
|
||||
canonicalRoot, err := canonicalDirectory(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
units, _, err := scanWorkflow(canonicalRoot, strings.TrimPrefix(group, taskGroupPrefix))
|
||||
units, _, err := scanWorkflow(canonicalRoot, slug)
|
||||
return units, err
|
||||
}
|
||||
|
||||
|
|
@ -216,69 +388,114 @@ func readArchivedUnits(
|
|||
root, milestone string,
|
||||
seen map[agenttask.WorkUnitID]string,
|
||||
) ([]agenttask.WorkUnit, []string, error) {
|
||||
directPattern := filepath.Join(
|
||||
root,
|
||||
"agent-task",
|
||||
"archive",
|
||||
"*",
|
||||
"*",
|
||||
taskGroupPrefix+milestone,
|
||||
completeFileName,
|
||||
)
|
||||
directCompletions, err := filepath.Glob(directPattern)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
archiveRoot := filepath.Join(root, "agent-task", "archive")
|
||||
years, err := os.ReadDir(archiveRoot)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
pattern := filepath.Join(
|
||||
root,
|
||||
"agent-task",
|
||||
"archive",
|
||||
"*",
|
||||
"*",
|
||||
taskGroupPrefix+milestone,
|
||||
"*",
|
||||
completeFileName,
|
||||
)
|
||||
completions, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, fmt.Errorf("taskloop: read archive root: %w", err)
|
||||
}
|
||||
sort.Strings(directCompletions)
|
||||
sort.Strings(completions)
|
||||
|
||||
type archiveMatch struct {
|
||||
completionPath string
|
||||
workID agenttask.WorkUnitID
|
||||
aliases []string
|
||||
taskDirectory string
|
||||
}
|
||||
var matches []archiveMatch
|
||||
|
||||
for _, year := range years {
|
||||
if !year.IsDir() {
|
||||
continue
|
||||
}
|
||||
yearPath := filepath.Join(archiveRoot, year.Name())
|
||||
months, err := os.ReadDir(yearPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("taskloop: read archive year %q: %w", year.Name(), err)
|
||||
}
|
||||
for _, month := range months {
|
||||
if !month.IsDir() {
|
||||
continue
|
||||
}
|
||||
monthPath := filepath.Join(yearPath, month.Name())
|
||||
groupDirs, err := os.ReadDir(monthPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("taskloop: read archive month %q/%q: %w", year.Name(), month.Name(), err)
|
||||
}
|
||||
for _, groupDir := range groupDirs {
|
||||
if !groupDir.IsDir() {
|
||||
continue
|
||||
}
|
||||
groupName := groupDir.Name()
|
||||
if !strings.HasPrefix(groupName, taskGroupPrefix) {
|
||||
continue
|
||||
}
|
||||
slug, err := parseArchiveGroupDir(groupName)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("taskloop: archived task group %q: %w", groupName, err)
|
||||
}
|
||||
if slug != milestone {
|
||||
continue
|
||||
}
|
||||
groupPath := filepath.Join(monthPath, groupName)
|
||||
|
||||
directCompletion := filepath.Join(groupPath, completeFileName)
|
||||
if info, statErr := os.Stat(directCompletion); statErr == nil && info.Mode().IsRegular() {
|
||||
matches = append(matches, archiveMatch{
|
||||
completionPath: directCompletion,
|
||||
workID: singlePlanWorkUnitID(milestone),
|
||||
aliases: []string{taskGroupPrefix + milestone, milestone},
|
||||
taskDirectory: taskGroupPrefix + milestone,
|
||||
})
|
||||
} else if statErr != nil && !errors.Is(statErr, fs.ErrNotExist) {
|
||||
return nil, nil, fmt.Errorf("taskloop: inspect archive completion %q: %w", directCompletion, statErr)
|
||||
}
|
||||
|
||||
subEntries, err := os.ReadDir(groupPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("taskloop: read archive task group %q: %w", groupName, err)
|
||||
}
|
||||
for _, sub := range subEntries {
|
||||
if !sub.IsDir() {
|
||||
continue
|
||||
}
|
||||
taskDirName := sub.Name()
|
||||
subCompletion := filepath.Join(groupPath, taskDirName, completeFileName)
|
||||
info, statErr := os.Stat(subCompletion)
|
||||
if statErr == nil && info.Mode().IsRegular() {
|
||||
id, _, aliases, parseErr := parseTaskDirectory(taskDirName)
|
||||
if parseErr != nil {
|
||||
return nil, nil, fmt.Errorf("taskloop: archived task %q: %w", taskDirName, parseErr)
|
||||
}
|
||||
matches = append(matches, archiveMatch{
|
||||
completionPath: subCompletion,
|
||||
workID: agenttask.WorkUnitID(id),
|
||||
aliases: aliases,
|
||||
taskDirectory: taskDirName,
|
||||
})
|
||||
} else if statErr != nil && !errors.Is(statErr, fs.ErrNotExist) {
|
||||
return nil, nil, fmt.Errorf("taskloop: inspect archive completion %q: %w", subCompletion, statErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(matches, func(i, j int) bool {
|
||||
return matches[i].completionPath < matches[j].completionPath
|
||||
})
|
||||
|
||||
var units []agenttask.WorkUnit
|
||||
var evidence []string
|
||||
directID := singlePlanWorkUnitID(milestone)
|
||||
for _, completion := range directCompletions {
|
||||
for _, m := range matches {
|
||||
unit, unitEvidence, include, err := readArchivedUnit(
|
||||
root,
|
||||
milestone,
|
||||
completion,
|
||||
directID,
|
||||
[]string{taskGroupPrefix + milestone, milestone},
|
||||
taskGroupPrefix+milestone,
|
||||
seen,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if include {
|
||||
units = append(units, unit)
|
||||
evidence = append(evidence, unitEvidence)
|
||||
}
|
||||
}
|
||||
for _, completion := range completions {
|
||||
taskDirectory := filepath.Base(filepath.Dir(completion))
|
||||
id, _, aliases, err := parseTaskDirectory(taskDirectory)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("taskloop: archived task %q: %w", taskDirectory, err)
|
||||
}
|
||||
unit, unitEvidence, include, err := readArchivedUnit(
|
||||
root,
|
||||
milestone,
|
||||
completion,
|
||||
agenttask.WorkUnitID(id),
|
||||
aliases,
|
||||
taskDirectory,
|
||||
m.completionPath,
|
||||
m.workID,
|
||||
m.aliases,
|
||||
m.taskDirectory,
|
||||
seen,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,180 @@ func TestWorkflowNormalizesActiveCompletionAndDependencies(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWorkflowMilestonesListsSelectableTaskGroups(t *testing.T) {
|
||||
fixture := newRuntimeFixture(t)
|
||||
archivedComp := filepath.Join(fixture.projectA, "agent-task", "archive", "2026", "07", "m-m2", "01_done", completeFileName)
|
||||
if err := os.MkdirAll(filepath.Dir(archivedComp), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeTaskFile(t, archivedComp, "complete\n")
|
||||
|
||||
milestones, err := fixture.runtime.Milestones(context.Background(), "project-a")
|
||||
if err != nil {
|
||||
t.Fatalf("Milestones: %v", err)
|
||||
}
|
||||
if len(milestones) != 2 {
|
||||
t.Fatalf("milestones count = %d, want 2", len(milestones))
|
||||
}
|
||||
if milestones[0].ID != "m1" || !milestones[0].Selected || milestones[0].WorkUnits != 2 || milestones[0].CompletedWorkUnits != 0 {
|
||||
t.Fatalf("milestones[0] = %#v", milestones[0])
|
||||
}
|
||||
if milestones[1].ID != "m2" || milestones[1].Selected || milestones[1].WorkUnits != 1 || milestones[1].CompletedWorkUnits != 1 {
|
||||
t.Fatalf("milestones[1] = %#v", milestones[1])
|
||||
}
|
||||
|
||||
for _, m := range milestones {
|
||||
if err := fixture.runtime.SelectMilestone(context.Background(), "project-a", m.ID); err != nil {
|
||||
t.Fatalf("SelectMilestone(%q): %v", m.ID, err)
|
||||
}
|
||||
snapshot, err := fixture.runtime.WorkflowSnapshot(context.Background(), "project-a")
|
||||
if err != nil {
|
||||
t.Fatalf("WorkflowSnapshot for %q: %v", m.ID, err)
|
||||
}
|
||||
if len(snapshot.Units) != m.WorkUnits {
|
||||
t.Fatalf("snapshot.Units length = %d, want %d for milestone %q", len(snapshot.Units), m.WorkUnits, m.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkflowMilestonesFailClosedBoundaryMatrix(t *testing.T) {
|
||||
t.Run("empty workspace returns error", func(t *testing.T) {
|
||||
fixture := newRuntimeFixture(t)
|
||||
if err := os.RemoveAll(filepath.Join(fixture.projectA, "agent-task")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := fixture.runtime.Milestones(context.Background(), "project-a")
|
||||
if err == nil || !strings.Contains(err.Error(), "no workflow-backed milestones") {
|
||||
t.Fatalf("Milestones error = %v, want no workflow-backed milestones", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("malformed selected workflow fails closed", func(t *testing.T) {
|
||||
fixture := newRuntimeFixture(t)
|
||||
malformed := filepath.Join(fixture.projectA, "agent-task", "m-m1", "3_bad")
|
||||
if err := os.MkdirAll(malformed, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeTaskFile(t, filepath.Join(malformed, "PLAN-test.md"), "# Bad Plan without summary\n")
|
||||
writeTaskFile(t, filepath.Join(malformed, "CODE_REVIEW-test.md"), "# Review\n")
|
||||
|
||||
_, err := fixture.runtime.Milestones(context.Background(), "project-a")
|
||||
if err == nil || !strings.Contains(err.Error(), "Modified Files Summary is missing") {
|
||||
t.Fatalf("Milestones error = %v, want Modified Files Summary is missing", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid active identity in agent-task fails closed", func(t *testing.T) {
|
||||
fixture := newRuntimeFixture(t)
|
||||
badDir := filepath.Join(fixture.projectA, "agent-task", "m-FOO")
|
||||
if err := os.MkdirAll(badDir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := fixture.runtime.Milestones(context.Background(), "project-a")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid active identity m-FOO")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid archive identity fails closed", func(t *testing.T) {
|
||||
fixture := newRuntimeFixture(t)
|
||||
badDir := filepath.Join(fixture.projectA, "agent-task", "archive", "2026", "07", "m-*")
|
||||
if err := os.MkdirAll(badDir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := fixture.runtime.Milestones(context.Background(), "project-a")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid archive identity m-*")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInspectTaskGroupRejectsInvalidIdentifiers(t *testing.T) {
|
||||
workspace := canonicalTempDir(t)
|
||||
invalid := []string{
|
||||
"m-*",
|
||||
"m-foo?",
|
||||
"m-FOO",
|
||||
"m-foo_bar",
|
||||
"m-foo bar",
|
||||
"m-foo/bar",
|
||||
"m-",
|
||||
"m-123+456",
|
||||
"foo",
|
||||
"",
|
||||
}
|
||||
for _, id := range invalid {
|
||||
t.Run(id, func(t *testing.T) {
|
||||
_, err := InspectTaskGroup(workspace, id)
|
||||
if err == nil {
|
||||
t.Fatalf("InspectTaskGroup(%q) expected error, got nil", id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkflowMilestonesNormalizesArchiveCollisionSuffixes(t *testing.T) {
|
||||
workspace := canonicalTempDir(t)
|
||||
createSinglePlanPair(t, workspace, "m1")
|
||||
|
||||
archive1 := filepath.Join(workspace, "agent-task", "archive", "2026", "06", "m-m1_1", "01_done", completeFileName)
|
||||
archive2 := filepath.Join(workspace, "agent-task", "archive", "2026", "07", "m-m1_2", "02_done", completeFileName)
|
||||
for _, p := range []string{archive1, archive2} {
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeTaskFile(t, p, "complete\n")
|
||||
}
|
||||
|
||||
views, err := scanMilestones(workspace, "m1")
|
||||
if err != nil {
|
||||
t.Fatalf("scanMilestones: %v", err)
|
||||
}
|
||||
if len(views) != 1 {
|
||||
t.Fatalf("views count = %d, want 1 (archive collision suffixes normalized)", len(views))
|
||||
}
|
||||
if views[0].ID != "m1" || views[0].WorkUnits != 3 || views[0].CompletedWorkUnits != 2 {
|
||||
t.Fatalf("views[0] = %#v, want ID=m1 WorkUnits=3 CompletedWorkUnits=2", views[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkflowArchiveOnlyMilestoneRemainsSelectable(t *testing.T) {
|
||||
fixture := newRuntimeFixture(t)
|
||||
if err := os.RemoveAll(filepath.Join(fixture.projectA, "agent-task", "m-m1")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archivedComp := filepath.Join(fixture.projectA, "agent-task", "archive", "2026", "07", "m-m1", completeFileName)
|
||||
if err := os.MkdirAll(filepath.Dir(archivedComp), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeTaskFile(t, archivedComp, "complete\n")
|
||||
|
||||
snapshot, err := fixture.runtime.WorkflowSnapshot(context.Background(), "project-a")
|
||||
if err != nil {
|
||||
t.Fatalf("WorkflowSnapshot for archive-only milestone: %v", err)
|
||||
}
|
||||
if len(snapshot.Units) != 1 || !snapshot.Units[0].Completed {
|
||||
t.Fatalf("snapshot units = %#v", snapshot.Units)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectTaskGroupArchiveOnly(t *testing.T) {
|
||||
workspace := canonicalTempDir(t)
|
||||
completion := filepath.Join(workspace, "agent-task", "archive", "2026", "07", "m-m1", completeFileName)
|
||||
if err := os.MkdirAll(filepath.Dir(completion), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeTaskFile(t, completion, "complete\n")
|
||||
|
||||
units, err := InspectTaskGroup(workspace, "m-m1")
|
||||
if err != nil {
|
||||
t.Fatalf("InspectTaskGroup: %v", err)
|
||||
}
|
||||
if len(units) != 1 || units[0].ID != "m-m1" || !units[0].Completed {
|
||||
t.Fatalf("units = %#v", units)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectTaskGroupSupportsSingleAndSplitLayouts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
Loading…
Reference in a new issue