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
43 changes: 43 additions & 0 deletions .github/workflows/dist-hygiene.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: dist hygiene

# Auto-adoption safeguard S6: the framework distribution source (dist/.straymark)
# ships templates only. A dated governance artifact appearing here means a
# `straymark` command (or an agent) wrote into dist/ instead of an installed
# project — the exact pollution that safeguard S1 (the distribution-source
# guard in resolve_project_root) exists to prevent. This is the CI backstop.

on:
pull_request:
paths:
- 'dist/**'
- '.github/workflows/dist-hygiene.yml'
push:
branches: [main]
paths:
- 'dist/**'
- '.github/workflows/dist-hygiene.yml'

permissions:
contents: read

jobs:
no-artifacts-in-distribution:
name: No generated artifacts in dist/.straymark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Fail if dated governance artifacts polluted the distribution source
run: |
hits=$(find dist/.straymark \
\( -name 'AILOG-2*.md' -o -name 'AIDEC-2*.md' -o -name 'ADR-2*.md' \
-o -name '*.telemetry.yaml' -o -name 'CHARTER-[0-9]*.md' \) \
2>/dev/null || true)
if [ -n "$hits" ]; then
echo "::error::Generated governance artifacts found inside the framework distribution source (dist/.straymark/):"
echo "$hits"
echo ""
echo "These belong in an INSTALLED project's .straymark/, never in the shipped template."
echo "A command likely resolved dist/ as a project. See auto-adoption safeguards S1 (source guard) / S6 (this check)."
exit 1
fi
echo "OK — no generated artifacts in dist/.straymark/ (templates only)."
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project uses [independent versioning](README.md#versioning) for Framewo

---

## CLI 3.34.0 — 2026-07-13

### Added (CLI)

- **Auto-adoption safeguard: refuse to operate on the framework distribution source.**
`resolve_project_root` now skips a `.straymark/` that sits next to a `dist-manifest.yml`
(i.e. StrayMark's own `dist/`), so a command run with cwd or `--path` inside `dist/` can no
longer resolve the shipped template as an installed project — it falls through to the real
git-root install or reports "not installed", printing a `note:` explaining the skip. This
is the mechanical guard (safeguard **S1**) that must exist before StrayMark self-adopts:
without it, `straymark new`/`ailog`/`validate` pointed at `dist/` would read and write into
the distribution. Pure detection helper `utils::is_distribution_source`.
- **CI hygiene backstop (safeguard S6):** a `dist-hygiene` workflow fails a PR/push if any
dated governance artifact (`AILOG-2*`, `AIDEC-2*`, `ADR-2*`, `*.telemetry.yaml`,
`CHARTER-<n>*`) appears under `dist/.straymark/` — the pollution S1 prevents, caught in CI
if it ever slips through.

---

## Framework 4.35.0 / CLI 3.33.0 — 2026-07-13

### Added (Framework)
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ StrayMark uses independent version tags for each component:
| Component | Tag prefix | Example | Includes |
| --- | --- | --- | --- |
| Framework | `fw-` | `fw-4.35.0` | Templates (12 types), governance, directives, Charter template + schema |
| CLI | `cli-` | `cli-3.33.0` | The `straymark` binary |
| CLI | `cli-` | `cli-3.34.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`.
Expand Down
2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "straymark-cli"
version = "3.33.0"
version = "3.34.0"
edition = "2021"
description = "CLI for StrayMark — the cognitive discipline your AI-assisted projects need"
license = "MIT"
Expand Down
87 changes: 78 additions & 9 deletions cli/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,21 @@ pub fn resolve_project_root(path: &str) -> Option<ResolvedPath> {
.canonicalize()
.unwrap_or_else(|_| std::path::PathBuf::from(path));

// Check the given path first
// Check the given path first. Never treat the framework DISTRIBUTION SOURCE
// (`dist/.straymark`, identified by a sibling `dist-manifest.yml`) as an
// installed project — operating on it would mutate the shipped template
// (write AILOGs/AIDECs into `dist/`, validate the source as if it were a
// project, …). Skip it so resolution falls through to the real install
// (git root) or reports not-installed. (Auto-adoption safeguard S1.)
if target.join(".straymark").exists() {
return Some(ResolvedPath {
path: target,
is_fallback: false,
});
if is_distribution_source(&target) {
warn_distribution_source_skipped(&target);
} else {
return Some(ResolvedPath {
path: target,
is_fallback: false,
});
}
}

// Try git repo root
Expand All @@ -94,16 +103,40 @@ pub fn resolve_project_root(path: &str) -> Option<ResolvedPath> {
if let Some(root) = git_root {
// Don't fallback to the same path we already checked
if root != target && root.join(".straymark").exists() {
return Some(ResolvedPath {
path: root,
is_fallback: true,
});
if is_distribution_source(&root) {
warn_distribution_source_skipped(&root);
} else {
return Some(ResolvedPath {
path: root,
is_fallback: true,
});
}
}
}

None
}

/// True when `dir` holds the framework **distribution source** rather than an
/// installed project: its `.straymark/` sits next to a `dist-manifest.yml`. That
/// manifest is unique to StrayMark's own `dist/` — no adopter project ships one
/// — so it is a precise, cheap signal. Used to refuse operating on the shipped
/// template (auto-adoption safeguard S1; the catastrophic path is running a
/// mutating command with cwd/`--path` inside `dist/`).
pub fn is_distribution_source(dir: &Path) -> bool {
dir.join("dist-manifest.yml").exists()
}

fn warn_distribution_source_skipped(dir: &Path) {
eprintln!(
"{} skipping {} — this is the framework distribution source (a sibling \
`dist-manifest.yml` marks it), not an installed StrayMark project. Run from \
the project root, or `straymark init` to install.",
"note:".yellow().bold(),
dir.join(".straymark").display()
);
}

/// Resolve `<dir>/<filename>` honoring an optional translation under
/// `<dir>/i18n/<lang>/<filename>`. When `lang` is `"en"` (or any value where
/// the localized variant is absent), returns the root path unchanged. This is
Expand Down Expand Up @@ -182,6 +215,42 @@ pub fn pad_right_visual(s: &str, cols: usize) -> String {
mod tests {
use super::*;

// ── Auto-adoption safeguard S1: distribution-source guard ─────────────

#[test]
fn is_distribution_source_detects_dist_manifest_sibling() {
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path();
assert!(!is_distribution_source(dir));
std::fs::write(dir.join("dist-manifest.yml"), "version: \"1.0.0\"\n").unwrap();
assert!(is_distribution_source(dir));
}

#[test]
fn resolve_project_root_refuses_distribution_source() {
// A `dist/.straymark` with a sibling `dist-manifest.yml` must NEVER
// resolve as a project (it is the shipped template). With no real
// install to fall back to, resolution is None — not `dist/`.
let tmp = tempfile::TempDir::new().unwrap();
let dist = tmp.path().join("dist");
std::fs::create_dir_all(dist.join(".straymark")).unwrap();
std::fs::write(dist.join("dist-manifest.yml"), "version: \"1.0.0\"\n").unwrap();
assert!(
resolve_project_root(dist.to_str().unwrap()).is_none(),
"distribution source must never resolve as an installed project"
);
}

#[test]
fn resolve_project_root_accepts_normal_install() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::create_dir_all(tmp.path().join(".straymark")).unwrap();
let resolved = resolve_project_root(tmp.path().to_str().unwrap())
.expect("a plain .straymark/ (no dist-manifest) must resolve");
assert!(!resolved.is_fallback);
assert!(resolved.path.join(".straymark").exists());
}

#[test]
fn visual_width_ascii() {
assert_eq!(visual_width("hello"), 5);
Expand Down
2 changes: 1 addition & 1 deletion docs/adopters/CLI-REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ StrayMark uses **independent version tags** for each component:
| Component | Tag prefix | Example | What it includes |
|-----------|-----------|---------|------------------|
| Framework | `fw-` | `fw-4.35.0` | Templates (12 types), governance docs, directives, Charter template + schema |
| CLI | `cli-` | `cli-3.33.0` | The `straymark` binary |
| CLI | `cli-` | `cli-3.34.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.
Expand Down
163 changes: 163 additions & 0 deletions docs/decisions/AIDEC-2026-07-13-001-straymark-self-adoption.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
---
id: AIDEC-2026-07-13-001
title: StrayMark adopts StrayMark — scoped, lagged self-adoption with a distribution-source guard
status: accepted
created: 2026-07-13
agent: claude-opus-4-8
confidence: high
review_required: true

# --- Approval workflow (fill at review time via `straymark approve`) ---
# reviewed_by: <reviewer-id>
# reviewed_at: YYYY-MM-DD
# review_outcome: approved
risk_level: medium
eu_ai_act_risk: not_applicable
nist_genai_risks: []
iso_42001_clause: []
tags: [self-adoption, dogfooding, governance, control-center, ouroboros]
related: [ADR-2026-06-03-followups-first-class]
---

# AIDEC: StrayMark adopts StrayMark — scoped, lagged self-adoption

## Context

StrayMark is a framework for AI-assisted software governance (Charters, AILOGs, AIDECs,
telemetry, the follow-ups registry, the external-audit cycle, the architecture model/Loom).
StrayMark itself is developed by AI agents. The natural question — the "ouroboros" — is whether
StrayMark should adopt StrayMark for its own development, especially as the implementation load
grows (the continuity of the Baton/Loom experiments and a forthcoming "Control Center").

Two facts frame the decision:

1. **The ouroboros already exists, half of it.** The experiments already practice the *discipline*
by hand: `experiment-baton/` has 3 Charters + 9 AILOGs, `experiment-loom/` has 2 Charters + 10
AILOGs — authored spontaneously, without an operator instruction or a discussed decision. What is
missing is the *tooling half*: an installed `.straymark/`, the CLI run against this repo, and the
full governance loop. `CLAUDE.md` already mirrors `STRAYMARK.md`'s git rules.

2. **A latent catastrophic ambiguity is mechanical and exists today.** `resolve_project_root` picks
the closest `.straymark/`, and `dist/.straymark/` (the shipped framework *source*) is a valid one.
So running any mutating command with cwd or `--path` inside `dist/` treats the distribution
template as an installed project — writing AILOGs/AIDECs into `dist/`, validating the source as a
project, reading governance from the source instead of an install. (Not yet realized:
`dist/.straymark/07-ai-audit/` currently holds only `.gitkeep`.)

## Problem

Should StrayMark self-adopt, and if so, how — without the catastrophic confusion between the
framework **distribution source** (`dist/.straymark/`) and an **installed framework** (`/.straymark/`),
and without the framework's in-development state breaking its own development loop?

## Alternatives Considered

### Alternative 1: Full, live ouroboros

**Description**: Install `.straymark/` at the repo root pointed at the *live* `dist/` framework, and
gate development on self-validation (CI fails on `straymark validate`, etc.). The snake eats today's tail.

**Pros**:
- Maximum dogfooding; every framework change is exercised on the framework immediately.
- Tightest possible feedback loop.

**Cons**:
- **Bootstrap paradox / version-skew (catastrophic):** editing `dist/.straymark/…AGENT-RULES.md` while
a self-install validates against those half-built rules means a bad framework commit can *brick the
maintainer's own development loop* — the tool can't develop itself when the in-progress version fails
on itself.
- **Meta-noise / hall of mirrors:** AILOGs about editing AILOG templates, telemetry about the telemetry
schema — the human loses orientation, precisely what StrayMark exists to prevent (and what the OKF
analysis identified as StrayMark's core differentiator to protect).
- Double bookkeeping of framework files that duplicate the source.

### Alternative 2: Scoped, lagged self-adoption (with safeguards first)

**Description**: Install `.straymark/` at the root **pinned to the last released framework** (not live
`dist/`) — the snake eats *yesterday's* tail. Scope the loop to the high-benefit / low-coupling governance
layer (Charters + AILOGs + follow-ups registry + architecture model/Loom) applied to **new heavy work**
(the experiments' continuity and especially the Control Center). Keep validation **advisory, not a gate**.
Exclude framework-meta-work from the loop. Preserve Sentinel/lnxdrive as the N=2 stabilization gate.
Ship the distribution-source guard (and companions) **before** any `straymark init`.

**Pros**:
- Tight feedback loop *without* the bricking hazard — the last stable release governs while the next is
developed.
- The Control Center is a genuine, non-trivial internal dogfood target for the Charter/Loom/Baton machinery.
- First-hand contact with adopter friction the project currently receives second-hand from external adopters.

**Cons**:
- Double maintenance (an installed `.straymark/` to keep valid on top of authoring the framework).
- Requires new safeguards (a distribution-source guard, provenance sentinel) before it is safe.
- A one-version lag between what governs and what ships.

### Alternative 3: No self-adoption

**Description**: Keep developing StrayMark without installing it on itself; rely solely on external
adopters (Sentinel, lnxdrive) for validation.

**Pros**:
- Zero bootstrap/ambiguity risk; no double bookkeeping.
- External adopters carry a blind-spot advantage — they surface what the maintainer cannot see from inside.

**Cons**:
- Loses the tight, first-hand feedback loop; every field report (e.g. #345/#346/#350) stays second-hand.
- Leaves the spontaneous, uncontrolled half-ouroboros (loose artifacts scattered in experiment dirs, agents
reading rules from the *source*) unmanaged.

## Decision

**Chosen**: Alternative 2 — scoped, lagged self-adoption, safeguards first.

**Justification**: The ouroboros hazard is not "adopting"; it is "adopting *live*." Eating *yesterday's*
tail (pinning to the last release) closes the feedback loop while the framework can still evolve safely.
The mechanical ambiguity that makes even a lagged adoption dangerous (the `dist/` vs installed confusion)
is closable with a precise, cheap guard, which becomes the non-negotiable prerequisite. Self-adoption
**complements, never replaces**, the external N=2 adopter gate: the blind-spot advantage of Sentinel/lnxdrive
is preserved as additive, not substituted.

## Consequences

### Positive
- First-hand feedback on adopter friction, with full context, faster than second-hand field reports.
- The Control Center becomes a real dogfood of the architecture/Loom/Baton machinery on an internal target.
- The spontaneous discipline (already happening) gains a canonical home instead of scattering.

### Negative
- Double bookkeeping: an installed `.straymark/` to keep valid alongside authoring the framework.
- A one-version governance lag (governs with the last release while the next is in `dist/`).

### Risks
- **R1/R2 — operating on / writing artifacts into `dist/`** (catastrophic): mitigated by **S1** (the
distribution-source guard) + **S6** (CI hygiene backstop). Landed in PR #358.
- **R3/R4 — reading context from both frameworks / invisible version-skew**: mitigated by **S3** (skew
visibility in `status`) + **S4** (agent directive: `/.straymark/` = governance-in-force, `/dist/.straymark/`
= product-under-edit).
- **R5 — a test fixture resolved as a project**: covered by **S2** (`role: test-fixture` sentinel).
- **R6 — divergent duplicate framework files in git**: **S5** (version artifacts; gitignore the pinned
framework-file copies).
- **R7 — meta-noise / hall of mirrors**: bounded by scope (govern *product*, not framework-meta-work).

## Implementation

Sequenced so the guard exists before the install (full detail in the accompanying implementation plan):

1. **S1 — distribution-source guard** + **S6 — CI hygiene backstop**. *Done — PR #358 (`cli-3.34.0`).*
2. **S2 — provenance sentinel**: `straymark init` writes `role: installed-project`; the shipped `dist/`
carries `role: distribution-source`; commands verify and refuse non-install roles, **tolerating an absent
sentinel** (legacy adopters must not break).
3. **S3/S4/S5** — skew visibility, the agent directive, and the git strategy for the installed instance.
4. **Gate:** no `straymark init` at the repo root until S1 **and** S2 exist.
5. Then `straymark init` at the root, **pinned to the last released framework**, with the Control Center as
the first pilot. Existing spontaneous artifacts stay as a pre-adoption historical record (not migrated).

## References

- Working analyses (local): `analisis-autoadopcion.md`, `spike-b-autoadopcion-riesgos.md`, `PLAN-centro-de-control.md`
- PR #358 — S1 distribution-source guard + S6 CI hygiene
- Related: verification-fidelity (#306), the close-time review checkpoint (#350), [ADR-2026-06-03-followups-first-class](ADR-2026-06-03-followups-first-class.md)
- Design principle #12 (N=2 stabilization gate)

---

<!-- Template: StrayMark | https://strangedays.tech -->
2 changes: 1 addition & 1 deletion docs/i18n/es/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ StrayMark usa tags de versión independientes para cada componente:
| Componente | Prefijo de tag | Ejemplo | Incluye |
|------------|---------------|---------|---------|
| Framework | `fw-` | `fw-4.35.0` | Plantillas (12 tipos), gobernanza, directivas, plantilla + schema de Charter |
| CLI | `cli-` | `cli-3.33.0` | El binario `straymark` |
| CLI | `cli-` | `cli-3.34.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`.
Expand Down
Loading