mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-09 01:49:49 -10:00
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:
commit
1d42af9967
4 files changed
+87
-10
No files matched your search
@@ -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>"
|
||||
);
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in new issue
Block a user