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