From 15cd5eac5ea89ca9c1e2df32f6b64fa957a3e03b Mon Sep 17 00:00:00 2001
From: Matthew
Date: Sat, 16 May 2026 17:47:52 +0700
Subject: [PATCH] fix: patch generated web diagnostics
---
CHANGELOG.md | 8 ++
Cargo.lock | 2 +-
README.md | 10 +-
package.json | 2 +-
phonton-cli/Cargo.toml | 2 +-
phonton-cli/src/focus.rs | 31 ++++++-
phonton-cli/src/main.rs | 66 +++++++++++++
phonton-worker/src/lib.rs | 68 +++++++++++++-
.../src/templates/chessRules.test.ts | 93 +++++++++----------
release-notes/v0.14.1.md | 31 +++++++
10 files changed, 254 insertions(+), 59 deletions(-)
create mode 100644 release-notes/v0.14.1.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 636c6a1..870b8ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,14 @@ All notable Phonton CLI release changes should be documented here.
This project follows pre-1.0 SemVer: minor versions may still include breaking changes while the public API and CLI surface settle.
+## 0.14.1 - Generated Web Failure Diagnostics
+
+### Fixed
+
+- Problems focus now shows the changed-file excerpt that matches the verifier diagnostic path. A failure such as `[typescript syntax] src/App.tsx:1:1 invalid syntax` now jumps to the `src/App.tsx` diff instead of showing the first unrelated changed file.
+- Generated web syntax diagnostics that include line/column suffixes such as `src/App.tsx:1:1` are normalized back to artifact paths before retry policy runs. This keeps generic repair contexts on the generated-web fast-fail path instead of spending another broad provider repair.
+- The existing Vite/React chess rules test seed no longer imports `vitest`. The seeded test file uses self-executing assertions, so it does not require Phonton to repair `package.json` before local rules verification can pass.
+
## 0.14.0 - Non-Interactive Node Verification
### Fixed
diff --git a/Cargo.lock b/Cargo.lock
index 2fa42b2..bd7e7d4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2274,7 +2274,7 @@ dependencies = [
[[package]]
name = "phonton-cli"
-version = "0.14.0"
+version = "0.14.1"
dependencies = [
"anyhow",
"async-trait",
diff --git a/README.md b/README.md
index 2fc8a09..31f68ad 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-Phonton CLI · v0.13.5
+Phonton CLI · v0.14.1
Verified code changes with repo memory.
@@ -12,7 +12,7 @@
-
+
@@ -75,6 +75,8 @@ It walks through the evidence trail a real run should expose: GoalContract, plan
- `/why-tokens` and `phonton why-tokens --by-source` explain the latest prompt manifest in plain language, including first-attempt, repair-attempt, context/artifact, system, goal, memory, attachment, repo-code, MCP/tool, retry, compaction, dedupe, and cached-token buckets.
- v0.11 context planning builds a compact repo map, selects only the highest-value code slices under a target budget, exposes omitted code tokens, and labels target-exceeded prompts honestly when one required slice must go over budget.
- v0.12 enforces lower spend before the provider call: generated app/game goals dispatch as acceptance-slice subtasks, simple/docs/test prompts use small task-class budgets, generated repairs use a sub-1k context target, semantic retrieval top-k and repo maps shrink by task class, MCP result context is capped, and provider output ceilings are lower.
+- v0.14.1 fixes generated-web failure diagnostics: Problems focus now jumps to the changed file named by the verifier, `src/App.tsx:1:1` diagnostics are normalized for retry policy, and the local chess rules test seed no longer requires a Vitest import.
+- v0.14.0 hardens Node verification so stock Vite/Vitest/Jest test scripts run in non-interactive CI mode instead of hanging in watch mode.
- v0.13.5 seeds the existing Vite/React chess rules/test boundary with a locally verified template before provider UI slices, including recovery from partial invalid `src/chessRules.ts` artifacts without another provider call.
- v0.13.4 detects existing Vite/React workspaces for chess benchmark prompts that say "use the existing project stack," then starts on source/test slices instead of fragile `package.json` or `index.html` scaffold edits.
- v0.13.1 hardens generated web-app token behavior: Vite/React chess prompts stay on compact acceptance slices even in partial workspaces, and first-attempt TSX/HTML syntax failures stop before automatic repair.
@@ -153,7 +155,7 @@ Windows PowerShell:
Direct Cargo install:
```bash
-cargo install --git https://github.com/phonton-dev/phonton-cli --tag v0.13.5 phonton-cli --locked --force
+cargo install --git https://github.com/phonton-dev/phonton-cli --tag v0.14.1 phonton-cli --locked --force
```
Check the install:
@@ -169,7 +171,7 @@ Phonton uses GitHub branches and releases as install channels:
| Channel | Install | Use when |
|---|---|---|
-| Stable | `cargo install --git https://github.com/phonton-dev/phonton-cli --tag v0.13.5 phonton-cli --locked --force` | You want the best validated public alpha |
+| Stable | `cargo install --git https://github.com/phonton-dev/phonton-cli --tag v0.14.1 phonton-cli --locked --force` | You want the best validated public alpha |
| Dev | `cargo install --git https://github.com/phonton-dev/phonton-cli --branch dev phonton-cli --locked --force` | You want next-release integration changes |
| Nightly | `cargo install --git https://github.com/phonton-dev/phonton-cli --branch nightly phonton-cli --locked --force` | You want daily snapshots and can tolerate breakage |
| Main | `cargo install --git https://github.com/phonton-dev/phonton-cli --branch main phonton-cli --locked --force` | You want the current release branch tip |
diff --git a/package.json b/package.json
index cc180f8..8a7c1d6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "phonton-cli",
- "version": "0.14.0",
+ "version": "0.14.1",
"description": "Local-first agentic development terminal with context packs, source handles, and verification gates.",
"license": "MIT OR Apache-2.0",
"homepage": "https://github.com/phonton-dev/phonton-cli#readme",
diff --git a/phonton-cli/Cargo.toml b/phonton-cli/Cargo.toml
index 4af320f..37a07d2 100644
--- a/phonton-cli/Cargo.toml
+++ b/phonton-cli/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "phonton-cli"
-version = "0.14.0"
+version = "0.14.1"
edition.workspace = true
license.workspace = true
repository.workspace = true
diff --git a/phonton-cli/src/focus.rs b/phonton-cli/src/focus.rs
index 3715f7d..44b107e 100644
--- a/phonton-cli/src/focus.rs
+++ b/phonton-cli/src/focus.rs
@@ -275,10 +275,11 @@ pub(crate) fn problems_focus_text(goal: &GoalEntry, selected_file: usize) -> Str
out.push_str("\nRepair\n- Press r or run /retry to queue a repair with compact diagnostics.\n- Use /why-tokens to inspect retry/context token buckets.\n");
let groups = diff_hunks_by_file(goal);
- if let Some((path, hunks)) = groups.get(selected_file.min(groups.len().saturating_sub(1))) {
+ let selected_group = problem_excerpt_index(&diagnostics, &groups, selected_file);
+ if let Some((path, hunks)) = groups.get(selected_group) {
out.push_str(&format!(
"\nChanged excerpt {}/{}\nfile: {}\n",
- selected_file.min(groups.len().saturating_sub(1)) + 1,
+ selected_group + 1,
groups.len(),
path.display()
));
@@ -309,6 +310,32 @@ pub(crate) fn problems_focus_text(goal: &GoalEntry, selected_file: usize) -> Str
out
}
+fn problem_excerpt_index(
+ diagnostics: &[String],
+ groups: &[(PathBuf, Vec)],
+ selected_file: usize,
+) -> usize {
+ if groups.is_empty() {
+ return 0;
+ }
+ let diagnostic_text = diagnostics
+ .iter()
+ .map(|item| item.replace('\\', "/").to_ascii_lowercase())
+ .collect::>()
+ .join("\n");
+ if let Some((idx, _)) = groups.iter().enumerate().find(|(_, (path, _))| {
+ diagnostic_text.contains(
+ &path
+ .to_string_lossy()
+ .replace('\\', "/")
+ .to_ascii_lowercase(),
+ )
+ }) {
+ return idx;
+ }
+ selected_file.min(groups.len().saturating_sub(1))
+}
+
pub(crate) fn problem_diagnostics(goal: &GoalEntry) -> Vec {
let mut items = Vec::new();
for record in &goal.flight_log {
diff --git a/phonton-cli/src/main.rs b/phonton-cli/src/main.rs
index 34410f4..15c64ff 100644
--- a/phonton-cli/src/main.rs
+++ b/phonton-cli/src/main.rs
@@ -9575,6 +9575,72 @@ fn extract_id(line: &str) -> Option {
assert!(app.focus_text().contains("Kimi was used"));
}
+ #[test]
+ fn problems_focus_prioritizes_changed_excerpt_for_diagnostic_file() {
+ let mut app = App::default();
+ app.goals.push(GoalEntry::new("make chess".into()));
+ app.apply_event(
+ 0,
+ EventRecord {
+ task_id: TaskId::new(),
+ timestamp_ms: 1,
+ event: OrchestratorEvent::SubtaskReviewReady {
+ subtask_id: SubtaskId::new(),
+ description: "generated chess slice".into(),
+ tier: ModelTier::Standard,
+ tokens_used: 10,
+ token_usage: TokenUsage::estimated(10),
+ cost: phonton_types::CostSummary::default(),
+ diff_hunks: vec![
+ DiffHunk {
+ file_path: PathBuf::from("src/chessRules.ts"),
+ old_start: 0,
+ old_count: 0,
+ new_start: 1,
+ new_count: 1,
+ lines: vec![DiffLine::Added("export const rules = true".into())],
+ },
+ DiffHunk {
+ file_path: PathBuf::from("src/App.tsx"),
+ old_start: 0,
+ old_count: 0,
+ new_start: 1,
+ new_count: 1,
+ lines: vec![DiffLine::Added("```tsx".into())],
+ },
+ ],
+ verify_result: VerifyResult::Fail {
+ layer: VerifyLayer::Syntax,
+ errors: vec!["[typescript syntax] src/App.tsx:1:1 invalid syntax".into()],
+ attempt: 1,
+ },
+ provider: ProviderKind::OpenAiCompatible,
+ model_name: "fixture".into(),
+ },
+ },
+ );
+ app.apply_event(
+ 0,
+ EventRecord {
+ task_id: TaskId::new(),
+ timestamp_ms: 2,
+ event: OrchestratorEvent::VerifyFail {
+ subtask_id: SubtaskId::new(),
+ layer: VerifyLayer::Syntax,
+ errors: vec!["[typescript syntax] src/App.tsx:1:1 invalid syntax".into()],
+ attempt: 1,
+ },
+ },
+ );
+ app.apply_state(0, failed_state("syntax verification failed"));
+
+ let text = app.focus_text();
+
+ assert!(text.contains("file: src/App.tsx"), "{text}");
+ assert!(!text.contains("file: src/chessRules.ts"), "{text}");
+ assert!(text.contains("+```tsx"), "{text}");
+ }
+
#[test]
fn problems_shortcuts_open_and_retry_failed_goal() {
let mut app = App::default();
diff --git a/phonton-worker/src/lib.rs b/phonton-worker/src/lib.rs
index db65f06..ae9d656 100644
--- a/phonton-worker/src/lib.rs
+++ b/phonton-worker/src/lib.rs
@@ -1104,7 +1104,8 @@ fn should_stop_before_generated_app_syntax_repair(
if !matches!(
classify_intent(&subtask.description).task_class,
TaskClass::GeneratedAppGame
- ) {
+ ) && !diagnostics_name_generated_web_artifact(&subtask.description, errors)
+ {
return false;
}
let diagnostics = errors.join("\n").to_ascii_lowercase();
@@ -1116,6 +1117,28 @@ fn should_stop_before_generated_app_syntax_repair(
|| diagnostics.contains("src/app")
}
+fn diagnostics_name_generated_web_artifact(description: &str, errors: &[String]) -> bool {
+ let mut paths = artifact_paths_from_subtask(description);
+ for path in artifact_paths_from_errors(errors) {
+ if !paths.iter().any(|existing| existing == &path) {
+ paths.push(path);
+ }
+ }
+ paths.iter().any(|path| {
+ let normalized = path
+ .to_string_lossy()
+ .replace('\\', "/")
+ .to_ascii_lowercase();
+ normalized == "index.html"
+ || normalized == "src/app.tsx"
+ || normalized == "src/app.jsx"
+ || normalized == "src/main.tsx"
+ || normalized == "src/main.jsx"
+ || normalized.ends_with(".tsx")
+ || normalized.ends_with(".jsx")
+ })
+}
+
fn repair_guidance_for_errors(errors: &[String]) -> Vec {
if !looks_like_stale_hunk_error(errors) {
return Vec::new();
@@ -1336,6 +1359,7 @@ fn artifact_paths_from_errors(errors: &[String]) -> Vec {
.trim_end_matches(':')
.trim_end_matches(',')
.trim_end_matches(';');
+ let cleaned = strip_diagnostic_location_suffix(cleaned);
if !looks_like_relative_artifact_path(cleaned) {
continue;
}
@@ -1348,6 +1372,16 @@ fn artifact_paths_from_errors(errors: &[String]) -> Vec {
paths
}
+fn strip_diagnostic_location_suffix(mut value: &str) -> &str {
+ while let Some((head, tail)) = value.rsplit_once(':') {
+ if head.is_empty() || tail.is_empty() || !tail.chars().all(|ch| ch.is_ascii_digit()) {
+ break;
+ }
+ value = head;
+ }
+ value
+}
+
fn looks_like_relative_artifact_path(value: &str) -> bool {
if value.is_empty() || value.contains("://") {
return false;
@@ -2772,6 +2806,20 @@ mod tests {
let _ = std::fs::remove_dir_all(base);
}
+ #[test]
+ fn local_chess_rules_test_seed_does_not_require_test_runner_dependency() {
+ let template = include_str!("templates/chessRules.test.ts");
+
+ assert!(
+ !template.contains("from 'vitest'") && !template.contains("from \"vitest\""),
+ "existing Vite seed must not require adding Vitest before package.json is repaired"
+ );
+ assert!(
+ template.contains("runRulesSeedTests()"),
+ "template should still self-execute assertions when a runner discovers the file"
+ );
+ }
+
#[derive(Clone)]
struct BrokenTsxProvider {
calls: Arc>,
@@ -2855,6 +2903,24 @@ mod tests {
let _ = std::fs::remove_dir_all(base);
}
+ #[test]
+ fn generated_web_syntax_fast_fail_uses_artifact_diagnostics_when_description_is_generic() {
+ let task = subtask("Repair verifier failure from previous attempt");
+ let errors =
+ vec!["Verifier Syntax: [typescript syntax] src/App.tsx:1:1 invalid syntax".into()];
+
+ assert!(
+ should_stop_before_generated_app_syntax_repair(
+ &task,
+ VerifyLayer::Syntax,
+ &errors,
+ 1,
+ false
+ ),
+ "web artifact syntax diagnostics should stop before another broad provider repair"
+ );
+ }
+
#[test]
fn repeated_diagnostic_signature_ignores_attempt_noise() {
let first = diagnostic_signature(
diff --git a/phonton-worker/src/templates/chessRules.test.ts b/phonton-worker/src/templates/chessRules.test.ts
index 033f4c0..c407188 100644
--- a/phonton-worker/src/templates/chessRules.test.ts
+++ b/phonton-worker/src/templates/chessRules.test.ts
@@ -1,4 +1,3 @@
-import { describe, expect, it } from 'vitest'
import {
createGame,
createInitialGame,
@@ -9,62 +8,58 @@ import {
movePiece,
} from './chessRules'
-describe('chess rules boundary', () => {
- it('creates the standard starting position', () => {
- const game = createInitialGame()
- const pieceCount = Object.values(game.board).filter(Boolean).length
+function assert(condition: unknown, message: string): asserts condition {
+ if (!condition) {
+ throw new Error(message)
+ }
+}
- expect(pieceCount).toBe(32)
- expect(game.turn).toBe('white')
- expect(game.board.e1?.kind).toBe('king')
- expect(game.board.e8?.kind).toBe('king')
- })
+function assertIncludes(values: string[], expected: string, message: string) {
+ assert(values.includes(expected), message)
+}
- it('allows normal pawn and knight moves and enforces turn order', () => {
- const game = createInitialGame()
+function assertExcludes(values: string[], expected: string, message: string) {
+ assert(!values.includes(expected), message)
+}
- expect(legalMovesFor(game, 'e2')).toContain('e4')
- expect(legalMovesFor(game, 'g1')).toContain('f3')
+export function runRulesSeedTests() {
+ const initial = createInitialGame()
+ const pieceCount = Object.values(initial.board).filter(Boolean).length
- const moved = movePiece(game, 'e2', 'e4')
- expect(moved.ok).toBe(true)
- if (moved.ok) {
- expect(moved.state.turn).toBe('black')
- expect(movePiece(moved.state, 'e4', 'e5').ok).toBe(false)
- }
- })
+ assert(pieceCount === 32, 'starting position has 32 pieces')
+ assert(initial.turn === 'white', 'white moves first')
+ assert(initial.board.e1?.kind === 'king', 'white king starts on e1')
+ assert(initial.board.e8?.kind === 'king', 'black king starts on e8')
- it('rejects blocked and illegal moves', () => {
- const game = createInitialGame()
+ assertIncludes(legalMovesFor(initial, 'e2'), 'e4', 'white pawn can move two squares')
+ assertIncludes(legalMovesFor(initial, 'g1'), 'f3', 'white knight can move from g1 to f3')
- expect(legalMovesFor(game, 'a1')).not.toContain('a4')
- expect(movePiece(game, 'e2', 'e5').ok).toBe(false)
- })
+ const moved = movePiece(initial, 'e2', 'e4')
+ assert(moved.ok, 'legal pawn move succeeds')
+ assert(moved.state.turn === 'black', 'turn switches after legal move')
+ assert(!movePiece(moved.state, 'e4', 'e5').ok, 'turn order is enforced')
- it('detects check and filters king moves into attacked squares', () => {
- const board = emptyBoard()
- board.e1 = makePiece('white', 'king')
- board.a8 = makePiece('black', 'king')
- board.e8 = makePiece('black', 'rook')
- const game = createGame(board, 'white')
+ assertExcludes(legalMovesFor(initial, 'a1'), 'a4', 'rook cannot jump blocked pawns')
+ assert(!movePiece(initial, 'e2', 'e5').ok, 'illegal pawn move is rejected')
- expect(isInCheck(game.board, 'white')).toBe(true)
- expect(legalMovesFor(game, 'e1')).not.toContain('e2')
- })
+ const checkBoard = emptyBoard()
+ checkBoard.e1 = makePiece('white', 'king')
+ checkBoard.a8 = makePiece('black', 'king')
+ checkBoard.e8 = makePiece('black', 'rook')
+ const checkGame = createGame(checkBoard, 'white')
+ assert(isInCheck(checkGame.board, 'white'), 'rook gives check on open file')
+ assertExcludes(legalMovesFor(checkGame, 'e1'), 'e2', 'king cannot move into check')
- it('promotes pawns to queens', () => {
- const board = emptyBoard()
- board.e1 = makePiece('white', 'king')
- board.e8 = makePiece('black', 'king')
- board.a7 = makePiece('white', 'pawn')
- const game = createGame(board, 'white')
+ const promotionBoard = emptyBoard()
+ promotionBoard.e1 = makePiece('white', 'king')
+ promotionBoard.e8 = makePiece('black', 'king')
+ promotionBoard.a7 = makePiece('white', 'pawn')
+ const promotionGame = createGame(promotionBoard, 'white')
+ const promotion = movePiece(promotionGame, 'a7', 'a8')
- const result = movePiece(game, 'a7', 'a8')
+ assert(promotion.ok, 'promotion move succeeds')
+ assert(promotion.state.board.a8?.kind === 'queen', 'pawn promotes to queen')
+ assert(promotion.move.promotion === 'queen', 'promotion is recorded')
+}
- expect(result.ok).toBe(true)
- if (result.ok) {
- expect(result.state.board.a8?.kind).toBe('queen')
- expect(result.move.promotion).toBe('queen')
- }
- })
-})
+runRulesSeedTests()
diff --git a/release-notes/v0.14.1.md b/release-notes/v0.14.1.md
new file mode 100644
index 0000000..50d578e
--- /dev/null
+++ b/release-notes/v0.14.1.md
@@ -0,0 +1,31 @@
+# Phonton CLI v0.14.1
+
+v0.14.1 is a patch release for generated-web failure diagnostics and the
+existing Vite/React chess seed path.
+
+## Fixed
+
+- Problems focus now selects the changed-file excerpt named by verifier
+ diagnostics. If syntax verification fails on `src/App.tsx`, the TUI shows
+ the `src/App.tsx` excerpt instead of the first changed file.
+- Worker retry policy now strips diagnostic line/column suffixes such as
+ `src/App.tsx:1:1` before extracting artifact paths. Generic repair contexts
+ still recognize generated web artifact failures and stop before another
+ broad provider repair.
+- The local chess rules test seed no longer imports `vitest`. It uses
+ self-executing assertions so existing Vite/React workspaces do not need a
+ package repair before seeded rule verification can run.
+
+## Verification
+
+- `cargo test -p phonton-worker local_chess_rules_test_seed_does_not_require_test_runner_dependency`
+- `cargo test -p phonton-worker generated_web_syntax_fast_fail_uses_artifact_diagnostics_when_description_is_generic`
+- `cargo test -p phonton-cli problems_focus_prioritizes_changed_excerpt_for_diagnostic_file`
+- `cargo fmt --all -- --check`
+- `cargo test --locked --workspace`
+- `cargo clippy --locked --workspace --all-targets -- -D warnings`
+- `cargo build --release --locked -p phonton-cli`
+- `target/release/phonton.exe version`
+- `target/release/phonton.exe --help`
+- `npm run test:npm-wrapper`
+- `npm pack --dry-run`