cueline 0.1.2 → 0.1.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.
Files changed (116) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.codex-plugin/plugin.json +3 -3
  3. package/CHANGELOG.md +38 -0
  4. package/README.ja.md +65 -21
  5. package/README.ko.md +65 -21
  6. package/README.md +70 -23
  7. package/README.zh-CN.md +65 -21
  8. package/README.zh-TW.md +68 -22
  9. package/dist/src/api-contracts.d.ts +77 -0
  10. package/dist/src/api-contracts.js +2 -0
  11. package/dist/src/api-contracts.js.map +1 -0
  12. package/dist/src/api-controller-handoff.d.ts +6 -0
  13. package/dist/src/api-controller-handoff.js +253 -0
  14. package/dist/src/api-controller-handoff.js.map +1 -0
  15. package/dist/src/api-runtime-lifecycle.d.ts +22 -0
  16. package/dist/src/api-runtime-lifecycle.js +568 -0
  17. package/dist/src/api-runtime-lifecycle.js.map +1 -0
  18. package/dist/src/api.d.ts +9 -27
  19. package/dist/src/api.js +138 -42
  20. package/dist/src/api.js.map +1 -1
  21. package/dist/src/browser/browser-adapter.d.ts +11 -1
  22. package/dist/src/browser/codex-iab/bootstrap.d.ts +16 -0
  23. package/dist/src/browser/codex-iab/bootstrap.js +49 -0
  24. package/dist/src/browser/codex-iab/bootstrap.js.map +1 -1
  25. package/dist/src/browser/codex-iab/chatgpt-client.js +346 -120
  26. package/dist/src/browser/codex-iab/chatgpt-client.js.map +1 -1
  27. package/dist/src/browser/codex-iab/recovery-evidence.d.ts +6 -0
  28. package/dist/src/browser/codex-iab/recovery-evidence.js +37 -0
  29. package/dist/src/browser/codex-iab/recovery-evidence.js.map +1 -0
  30. package/dist/src/browser/codex-iab/submission-url.d.ts +2 -0
  31. package/dist/src/browser/codex-iab/submission-url.js +53 -0
  32. package/dist/src/browser/codex-iab/submission-url.js.map +1 -0
  33. package/dist/src/cli/main.js +305 -20
  34. package/dist/src/cli/main.js.map +1 -1
  35. package/dist/src/core/controller-abort.d.ts +1 -0
  36. package/dist/src/core/controller-abort.js +11 -0
  37. package/dist/src/core/controller-abort.js.map +1 -0
  38. package/dist/src/core/controller-command-execution.d.ts +9 -0
  39. package/dist/src/core/controller-command-execution.js +299 -0
  40. package/dist/src/core/controller-command-execution.js.map +1 -0
  41. package/dist/src/core/controller-loop.d.ts +3 -37
  42. package/dist/src/core/controller-loop.js +386 -291
  43. package/dist/src/core/controller-loop.js.map +1 -1
  44. package/dist/src/core/controller-turn.d.ts +16 -0
  45. package/dist/src/core/controller-turn.js +309 -0
  46. package/dist/src/core/controller-turn.js.map +1 -0
  47. package/dist/src/core/controller-types.d.ts +58 -0
  48. package/dist/src/core/controller-types.js +2 -0
  49. package/dist/src/core/controller-types.js.map +1 -0
  50. package/dist/src/core/persisted-run.d.ts +4 -0
  51. package/dist/src/core/persisted-run.js +19 -0
  52. package/dist/src/core/persisted-run.js.map +1 -0
  53. package/dist/src/core/process-liveness.d.ts +2 -0
  54. package/dist/src/core/process-liveness.js +44 -0
  55. package/dist/src/core/process-liveness.js.map +1 -0
  56. package/dist/src/core/run-status.d.ts +53 -0
  57. package/dist/src/core/run-status.js +224 -0
  58. package/dist/src/core/run-status.js.map +1 -0
  59. package/dist/src/core/state-machine.d.ts +17 -4
  60. package/dist/src/core/state-machine.js +170 -16
  61. package/dist/src/core/state-machine.js.map +1 -1
  62. package/dist/src/jobs/status.d.ts +7 -1
  63. package/dist/src/jobs/status.js.map +1 -1
  64. package/dist/src/jobs/supervisor.d.ts +2 -0
  65. package/dist/src/jobs/supervisor.js +47 -13
  66. package/dist/src/jobs/supervisor.js.map +1 -1
  67. package/dist/src/protocol/types.d.ts +1 -1
  68. package/dist/src/protocol/validate-command.js +18 -0
  69. package/dist/src/protocol/validate-command.js.map +1 -1
  70. package/dist/src/router/resolver.d.ts +2 -1
  71. package/dist/src/router/resolver.js +31 -13
  72. package/dist/src/router/resolver.js.map +1 -1
  73. package/dist/src/runners/process-runner.d.ts +2 -2
  74. package/dist/src/runners/process-runner.js +113 -9
  75. package/dist/src/runners/process-runner.js.map +1 -1
  76. package/dist/src/runners/runner-adapter.d.ts +9 -2
  77. package/dist/src/runners/runner-adapter.js.map +1 -1
  78. package/dist/src/state/atomic-write.js +17 -2
  79. package/dist/src/state/atomic-write.js.map +1 -1
  80. package/dist/src/state/cancellation.d.ts +42 -0
  81. package/dist/src/state/cancellation.js +168 -0
  82. package/dist/src/state/cancellation.js.map +1 -0
  83. package/dist/src/state/event-log.d.ts +17 -1
  84. package/dist/src/state/event-log.js +328 -18
  85. package/dist/src/state/event-log.js.map +1 -1
  86. package/dist/src/state/paths.d.ts +4 -0
  87. package/dist/src/state/paths.js +4 -0
  88. package/dist/src/state/paths.js.map +1 -1
  89. package/dist/src/state/runtime-lease.d.ts +53 -0
  90. package/dist/src/state/runtime-lease.js +727 -0
  91. package/dist/src/state/runtime-lease.js.map +1 -0
  92. package/dist/src/state/runtime-retirement.d.ts +16 -0
  93. package/dist/src/state/runtime-retirement.js +95 -0
  94. package/dist/src/state/runtime-retirement.js.map +1 -0
  95. package/dist/src/state/runtime-takeover-intent.d.ts +1 -0
  96. package/dist/src/state/runtime-takeover-intent.js +18 -0
  97. package/dist/src/state/runtime-takeover-intent.js.map +1 -0
  98. package/dist/src/state/store.d.ts +12 -1
  99. package/dist/src/state/store.js +246 -40
  100. package/dist/src/state/store.js.map +1 -1
  101. package/dist/src/version.d.ts +1 -1
  102. package/dist/src/version.js +1 -1
  103. package/docs/architecture.md +16 -11
  104. package/docs/assets/cueline-loop-en.svg +11 -10
  105. package/docs/assets/cueline-loop-ja.svg +11 -10
  106. package/docs/assets/cueline-loop-ko.svg +11 -10
  107. package/docs/assets/cueline-loop-zh-CN.svg +11 -10
  108. package/docs/assets/cueline-loop-zh-TW.svg +11 -10
  109. package/docs/compatibility.md +16 -8
  110. package/docs/controller-protocol.md +8 -4
  111. package/docs/runner-contract.md +26 -6
  112. package/docs/state-and-recovery.md +69 -16
  113. package/package.json +4 -3
  114. package/schemas/controller-observation.schema.json +1 -1
  115. package/scripts/validate-plugin.mjs +66 -0
  116. package/skills/cueline/SKILL.md +89 -13
