mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 18:47:02 +03:00
Port /impeccable generate to the engine crates
The Node-era server, CLI, hook, and pin halves of the generate command move into the Rust workspace, with the protocol unchanged: - crates/live: POST /agent-target is held open on a channel plus a timer thread (the manual-apply deferred pattern), releasing its turnstile ticket before it parks like /poll; /agent-target-result resolves it; /agent-target-claim is the roll call with its renewable lease. SSE connections carry the overlay's clientId: a late overlay is replayed every pending target, and a disconnect retires that overlay's report, releases its lease, and re-judges each roll call. Shutdown drains held requests with server_stopping. - crates/live/src/live_generate.rs: the live-generate verb (the router already forwards every live* verb), same flags, verdicts, and _instructions, spelled with the engine's self command. - crates/hook: every entry stands down on live preview markers (skipped: live-preview), checking the proposed content and the file on disk for hook-before-edit. - crates/context: pin accepts generate; the crate's command-metadata.json copy carries its entry. Tests: crates/cli/tests/agent_target.rs (six HTTP cases with an SSE reader), tests/live-agent-target.test.mjs rewritten to drive the binary (28 cases, registered in the live suite), hook stand-down cases, oracle goldens for live-generate plus the re-recorded pin list goldens, the e2e prompt assertion waiting for the journaled event, and the contract documented in docs/CLI-CONTRACT.md. AI-assisted: implemented and tested with Claude Code under maintainer direction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Abdul Wahab
co-authored by
Claude Fable 5
parent
fc89b0ed62
commit
d397140a77
+18
-6
@@ -685,7 +685,7 @@ Tier 2 (`staleness-deep.mjs`, doctor only):
|
||||
#### `pin.mjs` -> `impeccable pin`
|
||||
|
||||
- **Invoked from**: `SKILL.src.md`: `node {{scripts_path}}/pin.mjs <pin|unpin> <command>`; "Report the script's result concisely; relay stderr verbatim on error."
|
||||
- **CLI args**: exactly `argv[2]` = action (`pin`|`unpin`), `argv[3]` = command. Missing either -> stdout `Usage: node pin.mjs <pin|unpin> <command>` + `\nAvailable commands: <VALID_COMMANDS joined ', '>`, exit 1. Bad action -> stderr `Unknown action: <a>. Use 'pin' or 'unpin'.`, exit 1. Bad command -> stderr `Unknown command: <c>` and `Available commands: ...`, exit 1. `VALID_COMMANDS = craft, init, extract, document, shape, critique, audit, polish, bolder, quieter, distill, harden, onboard, live, animate, colorize, typeset, layout, delight, overdrive, clarify, adapt, optimize` (23; `doctor`, `teach` not included).
|
||||
- **CLI args**: exactly `argv[2]` = action (`pin`|`unpin`), `argv[3]` = command. Missing either -> stdout `Usage: node pin.mjs <pin|unpin> <command>` + `\nAvailable commands: <VALID_COMMANDS joined ', '>`, exit 1. Bad action -> stderr `Unknown action: <a>. Use 'pin' or 'unpin'.`, exit 1. Bad command -> stderr `Unknown command: <c>` and `Available commands: ...`, exit 1. `VALID_COMMANDS = craft, init, extract, document, shape, critique, audit, polish, bolder, quieter, distill, harden, onboard, live, animate, colorize, typeset, layout, delight, overdrive, clarify, adapt, optimize, generate` (24; `doctor`, `teach` not included).
|
||||
- **Env vars**: none.
|
||||
- **Inputs**: project root = walk up from cwd until a dir containing `package.json`, `.git`, or `skills-lock.json` (stops at `/`; falls back to cwd). Harness dirs `HARNESS_DIRS = .claude .cursor .gemini .codex .agents .agent .github .grok .hermes .trae .trae-cn .pi .opencode .kiro .rovodev .vibe .qoder`; a harness is used only if `<root>/<h>/skills/impeccable` or `<root>/<h>/skills/i-impeccable` exists. `command-metadata.json` next to the script (`{ [command]: { description, argumentHint } }`).
|
||||
- **Outputs/side effects** (`pin`): no harness dirs -> stdout `No harness directories with impeccable installed found.`, exit 0. For each harness skills dir: `<skillsDir>/<command>/SKILL.md`; if it exists without the marker `<!-- impeccable-pinned-skill -->` -> ` SKIP: <dir> (non-pinned skill already exists)`; else mkdir + write, print ` + <dir>`. Then if any created: `\nPinned '<command>' as a standalone shortcut in <n> location(s).` and `Use the pinned command directly in each harness.`. Content (prefix `$` and codex frontmatter when the harness dir basename is `.codex` or `.agents`, else `/`):
|
||||
@@ -862,6 +862,8 @@ getCachePath(cwd) = <cwd>/.impeccable/hook.cache.json
|
||||
getPendingPath(cwd) = <cwd>/.impeccable/hook.pending.json (only ever deleted by hook-admin reset; never written)
|
||||
```
|
||||
|
||||
LIVE_PREVIEW_MARKERS: content containing `data-impeccable-variants=` or `impeccable-carbonize-start` (the wrapper a live generate publishes and the block a carbonize accept leaves until cleanup). Every hook entry stands down on it: `hook` records `skipped:'live-preview'` for the per-edit pass and skips the file silently in the Stop pass; `hook-before-edit` allows with `skipped:'live-preview'`.
|
||||
|
||||
#### 0.2 Config: `readConfig(cwd)`
|
||||
|
||||
Reads, in order, `config.json` then `config.local.json` (later wins for scalars, arrays are unioned). Each file is parsed with `JSON.parse`; a missing or malformed file is treated as `null` (silently ignored). For each file:
|
||||
@@ -1103,12 +1105,12 @@ Candidates in order: `<scripts>/detector/detect-antipatterns.mjs` (built skill l
|
||||
4. `config = readConfig(projectCwd)`; `enabled === false` → `'config-disabled'`.
|
||||
5. native platform → `skipped:'native-platform', platform`.
|
||||
6. `cache = readCache(projectCwd)`; `sessionId = event.session_id || 'unknown'`; detector missing → `'detector-missing'`; `scanOptions = designSystemOptions(...)`; `tiered = perEditTieringActive(config, harness)`; `quietMode = truthy(IMPECCABLE_HOOK_QUIET) || config.quiet`.
|
||||
7. For each target file (audit.file updated each iteration): skip with `lastSkip` = `'sensitive'` (contains `..` or SENSITIVE_PATH), `'generated'`, `'extension'` (not ALLOWED and not configured), `'config-ignore-file'` (`matchesAnyGlob(relativized)` or `(absolute)` vs `config.ignoreFiles`), `'file-missing'`, `'outside-project'`, `'too-large'` (records `skippedBytes`). If the file is a PRIMARY (not co-scanned): `editCount = bumpEditCount(...)`; if `editCount > 6` → if `=== 7` and no suppression winner yet → `suppressionWinner={filePath}`; `lastSkip='suppressed'`, `suppressedHit=true`, continue. Read content, run detector (throw → `findings=[]`, `detectorThrew=true`). `filtered = filterFindings(...)`; if tiered split into immediate/deferred else all immediate. If deferred non-empty → `touchFile`, `deferredTotal += n`. `fresh = dedupeAgainstCache(immediate, ...)`. `audit.findings = raw count`, `audit.freshFindings = fresh.length`, `audit.deferred = deferredTotal` (if >0). If detectorThrew → `detectorThrewAny=true`, continue (cache untouched for that file). `rememberFindings(cache, sid, file, immediate)` (replace). If fresh>0 → push `{filePath, findings: fresh}` to `freshGroups`, continue. Else if immediate>0 and no pendingWinner → `pendingWinner={filePath, known: immediate.map(findingCacheKey)}`; else if immediate==0 and no cleanWinner: if quiet or not ack-eligible → `cleanWinner={filePath}` (without consuming `cleanAcked`); else if `fileEntry.cleanAcked` → `cleanAckDeduped=true` (keep scanning); else set `cleanAcked=true`, `cleanWinner={filePath}`, `cleanAckDeduped=false`.
|
||||
7. For each target file (audit.file updated each iteration): skip with `lastSkip` = `'sensitive'` (contains `..` or SENSITIVE_PATH), `'generated'`, `'extension'` (not ALLOWED and not configured), `'config-ignore-file'` (`matchesAnyGlob(relativized)` or `(absolute)` vs `config.ignoreFiles`), `'file-missing'`, `'outside-project'`, `'too-large'` (records `skippedBytes`), and, once the content is read, `'live-preview'` (LIVE_PREVIEW_MARKERS). If the file is a PRIMARY (not co-scanned): `editCount = bumpEditCount(...)`; if `editCount > 6` → if `=== 7` and no suppression winner yet → `suppressionWinner={filePath}`; `lastSkip='suppressed'`, `suppressedHit=true`, continue. Read content, run detector (throw → `findings=[]`, `detectorThrew=true`). `filtered = filterFindings(...)`; if tiered split into immediate/deferred else all immediate. If deferred non-empty → `touchFile`, `deferredTotal += n`. `fresh = dedupeAgainstCache(immediate, ...)`. `audit.findings = raw count`, `audit.freshFindings = fresh.length`, `audit.deferred = deferredTotal` (if >0). If detectorThrew → `detectorThrewAny=true`, continue (cache untouched for that file). `rememberFindings(cache, sid, file, immediate)` (replace). If fresh>0 → push `{filePath, findings: fresh}` to `freshGroups`, continue. Else if immediate>0 and no pendingWinner → `pendingWinner={filePath, known: immediate.map(findingCacheKey)}`; else if immediate==0 and no cleanWinner: if quiet or not ack-eligible → `cleanWinner={filePath}` (without consuming `cleanAcked`); else if `fileEntry.cleanAcked` → `cleanAckDeduped=true` (keep scanning); else set `cleanAcked=true`, `cleanWinner={filePath}`, `cleanAckDeduped=false`.
|
||||
8. If `freshGroups` non-empty: `text = appendDesignSystemNoteOnce(renderGroupedTemplate(freshGroups, config, {cwd:projectCwd, footer: footerModeForSession, reserveChars: designNoteReserve}), ...)`; `commitFooterShown`; **`persistCache` always** (creates `.impeccable/` if needed); return `stdout = payload(text,'PostToolUse',harness)`, audit `{..., file: firstGroup.filePath, emitted:true, freshFiles, freshFindings(total), chars, durationMs}`, `emission:{kind:'fresh', file, findings, groups}`.
|
||||
9. Else compute `ack`: not quiet AND pendingWinner AND ack-eligible → `{kind:'pending', text: appendDesignSystemNoteOnce(renderPendingAck(...))}`; else not quiet AND no suppressionWinner AND cleanWinner AND !cleanAckDeduped AND ack-eligible → `{kind:'clean', text: appendDesignSystemNoteOnce(renderCleanAck(...))}`.
|
||||
10. Persist cache only if `deferredTotal > 0 || (cacheDirty && exists(<projectCwd>/.impeccable))` (a clean edit in a project with no `.impeccable/` footprint writes nothing to disk).
|
||||
11. Return precedence: `detectorThrewAny && !pendingWinner && !cleanWinner` → audit `{emitted:false, error:'detector-threw'}`; quiet → `{emitted:false, quiet:true}`; pending ack → stdout payload, audit `{file, emitted:true, kind:'pending', pending:<n>, chars}`; suppressionWinner → stdout `payload(suppressionNotice(relativize(file)))`, audit `{file, suppressed:true, emitted:true}`; clean ack → stdout payload, audit `{file, emitted:true, kind:'clean', chars}`; pendingWinner (non-UI) → `{emitted:false, skipped:'non-ui-ack'}`; cleanWinner → same `'non-ui-ack'`; cleanAckDeduped → `skipped:'clean-ack-deduped'`; suppressedHit → `{suppressed:true, emitted:false}`; else `{skipped:lastSkip, bytes?:skippedBytes (only when 'too-large')}`. Any exception → `{exitCode:0, stdout:'', audit:{..., error}}`.
|
||||
- **Stop algorithm (`runStopHook`)**: re-entrancy/disabled/malformed/empty as above; `event.stop_hook_active === true` → `skipped:'stop-hook-active'` (no scan, no output; prevents Claude Code re-invocation loops, issue #400). `projectCwd = resolve(event.cwd || cwd)` (no file-based re-keying); `sessionId = event.session_id || 'unknown'`; config disabled → `'config-disabled'`; `touched = keys(cache.sessions[sid].files)`; empty → `'no-touched-files'`; native → `'native-platform'`; detector missing → `'detector-missing'`. Iterate touched files (max 20 scanned): same skips (sensitive/generated/extension/ignoreFiles/missing/outside-project) silently; read (unreadable → skip); detect with full rule set (no tiering); `filtered = filterFindings`; `fresh = dedupeAgainstCache`; if fresh → `rememberFindings(cache, sid, file, fresh)` (NOTE: replaces the file's remembered set with only the fresh ones), push group. `audit.scannedFiles`. No groups → `{emitted:false, skipped:'stop-clean'}`. Else render grouped with footer mode + reserve, `appendDesignSystemNoteOnce`, `commitFooterShown`, `persistCache`, stdout `payload(text,'Stop',harness)`, audit `{emitted:true, freshFiles, freshFindings, chars, durationMs}`, `emission:{kind:'stop-deep-pass', groups}`.
|
||||
- **Stop algorithm (`runStopHook`)**: re-entrancy/disabled/malformed/empty as above; `event.stop_hook_active === true` → `skipped:'stop-hook-active'` (no scan, no output; prevents Claude Code re-invocation loops, issue #400). `projectCwd = resolve(event.cwd || cwd)` (no file-based re-keying); `sessionId = event.session_id || 'unknown'`; config disabled → `'config-disabled'`; `touched = keys(cache.sessions[sid].files)`; empty → `'no-touched-files'`; native → `'native-platform'`; detector missing → `'detector-missing'`. Iterate touched files (max 20 scanned): same skips (sensitive/generated/extension/ignoreFiles/missing/outside-project) silently; read (unreadable → skip; LIVE_PREVIEW_MARKERS in the content → skip); detect with full rule set (no tiering); `filtered = filterFindings`; `fresh = dedupeAgainstCache`; if fresh → `rememberFindings(cache, sid, file, fresh)` (NOTE: replaces the file's remembered set with only the fresh ones), push group. `audit.scannedFiles`. No groups → `{emitted:false, skipped:'stop-clean'}`. Else render grouped with footer mode + reserve, `appendDesignSystemNoteOnce`, `commitFooterShown`, `persistCache`, stdout `payload(text,'Stop',harness)`, audit `{emitted:true, freshFiles, freshFindings, chars, durationMs}`, `emission:{kind:'stop-deep-pass', groups}`.
|
||||
- **Outputs**:
|
||||
- stdout: exactly one JSON document (no trailing newline) when something is emitted, else nothing. Claude/Codex/Grok: `{"hookSpecificOutput":{"hookEventName":"PostToolUse"|"Stop","additionalContext":"<text>"}}`. Cursor-shaped: `{"additional_context":"<text>"}`. GitHub: `{"additionalContext":"<text>"}`.
|
||||
- stderr: only `[impeccable-hook] <err>` when `IMPECCABLE_HOOK_DEBUG` and an unexpected top-level error.
|
||||
@@ -1157,7 +1159,7 @@ DOM scans, design-system findings, co-scanned stylesheets without their own base
|
||||
2. stdin parse error → `'stdin-malformed'`; empty/non-object → `'stdin-empty'`.
|
||||
3. no filePath → `'no-file-path'`; outside project → `'outside-project'`; SENSITIVE → `'sensitive'`; GENERATED → `'generated'`.
|
||||
4. `config = readConfig(cwd)`; ext not allowed and not configured → `'extension'`.
|
||||
5. content skip object → that reason; empty content → `'no-proposed-content'`.
|
||||
5. content skip object → that reason; empty content → `'no-proposed-content'`; content over the cap → `'content-too-large'`; LIVE_PREVIEW_MARKERS in the proposed content OR in the file on disk → `'live-preview'` (a live variant session owns files carrying preview scaffolding; nagging mid-cycle derails it, and `live-complete` verifies the file once the accepted variant is permanent).
|
||||
6. `config.enabled === false` → `'config-disabled'`; native platform → `'native-platform'` (+`platform`).
|
||||
7. ignoreFiles glob (relative or absolute) → `'config-ignore-file'`.
|
||||
8. detector missing → `'detector-missing'`; `scanOptions = designSystemOptions`.
|
||||
@@ -1466,7 +1468,7 @@ Binds `127.0.0.1:PORT`. CORS: if request has `Origin` and (origin is loopback ht
|
||||
| `GET /design-system.json?token=` | 401 `Unauthorized` | 404 `{present:false}` if neither DESIGN.md nor `.impeccable/design.json`; else `{present:true, hasMd, hasSidecar, mdNewerThanJson, parsed?, parseError?, sidecar?, sidecarError?}` (`parsed` = parseDesignMd output; `sidecarError` = `'Failed to parse .impeccable/design.json: '+msg`) |
|
||||
| `GET /design-system/raw?token=` | 401 | 200 `text/markdown; charset=utf-8` DESIGN.md verbatim; 404 `Not found` |
|
||||
| `GET /source?token=&path=` | 401 | path required and no `..` else 400 `Bad path`; resolved must be inside cwd (relative check, not root itself) else 403 `Forbidden`; 404 `File not found`; 200 `text/html; charset=utf-8` raw file. Used by browser to read source, svelte manifest and `params.json`. |
|
||||
| `GET /events?token=` (SSE) | 401 | headers `text/event-stream`, `Cache-Control: no-cache`, `Connection: keep-alive`; first frame `data: {"type":"connected","hasProjectContext":b,"agentPolling":b,"activeSessions":[…]}\n\n`; `: keepalive\n\n` every 30s; on connect: cancels exit timer and removes queued anonymous `exit` events. On close: if 0 clients, after 8000 ms (still 0) enqueue `{type:'exit'}`. |
|
||||
| `GET /events?token=&clientId=` (SSE) | 401 | `clientId` (optional) is the overlay's per-page-load id; on close the server retires that client's agent-target roll-call report and releases a lease it held, then re-judges each pending roll call against the remaining clients. Headers `text/event-stream`, `Cache-Control: no-cache`, `Connection: keep-alive`; first frame `data: {"type":"connected","hasProjectContext":b,"agentPolling":b,"activeSessions":[…]}\n\n`; `: keepalive\n\n` every 30s; on connect: cancels exit timer and removes queued anonymous `exit` events. On close: if 0 clients, after 8000 ms (still 0) enqueue `{type:'exit'}`. |
|
||||
| `POST /events` | body JSON `token` mismatch → 401 `{"error":"Unauthorized"}`; invalid JSON → 400 `{"error":"Invalid JSON"}` | see 6.1 |
|
||||
| `GET /stop?token=` | 401 | 200 text `stopping`, then shutdown |
|
||||
| `GET /poll?token=&timeout=&leaseMs=&types=` | 401 `{"error":"Unauthorized"}` | see 6.3 |
|
||||
@@ -1477,6 +1479,9 @@ Binds `127.0.0.1:PORT`. CORS: if request has `Origin` and (origin is loopback ht
|
||||
| `POST /manual-edit-repair-decision` (token body or query) | 401 | see 10 |
|
||||
| `POST /manual-edit-discard?token=&pageUrl=` | 401 | see 10 |
|
||||
| `POST /manual-edit` | | 410 `{"error":"/manual-edit is removed; use /manual-edit-stash and /manual-edit-commit for staged copy edits."}` |
|
||||
| `POST /agent-target` | body JSON `token` mismatch → 401 `{"error":"Unauthorized"}`; invalid JSON → 400 `{"error":"Invalid JSON"}` | Agent-initiated targeting (the `generate` command). Validation (400 `{"error":<msg>}`, messages verbatim): `agent_target: selector is required`, `agent_target: selector too long` (>1000 chars), `agent_target: invalid action (valid: <VISUAL_ACTIONS joined ', '>)`, `agent_target: count must be 1-8`, `agent_target: text must be a string of at most 500 chars`, `agent_target: index must be a positive integer (1-based)`, `agent_target: prompt must be a string of at most 2000 chars`, `agent_target: dryRun must be a boolean`. No SSE client → 200 `{ok:false, error:'no_browser_connected'}`. Otherwise mint an 8-hex `targetId`, broadcast `agent_target` (see 6.2), and **hold the response** until `/agent-target-result` resolves it, every connected overlay has declined (busy roll call, see `/agent-target-claim`), or `IMPECCABLE_AGENT_TARGET_TIMEOUT_MS` (default 15000) elapses: busy verdict `{ok:false, error:'busy', state, reason}` from the first report when any report exists, else `{ok:false, error:'browser_timeout', timeoutMs}`. The held reply is 200 `{targetId, ...result}`; shutdown resolves every held request with `{ok:false, error:'server_stopping'}`. |
|
||||
| `POST /agent-target-result` | 401 / 400 Invalid JSON | `targetId` (non-empty string) required else 400 `{"error":"agent_target_result: missing targetId"}`; the remaining body fields (minus `token`) resolve the held request; 200 `{ok:true, delivered:boolean}` (`delivered:false` when nothing awaits that id). |
|
||||
| `POST /agent-target-claim` | 401 / 400 Invalid JSON | `targetId` and `clientId` (non-empty strings) required else 400 `{"error":"agent_target_claim: missing targetId or clientId"}`. Roll call plus a first-wins lease, so exactly one overlay acts on a broadcast target. Unknown or resolved target → `{ok:true, granted:false, pending:false}` (ends a rescuer's retry loop). `eligible !== true` → record `{state, reason}` under `clientId` (replacing an earlier report), release the lease if this client holds it, answer `{ok:true, granted:false}`, then complete the roll call when no owner holds the lease and reports ≥ connected SSE clients (verdict from the first report). `eligible === true` → drop this client's earlier report; `granted` when no owner, the same owner (renew), or the lease lapsed (`IMPECCABLE_AGENT_TARGET_CLAIM_LEASE_MS`, default 3000); answer `{ok:true, granted, pending:true}`. |
|
||||
| anything else | | 404 `Not found` |
|
||||
|
||||
Pending-event summary in `/status.pendingEvents[]`: `{id, type, leased:boolean, leaseUntil:number|null}` plus for `manual_edit_apply`: `pageUrl, chunk, repair, evidencePath, agentAction, manualApplySummary:{pageUrl, chunk, entryCount, opCount, files[]}`.
|
||||
@@ -1534,7 +1539,7 @@ Missed-completion redelivery: on a `checkpoint` with `phase==='generating'` and
|
||||
Generation checkpoint recording (only for reasons `variants_progress|variants_ready`, arrived>0, expected>0, session not canceled): broadcast `{type:'variant_progress', id, file: previewFile||file, sourceFile, previewFile, previewMode ('source' default), arrivedVariants, expectedVariants, publicationKind: event.publicationKind||'variants'}` and record agent phases `first_reviewable` (once), `second_reviewable` (arrived≥2 && expected≥3, once), `all_variants_ready` (arrived≥expected, once), each `{arrivedVariants, expectedVariants, checkpointReason, at}`.
|
||||
|
||||
#### 6.2 Server → browser (SSE `data:` JSON frames)
|
||||
`connected`, `agent_polling {connected}`, `agent_phase {id, phase, at, durationMs?, previewMode?, owner?}`, `variant_progress {…}`, `done`/`steer_done`/`complete`/`agent_done`/`discarded`/`error`/`discard`/any reply type: `{type: msg.type||'done', id, message, file, sourceFile, previewFile, previewMode, data}` (forwarded from `POST /poll`), redelivered `done`, manual-edit activity entries `{seq, type:'manual_edit_*', ts, …details}`.
|
||||
`connected`, `agent_polling {connected}`, `agent_phase {id, phase, at, durationMs?, previewMode?, owner?}`, `variant_progress {…}`, `agent_target {targetId, selector, text?, index?, action, count, prompt?, dryRun?}` (pushed by `POST /agent-target` and replayed to every overlay that connects while the target is pending), `done`/`steer_done`/`complete`/`agent_done`/`discarded`/`error`/`discard`/any reply type: `{type: msg.type||'done', id, message, file, sourceFile, previewFile, previewMode, data}` (forwarded from `POST /poll`), redelivered `done`, manual-edit activity entries `{seq, type:'manual_edit_*', ts, …details}`.
|
||||
|
||||
Manual-edit activity types broadcast: `manual_edit_stashed, manual_edit_discarded, manual_edit_commit_started, manual_edit_apply_dispatched, manual_edit_apply_reply_received, manual_edit_apply_reply_invalid, manual_edit_apply_stale_reply_rejected, manual_edit_apply_timeout, manual_edit_repair_needs_decision, manual_edit_repair_rollback_done, manual_edit_commit_done, manual_edit_commit_failed, manual_edit_transaction_rolled_back, manual_edit_poll_reply_unknown`.
|
||||
|
||||
@@ -1792,6 +1797,13 @@ Conventions: every script's "run directly" guard is `process.argv[1]` ending wit
|
||||
#### `live-target.mjs`
|
||||
- Library only (`resolveLiveTarget(cwd,args)` → `{originalCwd, projectRoot, targetPath, absoluteTargetPath, targetOptions}`); used by `live.mjs`. Tests: `tests/live-target-context.test.mjs`.
|
||||
|
||||
#### `live-generate.mjs` -> `impeccable live-generate`
|
||||
- **Invoked from**: `skill/reference/generate.md` (the `generate` command), after `impeccable live` booted the helper and the agent opened the app page: `impeccable live-generate --selector "section.pricing" --action bolder --count 3`.
|
||||
- **Args**: `--selector <css>` (required), `--text <snippet>`, `--index <n>` (1-based), `--action <name>` (default `impeccable`), `--count <n>` (default 3, 1-8), `--prompt <text>`, `--dry-run`, `--wait-for-browser <ms>`, `--target <path>` (consumed by `enterLiveRoot`), `--help`. A flag without a value → stdout `{"ok":false,"error":"missing_flag_value","flag":"--x"}`, exit 1.
|
||||
- **Env**: `IMPECCABLE_SELF` (how the boot and poll verbs are spelled in `_instructions`).
|
||||
- **Behavior**: `enterLiveRoot`; local verdicts first, each pretty-printed JSON on stdout with `_instructions`, exit 1: `selector_required`, `invalid_action` (+`action`, `validActions`), `invalid_count` (+`count`), `invalid_index` (+`index`), `invalid_wait` (+`wait`); no `server.json` (or one without port/token) → `server_not_running`. With `--wait-for-browser`, `GET /status` once a second until `connectedClients > 0` or the budget ends (`no_browser_connected` + `waitedMs`); an unanswered `/status` → `server_unreachable`. Then `POST /agent-target` with `{token, selector, action, count, text?, index?, prompt?, dryRun?}` under a 20 s client cap: a transport timeout → `request_timeout` (+`detail`, browser_timeout instructions), any other transport failure → `server_unreachable` (+`detail`); a non-2xx answer → `{ok:false, error:<body.error or http_<status>>, ...body}`; an unparseable body → `bad_server_response` (+`status`). A 2xx answer is printed as received plus `_instructions` for `ok` (dry run or started session, naming `impeccable live-poll`), `no_browser_connected`, `browser_timeout`, `invalid_selector`, `no_match` (wording depends on `rawMatchCount`), `ambiguous`, `index_out_of_range`, `busy`, `go_failed`, `server_stopping`; exit 0 when `ok:true`, else 1. `_instructions` are regenerated locally from the verdict, never taken from the wire.
|
||||
- **Tests**: `tests/oracle/cases/live-generate.mjs` (local verdicts, no-browser), `tests/live-agent-target.test.mjs` (protocol matrix against the binary), `crates/cli/tests/agent_target.rs`, `tests/live-e2e.test.mjs` (`agentTargetScenario`).
|
||||
|
||||
#### `live-commit-manual-edits.mjs` -> `impeccable commit-manual-edits`
|
||||
- Invoked by `/manual-edit-commit` (server) and manually (`node live-commit-manual-edits.mjs [--page-url=<url>] [--provider=auto|codex|claude|mock]`). live.md/status hint: never run it for a leased chat Apply event.
|
||||
- Env: `IMPECCABLE_LIVE_COPY_AGENT`, `IMPECCABLE_LIVE_COPY_AGENT_TIMEOUT_MS`, `IMPECCABLE_LIVE_COPY_AGENT_MODEL`, `IMPECCABLE_LIVE_COPY_AGENT_EFFORT`, `IMPECCABLE_LIVE_COPY_AGENT_MOCK_RESULT`, `IMPECCABLE_LIVE_COPY_AGENT_MOCK_WRITES`, `IMPECCABLE_LIVE_COPY_AGENT_MOCK_DELAY_MS`, `IMPECCABLE_LIVE_MANUAL_EDIT_REPAIR_ATTEMPTS`.
|
||||
|
||||
Reference in New Issue
Block a user