@bastani/atomic 0.8.20 → 0.8.21

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.
Files changed (124) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/agents/code-simplifier.md +78 -22
  5. package/dist/builtin/subagents/agents/debugger.md +4 -3
  6. package/dist/builtin/subagents/package.json +1 -1
  7. package/dist/builtin/web-access/package.json +1 -1
  8. package/dist/builtin/workflows/CHANGELOG.md +25 -0
  9. package/dist/builtin/workflows/package.json +1 -1
  10. package/dist/builtin/workflows/skills/create-spec/SKILL.md +169 -125
  11. package/dist/builtin/workflows/skills/impeccable/SKILL.md +89 -80
  12. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +92 -0
  13. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_manual_edit_applier.toml +95 -0
  14. package/dist/builtin/workflows/skills/impeccable/agents/openai.yaml +4 -0
  15. package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +122 -1
  16. package/dist/builtin/workflows/skills/impeccable/reference/animate.md +38 -12
  17. package/dist/builtin/workflows/skills/impeccable/reference/audit.md +5 -5
  18. package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +7 -7
  19. package/dist/builtin/workflows/skills/impeccable/reference/brand.md +4 -14
  20. package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +115 -1
  21. package/dist/builtin/workflows/skills/impeccable/reference/codex.md +3 -3
  22. package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +109 -6
  23. package/dist/builtin/workflows/skills/impeccable/reference/craft.md +7 -7
  24. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +623 -94
  25. package/dist/builtin/workflows/skills/impeccable/reference/delight.md +2 -2
  26. package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
  27. package/dist/builtin/workflows/skills/impeccable/reference/document.md +16 -14
  28. package/dist/builtin/workflows/skills/impeccable/reference/extract.md +1 -1
  29. package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -1
  30. package/dist/builtin/workflows/skills/impeccable/reference/init.md +172 -0
  31. package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -6
  32. package/dist/builtin/workflows/skills/impeccable/reference/layout.md +33 -13
  33. package/dist/builtin/workflows/skills/impeccable/reference/live.md +96 -19
  34. package/dist/builtin/workflows/skills/impeccable/reference/onboard.md +1 -1
  35. package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +1 -1
  36. package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -1
  37. package/dist/builtin/workflows/skills/impeccable/reference/polish.md +3 -4
  38. package/dist/builtin/workflows/skills/impeccable/reference/product.md +1 -3
  39. package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +2 -2
  40. package/dist/builtin/workflows/skills/impeccable/reference/shape.md +5 -5
  41. package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +158 -3
  42. package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +1 -1
  43. package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +2 -2
  44. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +225 -0
  45. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +266 -0
  46. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +17 -1
  47. package/dist/builtin/workflows/skills/impeccable/scripts/design-parser.mjs +16 -1
  48. package/dist/builtin/workflows/skills/impeccable/scripts/detect.mjs +21 -0
  49. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +1725 -0
  50. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +244 -0
  51. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4543 -0
  52. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
  53. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +252 -0
  54. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +535 -0
  55. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +986 -0
  56. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +208 -0
  57. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
  58. package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +12 -0
  59. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
  60. package/dist/builtin/workflows/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
  61. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +419 -0
  62. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2316 -0
  63. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
  64. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
  65. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
  66. package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +17 -1
  67. package/dist/builtin/workflows/skills/impeccable/scripts/is-generated.mjs +2 -2
  68. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +139 -96
  69. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +4491 -526
  70. package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +1241 -0
  71. package/dist/builtin/workflows/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
  72. package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
  73. package/dist/builtin/workflows/skills/impeccable/scripts/live-event-validation.mjs +136 -0
  74. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +22 -9
  75. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert-ui.mjs +458 -0
  76. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +232 -0
  77. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +363 -0
  78. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edits-buffer.mjs +152 -0
  79. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +288 -110
  80. package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +47 -1
  81. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +1443 -100
  82. package/dist/builtin/workflows/skills/impeccable/scripts/live-session-store.mjs +17 -0
  83. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +17 -3
  84. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +216 -6
  85. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +2 -3
  86. package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +633 -0
  87. package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +1 -1
  88. package/dist/builtin/workflows/src/extension/index.ts +67 -3
  89. package/dist/builtin/workflows/src/extension/render-result.ts +26 -1
  90. package/dist/builtin/workflows/src/runs/foreground/executor.ts +227 -3
  91. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +94 -7
  92. package/dist/builtin/workflows/src/shared/stage-prompt.ts +326 -0
  93. package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +62 -7
  94. package/dist/builtin/workflows/src/shared/store-types.ts +43 -0
  95. package/dist/builtin/workflows/src/shared/store.ts +37 -0
  96. package/dist/builtin/workflows/src/tui/chat-surface-message.ts +22 -4
  97. package/dist/builtin/workflows/src/tui/graph-view.ts +47 -0
  98. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +43 -1
  99. package/dist/builtin/workflows/src/tui/run-detail.ts +10 -4
  100. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +117 -15
  101. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +9 -0
  102. package/dist/core/skills.d.ts.map +1 -1
  103. package/dist/core/skills.js +2 -5
  104. package/dist/core/skills.js.map +1 -1
  105. package/dist/core/system-prompt.d.ts.map +1 -1
  106. package/dist/core/system-prompt.js +11 -29
  107. package/dist/core/system-prompt.js.map +1 -1
  108. package/dist/index.d.ts +1 -0
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +3 -0
  111. package/dist/index.js.map +1 -1
  112. package/docs/quickstart.md +1 -2
  113. package/package.json +4 -4
  114. package/dist/builtin/workflows/skills/impeccable/reference/cognitive-load.md +0 -106
  115. package/dist/builtin/workflows/skills/impeccable/reference/color-and-contrast.md +0 -105
  116. package/dist/builtin/workflows/skills/impeccable/reference/heuristics-scoring.md +0 -234
  117. package/dist/builtin/workflows/skills/impeccable/reference/motion-design.md +0 -109
  118. package/dist/builtin/workflows/skills/impeccable/reference/personas.md +0 -179
  119. package/dist/builtin/workflows/skills/impeccable/reference/responsive-design.md +0 -114
  120. package/dist/builtin/workflows/skills/impeccable/reference/spatial-design.md +0 -100
  121. package/dist/builtin/workflows/skills/impeccable/reference/teach.md +0 -156
  122. package/dist/builtin/workflows/skills/impeccable/reference/typography.md +0 -159
  123. package/dist/builtin/workflows/skills/impeccable/reference/ux-writing.md +0 -107
  124. package/dist/builtin/workflows/skills/impeccable/scripts/load-context.mjs +0 -141
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Usage:
5
5
  * npx impeccable poll # Block until browser event, print JSON
