Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions scripts/agent-log-analysis.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ function milestoneKinds(event) {
const kinds = [];
if (/\bnpm\s+(?:install|i)\b/.test(command)) kinds.push('installation');
if (/\bagent-context\b/.test(command)) kinds.push('contract');
if (event.name === 'Write' && /(?:build-ops\.[cm]?js|ops\.json)$/.test(path))
kinds.push('firstOperations');
if (event.name === 'Write' && /(?:build[-_]ops\.(?:[cm]?js|py)|ops\.json)$/i.test(path)) {
const probe = /(?:^|[\/_.-])probe(?:[\/_.-]|$)/i.test(path);
kinds.push(probe ? 'firstProbeOperations' : 'firstOperations');
}
for (const kind of ['apply', 'validate', 'export'])
if (new RegExp(`(?:deckuse|\\$CLI)\\s+${kind}\\b`).test(command)) kinds.push(kind);
if (/(?:deckuse|\$CLI)\s+render\b/.test(command) && !command.includes('--preflight'))
Expand Down Expand Up @@ -93,6 +95,7 @@ export function analyzeAgentLog(events) {
[
'installation',
'contract',
'firstProbeOperations',
'firstOperations',
'apply',
'validate',
Expand Down Expand Up @@ -129,7 +132,7 @@ export function analyzeAgentLog(events) {
notes: [
'Tool intervals are merged, not summed. Batched result timestamps do not identify the slow tool.',
'Unattributed time can include model output, reasoning, host scheduling and incomplete tool traces.',
'Milestones identify attempts; return status alone cannot prove a validated artifact or visual review.',
'Milestones identify attempts; return status alone cannot prove a validated artifact or visual review. Probe operations are identified by their path names.',
'Background notifications and summary backfills do not start tasks. No source payloads are included.',
],
};
Expand Down
60 changes: 59 additions & 1 deletion scripts/check-agent-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ if (
)
throw new Error('Authoring context probed preview or returned protocol schema');
const contract = compact.envelope.data.applyContract.examples;
const photo = join(staging, 'photo.png');
await writeFile(
photo,
Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/b1sAAAAASUVORK5CYII=',
'base64',
),
);
const generatedOps = join(staging, 'compact-nine-page-ops.json');
const generatedNames = join(staging, 'compact-nine-page-names.json');
const inventory = ['Blank starter', ...Array.from({ length: 8 }, (_, i) => 'Page ' + (i + 2))];
Expand All @@ -157,6 +165,7 @@ await writeFile(
...(i ? [{ ...contract.slide, name: slide }] : []),
{ ...contract.nativeShape, slide },
{ ...contract.richText, slide },
...(i === 0 ? [{ ...contract.image, slide, file: photo }] : []),
]),
),
);
Expand Down Expand Up @@ -191,6 +200,10 @@ if (
const firstPage = cli(['list', 'shapes', '--slide', '1', '--workspace', compactDeck.workspace]);
if (!JSON.stringify(firstPage).includes('产品 Overview'))
throw new Error('Rich text was not preserved');
if (
!firstPage.data.items.some((item) => item.kind === 'picture' && item.name === contract.image.name)
)
throw new Error('Compact contract image did not produce a native picture');

await access(join(root, context.recipe.deliveryRunner.resource));
const ops = join(root, context.resource.example);
Expand Down Expand Up @@ -223,15 +236,60 @@ if (generated.state !== 'structural-only' || !generated.sha256)
await access(generated.output);
const original = await readFile(generated.output);
const runner = join(root, 'scripts/rebuild-generation.mjs');
const maskedExit = join(staging, 'masked-preview-exit.mjs');
await writeFile(
maskedExit,
`
import processes from 'node:child_process';
import { syncBuiltinESMExports } from 'node:module';
const { spawnSync } = processes;
processes.spawnSync = (binary, argv, options) => {
const result = spawnSync(binary, argv, options);
return argv[1] === 'render' ? { ...result, status: 0 } : result;
};
syncBuiltinESMExports();
`,
);

const deniedPreview = spawnSync(
process.execPath,
[runner, staging, ops, expectedNames, '--preview'],
['--import', maskedExit, runner, staging, ops, expectedNames, '--preview'],
{ encoding: 'utf8' },
);
const blocked = JSON.parse(deniedPreview.stdout);
if (deniedPreview.status !== 1 || blocked.state !== 'preview-blocked' || !blocked.output)
throw Error('Unavailable preview did not preserve the structural artifact');
await access(blocked.output);
const previewProcess = JSON.parse(
await readFile(join(blocked.generation, 'preview.process.json'), 'utf8'),
);
if (previewProcess.status !== 0) throw Error('Preview wrapper did not mask the child exit');
if (
blocked.verification?.structure !== 'validated' ||
blocked.verification.preview !== 'blocked' ||
blocked.verification.visualReview !== 'not-performed' ||
blocked.verification.nativeReview !== 'not-performed' ||
blocked.previewFailure?.retryable !== false ||
blocked.nextAction?.type !== 'deliver-validated-pptx' ||
blocked.nextAction.path !== blocked.output
)
throw Error('Expected a machine-readable structural handoff for blocked preview');
const resumedBlocked = spawnSync(process.execPath, [runner, '--resume', blocked.generation], {
encoding: 'utf8',
env: { ...process.env, DECKUSE_BROWSER_PROVIDER: 'chrome' },
});
const resumedReport = JSON.parse(resumedBlocked.stdout);
if (
resumedBlocked.status !== 1 ||
resumedReport.previewFailure?.code !== blocked.previewFailure.code ||
resumedReport.nextAction?.path !== blocked.output ||
resumedReport.sha256 !== blocked.sha256 ||
(await readdir(blocked.generation)).filter((name) =>
/^preview(?:\.attempt-\d+)?\.started\.json$/.test(name),
).length !== 1
)
throw Error('Changing a preview setting replayed a nonretryable preview or lost its artifact');

const wrongNames = join(staging, 'wrong-slide-names.json');
await writeFile(wrongNames, JSON.stringify(['Blank starter', 'Unexpected']));
const invalidBuild = spawnSync(process.execPath, [runner, staging, ops, wrongNames], {
Expand Down
84 changes: 53 additions & 31 deletions scripts/rebuild-generation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,6 @@ let config,
checkpoint = {},
attempt,
previewFailure;
const previewContext = () =>
hash(
JSON.stringify(
Object.fromEntries(
Object.entries(process.env)
.filter(([key]) =>
/^(DECKUSE_(BROWSER|DECK2HTML)|PLAYWRIGHT_|CHROME_PATH|CHROMIUM_PATH|DOTNET_BUNDLE_EXTRACT_BASE_DIR)/.test(
key,
),
)
.sort(),
),
),
);
async function nextBase(name) {
if (
!(await exists(join(generation, name + '.started.json'))) &&
Expand Down Expand Up @@ -172,12 +158,22 @@ async function cli(argv, label = phase) {
envelope?.ok !== true ||
(['validate', 'export'].includes(label) && envelope.data?.valid !== true)
) {
if (label === 'preview')
let previewMessage;
if (label === 'preview') {
const failures = (envelope?.data?.results ?? []).filter((page) => !page.ok);
const failedPage =
failures.find((page) => page.errorDetails?.retryable === false) ?? failures[0];
const details = envelope?.error?.details ?? failedPage?.errorDetails;
previewMessage = failedPage?.error;
previewFailure = {
context: previewContext(),
retryable: envelope.error?.details?.retryable ?? false,
code: envelope?.error?.code ?? failedPage?.errorCode ?? 'PREVIEW_FAILED',
stage: details?.stage,
retryable:
details?.retryable === true &&
failures.every((page) => page.errorDetails?.retryable === true),
};
throw Error(envelope?.error?.message ?? label + ' failed');
}
throw Error(envelope?.error?.message ?? previewMessage ?? label + ' failed');
}
return { data: envelope.data, base };
}
Expand All @@ -192,6 +188,18 @@ async function report(result) {
revision,
attempt,
visualReview: 'not-performed',
verification: {
structure: artifact || validatedSnapshot ? 'validated' : 'unverified',
preview:
result.state === 'preview-ready'
? 'generated'
: result.state === 'preview-blocked'
? 'blocked'
: 'not-performed',
visualReview: 'not-performed',
nativeReview: 'not-performed',
},
nextAction: { type: 'inspect-report', path },
...result,
};
await writeFile(path, JSON.stringify(data, null, 2) + '\n', { flag: 'wx' });
Expand Down Expand Up @@ -474,14 +482,12 @@ async function run() {
}
if (config.preview) {
phase = 'preview';
if (
checkpoint.previewFailure?.retryable === false &&
checkpoint.previewFailure.context === previewContext() &&
!retryPreview
)
if (checkpoint.previewFailure?.retryable === false && !retryPreview) {
previewFailure = checkpoint.previewFailure;
throw Error(
'Preview previously blocked in this context; fix the environment and pass --retry-preview explicitly',
'Preview remains blocked. Deliver the validated PPTX and retry only after confirming host capability has changed.',
);
}
const { data: result, base } = await cli([
'render',
...(resume ? ['--since-revision', revision] : ['--all']),
Expand Down Expand Up @@ -511,10 +517,18 @@ async function run() {
state: config.preview ? 'preview-ready' : 'structural-only',
validationReport: checkpoint.validationReport,
...(config.preview ? { previewInventory: checkpoint.previewReport } : {}),
nextAction: {
type: config.preview ? 'review-preview' : 'deliver-validated-pptx',
path: config.preview ? checkpoint.previewReport : artifact.output,
},
});
} catch (cause) {
process.exitCode = 1;
if (previewFailure) await saveCheckpoint({ previewFailure });
const previewBlocked = Boolean(artifact && phase === 'preview');
if (previewBlocked) {
previewFailure ??= { code: failureKind ?? 'PREVIEW_FAILED', retryable: false };
if (previewFailure !== checkpoint.previewFailure) await saveCheckpoint({ previewFailure });
}
let recoverableSnapshot;
if (phase === 'export' && validatedSnapshot) {
const candidate = await inspectSnapshot().catch(() => undefined);
Expand All @@ -523,13 +537,21 @@ async function run() {
await report({
ok: false,
...artifact,
state:
artifact && phase === 'preview'
? 'preview-blocked'
: recoverableSnapshot
? 'recoverable-snapshot'
: 'failed',
state: previewBlocked
? 'preview-blocked'
: recoverableSnapshot
? 'recoverable-snapshot'
: 'failed',
...(recoverableSnapshot ? { recoverableSnapshot } : {}),
...(previewBlocked
? {
previewFailure,
validationReport: checkpoint.validationReport,
nextAction: { type: 'deliver-validated-pptx', path: artifact.output },
}
: phase === 'apply'
? { nextAction: { type: 'resume-generation', path: generation } }
: {}),
...(failureKind ? { failureKind } : {}),
error: cause instanceof Error ? cause.message : String(cause),
});
Expand Down
2 changes: 1 addition & 1 deletion skills/deckuse/scenarios/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Use a revision-guarded patch for local changes. For a full replacement, run
separate generation with its own
workspace, exported PPTX and reports. Add `--preview` to render into that
generation's preview directory and check the page inventory against the export.
A failed generation leaves all earlier workspaces and deliverables intact. The runner writes `run.json` and separate process results. Treat `preview-ready` as screenshots available for review and `preview-blocked` as structural delivery with visual review outstanding. After an error marked `retryable: false`, stop preview attempts until the host configuration changes.
A failed generation leaves all earlier workspaces and deliverables intact. The runner writes `run.json` and separate process results. Treat `preview-ready` as screenshots available for review and `preview-blocked` as structural delivery with visual review outstanding. For `preview-blocked`, follow `nextAction` to deliver the validated PPTX and report visual and native review as pending. Resume with `--retry-preview` only after confirming the required host capability has changed. The runner will not start another preview from a nonretryable checkpoint without that explicit flag.

### Default delivery and continuation

Expand Down
13 changes: 13 additions & 0 deletions src/agent-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ export async function agentContext(options: {
'Append --patch ./patch.json for each ordered patch; inputs are saved and compiled into one guarded apply',
resume:
'node <package>/scripts/rebuild-generation.mjs --resume <generation-directory>',
previewBlocked:
'Follow nextAction to deliver the validated PPTX. Use --retry-preview only after confirming host capability has changed.',
cleanup:
'Workspace cleanup moves owned artifacts into .deckuse-trash by default. Set DECKUSE_CLEANUP_MODE=delete only when the host permits deletion.',
},
Expand Down Expand Up @@ -275,6 +277,17 @@ export async function agentContext(options: {
fill: { color: 'EDF4FA' },
stroke: { color: '345671', width: 1 },
},
image: {
type: 'addShape',
slide: 'Overview',
shapeType: 'image',
name: 'Photo',
file: '/absolute/path/to/photo.png',
x: '0.75in',
y: '2in',
width: '6in',
height: '3in',
},
richText: {
type: 'addShape',
slide: 'Overview',
Expand Down
21 changes: 21 additions & 0 deletions test/cli/agent-log-analysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,24 @@ it('merges concurrent intervals and excludes summary/notification task boundarie
]),
).toBe(13);
});

it('keeps probe and formal operations milestones separate', () => {
const result = analyzeAgentLog([
{
timestamp: 100,
type: 'message',
role: 'user',
content: [{ text: '<user_query>Make a deck</user_query>' }],
},
...['./probe-ops.json', './job/build_ops.py'].map((file_path, i) => ({
timestamp: 200 + i * 100,
type: 'function_call',
callId: String(i),
name: 'Write',
arguments: JSON.stringify({ file_path }),
})),
]);
expect(result.tasks[0].milestones.firstProbeOperations.startedMs).toBe(100);
expect(result.tasks[0].milestones.firstOperations.startedMs).toBe(200);
expect(JSON.stringify(result)).not.toContain('probe-ops.json');
});
7 changes: 6 additions & 1 deletion test/cli/rebuild-generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,12 @@ it('retains exported bytes and avoids repeating a blocked preview in the same co
env,
});
expect(resumed.status).toBe(1);
expect(JSON.parse(resumed.stdout).error).toContain('previously blocked');
expect(JSON.parse(resumed.stdout)).toMatchObject({
state: 'preview-blocked',
previewFailure: { retryable: false },
verification: { structure: 'validated', preview: 'blocked', nativeReview: 'not-performed' },
nextAction: { type: 'deliver-validated-pptx', path: failed.output },
});
expect(await readFile(failed.output)).toEqual(before);
const { readdir } = await import('node:fs/promises');
expect(
Expand Down
Loading