@geraldmaron/construct 1.0.14 → 1.0.15

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 (259) hide show
  1. package/README.md +6 -0
  2. package/bin/construct +3 -3
  3. package/commands/build/feature.md +0 -6
  4. package/commands/build/fix.md +0 -6
  5. package/commands/design/access.md +0 -6
  6. package/commands/design/flow.md +0 -6
  7. package/commands/design/ui.md +0 -6
  8. package/commands/measure/experiment.md +0 -6
  9. package/commands/measure/metrics.md +0 -6
  10. package/commands/measure/results.md +0 -6
  11. package/commands/plan/api.md +0 -6
  12. package/commands/plan/challenge.md +0 -6
  13. package/commands/plan/decide.md +0 -6
  14. package/commands/plan/feature.md +0 -7
  15. package/commands/plan/requirements.md +0 -6
  16. package/commands/remember/context.md +0 -6
  17. package/commands/remember/handoff.md +0 -6
  18. package/commands/remember/runbook.md +0 -6
  19. package/commands/review/code.md +0 -6
  20. package/commands/review/quality.md +0 -6
  21. package/commands/review/security.md +0 -6
  22. package/commands/ship/ready.md +0 -6
  23. package/commands/ship/release.md +0 -6
  24. package/commands/ship/status.md +0 -6
  25. package/commands/understand/docs.md +0 -6
  26. package/commands/understand/research.md +0 -6
  27. package/commands/understand/this.md +0 -6
  28. package/commands/understand/why.md +0 -6
  29. package/commands/work/clean.md +0 -6
  30. package/commands/work/drive.md +0 -6
  31. package/commands/work/optimize-prompts.md +0 -6
  32. package/commands/work/parallel-review.md +0 -6
  33. package/lib/beads-client.mjs +19 -1
  34. package/lib/comment-lint.mjs +5 -1
  35. package/lib/context-state.mjs +15 -3
  36. package/lib/contracts/validate.mjs +54 -10
  37. package/lib/contracts/violation-log.mjs +154 -0
  38. package/lib/document-extract/docling-client.mjs +114 -0
  39. package/lib/document-extract/docling-sidecar.py +122 -0
  40. package/lib/document-extract/whisper-client.mjs +79 -0
  41. package/lib/document-extract.mjs +73 -8
  42. package/lib/document-ingest.mjs +26 -4
  43. package/lib/embed/daemon.mjs +6 -1
  44. package/lib/flavors/loader.mjs +1 -1
  45. package/lib/handoffs/contract.mjs +2 -2
  46. package/lib/hooks/session-start.mjs +1 -1
  47. package/lib/ingest/chunker.mjs +94 -0
  48. package/lib/ingest/pipeline.mjs +53 -0
  49. package/lib/ingest/store.mjs +82 -0
  50. package/lib/knowledge/search.mjs +12 -0
  51. package/lib/mcp/server.mjs +142 -1
  52. package/lib/mcp/tools/skills.mjs +6 -3
  53. package/lib/mcp/tools/workflow.mjs +6 -2
  54. package/lib/observation-store.mjs +14 -5
  55. package/lib/ollama-manager.mjs +32 -28
  56. package/lib/prompt-composer.js +11 -1
  57. package/lib/runtime/uv-bootstrap.mjs +129 -0
  58. package/lib/runtime/whisper-bootstrap.mjs +102 -0
  59. package/lib/server/index.mjs +22 -1
  60. package/lib/server/static/index.html +1 -1
  61. package/lib/specialists/postconditions.mjs +1 -1
  62. package/lib/sync/skill-frontmatter.mjs +113 -21
  63. package/lib/tracking-surfaces.mjs +2 -0
  64. package/lib/validators/skills.mjs +86 -54
  65. package/package.json +5 -5
  66. package/personas/construct.md +4 -7
  67. package/platforms/claude/settings.template.json +1 -1
  68. package/rules/common/beads-hygiene.md +3 -9
  69. package/rules/common/code-review.md +3 -6
  70. package/rules/common/coding-style.md +3 -6
  71. package/rules/common/comments.md +3 -7
  72. package/rules/common/commit-approval.md +3 -6
  73. package/rules/common/cx-agent-routing.md +3 -7
  74. package/rules/common/cx-skill-routing.md +3 -3
  75. package/rules/common/doc-ownership.md +3 -8
  76. package/rules/common/efficiency.md +3 -8
  77. package/rules/common/framing.md +3 -8
  78. package/rules/common/git-workflow.md +3 -5
  79. package/rules/common/no-fabrication.md +4 -12
  80. package/rules/common/patterns.md +3 -5
  81. package/rules/common/release-gates.md +3 -8
  82. package/rules/common/research.md +3 -8
  83. package/rules/common/review-before-change.md +3 -9
  84. package/rules/common/security.md +3 -6
  85. package/rules/common/skill-composition.md +3 -7
  86. package/rules/common/testing.md +3 -6
  87. package/rules/golang/coding-style.md +1 -5
  88. package/rules/golang/hooks.md +1 -5
  89. package/rules/golang/patterns.md +1 -5
  90. package/rules/golang/security.md +1 -5
  91. package/rules/golang/testing.md +1 -5
  92. package/rules/python/coding-style.md +1 -5
  93. package/rules/python/hooks.md +1 -5
  94. package/rules/python/patterns.md +1 -5
  95. package/rules/python/security.md +1 -5
  96. package/rules/python/testing.md +1 -5
  97. package/rules/swift/coding-style.md +1 -5
  98. package/rules/swift/hooks.md +1 -5
  99. package/rules/swift/patterns.md +1 -5
  100. package/rules/swift/security.md +1 -5
  101. package/rules/swift/testing.md +1 -5
  102. package/rules/typescript/coding-style.md +1 -5
  103. package/rules/typescript/hooks.md +1 -5
  104. package/rules/typescript/patterns.md +1 -5
  105. package/rules/typescript/security.md +1 -5
  106. package/rules/typescript/testing.md +1 -5
  107. package/rules/web/coding-style.md +3 -5
  108. package/rules/web/design-quality.md +3 -5
  109. package/rules/web/hooks.md +3 -5
  110. package/rules/web/patterns.md +3 -5
  111. package/rules/web/performance.md +3 -5
  112. package/rules/web/security.md +3 -5
  113. package/rules/web/testing.md +3 -5
  114. package/skills/ai/agent-dev.md +4 -5
  115. package/skills/ai/llm-security.md +4 -5
  116. package/skills/ai/ml-ops.md +4 -5
  117. package/skills/ai/orchestration-workflow.md +4 -5
  118. package/skills/ai/prompt-and-eval.md +4 -5
  119. package/skills/ai/prompt-optimizer.md +4 -6
  120. package/skills/ai/rag-system.md +4 -5
  121. package/skills/architecture/api-design.md +4 -5
  122. package/skills/architecture/caching.md +4 -5
  123. package/skills/architecture/cloud-native.md +4 -5
  124. package/skills/architecture/message-queue.md +4 -5
  125. package/skills/architecture/security-arch.md +4 -5
  126. package/skills/compliance/ai-disclosure.md +4 -5
  127. package/skills/compliance/data-privacy.md +4 -5
  128. package/skills/compliance/license-audit.md +4 -5
  129. package/skills/compliance/regulatory-review.md +4 -5
  130. package/skills/development/cpp.md +4 -5
  131. package/skills/development/go.md +4 -5
  132. package/skills/development/java.md +4 -5
  133. package/skills/development/kotlin.md +4 -5
  134. package/skills/development/mobile-crossplatform.md +4 -5
  135. package/skills/development/python.md +4 -5
  136. package/skills/development/rust.md +4 -5
  137. package/skills/development/shell.md +4 -5
  138. package/skills/development/swift.md +4 -5
  139. package/skills/development/typescript.md +4 -5
  140. package/skills/devops/ci-cd.md +4 -5
  141. package/skills/devops/containerization.md +4 -5
  142. package/skills/devops/cost-optimization.md +4 -5
  143. package/skills/devops/data-engineering.md +4 -5
  144. package/skills/devops/database.md +4 -5
  145. package/skills/devops/dependency-management.md +4 -5
  146. package/skills/devops/devsecops.md +4 -5
  147. package/skills/devops/git-workflow.md +4 -5
  148. package/skills/devops/incident-response.md +4 -5
  149. package/skills/devops/monorepo.md +4 -5
  150. package/skills/devops/observability.md +4 -5
  151. package/skills/devops/performance.md +4 -5
  152. package/skills/devops/testing.md +4 -5
  153. package/skills/docs/adr-workflow.md +4 -7
  154. package/skills/docs/backlog-proposal-workflow.md +4 -3
  155. package/skills/docs/customer-profile-workflow.md +4 -3
  156. package/skills/docs/document-ingest-workflow.md +4 -3
  157. package/skills/docs/evidence-ingest-workflow.md +4 -3
  158. package/skills/docs/init-docs.md +4 -5
  159. package/skills/docs/init-project.md +4 -5
  160. package/skills/docs/prd-workflow.md +4 -7
  161. package/skills/docs/prfaq-workflow.md +4 -3
  162. package/skills/docs/product-intelligence-review.md +4 -3
  163. package/skills/docs/product-intelligence-workflow.md +4 -6
  164. package/skills/docs/product-signal-workflow.md +4 -5
  165. package/skills/docs/research-workflow.md +4 -5
  166. package/skills/docs/runbook-workflow.md +4 -5
  167. package/skills/docs/strategy-workflow.md +4 -5
  168. package/skills/exploration/dependency-graph-reading.md +4 -7
  169. package/skills/exploration/repo-map.md +4 -5
  170. package/skills/exploration/tracer-bullet-method.md +4 -7
  171. package/skills/exploration/unknown-codebase-onboarding.md +4 -7
  172. package/skills/frameworks/django.md +4 -5
  173. package/skills/frameworks/nextjs.md +4 -5
  174. package/skills/frameworks/react.md +4 -5
  175. package/skills/frameworks/spring-boot.md +4 -5
  176. package/skills/frontend-design/accessibility.md +4 -5
  177. package/skills/frontend-design/component-patterns.md +4 -5
  178. package/skills/frontend-design/engineering.md +4 -5
  179. package/skills/frontend-design/state-management.md +4 -5
  180. package/skills/frontend-design/ui-aesthetics.md +4 -5
  181. package/skills/frontend-design/ux-principles.md +4 -5
  182. package/skills/operating/change-management.md +4 -8
  183. package/skills/operating/incident-response.md +4 -8
  184. package/skills/operating/oncall-rotation.md +4 -7
  185. package/skills/operating/orchestration-reference.md +4 -10
  186. package/skills/quality-gates/review-work.md +4 -5
  187. package/skills/quality-gates/verify-change.md +4 -5
  188. package/skills/quality-gates/verify-module.md +4 -5
  189. package/skills/quality-gates/verify-quality.md +4 -5
  190. package/skills/quality-gates/verify-security.md +4 -5
  191. package/skills/roles/architect.ai-systems.md +6 -9
  192. package/skills/roles/architect.data.md +6 -9
  193. package/skills/roles/architect.enterprise.md +6 -9
  194. package/skills/roles/architect.integration.md +6 -9
  195. package/skills/roles/architect.md +7 -14
  196. package/skills/roles/architect.platform.md +6 -9
  197. package/skills/roles/data-analyst.experiment.md +6 -9
  198. package/skills/roles/data-analyst.md +6 -9
  199. package/skills/roles/data-analyst.product-intelligence.md +7 -9
  200. package/skills/roles/data-analyst.product.md +6 -9
  201. package/skills/roles/data-analyst.telemetry.md +7 -9
  202. package/skills/roles/data-engineer.pipeline.md +6 -9
  203. package/skills/roles/data-engineer.vector-retrieval.md +7 -9
  204. package/skills/roles/data-engineer.warehouse.md +6 -9
  205. package/skills/roles/debugger.md +6 -9
  206. package/skills/roles/designer.accessibility.md +6 -9
  207. package/skills/roles/designer.md +7 -9
  208. package/skills/roles/engineer.ai.md +6 -9
  209. package/skills/roles/engineer.data.md +6 -9
  210. package/skills/roles/engineer.md +9 -9
  211. package/skills/roles/engineer.platform.md +6 -9
  212. package/skills/roles/operator.docs.md +6 -9
  213. package/skills/roles/operator.md +9 -9
  214. package/skills/roles/operator.release.md +6 -9
  215. package/skills/roles/operator.sre.md +6 -9
  216. package/skills/roles/orchestrator.md +6 -9
  217. package/skills/roles/product-manager.ai-product.md +6 -9
  218. package/skills/roles/product-manager.business-strategy.md +6 -9
  219. package/skills/roles/product-manager.enterprise.md +6 -9
  220. package/skills/roles/product-manager.growth.md +7 -9
  221. package/skills/roles/product-manager.md +7 -9
  222. package/skills/roles/product-manager.platform.md +6 -9
  223. package/skills/roles/product-manager.product.md +6 -9
  224. package/skills/roles/qa.ai-eval.md +8 -9
  225. package/skills/roles/qa.api-contract.md +7 -9
  226. package/skills/roles/qa.data-pipeline.md +7 -9
  227. package/skills/roles/qa.md +7 -9
  228. package/skills/roles/qa.test-automation.md +6 -9
  229. package/skills/roles/qa.web-ui.md +7 -9
  230. package/skills/roles/researcher.explorer.md +6 -9
  231. package/skills/roles/researcher.md +8 -9
  232. package/skills/roles/researcher.ux.md +6 -9
  233. package/skills/roles/reviewer.devil-advocate.md +6 -9
  234. package/skills/roles/reviewer.evaluator.md +6 -9
  235. package/skills/roles/reviewer.md +9 -9
  236. package/skills/roles/reviewer.trace.md +6 -9
  237. package/skills/roles/security.ai.md +7 -9
  238. package/skills/roles/security.appsec.md +6 -9
  239. package/skills/roles/security.cloud.md +6 -9
  240. package/skills/roles/security.legal-compliance.md +6 -9
  241. package/skills/roles/security.md +7 -9
  242. package/skills/roles/security.privacy.md +7 -9
  243. package/skills/roles/security.supply-chain.md +7 -9
  244. package/skills/security/blue-team.md +4 -5
  245. package/skills/security/code-audit.md +4 -5
  246. package/skills/security/pentest.md +4 -5
  247. package/skills/security/red-team.md +4 -5
  248. package/skills/security/threat-intel.md +4 -5
  249. package/skills/security/vuln-research.md +4 -5
  250. package/skills/strategy/competitive-landscape.md +4 -8
  251. package/skills/strategy/market-research-methods.md +4 -8
  252. package/skills/strategy/narrative-arc.md +4 -8
  253. package/skills/strategy/pricing-positioning.md +4 -8
  254. package/skills/utility/clean-code.md +4 -5
  255. package/specialists/policy-inventory.json +14 -0
  256. package/lib/specialist-contracts-enforce.mjs +0 -172
  257. package/rules/common/agents.md +0 -28
  258. package/rules/common/development-workflow.md +0 -32
  259. package/rules/common/performance.md +0 -55
@@ -2,38 +2,130 @@
2
2
  * lib/sync/skill-frontmatter.mjs — Build Anthropic Agent Skills frontmatter
3
3
  * for SKILL.md files emitted by sync-specialists.
4
4
  *
5
- * Anthropic Skills require YAML frontmatter with at minimum `name` and
6
- * `description`. The description gates skill selection without it, the
7
- * loader silently skips the file (the user-reported bug: 141 files dropped).
8
- * Source Construct skills carry an HTML comment header with the description;
9
- * extractSkillDescription pulls it out.
5
+ * Source skills carry YAML frontmatter with at minimum `name` + `description`
6
+ * (Anthropic spec). For role files, extra construct-internal keys (role,
7
+ * applies_to, inherits, version, profiles, cap) live in the same block; sync
8
+ * drops those at emit time so only spec-compliant keys ship in SKILL.md.
10
9
  *
11
- * Also exports a conservative stripLeadingFrontmatter helper so we never
12
- * emit double-frontmatter when the source body already has its own block.
10
+ * Backwards-compat path: source files missing YAML frontmatter fall through
11
+ * to HTML-comment preamble extraction, then first body paragraph. Keeps
12
+ * unmigrated files working during transition; the validator warns until
13
+ * they migrate.
14
+ *
15
+ * Zero-dep posture: runs from isolated environments (cpSync'd tmpdirs
16
+ * without node_modules), so the YAML reader/emitter stays in-tree. Surface
17
+ * is small — read flat top-level keys + simple inline arrays; write 2 keys
18
+ * (name, description). For richer YAML, the migration script and validator
19
+ * use js-yaml.
13
20
  */
14
21
 
22
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
15
23
  const HTML_COMMENT_RE = /^<!--([\s\S]*?)-->/;
16
- // Matches the source skill comment header shape: `skills/path/foo.md (Title)
17
- // Use when X.\n...rest of comment...`. Group 1 captures the optional title,
18
- // group 2 captures all subsequent prose inside the comment. No /m flag — `$`
19
- // matches end of input, not end of line, so multi-line "Use when..." prose
20
- // is captured in full and firstSentence() picks the first terminator.
21
-
22
24
  const SOURCE_HEADER_RE = /^skills\/[\w./-]+\s*(?:\(([^)]+)\))?\s*([\s\S]*?)$/;
23
25
 
24
- // Anthropic Skills cap descriptions at 1024 chars; this cap is tighter so
25
- // the YAML stays readable in `construct skills list` output too.
26
- const DESCRIPTION_MAX = 240;
26
+ const DESCRIPTION_MAX = 1024;
27
+
28
+ // In-tree minimal YAML parser. Handles the subset Construct skill frontmatter
29
+ // uses: flat scalar keys, double/single-quoted or unquoted strings, simple
30
+ // inline arrays ([a, b, c]), and block-style arrays (- item per line).
31
+ // Unparseable input returns null; callers fall back to extraction.
32
+
33
+ function parseFlatYaml(text) {
34
+ if (!text || typeof text !== 'string') return null;
35
+ const lines = text.split('\n');
36
+ const out = {};
37
+ let pendingArrayKey = null;
38
+ try {
39
+ for (const raw of lines) {
40
+ const line = raw.replace(/\r$/, '');
41
+ if (!line.trim() || line.trim().startsWith('#')) continue;
42
+ if (pendingArrayKey && /^\s+-\s+/.test(line)) {
43
+ out[pendingArrayKey].push(parseScalar(line.replace(/^\s+-\s+/, '')));
44
+ continue;
45
+ }
46
+ pendingArrayKey = null;
47
+ const m = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
48
+ if (!m) continue;
49
+ const key = m[1];
50
+ const rawValue = m[2];
51
+ if (rawValue === '' || rawValue === undefined) {
52
+ out[key] = [];
53
+ pendingArrayKey = key;
54
+ continue;
55
+ }
56
+ if (rawValue.startsWith('[') && rawValue.endsWith(']')) {
57
+ const inner = rawValue.slice(1, -1).trim();
58
+ out[key] = inner === '' ? [] : inner.split(',').map((s) => parseScalar(s.trim()));
59
+ continue;
60
+ }
61
+ out[key] = parseScalar(rawValue);
62
+ }
63
+ } catch (err) {
64
+ if (err instanceof YamlError) return null;
65
+ throw err;
66
+ }
67
+ return out;
68
+ }
69
+
70
+ class YamlError extends Error {}
71
+
72
+ function parseScalar(s) {
73
+ const trimmed = s.trim();
74
+ if (trimmed === '' || trimmed === 'null' || trimmed === '~') return null;
75
+ if (trimmed === 'true') return true;
76
+ if (trimmed === 'false') return false;
77
+ if (/^-?\d+$/.test(trimmed)) return Number(trimmed);
78
+ if (/^-?\d+\.\d+$/.test(trimmed)) return Number(trimmed);
79
+ if (trimmed.startsWith('"')) {
80
+ if (!trimmed.endsWith('"') || trimmed.length < 2) throw new YamlError('unclosed double quote');
81
+ return trimmed.slice(1, -1).replace(/\\"/g, '"');
82
+ }
83
+ if (trimmed.startsWith("'")) {
84
+ if (!trimmed.endsWith("'") || trimmed.length < 2) throw new YamlError('unclosed single quote');
85
+ return trimmed.slice(1, -1).replace(/\\'/g, "'");
86
+ }
87
+ return trimmed;
88
+ }
89
+
90
+ function emitScalar(value) {
91
+ if (value === null || value === undefined) return 'null';
92
+ if (typeof value === 'boolean' || typeof value === 'number') return String(value);
93
+ const s = String(value);
94
+ if (/[:"'\n#&*!|>%@`]|^[-?]/.test(s) || s.length === 0) {
95
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
96
+ }
97
+ return s;
98
+ }
99
+
100
+ export function readSkillFrontmatter(content) {
101
+ if (!content) return null;
102
+ const match = content.match(FRONTMATTER_RE);
103
+ if (!match) return null;
104
+ return parseFlatYaml(match[1]);
105
+ }
27
106
 
28
107
  export function buildSkillFrontmatter(name, sourceContent) {
29
- const skillName = String(name).replace(/\//g, '.');
30
- const description = extractSkillDescription(sourceContent) || `Construct skill: ${skillName}`;
31
- const safeDescription = description.replace(/\n+/g, ' ').slice(0, DESCRIPTION_MAX).replace(/"/g, "'");
32
- return `---\nname: ${skillName}\ndescription: "${safeDescription}"\n---\n`;
108
+ const fromSource = readSkillFrontmatter(sourceContent);
109
+ const skillName = (fromSource?.name && typeof fromSource.name === 'string')
110
+ ? fromSource.name
111
+ : kebabFromPath(name);
112
+ const description = (fromSource?.description && typeof fromSource.description === 'string')
113
+ ? fromSource.description
114
+ : (extractSkillDescription(sourceContent) || `Construct skill: ${skillName}`);
115
+ const safe = description.replace(/\n+/g, ' ').slice(0, DESCRIPTION_MAX).replace(/"/g, "'").trim();
116
+ return `---\nname: ${emitScalar(skillName)}\ndescription: ${emitScalar(safe)}\n---\n`;
117
+ }
118
+
119
+ function kebabFromPath(name) {
120
+ return String(name).replace(/[/.]/g, '-').toLowerCase();
33
121
  }
34
122
 
35
123
  export function extractSkillDescription(content) {
36
124
  if (!content) return null;
125
+ const fromYaml = readSkillFrontmatter(content);
126
+ if (fromYaml?.description && typeof fromYaml.description === 'string') {
127
+ return fromYaml.description;
128
+ }
37
129
  const commentMatch = content.match(HTML_COMMENT_RE);
38
130
  if (commentMatch) {
39
131
  const inner = commentMatch[1].trim();
@@ -45,7 +137,7 @@ export function extractSkillDescription(content) {
45
137
  const lines = inner.split('\n').map((l) => l.trim()).filter(Boolean);
46
138
  if (lines.length >= 2) return firstSentence(lines.slice(1).join(' '));
47
139
  }
48
- const body = content.replace(HTML_COMMENT_RE, '').trim();
140
+ const body = content.replace(FRONTMATTER_RE, '').replace(HTML_COMMENT_RE, '').trim();
49
141
  const paragraphs = body.split(/\n\s*\n/);
50
142
  for (const p of paragraphs) {
51
143
  const trimmed = p.trim();
@@ -26,6 +26,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSy
26
26
  import { join } from 'node:path';
27
27
 
28
28
  import { extractBeadsFromPlan, syncPlanWithBeads } from './beads-automation.mjs';
29
+ import { assertBeadId } from './beads-client.mjs';
29
30
  import { buildPlanTemplate } from './project-init-shared.mjs';
30
31
 
31
32
  const CONTEXT_RECENT_DAYS = 30;
@@ -191,6 +192,7 @@ export async function closeBeadsFromPrRefs({ prNumber, mergeCommitSha = '', cwd
191
192
  : `Merged via PR #${prNumber}`;
192
193
 
193
194
  for (const id of beadIds) {
195
+ try { assertBeadId(id); } catch (err) { errors.push({ id, reason: 'invalid-bead-id', detail: err.message }); continue; }
194
196
  const status = readBeadStatus(cwd, id);
195
197
  if (status === 'closed') { skipped.push(id); continue; }
196
198
  if (status === null) { errors.push({ id, reason: 'bd-show-failed' }); continue; }
@@ -1,62 +1,71 @@
1
1
  /**
2
2
  * lib/validators/skills.mjs — Validate the structure of skill files.
3
3
  *
4
- * Skills are markdown files under `skills/` (and any merged plugin or project
5
- * skill paths) that follow this convention:
4
+ * After the YAML-frontmatter migration, every skill file under `skills/` has:
6
5
  *
7
- * <!--
8
- * skills/<domain>/<name>.md <Title> — <one-line description>
9
- * ...
10
- * -->
6
+ * ---
7
+ * name: <kebab-case>
8
+ * description: "<≤1024 chars, includes a 'use when' trigger>"
9
+ * [role/applies_to/inherits/version/profiles/cap for role files]
10
+ * ---
11
11
  * # <Title>
12
- *
13
- * Use this skill when <triggering condition>.
14
- * ...
12
+ * Use when: <trigger condition>
13
+ * <body>
15
14
  *
16
15
  * The validator splits findings into hard errors (block the build) and soft
17
16
  * warnings (surfaced by `construct doctor` without failing it):
18
17
  *
19
18
  * Hard errors:
20
- * - missing H1 title
19
+ * - frontmatter missing or not parseable as YAML
20
+ * - name missing, > 64 chars, fails ^[a-z0-9][a-z0-9-]*$, or contains
21
+ * reserved tokens (anthropic, claude)
22
+ * - description missing, empty, > 1024 chars, contains XML/HTML tags,
23
+ * or missing a "use when" trigger clause
24
+ * - missing H1 title (in body)
21
25
  * - title over 80 chars
22
26
  * - duplicate relative paths across roots
23
27
  * - unreadable file
24
28
  *
25
29
  * Soft warnings:
26
- * - opener is missing or doesn't match a recognised "Use this skill
27
- * when/to/for ..." form (skills with non-trigger openers still load,
28
- * but they're harder for routing to surface)
29
- * - opener over 240 chars
30
- *
31
- * Hard checks prevent broken file structure; soft checks surface drift from
32
- * authoring conventions without blocking authors who legitimately diverge.
30
+ * - body opener missing or doesn't match a "Use this skill when/to/for ..."
31
+ * form (YAML description is now primary; body opener is a behavioral hint)
32
+ * - body opener over 240 chars
33
33
  */
34
34
 
35
35
  import { readdirSync, readFileSync, statSync } from 'node:fs';
36
36
  import path from 'node:path';
37
+ import yaml from 'js-yaml';
37
38
 
38
- const DESCRIPTION_MAX_CHARS = 240;
39
+ const DESCRIPTION_MAX = 1024;
40
+ const NAME_MAX = 64;
39
41
  const TITLE_MAX_CHARS = 80;
42
+ const BODY_OPENER_MAX = 240;
43
+ const NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
44
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
40
45
 
41
- // Accept any of the conventional skill-trigger openers actually in use across
42
- // the corpus: "Use this skill when/to/for", "Use when:", "Use this when",
43
- // "Trigger:", or a leading "When ..." that signals a triggering condition.
44
46
  const TRIGGER_PATTERN = /^(?:use\s+(?:this\s+(?:skill\s+)?)?(?:when|to|for|if)\b|when\s+to\s+use\b|trigger\s*:|use\s+when\s*:)/i;
45
47
 
46
48
  function* walk(root) {
47
- const entries = readdirSync(root, { withFileTypes: true });
48
- for (const entry of entries) {
49
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
49
50
  const full = path.join(root, entry.name);
50
- if (entry.isDirectory()) {
51
- yield* walk(full);
52
- } else if (entry.isFile() && entry.name.endsWith('.md')) {
53
- yield full;
54
- }
51
+ if (entry.isDirectory()) yield* walk(full);
52
+ else if (entry.isFile() && entry.name.endsWith('.md')) yield full;
53
+ }
54
+ }
55
+
56
+ function parseFrontmatter(content) {
57
+ const match = content.match(FRONTMATTER_RE);
58
+ if (!match) return { frontmatter: null, body: content };
59
+ try {
60
+ const parsed = yaml.load(match[1]) || {};
61
+ return { frontmatter: parsed, body: content.slice(match[0].length) };
62
+ } catch (err) {
63
+ return { frontmatter: null, body: content.slice(match[0].length), yamlError: err.message };
55
64
  }
56
65
  }
57
66
 
58
- function extractTitleAndOpener(text) {
59
- const lines = text.split(/\r?\n/);
67
+ function extractTitleAndOpener(body) {
68
+ const lines = body.split(/\r?\n/);
60
69
  let title = null;
61
70
  let opener = null;
62
71
  let inBody = false;
@@ -64,11 +73,7 @@ function extractTitleAndOpener(text) {
64
73
  const line = lines[i].trim();
65
74
  if (!title) {
66
75
  const m = line.match(/^#\s+(.+?)\s*$/);
67
- if (m) {
68
- title = m[1];
69
- inBody = true;
70
- continue;
71
- }
76
+ if (m) { title = m[1]; inBody = true; continue; }
72
77
  } else if (inBody && line && !line.startsWith('#') && !line.startsWith('<!--')) {
73
78
  opener = line;
74
79
  break;
@@ -77,23 +82,31 @@ function extractTitleAndOpener(text) {
77
82
  return { title, opener };
78
83
  }
79
84
 
80
- /**
81
- * Validate every skill file under one or more directories.
82
- *
83
- * @param {string[]|string} roots directories to walk recursively
84
- * @returns {{
85
- * valid: boolean,
86
- * errors: string[],
87
- * warnings: string[],
88
- * skills: Array<{ path: string, title: string|null, opener: string|null }>,
89
- * }}
90
- */
85
+ function validateName(name) {
86
+ if (!name) return 'frontmatter.name is missing';
87
+ if (typeof name !== 'string') return `frontmatter.name must be a string (got ${typeof name})`;
88
+ if (!NAME_RE.test(name)) return `frontmatter.name "${name}" must match ${NAME_RE}`;
89
+ if (name.length > NAME_MAX) return `frontmatter.name "${name}" exceeds ${NAME_MAX} chars`;
90
+ if (/\banthropic\b|\bclaude\b/.test(name)) return `frontmatter.name "${name}" contains reserved token`;
91
+ return null;
92
+ }
93
+
94
+ function validateDescription(description) {
95
+ if (!description) return 'frontmatter.description is missing or empty';
96
+ if (typeof description !== 'string') return `frontmatter.description must be a string (got ${typeof description})`;
97
+ if (description.length > DESCRIPTION_MAX) return `frontmatter.description ${description.length} > ${DESCRIPTION_MAX} chars`;
98
+ if (/<[A-Za-z][^>]*>/.test(description)) return 'frontmatter.description contains XML/HTML tags';
99
+ if (!/use\s+when|use\s+this/i.test(description)) return 'frontmatter.description missing "use when" trigger clause';
100
+ return null;
101
+ }
102
+
91
103
  export function validateSkills(roots) {
92
104
  const dirs = Array.isArray(roots) ? roots : [roots];
93
105
  const errors = [];
94
106
  const warnings = [];
95
107
  const skills = [];
96
108
  const seenRelative = new Map();
109
+ const seenNames = new Map();
97
110
 
98
111
  for (const dir of dirs) {
99
112
  let exists = false;
@@ -113,12 +126,31 @@ export function validateSkills(roots) {
113
126
 
114
127
  let raw;
115
128
  try { raw = readFileSync(filePath, 'utf8'); }
116
- catch (err) {
117
- errors.push(`${rel}: cannot read (${err.message})`);
129
+ catch (err) { errors.push(`${rel}: cannot read (${err.message})`); continue; }
130
+
131
+ const { frontmatter, body, yamlError } = parseFrontmatter(raw);
132
+
133
+ if (yamlError) {
134
+ errors.push(`${rel}: frontmatter YAML parse error — ${yamlError}`);
118
135
  continue;
119
136
  }
137
+ if (!frontmatter) {
138
+ errors.push(`${rel}: missing YAML frontmatter block (---name/description---)`);
139
+ continue;
140
+ }
141
+
142
+ const nameErr = validateName(frontmatter.name);
143
+ if (nameErr) errors.push(`${rel}: ${nameErr}`);
144
+ else {
145
+ const dupName = seenNames.get(frontmatter.name);
146
+ if (dupName) errors.push(`${rel}: duplicate frontmatter.name "${frontmatter.name}" (also in ${dupName})`);
147
+ else seenNames.set(frontmatter.name, rel);
148
+ }
149
+
150
+ const descErr = validateDescription(frontmatter.description);
151
+ if (descErr) errors.push(`${rel}: ${descErr}`);
120
152
 
121
- const { title, opener } = extractTitleAndOpener(raw);
153
+ const { title, opener } = extractTitleAndOpener(body);
122
154
 
123
155
  if (!title) {
124
156
  errors.push(`${rel}: missing H1 title (e.g. "# Skill Name")`);
@@ -127,14 +159,14 @@ export function validateSkills(roots) {
127
159
  }
128
160
 
129
161
  if (!opener) {
130
- warnings.push(`${rel}: no trigger opener after the H1 title routing will have nothing to match`);
162
+ warnings.push(`${rel}: no trigger opener in body after the H1 — YAML description still primary, but body opener helps in-task behavior`);
131
163
  } else if (!TRIGGER_PATTERN.test(opener)) {
132
- warnings.push(`${rel}: opener should start with "Use this skill when/to/for ..." (got "${opener.slice(0, 60)}…")`);
133
- } else if (opener.length > DESCRIPTION_MAX_CHARS) {
134
- warnings.push(`${rel}: opener exceeds ${DESCRIPTION_MAX_CHARS} chars (got ${opener.length})`);
164
+ warnings.push(`${rel}: body opener should start with "Use this skill when/to/for ..." (got "${opener.slice(0, 60)}…")`);
165
+ } else if (opener.length > BODY_OPENER_MAX) {
166
+ warnings.push(`${rel}: body opener exceeds ${BODY_OPENER_MAX} chars (got ${opener.length})`);
135
167
  }
136
168
 
137
- skills.push({ path: rel, title, opener });
169
+ skills.push({ path: rel, name: frontmatter.name, description: frontmatter.description, title, opener });
138
170
  }
139
171
  }
140
172
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geraldmaron/construct",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "type": "module",
5
5
  "description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
6
6
  "bin": {
@@ -57,7 +57,6 @@
57
57
  "lint:contracts": "node ./bin/construct lint:contracts",
58
58
  "lint:agents": "node ./bin/construct lint:agents",
59
59
  "build:sea": "esbuild bin/construct --bundle --platform=node --target=node20 --packages=external --outfile=dist/construct-bundle.cjs 2>&1 || (echo 'esbuild not found — install with: npm install -g esbuild' && exit 1)",
60
- "lint:prose": "node scripts/lint-prose.mjs",
61
60
  "lint:profiles": "node scripts/lint-profiles.mjs",
62
61
  "learning:status": "node scripts/learning-status.mjs",
63
62
  "lint:templates": "node scripts/lint-commits-pr.mjs",
@@ -73,16 +72,17 @@
73
72
  "@huggingface/transformers": "^4.2.0",
74
73
  "@modelcontextprotocol/sdk": "^1.12.0",
75
74
  "@xenova/transformers": "^2.0.1",
75
+ "js-yaml": "^4.2.0",
76
76
  "node-webvtt": "^1.9.3",
77
77
  "postgres": "^3.4.9"
78
78
  },
79
79
  "optionalDependencies": {
80
80
  "@opentelemetry/api": "^1.9.0",
81
- "@opentelemetry/sdk-trace-node": "^1.25.0",
81
+ "@opentelemetry/core": "^1.25.0",
82
82
  "@opentelemetry/exporter-trace-otlp-http": "^0.52.0",
83
83
  "@opentelemetry/resources": "^1.25.0",
84
- "@opentelemetry/semantic-conventions": "^1.25.0",
85
- "@opentelemetry/core": "^1.25.0"
84
+ "@opentelemetry/sdk-trace-node": "^1.25.0",
85
+ "@opentelemetry/semantic-conventions": "^1.25.0"
86
86
  },
87
87
  "overrides": {
88
88
  "express-rate-limit": "8.5.1",
@@ -1,10 +1,7 @@
1
- <!--
2
- personas/construct.md. Construct persona prompt.
3
-
4
- Defines the single user-facing AI interface and its session-start behavior,
5
- routing rules, approval boundaries, and output contract. Loaded by sync-specialists
6
- and emitted to every supported platform.
7
- -->
1
+ ---
2
+ name: construct
3
+ description: Construct persona prompt.
4
+ ---
8
5
  You are Construct. The user talks only to you; internal routing and specialist dispatch are implementation detail.
9
6
 
10
7
  **Anti-fabrication contract**: every load-bearing claim cites a verifiable source. Missing source becomes `unknown` or `[unverified]`. Specialists tailor; the persona never weakens. See `rules/common/no-fabrication.md`.
@@ -151,7 +151,7 @@
151
151
  }
152
152
  ],
153
153
  "id": "post:edit:comment-lint",
154
- "description": "Block edits that violate comment policy (banned patterns, missing required header). Bypass: CONSTRUCT_SKIP_COMMENT_LINT=1"
154
+ "description": "Block edits that violate comment policy (banned patterns, missing required header). No bypass — repair the policy if it fires wrong."
155
155
  },
156
156
  {
157
157
  "matcher": "Write|Edit|MultiEdit",
@@ -1,12 +1,6 @@
1
- <!--
2
- rules/common/beads-hygiene.md: Beads issue tracker hygiene contract.
3
-
4
- Beads is the system of record for durable work in this project. Status drifts
5
- when issues are not updated alongside the code, so every agent and persona
6
- operating in Construct treats hygiene as part of the work, not as cleanup.
7
- Loaded by AGENTS.md, CLAUDE.md, the construct persona, and the engineer /
8
- operator / planner role overlays.
9
- -->
1
+ ---
2
+ description: Beads issue tracker hygiene contract.
3
+ ---
10
4
  # Beads Hygiene: Project Contract
11
5
 
12
6
  Beads (`bd`) is the canonical durable tracker for Construct. Beads only earn their keep when their state matches the world. Stale "open" issues pollute `bd ready`, hide real work, and let agents propose work that already shipped. Every agent (human or AI, on any platform) is responsible for keeping the tracker honest.
@@ -1,9 +1,6 @@
1
- <!--
2
- rules/common/code-review.md: when and how to conduct code reviews.
3
-
4
- Defines mandatory review triggers, severity levels, approval criteria,
5
- and references coding-style.md and security.md for checklists.
6
- -->
1
+ ---
2
+ description: when and how to conduct code reviews.
3
+ ---
7
4
  # Code Review Standards
8
5
 
9
6
  ## When to Review
@@ -1,9 +1,6 @@
1
- <!--
2
- rules/common/coding-style.md: language-agnostic coding standards.
3
-
4
- Covers immutability, core principles (KISS/DRY/YAGNI), file organization,
5
- error handling, input validation, naming conventions, and quality checklist.
6
- -->
1
+ ---
2
+ description: language-agnostic coding standards.
3
+ ---
7
4
  # Coding Style
8
5
 
9
6
  ## Immutability (CRITICAL)
@@ -1,10 +1,6 @@
1
- <!--
2
- rules/common/comments.md: Construct comment convention for JS/TS/MJS source files.
3
-
4
- Defines the two allowed comment forms (file header, section context block) and
5
- what is never allowed (inline narration, trailing comments, mid-function notes).
6
- Enforced by lib/hooks/comment-lint.mjs and tests/hooks-budget.test.mjs.
7
- -->
1
+ ---
2
+ description: Construct comment convention for JS/TS/MJS source files.
3
+ ---
8
4
  # Comment Convention
9
5
 
10
6
  Two forms are allowed. Everything else is deleted.
@@ -1,9 +1,6 @@
1
- <!--
2
- rules/common/commit-approval.md: conversational approval rule for mutating git operations.
3
-
4
- Behavioral rule, not a hook. The agent asks and waits for a yes; the user
5
- replies in chat. Infrastructure stays out of the way.
6
- -->
1
+ ---
2
+ description: conversational approval rule for mutating git operations.
3
+ ---
7
4
  # Commit Approval
8
5
 
9
6
  Construct does not commit, push, or merge without the user explicitly saying yes in the current conversation.
@@ -1,10 +1,6 @@
1
- <!--
2
- rules/common/cx-agent-routing.md: auto-trigger routing rules for cx-* specialist agents.
3
-
4
- Defines when to route directly to a cx-* specialist vs through Construct orchestration.
5
- Covers intent-based routing table, complexity gate, and routing rules.
6
- Loaded by rule-loading systems that look for cx-agent-routing in the rules hierarchy.
7
- -->
1
+ ---
2
+ description: auto-trigger routing rules for cx-* specialist agents.
3
+ ---
8
4
  # cx-* Agent Routing: Auto-trigger Rules
9
5
 
10
6
  When a request matches the trigger patterns below, automatically route to the corresponding cx-* specialist or persona before responding.
@@ -1,6 +1,6 @@
1
- <!--
2
- rules/common/cx-skill-routing.md: Redirect to canonical skill routing table.
3
- -->
1
+ ---
2
+ description: Redirect to canonical skill routing table.
3
+ ---
4
4
  # cx-* Skill Routing
5
5
 
6
6
  > **Canonical file:** [`skills/routing.md`](../../skills/routing.md)
@@ -1,11 +1,6 @@
1
- <!--
2
- rules/common/doc-ownership.md: which specialist owns which document type.
3
-
4
- Prevents the orchestrator (or any general persona) from authoring specialist
5
- documents directly. Routing authorship to the owning role is how research,
6
- framing, and domain scrutiny actually fire: writing a PRD without the
7
- product manager bypasses those checks entirely.
8
- -->
1
+ ---
2
+ description: which specialist owns which document type.
3
+ ---
9
4
  # Document Ownership
10
5
 
11
6
  A document type names a body of work. That body of work has an owner. The orchestrator routes; it does not author.
@@ -1,11 +1,6 @@
1
- <!--
2
- rules/common/efficiency.md: session context and tool-use efficiency standards.
3
-
4
- Applies to all agents operating in a Construct session. Violations compound context
5
- cost and reduce throughput. These rules are enforced by read-tracker.mjs and surfaced
6
- in the session-start efficiency digest.
7
- -->
8
-
1
+ ---
2
+ description: session context and tool-use efficiency standards.
3
+ ---
9
4
  # Session Efficiency
10
5
 
11
6
  ## Read discipline
@@ -1,11 +1,6 @@
1
- <!--
2
- rules/common/framing.md: how to frame a problem before acting on it.
3
-
4
- Establishes the hard separation between execution artifacts (tickets, chat
5
- transcripts, existing docs) and sources of truth (the underlying problem).
6
- Applies to every specialist working on architecture, documentation, research,
7
- product, or strategy work. Read before scaffolding anything.
8
- -->
1
+ ---
2
+ description: how to frame a problem before acting on it.
3
+ ---
9
4
  # Framing Policy
10
5
 
11
6
  Most agent failures on ambiguous work trace to a single mistake: anchoring on the most concrete input available (usually a ticket or a prior doc) and building the output around its structure instead of around the actual problem.
@@ -1,8 +1,6 @@
1
- <!--
2
- rules/common/git-workflow.md: commit message format and PR workflow.
3
-
4
- Defines conventional commit types and pull request creation steps.
5
- -->
1
+ ---
2
+ description: commit message format and PR workflow.
3
+ ---
6
4
  # Git Workflow
7
5
 
8
6
  ## Commit Message Format
@@ -1,14 +1,6 @@
1
- <!--
2
- rules/common/no-fabrication.md: canonical anti-fabrication policy for Construct.
3
-
4
- Defines the trust contract between operators and the system: outputs stick to
5
- source, gaps stay visible, and confidence reflects evidence. Applies to every
6
- specialist, every artifact, and every summary — intake processing, document
7
- evaluation, knowledge writing, plan drafting, review verdicts, handoffs.
8
-
9
- Sibling rules: research.md (evidence hierarchy), framing.md (execution
10
- artifacts are not sources), comments.md (banned voice patterns).
11
- -->
1
+ ---
2
+ description: canonical anti-fabrication policy for Construct.
3
+ ---
12
4
  # No-Fabrication Policy
13
5
 
14
6
  Fabrication is the single largest threat to trust in an agent system. A persona that invents a customer quote, sharpens a vague signal into a confident assertion, or papers over a gap with plausible-sounding prose corrupts every artifact downstream. This rule applies to **every output** Construct produces: intake summaries, classification rationales, PRDs, ADRs, RFCs, knowledge notes, handoffs, review verdicts, plan entries, beads issues, MCP tool responses, dashboard text.
@@ -61,7 +53,7 @@ Fabrication is the single largest threat to trust in an agent system. A persona
61
53
  ## Enforcement
62
54
 
63
55
  - `lib/comment-lint.mjs` enforces a subset of these patterns on artifact paths (`docs/prd/**`, `docs/adr/**`, `docs/rfc/**`, `docs/research/**`, `.cx/knowledge/**`, `.cx/handoffs/**`, `.cx/research/**`). PostToolUse warns; `npm run lint:comments`, `construct lint:comments`, and the release gate block.
64
- - `specialists/contracts.json` postconditions check structural requirements (mandatory sections, intake traceability, citation density). `lib/contracts/validate.mjs#validateHandoff` blocks handoffs that fail validation when `CONSTRUCT_CONTRACT_ENFORCEMENT=block`.
56
+ - `specialists/contracts.json` postconditions check structural requirements (mandatory sections, intake traceability, citation density). `lib/contracts/validate.mjs#validateHandoff` blocks handoffs that fail validation; binary postconditions in `lib/specialists/postconditions.mjs` block rubber-stamp reviews, post-hoc threat models, symptom-only fixes, stale-doc PRs, and post-hoc accessibility. Enforcement is hard-default `block`.
65
57
  - `construct intake done <id> --output=<path>` stamps `intake_id`, `intake_confidence`, and `intake_rationale` into the artifact's frontmatter so every intake-derived artifact carries verifiable provenance.
66
58
 
67
59
  ## Bypass