Skip to content

fix(state-watcher): repair the five parser/protocol defects that left the UI empty - #11

Merged
JustAGhosT merged 3 commits into
mainfrom
claude/silly-sutherland-cdedd2
Aug 11, 2026
Merged

fix(state-watcher): repair the five parser/protocol defects that left the UI empty#11
JustAGhosT merged 3 commits into
mainfrom
claude/silly-sutherland-cdedd2

Conversation

@JustAGhosT

@JustAGhosT JustAGhosT commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

The state-watcher daemon reported a fully configured workspace as unconfigured. Found by running the daemon against a real Retort workspace (~/repos/retort) and reading what a UI client actually receives — every claim below is measured, not inferred.

Before / after

Same workspace, same probe (connect, send {type:'ready'}, wait 4 s):

before after
frames after ready 0 snapshot + health
teams 14, focus wrong on all 14 14, correct
backlog items 3 (from the priority legend) 24 (real rows)
tasks 0 of 6 6 of 6
/api/ask [] for every query data @ 0.4
UI onboarding screen, indefinitely panels populated

The five defects

D1 — no initial snapshot. useBridge sends {type:'ready'} on open and waits for a snapshot; the connection handler only branched on command:run, so nothing answered. Panels stayed empty until an unrelated watched file changed. createServer now takes a hooks object with initialFrames(), and ready is answered per-client.

D2 — task files rejected. isTaskShape() required createdAt/updatedAt strings the framework does not write, and parseTasks did not recurse — all of retort's tasks live under tasks/archive/, where /orchestrate leaves them. Fleet, Handoff and Mesh panels were therefore permanently empty.

