clawstrap 1.5.0 → 1.5.1

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 (2) hide show
  1. package/dist/index.cjs +374 -193
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -98,11 +98,11 @@ function formatEntry(source, text) {
98
98
  ${text}`;
99
99
  }
100
100
  function appendToMemory(rootDir, entries, source) {
101
- const memoryPath = import_node_path15.default.join(rootDir, ".claude", "memory", "MEMORY.md");
102
- import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(memoryPath), { recursive: true });
101
+ const memoryPath = import_node_path9.default.join(rootDir, ".claude", "memory", "MEMORY.md");
102
+ import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(memoryPath), { recursive: true });
103
103
  let existingContent = "";
104
- if (import_node_fs14.default.existsSync(memoryPath)) {
105
- existingContent = import_node_fs14.default.readFileSync(memoryPath, "utf-8");
104
+ if (import_node_fs8.default.existsSync(memoryPath)) {
105
+ existingContent = import_node_fs8.default.readFileSync(memoryPath, "utf-8");
106
106
  }
107
107
  const existingEntries = parseMemoryEntries(existingContent);
108
108
  const toAppend = [];
@@ -113,48 +113,48 @@ function appendToMemory(rootDir, entries, source) {
113
113
  }
114
114
  if (toAppend.length > 0) {
115
115
  const appendText = "\n" + toAppend.join("\n") + "\n";
116
- import_node_fs14.default.appendFileSync(memoryPath, appendText, "utf-8");
116
+ import_node_fs8.default.appendFileSync(memoryPath, appendText, "utf-8");
117
117
  }
118
118
  return toAppend.length;
119
119
  }
120
120
  function appendToGotchaLog(rootDir, entries) {
121
- const logPath = import_node_path15.default.join(rootDir, ".claude", "gotcha-log.md");
122
- import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(logPath), { recursive: true });
123
- if (!import_node_fs14.default.existsSync(logPath)) {
124
- import_node_fs14.default.writeFileSync(
121
+ const logPath = import_node_path9.default.join(rootDir, ".claude", "gotcha-log.md");
122
+ import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(logPath), { recursive: true });
123
+ if (!import_node_fs8.default.existsSync(logPath)) {
124
+ import_node_fs8.default.writeFileSync(
125
125
  logPath,
126
126
  "# Gotcha Log\n\nIncident log \u2014 why rules exist.\n\n",
127
127
  "utf-8"
128
128
  );
129
129
  }
130
130
  const toAppend = entries.map((e) => formatEntry("session", e)).join("\n");
131
- import_node_fs14.default.appendFileSync(logPath, "\n" + toAppend + "\n", "utf-8");
131
+ import_node_fs8.default.appendFileSync(logPath, "\n" + toAppend + "\n", "utf-8");
132
132
  }
133
133
  function appendToFutureConsiderations(rootDir, entries) {
134
- const fcPath = import_node_path15.default.join(rootDir, ".claude", "future-considerations.md");
135
- import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(fcPath), { recursive: true });
136
- if (!import_node_fs14.default.existsSync(fcPath)) {
137
- import_node_fs14.default.writeFileSync(
134
+ const fcPath = import_node_path9.default.join(rootDir, ".claude", "future-considerations.md");
135
+ import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(fcPath), { recursive: true });
136
+ if (!import_node_fs8.default.existsSync(fcPath)) {
137
+ import_node_fs8.default.writeFileSync(
138
138
  fcPath,
139
139
  "# Future Considerations\n\nDeferred ideas and potential improvements.\n\n",
140
140
  "utf-8"
141
141
  );
142
142
  }
143
143
  const toAppend = entries.map((e) => formatEntry("session", e)).join("\n");
144
- import_node_fs14.default.appendFileSync(fcPath, "\n" + toAppend + "\n", "utf-8");
144
+ import_node_fs8.default.appendFileSync(fcPath, "\n" + toAppend + "\n", "utf-8");
145
145
  }
146
146
  function appendToOpenThreads(rootDir, entries) {
147
- const otPath = import_node_path15.default.join(rootDir, ".claude", "memory", "open-threads.md");
148
- import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(otPath), { recursive: true });
149
- if (!import_node_fs14.default.existsSync(otPath)) {
150
- import_node_fs14.default.writeFileSync(
147
+ const otPath = import_node_path9.default.join(rootDir, ".claude", "memory", "open-threads.md");
148
+ import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(otPath), { recursive: true });
149
+ if (!import_node_fs8.default.existsSync(otPath)) {
150
+ import_node_fs8.default.writeFileSync(
151
151
  otPath,
152
152
  "# Open Threads\n\nUnresolved questions and next steps.\n\n",
153
153
  "utf-8"
154
154
  );
155
155
  }
156
156
  const toAppend = entries.map((e) => formatEntry("session", e)).join("\n");
157
- import_node_fs14.default.appendFileSync(otPath, "\n" + toAppend + "\n", "utf-8");
157
+ import_node_fs8.default.appendFileSync(otPath, "\n" + toAppend + "\n", "utf-8");
158
158
  }
159
159
  function buildAutoBlock(sections) {
160
160
  const lines = [AUTO_START];
@@ -201,10 +201,10 @@ function buildAutoBlock(sections) {
201
201
  return lines.join("\n");
202
202
  }
203
203
  function writeConventions(rootDir, sections) {
204
- const conventionsPath = import_node_path15.default.join(rootDir, ".claude", "rules", "conventions.md");
205
- import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(conventionsPath), { recursive: true });
204
+ const conventionsPath = import_node_path9.default.join(rootDir, ".claude", "rules", "conventions.md");
205
+ import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(conventionsPath), { recursive: true });
206
206
  const autoBlock = buildAutoBlock(sections);
207
- if (!import_node_fs14.default.existsSync(conventionsPath)) {
207
+ if (!import_node_fs8.default.existsSync(conventionsPath)) {
208
208
  const content = [
209
209
  "# Conventions",
210
210
  "",
@@ -215,28 +215,28 @@ function writeConventions(rootDir, sections) {
215
215
  "<!-- Add manual conventions below this line -->",
216
216
  ""
217
217
  ].join("\n");
218
- import_node_fs14.default.writeFileSync(conventionsPath, content, "utf-8");
218
+ import_node_fs8.default.writeFileSync(conventionsPath, content, "utf-8");
219
219
  return;
220
220
  }
221
- const existing = import_node_fs14.default.readFileSync(conventionsPath, "utf-8");
221
+ const existing = import_node_fs8.default.readFileSync(conventionsPath, "utf-8");
222
222
  const startIdx = existing.indexOf(AUTO_START);
223
223
  const endIdx = existing.indexOf(AUTO_END);
224
224
  if (startIdx === -1 || endIdx === -1) {
225
225
  const updated = existing.trimEnd() + "\n\n" + autoBlock + "\n";
226
- import_node_fs14.default.writeFileSync(conventionsPath, updated, "utf-8");
226
+ import_node_fs8.default.writeFileSync(conventionsPath, updated, "utf-8");
227
227
  } else {
228
228
  const before = existing.slice(0, startIdx);
229
229
  const after = existing.slice(endIdx + AUTO_END.length);
230
230
  const updated = before + autoBlock + after;
231
- import_node_fs14.default.writeFileSync(conventionsPath, updated, "utf-8");
231
+ import_node_fs8.default.writeFileSync(conventionsPath, updated, "utf-8");
232
232
  }
233
233
  }
234
- var import_node_fs14, import_node_path15, AUTO_START, AUTO_END;
234
+ var import_node_fs8, import_node_path9, AUTO_START, AUTO_END;
235
235
  var init_writers = __esm({
236
236
  "src/watch/writers.ts"() {
237
237
  "use strict";
238
- import_node_fs14 = __toESM(require("fs"), 1);
239
- import_node_path15 = __toESM(require("path"), 1);
238
+ import_node_fs8 = __toESM(require("fs"), 1);
239
+ import_node_path9 = __toESM(require("path"), 1);
240
240
  init_dedup();
241
241
  AUTO_START = "<!-- CLAWSTRAP:AUTO -->";
242
242
  AUTO_END = "<!-- CLAWSTRAP:END -->";
@@ -473,7 +473,7 @@ var OUTPUT_MANIFEST = [
473
473
  var EMPTY_DIRS = ["tmp", "research", "context", "artifacts"];
474
474
 
475
475
  // src/templates/governance-file.md.tmpl
476
- var governance_file_md_default = "# {%governanceFile%} \u2014 Master Governance Rules\n> **Workspace**: {%workspaceName%} | **Generated**: {%generatedDate%} | **Status**: active\n> Loaded every session. Keep lean. Move details to skills or rules files.\n\n---\n\n## Workflow Rules\n\n**Approval-first, always.**\nPlan work and get explicit approval before executing. No speculative actions.\nIf scope changes mid-task, pause and re-confirm.\n\n**If it's not on disk, it didn't happen.**\nSave findings immediately, not at session end. Flush {%flushCadence%}.\nWrite corrections to durable locations before applying them.\n{%#if hasSubagents%}Subagents return file paths, not raw data.{%/if%}\n\n**Quality > context cleanliness > speed > token cost.**\nQuality failures require full rework (100% waste). Never trade quality for tokens.\n{%#if isResearch%}\n**Research-specific:**\n- Cite sources for every claim \u2014 no unsourced assertions\n- Write findings to file immediately, not at session end\n- Separate facts from interpretation in all output\n{%/if%}\n{%#if isContent%}\n**Content-specific:**\n- Every piece of content follows the draft \u2192 review \u2192 approve cycle\n- Never publish or finalize without explicit approval\n- Track all revision feedback in a dedicated file\n{%/if%}\n{%#if isDataProcessing%}\n**Data processing-specific:**\n- Define batch size before starting \u2014 never process unbounded sets\n- Validate schema on every write\n- Log every transformation step for auditability\n{%/if%}\n\n---\n\n## Persistence Hierarchy\n\nFrom most ephemeral to most durable:\n\n| Layer | Location | Loaded When |\n|-------|----------|-------------|\n| Conversation | (in-context) | Always \u2014 volatile |\n| Temp files | `tmp/` | Per-task, gitignored |\n{%#if sessionHandoff%}| Memory | `{%systemDir%}/memory/` | On demand |\n{%/if%}| Skills | `{%systemDir%}/skills/*/SKILL.md` | When triggered |\n| Rules | `{%systemDir%}/rules/*.md` | Every session |\n| **{%governanceFile%}** | `./{%governanceFile%}` | **Every session** |\n\n---\n\n## Context Discipline\n\n- Flush working state to file {%flushCadence%}\n{%#if hasSubagents%}- Subagents write output to `tmp/{task}/{name}.json`; return one-line receipt\n- Main session reads subagent files ONLY if needed for the next step\n- Never hold raw batch results in conversation \u2014 write to disk first\n{%/if%}- Before batch work: write execution plan to file (survives context loss)\n\n---\n{%#if sessionHandoff%}\n## Session Handoff Checklist\n\nRun this at every session end (mandatory, not optional):\n\n1. Save all work to SSOT files\n2. Sync derived files (rebuild from SSOTs)\n3. SSOT integrity check (no duplicates, no stale data)\n4. Update progress tracker\n5. Write next-session plan (with pre-execution hooks listed)\n6. Launch QC on work done this session\n\n---\n{%/if%}\n\n## Security Rules\n\n- Never read `.env` files or echo credentials\n- Never install third-party MCP servers/plugins without explicit approval\n- Never write outside this workspace root \u2014 use project-local `tmp/`, not system `/tmp/`\n- Approved tools only \u2014 ask before using new tools/APIs\n\n---\n\n## Quality Rules\n{%#if isProductionQuality%}\n- QC is a structural gate, not an optional post-step\n- Run spot-checks after every 5 items in batch work (Ralph Loop)\n- Combined agents (extract + classify) beat split agents on quality\n- Disagreements between agents reveal ambiguity \u2014 escalate, don't suppress\n{%/if%}\n{%#if hasQualityGates%}\n{%#unless isProductionQuality%}- QC is a structural gate, not an optional post-step\n- Run QC checkpoints at regular intervals during batch work\n- All results must be reviewed before being marked complete\n{%/unless%}\n{%/if%}\n{%#unless hasQualityGates%}- Run a quick check before finishing any task\n- Review outputs before marking work complete\n{%/unless%}\n\n---\n\n## Pointers to Other Layers\n\n- Rules: `{%systemDir%}/rules/` \u2014 domain-specific rules loaded every session\n- Skills: `{%systemDir%}/skills/SKILL_REGISTRY.md` \u2014 index of all skills\n{%#if hasSubagents%}- Agents: `{%systemDir%}/agents/` \u2014 subagent definitions with governance baked in\n{%/if%}- Gotchas: `{%systemDir%}/gotcha-log.md` \u2014 incident log (why rules exist)\n- Future: `{%systemDir%}/future-considerations.md` \u2014 deferred ideas\n{%#if sddEnabled%}\n---\n\n## Spec-Driven Development\n\nThis workspace uses SDD. Before implementing any feature:\n\n1. Write a spec \u2192 `specs/{name}.md` (use `/spec` or copy `specs/_template.md`)\n2. Get explicit user approval\n3. Implement from the approved spec \u2014 not from the conversation\n\nRule details: `{%systemDir%}/rules/sdd.md`\n{%/if%}\n";
476
+ var governance_file_md_default = "# {%governanceFile%} \u2014 Master Governance Rules\n> **Workspace**: {%workspaceName%} | **Generated**: {%generatedDate%} | **Status**: active\n> Loaded every session. Keep lean. Move details to skills or rules files.\n\n---\n\n## Workflow Rules\n\n**Approval-first, always.**\nPlan work and get explicit approval before executing. No speculative actions.\nIf scope changes mid-task, pause and re-confirm.\n\n**If it's not on disk, it didn't happen.**\nSave findings immediately, not at session end. Flush {%flushCadence%}.\nWrite corrections to durable locations before applying them.\n{%#if hasSubagents%}Subagents return file paths, not raw data.{%/if%}\n\n**Quality > context cleanliness > speed > token cost.**\nQuality failures require full rework (100% waste). Never trade quality for tokens.\n{%#if isResearch%}\n**Research-specific:**\n- Cite sources for every claim \u2014 no unsourced assertions\n- Write findings to file immediately, not at session end\n- Separate facts from interpretation in all output\n{%/if%}\n{%#if isContent%}\n**Content-specific:**\n- Every piece of content follows the draft \u2192 review \u2192 approve cycle\n- Never publish or finalize without explicit approval\n- Track all revision feedback in a dedicated file\n{%/if%}\n{%#if isDataProcessing%}\n**Data processing-specific:**\n- Define batch size before starting \u2014 never process unbounded sets\n- Validate schema on every write\n- Log every transformation step for auditability\n{%/if%}\n\n---\n\n## Persistence Hierarchy\n\nFrom most ephemeral to most durable:\n\n| Layer | Location | Loaded When |\n|-------|----------|-------------|\n| Conversation | (in-context) | Always \u2014 volatile |\n| Temp files | `tmp/` | Per-task, gitignored |\n{%#if sessionHandoff%}| Memory | `{%systemDir%}/memory/` | On demand |\n{%/if%}| Skills | `{%systemDir%}/skills/*/SKILL.md` | When triggered |\n| Rules | `{%systemDir%}/rules/*.md` | Every session |\n| **{%governanceFile%}** | `./{%governanceFile%}` | **Every session** |\n\n---\n\n## Directory Map\n\n| Directory | Purpose | When Claude writes here |\n|-----------|---------|------------------------|\n| `artifacts/` | Architecture docs, ADRs, system overviews | After major design decisions; `artifacts/architecture.md` is the living system doc |\n| `context/` | Execution plans and session checkpoints | Before batch work \u2192 `context/plan-{date}-{task}.md`; every 5 ops \u2192 `context/checkpoint-{date}-{task}.md`; wrap-up \u2192 `context/next-session.md` |\n{%#if sessionHandoff%}| `projects/` | Active sub-projects (copy `projects/_template/`) | When a feature track needs its own `process.md` |\n{%/if%}| `research/` | Reference material from external sources | When reading specs, docs, or papers worth keeping |\n| `tmp/` | Subagent output, session summaries (gitignored) | Summaries \u2192 `tmp/sessions/YYYY-MM-DD-HHmm.md`; subagent output \u2192 `tmp/{task}/` |\n| `{%systemDir%}/memory/` | LLM-processed governance (fed by watch daemon) | Do not write directly \u2014 watch daemon only |\n\n---\n\n## Context Discipline\n\n- Flush working state to file {%flushCadence%}\n{%#if hasSubagents%}- Subagents write output to `tmp/{task}/{name}.json`; return one-line receipt\n- Main session reads subagent files ONLY if needed for the next step\n- Never hold raw batch results in conversation \u2014 write to disk first\n{%/if%}- Before batch work: write execution plan to `context/plan-{date}-{task}.md` (survives context loss)\n\n---\n{%#if sessionHandoff%}\n## Session Handoff Checklist\n\nRun this at every session end (mandatory, not optional):\n\n1. Save all work to SSOT files\n2. Sync derived files (rebuild from SSOTs)\n3. SSOT integrity check (no duplicates, no stale data)\n4. Update progress tracker \u2192 `context/progress-{date}.md`\n5. Write next-session plan \u2192 `context/next-session.md`\n6. Launch QC on work done this session \u2192 write results to `context/qc-{date}.md`\n\n---\n{%/if%}\n\n## Security Rules\n\n- Never read `.env` files or echo credentials\n- Never install third-party MCP servers/plugins without explicit approval\n- Never write outside this workspace root \u2014 use project-local `tmp/`, not system `/tmp/`\n- Approved tools only \u2014 ask before using new tools/APIs\n\n---\n\n## Quality Rules\n{%#if isProductionQuality%}\n- QC is a structural gate, not an optional post-step\n- Run spot-checks after every 5 items in batch work (Ralph Loop)\n- Combined agents (extract + classify) beat split agents on quality\n- Disagreements between agents reveal ambiguity \u2014 escalate, don't suppress\n{%/if%}\n{%#if hasQualityGates%}\n{%#unless isProductionQuality%}- QC is a structural gate, not an optional post-step\n- Run QC checkpoints at regular intervals during batch work\n- All results must be reviewed before being marked complete\n{%/unless%}\n{%/if%}\n{%#unless hasQualityGates%}- Run a quick check before finishing any task\n- Review outputs before marking work complete\n{%/unless%}\n\n---\n\n## Pointers to Other Layers\n\n- Rules: `{%systemDir%}/rules/` \u2014 domain-specific rules loaded every session\n- Skills: `{%systemDir%}/skills/SKILL_REGISTRY.md` \u2014 index of all skills\n{%#if hasSubagents%}- Agents: `{%systemDir%}/agents/` \u2014 subagent definitions with governance baked in\n{%/if%}- Gotchas: `{%systemDir%}/gotcha-log.md` \u2014 incident log (why rules exist)\n- Future: `{%systemDir%}/future-considerations.md` \u2014 deferred ideas\n{%#if sddEnabled%}\n---\n\n## Spec-Driven Development\n\nThis workspace uses SDD. Before implementing any feature:\n\n1. Write a spec \u2192 `specs/{name}.md` (use `/spec` or copy `specs/_template.md`)\n2. Get explicit user approval\n3. Implement from the approved spec \u2014 not from the conversation\n\nRule details: `{%systemDir%}/rules/sdd.md`\n{%/if%}\n";
477
477
 
478
478
  // src/templates/getting-started.md.tmpl
479
479
  var getting_started_md_default = "# Getting Started \u2014 {%workspaceName%}\n> Generated by Clawstrap v{%clawstrapVersion%} on {%generatedDate%}\n\nWelcome to your AI agent workspace. This workspace is configured for\n**{%workloadLabel%}** workflows.\n\n---\n\n## Quick Start\n\n1. **Read `{%governanceFile%}`** \u2014 this is your master governance file. It loads\n automatically every session and defines the rules your AI assistant follows.\n\n2. **Check `{%systemDir%}/rules/`** \u2014 these rule files also load every session.\n They contain detailed procedures for context discipline, approval workflows,\n and quality gates.\n\n3. **Start working** \u2014 open a Claude Code session in this directory. The\n governance files will load automatically.\n\n---\n\n## What's in This Workspace\n\n```\n{%governanceFile%} \u2190 Master governance (always loaded)\nGETTING_STARTED.md \u2190 You are here\n.clawstrap.json \u2190 Your workspace configuration\n\n{%systemDir%}/\n rules/ \u2190 Auto-loaded rules (every session)\n skills/ \u2190 Skill definitions (loaded when triggered)\n{%#if hasSubagents%} agents/ \u2190 Agent definitions\n{%/if%} gotcha-log.md \u2190 Incident log\n future-considerations.md \u2190 Deferred ideas\n{%#if sessionHandoff%} memory/ \u2190 Persistent memory across sessions\n{%/if%}\nprojects/\n _template/ \u2190 Template for new projects\n\ntmp/ \u2190 Temporary files (gitignored)\nresearch/ \u2190 Reference material\ncontext/ \u2190 Session checkpoints\nartifacts/ \u2190 Durable output\n{%#if sddEnabled%}specs/\n _template.md \u2190 Spec template (copy for each feature)\n{%/if%}```\n\n---\n\n## Key Principles\n\nThis workspace enforces five principles:\n\n1. **If it's not on disk, it didn't happen.** Context windows compress. Sessions\n end. The only thing that persists is files. Flush work {%flushCadence%}.\n\n2. **Approval-first, always.** Plan work, get approval, then execute. No\n speculative actions.\n\n3. **Quality is a structural gate.** Quality checks happen during work, not after.\n{%#if hasQualityGates%} See `{%systemDir%}/rules/quality-gates.md` for the full procedure.{%/if%}\n\n4. **Disagreements reveal ambiguity.** When agents disagree, the rules are\n ambiguous. Escalate and clarify \u2014 don't suppress.\n\n5. **One decision at a time.** Binary decisions made sequentially compound into\n reliable outcomes.\n{%#if hasSubagents%}\n\n---\n\n## Working with Multiple Agents\n\nYour workspace is configured for multi-agent workflows. Key rules:\n\n- **Subagents write to `tmp/`** \u2014 they return file paths and one-line summaries,\n never raw data in conversation.\n- **The main session is an orchestrator** \u2014 it tracks progress, launches agents,\n and checks output files. It does not hold subagent data in memory.\n- **Agent definitions live in `{%systemDir%}/agents/`** \u2014 each file becomes the\n system prompt for a subagent. Governance is baked into the definition.\n{%/if%}\n{%#if sessionHandoff%}\n\n---\n\n## Session Handoff\n\nThis workspace uses session handoff checklists. At the end of every session:\n\n1. Save all work to SSOT files\n2. Sync derived files\n3. Check SSOT integrity\n4. Update progress tracker\n5. Write next-session plan\n6. Run QC on session work\n\nThis ensures no context is lost between sessions.\n{%/if%}\n{%#if isResearch%}\n\n---\n\n## Research Tips\n\n- Write findings to disk immediately \u2014 don't accumulate in conversation\n- Cite sources for every claim\n- Separate facts from interpretation\n- Use `research/` for reference material, `artifacts/` for synthesis output\n{%/if%}\n{%#if isContent%}\n\n---\n\n## Content Workflow Tips\n\n- Every piece follows: draft \u2192 review \u2192 approve\n- Track revision feedback in dedicated files\n- Never finalize without explicit approval\n- Use `artifacts/` for final deliverables\n{%/if%}\n{%#if isDataProcessing%}\n\n---\n\n## Data Processing Tips\n\n- Define batch size before starting any processing run\n- Validate schema on every write\n- Checkpoint every 5 batch items\n- Log all transformations for auditability\n{%/if%}\n\n---\n\n{%#if sddEnabled%}\n\n---\n\n## Spec-Driven Development\n\nThis workspace enforces a spec-first workflow. Before any implementation:\n\n1. **Type `/spec`** to start a new spec interactively \u2014 Claude will guide you\n2. Or copy `specs/_template.md` manually, fill it in, and get approval\n3. Only implement after the spec is approved\n\nYour specs live in `specs/`. Each approved spec is the contract between you and\nClaude \u2014 implementation follows the spec, not the conversation.\n\nSee `{%systemDir%}/rules/sdd.md` for the full rules and exemptions.\n{%/if%}\n\n---\n\n*This workspace was generated by [Clawstrap](https://github.com/clawstrap/clawstrap).\nEdit any file to fit your needs \u2014 there's no lock-in.*\n";
@@ -482,7 +482,7 @@ var getting_started_md_default = "# Getting Started \u2014 {%workspaceName%}\n>
482
482
  var gitignore_default = "# Dependencies\nnode_modules/\n\n# Temporary files (subagent output, session data)\ntmp/\n\n# Secrets\n.env\n.env.*\ncredentials.json\n*.pem\n*.key\n\n# OS\n.DS_Store\nThumbs.db\n\n# Editor\n*.swp\n*.swo\n*~\n.clawstrap.watch.pid\ntmp/sessions/\n";
483
483
 
484
484
  // src/templates/rules/context-discipline.md.tmpl
485
- var context_discipline_md_default = "# Rule: Context Discipline\n> **Scope**: All sessions | **Generated**: {%generatedDate%}\n\n## Flush Cadence\n\nFlush working state to file {%flushCadence%}:\n- Write current state to a context checkpoint file\n- Include: what's done, what's next, accumulated results\n- Path: `context/checkpoint-{date}-{task}.md`\n{%#if hasSubagents%}\n\n## Subagent Output Rules\n\nSubagents MUST:\n1. Write all output to `tmp/{task}/{name}.json` (or `.md`)\n2. Return a one-line receipt: `\"Done. N items. File: {path}. Summary: {1 line}.\"`\n3. NOT dump raw results into the conversation\n\nMain session MUST:\n- Only read the subagent file if needed for the NEXT step\n- Never hold subagent output in conversation memory\n\n## Thin Orchestrator Principle\n\nDuring batch work, the main session is an orchestrator only:\n\n**DO**:\n- Track queue / done / next\n- Launch agents with correct prompts\n- Check output files for completeness\n- Update SSOT data files\n- Run QC gates\n\n**DO NOT**:\n- Read raw extraction data into main context\n- Classify results (agents do that)\n- Hold full subagent output in conversation\n{%/if%}\n\n## Before Batch Work\n\nAlways write an execution plan to `tmp/{task}/plan.md` before starting.\nThis file must survive context loss and be readable by any future session.\n\n## On User Correction\n\n1. FIRST write the correction to its durable home (memory/rule/skill file)\n2. THEN apply the correction to current work\n\nThis ensures the learning persists even if the session ends unexpectedly.\n";
485
+ var context_discipline_md_default = "# Rule: Context Discipline\n> **Scope**: All sessions | **Generated**: {%generatedDate%}\n\n## Flush Cadence\n\nFlush working state to file {%flushCadence%}:\n- Write current state to a context checkpoint file\n- Include: what's done, what's next, accumulated results\n- Path: `context/checkpoint-{date}-{task}.md`\n{%#if hasSubagents%}\n\n## Subagent Output Rules\n\nSubagents MUST:\n1. Write all output to `tmp/{task}/{name}.json` (or `.md`)\n2. Return a one-line receipt: `\"Done. N items. File: {path}. Summary: {1 line}.\"`\n3. NOT dump raw results into the conversation\n\nMain session MUST:\n- Only read the subagent file if needed for the NEXT step\n- Never hold subagent output in conversation memory\n\n## Thin Orchestrator Principle\n\nDuring batch work, the main session is an orchestrator only:\n\n**DO**:\n- Track queue / done / next\n- Launch agents with correct prompts\n- Check output files for completeness\n- Update SSOT data files\n- Run QC gates\n\n**DO NOT**:\n- Read raw extraction data into main context\n- Classify results (agents do that)\n- Hold full subagent output in conversation\n{%/if%}\n\n## Before Batch Work\n\nAlways write an execution plan before starting batch work:\n- Path: `context/plan-{YYYY-MM-DD}-{task-slug}.md`\n- This file must survive context loss and be readable by any future session.\n\nSubagent output goes to `tmp/{task}/` \u2014 not `context/`.\n\n## Session Handoff\n\nNext-session plan goes to `context/next-session.md` (overwrite each time).\nQC results go to `context/qc-{YYYY-MM-DD}.md`.\n\n## On User Correction\n\n1. FIRST write the correction to its durable home (memory/rule/skill file)\n2. THEN apply the correction to current work\n\nThis ensures the learning persists even if the session ends unexpectedly.\n";
486
486
 
487
487
  // src/templates/rules/approval-first.md.tmpl
488
488
  var approval_first_md_default = `# Rule: Approval-First Workflow
@@ -805,7 +805,7 @@ async function init(directory, options) {
805
805
  import_node_fs2.default.mkdirSync(targetDir, { recursive: true });
806
806
  }
807
807
  const config = ClawstrapConfigSchema.parse({
808
- version: "1.4.2",
808
+ version: "1.5.1",
809
809
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
810
810
  workspaceName: answers.workspaceName,
811
811
  targetDirectory: directory,
@@ -1018,8 +1018,8 @@ Error: project "${name}" already exists at projects/${name}/
1018
1018
  }
1019
1019
 
1020
1020
  // src/status.ts
1021
- var import_node_fs8 = __toESM(require("fs"), 1);
1022
- var import_node_path9 = __toESM(require("path"), 1);
1021
+ var import_node_fs10 = __toESM(require("fs"), 1);
1022
+ var import_node_path11 = __toESM(require("path"), 1);
1023
1023
 
1024
1024
  // src/watch/pid.ts
1025
1025
  var import_node_fs7 = __toESM(require("fs"), 1);
@@ -1054,6 +1054,169 @@ function isDaemonRunning(rootDir) {
1054
1054
  }
1055
1055
  }
1056
1056
 
1057
+ // src/watch/promote.ts
1058
+ var import_node_fs9 = __toESM(require("fs"), 1);
1059
+ var import_node_path10 = __toESM(require("path"), 1);
1060
+ init_dedup();
1061
+ init_writers();
1062
+
1063
+ // src/watch/stopwords.ts
1064
+ var STOPWORDS = new Set(
1065
+ "a an the is are was were be been being have has had do does did will would could should may might must can this that these those i we you he she it they of in on at to for with from by about".split(" ")
1066
+ );
1067
+
1068
+ // src/watch/promote.ts
1069
+ var SIMILARITY_THRESHOLD = 0.65;
1070
+ var MIN_GROUP_SIZE = 3;
1071
+ function tokenize2(text) {
1072
+ return new Set(
1073
+ text.split(/\s+/).map((w) => w.replace(/[^a-z0-9]/gi, "").toLowerCase()).filter((w) => w.length > 1 && !STOPWORDS.has(w))
1074
+ );
1075
+ }
1076
+ function jaccard2(a, b) {
1077
+ const setA = tokenize2(a);
1078
+ const setB = tokenize2(b);
1079
+ if (setA.size === 0 && setB.size === 0) return 1;
1080
+ if (setA.size === 0 || setB.size === 0) return 0;
1081
+ let intersection = 0;
1082
+ for (const t of setA) {
1083
+ if (setB.has(t)) intersection++;
1084
+ }
1085
+ return intersection / (setA.size + setB.size - intersection);
1086
+ }
1087
+ function groupSimilar(entries) {
1088
+ const groups = [];
1089
+ for (const entry of entries) {
1090
+ let placed = false;
1091
+ for (const group of groups) {
1092
+ if (group.some((member) => jaccard2(entry, member) >= SIMILARITY_THRESHOLD)) {
1093
+ group.push(entry);
1094
+ placed = true;
1095
+ break;
1096
+ }
1097
+ }
1098
+ if (!placed) groups.push([entry]);
1099
+ }
1100
+ return groups.filter((g) => g.length >= MIN_GROUP_SIZE);
1101
+ }
1102
+ function deriveSlug(entries) {
1103
+ const freq = /* @__PURE__ */ new Map();
1104
+ for (const entry of entries) {
1105
+ for (const token of tokenize2(entry)) {
1106
+ freq.set(token, (freq.get(token) ?? 0) + 1);
1107
+ }
1108
+ }
1109
+ const top = [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([t]) => t);
1110
+ return top.join("-") || "correction";
1111
+ }
1112
+ function parseRuleResponse(response) {
1113
+ const titleMatch = response.match(/^TITLE:\s*(.+)$/m);
1114
+ const principleMatch = response.match(/^PRINCIPLE:\s*(.+)$/m);
1115
+ const imperativesMatch = response.match(/^IMPERATIVES:\s*\n((?:\s*-\s*.+\n?)+)/m);
1116
+ if (!titleMatch || !principleMatch || !imperativesMatch) return null;
1117
+ const imperatives = imperativesMatch[1].split("\n").map((l) => l.replace(/^\s*-\s*/, "").trim()).filter(Boolean);
1118
+ if (imperatives.length === 0) return null;
1119
+ return {
1120
+ title: titleMatch[1].trim(),
1121
+ principle: principleMatch[1].trim(),
1122
+ imperatives
1123
+ };
1124
+ }
1125
+ function writeRuleFile(rulesDir, slug, data) {
1126
+ import_node_fs9.default.mkdirSync(rulesDir, { recursive: true });
1127
+ const imperativeLines = data.imperatives.map((i) => `- ${i}`).join("\n");
1128
+ const content = `---
1129
+ status: pending-review
1130
+ generated: ${(/* @__PURE__ */ new Date()).toISOString()}
1131
+ source: auto-promoted from gotcha-log
1132
+ ---
1133
+
1134
+ # ${data.title}
1135
+
1136
+ ${data.principle}
1137
+
1138
+ ## Imperatives
1139
+
1140
+ ${imperativeLines}
1141
+ `;
1142
+ import_node_fs9.default.writeFileSync(import_node_path10.default.join(rulesDir, `${slug}-auto.md`), content, "utf-8");
1143
+ }
1144
+ async function checkAndPromoteCorrections(rootDir, adapter, ui) {
1145
+ const logPath = import_node_path10.default.join(rootDir, ".claude", "gotcha-log.md");
1146
+ if (!import_node_fs9.default.existsSync(logPath)) return;
1147
+ let content;
1148
+ try {
1149
+ content = import_node_fs9.default.readFileSync(logPath, "utf-8");
1150
+ } catch {
1151
+ return;
1152
+ }
1153
+ const entries = parseMemoryEntries(content);
1154
+ if (entries.length < MIN_GROUP_SIZE) return;
1155
+ const promotableGroups = groupSimilar(entries);
1156
+ if (promotableGroups.length === 0) return;
1157
+ const rulesDir = import_node_path10.default.join(rootDir, ".claude", "rules");
1158
+ let written = 0;
1159
+ for (const group of promotableGroups) {
1160
+ const slug = deriveSlug(group);
1161
+ const ruleFile = import_node_path10.default.join(rulesDir, `${slug}-auto.md`);
1162
+ if (import_node_fs9.default.existsSync(ruleFile)) continue;
1163
+ ui.promoteStart();
1164
+ const prompt = `You are analysing a set of recurring corrections from an AI coding session log.
1165
+
1166
+ Corrections:
1167
+ ${group.map((e, i) => `${i + 1}. ${e}`).join("\n")}
1168
+
1169
+ Synthesise these into a governance rule. Respond in this exact format:
1170
+
1171
+ TITLE: (short rule name, 2\u20135 words)
1172
+ PRINCIPLE: (one sentence \u2014 the core principle this rule enforces)
1173
+ IMPERATIVES:
1174
+ - (specific imperative 1)
1175
+ - (specific imperative 2)
1176
+ - (specific imperative 3)
1177
+
1178
+ Output only the structured response \u2014 no explanation, no markdown fences.`;
1179
+ let response;
1180
+ try {
1181
+ response = await adapter.complete(prompt);
1182
+ } catch {
1183
+ ui.promoteDone(0);
1184
+ continue;
1185
+ }
1186
+ const data = parseRuleResponse(response);
1187
+ if (!data) {
1188
+ ui.promoteDone(0);
1189
+ continue;
1190
+ }
1191
+ try {
1192
+ writeRuleFile(rulesDir, slug, data);
1193
+ appendToMemory(rootDir, [`Auto-promoted correction group to rule: ${slug}-auto.md \u2014 "${data.title}"`], "promote");
1194
+ written++;
1195
+ ui.promoteDone(1);
1196
+ } catch {
1197
+ ui.promoteDone(0);
1198
+ }
1199
+ }
1200
+ void written;
1201
+ }
1202
+ function listPendingRules(rootDir) {
1203
+ const rulesDir = import_node_path10.default.join(rootDir, ".claude", "rules");
1204
+ if (!import_node_fs9.default.existsSync(rulesDir)) return [];
1205
+ const results = [];
1206
+ for (const entry of import_node_fs9.default.readdirSync(rulesDir)) {
1207
+ if (!entry.endsWith("-auto.md")) continue;
1208
+ try {
1209
+ const content = import_node_fs9.default.readFileSync(import_node_path10.default.join(rulesDir, entry), "utf-8");
1210
+ if (!content.includes("status: pending-review")) continue;
1211
+ const headingMatch = content.match(/^#\s+(.+)$/m);
1212
+ const title = headingMatch ? headingMatch[1].trim() : "(no title)";
1213
+ results.push({ file: entry, title });
1214
+ } catch {
1215
+ }
1216
+ }
1217
+ return results;
1218
+ }
1219
+
1057
1220
  // src/status.ts
1058
1221
  var WORKLOAD_LABELS3 = {
1059
1222
  research: "Research & Analysis",
@@ -1062,27 +1225,29 @@ var WORKLOAD_LABELS3 = {
1062
1225
  custom: "General Purpose"
1063
1226
  };
1064
1227
  function countEntries(dir, exclude = []) {
1065
- if (!import_node_fs8.default.existsSync(dir)) return 0;
1066
- return import_node_fs8.default.readdirSync(dir, { withFileTypes: true }).filter((e) => !exclude.includes(e.name) && !e.name.startsWith(".")).length;
1228
+ if (!import_node_fs10.default.existsSync(dir)) return 0;
1229
+ return import_node_fs10.default.readdirSync(dir, { withFileTypes: true }).filter((e) => !exclude.includes(e.name) && !e.name.startsWith(".")).length;
1067
1230
  }
1068
1231
  function countSkills(skillsDir) {
1069
- if (!import_node_fs8.default.existsSync(skillsDir)) return 0;
1070
- return import_node_fs8.default.readdirSync(skillsDir, { withFileTypes: true }).filter(
1071
- (e) => e.isDirectory() && import_node_fs8.default.existsSync(import_node_path9.default.join(skillsDir, e.name, "SKILL.md"))
1232
+ if (!import_node_fs10.default.existsSync(skillsDir)) return 0;
1233
+ return import_node_fs10.default.readdirSync(skillsDir, { withFileTypes: true }).filter(
1234
+ (e) => e.isDirectory() && import_node_fs10.default.existsSync(import_node_path11.default.join(skillsDir, e.name, "SKILL.md"))
1072
1235
  ).length;
1073
1236
  }
1074
1237
  async function showStatus() {
1075
1238
  const { config, vars, rootDir } = loadWorkspace();
1076
1239
  const systemDir = String(vars.systemDir);
1077
- const agentsDir = import_node_path9.default.join(rootDir, systemDir, "agents");
1078
- const skillsDir = import_node_path9.default.join(rootDir, systemDir, "skills");
1079
- const rulesDir = import_node_path9.default.join(rootDir, systemDir, "rules");
1080
- const projectsDir = import_node_path9.default.join(rootDir, "projects");
1240
+ const agentsDir = import_node_path11.default.join(rootDir, systemDir, "agents");
1241
+ const skillsDir = import_node_path11.default.join(rootDir, systemDir, "skills");
1242
+ const rulesDir = import_node_path11.default.join(rootDir, systemDir, "rules");
1243
+ const projectsDir = import_node_path11.default.join(rootDir, "projects");
1081
1244
  const agentCount = countEntries(agentsDir, ["_template.md"]);
1082
1245
  const skillCount = countSkills(skillsDir);
1083
1246
  const projectCount = countEntries(projectsDir, ["_template"]);
1084
1247
  const ruleCount = countEntries(rulesDir);
1085
1248
  const date = config.createdAt.split("T")[0];
1249
+ const pendingRulesList = listPendingRules(rootDir);
1250
+ const pendingRules = pendingRulesList.length;
1086
1251
  console.log(`
1087
1252
  Clawstrap Workspace: ${config.workspaceName}`);
1088
1253
  console.log(`Created: ${date} | Version: ${config.version}`);
@@ -1103,6 +1268,12 @@ Structure:`);
1103
1268
  console.log(` Skills: ${skillCount} (${systemDir}/skills/)`);
1104
1269
  console.log(` Projects: ${projectCount} (projects/)`);
1105
1270
  console.log(` Rules: ${ruleCount} (${systemDir}/rules/)`);
1271
+ if (pendingRules > 0) {
1272
+ console.log(` Pending rules: ${pendingRules} (.claude/rules/ \u2014 review *-auto.md files)`);
1273
+ for (const rule of pendingRulesList) {
1274
+ console.log(` \xB7 ${rule.file.padEnd(30)} "${rule.title}"`);
1275
+ }
1276
+ }
1106
1277
  if (config.lastExport) {
1107
1278
  const exportDate = config.lastExport.exportedAt.split("T")[0];
1108
1279
  console.log(`
@@ -1131,28 +1302,28 @@ Watch:`);
1131
1302
  }
1132
1303
 
1133
1304
  // src/export-paperclip.ts
1134
- var import_node_fs13 = __toESM(require("fs"), 1);
1135
- var import_node_path14 = __toESM(require("path"), 1);
1305
+ var import_node_fs15 = __toESM(require("fs"), 1);
1306
+ var import_node_path16 = __toESM(require("path"), 1);
1136
1307
  var import_prompts7 = require("@inquirer/prompts");
1137
1308
 
1138
1309
  // src/export-paperclip/translate-agents.ts
1139
- var import_node_fs9 = __toESM(require("fs"), 1);
1140
- var import_node_path10 = __toESM(require("path"), 1);
1310
+ var import_node_fs11 = __toESM(require("fs"), 1);
1311
+ var import_node_path12 = __toESM(require("path"), 1);
1141
1312
  function slugToName(slug) {
1142
1313
  return slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1143
1314
  }
1144
1315
  function translateAgents(rootDir, systemDir, workspaceName, skillSlugs) {
1145
- const agentsDir = import_node_path10.default.join(rootDir, systemDir, "agents");
1316
+ const agentsDir = import_node_path12.default.join(rootDir, systemDir, "agents");
1146
1317
  const agents = [];
1147
1318
  const workerNames = [];
1148
1319
  const workerAgents = [];
1149
- if (import_node_fs9.default.existsSync(agentsDir)) {
1150
- for (const entry of import_node_fs9.default.readdirSync(agentsDir)) {
1320
+ if (import_node_fs11.default.existsSync(agentsDir)) {
1321
+ for (const entry of import_node_fs11.default.readdirSync(agentsDir)) {
1151
1322
  if (entry === "primary-agent.md" || entry === "_template.md" || entry.startsWith(".") || !entry.endsWith(".md")) {
1152
1323
  continue;
1153
1324
  }
1154
1325
  const slug = entry.replace(/\.md$/, "");
1155
- const rawBody = import_node_fs9.default.readFileSync(import_node_path10.default.join(agentsDir, entry), "utf-8");
1326
+ const rawBody = import_node_fs11.default.readFileSync(import_node_path12.default.join(agentsDir, entry), "utf-8");
1156
1327
  const roleMatch = rawBody.match(/^>\s*\*\*Purpose\*\*:\s*(.+)/m);
1157
1328
  const description = roleMatch ? roleMatch[1].trim() : "";
1158
1329
  const name = slugToName(slug);
@@ -1262,8 +1433,8 @@ When your work is complete, hand off to the **CEO** for review routing. If a rev
1262
1433
  }
1263
1434
 
1264
1435
  // src/export-paperclip/translate-governance.ts
1265
- var import_node_fs10 = __toESM(require("fs"), 1);
1266
- var import_node_path11 = __toESM(require("path"), 1);
1436
+ var import_node_fs12 = __toESM(require("fs"), 1);
1437
+ var import_node_path13 = __toESM(require("path"), 1);
1267
1438
  var GOVERNANCE_TIERS = {
1268
1439
  solo: {
1269
1440
  tier: "light",
@@ -1297,39 +1468,39 @@ function getGovernanceConfig(qualityLevel) {
1297
1468
  }
1298
1469
 
1299
1470
  // src/export-paperclip/translate-skills.ts
1300
- var import_node_fs11 = __toESM(require("fs"), 1);
1301
- var import_node_path12 = __toESM(require("path"), 1);
1471
+ var import_node_fs13 = __toESM(require("fs"), 1);
1472
+ var import_node_path14 = __toESM(require("path"), 1);
1302
1473
  function translateSkills(rootDir, systemDir) {
1303
- const skillsDir = import_node_path12.default.join(rootDir, systemDir, "skills");
1474
+ const skillsDir = import_node_path14.default.join(rootDir, systemDir, "skills");
1304
1475
  const skills = [];
1305
- if (!import_node_fs11.default.existsSync(skillsDir)) return skills;
1306
- for (const entry of import_node_fs11.default.readdirSync(skillsDir, { withFileTypes: true })) {
1476
+ if (!import_node_fs13.default.existsSync(skillsDir)) return skills;
1477
+ for (const entry of import_node_fs13.default.readdirSync(skillsDir, { withFileTypes: true })) {
1307
1478
  if (!entry.isDirectory()) continue;
1308
- const skillMdPath = import_node_path12.default.join(skillsDir, entry.name, "SKILL.md");
1309
- if (!import_node_fs11.default.existsSync(skillMdPath)) continue;
1479
+ const skillMdPath = import_node_path14.default.join(skillsDir, entry.name, "SKILL.md");
1480
+ if (!import_node_fs13.default.existsSync(skillMdPath)) continue;
1310
1481
  skills.push({
1311
1482
  name: entry.name,
1312
1483
  sourcePath: `${systemDir}/skills/${entry.name}/SKILL.md`,
1313
- content: import_node_fs11.default.readFileSync(skillMdPath, "utf-8")
1484
+ content: import_node_fs13.default.readFileSync(skillMdPath, "utf-8")
1314
1485
  });
1315
1486
  }
1316
1487
  return skills;
1317
1488
  }
1318
1489
 
1319
1490
  // src/export-paperclip/translate-goals.ts
1320
- var import_node_fs12 = __toESM(require("fs"), 1);
1321
- var import_node_path13 = __toESM(require("path"), 1);
1491
+ var import_node_fs14 = __toESM(require("fs"), 1);
1492
+ var import_node_path15 = __toESM(require("path"), 1);
1322
1493
  function translateGoals(rootDir) {
1323
- const projectsDir = import_node_path13.default.join(rootDir, "projects");
1494
+ const projectsDir = import_node_path15.default.join(rootDir, "projects");
1324
1495
  const goals = [];
1325
- if (!import_node_fs12.default.existsSync(projectsDir)) return goals;
1326
- for (const entry of import_node_fs12.default.readdirSync(projectsDir, { withFileTypes: true })) {
1496
+ if (!import_node_fs14.default.existsSync(projectsDir)) return goals;
1497
+ for (const entry of import_node_fs14.default.readdirSync(projectsDir, { withFileTypes: true })) {
1327
1498
  if (!entry.isDirectory() || entry.name === "_template" || entry.name.startsWith(".")) {
1328
1499
  continue;
1329
1500
  }
1330
- const readmePath = import_node_path13.default.join(projectsDir, entry.name, "README.md");
1331
- if (!import_node_fs12.default.existsSync(readmePath)) continue;
1332
- const content = import_node_fs12.default.readFileSync(readmePath, "utf-8");
1501
+ const readmePath = import_node_path15.default.join(projectsDir, entry.name, "README.md");
1502
+ if (!import_node_fs14.default.existsSync(readmePath)) continue;
1503
+ const content = import_node_fs14.default.readFileSync(readmePath, "utf-8");
1333
1504
  const descMatch = content.match(
1334
1505
  /## What This Project Is\s*\n+([\s\S]*?)(?=\n---|\n##|$)/
1335
1506
  );
@@ -1357,7 +1528,7 @@ async function exportPaperclip(options) {
1357
1528
  default: `Governed AI workspace for ${String(vars.workloadLabel).toLowerCase()}`
1358
1529
  });
1359
1530
  const companySlug = toSlug(companyName);
1360
- const outDir = import_node_path14.default.resolve(
1531
+ const outDir = import_node_path16.default.resolve(
1361
1532
  options.out ?? `${config.workspaceName}-paperclip`
1362
1533
  );
1363
1534
  const skills = translateSkills(rootDir, systemDir);
@@ -1387,7 +1558,7 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1387
1558
  console.log("\nValidation passed. Run without --validate to export.\n");
1388
1559
  return;
1389
1560
  }
1390
- if (import_node_fs13.default.existsSync(outDir)) {
1561
+ if (import_node_fs15.default.existsSync(outDir)) {
1391
1562
  const proceed = await (0, import_prompts7.confirm)({
1392
1563
  message: `Output directory already exists at ${outDir}. Overwrite?`,
1393
1564
  default: false
@@ -1396,10 +1567,10 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1396
1567
  console.log("Aborted.\n");
1397
1568
  return;
1398
1569
  }
1399
- import_node_fs13.default.rmSync(outDir, { recursive: true, force: true });
1570
+ import_node_fs15.default.rmSync(outDir, { recursive: true, force: true });
1400
1571
  }
1401
1572
  console.log("\nExporting to Paperclip format (agentcompanies/v1)...\n");
1402
- import_node_fs13.default.mkdirSync(outDir, { recursive: true });
1573
+ import_node_fs15.default.mkdirSync(outDir, { recursive: true });
1403
1574
  const goalsYaml = goals.length > 0 ? goals.map((g) => ` - ${g.description.split("\n")[0]}`).join("\n") : ` - ${mission}`;
1404
1575
  const pipelineLines = agents.map((a, i) => {
1405
1576
  return `${i + 1}. **${a.name}** ${a.title.toLowerCase()}`;
@@ -1429,17 +1600,17 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1429
1600
  `Generated with [Clawstrap](https://github.com/peppinho89/clawstrap) v${CLI_VERSION}`,
1430
1601
  ""
1431
1602
  ].join("\n");
1432
- import_node_fs13.default.writeFileSync(import_node_path14.default.join(outDir, "COMPANY.md"), companyMd, "utf-8");
1603
+ import_node_fs15.default.writeFileSync(import_node_path16.default.join(outDir, "COMPANY.md"), companyMd, "utf-8");
1433
1604
  console.log(" \u2713 COMPANY.md");
1434
- import_node_fs13.default.writeFileSync(
1435
- import_node_path14.default.join(outDir, ".paperclip.yaml"),
1605
+ import_node_fs15.default.writeFileSync(
1606
+ import_node_path16.default.join(outDir, ".paperclip.yaml"),
1436
1607
  "schema: paperclip/v1\n",
1437
1608
  "utf-8"
1438
1609
  );
1439
1610
  console.log(" \u2713 .paperclip.yaml");
1440
1611
  for (const agent of agents) {
1441
- const agentDir = import_node_path14.default.join(outDir, "agents", agent.slug);
1442
- import_node_fs13.default.mkdirSync(agentDir, { recursive: true });
1612
+ const agentDir = import_node_path16.default.join(outDir, "agents", agent.slug);
1613
+ import_node_fs15.default.mkdirSync(agentDir, { recursive: true });
1443
1614
  const frontmatterLines = [
1444
1615
  "---",
1445
1616
  `name: ${agent.name}`,
@@ -1454,12 +1625,12 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1454
1625
  }
1455
1626
  frontmatterLines.push("---");
1456
1627
  const agentMd = frontmatterLines.join("\n") + "\n\n" + agent.body + "\n";
1457
- import_node_fs13.default.writeFileSync(import_node_path14.default.join(agentDir, "AGENTS.md"), agentMd, "utf-8");
1628
+ import_node_fs15.default.writeFileSync(import_node_path16.default.join(agentDir, "AGENTS.md"), agentMd, "utf-8");
1458
1629
  console.log(` \u2713 agents/${agent.slug}/AGENTS.md`);
1459
1630
  }
1460
1631
  if (nonCeoAgents.length > 0) {
1461
- const teamDir = import_node_path14.default.join(outDir, "teams", "engineering");
1462
- import_node_fs13.default.mkdirSync(teamDir, { recursive: true });
1632
+ const teamDir = import_node_path16.default.join(outDir, "teams", "engineering");
1633
+ import_node_fs15.default.mkdirSync(teamDir, { recursive: true });
1463
1634
  const includesList = nonCeoAgents.map((a) => ` - ../../agents/${a.slug}/AGENTS.md`).join("\n");
1464
1635
  const teamMd = [
1465
1636
  "---",
@@ -1476,13 +1647,13 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1476
1647
  `The engineering team at ${companyName}. Led by the CEO, who scopes and delegates work to specialists.`,
1477
1648
  ""
1478
1649
  ].join("\n");
1479
- import_node_fs13.default.writeFileSync(import_node_path14.default.join(teamDir, "TEAM.md"), teamMd, "utf-8");
1650
+ import_node_fs15.default.writeFileSync(import_node_path16.default.join(teamDir, "TEAM.md"), teamMd, "utf-8");
1480
1651
  console.log(" \u2713 teams/engineering/TEAM.md");
1481
1652
  }
1482
1653
  for (const skill of skills) {
1483
- const skillDir = import_node_path14.default.join(outDir, "skills", skill.name);
1484
- import_node_fs13.default.mkdirSync(skillDir, { recursive: true });
1485
- import_node_fs13.default.writeFileSync(import_node_path14.default.join(skillDir, "SKILL.md"), skill.content, "utf-8");
1654
+ const skillDir = import_node_path16.default.join(outDir, "skills", skill.name);
1655
+ import_node_fs15.default.mkdirSync(skillDir, { recursive: true });
1656
+ import_node_fs15.default.writeFileSync(import_node_path16.default.join(skillDir, "SKILL.md"), skill.content, "utf-8");
1486
1657
  console.log(` \u2713 skills/${skill.name}/SKILL.md`);
1487
1658
  }
1488
1659
  const importScript = [
@@ -1498,26 +1669,26 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1498
1669
  'echo "Done. Open your Paperclip dashboard to review."',
1499
1670
  ""
1500
1671
  ].join("\n");
1501
- const importPath = import_node_path14.default.join(outDir, "import.sh");
1502
- import_node_fs13.default.writeFileSync(importPath, importScript, "utf-8");
1503
- import_node_fs13.default.chmodSync(importPath, 493);
1672
+ const importPath = import_node_path16.default.join(outDir, "import.sh");
1673
+ import_node_fs15.default.writeFileSync(importPath, importScript, "utf-8");
1674
+ import_node_fs15.default.chmodSync(importPath, 493);
1504
1675
  console.log(" \u2713 import.sh");
1505
1676
  const updatedConfig = {
1506
1677
  ...config,
1507
1678
  lastExport: {
1508
1679
  format: "paperclip",
1509
1680
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1510
- outputDir: import_node_path14.default.relative(rootDir, outDir) || outDir
1681
+ outputDir: import_node_path16.default.relative(rootDir, outDir) || outDir
1511
1682
  }
1512
1683
  };
1513
- import_node_fs13.default.writeFileSync(
1514
- import_node_path14.default.join(rootDir, ".clawstrap.json"),
1684
+ import_node_fs15.default.writeFileSync(
1685
+ import_node_path16.default.join(rootDir, ".clawstrap.json"),
1515
1686
  JSON.stringify(updatedConfig, null, 2) + "\n",
1516
1687
  "utf-8"
1517
1688
  );
1518
1689
  console.log(
1519
1690
  `
1520
- Exported to ${import_node_path14.default.relative(process.cwd(), outDir) || outDir}`
1691
+ Exported to ${import_node_path16.default.relative(process.cwd(), outDir) || outDir}`
1521
1692
  );
1522
1693
  console.log(
1523
1694
  `${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
@@ -1527,42 +1698,14 @@ Exported to ${import_node_path14.default.relative(process.cwd(), outDir) || outD
1527
1698
  }
1528
1699
 
1529
1700
  // src/watch.ts
1530
- var import_node_path22 = __toESM(require("path"), 1);
1531
- var import_node_fs21 = __toESM(require("fs"), 1);
1701
+ var import_node_path23 = __toESM(require("path"), 1);
1702
+ var import_node_fs22 = __toESM(require("fs"), 1);
1532
1703
 
1533
1704
  // src/watch/git.ts
1534
1705
  var import_node_child_process = require("child_process");
1535
- var import_node_fs15 = __toESM(require("fs"), 1);
1536
- var import_node_path16 = __toESM(require("path"), 1);
1706
+ var import_node_fs16 = __toESM(require("fs"), 1);
1707
+ var import_node_path17 = __toESM(require("path"), 1);
1537
1708
  init_writers();
1538
- var STOPWORDS = /* @__PURE__ */ new Set([
1539
- "fix",
1540
- "add",
1541
- "update",
1542
- "remove",
1543
- "feat",
1544
- "chore",
1545
- "the",
1546
- "a",
1547
- "an",
1548
- "and",
1549
- "or",
1550
- "in",
1551
- "on",
1552
- "at",
1553
- "to",
1554
- "for",
1555
- "of",
1556
- "is",
1557
- "was",
1558
- "be",
1559
- "it",
1560
- "as",
1561
- "with",
1562
- "by",
1563
- "this",
1564
- "that"
1565
- ]);
1566
1709
  function parseGitLog(output) {
1567
1710
  const entries = [];
1568
1711
  const lines = output.split("\n");
@@ -1590,7 +1733,7 @@ function getTopDirs(entries) {
1590
1733
  for (const entry of entries) {
1591
1734
  const seenDirs = /* @__PURE__ */ new Set();
1592
1735
  for (const file of entry.files) {
1593
- const dir = import_node_path16.default.dirname(file);
1736
+ const dir = import_node_path17.default.dirname(file);
1594
1737
  if (dir !== "." && !seenDirs.has(dir)) {
1595
1738
  seenDirs.add(dir);
1596
1739
  dirCount.set(dir, (dirCount.get(dir) ?? 0) + 1);
@@ -1623,7 +1766,7 @@ function getRecurringWords(entries) {
1623
1766
  return Array.from(wordCount.entries()).filter(([, count]) => count >= 2).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([word]) => word);
1624
1767
  }
1625
1768
  async function runGitObserver(rootDir, sinceCommit) {
1626
- if (!import_node_fs15.default.existsSync(import_node_path16.default.join(rootDir, ".git"))) {
1769
+ if (!import_node_fs16.default.existsSync(import_node_path17.default.join(rootDir, ".git"))) {
1627
1770
  return null;
1628
1771
  }
1629
1772
  let headSha;
@@ -1678,8 +1821,8 @@ async function runGitObserver(rootDir, sinceCommit) {
1678
1821
  }
1679
1822
 
1680
1823
  // src/watch/scan.ts
1681
- var import_node_fs16 = __toESM(require("fs"), 1);
1682
- var import_node_path17 = __toESM(require("path"), 1);
1824
+ var import_node_fs17 = __toESM(require("fs"), 1);
1825
+ var import_node_path18 = __toESM(require("path"), 1);
1683
1826
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "tmp", "dist", ".claude"]);
1684
1827
  var CODE_EXTS = /* @__PURE__ */ new Set([".ts", ".js", ".tsx", ".jsx"]);
1685
1828
  function walkDir(dir, maxDepth = 10, depth = 0) {
@@ -1687,13 +1830,13 @@ function walkDir(dir, maxDepth = 10, depth = 0) {
1687
1830
  let results = [];
1688
1831
  let entries;
1689
1832
  try {
1690
- entries = import_node_fs16.default.readdirSync(dir, { withFileTypes: true });
1833
+ entries = import_node_fs17.default.readdirSync(dir, { withFileTypes: true });
1691
1834
  } catch {
1692
1835
  return [];
1693
1836
  }
1694
1837
  for (const entry of entries) {
1695
1838
  if (SKIP_DIRS.has(entry.name)) continue;
1696
- const fullPath = import_node_path17.default.join(dir, entry.name);
1839
+ const fullPath = import_node_path18.default.join(dir, entry.name);
1697
1840
  if (entry.isDirectory()) {
1698
1841
  results = results.concat(walkDir(fullPath, maxDepth, depth + 1));
1699
1842
  } else if (entry.isFile()) {
@@ -1726,7 +1869,7 @@ function analyzeNaming(files) {
1726
1869
  other: []
1727
1870
  };
1728
1871
  for (const file of files) {
1729
- const base = import_node_path17.default.basename(file, import_node_path17.default.extname(file));
1872
+ const base = import_node_path18.default.basename(file, import_node_path18.default.extname(file));
1730
1873
  const style2 = detectNamingCase(base);
1731
1874
  counts[style2]++;
1732
1875
  if (examples[style2].length < 3) examples[style2].push(base);
@@ -1746,14 +1889,14 @@ function analyzeNaming(files) {
1746
1889
  return result;
1747
1890
  }
1748
1891
  function analyzeImports(files) {
1749
- const sample = files.filter((f) => CODE_EXTS.has(import_node_path17.default.extname(f))).slice(0, 20);
1892
+ const sample = files.filter((f) => CODE_EXTS.has(import_node_path18.default.extname(f))).slice(0, 20);
1750
1893
  let relativeCount = 0;
1751
1894
  let absoluteCount = 0;
1752
1895
  let barrelCount = 0;
1753
1896
  for (const file of sample) {
1754
1897
  let content;
1755
1898
  try {
1756
- content = import_node_fs16.default.readFileSync(file, "utf-8");
1899
+ content = import_node_fs17.default.readFileSync(file, "utf-8");
1757
1900
  } catch {
1758
1901
  continue;
1759
1902
  }
@@ -1762,7 +1905,7 @@ function analyzeImports(files) {
1762
1905
  if (/from\s+['"]\.\.?\//.test(line)) relativeCount++;
1763
1906
  else if (/from\s+['"]/.test(line)) absoluteCount++;
1764
1907
  }
1765
- const base = import_node_path17.default.basename(file, import_node_path17.default.extname(file));
1908
+ const base = import_node_path18.default.basename(file, import_node_path18.default.extname(file));
1766
1909
  if (base === "index") barrelCount++;
1767
1910
  }
1768
1911
  const results = [];
@@ -1786,7 +1929,7 @@ function analyzeTesting(files) {
1786
1929
  let hasSpecExt = false;
1787
1930
  let hasTestsDir = false;
1788
1931
  for (const file of files) {
1789
- const base = import_node_path17.default.basename(file);
1932
+ const base = import_node_path18.default.basename(file);
1790
1933
  if (/\.test\.(ts|js|tsx|jsx)$/.test(base)) hasTestExt = true;
1791
1934
  if (/\.spec\.(ts|js|tsx|jsx)$/.test(base)) hasSpecExt = true;
1792
1935
  if (file.includes("/__tests__/") || file.includes("\\__tests__\\")) hasTestsDir = true;
@@ -1800,13 +1943,13 @@ function analyzeTesting(files) {
1800
1943
  return [`Test patterns found: ${testPatterns.join(", ")}`];
1801
1944
  }
1802
1945
  function analyzeErrorHandling(files) {
1803
- const sample = files.filter((f) => CODE_EXTS.has(import_node_path17.default.extname(f))).slice(0, 20);
1946
+ const sample = files.filter((f) => CODE_EXTS.has(import_node_path18.default.extname(f))).slice(0, 20);
1804
1947
  let tryCatchCount = 0;
1805
1948
  let resultTypeCount = 0;
1806
1949
  for (const file of sample) {
1807
1950
  let content;
1808
1951
  try {
1809
- content = import_node_fs16.default.readFileSync(file, "utf-8");
1952
+ content = import_node_fs17.default.readFileSync(file, "utf-8");
1810
1953
  } catch {
1811
1954
  continue;
1812
1955
  }
@@ -1831,14 +1974,14 @@ function analyzeErrorHandling(files) {
1831
1974
  return results;
1832
1975
  }
1833
1976
  function analyzeComments(files) {
1834
- const sample = files.filter((f) => CODE_EXTS.has(import_node_path17.default.extname(f))).slice(0, 20);
1977
+ const sample = files.filter((f) => CODE_EXTS.has(import_node_path18.default.extname(f))).slice(0, 20);
1835
1978
  let jsdocCount = 0;
1836
1979
  let inlineCount = 0;
1837
1980
  let totalLines = 0;
1838
1981
  for (const file of sample) {
1839
1982
  let content;
1840
1983
  try {
1841
- content = import_node_fs16.default.readFileSync(file, "utf-8");
1984
+ content = import_node_fs17.default.readFileSync(file, "utf-8");
1842
1985
  } catch {
1843
1986
  continue;
1844
1987
  }
@@ -1871,13 +2014,13 @@ async function runScan(rootDir) {
1871
2014
  init_writers();
1872
2015
 
1873
2016
  // src/watch/daemon.ts
1874
- var import_node_fs20 = __toESM(require("fs"), 1);
1875
- var import_node_path21 = __toESM(require("path"), 1);
2017
+ var import_node_fs21 = __toESM(require("fs"), 1);
2018
+ var import_node_path22 = __toESM(require("path"), 1);
1876
2019
  init_writers();
1877
2020
 
1878
2021
  // src/watch/synthesize.ts
1879
- var import_node_fs17 = __toESM(require("fs"), 1);
1880
- var import_node_path18 = __toESM(require("path"), 1);
2022
+ var import_node_fs18 = __toESM(require("fs"), 1);
2023
+ var import_node_path19 = __toESM(require("path"), 1);
1881
2024
  init_dedup();
1882
2025
  var SYNTH_START = "<!-- CLAWSTRAP:SYNTHESIS:START -->";
1883
2026
  var SYNTH_END = "<!-- CLAWSTRAP:SYNTHESIS:END -->";
@@ -1905,29 +2048,29 @@ function buildSynthBlock(summary) {
1905
2048
  ].join("\n");
1906
2049
  }
1907
2050
  function writeSynthBlock(memoryPath, summary) {
1908
- const content = import_node_fs17.default.existsSync(memoryPath) ? import_node_fs17.default.readFileSync(memoryPath, "utf-8") : "";
2051
+ const content = import_node_fs18.default.existsSync(memoryPath) ? import_node_fs18.default.readFileSync(memoryPath, "utf-8") : "";
1909
2052
  const block = buildSynthBlock(summary);
1910
2053
  const startIdx = content.indexOf(SYNTH_START);
1911
2054
  const endIdx = content.indexOf(SYNTH_END);
1912
2055
  if (startIdx !== -1 && endIdx !== -1) {
1913
2056
  const before = content.slice(0, startIdx);
1914
2057
  const after = content.slice(endIdx + SYNTH_END.length);
1915
- import_node_fs17.default.writeFileSync(memoryPath, before + block + after, "utf-8");
2058
+ import_node_fs18.default.writeFileSync(memoryPath, before + block + after, "utf-8");
1916
2059
  return;
1917
2060
  }
1918
2061
  const headingMatch = /^#[^\n]*\n?/m.exec(content);
1919
2062
  if (headingMatch) {
1920
2063
  const insertAt = headingMatch.index + headingMatch[0].length;
1921
2064
  const updated = content.slice(0, insertAt) + "\n" + block + "\n" + content.slice(insertAt);
1922
- import_node_fs17.default.writeFileSync(memoryPath, updated, "utf-8");
2065
+ import_node_fs18.default.writeFileSync(memoryPath, updated, "utf-8");
1923
2066
  } else {
1924
- import_node_fs17.default.writeFileSync(memoryPath, block + "\n\n" + content, "utf-8");
2067
+ import_node_fs18.default.writeFileSync(memoryPath, block + "\n\n" + content, "utf-8");
1925
2068
  }
1926
2069
  }
1927
2070
  async function synthesizeMemory(rootDir, adapter) {
1928
- const memoryPath = import_node_path18.default.join(rootDir, ".claude", "memory", "MEMORY.md");
1929
- if (!import_node_fs17.default.existsSync(memoryPath)) return null;
1930
- const content = import_node_fs17.default.readFileSync(memoryPath, "utf-8");
2071
+ const memoryPath = import_node_path19.default.join(rootDir, ".claude", "memory", "MEMORY.md");
2072
+ if (!import_node_fs18.default.existsSync(memoryPath)) return null;
2073
+ const content = import_node_fs18.default.readFileSync(memoryPath, "utf-8");
1931
2074
  const contentWithoutSynthBlock = content.replace(
1932
2075
  /<!-- CLAWSTRAP:SYNTHESIS:START -->[\s\S]*?<!-- CLAWSTRAP:SYNTHESIS:END -->/,
1933
2076
  ""
@@ -1973,8 +2116,8 @@ Write a concise 3\u20135 sentence summary of the persistent truths about how thi
1973
2116
 
1974
2117
  // src/watch/infer.ts
1975
2118
  var import_node_child_process2 = require("child_process");
1976
- var import_node_fs18 = __toESM(require("fs"), 1);
1977
- var import_node_path19 = __toESM(require("path"), 1);
2119
+ var import_node_fs19 = __toESM(require("fs"), 1);
2120
+ var import_node_path20 = __toESM(require("path"), 1);
1978
2121
  var CODE_EXTS2 = /* @__PURE__ */ new Set([".ts", ".js", ".tsx", ".jsx"]);
1979
2122
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "tmp", "dist", ".claude"]);
1980
2123
  var MAX_FILES = 10;
@@ -1986,16 +2129,16 @@ function walkCodeFiles(rootDir) {
1986
2129
  if (depth > 8) return;
1987
2130
  let entries;
1988
2131
  try {
1989
- entries = import_node_fs18.default.readdirSync(dir, { withFileTypes: true });
2132
+ entries = import_node_fs19.default.readdirSync(dir, { withFileTypes: true });
1990
2133
  } catch {
1991
2134
  return;
1992
2135
  }
1993
2136
  for (const entry of entries) {
1994
2137
  if (SKIP_DIRS2.has(entry.name)) continue;
1995
- const fullPath = import_node_path19.default.join(dir, entry.name);
2138
+ const fullPath = import_node_path20.default.join(dir, entry.name);
1996
2139
  if (entry.isDirectory()) {
1997
2140
  walk(fullPath, depth + 1);
1998
- } else if (entry.isFile() && CODE_EXTS2.has(import_node_path19.default.extname(entry.name))) {
2141
+ } else if (entry.isFile() && CODE_EXTS2.has(import_node_path20.default.extname(entry.name))) {
1999
2142
  if (!/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(entry.name)) {
2000
2143
  results.push(fullPath);
2001
2144
  }
@@ -2018,11 +2161,11 @@ function getRecentlyChangedFiles(rootDir) {
2018
2161
  const trimmed = line.trim();
2019
2162
  if (!trimmed || seen.has(trimmed)) continue;
2020
2163
  seen.add(trimmed);
2021
- const ext = import_node_path19.default.extname(trimmed);
2164
+ const ext = import_node_path20.default.extname(trimmed);
2022
2165
  if (!CODE_EXTS2.has(ext)) continue;
2023
2166
  if (/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(trimmed)) continue;
2024
- const abs = import_node_path19.default.join(rootDir, trimmed);
2025
- if (import_node_fs18.default.existsSync(abs)) files.push(abs);
2167
+ const abs = import_node_path20.default.join(rootDir, trimmed);
2168
+ if (import_node_fs19.default.existsSync(abs)) files.push(abs);
2026
2169
  }
2027
2170
  return files;
2028
2171
  } catch {
@@ -2032,12 +2175,12 @@ function getRecentlyChangedFiles(rootDir) {
2032
2175
  function readTruncated(filePath, rootDir) {
2033
2176
  let content;
2034
2177
  try {
2035
- content = import_node_fs18.default.readFileSync(filePath, "utf-8");
2178
+ content = import_node_fs19.default.readFileSync(filePath, "utf-8");
2036
2179
  } catch {
2037
2180
  return "";
2038
2181
  }
2039
2182
  const lines = content.split("\n");
2040
- const relPath = import_node_path19.default.relative(rootDir, filePath);
2183
+ const relPath = import_node_path20.default.relative(rootDir, filePath);
2041
2184
  const truncated = lines.length > MAX_LINES_PER_FILE ? lines.slice(0, MAX_LINES_PER_FILE).join("\n") + "\n// ... truncated" : lines.join("\n");
2042
2185
  return `=== ${relPath} ===
2043
2186
  ${truncated}`;
@@ -2079,12 +2222,12 @@ Output only the rules \u2014 no explanation, no numbering, no markdown.`;
2079
2222
  }
2080
2223
 
2081
2224
  // src/watch/transcripts.ts
2082
- var import_node_fs19 = __toESM(require("fs"), 1);
2083
- var import_node_path20 = __toESM(require("path"), 1);
2225
+ var import_node_fs20 = __toESM(require("fs"), 1);
2226
+ var import_node_path21 = __toESM(require("path"), 1);
2084
2227
  async function processTranscript(filePath, adapter) {
2085
2228
  let content;
2086
2229
  try {
2087
- content = import_node_fs19.default.readFileSync(filePath, "utf-8");
2230
+ content = import_node_fs20.default.readFileSync(filePath, "utf-8");
2088
2231
  } catch {
2089
2232
  return null;
2090
2233
  }
@@ -2127,13 +2270,13 @@ Each item must be a concise one-sentence string. Arrays may be empty.`;
2127
2270
  }
2128
2271
  }
2129
2272
  function watchTranscriptDir(rootDir, onNewFile) {
2130
- const sessionsDir = import_node_path20.default.join(rootDir, "tmp", "sessions");
2131
- import_node_fs19.default.mkdirSync(sessionsDir, { recursive: true });
2132
- const watcher = import_node_fs19.default.watch(sessionsDir, (event, filename) => {
2273
+ const sessionsDir = import_node_path21.default.join(rootDir, "tmp", "sessions");
2274
+ import_node_fs20.default.mkdirSync(sessionsDir, { recursive: true });
2275
+ const watcher = import_node_fs20.default.watch(sessionsDir, (event, filename) => {
2133
2276
  if (event !== "rename" || !filename) return;
2134
2277
  if (!filename.endsWith(".md")) return;
2135
- const filePath = import_node_path20.default.join(sessionsDir, filename);
2136
- if (!import_node_fs19.default.existsSync(filePath)) return;
2278
+ const filePath = import_node_path21.default.join(sessionsDir, filename);
2279
+ if (!import_node_fs20.default.existsSync(filePath)) return;
2137
2280
  onNewFile(filePath).catch(() => {
2138
2281
  });
2139
2282
  });
@@ -2268,6 +2411,18 @@ function createAdapter(config) {
2268
2411
  }
2269
2412
 
2270
2413
  // src/watch/daemon.ts
2414
+ function serializedAdapter(adapter) {
2415
+ let chain = Promise.resolve();
2416
+ return {
2417
+ complete(prompt) {
2418
+ const result = chain.then(() => adapter.complete(prompt));
2419
+ chain = result.then(() => {
2420
+ }, () => {
2421
+ });
2422
+ return result;
2423
+ }
2424
+ };
2425
+ }
2271
2426
  async function runDaemon(rootDir, config, ui) {
2272
2427
  const cleanup = [];
2273
2428
  const shutdown = () => {
@@ -2287,7 +2442,7 @@ async function runDaemon(rootDir, config, ui) {
2287
2442
  if (gitResult) {
2288
2443
  updateWatchState(rootDir, { lastGitCommit: gitResult.lastCommit });
2289
2444
  }
2290
- const adapter = createAdapter(config);
2445
+ const adapter = serializedAdapter(createAdapter(config));
2291
2446
  let entriesSinceLastSynthesis = config.watchState?.entriesSinceLastSynthesis ?? 0;
2292
2447
  const synthEnabled = config.watch?.synthesis?.enabled ?? false;
2293
2448
  const triggerEveryN = config.watch?.synthesis?.triggerEveryN ?? 10;
@@ -2300,7 +2455,7 @@ async function runDaemon(rootDir, config, ui) {
2300
2455
  updateWatchState(rootDir, { entriesSinceLastSynthesis: "0" });
2301
2456
  };
2302
2457
  const stopTranscripts = watchTranscriptDir(rootDir, async (filePath) => {
2303
- ui.transcriptStart(import_node_path21.default.basename(filePath));
2458
+ ui.transcriptStart(import_node_path22.default.basename(filePath));
2304
2459
  ui.llmCallStart();
2305
2460
  const result = await processTranscript(filePath, adapter);
2306
2461
  ui.llmCallDone(result ? { decisions: result.decisions.length, corrections: result.corrections.length, openThreads: result.openThreads.length } : null);
@@ -2308,7 +2463,10 @@ async function runDaemon(rootDir, config, ui) {
2308
2463
  const { appendToMemory: appendToMemory2, appendToGotchaLog: appendToGotchaLog2, appendToFutureConsiderations: appendToFutureConsiderations2, appendToOpenThreads: appendToOpenThreads2 } = await Promise.resolve().then(() => (init_writers(), writers_exports));
2309
2464
  let written = 0;
2310
2465
  if (result.decisions.length) written += appendToMemory2(rootDir, result.decisions, "session");
2311
- if (result.corrections.length) appendToGotchaLog2(rootDir, result.corrections);
2466
+ if (result.corrections.length) {
2467
+ appendToGotchaLog2(rootDir, result.corrections);
2468
+ await checkAndPromoteCorrections(rootDir, adapter, ui);
2469
+ }
2312
2470
  if (result.deferredIdeas.length) appendToFutureConsiderations2(rootDir, result.deferredIdeas);
2313
2471
  if (result.openThreads.length) appendToOpenThreads2(rootDir, result.openThreads);
2314
2472
  entriesSinceLastSynthesis += written;
@@ -2364,16 +2522,16 @@ async function runDaemon(rootDir, config, ui) {
2364
2522
  }
2365
2523
  const scanTimer = setInterval(doScan, scanIntervalMs);
2366
2524
  cleanup.push(() => clearInterval(scanTimer));
2367
- ui.showIdle(import_node_path21.default.join(rootDir, "tmp", "sessions"));
2525
+ ui.showIdle(import_node_path22.default.join(rootDir, "tmp", "sessions"));
2368
2526
  await new Promise(() => {
2369
2527
  });
2370
2528
  }
2371
2529
  function updateWatchState(rootDir, updates) {
2372
- const configPath = import_node_path21.default.join(rootDir, ".clawstrap.json");
2530
+ const configPath = import_node_path22.default.join(rootDir, ".clawstrap.json");
2373
2531
  try {
2374
- const raw = JSON.parse(import_node_fs20.default.readFileSync(configPath, "utf-8"));
2532
+ const raw = JSON.parse(import_node_fs21.default.readFileSync(configPath, "utf-8"));
2375
2533
  raw["watchState"] = { ...raw["watchState"] ?? {}, ...updates };
2376
- import_node_fs20.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2534
+ import_node_fs21.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2377
2535
  } catch {
2378
2536
  }
2379
2537
  }
@@ -2381,6 +2539,7 @@ function updateWatchState(rootDir, updates) {
2381
2539
  // src/watch/ui.ts
2382
2540
  var import_picocolors = __toESM(require("picocolors"), 1);
2383
2541
  var import_ora = __toESM(require("ora"), 1);
2542
+ var ora = typeof import_ora.default === "function" ? import_ora.default : import_ora.default.default;
2384
2543
  function formatAgo(date) {
2385
2544
  if (!date) return "never";
2386
2545
  const diffMs = Date.now() - date.getTime();
@@ -2439,6 +2598,10 @@ var SilentUI = class {
2439
2598
  }
2440
2599
  inferDone(_rulesCount) {
2441
2600
  }
2601
+ promoteStart() {
2602
+ }
2603
+ promoteDone(_rulesWritten) {
2604
+ }
2442
2605
  showIdle(_watchDir) {
2443
2606
  }
2444
2607
  clear() {
@@ -2477,7 +2640,7 @@ ${import_picocolors.default.cyan("\u25C6")} ${import_picocolors.default.bold("G
2477
2640
  header(`New session summary detected ${import_picocolors.default.cyan(filename)}`);
2478
2641
  }
2479
2642
  llmCallStart() {
2480
- this.spinner = (0, import_ora.default)({
2643
+ this.spinner = ora({
2481
2644
  text: `${T.branch} Sending to LLM adapter...`,
2482
2645
  prefixText: ""
2483
2646
  }).start();
@@ -2505,7 +2668,7 @@ ${import_picocolors.default.cyan("\u25C6")} ${import_picocolors.default.bold("G
2505
2668
  header(`Convention scan ${import_picocolors.default.dim(`(last run: ${formatAgo(lastRunAt)})`)}`);
2506
2669
  }
2507
2670
  scanFilesStart() {
2508
- this.spinner = (0, import_ora.default)({
2671
+ this.spinner = ora({
2509
2672
  text: `${T.branch} Scanning files...`,
2510
2673
  prefixText: ""
2511
2674
  }).start();
@@ -2525,7 +2688,7 @@ ${import_picocolors.default.cyan("\u25C6")} ${import_picocolors.default.bold("G
2525
2688
  // Memory synthesis ──────────────────────────────────────────────────────────
2526
2689
  synthStart() {
2527
2690
  this.spinner?.stop();
2528
- this.spinner = (0, import_ora.default)({
2691
+ this.spinner = ora({
2529
2692
  text: `${T.branch} Synthesising memory...`,
2530
2693
  prefixText: ""
2531
2694
  }).start();
@@ -2544,7 +2707,7 @@ ${import_picocolors.default.cyan("\u25C6")} ${import_picocolors.default.bold("G
2544
2707
  // Architecture inference ────────────────────────────────────────────────────
2545
2708
  inferStart() {
2546
2709
  this.spinner?.stop();
2547
- this.spinner = (0, import_ora.default)({
2710
+ this.spinner = ora({
2548
2711
  text: `${T.branch} Inferring architecture patterns...`,
2549
2712
  prefixText: ""
2550
2713
  }).start();
@@ -2559,6 +2722,24 @@ ${import_picocolors.default.cyan("\u25C6")} ${import_picocolors.default.bold("G
2559
2722
  this.spinner = null;
2560
2723
  }
2561
2724
  }
2725
+ // Correction promotion ──────────────────────────────────────────────────────
2726
+ promoteStart() {
2727
+ this.spinner?.stop();
2728
+ this.spinner = ora({
2729
+ text: `${T.branch} Promoting corrections to rule...`,
2730
+ prefixText: ""
2731
+ }).start();
2732
+ }
2733
+ promoteDone(rulesWritten) {
2734
+ if (this.spinner) {
2735
+ if (rulesWritten > 0) {
2736
+ this.spinner.succeed(`${T.branch} Draft rule written to .claude/rules/ ${T.check}`);
2737
+ } else {
2738
+ this.spinner.fail(`${T.branch} Rule promotion failed`);
2739
+ }
2740
+ this.spinner = null;
2741
+ }
2742
+ }
2562
2743
  // Idle ──────────────────────────────────────────────────────────────────────
2563
2744
  showIdle(watchDir) {
2564
2745
  process.stdout.write(`
@@ -2633,18 +2814,18 @@ Watch is already running (pid ${pid}). Use --stop to stop it.
2633
2814
  await runDaemon(rootDir, config, ui);
2634
2815
  }
2635
2816
  function persistWatchState(rootDir, updates) {
2636
- const configPath = import_node_path22.default.join(rootDir, ".clawstrap.json");
2817
+ const configPath = import_node_path23.default.join(rootDir, ".clawstrap.json");
2637
2818
  try {
2638
- const raw = JSON.parse(import_node_fs21.default.readFileSync(configPath, "utf-8"));
2819
+ const raw = JSON.parse(import_node_fs22.default.readFileSync(configPath, "utf-8"));
2639
2820
  raw.watchState = { ...raw.watchState ?? {}, ...updates };
2640
- import_node_fs21.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2821
+ import_node_fs22.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2641
2822
  } catch {
2642
2823
  }
2643
2824
  }
2644
2825
  function injectWatchHook(rootDir, config) {
2645
- const governanceFile = import_node_path22.default.join(rootDir, "CLAUDE.md");
2646
- if (!import_node_fs21.default.existsSync(governanceFile)) return;
2647
- const content = import_node_fs21.default.readFileSync(governanceFile, "utf-8");
2826
+ const governanceFile = import_node_path23.default.join(rootDir, "CLAUDE.md");
2827
+ if (!import_node_fs22.default.existsSync(governanceFile)) return;
2828
+ const content = import_node_fs22.default.readFileSync(governanceFile, "utf-8");
2648
2829
  if (content.includes("<!-- CLAWSTRAP:WATCH -->")) return;
2649
2830
  const _config = config;
2650
2831
  void _config;
@@ -2671,23 +2852,23 @@ function injectWatchHook(rootDir, config) {
2671
2852
 
2672
2853
  The watch daemon picks this up automatically and updates MEMORY.md and gotcha-log.md.
2673
2854
  `;
2674
- import_node_fs21.default.appendFileSync(governanceFile, hook, "utf-8");
2855
+ import_node_fs22.default.appendFileSync(governanceFile, hook, "utf-8");
2675
2856
  }
2676
2857
 
2677
2858
  // src/analyze.ts
2678
- var import_node_fs22 = __toESM(require("fs"), 1);
2679
- var import_node_path23 = __toESM(require("path"), 1);
2859
+ var import_node_fs23 = __toESM(require("fs"), 1);
2860
+ var import_node_path24 = __toESM(require("path"), 1);
2680
2861
  init_writers();
2681
2862
  async function analyze() {
2682
2863
  const { rootDir } = loadWorkspace();
2683
2864
  console.log("\nScanning codebase conventions...\n");
2684
2865
  const sections = await runScan(rootDir);
2685
2866
  writeConventions(rootDir, sections);
2686
- const configPath = import_node_path23.default.join(rootDir, ".clawstrap.json");
2867
+ const configPath = import_node_path24.default.join(rootDir, ".clawstrap.json");
2687
2868
  try {
2688
- const raw = JSON.parse(import_node_fs22.default.readFileSync(configPath, "utf-8"));
2869
+ const raw = JSON.parse(import_node_fs23.default.readFileSync(configPath, "utf-8"));
2689
2870
  raw["watchState"] = { ...raw["watchState"] ?? {}, lastScanAt: (/* @__PURE__ */ new Date()).toISOString() };
2690
- import_node_fs22.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2871
+ import_node_fs23.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2691
2872
  } catch {
2692
2873
  }
2693
2874
  console.log(" \u2713 .claude/rules/conventions.md updated\n");
@@ -2695,7 +2876,7 @@ async function analyze() {
2695
2876
 
2696
2877
  // src/index.ts
2697
2878
  var program = new import_commander.Command();
2698
- program.name("clawstrap").description("Scaffold a production-ready AI agent workspace").version("1.4.2");
2879
+ program.name("clawstrap").description("Scaffold a production-ready AI agent workspace").version("1.5.1");
2699
2880
  program.command("init").description("Create a new AI workspace in the current directory").argument("[directory]", "Target directory", ".").option("-y, --yes", "Use defaults, skip prompts").option("--sdd", "Enable Spec-Driven Development mode").action(async (directory, options) => {
2700
2881
  await init(directory, options);
2701
2882
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawstrap",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Scaffold a production-ready AI agent workspace in under 2 minutes",
5
5
  "type": "module",
6
6
  "bin": {