mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
The first live codex-multiblock-live run passed its real check (the /learn block found the install and printed the seeded learning) but the oracle flagged a sed of the learn SKILL.md, whose prelude source quotes the 'gstack: no install found (tried $_r)' text. Only an emitted line that starts with the message and names a real root counts now; free replay negative added.
72 lines
3.8 KiB
TypeScript
72 lines
3.8 KiB
TypeScript
/**
|
|
* Oracles for the live Codex multi-block case (E6,
|
|
* test/codex-e2e-multiblock-live.test.ts). Codex runs every shell block in a
|
|
* fresh shell, so a gstack skill works there only when each later block
|
|
* resolves the install on its own (the env-var host prelude). The case
|
|
* installs gstack with `./setup --host codex` into a fresh CODEX_HOME, seeds
|
|
* one learning, and runs /learn's "Show recent" block through `codex exec`.
|
|
* Pure functions: the free test replays synthetic `exec --json` events.
|
|
*/
|
|
|
|
export const MULTIBLOCK_SENTINEL_KEY = 'e6-multiblock-sentinel';
|
|
|
|
interface CommandEvent { command: string; output: string; exitCode: number | null }
|
|
|
|
function completedCommands(rawLines: readonly string[]): CommandEvent[] {
|
|
const commands: CommandEvent[] = [];
|
|
for (const line of rawLines) {
|
|
try {
|
|
const event = JSON.parse(line);
|
|
if (event?.type !== 'item.completed' || event.item?.type !== 'command_execution') continue;
|
|
commands.push({ command: String(event.item.command ?? ''), output: String(event.item.aggregated_output ?? ''),
|
|
exitCode: typeof event.item.exit_code === 'number' ? event.item.exit_code : null });
|
|
} catch { /* not an event line */ }
|
|
}
|
|
return commands;
|
|
}
|
|
|
|
/**
|
|
* Problems with a live run: any block that could not find the install, a
|
|
* gstack helper path that did not resolve, or no later block (the skill's
|
|
* learnings-search block, which only the prelude can root) that ran the
|
|
* installed helper and printed the seeded learning.
|
|
*/
|
|
export function multiblockProblems(rawLines: readonly string[], sentinel = MULTIBLOCK_SENTINEL_KEY): string[] {
|
|
const commands = completedCommands(rawLines);
|
|
const problems: string[] = [];
|
|
for (const { command, output } of commands) {
|
|
// The emitted line names the root it tried; the prelude's source text
|
|
// (a block that reads a SKILL.md) quotes it mid-line with `$_r`.
|
|
const missing = /^gstack: no install found \(tried (?!\$)[^\n]*/m.exec(output);
|
|
if (missing) problems.push(`a block could not find the install: ${missing[0].slice(0, 200)}`);
|
|
const unresolved = /\S*\/bin\/gstack-[\w-]+: (?:No such file or directory|command not found|not found)/.exec(output);
|
|
if (unresolved) problems.push(`a gstack helper path did not resolve: ${unresolved[0]} (in: ${command.slice(0, 120)})`);
|
|
}
|
|
const search = commands.filter(c => c.command.includes('gstack-learnings-search'));
|
|
if (search.length === 0) problems.push('no block ran gstack-learnings-search (the skill\'s "Show recent" block)');
|
|
else if (!search.some(c => c.exitCode === 0 && c.output.includes(sentinel) && !/GSTACK_ROOT=["']?\//.test(c.command))) {
|
|
problems.push(`no learnings-search block rooted by its own prelude printed the seeded learning ${sentinel}: `
|
|
+ search.map(c => `exit ${c.exitCode}, ${c.output.includes(sentinel) ? 'sentinel present' : 'sentinel absent'}`
|
|
+ `${/GSTACK_ROOT=["']?\//.test(c.command) ? ', GSTACK_ROOT set by hand' : ''}`).join('; '));
|
|
}
|
|
return problems;
|
|
}
|
|
|
|
/**
|
|
* The `gstack` router entry from `codex debug prompt-input` output (a JSON
|
|
* array of input items): the skill path it names, resolved through the skill
|
|
* roots table, or null when Codex does not list the router.
|
|
*/
|
|
export function routerPathFromPromptInput(promptInput: string): string | null {
|
|
let text = promptInput;
|
|
try {
|
|
const items = JSON.parse(promptInput.slice(promptInput.indexOf('[')));
|
|
text = (Array.isArray(items) ? items : []).flatMap((item: { content?: Array<{ text?: string }> }) => item.content ?? [])
|
|
.map(part => part.text ?? '').join('\n');
|
|
} catch { /* scan the raw text */ }
|
|
const entry = /^- gstack: .*\(file: (r\d+)\/gstack\/SKILL\.md\)$/m.exec(text);
|
|
if (!entry) return null;
|
|
const root = new RegExp(`^- \`${entry[1]}\` = \`([^\`]+)\`$`, 'm').exec(text);
|
|
return root ? `${root[1]}/gstack/SKILL.md` : null;
|
|
}
|