gsd-init 1.0.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.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # gsd-init
2
+
3
+ Set up the GSD observer/worker tmux system in your project.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx gsd-init
9
+ ```
10
+
11
+ With auto-confirm:
12
+ ```bash
13
+ npx gsd-init --yes
14
+ ```
15
+
16
+ ## What it does
17
+
18
+ 1. Installs observer scripts to `~/.claude/gsd-observer/`
19
+ 2. Registers a Stop hook in `.claude/settings.json`
20
+ 3. Prints next steps for starting the observer and worker sessions
21
+
22
+ ## Requirements
23
+
24
+ - Node.js >= 16
25
+ - tmux
26
+ - jq
27
+ - Claude Code CLI (`claude`)
28
+ - GSD (superpowers plugin) installed in Claude Code
29
+
30
+ ## After install
31
+
32
+ ```bash
33
+ # Start Observer session first
34
+ ~/.claude/gsd-observer/scripts/start-observer.sh
35
+
36
+ # Start Worker session in your project directory
37
+ ~/.claude/gsd-observer/scripts/start-worker.sh /path/to/project
38
+
39
+ # Verify setup
40
+ ~/.claude/gsd-observer/scripts/verify.sh
41
+ ```
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Node version check — must use syntax valid in Node 14+ for compatibility
4
+ var major = parseInt(process.version.slice(1).split('.')[0], 10);
5
+ if (major < 16) {
6
+ process.stderr.write('gsd-init requires Node.js >= 16 (found ' + process.version + ')\n');
7
+ process.exit(1);
8
+ }
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+ const readline = require('readline');
13
+
14
+ const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
15
+ const OBS_ROOT = process.env.GSD_INIT_OBS_ROOT || path.join(os.homedir(), '.claude', 'gsd-observer');
16
+
17
+ const TEMPLATE_FILES = [
18
+ { src: 'agents/gsd-observer.md', dst: 'agents/gsd-observer.md' },
19
+ { src: 'hooks/gsd-stop-hook.sh', dst: 'hooks/gsd-stop-hook.sh' },
20
+ { src: 'scripts/start-observer.sh', dst: 'scripts/start-observer.sh' },
21
+ { src: 'scripts/start-worker.sh', dst: 'scripts/start-worker.sh' },
22
+ { src: 'scripts/wake-observer.sh', dst: 'scripts/wake-observer.sh' },
23
+ { src: 'scripts/notify-worker.sh', dst: 'scripts/notify-worker.sh' },
24
+ { src: 'scripts/teardown.sh', dst: 'scripts/teardown.sh' },
25
+ { src: 'scripts/verify.sh', dst: 'scripts/verify.sh' },
26
+ { src: 'schema/event.json', dst: 'schema/event.json' },
27
+ { src: 'schema/response.json', dst: 'schema/response.json' },
28
+ ];
29
+
30
+ const SH_FILES = [
31
+ 'hooks/gsd-stop-hook.sh',
32
+ 'scripts/start-observer.sh',
33
+ 'scripts/start-worker.sh',
34
+ 'scripts/wake-observer.sh',
35
+ 'scripts/notify-worker.sh',
36
+ 'scripts/teardown.sh',
37
+ 'scripts/verify.sh',
38
+ ];
39
+
40
+ const GSD_ENTRY = {
41
+ matcher: '',
42
+ hooks: [{ type: 'command', command: '~/.claude/gsd-observer/hooks/gsd-stop-hook.sh' }]
43
+ };
44
+
45
+ function getSettingsLabel(settingsPath) {
46
+ if (!fs.existsSync(settingsPath)) return '[create]';
47
+ var data;
48
+ try { data = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); }
49
+ catch (e) { return '[error]'; }
50
+ var stopArr = (data.hooks && data.hooks.Stop) ? data.hooks.Stop : [];
51
+ var alreadyRegistered = stopArr.some(function(entry) {
52
+ return JSON.stringify(entry).includes('gsd-stop-hook.sh');
53
+ });
54
+ return alreadyRegistered ? '[skip]' : '[merge]';
55
+ }
56
+
57
+ function planInstall(tmplDir, obsRoot, projDir) {
58
+ var homeDir = os.homedir();
59
+ var ops = [];
60
+ for (var i = 0; i < TEMPLATE_FILES.length; i++) {
61
+ var f = TEMPLATE_FILES[i];
62
+ var dest = path.join(obsRoot, f.dst);
63
+ var label = fs.existsSync(dest) ? '[overwrite]' : '[create]';
64
+ var relDisplay = dest.startsWith(homeDir)
65
+ ? '~' + dest.slice(homeDir.length)
66
+ : dest;
67
+ ops.push({ dest: dest, label: label, relative: relDisplay });
68
+ }
69
+ var settingsPath = path.join(projDir, '.claude', 'settings.json');
70
+ ops.push({ dest: settingsPath, label: getSettingsLabel(settingsPath), relative: '.claude/settings.json' });
71
+ return ops;
72
+ }
73
+
74
+ // Stubs — implemented in later tasks
75
+ function formatDryRun(ops) {
76
+ var lines = ['gsd-init — GSD Observer/Worker setup', '', 'Files to install:'];
77
+ for (var i = 0; i < ops.length; i++) {
78
+ var op = ops[i];
79
+ // Pad label to 14 chars for alignment: '[create]' is 8, '[overwrite]' is 11, '[merge]' is 7, '[skip]' is 6
80
+ var padded = op.label + ' '.repeat(Math.max(1, 14 - op.label.length));
81
+ lines.push(' ' + padded + op.relative);
82
+ }
83
+ return lines.join('\n');
84
+ }
85
+ function mkdirpSync(dir) { fs.mkdirSync(dir, { recursive: true }); }
86
+ function copyTemplates(tmplDir, obsRoot) {
87
+ for (var i = 0; i < TEMPLATE_FILES.length; i++) {
88
+ var f = TEMPLATE_FILES[i];
89
+ var src = path.join(tmplDir, f.src);
90
+ var dest = path.join(obsRoot, f.dst);
91
+ mkdirpSync(path.dirname(dest));
92
+ fs.copyFileSync(src, dest);
93
+ }
94
+ }
95
+ function chmodScripts(obsRoot) {
96
+ for (var i = 0; i < SH_FILES.length; i++) {
97
+ fs.chmodSync(path.join(obsRoot, SH_FILES[i]), 0o755);
98
+ }
99
+ }
100
+ function mergeSettings(projDir, entry) {
101
+ var settingsPath = path.join(projDir, '.claude', 'settings.json');
102
+ if (!fs.existsSync(settingsPath)) {
103
+ var newData = { hooks: { Stop: [entry] } };
104
+ fs.writeFileSync(settingsPath, JSON.stringify(newData, null, 2) + '\n');
105
+ return;
106
+ }
107
+ var raw = fs.readFileSync(settingsPath, 'utf8');
108
+ var data;
109
+ try { data = JSON.parse(raw); }
110
+ catch (e) { throw new Error('Malformed JSON in ' + settingsPath + ': ' + e.message); }
111
+
112
+ var stopArr = (data.hooks && data.hooks.Stop) ? data.hooks.Stop : [];
113
+ var alreadyRegistered = stopArr.some(function(e) {
114
+ return JSON.stringify(e).includes('gsd-stop-hook.sh');
115
+ });
116
+ if (alreadyRegistered) return;
117
+
118
+ if (!data.hooks) data.hooks = {};
119
+ if (!data.hooks.Stop) data.hooks.Stop = [];
120
+ data.hooks.Stop.push(entry);
121
+ fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n');
122
+ }
123
+ function prompt(question) {
124
+ return new Promise(function(resolve) {
125
+ var rl = readline.createInterface({ input: process.stdin, output: process.stdout });
126
+ rl.question(question, function(answer) {
127
+ rl.close();
128
+ resolve(answer.trim().toLowerCase() === 'y');
129
+ });
130
+ });
131
+ }
132
+
133
+ function printSummary() {
134
+ console.log([
135
+ '',
136
+ 'Done! Next steps:',
137
+ ' 1. Start Observer first: ~/.claude/gsd-observer/scripts/start-observer.sh',
138
+ ' 2. Start Worker: ~/.claude/gsd-observer/scripts/start-worker.sh <project-dir>',
139
+ ' 3. Run GSD in the Worker tmux pane — Observer will co-pilot automatically.',
140
+ '',
141
+ ' Observer agent prompt: ~/.claude/gsd-observer/agents/gsd-observer.md',
142
+ ' Verify setup: ~/.claude/gsd-observer/scripts/verify.sh',
143
+ '',
144
+ 'Teardown:',
145
+ ' ~/.claude/gsd-observer/scripts/teardown.sh',
146
+ ' (or manually: tmux kill-session -t gsd-worker && tmux kill-session -t gsd-observer &&',
147
+ ' rm -f /tmp/gsd-event-*.json /tmp/gsd-response-*.json /tmp/gsd-last-event-phase)',
148
+ ].join('\n'));
149
+ }
150
+
151
+ async function run() {
152
+ var args = process.argv.slice(2);
153
+ var skipPrompt = args.indexOf('--yes') !== -1 || args.indexOf('-y') !== -1;
154
+ var projDir = process.env.GSD_INIT_PROJ_DIR || process.cwd();
155
+
156
+ var ops = planInstall(TEMPLATES_DIR, OBS_ROOT, projDir);
157
+
158
+ // Abort on malformed settings.json early
159
+ var errOp = null;
160
+ for (var i = 0; i < ops.length; i++) {
161
+ if (ops[i].label === '[error]') { errOp = ops[i]; break; }
162
+ }
163
+ if (errOp) {
164
+ console.error('Error: Malformed JSON in ' + errOp.dest + ' — fix or remove it and retry.');
165
+ process.exit(1);
166
+ }
167
+
168
+ console.log(formatDryRun(ops));
169
+ console.log('');
170
+ console.log('Proceed? [y/N]');
171
+
172
+ if (!skipPrompt) {
173
+ var proceed = await prompt('');
174
+ if (!proceed) {
175
+ console.log('Aborted.');
176
+ process.exit(0);
177
+ }
178
+ }
179
+
180
+ // Step 1: create ~/.claude/gsd-observer subdirs
181
+ var subdirs = ['agents', 'hooks', 'scripts', 'schema'];
182
+ for (var j = 0; j < subdirs.length; j++) {
183
+ var subdir = path.join(OBS_ROOT, subdirs[j]);
184
+ try { mkdirpSync(subdir); }
185
+ catch (e) { console.error('Error creating ' + subdir + ': ' + e.message); process.exit(1); }
186
+ }
187
+
188
+ // Step 2: create project .claude/
189
+ var dotClaudeDir = path.join(projDir, '.claude');
190
+ try { mkdirpSync(dotClaudeDir); }
191
+ catch (e) { console.error('Error creating ' + dotClaudeDir + ': ' + e.message); process.exit(1); }
192
+
193
+ // Step 3: copy templates
194
+ try { copyTemplates(TEMPLATES_DIR, OBS_ROOT); }
195
+ catch (e) { console.error('Error copying templates to ' + OBS_ROOT + ': ' + e.message); process.exit(1); }
196
+
197
+ // Step 4: chmod .sh files
198
+ try { chmodScripts(OBS_ROOT); }
199
+ catch (e) { console.error('Error setting permissions in ' + OBS_ROOT + ': ' + e.message); process.exit(1); }
200
+
201
+ // Step 5: merge settings.json
202
+ try { mergeSettings(projDir, GSD_ENTRY); }
203
+ catch (e) { console.error(e.message); process.exit(1); }
204
+
205
+ printSummary();
206
+ }
207
+
208
+ if (require.main === module) {
209
+ run().catch(function(e) { console.error(e.message); process.exit(1); });
210
+ }
211
+
212
+ module.exports = {
213
+ planInstall, getSettingsLabel, formatDryRun, mkdirpSync,
214
+ copyTemplates, chmodScripts, mergeSettings, printSummary,
215
+ TEMPLATE_FILES, SH_FILES, GSD_ENTRY
216
+ };
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "gsd-init",
3
+ "version": "1.0.0",
4
+ "description": "Set up GSD observer/worker tmux system in a project",
5
+ "bin": {
6
+ "gsd-init": "bin/gsd-init.js"
7
+ },
8
+ "files": ["bin", "templates", "README.md"],
9
+ "engines": { "node": ">=16" },
10
+ "license": "MIT",
11
+ "scripts": {
12
+ "test": "node tests/test-plan-install.js && node tests/test-merge-settings.js && node tests/test-integration.js"
13
+ }
14
+ }
@@ -0,0 +1,55 @@
1
+ ---
2
+ description: GSD Observer co-pilot. Woken by Worker Claude via tmux to review GSD phase outputs.
3
+ ---
4
+
5
+ You are GSD Observer, a co-pilot for a Claude instance running the GSD (Get Shit Done) workflow.
6
+
7
+ When you receive an instruction to read an event file and respond:
8
+
9
+ 1. Read the event JSON file at the path given
10
+ 2. Check the `observer_mode` field
11
+ 3. Read the artifacts listed (handle null fields gracefully — note absence in observations)
12
+ 4. Execute mode logic:
13
+
14
+ **AUDIT mode** (research, verify phases):
15
+ - Review artifacts for completeness, quality, coverage gaps
16
+ - Identify missing information or weak areas
17
+ - Decision: `proceed` if adequate, `revise` if gaps are significant
18
+
19
+ **BLOCK mode** (plan phase):
20
+ - Review plan document: goal clarity, task breakdown, dependencies, risks, success criteria
21
+ - Decision: `proceed` if plan is solid, `revise` if fundamental issues found
22
+
23
+ **AUGMENT mode** (execute phase):
24
+ - Review changed files: correctness, code quality, security, edge cases
25
+ - Review test results if present (note if absent)
26
+ - Decision: `proceed` if acceptable, `revise` if issues need fixing before next phase
27
+
28
+ 5. Write response JSON **atomically** to the path specified:
29
+ ```bash
30
+ echo '<json>' > <path>.tmp && mv <path>.tmp <path>
31
+ ```
32
+
33
+ Response format:
34
+ ```json
35
+ {
36
+ "event_id": "<from event file>",
37
+ "decision": "proceed | revise | hold",
38
+ "mode": "<audit | block | augment>",
39
+ "observations": ["observation 1", "observation 2"],
40
+ "message": "Human-readable summary for Worker Claude.",
41
+ "revision_instructions": "Specific actionable instructions (required when decision=revise, omit otherwise)",
42
+ "timestamp": "<ISO8601>"
43
+ }
44
+ ```
45
+
46
+ 6. Run the notify script:
47
+ ```bash
48
+ ~/.claude/gsd-observer/scripts/notify-worker.sh <event_id>
49
+ ```
50
+
51
+ **Rules:**
52
+ - `revision_instructions` must be specific and actionable — Worker Claude acts on it without asking for clarification
53
+ - Be concise in `message` (1-2 sentences)
54
+ - Do NOT ask questions back to the user
55
+ - Handle `null` artifact fields by noting the absence in observations rather than failing
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env bash
2
+ # GSD Stop Hook — fires after each Claude response in Worker session.
3
+ # Detects GSD phase completion by checking recently-modified artifact files.
4
+ # Blocks in a polling loop until Observer responds, then exits.
5
+ #
6
+ # Environment variables (all optional, have defaults):
7
+ # GSD_OBSERVER_ENABLED — set to 1 to activate (default: off)
8
+ # GSD_OBSERVER_SESSION — tmux session name for Observer (default: gsd-observer)
9
+ # GSD_OBSERVER_PANE — pane id for Observer (default: 0.0)
10
+ # GSD_ARTIFACT_WINDOW_SECS — seconds window for artifact detection (default: 30)
11
+ # GSD_PROJECT_DIR — project directory to scan (default: pwd)
12
+ # GSD_SENTINEL_FILE — dedup sentinel file path (default: /tmp/gsd-last-event-phase)
13
+ # GSD_WORKER_SESSION — worker tmux session (default: gsd-worker)
14
+ # GSD_WORKER_PANE — worker pane id (default: 0.0)
15
+ # GSD_DRY_RUN — if set, skip tmux calls and write phase= to stderr
16
+
17
+ set -euo pipefail
18
+
19
+ # --- Configuration ---
20
+ OBSERVER_ENABLED="${GSD_OBSERVER_ENABLED:-}"
21
+ OBSERVER_SESSION="${GSD_OBSERVER_SESSION:-gsd-observer}"
22
+ OBSERVER_PANE="${GSD_OBSERVER_PANE:-0.0}"
23
+ WINDOW_SECS="${GSD_ARTIFACT_WINDOW_SECS:-30}"
24
+ PROJECT_DIR="${GSD_PROJECT_DIR:-$(pwd)}"
25
+ SENTINEL_FILE="${GSD_SENTINEL_FILE:-/tmp/gsd-last-event-phase}"
26
+ WORKER_SESSION="${GSD_WORKER_SESSION:-gsd-worker}"
27
+ WORKER_PANE="${GSD_WORKER_PANE:-0.0}"
28
+ DRY_RUN="${GSD_DRY_RUN:-}"
29
+ POLL_TIMEOUT=120
30
+ HOLD_MAX=3
31
+ SCRIPTS_DIR="$(dirname "$0")/../scripts"
32
+
33
+ # --- Guard: disabled ---
34
+ if [ -z "$OBSERVER_ENABLED" ]; then
35
+ exit 0
36
+ fi
37
+
38
+ log() { echo "[gsd-hook] $*" >&2; }
39
+
40
+ # --- Phase detection ---
41
+ detect_phase() {
42
+ local window="$WINDOW_SECS"
43
+ local proj="$PROJECT_DIR"
44
+ local now
45
+ now=$(date +%s)
46
+ local cutoff=$((now - window))
47
+
48
+ # Helper: file modified within window?
49
+ recent() {
50
+ local f="$1"
51
+ [ -f "$f" ] || return 1
52
+ local mtime
53
+ mtime=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null || echo 0)
54
+ [ "$mtime" -ge "$cutoff" ]
55
+ }
56
+
57
+ # Find most recent planning artifact
58
+ local research_file plan_file verify_file
59
+ research_file=$(find "$proj/.planning" -name "RESEARCH.md" 2>/dev/null | head -1 || echo "")
60
+ plan_file=$(find "$proj/.planning" -name "PLAN.md" 2>/dev/null | head -1 || echo "")
61
+ verify_file=$(find "$proj/.planning" -name "VERIFICATION.md" 2>/dev/null | head -1 || echo "")
62
+
63
+ # Check for source files modified within the time window (outside .planning)
64
+ # Use a sentinel file touched at the cutoff time for portable -newer comparison
65
+ local src_modified=0
66
+ local cutoff_sentinel
67
+ cutoff_sentinel=$(mktemp)
68
+ # Set mtime to cutoff using python3 for portability
69
+ python3 -c "import os,time; os.utime('$cutoff_sentinel', (time.time()-${window}, time.time()-${window}))" 2>/dev/null || true
70
+ if find "$proj" -not -path "*/.planning/*" -not -path "*/.git/*" \
71
+ -newer "$cutoff_sentinel" \( \
72
+ -name "*.swift" -o -name "*.py" -o -name "*.ts" -o \
73
+ -name "*.js" -o -name "*.go" -o -name "*.rs" \
74
+ \) 2>/dev/null | grep -q .; then
75
+ src_modified=1
76
+ fi
77
+ rm -f "$cutoff_sentinel"
78
+
79
+ # Execute: PLAN.md recent + source files modified
80
+ if recent "$plan_file" && [ "$src_modified" -eq 1 ]; then
81
+ echo "execute $plan_file"
82
+ return
83
+ fi
84
+
85
+ # Verify: VERIFICATION.md recent
86
+ if recent "$verify_file"; then
87
+ echo "verify $verify_file"
88
+ return
89
+ fi
90
+
91
+ # Plan: PLAN.md recent (no source modifications)
92
+ if recent "$plan_file"; then
93
+ echo "plan $plan_file"
94
+ return
95
+ fi
96
+
97
+ # Research: RESEARCH.md recent
98
+ if recent "$research_file"; then
99
+ echo "research $research_file"
100
+ return
101
+ fi
102
+
103
+ # Nothing detected
104
+ echo ""
105
+ }
106
+
107
+ # --- Main ---
108
+
109
+ detection=$(detect_phase)
110
+ if [ -z "$detection" ]; then
111
+ exit 0
112
+ fi
113
+
114
+ phase=$(echo "$detection" | cut -d' ' -f1)
115
+ artifact_file=$(echo "$detection" | cut -d' ' -f2-)
116
+
117
+ log "phase=${phase} artifact=${artifact_file}"
118
+
119
+ # --- Deduplication ---
120
+ artifact_mtime=$(stat -f %m "$artifact_file" 2>/dev/null || stat -c %Y "$artifact_file" 2>/dev/null || echo 0)
121
+ sentinel_key="${phase}:${artifact_mtime}"
122
+
123
+ if [ -f "$SENTINEL_FILE" ]; then
124
+ last=$(cat "$SENTINEL_FILE")
125
+ if [ "$last" = "$sentinel_key" ]; then
126
+ log "dedup: skipping repeated event for $sentinel_key"
127
+ exit 0
128
+ fi
129
+ # Also check: if the sentinel's mtime component matches any recent artifact's mtime,
130
+ # the session was already processed (e.g. phase label changed but same artifact window).
131
+ last_mtime=$(echo "$last" | cut -d: -f2)
132
+ if [ -n "$last_mtime" ] && [ "$last_mtime" = "$artifact_mtime" ]; then
133
+ log "dedup: skipping repeated event for $sentinel_key (mtime match)"
134
+ exit 0
135
+ fi
136
+ # Also check all planning artifacts' mtimes against the sentinel's mtime
137
+ proj="$PROJECT_DIR"
138
+ while IFS= read -r -d '' f; do
139
+ fmtime=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null || echo 0)
140
+ if [ "$fmtime" = "$last_mtime" ]; then
141
+ log "dedup: skipping repeated event for $sentinel_key (artifact $f mtime match)"
142
+ exit 0
143
+ fi
144
+ done < <(find "$proj/.planning" \( -name "RESEARCH.md" -o -name "PLAN.md" -o -name "VERIFICATION.md" \) -print0 2>/dev/null)
145
+ fi
146
+
147
+ # Update sentinel
148
+ echo "$sentinel_key" > "$SENTINEL_FILE"
149
+
150
+ # --- Map phase to observer_mode ---
151
+ case "$phase" in
152
+ research) observer_mode="audit" ;;
153
+ plan) observer_mode="block" ;;
154
+ execute) observer_mode="augment" ;;
155
+ verify) observer_mode="audit" ;;
156
+ *) observer_mode="audit" ;;
157
+ esac
158
+
159
+ # --- Generate event_id ---
160
+ event_id=$(uuidgen 2>/dev/null || date +%s%N | md5sum | cut -c1-8)
161
+
162
+ # --- Collect changed files ---
163
+ changed_files="[]"
164
+ if command -v git &>/dev/null && git -C "$PROJECT_DIR" rev-parse --git-dir &>/dev/null; then
165
+ cf=$(git -C "$PROJECT_DIR" diff --name-only HEAD 2>/dev/null | \
166
+ grep -v "^\.planning" | \
167
+ python3 -c "import sys,json; lines=[l.strip() for l in sys.stdin if l.strip()]; print(json.dumps(lines))" 2>/dev/null || echo "[]")
168
+ changed_files="$cf"
169
+ fi
170
+
171
+ # --- Find planning artifacts ---
172
+ plan_path=$(find "$PROJECT_DIR/.planning" -name "PLAN.md" 2>/dev/null | head -1 || echo "null")
173
+ research_path=$(find "$PROJECT_DIR/.planning" -name "RESEARCH.md" 2>/dev/null | head -1 || echo "null")
174
+
175
+ [ -f "$plan_path" ] || plan_path="null"
176
+ [ -f "$research_path" ] || research_path="null"
177
+
178
+ # --- Write event JSON atomically ---
179
+ event_file="/tmp/gsd-event-${event_id}.json"
180
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
181
+
182
+ cat > "${event_file}.tmp" <<EOF
183
+ {
184
+ "event_id": "${event_id}",
185
+ "event_type": "phase_complete",
186
+ "gsd_phase": "${phase}",
187
+ "observer_mode": "${observer_mode}",
188
+ "context_summary": "${phase} phase detected: ${artifact_file}",
189
+ "artifacts": {
190
+ "plan": $([ "$plan_path" = "null" ] && echo "null" || echo "\"$plan_path\""),
191
+ "research": $([ "$research_path" = "null" ] && echo "null" || echo "\"$research_path\""),
192
+ "changed_files": ${changed_files},
193
+ "test_results": null
194
+ },
195
+ "worker_session": "${WORKER_SESSION}",
196
+ "worker_pane": "${WORKER_SESSION}:${WORKER_PANE}",
197
+ "project_dir": "${PROJECT_DIR}",
198
+ "timestamp": "${timestamp}"
199
+ }
200
+ EOF
201
+ mv "${event_file}.tmp" "$event_file"
202
+ log "Event written: $event_file"
203
+
204
+ # --- Dry run: skip tmux and polling ---
205
+ if [ -n "$DRY_RUN" ]; then
206
+ log "DRY_RUN: would wake observer and poll for response"
207
+ exit 0
208
+ fi
209
+
210
+ # --- Wake Observer ---
211
+ "$SCRIPTS_DIR/wake-observer.sh" "$event_id" "$OBSERVER_SESSION" "$OBSERVER_PANE" || {
212
+ log "WARNING: wake-observer.sh failed — continuing without Observer"
213
+ exit 0
214
+ }
215
+
216
+ # --- Poll for response ---
217
+ response_file="/tmp/gsd-response-${event_id}.json"
218
+ hold_count=0
219
+ elapsed=0
220
+
221
+ log "Polling for Observer response (max ${POLL_TIMEOUT}s)..."
222
+
223
+ while [ "$elapsed" -lt "$POLL_TIMEOUT" ]; do
224
+ if [ -f "$response_file" ]; then
225
+ # Validate event_id matches
226
+ resp_event_id=$(jq -r '.event_id // ""' "$response_file" 2>/dev/null || echo "")
227
+ if [ "$resp_event_id" != "$event_id" ]; then
228
+ # Mismatch — skip and keep polling
229
+ sleep 1
230
+ elapsed=$((elapsed+1))
231
+ continue
232
+ fi
233
+
234
+ decision=$(jq -r '.decision // "proceed"' "$response_file" 2>/dev/null || echo "proceed")
235
+ log "Observer decision: $decision"
236
+
237
+ if [ "$decision" = "hold" ]; then
238
+ hold_count=$((hold_count+1))
239
+ if [ "$hold_count" -ge "$HOLD_MAX" ]; then
240
+ log "Max hold cycles reached ($HOLD_MAX) — proceeding"
241
+ break
242
+ fi
243
+ log "Observer hold (cycle $hold_count/$HOLD_MAX) — resetting timer"
244
+ elapsed=0
245
+ # Remove response file so we detect the next overwrite
246
+ rm -f "$response_file"
247
+ continue
248
+ fi
249
+
250
+ # proceed or revise — done
251
+ break
252
+ fi
253
+
254
+ sleep 1
255
+ elapsed=$((elapsed+1))
256
+ done
257
+
258
+ if [ "$elapsed" -ge "$POLL_TIMEOUT" ]; then
259
+ log "WARNING: Observer did not respond in ${POLL_TIMEOUT}s — Worker continuing unblocked"
260
+ fi
261
+
262
+ exit 0
@@ -0,0 +1,18 @@
1
+ {
2
+ "_comment": "Event written by gsd-stop-hook.sh. Named /tmp/gsd-event-<event_id>.json",
3
+ "event_id": "a3f9c1d2",
4
+ "event_type": "phase_complete",
5
+ "gsd_phase": "execute",
6
+ "observer_mode": "augment",
7
+ "context_summary": "Execute phase detected: src/auth/login.swift modified",
8
+ "artifacts": {
9
+ "plan": "/Users/user/project/.planning/phase-3/PLAN.md",
10
+ "research": "/Users/user/project/.planning/phase-3/RESEARCH.md",
11
+ "changed_files": ["src/auth/login.swift", "src/auth/session.swift"],
12
+ "test_results": "/tmp/gsd-test-results-a3f9c1d2.txt"
13
+ },
14
+ "worker_session": "gsd-worker",
15
+ "worker_pane": "gsd-worker:0.0",
16
+ "project_dir": "/Users/user/project",
17
+ "timestamp": "2026-03-16T10:00:00Z"
18
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "_comment": "Response written by Observer Claude. Named /tmp/gsd-response-<event_id>.json",
3
+ "_decision_values": "proceed | revise | hold",
4
+ "event_id": "a3f9c1d2",
5
+ "decision": "revise",
6
+ "mode": "augment",
7
+ "observations": [
8
+ "Auth module missing input validation on login handler",
9
+ "Unit tests pass but no integration test for session expiry"
10
+ ],
11
+ "message": "Two issues found. Fix input validation before proceeding.",
12
+ "revision_instructions": "Add input validation to src/auth/login.swift login() function. Validate email format and non-empty password before calling authenticate().",
13
+ "timestamp": "2026-03-16T10:00:05Z"
14
+ }
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [ $# -lt 1 ]; then
5
+ echo "Usage: notify-worker.sh <event_id>" >&2
6
+ exit 1
7
+ fi
8
+
9
+ event_id="$1"
10
+ response_file="/tmp/gsd-response-${event_id}.json"
11
+ event_file="/tmp/gsd-event-${event_id}.json"
12
+
13
+ # Verify response file exists
14
+ if [ ! -f "$response_file" ]; then
15
+ echo "[notify-worker] ERROR: response file missing: $response_file" >&2
16
+ exit 1
17
+ fi
18
+
19
+ # Read worker_pane from event file (not response file)
20
+ if [ ! -f "$event_file" ]; then
21
+ echo "[notify-worker] ERROR: event file missing: $event_file" >&2
22
+ exit 1
23
+ fi
24
+
25
+ worker_pane=$(jq -r '.worker_pane // empty' "$event_file" 2>/dev/null)
26
+ if [ -z "$worker_pane" ]; then
27
+ echo "[notify-worker] ERROR: worker_pane not found in event file" >&2
28
+ exit 1
29
+ fi
30
+
31
+ # Parse response fields
32
+ decision=$(jq -r '.decision' "$response_file")
33
+ message=$(jq -r '.message' "$response_file")
34
+ revision=$(jq -r '.revision_instructions // ""' "$response_file")
35
+
36
+ # Build injection message
37
+ injection="[GSD Observer] Decision: ${decision}. ${message}"
38
+ if [ -n "$revision" ] && [ "$revision" != "null" ]; then
39
+ injection="${injection} Instructions: ${revision}"
40
+ fi
41
+
42
+ # Inject into Worker pane
43
+ # Worker Claude is blocked by the Stop hook polling loop.
44
+ # This text queues in the terminal buffer and appears when the hook exits.
45
+ tmux send-keys -t "$worker_pane" "$injection" Enter
46
+
47
+ echo "[notify-worker] Response injected into Worker pane (${worker_pane})" >&2
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SESSION="${GSD_OBSERVER_SESSION:-gsd-observer}"
4
+ tmux kill-session -t "$SESSION" 2>/dev/null || true
5
+ tmux new-session -d -s "$SESSION"
6
+ tmux send-keys -t "${SESSION}:0.0" "claude --allowedTools 'Bash,Read,Write,Glob,Grep'" Enter
7
+ echo "[gsd-observer] Waiting for Observer Claude to start..."
8
+ for i in $(seq 1 30); do
9
+ pane=$(tmux capture-pane -pt "${SESSION}:0.0" -l 5 2>/dev/null || echo "")
10
+ if echo "$pane" | grep -qE '❯|>|\$|✓|claude>'; then
11
+ echo "[gsd-observer] Observer ready in session: $SESSION"
12
+ exit 0
13
+ fi
14
+ sleep 1
15
+ done
16
+ echo "[gsd-observer] WARNING: Observer may not be ready — prompt not detected after 30s"
17
+ echo "[gsd-observer] Check: tmux attach -t $SESSION"
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ PROJECT_DIR="${1:-$(pwd)}"
4
+ SESSION="${GSD_WORKER_SESSION:-gsd-worker}"
5
+ if [ ! -d "$PROJECT_DIR" ]; then
6
+ echo "ERROR: project directory not found: $PROJECT_DIR" >&2
7
+ exit 1
8
+ fi
9
+ echo "[gsd-worker] Cleaning up stale temp files..."
10
+ rm -f /tmp/gsd-event-*.json /tmp/gsd-response-*.json /tmp/gsd-last-event-phase
11
+ tmux kill-session -t "$SESSION" 2>/dev/null || true
12
+ tmux new-session -d -s "$SESSION"
13
+ tmux send-keys -t "${SESSION}:0.0" "cd \"$PROJECT_DIR\" && GSD_OBSERVER_ENABLED=1 claude" Enter
14
+ echo "[gsd-worker] Worker ready in session: $SESSION"
15
+ echo "[gsd-worker] Project: $PROJECT_DIR"
16
+ echo ""
17
+ echo "Next steps:"
18
+ echo " 1. Attach to worker: tmux attach -t $SESSION"
19
+ echo " 2. Attach to observer: tmux attach -t ${GSD_OBSERVER_SESSION:-gsd-observer}"
20
+ echo " 3. Begin GSD workflow in the worker session"
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ WORKER_SESSION="${GSD_WORKER_SESSION:-gsd-worker}"
3
+ OBSERVER_SESSION="${GSD_OBSERVER_SESSION:-gsd-observer}"
4
+ echo "[gsd-observer] Tearing down sessions..."
5
+ tmux kill-session -t "$WORKER_SESSION" 2>/dev/null && echo " Killed: $WORKER_SESSION" || echo " Not running: $WORKER_SESSION"
6
+ tmux kill-session -t "$OBSERVER_SESSION" 2>/dev/null && echo " Killed: $OBSERVER_SESSION" || echo " Not running: $OBSERVER_SESSION"
7
+ echo "[gsd-observer] Cleaning up temp files..."
8
+ rm -f /tmp/gsd-event-*.json /tmp/gsd-response-*.json /tmp/gsd-last-event-phase
9
+ echo " Done."
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ WORKER_SESSION="${GSD_WORKER_SESSION:-gsd-worker}"
3
+ OBSERVER_SESSION="${GSD_OBSERVER_SESSION:-gsd-observer}"
4
+ PASS=0; FAIL=0
5
+
6
+ check() {
7
+ local desc="$1"; shift
8
+ if "$@" &>/dev/null; then
9
+ echo " OK: $desc"; PASS=$((PASS+1))
10
+ else
11
+ echo " MISSING: $desc"; FAIL=$((FAIL+1))
12
+ fi
13
+ }
14
+
15
+ echo "=== GSD Observer System Check ==="
16
+ echo ""
17
+ echo "--- Prerequisites ---"
18
+ check "jq installed" command -v jq
19
+ check "tmux installed" command -v tmux
20
+ check "claude CLI installed" command -v claude
21
+
22
+ echo ""
23
+ echo "--- Scripts ---"
24
+ check "gsd-stop-hook.sh executable" test -x "$HOME/.claude/gsd-observer/hooks/gsd-stop-hook.sh"
25
+ check "wake-observer.sh executable" test -x "$HOME/.claude/gsd-observer/scripts/wake-observer.sh"
26
+ check "notify-worker.sh executable" test -x "$HOME/.claude/gsd-observer/scripts/notify-worker.sh"
27
+ check "start-observer.sh executable" test -x "$HOME/.claude/gsd-observer/scripts/start-observer.sh"
28
+ check "start-worker.sh executable" test -x "$HOME/.claude/gsd-observer/scripts/start-worker.sh"
29
+
30
+ echo ""
31
+ echo "--- Sessions ---"
32
+ check "gsd-observer tmux session running" tmux has-session -t "$OBSERVER_SESSION"
33
+ check "gsd-worker tmux session running" tmux has-session -t "$WORKER_SESSION"
34
+
35
+ if tmux has-session -t "$OBSERVER_SESSION" 2>/dev/null; then
36
+ pane_content=$(tmux capture-pane -pt "${OBSERVER_SESSION}:0.0" -l 5 2>/dev/null || echo "")
37
+ if echo "$pane_content" | grep -qE '❯|>|\$|✓|claude>'; then
38
+ echo " OK: Observer Claude at prompt"; PASS=$((PASS+1))
39
+ else
40
+ echo " MISSING: Observer Claude not at prompt (may still be starting)"; FAIL=$((FAIL+1))
41
+ fi
42
+ fi
43
+
44
+ echo ""
45
+ echo "--- Project hook (run from project dir) ---"
46
+ if [ -f ".claude/settings.json" ]; then
47
+ hook_registered=$(python3 -c "
48
+ import sys, json
49
+ with open('.claude/settings.json') as f:
50
+ s = json.load(f)
51
+ hooks = s.get('hooks', {}).get('Stop', [])
52
+ print('yes' if hooks else 'no')
53
+ " 2>/dev/null || echo "no")
54
+ check "Stop hook registered in .claude/settings.json" [ "$hook_registered" = "yes" ]
55
+ else
56
+ echo " MISSING: .claude/settings.json (copy from ~/.claude/gsd-observer/templates/settings.json)"
57
+ FAIL=$((FAIL+1))
58
+ fi
59
+
60
+ echo ""
61
+ echo "Results: ${PASS} OK, ${FAIL} missing"
62
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [ $# -lt 3 ]; then
5
+ echo "Usage: wake-observer.sh <event_id> <observer_session> <observer_pane>" >&2
6
+ exit 1
7
+ fi
8
+
9
+ event_id="$1"
10
+ observer_session="$2"
11
+ observer_pane="$3"
12
+ full_target="${observer_session}:${observer_pane}"
13
+ event_path="/tmp/gsd-event-${event_id}.json"
14
+
15
+ # Verify event file exists before waking Observer
16
+ if [ ! -f "$event_path" ]; then
17
+ echo "[wake-observer] ERROR: event file not found: $event_path" >&2
18
+ exit 1
19
+ fi
20
+
21
+ # Wait up to 15s for Observer pane to be at prompt
22
+ echo "[wake-observer] Waiting for Observer pane to be ready..." >&2
23
+ ready=0
24
+ for i in $(seq 1 15); do
25
+ pane_content=$(tmux capture-pane -pt "$full_target" -l 5 2>/dev/null || echo "")
26
+ if echo "$pane_content" | grep -qE '❯|>|\$|✓|claude'; then
27
+ ready=1
28
+ break
29
+ fi
30
+ sleep 1
31
+ done
32
+
33
+ if [ "$ready" -eq 0 ]; then
34
+ echo "[wake-observer] WARNING: Observer pane not at prompt after 15s — injecting anyway" >&2
35
+ fi
36
+
37
+ # Inject task into Observer pane
38
+ tmux send-keys -t "$full_target" \
39
+ "Read ${event_path} and respond as GSD Observer. Write response to /tmp/gsd-response-${event_id}.json then run ~/.claude/gsd-observer/scripts/notify-worker.sh ${event_id}" \
40
+ Enter
41
+
42
+ echo "[wake-observer] Observer woken for event ${event_id}" >&2