@enter-pro/enter-cli 0.4.3 → 0.4.4
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/README.md +103 -21
- package/dist/commands/project.js +1 -1
- package/dist/commands/thread.d.ts +10 -1
- package/dist/commands/thread.js +194 -46
- package/dist/thread-events.js +4 -1
- package/dist/workflow.d.ts +2 -0
- package/dist/workflow.js +4 -2
- package/package.json +1 -1
- package/skills/enter/SKILL.md +10 -6
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ unset the variable in your shell.
|
|
|
31
31
|
enter-cli --output json thread chat PROJECT_ID --file requirement.txt
|
|
32
32
|
enter-cli --output json thread wait PROJECT_ID --task-id TASK_ID --timeout 10
|
|
33
33
|
enter-cli --output json thread status PROJECT_ID --task-id TASK_ID
|
|
34
|
-
enter-cli thread
|
|
34
|
+
enter-cli thread wait PROJECT_ID --task-id TASK_ID --stream --timeout 0
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
`chat --stdin` also accepts multiline input. Submission returns immediately with
|
|
@@ -41,16 +41,16 @@ can be submitted while Enter works; acceptance is not completion.
|
|
|
41
41
|
| Command | Output and default lifetime |
|
|
42
42
|
| --- | --- |
|
|
43
43
|
| `status` | One JSON/YAML snapshot, 30-second deadline |
|
|
44
|
-
| `wait` | One
|
|
45
|
-
| `
|
|
44
|
+
| `wait --stream` | One continuous observer with batched NDJSON; unlimited by default, explicit --timeout bounds lifetime |
|
|
45
|
+
| `wait` | One result on new narrative progress, a card, terminal state or timeout; 10 seconds |
|
|
46
|
+
| `watch` | Changed snapshots as NDJSON, then `type: result`; 60 seconds; unlimited only with explicit `--timeout 0` |
|
|
46
47
|
|
|
47
|
-
Use one observer per task.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
scheduling: CLI events cannot make a blocked host process new user input.
|
|
48
|
+
Use one observer per task. Hosts that buffer shell output use bounded `wait`;
|
|
49
|
+
Helix uses `--timeout 10` to return control regularly. On timeout, first handle
|
|
50
|
+
incoming user messages, then report the actual state and continue the returned command for the same task,
|
|
51
|
+
without sleep, resubmission or repeated permission questions. Hosts that consume
|
|
52
|
+
stdout incrementally should use `wait --stream`; unchanged successful observations emit a
|
|
53
|
+
heartbeat every max(30 seconds, progress interval). Hosts own scheduling and interruption.
|
|
54
54
|
|
|
55
55
|
`--task-id` follows exactly the submitted task, including queue time. `--turn N`
|
|
56
56
|
selects a fixed turn instead; without either, observation pins the first turn.
|
|
@@ -70,8 +70,8 @@ messages and the resulting change, and do not substitute the newest turn.
|
|
|
70
70
|
|
|
71
71
|
`project create --wait` uses the same observer and requires a matching successful
|
|
72
72
|
build. For existing tasks use `--require-build`; stale builds cannot satisfy it.
|
|
73
|
-
`build_matches_turn` checks commit identity. `status/wait`
|
|
74
|
-
`--
|
|
73
|
+
`build_matches_turn` checks commit identity. `status/wait/watch` return monitoring fields by default, excluding usage and unrelated metadata.
|
|
74
|
+
Use `--full` for detailed sanitized metadata; `--compact` remains an explicit alias for the default.
|
|
75
75
|
The additive `workflow` projection supplies `state`, `task`, `next_action`,
|
|
76
76
|
`observation`, build matching and action descriptors; full card details remain in
|
|
77
77
|
`actions`. `use_existing_authorization` does not grant new permission.
|
|
@@ -90,14 +90,12 @@ separately; do not replace Enter's validation with source keyword scans.
|
|
|
90
90
|
|
|
91
91
|
## Transport and errors
|
|
92
92
|
|
|
93
|
-
Observation
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
unsupported endpoints fall back without repeated reconnects. WebSockets respect
|
|
100
|
-
proxy variables and `NO_PROXY`; `NODE_USE_ENV_PROXY=0` disables their proxy use.
|
|
93
|
+
Observation defaults to HTTP polling every 2 seconds after each read. Each
|
|
94
|
+
snapshot query is bounded to 30 seconds. No WebSocket is opened by default.
|
|
95
|
+
`--transport auto` opts in to WebSocket events with HTTP fallback; 401/403 on the
|
|
96
|
+
stream stops reconnection but leaves state observation on HTTP. Raw message
|
|
97
|
+
follow still fails with STREAM_AUTH_ERROR. HTTP authentication failures stop
|
|
98
|
+
observation. WebSockets respect proxy variables and NO_PROXY.
|
|
101
99
|
|
|
102
100
|
`thread messages PROJECT_ID --follow --timeout 60 --cursor EVENT_ID` emits raw
|
|
103
101
|
NDJSON events with diagnostics on stderr. It supports `--max-events`, crosses turn
|
|
@@ -142,7 +140,7 @@ npm ci
|
|
|
142
140
|
npm test
|
|
143
141
|
npm run build
|
|
144
142
|
npm pack
|
|
145
|
-
npm run install:hosts -- --package /absolute/path/enter-pro-enter-cli-0.4.
|
|
143
|
+
npm run install:hosts -- --package /absolute/path/enter-pro-enter-cli-0.4.4.tgz
|
|
146
144
|
```
|
|
147
145
|
|
|
148
146
|
Tests use loopback HTTP/WebSocket fixtures and dummy credentials, not real cloud
|
|
@@ -165,3 +163,87 @@ Scenarios: `plan`, `cloud`, `ai`, `secret`, `questions`, `subscription`, `runnin
|
|
|
165
163
|
unsupported routes fail, and request bodies/secrets are not retained. Only use
|
|
166
164
|
fake values. `.env.integration.example` lists real-test variables; its ignored
|
|
167
165
|
`.env.integration.local` counterpart is never loaded automatically.
|
|
166
|
+
|
|
167
|
+
### Agent host integration
|
|
168
|
+
|
|
169
|
+
All observers return `progress` from the selected task's HTTP message history:
|
|
170
|
+
`source: assistant_messages`, `available`, a scoped `revision`, and the latest
|
|
171
|
+
assistant text in `messages` (up to 1600 characters, marked `truncated` if clipped).
|
|
172
|
+
Reasoning and raw tool arguments are excluded. `observed_at` timestamps the last
|
|
173
|
+
successful state read; `emitted_at` timestamps watch output, including stale results.
|
|
174
|
+
A narrative read failure is separate from authoritative task/action status.
|
|
175
|
+
|
|
176
|
+
`wait` batches ordinary narrative for 10 seconds by default, keeping the latest text,
|
|
177
|
+
and returns early when that interval has elapsed and progress changed.
|
|
178
|
+
`--progress-interval <seconds>` controls batching; `0` opts into every change.
|
|
179
|
+
Actions, terminal states and errors bypass batching. The overall `--timeout` still
|
|
180
|
+
bounds the call and returns the latest observation even before the interval elapses.
|
|
181
|
+
`status` remains an immediate read; no phase is guessed from narrative keywords. `workflow.next_action=report_progress` means relay the text first, then `after_reporting=observe`. Exit 0 means an observation result, not build completion. Execute the returned `wait_command`, including
|
|
182
|
+
`--after-progress`, to wait for the next revision without replaying the same update.
|
|
183
|
+
`watch` uses the same interval for ordinary narrative/metadata updates; state changes
|
|
184
|
+
and final results are emitted immediately.
|
|
185
|
+
These are latest-progress snapshots, not a lossless replay of every message.
|
|
186
|
+
|
|
187
|
+
The host agent relays the supplied text, forwards questions, and executes the next
|
|
188
|
+
command; it need not reason over logs or infer development phases. Relay new progress
|
|
189
|
+
proactively, suppress repeated revisions/heartbeats, and answer user questions from
|
|
190
|
+
the last observation before refreshing. Background hosts with only completion notifications fall back to bounded `wait`: batched progress ends the job and triggers that notification. Merely being able to read running stdout is insufficient for automatic `watch` delivery. For background jobs, read output nonblockingly
|
|
191
|
+
or with a short bounded wait; a five-minute blocking job-output call defeats background
|
|
192
|
+
execution. The CLI cannot schedule the host agent or interrupt its tool calls.
|
|
193
|
+
|
|
194
|
+
Both `wait` and `watch` use HTTP by default. The CLI performs the polling;
|
|
195
|
+
do not generate `sleep && enter-cli` or a loop of `thread status` commands.
|
|
196
|
+
|
|
197
|
+
For hosts that return shell output only after the command exits, use `wait`.
|
|
198
|
+
For the current Helix integration, use a short 10-second observation window
|
|
199
|
+
to return control for incoming messages:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
enter-cli --output json thread wait PROJECT_ID --task-id TASK_ID --timeout 10
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The CLI and Helix guidance both default to 10 seconds. A shell deadline alone
|
|
206
|
+
does not guarantee responsiveness. On exit 2, handle incoming messages, report
|
|
207
|
+
the returned Enter stage/status and execute the returned `wait_command` to keep observing.
|
|
208
|
+
It preserves the task or pinned turn, chat, build requirement and observation
|
|
209
|
+
timeout. Do not ask the user whether to continue, recreate the project or
|
|
210
|
+
resubmit the build. A timeout ends observation, not the remote task. On
|
|
211
|
+
`blocked`, forward the action and follow the continuation after answering;
|
|
212
|
+
on `completed` or `failed`, consume the result.
|
|
213
|
+
|
|
214
|
+
For hosts that notify the agent when incremental stdout arrives without blocking user interaction,
|
|
215
|
+
use `watch_command` (60 seconds by default). Only explicitly select `--timeout 0`
|
|
216
|
+
when the host can manage an unlimited background observer.
|
|
217
|
+
`snapshot` contains actual Enter task/turn state, `heartbeat` confirms a
|
|
218
|
+
successful observation without a state change, and `result` ends observation.
|
|
219
|
+
This is stage/status progress, not generated percentages or a raw model-token
|
|
220
|
+
log. Each snapshot query has a 30-second deadline. SIGINT/SIGTERM stop only
|
|
221
|
+
the observer (130/143); the host owns background execution and reconnection.
|
|
222
|
+
|
|
223
|
+
The current Helix sandbox adapter buffers stdout until command completion;
|
|
224
|
+
therefore an indefinitely running `watch` is not its default integration path.
|
|
225
|
+
Helix colleagues only need to update the actual sandbox CLI and the agent's
|
|
226
|
+
usage instructions for the bounded `wait` path, then verify it in a fresh
|
|
227
|
+
sandbox. Continuous in-flight display requires host-side incremental output
|
|
228
|
+
support; local CLI tests do not prove that integration.
|
|
229
|
+
|
|
230
|
+
State and pending-action reads run concurrently to avoid spending the observation
|
|
231
|
+
budget on sequential independent requests. Failed turns do not wait for pending
|
|
232
|
+
action reads. `QUERY_TIMEOUT` before the first snapshot means the observation
|
|
233
|
+
deadline expired without a known task state; it does not locate the cause in the
|
|
234
|
+
network or server. The overall deadline remains bounded. Usage changes are not
|
|
235
|
+
progress; continue quietly when there is no new narrative or actionable state.
|
|
236
|
+
|
|
237
|
+
### One background observer
|
|
238
|
+
|
|
239
|
+
Use `enter-cli thread wait PROJECT_ID --task-id TASK_ID --stream --timeout 0`
|
|
240
|
+
with a host that supports managed background execution and incremental output delivery.
|
|
241
|
+
The CLI polls HTTP internally and emits batched progress until an action, terminal state,
|
|
242
|
+
interruption or explicit deadline. Ordinary progress does not end this process.
|
|
243
|
+
Consume events without blocking user interaction; do not restart it on report_progress.
|
|
244
|
+
The returned `stream_argv` / `stream_command` starts this observer.
|
|
245
|
+
`watch` remains a compatible streaming entry point.
|
|
246
|
+
|
|
247
|
+
Without `--stream`, wait retains its bounded single-JSON contract for buffered hosts.
|
|
248
|
+
This fallback can require repeated calls; CLI cannot enable host background scheduling.
|
|
249
|
+
Do not detach with shell `&` or assume buffered Bash output is delivered incrementally.
|
package/dist/commands/project.js
CHANGED
|
@@ -91,7 +91,7 @@ projectCmd
|
|
|
91
91
|
.option("--prompt <text>", "Initial prompt for the project")
|
|
92
92
|
.option("--model <id>", "Pin AI model (see `enter-cli models list`). Invalid IDs silently fall back to 'auto'.")
|
|
93
93
|
.option("--plan-mode", "Start the project in Plan Mode (first turn produces a plan instead of code)")
|
|
94
|
-
.option("--wait", "
|
|
94
|
+
.option("--wait", "Observe the first build; return on progress, input, completion or timeout")
|
|
95
95
|
.option("--timeout <seconds>", "Timeout for --wait in seconds", "300")
|
|
96
96
|
.action(async (id, opts, cmd) => {
|
|
97
97
|
const seconds = Number(opts.timeout);
|
|
@@ -5,8 +5,11 @@ type InteractionOptions = {
|
|
|
5
5
|
chatId?: string;
|
|
6
6
|
turn?: number;
|
|
7
7
|
requireBuild?: boolean;
|
|
8
|
+
timeout?: string;
|
|
9
|
+
afterProgress?: string;
|
|
10
|
+
progressInterval?: string;
|
|
8
11
|
};
|
|
9
|
-
export declare function threadInteraction(id: string, { taskId, chatId, turn, requireBuild }?: InteractionOptions): {
|
|
12
|
+
export declare function threadInteraction(id: string, { taskId, chatId, turn, requireBuild, timeout, afterProgress, progressInterval }?: InteractionOptions): {
|
|
10
13
|
workflow: {
|
|
11
14
|
actions?: {
|
|
12
15
|
id: unknown;
|
|
@@ -26,6 +29,7 @@ export declare function threadInteraction(id: string, { taskId, chatId, turn, re
|
|
|
26
29
|
interrupted: boolean;
|
|
27
30
|
};
|
|
28
31
|
reason?: string | undefined;
|
|
32
|
+
after_reporting?: string | undefined;
|
|
29
33
|
state: string;
|
|
30
34
|
task: {
|
|
31
35
|
id: {} | null;
|
|
@@ -36,6 +40,9 @@ export declare function threadInteraction(id: string, { taskId, chatId, turn, re
|
|
|
36
40
|
};
|
|
37
41
|
monitoring_required: boolean;
|
|
38
42
|
wait_command: string;
|
|
43
|
+
wait_argv: string[];
|
|
44
|
+
stream_command: string;
|
|
45
|
+
stream_argv: string[];
|
|
39
46
|
watch_command: string;
|
|
40
47
|
status_command: string;
|
|
41
48
|
follow_up_command: string;
|
|
@@ -47,6 +54,8 @@ type MonitorOptions = {
|
|
|
47
54
|
chatId?: string;
|
|
48
55
|
cursor?: string;
|
|
49
56
|
timeout: string;
|
|
57
|
+
afterProgress?: string;
|
|
58
|
+
progressInterval?: string;
|
|
50
59
|
transport?: string;
|
|
51
60
|
full?: boolean;
|
|
52
61
|
compact?: boolean;
|
package/dist/commands/thread.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
4
|
import { writeFileSync, readFileSync } from "fs";
|
|
@@ -319,13 +320,7 @@ threadCmd
|
|
|
319
320
|
params.end_turn = endTurn;
|
|
320
321
|
const data = await client.get(`/v1/projects/${id}/thread/messages`, params);
|
|
321
322
|
if (opts.text) {
|
|
322
|
-
|
|
323
|
-
const messages = rows.flatMap((row) => {
|
|
324
|
-
const content = row?.detail?.assistant_content?.content;
|
|
325
|
-
return row?.message_type === "assistant_content" && typeof content === "string"
|
|
326
|
-
? [{ turn: row.turn, text: content }] : [];
|
|
327
|
-
});
|
|
328
|
-
print(getFormat(cmd), safeOutput({ messages }));
|
|
323
|
+
print(getFormat(cmd), safeOutput({ messages: assistantMessages(data) }));
|
|
329
324
|
}
|
|
330
325
|
else
|
|
331
326
|
print(getFormat(cmd), data);
|
|
@@ -397,17 +392,64 @@ threadCmd
|
|
|
397
392
|
]);
|
|
398
393
|
printTable(["Turn", "Name", "Status", "Model", "Credits", "Created"], rows);
|
|
399
394
|
});
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
395
|
+
function assistantMessages(data) {
|
|
396
|
+
const rows = Array.isArray(data) ? data : data?.messages ?? [];
|
|
397
|
+
return rows.flatMap((row) => {
|
|
398
|
+
const content = row?.detail?.assistant_content?.content;
|
|
399
|
+
return row?.message_type === "assistant_content" && typeof content === "string" && content.trim()
|
|
400
|
+
? [{ turn: row.turn, text: content }] : [];
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
async function readProgress(id, turn, signal, timeoutMs = 30000) {
|
|
404
|
+
const controller = new AbortController();
|
|
405
|
+
const abort = () => controller.abort(signal.reason);
|
|
406
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
407
|
+
if (signal.aborted)
|
|
408
|
+
abort();
|
|
409
|
+
// Optional narrative must not hold up action handling or status indefinitely.
|
|
410
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
411
|
+
try {
|
|
412
|
+
const data = await client.get(`/v1/projects/${id}/thread/messages`, {
|
|
413
|
+
start_turn: String(turn.turn), end_turn: String(turn.turn),
|
|
414
|
+
...(typeof turn.chat_id === "string" ? { chat_id: turn.chat_id } : {}),
|
|
415
|
+
}, controller.signal);
|
|
416
|
+
const messages = assistantMessages(data).filter(m => m.turn === undefined || String(m.turn) === String(turn.turn)).slice(-1);
|
|
417
|
+
const revision = createHash("sha256").update(JSON.stringify([id, turn.id, turn.chat_id, turn.turn, messages])).digest("hex");
|
|
418
|
+
return { source: "assistant_messages", available: true, revision,
|
|
419
|
+
messages: messages.map(m => ({ ...m, text: m.text.slice(0, 1600), ...(m.text.length > 1600 ? { truncated: true } : {}) })) };
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
return { source: "assistant_messages", available: false, messages: [],
|
|
423
|
+
error: controller.signal.aborted ? { code: "PROGRESS_QUERY_TIMEOUT", retryable: true } : errorEnvelope(error).error };
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
clearTimeout(timer);
|
|
427
|
+
signal.removeEventListener("abort", abort);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
export function threadInteraction(id, { taskId, chatId, turn, requireBuild, timeout, afterProgress, progressInterval } = {}) {
|
|
431
|
+
const targetArgs = [
|
|
432
|
+
...(taskId ? ["--task-id", taskId] : turn === undefined ? [] : ["--turn", String(turn)]),
|
|
433
|
+
...(chatId ? ["--chat-id", chatId] : []), ...(requireBuild ? ["--require-build"] : []),
|
|
434
|
+
];
|
|
435
|
+
const target = targetArgs.length ? ` ${targetArgs.join(" ")}` : "";
|
|
436
|
+
const waitArgv = ["enter-cli", "--output", "json", "thread", "wait", id, ...targetArgs,
|
|
437
|
+
...(afterProgress ? ["--after-progress", afterProgress] : []),
|
|
438
|
+
...(progressInterval !== undefined ? ["--progress-interval", progressInterval] : []),
|
|
439
|
+
"--timeout", timeout === "0" ? "10" : timeout ?? "10"];
|
|
440
|
+
const streamArgv = [...waitArgv.slice(0, -2), "--stream", "--timeout", "0"];
|
|
441
|
+
const shellCommand = (argv) => argv.map(arg => /^[a-zA-Z0-9_./:-]+$/.test(arg) ? arg : "'" + arg.replace(/'/g, "'\"'\"'") + "'").join(" ");
|
|
403
442
|
return {
|
|
404
443
|
workflow: workflowResult({ status: "running", task_id: taskId, turn: { turn, chat_id: chatId } }),
|
|
405
444
|
monitoring_required: true,
|
|
406
|
-
wait_command:
|
|
407
|
-
|
|
445
|
+
wait_command: shellCommand(waitArgv),
|
|
446
|
+
wait_argv: waitArgv,
|
|
447
|
+
stream_command: shellCommand(streamArgv),
|
|
448
|
+
stream_argv: streamArgv,
|
|
449
|
+
watch_command: `enter-cli thread watch ${id}${target}${afterProgress ? ` --after-progress ${afterProgress}` : ""}${progressInterval !== undefined ? ` --progress-interval ${progressInterval}` : ""} --timeout ${timeout ?? "60"}`,
|
|
408
450
|
status_command: `enter-cli --output json thread status ${id}${target} --timeout 10`,
|
|
409
451
|
follow_up_command: `enter-cli --output json thread chat ${id}${chatId ? ` --chat-id ${chatId}` : ""} --file <message-file>`,
|
|
410
|
-
instructions: "
|
|
452
|
+
instructions: "Follow workflow.next_action; tool completion or CLI exit 0 is not Enter task completion. First handle incoming user messages: reply from the last observation before refreshing status. Briefly relay batched progress.messages as Enter-reported progress; do not infer progress from credits, timestamps, or tool activity. With no new narrative, observe quietly; on user inquiry report the last known state and any unavailable progress. Suppress repeated revisions and heartbeats. After a bounded observer exits while observation is still required, continue with wait_argv or validated wait_command, including after observation timeout. Do not end the task or ask whether to continue merely because wait returned. Shell-quote each wait_argv argument and invoke bare enter-cli without sleep, cd, or other prefixes. Stop observing on completed, failed, blocked, resolve_error, or resume_when_requested and handle that state. Forward questions unchanged; approve only within existing authorization. Prefer one background stream_argv invocation when the host delivers incremental output and can handle user messages concurrently. While that process is active, consume its events without starting another observer; report_progress means relay text, not restart the process. Use bounded wait only as a fallback for buffered output or completion-only notifications; batched progress ends that observation. Keep the returned timeout and progress interval; do not lengthen waits. Streaming wait and watch require incremental output delivery; consume output nonblockingly. Do not resubmit work. Hosts own scheduling; observation never cancels or approves remote work.",
|
|
411
453
|
};
|
|
412
454
|
}
|
|
413
455
|
function positiveNumber(value, option, integer = false) {
|
|
@@ -418,7 +460,10 @@ function positiveNumber(value, option, integer = false) {
|
|
|
418
460
|
return number;
|
|
419
461
|
}
|
|
420
462
|
async function readThreadSnapshot(id, turnNumber, signal, taskId, chatId) {
|
|
421
|
-
|
|
463
|
+
// Both are read-only; serial reads unnecessarily consume the observation budget.
|
|
464
|
+
const turnsPromise = client.get(`/v1/projects/${id}/thread/turns`, chatId ? { chat_id: chatId } : undefined, signal);
|
|
465
|
+
const actionsPromise = fetchPendingActions(id, [], signal).then(actions => ({ actions }), error => ({ error }));
|
|
466
|
+
const data = await turnsPromise;
|
|
422
467
|
const turns = data.turns ?? [];
|
|
423
468
|
const selected = taskId ? turns.find(t => t.id === taskId || t.task_id === taskId)
|
|
424
469
|
: turnNumber === undefined ? turns[0] : turns.find(t => Number(t.turn) === turnNumber);
|
|
@@ -442,7 +487,10 @@ async function readThreadSnapshot(id, turnNumber, signal, taskId, chatId) {
|
|
|
442
487
|
if (["cancelled", "error", "failed"].includes(status)) {
|
|
443
488
|
return { ...base, status: "failed", reason: `turn_${status}` };
|
|
444
489
|
}
|
|
445
|
-
const
|
|
490
|
+
const actionResult = await actionsPromise;
|
|
491
|
+
if ("error" in actionResult)
|
|
492
|
+
throw actionResult.error;
|
|
493
|
+
const pending = actionResult.actions.filter(a => String(a.turn) === String(selected.turn));
|
|
446
494
|
if (pending.length) {
|
|
447
495
|
const context = { signal };
|
|
448
496
|
const actions = await Promise.all(pending.map(async (a) => {
|
|
@@ -476,8 +524,11 @@ async function readThreadSnapshot(id, turnNumber, signal, taskId, chatId) {
|
|
|
476
524
|
function projectSnapshot(snapshot, compact = false) {
|
|
477
525
|
const workflow = workflowResult(snapshot);
|
|
478
526
|
const result = { ...snapshot };
|
|
479
|
-
if (compact && snapshot.turn)
|
|
527
|
+
if (compact && snapshot.turn) {
|
|
480
528
|
result.turn = pick(snapshot.turn, ["id", "turn", "status", "turn_name", "created_at", "updated_at", "commit_id", "chat_id"]);
|
|
529
|
+
if (typeof snapshot.turn.turn_name === "string")
|
|
530
|
+
result.turn.turn_name = snapshot.turn.turn_name.slice(0, 1000);
|
|
531
|
+
}
|
|
481
532
|
if (snapshot.status === "completed" && snapshot.turn) {
|
|
482
533
|
result.messages_command = `enter-cli --output json thread messages ${snapshot.project_id} --turn ${snapshot.turn.turn} --text`;
|
|
483
534
|
}
|
|
@@ -490,10 +541,13 @@ function projectSnapshot(snapshot, compact = false) {
|
|
|
490
541
|
result.integrations = { cloud: supabase?.status ?? "unknown", ai: p.ai_connection_state ?? (p.ai_capability_enabled === true ? "enabled" : "unknown") };
|
|
491
542
|
}
|
|
492
543
|
result.workflow = workflow;
|
|
493
|
-
|
|
544
|
+
// Buffered hosts may truncate verbose metadata: keep the decision and continuation first.
|
|
545
|
+
return { status: snapshot.status, workflow,
|
|
546
|
+
...(result.wait_command ? { wait_command: result.wait_command, wait_argv: result.wait_argv, instructions: result.instructions } : {}),
|
|
547
|
+
...result };
|
|
494
548
|
}
|
|
495
549
|
// Owns observation and lifetime only; callers choose JSON or NDJSON rendering.
|
|
496
|
-
async function observeThread(id, opts, wait, onSnapshot) {
|
|
550
|
+
async function observeThread(id, opts, wait, onSnapshot, continuous = false) {
|
|
497
551
|
let target = opts.turn === undefined ? undefined : positiveNumber(opts.turn, "--turn", true);
|
|
498
552
|
if (opts.taskId && target !== undefined)
|
|
499
553
|
throw new Error("Use only one of --task-id and --turn");
|
|
@@ -501,25 +555,35 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
501
555
|
throw new Error("--transport must be auto or poll");
|
|
502
556
|
if (opts.cursor && !/^\d+-\d+$/.test(opts.cursor))
|
|
503
557
|
throw new Error("--cursor must be an event ID such as 123-0");
|
|
504
|
-
|
|
558
|
+
if (opts.afterProgress && !/^[a-f0-9]{64}$/.test(opts.afterProgress))
|
|
559
|
+
throw new Error("--after-progress must be a returned progress revision");
|
|
560
|
+
const progressIntervalMs = Number(opts.progressInterval ?? "10") * 1000;
|
|
561
|
+
if (!Number.isFinite(progressIntervalMs) || progressIntervalMs < 0 || progressIntervalMs > 2147483647)
|
|
562
|
+
throw new Error("--progress-interval must be between 0 and 2147483 seconds");
|
|
563
|
+
const progressStartedAt = Date.now();
|
|
564
|
+
const timeoutMs = continuous && opts.timeout === "0" ? 0 : positiveNumber(opts.timeout, "--timeout") * 1000;
|
|
505
565
|
if (timeoutMs > 2147483647)
|
|
506
566
|
throw new Error("--timeout is too large");
|
|
507
567
|
const controller = new AbortController();
|
|
508
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
568
|
+
const timer = timeoutMs === 0 ? undefined : setTimeout(() => controller.abort(), timeoutMs);
|
|
509
569
|
const interrupt = () => controller.abort("SIGINT");
|
|
510
570
|
const terminate = () => controller.abort("SIGTERM");
|
|
511
571
|
process.once("SIGINT", interrupt);
|
|
512
572
|
process.once("SIGTERM", terminate);
|
|
513
573
|
let last = { status: "unknown", project_id: id, turn: null };
|
|
514
574
|
let querying = false;
|
|
575
|
+
let hasSnapshot = false;
|
|
576
|
+
let progressPromise;
|
|
577
|
+
let progressRead;
|
|
515
578
|
let stream;
|
|
516
579
|
let reconcileUntil = 0;
|
|
517
580
|
const emit = (extra = {}, final = false) => onSnapshot({
|
|
518
581
|
...last,
|
|
519
582
|
...(["idle", "running", "queued", "pending", "unknown"].includes(last.status)
|
|
520
|
-
? threadInteraction(id, { taskId: opts.taskId, chatId: opts.chatId, turn: target, requireBuild: opts.requireBuild
|
|
583
|
+
? threadInteraction(id, { taskId: opts.taskId, chatId: opts.chatId, turn: target, requireBuild: opts.requireBuild,
|
|
584
|
+
timeout: wait ? opts.timeout : undefined, progressInterval: opts.progressInterval === "10" ? undefined : opts.progressInterval, afterProgress: last.progress?.messages.length ? last.progress.revision : opts.afterProgress }) : {}),
|
|
521
585
|
...(opts.taskId ? { task_id: opts.taskId } : {}),
|
|
522
|
-
...(stream ? { transport: stream.mode, transport_reason: stream.reason, cursor: stream.cursor } : {}), ...extra,
|
|
586
|
+
...(stream ? { transport: stream.mode, transport_reason: stream.reason, cursor: stream.cursor } : wait ? { transport: "polling" } : {}), ...extra,
|
|
523
587
|
}, final);
|
|
524
588
|
const interruption = () => {
|
|
525
589
|
const reason = controller.signal.reason;
|
|
@@ -534,19 +598,32 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
534
598
|
throw stream.failure;
|
|
535
599
|
const revision = stream?.revision ?? 0;
|
|
536
600
|
querying = true;
|
|
601
|
+
// A persistent observer still bounds each read so a stalled endpoint cannot hang forever.
|
|
602
|
+
const queryController = new AbortController();
|
|
603
|
+
const abortQuery = () => queryController.abort(controller.signal.reason);
|
|
604
|
+
controller.signal.addEventListener("abort", abortQuery, { once: true });
|
|
605
|
+
const queryTimer = setTimeout(() => queryController.abort(), 30000);
|
|
537
606
|
try {
|
|
538
|
-
last = await readThreadSnapshot(id, target,
|
|
607
|
+
last = await readThreadSnapshot(id, target, queryController.signal, opts.taskId, opts.chatId);
|
|
608
|
+
last.observed_at = new Date().toISOString();
|
|
609
|
+
hasSnapshot = true;
|
|
539
610
|
}
|
|
540
611
|
catch (error) {
|
|
541
|
-
|
|
542
|
-
|
|
612
|
+
if (queryController.signal.aborted && !controller.signal.aborted) {
|
|
613
|
+
error = new RequestError("QUERY_TIMEOUT", "Enter status query exceeded its deadline.", true);
|
|
614
|
+
}
|
|
543
615
|
if (!wait || controller.signal.aborted || !(error instanceof RequestError) || !error.retryable)
|
|
544
616
|
throw error;
|
|
545
617
|
querying = false;
|
|
546
|
-
emit({ ...errorEnvelope(error), observation_retrying: true, last_observed:
|
|
618
|
+
emit({ ...errorEnvelope(error), observation_retrying: true, last_observed: hasSnapshot });
|
|
547
619
|
await delay(1000, undefined, { signal: controller.signal });
|
|
548
620
|
continue;
|
|
549
621
|
}
|
|
622
|
+
finally {
|
|
623
|
+
queryController.abort();
|
|
624
|
+
clearTimeout(queryTimer);
|
|
625
|
+
controller.signal.removeEventListener("abort", abortQuery);
|
|
626
|
+
}
|
|
550
627
|
if (opts.requireBuild && last.status === "completed") {
|
|
551
628
|
const build = last.project?.build_status;
|
|
552
629
|
const matches = Boolean(last.turn?.commit_id && build?.commit_id === last.turn.commit_id);
|
|
@@ -556,6 +633,30 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
556
633
|
last = { ...last, status: "failed", reason: "build_failed" };
|
|
557
634
|
}
|
|
558
635
|
querying = false;
|
|
636
|
+
if (last.turn && ["running", "completed"].includes(last.status)) {
|
|
637
|
+
if (!wait || last.status === "completed") {
|
|
638
|
+
last.progress = await readProgress(id, last.turn, controller.signal, 3000);
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
const key = JSON.stringify([last.turn.id, last.turn.chat_id, last.turn.turn]);
|
|
642
|
+
if (progressRead?.key !== key)
|
|
643
|
+
progressRead = { key, pending: false, nextReadAt: 0 };
|
|
644
|
+
const read = progressRead;
|
|
645
|
+
if (!read.pending && Date.now() >= read.nextReadAt) {
|
|
646
|
+
read.pending = true;
|
|
647
|
+
read.nextReadAt = Date.now() + 2000;
|
|
648
|
+
// Narrative latency must not delay authoritative state/card checks.
|
|
649
|
+
progressPromise = readProgress(id, last.turn, controller.signal).then(value => {
|
|
650
|
+
read.value = value;
|
|
651
|
+
read.pending = false;
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
last.progress = read.value ?? { source: "assistant_messages", available: false, messages: [] };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const stopped = interruption();
|
|
658
|
+
if (stopped !== undefined)
|
|
659
|
+
return stopped;
|
|
559
660
|
if (target === undefined && last.turn && !opts.taskId)
|
|
560
661
|
target = positiveNumber(String(last.turn.turn), "server turn", true);
|
|
561
662
|
// A newly accepted task can be temporarily absent from both reads. Keep
|
|
@@ -565,7 +666,9 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
565
666
|
emit({}, true);
|
|
566
667
|
return last.status === "failed" || last.status === "unknown" ? 1 : 0;
|
|
567
668
|
}
|
|
568
|
-
if (
|
|
669
|
+
if (controller.signal.aborted)
|
|
670
|
+
break;
|
|
671
|
+
if (!stream && opts.transport === "auto") {
|
|
569
672
|
stream = new ThreadEvents(id, {
|
|
570
673
|
signal: controller.signal, turn: target ?? (last.turn ? Number(last.turn.turn) : undefined),
|
|
571
674
|
chatId: opts.chatId ?? last.turn?.chat_id, cursor: opts.cursor, stateOnly: true,
|
|
@@ -573,16 +676,24 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
573
676
|
reconcileUntil = Date.now() + 1500; },
|
|
574
677
|
});
|
|
575
678
|
}
|
|
679
|
+
const progressChanged = last.progress?.available && last.progress.messages.length
|
|
680
|
+
&& last.progress.revision !== opts.afterProgress;
|
|
681
|
+
if (!continuous && progressChanged && Date.now() - progressStartedAt >= progressIntervalMs) {
|
|
682
|
+
emit({ progress_changed: true }, true);
|
|
683
|
+
return 0;
|
|
684
|
+
}
|
|
576
685
|
emit();
|
|
577
686
|
if (stream)
|
|
578
687
|
await stream.wait(revision, Date.now() < reconcileUntil ? 250 : stream.mode === "websocket" ? 30000 : 2000);
|
|
579
|
-
else
|
|
580
|
-
|
|
688
|
+
else {
|
|
689
|
+
const pause = delay(2000, undefined, { signal: controller.signal });
|
|
690
|
+
await (progressRead?.pending && progressPromise ? Promise.race([pause, progressPromise]) : pause);
|
|
691
|
+
}
|
|
581
692
|
}
|
|
582
693
|
const interrupted = interruption();
|
|
583
694
|
if (interrupted !== undefined)
|
|
584
695
|
return interrupted;
|
|
585
|
-
emit({ wait_timed_out: true, query_timed_out: false }, true);
|
|
696
|
+
emit({ wait_timed_out: true, query_timed_out: false, last_observed: hasSnapshot }, true);
|
|
586
697
|
return 2;
|
|
587
698
|
}
|
|
588
699
|
catch (error) {
|
|
@@ -590,13 +701,20 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
590
701
|
if (interrupted !== undefined)
|
|
591
702
|
return interrupted;
|
|
592
703
|
if (!controller.signal.aborted) {
|
|
593
|
-
emit({ ...errorEnvelope(error), last_observed: true
|
|
704
|
+
emit({ ...errorEnvelope(error), last_observed: true,
|
|
705
|
+
...(error instanceof RequestError && error.code === "QUERY_TIMEOUT" ? { query_timed_out: true } : {}),
|
|
706
|
+
}, true);
|
|
594
707
|
return 1;
|
|
595
708
|
}
|
|
596
|
-
|
|
709
|
+
if (wait && hasSnapshot) {
|
|
710
|
+
emit({ wait_timed_out: true, query_timed_out: false, last_observed: true }, true);
|
|
711
|
+
return 2;
|
|
712
|
+
}
|
|
713
|
+
emit({ wait_timed_out: wait, query_timed_out: querying, ...(querying ? { error: { code: "QUERY_TIMEOUT", message: "The observation deadline expired before a status snapshot was available. Task state is unknown; this does not identify a network or server failure.", retryable: true, outcome_unknown: false } } : {}) }, true);
|
|
597
714
|
return wait && !querying ? 2 : 1;
|
|
598
715
|
}
|
|
599
716
|
finally {
|
|
717
|
+
controller.abort();
|
|
600
718
|
stream?.close();
|
|
601
719
|
clearTimeout(timer);
|
|
602
720
|
process.removeListener("SIGINT", interrupt);
|
|
@@ -606,38 +724,68 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
606
724
|
export async function reportThread(id, opts, cmd, wait, watch = false) {
|
|
607
725
|
if (opts.full && opts.compact)
|
|
608
726
|
throw new Error("Use only one of --full and --compact");
|
|
727
|
+
const progressIntervalMs = Number(opts.progressInterval ?? "10") * 1000;
|
|
728
|
+
const heartbeatIntervalMs = Math.max(30000, progressIntervalMs);
|
|
609
729
|
let signature = "";
|
|
730
|
+
let lastOutputAt = 0;
|
|
731
|
+
let lastState = "";
|
|
732
|
+
let reportedProgress = opts.afterProgress;
|
|
610
733
|
process.exitCode = await observeThread(id, opts, wait, (snapshot, final) => {
|
|
611
|
-
|
|
734
|
+
if (!watch && !final)
|
|
735
|
+
return;
|
|
736
|
+
const now = Date.now();
|
|
737
|
+
const state = JSON.stringify([snapshot.status, snapshot.reason, snapshot.turn?.id, snapshot.error]);
|
|
738
|
+
const stateChanged = state !== lastState;
|
|
739
|
+
const intervalElapsed = now - lastOutputAt >= progressIntervalMs;
|
|
740
|
+
const heartbeatDue = now - lastOutputAt >= heartbeatIntervalMs;
|
|
741
|
+
// Skip output preparation inside the batch window without delaying state transitions.
|
|
742
|
+
if (watch && !final && !stateChanged && !intervalElapsed && !heartbeatDue)
|
|
743
|
+
return;
|
|
744
|
+
const progressChanged = Boolean(snapshot.progress?.available && snapshot.progress.messages.length
|
|
745
|
+
&& snapshot.progress.revision !== reportedProgress);
|
|
746
|
+
const data = projectSnapshot({ ...snapshot, progress_changed: progressChanged }, !opts.full);
|
|
612
747
|
if (!watch) {
|
|
613
|
-
|
|
614
|
-
print(getFormat(cmd), safeOutput(data));
|
|
748
|
+
print(getFormat(cmd), safeOutput(data));
|
|
615
749
|
return;
|
|
616
750
|
}
|
|
617
751
|
// Cursor-only changes should not flood agents with identical snapshots.
|
|
618
|
-
const next = JSON.stringify({ ...data, cursor: undefined
|
|
619
|
-
|
|
620
|
-
|
|
752
|
+
const next = JSON.stringify({ ...data, cursor: undefined, observed_at: undefined, progress_changed: undefined,
|
|
753
|
+
workflow: workflowResult({ ...snapshot, progress_changed: false }) });
|
|
754
|
+
const changed = next !== signature;
|
|
755
|
+
if (final || stateChanged || (changed && intervalElapsed) || heartbeatDue) {
|
|
756
|
+
lastState = state;
|
|
757
|
+
process.stdout.write(JSON.stringify(safeOutput({ type: final ? "result" : changed || snapshot.error ? "snapshot" : "heartbeat", ...data, progress_changed: progressChanged, emitted_at: new Date(now).toISOString() })) + "\n");
|
|
621
758
|
signature = next;
|
|
759
|
+
lastOutputAt = now;
|
|
760
|
+
if (snapshot.progress?.available && snapshot.progress.messages.length)
|
|
761
|
+
reportedProgress = snapshot.progress.revision;
|
|
622
762
|
}
|
|
623
|
-
});
|
|
763
|
+
}, watch);
|
|
624
764
|
}
|
|
625
765
|
for (const [name, timeout, description] of [
|
|
626
766
|
["status", "30", "Read one task snapshot; never wait for completion"],
|
|
627
|
-
["wait", "10", "Wait for an action or terminal state using
|
|
767
|
+
["wait", "10", "Wait for batched progress, an action or terminal state using HTTP polling"],
|
|
628
768
|
["watch", "60", "Stream meaningful state changes as NDJSON; exit on pending actions or terminal state"],
|
|
629
769
|
]) {
|
|
630
|
-
threadCmd.command(`${name} <project_id>`).description(description)
|
|
770
|
+
const command = threadCmd.command(`${name} <project_id>`).description(description)
|
|
631
771
|
.option("--turn <n>", "Select a fixed turn number")
|
|
632
772
|
.option("--task-id <id>", "Follow exactly the task returned by chat/approve, including queue time")
|
|
633
773
|
.option("--chat-id <id>", "Scope turn lookup and events to a chat")
|
|
634
774
|
.option("--cursor <id>", "Resume stream after an event ID")
|
|
635
|
-
.option("--
|
|
775
|
+
.option("--after-progress <revision>", "Wait for narrative progress after a returned revision; preserves task identity")
|
|
776
|
+
.option("--progress-interval <seconds>", "Batch ordinary progress; 0 reports every change; actions/terminal states bypass this interval", "10")
|
|
777
|
+
.option("--transport <mode>", "poll: HTTP only (default); auto: opt in to WebSocket with fallback", "poll")
|
|
636
778
|
.option("--require-build", "Require a matching successful build before reporting completion")
|
|
637
|
-
.option("--compact", "Return only monitoring fields (default
|
|
638
|
-
.option("--full", "Include full sanitized metadata
|
|
639
|
-
.option("--timeout <seconds>", "Bound the entire call without cancelling Enter", timeout)
|
|
640
|
-
.action(async (id, opts, cmd) =>
|
|
779
|
+
.option("--compact", "Return only monitoring fields (default)")
|
|
780
|
+
.option("--full", "Include full sanitized metadata, including usage")
|
|
781
|
+
.option("--timeout <seconds>", name === "watch" ? "Observation lifetime; 0 waits until action/terminal state or interruption" : "Bound the entire call without cancelling Enter", timeout)
|
|
782
|
+
.action(async (id, opts, cmd) => {
|
|
783
|
+
if (opts.stream && cmd.getOptionValueSource("timeout") === "default")
|
|
784
|
+
opts.timeout = "0";
|
|
785
|
+
await reportThread(id, opts, cmd, name !== "status", name === "watch" || Boolean(opts.stream));
|
|
786
|
+
});
|
|
787
|
+
if (name === "wait")
|
|
788
|
+
command.option("--stream", "Keep one observer running and emit batched NDJSON; default timeout 0, host owns background execution");
|
|
641
789
|
}
|
|
642
790
|
threadCmd
|
|
643
791
|
.command("diff <project_id> <turn_number>")
|
package/dist/thread-events.js
CHANGED
|
@@ -110,7 +110,10 @@ export class ThreadEvents {
|
|
|
110
110
|
if ([401, 403].includes(status)) {
|
|
111
111
|
retryable = false;
|
|
112
112
|
reason = status === 401 ? "authentication_required" : "permission_denied";
|
|
113
|
-
|
|
113
|
+
// State observers retain authoritative HTTP polling; raw message followers cannot.
|
|
114
|
+
if (!this.options.stateOnly) {
|
|
115
|
+
this.failure = new RequestError("STREAM_AUTH_ERROR", "Enter event stream rejected access. Check authentication and project permissions.");
|
|
116
|
+
}
|
|
114
117
|
}
|
|
115
118
|
else if ([404, 405, 426, 501].includes(status)) {
|
|
116
119
|
retryable = false;
|
package/dist/workflow.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface WorkflowSnapshot {
|
|
|
10
10
|
interrupted?: boolean;
|
|
11
11
|
wait_timed_out?: boolean;
|
|
12
12
|
observation_retrying?: boolean;
|
|
13
|
+
progress_changed?: boolean;
|
|
13
14
|
error?: {
|
|
14
15
|
retryable?: boolean;
|
|
15
16
|
};
|
|
@@ -34,6 +35,7 @@ export declare function workflowResult(snapshot: WorkflowSnapshot): {
|
|
|
34
35
|
interrupted: boolean;
|
|
35
36
|
};
|
|
36
37
|
reason?: string | undefined;
|
|
38
|
+
after_reporting?: string | undefined;
|
|
37
39
|
state: string;
|
|
38
40
|
task: {
|
|
39
41
|
id: {} | null;
|
package/dist/workflow.js
CHANGED
|
@@ -10,12 +10,14 @@ export function workflowResult(snapshot) {
|
|
|
10
10
|
: state === 'needs_input' ? 'handle_actions'
|
|
11
11
|
: state === 'completed' ? 'read_delivery'
|
|
12
12
|
: state === 'failed' ? 'report_failure'
|
|
13
|
-
:
|
|
14
|
-
: '
|
|
13
|
+
: snapshot.progress_changed ? 'report_progress'
|
|
14
|
+
: state === 'idle' ? 'none'
|
|
15
|
+
: 'observe';
|
|
15
16
|
return {
|
|
16
17
|
state,
|
|
17
18
|
task: { id: snapshot.task_id ?? turn?.task_id ?? turn?.id ?? null, turn: turn?.turn ?? null, chat_id: turn?.chat_id ?? null },
|
|
18
19
|
next_action: next,
|
|
20
|
+
...(next === "report_progress" ? { after_reporting: "observe" } : {}),
|
|
19
21
|
...(snapshot.reason ? { reason: snapshot.reason } : {}),
|
|
20
22
|
observation: { timed_out: Boolean(snapshot.wait_timed_out), retrying: Boolean(snapshot.observation_retrying), interrupted: Boolean(snapshot.interrupted) },
|
|
21
23
|
...(snapshot.project ? { build: {
|
package/package.json
CHANGED
package/skills/enter/SKILL.md
CHANGED
|
@@ -14,17 +14,21 @@ description: 使用 Enter CLI 创建和修改网站或应用,跟踪任务、
|
|
|
14
14
|
## 流程
|
|
15
15
|
|
|
16
16
|
1. 从用户上下文确定项目;新建时确定 workspace。通过 `project create` 或 `thread chat` 提交一次,保存 task_id。accepted 或观察超时不代表完成,也不是重新提交的理由。
|
|
17
|
-
2.
|
|
17
|
+
2. 按下方宿主能力规则选择返回的 stream_command 或 wait_command,按同一 task_id 继续;需交付构建时加 `--require-build`。新增需求单独记录返回的 task_id。
|
|
18
18
|
3. 状态的 `workflow` 给出任务关联、next_action 和构建匹配信息。遇到 `handle_actions` 按下方卡片规则处理;`read_delivery` 执行 messages_command,取得 Enter 的总结与验证证据。未知关联不代表提交失败,不猜测它属于其他已完成任务。
|
|
19
19
|
4. 依用户已有授权继续,不因阶段切换反复确认。明确要求用户决策的计划和提问仍需转交。写操作超时或解析失败先查真实状态,再决定是否重试。
|
|
20
20
|
5. 交付说明完成内容、预览/发布地址、Enter 验证结果及待配置项;代码存在、配置保存、构建成功、真实服务可用分开判断。发布按用户授权范围执行。
|
|
21
21
|
|
|
22
22
|
## 观察与响应
|
|
23
23
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
24
|
+
- CLI 用 HTTP 获取真实状态与 Enter 最新进度原文。宿主作为传话者:`workflow.next_action=report_progress` 时先转述 `progress.messages`,再按 `after_reporting=observe` 续接;处理其他 `workflow.next_action`,执行返回的命令;不翻原始日志自行分析开发阶段,不把 Enter 的自述当独立验收。`progress.available=false` 表示暂时无法获取进度,不能说没有进展;有数据但 revision 未变才是没有新内容。不编造百分比,不用额度消耗、时间戳变化证明进展。默认只读取监控字段,除非用户查询详情,不使用 --full。
|
|
25
|
+
- 首选单个 `stream_command`(`thread wait --stream --timeout 0`):仅在宿主支持后台执行、增量输出通知和同时处理用户消息时使用。CLI 内部轮询,宿主非阻塞消费阶段性事件;运行期间不要另起 wait,不因 report_progress 重启观察。收到最终 result 后再按状态处理;不使用 shell & 脱离宿主管理。watch 保留为同一流式观察能力的兼容入口。
|
|
26
|
+
- 同步或仅有完成通知的宿主回退到 `wait_command`:它在普通进度合并窗口到达且有新进展时退出,所以只支持任务完成通知的后台宿主,也能被新进展唤醒。支持后台任务时在后台运行并用完成通知续接,不阻塞等整个网站完成;同步宿主直接执行有界 wait。
|
|
27
|
+
- `wait` 返回 `progress_changed=true` 时直接简短转述,再执行带 `--after-progress` 的续接命令。只有确认宿主能在新 stdout 到达时唤醒 Agent,才选择 `watch_command`;仅能手动读取运行中输出还不够。不得用反复 job_output 空读替代通知,也不得阻塞读取直到后台任务结束。watch 无限观察仅显式 `--timeout 0`。
|
|
28
|
+
- 按 CLI 合并后的进展简短汇报;细碎技术变化无需逐条复述。面向用户不解释 CLI 协议、job 状态或调度过程。相同 revision、heartbeat 和无变化超时无需重复刷屏。超时结束的是本次观察,不是远端工作。保持同一 task_id、turn、chat 和构建要求,不重提、不询问是否继续,不写 sleep 或 status 循环。
|
|
29
|
+
- 用户插话时立即用最近已知的进度与 `observed_at` 回答,标明是上次观察;之后按需刷新一次 status,再继续原任务。不要为了拿最新状态先阻塞回答。新增需求才提交 chat,单纯问进度不触发构建。
|
|
30
|
+
- 普通进度默认每 10 秒合并一次(`--progress-interval`),保留最新原文,不猜测阶段;问题卡、完成和失败立即返回。保留返回的间隔与时限,不自行调长等待。
|
|
31
|
+
- `wait` 默认最多 10 秒,`watch` 默认 60 秒,显式时限会保留在续接命令中。这是观察上限,不是心跳周期或响应保证;不得为等到完成不断延长阻塞调用。宿主如果本身不支持插话或后台唤醒,CLI 不能保证即时响应或主动通知。
|
|
28
32
|
|
|
29
33
|
## 卡片
|
|
30
34
|
|
|
@@ -33,4 +37,4 @@ description: 使用 Enter CLI 创建和修改网站或应用,跟踪任务、
|
|
|
33
37
|
- 普通确认:已有授权就 approve;需要新的决定时询问。用户拒绝才 reject。
|
|
34
38
|
- 配置输入:已有授权和值/文件就提交;缺什么只问什么。普通配置可直接询问,真实密钥使用可用的安全输入或用户指定文件;表单是替代入口,不强制绕路。OAuth/secret/Stripe 的字段、stdin 和更新方法见 [配置输入](references/configuration.md),仅遇到这些卡片时读取。
|
|
35
39
|
|
|
36
|
-
CLI
|
|
40
|
+
CLI 负责 HTTP 观察和可选的事件监听;宿主负责后台调度与用户交互。等待工具不自动批准卡片。宿主没有浏览器不影响 Enter 内部验证,不以关键词扫描替代其验证结果。
|