gsd-init 1.0.16 → 1.0.17

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
@@ -13,22 +13,24 @@ const readline = require('readline');
13
13
  const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
14
14
 
15
15
  const TEMPLATE_FILES = [
16
- { src: 'agents/gsd-observer.md', dst: 'agents/gsd-observer.md' },
17
- { src: 'hooks/gsd-stop-hook.sh', dst: 'hooks/gsd-stop-hook.sh' },
18
- { src: 'scripts/start-observer.sh', dst: 'scripts/start-observer.sh' },
19
- { src: 'scripts/start-worker.sh', dst: 'scripts/start-worker.sh' },
20
- { src: 'scripts/wake-observer.sh', dst: 'scripts/wake-observer.sh' },
21
- { src: 'scripts/notify-worker.sh', dst: 'scripts/notify-worker.sh' },
22
- { src: 'scripts/teardown.sh', dst: 'scripts/teardown.sh' },
23
- { src: 'scripts/listen.sh', dst: 'scripts/listen.sh' },
24
- { src: 'scripts/start.sh', dst: 'scripts/start.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' },
16
+ { src: 'agents/gsd-observer.md', dst: 'agents/gsd-observer.md' },
17
+ { src: 'hooks/gsd-stop-hook.sh', dst: 'hooks/gsd-stop-hook.sh' },
18
+ { src: 'hooks/gsd-session-end-hook.sh', dst: 'hooks/gsd-session-end-hook.sh' },
19
+ { src: 'scripts/start-observer.sh', dst: 'scripts/start-observer.sh' },
20
+ { src: 'scripts/start-worker.sh', dst: 'scripts/start-worker.sh' },
21
+ { src: 'scripts/wake-observer.sh', dst: 'scripts/wake-observer.sh' },
22
+ { src: 'scripts/notify-worker.sh', dst: 'scripts/notify-worker.sh' },
23
+ { src: 'scripts/teardown.sh', dst: 'scripts/teardown.sh' },
24
+ { src: 'scripts/listen.sh', dst: 'scripts/listen.sh' },
25
+ { src: 'scripts/start.sh', dst: 'scripts/start.sh' },
26
+ { src: 'scripts/verify.sh', dst: 'scripts/verify.sh' },
27
+ { src: 'schema/event.json', dst: 'schema/event.json' },
28
+ { src: 'schema/response.json', dst: 'schema/response.json' },
28
29
  ];
29
30
 
