mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 04:10:47 +02:00
feat: componentize GStack 2 runtime and release integrity
This commit is contained in:
@@ -25,6 +25,7 @@ import { GSTACK2_BASE_SHA, TREE_NAMES, type DispatcherDefinition, type SourceAss
|
||||
const GENERATED = '<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->';
|
||||
const DOCS = path.join(ROOT, 'docs', 'gstack-2');
|
||||
const EVALS = path.join(ROOT, 'evals', 'parity');
|
||||
const RUNTIME_SKILL_CONTRACT = { schemaVersion: 1, runtimeVersion: '2.0.0', skillApi: '2.0' } as const;
|
||||
|
||||
function sha256(value: string | Uint8Array): string {
|
||||
return createHash('sha256').update(value).digest('hex');
|
||||
@@ -128,7 +129,7 @@ function provenanceDetails(assignment: SourceAssignment, target: string): Record
|
||||
invocation_conditions: dispatcherMode?.inferWhen ?? `Internal compatibility invocation /${assignment.source}.`,
|
||||
modes: { public: assignment.publicMode, legacy_alias: assignment.mode },
|
||||
question_sequence: contract.question_order,
|
||||
follow_up_behavior: 'The complete source follow-up sequence is preserved verbatim inside new_location and hash-compared to the pinned base.',
|
||||
follow_up_behavior: 'Specialist follow-up order is preserved from the pinned source. The retired 1.x shared onboarding wrapper is excluded, and carved specialist phases are package-local lazy references loaded at their original workflow point.',
|
||||
smart_skip_rules: contract.smart_skips,
|
||||
pushback_rules: contract.pressure,
|
||||
stop_gates: contract.stop_approval_gates,
|
||||
@@ -142,7 +143,7 @@ function provenanceDetails(assignment: SourceAssignment, target: string): Record
|
||||
voice: contract.voice,
|
||||
response_posture: 'Direct, evidence-first builder language; preserve source-specific recommendations and constructive pressure.',
|
||||
new_location: target,
|
||||
parity_test: `${contractPath} + scripts/gstack2/run-parity.ts normalized full-body equality`,
|
||||
parity_test: `${contractPath} + scripts/gstack2/run-parity.ts canonical specialist render, excluded-wrapper, lazy-section, and pinned-source provenance checks`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -152,7 +153,7 @@ function renderModule(assignment: SourceAssignment): { content: string; renderSh
|
||||
const overlays = overlaysForSource(assignment.source);
|
||||
const disposition = assignment.source === 'gstack-upgrade'
|
||||
? 'DUPLICATE_INFRASTRUCTURE'
|
||||
: overlays.length ? 'BUG_FIX' : 'MECHANICAL_PORT';
|
||||
: overlays.length ? 'BUG_FIX' : 'JUDGMENT_PRESERVING_CARVE';
|
||||
const overlayText = overlays.map((overlay) => [
|
||||
`<!-- GSTACK2_BUG_FIX_START pr=${overlay.pr} anchor=${overlay.anchor} -->`,
|
||||
`## Upstream judgment port: PR #${overlay.pr}`,
|
||||
@@ -229,10 +230,10 @@ Web context: <none, optional, local-browser, or production>
|
||||
|
||||
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 legacy body, behavioral contract, STOP gates, and appended upstream judgment ports are binding.
|
||||
4. Read \`references/SHARED-JUDGMENT.md\` and \`references/AUTHORITY-POLICY.md\` for every invocation. Read \`references/WEB-CONTEXT.md\` before public-web or optional-runtime work.
|
||||
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.
|
||||
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. Commits, pushes, PRs, merges, deploys, messages, and other external mutations still require the authority stated by the active module and the user.
|
||||
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.
|
||||
8. At exit, report completed artifacts, evidence, unresolved decisions, skipped modules with reasons, and any blocked gate.
|
||||
${supplemental}
|
||||
@@ -257,7 +258,7 @@ ${internalRows}
|
||||
|
||||
## Completeness invariant
|
||||
|
||||
Do not work from this dispatcher summary when a module is active. Read the referenced module completely, including its provenance marker, behavioral contract, full mechanically rendered source, and bug-fix overlays.
|
||||
Do not work from this dispatcher summary when a module is active. Read the referenced module completely, including its provenance marker, specialist workflow, lazy-phase directives, behavioral contract, and bug-fix overlays. The pinned 1.x shared onboarding wrapper is provenance-only and never runs during canonical execution.
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -481,11 +482,37 @@ function webContextContract(): string {
|
||||
'',
|
||||
'Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent.',
|
||||
'',
|
||||
'Capability-dependent work performs one host-neutral runtime check. Pure judgment never requires the runtime. If the runtime is absent, offer ./setup from a trusted GStack checkout; skill placement remains npx skills add time-attack/gstack.',
|
||||
'Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack` and is never inferred from runtime state.',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function runtimeContract(): string {
|
||||
return `${GENERATED}
|
||||
# Optional runtime capabilities
|
||||
|
||||
The six Agent Skills are useful without a GStack runtime. Never install, download, build, select, update, or remove runtime capabilities merely because a skill was invoked.
|
||||
|
||||
When an active specialist first reaches a capability it cannot use, name the exact capability and why it is needed. Offer to continue without it when the judgment-only or host-native path remains valid. Before any network preview, ask whether the user wants to check official setup options and exact sizes. Disclose that an uncached preview makes one public GitHub request for signed manifest metadata and sends no repository content, private URL, file, cookie, token, or credential; then STOP. A cached already-verified manifest may preview offline, but never silently fetch.
|
||||
|
||||
Only after the user approves that metadata check, run the non-mutating preview from this skill root: \`node references/support/runtime-bootstrap.mjs preview --capability <name>\` (repeat \`--capability\` for additional requested capabilities). It dependency-expands, reports already verified local components, exact missing components, and their summed compressed bytes. It never downloads components or mutates runtime state. Preview consent is not install consent.
|
||||
|
||||
User-facing setup capabilities are exactly \`browser\`, \`design\`, \`diagram\`, \`pdf\`, and \`ios\`. \`all\` means those five and intentionally excludes visible Chromium. The internal \`browser-visible\` capability is additive and is offered only when a workflow actually reaches a headed browser, extension, or browser-handoff step. Never offer it during ordinary headless QA.
|
||||
|
||||
After showing the complete preview, STOP for explicit approval. Only after approval run \`node references/support/runtime-bootstrap.mjs install --capability <name> --yes\`; install must reprint the identical dependency-closed plan before downloading. Signed internal components are \`core\`, \`browser-code\` (browse code and dependencies), \`browser-headless\` (Playwright headless shell and FFmpeg), \`browser-visible\` (full Chromium), \`design\`, \`diagram\`, \`pdf\`, and \`ios\`. Logical \`browser\` expands to \`browser-code + browser-headless\`; internal \`browser-visible\` expands to \`browser-code + browser-visible\` and does not require headless. Component dependencies are \`browser-code → core\`, \`browser-headless → browser-code\`, and \`browser-visible → browser-code\`. \`diagram\` depends on logical \`browser\`; \`pdf\` depends on \`diagram\`; \`ios\` is Darwin-only. Therefore a first-time headed flow previews \`core + browser-code + browser-visible\`, while an existing verified headless runtime downloads only missing \`browser-visible\`. The manifest schema is v2 with global \`capabilityComponents\` and \`componentDependencies\`, plus \`targets[target].components[id]\` carrying signed exact-byte artifacts.
|
||||
|
||||
The dependency-free Node 18+ bootstrap accepts only official GStack GitHub Release hosts, validates manifest/runtime/skill API/platform/component metadata and SHA-256, verifies a declared Cosign bundle when Cosign is available, rejects unsafe archive paths/links, and invokes the atomic managed installer. Never run \`./setup\` inside a standard-installed skill directory, enroll a host, or treat skill installation as runtime consent.
|
||||
|
||||
After doctor confirms readiness, canonical launcher bindings are \`GSTACK_HOME=\${GSTACK_HOME:-$HOME/.gstack}\`, \`GSTACK_BIN=$GSTACK_HOME/bin\`, \`BUN_CMD=$GSTACK_BIN/bun\`, \`B=$GSTACK_BIN/browse\`, \`D=$GSTACK_BIN/gstack-design\`, and \`P=$GSTACK_BIN/make-pdf\`. Specialist modules may bind only the launchers they use. These assignments do not install or grant consent. Never download or install a second Bun for GStack helpers; the managed runtime owns its pinned Bun executable in the active slot.
|
||||
|
||||
Some retained helpers are shell scripts. \`gstack doctor\` verifies Bash and, on Windows, discovers Git for Windows Bash; disclose and stop at the affected helper if that prerequisite is unavailable. Python is not a global GStack prerequisite: only a specialist flow explicitly labeled as Python-dependent may request it, at the point of use and with the user's approval.
|
||||
|
||||
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.
|
||||
|
||||
The developer-only fallback is \`node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes\`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment.
|
||||
`;
|
||||
}
|
||||
|
||||
function systemFunctionalContract(): string {
|
||||
return `${GENERATED}
|
||||
# System-functional QA
|
||||
@@ -531,10 +558,14 @@ Do not put secrets in run IDs, effect keys, or command arguments. Existing appro
|
||||
}
|
||||
|
||||
function writeSharedContracts(): void {
|
||||
const bootstrap = fs.readFileSync(path.join(ROOT, 'runtime', 'runtime-bootstrap.mjs'));
|
||||
for (const tree of TREE_NAMES) {
|
||||
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());
|
||||
write(path.join(ROOT, 'skills', tree, 'references', 'RUNTIME.md'), runtimeContract());
|
||||
write(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-bootstrap.mjs'), bootstrap);
|
||||
writeJson(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-contract.json'), RUNTIME_SKILL_CONTRACT);
|
||||
}
|
||||
write(path.join(ROOT, 'skills', 'qa', 'references', 'SYSTEM-FUNCTIONAL.md'), systemFunctionalContract());
|
||||
write(path.join(ROOT, 'skills', 'ship', 'references', 'EXTERNAL-EFFECTS.md'), externalEffectsContract());
|
||||
@@ -663,7 +694,7 @@ function migrationDoc(): string {
|
||||
|
||||
Pinned baseline: \`${GSTACK2_BASE_SHA}\`.
|
||||
|
||||
GStack 2 exposes exactly six public Codex skills: \`plan\`, \`design\`, \`qa\`, \`debug\`, \`review\`, and \`ship\`. The 55 legacy templates remain mechanically rendered as internal reference modules; all 16 carved section templates are inlined with the canonical Codex resolver path. Thirty-one primary modules are mandatory specialist inputs, and 24 supporting modules remain reachable through compatibility routing.
|
||||
GStack 2 exposes exactly six public skills: \`plan\`, \`design\`, \`qa\`, \`debug\`, \`review\`, and \`ship\`. The specialist bodies from 55 legacy templates remain provenance-pinned internal reference modules. The retired 1.x shared onboarding wrapper is excluded from canonical execution, and all 16 carved specialist sections are package-local lazy references loaded only at their original workflow point. Thirty-one primary modules are mandatory specialist inputs, and 24 supporting modules remain reachable through compatibility routing.
|
||||
|
||||
The fixed public modes are: Design = \`Explore | Generate | Critique | Implement\`; QA = \`Report | Fix\`; Debug = \`Diagnose-only | Fix\`; Review = \`Normal | Security | Performance | Deep\`; Ship = \`Prepare | Land | Deploy | Monitor | Resume\`. Richer legacy modes are internal aliases only.
|
||||
|
||||
@@ -681,8 +712,8 @@ ${rows}
|
||||
|
||||
## Mechanical versus judgment changes
|
||||
|
||||
- \`MECHANICAL_PORT\`: canonical Codex resolver expansion, section inlining, safety prose, and path rewrites only.
|
||||
- \`BUG_FIX\`: the mechanical body plus a clearly delimited judgment overlay sourced from one of the 16 upstream PRs and its regression fixture.
|
||||
- \`JUDGMENT_PRESERVING_CARVE\`: pinned specialist workflow with the retired shared onboarding wrapper excluded, retired invocations resolved to six public routes, host/runtime paths normalized, and large carved phases loaded lazily from package-local pinned references.
|
||||
- \`BUG_FIX\`: the canonical carved body plus a clearly delimited judgment overlay sourced from one of the 16 upstream PRs and its regression fixture.
|
||||
- Asset relocation is byte-for-byte from the pinned Git blob and is indexed per tree.
|
||||
`;
|
||||
}
|
||||
@@ -709,14 +740,14 @@ The suite verifies:
|
||||
|
||||
- exactly six discoverable public skills and 55 internal legacy modules;
|
||||
- 55 canonical templates plus 16 carved section templates at base \`${GSTACK2_BASE_SHA}\`;
|
||||
- normalized legacy-body SHA-256 equality between source rendering and generated references;
|
||||
- immutable full 1.x render hashes plus canonical specialist-render equality, with the excluded onboarding wrapper and lazy section references asserted explicitly;
|
||||
- preservation of nine behavioral contract dimensions per module;
|
||||
- 25 structured non-keyword routing fixtures with active/skipped modules, depth, mutation, and web context;
|
||||
- 16 upstream judgment-port regression fixtures and anchors;
|
||||
- all linked asset copies against their pinned Git blobs;
|
||||
- frontmatter and \`agents/openai.yaml\` schema for each public skill.
|
||||
|
||||
Golden normalization removes only generated provenance wrappers, bug-fix overlays, and irrelevant whitespace. It never removes legacy workflow prose, gates, questions, evidence requirements, artifacts, or exit behavior.
|
||||
Golden normalization removes only generated provenance wrappers, bug-fix overlays, and irrelevant whitespace. Canonical rendering separately excludes the retired shared onboarding wrapper and packages large specialist phases as lazy references; parity checks both decisions explicitly without claiming full-body byte equality.
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -817,9 +848,7 @@ function main(): void {
|
||||
const sectionRecords = legacySections().map((section) => {
|
||||
const parent = sourceRecords.find((entry) => entry.source === section.source);
|
||||
if (!parent) throw new Error(`Section without parent assignment: ${section.relativePath}`);
|
||||
const generated = fs.readFileSync(path.join(ROOT, String(parent.target)), 'utf8');
|
||||
const portedSection = renderPortedLegacySection(section);
|
||||
if (!generated.includes(portedSection.trim())) throw new Error(`Rendered section missing from ${parent.target}: ${section.relativePath}`);
|
||||
const packagedTargets = sectionCopies.filter((copy) => copy.source_path === section.relativePath).map((copy) => copy.target_path);
|
||||
const ownerTarget = packagedTargets.find((target) => target.startsWith(`skills/${String(parent.tree)}/`));
|
||||
if (!ownerTarget) throw new Error(`Section was not packaged with its owner: ${section.relativePath}`);
|
||||
@@ -831,21 +860,21 @@ function main(): void {
|
||||
normalized_render_sha256: sha256(section.rendered),
|
||||
ported_render_sha256: sha256(portedSection),
|
||||
target: ownerTarget,
|
||||
inlined_module_target: parent.target,
|
||||
lazy_reference_module_target: parent.target,
|
||||
packaged_targets: packagedTargets,
|
||||
disposition: 'MECHANICAL_PORT',
|
||||
disposition: 'JUDGMENT_PRESERVING_CARVE',
|
||||
original_source_file: section.relativePath,
|
||||
original_line_range: sourceLineRange(section.relativePath),
|
||||
purpose: `Carved specialist section from ${section.source}, mechanically inlined into its preserved module.`,
|
||||
purpose: `Carved specialist section from ${section.source}, packaged behind a lazy reference at its original workflow point.`,
|
||||
invocation_conditions: `Loaded only when the parent ${section.source} workflow reaches this carved section.`,
|
||||
modes: { parent: section.source },
|
||||
question_sequence: 'Preserved verbatim in the inlined section.',
|
||||
follow_up_behavior: 'Preserved verbatim in the inlined section.',
|
||||
question_sequence: 'Preserved verbatim in the package-local lazy section.',
|
||||
follow_up_behavior: 'Preserved verbatim in the package-local lazy section and loaded only when its original phase is reached.',
|
||||
smart_skip_rules: 'Inherited unchanged from the parent specialist workflow.',
|
||||
pushback_rules: 'Inherited unchanged from the parent specialist workflow.',
|
||||
stop_gates: 'Inherited unchanged from the parent specialist workflow.',
|
||||
approval_gates: 'Inherited unchanged from the parent specialist workflow.',
|
||||
rubrics_and_scoring: 'Preserved verbatim in the inlined section.',
|
||||
rubrics_and_scoring: 'Preserved verbatim in the package-local lazy section.',
|
||||
cognitive_frameworks: sourceHeadings(section.rendered),
|
||||
evidence_requirements: 'Inherited unchanged from the parent specialist workflow.',
|
||||
artifacts_produced: 'Inherited unchanged from the parent specialist workflow.',
|
||||
@@ -854,7 +883,7 @@ function main(): void {
|
||||
voice: 'Inherited unchanged from the parent specialist workflow.',
|
||||
response_posture: 'Inherited unchanged from the parent specialist workflow.',
|
||||
new_location: ownerTarget,
|
||||
parity_test: `scripts/gstack2/run-parity.ts exact packaged-section, inline inclusion, and Git-blob checks`,
|
||||
parity_test: `scripts/gstack2/run-parity.ts exact packaged-section, lazy-reference, and Git-blob checks`,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -886,7 +915,7 @@ function main(): void {
|
||||
write(path.join(DOCS, 'JUDGMENT-PARITY.md'), parityDoc(assets.length));
|
||||
write(path.join(DOCS, 'SCENARIOS.md'), scenarioDoc());
|
||||
const semantic = runDeterministicSemanticParity(true);
|
||||
process.stdout.write(`Generated 6 dispatchers, ${sourceRecords.length} modules, ${sectionRecords.length} inlined sections, ${SCENARIOS.length} scenarios, ${BUG_FIX_OVERLAYS.length} bug-fix ports, and ${assets.length} asset copies.\n`);
|
||||
process.stdout.write(`Generated 6 dispatchers, ${sourceRecords.length} modules, ${sectionRecords.length} lazy specialist sections, ${SCENARIOS.length} scenarios, ${BUG_FIX_OVERLAYS.length} bug-fix ports, and ${assets.length} asset copies.\n`);
|
||||
process.stdout.write(`Generated semantic evidence: ${semantic.checks} checks across ${semantic.suites} suites and ${semantic.policyUnits} authority-policy unit cases.\n`);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getHostConfig } from '../../hosts/index';
|
||||
import { extractHookSafetyProse, extractNameAndDescription } from '../resolvers/codex-helpers';
|
||||
import { RESOLVERS } from '../resolvers/index';
|
||||
import { HOST_PATHS, unwrapResolver, type TemplateContext } from '../resolvers/types';
|
||||
import { SOURCE_ASSIGNMENTS } from './assignments';
|
||||
import { GSTACK2_BASE_SHA } from './types';
|
||||
|
||||
export const ROOT = path.resolve(import.meta.dir, '..', '..');
|
||||
@@ -70,13 +71,21 @@ function buildContext(tmplContent: string, tmplPath: string): TemplateContext {
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePlaceholders(template: string, context: TemplateContext, relativePath: string): string {
|
||||
type ResolverOverrides = Readonly<Record<string, string>>;
|
||||
|
||||
function resolvePlaceholders(
|
||||
template: string,
|
||||
context: TemplateContext,
|
||||
relativePath: string,
|
||||
overrides: ResolverOverrides = {},
|
||||
): string {
|
||||
const config = getHostConfig('codex');
|
||||
const suppressed = new Set(config.suppressedResolvers ?? []);
|
||||
const onePass = (input: string): string => input.replace(
|
||||
/\{\{(\w+(?::[^}]+)?)\}\}/g,
|
||||
(_match, fullKey: string) => {
|
||||
const [resolverName, ...args] = fullKey.split(':');
|
||||
if (Object.hasOwn(overrides, resolverName)) return overrides[resolverName];
|
||||
if (suppressed.has(resolverName)) return '';
|
||||
const entry = RESOLVERS[resolverName];
|
||||
if (!entry) throw new Error(`Unknown placeholder {{${resolverName}}} in ${relativePath}`);
|
||||
@@ -97,6 +106,53 @@ function resolvePlaceholders(template: string, context: TemplateContext, relativ
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* The 1.x PREAMBLE resolver is host installation and engagement machinery, not
|
||||
* specialist judgment. It performs first-run writes, telemetry/proactive
|
||||
* prompts, update checks, model overlays, checkpoint promotion, artifact-sync
|
||||
* enrollment, and CLAUDE.md mutation. GStack 2 keeps that immutable expansion
|
||||
* as the parity oracle while deliberately excluding it from normal canonical
|
||||
* execution. Runtime and authority are owned once by package-local contracts.
|
||||
*/
|
||||
export const CANONICAL_EXCLUDED_RESOLVERS = ['PREAMBLE'] as const;
|
||||
|
||||
const RETIRED_ASSIGNMENTS = [...SOURCE_ASSIGNMENTS]
|
||||
.filter((entry) => !['plan', 'design', 'qa', 'debug', 'review', 'ship'].includes(entry.source))
|
||||
.sort((a, b) => b.source.length - a.source.length);
|
||||
|
||||
export function retiredInvocationPattern(): RegExp {
|
||||
const sources = RETIRED_ASSIGNMENTS
|
||||
.map((entry) => entry.source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
||||
.join('|');
|
||||
return new RegExp(`(?<![A-Za-z0-9_.-])/(?:${sources})(?=$|[\\s\\x60'\"),.;:*_|\\]}>])`);
|
||||
}
|
||||
|
||||
export function replaceRetiredInvocations(value: string): string {
|
||||
let rewritten = value;
|
||||
for (const assignment of RETIRED_ASSIGNMENTS) {
|
||||
// A preceding word/path character means this slash is part of a file path
|
||||
// or URL, not an invocation. The trailing set includes Markdown emphasis,
|
||||
// table, and link punctuation in addition to prose punctuation.
|
||||
rewritten = rewritten.replace(
|
||||
new RegExp(`(?<![A-Za-z0-9_.-])/${assignment.source}(?=$|[\\s\\x60'\"),.;:*_|\\]}>])`, 'g'),
|
||||
assignment.replacement,
|
||||
);
|
||||
}
|
||||
return rewritten;
|
||||
}
|
||||
|
||||
function renderCanonicalSpecialistBody(source: string): string {
|
||||
const templatePath = legacyTemplatePath(source);
|
||||
const relativePath = repositoryRelativePath(templatePath);
|
||||
const template = pinnedText(relativePath);
|
||||
const context = buildContext(template, templatePath);
|
||||
const overrides = Object.fromEntries(CANONICAL_EXCLUDED_RESOLVERS.map((name) => [name, '']));
|
||||
// Hook advisories are part of the host-specific 1.x wrapper. Canonical
|
||||
// authority/safety policy is generated once beside every public dispatcher.
|
||||
const body = stripFrontmatter(resolvePlaceholders(template, context, relativePath, overrides));
|
||||
return `${applyCodexRewrites(body).trim()}\n`;
|
||||
}
|
||||
|
||||
function applyCodexRewrites(content: string): string {
|
||||
const config = getHostConfig('codex');
|
||||
let rewritten = content;
|
||||
@@ -135,6 +191,11 @@ function portLegacyText(value: string, source: string): string {
|
||||
return `# Legacy upgrade compatibility\n\nThe 1.x host-directory detector, vendored-copy synchronizer, and destructive Git replacement blocks were duplicated installation infrastructure. GStack 2 delegates skill placement and updates to the standard Agent Skills installer and manages the optional shared runtime atomically.\n\n- Update selected skills with \`npx skills add time-attack/gstack\` using the user's existing project/global choice. Never infer or enroll a host.\n- Upgrade a complete local runtime package with \`gstack upgrade --source <complete-gstack-package> --version <version>\`.\n- Roll back the runtime with \`gstack upgrade --rollback\`.\n- Run \`gstack doctor\` after either operation.\n- Do not reset, delete, move, or rewrite a host skill directory. Do not infer Context.dev choice or consent.\n\nThis compatibility module contains no specialist judgment; release readiness and rollback judgment remain in the preserved ship modules.\n`;
|
||||
}
|
||||
let body = value;
|
||||
|
||||
// Retired names remain valid only as opt-in compatibility aliases. Normal
|
||||
// canonical execution must recommend one of the six public routes, with the
|
||||
// exact internal refinement retained for deterministic dispatch.
|
||||
body = replaceRetiredInvocations(body);
|
||||
// Every state read/write follows the canonical override. Quote the root so
|
||||
// custom homes containing spaces remain valid. Compatibility pointer files
|
||||
// such as ~/.gstack-artifacts-remote.txt are outside this state root and are
|
||||
@@ -237,6 +298,36 @@ function portLegacyText(value: string, source: string): string {
|
||||
.replaceAll('~/.codex/skills/gstack/browse-remote.json', '${GSTACK_HOME:-$HOME/.gstack}/browse-remote.json')
|
||||
.replaceAll('${HOME}/.agents/skills/gstack/document-release/SKILL.md', 'references/legacy/document-release.md');
|
||||
|
||||
// The managed runtime owns a pinned Bun executable. Remove the old
|
||||
// per-skill curl installer and route GStack-owned helpers through their
|
||||
// stable launchers. Project-owned commands such as a repository's `bun
|
||||
// test` are intentionally untouched.
|
||||
body = body
|
||||
.replace(
|
||||
/3\. If `bun` is not installed:\n\s+```bash\n[\s\S]*?\n\s+```/g,
|
||||
'3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.',
|
||||
)
|
||||
.replaceAll('bun run $GSTACK_BIN/gstack-gbrain-sync.ts', '$GSTACK_BIN/gstack-gbrain-sync')
|
||||
.replaceAll('bun run $GSTACK_BIN/gstack-next-version', '$GSTACK_BIN/gstack-next-version')
|
||||
.replaceAll('bun run $GSTACK_BIN/gstack-version-bump', '$GSTACK_BIN/gstack-version-bump')
|
||||
.replaceAll('DISCOVER_BIN="bun run $GSTACK_BIN/gstack-global-discover"', 'DISCOVER_BIN="$GSTACK_BIN/gstack-global-discover"')
|
||||
.replaceAll('Tell the user: "gstack browse needs a one-time build (~10 seconds). OK to proceed?" Then STOP and wait.', 'Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.')
|
||||
.replace(/^command -v bun >\/dev\/null 2>&1 \|\| echo "redaction scan skipped — bun not on PATH"\n/gm, '');
|
||||
|
||||
body = body
|
||||
.replaceAll(
|
||||
'Run: `cd <SKILL_DIR> && ./setup`',
|
||||
'Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.',
|
||||
)
|
||||
.replaceAll(
|
||||
'run `cd <SKILL_DIR> && ./setup`',
|
||||
'read `references/RUNTIME.md` and follow its explicit capability bootstrap; never assume a standard-installed skill directory contains `./setup`',
|
||||
)
|
||||
.replaceAll(
|
||||
'Run `cd <SKILL_DIR> && ./setup`',
|
||||
'Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.',
|
||||
);
|
||||
|
||||
for (const helper of [
|
||||
'gstack-codex-probe',
|
||||
'gstack-global-discover.ts',
|
||||
@@ -249,6 +340,12 @@ function portLegacyText(value: string, source: string): string {
|
||||
const stableName = helper === 'gstack-global-discover.ts' ? 'gstack-global-discover' : helper;
|
||||
body = body.replaceAll(`bin/${helper}`, `$GSTACK_BIN/${stableName}`);
|
||||
}
|
||||
body = body
|
||||
.replaceAll('bun run $GSTACK_BIN/gstack-gbrain-sync.ts', '$GSTACK_BIN/gstack-gbrain-sync')
|
||||
.replaceAll('bun run $GSTACK_BIN/gstack-gbrain-sync', '$GSTACK_BIN/gstack-gbrain-sync')
|
||||
.replaceAll('bun run $GSTACK_BIN/gstack-next-version', '$GSTACK_BIN/gstack-next-version')
|
||||
.replaceAll('bun run $GSTACK_BIN/gstack-version-bump', '$GSTACK_BIN/gstack-version-bump')
|
||||
.replaceAll('DISCOVER_BIN="bun run $GSTACK_BIN/gstack-global-discover"', 'DISCOVER_BIN="$GSTACK_BIN/gstack-global-discover"');
|
||||
|
||||
body = body.replace(
|
||||
/BUNDLE=""\nfor c in "\$HOME\/\.agents\/skills\/gstack\/lib\/diagram-render\/dist\/diagram-render\.html" \\\n\s+"\$\(git rev-parse --show-toplevel 2>\/dev\/null\)\/lib\/diagram-render\/dist\/diagram-render\.html"; do\n\s+\[ -f "\$c" \] && BUNDLE="\$c" && break\ndone/,
|
||||
@@ -288,11 +385,92 @@ function portLegacyText(value: string, source: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
// Canonical skills do not run engagement instrumentation. Remove the few
|
||||
// source-local remnants that are outside {{PREAMBLE}} while leaving actual
|
||||
// specialist reports, requested context saves, and explicit setup modules
|
||||
// intact.
|
||||
body = body.replace(/```bash\n([\s\S]*?)```/g, (block, commands: string) => {
|
||||
const writesAnalytics = /(?:mkdir\s+-p|>>)[^\n]*\/(?:analytics|skill-usage\.jsonl)/.test(commands);
|
||||
return writesAnalytics
|
||||
? 'Canonical execution does not write engagement analytics or telemetry.'
|
||||
: block;
|
||||
});
|
||||
body = body
|
||||
.replace(/^.*gstack-telemetry-log.*\n?/gm, '')
|
||||
.replace(/^\s*_gstack_codex_log_event\s+.*\n?/gm, '')
|
||||
.replace(/^_TEL=\$\([^\n]*\)$/gm, '_TEL=off # Canonical execution does not emit GStack telemetry.')
|
||||
.replace(/Best-effort, record which way you routed[\s\S]*?directly, no skill matched\):\nCanonical execution does not write engagement analytics or telemetry\.\n*/g, '')
|
||||
.replace(/If `PROACTIVE` is `false`:[\s\S]*?Use the Skill tool to invoke it\. The skill has specialized workflows, checklists, and\nquality gates that produce better results than answering inline\.\n\n/g, '')
|
||||
.replace(/\nIf the user opts out of suggestions,[\s\S]*?gstack-config set proactive true`\.\n?/g, '\n')
|
||||
.replace(/#### TTHW telemetry \(DX11\/F7\)[\s\S]*?\n---\n/g, '---\n')
|
||||
.replace(/\n3\. Append metrics:\nCanonical execution does not write engagement analytics or telemetry\.\nReplace ITERATIONS,[\s\S]*?actual values from the review\.\n/g, '\n3. Report the iteration counts and quality score in the user-facing result; do not persist engagement analytics.\n');
|
||||
|
||||
return `${body.trim()}\n`;
|
||||
}
|
||||
|
||||
export function renderPortedLegacyBody(source: string): string {
|
||||
return portLegacyText(renderLegacyBody(source), source);
|
||||
let body = portLegacyText(renderCanonicalSpecialistBody(source), source);
|
||||
if (source === 'make-pdf') {
|
||||
body = [
|
||||
'## Optional runtime binding',
|
||||
'',
|
||||
'This workflow requires the `pdf` capability. Read `references/RUNTIME.md` before attempting installation. Pure planning or document review can continue without it.',
|
||||
'If unavailable, follow the consent-first `pdf` capability handoff in `references/RUNTIME.md`. It expands to `pdf`, `diagram`, and `browser`; do not guess a checkout-relative setup command.',
|
||||
'',
|
||||
body,
|
||||
].join('\n');
|
||||
}
|
||||
body = body.replace(
|
||||
'`./setup` auto-installs `fonts-noto-color-emoji` on Linux',
|
||||
'the explicitly approved `pdf` runtime capability attempts to install `fonts-noto-color-emoji` on Linux',
|
||||
);
|
||||
if (['open-gstack-browser', 'pair-agent', 'setup-browser-cookies'].includes(source)) {
|
||||
body = [
|
||||
'## Visible-browser point-of-use gate',
|
||||
'',
|
||||
'This workflow may require internal `browser-visible` because it reaches a headed browser, extension, interactive cookie picker, or browser handoff. Do not offer visible Chromium during ordinary headless QA.',
|
||||
'',
|
||||
'At the first actual visible-browser step, ask whether the user wants to check official setup options and exact sizes. Disclose that an uncached preview makes one public GitHub signed-manifest request and sends no repository/private data, then STOP. Only after that approval run `node references/support/runtime-bootstrap.mjs preview --capability browser-visible`. It expands to `core + browser-code + browser-visible` for a first install, but an existing verified headless runtime downloads only missing `browser-visible`; it never requires `browser-headless`. Show the exact missing components and summed incremental compressed bytes, then STOP again for separate install approval. Only after install approval run `node references/support/runtime-bootstrap.mjs install --capability browser-visible --yes`, recheck readiness, and resume the interrupted step.',
|
||||
'',
|
||||
body,
|
||||
].join('\n');
|
||||
}
|
||||
if (/\$(?:GSTACK_BIN|GSTACK_ROOT|GSTACK_STATE_ROOT)\b|\$(?:B|D|P)\b/.test(body)) {
|
||||
const bindings = [
|
||||
'## Host-neutral runtime bindings',
|
||||
'',
|
||||
'These assignments select stable paths only; they do not install anything or grant consent:',
|
||||
'',
|
||||
'```bash',
|
||||
'GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"',
|
||||
'GSTACK_ROOT="$GSTACK_HOME"',
|
||||
'GSTACK_STATE_ROOT="$GSTACK_HOME"',
|
||||
'GSTACK_BIN="$GSTACK_HOME/bin"',
|
||||
'BUN_CMD="$GSTACK_BIN/bun"',
|
||||
'B="$GSTACK_BIN/browse"',
|
||||
'D="$GSTACK_BIN/gstack-design"',
|
||||
'P="$GSTACK_BIN/make-pdf"',
|
||||
'```',
|
||||
'',
|
||||
].join('\n');
|
||||
body = `${bindings}${body}`;
|
||||
}
|
||||
// Large specialist phases remain byte-derived from the pinned source, but
|
||||
// are packaged as lazy references instead of duplicated inline. The active
|
||||
// workflow loads one only when it reaches that phase.
|
||||
for (const section of legacySections().filter((entry) => entry.source === source)) {
|
||||
const ported = portLegacyText(section.rendered, section.source).trim();
|
||||
const filename = path.basename(section.relativePath).replace(/\.tmpl$/, '');
|
||||
const reference = `references/sections/${source}/${filename}`;
|
||||
const directive = [
|
||||
`## Lazy specialist phase: ${filename.replace(/\.md$/, '')}`,
|
||||
'',
|
||||
`When the workflow reaches this phase, read \`${reference}\` completely and execute it before continuing. Do not summarize or skip its questions, pressure, gates, evidence, artifacts, mutation boundary, or exit behavior.`,
|
||||
].join('\n');
|
||||
if (!body.includes(ported)) throw new Error(`Unable to carve canonical section ${section.relativePath} from ${source}`);
|
||||
body = body.replace(ported, directive);
|
||||
}
|
||||
return `${body.trim()}\n`;
|
||||
}
|
||||
|
||||
export function renderPortedLegacySection(section: LegacySection): string {
|
||||
@@ -302,7 +480,7 @@ export function renderPortedLegacySection(section: LegacySection): string {
|
||||
/** Apply host-neutral runtime-path mechanics to linked text assets. */
|
||||
export function renderPortedAssetBytes(relativePath: string, input: Uint8Array): Uint8Array {
|
||||
if (!relativePath.endsWith('.md')) return input;
|
||||
const ported = Buffer.from(input).toString('utf8')
|
||||
const ported = replaceRetiredInvocations(Buffer.from(input).toString('utf8')
|
||||
.replaceAll(
|
||||
'~/.claude/skills/gstack/bin/gstack-diff-scope',
|
||||
'${GSTACK_HOME:-$HOME/.gstack}/bin/gstack-diff-scope',
|
||||
@@ -310,7 +488,7 @@ export function renderPortedAssetBytes(relativePath: string, input: Uint8Array):
|
||||
.replaceAll(
|
||||
'browse/bin/remote-slug 2>/dev/null || ~/.claude/skills/gstack/browse/bin/remote-slug',
|
||||
'${GSTACK_HOME:-$HOME/.gstack}/bin/remote-slug',
|
||||
);
|
||||
));
|
||||
return Buffer.from(ported, 'utf8');
|
||||
}
|
||||
|
||||
|
||||
@@ -134,10 +134,11 @@ export function routeStructured(signals: Record<string, unknown>): StructuredRou
|
||||
? 'fix-safe-after-root-cause'
|
||||
: specialist.defaultMutation;
|
||||
|
||||
// Structured routing may select a useful review/release mode without
|
||||
// granting the mutation that mode can perform. Explicit denials always win,
|
||||
// and irreversible ship stages require an affirmative external grant.
|
||||
if (signals.mutation_authorized === false && ['fix-safe', 'fix-safe-after-root-cause', 'code-generation'].includes(mutation)) {
|
||||
// Structured routing may select a useful review mode without granting the
|
||||
// mutation that specialist can perform. Missing authority fails closed;
|
||||
// fixing code requires an affirmative grant, not merely the absence of a
|
||||
// denial. Irreversible ship stages use their separate external grant below.
|
||||
if (signals.mutation_authorized !== true && ['fix-safe', 'fix-safe-after-root-cause', 'code-generation'].includes(mutation)) {
|
||||
mutation = 'report-only';
|
||||
}
|
||||
if (source === 'spec' && mutation === 'spec-and-issue' && signals.issue_mutation_allowed !== true) {
|
||||
|
||||
@@ -4,15 +4,15 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { BUG_FIX_OVERLAYS, evaluateBugFixRegression, overlaysForSource } from './bug-fix-overlays';
|
||||
import { contractFor, DISPATCHERS, SOURCE_ASSIGNMENTS } from './assignments';
|
||||
import { ROOT, blobShaForPath, legacySections, renderLegacyBody, renderPortedAssetBytes, renderPortedLegacyBody, renderPortedLegacySection, sourceBlobSha } from './render-legacy';
|
||||
import { ROOT, blobShaForPath, legacySections, renderLegacyBody, renderPortedAssetBytes, renderPortedLegacyBody, renderPortedLegacySection, retiredInvocationPattern, sourceBlobSha } from './render-legacy';
|
||||
import { routeStructured } from './route';
|
||||
import { SCENARIOS } from './scenarios';
|
||||
import { GSTACK2_BASE_SHA, TREE_NAMES } from './types';
|
||||
|
||||
const CONTRACT_KEYS = ['question_order', 'pressure', 'smart_skips', 'stop_approval_gates', 'evidence', 'artifacts', 'mutation', 'exit', 'voice'];
|
||||
const PROVENANCE_KEYS = ['original_source_file', 'original_line_range', 'purpose', 'invocation_conditions', 'modes', 'question_sequence', 'follow_up_behavior', 'smart_skip_rules', 'pushback_rules', 'stop_gates', 'approval_gates', 'rubrics_and_scoring', 'cognitive_frameworks', 'evidence_requirements', 'artifacts_produced', 'mutation_authority', 'exit_states', 'voice', 'response_posture', 'new_location', 'parity_test'];
|
||||
const ALLOWED_DISPOSITIONS = new Set(['VERBATIM_PORT', 'MECHANICAL_PORT', 'SHARED_MODULE', 'BUG_FIX', 'DUPLICATE_INFRASTRUCTURE', 'REMOVE_WITH_USER_APPROVAL']);
|
||||
export const EXPECTED_PARITY_CHECKS = 4681;
|
||||
const ALLOWED_DISPOSITIONS = new Set(['VERBATIM_PORT', 'MECHANICAL_PORT', 'JUDGMENT_PRESERVING_CARVE', 'SHARED_MODULE', 'BUG_FIX', 'DUPLICATE_INFRASTRUCTURE', 'REMOVE_WITH_USER_APPROVAL']);
|
||||
export const EXPECTED_PARITY_CHECKS = 4697;
|
||||
|
||||
function sha256(value: string | Uint8Array): string {
|
||||
return createHash('sha256').update(value).digest('hex');
|
||||
@@ -56,6 +56,7 @@ export function runParity(): ParityResult {
|
||||
checks += 1;
|
||||
if (!condition) failures.push(message);
|
||||
};
|
||||
const retiredInvocation = retiredInvocationPattern();
|
||||
|
||||
const publicSkills = fs.readdirSync(path.join(ROOT, 'skills'), { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(ROOT, 'skills', entry.name, 'SKILL.md')))
|
||||
@@ -184,6 +185,18 @@ export function runParity(): ParityResult {
|
||||
check(!/^\s*#{1,6}\s|^\s*[-*]\s|^\s*\d+\.\s/m.test(prelude), `${assignment.source} has visible generated prose before preserved judgment`);
|
||||
check(prelude.split('\n').length <= 5, `${assignment.source} generated prelude is not thin`);
|
||||
check(generatedBody === expectedBody, `${assignment.source} normalized legacy body differs`);
|
||||
check(!generatedBody.includes('## Preamble (run first)'), `${assignment.source} still executes the retired shared onboarding preamble`);
|
||||
check(!/MODEL_OVERLAY: claude|CLAUDE_PLAN_FILE|Add routing rules to CLAUDE\.md|Boil the Ocean principle|TEL_PROMPTED|PROACTIVE_PROMPTED/.test(generatedBody), `${assignment.source} retains host-specific onboarding, engagement, or telemetry machinery`);
|
||||
check(!/cd <SKILL_DIR> && \.\/setup/.test(generatedBody), `${assignment.source} tells a standard-installed skill to run a nonexistent local setup script`);
|
||||
check(!retiredInvocation.test(generatedBody), `${assignment.source} recommends a retired public invocation instead of a six-skill route`);
|
||||
const usesRuntimeBinding = /\$(?:GSTACK_BIN|GSTACK_ROOT|GSTACK_STATE_ROOT)\b|\$(?:B|D|P)\b/.test(generatedBody);
|
||||
check(!usesRuntimeBinding || generatedBody.includes('## Host-neutral runtime bindings'), `${assignment.source} uses an unbound retained runtime helper variable`);
|
||||
check(!/bun\.sh\/install|bun run \$GSTACK_BIN|command -v bun/.test(generatedBody), `${assignment.source} retains GStack-owned host Bun onboarding or invocation`);
|
||||
const visiblePointOfUse = ['open-gstack-browser', 'pair-agent', 'setup-browser-cookies'].includes(assignment.source);
|
||||
check(
|
||||
generatedBody.includes('## Visible-browser point-of-use gate') === visiblePointOfUse,
|
||||
`${assignment.source} visible-browser point-of-use gate classification is wrong`,
|
||||
);
|
||||
check(module.includes(`blob=${baseBlob}`), `${assignment.source} module lacks source blob provenance`);
|
||||
check(module.includes(`baseline_render_sha256=${sha256(baselineBody)}`), `${assignment.source} module lacks immutable baseline render hash`);
|
||||
check(module.includes(`ported_render_sha256=${sha256(expectedBody)}`), `${assignment.source} module lacks installable port render hash`);
|
||||
@@ -197,6 +210,10 @@ export function runParity(): ParityResult {
|
||||
}
|
||||
}
|
||||
|
||||
const baselinePromptBytes = SOURCE_ASSIGNMENTS.reduce((total, assignment) => total + Buffer.byteLength(renderLegacyBody(assignment.source)), 0);
|
||||
const canonicalPromptBytes = SOURCE_ASSIGNMENTS.reduce((total, assignment) => total + Buffer.byteLength(renderPortedLegacyBody(assignment.source)), 0);
|
||||
check(canonicalPromptBytes < baselinePromptBytes * 0.5, `Canonical specialist corpus did not cut prompt bytes by at least 50% (${canonicalPromptBytes}/${baselinePromptBytes})`);
|
||||
|
||||
const sections = legacySections();
|
||||
check(sections.length === 16, `Expected 16 section templates; got ${sections.length}`);
|
||||
for (const section of sections) {
|
||||
@@ -204,8 +221,9 @@ export function runParity(): ParityResult {
|
||||
const assignment = SOURCE_ASSIGNMENTS.find((entry) => entry.source === section.source)!;
|
||||
const module = fs.readFileSync(path.join(ROOT, 'skills', assignment.tree, 'references', 'legacy', `${assignment.source}.md`), 'utf8');
|
||||
const portedSection = renderPortedLegacySection(section);
|
||||
check(module.includes(portedSection.trim()), `${section.relativePath} was not mechanically inlined`);
|
||||
const sectionName = path.basename(section.relativePath).replace(/\.tmpl$/, '');
|
||||
check(module.includes(`references/sections/${section.source}/${sectionName}`), `${section.relativePath} lacks its package-local lazy reference`);
|
||||
check(!module.includes(portedSection.trim()), `${section.relativePath} is still duplicated inline instead of lazy-loaded`);
|
||||
const packaged = path.join(ROOT, 'skills', assignment.tree, 'references', 'sections', section.source, sectionName);
|
||||
check(fs.existsSync(packaged), `${section.relativePath} is referenced but not packaged`);
|
||||
if (fs.existsSync(packaged)) check(normalizeGolden(fs.readFileSync(packaged, 'utf8')) === normalizeGolden(portedSection), `${section.relativePath} packaged content drifted`);
|
||||
@@ -279,6 +297,7 @@ export function runParity(): ParityResult {
|
||||
check(sha256(installed) === sha256(expected), `Relocated asset hash mismatch: ${asset.target_path}`);
|
||||
if (asset.target_path.endsWith('.md')) {
|
||||
check(!/~\/.claude\/skills\/gstack|browse\/bin\/remote-slug/.test(installed.toString()), `Relocated asset retains a host-specific runtime path: ${asset.target_path}`);
|
||||
check(!retiredInvocationPattern().test(installed.toString()), `Relocated executable asset recommends a retired public invocation: ${asset.target_path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,7 +305,11 @@ export function runParity(): ParityResult {
|
||||
for (const sectionCopy of manifest.section_copies ?? []) {
|
||||
const target = path.join(ROOT, sectionCopy.target_path);
|
||||
check(fs.existsSync(target), `Missing packaged section ${sectionCopy.target_path}`);
|
||||
if (fs.existsSync(target)) check(sha256(fs.readFileSync(target)) === sectionCopy.sha256, `Packaged section hash mismatch: ${sectionCopy.target_path}`);
|
||||
if (fs.existsSync(target)) {
|
||||
const content = fs.readFileSync(target);
|
||||
check(sha256(content) === sectionCopy.sha256, `Packaged section hash mismatch: ${sectionCopy.target_path}`);
|
||||
check(!retiredInvocationPattern().test(content.toString()), `Packaged section recommends a retired public invocation: ${sectionCopy.target_path}`);
|
||||
}
|
||||
}
|
||||
for (const dependency of manifest.dependency_copies ?? []) {
|
||||
const target = path.join(ROOT, dependency.target);
|
||||
@@ -321,7 +344,7 @@ export function runParity(): ParityResult {
|
||||
for (const tree of TREE_NAMES) {
|
||||
const compatibility = fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'COMPATIBILITY.md'), 'utf8');
|
||||
check(!compatibility.includes('../../../') && !compatibility.includes('compat/README.md'), `${tree} compatibility map escapes the selected package`);
|
||||
for (const reference of ['SHARED-JUDGMENT.md', 'WEB-CONTEXT.md']) {
|
||||
for (const reference of ['SHARED-JUDGMENT.md', 'WEB-CONTEXT.md', 'RUNTIME.md']) {
|
||||
const referencePath = path.join(ROOT, 'skills', tree, 'references', reference);
|
||||
check(fs.existsSync(referencePath), `${tree} lacks ${reference}`);
|
||||
if (fs.existsSync(referencePath)) {
|
||||
@@ -331,6 +354,16 @@ export function runParity(): ParityResult {
|
||||
);
|
||||
}
|
||||
}
|
||||
const runtimeContract = fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'RUNTIME.md'), 'utf8');
|
||||
const packagedBootstrap = fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-bootstrap.mjs'));
|
||||
check(packagedBootstrap.equals(fs.readFileSync(path.join(ROOT, 'runtime', 'runtime-bootstrap.mjs'))), `${tree} packaged runtime bootstrap drifted from its source`);
|
||||
check(runtimeContract.includes('preview --capability <name>') && runtimeContract.includes('It never downloads components or mutates runtime state.'), `${tree} runtime contract lacks non-mutating exact-byte preview`);
|
||||
check(runtimeContract.includes('install --capability <name> --yes'), `${tree} runtime contract lacks explicit approved install invocation`);
|
||||
check(runtimeContract.includes('Logical `browser` expands to `browser-code + browser-headless`') && runtimeContract.includes('`browser-visible` expands to `browser-code + browser-visible` and does not require headless') && runtimeContract.includes('`pdf` depends on `diagram`'), `${tree} runtime contract omits component dependency closure`);
|
||||
check(runtimeContract.includes('`all` means those five and intentionally excludes visible Chromium'), `${tree} runtime contract lets eager setup install visible Chromium`);
|
||||
check(runtimeContract.includes('B=$GSTACK_BIN/browse') && runtimeContract.includes('P=$GSTACK_BIN/make-pdf'), `${tree} runtime contract omits stable launcher bindings`);
|
||||
check(runtimeContract.includes('BUN_CMD=$GSTACK_BIN/bun'), `${tree} runtime contract omits the managed Bun binding`);
|
||||
check(runtimeContract.includes('discovers Git for Windows Bash') && runtimeContract.includes('Python is not a global GStack prerequisite'), `${tree} runtime contract omits retained shell/Python prerequisite disclosure`);
|
||||
}
|
||||
for (const required of ['SKILL-MIGRATION.md', 'JUDGMENT-PROVENANCE.json', 'JUDGMENT-PARITY.md', 'SCENARIOS.md']) {
|
||||
check(fs.existsSync(path.join(ROOT, 'docs', 'gstack-2', required)), `Missing docs/gstack-2/${required}`);
|
||||
|
||||
@@ -78,7 +78,7 @@ function dimensionEvidence(execution: SemanticExecution, preservedPorts: boolean
|
||||
return [dimension, {
|
||||
classification: preservedPorts ? 'EQUIVALENT' : 'REGRESSION',
|
||||
evidence: preservedPorts
|
||||
? 'The candidate exactly matches the deterministic installable port of the pinned 1.x workflow; only enumerated package-local path mechanics differ from the immutable oracle.'
|
||||
? 'The candidate exactly matches the deterministic canonical specialist render of the pinned workflow. The retired shared onboarding wrapper is excluded and carved specialist phases are package-local lazy references; specialist questions, pressure, gates, evidence, artifacts, mutation boundaries, exit behavior, and voice remain governed by their source contracts.'
|
||||
: 'The candidate lost or changed authoritative workflow prose.',
|
||||
}];
|
||||
}));
|
||||
@@ -115,7 +115,7 @@ function deterministicTranscript(execution: SemanticExecution) {
|
||||
mechanical_port: {
|
||||
rendered_sha256: sha256(expectedPort),
|
||||
differs_from_baseline: baseline !== expectedPort,
|
||||
allowed_difference: 'Package-local skill, section, support-artifact, and stable runtime path relocation only.',
|
||||
allowed_difference: 'Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references.',
|
||||
},
|
||||
candidate: {
|
||||
target_path: repositoryRelativePath(candidateFile),
|
||||
@@ -171,15 +171,24 @@ function sectionTranscript() {
|
||||
const target = path.join(ROOT, 'skills', assignment.tree, 'references', 'legacy', `${section.source}.md`);
|
||||
const candidate = fs.readFileSync(target, 'utf8');
|
||||
const ported = renderPortedLegacySection(section);
|
||||
const filename = path.basename(section.relativePath).replace(/\.tmpl$/, '');
|
||||
const reference = `references/sections/${section.source}/${filename}`;
|
||||
const packagedPath = path.join(ROOT, 'skills', assignment.tree, reference);
|
||||
const packaged = fs.existsSync(packagedPath) ? fs.readFileSync(packagedPath, 'utf8') : '';
|
||||
const occurrences = candidate.split(ported.trim()).length - 1;
|
||||
const referenceOccurrences = candidate.split(reference).length - 1;
|
||||
return {
|
||||
source_path: section.relativePath,
|
||||
parent_source: section.source,
|
||||
target_path: repositoryRelativePath(target),
|
||||
baseline_render_sha256: sha256(section.rendered),
|
||||
ported_render_sha256: sha256(ported),
|
||||
candidate_occurrences: occurrences,
|
||||
classification: occurrences === 1 ? 'EQUIVALENT' : 'REGRESSION',
|
||||
candidate_inline_occurrences: occurrences,
|
||||
candidate_lazy_reference_occurrences: referenceOccurrences,
|
||||
packaged_sha256: packaged ? sha256(packaged) : null,
|
||||
classification: occurrences === 0 && referenceOccurrences >= 1 && normalizeGolden(packaged) === normalizeGolden(ported)
|
||||
? 'EQUIVALENT'
|
||||
: 'REGRESSION',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user