Skip to content

feat(temporal): DevLoopWorkflow — issue -> investigate -> approve -> implement - #105

Merged
mashkovd merged 4 commits into
mainfrom
agents/temporal-dev-loop-worker
Aug 5, 2026
Merged

mashkovd merged 4 commits into
mainfrom
agents/temporal-dev-loop-worker

Conversation

@mashkovd

@mashkovd mashkovd commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

  • Phase 4 of the dev-workflow control plane plan (.claude/plans/deployment-purrfect-umbrella.md): first Temporal worker + workflow, layered on top of the existing Argo pipeline (does not replace or duplicate any agent's actual SDK run).
  • orchestrator/temporal/activities/registry.py — resolve_agent_release, reading the phase-2 agent registry in mctl-api. Returns None (not an exception) when nothing has ever been promoted for an (agent, environment) pair — the expected steady state before a first mctl_promote_agent, not a failure.
  • orchestrator/temporal/activities/argo.py — submit_and_wait, submitting a CWFT via mctl-api's existing /api/v1/operations/{name}/execute path (same path mctl_trigger_issue already uses) and polling /api/v1/workflows/{name} to a terminal Argo phase. DevLoopWorkflow registers this activity with maximum_attempts=1 — the CWFTs already retry within a run (second-OAuth-account fallback on a 429), so a Temporal-level retry on top would multiply real SDK spend exactly when quota is already tight.
  • orchestrator/temporal/activities/state.py — record_execution, POSTing to /api/v1/agents/executions (mctl-api companion change — separate PR, mctl-api#TBD). Needed because Argo workflow objects expire after ttlStrategy.secondsAfterCompletion — this activity captures the outcome at completion time instead of relying on being able to re-read Argo later.
  • orchestrator/temporal/workflows/dev_loop.py — DevLoopWorkflow, covering exactly the plan's phase-4 acceptance slice: investigate → durable wait_condition for a signalled approve() → implement. The review-fix loop (shepherd) and deploy/monitor stages are phase 5/6, meant to layer on top of this workflow once the slice is proven, not part of this PR.
  • orchestrator/temporal/worker.py + cli.py — worker entrypoint (task queue mctl-dev-loop) and a manual start/approve/status CLI. Stands in for the mctl_trigger_issue use_temporal flag (a separate, larger mctl-api Go-client change) until that lands — lets a real workflow be started/approved/inspected for verification in the meantime.
  • pyproject.toml — added temporalio==1.19.0 as a new pinned direct dependency, and promoted httpx (already transitive via claude-agent-sdk/mcp) to a direct, deliberately-pinned one since the new activities import it directly.

Test plan

  • ruff check . — clean
  • mypy — clean (29 source files)
  • pytest -q — 306 passed (was 294; +12 new: 9 activity tests against a mocked httpx transport asserting real mctl-api request/response shapes, 3 DevLoopWorkflow tests against temporalio.testing.WorkflowEnvironment.start_time_skipping() with fake activities)
  • Live: once mctl-agents-worker is deployed (mctl-gitops, separate PR) and POST /api/v1/agents/executions exists (mctl-api, separate PR), start a real workflow via orchestrator.temporal.cli start <issue_url>, confirm it shows up in the Temporal UI, signal approve, confirm the implement step runs

…implement

Phase 4 of the dev-workflow control plane plan: the first Temporal worker
and workflow, layered on top of the Argo pipeline rather than replacing it.

- orchestrator/temporal/activities/registry.py: resolve_agent_release,
  reading the phase-2 agent registry in mctl-api (falls back to None, not
  an exception, when nothing has ever been promoted for an agent/env pair).
- orchestrator/temporal/activities/argo.py: submit_and_wait, submitting a
  CWFT via mctl-api's existing operations-execute path and polling to a
  terminal Argo phase. Registered with maximum_attempts=1 by the workflow —
  the CWFTs already retry within a run on a second OAuth account, so a
  Temporal-level retry on top would multiply real SDK spend.
- orchestrator/temporal/activities/state.py: record_execution, writing the
  phase-4 execution audit trail via POST /api/v1/agents/executions (mctl-api
  companion change, separate PR) — Argo workflow objects expire after
  ttlStrategy.secondsAfterCompletion, so this is the durable record.
- orchestrator/temporal/workflows/dev_loop.py: DevLoopWorkflow, covering
  exactly the plan's phase-4 acceptance slice (investigate -> durable wait
  for a signalled approval -> implement). Review-loop and deploy/monitor
  stages are phase 5/6, layered on top later.
- orchestrator/temporal/worker.py + cli.py: worker entrypoint (task queue
  mctl-dev-loop) and a manual start/approve/status CLI, standing in for the
  mctl_trigger_issue use_temporal wiring (separate mctl-api change) until
  that lands.

Tests: activities against a mocked httpx transport (real request/response
shapes asserted, not just "was called"); DevLoopWorkflow against
temporalio's time-skipping WorkflowEnvironment with fake activities.
@claude

claude Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Claude finished @mashkovd's task in 3m 19s —— View job


Reviewing PR #105

  • Gather context (repo layout, related MCTL_TOKEN/MCTL_MCP_URL conventions, CI status)
  • Understand the request (code review of DevLoopWorkflow PR)
  • Review correctness/logic, security, error handling, test coverage
  • Post inline comments for findings
  • Submit final review verdict

Summary

Solid, well-documented phase-4 slice (investigate → durable approval wait → implement) with good test coverage (activities against mocked httpx, workflow against Temporal's time-skipping test env). CI ("PR validation": ruff/mypy/pytest) passed.

Findings: 0 P1, 2 P2, 2 P3

  • P2 (workflows/dev_loop.py): _resolve/_record activity calls have no bounded retry_policy, so they inherit Temporal's default unlimited retries. Since POST /api/v1/agents/executions doesn't exist in mctl-api yet (separate PR, per the description), a real run could succeed at investigate and then retry record_execution forever, permanently blocking the approval wait_condition.
  • P2 (activities/argo.py): the poll loop has no resilience to transient HTTP errors on the status GET — any blip fails the whole activity. Combined with the deliberate maximum_attempts=1 (to avoid duplicate Argo/SDK spend), a network hiccup mid-poll forces a manual retry that resubmits a new CWFT — the same spend-multiplication problem the retry policy was designed to prevent.
  • P3 (activities/registry.py): /versions lookup assumes an unpaginated response.
  • P3 (cli.py): no test coverage for the manual CLI (low priority — verification tool, not production path).

Everything else — retry-ownership reasoning (no Temporal retry stacked on SDK-backed CWFT runs), the resolve_agent_release "None is not a failure" contract, workflow determinism (activities only, no non-deterministic calls in the workflow body), token handling (MCTL_TOKEN never logged), and the dependencies pin rationale in pyproject.toml — looked correct and consistent with existing repo conventions.

Submitted as request changes given the two P2s (both are real risk-of-duplicate-spend / stuck-workflow scenarios, not style nits).
· Branch

Comment on lines +61 to +80
async def _resolve(agent: str) -> ResolvedRelease | None:
return await workflow.execute_activity(
resolve_agent_release,
args=[agent, ENVIRONMENT],
start_to_close_timeout=FAST_ACTIVITY_TIMEOUT,
)


async def _run_cwft(operation: str, params: dict[str, str]) -> WorkflowResult:
return await workflow.execute_activity(
submit_and_wait,
SubmitAndWaitInput(operation=operation, params=params),
start_to_close_timeout=SDK_STEP_TIMEOUT,
heartbeat_timeout=SDK_STEP_HEARTBEAT_TIMEOUT,
retry_policy=SDK_STEP_RETRY_POLICY,
)


async def _record(agent: str, release: ResolvedRelease | None, result: WorkflowResult) -> None:
await workflow.execute_activity(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — _resolve and _record execute their activities with no explicit retry_policy, so they inherit Temporal's default of unlimited attempts with capped backoff. Per the PR description, POST /api/v1/agents/executions doesn't exist in mctl-api yet (separate PR). If mctl-agents-worker is deployed before that companion endpoint ships, every real run will succeed at investigate and then retry _record forever, permanently blocking wait_condition for approval — even though the actual SDK work already completed successfully.

Consider a bounded retry_policy (e.g. maximum_attempts=N) on record_execution (and resolve_agent_release), or making the audit-trail write best-effort (log + continue) so a state-recording failure can't wedge the workflow's critical path indefinitely.

Fix this →

Comment on lines +73 to +83
while True:
# Heartbeat before every poll, not just on change: a stuck
# mctl-api / cluster makes this loop spin on the `continue`
# below without ever reaching a terminal phase, and the
# heartbeat is what lets Temporal notice a genuinely wedged
# activity (worker crash, network partition) instead of the
# activity looking alive forever because polling itself is
# still succeeding.
activity.heartbeat(workflow_name)

status_resp = await client.get(f"/api/v1/workflows/{workflow_name}", headers=headers)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — The poll loop has no resilience to transient HTTP errors on the GET /api/v1/workflows/{name} call: status_resp.raise_for_status() propagates any transient 5xx/network blip straight out of the activity. Since DevLoopWorkflow registers this activity with maximum_attempts=1 (deliberately, to avoid duplicate Argo/SDK submissions), a single mctl-api hiccup hours into an already-submitted run kills the whole activity permanently — and the workflow_name of the in-flight Argo run isn't persisted anywhere. Recovering means either losing track of that run or an operator manually retrying, which resubmits a new CWFT and duplicates real SDK spend — exactly the failure mode maximum_attempts=1 was chosen to prevent, just triggered by a polling-side blip instead of a Temporal auto-retry.

Consider catching transient errors in the polling loop itself (e.g. retry the GET a few times before giving up) rather than letting any single poll failure fail the whole activity.

resolve_resp.raise_for_status()
version = resolve_resp.json()["version"]

versions_resp = await client.get(f"/api/v1/agents/{agent}/versions", headers=headers)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — versions_resp.json().get("items", []) assumes the /versions response is unpaginated. If mctl-api ever paginates this list, a version could silently fail to be found here, and resolve_agent_release would return image_ref="" (falling back to the CWFT default) rather than surfacing an error. Worth a comment noting the assumption, or handling pagination if the endpoint supports it.

@@ -0,0 +1,99 @@
"""Manual DevLoopWorkflow control: `python -m orchestrator.temporal.cli ...`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — No tests for this module (start/approve/status). It's described as a stand-in CLI for manual verification, so this is low priority, but _workflow_id_for's URL-parsing logic (lines 25-31) would be cheap to unit test directly.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has P1/P2 findings, changes requested: (0 P1, 2 P2, 2 P3) — record_execution/resolve_agent_release have no bounded retry_policy and can wedge the workflow forever if mctl-api's companion endpoint is unavailable; the Argo poll loop has no resilience to transient HTTP errors, which combined with maximum_attempts=1 risks duplicate CWFT/SDK submissions on recovery.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 24b586a8fe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

SubmitAndWaitInput(operation=operation, params=params),
start_to_close_timeout=SDK_STEP_TIMEOUT,
heartbeat_timeout=SDK_STEP_HEARTBEAT_TIMEOUT,
retry_policy=SDK_STEP_RETRY_POLICY,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep submitted Argo runs resumable

When the worker restarts or loses heartbeats after submit_and_wait has already POSTed the Argo workflow, the missed heartbeat times out this same activity attempt; with maximum_attempts=1, Temporal then fails the whole DevLoopWorkflow instead of resuming the poll, even though the Argo run may still finish and need to be recorded/approved. For long SDK-backed runs during a worker rollout or crash, this loses the durable per-issue state the new control plane is meant to provide; split submit from polling or resume polling from a persisted workflow name/idempotency key while keeping duplicate submits disabled.

Useful? React with 👍 / 👎.

Comment on lines +33 to +36
version: str # "" when no registry release existed yet (CWFT default image used)
image_ref: str # "" when no registry release existed yet
argo_workflow_name: str
phase: str # Succeeded | Failed | Error

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record the target repo SHA with executions

For issue-investigator and implementer runs, the repository inventory states that runtime inputs outside this repo are pinned by the target repo git SHA recorded on the execution record (docs/agent-inventory.yaml:55-63), because the same agent version against different target SHAs is not reproducible. This new execution record only captures the agent version/image and Argo result, so mctl_list_recent_agent_runs cannot reconstruct or compare the actual prompt surface when the target repo changes between runs; include the target repo/ref SHA in the recorded payload.

Useful? React with 👍 / 👎.

implement_params["agent_image"] = implementer_release.image_ref
implement_params["agent_version"] = f"implementer@{implementer_release.version}"

implement_result = await _run_cwft("mctl-agents-implement", implement_params)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Accept the proposal before running the implementer

In the Temporal-only approval path, the investigator still writes a proposed proposal and this signal only flips the workflow's in-memory _approved flag; nothing updates the gitops .status.yaml to accepted before invoking the legacy mctl-agents-implement template. Since run_implementer.py only selects proposals whose status is accepted, an approved DevLoopWorkflow will either do nothing for this issue or pick some unrelated already-accepted proposal because implement_params is empty.

Useful? React with 👍 / 👎.

temporal_workflow_id=workflow.info().workflow_id,
agent=agent,
environment=ENVIRONMENT,
version=release.version if release else "",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't record unpinned defaults as released versions

When resolve_agent_release finds a release version but cannot build an image_ref from the versions response, the workflow deliberately omits agent_image/agent_version and lets Argo run the CWFT's baked-in default image. This record still stores release.version, so the audit trail can claim version 1.2.0 produced a PR even though that version was never passed to the run; only record the version when the image was actually pinned, or explicitly mark that the default image ran.

Useful? React with 👍 / 👎.

- dev_loop.py: _resolve and _record now use a bounded RetryPolicy
  (maximum_attempts=5) instead of Temporal's unlimited default. _record's
  eventual failure is caught and logged rather than propagated — it's an
  audit-trail write for a step that already succeeded, so it must never
  block wait_condition/approval indefinitely (P2, claude[bot]).
- argo.py: submit_and_wait's poll loop now rides out up to 20 consecutive
  transient errors (mctl-api 5xx, network blip) before giving up, instead
  of letting any single poll failure kill the activity outright — this
  activity runs with maximum_attempts=1 on purpose, so a bare
  raise_for_status() on every poll meant one blip permanently lost track
  of an already-submitted Argo run (P2, claude[bot]).
- registry.py: note the unpaginated-response assumption on GET
  .../versions (P3).
- New tests/test_temporal_cli.py for _workflow_id_for (P3) — must stay
  byte-for-byte identical to mctl-api's Go-side WorkflowIDForIssueURL.
- New workflow test for the best-effort record_execution behavior, and two
  new activity tests for the poll-resilience fix (rides out N errors then
  succeeds; gives up after too many).
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@claude

claude Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Claude finished @mashkovd's task in 3m 55s —— View job


Reviewing PR #105 (new commit 82cf0ae)

  • Gather context (new commit — "fix: bound retry policies and poll resilience per review")
  • Check whether the fix commit addresses prior findings (mine + Codex's)
  • Review the diff for new issues
  • Post inline comments for findings
  • Submit final review verdict

Summary

Both of my prior P2 findings are well fixed in 82cf0ae:

  • Unbounded retries on _resolve/_record → now bounded (RetryPolicy(maximum_attempts=5)), and _record wraps record_execution in try/except so a persistent audit-write failure logs a warning instead of wedging wait_condition forever.
  • No resilience in the Argo poll loop → now catches HTTPStatusError/TransportError and rides out up to 20 consecutive failures (~5 min at the 15s poll interval) before giving up, keeping maximum_attempts=1 on the submit/poll activity itself (still correct — avoids duplicate CWFT submissions).
  • The registry.py P3 (unpaginated /versions assumption) got a documenting comment; cli.py P3 picked up tests for _workflow_id_for.

CI ("PR validation": ruff/mypy/pytest) passed on the new commit.

New findings this round: 0 P1, 2 P2 (both independently verified — same ones Codex's automated review already surfaced, confirmed here with direct code evidence rather than just restated):

  • P2 (activities/argo.py): submit_and_wait's Argo workflow_name still isn't persisted anywhere Temporal can use to resume. Combined with maximum_attempts=1, a worker crash or missed heartbeat mid-poll after a successful Argo submission permanently fails the workflow and orphans the in-flight Argo run — it may still finish, but nothing will record or approve it.
  • P2 (workflows/dev_loop.py): confirmed against run_implementer.py — approve() only flips an in-memory flag; nothing writes the gitops .status.yaml to accepted, and implement_params has no service/slug filter. An approved run either no-ops (nothing accepted) or can implement an unrelated already-accepted proposal instead of this issue's.

Both are explicitly called out as known simplifications in the module docstrings, so not a surprise to the author — flagged should-fix because they bear on the PR's own "approve → implement" acceptance claim and the still-unchecked "Live" verification step, not as pushback on the phase-4 scoping decision itself.

Submitted as request changes given the two P2s.

· Branch

while True:
# Heartbeat before every poll, not just on change: a stuck
# mctl-api / cluster makes this loop spin on the `continue`
# below without ever reaching a terminal phase, and the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — _run_cwft still registers submit_and_wait with maximum_attempts=1 and, per this activity's own docstring, the submitted Argo workflow_name is never persisted anywhere Temporal can use to resume (no query handler, no memo/search attribute — only activity.logger.info at line 79 and the per-poll activity.heartbeat(workflow_name), whose details are never read back on retry since there is no retry to read them on). If the worker restarts or a single heartbeat is missed mid-poll (SDK_STEP_HEARTBEAT_TIMEOUT = 2 min) after the Argo submission already succeeded, the whole DevLoopWorkflow run fails permanently and the in-flight Argo run is orphaned — it may still finish, but nothing will ever record or approve based on it.

This is the same tradeoff already called out in the module docstring (lines 5-12) as intentional — avoiding a Temporal retry stacked on the CWFT's own OAuth-account fallback — but the resumability gap itself (as opposed to the retry-multiplication one) wasn't in scope of that reasoning. Worth tracking before this worker is deployed for real (per the PR's own "Live" test-plan item, still unchecked): e.g. persist workflow_name via workflow.upsert_memo/a query handler so an operator can at least recover/re-poll it manually, even with maximum_attempts=1 staying at 1.

[Fix this →](https://claude.ai/code?q=In%20orchestrator%2Ftemporal%2Factivities%2Fargo.py%2C%20submit_and_wait%20(around%20line%2078)%20gets%20workflow_name%20from%20the%20Argo%20submit%20response%20but%20never%20persists%20it%20anywhere%20Temporal%20can%20use%20to%20resume%20polling%20after%20a%20worker%20crash%20or%20missed%20heartbeat.%20Since%20DevLoopWorkflow%20registers%20this%20activity%20with%20maximum_attempts%3D1%2C%20a%20crash%20mid-poll%20permanently%20fails%20the%20workflow%20and%20orphans%20the%20already-submitted%20Argo%20run.%20Add%20a%20way%20to%20recover%20the%20workflow_name%20(e.g.%20workflow.upsert_memo%20or%20a%20query%20handler%20in%20DevLoopWorkflow%20in%20orchestrator%2Ftemporal%2Fworkflows%2Fdev_loop.py)%20so%20an%20operator%20can%20manually%20re-poll%20or%20record%20the%20orphaned%20run%20on%20branch%20agents%2Ftemporal-dev-loop-worker.&repo=mctlhq/mctl-agents

implement_params["agent_image"] = implementer_release.image_ref
implement_params["agent_version"] = f"implementer@{implementer_release.version}"

implement_result = await _run_cwft("mctl-agents-implement", implement_params)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — Confirmed against orchestrator/run_implementer.py: find_accepted_proposals filters strictly on status == "accepted" and implement_one skips anything else. Nothing in this repo (this PR or pre-existing) writes .status.yaml to accepted automatically — run_issue_investigator.py only ever writes proposed and tells a human to flip it manually. approve() here (lines 125-127) has no side effect beyond self._approved = True.

So today, calling approve() on a real deployment and reaching this _run_cwft("mctl-agents-implement", implement_params) call does one of two things, neither of which is "implement this issue's proposal": with no accepted proposals it's a no-op, or — since implement_params (lines 152-156) carries no service/slug filter — it picks up whatever unrelated proposal already happens to be accepted (max_proposals=1 per the CWFT default). The latter is the concerning case: it's not just "nothing happens", it's "the wrong issue's proposal gets implemented."

This is called out as a known simplification in the module docstring (lines 12-17, 19-23), so not a surprise to the author, but it means the PR's own acceptance slice ("approval signal -> implement -> PR") doesn't actually close the loop end-to-end yet — worth confirming this is truly gated behind the still-unchecked "Live" test-plan item and isn't reachable from mctl_trigger_issue before phase 5's cutover lands.

[Fix this →](https://claude.ai/code?q=In%20orchestrator%2Ftemporal%2Fworkflows%2Fdev_loop.py%2C%20the%20approve%20signal%20(lines%20125-127)%20only%20sets%20an%20in-memory%20flag%20and%20never%20updates%20the%20gitops%20.status.yaml%20for%20this%20issue%27s%20proposal%20to%20accepted%20before%20the%20implement%20CWFT%20runs%20at%20line%20158.%20Since%20orchestrator%2Frun_implementer.py%27s%20find_accepted_proposals%20only%20selects%20proposals%20with%20status%3D%3Daccepted%2C%20and%20implement_params%20(lines%20152-156)%20has%20no%20service%2Fslug%20filter%2C%20an%20approved%20DevLoopWorkflow%20run%20may%20implement%20an%20unrelated%20already-accepted%20proposal%20instead%20of%20this%20issue%27s%20one%2C%20or%20do%20nothing.%20Either%20write%20the%20proposal%27s%20.status.yaml%20to%20accepted%20as%20part%20of%20handling%20the%20approve%20signal%2C%20or%20pass%20a%20scoping%20filter%20(issue%2Fslug)%20into%20implement_params%20so%20only%20this%20issue%27s%20proposal%20can%20be%20picked%20up%2C%20on%20branch%20agents%2Ftemporal-dev-loop-worker.&repo=mctlhq/mctl-agents

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has P1/P2 findings, changes requested: 0 P1, 2 P2, 0 P3.

Both P2s from my prior review (unbounded retries on _resolve/_record, no resilience in the Argo poll loop) are well fixed in 82cf0ae — bounded RetryPolicy(maximum_attempts=5) plus a best-effort try/except around record_execution, and the poll loop now rides out up to 20 consecutive transient errors (~5 min) before giving up. The registry.py P3 got a documenting comment and cli.py picked up tests for _workflow_id_for. Nice work.

Two new P2s, independently verified against the current code (both already surfaced by Codex's automated review, confirming here with direct evidence):

  1. activities/argo.py — submit_and_wait's Argo workflow_name still isn't persisted anywhere Temporal can use to resume; combined with maximum_attempts=1, a worker crash or missed heartbeat mid-poll after a successful Argo submission permanently fails the workflow and orphans the in-flight Argo run.
  2. workflows/dev_loop.py — confirmed against run_implementer.py: approve() only flips an in-memory flag, nothing writes .status.yaml to accepted, and implement_params has no service/slug filter — so an approved run either no-ops or can implement an unrelated already-accepted proposal instead of this issue's.

Both are explicitly called out as known simplifications in the module docstrings, so not a surprise to the author — flagging as should-fix given they affect the PR's core "approve -> implement" acceptance claim and the still-unchecked "Live" test-plan item, not as a rejection of the scoping decision itself.

while True:
# Heartbeat before every poll, not just on change: a stuck
# mctl-api / cluster makes this loop spin on the `continue`
# below without ever reaching a terminal phase, and the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — _run_cwft still registers submit_and_wait with maximum_attempts=1 and, per this activity's own docstring, the submitted Argo workflow_name is never persisted anywhere Temporal can use to resume (no query handler, no memo/search attribute — only activity.logger.info at line 79 and the per-poll activity.heartbeat(workflow_name), whose details are never read back on retry since there is no retry to read them on). If the worker restarts or a single heartbeat is missed mid-poll (SDK_STEP_HEARTBEAT_TIMEOUT = 2 min) after the Argo submission already succeeded, the whole DevLoopWorkflow run fails permanently and the in-flight Argo run is orphaned — it may still finish, but nothing will ever record or approve based on it.

This is the same tradeoff already called out in the module docstring (lines 5-12) as intentional — avoiding a Temporal retry stacked on the CWFT's own OAuth-account fallback — but the resumability gap itself (as opposed to the retry-multiplication one) wasn't in scope of that reasoning. Worth tracking before this worker is deployed for real (per the PR's own "Live" test-plan item, still unchecked): e.g. persist workflow_name via workflow.upsert_memo/a query handler so an operator can at least recover/re-poll it manually, even with maximum_attempts=1 staying at 1.

[Fix this →](https://claude.ai/code?q=In%20orchestrator%2Ftemporal%2Factivities%2Fargo.py%2C%20submit_and_wait%20(around%20line%2078)%20gets%20workflow_name%20from%20the%20Argo%20submit%20response%20but%20never%20persists%20it%20anywhere%20Temporal%20can%20use%20to%20resume%20polling%20after%20a%20worker%20crash%20or%20missed%20heartbeat.%20Since%20DevLoopWorkflow%20registers%20this%20activity%20with%20maximum_attempts%3D1%2C%20a%20crash%20mid-poll%20permanently%20fails%20the%20workflow%20and%20orphans%20the%20already-submitted%20Argo%20run.%20Add%20a%20way%20to%20recover%20the%20workflow_name%20(e.g.%20workflow.upsert_memo%20or%20a%20query%20handler%20in%20DevLoopWorkflow%20in%20orchestrator%2Ftemporal%2Fworkflows%2Fdev_loop.py)%20so%20an%20operator%20can%20manually%20re-poll%20or%20record%20the%20orphaned%20run%20on%20branch%20agents%2Ftemporal-dev-loop-worker.&repo=mctlhq/mctl-agents

…it trail (PR #105 review)

Addresses the 4 P1/P2 findings from claude[bot] + chatgpt-codex-connector[bot]
on the latest commit, all independently confirmed against the code:

- submit_and_wait now distinguishes a retry before submission (must
  re-submit) from a retry after submission (must not, only resume
  polling) via activity.info().heartbeat_details, so a worker crash or
  missed heartbeat mid-poll no longer orphans an already-submitted Argo
  run or fails the whole DevLoopWorkflow. SDK_STEP_RETRY_POLICY can now
  allow a few attempts without ever causing a second real SDK run.

- The implement step is now scoped to the issue's own repo (the CWFT's
  existing `service` parameter, derived from issue_url via the new
  shared orchestrator/temporal/issue_ref.py). approve() still doesn't
  automate the proposed -> accepted gitops flip (that needs a new
  gitops-committing CWFT step, deferred to phase 5 per the
  mctl-gitops-main-writes mutex constraint), but it can no longer cause
  a DIFFERENT repo's already-accepted proposal to be implemented.

- Execution records no longer claim a registry version/image_ref was
  used when resolve_agent_release found a version but no pullable
  image_ref (the CWFT's own baked-in default ran instead).

- Execution records now carry target_repo, the first half of the
  reproducibility-pinning story in docs/agent-inventory.yaml (the exact
  target SHA still isn't captured -- no CWFT exposes one as an output
  yet).
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@claude

claude Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Claude finished @mashkovd's task in 6m 4s —— View job


Reviewing PR #105 (new commit ecda152)

  • Gather context (new commit — "fix: resumable Argo polling, scope implement to issue's repo, fix audit trail")
  • Check whether the fix commit addresses prior findings (mine + Codex's)
  • Review the diff for new issues
  • Post inline comments for findings
  • Submit final review verdict

Summary

All 4 P1/P2 findings from the prior two review rounds are well fixed in ecda152:

  • Argo run resumability — submit_and_wait now distinguishes "retry before submission" from "retry after submission" via activity.info().heartbeat_details, so SDK_STEP_RETRY_POLICY can allow a few attempts (now maximum_attempts=3) without re-POSTing an already-submitted CWFT. A worker crash/missed heartbeat mid-poll now resumes polling instead of orphaning the Argo run and permanently failing the workflow.
  • Wrong-repo implement scoping — implement_params now carries "service": target_repo (derived from the issue URL via the new shared orchestrator/temporal/issue_ref.py), so an approve() can at worst no-op or pick up a different accepted proposal in the same repo, never a different one.
  • False audit-trail versions — an unpinned resolve_agent_release result (version found but no image_ref) is no longer recorded as if that version had actually run.
  • Reproducibility — execution records now carry target_repo, per docs/agent-inventory.yaml's pinning note.

CI ("PR validation": ruff/mypy/pytest) passed on the new commit.

New findings this round: 0 P1, 2 P2

  • P2 (activities/argo.py:99-105): the resumability fix narrows but doesn't fully close the duplicate-submission window. Between the Argo POST succeeding and activity.heartbeat(workflow_name) actually completing, a crash or malformed-response exception (e.g. KeyError on submit_resp.json()["workflow"]["workflowName"]) still leaves heartbeat_details empty on retry — and now that maximum_attempts=3 (was 1), that retry will resubmit and duplicate the real SDK run. The module docstring's "never causing a second real SDK run" claim slightly overstates the actual guarantee for this narrow gap.
  • P2 (workflows/dev_loop.py:186): the service-param scoping fix's correctness depends on the sibling mctl-gitops repo's CWFT already declaring service and wiring it to run_implementer.py --service — unverifiable from this repo (orchestrator/validate_manifest.py itself notes the real CWFT YAML lives in mctl-gitops). If that companion change hasn't shipped, this fix silently regresses to today's unscoped behavior with no error and no test able to catch it here. This was the exact P1 Codex's automated review flagged last round, so worth explicit confirmation before the still-unchecked "Live" test-plan item is attempted.

Both are narrower/more subtle than the previous round's findings — real risk-of-duplicate-spend and silent-no-op scenarios, not style nits — so submitted as request changes.

· Branch

Comment on lines +99 to +105
submit_resp.raise_for_status()
workflow_name = submit_resp.json()["workflow"]["workflowName"]
activity.logger.info("submitted %s -> %s", input.operation, workflow_name)
# Record the submission immediately, before the first poll, so
# even a crash on the very next line leaves a retry able to find
# workflow_name via heartbeat_details above instead of resubmitting.
activity.heartbeat(workflow_name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — The resumability fix narrows but doesn't fully close the duplicate-submission window it's meant to eliminate. Between submit_resp.raise_for_status() succeeding (Argo has already created the run server-side) and activity.heartbeat(workflow_name) actually completing, a crash, KeyError/malformed-JSON on submit_resp.json()["workflow"]["workflowName"], or any other exception in that gap leaves heartbeat_details empty on the next attempt — so SDK_STEP_RETRY_POLICY's maximum_attempts=3 will resubmit and duplicate the real SDK run, exactly the failure mode this design is built to prevent.

This is a much narrower window than before (previously the entire activity duration was unprotected, but maximum_attempts=1 meant no retry ever happened, so no duplicate risk either) — raising maximum_attempts to 3 is what reintroduces this specific gap. The module docstring's claim ("without ever causing a second real SDK run for the same step") slightly overstates the actual guarantee.

Given submit_resp.json()["workflow"]["workflowName"] is the most likely failure point in that window (an unexpected response shape), consider either wrapping that parse in a try/except that still heartbeats a "submitted, name unknown" sentinel (so retries at least know not to resubmit even if they can't resume polling), or — the more complete fix — having mctl-api's /execute accept a caller-supplied idempotency key so a duplicate POST after a crash returns the existing workflow instead of creating a new one.

[Fix this →](https://claude.ai/code?q=In%20orchestrator%2Ftemporal%2Factivities%2Fargo.py%2C%20submit_and_wait%20(lines%2094-105)%2C%20close%20the%20remaining%20duplicate-submission%20window%3A%20a%20crash%20or%20exception%20(e.g.%20malformed%20response%20body%20causing%20a%20KeyError%20on%20submit_resp.json()%5B%22workflow%22%5D%5B%22workflowName%22%5D)%20between%20submit_resp.raise_for_status()%20succeeding%20and%20activity.heartbeat(workflow_name)%20completing%20means%20heartbeat_details%20is%20still%20empty%20on%20retry%2C%20so%20SDK_STEP_RETRY_POLICY%27s%20maximum_attempts%3D3%20in%20workflows%2Fdev_loop.py%20will%20resubmit%20and%20duplicate%20the%20real%20SDK%20run.%20Either%20wrap%20the%20response%20parsing%20in%20a%20try%2Fexcept%20that%20still%20heartbeats%20a%20sentinel%20so%20retries%20know%20not%20to%20resubmit%2C%20or%20propose%20an%20idempotency%20key%20on%20mctl-api%27s%20%2Fapi%2Fv1%2Foperations%2F%7Bname%7D%2Fexecute%20endpoint%20so%20a%20duplicate%20POST%20after%20a%20crash%20returns%20the%20existing%20workflow%20instead%20of%20creating%20a%20new%20one%2C%20on%20branch%20agents%2Ftemporal-dev-loop-worker.&repo=mctlhq/mctl-agents

# implement CWFT only considers proposals under agents-state/<service>/,
# so an approve() here can at worst implement a different
# already-accepted proposal in the SAME repo, never a different one.
implement_params: dict[str, str] = {"service": target_repo}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — This fix's correctness depends entirely on the sibling mctl-gitops repo's cwft-mctl-agents-implement.yaml CWFT already declaring a service input parameter and threading it through to run_implementer.py --service <value> — nothing in this repo can verify that link (orchestrator/validate_manifest.py itself notes the real CWFT YAML lives in mctl-gitops, which isn't checked out here). run_implementer.py's find_accepted_proposals(service_filter=...) confirms the filter logic is correct if it receives a service_filter, and an absent one is exactly today's unscoped "pick up anything accepted" behavior — so if the CWFT param isn't wired yet, this fix silently regresses to the pre-fix behavior with no error and no test able to catch it from this repo.

Given this exact scoping gap was the P1 flagged by Codex's automated review last round, worth explicitly confirming the mctl-gitops companion change has landed (or at minimum tracking it) before this is exercised for real — the PR's own "Live" test-plan item is still unchecked.

[Fix this →](https://claude.ai/code?q=In%20orchestrator%2Ftemporal%2Fworkflows%2Fdev_loop.py%20line%20186%2C%20implement_params%3D%7B%22service%22%3A%20target_repo%7D%20is%20passed%20to%20the%20mctl-agents-implement%20CWFT%20to%20scope%20it%20to%20this%20issue%27s%20repo.%20This%20depends%20on%20the%20sibling%20mctl-gitops%20repo%27s%20cwft-mctl-agents-implement.yaml%20already%20declaring%20a%20service%20parameter%20wired%20to%20run_implementer.py%20--service%2C%20which%20cannot%20be%20verified%20from%20this%20repo%20alone.%20Confirm%20with%20the%20mctl-gitops%20companion%20PR%20that%20this%20wiring%20exists%2C%20or%20add%20a%20tracking%20note%2Ftest%20fixture%20clarifying%20the%20dependency%2C%20on%20branch%20agents%2Ftemporal-dev-loop-worker.&repo=mctlhq/mctl-agents

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has P1/P2 findings, changes requested: 0 P1, 2 P2, 0 P3. All 4 P1/P2s from the prior two rounds are well fixed in ecda152 (resumable Argo polling via heartbeat_details distinguishing pre/post-submission retries, implement-step scoped to the issue's own repo via the service param, unpinned releases no longer recorded as used, target_repo added to the audit trail). Two new P2s found on this round: (1) activities/argo.py — the resumability fix narrows but doesn't fully close the duplicate-submission window: a crash/malformed-response exception between the Argo POST succeeding and activity.heartbeat() completing still causes a resubmit on retry now that maximum_attempts=3 (previously =1 meant no retry, so no duplicate risk existed at all). (2) workflows/dev_loop.py — the service-param scoping fix's correctness depends on an unverified assumption about the sibling mctl-gitops repo's CWFT already wiring service through to run_implementer.py --service; if that companion change hasn't landed, the fix silently no-ops with no test able to catch it from this repo.

…ndency (PR #105 review round 3)

Addresses claude[bot]'s 2 new P2s on ecda152:

- submit_and_wait now wraps the workflow-name parse in try/except: if
  mctl-api returns 2xx (the Argo run genuinely exists) but the body
  can't be parsed for a name, a sentinel is heartbeated so a retry
  detects "already submitted, name unknown" and fails loudly instead
  of resubmitting (which would duplicate the real SDK run). Narrows
  the remaining unprotected window down to a crash strictly between
  the POST succeeding and that first heartbeat landing — no I/O in
  between, so vanishingly unlikely, and documented as such rather than
  overstated as fully closed.

- The implement-step `service` scoping depends on mctl-gitops's
  cwft-mctl-agents-implement.yaml already wiring a `service` parameter
  through to run_implementer.py --service. Verified directly against
  that file (not assumed) and documented with the exact evidence
  inline, since mctl-agents' CI has no way to check this cross-repo
  dependency itself.

Also mirrors StartDevLoopWorkflow's new WorkflowIDReusePolicy/
WorkflowIDConflictPolicy (mctl-api PR #129) in cli.py's start(), so a
repeated start against the same issue is a true no-op on both sides
instead of the SDK's ALLOW_DUPLICATE/UNSPECIFIED defaults.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@claude

claude Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Claude finished @mashkovd's task in 2m 27s —— View job


Reviewing PR #105 (new commit 9575ae3)

  • Gather context (new commit — "fix: close remaining duplicate-submit gap, verify gitops scoping dependency")
  • Check whether the fix commit addresses prior findings
  • Review the diff for new issues
  • Post inline comments for findings
  • Submit final review verdict

Summary

Both P2s from the prior round (ecda152) are well resolved in 9575ae3:

  • activities/argo.py — the remaining duplicate-submission window is closed for the identified gap: a submit response that returns 2xx but whose body fails to parse into a workflow name now heartbeats a sentinel (_SUBMITTED_UNKNOWN_NAME) before raising, and a retry that sees that sentinel refuses to resubmit and fails loudly instead — with direct test coverage for both the sentinel-heartbeat path and the refuse-to-resubmit path. The one gap left (a crash strictly between the Argo POST succeeding and the heartbeat call landing at the Temporal server — no I/O happens in between) is now explicitly documented as an accepted, vanishingly-small residual risk rather than left as an overstated guarantee. Closing it fully would need an idempotency key on mctl-api's /execute endpoint, out of scope here.
  • workflows/dev_loop.py — the service-param scoping dependency on the sibling mctl-gitops CWFT is now documented as directly verified (quoting the actual YAML wiring) rather than assumed, with an explicit note that an unannounced future change to that CWFT would silently regress scoping with no error surfaced on this side. Not a code-level fix — no test here can catch drift in a sibling repo — but a reasonable resolution for a cross-repo dependency this repo can't check out.

The cli.py change (REJECT_DUPLICATE + USE_EXISTING workflow-ID policies, mirroring mctl-api's planned Go client) is a reasonable idempotency hardening, not tied to a prior finding.

CI ("PR validation": ruff/mypy/pytest) passed on 9575ae3.

New findings this round: 0 P1, 0 P2, 1 P3

  • P3 (activities/argo.py:52): pre-existing stale comment — still says "this activity runs with maximum_attempts=1", but that was bumped to 3 two rounds ago (ecda152) and the module docstring was updated then; this one spot wasn't. Doesn't affect correctness, just misleading for a future reader.

Submitted as approve — no P1/P2 findings, and this round's fixes correctly narrow the two prior P2s down to a documented, effectively-unavoidable residual race and an honestly-flagged cross-repo doc dependency.

· Branch

# as an empty phase right after submission.
TERMINAL_PHASES = frozenset({"Succeeded", "Failed", "Error"})
# How many consecutive poll failures (mctl-api 5xx, network blip) to ride
# out before giving up. Because this activity runs with maximum_attempts=1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — pre-existing, not introduced by this commit, but worth a quick fix while touching this file: this comment still says "this activity runs with maximum_attempts=1", but SDK_STEP_RETRY_POLICY in workflows/dev_loop.py was bumped to maximum_attempts=3 back in ecda152 (the resumable-polling fix, two rounds ago) and the module docstring above was updated accordingly — this one spot wasn't. Doesn't affect correctness (MAX_CONSECUTIVE_POLL_ERRORS's reasoning holds either way), just stale/misleading for a future reader trying to reconstruct the actual retry-attempt count from this file alone.

Fix this →

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No P1/P2 findings (1 P3, pre-existing stale comment). Good to merge.

Both P2s from the prior round are well resolved in 9575ae3:

  • activities/argo.py — the remaining duplicate-submission window is closed for the identified gap: a submit response that returns 2xx but fails to parse a workflow name now heartbeats a sentinel (_SUBMITTED_UNKNOWN_NAME) before raising, and a retry that sees that sentinel refuses to resubmit and fails loudly instead, with direct test coverage for both the sentinel-heartbeat and refuse-to-resubmit paths. The one gap left (a crash strictly between the Argo POST succeeding and the heartbeat call landing at the Temporal server, no I/O in between) is now explicitly documented as an accepted, vanishingly-small residual risk rather than an overstated guarantee — reasonable given closing it fully would require an idempotency key on mctl-api's /execute endpoint, which is out of scope for this repo.
  • workflows/dev_loop.py — the service-param scoping dependency on the sibling mctl-gitops CWFT is now documented as directly verified (with the actual YAML snippet referenced) rather than assumed, with an explicit note that a future unannounced change to that CWFT would silently regress scoping with no error on this side. Not a code-level fix (no test can catch drift from this repo), but a reasonable resolution for a cross-repo dependency this repo can't check out.

The cli.py change (REJECT_DUPLICATE + USE_EXISTING id policies, mirroring mctl-api's planned Go client) is a reasonable idempotency hardening, not directly tied to a prior finding.

CI (PR validation: ruff/mypy/pytest) passed on 9575ae3.

@mashkovd
mashkovd merged commit 7b4479b into main Aug 5, 2026
4 checks passed
@mctl-app mctl-app Bot mentioned this pull request Aug 5, 2026
@mashkovd
mashkovd deleted the agents/temporal-dev-loop-worker branch August 8, 2026 11:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant