@geraldmaron/construct 1.0.11 โ†’ 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **One AI interface. A team of specialists behind it. Hard gates. Runs locally, or deploys for teams.**
4
4
 
5
- ๐Ÿ“– **[Read the docs โ†’](https://geraldmaron.github.io/construct/)** ยท ๐Ÿš€ **[5-minute quickstart โ†’](https://geraldmaron.github.io/construct/docs/start)** ยท ๐Ÿ“ฆ `npm install -g @geraldmaron/construct`
5
+ ๐Ÿ“– **[Read the docs โ†’](https://geraldmaron.github.io/construct/)** ยท ๐Ÿš€ **[5-minute quickstart โ†’](https://geraldmaron.github.io/construct/start)** ยท ๐Ÿ“ฆ `npm install -g @geraldmaron/construct`
6
6
 
7
7
  ---
8
8
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  Construct sits on top of Claude Code, OpenCode, Codex, Cursor, and Copilot. You talk to one persona called `construct`. Behind it is a team of specialists shaped by your **org profile**: software R&D by default, with curated profiles for operations, creative, and research orgs, plus a schema-validated escape hatch for custom profiles. Each profile organizes its specialists by department (Product, Engineering, Operations, etc.) and carries its own intake taxonomy, doc templates, and role set. Sessions survive boundary changes via durable state in `.cx/`, beads, and a local vector index. Solo by default. Can deploy centrally for teams that want shared memory, telemetry, queues, and policy.
12
12
 
13
- `construct profile show|list|set <id>` to switch. See [Profile lifecycle](https://geraldmaron.github.io/construct/docs/concepts/profile-lifecycle) for how new profiles are built (it's a research process, not a JSON exercise).
13
+ `construct profile show|list|set <id>` to switch. See [Profile lifecycle](https://geraldmaron.github.io/construct/concepts/profile-lifecycle) for how new profiles are built (it's a research process, not a JSON exercise).
14
14
 
15
15
  The team and enterprise modes exist because I wanted to learn what shipping a real multi-tenant tool would look like. The project is still open source, the code is still public, and the bar is still "does this help me learn." Run it solo if that's all you need.
16
16
 
@@ -39,20 +39,33 @@ Open your editor and talk to `@construct`. A walkthrough lives in `construct_gui
39
39
 
40
40
  No Node? Try `brew install geraldmaron/construct/construct`. Cloning a project that already uses Construct? `npx -y @geraldmaron/construct init` wires it up.
41
41
 
42
- [Five minute walkthrough](https://geraldmaron.github.io/construct/docs/start).
42
+ [Five minute walkthrough](https://geraldmaron.github.io/construct/start).
43
+
44
+ ## Usage
45
+
46
+ Most days, the loop is:
47
+
48
+ ```bash
49
+ construct status # confirm services and editor adapters are healthy
50
+ construct sync # refresh host adapters after registry, prompt, or config changes
51
+ construct intake list # review new signals, if your project uses the inbox
52
+ construct doctor # diagnose install, service, MCP, and adapter drift
53
+ ```
54
+
55
+ In your editor, start with `@construct`. Ask for the outcome, not the specialist. Construct routes to the right specialist chain, keeps durable state in `.cx/` and Beads, and blocks risky mutations until the configured gates pass.
43
56
 
44
57
  ## What you can do
45
58
 
46
59
  | If you want to... | Read |
47
60
  |---|---|
48
- | Install and run a first task | [Start](https://geraldmaron.github.io/construct/docs/start) |
49
- | Understand how it works | [Architecture](https://geraldmaron.github.io/construct/docs/concepts/architecture) |
50
- | Pick a deployment mode | [Deployment model](https://geraldmaron.github.io/construct/docs/concepts/deployment-model) |
51
- | Drop a signal and triage it | [Intake and triage](https://geraldmaron.github.io/construct/docs/concepts/intake-and-triage) |
52
- | Add a custom specialist | [Add a custom agent](https://geraldmaron.github.io/construct/docs/cookbook/add-a-custom-agent) |
53
- | Fix a blocked commit or red CI | [Fix a policy violation](https://geraldmaron.github.io/construct/docs/cookbook/fix-a-policy-violation) |
54
- | Plug in your own LLM | [Plug in your own LLM](https://geraldmaron.github.io/construct/docs/cookbook/plug-in-your-own-llm) |
55
- | Look up a CLI command | [CLI reference](https://geraldmaron.github.io/construct/docs/reference/cli) |
61
+ | Install and run a first task | [Start](https://geraldmaron.github.io/construct/start) |
62
+ | Understand how it works | [Architecture](https://geraldmaron.github.io/construct/concepts/architecture) |
63
+ | Pick a deployment mode | [Deployment model](https://geraldmaron.github.io/construct/concepts/deployment-model) |
64
+ | Drop a signal and triage it | [Intake and triage](https://geraldmaron.github.io/construct/concepts/intake-and-triage) |
65
+ | Add a custom specialist | [Add a custom specialist](https://geraldmaron.github.io/construct/cookbook/add-a-custom-agent) |
66
+ | Fix a blocked commit or red CI | [Fix a policy violation](https://geraldmaron.github.io/construct/cookbook/fix-a-policy-violation) |
67
+ | Plug in your own LLM | [Plug in your own LLM](https://geraldmaron.github.io/construct/cookbook/plug-in-your-own-llm) |
68
+ | Look up a CLI command | [CLI reference](https://geraldmaron.github.io/construct/reference/cli) |
56
69
 
57
70
  Works with Anthropic, OpenRouter, Ollama, and other OpenAI-compatible providers.
58
71
 
@@ -64,21 +77,21 @@ Three modes. `solo` is the default and runs everything locally. Filesystem queue
64
77
 
65
78
  `enterprise` adds tenant isolation, RBAC and ABAC scaffolding, isolated worker containers, signed MCP allowlists, and mandatory audit.
66
79
 
67
- Pick or change modes with `construct config mode [solo|team|enterprise]`. [Deployment model](https://geraldmaron.github.io/construct/docs/concepts/deployment-model).
80
+ Pick or change modes with `construct config mode [solo|team|enterprise]`. [Deployment model](https://geraldmaron.github.io/construct/concepts/deployment-model).
68
81
 
69
82
  ## Intake
70
83
 
71
84
  Anything dropped into `.cx/inbox/` (a bug report, a customer comment, a competitor PDF, a postmortem draft) is classified by the active profile's intake taxonomy. The default `rnd` profile uses bug, user-signal, experiment, architecture, incident, security, requirement, research, ops, eval-finding, launch-asset, legal-compliance. The `operations` profile uses request, incident, ops, security, docs. The `creative` profile uses brief, content-request, asset, experiment, report. The `research` profile uses question, study, synthesis, report.
72
85
 
73
- Each signal gets a primary owner and a recommended handoff chain. Inspect with `construct intake list` and `construct intake show <id>`. Generate a task graph with `construct graph from-intake <id>`. The classifier runs in the daemon and is deterministic. The agent in your editor does the actual analysis. [Intake and triage](https://geraldmaron.github.io/construct/docs/concepts/intake-and-triage).
86
+ Each signal gets a primary owner and a recommended handoff chain. Inspect with `construct intake list` and `construct intake show <id>`. Generate a task graph with `construct graph from-intake <id>`. The classifier runs in the daemon and is deterministic. The agent in your editor does the actual analysis. [Intake and triage](https://geraldmaron.github.io/construct/concepts/intake-and-triage).
74
87
 
75
88
  ## Hard gates
76
89
 
77
- Every code mutation runs through enforcement. No secrets committed, tests green, docs current, comments lint-clean, CI passes. Gates live in three places: write-time, commit-time, CI safety net. They can only be bypassed with explicit env vars so every exception leaves an audit trail. [Gates and enforcement](https://geraldmaron.github.io/construct/docs/concepts/gates-and-enforcement).
90
+ Every code mutation runs through enforcement. No secrets committed, tests green, docs current, comments lint-clean, CI passes. Gates live in three places: write-time, commit-time, CI safety net. They can only be bypassed with explicit env vars so every exception leaves an audit trail. [Gates and enforcement](https://geraldmaron.github.io/construct/concepts/gates-and-enforcement).
78
91
 
79
92
  ## Learning loops
80
93
 
81
- Construct gets smarter on its own. Every session ends with an automatic capture: tools used, files touched, what the final reply said. That goes into `.cx/observations/` and is searchable from the next session. See [`docs/concepts/learning-loops.md`](./docs/concepts/learning-loops.md) for what's wired, what's coming, and how to turn pieces off.
94
+ Construct gets smarter on its own. Every session ends with an automatic capture: tools used, files touched, what the final reply said. That goes into `.cx/observations/` and is searchable from the next session. See [`docs/concepts/learning-loops.mdx`](./docs/concepts/learning-loops.mdx) for what's wired, what's coming, and how to turn pieces off.
82
95
 
83
96
  ## Core commands
84
97
 
@@ -124,6 +137,16 @@ Construct gets smarter on its own. Every session ends with an automatic capture:
124
137
  | `construct workflow` | Instantiate workflow templates (PRD-to-review chains, onboarding, handoffs) |
125
138
  | `construct workspace` | Manage PM workspaces for multi-PM signal routing |
126
139
 
140
+ ### Models & Integrations
141
+
142
+ | Command | What it does |
143
+ |---|---|
144
+ | `construct claude:allow` | Manage Claude Code `permissions.allow` from the outside (auto-classifier blocks the agent from editing it) |
145
+ | `construct hosts` | Show host support for Construct orchestration |
146
+ | `construct mcp` | Manage MCP integrations |
147
+ | `construct models` | Show or update model tier assignments |
148
+ | `construct plugin` | Manage external Construct plugin manifests |
149
+
127
150
  ### Integrations
128
151
 
129
152
  | Command | What it does |
@@ -145,6 +168,14 @@ Construct gets smarter on its own. Every session ends with an automatic capture:
145
168
  | `construct telemetry` | Query telemetry traces and latency data |
146
169
  | `construct telemetry-backfill` | Backfill sparse traces with observations (trace backend) |
147
170
 
171
+ ### Diagnostics
172
+
173
+ | Command | What it does |
174
+ |---|---|
175
+ | `construct audit` | Audit Construct internals and review the mutation trail |
176
+ | `construct cleanup` | Release dev-agent memory pressure by cleaning stale helper and bridge processes |
177
+ | `construct doc` | Verify or inspect auditability stamps on Construct-generated markdown files |
178
+
148
179
  ### Advanced
149
180
 
150
181
  | Command | What it does |
@@ -176,7 +207,7 @@ Construct gets smarter on its own. Every session ends with an automatic capture:
176
207
 
177
208
  - [`CONTRIBUTING.md`](./CONTRIBUTING.md). Branch workflow, gates, review expectations.
178
209
  - [`CHANGELOG.md`](./CHANGELOG.md). Release history.
179
- - [`docs/concepts/architecture.md`](./docs/concepts/architecture.md). Canonical architecture.
210
+ - [`docs/concepts/architecture.mdx`](./docs/concepts/architecture.mdx). Canonical architecture.
180
211
  - [`AGENTS.md`](./AGENTS.md). Agent operating contract.
181
212
 
182
213
  ## Project structure
package/lib/auto-docs.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * lib/auto-docs.mjs โ€” regenerate managed regions in markdown docs and build the MkDocs site tree.
2
+ * lib/auto-docs.mjs โ€” regenerate managed regions in markdown docs and generated docs-site references.
3
3
  *
4
4
  * Managed regions are HTML comment markers in the form:
5
5
  * <!-- AUTO:region-name -->
@@ -9,8 +9,8 @@
9
9
  * Running regenerateDocs() is idempotent. With check:true it returns whether
10
10
  * anything would change without writing files โ€” the mode CI uses to detect drift.
11
11
  *
12
- * buildSite() writes site/docs/ from the same sources feeding the AUTO regions
13
- * so the GitHub Pages site never drifts from the in-repo markdown.
12
+ * buildSite() is retained for compatibility; the public site now renders docs/
13
+ * directly through Fumadocs and buildFumadocsReference() emits generated pages.
14
14
  */
15
15
 
16
16
  import fs from 'node:fs';
@@ -93,7 +93,7 @@ const DIR_DESCRIPTIONS = {
93
93
  opencode: 'OpenCode integration config',
94
94
  personas: 'Persona prompt definitions',
95
95
  rules: 'Coding and quality standards',
96
- site: 'MkDocs source for the GitHub Pages documentation site',
96
+ site: 'Legacy docs-site output',
97
97
  skills: 'Reusable domain knowledge files',
98
98
  tests: 'Test suite',
99
99
  };
@@ -237,12 +237,12 @@ export async function regenerateDocs({ rootDir, check = false } = {}) {
237
237
  * Check CLI command coverage against how-to links in docs/README.md.
238
238
  *
239
239
  * Returns an object with:
240
- * - covered: string[] commands that have a linked how-to
241
- * - uncovered: string[] commands with no how-to link in docs/README.md
240
+ * - covered: string[] commands that have a linked guide
241
+ * - uncovered: string[] commands with no linked guide in docs/README.md
242
242
  * - total: number
243
243
  *
244
- * A command is considered covered if its name appears in any markdown link
245
- * inside the how-to guides section of docs/README.md.
244
+ * A command is considered covered if its name appears in docs/README.md or in
245
+ * a linked cookbook/how-to guide.
246
246
  */
247
247
  export function checkDocsCoverage({ rootDir } = {}) {
248
248
  rootDir = rootDir ?? process.cwd();
@@ -251,11 +251,10 @@ export function checkDocsCoverage({ rootDir } = {}) {
251
251
  // Extract all href targets from markdown links in docs/README.md
252
252
  const linkTargets = [...docsReadme.matchAll(/\[.*?\]\((.*?)\)/g)].map(m => m[1]);
253
253
 
254
- // Build a combined corpus: docs/README.md + every linked how-to file
255
- const howToDir = path.join(rootDir, 'docs', 'how-to');
254
+ // Build a combined corpus: docs/README.md + every linked guide file
256
255
  let corpus = docsReadme;
257
256
  for (const target of linkTargets) {
258
- if (!target.startsWith('./how-to/')) continue;
257
+ if (!target.startsWith('./how-to/') && !target.startsWith('./cookbook/')) continue;
259
258
  const filePath = path.join(rootDir, 'docs', target.replace(/^\.\//, ''));
260
259
  const content = readFile(filePath);
261
260
  if (content) corpus += '\n' + content;
@@ -307,15 +306,13 @@ export function checkDocsCoverage({ rootDir } = {}) {
307
306
  // Categories surfaced in the Fumadocs reference. Internal commands (e.g.,
308
307
  // `construct hook <name>`) are not user-facing reference material.
309
308
  const REFERENCE_CATEGORIES = [
310
- 'Services',
311
- 'Agents & Sync',
309
+ 'Core',
312
310
  'Work',
313
- 'Embed',
314
311
  'Models & Integrations',
315
- 'Providers',
312
+ 'Integrations',
316
313
  'Observability',
317
- 'Docs',
318
314
  'Diagnostics',
315
+ 'Advanced',
319
316
  ];
320
317
 
321
318
  function slugify(category) {
@@ -749,8 +749,10 @@ for (const cat of Object.keys(CLI_COMMANDS_BY_CATEGORY)) {
749
749
  export const CATEGORY_ORDER = [
750
750
  'Core',
751
751
  'Work',
752
+ 'Models & Integrations',
752
753
  'Integrations',
753
754
  'Observability',
755
+ 'Diagnostics',
754
756
  'Advanced',
755
757
  ];
756
758
 
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * lib/hooks/registry-sync.mjs โ€” Registry sync hook โ€” reminds to run construct sync after registry changes.
4
4
  *
5
- * Runs as PostToolUse after edits to agents/registry.json. Checks if the file was modified and emits a reminder to run construct sync to regenerate platform adapters.
5
+ * Runs as PostToolUse after edits to specialists/registry.json. Checks if the file was modified and emits a reminder to run construct sync to regenerate platform adapters.
6
6
  *
7
7
  * @p95ms 12000
8
8
  * @maxBlockingScope none (PostToolUse, non-blocking)
@@ -0,0 +1,71 @@
1
+ /**
2
+ * lib/sync/skill-frontmatter.mjs โ€” Build Anthropic Agent Skills frontmatter
3
+ * for SKILL.md files emitted by sync-specialists.
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.
10
+ *
11
+ * Also exports a conservative stripLeadingFrontmatter helper so we never
12
+ * emit double-frontmatter when the source body already has its own block.
13
+ */
14
+
15
+ 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
+ const SOURCE_HEADER_RE = /^skills\/[\w./-]+\s*(?:\(([^)]+)\))?\s*([\s\S]*?)$/;
23
+
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;
27
+
28
+ 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`;
33
+ }
34
+
35
+ export function extractSkillDescription(content) {
36
+ if (!content) return null;
37
+ const commentMatch = content.match(HTML_COMMENT_RE);
38
+ if (commentMatch) {
39
+ const inner = commentMatch[1].trim();
40
+ const headerMatch = inner.match(SOURCE_HEADER_RE);
41
+ if (headerMatch) {
42
+ const prose = (headerMatch[2] || '').trim();
43
+ if (prose) return firstSentence(prose);
44
+ }
45
+ const lines = inner.split('\n').map((l) => l.trim()).filter(Boolean);
46
+ if (lines.length >= 2) return firstSentence(lines.slice(1).join(' '));
47
+ }
48
+ const body = content.replace(HTML_COMMENT_RE, '').trim();
49
+ const paragraphs = body.split(/\n\s*\n/);
50
+ for (const p of paragraphs) {
51
+ const trimmed = p.trim();
52
+ if (trimmed.startsWith('#')) continue;
53
+ if (trimmed) return firstSentence(trimmed);
54
+ }
55
+ return null;
56
+ }
57
+
58
+ function firstSentence(text) {
59
+ const match = text.match(/^[\s\S]*?[.!?](?:\s|$)/);
60
+ return (match ? match[0] : text).trim();
61
+ }
62
+
63
+ export function stripLeadingFrontmatter(content) {
64
+ if (!content) return content;
65
+ if (!content.startsWith('---\n') && !content.startsWith('---\r\n')) return content;
66
+ const closeIdx = content.indexOf('\n---', 4);
67
+ if (closeIdx === -1) return content;
68
+ const afterClose = content.indexOf('\n', closeIdx + 1);
69
+ if (afterClose === -1) return '';
70
+ return content.slice(afterClose + 1);
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geraldmaron/construct",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "type": "module",
5
5
  "description": "Construct โ€” agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
6
6
  "bin": {
@@ -41,6 +41,7 @@ import { inlineRoleAntiPatterns, PROMPT_WORD_CAP } from "../lib/role-preload.mjs
41
41
  import { resolveActiveProfile } from "../lib/profiles/loader.mjs";
42
42
  import { resolveTiersForPrimary } from "../lib/model-router.mjs";
43
43
  import { stampFrontmatter } from "../lib/doc-stamp.mjs";
44
+ import { buildSkillFrontmatter, stripLeadingFrontmatter } from "../lib/sync/skill-frontmatter.mjs";
44
45
 
45
46
  const home = os.homedir();
46
47
  const root = path.resolve(import.meta.dirname, "..");
@@ -180,9 +181,18 @@ function releaseLock() {
180
181
  */
181
182
  const _stagedPairs = []; // [{ staging, real, content }]
182
183
 
183
- function writeFile(file, content) {
184
+ function writeFile(file, content, { stamp = true } = {}) {
184
185
  mkdirp(path.dirname(file));
185
- const stamped = file.endsWith('.md') ? stampFrontmatter(content, { generator: 'construct/sync-specialists' }) : content;
186
+ // Doc-stamp wraps content with cx_doc_id + body_hash YAML frontmatter for
187
+ // tamper detection. That's right for content artifacts (research findings,
188
+ // knowledge files), wrong for host-platform adapter files that have their
189
+ // own frontmatter contract (Claude Code agents, Copilot prompts, Anthropic
190
+ // Agent Skills) or are user-managed (CLAUDE.md, copilot-instructions.md).
191
+ // Stamping those produces double-frontmatter that breaks the host loader.
192
+ // Callers writing those files pass { stamp: false }.
193
+
194
+ const shouldStamp = stamp && file.endsWith('.md');
195
+ const stamped = shouldStamp ? stampFrontmatter(content, { generator: 'construct/sync-specialists' }) : content;
186
196
 
187
197
  if (DRY_RUN) {
188
198
  // Stage in memory only โ€” compare against current on-disk content.
@@ -641,7 +651,7 @@ function syncClaude(entries, targetDir = null) {
641
651
  for (const entry of entries) {
642
652
  const name = adapterName(entry);
643
653
  const md = claudeAgentMarkdown(entry, entries);
644
- writeFile(path.join(claudeAgentsDir, `${name}.md`), md);
654
+ writeFile(path.join(claudeAgentsDir, `${name}.md`), md, { stamp: false });
645
655
  }
646
656
  removeStaleAdapters(claudeAgentsDir, ".md", entries);
647
657
 
@@ -663,7 +673,8 @@ ${personaList}
663
673
  ## Internal Specialists
664
674
 
665
675
  ${specialistList || "(all specialists are internal โ€” routed through Construct)"}`;
666
- writeFile(claudeMdPath, replaceManagedBlock(existing, note, mdManagedStart, mdManagedEnd));
676
+ // User-managed file with our managed-block carved out โ€” never doc-stamp.
677
+ writeFile(claudeMdPath, replaceManagedBlock(existing, note, mdManagedStart, mdManagedEnd), { stamp: false });
667
678
 
668
679
  // Sync MCP servers into ~/.claude/settings.json if it exists
669
680
  const claudeSettingsPath = path.join(home, ".claude", "settings.json");
@@ -797,7 +808,7 @@ function syncCopilot(entries) {
797
808
  const promptsDir = path.join(home, ".github", "prompts");
798
809
  if (!DRY_RUN) mkdirp(promptsDir);
799
810
  for (const entry of entries) {
800
- writeFile(path.join(promptsDir, `${adapterName(entry)}.prompt.md`), copilotPrompt(entry, entries));
811
+ writeFile(path.join(promptsDir, `${adapterName(entry)}.prompt.md`), copilotPrompt(entry, entries), { stamp: false });
801
812
  }
802
813
  removeStaleAdapters(promptsDir, ".prompt.md", entries);
803
814
 
@@ -811,7 +822,8 @@ function syncCopilot(entries) {
811
822
  Copilot does not expose true spawnable subagents. Use these reusable prompt profiles for role-specific passes:
812
823
 
813
824
  ${list || "(all specialists are internal โ€” use construct for all tasks)"}`;
814
- writeFile(instructionsPath, replaceManagedBlock(existing, note, mdManagedStart, mdManagedEnd));
825
+ // User-managed file with our managed-block carved out โ€” never doc-stamp.
826
+ writeFile(instructionsPath, replaceManagedBlock(existing, note, mdManagedStart, mdManagedEnd), { stamp: false });
815
827
  }
816
828
 
817
829
  // --- VS Code adapter ---
@@ -1143,20 +1155,12 @@ function collectSkills() {
1143
1155
  return results;
1144
1156
  }
1145
1157
 
1146
- /**
1147
- * Prefix prepended to every generated SKILL.md. The comment must survive
1148
- * round-trips โ€” it is the canonical signal that a file was produced by sync
1149
- * and must not be hand-edited.
1150
- */
1151
- const SKILL_GENERATED_PREFIX = "# Generated by construct sync\n\n";
1152
-
1153
1158
  /**
1154
1159
  * Write collected skills to both .claude/skills/ and .agents/skills/ in
1155
- * SKILL.md directory format.
1156
- *
1157
- * Idempotency: content written is deterministic; writing the same content
1158
- * twice is a no-op because writeFile skips unchanged files in dry-run mode
1159
- * and two-phase staging in normal mode produces identical results.
1160
+ * SKILL.md directory format. Each file gets Anthropic Agent Skills frontmatter
1161
+ * (name + description) so the loader can index it. Doc-stamping is opted out
1162
+ * โ€” a doc-stamp YAML block before the real frontmatter produces double-
1163
+ * frontmatter the loader can't parse.
1160
1164
  */
1161
1165
  function syncSkills() {
1162
1166
  const skills = collectSkills();
@@ -1166,9 +1170,13 @@ function syncSkills() {
1166
1170
  const agentsSkillsDir = path.join(home, ".agents", "skills");
1167
1171
 
1168
1172
  for (const { name, content } of skills) {
1169
- const generated = `${SKILL_GENERATED_PREFIX}${content}`;
1170
- writeFile(path.join(claudeSkillsDir, name, "SKILL.md"), generated);
1171
- writeFile(path.join(agentsSkillsDir, name, "SKILL.md"), generated);
1173
+ const frontmatter = buildSkillFrontmatter(name, content);
1174
+ // Strip any existing frontmatter from the source body so we don't emit two
1175
+ // blocks if a hand-authored skill already carries one.
1176
+ const body = stripLeadingFrontmatter(content);
1177
+ const generated = `${frontmatter}\n${body}`;
1178
+ writeFile(path.join(claudeSkillsDir, name, "SKILL.md"), generated, { stamp: false });
1179
+ writeFile(path.join(agentsSkillsDir, name, "SKILL.md"), generated, { stamp: false });
1172
1180
  }
1173
1181
 
1174
1182
  return skills.length;