context: [project] block plumbing (Phase 6 commit #1)

Foundation for Phase 6 — adds the field + composer + composition
order with no callers yet. Nothing sets ctx.project; the meta hookup
and startup auto-inject land in commit #2.

Changes:
  - Context.new gains `project` (string, nil) and `_project_opts`
    (cached scan opts for `:tree refresh`; R7).
  - compose_project(text) helper mirrors compose_background /
    compose_summary. Returns "" for nil/empty; otherwise emits
    "\n\n[project]\n" + text.
  - to_messages inserts compose_project BETWEEN compose_background
    and compose_summary so the model reads memory facts -> project
    tree -> earlier conversation -> NORRIS suffix.
  - Same Norris-suppression guard as the other two dynamic blocks
    (R-C1 / R-C4 parity; planner stays on goal anchor).
  - Context:reset preserves ctx.project (R8 — matches the Phase 4
    memory_items rule; startup-injected facts survive a user-driven
    context reset).

Smoke verified (14/14 inline cases):
  - project nil -> no [project] block in sys_content
  - project set -> block present with contents
  - ordering: [background] < [project] < [earlier conversation summary]
  - norris_active suppresses all three; NORRIS suffix still appears
  - :reset clears turns/pending_exec_output/summary; preserves
    memory_items AND project

Regression: test_safety 87/87, test_router_model 31/31, repl loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 22:08:54 +00:00
parent 261b230be8
commit c4fc7fde01
+20 -4
View File
@@ -56,6 +56,12 @@ function M.new(opts)
summarize_fn = opts.summarize_fn,
summary = nil, -- rolling summary string
max_summary_chars = opts.max_summary_chars or 2000,
-- Phase 6 (#issue Phase 6 §6): project file-tree block, set by
-- repl.lua via :tree meta or the cfg.project.auto_tree startup
-- hook. nil = no block injected. Cached scan opts (depth /
-- max_chars overrides) live on _project_opts for :tree refresh.
project = nil,
_project_opts = nil,
}, Context)
end
@@ -171,6 +177,15 @@ local function compose_summary(summary_text)
return "\n\n[earlier conversation summary]\n" .. summary_text
end
-- Phase 6: project file-tree composer. Inserted between [background]
-- and [earlier summary] so the reading order is memory facts →
-- project tree → earlier conversation → NORRIS suffix. Same Norris-
-- suppression rule (callers gate via self.norris_active).
local function compose_project(project_text)
if not project_text or project_text == "" then return "" end
return "\n\n[project]\n" .. project_text
end
-- Phase 3: NORRIS MODE suffix appended to the system prompt when
-- self.norris_active. Carries self.norris_goal so eviction of the
-- user's "[norris] goal: ..." turn doesn't lose the anchor.
@@ -200,12 +215,13 @@ message if they declined.]]
function Context:to_messages()
local sys_content = self.system_prompt
-- Phase 4 [background] memory block + Phase 5 [earlier summary]
-- block. Both suppressed during Norris (R-C1 / R-C4 — avoid
-- redundant tokens per planning iteration; planner stays focused
-- on its goal anchor).
-- Phase 4 [background] memory block + Phase 6 [project] file-tree
-- block + Phase 5 [earlier summary] block. All suppressed during
-- Norris (R-C1 / R-C4 — avoid redundant tokens per planning
-- iteration; planner stays focused on its goal anchor).
if not self.norris_active then
sys_content = sys_content .. compose_background(self.memory_items)
sys_content = sys_content .. compose_project(self.project)
sys_content = sys_content .. compose_summary(self.summary)
end
-- Phase 3 NORRIS MODE suffix. Last block so its instructions dominate.