Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions internal/api/handlers_write_devloop_params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api_test
import (
"net/http"
"sort"
"strings"
"testing"

"github.com/mctlhq/mctl-api/internal/auth"
Expand Down Expand Up @@ -60,6 +61,20 @@ var devLoopSubmissions = []struct {
"execution_request_id": "xr_0000",
},
},
{
// dev_loop.py human-input continuation (mctlhq/mctl-agents#473): the
// pinned, correlated investigate step plus the accepted answers.
name: "investigate, human-input continuation",
operation: "mctl-agents-investigate",
params: map[string]string{
"issue_url": "https://github.com/mctlhq/mctl-api/issues/372",
"agent_image": pinImageByTag,
"agent_version": "issue-investigator@1.54.0",
"temporal_workflow_id": "dev-loop-mctlhq-mctl-agents-494",
"temporal_run_id": "00000000-0000-0000-0000-000000000000",
"human_input_responses": `[{"request_id":"hir-0123456789abcdef","request_hash":"sha256:` + strings.Repeat("a", 64) + `","value":"library B","received_at":"2026-10-04T10:00:00Z"}]`,
},
},
{
// dev_loop.py investigate step, a published version with a digest.
name: "investigate, pinned by digest",
Expand Down Expand Up @@ -218,9 +233,9 @@ var devLoopParamsPendingDeclaration = []struct {
param string
value string
}{
// dev_loop.py human-input continuation; not declared by
// cwft-mctl-agents-investigate (mctlhq/mctl-api#372 item 3).
{"mctl-agents-investigate", "human_input_responses", `[{"request_id":"hir-0000000000000000"}]`},
// human_input_responses (mctl-api#372 item 3) moved to devLoopSubmissions
// above once cwft-mctl-agents-investigate declared it
// (mctlhq/mctl-gitops#1581).
// Loop identity (mctlhq/mctl-agents#461, #451) moved to devLoopSubmissions
// above: temporal_workflow_id, temporal_run_id and execution_request_id
// are now declared on all three DevLoop operations
Expand Down Expand Up @@ -321,3 +336,30 @@ func TestExecuteOperation_UncorrelatedCallerSendsNoCorrelationParams(t *testing.
})
}
}

// human_input_responses is opaque to mctl-api, but its shape is pinned: only
// a JSON array reaches the investigator, and an empty value is an absence,
// so a run without answers keeps the CWFT's exact argv (mctlhq/mctl-gitops#1581).
func TestExecuteOperation_HumanInputResponsesShape(t *testing.T) {
const issue = "https://github.com/mctlhq/mctl-api/issues/372"
t.Run("non-array rejected", func(t *testing.T) {
router, exec := newTestRouter(t)
w := postAs(t, router, "/api/v1/operations/mctl-agents-investigate/execute", map[string]string{
"issue_url": issue, "human_input_responses": `{"request_id":"hir-0123456789abcdef"}`,
}, auth.NewServiceUser())
assertStatus(t, w, http.StatusBadRequest)
if len(exec.submittedParams) != 0 {
t.Fatal("a rejected submission reached the executor")
}
})
t.Run("empty omitted", func(t *testing.T) {
router, exec := newTestRouter(t)
w := postAs(t, router, "/api/v1/operations/mctl-agents-investigate/execute", map[string]string{
"issue_url": issue, "human_input_responses": "",
}, auth.NewServiceUser())
assertStatus(t, w, http.StatusAccepted)
if _, ok := exec.submittedParams[len(exec.submittedParams)-1]["human_input_responses"]; ok {
t.Error("an empty human_input_responses reached the executor; it must be omitted")
}
})
}
13 changes: 10 additions & 3 deletions internal/operations/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -806,9 +806,16 @@ var builtinOperations = []Operation{
// DevLoop correlation identifiers (mctlhq/mctl-api#426,
// mctlhq/.github#50): dev_loop.py's investigate step sends these;
// cwft-mctl-agents-investigate declares all three.
// human_input_responses is deliberately NOT declared here: the CWFT
// does not declare it (mctl-api#372 item 3), so it stays in
// devLoopParamsPendingDeclaration.
// Human-input continuation (mctl-api#372 item 3, mctlhq/mctl-agents#473):
// dev_loop.py sends the accepted answers as a JSON array once a
// human answered a sealed question. cwft-mctl-agents-investigate
// declares it since mctlhq/mctl-gitops#1581. Opaque here: the
// investigator validates every entry (ids, hashes, bounded values)
// before any clone or model call; this only pins the array shape.
// OmitWhenEmpty so a run without answers keeps its exact argv.
{Name: "human_input_responses", Type: "string", Required: false, OmitWhenEmpty: true,
Description: "Optional. JSON array of accepted human-input answers for a DevLoop continuation (mctl-agents#473). Set only by the DevLoop; omit otherwise.",
Pattern: `(?s)^\[.*\]$`},
Comment thread
mashkovd marked this conversation as resolved.
}, append(agentPinParams("issue-investigator"),
devLoopCorrelationParams("temporal_workflow_id", "temporal_run_id", "execution_request_id")...)...),
},
Expand Down
1 change: 1 addition & 0 deletions internal/operations/registry_cwft_params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var cwftDeclaredParams = map[string][]string{
"issue_url", "agent_image", "agent_version",
"work_item_id", "execution_id",
"temporal_workflow_id", "temporal_run_id", "execution_request_id",
"human_input_responses",
},
"mctl-agents-implement": {
"service", "slug", "force", "max_proposals", "agent_image", "agent_version",
Expand Down
Loading