Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions changes/jfgy-neutral-parents-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
type: changed
facade: changed
---
[en]
`nxf show` now points at an item's parents in a neutral line — `Parents: <ids> (read for full context)` —
instead of "URGENT RECOMMENDATION: … !!!". Agents reading the board had flagged the old line as a
possible prompt injection. The `parents_notice` field in `--json` and on the facade carries the same
new text; its name and when it appears are unchanged.
[de]
`nxf show` verweist auf die Eltern eines Eintrags jetzt in einer neutralen Zeile —
`Parents: <ids> (read for full context)` — statt mit „URGENT RECOMMENDATION: … !!!“. Agenten, die das
Board lesen, hatten die alte Zeile als möglichen Prompt-Injection-Versuch gemeldet. Das Feld
`parents_notice` in `--json` und in der Fassade trägt denselben neuen Text; Name und Bedingung, wann es
erscheint, bleiben gleich.
2 changes: 1 addition & 1 deletion crates/cli/docs/guide/de/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ NOTES

- 2026-06-23 started on the command layer

This item has the following parents: ab12.0001. URGENT RECOMMENDATION: ALSO READ THESE ITEMS TO GET THE COMPLETE PICTURE!!!
Parents: ab12.0001 (read for full context)
```

`list` gibt jedes Item aus; mit `--json` ist es der kanonische, byte-stabile Schnappschuss
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/docs/guide/en/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ NOTES

- 2026-06-23 started on the command layer

This item has the following parents: ab12.0001. URGENT RECOMMENDATION: ALSO READ THESE ITEMS TO GET THE COMPLETE PICTURE!!!
Parents: ab12.0001 (read for full context)
```

