ai-foreman 1.7.19 → 1.7.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -5
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +18 -6
- package/dist/adapters/claude.d.ts +16 -0
- package/dist/adapters/claude.js +28 -3
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +99 -5
- package/dist/adapters/recovering.d.ts +9 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +7 -0
- package/dist/agentRun.js +24 -13
- package/dist/branch/currentGuard.d.ts +1 -0
- package/dist/branch/currentGuard.js +1 -0
- package/dist/branch/finalization.js +6 -1
- package/dist/branch/git.d.ts +1 -0
- package/dist/branch/git.js +15 -1
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +92 -5
- package/dist/buildAdmission.d.ts +61 -0
- package/dist/buildAdmission.js +286 -0
- package/dist/buildApproval.d.ts +2 -0
- package/dist/buildApproval.js +17 -0
- package/dist/buildAssignment.d.ts +33 -0
- package/dist/buildAssignment.js +215 -0
- package/dist/buildInterventions.d.ts +90 -0
- package/dist/buildInterventions.js +334 -0
- package/dist/buildLaunchGate.d.ts +1 -0
- package/dist/buildLaunchGate.js +36 -0
- package/dist/buildOwnershipReconciliation.d.ts +19 -0
- package/dist/buildOwnershipReconciliation.js +41 -0
- package/dist/buildOwnershipRepair.d.ts +33 -0
- package/dist/buildOwnershipRepair.js +111 -0
- package/dist/buildRuns.d.ts +24 -1
- package/dist/buildRuns.js +216 -70
- package/dist/buildSyntheticWork.d.ts +2 -0
- package/dist/buildSyntheticWork.js +30 -0
- package/dist/buildWorkAdmission.d.ts +17 -0
- package/dist/buildWorkAdmission.js +57 -0
- package/dist/buildWorkAuthorization.d.ts +2 -0
- package/dist/buildWorkAuthorization.js +23 -0
- package/dist/buildWorkContext.d.ts +3 -0
- package/dist/buildWorkContext.js +6 -0
- package/dist/buildWorkMigration.d.ts +9 -0
- package/dist/buildWorkMigration.js +206 -0
- package/dist/buildWorkReconciliation.d.ts +12 -0
- package/dist/buildWorkReconciliation.js +70 -0
- package/dist/builderGuidanceFollowup.d.ts +15 -0
- package/dist/builderGuidanceFollowup.js +34 -0
- package/dist/cli/doctor.js +3 -2
- package/dist/cli/graph.d.ts +2 -0
- package/dist/cli/graph.js +75 -0
- package/dist/cli/manager.d.ts +8 -0
- package/dist/cli/manager.js +234 -12
- package/dist/cli/recovery.js +3 -2
- package/dist/cli/runtimeAuthPrompt.d.ts +5 -0
- package/dist/cli/runtimeAuthPrompt.js +41 -7
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +2091 -1849
- package/dist/cli/status.js +20 -5
- package/dist/cli/tickets.js +9 -7
- package/dist/continuity.d.ts +16 -1
- package/dist/continuity.js +68 -12
- package/dist/foreman.d.ts +12 -4
- package/dist/foreman.js +397 -60
- package/dist/graph/boundary.d.ts +15 -0
- package/dist/graph/boundary.js +50 -0
- package/dist/graph/bridge.d.ts +9 -0
- package/dist/graph/bridge.js +146 -0
- package/dist/graph/bridge.py +205 -0
- package/dist/graph/config.d.ts +11 -0
- package/dist/graph/config.js +40 -0
- package/dist/graph/corpus.d.ts +16 -0
- package/dist/graph/corpus.js +188 -0
- package/dist/graph/derived.d.ts +17 -0
- package/dist/graph/derived.js +73 -0
- package/dist/graph/host.d.ts +4 -0
- package/dist/graph/host.js +160 -0
- package/dist/graph/install.d.ts +3 -0
- package/dist/graph/install.js +50 -0
- package/dist/graph/lifecycle.d.ts +11 -0
- package/dist/graph/lifecycle.js +124 -0
- package/dist/graph/maintenance.d.ts +43 -0
- package/dist/graph/maintenance.js +301 -0
- package/dist/graph/manager.d.ts +9 -0
- package/dist/graph/manager.js +92 -0
- package/dist/graph/preparation.d.ts +41 -0
- package/dist/graph/preparation.js +43 -0
- package/dist/graph/read.d.ts +27 -0
- package/dist/graph/read.js +152 -0
- package/dist/graph/semantic.d.ts +17 -0
- package/dist/graph/semantic.js +27 -0
- package/dist/graph/session.d.ts +8 -0
- package/dist/graph/session.js +92 -0
- package/dist/graph/sources.d.ts +4 -0
- package/dist/graph/sources.js +11 -0
- package/dist/graph/storage.d.ts +38 -0
- package/dist/graph/storage.js +159 -0
- package/dist/graph/turn.d.ts +17 -0
- package/dist/graph/turn.js +360 -0
- package/dist/graph/util.d.ts +5 -0
- package/dist/graph/util.js +51 -0
- package/dist/handoffs.d.ts +1 -0
- package/dist/handoffs.js +5 -1
- package/dist/humanDecision.d.ts +21 -2
- package/dist/humanDecision.js +169 -10
- package/dist/index.js +4 -1
- package/dist/installOwnership.d.ts +46 -0
- package/dist/installOwnership.js +440 -0
- package/dist/linuxProbeInventory.d.ts +12 -0
- package/dist/linuxProbeInventory.js +48 -0
- package/dist/log.d.ts +1 -1
- package/dist/log.js +7 -1
- package/dist/managerEvidence.d.ts +25 -0
- package/dist/managerEvidence.js +326 -0
- package/dist/managerEvidenceArtifacts.d.ts +38 -0
- package/dist/managerEvidenceArtifacts.js +118 -0
- package/dist/managerPacket.d.ts +2 -0
- package/dist/managerPacket.js +23 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +2 -2
- package/dist/observability.js +1 -1
- package/dist/processIdentity.d.ts +21 -1
- package/dist/processIdentity.js +87 -5
- package/dist/providerPhase.d.ts +13 -0
- package/dist/providerPhase.js +26 -0
- package/dist/qaAuthorizedDisposition.d.ts +24 -0
- package/dist/qaAuthorizedDisposition.js +26 -0
- package/dist/qaBuildGate.d.ts +4 -0
- package/dist/qaBuildGate.js +216 -0
- package/dist/qaBuilderCoverage.d.ts +5 -0
- package/dist/qaBuilderCoverage.js +78 -0
- package/dist/qaContractCoverage.d.ts +10 -0
- package/dist/qaContractCoverage.js +55 -0
- package/dist/qaContractDelivery.d.ts +23 -0
- package/dist/qaContractDelivery.js +145 -0
- package/dist/qaContractFreshness.d.ts +15 -0
- package/dist/qaContractFreshness.js +36 -0
- package/dist/qaDeliveryJournal.d.ts +10 -0
- package/dist/qaDeliveryJournal.js +16 -1
- package/dist/qaEffectiveConfig.d.ts +20 -0
- package/dist/qaEffectiveConfig.js +40 -0
- package/dist/qaEvidenceReader.d.ts +14 -0
- package/dist/qaEvidenceReader.js +176 -0
- package/dist/qaFailureDelivery.d.ts +2 -0
- package/dist/qaFailureDelivery.js +107 -10
- package/dist/qaHandbackMigration.d.ts +1 -1
- package/dist/qaHandbackMigration.js +3 -3
- package/dist/qaPreparation.d.ts +26 -0
- package/dist/qaPreparation.js +150 -0
- package/dist/qaPreparationInputs.d.ts +8 -0
- package/dist/qaPreparationInputs.js +47 -0
- package/dist/qaPreparationMetrics.d.ts +145 -0
- package/dist/qaPreparationMetrics.js +120 -0
- package/dist/qaPreparationPolicy.d.ts +5 -0
- package/dist/qaPreparationPolicy.js +51 -0
- package/dist/qaPreparationStore.d.ts +157 -0
- package/dist/qaPreparationStore.js +412 -0
- package/dist/qaProtocolV2.d.ts +28 -0
- package/dist/qaProtocolV2.js +12 -0
- package/dist/qaReview.d.ts +6 -0
- package/dist/qaReview.js +293 -41
- package/dist/qaTimeline.d.ts +18 -0
- package/dist/qaTimeline.js +123 -0
- package/dist/qaVerificationContract.d.ts +17 -0
- package/dist/qaVerificationContract.js +236 -0
- package/dist/readinessCleanup.d.ts +19 -0
- package/dist/readinessCleanup.js +122 -0
- package/dist/readinessGate.d.ts +1 -0
- package/dist/readinessGate.js +82 -0
- package/dist/recoveryGuidance.d.ts +17 -0
- package/dist/recoveryGuidance.js +38 -0
- package/dist/runtimeAuth.d.ts +1 -0
- package/dist/runtimeAuth.js +5 -1
- package/dist/runtimeReadiness.d.ts +13 -0
- package/dist/runtimeReadiness.js +206 -10
- package/dist/sessionIdentity.d.ts +2 -0
- package/dist/sessionIdentity.js +14 -0
- package/dist/sessionLifecycle.js +3 -3
- package/dist/sources/sourceRegistry.js +7 -0
- package/dist/stateTransfer.js +85 -8
- package/dist/statusReporter.d.ts +1 -0
- package/dist/statusReporter.js +1 -0
- package/dist/supervisedStart.d.ts +6 -0
- package/dist/supervisedStart.js +178 -62
- package/dist/supervisor.d.ts +2 -0
- package/dist/supervisor.js +26 -2
- package/dist/terminalStyle.d.ts +21 -0
- package/dist/terminalStyle.js +50 -0
- package/dist/ticketPopulation.d.ts +1 -0
- package/dist/ticketPopulation.js +5 -1
- package/dist/tickets/commands.js +2 -2
- package/dist/tickets/queue.js +1 -1
- package/dist/tickets/ticketSchema.d.ts +198 -0
- package/dist/tickets/ticketSchema.js +2 -0
- package/dist/windowsProbeJob.d.ts +12 -0
- package/dist/windowsProbeJob.js +144 -0
- package/dist/workflowDb.d.ts +121 -6
- package/dist/workflowDb.js +874 -30
- package/dist/workflowReader.d.ts +116 -2
- package/dist/workflowReader.js +131 -4
- package/package.json +89 -8
- package/scripts/copy-graph-assets.mjs +3 -0
- package/scripts/graph-benchmark.mjs +20 -0
- package/scripts/graph-evaluation.mjs +85 -0
package/README.md
CHANGED
|
@@ -131,7 +131,7 @@ Provider sessions are historical after import. Resume with `rafi build:resume <p
|
|
|
131
131
|
| `--branch-prefix <prefix>` | `feature` / `rafi` / another valid Git prefix | `feature` | Prefix for generated ticket branches. Explicit prefixes are preserved verbatim. |
|
|
132
132
|
| `--max-branch-depth <n>` | positive integer | `5` | Maximum selected branch/PR stack depth (root PR is depth 1). |
|
|
133
133
|
| `--show-session-cost` / `--hide-session-cost` | flags | saved role preferences | Show authoritative provider cost or trustworthy cumulative tokens for Builder and QA; does not change context occupancy display. |
|
|
134
|
-
| `--auto-compact-threshold <percent>` | integer `1`–`99` | saved Builder value, then `
|
|
134
|
+
| `--auto-compact-threshold <percent>` | integer `1`–`99` | saved Builder value, then `65` | Initial run threshold override. A newer persistent settings revision wins at the next safe boundary. |
|
|
135
135
|
|
|
136
136
|
Auto-detected tracker files:
|
|
137
137
|
|
|
@@ -166,21 +166,23 @@ Build stall recovery uses current context occupancy, not lifetime token usage. T
|
|
|
166
166
|
|
|
167
167
|
Unchanged, source-bound plan and ticket approvals are reused. Material ticket, plan, delivery, or workflow changes require a new decision, including with `--yes`. Legacy approval evidence that cannot prove unchanged scope requires review. `--steps` counts completed tickets or steps; it is neither a wall-clock limit nor a model-turn budget.
|
|
168
168
|
|
|
169
|
-
Questions are persisted before prompting. Redirected or detached runs return a recoverable needs-input outcome (exit 2). Use `rafi build:attach . --run <id>` to see decisions, `rafi build:decide . --run <id> --decision <id> --choice <id>` to answer, and `rafi
|
|
169
|
+
Questions are persisted before prompting. Redirected or detached runs return a recoverable needs-input outcome (exit 2). Use `rafi build:attach . --run <id>` to see decisions, `rafi build:decide . --run <id> --decision <id> --choice <id>` to answer, and `rafi resume` to select a build and continue. Custom answers additionally use `--choice custom --answer "text"`. Independent eligible tickets continue around a ticket blocker; dependent and unsafe shared-delivery work stays deferred. Answered ticket questions are reconsidered at safe boundaries.
|
|
170
170
|
|
|
171
171
|
CLI starts run under a durable parent supervisor by default. `--detach` writes output under `.rafi/logs/supervisor-<run>.log`; `--no-supervisor` retains direct execution. A preparation-only worker crash may restart within frozen budgets after proving the worker and registered readiness children are stopped. A worker crash with unresolved provider work pauses for reconciliation: it does not silently repeat a possibly completed action. `build:stop` signals only a verified supervisor process incarnation.
|
|
172
172
|
|
|
173
|
-
Runtime deadlines can be overridden in `autonomy.runtime_deadlines` using positive integer milliseconds: `preparation_ms` (120000), `rpc_ms` (60000), `compaction_ms` (120000), `shutdown_ms` (10000), and `turn_ms` (3600000). They are frozen with the run policy. Transport-idle and hard active-turn deadlines have distinct meanings; neither grants permission to repeat uncertain side effects. Attached-process elapsed time includes waiting and is not provider compute time. Activity warns after five minutes without a completed tool or agent turn; intentional human waits are excluded, and the warning alone does not cancel work. Final paused output lists the durable question and
|
|
173
|
+
Runtime deadlines can be overridden in `autonomy.runtime_deadlines` using positive integer milliseconds: `preparation_ms` (120000), `rpc_ms` (60000), `compaction_ms` (120000), `shutdown_ms` (10000), and `turn_ms` (3600000). They are frozen with the run policy. Transport-idle and hard active-turn deadlines have distinct meanings; neither grants permission to repeat uncertain side effects. Attached-process elapsed time includes waiting and is not provider compute time. Activity warns after five minutes without a completed tool or agent turn; intentional human waits are excluded, and the warning alone does not cancel work. Final paused output lists the durable question, its answer command, and the short `rafi resume` recovery command.
|
|
174
174
|
|
|
175
175
|
The current terminal status includes the role/provider, activity, truthful context state, successful compaction count, and handoff generation. At the Builder threshold, Foreman settles the in-flight action, verifies native compaction plus a fresh provider usage sample, and resumes the frozen action. The configured maximum defaults to ten successful compactions per location-scoped provider session; the next crossing uses a validated fresh handoff instead of exceeding it. Disposable QA snapshots never reuse a prior QA conversation in a new `/tmp/rafi-qa-*` worktree; they transfer cumulative state through an accepted handoff.
|
|
176
176
|
|
|
177
177
|
Failed QA reviews use the versioned `RAFI_QA_FAILURE_REPORT_START` / `RAFI_QA_FAILURE_REPORT_END` JSON contract. Foreman validates the report before Builder remediation, namespaces finding IDs by run, ticket, and review, and durably indexes the exact report, remediation response, and bounded fix summary by digest. A malformed failing report receives two correction-only turns in its original session, then a validated fresh successor performs one complete source-bound review followed by up to two correction-only turns. Any later operator-requested fresh successor performs one complete source-bound review followed by five usable correction-only turns.
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
A completed QA turn with missing/invalid continuity metadata or a malformed response can trigger one automatic complete fresh review per ticket. That allowance is durable across restarts and shared across protocol failures. The old session and snapshot close before the new review starts. Exhausted same-session report correction and malformed resumed report/acknowledgement responses use this route, rather than requiring an immediate manual resume. Earlier invalid responses cannot authorize Builder work or a QA pass. Provider execution failures, uncertain dispatch, invalid acceptance receipts, and modified recovery evidence still pause. Repeated failures exhaust the allowance and preserve recoverable state; independent eligible tickets can continue.
|
|
180
|
+
|
|
181
|
+
Owner-only recovery packets under `.foreman/qa-report-recovery/` bind the review to a full frozen source digest: commit, porcelain status, staged and unstaged bytes, and untracked content. Every fresh attempt captures and compares that complete state. Drift appends the new state and forces a complete review; correction-only reconstruction is never applied to changed source. Packet revisions and the authoritative WorkflowDb recovery head advance together. A successor must have a location-scoped identity, accept a handoff containing the complete resource inventory (purpose, byte count, digest, and recovery requirement), and acknowledge the copied packet before reporting. Paused recovery is available through `rafi resume` or `rafi build:resume` in the project directory. The picker resolves the ticket, current QA revision, and validated QA handoff internally; explicit recovery flags still reject stale revisions.
|
|
180
182
|
|
|
181
183
|
### `ai-foreman manager`
|
|
182
184
|
|
|
183
|
-
Manager
|
|
185
|
+
Manager provides project-wide diagnostic conversations and explicitly authorized, scoped controls over retained build runs. It starts with a verified live run when one exists, otherwise the most recently updated run; `--run <id>` changes only that initial focus. Each question refreshes the catalog, active state, deterministic aggregates, and relevant run details.
|
|
184
186
|
|
|
185
187
|
Permanent sanitized run summaries keep timing, counts, provider/model configuration, usage coverage, failure category, and terminal Git facts useful after detailed spans expire. Detailed spans retain the configured 30-day policy. Missing historical facts are reported as partial rather than reconstructed or treated as zero.
|
|
186
188
|
|
|
@@ -191,6 +193,16 @@ ai-foreman manager ./my-project
|
|
|
191
193
|
ai-foreman manager ./my-project --run <run-id> --ask "Compare this run with the last five successful runs"
|
|
192
194
|
```
|
|
193
195
|
|
|
196
|
+
Full QA history is available without a provider through `--ask '/qa-work <run>'`, `/qa-attempts <run> <work>`, `/qa-report <run> <work> <attempt> [occurrence]`, and `/qa-timeline <run> <work>`. One-shot output expands all pinned pages and artifacts before exiting, independently of bounded model packets. `/qa-export <run> <work> <attempt> [occurrence]` exports exact protected report bytes with their identity and digest. Interactive `/more <cursor>` and `/artifact <handle>` continuations belong to that session; a later invocation retrieves fresh evidence. Reads do not migrate workflow state.
|
|
197
|
+
|
|
198
|
+
Explicit `/guide-builder`, `/guide-qa`, and `/guide-both` commands take a run, work ID, and exact advice. The owning process delivers one-use guidance at its next safe boundary. `/pause <run> <work> [run]` waits for the active turn to finish; withdraw the pause to allow continuation. `/request-attempt <run> <work> <reason>` grants one existing-ledger attempt authorization for the current failed review or waiting Builder guidance after an unfinalized pass. Guidance alone never grants attempts. Inspect truthful delivery and verification receipts with `/qa-instruction <run> <work> <instruction>`.
|
|
199
|
+
|
|
200
|
+
If QA passes while Builder guidance is waiting, explicit resume applies that guidance within the existing approved ticket scope and remaining attempt allowance, then performs a complete fresh QA review. This works on the current branch and in isolated ticket worktrees. The follow-up consumes the same durable allowance as QA remediation; neither a fresh review nor a restart resets it. When the allowance is exhausted, authorize one scoped follow-up with `/request-attempt`, then resume. Changed requirements need renewed approval, and uncertain submissions require reconciliation before another Builder turn. Saving guidance alone does not start work.
|
|
201
|
+
|
|
202
|
+
Ownership schema 4 requires admitted work before Builder/QA dispatch. Legacy QA records are retained in quarantine when authorization cannot be proved. Both resume aliases assess conflicts before selecting recovery mode. Use `/qa-conflicts <run>` and `/qa-repair-plan <run> <work>` to inspect the source mapping, then submit an explicit `/qa-repair <JSON>` decision with the displayed revision, source digest, approved scope, complete path mapping, and operator attestation. Stop the existing owner first. Repairs preserve source and historical packets and require fresh verification; uncertain provider dispatch needs separate reconciliation.
|
|
203
|
+
|
|
204
|
+
The upgrade stores a coherent database backup and recovery packet copy under `.rafi/backups/work-admission-v4/<database digest>/` before committing its guards. Rollback means pausing execution and preserving newer evidence for forward repair. Older writers are rejected against upgraded databases; do not down-migrate or restore a backup without accounting for subsequent provider work.
|
|
205
|
+
|
|
194
206
|
### GitHub PR Failure Recovery
|
|
195
207
|
|
|
196
208
|
When `--create-pr` cannot prepare GitHub, push a branch, or create a PR, Foreman records a structured failure and blocks the affected ticket cleanly. Blocked worktrees are retained automatically so you can repair the environment and retry without losing the ticket session.
|
|
@@ -681,3 +693,15 @@ Package output:
|
|
|
681
693
|
- **`special-agents`** — library (rules + skills + agents + composition)
|
|
682
694
|
- **`ai-foreman`** — this runtime
|
|
683
695
|
- **`@rafi-ai/cli`** — CLI for `rafi create` and `rafi compile`
|
|
696
|
+
|
|
697
|
+
### Recovery ownership upgrades
|
|
698
|
+
|
|
699
|
+
Stop existing workers before upgrading. The recovery database now records preparation admission, single-use launches, retry lineage, owned readiness processes and monotonic lease generations. Older binaries must not write an upgraded database; preserve these records when troubleshooting rather than deleting locks. Unknown process identity, incomplete child-launch evidence or an unresolved provider dispatch requires reconciliation. A stale heartbeat alone does not authorize another worker. Read-only resume inspection supports older storage without migrating it.
|
|
700
|
+
|
|
701
|
+
Ownership schema version 2 adds bounded child registration/acknowledgement, explicit child-workflow ownership, and separate readiness/cleanup outcomes. Build readiness requires the original admission and owning project even when the command executes in an external worktree. Both the current build and competing starts wait for verified probe cleanup. Runtime snapshot, dispatch, continuity and session writes retain original authority; releasing a lease does not make those records writable by an arbitrary caller. Completed, cancelled and superseded builds cannot be reactivated by saving a snapshot.
|
|
702
|
+
|
|
703
|
+
Ownership schema version 3 gates provider readiness behind registered helper authorization and records cleanup separately. Cleanup is reachable through both short resume commands even when its owning build is terminal or has no valid build snapshot. Restricted cleanup connections can update only their selected readiness records. Migration waits for local legacy execution to become safely quiescent; persistent compatibility guards then reject already-open older writers as well as new older opens. Uncertain remote provider records survive the upgrade unchanged so their owning recovery flow remains reachable. Ordinary starts remain blocked until those operations are reconciled. A lost acknowledgement cannot revoke an already-authorized helper.
|
|
704
|
+
|
|
705
|
+
Standalone readiness also rejects unverified cleanup. The current process retains its cleanup evidence and checks it before another probe starts; restoring process visibility permits a retry. Standalone commands do not create a build recovery database, so this evidence does not survive process exit.
|
|
706
|
+
|
|
707
|
+
On macOS/Linux, readiness cleanup checks the dedicated process group and inherited probe tags, including tagged descendants that escape the group. This is not a sandbox for descendants that deliberately remove their tag and detach. Unavailable inventory and ambiguous legacy evidence remain unresolved. The Windows readiness implementation targets Windows 10 / Server 2016 APIs, but those minimum versions have not been verified. Native Windows verification remains a release gate. Readiness providers start inside a non-breakaway Job Object with kill-on-close behavior, using built-in Windows PowerShell and `Add-Type`. Standard npm `.cmd`/`.bat` provider shims resolve to direct Node arguments; arbitrary shell shims are rejected. The Windows Job Object applies to readiness probes; it does not add containment for arbitrary Builder commands. Native regression tests are in `.github/workflows/build-recovery.yml`; the implementation plan records actual platform verification.
|
package/dist/activity.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface ActivityReporterOptions {
|
|
|
14
14
|
usefulProgressWarningMs?: number;
|
|
15
15
|
/** Override the safe automatic TTY rendering mode for tests or embedding hosts. */
|
|
16
16
|
ttyMode?: ActivityTtyMode;
|
|
17
|
+
/** Environment used for color policy; useful for isolated hosts and tests. */
|
|
18
|
+
colorEnvironment?: NodeJS.ProcessEnv;
|
|
17
19
|
}
|
|
18
20
|
export type ActivityTtyMode = "auto" | "cursor" | "records";
|
|
19
21
|
/** One process-local live view shared by nested RAFI operations. */
|
|
@@ -30,6 +32,7 @@ export declare class ActivityReporter {
|
|
|
30
32
|
private usefulProgressWarningPrinted;
|
|
31
33
|
private pauseStartedAt;
|
|
32
34
|
private readonly ttyMode;
|
|
35
|
+
private readonly colorEnvironment;
|
|
33
36
|
private readonly commandStartedAt;
|
|
34
37
|
private readonly phases;
|
|
35
38
|
private nextPhaseId;
|
package/dist/activity.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { AsyncQueue } from "./util/asyncQueue.js";
|
|
3
|
+
import { accent, error, info, sanitizeTerminalText, success, warning } from "./terminalStyle.js";
|
|
3
4
|
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
4
5
|
/** One process-local live view shared by nested RAFI operations. */
|
|
5
6
|
export class ActivityReporter {
|
|
@@ -15,6 +16,7 @@ export class ActivityReporter {
|
|
|
15
16
|
usefulProgressWarningPrinted = false;
|
|
16
17
|
pauseStartedAt = 0;
|
|
17
18
|
ttyMode;
|
|
19
|
+
colorEnvironment;
|
|
18
20
|
commandStartedAt;
|
|
19
21
|
phases = new Map();
|
|
20
22
|
nextPhaseId = 1;
|
|
@@ -43,6 +45,7 @@ export class ActivityReporter {
|
|
|
43
45
|
this.quietWarningMs = options.quietWarningMs ?? 60_000;
|
|
44
46
|
this.usefulProgressWarningMs = options.usefulProgressWarningMs ?? 300_000;
|
|
45
47
|
this.ttyMode = resolveTtyMode(options.ttyMode);
|
|
48
|
+
this.colorEnvironment = options.colorEnvironment ?? process.env;
|
|
46
49
|
this.commandStartedAt = this.now();
|
|
47
50
|
}
|
|
48
51
|
begin(label) {
|
|
@@ -94,15 +97,16 @@ export class ActivityReporter {
|
|
|
94
97
|
}
|
|
95
98
|
note(message) {
|
|
96
99
|
this.clearLine();
|
|
97
|
-
this.output.write(`${
|
|
100
|
+
this.output.write(`${sanitizeTerminalText(message).replace(/[\r\t]+/g, " ").slice(0, 500)}\n`);
|
|
98
101
|
this.render(true);
|
|
99
102
|
}
|
|
100
103
|
writePersistent(text) {
|
|
101
104
|
if (!text)
|
|
102
105
|
return;
|
|
103
106
|
this.clearLine();
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
const safeText = sanitizeTerminalText(text);
|
|
108
|
+
this.output.write(safeText);
|
|
109
|
+
if (!safeText.endsWith("\n"))
|
|
106
110
|
this.output.write("\n");
|
|
107
111
|
this.render(true);
|
|
108
112
|
}
|
|
@@ -197,7 +201,14 @@ export class ActivityReporter {
|
|
|
197
201
|
const quiet = quietFor >= this.quietWarningMs ? `provider quiet ${formatDuration(quietFor)}; RAFI is responsive` : this.detail;
|
|
198
202
|
const body = [phase?.label, provider || undefined, phase ? quiet : undefined, this.agentStatusLine].filter(Boolean).join(" — ");
|
|
199
203
|
if (this.output.isTTY) {
|
|
200
|
-
const
|
|
204
|
+
const color = { stream: this.output, env: this.colorEnvironment };
|
|
205
|
+
const status = phase?.label.toLowerCase() ?? "";
|
|
206
|
+
const marker = status.includes("fail") || status.includes("error") ? error("!", color)
|
|
207
|
+
: status.includes("complete") || status.includes("success") ? success("✓", color)
|
|
208
|
+
: status.includes("warn") || status.includes("quiet") ? warning("!", color)
|
|
209
|
+
: info(FRAMES[this.frame++ % FRAMES.length], color);
|
|
210
|
+
const safeBody = sanitizeTerminalText(body);
|
|
211
|
+
const line = `${marker} ${accent("RAFI", color)} working: ${safeBody} (${formatDuration(now - this.commandStartedAt)})`;
|
|
201
212
|
const semanticKey = semanticStatusKey(body);
|
|
202
213
|
if (this.ttyMode === "records") {
|
|
203
214
|
if (semanticKey === this.lastSemanticKey)
|
|
@@ -223,7 +234,8 @@ export class ActivityReporter {
|
|
|
223
234
|
return;
|
|
224
235
|
if (force || now - this.lastHeartbeatAt >= this.heartbeatMs) {
|
|
225
236
|
this.lastHeartbeatAt = now;
|
|
226
|
-
|
|
237
|
+
const color = { stream: this.output, env: this.colorEnvironment };
|
|
238
|
+
this.output.write(`[${new Date(now).toISOString()}] ${accent("rafi", color)} working: ${sanitizeTerminalText(body)} (${formatDuration(now - this.commandStartedAt)})\n`);
|
|
227
239
|
this.lastSemanticKey = semanticKey;
|
|
228
240
|
}
|
|
229
241
|
}
|
|
@@ -341,7 +353,7 @@ function isFailedToolCompletion(event) {
|
|
|
341
353
|
|| (typeof event.exitCode === "number" && Number.isFinite(event.exitCode) && event.exitCode !== 0);
|
|
342
354
|
}
|
|
343
355
|
function clean(value, maximum) {
|
|
344
|
-
return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum);
|
|
356
|
+
return sanitizeTerminalText(value).replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum);
|
|
345
357
|
}
|
|
346
358
|
function semanticStatusKey(body) {
|
|
347
359
|
return body
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProviderPhaseBarrier, type ProviderTurnPurpose } from "../providerPhase.js";
|
|
1
2
|
import type { SDKSessionInfo, PermissionResult } from "@anthropic-ai/claude-agent-sdk";
|
|
2
3
|
/** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
|
|
3
4
|
export declare function requireClaudeSDK(): Promise<typeof import("@anthropic-ai/claude-agent-sdk")>;
|
|
@@ -43,6 +44,12 @@ export declare function claudeApiRetryEvent(message: {
|
|
|
43
44
|
export declare class ClaudeAdapter implements BuilderAdapter {
|
|
44
45
|
private readonly opts;
|
|
45
46
|
readonly agent: "claude";
|
|
47
|
+
graphRuntimeSettings(): {
|
|
48
|
+
model: string | undefined;
|
|
49
|
+
effort: import("./types.js").EffortLevel | undefined;
|
|
50
|
+
fast: boolean | undefined;
|
|
51
|
+
runtimeExecutable: string | undefined;
|
|
52
|
+
};
|
|
46
53
|
private readonly inbox;
|
|
47
54
|
private readonly eventQueue;
|
|
48
55
|
private readonly query;
|
|
@@ -74,6 +81,14 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
74
81
|
private pending?;
|
|
75
82
|
private questionTrace?;
|
|
76
83
|
private responseOnlyTurn;
|
|
84
|
+
readonly phaseBarrier: ProviderPhaseBarrier;
|
|
85
|
+
contractCapabilities(): {
|
|
86
|
+
sameSessionAcceptance: boolean;
|
|
87
|
+
nativeCompactionBarrier: boolean;
|
|
88
|
+
};
|
|
89
|
+
enableContractEnforcement(): void;
|
|
90
|
+
contractCompactionSequence(): number;
|
|
91
|
+
acceptContractDelivery(sequence: number): void;
|
|
77
92
|
private terminalResult?;
|
|
78
93
|
private streamEnded;
|
|
79
94
|
private closed;
|
|
@@ -86,6 +101,7 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
86
101
|
handback?: boolean;
|
|
87
102
|
responseOnly?: boolean;
|
|
88
103
|
logicalActionId?: string;
|
|
104
|
+
purpose?: ProviderTurnPurpose;
|
|
89
105
|
}): Promise<TurnResult>;
|
|
90
106
|
private sendTurnInternal;
|
|
91
107
|
private captureStderr;
|
package/dist/adapters/claude.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProviderPhaseBarrier } from "../providerPhase.js";
|
|
1
2
|
import { OperationDeadline } from "../util/deadline.js";
|
|
2
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
@@ -27,6 +28,7 @@ const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 30 * 60_000;
|
|
|
27
28
|
*/
|
|
28
29
|
export function buildClaudeQueryOptions(opts) {
|
|
29
30
|
const qaReadOnly = opts.sessionRole === "qa" || opts.sandboxMode === "read-only";
|
|
31
|
+
const confinedBuilder = opts.sessionRole === "builder";
|
|
30
32
|
const base = {
|
|
31
33
|
cwd: opts.cwd,
|
|
32
34
|
pathToClaudeCodeExecutable: opts.runtimeExecutable,
|
|
@@ -41,18 +43,24 @@ export function buildClaudeQueryOptions(opts) {
|
|
|
41
43
|
// settings in those roles.
|
|
42
44
|
settingSources: qaReadOnly ? ["user"] : ["user", "project", "local"],
|
|
43
45
|
...(qaReadOnly ? { disallowedTools: ["Write", "Edit", "NotebookEdit"] } : {}),
|
|
46
|
+
...(confinedBuilder && !qaReadOnly ? { sandbox: {
|
|
47
|
+
enabled: true, failIfUnavailable: true, autoAllowBashIfSandboxed: false, allowUnsandboxedCommands: false,
|
|
48
|
+
network: opts.networkAccess === true ? { allowedDomains: ["*"] } : { allowedDomains: [], deniedDomains: ["*"] },
|
|
49
|
+
filesystem: { allowWrite: [opts.cwd] },
|
|
50
|
+
} } : {}),
|
|
44
51
|
...(qaReadOnly ? { sandbox: {
|
|
45
52
|
enabled: true,
|
|
46
53
|
failIfUnavailable: true,
|
|
47
54
|
autoAllowBashIfSandboxed: true,
|
|
48
55
|
allowUnsandboxedCommands: false,
|
|
56
|
+
network: { allowedDomains: [], deniedDomains: ["*"] },
|
|
49
57
|
filesystem: { allowWrite: [join(dirname(opts.cwd), "scratch")], denyWrite: [opts.cwd, ...(opts.configRoot ? [opts.configRoot] : [])] },
|
|
50
58
|
} } : {}),
|
|
51
59
|
};
|
|
52
60
|
const exactSkills = opts.preloadedSkillContent?.length
|
|
53
61
|
? ["# Preloaded Skills", "Use the following skills for this run.", ...opts.preloadedSkillContent.map((skill) => skill.content)].join("\n\n")
|
|
54
62
|
: undefined;
|
|
55
|
-
const systemAppend = [opts.systemPromptAppend, exactSkills].filter((part) => Boolean(part)).join("\n\n");
|
|
63
|
+
const systemAppend = [opts.systemPromptAppend, opts.sessionRole === "builder" ? `Builder runtime contract: shell network ${opts.networkAccess === true ? "explicitly approved for this build" : "disabled"}; sandbox fallback disabled. If acquisition requires unavailable access, request input first or use a provenance-verified local bundle. A ticket answer does not grant runtime permissions.` : undefined, exactSkills].filter((part) => Boolean(part)).join("\n\n");
|
|
56
64
|
if (systemAppend) {
|
|
57
65
|
base.systemPrompt = { type: "preset", preset: "claude_code", append: systemAppend };
|
|
58
66
|
}
|
|
@@ -165,6 +173,7 @@ export function claudeApiRetryEvent(message) {
|
|
|
165
173
|
export class ClaudeAdapter {
|
|
166
174
|
opts;
|
|
167
175
|
agent = "claude";
|
|
176
|
+
graphRuntimeSettings() { return { model: this.opts.model, effort: this.opts.effort, fast: this.opts.fast, runtimeExecutable: this.opts.runtimeExecutable }; }
|
|
168
177
|
inbox = new AsyncQueue();
|
|
169
178
|
eventQueue = new BuilderEventQueue();
|
|
170
179
|
query;
|
|
@@ -196,6 +205,11 @@ export class ClaudeAdapter {
|
|
|
196
205
|
pending;
|
|
197
206
|
questionTrace;
|
|
198
207
|
responseOnlyTurn = false;
|
|
208
|
+
phaseBarrier = new ProviderPhaseBarrier();
|
|
209
|
+
contractCapabilities() { return { sameSessionAcceptance: true, nativeCompactionBarrier: true }; }
|
|
210
|
+
enableContractEnforcement() { this.phaseBarrier.enableEnforcement(); }
|
|
211
|
+
contractCompactionSequence() { return this.phaseBarrier.compactionSequence; }
|
|
212
|
+
acceptContractDelivery(sequence) { this.phaseBarrier.accept(sequence); }
|
|
199
213
|
terminalResult;
|
|
200
214
|
streamEnded = false;
|
|
201
215
|
closed = false;
|
|
@@ -239,6 +253,7 @@ export class ClaudeAdapter {
|
|
|
239
253
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
240
254
|
constructor(opts, query) {
|
|
241
255
|
this.opts = opts;
|
|
256
|
+
this.phaseBarrier.begin("initialization");
|
|
242
257
|
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
243
258
|
this._sessionRef = opts.resumeSessionRef;
|
|
244
259
|
this.sessionIdentityReady = new Promise((resolve, reject) => {
|
|
@@ -255,7 +270,15 @@ export class ClaudeAdapter {
|
|
|
255
270
|
options: {
|
|
256
271
|
...buildClaudeQueryOptions(opts),
|
|
257
272
|
abortController: this.abort,
|
|
258
|
-
hooks: {
|
|
273
|
+
hooks: {
|
|
274
|
+
PreToolUse: [{ hooks: [async (input) => {
|
|
275
|
+
if (input.hook_event_name !== "PreToolUse")
|
|
276
|
+
return {};
|
|
277
|
+
const reason = this.phaseBarrier.denial(input.tool_name);
|
|
278
|
+
return reason ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } : {};
|
|
279
|
+
}] }],
|
|
280
|
+
PreCompact: [{ hooks: [async () => { this.phaseBarrier.compact(); return {}; }] }],
|
|
281
|
+
SessionStart: [{ hooks: [async (input) => {
|
|
259
282
|
if (input.hook_event_name !== "SessionStart" || input.agent_id)
|
|
260
283
|
return {};
|
|
261
284
|
try {
|
|
@@ -266,7 +289,8 @@ export class ClaudeAdapter {
|
|
|
266
289
|
throw error;
|
|
267
290
|
}
|
|
268
291
|
return {};
|
|
269
|
-
}] }]
|
|
292
|
+
}] }]
|
|
293
|
+
},
|
|
270
294
|
stderr: (data) => this.captureStderr(data),
|
|
271
295
|
canUseTool: async (toolName, input, requestOptions = {}) => {
|
|
272
296
|
if (this.responseOnlyTurn)
|
|
@@ -512,6 +536,7 @@ export class ClaudeAdapter {
|
|
|
512
536
|
if (this.pending)
|
|
513
537
|
throw new Error("a turn is already in progress");
|
|
514
538
|
this.responseOnlyTurn = Boolean(policy?.responseOnly);
|
|
539
|
+
this.phaseBarrier.begin(policy?.purpose ?? (policy?.responseOnly ? "response-repair" : "implementation"));
|
|
515
540
|
const turnId = randomUUID();
|
|
516
541
|
this.activeProviderTurnId = turnId;
|
|
517
542
|
const observer = this.opts.observer;
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProviderTurnPurpose } from "../providerPhase.js";
|
|
1
2
|
import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextUsage, NativeAutoCompactionPolicy, NativeCompaction, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "./types.js";
|
|
2
3
|
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
3
4
|
export interface CodexLineResult {
|
|
@@ -11,6 +12,12 @@ export declare function parseCodexLine(raw: Record<string, unknown>): CodexLineR
|
|
|
11
12
|
export declare class CodexAdapter implements BuilderAdapter {
|
|
12
13
|
private readonly opts;
|
|
13
14
|
readonly agent: "codex";
|
|
15
|
+
graphRuntimeSettings(): {
|
|
16
|
+
model: string | undefined;
|
|
17
|
+
effort: import("./types.js").EffortLevel | undefined;
|
|
18
|
+
fast: boolean | undefined;
|
|
19
|
+
runtimeExecutable: string | undefined;
|
|
20
|
+
};
|
|
14
21
|
private readonly eventQueue;
|
|
15
22
|
private process?;
|
|
16
23
|
private nextId;
|
|
@@ -40,9 +47,17 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
40
47
|
private compactionUncertain;
|
|
41
48
|
private observedToolCalls;
|
|
42
49
|
private activeProviderTurnId?;
|
|
50
|
+
private activeNativeTurnId?;
|
|
51
|
+
private connectionGeneration;
|
|
52
|
+
private readonly approvalRequests;
|
|
53
|
+
private readonly answeredApprovalRequests;
|
|
43
54
|
private activeProviderTurnSpanId?;
|
|
44
55
|
private readonly toolSpans;
|
|
45
56
|
constructor(opts: BuilderAdapterOptions);
|
|
57
|
+
contractCapabilities(): {
|
|
58
|
+
sameSessionAcceptance: boolean;
|
|
59
|
+
nativeCompactionBarrier: boolean;
|
|
60
|
+
};
|
|
46
61
|
buildInstruction(instruction: string): string;
|
|
47
62
|
buildAppServerArgs(): string[];
|
|
48
63
|
/** Deprecated fixture helper. Runtime execution uses only app-server. */
|
|
@@ -51,6 +66,7 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
51
66
|
handback?: boolean;
|
|
52
67
|
responseOnly?: boolean;
|
|
53
68
|
logicalActionId?: string;
|
|
69
|
+
purpose?: ProviderTurnPurpose;
|
|
54
70
|
}): Promise<TurnResult>;
|
|
55
71
|
private sendTurnInternal;
|
|
56
72
|
compact(): Promise<CompactResult>;
|
|
@@ -85,6 +101,10 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
85
101
|
private traceLifecycle;
|
|
86
102
|
private touchWaiters;
|
|
87
103
|
private providerIdleTimeoutMs;
|
|
104
|
+
private effectiveNetworkAccess;
|
|
105
|
+
private effectiveApprovalPolicy;
|
|
106
|
+
/** Fail closed; approval is for this operation only, never a session cache or policy amendment. */
|
|
107
|
+
private handleServerRequest;
|
|
88
108
|
private disconnect;
|
|
89
109
|
private restartForAutoCompaction;
|
|
90
110
|
private buildSkillsAppendix;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -31,6 +31,7 @@ const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 30 * 60_000;
|
|
|
31
31
|
export class CodexAdapter {
|
|
32
32
|
opts;
|
|
33
33
|
agent = "codex";
|
|
34
|
+
graphRuntimeSettings() { return { model: this.opts.model, effort: this.opts.effort, fast: this.opts.fast, runtimeExecutable: this.opts.runtimeExecutable }; }
|
|
34
35
|
eventQueue = new BuilderEventQueue();
|
|
35
36
|
process;
|
|
36
37
|
nextId = 1;
|
|
@@ -60,6 +61,10 @@ export class CodexAdapter {
|
|
|
60
61
|
compactionUncertain = false;
|
|
61
62
|
observedToolCalls = 0;
|
|
62
63
|
activeProviderTurnId;
|
|
64
|
+
activeNativeTurnId;
|
|
65
|
+
connectionGeneration = 0;
|
|
66
|
+
approvalRequests = new Map();
|
|
67
|
+
answeredApprovalRequests = new Set();
|
|
63
68
|
activeProviderTurnSpanId;
|
|
64
69
|
toolSpans = new Map();
|
|
65
70
|
constructor(opts) {
|
|
@@ -67,8 +72,11 @@ export class CodexAdapter {
|
|
|
67
72
|
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
68
73
|
this._sessionRef = opts.resumeSessionRef;
|
|
69
74
|
}
|
|
75
|
+
contractCapabilities() { return { sameSessionAcceptance: true, nativeCompactionBarrier: false }; }
|
|
70
76
|
buildInstruction(instruction) {
|
|
71
|
-
return [this.opts.systemPromptAppend,
|
|
77
|
+
return [this.opts.systemPromptAppend,
|
|
78
|
+
this.opts.sessionRole === "builder" ? `Builder runtime contract: shell network ${this.effectiveNetworkAccess() ? "explicitly approved for this build" : "disabled"}; sandbox escalation ${this.effectiveApprovalPolicy() === "on-request" ? "requires separate approval for each exact operation" : "disabled"}. Web tools do not establish shell network access. If acquisition needs unavailable capabilities, request input before running acquisition, or use a local source/dependency bundle with verified provenance. A ticket answer does not change runtime permissions.` : undefined,
|
|
79
|
+
this.buildSkillsAppendix(), instruction]
|
|
72
80
|
.filter((part) => Boolean(part)).join("\n\n");
|
|
73
81
|
}
|
|
74
82
|
buildAppServerArgs() {
|
|
@@ -102,6 +110,7 @@ export class CodexAdapter {
|
|
|
102
110
|
}
|
|
103
111
|
finally {
|
|
104
112
|
this.activeProviderTurnId = undefined;
|
|
113
|
+
this.activeNativeTurnId = undefined;
|
|
105
114
|
}
|
|
106
115
|
}
|
|
107
116
|
const context = this.observationContext();
|
|
@@ -123,6 +132,7 @@ export class CodexAdapter {
|
|
|
123
132
|
this.toolSpans.delete(callId);
|
|
124
133
|
}
|
|
125
134
|
this.activeProviderTurnId = undefined;
|
|
135
|
+
this.activeNativeTurnId = undefined;
|
|
126
136
|
this.activeProviderTurnSpanId = undefined;
|
|
127
137
|
}
|
|
128
138
|
}
|
|
@@ -140,13 +150,18 @@ export class CodexAdapter {
|
|
|
140
150
|
this.turnUsageBaseline = this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined;
|
|
141
151
|
const completion = this.waitFor("turn/completed", (params) => params.threadId === this._sessionId, this.providerIdleTimeoutMs(), true);
|
|
142
152
|
turnStartDispatched = true;
|
|
143
|
-
await this.request("turn/start", {
|
|
153
|
+
const started = await this.request("turn/start", {
|
|
144
154
|
threadId: this._sessionId,
|
|
145
155
|
input: [{ type: "text", text: providerInstruction, text_elements: [] }],
|
|
146
156
|
cwd: this.opts.cwd,
|
|
147
157
|
model: this.opts.model ?? null,
|
|
148
158
|
effort: this.opts.effort ?? (this.opts.fast ? "low" : null),
|
|
159
|
+
approvalPolicy: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" ? "never" : this.effectiveApprovalPolicy(),
|
|
160
|
+
sandboxPolicy: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" || this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa"
|
|
161
|
+
? { type: "readOnly", networkAccess: false }
|
|
162
|
+
: { type: "workspaceWrite", writableRoots: [this.opts.cwd], networkAccess: this.effectiveNetworkAccess() },
|
|
149
163
|
});
|
|
164
|
+
this.activeNativeTurnId = String(started.turn?.id ?? "") || undefined;
|
|
150
165
|
const params = await new OperationDeadline("Codex active turn", this.opts.turnDeadlineMs ?? 3_600_000).run(() => completion);
|
|
151
166
|
const turn = params.turn;
|
|
152
167
|
const responseOnlyViolation = Boolean(policy?.responseOnly && this.observedToolCalls !== toolsBefore);
|
|
@@ -319,7 +334,7 @@ export class CodexAdapter {
|
|
|
319
334
|
if (this.opts.allowAutoCompactionSetupTurn === false)
|
|
320
335
|
return;
|
|
321
336
|
const toolsBefore = this.observedToolCalls;
|
|
322
|
-
const setup = await this.sendTurnInternal("Rafi internal initialization only. Do not call tools or modify files. Reply briefly that the context is ready.");
|
|
337
|
+
const setup = await this.sendTurnInternal("Rafi internal initialization only. Do not call tools or modify files. Reply briefly that the context is ready.", { purpose: "initialization", responseOnly: true });
|
|
323
338
|
if (setup.isError)
|
|
324
339
|
throw new Error(`Codex automatic-compaction setup failed: ${setup.text.slice(0, 240)}`);
|
|
325
340
|
if (this.observedToolCalls !== toolsBefore)
|
|
@@ -457,8 +472,9 @@ export class CodexAdapter {
|
|
|
457
472
|
try {
|
|
458
473
|
result = await this.request(method, {
|
|
459
474
|
...(this._sessionId ? { threadId: this._sessionId } : {}), cwd: this.opts.cwd,
|
|
460
|
-
model: this.opts.model ?? null, approvalPolicy:
|
|
461
|
-
|
|
475
|
+
model: this.opts.model ?? null, approvalPolicy: this.effectiveApprovalPolicy(),
|
|
476
|
+
config: { sandbox_workspace_write: { network_access: this.effectiveNetworkAccess() } },
|
|
477
|
+
sandbox: this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa" ? "read-only" : "workspace-write",
|
|
462
478
|
developerInstructions: this.opts.systemPromptAppend ?? null,
|
|
463
479
|
});
|
|
464
480
|
}
|
|
@@ -474,6 +490,17 @@ export class CodexAdapter {
|
|
|
474
490
|
}
|
|
475
491
|
throw error;
|
|
476
492
|
}
|
|
493
|
+
const effective = result.sandbox;
|
|
494
|
+
if (this.effectiveNetworkAccess() && !effective)
|
|
495
|
+
throw new Error("Codex did not report an effective sandbox policy for the requested network grant; no turn dispatched");
|
|
496
|
+
const requestedSandbox = this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa" ? "readOnly" : "workspaceWrite";
|
|
497
|
+
if (effective && (effective.type !== requestedSandbox || effective.networkAccess === true && !this.effectiveNetworkAccess()))
|
|
498
|
+
throw new Error("Codex effective sandbox exceeds the authorized role policy; no turn dispatched");
|
|
499
|
+
if (effective && this.effectiveNetworkAccess() && effective.networkAccess !== true)
|
|
500
|
+
throw new Error("Codex did not enable the explicitly approved builder network; use an offline bundle or correct provider policy before implementation");
|
|
501
|
+
if (result.approvalPolicy !== undefined && result.approvalPolicy !== this.effectiveApprovalPolicy())
|
|
502
|
+
throw new Error("Codex effective approval policy differs from the requested role policy; no turn dispatched");
|
|
503
|
+
this.eventQueue.push({ kind: "activity", state: "Codex role capabilities", provider: "codex", detail: JSON.stringify({ requested: { sandbox: requestedSandbox, networkAccess: this.effectiveNetworkAccess(), approvalPolicy: this.effectiveApprovalPolicy() }, effective: effective ?? "unknown", approvalPolicy: result.approvalPolicy ?? "unknown" }) });
|
|
477
504
|
const thread = result.thread;
|
|
478
505
|
const returnedSessionId = String(thread?.id ?? this._sessionId ?? "") || undefined;
|
|
479
506
|
if (this._sessionRef && returnedSessionId !== this._sessionRef.sessionId) {
|
|
@@ -567,6 +594,10 @@ export class CodexAdapter {
|
|
|
567
594
|
}
|
|
568
595
|
handle(message) {
|
|
569
596
|
this.touchWaiters();
|
|
597
|
+
if (message.id !== undefined && message.method) {
|
|
598
|
+
void this.handleServerRequest(message);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
570
601
|
if (message.id !== undefined) {
|
|
571
602
|
const pending = this.pending.get(message.id);
|
|
572
603
|
if (pending) {
|
|
@@ -737,6 +768,11 @@ export class CodexAdapter {
|
|
|
737
768
|
if (timeoutMs !== undefined) {
|
|
738
769
|
const arm = () => {
|
|
739
770
|
timeout = setTimeout(() => {
|
|
771
|
+
// Human approval has its own bounded deadline; it is not provider idleness.
|
|
772
|
+
if (resetOnProviderActivity && this.approvalRequests.size > 0) {
|
|
773
|
+
arm();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
740
776
|
const active = this.notificationWaiters.get(method) ?? [];
|
|
741
777
|
this.notificationWaiters.set(method, active.filter((candidate) => candidate !== waiter));
|
|
742
778
|
clear();
|
|
@@ -765,7 +801,65 @@ export class CodexAdapter {
|
|
|
765
801
|
const configured = this.opts.providerIdleTimeoutMs;
|
|
766
802
|
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_PROVIDER_IDLE_TIMEOUT_MS;
|
|
767
803
|
}
|
|
804
|
+
effectiveNetworkAccess() {
|
|
805
|
+
return this.opts.sandboxMode !== "read-only" && this.opts.sessionRole !== "qa" && this.opts.networkAccess === true;
|
|
806
|
+
}
|
|
807
|
+
effectiveApprovalPolicy() {
|
|
808
|
+
return this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa" ? "never" : this.opts.approvalPolicy ?? "never";
|
|
809
|
+
}
|
|
810
|
+
/** Fail closed; approval is for this operation only, never a session cache or policy amendment. */
|
|
811
|
+
async handleServerRequest(message) {
|
|
812
|
+
await Promise.resolve();
|
|
813
|
+
const id = message.id;
|
|
814
|
+
const identity = `${typeof id}:${id}`;
|
|
815
|
+
if (this.approvalRequests.has(identity) || this.answeredApprovalRequests.has(identity))
|
|
816
|
+
return;
|
|
817
|
+
const params = message.params ?? {};
|
|
818
|
+
const command = message.method === "item/commandExecution/requestApproval";
|
|
819
|
+
const fileChange = message.method === "item/fileChange/requestApproval";
|
|
820
|
+
if (!command && !fileChange) {
|
|
821
|
+
this.write({ id, error: { code: -32601, message: `Unsupported server request: ${message.method}` } });
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
const connection = this.process;
|
|
825
|
+
const generation = this.connectionGeneration;
|
|
826
|
+
const hostTurnId = this.activeProviderTurnId;
|
|
827
|
+
const controller = new AbortController();
|
|
828
|
+
this.approvalRequests.set(identity, controller);
|
|
829
|
+
let decision = "decline";
|
|
830
|
+
try {
|
|
831
|
+
if (this.effectiveApprovalPolicy() === "on-request" && params.threadId === this._sessionId && this.activeProviderTurnId && typeof params.turnId === "string" && typeof params.itemId === "string" && params.turnId === this.activeNativeTurnId && (params.cwd === undefined || canonicalSessionPath(String(params.cwd)) === canonicalSessionPath(this.opts.cwd))) {
|
|
832
|
+
const verdict = await new OperationDeadline("Codex approval", this.opts.approvalTimeoutMs ?? 300_000).run(() => this.opts.permission({
|
|
833
|
+
toolName: command ? "Bash" : "Write",
|
|
834
|
+
input: { ...params, providerSandboxEscalation: true, requestedCommand: params.command },
|
|
835
|
+
signal: controller.signal, toolUseID: String(params.itemId),
|
|
836
|
+
title: "Approve this exact sandbox escalation?", description: String(params.reason ?? "Provider requests access beyond the offline workspace sandbox"),
|
|
837
|
+
}), () => controller.abort());
|
|
838
|
+
if (!controller.signal.aborted && verdict.behavior === "allow" && params.threadId === this._sessionId && this.activeProviderTurnId === hostTurnId && params.turnId === this.activeNativeTurnId)
|
|
839
|
+
decision = "accept";
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
controller.abort();
|
|
844
|
+
}
|
|
845
|
+
finally {
|
|
846
|
+
this.approvalRequests.delete(identity);
|
|
847
|
+
this.touchWaiters();
|
|
848
|
+
this.answeredApprovalRequests.add(identity);
|
|
849
|
+
if (generation === this.connectionGeneration && connection === this.process && !this.closed) {
|
|
850
|
+
try {
|
|
851
|
+
this.write({ id, result: { decision } });
|
|
852
|
+
}
|
|
853
|
+
catch { /* disconnected: no stale reply on another connection */ }
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
768
857
|
disconnect(error) {
|
|
858
|
+
this.connectionGeneration++;
|
|
859
|
+
for (const controller of this.approvalRequests.values())
|
|
860
|
+
controller.abort();
|
|
861
|
+
this.approvalRequests.clear();
|
|
862
|
+
this.answeredApprovalRequests.clear();
|
|
769
863
|
const child = this.process;
|
|
770
864
|
this.process = undefined;
|
|
771
865
|
this.initialized = false;
|
|
@@ -23,8 +23,17 @@ export declare class RecoveringAdapter implements BuilderAdapter {
|
|
|
23
23
|
private readonly eventQueue;
|
|
24
24
|
private eventPump;
|
|
25
25
|
private closed;
|
|
26
|
+
private contractEnforcing;
|
|
26
27
|
constructor(opts: RecoveringAdapterOptions);
|
|
28
|
+
graphRuntimeSettings(): Pick<import("./types.js").BuilderAdapterOptions, "model" | "effort" | "fast" | "runtimeExecutable"> | undefined;
|
|
27
29
|
get agent(): "claude" | "codex";
|
|
30
|
+
contractCapabilities(): {
|
|
31
|
+
sameSessionAcceptance: boolean;
|
|
32
|
+
nativeCompactionBarrier: boolean;
|
|
33
|
+
};
|
|
34
|
+
enableContractEnforcement(): void;
|
|
35
|
+
contractCompactionSequence(): number;
|
|
36
|
+
acceptContractDelivery(sequence: number): void;
|
|
28
37
|
sendTurn(text: string, policy?: Parameters<BuilderAdapter["sendTurn"]>[1]): Promise<TurnResult>;
|
|
29
38
|
sessionId(): string | undefined;
|
|
30
39
|
sessionRef(): ProviderSessionRefV1 | undefined;
|
|
@@ -10,15 +10,21 @@ export class RecoveringAdapter {
|
|
|
10
10
|
eventQueue = new AsyncQueue();
|
|
11
11
|
eventPump;
|
|
12
12
|
closed = false;
|
|
13
|
+
contractEnforcing = false;
|
|
13
14
|
constructor(opts) {
|
|
14
15
|
this.opts = opts;
|
|
15
16
|
this.adapter = opts.initial;
|
|
16
17
|
this.runtime = opts.runtime;
|
|
17
18
|
this.eventPump = this.forwardEvents(this.adapter);
|
|
18
19
|
}
|
|
20
|
+
graphRuntimeSettings() { return this.adapter.graphRuntimeSettings?.(); }
|
|
19
21
|
get agent() {
|
|
20
22
|
return this.adapter.agent;
|
|
21
23
|
}
|
|
24
|
+
contractCapabilities() { return this.adapter.contractCapabilities?.() ?? { sameSessionAcceptance: false, nativeCompactionBarrier: false }; }
|
|
25
|
+
enableContractEnforcement() { this.contractEnforcing = true; this.adapter.enableContractEnforcement?.(); }
|
|
26
|
+
contractCompactionSequence() { return this.adapter.contractCompactionSequence?.() ?? 0; }
|
|
27
|
+
acceptContractDelivery(sequence) { this.adapter.acceptContractDelivery?.(sequence); }
|
|
22
28
|
async sendTurn(text, policy) {
|
|
23
29
|
while (true) {
|
|
24
30
|
const result = await this.adapter.sendTurn(text, policy);
|
|
@@ -37,6 +43,10 @@ export class RecoveringAdapter {
|
|
|
37
43
|
// instruction. Never replay it or silently switch providers.
|
|
38
44
|
if (result.failure.category === "session-unavailable" || result.failure.dispatchState === "unknown")
|
|
39
45
|
return result;
|
|
46
|
+
// A replacement has new host-side barriers, even when it resumes the
|
|
47
|
+
// same native session. Only the shared build gate can renew acceptance.
|
|
48
|
+
if (this.contractEnforcing)
|
|
49
|
+
return { ...result, text: `${result.text}\nContract-enforcing runtime recovery requires an explicit host resume through the shared preparation/delivery gate; this turn was not replayed.` };
|
|
40
50
|
const otherRuntime = this.runtime === "claude" ? "codex" : "claude";
|
|
41
51
|
const choice = this.opts.choose
|
|
42
52
|
? await this.opts.choose(result, this.runtime, otherRuntime)
|