Add time bound and path-fill gates
Epic context (Task 6294, verbatim): Implement the five improvements from the Project Memory audit (Task 6283). The audit scanned 251 memory-ON runner sessions (2026-10-11..14): only 21 distinct non-codemap units ever reached a prompt, codemap chunks were 42% of injected bytes with zero reads, 24 of 74 search/read tool calls failed on argument names, 16 accepted runner proposals never became injectable, and memory_feedback was called once. The five changes below are one coherent flywheel fix; each is independently shippable.
Full settled inputs: the BA PRD (R-*/AC-*) and the Architecture doc of epic 6294 (sections cited below). This body carries everything this child needs.
## Requirement (verbatim from the epic)
3(b) In gates.py: refuse a gotcha or qa whose body names a repo path (backticked path or file:line) but carries no `paths`, OR auto-fill `paths` from those backticked paths, so the node-side stale verifier can check it (mem-2 was followed 170 times after its fix landed because it had no paths).
3(c) Add a time-bound gate beside _TASK_STATE in gates.py: a body or title matching "yet", "what is left", "next ", "head is", "currently", "so far", "still" is state, not a fact, and is rejected with reason `time_bound`.
BA requirements (verbatim):
- R-C3: Decided: auto-fill, not refuse. When a `gotcha` or `qa` candidate from any feed has empty `paths`, `paths` is filled from repo paths named in the body or title: backticked paths, `file:line` / `file:line-line` refs with the line suffix stripped, and bare path tokens (mem-2 names its path unbackticked; backticked-only would be letter-compliant and useless). A repo path is a relative token that contains `/` and ends in a file extension or `/`. Excluded: URLs (`scheme://`), absolute paths (`/…`), `~`-paths, `..` traversal, tokens that are part of a shell command (e.g. `./dc`). Deduplicated and capped at `LIST_MAX_ITEMS` (20) and `LIST_ITEM_MAX` (200). A candidate that already carries `paths` is left unchanged.
- R-C4: New exclusion reason `time_bound` in `EXCLUSION_REASONS`, checked right after `task_state`. Rejects a candidate whose title or body matches, case-insensitively and on word boundaries, any of `yet`, `what is left`, `next ` (the word "next" followed by whitespace), `head is`, `currently`, `so far`, `still`. Applies to all feeds except `codemap`. Counted under `time_bound` in `memory_rejections`. The `memory_propose` refusal names `time_bound`. Existing units are not re-gated.
## Approach (settled by the Architect)
- `gates.py`: `_TIME_BOUND = re.compile(r"\b(?:yet|what\s+is\s+left|next(?=\s)|head\s+is|currently|so\s+far|still)\b", re.IGNORECASE)`; in `exclusion_reason`, right after the `_TASK_STATE` check: `if feed != "codemap" and _TIME_BOUND.search(text): return "time_bound"`. Append `"time_bound"` to `EXCLUSION_REASONS` (no migration: `memory_rejections.reason` has no CHECK).
- Add `time_bound` to the `_TOOL_MEMORY_PROPOSE` summary's "never a summary of your task or its state" clause (apps/jaahd/agent_vocabulary.py).
- `gates.py`: pure `inferred_paths(candidate) -> tuple[str, ...]`: scan title+body for backticked spans and bare tokens; strip `:N`/`:N-M`; strip one leading `./`; accept a token with `/`, no `://`, not starting `/` or `~`, no `..` segment, ending `.<ext>` (1-6 alphanumerics) or `/`; trim trailing `.,;:)` and closing backticks/quotes; dedupe in first-seen order; cap.
- `ingest.ingest_candidate`: before the gates, when `kind in ("gotcha","qa") and not candidate.paths`, `candidate = dataclasses.replace(candidate, paths=inferred_paths(candidate))` — covers every feed, and the secret pass also scans inferred paths.
- Intended interaction: an auto-pathed unit is served once the node verify job checks it (`verified_at` fresh); a missing file makes it `stale`.
Credentials & reach:
| Oracle | Credential / identity / environment | Status |
|---|---|---|
| All unit and integration tests (AC-A*, B*, C*, D*, E1-E3, X1, X2) | None beyond the worktree. Postgres comes from the slot's `./dc` stack under `./dc check full` | verified: the worktree and `./dc` exist in every coder slot. The BA noted Postgres is absent in analysis slots, so tests run in the coder's gate |
| Deploy watch (operator directive: Jaah Deploy of each PR) | `jaah-forge` (pre-authenticated per task) | verified by construction: provided to every build runner; no credential to fetch |
Out of scope: Do NOT change the existing `_TASK_STATE` regex (deferred F2), `gotcha_without_repro`, `locator_only`, or the canon/secret gates' logic; do NOT add the user/pass/org secret shape (deferred F3); do NOT re-gate existing units.
Epic out of scope (verbatim): "the two immediate data fixes (purging mem-7834's login triple and retiring mem-2/mem-3) are operator actions in Project > Memory, not code in this epic; profiling the node-side codemap job and curate/nightly cron cost; changing the OFF-arm behaviour (memory OFF must remain byte-identical, see apps/api/tests/memory_off_fixture.py); any new memory kind; org-scoped memory."
Do NOT edit `apps/api/tests/memory_off_*.golden.json`, the `memory_off_fixture.py` pins, or `portfolio/fleet/runner-charter.md`. No new kind, feed, flag, setting or env var. Schema changes additive only (coding-standards migration-overlap rule).
Done means:
- AC-C3 — A `gotcha` whose body names `tools/tests/test_context_router_dedupe.py` (bare, the mem-2 text verbatim) with `paths: []` is accepted with `paths == ["tools/tests/test_context_router_dedupe.py"]`. A `qa` naming `` `apps/api/app/x.py:12-30` `` gets `apps/api/app/x.py`. URLs, `/abs`, `~/x`, `../x` and `./dc` are not extracted. A candidate with explicit `paths` is unchanged. The fill happens before the bundle's verify-eligibility check, so the unit is verifiable. — gate unit tests in `apps/api/tests/test_project_memory.py` (gates section); observe pass
- AC-C4 — Each of the seven patterns, in body or title and in any case, rejects a non-codemap candidate with reason `time_bound`. The mem-5 title "What is left for the CP side?" is rejected. "nextjs" and "headless" are NOT rejected (word boundaries). A codemap chunk containing "next " is accepted. The `memory_propose` refusal text names `time_bound`. A `memory_rejections` row is counted under `time_bound`. — gate unit tests + `test_agent_memory.py -k propose`; observe pass
- AC-X1 — Memory-OFF parity: `memory_off_prompt.golden.json` and `memory_off_lease_payload.golden.json` are unchanged (no diff in the PR), and the OFF golden test passes. — `cd apps/api && pytest tests/test_project_memory_dispatch.py -k memory_off_prompt_golden`; `git diff --stat origin/develop -- apps/api/tests/memory_off_*.golden.json` is empty
- AC-X2 — Whole gate green. — `./dc check full` exits 0
- AC-X3 — Availability: every capability above is ON for any project whose memory arm is ON, with no new flag, setting or env var gating it, and it is live on the dev control plane after deploy. — this child has no dev flow; state so in the report
- The post-merge Jaah Deploy run on `develop` succeeds in GitHub (operator directive; a canceled/superseded run is left alone); cite the run.
Outcome
Added the time_bound exclusion gate and the gotcha/qa path auto-fill. ./dc check full is green; PR #5439 merged as 190c64c; Jaah Deploy 38605239487 succeeded. Deferred: R-C4's word list also rejects lasting uses of "still"/"yet".
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)