`list` dumps every item; with `--json` it is the canonical, byte-stable snapshot (id-ordered,
Expand Down
15 changes: 7 additions & 8 deletions crates/cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2268,8 +2268,8 @@ pub fn show(json: bool, db: Option<&str>, id: &str) -> Result<()> {
/// they apply), then the DESCRIPTION / DEFINITION OF DONE / DESIGN / NOTES sections whose
/// headings are the plugin's field labels. `--json` carries everything for machines; this is
/// the human surface, so due/defer/assignee live in the record but not in this layout.
/// `parents_notice` (6j6v.zvd0) — the engine-computed parent-pointer notice — is rendered
/// verbatim as the very last thing when the item has parent(s); omitted entirely otherwise.
/// `parents_notice` (6j6v.zvd0) — the engine-computed parent-pointer notice — is printed
/// unchanged as the very last thing when the item has parent(s); omitted entirely otherwise.
#[allow(clippy::too_many_arguments)]
fn render_show(
cfg: &PluginConfig,
Expand Down Expand Up @@ -2417,12 +2417,11 @@ fn render_show(
}
}

// Parent-pointer notice (6j6v.zvd0): the engine's verbatim, loud reminder to also read the
// parent(s) — appended last, only when the item actually has parent(s). Deliberately restates
// the quiet `PARENT:` line above for a single parent (Code Quality #2, PR #255 review): the
// `PARENT:` field is easy to skim past, and for MULTIPLE parents it shows only the LWW
// `belongs_to` winner — this notice is the one place that is both unmissable and lists every
// live parent.
// Parent-pointer notice (6j6v.zvd0, worded neutrally since 6j6v.jfgy): the engine's pointer to
// the parent(s) — appended last, only when the item actually has parent(s). Deliberately
// restates the `PARENT:` line above for a single parent (Code Quality #2, PR #255 review): for
// MULTIPLE parents that field shows only the LWW `belongs_to` winner — this notice is the one
// place that lists every live parent.
if let Some(notice) = parents_notice {
out.push('\n');
out.push_str(notice);
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/golden/issue-tracker/workflow.trycmd
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ NOTES

- 2026-06-23 started on the command layer

This item has the following parents: ab12.0001. URGENT RECOMMENDATION: ALSO READ THESE ITEMS TO GET THE COMPLETE PICTURE!!!
Parents: ab12.0001 (read for full context)


```
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/golden/personal-todo/workflow.trycmd
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ LOG

- 2026-06-23 checked two airlines

This item has the following parents: ab12.0001. URGENT RECOMMENDATION: ALSO READ THESE ITEMS TO GET THE COMPLETE PICTURE!!!
Parents: ab12.0001 (read for full context)


```
Expand Down
21 changes: 14 additions & 7 deletions crates/cli/tests/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ use assert_cmd::Command;
use std::path::Path;
use tempfile::TempDir;

/// How the parent notice opens its line (6j6v.jfgy). The positive test below asserts it is PRESENT,
/// and the two negative tests assert it is ABSENT — one constant for both, so a rewording that
/// moves the lead turns the positive test red instead of leaving the negative ones checking for a
/// string nobody prints any more.
const PARENTS_NOTICE_LEAD: &str = "\nParents: ";

fn nxf() -> Command {
nxs_test_support::cargo_bin("nxf")
}
Expand Down Expand Up @@ -207,8 +213,8 @@ fn section_headings_use_the_plugins_field_labels() {

#[test]
fn appends_parent_notice_at_the_very_end_when_parents_exist() {
// 6j6v.zvd0: an item with a parent gets a loud, verbatim notice appended at the very END of
// `nxf show`'s human output — after NOTES — pointing the reader at the parent(s).
// 6j6v.zvd0: an item with a parent gets a notice appended at the very END of `nxf show`'s
// human output — after NOTES — pointing the reader at the parent(s). Neutral since 6j6v.jfgy.
let tmp = TempDir::new().unwrap();
init(tmp.path(), "issue-tracker");
let parent = create(tmp.path(), "epic", "Epic", &[]);
Expand All @@ -220,10 +226,11 @@ fn appends_parent_notice_at_the_very_end_when_parents_exist() {
.success();

let out = show(tmp.path(), &task);
let expected = format!(
"This item has the following parents: {parent}. URGENT RECOMMENDATION: ALSO READ THESE \
ITEMS TO GET THE COMPLETE PICTURE!!!"
assert!(
out.contains(PARENTS_NOTICE_LEAD),
"the notice opens its own line with the shared lead:\n{out}"
);
let expected = format!("Parents: {parent} (read for full context)");
assert!(
out.trim_end().ends_with(expected.trim_end()),
"notice must be the very last thing in the output:\n{out}"
Expand All @@ -237,7 +244,7 @@ fn omits_parent_notice_when_no_parent() {
let task = create(tmp.path(), "bug", "Standalone", &[]);
let out = show(tmp.path(), &task);
assert!(
!out.contains("URGENT RECOMMENDATION"),
!out.contains(PARENTS_NOTICE_LEAD),
"no parent notice when there is no parent:\n{out}"
);
}
Expand All @@ -258,7 +265,7 @@ fn omits_parent_notice_for_a_contributes_to_only_edge() {

let out = show(tmp.path(), &cream);
assert!(
!out.contains("URGENT RECOMMENDATION"),
!out.contains(PARENTS_NOTICE_LEAD),
"a contributes_to edge is not a parent, so no notice:\n{out}"
);
}
Expand Down
20 changes: 12 additions & 8 deletions crates/facade/src/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1040,17 +1040,21 @@ fn live_parents_of(store: &Store, item_id: &str) -> Result<Vec<String>> {
Ok(live)
}

/// The loud, verbatim notice urging the reader to also read `parents` (6j6v.zvd0): `None` when
/// `parents` is empty, so a parentless item carries no notice at all. Lives here — not in the CLI
/// printer — so `nxf show` (human), `--json`, and every other consumer (MCP, embedders) render the
/// identical text off the identical engine computation.
/// The notice pointing the reader at `parents` (6j6v.zvd0): `None` when `parents` is empty, so a
/// parentless item carries no notice at all. Lives here — not in the CLI printer — so `nxf show`
/// (human), `--json`, and every other consumer (MCP, embedders) render the identical text off the
/// identical engine computation.
///
/// Neutral on purpose (6j6v.jfgy, superseding zvd0's "URGENT RECOMMENDATION … !!!"): agents read
/// this output, and a line that sounds like an order got flagged as a prompt injection in the
/// ticket data — or taught agents to skim the urgent line at the end, which blunts them against a
/// real one. It states where the context is; it does not command.
fn parents_notice(parents: &[String]) -> Option<String> {
if parents.is_empty() {
return None;
}
Some(format!(
"This item has the following parents: {}. URGENT RECOMMENDATION: ALSO READ THESE \
ITEMS TO GET THE COMPLETE PICTURE!!!",
"Parents: {} (read for full context)",
parents.join(", ")
))
}
Expand Down Expand Up @@ -1894,8 +1898,8 @@ pub struct ShowRecord {
/// The item's belongs-to/containment parent ids, sorted (6j6v.zvd0); empty when it has none.
/// `contributes_to` edges are NOT parents here — see [`parents_notice`] for the derived notice.
pub parents: Vec<String>,
/// The verbatim parent-pointer notice (6j6v.zvd0) when [`parents`](Self::parents) is
/// non-empty; `None` for a parentless item.
/// The engine's parent-pointer notice (6j6v.zvd0, neutral since 6j6v.jfgy) when
/// [`parents`](Self::parents) is non-empty; `None` for a parentless item.
pub parents_notice: Option<String>,
}

Expand Down
51 changes: 37 additions & 14 deletions crates/facade/tests/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,18 +453,47 @@ fn show_of_an_item_with_one_parent_carries_parents_and_notice() {
assert_eq!(rec.parents, vec!["ab12.0001".to_string()]);
assert_eq!(
rec.parents_notice.as_deref(),
Some(
"This item has the following parents: ab12.0001. URGENT RECOMMENDATION: ALSO READ \
THESE ITEMS TO GET THE COMPLETE PICTURE!!!"
)
Some("Parents: ab12.0001 (read for full context)")
);

let v = rec.to_value();
assert_eq!(v["parents"], serde_json::json!(["ab12.0001"]));
assert_eq!(
v["parents_notice"],
"This item has the following parents: ab12.0001. URGENT RECOMMENDATION: ALSO READ \
THESE ITEMS TO GET THE COMPLETE PICTURE!!!"
"Parents: ab12.0001 (read for full context)"
);
}

#[test]
fn the_parents_notice_reads_as_data_not_as_an_instruction() {
// 6j6v.jfgy: the old notice shouted ("URGENT RECOMMENDATION: … !!!"), and agents downstream
// flagged it as a prompt injection in the ticket data — or learned to skim the urgent line at
// the end of board output, which blunts them against real injections. Tool output in an agent
// chain must not sound like an order. Pinned as two properties, so a future rewording keeps
// them without re-deriving them: it does not shout (no exclamation mark, no word in capitals),
// and it is one statement that LEADS with the data — `Parents: <ids>` — with no second sentence
// after the ids where an imperative could sit ("Read these first: …", "Parents: x. Read them.").
let mut s = Store::open_in_memory(1);
open_task(&mut s, "ab12.0001", "Epic", "0");
open_task(&mut s, "ab12.0002", "Child", "0");
s.add_parent("ab12.0002", "ab12.0001", "t");

let notice = read::show(&s, "ab12.0002").unwrap().parents_notice.unwrap();
assert!(!notice.contains('!'), "no exclamation mark: {notice}");
let shouted: Vec<&str> = notice
.split(|c: char| !c.is_ascii_alphabetic())
.filter(|w| w.len() > 1 && w.chars().all(|c| c.is_ascii_uppercase()))
.collect();
assert!(
shouted.is_empty(),
"no word in capitals, found {shouted:?}: {notice}"
);
let after_ids = notice
.strip_prefix("Parents: ab12.0001")
.unwrap_or_else(|| panic!("the notice leads with the data, `Parents: <ids>`: {notice}"));
assert!(
!after_ids.contains(['.', ':', '!', '?']),
"one statement: no second sentence after the ids, found {after_ids:?}"
);
}

Expand All @@ -484,10 +513,7 @@ fn show_of_an_item_with_multiple_parents_lists_all_ids_comma_separated() {
);
assert_eq!(
rec.parents_notice.as_deref(),
Some(
"This item has the following parents: ab12.0001, ab12.0002. URGENT \
RECOMMENDATION: ALSO READ THESE ITEMS TO GET THE COMPLETE PICTURE!!!"
)
Some("Parents: ab12.0001, ab12.0002 (read for full context)")
);
}

Expand Down Expand Up @@ -537,10 +563,7 @@ fn show_omits_a_deleted_parent_from_parents_and_notice() {
);
assert_eq!(
rec.parents_notice.as_deref(),
Some(
"This item has the following parents: ab12.0002. URGENT RECOMMENDATION: ALSO READ \
THESE ITEMS TO GET THE COMPLETE PICTURE!!!"
)
Some("Parents: ab12.0002 (read for full context)")
);
}

Expand Down
11 changes: 11 additions & 0 deletions xtask/sweep-retired-vocabulary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,17 @@ VOCABULARY=(
'of a fully withdrawn round' # 7me0's rule, in prose
# NOT listable, and the review of PR #483 found it by hand: a verb that STAYS but may no longer
# be offered to an agent. `git grep 'nxc withdraw' -- crates/chat/src` is that sweep.
#
# nxf 6j6v.jfgy (2026-10-03) — `parents_notice` no longer shouts. zvd0's verbatim "This item has
# the following parents: … URGENT RECOMMENDATION: ALSO READ THESE ITEMS TO GET THE COMPLETE
# PICTURE!!!" became "Parents: <ids> (read for full context)": agents downstream flagged the old
# line as a prompt injection. Each phrase is its own term, because a stray copy in prose or a
# golden may carry only part of it. The field NAME stays and is not listed. Expected hits: the
# jfgy changelog fragment and the comments that name the old text as superseded.
'URGENT RECOMMENDATION'
'ALSO READ THESE ITEMS'
'COMPLETE PICTURE'
'has the following parents'
)

# ---- THE EXCLUSIONS, each with the reason it is not a defect ----------------------------------
Expand Down
Loading