jeo-code 0.12.26 → 0.12.27
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/CHANGELOG.md +10 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/agent/AGENTS.md +3 -2
- package/src/agent/engine.ts +82 -29
- package/src/agent/loop-guards.ts +366 -42
- package/src/agent/sandbox/docker.ts +15 -1
- package/src/agent/sandbox/hands-source.text +705 -705
- package/src/agent/snapshot-target.ts +188 -0
- package/src/agent/workspace-snapshot.ts +84 -19
- package/src/ai/types.ts +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,16 @@ The README mirrors the latest 5 entries — regenerate with `bun run changelog:s
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.12.27] - 2026-11-06
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **A stale managed container no longer blocks every sandbox launch** - `validateManagedRoots` inspects every labelled container, including exited ones, and canonicalized each bind source. A leaked container whose temp workspace had been deleted made `realpath` throw `ENOENT`, so every later launch failed with that opaque error. A vanished source (`ENOENT`, `ENOTDIR`) now resolves through its nearest existing ancestor (lexically for `ENOTDIR`) for the overlap check, which stays enforced; `EACCES`, `ELOOP` and invalid metadata still fail closed.
|
|
16
|
+
- **An unlistable directory or an unhashable fresh file no longer makes the whole workspace snapshot inconclusive** - a non-root directory that cannot be listed is recorded as `u:<rel>:<mode>:<ctimeNs>:<ino>` and its children skipped (a `chmod` or any entry change in it still flips the digest); a fresh file over the racy byte caps, unopenable or changed while read is recorded by its stat fingerprint plus `:x`. Both count in the new `degraded` field. An unreadable root and the entry and time caps still fail the snapshot. Residual risk, documented in the header: rewriting an existing child's content inside a write-only directory with no `chmod` and no entry change is not detected.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **The done gate recognizes far more real verification commands** - `parseVerifier` accepts `bash -c`, `time`, quoted arguments, `cd "x y" &&`, package-manager flags (`pnpm -r`, `--filter`, `--prefix`, `--cwd`, `yarn workspace`), `make ci|verify`, gradle, mvn, dotnet, rspec, phpunit, playwright and the silent linters ruff, mypy, flake8, golangci-lint, staticcheck, `prettier --check` and `cargo fmt --check`. Direct forms are trusted on tool success. Weak forms whose exit status is not the verifier's own (`| tail`, bare `| tee`, `|| true`, `; echo x`, `set -e;`, `cd x;`, a subshell) count only for test runners, and only with a positive pass summary, no failure marker anywhere in the output and the trailing `echo` text discounted. `make`, `mvn`, `gradle`, `rake` and `dotnet` accept only allow-listed targets, so `make ci deploy` is not a verifier. A direct run that reports zero tests (`0 pass`, `# pass 0`, `Tests run: 0`, `collected 0 items`) is no longer evidence.
|
|
20
|
+
- **A shell call from a shared scratch directory is bracketed on the project it works in** - from `$HOME`, `/` or a system temp dir, `cd <project> && sed -i ...`, or a `cwd`/`subdir` argument naming a project, is measured on the project root (the outermost directory with a `.git`, `package.json`, `Cargo.toml`, ... marker below any scratch root, so `cd app/pkg && echo x > ../src/a.ts` measures `app`) instead of being skipped, so a real unverified edit made that way blocks `done`. If that root cannot be measured BEFORE the call (over the entry or time cap), the call is skipped with one notice, as before, so a big or busy directory such as `/opt/homebrew` never blocks a read-only turn; if it is measurable before but not after, the call counts as a change. Unmeasured by design: a project over the 50k-entry cap (or one inflated earlier in the turn by a call that was not bracketed, such as a recognized `make build`), and a small project nested under a larger marker root. The transcript records `workspace.target` (relative to the cwd, `<outside cwd>` otherwise).
|
|
21
|
+
|
|
12
22
|
## [0.12.26] - 2026-11-06
|
|
13
23
|
|
|
14
24
|
### Fixed
|
package/README.ja.md
CHANGED
|
@@ -462,10 +462,10 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
462
462
|
|
|
463
463
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
464
464
|
- **[Unreleased]**
|
|
465
|
+
- **[0.12.27]** (2026-11-06)
|
|
465
466
|
- **[0.12.26]** (2026-11-06)
|
|
466
467
|
- **[0.12.25]** (2026-11-06)
|
|
467
468
|
- **[0.12.24]** (2026-11-06)
|
|
468
|
-
- **[0.12.23]** (2026-11-06)
|
|
469
469
|
|
|
470
470
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
471
471
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -517,10 +517,10 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
517
517
|
|
|
518
518
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
519
519
|
- **[Unreleased]**
|
|
520
|
+
- **[0.12.27]** (2026-11-06)
|
|
520
521
|
- **[0.12.26]** (2026-11-06)
|
|
521
522
|
- **[0.12.25]** (2026-11-06)
|
|
522
523
|
- **[0.12.24]** (2026-11-06)
|
|
523
|
-
- **[0.12.23]** (2026-11-06)
|
|
524
524
|
|
|
525
525
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
526
526
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -506,10 +506,10 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
506
506
|
|
|
507
507
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
508
508
|
- **[Unreleased]**
|
|
509
|
+
- **[0.12.27]** (2026-11-06)
|
|
509
510
|
- **[0.12.26]** (2026-11-06)
|
|
510
511
|
- **[0.12.25]** (2026-11-06)
|
|
511
512
|
- **[0.12.24]** (2026-11-06)
|
|
512
|
-
- **[0.12.23]** (2026-11-06)
|
|
513
513
|
|
|
514
514
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
515
515
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -460,10 +460,10 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
460
460
|
|
|
461
461
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
462
462
|
- **[Unreleased]**
|
|
463
|
+
- **[0.12.27]** (2026-11-06)
|
|
463
464
|
- **[0.12.26]** (2026-11-06)
|
|
464
465
|
- **[0.12.25]** (2026-11-06)
|
|
465
466
|
- **[0.12.24]** (2026-11-06)
|
|
466
|
-
- **[0.12.23]** (2026-11-06)
|
|
467
467
|
|
|
468
468
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
469
469
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/agent/AGENTS.md
CHANGED
|
@@ -29,8 +29,9 @@ The core runtime loop, tool registry, session management, and state persistence
|
|
|
29
29
|
| `hooks.ts` | Command-hook runtime: `loadHooks` (user registry `~/.jeo/hooks.json` ⊕ `config.json`/project `.jeo/hooks.json`, with the `hooks.enabled=false` kill switch and project `enabled:false` opt-out; the registry is skipped in sandboxed sessions), `runPreToolHooks` (veto by exit code or structured `permissionDecision`, `additionalContext` passthrough), `runPostTurnHooksForBatch`, `runPostImplementationHooks`, and the per-turn `runPrePromptHooks` (context injected by `commands/launch.ts` into the prompt message) / `runTurnEndHooks` (final reply to ingesters); payloads follow the Claude hook protocol on the host (`hook_event_name`, `session_id`, `cwd`, `tool_name`/`tool_input`) and stay legacy-shaped in a sandbox |
|
|
30
30
|
| `json.ts` | Brief description of purpose |
|
|
31
31
|
| `loop.ts` | The primary execution loop orchestrating model calls and tool execution |
|
|
32
|
-
| `loop-guards.ts` | Intermediate-judgment classification (gjc ultragoal-guard parity): named `GuardState` taxonomy, `GUARD_LIMITS` thresholds, and pure classifiers (`verifierCores`/`isVerificationSignal` — recognizes wrapped verifiers such as `cd x && bun test`, env/`timeout`/`uv run`/`rtk` prefixes and `2>&1`,
|
|
33
|
-
| `workspace-snapshot.ts` | Host-side workspace fingerprint (`snapshotWorkspace`, `defaultWorkspaceSnapshotter`, `workspaceSnapshotChanged`): bounded lstat walk excluding `.git`/`.jeo`/`node_modules`, racy content hash for files modified near the bracket anchor; the engine brackets non-verifier `bash` and state-affecting `computer` calls with it so only a real workspace change counts as a mutation (snapshot error or cap counts as a mutation); an ok snapshot keeps a relative-path fingerprint map (`snapshotChangedPaths`/`formatSnapshotChange` name what changed) and `isSharedScratchRoot` marks `$HOME`, `/` and system temp dirs where `bash`/`computer` calls are not bracketed |
|
|
32
|
+
| `loop-guards.ts` | Intermediate-judgment classification (gjc ultragoal-guard parity): named `GuardState` taxonomy, `GUARD_LIMITS` thresholds, and pure classifiers (`verifierCores`/`isVerificationSignal` — `parseVerifier` recognizes wrapped verifiers such as `cd x && bun test`, `bash -c`, env/`timeout`/`time`/`uv run`/`rtk` prefixes and `2>&1`; direct forms are trusted on tool success, while weak forms whose exit status is not the verifier's own (`| tail`, bare `| tee`, `|| true`, `; echo x`, `set -e;`, `cd x;`, a subshell) count only for test runners and only with a positive pass summary, no failure marker anywhere in the output and the trailing `echo` text discounted; `make`/`mvn`/`gradle`/`rake` accept only allow-listed targets — `repeatHint`, `classifyDoneGate` incl. `done_stale_verification`, and `formatRejectedCompletion`, which keeps the last candidate answer under an `UNVERIFIED` marker when corrections are exhausted) consumed by `engine.ts` |
|
|
33
|
+
| `workspace-snapshot.ts` | Host-side workspace fingerprint (`snapshotWorkspace`, `defaultWorkspaceSnapshotter`, `workspaceSnapshotChanged`): bounded lstat walk excluding `.git`/`.jeo`/`node_modules`, racy content hash for files modified near the bracket anchor; the engine brackets non-verifier `bash` and state-affecting `computer` calls with it so only a real workspace change counts as a mutation (snapshot error or cap counts as a mutation); an unlistable non-root directory (`u:<rel>:<mode>:<ctimeNs>:<ino>`) or a racy file that cannot be hashed (stat fingerprint plus `:x`) degrades to a marker and is counted in `degraded` instead of failing the snapshot; an ok snapshot keeps a relative-path fingerprint map (`snapshotChangedPaths`/`formatSnapshotChange` name what changed) and `isSharedScratchRoot` marks `$HOME`, `/` and system temp dirs where `bash`/`computer` calls are not bracketed unless `snapshot-target.ts` finds a nested project |
|
|
34
|
+
| `snapshot-target.ts` | `snapshotTarget`/`describeSnapshotTarget`: for a `bash` call from a shared scratch cwd, resolves the project directory the call works in (`args.cwd`/`args.subdir`, then a leading literal `cd <path> &&`/`;`, up to 8 hops) and returns it only when it has a project marker (`.git`, `package.json`, `Cargo.toml`, …) below any scratch root; the engine brackets that directory and fails OPEN (skipped, one notice) when it cannot be measured, so a big or busy directory never blocks a turn |
|
|
34
35
|
| `memory.ts` | OKF concept-bundle memory: session distill, query-aware budget injection with a **hybrid reranker ported from memsearch** — `priorityOrder` fuses two ranked channels via `reciprocalRankFusion` (RRF, k=60): lexical IDF relevance (`buildCorpusStats`/`scoreConcept`, the sparse/BM25 channel so rare discriminating terms steer retrieval) ⊕ graph proximity (`graphProximityOrder`, the local dense/semantic-neighbour channel — a hub linked from multiple query hits surfaces even with no keyword of its own); all embedding-free, deterministic, atop the failure-first tier + pinned-invariant reserved budget. Plus mid-loop `recordFailedAttempt` capture, shared `upsertConceptFile` writer, legacy MEMORY.md migration (`migrateLegacyMemory`) + `JEO_MEMORY_LEGACY` rollback toggle |
|
|
35
36
|
| `memory-okf.ts` | OKF v0.1 format layer: frontmatter parse/serialize, concept IDs, conformance validation |
|
|
36
37
|
| `memory-graph.ts` | Concept cross-link graph: build/expand (1-hop search), broken-link-tolerant lint, optional graphify detection |
|
package/src/agent/engine.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
defaultWorkspaceSnapshotter, formatSnapshotChange, isSharedScratchRoot, snapshotChangedPaths, workspaceSnapshotChanged,
|
|
38
38
|
type WorkspaceSnapshot, type WorkspaceSnapshotter,
|
|
39
39
|
} from "./workspace-snapshot";
|
|
40
|
+
import { describeSnapshotTarget, snapshotTarget, snapshotTargetLabel } from "./snapshot-target";
|
|
40
41
|
import { stripLeakedReasoningTags } from "../ai/think-tags";
|
|
41
42
|
|
|
42
43
|
const MID_TASK_PROSE_RE =
|
|
@@ -552,9 +553,13 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
552
553
|
}
|
|
553
554
|
const tools = opts.tools ?? DEFAULT_TOOLS;
|
|
554
555
|
const snapshotter: WorkspaceSnapshotter = opts.workspaceSnapshot ?? defaultWorkspaceSnapshotter;
|
|
555
|
-
// Turn-scoped memo
|
|
556
|
-
//
|
|
557
|
-
|
|
556
|
+
// Turn-scoped memo, keyed per bracketed directory, so one oversized directory never poisons the measurement of the
|
|
557
|
+
// cwd or of another target. For the cwd any cap is remembered (gated calls count as mutations without re-walking a
|
|
558
|
+
// tree known to be too large). For a NESTED target only a cap seen on the BEFORE side is remembered: that directory
|
|
559
|
+
// was already unmeasurable before the call ran, so nothing in it is attributable. An AFTER-side cap is never
|
|
560
|
+
// remembered, because it may have been this very call's files that pushed the tree over the cap — a later small
|
|
561
|
+
// write in the same directory must still be judged on its own, not skipped by that memo.
|
|
562
|
+
const cappedDirs = new Set<string>();
|
|
558
563
|
// Why a bash/computer bracket could not be measured (cap or error), when one failed. The call still
|
|
559
564
|
// counts as a possible mutation, but the user and the done gate are told the real reason instead of
|
|
560
565
|
// only "files were modified" — a busy or oversized cwd (e.g. $HOME, /tmp) looks identical otherwise.
|
|
@@ -569,29 +574,49 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
569
574
|
}
|
|
570
575
|
return snap;
|
|
571
576
|
};
|
|
572
|
-
|
|
573
|
-
|
|
577
|
+
// A BEFORE-side failure on a NESTED target is returned as-is (not noted as unmeasured): the bracket then fails open
|
|
578
|
+
// for it, because before nested targeting those calls were skipped as scratch, and a read-only `cd ~/.cache && ls`
|
|
579
|
+
// must not start counting as a mutation just because that directory is big or busy. An AFTER-side failure is noted
|
|
580
|
+
// like the cwd's: the directory WAS measurable a moment ago, so files appeared during the call (fail-safe).
|
|
581
|
+
// Exception: a directory this turn already made unmeasurable (`grownDirs`, an after-side failure with a good before)
|
|
582
|
+
// never fails open again — it was this session's own writes that inflated it, so a later call there (even after a
|
|
583
|
+
// passing verification) must not be waved through just because the tree is now too big to measure.
|
|
584
|
+
const grownDirs = new Set<string>();
|
|
585
|
+
const takeSnapshot = async (dir: string, side: "before" | "after", racySinceMs?: number): Promise<WorkspaceSnapshot> => {
|
|
586
|
+
const nested = dir !== cwd;
|
|
587
|
+
const failOpen = nested && side === "before" && !grownDirs.has(dir);
|
|
588
|
+
const memoApplies = !nested || side === "before";
|
|
589
|
+
if (memoApplies && cappedDirs.has(dir)) {
|
|
590
|
+
const memo: WorkspaceSnapshot = { ok: false, reason: "cap", detail: "workspace exceeded the snapshot cap earlier this turn" };
|
|
591
|
+
return nested && !failOpen ? noteUnmeasured(memo) : memo;
|
|
592
|
+
}
|
|
574
593
|
try {
|
|
575
|
-
const snap = await snapshotter(
|
|
576
|
-
if (!snap.ok && snap.reason === "cap")
|
|
577
|
-
return noteUnmeasured(snap);
|
|
594
|
+
const snap = await snapshotter(dir, racySinceMs);
|
|
595
|
+
if (memoApplies && !snap.ok && snap.reason === "cap") cappedDirs.add(dir);
|
|
596
|
+
return failOpen ? snap : noteUnmeasured(snap);
|
|
578
597
|
} catch (err) {
|
|
579
|
-
|
|
598
|
+
const failure: WorkspaceSnapshot = { ok: false, reason: "error", detail: err instanceof Error ? err.message : String(err) };
|
|
599
|
+
return failOpen ? failure : noteUnmeasured(failure);
|
|
580
600
|
}
|
|
581
601
|
};
|
|
602
|
+
const unmeasurableNested = new Set<string>();
|
|
582
603
|
// bash/computer calls whose mutation status is decided by a workspace diff. Recognized
|
|
583
604
|
// verifiers are never mutations, and observe-only computer actions are read-only.
|
|
584
605
|
// A shared scratch root ($HOME, /, the system temp dir) is written by unrelated processes (daemon heartbeats,
|
|
585
606
|
// logs, other agents) and holds no project verifier, so a snapshot diff there cannot be attributed to one shell
|
|
586
607
|
// call: bash and computer calls are not bracketed (a built-in write/edit still counts as a definite mutation).
|
|
587
|
-
//
|
|
608
|
+
// The one exception is a bash call whose literal target (`cd ~/work/app && …`, a `cwd`/`subdir` argument) is a
|
|
609
|
+
// project (it, or an ancestor below the scratch root, holds a marker such as `.git`/`package.json`) nested under
|
|
610
|
+
// the scratch cwd: `snapshotTarget` names the outermost such project root and the bracket measures THAT directory,
|
|
611
|
+
// so an edit made through a scratch cwd (even from a subdirectory, `cd app/pkg && echo X > ../src/a.ts`) is not
|
|
612
|
+
// invisible. If that root was already unmeasurable BEFORE the call it is skipped like scratch (fail open); one that
|
|
613
|
+
// becomes unmeasurable DURING the call counts as a mutation. A project directory used as cwd stays gated as
|
|
614
|
+
// before, and there an unmeasurable bracket counts as a mutation.
|
|
588
615
|
const scratchCwd = isSharedScratchRoot(cwd, opts.scratchRoots);
|
|
589
616
|
let scratchNoted = false;
|
|
590
617
|
const snapshotCandidate = (tool: string, args: Record<string, any> | undefined): boolean =>
|
|
591
618
|
(tool === "bash" && !isVerificationSignal(String(args?.command ?? args?.cmd ?? ""))) ||
|
|
592
619
|
(tool === "computer" && computerActionMayAffectState(args));
|
|
593
|
-
const snapshotGated = (tool: string, args: Record<string, any> | undefined): boolean =>
|
|
594
|
-
!scratchCwd && snapshotCandidate(tool, args);
|
|
595
620
|
// Mutation evidence that came ONLY from a shell/computer snapshot bracket, kept apart from definite mutators
|
|
596
621
|
// (write/edit/eval/…): an unrelated writer in the cwd is indistinguishable from the command's own writes, so a
|
|
597
622
|
// shell-only block gets one correction (naming the paths) instead of the full budget.
|
|
@@ -1430,15 +1455,23 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
1430
1455
|
success = false;
|
|
1431
1456
|
output = preHookResult.error + (preHookResult.output ? `\n${preHookResult.output}` : "");
|
|
1432
1457
|
} else {
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1458
|
+
// Which directory this call's bracket measures. Undefined = skip: a scratch cwd with no resolvable
|
|
1459
|
+
// nested project target (`snapshotTarget` returns `cwd` itself exactly when it found none).
|
|
1460
|
+
let bracketDir: string | undefined;
|
|
1461
|
+
if (snapshotCandidate(tool, args)) {
|
|
1462
|
+
const dir = scratchCwd ? snapshotTarget(cwd, tool, args, opts.scratchRoots) : cwd;
|
|
1463
|
+
if (scratchCwd && dir === cwd) {
|
|
1464
|
+
workspace = { mutation: false, skipped: "scratch" };
|
|
1465
|
+
if (!scratchNoted) {
|
|
1466
|
+
scratchNoted = true;
|
|
1467
|
+
ev.onNotice?.(`cwd ${cwd} is a shared scratch directory: shell and computer calls here are not checked for file changes`);
|
|
1468
|
+
}
|
|
1469
|
+
} else {
|
|
1470
|
+
bracketDir = dir;
|
|
1438
1471
|
}
|
|
1439
1472
|
}
|
|
1440
1473
|
// Taken after pre-tool hooks so hook writes are never attributed to the call.
|
|
1441
|
-
const before =
|
|
1474
|
+
const before = bracketDir !== undefined ? await takeSnapshot(bracketDir, "before") : undefined;
|
|
1442
1475
|
try {
|
|
1443
1476
|
const onProgress = ev.onToolProgress ? (partial: string) => ev.onToolProgress!(tool, partial) : undefined;
|
|
1444
1477
|
const sandboxedHandler = sandbox && !Object.hasOwn(parentTools, tool) && !SANDBOX_MEDIATED_TOOLS.has(handler)
|
|
@@ -1463,20 +1496,36 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
1463
1496
|
((tool === "monitor" || tool === "job") && String(args?.action ?? "").trim().toLowerCase() === "start");
|
|
1464
1497
|
if (explicitSignal || knownMutatingCall) mutationMayHaveOccurred = true;
|
|
1465
1498
|
}
|
|
1466
|
-
if (before) {
|
|
1499
|
+
if (before && bracketDir !== undefined) {
|
|
1467
1500
|
// bash/computer: the workspace diff is the sole mutation authority (overrides
|
|
1468
1501
|
// handler flags and the catch heuristic). Both sides share one racy anchor so
|
|
1469
1502
|
// an unchanged recently-modified file is hashed on both sides or neither.
|
|
1470
|
-
const after = before.ok ? await takeSnapshot(before.startedAtMs) : undefined;
|
|
1471
|
-
mutationMayHaveOccurred = after ? workspaceSnapshotChanged(before, after) : true;
|
|
1472
|
-
bracketed = true;
|
|
1503
|
+
const after = before.ok ? await takeSnapshot(bracketDir, "after", before.startedAtMs) : undefined;
|
|
1473
1504
|
const failed = !before.ok ? before : after && !after.ok ? after : undefined;
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1505
|
+
const nested = bracketDir !== cwd;
|
|
1506
|
+
if (nested && after && !after.ok) grownDirs.add(bracketDir);
|
|
1507
|
+
if (nested && !before.ok && !grownDirs.has(bracketDir)) {
|
|
1508
|
+
// Fail open ONLY when the nested target was already unmeasurable BEFORE the call (cap, time cap,
|
|
1509
|
+
// error): nothing in it is attributable, and it behaves like today's scratch skip so a big read-only
|
|
1510
|
+
// directory cannot block a finished answer. A failing AFTER snapshot (before was ok) falls through
|
|
1511
|
+
// below and counts as a mutation: files appeared during the call.
|
|
1512
|
+
workspace = { mutation: false, skipped: "scratch", target: describeSnapshotTarget(cwd, bracketDir) };
|
|
1513
|
+
if (!unmeasurableNested.has(bracketDir)) {
|
|
1514
|
+
unmeasurableNested.add(bracketDir);
|
|
1515
|
+
ev.onNotice?.(`nested target ${snapshotTargetLabel(cwd, bracketDir)} could not be measured; treated as scratch`);
|
|
1516
|
+
}
|
|
1517
|
+
} else {
|
|
1518
|
+
mutationMayHaveOccurred = after ? workspaceSnapshotChanged(before, after) : true;
|
|
1519
|
+
bracketed = true;
|
|
1520
|
+
const change = after ? snapshotChangedPaths(before, after) : null;
|
|
1521
|
+
workspace = {
|
|
1522
|
+
mutation: mutationMayHaveOccurred,
|
|
1523
|
+
// Paths stay relative to the bracketed directory; `target` says which directory that was.
|
|
1524
|
+
...(mutationMayHaveOccurred && change ? { changed: formatSnapshotChange(change) } : {}),
|
|
1525
|
+
...(failed && !failed.ok ? { inconclusive: `${failed.reason}: ${failed.detail}` } : {}),
|
|
1526
|
+
...(nested ? { target: describeSnapshotTarget(cwd, bracketDir) } : {}),
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1480
1529
|
}
|
|
1481
1530
|
// A pre-tool hook's structured `additionalContext` rides along with the result
|
|
1482
1531
|
// so the model sees it next to the output it concerns.
|
|
@@ -1699,7 +1748,11 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
1699
1748
|
sawMutation = true;
|
|
1700
1749
|
lastMutationSeq = ++mutVerSeq;
|
|
1701
1750
|
if (results[i].bracketed !== true) definiteMutation = true;
|
|
1702
|
-
else if (results[i].workspace?.changed)
|
|
1751
|
+
else if (results[i].workspace?.changed) {
|
|
1752
|
+
const w = results[i].workspace!;
|
|
1753
|
+
// A nested bracket's paths are relative to that directory, so the gate message names it.
|
|
1754
|
+
shellChanges.push(w.target ? `${w.changed} (in ${w.target})` : w.changed!);
|
|
1755
|
+
}
|
|
1703
1756
|
}
|
|
1704
1757
|
} else if (t === "debug" && MUTATING_DEBUG_ACTIONS[String(toolCalls[i].arguments?.action ?? "")] === true) {
|
|
1705
1758
|
sawMutation = true;
|