@ulysses-ai/create-workspace 0.13.0-beta.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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/bin/create.mjs +79 -0
  4. package/lib/git.mjs +26 -0
  5. package/lib/init.mjs +129 -0
  6. package/lib/payload.mjs +44 -0
  7. package/lib/prompts.mjs +113 -0
  8. package/lib/scaffold.mjs +84 -0
  9. package/lib/upgrade.mjs +42 -0
  10. package/package.json +43 -0
  11. package/template/.claude/agents/aside-researcher.md +48 -0
  12. package/template/.claude/agents/implementer.md +39 -0
  13. package/template/.claude/agents/researcher.md +40 -0
  14. package/template/.claude/agents/reviewer.md +47 -0
  15. package/template/.claude/hooks/_utils.mjs +196 -0
  16. package/template/.claude/hooks/_utils.test.mjs +99 -0
  17. package/template/.claude/hooks/post-compact.mjs +7 -0
  18. package/template/.claude/hooks/pre-compact.mjs +34 -0
  19. package/template/.claude/hooks/repo-write-detection.mjs +107 -0
  20. package/template/.claude/hooks/session-end.mjs +91 -0
  21. package/template/.claude/hooks/session-start.mjs +150 -0
  22. package/template/.claude/hooks/subagent-start.mjs +44 -0
  23. package/template/.claude/hooks/workspace-update-check.mjs +42 -0
  24. package/template/.claude/hooks/worktree-create.mjs +53 -0
  25. package/template/.claude/lib/session-frontmatter.mjs +265 -0
  26. package/template/.claude/lib/session-frontmatter.test.mjs +242 -0
  27. package/template/.claude/recipes/migrate-from-notion.md +120 -0
  28. package/template/.claude/rules/agent-rules.md.skip +32 -0
  29. package/template/.claude/rules/cloud-infrastructure.md.skip +15 -0
  30. package/template/.claude/rules/coherent-revisions.md +24 -0
  31. package/template/.claude/rules/documentation.md.skip +13 -0
  32. package/template/.claude/rules/git-conventions.md +34 -0
  33. package/template/.claude/rules/honest-pushback.md +56 -0
  34. package/template/.claude/rules/local-dev-environment.md.skip +60 -0
  35. package/template/.claude/rules/memory-guidance.md +26 -0
  36. package/template/.claude/rules/product-integrity.md.skip +24 -0
  37. package/template/.claude/rules/scope-guard.md.skip +22 -0
  38. package/template/.claude/rules/superpowers-workflow.md.skip +22 -0
  39. package/template/.claude/rules/token-economics.md.skip +31 -0
  40. package/template/.claude/rules/work-item-tracking.md +90 -0
  41. package/template/.claude/rules/workspace-structure.md +69 -0
  42. package/template/.claude/scripts/add-repo-to-session.mjs +78 -0
  43. package/template/.claude/scripts/cleanup-work-session.mjs +108 -0
  44. package/template/.claude/scripts/create-work-session.mjs +124 -0
  45. package/template/.claude/scripts/migrate-open-work.mjs +91 -0
  46. package/template/.claude/scripts/migrate-session-layout.mjs +236 -0
  47. package/template/.claude/scripts/migrate-session-layout.test.mjs +144 -0
  48. package/template/.claude/scripts/trackers/github-issues.mjs +170 -0
  49. package/template/.claude/scripts/trackers/github-issues.test.mjs +190 -0
  50. package/template/.claude/scripts/trackers/interface.mjs +25 -0
  51. package/template/.claude/scripts/trackers/interface.test.mjs +40 -0
  52. package/template/.claude/settings.json +107 -0
  53. package/template/.claude/skills/aside/SKILL.md +125 -0
  54. package/template/.claude/skills/braindump/SKILL.md +96 -0
  55. package/template/.claude/skills/build-docs-site/SKILL.md +323 -0
  56. package/template/.claude/skills/build-docs-site/checklists/framing.md +221 -0
  57. package/template/.claude/skills/build-docs-site/checklists/pitfalls.md +228 -0
  58. package/template/.claude/skills/build-docs-site/checklists/review.md +130 -0
  59. package/template/.claude/skills/build-docs-site/scripts/bulk-fill-migration.py +393 -0
  60. package/template/.claude/skills/build-docs-site/scripts/forbidden-word-grep.mjs +159 -0
  61. package/template/.claude/skills/build-docs-site/scripts/leak-grep.mjs +328 -0
  62. package/template/.claude/skills/build-docs-site/templates/custom.css.tmpl +212 -0
  63. package/template/.claude/skills/build-docs-site/templates/docusaurus.config.ts.tmpl +95 -0
  64. package/template/.claude/skills/build-docs-site/templates/primitives/Arrow.tsx +87 -0
  65. package/template/.claude/skills/build-docs-site/templates/primitives/Box.tsx +90 -0
  66. package/template/.claude/skills/build-docs-site/templates/primitives/DiagramContainer.tsx +46 -0
  67. package/template/.claude/skills/build-docs-site/templates/primitives/Region.tsx +68 -0
  68. package/template/.claude/skills/build-docs-site/templates/primitives/SectionTitle.tsx +42 -0
  69. package/template/.claude/skills/build-docs-site/templates/primitives/tokens.ts +67 -0
  70. package/template/.claude/skills/build-docs-site/templates/sidebars.ts.tmpl +89 -0
  71. package/template/.claude/skills/build-docs-site/templates/spec.md.tmpl +119 -0
  72. package/template/.claude/skills/complete-work/SKILL.md +369 -0
  73. package/template/.claude/skills/handoff/SKILL.md +98 -0
  74. package/template/.claude/skills/maintenance/SKILL.md +116 -0
  75. package/template/.claude/skills/pause-work/SKILL.md +98 -0
  76. package/template/.claude/skills/promote/SKILL.md +77 -0
  77. package/template/.claude/skills/release/SKILL.md +126 -0
  78. package/template/.claude/skills/setup-tracker/SKILL.md +117 -0
  79. package/template/.claude/skills/start-work/SKILL.md +234 -0
  80. package/template/.claude/skills/sync-work/SKILL.md +73 -0
  81. package/template/.claude/skills/workspace-init/SKILL.md +420 -0
  82. package/template/.claude/skills/workspace-update/SKILL.md +108 -0
  83. package/template/.mcp.json +12 -0
  84. package/template/CLAUDE.md.tmpl +32 -0
  85. package/template/_gitignore +28 -0
  86. package/template/workspace.json.tmpl +15 -0
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@ulysses-ai/create-workspace",
3
+ "version": "0.13.0-beta.0",
4
+ "description": "A workspace convention for Claude Code: sessions, handoffs, and shared context as files in git",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "anthropic",
9
+ "workspace",
10
+ "scaffold",
11
+ "monorepo",
12
+ "agent",
13
+ "cli"
14
+ ],
15
+ "homepage": "https://github.com/ukt-solutions/create-ulysses-workspace#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/ukt-solutions/create-ulysses-workspace/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/ukt-solutions/create-ulysses-workspace.git"
22
+ },
23
+ "license": "MIT",
24
+ "type": "module",
25
+ "engines": {
26
+ "node": ">=20.9.0"
27
+ },
28
+ "bin": {
29
+ "create-workspace": "./bin/create.mjs"
30
+ },
31
+ "files": [
32
+ "bin/",
33
+ "lib/",
34
+ "template/"
35
+ ],
36
+ "scripts": {
37
+ "audit:tarball": "node scripts/audit-tarball.mjs",
38
+ "prepublishOnly": "npm run audit:tarball"
39
+ },
40
+ "dependencies": {
41
+ "prompts": "^2.4.2"
42
+ }
43
+ }
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: aside-researcher
3
+ description: Research and expand on a drive-by idea. Writes exactly one local-only file with the user's original thought preserved verbatim alongside codebase and web research.
4
+ model: sonnet
5
+ tools:
6
+ - Read
7
+ - Glob
8
+ - Grep
9
+ - WebSearch
10
+ - WebFetch
11
+ - Write
12
+ disallowedTools:
13
+ - Edit
14
+ - Bash
15
+ - Agent
16
+ effort: high
17
+ ---
18
+
19
+ You are a research agent for the `/aside` skill. You receive a user's drive-by idea and your job is to research it and write a single output file.
20
+
21
+ ## What you receive
22
+
23
+ - The user's original thought (verbatim — this is the most important input)
24
+ - A target file path where you must write the output
25
+ - Locked shared context (auto-injected by SubagentStart hook)
26
+
27
+ ## How to work
28
+
29
+ 1. **Preserve the original thought.** Copy it verbatim into the `## User's Original Thought` section. Never paraphrase, edit, or "improve" it.
30
+ 2. **Research the workspace.** Search shared context files for related braindumps, handoffs, and prior art. Check for existing work on the same topic.
31
+ 3. **Research the codebase.** Search project repos in `repos/` for relevant code, patterns, and documentation.
32
+ 4. **Research the web (when relevant).** If the idea references external technologies, patterns, or tools, search for current best practices, documentation, and prior art. Use your judgment — not every idea needs web research.
33
+ 5. **Write the output file.** Use the exact template provided in your prompt. Write to the exact path provided.
34
+
35
+ ## What you must NOT do
36
+
37
+ - Modify any existing files
38
+ - Create more than one file
39
+ - Paraphrase or rewrite the user's original thought
40
+ - Dispatch nested subagents
41
+ - Write files outside the target directory
42
+
43
+ ## Output quality
44
+
45
+ - Reference specific file paths and line numbers when citing codebase findings
46
+ - Clearly separate your analysis from the user's words
47
+ - The Further Investigation section should surface genuine unknowns, not padding
48
+ - If you find very little relevant material, say so honestly — suggest the user try `/aside --deep` or start a dedicated braindump session
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: implementer
3
+ description: Implement a single, well-defined task. Use when you have a clear spec for what to build and want isolated, focused execution.
4
+ model: inherit
5
+ isolation: worktree
6
+ effort: high
7
+ ---
8
+
9
+ You are implementing a focused task. You will receive the FULL task description in your prompt — do not read plan files.
10
+
11
+ ## What you receive automatically
12
+ - Locked shared context (team knowledge) via SubagentStart hook
13
+ - Full task description pasted into your prompt by the controller
14
+
15
+ ## How to work
16
+ - Read existing code patterns before writing new code
17
+ - Follow conventions from .claude/rules/
18
+ - Write tests alongside implementation (TDD when possible)
19
+ - Keep changes minimal and focused on the task
20
+ - Commit frequently with conventional commit messages
21
+
22
+ ## Questions gate
23
+ If anything in the task description is ambiguous, ASK before implementing. Do not guess. It is better to ask one clarifying question than to implement the wrong thing.
24
+
25
+ ## Self-review before reporting
26
+ Before reporting DONE:
27
+ - Did you implement everything in the task description?
28
+ - Do all tests pass?
29
+ - Did you follow the conventions in .claude/rules/?
30
+ - Are there any edge cases you didn't handle?
31
+
32
+ ## Escalation protocol
33
+ Report your status using one of these:
34
+ - **DONE** — task complete, tests passing, ready for review
35
+ - **DONE_WITH_CONCERNS** — complete but something feels wrong (explain what)
36
+ - **BLOCKED** — can't proceed (explain what's missing or broken)
37
+ - **NEEDS_CONTEXT** — need information not in the prompt (say exactly what)
38
+
39
+ It is always OK to say "this is too complex for a single task." That is useful information, not a failure.
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: researcher
3
+ description: Deep codebase and documentation research. Use when you need to understand existing patterns, find prior art, or investigate how something works before making changes.
4
+ model: sonnet
5
+ tools:
6
+ - Read
7
+ - Glob
8
+ - Grep
9
+ - WebSearch
10
+ - WebFetch
11
+ - LSP
12
+ disallowedTools:
13
+ - Edit
14
+ - Write
15
+ - Bash
16
+ effort: high
17
+ ---
18
+
19
+ You are a research specialist. Your job is to find information, not to change anything.
20
+
21
+ ## What you receive automatically
22
+ - Locked shared context (team knowledge) via SubagentStart hook
23
+ - CLAUDE.md and auto-memory from the workspace
24
+
25
+ ## How to work
26
+ - Search thoroughly before reporting "not found"
27
+ - Cross-reference multiple sources (code, docs, web)
28
+ - Report findings with exact file paths and line numbers
29
+ - Flag contradictions between code and documentation
30
+ - Be exhaustive — check multiple naming conventions, look in test files, check imports
31
+
32
+ ## Output format
33
+ Structure your findings clearly:
34
+ - **Found:** what you discovered, with exact locations
35
+ - **Relevant patterns:** existing code that relates to the question
36
+ - **Gaps:** what you looked for but couldn't find
37
+ - **Suggestions:** informed recommendations based on what you found
38
+
39
+ ## Escalation
40
+ If the question requires understanding conversation context you don't have, report NEEDS_CONTEXT with a specific description of what context you need.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: reviewer
3
+ description: Review implementation against spec, conventions, and quality standards. Use after implementation to catch issues before merging.
4
+ model: opus
5
+ tools:
6
+ - Read
7
+ - Glob
8
+ - Grep
9
+ - Bash
10
+ - LSP
11
+ disallowedTools:
12
+ - Edit
13
+ - Write
14
+ effort: high
15
+ ---
16
+
17
+ You are reviewing code changes. Your job is to find problems, not to fix them.
18
+
19
+ ## What you receive automatically
20
+ - Locked shared context (team knowledge) via SubagentStart hook
21
+ - The spec/task description in your prompt
22
+ - The diff or changed files to review
23
+
24
+ ## Review checklist
25
+ 1. **Spec compliance** — does the implementation match what was specified?
26
+ 2. **Convention adherence** — does it follow .claude/rules/?
27
+ 3. **Edge cases** — are there scenarios not handled?
28
+ 4. **Test coverage** — are tests covering the right scenarios? Are they testing behavior, not implementation?
29
+ 5. **Security** — any injection, XSS, or auth issues?
30
+ 6. **Consistency** — does it match existing patterns in shared-context/locked/?
31
+
32
+ ## What NOT to review
33
+ - Style preferences (defer to linters/formatters)
34
+ - Unrelated code (only review what changed)
35
+ - Hypothetical future requirements (YAGNI)
36
+
37
+ ## Output format
38
+ Report your verdict:
39
+ - **PASS** — no issues found
40
+ - **PASS_WITH_NOTES** — minor suggestions, non-blocking (list them)
41
+ - **NEEDS_CHANGES** — issues that must be fixed before merging
42
+
43
+ For each issue, be specific:
44
+ - File and line number
45
+ - What's wrong
46
+ - What to do instead
47
+ - Why it matters
@@ -0,0 +1,196 @@
1
+ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, unlinkSync, statSync, rmSync } from 'fs';
2
+ import { resolve, dirname, join } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { parseSessionContent, updateSessionContent, writeSessionFile, readSessionFile } from '../lib/session-frontmatter.mjs';
5
+
6
+ export function getWorkspaceRoot(importMetaUrl) {
7
+ const hookDir = dirname(fileURLToPath(importMetaUrl));
8
+ return resolve(hookDir, '..', '..');
9
+ }
10
+
11
+ export async function readStdin() {
12
+ const chunks = [];
13
+ for await (const chunk of process.stdin) {
14
+ chunks.push(chunk);
15
+ }
16
+ const raw = Buffer.concat(chunks).toString();
17
+ try {
18
+ return JSON.parse(raw);
19
+ } catch {
20
+ return {};
21
+ }
22
+ }
23
+
24
+ export function readJSON(filePath) {
25
+ try {
26
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ export function respond(additionalContext) {
33
+ if (additionalContext) {
34
+ console.log(JSON.stringify({ additionalContext }));
35
+ } else {
36
+ console.log('{}');
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Resolve the work-sessions directory (default "work-sessions") and the
42
+ * workspace scratchpad dir (default "workspace-scratchpad") from workspace.json.
43
+ */
44
+ export function getWorkspacePaths(root) {
45
+ const config = readJSON(join(root, 'workspace.json'));
46
+ return {
47
+ workSessionsDir: join(root, config?.workspace?.workSessionsDir || 'work-sessions'),
48
+ scratchpadDir: join(root, config?.workspace?.scratchpadDir || 'workspace-scratchpad'),
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Defensive normalization for a session tracker's `repos` field. A fresh
54
+ * tracker written by create-work-session always holds an array, but an
55
+ * older hand-edited tracker or a tracker migrated from the previous layout
56
+ * might carry a scalar string (single repo) or null. Iterating a string
57
+ * with `for (const x of s)` yields characters — this helper prevents that.
58
+ */
59
+ export function normalizeRepos(value) {
60
+ if (Array.isArray(value)) return value;
61
+ if (value === null || value === undefined || value === '') return [];
62
+ return [String(value)];
63
+ }
64
+
65
+ // === Session tracker helpers ===
66
+
67
+ export function sessionFilePath(root, sessionName) {
68
+ const { workSessionsDir } = getWorkspacePaths(root);
69
+ return join(workSessionsDir, sessionName, 'workspace', 'session.md');
70
+ }
71
+
72
+ export function sessionFolderPath(root, sessionName) {
73
+ const { workSessionsDir } = getWorkspacePaths(root);
74
+ return join(workSessionsDir, sessionName);
75
+ }
76
+
77
+ export function sessionWorktreePath(root, sessionName) {
78
+ const { workSessionsDir } = getWorkspacePaths(root);
79
+ return join(workSessionsDir, sessionName, 'workspace');
80
+ }
81
+
82
+ /**
83
+ * Walk work-sessions/ and return one descriptor per session.md found.
84
+ * Each descriptor is { name, path, ...frontmatterFields }.
85
+ */
86
+ export function getSessionTrackers(root) {
87
+ const { workSessionsDir } = getWorkspacePaths(root);
88
+ if (!existsSync(workSessionsDir)) return [];
89
+ const results = [];
90
+ for (const entry of readdirSync(workSessionsDir)) {
91
+ const sessionPath = join(workSessionsDir, entry, 'workspace', 'session.md');
92
+ if (!existsSync(sessionPath)) continue;
93
+ try {
94
+ const parsed = readSessionFile(sessionPath);
95
+ results.push({
96
+ ...parsed.fields,
97
+ _path: sessionPath,
98
+ _folder: join(workSessionsDir, entry),
99
+ });
100
+ } catch {
101
+ // Skip malformed session files rather than crashing hooks
102
+ }
103
+ }
104
+ return results;
105
+ }
106
+
107
+ /**
108
+ * Read a single session tracker by name. Returns the parsed fields object
109
+ * (not the full { fields, body, raw } shape). Returns null if missing.
110
+ */
111
+ export function readSessionTracker(root, sessionName) {
112
+ const path = sessionFilePath(root, sessionName);
113
+ if (!existsSync(path)) return null;
114
+ try {
115
+ return readSessionFile(path).fields;
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Update specific fields in an existing session tracker. Lossless for
123
+ * unchanged fields and the body. Creates the file if it does not exist.
124
+ */
125
+ export function updateSessionTracker(root, sessionName, updates) {
126
+ const path = sessionFilePath(root, sessionName);
127
+ if (!existsSync(path)) {
128
+ // Create a minimal stub; callers will usually supply all fields
129
+ const folder = dirname(path);
130
+ if (!existsSync(folder)) mkdirSync(folder, { recursive: true });
131
+ writeSessionFile(path, updates, '\n# Work Session\n');
132
+ return;
133
+ }
134
+ const content = readFileSync(path, 'utf-8');
135
+ const next = updateSessionContent(content, updates);
136
+ if (next !== content) writeFileSync(path, next);
137
+ }
138
+
139
+ /**
140
+ * Create a brand-new session tracker file with the given fields and body.
141
+ * Creates the work-sessions/{name}/ folder if it does not exist.
142
+ */
143
+ export function createSessionTracker(root, sessionName, fields, body) {
144
+ const path = sessionFilePath(root, sessionName);
145
+ const folder = dirname(path);
146
+ if (!existsSync(folder)) mkdirSync(folder, { recursive: true });
147
+ writeSessionFile(path, fields, body);
148
+ }
149
+
150
+ /**
151
+ * Delete the entire work-sessions/{name}/ folder. Used by /complete-work
152
+ * after the session is finalized and archived into release notes.
153
+ * Caller is responsible for any git bookkeeping (branch deletes, prunes).
154
+ */
155
+ export function deleteSessionFolder(root, sessionName) {
156
+ const folder = sessionFolderPath(root, sessionName);
157
+ if (!existsSync(folder)) return;
158
+ rmSync(folder, { recursive: true, force: true });
159
+ }
160
+
161
+ // === Active session pointer (per-worktree) ===
162
+ // A workspace worktree writes a tiny JSON pointer file at:
163
+ // {worktree}/.claude/.active-session.json
164
+ // to tell hooks which session is currently in scope. Scoped to the
165
+ // worktree itself — each worktree has its own pointer.
166
+
167
+ export function activeSessionPointerPath(worktreeRoot) {
168
+ return join(worktreeRoot, '.claude', '.active-session.json');
169
+ }
170
+
171
+ export function getActiveSessionPointer(worktreeRoot) {
172
+ return readJSON(activeSessionPointerPath(worktreeRoot));
173
+ }
174
+
175
+ export function writeActiveSessionPointer(worktreeRoot, data) {
176
+ const path = activeSessionPointerPath(worktreeRoot);
177
+ const dir = dirname(path);
178
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
179
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
180
+ }
181
+
182
+ export function getMainRoot(root) {
183
+ const pointer = getActiveSessionPointer(root);
184
+ return pointer?.rootPath || root;
185
+ }
186
+
187
+ export function timeAgo(isoString) {
188
+ if (!isoString) return 'unknown';
189
+ const diff = Date.now() - new Date(isoString).getTime();
190
+ const mins = Math.floor(diff / 60000);
191
+ if (mins < 60) return `${mins}m ago`;
192
+ const hours = Math.floor(mins / 60);
193
+ if (hours < 24) return `${hours}h ago`;
194
+ const days = Math.floor(hours / 24);
195
+ return `${days}d ago`;
196
+ }
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ // Unit tests for _utils.mjs tracker path helpers.
3
+ // Run: node .claude/hooks/_utils.test.mjs
4
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs';
5
+ import { tmpdir } from 'os';
6
+ import { join } from 'path';
7
+ import {
8
+ sessionFilePath,
9
+ sessionWorktreePath,
10
+ sessionFolderPath,
11
+ getSessionTrackers,
12
+ } from './_utils.mjs';
13
+
14
+ let failed = 0;
15
+ let passed = 0;
16
+
17
+ function assertEq(actual, expected, msg) {
18
+ const a = JSON.stringify(actual);
19
+ const e = JSON.stringify(expected);
20
+ if (a === e) {
21
+ passed++;
22
+ } else {
23
+ failed++;
24
+ console.error(` FAIL: ${msg}\n expected: ${e}\n actual: ${a}`);
25
+ }
26
+ }
27
+
28
+ function fixture() {
29
+ const root = mkdtempSync(join(tmpdir(), 'utils-test-'));
30
+ writeFileSync(
31
+ join(root, 'workspace.json'),
32
+ JSON.stringify({
33
+ workspace: { workSessionsDir: 'work-sessions', scratchpadDir: 'workspace-scratchpad' },
34
+ })
35
+ );
36
+ mkdirSync(join(root, 'work-sessions', 'demo', 'workspace'), { recursive: true });
37
+ writeFileSync(
38
+ join(root, 'work-sessions', 'demo', 'workspace', 'session.md'),
39
+ '---\ntype: session-tracker\nname: demo\nstatus: active\n---\n\nbody\n'
40
+ );
41
+ return root;
42
+ }
43
+
44
+ // sessionFilePath points inside the worktree
45
+ {
46
+ const root = fixture();
47
+ try {
48
+ assertEq(
49
+ sessionFilePath(root, 'demo'),
50
+ join(root, 'work-sessions', 'demo', 'workspace', 'session.md'),
51
+ 'sessionFilePath resolves to in-worktree tracker'
52
+ );
53
+ } finally {
54
+ rmSync(root, { recursive: true, force: true });
55
+ }
56
+ }
57
+
58
+ // sessionWorktreePath returns the workspace worktree root
59
+ {
60
+ const root = fixture();
61
+ try {
62
+ assertEq(
63
+ sessionWorktreePath(root, 'demo'),
64
+ join(root, 'work-sessions', 'demo', 'workspace'),
65
+ 'sessionWorktreePath returns worktree root'
66
+ );
67
+ } finally {
68
+ rmSync(root, { recursive: true, force: true });
69
+ }
70
+ }
71
+
72
+ // sessionFolderPath still returns the session parent folder
73
+ {
74
+ const root = fixture();
75
+ try {
76
+ assertEq(
77
+ sessionFolderPath(root, 'demo'),
78
+ join(root, 'work-sessions', 'demo'),
79
+ 'sessionFolderPath unchanged'
80
+ );
81
+ } finally {
82
+ rmSync(root, { recursive: true, force: true });
83
+ }
84
+ }
85
+
86
+ // getSessionTrackers walks the in-worktree path
87
+ {
88
+ const root = fixture();
89
+ try {
90
+ const trackers = getSessionTrackers(root);
91
+ assertEq(trackers.length, 1, 'getSessionTrackers finds demo session');
92
+ assertEq(trackers[0].name, 'demo', 'tracker name parsed');
93
+ } finally {
94
+ rmSync(root, { recursive: true, force: true });
95
+ }
96
+ }
97
+
98
+ console.log(`Passed: ${passed}, Failed: ${failed}`);
99
+ if (failed > 0) process.exit(1);
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // PostCompact hook — remind user that earlier context was lost
3
+ import { respond } from './_utils.mjs';
4
+
5
+ respond(`Earlier context was compacted. Discussion details from before this point may be incomplete.
6
+
7
+ If you had uncaptured decisions or progress, use /braindump or /handoff now while you still remember.`);
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ // PreCompact hook — session-aware context capture nudge.
3
+ import { existsSync, statSync } from 'fs';
4
+ import {
5
+ getWorkspaceRoot,
6
+ respond,
7
+ getActiveSessionPointer,
8
+ getMainRoot,
9
+ sessionFilePath,
10
+ timeAgo,
11
+ } from './_utils.mjs';
12
+
13
+ const root = getWorkspaceRoot(import.meta.url);
14
+ const pointer = getActiveSessionPointer(root);
15
+
16
+ if (pointer) {
17
+ const mainRoot = getMainRoot(root);
18
+ const trackerPath = sessionFilePath(mainRoot, pointer.name);
19
+
20
+ if (existsSync(trackerPath)) {
21
+ const stat = statSync(trackerPath);
22
+ const minsAgo = Math.floor((Date.now() - stat.mtimeMs) / 60000);
23
+
24
+ if (minsAgo > 30) {
25
+ respond(`Context is about to be compacted. Your session tracker for "${pointer.name}" was last updated ${timeAgo(stat.mtime.toISOString())}.\n\nConsider /handoff to update the tracker before context is lost.`);
26
+ } else {
27
+ respond(`Context compacting. Session tracker for "${pointer.name}" is recent (updated ${minsAgo}m ago). Earlier conversation details may still be lost — use /handoff if needed.`);
28
+ }
29
+ } else {
30
+ respond(`Context is about to be compacted. No session tracker found for "${pointer.name}". Use /handoff to capture progress before context is lost.`);
31
+ }
32
+ } else {
33
+ respond(`Context is about to be compacted — earlier conversation details will be lost.\n\nIf this session produced decisions or progress worth keeping:\n /braindump [name] — capture discussion and reasoning\n /handoff [name] — capture workstream state and next steps\n\nFiles in shared-context/ will persist. Conversation details won't.`);
34
+ }
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse hook — enforce workspace root write restrictions and detect
3
+ // out-of-session repo writes.
4
+ //
5
+ // New layout paths:
6
+ // Workspace worktree: work-sessions/{name}/workspace/
7
+ // Project worktree: work-sessions/{name}/workspace/repos/{repo}/
8
+ // Bare clone: repos/{repo}/ (at workspace root)
9
+ import { join, basename } from 'path';
10
+ import {
11
+ getWorkspaceRoot,
12
+ readStdin,
13
+ respond,
14
+ getActiveSessionPointer,
15
+ readSessionTracker,
16
+ readJSON,
17
+ getWorkspacePaths,
18
+ } from './_utils.mjs';
19
+
20
+ const root = getWorkspaceRoot(import.meta.url);
21
+ const input = await readStdin();
22
+ const toolName = input.tool_name || '';
23
+
24
+ if (!['Bash', 'Edit', 'Write'].includes(toolName)) {
25
+ respond();
26
+ process.exit(0);
27
+ }
28
+
29
+ const toolInput = input.tool_input || {};
30
+ const paths = [toolInput.file_path, toolInput.command, toolInput.path]
31
+ .filter(Boolean)
32
+ .join(' ')
33
+ .replace(/\\/g, '/');
34
+
35
+ // If we're in a workspace worktree, check for out-of-session repo writes
36
+ const pointer = getActiveSessionPointer(root);
37
+ if (pointer) {
38
+ const mainRoot = pointer.rootPath || root;
39
+ const config = readJSON(join(mainRoot, 'workspace.json'));
40
+ const tracker = readSessionTracker(mainRoot, pointer.name);
41
+
42
+ if (tracker && config?.repos) {
43
+ // Find references to repos inside work-sessions/{name}/workspace/repos/{repo}/
44
+ // and also the workspace-root repos/{repo}/ for direct writes.
45
+ const wtMatch = paths.match(/work-sessions\/[^/\s]+\/workspace\/repos\/([^/\s]+)/);
46
+ const cloneMatch = paths.match(/(?:^|\s|\/)repos\/([^/\s]+)/);
47
+ const targetRepo = wtMatch ? wtMatch[1] : (cloneMatch ? cloneMatch[1] : null);
48
+ if (targetRepo) {
49
+ const sessionRepos = tracker.repos || [];
50
+ if (config.repos[targetRepo] && !sessionRepos.includes(targetRepo)) {
51
+ respond(`You're about to write to ${targetRepo}, which isn't part of this session. Consider adding it first so changes land on the session branch.`);
52
+ process.exit(0);
53
+ }
54
+ }
55
+ }
56
+
57
+ respond();
58
+ process.exit(0);
59
+ }
60
+
61
+ // We're at the main workspace root — restrict writes
62
+
63
+ const { scratchpadDir } = getWorkspacePaths(root);
64
+ const scratchpadName = scratchpadDir.slice(root.length + 1); // "workspace-scratchpad"
65
+
66
+ // Allow writes to the workspace scratchpad
67
+ if (paths.includes(scratchpadName)) {
68
+ respond();
69
+ process.exit(0);
70
+ }
71
+
72
+ // Allow writes to local-only-* files
73
+ const filePathArg = toolInput.file_path || '';
74
+ if (basename(filePathArg).startsWith('local-only-')) {
75
+ respond();
76
+ process.exit(0);
77
+ }
78
+
79
+ // For Bash commands, check if the command targets allowed paths
80
+ if (toolName === 'Bash') {
81
+ const cmd = toolInput.command || '';
82
+ if (/^\s*(git|ls|cat|head|tail|grep|rg|find|echo|pwd|cd|which|node\s+-c)\b/.test(cmd)) {
83
+ respond();
84
+ process.exit(0);
85
+ }
86
+ if (cmd.includes(scratchpadName) || cmd.includes('local-only-')) {
87
+ respond();
88
+ process.exit(0);
89
+ }
90
+ // Allow helper script invocations from the workspace root
91
+ if (/node\s+.*\.claude\/scripts\//.test(cmd)) {
92
+ respond();
93
+ process.exit(0);
94
+ }
95
+ }
96
+
97
+ // Check if this write targets repos/, shared-context/, work-sessions/, or template files
98
+ const isRepoWrite = /(?:^|[\s/])repos\//.test(paths) || paths.includes('work-sessions/');
99
+ const isContextWrite = paths.includes('shared-context/') && !basename(filePathArg).startsWith('local-only-');
100
+ const isTemplateWrite = paths.includes('.claude/') && !paths.includes(scratchpadName);
101
+
102
+ if (isRepoWrite || isContextWrite || isTemplateWrite) {
103
+ respond("You're on main. All work should happen in a workspace worktree. Run /start-work to create or resume a work session.");
104
+ process.exit(0);
105
+ }
106
+
107
+ respond();