Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/SURFACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,35 @@ Workspace surfaces are a different thing and are not a directory selector:
`surfaces.workspace` names the revisions a step pins and writes back to, not a
path on the host running the CLI.

### Continuing an agent's CLI session (`resume`)

`f.agent` reports the CLI session it ran as in `AgentResult.sessionId`
(Claude's session id, Codex's thread id). Pass it as a later step's `resume`,
or the `resume:` key on a declarative agent step, to continue that conversation
instead of starting a new one:

```ts
const first = await f.agent("fix", { task: "Address the review feedback." });
// ...later, in a run on the same host...
await f.agent("fix-again", { task: "Address the new feedback.", resume: first.sessionId });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: first.sessionId is optional, so this call silently starts a fresh session when the CLI reported no id. Guard the value and fail or skip the follow-up rather than presenting it as resumed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/SURFACE.md, line 488:

<comment>`first.sessionId` is optional, so this call silently starts a fresh session when the CLI reported no id. Guard the value and fail or skip the follow-up rather than presenting it as resumed.</comment>

<file context>
@@ -475,6 +475,35 @@ Workspace surfaces are a different thing and are not a directory selector:
+```ts
+const first = await f.agent("fix", { task: "Address the review feedback." });
+// ...later, in a run on the same host...
+await f.agent("fix-again", { task: "Address the new feedback.", resume: first.sessionId });
+```
+
</file context>
Suggested change
await f.agent("fix-again", { task: "Address the new feedback.", resume: first.sessionId });
if (!first.sessionId) throw new Error("The agent did not report a session id.");
await f.agent("fix-again", { task: "Address the new feedback.", resume: first.sessionId });

```

- Claude runs `claude -p --resume <id>`; Codex runs `codex exec resume <id>`.
Any other CLI refuses the step rather than silently starting cold.
- The CLI finds the session in its own store (`~/.claude`, `~/.codex`), so a
resume works only where that store survives — the same machine, or a
sandbox reused for the same pull request. A session the store no longer has
fails the step.
- The id is checked lexically, identically by `flows check` and the kernel:
1-200 of `A-Z a-z 0-9 . _ : -`, starting with a letter or digit, so it can
never be read as a CLI flag. `resume` is not supported with
`transport: "relay"`.
- `sessionId` is read from the attempt's journaled transcript evidence, so it
is reported whether the agent answered in text or with a JSON object. It is
omitted when the CLI reported none.
- Setting `resume` is part of the step's spec hash; omitting it hashes exactly
as before.

### Supported TypeScript LLM calls

The local authored executor supports these signatures:
Expand Down
12 changes: 12 additions & 0 deletions evidence/agent-resume/01-red.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# packages/sdk/src and packages/surface/src at origin/main (22cd1836) via git stash; tests and testdata from this branch.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Capture the source-reversion command in the red transcript

The transcript claims the product sources were reverted to 22cd1836 “via git stash,” but it captures only the subsequent test invocation; there is no literal stash/checkout command or output proving which files were reverted and restored. This makes the claimed red baseline unreproducible under the repository's evidence rules.

AGENTS.md reference: AGENTS.md:L98-L100

Useful? React with 👍 / 👎.

$ (cd packages/sdk && npx vitest run tests/agent-resume.test.ts tests/spec-parity.test.ts)
× spec parity: one dialect at the SDK<->kernel boundary > compiles agent-resume to the pinned canonical JSON 5ms
× spec parity: one dialect at the SDK<->kernel boundary > hashes agent-resume to the pinned spec_hash 1ms
× spec parity: one dialect at the SDK<->kernel boundary > round-trips agent-resume across the kernel dialect 1ms
Error: Failed to load url ../src/agent-resume.js (resolved id: ../src/agent-resume.js) in /home/khaliqgant/Projects/AgentWorkforce/flows-worktrees/agent-resume/packages/sdk/tests/agent-resume.test.ts. Does the file exist?
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯
CompileError: spec compile failed:
CompileError: spec compile failed:
CompileError: spec compile failed:
Test Files 2 failed (2)
Tests 3 failed | 46 passed (49)
13 changes: 13 additions & 0 deletions evidence/agent-resume/02-sdk-typecheck-build.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
$ (cd packages/sdk && npm run typecheck && npm run build && npm run typecheck:tests)
$ npm run typecheck
> @relayflows/sdk@2.0.42 typecheck
> tsc --noEmit && tsc -p tsconfig.type-tests.json

