mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 04:10:47 +02:00
feat(gstack2): add unified execution result contract
This commit is contained in:
+47
-4
@@ -39,6 +39,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;
|
||||
@@ -313,7 +318,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") {
|
||||
@@ -345,6 +350,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, {
|
||||
@@ -353,12 +359,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();
|
||||
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,
|
||||
));
|
||||
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(
|
||||
|
||||
@@ -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({
|
||||
$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 },
|
||||
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]) });
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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,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';
|
||||
@@ -512,6 +513,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> --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.
|
||||
`;
|
||||
}
|
||||
@@ -572,6 +575,7 @@ function writeSharedContracts(): void {
|
||||
write(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-bootstrap.mjs'), bootstrap);
|
||||
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());
|
||||
|
||||
@@ -21,4 +21,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> --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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -21,4 +21,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> --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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -21,4 +21,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> --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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -21,4 +21,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> --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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -21,4 +21,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> --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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -21,4 +21,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> --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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -95,4 +95,23 @@ describe('gstack state external-effect CLI', () => {
|
||||
{ cwd, env, stdout: out, stderr: err },
|
||||
)).toBe(2);
|
||||
});
|
||||
|
||||
test('an empty exit-zero tool result is uncertain, never successful', async () => {
|
||||
const { cwd, env } = await fixture();
|
||||
const out = sink();
|
||||
const err = sink();
|
||||
await main(['state', 'begin', 'ship', '--run-id', 'run_empty'], { cwd, env, stdout: out, stderr: err });
|
||||
const silent = path.join(cwd, 'silent.sh');
|
||||
await fs.writeFile(silent, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
|
||||
const code = await main([
|
||||
'state', 'effect', 'run_empty', 'silent.tool', '--', silent,
|
||||
], { cwd, env, stdout: out, stderr: err });
|
||||
expect(code).toBe(1);
|
||||
expect(err.value()).toContain('may have occurred');
|
||||
expect(out.value()).not.toContain('"status":"success"');
|
||||
expect(await main([
|
||||
'state', 'effect', 'run_empty', 'silent.tool', '--', silent,
|
||||
], { cwd, env, stdout: out, stderr: err })).toBe(1);
|
||||
expect(err.value()).toContain('was already claimed');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
EXECUTION_RESULT_ERROR_CODES,
|
||||
executionResult,
|
||||
renderExecutionResult,
|
||||
validateExecutionResult,
|
||||
} from "../runtime/execution-result.js";
|
||||
|
||||
describe("GStack execution-result contract", () => {
|
||||
test("accepts evidenced success and preserves it in both renderings", () => {
|
||||
const result = executionResult({ status: "success", summary: "Probe completed", evidence: ["exit code 0"], data: { count: 1 } });
|
||||
expect(JSON.parse(renderExecutionResult(result, { json: true }))).toEqual(result);
|
||||
expect(renderExecutionResult(result)).toContain("SUCCESS: Probe completed");
|
||||
});
|
||||
|
||||
test("rejects empty and malformed values instead of inferring success", () => {
|
||||
expect(() => validateExecutionResult(null)).toThrow();
|
||||
expect(() => executionResult({ status: "success", summary: "Done", evidence: [], data: null }))
|
||||
.toThrow("requires evidence");
|
||||
try {
|
||||
executionResult({ status: "success", summary: "Done", evidence: [], data: null });
|
||||
} catch (error: any) {
|
||||
expect(error.code).toBe(EXECUTION_RESULT_ERROR_CODES.EMPTY);
|
||||
}
|
||||
});
|
||||
|
||||
test.each([
|
||||
["degraded", "EXECUTION_DEGRADED"],
|
||||
["unsupported", "EXECUTION_UNSUPPORTED"],
|
||||
["failed", "EXECUTION_FAILED"],
|
||||
])("keeps %s explicitly non-success", (status, code) => {
|
||||
const result = executionResult({ status, code, summary: `${status} result`, evidence: [], data: null });
|
||||
expect(renderExecutionResult(result)).toStartWith(`${status.toUpperCase()} [${code}]`);
|
||||
});
|
||||
});
|
||||
@@ -62,8 +62,11 @@ describe('GStack 2 canonical skill UX', () => {
|
||||
const runtime = fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'RUNTIME.md'), 'utf8');
|
||||
const bootstrap = fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-bootstrap.mjs'));
|
||||
const contract = JSON.parse(fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-contract.json'), 'utf8'));
|
||||
const resultContract = JSON.parse(fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'support', 'execution-result-contract.json'), 'utf8'));
|
||||
expect(bootstrap, tree).toEqual(source);
|
||||
expect(contract, tree).toEqual({ schemaVersion: 1, runtimeVersion: '2.0.0', skillApi: '2.0' });
|
||||
expect(resultContract.properties.status.enum, tree).toEqual(['success', 'degraded', 'unsupported', 'failed']);
|
||||
expect(resultContract.allOf[0].then.properties.evidence.minItems, tree).toBe(1);
|
||||
expect(runtime, tree).toContain('preview --capability <name>');
|
||||
expect(runtime, tree).toContain('It never downloads components or mutates runtime state.');
|
||||
expect(runtime, tree).toContain('install --capability <name> --yes');
|
||||
|
||||
Reference in New Issue
Block a user