jeo-code 0.6.12 → 0.6.14
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 +17 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +2 -1
- package/src/agent/memory.ts +66 -50
- package/src/ai/model-manager.ts +14 -3
- package/src/cli/runner.ts +2 -1
- package/src/commands/team.ts +41 -20
- package/src/util/retry.ts +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
8
|
|
|
9
|
+
## [0.6.14] - 2026-06-16
|
|
10
|
+
_Memory distillation survives malformed model output, and stream-idle stalls retry instead of failing the turn._
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Malformed `concepts` arrays no longer discard the whole distillation batch.** A text-only / small model can emit stray non-object array elements (`null`, strings, numbers) or non-string `type`/`title` fields. Each element is now validated and its persistence wrapped in a per-concept `try`/`catch`, so one bad concept is skipped instead of throwing out of the loop into the outer catch — which previously silently dropped every valid learning distilled in that run. Junk frontmatter fields (`description`/`tags`/`body`/`confidence`/`links`) are coerced to safe defaults so the written file stays OKF-conformant.
|
|
14
|
+
- **Per-chunk stream-idle stalls now retry instead of failing the turn.** A `stream idle for <ms>ms (no chunk)` stall (provider load or long time-to-first-token) is treated as transient and retried like a timeout, while the hard overall wall-clock cap (`stream exceeded the overall deadline`) still fails fast. The idle-stall error message now explains the cause and remediation.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **`JEO_STREAM_IDLE_MS` opt-in override.** Reasoning workloads whose "thinking" phase can legitimately emit no visible token for longer than the 120s default can raise the per-chunk idle threshold without a code change.
|
|
18
|
+
|
|
19
|
+
## [0.6.13] - 2026-06-16
|
|
20
|
+
_`team` engine: concrete uncommitted-work reporting and stricter empty-run handling._
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- **`team` re-runs report concrete uncommitted work.** Instead of a speculative warning, the engine now probes the working tree with `git status --porcelain` and reports the actual uncommitted-file count, so you know whether real partial work is present before re-running on it.
|
|
24
|
+
- **`--strict-mutations` fails a no-op mutating run.** A mutating role that performed no write/edit/bash is now a hard failure (`stream:error`) rather than silently passing; a bash-only run stays an advisory `stream:warn` (new tone) so a passing advisory doesn't masquerade as an error.
|
|
25
|
+
|
|
9
26
|
## [0.6.12] - 2026-06-16
|
|
10
27
|
_OKF-backed memory distillation — session learnings become structured concept files._
|
|
11
28
|
|
package/README.ja.md
CHANGED
|
@@ -158,11 +158,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
158
158
|
## 変更履歴 (Changelog)
|
|
159
159
|
|
|
160
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.14]** (2026-06-16) — Memory distillation survives malformed model output, and stream-idle stalls retry instead of failing the turn.
|
|
162
|
+
- **[0.6.13]** (2026-06-16) — `team` engine: concrete uncommitted-work reporting and stricter empty-run handling.
|
|
161
163
|
- **[0.6.12]** (2026-06-16) — OKF-backed memory distillation — session learnings become structured concept files.
|
|
162
164
|
- **[0.6.11]** (2026-06-16) — Larger reasoning budgets, and terminal capability-response sequences kept out of the prompt.
|
|
163
165
|
- **[0.6.10]** (2026-06-16) — OKF memory-format foundation and a hardened bashTool subprocess drain.
|
|
164
|
-
- **[0.6.9]** (2026-06-16) — Live streaming blocks size to their content and the viewport instead of a fixed rectangle.
|
|
165
|
-
- **[0.6.8]** (2026-06-16) — OAuth loopback callback host pinned to `localhost` to match provider-registered redirect URIs.
|
|
166
166
|
|
|
167
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
168
168
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -158,11 +158,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
158
158
|
## 변경 이력 (Changelog)
|
|
159
159
|
|
|
160
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.14]** (2026-06-16) — Memory distillation survives malformed model output, and stream-idle stalls retry instead of failing the turn.
|
|
162
|
+
- **[0.6.13]** (2026-06-16) — `team` engine: concrete uncommitted-work reporting and stricter empty-run handling.
|
|
161
163
|
- **[0.6.12]** (2026-06-16) — OKF-backed memory distillation — session learnings become structured concept files.
|
|
162
164
|
- **[0.6.11]** (2026-06-16) — Larger reasoning budgets, and terminal capability-response sequences kept out of the prompt.
|
|
163
165
|
- **[0.6.10]** (2026-06-16) — OKF memory-format foundation and a hardened bashTool subprocess drain.
|
|
164
|
-
- **[0.6.9]** (2026-06-16) — Live streaming blocks size to their content and the viewport instead of a fixed rectangle.
|
|
165
|
-
- **[0.6.8]** (2026-06-16) — OAuth loopback callback host pinned to `localhost` to match provider-registered redirect URIs.
|
|
166
166
|
|
|
167
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
168
168
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -158,11 +158,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
|
|
|
158
158
|
## Changelog
|
|
159
159
|
|
|
160
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.14]** (2026-06-16) — Memory distillation survives malformed model output, and stream-idle stalls retry instead of failing the turn.
|
|
162
|
+
- **[0.6.13]** (2026-06-16) — `team` engine: concrete uncommitted-work reporting and stricter empty-run handling.
|
|
161
163
|
- **[0.6.12]** (2026-06-16) — OKF-backed memory distillation — session learnings become structured concept files.
|
|
162
164
|
- **[0.6.11]** (2026-06-16) — Larger reasoning budgets, and terminal capability-response sequences kept out of the prompt.
|
|
163
165
|
- **[0.6.10]** (2026-06-16) — OKF memory-format foundation and a hardened bashTool subprocess drain.
|
|
164
|
-
- **[0.6.9]** (2026-06-16) — Live streaming blocks size to their content and the viewport instead of a fixed rectangle.
|
|
165
|
-
- **[0.6.8]** (2026-06-16) — OAuth loopback callback host pinned to `localhost` to match provider-registered redirect URIs.
|
|
166
166
|
|
|
167
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
168
168
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -158,11 +158,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
158
158
|
## 更新日志 (Changelog)
|
|
159
159
|
|
|
160
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.14]** (2026-06-16) — Memory distillation survives malformed model output, and stream-idle stalls retry instead of failing the turn.
|
|
162
|
+
- **[0.6.13]** (2026-06-16) — `team` engine: concrete uncommitted-work reporting and stricter empty-run handling.
|
|
161
163
|
- **[0.6.12]** (2026-06-16) — OKF-backed memory distillation — session learnings become structured concept files.
|
|
162
164
|
- **[0.6.11]** (2026-06-16) — Larger reasoning budgets, and terminal capability-response sequences kept out of the prompt.
|
|
163
165
|
- **[0.6.10]** (2026-06-16) — OKF memory-format foundation and a hardened bashTool subprocess drain.
|
|
164
|
-
- **[0.6.9]** (2026-06-16) — Live streaming blocks size to their content and the viewport instead of a fixed rectangle.
|
|
165
|
-
- **[0.6.8]** (2026-06-16) — OAuth loopback callback host pinned to `localhost` to match provider-registered redirect URIs.
|
|
166
166
|
|
|
167
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
168
168
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/agent/memory.ts
CHANGED
|
@@ -302,60 +302,76 @@ export async function distillSessionMemory(
|
|
|
302
302
|
await fs.mkdir(bundleDir, { recursive: true });
|
|
303
303
|
const updatedConcepts: { title: string; type: string }[] = [];
|
|
304
304
|
|
|
305
|
-
for (const
|
|
306
|
-
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
305
|
+
for (const raw of parsedJson.concepts) {
|
|
306
|
+
// A text-only / small model (the default antigravity backend) can emit
|
|
307
|
+
// stray non-object array elements (null, strings, numbers) or non-string
|
|
308
|
+
// type/title fields. Validate each element and isolate per-concept failures:
|
|
309
|
+
// one malformed concept must NEVER throw out of the loop, because the outer
|
|
310
|
+
// catch would then discard every valid learning distilled in this run.
|
|
311
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
312
|
+
const concept = raw as {
|
|
313
|
+
type?: unknown; title?: unknown; description?: unknown; body?: unknown;
|
|
314
|
+
tags?: unknown; confidence?: unknown; links?: unknown;
|
|
315
|
+
};
|
|
316
|
+
const type = typeof concept.type === "string" ? concept.type.trim() : "";
|
|
317
|
+
const title = typeof concept.title === "string" ? concept.title.trim() : "";
|
|
318
|
+
if (!type || !title) continue;
|
|
319
|
+
try {
|
|
320
|
+
// Unknown types fall back to facts/ (lenient — OKF tolerates extra types).
|
|
321
|
+
const dir = DIR_BY_TYPE[type] ?? "facts";
|
|
322
|
+
|
|
323
|
+
const targetDir = path.join(bundleDir, dir);
|
|
324
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
325
|
+
|
|
326
|
+
let slug = slugify(title);
|
|
327
|
+
let relPath = `${dir}/${slug}.md`;
|
|
328
|
+
let fullPath = path.join(bundleDir, relPath);
|
|
329
|
+
|
|
330
|
+
let suffix = 1;
|
|
331
|
+
while (true) {
|
|
332
|
+
try {
|
|
333
|
+
const existingContent = await fs.readFile(fullPath, "utf-8");
|
|
334
|
+
const parsed = parseConcept(existingContent);
|
|
335
|
+
const existingTitle = parsed.frontmatter.title || "";
|
|
336
|
+
if (existingTitle === title) {
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
slug = `${slugify(title)}-${suffix}`;
|
|
340
|
+
relPath = `${dir}/${slug}.md`;
|
|
341
|
+
fullPath = path.join(bundleDir, relPath);
|
|
342
|
+
suffix++;
|
|
343
|
+
} catch {
|
|
324
344
|
break;
|
|
325
345
|
}
|
|
326
|
-
slug = `${slugify(concept.title)}-${suffix}`;
|
|
327
|
-
relPath = `${dir}/${slug}.md`;
|
|
328
|
-
fullPath = path.join(bundleDir, relPath);
|
|
329
|
-
suffix++;
|
|
330
|
-
} catch {
|
|
331
|
-
break;
|
|
332
346
|
}
|
|
333
|
-
}
|
|
334
347
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
348
|
+
let existingFm = {};
|
|
349
|
+
try {
|
|
350
|
+
const existingContent = await fs.readFile(fullPath, "utf-8");
|
|
351
|
+
existingFm = parseConcept(existingContent).frontmatter;
|
|
352
|
+
} catch {}
|
|
353
|
+
|
|
354
|
+
const frontmatter = {
|
|
355
|
+
...existingFm,
|
|
356
|
+
type,
|
|
357
|
+
title,
|
|
358
|
+
description: typeof concept.description === "string" ? concept.description : "",
|
|
359
|
+
tags: Array.isArray(concept.tags) ? concept.tags.filter((t): t is string => typeof t === "string") : [],
|
|
360
|
+
timestamp: new Date().toISOString(),
|
|
361
|
+
confidence: typeof concept.confidence === "string" ? concept.confidence : "high",
|
|
362
|
+
last_verified: new Date().toISOString().split("T")[0],
|
|
363
|
+
links: Array.isArray(concept.links) ? concept.links.filter((l): l is string => typeof l === "string") : [],
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const serialized = serializeConcept(frontmatter, typeof concept.body === "string" ? concept.body : "");
|
|
367
|
+
const tmpPath = `${fullPath}.tmp-${process.pid}`;
|
|
368
|
+
await fs.writeFile(tmpPath, serialized, "utf-8");
|
|
369
|
+
await fs.rename(tmpPath, fullPath);
|
|
370
|
+
|
|
371
|
+
updatedConcepts.push({ title, type });
|
|
372
|
+
} catch {
|
|
373
|
+
// Skip just this concept; keep distilling the rest of the batch.
|
|
374
|
+
}
|
|
359
375
|
}
|
|
360
376
|
|
|
361
377
|
await rebuildIndex(bundleDir);
|
package/src/ai/model-manager.ts
CHANGED
|
@@ -356,7 +356,9 @@ const DEFAULT_CALL_TIMEOUT_MS = 120_000;
|
|
|
356
356
|
|
|
357
357
|
/** Per-chunk idle cap for streaming: a stream that emits NOTHING for this long is
|
|
358
358
|
* aborted, but a healthy long generation (chunks keep arriving) runs unbounded —
|
|
359
|
-
* unlike a single wall-clock cap that would kill a long-but-active stream.
|
|
359
|
+
* unlike a single wall-clock cap that would kill a long-but-active stream.
|
|
360
|
+
* Opt-in override via JEO_STREAM_IDLE_MS for reasoning workloads whose "thinking"
|
|
361
|
+
* phase can legitimately emit no visible token for longer than the default. */
|
|
360
362
|
const STREAM_IDLE_TIMEOUT_MS = 120_000;
|
|
361
363
|
|
|
362
364
|
/** Combine two abort signals into one. Preserves BOTH even when `AbortSignal.any`
|
|
@@ -418,7 +420,7 @@ async function nextMaybeIdle(iter: AsyncIterator<string>, idle?: StreamIdleOptio
|
|
|
418
420
|
idle.onIdle?.();
|
|
419
421
|
reject(new Error(deadlineFires
|
|
420
422
|
? `stream exceeded the overall deadline (JEO_STREAM_MAX_MS) — slow-drip stream aborted`
|
|
421
|
-
: `stream idle for ${idle.idleMs}ms (no chunk)
|
|
423
|
+
: `stream idle for ${idle.idleMs}ms (no chunk) — provider sent no token within the idle window (load or long thinking); retrying. Raise JEO_STREAM_IDLE_MS or lower the thinking level if this persists.`));
|
|
422
424
|
}, waitMs);
|
|
423
425
|
});
|
|
424
426
|
try {
|
|
@@ -435,6 +437,15 @@ export function streamMaxMs(env?: Record<string, string | undefined>): number |
|
|
|
435
437
|
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
436
438
|
}
|
|
437
439
|
|
|
440
|
+
/** Per-chunk idle cap (ms) from the environment, falling back to the built-in default.
|
|
441
|
+
* Lets reasoning workloads whose "thinking" phase emits no visible token for a long
|
|
442
|
+
* time raise the stall threshold via JEO_STREAM_IDLE_MS without a code change. */
|
|
443
|
+
export function streamIdleMs(env?: Record<string, string | undefined>): number {
|
|
444
|
+
const raw = jeoEnv("STREAM_IDLE_MS", env);
|
|
445
|
+
const n = raw !== undefined ? parseInt(raw, 10) : NaN;
|
|
446
|
+
return Number.isFinite(n) && n > 0 ? n : STREAM_IDLE_TIMEOUT_MS;
|
|
447
|
+
}
|
|
448
|
+
|
|
438
449
|
export async function* retryableStream(
|
|
439
450
|
makeIter: () => AsyncIterator<string>,
|
|
440
451
|
retry: RetryOptions,
|
|
@@ -475,7 +486,7 @@ export function createModelManager(): ModelManager {
|
|
|
475
486
|
};
|
|
476
487
|
const maxMs = streamMaxMs();
|
|
477
488
|
yield* retryableStream(makeIter, retry, {
|
|
478
|
-
idleMs:
|
|
489
|
+
idleMs: streamIdleMs(),
|
|
479
490
|
...(maxMs !== undefined ? { deadlineAt: Date.now() + maxMs } : {}),
|
|
480
491
|
onIdle: () => attempt?.abort(),
|
|
481
492
|
});
|
package/src/cli/runner.ts
CHANGED
|
@@ -70,9 +70,10 @@ export const COMMANDS: readonly CommandSpec[] = [
|
|
|
70
70
|
{
|
|
71
71
|
name: "team",
|
|
72
72
|
summary: "Execute the planning blueprint (Executor subagent tools).",
|
|
73
|
+
usage: "team [--strict-mutations]",
|
|
73
74
|
loader: async () => {
|
|
74
75
|
const m = await import("../commands/team");
|
|
75
|
-
return
|
|
76
|
+
return args => m.runTeamCommand(args);
|
|
76
77
|
},
|
|
77
78
|
},
|
|
78
79
|
{
|
package/src/commands/team.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { runAgentLoop } from "../agent/engine";
|
|
12
12
|
import { maybeCompact } from "../agent/compaction";
|
|
13
13
|
import { catalogMetadata } from "../ai";
|
|
14
|
+
import { gitDirtyCount } from "./launch/tmux";
|
|
14
15
|
import { readGlobalConfig } from "../agent/state";
|
|
15
16
|
import {
|
|
16
17
|
defaultSubagentRole,
|
|
@@ -26,7 +27,7 @@ import type { Message } from "../agent/loop";
|
|
|
26
27
|
import { loadProjectContext, withProjectContext } from "../agent/context-files";
|
|
27
28
|
import { categoryBadge } from "../tui/components/category-index";
|
|
28
29
|
|
|
29
|
-
export type RalphStreamKind = "step" | "complete" | "error";
|
|
30
|
+
export type RalphStreamKind = "step" | "complete" | "error" | "warn";
|
|
30
31
|
|
|
31
32
|
export interface RalphRenderOptions {
|
|
32
33
|
color?: boolean;
|
|
@@ -55,19 +56,20 @@ export function formatRalphTodoGuide(
|
|
|
55
56
|
return lines;
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
const STREAM_TINTS = {
|
|
60
|
+
complete: chalk.green.bold,
|
|
61
|
+
error: chalk.red.bold,
|
|
62
|
+
warn: chalk.yellow.bold,
|
|
63
|
+
step: chalk.cyan.bold,
|
|
64
|
+
} satisfies Record<RalphStreamKind, (s: string) => string>;
|
|
65
|
+
|
|
58
66
|
export function formatRalphStreamEvent(kind: RalphStreamKind, message: string, opts: RalphRenderOptions = {}): string {
|
|
59
|
-
|
|
60
|
-
if (!opts.color && !opts.indexed) return ` └─ stream:${
|
|
67
|
+
// ponytail: the label is always the kind itself — no mapping table needed.
|
|
68
|
+
if (!opts.color && !opts.indexed) return ` └─ stream:${kind} ${message}`;
|
|
61
69
|
const color = opts.color === true;
|
|
62
70
|
const badge = `${categoryBadge("subagent", { color })} `;
|
|
63
|
-
const tint = color
|
|
64
|
-
|
|
65
|
-
? chalk.green.bold
|
|
66
|
-
: kind === "error"
|
|
67
|
-
? chalk.red.bold
|
|
68
|
-
: chalk.cyan.bold
|
|
69
|
-
: (s: string) => s;
|
|
70
|
-
return ` ${badge}${tint(`stream:${label}`)} ${message}`;
|
|
71
|
+
const tint = color ? STREAM_TINTS[kind] : (s: string) => s;
|
|
72
|
+
return ` ${badge}${tint(`stream:${kind}`)} ${message}`;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
export interface RalphSubagentPromptContext {
|
|
@@ -163,6 +165,9 @@ export interface TeamEngineOptions {
|
|
|
163
165
|
io?: {
|
|
164
166
|
output?: (line: string) => void;
|
|
165
167
|
};
|
|
168
|
+
/** When true, a mutating role that finishes WITHOUT any successful
|
|
169
|
+
* write/edit/bash fails the task instead of merely warning (round-11). */
|
|
170
|
+
strictMutations?: boolean;
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok: boolean; reason?: string }> {
|
|
@@ -301,12 +306,17 @@ export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok:
|
|
|
301
306
|
}
|
|
302
307
|
|
|
303
308
|
// Round-8: a previous run halted on a task — its partial edits may still be
|
|
304
|
-
// on disk.
|
|
309
|
+
// on disk. Round-12: instead of a speculative warning, probe the working tree
|
|
310
|
+
// with `git status --porcelain` and report the CONCRETE uncommitted count so
|
|
311
|
+
// the user knows whether real partial work is present before re-running on it.
|
|
305
312
|
if (teamState.current_phase === "failed" && teamState.failed_task) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
`
|
|
309
|
-
|
|
313
|
+
const dirty = gitDirtyCount(cwd);
|
|
314
|
+
const treeNote = dirty === undefined
|
|
315
|
+
? `The working tree could not be inspected (not a git repo or git unavailable) — review it manually before trusting this re-run.`
|
|
316
|
+
: dirty > 0
|
|
317
|
+
? `git reports ${dirty} uncommitted change(s) — these may include partial edits from the halted task; review (e.g. 'git status', 'git diff') before re-running, as executing the task again on top of partial work can duplicate changes.`
|
|
318
|
+
: `git reports a clean working tree — no partial edits from the halted task remain on disk, so this re-run starts from a known state.`;
|
|
319
|
+
log(`[WARN] The previous run FAILED on "${teamState.failed_task}". ${treeNote}`);
|
|
310
320
|
teamState.current_phase = "executing";
|
|
311
321
|
delete teamState.failed_task;
|
|
312
322
|
}
|
|
@@ -336,6 +346,7 @@ export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok:
|
|
|
336
346
|
completed: teamState.completed_tasks ?? [],
|
|
337
347
|
cwd,
|
|
338
348
|
roleId: roleByIndex[activeIndex],
|
|
349
|
+
strictMutations: opts.strictMutations ?? false,
|
|
339
350
|
});
|
|
340
351
|
|
|
341
352
|
if (opts.signal?.aborted) {
|
|
@@ -374,14 +385,15 @@ export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok:
|
|
|
374
385
|
}
|
|
375
386
|
}
|
|
376
387
|
|
|
377
|
-
export async function runTeamCommand(): Promise<void> {
|
|
378
|
-
const
|
|
388
|
+
export async function runTeamCommand(args: string[] = []): Promise<void> {
|
|
389
|
+
const strictMutations = args.includes("--strict-mutations") || args.includes("--strict");
|
|
390
|
+
const res = await runTeamEngine({ strictMutations });
|
|
379
391
|
if (!res.ok) {
|
|
380
392
|
process.exitCode = 1;
|
|
381
393
|
}
|
|
382
394
|
}
|
|
383
395
|
|
|
384
|
-
async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: string; roleId?: string }): Promise<boolean> {
|
|
396
|
+
async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: string; roleId?: string; strictMutations?: boolean }): Promise<boolean> {
|
|
385
397
|
const config = await readGlobalConfig();
|
|
386
398
|
const role = getSubagentRole(ctx.roleId, config) ?? defaultSubagentRole();
|
|
387
399
|
const renderOpts: RalphRenderOptions = { color: !!process.stdout.isTTY, indexed: true };
|
|
@@ -465,7 +477,16 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
|
|
|
465
477
|
const msg = bashRuns === 0
|
|
466
478
|
? `${role.title} completed WITHOUT any successful write/edit/bash — treat its changed-files claim as unverified.`
|
|
467
479
|
: `${role.title} completed with only bash (no write/edit) — verify its changed-files claim independently.`;
|
|
468
|
-
|
|
480
|
+
// Round-11: under --strict-mutations, a mutating role that took NO action at
|
|
481
|
+
// all (no write/edit/bash) is a hard failure — an empty run must not pass as
|
|
482
|
+
// a completed task. bash-only stays advisory to avoid penalizing shell edits.
|
|
483
|
+
const hardFail = ctx.strictMutations && bashRuns === 0;
|
|
484
|
+
// Round-12: separate the tones so a passing advisory run doesn't masquerade
|
|
485
|
+
// as a stream:error — only a real hard-fail is red; an advisory note is warn.
|
|
486
|
+
console.log(formatRalphStreamEvent(hardFail ? "error" : "warn", msg, renderOpts));
|
|
487
|
+
if (hardFail) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
469
490
|
}
|
|
470
491
|
console.log(formatRalphStreamEvent("complete", `${role.title} finished task`, renderOpts));
|
|
471
492
|
return true;
|
package/src/util/retry.ts
CHANGED
|
@@ -56,7 +56,13 @@ export function defaultRetryable(err: unknown): boolean {
|
|
|
56
56
|
lowerMessage.includes("timeout") ||
|
|
57
57
|
lowerMessage.includes("overloaded") ||
|
|
58
58
|
lowerMessage.includes("rate limit") ||
|
|
59
|
-
lowerMessage.includes("rate_limit")
|
|
59
|
+
lowerMessage.includes("rate_limit") ||
|
|
60
|
+
// A per-chunk stream-idle stall ("stream idle for <ms>ms (no chunk)") is a
|
|
61
|
+
// transient stall (provider load / long TTFT) — retry it like a timeout. The
|
|
62
|
+
// OVERALL-deadline message ("stream exceeded the overall deadline") is a hard
|
|
63
|
+
// wall-clock cap and is deliberately NOT matched here (it must fail fast).
|
|
64
|
+
lowerMessage.includes("stream idle") ||
|
|
65
|
+
lowerMessage.includes("no chunk")
|
|
60
66
|
) {
|
|
61
67
|
return true;
|
|
62
68
|
}
|