@cleocode/skills 2026.5.83 → 2026.5.86

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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/skills/_shared/__tests__/lifecycle-protocol-reconcile.test.ts +112 -0
  3. package/skills/_shared/__tests__/loom-adr-links.test.ts +163 -0
  4. package/skills/_shared/__tests__/loom-stage-coverage.test.ts +167 -0
  5. package/skills/ct-adr-recorder/SKILL.md +92 -0
  6. package/skills/ct-adr-recorder/__tests__/skill-adr-recorder.test.ts +65 -0
  7. package/skills/ct-consensus-voter/SKILL.md +14 -0
  8. package/skills/ct-contribution/SKILL.md +80 -0
  9. package/skills/ct-docs-lookup/SKILL.md +116 -1
  10. package/skills/ct-docs-lookup/references/ctx7-workflow.md +198 -0
  11. package/skills/ct-docs-lookup/references/library-id-resolution.md +217 -0
  12. package/skills/ct-docs-lookup/references/version-specific-docs.md +220 -0
  13. package/skills/ct-docs-review/SKILL.md +133 -1
  14. package/skills/ct-docs-review/__tests__/skill-docs-review.test.ts +53 -0
  15. package/skills/ct-docs-review/references/inline-comment-patterns.md +268 -0
  16. package/skills/ct-docs-review/references/pr-review-mode.md +270 -0
  17. package/skills/ct-docs-review/references/style-violations.md +341 -0
  18. package/skills/ct-docs-write/SKILL.md +157 -1
  19. package/skills/ct-docs-write/__tests__/skill-docs-write.test.ts +55 -0
  20. package/skills/ct-docs-write/references/audience-targeting.md +305 -0
  21. package/skills/ct-docs-write/references/cleo-style-guide.md +234 -0
  22. package/skills/ct-docs-write/references/markdown-patterns.md +329 -0
  23. package/skills/ct-documentor/SKILL.md +11 -0
  24. package/skills/ct-documentor/references/anti-patterns.md +216 -0
  25. package/skills/ct-documentor/references/chain-orchestration.md +194 -0
  26. package/skills/ct-documentor/references/doc-types-and-templates.md +301 -0
  27. package/skills/ct-documentor/references/style-coordination.md +195 -0
  28. package/skills/ct-epic-architect/SKILL.md +15 -0
  29. package/skills/ct-ivt-looper/SKILL.md +32 -0
  30. package/skills/ct-release-orchestrator/SKILL.md +16 -0
  31. package/skills/ct-research-agent/SKILL.md +24 -0
  32. package/skills/ct-research-agent/references/anti-patterns.md +154 -0
  33. package/skills/ct-research-agent/references/citation-and-evidence.md +140 -0
  34. package/skills/ct-research-agent/references/source-strategy.md +116 -0
  35. package/skills/ct-research-agent/references/triggers-and-routing.md +93 -0
  36. package/skills/ct-skill-validator/SKILL.md +19 -0
  37. package/skills/ct-skill-validator/scripts/check_depth.py +306 -0
  38. package/skills/ct-spec-writer/SKILL.md +86 -1
  39. package/skills/ct-spec-writer/__tests__/skill-spec-writer.test.ts +60 -0
  40. package/skills/ct-spec-writer/references/anti-patterns.md +176 -0
  41. package/skills/ct-spec-writer/references/rfc2119-language.md +138 -0
  42. package/skills/ct-spec-writer/references/spec-templates.md +233 -0
  43. package/skills/ct-spec-writer/references/traceability-matrix.md +145 -0
  44. package/skills/ct-task-executor/SKILL.md +25 -0
  45. package/skills/ct-task-executor/references/acceptance-criteria-mapping.md +163 -0
  46. package/skills/ct-task-executor/references/anti-patterns.md +201 -0
  47. package/skills/ct-task-executor/references/common-failures.md +193 -0
  48. package/skills/ct-task-executor/references/evidence-and-gates.md +179 -0
  49. package/skills/ct-task-executor/references/implementation-patterns.md +160 -0
  50. package/skills/ct-validator/SKILL.md +44 -0
  51. package/skills/ct-validator/references/anti-patterns.md +194 -0
  52. package/skills/ct-validator/references/compliance-reports.md +199 -0
  53. package/skills/ct-validator/references/schema-checking.md +191 -0
  54. package/skills/ct-validator/references/validation-modes.md +185 -0
  55. package/skills/manifest.json +82 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/skills",
