fix: DECOMPOSE and REQUEST_CHANGES parity with run-loop.sh - #33
Conversation
Both commands now match the run-loop.sh spawn pattern: - -p flag for headless mode (permissions without prompting) - --output-format stream-json for streaming log output - --verbose for detailed logging - --allowedTools with full tool set - --max-turns 200 Output is piped through stream_formatter.py (same pipeline as run-loop.sh) for human-readable logs instead of raw JSON. Also removes the settings.local.json write that attempted to grant Edit/Write permissions for .claude/work/ — this doesn't work because .claude/ is a hardcoded protected path in Claude Code.
| * Find the stream_formatter.py script from the code plugin. | ||
| * Falls back to null if not installed — caller should degrade gracefully. | ||
| */ | ||
| function findStreamFormatter(): string | null { |
There was a problem hiding this comment.
[MEDIUM] Code Quality
[P2] findStreamFormatter duplicates plugin-cache utilities already exported by plugin-cache.ts
Recommendation: Rewrite findStreamFormatter to call getPluginCacheRoot() and findPluginVersions() from plugin-cache.ts, then iterate over versions looking for {version}/tools/python/stream_formatter.py. Alternatively, add a findPluginTool(pluginName, toolSubPath) helper to plugin-cache.ts that accepts an arbitrary sub-path.
const cacheRoot = path.join(os.homedir(), ".claude", "plugins", "cache", "closedloop-ai", "code");
try {
const versions = readdirSync(cacheRoot)
.filter((e: string) => /^\d+\.\d+\.\d+/.test(e))
.sort((a: string, b: string) => { ... });There was a problem hiding this comment.
Fixed — now uses getPluginCacheRoot() and findPluginVersions() from plugin-cache.ts. Removed the duplicated semver sort and readdirSync import.
| @@ -1013,22 +1080,6 @@ async function handleLoopRequest( | |||
| claudeWorkDir = path.join(worktreeDir, ".claude", "work"); | |||
| await fs.mkdir(claudeWorkDir, { recursive: true }); | |||
There was a problem hiding this comment.
[HIGH] Correctness
[P1] Removal of settings.local.json permission grant breaks writes to .claude/work/
Recommendation: Re-add the settings.local.json permission grant before the command dispatch, or add an alternative mechanism to permit writes to .claude/work/**. If the behavior was intentionally removed because a newer Claude Code version no longer needs it, add a comment explaining that.
claudeWorkDir = path.join(worktreeDir, ".claude", "work");
await fs.mkdir(claudeWorkDir, { recursive: true });
// <-- settings.local.json permission grant was removed hereThere was a problem hiding this comment.
Intentional removal. We verified that settings.local.json allow rules do NOT override the .claude/ sensitive path protection — it's hardcoded in Claude Code (even bypassPermissions mode still prompts for .claude/ writes, per docs). The plugin already works around this by routing file modifications through Bash(cat/sed/python) instead of the Edit/Write tools. The work directory will move to .closedloop-ai/work/ in a future change to bypass this entirely.
| try { | ||
| const versions = readdirSync(cacheRoot) | ||
| .filter((e: string) => /^\d+\.\d+\.\d+/.test(e)) | ||
| .sort((a: string, b: string) => { |
There was a problem hiding this comment.
[MEDIUM] Code Quality
[P2] findStreamFormatter duplicates compareSemverDescending from plugin-cache.ts
Recommendation: Replace the inline sort with compareSemverDescending and the version scan with findPluginVersions, both already imported from ./plugin-cache.js. The only local logic needed is the tools/python/stream_formatter.py path construction.
.sort((a: string, b: string) => {
const pa = a.split(".").map(Number);
const pb = b.split(".").map(Number);
for (let i = 0; i < 3; i++) {
const diff = (pb[i] ?? 0) - (pa[i] ?? 0);
if (diff !== 0) { return diff; }
}
return 0;
});There was a problem hiding this comment.
Fixed in same commit as comment #1 — findStreamFormatter now uses getPluginCacheRoot() and findPluginVersions() from plugin-cache.ts. No more inline semver sort.
Code Review SummaryStatus: Needs Attention Reviewers: Bug Hunter A, Bug Hunter B, Unified Auditor, Premise Reviewer, gateway-core-architect Findings
HIGH Issues (should fix)
MEDIUM Issues (consider)
Validation Stats
Recommendation: Address the HIGH issue — the removal of the |
…_CHANGES PR #30 branched before PRs #31-#33 landed, leaving all three commands on the old spawnClaudeFromFile path (--output-format json, no streaming, no token tracking, no --allowedTools, no --max-turns). This commit brings parity with the fixes merged to main: - Add findStreamFormatter() and buildClaudePipeline() (ported from main) - DECOMPOSE: switch from spawnClaudeFromFile to buildClaudePipeline with -p -, --output-format stream-json, --verbose, --allowedTools, --max-turns 200 - EVALUATE_PRD: same treatment — produces claude-output.jsonl for token tracking, claude-stderr.log for debugging, and readable formatted logs - REQUEST_CHANGES: add -p, --output-format stream-json, --verbose, and route through buildClaudePipeline (was spawning claude directly) - Remove now-dead spawnClaudeFromFile helper Testing: `just desktop-typecheck` clean; all 242 tests pass Risks: None — behaviour change is intentional parity fix; formatter falls back gracefully when stream_formatter.py is not installed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
-p,--output-format stream-json,--verbose,--allowedTools,--max-turns 200-p,--output-format stream-json,--verbose(tools/max-turns were already there)stream_formatter.pyfor human-readable log output (same pipeline as run-loop.sh)settings.local.jsonwrite —.claude/is a hardcoded protected path in Claude Code, allow rules don't override itRoot cause
DECOMPOSE and REQUEST_CHANGES were spawned with minimal args copied from the ECS harness's
buildClaudeDirectArgs(), which builds args separately from the spawn site. Missing:-p(headless),--output-format stream-json(streaming logs),--verbose, tool permissions (DECOMPOSE), max-turns (DECOMPOSE).Test plan