clawstrap 1.3.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 +168 -124
  2. package/dist/index.cjs +1057 -60
  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
 
@@ -58,6 +258,18 @@ var ClawstrapConfigSchema = import_zod.z.object({
58
258
  qualityLevel: import_zod.z.enum(QUALITY_LEVELS),
59
259
  sessionHandoff: import_zod.z.boolean(),
60
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(),
61
273
  lastExport: LastExportSchema
62
274
  });
63
275
 
@@ -239,7 +451,7 @@ var governance_file_md_default = "# {%governanceFile%} \u2014 Master Governance
239
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";
240
452
 
241
453
  // src/templates/gitignore.tmpl
242
- 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";
243
455
 
244
456
  // src/templates/rules/context-discipline.md.tmpl
245
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";
@@ -771,8 +983,43 @@ Error: project "${name}" already exists at projects/${name}/
771
983
  }
772
984
 
773
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
774
990
  var import_node_fs7 = __toESM(require("fs"), 1);
775
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
776
1023
  var WORKLOAD_LABELS3 = {
777
1024
  research: "Research & Analysis",
778
1025
  content: "Content & Writing",
@@ -780,22 +1027,22 @@ var WORKLOAD_LABELS3 = {
780
1027
  custom: "General Purpose"
781
1028
  };
782
1029
  function countEntries(dir, exclude = []) {
783
- if (!import_node_fs7.default.existsSync(dir)) return 0;
784
- 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;
785
1032
  }
786
1033
  function countSkills(skillsDir) {
787
- if (!import_node_fs7.default.existsSync(skillsDir)) return 0;
788
- return import_node_fs7.default.readdirSync(skillsDir, { withFileTypes: true }).filter(
789
- (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"))
790
1037
  ).length;
791
1038
  }
792
1039
  async function showStatus() {
793
1040
  const { config, vars, rootDir } = loadWorkspace();
794
1041
  const systemDir = String(vars.systemDir);
795
- const agentsDir = import_node_path8.default.join(rootDir, systemDir, "agents");
796
- const skillsDir = import_node_path8.default.join(rootDir, systemDir, "skills");
797
- const rulesDir = import_node_path8.default.join(rootDir, systemDir, "rules");
798
- 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");
799
1046
  const agentCount = countEntries(agentsDir, ["_template.md"]);
800
1047
  const skillCount = countSkills(skillsDir);
801
1048
  const projectCount = countEntries(projectsDir, ["_template"]);
@@ -829,32 +1076,48 @@ Last Export:`);
829
1076
  console.log(` Date: ${exportDate}`);
830
1077
  console.log(` Output: ${config.lastExport.outputDir}`);
831
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
+ }
832
1095
  console.log();
833
1096
  }
834
1097
 
835
1098
  // src/export-paperclip.ts
836
- var import_node_fs12 = __toESM(require("fs"), 1);
837
- 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);
838
1101
  var import_prompts7 = require("@inquirer/prompts");
839
1102
 
840
1103
  // src/export-paperclip/translate-agents.ts
841
- var import_node_fs8 = __toESM(require("fs"), 1);
842
- 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);
843
1106
  function slugToName(slug) {
844
1107
  return slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
845
1108
  }
846
1109
  function translateAgents(rootDir, systemDir, workspaceName, skillSlugs) {
847
- const agentsDir = import_node_path9.default.join(rootDir, systemDir, "agents");
1110
+ const agentsDir = import_node_path10.default.join(rootDir, systemDir, "agents");
848
1111
  const agents = [];
849
1112
  const workerNames = [];
850
1113
  const workerAgents = [];
851
- if (import_node_fs8.default.existsSync(agentsDir)) {
852
- 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)) {
853
1116
  if (entry === "primary-agent.md" || entry === "_template.md" || entry.startsWith(".") || !entry.endsWith(".md")) {
854
1117
  continue;
855
1118
  }
856
1119
  const slug = entry.replace(/\.md$/, "");
857
- 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");
858
1121
  const roleMatch = rawBody.match(/^>\s*\*\*Purpose\*\*:\s*(.+)/m);
859
1122
  const description = roleMatch ? roleMatch[1].trim() : "";
860
1123
  const name = slugToName(slug);
@@ -964,8 +1227,8 @@ When your work is complete, hand off to the **CEO** for review routing. If a rev
964
1227
  }
965
1228
 
966
1229
  // src/export-paperclip/translate-governance.ts
967
- var import_node_fs9 = __toESM(require("fs"), 1);
968
- 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);
969
1232
  var GOVERNANCE_TIERS = {
970
1233
  solo: {
971
1234
  tier: "light",
@@ -999,39 +1262,39 @@ function getGovernanceConfig(qualityLevel) {
999
1262
  }
1000
1263
 
1001
1264
  // src/export-paperclip/translate-skills.ts
1002
- var import_node_fs10 = __toESM(require("fs"), 1);
1003
- 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);
1004
1267
  function translateSkills(rootDir, systemDir) {
1005
- const skillsDir = import_node_path11.default.join(rootDir, systemDir, "skills");
1268
+ const skillsDir = import_node_path12.default.join(rootDir, systemDir, "skills");
1006
1269
  const skills = [];
1007
- if (!import_node_fs10.default.existsSync(skillsDir)) return skills;
1008
- 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 })) {
1009
1272
  if (!entry.isDirectory()) continue;
1010
- const skillMdPath = import_node_path11.default.join(skillsDir, entry.name, "SKILL.md");
1011
- 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;
1012
1275
  skills.push({
1013
1276
  name: entry.name,
1014
1277
  sourcePath: `${systemDir}/skills/${entry.name}/SKILL.md`,
1015
- content: import_node_fs10.default.readFileSync(skillMdPath, "utf-8")
1278
+ content: import_node_fs11.default.readFileSync(skillMdPath, "utf-8")
1016
1279
  });
1017
1280
  }
1018
1281
  return skills;
1019
1282
  }
1020
1283
 
1021
1284
  // src/export-paperclip/translate-goals.ts
1022
- var import_node_fs11 = __toESM(require("fs"), 1);
1023
- 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);
1024
1287
  function translateGoals(rootDir) {
1025
- const projectsDir = import_node_path12.default.join(rootDir, "projects");
1288
+ const projectsDir = import_node_path13.default.join(rootDir, "projects");
1026
1289
  const goals = [];
1027
- if (!import_node_fs11.default.existsSync(projectsDir)) return goals;
1028
- 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 })) {
1029
1292
  if (!entry.isDirectory() || entry.name === "_template" || entry.name.startsWith(".")) {
1030
1293
  continue;
1031
1294
  }
1032
- const readmePath = import_node_path12.default.join(projectsDir, entry.name, "README.md");
1033
- if (!import_node_fs11.default.existsSync(readmePath)) continue;
1034
- 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");
1035
1298
  const descMatch = content.match(
1036
1299
  /## What This Project Is\s*\n+([\s\S]*?)(?=\n---|\n##|$)/
1037
1300
  );
@@ -1059,7 +1322,7 @@ async function exportPaperclip(options) {
1059
1322
  default: `Governed AI workspace for ${String(vars.workloadLabel).toLowerCase()}`
1060
1323
  });
1061
1324
  const companySlug = toSlug(companyName);
1062
- const outDir = import_node_path13.default.resolve(
1325
+ const outDir = import_node_path14.default.resolve(
1063
1326
  options.out ?? `${config.workspaceName}-paperclip`
1064
1327
  );
1065
1328
  const skills = translateSkills(rootDir, systemDir);
@@ -1089,7 +1352,7 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1089
1352
  console.log("\nValidation passed. Run without --validate to export.\n");
1090
1353
  return;
1091
1354
  }
1092
- if (import_node_fs12.default.existsSync(outDir)) {
1355
+ if (import_node_fs13.default.existsSync(outDir)) {
1093
1356
  const proceed = await (0, import_prompts7.confirm)({
1094
1357
  message: `Output directory already exists at ${outDir}. Overwrite?`,
1095
1358
  default: false
@@ -1098,10 +1361,10 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1098
1361
  console.log("Aborted.\n");
1099
1362
  return;
1100
1363
  }
1101
- import_node_fs12.default.rmSync(outDir, { recursive: true, force: true });
1364
+ import_node_fs13.default.rmSync(outDir, { recursive: true, force: true });
1102
1365
  }
1103
1366
  console.log("\nExporting to Paperclip format (agentcompanies/v1)...\n");
1104
- import_node_fs12.default.mkdirSync(outDir, { recursive: true });
1367
+ import_node_fs13.default.mkdirSync(outDir, { recursive: true });
1105
1368
  const goalsYaml = goals.length > 0 ? goals.map((g) => ` - ${g.description.split("\n")[0]}`).join("\n") : ` - ${mission}`;
1106
1369
  const pipelineLines = agents.map((a, i) => {
1107
1370
  return `${i + 1}. **${a.name}** ${a.title.toLowerCase()}`;
@@ -1131,17 +1394,17 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1131
1394
  `Generated with [Clawstrap](https://github.com/peppinho89/clawstrap) v${CLI_VERSION}`,
1132
1395
  ""
1133
1396
  ].join("\n");
1134
- 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");
1135
1398
  console.log(" \u2713 COMPANY.md");
1136
- import_node_fs12.default.writeFileSync(
1137
- import_node_path13.default.join(outDir, ".paperclip.yaml"),
1399
+ import_node_fs13.default.writeFileSync(
1400
+ import_node_path14.default.join(outDir, ".paperclip.yaml"),
1138
1401
  "schema: paperclip/v1\n",
1139
1402
  "utf-8"
1140
1403
  );
1141
1404
  console.log(" \u2713 .paperclip.yaml");
1142
1405
  for (const agent of agents) {
1143
- const agentDir = import_node_path13.default.join(outDir, "agents", agent.slug);
1144
- 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 });
1145
1408
  const frontmatterLines = [
1146
1409
  "---",
1147
1410
  `name: ${agent.name}`,
@@ -1156,12 +1419,12 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1156
1419
  }
1157
1420
  frontmatterLines.push("---");
1158
1421
  const agentMd = frontmatterLines.join("\n") + "\n\n" + agent.body + "\n";
1159
- 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");
1160
1423
  console.log(` \u2713 agents/${agent.slug}/AGENTS.md`);
1161
1424
  }
1162
1425
  if (nonCeoAgents.length > 0) {
1163
- const teamDir = import_node_path13.default.join(outDir, "teams", "engineering");
1164
- 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 });
1165
1428
  const includesList = nonCeoAgents.map((a) => ` - ../../agents/${a.slug}/AGENTS.md`).join("\n");
1166
1429
  const teamMd = [
1167
1430
  "---",
@@ -1178,13 +1441,13 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1178
1441
  `The engineering team at ${companyName}. Led by the CEO, who scopes and delegates work to specialists.`,
1179
1442
  ""
1180
1443
  ].join("\n");
1181
- 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");
1182
1445
  console.log(" \u2713 teams/engineering/TEAM.md");
1183
1446
  }
1184
1447
  for (const skill of skills) {
1185
- const skillDir = import_node_path13.default.join(outDir, "skills", skill.name);
1186
- import_node_fs12.default.mkdirSync(skillDir, { recursive: true });
1187
- 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");
1188
1451
  console.log(` \u2713 skills/${skill.name}/SKILL.md`);
1189
1452
  }
1190
1453
  const importScript = [
@@ -1200,26 +1463,26 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1200
1463
  'echo "Done. Open your Paperclip dashboard to review."',
1201
1464
  ""
1202
1465
  ].join("\n");
1203
- const importPath = import_node_path13.default.join(outDir, "import.sh");
1204
- import_node_fs12.default.writeFileSync(importPath, importScript, "utf-8");
1205
- 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);
1206
1469
  console.log(" \u2713 import.sh");
1207
1470
  const updatedConfig = {
1208
1471
  ...config,
1209
1472
  lastExport: {
1210
1473
  format: "paperclip",
1211
1474
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1212
- outputDir: import_node_path13.default.relative(rootDir, outDir) || outDir
1475
+ outputDir: import_node_path14.default.relative(rootDir, outDir) || outDir
1213
1476
  }
1214
1477
  };
1215
- import_node_fs12.default.writeFileSync(
1216
- import_node_path13.default.join(rootDir, ".clawstrap.json"),
1478
+ import_node_fs13.default.writeFileSync(
1479
+ import_node_path14.default.join(rootDir, ".clawstrap.json"),
1217
1480
  JSON.stringify(updatedConfig, null, 2) + "\n",
1218
1481
  "utf-8"
1219
1482
  );
1220
1483
  console.log(
1221
1484
  `
1222
- Exported to ${import_node_path13.default.relative(process.cwd(), outDir) || outDir}`
1485
+ Exported to ${import_node_path14.default.relative(process.cwd(), outDir) || outDir}`
1223
1486
  );
1224
1487
  console.log(
1225
1488
  `${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
@@ -1228,9 +1491,737 @@ Exported to ${import_node_path13.default.relative(process.cwd(), outDir) || outD
1228
1491
  `);
1229
1492
  }
1230
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
+
1231
2222
  // src/index.ts
1232
2223
  var program = new import_commander.Command();
1233
- program.name("clawstrap").description("Scaffold a production-ready AI agent workspace").version("1.3.0");
2224
+ program.name("clawstrap").description("Scaffold a production-ready AI agent workspace").version("1.4.0");
1234
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) => {
1235
2226
  await init(directory, options);
1236
2227
  });
@@ -1258,4 +2249,10 @@ Unknown format: ${options.format}. Supported: paperclip
1258
2249
  }
1259
2250
  await exportPaperclip(options);
1260
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
+ });
1261
2258
  program.parse();