@@ -9,6 +9,13 @@ ${CUELINE_HOME:-$HOME/.cueline}/
9
9
  ├── runs/
10
10
  │ └── <run-id>/
11
11
  │ ├── events.jsonl
12
+ │ ├── events.jsonl.segments/
13
+ │ ├── runtime.json.fence
14
+ │ ├── runtime.json.epochs/
15
+ │ ├── runtime.json.retired-owners/
16
+ │ ├── runtime.json.takeover-intents/
17
+ │ ├── cancel.json # only after run cancellation is requested
18
+ │ ├── job-cancellations/ # only after job cancellation is requested
12
19
  │ └── snapshot.json
13
20
  └── jobs/
14
21
  └── <job-id>.json
@@ -18,7 +25,7 @@ ${CUELINE_HOME:-$HOME/.cueline}/
18
25
 
19
26
  ## Event log
20
27
 
21
- `events.jsonl` is append-only and authoritative. Each line contains a monotonically increasing sequence number, timestamp, event type, and payload. The writer opens the file with owner-only mode when creating it and calls `sync` after every append.
28
+ The logical event log is append-only and authoritative. A legacy or atomically created first event may live in `events.jsonl`; subsequent events use one immutable file per sequence under `events.jsonl.segments/`. Each event contains a monotonically increasing sequence number, timestamp, type, payload, and an optional runtime owner ID. The writer fully writes and syncs a temporary segment, hard-links it to the sequence name with create-if-absent semantics, then syncs the containing directory before reporting success. A `legacy-fence.json` freezes the accepted byte prefix of an older JSONL writer, so a still-loaded pre-segment process may append a diagnostic suffix without creating duplicate logical sequence numbers. Concurrent or recovered writers that lose a sequence race reread and retry; no global event lock or partial segment becomes authoritative.
22
29
 
23
30
  Important transitions include:
24
31
 
@@ -26,38 +33,71 @@ Important transitions include:
26
33
  - controller turn intent, submission checkpoints, response, reconciliation, rejection, and accepted command
27
34
  - job registration and status changes
28
35
  - notices
29
- - complete, blocked, and failed terminal records
36
+ - complete, blocked, cancelled, and failed terminal records
30
37
 
31
- The controller turn is recorded before sending it through the browser. Immediately before the send click, CueLine records `controller_turn_submission_started` with `submission_state: possibly_sent`; after the click it records `controller_turn_submitted`. A verified response is recorded separately. A job is registered only after the entire dispatch has passed pre-spawn route validation, and before its process starts. This ordering leaves evidence when an interruption occurs between intent and side effect.
38
+ The controller turn is recorded before sending it through the browser. After the composer settles, CueLine records whether the prompt is `inline_ready` or `attachment_ready`. Immediately before its one allowed send attempt it records `controller_turn_submission_started` with `submission_state: submitting`; a verified post-click boundary becomes `submitted`, while an ambiguous click becomes `possibly_sent`. A verified response is recorded separately. A job is registered only after the entire dispatch has passed validation, and before either caller handoff or process start. This ordering leaves evidence when an interruption occurs between intent and side effect.
32
39
 
33
40
  The absence of `controller_response_received` proves only that the local runtime did not record a response. It does **not** prove that ChatGPT did not answer. `run_failed` therefore retains the safe error message, stage, exact request ID, submission state, and known conversation URL. CueLine never treats a missing response event as permission to resend an ambiguous prompt.
34
41
 
35
42
  ## Snapshot
36
43
 
37
- `snapshot.json` is a materialized view of the event stream. CueLine writes a temporary file in the same directory and renames it over the destination. A snapshot records its state protocol, run ID, and last applied sequence.
38
-
39
- On load, CueLine uses a snapshot only when its identity and sequence are valid. A missing, malformed, or out-of-range snapshot is ignored and the state is replayed from event 1. Events newer than a valid snapshot are applied afterward.
44
+ `snapshot.json` is a disposable materialized view of the event stream. CueLine writes a temporary file in the same directory and renames it over the destination. A snapshot records its state protocol, run ID, and last applied physical sequence. Loads replay the authoritative event stream from event 1 instead of trusting a snapshot that a retired owner could have replaced.
40
45
 
41
46
  The snapshot is therefore disposable. Do not edit the event log by hand; invalid JSON or a broken sequence makes the run unreplayable and is reported rather than silently skipped.
42
47
 
43
48
  ## Job status
44
49
 