6
+ * npx impeccable poll --stream # Experimental: keep polling; one JSON line per event
6
7
  * npx impeccable poll --timeout=600000 # Custom timeout (ms); default is long-poll friendly
7
8
  * npx impeccable poll --reply <id> done # Reply "done" to event <id>
8
9
  * npx impeccable poll --reply <id> error "msg" # Reply with error
@@ -18,7 +19,9 @@ import { readLiveServerInfo } from './impeccable-paths.mjs';
18
19
  // timeout that can't be lowered per-request. We cap each request below
19
20
  // that ceiling and loop in `pollOnce` to synthesize a long poll without
20
21
  // depending on the standalone undici package.
21
- const PER_REQUEST_TIMEOUT_MS = 270_000;
22
+ export const PER_REQUEST_TIMEOUT_MS = 270_000;
23
+
24
+ const EVENT_TYPES_NEEDING_AGENT_REPLY = new Set(['generate', 'steer', 'manual_edit_apply']);
22
25
 
23
26
  function readServerInfo() {
24
27
  const record = readLiveServerInfo(process.cwd());
@@ -33,7 +36,74 @@ export function buildPollReplyPayload(token, { id, type, message, file, data })
33
36
  return { token, id, type, message, file, data };
34
37
  }
35
38
 
36
- async function postReply(base, token, reply) {
39
+ export function manualApplyPollBanner(event = {}) {
40
+ const id = event.id || 'EVENT_ID';
41
+ return [
42
+ `Manual Apply action required: edit source, then reply with \`live-poll.mjs --reply ${id} done --data '<json>'\`.`,
43
+ 'The JSON data must include status, appliedEntryIds, failed, files, and notes; summary counters are only a recovery fallback.',
44
+ 'Do not run live-commit-manual-edits.mjs for this leased event.',
45
+ 'Do not poll again before replying.',
46
+ ].join('\n') + '\n';
47
+ }
48
+
49
+ /**
50
+ * Parse `--reply <id> <status> [--file path] [--data '<json>'] [message]` argv
51
+ * into a reply object. Returns null when `--reply` is absent. Throws (code
52
+ * INVALID_REPLY_ARGS) when the reply shape is missing its event id/status and
53
+ * INVALID_DATA_JSON when `--data` is present but not valid JSON.
54
+ */
55
+ export function parseReplyArgs(args) {
56
+ const replyIdx = args.indexOf('--reply');
57
+ if (replyIdx === -1) return null;
58
+ const id = args[replyIdx + 1];
59
+ const status = args[replyIdx + 2];
60
+ validateReplyArgs({ id, status });
61
+ const fileIdx = args.indexOf('--file');
62
+ const file = fileIdx !== -1 && fileIdx + 1 < args.length ? args[fileIdx + 1] : undefined;
63
+ const dataIdx = args.indexOf('--data');
64
+ let data;
65
+ if (dataIdx !== -1 && dataIdx + 1 < args.length) {
66
+ try {
67
+ data = JSON.parse(args[dataIdx + 1]);
68
+ } catch (err) {
69
+ const wrapped = new Error('--data must be valid JSON: ' + err.message);
70
+ wrapped.code = 'INVALID_DATA_JSON';
71
+ throw wrapped;
72
+ }
73
+ }
74
+ const message = args.find((a, i) =>
75
+ i > replyIdx + 2
76
+ && !a.startsWith('--')
77
+ && i !== fileIdx + 1
78
+ && i !== dataIdx + 1
79
+ ) || undefined;
80
+ return { id, type: status, message, file, data };
81
+ }
82
+
83
+ function validateReplyArgs({ id, status }) {
84
+ const usage = "Usage: npx impeccable poll --reply <id> <status> [--file path] [--data '<json>'] [message]";
85
+ if (!id || id.startsWith('--')) {
86
+ const err = new Error(`${usage}\nMissing event id after --reply.`);
87
+ err.code = 'INVALID_REPLY_ARGS';
88
+ throw err;
89
+ }
90
+ if (['done', 'error', 'complete', 'discard', 'discarded'].includes(id)) {
91
+ const err = new Error(`${usage}\nThe value after --reply must be the event id, not the status ${JSON.stringify(id)}. Use --reply EVENT_ID ${id}.`);
92
+ err.code = 'INVALID_REPLY_ARGS';
93
+ throw err;
94
+ }
95
+ if (!status || status.startsWith('--')) {
96
+ const err = new Error(`${usage}\nMissing reply status after event id ${JSON.stringify(id)}.`);
97
+ err.code = 'INVALID_REPLY_ARGS';
98
+ throw err;
99
+ }
100
+ }
101
+
102
+ export function requiresAgentReply(event) {
103
+ return EVENT_TYPES_NEEDING_AGENT_REPLY.has(event?.type);
104
+ }
105
+
106
+ export async function postReply(base, token, reply) {
37
107
  const res = await fetch(`${base}/poll`, {
38
108
  method: 'POST',
39
109
  headers: { 'Content-Type': 'application/json' },
@@ -41,10 +111,192 @@ async function postReply(base, token, reply) {
41
111
  });
42
112
  if (!res.ok) {
43
113
  const body = await res.json().catch(() => ({}));
44
- throw new Error(body.error || res.statusText);
114
+ const parts = [body.error || res.statusText, body.reason, body.hint].filter(Boolean);
115
+ throw new Error(parts.join(': '));
45
116
  }
46
117
  }
47
118
 
119
+ export async function fetchServerStatus(base, token) {
120
+ const res = await fetch(`${base}/status?token=${token}`);
121
+ if (res.status === 401) {
122
+ const err = new Error('Authentication failed. The server token may have changed.');
123
+ err.code = 'AUTH_FAILED';
124
+ throw err;
125
+ }
126
+ if (!res.ok) {
127
+ throw new Error(`Status failed: ${res.status} ${res.statusText}`);
128
+ }
129
+ return res.json();
130
+ }
131
+
132
+ export function isEventPending(status, eventId) {
133
+ return (status.pendingEvents || []).some((entry) => entry.id === eventId);
134
+ }
135
+
136
+ export async function waitForEventAck(base, token, eventId, {
137
+ pollIntervalMs = 400,
138
+ maxWaitMs = 600_000,
139
+ } = {}) {
140
+ const deadline = Date.now() + maxWaitMs;
141
+ while (Date.now() < deadline) {
142
+ const status = await fetchServerStatus(base, token);
143
+ if (!isEventPending(status, eventId)) return true;
144
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
145
+ }
146
+ return false;
147
+ }
148
+
149
+ export async function fetchNextEvent(base, token, { totalDeadline } = {}) {
150
+ while (true) {
151
+ if (totalDeadline && Date.now() >= totalDeadline) {
152
+ return { type: 'timeout' };
153
+ }
154
+
155
+ const remaining = totalDeadline
156
+ ? totalDeadline - Date.now()
157
+ : PER_REQUEST_TIMEOUT_MS;
158
+ const slice = Math.min(Math.max(remaining, 1000), PER_REQUEST_TIMEOUT_MS);
159
+ const res = await fetch(`${base}/poll?token=${token}&timeout=${slice}`);
160
+
161
+ if (res.status === 401) {
162
+ const err = new Error('Authentication failed. The server token may have changed.');
163
+ err.code = 'AUTH_FAILED';
164
+ throw err;
165
+ }
166
+
167
+ if (!res.ok) {
168
+ throw new Error(`Poll failed: ${res.status} ${res.statusText}`);
169
+ }
170
+
171
+ const next = await res.json();
172
+ if (next?.type === 'timeout') {
173
+ if (totalDeadline && Date.now() < totalDeadline) continue;
174
+ if (!totalDeadline) continue;
175
+ return next;
176
+ }
177
+ return next;
178
+ }
179
+ }
180
+
181
+ export async function augmentEventWithAcceptHandling(event, base, token) {
182
+ if (event.type !== 'accept' && event.type !== 'discard') return event;
183
+
184
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
185
+ const acceptScript = path.join(__dirname, 'live-accept.mjs');
186
+ const scriptArgs = buildAcceptScriptArgs(event);
187
+
188
+ try {
189
+ const out = execFileSync(
190
+ 'node',
191
+ [acceptScript, ...scriptArgs],
192
+ { encoding: 'utf-8', cwd: process.cwd(), timeout: 30_000 },
193
+ );
194
+ event._acceptResult = JSON.parse(out.trim());
195
+ } catch (err) {
196
+ event._acceptResult = { handled: false, mode: 'error', error: err.message };
197
+ }
198
+
199
+ const completionType = completionTypeForAcceptResult(event.type, event._acceptResult);
200
+ try {
201
+ await postReply(base, token, {
202
+ id: event.id,
203
+ type: completionType,
204
+ message: event._acceptResult?.error,
205
+ file: event._acceptResult?.file,
206
+ data: event._acceptResult?.carbonize === true ? { carbonize: true } : undefined,
207
+ });
208
+ } catch (err) {
209
+ event._completionAck = { ok: false, error: err.message };
210
+ }
211
+ if (!event._completionAck) {
212
+ event._completionAck = completionAckForAcceptResult(event.id, completionType, event._acceptResult);
213
+ }
214
+
215
+ return event;
216
+ }
217
+
218
+ export function buildAcceptScriptArgs(event) {
219
+ const scriptArgs = event.type === 'discard'
220
+ ? ['--id', String(event.id), '--discard']
221
+ : ['--id', String(event.id), '--variant', String(event.variantId)];
222
+ if (event.pageUrl) scriptArgs.push('--page-url', String(event.pageUrl));
223
+ if (event.type === 'accept' && event.paramValues && Object.keys(event.paramValues).length > 0) {
224
+ scriptArgs.push('--param-values', JSON.stringify(event.paramValues));
225
+ }
226
+ return scriptArgs;
227
+ }
228
+
229
+ export function writeCarbonizeBanner(event) {
230
+ if (event.type === 'manual_edit_apply') {
231
+ process.stderr.write('\n' + manualApplyPollBanner(event) + '\n');
232
+ }
233
+ if (event._acceptResult?.carbonize === true) {
234
+ process.stderr.write('\n⚠ Carbonize cleanup REQUIRED before next poll. After cleanup, run live-complete.mjs --id ' + event.id + '. See reference/live.md "Required after accept".\n\n');
235
+ }
236
+ }
237
+
238
+ export function printPollEvent(event) {
239
+ console.log(JSON.stringify(event));
240
+ }
241
+
242
+ export async function runPollOnce(base, token, { totalTimeout = 600_000 } = {}) {
243
+ const deadline = Date.now() + totalTimeout;
244
+ const event = await fetchNextEvent(base, token, { totalDeadline: deadline });
245
+ await augmentEventWithAcceptHandling(event, base, token);
246
+ writeCarbonizeBanner(event);
247
+ printPollEvent(event);
248
+ return event;
249
+ }
250
+
251
+ export async function runPollStream(base, token, {
252
+ ackTimeoutMs = 600_000,
253
+ ackPollIntervalMs = 400,
254
+ shouldContinue = () => true,
255
+ } = {}) {
256
+ process.stderr.write('[impeccable-poll] stream mode: one JSON object per line on stdout; use --reply while this process stays running\n');
257
+
258
+ while (shouldContinue()) {
259
+ const event = await fetchNextEvent(base, token);
260
+ await augmentEventWithAcceptHandling(event, base, token);
261
+ writeCarbonizeBanner(event);
262
+ printPollEvent(event);
263
+
264
+ if (event.type === 'exit') return event;
265
+
266
+ if (requiresAgentReply(event)) {
267
+ const acked = await waitForEventAck(base, token, event.id, {
268
+ pollIntervalMs: ackPollIntervalMs,
269
+ maxWaitMs: ackTimeoutMs,
270
+ });
271
+ if (!acked) {
272
+ const err = new Error(`Timed out waiting for --reply on event ${event.id}`);
273
+ err.code = 'ACK_TIMEOUT';
274
+ throw err;
275
+ }
276
+ }
277
+ }
278
+
279
+ return null;
280
+ }
281
+
282
+ function handlePollError(err) {
283
+ if (err.code === 'AUTH_FAILED') {
284
+ console.error(err.message);
285
+ console.error('Try restarting: npx impeccable live stop && npx impeccable live');
286
+ process.exit(1);
287
+ }
288
+ if (err.cause?.code === 'ECONNREFUSED') {
289
+ console.error('Live server not running. Start one with: npx impeccable live');
290
+ process.exit(1);
291
+ }
292
+ if (err.code === 'ACK_TIMEOUT') {
293
+ console.error(err.message);
294
+ process.exit(1);
295
+ }
296
+ console.error('Poll failed:', err.message);
297
+ process.exit(1);
298
+ }
299
+
48
300
  export async function pollCli() {
49
301
  const args = process.argv.slice(2);
50
302
 
@@ -54,38 +306,42 @@ export async function pollCli() {
54
306
  Wait for a browser event from the live variant server, or reply to one.
55
307
 
56
308
  Modes:
57
- poll Block until a browser event arrives, print JSON
58
- poll --reply <id> done Reply "done" to event <id>
309
+ poll Block until a browser event arrives, print JSON, exit
310
+ poll --stream Keep polling; print one JSON line per event (see live.md)
311
+ poll --reply <id> done Reply "done" to event <id> (replace or insert generate)
312
+ poll --reply <id> steer_done Reply after handling a steer event (unlocks Steer bar)
59
313
  poll --reply <id> error "msg" Reply with an error message
314
+ poll --reply <id> done --data '<json>'
315
+ Reply with a structured JSON result (manual_edit_apply)
60
316
 
61
317
  Options:
62
- --timeout=MS Long-poll timeout in ms (default: 600000). Use the default unless the user asked to pause live; never use a short timeout to end the chat turn
63
- --help Show this help message`);
318
+ --timeout=MS One-shot poll timeout in ms (default: 600000). Ignored in --stream mode
319
+ --ack-timeout=MS Stream mode: max wait for --reply after generate/steer (default: 600000)
320
+ --file PATH Attach a source file path to the reply (generate flow)
321
+ --data JSON Attach a JSON result object to the reply (manual_edit_apply flow). Must be valid JSON
322
+ --help Show this help message
323
+
324
+ Harness note:
325
+ Default one-shot mode is the portable contract for Claude Code, Codex, and Cursor.
326
+ --stream is experimental for harnesses with fast incremental stdout; do not use on Cursor.`);
64
327
  process.exit(0);
65
328
  }
66
329
 
67
330
  const info = readServerInfo();
68
331
  const base = `http://localhost:${info.port}`;
69
332
 
70
- // Reply mode: npx impeccable poll --reply <id> <status> [--file path] [message]
71
- const replyIdx = args.indexOf('--reply');
72
- if (replyIdx !== -1) {
73
- const id = args[replyIdx + 1];
74
- const status = args[replyIdx + 2] || 'done';
75
- const fileIdx = args.indexOf('--file');
76
- const filePath = fileIdx !== -1 && fileIdx + 1 < args.length ? args[fileIdx + 1] : undefined;
77
- // Message is any remaining positional arg that isn't a flag
78
- const message = args.find((a, i) => i > replyIdx + 2 && !a.startsWith('--') && i !== fileIdx + 1) || undefined;
79
-
80
- if (!id) {
81
- console.error('Usage: npx impeccable poll --reply <id> <status> [--file path] [message]');
333
+ // Reply mode: npx impeccable poll --reply <id> <status> [--file path] [--data '<json>'] [message]
334
+ if (args.includes('--reply')) {
335
+ let reply;
336
+ try {
337
+ reply = parseReplyArgs(args);
338
+ } catch (err) {
339
+ console.error(err.message);
82
340
  process.exit(1);
83
341
  }
84
342
 
85
343
  try {
86
- await postReply(base, info.token, { id, type: status, message, file: filePath });
87
-
88
- // Success — silent exit (agent doesn't need output for replies)
344
+ await postReply(base, info.token, reply);
89
345
  } catch (err) {
90
346
  if (err.cause?.code === 'ECONNREFUSED') {
91
347
  console.error('Live server not running. Start one with: npx impeccable live');
@@ -97,99 +353,21 @@ Options:
97
353
  return;
98
354
  }
99
355
 
100
- // Poll mode: block until browser event. Default 10 min. Node's built-in
101
- // fetch enforces a 300s headers timeout, so we loop in slices under that
102
- // ceiling and keep re-polling until we get a real event or the user's
103
- // total timeout runs out.
104
- const timeoutArg = args.find(a => a.startsWith('--timeout='));
105
- const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000;
356
+ const streamMode = args.includes('--stream');
357
+ const ackTimeoutArg = args.find((a) => a.startsWith('--ack-timeout='));
358
+ const ackTimeoutMs = ackTimeoutArg ? parseInt(ackTimeoutArg.split('=')[1], 10) : 600_000;
106
359
 
107
- const deadline = Date.now() + totalTimeout;
108
- let event;
109
360
  try {
110
- while (true) {
111
- const remaining = deadline - Date.now();
112
- if (remaining <= 0) {
113
- event = { type: 'timeout' };
114
- break;
115
- }
116
- const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS);
117
- const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`);
118
-
119
- if (res.status === 401) {
120
- console.error('Authentication failed. The server token may have changed.');
121
- console.error('Try restarting: npx impeccable live stop && npx impeccable live');
122
- process.exit(1);
123
- }
124
-
125
- if (!res.ok) {
126
- console.error(`Poll failed: ${res.status} ${res.statusText}`);
127
- process.exit(1);
128
- }
129
-
130
- const next = await res.json();
131
- // Server-side timeout means no browser event arrived in this slice.
132
- // Loop and re-poll until we get a real event or we hit the user's
133
- // total deadline.
134
- if (next?.type === 'timeout' && Date.now() < deadline) continue;
135
- event = next;
136
- break;
361
+ if (streamMode) {
362
+ await runPollStream(base, info.token, { ackTimeoutMs });
363
+ return;
137
364
  }
138
365
 
139
- // Auto-handle accept/discard via deterministic script
140
- if (event.type === 'accept' || event.type === 'discard') {
141
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
142
- const acceptScript = path.join(__dirname, 'live-accept.mjs');
143
- const scriptArgs = event.type === 'discard'
144
- ? ['--id', event.id, '--discard']
145
- : ['--id', event.id, '--variant', event.variantId];
146
- if (event.type === 'accept' && event.paramValues && Object.keys(event.paramValues).length > 0) {
147
- scriptArgs.push('--param-values', JSON.stringify(event.paramValues));
148
- }
149
- try {
150
- const out = execFileSync(
151
- 'node',
152
- [acceptScript, ...scriptArgs],
153
- { encoding: 'utf-8', cwd: process.cwd(), timeout: 30_000 }
154
- );
155
- event._acceptResult = JSON.parse(out.trim());
156
- } catch (err) {
157
- event._acceptResult = { handled: false, mode: 'error', error: err.message };
158
- }
159
-
160
- const completionType = completionTypeForAcceptResult(event.type, event._acceptResult);
161
- try {
162
- await postReply(base, info.token, {
163
- id: event.id,
164
- type: completionType,
165
- message: event._acceptResult?.error,
166
- file: event._acceptResult?.file,
167
- data: event._acceptResult?.carbonize === true ? { carbonize: true } : undefined,
168
- });
169
- } catch (err) {
170
- event._completionAck = { ok: false, error: err.message };
171
- }
172
- if (!event._completionAck) {
173
- event._completionAck = completionAckForAcceptResult(event.id, completionType, event._acceptResult);
174
- }
175
- }
176
-
177
- // Second signal path: stderr banner in case the agent parses stdout
178
- // JSON but skips nested fields. One line is enough — the full checklist
179
- // is in reference/live.md.
180
- if (event._acceptResult?.carbonize === true) {
181
- process.stderr.write('\n⚠ Carbonize cleanup REQUIRED before next poll. After cleanup, run live-complete.mjs --id ' + event.id + '. See reference/live.md "Required after accept".\n\n');
182
- }
183
-
184
- // Print the event as JSON — the agent reads this from stdout
185
- console.log(JSON.stringify(event));
366
+ const timeoutArg = args.find((a) => a.startsWith('--timeout='));
367
+ const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600_000;
368
+ await runPollOnce(base, info.token, { totalTimeout });
186
369
  } catch (err) {
187
- if (err.cause?.code === 'ECONNREFUSED') {
188
- console.error('Live server not running. Start one with: npx impeccable live');
189
- } else {
190
- console.error('Poll failed:', err.message);
191
- }
192
- process.exit(1);
370
+ handlePollError(err);
193
371
  }
194
372
  }
195
373
 
@@ -5,6 +5,50 @@
5
5
 
6
6
  import { createLiveSessionStore } from './live-session-store.mjs';
7
7
 
8
+ function manualApplyReplyCommand(eventOrId = 'EVENT_ID') {
9
+ const id = typeof eventOrId === 'string' ? eventOrId : eventOrId?.id || 'EVENT_ID';
10
+ return `live-poll.mjs --reply ${id} done --data '<json>'`;
11
+ }
12
+
13
+ export function manualApplyResumeHint(event = {}) {
14
+ const summary = event.manualApplySummary || summarizeManualApplyEvent(event);
15
+ const parts = [];
16
+ if (summary.pageUrl) parts.push(`page ${summary.pageUrl}`);
17
+ if (summary.chunk) parts.push(`chunk ${summary.chunk.index}/${summary.chunk.total}`);
18
+ if (Number.isFinite(summary.opCount)) parts.push(`${summary.opCount} op(s)`);
19
+ if (Number.isFinite(summary.entryCount)) parts.push(`${summary.entryCount} entr${summary.entryCount === 1 ? 'y' : 'ies'}`);
20
+ if (summary.files?.length) parts.push(`likely files: ${summary.files.join(', ')}`);
21
+ const scope = parts.length ? ` (${parts.join(', ')})` : '';
22
+ return `Manual Apply pending${scope}. If you have not already leased it, run live-poll.mjs. Apply the source edits from the manual_edit_apply batch, then reply with ${manualApplyReplyCommand(event.id)}. Polling only leases this work item; it does not commit source edits. Do not run live-commit-manual-edits.mjs for this leased event. Do not poll again before replying.`;
23
+ }
24
+
25
+ function summarizeManualApplyEvent(event = {}) {
26
+ const entries = Array.isArray(event.batch?.entries) ? event.batch.entries : [];
27
+ const opCount = entries.reduce((sum, entry) => sum + (Array.isArray(entry.ops) ? entry.ops.length : 0), 0);
28
+ return {
29
+ pageUrl: event.pageUrl || null,
30
+ chunk: event.chunk || null,
31
+ entryCount: entries.length,
32
+ opCount,
33
+ files: collectManualApplyFiles(event.batch),
34
+ };
35
+ }
36
+
37
+ function collectManualApplyFiles(batch) {
38
+ const files = [];
39
+ for (const entry of batch?.entries || []) {
40
+ for (const op of entry.ops || []) files.push(op.sourceHint?.file);
41
+ }
42
+ for (const candidate of batch?.candidates || []) {
43
+ files.push(candidate.sourceHint?.relativeFile, candidate.sourceHint?.file);
44
+ for (const item of candidate.textMatches || []) files.push(item.file);
45
+ for (const item of candidate.objectKeyMatches || []) files.push(item.file);
46
+ for (const item of candidate.locatorMatches || []) files.push(item.file);
47
+ for (const item of candidate.contextTextMatches || []) files.push(item.file);
48
+ }
49
+ return [...new Set(files.filter((file) => typeof file === 'string' && file.length > 0))].sort();
50
+ }
51
+
8
52
  function parseArgs(argv) {
9
53
  const out = { id: null };
10
54
  for (let i = 0; i < argv.length; i++) {
@@ -32,7 +76,9 @@ export async function resumeCli() {
32
76
 
33
77
  const pending = snapshot.pendingEvent || null;
34
78
  const nextAction = pending
35
- ? `Run live-poll.mjs, handle ${pending.type} ${pending.id}, then acknowledge with live-poll.mjs --reply ${pending.id} done.`
79
+ ? pending.type === 'manual_edit_apply'
80
+ ? manualApplyResumeHint(pending)
81
+ : `Run live-poll.mjs, handle ${pending.type} ${pending.id}, then acknowledge with live-poll.mjs --reply ${pending.id} done.`
36
82
  : snapshot.phase === 'carbonize_required'
37
83
  ? `Finish carbonize cleanup${snapshot.sourceFile ? ` in ${snapshot.sourceFile}` : ''}, then run live-complete.mjs --id ${snapshot.id}.`
38
84
  : snapshot.phase === 'accept_requested'