gsd-init 1.0.16 → 1.0.18
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',
|
|
17
|
-
{ src: 'hooks/gsd-stop-hook.sh',
|
|
18
|
-
{ src: '
|
|
19
|
-
{ src: 'scripts/start-
|
|
20
|
-
{ src: 'scripts/
|
|
21
|
-
{ src: 'scripts/
|
|
22
|
-
{ src: 'scripts/
|
|
23
|
-
{ src: 'scripts/
|
|
24
|
-
{ src: 'scripts/
|
|
25
|
-
{ src: 'scripts/
|
|
26
|
-
{ src: '
|
|
27
|
-
{ src: 'schema/
|
|
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
|
|
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,
|
|
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: [
|
|
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
|
|
132
|
+
var stopRegistered = stopArr.some(function(e) {
|
|
119
133
|
return JSON.stringify(e).includes('gsd-stop-hook.sh');
|
|
120
134
|
});
|
|
121
|
-
if (
|
|
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
|
|
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,
|
|
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
|
@@ -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
|
|
@@ -11,7 +11,7 @@ tmux send-keys -t "${SESSION}:0.0" "claude --allowedTools 'Bash,Read,Write,Glob,
|
|
|
11
11
|
|
|
12
12
|
# Pane 0.1 — Listener daemon (split horizontally, small strip at bottom)
|
|
13
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
|
|
14
|
+
tmux send-keys -t "${SESSION}:0.1" "GSD_OBSERVER_SESSION=${SESSION} \"${SCRIPTS_DIR}/listen.sh\"" Enter
|
|
15
15
|
|
|
16
16
|
# Focus Claude pane
|
|
17
17
|
tmux select-pane -t "${SESSION}:0.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 ${
|
|
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
|