clawstrap 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +168 -124
  2. package/dist/index.cjs +1068 -64
  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";
@@ -539,7 +751,8 @@ var WORKLOAD_LABELS2 = {
539
751
  custom: "General Purpose"
540
752
  };
541
753
  async function init(directory, options) {
542
- const targetDir = import_node_path3.default.resolve(directory);
754
+ const answers = options.yes ? getDefaults(directory) : await runPrompts();
755
+ const targetDir = directory === "." ? import_node_path3.default.resolve(answers.workspaceName) : import_node_path3.default.resolve(directory);
543
756
  if (import_node_fs2.default.existsSync(targetDir)) {
544
757
  const hasClawstrap = import_node_fs2.default.existsSync(
545
758
  import_node_path3.default.join(targetDir, ".clawstrap.json")
@@ -563,9 +776,8 @@ async function init(directory, options) {
563
776
  } else {
564
777
  import_node_fs2.default.mkdirSync(targetDir, { recursive: true });
565
778
  }
566
- const answers = options.yes ? getDefaults(directory) : await runPrompts();
567
779
  const config = ClawstrapConfigSchema.parse({
568
- version: "1.0.0",
780
+ version: "1.4.1",
569
781
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
570
782
  workspaceName: answers.workspaceName,
571
783
  targetDirectory: directory,
@@ -594,9 +806,16 @@ async function init(directory, options) {
594
806
  for (const dir of result.dirsCreated) {
595
807
  console.log(` \u2713 ${dir}/`);
596
808
  }
597
- console.log(`
809
+ const folderName = import_node_path3.default.basename(targetDir);
810
+ if (directory === ".") {
811
+ console.log(`
812
+ Done. Run \`cd ${folderName}\` and open GETTING_STARTED.md to begin.
813
+ `);
814
+ } else {
815
+ console.log(`
598
816
  Done. Open GETTING_STARTED.md to begin.
599
817
  `);
818
+ }
600
819
  }
601
820
 
602
821
  // src/add-agent.ts
@@ -771,8 +990,43 @@ Error: project "${name}" already exists at projects/${name}/
771
990
  }
772
991
 
773
992
  // src/status.ts
993
+ var import_node_fs8 = __toESM(require("fs"), 1);
994
+ var import_node_path9 = __toESM(require("path"), 1);
995
+
996
+ // src/watch/pid.ts
774
997
  var import_node_fs7 = __toESM(require("fs"), 1);
775
998
  var import_node_path8 = __toESM(require("path"), 1);
999
+ var PID_FILE = ".clawstrap.watch.pid";
1000
+ function pidPath(rootDir) {
1001
+ return import_node_path8.default.join(rootDir, PID_FILE);
1002
+ }
1003
+ function writePid(rootDir, pid) {
1004
+ import_node_fs7.default.writeFileSync(pidPath(rootDir), String(pid), "utf-8");
1005
+ }
1006
+ function readPid(rootDir) {
1007
+ const p = pidPath(rootDir);
1008
+ if (!import_node_fs7.default.existsSync(p)) return null;
1009
+ const raw = import_node_fs7.default.readFileSync(p, "utf-8").trim();
1010
+ const pid = parseInt(raw, 10);
1011
+ return isNaN(pid) ? null : pid;
1012
+ }
1013
+ function clearPid(rootDir) {
1014
+ const p = pidPath(rootDir);
1015
+ if (import_node_fs7.default.existsSync(p)) import_node_fs7.default.unlinkSync(p);
1016
+ }
1017
+ function isDaemonRunning(rootDir) {
1018
+ const pid = readPid(rootDir);
1019
+ if (pid === null) return false;
1020
+ try {
1021
+ process.kill(pid, 0);
1022
+ return true;
1023
+ } catch {
1024
+ clearPid(rootDir);
1025
+ return false;
1026
+ }
1027
+ }
1028
+
1029
+ // src/status.ts
776
1030
  var WORKLOAD_LABELS3 = {
777
1031
  research: "Research & Analysis",
778
1032
  content: "Content & Writing",
@@ -780,22 +1034,22 @@ var WORKLOAD_LABELS3 = {
780
1034
  custom: "General Purpose"
781
1035
  };
782
1036
  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;
1037
+ if (!import_node_fs8.default.existsSync(dir)) return 0;
1038
+ return import_node_fs8.default.readdirSync(dir, { withFileTypes: true }).filter((e) => !exclude.includes(e.name) && !e.name.startsWith(".")).length;
785
1039
  }
786
1040
  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"))
1041
+ if (!import_node_fs8.default.existsSync(skillsDir)) return 0;
1042
+ return import_node_fs8.default.readdirSync(skillsDir, { withFileTypes: true }).filter(
1043
+ (e) => e.isDirectory() && import_node_fs8.default.existsSync(import_node_path9.default.join(skillsDir, e.name, "SKILL.md"))
790
1044
  ).length;
791
1045
  }
792
1046
  async function showStatus() {
793
1047
  const { config, vars, rootDir } = loadWorkspace();
794
1048
  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");
1049
+ const agentsDir = import_node_path9.default.join(rootDir, systemDir, "agents");
1050
+ const skillsDir = import_node_path9.default.join(rootDir, systemDir, "skills");
1051
+ const rulesDir = import_node_path9.default.join(rootDir, systemDir, "rules");
1052
+ const projectsDir = import_node_path9.default.join(rootDir, "projects");
799
1053
  const agentCount = countEntries(agentsDir, ["_template.md"]);
800
1054
  const skillCount = countSkills(skillsDir);
801
1055
  const projectCount = countEntries(projectsDir, ["_template"]);
@@ -829,32 +1083,48 @@ Last Export:`);
829
1083
  console.log(` Date: ${exportDate}`);
830
1084
  console.log(` Output: ${config.lastExport.outputDir}`);
831
1085
  }
1086
+ const watchRunning = isDaemonRunning(rootDir);
1087
+ console.log(`
1088
+ Watch:`);
1089
+ console.log(` Status: ${watchRunning ? "running" : "stopped"}`);
1090
+ if (config.watch) {
1091
+ console.log(` Adapter: ${config.watch.adapter}`);
1092
+ }
1093
+ if (config.watchState?.lastGitCommit) {
1094
+ console.log(` Last git: ${config.watchState.lastGitCommit.slice(0, 8)}`);
1095
+ }
1096
+ if (config.watchState?.lastScanAt) {
1097
+ console.log(` Last scan: ${config.watchState.lastScanAt.split("T")[0]}`);
1098
+ }
1099
+ if (config.watchState?.lastTranscriptAt) {
1100
+ console.log(` Last transcript: ${config.watchState.lastTranscriptAt.replace("T", " ").slice(0, 16)}`);
1101
+ }
832
1102
  console.log();
833
1103
  }
834
1104
 
835
1105
  // src/export-paperclip.ts
836
- var import_node_fs12 = __toESM(require("fs"), 1);
837
- var import_node_path13 = __toESM(require("path"), 1);
1106
+ var import_node_fs13 = __toESM(require("fs"), 1);
1107
+ var import_node_path14 = __toESM(require("path"), 1);
838
1108
  var import_prompts7 = require("@inquirer/prompts");
839
1109
 
840
1110
  // src/export-paperclip/translate-agents.ts
841
- var import_node_fs8 = __toESM(require("fs"), 1);
842
- var import_node_path9 = __toESM(require("path"), 1);
1111
+ var import_node_fs9 = __toESM(require("fs"), 1);
1112
+ var import_node_path10 = __toESM(require("path"), 1);
843
1113
  function slugToName(slug) {
844
1114
  return slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
845
1115
  }
846
1116
  function translateAgents(rootDir, systemDir, workspaceName, skillSlugs) {
847
- const agentsDir = import_node_path9.default.join(rootDir, systemDir, "agents");
1117
+ const agentsDir = import_node_path10.default.join(rootDir, systemDir, "agents");
848
1118
  const agents = [];
849
1119
  const workerNames = [];
850
1120
  const workerAgents = [];
851
- if (import_node_fs8.default.existsSync(agentsDir)) {
852
- for (const entry of import_node_fs8.default.readdirSync(agentsDir)) {
1121
+ if (import_node_fs9.default.existsSync(agentsDir)) {
1122
+ for (const entry of import_node_fs9.default.readdirSync(agentsDir)) {
853
1123
  if (entry === "primary-agent.md" || entry === "_template.md" || entry.startsWith(".") || !entry.endsWith(".md")) {
854
1124
  continue;
855
1125
  }
856
1126
  const slug = entry.replace(/\.md$/, "");
857
- const rawBody = import_node_fs8.default.readFileSync(import_node_path9.default.join(agentsDir, entry), "utf-8");
1127
+ const rawBody = import_node_fs9.default.readFileSync(import_node_path10.default.join(agentsDir, entry), "utf-8");
858
1128
  const roleMatch = rawBody.match(/^>\s*\*\*Purpose\*\*:\s*(.+)/m);
859
1129
  const description = roleMatch ? roleMatch[1].trim() : "";
860
1130
  const name = slugToName(slug);
@@ -964,8 +1234,8 @@ When your work is complete, hand off to the **CEO** for review routing. If a rev
964
1234
  }
965
1235
 
966
1236
  // src/export-paperclip/translate-governance.ts
967
- var import_node_fs9 = __toESM(require("fs"), 1);
968
- var import_node_path10 = __toESM(require("path"), 1);
1237
+ var import_node_fs10 = __toESM(require("fs"), 1);
1238
+ var import_node_path11 = __toESM(require("path"), 1);
969
1239
  var GOVERNANCE_TIERS = {
970
1240
  solo: {
971
1241
  tier: "light",
@@ -999,39 +1269,39 @@ function getGovernanceConfig(qualityLevel) {
999
1269
  }
1000
1270
 
1001
1271
  // src/export-paperclip/translate-skills.ts
1002
- var import_node_fs10 = __toESM(require("fs"), 1);
1003
- var import_node_path11 = __toESM(require("path"), 1);
1272
+ var import_node_fs11 = __toESM(require("fs"), 1);
1273
+ var import_node_path12 = __toESM(require("path"), 1);
1004
1274
  function translateSkills(rootDir, systemDir) {
1005
- const skillsDir = import_node_path11.default.join(rootDir, systemDir, "skills");
1275
+ const skillsDir = import_node_path12.default.join(rootDir, systemDir, "skills");
1006
1276
  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 })) {
1277
+ if (!import_node_fs11.default.existsSync(skillsDir)) return skills;
1278
+ for (const entry of import_node_fs11.default.readdirSync(skillsDir, { withFileTypes: true })) {
1009
1279
  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;
1280
+ const skillMdPath = import_node_path12.default.join(skillsDir, entry.name, "SKILL.md");
1281
+ if (!import_node_fs11.default.existsSync(skillMdPath)) continue;
1012
1282
  skills.push({
1013
1283
  name: entry.name,
1014
1284
  sourcePath: `${systemDir}/skills/${entry.name}/SKILL.md`,
1015
- content: import_node_fs10.default.readFileSync(skillMdPath, "utf-8")
1285
+ content: import_node_fs11.default.readFileSync(skillMdPath, "utf-8")
1016
1286
  });
1017
1287
  }
1018
1288
  return skills;
1019
1289
  }
1020
1290
 
1021
1291
  // src/export-paperclip/translate-goals.ts
1022
- var import_node_fs11 = __toESM(require("fs"), 1);
1023
- var import_node_path12 = __toESM(require("path"), 1);
1292
+ var import_node_fs12 = __toESM(require("fs"), 1);
1293
+ var import_node_path13 = __toESM(require("path"), 1);
1024
1294
  function translateGoals(rootDir) {
1025
- const projectsDir = import_node_path12.default.join(rootDir, "projects");
1295
+ const projectsDir = import_node_path13.default.join(rootDir, "projects");
1026
1296
  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 })) {
1297
+ if (!import_node_fs12.default.existsSync(projectsDir)) return goals;
1298
+ for (const entry of import_node_fs12.default.readdirSync(projectsDir, { withFileTypes: true })) {
1029
1299
  if (!entry.isDirectory() || entry.name === "_template" || entry.name.startsWith(".")) {
1030
1300
  continue;
1031
1301
  }
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");
1302
+ const readmePath = import_node_path13.default.join(projectsDir, entry.name, "README.md");
1303
+ if (!import_node_fs12.default.existsSync(readmePath)) continue;
1304
+ const content = import_node_fs12.default.readFileSync(readmePath, "utf-8");
1035
1305
  const descMatch = content.match(
1036
1306
  /## What This Project Is\s*\n+([\s\S]*?)(?=\n---|\n##|$)/
1037
1307
  );
@@ -1059,7 +1329,7 @@ async function exportPaperclip(options) {
1059
1329
  default: `Governed AI workspace for ${String(vars.workloadLabel).toLowerCase()}`
1060
1330
  });
1061
1331
  const companySlug = toSlug(companyName);
1062
- const outDir = import_node_path13.default.resolve(
1332
+ const outDir = import_node_path14.default.resolve(
1063
1333
  options.out ?? `${config.workspaceName}-paperclip`
1064
1334
  );
1065
1335
  const skills = translateSkills(rootDir, systemDir);
@@ -1089,7 +1359,7 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1089
1359
  console.log("\nValidation passed. Run without --validate to export.\n");
1090
1360
  return;
1091
1361
  }
1092
- if (import_node_fs12.default.existsSync(outDir)) {
1362
+ if (import_node_fs13.default.existsSync(outDir)) {
1093
1363
  const proceed = await (0, import_prompts7.confirm)({
1094
1364
  message: `Output directory already exists at ${outDir}. Overwrite?`,
1095
1365
  default: false
@@ -1098,10 +1368,10 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1098
1368
  console.log("Aborted.\n");
1099
1369
  return;
1100
1370
  }
1101
- import_node_fs12.default.rmSync(outDir, { recursive: true, force: true });
1371
+ import_node_fs13.default.rmSync(outDir, { recursive: true, force: true });
1102
1372
  }
1103
1373
  console.log("\nExporting to Paperclip format (agentcompanies/v1)...\n");
1104
- import_node_fs12.default.mkdirSync(outDir, { recursive: true });
1374
+ import_node_fs13.default.mkdirSync(outDir, { recursive: true });
1105
1375
  const goalsYaml = goals.length > 0 ? goals.map((g) => ` - ${g.description.split("\n")[0]}`).join("\n") : ` - ${mission}`;
1106
1376
  const pipelineLines = agents.map((a, i) => {
1107
1377
  return `${i + 1}. **${a.name}** ${a.title.toLowerCase()}`;
@@ -1131,17 +1401,17 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1131
1401
  `Generated with [Clawstrap](https://github.com/peppinho89/clawstrap) v${CLI_VERSION}`,
1132
1402
  ""
1133
1403
  ].join("\n");
1134
- import_node_fs12.default.writeFileSync(import_node_path13.default.join(outDir, "COMPANY.md"), companyMd, "utf-8");
1404
+ import_node_fs13.default.writeFileSync(import_node_path14.default.join(outDir, "COMPANY.md"), companyMd, "utf-8");
1135
1405
  console.log(" \u2713 COMPANY.md");
1136
- import_node_fs12.default.writeFileSync(
1137
- import_node_path13.default.join(outDir, ".paperclip.yaml"),
1406
+ import_node_fs13.default.writeFileSync(
1407
+ import_node_path14.default.join(outDir, ".paperclip.yaml"),
1138
1408
  "schema: paperclip/v1\n",
1139
1409
  "utf-8"
1140
1410
  );
1141
1411
  console.log(" \u2713 .paperclip.yaml");
1142
1412
  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 });
1413
+ const agentDir = import_node_path14.default.join(outDir, "agents", agent.slug);
1414
+ import_node_fs13.default.mkdirSync(agentDir, { recursive: true });
1145
1415
  const frontmatterLines = [
1146
1416
  "---",
1147
1417
  `name: ${agent.name}`,
@@ -1156,12 +1426,12 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1156
1426
  }
1157
1427
  frontmatterLines.push("---");
1158
1428
  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");
1429
+ import_node_fs13.default.writeFileSync(import_node_path14.default.join(agentDir, "AGENTS.md"), agentMd, "utf-8");
1160
1430
  console.log(` \u2713 agents/${agent.slug}/AGENTS.md`);
1161
1431
  }
1162
1432
  if (nonCeoAgents.length > 0) {
1163
- const teamDir = import_node_path13.default.join(outDir, "teams", "engineering");
1164
- import_node_fs12.default.mkdirSync(teamDir, { recursive: true });
1433
+ const teamDir = import_node_path14.default.join(outDir, "teams", "engineering");
1434
+ import_node_fs13.default.mkdirSync(teamDir, { recursive: true });
1165
1435
  const includesList = nonCeoAgents.map((a) => ` - ../../agents/${a.slug}/AGENTS.md`).join("\n");
1166
1436
  const teamMd = [
1167
1437
  "---",
@@ -1178,13 +1448,13 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1178
1448
  `The engineering team at ${companyName}. Led by the CEO, who scopes and delegates work to specialists.`,
1179
1449
  ""
1180
1450
  ].join("\n");
1181
- import_node_fs12.default.writeFileSync(import_node_path13.default.join(teamDir, "TEAM.md"), teamMd, "utf-8");
1451
+ import_node_fs13.default.writeFileSync(import_node_path14.default.join(teamDir, "TEAM.md"), teamMd, "utf-8");
1182
1452
  console.log(" \u2713 teams/engineering/TEAM.md");
1183
1453
  }
1184
1454
  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");
1455
+ const skillDir = import_node_path14.default.join(outDir, "skills", skill.name);
1456
+ import_node_fs13.default.mkdirSync(skillDir, { recursive: true });
1457
+ import_node_fs13.default.writeFileSync(import_node_path14.default.join(skillDir, "SKILL.md"), skill.content, "utf-8");
1188
1458
  console.log(` \u2713 skills/${skill.name}/SKILL.md`);
1189
1459
  }
1190
1460
  const importScript = [
@@ -1200,26 +1470,26 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
1200
1470
  'echo "Done. Open your Paperclip dashboard to review."',
1201
1471
  ""
1202
1472
  ].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);
1473
+ const importPath = import_node_path14.default.join(outDir, "import.sh");
1474
+ import_node_fs13.default.writeFileSync(importPath, importScript, "utf-8");
1475
+ import_node_fs13.default.chmodSync(importPath, 493);
1206
1476
  console.log(" \u2713 import.sh");
1207
1477
  const updatedConfig = {
1208
1478
  ...config,
1209
1479
  lastExport: {
1210
1480
  format: "paperclip",
1211
1481
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1212
- outputDir: import_node_path13.default.relative(rootDir, outDir) || outDir
1482
+ outputDir: import_node_path14.default.relative(rootDir, outDir) || outDir
1213
1483
  }
1214
1484
  };
1215
- import_node_fs12.default.writeFileSync(
1216
- import_node_path13.default.join(rootDir, ".clawstrap.json"),
1485
+ import_node_fs13.default.writeFileSync(
1486
+ import_node_path14.default.join(rootDir, ".clawstrap.json"),
1217
1487
  JSON.stringify(updatedConfig, null, 2) + "\n",
1218
1488
  "utf-8"
1219
1489
  );
1220
1490
  console.log(
1221
1491
  `
1222
- Exported to ${import_node_path13.default.relative(process.cwd(), outDir) || outDir}`
1492
+ Exported to ${import_node_path14.default.relative(process.cwd(), outDir) || outDir}`
1223
1493
  );
1224
1494
  console.log(
1225
1495
  `${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
@@ -1228,9 +1498,737 @@ Exported to ${import_node_path13.default.relative(process.cwd(), outDir) || outD
1228
1498
  `);
1229
1499
  }
1230
1500
 
1501
+ // src/watch.ts
1502
+ var import_node_path20 = __toESM(require("path"), 1);
1503
+ var import_node_child_process4 = require("child_process");
1504
+ var import_node_fs19 = __toESM(require("fs"), 1);
1505
+
1506
+ // src/watch/git.ts
1507
+ var import_node_child_process = require("child_process");
1508
+ var import_node_fs15 = __toESM(require("fs"), 1);
1509
+ var import_node_path16 = __toESM(require("path"), 1);
1510
+ init_writers();
1511
+ var STOPWORDS = /* @__PURE__ */ new Set([
1512
+ "fix",
1513
+ "add",
1514
+ "update",
1515
+ "remove",
1516
+ "feat",
1517
+ "chore",
1518
+ "the",
1519
+ "a",
1520
+ "an",
1521
+ "and",
1522
+ "or",
1523
+ "in",
1524
+ "on",
1525
+ "at",
1526
+ "to",
1527
+ "for",
1528
+ "of",
1529
+ "is",
1530
+ "was",
1531
+ "be",
1532
+ "it",
1533
+ "as",
1534
+ "with",
1535
+ "by",
1536
+ "this",
1537
+ "that"
1538
+ ]);
1539
+ function parseGitLog(output) {
1540
+ const entries = [];
1541
+ const lines = output.split("\n");
1542
+ let current = null;
1543
+ for (const line of lines) {
1544
+ if (line.includes("|||")) {
1545
+ if (current) entries.push(current);
1546
+ const parts = line.split("|||");
1547
+ current = {
1548
+ sha: parts[0]?.trim() ?? "",
1549
+ subject: parts[1]?.trim() ?? "",
1550
+ author: parts[2]?.trim() ?? "",
1551
+ date: parts[3]?.trim() ?? "",
1552
+ files: []
1553
+ };
1554
+ } else if (line.trim() && current) {
1555
+ current.files.push(line.trim());
1556
+ }
1557
+ }
1558
+ if (current) entries.push(current);
1559
+ return entries.filter((e) => e.sha.length > 0);
1560
+ }
1561
+ function getTopDirs(entries) {
1562
+ const dirCount = /* @__PURE__ */ new Map();
1563
+ for (const entry of entries) {
1564
+ const seenDirs = /* @__PURE__ */ new Set();
1565
+ for (const file of entry.files) {
1566
+ const dir = import_node_path16.default.dirname(file);
1567
+ if (dir !== "." && !seenDirs.has(dir)) {
1568
+ seenDirs.add(dir);
1569
+ dirCount.set(dir, (dirCount.get(dir) ?? 0) + 1);
1570
+ }
1571
+ }
1572
+ }
1573
+ return Array.from(dirCount.entries()).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([dir]) => dir);
1574
+ }
1575
+ function getCoChangingFiles(entries, minCommits = 3) {
1576
+ const pairCount = /* @__PURE__ */ new Map();
1577
+ for (const entry of entries) {
1578
+ const files = entry.files.slice().sort();
1579
+ for (let i = 0; i < files.length; i++) {
1580
+ for (let j = i + 1; j < files.length; j++) {
1581
+ const key = `${files[i]}|||${files[j]}`;
1582
+ pairCount.set(key, (pairCount.get(key) ?? 0) + 1);
1583
+ }
1584
+ }
1585
+ }
1586
+ return Array.from(pairCount.entries()).filter(([, count]) => count >= minCommits).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([key]) => key.split("|||"));
1587
+ }
1588
+ function getRecurringWords(entries) {
1589
+ const wordCount = /* @__PURE__ */ new Map();
1590
+ for (const entry of entries) {
1591
+ const words = entry.subject.toLowerCase().split(/\s+/).map((w) => w.replace(/[^a-z0-9]/g, "")).filter((w) => w.length > 2 && !STOPWORDS.has(w));
1592
+ for (const word of new Set(words)) {
1593
+ wordCount.set(word, (wordCount.get(word) ?? 0) + 1);
1594
+ }
1595
+ }
1596
+ return Array.from(wordCount.entries()).filter(([, count]) => count >= 2).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([word]) => word);
1597
+ }
1598
+ async function runGitObserver(rootDir, sinceCommit) {
1599
+ if (!import_node_fs15.default.existsSync(import_node_path16.default.join(rootDir, ".git"))) {
1600
+ return null;
1601
+ }
1602
+ let headSha;
1603
+ try {
1604
+ headSha = (0, import_node_child_process.execSync)(`git -C "${rootDir}" rev-parse HEAD`, {
1605
+ encoding: "utf-8"
1606
+ }).trim();
1607
+ } catch {
1608
+ return null;
1609
+ }
1610
+ if (!headSha) return null;
1611
+ if (sinceCommit && sinceCommit === headSha) {
1612
+ return { lastCommit: headSha, entriesWritten: 0 };
1613
+ }
1614
+ let logOutput;
1615
+ try {
1616
+ const rangeArg = sinceCommit ? `${sinceCommit}..HEAD` : "";
1617
+ const cmd = `git -C "${rootDir}" log ${rangeArg} --pretty=format:"%H|||%s|||%ae|||%ad" --date=short --name-only`;
1618
+ logOutput = (0, import_node_child_process.execSync)(cmd, { encoding: "utf-8" }).trim();
1619
+ } catch {
1620
+ return null;
1621
+ }
1622
+ if (!logOutput) {
1623
+ return { lastCommit: headSha, entriesWritten: 0 };
1624
+ }
1625
+ const entries = parseGitLog(logOutput);
1626
+ if (entries.length === 0) {
1627
+ return { lastCommit: headSha, entriesWritten: 0 };
1628
+ }
1629
+ const memoryEntries = [];
1630
+ const coChangingPairs = getCoChangingFiles(entries, 3);
1631
+ if (coChangingPairs.length > 0) {
1632
+ const pairs = coChangingPairs.map(([a, b]) => `${a} <-> ${b}`).join(", ");
1633
+ memoryEntries.push(`Co-changing file pairs (frequently modified together): ${pairs}`);
1634
+ }
1635
+ const topDirs = getTopDirs(entries);
1636
+ if (topDirs.length > 0) {
1637
+ memoryEntries.push(`Top high-churn directories (most commits): ${topDirs.join(", ")}`);
1638
+ }
1639
+ const recurringWords = getRecurringWords(entries);
1640
+ if (recurringWords.length > 0) {
1641
+ memoryEntries.push(`Recurring themes in recent commits: ${recurringWords.join(", ")}`);
1642
+ }
1643
+ const dateRange = entries.length > 0 ? `from ${entries[entries.length - 1]?.date ?? "?"} to ${entries[0]?.date ?? "?"}` : "";
1644
+ memoryEntries.push(
1645
+ `Git history analyzed: ${entries.length} commit(s) ${dateRange}. Authors: ${[...new Set(entries.map((e) => e.author))].join(", ")}`
1646
+ );
1647
+ if (memoryEntries.length > 0) {
1648
+ appendToMemory(rootDir, memoryEntries, "git");
1649
+ }
1650
+ return { lastCommit: headSha, entriesWritten: memoryEntries.length };
1651
+ }
1652
+
1653
+ // src/watch/scan.ts
1654
+ var import_node_fs16 = __toESM(require("fs"), 1);
1655
+ var import_node_path17 = __toESM(require("path"), 1);
1656
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "tmp", "dist", ".claude"]);
1657
+ var CODE_EXTS = /* @__PURE__ */ new Set([".ts", ".js", ".tsx", ".jsx"]);
1658
+ function walkDir(dir, maxDepth = 10, depth = 0) {
1659
+ if (depth > maxDepth) return [];
1660
+ let results = [];
1661
+ let entries;
1662
+ try {
1663
+ entries = import_node_fs16.default.readdirSync(dir, { withFileTypes: true });
1664
+ } catch {
1665
+ return [];
1666
+ }
1667
+ for (const entry of entries) {
1668
+ if (SKIP_DIRS.has(entry.name)) continue;
1669
+ const fullPath = import_node_path17.default.join(dir, entry.name);
1670
+ if (entry.isDirectory()) {
1671
+ results = results.concat(walkDir(fullPath, maxDepth, depth + 1));
1672
+ } else if (entry.isFile()) {
1673
+ results.push(fullPath);
1674
+ }
1675
+ }
1676
+ return results;
1677
+ }
1678
+ function detectNamingCase(name) {
1679
+ if (/^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(name)) return "kebab-case";
1680
+ if (/^[A-Z][A-Z0-9_]*$/.test(name)) return "other";
1681
+ if (/^[A-Z][a-zA-Z0-9]+$/.test(name)) return "PascalCase";
1682
+ if (/^[a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*$/.test(name)) return "camelCase";
1683
+ if (/^[a-z][a-z0-9]*(_[a-z0-9]+)+$/.test(name)) return "snake_case";
1684
+ return "other";
1685
+ }
1686
+ function analyzeNaming(files) {
1687
+ const counts = {
1688
+ "kebab-case": 0,
1689
+ camelCase: 0,
1690
+ snake_case: 0,
1691
+ PascalCase: 0,
1692
+ other: 0
1693
+ };
1694
+ const examples = {
1695
+ "kebab-case": [],
1696
+ camelCase: [],
1697
+ snake_case: [],
1698
+ PascalCase: [],
1699
+ other: []
1700
+ };
1701
+ for (const file of files) {
1702
+ const base = import_node_path17.default.basename(file, import_node_path17.default.extname(file));
1703
+ const style2 = detectNamingCase(base);
1704
+ counts[style2]++;
1705
+ if (examples[style2].length < 3) examples[style2].push(base);
1706
+ }
1707
+ const dominant = Object.entries(counts).sort((a, b) => b[1] - a[1]).filter(([key]) => key !== "other")[0];
1708
+ if (!dominant || dominant[1] === 0) {
1709
+ return ["No dominant file naming convention detected."];
1710
+ }
1711
+ const [style, count] = dominant;
1712
+ const exampleList = examples[style].slice(0, 3).join(", ");
1713
+ const result = [`Dominant file naming: ${style} (${count} files). Examples: ${exampleList}`];
1714
+ const total = Object.values(counts).reduce((a, b) => a + b, 0) - counts.other;
1715
+ if (total > 0) {
1716
+ const pct = Math.round(count / total * 100);
1717
+ result.push(`${style} used in ${pct}% of named files.`);
1718
+ }
1719
+ return result;
1720
+ }
1721
+ function analyzeImports(files) {
1722
+ const sample = files.filter((f) => CODE_EXTS.has(import_node_path17.default.extname(f))).slice(0, 20);
1723
+ let relativeCount = 0;
1724
+ let absoluteCount = 0;
1725
+ let barrelCount = 0;
1726
+ for (const file of sample) {
1727
+ let content;
1728
+ try {
1729
+ content = import_node_fs16.default.readFileSync(file, "utf-8");
1730
+ } catch {
1731
+ continue;
1732
+ }
1733
+ const importLines = content.split("\n").filter((l) => /^import\s/.test(l));
1734
+ for (const line of importLines) {
1735
+ if (/from\s+['"]\.\.?\//.test(line)) relativeCount++;
1736
+ else if (/from\s+['"]/.test(line)) absoluteCount++;
1737
+ }
1738
+ const base = import_node_path17.default.basename(file, import_node_path17.default.extname(file));
1739
+ if (base === "index") barrelCount++;
1740
+ }
1741
+ const results = [];
1742
+ const total = relativeCount + absoluteCount;
1743
+ if (total > 0) {
1744
+ const relPct = Math.round(relativeCount / total * 100);
1745
+ results.push(
1746
+ `Import style: ${relPct}% relative (./), ${100 - relPct}% absolute/alias. Analyzed ${sample.length} files.`
1747
+ );
1748
+ } else {
1749
+ results.push("No import statements found in sampled files.");
1750
+ }
1751
+ if (barrelCount > 0) {
1752
+ results.push(`Barrel exports detected: ${barrelCount} index file(s) found.`);
1753
+ }
1754
+ return results;
1755
+ }
1756
+ function analyzeTesting(files) {
1757
+ const testPatterns = [];
1758
+ let hasTestExt = false;
1759
+ let hasSpecExt = false;
1760
+ let hasTestsDir = false;
1761
+ for (const file of files) {
1762
+ const base = import_node_path17.default.basename(file);
1763
+ if (/\.test\.(ts|js|tsx|jsx)$/.test(base)) hasTestExt = true;
1764
+ if (/\.spec\.(ts|js|tsx|jsx)$/.test(base)) hasSpecExt = true;
1765
+ if (file.includes("/__tests__/") || file.includes("\\__tests__\\")) hasTestsDir = true;
1766
+ }
1767
+ if (hasTestExt) testPatterns.push("*.test.ts/js");
1768
+ if (hasSpecExt) testPatterns.push("*.spec.ts/js");
1769
+ if (hasTestsDir) testPatterns.push("__tests__/ directories");
1770
+ if (testPatterns.length === 0) {
1771
+ return ["No test files detected."];
1772
+ }
1773
+ return [`Test patterns found: ${testPatterns.join(", ")}`];
1774
+ }
1775
+ function analyzeErrorHandling(files) {
1776
+ const sample = files.filter((f) => CODE_EXTS.has(import_node_path17.default.extname(f))).slice(0, 20);
1777
+ let tryCatchCount = 0;
1778
+ let resultTypeCount = 0;
1779
+ for (const file of sample) {
1780
+ let content;
1781
+ try {
1782
+ content = import_node_fs16.default.readFileSync(file, "utf-8");
1783
+ } catch {
1784
+ continue;
1785
+ }
1786
+ const tryCatches = (content.match(/\btry\s*\{/g) ?? []).length;
1787
+ const resultTypes = (content.match(/Result<|Either</g) ?? []).length;
1788
+ tryCatchCount += tryCatches;
1789
+ resultTypeCount += resultTypes;
1790
+ }
1791
+ const results = [];
1792
+ if (tryCatchCount > 0 && resultTypeCount === 0) {
1793
+ results.push(`Error handling: try/catch dominant (${tryCatchCount} occurrences in ${sample.length} files).`);
1794
+ } else if (resultTypeCount > 0 && tryCatchCount === 0) {
1795
+ results.push(`Error handling: Result/Either type pattern dominant (${resultTypeCount} occurrences).`);
1796
+ } else if (tryCatchCount > 0 && resultTypeCount > 0) {
1797
+ const dominant = tryCatchCount >= resultTypeCount ? "try/catch" : "Result/Either";
1798
+ results.push(
1799
+ `Error handling: mixed \u2014 ${tryCatchCount} try/catch and ${resultTypeCount} Result/Either. Dominant: ${dominant}.`
1800
+ );
1801
+ } else {
1802
+ results.push(`No explicit error handling patterns detected in ${sample.length} sampled files.`);
1803
+ }
1804
+ return results;
1805
+ }
1806
+ function analyzeComments(files) {
1807
+ const sample = files.filter((f) => CODE_EXTS.has(import_node_path17.default.extname(f))).slice(0, 20);
1808
+ let jsdocCount = 0;
1809
+ let inlineCount = 0;
1810
+ let totalLines = 0;
1811
+ for (const file of sample) {
1812
+ let content;
1813
+ try {
1814
+ content = import_node_fs16.default.readFileSync(file, "utf-8");
1815
+ } catch {
1816
+ continue;
1817
+ }
1818
+ const lines = content.split("\n");
1819
+ totalLines += lines.length;
1820
+ jsdocCount += (content.match(/\/\*\*/g) ?? []).length;
1821
+ inlineCount += lines.filter((l) => /^\s*\/\//.test(l)).length;
1822
+ }
1823
+ const commentDensity = totalLines > 0 ? (jsdocCount + inlineCount) / totalLines : 0;
1824
+ let density;
1825
+ if (commentDensity > 0.15) density = "heavy";
1826
+ else if (commentDensity > 0.05) density = "moderate";
1827
+ else density = "minimal";
1828
+ return [
1829
+ `Comment density: ${density} (${jsdocCount} JSDoc blocks, ${inlineCount} inline comments across ${sample.length} files).`
1830
+ ];
1831
+ }
1832
+ async function runScan(rootDir) {
1833
+ const allFiles = walkDir(rootDir);
1834
+ return {
1835
+ naming: analyzeNaming(allFiles),
1836
+ imports: analyzeImports(allFiles),
1837
+ testing: analyzeTesting(allFiles),
1838
+ errorHandling: analyzeErrorHandling(allFiles),
1839
+ comments: analyzeComments(allFiles)
1840
+ };
1841
+ }
1842
+
1843
+ // src/watch.ts
1844
+ init_writers();
1845
+
1846
+ // src/watch/daemon.ts
1847
+ var import_node_fs18 = __toESM(require("fs"), 1);
1848
+ var import_node_path19 = __toESM(require("path"), 1);
1849
+ init_writers();
1850
+
1851
+ // src/watch/transcripts.ts
1852
+ var import_node_fs17 = __toESM(require("fs"), 1);
1853
+ var import_node_path18 = __toESM(require("path"), 1);
1854
+ async function processTranscript(filePath, adapter) {
1855
+ let content;
1856
+ try {
1857
+ content = import_node_fs17.default.readFileSync(filePath, "utf-8");
1858
+ } catch {
1859
+ return null;
1860
+ }
1861
+ const prompt = `Extract structured information from this session summary. Return ONLY valid JSON with no markdown or explanation.
1862
+
1863
+ Session content:
1864
+ ${content}
1865
+
1866
+ Return JSON with exactly these keys:
1867
+ {
1868
+ "decisions": ["what approach was chosen and why"],
1869
+ "corrections": ["what the agent got wrong and how it was fixed"],
1870
+ "deferredIdeas": ["ideas mentioned but not acted on"],
1871
+ "openThreads": ["unresolved questions or next steps"]
1872
+ }
1873
+ Each item must be a concise one-sentence string. Arrays may be empty.`;
1874
+ let response;
1875
+ try {
1876
+ response = await adapter.complete(prompt);
1877
+ } catch {
1878
+ return null;
1879
+ }
1880
+ try {
1881
+ const cleaned = response.replace(/^```(?:json)?\s*/m, "").replace(/\s*```\s*$/m, "").trim();
1882
+ const parsed = JSON.parse(cleaned);
1883
+ if (typeof parsed !== "object" || parsed === null) return null;
1884
+ const obj = parsed;
1885
+ const toArray = (v) => {
1886
+ if (!Array.isArray(v)) return [];
1887
+ return v.filter((item) => typeof item === "string");
1888
+ };
1889
+ return {
1890
+ decisions: toArray(obj["decisions"]),
1891
+ corrections: toArray(obj["corrections"]),
1892
+ deferredIdeas: toArray(obj["deferredIdeas"]),
1893
+ openThreads: toArray(obj["openThreads"])
1894
+ };
1895
+ } catch {
1896
+ return null;
1897
+ }
1898
+ }
1899
+ function watchTranscriptDir(rootDir, onNewFile) {
1900
+ const sessionsDir = import_node_path18.default.join(rootDir, "tmp", "sessions");
1901
+ import_node_fs17.default.mkdirSync(sessionsDir, { recursive: true });
1902
+ const watcher = import_node_fs17.default.watch(sessionsDir, (event, filename) => {
1903
+ if (event !== "rename" || !filename) return;
1904
+ if (!filename.endsWith(".md")) return;
1905
+ const filePath = import_node_path18.default.join(sessionsDir, filename);
1906
+ if (!import_node_fs17.default.existsSync(filePath)) return;
1907
+ onNewFile(filePath).catch(() => {
1908
+ });
1909
+ });
1910
+ return () => {
1911
+ watcher.close();
1912
+ };
1913
+ }
1914
+
1915
+ // src/watch/adapters/claude-local.ts
1916
+ var import_node_child_process2 = require("child_process");
1917
+ var ClaudeLocalAdapter = class {
1918
+ async complete(prompt) {
1919
+ const escaped = prompt.replace(/'/g, "'\\''");
1920
+ try {
1921
+ const result = (0, import_node_child_process2.execSync)(`claude -p '${escaped}'`, {
1922
+ encoding: "utf-8",
1923
+ timeout: 6e4,
1924
+ stdio: ["pipe", "pipe", "pipe"]
1925
+ });
1926
+ return result.trim();
1927
+ } catch (err) {
1928
+ const msg = err instanceof Error ? err.message : String(err);
1929
+ if (msg.includes("ENOENT")) {
1930
+ throw new Error("Claude Code CLI not found. Install it or use a different adapter.");
1931
+ }
1932
+ throw err;
1933
+ }
1934
+ }
1935
+ };
1936
+
1937
+ // src/watch/adapters/claude-api.ts
1938
+ var API_URL = "https://api.anthropic.com/v1/messages";
1939
+ var DEFAULT_MODEL = "claude-haiku-4-5-20251001";
1940
+ var ClaudeApiAdapter = class {
1941
+ apiKey;
1942
+ model;
1943
+ constructor(apiKey, model) {
1944
+ this.apiKey = apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
1945
+ this.model = model ?? DEFAULT_MODEL;
1946
+ if (!this.apiKey) {
1947
+ throw new Error("ANTHROPIC_API_KEY not set. Set it or use a different adapter.");
1948
+ }
1949
+ }
1950
+ async complete(prompt) {
1951
+ const response = await fetch(API_URL, {
1952
+ method: "POST",
1953
+ headers: {
1954
+ "x-api-key": this.apiKey,
1955
+ "anthropic-version": "2023-06-01",
1956
+ "content-type": "application/json"
1957
+ },
1958
+ body: JSON.stringify({
1959
+ model: this.model,
1960
+ max_tokens: 1024,
1961
+ messages: [{ role: "user", content: prompt }]
1962
+ })
1963
+ });
1964
+ if (!response.ok) {
1965
+ throw new Error(`Anthropic API error: ${response.status} ${response.statusText}`);
1966
+ }
1967
+ const data = await response.json();
1968
+ const text = data.content.find((c) => c.type === "text")?.text ?? "";
1969
+ return text.trim();
1970
+ }
1971
+ };
1972
+
1973
+ // src/watch/adapters/ollama.ts
1974
+ var DEFAULT_OLLAMA_URL = "http://localhost:11434/api/generate";
1975
+ var DEFAULT_MODEL2 = "llama3.2";
1976
+ var OllamaAdapter = class {
1977
+ model;
1978
+ url;
1979
+ constructor(model, url) {
1980
+ this.model = model ?? DEFAULT_MODEL2;
1981
+ this.url = url ?? DEFAULT_OLLAMA_URL;
1982
+ }
1983
+ async complete(prompt) {
1984
+ const response = await fetch(this.url, {
1985
+ method: "POST",
1986
+ headers: { "content-type": "application/json" },
1987
+ body: JSON.stringify({
1988
+ model: this.model,
1989
+ prompt,
1990
+ stream: false
1991
+ })
1992
+ });
1993
+ if (!response.ok) {
1994
+ throw new Error(`Ollama error: ${response.status}. Is Ollama running?`);
1995
+ }
1996
+ const data = await response.json();
1997
+ return data.response.trim();
1998
+ }
1999
+ };
2000
+
2001
+ // src/watch/adapters/codex-local.ts
2002
+ var import_node_child_process3 = require("child_process");
2003
+ var CodexLocalAdapter = class {
2004
+ async complete(prompt) {
2005
+ const escaped = prompt.replace(/'/g, "'\\''");
2006
+ try {
2007
+ const result = (0, import_node_child_process3.execSync)(`codex '${escaped}'`, {
2008
+ encoding: "utf-8",
2009
+ timeout: 6e4,
2010
+ stdio: ["pipe", "pipe", "pipe"]
2011
+ });
2012
+ return result.trim();
2013
+ } catch (err) {
2014
+ const msg = err instanceof Error ? err.message : String(err);
2015
+ if (msg.includes("ENOENT")) {
2016
+ throw new Error("Codex CLI not found. Install it or use a different adapter.");
2017
+ }
2018
+ throw err;
2019
+ }
2020
+ }
2021
+ };
2022
+
2023
+ // src/watch/adapters/index.ts
2024
+ function createAdapter(config) {
2025
+ const type = config.watch?.adapter ?? "claude-local";
2026
+ switch (type) {
2027
+ case "claude-local":
2028
+ return new ClaudeLocalAdapter();
2029
+ case "claude-api":
2030
+ return new ClaudeApiAdapter();
2031
+ case "ollama":
2032
+ return new OllamaAdapter();
2033
+ case "codex-local":
2034
+ return new CodexLocalAdapter();
2035
+ default:
2036
+ return new ClaudeLocalAdapter();
2037
+ }
2038
+ }
2039
+
2040
+ // src/watch/daemon.ts
2041
+ async function runDaemon(rootDir, config) {
2042
+ const silent = config.watch?.silent ?? false;
2043
+ const log = silent ? () => {
2044
+ } : (msg) => process.stdout.write(msg + "\n");
2045
+ const cleanup = [];
2046
+ const shutdown = () => {
2047
+ cleanup.forEach((fn) => fn());
2048
+ clearPid(rootDir);
2049
+ process.exit(0);
2050
+ };
2051
+ process.on("SIGTERM", shutdown);
2052
+ process.on("SIGINT", shutdown);
2053
+ log("[clawstrap watch] daemon started");
2054
+ const sinceCommit = config.watchState?.lastGitCommit ?? null;
2055
+ const gitResult = await runGitObserver(rootDir, sinceCommit);
2056
+ if (gitResult) {
2057
+ updateWatchState(rootDir, { lastGitCommit: gitResult.lastCommit });
2058
+ log(`[clawstrap watch] git: ${gitResult.entriesWritten} entries written`);
2059
+ }
2060
+ const adapter = createAdapter(config);
2061
+ const stopTranscripts = watchTranscriptDir(rootDir, async (filePath) => {
2062
+ log(`[clawstrap watch] transcript: processing ${import_node_path19.default.basename(filePath)}`);
2063
+ const result = await processTranscript(filePath, adapter);
2064
+ if (result) {
2065
+ const { appendToMemory: appendToMemory2, appendToGotchaLog: appendToGotchaLog2, appendToFutureConsiderations: appendToFutureConsiderations2 } = await Promise.resolve().then(() => (init_writers(), writers_exports));
2066
+ if (result.decisions.length) appendToMemory2(rootDir, result.decisions, "session");
2067
+ if (result.corrections.length) appendToGotchaLog2(rootDir, result.corrections);
2068
+ if (result.deferredIdeas.length) appendToFutureConsiderations2(rootDir, result.deferredIdeas);
2069
+ updateWatchState(rootDir, { lastTranscriptAt: (/* @__PURE__ */ new Date()).toISOString() });
2070
+ log(
2071
+ `[clawstrap watch] transcript: decisions=${result.decisions.length} corrections=${result.corrections.length}`
2072
+ );
2073
+ }
2074
+ });
2075
+ cleanup.push(stopTranscripts);
2076
+ const intervalDays = config.watch?.scan?.intervalDays ?? 7;
2077
+ const lastScan = config.watchState?.lastScanAt ? new Date(config.watchState.lastScanAt) : null;
2078
+ const msSinceLastScan = lastScan ? Date.now() - lastScan.getTime() : Infinity;
2079
+ const scanIntervalMs = intervalDays * 24 * 60 * 60 * 1e3;
2080
+ const doScan = async () => {
2081
+ log("[clawstrap watch] scan: running convention scan...");
2082
+ const sections = await runScan(rootDir);
2083
+ writeConventions(rootDir, sections);
2084
+ updateWatchState(rootDir, { lastScanAt: (/* @__PURE__ */ new Date()).toISOString() });
2085
+ log("[clawstrap watch] scan: conventions.md updated");
2086
+ };
2087
+ if (msSinceLastScan >= scanIntervalMs) {
2088
+ await doScan();
2089
+ }
2090
+ const scanTimer = setInterval(doScan, scanIntervalMs);
2091
+ cleanup.push(() => clearInterval(scanTimer));
2092
+ log("[clawstrap watch] watching for changes...");
2093
+ await new Promise(() => {
2094
+ });
2095
+ }
2096
+ function updateWatchState(rootDir, updates) {
2097
+ const configPath = import_node_path19.default.join(rootDir, ".clawstrap.json");
2098
+ try {
2099
+ const raw = JSON.parse(import_node_fs18.default.readFileSync(configPath, "utf-8"));
2100
+ raw["watchState"] = { ...raw["watchState"] ?? {}, ...updates };
2101
+ import_node_fs18.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2102
+ } catch {
2103
+ }
2104
+ }
2105
+
2106
+ // src/watch.ts
2107
+ async function watch(options) {
2108
+ const { config, rootDir } = loadWorkspace();
2109
+ if (options._daemon) {
2110
+ await runDaemon(rootDir, config);
2111
+ return;
2112
+ }
2113
+ if (options.stop) {
2114
+ const pid = readPid(rootDir);
2115
+ if (!pid || !isDaemonRunning(rootDir)) {
2116
+ console.log("\nNo daemon running.\n");
2117
+ return;
2118
+ }
2119
+ process.kill(pid, "SIGTERM");
2120
+ clearPid(rootDir);
2121
+ console.log(`
2122
+ Daemon stopped (pid ${pid}).
2123
+ `);
2124
+ return;
2125
+ }
2126
+ if (options.once) {
2127
+ console.log("\nRunning all observers once...\n");
2128
+ const gitResult = await runGitObserver(rootDir, config.watchState?.lastGitCommit ?? null);
2129
+ if (gitResult) {
2130
+ persistWatchState(rootDir, { lastGitCommit: gitResult.lastCommit });
2131
+ console.log(` \u2713 git: ${gitResult.entriesWritten} entries`);
2132
+ }
2133
+ const sections = await runScan(rootDir);
2134
+ writeConventions(rootDir, sections);
2135
+ persistWatchState(rootDir, { lastScanAt: (/* @__PURE__ */ new Date()).toISOString() });
2136
+ console.log(" \u2713 scan: conventions.md updated");
2137
+ console.log("\nDone.\n");
2138
+ return;
2139
+ }
2140
+ if (isDaemonRunning(rootDir)) {
2141
+ const pid = readPid(rootDir);
2142
+ console.log(`
2143
+ Daemon already running (pid ${pid}). Use --stop to stop it.
2144
+ `);
2145
+ return;
2146
+ }
2147
+ injectWatchHook(rootDir, config);
2148
+ const self = process.argv[1];
2149
+ const child = (0, import_node_child_process4.spawn)(process.execPath, [self, "watch", "--_daemon"], {
2150
+ detached: true,
2151
+ stdio: "ignore",
2152
+ cwd: rootDir
2153
+ });
2154
+ child.unref();
2155
+ if (child.pid) {
2156
+ writePid(rootDir, child.pid);
2157
+ if (!options.silent) {
2158
+ console.log(`
2159
+ Daemon started (pid ${child.pid}).`);
2160
+ console.log(`Run 'clawstrap watch --stop' to stop it.
2161
+ `);
2162
+ }
2163
+ } else {
2164
+ console.error("\nFailed to start daemon.\n");
2165
+ process.exit(1);
2166
+ }
2167
+ }
2168
+ function persistWatchState(rootDir, updates) {
2169
+ const configPath = import_node_path20.default.join(rootDir, ".clawstrap.json");
2170
+ try {
2171
+ const raw = JSON.parse(import_node_fs19.default.readFileSync(configPath, "utf-8"));
2172
+ raw.watchState = { ...raw.watchState ?? {}, ...updates };
2173
+ import_node_fs19.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2174
+ } catch {
2175
+ }
2176
+ }
2177
+ function injectWatchHook(rootDir, config) {
2178
+ const governanceFile = import_node_path20.default.join(rootDir, "CLAUDE.md");
2179
+ if (!import_node_fs19.default.existsSync(governanceFile)) return;
2180
+ const content = import_node_fs19.default.readFileSync(governanceFile, "utf-8");
2181
+ if (content.includes("<!-- CLAWSTRAP:WATCH -->")) return;
2182
+ const _config = config;
2183
+ void _config;
2184
+ const hook = `
2185
+ <!-- CLAWSTRAP:WATCH -->
2186
+ ## Session Watch Hook
2187
+
2188
+ \`clawstrap watch\` is active. At every session end, write a session summary to
2189
+ \`tmp/sessions/YYYY-MM-DD-HHmm.md\` using this format:
2190
+
2191
+ \`\`\`
2192
+ ## Decisions
2193
+ - [what approach was chosen and why]
2194
+
2195
+ ## Corrections
2196
+ - [what the agent got wrong and how it was fixed]
2197
+
2198
+ ## Deferred Ideas
2199
+ - [mentioned but not acted on]
2200
+
2201
+ ## Open Threads
2202
+ - [unresolved questions or next steps]
2203
+ \`\`\`
2204
+
2205
+ The watch daemon picks this up automatically and updates MEMORY.md and gotcha-log.md.
2206
+ `;
2207
+ import_node_fs19.default.appendFileSync(governanceFile, hook, "utf-8");
2208
+ }
2209
+
2210
+ // src/analyze.ts
2211
+ var import_node_fs20 = __toESM(require("fs"), 1);
2212
+ var import_node_path21 = __toESM(require("path"), 1);
2213
+ init_writers();
2214
+ async function analyze() {
2215
+ const { rootDir } = loadWorkspace();
2216
+ console.log("\nScanning codebase conventions...\n");
2217
+ const sections = await runScan(rootDir);
2218
+ writeConventions(rootDir, sections);
2219
+ const configPath = import_node_path21.default.join(rootDir, ".clawstrap.json");
2220
+ try {
2221
+ const raw = JSON.parse(import_node_fs20.default.readFileSync(configPath, "utf-8"));
2222
+ raw["watchState"] = { ...raw["watchState"] ?? {}, lastScanAt: (/* @__PURE__ */ new Date()).toISOString() };
2223
+ import_node_fs20.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2224
+ } catch {
2225
+ }
2226
+ console.log(" \u2713 .claude/rules/conventions.md updated\n");
2227
+ }
2228
+
1231
2229
  // src/index.ts
1232
2230
  var program = new import_commander.Command();
1233
- program.name("clawstrap").description("Scaffold a production-ready AI agent workspace").version("1.3.0");
2231
+ program.name("clawstrap").description("Scaffold a production-ready AI agent workspace").version("1.4.1");
1234
2232
  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
2233
  await init(directory, options);
1236
2234
  });
@@ -1258,4 +2256,10 @@ Unknown format: ${options.format}. Supported: paperclip
1258
2256
  }
1259
2257
  await exportPaperclip(options);
1260
2258
  });
2259
+ 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) => {
2260
+ await watch(options);
2261
+ });
2262
+ program.command("analyze").description("Run codebase convention scan immediately").action(async () => {
2263
+ await analyze();
2264
+ });
1261
2265
  program.parse();