45
- The supervisor atomically replaces one JSON file per job. Foreground work returns its terminal status directly. Background work first persists `running`; later `wait` reads the same in-process completion or the last persisted status.
50
+ The supervisor atomically replaces one JSON status file per job. Status records include run ID, job key, lane, mode, and—when a process exists—its child PID. The task remains in the authoritative run's `job_registered` event; `cueline jobs` joins that event metadata with the status file so the task is still visible. Caller jobs are visible as `pending` before the current Codex submits a terminal result. PID is diagnostic evidence only; CueLine never treats a PID alone as authority to kill a process. Foreground process work returns its terminal status directly. Background process work first persists `running`; later `wait` reads the same in-process completion or the last persisted status.
51
+
52
+ The run event log still records the controller-visible job transitions. A status file is execution evidence, not a substitute for the run history. `cueline jobs --json` includes run ID, job key, lane, mode, task, and PID when known. For process runs it adds a derived observed status: `running` requires active runtime ownership, `orphaned` means persisted process work says active but no owner is proven, `unverified` covers legacy records without a run ID, and `conflict` means a status file disagrees with authoritative run events. A conflict reports the authoritative `status`, exposes the file's value as `persistedStatus`, and omits the untrusted late result. Pending caller jobs remain pending during their intentional ownerless handoff.
53
+
54
+ ## Runtime ownership and status
55
+
56
+ `runtime.json.fence` selects the authoritative owner-heartbeat record under `runtime.json.epochs/`. A live controller refreshes only its selected epoch and removes its own record on normal exit. If a mutation lock is abandoned, recovery rotates the fence before reuse; a paused old writer can then modify only its old epoch and cannot overwrite or release the new owner. An explicit takeover first records the operator's exact expected owner and heartbeat under `runtime.json.takeover-intents/`, including rejected race attempts. While holding the same mutation lock as runtime-authored event appends, a successful takeover then atomically replaces the exact stale lease and embeds the retired owner's last authoritative event sequence in the new lease. That lease replacement is the commit boundary: a failed replacement leaves the old owner authoritative. Before the replacement lease is removed, embedded cutoffs are also mirrored to immutable records under `runtime.json.retired-owners/`. Owner-tagged events after a committed cutoff remain on disk for audit but are excluded from state, status, reconciliation, and job metadata replay. Legacy `runtime.json` records are migrated on the first mutation. Creation and dead-owner retirement remain atomic and owner-checked so two sessions cannot both claim the run. `cueline run status <run-id> --json` combines event replay, lease ownership, cancellation requests, and job state.
46
57
 
47
- The run event log still records the controller-visible job transitions. A status file is execution evidence, not a substitute for the run history.
58
+ - `phase: controller_response_pending`, `controller.pendingTurns === 1`, missing runtime ownership, and `safeNextAction: observe` mean the exact normally submitted request awaits one read-only observation. `controller.responseAccepted` is false even when `lastAcceptedAction` summarizes an earlier round; continue the same run without resend. `safeNextAction: reconcile` is reserved for ambiguous, manually submitted, or multiple pending turns.
59
+ - `controller.responseAccepted: true` means no newer turn is pending and a controller response was accepted. `lastAcceptedAction` and `lastAcceptedJobKeys` summarize what that accepted response ordered without dumping its full task text.
60
+ - `phase: jobs_running` plus `runtime.ownership: active` means local jobs are executing under the original loop.
61
+ - `executor: caller`, `phase: caller_jobs_pending`, `runtime.ownership: missing`, and `safeNextAction: execute_caller_jobs` is a healthy durable pause. The current Codex should execute the listed `advise` tasks and submit their results; it must not claim that ChatGPT used local tools.
62
+ - Caller handoff has no execution claim. Coordinate one session before doing the local advice; if two sessions execute it, the first terminal evidence submitted wins and the later submission returns `already_terminal`.
63
+ - `runtime_ownership_unknown` means persisted `running` is not proof of a live process. `runtime_stale` requires explicit `cueline run takeover <run-id>` confirmation before the exact stale heartbeat can be retired. Active-looking process jobs are reported as `orphaned`.
64
+ - `runtime_active` means a live owner is still settling a failed state; another session must observe rather than continue.
65
+ - `continueAllowed: false` is a hard stop. Another session must not send, resume, or claim completion.
48
66
 
49
67
  ## Continue behavior
50
68
 
51
- `continueCueLineRun` loads the exact `runId`, replays state as needed, and resumes the same persisted run. The public runtime also reuses the stored ChatGPT conversation URL unless an explicit compatible adapter is supplied.
69
+ Always run `cueline run status <run-id> --json` before continuation. `continueCueLineRun` loads the exact `runId`, replays state as needed, and resumes the same persisted run. The public runtime also reuses the stored ChatGPT conversation URL unless an explicit compatible adapter is supplied.
52
70
 
53
- - `complete` and `blocked` runs are returned as-is; they are not dispatched again.
71
+ - `complete`, `blocked`, and `cancelled` runs are returned as-is; they are not dispatched again.
72
+ - an active owner is rejected with `RUN_ALREADY_ACTIVE`; a stale lease requires dead-owner inspection; missing ownership is healthy only for a pristine or caller-mode run
54
73
  - a non-terminal or locally `failed` run with no pending controller turn can be marked resumed and driven for additional rounds
74
+ - `maxRounds` is a durable total-run contract, not a per-continuation allowance; continuation reuses the created value and cannot widen or shrink it
55
75
  - when a failure proves `definitely_not_sent` for the exact sole pending request, CueLine records that turn as abandoned and safely starts a new round
56
76
  - a pending controller turn is reconciled read-only from the exact conversation before any new prompt is sent
57
- - reconciliation requires the page's last user message to equal the persisted prompt, the last message to be a completed assistant response, and both Pro model checks to pass
77
+ - built-in caller/IAB submission returns `awaiting_controller` after one durable send and exact URL capture; each later continuation performs one non-blocking observation and returns the same status again if Pro is unfinished
78
+ - inline reconciliation requires the page's last user message to equal the persisted prompt, a completed assistant response, and both Pro model checks
79
+ - attachment reconciliation may omit the full prompt from the visible user message, but only after a formal manual-send confirmation and exact conversation plus protocol/run/round/request/Pro evidence; response and URL must come from the same DOM snapshot; this exact operator-confirmed path also supports legacy turns that predate the durable assistant-count baseline, while automatic attachment recovery still requires that baseline
80
+ - a specified recoverably abandoned turn can be restored through that same append-only confirmation path when no same/newer command was accepted; it is never resent
58
81
  - when more than one legacy turn is pending, CueLine stops with `MULTIPLE_CONTROLLER_TURNS_PENDING` rather than guessing
59
82
  - deterministic job IDs suppress a repeated dispatch already present in state
