Skip to content

fix(pulse): proposal target_file is used unresolved — relative or wrong-absolute paths silently drop proposals #1611

Description

@xmasyx

Summary

applyProposalEdit() uses the incoming target_file string as-is. When that string is relative, it resolves against the caller's current working directory; when it is absolute but wrong, nothing corrects it. Either way the function returns target file missing and the proposal is dropped — including high-confidence ones — with no louder signal than a status string.

Root cause

LIFEOS/PULSE/lib/telegram-proposals.ts:

export function applyProposalEdit(targetFile: string, editText: string): { ok: true } | { ok: false; reason: string } {
  if (!existsSync(targetFile)) return { ok: false, reason: `target file missing: ${targetFile}` };

Two ways this fails in practice:

  1. Relative target"LIFEOS/USER/CONFIG/OPERATIONAL_RULES.md" resolves against process.cwd(). The reviewer that produces proposals can run from a launchd agent, whose working directory is not the LifeOS root, so the same proposal applies or vanishes depending on who invoked the process.
  2. Wrong-absolute target — the model writing the proposal emits a plausible but incorrect absolute path (a different home segment, or LifeOS where the directory is LIFEOS). It passes the "is absolute" smell test and fails existsSync.

Both produce the same outcome: a proposal that was correct in content is discarded for an addressing problem.

Proposed fix

Every real proposal target lives under ~/.claude. Re-anchor on the first case-insensitive LIFEOS/ segment instead of trusting the string:

// The target_file that reaches us is unreliable in two ways, both of which used to drop
// high-confidence proposals silently: relative (resolved against the caller's cwd — a
// launchd job runs from anywhere) and wrong-absolute (a hallucinated home or casing).
// Both are cured by re-anchoring on the first case-insensitive "LIFEOS/" segment.
export function resolveProposalTarget(targetFile: string): string {
  const m = targetFile.match(/(?:^|\/)(LIFEOS\/.*)$/i);
  if (m) return join(HOME, ".claude", m[1].replace(/^LIFEOS/i, "LIFEOS"));
  return isAbsolute(targetFile) ? targetFile : join(HOME, ".claude", targetFile);
}

then call applyProposalEdit(resolveProposalTarget(p.target_file), …).

Running locally since 2026-07-25. Worth considering alongside it: target file missing deserves to be louder than a status field, since it is the one failure that silently discards work the pipeline already paid for.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions