feat(progress): replace initial progress comment with final summary via state file#411
Conversation
chore: release - merge dev into main
chore: release - merge dev into main
|
🔍 Reviewing PR... |
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
The implementation is clean and well-tested, but the feature is currently inert — repoDir is never passed by any caller of createProgressMonitor, so the state file is never written and the PostComment replacement logic never activates.
Architecture & Design
[SHOULD_FIX] repoDir is not wired through from any caller
The new repoDir parameter on ProgressMonitorOptions is the linchpin of this feature — without it, writeProgressCommentId is never called and the state file is never created. All three call sites that create progress monitors omit it:
src/backends/adapter.ts:593—createProgressMonitor({...})— norepoDir(Claude Code backend path).repoDiris in scope as a local variable.src/agents/base.ts:617—createProgressMonitor({...})— norepoDir(llmist backend, Trello agents).repoDiris available as a closure variable.src/agents/shared/githubAgent.ts:201—createProgressMonitor({...})— norepoDir(llmist backend, GitHub agents). These use GitHub comments rather than Trello, so this may be intentionally omitted.
Without wiring repoDir into at least the first two callers, the state file is never written, readProgressCommentId() always returns null, and the PostComment gadget always falls through to addComment — meaning the progress comment is never replaced. The adapter.ts caller is the most critical since the Claude Code backend is where the cross-process state file bridge is most needed.
Code Quality
The refactoring of postProgress into postProgressToPM is a clean extraction that preserves behavior. The new progressState.ts module is well-structured with good edge case handling (colons in comment IDs, malformed files, missing files). Tests are comprehensive and cover the important scenarios including fallback paths.
The design of readProgressCommentId() reading from process.cwd() while writeProgressCommentId() writes to an explicit repoDir is reasonable for cross-process communication (since the subprocess chdirs to repoDir), though it creates an implicit coupling that should be documented.
|
🔍 Reviewing PR... |
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
The core feature — replacing the progress comment with the final summary — cannot work in production because repoDir is never passed to createProgressMonitor by any of the three callers, so the state file is never written.
Architecture & Design
- [SHOULD_FIX] Write/Read location asymmetry:
writeProgressCommentId()writes to an explicitrepoDirparameter, butreadProgressCommentId()reads fromprocess.cwd(). This works only because the agent lifecycle doesprocess.chdir(repoDir)before execution, but the coupling is implicit. Consider making both functions use the same parameter (or both useprocess.cwd()) to make the contract explicit and reduce the chance of bugs when the lifecycle changes.
Code Issues
Blocking
- All three callers of
createProgressMonitornever passrepoDir—src/agents/base.ts:616,src/agents/shared/githubAgent.ts:200, andsrc/backends/adapter.ts:593all callcreateProgressMonitor({...})without includingrepoDir. SinceProgressMonitorguards state file writes withif (this.config.repoDir && ...), the state file is never written, andreadProgressCommentId()inPostCommentalways returnsnull. The entire replacement feature is dead code. All three callers haverepoDiravailable in scope and just need to pass it through.
Should Fix
.cascade-progress-comment-idmissing from.gitignore— Once the feature is wired up, this ephemeral state file will sit in the repo working directory. If an agent happens togit add .or similar, it could be committed. Add.cascade-progress-comment-idto.gitignore.
| */ | ||
| export function readProgressCommentId(): { workItemId: string; commentId: string } | null { | ||
| const filePath = join(process.cwd(), STATE_FILE_NAME); | ||
|
|
There was a problem hiding this comment.
[SHOULD_FIX] readProgressCommentId() reads from process.cwd() while writeProgressCommentId() writes to an explicit repoDir parameter. This implicit coupling to the lifecycle's process.chdir(repoDir) is fragile — if any caller reads before chdir or after chdir-back, it silently returns null. Consider accepting an optional repoDir parameter here too (defaulting to process.cwd() for backward compat) to make the contract explicit.
There was a problem hiding this comment.
Fixed. readProgressCommentId() now accepts an optional repoDir?: string parameter (defaulting to process.cwd()) — matching the API of both writeProgressCommentId() and clearProgressCommentId(). Also added a new test case for the explicit repoDir path.
| commentId: this.progressCommentId, | ||
| }); | ||
|
|
||
| // Write state file so PostComment gadget can update this comment |
There was a problem hiding this comment.
[BLOCKING] This guard (if (this.config.repoDir && this.progressCommentId)) means the state file is only written when repoDir is provided. However, none of the three callers of createProgressMonitor pass repoDir:
src/agents/base.ts:616— hasrepoDirin scope, does not pass itsrc/agents/shared/githubAgent.ts:200— GitHub agents don't use Trello, so N/Asrc/backends/adapter.ts:593— hasrepoDirin scope (line 545), does not pass it
Since this.config.repoDir is always undefined, writeProgressCommentId() is never called, the state file is never written, and the PostComment replacement logic is dead code.
Fix: add repoDir to the createProgressMonitor call sites that use Trello (at minimum adapter.ts and base.ts).
There was a problem hiding this comment.
Fixed. repoDir is now wired through at both call sites:
adapter.ts:593— passesrepoDir ?? undefined(converts fromstring | null)base.ts:616— thecreateProgressMonitorcallback signature inlifecycle.tsnow passes(fileLogger, repoDir), andbase.tsforwards it tocreateProgressMonitor({ repoDir, ... })
The state file will now actually be written when the ProgressMonitor posts the initial comment.
|
👀 Checking this out... |
nhopeatall
left a comment
There was a problem hiding this comment.
Review already submitted - see inline comments above.
|
👀 Checking this out... |
|
🔍 Reviewing PR... |
1 similar comment
|
🔍 Reviewing PR... |
aaight
left a comment
There was a problem hiding this comment.
Code Review — PR #411: Progress Comment Replacement via State File
The overall approach is sound — using a filesystem state file as a bridge between the ProgressMonitor and the PostComment gadget is elegant, particularly for the Claude Code subprocess backend. The refactoring of postProgressToPM() is a nice readability win, and the test coverage is comprehensive. However, there are two issues that need to be addressed before merging.
🔴 BLOCKING: repoDir is never passed at the call sites — feature is dead code
The state file is only written when repoDir is provided in ProgressMonitorConfig, but none of the callers of createProgressMonitor actually pass this option (noted at src/agents/base.ts:616 and src/backends/adapter.ts:593). This means writeProgressCommentId() is never called at runtime, the state file is never created, and readProgressCommentId() in postComment.ts always returns null. The entire replacement flow is unreachable in production.
Fix: add repoDir to the createProgressMonitor call sites that already have it in scope — at minimum adapter.ts and base.ts.
🟡 SHOULD FIX: Asymmetry between writeProgressCommentId and readProgressCommentId
writeProgressCommentId(repoDir, ...) takes an explicit directory, but readProgressCommentId() hardcodes process.cwd(). This works correctly only when the agent subprocess has already chdir'd to the repo directory before calling the gadget — an implicit coupling that's easy to silently break.
Suggestion: add an optional repoDir?: string parameter to readProgressCommentId() (defaulting to process.cwd()) to make the contract explicit and allow callers to be more defensive.
✅ What's working well
postInitialComment()integration — writing the state file immediately after the "🚀 Starting" comment is posted is correct; the timing guaranteesPostCommentwill always find it.stop()cleanup — clearing the state file on stop prevents stale IDs from leaking into the next session.- Fallback logic in
postComment.ts— thetry/catcharoundupdateCommentwithclearProgressCommentId()in both success and failure branches is defensive and correct. clearProgressCommentId()API — accepting optionalrepoDirdefaulting toprocess.cwd()is the right design;readProgressCommentIdshould follow the same pattern.- Test coverage — the
progressState.test.tssuite is thorough: colon-in-commentId edge case (JIRA IDs), empty file, malformed file. Theprogress.test.tsintegration block correctly validates state file writes and clears. - File format parsing — using
indexOf(':')+sliceto handle JIRA-style IDs with embedded colons in the comment ID is a good touch. - Minor nit:
require('node:fs')insideprogressState.test.tstests should useimport { readFileSync } from 'node:fs'(the module is already imported at the top) for ESM consistency.
aaight
left a comment
There was a problem hiding this comment.
Code Review
The approach is solid — a file-based state bridge is a clean way to share the progress comment ID between the ProgressMonitor and the PostComment gadget, and it works for both the llmist backend (same process) and Claude Code (subprocess). The file format, error handling, and test coverage are all well done.
However, there are two issues that need to be fixed before this can land.
🔴 BLOCKING: repoDir is never passed to createProgressMonitor — the feature is dead code
The BLOCKING reviewer comment (#2823925621) is correct and confirmed. adapter.ts:593 and base.ts:617 both call createProgressMonitor(...) without passing repoDir, so this.config.repoDir is always undefined, writeProgressCommentId() is never called, the state file is never written, and postComment's replacement logic never activates.
In src/backends/adapter.ts around line 593–601, repoDir is in scope (declared at line 526, set at line 545) but not forwarded:
const monitor = createProgressMonitor({
// ...
trello: cardId ? { cardId } : undefined,
// ← repoDir missing here
});Same issue in src/agents/base.ts around line 616–625.
Fix: Pass repoDir to createProgressMonitor at both call sites. Since repoDir is string | null in adapter.ts, convert with repoDir ?? undefined to satisfy the string | undefined type.
🟡 SHOULD_FIX: readProgressCommentId() has an asymmetric API (process.cwd() vs explicit repoDir)
The reviewer comment (#2823925616) is well-founded. writeProgressCommentId takes an explicit repoDir, while readProgressCommentId implicitly reads from process.cwd(). The correctness depends on callers ensuring process.chdir(repoDir) has been called before reading — which in adapter.ts happens at line 614, after createProgressMonitor is called (line 593). This fragility will cause subtle bugs if the ordering ever changes.
The fix is straightforward: add an optional repoDir?: string param to readProgressCommentId() (defaulting to process.cwd()), then update postComment.ts to pass the working directory. Since postComment doesn't currently receive a repoDir, the simplest fix is to keep defaulting to process.cwd() inside readProgressCommentId but document that callers must have already chdir'd — or better, thread repoDir through to the postComment gadget.
Minor observations (no changes needed)
require()in ESM tests (progressState.test.tslines 39 and 69):require('node:fs').readFileSync(...)andrequire('node:fs').writeFileSync(...)are used in a few places. These are in a Vitest environment that supports it, but it would be more consistent to use the ES imports already declared at the top of the file.- Split
importstatements inprogressState.ts:existsSync,rmSync,writeFileSyncare imported in one statement andreadFileSyncin a separateimport { readFileSync }— these can be merged into one import for cleanliness. maybeWriteStateFileis only called in the update-failure fallback path — the success path ofpostProgressToPMdoes not call it, which is intentional (the state file written bypostInitialCommentremains current), but a short comment would make this easier to reason about.
The core feature design is right — fixing the two issues above (especially the BLOCKING one) will make this fully functional.
|
@aaight address code review concerns |
Request AddressedWhat was askedAddress code review concerns on PR #411 (progress comment replacement feature). Changes Made
Verification
|
- Pass repoDir to createProgressMonitor in adapter.ts and base.ts so the
state file is actually written (previously dead code)
- Update lifecycle.ts callback signature to forward repoDir
- Add optional repoDir param to readProgressCommentId() for API symmetry
with writeProgressCommentId() and clearProgressCommentId()
- Add .cascade-progress-comment-id to .gitignore
- Merge split fs imports in progressState.ts
- Replace require('node:fs') with ESM imports in progressState tests
- Add clarifying comment on maybeWriteStateFile call path
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
All code review concerns have been addressed in commit e91c930: 🔴 BLOCKING:
|
|
🔍 Reviewing PR... |
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
Sound implementation of a file-based state bridge for replacing the initial progress comment with the final summary. The approach of using a filesystem state file to coordinate between the ProgressMonitor (which creates the initial comment) and the PostComment gadget (which posts the final summary) is well-suited for cross-process communication with the Claude Code subprocess backend.
CI passes, types check, and tests are comprehensive.
Should Fix
Missing state file write in postProgressToPM fallback path
src/backends/progressMonitor.ts:266-273 — The else branch in postProgressToPM (first tick when this.progressCommentId is null) creates a new comment but does NOT call maybeWriteStateFile():
} else {
// First tick: create the comment and store its ID
this.progressCommentId = await provider.addComment(cardId, summary);
this.config.logWriter('INFO', 'Posted progress update to work item', { ... });
// ← missing: this.maybeWriteStateFile(cardId, this.progressCommentId);
}Scenario: If postInitialComment() fails (transient API error on startup), then the first tick() will enter this else branch. The comment is created successfully, this.progressCommentId is set (so future updates work), but the state file is never written. The PostComment gadget won't find the state and will create a duplicate comment instead of replacing it.
The fix is simply adding this.maybeWriteStateFile(cardId, this.progressCommentId); at the end of the else block. The update-success path (line 248) correctly does NOT rewrite the state file (same comment ID), and the update-failure fallback path (line 264) correctly DOES rewrite it (new comment ID). This else branch should be consistent with the fallback path.
This is a low-probability scenario (initial comment fails but subsequent tick succeeds) but worth fixing for completeness since the mechanism is explicitly designed for exactly this kind of resilience.
|
@aaight address code review concerns |
Request AddressedWhat was askedAddress code review concerns on PR #411 (progress comment replacement feature). Changes Made
Verification
|
…fails The `else` branch in `postProgressToPM` (entered when `postInitialComment()` fails and the first tick creates the comment) was not calling `maybeWriteStateFile()`, leaving the state file unwritten. This meant the PostComment gadget would not find the progress comment ID and would create a duplicate instead of replacing it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Addressed the remaining code review concern from @nhopeatall: 🟡 SHOULD_FIX: Missing state file write in
|
|
✅ Review complete — APPROVED Clean implementation of a file-based state bridge for replacing the initial progress comment with the final summary. The state file approach correctly handles cross-process communication (llmist in-process + Claude Code subprocess). One suggestion: wrap |
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
Clean implementation of a file-based state bridge for replacing the initial progress comment with the final summary. The design is sound — using a filesystem state file is the right approach for cross-process communication between the ProgressMonitor (parent process) and PostComment (potentially a Claude Code subprocess). Tests are comprehensive and CI passes.
Should Fix
- src/backends/progressMonitor.ts:139 —
clearProgressCommentId()instop()can throw (e.g.,rmSyncfailure on permissions), andstop()is called fromfinallyblocks in bothlifecycle.ts:375andadapter.ts:648. An uncaught exception here would mask the actual agent execution result. Wrap the cleanup in a try-catch:Alternatively, add a try-catch insidestop(): void { if (this.timer) { clearInterval(this.timer); this.timer = null; } try { clearProgressCommentId(this.config.repoDir); } catch { // State file cleanup is best-effort } }
clearProgressCommentIditself, since cleanup operations should generally be fault-tolerant.
|
@aaight address code review concerns |
Request AddressedWhat was askedAddress code review concerns on PR #411 (progress comment replacement feature). Changes Made
Verification
|
clearProgressCommentId() uses rmSync which can throw on permissions errors. Since stop() is called from finally blocks in lifecycle.ts and adapter.ts, an uncaught exception would mask the actual agent execution result. Wrap in try-catch for best-effort cleanup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Addressed the remaining code review concern from @nhopeatall's approved review: 🟡 SHOULD_FIX: Wrap
|
|
🔍 Reviewing PR... |
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
LGTM — Clean implementation of file-based state bridge for replacing progress comments with final summaries. The approach is well-suited to the cross-process constraint (llmist same-process + Claude Code subprocess) and all edge cases are handled gracefully.
What I verified:
- State file parsing correctly handles colons in commentId (first-colon split), empty/malformed files, and missing files
- No real race conditions: single Node.js event loop for llmist backend; sequential write-then-read ordering for Claude Code subprocess
postComment.tsrelying onprocess.cwd()forreadProgressCommentId()is correct —executeAgentLifecycledoesprocess.chdir(repoDir)before agent execution- Fallback paths are robust: update failure → create new + clear state; state read failure → null → normal addComment
stop()cleanup is best-effort (won't mask agent errors)githubAgent.tscorrectly ignoresrepoDir(GitHub agents use PR comments, not Trello progress)- All 49 new/modified tests pass; TypeScript and CI checks pass
.gitignoreupdated for the ephemeral state file- Dependency direction (gadgets/pm/core → backends/progressState) follows existing patterns (gadgets/tmux → backends/claude-code)
Summary
src/backends/progressState.ts— a file-based state bridge that stores the progress comment ID so thePostCommentgadget can update instead of duplicate itProgressMonitorto write the state file after posting the initial "🚀 Starting" comment and clear it onstop()PostCommentcore to check the state file before callingaddComment, updating the existing progress comment in-place instead of creating a new oneCloses: https://trello.com/c/cdol7mFL/39-planning-agent-final-summary-comment-should-replace-progress-comment
Changes
New file:
src/backends/progressState.tswriteProgressCommentId(repoDir, workItemId, commentId)— writesworkItemId:commentIdto.cascade-progress-comment-idin the repo dirreadProgressCommentId()— reads fromprocess.cwd(), returns{ workItemId, commentId } | nullclearProgressCommentId(repoDir?)— deletes state file from repo dir (or cwd)Modified:
src/backends/progressMonitor.tsrepoDir?: stringtoProgressMonitorConfigpostInitialComment()— callswriteProgressCommentId()after posting initial commentstop()— callsclearProgressCommentId()to clean up on exitpostProgressToPM()— extracted helper (reduces complexity), callswriteProgressCommentId()when fallback comment is createdModified:
src/backends/progress.tsrepoDiroption fromProgressMonitorOptionstoProgressMonitorConfigModified:
src/gadgets/pm/core/postComment.tsaddComment, checksreadProgressCommentId()workItemIdmatches: callsupdateComment()thenclearProgressCommentId()updateCommentfails: falls back toaddCommentand clears stateaddCommentbehaviour (unchanged)New tests
tests/unit/backends/progressState.test.ts— CRUD operations with real FS using temp dirstests/unit/gadgets/pm/core/postComment.test.ts— updated with replacement and fallback scenariostests/unit/backends/progress.test.ts— new describe block for state file integrationTest plan
🤖 Generated with Claude Code