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
65 changes: 64 additions & 1 deletion cli/src/commands/charter/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ pub fn run(
let audit_dir = straymark_dir.join("audits").join(&canonical_id);
utils::ensure_dir(&audit_dir)?;

let range_explicit = range.is_some();
let range = match range {
Some(r) => r.to_string(),
None => resolve_default_range(project_root),
Expand Down Expand Up @@ -166,7 +167,7 @@ pub fn run(
// Default action: prepare. The --prepare flag is accepted for
// self-documenting invocations but is also the implicit default.
let _ = prepare;
run_prepare(project_root, &straymark_dir, &audit_dir, &charter, &range)
run_prepare(project_root, &straymark_dir, &audit_dir, &charter, &range, range_explicit)
}

// ── Step 1: prepare ────────────────────────────────────────────────────────
Expand All @@ -177,6 +178,7 @@ fn run_prepare(
audit_dir: &Path,
charter: &Charter,
range: &str,
range_explicit: bool,
) -> Result<()> {
println!(
"{} {} ({})",
Expand All @@ -185,6 +187,29 @@ fn run_prepare(
charter.frontmatter.charter_id.dimmed()
);

// #208: a multi-batch Charter whose earlier phases already merged to the base
// branch will silently under-cover when audited with the default range
// (origin/main..HEAD excludes the merged commits). Warn when completed batches
// are detected and the operator did not pin an explicit --range.
if !range_explicit {
let completed = completed_batch_numbers(project_root, charter);
if !completed.is_empty() {
let list = completed
.iter()
.map(|n| format!("Batch {n}"))
.collect::<Vec<_>>()
.join(", ");
eprintln!(
"{} this Charter has completed batches ({list}) — earlier phases may \
already be merged to the base branch. The default audit range \
(origin/main..HEAD) EXCLUDES already-merged commits, so a phase-scoped \
audit can silently under-cover. Pass --range <charter-first-commit>..HEAD \
explicitly to span the whole phase.",
"warn:".yellow().bold()
);
}
}

let context = build_audit_context(project_root, charter, range)?;

let lang = crate::config::StrayMarkConfig::resolve_language(project_root);
Expand Down Expand Up @@ -578,6 +603,44 @@ fn build_audit_context(
})
}

/// Batch numbers marked completed in the Charter body or its referenced AILOGs'
/// Batch Ledgers (an entry is completed when its body is not `(pending)`). A
/// non-empty result signals earlier phases likely landed on the base branch
/// already — the condition that makes the default audit range under-cover (#208).
fn completed_batch_numbers(project_root: &Path, charter: &Charter) -> Vec<u32> {
let agent_logs = project_root
.join(".straymark")
.join("07-ai-audit")
.join("agent-logs");
let mut sources: Vec<String> = vec![charter.body.clone()];
let ids = charter
.frontmatter
.originating_ailogs
.iter()
.flatten()
.chain(charter.frontmatter.execution_ailogs.iter().flatten());
for id in ids {
let prefix = id.split('-').take(5).collect::<Vec<_>>().join("-");
if let Some(found) = walk_for_prefix(&agent_logs, &prefix) {
if let Ok(body) = std::fs::read_to_string(&found) {
sources.push(body);
}
}
}
let mut completed: Vec<u32> = Vec::new();
for src in &sources {
if let Some(entries) = crate::ailog::parse_batch_ledger(src) {
for e in entries {
if !e.is_pending && !completed.contains(&e.n) {
completed.push(e.n);
}
}
}
}
completed.sort_unstable();
completed
}

fn read_originating_ailogs(project_root: &Path, charter: &Charter) -> Result<(String, String)> {
let ailog_ids = match &charter.frontmatter.originating_ailogs {
Some(ids) if !ids.is_empty() => ids.clone(),
Expand Down
4 changes: 0 additions & 4 deletions cli/src/commands/charter/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,6 @@ fn print_detail(c: &Charter, project_root: &std::path::Path) {
}

println!();
println!(" {}", "Phase 2 features (not yet available):".dimmed());
println!(" {}", "telemetry — straymark charter close (planned cli-3.7.0)".dimmed());
println!(" {}", "drift-check — straymark charter drift (planned cli-3.7.0)".dimmed());
println!();
}

fn print_recent(charters: &[Charter]) {
Expand Down
5 changes: 4 additions & 1 deletion cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,10 @@ enum CharterCommands {
charter_id: String,
/// Git revision range (default: origin/main..HEAD with fallback to
/// origin/master..HEAD; falls back to HEAD~1..HEAD with warning when
/// no upstream is reachable). Override with explicit value as needed.
/// no upstream is reachable). For a phase-scoped audit of a multi-batch
/// Charter whose earlier phases already merged, pass an explicit range
/// from the Charter's first commit (e.g. <decl-commit>..HEAD) — the
/// default excludes already-merged commits and would under-cover.
#[arg(long)]
range: Option<String>,
/// Generate the unified audit prompt and write it to
Expand Down
66 changes: 66 additions & 0 deletions cli/tests/charter_audit_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1010,3 +1010,69 @@ fn audit_prepare_uses_en_canonical_when_language_en() {
assert!(!resolved.contains("## Tu rol"));
}


/// #208: a Charter whose AILOG batch ledger has a completed batch triggers a
/// multi-batch under-coverage warning when prepared WITHOUT an explicit --range,
/// and stays silent WITH an explicit range.
#[test]
fn audit_prepare_warns_on_multibatch_without_explicit_range() {
if !bash_available() {
eprintln!("skipping: git not available");
return;
}
let dir = TempDir::new().unwrap();
setup_straymark(dir.path());

// AILOG with a Batch Ledger: Batch 1 completed, Batch 2 still pending.
let ailog = "\
---
id: AILOG-2026-06-20-001-exec
title: Execution log
status: accepted
---

## Batch Ledger

### Batch 1 — Phase one

Done on 2026-06-19. Files: src/foo.rs.

### Batch 2 — Phase two

(pending)
";
std::fs::write(
dir.path().join(".straymark/07-ai-audit/agent-logs/AILOG-2026-06-20-001-exec.md"),
ailog,
)
.unwrap();

// Charter referencing that AILOG.
let charters = dir.path().join(".straymark/charters");
std::fs::create_dir_all(&charters).unwrap();
std::fs::write(
charters.join("01-multibatch.md"),
"---\ncharter_id: CHARTER-01\nstatus: in-progress\neffort_estimate: M\ntrigger: \"t\"\noriginating_ailogs: [AILOG-2026-06-20-001-exec]\n---\n\n# Charter: Multibatch\n\n## Files to modify\n\n| File | Change |\n|---|---|\n| `src/foo.rs` | edit |\n",
)
.unwrap();
init_repo_with_diff(dir.path());

// Without --range: the warning fires.
cargo_bin_cmd!("straymark")
.args(["charter", "audit", "CHARTER-01", "--prepare", "--path"])
.arg(dir.path().to_str().unwrap())
.assert()
.success()
.stderr(predicate::str::contains("completed batches"))
.stderr(predicate::str::contains("under-cover"));

// With explicit --range: the warning is suppressed.
cargo_bin_cmd!("straymark")
.args([
"charter", "audit", "CHARTER-01", "--prepare", "--range", "HEAD~1..HEAD", "--path",
])
.arg(dir.path().to_str().unwrap())
.assert()
.success()
.stderr(predicate::str::contains("completed batches").not());
}
28 changes: 28 additions & 0 deletions cli/tests/charter_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1059,3 +1059,31 @@ fn charter_new_empty_slug_flag_falls_back_to_title() {

assert!(dir.path().join(".straymark/charters/01-hello-world.md").exists());
}

#[test]
fn charter_status_omits_stale_phase2_section() {
// #207 Part 2: drift-check and telemetry have shipped; `charter status`
// must no longer advertise them as "not yet available".
let dir = TempDir::new().unwrap();
setup_straymark_with_charter_template(dir.path());

cargo_bin_cmd!("straymark")
.arg("charter")
.arg("new")
.arg("--title")
.arg("Phase Two")
.arg(dir.path().to_str().unwrap())
.assert()
.success();

cargo_bin_cmd!("straymark")
.arg("charter")
.arg("status")
.arg("CHARTER-01-phase-two")
.arg("--path")
.arg(dir.path().to_str().unwrap())
.assert()
.success()
.stdout(predicate::str::contains("not yet available").not())
.stdout(predicate::str::contains("planned cli-3.7.0").not());
}
2 changes: 2 additions & 0 deletions dist/.agent/workflows/straymark-audit-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ The CLI writes the resolved prompt to:

The prompt is self-contained: it embeds the Charter content, the originating AILOGs, the git diff over the resolved range (default `origin/main..HEAD`, falls back to `HEAD~1..HEAD` if no upstream is reachable), and the discipline rules (REGLA ABSOLUTA — SOLO LECTURA, evidence-citation, severity calibration). The prompt template lifts the seven universal sections from Sentinel's pre-StrayMark audit skill and parameterizes the project-specific hardcodes.

> Multi-batch Charters — pass an explicit `--range`. When auditing one phase of a Charter whose earlier phases already merged to the base branch, the default `origin/main..HEAD` excludes the already-merged commits and the prompt silently under-covers the phase. Pass `--range <charter-first-commit>..HEAD` so all of the phase's commits are in the diff. The CLI prints a warning when it detects completed batches in the Charter's Batch Ledger and no explicit range was given.

The CLI does NOT invoke any LLM. It only resolves placeholders.

### 3. Notify the operator
Expand Down
2 changes: 2 additions & 0 deletions dist/.claude/skills/straymark-audit-prompt/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ The CLI writes the resolved prompt to:

The prompt is self-contained: it embeds the Charter content, the originating AILOGs, the git diff over the resolved range (default `origin/main..HEAD`, falls back to `HEAD~1..HEAD` if no upstream is reachable), and the discipline rules (REGLA ABSOLUTA — SOLO LECTURA, evidence-citation, severity calibration). The prompt template lifts the seven universal sections from Sentinel's pre-StrayMark audit skill and parameterizes the project-specific hardcodes.

> **Multi-batch Charters — pass an explicit `--range`.** When auditing one phase of a Charter whose earlier phases already merged to the base branch, the default `origin/main..HEAD` *excludes* the already-merged commits and the prompt silently under-covers the phase. Pass `--range <charter-first-commit>..HEAD` so all of the phase's commits are in the diff. The CLI prints a warning when it detects completed batches in the Charter's Batch Ledger and no explicit range was given.

The CLI does NOT invoke any LLM. It only resolves placeholders.

### 3. Notify the operator
Expand Down
2 changes: 2 additions & 0 deletions dist/.codex/skills/straymark-audit-prompt/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ The CLI writes the resolved prompt to:

The prompt is self-contained: it embeds the Charter content, the originating AILOGs, the git diff over the resolved range (default `origin/main..HEAD`, falls back to `HEAD~1..HEAD` if no upstream is reachable), and the discipline rules (REGLA ABSOLUTA — SOLO LECTURA, evidence-citation, severity calibration). The prompt template lifts the seven universal sections from Sentinel's pre-StrayMark audit skill and parameterizes the project-specific hardcodes.

> **Multi-batch Charters — pass an explicit `--range`.** When auditing one phase of a Charter whose earlier phases already merged to the base branch, the default `origin/main..HEAD` *excludes* the already-merged commits and the prompt silently under-covers the phase. Pass `--range <charter-first-commit>..HEAD` so all of the phase's commits are in the diff. The CLI prints a warning when it detects completed batches in the Charter's Batch Ledger and no explicit range was given.

The CLI does NOT invoke any LLM. It only resolves placeholders.

### 3. Notify the operator
Expand Down
2 changes: 2 additions & 0 deletions dist/.gemini/skills/straymark-audit-prompt/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ The CLI writes the resolved prompt to:

The prompt is self-contained: it embeds the Charter content, the originating AILOGs, the git diff over the resolved range (default `origin/main..HEAD`, falls back to `HEAD~1..HEAD` if no upstream is reachable), and the discipline rules (REGLA ABSOLUTA — SOLO LECTURA, evidence-citation, severity calibration). The prompt template lifts the seven universal sections from Sentinel's pre-StrayMark audit skill and parameterizes the project-specific hardcodes.

> **Multi-batch Charters — pass an explicit `--range`.** When auditing one phase of a Charter whose earlier phases already merged to the base branch, the default `origin/main..HEAD` *excludes* the already-merged commits and the prompt silently under-covers the phase. Pass `--range <charter-first-commit>..HEAD` so all of the phase's commits are in the diff. The CLI prints a warning when it detects completed batches in the Charter's Batch Ledger and no explicit range was given.

The CLI does NOT invoke any LLM. It only resolves placeholders.

### 3. Notify the operator
Expand Down
21 changes: 10 additions & 11 deletions dist/.straymark/templates/charter/charter-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,13 @@ follow-up insights are captured if the risk surfaces lessons for a later cycle.]
a bounded code-level fix, see STRAYMARK.md §15.B (post-close Batch N.4
amendment) and `straymark charter amend` instead of opening a new Charter.
7. Local verification passes clean.
8. **Auto-checklist drift** (when Phase 2 of the CLI roadmap ships):
`straymark charter drift CHARTER-NN <range>` to detect drifts between declared
and modified files **before** commit. If it reports omissions, complete the work
8. **Auto-checklist drift**:
`straymark charter drift CHARTER-NN --range <range>` to detect drifts between
declared and modified files **before** commit (the range is optional; it defaults
to `HEAD~1..HEAD`). If it reports omissions, complete the work
or document in the AILOG under `## Risk` as `R<N+1> (new, not in Charter)`. If it
reports scope expansion, document in the AILOG the reason (mock updates, generated
files, drift fix pre-existing, etc.). Until Phase 2 ships, run Sentinel's
`check-plan-drift.sh` manually for the same effect.
files, drift fix pre-existing, etc.).
9. Commit + push + open PR.

## Charter Closure
Expand All @@ -171,10 +171,9 @@ When closing this Charter:
stale and confuses future readers (PLAN-07 of Sentinel demonstrated the failure
mode that this step prevents).

2. **Post-merge drift check** (automated when Phase 2 ships + manual review):
- Run `straymark charter drift CHARTER-NN origin/main..HEAD` (Phase 2) or the
equivalent Sentinel script, and validate the output is clean or that all
drifts are documented in the AILOG.
2. **Post-merge drift check**:
- Run `straymark charter drift CHARTER-NN --range origin/main..HEAD`, and
validate the output is clean or that all drifts are documented in the AILOG.
- This catches the rare case where drift is introduced post-merge (squash
mangling, admin amendments, etc.) and the atomic step in #1 could not apply.

Expand Down Expand Up @@ -241,8 +240,8 @@ v3 addition" — the partition was Sentinel's iteration log, not structural).
Sentinel 2026-05-02-001 formalized the gap and proposed format v4 (this template
embodies it).

6. Auto-checklist drift (`straymark charter drift`, Phase 2 of the CLI roadmap;
Sentinel had `scripts/check-plan-drift.sh`) runs in pre-commit (Tasks #7) and at
6. Auto-checklist drift (`straymark charter drift`; Sentinel originally had
`scripts/check-plan-drift.sh`) runs in pre-commit (Tasks #7) and at
Charter closure. Detects OMISSION drifts (file declared, not touched) and SCOPE
EXPANSION drifts (file touched, not declared). Reason: external auditors caught
implementation-gap and hallucination drifts that the implementer did not document
Expand Down
21 changes: 10 additions & 11 deletions dist/.straymark/templates/charter/i18n/es/charter-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,13 @@ captura el follow-up si el riesgo destapa lecciones para un ciclo posterior.]
cualquier `### Batch N` que quede como `(pending)`. Saltar este paso
en Charters de un solo lote — `## Acciones Realizadas` en el AILOG basta.
7. Verification local pasa limpio.
8. **Auto-checklist drift** (cuando entregue Fase 2 del CLI roadmap):
`straymark charter drift CHARTER-NN <range>` para detectar drifts entre lo declarado
y lo modificado **antes** del commit. Si reporta omisiones, completar el trabajo
8. **Auto-checklist drift**:
`straymark charter drift CHARTER-NN --range <range>` para detectar drifts entre lo
declarado y lo modificado **antes** del commit (el rango es opcional; por defecto
`HEAD~1..HEAD`). Si reporta omisiones, completar el trabajo
o documentar en AILOG bajo `## Risk` como `R<N+1> (nuevo, no en Charter)`. Si
reporta scope expansion, documentar en AILOG el motivo (mock updates, generated
files, drift fix pre-existente, etc.). Hasta que Fase 2 entregue, correr el
`check-plan-drift.sh` de Sentinel manualmente para el mismo efecto.
files, drift fix pre-existente, etc.).
9. Commit + push + abrir PR.

## Cierre del Charter
Expand All @@ -171,10 +171,9 @@ Al cerrar este Charter:
el Charter coherente con la ejecución; diferirlo deja el Charter stale y confunde a
lectores futuros (PLAN-07 de Sentinel demostró el failure mode que este step previene).

2. **Post-merge drift check** (automatizado cuando entregue Fase 2 + revisión manual):
- Correr `straymark charter drift CHARTER-NN origin/main..HEAD` (Fase 2) o el
script de Sentinel equivalente, y validar que el output esté limpio o que
todos los drifts estén documentados en el AILOG.
2. **Post-merge drift check**:
- Correr `straymark charter drift CHARTER-NN --range origin/main..HEAD`, y validar
que el output esté limpio o que todos los drifts estén documentados en el AILOG.
- Esto atrapa el caso raro donde drift se introduce post-merge (squash mangling,
amendments admin, etc.) y el step atomic en #1 no pudo aplicar.

Expand Down Expand Up @@ -243,8 +242,8 @@ Sentinel, no estructural).
futuros — AIDEC-2026-05-02-001 de Sentinel formalizó el gap y propuso format v4
(este template lo encarna).

6. Auto-checklist drift (`straymark charter drift`, Fase 2 del CLI roadmap; Sentinel
tenía `scripts/check-plan-drift.sh`) corre en pre-commit (Tasks #7) y al cierre
6. Auto-checklist drift (`straymark charter drift`; Sentinel tenía originalmente
`scripts/check-plan-drift.sh`) corre en pre-commit (Tasks #7) y al cierre
del Charter. Detecta drifts de OMISIÓN (archivo declarado, no tocado) y de SCOPE
EXPANSION (archivo tocado, no declarado). Razón: los auditores externos capturaron
drifts de implementation-gap y hallucination que el implementador no documentó
Expand Down
Loading