60
- - running jobs can be observed or waited through their persisted status
83
+ - caller jobs are returned as `awaiting_caller`; after local execution, `submitCueLineCallerJobResult` persists the full result and continuation sends bounded evidence to the same controller
84
+ - `complete` and `blocked` are rejected while any required or optional job is pending/running, so a terminal command cannot orphan background work
85
+ - any non-normal process-loop exit cancels and settles its owned active jobs before releasing the runtime lease, including round-limit and controller-validation failures
86
+ - process jobs can be observed or waited through their persisted status
87
+
88
+ Caller mode accepts `advise` only. Explicit process execution defaults to at most two concurrent jobs globally and two per lane; a dispatch containing any `work` job is serialized in command order to avoid overlapping mutations.
89
+
90
+ ## Cancellation and deadlines
91
+
92
+ `cueline run cancel <run-id>` and its `run stop` alias write a durable request. `cueline job cancel <run-id> <job-id>` targets one job. An active owner observes the request, sends `SIGTERM` to its owned child, escalates to `SIGKILL` after the grace interval, and persists the terminal transition. Cancelled `advise` is `cancelled`; started `work` is `ambiguous` because partial side effects cannot be disproved.
93
+
94
+ The CLI does not drive the browser. `doctor`, `routing`, `jobs`, `run status`, `api path`, and `config path` are read-only. `install`/`uninstall` change only the package-owned skill link. `run reconcile`, `run takeover`, `run reconcile-runtime`, `run cancel`/`run stop`, and `job cancel` append audit evidence or mutate durable local state; their complete positional syntax and options are listed by `cueline help`.
95
+
96
+ When a process run has no verifiable owner, runtime reconciliation checks both the recorded process and its POSIX process group. Surviving processes block settlement. Only dead `advise` work can become failed/cancelled; `work` remains `ambiguous` when partial side effects cannot be disproved. `cueline run reconcile-runtime <run-id>` exposes this transition without treating a stale PID as kill authority.
97
+
98
+ `cueline run takeover <run-id>` is narrower: it atomically replaces only the exact stale owner/heartbeat observed by the command and writes `runtime_stale_owner_takeover_requested` followed by `runtime_stale_owner_takeover_confirmed` while the replacement lease is held. A fresh active heartbeat is refused. The result says `next: continue` when the run may resume directly, or `next: reconcile_runtime` when ownerless process jobs must first pass process/group liveness reconciliation.
99
+
100
+ `runTimeoutMs` is an optional owned-advancement deadline. In explicit process mode it limits that controller-loop invocation and aborts owned jobs before returning `RUN_TIMEOUT`. Caller runs intentionally release ownership at controller/caller pauses, so the value limits each `runCueLine` or `continueCueLineRun` call in which it is supplied; ownerless Pro thinking and local handoff time between calls are not counted. `defaultTimeoutMs` and job `timeout_ms` remain per-job limits. A caller-side or tool-side wait timeout is not a cancellation signal; after one fires, inspect `run status`, then explicitly cancel if required. API callers may pass `signal` for direct cancellation propagation.
61
101
 
62
102
  For one unambiguous pending turn, supply the exact conversation URL when it was not captured before the failure:
63
103
 
