fix(state-watcher): repair the five parser/protocol defects that left the UI empty - #11
Conversation
… 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>
📝 WalkthroughWalkthroughThe 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. ChangesState watcher protocol and VS Code integration
Parser and workspace-state updates
Estimated code review effort: 4 (Complex) | ~45 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (29)
.github/workflows/vscode.ymlextensions/vscode/src/extension.tsextensions/vscode/src/services/stateWatcherProcess.tspackages/state-watcher/package.jsonpackages/state-watcher/src/index.tspackages/state-watcher/src/parsers/backlog.test.tspackages/state-watcher/src/parsers/backlog.tspackages/state-watcher/src/parsers/markdown-table.test.tspackages/state-watcher/src/parsers/markdown-table.tspackages/state-watcher/src/parsers/session.test.tspackages/state-watcher/src/parsers/tasks.test.tspackages/state-watcher/src/parsers/tasks.tspackages/state-watcher/src/parsers/teams.test.tspackages/state-watcher/src/parsers/teams.tspackages/state-watcher/src/parsers/test-fixtures.tspackages/state-watcher/src/parsers/yaml-lite.test.tspackages/state-watcher/src/parsers/yaml-lite.tspackages/state-watcher/src/router-index.test.tspackages/state-watcher/src/server.tspackages/state-watcher/test/fixtures/retort/.agentkit/spec/teams.yamlpackages/state-watcher/test/fixtures/retort/.claude/state/orchestrator.jsonpackages/state-watcher/test/fixtures/retort/.claude/state/tasks/archive/task-p0-ci-pipeline.jsonpackages/state-watcher/test/fixtures/retort/.claude/state/tasks/archive/task-p1-api-routes.jsonpackages/state-watcher/test/fixtures/retort/.claude/state/tasks/archive/task-p1-db-schema.jsonpackages/state-watcher/test/fixtures/retort/.claude/state/tasks/archive/task-p1-health-check.jsonpackages/state-watcher/test/fixtures/retort/.claude/state/tasks/archive/task-p1-migration-tooling.jsonpackages/state-watcher/test/fixtures/retort/.claude/state/tasks/archive/task-p1-staging-env.jsonpackages/state-watcher/test/fixtures/retort/AGENT_BACKLOG.mdpackages/state-watcher/test/fixtures/retort/AGENT_TEAMS.md
| ``` | ||
| Discovery -> Planning -> Implementation -> Validation -> Ship | ||
| ``` |
There was a problem hiding this comment.
📐 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.
| ``` | |
| 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
…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>
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (13)
.github/workflows/vscode.ymlextensions/vscode/.vscodeignoreextensions/vscode/package.jsonextensions/vscode/src/extension.tsextensions/vscode/src/services/lineReader.test.tsextensions/vscode/src/services/lineReader.tsextensions/vscode/src/services/stateWatcherProcess.tsextensions/vscode/src/services/workspacePath.test.tsextensions/vscode/src/services/workspacePath.tsextensions/vscode/vitest.config.tspackages/state-watcher/src/parsers/tasks.test.tspackages/state-watcher/src/parsers/tasks.tspackages/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
| 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$/, '')); |
There was a problem hiding this comment.
🎯 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.tsRepository: 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/srcRepository: 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/srcRepository: 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.
| 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); | ||
| } |
There was a problem hiding this comment.
🎯 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/servicesRepository: 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,
});
}
JSRepository: 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 320Repository: 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.
| // '' 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; |
There was a problem hiding this comment.
🎯 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.tsRepository: 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 || trueRepository: 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}")
PYRepository: 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 }));
}
}
JSRepository: 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').
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):readyfocuswrong on all 14/api/ask[]for every querydata@ 0.4The five defects
D1 — no initial snapshot.
useBridgesends{type:'ready'}on open and waits for a snapshot; theconnectionhandler only branched oncommand:run, so nothing answered. Panels stayed empty until an unrelated watched file changed.createServernow takes a hooks object withinitialFrames(), andreadyis answered per-client.D2 — task files rejected.
isTaskShape()requiredcreatedAt/updatedAtstrings the framework does not write, andparseTasksdid not recurse — all of retort's tasks live undertasks/archive/, where/orchestrateleaves 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"(strippingteam-so a task's team matches itsteams.yamlid), and timestamps are derived from themessages[]span, falling back to file mtime — derived rather than left absent becauseHandoffFeedPanelcallsupdatedAt.localeCompareandCognitiveMeshPanelformats it as a date. An unrecognised status falls back tosubmittedso 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.yamlcontinues with sibling top-level keys (intake:,techStacks:— itself a sequence of maps), so reading must stop at column 0; andscope:is sometimes a bracketed list the formatter wrapped across lines, which would have leftdocs,forgeandstrategic-opswith empty scope. Also fixeshandoff-chainvshandoffChain. 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.tsasserts 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 takedonefrom the section heading.Also fixed
file:openwas silently dropped. Now relayed asOPEN:<json>and wired throughStateWatcherProcess.onOpenFiletoshowTextDocument.task:updatedform 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, endingsnapshot(tasks=0).Tests
84 new tests in
packages/state-watcher(which had none), built on verbatim copies of~/repos/retortundertest/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 intest-fixtures.ts. Added aTest state-watcherCI step after the router build, since the integration test resolves@retort-plugins/routerfrom its dist.Router's 27 tests still pass. Workspace builds, typechecks and lints clean.
Deliberately not changed
normalisePrioritymaps 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:updatedis now unused by the daemon but remains in the protocol anduseStorehandles it, so incremental pushes stay available — followup filed.packages/uiis untouched. All seven panels render correctly once fed protocol-shaped data.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests