@paths.design/caws-cli 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/commands/init.d.ts.map +1 -1
  2. package/dist/commands/parallel.d.ts +7 -0
  3. package/dist/commands/parallel.d.ts.map +1 -0
  4. package/dist/commands/session.d.ts +7 -0
  5. package/dist/commands/session.d.ts.map +1 -0
  6. package/dist/commands/specs.d.ts +6 -0
  7. package/dist/commands/specs.d.ts.map +1 -1
  8. package/dist/commands/status.d.ts.map +1 -1
  9. package/dist/commands/tutorial.js +0 -2
  10. package/dist/commands/waivers.d.ts.map +1 -1
  11. package/dist/constants/spec-types.d.ts +52 -0
  12. package/dist/constants/spec-types.d.ts.map +1 -1
  13. package/dist/parallel/parallel-manager.d.ts +67 -0
  14. package/dist/parallel/parallel-manager.d.ts.map +1 -0
  15. package/dist/parallel/parallel-manager.js +1 -4
  16. package/dist/scaffold/claude-hooks.d.ts.map +1 -1
  17. package/dist/scaffold/claude-hooks.js +24 -1
  18. package/dist/scaffold/git-hooks.d.ts.map +1 -1
  19. package/dist/scaffold/git-hooks.js +49 -59
  20. package/dist/scaffold/index.d.ts.map +1 -1
  21. package/dist/session/session-manager.d.ts +94 -0
  22. package/dist/session/session-manager.d.ts.map +1 -0
  23. package/dist/templates/.claude/hooks/session-log.sh +528 -0
  24. package/dist/utils/gitignore-updater.d.ts +1 -1
  25. package/dist/utils/gitignore-updater.d.ts.map +1 -1
  26. package/dist/utils/gitignore-updater.js +3 -0
  27. package/dist/utils/ide-detection.d.ts +89 -0
  28. package/dist/utils/ide-detection.d.ts.map +1 -0
  29. package/dist/validation/spec-validation.d.ts.map +1 -1
  30. package/dist/worktree/worktree-manager.d.ts.map +1 -1
  31. package/package.json +2 -2
  32. package/templates/.claude/hooks/session-log.sh +528 -0
@@ -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
@@ -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":"AAiEA;;;;;;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;AAvID;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,22BAgCE"}
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"}
@@ -61,6 +61,9 @@ caws.local.*
61
61
  # CAWS Worktrees (local, should not be tracked)
62
62
  .caws/worktrees/
63
63
  .caws/worktrees.json
64
+
65
+ # Session transcripts (generated by session-log hook)
66
+ tmp/
64
67
  `;
65
68
 
66
69
  /**
@@ -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,mEA8HC;AAED;;;;;GAKG;AACH,kFAgdC;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"}
1
+ {"version":3,"file":"spec-validation.d.ts","sourceRoot":"","sources":["../../src/validation/spec-validation.js"],"names":[],"mappings":"AA6DA;;;;;GAKG;AACH,mEA8IC;AAED;;;;;GAKG;AACH,kFAgdC;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"}
@@ -1 +1 @@
1
- {"version":3,"file":"worktree-manager.d.ts","sourceRoot":"","sources":["../../src/worktree/worktree-manager.js"],"names":[],"mappings":"AA+DA;;;;;;;;GAQG;AACH,qCAPW,MAAM,YAEd;IAAyB,KAAK,GAAtB,MAAM;IACW,UAAU,GAA3B,MAAM;IACW,MAAM,GAAvB,MAAM;CACd,OA4IF;AAED;;;GAGG;AACH,uCAqCC;AAED;;;;;;GAMG;AACH,sCALW,MAAM,YAEd;IAA0B,YAAY,GAA9B,OAAO;IACW,KAAK,GAAvB,OAAO;CACjB,QAqDA;AAED;;;;;GAKG;AACH,yCAHG;IAAyB,UAAU,GAA3B,MAAM;CACd,SA6CF;AA1UD;;;;GAIG;AACH,mCAHW,MAAM,OAahB;AAnCD;;;GAGG;AACH,+BAFa,MAAM,CAMlB;AAZD,4BAAsB,iBAAiB,CAAC;AACxC,4BAAsB,sBAAsB,CAAC;AAC7C,4BAAsB,OAAO,CAAC"}
1
+ {"version":3,"file":"worktree-manager.d.ts","sourceRoot":"","sources":["../../src/worktree/worktree-manager.js"],"names":[],"mappings":"AA+DA;;;;;;;;GAQG;AACH,qCAPW,MAAM,YAEd;IAAyB,KAAK,GAAtB,MAAM;IACW,UAAU,GAA3B,MAAM;IACW,MAAM,GAAvB,MAAM;CACd,OAmJF;AAED;;;GAGG;AACH,uCAqCC;AAED;;;;;;GAMG;AACH,sCALW,MAAM,YAEd;IAA0B,YAAY,GAA9B,OAAO;IACW,KAAK,GAAvB,OAAO;CACjB,QA0DA;AAED;;;;;GAKG;AACH,yCAHG;IAAyB,UAAU,GAA3B,MAAM;CACd,SAiDF;AA1VD;;;;GAIG;AACH,mCAHW,MAAM,OAahB;AAnCD;;;GAGG;AACH,+BAFa,MAAM,CAMlB;AAZD,4BAAsB,iBAAiB,CAAC;AACxC,4BAAsB,sBAAsB,CAAC;AAC7C,4BAAsB,OAAO,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paths.design/caws-cli",
3
- "version": "9.0.0",
3
+ "version": "9.1.0",
4
4
  "description": "CAWS CLI - Coding Agent Workflow System command-line tools for spec management, quality gates, and AI-assisted development",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "typecheck": "tsc --emitDeclarationOnly --outDir dist",
19
19
  "start": "node dist/index.js",
20
20
  "test": "npm run build && jest && npm run test:cleanup",
21
- "test:unit": "npm run build && jest && npm run test:cleanup",
21
+ "test:unit": "npm run build && jest --testPathIgnorePatterns='perf-budgets|integration|e2e|mutation|axe|contract' && npm run test:cleanup",
22
22
  "test:contract": "npm run build && jest --testPathPatterns=contract && npm run test:cleanup",
23
23
  "test:integration": "npm run build && jest --testPathPatterns=integration && npm run test:cleanup",
24
24
  "test:e2e:smoke": "npm run build && jest --testPathPatterns=e2e && npm run test:cleanup",