Skip to content
This repository was archived by the owner on Sep 28, 2026. It is now read-only.
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable product releases are documented here.

## Unreleased

- fix: choose task environment per node (local for read-only, worktree only for writers) and strengthen pending `clientThreadId` resolution (Lisbon dogfood v4)

## [0.4.4] - 2026-08-09

- fix: collect handoffs when workers finish before wait sees them (#13)
Expand Down
6 changes: 3 additions & 3 deletions skills/codex-graph/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ Code Mode tool availability and names can vary by client, version, configuration
5. Derive arguments from the exposed declaration. Account for version differences such as `task_name`, `fork_turns`, or `fork_context` only when those fields are actually supported.
6. If required agent tools are unavailable, the script must emit a structured `blocked` result listing the missing capabilities and exit successfully without attempting substitute mutations.
7. Outside the script, include a direct-subagent fallback that preserves the same graph for clients without Code Mode. The fallback is not permission to omit the script.
8. When the goal belongs to a saved Codex project, resolve its exact path through the project-list tool before task creation. Use the saved project with a local environment for a strictly read-only graph and isolated worktrees for any graph that can write. Use a projectless target only when no saved project applies or the user explicitly requests it.
8. When the goal belongs to a saved Codex project, resolve its exact path through the project-list tool before task creation. Choose the task environment **per node**: read-only nodes use a local environment; nodes that write repository files use isolated worktrees. Never apply worktree to every node because one writer exists. Prefer root-orchestrator writes for final project-root artifacts. Use a projectless target only when no saved project applies or the user explicitly requests it.

### Graph-to-script parity

Expand Down Expand Up @@ -238,7 +238,7 @@ Every Mermaid node must map to an executable JavaScript stage, explicit gate, or
5. Keep dependencies, adaptive decisions, approvals, waits, overlapping writes, synthesis, integration, and repair sequential.
6. Use the concurrency supported by the active tools and task. Add a cap or
staged fan-in only when a tool, runtime, or observed workload requires it.
7. Treat task creation as a lifecycle, not one response. Preserve the complete setup result, ready `threadId`, pending `clientThreadId`, `hostId`, exact project ID, unique run tag and title, and node ID. Resolve pending setup through bounded task-list polling. Match the exact project ID plus the unique run tag in `title`; while setup is loading, also allow the same exact tag in `summary`.
7. Treat task creation as a lifecycle, not one response. Preserve the complete setup result, ready `threadId`, pending `clientThreadId`, `hostId`, exact project ID, unique run tag and title, and node ID. Resolve pending setup through bounded task-list polling (chat-scale for local, provisioning-scale for worktree). Match the unique run tag in `title` or `summary`; prefer project ID when present but do not require it while setup is still loading; correlate by `clientThreadId` when the list exposes it.
8. Wait for declared completion before consuming a handoff. A wait timeout is
an observation point, not proof of failure. Use the active tool's wait
semantics; add a deadline or polling cap only when the operation could
Expand Down Expand Up @@ -465,7 +465,7 @@ Before returning the paired deliverable, verify all of the following:
- Required tools are discovered or bound from actual exposed metadata; no tool APIs are invented.
- Integration ownership, fail-closed behavior, and the one-repair rule are encoded in code; concurrency caps are added only when justified by active tools or observations.
- Validator decisions are machine-readable and malformed decisions fail closed.
- Saved-project graphs resolve and preserve the exact project ID; read-only tasks use local and writing tasks use worktrees.
- Saved-project graphs resolve and preserve the exact project ID; per-node environment: read-only tasks use local and writing tasks use worktrees (never global worktree for one writer).
- Self-testing freezes acceptance before the child starts, accepts only explicit child evidence, records observed roadmap items, and uses at most one root-cause repair and re-run.
- Pending setup handles are retained and resolved; wait timeouts are checked against fresh task state.
- Early-returning wait tools cannot consume the full collection budget in a tight loop.
Expand Down
18 changes: 16 additions & 2 deletions skills/codex-graph/references/code-mode-script-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ async function startRequiredNode(node, spawnTool, projectId) {
const handle = {
node_id: node.id,
project_id: projectId,
environment: node.environment,
run_tag: node.runTag,
title: node.title,
state: "pending_setup",
Expand Down Expand Up @@ -193,11 +194,24 @@ Do not create a worker that spawns another worker.

## Bind visible tasks to the correct project

When the script creates visible Codex tasks, read `task-lifecycle.md` and use its complete contract. Resolve the exact saved project before task creation. Use its local environment only when every task is read-only. Use isolated project worktrees when any task can write. Use a projectless target only when no saved project applies or the user explicitly requests it.
When the script creates visible Codex tasks, read `task-lifecycle.md` and use its complete contract. Resolve the exact saved project before task creation. Choose the task environment **per node**:

- Read-only nodes (research, discovery, audit) →
`environment: { type: "local" }`.
- Nodes that write repository files →
`environment: { type: "worktree" }` with disjoint write scopes.
- Never apply `worktree` to every node because one writer exists in the graph
(Lisbon dogfood v4: global worktree on research workers left all three in
`pending_setup` with unresolved `clientThreadId`).
- Write final user-facing project-root artifacts from the root orchestrator
(or a single integration owner on the real project), not only inside a
disposable worker worktree.

Use a projectless target only when no saved project applies or the user explicitly requests it.

## Preserve setup handles and terminal handoffs

A task-creation result can contain a ready `threadId` or only a pending `clientThreadId`. Both are successful setup states. Retain the complete result and resolve pending setup with bounded polling before calling wait or read tools. Match the unique run tag, and the exact project ID when one exists. The tag normally appears in `title`, but can appear in `summary` while project setup is loading. Retain `hostId` when present. See `task-lifecycle.md` for the state machine and code pattern.
A task-creation result can contain a ready `threadId` or only a pending `clientThreadId`. Both are successful setup states. Retain the complete result and resolve pending setup with bounded polling before calling wait or read tools. Match the unique run tag (prefer `title`, accept `summary` while loading). Prefer exact project ID when present on the list row, but do not require it during early setup loading. Correlate by `clientThreadId` when the list exposes it. Use a chat-scale bound for local starts and a longer provisioning-scale bound for worktree starts. Retain `hostId` when present. See `task-lifecycle.md` for the state machine and code pattern.

Do not reduce a task handle to one convenience ID while setup or execution is live. A blocked terminal report must include each node ID, ready or pending ID, project ID, host ID, title, state, and the exact start or collection error.

Expand Down
72 changes: 59 additions & 13 deletions skills/codex-graph/references/task-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,21 @@ Inspect the active declarations for project listing and task creation.

1. If the goal belongs to a saved Codex project, resolve the exact project path through the project-list tool.
2. Match the exact path. Fail closed on no match or more than one match.
3. Use `target: { type: "project", projectId, environment: { type: "local" } }` only when every node is strictly read-only.
4. Use the saved project with a worktree environment when any node can write. Give concurrent writers disjoint scopes.
3. Choose the task **environment per node**, not once for the whole graph:
- Read-only nodes (research, discovery, audit/read lenses) use
`target: { type: "project", projectId, environment: { type: "local" } }`.
- Nodes that write repository files use
`target: { type: "project", projectId, environment: { type: "worktree" } }`
with disjoint write scopes for concurrent writers.
- Never apply `worktree` globally because one writer exists. Observed
Lisbon dogfood v4: research workers created with worktree returned only
`clientThreadId`, never resolved within a chat-scale bound, and blocked
before collection while v3's `local` research workers got ready
`threadId`s immediately.
4. Prefer the root orchestrator (or a single integration owner on `local` /
the real project checkout) for final user-facing artifacts such as report
files in the project root. Do not strand publication writes only inside a
disposable worker worktree.
5. Use a projectless target only when no saved project applies or the user explicitly requests it.

Keep the resolved project ID in the workflow result. Do not use a generated directory as the identity of a saved-project task.
Expand Down Expand Up @@ -41,6 +54,7 @@ A pending setup handle is success in progress. Preserve it. Use a unique run tag
client_thread_id,
host_id,
project_id,
environment: { type: "local" | "worktree" },
run_tag,
title,
state: "pending_setup | active | complete | failed",
Expand All @@ -56,39 +70,71 @@ pending setup can still resolve within the named bound, and never create a
replacement task while the original setup can still resolve. Prefer `title`;
while a project task is loading, Codex can temporarily put the
requested title in `summary`. Accept it only when it contains the same exact unique run
tag, and the project ID also matches when one exists. Copy the ready thread and host IDs into
tag. When a project is bound, prefer an exact `projectId` match, but while
setup is still loading allow the same unique run tag in `title`/`summary`
even if `projectId` is not yet present on the list row (Lisbon v4: list
matching that *required* projectId during worktree setup never resolved).
When the list or create payload exposes `clientThreadId` (or equivalent), also
correlate the pending handle by that ID. Copy the ready thread and host IDs into
the existing handle.

Use a **chat-scale** bound for local/projectless starts (for example 30×1s or
60×2s). Use a **provisioning-scale** bound when the node uses a worktree
environment (worktree creation alone can exceed two minutes). Name both
constants in the script.

```javascript
const START_RESOLVE_ATTEMPTS = 30;
const START_RESOLVE_DELAY_MS = 1000;
// Chat-scale default for local / projectless. Raise for worktree nodes.
const isWorktree = handle.environment?.type === "worktree";
const START_RESOLVE_ATTEMPTS = isWorktree ? 90 : 30;
const START_RESOLVE_DELAY_MS = isWorktree ? 2000 : 1000;

function findExactThread(value, projectId, runTag) {
function findExactThread(value, projectId, runTag, clientThreadId) {
if (typeof value === "string") {
try {
return findExactThread(JSON.parse(value), projectId, runTag);
return findExactThread(JSON.parse(value), projectId, runTag, clientThreadId);
} catch {
return null;
}
}
if (!value || typeof value !== "object") return null;
const listedClient =
value.clientThreadId ?? value.client_thread_id ?? null;
if (
clientThreadId &&
listedClient &&
String(listedClient) === String(clientThreadId)
) {
return value;
}
// Projectless handles have no projectId; match the unique run tag alone.
// When a project is bound, require an exact projectId match as well.
const projectRequired = projectId != null && projectId !== "";
const sameProject = !projectRequired || value.projectId === projectId;
// When a project is bound, prefer projectId match but do not require it
// while setup is still loading (projectId may be absent on early list rows).
const title = typeof value.title === "string" ? value.title : "";
const summary = typeof value.summary === "string" ? value.summary : "";
if (sameProject && (title.includes(runTag) || summary.includes(runTag))) return value;
const tagHit = title.includes(runTag) || summary.includes(runTag);
if (tagHit) {
const projectRequired = projectId != null && projectId !== "";
const listedProject = value.projectId ?? value.project_id;
if (!projectRequired || listedProject == null || listedProject === projectId) {
return value;
}
}
for (const nested of Object.values(value)) {
const found = findExactThread(nested, projectId, runTag);
const found = findExactThread(nested, projectId, runTag, clientThreadId);
if (found) return found;
}
return null;
}

for (let attempt = 1; !handle.thread_id && attempt <= START_RESOLVE_ATTEMPTS; attempt += 1) {
const snapshot = await listThreads({ limit: 50 });
const record = findExactThread(snapshot, handle.project_id, handle.run_tag);
const record = findExactThread(
snapshot,
handle.project_id,
handle.run_tag,
handle.client_thread_id,
);
if (record) {
handle.thread_id = findString(record, ["threadId", "thread_id", "id"]);
handle.host_id = findString(record, ["hostId", "host_id"]);
Expand Down
11 changes: 10 additions & 1 deletion skills/codex-graph/scripts/validate_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ def main() -> None:
"complete and reachable",
"Many files \u2260 parallel",
"graph_coherence.py",
"per node",
"provisioning-scale",
Comment on lines +178 to +179

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require the validator to check the complete environment contract.

The new checks only require generic phrases such as per node and provisioning-scale. They do not require local execution for read-only nodes, worktrees for repository-writing nodes, or chat-scale polling for local starts. A later edit can remove these mappings and still pass validation. Add checks for each required mapping clause.

Also applies to: 211-215, 256-258

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/codex-graph/scripts/validate_skill.py` around lines 178 - 179, Extend
the validation checks in validate_skill.py for each environment-contract mapping
to require its complete clause, not just generic phrases such as “per node” or
“provisioning-scale”. Require read-only nodes to specify local execution,
repository-writing nodes to specify worktrees, and local starts to specify
chat-scale polling, covering the related checks near the existing mapping phrase
lists.

]:
if required_phrase not in text:
fail(f"missing required behavior: {required_phrase}")
Expand Down Expand Up @@ -206,7 +208,11 @@ def main() -> None:

"completed upstream handoff",
"Read first, then wait",
"Read again after every wait",]:
"Read again after every wait",
"environment per node",
"provisioning-scale",
"clientThreadId",
]:
if required_phrase not in lifecycle:
fail(f"task lifecycle reference is missing: {required_phrase}")

Expand Down Expand Up @@ -247,6 +253,9 @@ def main() -> None:
"Scope worker prompts to node-local obligations",
"mid-graph worker",
"Your only obligation is the",
"per node",
"Never apply `worktree` to every node",
"provisioning-scale",
]:
if required_phrase not in patterns:
fail(f"code patterns are missing verified graph lesson: {required_phrase}")
Expand Down
Loading