From 5518a3d7a4b2eb96d9005b1670cd8c38af61342c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Villase=C3=B1or=20Montfort?= <195970+montfort@users.noreply.github.com> Date: Fri, 7 Aug 2026 00:01:27 -0600 Subject: [PATCH] =?UTF-8?q?fix(cli+framework):=20fw-4.43.0=20/=20cli-3.45.?= =?UTF-8?q?0=20=E2=80=94=20close=20#416=20and=20#415?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two adopter reports, both silent-failure shaped: the failure produced a message that looked normal. #416 — charter refresh-suggest never found telemetry. close writes CHARTER-NN.telemetry.yaml (slug stripped so the name survives a rename); refresh_suggest built .telemetry.yaml from the Charter's own file stem. The reader never found what the writer wrote, so the refresh heuristic has been inert in EVERY repo with closed Charters since the names diverged. Reproduced on a second repo before touching anything: 27 telemetry files on disk, every Charter reported (missing), chain length 0. What makes it worse than its size: "Chain shorter than 3 closed Charters with telemetry — heuristic not yet meaningful" is indistinguishable from the normal early state, so a seven-Charter chain ran past it unsuspected. Both sides now derive the name from one helper in charter/mod.rs, with a fallback to the legacy name. The regression test asserts what matters — not the string, but that writer and reader derive it from the same place. Also labels the two thresholds (3 = sample size, 6 = trigger value) that were printed as bare numbers and read as contradictory. #415 — duplicate FU ids. THE REPORTED CAUSE DOES NOT REPRODUCE: ids are already assigned per entry, not per AILOG, and have been since before cli-3.41.0. Two real paths, both reproduced: - Parallel branches each computing max+1 against their own copy, then merged. The #391 merge driver renumbers on merge — it was wired in no repo. - Triage pruning a closed entry to a provenance bullet removes its heading, so max() drops and the next drift --apply reissues the number. This one was documented in Sentinel's own registry on 2026-06-04 and went unfixed for two months. Fixes: the high-water mark scans every FU-NNN mention in the body, so a pruned id stays retired; find_entry_unique refuses an ambiguous id and names both entries, replacing first-match resolution in note/set-status/verify/promote/ status; FOLLOWUP-DUPLICATE-ID reports the state as an error, because unlike most registry findings it silently misdirects writes. Docs: the telemetry convention was documented wrong in STRAYMARK.md and QUICK-REFERENCE.md in all three languages — the docs corroborated the reader, not the writer. 967 tests pass. Work in AILOG-2026-08-07-001. Co-Authored-By: Claude Opus 5 (1M context) --- .../AILOG-2026-08-07-001-issues-415-416.md | 126 +++++++++++++ .straymark/follow-ups-backlog.md | 14 +- CHANGELOG.md | 28 +++ Cargo.lock | 2 +- README.md | 4 +- cli/Cargo.toml | 2 +- cli/src/commands/charter/amend.rs | 2 +- cli/src/commands/charter/close.rs | 16 +- cli/src/commands/charter/mod.rs | 108 +++++++++++ cli/src/commands/charter/refresh_suggest.rs | 31 +++- cli/src/commands/followups/note.rs | 9 +- cli/src/commands/followups/promote.rs | 9 +- cli/src/commands/followups/set_status.rs | 9 +- cli/src/commands/followups/status.rs | 10 +- cli/src/commands/followups/verify.rs | 9 +- cli/src/followups.rs | 111 +++++++++++- cli/src/validation.rs | 68 +++++++ cli/tests/duplicate_fu_ids_test.rs | 168 ++++++++++++++++++ dist/.straymark/00-governance/AGENT-RULES.md | 2 +- .../00-governance/C4-DIAGRAM-GUIDE.md | 2 +- .../00-governance/DOCUMENTATION-POLICY.md | 2 +- .../FOLLOW-UPS-BACKLOG-PATTERN.md | 2 +- .../00-governance/QUICK-REFERENCE.md | 6 +- .../00-governance/i18n/es/AGENT-RULES.md | 2 +- .../00-governance/i18n/es/C4-DIAGRAM-GUIDE.md | 2 +- .../i18n/es/DOCUMENTATION-POLICY.md | 2 +- .../i18n/es/FOLLOW-UPS-BACKLOG-PATTERN.md | 2 +- .../00-governance/i18n/es/QUICK-REFERENCE.md | 6 +- .../00-governance/i18n/zh-CN/AGENT-RULES.md | 2 +- .../i18n/zh-CN/C4-DIAGRAM-GUIDE.md | 2 +- .../i18n/zh-CN/DOCUMENTATION-POLICY.md | 2 +- .../i18n/zh-CN/FOLLOW-UPS-BACKLOG-PATTERN.md | 2 +- .../i18n/zh-CN/QUICK-REFERENCE.md | 6 +- dist/STRAYMARK.md | 4 +- dist/dist-manifest.yml | 2 +- docs/adopters/CLI-REFERENCE.md | 4 +- docs/i18n/es/README.md | 4 +- docs/i18n/es/adopters/CLI-REFERENCE.md | 4 +- docs/i18n/zh-CN/README.md | 4 +- docs/i18n/zh-CN/adopters/CLI-REFERENCE.md | 4 +- 40 files changed, 686 insertions(+), 108 deletions(-) create mode 100644 .straymark/07-ai-audit/agent-logs/AILOG-2026-08-07-001-issues-415-416.md create mode 100644 cli/tests/duplicate_fu_ids_test.rs diff --git a/.straymark/07-ai-audit/agent-logs/AILOG-2026-08-07-001-issues-415-416.md b/.straymark/07-ai-audit/agent-logs/AILOG-2026-08-07-001-issues-415-416.md new file mode 100644 index 00000000..c358bd86 --- /dev/null +++ b/.straymark/07-ai-audit/agent-logs/AILOG-2026-08-07-001-issues-415-416.md @@ -0,0 +1,126 @@ +--- +id: AILOG-2026-08-07-001 +title: Two silent failures from adopter reports — an inert refresh heuristic (#416) and duplicate FU ids that misdirected writes (#415) +status: accepted +created: 2026-08-07 +agent: claude-opus-5-v1.0 +confidence: high +review_required: false +risk_level: medium +eu_ai_act_risk: not_applicable +nist_genai_risks: [] +iso_42001_clause: [] +lines_changed: 520 +files_modified: + - cli/src/commands/charter/mod.rs + - cli/src/commands/charter/close.rs + - cli/src/commands/charter/refresh_suggest.rs + - cli/src/commands/charter/amend.rs + - cli/src/followups.rs + - cli/src/validation.rs + - cli/src/commands/followups/ + - cli/tests/duplicate_fu_ids_test.rs + - dist/STRAYMARK.md + - dist/.straymark/00-governance/QUICK-REFERENCE.md + - CHANGELOG.md +observability_scope: none +tags: [charter, telemetry, follow-ups, adopter-feedback, silent-failure, cli, i18n] +related: + - AILOG-2026-08-06-001 + - AILOG-2026-08-04-003 +--- + +# AILOG: issues #415 and #416 + +## Summary + +Two adopter reports, both the same shape: the failure produced a message that +looked normal. #416 had a heuristic reporting "not enough Charters yet" in every +repo because the reader and writer disagreed on a filename. #415 had a registry +where a write could land on the wrong entry and answer `already closed`. + +Both fixed. In #415 the reported cause turned out to be wrong, and the real one +had been sitting in the adopter's own registry since June. + +## #416 — `refresh-suggest` never found telemetry + +The report was correct in every particular, including which of the two +filenames was the right one to keep. `close` writes `CHARTER-NN.telemetry.yaml` +with the slug stripped so the name survives a rename; `refresh_suggest` built +`.telemetry.yaml` from the Charter's own file stem. + +Reproduced on a second repo before touching anything: Sentinel has 27 telemetry +files, all canonical, and `refresh-suggest` reported `(missing)` for its Charter +with `Chain length: 0`. Not specific to the reporting adopter. + +**Fix**: one `canonical_telemetry_name` in `charter/mod.rs`, used by both sides, +with a fallback to the legacy name for telemetry older CLIs may have written. +The regression test asserts what actually matters — not the string, but that +writer and reader derive it from the same place. + +Also corrected: the two thresholds printed as bare numbers (`3` sample size vs +`6` trigger value) read as contradictory; each line now names what it gates. + +**Scope correction.** In my first comment on #416 I said the stale convention +lived in five files including `SPECKIT-CHARTER-BRIDGE.md`. It does not — my grep +matched a line where `NN-slug.md` (correct, that is the Charter file) and the +word "telemetry" merely co-occur. Four files: `STRAYMARK.md` and +`QUICK-REFERENCE.md` in three languages, plus the `charter amend` guidance +string. Corrected in the closing comment. + +## #415 — duplicate FU ids + +**The reported cause does not reproduce.** The issue proposed that ids are +assigned per-AILOG without atomic reservation; `next_n += 1` has been inside the +per-entry loop since before cli-3.41.0, and two AILOGs in one pass produce +`FU-001` and `FU-002` correctly. Suggestion 1 was already implemented. + +Two real paths, both reproduced: + +1. **Parallel branches.** `next_fu_number` is `max + 1` over the *local* copy, + so two branches extracting different follow-ups both take the same number. A + merge then carries both. Reproduced with the exact reported shape — same id, + different `Origin`, different `Source-hash`. With the #391 merge driver wired + the collision does not survive: it renumbers. It was wired nowhere. +2. **Triage pruning.** Pruning a closed entry to a provenance bullet removes its + heading, `max()` drops, and the next `drift --apply` reissues the number. + This one was documented in Sentinel's own registry on **2026-06-04** — + *"the CLI numbers from the max `### FU-` heading and prune-to-provenance-bullet + hides closed ids … collision reported upstream"* — and had gone unfixed for + two months. + +**Fixes**: the high-water mark now scans every `FU-NNN` mention in the registry +body, so a pruned id stays retired; `find_entry_unique` refuses an ambiguous id +and names both entries, replacing first-match resolution in `note`, +`set-status`, `verify`, `promote` and `status`; and `FOLLOWUP-DUPLICATE-ID` +reports the state as an **error** — unlike most registry findings it silently +misdirects writes, and no reading of a duplicate id is intentional. + +## Verification + +- `cargo test`: **967 passed, 0 failed**. +- `charter/mod.rs`: 4 unit tests, including the writer/reader agreement one. +- `duplicate_fu_ids_test.rs`: 5 tests reproducing the reported sequence — the + refused `note`/`set-status` leave the file byte-identical; `validate` reports + the duplicate; a clean registry stays silent (the rule keys on headings, so a + cited id is not a second entry); a pruned id is not reissued. +- Against real repos: `refresh-suggest` on Sentinel now resolves + `CHARTER-02.telemetry.yaml`, chain length `1`, rolling mean computed — it was + `(missing)` and `0`. `validate` reports no duplicate ids there today (the + operator repaired the FU-345 collision by hand before reporting). + +## Risk + +| Id | Risk | Handling | +|----|------|----------| +| R1 | `find_entry_unique` makes `status` fail where it used to print something. A read-only command refusing is a behaviour change. | Deliberate: showing one of two entries sharing an id is how an operator concludes the wrong one is the one they are about to write to. `followups list` still shows everything unfiltered. | +| R2 | `FOLLOWUP-DUPLICATE-ID` is an **error**, so a repo that currently has duplicates will start failing `validate` — including in CI. | Intended. The state misdirects writes and the fix is mechanical (renumber, then `recount`); the hint says so. A warning would reproduce the original failure, which is that nothing said anything. | +| R3 (new, not in Charter) | The high-water mark now derives from raw text, so a stray `FU-9999` in prose would inflate every future id. | Accepted: numbers are cheap and a collision is not, so the scan errs toward retiring too much. Worth revisiting only if an adopter reports id inflation. | +| R4 (new, not in Charter) | The legacy telemetry fallback is unverified — no adopter is known to have `.telemetry.yaml` on disk. It was added because the reporter suggested it and it costs one `exists()` call. | Noted rather than claimed as tested; the unit test covers the resolution order, not a real-world file. | + +## Follow-ups + +- Wire the merge driver in Sentinel and LNXDrive. It is the mitigation for + #415's parallel-branch path and remains unexercised in both — already tracked + from AILOG-2026-08-06-001, restated here because #415 is the first confirmed + instance of that path causing damage in production. diff --git a/.straymark/follow-ups-backlog.md b/.straymark/follow-ups-backlog.md index a4852952..ebeb149c 100644 --- a/.straymark/follow-ups-backlog.md +++ b/.straymark/follow-ups-backlog.md @@ -1,7 +1,7 @@ --- -last_scan: 2026-08-06 +last_scan: 2026-08-07 schema_version: v1 -total_open: 3 +total_open: 4 total_promoted: 0 total_closed_in_session: 2 total_phase_blocked: 0 @@ -17,6 +17,7 @@ fully_extracted_ailogs: - AILOG-2026-08-05-003 - AILOG-2026-08-05-004 - AILOG-2026-08-06-001 + - AILOG-2026-08-07-001 --- # Follow-ups Backlog @@ -88,6 +89,15 @@ Entry shape (v1 — optional fields marked): - **Cost**: TBD - **Notes**: Auto-appended by `straymark followups drift --apply` 2026-08-06. +### FU-006 — Wire the merge driver in Sentinel and LNXDrive. +- **Origin**: AILOG-2026-08-07-001 §Follow-ups +- **Source-hash**: 937ac2c96b96 +- **Status**: open +- **Trigger**: TBD +- **Destination**: TBD +- **Cost**: TBD +- **Notes**: Auto-appended by `straymark followups drift --apply` 2026-08-07. + ## Bucket: time-triggered ## Bucket: charter-triggered diff --git a/CHANGELOG.md b/CHANGELOG.md index 137c8c07..08576851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,34 @@ and this project uses [independent versioning](README.md#versioning) for Framewo --- +## Framework 4.43.0 / CLI 3.45.0 — 2026-08-06 + +Two adopter reports, both silent-failure shaped: a heuristic that had been inert in every repo while reporting a reassuring message, and a registry that let writes land on the wrong entry. + +### Fixed (CLI) + +- **`charter refresh-suggest` never found telemetry** (#416). `close` writes `CHARTER-NN.telemetry.yaml` (slug stripped, so the name survives a Charter rename); `refresh-suggest` built `.telemetry.yaml` from the Charter's own filename. The reader never found what the writer wrote, so the refresh heuristic — the mechanism that detects accumulated spec drift across a Charter chain — has been inert in **every repo with closed Charters** since the names diverged. Both sides now derive the name from one helper, with a fallback to the legacy name for telemetry written by older CLIs. Confirmed on a second repo: 27 telemetry files, every Charter reported `(missing)`, chain length `0`. + + The failure mode is why this matters more than its size: `Chain shorter than 3 closed Charters with telemetry — heuristic not yet meaningful` is indistinguishable from the normal early state, so a seven-Charter chain ran past it without anyone suspecting a defect. + +- **Duplicate FU ids silently misdirected writes** (#415). Commands resolving by id took the **first** match, so with two entries sharing a number `note` annotated the wrong follow-up and `set-status` answered `already closed — nothing to change` — which reads as success while the intended entry stayed open. `note` / `set-status` / `verify` / `promote` / `status` now refuse an ambiguous id and name both entries. + +- **Triage pruning released ids for reuse** (#415, reported from Sentinel 2026-06-04). `next_fu_number` was `max(parsed entries) + 1`, so an id stopped being reserved the moment its entry lost its `### FU-NNN` heading — which is exactly what triage does when it prunes a closed entry to a provenance bullet. The high-water mark now comes from every `FU-NNN` mentioned anywhere in the registry body. + + The other collision path — parallel branches each computing `max + 1` against their own copy — is handled by the #391 merge driver, which renumbers on merge. Reproduced both. + +### Added (CLI) + +- **`FOLLOWUP-DUPLICATE-ID`** validation rule (#415): two entries sharing a `### FU-NNN` heading are reported as an **error**. Unlike most registry findings this one silently misdirects writes, and no reading of a duplicate id is intentional. + +### Changed (CLI) + +- **`refresh-suggest` labels its two thresholds** (#416). `3` (sample size needed to evaluate) and `6` (value the rolling mean must beat) were both printed as bare numbers and read as contradictory. Each line now names what it gates. + +### Fixed (Framework) + +- **The telemetry filename convention was documented wrong** (#416) in `STRAYMARK.md` (§10 and §15) and `QUICK-REFERENCE.md` (EN/es/zh-CN) — the docs corroborated the reader, not the writer, in every language. Same correction in the `charter amend --merge-into` guidance the CLI prints. + ## CLI 3.44.0 — 2026-08-06 ### Added (CLI) diff --git a/Cargo.lock b/Cargo.lock index 617f7f30..5b0e867f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2574,7 +2574,7 @@ dependencies = [ [[package]] name = "straymark-cli" -version = "3.44.0" +version = "3.45.0" dependencies = [ "anyhow", "arborist-metrics", diff --git a/README.md b/README.md index 18f37744..44bddec5 100644 --- a/README.md +++ b/README.md @@ -278,8 +278,8 @@ StrayMark uses independent version tags for each component: | Component | Tag prefix | Example | Includes | | --- | --- | --- | --- | -| Framework | `fw-` | `fw-4.42.0` | Templates (12 types), governance, directives, Charter template + schema | -| CLI | `cli-` | `cli-3.44.0` | The `straymark` binary | +| Framework | `fw-` | `fw-4.43.0` | Templates (12 types), governance, directives, Charter template + schema | +| CLI | `cli-` | `cli-3.45.0` | The `straymark` binary | | Loom (EXPERIMENTAL) | `loom-` | `loom-0.4.2` | The `straymark-loom` visualization server, downloaded on demand by `straymark loom serve` | Check installed versions with `straymark status` or `straymark about`. diff --git a/cli/Cargo.toml b/cli/Cargo.toml index eafd1e2f..d2146fca 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "straymark-cli" -version = "3.44.0" +version = "3.45.0" edition = "2021" description = "CLI for StrayMark — the cognitive discipline your AI-assisted projects need" license = "MIT" diff --git a/cli/src/commands/charter/amend.rs b/cli/src/commands/charter/amend.rs index 524ab619..6bbd55ca 100644 --- a/cli/src/commands/charter/amend.rs +++ b/cli/src/commands/charter/amend.rs @@ -352,7 +352,7 @@ findings_closed: {findings_closed}\n\ \n\ ## Telemetry\n\ \n\ -This amendment populates `charter_telemetry.post_close_amendment:` in the Charter's `.telemetry.yaml`. Run `straymark charter amend {charter_id} --trigger {trigger} --merge-into .straymark/charters/.telemetry.yaml` (this command) with the appropriate path to auto-merge, or paste the YAML block printed by the CLI.\n\ +This amendment populates `charter_telemetry.post_close_amendment:` in the Charter's `.telemetry.yaml`. Run `straymark charter amend {charter_id} --trigger {trigger} --merge-into .straymark/charters/CHARTER-NN.telemetry.yaml` (this command) with the appropriate path to auto-merge, or paste the YAML block printed by the CLI.\n\ ", amends .map(|a| format!("- **Amends**: [{a}]({a}.md) (forward pointer; the original gets a `## Historical correction` subsection)\n")) diff --git a/cli/src/commands/charter/close.rs b/cli/src/commands/charter/close.rs index 8eb7f929..eea6a6e6 100644 --- a/cli/src/commands/charter/close.rs +++ b/cli/src/commands/charter/close.rs @@ -305,19 +305,11 @@ fn truncate_desc(s: &str, max: usize) -> String { } /// Build the `.straymark/charters/CHARTER-NN.telemetry.yaml` path for a Charter. +/// +/// Delegates to the shared helper so the writer and every reader derive the +/// name from one place (GH #416). fn telemetry_path_for(charters_state_dir: &Path, charter: &Charter) -> PathBuf { - // Strip optional slug suffix to keep the telemetry filename stable across - // Charter renames: CHARTER-01-foo → CHARTER-01.telemetry.yaml. - let id = &charter.frontmatter.charter_id; - let canonical = id - .split_once('-') - .and_then(|(prefix, rest)| { - // CHARTER-NN[-slug] → CHARTER-NN - let nn = rest.split('-').next()?; - Some(format!("{}-{}", prefix, nn)) - }) - .unwrap_or_else(|| id.clone()); - charters_state_dir.join(format!("{}.telemetry.yaml", canonical)) + super::canonical_telemetry_path(charters_state_dir, &charter.frontmatter.charter_id) } /// Copy the template YAML to the telemetry path. Refuses to overwrite an diff --git a/cli/src/commands/charter/mod.rs b/cli/src/commands/charter/mod.rs index 11cb053c..bc6fa571 100644 --- a/cli/src/commands/charter/mod.rs +++ b/cli/src/commands/charter/mod.rs @@ -15,3 +15,111 @@ pub mod list; pub mod new; pub mod refresh_suggest; pub mod status; + +use std::path::{Path, PathBuf}; + +/// Canonical name of a Charter's telemetry sidecar: `CHARTER-NN.telemetry.yaml`. +/// +/// Derived from `charter_id` with any slug suffix stripped, so the filename +/// survives a Charter rename (`CHARTER-01-foo` → `CHARTER-01.telemetry.yaml`). +/// +/// **This is the single definition on purpose** (GH #416). `close` built this +/// name while `refresh-suggest` built `.telemetry.yaml` from the +/// Charter's own filename, so the reader never found what the writer wrote and +/// the refresh heuristic was inert in every repo — reported as `(missing)` for +/// every Charter, indistinguishable from "not enough closed Charters yet". +pub fn canonical_telemetry_name(charter_id: &str) -> String { + let canonical = charter_id + .split_once('-') + .and_then(|(prefix, rest)| { + // CHARTER-NN[-slug] → CHARTER-NN + let nn = rest.split('-').next()?; + Some(format!("{prefix}-{nn}")) + }) + .unwrap_or_else(|| charter_id.to_string()); + format!("{canonical}.telemetry.yaml") +} + +/// Where `close` writes a Charter's telemetry. +pub fn canonical_telemetry_path(charters_state_dir: &Path, charter_id: &str) -> PathBuf { + charters_state_dir.join(canonical_telemetry_name(charter_id)) +} + +/// Legacy sidecar name written before the canonical one: the Charter's own file +/// stem plus `.telemetry.yaml`. Read-only — never written any more, but adopters +/// may still have files under it. +pub fn legacy_telemetry_path(charter_path: &Path) -> Option { + let stem = charter_path.file_stem()?.to_str()?; + let parent = charter_path.parent()?; + Some(parent.join(format!("{stem}.telemetry.yaml"))) +} + +/// Locate a Charter's telemetry on disk: canonical name first, legacy second. +/// +/// Returns the canonical path even when nothing exists, so callers can report a +/// meaningful "expected here" rather than `None`. +pub fn resolve_telemetry_path(charter_path: &Path, charter_id: &str) -> PathBuf { + let dir = charter_path.parent().unwrap_or_else(|| Path::new(".")); + let canonical = canonical_telemetry_path(dir, charter_id); + if canonical.exists() { + return canonical; + } + if let Some(legacy) = legacy_telemetry_path(charter_path) { + if legacy.exists() { + return legacy; + } + } + canonical +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn canonical_name_strips_the_slug() { + assert_eq!( + canonical_telemetry_name("CHARTER-22-fundacion-expedientes"), + "CHARTER-22.telemetry.yaml" + ); + assert_eq!(canonical_telemetry_name("CHARTER-02"), "CHARTER-02.telemetry.yaml"); + } + + #[test] + fn canonical_name_passes_through_unrecognized_shapes() { + assert_eq!(canonical_telemetry_name("WEIRD"), "WEIRD.telemetry.yaml"); + } + + /// GH #416: the writer and the reader disagreed for two releases and the + /// only visible symptom was a reassuring "not enough Charters yet" message. + /// What must never regress is not the name — it is that both sides derive + /// it from the same place. + #[test] + fn writer_and_reader_agree_on_the_name() { + let dir = std::path::Path::new("/p/.straymark/charters"); + let charter_path = dir.join("22-fundacion-expedientes.md"); + let id = "CHARTER-22-fundacion-expedientes"; + + let written = canonical_telemetry_path(dir, id); + let read = resolve_telemetry_path(&charter_path, id); + assert_eq!(written, read); + assert!(written.ends_with("CHARTER-22.telemetry.yaml")); + } + + #[test] + fn falls_back_to_the_legacy_name_when_only_that_exists() { + let tmp = tempfile::tempdir().unwrap(); + let dir = tmp.path(); + let charter_path = dir.join("07-old.md"); + std::fs::write(&charter_path, "x").unwrap(); + std::fs::write(dir.join("07-old.telemetry.yaml"), "y").unwrap(); + + let resolved = resolve_telemetry_path(&charter_path, "CHARTER-07-old"); + assert!(resolved.ends_with("07-old.telemetry.yaml")); + + // Once the canonical file appears it wins, without touching the legacy one. + std::fs::write(dir.join("CHARTER-07.telemetry.yaml"), "z").unwrap(); + let resolved = resolve_telemetry_path(&charter_path, "CHARTER-07-old"); + assert!(resolved.ends_with("CHARTER-07.telemetry.yaml")); + } +} diff --git a/cli/src/commands/charter/refresh_suggest.rs b/cli/src/commands/charter/refresh_suggest.rs index bbd1bffa..bb30c70d 100644 --- a/cli/src/commands/charter/refresh_suggest.rs +++ b/cli/src/commands/charter/refresh_suggest.rs @@ -127,13 +127,18 @@ fn load_charter_row(project_root: &Path, charter: &Charter) -> Result.md`; telemetry sits next to them as -/// `.telemetry.yaml` per the convention in charter.rs. +/// Locate the telemetry sidecar for a Charter. +/// +/// Canonical name first (`CHARTER-NN.telemetry.yaml`, what `close` writes), +/// then the legacy `.telemetry.yaml` for telemetry written by older +/// CLIs. Until cli-3.45.0 this built only the legacy name, from the Charter's +/// own file stem — so it never found anything and the heuristic reported +/// `(missing)` for every Charter in every repo (GH #416). fn telemetry_path_for(_project_root: &Path, charter: &Charter) -> Option { - let stem = charter.path.file_stem()?.to_str()?; - let parent = charter.path.parent()?; - Some(parent.join(format!("{stem}.telemetry.yaml"))) + Some(super::resolve_telemetry_path( + &charter.path, + &charter.frontmatter.charter_id, + )) } fn parse_telemetry(path: &Path) -> Result<(Option, Option)> { @@ -229,12 +234,20 @@ fn print_recommendation( window: &[&CharterRow], rolling_mean: Option, ) { + // Two distinct gates, and printing both as bare numbers read as a + // contradiction (GH #416): `3` is the sample size the mean needs to mean + // anything, `6` is the value that mean must beat. Each line now names what + // it gates. println!(" {}", "Heuristic".bold()); println!( - " Chain length (closed Charters in window): {}", - window.len() + " Sample size — closed Charters with telemetry: {} (need {} to evaluate)", + window.len(), + ROLLING_WINDOW + ); + println!( + " Refresh trigger — rolling mean must exceed: {}", + threshold ); - println!(" Threshold for rolling mean: > {}", threshold); match rolling_mean { Some(m) => println!( " Rolling mean of agent_quality.r_n_plus_one_emergent_count: {:.2}", diff --git a/cli/src/commands/followups/note.rs b/cli/src/commands/followups/note.rs index da0ec7b1..bb231c6c 100644 --- a/cli/src/commands/followups/note.rs +++ b/cli/src/commands/followups/note.rs @@ -42,14 +42,7 @@ pub fn run(path: &str, fu_id: &str, text: &str, source: Option<&str>) -> Result< // artifact gets half-corrupted. guard_parse_warnings(®istry)?; - let entry = followups::find_entry(®istry, fu_id) - .ok_or_else(|| { - anyhow!( - "Entry {} not found in the registry.\n hint: run `straymark followups list` to see entries.", - fu_id - ) - })? - .clone(); + let entry = followups::find_entry_unique(®istry, fu_id)?.clone(); let date = Local::now().format("%Y-%m-%d").to_string(); let notes = followups::append_note(entry.notes.as_deref(), text, &date, source); diff --git a/cli/src/commands/followups/promote.rs b/cli/src/commands/followups/promote.rs index 1f993545..2b55750d 100644 --- a/cli/src/commands/followups/promote.rs +++ b/cli/src/commands/followups/promote.rs @@ -37,14 +37,7 @@ pub fn run(path: &str, fu_id: &str, title: Option<&str>, premise_verified: bool) ); } let registry = followups::parse_registry(®istry_path)?; - let entry = followups::find_entry(®istry, fu_id) - .ok_or_else(|| { - anyhow!( - "Entry {} not found in the registry.\n hint: run `straymark followups list` to see entries.", - fu_id - ) - })? - .clone(); + let entry = followups::find_entry_unique(®istry, fu_id)?.clone(); if entry.status == FuStatus::Promoted { bail!( diff --git a/cli/src/commands/followups/set_status.rs b/cli/src/commands/followups/set_status.rs index 0ccb1186..749bf02d 100644 --- a/cli/src/commands/followups/set_status.rs +++ b/cli/src/commands/followups/set_status.rs @@ -54,14 +54,7 @@ pub fn run(path: &str, fu_id: &str, status_input: &str) -> Result<()> { let registry = followups::parse_registry(®istry_path)?; guard_parse_warnings(®istry)?; - let entry = followups::find_entry(®istry, fu_id) - .ok_or_else(|| { - anyhow!( - "Entry {} not found in the registry.\n hint: run `straymark followups list` to see entries.", - fu_id - ) - })? - .clone(); + let entry = followups::find_entry_unique(®istry, fu_id)?.clone(); let before = followups::compute_counters(®istry); let previous = entry.status_raw.clone().unwrap_or_else(|| entry.status.as_str().to_string()); diff --git a/cli/src/commands/followups/status.rs b/cli/src/commands/followups/status.rs index 88cc2a0f..f227eee9 100644 --- a/cli/src/commands/followups/status.rs +++ b/cli/src/commands/followups/status.rs @@ -197,12 +197,10 @@ fn print_pulse(registry: &Registry) { } fn print_entry_detail(registry: &Registry, id: &str) -> Result<()> { - let entry = followups::find_entry(registry, id).ok_or_else(|| { - anyhow!( - "Entry {} not found in the registry.\n hint: run `straymark followups list` to see entries.", - id - ) - })?; + // Read-only, but it still refuses an ambiguous id: showing one of two + // entries that share a number is how an operator concludes the wrong one is + // the one they are about to write to (GH #415). + let entry = followups::find_entry_unique(registry, id)?; println!(); println!(" {} — {}", entry.fu_id.bold().cyan(), entry.description.bold()); diff --git a/cli/src/commands/followups/verify.rs b/cli/src/commands/followups/verify.rs index de6b5844..3d56ba0f 100644 --- a/cli/src/commands/followups/verify.rs +++ b/cli/src/commands/followups/verify.rs @@ -47,14 +47,7 @@ pub fn run( ); } let registry = followups::parse_registry(®istry_path)?; - let entry = followups::find_entry(®istry, fu_id) - .ok_or_else(|| { - anyhow!( - "Entry {} not found in the registry.\n hint: run `straymark followups list` to see entries.", - fu_id - ) - })? - .clone(); + let entry = followups::find_entry_unique(®istry, fu_id)?.clone(); // ── Surface the premise (the assumption to re-check) ── let effective_premise = premise diff --git a/cli/src/followups.rs b/cli/src/followups.rs index 364feefc..b1d6ee15 100644 --- a/cli/src/followups.rs +++ b/cli/src/followups.rs @@ -20,7 +20,7 @@ //! Move target: straymark-core (Loom M0) — keep this module free of //! clap/colored/dialoguer; presentation lives in `commands/followups/`. -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context, Result, bail}; use sha2::{Digest, Sha256}; use std::collections::HashSet; use std::path::{Path, PathBuf}; @@ -512,14 +512,109 @@ pub fn find_entry<'a>(registry: &'a Registry, id_input: &str) -> Option<&'a Entr None } -/// Next sequential FU number: `max(NNN) + 1`, or 1 on an empty registry. +/// Every entry an id resolves to. Normally one; more than one means the +/// registry carries duplicate ids and any write addressed by that id is a +/// coin flip. +pub fn find_entries<'a>(registry: &'a Registry, id_input: &str) -> Vec<&'a Entry> { + let trimmed = id_input.trim(); + if trimmed.is_empty() { + return Vec::new(); + } + let exact: Vec<&Entry> = registry.entries().filter(|e| e.fu_id == trimmed).collect(); + if !exact.is_empty() { + return exact; + } + let digits = trimmed.strip_prefix("FU-").unwrap_or(trimmed); + match digits.parse::() { + Ok(n) => registry.entries().filter(|e| e.fu_number == n).collect(), + Err(_) => Vec::new(), + } +} + +/// Resolve an id to exactly one entry, or fail. +/// +/// GH #415: every mutating command resolved by taking the **first** match. +/// With duplicate ids in the registry — which parallel branches and triage +/// pruning both produce — `note` wrote into the wrong entry and `set-status` +/// answered `already closed — nothing to change`, which reads as success. The +/// operator's actual follow-up stayed open and unannotated, and nothing said so. +/// +/// Refusing is the only safe answer: the command cannot know which entry was +/// meant, and guessing is what caused the silent write. +pub fn find_entry_unique<'a>(registry: &'a Registry, id_input: &str) -> Result<&'a Entry> { + let matches = find_entries(registry, id_input); + match matches.len() { + 1 => Ok(matches[0]), + 0 => bail!( + "{} not found in {}", + id_input, + registry.path.display() + ), + n => { + let mut detail = String::new(); + for e in &matches { + detail.push_str(&format!( + "\n - {} [{}] {} — {}", + e.fu_id, + e.status.as_str(), + e.origin.as_deref().unwrap_or("origin unknown"), + crate::utils::truncate_visual(&e.description, 60) + )); + } + bail!( + "{id_input} is ambiguous: {n} entries share that id in {}.{detail}\n \ + hint: ids are positional, so a merge of parallel branches — or triage pruning a \ + closed entry's heading — can hand the same number to two follow-ups. Renumber \ + all but one to a free id (`straymark followups status` shows the highest in \ + use), then re-run. Addressing a duplicate id would write into whichever entry \ + happens to come first in the file.", + registry.path.display() + ) + } + } +} + +/// Next sequential FU number: one past the highest number the registry has +/// *ever* used, not one past the highest entry still standing. +/// +/// GH #415: the high-water mark used to come from parsed entries alone, so an +/// id stopped being reserved the moment its entry lost its `### FU-NNN` +/// heading — which is exactly what triage does when it prunes a closed entry +/// down to a provenance bullet. The next `drift --apply` then handed that +/// number to an unrelated follow-up, and the registry ended up with two +/// different items answering to one id. Reported from Sentinel on 2026-06-04 +/// (`FU-123` reused after supersession) and again in #415. +/// +/// So the scan is over the raw body, not the entry list: any `FU-NNN` mention +/// anywhere — a pruned bullet, a `Notes` back-reference, a superseded entry — +/// keeps its number retired. Numbers are cheap; a collision is not. pub fn next_fu_number(registry: &Registry) -> u32 { - registry - .entries() - .map(|e| e.fu_number) - .max() - .map(|n| n + 1) - .unwrap_or(1) + let from_entries = registry.entries().map(|e| e.fu_number).max().unwrap_or(0); + let from_body = highest_fu_mention(®istry.body); + from_entries.max(from_body) + 1 +} + +/// Highest `FU-NNN` number mentioned anywhere in the registry body, including +/// ids that no longer have an entry of their own. Author-scoped ids like +/// `FU-058-022` contribute their first group only — the registry-assigned part. +fn highest_fu_mention(body: &str) -> u32 { + let mut highest = 0u32; + let bytes = body.as_bytes(); + let mut i = 0usize; + while let Some(pos) = body[i..].find("FU-") { + let start = i + pos + 3; + let mut end = start; + while end < bytes.len() && bytes[end].is_ascii_digit() { + end += 1; + } + if end > start { + if let Ok(n) = body[start..end].parse::() { + highest = highest.max(n); + } + } + i = start.max(i + pos + 1); + } + highest } /// Counters recomputed from actual entry statuses — the CLI-owned source of diff --git a/cli/src/validation.rs b/cli/src/validation.rs index 404acba6..14cf65f4 100644 --- a/cli/src/validation.rs +++ b/cli/src/validation.rs @@ -430,6 +430,73 @@ fn check_followups_work_verb(straymark_dir: &Path, result: &mut ValidationResult } } +/// GH #415: two entries carrying the same `### FU-NNN` heading. +/// +/// Ids are positional, and two paths hand out a number that is already taken: +/// parallel branches each computing `max + 1` against their own copy of the +/// registry, and (before cli-3.45.0) triage pruning a closed entry's heading, +/// which released its number for reuse. +/// +/// The state itself is recoverable — renumber one of them — but it is +/// **invisible until something writes to the wrong entry**, which is how #415 +/// was found: a `note` landed on the wrong follow-up and the follow-up +/// `set-status` reported as "already closed" was a different item entirely. +/// The mutating commands now refuse an ambiguous id; this rule surfaces the +/// duplicate before anyone reaches for one. +/// +/// Error, not warning: unlike most registry findings this one silently +/// misdirects writes, and there is no reading of a duplicate id that is +/// intentional. +fn check_followup_duplicate_ids(straymark_dir: &Path, result: &mut ValidationResult) { + let backlog = straymark_dir.join("follow-ups-backlog.md"); + let Ok(content) = std::fs::read_to_string(&backlog) else { + return; // No registry — nothing to check. + }; + + // Heading lines only. A bare `FU-NNN` in prose or a Notes back-reference is + // a citation, not a second entry. + let mut seen: std::collections::BTreeMap> = Default::default(); + for (line_no, line) in content.lines().enumerate() { + let Some(rest) = line.strip_prefix("### ") else { + continue; + }; + let Some(after) = rest.strip_prefix("FU-") else { + continue; + }; + let digits: String = after.chars().take_while(|c| c.is_ascii_digit()).collect(); + if digits.is_empty() { + continue; + } + seen.entry(format!("FU-{digits}")) + .or_default() + .push(line_no + 1); + } + + for (fu_id, lines) in seen.iter().filter(|(_, l)| l.len() > 1) { + let where_ = lines + .iter() + .map(|l| l.to_string()) + .collect::>() + .join(", "); + result.add(ValidationIssue { + file: backlog.clone(), + rule: "FOLLOWUP-DUPLICATE-ID".to_string(), + message: format!( + "{} is the heading of {} different entries (lines {}). Commands that resolve by \ + id cannot tell them apart.", + fu_id, + lines.len(), + where_ + ), + severity: Severity::Error, + fix_hint: Some(format!( + "Renumber all but one {fu_id} to a free id, then run `straymark followups recount`. \ + `straymark followups status` shows the highest id in use." + )), + }); + } +} + /// GH #392: warn when an AILOG's body mentions a `FU-NNN` / `FU-NNN-NNN` id /// outside its own `## Follow-ups` section and *nothing* knows about that id. /// The extractor only reads `## Follow-ups` (plus structural risk @@ -637,6 +704,7 @@ pub fn validate_all(straymark_dir: &Path) -> (ValidationResult, usize) { check_followups_work_verb(straymark_dir, &mut result); // GH #392: FU ids mentioned outside `## Follow-ups` never reach the registry. + check_followup_duplicate_ids(straymark_dir, &mut result); check_followup_mentions(straymark_dir, &paths, &mut result); // REF-002: Detect orphan documents (no traceability links) diff --git a/cli/tests/duplicate_fu_ids_test.rs b/cli/tests/duplicate_fu_ids_test.rs new file mode 100644 index 00000000..8e708a8e --- /dev/null +++ b/cli/tests/duplicate_fu_ids_test.rs @@ -0,0 +1,168 @@ +//! GH #415 — duplicate FU ids in the registry. +//! +//! Two entries answering to one id is recoverable state; what was not +//! recoverable was finding out. `note` wrote into whichever entry came first +//! and `set-status` answered `already closed — nothing to change`, which reads +//! as success while the intended follow-up stayed open and unannotated. +//! +//! These tests pin the three surfaces that close that: mutating commands refuse +//! an ambiguous id, `validate` reports the duplicate, and a pruned entry's +//! number is never handed out again. + +use assert_cmd::cargo_bin_cmd; +use predicates::prelude::*; +use tempfile::TempDir; + +const REGISTRY_WITH_DUPLICATES: &str = r#"--- +schema_version: v1 +last_scan: 2026-08-06 +buckets: [ready] +fully_extracted_ailogs: [] +total_open: 2 +--- + +# Follow-ups Backlog + +## Bucket: ready + +### FU-007 — primera entrada, la que quedó cerrada +- **Origin**: AILOG-2026-08-06-003 §Follow-ups +- **Source-hash**: aaaaaaaaaaaa +- **Status**: closed + +### FU-007 — segunda entrada, distinta, que el operador quería anotar +- **Origin**: AILOG-2026-08-06-004 §Follow-ups +- **Source-hash**: bbbbbbbbbbbb +- **Status**: open +"#; + +fn project_with(registry: &str) -> TempDir { + let tmp = TempDir::new().unwrap(); + let sm = tmp.path().join(".straymark"); + std::fs::create_dir_all(&sm).unwrap(); + std::fs::write(sm.join("config.yml"), "language: en\n").unwrap(); + std::fs::write(tmp.path().join("STRAYMARK.md"), "# rules\n").unwrap(); + std::fs::write(sm.join("follow-ups-backlog.md"), registry).unwrap(); + tmp +} + +/// The exact sequence from the report: the note landed on the wrong entry and +/// `set-status` reported a no-op that read as success. +#[test] +fn mutating_commands_refuse_an_ambiguous_id() { + let tmp = project_with(REGISTRY_WITH_DUPLICATES); + let before = std::fs::read_to_string(tmp.path().join(".straymark/follow-ups-backlog.md")).unwrap(); + + for args in [ + vec!["followups", "note", "FU-007", "la medición"], + vec!["followups", "set-status", "FU-007", "closed"], + ] { + cargo_bin_cmd!("straymark") + .args(&args) + .args(["--path", tmp.path().to_str().unwrap()]) + .assert() + .failure() + .stderr(predicate::str::contains("ambiguous")) + // Both entries named, so the operator can tell which is which. + .stderr(predicate::str::contains("AILOG-2026-08-06-003")) + .stderr(predicate::str::contains("AILOG-2026-08-06-004")); + } + + let after = std::fs::read_to_string(tmp.path().join(".straymark/follow-ups-backlog.md")).unwrap(); + assert_eq!( + before, after, + "a refused command must not have written anything" + ); +} + +/// Reading is refused too: showing one of the two is how an operator concludes +/// the wrong entry is the one they are about to act on. +#[test] +fn status_detail_refuses_an_ambiguous_id() { + let tmp = project_with(REGISTRY_WITH_DUPLICATES); + cargo_bin_cmd!("straymark") + .args(["followups", "status", "FU-007", "--path"]) + .arg(tmp.path()) + .assert() + .failure() + .stderr(predicate::str::contains("ambiguous")); +} + +#[test] +fn validate_reports_duplicate_ids_as_an_error() { + let tmp = project_with(REGISTRY_WITH_DUPLICATES); + cargo_bin_cmd!("straymark") + .args(["validate"]) + .arg(tmp.path()) + .assert() + .failure() + .stdout(predicate::str::contains("FOLLOWUP-DUPLICATE-ID")) + .stdout(predicate::str::contains("FU-007")); +} + +/// A registry with unique ids must stay silent — the rule keys on headings, so +/// a `FU-NNN` cited in prose or in `Notes` is not a second entry. +#[test] +fn a_clean_registry_reports_nothing() { + let clean = REGISTRY_WITH_DUPLICATES + .replacen("### FU-007 — segunda", "### FU-008 — segunda", 1) + + "\n### FU-009 — cita a FU-007 y FU-008 en su cuerpo\n- **Status**: open\n"; + let tmp = project_with(&clean); + cargo_bin_cmd!("straymark") + .args(["validate"]) + .arg(tmp.path()) + .assert() + .stdout(predicate::str::contains("FOLLOWUP-DUPLICATE-ID").not()); +} + +/// The id of an entry triage pruned down to a provenance bullet must stay +/// retired. Reported from Sentinel on 2026-06-04, before #415 restated it. +#[test] +fn a_pruned_id_is_never_handed_out_again() { + let pruned = r#"--- +schema_version: v1 +last_scan: 2026-08-06 +buckets: [ready] +fully_extracted_ailogs: [] +total_open: 1 +--- + +# Follow-ups Backlog + +## Bucket: ready + +### FU-001 — la que sigue viva +- **Origin**: AILOG-2026-08-06-001 §Follow-ups +- **Status**: open + +## Closed at triage + +- FU-002 (closed 2026-08-06) — pruned to a provenance bullet, heading gone +"#; + let tmp = project_with(pruned); + std::fs::create_dir_all(tmp.path().join(".straymark/07-ai-audit/agent-logs")).unwrap(); + std::fs::write( + tmp.path().join(".straymark/07-ai-audit/agent-logs/AILOG-2026-08-06-009-x.md"), + "---\nid: AILOG-2026-08-06-009\ntitle: x\nstatus: accepted\ncreated: 2026-08-06\n\ + agent: claude-opus-5-v1.0\nconfidence: high\nreview_required: false\nrisk_level: low\n---\n\n\ + # AILOG: x\n\n## Summary\ns\n\n## Follow-ups\n\n- (new) una entrada nueva que no debe reusar FU-002\n", + ) + .unwrap(); + + cargo_bin_cmd!("straymark") + .args(["followups", "drift", "--apply", "--scan-all", "--path"]) + .arg(tmp.path()) + .assert() + .success(); + + let registry = + std::fs::read_to_string(tmp.path().join(".straymark/follow-ups-backlog.md")).unwrap(); + assert!( + !registry.contains("### FU-002 "), + "FU-002 was pruned, not freed — reassigning it collides with the pruned record:\n{registry}" + ); + assert!( + registry.contains("### FU-003 "), + "the new entry should take the next number past the high-water mark:\n{registry}" + ); +} diff --git a/dist/.straymark/00-governance/AGENT-RULES.md b/dist/.straymark/00-governance/AGENT-RULES.md index c97e2437..44cd0f16 100644 --- a/dist/.straymark/00-governance/AGENT-RULES.md +++ b/dist/.straymark/00-governance/AGENT-RULES.md @@ -418,4 +418,4 @@ When a project accumulates a high volume of AILOGs across multiple Charters and --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/C4-DIAGRAM-GUIDE.md b/dist/.straymark/00-governance/C4-DIAGRAM-GUIDE.md index 1fd5f146..3d88f596 100644 --- a/dist/.straymark/00-governance/C4-DIAGRAM-GUIDE.md +++ b/dist/.straymark/00-governance/C4-DIAGRAM-GUIDE.md @@ -234,4 +234,4 @@ Use a Level 1 (Context) diagram to illustrate: --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/DOCUMENTATION-POLICY.md b/dist/.straymark/00-governance/DOCUMENTATION-POLICY.md index bcf20f41..c9ef0f42 100644 --- a/dist/.straymark/00-governance/DOCUMENTATION-POLICY.md +++ b/dist/.straymark/00-governance/DOCUMENTATION-POLICY.md @@ -319,4 +319,4 @@ See also [ADR-2025-01-20-001] for architectural context. --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/FOLLOW-UPS-BACKLOG-PATTERN.md b/dist/.straymark/00-governance/FOLLOW-UPS-BACKLOG-PATTERN.md index 97c797cf..690ab972 100644 --- a/dist/.straymark/00-governance/FOLLOW-UPS-BACKLOG-PATTERN.md +++ b/dist/.straymark/00-governance/FOLLOW-UPS-BACKLOG-PATTERN.md @@ -370,4 +370,4 @@ Contributed via [issue #111](https://github.com/StrangeDaysTech/straymark/issues --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/QUICK-REFERENCE.md b/dist/.straymark/00-governance/QUICK-REFERENCE.md index 5eb4152a..c3288b43 100644 --- a/dist/.straymark/00-governance/QUICK-REFERENCE.md +++ b/dist/.straymark/00-governance/QUICK-REFERENCE.md @@ -72,7 +72,7 @@ Charters are **not** doc types — they wrap a multi-session implementation bloc | Concept | Folder | Agent Autonomy | |---------|--------|---------------| -| `Charter` | `.straymark/charters/` (declarative `NN-slug.md` + telemetry `NN-slug.telemetry.yaml`) | Scaffold via `charter new`; operator owns trigger and lifecycle transitions | +| `Charter` | `.straymark/charters/` (declarative `NN-slug.md` + telemetry `CHARTER-NN.telemetry.yaml`) | Scaffold via `charter new`; operator owns trigger and lifecycle transitions | > See section 15 of `STRAYMARK.md` and `.straymark/00-governance/SPECKIT-CHARTER-BRIDGE.md` for granularity heuristics, lifecycle, and the SpecKit ↔ Charter bridge. @@ -167,7 +167,7 @@ Mark `review_required: true` when: ├── 08-security/ ← SEC ├── 09-ai-models/ ← MCARD │ └── labeling/ ← AILABEL* -├── charters/ ← Charter (NN-slug.md + NN-slug.telemetry.yaml) +├── charters/ ← Charter (NN-slug.md + CHARTER-NN.telemetry.yaml) ├── follow-ups-backlog.md ← Follow-ups registry (FU-NNN entries, first-class since fw-4.21.0) └── templates/ ← Templates (incl. charter/ subdir + follow-ups-backlog.md) @@ -263,4 +263,4 @@ Mark `review_required: true` when: --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/i18n/es/AGENT-RULES.md b/dist/.straymark/00-governance/i18n/es/AGENT-RULES.md index 719d4592..829aba4a 100644 --- a/dist/.straymark/00-governance/i18n/es/AGENT-RULES.md +++ b/dist/.straymark/00-governance/i18n/es/AGENT-RULES.md @@ -418,4 +418,4 @@ Cuando un proyecto acumula un volumen alto de AILOGs a lo largo de múltiples Ch --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/i18n/es/C4-DIAGRAM-GUIDE.md b/dist/.straymark/00-governance/i18n/es/C4-DIAGRAM-GUIDE.md index c4487f6d..750b73f2 100644 --- a/dist/.straymark/00-governance/i18n/es/C4-DIAGRAM-GUIDE.md +++ b/dist/.straymark/00-governance/i18n/es/C4-DIAGRAM-GUIDE.md @@ -234,4 +234,4 @@ Usar un diagrama de Nivel 1 (Contexto) para ilustrar: --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/i18n/es/DOCUMENTATION-POLICY.md b/dist/.straymark/00-governance/i18n/es/DOCUMENTATION-POLICY.md index f1403751..90af2c7b 100644 --- a/dist/.straymark/00-governance/i18n/es/DOCUMENTATION-POLICY.md +++ b/dist/.straymark/00-governance/i18n/es/DOCUMENTATION-POLICY.md @@ -312,4 +312,4 @@ Ver también [ADR-2025-01-20-001] para contexto arquitectónico. --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/i18n/es/FOLLOW-UPS-BACKLOG-PATTERN.md b/dist/.straymark/00-governance/i18n/es/FOLLOW-UPS-BACKLOG-PATTERN.md index 913a8f49..04989497 100644 --- a/dist/.straymark/00-governance/i18n/es/FOLLOW-UPS-BACKLOG-PATTERN.md +++ b/dist/.straymark/00-governance/i18n/es/FOLLOW-UPS-BACKLOG-PATTERN.md @@ -356,4 +356,4 @@ Contribuido vía [issue #111](https://github.com/StrangeDaysTech/straymark/issue --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/i18n/es/QUICK-REFERENCE.md b/dist/.straymark/00-governance/i18n/es/QUICK-REFERENCE.md index 9e11f52d..05941e7f 100644 --- a/dist/.straymark/00-governance/i18n/es/QUICK-REFERENCE.md +++ b/dist/.straymark/00-governance/i18n/es/QUICK-REFERENCE.md @@ -63,7 +63,7 @@ Los Charters **no** son tipos de documento — envuelven un bloque de implementa | Concepto | Carpeta | Autonomía del Agente | |----------|---------|---------------------| -| `Charter` | `.straymark/charters/` (declarativo `NN-slug.md` + telemetría `NN-slug.telemetry.yaml`) | Andamiar vía `charter new`; el operador es dueño del trigger y de las transiciones de ciclo de vida | +| `Charter` | `.straymark/charters/` (declarativo `NN-slug.md` + telemetría `CHARTER-NN.telemetry.yaml`) | Andamiar vía `charter new`; el operador es dueño del trigger y de las transiciones de ciclo de vida | > Ver sección 15 de `STRAYMARK.md` y `.straymark/00-governance/SPECKIT-CHARTER-BRIDGE.md` para heurísticas de granularidad, ciclo de vida y el puente SpecKit ↔ Charter. @@ -155,7 +155,7 @@ Marcar `review_required: true` cuando: │ └── ethical-reviews/ ← ETH, DPIA ├── 08-security/ ← SEC ├── 09-ai-models/ ← MCARD -├── charters/ ← Charter (NN-slug.md + NN-slug.telemetry.yaml) +├── charters/ ← Charter (NN-slug.md + CHARTER-NN.telemetry.yaml) ├── follow-ups-backlog.md ← Registry de follow-ups (entradas FU-NNN, primera clase desde fw-4.21.0) └── templates/ ← Plantillas (incl. subdir charter/ + follow-ups-backlog.md) ``` @@ -235,4 +235,4 @@ Marcar `review_required: true` cuando: --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/i18n/zh-CN/AGENT-RULES.md b/dist/.straymark/00-governance/i18n/zh-CN/AGENT-RULES.md index 0df9850f..fdfd92a4 100644 --- a/dist/.straymark/00-governance/i18n/zh-CN/AGENT-RULES.md +++ b/dist/.straymark/00-governance/i18n/zh-CN/AGENT-RULES.md @@ -413,4 +413,4 @@ confidence: high | medium | low --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/i18n/zh-CN/C4-DIAGRAM-GUIDE.md b/dist/.straymark/00-governance/i18n/zh-CN/C4-DIAGRAM-GUIDE.md index 22c3b19b..dd8e0779 100644 --- a/dist/.straymark/00-governance/i18n/zh-CN/C4-DIAGRAM-GUIDE.md +++ b/dist/.straymark/00-governance/i18n/zh-CN/C4-DIAGRAM-GUIDE.md @@ -234,4 +234,4 @@ Rel(api, db, "Reads/Writes", "SQL") --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/i18n/zh-CN/DOCUMENTATION-POLICY.md b/dist/.straymark/00-governance/i18n/zh-CN/DOCUMENTATION-POLICY.md index b8953606..81f2e287 100644 --- a/dist/.straymark/00-governance/i18n/zh-CN/DOCUMENTATION-POLICY.md +++ b/dist/.straymark/00-governance/i18n/zh-CN/DOCUMENTATION-POLICY.md @@ -311,4 +311,4 @@ review_outcome: approved # approved | revisions_requested | rejec --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/i18n/zh-CN/FOLLOW-UPS-BACKLOG-PATTERN.md b/dist/.straymark/00-governance/i18n/zh-CN/FOLLOW-UPS-BACKLOG-PATTERN.md index da41f240..0f17d12d 100644 --- a/dist/.straymark/00-governance/i18n/zh-CN/FOLLOW-UPS-BACKLOG-PATTERN.md +++ b/dist/.straymark/00-governance/i18n/zh-CN/FOLLOW-UPS-BACKLOG-PATTERN.md @@ -356,4 +356,4 @@ straymark followups new --title "..." --origin "CHARTER-NN §Scope" [--bucket --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/.straymark/00-governance/i18n/zh-CN/QUICK-REFERENCE.md b/dist/.straymark/00-governance/i18n/zh-CN/QUICK-REFERENCE.md index 218ae7a3..14d645da 100644 --- a/dist/.straymark/00-governance/i18n/zh-CN/QUICK-REFERENCE.md +++ b/dist/.straymark/00-governance/i18n/zh-CN/QUICK-REFERENCE.md @@ -63,7 +63,7 @@ Charter **不是**文档类型——它包裹一个跨多个会话的实施块 | 概念 | 目录 | 代理自主权 | |------|------|-----------| -| `Charter` | `.straymark/charters/`(声明式 `NN-slug.md` + 遥测 `NN-slug.telemetry.yaml`) | 通过 `charter new` 搭建脚手架;操作者拥有 trigger 与生命周期的转换 | +| `Charter` | `.straymark/charters/`(声明式 `NN-slug.md` + 遥测 `CHARTER-NN.telemetry.yaml`) | 通过 `charter new` 搭建脚手架;操作者拥有 trigger 与生命周期的转换 | > 参见 `STRAYMARK.md` 第 15 节及 `.straymark/00-governance/SPECKIT-CHARTER-BRIDGE.md`,了解粒度启发式、生命周期与 SpecKit ↔ Charter 桥接。 @@ -155,7 +155,7 @@ risk_level: low | medium | high | critical │ └── ethical-reviews/ ← ETH, DPIA ├── 08-security/ ← SEC ├── 09-ai-models/ ← MCARD -├── charters/ ← Charter(NN-slug.md + NN-slug.telemetry.yaml) +├── charters/ ← Charter(NN-slug.md + CHARTER-NN.telemetry.yaml) ├── follow-ups-backlog.md ← Follow-ups 注册表(FU-NNN 条目,自 fw-4.21.0 起为一等公民) └── templates/ ← 模板(包括 charter/ 子目录 + follow-ups-backlog.md) ``` @@ -235,4 +235,4 @@ risk_level: low | medium | high | critical --- -*StrayMark fw-4.42.0 | [Strange Days Tech](https://strangedays.tech)* +*StrayMark fw-4.43.0 | [Strange Days Tech](https://strangedays.tech)* diff --git a/dist/STRAYMARK.md b/dist/STRAYMARK.md index 14c75317..f0462a1b 100644 --- a/dist/STRAYMARK.md +++ b/dist/STRAYMARK.md @@ -284,7 +284,7 @@ related: │ ├── charters/ ← CHARTERS — BOUNDED UNITS OF WORK │ ├── [NN-slug.md] # Declarative ex-ante scope (filename: NN-slug.md, NOT TYPE-YYYY-...) -│ └── [NN-slug.telemetry.yaml] # Post-close telemetry (created by `straymark charter close`) +│ └── [CHARTER-NN.telemetry.yaml] # Post-close telemetry (created by `straymark charter close`) │ ├── templates/ ← TEMPLATES (12 base + 4 China* + Charter) @@ -382,7 +382,7 @@ Charters are **conceptually distinct** from the 12+4 document types listed in se - They live at `.straymark/charters/NN-slug.md` (sequential prefix, not date-prefix). - Their lifecycle is `declared` → `in-progress` → `closed`, persisted in frontmatter as the source of truth. -- Telemetry sits beside them as `NN-slug.telemetry.yaml`, written by `straymark charter close`. +- Telemetry sits beside them as `CHARTER-NN.telemetry.yaml`, written by `straymark charter close`. The slug is deliberately absent so the filename survives a Charter rename. - External audits resolve to `.straymark/audits/CHARTER-NN/` and merge back into telemetry. ### When to declare a Charter diff --git a/dist/dist-manifest.yml b/dist/dist-manifest.yml index bb6ab302..05accd65 100644 --- a/dist/dist-manifest.yml +++ b/dist/dist-manifest.yml @@ -1,4 +1,4 @@ -version: "4.42.0" +version: "4.43.0" description: "StrayMark distribution manifest" repository: "https://github.com/StrangeDaysTech/straymark" diff --git a/docs/adopters/CLI-REFERENCE.md b/docs/adopters/CLI-REFERENCE.md index 8913e568..f4a47e09 100644 --- a/docs/adopters/CLI-REFERENCE.md +++ b/docs/adopters/CLI-REFERENCE.md @@ -45,8 +45,8 @@ StrayMark uses **independent version tags** for each component: | Component | Tag prefix | Example | What it includes | |-----------|-----------|---------|------------------| -| Framework | `fw-` | `fw-4.42.0` | Templates (12 types), governance docs, directives, Charter template + schema | -| CLI | `cli-` | `cli-3.44.0` | The `straymark` binary | +| Framework | `fw-` | `fw-4.43.0` | Templates (12 types), governance docs, directives, Charter template + schema | +| CLI | `cli-` | `cli-3.45.0` | The `straymark` binary | | Loom (EXPERIMENTAL) | `loom-` | `loom-0.4.2` | The `straymark-loom` visualization server, downloaded on demand by `straymark loom serve` | Framework and CLI are released independently. A framework update does not require a CLI update, and vice versa. diff --git a/docs/i18n/es/README.md b/docs/i18n/es/README.md index 327b8d8a..5299ffb8 100644 --- a/docs/i18n/es/README.md +++ b/docs/i18n/es/README.md @@ -241,8 +241,8 @@ StrayMark usa tags de versión independientes para cada componente: | Componente | Prefijo de tag | Ejemplo | Incluye | |------------|---------------|---------|---------| -| Framework | `fw-` | `fw-4.42.0` | Plantillas (12 tipos), gobernanza, directivas, plantilla + schema de Charter | -| CLI | `cli-` | `cli-3.44.0` | El binario `straymark` | +| Framework | `fw-` | `fw-4.43.0` | Plantillas (12 tipos), gobernanza, directivas, plantilla + schema de Charter | +| CLI | `cli-` | `cli-3.45.0` | El binario `straymark` | | Loom (EXPERIMENTAL) | `loom-` | `loom-0.4.2` | El servidor de visualización `straymark-loom`, descargado bajo demanda por `straymark loom serve` | Verifica las versiones instaladas con `straymark status` o `straymark about`. diff --git a/docs/i18n/es/adopters/CLI-REFERENCE.md b/docs/i18n/es/adopters/CLI-REFERENCE.md index 2b50d96e..bd042650 100644 --- a/docs/i18n/es/adopters/CLI-REFERENCE.md +++ b/docs/i18n/es/adopters/CLI-REFERENCE.md @@ -45,8 +45,8 @@ StrayMark usa **tags de versión independientes** para cada componente: | Componente | Prefijo de tag | Ejemplo | Qué incluye | |------------|---------------|---------|-------------| -| Framework | `fw-` | `fw-4.42.0` | Plantillas (12 tipos), docs de gobernanza, directivas | -| CLI | `cli-` | `cli-3.44.0` | El binario `straymark` | +| Framework | `fw-` | `fw-4.43.0` | Plantillas (12 tipos), docs de gobernanza, directivas | +| CLI | `cli-` | `cli-3.45.0` | El binario `straymark` | | Loom (EXPERIMENTAL) | `loom-` | `loom-0.4.2` | El servidor de visualización `straymark-loom`, descargado bajo demanda por `straymark loom serve` | Framework y CLI se publican de forma independiente. Una actualización del framework no requiere actualización del CLI, y viceversa. diff --git a/docs/i18n/zh-CN/README.md b/docs/i18n/zh-CN/README.md index 8aea7a1b..dd633d7f 100644 --- a/docs/i18n/zh-CN/README.md +++ b/docs/i18n/zh-CN/README.md @@ -259,8 +259,8 @@ StrayMark 为每个组件使用独立的版本标签: | 组件 | 标签前缀 | 示例 | 包含内容 | |------|----------|------|----------| -| Framework | `fw-` | `fw-4.42.0` | 模板(12 种类型)、治理文档、指令、Charter 模板 + schema | -| CLI | `cli-` | `cli-3.44.0` | `straymark` 二进制文件 | +| Framework | `fw-` | `fw-4.43.0` | 模板(12 种类型)、治理文档、指令、Charter 模板 + schema | +| CLI | `cli-` | `cli-3.45.0` | `straymark` 二进制文件 | | Loom(实验性) | `loom-` | `loom-0.4.2` | `straymark-loom` 可视化服务器,由 `straymark loom serve` 按需下载 | 使用 `straymark status` 或 `straymark about` 查看已安装的版本。 diff --git a/docs/i18n/zh-CN/adopters/CLI-REFERENCE.md b/docs/i18n/zh-CN/adopters/CLI-REFERENCE.md index cee59d10..6d4f5af1 100644 --- a/docs/i18n/zh-CN/adopters/CLI-REFERENCE.md +++ b/docs/i18n/zh-CN/adopters/CLI-REFERENCE.md @@ -45,8 +45,8 @@ StrayMark 为每个组件使用**独立的版本标签**: | 组件 | 标签前缀 | 示例 | 包含内容 | |------|----------|------|----------| -| Framework | `fw-` | `fw-4.42.0` | 模板(12 种类型)、治理文档、指令 | -| CLI | `cli-` | `cli-3.44.0` | `straymark` 二进制文件 | +| Framework | `fw-` | `fw-4.43.0` | 模板(12 种类型)、治理文档、指令 | +| CLI | `cli-` | `cli-3.45.0` | `straymark` 二进制文件 | | Loom(实验性) | `loom-` | `loom-0.4.2` | `straymark-loom` 可视化服务器,由 `straymark loom serve` 按需下载 | Framework 和 CLI 独立发布。Framework 更新不需要 CLI 更新,反之亦然。