30
31
  const SH_FILES = [
31
32
  'hooks/gsd-stop-hook.sh',
33
+ 'hooks/gsd-session-end-hook.sh',
32
34
  'scripts/start-observer.sh',
33
35
  'scripts/start-worker.sh',
34
36
  'scripts/wake-observer.sh',
@@ -43,13 +45,23 @@ function gsdRoot(projDir) {
43
45
  return process.env.GSD_INIT_OBS_ROOT || path.join(projDir, '.gsd');
44
46
  }
45
47
 
46
- function makeGsdEntry(projDir) {
48
+ function makeGsdStopEntry(projDir) {
47
49
  return {
48
50
  matcher: '',
49
51
  hooks: [{ type: 'command', command: path.join(gsdRoot(projDir), 'hooks', 'gsd-stop-hook.sh') }]
50
52
  };
51
53
  }
52
54
 
55
+ function makeGsdSessionEndEntry(projDir) {
56
+ return {
57
+ matcher: '',
58
+ hooks: [{ type: 'command', command: path.join(gsdRoot(projDir), 'hooks', 'gsd-session-end-hook.sh') }]
59
+ };
60
+ }
61
+
62
+ // Keep old name as alias for backwards compat
63
+ function makeGsdEntry(projDir) { return makeGsdStopEntry(projDir); }
64
+
53
65
  function getSettingsLabel(settingsPath) {
54
66
  if (!fs.existsSync(settingsPath)) return '[create]';
55
67
  var data;
@@ -102,10 +114,10 @@ function chmodScripts(obsRoot) {
102
114
  catch (e) { /* ignore */ }
103
115
  }
104
116
  }
105
- function mergeSettings(projDir, entry) {
117
+ function mergeSettings(projDir, stopEntry, sessionEndEntry) {
106
118
  var settingsPath = path.join(projDir, '.claude', 'settings.json');
107
119
  if (!fs.existsSync(settingsPath)) {
108
- var newData = { hooks: { Stop: [entry] } };
120
+ var newData = { hooks: { Stop: [stopEntry], SessionEnd: [sessionEndEntry] } };
109
121
  fs.writeFileSync(settingsPath, JSON.stringify(newData, null, 2) + '\n');
110
122
  return;
111
123
  }
@@ -114,15 +126,26 @@ function mergeSettings(projDir, entry) {
114
126
  try { data = JSON.parse(raw); }
115
127
  catch (e) { throw new Error('Malformed JSON in ' + settingsPath + ': ' + e.message); }
116
128
 
129
+ if (!data.hooks) data.hooks = {};
130
+
117
131
  var stopArr = (data.hooks && data.hooks.Stop) ? data.hooks.Stop : [];
118
- var alreadyRegistered = stopArr.some(function(e) {
132
+ var stopRegistered = stopArr.some(function(e) {
119
133
  return JSON.stringify(e).includes('gsd-stop-hook.sh');
120
134
  });
121
- if (alreadyRegistered) return;
135
+ if (!stopRegistered) {
136
+ if (!data.hooks.Stop) data.hooks.Stop = [];
137
+ data.hooks.Stop.push(stopEntry);
138
+ }
139
+
140
+ var endArr = (data.hooks && data.hooks.SessionEnd) ? data.hooks.SessionEnd : [];
141
+ var endRegistered = endArr.some(function(e) {
142
+ return JSON.stringify(e).includes('gsd-session-end-hook.sh');
143
+ });
144
+ if (!endRegistered) {
145
+ if (!data.hooks.SessionEnd) data.hooks.SessionEnd = [];
146
+ data.hooks.SessionEnd.push(sessionEndEntry);
147
+ }
122
148
 
123
- if (!data.hooks) data.hooks = {};
124
- if (!data.hooks.Stop) data.hooks.Stop = [];
125
- data.hooks.Stop.push(entry);
126
149
  fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n');
127
150
  }
128
151
  function prompt(question) {
@@ -166,7 +189,8 @@ async function run() {
166
189
  projDir = process.cwd();
167
190
  }
168
191
  var obsRoot = gsdRoot(projDir);
169
- var gsdEntry = makeGsdEntry(projDir);
192
+ var gsdStopEntry = makeGsdStopEntry(projDir);
193
+ var gsdSessionEndEntry = makeGsdSessionEndEntry(projDir);
170
194
 
171
195
  var ops = planInstall(TEMPLATES_DIR, obsRoot, projDir);
172
196
 
@@ -220,7 +244,7 @@ async function run() {
220
244
  catch (e) { console.error('Error setting permissions in ' + obsRoot + ': ' + e.message); process.exit(1); }
221
245
 
222
246
  // Step 5: merge settings.json
223
- try { mergeSettings(projDir, gsdEntry); }
247
+ try { mergeSettings(projDir, gsdStopEntry, gsdSessionEndEntry); }
224
248
  catch (e) { console.error(e.message); process.exit(1); }
225
249
 
226
250
  printSummary(projDir, projectName);
@@ -233,5 +257,5 @@ if (require.main === module) {
233
257
  module.exports = {
234
258
  planInstall, getSettingsLabel, formatDryRun, mkdirpSync,
235
259
  copyTemplates, chmodScripts, mergeSettings, printSummary,
236
- TEMPLATE_FILES, SH_FILES, makeGsdEntry, gsdRoot, run
260
+ TEMPLATE_FILES, SH_FILES, makeGsdEntry, makeGsdStopEntry, makeGsdSessionEndEntry, gsdRoot, run
237
261
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-init",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "Set up GSD observer/worker tmux system in a project",
5
5
  "bin": {
6
6
  "gsd-init": "bin/gsd.js"
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
2
+ # GSD Session End Hook — fires when Worker Claude session terminates.
3
+ # Sends a session_end event to the Observer so it can do a final review.
4
+ # Does NOT block — session is ending, fire-and-forget only.
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_PROJECT_DIR — project directory (default: pwd)
11
+ # GSD_WORKER_SESSION — worker tmux session (default: gsd-worker)
12
+ # GSD_WORKER_PANE — worker pane id (default: 0.0)
13
+
14
+ set -euo pipefail
15
+ SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../scripts"
16
+
17
+ OBSERVER_ENABLED="${GSD_OBSERVER_ENABLED:-}"
18
+ OBSERVER_SESSION="${GSD_OBSERVER_SESSION:-gsd-observer}"
19
+ OBSERVER_PANE="${GSD_OBSERVER_PANE:-0.0}"
20
+ PROJECT_DIR="${GSD_PROJECT_DIR:-$(pwd)}"
21
+ WORKER_SESSION="${GSD_WORKER_SESSION:-gsd-worker}"
22
+ WORKER_PANE="${GSD_WORKER_PANE:-0.0}"
23
+
24
+ # --- Guard: disabled ---
25
+ if [ -z "$OBSERVER_ENABLED" ]; then
26
+ exit 0
27
+ fi
28
+
29
+ log() { echo "[gsd-session-end] $*" >&2; }
30
+
31
+ # --- Generate event_id ---
32
+ event_id=$(uuidgen 2>/dev/null || date +%s%N | md5sum | cut -c1-8)
33
+
34
+ # --- Write session_end event JSON ---
35
+ event_file="/tmp/gsd-event-${event_id}.json"
36
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
37
+
38
+ cat > "${event_file}.tmp" <<EOF
39
+ {
40
+ "event_id": "${event_id}",
41
+ "event_type": "session_end",
42
+ "gsd_phase": "session_end",
43
+ "observer_mode": "audit",
44
+ "context_summary": "Worker Claude session ended — final review opportunity",
45
+ "artifacts": {
46
+ "plan": null,
47
+ "research": null,
48
+ "changed_files": [],
49
+ "test_results": null
50
+ },
51
+ "worker_session": "${WORKER_SESSION}",
52
+ "worker_pane": "${WORKER_SESSION}:${WORKER_PANE}",
53
+ "project_dir": "${PROJECT_DIR}",
54
+ "timestamp": "${timestamp}"
55
+ }
56
+ EOF
57
+ mv "${event_file}.tmp" "$event_file"
58
+ log "Session-end event written: $event_file"
59
+
60
+ # --- Wake Observer (fire-and-forget, no polling) ---
61
+ "$SCRIPTS_DIR/wake-observer.sh" "$event_id" "$OBSERVER_SESSION" "$OBSERVER_PANE" || \
62
+ log "WARNING: wake-observer.sh failed — Observer not notified of session end"
63
+
64
+ exit 0
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
2
+ # GSD Session End Hook — fires when Worker Claude session terminates.
3
+ # Sends a session_end event to the Observer so it can do a final review.
4
+ # Does NOT block — session is ending, fire-and-forget only.
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_PROJECT_DIR — project directory (default: pwd)
11
+ # GSD_WORKER_SESSION — worker tmux session (default: gsd-worker)
12
+ # GSD_WORKER_PANE — worker pane id (default: 0.0)
13
+
14
+ set -euo pipefail
15
+ SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+
17
+ OBSERVER_ENABLED="${GSD_OBSERVER_ENABLED:-}"
18
+ OBSERVER_SESSION="${GSD_OBSERVER_SESSION:-gsd-observer}"
19
+ OBSERVER_PANE="${GSD_OBSERVER_PANE:-0.0}"
20
+ PROJECT_DIR="${GSD_PROJECT_DIR:-$(pwd)}"
21
+ WORKER_SESSION="${GSD_WORKER_SESSION:-gsd-worker}"
22
+ WORKER_PANE="${GSD_WORKER_PANE:-0.0}"
23
+
24
+ # --- Guard: disabled ---
25
+ if [ -z "$OBSERVER_ENABLED" ]; then
26
+ exit 0
27
+ fi
28
+
29
+ log() { echo "[gsd-session-end] $*" >&2; }
30
+
31
+ # --- Generate event_id ---
32
+ event_id=$(uuidgen 2>/dev/null || date +%s%N | md5sum | cut -c1-8)
33
+
34
+ # --- Write session_end event JSON ---
35
+ event_file="/tmp/gsd-event-${event_id}.json"
36
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
37
+
38
+ cat > "${event_file}.tmp" <<EOF
39
+ {
40
+ "event_id": "${event_id}",
41
+ "event_type": "session_end",
42
+ "gsd_phase": "session_end",
43
+ "observer_mode": "audit",
44
+ "context_summary": "Worker Claude session ended — final review opportunity",
45
+ "artifacts": {
46
+ "plan": null,
47
+ "research": null,
48
+ "changed_files": [],
49
+ "test_results": null
50
+ },
51
+ "worker_session": "${WORKER_SESSION}",
52
+ "worker_pane": "${WORKER_SESSION}:${WORKER_PANE}",
53
+ "project_dir": "${PROJECT_DIR}",
54
+ "timestamp": "${timestamp}"
55
+ }
56
+ EOF
57
+ mv "${event_file}.tmp" "$event_file"
58
+ log "Session-end event written: $event_file"
59
+
60
+ # --- Wake Observer (fire-and-forget, no polling) ---
61
+ "$SCRIPTS_DIR/wake-observer.sh" "$event_id" "$OBSERVER_SESSION" "$OBSERVER_PANE" || \
62
+ log "WARNING: wake-observer.sh failed — Observer not notified of session end"
63
+
64
+ exit 0
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
+ SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
3
4
 
4
5
  if [ $# -lt 3 ]; then
5
6
  echo "Usage: wake-observer.sh <event_id> <observer_session> <observer_pane>" >&2
@@ -35,8 +36,10 @@ if [ "$ready" -eq 0 ]; then
35
36
  fi
36
37
 
37
38
  # Inject task into Observer pane
39
+ # Quote the notify script path to handle spaces in SCRIPTS_DIR
40
+ notify_cmd="'${SCRIPTS_DIR}/notify-worker.sh'"
38
41
  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 ${SCRIPTS_DIR}/notify-worker.sh ${event_id}" \
42
+ "Read ${event_path} and respond as GSD Observer. Write response to /tmp/gsd-response-${event_id}.json then run ${notify_cmd} ${event_id}" \
40
43
  Enter
41
44
 
42
45
  echo "[wake-observer] Observer woken for event ${event_id}" >&2