jeo-code 0.7.14 → 0.7.16
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 +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/agent/engine.ts +2 -22
- package/src/agent/task-tool.ts +15 -20
- package/src/agent/tool-output.ts +1 -1
- package/src/ai/model-manager.ts +1 -2
- package/src/ai/providers/anthropic.ts +2 -3
- package/src/cli/runner.ts +3 -4
- package/src/commands/launch/flags.ts +1 -4
- package/src/commands/launch.ts +19 -24
- package/src/commands/skills.ts +113 -2
- package/src/commands/team.ts +6 -11
- package/src/skills/catalog.ts +7 -0
- package/src/tui/components/transcript.ts +25 -73
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.16] - 2026-06-25
|
|
11
|
+
_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
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **`jeo skills sync` installs and reconciles the bundled workflow skills into `~/.jeo/skills`.** New `syncBundledSkills(dir, { check, force })` (`src/commands/skills.ts`) reconciles the built-in `SKILLS` against a target dir — defaulting to `userSkillsDir()` (`~/.jeo/skills`, honoring `JEO_CONFIG_DIR`/`$HOME`, matching the highest-precedence flat dir in `skillDirs`). Missing skills are installed; a differing local copy is **preserved** by default (gjc `setup defaults` parity) unless `--force` overwrites it. `--check` is a pure drift report that writes nothing and sets a non-zero exit code on drift, so CI can gate on bundled-skill staleness. `--json` emits the structured `SkillSyncResult` (per-skill `status`/`action`, `drift`, `wrote`, `mode`). A new `bundledSkillFileContent` helper (`src/skills/catalog.ts`) defines the canonical on-disk bytes (raw `SKILL.md` when present, decorated render otherwise) shared by `sync` and the existing `--write` path. The `skills` command summary/usage (`src/cli/runner.ts`) and the list footer now advertise the subcommand.
|
|
15
|
+
|
|
16
|
+
### Verified
|
|
17
|
+
- `bun run typecheck` clean; full suite green — 1960 pass / 0 fail across 233 files — including the new `test/skills-sync.test.ts` (fresh install, idempotent re-run, `--check` drift report without writing, default-preserve vs `--force` overwrite, single-file reinstall, non-zero `--check` exit via `JEO_CONFIG_DIR`, and `--json` shape).
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## [0.7.15] - 2026-06-25
|
|
21
|
+
_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._
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **`src/` and `test/` reverted to the v0.7.9 tree.** Every runtime and test file is byte-identical to the `v0.7.9` tag; the post-0.7.9 test `test/mutated-files-artifacts.test.ts` is removed along with the feature it covered.
|
|
25
|
+
|
|
26
|
+
### Notes
|
|
27
|
+
- Release tooling (`scripts/ci-release-build-binaries.ts`, keeping the `bun-darwin-x64-baseline` target for older-CPU compatibility) and documentation history (this CHANGELOG, READMEs) are intentionally **not** rolled back, since they are not part of the user-facing runtime and reverting them would regress binary compatibility or erase the record of what was shipped.
|
|
28
|
+
|
|
10
29
|
## [0.7.14] - 2026-06-25
|
|
11
30
|
_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._
|
|
12
31
|
|
package/README.ja.md
CHANGED
|
@@ -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.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
|
+
- **[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.
|
|
208
210
|
- **[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.
|
|
209
211
|
- **[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.
|
|
210
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.
|
|
211
|
-
- **[0.7.7]** (2026-06-23) — Long-running-process hygiene & input robustness: backgrounded grandchildren (`next dev &`, daemons) are now reaped at the turn boundary via per-command process groups — closing the climbing `next-server` RSS leak — while the prompt gains recursive fuzzy `@path` search, ANSI-safe and idle-merged bracketed paste, a configurable OSC 52 clipboard cap, per-role ralplan model routing, and leaked-reasoning-tag stripping on salvaged answers.
|
|
212
|
-
- **[0.7.6]** (2026-06-23) — Release & docs plumbing: the running version's release notes now travel inside the `--compile` single binary (so `/whats-new` works offline and from a global install), the in-TUI markdown table renderer stops splitting on escaped `\|` pipes, and the release pipeline gains a cross-compiled standalone-binary matrix — macOS/Linux **plus a first-class Windows x64 `.exe`** — built on one Linux runner and auto-attached to each GitHub release.
|
|
213
213
|
|
|
214
214
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
215
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -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.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
|
+
- **[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.
|
|
208
210
|
- **[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.
|
|
209
211
|
- **[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.
|
|
210
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.
|
|
211
|
-
- **[0.7.7]** (2026-06-23) — Long-running-process hygiene & input robustness: backgrounded grandchildren (`next dev &`, daemons) are now reaped at the turn boundary via per-command process groups — closing the climbing `next-server` RSS leak — while the prompt gains recursive fuzzy `@path` search, ANSI-safe and idle-merged bracketed paste, a configurable OSC 52 clipboard cap, per-role ralplan model routing, and leaked-reasoning-tag stripping on salvaged answers.
|
|
212
|
-
- **[0.7.6]** (2026-06-23) — Release & docs plumbing: the running version's release notes now travel inside the `--compile` single binary (so `/whats-new` works offline and from a global install), the in-TUI markdown table renderer stops splitting on escaped `\|` pipes, and the release pipeline gains a cross-compiled standalone-binary matrix — macOS/Linux **plus a first-class Windows x64 `.exe`** — built on one Linux runner and auto-attached to each GitHub release.
|
|
213
213
|
|
|
214
214
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
215
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -207,11 +207,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
207
207
|
## Changelog
|
|
208
208
|
|
|
209
209
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
210
|
+
- **[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
|
+
- **[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
212
|
- **[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
213
|
- **[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
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.
|
|
213
|
-
- **[0.7.7]** (2026-06-23) — Long-running-process hygiene & input robustness: backgrounded grandchildren (`next dev &`, daemons) are now reaped at the turn boundary via per-command process groups — closing the climbing `next-server` RSS leak — while the prompt gains recursive fuzzy `@path` search, ANSI-safe and idle-merged bracketed paste, a configurable OSC 52 clipboard cap, per-role ralplan model routing, and leaked-reasoning-tag stripping on salvaged answers.
|
|
214
|
-
- **[0.7.6]** (2026-06-23) — Release & docs plumbing: the running version's release notes now travel inside the `--compile` single binary (so `/whats-new` works offline and from a global install), the in-TUI markdown table renderer stops splitting on escaped `\|` pipes, and the release pipeline gains a cross-compiled standalone-binary matrix — macOS/Linux **plus a first-class Windows x64 `.exe`** — built on one Linux runner and auto-attached to each GitHub release.
|
|
215
215
|
|
|
216
216
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
217
217
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -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.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
|
+
- **[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.
|
|
208
210
|
- **[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.
|
|
209
211
|
- **[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.
|
|
210
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.
|
|
211
|
-
- **[0.7.7]** (2026-06-23) — Long-running-process hygiene & input robustness: backgrounded grandchildren (`next dev &`, daemons) are now reaped at the turn boundary via per-command process groups — closing the climbing `next-server` RSS leak — while the prompt gains recursive fuzzy `@path` search, ANSI-safe and idle-merged bracketed paste, a configurable OSC 52 clipboard cap, per-role ralplan model routing, and leaked-reasoning-tag stripping on salvaged answers.
|
|
212
|
-
- **[0.7.6]** (2026-06-23) — Release & docs plumbing: the running version's release notes now travel inside the `--compile` single binary (so `/whats-new` works offline and from a global install), the in-TUI markdown table renderer stops splitting on escaped `\|` pipes, and the release pipeline gains a cross-compiled standalone-binary matrix — macOS/Linux **plus a first-class Windows x64 `.exe`** — built on one Linux runner and auto-attached to each GitHub release.
|
|
213
213
|
|
|
214
214
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
215
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/agent/engine.ts
CHANGED
|
@@ -272,11 +272,6 @@ export interface AgentLoopResult {
|
|
|
272
272
|
doneReason?: string;
|
|
273
273
|
/** Summed provider token usage across the turn's steps, when reported. */
|
|
274
274
|
usage?: { inputTokens: number; outputTokens: number };
|
|
275
|
-
/** Repo-relative paths of files this turn SUCCESSFULLY wrote/edited, sorted and
|
|
276
|
-
* de-duplicated. Lets a caller (task/team subagent, external orchestrator) collect
|
|
277
|
-
* the run's code artifacts instead of inferring an empty change set after real edits.
|
|
278
|
-
* Omitted when the turn mutated no file. */
|
|
279
|
-
mutatedFiles?: string[];
|
|
280
275
|
}
|
|
281
276
|
|
|
282
277
|
|
|
@@ -369,14 +364,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
369
364
|
let step = 1;
|
|
370
365
|
const acc = { inputTokens: 0, outputTokens: 0 };
|
|
371
366
|
let sawUsage = false;
|
|
372
|
-
|
|
373
|
-
// result so callers can collect the run's code artifacts rather than report an empty
|
|
374
|
-
// change set after real edits (the "finalArtifacts is empty" orchestrator failure).
|
|
375
|
-
const mutatedFiles = new Set<string>();
|
|
376
|
-
const finish = (r: AgentLoopResult): AgentLoopResult => {
|
|
377
|
-
const withArtifacts = mutatedFiles.size ? { ...r, mutatedFiles: [...mutatedFiles].sort() } : r;
|
|
378
|
-
return sawUsage ? { ...withArtifacts, usage: { ...acc } } : withArtifacts;
|
|
379
|
-
};
|
|
367
|
+
const finish = (r: AgentLoopResult): AgentLoopResult => (sawUsage ? { ...r, usage: { ...acc } } : r);
|
|
380
368
|
// Salvage a spin-stop into a useful answer (C): instead of returning a bare
|
|
381
369
|
// "Stopped: …" — throwing away everything found this turn — do ONE final no-tools
|
|
382
370
|
// call asking the model to answer with what it already has. Mirrors the
|
|
@@ -1054,15 +1042,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
1054
1042
|
for (let i = 0; i < toolCalls.length; i++) {
|
|
1055
1043
|
if (!results[i].executed || !results[i].success) continue;
|
|
1056
1044
|
const t = toolCalls[i].tool;
|
|
1057
|
-
if (t === "write" || t === "edit")
|
|
1058
|
-
sawMutation = true;
|
|
1059
|
-
// Record the actual file so the run's artifacts can be collected/returned.
|
|
1060
|
-
const rawPath = toolCalls[i].arguments?.filePath ?? toolCalls[i].arguments?.path;
|
|
1061
|
-
if (typeof rawPath === "string" && rawPath.trim()) {
|
|
1062
|
-
const rel = path.relative(cwd, path.resolve(cwd, rawPath)) || rawPath;
|
|
1063
|
-
mutatedFiles.add(rel.split(path.sep).join("/"));
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1045
|
+
if (t === "write" || t === "edit") sawMutation = true;
|
|
1066
1046
|
else if (t === "bash") {
|
|
1067
1047
|
const cmd = String(toolCalls[i].arguments?.command ?? "");
|
|
1068
1048
|
if (isVerificationSignal(cmd, results[i].output)) sawVerification = true;
|
package/src/agent/task-tool.ts
CHANGED
|
@@ -207,12 +207,12 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
|
|
|
207
207
|
const trace: string[] = [];
|
|
208
208
|
let lastTarget = "";
|
|
209
209
|
let currentStep = 0;
|
|
210
|
-
// Round-8 (architect ref 7-Round7Workflow):
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
210
|
+
// Round-8 (architect ref 7-Round7Workflow): count the subagent's SUCCESSFUL
|
|
211
|
+
// calls so the parent can audit a "Changed Files:" claim against observed
|
|
212
|
+
// reality. File-writing tools (write/edit/mkdir/delete) are tracked apart from
|
|
213
|
+
// bash: read-only bash (e.g. `bun test`) MUST NOT count as edit evidence, but
|
|
214
|
+
// bash CAN mutate, so the audit message distinguishes the two cases.
|
|
215
|
+
let fileMutations = 0;
|
|
216
216
|
let bashRuns = 0;
|
|
217
217
|
emit({ role: role.id, kind: "start", detail: taskText, maxSteps, model });
|
|
218
218
|
const result = await runAgentLoop(history, {
|
|
@@ -236,7 +236,10 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
|
|
|
236
236
|
}
|
|
237
237
|
},
|
|
238
238
|
onToolResult: (tool, success, output) => {
|
|
239
|
-
if (success
|
|
239
|
+
if (success) {
|
|
240
|
+
if (tool === "write" || tool === "edit" || tool === "mkdir" || tool === "delete") fileMutations++;
|
|
241
|
+
else if (tool === "bash") bashRuns++;
|
|
242
|
+
}
|
|
240
243
|
const label = lastTarget || tool;
|
|
241
244
|
const summary = firstUsefulLine(output);
|
|
242
245
|
const suffix = summary ? ` — ${summary}` : "";
|
|
@@ -260,24 +263,16 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
|
|
|
260
263
|
const tokNote = result.usage ? `, ${result.usage.inputTokens + result.usage.outputTokens} tok` : "";
|
|
261
264
|
const header = `[${role.title} subagent] ${complete ? "completed" : "stopped"} in ${result.steps} step(s) on ${model}${tokNote}.`;
|
|
262
265
|
const body = trace.length ? `\nSteps:\n${trace.join("\n")}` : "";
|
|
263
|
-
// Collect/return the run's code artifacts: the engine reports every file the
|
|
264
|
-
// subagent actually wrote/edited, so the parent (and any orchestrator above it)
|
|
265
|
-
// gets a CONCRETE change set instead of an unverifiable "Changed Files:" prose
|
|
266
|
-
// claim. Empty list ⇒ no observed file mutation.
|
|
267
|
-
const changedFiles = result.mutatedFiles ?? [];
|
|
268
|
-
const artifactNote = changedFiles.length
|
|
269
|
-
? `\nChanged files (${changedFiles.length}): ${changedFiles.join(", ")}`
|
|
270
|
-
: "";
|
|
271
266
|
// Parent-side audit: a mutating role that "completed" without a successful file
|
|
272
|
-
// mutation (write/edit) likely changed nothing — flag the claim.
|
|
273
|
-
// separately: it CAN mutate, so an only-bash run downgrades to
|
|
274
|
-
// independently" instead of the stronger UNVERIFIED.
|
|
275
|
-
const audit = complete && !role.readOnly &&
|
|
267
|
+
// mutation (write/edit/mkdir/delete) likely changed nothing — flag the claim.
|
|
268
|
+
// bash is tracked separately: it CAN mutate, so an only-bash run downgrades to
|
|
269
|
+
// "verify independently" instead of the stronger UNVERIFIED.
|
|
270
|
+
const audit = complete && !role.readOnly && fileMutations === 0
|
|
276
271
|
? bashRuns === 0
|
|
277
272
|
? `\n[parent audit] No successful write/edit/bash was observed in this run — treat any "Changed Files:" claims above as UNVERIFIED.`
|
|
278
273
|
: `\n[parent audit] No successful write/edit was observed (only bash ran); bash may or may not have mutated files — verify any "Changed Files:" claims above independently.`
|
|
279
274
|
: "";
|
|
280
|
-
return { success: complete, output: `${header}${body}
|
|
275
|
+
return { success: complete, output: `${header}${body}\n\nResult:\n${fenceSubagentReport(detail)}${audit}` };
|
|
281
276
|
};
|
|
282
277
|
|
|
283
278
|
return async (args: Record<string, any>, cwd: string): Promise<ToolResult> => {
|
package/src/agent/tool-output.ts
CHANGED
|
@@ -83,7 +83,7 @@ export async function spillToolResult(tool: string, output: string, cwd: string)
|
|
|
83
83
|
await fs.mkdir(dir, { recursive: true });
|
|
84
84
|
const safeTool = tool.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 32) || "tool";
|
|
85
85
|
const stamp = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
86
|
-
const rel =
|
|
86
|
+
const rel = path.join(".jeo", "artifacts", "tool-results", `${stamp}-${safeTool}.txt`);
|
|
87
87
|
await fs.writeFile(path.join(cwd, rel), output, "utf-8");
|
|
88
88
|
// Retention so a long session can't grow the artifact dir without bound.
|
|
89
89
|
await pruneToolArtifacts(dir);
|
package/src/ai/model-manager.ts
CHANGED
|
@@ -533,8 +533,7 @@ export function streamIdleMs(env?: Record<string, string | undefined>): number {
|
|
|
533
533
|
* built-in default. Mirrors streamIdleMs so a slow non-interactive turn (callLlm without
|
|
534
534
|
* onToken) can relax the cap via JEO_CALL_TIMEOUT_MS without a code change. */
|
|
535
535
|
export function callTimeoutMs(env?: Record<string, string | undefined>): number {
|
|
536
|
-
const
|
|
537
|
-
const raw = jeoEnv("CALL_TIMEOUT_MS", e) ?? e["ANTHROPIC_TIMEOUT"];
|
|
536
|
+
const raw = jeoEnv("CALL_TIMEOUT_MS", env);
|
|
538
537
|
const n = raw !== undefined ? parseInt(raw, 10) : NaN;
|
|
539
538
|
return Number.isFinite(n) && n > 0 ? n : DEFAULT_CALL_TIMEOUT_MS;
|
|
540
539
|
}
|
|
@@ -135,7 +135,7 @@ function anthropicThinkingMode(model: string): AnthropicThinkingMode {
|
|
|
135
135
|
const v = parseAnthropicVersion(model);
|
|
136
136
|
if (!v) return "budget";
|
|
137
137
|
if (v.major > 4 || (v.major === 4 && v.minor >= 6)) return "adaptive";
|
|
138
|
-
|
|
138
|
+
if (v.major === 4 && v.minor === 5) return "budget-effort";
|
|
139
139
|
return "budget";
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -266,7 +266,7 @@ export function anthropicPayload(
|
|
|
266
266
|
// Budget-based extended thinking. `display: "summarized"` keeps human-readable thought
|
|
267
267
|
// streaming. The 4.5 (budget-effort) transport also carries an output_config effort.
|
|
268
268
|
payload.thinking = { type: "enabled", budget_tokens: thinkingBudget, display: "summarized" };
|
|
269
|
-
|
|
269
|
+
if (thinkingMode === "budget-effort") payload.output_config = { effort: anthropicAdaptiveEffort(effort) };
|
|
270
270
|
}
|
|
271
271
|
} else if (includeTemperature && options.temperature !== undefined) {
|
|
272
272
|
payload.temperature = options.temperature;
|
|
@@ -281,7 +281,6 @@ export function anthropicPayload(
|
|
|
281
281
|
if (stream) payload.stream = true;
|
|
282
282
|
const system = anthropicSystemBlocks(systemPrompt, model, credential, payload);
|
|
283
283
|
if (system) payload.system = system;
|
|
284
|
-
console.log("PAYLOAD:", JSON.stringify(payload));
|
|
285
284
|
return JSON.stringify(payload);
|
|
286
285
|
}
|
|
287
286
|
|
package/src/cli/runner.ts
CHANGED
|
@@ -103,8 +103,8 @@ export const COMMANDS: readonly CommandSpec[] = [
|
|
|
103
103
|
},
|
|
104
104
|
{
|
|
105
105
|
name: "skills",
|
|
106
|
-
summary: "List bundled workflow skills (jeo skills <name> for details).",
|
|
107
|
-
usage: "skills [name]",
|
|
106
|
+
summary: "List bundled workflow skills, or sync them into ~/.jeo/skills (jeo skills <name> for details).",
|
|
107
|
+
usage: "skills [list|read <name>|sync [--check|--force] [dir]|--write [dir]] [--json]",
|
|
108
108
|
loader: async () => {
|
|
109
109
|
const m = await import("../commands/skills");
|
|
110
110
|
return args => m.runSkillsCommand(args);
|
|
@@ -299,7 +299,6 @@ export function renderHelp(ctx: DispatchContext): string {
|
|
|
299
299
|
lines.push("Options:");
|
|
300
300
|
lines.push(" -v, --version Show version.");
|
|
301
301
|
lines.push(" -h, --help Show help.");
|
|
302
|
-
lines.push(" -q, --quiet Suppress startup banners and logs.");
|
|
303
302
|
lines.push(" --model <id> Use a session model for launch/one-shot.");
|
|
304
303
|
lines.push(" --provider <name> Start launch on a provider default (anthropic/openai/gemini/antigravity/ollama).");
|
|
305
304
|
lines.push(" --smol|--slow|--plan Start launch with the configured model role tier.");
|
|
@@ -321,7 +320,7 @@ export function renderCommandHelp(spec: CommandSpec, ctx: DispatchContext): stri
|
|
|
321
320
|
const VALUE_FLAGS = new Set(["--worktree", "--model", "--provider", "--thinking", "--max-steps", "--append-system-prompt", "--skills", "--tools", "--system-prompt"]);
|
|
322
321
|
const OPTIONAL_UUID_FLAGS = new Set(["--resume", "--continue", "-c"]);
|
|
323
322
|
const VALUE_PREFIXES = ["--worktree=", "--model=", "--provider=", "--thinking=", "--max-steps=", "--append-system-prompt=", "--skills=", "--tools=", "--system-prompt="];
|
|
324
|
-
const LAUNCH_ONLY_FLAGS = new Set(["--tmux", "--no-tui", "--no-session", "--list", "--smol", "--slow", "--plan", "-p", "--print", "
|
|
323
|
+
const LAUNCH_ONLY_FLAGS = new Set(["--tmux", "--no-tui", "--no-session", "--list", "--smol", "--slow", "--plan", "-p", "--print", "--no-skills", "--no-tools"]);
|
|
325
324
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
326
325
|
|
|
327
326
|
function flagName(arg: string): string {
|
|
@@ -20,7 +20,6 @@ export interface LaunchFlags {
|
|
|
20
20
|
thinking?: ThinkLevel;
|
|
21
21
|
errors: string[];
|
|
22
22
|
print?: boolean;
|
|
23
|
-
quiet: boolean;
|
|
24
23
|
appendSystemPromptRaw?: string;
|
|
25
24
|
appendSystemPrompt?: string;
|
|
26
25
|
noSkills: boolean;
|
|
@@ -64,7 +63,7 @@ export function fastThinkingLevelForModel(modelId: string): ThinkLevel | undefin
|
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
export function parseFlags(args: string[], cwd: string = process.cwd()): LaunchFlags {
|
|
67
|
-
const flags: LaunchFlags = { list: false, resume: false, noSession: false, noTui: false, maxSteps: 0, message: "", tmux: false, errors: [], print: false,
|
|
66
|
+
const flags: LaunchFlags = { list: false, resume: false, noSession: false, noTui: false, maxSteps: 0, message: "", tmux: false, errors: [], print: false, noSkills: false, noTools: false };
|
|
68
67
|
const rest: string[] = [];
|
|
69
68
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
70
69
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -78,8 +77,6 @@ export function parseFlags(args: string[], cwd: string = process.cwd()): LaunchF
|
|
|
78
77
|
} else if (a === "-p" || a === "--print") {
|
|
79
78
|
flags.print = true;
|
|
80
79
|
flags.noTui = true;
|
|
81
|
-
} else if (a === "-q" || a === "--quiet") {
|
|
82
|
-
flags.quiet = true;
|
|
83
80
|
} else if (a === "--tmux") {
|
|
84
81
|
flags.tmux = true;
|
|
85
82
|
} else if (a === "--worktree") {
|
package/src/commands/launch.ts
CHANGED
|
@@ -309,7 +309,6 @@ export function formatResumeHint(sessionId: string): string {
|
|
|
309
309
|
export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
310
310
|
let cwd = process.cwd();
|
|
311
311
|
const flags = parseFlags(args, cwd);
|
|
312
|
-
const isQuiet = flags.quiet || flags.print || jeoEnv("QUIET") === "1";
|
|
313
312
|
if (flags.errors.length) {
|
|
314
313
|
for (const err of flags.errors) {
|
|
315
314
|
console.error(`error: ${err}`);
|
|
@@ -323,7 +322,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
323
322
|
if (wt !== cwd) {
|
|
324
323
|
process.chdir(wt);
|
|
325
324
|
cwd = wt;
|
|
326
|
-
if (jeoEnv("TMUX_LAUNCHED") !== "1"
|
|
325
|
+
if (jeoEnv("TMUX_LAUNCHED") !== "1") console.log(`Using worktree: ${wt}`);
|
|
327
326
|
}
|
|
328
327
|
}
|
|
329
328
|
let branch: string | undefined;
|
|
@@ -657,7 +656,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
657
656
|
if (flags.resume) {
|
|
658
657
|
const id = flags.resumeId ?? (await latestSessionId(cwd));
|
|
659
658
|
if (!id) {
|
|
660
|
-
|
|
659
|
+
console.log("No session to resume. Starting a new one.");
|
|
661
660
|
sessionId = (await createSession(cwd, undefined, sessionModel)).id;
|
|
662
661
|
} else {
|
|
663
662
|
try {
|
|
@@ -668,9 +667,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
668
667
|
// pinned one (flags.model/role/provider → initialSessionModel wins).
|
|
669
668
|
if (!initialSessionModel && header.model) sessionModel = header.model;
|
|
670
669
|
const modelNote = sessionModel ? ` · model ${sessionModel}` : "";
|
|
671
|
-
|
|
670
|
+
console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
|
|
672
671
|
} catch (err) {
|
|
673
|
-
|
|
672
|
+
console.log(`Could not resume ${id}: ${(err as Error).message}. Starting fresh.`);
|
|
674
673
|
sessionId = (await createSession(cwd, undefined, sessionModel)).id;
|
|
675
674
|
}
|
|
676
675
|
}
|
|
@@ -1267,7 +1266,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1267
1266
|
renderWelcome({ ...welcomeData, cols: terminalSize().cols });
|
|
1268
1267
|
// gjc-style fresh-start clear so the banner opens atop a clean screen. TTY only,
|
|
1269
1268
|
// never mid-turn (scrollback flood). ponytail: add an opt-out env if anyone misses their scrollback.
|
|
1270
|
-
if (process.stdout.isTTY
|
|
1269
|
+
if (process.stdout.isTTY) process.stdout.write(clearScreen());
|
|
1271
1270
|
// Launch sweep: the forge mark's gradient loops seamlessly (default 2 full
|
|
1272
1271
|
// cycles, JEO_WELCOME_ANIM_CYCLES overrides), ending on the static banner.
|
|
1273
1272
|
// Truecolor TTYs only; JEO_NO_WELCOME_ANIM=1 opts out.
|
|
@@ -1277,10 +1276,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1277
1276
|
detectColorLevel(process.env, true) === ColorLevel.TrueColor &&
|
|
1278
1277
|
jeoEnv("NO_WELCOME_ANIM") !== "1";
|
|
1279
1278
|
const sweepCycles = Math.min(10, Math.max(1, Number(jeoEnv("WELCOME_ANIM_CYCLES")) || 2));
|
|
1280
|
-
if (
|
|
1281
|
-
|
|
1282
|
-
else console.log(renderWelcome(welcomeData).join("\n"));
|
|
1283
|
-
}
|
|
1279
|
+
if (sweepable) await playWelcomeSweep(welcomeData, { cycles: sweepCycles });
|
|
1280
|
+
else console.log(renderWelcome(welcomeData).join("\n"));
|
|
1284
1281
|
|
|
1285
1282
|
// Surface the "New version" banner reliably: render ONCE from the on-disk cache
|
|
1286
1283
|
// instantly (no network wait, works offline — the common path after the first
|
|
@@ -1288,7 +1285,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1288
1285
|
// bump still shows it this launch. Both must run BEFORE the prompt is armed.
|
|
1289
1286
|
let updateBannerShown = false;
|
|
1290
1287
|
const showUpdateBanner = (u: { current: string; latest: string; updateAvailable: boolean } | null): void => {
|
|
1291
|
-
if (updateBannerShown || !u?.updateAvailable
|
|
1288
|
+
if (updateBannerShown || !u?.updateAvailable) return;
|
|
1292
1289
|
updateBannerShown = true;
|
|
1293
1290
|
console.log(renderUpdateBox(u.current, u.latest).join("\n"));
|
|
1294
1291
|
};
|
|
@@ -1297,17 +1294,15 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1297
1294
|
// First launch after a version bump: surface the bundled release notes ONCE
|
|
1298
1295
|
// (offline, from the new package's CHANGELOG.md) and record the seen version
|
|
1299
1296
|
// so it never repeats. Screen-safe: prints BEFORE the prompt is armed.
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
}
|
|
1310
|
-
if (!LaunchTui.usable(flags.noTui) && !isQuiet) console.log("(plain output)");
|
|
1297
|
+
try {
|
|
1298
|
+
const whatsNew = await consumeLaunchWhatsNew({
|
|
1299
|
+
cols: Math.min(100, Math.max(40, (process.stdout.columns ?? 80) - 2)),
|
|
1300
|
+
unicode: supportsUnicode(),
|
|
1301
|
+
color: welcomeTheme.color,
|
|
1302
|
+
});
|
|
1303
|
+
if (whatsNew && whatsNew.length) console.log(whatsNew.join("\n"));
|
|
1304
|
+
} catch { /* release notes are a courtesy; never block launch */ }
|
|
1305
|
+
if (!LaunchTui.usable(flags.noTui)) console.log("(plain output)");
|
|
1311
1306
|
|
|
1312
1307
|
const useTui = LaunchTui.usable(flags.noTui);
|
|
1313
1308
|
const runSkillInvocation = async (skill: SkillDoc, intent: string, invokedAs?: string): Promise<void> => {
|
|
@@ -4430,11 +4425,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4430
4425
|
// of blocking up to 20s on a final LLM call. The child writes MEMORY.md atomically.
|
|
4431
4426
|
if (sessionUsage.turns > 0) {
|
|
4432
4427
|
const spawned = await spawnDetachedDistill(history, cwd, sessionModel || defaultModel);
|
|
4433
|
-
if (spawned
|
|
4428
|
+
if (spawned) console.log(chalk.gray("(session memory distilling in background)"));
|
|
4434
4429
|
}
|
|
4435
4430
|
// gjc-parity resume pointer (logs/gjc-tui-study analysis Gap C): leave the exact
|
|
4436
4431
|
// resume command in scrollback on exit, mirroring the --list handler's convention.
|
|
4437
|
-
if (sessionId && !flags.noSession
|
|
4432
|
+
if (sessionId && !flags.noSession) console.log(formatResumeHint(sessionId));
|
|
4438
4433
|
process.removeListener("SIGINT", handleCtrlC);
|
|
4439
4434
|
process.stdin.off("data", handleCtrlCByte);
|
|
4440
4435
|
drainPromptListeners();
|
package/src/commands/skills.ts
CHANGED
|
@@ -1,7 +1,109 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { SKILLS, getSkillFrom, formatSkill, loadSkills, skillDirs, bundledSkillFileContent } from "../skills/catalog";
|
|
4
5
|
import { getLocalJeoDir } from "../agent/state";
|
|
6
|
+
import { jeoEnv } from "../util/env";
|
|
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
|
+
}
|
|
16
|
+
|
|
17
|
+
export type SkillSyncStatus = "missing" | "up-to-date" | "differs";
|
|
18
|
+
export type SkillSyncAction = "installed" | "overwritten" | "preserved" | "unchanged" | "none";
|
|
19
|
+
|
|
20
|
+
export interface SkillSyncEntry {
|
|
21
|
+
name: string;
|
|
22
|
+
/** State of the on-disk file BEFORE any write this run. */
|
|
23
|
+
status: SkillSyncStatus;
|
|
24
|
+
/** What sync did (or, for --check, "none"). */
|
|
25
|
+
action: SkillSyncAction;
|
|
26
|
+
path: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SkillSyncResult {
|
|
30
|
+
dir: string;
|
|
31
|
+
entries: SkillSyncEntry[];
|
|
32
|
+
/** true when any bundled skill is missing or differs from its on-disk copy. */
|
|
33
|
+
drift: boolean;
|
|
34
|
+
/** Number of files actually written this run. */
|
|
35
|
+
wrote: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Reconcile the bundled workflow skills against `dir` (default {@link userSkillsDir}).
|
|
39
|
+
* Default install preserves existing local files (gjc `setup defaults` parity):
|
|
40
|
+
* missing skills are installed, differing ones preserved unless `force`. `check`
|
|
41
|
+
* is a pure drift report — it writes nothing. Returns a structured result so the
|
|
42
|
+
* CLI wrapper owns all console output / exit codes. */
|
|
43
|
+
export async function syncBundledSkills(
|
|
44
|
+
dir: string,
|
|
45
|
+
opts: { check?: boolean; force?: boolean } = {},
|
|
46
|
+
): Promise<SkillSyncResult> {
|
|
47
|
+
const entries: SkillSyncEntry[] = [];
|
|
48
|
+
let wrote = 0;
|
|
49
|
+
if (!opts.check) await fs.mkdir(dir, { recursive: true });
|
|
50
|
+
for (const s of SKILLS) {
|
|
51
|
+
const file = path.join(dir, `${s.name}.md`);
|
|
52
|
+
const want = bundledSkillFileContent(s);
|
|
53
|
+
let have: string | null = null;
|
|
54
|
+
try { have = await fs.readFile(file, "utf-8"); } catch { /* missing */ }
|
|
55
|
+
const status: SkillSyncStatus = have === null ? "missing" : have === want ? "up-to-date" : "differs";
|
|
56
|
+
let action: SkillSyncAction = "none";
|
|
57
|
+
if (!opts.check) {
|
|
58
|
+
if (status === "missing") {
|
|
59
|
+
await fs.writeFile(file, want, "utf-8");
|
|
60
|
+
action = "installed";
|
|
61
|
+
wrote++;
|
|
62
|
+
} else if (status === "differs") {
|
|
63
|
+
if (opts.force) {
|
|
64
|
+
await fs.writeFile(file, want, "utf-8");
|
|
65
|
+
action = "overwritten";
|
|
66
|
+
wrote++;
|
|
67
|
+
} else {
|
|
68
|
+
action = "preserved";
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
action = "unchanged";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
entries.push({ name: s.name, status, action, path: file });
|
|
75
|
+
}
|
|
76
|
+
return { dir, entries, drift: entries.some(e => e.status !== "up-to-date"), wrote };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runSkillsSync(cleanArgs: string[], isJson: boolean, cwd: string): Promise<void> {
|
|
80
|
+
const check = cleanArgs.includes("--check");
|
|
81
|
+
const force = cleanArgs.includes("--force");
|
|
82
|
+
const dirArg = cleanArgs.slice(1).find(a => !a.startsWith("--"));
|
|
83
|
+
const dir = dirArg ? path.resolve(cwd, dirArg) : userSkillsDir();
|
|
84
|
+
const result = await syncBundledSkills(dir, { check, force });
|
|
85
|
+
|
|
86
|
+
if (isJson) {
|
|
87
|
+
console.log(JSON.stringify({ ...result, mode: check ? "check" : force ? "force" : "install" }, null, 2));
|
|
88
|
+
} else if (check) {
|
|
89
|
+
console.log(`\n=== jeo skills sync --check ===\nTarget: ${dir}\n`);
|
|
90
|
+
for (const e of result.entries) console.log(` ${e.status.padEnd(11)} ${e.name}`);
|
|
91
|
+
console.log(
|
|
92
|
+
result.drift
|
|
93
|
+
? `\nDrift detected. Run 'jeo skills sync' to install missing skills, or '--force' to overwrite differing local copies.`
|
|
94
|
+
: `\nAll ${result.entries.length} bundled skills are in sync.`,
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
console.log(`\n=== jeo skills sync ===\nTarget: ${dir}\n`);
|
|
98
|
+
for (const e of result.entries) console.log(` ${e.action.padEnd(11)} ${e.name}`);
|
|
99
|
+
const preserved = result.entries.filter(e => e.action === "preserved").length;
|
|
100
|
+
console.log(`\nWrote ${result.wrote} file(s)${preserved ? `, preserved ${preserved} local copy(ies) (use --force to overwrite)` : ""}.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// --check is CI-usable: non-zero exit signals drift (gjc `setup defaults --check` parity).
|
|
104
|
+
if (check && result.drift) process.exitCode = 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
5
107
|
|
|
6
108
|
function editDistance(a: string, b: string): number {
|
|
7
109
|
const m = a.length, n = b.length;
|
|
@@ -40,12 +142,20 @@ export async function runSkillsCommand(args: string[] = []): Promise<void> {
|
|
|
40
142
|
await fs.mkdir(dir, { recursive: true });
|
|
41
143
|
for (const s of SKILLS) {
|
|
42
144
|
const file = path.join(dir, `${s.name}.md`);
|
|
43
|
-
await fs.writeFile(file,
|
|
145
|
+
await fs.writeFile(file, bundledSkillFileContent(s), "utf-8");
|
|
44
146
|
}
|
|
45
147
|
console.log(`Wrote ${SKILLS.length} skill docs to ${dir}`);
|
|
46
148
|
return;
|
|
47
149
|
}
|
|
48
150
|
|
|
151
|
+
// `jeo skills sync [--check|--force] [dir]` — bundled skill inspection & safe install
|
|
152
|
+
// (gjc `setup defaults --check/--force` parity): install missing bundled skills into
|
|
153
|
+
// ~/.jeo/skills, preserve existing local copies by default, and report drift.
|
|
154
|
+
if (cleanArgs[0] === "sync") {
|
|
155
|
+
await runSkillsSync(cleanArgs, isJson, cwd);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
49
159
|
// List/lookup over the MERGED set (bundled + ~/.jeo/skills + ~/.agents/skills + project dirs), matching the REPL /skill.
|
|
50
160
|
const skills = await loadSkills(cwd);
|
|
51
161
|
const command = cleanArgs[0];
|
|
@@ -60,6 +170,7 @@ export async function runSkillsCommand(args: string[] = []): Promise<void> {
|
|
|
60
170
|
console.log(` ${s.name.padEnd(16)} ${s.summary}`);
|
|
61
171
|
}
|
|
62
172
|
console.log("\nInvoke: /skill <name> [intent] · $<name> [intent] · skill-owned slash aliases (e.g. /speckit.plan)");
|
|
173
|
+
console.log("Install/inspect bundled defaults: 'jeo skills sync' (preserve local) · '--check' (drift report) · '--force' (overwrite local)");
|
|
63
174
|
console.log("Discovery dirs (later wins on name clash; JEO_SKILLS_DIR adds more):");
|
|
64
175
|
for (const d of skillDirs(cwd)) console.log(` ${d}`);
|
|
65
176
|
console.log("");
|
package/src/commands/team.ts
CHANGED
|
@@ -431,8 +431,8 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
|
|
|
431
431
|
// LLM summary failure does not halt team
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
let fileMutations = 0; // round-8 parent audit: successful write/edit/mkdir/delete
|
|
434
435
|
let bashRuns = 0; // bash counted apart so read-only bash isn't edit evidence
|
|
435
|
-
// (the actual write/edit set comes from result.mutatedFiles)
|
|
436
436
|
const result = await runAgentLoop(history, {
|
|
437
437
|
cwd: ctx.cwd,
|
|
438
438
|
model,
|
|
@@ -458,7 +458,10 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
|
|
|
458
458
|
}
|
|
459
459
|
},
|
|
460
460
|
onToolResult: (tool, ok) => {
|
|
461
|
-
if (ok
|
|
461
|
+
if (ok) {
|
|
462
|
+
if (tool === "write" || tool === "edit" || tool === "mkdir" || tool === "delete") fileMutations++;
|
|
463
|
+
else if (tool === "bash") bashRuns++;
|
|
464
|
+
}
|
|
462
465
|
log(formatRalphStreamEvent(ok ? "complete" : "error", `tool ${tool}`, renderOpts));
|
|
463
466
|
},
|
|
464
467
|
onNotice: msg => log(formatRalphStreamEvent("step", msg, renderOpts)),
|
|
@@ -483,15 +486,7 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
|
|
|
483
486
|
return false;
|
|
484
487
|
}
|
|
485
488
|
|
|
486
|
-
|
|
487
|
-
// subagent actually wrote/edited, so the task's change set is concrete instead of
|
|
488
|
-
// an unverifiable prose claim. An empty list ⇒ no observed file mutation.
|
|
489
|
-
const changedFiles = result.mutatedFiles ?? [];
|
|
490
|
-
if (changedFiles.length > 0) {
|
|
491
|
-
log(formatRalphStreamEvent("complete", `${role.title} changed ${changedFiles.length} file(s): ${changedFiles.join(", ")}`, renderOpts));
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (!role.readOnly && changedFiles.length === 0) {
|
|
489
|
+
if (!role.readOnly && fileMutations === 0) {
|
|
495
490
|
// Round-8: a mutating role finished without a successful file mutation — the
|
|
496
491
|
// task may be legitimately read-only, but its "Changed Files:" claim is
|
|
497
492
|
// unverified. bash is tracked apart: an only-bash run MIGHT have mutated.
|
package/src/skills/catalog.ts
CHANGED
|
@@ -70,6 +70,13 @@ export function formatSkill(s: SkillDoc): string {
|
|
|
70
70
|
].filter(Boolean).join("\n");
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** Canonical on-disk content for a bundled skill — the exact bytes `jeo skills
|
|
74
|
+
* --write` / `jeo skills sync` materialize into a `<name>.md` file. Prefers the
|
|
75
|
+
* original SKILL.md text; falls back to a decorated render for skills without raw. */
|
|
76
|
+
export function bundledSkillFileContent(s: SkillDoc): string {
|
|
77
|
+
return s.raw || `# ${s.name}\n\n${formatSkill(s)}\n`;
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
function compactSkillExecutionBrief(skill: SkillDoc): string {
|
|
74
81
|
const aliases = skillSlashAliases(skill);
|
|
75
82
|
let details = skill.details;
|
|
@@ -108,45 +108,19 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
|
|
|
108
108
|
continue;
|
|
109
109
|
}
|
|
110
110
|
// assistant: a JSON tool call (compact ledger lines) or a prose/done reply.
|
|
111
|
-
// A tool-call message
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// The protocol allows a leading `"reasoning"` field on a tool call; surface that
|
|
125
|
-
// narration (clipped) instead of losing it, and never re-dump it as raw JSON.
|
|
126
|
-
const reasoningText = parsed && typeof (parsed as { reasoning?: unknown }).reasoning === "string"
|
|
127
|
-
? String((parsed as { reasoning?: unknown }).reasoning).trim()
|
|
128
|
-
: "";
|
|
129
|
-
// `looksLikeToolAttempt` flags a (possibly malformed/truncated) tool call so its
|
|
130
|
-
// raw JSON is suppressed even when parsing fails — e.g. a bounced reply the model
|
|
131
|
-
// later resent. Prose narrating a call is the text BEFORE the first `{` (the start
|
|
132
|
-
// of the parsed object): a prose-prefixed call keeps its lead-in; a JSON-first or
|
|
133
|
-
// reasoning-first call has none (its narration comes from `reasoningText`).
|
|
134
|
-
const looksLikeToolAttempt = /\{\s*"tools?"\s*:/.test(m.content);
|
|
135
|
-
const firstBrace = m.content.indexOf("{");
|
|
136
|
-
const prosePrefix = firstBrace > 0
|
|
137
|
-
? m.content.slice(0, firstBrace).replace(/```(?:json)?[ \t]*\r?\n?/i, "").trim()
|
|
138
|
-
: "";
|
|
139
|
-
|
|
140
|
-
// A reply whose content (after an optional ```json fence) STARTS with `{` is a JSON
|
|
141
|
-
// emission, not prose — even a giant malformed reasoning blob the model couldn't close.
|
|
142
|
-
const startsWithJson = m.content.trimStart().replace(/^```(?:json)?[ \t]*\r?\n?/i, "").startsWith("{");
|
|
143
|
-
// Any JSON object/tool-call emission — fenced, prose-prefixed, reasoning-prefixed,
|
|
144
|
-
// or one we could not parse into a renderable call — must never reach scrollback as
|
|
145
|
-
// raw escaped JSON (the "/resume reads a wall of broken data" report).
|
|
146
|
-
const isJsonEmission = looksLikeToolAttempt || startsWithJson;
|
|
147
|
-
const narration = prosePrefix || reasoningText;
|
|
148
|
-
|
|
149
|
-
|
|
111
|
+
// A tool-call message IS a JSON object (optionally inside a ```json fence) — so
|
|
112
|
+
// only parse when the content actually begins with `{` after stripping a leading
|
|
113
|
+
// fence. This renders fenced/decorated tool calls as cards (the "/resume shows
|
|
114
|
+
// raw JSON and breaks the TUI" bug — naive JSON.parse failed on any fence and
|
|
115
|
+
// dumped the block) while prose that merely CONTAINS tool-like JSON stays prose.
|
|
116
|
+
const stripped = m.content.trim().replace(/^```(?:json)?[ \t]*\r?\n?/i, "").trimStart();
|
|
117
|
+
const looksLikeCall = stripped.startsWith("{");
|
|
118
|
+
const parsed = looksLikeCall
|
|
119
|
+
? tryExtractJsonObject<{ tool?: unknown; tools?: unknown; arguments?: unknown }>(
|
|
120
|
+
m.content,
|
|
121
|
+
{ preferKeys: ["tool", "tools"] },
|
|
122
|
+
)
|
|
123
|
+
: null;
|
|
150
124
|
const calls: { tool: string; arguments?: unknown }[] =
|
|
151
125
|
parsed && typeof parsed.tool === "string"
|
|
152
126
|
? [{ tool: parsed.tool, arguments: parsed.arguments }]
|
|
@@ -156,27 +130,13 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
|
|
|
156
130
|
.map(c => ({ tool: c.tool as string, arguments: c.arguments }))
|
|
157
131
|
: [];
|
|
158
132
|
const toolCalls = calls.filter(c => c.tool !== "done");
|
|
159
|
-
|
|
160
|
-
// JSON is the final token; an illustrative JSON snippet quoted inside a prose reply is
|
|
161
|
-
// not, and it carries meaningful prose AFTER the object. Use both signals so a real
|
|
162
|
-
// prose-prefixed call renders as a ledger line while a sentence that merely shows
|
|
163
|
-
// `{"tool":...}` as an example stays prose.
|
|
164
|
-
const next = messages[i + 1];
|
|
165
|
-
const hasResult = next?.role === "user" && TOOL_RESULT_RE.test(next.content);
|
|
166
|
-
const lastBrace = m.content.lastIndexOf("}");
|
|
167
|
-
const trailing = lastBrace >= 0 ? m.content.slice(lastBrace + 1).trim() : "";
|
|
168
|
-
// A reply that STARTS with `{` is never illustrative prose — it is a (possibly
|
|
169
|
-
// malformed) JSON emission whose raw body must stay out of scrollback.
|
|
170
|
-
const looksIllustrative = !startsWithJson && !hasResult && trailing.length > 12 && /[A-Za-z]/.test(trailing);
|
|
171
|
-
|
|
172
|
-
if (toolCalls.length > 0 && !looksIllustrative) {
|
|
133
|
+
if (toolCalls.length > 0) {
|
|
173
134
|
if (lines.length > 0 && lines[lines.length - 1] !== "") {
|
|
174
135
|
lines.push("");
|
|
175
136
|
}
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
// in call order.
|
|
137
|
+
// The matching `Tool [x] result (ok|fail)` user message follows; for a batch it
|
|
138
|
+
// is ONE message with several blocks. Parse verdicts in call order.
|
|
139
|
+
const next = messages[i + 1];
|
|
180
140
|
const verdicts = next?.role === "user" ? parseToolVerdicts(next.content) : [];
|
|
181
141
|
toolCalls.forEach((c, ci) => {
|
|
182
142
|
const v = verdicts[ci] ?? verdicts.find(x => x.tool === c.tool);
|
|
@@ -187,22 +147,14 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
|
|
|
187
147
|
});
|
|
188
148
|
continue;
|
|
189
149
|
}
|
|
190
|
-
// No
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
? [narration, doneReason].filter(s => s.trim()).join("\n\n")
|
|
199
|
-
: isJsonEmission && !looksIllustrative
|
|
200
|
-
? narration // JSON/tool-call emission we couldn't render — keep narration, drop raw JSON
|
|
201
|
-
: m.content;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
150
|
+
// No tool calls → a lone `{tool:"done", reason}` (show its reason), a JSON object
|
|
151
|
+
// that isn't a renderable call (skip — never dump raw JSON), or genuine prose.
|
|
152
|
+
const reason =
|
|
153
|
+
parsed && parsed.tool === "done"
|
|
154
|
+
? String((parsed.arguments as { reason?: unknown } | undefined)?.reason ?? "")
|
|
155
|
+
: looksLikeCall
|
|
156
|
+
? ""
|
|
157
|
+
: m.content;
|
|
206
158
|
if (!reason.trim()) continue;
|
|
207
159
|
// Persisted thinking (gjc "think → answer" order): show the turn's reasoning,
|
|
208
160
|
// dimmed, above the reply so the durable record carries it across /resume + export.
|