@akinet/akidevrule 3.0.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 +835 -0
- package/LICENSE +21 -0
- package/README.md +356 -0
- package/claude/CLAUDE.md +40 -0
- package/claude/agents/aki-challenger.md +38 -0
- package/claude/agents/aki-conduct.md +54 -0
- package/claude/agents/aki-hands.md +59 -0
- package/claude/agents/aki-judge.md +37 -0
- package/claude/agents/aki-maker.md +36 -0
- package/claude/fragments/settings.akidoc.fragment.json +15 -0
- package/claude/hooks/aki-update-check.mjs +160 -0
- package/claude/hooks/aki_version_check.mjs +83 -0
- package/docs/ref/macos-codesign-tcc.md +59 -0
- package/install.mjs +1067 -0
- package/install.ps1 +11 -0
- package/install.sh +12 -0
- package/package.json +52 -0
- package/payload/GEMINI.md +147 -0
- package/payload/METHOD-audit-flow.md +147 -0
- package/payload/METHOD-audit-subtraction.md +67 -0
- package/payload/METHOD-audit-zero-trust.md +49 -0
- package/payload/METHOD-deep-think.md +172 -0
- package/payload/METHOD-proportionality.md +62 -0
- package/payload/METHOD-ux-psych.md +60 -0
- package/payload/RULE-agent-behavior.md +138 -0
- package/payload/RULE-biz.md +51 -0
- package/payload/RULE-coding.md +130 -0
- package/payload/RULE-content-write.md +54 -0
- package/payload/RULE-db-design.md +26 -0
- package/payload/RULE-docs.md +144 -0
- package/payload/RULE-pattern-core.md +80 -0
- package/payload/RULE-release.md +215 -0
- package/payload/RULE-seo.md +173 -0
- package/payload/RULE-stack-akiNuxtCf.md +179 -0
- package/payload/RULE-stack-tauri.md +59 -0
- package/payload/RULE-ui-pattern.md +167 -0
- package/payload/index.md +91 -0
- package/skills/aki-article-writer/SKILL.md +50 -0
- package/skills/aki-article-writer/references/article-workflow.md +377 -0
- package/skills/akidevsync-notes/SKILL.md +48 -0
- package/skills/akidevsync-notes/scripts/notes_cli.py +212 -0
- package/skills/akiflow/SKILL.md +221 -0
- package/skills/akiflow/references/harness-facts.md +215 -0
- package/skills/akiflow/scripts/council-cost.sh +4 -0
- package/skills/akiflow/scripts/council-open.sh +4 -0
- package/skills/akiflow/scripts/council-read.sh +4 -0
- package/skills/akiflow/scripts/council-verify.sh +4 -0
- package/skills/akiflow/scripts/council_cost.py +149 -0
- package/skills/akiflow/scripts/council_open.py +323 -0
- package/skills/akiflow/scripts/council_read.py +148 -0
- package/skills/akiflow/scripts/council_verify.py +315 -0
- package/skills/akiflow/scripts/scythe.py +307 -0
- package/skills/akiflow/scripts/scythe.sh +4 -0
- package/skills/akigitcommit/SKILL.md +85 -0
- package/skills/akihelp/SKILL.md +47 -0
- package/skills/akihtmlreport/SKILL.md +59 -0
- package/skills/akilint/SKILL.md +29 -0
- package/skills/akirule/SKILL.md +155 -0
- package/skills/akiship/SKILL.md +55 -0
- package/skills/akithink/SKILL.md +59 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# aki-article-writer — Full Six-Phase Pipeline
|
|
2
|
+
|
|
3
|
+
This document is the complete operating procedure for the **Article Worker subagent**.
|
|
4
|
+
The Article Worker reads this file in full before beginning any work.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Phase 1 — Research & Fact-Verification
|
|
9
|
+
|
|
10
|
+
**Goal:** Collect verified information. Zero hallucination.
|
|
11
|
+
|
|
12
|
+
### 1.1 Source collection
|
|
13
|
+
|
|
14
|
+
Use `search_web` to gather information from at least **two independent authoritative sources** for every major claim. Authority by project domain:
|
|
15
|
+
|
|
16
|
+
| Domain | Authoritative sources |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `kinhdich` / `tuvi` | Classical texts (I Ching, Tử Vi Bình Chú), recognised scholars with wide citations |
|
|
19
|
+
| `vstshop` | Manufacturer website, official release notes (FabFilter, Spectrasonics, Native Instruments…) |
|
|
20
|
+
| `akinet` / `akitao` | Official API docs, RFC, vendor technical documentation |
|
|
21
|
+
| General | Wikipedia (as a pointer to primary sources, not the source itself), reputable publications |
|
|
22
|
+
|
|
23
|
+
### 1.2 Claim classification
|
|
24
|
+
|
|
25
|
+
Label every load-bearing statement before writing:
|
|
26
|
+
|
|
27
|
+
| Tag | Meaning | Requirement |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `FACT` | Verified across 2+ independent sources | Cite sources inline in draft notes |
|
|
30
|
+
| `ANALYSIS` | Author's reasoned interpretation | Signal in text: "Có thể thấy rằng…" / "According to this reading…" |
|
|
31
|
+
| `UNVERIFIED` | Cannot be confirmed right now | **Do not include in the published article** |
|
|
32
|
+
|
|
33
|
+
A mislabelled FACT that is actually an ASSUMPTION is the one unrecoverable error in this phase.
|
|
34
|
+
|
|
35
|
+
### 1.3 File vs chat separation
|
|
36
|
+
|
|
37
|
+
Article content must be context-independent and durable. Do not copy conversation wording, task-specific shorthand, or ephemeral discussion into the published file.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Phase 2 — Metadata & JSON-LD Schema
|
|
42
|
+
|
|
43
|
+
**Execute before writing body content.** Everything downstream inherits the terminology and framing defined here.
|
|
44
|
+
|
|
45
|
+
### 2.1 Slug
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Format: lowercase, hyphen-separated, no diacritics
|
|
49
|
+
Example: giai-ma-que-thuan-can
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2.2 Meta title
|
|
53
|
+
|
|
54
|
+
- ≤ 60 characters total (articles/knowledge/post slug pages: ≤ 80 chars)
|
|
55
|
+
- **Do NOT include the brand name** — the framework appends ` | BrandName` automatically; including it produces a double suffix
|
|
56
|
+
- Focus keyphrase at the start
|
|
57
|
+
- No em-dash (`—`) or en-dash (`–`) — use `|` or `-` instead
|
|
58
|
+
- Define once as a `const`; pass the same variable to OG, Twitter card, and JSON-LD — never repeat the literal string
|
|
59
|
+
|
|
60
|
+
### 2.3 Meta description
|
|
61
|
+
|
|
62
|
+
- ≤ 155 characters
|
|
63
|
+
- Structure: `[Action verb] + [Focus keyphrase] + [User benefit]`
|
|
64
|
+
- Example: "Khám phá quẻ Thuần Càn trong Kinh Dịch — ý nghĩa 6 hào, ứng dụng phong thủy và bài học lãnh đạo đích thực."
|
|
65
|
+
|
|
66
|
+
### 2.4 JSON-LD schema — type matrix
|
|
67
|
+
|
|
68
|
+
Select the correct schema type based on project:
|
|
69
|
+
|
|
70
|
+
| Content type | Required schemas | Key fields |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| Blog / news | `BlogPosting` + `Person` + `Organization` + `BreadcrumbList` | `headline`, `author`, `publisher`, `datePublished`, `image` |
|
|
73
|
+
| Knowledge / glossary (kinhdich, tuvi) | `Article` + `DefinedTerm` + `DefinedTermSet` + `BreadcrumbList` | `definedTermCode`, `inDefinedTermSet` |
|
|
74
|
+
| Product (vstshop) | `Product` + `Organization` + `Offer` + `BreadcrumbList` | `price`, `priceCurrency`, `availability` |
|
|
75
|
+
| Service / feature | `Service` + `Organization` + `BreadcrumbList` | `serviceType`, `provider` |
|
|
76
|
+
|
|
77
|
+
**Organization block — required on every page:**
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"@type": "Organization",
|
|
82
|
+
"name": "BrandName",
|
|
83
|
+
"alternateName": ["Brand Name", "brandname", "brandname.com"],
|
|
84
|
+
"url": "https://domain.com/",
|
|
85
|
+
"logo": "https://domain.com/favicon/icon-192.png",
|
|
86
|
+
"sameAs": ["https://facebook.com/...", "https://wikidata.org/..."],
|
|
87
|
+
"knowsAbout": ["Topic 1", "Topic 2"]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
> **FAQPage schema (2026 note):** Google retired FAQ rich results in May 2026. Do not write an FAQ block solely to emit schema. Instead, write H2s phrased as questions — AI crawlers (Perplexity, ChatGPT, Gemini) cite paragraphs following question-phrased H2s at roughly double the rate of JSON-LD FAQPage entries. Existing FAQPage markup may be kept (it causes no harm), but never create it as a new SEO deliverable.
|
|
92
|
+
|
|
93
|
+
### 2.5 URL canonical & trailing slash
|
|
94
|
+
|
|
95
|
+
All canonical URLs, sitemap entries, `og:url`, internal links, and JSON-LD `url` fields must end with `/`. Required for Cloudflare Pages compatibility.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Phase 3 — Content Writing & UX Psychology
|
|
100
|
+
|
|
101
|
+
### 3.1 Heading structure
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
H1 — exactly one per article; contains the focus keyphrase
|
|
105
|
+
H2 — 3–5 main sections
|
|
106
|
+
H3 — sub-detail within an H2 (only when genuinely needed)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Write at least one H2 as a direct question (ending with `?`). AI crawlers are roughly twice as likely to cite a passage when it follows a question-shaped heading.
|
|
110
|
+
|
|
111
|
+
### 3.2 Cognitive budget — opening sentences
|
|
112
|
+
|
|
113
|
+
Users scan web content; they do not read linearly. Every paragraph opening and every FAQ answer must lead with the core answer in the first sentence (Subject + Verb + Predicate). No warm-up.
|
|
114
|
+
|
|
115
|
+
**Banned openers:**
|
|
116
|
+
- "Đây là…" / "This is…"
|
|
117
|
+
- "Trong bài viết này…" / "In this article…"
|
|
118
|
+
- "Theo như chúng ta đã biết…" / "As we all know…"
|
|
119
|
+
- "According to…" at the start of a paragraph
|
|
120
|
+
|
|
121
|
+
**Paragraph length:** ≤ 5 lines. If longer, split or convert to a list.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
```
|
|
125
|
+
❌ "Trong phần này, chúng ta sẽ cùng tìm hiểu về những ứng dụng thú vị…"
|
|
126
|
+
✅ "Quẻ Thuần Càn chỉ dẫn 3 nguyên tắc lãnh đạo: kiên trì, thuận thời và học hỏi không ngừng."
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 3.3 Semantic stability
|
|
130
|
+
|
|
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
|
+
|
|
133
|
+
### 3.4 Vietnamese dual-coverage (vi locale)
|
|
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
|
|
143
|
+
|
|
144
|
+
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
|
+
|
|
146
|
+
| Anxiety | Answer at the CTA |
|
|
147
|
+
|---|---|
|
|
148
|
+
| Price / lock-in | "Dùng thử miễn phí 14 ngày, không cần thẻ tín dụng" |
|
|
149
|
+
| Complexity | "Tư vấn 1-1 miễn phí trong 15 phút" |
|
|
150
|
+
| Compatibility | "Hỗ trợ Win/Mac, tương thích mọi DAW phổ biến" |
|
|
151
|
+
| Privacy | "Không lưu dữ liệu cá nhân, xoá tài khoản bất cứ lúc nào" |
|
|
152
|
+
|
|
153
|
+
### 3.6 Internal & external links
|
|
154
|
+
|
|
155
|
+
- **Internal links:** ≥ 2 links to related articles or service pages within the same project
|
|
156
|
+
- **External links:** link to authoritative sources when citing data; add `rel="noopener"`
|
|
157
|
+
|
|
158
|
+
### 3.7 SSR / prerender requirement
|
|
159
|
+
|
|
160
|
+
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
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Phase 4 — Image Scout Pipeline
|
|
165
|
+
|
|
166
|
+
Spawn a separate **Image Scout subagent** with model Gemini Flash or Claude Haiku.
|
|
167
|
+
|
|
168
|
+
### Step 0 — Resolve `article_arch` before spawning
|
|
169
|
+
|
|
170
|
+
Check the render path, not just `CLAUDE.md` prose: open the component/template that actually renders an article body (e.g. `ContentArticle.vue`, an MDX renderer, a markdown-to-HTML pipeline) and confirm whether it parses inline markdown image syntax `![]()` inside body content.
|
|
171
|
+
|
|
172
|
+
- **Renderer parses `![]()` or reads a per-block `image` field → `article_arch: markdown`.** Multiple images (hero + N body) are safe. Proceed with `count_needed: <1 hero + N body images>` as before.
|
|
173
|
+
- **Renderer does NOT parse `![]()`** (e.g. a hand-written `renderMarkdown()` that only handles bold/link/code, with no image field in the content schema) **→ `article_arch: component`, single-image mode.** `count_needed` is forced to **1**. Do not generate body images the renderer cannot display — they would ship as literal, broken `` text.
|
|
174
|
+
|
|
175
|
+
**Brief to pass:**
|
|
176
|
+
```
|
|
177
|
+
{
|
|
178
|
+
"slug": "<article-slug>",
|
|
179
|
+
"topic": "<article topic in plain language>",
|
|
180
|
+
"article_arch": "markdown" | "component",
|
|
181
|
+
"count_needed": <1 hero + N body images, or exactly 1 if article_arch is "component">,
|
|
182
|
+
"output_dir": "<project image directory, e.g. public/images/articles/>",
|
|
183
|
+
"format": "webp" // or "jpg" per project config
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The Image Scout executes the following seven steps:
|
|
188
|
+
|
|
189
|
+
### Step 1 — Search (`search_web`)
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
Query patterns:
|
|
193
|
+
"<topic> cover art official high resolution"
|
|
194
|
+
"<topic> image 16:9 landscape"
|
|
195
|
+
"<product name> <brand> official product image square"
|
|
196
|
+
|
|
197
|
+
Preferred sources:
|
|
198
|
+
Manufacturer / official rights-holder website
|
|
199
|
+
Unsplash, Pexels (CC0 / royalty-free)
|
|
200
|
+
Wikimedia Commons
|
|
201
|
+
|
|
202
|
+
Disqualified sources:
|
|
203
|
+
Images bearing a third-party watermark
|
|
204
|
+
Images showing a competitor's logo or branding
|
|
205
|
+
Images with visible text unrelated to the topic
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Collect 3–5 candidate image URLs.
|
|
209
|
+
|
|
210
|
+
### Step 2 — Download (`curl`)
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
curl -sSL \
|
|
214
|
+
-A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" \
|
|
215
|
+
"<IMAGE_URL>" \
|
|
216
|
+
-o /tmp/raw-<slug>-<index>.jpg
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The browser User-Agent string is required — many CDNs block requests without it.
|
|
220
|
+
|
|
221
|
+
### Step 3 — Visual inspection before processing (`view_file`)
|
|
222
|
+
|
|
223
|
+
**Mandatory:** call `view_file` on `/tmp/raw-<slug>-<index>.jpg` for every candidate.
|
|
224
|
+
|
|
225
|
+
Reject if any of the following is true:
|
|
226
|
+
- [ ] Wrong subject / wrong product version
|
|
227
|
+
- [ ] Blurry, pixelated, or heavily compression-artefacted
|
|
228
|
+
- [ ] Visible third-party watermark
|
|
229
|
+
- [ ] Competitor logo or branding visible
|
|
230
|
+
- [ ] Key content is not centred (will be lost in square or 16:9 crop)
|
|
231
|
+
|
|
232
|
+
If a candidate fails → go back to Step 1 and find a replacement URL.
|
|
233
|
+
|
|
234
|
+
### Step 4 — File naming (slug convention)
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
Multi-image mode (article_arch: markdown):
|
|
238
|
+
Pattern: <article-slug>-<zero-padded-index>.<ext>
|
|
239
|
+
Hero: giai-ma-que-thuan-can-01.webp
|
|
240
|
+
Body 1: giai-ma-que-thuan-can-02.webp
|
|
241
|
+
Body 2: giai-ma-que-thuan-can-03.webp
|
|
242
|
+
|
|
243
|
+
Single-image mode (article_arch: component, count_needed: 1):
|
|
244
|
+
Pattern: <article-slug>.<ext>
|
|
245
|
+
File: giai-ma-que-thuan-can.webp
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Single-image mode always writes to `public/images/articles/<slug>.<ext>` regardless of the project's `image_dir` fallback default — this is the one path the record's `ogImage`/share-image field and the rendered hero both point to (see Phase 5).
|
|
249
|
+
|
|
250
|
+
### Step 5 — Processing (`ffmpeg` / `sips`)
|
|
251
|
+
|
|
252
|
+
**Option A — `ffmpeg` (recommended, most accurate):**
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# Hero / OG image — 1200×630 px, 16:9 center crop
|
|
256
|
+
ffmpeg -i /tmp/raw-<slug>-01.jpg \
|
|
257
|
+
-vf "crop='min(iw,ih*16/9)':'min(ih,iw*9/16)',scale=1200:630:flags=lanczos" \
|
|
258
|
+
-q:v 2 <output_dir>/<slug>-01.webp -y
|
|
259
|
+
|
|
260
|
+
# Body image — 800 px width, original ratio preserved
|
|
261
|
+
ffmpeg -i /tmp/raw-<slug>-02.jpg \
|
|
262
|
+
-vf "scale=800:-1:flags=lanczos" \
|
|
263
|
+
-q:v 80 <output_dir>/<slug>-02.webp -y
|
|
264
|
+
|
|
265
|
+
# Square product image — 800×800 px, center crop (for vstshop-style)
|
|
266
|
+
ffmpeg -i /tmp/raw-<slug>-01.jpg \
|
|
267
|
+
-vf "crop='min(iw,ih)':'min(iw,ih)',scale=800:800:flags=lanczos" \
|
|
268
|
+
-q:v 2 <output_dir>/<slug>-01.webp -y
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Option B — `sips` (macOS native fallback):**
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# Resize to 1200 px wide first
|
|
275
|
+
sips --resampleWidth 1200 /tmp/raw-<slug>-01.jpg --out /tmp/resized-<slug>-01.jpg
|
|
276
|
+
# Crop 1200×630 from centre
|
|
277
|
+
sips --cropToHeightWidth 630 1200 /tmp/resized-<slug>-01.jpg --out <output_dir>/<slug>-01.jpg
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Size and weight targets:**
|
|
281
|
+
|
|
282
|
+
| Image role | Dimensions | Max file size |
|
|
283
|
+
|---|---|---|
|
|
284
|
+
| Hero / OG (16:9) | 1200 × 630 px | < 150 KB |
|
|
285
|
+
| Square product cover | 800 × 800 px | < 120 KB |
|
|
286
|
+
| Body / inline | Width 800 px | < 90 KB |
|
|
287
|
+
|
|
288
|
+
### Step 6 — Visual inspection after processing (`view_file`)
|
|
289
|
+
|
|
290
|
+
**Mandatory:** call `view_file` on the output file.
|
|
291
|
+
|
|
292
|
+
Confirm:
|
|
293
|
+
- [ ] File opens and displays correctly (not corrupt)
|
|
294
|
+
- [ ] Correct dimensions (width and height match spec)
|
|
295
|
+
- [ ] No important content clipped by the crop
|
|
296
|
+
- [ ] Acceptable visual quality
|
|
297
|
+
|
|
298
|
+
### Step 7 — Cleanup and handoff
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
rm /tmp/raw-<slug>-*.jpg /tmp/resized-<slug>-*.jpg 2>/dev/null
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Return structured result to Article Worker:
|
|
305
|
+
```
|
|
306
|
+
hero: { file: "<slug>-01.webp", alt: "<SEO alt text containing focus keyphrase>" }
|
|
307
|
+
body: [
|
|
308
|
+
{ file: "<slug>-02.webp", alt: "..." },
|
|
309
|
+
{ file: "<slug>-03.webp", alt: "..." }
|
|
310
|
+
]
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Phase 5 — Image Embed
|
|
316
|
+
|
|
317
|
+
Article Worker receives the Image Scout's result. Embed method depends on `article_arch` resolved in Phase 4 Step 0 — never default to markdown syntax without checking.
|
|
318
|
+
|
|
319
|
+
**Alt text rules (both paths):**
|
|
320
|
+
- Must contain the focus keyphrase
|
|
321
|
+
- Must accurately describe the image content (no keyword stuffing)
|
|
322
|
+
- ≤ 125 characters
|
|
323
|
+
|
|
324
|
+
### `article_arch: markdown`
|
|
325
|
+
|
|
326
|
+
Embed with markdown image syntax directly in body content:
|
|
327
|
+
```markdown
|
|
328
|
+

|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### `article_arch: component` (single-image mode)
|
|
332
|
+
|
|
333
|
+
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>`.
|
|
335
|
+
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
|
+
3. No separate body images in this mode; one image serves as both hero and OG/share image.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Phase 6 — Self-Audit & Delivery
|
|
341
|
+
|
|
342
|
+
Article Worker runs through the full checklist before reporting completion.
|
|
343
|
+
|
|
344
|
+
### 6.1 Meta & technical SEO
|
|
345
|
+
- [ ] Meta title ≤ 60 chars (≤ 80 for article/knowledge slug pages)
|
|
346
|
+
- [ ] Meta title: no brand suffix, no em-dash, no en-dash
|
|
347
|
+
- [ ] Meta description ≤ 155 chars, starts with an action verb
|
|
348
|
+
- [ ] Canonical URL and all internal links end with `/`
|
|
349
|
+
- [ ] Exactly one H1
|
|
350
|
+
- [ ] JSON-LD valid; Organization block has `alternateName`, `knowsAbout`, `sameAs`
|
|
351
|
+
- [ ] Focus keyphrase appears in: H1, Meta title, Meta description, first paragraph opening, ≥ 1 H2
|
|
352
|
+
|
|
353
|
+
### 6.2 Content quality
|
|
354
|
+
- [ ] No `UNVERIFIED` claim appears in the published text
|
|
355
|
+
- [ ] No banned openers in any paragraph or FAQ answer
|
|
356
|
+
- [ ] No paragraph exceeds 5 lines
|
|
357
|
+
- [ ] Unaccented Vietnamese keyword embedded in parentheses at first occurrence in body / FAQ (vi locale only)
|
|
358
|
+
- [ ] CTA point has an anxiety-answering line
|
|
359
|
+
- [ ] ≥ 1 H2 is a direct question (ends with `?`)
|
|
360
|
+
- [ ] ≥ 2 internal links
|
|
361
|
+
- [ ] External links carry `rel="noopener"`
|
|
362
|
+
|
|
363
|
+
### 6.3 Image
|
|
364
|
+
- [ ] Hero image exists at the correct path, correct dimensions, visually sharp
|
|
365
|
+
- [ ] `article_arch` was resolved from the actual renderer, not assumed
|
|
366
|
+
- [ ] If `markdown`: all body images exist, correct spec, filenames follow slug-index convention (`<slug>-01.webp`, etc.), every `![]()` has a focus-keyphrase alt text ≤ 125 chars
|
|
367
|
+
- [ ] If `component` (single-image mode): exactly one image at `public/images/articles/<slug>.<ext>`, no `![]()` written into any body/paragraph field, the record's `ogImage`/share-image field points to that same file
|
|
368
|
+
- [ ] `/tmp/` scratch files removed
|
|
369
|
+
|
|
370
|
+
### 6.4 SSR / prerender
|
|
371
|
+
- [ ] Article content, meta tags, and schema are present in server-rendered HTML, not deferred to client-side JS
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Antigravity / AGY note
|
|
376
|
+
|
|
377
|
+
AGY has no native subagent spawn mechanism. When running under AGY (CLI or IDE), execute phases 1–6 sequentially in a single session. For Phase 4, open the Image Scout as a separate AGY conversation and paste its output back into the main session. This trades true isolation for sequential clarity — the content quality rules are unchanged.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akidevsync-notes
|
|
3
|
+
description: Read and edit a project's `.akidevsync/notes.json` task/note file — the per-project task list written by the Aki-Dev-Sync app (github.com/lacvietanh/aki-dev-sync). Use when the user asks to list, add, pin/unpin, mark done, edit, or delete a task in that file, or mentions "task note", "note ghim", "pin task", "mark done", "notes.json", "akidevsync task". Also use when asked to cross-check pinned/open notes against what a release actually shipped (CHANGELOG, code) before marking them done.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# akidevsync-notes — edit a project's Aki-Dev-Sync task file safely
|
|
7
|
+
|
|
8
|
+
The Aki-Dev-Sync app stores every project's task list at `<project>/.akidevsync/notes.json` (schema: `about`, `schema`, `notes`, `tasks[]`, `updated_at`; each task has `id`, `title`, `detail`, `done`, `pin`, `wish`, `created_at`, `updated_at` in epoch-ms). The file is normally gitignored (`.akidevsync/` — the app excludes it from PUSH/PULL by default too), so edits here are local-only and never show up in `git diff`.
|
|
9
|
+
|
|
10
|
+
**Never hand-edit the JSON with the Edit tool.** The app itself reads/writes this file, so any mutation must match its exact formatting (2-space indent, `ensure_ascii=False` so Vietnamese text stays literal, alphabetical per-task key order, a trailing newline) or the next diff a human looks at gets noisy for no reason. Always go through the bundled script:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
python3 ~/.claude/skills/akidevsync-notes/scripts/notes_cli.py <path-to-notes.json> <command> [args]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
(On Antigravity/Codex/Kiro/Grok, substitute that CLI's skills root — see `~/.aki/akidevrule/.source-repo`'s `README.md` for the list of sync targets.)
|
|
17
|
+
|
|
18
|
+
## Locating the file
|
|
19
|
+
|
|
20
|
+
`find <project-root> -maxdepth 2 -name notes.json -path '*/.akidevsync/*'` (or just check `<cwd>/.akidevsync/notes.json`). If it does not exist, the project has never opened in Aki-Dev-Sync, or task notes were never migrated — **do not create one uninvited**. Only run `... init` when the user explicitly asks to start tracking tasks for a project that doesn't use the app.
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
list [--pin] [--done] [--pending] [--wish] [--detail] # --detail also prints each task's detail body
|
|
26
|
+
add "<title>" [--detail "<text>"] [--pin] [--wish]
|
|
27
|
+
set <task-id> [--done true|false] [--pin true|false] [--wish true|false] [--title "<t>"] [--detail "<t>"]
|
|
28
|
+
delete <task-id> # permanent — confirm with the user first
|
|
29
|
+
note "<text>" # replaces the project-wide notes field (not a task)
|
|
30
|
+
init # only if the user explicitly wants a new file
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`set`/`delete` need the task's `id` (shown by `list`) — never guess it from the title, titles are not unique.
|
|
34
|
+
|
|
35
|
+
## Cross-checking pinned notes against a shipped release
|
|
36
|
+
|
|
37
|
+
A recurring use: the user pins a wish/bug note while working, ships a release, then asks "does what I fixed in vX.Y actually cover this note — mark it done." Work like this, not by pattern-matching titles alone:
|
|
38
|
+
|
|
39
|
+
1. `list --pin` (or `list --pending` for everything still open) to get the candidate set.
|
|
40
|
+
2. For each note, read its `detail` too — the real bug report is usually there, not in the short title, and is often the most reliable string to grep the CHANGELOG for.
|
|
41
|
+
3. Read the actual CHANGELOG entry (or `git log`) for the release, and where the claim is non-trivial, verify against the code itself (grep the relevant file/function) rather than trusting the changelog prose alone — changelog text can overstate what shipped.
|
|
42
|
+
4. If the changelog or code itself flags the fix as unverified/runtime-only/needs-owner-confirmation, **ask the user** whether they've actually confirmed it before marking done — do not mark done on an unverified claim just because the code changed.
|
|
43
|
+
5. Only `set <id> --done true` for notes you can point to a specific matching commit/changelog line/code diff for. Leave ambiguous or unmatched ones pinned and tell the user why, rather than silently leaving them out of the report.
|
|
44
|
+
6. Summarize what you marked done and why (one line per note, citing the matching change) so the user can sanity-check the batch rather than re-deriving it.
|
|
45
|
+
|
|
46
|
+
## What this skill does not do
|
|
47
|
+
|
|
48
|
+
It has no opinion on the app's UI, sync, or release process — it only edits `notes.json`. Never touch other files in `.akidevsync/`, never run git commands on this path (it's meant to stay untracked), and never bulk-delete tasks without the user naming them explicitly.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLI for reading/mutating an Aki-Dev-Sync `.akidevsync/notes.json` task file.
|
|
3
|
+
|
|
4
|
+
Schema (as written by the Aki-Dev-Sync app itself):
|
|
5
|
+
{ "about": "<url>", "schema": 1, "notes": "<free text>",
|
|
6
|
+
"tasks": [ { "id", "title", "detail", "done", "pin", "wish",
|
|
7
|
+
"created_at", "updated_at" } (ms epoch ints) ],
|
|
8
|
+
"updated_at": <ms epoch int> }
|
|
9
|
+
|
|
10
|
+
Every mutation preserves the on-disk key order (alphabetical per task,
|
|
11
|
+
declared order at root) so a diff against the app's own next write stays
|
|
12
|
+
quiet, and writes back with ensure_ascii=False (Vietnamese text unescaped)
|
|
13
|
+
and a trailing newline, matching the app's own formatting.
|
|
14
|
+
"""
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
TASK_KEY_ORDER = ["created_at", "detail", "done", "id", "pin", "title", "updated_at", "wish"]
|
|
22
|
+
ROOT_KEY_ORDER = ["about", "schema", "notes", "tasks", "updated_at"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def now_ms():
|
|
26
|
+
return int(time.time() * 1000)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load(path):
|
|
30
|
+
p = Path(path)
|
|
31
|
+
if not p.exists():
|
|
32
|
+
sys.exit(f"error: {path} does not exist (this project has no Aki-Dev-Sync task notes yet)")
|
|
33
|
+
with p.open(encoding="utf-8") as f:
|
|
34
|
+
return json.load(f)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def save(path, data):
|
|
38
|
+
data["updated_at"] = now_ms()
|
|
39
|
+
with Path(path).open("w", encoding="utf-8") as f:
|
|
40
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
41
|
+
f.write("\n")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def new_id(data):
|
|
45
|
+
existing = {t["id"] for t in data["tasks"]}
|
|
46
|
+
ts = now_ms()
|
|
47
|
+
tid = f"task-{ts}"
|
|
48
|
+
while tid in existing:
|
|
49
|
+
ts += 1
|
|
50
|
+
tid = f"task-{ts}"
|
|
51
|
+
return tid, ts
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def ordered_task(fields):
|
|
55
|
+
return {k: fields[k] for k in TASK_KEY_ORDER}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def find_task(data, task_id):
|
|
59
|
+
for t in data["tasks"]:
|
|
60
|
+
if t["id"] == task_id:
|
|
61
|
+
return t
|
|
62
|
+
sys.exit(f"error: no task with id {task_id!r}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cmd_list(args):
|
|
66
|
+
data = load(args.path)
|
|
67
|
+
tasks = data["tasks"]
|
|
68
|
+
if args.pin:
|
|
69
|
+
tasks = [t for t in tasks if t["pin"]]
|
|
70
|
+
if args.pending:
|
|
71
|
+
tasks = [t for t in tasks if not t["done"]]
|
|
72
|
+
if args.done:
|
|
73
|
+
tasks = [t for t in tasks if t["done"]]
|
|
74
|
+
if args.wish:
|
|
75
|
+
tasks = [t for t in tasks if t["wish"]]
|
|
76
|
+
for t in tasks:
|
|
77
|
+
mark = "x" if t["done"] else " "
|
|
78
|
+
pin = "📌" if t["pin"] else " "
|
|
79
|
+
print(f"[{mark}] {pin} {t['id']} {t['title']}")
|
|
80
|
+
if args.detail and t["detail"]:
|
|
81
|
+
for line in t["detail"].splitlines():
|
|
82
|
+
print(f" {line}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def cmd_add(args):
|
|
86
|
+
data = load(args.path)
|
|
87
|
+
tid, ts = new_id(data)
|
|
88
|
+
task = ordered_task({
|
|
89
|
+
"created_at": ts,
|
|
90
|
+
"detail": args.detail or "",
|
|
91
|
+
"done": False,
|
|
92
|
+
"id": tid,
|
|
93
|
+
"pin": bool(args.pin),
|
|
94
|
+
"title": args.title,
|
|
95
|
+
"updated_at": ts,
|
|
96
|
+
"wish": bool(args.wish),
|
|
97
|
+
})
|
|
98
|
+
data["tasks"].append(task)
|
|
99
|
+
save(args.path, data)
|
|
100
|
+
print(json.dumps(task, ensure_ascii=False, indent=2))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def cmd_set(args):
|
|
104
|
+
data = load(args.path)
|
|
105
|
+
t = find_task(data, args.task_id)
|
|
106
|
+
changed = False
|
|
107
|
+
if args.done is not None:
|
|
108
|
+
t["done"] = args.done
|
|
109
|
+
changed = True
|
|
110
|
+
if args.pin is not None:
|
|
111
|
+
t["pin"] = args.pin
|
|
112
|
+
changed = True
|
|
113
|
+
if args.wish is not None:
|
|
114
|
+
t["wish"] = args.wish
|
|
115
|
+
changed = True
|
|
116
|
+
if args.title is not None:
|
|
117
|
+
t["title"] = args.title
|
|
118
|
+
changed = True
|
|
119
|
+
if args.detail is not None:
|
|
120
|
+
t["detail"] = args.detail
|
|
121
|
+
changed = True
|
|
122
|
+
if not changed:
|
|
123
|
+
sys.exit("error: set needs at least one of --done/--pin/--wish/--title/--detail")
|
|
124
|
+
t["updated_at"] = now_ms()
|
|
125
|
+
save(args.path, data)
|
|
126
|
+
print(json.dumps(t, ensure_ascii=False, indent=2))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def cmd_delete(args):
|
|
130
|
+
data = load(args.path)
|
|
131
|
+
before = len(data["tasks"])
|
|
132
|
+
data["tasks"] = [t for t in data["tasks"] if t["id"] != args.task_id]
|
|
133
|
+
if len(data["tasks"]) == before:
|
|
134
|
+
sys.exit(f"error: no task with id {args.task_id!r}")
|
|
135
|
+
save(args.path, data)
|
|
136
|
+
print(f"deleted {args.task_id}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def cmd_note(args):
|
|
140
|
+
data = load(args.path)
|
|
141
|
+
data["notes"] = args.text
|
|
142
|
+
save(args.path, data)
|
|
143
|
+
print("notes field updated")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def cmd_init(args):
|
|
147
|
+
p = Path(args.path)
|
|
148
|
+
if p.exists():
|
|
149
|
+
sys.exit(f"error: {args.path} already exists — refusing to overwrite")
|
|
150
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
data = {k: v for k, v in {
|
|
152
|
+
"about": "https://github.com/lacvietanh/aki-dev-sync",
|
|
153
|
+
"schema": 1,
|
|
154
|
+
"notes": "",
|
|
155
|
+
"tasks": [],
|
|
156
|
+
"updated_at": now_ms(),
|
|
157
|
+
}.items()}
|
|
158
|
+
data = {k: data[k] for k in ROOT_KEY_ORDER}
|
|
159
|
+
save(args.path, data)
|
|
160
|
+
print(f"created {args.path}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_bool(s):
|
|
164
|
+
return s.lower() in ("1", "true", "yes", "on")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main():
|
|
168
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
169
|
+
ap.add_argument("path", help="path to .akidevsync/notes.json")
|
|
170
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
171
|
+
|
|
172
|
+
p_list = sub.add_parser("list", help="list tasks")
|
|
173
|
+
p_list.add_argument("--pin", action="store_true", help="only pinned tasks")
|
|
174
|
+
p_list.add_argument("--done", action="store_true", help="only done tasks")
|
|
175
|
+
p_list.add_argument("--pending", action="store_true", help="only not-done tasks")
|
|
176
|
+
p_list.add_argument("--wish", action="store_true", help="only wishlist tasks")
|
|
177
|
+
p_list.add_argument("--detail", action="store_true", help="also print each task's detail")
|
|
178
|
+
p_list.set_defaults(func=cmd_list)
|
|
179
|
+
|
|
180
|
+
p_add = sub.add_parser("add", help="add a new task")
|
|
181
|
+
p_add.add_argument("title")
|
|
182
|
+
p_add.add_argument("--detail", default="")
|
|
183
|
+
p_add.add_argument("--pin", action="store_true")
|
|
184
|
+
p_add.add_argument("--wish", action="store_true")
|
|
185
|
+
p_add.set_defaults(func=cmd_add)
|
|
186
|
+
|
|
187
|
+
p_set = sub.add_parser("set", help="update fields on an existing task")
|
|
188
|
+
p_set.add_argument("task_id")
|
|
189
|
+
p_set.add_argument("--done", type=parse_bool, default=None)
|
|
190
|
+
p_set.add_argument("--pin", type=parse_bool, default=None)
|
|
191
|
+
p_set.add_argument("--wish", type=parse_bool, default=None)
|
|
192
|
+
p_set.add_argument("--title", default=None)
|
|
193
|
+
p_set.add_argument("--detail", default=None)
|
|
194
|
+
p_set.set_defaults(func=cmd_set)
|
|
195
|
+
|
|
196
|
+
p_del = sub.add_parser("delete", help="remove a task permanently")
|
|
197
|
+
p_del.add_argument("task_id")
|
|
198
|
+
p_del.set_defaults(func=cmd_delete)
|
|
199
|
+
|
|
200
|
+
p_note = sub.add_parser("note", help="replace the project-wide notes text")
|
|
201
|
+
p_note.add_argument("text")
|
|
202
|
+
p_note.set_defaults(func=cmd_note)
|
|
203
|
+
|
|
204
|
+
p_init = sub.add_parser("init", help="create a new empty notes.json (only if missing)")
|
|
205
|
+
p_init.set_defaults(func=cmd_init)
|
|
206
|
+
|
|
207
|
+
args = ap.parse_args()
|
|
208
|
+
args.func(args)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
main()
|