Skip to content

Commit dcd3b95

Browse files
montfortclaude
andauthored
fix: O3 — devtrail charter drift --no-ailog-suppress always emits INFO line (#92)
Closes the last pending design discussion from issue #81 (option (c) per Sentinel CHARTER-06 telemetry on issue #91). Resolution: --no-ailog-suppress now always emits at least one line at end of output, regardless of N. Default mode is unchanged. Default + N=0: silent (no new noise — the common case) Default + N>0: AILOG-suppressed: N path(s) block (existing behavior) --flag + N=0: INFO: AILOG-aware suppression bypassed (would have suppressed: 0 paths) --flag + N>0: INFO: AILOG-aware suppression bypassed (would have suppressed: N path(s) listed above) + per-path list with documenting AILOG ID Implementation: compute_ailog_suppressions now runs unconditionally so the count is available regardless of whether suppression is applied. The flag controls only whether suppression mutates the rendered drift list. The asymmetry matches `git diff --stat` — silent default, signal on explicit opt-in. Operators with dispatch suspicion have a one-flag debug path that always says something. Tests: 3 new integration tests (N=0 with flag emits INFO; N>0 with flag emits INFO + per-path list; default at N=0 stays silent — negative test that we didn't add noise). 414/414 pass. Bumps cli-3.8.1 / fw-4.7.1 across canonical surface (governance footers, README + CLI-REFERENCE versioning tables, ADOPTION-GUIDE references). Inline-annotation observation from the same Sentinel telemetry (paths in WARNING block don't carry [suppressed] inline) is NOT bundled — flagged as separate UX polish item, tracked in a follow-up issue, deferred for empirical confirmation in a future cycle before changing. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d82d279 commit dcd3b95

28 files changed

Lines changed: 315 additions & 63 deletions

CHANGELOG.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,75 @@ and this project uses [independent versioning](README.md#versioning) for Framewo
77

88
---
99

10+
## Framework 4.7.1 / CLI 3.8.1 — O3 resolved (`--no-ailog-suppress` always emits INFO line)
11+
12+
Closes the last `pending design discussion` carried forward from issue #81:
13+
**O3** — when `devtrail charter drift --no-ailog-suppress` was passed and there
14+
was nothing for the AILOG-aware filter to suppress (N=0), the output was
15+
**byte-identical** to the default. Operators couldn't tell whether the
16+
suppression logic ran and found nothing, or whether the flag was wired
17+
incorrectly.
18+
19+
Resolution voted by Sentinel CHARTER-06 telemetry on issue #91 (option (c)
20+
"--no-ailog-suppress only", with the N=0 confirming-line extension):
21+
22+
- Default mode stays silent at N=0 — no new noise in the common case.
23+
- `--no-ailog-suppress` always emits at least one line confirming dispatch:
24+
- At N=0: `INFO: AILOG-aware suppression bypassed (would have suppressed: 0 paths)`
25+
- At N>0: `INFO: AILOG-aware suppression bypassed (would have suppressed: N path(s) listed above as drift)` followed by a per-path list with the AILOG ID that documents the risk.
26+
27+
The asymmetry matches the `git diff --stat` shape — silent default, signal on
28+
explicit opt-in. Operators with dispatch suspicion now have a one-flag debug
29+
path that always says something.
30+
31+
### Fixed (CLI)
32+
33+
- **`devtrail charter drift --no-ailog-suppress`** — emits a confirming
34+
`INFO:` line at end of output regardless of N. The line names the count
35+
and (when N>0) lists each path that would have been suppressed with its
36+
documenting AILOG ID. Default mode (suppression on) is unchanged: silent
37+
at N=0, existing `AILOG-suppressed: N path(s)` block at N>0.
38+
39+
Implementation: `compute_ailog_suppressions` now runs unconditionally so
40+
the count is available regardless of whether suppression is applied. The
41+
flag controls only whether the suppression mutates the rendered drift
42+
list.
43+
44+
### Tests
45+
46+
- 3 new integration tests:
47+
- `charter_drift_no_ailog_suppress_emits_info_line_when_n_zero` (the
48+
primary case the issue is about).
49+
- `charter_drift_no_ailog_suppress_emits_info_line_when_n_nonzero`
50+
(count + per-path listing).
51+
- `charter_drift_default_stays_silent_when_n_zero` (negative test:
52+
confirms we did NOT add noise to the common case).
53+
- 414/414 tests pass (3 new on top of 411 from cli-3.8.0).
54+
55+
### Empirical signal that drove this decision
56+
57+
Sentinel CHARTER-06 was constructed deliberately to exercise the
58+
N>0 path (`subscriber.go` declared in the Charter, named in
59+
AILOG-2026-05-03-034's `## Risk` section, not modified during execution).
60+
The captured outputs confirmed the byte-identical-at-N=0 ambiguity and
61+
voted for option (c) with a one-line N=0 confirmation. Full telemetry is
62+
on issue #91.
63+
64+
A secondary observation from the same run — that paths in the WARNING
65+
block don't carry an inline `[suppressed]` annotation, forcing the reader
66+
to scan top-to-bottom to know a WARNING was OK'd — is **not bundled here**.
67+
It's a UX polish item flagged for separate validation; tracked in a
68+
follow-up issue rather than rolled into this patch to keep the change
69+
surgical.
70+
71+
### What's NOT in this release
72+
73+
- Inline `[suppressed]` annotation on WARNING block items (separate issue).
74+
- HTTP API clients for `charter audit` (Phase 3 v1).
75+
- Inter-family heterogeneity auto-enforcement.
76+
77+
---
78+
1079
## Framework 4.7.0 / CLI 3.8.0 — Phase 3 (multi-model external audit, orchestration-only) + open frictions F2/F5/F7
1180

1281
The first feature-bearing release since Phase 2 (fw-4.6.0/cli-3.7.0). Lands the

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,8 @@ DevTrail uses independent version tags for each component:
259259

260260
| Component | Tag prefix | Example | Includes |
261261
|-----------|-----------|---------|----------|
262-
| Framework | `fw-` | `fw-4.7.0` | Templates (12 types), governance, directives, Charter template + schema |
263-
| CLI | `cli-` | `cli-3.8.0` | The `devtrail` binary |
262+
| Framework | `fw-` | `fw-4.7.1` | Templates (12 types), governance, directives, Charter template + schema |
263+
| CLI | `cli-` | `cli-3.8.1` | The `devtrail` binary |
264264

265265
Check installed versions with `devtrail status` or `devtrail about`.
266266

@@ -292,7 +292,7 @@ See [CLI Reference](https://github.com/StrangeDaysTech/devtrail/blob/main/docs/a
292292
```bash
293293
# Download the latest framework release ZIP from GitHub
294294
# Go to https://github.com/StrangeDaysTech/devtrail/releases
295-
# and download the latest fw-* release (e.g., fw-4.7.0)
295+
# and download the latest fw-* release (e.g., fw-4.7.1)
296296
297297
# Extract and copy to your project
298298
unzip devtrail-fw-*.zip -d your-project/

cli/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "devtrail-cli"
3-
version = "3.8.0"
3+
version = "3.8.1"
44
edition = "2021"
55
description = "CLI for DevTrail — the cognitive discipline your AI-assisted projects need"
66
license = "MIT"

cli/src/commands/charter/drift.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,28 @@ pub fn run(
8989
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
9090
let raw_exit = output.status.code().unwrap_or(-1);
9191

92-
// Parse the script output to identify declared-but-not-modified paths so
93-
// we can apply AILOG-suppression. Scope-expansion ("Modified but NOT
94-
// declared") is informational and not suppressed — those paths are not
95-
// in the Charter's declared list and rarely overlap with documented
96-
// risks.
92+
// Parse the script output to identify declared-but-not-modified paths.
93+
// Scope-expansion ("Modified but NOT declared") is informational and not
94+
// suppressed — those paths are not in the Charter's declared list and
95+
// rarely overlap with documented risks.
9796
let omitted = extract_omitted_paths(&stdout);
98-
let suppressions: Vec<(String, String)> = if no_ailog_suppress || omitted.is_empty() {
97+
98+
// O3 (cli-3.8.1, issue #91): always compute what AILOG-aware suppression
99+
// would have done, regardless of the flag. The flag only controls
100+
// whether to APPLY that suppression to the rendered output. This lets us
101+
// emit a confirming INFO line when --no-ailog-suppress is passed (the
102+
// operator opted into the diagnostic mode and deserves visible signal,
103+
// even when N=0).
104+
let would_have_suppressed: Vec<(String, String)> = if omitted.is_empty() {
99105
Vec::new()
100106
} else {
101107
compute_ailog_suppressions(project_root, &charter, &omitted)?
102108
};
109+
let suppressions: Vec<(String, String)> = if no_ailog_suppress {
110+
Vec::new()
111+
} else {
112+
would_have_suppressed.clone()
113+
};
103114
let suppressed_paths: std::collections::HashSet<String> =
104115
suppressions.iter().map(|(p, _)| p.clone()).collect();
105116
let omitted_after: Vec<String> = omitted
@@ -127,6 +138,35 @@ pub fn run(
127138
}
128139
}
129140

141+
// O3 (cli-3.8.1): when --no-ailog-suppress is passed, always emit at
142+
// least one line confirming dispatch — closes the "default and
143+
// --no-ailog-suppress produce byte-identical output at N=0" ambiguity
144+
// reported in Sentinel CHARTER-02 telemetry. Issue #91 vote: option
145+
// (c) "--no-ailog-suppress only", with explicit confirmation when N=0.
146+
if no_ailog_suppress {
147+
println!();
148+
let n = would_have_suppressed.len();
149+
if n == 0 {
150+
println!(
151+
"{} AILOG-aware suppression bypassed (would have suppressed: 0 paths)",
152+
"INFO:".cyan().bold()
153+
);
154+
} else {
155+
println!(
156+
"{} AILOG-aware suppression bypassed (would have suppressed: {} path(s) listed above as drift)",
157+
"INFO:".cyan().bold(),
158+
n
159+
);
160+
for (path, ailog_id) in &would_have_suppressed {
161+
println!(
162+
" - {} [would suppress: {}]",
163+
path,
164+
ailog_id.dimmed()
165+
);
166+
}
167+
}
168+
}
169+
130170
// Final exit code: if AILOG-suppression cleared all omitted paths, the
131171
// script-reported exit is overridden to 0 (the user did the right thing
132172
// by documenting the risk in an AILOG).

cli/tests/charter_drift_test.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,149 @@ review_required: false
279279
.stdout(predicate::str::contains("Declared in Charter but NOT modified"));
280280
}
281281

282+
// ── O3 (cli-3.8.1): --no-ailog-suppress always emits INFO line ──────
283+
284+
#[test]
285+
fn charter_drift_no_ailog_suppress_emits_info_line_when_n_zero() {
286+
// O3 (issue #91): when --no-ailog-suppress is passed AND there's
287+
// nothing the AILOG-aware filter would have suppressed (N=0), we
288+
// still emit one INFO line confirming the flag was honored. Closes
289+
// the byte-identical-output ambiguity Sentinel CHARTER-02 reported.
290+
if !bash_available() {
291+
eprintln!("skipping: bash not available");
292+
return;
293+
}
294+
let dir = TempDir::new().unwrap();
295+
setup_devtrail(dir.path());
296+
write_charter_with_files(dir.path(), &["src/foo.rs"], None);
297+
std::fs::create_dir_all(dir.path().join("src")).unwrap();
298+
std::fs::write(dir.path().join("src/foo.rs"), "// initial\n").unwrap();
299+
git(dir.path(), &["init", "-q", "-b", "main"]);
300+
git(dir.path(), &["add", "."]);
301+
git(dir.path(), &["commit", "-q", "-m", "initial"]);
302+
std::fs::write(dir.path().join("src/foo.rs"), "// edited\n").unwrap();
303+
git(dir.path(), &["add", "."]);
304+
git(dir.path(), &["commit", "-q", "-m", "edit foo"]);
305+
306+
Command::cargo_bin("devtrail")
307+
.unwrap()
308+
.args([
309+
"charter",
310+
"drift",
311+
"CHARTER-01",
312+
"--no-ailog-suppress",
313+
"--path",
314+
])
315+
.arg(dir.path().to_str().unwrap())
316+
.assert()
317+
.success()
318+
.stdout(predicate::str::contains(
319+
"AILOG-aware suppression bypassed (would have suppressed: 0 paths)",
320+
));
321+
}
322+
323+
#[test]
324+
fn charter_drift_no_ailog_suppress_emits_info_line_when_n_nonzero() {
325+
// When --no-ailog-suppress is passed AND there's something the filter
326+
// would have suppressed (N>0), the INFO line names the count and
327+
// lists each path that was bypassed (with the AILOG ID that documents
328+
// the risk). The drift itself is still surfaced as failure exit.
329+
if !bash_available() {
330+
return;
331+
}
332+
let dir = TempDir::new().unwrap();
333+
setup_devtrail(dir.path());
334+
335+
let ailog_path = dir
336+
.path()
337+
.join(".devtrail/07-ai-audit/agent-logs/AILOG-2026-05-03-001-document-bar.md");
338+
std::fs::write(
339+
&ailog_path,
340+
r#"---
341+
id: AILOG-2026-05-03-001
342+
title: Document bar
343+
agent: test
344+
confidence: high
345+
risk_level: low
346+
review_required: false
347+
---
348+
349+
## Risk
350+
351+
- **R3**: `src/bar.rs` documented as scope-simplified.
352+
"#,
353+
)
354+
.unwrap();
355+
356+
write_charter_with_files(
357+
dir.path(),
358+
&["src/foo.rs", "src/bar.rs"],
359+
Some("AILOG-2026-05-03-001"),
360+
);
361+
std::fs::create_dir_all(dir.path().join("src")).unwrap();
362+
std::fs::write(dir.path().join("src/foo.rs"), "// initial\n").unwrap();
363+
std::fs::write(dir.path().join("src/bar.rs"), "// initial\n").unwrap();
364+
git(dir.path(), &["init", "-q", "-b", "main"]);
365+
git(dir.path(), &["add", "."]);
366+
git(dir.path(), &["commit", "-q", "-m", "initial"]);
367+
std::fs::write(dir.path().join("src/foo.rs"), "// edited\n").unwrap();
368+
git(dir.path(), &["add", "."]);
369+
git(dir.path(), &["commit", "-q", "-m", "edit foo only"]);
370+
371+
Command::cargo_bin("devtrail")
372+
.unwrap()
373+
.args([
374+
"charter",
375+
"drift",
376+
"CHARTER-01",
377+
"--no-ailog-suppress",
378+
"--path",
379+
])
380+
.arg(dir.path().to_str().unwrap())
381+
.assert()
382+
.failure()
383+
// INFO line names the count.
384+
.stdout(predicate::str::contains(
385+
"AILOG-aware suppression bypassed (would have suppressed: 1 path(s)",
386+
))
387+
// And lists each would-have-been-suppressed path.
388+
.stdout(predicate::str::contains("src/bar.rs"))
389+
.stdout(predicate::str::contains("would suppress: AILOG-2026-05-03-001"));
390+
}
391+
392+
#[test]
393+
fn charter_drift_default_stays_silent_when_n_zero() {
394+
// The flip side of O3: the DEFAULT (suppression on) must NOT emit
395+
// an INFO line when there's nothing to suppress. The common-case
396+
// output stays minimal — adding ceremony there is what (a)
397+
// "always-on" would have done, which we explicitly rejected per
398+
// the Sentinel CHARTER-06 vote.
399+
if !bash_available() {
400+
return;
401+
}
402+
let dir = TempDir::new().unwrap();
403+
setup_devtrail(dir.path());
404+
write_charter_with_files(dir.path(), &["src/foo.rs"], None);
405+
std::fs::create_dir_all(dir.path().join("src")).unwrap();
406+
std::fs::write(dir.path().join("src/foo.rs"), "// initial\n").unwrap();
407+
git(dir.path(), &["init", "-q", "-b", "main"]);
408+
git(dir.path(), &["add", "."]);
409+
git(dir.path(), &["commit", "-q", "-m", "initial"]);
410+
std::fs::write(dir.path().join("src/foo.rs"), "// edited\n").unwrap();
411+
git(dir.path(), &["add", "."]);
412+
git(dir.path(), &["commit", "-q", "-m", "edit foo"]);
413+
414+
Command::cargo_bin("devtrail")
415+
.unwrap()
416+
.args(["charter", "drift", "CHARTER-01", "--path"])
417+
.arg(dir.path().to_str().unwrap())
418+
.assert()
419+
.success()
420+
.stdout(predicate::str::contains("OK No drift detected"))
421+
// No INFO line in default mode at N=0.
422+
.stdout(predicate::str::contains("AILOG-aware suppression bypassed").not());
423+
}
424+
282425
#[test]
283426
fn charter_drift_resolves_glob_wildcards_in_declared_paths() {
284427
// fw-4.6.2: bulk Charters can declare `prefix*suffix` glob patterns

dist/.devtrail/00-governance/AGENT-RULES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,4 @@ When a change modifies API endpoints:
270270
271271
---
272272
273-
*DevTrail v4.7.0 | [Strange Days Tech](https://strangedays.tech)*
273+
*DevTrail v4.7.1 | [Strange Days Tech](https://strangedays.tech)*

dist/.devtrail/00-governance/C4-DIAGRAM-GUIDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,4 @@ Use a Level 1 (Context) diagram to illustrate:
234234

235235
---
236236

237-
*DevTrail v4.7.0 | [Strange Days Tech](https://strangedays.tech)*
237+
*DevTrail v4.7.1 | [Strange Days Tech](https://strangedays.tech)*

dist/.devtrail/00-governance/DOCUMENTATION-POLICY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,4 +307,4 @@ See also [ADR-2025-01-20-001] for architectural context.
307307
308308
---
309309

310-
*DevTrail v4.7.0 | [Strange Days Tech](https://strangedays.tech)*
310+
*DevTrail v4.7.1 | [Strange Days Tech](https://strangedays.tech)*

dist/.devtrail/00-governance/QUICK-REFERENCE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,4 @@ Mark `review_required: true` when:
213213

214214
---
215215

216-
*DevTrail v4.7.0 | [Strange Days Tech](https://strangedays.tech)*
216+
*DevTrail v4.7.1 | [Strange Days Tech](https://strangedays.tech)*

0 commit comments

Comments
 (0)