-
Notifications
You must be signed in to change notification settings - Fork 644
fix(sdd): fail closed on empty task results #2186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Alan-TheGentleman
merged 13 commits into
Gentleman-Programming:main
from
dnlrsls:fix/2117-sdd-empty-task-result
Aug 6, 2026
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
5131b4f
fix(sdd): fail closed on empty task results
dnlrsls 5a8e793
fix(sdd): address empty-result review follow-up
dnlrsls 14f32d5
chore: merge current main into PR 2186
dnlrsls eaa991c
fix(bench): bound Node capability probe
dnlrsls fda241e
chore(bench): keep PR within review budget
dnlrsls 377e22c
test(sdd): fix unrelated-agent expectation
dnlrsls 8e15a20
chore: merge current main into sdd task result fix
dnlrsls 76e2236
test(sdd): refresh Kilocode settings baseline
dnlrsls 23c934e
chore: keep SDD result fix within review budget
dnlrsls b4c5151
chore: merge origin/main into sdd task result fix
Alan-TheGentleman 2a10b1b
fix(sdd): emit terminal result handoff
Alan-TheGentleman 28c4a26
chore: merge origin/main into sdd task result fix
Alan-TheGentleman 2647795
Merge remote-tracking branch 'origin/main' into takeover/2186-sdd-empβ¦
Alan-TheGentleman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "strings" | ||
| "time" | ||
| ) | ||
|
|
||
| const sddTaskResultAxis = "sdd-task-result" | ||
|
|
||
| func init() { | ||
| RegisterAxis(Axis{ | ||
| Name: sddTaskResultAxis, | ||
| Title: "OpenCode SDD task-result transport failures", | ||
| BlackBox: false, | ||
| Properties: []string{ | ||
| "Runs the installed OpenCode plugin through Node with a provider-shaped empty task_result; it does not drive the gentle-ai CLI alone.", | ||
| "Requires GENTLE_AI_BENCH_SDD_PLUGIN and a Node runtime that executes .mts files with built-in TypeScript type stripping; skips honestly when the plugin or runtime capability is unavailable.", | ||
| }, | ||
| Journeys: sddTaskResultJourneys, | ||
| }) | ||
| } | ||
|
|
||
| func sddTaskResultJourneys() []Journey { | ||
| return []Journey{{ | ||
| ID: "tr01-sdd-empty-task-result", | ||
| Title: "Empty SDD task result: typed terminal failure without artifact mutation or downstream launch", | ||
| Source: "issue #2117 provider transport report", | ||
| Steps: []Step{{ | ||
| Name: "provider-shaped empty task result reaches the installed OpenCode plugin", | ||
| Skip: sddTaskResultUnavailable, | ||
| Composite: sddEmptyTaskResult, | ||
| }}, | ||
| }} | ||
| } | ||
|
|
||
| func sddTaskResultUnavailable(*Sandbox) string { | ||
| path := os.Getenv("GENTLE_AI_BENCH_SDD_PLUGIN") | ||
| if path == "" { | ||
| return "GENTLE_AI_BENCH_SDD_PLUGIN is not set" | ||
| } | ||
| if info, err := os.Stat(path); err != nil || !info.Mode().IsRegular() { | ||
| return "GENTLE_AI_BENCH_SDD_PLUGIN does not name an installed plugin" | ||
| } | ||
| node, err := exec.LookPath("node") | ||
| if err != nil { | ||
| return "node is unavailable" | ||
| } | ||
| return sddTaskResultNodeUnavailable(node) | ||
| } | ||
|
|
||
| func sddTaskResultNodeUnavailable(node string) string { | ||
| dir, err := os.MkdirTemp("", "gentle-ai-sdd-task-result-node-*") | ||
| if err != nil { | ||
| return "node TypeScript capability check could not create a temporary directory" | ||
| } | ||
| defer os.RemoveAll(dir) | ||
| capability := filepath.Join(dir, "capability.mts") | ||
| if err := os.WriteFile(capability, []byte(`const marker: string = "gentle-ai-sdd-task-result"`), 0o600); err != nil { | ||
| return "node TypeScript capability check could not write a temporary .mts file" | ||
| } | ||
| ctx, cancel := context.WithTimeout(context.Background(), sddTaskResultHarnessTimeout) | ||
| defer cancel() | ||
| output, err := exec.CommandContext(ctx, node, capability).CombinedOutput() | ||
| if err == nil { | ||
| return "" | ||
| } | ||
| detail := strings.TrimSpace(string(output)) | ||
| if detail == "" && ctx.Err() != nil { | ||
| detail = ctx.Err().Error() | ||
| } | ||
| if detail == "" { | ||
| detail = err.Error() | ||
| } | ||
| return fmt.Sprintf("node cannot execute .mts TypeScript with type stripping required by the installed plugin/harness: %s", detail) | ||
| } | ||
|
|
||
| const sddTaskResultHarness = `import { readFile, writeFile } from "node:fs/promises" | ||
| const source = process.argv[2] | ||
| const cwd = process.argv[3] | ||
| await writeFile("./plugin.mts", await readFile(source)) | ||
| const { default: plugin } = await import("./plugin.mts") | ||
| const hooks = await plugin({ directory: cwd, worktree: cwd }) | ||
| const artifact = cwd + "/proposal.md" | ||
| await writeFile(artifact, "existing artifact") | ||
| const input = { tool: "task", sessionID: "bench-sdd", callID: "empty", args: { subagent_type: "sdd-propose", prompt: "create a proposal" } } | ||
| let failure = "NO_ERROR" | ||
| try { | ||
| await hooks["tool.execute.after"](input, { title: "", output: "<task id=\"phase\" state=\"completed\">\n<task_result>\n\n</task_result>\n</task>", metadata: {} }) | ||
| } catch (cause) { | ||
| failure = cause instanceof Error ? cause.message : String(cause) | ||
| } | ||
| let downstream = "NO_ERROR" | ||
| try { | ||
| await hooks["tool.execute.before"]({ ...input, callID: "downstream", args: { subagent_type: "sdd-apply", prompt: "continue" } }, { args: { subagent_type: "sdd-apply", prompt: "continue" } }) | ||
| } catch (cause) { | ||
| downstream = cause instanceof Error ? cause.message : String(cause) | ||
| } | ||
| console.log([failure, downstream, await readFile(artifact, "utf8")].join("\n---\n")) | ||
| ` | ||
|
|
||
| const sddTaskResultHarnessTimeout = 30 * time.Second | ||
|
|
||
| func sddEmptyTaskResult(r *journeyRun) error { | ||
| root := filepath.Join(r.sandbox.Root, "sdd-task-result") | ||
| work := filepath.Join(root, "work") | ||
| if err := os.MkdirAll(work, 0o755); err != nil { | ||
| return err | ||
| } | ||
| if err := os.WriteFile(filepath.Join(root, "harness.mts"), []byte(sddTaskResultHarness), 0o600); err != nil { | ||
| return err | ||
| } | ||
| ctx, cancel := context.WithTimeout(context.Background(), sddTaskResultHarnessTimeout) | ||
| defer cancel() | ||
| cmd := exec.CommandContext(ctx, "node", "harness.mts", os.Getenv("GENTLE_AI_BENCH_SDD_PLUGIN"), work) | ||
| cmd.Dir = root | ||
| cmd.Env = r.sandbox.env() | ||
| var output bytes.Buffer | ||
| cmd.Stdout, cmd.Stderr = &output, &output | ||
| err := cmd.Run() | ||
| observation := Observation{Args: []string{"opencode", "task", "sdd-propose"}, ExitCode: 0, Stdout: output.String(), StdoutCaptured: true, StderrCaptured: true} | ||
| if err != nil { | ||
| observation.ExitCode = 1 | ||
| observation.Stderr = output.String() | ||
| } | ||
| record := r.accumulator.observe(r.step, observation, nil, true) | ||
| r.accumulator.records = append(r.accumulator.records, record) | ||
| if err != nil { | ||
| return fmt.Errorf("run OpenCode task-result harness: %w: %s", err, output.String()) | ||
| } | ||
| parts := strings.Split(strings.TrimSpace(output.String()), "\n---\n") | ||
| if len(parts) != 3 { | ||
| return fmt.Errorf("task-result harness output = %q", output.String()) | ||
| } | ||
| if !strings.Contains(parts[0], "sdd_task_result_empty") || !strings.Contains(parts[1], "sdd_task_result_empty") { | ||
| return fmt.Errorf("empty result was not routed as one typed terminal failure: %q", parts[:2]) | ||
| } | ||
| if parts[2] != "existing artifact" { | ||
| return fmt.Errorf("task-result failure mutated artifact state: %q", parts[2]) | ||
| } | ||
| return nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.