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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

**[Try the live demo →](https://peopleworks.github.io/SignsofAI/)** — English & Spanish, runs in your browser. No signup, and the analysis uploads nothing.

**[Use it in your editor →](https://peopleworks.github.io/SignsofAI/skill)** — an agent skill for
Claude Code, Codex, Gemini CLI and Cursor, in one line: `npx skills add peopleworks/SignsofAI -g`.
It edits by the same rules this engine scores by, and it never invents a number.

**[Download the Windows app →](https://github.com/peopleworks/SignsofAI/releases?q=desktop&expanded=true)** — the same tool in a window. Nothing to install alongside it: the .NET runtime is bundled.

![Signs of AI Writing analyzing text live: the score climbs as AI tells accumulate, then every tell is highlighted with a fix](Docs/screenshots/analyze-live.gif)
Expand Down Expand Up @@ -232,6 +236,9 @@ already generated — copy it into `.vscode/mcp.json` and you're done.

## 5. Use it as an agent skill — `/signs-of-ai`

**[The skill has its own page →](https://peopleworks.github.io/SignsofAI/skill)**, with the two
modes side by side and what it refuses to do.

Prefer to work inside your editor? [`SKILL.md`](SKILL.md) is a drop-in **agent skill** that de-slops a
draft — or reports the tells a text carries — in **English and Spanish**. It is a human-readable
distillation of the same `rules.en.json` / `rules.es.json` taxonomy, so it edits by the same rules the
Expand Down
4 changes: 4 additions & 0 deletions src/SignsOfAI.UI/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
@* No host condition, unlike the two below: somebody using the desktop app writes in
Word too, and the add-in is useful to them for the same reasons. *@
<NavLink class="nav-link" href="word-addin">@L["nav.word"]</NavLink>
@* Same reasoning as the Word link: every host can use it, because it runs in the
reader's editor rather than in this one. It had no link at all until #98, which is
how the one channel people install by choice stayed the hardest one to find. *@
<NavLink class="nav-link" href="skill">@L["nav.skill"]</NavLink>
@* Only where there is something to gain by downloading it. The same signal as the
folder link above, and for the same reason: this host cannot open a folder, so a
better host exists for this machine and hiding that would be the lowest-common-
Expand Down
85 changes: 85 additions & 0 deletions src/SignsOfAI.UI/Pages/AgentSkill.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
@page "/skill"
@using SignsOfAI.Core.Rules
@inherits LocalizedComponent

@*
The agent skill's own page, the way the desktop app and the Word add-in have one.

It had none, and the repository mentioned it at section 5, past two hundred lines of badges for
other people's registries. That is backwards: of every channel this project ships to, the skill
is the only one a person installs by choice rather than a machine indexes, and it was the only
one with nowhere to land.

The rule counts are read from the packs the build ships, never typed. Everything this project
has typed about itself has gone stale at least once.
*@

<PageTitle>@L["skill.pagetitle"]</PageTitle>

<header class="hero">
<h1>@L["skill.h1"]</h1>
<p class="tagline">@L["skill.tagline"]</p>
</header>

@* Installing first: one line, because that is the whole argument against scrolling. *@
<section class="card">
<h2 class="dl-h2"><Icon Name="download" /> @L["skill.install.title"]</h2>
<p class="hint">@L["skill.install.lede"]</p>

<h3 class="wa-h3">@L["skill.install.any.title"]</h3>
<pre class="skill-cmd"><code>npx skills add peopleworks/SignsofAI -g</code></pre>
<p>@L.M("skill.install.any.body")</p>

<h3 class="wa-h3">@L["skill.install.plugin.title"]</h3>
<pre class="skill-cmd"><code>/plugin marketplace add peopleworks/SignsofAI
/plugin install signs-of-ai</code></pre>
<p>@L.M("skill.install.plugin.body")</p>
</section>

<section class="card">
<h2 class="dl-h2"><Icon Name="wand" /> @L["skill.use.title"]</h2>
<pre class="skill-cmd"><code>/signs-of-ai @L["skill.use.edit.arg"]
/signs-of-ai @L["skill.use.examine.arg"]</code></pre>
<ul class="dl-grid">
<li class="dl-item">
<h3>@L["skill.use.edit.name"]</h3>
<p>@L["skill.use.edit.what"]</p>
</li>
<li class="dl-item">
<h3>@L["skill.use.examine.name"]</h3>
<p>@L["skill.use.examine.what"]</p>
</li>
</ul>
</section>

@* The differentiator, and the reason this is not just another de-slop prompt: the two things it is
built to refuse. A reader deciding whether to let a tool near coursework is deciding about these. *@
<section class="card">
<h2 class="dl-h2"><Icon Name="lock" /> @L["skill.refuses.title"]</h2>
<p>@L.M("skill.refuses.score")</p>
<p>@L.M("skill.refuses.author")</p>
</section>

<section class="card">
<h2 class="dl-h2"><Icon Name="layers" /> @L["skill.rules.title"]</h2>
<p>@L.M("skill.rules.body", EnglishRules, SpanishRules)</p>
<div class="cta">
<a class="primary" href="@SkillUrl" target="_blank" rel="noopener">@L["skill.cta.read"]</a>
<a href="catalog">@L["skill.cta.catalog"]</a>
</div>
</section>

@code {
private const string SkillUrl = "https://github.com/peopleworks/SignsofAI/blob/main/SKILL.md";

// Counted from the packs this build carries. The number in a sentence about how many rules there
// are is exactly the kind that outlives its truth.
private static int Count(string language)
{
var pack = RulePackLoader.Load(language);
return pack.Lexical.Length + pack.Patterns.Length;
}

private static int EnglishRules => Count("en");
private static int SpanishRules => Count("es");
}
11 changes: 11 additions & 0 deletions src/SignsOfAI.UI/wwwroot/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,17 @@ button.ghost.sm { padding: .35rem .7rem; font-size: .82rem; }
a sub-heading inside a card, and a pulled quote for the sentence the pane actually prints. */
.wa-h3 { font-size: .94rem; font-weight: 650; margin: 1.1rem 0 .4rem; }

/* The one command that installs the skill. It is the whole argument of that page, so it is set as a
block a reader can select in one gesture rather than as inline code inside a sentence. Wraps
rather than scrolls: a command you cannot see the end of is a command you retype wrong. */
.skill-cmd {
margin: .5rem 0 .8rem; padding: .7rem .85rem;
background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px;
font-size: .86rem; line-height: 1.55;
white-space: pre-wrap; overflow-wrap: anywhere;
}
.skill-cmd code { background: none; padding: 0; font: inherit; }

/* A row of links where one of them is the action. why.html has a .cta of its own — it is a
standalone page with its own stylesheet — and copying the markup here without the style is
exactly how this shipped as two bare links sitting against each other. */
Expand Down
26 changes: 25 additions & 1 deletion src/SignsOfAI.UI/wwwroot/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -482,5 +482,29 @@
"home.report.title": "Writing analysis report",
"batch.report.hint": "Save the scan as a document you can keep or hand to a colleague. It names your students' files.",
"batch.report.title": "Folder scan — reading order",
"dl.word.page": "How it works, and how to install it"
"dl.word.page": "How it works, and how to install it",
"nav.skill": "In your editor",
"skill.pagetitle": "The agent skill — Signs of AI Writing",
"skill.h1": "Use it where you already write",
"skill.tagline": "One line installs it into Claude Code, Codex, Gemini CLI or Cursor. It edits by the same rules this engine scores by, in English and Spanish.",
"skill.install.title": "Installing it",
"skill.install.lede": "Nothing to download and nothing to run. The skill is a document your agent reads.",
"skill.install.any.title": "Any agent that supports skills",
"skill.install.any.body": "Works in <strong>Claude Code</strong>, <strong>Codex</strong>, <strong>Gemini CLI</strong>, <strong>Cursor</strong> and the rest. The <code>-g</code> installs it once for every project.",
"skill.install.plugin.title": "Or as a Claude Code plugin",
"skill.install.plugin.body": "From the marketplace manifest in the repository itself, so there is no third party between you and the source.",
"skill.use.title": "Two modes, and they are not the same job",
"skill.use.edit.arg": "<your draft>",
"skill.use.examine.arg": "is this AI slop? <the text>",
"skill.use.edit.name": "Edit",
"skill.use.edit.what": "Rewrites the draft and tells you what it changed, keeping the voice it found.",
"skill.use.examine.name": "Examine",
"skill.use.examine.what": "Quotes the tells the text carries and changes nothing.",
"skill.refuses.title": "The two things it will not do",
"skill.refuses.score": "<strong>It never invents a number.</strong> A skill reading a document cannot measure a false-positive rate, so it does not pretend to. For a calibrated score it hands off to this engine — the app you are reading, the command line, or the MCP server.",
"skill.refuses.author": "<strong>It never says who wrote a text.</strong> No tool can, this one included, and a skill that implied otherwise would be the thing this project argues against.",
"skill.rules.title": "The same taxonomy, not a second opinion",
"skill.rules.body": "It is a human-readable distillation of the rule packs this build ships — <strong>{0}</strong> rules in English and <strong>{1}</strong> in Spanish — so what it edits and what the engine scores are the same catalogue. The Spanish pack is written from scratch, not machine-translated.",
"skill.cta.read": "Read the skill",
"skill.cta.catalog": "Browse the rules"
}
26 changes: 25 additions & 1 deletion src/SignsOfAI.UI/wwwroot/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -482,5 +482,29 @@
"home.report.title": "Reporte de análisis de escritura",
"batch.report.hint": "Guarda el análisis como un documento para conservar o pasar a un colega. Nombra los archivos de tus estudiantes.",
"batch.report.title": "Análisis de carpeta — orden de lectura",
"dl.word.page": "Cómo funciona y cómo se instala"
"dl.word.page": "Cómo funciona y cómo se instala",
"nav.skill": "En tu editor",
"skill.pagetitle": "El skill para agentes — Señales de escritura IA",
"skill.h1": "Úsalo donde ya escribes",
"skill.tagline": "Una línea lo instala en Claude Code, Codex, Gemini CLI o Cursor. Edita con las mismas reglas con las que este motor puntúa, en español e inglés.",
"skill.install.title": "Cómo se instala",
"skill.install.lede": "Nada que descargar y nada que ejecutar. El skill es un documento que lee tu agente.",
"skill.install.any.title": "Cualquier agente que admita skills",
"skill.install.any.body": "Funciona en <strong>Claude Code</strong>, <strong>Codex</strong>, <strong>Gemini CLI</strong>, <strong>Cursor</strong> y los demás. La <code>-g</code> lo instala una vez para todos los proyectos.",
"skill.install.plugin.title": "O como plugin de Claude Code",
"skill.install.plugin.body": "Desde el manifiesto de marketplace del propio repositorio, así que no hay terceros entre tú y la fuente.",
"skill.use.title": "Dos modos, y no son el mismo trabajo",
"skill.use.edit.arg": "<tu borrador>",
"skill.use.examine.arg": "¿esto es slop de IA? <el texto>",
"skill.use.edit.name": "Editar",
"skill.use.edit.what": "Reescribe el borrador y te dice qué cambió, conservando la voz que encontró.",
"skill.use.examine.name": "Examinar",
"skill.use.examine.what": "Cita las señales que lleva el texto y no cambia nada.",
"skill.refuses.title": "Las dos cosas que no hará",
"skill.refuses.score": "<strong>Nunca se inventa un número.</strong> Un skill que lee un documento no puede medir una tasa de falsos positivos, así que no finge hacerlo. Para una puntuación calibrada pasa el trabajo a este motor: la aplicación que estás leyendo, la línea de comandos o el servidor MCP.",
"skill.refuses.author": "<strong>Nunca dice quién escribió un texto.</strong> Ninguna herramienta puede, esta incluida, y un skill que insinuara lo contrario sería justo aquello contra lo que argumenta este proyecto.",
"skill.rules.title": "La misma taxonomía, no una segunda opinión",
"skill.rules.body": "Es una destilación legible de los packs de reglas que trae esta compilación —<strong>{0}</strong> reglas en inglés y <strong>{1}</strong> en español—, así que lo que edita y lo que el motor puntúa son el mismo catálogo. El pack español está escrito desde cero, no traducido a máquina.",
"skill.cta.read": "Leer el skill",
"skill.cta.catalog": "Ver las reglas"
}
32 changes: 32 additions & 0 deletions tests/SignsOfAI.Core.Tests/LocaleFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,38 @@ public void No_locale_tells_the_reader_a_person_wrote_the_text()
"verdict.none. Found:\n " + string.Join("\n ", problems));
}

/// <summary>
/// Being current is not the same as being complete, and only the first was guarded.
///
/// This project claims the interface is bilingual, not that Spanish is a courtesy translation
/// kept roughly up to date. A missing key renders in English inside an otherwise Spanish page,
/// which is the same defect #77 found in the evidence report: three tests watched report.es.json
/// and none of them noticed it carried 39 of 76 blocks.
///
/// The line is the manifest's own credit. A locale this project puts its own name to has to be
/// complete; a locale contributed by somebody else may be partial, because demanding all 500
/// keys from a first pull request would turn a partial translation into no translation.
/// </summary>
[Fact]
public void A_locale_this_project_credits_itself_for_is_complete()
{
var manifest = ReadManifest();
var fallback = ReadLocale(manifest.Fallback);

foreach (var locale in manifest.Locales.Where(l =>
l.Code != manifest.Fallback &&
string.Equals(l.Credit, "PeopleWorks", StringComparison.OrdinalIgnoreCase)))
{
var strings = ReadLocale(locale.Code);
var missing = fallback.Keys.Where(k => !strings.ContainsKey(k)).OrderBy(k => k).ToList();

Assert.True(missing.Count == 0,
$"{locale.Code}.json is missing {missing.Count} of {fallback.Count} keys, so those "
+ $"strings appear in {manifest.Fallback} inside a {locale.Code} page: "
+ string.Join(", ", missing.Take(20)));
}
}

[Fact]
public void Translations_use_only_known_keys()
{
Expand Down
Loading