Skip to content

Commit 1103918

Browse files
montfortclaude
andauthored
feat(framework+cli): fw-4.23.0 / cli-3.20.0 — followups recount + born-resolved closure idioms (closes #222) (#223)
N=2 feedback from the first external adoption (lnxdrive): - New `straymark followups recount` (#222 Finding 1): recompute the CLI-owned counters after a manual-triage session — the §13-compliant fix for counters stranded when drift --apply/promote are no-ops. - drift --apply now recomputes counters even with zero extractions. - Closure-marker vocabulary gains the born-resolved idiom family (#222 Finding 2): closure verb + "in this PR"/"in this commit", e.g. "updated atomically in this PR" → suspected-closed. - Canonical closure-marker idioms documented in FOLLOW-UPS-BACKLOG-PATTERN.md (EN/ES/zh-CN); AGENT-RULES §13, /straymark-followups skill (4 surfaces), STRAYMARK.md §16 and QUICK-REFERENCE gain the recount step. - 6 new tests (2 unit families, 4 integration); 619 passing total. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0edf396 commit 1103918

37 files changed

Lines changed: 422 additions & 64 deletions

CHANGELOG.md

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

88
---
99

10+
## Framework 4.23.0 / CLI 3.20.0 — N=2 feedback: `followups recount` + born-resolved closure idioms
11+
12+
First fixes driven by **external adopter feedback**: the lnxdrive adoption run ([#222](https://github.com/StrangeDaysTech/straymark/issues/222), the first N=2 data point gating the v1 schema's hard stabilization per principle #12 / ADR-2026-06-03-001) surfaced two gaps in the follow-ups lane — both ergonomic/vocabulary, neither schema-level.
13+
14+
### Added (CLI)
15+
16+
- **`straymark followups recount`** (#222 Finding 1) — recompute the CLI-owned `total_*` counters from actual entry statuses and rewrite the frontmatter, without scanning AILOGs or touching entries. Closes the loop between the sanctioned manual-triage workflow (Triage/Consumption = manual status edits) and the CLI-owned counter invariant: until now the only recomputing commands (`drift --apply`, `promote`) were no-ops after a pure-triage session, stranding the file with knowingly stale counters and no §13-compliant fix. Idempotent; upgrades v0 → v1 in place like every other write command.
17+
- **Born-resolved closure idioms** (#222 Finding 2) — the anti-noise vocabulary now recognizes a closure verb (`updated` / `corrected` / `remediated` / `resolved` / `fixed` / `closed`) followed by `in this PR` / `in this commit` (e.g. the lnxdrive phrasing `Charter row updated atomically in this PR`), extracting those bullets as `suspected-closed` instead of reintroducing the TBD noise that #214 Signal 1 removed.
18+
19+
### Changed (CLI)
20+
21+
- **`followups drift --apply` recomputes counters even with zero extractions** (#222 Finding 1) — a pre-commit `drift --apply` now also reconciles counters left stale by a manual-triage session, making the `status` warning's remediation claim actually true.
22+
- `followups status` stale-counter warning now points at `straymark followups recount`.
23+
24+
### Added (Framework)
25+
26+
- **Canonical closure-marker idioms** documented in `FOLLOW-UPS-BACKLOG-PATTERN.md` (EN/ES/zh-CN) — the fixed vocabulary the anti-noise refinement recognizes, so AILOG authors converge on recognizable phrasings at write time instead of discovering unrecognized idioms at extraction.
27+
28+
### Changed (Framework)
29+
30+
- **`AGENT-RULES.md §13` post-Charter-close directive** (EN/ES/zh-CN) gains the recount step: after flipping statuses by hand, run `straymark followups recount` so the CLI-owned counters ride the same commit as the triage. The `/straymark-followups` skill (4 surfaces), `STRAYMARK.md §16` lifecycle table, and `QUICK-REFERENCE.md` command line follow suit.
31+
32+
### Adopter guidance
33+
34+
Run `straymark update` (CLI → `cli-3.20.0`, framework → `fw-4.23.0`). If your registry carries stale counters from a manual triage (the lnxdrive case): `straymark followups recount` fixes it in one command.
35+
36+
---
37+
1038
## Framework 4.22.0 — `/straymark-followups` skill ships the §13 directives as an invocable wrapper
1139

1240
Closes the last gap in the follow-ups first-class lane (fw-4.21.0 / cli-3.19.0): the `AGENT-RULES.md §13` directives — session-start "what's pending?" answered from the canonical registry, pre-commit `followups drift --apply` riding the same commit as the AILOG, post-Charter-close triage and operator-gated `promote` — now ship as an invocable skill across all four agent surfaces, mirroring the `straymark-charter-new` lane. Driven by [#220](https://github.com/StrangeDaysTech/straymark/issues/220) (deferred from the cli-3.19.0 PR because skills ride framework releases and fw-4.21.0 was already tagged). The skill is a **thin wrapper**: parsing, schema validation, counter recomputation, and the FU → TDE elevation all stay in the CLI (`straymark followups list/status/drift/promote`, cli-3.19.0+); the skill only drives the discipline.

README.md

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

278278
| Component | Tag prefix | Example | Includes |
279279
| --- | --- | --- | --- |
280-
| Framework | `fw-` | `fw-4.22.0` | Templates (12 types), governance, directives, Charter template + schema |
281-
| CLI | `cli-` | `cli-3.19.1` | The `straymark` binary |
280+
| Framework | `fw-` | `fw-4.23.0` | Templates (12 types), governance, directives, Charter template + schema |
281+
| CLI | `cli-` | `cli-3.20.0` | The `straymark` binary |
282282

283283
Check installed versions with `straymark status` or `straymark about`.
284284

@@ -311,7 +311,7 @@ See [CLI Reference](https://github.com/StrangeDaysTech/straymark/blob/main/docs/
311311
```bash
312312
# Download the latest framework release ZIP from GitHub
313313
# Go to https://github.com/StrangeDaysTech/straymark/releases
314-
# and download the latest fw-* release (e.g., fw-4.22.0)
314+
# and download the latest fw-* release (e.g., fw-4.23.0)
315315
316316
# Extract and copy to your project
317317
unzip straymark-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 = "straymark-cli"
3-
version = "3.19.1"
3+
version = "3.20.0"
44
edition = "2021"
55
description = "CLI for StrayMark — the cognitive discipline your AI-assisted projects need"
66
license = "MIT"

cli/src/commands/followups/drift.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,28 @@ pub fn run(path: &str, apply: bool, scan_all: bool, range: Option<&str>) -> Resu
114114
"{} registry in sync — no AILOGs with unextracted follow-up content.",
115115
"OK".green().bold()
116116
);
117+
// --apply always reconciles the CLI-owned counters, even with zero
118+
// extractions (#222 Finding 1): after a sanctioned manual-triage
119+
// session the statuses changed but nothing is left to extract, and
120+
// without this write the frontmatter stays knowingly stale.
121+
if apply {
122+
let counters = followups::compute_counters(&registry);
123+
let fm =
124+
followups::fm_apply_counters_and_v1(&registry.frontmatter_raw, &counters);
125+
if fm != registry.frontmatter_raw {
126+
let was_v0 = registry.is_v0();
127+
std::fs::write(&registry_path, followups::assemble(&fm, &registry.body))?;
128+
println!(
129+
" Counters recomputed: {} open / {} suspected-closed / {} promoted (total {}).",
130+
counters.open, counters.suspected_closed, counters.promoted, counters.total
131+
);
132+
if was_v0 {
133+
utils::info(
134+
"Registry upgraded to schema v1 (non-destructive — counters are now CLI-owned).",
135+
);
136+
}
137+
}
138+
}
117139
return Ok(());
118140
}
119141

cli/src/commands/followups/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
//!
44
//! Subcommands mirror the `charter` namespace: `list` / `status` enumerate
55
//! and inspect, `drift` keeps the registry in sync with AILOGs (native
6-
//! replacement for the deprecated adopter-side bash script), `promote`
7-
//! automates the FU → TDE elevation. See `FOLLOW-UPS-BACKLOG-PATTERN.md`
8-
//! and `STRAYMARK.md §16`.
6+
//! replacement for the deprecated adopter-side bash script), `recount`
7+
//! reconciles the CLI-owned counters after a manual-triage session,
8+
//! `promote` automates the FU → TDE elevation. See
9+
//! `FOLLOW-UPS-BACKLOG-PATTERN.md` and `STRAYMARK.md §16`.
910
1011
pub mod drift;
1112
pub mod list;
1213
pub mod promote;
14+
pub mod recount;
1315
pub mod status;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//! `straymark followups recount` — recompute the CLI-owned frontmatter
2+
//! counters from actual entry statuses, without scanning AILOGs.
3+
//!
4+
//! Closes the manual-triage loop found in the first external adoption
5+
//! (issue #222 Finding 1): the v1 lifecycle sanctions Triage and Consumption
6+
//! as manual status edits, but until cli-3.20.0 the only commands that
7+
//! recomputed the CLI-owned `total_*` counters were `drift --apply` and
8+
//! `promote` — both no-ops after a pure-triage session, stranding the
9+
//! frontmatter with knowingly stale counters and no §13-compliant fix.
10+
//!
11+
//! `recount` is the dedicated, idempotent verb: parse the registry, recompute
12+
//! the counters from entry statuses (the same `compute_counters` the pulse
13+
//! uses), rewrite the frontmatter (upgrading v0 → v1 in place, like every
14+
//! other write command), and report. No AILOG scan, no extraction, no entry
15+
//! mutation — counters only.
16+
17+
use anyhow::{anyhow, bail, Result};
18+
use colored::Colorize;
19+
20+
use crate::followups;
21+
use crate::utils;
22+
23+
pub fn run(path: &str) -> Result<()> {
24+
let resolved = utils::resolve_project_root(path)
25+
.ok_or_else(|| anyhow!("StrayMark not installed. Run 'straymark init' first."))?;
26+
let project_root = &resolved.path;
27+
28+
let registry_path = followups::registry_path(project_root);
29+
if !registry_path.exists() {
30+
bail!(
31+
"No follow-ups registry at {}.\n hint: run `straymark followups drift --scan-all --apply` to create and seed it (see STRAYMARK.md §16).",
32+
registry_path.display()
33+
);
34+
}
35+
36+
let registry = followups::parse_registry(&registry_path)?;
37+
for w in &registry.warnings {
38+
utils::warn(w);
39+
}
40+
41+
let counters = followups::compute_counters(&registry);
42+
let fm = followups::fm_apply_counters_and_v1(&registry.frontmatter_raw, &counters);
43+
44+
if fm == registry.frontmatter_raw {
45+
println!(
46+
"{} counters already in sync: {} open / {} suspected-closed / {} promoted (total {}).",
47+
"OK".green().bold(),
48+
counters.open,
49+
counters.suspected_closed,
50+
counters.promoted,
51+
counters.total
52+
);
53+
return Ok(());
54+
}
55+
56+
let was_v0 = registry.is_v0();
57+
std::fs::write(
58+
&registry_path,
59+
followups::assemble(&fm, &registry.body),
60+
)?;
61+
62+
utils::success(&format!(
63+
"Counters recomputed: {} open / {} suspected-closed / {} promoted (total {}).",
64+
counters.open, counters.suspected_closed, counters.promoted, counters.total
65+
));
66+
if was_v0 {
67+
utils::info("Registry upgraded to schema v1 (non-destructive — counters are now CLI-owned).");
68+
}
69+
Ok(())
70+
}

cli/src/commands/followups/status.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,12 @@ fn print_pulse(registry: &Registry) {
181181
println!();
182182
}
183183

184-
// Flag stale frontmatter counters (informational — the next write fixes them).
184+
// Flag stale frontmatter counters (informational — `recount` fixes them).
185185
let declared_open = fm.total_open;
186186
if let Some(declared) = declared_open {
187187
if declared != counters.open {
188188
println!(
189-
" {} frontmatter says total_open: {} but the real count is {} — the next `followups drift --apply` or `promote` recomputes it.",
189+
" {} frontmatter says total_open: {} but the real count is {} — run `straymark followups recount` to reconcile (any write command also recomputes).",
190190
"!".yellow().bold(),
191191
declared,
192192
counters.open

cli/src/followups.rs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -748,11 +748,25 @@ fn extract_section(content: &str, pred: impl Fn(&str) -> bool) -> Option<String>
748748
}
749749
}
750750

751+
/// Closure verbs recognized by the born-resolved idiom family
752+
/// "<verb> … in this PR / in this commit" (#222 Finding 2).
753+
const CLOSURE_VERBS: [&str; 6] = [
754+
"updated",
755+
"corrected",
756+
"remediated",
757+
"resolved",
758+
"fixed",
759+
"closed",
760+
];
761+
751762
/// True when the text carries an explicit in-Charter closure marker:
752763
/// "closed in-Charter" / "closed in Charter" / "resolved in-Charter" /
753-
/// "fixed in batch N" (case-insensitive), or a backtick-wrapped commit hash
754-
/// (7-40 hex chars). The signal that drives `suspected-closed` extraction
755-
/// (#214 Signal 1 — 20-75% of auto-appended entries per batch were already
764+
/// "fixed in batch N", a born-resolved idiom — a closure verb (`updated` /
765+
/// `corrected` / `remediated` / `resolved` / `fixed` / `closed`) followed by
766+
/// "in this PR" or "in this commit" (#222 Finding 2, first external adopter)
767+
/// — all case-insensitive, or a backtick-wrapped commit hash (7-40 hex
768+
/// chars). The signal that drives `suspected-closed` extraction (#214
769+
/// Signal 1 — 20-75% of auto-appended entries per batch were already
756770
/// resolved in-Charter across both documented occurrences).
757771
pub fn has_closure_marker(text: &str) -> bool {
758772
let lower = text.to_lowercase();
@@ -770,6 +784,17 @@ pub fn has_closure_marker(text: &str) -> bool {
770784
return true;
771785
}
772786
}
787+
// Born-resolved idiom: a closure verb anywhere before "in this PR" /
788+
// "in this commit" (e.g. "Charter row updated atomically in this PR").
789+
// rfind takes the last context occurrence, maximizing the verb window.
790+
for ctx in ["in this pr", "in this commit"] {
791+
if let Some(ctx_idx) = lower.rfind(ctx) {
792+
let before = &lower[..ctx_idx];
793+
if CLOSURE_VERBS.iter().any(|v| before.contains(v)) {
794+
return true;
795+
}
796+
}
797+
}
773798
// Backtick-wrapped hex run of 7-40 chars (commit hash reference).
774799
let mut chars = text.char_indices().peekable();
775800
while let Some((i, c)) = chars.next() {
@@ -1174,6 +1199,23 @@ Done.
11741199
assert!(!has_closure_marker("run `cargo test` first"));
11751200
}
11761201

1202+
#[test]
1203+
fn closure_markers_born_resolved_idiom_family() {
1204+
// #222 Finding 2 — the exact lnxdrive phrasing that landed as `open`.
1205+
assert!(has_closure_marker(
1206+
"Charter `## Files to modify` row updated atomically in this PR."
1207+
));
1208+
assert!(has_closure_marker("remediated in this PR"));
1209+
assert!(has_closure_marker("the regression was Corrected in this commit"));
1210+
assert!(has_closure_marker("scope drift recognized and fixed in this PR"));
1211+
// Verb required — context phrase alone is not a closure.
1212+
assert!(!has_closure_marker("discussed in this PR"));
1213+
assert!(!has_closure_marker("tracked in this PR for visibility"));
1214+
// Context phrase required — future-tense / follow-up phrasing is not.
1215+
assert!(!has_closure_marker("will be updated in a follow-up PR"));
1216+
assert!(!has_closure_marker("should be corrected in the next commit"));
1217+
}
1218+
11771219
#[test]
11781220
fn ailog_id_from_path_takes_five_tokens() {
11791221
assert_eq!(

cli/src/main.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,16 @@ enum FollowupsCommands {
339339
#[arg(long = "path", default_value = ".")]
340340
path: String,
341341
},
342+
/// Recompute the CLI-owned frontmatter counters from actual entry
343+
/// statuses, without scanning AILOGs. The §13-compliant way to reconcile
344+
/// counters after a manual-triage session (statuses flipped by hand,
345+
/// nothing left to extract or promote). Idempotent; upgrades v0
346+
/// registries to v1 in place like every other write command.
347+
Recount {
348+
/// Project directory (default: current directory)
349+
#[arg(long = "path", default_value = ".")]
350+
path: String,
351+
},
342352
/// Promote an entry to a TDE document (creates the TDE with
343353
/// promoted_from_followup traceability, marks the entry `promoted`,
344354
/// recomputes counters). Non-interactive; prioritization stays human.
@@ -834,6 +844,7 @@ fn main() {
834844
range,
835845
path,
836846
} => commands::followups::drift::run(&path, apply, scan_all, range.as_deref()),
847+
FollowupsCommands::Recount { path } => commands::followups::recount::run(&path),
837848
FollowupsCommands::Promote { fu_id, title, path } => {
838849
commands::followups::promote::run(&path, &fu_id, title.as_deref())
839850
}

0 commit comments

Comments
 (0)