@paths.design/caws-cli 9.0.0 → 9.1.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/commands/init.d.ts.map +1 -1
- package/dist/commands/parallel.d.ts +7 -0
- package/dist/commands/parallel.d.ts.map +1 -0
- package/dist/commands/session.d.ts +7 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/specs.d.ts +6 -0
- package/dist/commands/specs.d.ts.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/tutorial.d.ts.map +1 -1
- package/dist/commands/tutorial.js +0 -2
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +42 -1
- package/dist/commands/waivers.d.ts.map +1 -1
- package/dist/constants/spec-types.d.ts +52 -0
- package/dist/constants/spec-types.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/parallel/parallel-manager.d.ts +67 -0
- package/dist/parallel/parallel-manager.d.ts.map +1 -0
- package/dist/parallel/parallel-manager.js +1 -4
- package/dist/scaffold/claude-hooks.d.ts.map +1 -1
- package/dist/scaffold/claude-hooks.js +24 -1
- package/dist/scaffold/git-hooks.d.ts.map +1 -1
- package/dist/scaffold/git-hooks.js +49 -59
- package/dist/scaffold/index.d.ts.map +1 -1
- package/dist/session/session-manager.d.ts +94 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/templates/.claude/hooks/session-log.sh +528 -0
- package/dist/templates/CLAUDE.md +7 -3
- package/dist/utils/gitignore-updater.d.ts +1 -1
- package/dist/utils/gitignore-updater.d.ts.map +1 -1
- package/dist/utils/gitignore-updater.js +3 -0
- package/dist/utils/ide-detection.d.ts +89 -0
- package/dist/utils/ide-detection.d.ts.map +1 -0
- package/dist/validation/spec-validation.d.ts.map +1 -1
- package/dist/validation/spec-validation.js +3 -10
- package/dist/worktree/worktree-manager.d.ts.map +1 -1
- package/package.json +2 -2
- package/templates/.claude/hooks/session-log.sh +528 -0
- package/templates/CLAUDE.md +7 -3
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Session Logger for Claude Code → ChatGPT Context Transfer
|
|
3
|
+
#
|
|
4
|
+
# On Stop/PreCompact: reads the full transcript from ~/.claude/ and generates:
|
|
5
|
+
# session.txt — lightweight index (header + turn list + exploration + audit)
|
|
6
|
+
# turn-001.txt — per-turn narrative (user message + reasoning + key tool output)
|
|
7
|
+
# turn-001.json — per-turn structured data (reasoning + tools + edits + results)
|
|
8
|
+
#
|
|
9
|
+
# Output: ./tmp/<session-id>/
|
|
10
|
+
#
|
|
11
|
+
# Wired into: SessionStart (metadata), Stop (generate), PreCompact (safety net)
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
INPUT=$(cat)
|
|
16
|
+
|
|
17
|
+
# --- Parse common fields ---
|
|
18
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
19
|
+
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // "unknown"')
|
|
20
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // "."')
|
|
21
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""')
|
|
22
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
23
|
+
|
|
24
|
+
# --- Log directory ---
|
|
25
|
+
LOG_DIR="${CWD}/tmp/${SESSION_ID}"
|
|
26
|
+
mkdir -p "$LOG_DIR"
|
|
27
|
+
|
|
28
|
+
SESSION_MD="$LOG_DIR/session.txt"
|
|
29
|
+
META_FILE="$LOG_DIR/.meta.json"
|
|
30
|
+
|
|
31
|
+
# ============================================================
|
|
32
|
+
# Helper: resolve transcript path
|
|
33
|
+
# ============================================================
|
|
34
|
+
resolve_transcript() {
|
|
35
|
+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
36
|
+
echo "$TRANSCRIPT_PATH"
|
|
37
|
+
return
|
|
38
|
+
fi
|
|
39
|
+
local slug
|
|
40
|
+
slug=$(echo "$CWD" | sed 's|/|-|g; s|^-||')
|
|
41
|
+
local candidate="$HOME/.claude/projects/${slug}/${SESSION_ID}.jsonl"
|
|
42
|
+
if [ -f "$candidate" ]; then
|
|
43
|
+
echo "$candidate"
|
|
44
|
+
return
|
|
45
|
+
fi
|
|
46
|
+
candidate="$HOME/.claude/projects/-${slug}/${SESSION_ID}.jsonl"
|
|
47
|
+
if [ -f "$candidate" ]; then
|
|
48
|
+
echo "$candidate"
|
|
49
|
+
return
|
|
50
|
+
fi
|
|
51
|
+
echo ""
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ============================================================
|
|
55
|
+
# Helper: make path relative to project
|
|
56
|
+
# ============================================================
|
|
57
|
+
rel_path() {
|
|
58
|
+
echo "$1" | sed "s|${CWD}/||"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# ============================================================
|
|
62
|
+
# Generate per-turn files + session.md index from transcript
|
|
63
|
+
# ============================================================
|
|
64
|
+
generate_session_output() {
|
|
65
|
+
local transcript="$1"
|
|
66
|
+
local branch head_sha dirty_count
|
|
67
|
+
branch=$(cd "$CWD" 2>/dev/null && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
68
|
+
head_sha=$(cd "$CWD" 2>/dev/null && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
69
|
+
dirty_count=$(cd "$CWD" 2>/dev/null && git status --porcelain 2>/dev/null | wc -l | tr -d ' ' || echo "0")
|
|
70
|
+
|
|
71
|
+
# --- Read metadata if available ---
|
|
72
|
+
local started_at model start_sha
|
|
73
|
+
if [ -f "$META_FILE" ]; then
|
|
74
|
+
started_at=$(jq -r '.local_time // "unknown"' "$META_FILE")
|
|
75
|
+
model=$(jq -r '.model // "unknown"' "$META_FILE")
|
|
76
|
+
start_sha=$(jq -r '.head_sha // ""' "$META_FILE")
|
|
77
|
+
else
|
|
78
|
+
started_at="(resumed session)"
|
|
79
|
+
model="unknown"
|
|
80
|
+
start_sha=""
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
if [ -z "$transcript" ] || [ ! -f "$transcript" ]; then
|
|
84
|
+
cat > "$SESSION_MD" << MDEOF
|
|
85
|
+
# Session Log: $(basename "$CWD")
|
|
86
|
+
|
|
87
|
+
| Field | Value |
|
|
88
|
+
|-------|-------|
|
|
89
|
+
| Session ID | \`${SESSION_ID}\` |
|
|
90
|
+
| Started | ${started_at} |
|
|
91
|
+
| Model | ${model} |
|
|
92
|
+
| Branch | \`${branch}\` @ \`${head_sha}\` |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
_No transcript found. Narrative extraction unavailable._
|
|
97
|
+
MDEOF
|
|
98
|
+
return
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# --- Generate per-turn files via python ---
|
|
102
|
+
# jq emits each content block as a separate chronological event.
|
|
103
|
+
# Python accumulates into turns and writes sequential timeline per turn.
|
|
104
|
+
jq -c '
|
|
105
|
+
if .type == "user" then
|
|
106
|
+
if (.message.content | type) == "string" then
|
|
107
|
+
{ev: "user_text", text: .message.content}
|
|
108
|
+
elif (.message.content | type) == "array" then
|
|
109
|
+
.message.content[]? |
|
|
110
|
+
if .type == "tool_result" then
|
|
111
|
+
{ev: "tool_result", id: .tool_use_id, content: ((.content // "") | tostring), is_error: (.is_error // false)}
|
|
112
|
+
else
|
|
113
|
+
empty
|
|
114
|
+
end
|
|
115
|
+
else
|
|
116
|
+
empty
|
|
117
|
+
end
|
|
118
|
+
elif .type == "assistant" then
|
|
119
|
+
.message.content[]? |
|
|
120
|
+
if .type == "text" then
|
|
121
|
+
{ev: "text", text: .text}
|
|
122
|
+
elif .type == "tool_use" then
|
|
123
|
+
{ev: "tool_use", name, id,
|
|
124
|
+
file: (.input.file_path // null),
|
|
125
|
+
command: (.input.command // null),
|
|
126
|
+
description: (.input.description // null),
|
|
127
|
+
pattern: (.input.pattern // null),
|
|
128
|
+
prompt: (.input.prompt // null),
|
|
129
|
+
subagent_type: (.input.subagent_type // null)}
|
|
130
|
+
else
|
|
131
|
+
empty
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
empty
|
|
135
|
+
end
|
|
136
|
+
' "$transcript" 2>/dev/null > "$LOG_DIR/.events.jsonl"
|
|
137
|
+
|
|
138
|
+
# Write python script to temp file (can't pipe + heredoc simultaneously)
|
|
139
|
+
local pyscript
|
|
140
|
+
pyscript=$(mktemp "${TMPDIR:-/tmp}/session-log-XXXX.py")
|
|
141
|
+
trap "rm -f '$pyscript'" RETURN
|
|
142
|
+
cat > "$pyscript" << 'PYEOF'
|
|
143
|
+
import json, sys, os
|
|
144
|
+
|
|
145
|
+
log_dir = sys.argv[1]
|
|
146
|
+
cwd = sys.argv[2]
|
|
147
|
+
session_id = sys.argv[3]
|
|
148
|
+
started_at = sys.argv[4]
|
|
149
|
+
model = sys.argv[5]
|
|
150
|
+
branch = sys.argv[6]
|
|
151
|
+
head_sha = sys.argv[7]
|
|
152
|
+
dirty_count = sys.argv[8]
|
|
153
|
+
start_sha = sys.argv[9]
|
|
154
|
+
|
|
155
|
+
def rel(path):
|
|
156
|
+
if path and path.startswith(cwd + "/"):
|
|
157
|
+
return path[len(cwd) + 1:]
|
|
158
|
+
return path or ""
|
|
159
|
+
|
|
160
|
+
# ---- Accumulate turns as chronological event timelines ----
|
|
161
|
+
turns = []
|
|
162
|
+
# Each turn: {user, timeline: [{kind, ...}, ...], edits, reads, searches, commands}
|
|
163
|
+
current = {"user": None, "timeline": [], "edits": [], "reads": [], "searches": [], "commands": []}
|
|
164
|
+
|
|
165
|
+
def new_turn(user_text):
|
|
166
|
+
return {
|
|
167
|
+
"user": user_text if user_text else None,
|
|
168
|
+
"timeline": [], "edits": [], "reads": [], "searches": [], "commands": [],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Track pending tool_use IDs to match with results
|
|
172
|
+
pending_tools = {} # id -> {name, ...}
|
|
173
|
+
|
|
174
|
+
NOISE_PREFIXES = ("<local-command", "<command-name", "<local-command-stdout",
|
|
175
|
+
"<local-command-caveat", "This session is being continued")
|
|
176
|
+
|
|
177
|
+
# Keywords that make a tool result "notable" (worth showing inline)
|
|
178
|
+
NOTABLE_KW = ["error", "fail", "refusal", "mismatch", "passed", "assert",
|
|
179
|
+
"traceback", "exception", "pytest", "PASSED", "FAILED", "TypedRefusal"]
|
|
180
|
+
|
|
181
|
+
for line in sys.stdin:
|
|
182
|
+
try:
|
|
183
|
+
entry = json.loads(line)
|
|
184
|
+
except json.JSONDecodeError:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
ev = entry.get("ev")
|
|
188
|
+
|
|
189
|
+
if ev == "user_text":
|
|
190
|
+
text = entry["text"]
|
|
191
|
+
if any(text.startswith(p) for p in NOISE_PREFIXES):
|
|
192
|
+
continue
|
|
193
|
+
if not text.strip():
|
|
194
|
+
continue
|
|
195
|
+
if current["user"] or current["timeline"]:
|
|
196
|
+
turns.append(current)
|
|
197
|
+
current = new_turn(text)
|
|
198
|
+
|
|
199
|
+
elif ev == "text":
|
|
200
|
+
text = entry.get("text", "")
|
|
201
|
+
if len(text) > 20:
|
|
202
|
+
current["timeline"].append({"kind": "reasoning", "text": text})
|
|
203
|
+
|
|
204
|
+
elif ev == "tool_use":
|
|
205
|
+
name = entry.get("name", "")
|
|
206
|
+
tid = entry.get("id", "")
|
|
207
|
+
tool_entry = {"kind": "tool_call", "name": name, "id": tid}
|
|
208
|
+
|
|
209
|
+
if name in ("Write", "Edit"):
|
|
210
|
+
f = rel(entry.get("file"))
|
|
211
|
+
tool_entry["file"] = f
|
|
212
|
+
if f and f not in current["edits"]:
|
|
213
|
+
current["edits"].append(f)
|
|
214
|
+
elif name == "Read":
|
|
215
|
+
f = rel(entry.get("file"))
|
|
216
|
+
tool_entry["file"] = f
|
|
217
|
+
if f and f not in current["reads"]:
|
|
218
|
+
current["reads"].append(f)
|
|
219
|
+
elif name in ("Grep", "Glob"):
|
|
220
|
+
pat = entry.get("pattern", "")
|
|
221
|
+
tool_entry["pattern"] = pat
|
|
222
|
+
if pat:
|
|
223
|
+
current["searches"].append(pat)
|
|
224
|
+
elif name == "Bash":
|
|
225
|
+
cmd = entry.get("command", "")
|
|
226
|
+
desc = entry.get("description", "")
|
|
227
|
+
tool_entry["command"] = cmd
|
|
228
|
+
tool_entry["description"] = desc or ""
|
|
229
|
+
if cmd:
|
|
230
|
+
current["commands"].append({"cmd": cmd, "desc": desc or ""})
|
|
231
|
+
elif name == "Task":
|
|
232
|
+
tool_entry["prompt"] = entry.get("prompt", "")
|
|
233
|
+
tool_entry["subagent_type"] = entry.get("subagent_type", "")
|
|
234
|
+
|
|
235
|
+
current["timeline"].append(tool_entry)
|
|
236
|
+
pending_tools[tid] = tool_entry
|
|
237
|
+
|
|
238
|
+
elif ev == "tool_result":
|
|
239
|
+
tid = entry.get("id", "")
|
|
240
|
+
content = entry.get("content", "")
|
|
241
|
+
is_error = entry.get("is_error", False)
|
|
242
|
+
tool_info = pending_tools.get(tid, {})
|
|
243
|
+
name = tool_info.get("name", "unknown")
|
|
244
|
+
|
|
245
|
+
# Decide if this result is notable enough to show inline
|
|
246
|
+
# Task results are always notable (subagent did substantive work)
|
|
247
|
+
notable = is_error or name == "Task"
|
|
248
|
+
if not notable and content:
|
|
249
|
+
content_lower = content.lower()
|
|
250
|
+
notable = any(kw.lower() in content_lower for kw in NOTABLE_KW)
|
|
251
|
+
|
|
252
|
+
if notable and content:
|
|
253
|
+
# Cap file-content tools (full file reads/writes blow out turn files)
|
|
254
|
+
display = content
|
|
255
|
+
if name in ("Read", "Write", "Edit") and len(content) > 2000:
|
|
256
|
+
display = content[:2000] + "\n...(file content truncated)"
|
|
257
|
+
# Graft result onto the original tool_call entry (not a separate timeline item)
|
|
258
|
+
if tool_info:
|
|
259
|
+
tool_info["output"] = display
|
|
260
|
+
tool_info["is_error"] = is_error
|
|
261
|
+
else:
|
|
262
|
+
# Orphan result (no matching call) — append standalone
|
|
263
|
+
current["timeline"].append({
|
|
264
|
+
"kind": "tool_output",
|
|
265
|
+
"name": name,
|
|
266
|
+
"content": display,
|
|
267
|
+
"is_error": is_error,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
if current["user"] or current["timeline"]:
|
|
271
|
+
turns.append(current)
|
|
272
|
+
|
|
273
|
+
# ---- Write per-turn files ----
|
|
274
|
+
turn_index = []
|
|
275
|
+
|
|
276
|
+
for i, turn in enumerate(turns):
|
|
277
|
+
num = i + 1
|
|
278
|
+
padded = f"{num:03d}"
|
|
279
|
+
|
|
280
|
+
# --- Build per-turn markdown: chronological timeline ---
|
|
281
|
+
md_lines = [f"# Turn {num}", ""]
|
|
282
|
+
|
|
283
|
+
if turn["user"]:
|
|
284
|
+
md_lines.extend([f"> ---user---\n{turn['user']}\n---/user---", ""])
|
|
285
|
+
|
|
286
|
+
for event in turn["timeline"]:
|
|
287
|
+
kind = event["kind"]
|
|
288
|
+
|
|
289
|
+
if kind == "reasoning":
|
|
290
|
+
text = event["text"]
|
|
291
|
+
md_lines.append(text)
|
|
292
|
+
md_lines.extend(["", "---", ""])
|
|
293
|
+
|
|
294
|
+
elif kind == "tool_call":
|
|
295
|
+
name = event.get("name", "")
|
|
296
|
+
if name in ("Read", "Glob"):
|
|
297
|
+
f = event.get("file") or event.get("pattern", "")
|
|
298
|
+
md_lines.append(f"`{name}` {f}")
|
|
299
|
+
elif name in ("Write", "Edit"):
|
|
300
|
+
md_lines.append(f"`{name}` {event.get('file', '')}")
|
|
301
|
+
elif name == "Bash":
|
|
302
|
+
cmd = event.get("command", "")
|
|
303
|
+
desc = event.get("description", "")
|
|
304
|
+
header = f"`Bash` _{desc}_" if desc else "`Bash`"
|
|
305
|
+
if len(cmd) > 120:
|
|
306
|
+
md_lines.extend([header, "```", cmd, "```"])
|
|
307
|
+
else:
|
|
308
|
+
md_lines.append(f"{header} `{cmd}`" if cmd else header)
|
|
309
|
+
elif name in ("Grep",):
|
|
310
|
+
md_lines.append(f"`Grep` {event.get('pattern', '')}")
|
|
311
|
+
elif name == "Task":
|
|
312
|
+
sa = event.get("subagent_type", "subagent")
|
|
313
|
+
prompt = event.get("prompt", "")
|
|
314
|
+
header = f"`Task` ({sa})" if sa else "`Task` (subagent)"
|
|
315
|
+
if prompt:
|
|
316
|
+
# Show the dispatch prompt so readers know what the subagent was asked
|
|
317
|
+
short_prompt = prompt[:500]
|
|
318
|
+
if len(prompt) > 500:
|
|
319
|
+
short_prompt += "..."
|
|
320
|
+
md_lines.extend([header, "", f"> {short_prompt}", ""])
|
|
321
|
+
else:
|
|
322
|
+
md_lines.append(f"`{name}`")
|
|
323
|
+
md_lines.append("")
|
|
324
|
+
|
|
325
|
+
# If tool result was grafted onto this call, render it inline
|
|
326
|
+
if "output" in event:
|
|
327
|
+
output = event["output"]
|
|
328
|
+
is_error = event.get("is_error", False)
|
|
329
|
+
label = "error" if is_error else "output"
|
|
330
|
+
md_lines.extend([
|
|
331
|
+
f"**{name}** ({label}):",
|
|
332
|
+
"```",
|
|
333
|
+
output,
|
|
334
|
+
"```",
|
|
335
|
+
"",
|
|
336
|
+
])
|
|
337
|
+
|
|
338
|
+
elif kind == "tool_output":
|
|
339
|
+
# Orphan result (no matching call found) — render standalone
|
|
340
|
+
content = event.get("content", "")
|
|
341
|
+
name = event.get("name", "")
|
|
342
|
+
is_error = event.get("is_error", False)
|
|
343
|
+
label = "error" if is_error else "output"
|
|
344
|
+
md_lines.extend([
|
|
345
|
+
f"**{name}** ({label}):",
|
|
346
|
+
"```",
|
|
347
|
+
content,
|
|
348
|
+
"```",
|
|
349
|
+
"",
|
|
350
|
+
])
|
|
351
|
+
|
|
352
|
+
# Write turn markdown
|
|
353
|
+
with open(os.path.join(log_dir, f"turn-{padded}.txt"), "w") as f:
|
|
354
|
+
f.write("\n".join(md_lines))
|
|
355
|
+
|
|
356
|
+
# --- Build per-turn JSON: chronological timeline ---
|
|
357
|
+
tool_summary = {}
|
|
358
|
+
for event in turn["timeline"]:
|
|
359
|
+
if event["kind"] == "tool_call":
|
|
360
|
+
n = event.get("name", "")
|
|
361
|
+
tool_summary[n] = tool_summary.get(n, 0) + 1
|
|
362
|
+
|
|
363
|
+
def group_by_ext(paths):
|
|
364
|
+
groups = {}
|
|
365
|
+
for p in paths:
|
|
366
|
+
ext = os.path.splitext(p)[1] or "(no ext)"
|
|
367
|
+
groups.setdefault(ext, []).append(p)
|
|
368
|
+
return groups
|
|
369
|
+
|
|
370
|
+
turn_json = {
|
|
371
|
+
"turn": num,
|
|
372
|
+
"user": turn["user"],
|
|
373
|
+
"timeline": turn["timeline"],
|
|
374
|
+
"tool_summary": tool_summary,
|
|
375
|
+
"files_edited": group_by_ext(turn["edits"]),
|
|
376
|
+
"files_read": group_by_ext(turn["reads"]),
|
|
377
|
+
"searches": turn["searches"],
|
|
378
|
+
"commands": [c["cmd"] for c in turn["commands"]],
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
with open(os.path.join(log_dir, f"turn-{padded}.json"), "w") as f:
|
|
382
|
+
json.dump(turn_json, f, indent=2)
|
|
383
|
+
|
|
384
|
+
# Index entry
|
|
385
|
+
user_preview = (turn["user"] or "(no user message)")[:120]
|
|
386
|
+
reasoning_count = sum(1 for e in turn["timeline"] if e["kind"] == "reasoning")
|
|
387
|
+
tool_count = sum(1 for e in turn["timeline"] if e["kind"] == "tool_call")
|
|
388
|
+
turn_index.append({
|
|
389
|
+
"num": num,
|
|
390
|
+
"padded": padded,
|
|
391
|
+
"user_preview": user_preview,
|
|
392
|
+
"reasoning_count": reasoning_count,
|
|
393
|
+
"tool_count": tool_count,
|
|
394
|
+
"edits": turn["edits"],
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
# ---- Write session.md index ----
|
|
398
|
+
with open(os.path.join(log_dir, "session.txt"), "w") as f:
|
|
399
|
+
f.write(f"# Session Log: {os.path.basename(cwd)}\n\n")
|
|
400
|
+
f.write("| Field | Value |\n")
|
|
401
|
+
f.write("|-------|-------|\n")
|
|
402
|
+
f.write(f"| Session ID | `{session_id}` |\n")
|
|
403
|
+
f.write(f"| Started | {started_at} |\n")
|
|
404
|
+
f.write(f"| Model | {model} |\n")
|
|
405
|
+
f.write(f"| Branch | `{branch}` @ `{head_sha}` |\n")
|
|
406
|
+
f.write(f"| Turns | {len(turn_index)} |\n")
|
|
407
|
+
f.write("\n---\n\n")
|
|
408
|
+
|
|
409
|
+
f.write("## Turns\n\n")
|
|
410
|
+
for t in turn_index:
|
|
411
|
+
edits_str = ", ".join(f"`{e}`" for e in t["edits"][:3])
|
|
412
|
+
if len(t["edits"]) > 3:
|
|
413
|
+
edits_str += f" +{len(t['edits'])-3} more"
|
|
414
|
+
summary = f"{t['reasoning_count']} msgs, {t['tool_count']} tools"
|
|
415
|
+
if edits_str:
|
|
416
|
+
summary += f" | {edits_str}"
|
|
417
|
+
f.write(f"- **[Turn {t['num']}](turn-{t['padded']}.md)** — {t['user_preview']}\n")
|
|
418
|
+
f.write(f" _{summary}_\n")
|
|
419
|
+
|
|
420
|
+
f.write("\n---\n\n")
|
|
421
|
+
|
|
422
|
+
# Exploration summary (deduplicated across all turns)
|
|
423
|
+
all_reads = []
|
|
424
|
+
all_searches = []
|
|
425
|
+
all_edits = []
|
|
426
|
+
all_commands = []
|
|
427
|
+
for turn in turns:
|
|
428
|
+
all_reads.extend(turn["reads"])
|
|
429
|
+
all_searches.extend(turn["searches"])
|
|
430
|
+
all_edits.extend(turn["edits"])
|
|
431
|
+
all_commands.extend(turn["commands"])
|
|
432
|
+
|
|
433
|
+
f.write("## Exploration\n")
|
|
434
|
+
f.write("_Files read and searches performed (deduplicated)._\n\n")
|
|
435
|
+
for r in sorted(set(all_reads)):
|
|
436
|
+
f.write(f"- READ `{r}`\n")
|
|
437
|
+
for s in sorted(set(all_searches)):
|
|
438
|
+
f.write(f"- SEARCH `{s}`\n")
|
|
439
|
+
f.write("\n")
|
|
440
|
+
|
|
441
|
+
f.write("## Audit\n")
|
|
442
|
+
f.write("_Edits, commands, git activity._\n\n")
|
|
443
|
+
for e in sorted(set(all_edits)):
|
|
444
|
+
f.write(f"- EDIT `{e}`\n")
|
|
445
|
+
for cmd in all_commands:
|
|
446
|
+
short = cmd["cmd"][:120]
|
|
447
|
+
# Only log meaningful commands
|
|
448
|
+
meaningful = any(kw in short for kw in [
|
|
449
|
+
"pytest", "cargo test", "ruff", "mypy", "npm test",
|
|
450
|
+
"git log", "git diff", "git status", "git add", "git commit",
|
|
451
|
+
"git merge", "caws ", "pip install", "make", "cargo build"
|
|
452
|
+
])
|
|
453
|
+
if meaningful:
|
|
454
|
+
if cmd["desc"]:
|
|
455
|
+
f.write(f"- BASH `{short}` — {cmd['desc']}\n")
|
|
456
|
+
else:
|
|
457
|
+
f.write(f"- BASH `{short}`\n")
|
|
458
|
+
f.write("\n")
|
|
459
|
+
|
|
460
|
+
f.write("## Session Snapshot\n\n")
|
|
461
|
+
f.write("| Field | Value |\n")
|
|
462
|
+
f.write("|-------|-------|\n")
|
|
463
|
+
f.write(f"| Branch | `{branch}` @ `{head_sha}` |\n")
|
|
464
|
+
f.write(f"| Dirty files | {dirty_count} |\n")
|
|
465
|
+
f.write(f"| Total turns | {len(turn_index)} |\n")
|
|
466
|
+
|
|
467
|
+
PYEOF
|
|
468
|
+
|
|
469
|
+
# Run the python script with events as input
|
|
470
|
+
python3 "$pyscript" "$LOG_DIR" "$CWD" "$SESSION_ID" "$started_at" "$model" "$branch" "$head_sha" "$dirty_count" "$start_sha" < "$LOG_DIR/.events.jsonl"
|
|
471
|
+
rm -f "$LOG_DIR/.events.jsonl"
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
# ============================================================
|
|
475
|
+
# EVENT: SessionStart — save metadata
|
|
476
|
+
# ============================================================
|
|
477
|
+
handle_session_start() {
|
|
478
|
+
local model source branch head_sha dirty_count full_time
|
|
479
|
+
model=$(echo "$INPUT" | jq -r '.model // "unknown"')
|
|
480
|
+
source=$(echo "$INPUT" | jq -r '.source // "unknown"')
|
|
481
|
+
branch=$(cd "$CWD" 2>/dev/null && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
482
|
+
head_sha=$(cd "$CWD" 2>/dev/null && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
483
|
+
dirty_count=$(cd "$CWD" 2>/dev/null && git status --porcelain 2>/dev/null | wc -l | tr -d ' ' || echo "0")
|
|
484
|
+
full_time=$(date +"%Y-%m-%d %H:%M:%S %Z")
|
|
485
|
+
|
|
486
|
+
jq -cn \
|
|
487
|
+
--arg sid "$SESSION_ID" \
|
|
488
|
+
--arg ts "$TIMESTAMP" \
|
|
489
|
+
--arg lt "$full_time" \
|
|
490
|
+
--arg model "$model" \
|
|
491
|
+
--arg source "$source" \
|
|
492
|
+
--arg branch "$branch" \
|
|
493
|
+
--arg head "$head_sha" \
|
|
494
|
+
--arg dirty "$dirty_count" \
|
|
495
|
+
--arg project "$(basename "$CWD")" \
|
|
496
|
+
--arg transcript "$TRANSCRIPT_PATH" \
|
|
497
|
+
'{session_id: $sid, started_at: $ts, local_time: $lt, model: $model, source: $source, branch: $branch, head_sha: $head, dirty_files: $dirty, project: $project, transcript_path: $transcript}' \
|
|
498
|
+
> "$META_FILE"
|
|
499
|
+
|
|
500
|
+
# Generate initial output (may be empty if transcript not ready)
|
|
501
|
+
generate_session_output "$(resolve_transcript)"
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
# ============================================================
|
|
505
|
+
# EVENT: Stop — regenerate from transcript
|
|
506
|
+
# ============================================================
|
|
507
|
+
handle_stop() {
|
|
508
|
+
generate_session_output "$(resolve_transcript)"
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
# ============================================================
|
|
512
|
+
# EVENT: PreCompact — safety net before context eviction
|
|
513
|
+
# ============================================================
|
|
514
|
+
handle_pre_compact() {
|
|
515
|
+
generate_session_output "$(resolve_transcript)"
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
# ============================================================
|
|
519
|
+
# DISPATCH
|
|
520
|
+
# ============================================================
|
|
521
|
+
case "$HOOK_EVENT" in
|
|
522
|
+
SessionStart) handle_session_start ;;
|
|
523
|
+
Stop) handle_stop ;;
|
|
524
|
+
PreCompact) handle_pre_compact ;;
|
|
525
|
+
*) ;; # Other events: no-op
|
|
526
|
+
esac
|
|
527
|
+
|
|
528
|
+
exit 0
|
package/dist/templates/CLAUDE.md
CHANGED
|
@@ -38,15 +38,19 @@ caws agent evaluate
|
|
|
38
38
|
|
|
39
39
|
### Working Spec
|
|
40
40
|
|
|
41
|
-
The project spec lives at `.caws/working-spec.yaml`. It defines:
|
|
41
|
+
The project spec lives at `.caws/working-spec.yaml`. Feature specs live at `.caws/specs/<ID>.yaml`. It defines:
|
|
42
42
|
|
|
43
43
|
- **Risk tier**: Quality requirements (T1: critical, T2: standard, T3: low risk)
|
|
44
44
|
- **Scope**: Which files you can edit (`scope.in`) and which are off-limits (`scope.out`)
|
|
45
|
-
- **Change budget**: Max files and lines of code per change
|
|
46
|
-
- **Acceptance criteria**: What "done" means
|
|
45
|
+
- **Change budget**: Max files and lines of code per change (see note below)
|
|
46
|
+
- **Acceptance criteria**: What "done" means — IDs must match `^A\d+$` (e.g. `A1`, `A12`)
|
|
47
47
|
|
|
48
48
|
Always stay within scope boundaries and change budgets.
|
|
49
49
|
|
|
50
|
+
> **Budget note**: `change_budget:` in a spec is informational documentation only. CAWS
|
|
51
|
+
> derives the enforced budget from `policy.yaml` keyed on `risk_tier`. The field in the
|
|
52
|
+
> spec is not used by `caws validate` for enforcement.
|
|
53
|
+
|
|
50
54
|
### Quality Gates
|
|
51
55
|
|
|
52
56
|
Quality requirements are tiered:
|
|
@@ -35,5 +35,5 @@ export function verifyGitignore(projectRoot: string): Promise<boolean>;
|
|
|
35
35
|
* - Logs (caws.log, debug logs)
|
|
36
36
|
* - Local overrides (caws.local.*)
|
|
37
37
|
*/
|
|
38
|
-
export const CAWS_GITIGNORE_ENTRIES: "\n# CAWS Local Runtime Data (developer-specific, should not be tracked)\n# ====================================================================\n# Note: Specs, policy, waivers, provenance, and plans ARE tracked for team collaboration\n# Only local agent tracking, generated tools, and temporary files are ignored\n\n# Agent runtime tracking (local to each developer)\n.agent/\n\n# CAWS tools (now in .caws/tools/)\n.caws/tools/\n# Legacy location (for backward compatibility)\napps/tools/caws/\n\n# Temporary CAWS files\n**/*.caws.tmp\n**/*.working-spec.bak\n.caws/*.tmp\n.caws/*.bak\n\n# CAWS logs (local debugging)\ncaws-debug.log*\n**/caws.log\n.caws/*.log\n\n# Local development overrides (developer-specific)\ncaws.local.*\n.caws/local.*\n\n# CAWS Worktrees (local, should not be tracked)\n.caws/worktrees/\n.caws/worktrees.json\n";
|
|
38
|
+
export const CAWS_GITIGNORE_ENTRIES: "\n# CAWS Local Runtime Data (developer-specific, should not be tracked)\n# ====================================================================\n# Note: Specs, policy, waivers, provenance, and plans ARE tracked for team collaboration\n# Only local agent tracking, generated tools, and temporary files are ignored\n\n# Agent runtime tracking (local to each developer)\n.agent/\n\n# CAWS tools (now in .caws/tools/)\n.caws/tools/\n# Legacy location (for backward compatibility)\napps/tools/caws/\n\n# Temporary CAWS files\n**/*.caws.tmp\n**/*.working-spec.bak\n.caws/*.tmp\n.caws/*.bak\n\n# CAWS logs (local debugging)\ncaws-debug.log*\n**/caws.log\n.caws/*.log\n\n# Local development overrides (developer-specific)\ncaws.local.*\n.caws/local.*\n\n# CAWS Worktrees (local, should not be tracked)\n.caws/worktrees/\n.caws/worktrees.json\n\n# Session transcripts (generated by session-log hook)\ntmp/\n";
|
|
39
39
|
//# sourceMappingURL=gitignore-updater.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gitignore-updater.d.ts","sourceRoot":"","sources":["../../src/utils/gitignore-updater.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gitignore-updater.d.ts","sourceRoot":"","sources":["../../src/utils/gitignore-updater.js"],"names":[],"mappings":"AAoEA;;;;;;GAMG;AACH,6CALW,MAAM,YAEd;IAAyB,KAAK,EAAtB,OAAO;CACf,GAAU,OAAO,CAAC,OAAO,CAAC,CA2D5B;AAED;;;;GAIG;AACH,6CAHW,MAAM,GACJ,OAAO,CAAC,OAAO,CAAC,CAW5B;AA1ID;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,06BAmCE"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export namespace IDE_REGISTRY {
|
|
2
|
+
namespace cursor {
|
|
3
|
+
let id: string;
|
|
4
|
+
let name: string;
|
|
5
|
+
let description: string;
|
|
6
|
+
let envVars: string[];
|
|
7
|
+
}
|
|
8
|
+
namespace claude {
|
|
9
|
+
let id_1: string;
|
|
10
|
+
export { id_1 as id };
|
|
11
|
+
let name_1: string;
|
|
12
|
+
export { name_1 as name };
|
|
13
|
+
let description_1: string;
|
|
14
|
+
export { description_1 as description };
|
|
15
|
+
let envVars_1: string[];
|
|
16
|
+
export { envVars_1 as envVars };
|
|
17
|
+
}
|
|
18
|
+
namespace vscode {
|
|
19
|
+
let id_2: string;
|
|
20
|
+
export { id_2 as id };
|
|
21
|
+
let name_2: string;
|
|
22
|
+
export { name_2 as name };
|
|
23
|
+
let description_2: string;
|
|
24
|
+
export { description_2 as description };
|
|
25
|
+
let envVars_2: string[];
|
|
26
|
+
export { envVars_2 as envVars };
|
|
27
|
+
}
|
|
28
|
+
namespace intellij {
|
|
29
|
+
let id_3: string;
|
|
30
|
+
export { id_3 as id };
|
|
31
|
+
let name_3: string;
|
|
32
|
+
export { name_3 as name };
|
|
33
|
+
let description_3: string;
|
|
34
|
+
export { description_3 as description };
|
|
35
|
+
let envVars_3: string[];
|
|
36
|
+
export { envVars_3 as envVars };
|
|
37
|
+
}
|
|
38
|
+
namespace windsurf {
|
|
39
|
+
let id_4: string;
|
|
40
|
+
export { id_4 as id };
|
|
41
|
+
let name_4: string;
|
|
42
|
+
export { name_4 as name };
|
|
43
|
+
let description_4: string;
|
|
44
|
+
export { description_4 as description };
|
|
45
|
+
let envVars_4: string[];
|
|
46
|
+
export { envVars_4 as envVars };
|
|
47
|
+
}
|
|
48
|
+
namespace copilot {
|
|
49
|
+
let id_5: string;
|
|
50
|
+
export { id_5 as id };
|
|
51
|
+
let name_5: string;
|
|
52
|
+
export { name_5 as name };
|
|
53
|
+
let description_5: string;
|
|
54
|
+
export { description_5 as description };
|
|
55
|
+
let envVars_5: any[];
|
|
56
|
+
export { envVars_5 as envVars };
|
|
57
|
+
}
|
|
58
|
+
namespace junie {
|
|
59
|
+
let id_6: string;
|
|
60
|
+
export { id_6 as id };
|
|
61
|
+
let name_6: string;
|
|
62
|
+
export { name_6 as name };
|
|
63
|
+
let description_6: string;
|
|
64
|
+
export { description_6 as description };
|
|
65
|
+
let envVars_6: any[];
|
|
66
|
+
export { envVars_6 as envVars };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export const ALL_IDE_IDS: string[];
|
|
70
|
+
/**
|
|
71
|
+
* Detect currently active IDEs from environment variables.
|
|
72
|
+
* @returns {string[]} Array of detected IDE identifiers
|
|
73
|
+
*/
|
|
74
|
+
export function detectActiveIDEs(): string[];
|
|
75
|
+
/**
|
|
76
|
+
* Get recommended IDE set based on detection and natural pairings.
|
|
77
|
+
* - Cursor detected -> also recommend Claude Code
|
|
78
|
+
* - VS Code detected -> also recommend Copilot
|
|
79
|
+
* - Nothing detected -> default to cursor + claude (AI-first set)
|
|
80
|
+
* @returns {string[]} Array of recommended IDE identifiers
|
|
81
|
+
*/
|
|
82
|
+
export function getRecommendedIDEs(): string[];
|
|
83
|
+
/**
|
|
84
|
+
* Parse an IDE selection from a CLI flag value or prompt answer.
|
|
85
|
+
* @param {string|string[]} input - Comma-separated string or array of IDE ids
|
|
86
|
+
* @returns {string[]} Normalized, validated array of IDE identifiers
|
|
87
|
+
*/
|
|
88
|
+
export function parseIDESelection(input: string | string[]): string[];
|
|
89
|
+
//# sourceMappingURL=ide-detection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ide-detection.d.ts","sourceRoot":"","sources":["../../src/utils/ide-detection.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,mCAA8C;AAE9C;;;GAGG;AACH,oCAFa,MAAM,EAAE,CAUpB;AAED;;;;;;GAMG;AACH,sCAFa,MAAM,EAAE,CAcpB;AAED;;;;GAIG;AACH,yCAHW,MAAM,GAAC,MAAM,EAAE,GACb,MAAM,EAAE,CA0BpB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec-validation.d.ts","sourceRoot":"","sources":["../../src/validation/spec-validation.js"],"names":[],"mappings":"AA6DA;;;;;GAKG;AACH,
|
|
1
|
+
{"version":3,"file":"spec-validation.d.ts","sourceRoot":"","sources":["../../src/validation/spec-validation.js"],"names":[],"mappings":"AA6DA;;;;;GAKG;AACH,mEA8IC;AAED;;;;;GAKG;AACH,kFAycC;AAoCD;;;;;GAKG;AACH,0CAJW,MAAM,eAEJ,MAAM,CAkBlB;AAED;;;;;GAKG;AACH,uCAJW,MAAM,eAEJ,OAAO,CAKnB;AAnED;;;;;;GAMG;AACH,0EAFa,MAAM,CAclB;AAED;;;;GAIG;AACH,0CAHW,MAAM,GACJ,MAAM,CAQlB"}
|