Skip to content

feat(gstack2): unified execution-result contract#9

Closed
time-attack wants to merge 1 commit into
codex/gstack-2from
time-attack/Add-unified-execution-result-2
Closed

feat(gstack2): unified execution-result contract#9
time-attack wants to merge 1 commit into
codex/gstack-2from
time-attack/Add-unified-execution-result-2

Conversation

@time-attack

@time-attack time-attack commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Unified execution-result contract

Adds a shared GStack 2 execution-result contract so empty, malformed, degraded, unsupported, or failed tool output can no longer be presented as success. Integrated at the smallest high-leverage boundary (the optional runtime's external-effect path) without touching the six-skill public surface or any specialist judgment.

What's in it

  • runtime/execution-result.js — a frozen JSON Schema (draft 2020-12), four statuses (success / degraded / unsupported / failed), stable error codes (EXECUTION_EMPTY, EXECUTION_MALFORMED, EXECUTION_DEGRADED, EXECUTION_UNSUPPORTED, EXECUTION_FAILED), a validator, a builder, a dual JSON/human renderer, and an assertSuccessfulExecution guard. Core rule: success requires non-empty evidence.
  • runtime/cli.jsgstack state effect now validates before rendering. An exit-zero command that produces no output becomes EXECUTION_EMPTY (uncertain, not success); output over the 1 MB capture limit becomes EXECUTION_DEGRADED; success emits a validated envelope carrying exitCode / executable / stdout / stderr in data.
  • Generated sources — schema emitted to references/support/execution-result-contract.json in all six skill packages, plus a documented paragraph in each RUNTIME.md (regenerated via bun run gen:gstack2, freshness gate passes).
  • Tests — focused contract suite, an effect-CLI regression ("empty exit-zero is uncertain, never success"), and a skill-UX assertion.

Compatibility

gstack state effect now emits a clean execution-result envelope instead of its previous ad hoc {status, effectKey, idempotencyKey, result} wrapper. No skill reference or test consumes the dropped top-level keys; the equivalent data lives in the envelope's data field. Child stdout/stderr are captured into the result rather than streamed live. Other runtime commands are unchanged.

Verification

  • Full GStack 2 suite: 224 pass, 0 fail (bun test test/gstack2-*.test.ts).
  • Generated freshness gate: fresh (7 path roots).
  • git diff --check: clean.

Remaining rollout

Additional optional-runtime tool boundaries can migrate to the shared validator incrementally; this PR covers the central external-effect path.

🤖 Generated with Claude Code


Summary by cubic

Adds a unified execution-result contract for GStack 2 and wires it into gstack state effect so empty, malformed, degraded, or unsupported tool output can’t be reported as success. Standardizes JSON and human renderings and captures stdout/stderr for verifiable evidence.

  • New Features

    • Added runtime/execution-result.js with schema v1, statuses (success | degraded | unsupported | failed), stable error codes, validator, builder, renderer, and assertSuccessfulExecution.
    • gstack state effect now validates and emits the envelope via renderExecutionResult(..., { json: true }).
    • Captures child stdout/stderr up to 1 MB; exit-zero with no output => EXECUTION_EMPTY; oversized capture => EXECUTION_DEGRADED.
    • Generated references/support/execution-result-contract.json and updated RUNTIME.md across all six skills.
    • Added tests for the contract, CLI regression, and skill UX.
  • Migration

    • gstack state effect now outputs the validated envelope (with data.exitCode, data.executable, data.stdout, data.stderr) instead of the old {status, effectKey, idempotencyKey, result} wrapper.
    • Stdout/stderr are captured in the result rather than streamed live.
    • Other runtime commands are unchanged.

Written for commit 2938cef. Summary will update on new commits.

Review in cubic

@cursor

cursor Bot commented Jul 21, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 21, 2026

Copy link
Copy Markdown

We've triggered an ultrareview automatically — This PR introduces a new execution-result contract and validation logic for external commands, changing how tool outputs are captured and reported — a subtle bug here could silently present incomplete or failed commands as success, breaking the effect flow across all six skill packages.. I'll post findings when complete.

An ultrareview is cubic's deepest review, catching hard-to-find bugs in the most critical PRs. It runs a longer, multi-pass analysis using cubic's most capable review models, and typically takes around 30 minutes. It consumes your team's reviewed-lines allowance at 3× the standard rate.

Automated ultrareviews are disabled by default. We triggered this run as part of your trial. Want cubic to do this for every high-risk PR? Enable auto-ultrareview in your settings.

@cubic-dev-ai cubic-dev-ai 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.

Ultrareview completed in 11m 39s

7 issues found across 19 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/gstack2/generate-skill-tree.ts">

<violation number="1" location="scripts/gstack2/generate-skill-tree.ts:518">
P2: Browser setup can be incorrectly blocked after a successful `runtime-bootstrap ... options` or `preview` call: this rule says every optional-runtime result must validate as an execution-result envelope, but those commands return their existing `{ ok, action, ... }` payloads and therefore fail that validator as malformed. The implementation currently applies the envelope at the `gstack state effect` boundary, so scoping this instruction to that command (until the other boundaries migrate) keeps the generated contract consistent with actual runtime output.</violation>
</file>

<file name="runtime/cli.js">

<violation number="1" location="runtime/cli.js:445">
P2: External command output can be corrupted when a UTF-8 character spans two pipe chunks. Decoding each chunk separately turns a valid character such as `é` into replacement characters before it is saved in `data.stdout`/`data.stderr`, so consumers no longer receive the command's actual evidence. Buffer the raw bytes and decode once after the stream ends, or use a `StringDecoder` per stream and flush it on close.</violation>

<violation number="2" location="runtime/cli.js:468">
P2: Empty and oversized results are classified internally but that classification is lost before callers see it: `runExternalEffect` rewrites the rejection to `EXTERNAL_EFFECT_UNCERTAIN`. This prevents an automation client from consuming the advertised `EXECUTION_EMPTY`/`EXECUTION_DEGRADED` outcome or its stable code. Preserve and render a validated non-success execution result (while keeping the effect state uncertain) when propagating these boundary failures.</violation>
</file>

<file name="runtime/execution-result.js">

<violation number="1" location="runtime/execution-result.js:19">
P3: The exported schema is still mutable below its root despite being presented as frozen. A consumer can rewrite nested constraints such as the success `evidence.minItems` rule, which makes the in-memory contract diverge from the intended immutable shared definition. Recursively freezing the schema tree would make the exported contract actually immutable.</violation>

<violation number="2" location="runtime/execution-result.js:30">
P3: Schema-only consumers can classify whitespace-only evidence as an evidenced success, while the runtime validator rejects it as malformed. `minLength` and `minItems` do not require any non-whitespace character, so the shared schema no longer matches the validator's `trim()` rule. Adding a non-whitespace pattern to both textual fields keeps externally validated envelopes consistent with runtime behavior.</violation>

<violation number="3" location="runtime/execution-result.js:58">
P2: JSON rendering can emit a success envelope that violates the published required `data` field. The validator accepts `data: undefined` (or a function) and returns it here, but `JSON.stringify` subsequently omits that field; a `BigInt` instead makes rendering throw. Validating that `data` serializes to a defined JSON value and returning `EXECUTION_MALFORMED` otherwise would keep accepted results renderable and schema-conformant.</violation>

<violation number="4" location="runtime/execution-result.js:64">
P3: Malformed builder input loses the stable error-code contract: `executionResult(null)` and `executionResult(undefined)` throw an uncoded native `TypeError` at this dereference instead of `EXECUTION_MALFORMED`. Safely reading the builder fields (or guarding that `input` is an object) lets the existing validator report the malformed input consistently.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic


The package/runtime compatibility tuple is \`schemaVersion=1\`, \`runtimeVersion=2.0.0\`, and \`skillApi=2.0\`; the machine-readable copy is \`references/support/runtime-contract.json\`. An incompatible active runtime is unavailable, not permission to upgrade it.

Every optional-runtime tool result must satisfy \`references/support/execution-result-contract.json\` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code.

@cubic-dev-ai cubic-dev-ai Bot Jul 21, 2026

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: Browser setup can be incorrectly blocked after a successful runtime-bootstrap ... options or preview call: this rule says every optional-runtime result must validate as an execution-result envelope, but those commands return their existing { ok, action, ... } payloads and therefore fail that validator as malformed. The implementation currently applies the envelope at the gstack state effect boundary, so scoping this instruction to that command (until the other boundaries migrate) keeps the generated contract consistent with actual runtime output.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/gstack2/generate-skill-tree.ts, line 518:

<comment>Browser setup can be incorrectly blocked after a successful `runtime-bootstrap ... options` or `preview` call: this rule says every optional-runtime result must validate as an execution-result envelope, but those commands return their existing `{ ok, action, ... }` payloads and therefore fail that validator as malformed. The implementation currently applies the envelope at the `gstack state effect` boundary, so scoping this instruction to that command (until the other boundaries migrate) keeps the generated contract consistent with actual runtime output.</comment>

<file context>
@@ -514,6 +515,8 @@ Some retained helpers are shell scripts. \`gstack doctor\` verifies Bash and, on
 
 The package/runtime compatibility tuple is \`schemaVersion=1\`, \`runtimeVersion=2.0.0\`, and \`skillApi=2.0\`; the machine-readable copy is \`references/support/runtime-contract.json\`. An incompatible active runtime is unavailable, not permission to upgrade it.
 
+Every optional-runtime tool result must satisfy \`references/support/execution-result-contract.json\` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code.
+
 The developer-only fallback is \`node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> [matching browser flags] --yes\`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment.
</file context>
Suggested change
Every optional-runtime tool result must satisfy \`references/support/execution-result-contract.json\` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code.
Results returned by \`gstack state effect\` must satisfy \`references/support/execution-result-contract.json\` before they are presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code.
Fix with cubic

Comment thread runtime/cli.js
const remaining = maxCapturedBytes - captured.bytes;
if (remaining <= 0) { captured.truncated = true; return; }
const buffer = Buffer.from(chunk);
captured[key] += buffer.subarray(0, remaining).toString();

@cubic-dev-ai cubic-dev-ai Bot Jul 21, 2026

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: External command output can be corrupted when a UTF-8 character spans two pipe chunks. Decoding each chunk separately turns a valid character such as é into replacement characters before it is saved in data.stdout/data.stderr, so consumers no longer receive the command's actual evidence. Buffer the raw bytes and decode once after the stream ends, or use a StringDecoder per stream and flush it on close.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At runtime/cli.js, line 445:

<comment>External command output can be corrupted when a UTF-8 character spans two pipe chunks. Decoding each chunk separately turns a valid character such as `é` into replacement characters before it is saved in `data.stdout`/`data.stderr`, so consumers no longer receive the command's actual evidence. Buffer the raw bytes and decode once after the stream ends, or use a `StringDecoder` per stream and flush it on close.</comment>

<file context>
@@ -431,12 +437,49 @@ async function runExternalCommand(command, { cwd, env, stdout, stderr }) {
+      const remaining = maxCapturedBytes - captured.bytes;
+      if (remaining <= 0) { captured.truncated = true; return; }
+      const buffer = Buffer.from(chunk);
+      captured[key] += buffer.subarray(0, remaining).toString();
+      captured.bytes += Math.min(buffer.length, remaining);
+      if (buffer.length > remaining) captured.truncated = true;
</file context>
Fix with cubic

Comment thread runtime/cli.js
if (evidence.length === 0) {
reject(cliError(
`External command ${name} exited 0 but produced no output; verify its side effect before reconciling it as applied.`,
EXECUTION_RESULT_ERROR_CODES.EMPTY,

@cubic-dev-ai cubic-dev-ai Bot Jul 21, 2026

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: Empty and oversized results are classified internally but that classification is lost before callers see it: runExternalEffect rewrites the rejection to EXTERNAL_EFFECT_UNCERTAIN. This prevents an automation client from consuming the advertised EXECUTION_EMPTY/EXECUTION_DEGRADED outcome or its stable code. Preserve and render a validated non-success execution result (while keeping the effect state uncertain) when propagating these boundary failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At runtime/cli.js, line 468:

<comment>Empty and oversized results are classified internally but that classification is lost before callers see it: `runExternalEffect` rewrites the rejection to `EXTERNAL_EFFECT_UNCERTAIN`. This prevents an automation client from consuming the advertised `EXECUTION_EMPTY`/`EXECUTION_DEGRADED` outcome or its stable code. Preserve and render a validated non-success execution result (while keeping the effect state uncertain) when propagating these boundary failures.</comment>

<file context>
@@ -431,12 +437,49 @@ async function runExternalCommand(command, { cwd, env, stdout, stderr }) {
+        if (evidence.length === 0) {
+          reject(cliError(
+            `External command ${name} exited 0 but produced no output; verify its side effect before reconciling it as applied.`,
+            EXECUTION_RESULT_ERROR_CODES.EMPTY,
+          ));
+          return;
</file context>
Fix with cubic

} else if (typeof value.code !== "string" || !/^[A-Z][A-Z0-9_]{2,63}$/.test(value.code)) {
return invalid("Non-success execution requires a stable error code");
}
return Object.freeze({ ...value, evidence: Object.freeze([...value.evidence]) });

@cubic-dev-ai cubic-dev-ai Bot Jul 21, 2026

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: JSON rendering can emit a success envelope that violates the published required data field. The validator accepts data: undefined (or a function) and returns it here, but JSON.stringify subsequently omits that field; a BigInt instead makes rendering throw. Validating that data serializes to a defined JSON value and returning EXECUTION_MALFORMED otherwise would keep accepted results renderable and schema-conformant.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At runtime/execution-result.js, line 58:

<comment>JSON rendering can emit a success envelope that violates the published required `data` field. The validator accepts `data: undefined` (or a function) and returns it here, but `JSON.stringify` subsequently omits that field; a `BigInt` instead makes rendering throw. Validating that `data` serializes to a defined JSON value and returning `EXECUTION_MALFORMED` otherwise would keep accepted results renderable and schema-conformant.</comment>

<file context>
@@ -0,0 +1,89 @@
+  } else if (typeof value.code !== "string" || !/^[A-Z][A-Z0-9_]{2,63}$/.test(value.code)) {
+    return invalid("Non-success execution requires a stable error code");
+  }
+  return Object.freeze({ ...value, evidence: Object.freeze([...value.evidence]) });
+}
+
</file context>
Fix with cubic

FAILED: "EXECUTION_FAILED",
});

export const EXECUTION_RESULT_SCHEMA = Object.freeze({

@cubic-dev-ai cubic-dev-ai Bot Jul 21, 2026

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: The exported schema is still mutable below its root despite being presented as frozen. A consumer can rewrite nested constraints such as the success evidence.minItems rule, which makes the in-memory contract diverge from the intended immutable shared definition. Recursively freezing the schema tree would make the exported contract actually immutable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At runtime/execution-result.js, line 19:

<comment>The exported schema is still mutable below its root despite being presented as frozen. A consumer can rewrite nested constraints such as the success `evidence.minItems` rule, which makes the in-memory contract diverge from the intended immutable shared definition. Recursively freezing the schema tree would make the exported contract actually immutable.</comment>

<file context>
@@ -0,0 +1,89 @@
+  FAILED: "EXECUTION_FAILED",
+});
+
+export const EXECUTION_RESULT_SCHEMA = Object.freeze({
+  $schema: "https://json-schema.org/draft/2020-12/schema",
+  $id: "https://gstack.dev/schemas/execution-result-v1.json",
</file context>
Fix with cubic

schemaVersion: { const: EXECUTION_RESULT_SCHEMA_VERSION },
status: { enum: EXECUTION_RESULT_STATUSES },
code: { type: ["string", "null"], pattern: "^[A-Z][A-Z0-9_]{2,63}$" },
summary: { type: "string", minLength: 1 },

@cubic-dev-ai cubic-dev-ai Bot Jul 21, 2026

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: Schema-only consumers can classify whitespace-only evidence as an evidenced success, while the runtime validator rejects it as malformed. minLength and minItems do not require any non-whitespace character, so the shared schema no longer matches the validator's trim() rule. Adding a non-whitespace pattern to both textual fields keeps externally validated envelopes consistent with runtime behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At runtime/execution-result.js, line 30:

<comment>Schema-only consumers can classify whitespace-only evidence as an evidenced success, while the runtime validator rejects it as malformed. `minLength` and `minItems` do not require any non-whitespace character, so the shared schema no longer matches the validator's `trim()` rule. Adding a non-whitespace pattern to both textual fields keeps externally validated envelopes consistent with runtime behavior.</comment>

<file context>
@@ -0,0 +1,89 @@
+    schemaVersion: { const: EXECUTION_RESULT_SCHEMA_VERSION },
+    status: { enum: EXECUTION_RESULT_STATUSES },
+    code: { type: ["string", "null"], pattern: "^[A-Z][A-Z0-9_]{2,63}$" },
+    summary: { type: "string", minLength: 1 },
+    evidence: { type: "array", items: { type: "string", minLength: 1 } },
+    data: {},
</file context>
Fix with cubic

Comment on lines +64 to +68
status: input.status,
code: input.code ?? null,
summary: input.summary,
evidence: input.evidence ?? [],
data: input.data ?? null,

@cubic-dev-ai cubic-dev-ai Bot Jul 21, 2026

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: Malformed builder input loses the stable error-code contract: executionResult(null) and executionResult(undefined) throw an uncoded native TypeError at this dereference instead of EXECUTION_MALFORMED. Safely reading the builder fields (or guarding that input is an object) lets the existing validator report the malformed input consistently.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At runtime/execution-result.js, line 64:

<comment>Malformed builder input loses the stable error-code contract: `executionResult(null)` and `executionResult(undefined)` throw an uncoded native `TypeError` at this dereference instead of `EXECUTION_MALFORMED`. Safely reading the builder fields (or guarding that `input` is an object) lets the existing validator report the malformed input consistently.</comment>

<file context>
@@ -0,0 +1,89 @@
+export function executionResult(input) {
+  return validateExecutionResult({
+    schemaVersion: EXECUTION_RESULT_SCHEMA_VERSION,
+    status: input.status,
+    code: input.code ?? null,
+    summary: input.summary,
</file context>
Suggested change
status: input.status,
code: input.code ?? null,
summary: input.summary,
evidence: input.evidence ?? [],
data: input.data ?? null,
status: input?.status,
code: input?.code ?? null,
summary: input?.summary,
evidence: input?.evidence ?? [],
data: input?.data ?? null,
Fix with cubic

@time-attack

Copy link
Copy Markdown
Owner Author

Superseded by #10, which consolidates all GStack 2 runtime work (unified execution-result contract, execution profiles, capability readiness, workflow + runtime hardening) into one branch. This feature is folded in there.

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