3
- "version": "2026.5.83",
3
+ "version": "2026.5.86",
4
4
  "description": "CLEO skill definitions - bundled with CLEO monorepo",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Lifecycle-protocol reconcile gate (T9672).
3
+ *
4
+ * Asserts the SET equality:
5
+ *
6
+ * SET( cleo lifecycle stages )
7
+ * ==
8
+ * SET( manifest.dispatch_matrix.by_protocol keys )
9
+ * − { "artifact-publish", "provenance", "agent-protocol" } # cross-cutting
10
+ *
11
+ * No normalization, no dashed-alias allowance. This is the strict gate that
12
+ * lands together with the manifest rename `architecture-decision` →
13
+ * `architecture_decision` so that a future drift fails CI.
14
+ *
15
+ * @task T9672
16
+ * @epic T9568
17
+ */
18
+
19
+ import { readFileSync } from 'node:fs';
20
+ import { dirname, resolve } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { describe, expect, it } from 'vitest';
23
+
24
+ const thisDir = dirname(fileURLToPath(import.meta.url));
25
+ const manifestPath = resolve(thisDir, '../../manifest.json');
26
+
27
+ interface Manifest {
28
+ dispatch_matrix: {
29
+ by_protocol: Record<string, string>;
30
+ by_keyword: Record<string, string>;
31
+ };
32
+ }
33
+
34
+ const manifest: Manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
35
+
36
+ /**
37
+ * The canonical 10 LOOM lifecycle stages emitted by `cleo lifecycle --help`.
38
+ * Underscored form is authoritative — see `packages/core/src/lifecycle/`.
39
+ */
40
+ const LIFECYCLE_STAGES = [
41
+ 'research',
42
+ 'consensus',
43
+ 'architecture_decision',
44
+ 'specification',
45
+ 'decomposition',
46
+ 'implementation',
47
+ 'validation',
48
+ 'testing',
49
+ 'release',
50
+ 'contribution',
51
+ ] as const;
52
+
53
+ /**
54
+ * Cross-cutting protocols that live in `dispatch_matrix.by_protocol` but are
55
+ * not LOOM lifecycle stages.
56
+ */
57
+ const CROSS_CUTTING_PROTOCOLS = new Set([
58
+ 'artifact-publish',
59
+ 'provenance',
60
+ 'agent-protocol',
61
+ ]);
62
+
63
+ describe('lifecycle ↔ protocol reconcile (T9672)', () => {
64
+ const byProtocolKeys = new Set(Object.keys(manifest.dispatch_matrix.by_protocol));
65
+ const stageOnlyKeys = new Set(
66
+ [...byProtocolKeys].filter((k) => !CROSS_CUTTING_PROTOCOLS.has(k)),
67
+ );
68
+ const expected = new Set<string>(LIFECYCLE_STAGES);
69
+
70
+ it('every cleo lifecycle stage is present as a strict (underscored) key in dispatch_matrix.by_protocol', () => {
71
+ for (const stage of LIFECYCLE_STAGES) {
72
+ expect(
73
+ byProtocolKeys.has(stage),
74
+ `lifecycle stage "${stage}" missing from dispatch_matrix.by_protocol — keys: ${[...byProtocolKeys].sort().join(', ')}`,
75
+ ).toBe(true);
76
+ }
77
+ });
78
+
79
+ it('dispatch_matrix.by_protocol contains no surplus stage-like keys', () => {
80
+ for (const key of stageOnlyKeys) {
81
+ expect(
82
+ expected.has(key),
83
+ `dispatch_matrix.by_protocol has key "${key}" which is not a cleo lifecycle stage and not a known cross-cutting protocol`,
84
+ ).toBe(true);
85
+ }
86
+ });
87
+
88
+ it('the dashed legacy form "architecture-decision" is NOT a dispatch_matrix.by_protocol key', () => {
89
+ expect(byProtocolKeys.has('architecture-decision')).toBe(false);
90
+ });
91
+
92
+ it('the dashed legacy form "architecture-decision" IS preserved as a keyword alias', () => {
93
+ const keywordKeys = Object.keys(manifest.dispatch_matrix.by_keyword);
94
+ const adrLine = keywordKeys.find((k) => k.includes('adr') && k.includes('formalize'));
95
+ expect(
96
+ adrLine,
97
+ `expected the ct-adr-recorder keyword dispatch line to exist; keys: ${keywordKeys.join(', ')}`,
98
+ ).toBeDefined();
99
+ expect(
100
+ (adrLine ?? '').includes('architecture-decision'),
101
+ `architecture-decision keyword alias must be retained in ${adrLine}`,
102
+ ).toBe(true);
103
+ });
104
+
105
+ it('the underscored canonical form "architecture_decision" maps to ct-adr-recorder', () => {
106
+ expect(manifest.dispatch_matrix.by_protocol.architecture_decision).toBe('ct-adr-recorder');
107
+ });
108
+
109
+ it('the by_protocol stage-only key set is exactly equal to the lifecycle stage set', () => {
110
+ expect([...stageOnlyKeys].sort()).toEqual([...expected].sort());
111
+ });
112
+ });
@@ -0,0 +1,163 @@
1
+ /**
2
+ * ADR-link gate for LOOM-stage skills (T9665).
3
+ *
4
+ * Enforces two invariants on every canonical LOOM-stage skill in
5
+ * `packages/skills/skills/manifest.json`:
6
+ *
7
+ * 1. The skill entry declares a non-empty `adrRefs[]` array of ADR IDs.
8
+ * 2. Every ADR id in the array resolves to a real file under `.cleo/adrs/`.
9
+ *
10
+ * The mapping of (stage -> required adrRefs minimum) is the authoritative
11
+ * source-of-truth defined in `docs/skills/loom-coverage-matrix.md` under
12
+ * the "ADR Bindings Section". When that doc updates, this test updates.
13
+ *
14
+ * @task T9665
15
+ * @epic T9568
16
+ */
17
+
18
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
19
+ import { dirname, resolve } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { describe, expect, it } from 'vitest';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Setup
25
+ // ---------------------------------------------------------------------------
26
+
27
+ const thisDir = dirname(fileURLToPath(import.meta.url));
28
+ const manifestPath = resolve(thisDir, '../../manifest.json');
29
+ const adrDir = resolve(thisDir, '../../../../../.cleo/adrs');
30
+
31
+ interface SkillEntry {
32
+ name: string;
33
+ loomStage?: string;
34
+ adrRefs?: string[];
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ interface Manifest {
39
+ skills: SkillEntry[];
40
+ }
41
+
42
+ const manifest: Manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
43
+
44
+ /**
45
+ * The canonical 10 LOOM lifecycle stages — underscored form is authoritative.
46
+ * Must match `cleo lifecycle --help` output.
47
+ */
48
+ const CANONICAL_LOOM_STAGES = [
49
+ 'research',
50
+ 'consensus',
51
+ 'architecture_decision',
52
+ 'specification',
53
+ 'decomposition',
54
+ 'implementation',
55
+ 'validation',
56
+ 'testing',
57
+ 'release',
58
+ 'contribution',
59
+ ] as const;
60
+
61
+ /**
62
+ * Required ADR-id minimums per stage. The matrix doc may extend each entry;
63
+ * this gate only enforces the floor.
64
+ */
65
+ const REQUIRED_ADR_REFS: Record<(typeof CANONICAL_LOOM_STAGES)[number], string[]> = {
66
+ research: ['ADR-023', 'ADR-070'],
67
+ consensus: ['ADR-015', 'ADR-023'],
68
+ architecture_decision: ['ADR-053', 'ADR-070'],
69
+ specification: ['ADR-014', 'ADR-023'],
70
+ decomposition: ['ADR-066', 'ADR-073'],
71
+ implementation: ['ADR-070', 'ADR-062'],
72
+ validation: ['ADR-051', 'ADR-023'],
73
+ testing: ['ADR-051', 'ADR-061'],
74
+ release: ['ADR-053', 'ADR-063', 'ADR-065'],
75
+ contribution: ['ADR-015', 'ADR-053'],
76
+ };
77
+
78
+ /**
79
+ * Build a set of ADR-id prefixes from .cleo/adrs/. Each filename starts
80
+ * with `ADR-NNN-...`; we extract the prefix before the second dash.
81
+ */
82
+ function loadAdrIdSet(): Set<string> {
83
+ const files = readdirSync(adrDir);
84
+ const ids = new Set<string>();
85
+ for (const file of files) {
86
+ const match = file.match(/^(ADR-\d{3})-/);
87
+ if (match) ids.add(match[1]);
88
+ }
89
+ return ids;
90
+ }
91
+
92
+ const adrIds = loadAdrIdSet();
93
+ const skillsByStage = new Map<string, SkillEntry>();
94
+ for (const s of manifest.skills) {
95
+ if (typeof s.loomStage === 'string') {
96
+ skillsByStage.set(s.loomStage, s);
97
+ }
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Gate 1: every LOOM-stage skill declares a non-empty adrRefs[]
102
+ // ---------------------------------------------------------------------------
103
+
104
+ describe('LOOM ADR links — adrRefs[] declared on every LOOM-stage skill', () => {
105
+ for (const stage of CANONICAL_LOOM_STAGES) {
106
+ it(`stage "${stage}" skill carries a non-empty adrRefs[]`, () => {
107
+ const skill = skillsByStage.get(stage);
108
+ expect(skill, `no skill found with loomStage="${stage}"`).toBeDefined();
109
+ expect(
110
+ Array.isArray(skill?.adrRefs),
111
+ `skill ${skill?.name} adrRefs is not an array`,
112
+ ).toBe(true);
113
+ expect(
114
+ (skill?.adrRefs ?? []).length,
115
+ `skill ${skill?.name} adrRefs is empty`,
116
+ ).toBeGreaterThan(0);
117
+ });
118
+ }
119
+ });
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Gate 2: every adrRef resolves to a real file under .cleo/adrs/
123
+ // ---------------------------------------------------------------------------
124
+
125
+ describe('LOOM ADR links — every adrRefs entry resolves to .cleo/adrs/<id>-*.md', () => {
126
+ it('.cleo/adrs/ exists and was loaded', () => {
127
+ expect(existsSync(adrDir), `.cleo/adrs/ not found at ${adrDir}`).toBe(true);
128
+ expect(adrIds.size).toBeGreaterThan(0);
129
+ });
130
+
131
+ for (const stage of CANONICAL_LOOM_STAGES) {
132
+ const skill = skillsByStage.get(stage);
133
+ const refs = skill?.adrRefs ?? [];
134
+ for (const ref of refs) {
135
+ it(`stage "${stage}" skill ${skill?.name} references real ADR file "${ref}"`, () => {
136
+ expect(
137
+ adrIds.has(ref),
138
+ `${ref} not found under .cleo/adrs/ — known prefixes: ${[...adrIds].sort().join(', ')}`,
139
+ ).toBe(true);
140
+ });
141
+ }
142
+ }
143
+ });
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Gate 3: required ADR floor per stage is met
147
+ // ---------------------------------------------------------------------------
148
+
149
+ describe('LOOM ADR links — required ADR floor met per stage', () => {
150
+ for (const stage of CANONICAL_LOOM_STAGES) {
151
+ const required = REQUIRED_ADR_REFS[stage];
152
+ const skill = skillsByStage.get(stage);
153
+ const refs = new Set(skill?.adrRefs ?? []);
154
+ for (const requiredAdr of required) {
155
+ it(`stage "${stage}" includes required ADR "${requiredAdr}" in adrRefs`, () => {
156
+ expect(
157
+ refs.has(requiredAdr),
158
+ `skill ${skill?.name} for stage ${stage} missing required ADR ${requiredAdr} (has: ${[...refs].join(', ')})`,
159
+ ).toBe(true);
160
+ });
161
+ }
162
+ }
163
+ });
@@ -0,0 +1,167 @@
1
+ /**
2
+ * LOOM-stage coverage gate (T9664).
3
+ *
4
+ * Enforces that every canonical LOOM lifecycle stage emitted by `cleo lifecycle`
5
+ * has a bound skill in `packages/skills/skills/manifest.json` and that the
6
+ * skill's entry declares a `loomStage` field equal to the lifecycle stage name
7
+ * in underscored canonical form (the `cleo lifecycle` source-of-truth form).
8
+ *
9
+ * Why this exists:
10
+ * - The lifecycle CLI is the runtime source of truth for stage names.
11
+ * - The manifest's `dispatch_matrix.by_protocol` is the dispatch routing table.
12
+ * - Historically those two surfaces drifted (T9568 audit found
13
+ * `architecture-decision` dashed in manifest vs `architecture_decision`
14
+ * underscored in lifecycle CLI).
15
+ * - This test pins the contract so future drift fails CI instead of silently
16
+ * reaching agents at spawn time.
17
+ *
18
+ * @task T9664
19
+ * @epic T9568
20
+ */
21
+
22
+ import { readFileSync } from 'node:fs';
23
+ import { dirname, resolve } from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { describe, expect, it } from 'vitest';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Setup
29
+ // ---------------------------------------------------------------------------
30
+
31
+ const thisDir = dirname(fileURLToPath(import.meta.url));
32
+ const manifestPath = resolve(thisDir, '../../manifest.json');
33
+
34
+ interface SkillEntry {
35
+ name: string;
36
+ protocol?: string;
37
+ loomStage?: string;
38
+ status?: string;
39
+ // intentionally permissive — other fields ignored for this gate
40
+ [key: string]: unknown;
41
+ }
42
+
43
+ interface Manifest {
44
+ dispatch_matrix: {
45
+ by_protocol: Record<string, string>;
46
+ };
47
+ skills: SkillEntry[];
48
+ }
49
+
50
+ const manifest: Manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
51
+
52
+ /**
53
+ * The canonical 10 LOOM lifecycle stages emitted by `cleo lifecycle` (see
54
+ * `packages/core/src/lifecycle/`). Underscored form is authoritative.
55
+ * Update this constant ONLY when the lifecycle CLI itself adds or removes
56
+ * a stage — never to silence a drift failure.
57
+ */
58
+ const CANONICAL_LOOM_STAGES = [
59
+ 'research',
60
+ 'consensus',
61
+ 'architecture_decision',
62
+ 'specification',
63
+ 'decomposition',
64
+ 'implementation',
65
+ 'validation',
66
+ 'testing',
67
+ 'release',
68
+ 'contribution',
69
+ ] as const;
70
+
71
+ /**
72
+ * Cross-cutting protocols that live in `dispatch_matrix.by_protocol` but are
73
+ * NOT LOOM lifecycle stages. They route by capability, not by lifecycle
74
+ * position, and are excluded from the 10-stage gate.
75
+ */
76
+ const CROSS_CUTTING_PROTOCOLS = new Set([
77
+ 'artifact-publish',
78
+ 'provenance',
79
+ 'agent-protocol',
80
+ ]);
81
+
82
+ /**
83
+ * Skill-name lookup keyed by the `name` field.
84
+ */
85
+ const skillByName = new Map(manifest.skills.map((s) => [s.name, s]));
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Gate 1: every canonical stage has a binding in dispatch_matrix.by_protocol
89
+ // ---------------------------------------------------------------------------
90
+
91
+ describe('LOOM stage coverage — dispatch_matrix.by_protocol', () => {
92
+ for (const stage of CANONICAL_LOOM_STAGES) {
93
+ it(`stage "${stage}" is bound to a skill in dispatch_matrix.by_protocol`, () => {
94
+ // We accept either the underscored form (canonical) or the dashed
95
+ // legacy alias (architecture-decision) until T9672 reconciles. After
96
+ // T9672, only the underscored key is required.
97
+ const dashed = stage.replace(/_/g, '-');
98
+ const skillName =
99
+ manifest.dispatch_matrix.by_protocol[stage] ??
100
+ manifest.dispatch_matrix.by_protocol[dashed];
101
+ expect(
102
+ skillName,
103
+ `LOOM stage "${stage}" has no skill binding in manifest.dispatch_matrix.by_protocol (checked both "${stage}" and "${dashed}")`,
104
+ ).toBeTruthy();
105
+ });
106
+ }
107
+ });
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Gate 2: every protocol-bound skill carries a matching loomStage frontmatter
111
+ // ---------------------------------------------------------------------------
112
+
113
+ describe('LOOM stage coverage — loomStage field on bound skills', () => {
114
+ for (const stage of CANONICAL_LOOM_STAGES) {
115
+ it(`bound skill for "${stage}" declares loomStage === "${stage}"`, () => {
116
+ const dashed = stage.replace(/_/g, '-');
117
+ const skillName =
118
+ manifest.dispatch_matrix.by_protocol[stage] ??
119
+ manifest.dispatch_matrix.by_protocol[dashed];
120
+ const skill = skillName ? skillByName.get(skillName) : undefined;
121
+ expect(skill, `skill "${skillName}" not found in manifest.skills[]`).toBeDefined();
122
+ expect(
123
+ skill?.loomStage,
124
+ `skill "${skillName}" missing loomStage field; expected "${stage}"`,
125
+ ).toBe(stage);
126
+ });
127
+ }
128
+ });
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Gate 3: every skill that has loomStage uses a canonical (underscored) value
132
+ // ---------------------------------------------------------------------------
133
+
134
+ describe('LOOM stage coverage — loomStage values are canonical', () => {
135
+ const stagesSet = new Set<string>(CANONICAL_LOOM_STAGES);
136
+ const skillsWithLoomStage = manifest.skills.filter((s) => typeof s.loomStage === 'string');
137
+
138
+ it('at least 10 skills carry a loomStage field (one per LOOM stage)', () => {
139
+ expect(skillsWithLoomStage.length).toBeGreaterThanOrEqual(CANONICAL_LOOM_STAGES.length);
140
+ });
141
+
142
+ for (const skill of skillsWithLoomStage) {
143
+ it(`skill "${skill.name}" loomStage value "${skill.loomStage}" is a canonical LOOM stage`, () => {
144
+ expect(stagesSet.has(skill.loomStage as string)).toBe(true);
145
+ });
146
+ }
147
+ });
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Gate 4: dispatch_matrix.by_protocol keys minus cross-cutting == LOOM stages
151
+ // (the union of the 10 lifecycle stages — checked allowing dashed alias for
152
+ // architecture_decision until T9672 lands the reconcile)
153
+ // ---------------------------------------------------------------------------
154
+
155
+ describe('LOOM stage coverage — dispatch_matrix.by_protocol key set', () => {
156
+ it('by_protocol keys minus cross-cutting protocols cover every canonical LOOM stage', () => {
157
+ const keys = new Set(Object.keys(manifest.dispatch_matrix.by_protocol));
158
+ const stageKeys = [...keys].filter((k) => !CROSS_CUTTING_PROTOCOLS.has(k));
159
+ const normalized = new Set(stageKeys.map((k) => k.replace(/-/g, '_')));
160
+ for (const stage of CANONICAL_LOOM_STAGES) {
161
+ expect(
162
+ normalized.has(stage),
163
+ `LOOM stage "${stage}" not represented in dispatch_matrix.by_protocol (normalized keys: ${[...normalized].join(', ')})`,
164
+ ).toBe(true);
165
+ }
166
+ });
167
+ });
@@ -1,6 +1,11 @@
1
1
  ---
2
2
  name: ct-adr-recorder
3
3
  description: "Records Architecture Decision Records from accepted consensus verdicts. Use when promoting a consensus outcome to a formal ADR: drafts the document in the proposed-then-accepted HITL lifecycle, links to the originating consensus manifest, persists the decision to the canonical SQLite decisions table, and triggers downstream invalidation when an accepted ADR is later superseded. Triggers on phrases like 'write ADR', 'record architecture decision', 'formalize this decision', 'lock in the choice', 'create ADR-XXX', or when a consensus task reaches completed status and needs formalization."
4
+ protocol: architecture_decision
5
+ loomStage: architecture_decision
6
+ adrRefs:
7
+ - ADR-053
8
+ - ADR-070
4
9
  ---
5
10
 
6
11
  # ADR Recorder
@@ -88,6 +93,80 @@ Flags specs T4776, T4781; decomposition epic T4772; live impl T4790.
88
93
 
89
94
  A longer, realistic example with all six sections filled out lives in [references/examples.md](references/examples.md).
90
95
 
96
+ ## Through SDK (preferred)
97
+
98
+ ADRs are first-class docs SSoT records — drafted via
99
+ `cleo docs add --type adr`, attached to the originating consensus task,
100
+ and numbered through the slug. This is the canonical write path; the
101
+ legacy "write to `docs/adr/ADR-NNNN.md` and commit" pattern is
102
+ deprecated below.
103
+
104
+ ### Draft the ADR attached to its consensus task
105
+
106
+ ```bash
107
+ cleo docs add T4798 docs/adr/ADR-0042.md \
108
+ --type adr \
109
+ --slug adr-0042-drizzle-v1-beta \
110
+ --desc "ADR-0042: Adopt Drizzle ORM v1 beta for all SQLite access (status: proposed)" \
111
+ --labels "adr,proposed"
112
+ ```
113
+
114
+ - `--type adr` is the canonical taxonomy value. The closed set is
115
+ `spec | adr | research | handoff | note | llm-readme`.
116
+ - `--slug` MUST follow `adr-<NNNN>-<short-topic>`. The numeric segment
117
+ is the canonical ADR id; the topic segment makes the slug human
118
+ readable. Collisions return `E_SLUG_TAKEN` with 3 alternatives.
119
+ - The owner ID is the consensus task whose verdict drives the ADR
120
+ (`T4798` above). This is how downstream supersession cascades find
121
+ the chain — never attach an ADR to an arbitrary task.
122
+
123
+ ### Persist the decision row alongside the doc blob
124
+
125
+ `cleo docs add --type adr` writes the document and the docs-side
126
+ manifest entry, but per Immutable Constraint ADR-006 the canonical
127
+ `decisions` table MUST also be populated via Drizzle. The two writes
128
+ are paired: doc blob first (SSoT-of-the-prose), then the relational
129
+ row (SSoT-of-the-decision). Skipping either is a validation failure.
130
+
131
+ ### Publish the ADR to a git-tracked path
132
+
133
+ ```bash
134
+ cleo docs publish --for T4798 --to docs/adr/ADR-0042.md
135
+ ```
136
+
137
+ Atomic tmp-then-rename. The published file lands in the next commit;
138
+ the SSoT blob remains the canonical version-history root.
139
+
140
+ ### Fetch the ADR back by slug (for HITL review + downstream supersession)
141
+
142
+ ```bash
143
+ cleo docs fetch adr-0042-drizzle-v1-beta # latest version
144
+ cleo docs versions --for T4798 # every SHA version
145
+ ```
146
+
147
+ The HITL reviewer reads the proposed ADR via `cleo docs fetch` rather
148
+ than the on-disk file so the review anchors on the canonical SSoT
149
+ blob — drift between the published file and the blob is its own
150
+ review finding.
151
+
152
+ ### List ADRs by type or status
153
+
154
+ ```bash
155
+ cleo docs list --type adr --project # every ADR in the project
156
+ cleo docs list --task T4798 --type adr # ADRs attached to T4798
157
+ ```
158
+
159
+ ## Deprecated: Direct filesystem write
160
+
161
+ The legacy "write to `docs/adr/ADR-NNNN.md` and commit" pattern is
162
+ deprecated. The on-disk file drifts from the SSoT, the ADR has no
163
+ slug for the supersession cascade to retrieve it by, and the
164
+ consensus-task↔ADR linkage exists only as a frontmatter field
165
+ (`consensus_manifest_id`) rather than a relational owner edge.
166
+ Migrate to `cleo docs add --type adr --slug adr-<NNNN>-<topic>` for
167
+ every new ADR — and use `cleo docs sync --from docs/adr/ADR-NNNN.md
168
+ --for <taskId>` to back-fill existing on-disk ADRs into the SSoT.
169
+
91
170
  ## HITL Approval Gate
92
171
 
93
172
  When a draft reaches `proposed`, the skill MUST:
@@ -173,3 +252,16 @@ Exit code 0 = valid. Exit code 65 = `HANDOFF_REQUIRED`. Exit code 18 = `CASCADE_
173
252
  6. Superseding an accepted ADR MUST trigger the downstream cascade over linked specs, decomps, and impls.
174
253
  7. Agents MUST NOT retry the HITL handoff on a loop; wait for the human reviewer.
175
254
  8. Always validate via `cleo check protocol --protocolType architecture-decision` before exiting.
255
+
256
+ ## See also / References
257
+
258
+ This skill binds to the **architecture_decision** LOOM lifecycle stage (underscored canonical form — `cleo lifecycle` source of truth). Governing ADRs:
259
+
260
+ - [ADR-053 — playbook runtime](../../../../.cleo/adrs/ADR-053-playbook-runtime.md) — defines the lifecycle state machine; the ADR stage is one of its 10 nodes.
261
+ - [ADR-070 — three-tier orchestration](../../../../.cleo/adrs/ADR-070-three-tier-orchestration.md) — defines the Orchestrator HITL gate that owns the `proposed → accepted` ADR transition.
262
+
263
+ ### Naming Note (T9672)
264
+
265
+ The stage's canonical name is **`architecture_decision`** (underscored) because the `cleo lifecycle` CLI emits it that way. Historically `packages/skills/skills/manifest.json` `dispatch_matrix.by_protocol` used the dashed form `architecture-decision` — T9672 reconciled that key to the underscored form. The dashed form is retained only as a keyword alias under `dispatch_matrix.by_keyword` so legacy dispatch paths continue to resolve. Frontmatter on this skill (`protocol: architecture_decision`, `loomStage: architecture_decision`) uses the underscored form.
266
+
267
+ LOOM coverage matrix: [docs/skills/loom-coverage-matrix.md](../../../../docs/skills/loom-coverage-matrix.md).
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Regression test for ct-adr-recorder/SKILL.md (T9643 / Epic T9629 / Saga T9625).
3
+ *
4
+ * Pins the SDK-first ADR contract: ADRs MUST be drafted via
5
+ * `cleo docs add --type adr --slug adr-<NNNN>-<topic>` so the document
6
+ * is owned by the originating consensus task and addressable by slug
7
+ * for the HITL approval gate and the downstream supersession cascade.
8
+ * The relational `decisions` row (ADR-006) is paired with the doc blob
9
+ * but persisted separately via Drizzle.
10
+ *
11
+ * @task T9643
12
+ * @epic T9629
13
+ * @saga T9625
14
+ */
15
+
16
+ import { readFileSync } from 'node:fs';
17
+ import { dirname, join, resolve } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { describe, expect, it } from 'vitest';
20
+
21
+ const thisFile = fileURLToPath(import.meta.url);
22
+ const skillRoot = resolve(dirname(thisFile), '..');
23
+ const skillPath = join(skillRoot, 'SKILL.md');
24
+ const skillContent = readFileSync(skillPath, 'utf-8');
25
+
26
+ describe('ct-adr-recorder SKILL.md — SDK-first contract (T9643)', () => {
27
+ it('teaches `cleo docs add --type adr` as the canonical write path', () => {
28
+ expect(skillContent).toMatch(/cleo docs add[\s\S]+--type adr/);
29
+ });
30
+
31
+ it('shows the `adr-<NNNN>-<topic>` slug convention', () => {
32
+ expect(skillContent).toMatch(/adr-<NNNN>-<.*?topic/i);
33
+ });
34
+
35
+ it('attaches the ADR to the consensus task via the owner ID', () => {
36
+ // The example MUST use a T### owner ID to demonstrate consensus-task linkage
37
+ expect(skillContent).toMatch(/cleo docs add\s+T\d+\s/);
38
+ });
39
+
40
+ it('keeps the relational `decisions` write paired with the doc blob (ADR-006)', () => {
41
+ expect(skillContent).toMatch(/decisions[\s\S]+Drizzle/i);
42
+ expect(skillContent).toContain('ADR-006');
43
+ });
44
+
45
+ it('shows `cleo docs publish --for ... --to docs/adr/...` for git publication', () => {
46
+ expect(skillContent).toMatch(/cleo docs publish[\s\S]+--for[\s\S]+--to[\s\S]+docs\/adr/);
47
+ });
48
+
49
+ it('shows `cleo docs fetch <slug>` for HITL review + supersession', () => {
50
+ expect(skillContent).toContain('cleo docs fetch');
51
+ });
52
+
53
+ it('shows `cleo docs list --type adr` for ADR discovery', () => {
54
+ expect(skillContent).toMatch(/cleo docs list[\s\S]+--type adr/);
55
+ });
56
+
57
+ it('marks the old direct-filesystem write as deprecated with a migration note', () => {
58
+ expect(skillContent).toContain('Deprecated: Direct filesystem write');
59
+ expect(skillContent).toMatch(/cleo docs (add|sync)/);
60
+ });
61
+
62
+ it('references E_SLUG_TAKEN for collision handling', () => {
63
+ expect(skillContent).toContain('E_SLUG_TAKEN');
64
+ });
65
+ });
@@ -1,6 +1,11 @@
1
1
  ---
2
2
  name: ct-consensus-voter
3
3
  description: "Runs structured multi-agent voting for decision tasks with confidence scores, conflict detection, and HITL escalation when the threshold is not met. Use when two or more agents must vote on options: architecture choices, tool selection, policy decisions, when a task carries agent_type:analysis, or on phrases like 'reach consensus', 'vote on options', 'resolve the debate', 'pick the best approach'. Produces a voting matrix JSON, enforces the 0.5 threshold, flags ties within 0.1 confidence as contested and escalates to human tiebreak."
4
+ protocol: consensus
5
+ loomStage: consensus
6
+ adrRefs:
7
+ - ADR-015
8
+ - ADR-023
4
9
  ---
5
10
 
6
11
  # Consensus Voter
@@ -156,3 +161,12 @@ This skill typically hands off to ct-adr-recorder on a `PROVEN` verdict so the d
156
161
  6. Manifest entry MUST set `agent_type: "analysis"` and include the verdict.
157
162
  7. On PROVEN, hand off to ct-adr-recorder; on CONTESTED or INSUFFICIENT_EVIDENCE, hand off to HITL.
158
163
  8. Always validate via `cleo check protocol --protocolType consensus`.
164
+
165
+ ## See also / References
166
+
167
+ This skill binds to the **consensus** LOOM lifecycle stage. Governing ADRs:
168
+
169
+ - [ADR-015 — multi-contributor architecture](../../../../.cleo/adrs/ADR-015-multi-contributor-architecture.md) — defines the consensus framework that this skill implements.
170
+ - [ADR-023 — protocol validation dispatch](../../../../.cleo/adrs/ADR-023-protocol-validation-dispatch.md) — defines how consensus output is validated before downstream stages consume it.
171
+
172
+ LOOM coverage matrix: [docs/skills/loom-coverage-matrix.md](../../../../docs/skills/loom-coverage-matrix.md).