Skip to content
Closed
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
51 changes: 47 additions & 4 deletions runtime/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ import { installManagedRuntime, uninstallManagedRuntime } from "./install.js";
import { assertManagedHome, withRuntimeLifecycleLock } from "./managed-home.js";
import { errorWithCode as cliError } from "./errors.js";
import { RUNTIME_VERSION } from "./index.js";
import {
EXECUTION_RESULT_ERROR_CODES,
executionResult,
renderExecutionResult,
} from "./execution-result.js";

export async function main(argv = process.argv.slice(2), options = {}) {
const env = options.env ?? process.env;
Expand Down Expand Up @@ -391,7 +396,7 @@ async function stateCommand({ args, home, cwd, env, stdout, stderr }) {
"EXTERNAL_EFFECT_UNCERTAIN",
);
}
write(stdout, `${JSON.stringify({ status: result.status, effectKey, idempotencyKey: result.idempotencyKey ?? null, result: result.result })}\n`);
write(stdout, renderExecutionResult(result.result, { json: true }));
return 0;
}
if (action === "reconcile-not-applied") {
Expand Down Expand Up @@ -423,6 +428,7 @@ async function stateCommand({ args, home, cwd, env, stdout, stderr }) {
}

async function runExternalCommand(command, { cwd, env, stdout, stderr }) {
const maxCapturedBytes = 1024 * 1024;
const [executable, ...args] = command;
return new Promise((resolve, reject) => {
const child = spawn(executable, args, {
Expand All @@ -431,12 +437,49 @@ async function runExternalCommand(command, { cwd, env, stdout, stderr }) {
shell: false,
stdio: ["inherit", "pipe", "pipe"],
});
child.stdout?.on("data", (chunk) => write(stdout, chunk));
child.stderr?.on("data", (chunk) => write(stderr, chunk));
const captured = { stdout: "", stderr: "", bytes: 0, truncated: false };
const capture = (key, chunk) => {
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

captured.bytes += Math.min(buffer.length, remaining);
if (buffer.length > remaining) captured.truncated = true;
};
child.stdout?.on("data", (chunk) => capture("stdout", chunk));
child.stderr?.on("data", (chunk) => capture("stderr", chunk));
child.once("error", reject);
child.once("close", (code, signal) => {
if (code === 0) {
resolve({ exitCode: 0, executable: path.basename(executable) });
if (captured.truncated) {
reject(cliError(
`External command ${path.basename(executable)} exceeded the ${maxCapturedBytes}-byte result limit; verify its side effect before reconciling it as applied.`,
EXECUTION_RESULT_ERROR_CODES.DEGRADED,
));
return;
}
const evidence = [];
if (captured.stdout.trim()) evidence.push("non-empty stdout");
if (captured.stderr.trim()) evidence.push("non-empty stderr");
const name = path.basename(executable);
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

));
return;
}
resolve(executionResult({
status: "success",
summary: `External command ${name} completed`,
evidence: [`exit code 0`, ...evidence],
data: {
exitCode: 0,
executable: name,
stdout: captured.stdout,
stderr: captured.stderr,
},
}));
return;
}
const error = cliError(
Expand Down
89 changes: 89 additions & 0 deletions runtime/execution-result.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { errorWithCode } from "./errors.js";

export const EXECUTION_RESULT_SCHEMA_VERSION = 1;
export const EXECUTION_RESULT_STATUSES = Object.freeze([
"success",
"degraded",
"unsupported",
"failed",
]);

export const EXECUTION_RESULT_ERROR_CODES = Object.freeze({
EMPTY: "EXECUTION_EMPTY",
MALFORMED: "EXECUTION_MALFORMED",
DEGRADED: "EXECUTION_DEGRADED",
UNSUPPORTED: "EXECUTION_UNSUPPORTED",
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

$schema: "https://json-schema.org/draft/2020-12/schema",
$id: "https://gstack.dev/schemas/execution-result-v1.json",
title: "GStack execution result",
type: "object",
additionalProperties: false,
required: ["schemaVersion", "status", "code", "summary", "evidence", "data"],
properties: {
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

evidence: { type: "array", items: { type: "string", minLength: 1 } },
data: {},
},
allOf: [
{ if: { properties: { status: { const: "success" } } }, then: { properties: { code: { type: "null" }, evidence: { minItems: 1 } } } },
{ if: { properties: { status: { enum: ["degraded", "unsupported", "failed"] } } }, then: { properties: { code: { type: "string" } } } },
],
});

/** Validate the host-neutral result before any caller can render it as success. */
export function validateExecutionResult(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return invalid("Execution result must be an object");
const keys = Object.keys(value).sort();
const expected = ["code", "data", "evidence", "schemaVersion", "status", "summary"];
if (JSON.stringify(keys) !== JSON.stringify(expected)) return invalid("Execution result contains missing or unknown fields");
if (value.schemaVersion !== EXECUTION_RESULT_SCHEMA_VERSION) return invalid("Execution result schema version is unsupported");
if (!EXECUTION_RESULT_STATUSES.includes(value.status)) return invalid("Execution result status is unsupported");
if (typeof value.summary !== "string" || value.summary.trim().length === 0) return invalid("Execution result summary is empty");
if (!Array.isArray(value.evidence) || value.evidence.some((item) => typeof item !== "string" || item.trim().length === 0)) {
return invalid("Execution result evidence is malformed");
}
if (value.status === "success") {
if (value.code !== null) return invalid("Successful execution must not carry an error code");
if (value.evidence.length === 0) return invalid("Successful execution requires evidence", EXECUTION_RESULT_ERROR_CODES.EMPTY);
} 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

}

export function executionResult(input) {
return validateExecutionResult({
schemaVersion: EXECUTION_RESULT_SCHEMA_VERSION,
status: input.status,
code: input.code ?? null,
summary: input.summary,
evidence: input.evidence ?? [],
data: input.data ?? null,
Comment on lines +64 to +68

@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

});
}

export function renderExecutionResult(result, options = {}) {
const valid = validateExecutionResult(result);
if (options.json) return `${JSON.stringify(valid, null, 2)}\n`;
const label = valid.status.toUpperCase();
const code = valid.code ? ` [${valid.code}]` : "";
const evidence = valid.evidence.map((item) => `- ${item}`).join("\n");
return `${label}${code}: ${valid.summary}${evidence ? `\nEvidence:\n${evidence}` : ""}\n`;
}

export function assertSuccessfulExecution(result) {
const valid = validateExecutionResult(result);
if (valid.status === "success") return valid;
throw errorWithCode(valid.summary, valid.code);
}

function invalid(message, code = EXECUTION_RESULT_ERROR_CODES.MALFORMED) {
throw errorWithCode(message, code);
}
1 change: 1 addition & 0 deletions runtime/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export * from "./doctor.js";
export * from "./cleanup.js";
export * from "./upgrade.js";
export * from "./install.js";
export * from "./execution-result.js";
4 changes: 4 additions & 0 deletions scripts/gstack2/generate-skill-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { BUG_FIX_OVERLAYS, overlaysForSource } from './bug-fix-overlays';
import { renderBrowserProviderContract } from './browser-provider-contract';
import { EXECUTION_RESULT_SCHEMA } from '../../runtime/execution-result.js';
import { contractFor, DISPATCHERS, SOURCE_ASSIGNMENTS } from './assignments';
import { SCENARIOS } from './scenarios';
import { runDeterministicSemanticParity } from './semantic-parity';
Expand Down Expand Up @@ -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.

@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


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.
`;
}
Expand Down Expand Up @@ -576,6 +579,7 @@ function writeSharedContracts(): void {
write(path.join(ROOT, 'skills', tree, 'references', 'support', 'browser-choice.mjs'), browserChoice);
write(path.join(ROOT, 'skills', tree, 'references', 'support', 'browser-provider-smoke.mjs'), browserSmoke);
writeJson(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-contract.json'), RUNTIME_SKILL_CONTRACT);
writeJson(path.join(ROOT, 'skills', tree, 'references', 'support', 'execution-result-contract.json'), EXECUTION_RESULT_SCHEMA);
}
write(path.join(ROOT, 'skills', 'qa', 'references', 'SYSTEM-FUNCTIONAL.md'), systemFunctionalContract());
write(path.join(ROOT, 'skills', 'ship', 'references', 'EXTERNAL-EFFECTS.md'), externalEffectsContract());
Expand Down
2 changes: 2 additions & 0 deletions skills/debug/references/RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ Some retained helpers are shell scripts. `gstack doctor` verifies Bash and, on W

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.
88 changes: 88 additions & 0 deletions skills/debug/references/support/execution-result-contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://gstack.dev/schemas/execution-result-v1.json",
"title": "GStack execution result",
"type": "object",
"additionalProperties": false,
"required": [
"schemaVersion",
"status",
"code",
"summary",
"evidence",
"data"
],
"properties": {
"schemaVersion": {
"const": 1
},
"status": {
"enum": [
"success",
"degraded",
"unsupported",
"failed"
]
},
"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": {}
},
"allOf": [
{
"if": {
"properties": {
"status": {
"const": "success"
}
}
},
"then": {
"properties": {
"code": {
"type": "null"
},
"evidence": {
"minItems": 1
}
}
}
},
{
"if": {
"properties": {
"status": {
"enum": [
"degraded",
"unsupported",
"failed"
]
}
}
},
"then": {
"properties": {
"code": {
"type": "string"
}
}
}
}
]
}
2 changes: 2 additions & 0 deletions skills/design/references/RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ Some retained helpers are shell scripts. `gstack doctor` verifies Bash and, on W

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.
88 changes: 88 additions & 0 deletions skills/design/references/support/execution-result-contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://gstack.dev/schemas/execution-result-v1.json",
"title": "GStack execution result",
"type": "object",
"additionalProperties": false,
"required": [
"schemaVersion",
"status",
"code",
"summary",
"evidence",
"data"
],
"properties": {
"schemaVersion": {
"const": 1
},
"status": {
"enum": [
"success",
"degraded",
"unsupported",
"failed"
]
},
"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": {}
},
"allOf": [
{
"if": {
"properties": {
"status": {
"const": "success"
}
}
},
"then": {
"properties": {
"code": {
"type": "null"
},
"evidence": {
"minItems": 1
}
}
}
},
{
"if": {
"properties": {
"status": {
"enum": [
"degraded",
"unsupported",
"failed"
]
}
}
},
"then": {
"properties": {
"code": {
"type": "string"
}
}
}
}
]
}
Loading
Loading