exit=0
$ npm run build
> tsc && node scripts/make-cli-executable.mjs

$ npm run typecheck:tests

> @relayflows/sdk@2.0.42 typecheck:tests
> tsc -p tsconfig.tests.json
15 changes: 15 additions & 0 deletions evidence/agent-resume/03-sdk-vitest.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Supersedes the earlier capture, whose output filter (Test Files|Tests|×) hid a collection-time FAIL: the verb-field lint, fixed in aa115352.
# RELAYFLOWD_BIN points at this branch's kernel build (kernel/target/debug/relayflowd); the ambient value named a missing toolchain binary.
$ (cd packages/sdk && RELAYFLOWD_BIN=$PWD/../../kernel/target/debug/relayflowd npx vitest run) | grep -E "FAIL|Test Files|Tests | × "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This pipeline exits with grep's status, masking Vitest's nonzero exit when tests fail. Enable pipefail so the recorded test command preserves the test result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At evidence/agent-resume/03-sdk-vitest.txt, line 3:

<comment>This pipeline exits with `grep`'s status, masking Vitest's nonzero exit when tests fail. Enable `pipefail` so the recorded test command preserves the test result.</comment>

<file context>
@@ -1,17 +1,15 @@
-⎯⎯⎯⎯⎯⎯⎯ Failed Tests 7 ⎯⎯⎯⎯⎯⎯⎯
- Test Files  6 failed | 267 passed | 1 skipped (274)
-      Tests  7 failed | 4166 passed | 20 skipped (4193)
+$ (cd packages/sdk && RELAYFLOWD_BIN=$PWD/../../kernel/target/debug/relayflowd npx vitest run) | grep -E "FAIL|Test Files|Tests  | × "
+   ✓ built flows CLI against live relayflowd > hn-monitor analyze-story FAILS verification when the CLI omits required schema fields 340ms
+ FAIL  tests/authored-node-runtime.test.ts [ tests/authored-node-runtime.test.ts ]
</file context>
Suggested change
$ (cd packages/sdk && RELAYFLOWD_BIN=$PWD/../../kernel/target/debug/relayflowd npx vitest run) | grep -E "FAIL|Test Files|Tests | × "
$ set -o pipefail; (cd packages/sdk && RELAYFLOWD_BIN=$PWD/../../kernel/target/debug/relayflowd npx vitest run) | grep -E "FAIL|Test Files|Tests | × "

✓ built flows CLI against live relayflowd > hn-monitor analyze-story FAILS verification when the CLI omits required schema fields 340ms
FAIL tests/authored-node-runtime.test.ts [ tests/authored-node-runtime.test.ts ]
Test Files 1 failed | 272 passed | 1 skipped (274)
Tests 4278 passed | 20 skipped (4298)

# The one FAIL is environmental: the test pins bun 1.4.0 and this host has 1.4.2.
FAIL tests/authored-node-runtime.test.ts [ tests/authored-node-runtime.test.ts ]
AssertionError: expected '1.4.2' to be '1.4.0' // Object.is equality

Expected: "1.4.0"
Received: "1.4.2"

