@12ui/design 0.2.67 → 0.2.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +1 -1
- package/open-design.json +2 -2
- package/package.json +1 -1
- package/runtime/bootstrap.mjs +131 -0
- package/runtime/codex-conversion/README.md +75 -0
- package/runtime/codex-conversion/bin/convert.mjs +21 -0
- package/runtime/codex-conversion/bin/export.mjs +4 -0
- package/runtime/codex-conversion/bin/finish.mjs +11 -0
- package/runtime/codex-conversion/bin/improve.mjs +12 -0
- package/runtime/codex-conversion/bin/install-skill.mjs +36 -0
- package/runtime/codex-conversion/bin/recover-image.mjs +11 -0
- package/runtime/codex-conversion/bin/recovery/prepare.mjs +68 -0
- package/runtime/codex-conversion/bin/recovery/run.mjs +37 -0
- package/runtime/codex-conversion/npm-shrinkwrap.json +1199 -0
- package/runtime/codex-conversion/package.json +45 -0
- package/runtime/codex-conversion/src/codex/arguments.mjs +14 -0
- package/runtime/codex-conversion/src/codex/completion.mjs +14 -0
- package/runtime/codex-conversion/src/codex/events.mjs +41 -0
- package/runtime/codex-conversion/src/codex/home.mjs +37 -0
- package/runtime/codex-conversion/src/codex/invoke.mjs +79 -0
- package/runtime/codex-conversion/src/contract-repair.mjs +44 -0
- package/runtime/codex-conversion/src/contracts/compile.mjs +53 -0
- package/runtime/codex-conversion/src/contracts/contracts.mjs +19 -0
- package/runtime/codex-conversion/src/contracts/geometry.mjs +38 -0
- package/runtime/codex-conversion/src/contracts/grid-frame.mjs +22 -0
- package/runtime/codex-conversion/src/contracts/groups.mjs +14 -0
- package/runtime/codex-conversion/src/contracts/initial.mjs +9 -0
- package/runtime/codex-conversion/src/contracts/normalize.mjs +46 -0
- package/runtime/codex-conversion/src/exports/browser.mjs +26 -0
- package/runtime/codex-conversion/src/exports/editable.mjs +34 -0
- package/runtime/codex-conversion/src/exports/files.mjs +7 -0
- package/runtime/codex-conversion/src/exports/index.mjs +24 -0
- package/runtime/codex-conversion/src/exports/react.mjs +44 -0
- package/runtime/codex-conversion/src/exports/viewport.mjs +17 -0
- package/runtime/codex-conversion/src/finish/index.mjs +58 -0
- package/runtime/codex-conversion/src/finish/patch.mjs +14 -0
- package/runtime/codex-conversion/src/images/execute.mjs +52 -0
- package/runtime/codex-conversion/src/images/frame-normalization.mjs +10 -0
- package/runtime/codex-conversion/src/images/geometry.mjs +9 -0
- package/runtime/codex-conversion/src/images/local-operation.mjs +17 -0
- package/runtime/codex-conversion/src/images/local.mjs +18 -0
- package/runtime/codex-conversion/src/images/native-collect.mjs +43 -0
- package/runtime/codex-conversion/src/images/native-files.mjs +11 -0
- package/runtime/codex-conversion/src/images/native-invoke.mjs +49 -0
- package/runtime/codex-conversion/src/images/native-records.mjs +57 -0
- package/runtime/codex-conversion/src/images/native.mjs +41 -0
- package/runtime/codex-conversion/src/images/prompt.mjs +14 -0
- package/runtime/codex-conversion/src/images/provider.mjs +22 -0
- package/runtime/codex-conversion/src/output.mjs +28 -0
- package/runtime/codex-conversion/src/project/invoke.mjs +35 -0
- package/runtime/codex-conversion/src/project/prompt.mjs +13 -0
- package/runtime/codex-conversion/src/project/run.mjs +83 -0
- package/runtime/codex-conversion/src/project/source.mjs +21 -0
- package/runtime/codex-conversion/src/references.mjs +114 -0
- package/runtime/codex-conversion/src/run-store-io.mjs +38 -0
- package/runtime/codex-conversion/src/run-store.mjs +114 -0
- package/runtime/codex-conversion/src/run.mjs +108 -0
- package/runtime/codex-conversion/src/runtime-pin.mjs +12 -0
- package/runtime/codex-conversion/templates/initial.schema.json +90 -0
- package/runtime/codex-conversion/templates/initial.txt +22 -0
package/SKILL.md
CHANGED
|
@@ -28,7 +28,7 @@ dispatch paid work when it is absent, rejected, or times out.
|
|
|
28
28
|
Use `12ui` when it is available on `PATH`. Otherwise invoke the pinned CLI that
|
|
29
29
|
the plugin's `open-design.json` MCP entry uses:
|
|
30
30
|
|
|
31
|
-
npx -y @12ui/design@0.2.
|
|
31
|
+
npx -y @12ui/design@0.2.68 <command> ...
|
|
32
32
|
|
|
33
33
|
Run `12ui auth status --json` (or its pinned `npx` equivalent) before the paid
|
|
34
34
|
preflight. A desktop user can authenticate with `12ui auth login`; a headless
|
package/open-design.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"specVersion": "1.0.0",
|
|
4
4
|
"name": "12ui-design",
|
|
5
5
|
"title": "12UI Design",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.68",
|
|
7
7
|
"description": "Explore corpus-grounded interface directions, choose one, and convert it into production-ready responsive or native artifacts with 12UI.",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Just Every",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"command": "npx",
|
|
50
50
|
"args": [
|
|
51
51
|
"-y",
|
|
52
|
-
"@12ui/design@0.2.
|
|
52
|
+
"@12ui/design@0.2.68",
|
|
53
53
|
"mcp"
|
|
54
54
|
]
|
|
55
55
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { cp, mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
|
|
9
|
+
async function files(root, relative = '') {
|
|
10
|
+
const entries = await readdir(path.join(root, relative), { withFileTypes: true });
|
|
11
|
+
const result = [];
|
|
12
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
13
|
+
const name = path.join(relative, entry.name);
|
|
14
|
+
if (entry.isSymbolicLink()) throw new Error(`Runtime payload must not contain symlinks: ${name}`);
|
|
15
|
+
if (entry.isDirectory()) result.push(...await files(root, name));
|
|
16
|
+
else result.push(name);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function execute(command, args, options = {}) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const child = spawn(command, args, { stdio: 'inherit', ...options });
|
|
24
|
+
child.on('error', reject);
|
|
25
|
+
child.on('exit', (code, signal) => {
|
|
26
|
+
if (code === 0) resolve();
|
|
27
|
+
else reject(new Error(`${command} failed (${signal ?? code})`));
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const payloadEntries = ['bin', 'src', 'templates', 'package.json', 'npm-shrinkwrap.json', 'README.md'];
|
|
33
|
+
|
|
34
|
+
async function runtimeKey(payload) {
|
|
35
|
+
if (Number(process.versions.node.split('.')[0]) < 22) throw new Error('12ui Codex conversion requires Node.js 22 or newer.');
|
|
36
|
+
const hash = createHash('sha256');
|
|
37
|
+
hash.update(`${process.platform}/${process.arch}/node-${process.versions.node.split('.')[0]}\0`);
|
|
38
|
+
const names = [];
|
|
39
|
+
for (const entry of payloadEntries) {
|
|
40
|
+
if ((await stat(path.join(payload, entry))).isDirectory()) names.push(...await files(payload, entry));
|
|
41
|
+
else names.push(entry);
|
|
42
|
+
}
|
|
43
|
+
for (const file of names.sort()) {
|
|
44
|
+
hash.update(file).update('\0').update(await readFile(path.join(payload, file))).update('\0');
|
|
45
|
+
}
|
|
46
|
+
return hash.digest('hex');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function prepareRuntime(payload, cacheBase) {
|
|
50
|
+
const key = await runtimeKey(payload);
|
|
51
|
+
const root = path.join(cacheBase, key);
|
|
52
|
+
const ready = async () => {
|
|
53
|
+
try { return (await readFile(path.join(root, '.12ui-ready'), 'utf8')) === key; }
|
|
54
|
+
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
55
|
+
};
|
|
56
|
+
if (await ready()) return { root, key };
|
|
57
|
+
await mkdir(cacheBase, { recursive: true });
|
|
58
|
+
const staging = path.join(cacheBase, `.${key}-${randomUUID()}`);
|
|
59
|
+
try {
|
|
60
|
+
await cp(payload, staging, { recursive: true, errorOnExist: true });
|
|
61
|
+
// npm runs only against the content-keyed user cache, never the installed skill.
|
|
62
|
+
await execute(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['ci', '--omit=dev', '--no-audit', '--no-fund'], { cwd: staging, stdio: ['inherit', 2, 2] });
|
|
63
|
+
await writeFile(path.join(staging, '.12ui-ready'), key);
|
|
64
|
+
try { await rename(staging, root); }
|
|
65
|
+
catch (error) { if (!(await ready())) throw error; }
|
|
66
|
+
return { root, key };
|
|
67
|
+
} finally {
|
|
68
|
+
await rm(staging, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseRunInvocation(command, args) {
|
|
73
|
+
if (!['convert', 'improve'].includes(command)) return {};
|
|
74
|
+
const names = command === 'convert'
|
|
75
|
+
? ['source', 'run-dir', 'auth-home', 'clean-assets', 'image-backend', 'reasoning', 'codex-bin', 'timeout-ms']
|
|
76
|
+
: ['project', 'target', 'source-capture', 'instructions', 'run-dir', 'auth-home', 'codex-bin', 'width', 'height'];
|
|
77
|
+
const options = Object.fromEntries(names.map(name => [name, { type: 'string' }]));
|
|
78
|
+
options.help = { type: 'boolean' };
|
|
79
|
+
const { values, positionals } = parseArgs({ args, options, allowPositionals: true });
|
|
80
|
+
const mode = positionals[0] ?? (command === 'improve' ? 'improve' : undefined);
|
|
81
|
+
const allowed = command === 'convert' ? ['convert', 'resume'] : ['improve', 'prepare', 'apply', 'resume'];
|
|
82
|
+
if (!values.help && (!allowed.includes(mode) || positionals.length > 1 || !values['run-dir'])) {
|
|
83
|
+
throw new Error(`Expected ${allowed.join('/')} and --run-dir; use --help`);
|
|
84
|
+
}
|
|
85
|
+
return { runDir: values['run-dir'] ? path.resolve(values['run-dir']) : undefined,
|
|
86
|
+
createsRun: ['convert', 'prepare', 'improve'].includes(mode), help: values.help };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function launch(command, args = process.argv.slice(2)) {
|
|
90
|
+
if (!['convert', 'export', 'finish', 'improve'].includes(command)) throw new Error(`Unknown runtime command: ${command}`);
|
|
91
|
+
const payload = fileURLToPath(new URL('./codex-conversion/', import.meta.url));
|
|
92
|
+
const cacheBase = process.env.TWELVE_UI_CODEX_RUNTIME_CACHE
|
|
93
|
+
?? path.join(os.homedir(), '.cache', '12ui', 'codex-conversion');
|
|
94
|
+
const cache = path.resolve(cacheBase);
|
|
95
|
+
const { runDir, createsRun, help } = parseRunInvocation(command, args);
|
|
96
|
+
const pinPath = runDir ? path.join(runDir, '.12ui-runtime.json') : undefined;
|
|
97
|
+
let runExisted = false;
|
|
98
|
+
if (runDir) {
|
|
99
|
+
try { await stat(runDir); runExisted = true; }
|
|
100
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
101
|
+
}
|
|
102
|
+
let runtime;
|
|
103
|
+
if (pinPath && !createsRun && !help) {
|
|
104
|
+
let pin;
|
|
105
|
+
try { pin = JSON.parse(await readFile(pinPath, 'utf8')); }
|
|
106
|
+
catch (error) { throw new Error(`Run has no valid runtime pin: ${pinPath}. Resume it with its original preserved skill/runtime; no new implementation was run.`); }
|
|
107
|
+
if (!/^[a-f0-9]{64}$/.test(pin.key)) throw new Error('Invalid run runtime key.');
|
|
108
|
+
const root = path.join(cache, pin.key);
|
|
109
|
+
if (await runtimeKey(root) !== pin.key) throw new Error(`Pinned runtime is missing or changed: ${root}. Restore the preserved runtime before resuming.`);
|
|
110
|
+
if (await readFile(path.join(root, '.12ui-ready'), 'utf8') !== pin.key) throw new Error(`Pinned runtime is not ready: ${root}`);
|
|
111
|
+
runtime = { root, key: pin.key };
|
|
112
|
+
} else runtime = await prepareRuntime(payload, cache);
|
|
113
|
+
const { root, key } = runtime;
|
|
114
|
+
const entry = await realpath(path.join(root, 'bin', `${command}.mjs`));
|
|
115
|
+
try {
|
|
116
|
+
await execute(process.execPath, [entry, ...args], {
|
|
117
|
+
env: { ...process.env, TWELVE_UI_CODEX_RUNTIME_KEY: key, TWELVE_UI_CODEX_RUNTIME_ROOT: root },
|
|
118
|
+
});
|
|
119
|
+
} finally {
|
|
120
|
+
if (pinPath && createsRun && !runExisted) {
|
|
121
|
+
try {
|
|
122
|
+
await writeFile(pinPath, JSON.stringify({ schemaVersion: 1, key }) + '\n', { flag: 'wx' });
|
|
123
|
+
} catch (error) { if (!['ENOENT', 'EEXIST'].includes(error.code)) throw error; }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (process.argv[1] && await realpath(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
129
|
+
try { await launch(process.argv[2], process.argv.slice(3)); }
|
|
130
|
+
catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
131
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Codex conversion
|
|
2
|
+
|
|
3
|
+
Node 22+ CLI for the demonstrated A workflow: one schema-constrained Astra ChatGPT planner response (low by default), followed by parallel image requests. The default external backend uses **OpenAI image API** requests with `gpt-image-2.5-flare` high; `--image-backend native` selects Codex-native image execution. It returns HTML/CSS and independent image files. It does not return native LayerDoc. External image calls require separate API credentials.
|
|
4
|
+
|
|
5
|
+
Install this package and an authenticated Codex CLI supporting the preserved invocation flags. Pass the existing Codex authentication directory explicitly; it must contain `auth.json` and `models_cache.json` with the selected Astra reasoning effort available. Credentials remain outside the run directory. The isolated planner environment excludes API keys; external images use `OPENAI_API_KEY` from the calling environment.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
12ui-convert convert --source design.png --run-dir ./new-run --auth-home /path/to/codex-home
|
|
9
|
+
12ui-convert resume --run-dir ./new-run --auth-home /path/to/codex-home
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`--reasoning low|medium` selects initial planner reasoning (default low); bounded contract repair remains medium. `--image-backend external|native` selects image execution (default external). All selectors are frozen for resume; conflicting explicit values are rejected.
|
|
13
|
+
|
|
14
|
+
`--codex-bin` selects the installed binary; `--timeout-ms` sets the per-call planner wall cap (1–1800000, default 1800000). `CODEX_CONVERSION_IMAGE_ENDPOINT` explicitly overrides the image endpoint and is frozen in the run configuration. Do not put credentials in that URL. Stdout contains one JSON result; progress goes to stderr. Exit code is zero only for completion.
|
|
15
|
+
|
|
16
|
+
Validation normalizes recoverable representation differences first, including exact integer strings, enum formatting, redundant omissions, a single JSON code fence and incompatible request grouping. Valid responses take the original fast path. When source or document issues remain unresolved, one additional Astra medium call repairs the saved response using the source and explicit issues. The original request is never repeated. Both responses, normalization changes and unresolved issues remain inspectable; exhausted repair returns those issues when no usable artifact can be produced. This contract repair is distinct from optional visual correction experiments; it does not automatically judge appearance or substitute images.
|
|
17
|
+
|
|
18
|
+
Each conversion needs a new directory. Source bytes, dimensions, prompt/schema hashes, implementation hash, model configuration and image endpoint are immutable. Resume rejects changed source or implementation; it can rebuild local output from previously received planner/provider bytes without credentials. A run can move to another directory, and its `output/` directory can be copied independently. The manifest records relative file paths, content hashes, source ownership and external dependencies, including allowed Google Fonts; those fonts still require network access.
|
|
19
|
+
|
|
20
|
+
Paid calls use durable `prepared`, `dispatched`, and `received` states. A response whose complete bytes and hash are in custody is reused. A planner final also recovers from its verified completion receipt. An interrupted dispatch without proven response custody returns `needs-reconciliation` and is never automatically submitted again. Keep the run directory for investigation; do not delete it and treat a fresh run as a free retry. An exclusive process lock prevents concurrent resumes. Dead local process locks can be reclaimed; locks from other hosts require investigation.
|
|
21
|
+
|
|
22
|
+
`result.json` separates initial planner, contract-repair and external image provider usage; these are usage records, not complete dollar accounting. `output/manifest.json` describes the portable artifact. `calls/` holds immutable request identities, response bytes, public metadata, planner event output and receipt hashes. Transport completion is recorded independently of payload syntax so a completed malformed response can be normalized or repaired. Never share the run directory without inspecting model and provider output for source content.
|
|
23
|
+
|
|
24
|
+
Validation: `pnpm test` and `pnpm typecheck` from this package. Tests use test-only provider/planner doubles and real filesystem/process interruption boundaries; real account behavior and visual quality require the separate frozen study.
|
|
25
|
+
|
|
26
|
+
## Install as a reusable Codex skill
|
|
27
|
+
|
|
28
|
+
Run `node bin/install-skill.mjs` from the installed package, or pass `--destination /absolute/skills/codex-convert`. The default destination is `${CODEX_HOME:-~/.codex}/skills/codex-convert`. Installation copies the bundled runtime and installs its production dependencies; an existing destination is rejected.
|
|
29
|
+
|
|
30
|
+
The source skill lives in `skill/codex-convert`. Its installed `scripts/convert.mjs` wrapper accepts the same conversion and resume arguments as the CLI.
|
|
31
|
+
|
|
32
|
+
### Native image custody and geometry
|
|
33
|
+
|
|
34
|
+
The native backend launches a separate Codex session with the native image tool enabled,
|
|
35
|
+
using Astra medium as coordinator. The underlying image model is not exposed. It uses
|
|
36
|
+
ChatGPT authentication from `--auth-home`, then `CODEX_HOME`, then `~/.codex`; no
|
|
37
|
+
`OPENAI_API_KEY` is required for native artwork. The local Codex CLI must support the
|
|
38
|
+
native image tool and the strict invocation flags used by this package.
|
|
39
|
+
|
|
40
|
+
Each group permits exactly one image generation. The durable call response retains the
|
|
41
|
+
original PNG bytes, original dimensions, tool audit, and quota observations. Native image
|
|
42
|
+
sizes can differ from the frozen frame: an explicit Lanczos3 exact-stretch derivative
|
|
43
|
+
maps the complete native image into that frame before taking the unchanged source-owner
|
|
44
|
+
crops. Receipts name the transform and scale factors; originals are never overwritten.
|
|
45
|
+
Six prior native study calls supported this treatment at rendered size, with fine detail
|
|
46
|
+
variation and no demonstrated quality advantage over the external route.
|
|
47
|
+
|
|
48
|
+
Completed calls replay without authentication or inference. Interrupted calls collect
|
|
49
|
+
existing completion records only; uncertain calls require reconciliation and are never
|
|
50
|
+
redispatched. Account quota snapshots are not per-image cost measurements.
|
|
51
|
+
|
|
52
|
+
Run the optional real-byte geometry/resume check against a retained native study directory:
|
|
53
|
+
`NATIVE_IMAGE_EVIDENCE=/absolute/path/to/native-v1 node --test test/native-images.test.mjs`.
|
|
54
|
+
|
|
55
|
+
## Original source crops
|
|
56
|
+
|
|
57
|
+
`--clean-assets crop|enhance` selects the requested treatment of clean opaque artwork (default `enhance`) and is frozen for resume. `crop` asks the planner to declare `crop-source` only for owners needing no removal, transparency or new detail. These original-pixel crops execute locally at native dimensions, with no AI call or implicit 2x scaling. Other owners still use the selected image backend. `preserve-source` retains its original AI enhancement meaning. Contradictory declarations remain explicit repair issues.
|
|
58
|
+
|
|
59
|
+
## Export and inspection
|
|
60
|
+
|
|
61
|
+
After rendering and inspecting the output, `node bin/export.mjs --source RUN/output --destination NEW_DIR --target assets|png|jpg|webp|pdf|react [--width PIXELS --height PIXELS]` creates deterministic derivatives without inference. PNG/JPG/WebP and browser PDF require Chromium (`npx playwright install chromium`). Omitted viewport dimensions use the source dimensions saved in the manifest, or a documented 1440×1000 viewport when no valid source geometry exists. PDF uses one full-content page by default; explicit `--height` selects paginated browser output. Raster metadata records actual rendered dimensions separately from the viewport. PDF is not a native LayerDoc export. React produces editable JSX with preserved document-global CSS and assets, suitable for one full page per document. It is not an isolated widget or application behavior implementation. `npm install && npm run build` in its output rebuilds edits. Source scripts/handlers are reported as unresolved actions.
|
|
62
|
+
|
|
63
|
+
For focused HTML/CSS corrections after parent Codex inspection, read the skill's `references/finishing.md`. `node bin/finish.mjs --source-dir RUN/output --patch PATCH.json --output-dir NEW_DIR` applies uniquely anchored replacements without inference. Both helpers require new destinations, preserve originals, and write manifests with actual derivative hashes. Keep the old installed runtime when upgrading: immutable old runs must resume with their original implementation. The installer rejects an existing destination; install the candidate to a separate path.
|
|
64
|
+
|
|
65
|
+
### Source-boundary grid normalization
|
|
66
|
+
|
|
67
|
+
Previously legal image frames and requests are unchanged. When an exact source boundary cannot fit the external provider's output grid, the executor preserves the complete original input and owner coordinates, requests a legal rounded canvas, then records a Lanczos3 mapping to the exact 2x delivery frame before cropping. Original provider bytes/PNG and the mapped frame remain separate. Unsupported size or aspect constraints still return explicit issues; this mapping does not invent padding, trim owners, or spend a planner repair call on that representational grid mismatch.
|
|
68
|
+
|
|
69
|
+
## Approved redesigns in existing projects
|
|
70
|
+
|
|
71
|
+
`12ui-improve improve --project /absolute/project --target /absolute/approved.png --source-capture /absolute/current.png --instructions /absolute/brief.md --run-dir /absolute/new-run --auth-home /absolute/codex-home` prepares and invokes one Astra low turn on the actual application. The brief supplies applicable project instructions, runtime/build commands, desired changes and behavior checks. `--codex-bin` selects the executable; `--width` and `--height` select the viewport (otherwise target dimensions). PNG/JPEG/WebP inputs are supported.
|
|
72
|
+
|
|
73
|
+
Use `prepare` with those inputs to freeze and inspect without dispatch, `apply --run-dir RUN --auth-home HOME` to start the prepared run once, and `resume --run-dir RUN` to reconcile saved work without dispatch. Preserve the current source capture, target, project content and durable records. Inspect actual code and browser behavior after execution; a completed call is not a visual-quality verdict. The standalone skill exposes the same commands through `scripts/improve.mjs`.
|
|
74
|
+
|
|
75
|
+
The Codex-specific 12ui-design skill coordinates draft/corpus/branch imagery through 12ui, single-image conversion through this package, and approved redesigns through local project edits. The shared API skill for other clients remains separate. Full hosted branch prototype assembly and native LayerDoc/SVG/PSD/PPTX/Sketch outputs remain explicit API features; a failed local run does not trigger them automatically. React export produces full-page JSX, not an application's data or behavior.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { convert, resume } from '../src/run.mjs';
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const { values, positionals } = parseArgs({ allowPositionals: true, options: {
|
|
7
|
+
source: { type: 'string' }, 'run-dir': { type: 'string' }, 'auth-home': { type: 'string' },
|
|
8
|
+
'clean-assets': { type: 'string' }, 'image-backend': { type: 'string' }, reasoning: { type: 'string' }, 'codex-bin': { type: 'string' }, 'timeout-ms': { type: 'string' }, help: { type: 'boolean' },
|
|
9
|
+
} });
|
|
10
|
+
if (values.help) {
|
|
11
|
+
console.log(JSON.stringify({ usage: '12ui-convert convert --source IMAGE --run-dir NEW_DIR --auth-home CODEX_HOME [--reasoning low|medium] [--clean-assets crop|enhance] [--image-backend external|native] | 12ui-convert resume --run-dir DIR [--auth-home CODEX_HOME]', imageCredentials: { external: 'OPENAI_API_KEY', native: 'Codex account via --auth-home' }, format: 'html-css', defaultImageBackend: 'external', defaultReasoning: 'low' }));
|
|
12
|
+
} else {
|
|
13
|
+
if (!values['run-dir'] || !['convert', 'resume'].includes(positionals[0]) || positionals.length !== 1) throw new Error('Expected convert or resume and --run-dir');
|
|
14
|
+
const options = { cleanAssets: values['clean-assets'], imageBackend: values['image-backend'], reasoning: values.reasoning, source: values.source, runDir: values['run-dir'], authHome: values['auth-home'], binary: values['codex-bin'], timeoutMs: values['timeout-ms'] ? Number(values['timeout-ms']) : undefined, progress: message => process.stderr.write(message + '\n') };
|
|
15
|
+
const result = await (positionals[0] === 'convert' ? convert(options) : resume(options));
|
|
16
|
+
console.log(JSON.stringify(result)); if (result.status !== 'complete') process.exitCode = 1;
|
|
17
|
+
}
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.log(JSON.stringify({ schemaVersion: 1, status: 'failed', error: { message: error.message } }));
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {parseArgs} from 'node:util';
|
|
3
|
+
import {exportOutput} from '../src/exports/index.mjs';
|
|
4
|
+
try{const {values}=parseArgs({options:{source:{type:'string'},destination:{type:'string'},target:{type:'string'},width:{type:'string'},height:{type:'string'},help:{type:'boolean'}}});if(values.help)console.log('export --source OUTPUT_DIR --destination NEW_DIR --target assets|png|jpg|webp|pdf|react [--width PIXELS --height PIXELS]. Omitted dimensions use the saved source, or 1440x1000 without source geometry. PDF uses one full-content page unless --height requests pagination. Raster/PDF require: npx playwright install chromium');else{if(!values.source||!values.destination||!values.target)throw Error('source, destination and target required');console.log(JSON.stringify(await exportOutput({sourceDir:values.source,outputDir:values.destination,target:values.target,width:values.width?Number(values.width):undefined,height:values.height?Number(values.height):undefined})));}}catch(error){console.error(error.message);process.exitCode=1;}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {parseArgs} from 'node:util';
|
|
3
|
+
import {finishOutput} from '../src/finish/index.mjs';
|
|
4
|
+
export async function main(args = process.argv.slice(2)) {
|
|
5
|
+
const {values} = parseArgs({args, options: {'source-dir': {type:'string'}, patch: {type:'string'}, 'output-dir': {type:'string'}}});
|
|
6
|
+
if (!values['source-dir'] || !values.patch || !values['output-dir']) throw Error('Expected --source-dir DIR --patch FILE --output-dir NEW_DIR');
|
|
7
|
+
const result = await finishOutput({sourceDir: values['source-dir'], patchFile: values.patch, outputDir: values['output-dir']});
|
|
8
|
+
console.log(JSON.stringify(result));
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
if (process.argv[1] === import.meta.filename) main().catch(error => {console.log(JSON.stringify({status:'failed', error: error.message})); process.exitCode = 1;});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {parseArgs} from 'node:util';
|
|
3
|
+
import {prepareProject,applyProject,resumeProject,improveProject} from '../src/project/run.mjs';
|
|
4
|
+
try {
|
|
5
|
+
const {values:v,positionals}=parseArgs({allowPositionals:true,options:{project:{type:'string'},target:{type:'string'},'source-capture':{type:'string'},instructions:{type:'string'},'run-dir':{type:'string'},'auth-home':{type:'string'},'codex-bin':{type:'string'},width:{type:'string'},height:{type:'string'},help:{type:'boolean'}}});
|
|
6
|
+
if(v.help)console.log('12ui-improve improve|prepare --project ABS --target IMAGE --source-capture IMAGE --instructions FILE --run-dir NEW [--auth-home HOME] [--codex-bin BIN] [--width N --height N]\n12ui-improve apply --run-dir RUN --auth-home HOME [--codex-bin BIN]\n12ui-improve resume --run-dir RUN\nImprove prepares and invokes one tool-enabled Astra low turn. Prepare makes no call. Apply starts a prepared run once. Resume only reconciles saved work and never dispatches. Images: PNG/JPEG/WebP; viewport defaults to target dimensions. Project instructions must include applicable rules, runtime/build and behavior checks.');
|
|
7
|
+
else {
|
|
8
|
+
const mode=positionals[0]??'improve';if(positionals.length>1||!['improve','prepare','apply','resume'].includes(mode)||!v['run-dir'])throw Error('Expected improve/prepare/apply/resume and --run-dir; use --help');
|
|
9
|
+
const options={project:v.project,target:v.target,sourceCapture:v['source-capture'],instructions:v.instructions,runDir:v['run-dir'],authHome:v['auth-home'],binary:v['codex-bin'],width:v.width===undefined?undefined:Number(v.width),height:v.height===undefined?undefined:Number(v.height)};
|
|
10
|
+
const result=await ({improve:improveProject,prepare:prepareProject,apply:applyProject,resume:resumeProject}[mode])(options);console.log(JSON.stringify(result));if(!['complete','prepared'].includes(result.status))process.exitCode=1;
|
|
11
|
+
}
|
|
12
|
+
}catch(error){console.error(JSON.stringify({status:'failed',error:{message:error.message}}));process.exitCode=1;}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cp, mkdir, readFile, rm, access } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const { values } = parseArgs({ options: { destination: { type: 'string' }, help: { type: 'boolean' } } });
|
|
11
|
+
if (values.help) {
|
|
12
|
+
console.log('node bin/install-skill.mjs [--destination /absolute/skills/codex-convert]\nInstalls a self-contained local skill and its npm production dependencies. Existing destinations are never replaced.');
|
|
13
|
+
} else {
|
|
14
|
+
const destination = resolve(values.destination ?? join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'skills/codex-convert'));
|
|
15
|
+
let created = false;
|
|
16
|
+
try {
|
|
17
|
+
const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
18
|
+
await access(join(root, 'skill/codex-convert/SKILL.md'));
|
|
19
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
20
|
+
await mkdir(destination);
|
|
21
|
+
created = true;
|
|
22
|
+
await cp(join(root, 'skill/codex-convert'), destination, { recursive: true });
|
|
23
|
+
const runtime = join(destination, 'runtime');
|
|
24
|
+
await mkdir(runtime);
|
|
25
|
+
for (const file of ['bin', 'src', 'templates', 'README.md', 'package.json', 'npm-shrinkwrap.json']) {
|
|
26
|
+
await cp(join(root, file), join(runtime, file), { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
const child = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['ci', '--omit=dev', '--no-audit', '--no-fund'], { cwd: runtime, stdio: 'inherit' });
|
|
29
|
+
if (child.error || child.status !== 0) throw child.error ?? new Error(`Dependency installation exited ${child.status}`);
|
|
30
|
+
console.log(JSON.stringify({ skill: 'codex-convert', version: manifest.version, destination, invoke: `node ${join(destination, 'scripts/convert.mjs')} --help` }));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (created) await rm(destination, { recursive: true, force: true });
|
|
33
|
+
console.error(`Skill installation failed: ${error.message}`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {parseArgs} from 'node:util';
|
|
3
|
+
import {recoverImage} from './recovery/run.mjs';
|
|
4
|
+
try {
|
|
5
|
+
const {values:v,positionals}=parseArgs({allowPositionals:true,options:{'original-run':{type:'string'},group:{type:'string'},'run-dir':{type:'string'},help:{type:'boolean'}}});
|
|
6
|
+
if(v.help)console.log(JSON.stringify({usage:'recover-image.mjs recover --original-run RUN --group group-NN --run-dir NEW_DIR | recover-image.mjs resume --run-dir RECOVERY_DIR',condition:'External Flare only; verified received HTTP500 JSON error with no image data; one new attempt, never unknown dispatch or completed images',credential:'OPENAI_API_KEY for a new request only'}));
|
|
7
|
+
else {
|
|
8
|
+
const mode=positionals[0];if(positionals.length!==1||!['recover','resume'].includes(mode)||!v['run-dir']||mode==='recover'&&(!v['original-run']||!v.group))throw Error('Expected recover/resume and required inputs; use --help');
|
|
9
|
+
const result=await recoverImage({originalRun:v['original-run'],group:v.group,runDir:v['run-dir']},{resume:mode==='resume'});console.log(JSON.stringify(result));if(result.status!=='complete')process.exitCode=1;
|
|
10
|
+
}
|
|
11
|
+
}catch(error){console.log(JSON.stringify({schemaVersion:1,status:error.code==='NEEDS_RECONCILIATION'?'needs-reconciliation':'failed',error:{message:error.message}}));process.exitCode=1;}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {hash,readJson,writeJson} from '../../src/run-store.mjs';
|
|
4
|
+
import {canonical} from '../../src/run-store-io.mjs';
|
|
5
|
+
import {assessContract} from '../../src/contract-repair.mjs';
|
|
6
|
+
import {prepareReference} from '../../src/images/geometry.mjs';
|
|
7
|
+
import {operationPrompt} from '../../src/images/prompt.mjs';
|
|
8
|
+
|
|
9
|
+
export async function readReceivedCall(root,id,folder='calls') {
|
|
10
|
+
const dir=path.join(root,folder,hash(id));
|
|
11
|
+
const state=await readJson(path.join(dir,'state.json'));
|
|
12
|
+
if(state?.status!=='received')throw Object.assign(Error(`Call ${id} is not confirmed received; reconcile without redispatch`),{code:'NEEDS_RECONCILIATION'});
|
|
13
|
+
const [custody,request,bytes]=await Promise.all([readJson(path.join(dir,'custody.json')),readJson(path.join(dir,'request.json')),fs.readFile(path.join(dir,'response.bin'))]);
|
|
14
|
+
const requestHash=hash(canonical(request)),responseHash=hash(bytes);
|
|
15
|
+
if(state?.id!==id||state.status!=='received'||state.requestHash!==requestHash||custody?.requestHash!==requestHash||state.responseHash!==responseHash||custody.responseHash!==responseHash||custody.length!==bytes.length||canonical(state.metadata)!==canonical(custody.metadata))throw Error(`Unverified received custody for ${id}; reconcile without redispatch`);
|
|
16
|
+
return {id,request,bytes,metadata:state.metadata,requestHash,responseHash};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function assertSameRequest(source,target,request) {
|
|
20
|
+
const current={endpoint:request.endpoint,model:'gpt-image-2.5-flare',quality:'high',size:`${target.frame.output.width}x${target.frame.output.height}`,background:target.background,prompt:operationPrompt(target),inputSha256:hash(await prepareReference(source,target))};
|
|
21
|
+
if(canonical(current)!==canonical(request))throw Error('Current executor request differs from the original; no new request dispatched');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function requireFailed500(call) {
|
|
25
|
+
let payload;try{payload=JSON.parse(call.bytes);}catch{throw Error('Failed response is not complete JSON; reconcile without redispatch');}
|
|
26
|
+
if(call.metadata?.status!==500||typeof payload?.error?.message!=='string'||payload.data!=null||payload.output!=null||payload.b64_json!=null)throw Error('Recovery requires a verified HTTP500 error with no image data; completed or unknown work is not retried');
|
|
27
|
+
if(call.request.backend||call.request.model!=='gpt-image-2.5-flare')throw Error('This recovery operation supports the external Flare transport only');
|
|
28
|
+
return payload.error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function prepareRecovery({originalRun,group,runDir}) {
|
|
32
|
+
originalRun=path.resolve(originalRun);runDir=path.resolve(runDir);
|
|
33
|
+
if(!/^group-\d+$/.test(group))throw Error('Expected exact group-NN id');
|
|
34
|
+
const identity=await readJson(path.join(originalRun,'run.json'));
|
|
35
|
+
const source=await fs.readFile(path.join(originalRun,identity.source.path));
|
|
36
|
+
if(hash(source)!==identity.source.sha256)throw Error('Original source identity changed');
|
|
37
|
+
const plan=await readJson(path.join(originalRun,'plan.json')),target=plan.groups.find(g=>g.id===group);
|
|
38
|
+
if(!target)throw Error('Requested group is absent from saved plan');
|
|
39
|
+
const failed=await readReceivedCall(originalRun,'image-'+group),error=requireFailed500(failed);
|
|
40
|
+
await assertSameRequest(source,target,failed.request);
|
|
41
|
+
let plannerId='planner';
|
|
42
|
+
if(await readJson(path.join(originalRun,'contract-repair.json')))plannerId='contract-repair-1';
|
|
43
|
+
const planner=await readReceivedCall(originalRun,plannerId),assessment=assessContract(planner.bytes,identity.source);
|
|
44
|
+
if(!assessment.ok||canonical(assessment.plan.assets)!==canonical(plan.assets)||canonical(assessment.plan.groups)!==canonical(plan.groups))throw Error('Saved planner and plan do not agree; no new request dispatched');
|
|
45
|
+
const inheritedCalls=[];
|
|
46
|
+
for(const g of plan.groups.filter(g=>g.id!==group)){
|
|
47
|
+
const c=await readReceivedCall(originalRun,'image-'+g.id);
|
|
48
|
+
if(c.metadata?.status<200||c.metadata?.status>=300)throw Error('Another image group is not complete; recovery requires all other groups received successfully');
|
|
49
|
+
inheritedCalls.push({id:c.id,requestHash:c.requestHash,responseHash:c.responseHash,metadata:c.metadata});
|
|
50
|
+
}
|
|
51
|
+
const inheritedAssets=[];
|
|
52
|
+
const targetIds=new Set(target.members.map(a=>a.id));
|
|
53
|
+
for(const asset of plan.assets.filter(a=>!targetIds.has(a.id))){
|
|
54
|
+
const receipt=await readJson(path.join(originalRun,asset.id+'-delivery.json')),bytes=await fs.readFile(path.join(originalRun,receipt.path));
|
|
55
|
+
if(hash(bytes)!==receipt.sha256)throw Error('Completed asset identity changed');
|
|
56
|
+
inheritedAssets.push({receipt,bytes});
|
|
57
|
+
}
|
|
58
|
+
await fs.mkdir(runDir);
|
|
59
|
+
await fs.writeFile(path.join(runDir,identity.source.path),source);
|
|
60
|
+
await fs.cp(path.join(originalRun,'calls'),path.join(runDir,'inherited-calls'),{recursive:true});
|
|
61
|
+
await fs.copyFile(path.join(originalRun,'result.json'),path.join(runDir,'original-result.json'));
|
|
62
|
+
await writeJson(path.join(runDir,'run.json'),{...identity,kind:'confirmed-http500-recovery',originalRun,createdAt:new Date().toISOString()});
|
|
63
|
+
await writeJson(path.join(runDir,'plan.json'),plan);
|
|
64
|
+
for(const a of inheritedAssets){await fs.writeFile(path.join(runDir,a.receipt.path),a.bytes);await writeJson(path.join(runDir,a.receipt.id+'-delivery.json'),a.receipt);}
|
|
65
|
+
const record={schemaVersion:1,originalRun,group,plannerId,source:identity.source,plannedHtmlSha256:hash(assessment.normalized.html),planSha256:hash(canonical(plan)),originalResultSha256:hash(await fs.readFile(path.join(runDir,'original-result.json'))),failedCall:{id:failed.id,request:failed.request,requestHash:failed.requestHash,responseHash:failed.responseHash,metadata:failed.metadata,error},inheritedCalls,inheritedAssets:inheritedAssets.map(a=>a.receipt),maximumNewAttempts:1,createdAt:new Date().toISOString()};
|
|
66
|
+
await writeJson(path.join(runDir,'recovery.json'),record);
|
|
67
|
+
return record;
|
|
68
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {createStore,withRunLock,hash,readJson,writeJson} from '../../src/run-store.mjs';
|
|
4
|
+
import {canonical} from '../../src/run-store-io.mjs';
|
|
5
|
+
import {assessContract} from '../../src/contract-repair.mjs';
|
|
6
|
+
import {executeImages} from '../../src/images/execute.mjs';
|
|
7
|
+
import {publishOutput} from '../../src/output.mjs';
|
|
8
|
+
import {prepareRecovery,readReceivedCall,requireFailed500,assertSameRequest} from './prepare.mjs';
|
|
9
|
+
|
|
10
|
+
export async function recoverImage(options,{resume=false}={}) {
|
|
11
|
+
const runDir=path.resolve(options.runDir);
|
|
12
|
+
if(!resume)await prepareRecovery({...options,runDir});
|
|
13
|
+
return withRunLock(runDir,async()=>{
|
|
14
|
+
const started=Date.now(),startedAt=new Date().toISOString();
|
|
15
|
+
try {
|
|
16
|
+
const record=await readJson(path.join(runDir,'recovery.json'));
|
|
17
|
+
if(!record||record.maximumNewAttempts!==1)throw Error('No supported recovery identity');
|
|
18
|
+
if(options.group&&options.group!==record.group||options.originalRun&&path.resolve(options.originalRun)!==record.originalRun)throw Error('Recovery input differs from recorded identity');
|
|
19
|
+
const sourcePath=path.join(runDir,record.source.path),source=await fs.readFile(sourcePath),plan=await readJson(path.join(runDir,'plan.json'));
|
|
20
|
+
if(hash(source)!==record.source.sha256||hash(canonical(plan))!==record.planSha256)throw Error('Recovery source or plan changed');
|
|
21
|
+
const failed=await readReceivedCall(runDir,record.failedCall.id,'inherited-calls');requireFailed500(failed);
|
|
22
|
+
if(failed.requestHash!==record.failedCall.requestHash||failed.responseHash!==record.failedCall.responseHash)throw Error('Original failed call changed');
|
|
23
|
+
const target=plan.groups.find(g=>g.id===record.group);await assertSameRequest(source,target,failed.request);
|
|
24
|
+
const planner=await readReceivedCall(runDir,record.plannerId,'inherited-calls'),assessment=assessContract(planner.bytes,record.source);
|
|
25
|
+
if(!assessment.ok||hash(assessment.normalized.html)!==record.plannedHtmlSha256)throw Error('Original planned HTML changed');
|
|
26
|
+
for(const asset of record.inheritedAssets)if(hash(await fs.readFile(path.join(runDir,asset.path)))!==asset.sha256)throw Error('Inherited asset changed');
|
|
27
|
+
const env={...(options.env??process.env),CODEX_CONVERSION_IMAGE_ENDPOINT:failed.request.endpoint};
|
|
28
|
+
const images=await executeImages({plan:{...plan,assets:target.members,groups:[target]},sourcePath,runDir,store:createStore(runDir),env,progress:undefined,imageBackend:'external',...(options.fetchImpl?{fetchImpl:options.fetchImpl}:{})});
|
|
29
|
+
const output=await publishOutput({runDir,normalized:assessment.normalized,assets:[...record.inheritedAssets,...images.assets],source:record.source,plan});
|
|
30
|
+
const unchangedAssets=[];
|
|
31
|
+
for(const a of record.inheritedAssets){const bytes=await fs.readFile(path.join(runDir,'output',a.id+'.png'));if(hash(bytes)!==a.sha256)throw Error('Published inherited asset changed');unchangedAssets.push({id:a.id,sha256:a.sha256});}
|
|
32
|
+
if(hash(await fs.readFile(path.join(runDir,'output/index.html')))!==record.plannedHtmlSha256)throw Error('Published planned HTML changed');
|
|
33
|
+
const result={schemaVersion:1,status:'complete',kind:'confirmed-provider-failure-recovery',output,originalRun:record.originalRun,receipts:'calls/',inheritedReceipts:'inherited-calls/',timing:{startedAt,completedAt:new Date().toISOString(),thisResumeMs:Date.now()-started,elapsedSinceRecoveryCreationMs:Date.now()-Date.parse(record.createdAt)},usage:{newCodexCalls:0,newImageCalls:images.usage.filter(g=>!g.reused).length,externalImages:images.usage},unchangedAssets,htmlSha256:record.plannedHtmlSha256,uncertainties:plan.uncertainties};
|
|
34
|
+
await writeJson(path.join(runDir,'result.json'),result);return result;
|
|
35
|
+
}catch(error){const result={schemaVersion:1,status:error.code==='NEEDS_RECONCILIATION'?'needs-reconciliation':'failed',error:{message:error.message},receipts:'calls/',inheritedReceipts:'inherited-calls/',timing:{startedAt,completedAt:new Date().toISOString(),thisResumeMs:Date.now()-started}};await writeJson(path.join(runDir,'result.json'),result);return result;}
|
|
36
|
+
});
|
|
37
|
+
}
|