jeo-code 0.7.16 → 0.7.17
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 +19 -0
- package/README.ja.md +6 -6
- package/README.ko.md +6 -6
- package/README.md +31 -6
- package/README.zh.md +6 -6
- package/package.json +4 -2
- package/src/agent/AGENTS.md +2 -2
- package/src/agent/engine.ts +36 -13
- package/src/agent/goal-verifier.ts +6 -0
- package/src/agent/loop-guards.ts +24 -5
- package/src/agent/memory.ts +137 -63
- package/src/commands/deep-interview.ts +3 -1
- package/src/commands/launch.ts +62 -14
- package/src/commands/skills.ts +3 -12
- package/src/prompts/agents/architect.md +2 -0
- package/src/prompts/agents/critic.md +1 -0
- package/src/skills/catalog.ts +10 -1
- package/src/util/file-attachment.ts +58 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
## [0.7.17] - 2026-06-25
|
|
11
|
+
_Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact._
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **`bun run dev:link` / `dev:doctor` run the global `jeo` from this checkout's source.** New `scripts/dev-link.ts`: `link` symlinks `jeo` → `<repo>/src/cli.ts` into `~/.local/bin` (override `JEO_DEV_LINK_DIR`), refuses to proceed when another `jeo` shadows the managed link earlier on `PATH`, ensures the `#!/usr/bin/env bun` entry stays executable, and runs a `--version` smoke test; `doctor` classifies whatever `jeo` the current `PATH` resolves to as `linked` (this hot source), `drift` (a compiled binary or npm-installed copy — exit 1), or `missing`. Unlike gjc there is no `build:native` prerequisite — jeo has zero native deps, so a bare symlink to the shebang entry is fully functional. Pure helpers (`findShadowingJeo`, `classifyDevDoctor`, `splitPathEntries`, `defaultLinkDir`, `resolveJeoOnPath`) back the `dev:link`/`dev:doctor` `package.json` scripts.
|
|
15
|
+
- **OKF concept memory gains in-memory search, scoring, and budget-aware injection.** `src/agent/memory.ts` adds `loadConcepts`/`scoreConcept`/`searchConcepts` over the OKF concept bundle (`.jeo/memory`), `index.md` progressive disclosure, and a relevance-selected `memoryPromptSection` that fills the `MEMORY_INJECT_MAX_CHARS` budget by picking whole high-scoring concepts instead of mid-string truncation, plus `recordFailedAttempt` for failure-aware recall (docs/okf_mem sprint-03 Search & Reference).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **README documents skill migration/inspection and the dev workflow.** New "Skill migration and bundled skill inspection" section walks `jeo skills list` / `read <name>` / `sync --check` / `sync --force`, and a new "Development" section covers `dev:link`/`dev:doctor` and running from source — bringing the gajae-code README's targeted sections to jeo.
|
|
19
|
+
- **Workflow prompts (engine, goal-verifier, deep-interview, critic, architect) tightened against punting and false blocks.** The turn protocol now asks a multi-edit batch to name each change in its `reasoning` and a multi-file task to sketch a todo plan before the first edit, the `web_search` reflex re-searches when sources conflict, and OUTPUT_DISCIPLINE forbids punting with only a disclaimer/cutoff caveat/offer-to-search. The Goal Verifier judges the goal as written with positive per-requirement evidence (MET when satisfied, IMPOSSIBLE only when genuinely unsatisfiable); deep-interview stops filling ambiguity with assumptions yet still lowers its score when dimensions truly resolve; the critic treats softening as signal but never manufactures blocks; the architect notes a clean verdict ≠ no inspection and adds an `Inspected:` line. All escape hatches (single-use done latches, `MAX_RE_BLOCKS`, step-budget hard cap) are unchanged, so no new loop risk.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Attaching an image no longer pushes the input caret several columns past the `[image #N]` tag.** Terminals pad a dragged-and-dropped image path with their own surrounding spaces, and the Ctrl+V clipboard insert added its own separator, so the swapped-in tag ended up flanked by multiple spaces and the caret parked well past it (the "spacebar ×3" feel). The two attach paths now share one tested helper in `src/util/file-attachment.ts`: `normalizeImageTags` collapses the whitespace around every `[image #N]` to exactly one space (trimming line edges, idempotent), `insertImageTag` (Ctrl+V) inserts the tag single-spaced with the caret right after it, and `attachImagePaths` (drag-drop / submit-time) normalizes its output and returns the precise post-tag `cursor`. The live REPL parks the box caret at that offset, so it sits immediately after the tag's single trailing space — ready for the prompt.
|
|
23
|
+
|
|
24
|
+
### Verified
|
|
25
|
+
- **"Works beside your existing agent or bot" now reflects jeo's real interop surface (all four READMEs).** The table documents `--worktree <name>` (run in an isolated sibling git worktree — `src/commands/launch/tmux.ts` `resolveWorktree`) for the Codex/Claude/Claw rows and `jeo mcp serve` / `jeo mcp tools` (the MCP stdio tool contract — `src/commands/mcp.ts`) for the external-controller/bot row, instead of the prior generic "standard terminal/CLI interfaces" wording.
|
|
26
|
+
- `bun run typecheck` clean; full suite green — 1991 pass / 0 fail across 234 files — including the new `test/dev-link.test.ts` (target/link-dir resolution incl. `JEO_DEV_LINK_DIR` override, PATH splitting, earlier-on-PATH shadow detection, and doctor `linked`/`drift`/`missing` classification), the new `test/memory-search-okf.test.ts` (concept load/score/search, `index.md` disclosure, budget-aware relevance injection, failed-attempt recall), expanded `test/cycle-and-turn-budget.test.ts`, and expanded `test/file-attachment.test.ts` + `test/input-box.test.ts` (whitespace normalization around `[image #N]`, idempotence, `insertImageTag`/`caretAfterTag`/`attachImagePaths` caret offsets, and the end-to-end render proving the caret column lands right after the rendered tag). Real CLI runs verified `dev:link`/`dev:doctor` end-to-end against a temp link dir (symlink + `--version` smoke test, shadow-guard refusal, drift detection, `linked` after linking) and the macOS clipboard image reader end-to-end (`readClipboardImage` decoded a clipboard PNG via the `osascript` fallback).
|
|
27
|
+
|
|
28
|
+
|
|
10
29
|
## [0.7.16] - 2026-06-25
|
|
11
30
|
_Bundled skills get a safe installer: `jeo skills sync` materializes the built-in workflow skills into `~/.jeo/skills`, preserving local edits by default, with a CI-friendly `--check` drift report and a `--force` overwrite._
|
|
12
31
|
|
package/README.ja.md
CHANGED
|
@@ -153,13 +153,13 @@ jeo ultragoal
|
|
|
153
153
|
|
|
154
154
|
| ツールまたはボット | 推奨される jeo コマンド | 境界 |
|
|
155
155
|
| ----------- | ----------------------- | -------- |
|
|
156
|
-
| Codex CLI | `jeo --tmux
|
|
157
|
-
| Claude Code | `jeo --tmux` または `jeo
|
|
156
|
+
| Codex CLI | `jeo --tmux --worktree <name>` または `jeo` | `--worktree` は jeo が管理する兄弟 git worktree を指定します(basename → 新しいブランチ)。既存のパスは先に `cd` してください。 |
|
|
157
|
+
| Claude Code | `jeo --tmux` または `jeo --tmux --worktree <name>` | jeo は Claude Code の拡張機能にはなりません。 |
|
|
158
158
|
| OpenCode | `jeo` または `jeo --tmux` | 外部ランナーのワークフローのみ。 |
|
|
159
|
-
| Claw Code | `jeo --tmux
|
|
160
|
-
| 外部コントローラー / ボット | `jeo` (
|
|
159
|
+
| Claw Code | `jeo --tmux --worktree <name>` | jeo は Claw Code にインストールされたり、置き換えたりしません。 |
|
|
160
|
+
| 外部コントローラー / ボット | `jeo mcp serve` (MCP stdio サーバー) | 外部コントローラーはスクロールバックのスクレイピングではなく、MCP ツール契約を介して jeo を駆動します。 |
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
`--worktree <name>` は隔離された兄弟 git worktree で jeo を実行するため(パスがあれば再利用、なければ basename ブランチで作成)、リスクのある作業やレビュー対象の作業がメインのチェックアウトに触れることはありません。`jeo mcp serve` は stdio を介して MCP 対応のあらゆるコントローラーに jeo のツールを公開します(`jeo mcp tools` で一覧表示)。`-q`/`--quiet` (または `JEO_QUIET=1`) を追加すると、起動バナー・ウェルカムアニメーション・リリースノート・再開ヒントが抑制され、jeo を別のエージェントと並べて実行したりボットから駆動したりできます。`-p`/`--print` は quiet を含みます。
|
|
163
163
|
|
|
164
164
|
|
|
165
165
|
## ローカルモデル
|
|
@@ -205,11 +205,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
205
205
|
## 変更履歴 (Changelog)
|
|
206
206
|
|
|
207
207
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
208
|
+
- **[0.7.17]** (2026-06-25) — Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact.
|
|
208
209
|
- **[0.7.16]** (2026-06-25) — Bundled skills get a safe installer: `jeo skills sync` materializes the built-in workflow skills into `~/.jeo/skills`, preserving local edits by default, with a CI-friendly `--check` drift report and a `--force` overwrite.
|
|
209
210
|
- **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
|
|
210
211
|
- **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
|
|
211
212
|
- **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
|
|
212
|
-
- **[0.7.8]** (2026-06-24) — Long-session performance & resource hygiene: the detached memory-distill worker now always terminates itself (closing the `jeo memory-distill` orphan pileup that pinned each session's transcript in RSS), the live reasoning/output block re-wraps only its trailing window (O(tail) per frame instead of O(len²) over a long stream), the team loop stops reprinting the todo guide every iteration, and the readline caret stays aligned after an image attach.
|
|
213
213
|
|
|
214
214
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
215
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -153,13 +153,13 @@ jeo ultragoal
|
|
|
153
153
|
|
|
154
154
|
| 도구 또는 봇 | 권장 jeo 명령 | 경계 |
|
|
155
155
|
| ----------- | ----------------------- | -------- |
|
|
156
|
-
| Codex CLI | `jeo --tmux
|
|
157
|
-
| Claude Code | `jeo --tmux` 또는 `jeo
|
|
156
|
+
| Codex CLI | `jeo --tmux --worktree <name>` 또는 `jeo` | `--worktree`는 jeo가 관리하는 형제 git worktree를 지정합니다(basename → 새 브랜치). 기존 경로는 먼저 `cd`로 이동하세요. |
|
|
157
|
+
| Claude Code | `jeo --tmux` 또는 `jeo --tmux --worktree <name>` | jeo는 Claude Code 확장 프로그램이 되지 않습니다. |
|
|
158
158
|
| OpenCode | `jeo` 또는 `jeo --tmux` | 현재는 외부 러너 워크플로만 지원합니다. |
|
|
159
|
-
| Claw Code | `jeo --tmux
|
|
160
|
-
| 외부 컨트롤러 / 봇 | `jeo` (
|
|
159
|
+
| Claw Code | `jeo --tmux --worktree <name>` | jeo는 Claw Code에 설치되거나 대체되지 않습니다. |
|
|
160
|
+
| 외부 컨트롤러 / 봇 | `jeo mcp serve` (MCP stdio 서버) | 외부 컨트롤러는 스크롤백 스크래핑이 아니라 MCP 도구 계약을 통해 jeo를 구동합니다. |
|
|
161
161
|
|
|
162
|
-
`-q`/`--quiet` (또는 `JEO_QUIET=1`)를 추가하면 시작 배너, 환영 애니메이션, 릴리스 노트, 재개 힌트가 억제되어 jeo를 다른 에이전트와 나란히 실행하거나 봇으로 구동할 수 있습니다. `-p`/`--print`는 quiet를 함의합니다.
|
|
162
|
+
`--worktree <name>`는 격리된 형제 git worktree에서 jeo를 실행하므로(경로가 있으면 재사용, 없으면 basename 브랜치로 생성) 위험하거나 검토가 필요한 작업이 메인 체크아웃을 건드리지 않습니다. `jeo mcp serve`는 stdio를 통해 MCP를 지원하는 모든 컨트롤러에 jeo의 도구를 노출합니다(`jeo mcp tools`로 목록 확인). `-q`/`--quiet` (또는 `JEO_QUIET=1`)를 추가하면 시작 배너, 환영 애니메이션, 릴리스 노트, 재개 힌트가 억제되어 jeo를 다른 에이전트와 나란히 실행하거나 봇으로 구동할 수 있습니다. `-p`/`--print`는 quiet를 함의합니다.
|
|
163
163
|
|
|
164
164
|
|
|
165
165
|
## 로컬 모델
|
|
@@ -205,11 +205,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
205
205
|
## 변경 이력 (Changelog)
|
|
206
206
|
|
|
207
207
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
208
|
+
- **[0.7.17]** (2026-06-25) — Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact.
|
|
208
209
|
- **[0.7.16]** (2026-06-25) — Bundled skills get a safe installer: `jeo skills sync` materializes the built-in workflow skills into `~/.jeo/skills`, preserving local edits by default, with a CI-friendly `--check` drift report and a `--force` overwrite.
|
|
209
210
|
- **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
|
|
210
211
|
- **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
|
|
211
212
|
- **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
|
|
212
|
-
- **[0.7.8]** (2026-06-24) — Long-session performance & resource hygiene: the detached memory-distill worker now always terminates itself (closing the `jeo memory-distill` orphan pileup that pinned each session's transcript in RSS), the live reasoning/output block re-wraps only its trailing window (O(tail) per frame instead of O(len²) over a long stream), the team loop stops reprinting the todo guide every iteration, and the readline caret stays aligned after an image attach.
|
|
213
213
|
|
|
214
214
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
215
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -154,13 +154,13 @@ Non-zero hook output is appended to the tool result the model reads (deduped per
|
|
|
154
154
|
|
|
155
155
|
| Tool or bot | Recommended jeo command | Boundary |
|
|
156
156
|
| ----------- | ----------------------- | -------- |
|
|
157
|
-
| Codex CLI | `jeo --tmux
|
|
158
|
-
| Claude Code | `jeo --tmux` or `jeo
|
|
157
|
+
| Codex CLI | `jeo --tmux --worktree <name>` or `jeo` | `--worktree` names a jeo-managed sibling git worktree (basename → new branch); for an existing path, `cd` there first. |
|
|
158
|
+
| Claude Code | `jeo --tmux` or `jeo --tmux --worktree <name>` | jeo does not become a Claude Code extension. |
|
|
159
159
|
| OpenCode | `jeo` or `jeo --tmux` | External-runner workflow only. |
|
|
160
|
-
| Claw Code | `jeo --tmux
|
|
161
|
-
| External controller / bot | `jeo` (
|
|
160
|
+
| Claw Code | `jeo --tmux --worktree <name>` | jeo does not install into or replace Claw Code. |
|
|
161
|
+
| External controller / bot | `jeo mcp serve` (MCP stdio server) | External controllers drive jeo over the MCP tool contract, not scrollback scraping. |
|
|
162
162
|
|
|
163
|
-
Add `-q`/`--quiet` (or `JEO_QUIET=1`) to suppress startup banners, welcome animation, release notes, and resume hints so jeo
|
|
163
|
+
`--worktree <name>` runs jeo in an isolated sibling git worktree (reused if the path exists, else created on a branch named after the basename) so risky or reviewable work never touches your main checkout. `jeo mcp serve` exposes jeo's tools to any MCP-capable controller over stdio (`jeo mcp tools` lists them). Add `-q`/`--quiet` (or `JEO_QUIET=1`) to suppress startup banners, the welcome animation, release notes, and resume hints so jeo runs cleanly beside another agent or is driven by a bot — `-p`/`--print` implies quiet.
|
|
164
164
|
|
|
165
165
|
|
|
166
166
|
## Local models
|
|
@@ -191,8 +191,33 @@ JEO_TOOL_OUTPUT_MAX=4000 # model-visible tool output cap (full output spi
|
|
|
191
191
|
|
|
192
192
|
Retry behavior is tunable via `retry` in `~/.jeo/config.json` (`requestMaxRetries`, `streamMaxRetries`, `rateLimitRetries`, `failFastStatuses`, …). The step budget is dynamic by default — it extends while recent tool calls show novel progress and consolidates with a wrap-up when stalled; `--max-steps N` restores a bounded flow.
|
|
193
193
|
|
|
194
|
+
## Skill migration and bundled skill inspection
|
|
195
|
+
|
|
196
|
+
When moving a workflow into jeo, inspect the bundled defaults before installing or overwriting anything:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
jeo skills list # bundled + user + project skills, with discovery dirs
|
|
200
|
+
jeo skills read ralplan # print one skill's full SKILL.md
|
|
201
|
+
jeo skills sync --check # report drift vs ~/.jeo/skills (non-zero exit on drift)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`jeo skills sync` installs the bundled workflow skills (deep-interview, deep-dive, ralplan, team, ultragoal) into `~/.jeo/skills` and **preserves existing local files by default** — a differing local copy is reported as `preserved`, never clobbered. If `--check` flags a missing or different file, compare it with `jeo skills read <name>` first; use `jeo skills sync --force` only when you intentionally want to replace local default workflow skill files. Target a different dir with a trailing path argument (or `JEO_CONFIG_DIR`), and add `--json` for the structured `SkillSyncResult`.
|
|
205
|
+
|
|
206
|
+
## Development
|
|
207
|
+
|
|
208
|
+
jeo is pure TypeScript on Bun with **zero native dependencies**, so the global `jeo` command can run this checkout's source directly — no build step, hot to every edit.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
bun install
|
|
212
|
+
bun run dev:link # symlink `jeo` -> <repo>/src/cli.ts into ~/.local/bin
|
|
213
|
+
bun run dev:doctor # report whether global `jeo` runs this source (linked/drift/missing)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`dev:link` refuses to proceed if another `jeo` shadows the managed link earlier on `PATH` (override the destination with `JEO_DEV_LINK_DIR`) and runs a `--version` smoke test. `dev:doctor` exits non-zero when the resolved `jeo` is a compiled binary or an installed copy rather than this source. Run from source without linking via `bun src/cli.ts --help`. Bundled workflow skills live in source at `src/prompts/skills/<name>/SKILL.md`; verify with `bun run typecheck` and `bun test`.
|
|
217
|
+
|
|
194
218
|
## Publishing
|
|
195
219
|
|
|
220
|
+
|
|
196
221
|
CI publishes via `.github/workflows/npm-publish.yml` — triggered by a published GitHub release, or manually with `workflow_dispatch` (optional dry-run). The workflow typechecks, tests, verifies the token (`npm whoami`), then runs `npm publish --provenance`.
|
|
197
222
|
|
|
198
223
|
Required npm token permissions (repository secret `NPM_TOKEN`):
|
|
@@ -207,11 +232,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
207
232
|
## Changelog
|
|
208
233
|
|
|
209
234
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
235
|
+
- **[0.7.17]** (2026-06-25) — Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact.
|
|
210
236
|
- **[0.7.16]** (2026-06-25) — Bundled skills get a safe installer: `jeo skills sync` materializes the built-in workflow skills into `~/.jeo/skills`, preserving local edits by default, with a CI-friendly `--check` drift report and a `--force` overwrite.
|
|
211
237
|
- **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
|
|
212
238
|
- **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
|
|
213
239
|
- **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
|
|
214
|
-
- **[0.7.8]** (2026-06-24) — Long-session performance & resource hygiene: the detached memory-distill worker now always terminates itself (closing the `jeo memory-distill` orphan pileup that pinned each session's transcript in RSS), the live reasoning/output block re-wraps only its trailing window (O(tail) per frame instead of O(len²) over a long stream), the team loop stops reprinting the todo guide every iteration, and the readline caret stays aligned after an image attach.
|
|
215
240
|
|
|
216
241
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
217
242
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -153,13 +153,13 @@ jeo ultragoal
|
|
|
153
153
|
|
|
154
154
|
| 工具或机器人 | 推荐的 jeo 命令 | 边界 |
|
|
155
155
|
| ----------- | ----------------------- | -------- |
|
|
156
|
-
| Codex CLI | `jeo --tmux
|
|
157
|
-
| Claude Code | `jeo --tmux` 或 `jeo
|
|
156
|
+
| Codex CLI | `jeo --tmux --worktree <name>` 或 `jeo` | `--worktree` 指定一个由 jeo 管理的同级 git worktree(basename → 新分支);对于已有路径,请先 `cd` 进入。 |
|
|
157
|
+
| Claude Code | `jeo --tmux` 或 `jeo --tmux --worktree <name>` | jeo 不会成为 Claude Code 的扩展。 |
|
|
158
158
|
| OpenCode | `jeo` 或 `jeo --tmux` | 仅限外部运行器工作流。 |
|
|
159
|
-
| Claw Code | `jeo --tmux
|
|
160
|
-
| 外部控制器 / 机器人 | `jeo` (
|
|
159
|
+
| Claw Code | `jeo --tmux --worktree <name>` | jeo 不会安装到或替换 Claw Code。 |
|
|
160
|
+
| 外部控制器 / 机器人 | `jeo mcp serve` (MCP stdio 服务器) | 外部控制器通过 MCP 工具契约驱动 jeo,而非抓取滚动回显。 |
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
`--worktree <name>` 在隔离的同级 git worktree 中运行 jeo(路径存在则复用,否则以 basename 分支创建),因此有风险或需审查的工作绝不会触及您的主检出。`jeo mcp serve` 通过 stdio 向任何支持 MCP 的控制器公开 jeo 的工具(用 `jeo mcp tools` 列出)。添加 `-q`/`--quiet`(或 `JEO_QUIET=1`)可抑制启动横幅、欢迎动画、发布说明和恢复提示,使 jeo 能够与其他代理并行运行或由机器人驱动。`-p`/`--print` 隐含 quiet。
|
|
163
163
|
|
|
164
164
|
|
|
165
165
|
## 本地模型
|
|
@@ -205,11 +205,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
205
205
|
## 更新日志 (Changelog)
|
|
206
206
|
|
|
207
207
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
208
|
+
- **[0.7.17]** (2026-06-25) — Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact.
|
|
208
209
|
- **[0.7.16]** (2026-06-25) — Bundled skills get a safe installer: `jeo skills sync` materializes the built-in workflow skills into `~/.jeo/skills`, preserving local edits by default, with a CI-friendly `--check` drift report and a `--force` overwrite.
|
|
209
210
|
- **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
|
|
210
211
|
- **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
|
|
211
212
|
- **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
|
|
212
|
-
- **[0.7.8]** (2026-06-24) — Long-session performance & resource hygiene: the detached memory-distill worker now always terminates itself (closing the `jeo memory-distill` orphan pileup that pinned each session's transcript in RSS), the live reasoning/output block re-wraps only its trailing window (O(tail) per frame instead of O(len²) over a long stream), the team loop stops reprinting the todo guide every iteration, and the readline caret stays aligned after an image attach.
|
|
213
213
|
|
|
214
214
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
215
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jeo-code",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.17",
|
|
4
4
|
"description": "Clean, highly optimized AI coding agent using spec-first loop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.ts",
|
|
@@ -52,7 +52,9 @@
|
|
|
52
52
|
"publish:npm": "npm publish --access public --registry https://registry.npmjs.org/",
|
|
53
53
|
"changelog:sync": "bun scripts/sync-changelog.ts",
|
|
54
54
|
"test": "bun test",
|
|
55
|
-
"verify:tmux": "bash scripts/tmux-verify.sh"
|
|
55
|
+
"verify:tmux": "bash scripts/tmux-verify.sh",
|
|
56
|
+
"dev:link": "bun scripts/dev-link.ts link",
|
|
57
|
+
"dev:doctor": "bun scripts/dev-link.ts doctor"
|
|
56
58
|
},
|
|
57
59
|
"dependencies": {
|
|
58
60
|
"zod": "^3.24.1",
|
package/src/agent/AGENTS.md
CHANGED
|
@@ -17,8 +17,8 @@ The core runtime loop, tool registry, session management, and state persistence
|
|
|
17
17
|
| `hooks.ts` | Brief description of purpose |
|
|
18
18
|
| `json.ts` | Brief description of purpose |
|
|
19
19
|
| `loop.ts` | The primary execution loop orchestrating model calls and tool execution |
|
|
20
|
-
| `loop-guards.ts` | Intermediate-judgment classification (gjc ultragoal-guard parity): named `GuardState` taxonomy, `GUARD_LIMITS` thresholds, and pure classifiers (`isVerificationSignal`, `repeatHint`, `classifyDoneGate`) consumed by `engine.ts` |
|
|
21
|
-
| `memory.ts` | OKF concept-bundle memory: session distill, query-aware budget injection, legacy MEMORY.md migration (`migrateLegacyMemory`) + `JEO_MEMORY_LEGACY` rollback toggle |
|
|
20
|
+
| `loop-guards.ts` | Intermediate-judgment classification (gjc ultragoal-guard parity): named `GuardState` taxonomy, `GUARD_LIMITS` thresholds, and pure classifiers (`isVerificationSignal`, `repeatHint`, `classifyDoneGate` — incl. `done_stale_verification` for a passing test/build that predates the last edit) consumed by `engine.ts` |
|
|
21
|
+
| `memory.ts` | OKF concept-bundle memory: session distill, query-aware budget injection (failure-first `priorityOrder` + pinned-invariant reserved budget), mid-loop `recordFailedAttempt` capture, shared `upsertConceptFile` writer, legacy MEMORY.md migration (`migrateLegacyMemory`) + `JEO_MEMORY_LEGACY` rollback toggle |
|
|
22
22
|
| `memory-okf.ts` | OKF v0.1 format layer: frontmatter parse/serialize, concept IDs, conformance validation |
|
|
23
23
|
| `memory-graph.ts` | Concept cross-link graph: build/expand (1-hop search), broken-link-tolerant lint, optional graphify detection |
|
|
24
24
|
| `model-recency.ts` | Brief description of purpose |
|
package/src/agent/engine.ts
CHANGED
|
@@ -105,9 +105,9 @@ export const TOOL_PROTOCOL = [
|
|
|
105
105
|
"",
|
|
106
106
|
"Alternatively, you may batch up to 6 independent calls in a single turn using the following format:",
|
|
107
107
|
'{ "reasoning": "<one short sentence>", "tools": [{ "tool": "<name>", "arguments": { ... } }, ...] }',
|
|
108
|
-
"Batch only independent calls; NEVER batch 'done', and NEVER put a mutating tool (write/edit/bash) after another mutating tool in one batch whose inputs depend on the earlier one.",
|
|
109
|
-
"Tool calibration: scale calls to difficulty — one for a known fact, a few for a normal task, more only when evidence is genuinely missing. Locate before you open: search/find first, then read the hit, instead of guessing paths.",
|
|
110
|
-
"web_search reflex: if the request hinges on a name, version, library, or event you do not actually recognize, search before answering instead of guessing; never claim a result's absence proves nonexistence.",
|
|
108
|
+
"Batch only independent calls; NEVER batch 'done', and NEVER put a mutating tool (write/edit/bash) after another mutating tool in one batch whose inputs depend on the earlier one. In a multi-edit batch, let the turn `reasoning` name each change — not a vague one-liner.",
|
|
109
|
+
"Tool calibration: scale calls to difficulty — one for a known fact, a few for a normal task, more only when evidence is genuinely missing. For multi-file or multi-step work, sketch a short plan (todo) before the first edit; a one-line change goes straight to the edit. Locate before you open: search/find first, then read the hit, instead of guessing paths.",
|
|
110
|
+
"web_search reflex: if the request hinges on a name, version, library, or event you do not actually recognize, search before answering instead of guessing; never claim a result's absence proves nonexistence. When sources conflict or look incomplete, run another targeted search to disambiguate rather than picking one at random.",
|
|
111
111
|
"Quoting fetched/searched text: paraphrase by default — quote at most one short phrase per source, cite it, and never paste long passages.",
|
|
112
112
|
].join("\n");
|
|
113
113
|
|
|
@@ -165,6 +165,7 @@ export const OUTPUT_DISCIPLINE = [
|
|
|
165
165
|
"- When using lists, ensure each bullet carries a complete thought; avoid fragmented or shredded reports.",
|
|
166
166
|
"- Don't stall on ambiguity: make reasonable assumptions and ask at most one clarifying question if absolutely necessary.",
|
|
167
167
|
"- Report only what is done or in progress; never announce future work instead of doing it.",
|
|
168
|
+
"- Give a substantive answer first; never punt with only a disclaimer, a knowledge-cutoff caveat, or an offer to search — answer with what you have, then note the caveat.",
|
|
168
169
|
"- Match reply length to the task: a one-line change gets a one-line report.",
|
|
169
170
|
].join("\n");
|
|
170
171
|
|
|
@@ -272,6 +273,10 @@ export interface AgentLoopResult {
|
|
|
272
273
|
doneReason?: string;
|
|
273
274
|
/** Summed provider token usage across the turn's steps, when reported. */
|
|
274
275
|
usage?: { inputTokens: number; outputTokens: number };
|
|
276
|
+
/** Set ONLY when the turn ended in a guard-detected dead end the model could not
|
|
277
|
+
* recover from (repeated/cycled/consecutively-failing calls). Lets the caller
|
|
278
|
+
* capture the stall into failure memory; `undefined` for done/budget/cancelled. */
|
|
279
|
+
stopClass?: "consecutive_failure" | "cycle" | "repeat";
|
|
275
280
|
}
|
|
276
281
|
|
|
277
282
|
|
|
@@ -369,7 +374,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
369
374
|
// "Stopped: …" — throwing away everything found this turn — do ONE final no-tools
|
|
370
375
|
// call asking the model to answer with what it already has. Mirrors the
|
|
371
376
|
// budget-exhaustion wrap-up below. Best-effort: falls back to the plain stop.
|
|
372
|
-
const consolidateStop = async (stopReason: string): Promise<AgentLoopResult> => {
|
|
377
|
+
const consolidateStop = async (stopReason: string, stopClass: AgentLoopResult["stopClass"]): Promise<AgentLoopResult> => {
|
|
373
378
|
try {
|
|
374
379
|
if (!opts.signal?.aborted) {
|
|
375
380
|
const wrapUp = await invokeCallLlm(
|
|
@@ -391,12 +396,13 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
391
396
|
return finish({
|
|
392
397
|
done: false,
|
|
393
398
|
steps: step,
|
|
399
|
+
stopClass,
|
|
394
400
|
doneReason: `${consolidated}\n\n(Stopped: ${stopReason} — consolidated answer above from what was found; continue with a follow-up request)`,
|
|
395
401
|
});
|
|
396
402
|
}
|
|
397
403
|
}
|
|
398
404
|
} catch { /* best-effort; fall through to the plain stop message */ }
|
|
399
|
-
return finish({ done: false, steps: step, doneReason: `Stopped: ${stopReason}` });
|
|
405
|
+
return finish({ done: false, steps: step, stopClass, doneReason: `Stopped: ${stopReason}` });
|
|
400
406
|
};
|
|
401
407
|
// No-progress guard: weak/local models often repeat the same tool call without
|
|
402
408
|
// ever emitting `done`. Two escalating corrections (B), then a consolidated stop.
|
|
@@ -414,6 +420,12 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
414
420
|
// doc/config changes where verification is genuinely not applicable).
|
|
415
421
|
let sawMutation = false;
|
|
416
422
|
let sawVerification = false;
|
|
423
|
+
// Monotonic ordering of mutation vs. verification events so the done gate can
|
|
424
|
+
// tell a STALE verification (passing test/build that predates the last edit)
|
|
425
|
+
// from a fresh one. A single counter captures intra-batch order too.
|
|
426
|
+
let mutVerSeq = 0;
|
|
427
|
+
let lastMutationSeq = 0;
|
|
428
|
+
let lastVerificationSeq = 0;
|
|
417
429
|
let donePushbackUsed = false;
|
|
418
430
|
// Caller-owned done gate (onBeforeDone) — also strictly once per turn.
|
|
419
431
|
let beforeDoneNudgeUsed = false;
|
|
@@ -717,7 +729,12 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
717
729
|
// done-verification gate — jeo's descendant of gjc's ultragoal-guard completion
|
|
718
730
|
// state machine (plan/gjc-inheritance.md B4). The classifier owns the JUDGMENT
|
|
719
731
|
// (which named state, which message); the loop owns the once-pushback latch.
|
|
720
|
-
const doneGate = classifyDoneGate({
|
|
732
|
+
const doneGate = classifyDoneGate({
|
|
733
|
+
sawMutation,
|
|
734
|
+
sawVerification,
|
|
735
|
+
verificationStale: sawMutation && lastMutationSeq > lastVerificationSeq,
|
|
736
|
+
pendingHookFailure,
|
|
737
|
+
});
|
|
721
738
|
if (doneGate.block && !donePushbackUsed) {
|
|
722
739
|
donePushbackUsed = true; // second done always passes — escape hatch
|
|
723
740
|
pushAssistantTurn(history, responseText, reasonBuf, artifactBuf);
|
|
@@ -805,7 +822,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
805
822
|
}
|
|
806
823
|
if (repeatCount >= MAX_REPEAT) {
|
|
807
824
|
const what = toolCalls.length === 1 ? `the same '${toolCalls[0].tool}' call` : "the same tool calls";
|
|
808
|
-
return await consolidateStop(`repeated ${what} ${MAX_REPEAT}× even after explicit corrections (the model never signaled done)
|
|
825
|
+
return await consolidateStop(`repeated ${what} ${MAX_REPEAT}× even after explicit corrections (the model never signaled done)`, "repeat");
|
|
809
826
|
}
|
|
810
827
|
|
|
811
828
|
// Cycle guard: an A↔B (or A↔B↔C-minus-one) alternation never trips the
|
|
@@ -833,7 +850,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
833
850
|
step++;
|
|
834
851
|
continue;
|
|
835
852
|
}
|
|
836
|
-
return await consolidateStop(`the model cycled through the same tool calls for ${CYCLE_WINDOW} consecutive steps even after an explicit correction (it never signaled done)
|
|
853
|
+
return await consolidateStop(`the model cycled through the same tool calls for ${CYCLE_WINDOW} consecutive steps even after an explicit correction (it never signaled done)`, "cycle");
|
|
837
854
|
}
|
|
838
855
|
|
|
839
856
|
// Helper to execute a single tool call
|
|
@@ -1037,15 +1054,20 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
1037
1054
|
}
|
|
1038
1055
|
// done-verification guard bookkeeping: write/edit successes mark the turn as
|
|
1039
1056
|
// mutating; a successful bash whose command/output looks like a test/build run
|
|
1040
|
-
// counts as verification. A
|
|
1041
|
-
//
|
|
1057
|
+
// counts as verification. A monotonic seq records ORDER so the done gate can
|
|
1058
|
+
// reject a verification that predates the last mutation (stale evidence).
|
|
1042
1059
|
for (let i = 0; i < toolCalls.length; i++) {
|
|
1043
1060
|
if (!results[i].executed || !results[i].success) continue;
|
|
1044
1061
|
const t = toolCalls[i].tool;
|
|
1045
|
-
if (t === "write" || t === "edit")
|
|
1046
|
-
|
|
1062
|
+
if (t === "write" || t === "edit") {
|
|
1063
|
+
sawMutation = true;
|
|
1064
|
+
lastMutationSeq = ++mutVerSeq;
|
|
1065
|
+
} else if (t === "bash") {
|
|
1047
1066
|
const cmd = String(toolCalls[i].arguments?.command ?? "");
|
|
1048
|
-
if (isVerificationSignal(cmd, results[i].output))
|
|
1067
|
+
if (isVerificationSignal(cmd, results[i].output)) {
|
|
1068
|
+
sawVerification = true;
|
|
1069
|
+
lastVerificationSeq = ++mutVerSeq;
|
|
1070
|
+
}
|
|
1049
1071
|
}
|
|
1050
1072
|
}
|
|
1051
1073
|
// F6 (round 4 architect, Low): judge the step by its NON-TRIVIAL calls — a
|
|
@@ -1069,6 +1091,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
1069
1091
|
return finish({
|
|
1070
1092
|
done: false,
|
|
1071
1093
|
steps: step,
|
|
1094
|
+
stopClass: "consecutive_failure",
|
|
1072
1095
|
doneReason: stopMsg,
|
|
1073
1096
|
});
|
|
1074
1097
|
}
|
|
@@ -33,6 +33,12 @@ Analyze the transcript carefully. Pay attention to:
|
|
|
33
33
|
2. What actions the agent took (tool calls, file modifications, tests run).
|
|
34
34
|
3. The final outcome and verification results.
|
|
35
35
|
|
|
36
|
+
Verdict discipline:
|
|
37
|
+
- Judge the goal exactly as written. Do NOT reframe, narrow, or reinterpret it to fit what the agent happened to build — if the goal as stated is not satisfied, that is NOT_MET, and name precisely what is missing.
|
|
38
|
+
- Require positive evidence per requirement: cite the concrete command, test, or file change that shows it was satisfied. The mere absence of an observed failure is NOT proof of success.
|
|
39
|
+
- Do not invent requirements beyond the stated goal. When every part of the goal as written has positive evidence, return MET — withholding MET to demand unrequested extras is itself an error.
|
|
40
|
+
- Reserve IMPOSSIBLE for goals that cannot be satisfied as specified (self-contradictory, or blocked by missing credentials/services), not for goals that are merely incomplete.
|
|
41
|
+
|
|
36
42
|
You must respond with a JSON object containing:
|
|
37
43
|
{
|
|
38
44
|
"verdict": "MET" | "NOT_MET" | "IMPOSSIBLE",
|
package/src/agent/loop-guards.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type GuardState =
|
|
|
27
27
|
| "context_overflow_retry" // provider reported context overflow → ONE trim + retry
|
|
28
28
|
| "refusal_retry" // transient safety refusal → bounded resend ladder
|
|
29
29
|
| "done_unverified" // mutated files, no verification signal → pushback on done
|
|
30
|
+
| "done_stale_verification" // verified, then mutated again → re-verify before done
|
|
30
31
|
| "done_hook_failing" // post-turn hook still failing → pushback on done
|
|
31
32
|
| "done_ok"; // done accepted — the turn is finished
|
|
32
33
|
|
|
@@ -100,13 +101,19 @@ export interface DoneGateInput {
|
|
|
100
101
|
sawMutation: boolean;
|
|
101
102
|
/** A test/build/typecheck/lint command succeeded this turn. */
|
|
102
103
|
sawVerification: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* A mutation landed AFTER the most recent successful verification — so the
|
|
106
|
+
* passing test/build no longer reflects the current tree. Optional; defaults
|
|
107
|
+
* to false, in which case the gate uses the order-insensitive `sawVerification`.
|
|
108
|
+
*/
|
|
109
|
+
verificationStale?: boolean;
|
|
103
110
|
/** The run-command of the most recent still-failing post-turn hook, or null. */
|
|
104
111
|
pendingHookFailure: string | null;
|
|
105
112
|
}
|
|
106
113
|
|
|
107
114
|
/** Verdict from {@link classifyDoneGate}: whether to bounce `done`, and the message. */
|
|
108
115
|
export interface DoneGateVerdict {
|
|
109
|
-
state: Extract<GuardState, "done_ok" | "done_unverified" | "done_hook_failing">;
|
|
116
|
+
state: Extract<GuardState, "done_ok" | "done_unverified" | "done_stale_verification" | "done_hook_failing">;
|
|
110
117
|
/** When true, `done` should be bounced ONCE with `message` (the caller owns the once-gate). */
|
|
111
118
|
block: boolean;
|
|
112
119
|
/** Corrective message to push back on `done`; empty when `state === "done_ok"`. */
|
|
@@ -117,13 +124,15 @@ export interface DoneGateVerdict {
|
|
|
117
124
|
* Classify whether a `done` should be accepted or bounced — the direct descendant of
|
|
118
125
|
* gjc's `ultragoal-guard` completion gate (plan/gjc-inheritance.md B4).
|
|
119
126
|
*
|
|
120
|
-
* A turn that MUTATED files
|
|
121
|
-
*
|
|
122
|
-
*
|
|
127
|
+
* A turn that MUTATED files is blocked ONCE when its verification is missing
|
|
128
|
+
* (no test/build signal), STALE (a passing run that predates the last edit), or
|
|
129
|
+
* a post-turn hook is still failing. The caller owns the single-pushback latch; a
|
|
130
|
+
* second `done` always passes (the escape hatch for unverifiable docs/config changes).
|
|
123
131
|
*/
|
|
124
132
|
export function classifyDoneGate(input: DoneGateInput): DoneGateVerdict {
|
|
125
133
|
const hookFailing = input.pendingHookFailure !== null;
|
|
126
|
-
const
|
|
134
|
+
const stale = input.sawVerification && input.verificationStale === true;
|
|
135
|
+
const block = input.sawMutation && (!input.sawVerification || stale || hookFailing);
|
|
127
136
|
if (!block) return { state: "done_ok", block: false, message: "" };
|
|
128
137
|
if (hookFailing) {
|
|
129
138
|
return {
|
|
@@ -135,6 +144,16 @@ export function classifyDoneGate(input: DoneGateInput): DoneGateVerdict {
|
|
|
135
144
|
"If the hook failure is a false positive, call done again and say why in the reason.",
|
|
136
145
|
};
|
|
137
146
|
}
|
|
147
|
+
if (stale) {
|
|
148
|
+
return {
|
|
149
|
+
state: "done_stale_verification",
|
|
150
|
+
block: true,
|
|
151
|
+
message:
|
|
152
|
+
"You verified earlier, but then modified files again — your last passing test/build no longer reflects the current tree. " +
|
|
153
|
+
"Re-run the narrowest verification command against the latest changes, then call done. " +
|
|
154
|
+
"If the later edits are verification-irrelevant (docs/config-only), call done again and say why in the reason.",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
138
157
|
return {
|
|
139
158
|
state: "done_unverified",
|
|
140
159
|
block: true,
|
package/src/agent/memory.ts
CHANGED
|
@@ -202,6 +202,10 @@ export interface Concept {
|
|
|
202
202
|
tags: string[];
|
|
203
203
|
/** high | medium | low — distiller defaults to "high"; drives core selection. */
|
|
204
204
|
confidence: string;
|
|
205
|
+
/** Load-bearing invariant (frontmatter `pinned: true`): selected into the injection
|
|
206
|
+
* budget BEFORE any query/recency fill, so the cap can never evict it (philosophy
|
|
207
|
+
* synthesis R8 / consensus #3 — pinned invariants survive on a reserved budget). */
|
|
208
|
+
pinned: boolean;
|
|
205
209
|
/** Bundle-relative path, e.g. `commands/bun-test.md`. */
|
|
206
210
|
relPath: string;
|
|
207
211
|
}
|
|
@@ -284,6 +288,7 @@ async function loadConceptsFromBundle(bundleDir: string): Promise<Concept[]> {
|
|
|
284
288
|
body: parsed.body.trim(),
|
|
285
289
|
tags: Array.isArray(fm.tags) ? fm.tags.filter((t): t is string => typeof t === "string") : [],
|
|
286
290
|
confidence: typeof fm.confidence === "string" ? fm.confidence : "high",
|
|
291
|
+
pinned: fm.pinned === true,
|
|
287
292
|
relPath,
|
|
288
293
|
};
|
|
289
294
|
cacheParsedConcept(file, sig, concept);
|
|
@@ -339,10 +344,16 @@ export function searchConcepts(concepts: Concept[], query: string): { concept: C
|
|
|
339
344
|
.sort((a, b) => b.score - a.score);
|
|
340
345
|
}
|
|
341
346
|
|
|
342
|
-
/** Priority order for injection
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
347
|
+
/** Priority order for injection. Failure-first: a query-relevant FailedAttempt is
|
|
348
|
+
* surfaced AHEAD of everything else — resurfacing a known dead end is higher-leverage
|
|
349
|
+
* than reinforcing what already works ("못하는 게 없도록" — close the gaps, don't just
|
|
350
|
+
* polish strengths), and it is the mechanism by which the loop gets more precise the
|
|
351
|
+
* more it repeats (llm-wiki: accumulated failure knowledge sharpens future runs). Then
|
|
352
|
+
* high-confidence "core" concepts, then query relevance (descending), then concepts the
|
|
353
|
+
* relevant ones LINK TO (1-hop graph expansion — Sprint 04: a directly-hit concept pulls
|
|
354
|
+
* its neighbours in as context ahead of unrelated noise), preserving input order as a
|
|
355
|
+
* stable tiebreak. The failure boost only fires when the query actually hits the concept
|
|
356
|
+
* (score > 0), so an unrelated FailedAttempt never crowds out relevant context. */
|
|
346
357
|
function priorityOrder(concepts: Concept[], query?: string): Concept[] {
|
|
347
358
|
const tokens = tokenize(query);
|
|
348
359
|
// 1-hop graph expansion: seed from concepts the query directly hits, then mark
|
|
@@ -354,14 +365,22 @@ function priorityOrder(concepts: Concept[], query?: string): Concept[] {
|
|
|
354
365
|
for (const id of expandByGraph(seeds, graph, 1)) related.add(id);
|
|
355
366
|
}
|
|
356
367
|
return concepts
|
|
357
|
-
.map((concept, i) =>
|
|
358
|
-
concept,
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
368
|
+
.map((concept, i) => {
|
|
369
|
+
const score = scoreConcept(concept, tokens);
|
|
370
|
+
return {
|
|
371
|
+
concept,
|
|
372
|
+
i,
|
|
373
|
+
// Query-relevant past failure → resurface first so the loop is reminded of
|
|
374
|
+
// what NOT to repeat for the current task. Gated on score > 0: a stale,
|
|
375
|
+
// unrelated dead end stays out of the way.
|
|
376
|
+
failure: concept.type === "FailedAttempt" && score > 0,
|
|
377
|
+
core: concept.confidence === "high",
|
|
378
|
+
score,
|
|
379
|
+
related: related.has(conceptId(concept.relPath)),
|
|
380
|
+
};
|
|
381
|
+
})
|
|
364
382
|
.sort((a, b) => {
|
|
383
|
+
if (a.failure !== b.failure) return a.failure ? -1 : 1;
|
|
365
384
|
if (a.core !== b.core) return a.core ? -1 : 1;
|
|
366
385
|
if (b.score !== a.score) return b.score - a.score;
|
|
367
386
|
if (a.related !== b.related) return a.related ? -1 : 1;
|
|
@@ -404,14 +423,22 @@ function renderConcepts(concepts: Concept[]): string {
|
|
|
404
423
|
|
|
405
424
|
/** Greedily select concepts (in priority order) whose grouped render stays within
|
|
406
425
|
* `budget` chars, dropping the lowest-priority concepts first. At least the
|
|
407
|
-
* top-priority concept is always kept (the framing/backstop cap still applies).
|
|
426
|
+
* top-priority concept is always kept (the framing/backstop cap still applies).
|
|
427
|
+
*
|
|
428
|
+
* Pinned (load-bearing) invariants get a RESERVED budget: they are selected FIRST,
|
|
429
|
+
* before any query/recency fill, so a tight cap can never evict an invariant the
|
|
430
|
+
* gates depend on (philosophy-synthesis consensus #3). The remaining budget then
|
|
431
|
+
* holds the failure-first / core / query-relevant fill. */
|
|
408
432
|
function selectWithinBudget(concepts: Concept[], query: string | undefined, budget: number): Concept[] {
|
|
409
|
-
const ordered = priorityOrder(concepts, query);
|
|
410
433
|
const selected: Concept[] = [];
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
434
|
+
const take = (pool: Concept[]) => {
|
|
435
|
+
for (const c of priorityOrder(pool, query)) {
|
|
436
|
+
if (renderConcepts([...selected, c]).length <= budget) selected.push(c);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
take(concepts.filter(c => c.pinned)); // reserved budget: invariants survive the cap
|
|
440
|
+
take(concepts.filter(c => !c.pinned)); // recency/query fill in the remaining budget
|
|
441
|
+
if (selected.length === 0 && concepts.length > 0) selected.push(priorityOrder(concepts, query)[0]!);
|
|
415
442
|
return selected;
|
|
416
443
|
}
|
|
417
444
|
|
|
@@ -610,6 +637,55 @@ async function cleanupStalePendingFiles(dir: string): Promise<void> {
|
|
|
610
637
|
}
|
|
611
638
|
}
|
|
612
639
|
|
|
640
|
+
/** Upsert ONE concept document into the bundle: resolve its file (dedup by title —
|
|
641
|
+
* same title overwrites in place, a colliding slug gets a numeric suffix), merge
|
|
642
|
+
* over any existing frontmatter, and atomically write it. The single source of
|
|
643
|
+
* truth for a concept file's on-disk shape — shared by the session-exit distiller
|
|
644
|
+
* and the deterministic recordFailedAttempt path. Does NOT touch index/log (callers
|
|
645
|
+
* batch that). Assumes a non-empty title; unknown types fall back to facts/. */
|
|
646
|
+
async function upsertConceptFile(
|
|
647
|
+
bundleDir: string,
|
|
648
|
+
c: { type: string; title: string; description?: string; body?: string; tags?: string[]; confidence?: string; links?: string[]; pinned?: boolean },
|
|
649
|
+
): Promise<string> {
|
|
650
|
+
const dir = DIR_BY_TYPE[c.type] ?? "facts";
|
|
651
|
+
await fs.mkdir(path.join(bundleDir, dir), { recursive: true });
|
|
652
|
+
let slug = slugify(c.title);
|
|
653
|
+
let relPath = `${dir}/${slug}.md`;
|
|
654
|
+
let fullPath = path.join(bundleDir, relPath);
|
|
655
|
+
let suffix = 1;
|
|
656
|
+
while (true) {
|
|
657
|
+
try {
|
|
658
|
+
if ((parseConcept(await fs.readFile(fullPath, "utf-8")).frontmatter.title || "") === c.title) break;
|
|
659
|
+
slug = `${slugify(c.title)}-${suffix}`;
|
|
660
|
+
relPath = `${dir}/${slug}.md`;
|
|
661
|
+
fullPath = path.join(bundleDir, relPath);
|
|
662
|
+
suffix++;
|
|
663
|
+
} catch {
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
let existingFm: Record<string, unknown> = {};
|
|
668
|
+
try {
|
|
669
|
+
existingFm = parseConcept(await fs.readFile(fullPath, "utf-8")).frontmatter as Record<string, unknown>;
|
|
670
|
+
} catch {}
|
|
671
|
+
const frontmatter = {
|
|
672
|
+
...existingFm,
|
|
673
|
+
type: c.type,
|
|
674
|
+
title: c.title,
|
|
675
|
+
description: c.description ?? "",
|
|
676
|
+
tags: c.tags ?? [],
|
|
677
|
+
timestamp: new Date().toISOString(),
|
|
678
|
+
confidence: c.confidence ?? "high",
|
|
679
|
+
last_verified: new Date().toISOString().split("T")[0],
|
|
680
|
+
links: c.links ?? [],
|
|
681
|
+
...(c.pinned ? { pinned: true } : {}),
|
|
682
|
+
};
|
|
683
|
+
const tmpPath = `${fullPath}.tmp-${process.pid}`;
|
|
684
|
+
await fs.writeFile(tmpPath, serializeConcept(frontmatter, c.body ?? ""), "utf-8");
|
|
685
|
+
await fs.rename(tmpPath, fullPath);
|
|
686
|
+
return relPath;
|
|
687
|
+
}
|
|
688
|
+
|
|
613
689
|
export async function distillSessionMemory(
|
|
614
690
|
history: Message[],
|
|
615
691
|
cwd: string,
|
|
@@ -721,57 +797,15 @@ export async function distillSessionMemory(
|
|
|
721
797
|
const title = typeof concept.title === "string" ? concept.title.trim() : "";
|
|
722
798
|
if (!type || !title) continue;
|
|
723
799
|
try {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const targetDir = path.join(bundleDir, dir);
|
|
728
|
-
await fs.mkdir(targetDir, { recursive: true });
|
|
729
|
-
|
|
730
|
-
let slug = slugify(title);
|
|
731
|
-
let relPath = `${dir}/${slug}.md`;
|
|
732
|
-
let fullPath = path.join(bundleDir, relPath);
|
|
733
|
-
|
|
734
|
-
let suffix = 1;
|
|
735
|
-
while (true) {
|
|
736
|
-
try {
|
|
737
|
-
const existingContent = await fs.readFile(fullPath, "utf-8");
|
|
738
|
-
const parsed = parseConcept(existingContent);
|
|
739
|
-
const existingTitle = parsed.frontmatter.title || "";
|
|
740
|
-
if (existingTitle === title) {
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
slug = `${slugify(title)}-${suffix}`;
|
|
744
|
-
relPath = `${dir}/${slug}.md`;
|
|
745
|
-
fullPath = path.join(bundleDir, relPath);
|
|
746
|
-
suffix++;
|
|
747
|
-
} catch {
|
|
748
|
-
break;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
let existingFm = {};
|
|
753
|
-
try {
|
|
754
|
-
const existingContent = await fs.readFile(fullPath, "utf-8");
|
|
755
|
-
existingFm = parseConcept(existingContent).frontmatter;
|
|
756
|
-
} catch {}
|
|
757
|
-
|
|
758
|
-
const frontmatter = {
|
|
759
|
-
...existingFm,
|
|
760
|
-
type,
|
|
800
|
+
await upsertConceptFile(bundleDir, {
|
|
801
|
+
type, // unknown types fall back to facts/ inside the helper (lenient)
|
|
761
802
|
title,
|
|
762
803
|
description: typeof concept.description === "string" ? concept.description : "",
|
|
804
|
+
body: typeof concept.body === "string" ? concept.body : "",
|
|
763
805
|
tags: Array.isArray(concept.tags) ? concept.tags.filter((t): t is string => typeof t === "string") : [],
|
|
764
|
-
timestamp: new Date().toISOString(),
|
|
765
806
|
confidence: typeof concept.confidence === "string" ? concept.confidence : "high",
|
|
766
|
-
last_verified: new Date().toISOString().split("T")[0],
|
|
767
807
|
links: Array.isArray(concept.links) ? concept.links.filter((l): l is string => typeof l === "string") : [],
|
|
768
|
-
};
|
|
769
|
-
|
|
770
|
-
const serialized = serializeConcept(frontmatter, typeof concept.body === "string" ? concept.body : "");
|
|
771
|
-
const tmpPath = `${fullPath}.tmp-${process.pid}`;
|
|
772
|
-
await fs.writeFile(tmpPath, serialized, "utf-8");
|
|
773
|
-
await fs.rename(tmpPath, fullPath);
|
|
774
|
-
|
|
808
|
+
});
|
|
775
809
|
updatedConcepts.push({ title, type });
|
|
776
810
|
} catch {
|
|
777
811
|
// Skip just this concept; keep distilling the rest of the batch.
|
|
@@ -819,6 +853,46 @@ export async function distillSessionMemory(
|
|
|
819
853
|
}
|
|
820
854
|
}
|
|
821
855
|
|
|
856
|
+
/** Mid-session, deterministic (no-LLM) capture of a dead end into the OKF bundle.
|
|
857
|
+
* The session-exit distill only learns AFTER a session ends; a turn that stalled
|
|
858
|
+
* (consecutive-failure / cycle / repeat guard fired) is a dead end the *next turn
|
|
859
|
+
* of the SAME session* should already avoid. This writes ONE FailedAttempt concept
|
|
860
|
+
* immediately so the per-turn memory injection resurfaces it (priorityOrder puts a
|
|
861
|
+
* query-relevant FailedAttempt first), closing the loop the user described: each
|
|
862
|
+
* iteration gets sharper by building on accumulated failure knowledge. Confidence
|
|
863
|
+
* is "medium" (not "high") so it never enters the always-injected "core" tier — it
|
|
864
|
+
* only surfaces when the query actually hits it, and the richer session-exit distill
|
|
865
|
+
* later consolidates or replaces it. Best-effort: a write failure is swallowed. */
|
|
866
|
+
export async function recordFailedAttempt(
|
|
867
|
+
cwd: string,
|
|
868
|
+
attempt: { title: string; description?: string; body?: string; tags?: string[] },
|
|
869
|
+
): Promise<{ recorded: boolean; skipped?: string }> {
|
|
870
|
+
if (jeoEnv("NO_MEMORY") === "1") return { recorded: false, skipped: "disabled (JEO_NO_MEMORY=1)" };
|
|
871
|
+
const title = attempt.title?.trim();
|
|
872
|
+
if (!title) return { recorded: false, skipped: "empty title" };
|
|
873
|
+
try {
|
|
874
|
+
const bundleDir = path.join(cwd, ".jeo", "memory");
|
|
875
|
+
// confidence "medium" keeps it out of the always-injected core tier (priorityOrder)
|
|
876
|
+
// — it only surfaces when the query actually hits it.
|
|
877
|
+
await upsertConceptFile(bundleDir, {
|
|
878
|
+
type: "FailedAttempt",
|
|
879
|
+
title,
|
|
880
|
+
description: attempt.description ?? "",
|
|
881
|
+
body: attempt.body ?? "",
|
|
882
|
+
tags: Array.isArray(attempt.tags) ? attempt.tags.filter(t => typeof t === "string") : [],
|
|
883
|
+
confidence: "medium",
|
|
884
|
+
});
|
|
885
|
+
await withIndexLock(bundleDir, async () => {
|
|
886
|
+
await rebuildIndex(bundleDir);
|
|
887
|
+
await updateLog(bundleDir, [{ title, type: "FailedAttempt" }]);
|
|
888
|
+
});
|
|
889
|
+
invalidateConceptCache();
|
|
890
|
+
return { recorded: true };
|
|
891
|
+
} catch (err: any) {
|
|
892
|
+
return { recorded: false, skipped: `record failed: ${err?.message ?? String(err)}` };
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
822
896
|
|
|
823
897
|
|
|
824
898
|
// ── Detached background distillation (round-16) ──
|
|
@@ -497,7 +497,9 @@ export async function runDeepInterviewEngine(opts: DeepInterviewEngineOptions =
|
|
|
497
497
|
` "constraints": ["Optional list of constraints once ambiguity is <= ${threshold}"],\n` +
|
|
498
498
|
` "acceptance_criteria": ["Concrete, testable acceptance criteria required before the seed can freeze"]\n` +
|
|
499
499
|
`}\n` +
|
|
500
|
-
`Ensure ambiguityScore drops dynamically as more detail is gathered. Do not report ambiguityScore <= ${threshold} unless acceptance_criteria is populated with concrete, testable checks
|
|
500
|
+
`Ensure ambiguityScore drops dynamically as more detail is gathered. Do not report ambiguityScore <= ${threshold} unless acceptance_criteria is populated with concrete, testable checks.\n` +
|
|
501
|
+
`Do not fill an ambiguous point with your own convenient assumption to push the score down — surface that assumption as the nextQuestion instead. Never supply unstated detail that makes the request look simpler or safer than the user actually wrote it.\n` +
|
|
502
|
+
`Conversely, when the user's answers genuinely resolve a dimension, lower the score accordingly — do not keep a dimension ambiguous just to prolong the interview. If you notice yourself quietly narrowing the user's idea to make it easier to score, that narrowing is the signal the dimension is still open: ask, don't assume.`
|
|
501
503
|
},
|
|
502
504
|
{
|
|
503
505
|
role: "user",
|
package/src/commands/launch.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { PassThrough } from "node:stream";
|
|
|
4
4
|
import { runAgentLoop, DEFAULT_TOOLS, TOOL_PROTOCOL, WORKING_DISCIPLINE, OUTPUT_DISCIPLINE, VERIFICATION_DIRECTIVE, type AgentLoopEvents } from "../agent/engine";
|
|
5
5
|
import { createOpikTracer, wrapEvents } from "../agent/opik-tracer";
|
|
6
6
|
import { initialDynamicStepLimit } from "../agent/step-budget";
|
|
7
|
-
import { memoryPromptSection, spawnDetachedDistill } from "../agent/memory";
|
|
7
|
+
import { memoryPromptSection, spawnDetachedDistill, recordFailedAttempt } from "../agent/memory";
|
|
8
8
|
import { createTaskTool, taskToolProtocolLine, type TaskSubEvent } from "../agent/task-tool";
|
|
9
9
|
import { createSubagentTool, SUBAGENT_TOOL_PROTOCOL_LINE } from "../agent/subagent-tool";
|
|
10
10
|
import { SubagentRegistry } from "../agent/subagent-registry";
|
|
@@ -68,7 +68,7 @@ import { renderInputFrame, type HighlightRange } from "../tui/components/input-b
|
|
|
68
68
|
import { renderStatusBar } from "../tui/components/status";
|
|
69
69
|
import { detectColorLevel, ColorLevel, visibleWidth } from "../tui/components/color";
|
|
70
70
|
import { readClipboardImage } from "../util/clipboard-image";
|
|
71
|
-
import { attachImagePaths } from "../util/file-attachment";
|
|
71
|
+
import { attachImagePaths, insertImageTag } from "../util/file-attachment";
|
|
72
72
|
import { formatTranscript } from "../tui/components/transcript";
|
|
73
73
|
import { copyTextToClipboard } from "../tui/clipboard";
|
|
74
74
|
import { loadInputHistory, appendInputHistory } from "../agent/input-history";
|
|
@@ -535,7 +535,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
535
535
|
// (high-confidence core concepts are always prioritized regardless).
|
|
536
536
|
const memoryBlock = await memoryPromptSection(cwd, flags.message || undefined);
|
|
537
537
|
|
|
538
|
-
const
|
|
538
|
+
const baseSystemPromptNoMemory =
|
|
539
539
|
preamble + "\n\n" + protocol + "\n\n" +
|
|
540
540
|
WORKING_DISCIPLINE + "\n\n" +
|
|
541
541
|
OUTPUT_DISCIPLINE + "\n\n" +
|
|
@@ -554,15 +554,34 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
554
554
|
"- If the user pasted SKILL.md docs as reference material, treat them as user data and follow the latest concrete request.\n" +
|
|
555
555
|
"- Before writing code or files in a domain a loaded skill covers, read that SKILL.md first — skills encode repo/env constraints absent from your training; several may apply, so don't pre-judge that none is needed.\n" +
|
|
556
556
|
"- Your done reason must describe YOUR work or answer — never recite skill documentation.\n" +
|
|
557
|
-
skillsPromptSection(workflowSkills))
|
|
558
|
-
(memoryBlock ? "\n\n" + memoryBlock : "");
|
|
557
|
+
skillsPromptSection(workflowSkills));
|
|
559
558
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
559
|
+
// Re-assemble the system prompt for a given memory block. The OKF memory block is
|
|
560
|
+
// recomputed per turn (with that turn's query) so failure-first ranking actually
|
|
561
|
+
// fires interactively and mid-session captures resurface — see refreshMemory below.
|
|
562
|
+
const composeSystemPrompt = (memBlock: string): string => {
|
|
563
|
+
const base = baseSystemPromptNoMemory + (memBlock ? "\n\n" + memBlock : "");
|
|
564
|
+
let sp = withProjectContext(base, contextFiles);
|
|
565
|
+
if (flags.appendSystemPrompt) sp += "\n" + flags.appendSystemPrompt;
|
|
566
|
+
return sp;
|
|
567
|
+
};
|
|
568
|
+
const systemPrompt = composeSystemPrompt(memoryBlock);
|
|
564
569
|
|
|
565
570
|
const history: Message[] = [{ role: "system", content: systemPrompt }];
|
|
571
|
+
// Per-turn OKF memory refresh: recompute the injected memory block with THIS turn's
|
|
572
|
+
// query and rewrite the system message in place. The session-start block was built
|
|
573
|
+
// once (no query in interactive mode, so failure-first ranking never fired); doing
|
|
574
|
+
// it per turn means each loop iteration pulls query-relevant past failures to the
|
|
575
|
+
// top and picks up dead ends captured earlier in the SAME session. Opt out with
|
|
576
|
+
// JEO_STATIC_MEMORY=1 (freeze the session-start block); JEO_NO_MEMORY=1 still wins.
|
|
577
|
+
const refreshSessionMemory = async (query: string): Promise<void> => {
|
|
578
|
+
if (jeoEnv("STATIC_MEMORY") === "1") return;
|
|
579
|
+
if (!history[0] || history[0].role !== "system") return;
|
|
580
|
+
try {
|
|
581
|
+
const fresh = await memoryPromptSection(cwd, query || undefined);
|
|
582
|
+
history[0] = { ...history[0], content: composeSystemPrompt(fresh) };
|
|
583
|
+
} catch { /* keep the prior system prompt on any failure */ }
|
|
584
|
+
};
|
|
566
585
|
let sessionModel: string | undefined = initialSessionModel;
|
|
567
586
|
// Session thinking-level override (`/thinking`); falls back to the config level.
|
|
568
587
|
let sessionThinking: "minimal" | "low" | "medium" | "high" | "xhigh" | undefined = flags.thinking ?? cfg.thinkingLevel;
|
|
@@ -764,6 +783,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
764
783
|
interactiveTurnActive = true;
|
|
765
784
|
tui.start();
|
|
766
785
|
}
|
|
786
|
+
// Refresh injected OKF memory with THIS turn's query before the model call so
|
|
787
|
+
// failure-first ranking fires and same-session dead ends resurface.
|
|
788
|
+
await refreshSessionMemory(userInput);
|
|
767
789
|
const compRes = await maybeCompact(history, {
|
|
768
790
|
model: activeModel,
|
|
769
791
|
contextTokens,
|
|
@@ -1062,6 +1084,30 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1062
1084
|
sessionUsage.outputTokens += result.usage.outputTokens;
|
|
1063
1085
|
}
|
|
1064
1086
|
sessionUsage.turns++;
|
|
1087
|
+
|
|
1088
|
+
// Mid-session failure capture: a guard-detected stall (the model repeated /
|
|
1089
|
+
// cycled / consecutively failed without recovering) is a dead end the rest of
|
|
1090
|
+
// THIS session should avoid. Record it into OKF now (deterministic, no LLM) so
|
|
1091
|
+
// the next turn's memory refresh resurfaces it. Best-effort; never blocks the turn.
|
|
1092
|
+
if (result.stopClass) {
|
|
1093
|
+
const task = userInput.replace(/\s+/g, " ").trim();
|
|
1094
|
+
const excerpt = task.length > 70 ? task.slice(0, 70) + "…" : task;
|
|
1095
|
+
const why = result.stopClass === "consecutive_failure"
|
|
1096
|
+
? "consecutive failing tool calls"
|
|
1097
|
+
: result.stopClass === "cycle"
|
|
1098
|
+
? "cycling through the same tool calls"
|
|
1099
|
+
: "repeating the same tool call";
|
|
1100
|
+
const tags = Array.from(new Set(
|
|
1101
|
+
task.toLowerCase().match(/[a-z0-9][a-z0-9_-]{3,}/g) ?? [],
|
|
1102
|
+
)).slice(0, 8);
|
|
1103
|
+
void recordFailedAttempt(cwd, {
|
|
1104
|
+
title: `Stalled on: ${excerpt}`,
|
|
1105
|
+
description: `A prior turn stalled (${why}) on this task — change approach before retrying.`,
|
|
1106
|
+
body: `Task: ${task.slice(0, 240)}\n\nThe agent gave up after ${why} (${result.steps} steps) and could not recover. ` +
|
|
1107
|
+
`Do NOT repeat the same line of attack; try a different decomposition, tool, or verification path.`,
|
|
1108
|
+
tags,
|
|
1109
|
+
}).catch(() => {});
|
|
1110
|
+
}
|
|
1065
1111
|
const usage = result.usage ? ` (${result.usage.inputTokens} in / ${result.usage.outputTokens} out tokens)` : "";
|
|
1066
1112
|
|
|
1067
1113
|
if (turnConfig.gitAutoCommit) {
|
|
@@ -2671,7 +2717,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2671
2717
|
if (dropped.images.length === 0 || dropped.text === before) return;
|
|
2672
2718
|
pendingImages.push(...dropped.images);
|
|
2673
2719
|
rli.line = dropped.text;
|
|
2674
|
-
rli.cursor = dropped.
|
|
2720
|
+
rli.cursor = dropped.cursor; // park right after the swapped-in tag (whitespace normalized)
|
|
2675
2721
|
typedLine = dropped.text;
|
|
2676
2722
|
// Keep readline's internal screen model in step with the swapped-in tag so
|
|
2677
2723
|
// the caret is not offset on the next keystroke (mirrors the Ctrl+V path).
|
|
@@ -2736,12 +2782,14 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2736
2782
|
const img = await readClipboardImage();
|
|
2737
2783
|
if (!img) return;
|
|
2738
2784
|
pendingImages.push(img);
|
|
2739
|
-
const tag = `[image #${pendingImages.length}]`;
|
|
2740
2785
|
const rli = rl as unknown as { line: string; cursor: number; _refreshLine?: () => void };
|
|
2741
2786
|
const at = typeof rli.cursor === "number" ? rli.cursor : rli.line.length;
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2787
|
+
// Insert the [image #N] tag at the caret with normalized single-space
|
|
2788
|
+
// padding (shared with the drag-drop path) so the caret lands right after
|
|
2789
|
+
// the tag instead of being pushed several columns by stray spaces.
|
|
2790
|
+
const ins = insertImageTag(rli.line, at, pendingImages.length);
|
|
2791
|
+
rli.line = ins.text;
|
|
2792
|
+
rli.cursor = ins.cursor;
|
|
2745
2793
|
typedLine = rli.line;
|
|
2746
2794
|
// Sync readline's internal screen model to the injected tag (same as the
|
|
2747
2795
|
// slash/tab completion accept paths): without it readline's stale row/cursor
|
package/src/commands/skills.ts
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import
|
|
4
|
-
import { SKILLS, getSkillFrom, formatSkill, loadSkills, skillDirs, bundledSkillFileContent } from "../skills/catalog";
|
|
3
|
+
import { SKILLS, getSkillFrom, formatSkill, loadSkills, skillDirs, bundledSkillFileContent, userSkillsDir } from "../skills/catalog";
|
|
5
4
|
import { getLocalJeoDir } from "../agent/state";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/** User-level bundled-skills install dir (`~/.jeo/skills`, honoring JEO_CONFIG_DIR /
|
|
9
|
-
* $HOME overrides) — the destination `jeo skills sync` reconciles against, matching
|
|
10
|
-
* the highest-precedence flat dir in {@link skillDirs}. */
|
|
11
|
-
export function userSkillsDir(): string {
|
|
12
|
-
const userHome = process.env.HOME || os.homedir();
|
|
13
|
-
const home = jeoEnv("CONFIG_DIR") || path.join(userHome, ".jeo");
|
|
14
|
-
return path.join(home, "skills");
|
|
15
|
-
}
|
|
5
|
+
|
|
6
|
+
export { userSkillsDir };
|
|
16
7
|
|
|
17
8
|
export type SkillSyncStatus = "missing" | "up-to-date" | "differs";
|
|
18
9
|
export type SkillSyncAction = "installed" | "overwritten" | "preserved" | "unchanged" | "none";
|
|
@@ -11,6 +11,7 @@ Assess architecture, maintainability, correctness, and spec compliance with file
|
|
|
11
11
|
- Prioritize spec/root-cause correctness before style comments.
|
|
12
12
|
- Rate findings by severity: `CRITICAL`, `HIGH`, `MEDIUM`, `LOW`.
|
|
13
13
|
- Never return `APPROVE` if CRITICAL/HIGH issues remain.
|
|
14
|
+
- A clean verdict is not the absence of inspection: do not return `CLEAR`/`APPROVE` merely because no problem surfaced. Base the verdict on files and paths you concretely examined, and say which ones.
|
|
14
15
|
</constraints>
|
|
15
16
|
|
|
16
17
|
<execution_loop>
|
|
@@ -29,6 +30,7 @@ Assess architecture, maintainability, correctness, and spec compliance with file
|
|
|
29
30
|
Your final `done.reason` MUST be markdown with these sections:
|
|
30
31
|
- `Summary:`
|
|
31
32
|
- `Findings:`
|
|
33
|
+
- `Inspected:` the files/paths you actually examined (evidence for the verdict)
|
|
32
34
|
- `Recommendations:`
|
|
33
35
|
- `Architectural Status:` one of `CLEAR`, `WATCH`, `BLOCK`
|
|
34
36
|
- `Code Review Recommendation:` one of `APPROVE`, `COMMENT`, `REQUEST CHANGES`
|
|
@@ -10,6 +10,7 @@ Decide whether a plan or approach is actionable before execution proceeds.
|
|
|
10
10
|
- Read-only: never modify files.
|
|
11
11
|
- Do not invent problems; reject only with concrete gaps.
|
|
12
12
|
- Simulate representative tasks against inspected evidence before deciding.
|
|
13
|
+
- Honesty cuts both ways: if you catch yourself softening a real, blocking gap into `[ITERATE]` just to avoid blocking, that softening is the signal the gap is real — name it. But never manufacture a block: when gaps are concrete yet fixable in-flight prefer `[ITERATE]` over `[REJECT]`, and return `[OKAY]` once the plan is genuinely actionable.
|
|
13
14
|
</constraints>
|
|
14
15
|
|
|
15
16
|
<execution_loop>
|
package/src/skills/catalog.ts
CHANGED
|
@@ -261,6 +261,15 @@ export function skillSlashAliases(skill: SkillDoc): string[] {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
|
|
264
|
+
/** User-level bundled-skills install dir (`<config>/skills`, i.e. `~/.jeo/skills`
|
|
265
|
+
* honoring JEO_CONFIG_DIR / $HOME) — the highest-precedence flat skill dir and the
|
|
266
|
+
* destination `jeo skills sync` reconciles against. $HOME wins over os.homedir() so
|
|
267
|
+
* tests/sandboxes that re-point HOME are honored (see {@link skillDirs}). */
|
|
268
|
+
export function userSkillsDir(): string {
|
|
269
|
+
const userHome = process.env.HOME || os.homedir();
|
|
270
|
+
return path.join(jeoEnv("CONFIG_DIR") || path.join(userHome, ".jeo"), "skills");
|
|
271
|
+
}
|
|
272
|
+
|
|
264
273
|
/** Global + per-project skill-doc directories (user-configurable SKILL.md files).
|
|
265
274
|
* Ordered lowest → highest precedence (a later dir's skill overrides an earlier
|
|
266
275
|
* one with the same name): foreign-ecosystem roots first, jeo-native last.
|
|
@@ -283,7 +292,7 @@ export function skillDirs(cwd: string = process.cwd()): string[] {
|
|
|
283
292
|
path.join(userHome, ".claude", "skills"),
|
|
284
293
|
path.join(home, "agent", "skills"),
|
|
285
294
|
path.join(userHome, ".agents", "skills"),
|
|
286
|
-
|
|
295
|
+
userSkillsDir(),
|
|
287
296
|
path.join(cwd, ".claude", "skills"),
|
|
288
297
|
path.join(cwd, ".agents", "skills"),
|
|
289
298
|
path.join(cwd, ".jeo", "skills"),
|
|
@@ -18,6 +18,54 @@ import type { ImageAttachment } from "../ai/types";
|
|
|
18
18
|
|
|
19
19
|
const IMAGE_EXT_RE = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
|
|
20
20
|
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Normalize the whitespace around every `[image #N]` tag to a single space:
|
|
24
|
+
* - one space before a tag that follows other text (none at line start),
|
|
25
|
+
* - one space after a tag that is followed by more text,
|
|
26
|
+
* - any run of spaces/tabs collapsed to one, and the whole string trimmed.
|
|
27
|
+
*
|
|
28
|
+
* A tag at the very end keeps NO trailing space (callers that want the caret to sit
|
|
29
|
+
* past it add exactly one). Terminals pad a dragged-and-dropped path with their own
|
|
30
|
+
* spacing and the Ctrl+V insert adds a separator, so without this the caret parks
|
|
31
|
+
* several columns past the tag — the "input point looks pushed by a few spaces" bug.
|
|
32
|
+
* Idempotent: re-normalizing already-clean text is a no-op.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeImageTags(text: string): string {
|
|
35
|
+
return text
|
|
36
|
+
.replace(/[ \t]*(\[image #\d+\])[ \t]*/g, (m, tag: string, off: number, s: string) =>
|
|
37
|
+
(off > 0 ? " " : "") + tag + (off + m.length < s.length ? " " : ""),
|
|
38
|
+
)
|
|
39
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
40
|
+
.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Caret offset just past the `[image #n]` tag in `text` (and its single trailing
|
|
44
|
+
* space when present); falls back to end-of-text when the tag is absent. */
|
|
45
|
+
export function caretAfterTag(text: string, n: number): number {
|
|
46
|
+
const tag = `[image #${n}]`;
|
|
47
|
+
const idx = text.indexOf(tag);
|
|
48
|
+
if (idx < 0) return text.length;
|
|
49
|
+
let end = idx + tag.length;
|
|
50
|
+
if (text[end] === " ") end += 1; // sit after the separating space, not on it
|
|
51
|
+
return end;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Insert an `[image #n]` tag at `cursor` within `line` (the Ctrl+V clipboard-image
|
|
56
|
+
* path), normalizing surrounding whitespace so the tag is flanked by exactly one
|
|
57
|
+
* space. A tag landing at end-of-line gets one trailing space so the user can keep
|
|
58
|
+
* typing; the returned caret sits right after the tag (and that space).
|
|
59
|
+
*/
|
|
60
|
+
export function insertImageTag(line: string, cursor: number, n: number): { text: string; cursor: number } {
|
|
61
|
+
const at = Math.max(0, Math.min(cursor, line.length));
|
|
62
|
+
const tag = `[image #${n}]`;
|
|
63
|
+
let text = normalizeImageTags(line.slice(0, at) + tag + line.slice(at));
|
|
64
|
+
const idx = text.indexOf(tag);
|
|
65
|
+
if (idx >= 0 && idx + tag.length >= text.length) text += " "; // trailing space at EOL for typing
|
|
66
|
+
return { text, cursor: caretAfterTag(text, n) };
|
|
67
|
+
}
|
|
68
|
+
|
|
21
69
|
/** Detect an image media type from magic bytes, or null when the bytes are not a
|
|
22
70
|
* recognised image. Used as the authoritative check (extension only gates the
|
|
23
71
|
* candidate scan; the bytes decide). */
|
|
@@ -114,6 +162,10 @@ export interface AttachResult {
|
|
|
114
162
|
text: string;
|
|
115
163
|
/** The newly-read image attachments, in source order. */
|
|
116
164
|
images: ImageAttachment[];
|
|
165
|
+
/** Caret offset to park after the swap: just past the trailing space of the LAST
|
|
166
|
+
* inserted tag (so the live box places the cursor right after the attachment,
|
|
167
|
+
* ready for the user's prompt). Equals `text.length` when nothing was attached. */
|
|
168
|
+
cursor: number;
|
|
117
169
|
}
|
|
118
170
|
|
|
119
171
|
/**
|
|
@@ -131,7 +183,7 @@ export async function attachImagePaths(
|
|
|
131
183
|
read: FileReader = defaultReader,
|
|
132
184
|
): Promise<AttachResult> {
|
|
133
185
|
const tokens = findImagePathTokens(text);
|
|
134
|
-
if (tokens.length === 0) return { text, images: [] };
|
|
186
|
+
if (tokens.length === 0) return { text, images: [], cursor: text.length };
|
|
135
187
|
|
|
136
188
|
const reads = await Promise.all(
|
|
137
189
|
tokens.map(async (t) => {
|
|
@@ -154,5 +206,9 @@ export async function attachImagePaths(
|
|
|
154
206
|
n += 1;
|
|
155
207
|
}
|
|
156
208
|
out += text.slice(cursor);
|
|
157
|
-
return { text: out, images };
|
|
209
|
+
if (images.length === 0) return { text: out, images, cursor: out.length };
|
|
210
|
+
// Collapse the spacing terminals add around a dropped path so the swapped-in tag
|
|
211
|
+
// (and the caret parked after it) is not pushed several columns to the right.
|
|
212
|
+
const normalized = normalizeImageTags(out);
|
|
213
|
+
return { text: normalized, images, cursor: caretAfterTag(normalized, n - 1) };
|
|
158
214
|
}
|