@@ -81,6 +121,16 @@ await continueCueLineRun({
81
121
 
82
122
  Do not set `abandonOtherPendingTurns` from sequence order alone. Use direct page evidence to match the visible user prompt to the persisted request. CueLine records every explicit abandonment.
83
123
 
124
+ When ChatGPT converted the prompt into an attachment and the operator manually performed the one send, record the confirmation formally instead of editing `events.jsonl`:
125
+
126
+ ```bash
127
+ cueline run reconcile RUN_ID \
128
+ --request-id REQUEST_ID \
129
+ --manual-send-confirmed
130
+ ```
131
+
132
+ The command requires the exact persisted conversation and writes `controller_turn_manual_submission_confirmed`. Then continue with that request ID. CueLine accepts the response only if its Pro evidence and full envelope identity match; a mismatch is rejected without resend or dispatch.
133
+
84
134
  Continuation cannot reconstruct an expired ChatGPT login, a deleted conversation, an unavailable registered executable, or an in-memory child process that disappeared with the host process. In those cases CueLine reports the concrete failure; it does not fabricate completion.
85
135
 
86
136
  ## Recovery procedure
@@ -89,9 +139,12 @@ Continuation cannot reconstruct an expired ChatGPT login, a deleted conversation
89
139
  2. Record the `runId` from the earlier result or directory name.
90
140
  3. Restore access to the same ChatGPT conversation in Codex's built-in Browser.
91
141
  4. Restore any locally required executable/configuration without copying browser credentials.
92
- 5. Inspect `loadCueLineRunState(runId, ...)`. If multiple `pendingControllerTurns` exist, match the visible page prompt and select its exact `requestId`.
93
- 6. Call `continueCueLineRun({ runId, conversationUrl, ... })`. Add `reconcileRequestId` and `abandonOtherPendingTurns: true` only for an explicitly resolved multi-pending case.
94
- 7. If reconciliation fails, do not resend manually. Preserve the page and report the exact `CONTROLLER_RECONCILIATION_*`, `IAB_RECONCILIATION_FAILED`, or `TAB_RECOVERY_UNSAFE` error.
95
- 8. Treat the new terminal result as valid only after its event and job evidence is present.
142
+ 5. Run `cueline run status <run-id> --json`. If the owner is active, observe or cancel; do not continue. If it is stale, use `cueline run takeover <run-id> --json` once and follow its exact `next` field. `controller_response_pending` means observe the same submitted turn; an accepted response plus jobs means do not call that accepted round “waiting for ChatGPT.”
143
+ 6. Inspect `loadCueLineRunState(runId, ...)` only when status says recovery is needed. If multiple `pendingControllerTurns` exist, match the visible page prompt and select its exact `requestId`.
144
+ 7. For `controller_response_pending`, wait a bounded interval and call `continueCueLineRun` once; unfinished Pro output returns `awaiting_controller` without resend.
145
+ 8. For `caller_jobs_pending`, execute only the listed `advise` jobs locally, submit each terminal result, then call `continueCueLineRun`; no browser resend is involved.
146
+ 9. Otherwise call `continueCueLineRun({ runId, conversationUrl, ... })` only when continuation is allowed. Add `reconcileRequestId` and `abandonOtherPendingTurns: true` only for an explicitly resolved multi-pending case. For a manually sent attachment, first use the formal reconcile command above.
147
+ 10. If reconciliation fails, do not resend. Preserve the page and report the exact `CONTROLLER_RECONCILIATION_*`, `IAB_RECONCILIATION_FAILED`, or `TAB_RECOVERY_UNSAFE` error. Post-creation errors expose `details.run_id` for this recovery.
148
+ 11. Treat the new terminal result as valid only after its event and job evidence is present.
96
149
 
97
150
  For manual diagnosis, use `cueline jobs` and inspect the run's JSONL as read-only evidence. Do not copy `CUELINE_HOME` between machines as a replacement for local process or browser session state.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cueline",
3
- "version": "0.1.2",
4
- "description": "A ChatGPT web conversation directs; CueLine validates each command and runs the work on your machine, with a durable record of both.",
3
+ "version": "0.1.4",
4
+ "description": "A ChatGPT web conversation directs; CueLine validates each text command and hands durable local advice to the current Codex.",
5
5
  "keywords": [
6
6
  "chatgpt",
7
7
  "codex",
@@ -46,6 +46,7 @@
46
46
  "!docs/superpowers",
47
47
  "evals",
48
48
  "schemas",
49
+ "scripts",
49
50
  "skills",
50
51
  "install.sh",
51
52
  "CHANGELOG.md",
@@ -65,7 +66,7 @@
65
66
  "test:unit": "npm run build && node --test dist/test/unit/*.test.js",
66
67
  "test:integration": "npm run build && node --test dist/test/integration/*.test.js",
67
68
  "smoke:fake": "npm run build && node --test dist/test/smoke/fake-smoke.test.js",
68
- "validate:plugin": "python3 \"${CODEX_HOME:-$HOME/.codex}/skills/.system/plugin-creator/scripts/validate_plugin.py\" ."
69
+ "validate:plugin": "node scripts/validate-plugin.mjs"
69
70
  },
70
71
  "devDependencies": {
71
72
  "@types/node": "^22.20.1",
@@ -20,7 +20,7 @@
20
20
  "job_id": { "type": "string" },
21
21
  "job_key": { "type": "string" },
22
22
  "required": { "type": "boolean" },
23
- "status": { "enum": ["pending", "running", "succeeded", "failed", "timed_out", "ambiguous"] },
23
+ "status": { "enum": ["pending", "running", "succeeded", "failed", "timed_out", "cancelled", "ambiguous"] },
24
24
  "output": { "type": "string" },
25
25
  "error": { "type": "string" }
26
26
  },
@@ -0,0 +1,66 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import path from "node:path";
4
+
5
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
6
+
7
+ async function readJson(relativePath) {
8
+ const absolutePath = path.join(root, relativePath);
9
+ try {
10
+ return JSON.parse(await readFile(absolutePath, "utf8"));
11
+ } catch (error) {
12
+ throw new Error(`${relativePath} must exist and contain valid JSON: ${error.message}`);
13
+ }
14
+ }
15
+
16
+ function requireNonEmptyString(value, field) {
17
+ if (typeof value !== "string" || value.trim() === "") {
18
+ throw new Error(`${field} must be a non-empty string`);
19
+ }
20
+ }
21
+
22
+ const semver = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
23
+ const packageJson = await readJson("package.json");
24
+ const codexManifest = await readJson(".codex-plugin/plugin.json");
25
+ const claudeManifest = await readJson(".claude-plugin/plugin.json");
26
+
27
+ for (const [name, manifest] of [
28
+ [".codex-plugin/plugin.json", codexManifest],
29
+ [".claude-plugin/plugin.json", claudeManifest],
30
+ ]) {
31
+ requireNonEmptyString(manifest.name, `${name}: name`);
32
+ requireNonEmptyString(manifest.version, `${name}: version`);
33
+ requireNonEmptyString(manifest.description, `${name}: description`);
34
+ requireNonEmptyString(manifest.author?.name, `${name}: author.name`);
35
+ if (!semver.test(manifest.version)) {
36
+ throw new Error(`${name}: version must be strict semver`);
37
+ }
38
+ if (manifest.name !== packageJson.name || manifest.version !== packageJson.version) {
39
+ throw new Error(`${name}: name and version must match package.json`);
40
+ }
41
+ }
42
+
43
+ const pluginInterface = codexManifest.interface;
44
+ for (const field of [
45
+ "displayName",
46
+ "shortDescription",
47
+ "longDescription",
48
+ "developerName",
49
+ "category",
50
+ "defaultPrompt",
51
+ ]) {
52
+ requireNonEmptyString(pluginInterface?.[field], `.codex-plugin/plugin.json: interface.${field}`);
53
+ }
54
+ if (!Array.isArray(pluginInterface.capabilities) || pluginInterface.capabilities.some((item) => typeof item !== "string")) {
55
+ throw new Error(".codex-plugin/plugin.json: interface.capabilities must be an array of strings");
56
+ }
57
+ if (codexManifest.skills !== "./skills/") {
58
+ throw new Error(".codex-plugin/plugin.json: skills must be ./skills/");
59
+ }
60
+
61
+ const skill = await readFile(path.join(root, "skills/cueline/SKILL.md"), "utf8");
62
+ if (!skill.startsWith("---\n") || !/^name: cueline$/m.test(skill) || !/^description: .+$/m.test(skill)) {
63
+ throw new Error("skills/cueline/SKILL.md must contain cueline name and description frontmatter");
64
+ }
65
+
66
+ console.log(`Plugin validation passed: ${root} (${packageJson.version})`);
@@ -1,20 +1,20 @@
1
1
  ---
2
2
  name: cueline
3
- description: Use CueLine whenever the user wants a ChatGPT web Pro conversation to be the top-level controller that plans, delegates, reviews evidence, or decides completion while Codex performs the permitted local execution. Trigger for requests to let web ChatGPT lead Codex, combine browser advice with local workers, run a durable controller loop, or continue an existing CueLine run. CueLine is text-only in v0.1 and uses Codex's built-in in-app Browser.
3
+ description: Use CueLine whenever the user wants a ChatGPT web Pro conversation to be the top-level text controller that plans, delegates, reviews evidence, or decides completion while the current Codex performs permitted caller-mode advice. Trigger for requests to let web ChatGPT lead Codex, combine browser judgment with local tools, run a durable controller loop, or continue an existing CueLine run. CueLine uses Codex's built-in in-app Browser.
4
4
  ---
5
5
 
6
6
  # CueLine
7
7
 
8
- Use CueLine to put the current ChatGPT web conversation in charge of planning and next-step decisions while Codex remains the local intermediary and executor. The web page has no direct machine access; it sends validated text commands that CueLine applies through local policy.
8
+ Use CueLine to put the current ChatGPT web conversation in charge of planning and next-step decisions while the current Codex remains the local intermediary and executor. The web page has no direct machine tools or filesystem access; it only sends validated text commands. Never describe a Pro response as having inspected the repository or used local tools.
9
9
 
10
10
  ## Preconditions
11
11
 
12
12
  1. Confirm the current host, operator, and work directory before local work.
13
- 2. Run `cueline doctor` and require Node.js 22 or newer plus at least one usable lane. Run `cueline api path` and require the returned API module to exist. The bundled default lane needs the `codex` CLI on `PATH`.
13
+ 2. Run `cueline version`, then `cueline doctor`, and require the same expected package version, Node.js 22 or newer, and `caller_ready yes`. Run `cueline api path` and require the returned API module to exist. Do not reuse a module imported before a package update. `process_available_lanes` may be zero for caller mode; a usable process lane and `codex` CLI are required only when explicitly selecting the process executor.
14
14
  3. Use Codex's persistent Node REPL/runtime together with the built-in in-app Browser (IAB), not a separate plain `node` child, Chrome automation, or GPT Relay. Claim the logged-in `chatgpt.com` tab intended for this run.
15
15
  4. CueLine requires the composer model selector to show `Pro` before every controller turn and requires the completed assistant message's `data-message-model-slug` to identify a Pro model. The account label (for example, a profile name ending in `Pro`) is subscription evidence only and never model evidence. Do not bypass `MODEL_SELECTOR_MISSING`, `PRO_MODEL_UNAVAILABLE`, `PRO_MODEL_SELECTION_FAILED`, or `PRO_MODEL_MISMATCH`.
16
16
  5. Do not request, read, copy, or print cookies, access tokens, browser session material, or private environment values.
17
- 6. Keep v0.1 controller traffic text-only. Do not attempt images, file upload, Deep Research, Projects, or Apps. CueLine may switch the composer from another model to `Pro`; no other model switching is allowed.
17
+ 6. Keep v0.1 controller traffic text-only. ChatGPT may automatically convert a long filled prompt into an attachment; CueLine recognizes that state. Do not deliberately upload files, images, use Deep Research, Projects, or Apps. CueLine may switch the composer from another model to `Pro`; no other model switching is allowed.
18
18
 
19
19
  If live IAB, authentication, build output, or a required runner is missing, report that exact prerequisite. Do not claim a live run from fake or read-only evidence.
20
20
 
@@ -24,36 +24,91 @@ Drive CueLine from Codex's Node runtime so the injected IAB object remains avail
24
24
 
25
25
  ```js
26
26
  var cuelineApiPath = "/exact/output/from/cueline/api/path";
27
+ var expectedCueLineVersion = "exact output from cueline version";
28
+ const { pathToFileURL } = await import("node:url");
29
+ var cuelineModuleUrl = `${pathToFileURL(cuelineApiPath).href}?v=${encodeURIComponent(expectedCueLineVersion)}`;
27
30
  const {
31
+ CUELINE_VERSION,
28
32
  createCodexIabAdapter,
33
+ continueCueLineRun,
29
34
  runCueLine,
30
35
  startCueLineRun,
31
- } = await import(cuelineApiPath);
36
+ submitCueLineCallerJobResult,
37
+ } = await import(cuelineModuleUrl);
38
+ if (CUELINE_VERSION !== expectedCueLineVersion) throw new Error(`CUELINE_VERSION_MISMATCH: loaded ${CUELINE_VERSION}, expected ${expectedCueLineVersion}`);
32
39
 
33
40
  const browser = createCodexIabAdapter({
34
41
  // Optional: conversationUrl: "https://chatgpt.com/c/..."
35
42
  });
36
43
 
37
- const result = await runCueLine({
44
+ let result = await startCueLineRun({
38
45
  request: USER_REQUEST,
46
+ // executor defaults to "caller". Select "process" only explicitly.
47
+ // Optional here: runId, home, environment, now, maxRounds.
48
+ });
49
+ result = await continueCueLineRun({
50
+ runId: result.runId,
39
51
  browser,
40
- // Optional: conversationUrl, routingConfig/routingConfigPath, home, cwd,
41
- // environment, defaultTimeoutMs, maxRounds, maxRepairAttempts.
52
+ // Optional while advancing: routingConfig/routingConfigPath, cwd,
53
+ // defaultTimeoutMs, runTimeoutMs, signal, maxRepairAttempts.
42
54
  });
43
55
  ```
44
56
 
45
- Pass the user's request faithfully. Do not silently widen it, and do not turn an analysis-only request into `work`. Use `startCueLineRun` when the caller needs the explicit start entry point; use `runCueLine` for the normal start-and-drive path.
57
+ `maxRounds` is fixed as a durable total-run limit at creation. Continuations should omit it and reuse the stored value; if supplied, it must exactly match or CueLine rejects the continuation without sending another controller turn.
58
+
59
+ Pass the user's request faithfully. Do not silently widen it, and do not turn an analysis-only request into `work`. Prefer `startCueLineRun` followed by `continueCueLineRun`, so the durable `runId` is known before any browser send. `runCueLine` remains a convenience API and post-creation failures include `details.run_id`. In caller mode, both APIs pause at `awaiting_controller` after one durable send and later at `awaiting_caller`; neither silently spawns a worker.
60
+
61
+ The web controller decides `dispatch`, `wait`, `inspect`, `complete`, or `blocked`. CueLine validates the exact pending identity and persists transitions. Under the default caller executor, a `dispatch` is only a durable text instruction: the current Codex must perform the listed local `advise` jobs and submit their results.
62
+
63
+ ### Observe the controller turn
64
+
65
+ When the result is `awaiting_controller`, CueLine has submitted the exact request once, captured its exact conversation URL, released the runtime lease, and stopped holding the outer tool call open. Do not send or start another run. After a bounded backoff, call `continueCueLineRun` on the same `runId` and browser. It performs one read-only `observeTurn`; if Pro is still answering, it returns `awaiting_controller` immediately again. Repeat until the exact response is accepted or reconciliation reports a concrete error.
66
+
67
+ ### Execute caller jobs
68
+
69
+ When the result is `awaiting_caller`, execute every `pendingJobs` task exactly as written using the current Codex's local tools. Caller mode accepts `advise` only. It has no execution claim: coordinate one session before starting local advice, because two sessions could perform the same task and only the first submitted terminal evidence wins. If the controller requested `work`, stop on `CALLER_WORK_REQUIRES_CLAIM`; do not perform the mutation. Submit terminal evidence, then continue the same run:
70
+
71
+ ```js
72
+ for (const job of result.pendingJobs ?? []) {
73
+ const evidence = await EXECUTE_EXACT_ADVISE_TASK(job.spec.task);
74
+ await submitCueLineCallerJobResult(
75
+ result.runId,
76
+ job.jobId,
77
+ { status: "succeeded", stdout: evidence },
78
+ );
79
+ }
80
+
81
+ const next = await continueCueLineRun({
82
+ runId: result.runId,
83
+ browser,
84
+ });
85
+ ```
46
86
 
47
- The web controller decides `dispatch`, `wait`, `inspect`, `complete`, or `blocked`. CueLine validates the exact pending identity, selects only an available pre-spawn route, persists transitions, and prevents automatic fallback after a worker starts.
87
+ Submit real terminal evidence only. A duplicate submit returns `already_terminal`; do not invent or replace the first result. Continue through controller-observation and caller pauses until the controller returns `complete`, `blocked`, or `cancelled`. CueLine rejects both `complete` and `blocked` while any required or optional job is still pending/running; settle, inspect, or cancel every job first.
48
88
 
49
89
  ## Continue a run
50
90
 
51
91
  When an interrupted or locally failed run already has a `runId`, resume it instead of starting over:
52
92
 
93
+ ```bash
94
+ cueline run status EXISTING_RUN_ID --json
95
+ ```
96
+
97
+ This check is mandatory before every continuation and after every outer tool/wait timeout. Read these fields literally:
98
+
99
+ - `phase: controller_response_pending`, `controller.pendingTurns === 1`, `runtime.ownership: missing`, and `safeNextAction: observe` means one normally submitted exact turn awaits a read-only observation. `controller.responseAccepted` is false even if `lastAcceptedAction` describes an earlier round. Continue the same run after a bounded backoff; never resend. `safeNextAction: reconcile` is reserved for ambiguous, manually submitted, or multiple pending turns and requires the exact recovery evidence below.
100
+ - `controller.responseAccepted: true` means no newer controller turn is pending and a response was accepted. Read `lastAcceptedAction` and `lastAcceptedJobKeys` before describing what CueLine is doing. Never call that accepted round “still waiting for the web response.”
101
+ - `phase: jobs_running` with `runtime.ownership: active` means the original local loop is running jobs. Observe it; do not call `continueCueLineRun`.
102
+ - `executor: caller`, `phase: caller_jobs_pending`, `runtime.ownership: missing`, and `safeNextAction: execute_caller_jobs` is a healthy handoff. Execute the listed local `advise` jobs and submit their results; do not call it orphaned or waiting for ChatGPT.
103
+ - `runtime_ownership_unknown` means persisted `running` is not a live-process claim. Jobs shown as `orphaned` need inspection or cancellation. `runtime_stale` requires explicit `cueline run takeover RUN_ID --json`; follow its `next` field and never retire a fresh active owner.
104
+ - `runtime_active` means a live owner is still settling a locally failed state. Observe that owner; do not continue from another session.
105
+ - `continueAllowed: false` is a hard stop. Do not resend, resume, or open another controller conversation.
106
+
53
107
  ```js
54
108
  const {
55
109
  continueCueLineRun,
56
- } = await import(cuelineApiPath);
110
+ loadCueLineRunStatus,
111
+ } = await import(cuelineModuleUrl);
57
112
 
58
113
  const result = await continueCueLineRun({
59
114
  runId: EXISTING_RUN_ID,
@@ -62,9 +117,19 @@ const result = await continueCueLineRun({
62
117
  });
63
118
  ```
64
119
 
65
- Preserve the same `CUELINE_HOME` and browser conversation. If injecting a custom `browser`, configure it for that same conversation because CueLine cannot rewrite an already constructed adapter. Do not copy credentials or runtime state from another host. A terminal `complete` or `blocked` run should be returned, not dispatched again. Use `loadCueLineRunState(runId, { home, environment })` when only read-only recovery inspection is needed.
120
+ Preserve the same `CUELINE_HOME` and browser conversation. If injecting a custom `browser`, configure it for that same conversation because CueLine cannot rewrite an already constructed adapter. Do not copy credentials or runtime state from another host. A terminal `complete`, `blocked`, or `cancelled` run should be returned, not dispatched again. Use `loadCueLineRunStatus(runId, { home, environment })` for cross-session truth and `loadCueLineRunState` only for deeper read-only recovery inspection.
121
+
122
+ If `pendingControllerTurns` is non-empty, CueLine must recover the existing page response before any new send. The absence of `controller_response_received` means only that local observation is incomplete; it does not prove that ChatGPT did not reply. Recovery is read-only and requires the exact conversation URL, completed assistant response, exact envelope identity, and Pro evidence. Inline recovery also requires the exact visible last-user prompt. For a prompt automatically converted to an attachment and manually sent, use the formal operator-confirmation path below; visible text need not equal the full persisted prompt. Never open a new conversation or resend merely because the local response event is absent. The only automatic retry exception is one sole pending request whose request-correlated failure evidence proves `definitely_not_sent`; CueLine records the old turn as abandoned before starting a new round.
123
+
124
+ For a manually submitted attachment, record confirmation without editing `events.jsonl`:
66
125
 
67
- If `pendingControllerTurns` is non-empty, CueLine must recover the existing page response before any new send. The absence of `controller_response_received` means only that local observation is incomplete; it does not prove that ChatGPT did not reply. Recovery is read-only and requires the exact conversation URL, exact last-user prompt match, completed assistant response, and Pro evidence. Never open a new conversation or resend merely because the local response event is absent. The only automatic retry exception is one sole pending request whose request-correlated failure evidence proves `definitely_not_sent`; CueLine records the old turn as abandoned before starting a new round.
126
+ ```bash
127
+ cueline run reconcile RUN_ID \
128
+ --request-id REQUEST_ID \
129
+ --manual-send-confirmed
130
+ ```
131
+
132
+ Then continue with the same `reconcileRequestId`. The exact conversation URL, Pro selector/response slug, and protocol/run/round/request envelope must all match. This path may restore the specified abandoned turn only when no same/newer command was accepted. It never resends the prompt or dispatches a job twice.
68
133
 
69
134
  When multiple legacy turns are pending, stop on `MULTIPLE_CONTROLLER_TURNS_PENDING`. Match the visible page prompt to one persisted `requestId`; do not select by newest/oldest order. Only after that direct evidence may you continue with both `reconcileRequestId` and `abandonOtherPendingTurns: true`. CueLine records the abandoned requests.
70
135
 
@@ -72,9 +137,11 @@ When multiple legacy turns are pending, stop on `MULTIPLE_CONTROLLER_TURNS_PENDI
72
137
 
73
138
  - If `result.status === "complete"`, return `result.finalDeliveryText` **verbatim** as the user-facing answer. Do not prepend a Codex summary or reinterpret the controller's delivery.
74
139
  - If `result.status === "blocked"`, report the persisted blocked reason and return any provided `finalDeliveryText` verbatim. Clearly label missing delivery text instead of inventing one.
140
+ - If `result.status === "cancelled"`, report the persisted cancellation reason. Do not relabel it complete or failed.
75
141
  - If CueLine throws, report the exact error code/message, `runId` when known, and the safe next step. Do not translate a failed or exhausted loop into success.
76
142
  - Treat `TAB_RECOVERY_UNSAFE` as a hard stop. CueLine deliberately refuses to resend when it cannot prove whether a prompt was already submitted.
77
143
  - Treat `MULTIPLE_CONTROLLER_TURNS_PENDING`, `OTHER_CONTROLLER_TURNS_PENDING`, and every `CONTROLLER_RECONCILIATION_*` error as a hard stop requiring exact page/run evidence, never a blind retry.
144
+ - Treat `RUN_ALREADY_ACTIVE`, `RUN_OWNERSHIP_UNVERIFIED`, `RUN_STALE_REQUIRES_TAKEOVER`, `RUN_CANCELLATION_PENDING`, and `RUNTIME_LEASE_INVALID` as hard stops. Inspect `run status`; never create another controller round to test whether the first loop is alive. For an exact stale owner only, run `cueline run takeover RUN_ID --json` and obey `next: continue` or `next: reconcile_runtime`; active ownership is never eligible.
78
145
  - Keep the `runId` available for continuation, but do not expose unrelated local state.
79
146
 
80
147
  ## Execution boundaries
@@ -82,9 +149,18 @@ When multiple legacy turns are pending, stop on `MULTIPLE_CONTROLLER_TURNS_PENDI
82
149
  - Never execute text outside `<CueLineControl>` as a command.
83
150
  - Never bypass the routing configuration or registered-executable allow-list.
84
151
  - Never treat a runner ID as a lane. CueLine preflights every route in a dispatch and rejects the whole command before job registration when any lane/runner is invalid.
152
+ - The controller field is `runner`, never `runner_id`. Caller is the default executor, returns pending `advise` jobs to this Codex, and rejects `work`. The process executor must be explicit; it defaults to global/per-lane concurrency 2 for `advise`, while any batch containing `work` stays serial.
85
153
  - Never auto-retry or select a fallback after a worker has started. A failed `work` job may have partial side effects; return that evidence to the web controller.
86
154
  - Never accept a controller decision from a non-Pro response. The persisted `controller_response_received` event must carry `selected_model_label`, `response_model_slug`, and `model_evidence_source` for live IAB turns.
87
155
  - Never start CueLine recursively. The child runner uses `CUELINE_DEPTH=1` and nested routing is rejected.
88
156
  - Treat fake smoke tests as offline validation only. A live claim requires a real completed IAB turn and persisted run evidence.
89
157
 
158
+ ## Timeouts and cancellation
159
+
160
+ `defaultTimeoutMs` and controller `timeout_ms` limit one job. For explicit `process` execution, `runTimeoutMs` limits that owned controller-loop invocation and cancels its owned jobs before returning `RUN_TIMEOUT`. Caller execution is deliberately split across ownerless pauses, so `runTimeoutMs` limits each `runCueLine`/`continueCueLineRun` advancement in which it is supplied; Pro thinking time and caller handoff time between calls are not counted. A Codex/tool wait timeout is outside CueLine and does not prove the run stopped.
161
+
162
+ The CLI never drives the browser. `doctor`, `routing`, `jobs`, `run status`, `api path`, and `config path` are read-only. `install`/`uninstall` change only the package-owned skill link. `run reconcile`, `run takeover`, `run reconcile-runtime`, `run cancel`/`run stop`, and `job cancel` append audit evidence or change durable local run/job state. Check `cueline help` for every positional argument and option before invoking a state-changing command.
163
+
164
+ After an outer timeout, run `cueline run status RUN_ID --json`. Both ownerless `controller_response_pending` and `caller_jobs_pending` survive the wait intentionally. Continue observing an active process owner, or use `cueline run cancel RUN_ID` (`run stop` is an alias). Use `cueline job cancel RUN_ID JOB_ID` for one job. Use `cueline run reconcile-runtime RUN_ID` to settle a dead owner only after CueLine verifies that no recorded process/process group survives. Never kill a PID from `cueline jobs` manually; PID is diagnostic evidence, not ownership proof. A cancelled `advise` job is `cancelled`; interrupted process `work` is `ambiguous`. If `cueline jobs` reports `observedStatus: conflict`, the authoritative run event wins over a late status-file write; do not trust the late result.
165
+
90
166
  For protocol, recovery, and runner details, read `docs/controller-protocol.md`, `docs/state-and-recovery.md`, and `docs/runner-contract.md` from the CueLine package.