@akinet/akidevrule 3.4.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +36 -30
- package/claude/CLAUDE.md +2 -33
- package/claude/agents/aki-hands.md +2 -2
- package/claude/hooks/aki-route-guard.mjs +145 -0
- package/claude/hooks/aki_version_check.mjs +2 -2
- package/docs/ref/{macos-codesign-tcc.md → fact-macos-codesign-tcc.md} +3 -1
- package/install.mjs +42 -28
- package/lib/permissions.mjs +1 -1
- package/package.json +2 -2
- package/payload/GEMINI.md +2 -2
- package/payload/RULE-agent-behavior.md +23 -24
- package/payload/RULE-coding.md +19 -29
- package/payload/RULE-content-write.md +3 -3
- package/payload/RULE-docs.md +17 -7
- package/payload/RULE-pattern-core.md +5 -3
- package/payload/RULE-release.md +37 -17
- package/payload/RULE-seo.md +15 -13
- package/payload/RULE-stack-akiNuxtCf.md +1 -0
- package/payload/RULE-stack-tauri.md +1 -1
- package/payload/RULE-ui-pattern.md +1 -0
- package/skills/aki-article-writer/SKILL.md +7 -7
- package/skills/aki-article-writer/references/article-workflow.md +11 -15
- package/skills/akiflow/SKILL.md +1 -1
- package/skills/akiflow/scripts/release_lint.py +159 -0
- package/skills/akihelp/SKILL.md +3 -3
- package/skills/akiopen/SKILL.md +38 -0
- package/skills/akirule/SKILL.md +28 -24
- package/skills/akiship/SKILL.md +4 -1
- package/payload/index.md +0 -95
package/payload/RULE-release.md
CHANGED
|
@@ -106,11 +106,7 @@ After updating CHANGELOG and the version bump, produce the GitHub Release withou
|
|
|
106
106
|
- **Otherwise** (no `gh`, or the user will publish manually) → output the copy-ready block below instead.
|
|
107
107
|
- Before minting, cross-check tags against Releases (`gh release list` vs `git tag`) and offer to backfill any tag that has no matching Release, so the Releases page has no gaps.
|
|
108
108
|
|
|
109
|
-
**Title:** `v{version}: {
|
|
110
|
-
- Good: `v1.5.1: fix production icons blank, caret, grid gap`
|
|
111
|
-
- Bad: `v1.5.1: patch fixes`, `v1.5.1: various improvements`
|
|
112
|
-
|
|
113
|
-
**Body:** same `#### Fixed` / `#### Changed` / `#### Added` sections as CHANGELOG, but each bullet trimmed to one short sentence — symptom first, no file paths, no internal jargon.
|
|
109
|
+
**Title and body are B6's tiers, written once:** title `v{version}: {Headline}`, body = the Full tier. Nothing about their wording lives here.
|
|
114
110
|
|
|
115
111
|
**Compare link (GitHub-hosted repos — mandatory footer):** the notes end with `**Full Changelog**: <repo-url>/compare/<prev-tag>...<new-tag>` — or use `gh release create --generate-notes`, which inserts it automatically. The Release page renders notes only, never a diff, and the tag itself points at a single commit (usually the version-mint commit, a tiny diff) — without this line there is no one-click view of the commits accumulated since the previous release. First release with no prior tag: link `<repo-url>/commits/<new-tag>` instead.
|
|
116
112
|
|
|
@@ -133,10 +129,32 @@ Do not report a plan, task, or release/deploy as complete when a migration/infra
|
|
|
133
129
|
4. **REHEARSE from the PREVIOUS state, never from empty.** A test or dry-run that starts from a fresh database exercises `CREATE`, not the migration, and proves NOTHING about an upgrade. REQUIRED evidence: the migration executed against (a) a schema generated or snapshotted from the previous release, AND (b) for any data-dependent change (unique index, `NOT NULL` backfill, type change, dedupe) a COPY of real target data. State which was run and quote its output. "The tests pass" is not evidence.
|
|
134
130
|
5. **POSTCONDITIONS asserted, ROLLBACK named.** After the real run, assert expected columns, indexes and row counts by query (condition 1 above), and record the backup or fix-forward path BEFORE any destructive step ([[RULE-agent-behavior]] B3). A migration with no stated rollback or fix-forward path does NOT ship.
|
|
135
131
|
|
|
136
|
-
### B6.
|
|
137
|
-
|
|
138
|
-
-
|
|
139
|
-
-
|
|
132
|
+
### B6. Release copy — one user-facing text per release, three lengths, printed by default
|
|
133
|
+
The developer record and the user-facing copy are two channels (C1); this item owns the user-facing one for every project type, and every surface that shows a release to a person renders it: the GitHub Release (B4), `releases.json` (C2, web only), an in-app "what's new", a store listing, a post or notification the owner sends by hand. One source, three lengths, all telling one story (`biz.C4`):
|
|
134
|
+
- **Headline** — at most 12 words naming the one thing the user gets: the C2 highlight when there is one, else the most user-visible change. It is the GitHub Release title after `v{version}: ` (A3: the `v` is render-time only) and the `releases.json` `title`. Good: `fix production icons blank, caret, grid gap`; bad: `patch fixes`, `various improvements`, `bug fixes`.
|
|
135
|
+
- **Short** — one or two sentences for a post, a notification, a chat message: the headline's benefit, the concrete mechanism as proof, the link. Says nothing the Full tier does not.
|
|
136
|
+
- **Full** — the highlight first as its own line, then every other change as one sentence each, grouped under C1's sections in C1's order, symptom first for fixes; the compare-link footer per B4 when GitHub-hosted.
|
|
137
|
+
Wording in every tier: benefit first, then proof (`biz.C1`); what the user can now do, never the file, route, symbol or component; no em/en dash, short sentences (`content.B2`); the audience's language per C1's table (English default, plus Vietnamese where the product is bilingual); one canonical term per concept across versions ("Release Notes", never a synonym — `content.A3`). Internal-only work is one honest line ("under-the-hood improvements…", C3), never dressed as a feature.
|
|
138
|
+
**Choosing the headline — a protocol with a written verdict, never a feeling.** The agent that wrote the code is the worst judge of what users gained: it ranks by effort spent, by what landed last, or by what the owner talked about most, and none of those is value. So the choice is made from the user's side, in five steps, and the reasoning is written into the run's receipt where the owner can overrule it:
|
|
139
|
+
1. **Candidates** — every change in the accumulation that alters what a user can do: a new tool, page, mode, format, integration, or an option inside one. `fixed` and `internal` are excluded by construction; a fix that unblocks a core flow may become the *Headline* when nothing else qualifies, but it is never a `highlight` (C2), because the highlight tier means capability gained, not capability restored.
|
|
140
|
+
2. **Three kill-tests per candidate, from the primary audience's seat** (`docs/biz/`, `biz.A1`; when no audience is recorded, the person the product's front page addresses). *Return:* would someone who last used the product before this version come back, or use it differently, because of this? *Tell:* can it be said in one sentence that person would repeat to a peer, with a concrete verb and no file, route, component or internal term? *Before/after:* is there something they could not do before, or could only do with a workaround? A polish, copy, layout or speed change fails the third unless it removes a workaround; an admin-only or owner-only capability fails the first; a candidate whose Tell sentence needs internal vocabulary fails the second. One failed test disqualifies.
|
|
141
|
+
3. **Rank survivors by reach × delta**, both estimated and labeled so: reach is how many of that audience meet it in a normal session (every session > a common task > a niche path); delta is the size of the gain (a new tool or mode > a new option inside an existing tool > a removed workaround).
|
|
142
|
+
4. **Cut to one.** A second only when it is independent of the first (a different job, not a sub-feature of it) and ranks close; three never — a third means the release bundled two releases (A5 materiality) or the ranking is undecided, and undecided resolves to one, not two. Zero survivors is a normal result: the Headline then names the most user-visible fix or change, and the announce verdict is judged on that.
|
|
143
|
+
5. **Cross-check the surfaces before writing.** The Headline, the first `changes[]` line, the `highlight` flag, the Short tier and the GitHub Release title must all point at the same thing; when the title you would naturally write names something the highlight does not, the selection is wrong, not the title. Then write the verdict into the receipt: one line per candidate — `highlight: <thing> — passes return/tell/before-after, reach every session, delta new mode` or `rejected: <thing> — fails return (admin-only)` — so a `release_lint.py` `[HILITE]` line is answered by this record and the owner can overrule a choice without re-deriving it.
|
|
144
|
+
|
|
145
|
+
**Announce verdict.** The copy ends with `Announce: yes` when the version carries a highlight or a fix the user would notice, or `Announce: no — <reason>` for an internal-only or invisible-fix accumulation: a follower who reads three "stability improvements" posts in a week stops reading the fourth, so the materiality test (A5) applies at the channel too. Channels are the project's own, read from `docs/biz/`, the project `CLAUDE.md` or an existing post history — never invented; with none recorded the verdict stands alone.
|
|
146
|
+
**Where it is produced.** Every `/akiship` run ends with this block, a deferred version included (`deferred — no copy`), and any other run that mints a version prints it in its closing report. It is reused, never rewritten: the `releases.json` entry and the GitHub Release body already are this copy, so the block quotes them rather than composing a third variant; where neither exists (CLI, desktop app without a GitHub Release) the block is the copy's only home and the owner pastes it where it goes.
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
## Release copy — {version} ({date})
|
|
150
|
+
Headline: …
|
|
151
|
+
Short: …
|
|
152
|
+
Full:
|
|
153
|
+
…
|
|
154
|
+
Announce: yes | no — <reason>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Doc/version moves are part of the change, not an afterthought ([[RULE-docs]]).
|
|
140
158
|
|
|
141
159
|
### B7. Pre-ship gate — work finished, nothing pushed yet
|
|
142
160
|
|
|
@@ -154,7 +172,7 @@ Run in order; each step names the rule that owns it.
|
|
|
154
172
|
1. **Release state** — derive it cold from the repo per B1, never from session memory. `Drifted` blocks everything until A5's recovery has run.
|
|
155
173
|
2. **Hygiene sweep — scoped to the accumulation, never the whole repo.** On the files touched since the boundary commit (B1.5): scythe `[WRAP]`/`[YAP]` lint ([[RULE-agent-behavior]] §0), dead code / redundant guards / duplication the accumulation itself introduced (`pattern.A8`; subtract-class detectors at diff scope), and doc references in touched comments still resolving ([[RULE-docs]] B3). A repo-wide subtraction or zero-trust sweep is a separately scheduled audit, never a per-release cost — diff scope is what keeps this gate affordable at many releases per day. Unlike an audit, findings here are fixed in place: this is a gate, not a report.
|
|
156
174
|
3. **Migration & external-action completeness — the B5 detector runs FIRST, on EVERY release, without exception.** Paste its output (or `empty`) into the receipt. A hit obliges a written answer to each of B5 points 2–5: is it separate, is the order expand → migrate → deploy → contract, was it rehearsed from the PREVIOUS state (which one, quoted output), are postconditions and rollback stated. Startup-embedded migration code counts. Then every other change whose "done" lives outside the repo (remote config, env vars, cron registrations, cache purges) is confirmed live, and each script sits in its completion location ([[RULE-coding]] B3). A green build proves nothing about the database; a green test on an empty database proves nothing about an upgrade.
|
|
157
|
-
4. **Record truthfulness** — every closed problem has its `CHANGELOG.md` entry, and no entry claims something step 3 has not cleared (B2). Web stacks additionally need `releases.json` parity (C3).
|
|
175
|
+
4. **Record truthfulness** — every closed problem has its `CHANGELOG.md` entry, and no entry claims something step 3 has not cleared (B2). Web stacks additionally need `releases.json` parity (C3). Shape is mechanical: `python3 ~/.claude/skills/akiflow/scripts/release_lint.py --latest .` (C4) must exit 0; a `[HILITE]` review line is answered in writing per C2, never silently passed.
|
|
158
176
|
5. **Doc sync — every record surface the accumulation touched, not only `docs/`.** Enumerate, then check each against the diff: plans whose work shipped moved to `docs/plan/done/`; `arch`/`feat` docs match what is about to ship ([[RULE-docs]] B1, B3); `README.md` wherever the accumulation changed setup, commands, layout, or a documented behavior; the project's task-note file when one exists (`.akidevsync/notes.json`, edited only through the `akidevsync-notes` skill — a note whose fix is in this accumulation is marked done with the matching CHANGELOG line, an unmatched or unverified one stays open and is named in the report); and any external standards doc the project `CLAUDE.md` binds the project to, updated in place when the accumulation changed a convention that doc owns. A surface skipped because it was not in `docs/` is the same drift finding as a stale doc.
|
|
159
177
|
6. **Build & test — mirror CI.** Commands are derived, never invented: the jobs `.github/workflows/*` run on push/tag take priority; a repo with no such workflow falls back to the manifest's own scripts (`npm run typecheck`/`build`/`test`, `cargo build`/`cargo test`, equivalent). Run every one of them locally, self-authorized ([[RULE-coding]] B3 — ship/release is the moment full build+test is mandatory, not optional). A failure blocks the gate and is fixed in place, same as step 2. A CI step that cannot be reproduced locally (an other-OS matrix leg, a job needing secrets) is named explicitly and left to B10 to catch post-push. A repo with no build/test command at all says so plainly — that is a finding, not a silent pass. This step sits after 2–5 because those fix code and docs first, and the build must cover what is actually about to ship.
|
|
160
178
|
7. **Verification honesty** — anything only checkable at runtime is reported as unverified rather than assumed ([[RULE-coding]] B3). "Untested but I expect it works" is a valid gate output; a silent "Done" is not.
|
|
@@ -176,8 +194,8 @@ The B7 gate plus its surrounding ritual (fix findings → sync docs → CHANGELO
|
|
|
176
194
|
### B9. Registry-published package (npm, crates.io, PyPI, …) — the registry version is the release
|
|
177
195
|
|
|
178
196
|
A package installed from a registry is a distributed artifact (A5): users get what the registry serves, so a tag plus GitHub Release with no registry version leaves `npx`/`pip install` on the old one. Released = tag + GitHub Release + `npm view <pkg>@<version> version` (or the registry's equivalent) returning the new version (`coding.B3`).
|
|
179
|
-
- **The publish mechanism is derived, never designed.** Read the existing convention first: project `CLAUDE.md`, `.github/workflows/`, and sibling packages the same account already publishes (`npm access list packages`) — a working sibling is the template (`coding.
|
|
180
|
-
- **Account facts are probed, not inferred** (`coding.
|
|
197
|
+
- **The publish mechanism is derived, never designed.** Read the existing convention first: project `CLAUDE.md`, `.github/workflows/`, and sibling packages the same account already publishes (`npm access list packages`) — a working sibling is the template (`coding.B3` rung 2). A CI publish job with a registry token adds a secret and automation: `agent.B3` territory, never the default.
|
|
198
|
+
- **Account facts are probed, not inferred** (`coding.B3` rung 5): `npm whoami` (session), `npm org ls <scope>` (scope ownership — a 404 on the package name means the name is unpublished, never that the scope is unowned), `npm profile get` (2FA mode).
|
|
181
199
|
- **2FA `auth-and-writes` makes `npm publish` the run's single hand-off** (rung 6: the OTP is human-held). Everything else is agent work — push, tag, GitHub Release, tarball verification — so the owner receives one command and the `npm view` check that proves it landed, never a list of prerequisites.
|
|
182
200
|
- **A published version number is burned forever** (`npm unpublish` is time-limited and a number is never reusable), so verify the tarball before publishing: `npm pack --dry-run` against the `files` allowlist, manifest version == CHANGELOG top == tag (A3), and the `bin` executed from the packed tarball installed in the scratchpad. A `bin` that writes to `$HOME` takes the override on its own command — `printf y | HOME="$SANDBOX" bin`, never `HOME="$SANDBOX" printf y | bin`, which scopes the variable to `printf` and runs against the real home.
|
|
183
201
|
|
|
@@ -204,17 +222,20 @@ After ANY deploy or restart, in any flow (not only `/akiship`), verify FUNCTION,
|
|
|
204
222
|
### C1. Two separate channels — do not merge them
|
|
205
223
|
| File | Audience | Language | Tone |
|
|
206
224
|
|------|----------|----------|------|
|
|
207
|
-
| `CHANGELOG.md` | developer / technical | English only | Precise, may name files/symbols. Keep a Changelog
|
|
225
|
+
| `CHANGELOG.md` | developer / technical | English only | Precise, may name files/symbols. Keep a Changelog shape, closed and ordered — see below |
|
|
208
226
|
| `app/data/releases.json` | public / end user | Bilingual EN + VI if the site is multilingual (default EN); EN-only if single-language | Popular, user-friendly, benefit-first. No jargon, no file paths |
|
|
209
227
|
|
|
210
228
|
The changelog explains *what changed and why* for maintainers. The release note tells users *what they get*. Write them separately; do not paste changelog lines into the release note.
|
|
211
229
|
|
|
230
|
+
**CHANGELOG shape — one canonical form, mechanically checked (`release_lint.py`, C4).** Version heading `## [x.y.z] - YYYY-MM-DD` (`## [Unreleased]` while open), sections `### <Name>` at exactly one level below, each at most once per version, in this fixed order and from this closed vocabulary: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`. Sections that ship nothing are omitted, never left empty. There is no `Internal`, `Docs`, `Notes`, `Verified`, `Refactored` or any other heading: internal, tooling and docs work is `Changed`; a verification or caveat is a clause on the bullet it qualifies; a decision's reasoning lives in the bullet or in `docs/research/`. The order is the standard's, not importance-ranked: an order chosen per release is unobservable across releases and drifts the moment two people, or two sessions, write entries. Evidence: 21 of 25 audited projects had entries out of order, 9 distinct orderings within one file, 13 invented headings across the set, and this rule itself named two different orders in two sections.
|
|
231
|
+
|
|
212
232
|
`releases.json` exists **only where a public web page renders it** (the Nuxt stack's release-notes page). Tauri, CLI, and other non-web projects keep `CHANGELOG.md` only — a release-notes file nothing renders is dead data; do not create one. Where `releases.json` does not exist, every rule below that mentions it simply does not apply.
|
|
213
233
|
|
|
214
234
|
### C2. releases.json schema
|
|
215
235
|
- Single-language site: `{ version, date, title, changes: [{ type, text }] }`
|
|
216
236
|
- Multilingual site: localize the human text — `title: { en, vi }`, `changes: [{ type, text: { en, vi } }]`. Keep `version`, `date`, `type` locale-neutral. Default/fallback language is English.
|
|
217
237
|
- `type` is one of `new` | `improved` | `fixed` | `internal` (stable badge keys).
|
|
238
|
+
- `highlight: true` (optional, locale-neutral) marks the line B6's headline protocol selected — a capability gained, chosen from the primary audience's seat through the three kill-tests and the reach × delta ranking, with the verdict written in the run's receipt. It is a tier, never a filter: every change still ships as a line (C3); the page renders the highlighted line as a distinct card, because a flat list gives a bug fix and a new tool the same weight, so the reader's eye has nothing to land on and the page reads as a log, not a product moving. Shape: the highlighted line is the **first** in `changes[]`; at most one, two only when B6 step 4 allows it; never on `fixed` or `internal`; zero is correct when no candidate survives. The `title` is B6's Headline tier and names the same thing the highlight marks (one story per surface, `biz.C4`); a title that headlines something the highlight does not, or vice versa, means one of them is wrong. Write the highlighted line benefit-first with the concrete mechanism as proof (`biz.C1`, `content.B2`): what the user can now do, then how — never the file, route or component that does it. Mechanical review: `release_lint.py` emits `[HILITE]` for a version with a `new` change and no highlight — a candidate, answered by B6 step 5's per-candidate verdict lines in the gate receipt, never silently passed.
|
|
218
239
|
|
|
219
240
|
### C3. No version gaps, and no content gaps, in releases.json
|
|
220
241
|
Every version that appears in `CHANGELOG.md` MUST also appear in `releases.json` (no missing version), and every `Added`/`Changed`/`Fixed`/`Removed` section in that version's CHANGELOG entry must be represented by at least one `changes[]` line in `releases.json` (no missing content) — skipping a version, or silently dropping a whole category of its work, because it reads as "internal" or "technical" is not allowed. This page is the one place both a human visitor and a crawling/LLM bot judge whether the product is actively maintained; a version that reads as empty is worse than one that reads as unglamorous.
|
|
@@ -228,14 +249,13 @@ Every version that appears in `CHANGELOG.md` MUST also appear in `releases.json`
|
|
|
228
249
|
Never leave a gap like `1.0.5 → 1.0.7` or `0.1.0 → 0.1.3` in releases.json. A one-line entry is better than a missing version.
|
|
229
250
|
|
|
230
251
|
### C4. Sync check — required before closing a task
|
|
231
|
-
After editing `CHANGELOG.md` or `releases.json`, run:
|
|
252
|
+
After editing `CHANGELOG.md` or `releases.json`, run from the project root:
|
|
232
253
|
|
|
233
254
|
```
|
|
234
|
-
|
|
235
|
-
grep -E '^## \[' CHANGELOG.md
|
|
255
|
+
python3 ~/.claude/skills/akiflow/scripts/release_lint.py --latest .
|
|
236
256
|
```
|
|
237
257
|
|
|
238
|
-
|
|
258
|
+
Exit 0 is the pass. Verdict tags — `[ORDER]`, `[SECTION]`, `[LEVEL]` (C1 shape), `[PARITY]` (a version in one surface and not the other, C3), `[TYPE]` (a badge key outside C2) — are fixed before the task closes. `[HILITE]` is a review line (C2), answered, never auto-fixed. Without `--latest` the script sweeps every version — that is an audit run (`agent.B5`), never a per-task cost; historical entries are corrected only when a task already touches them, never as a backfill sweep.
|
|
239
259
|
|
|
240
260
|
### C5. Live production verification
|
|
241
261
|
Never trust a deployment CLI's success status alone. A web deploy is only verified when the live production URL explicitly returns the new version data.
|
package/payload/RULE-seo.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SEO Rule — Nuxt + Cloudflare Stack
|
|
2
2
|
|
|
3
|
-
<!-- Address map: seo.A1-
|
|
3
|
+
<!-- Address map: seo.A1-6 · seo.B1-3 · seo.C1-3 (⟨Aki⟩) -->
|
|
4
4
|
|
|
5
5
|
## Scope
|
|
6
6
|
Cross-project rules for all Nuxt 4 + Cloudflare Pages sites. For project-specific keyword strategy and schema values, see the project's own `docs/ref/seo.md` or equivalent.
|
|
@@ -78,6 +78,15 @@ sitemap: {
|
|
|
78
78
|
- **Location**: `public/ogimage/[slug].jpg` or `.png`
|
|
79
79
|
- **Fallback**: if page-level image is absent, the composable falls back to the site-wide default OG image
|
|
80
80
|
- Never reference an OG image path that doesn't exist in `public/`
|
|
81
|
+
- Referenced site-relative (`/ogimage/slug.jpg`); the SEO composable makes it absolute (A6)
|
|
82
|
+
|
|
83
|
+
### A6. URL form — relative at rest, absolute only at emission
|
|
84
|
+
|
|
85
|
+
- Every same-site URL is stored and rendered root-relative (`/images/x.jpg`, `/path/`): content data, internal links, `<img src>`/`srcset`, asset references. An own-origin literal (`https://domain.com/...`) there breaks localhost and preview deploys, and hides a missing local asset behind the production copy.
|
|
86
|
+
- Consumers that require an absolute URL (`og:image`, `twitter:image`, `og:url`, canonical, hreflang, JSON-LD `url`/`image`/`logo`, sitemap, RSS, email, share text) get it from one helper at the emission boundary (SEO composable, schema builder, feed generator), never from the stored value. The helper is idempotent (relative → absolute, absolute → unchanged), so legacy absolute data keeps working without a migration.
|
|
87
|
+
- The origin comes from the one configured site URL (`pattern.A1`), never from the request host: a preview deploy must still emit production canonical and OG URLs.
|
|
88
|
+
- A subpath deploy (`baseURL` ≠ `/`) prefixes through the framework's base-URL mechanism, never by string concatenation per call site.
|
|
89
|
+
- Detected mechanically in C3.
|
|
81
90
|
|
|
82
91
|
## B. AI visibility & entity
|
|
83
92
|
|
|
@@ -91,7 +100,7 @@ These rules help content appear in AI-generated answers (Perplexity, ChatGPT, Ge
|
|
|
91
100
|
- **alternateName in Organization/WebSite schema**: include all brand spelling variants (accented + unaccented + lowercase + domain form) so AI can resolve them to a single entity
|
|
92
101
|
- **knowsAbout**: list the topics the brand covers — helps AI cite the site as a relevant source
|
|
93
102
|
|
|
94
|
-
> ⚠️ **`llms.txt` is not an AI-visibility strategy (2026).** A log study across 137,000 domains found **97% of `llms.txt` files received zero requests over a full month**; no major LLM vendor has committed to reading the format, and Google's John Mueller has compared it to the meta keywords tag — a standard proposed by publishers that no consumer agreed to honour. Keep the file if it already exists (it costs nothing and is genuinely useful for *internal* agents reading the site), but never list it as an SEO/GEO deliverable and never let it substitute for the thing that does work: getting the content into the server-rendered HTML (
|
|
103
|
+
> ⚠️ **`llms.txt` is not an AI-visibility strategy (2026).** A log study across 137,000 domains found **97% of `llms.txt` files received zero requests over a full month**; no major LLM vendor has committed to reading the format, and Google's John Mueller has compared it to the meta keywords tag — a standard proposed by publishers that no consumer agreed to honour. Keep the file if it already exists (it costs nothing and is genuinely useful for *internal* agents reading the site), but never list it as an SEO/GEO deliverable and never let it substitute for the thing that does work: getting the content into the server-rendered HTML (B3).
|
|
95
104
|
|
|
96
105
|
### B2. Entity & ecosystem linking
|
|
97
106
|
|
|
@@ -103,16 +112,7 @@ For sites that belong to a multi-site ecosystem or brand family:
|
|
|
103
112
|
|
|
104
113
|
Keep the concrete domain list (parent org URL, sibling sites) in the project's own docs — one source of truth per ecosystem, not hardcoded in shared rules.
|
|
105
114
|
|
|
106
|
-
### B3.
|
|
107
|
-
|
|
108
|
-
Google treats accented and unaccented Vietnamese as different queries (`vst là gì` ≠ `vst la gi`). To cover both without degrading UX:
|
|
109
|
-
|
|
110
|
-
- **Embed the unaccented form in parentheses** in the first mention of a term in body copy or FAQ: *"...VST (vst la gi)..."*
|
|
111
|
-
- **Or include it in** `keywords` meta or `alternateName` in schema
|
|
112
|
-
- **Never** put unaccented forms in H1, H2, visible headings, or the FAQ question text — it looks unprofessional
|
|
113
|
-
- **Meta title and description**: use correctly accented Vietnamese; unaccented coverage comes from schema + body copy
|
|
114
|
-
|
|
115
|
-
### B4. Prerendering & SSR
|
|
115
|
+
### B3. Prerendering & SSR
|
|
116
116
|
|
|
117
117
|
- SEO-critical content must be in the HTML at crawl time — not injected by client-side JS. **This is the single highest-evidence rule in this file for AI visibility**: roughly 69% of AI crawlers (GPTBot, OAI-SearchBot, ClaudeBot, Claude-SearchBot, PerplexityBot) do **not** execute JavaScript, so a client-only SPA is simply invisible to them. Googlebot and Gemini do render; ChatGPT, Claude and Perplexity do not. Prerendering beats every schema tweak combined.
|
|
118
118
|
- Public pages: prerender/SSG preferred
|
|
@@ -129,7 +129,7 @@ Every public page must call `usePageSeo()`. Canonical URL is derived automatical
|
|
|
129
129
|
usePageSeo({
|
|
130
130
|
title: 'Page Topic', // Max 60 chars total — NO brand suffix (see @nuxtjs/seo note below)
|
|
131
131
|
description: 'Action-oriented…', // Max 155 chars, unique per page
|
|
132
|
-
ogImage: '
|
|
132
|
+
ogImage: '/ogimage/slug.jpg', // optional, site-relative — the composable absolutizes it (A6)
|
|
133
133
|
ogImageAlt: 'Description of image', // optional
|
|
134
134
|
noindex: true, // optional, for admin/private pages
|
|
135
135
|
})
|
|
@@ -161,6 +161,8 @@ Run `scripts/validate-seo.js` (or equivalent) after every build. At minimum it s
|
|
|
161
161
|
- [ ] All descriptions ≤ 155 chars
|
|
162
162
|
- [ ] No em dash (`—`) or en dash (`–`) in title or description
|
|
163
163
|
- [ ] All canonical URLs end with `/`
|
|
164
|
+
- [ ] No rendered `src`, `srcset` or `<a href>` carries the site's own origin (A6); `<head>` `link`/`meta` are exempt, they are emission targets
|
|
165
|
+
- [ ] Every `og:image`, `twitter:image` and JSON-LD `image` is an absolute `https://` URL
|
|
164
166
|
- [ ] Homepage `Organization` schema has `alternateName` and `sameAs`
|
|
165
167
|
- [ ] `/admin/**` pages absent from sitemap output
|
|
166
168
|
- [ ] Skip redirect stub files (`http-equiv="refresh"`) — they have no SEO content to validate
|
|
@@ -27,6 +27,7 @@ Nuxt 4 · Vue 3 · Tailwind v4 · @nuxtjs/i18n · @nuxtjs/seo · SweetAlert2 ·
|
|
|
27
27
|
- `crypto.subtle` operates on bytes — feed it `new TextEncoder().encode(str)`, not the string.
|
|
28
28
|
- Do not enable `nodejs_compat` in `wrangler.toml` unless upstream issues are confirmed fixed
|
|
29
29
|
- Trailing slash: `trailingSlash: true` everywhere (routing, canonical, og:url, sitemap, schema.org) — canonical config lives in the i18n section below
|
|
30
|
+
- Site origin: `site.url` in `nuxt.config` is the single source, read through `useSiteConfig().url` (nuxt-site-config, bundled with `@nuxtjs/seo`) inside the one URL helper the SEO composable and schema use; never a `'https://domain'` literal in a composable, page, component or content record. URL form: `seo.A6`
|
|
30
31
|
- Never call h3's `readBody()` in a DELETE handler. On workerd, reading a body the runtime never actually attached to a DELETE request hangs the promise instead of rejecting it — the platform then kills the request as a bare 500 with no stack trace. This reproduces on real Cloudflare Pages/Workers but NOT under `nuxt dev` (Node), so it survives local testing and only surfaces in production. Pass the id (or any DELETE payload) via query string on both client and server instead — never body.
|
|
31
32
|
- After the response, keep the isolate alive with `event.context.cloudflare.context.waitUntil` (the path live notify modules use on tachnhac / vstshop / tuvi / kinhdich). Nitro 2.13 also wraps the same CF context as `event.waitUntil`. Do not copy Nitro v3 docs onto this stack: v3 puts the platform on `event.req.runtime.cloudflare`, and `event.req.waitUntil` does not exist on 2.13, so the isolate dies. `nuxt dev` cannot prove isolate lifetime. A 200 that returns before the background work is not evidence the work ran.
|
|
32
33
|
|
|
@@ -56,4 +56,4 @@ Three switches, routinely confused — pick by what each actually controls:
|
|
|
56
56
|
- **Ad-hoc signing loses the grant on every rebuild.** `codesign --sign -` (Xcode's "Sign to Run Locally") produces a new signature each build, and the authorization is tied to that exact build — so a permission granted yesterday is simply gone today, which reads as a random TCC bug. The fix is a **stable self-signed certificate**, which keeps grants across rebuilds; `tccutil reset All <bundle-id>` only clears the stale state, it does not prevent the next loss.
|
|
57
57
|
- **Scope limit — this chain governs consent-based reads.** It does not apply to paths the user picked in an Open/Save dialog or by drag-and-drop (user intent grants access directly), and Apple's own analysis excludes file *writes* from it. A write-only or file-picker-driven sidecar failing is a different diagnosis; don't reach for these switches first.
|
|
58
58
|
|
|
59
|
-
When you cannot tell which switch fired, watch it rather than guess: `log show --predicate 'subsystem == "com.apple.TCC"' --last 5m` prints the `AttributionChain` (which process was held responsible) and the request's result. Claims and sources: `docs/research/macos-tcc-tauri-boundary-aug21.md` in the akidevrule repo. Full lookup (switches + rebuild/DR mechanism): `~/.aki/akidevrule/docs/ref/macos-codesign-tcc.md`.
|
|
59
|
+
When you cannot tell which switch fired, watch it rather than guess: `log show --predicate 'subsystem == "com.apple.TCC"' --last 5m` prints the `AttributionChain` (which process was held responsible) and the request's result. Claims and sources: `docs/research/macos-tcc-tauri-boundary-aug21.md` in the akidevrule repo. Full lookup (switches + rebuild/DR mechanism): `~/.aki/akidevrule/docs/ref/fact-macos-codesign-tcc.md`.
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
- **Composition over duplication (Law 5)** → slots / dynamic components / `v-for`, never hand-copied markup.
|
|
12
12
|
- **OCP (Law 4)** → extend a component via props / variant / slot, never fork a copy.
|
|
13
13
|
- **Name by role (Law 7)** → semantic tokens and variants, never value-names.
|
|
14
|
+
- **Draft, then commit once (Law 9)** → `dragenter`/`dragover`/`pointermove`/`input` handlers touch a local draft only; `drop`/`pointerup`/`Enter`/Save writes the store once. Grep: no store setter, `localStorage`, IPC or fetch inside a preview handler.
|
|
14
15
|
- **Reshape, don't stack (Law 8)** → before packaging a repeated style, try to remove it. The tier ladder only *packages* repetition; Law 8 is the only thing that *eliminates* it, and without it a codebase obeys every rule here while growing without bound.
|
|
15
16
|
- **Documentation** → every global pattern is looked up before writing and recorded after, so the next agent reuses instead of rewriting.
|
|
16
17
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aki-article-writer
|
|
3
3
|
description: >-
|
|
4
|
-
Per-project
|
|
5
|
-
JSON-LD
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
Per-project pipeline for publishable site prose: research & fact-verification, SEO
|
|
5
|
+
metadata, JSON-LD, UX-psychology-aware content, a rendered-output pass, and a
|
|
6
|
+
separate Image Scout subagent for images. Activate whenever the task writes,
|
|
7
|
+
rewrites, translates or reviews an article, news or blog post, announcement or
|
|
8
|
+
knowledge entry for any site, in any wording or language, including turning a
|
|
9
|
+
release, changelog or finding into a post.
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# aki-article-writer
|
|
13
13
|
|
|
14
|
-
Invoke with `/aki-article-writer`, or whenever
|
|
14
|
+
Invoke with `/aki-article-writer`, or whenever a task writes, rewrites, translates or reviews publishable site prose (article, news/blog post, announcement, knowledge entry), in any wording — including a release or finding to be turned into a post.
|
|
15
15
|
|
|
16
16
|
This skill delegates one full article to a dedicated **Article Worker subagent**. The worker spawns a separate **Image Scout subagent** (lightweight model) for all image work, keeping both agents' contexts clean and independent.
|
|
17
17
|
|
|
@@ -92,7 +92,7 @@ Select the correct schema type based on project:
|
|
|
92
92
|
|
|
93
93
|
### 2.5 URL canonical & trailing slash
|
|
94
94
|
|
|
95
|
-
All canonical URLs, sitemap entries, `og:url`, internal links, and JSON-LD `url` fields must end with `/`. Required for Cloudflare Pages compatibility.
|
|
95
|
+
All canonical URLs, sitemap entries, `og:url`, internal links, and JSON-LD `url` fields must end with `/`. Required for Cloudflare Pages compatibility. Those absolute URLs are produced at emission; every URL stored in the record or rendered in the body is site-relative (`seo.A6`).
|
|
96
96
|
|
|
97
97
|
---
|
|
98
98
|
|
|
@@ -130,16 +130,7 @@ Example:
|
|
|
130
130
|
|
|
131
131
|
Use exactly one canonical term for each concept throughout the article. Synonym variation may seem stylistically rich but confuses both readers and AI crawlers. Pick the term, define it once, use it consistently.
|
|
132
132
|
|
|
133
|
-
### 3.4
|
|
134
|
-
|
|
135
|
-
Google treats `vst là gì` and `vst la gi` as different queries. To cover both without degrading readability:
|
|
136
|
-
|
|
137
|
-
- Embed the unaccented form in parentheses at its **first occurrence** in body copy or FAQ: `…VST (vst la gi) là loại phần mềm…`
|
|
138
|
-
- Or place it in `keywords` meta or `alternateName` in schema
|
|
139
|
-
|
|
140
|
-
**Never** place unaccented forms in H1, H2, H3, or FAQ question text — it degrades the visual quality of the interface.
|
|
141
|
-
|
|
142
|
-
### 3.5 Anxiety handling at CTA
|
|
133
|
+
### 3.4 Anxiety handling at CTA
|
|
143
134
|
|
|
144
135
|
At every call-to-action point (sign-up, purchase, download, consult), identify the dominant user anxiety at that moment and answer it right there — not on a distant FAQ page:
|
|
145
136
|
|
|
@@ -150,12 +141,12 @@ At every call-to-action point (sign-up, purchase, download, consult), identify t
|
|
|
150
141
|
| Compatibility | "Hỗ trợ Win/Mac, tương thích mọi DAW phổ biến" |
|
|
151
142
|
| Privacy | "Không lưu dữ liệu cá nhân, xoá tài khoản bất cứ lúc nào" |
|
|
152
143
|
|
|
153
|
-
### 3.
|
|
144
|
+
### 3.5 Internal & external links
|
|
154
145
|
|
|
155
146
|
- **Internal links:** ≥ 2 links to related articles or service pages within the same project
|
|
156
147
|
- **External links:** link to authoritative sources when citing data; add `rel="noopener"`
|
|
157
148
|
|
|
158
|
-
### 3.
|
|
149
|
+
### 3.6 SSR / prerender requirement
|
|
159
150
|
|
|
160
151
|
69% of AI crawlers (ChatGPT, ClaudeBot, PerplexityBot, OAI-SearchBot) do not execute JavaScript. All article content, meta tags, and schema must be present in the server-rendered HTML at crawl time — never client-side only.
|
|
161
152
|
|
|
@@ -331,7 +322,7 @@ Embed with markdown image syntax directly in body content:
|
|
|
331
322
|
### `article_arch: component` (single-image mode)
|
|
332
323
|
|
|
333
324
|
Do **not** write `![]()` anywhere in body/paragraph fields — the renderer does not parse it and will print the literal text. Instead:
|
|
334
|
-
1. Set the content record's existing share-image field (e.g. `ogImage`) to `/images/articles/<slug>.<ext
|
|
325
|
+
1. Set the content record's existing share-image field (e.g. `ogImage`) to `/images/articles/<slug>.<ext>`, site-relative. Confirm the SEO composable and schema make it absolute (`seo.A6`); if either passes it through raw, fix that helper once — never write the site origin into the record, even when neighbouring records do.
|
|
335
326
|
2. Confirm the shared render component (e.g. `ContentArticle.vue`) already displays that field as a hero image above the body. If it does not yet, that is a one-time component change to flag to the user — do not route around it with markdown text in a paragraph.
|
|
336
327
|
3. No separate body images in this mode; one image serves as both hero and OG/share image.
|
|
337
328
|
|
|
@@ -354,7 +345,6 @@ Article Worker runs through the full checklist before reporting completion.
|
|
|
354
345
|
- [ ] No `UNVERIFIED` claim appears in the published text
|
|
355
346
|
- [ ] No banned openers in any paragraph or FAQ answer
|
|
356
347
|
- [ ] No paragraph exceeds 5 lines
|
|
357
|
-
- [ ] Unaccented Vietnamese keyword embedded in parentheses at first occurrence in body / FAQ (vi locale only)
|
|
358
348
|
- [ ] CTA point has an anxiety-answering line
|
|
359
349
|
- [ ] ≥ 1 H2 is a direct question (ends with `?`)
|
|
360
350
|
- [ ] ≥ 2 internal links
|
|
@@ -370,6 +360,12 @@ Article Worker runs through the full checklist before reporting completion.
|
|
|
370
360
|
### 6.4 SSR / prerender
|
|
371
361
|
- [ ] Article content, meta tags, and schema are present in server-rendered HTML, not deferred to client-side JS
|
|
372
362
|
|
|
363
|
+
### 6.5 Rendered-output pass
|
|
364
|
+
Source strings, a green build and a passing SEO validator do not show what the reader sees. After the build, open the article's built HTML (every locale) — the build is self-authorized, no dev server needed (`coding.B3`):
|
|
365
|
+
- [ ] `grep` the page body: no `src`, `srcset` or `<a href>` carries the site's own origin; `og:image` and JSON-LD `image` are absolute (`seo.C3`)
|
|
366
|
+
- [ ] Read the full rendered text top to bottom as the target reader, in each locale, and fix anything that reader would stumble on — before reporting the article done
|
|
367
|
+
- [ ] Any SEO device visible in the text (a parenthetical keyword variant, a repeated keyphrase) is listed as a review line, never shipped silently
|
|
368
|
+
|
|
373
369
|
---
|
|
374
370
|
|
|
375
371
|
## Antigravity / AGY note
|
package/skills/akiflow/SKILL.md
CHANGED
|
@@ -212,7 +212,7 @@ python3 ~/.claude/skills/akiflow/scripts/council_cost.py --session <uuid>
|
|
|
212
212
|
- **Claude Code:** roster in one batch; `SendMessage` for peer challenge and for resuming completed seats; continuity travels as the plan doc or diff named in the prompt, since the one `subagent_type` that inherits session history (`fork`) is gated off by default; `isolation: "worktree"` for concurrent writers. An agent the *user* stopped refuses to resume via message and must be resumed from its own transcript panel — do not respawn a duplicate.
|
|
213
213
|
- **Headless (`claude -p`):** nobody can answer an escalation or a permission prompt. Record it as `BLOCKED: needs owner` in `checklist.md` and continue the other items — never guess what the owner would have wanted.
|
|
214
214
|
- **Antigravity / AGY:** supports native subagents via `invoke_subagent`. When `/akiflow` is invoked with multiple experts or dispatch lanes, the lead MUST spawn the roster via `invoke_subagent` concurrently in one batch. Simulating multiple seats sequentially in a single session context without spawning real subagents is strictly forbidden (role collapse / self-approval violation). Where AGY is reachable from a Claude Code lead, it may also serve as a wide-context worker substrate (`aki-hands`).
|
|
215
|
-
- **Script paths in this skill's literal commands are written for Claude Code** (`~/.claude/skills/akiflow/scripts/...`). This file is deployed byte-identical to `~/.gemini/config/skills/` too (`docs/ref/agent-skills-standard.md`), so either root's path runs the same script under Antigravity/agy. **Run the command exactly as written above** — the installer pre-allows both roots in both renderings (expanded and tilde-literal), so the form you copy is never what gets denied. Background: agy's matcher compares command strings literally, with no glob or tilde expansion, so a rule and a command that render the same path differently do not match — which is why the pre-allow covers every rendering instead of asking you to normalize one (`docs/ref/cli-permission-allowlist-standard.md` §1.2).
|
|
215
|
+
- **Script paths in this skill's literal commands are written for Claude Code** (`~/.claude/skills/akiflow/scripts/...`). This file is deployed byte-identical to `~/.gemini/config/skills/` too (`docs/ref/fact-agent-skills-standard.md`), so either root's path runs the same script under Antigravity/agy. **Run the command exactly as written above** — the installer pre-allows both roots in both renderings (expanded and tilde-literal), so the form you copy is never what gets denied. Background: agy's matcher compares command strings literally, with no glob or tilde expansion, so a rule and a command that render the same path differently do not match — which is why the pre-allow covers every rendering instead of asking you to normalize one (`docs/ref/fact-cli-permission-allowlist-standard.md` §1.2).
|
|
216
216
|
|
|
217
217
|
Verified harness facts behind every flag named here: `references/harness-facts.md` — its § Worker invocation quick-facts is the lookup table (literal command, read-only mechanism, silent failure per lane); the rest of the file is why. Design record: `docs/arch/akiflow.md` in the akidevrule repo.
|
|
218
218
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# release_lint.py — mechanical checks for the release record surfaces RULE-release.md owns: CHANGELOG.md shape (C1) and releases.json parity + highlight (C2–C4).
|
|
3
|
+
# Usage: release_lint.py [--latest] [--all] <project-dir|CHANGELOG.md> [...] --latest checks only the newest version block (the B7 gate scope).
|
|
4
|
+
# Output: [TAG] path:line | short label Exit: 0 clean · 1 findings · 2 usage error. Same grammar as scythe.py.
|
|
5
|
+
# Verdict tags: [ORDER] sections out of canonical order · [SECTION] heading outside the closed vocabulary or duplicated · [LEVEL] version/section heading at the wrong level · [PARITY] version present in one surface and absent from the other · [TYPE] releases.json type outside new|improved|fixed|internal.
|
|
6
|
+
# Review tag (never a verdict): [HILITE] a version with a `new` change and no `highlight: true` line, more than two highlighted lines, a highlight not first, or a highlight on a `fixed`/`internal` line — a candidate for C2 judgment.
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
SECTIONS = ['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security']
|
|
16
|
+
TYPES = {'new', 'improved', 'fixed', 'internal'}
|
|
17
|
+
HIGHLIGHT_MAX = 2
|
|
18
|
+
|
|
19
|
+
_VERSION = re.compile(r'^(#+)\s*\[?(v?\d+\.\d+\.\d+[^\]\s]*|Unreleased)\]?', re.I)
|
|
20
|
+
_SECTION = re.compile(r'^(#+)\s*(\S+)')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _changelog_blocks(lines: list[str]) -> list[dict]:
|
|
24
|
+
"""Split a CHANGELOG into version blocks: {version, line, level, sections:[(name, line, level)]}."""
|
|
25
|
+
blocks: list[dict] = []
|
|
26
|
+
fence = False
|
|
27
|
+
for n, raw in enumerate(lines, 1):
|
|
28
|
+
if raw.lstrip().startswith(('```', '~~~')):
|
|
29
|
+
fence = not fence
|
|
30
|
+
continue
|
|
31
|
+
if fence or not raw.startswith('#'):
|
|
32
|
+
continue
|
|
33
|
+
vm = _VERSION.match(raw)
|
|
34
|
+
if vm and (not blocks or len(vm.group(1)) <= 2 or blocks[-1]['level'] == len(vm.group(1))):
|
|
35
|
+
blocks.append({'version': vm.group(2), 'line': n, 'level': len(vm.group(1)), 'sections': []})
|
|
36
|
+
continue
|
|
37
|
+
sm = _SECTION.match(raw)
|
|
38
|
+
if sm and len(sm.group(1)) == 2 and sm.group(2) != 'Changelog':
|
|
39
|
+
blocks.append({'version': None, 'line': n, 'level': 2, 'sections': [], 'heading': raw.lstrip('# ').strip()})
|
|
40
|
+
continue
|
|
41
|
+
if sm and blocks and blocks[-1]['version'] and len(sm.group(1)) > blocks[-1]['level']:
|
|
42
|
+
blocks[-1]['sections'].append((sm.group(2).strip('*').rstrip(':'), n, len(sm.group(1))))
|
|
43
|
+
return blocks
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def lint_changelog(path: Path, latest: bool) -> tuple[list[str], list[str], list[str]]:
|
|
47
|
+
lines = path.read_text(encoding='utf-8', errors='replace').splitlines()
|
|
48
|
+
all_blocks = _changelog_blocks(lines)
|
|
49
|
+
blocks = all_blocks[:1] if latest else all_blocks
|
|
50
|
+
findings: list[str] = []
|
|
51
|
+
for b in blocks:
|
|
52
|
+
if b['version'] is None:
|
|
53
|
+
findings.append(f"[SECTION] {path}:{b['line']} | H2 `{b['heading']}` is not a version heading")
|
|
54
|
+
continue
|
|
55
|
+
if b['level'] != 2:
|
|
56
|
+
findings.append(f"[LEVEL] {path}:{b['line']} | version heading at H{b['level']}, expected `## [{b['version']}] - <date>`")
|
|
57
|
+
names = [s[0] for s in b['sections']]
|
|
58
|
+
seen: set[str] = set()
|
|
59
|
+
for name, n, lvl in b['sections']:
|
|
60
|
+
if name not in SECTIONS:
|
|
61
|
+
findings.append(f"[SECTION] {path}:{n} | `{name}` is not one of {', '.join(SECTIONS)}")
|
|
62
|
+
elif name in seen:
|
|
63
|
+
findings.append(f"[SECTION] {path}:{n} | `{name}` repeated inside {b['version']}")
|
|
64
|
+
elif lvl != b['level'] + 1:
|
|
65
|
+
findings.append(f"[LEVEL] {path}:{n} | section at H{lvl}, expected H{b['level'] + 1}")
|
|
66
|
+
seen.add(name)
|
|
67
|
+
std = [x for x in names if x in SECTIONS]
|
|
68
|
+
if std != sorted(dict.fromkeys(std), key=SECTIONS.index) and len(set(std)) == len(std):
|
|
69
|
+
expected = ', '.join(sorted(std, key=SECTIONS.index))
|
|
70
|
+
findings.append(f"[ORDER] {path}:{b['line']} | {b['version']}: {', '.join(std)} — expected {expected}")
|
|
71
|
+
scoped_versions = [b['version'].lstrip('v') for b in blocks if b['version']]
|
|
72
|
+
all_versions = [b['version'].lstrip('v') for b in all_blocks if b['version']]
|
|
73
|
+
return findings, scoped_versions, all_versions
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def lint_releases(path: Path, changelog_versions: list[str], all_changelog_versions: list[str], latest: bool) -> list[str]:
|
|
77
|
+
findings: list[str] = []
|
|
78
|
+
try:
|
|
79
|
+
data = json.loads(path.read_text(encoding='utf-8'))
|
|
80
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
81
|
+
return [f"[PARITY] {path}:1 | unreadable releases.json ({e})"]
|
|
82
|
+
items = data if isinstance(data, list) else data.get('releases', [])
|
|
83
|
+
if latest:
|
|
84
|
+
items = items[:1]
|
|
85
|
+
text = path.read_text(encoding='utf-8').splitlines()
|
|
86
|
+
|
|
87
|
+
def line_of(version: str) -> int:
|
|
88
|
+
for n, raw in enumerate(text, 1):
|
|
89
|
+
if f'"{version}"' in raw:
|
|
90
|
+
return n
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
json_versions = [str(r.get('version', '')).lstrip('v') for r in items]
|
|
94
|
+
for v in [x for x in changelog_versions if x.lower() != 'unreleased']:
|
|
95
|
+
if v not in json_versions:
|
|
96
|
+
findings.append(f"[PARITY] {path}:1 | CHANGELOG version {v} has no releases.json entry")
|
|
97
|
+
# Reverse check needs the full history: with [Unreleased] on top, --latest's one block never holds the newest shipped version.
|
|
98
|
+
for r, v in zip(items, json_versions):
|
|
99
|
+
n = line_of(v)
|
|
100
|
+
if v not in all_changelog_versions:
|
|
101
|
+
findings.append(f"[PARITY] {path}:{n} | releases.json version {v} has no CHANGELOG entry")
|
|
102
|
+
changes = r.get('changes', [])
|
|
103
|
+
for c in changes:
|
|
104
|
+
if c.get('type') not in TYPES:
|
|
105
|
+
findings.append(f"[TYPE] {path}:{n} | {v}: type `{c.get('type')}` not in {', '.join(sorted(TYPES))}")
|
|
106
|
+
hl = [c for c in changes if c.get('highlight')]
|
|
107
|
+
if not hl and any(c.get('type') == 'new' for c in changes):
|
|
108
|
+
findings.append(f"[HILITE] {path}:{n} | {v}: has a `new` change but no `highlight: true` (review)")
|
|
109
|
+
if len(hl) > HIGHLIGHT_MAX:
|
|
110
|
+
findings.append(f"[HILITE] {path}:{n} | {v}: {len(hl)} highlighted lines, more than {HIGHLIGHT_MAX} (review)")
|
|
111
|
+
if hl and changes and not changes[0].get('highlight'):
|
|
112
|
+
findings.append(f"[HILITE] {path}:{n} | {v}: highlighted line is not the first change (review)")
|
|
113
|
+
for c in hl:
|
|
114
|
+
if c.get('type') in ('fixed', 'internal'):
|
|
115
|
+
findings.append(f"[HILITE] {path}:{n} | {v}: highlight on a `{c.get('type')}` change, C2 allows only new/improved (review)")
|
|
116
|
+
return findings
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def lint_target(target: str, latest: bool) -> list[str]:
|
|
120
|
+
p = Path(target)
|
|
121
|
+
changelog = p if p.is_file() else p / 'CHANGELOG.md'
|
|
122
|
+
if not changelog.is_file():
|
|
123
|
+
print(f"release_lint: no CHANGELOG.md at {target}", file=sys.stderr)
|
|
124
|
+
sys.exit(2)
|
|
125
|
+
findings, versions, all_versions = lint_changelog(changelog, latest)
|
|
126
|
+
releases = changelog.parent / 'app' / 'data' / 'releases.json'
|
|
127
|
+
if releases.is_file():
|
|
128
|
+
findings.extend(lint_releases(releases, versions, all_versions, latest))
|
|
129
|
+
return findings
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main() -> None:
|
|
133
|
+
latest = '--latest' in sys.argv
|
|
134
|
+
show_all = '--all' in sys.argv
|
|
135
|
+
targets = [a for a in sys.argv[1:] if not a.startswith('--')]
|
|
136
|
+
if not targets:
|
|
137
|
+
print("usage: release_lint.py [--latest] [--all] <project-dir|CHANGELOG.md> [...]", file=sys.stderr)
|
|
138
|
+
sys.exit(2)
|
|
139
|
+
findings: list[str] = []
|
|
140
|
+
for t in targets:
|
|
141
|
+
findings.extend(lint_target(t, latest))
|
|
142
|
+
if not findings:
|
|
143
|
+
sys.exit(0)
|
|
144
|
+
cap = 40
|
|
145
|
+
shown = findings if show_all or len(findings) <= cap else findings[:cap]
|
|
146
|
+
for line in shown:
|
|
147
|
+
print(line)
|
|
148
|
+
if len(shown) < len(findings):
|
|
149
|
+
counts: dict[str, int] = {}
|
|
150
|
+
for line in findings:
|
|
151
|
+
tag = line.split()[0]
|
|
152
|
+
counts[tag] = counts.get(tag, 0) + 1
|
|
153
|
+
print(f"--- {len(findings) - cap} more findings suppressed ---")
|
|
154
|
+
print(' '.join(f"{t} {c}" for t, c in counts.items()) + f" (total {len(findings)})")
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == '__main__':
|
|
159
|
+
main()
|
package/skills/akihelp/SKILL.md
CHANGED
|
@@ -11,14 +11,14 @@ Invoke with `/akihelp`, or whenever the user asks, in any wording, what this set
|
|
|
11
11
|
|
|
12
12
|
## Steps
|
|
13
13
|
|
|
14
|
-
1. Read `~/.
|
|
14
|
+
1. Read the Routes table in `~/.claude/skills/akirule/SKILL.md` — one row per rule file: its topic, when it loads, its signals. `RULE-agent-behavior.md` is the one resident rule and is not in the table.
|
|
15
15
|
2. List `~/.claude/skills/` and read the frontmatter (`name` + `description`) of each skill whose directory is prefixed `aki` — these are the installed Aki skills.
|
|
16
16
|
3. List `~/.claude/agents/` and read the frontmatter (`name` / `description` / `tools` / `model`) of each file prefixed `aki-` — these are the installed Aki agent definitions. The directory is shared with the user's own agents, so introduce only the `aki-` ones. If the directory does not exist, that layer is simply not installed: drop the section rather than describing it.
|
|
17
17
|
4. Render a compact overview with these sections:
|
|
18
18
|
|
|
19
19
|
- **Skills (active, user-invoked)** — one row per aki-skill: its `/name`, its one-line description (from frontmatter), and when to reach for it.
|
|
20
20
|
- **Agent definitions (who the work gets handed to)** — one row per installed `aki-` agent from step 3: what it is for, and the property that is mechanical rather than promised (its `tools:` list, which is what makes a read-only agent actually read-only, and its `model:`, so a tier is never improvised). Say the thing people get wrong: this is a catalog, not a roster — an agent is spawned because a specific requirement needs it, never because it exists.
|
|
21
|
-
- **Passive system (akirule)** — explain the load mechanisms:
|
|
21
|
+
- **Passive system (akirule)** — explain the load mechanisms: the behavior floor and the router always loaded (`@`-imported by `CLAUDE.md`); every other rule read when the task's domain matches a route — by meaning, never keywords — and, on Claude Code, enforced for artifact routes by the `aki-route-guard` hook (the first edit of a code file, `.md`, `CHANGELOG.md`, `.vue`, `.rs`, `.sql` … is denied until its rule was read); full load when the owner asks for the whole corpus. `akirule` is hidden from the `/` menu by design (`user-invocable: false`) — it is imported, not a command.
|
|
22
22
|
- **One brain, three modes** — `METHOD-deep-think.md` is read passively by the router inside normal tasks (brief, inline, at most one clarifying question), as a triggered self-run when an `agent.A3` trigger holds or `/akithink` self-runs on a genuine decision (non-interactive, depth scaled to difficulty, ends in decide-and-report or escalation), and interactively by `/akithink` when the owner asks for a session. Short version of the comparison, not the full METHOD text.
|
|
23
23
|
- **Editing rules** — this whole system is generated from a source repo (akidevrule); the installed copies under `~/.aki/akidevrule` and `~/.claude` are deployed output, never edited directly. Changes go through the source repo + `install.sh`. Note for context: the same skill corpus (not the rule corpus) is also synced by `install.sh` to Antigravity/Gemini and to Codex, Kiro, and Grok CLIs on this machine if present — this skill itself only introduces the Claude Code side.
|
|
24
24
|
|
|
@@ -42,6 +42,6 @@ Invoke with `/akihelp`, or whenever the user asks, in any wording, what this set
|
|
|
42
42
|
| An analysis in chat is too dense to read as text | `/akihtmlreport` | Renders the analysis already in the conversation as one self-contained HTML file — it visualizes, it does not re-analyze |
|
|
43
43
|
| Unsure whether a rule loaded at all | Read the `[RULES]` line, or ask to load the whole corpus | Every response carries a `[RULES]` receipt naming every rule file in context, so "the rule never arrived" (absent from the line) is visibly different from "the rule arrived and was ignored" — the two have opposite fixes. A full-load request reads everything |
|
|
44
44
|
|
|
45
|
-
6. Close with the one caveat that changes how people use all of the above: **the router is guaranteed, a routed file is not** — `
|
|
45
|
+
6. Close with the one caveat that changes how people use all of the above: **the router is guaranteed, a routed file is not** — `RULE-agent-behavior.md` and the router are `@`-imported through `CLAUDE.md`; a routed file enters context when the model `Read`s it — enforced by the route-gate hook when the task edits a matching artifact, best-effort on meaning-only routes. When something on a meaning-only route must be deterministic, name the file in the prompt (*"Read `~/.aki/akidevrule/RULE-ui-pattern.md`, then …"*) instead of trusting the signal to fire.
|
|
46
46
|
|
|
47
47
|
7. Keep the output scannable: compact tables or short bulleted sections, not an essay. Respond in the user's language, and translate the example prompts into that language rather than pasting them verbatim in English.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akiopen
|
|
3
|
+
description: Session-opening brief — what is still pending in this project, and nothing else. Use whenever a session opens on a project or the owner asks, in any wording, what is unfinished, what the active plans or notes say, what an inbox file still lists, or what to pick up next. Read-only; reports only what needs doing, in a fixed four-field shape, ranked by severity.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# akiopen — what is pending here
|
|
8
|
+
|
|
9
|
+
Opening a project means rebuilding the picture of unfinished work from five places nobody should have to remember (`ux.A2`). This skill reads all five in one pass and reports only what still needs doing. It is an audit (`agent.B5`): no file edits, no git mutation, no note or plan marked done.
|
|
10
|
+
|
|
11
|
+
## Scan — one batched pass, only surfaces that exist
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git status --short && git diff --stat | tail -1
|
|
15
|
+
python3 ~/.claude/skills/akidevsync-notes/scripts/notes_cli.py .akidevsync/notes.json list --pending --detail
|
|
16
|
+
grep -c '^\s*- \[ \]' docs/plan/*.md docs/*.md /dev/null 2>/dev/null | grep -v ':0$'
|
|
17
|
+
awk '/^## \[Unreleased\]/{f=1;next} /^## \[/{f=0} f' CHANGELOG.md | grep -c '^- '
|
|
18
|
+
grep -rn -i 'needs owner\|needs mac\|manual test\|unverified' docs/plan/*.md 2>/dev/null
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Surface | Pending means |
|
|
22
|
+
|---|---|
|
|
23
|
+
| Working tree | modified or untracked files; group them by directory, not one line per file |
|
|
24
|
+
| Notes | `.akidevsync/notes.json` tasks with `done: false`; pinned first |
|
|
25
|
+
| Active plans | `docs/plan/*.md` outside `done/` with open `- [ ]` items, or a plan whose text says done but still sits outside `done/` (`docs.B1`) |
|
|
26
|
+
| Inbox files | any top-level `docs/*.md` with open `- [ ]` items — tasks pushed in from an upstream standard or another repo |
|
|
27
|
+
| Release | `[Unreleased]` has entries, or the tree changed with no entry yet (`release.A`) |
|
|
28
|
+
| Hand-offs | a line in an active plan waiting on the owner or another machine (`coding.B3`) |
|
|
29
|
+
|
|
30
|
+
A surface the project does not have is skipped silently. A project `CLAUDE.md` may name additional inbox or plan paths; read it before scanning.
|
|
31
|
+
|
|
32
|
+
## Report — pending only, never an inventory
|
|
33
|
+
|
|
34
|
+
- One item per pending thing, at most seven, ranked by severity (`ux.C1`); the rest collapse into one count line.
|
|
35
|
+
- Each item carries four fields: **problem** (what is pending, `path:line`) · **why it is still open** (from the plan text, note age, git dates — measured, never guessed; write *unclear* when the sources do not say) · **proposal** (one sentence) · **goal** (what closes it).
|
|
36
|
+
- Nothing pending: one line, nothing else.
|
|
37
|
+
- Half-finished work that cannot be told from an abandoned experiment is reported as unclassified and asked about, never sorted by guess (`agent.B5`).
|
|
38
|
+
- The report is the deliverable. Fixing anything, including moving a finished plan to `done/`, is a separate request.
|