gsd-init 1.0.5 → 1.0.7

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/bin/gsd-init.js CHANGED
@@ -8,11 +8,9 @@ if (major < 16) {
8
8
  }
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
- const os = require('os');
12
11
  const readline = require('readline');
13
12
 
14
13
  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
14
 
17
15
  const TEMPLATE_FILES = [
18
16
  { src: 'agents/gsd-observer.md', dst: 'agents/gsd-observer.md' },
@@ -22,6 +20,7 @@ const TEMPLATE_FILES = [
22
20
  { src: 'scripts/wake-observer.sh', dst: 'scripts/wake-observer.sh' },
23
21
  { src: 'scripts/notify-worker.sh', dst: 'scripts/notify-worker.sh' },
24
22
  { src: 'scripts/teardown.sh', dst: 'scripts/teardown.sh' },
23
+ { src: 'scripts/listen.sh', dst: 'scripts/listen.sh' },
25
24
  { src: 'scripts/start.sh', dst: 'scripts/start.sh' },
26
25
  { src: 'scripts/verify.sh', dst: 'scripts/verify.sh' },
27
26
  { src: 'schema/event.json', dst: 'schema/event.json' },
@@ -35,14 +34,21 @@ const SH_FILES = [
35
34
  'scripts/wake-observer.sh',
36
35
  'scripts/notify-worker.sh',
37
36
  'scripts/teardown.sh',
37
+ 'scripts/listen.sh',
38
38
  'scripts/start.sh',
39
39
  'scripts/verify.sh',
40
40
  ];
41
41
 
42
- const GSD_ENTRY = {
43
- matcher: '',
44
- hooks: [{ type: 'command', command: '~/.claude/gsd-observer/hooks/gsd-stop-hook.sh' }]
45
- };
42
+ function gsdRoot(projDir) {
43
+ return process.env.GSD_INIT_OBS_ROOT || path.join(projDir, '.gsd');
44
+ }
45
+
46
+ function makeGsdEntry(projDir) {
47
+ return {
48
+ matcher: '',
49
+ hooks: [{ type: 'command', command: path.join(gsdRoot(projDir), 'hooks', 'gsd-stop-hook.sh') }]
50
+ };
51
+ }
46
52
 
47
53
  function getSettingsLabel(settingsPath) {
48
54
  if (!fs.existsSync(settingsPath)) return '[create]';
@@ -57,28 +63,24 @@ function getSettingsLabel(settingsPath) {
57
63
  }
58
64
 
59
65
  function planInstall(tmplDir, obsRoot, projDir) {
60
- var homeDir = os.homedir();
61
66
  var ops = [];
62
67
  for (var i = 0; i < TEMPLATE_FILES.length; i++) {
63
68
  var f = TEMPLATE_FILES[i];
64
69
  var dest = path.join(obsRoot, f.dst);
65
70
  var label = fs.existsSync(dest) ? '[overwrite]' : '[create]';
66
- var relDisplay = dest.startsWith(homeDir)
67
- ? '~' + dest.slice(homeDir.length)
68
- : dest;
69
- ops.push({ dest: dest, label: label, relative: relDisplay });
71
+ // Display relative to projDir
72
+ var rel = path.relative(projDir, dest);
73
+ ops.push({ dest: dest, label: label, relative: rel });
70
74
  }
71
75
  var settingsPath = path.join(projDir, '.claude', 'settings.json');
72
76
  ops.push({ dest: settingsPath, label: getSettingsLabel(settingsPath), relative: '.claude/settings.json' });
73
77
  return ops;
74
78
  }
75
79
 
76
- // Stubs — implemented in later tasks
77
80
  function formatDryRun(ops) {
78
81
  var lines = ['gsd-init — GSD Observer/Worker setup', '', 'Files to install:'];
79
82
  for (var i = 0; i < ops.length; i++) {
80
83
  var op = ops[i];
81
- // Pad label to 14 chars for alignment: '[create]' is 8, '[overwrite]' is 11, '[merge]' is 7, '[skip]' is 6
82
84
  var padded = op.label + ' '.repeat(Math.max(1, 14 - op.label.length));
83
85
  lines.push(' ' + padded + op.relative);
84
86
  }
@@ -96,7 +98,8 @@ function copyTemplates(tmplDir, obsRoot) {
96
98
  }
97
99
  function chmodScripts(obsRoot) {
98
100
  for (var i = 0; i < SH_FILES.length; i++) {
99
- fs.chmodSync(path.join(obsRoot, SH_FILES[i]), 0o755);
101
+ try { fs.chmodSync(path.join(obsRoot, SH_FILES[i]), 0o755); }
102
+ catch (e) { /* ignore */ }
100
103
  }
101
104
  }
102
105
  function mergeSettings(projDir, entry) {
@@ -132,18 +135,18 @@ function prompt(question) {
132
135
  });
133
136
  }
134
137
 
135
- function printSummary() {
138
+ function printSummary(projDir) {
139
+ var gsdScripts = path.join(gsdRoot(projDir), 'scripts');
136
140
  console.log([
137
141
  '',
138
142
  'Done! Next steps:',
139
- ' ~/.claude/gsd-observer/scripts/start.sh <project-name> [project-dir]',
143
+ ' ' + path.join(gsdScripts, 'start.sh') + ' <project-name>',
140
144
  '',
141
- ' Creates tmux sessions named gsd-observer-<project-name> and gsd-worker-<project-name>.',
142
- ' project-dir defaults to current directory if omitted.',
145
+ ' Creates tmux sessions gsd-observer-<project-name> and gsd-worker-<project-name>.',
143
146
  ' Opens Terminal windows attached to each session.',
144
147
  '',
145
- ' Observer agent prompt: ~/.claude/gsd-observer/agents/gsd-observer.md',
146
- ' Verify setup: ~/.claude/gsd-observer/scripts/verify.sh',
148
+ ' Observer agent: ' + path.join(gsdRoot(projDir), 'agents', 'gsd-observer.md'),
149
+ ' Verify setup: ' + path.join(gsdScripts, 'verify.sh'),
147
150
  ].join('\n'));
148
151
  }
149
152
 
@@ -151,8 +154,10 @@ async function run() {
151
154
  var args = process.argv.slice(2);
152
155
  var skipPrompt = args.indexOf('--yes') !== -1 || args.indexOf('-y') !== -1;
153
156
  var projDir = process.env.GSD_INIT_PROJ_DIR || process.cwd();
157
+ var obsRoot = gsdRoot(projDir);
158
+ var gsdEntry = makeGsdEntry(projDir);
154
159
 
155
- var ops = planInstall(TEMPLATES_DIR, OBS_ROOT, projDir);
160
+ var ops = planInstall(TEMPLATES_DIR, obsRoot, projDir);
156
161
 
157
162
  // Abort on malformed settings.json early
158
163
  var errOp = null;
@@ -176,10 +181,10 @@ async function run() {
176
181
  }
177
182
  }
178
183
 
179
- // Step 1: create ~/.claude/gsd-observer subdirs
184
+ // Step 1: create .gsd/ subdirs
180
185
  var subdirs = ['agents', 'hooks', 'scripts', 'schema'];
181
186
  for (var j = 0; j < subdirs.length; j++) {
182
- var subdir = path.join(OBS_ROOT, subdirs[j]);
187
+ var subdir = path.join(obsRoot, subdirs[j]);
183
188
  try { mkdirpSync(subdir); }
184
189
  catch (e) { console.error('Error creating ' + subdir + ': ' + e.message); process.exit(1); }
185
190
  }
@@ -190,18 +195,18 @@ async function run() {
190
195
  catch (e) { console.error('Error creating ' + dotClaudeDir + ': ' + e.message); process.exit(1); }
191
196
 
192
197
  // Step 3: copy templates
193
- try { copyTemplates(TEMPLATES_DIR, OBS_ROOT); }
194
- catch (e) { console.error('Error copying templates to ' + OBS_ROOT + ': ' + e.message); process.exit(1); }
198
+ try { copyTemplates(TEMPLATES_DIR, obsRoot); }
199
+ catch (e) { console.error('Error copying templates to ' + obsRoot + ': ' + e.message); process.exit(1); }
195
200
 
196
201
  // Step 4: chmod .sh files
197
- try { chmodScripts(OBS_ROOT); }
198
- catch (e) { console.error('Error setting permissions in ' + OBS_ROOT + ': ' + e.message); process.exit(1); }
202
+ try { chmodScripts(obsRoot); }
203
+ catch (e) { console.error('Error setting permissions in ' + obsRoot + ': ' + e.message); process.exit(1); }
199
204
 
200
205
  // Step 5: merge settings.json
201
- try { mergeSettings(projDir, GSD_ENTRY); }
206
+ try { mergeSettings(projDir, gsdEntry); }
202
207
  catch (e) { console.error(e.message); process.exit(1); }
203
208
 
204
- printSummary();
209
+ printSummary(projDir);
205
210
  }
206
211
 
207
212
  if (require.main === module) {
@@ -211,5 +216,5 @@ if (require.main === module) {
211
216
  module.exports = {
212
217
  planInstall, getSettingsLabel, formatDryRun, mkdirpSync,
213
218
  copyTemplates, chmodScripts, mergeSettings, printSummary,
214
- TEMPLATE_FILES, SH_FILES, GSD_ENTRY
219
+ TEMPLATE_FILES, SH_FILES, makeGsdEntry, gsdRoot
215
220
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-init",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Set up GSD observer/worker tmux system in a project",
5
5
  "bin": {
6
6
  "gsd-init": "bin/gsd-init.js"
@@ -43,9 +43,9 @@ When you receive an instruction to read an event file and respond:
43
43
  }
44
44
  ```
45
45
 
46
- 6. Run the notify script:
46
+ 6. Run the notify script (path is provided in the wake instruction):
47
47
  ```bash
48
- ~/.claude/gsd-observer/scripts/notify-worker.sh <event_id>
48
+ <project_dir>/.gsd/scripts/notify-worker.sh <event_id>
49
49
  ```
50
50
 
51
51
  **Rules:**
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # GSD Listener — runs in observer session, polls for new events and wakes Observer Claude.
3
+ OBSERVER_SESSION="${GSD_OBSERVER_SESSION:-gsd-observer}"
4
+ OBSERVER_PANE="${GSD_OBSERVER_PANE:-0.0}"
5
+ SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ POLL_INTERVAL=2
7
+
8
+ log() { echo "[gsd-listener] $*"; }
9
+
10
+ log "Listening for GSD events... (session: ${OBSERVER_SESSION}, pane: ${OBSERVER_PANE})"
11
+
12
+ declare -A seen
13
+
14
+ while true; do
15
+ for event_file in /tmp/gsd-event-*.json; do
16
+ [ -f "$event_file" ] || continue
17
+ event_id="${event_file#/tmp/gsd-event-}"
18
+ event_id="${event_id%.json}"
19
+
20
+ # Skip already seen or already responded
21
+ [ -n "${seen[$event_id]+_}" ] && continue
22
+ if [ -f "/tmp/gsd-response-${event_id}.json" ]; then
23
+ seen[$event_id]=1
24
+ continue
25
+ fi
26
+
27
+ log "New event: $event_id — waking Observer Claude"
28
+ seen[$event_id]=1
29
+ "$SCRIPTS_DIR/wake-observer.sh" "$event_id" "$OBSERVER_SESSION" "$OBSERVER_PANE" || \
30
+ log "WARNING: wake-observer.sh failed for $event_id"
31
+ done
32
+
33
+ sleep "$POLL_INTERVAL"
34
+ done
@@ -1,9 +1,21 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
  SESSION="${GSD_OBSERVER_SESSION:-gsd-observer}"
4
+ SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+
4
6
  tmux kill-session -t "$SESSION" 2>/dev/null || true
5
7
  tmux new-session -d -s "$SESSION"
8
+
9
+ # Pane 0.0 — Observer Claude
6
10
  tmux send-keys -t "${SESSION}:0.0" "claude --allowedTools 'Bash,Read,Write,Glob,Grep'" Enter
11
+
12
+ # Pane 0.1 — Listener daemon (split horizontally, small strip at bottom)
13
+ tmux split-window -t "${SESSION}:0" -v -l 6
14
+ tmux send-keys -t "${SESSION}:0.1" "GSD_OBSERVER_SESSION=${SESSION} ${SCRIPTS_DIR}/listen.sh" Enter
15
+
16
+ # Focus Claude pane
17
+ tmux select-pane -t "${SESSION}:0.0"
18
+
7
19
  echo "[gsd-observer] Waiting for Observer Claude to start..."
8
20
  for i in $(seq 1 30); do
9
21
  pane=$(tmux capture-pane -pt "${SESSION}:0.0" 2>/dev/null || echo "")
@@ -22,7 +22,7 @@ fi
22
22
  echo "[wake-observer] Waiting for Observer pane to be ready..." >&2
23
23
  ready=0
24
24
  for i in $(seq 1 15); do
25
- pane_content=$(tmux capture-pane -pt "$full_target" -l 5 2>/dev/null || echo "")
25
+ pane_content=$(tmux capture-pane -pt "$full_target" 2>/dev/null || echo "")
26
26
  if echo "$pane_content" | grep -qE '❯|>|\$|✓|claude'; then
27
27
  ready=1
28
28
  break
@@ -36,7 +36,7 @@ fi
36
36
 
37
37
  # Inject task into Observer pane
38
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}" \
39
+ "Read ${event_path} and respond as GSD Observer. Write response to /tmp/gsd-response-${event_id}.json then run ${SCRIPTS_DIR}/notify-worker.sh ${event_id}" \
40
40
  Enter
41
41
 
42
42
  echo "[wake-observer] Observer woken for event ${event_id}" >&2