Skip to content

fix(core): state perms, contract validation, cleanup + unit-test coverage#420

Merged
devinoldenburg merged 2 commits into
mainfrom
fix/core-followup
Jun 21, 2026
Merged

fix(core): state perms, contract validation, cleanup + unit-test coverage#420
devinoldenburg merged 2 commits into
mainfrom
fix/core-followup

Conversation

@devinoldenburg

Copy link
Copy Markdown
Owner

Closes the remaining core-runtime follow-up gaps from the PR #416 hardening.

What changed

Security & correctness

  • Security: Session state file world-readable (umask default permissions) #72persistence.js now writes state files with mode 0o600 (owner read/write only). The snapshot carries the verbatim goal text, contract, changed-file paths, evidence and reviewer findings, all of which previously landed at the default umask (typically 0644, world-readable on a shared host). The mode is set on the temp file before the atomic rename, so the target is never momentarily world-readable.
  • goal_contract tool: acceptanceCriteria elements not validated — objects pass through as [object Object] #151goal_contract now validates/coerces every string-array field (acceptanceCriteria, requirements, inferred, nonGoals) through a new exported coerceStringList: strings/finite-numbers/booleans are stringified+normalized, objects/arrays/null/NaN are dropped, so a malformed element can never be stored as "[object Object]" and render a garbled sidebar todo.
  • Goal Guard persistence file is never cleaned up; orphaned worktree files accumulate forever #218 — added cleanup() and remove() to createPersistence. cleanup() prunes orphaned OTHER-worktree <hash>.json files that are stale by mtime OR by their embedded updatedAt (default on-disk TTL DEFAULT_PERSIST_TTL_MS = 7 days, deliberately longer than the 24h in-memory TTL), always preserving the current worktree and fresh siblings, sweeping stray .tmp files, and never touching unrelated files. Best-effort/degrade-safe. (Note: guard.js is owned elsewhere; the primitive is added and unit-tested here for that owner to wire into dispose/flush.)
  • Blocked completion marker 'Goal Not Completed' is indistinguishable from a normal 'I am not done yet' agent message #244 — every blocked-completion rewrite is now prefixed with an unambiguous, guard-only sentinel line (⛔ [Goal Guard · blocked completion]) so a guard intervention is searchable and visibly distinct from a benign "I am not done yet" agent message. The existing marker flip (Goal CompletedGoal Not Completed, markdown prefix preserved) and detection contract are unchanged.

Test coverage

Verification

  • node --test "tests/*.test.mjs" → 619 pass, 0 fail (was 516).
  • node scripts/validate-opencode-config.mjs → passes.

Closes #72
Closes #151
Closes #218
Closes #244
Closes #262
Closes #291
Closes #373
Closes #374
Closes #375
Closes #67

…rage

- #72: write guard state files with mode 0600 (owner-only); they hold goal
  text/contract/evidence and were landing at the default umask (world-readable).
- #151: validate/coerce goal_contract string-array fields (acceptanceCriteria,
  requirements, inferred, nonGoals) via coerceStringList so a non-string element
  is dropped instead of stored as "[object Object]".
- #218: add persistence cleanup()/remove() to prune orphaned per-worktree state
  files by on-disk TTL (DEFAULT_PERSIST_TTL_MS = 7d), preserving the current
  worktree and fresh siblings; sweep stray .tmp files; ignore unrelated files.
- #244: prefix every blocked-completion rewrite with an unambiguous guard
  sentinel ("⛔ [Goal Guard · blocked completion]") so an intervention can't be
  confused with a benign "I am not done yet" message; marker flip is preserved.
- #262: deepen logger.js coverage (synthetic-turn edge cases, guardPrompt error
  path/return values, emitGoalCompleted format, best-effort info/warn/toast).
- #291: add direct unit tests for completion/events/system/summary/agents.
- #373: make plugin.test.mjs assert loudly with an ERR_MODULE_NOT_FOUND hint
  instead of a vague falsy check when the tool hook is absent.
- #374: add tests/concurrency.test.mjs proving concurrent goal sessions complete
  independently with no cross-contamination.
- #375: add case-insensitive last-wins parseVerdict coverage (no source change).
- #67: clean up temp dirs in install.test.mjs and persistence.test.mjs (t.after).

Note: guard.js is owned elsewhere; the persistence cleanup primitive is added and
unit-tested here, to be wired into the guard dispose/flush path by that owner.
Comment thread tests/logger.test.mjs Fixed
CodeQL js/incomplete-sanitization: the assertion escaped only [ and ] before
new RegExp(); escape all regex metacharacters so the anchored match is correct
regardless of GUARD_PREFIX content.
@devinoldenburg devinoldenburg merged commit 2f356c5 into main Jun 21, 2026
10 checks passed
@devinoldenburg devinoldenburg deleted the fix/core-followup branch June 21, 2026 11:36
devinoldenburg added a commit that referenced this pull request Jun 21, 2026
…) (#421)

#420 added and unit-tested persistence.cleanup() (prunes orphaned snapshot
files from worktrees that no longer run) but never called it, so orphans still
accumulated. Wire it into guard startup (best-effort, optional-chained so test
doubles are unaffected) and assert the wiring.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment