@geraldmaron/construct 1.0.14 → 1.0.16

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 (275) hide show
  1. package/README.md +21 -2
  2. package/bin/construct +146 -83
  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/db/schema/010_cx_scores.sql +51 -0
  34. package/lib/beads-client.mjs +19 -1
  35. package/lib/cli-commands.mjs +280 -146
  36. package/lib/comment-lint.mjs +5 -1
  37. package/lib/context-state.mjs +15 -3
  38. package/lib/contracts/validate.mjs +72 -18
  39. package/lib/contracts/violation-log.mjs +169 -0
  40. package/lib/document-extract/docling-client.mjs +114 -0
  41. package/lib/document-extract/docling-sidecar.py +122 -0
  42. package/lib/document-extract/whisper-client.mjs +79 -0
  43. package/lib/document-extract.mjs +73 -8
  44. package/lib/document-ingest.mjs +26 -4
  45. package/lib/embed/daemon.mjs +6 -1
  46. package/lib/flavors/loader.mjs +1 -1
  47. package/lib/handoffs/contract.mjs +2 -2
  48. package/lib/hooks/session-start.mjs +1 -1
  49. package/lib/hooks/stop-notify.mjs +7 -0
  50. package/lib/ingest/chunker.mjs +94 -0
  51. package/lib/ingest/pipeline.mjs +53 -0
  52. package/lib/ingest/store.mjs +82 -0
  53. package/lib/install/first-invocation.mjs +5 -1
  54. package/lib/intake/prepare.mjs +22 -4
  55. package/lib/integrations/intake-integrations.mjs +28 -2
  56. package/lib/knowledge/search.mjs +12 -0
  57. package/lib/mcp/server.mjs +142 -1
  58. package/lib/mcp/tools/skills.mjs +6 -3
  59. package/lib/mcp/tools/telemetry.mjs +30 -0
  60. package/lib/mcp/tools/workflow.mjs +6 -2
  61. package/lib/observation-store.mjs +14 -5
  62. package/lib/ollama-manager.mjs +32 -28
  63. package/lib/orchestration-policy.mjs +15 -0
  64. package/lib/prompt-composer.js +11 -1
  65. package/lib/roles/gateway.mjs +30 -1
  66. package/lib/runtime/uv-bootstrap.mjs +129 -0
  67. package/lib/runtime/whisper-bootstrap.mjs +102 -0
  68. package/lib/scheduler/index.mjs +24 -4
  69. package/lib/server/index.mjs +22 -1
  70. package/lib/server/insights.mjs +12 -0
  71. package/lib/server/static/index.html +1 -1
  72. package/lib/setup.mjs +54 -9
  73. package/lib/specialists/postconditions.mjs +1 -1
  74. package/lib/sync/skill-frontmatter.mjs +113 -21
  75. package/lib/tracking-surfaces.mjs +2 -0
  76. package/lib/update.mjs +31 -3
  77. package/lib/upgrade.mjs +31 -4
  78. package/lib/validators/skills.mjs +86 -54
  79. package/package.json +5 -5
  80. package/personas/construct.md +6 -7
  81. package/platforms/claude/CLAUDE.md +43 -15
  82. package/platforms/claude/settings.template.json +1 -1
  83. package/rules/common/beads-hygiene.md +3 -9
  84. package/rules/common/code-review.md +3 -6
  85. package/rules/common/coding-style.md +3 -6
  86. package/rules/common/comments.md +3 -7
  87. package/rules/common/commit-approval.md +3 -6
  88. package/rules/common/cx-agent-routing.md +3 -7
  89. package/rules/common/cx-skill-routing.md +3 -3
  90. package/rules/common/doc-ownership.md +3 -8
  91. package/rules/common/efficiency.md +3 -8
  92. package/rules/common/framing.md +3 -8
  93. package/rules/common/git-workflow.md +3 -5
  94. package/rules/common/no-fabrication.md +4 -12
  95. package/rules/common/patterns.md +3 -5
  96. package/rules/common/release-gates.md +3 -8
  97. package/rules/common/research.md +3 -8
  98. package/rules/common/review-before-change.md +3 -9
  99. package/rules/common/security.md +3 -6
  100. package/rules/common/skill-composition.md +3 -7
  101. package/rules/common/testing.md +3 -6
  102. package/rules/golang/coding-style.md +1 -5
  103. package/rules/golang/hooks.md +1 -5
  104. package/rules/golang/patterns.md +1 -5
  105. package/rules/golang/security.md +1 -5
  106. package/rules/golang/testing.md +1 -5
  107. package/rules/python/coding-style.md +1 -5
  108. package/rules/python/hooks.md +1 -5
  109. package/rules/python/patterns.md +1 -5
  110. package/rules/python/security.md +1 -5
  111. package/rules/python/testing.md +1 -5
  112. package/rules/swift/coding-style.md +1 -5
  113. package/rules/swift/hooks.md +1 -5
  114. package/rules/swift/patterns.md +1 -5
  115. package/rules/swift/security.md +1 -5
  116. package/rules/swift/testing.md +1 -5
  117. package/rules/typescript/coding-style.md +1 -5
  118. package/rules/typescript/hooks.md +1 -5
  119. package/rules/typescript/patterns.md +1 -5
  120. package/rules/typescript/security.md +1 -5
  121. package/rules/typescript/testing.md +1 -5
  122. package/rules/web/coding-style.md +3 -5
  123. package/rules/web/design-quality.md +3 -5
  124. package/rules/web/hooks.md +3 -5
  125. package/rules/web/patterns.md +3 -5
  126. package/rules/web/performance.md +3 -5
  127. package/rules/web/security.md +3 -5
  128. package/rules/web/testing.md +3 -5
  129. package/scripts/sync-specialists.mjs +20 -2
  130. package/skills/ai/agent-dev.md +4 -5
  131. package/skills/ai/llm-security.md +4 -5
  132. package/skills/ai/ml-ops.md +4 -5
  133. package/skills/ai/orchestration-workflow.md +4 -5
  134. package/skills/ai/prompt-and-eval.md +4 -5
  135. package/skills/ai/prompt-optimizer.md +4 -6
  136. package/skills/ai/rag-system.md +4 -5
  137. package/skills/architecture/api-design.md +4 -5
  138. package/skills/architecture/caching.md +4 -5
  139. package/skills/architecture/cloud-native.md +4 -5
  140. package/skills/architecture/message-queue.md +4 -5
  141. package/skills/architecture/security-arch.md +4 -5
  142. package/skills/compliance/ai-disclosure.md +4 -5
  143. package/skills/compliance/data-privacy.md +4 -5
  144. package/skills/compliance/license-audit.md +4 -5
  145. package/skills/compliance/regulatory-review.md +4 -5
  146. package/skills/development/cpp.md +4 -5
  147. package/skills/development/go.md +4 -5
  148. package/skills/development/java.md +4 -5
  149. package/skills/development/kotlin.md +4 -5
  150. package/skills/development/mobile-crossplatform.md +4 -5
  151. package/skills/development/python.md +4 -5
  152. package/skills/development/rust.md +4 -5
  153. package/skills/development/shell.md +4 -5
  154. package/skills/development/swift.md +4 -5
  155. package/skills/development/typescript.md +4 -5
  156. package/skills/devops/ci-cd.md +4 -5
  157. package/skills/devops/containerization.md +4 -5
  158. package/skills/devops/cost-optimization.md +4 -5
  159. package/skills/devops/data-engineering.md +4 -5
  160. package/skills/devops/database.md +4 -5
  161. package/skills/devops/dependency-management.md +4 -5
  162. package/skills/devops/devsecops.md +4 -5
  163. package/skills/devops/git-workflow.md +4 -5
  164. package/skills/devops/incident-response.md +4 -5
  165. package/skills/devops/monorepo.md +4 -5
  166. package/skills/devops/observability.md +4 -5
  167. package/skills/devops/performance.md +4 -5
  168. package/skills/devops/testing.md +4 -5
  169. package/skills/docs/adr-workflow.md +4 -7
  170. package/skills/docs/backlog-proposal-workflow.md +4 -3
  171. package/skills/docs/customer-profile-workflow.md +4 -3
  172. package/skills/docs/document-ingest-workflow.md +4 -3
  173. package/skills/docs/evidence-ingest-workflow.md +4 -3
  174. package/skills/docs/init-docs.md +4 -5
  175. package/skills/docs/init-project.md +4 -5
  176. package/skills/docs/prd-workflow.md +4 -7
  177. package/skills/docs/prfaq-workflow.md +4 -3
  178. package/skills/docs/product-intelligence-review.md +4 -3
  179. package/skills/docs/product-intelligence-workflow.md +4 -6
  180. package/skills/docs/product-signal-workflow.md +4 -5
  181. package/skills/docs/research-workflow.md +4 -5
  182. package/skills/docs/runbook-workflow.md +4 -5
  183. package/skills/docs/strategy-workflow.md +4 -5
  184. package/skills/exploration/dependency-graph-reading.md +4 -7
  185. package/skills/exploration/repo-map.md +4 -5
  186. package/skills/exploration/tracer-bullet-method.md +4 -7
  187. package/skills/exploration/unknown-codebase-onboarding.md +4 -7
  188. package/skills/frameworks/django.md +4 -5
  189. package/skills/frameworks/nextjs.md +4 -5
  190. package/skills/frameworks/react.md +4 -5
  191. package/skills/frameworks/spring-boot.md +4 -5
  192. package/skills/frontend-design/accessibility.md +4 -5
  193. package/skills/frontend-design/component-patterns.md +4 -5
  194. package/skills/frontend-design/engineering.md +4 -5
  195. package/skills/frontend-design/state-management.md +4 -5
  196. package/skills/frontend-design/ui-aesthetics.md +4 -5
  197. package/skills/frontend-design/ux-principles.md +4 -5
  198. package/skills/operating/change-management.md +4 -8
  199. package/skills/operating/incident-response.md +4 -8
  200. package/skills/operating/oncall-rotation.md +4 -7
  201. package/skills/operating/orchestration-reference.md +4 -10
  202. package/skills/quality-gates/review-work.md +4 -5
  203. package/skills/quality-gates/verify-change.md +4 -5
  204. package/skills/quality-gates/verify-module.md +4 -5
  205. package/skills/quality-gates/verify-quality.md +4 -5
  206. package/skills/quality-gates/verify-security.md +4 -5
  207. package/skills/roles/architect.ai-systems.md +6 -9
  208. package/skills/roles/architect.data.md +6 -9
  209. package/skills/roles/architect.enterprise.md +6 -9
  210. package/skills/roles/architect.integration.md +6 -9
  211. package/skills/roles/architect.md +7 -14
  212. package/skills/roles/architect.platform.md +6 -9
  213. package/skills/roles/data-analyst.experiment.md +6 -9
  214. package/skills/roles/data-analyst.md +6 -9
  215. package/skills/roles/data-analyst.product-intelligence.md +7 -9
  216. package/skills/roles/data-analyst.product.md +6 -9
  217. package/skills/roles/data-analyst.telemetry.md +7 -9
  218. package/skills/roles/data-engineer.pipeline.md +6 -9
  219. package/skills/roles/data-engineer.vector-retrieval.md +7 -9
  220. package/skills/roles/data-engineer.warehouse.md +6 -9
  221. package/skills/roles/debugger.md +6 -9
  222. package/skills/roles/designer.accessibility.md +6 -9
  223. package/skills/roles/designer.md +7 -9
  224. package/skills/roles/engineer.ai.md +6 -9
  225. package/skills/roles/engineer.data.md +6 -9
  226. package/skills/roles/engineer.md +9 -9
  227. package/skills/roles/engineer.platform.md +6 -9
  228. package/skills/roles/operator.docs.md +6 -9
  229. package/skills/roles/operator.md +9 -9
  230. package/skills/roles/operator.release.md +6 -9
  231. package/skills/roles/operator.sre.md +6 -9
  232. package/skills/roles/orchestrator.md +6 -9
  233. package/skills/roles/product-manager.ai-product.md +6 -9
  234. package/skills/roles/product-manager.business-strategy.md +6 -9
  235. package/skills/roles/product-manager.enterprise.md +6 -9
  236. package/skills/roles/product-manager.growth.md +7 -9
  237. package/skills/roles/product-manager.md +7 -9
  238. package/skills/roles/product-manager.platform.md +6 -9
  239. package/skills/roles/product-manager.product.md +6 -9
  240. package/skills/roles/qa.ai-eval.md +8 -9
  241. package/skills/roles/qa.api-contract.md +7 -9
  242. package/skills/roles/qa.data-pipeline.md +7 -9
  243. package/skills/roles/qa.md +7 -9
  244. package/skills/roles/qa.test-automation.md +6 -9
  245. package/skills/roles/qa.web-ui.md +7 -9
  246. package/skills/roles/researcher.explorer.md +6 -9
  247. package/skills/roles/researcher.md +8 -9
  248. package/skills/roles/researcher.ux.md +6 -9
  249. package/skills/roles/reviewer.devil-advocate.md +6 -9
  250. package/skills/roles/reviewer.evaluator.md +6 -9
  251. package/skills/roles/reviewer.md +9 -9
  252. package/skills/roles/reviewer.trace.md +6 -9
  253. package/skills/roles/security.ai.md +7 -9
  254. package/skills/roles/security.appsec.md +6 -9
  255. package/skills/roles/security.cloud.md +6 -9
  256. package/skills/roles/security.legal-compliance.md +6 -9
  257. package/skills/roles/security.md +7 -9
  258. package/skills/roles/security.privacy.md +7 -9
  259. package/skills/roles/security.supply-chain.md +7 -9
  260. package/skills/security/blue-team.md +4 -5
  261. package/skills/security/code-audit.md +4 -5
  262. package/skills/security/pentest.md +4 -5
  263. package/skills/security/red-team.md +4 -5
  264. package/skills/security/threat-intel.md +4 -5
  265. package/skills/security/vuln-research.md +4 -5
  266. package/skills/strategy/competitive-landscape.md +4 -8
  267. package/skills/strategy/market-research-methods.md +4 -8
  268. package/skills/strategy/narrative-arc.md +4 -8
  269. package/skills/strategy/pricing-positioning.md +4 -8
  270. package/skills/utility/clean-code.md +4 -5
  271. package/specialists/policy-inventory.json +14 -0
  272. package/lib/specialist-contracts-enforce.mjs +0 -172
  273. package/rules/common/agents.md +0 -28
  274. package/rules/common/development-workflow.md +0 -32
  275. 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; }
package/lib/update.mjs CHANGED
@@ -10,6 +10,8 @@ import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { spawnSync } from 'node:child_process';
12
12
 
13
+ import { writeVersionStamp } from './maintenance/cleanup.mjs';
14
+
13
15
  const PACKAGE_NAME = '@geraldmaron/construct';
14
16
 
15
17
  function readPackageJson(dir) {
@@ -70,6 +72,15 @@ export function buildUpdatePlan({ cwd }) {
70
72
  args: [binPath, 'sync', '--no-docs'],
71
73
  cwd: sourceRoot,
72
74
  },
75
+ // Sweep legacy cx-*.md/.toml/.prompt.md left over from older releases
76
+ // at user scope, so the next doctor doesn't surface the warning.
77
+ {
78
+ label: 'Sweep legacy agents at user scope',
79
+ command: process.execPath,
80
+ args: [binPath, 'doctor', '--fix-legacy-agents'],
81
+ cwd: sourceRoot,
82
+ optional: true,
83
+ },
73
84
  {
74
85
  label: 'Run health checks',
75
86
  command: process.execPath,
@@ -84,14 +95,24 @@ function formatCommand(command, args) {
84
95
  return [command, ...args].join(' ');
85
96
  }
86
97
 
87
- function runStep(step, { spawn, env }) {
98
+ function runStep(step, { spawn, env, stdout }) {
88
99
  const result = spawn(step.command, step.args, {
89
100
  cwd: step.cwd,
90
101
  env,
91
102
  stdio: 'inherit',
92
103
  });
93
- if (result?.error) throw result.error;
104
+ if (result?.error) {
105
+ if (step.optional) {
106
+ stdout?.write?.(` (skipped: ${result.error.message})\n`);
107
+ return;
108
+ }
109
+ throw result.error;
110
+ }
94
111
  if ((result?.status ?? 1) !== 0) {
112
+ if (step.optional) {
113
+ stdout?.write?.(` (skipped: exit ${result?.status ?? 1})\n`);
114
+ return;
115
+ }
95
116
  throw new Error(`${step.label} failed with exit code ${result.status ?? 1}`);
96
117
  }
97
118
  }
@@ -107,9 +128,16 @@ export function runUpdate({ cwd, env = process.env, spawn = spawnSync, stdout =
107
128
  for (const step of plan.steps) {
108
129
  stdout.write(`→ ${step.label}\n`);
109
130
  stdout.write(` ${formatCommand(step.command, step.args)}\n`);
110
- runStep(step, { spawn, env });
131
+ runStep(step, { spawn, env, stdout });
111
132
  }
112
133
 
134
+ // Advance the cleanup stamp to the checkout version so the next CLI
135
+ // invocation doesn't run a full cleanup pass on the assumption that
136
+ // an upgrade just happened.
137
+ try {
138
+ writeVersionStamp({ version: plan.version, summary: { freedBytes: 0, durationMs: 0 } });
139
+ } catch { /* stamp write is best-effort */ }
140
+
113
141
  stdout.write('\n✓ Construct updated from current checkout.\n');
114
142
  return plan;
115
143
  }
package/lib/upgrade.mjs CHANGED
@@ -10,6 +10,8 @@ import path from 'node:path';
10
10
  import { spawnSync } from 'node:child_process';
11
11
  import { fileURLToPath } from 'node:url';
12
12
 
13
+ import { writeVersionStamp } from './maintenance/cleanup.mjs';
14
+
13
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
16
  const ROOT_DIR = path.resolve(__dirname, '..');
15
17
  const PACKAGE_NAME = '@geraldmaron/construct';
@@ -97,7 +99,7 @@ export function runUpgrade({ cwd = process.cwd(), env = process.env, stdout = pr
97
99
  }
98
100
 
99
101
  stdout.write('Upgrading...\n\n');
100
-
102
+
101
103
  const steps = [
102
104
  {
103
105
  label: 'Fetching latest from npm',
@@ -109,6 +111,15 @@ export function runUpgrade({ cwd = process.cwd(), env = process.env, stdout = pr
109
111
  command: 'construct',
110
112
  args: ['sync', '--no-docs'],
111
113
  },
114
+ // Sweep legacy cx-*.md/.toml/.prompt.md left behind by older releases at
115
+ // user scope. Without this the user gets a doctor warning every session
116
+ // until they manually run `construct doctor --fix-legacy-agents`.
117
+ {
118
+ label: 'Sweeping legacy agents at user scope',
119
+ command: 'construct',
120
+ args: ['doctor', '--fix-legacy-agents'],
121
+ optional: true,
122
+ },
112
123
  {
113
124
  label: 'Running health checks',
114
125
  command: 'construct',
@@ -123,18 +134,34 @@ export function runUpgrade({ cwd = process.cwd(), env = process.env, stdout = pr
123
134
  env,
124
135
  stdio: 'inherit',
125
136
  });
126
-
137
+
127
138
  if (result.error) {
139
+ if (step.optional) {
140
+ stdout.write(` (skipped: ${result.error.message})\n`);
141
+ continue;
142
+ }
128
143
  stdout.write(`\n✗ ${step.label} failed: ${result.error.message}\n`);
129
144
  return { success: false, reason: 'step_failed', step: step.label };
130
145
  }
131
-
146
+
132
147
  if (result.status !== 0) {
148
+ if (step.optional) {
149
+ stdout.write(` (skipped: exit ${result.status})\n`);
150
+ continue;
151
+ }
133
152
  stdout.write(`\n✗ ${step.label} failed with exit code ${result.status}\n`);
134
153
  return { success: false, reason: 'step_failed', step: step.label };
135
154
  }
136
155
  }
137
-
156
+
157
+ // Advance the cleanup stamp so the next CLI invocation doesn't re-run a
158
+ // full cleanup pass under the assumption that the user just upgraded.
159
+ // Without this, every command pays the cleanup cost until something
160
+ // else writes the stamp.
161
+ try {
162
+ writeVersionStamp({ version: latestVersion, summary: { freedBytes: 0, durationMs: 0 } });
163
+ } catch { /* stamp write is best-effort */ }
164
+
138
165
  stdout.write('\n');
139
166
  stdout.write(`✓ Upgraded to ${latestVersion}\n`);
140
167
  return { success: true, upgraded: true, from: currentVersion, to: latestVersion };
@@ -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.16",
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`.
@@ -37,6 +34,8 @@ Execution model:
37
34
 
38
35
  Devil's advocate is mandatory for new architectural directions, AI/agent workflow changes, security or data-integrity changes, and promoting a temporary capability to persistent.
39
36
 
37
+ Orchestrated dispatches emit a task-packet with `goal`, `intent`, `workCategory`, `riskFlags`, `acceptanceCriteria` before naming specialists (`specialists/contracts.json:construct-to-orchestrator`).
38
+
40
39
  ## Gates and contracts (org-in-a-box)
41
40
 
42
41
  `routeRequest` returns three artifacts; honor all three: