clawstrap 1.2.0 → 1.4.0

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 (3) hide show
  1. package/README.md +176 -74
  2. package/dist/index.cjs +1104 -68
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
9
16
  var __copyProps = (to, from, except, desc) => {
10
17
  if (from && typeof from === "object" || typeof from === "function") {
11
18
  for (let key of __getOwnPropNames(from))
@@ -23,6 +30,199 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
30
  mod
24
31
  ));
25
32
 
33
+ // src/watch/dedup.ts
34
+ function tokenize(text) {
35
+ const words = text.split(/\s+/).map((w) => w.replace(/[^a-z0-9]/gi, "").toLowerCase()).filter(Boolean);
36
+ return new Set(words);
37
+ }
38
+ function jaccard(a, b) {
39
+ const setA = tokenize(a);
40
+ const setB = tokenize(b);
41
+ if (setA.size === 0 && setB.size === 0) return 1;
42
+ if (setA.size === 0 || setB.size === 0) return 0;
43
+ let intersection = 0;
44
+ for (const token of setA) {
45
+ if (setB.has(token)) intersection++;
46
+ }
47
+ const union = setA.size + setB.size - intersection;
48
+ return intersection / union;
49
+ }
50
+ function isDuplicate(newEntry, existingEntries, threshold = 0.75) {
51
+ for (const existing of existingEntries) {
52
+ if (jaccard(newEntry, existing) >= threshold) {
53
+ return true;
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+ function parseMemoryEntries(content) {
59
+ const lines = content.split("\n");
60
+ const entries = [];
61
+ let current = [];
62
+ for (const line of lines) {
63
+ if (line.startsWith("---")) {
64
+ if (current.length > 0) {
65
+ const entry = current.join("\n").trim();
66
+ if (entry) entries.push(entry);
67
+ current = [];
68
+ }
69
+ } else {
70
+ current.push(line);
71
+ }
72
+ }
73
+ if (current.length > 0) {
74
+ const entry = current.join("\n").trim();
75
+ if (entry) entries.push(entry);
76
+ }
77
+ return entries;
78
+ }
79
+ var init_dedup = __esm({
80
+ "src/watch/dedup.ts"() {
81
+ "use strict";
82
+ }
83
+ });
84
+
85
+ // src/watch/writers.ts
86
+ var writers_exports = {};
87
+ __export(writers_exports, {
88
+ appendToFutureConsiderations: () => appendToFutureConsiderations,
89
+ appendToGotchaLog: () => appendToGotchaLog,
90
+ appendToMemory: () => appendToMemory,
91
+ writeConventions: () => writeConventions
92
+ });
93
+ function formatEntry(source, text) {
94
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
95
+ return `---
96
+ [${source}] ${ts}
97
+ ${text}`;
98
+ }
99
+ function appendToMemory(rootDir, entries, source) {
100
+ const memoryPath = import_node_path15.default.join(rootDir, ".claude", "memory", "MEMORY.md");
101
+ import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(memoryPath), { recursive: true });
102
+ let existingContent = "";
103
+ if (import_node_fs14.default.existsSync(memoryPath)) {
104
+ existingContent = import_node_fs14.default.readFileSync(memoryPath, "utf-8");
105
+ }
106
+ const existingEntries = parseMemoryEntries(existingContent);
107
+ const toAppend = [];
108
+ for (const entry of entries) {
109
+ if (!isDuplicate(entry, existingEntries)) {
110
+ toAppend.push(formatEntry(source, entry));
111
+ }
112
+ }
113
+ if (toAppend.length > 0) {
114
+ const appendText = "\n" + toAppend.join("\n") + "\n";
115
+ import_node_fs14.default.appendFileSync(memoryPath, appendText, "utf-8");
116
+ }
117
+ }
118
+ function appendToGotchaLog(rootDir, entries) {
119
+ const logPath = import_node_path15.default.join(rootDir, ".claude", "gotcha-log.md");
120
+ import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(logPath), { recursive: true });
121
+ if (!import_node_fs14.default.existsSync(logPath)) {
122
+ import_node_fs14.default.writeFileSync(
123
+ logPath,
124
+ "# Gotcha Log\n\nIncident log \u2014 why rules exist.\n\n",
125
+ "utf-8"
126
+ );
127
+ }
128
+ const toAppend = entries.map((e) => formatEntry("session", e)).join("\n");
129
+ import_node_fs14.default.appendFileSync(logPath, "\n" + toAppend + "\n", "utf-8");
130
+ }
131
+ function appendToFutureConsiderations(rootDir, entries) {
132
+ const fcPath = import_node_path15.default.join(rootDir, ".claude", "future-considerations.md");
133
+ import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(fcPath), { recursive: true });
134
+ if (!import_node_fs14.default.existsSync(fcPath)) {
135
+ import_node_fs14.default.writeFileSync(
136
+ fcPath,
137
+ "# Future Considerations\n\nDeferred ideas and potential improvements.\n\n",
138
+ "utf-8"
139
+ );
140
+ }
141
+ const toAppend = entries.map((e) => formatEntry("session", e)).join("\n");
142
+ import_node_fs14.default.appendFileSync(fcPath, "\n" + toAppend + "\n", "utf-8");
143
+ }
144
+ function buildAutoBlock(sections) {
145
+ const lines = [AUTO_START];
146
+ lines.push("## Naming Conventions");
147
+ if (sections.naming.length > 0) {
148
+ for (const item of sections.naming) lines.push(`- ${item}`);
149
+ } else {
150
+ lines.push("- No naming conventions detected.");
151
+ }
152
+ lines.push("");
153
+ lines.push("## Import Style");
154
+ if (sections.imports.length > 0) {
155
+ for (const item of sections.imports) lines.push(`- ${item}`);
156
+ } else {
157
+ lines.push("- No import patterns detected.");
158
+ }
159
+ lines.push("");
160
+ lines.push("## Testing");
161
+ if (sections.testing.length > 0) {
162
+ for (const item of sections.testing) lines.push(`- ${item}`);
163
+ } else {
164
+ lines.push("- No test files detected.");
165
+ }
166
+ lines.push("");
167
+ lines.push("## Error Handling");
168
+ if (sections.errorHandling.length > 0) {
169
+ for (const item of sections.errorHandling) lines.push(`- ${item}`);
170
+ } else {
171
+ lines.push("- No error handling patterns detected.");
172
+ }
173
+ lines.push("");
174
+ lines.push("## Comments");
175
+ if (sections.comments.length > 0) {
176
+ for (const item of sections.comments) lines.push(`- ${item}`);
177
+ } else {
178
+ lines.push("- No comment patterns detected.");
179
+ }
180
+ lines.push(AUTO_END);
181
+ return lines.join("\n");
182
+ }
183
+ function writeConventions(rootDir, sections) {
184
+ const conventionsPath = import_node_path15.default.join(rootDir, ".claude", "rules", "conventions.md");
185
+ import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(conventionsPath), { recursive: true });
186
+ const autoBlock = buildAutoBlock(sections);
187
+ if (!import_node_fs14.default.existsSync(conventionsPath)) {
188
+ const content = [
189
+ "# Conventions",
190
+ "",
191
+ "> Auto-generated by `clawstrap analyze`. Do not edit the AUTO block manually.",
192
+ "",
193
+ autoBlock,
194
+ "",
195
+ "<!-- Add manual conventions below this line -->",
196
+ ""
197
+ ].join("\n");
198
+ import_node_fs14.default.writeFileSync(conventionsPath, content, "utf-8");
199
+ return;
200
+ }
201
+ const existing = import_node_fs14.default.readFileSync(conventionsPath, "utf-8");
202
+ const startIdx = existing.indexOf(AUTO_START);
203
+ const endIdx = existing.indexOf(AUTO_END);
204
+ if (startIdx === -1 || endIdx === -1) {
205
+ const updated = existing.trimEnd() + "\n\n" + autoBlock + "\n";
206
+ import_node_fs14.default.writeFileSync(conventionsPath, updated, "utf-8");
207
+ } else {
208
+ const before = existing.slice(0, startIdx);
209
+ const after = existing.slice(endIdx + AUTO_END.length);
210
+ const updated = before + autoBlock + after;
211
+ import_node_fs14.default.writeFileSync(conventionsPath, updated, "utf-8");
212
+ }
213
+ }
214
+ var import_node_fs14, import_node_path15, AUTO_START, AUTO_END;
215
+ var init_writers = __esm({
216
+ "src/watch/writers.ts"() {
217
+ "use strict";
218
+ import_node_fs14 = __toESM(require("fs"), 1);
219
+ import_node_path15 = __toESM(require("path"), 1);
220
+ init_dedup();
221
+ AUTO_START = "<!-- CLAWSTRAP:AUTO -->";
222
+ AUTO_END = "<!-- CLAWSTRAP:END -->";
223
+ }
224
+ });
225
+
26
226
  // src/index.ts
27
227
  var import_commander = require("commander");
28
228
 
@@ -57,6 +257,19 @@ var ClawstrapConfigSchema = import_zod.z.object({
57
257
  parallelAgents: import_zod.z.enum(PARALLEL_AGENTS),
58
258
  qualityLevel: import_zod.z.enum(QUALITY_LEVELS),
59
259
  sessionHandoff: import_zod.z.boolean(),
260
+ sdd: import_zod.z.boolean().default(false),
261
+ watch: import_zod.z.object({
262
+ adapter: import_zod.z.enum(["claude-local", "claude-api", "ollama", "codex-local"]).default("claude-local"),
263
+ scan: import_zod.z.object({
264
+ intervalDays: import_zod.z.number().default(7)
265
+ }).default({}),
266
+ silent: import_zod.z.boolean().default(false)
267
+ }).optional(),
268
+ watchState: import_zod.z.object({
269
+ lastGitCommit: import_zod.z.string().optional(),
270
+ lastScanAt: import_zod.z.string().optional(),
271
+ lastTranscriptAt: import_zod.z.string().optional()
272
+ }).optional(),
60
273
  lastExport: LastExportSchema
61
274
  });
62
275
 
@@ -104,7 +317,9 @@ function deriveTemplateVars(config) {
104
317
  hasQualityGates: config.qualityLevel !== "solo",
105
318
  isProductionQuality: config.qualityLevel === "production",
106
319
  // Session handoff
107
- sessionHandoff: config.sessionHandoff
320
+ sessionHandoff: config.sessionHandoff,
321
+ // Spec-Driven Development
322
+ sddEnabled: config.sdd
108
323
  };
109
324
  }
110
325
 
@@ -210,18 +425,33 @@ var OUTPUT_MANIFEST = [
210
425
  templateKey: "memoryIndex",
211
426
  outputPath: "{%systemDir%}/memory/MEMORY.md",
212
427
  condition: "sessionHandoff"
428
+ },
429
+ {
430
+ templateKey: "sddRule",
431
+ outputPath: "{%systemDir%}/rules/sdd.md",
432
+ condition: "sddEnabled"
433
+ },
434
+ {
435
+ templateKey: "sddSpecTemplate",
436
+ outputPath: "specs/_template.md",
437
+ condition: "sddEnabled"
438
+ },
439
+ {
440
+ templateKey: "sddSpecCommand",
441
+ outputPath: "{%systemDir%}/commands/spec.md",
442
+ condition: "sddEnabled"
213
443
  }
214
444
  ];
215
445
  var EMPTY_DIRS = ["tmp", "research", "context", "artifacts"];
216
446
 
217
447
  // src/templates/governance-file.md.tmpl
218
- 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";
448
+ 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";
219
449
 
220
450
  // src/templates/getting-started.md.tmpl
221
- 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```\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*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";
451
+ 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";
222
452
 
223
453
  // src/templates/gitignore.tmpl
224
- 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";
454
+ 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";
225
455
 
226
456
  // src/templates/rules/context-discipline.md.tmpl
227
457
  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";
@@ -327,6 +557,15 @@ var add_project_readme_md_default = "# Project: {%projectName%}\n> **Status**: a
327
557
  // src/templates/projects/add-project-process.md.tmpl
328
558
  var add_project_process_md_default = '# Process: {%projectName%}\n> Workflow, hooks, and session checklist for this project.\n> Update this file as the workflow evolves.\n\n---\n{%#if sessionHandoff%}\n## Session Start Checklist (Pre-Hooks)\n\nRun these at the start of every session on this project:\n\n1. [ ] Read `README.md` \u2014 confirm current status and next step\n2. [ ] Read `{%governanceFile%}` (auto-loaded, but confirm it\'s current)\n3. [ ] Check `tmp/{task}/plan.md` if a batch is in progress\n4. [ ] Confirm SSOT files are in expected state (no stale data)\n\n---\n\n## Session End Checklist (Post-Hooks)\n\nRun these at the end of every session (mandatory \u2014 not optional):\n\n1. [ ] Save all work to SSOT files\n2. [ ] Sync derived files (regenerate from SSOTs)\n3. [ ] SSOT integrity check (no duplicates, no stale data)\n4. [ ] Update `README.md` \u2192 "Last done" and "Next" fields\n5. [ ] Write next-session plan to `tmp/{task}/plan.md`\n6. [ ] Run QC on work done this session\n\n---\n{%/if%}\n\n## Execution Workflow\n\n*(Describe the main execution loop for this project\'s primary task)*\n{%#if isResearch%}\n```\n1. Define research question \u2192 get approval\n2. Gather sources \u2192 write findings to file immediately\n3. Synthesize \u2192 cite all sources\n4. QC gate: {%flushCadence%}\n5. Output: artifacts/{topic}/synthesis.md\n```\n{%/if%}\n{%#if isContent%}\n```\n1. Brief \u2192 get approval on scope and outline\n2. Draft \u2192 write to artifacts/\n3. Review \u2192 collect feedback in dedicated file\n4. Revise \u2192 approval required before finalizing\n5. QC gate: {%flushCadence%}\n```\n{%/if%}\n{%#if isDataProcessing%}\n```\n1. Plan \u2192 get approval \u2192 define batch size\n2. Process batch \u2192 validate schema on every write\n3. QC gate: {%flushCadence%} (Ralph Loop)\n4. Output: tmp/{task}/{name}.json\n5. Human review \u2192 finalize\n```\n{%/if%}\n{%#if isCustom%}\n```\n1. Plan \u2192 get approval \u2192 execute\n2. Batch size: [N items per batch]\n3. QC gate: {%flushCadence%}\n4. Output: tmp/{task}/{name}.json\n```\n{%/if%}\n\n---\n{%#if hasQualityGates%}\n## Active Batch Tracking\n\n| Batch | Status | QC Grade | Notes |\n|-------|--------|----------|-------|\n| *(none yet)* | \u2014 | \u2014 | \u2014 |\n\n---\n{%/if%}\n\n## Known Gotchas (Project-Specific)\n\n*(1-2 line summaries of project-specific failure modes)*\n\n*(None yet \u2014 add entries as issues arise)*\n';
329
559
 
560
+ // src/templates/rules/sdd.md.tmpl
561
+ var sdd_md_default = '# Rule: Spec-Driven Development\n> **Scope**: All sessions | **Version**: 1.0 | **Generated**: {%generatedDate%}\n\n## The Rule\n\nNever implement from a vague prompt. Before writing any code or making structural\nchanges, write a spec. Get explicit approval. Then implement from the spec.\n\nThis rule exists because "just do it" prompts produce work that satisfies the\nsurface request while violating unstated constraints. Specs surface those\nconstraints before the work starts.\n\n---\n\n## When to Write a Spec\n\nA spec is required for:\n\n- Any new feature or component\n- Any refactor that touches more than 3 files\n- Any change to a public API or data schema\n- Any work whose scope is not fully clear from the request\n\nA spec is **not** required for:\n\n- Bug fixes under 5 lines\n- Documentation-only changes\n- Adding tests for existing, already-specified behavior\n- Changes explicitly described as "no spec needed" by the user\n\n---\n\n## The Workflow\n\n1. **Receive request** \u2014 user describes what they want\n2. **Write spec** \u2014 create `specs/{kebab-name}.md` using `specs/_template.md`\n3. **Present for approval** \u2014 show the spec to the user before doing any implementation\n4. **Incorporate feedback** \u2014 update the spec until approved; do not start work until then\n5. **Implement from spec** \u2014 treat the approved spec as the contract; flag deviations\n6. **Mark complete** \u2014 update spec status to `complete` after implementation\n\n---\n\n## Spec Naming\n\nUse kebab-case, descriptive, scoped to the feature:\n\n```\nspecs/user-authentication.md\nspecs/csv-export-pipeline.md\nspecs/agent-retry-logic.md\n```\n\n---\n\n## On Pushback\n\nIf the user says "just do it, skip the spec":\n\n- Acknowledge the preference\n- Offer a 5-minute lightweight spec (problem + criteria only) as a compromise\n- If they explicitly confirm "no spec", proceed \u2014 but note the deviation\n\nThe rule exists to protect the user\'s time, not to create friction for its own sake.\n';
562
+
563
+ // src/templates/sdd-spec-template.md.tmpl
564
+ var sdd_spec_template_md_default = "# Spec: {title}\n> **Status**: draft | **Created**: {%generatedDate%} | **Workspace**: {%workspaceName%}\n\n---\n\n## Problem Statement\n\n_What problem does this solve? What is the user need or pain point?_\n\n---\n\n## Acceptance Criteria\n\n_What must be true for this to be considered done? Be specific and testable._\n\n- [ ] ...\n- [ ] ...\n- [ ] ...\n\n---\n\n## Technical Constraints\n\n_What must this solution comply with?_\n\n- Must use: ...\n- Must not: ...\n- Performance: ...\n- Compatibility: ...\n\n---\n\n## Out of Scope\n\n_What are we explicitly NOT doing in this spec?_\n\n- ...\n\n---\n\n## Open Questions\n\n_What needs to be decided before or during implementation?_\n\n- [ ] ...\n\n---\n\n## Implementation Notes\n\n_Claude fills this in after implementation is complete._\n\n- Approach taken: ...\n- Key decisions made: ...\n- Deviations from spec (if any): ...\n\n---\n\n> Copy this file to `specs/{kebab-feature-name}.md`, fill it in, and get approval\n> before starting work. See `.claude/rules/sdd.md` for the full workflow.\n";
565
+
566
+ // src/templates/commands/spec.md.tmpl
567
+ var spec_md_default = 'Write a spec for the feature or change the user just described. Follow these steps exactly:\n\n1. Ask the user for the feature name if they haven\'t provided one. Use it to create the spec filename in kebab-case: `specs/{name}.md`.\n\n2. Copy the structure from `specs/_template.md` and fill in each section based on what you know from the conversation:\n - **Problem Statement**: why this feature is needed\n - **Acceptance Criteria**: specific, testable conditions for done\n - **Technical Constraints**: things the solution must or must not do\n - **Out of Scope**: what you are explicitly not building\n - **Open Questions**: anything that needs a decision before or during work\n\n3. Save the completed spec to `specs/{name}.md`.\n\n4. Present the spec to the user and ask: "Does this spec look right? Any changes before I start?"\n\n5. Do NOT begin implementation until the user explicitly approves the spec. If they request changes, update the spec and re-present it.\n\n6. Once approved, implement from the spec. If you need to deviate from it during implementation, flag the deviation and get confirmation before proceeding.\n\nThe spec is the contract. Build what the spec says, not what you think was meant.\n';
568
+
330
569
  // src/templates/index.ts
331
570
  var templates = {
332
571
  governanceFile: governance_file_md_default,
@@ -347,7 +586,10 @@ var templates = {
347
586
  newAgent: new_agent_md_default,
348
587
  newSkill: new_skill_md_default,
349
588
  addProjectReadme: add_project_readme_md_default,
350
- addProjectProcess: add_project_process_md_default
589
+ addProjectProcess: add_project_process_md_default,
590
+ sddRule: sdd_md_default,
591
+ sddSpecTemplate: sdd_spec_template_md_default,
592
+ sddSpecCommand: spec_md_default
351
593
  };
352
594
 
353
595
  // src/writer.ts
@@ -476,12 +718,17 @@ async function runPrompts() {
476
718
  message: "Enable session handoff checklists? (for multi-session work)",
477
719
  default: true
478
720
  });
721
+ const sdd = await (0, import_prompts.confirm)({
722
+ message: "Enable Spec-Driven Development? (write specs before implementing)",
723
+ default: false
724
+ });
479
725
  return {
480
726
  workspaceName,
481
727
  workloadType,
482
728
  parallelAgents,
483
729
  qualityLevel,
484
- sessionHandoff
730
+ sessionHandoff,
731
+ sdd
485
732
  };
486
733
  }
487
734
  function getDefaults(targetDir) {
@@ -491,7 +738,8 @@ function getDefaults(targetDir) {
491
738
  workloadType: "custom",
492
739
  parallelAgents: "single",
493
740
  qualityLevel: "solo",
494
- sessionHandoff: true
741
+ sessionHandoff: true,
742
+ sdd: false
495
743
  };
496
744
  }
497
745
 
@@ -537,7 +785,8 @@ async function init(directory, options) {
537
785
  workloadType: answers.workloadType,
538
786
  parallelAgents: answers.parallelAgents,
539
787
  qualityLevel: answers.qualityLevel,
540
- sessionHandoff: answers.sessionHandoff
788
+ sessionHandoff: answers.sessionHandoff,
789
+ sdd: options.sdd ?? answers.sdd
541
790
  });
542
791
  console.log("\nConfiguration:");
543
792
  console.log(` Workspace: ${config.workspaceName}`);
@@ -547,6 +796,7 @@ async function init(directory, options) {
547
796
  console.log(
548
797
  ` Session handoff: ${config.sessionHandoff ? "yes" : "no"}`
549
798
  );
799
+ console.log(` Spec-driven dev: ${config.sdd ? "yes" : "no"}`);
550
800
  const vars = deriveTemplateVars(config);
551
801
  console.log("\nGenerating your workspace...\n");
552
802
  const result = writeWorkspace(targetDir, vars, config);
@@ -733,8 +983,43 @@ Error: project "${name}" already exists at projects/${name}/
733
983
  }
734
984
 
735
985
  // src/status.ts
986
+ var import_node_fs8 = __toESM(require("fs"), 1);
987
+ var import_node_path9 = __toESM(require("path"), 1);
988
+
989
+ // src/watch/pid.ts
736
990
  var import_node_fs7 = __toESM(require("fs"), 1);
737
991
  var import_node_path8 = __toESM(require("path"), 1);
992
+ var PID_FILE = ".clawstrap.watch.pid";
993
+ function pidPath(rootDir) {
994
+ return import_node_path8.default.join(rootDir, PID_FILE);
995
+ }
996
+ function writePid(rootDir, pid) {
997
+ import_node_fs7.default.writeFileSync(pidPath(rootDir), String(pid), "utf-8");
998
+ }
999
+ function readPid(rootDir) {
1000
+ const p = pidPath(rootDir);
1001
+ if (!import_node_fs7.default.existsSync(p)) return null;
1002
+ const raw = import_node_fs7.default.readFileSync(p, "utf-8").trim();
1003
+ const pid = parseInt(raw, 10);
1004
+ return isNaN(pid) ? null : pid;
1005
+ }
1006
+ function clearPid(rootDir) {
1007
+ const p = pidPath(rootDir);
1008
+ if (import_node_fs7.default.existsSync(p)) import_node_fs7.default.unlinkSync(p);
1009
+ }
1010
+ function isDaemonRunning(rootDir) {
1011
+ const pid = readPid(rootDir);
1012
+ if (pid === null) return false;
1013
+ try {
1014
+ process.kill(pid, 0);
1015
+ return true;
1016
+ } catch {
1017
+ clearPid(rootDir);
1018
+ return false;
1019
+ }
1020
+ }
1021
+
1022
+ // src/status.ts
738
1023
  var WORKLOAD_LABELS3 = {
739
1024
  research: "Research & Analysis",
740
1025
  content: "Content & Writing",
@@ -742,22 +1027,22 @@ var WORKLOAD_LABELS3 = {
742
1027
  custom: "General Purpose"
743
1028
  };
744
1029
  function countEntries(dir, exclude = []) {
745
- if (!import_node_fs7.default.existsSync(dir)) return 0;
746
- return import_node_fs7.default.readdirSync(dir, { withFileTypes: true }).filter((e) => !exclude.includes(e.name) && !e.name.startsWith(".")).length;
1030
+ if (!import_node_fs8.default.existsSync(dir)) return 0;
1031
+ return import_node_fs8.default.readdirSync(dir, { withFileTypes: true }).filter((e) => !exclude.includes(e.name) && !e.name.startsWith(".")).length;
747
1032
  }
748
1033
  function countSkills(skillsDir) {
749
- if (!import_node_fs7.default.existsSync(skillsDir)) return 0;
750
- return import_node_fs7.default.readdirSync(skillsDir, { withFileTypes: true }).filter(
751
- (e) => e.isDirectory() && import_node_fs7.default.existsSync(import_node_path8.default.join(skillsDir, e.name, "SKILL.md"))
1034
+ if (!import_node_fs8.default.existsSync(skillsDir)) return 0;
1035
+ return import_node_fs8.default.readdirSync(skillsDir, { withFileTypes: true }).filter(
1036
+ (e) => e.isDirectory() && import_node_fs8.default.existsSync(import_node_path9.default.join(skillsDir, e.name, "SKILL.md"))
752
1037
  ).length;
753
1038
  }
754
1039
  async function showStatus() {
755
1040
  const { config, vars, rootDir } = loadWorkspace();
756
1041
  const systemDir = String(vars.systemDir);
757
- const agentsDir = import_node_path8.default.join(rootDir, systemDir, "agents");
758
- const skillsDir = import_node_path8.default.join(rootDir, systemDir, "skills");
759
- const rulesDir = import_node_path8.default.join(rootDir, systemDir, "rules");
760
- const projectsDir = import_node_path8.default.join(rootDir, "projects");
1042
+ const agentsDir = import_node_path9.default.join(rootDir, systemDir, "agents");
1043
+ const skillsDir = import_node_path9.default.join(rootDir, systemDir, "skills");
1044
+ const rulesDir = import_node_path9.default.join(rootDir, systemDir, "rules");
1045
+ const projectsDir = import_node_path9.default.join(rootDir, "projects");
761
1046
  const agentCount = countEntries(agentsDir, ["_template.md"]);
762
1047
  const skillCount = countSkills(skillsDir);
763
1048
  const projectCount = countEntries(projectsDir, ["_template"]);
@@ -776,6 +1061,7 @@ Configuration:`);
776
1061
  console.log(
777
1062
  ` Session handoff: ${config.sessionHandoff ? "yes" : "no"}`
778
1063
  );
1064
+ console.log(` Spec-driven dev: ${config.sdd ? "yes" : "no"}`);
779
1065
  console.log(`
780
1066
  Structure:`);
781
1067
  console.log(` Agents: ${agentCount} (${systemDir}/agents/)`);
@@ -790,32 +1076,48 @@ Last Export:`);
790
1076
  console.log(` Date: ${exportDate}`);
791
1077
  console.log(` Output: ${config.lastExport.outputDir}`);
792
1078
  }
1079
+ const watchRunning = isDaemonRunning(rootDir);
1080
+ console.log(`
1081
+ Watch:`);
1082
+ console.log(` Status: ${watchRunning ? "running" : "stopped"}`);
1083
+ if (config.watch) {
1084
+ console.log(` Adapter: ${config.watch.adapter}`);
1085
+ }
1086
+ if (config.watchState?.lastGitCommit) {
1087
+ console.log(` Last git: ${config.watchState.lastGitCommit.slice(0, 8)}`);
1088
+ }
1089
+ if (config.watchState?.lastScanAt) {
1090
+ console.log(` Last scan: ${config.watchState.lastScanAt.split("T")[0]}`);
1091
+ }
1092
+ if (config.watchState?.lastTranscriptAt) {
1093
+ console.log(` Last transcript: ${config.watchState.lastTranscriptAt.replace("T", " ").slice(0, 16)}`);
1094
+ }
793
1095
  console.log();
794
1096
  }
795
1097
 
796
1098
  // src/export-paperclip.ts
797
- var import_node_fs12 = __toESM(require("fs"), 1);
798
- var import_node_path13 = __toESM(require("path"), 1);
1099
+ var import_node_fs13 = __toESM(require("fs"), 1);
1100
+ var import_node_path14 = __toESM(require("path"), 1);
799
1101
  var import_prompts7 = require("@inquirer/prompts");
800
1102
 
801
1103
  // src/export-paperclip/translate-agents.ts
802
- var import_node_fs8 = __toESM(require("fs"), 1);
803
- var import_node_path9 = __toESM(require("path"), 1);
1104
+ var import_node_fs9 = __toESM(require("fs"), 1);
1105
+ var import_node_path10 = __toESM(require("path"), 1);
804
1106
  function slugToName(slug) {
805
1107
  return slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
806
1108
  }
807
1109
  function translateAgents(rootDir, systemDir, workspaceName, skillSlugs) {
808
- const agentsDir = import_node_path9.default.join(rootDir, systemDir, "agents");
1110
+ const agentsDir = import_node_path10.default.join(rootDir, systemDir, "agents");
809
1111
  const agents = [];
810
1112
  const workerNames = [];
811
1113
  const workerAgents = [];
812
- if (import_node_fs8.default.existsSync(agentsDir)) {
813
- for (const entry of import_node_fs8.default.readdirSync(agentsDir)) {
1114
+ if (import_node_fs9.default.existsSync(agentsDir)) {
1115
+ for (const entry of import_node_fs9.default.readdirSync(agentsDir)) {
814
1116
  if (entry === "primary-agent.md" || entry === "_template.md" || entry.startsWith(".") || !entry.endsWith(".md")) {
815
1117
  continue;
816
1118
  }
817
1119
  const slug = entry.replace(/\.md$/, "");
818
- const rawBody = import_node_fs8.default.readFileSync(import_node_path9.default.join(agentsDir, entry), "utf-8");
1120
+ const rawBody = import_node_fs9.default.readFileSync(import_node_path10.default.join(agentsDir, entry), "utf-8");
819
1121
  const roleMatch = rawBody.match(/^>\s*\*\*Purpose\*\*:\s*(.+)/m);
820
1122
  const description = roleMatch ? roleMatch[1].trim() : "";
821
1123
  const name = slugToName(slug);
@@ -925,8 +1227,8 @@ When your work is complete, hand off to the **CEO** for review routing. If a rev
925
1227
  }
926
1228
 
927
1229
  // src/export-paperclip/translate-governance.ts
928
- var import_node_fs9 = __toESM(require("fs"), 1);
929
- var import_node_path10 = __toESM(require("path"), 1);
1230
+ var import_node_fs10 = __toESM(require("fs"), 1);
1231
+ var import_node_path11 = __toESM(require("path"), 1);
930
1232
  var GOVERNANCE_TIERS = {
931
1233
  solo: {
932
1234
  tier: "light",
@@ -960,39 +1262,39 @@ function getGovernanceConfig(qualityLevel) {
960
1262
  }
961
1263
 
962
1264
  // src/export-paperclip/translate-skills.ts
963
- var import_node_fs10 = __toESM(require("fs"), 1);
964
- var import_node_path11 = __toESM(require("path"), 1);
1265
+ var import_node_fs11 = __toESM(require("fs"), 1);
1266
+ var import_node_path12 = __toESM(require("path"), 1);
965
1267
  function translateSkills(rootDir, systemDir) {
966
- const skillsDir = import_node_path11.default.join(rootDir, systemDir, "skills");
1268
+ const skillsDir = import_node_path12.default.join(rootDir, systemDir, "skills");
967
1269
  const skills = [];
968
- if (!import_node_fs10.default.existsSync(skillsDir)) return skills;
969
- for (const entry of import_node_fs10.default.readdirSync(skillsDir, { withFileTypes: true })) {
1270
+ if (!import_node_fs11.default.existsSync(skillsDir)) return skills;
1271
+ for (const entry of import_node_fs11.default.readdirSync(skillsDir, { withFileTypes: true })) {
970
1272
  if (!entry.isDirectory()) continue;
971
- const skillMdPath = import_node_path11.default.join(skillsDir, entry.name, "SKILL.md");
972
- if (!import_node_fs10.default.existsSync(skillMdPath)) continue;
1273
+ const skillMdPath = import_node_path12.default.join(skillsDir, entry.name, "SKILL.md");
1274
+ if (!import_node_fs11.default.existsSync(skillMdPath)) continue;
973
1275
  skills.push({
974
1276
  name: entry.name,
975
1277
  sourcePath: `${systemDir}/skills/${entry.name}/SKILL.md`,
976
- content: import_node_fs10.default.readFileSync(skillMdPath, "utf-8")
1278
+ content: import_node_fs11.default.readFileSync(skillMdPath, "utf-8")
977
1279
  });
978
1280
  }
979
1281
  return skills;
980
1282
  }
981
1283
 
982
1284
  // src/export-paperclip/translate-goals.ts
983
- var import_node_fs11 = __toESM(require("fs"), 1);
984
- var import_node_path12 = __toESM(require("path"), 1);
1285
+ var import_node_fs12 = __toESM(require("fs"), 1);
1286
+ var import_node_path13 = __toESM(require("path"), 1);
985
1287
  function translateGoals(rootDir) {
986
- const projectsDir = import_node_path12.default.join(rootDir, "projects");
1288
+ const projectsDir = import_node_path13.default.join(rootDir, "projects");
987
1289
  const goals = [];
988
- if (!import_node_fs11.default.existsSync(projectsDir)) return goals;
989
- for (const entry of import_node_fs11.default.readdirSync(projectsDir, { withFileTypes: true })) {
1290
+ if (!import_node_fs12.default.existsSync(projectsDir)) return goals;
1291
+ for (const entry of import_node_fs12.default.readdirSync(projectsDir, { withFileTypes: true })) {
990
1292
  if (!entry.isDirectory() || entry.name === "_template" || entry.name.startsWith(".")) {
991
1293
  continue;
992
1294
  }
993
- const readmePath = import_node_path12.default.join(projectsDir, entry.name, "README.md");
994
- if (!import_node_fs11.default.existsSync(readmePath)) continue;
995
- const content = import_node_fs11.default.readFileSync(readmePath, "utf-8");
1295
+ const readmePath = import_node_path13.default.join(projectsDir, entry.name, "README.md");
1296
+ if (!import_node_fs12.default.existsSync(readmePath)) continue;
1297
+ const content = import_node_fs12.default.readFileSync(readmePath, "utf-8");
996
1298
  const descMatch = content.match(
997
1299
  /## What This Project Is\s*\n+([\s\S]*?)(?=\n---|\n##|$)/
998
1300
  );
@@ -1020,7 +1322,7 @@ async function exportPaperclip(options) {
1020
1322
  default: `Governed AI workspace for ${String(vars.workloadLabel).toLowerCase()}`
1021
1323
  });
1022
1324
  const companySlug = toSlug(companyName);
1023
- const outDir = import_node_path13.default.resolve(
1325
+ const outDir = import_node_path14.default.resolve(
1024
1326
  options.out ?? `${config.workspaceName}-paperclip`
1025
1327
  );
1026
1328
  const skills = translateSkills(rootDir, systemDir);
@@ -1050,7 +1352,7 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1050
1352
  console.log("\nValidation passed. Run without --validate to export.\n");
1051
1353
  return;
1052
1354
  }
1053
- if (import_node_fs12.default.existsSync(outDir)) {
1355
+ if (import_node_fs13.default.existsSync(outDir)) {
1054
1356
  const proceed = await (0, import_prompts7.confirm)({
1055
1357
  message: `Output directory already exists at ${outDir}. Overwrite?`,
1056
1358
  default: false
@@ -1059,10 +1361,10 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1059
1361
  console.log("Aborted.\n");
1060
1362
  return;
1061
1363
  }
1062
- import_node_fs12.default.rmSync(outDir, { recursive: true, force: true });
1364
+ import_node_fs13.default.rmSync(outDir, { recursive: true, force: true });
1063
1365
  }
1064
1366
  console.log("\nExporting to Paperclip format (agentcompanies/v1)...\n");
1065
- import_node_fs12.default.mkdirSync(outDir, { recursive: true });
1367
+ import_node_fs13.default.mkdirSync(outDir, { recursive: true });
1066
1368
  const goalsYaml = goals.length > 0 ? goals.map((g) => ` - ${g.description.split("\n")[0]}`).join("\n") : ` - ${mission}`;
1067
1369
  const pipelineLines = agents.map((a, i) => {
1068
1370
  return `${i + 1}. **${a.name}** ${a.title.toLowerCase()}`;
@@ -1092,17 +1394,17 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1092
1394
  `Generated with [Clawstrap](https://github.com/peppinho89/clawstrap) v${CLI_VERSION}`,
1093
1395
  ""
1094
1396
  ].join("\n");
1095
- import_node_fs12.default.writeFileSync(import_node_path13.default.join(outDir, "COMPANY.md"), companyMd, "utf-8");
1397
+ import_node_fs13.default.writeFileSync(import_node_path14.default.join(outDir, "COMPANY.md"), companyMd, "utf-8");
1096
1398
  console.log(" \u2713 COMPANY.md");
1097
- import_node_fs12.default.writeFileSync(
1098
- import_node_path13.default.join(outDir, ".paperclip.yaml"),
1399
+ import_node_fs13.default.writeFileSync(
1400
+ import_node_path14.default.join(outDir, ".paperclip.yaml"),
1099
1401
  "schema: paperclip/v1\n",
1100
1402
  "utf-8"
1101
1403
  );
1102
1404
  console.log(" \u2713 .paperclip.yaml");
1103
1405
  for (const agent of agents) {
1104
- const agentDir = import_node_path13.default.join(outDir, "agents", agent.slug);
1105
- import_node_fs12.default.mkdirSync(agentDir, { recursive: true });
1406
+ const agentDir = import_node_path14.default.join(outDir, "agents", agent.slug);
1407
+ import_node_fs13.default.mkdirSync(agentDir, { recursive: true });
1106
1408
  const frontmatterLines = [
1107
1409
  "---",
1108
1410
  `name: ${agent.name}`,
@@ -1117,12 +1419,12 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1117
1419
  }
1118
1420
  frontmatterLines.push("---");
1119
1421
  const agentMd = frontmatterLines.join("\n") + "\n\n" + agent.body + "\n";
1120
- import_node_fs12.default.writeFileSync(import_node_path13.default.join(agentDir, "AGENTS.md"), agentMd, "utf-8");
1422
+ import_node_fs13.default.writeFileSync(import_node_path14.default.join(agentDir, "AGENTS.md"), agentMd, "utf-8");
1121
1423
  console.log(` \u2713 agents/${agent.slug}/AGENTS.md`);
1122
1424
  }
1123
1425
  if (nonCeoAgents.length > 0) {
1124
- const teamDir = import_node_path13.default.join(outDir, "teams", "engineering");
1125
- import_node_fs12.default.mkdirSync(teamDir, { recursive: true });
1426
+ const teamDir = import_node_path14.default.join(outDir, "teams", "engineering");
1427
+ import_node_fs13.default.mkdirSync(teamDir, { recursive: true });
1126
1428
  const includesList = nonCeoAgents.map((a) => ` - ../../agents/${a.slug}/AGENTS.md`).join("\n");
1127
1429
  const teamMd = [
1128
1430
  "---",
@@ -1139,13 +1441,13 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1139
1441
  `The engineering team at ${companyName}. Led by the CEO, who scopes and delegates work to specialists.`,
1140
1442
  ""
1141
1443
  ].join("\n");
1142
- import_node_fs12.default.writeFileSync(import_node_path13.default.join(teamDir, "TEAM.md"), teamMd, "utf-8");
1444
+ import_node_fs13.default.writeFileSync(import_node_path14.default.join(teamDir, "TEAM.md"), teamMd, "utf-8");
1143
1445
  console.log(" \u2713 teams/engineering/TEAM.md");
1144
1446
  }
1145
1447
  for (const skill of skills) {
1146
- const skillDir = import_node_path13.default.join(outDir, "skills", skill.name);
1147
- import_node_fs12.default.mkdirSync(skillDir, { recursive: true });
1148
- import_node_fs12.default.writeFileSync(import_node_path13.default.join(skillDir, "SKILL.md"), skill.content, "utf-8");
1448
+ const skillDir = import_node_path14.default.join(outDir, "skills", skill.name);
1449
+ import_node_fs13.default.mkdirSync(skillDir, { recursive: true });
1450
+ import_node_fs13.default.writeFileSync(import_node_path14.default.join(skillDir, "SKILL.md"), skill.content, "utf-8");
1149
1451
  console.log(` \u2713 skills/${skill.name}/SKILL.md`);
1150
1452
  }
1151
1453
  const importScript = [
@@ -1161,26 +1463,26 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1161
1463
  'echo "Done. Open your Paperclip dashboard to review."',
1162
1464
  ""
1163
1465
  ].join("\n");
1164
- const importPath = import_node_path13.default.join(outDir, "import.sh");
1165
- import_node_fs12.default.writeFileSync(importPath, importScript, "utf-8");
1166
- import_node_fs12.default.chmodSync(importPath, 493);
1466
+ const importPath = import_node_path14.default.join(outDir, "import.sh");
1467
+ import_node_fs13.default.writeFileSync(importPath, importScript, "utf-8");
1468
+ import_node_fs13.default.chmodSync(importPath, 493);
1167
1469
  console.log(" \u2713 import.sh");
1168
1470
  const updatedConfig = {
1169
1471
  ...config,
1170
1472
  lastExport: {
1171
1473
  format: "paperclip",
1172
1474
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1173
- outputDir: import_node_path13.default.relative(rootDir, outDir) || outDir
1475
+ outputDir: import_node_path14.default.relative(rootDir, outDir) || outDir
1174
1476
  }
1175
1477
  };
1176
- import_node_fs12.default.writeFileSync(
1177
- import_node_path13.default.join(rootDir, ".clawstrap.json"),
1478
+ import_node_fs13.default.writeFileSync(
1479
+ import_node_path14.default.join(rootDir, ".clawstrap.json"),
1178
1480
  JSON.stringify(updatedConfig, null, 2) + "\n",
1179
1481
  "utf-8"
1180
1482
  );
1181
1483
  console.log(
1182
1484
  `
1183
- Exported to ${import_node_path13.default.relative(process.cwd(), outDir) || outDir}`
1485
+ Exported to ${import_node_path14.default.relative(process.cwd(), outDir) || outDir}`
1184
1486
  );
1185
1487
  console.log(
1186
1488
  `${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
@@ -1189,10 +1491,738 @@ Exported to ${import_node_path13.default.relative(process.cwd(), outDir) || outD
1189
1491
  `);
1190
1492
  }
1191
1493
 
1494
+ // src/watch.ts
1495
+ var import_node_path20 = __toESM(require("path"), 1);
1496
+ var import_node_child_process4 = require("child_process");
1497
+ var import_node_fs19 = __toESM(require("fs"), 1);
1498
+
1499
+ // src/watch/git.ts
1500
+ var import_node_child_process = require("child_process");
1501
+ var import_node_fs15 = __toESM(require("fs"), 1);
1502
+ var import_node_path16 = __toESM(require("path"), 1);
1503
+ init_writers();
1504
+ var STOPWORDS = /* @__PURE__ */ new Set([
1505
+ "fix",
1506
+ "add",
1507
+ "update",
1508
+ "remove",
1509
+ "feat",
1510
+ "chore",
1511
+ "the",
1512
+ "a",
1513
+ "an",
1514
+ "and",
1515
+ "or",
1516
+ "in",
1517
+ "on",
1518
+ "at",
1519
+ "to",
1520
+ "for",
1521
+ "of",
1522
+ "is",
1523
+ "was",
1524
+ "be",
1525
+ "it",
1526
+ "as",
1527
+ "with",
1528
+ "by",
1529
+ "this",
1530
+ "that"
1531
+ ]);
1532
+ function parseGitLog(output) {
1533
+ const entries = [];
1534
+ const lines = output.split("\n");
1535
+ let current = null;
1536
+ for (const line of lines) {
1537
+ if (line.includes("|||")) {
1538
+ if (current) entries.push(current);
1539
+ const parts = line.split("|||");
1540
+ current = {
1541
+ sha: parts[0]?.trim() ?? "",
1542
+ subject: parts[1]?.trim() ?? "",
1543
+ author: parts[2]?.trim() ?? "",
1544
+ date: parts[3]?.trim() ?? "",
1545
+ files: []
1546
+ };
1547
+ } else if (line.trim() && current) {
1548
+ current.files.push(line.trim());
1549
+ }
1550
+ }
1551
+ if (current) entries.push(current);
1552
+ return entries.filter((e) => e.sha.length > 0);
1553
+ }
1554
+ function getTopDirs(entries) {
1555
+ const dirCount = /* @__PURE__ */ new Map();
1556
+ for (const entry of entries) {
1557
+ const seenDirs = /* @__PURE__ */ new Set();
1558
+ for (const file of entry.files) {
1559
+ const dir = import_node_path16.default.dirname(file);
1560
+ if (dir !== "." && !seenDirs.has(dir)) {
1561
+ seenDirs.add(dir);
1562
+ dirCount.set(dir, (dirCount.get(dir) ?? 0) + 1);
1563
+ }
1564
+ }
1565
+ }
1566
+ return Array.from(dirCount.entries()).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([dir]) => dir);
1567
+ }
1568
+ function getCoChangingFiles(entries, minCommits = 3) {
1569
+ const pairCount = /* @__PURE__ */ new Map();
1570
+ for (const entry of entries) {
1571
+ const files = entry.files.slice().sort();
1572
+ for (let i = 0; i < files.length; i++) {
1573
+ for (let j = i + 1; j < files.length; j++) {
1574
+ const key = `${files[i]}|||${files[j]}`;
1575
+ pairCount.set(key, (pairCount.get(key) ?? 0) + 1);
1576
+ }
1577
+ }
1578
+ }
1579
+ return Array.from(pairCount.entries()).filter(([, count]) => count >= minCommits).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([key]) => key.split("|||"));
1580
+ }
1581
+ function getRecurringWords(entries) {
1582
+ const wordCount = /* @__PURE__ */ new Map();
1583
+ for (const entry of entries) {
1584
+ const words = entry.subject.toLowerCase().split(/\s+/).map((w) => w.replace(/[^a-z0-9]/g, "")).filter((w) => w.length > 2 && !STOPWORDS.has(w));
1585
+ for (const word of new Set(words)) {
1586
+ wordCount.set(word, (wordCount.get(word) ?? 0) + 1);
1587
+ }
1588
+ }
1589
+ return Array.from(wordCount.entries()).filter(([, count]) => count >= 2).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([word]) => word);
1590
+ }
1591
+ async function runGitObserver(rootDir, sinceCommit) {
1592
+ if (!import_node_fs15.default.existsSync(import_node_path16.default.join(rootDir, ".git"))) {
1593
+ return null;
1594
+ }
1595
+ let headSha;
1596
+ try {
1597
+ headSha = (0, import_node_child_process.execSync)(`git -C "${rootDir}" rev-parse HEAD`, {
1598
+ encoding: "utf-8"
1599
+ }).trim();
1600
+ } catch {
1601
+ return null;
1602
+ }
1603
+ if (!headSha) return null;
1604
+ if (sinceCommit && sinceCommit === headSha) {
1605
+ return { lastCommit: headSha, entriesWritten: 0 };
1606
+ }
1607
+ let logOutput;
1608
+ try {
1609
+ const rangeArg = sinceCommit ? `${sinceCommit}..HEAD` : "";
1610
+ const cmd = `git -C "${rootDir}" log ${rangeArg} --pretty=format:"%H|||%s|||%ae|||%ad" --date=short --name-only`;
1611
+ logOutput = (0, import_node_child_process.execSync)(cmd, { encoding: "utf-8" }).trim();
1612
+ } catch {
1613
+ return null;
1614
+ }
1615
+ if (!logOutput) {
1616
+ return { lastCommit: headSha, entriesWritten: 0 };
1617
+ }
1618
+ const entries = parseGitLog(logOutput);
1619
+ if (entries.length === 0) {
1620
+ return { lastCommit: headSha, entriesWritten: 0 };
1621
+ }
1622
+ const memoryEntries = [];
1623
+ const coChangingPairs = getCoChangingFiles(entries, 3);
1624
+ if (coChangingPairs.length > 0) {
1625
+ const pairs = coChangingPairs.map(([a, b]) => `${a} <-> ${b}`).join(", ");
1626
+ memoryEntries.push(`Co-changing file pairs (frequently modified together): ${pairs}`);
1627
+ }
1628
+ const topDirs = getTopDirs(entries);
1629
+ if (topDirs.length > 0) {
1630
+ memoryEntries.push(`Top high-churn directories (most commits): ${topDirs.join(", ")}`);
1631
+ }
1632
+ const recurringWords = getRecurringWords(entries);
1633
+ if (recurringWords.length > 0) {
1634
+ memoryEntries.push(`Recurring themes in recent commits: ${recurringWords.join(", ")}`);
1635
+ }
1636
+ const dateRange = entries.length > 0 ? `from ${entries[entries.length - 1]?.date ?? "?"} to ${entries[0]?.date ?? "?"}` : "";
1637
+ memoryEntries.push(
1638
+ `Git history analyzed: ${entries.length} commit(s) ${dateRange}. Authors: ${[...new Set(entries.map((e) => e.author))].join(", ")}`
1639
+ );
1640
+ if (memoryEntries.length > 0) {
1641
+ appendToMemory(rootDir, memoryEntries, "git");
1642
+ }
1643
+ return { lastCommit: headSha, entriesWritten: memoryEntries.length };
1644
+ }
1645
+
1646
+ // src/watch/scan.ts
1647
+ var import_node_fs16 = __toESM(require("fs"), 1);
1648
+ var import_node_path17 = __toESM(require("path"), 1);
1649
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "tmp", "dist", ".claude"]);
1650
+ var CODE_EXTS = /* @__PURE__ */ new Set([".ts", ".js", ".tsx", ".jsx"]);
1651
+ function walkDir(dir, maxDepth = 10, depth = 0) {
1652
+ if (depth > maxDepth) return [];
1653
+ let results = [];
1654
+ let entries;
1655
+ try {
1656
+ entries = import_node_fs16.default.readdirSync(dir, { withFileTypes: true });
1657
+ } catch {
1658
+ return [];
1659
+ }
1660
+ for (const entry of entries) {
1661
+ if (SKIP_DIRS.has(entry.name)) continue;
1662
+ const fullPath = import_node_path17.default.join(dir, entry.name);
1663
+ if (entry.isDirectory()) {
1664
+ results = results.concat(walkDir(fullPath, maxDepth, depth + 1));
1665
+ } else if (entry.isFile()) {
1666
+ results.push(fullPath);
1667
+ }
1668
+ }
1669
+ return results;
1670
+ }
1671
+ function detectNamingCase(name) {
1672
+ if (/^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(name)) return "kebab-case";
1673
+ if (/^[A-Z][A-Z0-9_]*$/.test(name)) return "other";
1674
+ if (/^[A-Z][a-zA-Z0-9]+$/.test(name)) return "PascalCase";
1675
+ if (/^[a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*$/.test(name)) return "camelCase";
1676
+ if (/^[a-z][a-z0-9]*(_[a-z0-9]+)+$/.test(name)) return "snake_case";
1677
+ return "other";
1678
+ }
1679
+ function analyzeNaming(files) {
1680
+ const counts = {
1681
+ "kebab-case": 0,
1682
+ camelCase: 0,
1683
+ snake_case: 0,
1684
+ PascalCase: 0,
1685
+ other: 0
1686
+ };
1687
+ const examples = {
1688
+ "kebab-case": [],
1689
+ camelCase: [],
1690
+ snake_case: [],
1691
+ PascalCase: [],
1692
+ other: []
1693
+ };
1694
+ for (const file of files) {
1695
+ const base = import_node_path17.default.basename(file, import_node_path17.default.extname(file));
1696
+ const style2 = detectNamingCase(base);
1697
+ counts[style2]++;
1698
+ if (examples[style2].length < 3) examples[style2].push(base);
1699
+ }
1700
+ const dominant = Object.entries(counts).sort((a, b) => b[1] - a[1]).filter(([key]) => key !== "other")[0];
1701
+ if (!dominant || dominant[1] === 0) {
1702
+ return ["No dominant file naming convention detected."];
1703
+ }
1704
+ const [style, count] = dominant;
1705
+ const exampleList = examples[style].slice(0, 3).join(", ");
1706
+ const result = [`Dominant file naming: ${style} (${count} files). Examples: ${exampleList}`];
1707
+ const total = Object.values(counts).reduce((a, b) => a + b, 0) - counts.other;
1708
+ if (total > 0) {
1709
+ const pct = Math.round(count / total * 100);
1710
+ result.push(`${style} used in ${pct}% of named files.`);
1711
+ }
1712
+ return result;
1713
+ }
1714
+ function analyzeImports(files) {
1715
+ const sample = files.filter((f) => CODE_EXTS.has(import_node_path17.default.extname(f))).slice(0, 20);
1716
+ let relativeCount = 0;
1717
+ let absoluteCount = 0;
1718
+ let barrelCount = 0;
1719
+ for (const file of sample) {
1720
+ let content;
1721
+ try {
1722
+ content = import_node_fs16.default.readFileSync(file, "utf-8");
1723
+ } catch {
1724
+ continue;
1725
+ }
1726
+ const importLines = content.split("\n").filter((l) => /^import\s/.test(l));
1727
+ for (const line of importLines) {
1728
+ if (/from\s+['"]\.\.?\//.test(line)) relativeCount++;
1729
+ else if (/from\s+['"]/.test(line)) absoluteCount++;
1730
+ }
1731
+ const base = import_node_path17.default.basename(file, import_node_path17.default.extname(file));
1732
+ if (base === "index") barrelCount++;
1733
+ }
1734
+ const results = [];
1735
+ const total = relativeCount + absoluteCount;
1736
+ if (total > 0) {
1737
+ const relPct = Math.round(relativeCount / total * 100);
1738
+ results.push(
1739
+ `Import style: ${relPct}% relative (./), ${100 - relPct}% absolute/alias. Analyzed ${sample.length} files.`
1740
+ );
1741
+ } else {
1742
+ results.push("No import statements found in sampled files.");
1743
+ }
1744
+ if (barrelCount > 0) {
1745
+ results.push(`Barrel exports detected: ${barrelCount} index file(s) found.`);
1746
+ }
1747
+ return results;
1748
+ }
1749
+ function analyzeTesting(files) {
1750
+ const testPatterns = [];
1751
+ let hasTestExt = false;
1752
+ let hasSpecExt = false;
1753
+ let hasTestsDir = false;
1754
+ for (const file of files) {
1755
+ const base = import_node_path17.default.basename(file);
1756
+ if (/\.test\.(ts|js|tsx|jsx)$/.test(base)) hasTestExt = true;
1757
+ if (/\.spec\.(ts|js|tsx|jsx)$/.test(base)) hasSpecExt = true;
1758
+ if (file.includes("/__tests__/") || file.includes("\\__tests__\\")) hasTestsDir = true;
1759
+ }
1760
+ if (hasTestExt) testPatterns.push("*.test.ts/js");
1761
+ if (hasSpecExt) testPatterns.push("*.spec.ts/js");
1762
+ if (hasTestsDir) testPatterns.push("__tests__/ directories");
1763
+ if (testPatterns.length === 0) {
1764
+ return ["No test files detected."];
1765
+ }
1766
+ return [`Test patterns found: ${testPatterns.join(", ")}`];
1767
+ }
1768
+ function analyzeErrorHandling(files) {
1769
+ const sample = files.filter((f) => CODE_EXTS.has(import_node_path17.default.extname(f))).slice(0, 20);
1770
+ let tryCatchCount = 0;
1771
+ let resultTypeCount = 0;
1772
+ for (const file of sample) {
1773
+ let content;
1774
+ try {
1775
+ content = import_node_fs16.default.readFileSync(file, "utf-8");
1776
+ } catch {
1777
+ continue;
1778
+ }
1779
+ const tryCatches = (content.match(/\btry\s*\{/g) ?? []).length;
1780
+ const resultTypes = (content.match(/Result<|Either</g) ?? []).length;
1781
+ tryCatchCount += tryCatches;
1782
+ resultTypeCount += resultTypes;
1783
+ }
1784
+ const results = [];
1785
+ if (tryCatchCount > 0 && resultTypeCount === 0) {
1786
+ results.push(`Error handling: try/catch dominant (${tryCatchCount} occurrences in ${sample.length} files).`);
1787
+ } else if (resultTypeCount > 0 && tryCatchCount === 0) {
1788
+ results.push(`Error handling: Result/Either type pattern dominant (${resultTypeCount} occurrences).`);
1789
+ } else if (tryCatchCount > 0 && resultTypeCount > 0) {
1790
+ const dominant = tryCatchCount >= resultTypeCount ? "try/catch" : "Result/Either";
1791
+ results.push(
1792
+ `Error handling: mixed \u2014 ${tryCatchCount} try/catch and ${resultTypeCount} Result/Either. Dominant: ${dominant}.`
1793
+ );
1794
+ } else {
1795
+ results.push(`No explicit error handling patterns detected in ${sample.length} sampled files.`);
1796
+ }
1797
+ return results;
1798
+ }
1799
+ function analyzeComments(files) {
1800
+ const sample = files.filter((f) => CODE_EXTS.has(import_node_path17.default.extname(f))).slice(0, 20);
1801
+ let jsdocCount = 0;
1802
+ let inlineCount = 0;
1803
+ let totalLines = 0;
1804
+ for (const file of sample) {
1805
+ let content;
1806
+ try {
1807
+ content = import_node_fs16.default.readFileSync(file, "utf-8");
1808
+ } catch {
1809
+ continue;
1810
+ }
1811
+ const lines = content.split("\n");
1812
+ totalLines += lines.length;
1813
+ jsdocCount += (content.match(/\/\*\*/g) ?? []).length;
1814
+ inlineCount += lines.filter((l) => /^\s*\/\//.test(l)).length;
1815
+ }
1816
+ const commentDensity = totalLines > 0 ? (jsdocCount + inlineCount) / totalLines : 0;
1817
+ let density;
1818
+ if (commentDensity > 0.15) density = "heavy";
1819
+ else if (commentDensity > 0.05) density = "moderate";
1820
+ else density = "minimal";
1821
+ return [
1822
+ `Comment density: ${density} (${jsdocCount} JSDoc blocks, ${inlineCount} inline comments across ${sample.length} files).`
1823
+ ];
1824
+ }
1825
+ async function runScan(rootDir) {
1826
+ const allFiles = walkDir(rootDir);
1827
+ return {
1828
+ naming: analyzeNaming(allFiles),
1829
+ imports: analyzeImports(allFiles),
1830
+ testing: analyzeTesting(allFiles),
1831
+ errorHandling: analyzeErrorHandling(allFiles),
1832
+ comments: analyzeComments(allFiles)
1833
+ };
1834
+ }
1835
+
1836
+ // src/watch.ts
1837
+ init_writers();
1838
+
1839
+ // src/watch/daemon.ts
1840
+ var import_node_fs18 = __toESM(require("fs"), 1);
1841
+ var import_node_path19 = __toESM(require("path"), 1);
1842
+ init_writers();
1843
+
1844
+ // src/watch/transcripts.ts
1845
+ var import_node_fs17 = __toESM(require("fs"), 1);
1846
+ var import_node_path18 = __toESM(require("path"), 1);
1847
+ async function processTranscript(filePath, adapter) {
1848
+ let content;
1849
+ try {
1850
+ content = import_node_fs17.default.readFileSync(filePath, "utf-8");
1851
+ } catch {
1852
+ return null;
1853
+ }
1854
+ const prompt = `Extract structured information from this session summary. Return ONLY valid JSON with no markdown or explanation.
1855
+
1856
+ Session content:
1857
+ ${content}
1858
+
1859
+ Return JSON with exactly these keys:
1860
+ {
1861
+ "decisions": ["what approach was chosen and why"],
1862
+ "corrections": ["what the agent got wrong and how it was fixed"],
1863
+ "deferredIdeas": ["ideas mentioned but not acted on"],
1864
+ "openThreads": ["unresolved questions or next steps"]
1865
+ }
1866
+ Each item must be a concise one-sentence string. Arrays may be empty.`;
1867
+ let response;
1868
+ try {
1869
+ response = await adapter.complete(prompt);
1870
+ } catch {
1871
+ return null;
1872
+ }
1873
+ try {
1874
+ const cleaned = response.replace(/^```(?:json)?\s*/m, "").replace(/\s*```\s*$/m, "").trim();
1875
+ const parsed = JSON.parse(cleaned);
1876
+ if (typeof parsed !== "object" || parsed === null) return null;
1877
+ const obj = parsed;
1878
+ const toArray = (v) => {
1879
+ if (!Array.isArray(v)) return [];
1880
+ return v.filter((item) => typeof item === "string");
1881
+ };
1882
+ return {
1883
+ decisions: toArray(obj["decisions"]),
1884
+ corrections: toArray(obj["corrections"]),
1885
+ deferredIdeas: toArray(obj["deferredIdeas"]),
1886
+ openThreads: toArray(obj["openThreads"])
1887
+ };
1888
+ } catch {
1889
+ return null;
1890
+ }
1891
+ }
1892
+ function watchTranscriptDir(rootDir, onNewFile) {
1893
+ const sessionsDir = import_node_path18.default.join(rootDir, "tmp", "sessions");
1894
+ import_node_fs17.default.mkdirSync(sessionsDir, { recursive: true });
1895
+ const watcher = import_node_fs17.default.watch(sessionsDir, (event, filename) => {
1896
+ if (event !== "rename" || !filename) return;
1897
+ if (!filename.endsWith(".md")) return;
1898
+ const filePath = import_node_path18.default.join(sessionsDir, filename);
1899
+ if (!import_node_fs17.default.existsSync(filePath)) return;
1900
+ onNewFile(filePath).catch(() => {
1901
+ });
1902
+ });
1903
+ return () => {
1904
+ watcher.close();
1905
+ };
1906
+ }
1907
+
1908
+ // src/watch/adapters/claude-local.ts
1909
+ var import_node_child_process2 = require("child_process");
1910
+ var ClaudeLocalAdapter = class {
1911
+ async complete(prompt) {
1912
+ const escaped = prompt.replace(/'/g, "'\\''");
1913
+ try {
1914
+ const result = (0, import_node_child_process2.execSync)(`claude -p '${escaped}'`, {
1915
+ encoding: "utf-8",
1916
+ timeout: 6e4,
1917
+ stdio: ["pipe", "pipe", "pipe"]
1918
+ });
1919
+ return result.trim();
1920
+ } catch (err) {
1921
+ const msg = err instanceof Error ? err.message : String(err);
1922
+ if (msg.includes("ENOENT")) {
1923
+ throw new Error("Claude Code CLI not found. Install it or use a different adapter.");
1924
+ }
1925
+ throw err;
1926
+ }
1927
+ }
1928
+ };
1929
+
1930
+ // src/watch/adapters/claude-api.ts
1931
+ var API_URL = "https://api.anthropic.com/v1/messages";
1932
+ var DEFAULT_MODEL = "claude-haiku-4-5-20251001";
1933
+ var ClaudeApiAdapter = class {
1934
+ apiKey;
1935
+ model;
1936
+ constructor(apiKey, model) {
1937
+ this.apiKey = apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
1938
+ this.model = model ?? DEFAULT_MODEL;
1939
+ if (!this.apiKey) {
1940
+ throw new Error("ANTHROPIC_API_KEY not set. Set it or use a different adapter.");
1941
+ }
1942
+ }
1943
+ async complete(prompt) {
1944
+ const response = await fetch(API_URL, {
1945
+ method: "POST",
1946
+ headers: {
1947
+ "x-api-key": this.apiKey,
1948
+ "anthropic-version": "2023-06-01",
1949
+ "content-type": "application/json"
1950
+ },
1951
+ body: JSON.stringify({
1952
+ model: this.model,
1953
+ max_tokens: 1024,
1954
+ messages: [{ role: "user", content: prompt }]
1955
+ })
1956
+ });
1957
+ if (!response.ok) {
1958
+ throw new Error(`Anthropic API error: ${response.status} ${response.statusText}`);
1959
+ }
1960
+ const data = await response.json();
1961
+ const text = data.content.find((c) => c.type === "text")?.text ?? "";
1962
+ return text.trim();
1963
+ }
1964
+ };
1965
+
1966
+ // src/watch/adapters/ollama.ts
1967
+ var DEFAULT_OLLAMA_URL = "http://localhost:11434/api/generate";
1968
+ var DEFAULT_MODEL2 = "llama3.2";
1969
+ var OllamaAdapter = class {
1970
+ model;
1971
+ url;
1972
+ constructor(model, url) {
1973
+ this.model = model ?? DEFAULT_MODEL2;
1974
+ this.url = url ?? DEFAULT_OLLAMA_URL;
1975
+ }
1976
+ async complete(prompt) {
1977
+ const response = await fetch(this.url, {
1978
+ method: "POST",
1979
+ headers: { "content-type": "application/json" },
1980
+ body: JSON.stringify({
1981
+ model: this.model,
1982
+ prompt,
1983
+ stream: false
1984
+ })
1985
+ });
1986
+ if (!response.ok) {
1987
+ throw new Error(`Ollama error: ${response.status}. Is Ollama running?`);
1988
+ }
1989
+ const data = await response.json();
1990
+ return data.response.trim();
1991
+ }
1992
+ };
1993
+
1994
+ // src/watch/adapters/codex-local.ts
1995
+ var import_node_child_process3 = require("child_process");
1996
+ var CodexLocalAdapter = class {
1997
+ async complete(prompt) {
1998
+ const escaped = prompt.replace(/'/g, "'\\''");
1999
+ try {
2000
+ const result = (0, import_node_child_process3.execSync)(`codex '${escaped}'`, {
2001
+ encoding: "utf-8",
2002
+ timeout: 6e4,
2003
+ stdio: ["pipe", "pipe", "pipe"]
2004
+ });
2005
+ return result.trim();
2006
+ } catch (err) {
2007
+ const msg = err instanceof Error ? err.message : String(err);
2008
+ if (msg.includes("ENOENT")) {
2009
+ throw new Error("Codex CLI not found. Install it or use a different adapter.");
2010
+ }
2011
+ throw err;
2012
+ }
2013
+ }
2014
+ };
2015
+
2016
+ // src/watch/adapters/index.ts
2017
+ function createAdapter(config) {
2018
+ const type = config.watch?.adapter ?? "claude-local";
2019
+ switch (type) {
2020
+ case "claude-local":
2021
+ return new ClaudeLocalAdapter();
2022
+ case "claude-api":
2023
+ return new ClaudeApiAdapter();
2024
+ case "ollama":
2025
+ return new OllamaAdapter();
2026
+ case "codex-local":
2027
+ return new CodexLocalAdapter();
2028
+ default:
2029
+ return new ClaudeLocalAdapter();
2030
+ }
2031
+ }
2032
+
2033
+ // src/watch/daemon.ts
2034
+ async function runDaemon(rootDir, config) {
2035
+ const silent = config.watch?.silent ?? false;
2036
+ const log = silent ? () => {
2037
+ } : (msg) => process.stdout.write(msg + "\n");
2038
+ const cleanup = [];
2039
+ const shutdown = () => {
2040
+ cleanup.forEach((fn) => fn());
2041
+ clearPid(rootDir);
2042
+ process.exit(0);
2043
+ };
2044
+ process.on("SIGTERM", shutdown);
2045
+ process.on("SIGINT", shutdown);
2046
+ log("[clawstrap watch] daemon started");
2047
+ const sinceCommit = config.watchState?.lastGitCommit ?? null;
2048
+ const gitResult = await runGitObserver(rootDir, sinceCommit);
2049
+ if (gitResult) {
2050
+ updateWatchState(rootDir, { lastGitCommit: gitResult.lastCommit });
2051
+ log(`[clawstrap watch] git: ${gitResult.entriesWritten} entries written`);
2052
+ }
2053
+ const adapter = createAdapter(config);
2054
+ const stopTranscripts = watchTranscriptDir(rootDir, async (filePath) => {
2055
+ log(`[clawstrap watch] transcript: processing ${import_node_path19.default.basename(filePath)}`);
2056
+ const result = await processTranscript(filePath, adapter);
2057
+ if (result) {
2058
+ const { appendToMemory: appendToMemory2, appendToGotchaLog: appendToGotchaLog2, appendToFutureConsiderations: appendToFutureConsiderations2 } = await Promise.resolve().then(() => (init_writers(), writers_exports));
2059
+ if (result.decisions.length) appendToMemory2(rootDir, result.decisions, "session");
2060
+ if (result.corrections.length) appendToGotchaLog2(rootDir, result.corrections);
2061
+ if (result.deferredIdeas.length) appendToFutureConsiderations2(rootDir, result.deferredIdeas);
2062
+ updateWatchState(rootDir, { lastTranscriptAt: (/* @__PURE__ */ new Date()).toISOString() });
2063
+ log(
2064
+ `[clawstrap watch] transcript: decisions=${result.decisions.length} corrections=${result.corrections.length}`
2065
+ );
2066
+ }
2067
+ });
2068
+ cleanup.push(stopTranscripts);
2069
+ const intervalDays = config.watch?.scan?.intervalDays ?? 7;
2070
+ const lastScan = config.watchState?.lastScanAt ? new Date(config.watchState.lastScanAt) : null;
2071
+ const msSinceLastScan = lastScan ? Date.now() - lastScan.getTime() : Infinity;
2072
+ const scanIntervalMs = intervalDays * 24 * 60 * 60 * 1e3;
2073
+ const doScan = async () => {
2074
+ log("[clawstrap watch] scan: running convention scan...");
2075
+ const sections = await runScan(rootDir);
2076
+ writeConventions(rootDir, sections);
2077
+ updateWatchState(rootDir, { lastScanAt: (/* @__PURE__ */ new Date()).toISOString() });
2078
+ log("[clawstrap watch] scan: conventions.md updated");
2079
+ };
2080
+ if (msSinceLastScan >= scanIntervalMs) {
2081
+ await doScan();
2082
+ }
2083
+ const scanTimer = setInterval(doScan, scanIntervalMs);
2084
+ cleanup.push(() => clearInterval(scanTimer));
2085
+ log("[clawstrap watch] watching for changes...");
2086
+ await new Promise(() => {
2087
+ });
2088
+ }
2089
+ function updateWatchState(rootDir, updates) {
2090
+ const configPath = import_node_path19.default.join(rootDir, ".clawstrap.json");
2091
+ try {
2092
+ const raw = JSON.parse(import_node_fs18.default.readFileSync(configPath, "utf-8"));
2093
+ raw["watchState"] = { ...raw["watchState"] ?? {}, ...updates };
2094
+ import_node_fs18.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2095
+ } catch {
2096
+ }
2097
+ }
2098
+
2099
+ // src/watch.ts
2100
+ async function watch(options) {
2101
+ const { config, rootDir } = loadWorkspace();
2102
+ if (options._daemon) {
2103
+ await runDaemon(rootDir, config);
2104
+ return;
2105
+ }
2106
+ if (options.stop) {
2107
+ const pid = readPid(rootDir);
2108
+ if (!pid || !isDaemonRunning(rootDir)) {
2109
+ console.log("\nNo daemon running.\n");
2110
+ return;
2111
+ }
2112
+ process.kill(pid, "SIGTERM");
2113
+ clearPid(rootDir);
2114
+ console.log(`
2115
+ Daemon stopped (pid ${pid}).
2116
+ `);
2117
+ return;
2118
+ }
2119
+ if (options.once) {
2120
+ console.log("\nRunning all observers once...\n");
2121
+ const gitResult = await runGitObserver(rootDir, config.watchState?.lastGitCommit ?? null);
2122
+ if (gitResult) {
2123
+ persistWatchState(rootDir, { lastGitCommit: gitResult.lastCommit });
2124
+ console.log(` \u2713 git: ${gitResult.entriesWritten} entries`);
2125
+ }
2126
+ const sections = await runScan(rootDir);
2127
+ writeConventions(rootDir, sections);
2128
+ persistWatchState(rootDir, { lastScanAt: (/* @__PURE__ */ new Date()).toISOString() });
2129
+ console.log(" \u2713 scan: conventions.md updated");
2130
+ console.log("\nDone.\n");
2131
+ return;
2132
+ }
2133
+ if (isDaemonRunning(rootDir)) {
2134
+ const pid = readPid(rootDir);
2135
+ console.log(`
2136
+ Daemon already running (pid ${pid}). Use --stop to stop it.
2137
+ `);
2138
+ return;
2139
+ }
2140
+ injectWatchHook(rootDir, config);
2141
+ const self = process.argv[1];
2142
+ const child = (0, import_node_child_process4.spawn)(process.execPath, [self, "watch", "--_daemon"], {
2143
+ detached: true,
2144
+ stdio: "ignore",
2145
+ cwd: rootDir
2146
+ });
2147
+ child.unref();
2148
+ if (child.pid) {
2149
+ writePid(rootDir, child.pid);
2150
+ if (!options.silent) {
2151
+ console.log(`
2152
+ Daemon started (pid ${child.pid}).`);
2153
+ console.log(`Run 'clawstrap watch --stop' to stop it.
2154
+ `);
2155
+ }
2156
+ } else {
2157
+ console.error("\nFailed to start daemon.\n");
2158
+ process.exit(1);
2159
+ }
2160
+ }
2161
+ function persistWatchState(rootDir, updates) {
2162
+ const configPath = import_node_path20.default.join(rootDir, ".clawstrap.json");
2163
+ try {
2164
+ const raw = JSON.parse(import_node_fs19.default.readFileSync(configPath, "utf-8"));
2165
+ raw.watchState = { ...raw.watchState ?? {}, ...updates };
2166
+ import_node_fs19.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2167
+ } catch {
2168
+ }
2169
+ }
2170
+ function injectWatchHook(rootDir, config) {
2171
+ const governanceFile = import_node_path20.default.join(rootDir, "CLAUDE.md");
2172
+ if (!import_node_fs19.default.existsSync(governanceFile)) return;
2173
+ const content = import_node_fs19.default.readFileSync(governanceFile, "utf-8");
2174
+ if (content.includes("<!-- CLAWSTRAP:WATCH -->")) return;
2175
+ const _config = config;
2176
+ void _config;
2177
+ const hook = `
2178
+ <!-- CLAWSTRAP:WATCH -->
2179
+ ## Session Watch Hook
2180
+
2181
+ \`clawstrap watch\` is active. At every session end, write a session summary to
2182
+ \`tmp/sessions/YYYY-MM-DD-HHmm.md\` using this format:
2183
+
2184
+ \`\`\`
2185
+ ## Decisions
2186
+ - [what approach was chosen and why]
2187
+
2188
+ ## Corrections
2189
+ - [what the agent got wrong and how it was fixed]
2190
+
2191
+ ## Deferred Ideas
2192
+ - [mentioned but not acted on]
2193
+
2194
+ ## Open Threads
2195
+ - [unresolved questions or next steps]
2196
+ \`\`\`
2197
+
2198
+ The watch daemon picks this up automatically and updates MEMORY.md and gotcha-log.md.
2199
+ `;
2200
+ import_node_fs19.default.appendFileSync(governanceFile, hook, "utf-8");
2201
+ }
2202
+
2203
+ // src/analyze.ts
2204
+ var import_node_fs20 = __toESM(require("fs"), 1);
2205
+ var import_node_path21 = __toESM(require("path"), 1);
2206
+ init_writers();
2207
+ async function analyze() {
2208
+ const { rootDir } = loadWorkspace();
2209
+ console.log("\nScanning codebase conventions...\n");
2210
+ const sections = await runScan(rootDir);
2211
+ writeConventions(rootDir, sections);
2212
+ const configPath = import_node_path21.default.join(rootDir, ".clawstrap.json");
2213
+ try {
2214
+ const raw = JSON.parse(import_node_fs20.default.readFileSync(configPath, "utf-8"));
2215
+ raw["watchState"] = { ...raw["watchState"] ?? {}, lastScanAt: (/* @__PURE__ */ new Date()).toISOString() };
2216
+ import_node_fs20.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2217
+ } catch {
2218
+ }
2219
+ console.log(" \u2713 .claude/rules/conventions.md updated\n");
2220
+ }
2221
+
1192
2222
  // src/index.ts
1193
2223
  var program = new import_commander.Command();
1194
- program.name("clawstrap").description("Scaffold a production-ready AI agent workspace").version("1.2.0");
1195
- program.command("init").description("Create a new AI workspace in the current directory").argument("[directory]", "Target directory", ".").option("-y, --yes", "Use defaults, skip prompts").action(async (directory, options) => {
2224
+ program.name("clawstrap").description("Scaffold a production-ready AI agent workspace").version("1.4.0");
2225
+ 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) => {
1196
2226
  await init(directory, options);
1197
2227
  });
1198
2228
  var add = program.command("add").description("Add components to the workspace");
@@ -1219,4 +2249,10 @@ Unknown format: ${options.format}. Supported: paperclip
1219
2249
  }
1220
2250
  await exportPaperclip(options);
1221
2251
  });
2252
+ program.command("watch").description("Start adaptive memory daemon for this workspace").option("--stop", "Stop the running daemon").option("--silent", "Run without output").option("--once", "Run all observers once and exit (no persistent daemon)").option("--_daemon", void 0).action(async (options) => {
2253
+ await watch(options);
2254
+ });
2255
+ program.command("analyze").description("Run codebase convention scan immediately").action(async () => {
2256
+ await analyze();
2257
+ });
1222
2258
  program.parse();