Merge PR #6737 head 6cf3732a88 unmodified

Original authorship preserved; lands as its own commits.

Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
CodeWhale Bot committed 2026-09-30 06:59:05 -07:00
commit 1d42af9967
4 files changed
+87 -10

No files matched your search

+7 -4
View File
@@ -491,10 +491,13 @@ fn base_source_entries(
}
if let Some(content) = project_context.instructions.as_deref() {
let source = project_context
.source_path
.as_ref()
.map_or_else(|| "project".to_string(), |p| p.display().to_string());
// Same helper as ProjectContext::as_system_block, so the report's
// source token derives from the same label the prompt shows. (The
// entry below still displays the absolute source path for operators;
// only the prompt label is relativized.)
let source = crate::project_context::project_instructions_source_label(
project_context.source_path.as_deref(),
);
let mut block = format!(
"<project_instructions source=\"{source}\">\n{content}\n</project_instructions>"
);
+54
View File
@@ -27,6 +27,7 @@ use self::constitution::{load_repo_constitution_block, repo_constitution_candida
use self::pack::generate_bounded_project_overview;
pub use self::pack::generate_project_context_pack;
pub use self::types::ProjectContext;
pub(crate) use self::types::project_instructions_source_label;
use self::types::ProjectContextError;
/// Names of project context files to look for, in priority order.
@@ -1791,6 +1792,51 @@ mod tests {
assert!(block.contains("</project_instructions>"));
}
#[test]
fn project_instructions_source_label_falls_back_to_project() {
use crate::project_context::project_instructions_source_label;
assert_eq!(
project_instructions_source_label(None),
"project",
"a missing source path keeps the historical literal label"
);
assert_eq!(
project_instructions_source_label(Some(std::path::Path::new("/etc/AGENTS.md"))),
"AGENTS.md"
);
}
#[test]
fn project_instructions_source_is_file_name_not_absolute_path() {
// The source label sits inside the pinned system prompt: loading the
// same AGENTS.md from a different directory must leave the
// instructions block byte-identical, so a move emits no spurious
// `<context_update>` history append and no absolute path enters a
// provider-bound label. Scope note: ancestor-chain and project-rule
// labels keep their own (absolute) spellings; relativizing those is
// a separate decision.
let dir_a = tempdir().expect("tempdir a");
let dir_b = tempdir().expect("tempdir b");
fs::write(dir_a.path().join("AGENTS.md"), "Pinned content").expect("write a");
fs::write(dir_b.path().join("AGENTS.md"), "Pinned content").expect("write b");
let block_a = load_project_context(dir_a.path())
.as_system_block()
.expect("block a");
let block_b = load_project_context(dir_b.path())
.as_system_block()
.expect("block b");
assert_eq!(
block_a, block_b,
"a directory move must not change the project instructions block"
);
assert!(block_a.contains("source=\"AGENTS.md\""));
assert!(
!block_a.contains(&dir_a.path().display().to_string()),
"absolute paths must not enter prompt source labels"
);
}
#[test]
fn test_empty_file_warning() {
let tmp = tempdir().expect("tempdir");
@@ -2057,6 +2103,14 @@ mod tests {
.as_deref()
.expect("constitution block rendered");
assert!(block.contains("<codewhale_repo_constitution"));
// Same origin-label convention as project_instructions: file name
// only, no absolute path in the provider-bound label (the locator
// stays available via constitution_source_path and /constitution).
assert!(block.contains("source=\"constitution.json\""));
assert!(
!block.contains(&tmp.path().display().to_string()),
"the constitution prompt label must not carry the absolute path"
);
assert!(block.contains("current user request"));
assert!(block.contains("run focused tests"));
assert!(block.contains("keep the tool-catalog head byte-stable"));
@@ -246,7 +246,15 @@ impl RepoConstitution {
}
format!(
"<codewhale_repo_constitution source=\"{}\">\nCodewhale-specific repo authority policy (local law: subordinate to the global Constitution and the current user request, but above memory and old handoffs; WHALE.md is ignored and should be migrated, not treated as law).\n\n{}</codewhale_repo_constitution>",
source.display(),
// Same origin-label convention as `<project_instructions>`: file
// name only. The rendered `source` here is a runtime-canonicalized
// absolute path (workspace-relative traversal from the
// constitution's fixed relative path), but its final segment is a
// compile-time constant, so the base name is stable across
// directory moves and recasings. This keeps absolute paths out of
// provider-bound prompt labels. Operators still get the locator
// via `constitution_source_path` in the report and /constitution.
super::project_instructions_source_label(Some(source)),
body.trim_end()
)
}
+17 -5
View File
@@ -2,7 +2,7 @@
//! `ProjectContext` value that carries loaded instructions, rules, and the
//! rendered repo-constitution block into the system prompt.
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use thiserror::Error;
@@ -83,10 +83,7 @@ impl ProjectContext {
/// cross-agent `<project_instructions>` prose. Either may be absent.
pub fn as_system_block(&self) -> Option<String> {
let instructions_block = self.instructions.as_ref().map(|content| {
let source = self
.source_path
.as_ref()
.map_or_else(|| "project".to_string(), |p| p.display().to_string());
let source = project_instructions_source_label(self.source_path.as_deref());
let mut block = format!(
"<project_instructions source=\"{source}\">\n{content}\n</project_instructions>"
@@ -122,6 +119,21 @@ impl ProjectContext {
}
}
/// The `source` label for `<project_instructions>` (and repo constitution)
/// blocks: the context file's name only, never its absolute path. The label
/// sits inside the pinned system prompt, so keeping it stable across
/// directory moves and recasings means an unchanged file does not emit a
/// spurious `<context_update>` history append after a move, and absolute
/// project paths stay out of provider-bound prompt labels. Directory
/// identity stays discoverable via the shell; the label names the origin,
/// it is not a locator.
pub(crate) fn project_instructions_source_label(source_path: Option<&Path>) -> String {
source_path
.and_then(|path| path.file_name())
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "project".to_string())
}
/// Merge multiple project contexts (e.g., from nested directories)
#[allow(dead_code)] // Public API for monorepo context merging
pub fn merge_contexts(contexts: &[ProjectContext]) -> Option<String> {