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 +41 -0
- package/bin/gsd-init.js +216 -0
- package/package.json +14 -0
- package/templates/agents/gsd-observer.md +55 -0
- package/templates/hooks/gsd-stop-hook.sh +262 -0
- package/templates/schema/event.json +18 -0
- package/templates/schema/response.json +14 -0
- package/templates/scripts/notify-worker.sh +47 -0
- package/templates/scripts/start-observer.sh +17 -0
- package/templates/scripts/start-worker.sh +20 -0
- package/templates/scripts/teardown.sh +9 -0
- package/templates/scripts/verify.sh +62 -0
- package/templates/scripts/wake-observer.sh +42 -0
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
|
+
```
|
package/bin/gsd-init.js
ADDED
|
@@ -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
|