From 9b5ae4071d3309e1e4322985c9d23cd47bbfd064 Mon Sep 17 00:00:00 2001 From: Sinabina Date: Mon, 20 Jul 2026 16:22:24 -0700 Subject: [PATCH 1/7] feat(gstack2): add unified execution result contract --- runtime/cli.js | 51 ++++++++++- runtime/execution-result.js | 89 +++++++++++++++++++ runtime/index.js | 1 + scripts/gstack2/generate-skill-tree.ts | 4 + skills/debug/references/RUNTIME.md | 2 + .../support/execution-result-contract.json | 88 ++++++++++++++++++ skills/design/references/RUNTIME.md | 2 + .../support/execution-result-contract.json | 88 ++++++++++++++++++ skills/plan/references/RUNTIME.md | 2 + .../support/execution-result-contract.json | 88 ++++++++++++++++++ skills/qa/references/RUNTIME.md | 2 + .../support/execution-result-contract.json | 88 ++++++++++++++++++ skills/review/references/RUNTIME.md | 2 + .../support/execution-result-contract.json | 88 ++++++++++++++++++ skills/ship/references/RUNTIME.md | 2 + .../support/execution-result-contract.json | 88 ++++++++++++++++++ test/gstack2-runtime-effect-cli.test.ts | 19 ++++ test/gstack2-runtime-execution-result.test.ts | 35 ++++++++ test/gstack2-skill-ux.test.ts | 3 + 19 files changed, 738 insertions(+), 4 deletions(-) create mode 100644 runtime/execution-result.js create mode 100644 skills/debug/references/support/execution-result-contract.json create mode 100644 skills/design/references/support/execution-result-contract.json create mode 100644 skills/plan/references/support/execution-result-contract.json create mode 100644 skills/qa/references/support/execution-result-contract.json create mode 100644 skills/review/references/support/execution-result-contract.json create mode 100644 skills/ship/references/support/execution-result-contract.json create mode 100644 test/gstack2-runtime-execution-result.test.ts diff --git a/runtime/cli.js b/runtime/cli.js index 620b8803a2..31cd2d904c 100644 --- a/runtime/cli.js +++ b/runtime/cli.js @@ -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( diff --git a/runtime/execution-result.js b/runtime/execution-result.js new file mode 100644 index 0000000000..dd92128809 --- /dev/null +++ b/runtime/execution-result.js @@ -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); +} diff --git a/runtime/index.js b/runtime/index.js index 7dd98939d1..8e5179867f 100644 --- a/runtime/index.js +++ b/runtime/index.js @@ -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"; diff --git a/scripts/gstack2/generate-skill-tree.ts b/scripts/gstack2/generate-skill-tree.ts index 90159925e1..db1f73f200 100644 --- a/scripts/gstack2/generate-skill-tree.ts +++ b/scripts/gstack2/generate-skill-tree.ts @@ -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 --capability --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()); diff --git a/skills/debug/references/RUNTIME.md b/skills/debug/references/RUNTIME.md index 6ce746ba03..43013427d9 100644 --- a/skills/debug/references/RUNTIME.md +++ b/skills/debug/references/RUNTIME.md @@ -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 --capability --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. diff --git a/skills/debug/references/support/execution-result-contract.json b/skills/debug/references/support/execution-result-contract.json new file mode 100644 index 0000000000..2e6a1d0256 --- /dev/null +++ b/skills/debug/references/support/execution-result-contract.json @@ -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" + } + } + } + } + ] +} diff --git a/skills/design/references/RUNTIME.md b/skills/design/references/RUNTIME.md index 6ce746ba03..43013427d9 100644 --- a/skills/design/references/RUNTIME.md +++ b/skills/design/references/RUNTIME.md @@ -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 --capability --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. diff --git a/skills/design/references/support/execution-result-contract.json b/skills/design/references/support/execution-result-contract.json new file mode 100644 index 0000000000..2e6a1d0256 --- /dev/null +++ b/skills/design/references/support/execution-result-contract.json @@ -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" + } + } + } + } + ] +} diff --git a/skills/plan/references/RUNTIME.md b/skills/plan/references/RUNTIME.md index 6ce746ba03..43013427d9 100644 --- a/skills/plan/references/RUNTIME.md +++ b/skills/plan/references/RUNTIME.md @@ -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 --capability --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. diff --git a/skills/plan/references/support/execution-result-contract.json b/skills/plan/references/support/execution-result-contract.json new file mode 100644 index 0000000000..2e6a1d0256 --- /dev/null +++ b/skills/plan/references/support/execution-result-contract.json @@ -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" + } + } + } + } + ] +} diff --git a/skills/qa/references/RUNTIME.md b/skills/qa/references/RUNTIME.md index 6ce746ba03..43013427d9 100644 --- a/skills/qa/references/RUNTIME.md +++ b/skills/qa/references/RUNTIME.md @@ -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 --capability --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. diff --git a/skills/qa/references/support/execution-result-contract.json b/skills/qa/references/support/execution-result-contract.json new file mode 100644 index 0000000000..2e6a1d0256 --- /dev/null +++ b/skills/qa/references/support/execution-result-contract.json @@ -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" + } + } + } + } + ] +} diff --git a/skills/review/references/RUNTIME.md b/skills/review/references/RUNTIME.md index 6ce746ba03..43013427d9 100644 --- a/skills/review/references/RUNTIME.md +++ b/skills/review/references/RUNTIME.md @@ -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 --capability --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. diff --git a/skills/review/references/support/execution-result-contract.json b/skills/review/references/support/execution-result-contract.json new file mode 100644 index 0000000000..2e6a1d0256 --- /dev/null +++ b/skills/review/references/support/execution-result-contract.json @@ -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" + } + } + } + } + ] +} diff --git a/skills/ship/references/RUNTIME.md b/skills/ship/references/RUNTIME.md index 6ce746ba03..43013427d9 100644 --- a/skills/ship/references/RUNTIME.md +++ b/skills/ship/references/RUNTIME.md @@ -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 --capability --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. diff --git a/skills/ship/references/support/execution-result-contract.json b/skills/ship/references/support/execution-result-contract.json new file mode 100644 index 0000000000..2e6a1d0256 --- /dev/null +++ b/skills/ship/references/support/execution-result-contract.json @@ -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" + } + } + } + } + ] +} diff --git a/test/gstack2-runtime-effect-cli.test.ts b/test/gstack2-runtime-effect-cli.test.ts index 53d2457fb9..b1f8f4b3cb 100644 --- a/test/gstack2-runtime-effect-cli.test.ts +++ b/test/gstack2-runtime-effect-cli.test.ts @@ -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'); + }); }); diff --git a/test/gstack2-runtime-execution-result.test.ts b/test/gstack2-runtime-execution-result.test.ts new file mode 100644 index 0000000000..dd36ff7ec3 --- /dev/null +++ b/test/gstack2-runtime-execution-result.test.ts @@ -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}]`); + }); +}); diff --git a/test/gstack2-skill-ux.test.ts b/test/gstack2-skill-ux.test.ts index 8f7424a55f..1a7dcdaf3d 100644 --- a/test/gstack2-skill-ux.test.ts +++ b/test/gstack2-skill-ux.test.ts @@ -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 '); expect(runtime, tree).toContain('It never downloads components or mutates runtime state.'); expect(runtime, tree).toContain('install --capability --yes'); From a1b2b05a1880fe3dd96da29f656f43e66a16faec Mon Sep 17 00:00:00 2001 From: Sinabina Date: Mon, 20 Jul 2026 16:22:24 -0700 Subject: [PATCH 2/7] feat(gstack2): infer execution profiles --- evals/parity/transcripts/policy-units.json | 18 ++--- scripts/gstack2/execution-profiles.ts | 77 +++++++++++++++++++ scripts/gstack2/generate-skill-tree.ts | 6 +- scripts/gstack2/host-adversarial.ts | 6 +- scripts/gstack2/route.ts | 3 +- scripts/gstack2/types.ts | 7 +- skills/debug/SKILL.md | 4 +- skills/debug/references/EXECUTION-PROFILES.md | 28 +++++++ skills/design/SKILL.md | 4 +- .../design/references/EXECUTION-PROFILES.md | 28 +++++++ skills/plan/SKILL.md | 4 +- skills/plan/references/EXECUTION-PROFILES.md | 28 +++++++ skills/qa/SKILL.md | 4 +- skills/qa/references/EXECUTION-PROFILES.md | 28 +++++++ skills/review/SKILL.md | 4 +- .../review/references/EXECUTION-PROFILES.md | 28 +++++++ skills/ship/SKILL.md | 4 +- skills/ship/references/EXECUTION-PROFILES.md | 28 +++++++ test/gstack2-skill-ux.test.ts | 15 ++++ test/gstack2-skills-routing.test.ts | 35 +++++++++ 20 files changed, 329 insertions(+), 30 deletions(-) create mode 100644 scripts/gstack2/execution-profiles.ts create mode 100644 skills/debug/references/EXECUTION-PROFILES.md create mode 100644 skills/design/references/EXECUTION-PROFILES.md create mode 100644 skills/plan/references/EXECUTION-PROFILES.md create mode 100644 skills/qa/references/EXECUTION-PROFILES.md create mode 100644 skills/review/references/EXECUTION-PROFILES.md create mode 100644 skills/ship/references/EXECUTION-PROFILES.md diff --git a/evals/parity/transcripts/policy-units.json b/evals/parity/transcripts/policy-units.json index eec2438fee..6afea0e7c2 100644 --- a/evals/parity/transcripts/policy-units.json +++ b/evals/parity/transcripts/policy-units.json @@ -43,7 +43,7 @@ "prompt_sha256": "a0fcff9cad68f9305da861fa5a58990e1ef51d9d84298fe2df7ba8a2f56b1dba", "semantic_attempt_sha256": "7724c3d954f421753c597364c383bd76bc01ba9803f99fb14488287bb925aeb6" }, - "policy_sha256": "951fd945e7ecb438bcedadccd563df430652d35f4cff1d513dfdcf16ee77d02d", + "policy_sha256": "c7df25a16073d69da15243f258a2ef72a69c03ee3114eac2c19cf1c7e126532b", "policy_present": true, "prompt_is_not_authority_input": true, "verdict": "PASS" @@ -88,7 +88,7 @@ "prompt_sha256": "04d82a21358f188cb823dcceeecaebea0b4ed8b9c1f8d00220ef3b499c48b1c8", "semantic_attempt_sha256": "9639406995955284517acb30f56b37dedc42a6c08142163dfa8fe0295105cbbf" }, - "policy_sha256": "e5fc3019062512de84ade87c8c45b34c8f46b95a65ef84a49fd25bbdee63e41e", + "policy_sha256": "8dd78f6f8e01eb9cf1fe0f3f43d7fcd072a4f3f2f0dc14d9f1b5d3ff1250b972", "policy_present": true, "prompt_is_not_authority_input": true, "verdict": "PASS" @@ -138,7 +138,7 @@ "prompt_sha256": "8ad639f708a2ccd5c7c438b1dedf6afacdfa4eac3883a7a046ea23f29314e1c4", "semantic_attempt_sha256": "4c450039e4c622fbeaa34b7f7dfc810d3b7369aaf7e5f4d240cd6cc18a31331a" }, - "policy_sha256": "78df79ecb49744ff13f044b8f5a486705ef43172c4dc19d7dbc616d84f4d578c", + "policy_sha256": "7c7ccb7ff64ea6c73b668848ae5a663606798abc345d7e5c6bb0880732bb29c3", "policy_present": true, "prompt_is_not_authority_input": true, "verdict": "PASS" @@ -188,7 +188,7 @@ "prompt_sha256": "d2aa9aee06a116bf04cb62772e8abdf8dbd606811b6d38565389c88e86c79ede", "semantic_attempt_sha256": "64885a66bdc6688b880cbb9a59babf314834c068eb040657501bf287bfcb0d2b" }, - "policy_sha256": "951fd945e7ecb438bcedadccd563df430652d35f4cff1d513dfdcf16ee77d02d", + "policy_sha256": "c7df25a16073d69da15243f258a2ef72a69c03ee3114eac2c19cf1c7e126532b", "policy_present": true, "prompt_is_not_authority_input": true, "verdict": "PASS" @@ -232,7 +232,7 @@ "prompt_sha256": "377e3a2dfc7b04272f857d27e77c9aa3c9f36c3feb63c03e5aee3ecd3dea8e2a", "semantic_attempt_sha256": "a49b1528091886749278dc22e7da4556c090800989b2bca9c0e1c09dd880fed7" }, - "policy_sha256": "7a8cb7346851f9d948139dbf33cb8c6dd5eb8b2cc7de94e30a53824a94f027a6", + "policy_sha256": "7969ac784f398b09cec2ad44f623de95f1e326721043272b12aaec1c622e840c", "policy_present": true, "prompt_is_not_authority_input": true, "verdict": "PASS" @@ -279,7 +279,7 @@ "prompt_sha256": "a01bb977de84e53d8ce3dfa427bcc93d73c6e449cd4e9c1ff63b436fd41fb0d1", "semantic_attempt_sha256": "ce5c64c62c3ff9b60949f34717dffbc908f62ce30f8a4a48ec182eba4363a206" }, - "policy_sha256": "a65efa55d992f63d0bdb356e1e8c4900b966364af1e22f0b6412e20e7ef30a58", + "policy_sha256": "4134088a9b22fd7d3d2d6492c812ecbc55650df25ab1101189dd24721fe1763d", "policy_present": true, "prompt_is_not_authority_input": true, "verdict": "PASS" @@ -330,7 +330,7 @@ "prompt_sha256": "95e97e26268ad7e509527e0f54c943ed6c4105d919f250648a2ad59ce77cbdb9", "semantic_attempt_sha256": "dacd11a78e32aeb6c0065496bedd4a9770f7bbac1036e003d3768fac41eb84f0" }, - "policy_sha256": "951fd945e7ecb438bcedadccd563df430652d35f4cff1d513dfdcf16ee77d02d", + "policy_sha256": "c7df25a16073d69da15243f258a2ef72a69c03ee3114eac2c19cf1c7e126532b", "policy_present": true, "prompt_is_not_authority_input": true, "verdict": "PASS" @@ -381,7 +381,7 @@ "prompt_sha256": "bdd7e8adfa7c15cf8531f84c3adaaacc725075f1a78f7487225300750547b82d", "semantic_attempt_sha256": "32c11b63412c5d873ff29dcc87c45bef1b50daa218a5c6c1075864aa24d7c3b6" }, - "policy_sha256": "951fd945e7ecb438bcedadccd563df430652d35f4cff1d513dfdcf16ee77d02d", + "policy_sha256": "c7df25a16073d69da15243f258a2ef72a69c03ee3114eac2c19cf1c7e126532b", "policy_present": true, "prompt_is_not_authority_input": true, "verdict": "PASS" @@ -426,7 +426,7 @@ "prompt_sha256": "51b7d53bc342e8632e34ae31a167cbde568ab5ee2a6dd2ccc980583a30604164", "semantic_attempt_sha256": "06f77b27c9bd360562655b23ca00a0dd021bdb14ebaaf3d836845e4e0cb43d68" }, - "policy_sha256": "65f28f454b67d4904c1857a2a6cfcff387c0aa6e4ae58aec490bc95289dce59f", + "policy_sha256": "43d5cbbf88ed4f9afa3e7af214b4aa8c3a764a7006d76a864344df05d1f76392", "policy_present": true, "prompt_is_not_authority_input": true, "verdict": "PASS" diff --git a/scripts/gstack2/execution-profiles.ts b/scripts/gstack2/execution-profiles.ts new file mode 100644 index 0000000000..b6cff9d69f --- /dev/null +++ b/scripts/gstack2/execution-profiles.ts @@ -0,0 +1,77 @@ +import type { ExecutionProfile } from './types'; + +export interface ExecutionProfileContract { + profile: ExecutionProfile; + inferWhen: string; + mandatoryModules: string; + legalSkips: string; + artifacts: string; + allowedClaims: string; +} + +export const EXECUTION_PROFILE_CONTRACTS: Record = { + readiness: { + profile: 'readiness', + inferWhen: 'A narrow, reversible, pre-deployment or operational readiness decision needs bounded evidence; the requested claim is readiness, not completeness.', + mandatoryModules: 'Every selected specialist module remains mandatory. Run its entry gates and the smallest source-authorized evidence path that can answer the readiness question.', + legalSkips: 'Only source-declared conditional work whose condition is demonstrably false, or evidence unavailable after a named attempt. Never skip a STOP gate, approval boundary, reproduction/root-cause gate, or required physical-device/production evidence.', + artifacts: 'A readiness record naming the exact scope, probes run, evidence and freshness, failures, skipped work with reasons, and the next standard/deep step.', + allowedClaims: 'Only ready/not-ready for the named bounded decision. Must say “Readiness profile — not a complete review.” Never claim comprehensive, fully verified, production-safe, or no issues found outside the inspected scope.', + }, + standard: { + profile: 'standard', + inferWhen: 'Normal feature or change work has bounded scope and risk and needs the selected specialist’s complete default workflow.', + mandatoryModules: 'Every selected specialist module and all of its mandatory phases, gates, artifacts, and exit checks.', + legalSkips: 'Only smart skips explicitly authorized by the specialist and supported by inspected evidence; list each skipped primary module and each skipped conditional phase.', + artifacts: 'Every artifact required by the selected specialist, plus evidence provenance, unresolved decisions, and a skip ledger.', + allowedClaims: 'Complete only for the named specialist scope and evidence layer. Broader product, security, production, or device claims require those modules and evidence.', + }, + deep: { + profile: 'deep', + inferWhen: 'Risk, ambiguity, blast radius, cross-system effects, irreversible mutation, security/reliability needs, or production deployment demands stronger evidence.', + mandatoryModules: 'Every selected specialist module, all mandatory phases and outside-voice/cross-consumer modules selected by the dispatcher, with unchanged STOP and approval gates.', + legalSkips: 'Only specialist-authorized smart skips proven irrelevant. Missing, stale, malformed, or contradictory evidence is a gap or blocker, never a successful skip.', + artifacts: 'All specialist artifacts plus changed-input/unchanged-consumer trace, negative and failure-path evidence, provenance/freshness, rollback or reversibility evidence where applicable, and unresolved-risk ledger.', + allowedClaims: 'Complete only across the explicitly listed modules and evidence layers. “Confirmed” requires independent supporting evidence; production/device/security claims require matching production/device/security evidence.', + }, +}; + +/** Infer from structured operating conditions, never prompt text. */ +export function inferExecutionProfile( + signals: Record, + specialistDefault: ExecutionProfile, +): ExecutionProfile { + const highRisk = signals.risk === 'high' + || signals.blast_radius === 'broad' + || signals.irreversible === true + || signals.audit_focus === 'security' + || signals.audit_focus === 'deep' + || signals.evidence_need === 'independent' + || signals.failure_impact === 'critical' + || signals.mutation_scope === 'consequential' + || signals.external_mutation_authorized === true + || signals.deployment_state === 'production' + || signals.release_stage === 'approved-pr' + || signals.release_stage === 'landed'; + if (highRisk) return 'deep'; + + const boundedReadiness = signals.evidence_need === 'readiness' + && signals.scope === 'narrow' + && signals.irreversible !== true + && signals.mutation_scope !== 'consequential' + && signals.external_mutation_authorized !== true + && signals.deployment_state !== 'production' + && signals.release_stage !== 'approved-pr' + && signals.release_stage !== 'landed'; + if (boundedReadiness) return 'readiness'; + + return specialistDefault; +} + +export function renderExecutionProfiles(): string { + const rows = (['readiness', 'standard', 'deep'] as const).map((name) => { + const contract = EXECUTION_PROFILE_CONTRACTS[name]; + return `## ${name === 'readiness' ? 'Smoke/readiness' : name[0].toUpperCase() + name.slice(1)}\n\n- Infer when: ${contract.inferWhen}\n- Mandatory modules: ${contract.mandatoryModules}\n- Legal skips: ${contract.legalSkips}\n- Artifacts: ${contract.artifacts}\n- Claims: ${contract.allowedClaims}`; + }); + return `# Inferred execution profiles\n\nChoose a profile from product stage, mutation authority, risk/evidence needs, and deployment state. Prompt keywords and a request to “be quick” are not routing evidence. A profile narrows or strengthens evidence; it never overrides a specialist’s binding question order, pressure, gates, mutation boundary, or exit behavior.\n\n${rows.join('\n\n')}\n`; +} diff --git a/scripts/gstack2/generate-skill-tree.ts b/scripts/gstack2/generate-skill-tree.ts index db1f73f200..98642a63fd 100644 --- a/scripts/gstack2/generate-skill-tree.ts +++ b/scripts/gstack2/generate-skill-tree.ts @@ -5,6 +5,7 @@ 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 { renderExecutionProfiles } from './execution-profiles'; import { contractFor, DISPATCHERS, SOURCE_ASSIGNMENTS } from './assignments'; import { SCENARIOS } from './scenarios'; import { runDeterministicSemanticParity } from './semantic-parity'; @@ -221,7 +222,7 @@ Before any substantive output, print these exact labels in this exact order. Res \`\`\`text Target: Mode: -Depth: +Depth: Mutation: Active modules: Skipped modules: @@ -233,7 +234,7 @@ Web context: 1. Infer the mode from product stage, surface, requested artifact, mutation authorization, evidence needs, and deployment state. Do not route by keyword alone. 2. Refine the public mode to the smallest applicable internal specialist set, then print the required execution header before any substantive output. 3. Read each active module in full from the path shown in the mode/alias tables. Its specialist body, behavioral contract, STOP gates, and appended upstream judgment ports are binding. Read a lazy specialist phase in full only when the workflow reaches its package-local reference. -4. Read \`references/SHARED-JUDGMENT.md\` and \`references/AUTHORITY-POLICY.md\` for every invocation. Read \`references/RUNTIME.md\` before capability-dependent work and \`references/WEB-CONTEXT.md\` before public-web work. +4. Read \`references/EXECUTION-PROFILES.md\`, \`references/SHARED-JUDGMENT.md\`, and \`references/AUTHORITY-POLICY.md\` for every invocation. Infer Depth from structured operating conditions, then obey its mandatory modules, legal skips, artifacts, and claim limits. Read \`references/RUNTIME.md\` before capability-dependent work and \`references/WEB-CONTEXT.md\` before public-web work. 5. If an old asset path is unavailable, use \`references/ASSETS.md\`. If legacy prose invokes another retired skill, resolve it through \`references/COMPATIBILITY.md\` and stay inside these six dispatchers. 6. Preserve report-only versus mutation boundaries. Missing mutation authorization fails closed: do not edit merely because a specialist can fix. Commits, pushes, PRs, merges, deploys, messages, and other external mutations still require affirmative authority from the user. 7. Match the user's language. Keep code identifiers, commands, and source quotations original when translation would reduce accuracy. @@ -567,6 +568,7 @@ function writeSharedContracts(): void { const bootstrap = fs.readFileSync(path.join(ROOT, 'runtime', 'runtime-bootstrap.mjs')); const browserSmoke = fs.readFileSync(path.join(ROOT, 'runtime', 'browser-provider-smoke.mjs')); for (const tree of TREE_NAMES) { + write(path.join(ROOT, 'skills', tree, 'references', 'EXECUTION-PROFILES.md'), `${GENERATED}\n${renderExecutionProfiles()}`); write(path.join(ROOT, 'skills', tree, 'references', 'SHARED-JUDGMENT.md'), sharedJudgmentContract()); write(path.join(ROOT, 'skills', tree, 'references', 'AUTHORITY-POLICY.md'), authorityPolicyContract()); write(path.join(ROOT, 'skills', tree, 'references', 'WEB-CONTEXT.md'), webContextContract()); diff --git a/scripts/gstack2/host-adversarial.ts b/scripts/gstack2/host-adversarial.ts index 7d6244c867..35b87019e5 100644 --- a/scripts/gstack2/host-adversarial.ts +++ b/scripts/gstack2/host-adversarial.ts @@ -116,7 +116,7 @@ export interface StructuredHostResult { target: string; skill: PublicSkill; mode: string; - depth: 'quick' | 'standard' | 'deep'; + depth: 'readiness' | 'standard' | 'deep'; mutation: string; active_modules: string[]; skipped_modules: string[]; @@ -236,7 +236,7 @@ export const FINAL_OUTPUT_SCHEMA = { target: { type: 'string' }, skill: { type: 'string', enum: [...PUBLIC_SKILLS] }, mode: { type: 'string' }, - depth: { type: 'string', enum: ['quick', 'standard', 'deep'] }, + depth: { type: 'string', enum: ['readiness', 'standard', 'deep'] }, mutation: { type: 'string' }, active_modules: { type: 'array', items: { type: 'string' } }, skipped_modules: { type: 'array', items: { type: 'string' } }, @@ -625,7 +625,7 @@ export function validateStructuredResult(value: unknown): value is StructuredHos route && typeof route.target === 'string' && PUBLIC_SKILLS.includes(route.skill) && typeof route.mode === 'string' - && ['quick', 'standard', 'deep'].includes(route.depth) + && ['readiness', 'standard', 'deep'].includes(route.depth) && typeof route.mutation === 'string' && isStringArray(route.active_modules) && isStringArray(route.skipped_modules) diff --git a/scripts/gstack2/route.ts b/scripts/gstack2/route.ts index f3cc56e5cd..b7ef322c12 100644 --- a/scripts/gstack2/route.ts +++ b/scripts/gstack2/route.ts @@ -1,6 +1,7 @@ import { DISPATCHERS, SOURCE_ASSIGNMENTS, assignmentBySource } from './assignments'; import type { ScenarioFixture, TreeName } from './types'; import { evaluateAuthorityPolicy, type AdversarialAttempt } from './authority-policy'; +import { inferExecutionProfile } from './execution-profiles'; export interface StructuredRoute { tree: TreeName; @@ -154,7 +155,7 @@ export function routeStructured(signals: Record): StructuredRou return { tree, mode, - depth: specialist.defaultDepth, + depth: inferExecutionProfile(signals, specialist.defaultDepth), mutation, active_modules: active, skipped_modules: primary.filter((candidate) => !active.includes(candidate)), diff --git a/scripts/gstack2/types.ts b/scripts/gstack2/types.ts index 5a2ff0bd7e..a0132a6278 100644 --- a/scripts/gstack2/types.ts +++ b/scripts/gstack2/types.ts @@ -2,6 +2,7 @@ export const GSTACK2_BASE_SHA = 'bb57306d98c97011b0919c6132705a15b1579781'; export const TREE_NAMES = ['plan', 'design', 'qa', 'debug', 'review', 'ship'] as const; export type TreeName = (typeof TREE_NAMES)[number]; +export type ExecutionProfile = 'readiness' | 'standard' | 'deep'; export type ModuleVisibility = 'primary' | 'internal'; @@ -27,7 +28,7 @@ export interface SourceAssignment { mandatory: boolean; replacement: string; summary: string; - defaultDepth: 'quick' | 'standard' | 'deep'; + defaultDepth: ExecutionProfile; defaultMutation: string; webContext: 'none' | 'optional' | 'local-browser' | 'production'; overlays?: number[]; @@ -39,7 +40,7 @@ export interface DispatcherMode { target: string; modules: string[]; inferWhen: string; - depth: 'quick' | 'standard' | 'deep'; + depth: ExecutionProfile; mutation: string; webContext: 'none' | 'optional' | 'local-browser' | 'production'; } @@ -75,7 +76,7 @@ export interface ScenarioFixture { expected: { tree: TreeName; mode: string; - depth: 'quick' | 'standard' | 'deep'; + depth: ExecutionProfile; mutation: string; active_modules: string[]; skipped_modules: string[]; diff --git a/skills/debug/SKILL.md b/skills/debug/SKILL.md index abdbcff642..7c8cf14db6 100644 --- a/skills/debug/SKILL.md +++ b/skills/debug/SKILL.md @@ -15,7 +15,7 @@ Before any substantive output, print these exact labels in this exact order. Res ```text Target: Mode: -Depth: +Depth: Mutation: Active modules: Skipped modules: @@ -27,7 +27,7 @@ Web context: 1. Infer the mode from product stage, surface, requested artifact, mutation authorization, evidence needs, and deployment state. Do not route by keyword alone. 2. Refine the public mode to the smallest applicable internal specialist set, then print the required execution header before any substantive output. 3. Read each active module in full from the path shown in the mode/alias tables. Its specialist body, behavioral contract, STOP gates, and appended upstream judgment ports are binding. Read a lazy specialist phase in full only when the workflow reaches its package-local reference. -4. Read `references/SHARED-JUDGMENT.md` and `references/AUTHORITY-POLICY.md` for every invocation. Read `references/RUNTIME.md` before capability-dependent work and `references/WEB-CONTEXT.md` before public-web work. +4. Read `references/EXECUTION-PROFILES.md`, `references/SHARED-JUDGMENT.md`, and `references/AUTHORITY-POLICY.md` for every invocation. Infer Depth from structured operating conditions, then obey its mandatory modules, legal skips, artifacts, and claim limits. Read `references/RUNTIME.md` before capability-dependent work and `references/WEB-CONTEXT.md` before public-web work. 5. If an old asset path is unavailable, use `references/ASSETS.md`. If legacy prose invokes another retired skill, resolve it through `references/COMPATIBILITY.md` and stay inside these six dispatchers. 6. Preserve report-only versus mutation boundaries. Missing mutation authorization fails closed: do not edit merely because a specialist can fix. Commits, pushes, PRs, merges, deploys, messages, and other external mutations still require affirmative authority from the user. 7. Match the user's language. Keep code identifiers, commands, and source quotations original when translation would reduce accuracy. diff --git a/skills/debug/references/EXECUTION-PROFILES.md b/skills/debug/references/EXECUTION-PROFILES.md new file mode 100644 index 0000000000..758e32c86d --- /dev/null +++ b/skills/debug/references/EXECUTION-PROFILES.md @@ -0,0 +1,28 @@ + +# Inferred execution profiles + +Choose a profile from product stage, mutation authority, risk/evidence needs, and deployment state. Prompt keywords and a request to “be quick” are not routing evidence. A profile narrows or strengthens evidence; it never overrides a specialist’s binding question order, pressure, gates, mutation boundary, or exit behavior. + +## Smoke/readiness + +- Infer when: A narrow, reversible, pre-deployment or operational readiness decision needs bounded evidence; the requested claim is readiness, not completeness. +- Mandatory modules: Every selected specialist module remains mandatory. Run its entry gates and the smallest source-authorized evidence path that can answer the readiness question. +- Legal skips: Only source-declared conditional work whose condition is demonstrably false, or evidence unavailable after a named attempt. Never skip a STOP gate, approval boundary, reproduction/root-cause gate, or required physical-device/production evidence. +- Artifacts: A readiness record naming the exact scope, probes run, evidence and freshness, failures, skipped work with reasons, and the next standard/deep step. +- Claims: Only ready/not-ready for the named bounded decision. Must say “Readiness profile — not a complete review.” Never claim comprehensive, fully verified, production-safe, or no issues found outside the inspected scope. + +## Standard + +- Infer when: Normal feature or change work has bounded scope and risk and needs the selected specialist’s complete default workflow. +- Mandatory modules: Every selected specialist module and all of its mandatory phases, gates, artifacts, and exit checks. +- Legal skips: Only smart skips explicitly authorized by the specialist and supported by inspected evidence; list each skipped primary module and each skipped conditional phase. +- Artifacts: Every artifact required by the selected specialist, plus evidence provenance, unresolved decisions, and a skip ledger. +- Claims: Complete only for the named specialist scope and evidence layer. Broader product, security, production, or device claims require those modules and evidence. + +## Deep + +- Infer when: Risk, ambiguity, blast radius, cross-system effects, irreversible mutation, security/reliability needs, or production deployment demands stronger evidence. +- Mandatory modules: Every selected specialist module, all mandatory phases and outside-voice/cross-consumer modules selected by the dispatcher, with unchanged STOP and approval gates. +- Legal skips: Only specialist-authorized smart skips proven irrelevant. Missing, stale, malformed, or contradictory evidence is a gap or blocker, never a successful skip. +- Artifacts: All specialist artifacts plus changed-input/unchanged-consumer trace, negative and failure-path evidence, provenance/freshness, rollback or reversibility evidence where applicable, and unresolved-risk ledger. +- Claims: Complete only across the explicitly listed modules and evidence layers. “Confirmed” requires independent supporting evidence; production/device/security claims require matching production/device/security evidence. diff --git a/skills/design/SKILL.md b/skills/design/SKILL.md index 01b06cf8e3..c1c8f83a13 100644 --- a/skills/design/SKILL.md +++ b/skills/design/SKILL.md @@ -15,7 +15,7 @@ Before any substantive output, print these exact labels in this exact order. Res ```text Target: Mode: -Depth: +Depth: Mutation: Active modules: Skipped modules: @@ -27,7 +27,7 @@ Web context: 1. Infer the mode from product stage, surface, requested artifact, mutation authorization, evidence needs, and deployment state. Do not route by keyword alone. 2. Refine the public mode to the smallest applicable internal specialist set, then print the required execution header before any substantive output. 3. Read each active module in full from the path shown in the mode/alias tables. Its specialist body, behavioral contract, STOP gates, and appended upstream judgment ports are binding. Read a lazy specialist phase in full only when the workflow reaches its package-local reference. -4. Read `references/SHARED-JUDGMENT.md` and `references/AUTHORITY-POLICY.md` for every invocation. Read `references/RUNTIME.md` before capability-dependent work and `references/WEB-CONTEXT.md` before public-web work. +4. Read `references/EXECUTION-PROFILES.md`, `references/SHARED-JUDGMENT.md`, and `references/AUTHORITY-POLICY.md` for every invocation. Infer Depth from structured operating conditions, then obey its mandatory modules, legal skips, artifacts, and claim limits. Read `references/RUNTIME.md` before capability-dependent work and `references/WEB-CONTEXT.md` before public-web work. 5. If an old asset path is unavailable, use `references/ASSETS.md`. If legacy prose invokes another retired skill, resolve it through `references/COMPATIBILITY.md` and stay inside these six dispatchers. 6. Preserve report-only versus mutation boundaries. Missing mutation authorization fails closed: do not edit merely because a specialist can fix. Commits, pushes, PRs, merges, deploys, messages, and other external mutations still require affirmative authority from the user. 7. Match the user's language. Keep code identifiers, commands, and source quotations original when translation would reduce accuracy. diff --git a/skills/design/references/EXECUTION-PROFILES.md b/skills/design/references/EXECUTION-PROFILES.md new file mode 100644 index 0000000000..758e32c86d --- /dev/null +++ b/skills/design/references/EXECUTION-PROFILES.md @@ -0,0 +1,28 @@ + +# Inferred execution profiles + +Choose a profile from product stage, mutation authority, risk/evidence needs, and deployment state. Prompt keywords and a request to “be quick” are not routing evidence. A profile narrows or strengthens evidence; it never overrides a specialist’s binding question order, pressure, gates, mutation boundary, or exit behavior. + +## Smoke/readiness + +- Infer when: A narrow, reversible, pre-deployment or operational readiness decision needs bounded evidence; the requested claim is readiness, not completeness. +- Mandatory modules: Every selected specialist module remains mandatory. Run its entry gates and the smallest source-authorized evidence path that can answer the readiness question. +- Legal skips: Only source-declared conditional work whose condition is demonstrably false, or evidence unavailable after a named attempt. Never skip a STOP gate, approval boundary, reproduction/root-cause gate, or required physical-device/production evidence. +- Artifacts: A readiness record naming the exact scope, probes run, evidence and freshness, failures, skipped work with reasons, and the next standard/deep step. +- Claims: Only ready/not-ready for the named bounded decision. Must say “Readiness profile — not a complete review.” Never claim comprehensive, fully verified, production-safe, or no issues found outside the inspected scope. + +## Standard + +- Infer when: Normal feature or change work has bounded scope and risk and needs the selected specialist’s complete default workflow. +- Mandatory modules: Every selected specialist module and all of its mandatory phases, gates, artifacts, and exit checks. +- Legal skips: Only smart skips explicitly authorized by the specialist and supported by inspected evidence; list each skipped primary module and each skipped conditional phase. +- Artifacts: Every artifact required by the selected specialist, plus evidence provenance, unresolved decisions, and a skip ledger. +- Claims: Complete only for the named specialist scope and evidence layer. Broader product, security, production, or device claims require those modules and evidence. + +## Deep + +- Infer when: Risk, ambiguity, blast radius, cross-system effects, irreversible mutation, security/reliability needs, or production deployment demands stronger evidence. +- Mandatory modules: Every selected specialist module, all mandatory phases and outside-voice/cross-consumer modules selected by the dispatcher, with unchanged STOP and approval gates. +- Legal skips: Only specialist-authorized smart skips proven irrelevant. Missing, stale, malformed, or contradictory evidence is a gap or blocker, never a successful skip. +- Artifacts: All specialist artifacts plus changed-input/unchanged-consumer trace, negative and failure-path evidence, provenance/freshness, rollback or reversibility evidence where applicable, and unresolved-risk ledger. +- Claims: Complete only across the explicitly listed modules and evidence layers. “Confirmed” requires independent supporting evidence; production/device/security claims require matching production/device/security evidence. diff --git a/skills/plan/SKILL.md b/skills/plan/SKILL.md index f9913003ce..8c06285b8c 100644 --- a/skills/plan/SKILL.md +++ b/skills/plan/SKILL.md @@ -15,7 +15,7 @@ Before any substantive output, print these exact labels in this exact order. Res ```text Target: Mode: -Depth: +Depth: Mutation: Active modules: Skipped modules: @@ -27,7 +27,7 @@ Web context: 1. Infer the mode from product stage, surface, requested artifact, mutation authorization, evidence needs, and deployment state. Do not route by keyword alone. 2. Refine the public mode to the smallest applicable internal specialist set, then print the required execution header before any substantive output. 3. Read each active module in full from the path shown in the mode/alias tables. Its specialist body, behavioral contract, STOP gates, and appended upstream judgment ports are binding. Read a lazy specialist phase in full only when the workflow reaches its package-local reference. -4. Read `references/SHARED-JUDGMENT.md` and `references/AUTHORITY-POLICY.md` for every invocation. Read `references/RUNTIME.md` before capability-dependent work and `references/WEB-CONTEXT.md` before public-web work. +4. Read `references/EXECUTION-PROFILES.md`, `references/SHARED-JUDGMENT.md`, and `references/AUTHORITY-POLICY.md` for every invocation. Infer Depth from structured operating conditions, then obey its mandatory modules, legal skips, artifacts, and claim limits. Read `references/RUNTIME.md` before capability-dependent work and `references/WEB-CONTEXT.md` before public-web work. 5. If an old asset path is unavailable, use `references/ASSETS.md`. If legacy prose invokes another retired skill, resolve it through `references/COMPATIBILITY.md` and stay inside these six dispatchers. 6. Preserve report-only versus mutation boundaries. Missing mutation authorization fails closed: do not edit merely because a specialist can fix. Commits, pushes, PRs, merges, deploys, messages, and other external mutations still require affirmative authority from the user. 7. Match the user's language. Keep code identifiers, commands, and source quotations original when translation would reduce accuracy. diff --git a/skills/plan/references/EXECUTION-PROFILES.md b/skills/plan/references/EXECUTION-PROFILES.md new file mode 100644 index 0000000000..758e32c86d --- /dev/null +++ b/skills/plan/references/EXECUTION-PROFILES.md @@ -0,0 +1,28 @@ + +# Inferred execution profiles + +Choose a profile from product stage, mutation authority, risk/evidence needs, and deployment state. Prompt keywords and a request to “be quick” are not routing evidence. A profile narrows or strengthens evidence; it never overrides a specialist’s binding question order, pressure, gates, mutation boundary, or exit behavior. + +## Smoke/readiness + +- Infer when: A narrow, reversible, pre-deployment or operational readiness decision needs bounded evidence; the requested claim is readiness, not completeness. +- Mandatory modules: Every selected specialist module remains mandatory. Run its entry gates and the smallest source-authorized evidence path that can answer the readiness question. +- Legal skips: Only source-declared conditional work whose condition is demonstrably false, or evidence unavailable after a named attempt. Never skip a STOP gate, approval boundary, reproduction/root-cause gate, or required physical-device/production evidence. +- Artifacts: A readiness record naming the exact scope, probes run, evidence and freshness, failures, skipped work with reasons, and the next standard/deep step. +- Claims: Only ready/not-ready for the named bounded decision. Must say “Readiness profile — not a complete review.” Never claim comprehensive, fully verified, production-safe, or no issues found outside the inspected scope. + +## Standard + +- Infer when: Normal feature or change work has bounded scope and risk and needs the selected specialist’s complete default workflow. +- Mandatory modules: Every selected specialist module and all of its mandatory phases, gates, artifacts, and exit checks. +- Legal skips: Only smart skips explicitly authorized by the specialist and supported by inspected evidence; list each skipped primary module and each skipped conditional phase. +- Artifacts: Every artifact required by the selected specialist, plus evidence provenance, unresolved decisions, and a skip ledger. +- Claims: Complete only for the named specialist scope and evidence layer. Broader product, security, production, or device claims require those modules and evidence. + +## Deep + +- Infer when: Risk, ambiguity, blast radius, cross-system effects, irreversible mutation, security/reliability needs, or production deployment demands stronger evidence. +- Mandatory modules: Every selected specialist module, all mandatory phases and outside-voice/cross-consumer modules selected by the dispatcher, with unchanged STOP and approval gates. +- Legal skips: Only specialist-authorized smart skips proven irrelevant. Missing, stale, malformed, or contradictory evidence is a gap or blocker, never a successful skip. +- Artifacts: All specialist artifacts plus changed-input/unchanged-consumer trace, negative and failure-path evidence, provenance/freshness, rollback or reversibility evidence where applicable, and unresolved-risk ledger. +- Claims: Complete only across the explicitly listed modules and evidence layers. “Confirmed” requires independent supporting evidence; production/device/security claims require matching production/device/security evidence. diff --git a/skills/qa/SKILL.md b/skills/qa/SKILL.md index a3dd07f4a1..a51f977467 100644 --- a/skills/qa/SKILL.md +++ b/skills/qa/SKILL.md @@ -15,7 +15,7 @@ Before any substantive output, print these exact labels in this exact order. Res ```text Target: Mode: -Depth: +Depth: Mutation: Active modules: Skipped modules: @@ -27,7 +27,7 @@ Web context: 1. Infer the mode from product stage, surface, requested artifact, mutation authorization, evidence needs, and deployment state. Do not route by keyword alone. 2. Refine the public mode to the smallest applicable internal specialist set, then print the required execution header before any substantive output. 3. Read each active module in full from the path shown in the mode/alias tables. Its specialist body, behavioral contract, STOP gates, and appended upstream judgment ports are binding. Read a lazy specialist phase in full only when the workflow reaches its package-local reference. -4. Read `references/SHARED-JUDGMENT.md` and `references/AUTHORITY-POLICY.md` for every invocation. Read `references/RUNTIME.md` before capability-dependent work and `references/WEB-CONTEXT.md` before public-web work. +4. Read `references/EXECUTION-PROFILES.md`, `references/SHARED-JUDGMENT.md`, and `references/AUTHORITY-POLICY.md` for every invocation. Infer Depth from structured operating conditions, then obey its mandatory modules, legal skips, artifacts, and claim limits. Read `references/RUNTIME.md` before capability-dependent work and `references/WEB-CONTEXT.md` before public-web work. 5. If an old asset path is unavailable, use `references/ASSETS.md`. If legacy prose invokes another retired skill, resolve it through `references/COMPATIBILITY.md` and stay inside these six dispatchers. 6. Preserve report-only versus mutation boundaries. Missing mutation authorization fails closed: do not edit merely because a specialist can fix. Commits, pushes, PRs, merges, deploys, messages, and other external mutations still require affirmative authority from the user. 7. Match the user's language. Keep code identifiers, commands, and source quotations original when translation would reduce accuracy. diff --git a/skills/qa/references/EXECUTION-PROFILES.md b/skills/qa/references/EXECUTION-PROFILES.md new file mode 100644 index 0000000000..758e32c86d --- /dev/null +++ b/skills/qa/references/EXECUTION-PROFILES.md @@ -0,0 +1,28 @@ + +# Inferred execution profiles + +Choose a profile from product stage, mutation authority, risk/evidence needs, and deployment state. Prompt keywords and a request to “be quick” are not routing evidence. A profile narrows or strengthens evidence; it never overrides a specialist’s binding question order, pressure, gates, mutation boundary, or exit behavior. + +## Smoke/readiness + +- Infer when: A narrow, reversible, pre-deployment or operational readiness decision needs bounded evidence; the requested claim is readiness, not completeness. +- Mandatory modules: Every selected specialist module remains mandatory. Run its entry gates and the smallest source-authorized evidence path that can answer the readiness question. +- Legal skips: Only source-declared conditional work whose condition is demonstrably false, or evidence unavailable after a named attempt. Never skip a STOP gate, approval boundary, reproduction/root-cause gate, or required physical-device/production evidence. +- Artifacts: A readiness record naming the exact scope, probes run, evidence and freshness, failures, skipped work with reasons, and the next standard/deep step. +- Claims: Only ready/not-ready for the named bounded decision. Must say “Readiness profile — not a complete review.” Never claim comprehensive, fully verified, production-safe, or no issues found outside the inspected scope. + +## Standard + +- Infer when: Normal feature or change work has bounded scope and risk and needs the selected specialist’s complete default workflow. +- Mandatory modules: Every selected specialist module and all of its mandatory phases, gates, artifacts, and exit checks. +- Legal skips: Only smart skips explicitly authorized by the specialist and supported by inspected evidence; list each skipped primary module and each skipped conditional phase. +- Artifacts: Every artifact required by the selected specialist, plus evidence provenance, unresolved decisions, and a skip ledger. +- Claims: Complete only for the named specialist scope and evidence layer. Broader product, security, production, or device claims require those modules and evidence. + +## Deep + +- Infer when: Risk, ambiguity, blast radius, cross-system effects, irreversible mutation, security/reliability needs, or production deployment demands stronger evidence. +- Mandatory modules: Every selected specialist module, all mandatory phases and outside-voice/cross-consumer modules selected by the dispatcher, with unchanged STOP and approval gates. +- Legal skips: Only specialist-authorized smart skips proven irrelevant. Missing, stale, malformed, or contradictory evidence is a gap or blocker, never a successful skip. +- Artifacts: All specialist artifacts plus changed-input/unchanged-consumer trace, negative and failure-path evidence, provenance/freshness, rollback or reversibility evidence where applicable, and unresolved-risk ledger. +- Claims: Complete only across the explicitly listed modules and evidence layers. “Confirmed” requires independent supporting evidence; production/device/security claims require matching production/device/security evidence. diff --git a/skills/review/SKILL.md b/skills/review/SKILL.md index fce9c7c3be..1f511d469b 100644 --- a/skills/review/SKILL.md +++ b/skills/review/SKILL.md @@ -15,7 +15,7 @@ Before any substantive output, print these exact labels in this exact order. Res ```text Target: Mode: -Depth: +Depth: Mutation: Active modules: Skipped modules: @@ -27,7 +27,7 @@ Web context: 1. Infer the mode from product stage, surface, requested artifact, mutation authorization, evidence needs, and deployment state. Do not route by keyword alone. 2. Refine the public mode to the smallest applicable internal specialist set, then print the required execution header before any substantive output. 3. Read each active module in full from the path shown in the mode/alias tables. Its specialist body, behavioral contract, STOP gates, and appended upstream judgment ports are binding. Read a lazy specialist phase in full only when the workflow reaches its package-local reference. -4. Read `references/SHARED-JUDGMENT.md` and `references/AUTHORITY-POLICY.md` for every invocation. Read `references/RUNTIME.md` before capability-dependent work and `references/WEB-CONTEXT.md` before public-web work. +4. Read `references/EXECUTION-PROFILES.md`, `references/SHARED-JUDGMENT.md`, and `references/AUTHORITY-POLICY.md` for every invocation. Infer Depth from structured operating conditions, then obey its mandatory modules, legal skips, artifacts, and claim limits. Read `references/RUNTIME.md` before capability-dependent work and `references/WEB-CONTEXT.md` before public-web work. 5. If an old asset path is unavailable, use `references/ASSETS.md`. If legacy prose invokes another retired skill, resolve it through `references/COMPATIBILITY.md` and stay inside these six dispatchers. 6. Preserve report-only versus mutation boundaries. Missing mutation authorization fails closed: do not edit merely because a specialist can fix. Commits, pushes, PRs, merges, deploys, messages, and other external mutations still require affirmative authority from the user. 7. Match the user's language. Keep code identifiers, commands, and source quotations original when translation would reduce accuracy. diff --git a/skills/review/references/EXECUTION-PROFILES.md b/skills/review/references/EXECUTION-PROFILES.md new file mode 100644 index 0000000000..758e32c86d --- /dev/null +++ b/skills/review/references/EXECUTION-PROFILES.md @@ -0,0 +1,28 @@ + +# Inferred execution profiles + +Choose a profile from product stage, mutation authority, risk/evidence needs, and deployment state. Prompt keywords and a request to “be quick” are not routing evidence. A profile narrows or strengthens evidence; it never overrides a specialist’s binding question order, pressure, gates, mutation boundary, or exit behavior. + +## Smoke/readiness + +- Infer when: A narrow, reversible, pre-deployment or operational readiness decision needs bounded evidence; the requested claim is readiness, not completeness. +- Mandatory modules: Every selected specialist module remains mandatory. Run its entry gates and the smallest source-authorized evidence path that can answer the readiness question. +- Legal skips: Only source-declared conditional work whose condition is demonstrably false, or evidence unavailable after a named attempt. Never skip a STOP gate, approval boundary, reproduction/root-cause gate, or required physical-device/production evidence. +- Artifacts: A readiness record naming the exact scope, probes run, evidence and freshness, failures, skipped work with reasons, and the next standard/deep step. +- Claims: Only ready/not-ready for the named bounded decision. Must say “Readiness profile — not a complete review.” Never claim comprehensive, fully verified, production-safe, or no issues found outside the inspected scope. + +## Standard + +- Infer when: Normal feature or change work has bounded scope and risk and needs the selected specialist’s complete default workflow. +- Mandatory modules: Every selected specialist module and all of its mandatory phases, gates, artifacts, and exit checks. +- Legal skips: Only smart skips explicitly authorized by the specialist and supported by inspected evidence; list each skipped primary module and each skipped conditional phase. +- Artifacts: Every artifact required by the selected specialist, plus evidence provenance, unresolved decisions, and a skip ledger. +- Claims: Complete only for the named specialist scope and evidence layer. Broader product, security, production, or device claims require those modules and evidence. + +## Deep + +- Infer when: Risk, ambiguity, blast radius, cross-system effects, irreversible mutation, security/reliability needs, or production deployment demands stronger evidence. +- Mandatory modules: Every selected specialist module, all mandatory phases and outside-voice/cross-consumer modules selected by the dispatcher, with unchanged STOP and approval gates. +- Legal skips: Only specialist-authorized smart skips proven irrelevant. Missing, stale, malformed, or contradictory evidence is a gap or blocker, never a successful skip. +- Artifacts: All specialist artifacts plus changed-input/unchanged-consumer trace, negative and failure-path evidence, provenance/freshness, rollback or reversibility evidence where applicable, and unresolved-risk ledger. +- Claims: Complete only across the explicitly listed modules and evidence layers. “Confirmed” requires independent supporting evidence; production/device/security claims require matching production/device/security evidence. diff --git a/skills/ship/SKILL.md b/skills/ship/SKILL.md index 152cdc2114..599c120f0d 100644 --- a/skills/ship/SKILL.md +++ b/skills/ship/SKILL.md @@ -15,7 +15,7 @@ Before any substantive output, print these exact labels in this exact order. Res ```text Target: Mode: -Depth: +Depth: Mutation: Active modules: Skipped modules: @@ -27,7 +27,7 @@ Web context: 1. Infer the mode from product stage, surface, requested artifact, mutation authorization, evidence needs, and deployment state. Do not route by keyword alone. 2. Refine the public mode to the smallest applicable internal specialist set, then print the required execution header before any substantive output. 3. Read each active module in full from the path shown in the mode/alias tables. Its specialist body, behavioral contract, STOP gates, and appended upstream judgment ports are binding. Read a lazy specialist phase in full only when the workflow reaches its package-local reference. -4. Read `references/SHARED-JUDGMENT.md` and `references/AUTHORITY-POLICY.md` for every invocation. Read `references/RUNTIME.md` before capability-dependent work and `references/WEB-CONTEXT.md` before public-web work. +4. Read `references/EXECUTION-PROFILES.md`, `references/SHARED-JUDGMENT.md`, and `references/AUTHORITY-POLICY.md` for every invocation. Infer Depth from structured operating conditions, then obey its mandatory modules, legal skips, artifacts, and claim limits. Read `references/RUNTIME.md` before capability-dependent work and `references/WEB-CONTEXT.md` before public-web work. 5. If an old asset path is unavailable, use `references/ASSETS.md`. If legacy prose invokes another retired skill, resolve it through `references/COMPATIBILITY.md` and stay inside these six dispatchers. 6. Preserve report-only versus mutation boundaries. Missing mutation authorization fails closed: do not edit merely because a specialist can fix. Commits, pushes, PRs, merges, deploys, messages, and other external mutations still require affirmative authority from the user. 7. Match the user's language. Keep code identifiers, commands, and source quotations original when translation would reduce accuracy. diff --git a/skills/ship/references/EXECUTION-PROFILES.md b/skills/ship/references/EXECUTION-PROFILES.md new file mode 100644 index 0000000000..758e32c86d --- /dev/null +++ b/skills/ship/references/EXECUTION-PROFILES.md @@ -0,0 +1,28 @@ + +# Inferred execution profiles + +Choose a profile from product stage, mutation authority, risk/evidence needs, and deployment state. Prompt keywords and a request to “be quick” are not routing evidence. A profile narrows or strengthens evidence; it never overrides a specialist’s binding question order, pressure, gates, mutation boundary, or exit behavior. + +## Smoke/readiness + +- Infer when: A narrow, reversible, pre-deployment or operational readiness decision needs bounded evidence; the requested claim is readiness, not completeness. +- Mandatory modules: Every selected specialist module remains mandatory. Run its entry gates and the smallest source-authorized evidence path that can answer the readiness question. +- Legal skips: Only source-declared conditional work whose condition is demonstrably false, or evidence unavailable after a named attempt. Never skip a STOP gate, approval boundary, reproduction/root-cause gate, or required physical-device/production evidence. +- Artifacts: A readiness record naming the exact scope, probes run, evidence and freshness, failures, skipped work with reasons, and the next standard/deep step. +- Claims: Only ready/not-ready for the named bounded decision. Must say “Readiness profile — not a complete review.” Never claim comprehensive, fully verified, production-safe, or no issues found outside the inspected scope. + +## Standard + +- Infer when: Normal feature or change work has bounded scope and risk and needs the selected specialist’s complete default workflow. +- Mandatory modules: Every selected specialist module and all of its mandatory phases, gates, artifacts, and exit checks. +- Legal skips: Only smart skips explicitly authorized by the specialist and supported by inspected evidence; list each skipped primary module and each skipped conditional phase. +- Artifacts: Every artifact required by the selected specialist, plus evidence provenance, unresolved decisions, and a skip ledger. +- Claims: Complete only for the named specialist scope and evidence layer. Broader product, security, production, or device claims require those modules and evidence. + +## Deep + +- Infer when: Risk, ambiguity, blast radius, cross-system effects, irreversible mutation, security/reliability needs, or production deployment demands stronger evidence. +- Mandatory modules: Every selected specialist module, all mandatory phases and outside-voice/cross-consumer modules selected by the dispatcher, with unchanged STOP and approval gates. +- Legal skips: Only specialist-authorized smart skips proven irrelevant. Missing, stale, malformed, or contradictory evidence is a gap or blocker, never a successful skip. +- Artifacts: All specialist artifacts plus changed-input/unchanged-consumer trace, negative and failure-path evidence, provenance/freshness, rollback or reversibility evidence where applicable, and unresolved-risk ledger. +- Claims: Complete only across the explicitly listed modules and evidence layers. “Confirmed” requires independent supporting evidence; production/device/security claims require matching production/device/security evidence. diff --git a/test/gstack2-skill-ux.test.ts b/test/gstack2-skill-ux.test.ts index 1a7dcdaf3d..af647b3437 100644 --- a/test/gstack2-skill-ux.test.ts +++ b/test/gstack2-skill-ux.test.ts @@ -45,6 +45,21 @@ describe('GStack 2 canonical skill UX', () => { } }); + test('packages one binding inferred execution-profile contract in every dispatcher', () => { + for (const tree of TREE_NAMES) { + const dispatcher = fs.readFileSync(path.join(ROOT, 'skills', tree, 'SKILL.md'), 'utf8'); + const profiles = fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'EXECUTION-PROFILES.md'), 'utf8'); + expect(dispatcher, tree).toContain('Depth: '); + expect(dispatcher, tree).toContain('Read `references/EXECUTION-PROFILES.md`'); + expect(profiles, tree).toContain('## Smoke/readiness'); + expect(profiles, tree).toContain('Readiness profile — not a complete review.'); + expect(profiles, tree).toContain('Every selected specialist module remains mandatory.'); + expect(profiles, tree).toContain('## Standard'); + expect(profiles, tree).toContain('## Deep'); + expect(profiles, tree).toContain('never overrides a specialist’s binding question order'); + } + }); + test('resolves retired user-facing recommendations without rewriting package paths', () => { for (const assignment of SOURCE_ASSIGNMENTS) { const body = fs.readFileSync(ownerModule(assignment.source), 'utf8'); diff --git a/test/gstack2-skills-routing.test.ts b/test/gstack2-skills-routing.test.ts index bda2f4ce15..44bb85abb9 100644 --- a/test/gstack2-skills-routing.test.ts +++ b/test/gstack2-skills-routing.test.ts @@ -24,6 +24,41 @@ describe('GStack 2 structured dispatch', () => { expect(routeStructured({ ...scenario.signals })).toEqual(original); }); + test('infers readiness only from bounded structured operating conditions', () => { + const readiness = routeStructured({ + surface: 'web', + implementation_exists: true, + evidence_need: 'readiness', + scope: 'narrow', + deployment_state: 'pre-deployment', + }); + expect(readiness.depth).toBe('readiness'); + expect(readiness.active_modules).toEqual(['qa-only']); + + expect(routeStructured({ + surface: 'web', + implementation_exists: true, + evidence_need: 'readiness', + scope: 'broad', + }).depth).not.toBe('readiness'); + }); + + test('risk and deployment evidence promote work to deep', () => { + expect(routeStructured({ surface: 'web', implementation_exists: true, risk: 'high' }).depth) + .toBe('deep'); + expect(routeStructured({ surface: 'web', implementation_exists: true, deployment_state: 'production' }).depth) + .toBe('deep'); + expect(routeStructured({ surface: 'web', implementation_exists: true, mutation_scope: 'consequential' }).depth) + .toBe('deep'); + expect(routeStructured({ + surface: 'web', + implementation_exists: true, + evidence_need: 'readiness', + scope: 'narrow', + irreversible: true, + }).depth).toBe('deep'); + }); + test('explicit mutation denials override otherwise mutating modes', () => { const review = routeStructured({ audit_focus: 'broad', mutation_authorized: false }); expect(review.mode).toBe('Normal'); From 2e1e52eae7a0553243d7cdaed43429678e75f974 Mon Sep 17 00:00:00 2001 From: Sinabina Date: Mon, 20 Jul 2026 16:22:24 -0700 Subject: [PATCH 3/7] feat(gstack2): add capability readiness process --- README.md | 8 +- docs/gstack-2/CAPABILITY-READINESS.md | 75 ++++++++++++++ runtime/cli.js | 25 +++-- runtime/doctor.js | 97 +++++++++++++++++++ scripts/gstack2/generate-skill-tree.ts | 2 + skills/debug/references/RUNTIME.md | 2 + skills/design/references/RUNTIME.md | 2 + skills/plan/references/RUNTIME.md | 2 + skills/qa/references/RUNTIME.md | 2 + skills/review/references/RUNTIME.md | 2 + skills/ship/references/RUNTIME.md | 2 + test/gstack2-capability-readiness.test.ts | 113 ++++++++++++++++++++++ 12 files changed, 324 insertions(+), 8 deletions(-) create mode 100644 docs/gstack-2/CAPABILITY-READINESS.md create mode 100644 test/gstack2-capability-readiness.test.ts diff --git a/README.md b/README.md index ed00ef336f..dc368a644f 100644 --- a/README.md +++ b/README.md @@ -611,8 +611,12 @@ Data is stored in [Supabase](https://supabase.com) (open source Firebase alterna ## Troubleshooting -For GStack 2, first run `gstack doctor` (or `gstack doctor --json`) when the -optional runtime is installed. For skill discovery problems, use the standard +For GStack 2, run `gstack doctor --capability browser|design|diagram|pdf|ios` +(optionally `--json`) for a focused, non-mutating readiness result. It reports +pure-judgment availability, platform support, preview consent, install consent, +and runtime readiness separately; see +[`docs/gstack-2/CAPABILITY-READINESS.md`](docs/gstack-2/CAPABILITY-READINESS.md). +For skill discovery problems, use the standard installer's list and reinstall commands; do not manually copy host paths. The entries below apply to legacy 1.x installations. diff --git a/docs/gstack-2/CAPABILITY-READINESS.md b/docs/gstack-2/CAPABILITY-READINESS.md new file mode 100644 index 0000000000..c52f11fb88 --- /dev/null +++ b/docs/gstack-2/CAPABILITY-READINESS.md @@ -0,0 +1,75 @@ +# Capability readiness + +GStack's six public skills provide pure judgment without the optional runtime. +When a workflow needs executable evidence, inspect one of the five user-facing +capabilities without changing the machine: + +```bash +gstack doctor --capability browser +gstack doctor --capability design --json +gstack doctor --capability diagram +gstack doctor --capability pdf +gstack doctor --capability ios +``` + +The focused result deliberately keeps five questions separate: + +| Axis | Meaning | +|---|---| +| `judgment` | `available` even when no optional runtime exists. | +| `platform` | `supported` or `unsupported`; physical iOS is macOS-only. | +| `consent.preview` | Whether consent is required before an uncached public signed-manifest metadata request. Doctor never makes that request or grants consent. | +| `consent.install` | Separate consent required only after the complete preview. Doctor never installs or records consent. | +| `readiness` | Whether the selected local runtime capability is usable now. | + +Readiness has exactly five states: + +- `ready`: capability and managed-runtime checks passed. +- `degraded`: the capability passed, but the managed runtime reported a warning + such as recovery from an interrupted upgrade. +- `unavailable`: the capability is not selected, no runtime is active, or runtime + inspection could not establish availability. +- `unsupported`: the platform cannot provide the capability with GStack's + existing architecture. +- `failed`: an installed/selected capability failed its readiness evidence. + +The JSON form follows the runtime's result convention: `ok` is true only for +`ready` and `degraded`; a non-ready focused doctor exits 1. Usage errors still +exit 2 through the standard CLI error envelope. + +## Consent and setup flow + +```text +pure judgment (always available) + | + v +capability doctor (local, non-mutating) + | + +-- ready/degraded --> capability-dependent work + | + +-- unsupported ----> judgment-only work or supported platform + | + +-- unavailable/failed + | + v + ask for preview consent + | + v + packaged bootstrap preview (no install) + | + v + ask for install consent + | + v + packaged bootstrap install + | + v + capability doctor again +``` + +Preview consent is not install consent. Deferring either does not block pure +judgment. Follow the packaged bootstrap contract for exact dependency closure, +compressed bytes, signature checks, and atomic installation. Do not run setup +from a standards-installed skill directory, add a browser provider, replace the +local Chromium/Playwright backend, or replace the physical-iOS +DebugBridge/CoreDevice harness. diff --git a/runtime/cli.js b/runtime/cli.js index 31cd2d904c..385a2933e3 100644 --- a/runtime/cli.js +++ b/runtime/cli.js @@ -25,7 +25,13 @@ import { runExternalEffect, updateRunWorkflow, } from "./state.js"; -import { runDoctor, formatDoctor } from "./doctor.js"; +import { + CAPABILITY_READINESS_CAPABILITIES, + capabilityReadiness, + formatCapabilityReadiness, + runDoctor, + formatDoctor, +} from "./doctor.js"; import { cleanupRuntime } from "./cleanup.js"; import { ContextClient, @@ -157,11 +163,18 @@ async function initCommand({ args, home, cwd, stdout, legacyAlias = false }) { } async function doctorCommand({ args, home, cwd, stdout }) { - const parsed = parseFlags(args, new Set(["--json", "--skill-api"])); + const parsed = parseFlags(args, new Set(["--json", "--skill-api", "--capability"])); if (parsed.positionals.length) throw cliError("Doctor accepts only named options", "USAGE"); + const capability = parsed.values.get("--capability"); + if (capability && !CAPABILITY_READINESS_CAPABILITIES.includes(capability)) { + throw cliError(`Unknown capability: ${capability}. Choose ${CAPABILITY_READINESS_CAPABILITIES.join(", ")}.`, "USAGE"); + } const report = await runDoctor({ home, cwd, expectedSkillApi: parsed.values.get("--skill-api") }); - write(stdout, parsed.flags.has("--json") ? `${JSON.stringify(report, null, 2)}\n` : formatDoctor(report)); - return report.ok ? 0 : 1; + const result = capability ? capabilityReadiness(report, capability) : report; + write(stdout, parsed.flags.has("--json") + ? `${JSON.stringify(result, null, 2)}\n` + : capability ? formatCapabilityReadiness(result) : formatDoctor(result)); + return result.ok ? 0 : 1; } async function configCommand({ args, home, cwd, stdout }) { @@ -673,7 +686,7 @@ function parseFlags(args, allowed) { continue; } if (!allowed.has(arg)) throw cliError(`Unknown option: ${arg}`, "USAGE"); - if (["--source", "--version", "--url", "--older-than-hours", "--max-pages", "--max-depth", "--max-links", "--skill-api"].includes(arg)) { + if (["--source", "--version", "--url", "--older-than-hours", "--max-pages", "--max-depth", "--max-links", "--skill-api", "--capability"].includes(arg)) { const value = args[++index]; if (value == null || value.startsWith("--")) throw cliError(`${arg} requires a value`, "USAGE"); values.set(arg, value); @@ -750,7 +763,7 @@ function usage() { "Usage:\n" + " gstack init # initialize state only\n" + " gstack setup # compatibility alias for init\n" + - " gstack doctor [--skill-api ] [--json]\n" + + " gstack doctor [--capability browser|design|diagram|pdf|ios] [--skill-api ] [--json]\n" + " gstack paths [--json|--shell]\n" + " gstack runtime path \n" + " gstack config get [key]\n" + diff --git a/runtime/doctor.js b/runtime/doctor.js index 1f9601bada..81b9a4440b 100644 --- a/runtime/doctor.js +++ b/runtime/doctor.js @@ -17,6 +17,14 @@ import { managedBunRelativePath, } from "./install.js"; +export const CAPABILITY_READINESS_CAPABILITIES = Object.freeze([ + "browser", + "design", + "diagram", + "pdf", + "ios", +]); + export async function runDoctor(options = {}) { const paths = resolveRuntimePaths(options); const checks = []; @@ -183,6 +191,82 @@ export async function runDoctor(options = {}) { }; } +export function capabilityReadiness(report, capability, options = {}) { + if (!CAPABILITY_READINESS_CAPABILITIES.includes(capability)) { + throw new TypeError(`Unknown capability: ${capability}`); + } + const platform = options.platform ?? process.platform; + const checkedAt = report.checkedAt; + const judgment = { + status: "available", + message: "Pure-judgment skill guidance is available without the optional runtime.", + }; + if (capability === "ios" && platform !== "darwin") { + return { + ok: false, + capability, + checkedAt, + judgment, + platform: { status: "unsupported", platform, message: "Physical iOS requires macOS and the existing CoreDevice harness." }, + consent: { + preview: { status: "not-applicable", granted: false }, + install: { status: "not-applicable", granted: false }, + }, + readiness: { status: "unsupported", message: "The runtime capability is unsupported on this platform." }, + nextAction: "Continue with pure judgment or move the physical-iOS workflow to macOS.", + }; + } + + const capabilityCheck = report.checks.find((check) => check.id === `capability:${capability}`); + const runtimeCheck = report.checks.find((check) => check.id === "managed-runtime"); + let status; + if (capabilityCheck?.status === "pass" && runtimeCheck?.status === "pass") status = "ready"; + else if (capabilityCheck?.status === "pass") status = "degraded"; + else if (capabilityCheck?.status === "fail") status = "failed"; + else status = "unavailable"; + + const needsSetup = status === "unavailable" || status === "failed"; + const messages = { + ready: "The selected capability passed its runtime readiness checks.", + degraded: "The capability check passed, but the managed runtime reported a degraded condition.", + unavailable: "The optional runtime capability is not installed or cannot currently be inspected.", + failed: "The selected capability is installed but failed readiness checks.", + }; + return { + ok: status === "ready" || status === "degraded", + capability, + checkedAt, + judgment, + platform: { status: "supported", platform }, + consent: { + preview: { + status: needsSetup ? "required" : "not-required", + granted: false, + message: needsSetup + ? "Consent is required before an uncached signed-manifest metadata preview; this command does not grant it." + : "No setup preview is needed for the current readiness state.", + }, + install: { + status: needsSetup ? "required-after-preview" : "not-required", + granted: false, + message: needsSetup + ? "Install consent is separate and may be requested only after the complete preview; this command does not grant it." + : "No install is needed for the current readiness state.", + }, + }, + readiness: { + status, + message: messages[status], + evidence: [runtimeCheck, capabilityCheck].filter(Boolean), + }, + nextAction: needsSetup + ? `Ask for preview consent, then run the packaged bootstrap preview for ${capability}; continue judgment-only work if setup is deferred.` + : status === "degraded" + ? "Review the managed-runtime warning before capability-dependent evidence work." + : "Proceed with capability-dependent work.", + }; +} + async function inspectRuntimeBun(activeRoot, manifest) { const relative = managedBunRelativePath(); const declared = manifest?.tools?.bun; @@ -328,3 +412,16 @@ export function formatDoctor(report) { for (const check of report.checks) lines.push(`${symbol[check.status]} ${check.id}: ${check.message}`); return `${lines.join("\n")}\n`; } + +export function formatCapabilityReadiness(report) { + const lines = [ + `gstack capability readiness: ${report.capability}`, + `judgment: ${report.judgment.status}`, + `platform: ${report.platform.status}`, + `preview consent: ${report.consent.preview.status}`, + `install consent: ${report.consent.install.status}`, + `readiness: ${report.readiness.status}`, + `next: ${report.nextAction}`, + ]; + return `${lines.join("\n")}\n`; +} diff --git a/scripts/gstack2/generate-skill-tree.ts b/scripts/gstack2/generate-skill-tree.ts index 98642a63fd..dafd1f401e 100644 --- a/scripts/gstack2/generate-skill-tree.ts +++ b/scripts/gstack2/generate-skill-tree.ts @@ -516,6 +516,8 @@ The package/runtime compatibility tuple is \`schemaVersion=1\`, \`runtimeVersion 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. +Use \`gstack doctor --capability browser|design|diagram|pdf|ios\` (optionally \`--json\`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly \`ready\`, \`degraded\`, \`unavailable\`, \`unsupported\`, or \`failed\`. Doctor never grants or persists consent, previews metadata, or installs anything. \`unavailable\` means setup may be offered; \`failed\` means selected runtime evidence failed; \`unsupported\` is a platform boundary; and \`degraded\` means the capability passed while the managed runtime has a warning. + The developer-only fallback is \`node references/support/runtime-bootstrap.mjs install --source --capability --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. `; } diff --git a/skills/debug/references/RUNTIME.md b/skills/debug/references/RUNTIME.md index 43013427d9..21b329a103 100644 --- a/skills/debug/references/RUNTIME.md +++ b/skills/debug/references/RUNTIME.md @@ -23,4 +23,6 @@ The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2. 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. +Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning. + The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source --capability --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. diff --git a/skills/design/references/RUNTIME.md b/skills/design/references/RUNTIME.md index 43013427d9..21b329a103 100644 --- a/skills/design/references/RUNTIME.md +++ b/skills/design/references/RUNTIME.md @@ -23,4 +23,6 @@ The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2. 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. +Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning. + The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source --capability --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. diff --git a/skills/plan/references/RUNTIME.md b/skills/plan/references/RUNTIME.md index 43013427d9..21b329a103 100644 --- a/skills/plan/references/RUNTIME.md +++ b/skills/plan/references/RUNTIME.md @@ -23,4 +23,6 @@ The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2. 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. +Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning. + The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source --capability --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. diff --git a/skills/qa/references/RUNTIME.md b/skills/qa/references/RUNTIME.md index 43013427d9..21b329a103 100644 --- a/skills/qa/references/RUNTIME.md +++ b/skills/qa/references/RUNTIME.md @@ -23,4 +23,6 @@ The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2. 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. +Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning. + The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source --capability --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. diff --git a/skills/review/references/RUNTIME.md b/skills/review/references/RUNTIME.md index 43013427d9..21b329a103 100644 --- a/skills/review/references/RUNTIME.md +++ b/skills/review/references/RUNTIME.md @@ -23,4 +23,6 @@ The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2. 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. +Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning. + The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source --capability --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. diff --git a/skills/ship/references/RUNTIME.md b/skills/ship/references/RUNTIME.md index 43013427d9..21b329a103 100644 --- a/skills/ship/references/RUNTIME.md +++ b/skills/ship/references/RUNTIME.md @@ -23,4 +23,6 @@ The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2. 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. +Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning. + The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source --capability --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. diff --git a/test/gstack2-capability-readiness.test.ts b/test/gstack2-capability-readiness.test.ts new file mode 100644 index 0000000000..9691adfc6b --- /dev/null +++ b/test/gstack2-capability-readiness.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, test } from "bun:test"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { main } from "../runtime/cli.js"; +import { capabilityReadiness, formatCapabilityReadiness } from "../runtime/doctor.js"; +import { setupRuntime } from "../runtime/setup.js"; + +function report(checks: Array>) { + return { + ok: !checks.some((check) => check.status === "fail"), + home: "/fixture/.gstack", + checkedAt: "2026-07-20T00:00:00.000Z", + checks, + }; +} + +function capture() { + let value = ""; + return { stream: { write: (chunk: string) => { value += chunk; } }, value: () => value }; +} + +describe("capability readiness", () => { + test("keeps pure judgment, preview consent, and install consent separate when unavailable", () => { + const result = capabilityReadiness(report([ + { id: "managed-runtime", status: "fail", message: "No active managed runtime" }, + { id: "capability:pdf", status: "warn", message: "not installed" }, + ]), "pdf"); + + expect(result).toMatchObject({ + ok: false, + judgment: { status: "available" }, + platform: { status: "supported" }, + consent: { + preview: { status: "required", granted: false }, + install: { status: "required-after-preview", granted: false }, + }, + readiness: { status: "unavailable" }, + }); + }); + + test("distinguishes ready, degraded, and failed runtime states", () => { + const ready = capabilityReadiness(report([ + { id: "managed-runtime", status: "pass", message: "active" }, + { id: "capability:design", status: "pass", message: "runnable" }, + ]), "design"); + const degraded = capabilityReadiness(report([ + { id: "managed-runtime", status: "warn", message: "recovered" }, + { id: "capability:design", status: "pass", message: "runnable" }, + ]), "design"); + const failed = capabilityReadiness(report([ + { id: "managed-runtime", status: "pass", message: "active" }, + { id: "capability:diagram", status: "fail", message: "launcher metadata missing" }, + ]), "diagram"); + + expect(ready).toMatchObject({ ok: true, readiness: { status: "ready" } }); + expect(degraded).toMatchObject({ ok: true, readiness: { status: "degraded" } }); + expect(failed).toMatchObject({ ok: false, readiness: { status: "failed" } }); + expect(failed.consent.install.status).toBe("required-after-preview"); + }); + + test("reports physical iOS as unsupported without turning off pure judgment", () => { + const result = capabilityReadiness(report([]), "ios", { platform: "linux" }); + expect(result).toMatchObject({ + ok: false, + judgment: { status: "available" }, + platform: { status: "unsupported", platform: "linux" }, + consent: { + preview: { status: "not-applicable" }, + install: { status: "not-applicable" }, + }, + readiness: { status: "unsupported" }, + }); + }); + + test("human output exposes every state axis", () => { + const result = capabilityReadiness(report([ + { id: "managed-runtime", status: "pass", message: "active" }, + { id: "capability:browser", status: "pass", message: "launched" }, + ]), "browser"); + expect(formatCapabilityReadiness(result)).toContain("judgment: available"); + expect(formatCapabilityReadiness(result)).toContain("preview consent: not-required"); + expect(formatCapabilityReadiness(result)).toContain("install consent: not-required"); + expect(formatCapabilityReadiness(result)).toContain("readiness: ready"); + }); + + test("doctor capability JSON is non-mutating and uses the unified result envelope", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-capability-doctor-")); + const home = path.join(root, "home"); + const stdout = capture(); + const stderr = capture(); + try { + await setupRuntime({ home, cwd: root }); + const exit = await main(["doctor", "--capability", "pdf", "--json"], { + cwd: root, + env: { ...process.env, GSTACK_HOME: home }, + stdout: stdout.stream, + stderr: stderr.stream, + }); + const result = JSON.parse(stdout.value()); + expect(exit).toBe(1); + expect(stderr.value()).toBe(""); + expect(result).toMatchObject({ + ok: false, + capability: "pdf", + judgment: { status: "available" }, + readiness: { status: "unavailable" }, + }); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); +}); From 09492d34b42f628dfdb5ca02ca66e8f7f62f9a8c Mon Sep 17 00:00:00 2001 From: Sinabina Date: Mon, 20 Jul 2026 16:22:25 -0700 Subject: [PATCH 4/7] chore(security): harden GitHub workflows --- .github/workflows/actionlint.yml | 4 +++ .github/workflows/ci-image.yml | 4 +++ .github/workflows/dependency-review.yml | 21 +++++++++++ .github/workflows/osv-scanner.yml | 26 ++++++++++++++ .github/workflows/quality-gate.yml | 4 +++ .github/workflows/scorecard.yml | 46 +++++++++++++++++++++++++ .github/workflows/skill-docs.yml | 4 +++ test/release-hardening.test.ts | 19 ++++++++++ 8 files changed, 128 insertions(+) create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/osv-scanner.yml create mode 100644 .github/workflows/scorecard.yml diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml index 59c6159330..39eb7100c4 100644 --- a/.github/workflows/actionlint.yml +++ b/.github/workflows/actionlint.yml @@ -4,6 +4,10 @@ on: [push, pull_request] permissions: contents: read +concurrency: + group: actionlint-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: actionlint: runs-on: ubicloud-standard-8 diff --git a/.github/workflows/ci-image.yml b/.github/workflows/ci-image.yml index 3f8debe02c..ad638c2c93 100644 --- a/.github/workflows/ci-image.yml +++ b/.github/workflows/ci-image.yml @@ -16,6 +16,10 @@ on: # Manual trigger workflow_dispatch: +concurrency: + group: ci-image + cancel-in-progress: false + jobs: build: runs-on: ubicloud-standard-8 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000000..a5f20973bc --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,21 @@ +name: Dependency Review + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: dependency-review-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + dependency-review: + runs-on: ubuntu-24.04 + steps: + - name: Review dependency changes + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 + with: + fail-on-severity: high diff --git a/.github/workflows/osv-scanner.yml b/.github/workflows/osv-scanner.yml new file mode 100644 index 0000000000..22b8ae6eb3 --- /dev/null +++ b/.github/workflows/osv-scanner.yml @@ -0,0 +1,26 @@ +name: OSV Scanner + +on: + schedule: + - cron: '23 7 * * 1' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: osv-scanner + cancel-in-progress: true + +jobs: + scan: + permissions: + actions: read + contents: read + security-events: write + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@3adb4b14a2b0623876d18d863a498b785fb3752d # v2.3.8 + with: + scan-args: |- + --include-git-root + --recursive + ./ diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml index 932a82c142..62554cca2f 100644 --- a/.github/workflows/quality-gate.yml +++ b/.github/workflows/quality-gate.yml @@ -10,6 +10,10 @@ on: permissions: contents: read +concurrency: + group: quality-gate-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: quality: runs-on: ubuntu-24.04 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000000..a35bc680e7 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,46 @@ +name: OpenSSF Scorecard + +on: + branch_protection_rule: + schedule: + - cron: '41 7 * * 1' + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: scorecard-${{ github.ref }} + cancel-in-progress: true + +jobs: + analysis: + runs-on: ubuntu-24.04 + permissions: + contents: read + security-events: write + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + + - name: Run Scorecard analysis + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + publish_results: false + + - name: Upload Scorecard results to code scanning + uses: github/codeql-action/upload-sarif@85b88275909735f5bc23196090e03d2eb148b3de # v3.32.4 + with: + sarif_file: results.sarif + + - name: Upload Scorecard results artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: scorecard-results + path: results.sarif + retention-days: 5 diff --git a/.github/workflows/skill-docs.yml b/.github/workflows/skill-docs.yml index 2757f5e3a0..8d410d6102 100644 --- a/.github/workflows/skill-docs.yml +++ b/.github/workflows/skill-docs.yml @@ -4,6 +4,10 @@ on: [push, pull_request] permissions: contents: read +concurrency: + group: skill-docs-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: check-freshness: runs-on: ubicloud-standard-8 diff --git a/test/release-hardening.test.ts b/test/release-hardening.test.ts index 5296839571..7db3fad03f 100644 --- a/test/release-hardening.test.ts +++ b/test/release-hardening.test.ts @@ -12,6 +12,7 @@ describe("release and CI hardening", () => { for (const name of fs.readdirSync(workflowRoot).filter((entry) => entry.endsWith(".yml"))) { const source = fs.readFileSync(path.join(workflowRoot, name), "utf8"); expect(source, `${name} must declare top-level permissions`).toMatch(/^permissions:\s*$/m); + expect(source, `${name} must declare workflow concurrency`).toMatch(/^concurrency:\s*$/m); for (const match of source.matchAll(/\buses:\s*[^\s@]+@([^\s#]+)/g)) { expect(match[1], `${name} contains a mutable action ref`).toMatch(/^[a-f0-9]{40}$/); } @@ -24,6 +25,24 @@ describe("release and CI hardening", () => { expect(source.match(new RegExp(guard.replaceAll(".", "\\."), "g"))?.length).toBeGreaterThanOrEqual(3); }); + test("public dependency and repository security workflows stay enabled and least-privileged", () => { + const dependencyReview = read(".github/workflows/dependency-review.yml"); + expect(dependencyReview).toContain("actions/dependency-review-action@"); + expect(dependencyReview).toContain("fail-on-severity: high"); + expect(dependencyReview).not.toContain("pull_request_target:"); + + const osv = read(".github/workflows/osv-scanner.yml"); + expect(osv).toContain("google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@"); + expect(osv).toContain("security-events: write"); + expect(osv).toContain("schedule:"); + + const scorecard = read(".github/workflows/scorecard.yml"); + expect(scorecard).toContain("ossf/scorecard-action@"); + expect(scorecard).toContain("github/codeql-action/upload-sarif@"); + expect(scorecard).toContain("publish_results: false"); + expect(scorecard).not.toContain("id-token: write"); + }); + test("npm package is an explicit small runtime-control surface", () => { const pkg = JSON.parse(read("package.json")); expect(pkg.version).toBe(read("VERSION").trim()); From 37144e8b056e70f523a5619596004969eafe7313 Mon Sep 17 00:00:00 2001 From: Sinabina Date: Tue, 21 Jul 2026 12:29:01 -0700 Subject: [PATCH 5/7] fix(gstack2): harden integrated runtime verification --- .gitattributes | 2 +- browse/test/data-platform.test.ts | 12 +- browse/test/gstack-update-check.test.ts | 556 +-- browse/test/path-validation.test.ts | 5 +- bun.lock | 77 +- lib/diagram-render/bun.lock | 125 +- lib/diagram-render/dist/BUILD_INFO.json | 12 +- lib/diagram-render/dist/diagram-render.html | 4253 ++++++++----------- lib/diagram-render/package.json | 11 +- package.json | 21 +- runtime/cli.js | 72 +- runtime/execution-result.js | 3 + runtime/install.js | 3 + scripts/gstack2/ensure-runtime-payloads.ts | 7 +- scripts/gstack2/test-suite.ts | 22 + scripts/test-free-shards.ts | 7 +- scripts/test-free-strict.ts | 7 +- test/gstack2-capability-readiness.test.ts | 13 +- test/gstack2-runtime-effect-cli.test.ts | 28 +- test/gstack2-runtime-install.test.ts | 3 + test/gstack2-runtime-payloads.test.ts | 4 +- test/gstack2-semantic-parity.test.ts | 2 +- test/release-hardening.test.ts | 7 +- test/test-free-shards.test.ts | 11 + 24 files changed, 1991 insertions(+), 3272 deletions(-) create mode 100644 scripts/gstack2/test-suite.ts diff --git a/.gitattributes b/.gitattributes index 2b55295aa2..92e301fde7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -49,5 +49,5 @@ bin/* text eol=lf # The committed diagram-render bundle is hash-pinned (BUILD_INFO sha256); # a CRLF rewrite on Windows checkout would break the drift test and change # the content-addressed staged filename. -lib/diagram-render/dist/*.html text eol=lf +lib/diagram-render/dist/*.html text eol=lf whitespace=-trailing-space lib/diagram-render/dist/*.json text eol=lf diff --git a/browse/test/data-platform.test.ts b/browse/test/data-platform.test.ts index 7dc0f5844c..2a795a7fa3 100644 --- a/browse/test/data-platform.test.ts +++ b/browse/test/data-platform.test.ts @@ -118,10 +118,14 @@ describe('validateTempPath', () => { expect(() => validateTempPath('/tmp/nonexistent-file-12345.jpg')).toThrow(/not found/i); }); - it('rejects paths in cwd', () => { - // Create a real file in cwd to test the path check (not the existence check) - const cwdFile = path.join(process.cwd(), 'package.json'); - expect(() => validateTempPath(cwdFile)).toThrow(/temp directory/i); + it('rejects a temp-directory symlink that resolves outside temp', () => { + const link = path.join(TEMP_DIR, `test-temp-link-${Date.now()}`); + fs.symlinkSync('/etc/passwd', link); + try { + expect(() => validateTempPath(link)).toThrow(/temp directory/i); + } finally { + fs.unlinkSync(link); + } }); it('rejects absolute paths outside safe dirs', () => { diff --git a/browse/test/gstack-update-check.test.ts b/browse/test/gstack-update-check.test.ts index 0edd366e49..38efaa9b31 100644 --- a/browse/test/gstack-update-check.test.ts +++ b/browse/test/gstack-update-check.test.ts @@ -1,25 +1,29 @@ -/** - * Tests for bin/gstack-update-check bash script. - * - * Uses Bun.spawnSync to invoke the script with temp dirs and - * GSTACK_DIR / GSTACK_STATE_DIR / GSTACK_REMOTE_URL env overrides - * for full isolation. - */ - -import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; -import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync, mkdirSync, symlinkSync, utimesSync } from 'fs'; -import { join } from 'path'; -import { tmpdir } from 'os'; - -const SCRIPT = join(import.meta.dir, '..', '..', 'bin', 'gstack-update-check'); +/** Tests for the retired passive updater and its explicit compatibility check. */ + +import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +const ROOT = join(import.meta.dir, '..', '..'); +const SCRIPT = join(ROOT, 'bin', 'gstack-update-check'); let gstackDir: string; let stateDir: string; -function run(extraEnv: Record = {}, args: string[] = []) { +function run(args: string[] = [], extraEnv: Record = {}) { const result = Bun.spawnSync(['bash', SCRIPT, ...args], { env: { ...process.env, + GSTACK_HOME: '', GSTACK_DIR: gstackDir, GSTACK_STATE_DIR: stateDir, GSTACK_REMOTE_URL: `file://${join(gstackDir, 'REMOTE_VERSION')}`, @@ -38,10 +42,9 @@ function run(extraEnv: Record = {}, args: string[] = []) { beforeEach(() => { gstackDir = mkdtempSync(join(tmpdir(), 'gstack-upd-test-')); stateDir = mkdtempSync(join(tmpdir(), 'gstack-state-test-')); - // Link real gstack-config so update_check config check works const binDir = join(gstackDir, 'bin'); mkdirSync(binDir); - symlinkSync(join(import.meta.dir, '..', '..', 'bin', 'gstack-config'), join(binDir, 'gstack-config')); + symlinkSync(join(ROOT, 'bin', 'gstack-config'), join(binDir, 'gstack-config')); }); afterEach(() => { @@ -49,500 +52,69 @@ afterEach(() => { rmSync(stateDir, { recursive: true, force: true }); }); -function writeSnooze(version: string, level: number, epochSeconds: number) { - writeFileSync(join(stateDir, 'update-snoozed'), `${version} ${level} ${epochSeconds}`); -} - -function writeConfig(content: string) { - writeFileSync(join(stateDir, 'config.yaml'), content); -} - -function nowEpoch(): number { - return Math.floor(Date.now() / 1000); -} - -describe('gstack-update-check', () => { - // ─── Path A: No VERSION file ──────────────────────────────── - test('exits 0 with no output when VERSION file is missing', () => { - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); - }); - - // ─── Path B: Empty VERSION file ───────────────────────────── - test('exits 0 with no output when VERSION file is empty', () => { - writeFileSync(join(gstackDir, 'VERSION'), ''); - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); - }); - - // ─── Path C: Just-upgraded marker ─────────────────────────── - test('outputs JUST_UPGRADED and deletes marker', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.4.0\n'); - writeFileSync(join(stateDir, 'just-upgraded-from'), '0.3.3\n'); - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('JUST_UPGRADED 0.3.3 0.4.0'); - // Marker should be deleted - expect(existsSync(join(stateDir, 'just-upgraded-from'))).toBe(false); - // Cache should be written - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UP_TO_DATE'); - }); - - // ─── Path C2: Just-upgraded marker + newer remote ────────── - test('just-upgraded marker does not mask newer remote version', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.4.0\n'); - writeFileSync(join(stateDir, 'just-upgraded-from'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.5.0\n'); - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - // Should output both the just-upgraded notice AND the new upgrade - expect(stdout).toContain('JUST_UPGRADED 0.3.3 0.4.0'); - expect(stdout).toContain('UPGRADE_AVAILABLE 0.4.0 0.5.0'); - // Cache should reflect the upgrade available, not UP_TO_DATE - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UPGRADE_AVAILABLE 0.4.0 0.5.0'); - }); - - // ─── Path C3: Just-upgraded marker + remote matches local ── - test('just-upgraded with no further updates writes UP_TO_DATE cache', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.4.0\n'); - writeFileSync(join(stateDir, 'just-upgraded-from'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('JUST_UPGRADED 0.3.3 0.4.0'); - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UP_TO_DATE'); - }); - - // ─── Path D1: Fresh cache, UP_TO_DATE ─────────────────────── - test('exits silently when cache says UP_TO_DATE and is fresh', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UP_TO_DATE 0.3.3'); - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); - }); - - // ─── Path D1b: Fresh UP_TO_DATE cache, but local version changed ── - test('re-checks when UP_TO_DATE cache version does not match local', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.4.0\n'); - // Cache says UP_TO_DATE for 0.3.3, but local is now 0.4.0 - writeFileSync(join(stateDir, 'last-update-check'), 'UP_TO_DATE 0.3.3'); - // Remote says 0.5.0 — should detect upgrade - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.5.0\n'); - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('UPGRADE_AVAILABLE 0.4.0 0.5.0'); - }); - - // ─── Path D2: Fresh cache, UPGRADE_AVAILABLE ──────────────── - test('echoes cached UPGRADE_AVAILABLE when cache is fresh', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); - }); - - // ─── Path D3: Fresh cache, but local version changed ──────── - test('re-checks when local version does not match cached old version', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.4.0\n'); - // Cache says 0.3.3 → 0.4.0 but we're already on 0.4.0 - writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); - // Remote also says 0.4.0 — should be up to date - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); +describe('gstack-update-check compatibility boundary', () => { + test('passive invocation is a no-op even when an update and legacy state exist', () => { + writeFileSync(join(gstackDir, 'VERSION'), '1.0.0\n'); + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '2.0.0\n'); + writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 1.0.0 2.0.0'); + writeFileSync(join(stateDir, 'update-snoozed'), '2.0.0 3 1'); - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); // Up to date after re-check - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UP_TO_DATE'); + expect(run()).toEqual({ exitCode: 0, stdout: '', stderr: '' }); + expect(readFileSync(join(stateDir, 'last-update-check'), 'utf8')).toContain('UPGRADE_AVAILABLE'); + expect(existsSync(join(stateDir, 'update-snoozed'))).toBe(true); }); - // ─── Path E: Versions match (remote fetch) ───────────────── - test('writes UP_TO_DATE cache when versions match', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n'); - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UP_TO_DATE'); - }); - - // ─── Path F: Versions differ (remote fetch) ───────────────── - test('outputs UPGRADE_AVAILABLE when versions differ', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UPGRADE_AVAILABLE 0.3.3 0.4.0'); - }); - - // ─── Path G: Invalid remote response ──────────────────────── - test('treats invalid remote response as up to date', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '404 Not Found\n'); - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UP_TO_DATE'); - }); - - // ─── Path H: Curl fails (bad URL) ────────────────────────── - test('exits silently when remote URL is unreachable', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - - const { exitCode, stdout } = run({ - GSTACK_REMOTE_URL: 'file:///nonexistent/path/VERSION', + test('passive invocation performs no network or state setup', () => { + writeFileSync(join(gstackDir, 'VERSION'), '1.0.0\n'); + const missingStateDir = join(stateDir, 'not-created'); + const result = run([], { + GSTACK_STATE_DIR: missingStateDir, + GSTACK_REMOTE_URL: 'https://127.0.0.1:1/must-not-be-requested', }); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UP_TO_DATE'); - }); - - // ─── Path I: Corrupt cache file ───────────────────────────── - test('falls through to remote fetch when cache is corrupt', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'garbage data here'); - // Remote says same version — should end up UP_TO_DATE - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n'); - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); - // Cache should be overwritten with valid content - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UP_TO_DATE'); - }); - - // ─── State dir creation ───────────────────────────────────── - test('creates state dir if it does not exist', () => { - const newStateDir = join(stateDir, 'nested', 'dir'); - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n'); - - const { exitCode } = run({ GSTACK_STATE_DIR: newStateDir }); - expect(exitCode).toBe(0); - expect(existsSync(join(newStateDir, 'last-update-check'))).toBe(true); + expect(result).toEqual({ exitCode: 0, stdout: '', stderr: '' }); + expect(existsSync(missingStateDir)).toBe(false); }); - // ─── E2E regression: always exit 0 ─────────────────────────── - // Agents call this on every skill invocation. Exit code 1 breaks - // the preamble and confuses the agent. This test guards against - // regressions like the "exits 1 when up to date" bug. - test('exits 0 with real project VERSION and unreachable remote', () => { - // Simulate agent context: real VERSION file, network unavailable - const projectRoot = join(import.meta.dir, '..', '..'); - const versionFile = join(projectRoot, 'VERSION'); - if (!existsSync(versionFile)) return; // skip if no VERSION - const version = readFileSync(versionFile, 'utf-8').trim(); - - // Copy VERSION into test dir - writeFileSync(join(gstackDir, 'VERSION'), version + '\n'); - - // Remote is unreachable (simulates offline / CI / sandboxed agent) - const { exitCode, stdout } = run({ - GSTACK_REMOTE_URL: 'file:///nonexistent/path/VERSION', - }); - expect(exitCode).toBe(0); - // Should write UP_TO_DATE cache (not crash) - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UP_TO_DATE'); - }); - - test('exits 0 when up to date (not exit 1)', () => { - // Regression test: script previously exited 1 when versions matched. - // This broke every skill preamble that called it without || true. - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n'); - - // First call: fetches remote, writes cache - const first = run(); - expect(first.exitCode).toBe(0); - expect(first.stdout).toBe(''); - - // Second call: reads fresh cache - const second = run(); - expect(second.exitCode).toBe(0); - expect(second.stdout).toBe(''); - - // Third call with upgrade available: still exit 0 - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); - rmSync(join(stateDir, 'last-update-check')); // force re-fetch - const third = run(); - expect(third.exitCode).toBe(0); - expect(third.stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); - }); - - // ─── Snooze tests ─────────────────────────────────────────── - test('snoozed level 1 within 24h → silent (cached path)', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); - writeSnooze('0.4.0', 1, nowEpoch() - 3600); // 1h ago (within 24h) - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); - }); - - test('snoozed level 1 expired (25h ago) → outputs UPGRADE_AVAILABLE', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); - writeSnooze('0.4.0', 1, nowEpoch() - 90000); // 25h ago - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); - }); - - test('snoozed level 2 within 48h → silent', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); - writeSnooze('0.4.0', 2, nowEpoch() - 86400); // 24h ago (within 48h) - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); - }); - - test('snoozed level 2 expired (49h ago) → outputs', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); - writeSnooze('0.4.0', 2, nowEpoch() - 176400); // 49h ago - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); - }); - - test('snoozed level 3 within 7d → silent', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); - writeSnooze('0.4.0', 3, nowEpoch() - 518400); // 6d ago (within 7d) - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); - }); - - test('snoozed level 3 expired (8d ago) → outputs', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); - writeSnooze('0.4.0', 3, nowEpoch() - 691200); // 8d ago - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); - }); - - test('snooze ignored when version differs (new version resets snooze)', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.5.0'); - // Snoozed for 0.4.0, but remote is now 0.5.0 - writeSnooze('0.4.0', 3, nowEpoch() - 60); // very recent - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.5.0'); - }); - - test('corrupt snooze file → outputs normally', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); - writeFileSync(join(stateDir, 'update-snoozed'), 'garbage'); - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); - }); - - test('non-numeric epoch in snooze file → outputs', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); - writeFileSync(join(stateDir, 'update-snoozed'), '0.4.0 1 abc'); + test('--force reports and caches a newer valid version', () => { + writeFileSync(join(gstackDir, 'VERSION'), '1.9.0.0\n'); + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '1.10.0.0\n'); - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); + expect(run(['--force']).stdout).toBe('UPGRADE_AVAILABLE 1.9.0.0 1.10.0.0'); + expect(readFileSync(join(stateDir, 'last-update-check'), 'utf8').trim()) + .toBe('UPGRADE_AVAILABLE 1.9.0.0 1.10.0.0'); }); - test('non-numeric level in snooze file → outputs', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); - writeFileSync(join(stateDir, 'update-snoozed'), `0.4.0 abc ${nowEpoch()}`); + test('--force never offers a downgrade', () => { + writeFileSync(join(gstackDir, 'VERSION'), '1.10.0.0\n'); + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '1.9.0.0\n'); - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); + expect(run(['--force']).stdout).toBe(''); + expect(readFileSync(join(stateDir, 'last-update-check'), 'utf8').trim()) + .toBe('UP_TO_DATE 1.10.0.0'); }); - test('snooze respected on remote fetch path (no cache)', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); - // No cache file — goes to remote fetch path - writeSnooze('0.4.0', 1, nowEpoch() - 3600); // 1h ago + test('--force treats malformed or unavailable responses as non-updates', () => { + writeFileSync(join(gstackDir, 'VERSION'), '1.0.0\n'); + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), 'not a version\n'); - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); - // Cache should still be written - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UPGRADE_AVAILABLE 0.3.3 0.4.0'); + expect(run(['--force']).stdout).toBe(''); + expect(readFileSync(join(stateDir, 'last-update-check'), 'utf8').trim()) + .toBe('UP_TO_DATE 1.0.0'); }); - test('just-upgraded clears snooze file', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.4.0\n'); - writeFileSync(join(stateDir, 'just-upgraded-from'), '0.3.3\n'); - writeSnooze('0.4.0', 2, nowEpoch() - 3600); + test('--force clears obsolete snooze state and consumes the upgrade marker', () => { + writeFileSync(join(gstackDir, 'VERSION'), '1.0.0\n'); + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '1.0.0\n'); + writeFileSync(join(stateDir, 'just-upgraded-from'), '0.9.0\n'); + writeFileSync(join(stateDir, 'update-snoozed'), '1.0.0 3 9999999999'); - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('JUST_UPGRADED 0.3.3 0.4.0'); + expect(run(['--force']).stdout).toBe('JUST_UPGRADED 0.9.0 1.0.0'); + expect(existsSync(join(stateDir, 'just-upgraded-from'))).toBe(false); expect(existsSync(join(stateDir, 'update-snoozed'))).toBe(false); }); - // ─── Config tests ────────────────────────────────────────── - test('update_check: false disables all checks', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); - writeConfig('update_check: false\n'); - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); - // No cache should be written + test('--force with no local version exits cleanly without creating a cache', () => { + expect(run(['--force'])).toEqual({ exitCode: 0, stdout: '', stderr: '' }); expect(existsSync(join(stateDir, 'last-update-check'))).toBe(false); }); - - test('missing config.yaml does not crash', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); - // No config file — should behave normally - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); - }); - - // ─── --force flag tests ────────────────────────────────────── - - test('--force busts fresh UP_TO_DATE cache', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UP_TO_DATE 0.3.3'); - - // Without --force: cache hit, silent - const cached = run(); - expect(cached.stdout).toBe(''); - - // With --force: cache busted, re-fetches, finds upgrade - const forced = run({}, ['--force']); - expect(forced.exitCode).toBe(0); - expect(forced.stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); - }); - - test('--force busts fresh UPGRADE_AVAILABLE cache', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); - - // Without --force: cache hit, outputs stale upgrade - const cached = run(); - expect(cached.stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); - - // With --force: cache busted, re-fetches, now up to date - const forced = run({}, ['--force']); - expect(forced.exitCode).toBe(0); - expect(forced.stdout).toBe(''); - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UP_TO_DATE'); - }); - - test('--force clears snooze so user can upgrade after snoozing', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); - writeSnooze('0.4.0', 1, nowEpoch() - 60); // snoozed 1 min ago (within 24h) - - // Without --force: snoozed, silent - const snoozed = run(); - expect(snoozed.exitCode).toBe(0); - expect(snoozed.stdout).toBe(''); - - // With --force: snooze cleared, outputs upgrade - const forced = run({}, ['--force']); - expect(forced.exitCode).toBe(0); - expect(forced.stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); - // Snooze file should be deleted - expect(existsSync(join(stateDir, 'update-snoozed'))).toBe(false); - }); - - // ─── Split TTL tests ───────────────────────────────────────── - - // ─── Semver-order guard ───────────────────────────────────── - // When the upstream raw CDN serves a stale (older) VERSION right after a - // release, the script previously emitted a backwards UPGRADE_AVAILABLE - // line. The guard treats REMOTE < LOCAL as up-to-date. - - test('remote older than local (stale CDN) → silent, cache UP_TO_DATE', () => { - writeFileSync(join(gstackDir, 'VERSION'), '1.34.0.0\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '1.33.2.0\n'); - - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe(''); - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UP_TO_DATE 1.34.0.0'); - }); - - test('multi-segment sort: 1.9.0.0 < 1.10.0.0', () => { - writeFileSync(join(gstackDir, 'VERSION'), '1.9.0.0\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '1.10.0.0\n'); - - const { stdout } = run(); - expect(stdout).toBe('UPGRADE_AVAILABLE 1.9.0.0 1.10.0.0'); - }); - - test('multi-segment reverse sort: 1.10.0.0 > 1.9.0.0 → no rewind', () => { - writeFileSync(join(gstackDir, 'VERSION'), '1.10.0.0\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '1.9.0.0\n'); - - const { stdout } = run(); - expect(stdout).toBe(''); - const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); - expect(cache).toContain('UP_TO_DATE 1.10.0.0'); - }); - - test('UP_TO_DATE cache expires after 60 min (not 720)', () => { - writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); - writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); - writeFileSync(join(stateDir, 'last-update-check'), 'UP_TO_DATE 0.3.3'); - - // Set cache mtime to 90 minutes ago (past 60-min TTL) - const ninetyMinAgo = new Date(Date.now() - 90 * 60 * 1000); - const cachePath = join(stateDir, 'last-update-check'); - utimesSync(cachePath, ninetyMinAgo, ninetyMinAgo); - - // Cache should be stale at 60-min TTL, re-fetches and finds upgrade - const { exitCode, stdout } = run(); - expect(exitCode).toBe(0); - expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); - }); }); diff --git a/browse/test/path-validation.test.ts b/browse/test/path-validation.test.ts index 838d22098a..c9c7f8cc28 100644 --- a/browse/test/path-validation.test.ts +++ b/browse/test/path-validation.test.ts @@ -4,7 +4,7 @@ import { validateReadPath, SENSITIVE_COOKIE_NAME, SENSITIVE_COOKIE_VALUE } from import { BLOCKED_METADATA_HOSTS } from '../src/url-validation'; import { readFileSync, symlinkSync, unlinkSync, writeFileSync, realpathSync } from 'fs'; import { tmpdir } from 'os'; -import { join } from 'path'; +import { join, relative } from 'path'; describe('validateOutputPath', () => { it('allows paths within /tmp', () => { @@ -82,7 +82,8 @@ describe('validateReadPath', () => { }); it('blocks nested path traversal', () => { - expect(() => validateReadPath('src/../../etc/passwd')).toThrow(/Path must be within/); + const escapeToEtc = relative(process.cwd(), '/etc/passwd'); + expect(() => validateReadPath(`src/../${escapeToEtc}`)).toThrow(/Path must be within/); }); it('blocks symlink inside safe dir pointing outside', () => { diff --git a/bun.lock b/bun.lock index 1dd1ec8697..a540f38ebc 100644 --- a/bun.lock +++ b/bun.lock @@ -5,43 +5,52 @@ "": { "name": "gstack", "dependencies": { - "@anthropic-ai/sdk": "^0.78.0", + "@anthropic-ai/sdk": "^0.112.4", "@ngrok/ngrok": "^1.7.0", "diff": "^9.0.0", "html-to-docx": "1.8.0", - "marked": "^18.0.2", + "marked": "^18.0.6", "playwright": "^1.58.2", "sharp": "^0.34.5", - "socks": "^2.8.8", + "socks": "^2.8.9", "xterm": "5", "xterm-addon-fit": "^0.8.0", }, "devDependencies": { - "@anthropic-ai/claude-agent-sdk": "0.2.117", - "@huggingface/transformers": "^4.1.0", + "@anthropic-ai/claude-agent-sdk": "0.3.216", + "@huggingface/transformers": "4.2.0", }, }, }, + "overrides": { + "@protobufjs/utf8": "1.1.1", + "adm-zip": "0.6.0", + "fast-uri": "3.1.2", + "hono": "4.12.25", + "ip-address": "10.2.0", + "protobufjs": "7.6.5", + "qs": "6.15.2", + }, "packages": { - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.117", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.117", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.117", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.117", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.117", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.117", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.117", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.117", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.117" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-pVBss1Vu0w87nKCBhWtjMggSgCh6GVUtdRmuE58ZvXv0E2q0JcnUCQHehmn92BAW0+VCwPY8q/k7uKWkgwz/gA=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.216", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.216", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.216", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.216", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.216", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.216", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.216", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.216", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.216" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-Fl/d9yGW7Pm0aGwUNkJMvruOVobleYcoprGJqufdP4J9W2XL/2xKXqanY8KqUdfySRMI2N46rY4/lryq5SfgFg=="], - "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.117", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZeC/Lz8XMKQ5w+GmjTziPR8bSSarBtNCJMkMAYRT9ekNmyXSWXEwGLENe5TDDmtpzNNzAB1mQNuIYoqTsqgV3w=="], + "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.216", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TWtTUabXj+AMOBMknSe3EVQb/7qucw5pHYi0HWoKoixQt1JE6J/CiLDBIY69OD3eQn9k1XG6LaurT239kFxLUw=="], - "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.117", "", { "os": "darwin", "cpu": "x64" }, "sha512-DKyggGzzpDcr9S435xlpbpwkEYKZNbePSekug75tJclK8l4ddD9+M9BFgMiSUq9F1Zt53kUaRDihDu/cBKvkdQ=="], + "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.216", "", { "os": "darwin", "cpu": "x64" }, "sha512-tULuvqhJUwGTj71Uln4uPumm0la4EO1XydFbISTbdc1BsuvGx3FOtFcVs0Qg3dhNBFAoU/iYVTO/B1rImeJZig=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.117", "", { "os": "linux", "cpu": "arm64" }, "sha512-jyHmyZQavpPOe3zxBRX3KbdOAJ8JwZ8m/wMr5bhHhhcstugm/vJx6IIs7D44VvFjk+8sqdvR2ZrliL8PUcJL0g=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.216", "", { "os": "linux", "cpu": "arm64" }, "sha512-kX6x0otwOuA1zIeEiuAKQqOk2TUAOkDUzm4MmLoQHOM2xWvr/pc+xY4DZxaX4ef9dUFQuHA4r5Vtbk7tPTL7Bw=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.117", "", { "os": "linux", "cpu": "arm64" }, "sha512-bJU5gEOmM4VCOn4h8vipOKgdhPATePQ23mMpvyVqtVyipWppHfOUfVkqXb+SrF/hfkNSMYxDuoKxbJ+MmKtGjg=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.216", "", { "os": "linux", "cpu": "arm64" }, "sha512-sisWj2nhQolKu6aGWnu+I7d8EU6/m5J3G6bGfR7YvZ1wkQTQZAX3aOq6DVLIfCaZSH22wtqyuXhE/gn5eU1syw=="], - "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.2.117", "", { "os": "linux", "cpu": "x64" }, "sha512-Zb5PXKrDNbQ1dyNYwxZMNL+F2Dhgjh9f9B21wZUJqkhJL69hRJwJyxO42HiNmB2zGCaTxQTyjPhLdB/eQJo74Q=="], + "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.216", "", { "os": "linux", "cpu": "x64" }, "sha512-WbJTLQafcYw9wl+gh/YpepdbWsOoJS7JZQA0obpzBBWzpx9PqPbjUDXDfCDrtzDI2fmr3Nz0//vC12bvlMZd8g=="], - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.117", "", { "os": "linux", "cpu": "x64" }, "sha512-LIkKTAYZGugEVssAuWCPqlDWSqhVZAveNPNsfKLbuG1naIMCR04fUqil6i3d3mAAfk7FaS5D4IdHp45psi+GDw=="], + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.216", "", { "os": "linux", "cpu": "x64" }, "sha512-3Ts2Ab6oEGG2r+epmWc6lOR1ahECjvH4y+lPb32qLKMdYxT2PU6TAc4Z/USLLuqDpgw3nSb8xuiYEH77rv88fw=="], - "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.117", "", { "os": "win32", "cpu": "arm64" }, "sha512-uetggH3B83PiH0a9D/5MVXB5Hqnlr2DVajehwAP2x0Mt4DBd632ICnHpu6pnSP+vVkWgq3FgQlkHe91RfP+peA=="], + "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.216", "", { "os": "win32", "cpu": "arm64" }, "sha512-xGB8rKatsXl9enqmPXixYNvdjGh1AL2jFmMRC9lR9M1prf2FGl7dkpdHz8YyItKAciRwpPIfts/9dSGhxlgT3A=="], - "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.2.117", "", { "os": "win32", "cpu": "x64" }, "sha512-TT4KngAokDTJSvQ2mrAP6ZRkXj50OLj7Tb1zZA4CnkmrrEidgs4KrMx7er1ZwoivngIvCekV9+TbtC9giknr5w=="], + "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.216", "", { "os": "win32", "cpu": "x64" }, "sha512-cGbBgoKNzB6wRL6bMOwM2iJEG/wx0B6FCblmKF+ZAYKcj5oNi3jM6oQsnPJbKfrzVKrip3YEpXxopQ9O30LrSg=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.78.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.112.4", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-7eXJJnrmBI5GMC6drrCiSkycVsT7crRZX3qv5HusLSm+qiILjmtqP7gf+UiT7ASu/7Gdj+Zfl4f2haV8wATKUg=="], "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], @@ -53,7 +62,7 @@ "@huggingface/tokenizers": ["@huggingface/tokenizers@0.1.3", "", {}, "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA=="], - "@huggingface/transformers": ["@huggingface/transformers@4.1.0", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "@huggingface/tokenizers": "^0.1.3", "onnxruntime-node": "1.24.3", "onnxruntime-web": "1.26.0-dev.20260410-5e55544225", "sharp": "^0.34.5" } }, "sha512-WiMf9eyvF6V2pj4gs12A7GQV3svyFIBtB/W+Hn5lT5E5DyqWUno1ZrWoAfJv69X1RNv/0GoOo6DFmL6NOYd+rg=="], + "@huggingface/transformers": ["@huggingface/transformers@4.2.0", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "@huggingface/tokenizers": "^0.1.3", "onnxruntime-node": "1.24.3", "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c", "sharp": "^0.34.5" } }, "sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ=="], "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], @@ -147,27 +156,27 @@ "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], + + "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "adm-zip": ["adm-zip@0.5.17", "", {}, "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ=="], + "adm-zip": ["adm-zip@0.6.0", "", {}, "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg=="], "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], @@ -265,7 +274,9 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], + + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], @@ -301,7 +312,7 @@ "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], - "hono": ["hono@4.12.14", "", {}, "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w=="], + "hono": ["hono@4.12.25", "", {}, "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ=="], "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], @@ -357,7 +368,7 @@ "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - "marked": ["marked@18.0.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-NsmlUYBS/Zg57rgDWMYdnre6OTj4e+qq/JS2ot3KrYLSoHLw+sDu0Nm1ZGpRgYAq6c+b1ekaY5NzVchMCQnzcg=="], + "marked": ["marked@18.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-MrV5puXBfuiy6wl6DLaq3BtIJQAJToAd5zt/ZKhRfGRAuFPALE7/4Y7jnxRQoEgK/pBgurGqLyAuRgZ2xOjr6w=="], "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], @@ -397,7 +408,7 @@ "onnxruntime-node": ["onnxruntime-node@1.24.3", "", { "dependencies": { "adm-zip": "^0.5.16", "global-agent": "^3.0.0", "onnxruntime-common": "1.24.3" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg=="], - "onnxruntime-web": ["onnxruntime-web@1.26.0-dev.20260410-5e55544225", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-hHd9n8DzIfGSAjM4Dvslesc8i6h9HEEcl8qt7X3LfhUxMgls6FBJ32j2xrDtJjKJFEehFeJmyB/pvad1I8KS8w=="], + "onnxruntime-web": ["onnxruntime-web@1.26.0-dev.20260416-b7804b056c", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw=="], "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], @@ -419,13 +430,13 @@ "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], - "protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], + "protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "punycode": ["punycode@1.4.1", "", {}, "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="], - "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], + "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], @@ -477,10 +488,12 @@ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], - "socks": ["socks@2.8.8", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog=="], + "socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="], "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "string-template": ["string-template@0.2.1", "", {}, "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw=="], @@ -533,8 +546,6 @@ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "@anthropic-ai/claude-agent-sdk/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="], - "@oozcitak/infra/@oozcitak/util": ["@oozcitak/util@8.0.0", "", {}, "sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw=="], "@oozcitak/url/@oozcitak/infra": ["@oozcitak/infra@1.0.3", "", { "dependencies": { "@oozcitak/util": "1.0.1" } }, "sha512-9O2wxXGnRzy76O1XUxESxDGsXT5kzETJPvYbreO4mv6bqe1+YSuux2cZTagjJ/T4UfEwFJz5ixanOqB0QgYAag=="], @@ -549,8 +560,6 @@ "express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "express-rate-limit/ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], - "htmlparser2/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.24.0-dev.20251116-b39e144322", "", {}, "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw=="], diff --git a/lib/diagram-render/bun.lock b/lib/diagram-render/bun.lock index a9472740ee..9a666b99c9 100644 --- a/lib/diagram-render/bun.lock +++ b/lib/diagram-render/bun.lock @@ -5,14 +5,19 @@ "": { "name": "@gstack/diagram-render", "dependencies": { - "@excalidraw/excalidraw": "0.18.0", - "@excalidraw/mermaid-to-excalidraw": "1.1.2", - "mermaid": "11.12.2", + "@excalidraw/excalidraw": "0.18.1", + "@excalidraw/mermaid-to-excalidraw": "2.2.2", + "mermaid": "11.16.0", "react": "18.3.1", "react-dom": "18.3.1", }, }, }, + "overrides": { + "dompurify": "3.4.11", + "lodash-es": "4.18.1", + "nanoid": "5.0.9", + }, "packages": { "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], @@ -26,17 +31,17 @@ "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.0.3", "", {}, "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="], - "@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="], + "@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], - "@excalidraw/excalidraw": ["@excalidraw/excalidraw@0.18.0", "", { "dependencies": { "@braintree/sanitize-url": "6.0.2", "@excalidraw/laser-pointer": "1.3.1", "@excalidraw/mermaid-to-excalidraw": "1.1.2", "@excalidraw/random-username": "1.1.0", "@radix-ui/react-popover": "1.1.6", "@radix-ui/react-tabs": "1.0.2", "browser-fs-access": "0.29.1", "canvas-roundrect-polyfill": "0.0.1", "clsx": "1.1.1", "cross-env": "7.0.3", "es6-promise-pool": "2.5.0", "fractional-indexing": "3.2.0", "fuzzy": "0.1.3", "image-blob-reduce": "3.0.1", "jotai": "2.11.0", "jotai-scope": "0.7.2", "lodash.debounce": "4.0.8", "lodash.throttle": "4.1.1", "nanoid": "3.3.3", "open-color": "1.9.1", "pako": "2.0.3", "perfect-freehand": "1.2.0", "pica": "7.1.1", "png-chunk-text": "1.0.0", "png-chunks-encode": "1.0.0", "png-chunks-extract": "1.0.0", "points-on-curve": "1.0.1", "pwacompat": "2.0.17", "roughjs": "4.6.4", "sass": "1.51.0", "tunnel-rat": "0.1.2" }, "peerDependencies": { "react": "^17.0.2 || ^18.2.0 || ^19.0.0", "react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0" } }, "sha512-QkIiS+5qdy8lmDWTKsuy0sK/fen/LRDtbhm2lc2xcFcqhv2/zdg95bYnl+wnwwXGHo7kEmP65BSiMHE7PJ3Zpw=="], + "@excalidraw/excalidraw": ["@excalidraw/excalidraw@0.18.1", "", { "dependencies": { "@braintree/sanitize-url": "6.0.2", "@excalidraw/laser-pointer": "1.3.1", "@excalidraw/mermaid-to-excalidraw": "2.2.2", "@excalidraw/random-username": "1.1.0", "@radix-ui/react-popover": "1.1.6", "@radix-ui/react-tabs": "1.0.2", "browser-fs-access": "0.29.1", "canvas-roundrect-polyfill": "0.0.1", "clsx": "1.1.1", "cross-env": "7.0.3", "es6-promise-pool": "2.5.0", "fractional-indexing": "3.2.0", "fuzzy": "0.1.3", "image-blob-reduce": "3.0.1", "jotai": "2.11.0", "jotai-scope": "0.7.2", "lodash.debounce": "4.0.8", "lodash.throttle": "4.1.1", "nanoid": "3.3.3", "open-color": "1.9.1", "pako": "2.0.3", "perfect-freehand": "1.2.0", "pica": "7.1.1", "png-chunk-text": "1.0.0", "png-chunks-encode": "1.0.0", "png-chunks-extract": "1.0.0", "points-on-curve": "1.0.1", "pwacompat": "2.0.17", "roughjs": "4.6.4", "sass": "1.51.0", "tunnel-rat": "0.1.2" }, "peerDependencies": { "react": "^17.0.2 || ^18.2.0 || ^19.0.0", "react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0" } }, "sha512-6i5Gt7IDTOH//qa0Z315Ly5iVRhjWpu2whrlQFqkuwrkKUWgRsMk0P5qdE7bpyDpai7jeLeWYkyj1eVAfni1lw=="], "@excalidraw/laser-pointer": ["@excalidraw/laser-pointer@1.3.1", "", {}, "sha512-psA1z1N2qeAfsORdXc9JmD2y4CmDwmuMRxnNdJHZexIcPwaNEyIpNcelw+QkL9rz9tosaN9krXuKaRqYpRAR6g=="], "@excalidraw/markdown-to-text": ["@excalidraw/markdown-to-text@0.1.2", "", {}, "sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg=="], - "@excalidraw/mermaid-to-excalidraw": ["@excalidraw/mermaid-to-excalidraw@1.1.2", "", { "dependencies": { "@excalidraw/markdown-to-text": "0.1.2", "mermaid": "10.9.3", "nanoid": "4.0.2" } }, "sha512-hAFv/TTIsOdoy0dL5v+oBd297SQ+Z88gZ5u99fCIFuEMHfQuPgLhU/ztKhFSTs7fISwVo6fizny/5oQRR3d4tQ=="], + "@excalidraw/mermaid-to-excalidraw": ["@excalidraw/mermaid-to-excalidraw@2.2.2", "", { "dependencies": { "@excalidraw/markdown-to-text": "0.1.2", "@mermaid-js/parser": "^0.6.3", "mermaid": "^11.12.1", "nanoid": "4.0.2" } }, "sha512-5VKQq5CdRocC82vOIUpQ5ufJOVV9FpBTdHGA+ULqazeIVV+cr299877omQCibsdS3Bpitz2fsnTwnIXEmLVDSg=="], "@excalidraw/random-username": ["@excalidraw/random-username@1.1.0", "", {}, "sha512-nULYsQxkWHnbmHvcs+efMkJ4/9TtvNyFeLyHdeGxW0zHs6P+jYVqcRff9A6Vq9w9JXeDRnRh2VKvTtS19GW2qA=="], @@ -166,17 +171,11 @@ "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], - "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], - "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], - "@types/mdast": ["@types/mdast@3.0.15", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ=="], - - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], - "@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], @@ -190,8 +189,6 @@ "canvas-roundrect-polyfill": ["canvas-roundrect-polyfill@0.0.1", "", {}, "sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw=="], - "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], - "chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="], "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="], @@ -280,25 +277,17 @@ "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], - "dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q=="], + "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], "dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], - "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - "diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], - - "dompurify": ["dompurify@3.4.9", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ=="], + "dompurify": ["dompurify@3.4.11", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw=="], - "elkjs": ["elkjs@0.9.3", "", {}, "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="], + "es-toolkit": ["es-toolkit@1.49.0", "", {}, "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g=="], "es6-promise-pool": ["es6-promise-pool@2.5.0", "", {}, "sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA=="], @@ -350,8 +339,6 @@ "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], - "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], - "langium": ["langium@3.3.1", "", { "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.0.8" } }, "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w=="], "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], @@ -366,63 +353,11 @@ "marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], - "mdast-util-from-markdown": ["mdast-util-from-markdown@1.3.1", "", { "dependencies": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", "decode-named-character-reference": "^1.0.0", "mdast-util-to-string": "^3.1.0", "micromark": "^3.0.0", "micromark-util-decode-numeric-character-reference": "^1.0.0", "micromark-util-decode-string": "^1.0.0", "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "unist-util-stringify-position": "^3.0.0", "uvu": "^0.5.0" } }, "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww=="], - - "mdast-util-to-string": ["mdast-util-to-string@3.2.0", "", { "dependencies": { "@types/mdast": "^3.0.0" } }, "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg=="], - - "mermaid": ["mermaid@11.12.2", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^0.6.3", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w=="], - - "micromark": ["micromark@3.2.0", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "micromark-core-commonmark": "^1.0.1", "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-chunked": "^1.0.0", "micromark-util-combine-extensions": "^1.0.0", "micromark-util-decode-numeric-character-reference": "^1.0.0", "micromark-util-encode": "^1.0.0", "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-resolve-all": "^1.0.0", "micromark-util-sanitize-uri": "^1.0.0", "micromark-util-subtokenize": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.1", "uvu": "^0.5.0" } }, "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA=="], - - "micromark-core-commonmark": ["micromark-core-commonmark@1.1.0", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-factory-destination": "^1.0.0", "micromark-factory-label": "^1.0.0", "micromark-factory-space": "^1.0.0", "micromark-factory-title": "^1.0.0", "micromark-factory-whitespace": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-chunked": "^1.0.0", "micromark-util-classify-character": "^1.0.0", "micromark-util-html-tag-name": "^1.0.0", "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-resolve-all": "^1.0.0", "micromark-util-subtokenize": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.1", "uvu": "^0.5.0" } }, "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw=="], - - "micromark-factory-destination": ["micromark-factory-destination@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg=="], - - "micromark-factory-label": ["micromark-factory-label@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "uvu": "^0.5.0" } }, "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w=="], - - "micromark-factory-space": ["micromark-factory-space@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ=="], - - "micromark-factory-title": ["micromark-factory-title@1.1.0", "", { "dependencies": { "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ=="], - - "micromark-factory-whitespace": ["micromark-factory-whitespace@1.1.0", "", { "dependencies": { "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ=="], - - "micromark-util-character": ["micromark-util-character@1.2.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg=="], - - "micromark-util-chunked": ["micromark-util-chunked@1.1.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0" } }, "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ=="], - - "micromark-util-classify-character": ["micromark-util-classify-character@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw=="], - - "micromark-util-combine-extensions": ["micromark-util-combine-extensions@1.1.0", "", { "dependencies": { "micromark-util-chunked": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA=="], - - "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@1.1.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0" } }, "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw=="], - - "micromark-util-decode-string": ["micromark-util-decode-string@1.1.0", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-decode-numeric-character-reference": "^1.0.0", "micromark-util-symbol": "^1.0.0" } }, "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ=="], - - "micromark-util-encode": ["micromark-util-encode@1.1.0", "", {}, "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw=="], - - "micromark-util-html-tag-name": ["micromark-util-html-tag-name@1.2.0", "", {}, "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q=="], - - "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@1.1.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0" } }, "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q=="], - - "micromark-util-resolve-all": ["micromark-util-resolve-all@1.1.0", "", { "dependencies": { "micromark-util-types": "^1.0.0" } }, "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA=="], - - "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@1.2.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-encode": "^1.0.0", "micromark-util-symbol": "^1.0.0" } }, "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A=="], - - "micromark-util-subtokenize": ["micromark-util-subtokenize@1.1.0", "", { "dependencies": { "micromark-util-chunked": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "uvu": "^0.5.0" } }, "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A=="], - - "micromark-util-symbol": ["micromark-util-symbol@1.1.0", "", {}, "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag=="], - - "micromark-util-types": ["micromark-util-types@1.1.0", "", {}, "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg=="], - - "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mermaid": ["mermaid@11.16.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.20", "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", "katex": "^0.16.45", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA=="], "multimath": ["multimath@2.0.0", "", { "dependencies": { "glur": "^1.1.2", "object-assign": "^4.1.1" } }, "sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g=="], - "nanoid": ["nanoid@3.3.3", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w=="], - - "non-layered-tidy-tree-layout": ["non-layered-tidy-tree-layout@2.0.2", "", {}, "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw=="], + "nanoid": ["nanoid@5.0.9", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -474,8 +409,6 @@ "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], - "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "sass": ["sass@1.51.0", "", { "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { "sass": "sass.js" } }, "sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA=="], @@ -502,8 +435,6 @@ "tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="], - "unist-util-stringify-position": ["unist-util-stringify-position@3.0.3", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg=="], - "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], @@ -512,8 +443,6 @@ "uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], - "uvu": ["uvu@0.5.6", "", { "dependencies": { "dequal": "^2.0.0", "diff": "^5.0.0", "kleur": "^4.0.3", "sade": "^1.7.3" }, "bin": { "uvu": "bin.js" } }, "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA=="], - "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], @@ -526,21 +455,15 @@ "vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="], - "web-worker": ["web-worker@1.5.0", "", {}, "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw=="], - "webworkify": ["webworkify@1.5.0", "", {}, "sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], - "@chevrotain/cst-dts-gen/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], - - "@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "@chevrotain/cst-dts-gen/@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="], - "@excalidraw/mermaid-to-excalidraw/mermaid": ["mermaid@10.9.3", "", { "dependencies": { "@braintree/sanitize-url": "^6.0.1", "@types/d3-scale": "^4.0.3", "@types/d3-scale-chromatic": "^3.0.0", "cytoscape": "^3.28.1", "cytoscape-cose-bilkent": "^4.1.0", "d3": "^7.4.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.10", "dayjs": "^1.11.7", "dompurify": "^3.0.5 <3.1.7", "elkjs": "^0.9.0", "katex": "^0.16.9", "khroma": "^2.0.0", "lodash-es": "^4.17.21", "mdast-util-from-markdown": "^1.3.0", "non-layered-tidy-tree-layout": "^2.0.2", "stylis": "^4.1.3", "ts-dedent": "^2.2.0", "uuid": "^9.0.0", "web-worker": "^1.2.0" } }, "sha512-V80X1isSEvAewIL3xhmz/rVmc27CVljcsbWxkxlWJWY/1kQa4XOABqpDl2qQLGKzpKm6WbTfUEKImBlUfFYArw=="], - - "@excalidraw/mermaid-to-excalidraw/nanoid": ["nanoid@4.0.2", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw=="], + "@chevrotain/gast/@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="], "@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], @@ -576,7 +499,7 @@ "@radix-ui/react-tabs/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg=="], - "chevrotain/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "chevrotain/@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="], "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], @@ -588,18 +511,14 @@ "mermaid/@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], + "mermaid/@mermaid-js/parser": ["@mermaid-js/parser@1.2.0", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA=="], + "mermaid/roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], "points-on-path/points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], "roughjs/points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], - "@excalidraw/mermaid-to-excalidraw/mermaid/dagre-d3-es": ["dagre-d3-es@7.0.10", "", { "dependencies": { "d3": "^7.8.2", "lodash-es": "^4.17.21" } }, "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A=="], - - "@excalidraw/mermaid-to-excalidraw/mermaid/dompurify": ["dompurify@3.1.6", "", {}, "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ=="], - - "@excalidraw/mermaid-to-excalidraw/mermaid/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - "@radix-ui/react-roving-focus/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="], "@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], diff --git a/lib/diagram-render/dist/BUILD_INFO.json b/lib/diagram-render/dist/BUILD_INFO.json index b664e63092..4e635998c8 100644 --- a/lib/diagram-render/dist/BUILD_INFO.json +++ b/lib/diagram-render/dist/BUILD_INFO.json @@ -1,13 +1,13 @@ { "name": "gstack-diagram-render", - "sha256": "da9c363071afbe79e06807bd1e67dbacc1123187db7b99e2608dd4a1a9567e94", + "sha256": "90148189fc8688870cbb822593112b5621bcbf265f4c44f363598390e665d329", "srcSha256": "07238fae312bc0444f62b0a0a3404a8a38c45cef505aa1528c60a0ded17cbe06", - "bytes": 9645479, - "bunVersion": "1.3.13", + "bytes": 7953601, + "bunVersion": "1.3.14", "deps": { - "@excalidraw/excalidraw": "0.18.0", - "@excalidraw/mermaid-to-excalidraw": "1.1.2", - "mermaid": "11.12.2", + "@excalidraw/excalidraw": "0.18.1", + "@excalidraw/mermaid-to-excalidraw": "2.2.2", + "mermaid": "11.16.0", "react": "18.3.1", "react-dom": "18.3.1" } diff --git a/lib/diagram-render/dist/diagram-render.html b/lib/diagram-render/dist/diagram-render.html index 25de82b9f5..5997541bd4 100644 --- a/lib/diagram-render/dist/diagram-render.html +++ b/lib/diagram-render/dist/diagram-render.html @@ -20,11 +20,11 @@
loading
diff --git a/lib/diagram-render/package.json b/lib/diagram-render/package.json index f164ba7047..3aa6580f01 100644 --- a/lib/diagram-render/package.json +++ b/lib/diagram-render/package.json @@ -7,10 +7,15 @@ "build": "bun run scripts/build.ts" }, "dependencies": { - "@excalidraw/excalidraw": "0.18.0", - "@excalidraw/mermaid-to-excalidraw": "1.1.2", - "mermaid": "11.12.2", + "@excalidraw/excalidraw": "0.18.1", + "@excalidraw/mermaid-to-excalidraw": "2.2.2", + "mermaid": "11.16.0", "react": "18.3.1", "react-dom": "18.3.1" + }, + "overrides": { + "dompurify": "3.4.11", + "lodash-es": "4.18.1", + "nanoid": "5.0.9" } } diff --git a/package.json b/package.json index bd4ab1dcd2..f022449f28 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "server": "bun run browse/src/server.ts", "test": "bun run scripts/test-free-strict.ts", "check:gstack2-generated": "bun run scripts/gstack2/check-generated.ts", - "test:gstack2": "bun run gen:gstack2 && bun run check:gstack2-generated && bun test --timeout 30000 test/gstack2-*.test.ts", + "test:gstack2": "bun run gen:gstack2 && bun run check:gstack2-generated && bun run scripts/gstack2/test-suite.ts", "test:gstack2:install": "bun run scripts/gstack2/test-install-matrix.ts --full", "test:gstack2:parity": "bun run ensure:gstack2-runtime && bun run scripts/gstack2/run-parity.ts", "test:free": "bun run scripts/test-free-shards.ts", @@ -70,17 +70,26 @@ "slop:diff": "bun run scripts/slop-diff.ts" }, "dependencies": { - "@anthropic-ai/sdk": "^0.78.0", + "@anthropic-ai/sdk": "^0.112.4", "@ngrok/ngrok": "^1.7.0", "diff": "^9.0.0", "html-to-docx": "1.8.0", - "marked": "^18.0.2", + "marked": "^18.0.6", "playwright": "^1.58.2", "sharp": "^0.34.5", - "socks": "^2.8.8", + "socks": "^2.8.9", "xterm": "5", "xterm-addon-fit": "^0.8.0" }, + "overrides": { + "@protobufjs/utf8": "1.1.1", + "adm-zip": "0.6.0", + "fast-uri": "3.1.2", + "hono": "4.12.25", + "ip-address": "10.2.0", + "protobufjs": "7.6.5", + "qs": "6.15.2" + }, "engines": { "bun": ">=1.0.0", "node": ">=18.0.0" @@ -96,7 +105,7 @@ "devtools" ], "devDependencies": { - "@anthropic-ai/claude-agent-sdk": "0.2.117", - "@huggingface/transformers": "^4.1.0" + "@anthropic-ai/claude-agent-sdk": "0.3.216", + "@huggingface/transformers": "4.2.0" } } diff --git a/runtime/cli.js b/runtime/cli.js index 385a2933e3..dfdad40963 100644 --- a/runtime/cli.js +++ b/runtime/cli.js @@ -28,7 +28,6 @@ import { import { CAPABILITY_READINESS_CAPABILITIES, capabilityReadiness, - formatCapabilityReadiness, runDoctor, formatDoctor, } from "./doctor.js"; @@ -99,6 +98,11 @@ export async function main(argv = process.argv.slice(2), options = {}) { throw cliError(`Unknown command: ${command}`, "USAGE"); } } catch (error) { + const structuredResult = error?.executionResult ?? error?.cause?.executionResult; + if (structuredResult) { + write(stderr, renderExecutionResult(structuredResult, { json: true })); + return 1; + } const json = args.includes("--json"); const safeMessage = redactSensitiveText(error?.message ?? String(error)); if (json) { @@ -171,12 +175,47 @@ async function doctorCommand({ args, home, cwd, stdout }) { } const report = await runDoctor({ home, cwd, expectedSkillApi: parsed.values.get("--skill-api") }); const result = capability ? capabilityReadiness(report, capability) : report; + if (capability) { + const envelope = capabilityExecutionResult(result); + write(stdout, renderExecutionResult(envelope, { json: parsed.flags.has("--json") })); + return envelope.status === "success" ? 0 : 1; + } write(stdout, parsed.flags.has("--json") ? `${JSON.stringify(result, null, 2)}\n` - : capability ? formatCapabilityReadiness(result) : formatDoctor(result)); + : formatDoctor(result)); return result.ok ? 0 : 1; } +function capabilityExecutionResult(result) { + const statusByReadiness = { + ready: "success", + degraded: "degraded", + unavailable: "degraded", + unsupported: "unsupported", + failed: "failed", + }; + const codeByReadiness = { + ready: null, + degraded: EXECUTION_RESULT_ERROR_CODES.DEGRADED, + unavailable: EXECUTION_RESULT_ERROR_CODES.CAPABILITY_UNAVAILABLE, + unsupported: EXECUTION_RESULT_ERROR_CODES.CAPABILITY_UNSUPPORTED, + failed: EXECUTION_RESULT_ERROR_CODES.CAPABILITY_FAILED, + }; + const readiness = result.readiness.status; + const evidence = result.readiness.evidence?.map((item) => + `${item.id}: ${item.status} — ${item.message}`) ?? [ + `platform: ${result.platform.status}`, + `judgment: ${result.judgment.status}`, + ]; + return executionResult({ + status: statusByReadiness[readiness], + code: codeByReadiness[readiness], + summary: result.readiness.message, + evidence, + data: result, + }); +} + async function configCommand({ args, home, cwd, stdout }) { const [action, ...tail] = args; if (action === "get") { @@ -387,9 +426,12 @@ async function runExternalCommand(command, { cwd, env, stdout, stderr }) { child.once("close", (code, signal) => { if (code === 0) { if (captured.truncated) { - reject(cliError( + reject(executionResultError( `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, + "degraded", + ["output exceeded 1048576-byte capture limit"], + { executable: path.basename(executable), exitCode: 0, truncated: true }, )); return; } @@ -398,9 +440,12 @@ async function runExternalCommand(command, { cwd, env, stdout, stderr }) { if (captured.stderr.trim()) evidence.push("non-empty stderr"); const name = path.basename(executable); if (evidence.length === 0) { - reject(cliError( + reject(executionResultError( `External command ${name} exited 0 but produced no output; verify its side effect before reconciling it as applied.`, EXECUTION_RESULT_ERROR_CODES.EMPTY, + "degraded", + ["exit code 0", "stdout and stderr were empty"], + { executable: name, exitCode: 0, stdout: "", stderr: "" }, )); return; } @@ -417,15 +462,30 @@ async function runExternalCommand(command, { cwd, env, stdout, stderr }) { })); return; } - const error = cliError( + const error = executionResultError( `External command ${path.basename(executable)} ${signal ? `ended by ${signal}` : `exited ${code}`}`, - "EXTERNAL_COMMAND_FAILED", + EXECUTION_RESULT_ERROR_CODES.FAILED, + "failed", + [signal ? `terminated by signal ${signal}` : `exit code ${code}`], + { + executable: path.basename(executable), + exitCode: code, + signal: signal ?? null, + stdout: captured.stdout, + stderr: captured.stderr, + }, ); reject(error); }); }); } +function executionResultError(summary, code, status, evidence, data) { + const error = cliError(summary, code); + error.executionResult = executionResult({ status, code, summary, evidence, data }); + return error; +} + async function withOwnedRuntimeMutation(home, callback) { return withRuntimeLifecycleLock(home, async () => { await assertManagedHome(home); diff --git a/runtime/execution-result.js b/runtime/execution-result.js index dd92128809..89f603b7a6 100644 --- a/runtime/execution-result.js +++ b/runtime/execution-result.js @@ -14,6 +14,9 @@ export const EXECUTION_RESULT_ERROR_CODES = Object.freeze({ DEGRADED: "EXECUTION_DEGRADED", UNSUPPORTED: "EXECUTION_UNSUPPORTED", FAILED: "EXECUTION_FAILED", + CAPABILITY_UNAVAILABLE: "CAPABILITY_UNAVAILABLE", + CAPABILITY_UNSUPPORTED: "CAPABILITY_UNSUPPORTED", + CAPABILITY_FAILED: "CAPABILITY_FAILED", }); export const EXECUTION_RESULT_SCHEMA = Object.freeze({ diff --git a/runtime/install.js b/runtime/install.js index 8d3dbe2e6b..a5e05c1ace 100644 --- a/runtime/install.js +++ b/runtime/install.js @@ -281,6 +281,9 @@ export const DEFAULT_RUNTIME_BUNDLE = Object.freeze([ entry("node_modules/detect-libc"), entry("node_modules/semver"), entry("node_modules/@anthropic-ai/sdk"), + entry("node_modules/standardwebhooks"), + entry("node_modules/@stablelib/base64"), + entry("node_modules/fast-sha256"), entry(platformBinary("design/dist/design"), "core", true), entry("design/dist/.version", "core"), entry(platformBinary("make-pdf/dist/pdf"), "core", true), diff --git a/scripts/gstack2/ensure-runtime-payloads.ts b/scripts/gstack2/ensure-runtime-payloads.ts index c46984833f..b7884c2e0d 100644 --- a/scripts/gstack2/ensure-runtime-payloads.ts +++ b/scripts/gstack2/ensure-runtime-payloads.ts @@ -8,7 +8,12 @@ import { } from '../../runtime/install.js'; const ROOT = path.resolve(import.meta.dir, '../..'); -const REQUIRED_CAPABILITIES = ['browse', 'gstack-design', 'make-pdf'] as const; +const REQUIRED_CAPABILITIES = [ + 'browse', + 'gstack-design', + 'make-pdf', + ...(process.platform === 'darwin' ? ['gstack-ios-qa-daemon', 'gstack-ios-qa-mint'] : []), +] as const; export interface RuntimePayloadEntry { path: string; diff --git a/scripts/gstack2/test-suite.ts b/scripts/gstack2/test-suite.ts new file mode 100644 index 0000000000..72535eeec6 --- /dev/null +++ b/scripts/gstack2/test-suite.ts @@ -0,0 +1,22 @@ +#!/usr/bin/env bun + +import { collectFreeTestFiles } from '../test-free-shards'; +import { runStrictTestShard } from '../test-free-strict'; + +const GSTACK2_TEST_TIMEOUT_MS = 30_000; +const files = collectFreeTestFiles().filter((file) => /^test\/gstack2-.*\.test\.ts$/.test(file)); + +if (files.length === 0) { + throw new Error('No GStack 2 test files were discovered.'); +} + +console.log(`[test:gstack2] ${files.length} files across ${files.length} isolated processes`); +for (let index = 0; index < files.length; index += 1) { + const file = files[index]; + console.log(`[test:gstack2] file ${index + 1}/${files.length}: ${file}`); + const exitCode = await runStrictTestShard([file], GSTACK2_TEST_TIMEOUT_MS); + if (exitCode !== 0) { + process.exitCode = exitCode; + break; + } +} diff --git a/scripts/test-free-shards.ts b/scripts/test-free-shards.ts index 943f2ad4cf..c98ed9482b 100755 --- a/scripts/test-free-shards.ts +++ b/scripts/test-free-shards.ts @@ -302,8 +302,11 @@ export function planBoundedFreeTestShards( return shards; } -export function buildShardArgs(files: string[]): string[] { - return ['test', ...files, '--max-concurrency=1', `--timeout=${FREE_TEST_TIMEOUT_MS}`]; +export function buildShardArgs(files: string[], timeoutMs = FREE_TEST_TIMEOUT_MS): string[] { + if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) { + throw new Error(`Test timeout must be a positive integer. Received: ${timeoutMs}`); + } + return ['test', ...files, '--max-concurrency=1', `--timeout=${timeoutMs}`]; } type CliOptions = { diff --git a/scripts/test-free-strict.ts b/scripts/test-free-strict.ts index ba3a2fbbd0..ad0268d13d 100644 --- a/scripts/test-free-strict.ts +++ b/scripts/test-free-strict.ts @@ -261,9 +261,12 @@ export async function runDefaultFreeTests(): Promise { return slopSignal === null ? 0 : terminationSignalExitCode(slopSignal); } -export async function runStrictTestShard(files: string[]): Promise { +export async function runStrictTestShard( + files: string[], + timeoutMs?: number, +): Promise { if (files.length === 0) throw new Error('Cannot run an empty free-test shard.'); - const child = spawn(process.execPath, buildShardArgs(exactTestFileSelectors(files)), { + const child = spawn(process.execPath, buildShardArgs(exactTestFileSelectors(files), timeoutMs), { cwd: ROOT, env: process.env, stdio: ['inherit', 'pipe', 'pipe'], diff --git a/test/gstack2-capability-readiness.test.ts b/test/gstack2-capability-readiness.test.ts index 9691adfc6b..fc5e8ee736 100644 --- a/test/gstack2-capability-readiness.test.ts +++ b/test/gstack2-capability-readiness.test.ts @@ -101,10 +101,15 @@ describe("capability readiness", () => { expect(exit).toBe(1); expect(stderr.value()).toBe(""); expect(result).toMatchObject({ - ok: false, - capability: "pdf", - judgment: { status: "available" }, - readiness: { status: "unavailable" }, + schemaVersion: 1, + status: "degraded", + code: "CAPABILITY_UNAVAILABLE", + data: { + ok: false, + capability: "pdf", + judgment: { status: "available" }, + readiness: { status: "unavailable" }, + }, }); } finally { await fs.rm(root, { recursive: true, force: true }); diff --git a/test/gstack2-runtime-effect-cli.test.ts b/test/gstack2-runtime-effect-cli.test.ts index b1f8f4b3cb..0ff1edfaa6 100644 --- a/test/gstack2-runtime-effect-cli.test.ts +++ b/test/gstack2-runtime-effect-cli.test.ts @@ -107,11 +107,33 @@ describe('gstack state external-effect CLI', () => { '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(JSON.parse(err.value())).toMatchObject({ + status: 'degraded', + code: 'EXECUTION_EMPTY', + evidence: ['exit code 0', 'stdout and stderr were empty'], + }); expect(out.value()).not.toContain('"status":"success"'); + const retryError = sink(); 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'); + ], { cwd, env, stdout: out, stderr: retryError })).toBe(1); + expect(retryError.value()).toContain('was already claimed'); + }); + + test('a nonzero command returns a structured failed result', async () => { + const { cwd, env } = await fixture(); + const out = sink(); + const err = sink(); + await main(['state', 'begin', 'ship', '--run-id', 'run_failed'], { cwd, env, stdout: out, stderr: err }); + const code = await main([ + 'state', 'effect', 'run_failed', 'failed.tool', '--', process.execPath, '-e', 'process.exit(7)', + ], { cwd, env, stdout: out, stderr: err }); + expect(code).toBe(1); + expect(JSON.parse(err.value())).toMatchObject({ + status: 'failed', + code: 'EXECUTION_FAILED', + evidence: ['exit code 7'], + data: { exitCode: 7 }, + }); }); }); diff --git a/test/gstack2-runtime-install.test.ts b/test/gstack2-runtime-install.test.ts index fd2c71103c..307654d89c 100644 --- a/test/gstack2-runtime-install.test.ts +++ b/test/gstack2-runtime-install.test.ts @@ -376,6 +376,9 @@ describe("GStack 2 managed runtime installer", () => { "node_modules/sharp", "node_modules/detect-libc", "node_modules/semver", + "node_modules/standardwebhooks", + "node_modules/@stablelib/base64", + "node_modules/fast-sha256", ...runtimeNativePackagePaths(), ]) expect(bundlePaths.has(dependency)).toBe(true); expect(bundlePaths.has("node_modules/@img")).toBe(false); diff --git a/test/gstack2-runtime-payloads.test.ts b/test/gstack2-runtime-payloads.test.ts index 6d83d426c0..b2496730e1 100644 --- a/test/gstack2-runtime-payloads.test.ts +++ b/test/gstack2-runtime-payloads.test.ts @@ -45,7 +45,9 @@ describe('GStack 2 generated runtime payload prerequisites', () => { expect(result.built).toBe(true); expect(calls).toHaveLength(1); expect(calls[0].map((entry) => entry.path)).toEqual(REQUIRED_RUNTIME_PAYLOADS.map((entry) => entry.path)); - expect(new Set(calls[0].map((entry) => entry.build))).toEqual(new Set(['core'])); + expect(new Set(calls[0].map((entry) => entry.build))).toEqual( + new Set(process.platform === 'darwin' ? ['core', 'ios'] : ['core']), + ); }); test('does not rebuild payloads that already exist', async () => { diff --git a/test/gstack2-semantic-parity.test.ts b/test/gstack2-semantic-parity.test.ts index e9bc9eecd0..c4ea299a15 100644 --- a/test/gstack2-semantic-parity.test.ts +++ b/test/gstack2-semantic-parity.test.ts @@ -18,7 +18,7 @@ describe('GStack 2 semantic parity', () => { expect(result.sections).toBe(16); expect(result.policyUnits).toBe(AUTHORITY_POLICY_CASES.length); expect(result.checks).toBeGreaterThan(250); - }); + }, 15_000); test('authority-policy units cover evidence, trust, and routing controls', () => { expect(AUTHORITY_POLICY_CASES.length).toBeGreaterThanOrEqual(9); diff --git a/test/release-hardening.test.ts b/test/release-hardening.test.ts index 7db3fad03f..53edc09983 100644 --- a/test/release-hardening.test.ts +++ b/test/release-hardening.test.ts @@ -72,7 +72,8 @@ describe("release and CI hardening", () => { expect(workflow).toContain("versions/current.json"); expect(workflow).not.toContain('active="$GSTACK_HOME/versions/2.0.0"'); expect(workflow).toContain(".gstack-runtime-browsers"); - expect(workflow).toContain('chromium.launch({ headless: true, channel: "chromium" })'); + expect(workflow).toContain('for (const options of [{ headless: true }, { headless: true, channel: "chromium" }])'); + expect(workflow).toContain("chromium.launch(options)"); expect(workflow).not.toContain("--with-deps"); expect(workflow).toContain(".gstack-runtime-tools/bun"); expect(workflow).toContain('"$GSTACK_HOME/bin/bun" --version'); @@ -99,7 +100,9 @@ describe("release and CI hardening", () => { expect(installer).toContain('entry(managedBunRelativePath(), "managed-bun", true)'); const browser = read("browse/src/cli.ts"); expect(browser).toContain("Every installed/compiled client must use the adjacent Node-compatible daemon"); - expect(browser).toContain("if (IS_COMPILED && !NODE_SERVER_SCRIPT)"); + expect(browser).toContain("if (isCompiled)"); + expect(browser).toContain("if (!nodeServerScript)"); + expect(browser).toContain("return { isCompiled, nodeServerScript, sourceServerScript: null }"); }); test("Windows setup lane installs, doctors, and uninstalls rather than only building", () => { diff --git a/test/test-free-shards.test.ts b/test/test-free-shards.test.ts index 3fc540143a..0361ef2798 100644 --- a/test/test-free-shards.test.ts +++ b/test/test-free-shards.test.ts @@ -5,6 +5,7 @@ import * as os from 'os'; import { DEFAULT_MAX_FILES_PER_SHARD, FREE_TEST_ROOTS, + buildShardArgs, isFreeTestFile, collectFreeTestFiles, containsScheduledProcessExitZero, @@ -138,6 +139,16 @@ describe('test-free-shards: sharding', () => { expect(() => assignFilesToShards(['a.test.ts'], -1)).toThrow(); }); + test('buildShardArgs accepts a suite-specific timeout without enabling file concurrency', () => { + expect(buildShardArgs(['test/gstack2-skills.test.ts'], 30_000)).toEqual([ + 'test', + 'test/gstack2-skills.test.ts', + '--max-concurrency=1', + '--timeout=30000', + ]); + expect(() => buildShardArgs(['test/example.test.ts'], 0)).toThrow(); + }); + test('shards are stable across runs (same files always land in same shard)', () => { const files = ['x.test.ts', 'y.test.ts', 'z.test.ts']; const a = assignFilesToShards(files, 5); From d240b0fa6ae08f7e3581e4167541146e99e5d548 Mon Sep 17 00:00:00 2001 From: Sinabina Date: Tue, 21 Jul 2026 13:38:13 -0700 Subject: [PATCH 6/7] fix(security): clear post-merge OSV advisories New advisories landed after the integration work: fast-uri (High) and hono (3x Medium) picked up fixed releases, and a Medium surfaced on @hono/node-server. Bump the fast-uri and hono override pins to their fixed patch releases (3.1.3, 4.12.27), clearing four findings. The remaining @hono/node-server advisory is reachable only through the unused @modelcontextprotocol/sdk transitive (no source imports it, no Hono server is started); its only fix is a major bump the SDK pins against. Record that assessment in .osv-scanner.toml so the scheduled scan stays honest instead of alarming on an unreachable path. Co-Authored-By: Claude Opus 4.8 (1M context) --- .osv-scanner.toml | 15 +++++++++++++++ bun.lock | 8 ++++---- package.json | 4 ++-- 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 .osv-scanner.toml diff --git a/.osv-scanner.toml b/.osv-scanner.toml new file mode 100644 index 0000000000..56ca321230 --- /dev/null +++ b/.osv-scanner.toml @@ -0,0 +1,15 @@ +# OSV-Scanner configuration. +# Direct/transitive dependency versions are pinned to their fixed releases via +# the `overrides` block in package.json; this file only records advisories we +# have assessed as not-reachable or not-fixable without disproportionate risk. + +[[IgnoredVulns]] +id = "GHSA-frvp-7c67-39w9" +# @hono/node-server 1.19.x. Reachable only through @modelcontextprotocol/sdk, +# which is an unused transitive dependency (no source file imports it) and never +# starts a Hono HTTP server, so the advisory's request path is not exercised. +# The only fix is @hono/node-server 2.0.5, a major bump the MCP SDK pins against +# (^1.19.9); forcing it via override risks breaking the SDK at runtime for a +# vulnerability we do not expose. Re-evaluate if the MCP SDK becomes a direct, +# server-hosting dependency. +reason = "Unreachable transitive (unused @modelcontextprotocol/sdk); fix requires a risky major override on a pinned peer dep." diff --git a/bun.lock b/bun.lock index cc1adf6ce9..70409981ac 100644 --- a/bun.lock +++ b/bun.lock @@ -25,8 +25,8 @@ "overrides": { "@protobufjs/utf8": "1.1.1", "adm-zip": "0.6.0", - "fast-uri": "3.1.2", - "hono": "4.12.25", + "fast-uri": "3.1.3", + "hono": "4.12.27", "ip-address": "10.2.0", "protobufjs": "7.6.5", "qs": "6.15.2", @@ -276,7 +276,7 @@ "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], - "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + "fast-uri": ["fast-uri@3.1.3", "", {}, "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], @@ -310,7 +310,7 @@ "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], - "hono": ["hono@4.12.25", "", {}, "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ=="], + "hono": ["hono@4.12.27", "", {}, "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q=="], "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], diff --git a/package.json b/package.json index 127c1a8601..d7bc21cca9 100644 --- a/package.json +++ b/package.json @@ -84,8 +84,8 @@ "overrides": { "@protobufjs/utf8": "1.1.1", "adm-zip": "0.6.0", - "fast-uri": "3.1.2", - "hono": "4.12.25", + "fast-uri": "3.1.3", + "hono": "4.12.27", "ip-address": "10.2.0", "protobufjs": "7.6.5", "qs": "6.15.2" From 52a610170641ea370330a9e98b6a66dbdf3c736a Mon Sep 17 00:00:00 2001 From: Sinabina Date: Tue, 21 Jul 2026 13:59:29 -0700 Subject: [PATCH 7/7] fix(gstack2): treat hard runtime failure as not-ready in capability readiness A capability whose binary launches while the managed runtime hard-fails (e.g. skill-API mismatch) was reported as `degraded`/ok:true/exit 0, diverging from plain `gstack doctor` (ok:false/exit 1) for the identical report. Split the branch so runtime `warn` stays `degraded` and runtime `fail` maps to `failed`, and add a regression test for the runtime-fail + capability-pass case. Co-Authored-By: Claude Opus 4.8 (1M context) --- runtime/doctor.js | 3 ++- test/gstack2-capability-readiness.test.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/runtime/doctor.js b/runtime/doctor.js index 70fcff94b2..21ab58c985 100644 --- a/runtime/doctor.js +++ b/runtime/doctor.js @@ -247,7 +247,8 @@ export function capabilityReadiness(report, capability, options = {}) { const runtimeCheck = report.checks.find((check) => check.id === "managed-runtime"); let status; if (capabilityCheck?.status === "pass" && runtimeCheck?.status === "pass") status = "ready"; - else if (capabilityCheck?.status === "pass") status = "degraded"; + else if (capabilityCheck?.status === "pass" && runtimeCheck?.status === "warn") status = "degraded"; + else if (capabilityCheck?.status === "pass") status = "failed"; else if (capabilityCheck?.status === "fail") status = "failed"; else status = "unavailable"; diff --git a/test/gstack2-capability-readiness.test.ts b/test/gstack2-capability-readiness.test.ts index fc5e8ee736..444a14ab6e 100644 --- a/test/gstack2-capability-readiness.test.ts +++ b/test/gstack2-capability-readiness.test.ts @@ -52,11 +52,18 @@ describe("capability readiness", () => { { id: "managed-runtime", status: "pass", message: "active" }, { id: "capability:diagram", status: "fail", message: "launcher metadata missing" }, ]), "diagram"); + // A hard runtime failure under a launchable capability is not a warning: + // it must not report ok:true, matching plain `gstack doctor`'s exit code. + const runtimeFailed = capabilityReadiness(report([ + { id: "managed-runtime", status: "fail", message: "incompatible skill API" }, + { id: "capability:browser", status: "pass", message: "launched" }, + ]), "browser"); expect(ready).toMatchObject({ ok: true, readiness: { status: "ready" } }); expect(degraded).toMatchObject({ ok: true, readiness: { status: "degraded" } }); expect(failed).toMatchObject({ ok: false, readiness: { status: "failed" } }); expect(failed.consent.install.status).toBe("required-after-preview"); + expect(runtimeFailed).toMatchObject({ ok: false, readiness: { status: "failed" } }); }); test("reports physical iOS as unsupported without turning off pure judgment", () => {