15 changes: 15 additions & 0 deletions evidence/agent-resume/04-green.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
$ (cd packages/sdk && npx vitest run tests/agent-resume.test.ts tests/spec-parity.test.ts)
Test Files 2 passed (2)
Tests 75 passed (75)
$ (cd kernel && cargo test -p relayflowd-core)
test result: ok. 86 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.71s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ node scripts/generate-json-schema.mjs && git diff --stat -- packages/schema
Generated packages/schema/flows.schema.json (74 definitions)
packages/schema/flows.schema.json | 10 ++++++++++
1 file changed, 10 insertions(+)
$ (cd packages/schema && bun run test)
86 pass
0 fail
10 changes: 10 additions & 0 deletions evidence/agent-resume/05-real-cli-resume.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Missing session, real Claude CLI (2.x on this host):
$ claude -p --resume 00000000-0000-4000-8000-0000000000aa "hi" --output-format json
No conversation found with session ID: 00000000-0000-4000-8000-0000000000aa
exit=1

# Resume from a different directory than the session was created in:
$ (cd A && claude -p "Remember the code word PELICAN-42. Reply only with OK." --output-format json)
A session: 1908508f-989e-4159-82b0-6a676c3d74d3 result: OK
$ (cd B && claude -p --resume 1908508f-989e-4159-82b0-6a676c3d74d3 "What was the code word? Reply with just it." --output-format json)
B session: 1908508f-989e-4159-82b0-6a676c3d74d3 result: 'PELICAN-42' error: False
57 changes: 46 additions & 11 deletions kernel/relayflowd-core/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,13 @@ impl RunSpec {
step.id
)));
}
if let StepKind::Agent { surfaces, cwd, .. } = &step.kind {
if let StepKind::Agent {
surfaces,
cwd,
resume,
..
} = &step.kind
{
if let Some(cwd) = cwd
&& !is_run_root_relative_path(cwd)
{
Expand All @@ -189,6 +195,14 @@ impl RunSpec {
cwd: cwd.clone(),
});
}
if let Some(resume) = resume
&& !is_cli_session_id(resume)
{
return Err(SpecError::InvalidAgentResume {
step: step.id.clone(),
resume: resume.clone(),
});
}
for workspace in &surfaces.workspace {
if path_surface_identity(&workspace.surface).is_none() {
return Err(SpecError::InvalidWorkspaceSurface {
Expand Down Expand Up @@ -311,6 +325,17 @@ fn is_run_root_relative_path(path: &str) -> bool {
if namespace.is_empty() && !components.is_empty())
}

/// Whether a declared agent `resume` is a CLI session id: 1-200 of
/// `A-Z a-z 0-9 . _ : -`, starting with a letter or digit, so it can never be
/// read as a CLI flag. The SDK's `agent-resume.ts` applies the same rule, and
/// `testdata/agent-resume-cases.json` is the corpus both are tested against.
pub fn is_cli_session_id(id: &str) -> bool {
let mut chars = id.chars();
chars.next().is_some_and(|c| c.is_ascii_alphanumeric())
&& id.len() <= 200
&& chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | ':' | '-'))
}

pub fn is_canonical_external_surface(path: &str) -> bool {
path_surface_identity(path).is_some()
}
Expand Down Expand Up @@ -363,6 +388,7 @@ const STEP_AGENT_FIELDS: &[&str] = &[
"cli_identity",
"model",
"cwd",
"resume",
"transport",
"recovery_mode",
"surfaces",
Expand All @@ -386,16 +412,15 @@ fn reject_unknown_step_fields(value: &Value) -> Result<(), SpecError> {
// default directory under a spec that declared otherwise. The
// shape is checked before serde so a null, a number or an
// object is refused instead of silently defaulted.
if let Some(cwd) = object.get("cwd")
&& !cwd.is_string()
{
return Err(SpecError::Malformed(format!(
"step {}: cwd must be a string",
object
.get("id")
.and_then(Value::as_str)
.unwrap_or("?")
)));
for field in ["cwd", "resume"] {
if let Some(value) = object.get(field)
&& !value.is_string()
{
return Err(SpecError::Malformed(format!(
"step {}: {field} must be a string",
object.get("id").and_then(Value::as_str).unwrap_or("?")
)));
}
}
STEP_AGENT_FIELDS
}
Expand Down Expand Up @@ -538,6 +563,12 @@ pub enum StepKind {
/// filesystem. Absent means the run root itself.
#[serde(default, skip_serializing_if = "Option::is_none")]
cwd: Option<String>,
/// A CLI session the attached worker continues instead of starting a
/// new one (an earlier step's reported session id). The kernel checks
/// its shape and carries it verbatim; resuming is the worker's, which
/// shares the CLI's session store.
#[serde(default, skip_serializing_if = "Option::is_none")]
resume: Option<String>,
/// How the attached worker invokes the declared CLI. The kernel does
/// not implement either transport; it journals and dispatches the
/// choice so the worker can honor it deterministically.
Expand Down Expand Up @@ -874,6 +905,10 @@ pub enum SpecError {
"agent step {step} declares working directory {cwd:?}, which is not a run-root-relative path (no absolute paths, empty components, \".\" or \"..\")"
)]
InvalidAgentCwd { step: String, cwd: String },
#[error(
"agent step {step} declares resume {resume:?}, which is not a CLI session id (1-200 of A-Z a-z 0-9 . _ : -, starting with a letter or digit)"
)]
InvalidAgentResume { step: String, resume: String },
#[error("agent step {step} declares non-canonical external surface {path:?}")]
InvalidExternalSurface { step: String, path: String },
#[error("agent step {step} declares non-canonical workspace surface {surface:?}")]
Expand Down
58 changes: 58 additions & 0 deletions kernel/relayflowd-core/tests/spec_parity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,61 @@ fn agent_timeout_bounds_match_the_kernel_corpus() {
let parsed = RunSpec::parse(&value).unwrap();
assert!(serde_json::to_value(parsed).unwrap()["steps"][0].get("timeout_ms").is_none());
}

/// `resume` (continue a recorded CLI session) crosses the boundary exactly as
/// `cwd` does: declared on one agent step and not on another, with identical
/// canonical bytes and hash on both sides.
#[test]
fn agent_resume_has_identical_canonical_bytes_and_hash() {
assert_parity(
include_str!("../../../testdata/agent-resume.spec.canonical.json"),
include_str!("../../../testdata/agent-resume.spec.sha256"),
);
}

#[test]
fn agent_resume_declaration_acceptance_matches_the_sdk_corpus() {
let cases: Vec<Value> =
serde_json::from_str(include_str!("../../../testdata/agent-resume-cases.json")).unwrap();
for case in cases {
let spec = serde_json::json!({"steps":[{
"id":"s","type":"agent","instruction":"work","resume":case["resume"],
}]});
let accepted = RunSpec::parse(&spec)
.and_then(|spec| spec.validate())
.is_ok();
assert_eq!(
accepted,
case["valid"].as_bool().unwrap(),
"{}",
case["name"]
);
}
}

/// An undeclared `resume` is absent from the re-serialized spec, so every
/// fixture committed before the field existed keeps its bytes and hash; a
/// non-string spelling fails closed instead of reading as absence.
#[test]
fn agent_resume_is_absent_when_undeclared_and_must_be_a_string() {
let value =
serde_json::json!({"steps": [{"id": "agent", "type": "agent", "instruction": "work"}]});
let parsed = RunSpec::parse(&value).expect("an agent step without resume must parse");
parsed.validate().expect("and must validate");
assert!(
serde_json::to_value(parsed).unwrap()["steps"][0]
.get("resume")
.is_none()
);
for resume in [
serde_json::json!(null),
serde_json::json!(7),
serde_json::json!({}),
] {
let spec = serde_json::json!({"steps": [{"id": "agent", "type": "agent", "instruction": "work", "resume": resume}]});
assert!(
RunSpec::parse(&spec).is_err(),
"resume {resume} must be refused"
);
}
}
10 changes: 10 additions & 0 deletions packages/schema/flows.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,11 @@
"description": "Directory the declared CLI is spawned in, as a path relative to the run\nroot — the flow-runner's working directory, which is also where the CLI\nruns when this is absent. Absolute paths, `.`, `..` and empty components\nare refused lexically by `flows check` and by the kernel; the worker that\nspawns the CLI additionally requires the symlink-free directory to exist\ninside the symlink-free run root. A declaration, not a sandbox: nothing\nstops a CLI from writing outside it. Not supported with\n`transport: 'relay'`, where the agent runs on a host this process cannot\nresolve. See docs/SURFACE.md.",
"type": "string"
},
"resume": {
"title": "resume",
"description": "A CLI session id to continue: the `AgentResult.sessionId` an earlier step\nreported. Claude and Codex resume it in place of starting a new session;\nany other CLI refuses the step. 1-200 of A-Z a-z 0-9 . _ : -, starting\nwith a letter or digit. Not supported with `transport: 'relay'`, where the\nagent runs on a host whose session store this worker cannot see.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This schema accepts any string even though its description specifies a 1–200-character session ID, so schema-based editors accept values that flows check rejects. Add the length and character constraints to the generated schema.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/schema/flows.schema.json, line 1458:

<comment>This schema accepts any string even though its description specifies a 1–200-character session ID, so schema-based editors accept values that `flows check` rejects. Add the length and character constraints to the generated schema.</comment>

<file context>
@@ -1453,6 +1453,11 @@
         },
+        "resume": {
+          "title": "resume",
+          "description": "A CLI session id to continue: the `AgentResult.sessionId` an earlier step\nreported. Claude and Codex resume it in place of starting a new session;\nany other CLI refuses the step. 1-200 of A-Z a-z 0-9 . _ : -, starting\nwith a letter or digit. Not supported with `transport: 'relay'`, where the\nagent runs on a host whose session store this worker cannot see.",
+          "type": "string"
+        },
</file context>

"type": "string"
},
"transport": {
"title": "transport",
"description": "Dispatch transport (flows#385). `'direct'` (default) spawns the CLI as\na local subprocess. `'relay'` posts to agent-relay so the agent\nregisters as a first-class workspace participant.",
Expand Down Expand Up @@ -2484,6 +2489,11 @@
"description": "Run-root-relative directory the attached worker spawns the CLI in. The\nkernel checks the shape and does no I/O: existence and containment are\ndecided by the worker, on the host that shares the agent's filesystem.",
"type": "string"
},
"resume": {
"title": "resume",
"description": "CLI session id the worker resumes; the kernel checks its shape only.",
"type": "string"
},
"transport": {
"title": "transport",
"description": "Dispatch transport (flows#385). Kernel passes through untouched;\nthe SDK worker selects between direct-spawn and agent-relay routing.",
Expand Down
5 changes: 4 additions & 1 deletion packages/sdk/src/adapters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ export interface HeadlessAdapter {
classifyModelProbeFailure?(output: string): ModelProbeFailure | undefined;

/** Agent-step worker argv for an instruction under an optional model. */
buildAgentInvocation(instruction: string, model?: string): CliInvocation;
buildAgentInvocation(instruction: string, model?: string, resume?: string): CliInvocation;

/** Whether `buildAgentInvocation` honors `resume`; a step asking another CLI to resume is refused. */
readonly resumable?: true;

/** LLM-step worker argv for a prompt under an optional model. */
buildLlmInvocation(prompt: string, model?: string): CliInvocation;
Expand Down
5 changes: 4 additions & 1 deletion packages/sdk/src/adapters/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const MODEL_PROBE_PROMPT = 'Reply with exactly RELAYFLOWS_MODEL_READY and nothin
export const claudeAdapter: HeadlessAdapter = {
kind: 'claude',
defaultModel: 'claude-opus-5',
resumable: true,

buildIdentification(): CliAdapterIdentification {
return { invocation: { args: ['auth', 'status', '--help'], timeoutMs: 10_000 } };
Expand Down Expand Up @@ -46,10 +47,12 @@ export const claudeAdapter: HeadlessAdapter = {
return undefined;
},

buildAgentInvocation(instruction: string, model?: string): CliInvocation {
buildAgentInvocation(instruction: string, model?: string, resume?: string): CliInvocation {
return {
args: [
'-p',
// Continues the recorded session (it is found by id, from any cwd).
...(resume === undefined ? [] : ['--resume', resume]),
// Symmetric to codex's --dangerously-bypass-approvals-and-sandbox: in
// agent mode the flow explicitly delegates writes. Without this
// claude's headless mode prompts for tool approval, gets no TTY,
Expand Down
7 changes: 5 additions & 2 deletions packages/sdk/src/adapters/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const MODEL_PROBE_PROMPT = 'Reply with exactly RELAYFLOWS_MODEL_READY and nothin
* inline shape in `cli-adapter.ts` — only the packaging changed. */
export const codexAdapter: HeadlessAdapter = {
kind: 'codex',
resumable: true,

buildIdentification(): CliAdapterIdentification {
return { invocation: { args: ['login', 'status', '--help'], timeoutMs: 10_000 } };
Expand All @@ -36,22 +37,24 @@ export const codexAdapter: HeadlessAdapter = {
return providerUsageLimited(output) ? { cause: 'provider_usage_limited' } : undefined;
},

buildAgentInvocation(instruction: string, model?: string): CliInvocation {
buildAgentInvocation(instruction: string, model?: string, resume?: string): CliInvocation {
return {
args: [
// Real steps must retain Codex's native session under CODEX_HOME (or
// ~/.codex). Hosted Relayflow teardown captures that provider history
// and uploads it to Relayhistory; --ephemeral silently leaves nothing
// for the bounded flush to capture. Readiness probes above stay
// ephemeral because they are diagnostics, not workflow steps.
'exec', '--skip-git-repo-check',
// `exec resume <id>` continues a recorded thread with the same flags.
...(resume === undefined ? ['exec'] : ['exec', 'resume']), '--skip-git-repo-check',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Codex resume stays scoped to the current directory, so it cannot find a session created under another step's cwd. Add --all to the resume branch to search across directories.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/sdk/src/adapters/codex.ts, line 49:

<comment>Codex resume stays scoped to the current directory, so it cannot find a session created under another step's `cwd`. Add `--all` to the resume branch to search across directories.</comment>

<file context>
@@ -36,22 +37,24 @@ export const codexAdapter: HeadlessAdapter = {
         // ephemeral because they are diagnostics, not workflow steps.
-        'exec', '--skip-git-repo-check',
+        // `exec resume <id>` continues a recorded thread with the same flags.
+        ...(resume === undefined ? ['exec'] : ['exec', 'resume']), '--skip-git-repo-check',
         // Agent-mode is where the flow explicitly delegates code changes to
         // the CLI. Without this flag codex prompts for approval on every
</file context>
Suggested change
...(resume === undefined ? ['exec'] : ['exec', 'resume']), '--skip-git-repo-check',
...(resume === undefined ? ['exec'] : ['exec', 'resume', '--all']), '--skip-git-repo-check',

// Agent-mode is where the flow explicitly delegates code changes to
// the CLI. Without this flag codex prompts for approval on every
// write, gets nothing (no TTY), and completes "successfully" without
// touching files — the dogfood no-op failure mode. LLM-mode stays
// read-only and does NOT get the bypass.
'--dangerously-bypass-approvals-and-sandbox',
...(model === undefined ? [] : ['--model', model]),
...(resume === undefined ? [] : [resume]),
...promptOperand(instruction),
],
timeoutMs: 0,
Expand Down
Loading
Loading