I made the framework canonical and the daemon normalise: it writes these files, the daemon only reads them, so a format change isn't the daemon's call. assignees: ["team-backend"]assignedTo: "backend" (stripping team- so a task's team matches its teams.yaml id), and timestamps are derived from the messages[] span, falling back to file mtime — derived rather than left absent because HandoffFeedPanel calls updatedAt.localeCompare and CognitiveMeshPanel formats it as a date. An unrecognised status falls back to submitted so a task can never silently vanish again.

D3 — teams YAML never matched, and failed silently into a lossy fallback. extractScalar(block,'id') used ^\s+id:, which cannot match - id: backend — the - intervenes. Every valid spec parsed to zero teams and control fell through to the markdown table, which took column 2 (the ID column) as focus.

Replaced the regexes with a small line-based reader. Two further causes were in play beyond the dash, and fixing only the dash would have left both: teams.yaml continues with sibling top-level keys (intake:, techStacks: — itself a sequence of maps), so reading must stop at column 0; and scope: is sometimes a bracketed list the formatter wrapped across lines, which would have left docs, forge and strategic-ops with empty scope. Also fixes handoff-chain vs handoffChain. The markdown fallback now addresses columns by header name, and a spec file that parses to zero teams warns on stderr instead of degrading quietly.

D4 — router returned []. No router change; it was fed degraded teams. router-index.test.ts asserts both directions — the real fixture routes correctly, and the same queries against deliberately degraded teams return [], pinning D3 as the cause.

D5 — wrong backlog table. The regex expected the priority in column 2, but the file is Priority | Team | Task | Phase | Status | Notes. It skipped all 20 sprint/backlog rows and matched the Priority Definitions legend, producing three items titled P1/P2/P3 with teams "Within sprint"/"Next sprint"/"Best effort". Columns are now addressed by header name and tables without a task column are skipped, which excludes both legends structurally. The Completed table has no Status column, so those rows take done from the section heading.

Also fixed

  • file:open was silently dropped. Now relayed as OPEN:<json> and wired through StateWatcherProcess.onOpenFile to showTextDocument.
  • Task deletions never reached the UI — the per-task task:updated form cannot express a removal, so deleting the last task emitted nothing. Task changes now broadcast a full snapshot. Verified by deleting all six files one at a time, ending snapshot(tasks=0).
  • The first cogmesh health result resolved before any client could connect, then waited 30 s. It is now cached and replayed on handshake.

Tests

84 new tests in packages/state-watcher (which had none), built on verbatim copies of ~/repos/retort under test/fixtures/retort/. Hand-written fixtures would have been shaped to match the parsers and would have passed while the daemon stayed broken; the refresh command is documented in test-fixtures.ts. Added a Test state-watcher CI step after the router build, since the integration test resolves @retort-plugins/router from its dist.

Router's 27 tests still pass. Workspace builds, typechecks and lints clean.

Deliberately not changed

  • normalisePriority maps P1 → medium, but retort's own legend labels P1 "High". Four P-levels into a three-value union loses something either way, and it wasn't among the reported defects — followup filed.
  • task:updated is now unused by the daemon but remains in the protocol and useStore handles it, so incremental pushes stay available — followup filed.
  • packages/ui is untouched. All seven panels render correctly once fed protocol-shaped data.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Dashboard file requests can open workspace-relative or absolute paths directly in VS Code, with warnings for invalid or failed opens.
    • Health status is restored for newly connected dashboard clients.
    • Task changes refresh the complete task snapshot, including removals.
    • Improved Markdown, YAML, task, team, and backlog parsing across supported formats.
  • Bug Fixes

    • Added safer handling for fragmented messages and missing, malformed, or incomplete project files.
  • Tests

    • Expanded coverage for parsing, routing, state updates, and VS Code integration.

… the UI empty

Measured by running the daemon against a real Retort workspace
(~/repos/retort), not inferred. Before: the dashboard showed the
"No Retort configuration detected" onboarding screen indefinitely
against a fully configured workspace.

- server: answer the UI's `ready` handshake with a snapshot. useBridge
  sends `{type:'ready'}` on open and waits for one; nothing sent it, so
  panels stayed empty until an unrelated watched file changed.
  createServer now takes a hooks object with initialFrames().
- tasks: parse the format the framework actually writes. Recurse into
  tasks/archive/ (where a finished run leaves everything), require only
  id+title, map `assignees:["team-backend"]` to `assignedTo:"backend"`,
  and derive createdAt/updatedAt from the message log or file mtime —
  the UI sorts and formats on them. 0/6 files parsed before, 6/6 now.
  The framework owns this format; the daemon normalises.
- teams: replace the YAML regexes with a line-based reader. `^\s+id:`
  could not match `  - id: backend`, so every valid spec parsed to zero
  teams and fell through to the markdown table, which read the ID
  column as focus. Also fixes `handoff-chain` vs handoffChain, flow
  sequences the formatter wraps across lines, and reading past `teams:`
  into the sibling `techStacks:` section. A spec that parses to nothing
  now warns instead of degrading silently.
- router index: no router change needed — it was fed degraded teams.
  /api/ask now answers "which team handles database migrations" with
  data (0.4) instead of [].
- backlog: address table columns by header name. The regex expected the
  priority in column 2, so it skipped all 20 sprint/backlog rows and
  matched the Priority Definitions legend instead. 24 items now, and
  tables without a task column are skipped.

Also: handle the `file:open` message the mesh panel sends, relayed as
OPEN:<json> and wired to showTextDocument; broadcast a full snapshot on
task changes so deletions reach the UI (the per-task form could not
express one, so removing the last task emitted nothing); and replay the
first cogmesh health result on handshake, since it resolves before any
client can connect and the next probe is 30 s away.

Adds 84 tests built on verbatim fixtures copied from ~/repos/retort —
hand-written ones would have been shaped to match the parsers and would
have passed while the daemon stayed broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The state watcher now supports dashboard file opening, retained health-state replay, complete task snapshots, structured Markdown and YAML parsing, normalized task data, fixture-based routing tests, and separate state-watcher and VS Code unit-test workflow steps.

Changes

State watcher protocol and VS Code integration

Layer / File(s) Summary
File-open protocol and server hooks
extensions/vscode/src/extension.ts, extensions/vscode/src/services/*, packages/state-watcher/src/server.ts, packages/state-watcher/src/index.ts
The server forwards validated file-open messages. The process buffers and validates records, then emits onOpenFile. The extension restricts paths to the workspace and opens valid files in preview mode.
State synchronization and test execution
packages/state-watcher/package.json, packages/state-watcher/src/index.ts, extensions/vscode/package.json, extensions/vscode/vitest.config.ts, extensions/vscode/.vscodeignore, .github/workflows/vscode.yml
The entry point replays health state and broadcasts full task snapshots. Vitest scripts, packaging rules, and separate unit-test workflow steps were added.

Parser and workspace-state updates

Layer / File(s) Summary
Markdown, YAML, team, and backlog parsing
packages/state-watcher/src/parsers/markdown-table.ts, packages/state-watcher/src/parsers/yaml-lite.ts, packages/state-watcher/src/parsers/teams.ts, packages/state-watcher/src/parsers/backlog.ts, packages/state-watcher/src/parsers/*test.ts, packages/state-watcher/test/fixtures/retort/AGENT_*, packages/state-watcher/test/fixtures/retort/.agentkit/spec/teams.yaml
Shared parsers now read named Markdown columns and YAML sequences. Team and backlog parsers normalize fields, support aliases and fallback sources, and generate unique identifiers.
Task and session normalization
packages/state-watcher/src/parsers/tasks.ts, packages/state-watcher/src/parsers/session.test.ts, packages/state-watcher/src/parsers/tasks.test.ts, packages/state-watcher/test/fixtures/retort/.claude/...
Task discovery supports nested directories and stable ordering. Task records normalize partial input, statuses, teams, assignees, metadata, and timestamps. Session and task fixtures cover valid and invalid input.
Routing validation
packages/state-watcher/src/router-index.test.ts, packages/state-watcher/src/parsers/test-fixtures.ts
Router tests use the discovered Retort fixture root and validate team indexing, routing, explanations, keyword construction, and degraded-team behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary parser and protocol fixes that resolve empty UI panels.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/silly-sutherland-cdedd2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@extensions/vscode/src/services/stateWatcherProcess.ts`:
- Around line 51-60: Update the stdout handling in the state watcher process to
retain a carry buffer across chunks, append incoming data, and parse only
complete newline-terminated OPEN: records so fragmented JSON is not dropped;
preserve any incomplete trailing record for the next chunk and process multiple
coalesced records. Add tests covering fragmented and coalesced stdout output.

In `@packages/state-watcher/src/parsers/tasks.ts`:
- Around line 175-179: Validate explicit createdAt and updatedAt values in the
task parser before returning them: replace the current str-based acceptance with
a timestamp-validating helper or equivalent that rejects values Date.parse
cannot parse, while preserving valid strings and allowing the existing
message-log and mtime fallbacks when either explicit timestamp is invalid.

In `@packages/state-watcher/src/server.ts`:
- Around line 115-116: Validate file:open msg.path in
packages/state-watcher/src/server.ts lines 115-116 as a non-empty string before
calling hooks.onOpenFile. In extensions/vscode/src/extension.ts lines 34-36,
resolve the path beneath workspaceRoot and reject absolute paths,
parent-directory traversal, and symlink targets outside the workspace before
opening the document; keep path operations inside the try block or validate the
type before path.isAbsolute().

In `@packages/state-watcher/test/fixtures/retort/AGENT_BACKLOG.md`:
- Around line 133-135: Update the Retort generator that emits the
AGENT_BACKLOG.md code fence so it includes a language identifier such as text,
ensuring regeneration produces a Markdownlint-compliant fence; do not edit the
generated fixture directly.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d56712d0-9866-4eec-9d8f-fca634b8cedb

📥 Commits

Reviewing files that changed from the base of the PR and between d35d89c and bab0707.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (29)
  • .github/workflows/vscode.yml
  • extensions/vscode/src/extension.ts
  • extensions/vscode/src/services/stateWatcherProcess.ts
  • packages/state-watcher/package.json
  • packages/state-watcher/src/index.ts
  • packages/state-watcher/src/parsers/backlog.test.ts
  • packages/state-watcher/src/parsers/backlog.ts
  • packages/state-watcher/src/parsers/markdown-table.test.ts
  • packages/state-watcher/src/parsers/markdown-table.ts
  • packages/state-watcher/src/parsers/session.test.ts
  • packages/state-watcher/src/parsers/tasks.test.ts
  • packages/state-watcher/src/parsers/tasks.ts
  • packages/state-watcher/src/parsers/teams.test.ts
  • packages/state-watcher/src/parsers/teams.ts
  • packages/state-watcher/src/parsers/test-fixtures.ts
  • packages/state-watcher/src/parsers/yaml-lite.test.ts
  • packages/state-watcher/src/parsers/yaml-lite.ts
  • packages/state-watcher/src/router-index.test.ts
  • packages/state-watcher/src/server.ts
  • packages/state-watcher/test/fixtures/retort/.agentkit/spec/teams.yaml
  • packages/state-watcher/test/fixtures/retort/.claude/state/orchestrator.json
  • packages/state-watcher/test/fixtures/retort/.claude/state/tasks/archive/task-p0-ci-pipeline.json
  • packages/state-watcher/test/fixtures/retort/.claude/state/tasks/archive/task-p1-api-routes.json
  • packages/state-watcher/test/fixtures/retort/.claude/state/tasks/archive/task-p1-db-schema.json
  • packages/state-watcher/test/fixtures/retort/.claude/state/tasks/archive/task-p1-health-check.json
  • packages/state-watcher/test/fixtures/retort/.claude/state/tasks/archive/task-p1-migration-tooling.json
  • packages/state-watcher/test/fixtures/retort/.claude/state/tasks/archive/task-p1-staging-env.json
  • packages/state-watcher/test/fixtures/retort/AGENT_BACKLOG.md
  • packages/state-watcher/test/fixtures/retort/AGENT_TEAMS.md

Comment thread extensions/vscode/src/services/stateWatcherProcess.ts Outdated
Comment thread packages/state-watcher/src/parsers/tasks.ts Outdated
Comment thread packages/state-watcher/src/server.ts Outdated
Comment on lines +133 to +135
```
Discovery -> Planning -> Implementation -> Validation -> Ship
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language identifier to this generated code fence.

Markdownlint reports MD040 for this fence. Update the Retort generator so regeneration emits a language-qualified fence, such as text. Direct fixture edits will be overwritten.

Proposed fix
-```
+```text
 Discovery -> Planning -> Implementation -> Validation -> Ship
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
Discovery -> Planning -> Implementation -> Validation -> Ship
```
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 133-133: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/state-watcher/test/fixtures/retort/AGENT_BACKLOG.md` around lines
133 - 135, Update the Retort generator that emits the AGENT_BACKLOG.md code
fence so it includes a language identifier such as text, ensuring regeneration
produces a Markdownlint-compliant fence; do not edit the generated fixture
directly.

Source: Linters/SAST tools

JustAGhosT and others added 2 commits August 11, 2026 17:55
…space

Addresses the two Major findings from CodeRabbit on #11, plus one Minor.
Both were in code this PR introduced.

- stdout was parsed per 'data' chunk, but chunk boundaries do not respect
  line boundaries: a record split across two events was dropped outright
  and never reassembled. That affects PORT: as much as the new OPEN: —
  a lost PORT: line means the webview never connects at all. Extracted a
  LineReader that carries the trailing partial record until its newline
  arrives, and flushes it on stream end.
- file:open carried an unvalidated path. ClientMessage is a compile-time
  type only, so JSON.parse guaranteed nothing: a non-string reached
  path.isAbsolute() outside the try block and threw, and a relative path
  could traverse out of the workspace. The daemon now checks the field
  before relaying, and the extension resolves it through
  resolveInWorkspace(), which accepts absolute paths inside the workspace
  (a task's resultPath may be one) and rejects anything that escapes.
  These requests originate in task JSON files on disk — data, not a
  trusted instruction — so they do not get to name an arbitrary path.
- deriveTimestamps() accepted any non-empty string as an explicit
  createdAt/updatedAt, so "not-a-date" bypassed the message-log and mtime
  fallbacks. That contradicted the reason for deriving them at all, since
  the UI sorts and formats on these. Only a Date-parseable value now
  counts as present, matching how message timestamps were already treated.

Both pure helpers were extracted so they could be tested: the extension
package had no runnable unit-test setup (its `test` script drives a real
VS Code instance and needs a display). Adds a `test:unit` vitest script
alongside it and 20 tests covering fragmented, coalesced, byte-at-a-time
and CRLF stdout plus the traversal cases, wired into CI. state-watcher
gains 2 timestamp tests, now 86.

Not fixed: MD040 on a code fence in test/fixtures/retort/AGENT_BACKLOG.md.
That file is a verbatim copy of Retort's generated output — editing it
would defeat the purpose of the fixture and be overwritten on refresh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI failed on d710791: `tsc -p ./` emits the new unit tests into out/ as
CommonJS, and vitest scans out/ — unlike dist/, which its default exclude
already covers, which is why the router and state-watcher packages never
hit this. Vitest then loaded the compiled copies and they cannot
require('vitest'), so 2 phantom test files failed alongside the 20 real
tests passing.

It passed locally only because I ran test:unit before ever building, so
out/ did not exist. Reproduced by running build then test:unit, the order
CI uses.

Restricts vitest to src/**/*.test.ts, and stops the compiled tests from
being packaged into the shipped extension while here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@extensions/vscode/src/services/lineReader.ts`:
- Around line 16-21: Update LineReader.push to use a persistent StringDecoder
for Buffer chunks, preserving UTF-8 characters split across chunk boundaries
while retaining normal string handling. In flush, append the decoder’s final
output before processing remaining records, and add coverage that splits the
UTF-8 bytes of “café.ts” across chunks and verifies the emitted path.

In `@extensions/vscode/src/services/stateWatcherProcess.ts`:
- Around line 51-56: Update the PORT: parsing logic in the state watcher to
parse the entire trimmed value with Number, accepting only safe integers in the
inclusive range 1–65535. Reject values with trailing non-numeric text such as
PORT:12junk, and only assign _port and fire _onPortReady after validation
succeeds.

In `@extensions/vscode/src/services/workspacePath.ts`:
- Around line 24-26: Update the containment check in resolveInWorkspace to
reject only relative === '..' or paths beginning with '..' followed by path.sep,
while continuing to reject absolute paths and the workspace root. Preserve valid
filenames such as '..settings.json', and add coverage for
resolveInWorkspace(ROOT, '..settings.json').
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 78dba683-1787-4681-b1ed-33c921e909cf

📥 Commits

Reviewing files that changed from the base of the PR and between bab0707 and bf2dc2d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • .github/workflows/vscode.yml
  • extensions/vscode/.vscodeignore
  • extensions/vscode/package.json
  • extensions/vscode/src/extension.ts
  • extensions/vscode/src/services/lineReader.test.ts
  • extensions/vscode/src/services/lineReader.ts
  • extensions/vscode/src/services/stateWatcherProcess.ts
  • extensions/vscode/src/services/workspacePath.test.ts
  • extensions/vscode/src/services/workspacePath.ts
  • extensions/vscode/vitest.config.ts
  • packages/state-watcher/src/parsers/tasks.test.ts
  • packages/state-watcher/src/parsers/tasks.ts
  • packages/state-watcher/src/server.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/state-watcher/src/parsers/tasks.test.ts
  • extensions/vscode/src/extension.ts
  • .github/workflows/vscode.yml
  • packages/state-watcher/src/parsers/tasks.ts
  • packages/state-watcher/src/server.ts

Comment on lines +16 to +21
push(chunk: string | Buffer): string[] {
this.carry += chunk.toString();
const records = this.carry.split('\n');
// The final element is whatever follows the last newline — possibly ''.
this.carry = records.pop() ?? '';
return records.map((record) => record.replace(/\r$/, ''));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 'StringDecoder|decoder\.(write|end)|chunk\.toString\(\)|café|Buffer\.from' \
  extensions/vscode/src/services/lineReader.ts \
  extensions/vscode/src/services/lineReader.test.ts

Repository: phoenixvc/retort-plugins

Length of output: 1429


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- lineReader.ts ---'
cat -n extensions/vscode/src/services/lineReader.ts
printf '%s\n' '--- lineReader.test.ts ---'
cat -n extensions/vscode/src/services/lineReader.test.ts
printf '%s\n' '--- LineReader usages ---'
rg -n -C 3 'new LineReader|\.push\(|\.flush\(' extensions/vscode/src

Repository: phoenixvc/retort-plugins

Length of output: 13063


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- split UTF-8 probe ---'
node - <<'JS'
const { StringDecoder } = require('node:string_decoder');

const encoded = Buffer.from('OPEN:{"path":"café.ts"}\n', 'utf8');
const marker = encoded.indexOf(Buffer.from('é', 'utf8')) + 1;
const first = encoded.subarray(0, marker);
const second = encoded.subarray(marker);

console.log('independent:', JSON.stringify(first.toString('utf8') + second.toString('utf8')));
const decoder = new StringDecoder('utf8');
console.log('decoder:', JSON.stringify(decoder.write(first) + decoder.write(second) + decoder.end()));
JS

printf '%s\n' '--- state watcher consumer ---'
cat -n extensions/vscode/src/services/stateWatcherProcess.ts
printf '%s\n' '--- open-file handling ---'
rg -n -C 5 'onOpenFile|resolveInWorkspace|OPEN:' extensions/vscode/src

Repository: phoenixvc/retort-plugins

Length of output: 19206


Preserve UTF-8 characters split across Buffer chunks.

Buffer.toString() decodes each chunk independently. Splitting é across chunks changes café.ts to caf��.ts, so OPEN: emits the wrong path. Use one StringDecoder for Buffer chunks and append decoder.end() in flush(). Add a test that splits the UTF-8 encoding of café.ts between bytes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extensions/vscode/src/services/lineReader.ts` around lines 16 - 21, Update
LineReader.push to use a persistent StringDecoder for Buffer chunks, preserving
UTF-8 characters split across chunk boundaries while retaining normal string
handling. In flush, append the decoder’s final output before processing
remaining records, and add coverage that splits the UTF-8 bytes of “café.ts”
across chunks and verifies the emitted path.

Comment on lines +51 to +56
if (line.startsWith('PORT:')) {
const port = parseInt(line.slice(5).trim(), 10);
if (Number.isInteger(port) && port > 0) {
this._port = port;
this._onPortReady.fire(port);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -t f --glob '*stateWatcherProcess*.test.ts' extensions/vscode
rg -n -C 3 'PORT:|parseInt|Number\.isSafeInteger|65535' extensions/vscode/src/services

Repository: phoenixvc/retort-plugins

Length of output: 5191


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n extensions/vscode/src/services/stateWatcherProcess.ts | sed -n '1,130p'
printf '\n--- PORT producers and consumers ---\n'
rg -n -C 3 "PORT:|onPortReady|_port|port" extensions/vscode/src extensions/vscode -g '*.ts' -g '*.js' -g '*.json' | head -n 240
printf '\n--- behavioral probe ---\n'
node - <<'JS'
for (const value of ['12junk', '70000', '65535', '65536', '1', '0', '-1', '1.5', '']) {
  const parsed = parseInt(value, 10);
  const strict = Number(value);
  console.log(JSON.stringify(value), {
    parseInt: parsed,
    parseIntIntegerPositive: Number.isInteger(parsed) && parsed > 0,
    number: strict,
    safePort: Number.isSafeInteger(strict) && strict >= 1 && strict <= 65535,
  });
}
JS

Repository: phoenixvc/retort-plugins

Length of output: 23727


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- state-watcher files ---'
fd -t f -i 'state-watcher|statewatcher|watcher' . | head -n 120

printf '%s\n' '--- PORT protocol references across repository ---'
rg -n -C 4 'PORT:' . -g '!node_modules' -g '!dist' -g '!build' | head -n 240

printf '%s\n' '--- listen/bind calls near protocol output ---'
rg -n -C 4 'listen\(|PORT|port' packages extensions -g '*.ts' -g '*.js' -g '*.mjs' 2>/dev/null | head -n 320

Repository: phoenixvc/retort-plugins

Length of output: 30332


Validate PORT: records as TCP ports.

Use Number and accept only safe integers from 1 through 65535. Reject numeric prefixes such as PORT:12junk before firing onPortReady.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extensions/vscode/src/services/stateWatcherProcess.ts` around lines 51 - 56,
Update the PORT: parsing logic in the state watcher to parse the entire trimmed
value with Number, accepting only safe integers in the inclusive range 1–65535.
Reject values with trailing non-numeric text such as PORT:12junk, and only
assign _port and fire _onPortReady after validation succeeds.

Comment on lines +24 to +26
// '' means the target is the root itself; '..' or an absolute relative path
// both mean it resolved outside.
if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 "startsWith\\('\\.\\.'\\)|startsWith\\(`\\.\\." \
  extensions/vscode/src/services/workspacePath.ts \
  extensions/vscode/src/services/workspacePath.test.ts

Repository: phoenixvc/retort-plugins

Length of output: 222


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- workspacePath.ts ---'
cat -n extensions/vscode/src/services/workspacePath.ts

printf '%s\n' '--- related tests ---'
if [ -f extensions/vscode/src/services/workspacePath.test.ts ]; then
  cat -n extensions/vscode/src/services/workspacePath.test.ts
else
  fd -i 'workspacePath' extensions/vscode
fi

printf '%s\n' '--- usages ---'
rg -n -C 3 'resolveInWorkspace|workspacePath' extensions/vscode/src extensions/vscode/test 2>/dev/null || true

Repository: phoenixvc/retort-plugins

Length of output: 11454


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import posixpath
import ntpath

cases = ["", "..", "../file", "..settings.json", "/outside", "file.txt"]

for name, mod in [("posix", posixpath), ("windows", ntpath)]:
    print(name)
    for value in cases:
        relative = mod.relpath(mod.join("/workspace", value), "/workspace")
        rejected_original = (
            relative == "" or relative.startswith("..") or mod.isabs(relative)
        )
        rejected_boundary = (
            relative == "" or relative == ".."
            or relative.startswith(".." + ("\\" if name == "windows" else "/"))
            or mod.isabs(relative)
        )
        print(f"{value!r} -> {relative!r}: original={rejected_original}, boundary={rejected_boundary}")
PY

Repository: phoenixvc/retort-plugins

Length of output: 822


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const path = require('node:path');

for (const [name, p] of [['posix', path.posix], ['win32', path.win32]]) {
  const root = name === 'win32' ? 'C:\\workspace\\project' : '/workspace/project';
  console.log(name);
  for (const filePath of ['..settings.json', '..', '../outside.txt', 'sub/../inside.txt']) {
    const rootResolved = p.resolve(root);
    const target = p.resolve(rootResolved, filePath);
    const relative = p.relative(rootResolved, target);
    const originalRejects =
      relative === '' || relative.startsWith('..') || p.isAbsolute(relative);
    const boundaryRejects =
      relative === '' ||
      relative === '..' ||
      relative.startsWith(`..${p.sep}`) ||
      p.isAbsolute(relative);
    console.log(JSON.stringify({ filePath, target, relative, originalRejects, boundaryRejects }));
  }
}
JS

Repository: phoenixvc/retort-plugins

Length of output: 1270


Keep valid filenames that begin with two dots.

relative.startsWith('..') rejects ..settings.json, even when it remains inside the workspace. Reject only .., a parent-directory prefix followed by path.sep, or an absolute relative path. Add coverage for resolveInWorkspace(ROOT, '..settings.json').

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extensions/vscode/src/services/workspacePath.ts` around lines 24 - 26, Update
the containment check in resolveInWorkspace to reject only relative === '..' or
paths beginning with '..' followed by path.sep, while continuing to reject
absolute paths and the workspace root. Preserve valid filenames such as
'..settings.json', and add coverage for resolveInWorkspace(ROOT,
'..settings.json').

@JustAGhosT
JustAGhosT merged commit f112977 into main Aug 11, 2026
2 checks passed
@JustAGhosT
JustAGhosT deleted the claude/silly-sutherland-cdedd2 branch August 11, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant