delimit-cli 4.0.2 → 4.0.4
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 +9 -242
- package/bin/delimit-cli.js +352 -4
- package/bin/delimit-setup.js +37 -6
- package/gateway/ai/agent_dispatch.py +34 -2
- package/gateway/ai/github_scanner.py +1 -1
- package/gateway/ai/ledger_manager.py +13 -3
- package/gateway/ai/ledger_propose.py +240 -0
- package/gateway/ai/loop_engine.py +175 -372
- package/gateway/ai/notify.py +700 -13
- package/gateway/ai/reddit_proxy.py +106 -0
- package/gateway/ai/reddit_scanner.py +34 -0
- package/gateway/ai/server.py +343 -81
- package/gateway/ai/siem_streaming.py +290 -0
- package/gateway/ai/social_daemon.py +189 -0
- package/gateway/ai/swarm.py +434 -0
- package/lib/continuity-resolver.js +325 -0
- package/lib/cross-model-hooks.js +214 -2
- package/lib/delimit-template.js +5 -0
- package/lib/session-shell.js +655 -0
- package/lib/session-worker.js +479 -0
- package/package.json +1 -1
- package/scripts/security-check.sh +12 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const KNOWN_WORKSPACES_FILE = path.join(os.homedir(), '.delimit', 'known_workspaces.json');
|
|
6
|
+
const WORKSPACE_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
7
|
+
const ACTIVE_VENTURE_FILE = path.join(os.homedir(), '.delimit', 'active_venture.json');
|
|
8
|
+
|
|
9
|
+
function resolveRepoRoot(startDir = process.cwd()) {
|
|
10
|
+
let current = path.resolve(startDir);
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const homeDir = os.homedir();
|
|
13
|
+
|
|
14
|
+
while (!seen.has(current)) {
|
|
15
|
+
seen.add(current);
|
|
16
|
+
if (
|
|
17
|
+
fs.existsSync(path.join(current, '.git'))
|
|
18
|
+
|| (
|
|
19
|
+
current !== homeDir
|
|
20
|
+
&& fs.existsSync(path.join(current, '.delimit', 'ledger', 'operations.jsonl'))
|
|
21
|
+
)
|
|
22
|
+
) {
|
|
23
|
+
return current;
|
|
24
|
+
}
|
|
25
|
+
const parent = path.dirname(current);
|
|
26
|
+
if (parent === current) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
current = parent;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readGitHubIdentity(homeDir) {
|
|
35
|
+
const candidates = [
|
|
36
|
+
path.join(homeDir, '.config', 'gh', 'hosts.yml'),
|
|
37
|
+
path.join(homeDir, '.delimit', 'github-user.json')
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
for (const candidate of candidates) {
|
|
41
|
+
if (!fs.existsSync(candidate)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const content = fs.readFileSync(candidate, 'utf8');
|
|
46
|
+
const loginMatch = content.match(/user:\s*([^\s]+)/) || content.match(/"login"\s*:\s*"([^"]+)"/);
|
|
47
|
+
if (loginMatch) {
|
|
48
|
+
return loginMatch[1];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return process.env.GITHUB_USER || process.env.GITHUB_ACTOR || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sanitizeSegment(value, fallback) {
|
|
56
|
+
if (!value || typeof value !== 'string') {
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
const cleaned = value.trim().replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
60
|
+
return cleaned || fallback;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveLedgerRoot(repoGovernanceRoot, delimitHome) {
|
|
64
|
+
const repoLedgerRoot = repoGovernanceRoot ? path.join(repoGovernanceRoot, 'ledger') : null;
|
|
65
|
+
if (repoLedgerRoot && fs.existsSync(path.join(repoLedgerRoot, 'operations.jsonl'))) {
|
|
66
|
+
return {
|
|
67
|
+
ledgerRoot: repoLedgerRoot,
|
|
68
|
+
ledgerScope: 'repo',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
ledgerRoot: path.join(delimitHome, 'ledger'),
|
|
73
|
+
ledgerScope: 'global',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readJson(filePath, fallback = null) {
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
80
|
+
} catch {
|
|
81
|
+
return fallback;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeJson(filePath, value) {
|
|
86
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
87
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function countImmediateRepoLedgers(basePath) {
|
|
91
|
+
if (!fs.existsSync(basePath)) {
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
let count = 0;
|
|
95
|
+
for (const child of ['ventures', 'apps', 'repos']) {
|
|
96
|
+
const childRoot = path.join(basePath, child);
|
|
97
|
+
if (!fs.existsSync(childRoot)) continue;
|
|
98
|
+
for (const entry of fs.readdirSync(childRoot, { withFileTypes: true })) {
|
|
99
|
+
if (!entry.isDirectory()) continue;
|
|
100
|
+
const ledgerFile = path.join(childRoot, entry.name, '.delimit', 'ledger', 'operations.jsonl');
|
|
101
|
+
if (fs.existsSync(ledgerFile)) {
|
|
102
|
+
count += 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return count;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function loadKnownWorkspaces() {
|
|
110
|
+
const data = readJson(KNOWN_WORKSPACES_FILE, { workspaces: [] });
|
|
111
|
+
const workspaces = Array.isArray(data?.workspaces) ? data.workspaces : [];
|
|
112
|
+
return workspaces.filter(item => item && typeof item.path === 'string');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function saveKnownWorkspace(candidate) {
|
|
116
|
+
const existing = loadKnownWorkspaces();
|
|
117
|
+
const now = new Date().toISOString();
|
|
118
|
+
const next = existing.filter(item => item.path !== candidate.path);
|
|
119
|
+
next.unshift({
|
|
120
|
+
path: candidate.path,
|
|
121
|
+
source: candidate.source,
|
|
122
|
+
score: candidate.score,
|
|
123
|
+
updatedAt: now,
|
|
124
|
+
});
|
|
125
|
+
writeJson(KNOWN_WORKSPACES_FILE, { workspaces: next.slice(0, 20) });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function loadActiveVenture() {
|
|
129
|
+
return readJson(ACTIVE_VENTURE_FILE, null);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function saveActiveVenture(entry) {
|
|
133
|
+
writeJson(ACTIVE_VENTURE_FILE, {
|
|
134
|
+
venture: entry.venture,
|
|
135
|
+
repoRoot: entry.repoRoot || null,
|
|
136
|
+
updatedAt: new Date().toISOString(),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function scoreWorkspaceCandidate(candidatePath) {
|
|
141
|
+
if (!candidatePath || !fs.existsSync(candidatePath)) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const venturesPath = path.join(candidatePath, 'ventures');
|
|
145
|
+
const appsPath = path.join(candidatePath, 'apps');
|
|
146
|
+
const reposPath = path.join(candidatePath, 'repos');
|
|
147
|
+
const rootLedger = path.join(candidatePath, '.delimit', 'ledger', 'operations.jsonl');
|
|
148
|
+
const repoLedgerCount = countImmediateRepoLedgers(candidatePath);
|
|
149
|
+
const score =
|
|
150
|
+
(fs.existsSync(venturesPath) ? 40 : 0) +
|
|
151
|
+
(fs.existsSync(appsPath) ? 20 : 0) +
|
|
152
|
+
(fs.existsSync(reposPath) ? 20 : 0) +
|
|
153
|
+
(fs.existsSync(rootLedger) ? 10 : 0) +
|
|
154
|
+
Math.min(repoLedgerCount, 10) * 3;
|
|
155
|
+
return {
|
|
156
|
+
path: candidatePath,
|
|
157
|
+
score,
|
|
158
|
+
repoLedgerCount,
|
|
159
|
+
hasVentures: fs.existsSync(venturesPath),
|
|
160
|
+
hasApps: fs.existsSync(appsPath),
|
|
161
|
+
hasRepos: fs.existsSync(reposPath),
|
|
162
|
+
hasRootLedger: fs.existsSync(rootLedger),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function listVentureLedgers(repoBase, delimitHome) {
|
|
167
|
+
const results = [];
|
|
168
|
+
const seen = new Set();
|
|
169
|
+
|
|
170
|
+
const addLedger = (ledgerRoot, venture, repoRoot, scope) => {
|
|
171
|
+
const key = `${scope}:${ledgerRoot}`;
|
|
172
|
+
if (seen.has(key)) return;
|
|
173
|
+
seen.add(key);
|
|
174
|
+
results.push({ ledgerRoot, venture, repoRoot, scope });
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const globalLedgerRoot = path.join(delimitHome, 'ledger');
|
|
178
|
+
if (fs.existsSync(path.join(globalLedgerRoot, 'operations.jsonl'))) {
|
|
179
|
+
addLedger(globalLedgerRoot, 'root', null, 'global');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const scanRoots = [repoBase];
|
|
183
|
+
for (const child of ['ventures', 'apps', 'repos']) {
|
|
184
|
+
const childRoot = path.join(repoBase, child);
|
|
185
|
+
if (fs.existsSync(childRoot)) {
|
|
186
|
+
scanRoots.push(childRoot);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const scanRoot of scanRoots) {
|
|
191
|
+
if (fs.existsSync(scanRoot)) {
|
|
192
|
+
for (const entry of fs.readdirSync(scanRoot, { withFileTypes: true })) {
|
|
193
|
+
if (!entry.isDirectory()) continue;
|
|
194
|
+
const repoRoot = path.join(scanRoot, entry.name);
|
|
195
|
+
const ledgerRoot = path.join(repoRoot, '.delimit', 'ledger');
|
|
196
|
+
const ventureName = path.relative(repoBase, repoRoot) || entry.name;
|
|
197
|
+
if (fs.existsSync(path.join(ledgerRoot, 'operations.jsonl'))) {
|
|
198
|
+
addLedger(ledgerRoot, ventureName, repoRoot, 'repo');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function detectRepoBase(repoRoot) {
|
|
208
|
+
const explicit = process.env.DELIMIT_REPO_BASE;
|
|
209
|
+
if (explicit) {
|
|
210
|
+
return explicit;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const candidates = [];
|
|
214
|
+
const seen = new Set();
|
|
215
|
+
const pushCandidate = (candidatePath, source) => {
|
|
216
|
+
if (!candidatePath || seen.has(candidatePath)) return;
|
|
217
|
+
seen.add(candidatePath);
|
|
218
|
+
const scored = scoreWorkspaceCandidate(candidatePath);
|
|
219
|
+
if (scored && scored.score > 0) {
|
|
220
|
+
candidates.push({ ...scored, source });
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (repoRoot) {
|
|
225
|
+
let current = path.dirname(repoRoot);
|
|
226
|
+
const stopAt = path.parse(current).root;
|
|
227
|
+
while (current && current !== stopAt) {
|
|
228
|
+
pushCandidate(current, 'ancestor');
|
|
229
|
+
current = path.dirname(current);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const known of loadKnownWorkspaces()) {
|
|
234
|
+
const ageMs = Date.now() - new Date(known.updatedAt || 0).getTime();
|
|
235
|
+
if (ageMs < WORKSPACE_CACHE_TTL_MS) {
|
|
236
|
+
pushCandidate(known.path, 'cache');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const homeDir = os.homedir();
|
|
241
|
+
for (const candidate of [
|
|
242
|
+
path.join(homeDir, 'projects'),
|
|
243
|
+
path.join(homeDir, 'repos'),
|
|
244
|
+
path.join(homeDir, 'ventures'),
|
|
245
|
+
'/projects',
|
|
246
|
+
'/workspace',
|
|
247
|
+
'/workspaces',
|
|
248
|
+
os.homedir(),
|
|
249
|
+
]) {
|
|
250
|
+
pushCandidate(candidate, 'heuristic');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
254
|
+
if (candidates.length > 0) {
|
|
255
|
+
saveKnownWorkspace(candidates[0]);
|
|
256
|
+
return candidates[0].path;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return repoRoot ? path.dirname(repoRoot) : homeDir;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function resolveContinuityContext(options = {}) {
|
|
263
|
+
const homeDir = process.env.DELIMIT_HOME || path.join(os.homedir(), '.delimit');
|
|
264
|
+
const repoRoot = resolveRepoRoot(options.cwd || process.cwd());
|
|
265
|
+
const repoBase = detectRepoBase(repoRoot);
|
|
266
|
+
const repoName = repoRoot ? path.basename(repoRoot) : 'no-repo';
|
|
267
|
+
const osUser = os.userInfo().username;
|
|
268
|
+
const githubUser = readGitHubIdentity(os.homedir());
|
|
269
|
+
const actor = sanitizeSegment(githubUser || osUser, 'unknown-user');
|
|
270
|
+
const requestedScope = sanitizeSegment(options.scope || process.env.DELIMIT_SCOPE || '', '');
|
|
271
|
+
const scope = requestedScope === 'all' ? 'all' : '';
|
|
272
|
+
const venture = scope === 'all'
|
|
273
|
+
? 'portfolio'
|
|
274
|
+
: sanitizeSegment(options.venture || process.env.DELIMIT_VENTURE || repoName || 'global', 'global');
|
|
275
|
+
const continuityRoot = path.join(homeDir, 'continuity', actor, venture);
|
|
276
|
+
const repoGovernanceRoot = repoRoot ? path.join(repoRoot, '.delimit') : null;
|
|
277
|
+
const ledgerInfo = scope === 'all'
|
|
278
|
+
? { ledgerRoot: path.join(homeDir, 'ledger'), ledgerScope: 'all' }
|
|
279
|
+
: resolveLedgerRoot(repoGovernanceRoot, homeDir);
|
|
280
|
+
const ventureLedgers = listVentureLedgers(repoBase, homeDir);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
actor,
|
|
284
|
+
osUser,
|
|
285
|
+
githubUser,
|
|
286
|
+
venture,
|
|
287
|
+
repoRoot,
|
|
288
|
+
repoName,
|
|
289
|
+
delimitHome: homeDir,
|
|
290
|
+
continuityRoot,
|
|
291
|
+
privateStateRoot: homeDir,
|
|
292
|
+
repoGovernanceRoot,
|
|
293
|
+
ledgerRoot: ledgerInfo.ledgerRoot,
|
|
294
|
+
ledgerScope: ledgerInfo.ledgerScope,
|
|
295
|
+
repoBase,
|
|
296
|
+
ventureLedgers,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function formatContinuityReport(context) {
|
|
301
|
+
return [
|
|
302
|
+
'Delimit continuity context:',
|
|
303
|
+
` actor: ${context.actor}`,
|
|
304
|
+
` osUser: ${context.osUser}`,
|
|
305
|
+
` githubUser: ${context.githubUser || 'unresolved'}`,
|
|
306
|
+
` venture: ${context.venture}`,
|
|
307
|
+
` repoRoot: ${context.repoRoot || 'none'}`,
|
|
308
|
+
` privateStateRoot: ${context.privateStateRoot}`,
|
|
309
|
+
` continuityRoot: ${context.continuityRoot}`,
|
|
310
|
+
` repoGovernanceRoot: ${context.repoGovernanceRoot || 'none'}`,
|
|
311
|
+
` repoBase: ${context.repoBase}`,
|
|
312
|
+
` ledgerRoot: ${context.ledgerRoot}`,
|
|
313
|
+
` ledgerScope: ${context.ledgerScope}`,
|
|
314
|
+
` ventures: ${context.ventureLedgers?.length || 0}`,
|
|
315
|
+
].join('\n');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = {
|
|
319
|
+
resolveContinuityContext,
|
|
320
|
+
formatContinuityReport,
|
|
321
|
+
listVentureLedgers,
|
|
322
|
+
resolveRepoRoot,
|
|
323
|
+
loadActiveVenture,
|
|
324
|
+
saveActiveVenture,
|
|
325
|
+
};
|
package/lib/cross-model-hooks.js
CHANGED
|
@@ -24,6 +24,110 @@ const { getDelimitSection, getDelimitSectionCondensed } = require('./delimit-tem
|
|
|
24
24
|
function getHome() { return process.env.HOME || os.homedir(); }
|
|
25
25
|
function getDelimitHome() { return path.join(getHome(), '.delimit'); }
|
|
26
26
|
|
|
27
|
+
function readJsonl(filePath) {
|
|
28
|
+
if (!fs.existsSync(filePath)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return fs.readFileSync(filePath, 'utf-8')
|
|
32
|
+
.split('\n')
|
|
33
|
+
.map(line => line.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.map(line => {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(line);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readLatestSessionSummary(sessionDir) {
|
|
46
|
+
if (!fs.existsSync(sessionDir)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const files = fs.readdirSync(sessionDir)
|
|
50
|
+
.filter(name => name.endsWith('.json'))
|
|
51
|
+
.map(name => path.join(sessionDir, name))
|
|
52
|
+
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
53
|
+
if (files.length === 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const data = JSON.parse(fs.readFileSync(files[0], 'utf-8'));
|
|
58
|
+
return {
|
|
59
|
+
id: data.id || path.basename(files[0], '.json'),
|
|
60
|
+
timestamp: data.timestamp || null,
|
|
61
|
+
summary: data.summary || '',
|
|
62
|
+
blockers: Array.isArray(data.blockers) ? data.blockers : [],
|
|
63
|
+
itemsCompleted: Array.isArray(data.items_completed) ? data.items_completed : [],
|
|
64
|
+
};
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildLedgerState(ledgerDir) {
|
|
71
|
+
const opsPath = path.join(ledgerDir, 'operations.jsonl');
|
|
72
|
+
const entries = readJsonl(opsPath);
|
|
73
|
+
const latestById = new Map();
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (!entry || !entry.id) continue;
|
|
76
|
+
const current = latestById.get(entry.id);
|
|
77
|
+
const nextTime = entry.updated_at || entry.created_at || '';
|
|
78
|
+
const currentTime = current ? (current.updated_at || current.created_at || '') : '';
|
|
79
|
+
if (!current || nextTime >= currentTime) {
|
|
80
|
+
latestById.set(entry.id, entry);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const items = Array.from(latestById.values());
|
|
84
|
+
const open = items
|
|
85
|
+
.filter(item => !['done', 'blocked'].includes(String(item.status || 'open')))
|
|
86
|
+
.sort((a, b) => {
|
|
87
|
+
const prio = { P0: 0, P1: 1, P2: 2 };
|
|
88
|
+
return (prio[a.priority] ?? 9) - (prio[b.priority] ?? 9);
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
items,
|
|
92
|
+
open,
|
|
93
|
+
next: open[0] || null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getServiceState(serviceName) {
|
|
98
|
+
try {
|
|
99
|
+
const active = execSync(`systemctl is-active ${serviceName} 2>/dev/null`, { encoding: 'utf-8', timeout: 2000 }).trim();
|
|
100
|
+
const enabled = execSync(`systemctl is-enabled ${serviceName} 2>/dev/null`, { encoding: 'utf-8', timeout: 2000 }).trim();
|
|
101
|
+
return { active, enabled };
|
|
102
|
+
} catch {
|
|
103
|
+
const processPattern = serviceName.includes('social')
|
|
104
|
+
? 'social_daemon.py'
|
|
105
|
+
: serviceName.includes('inbox')
|
|
106
|
+
? 'inbox_daemon.py'
|
|
107
|
+
: '';
|
|
108
|
+
if (processPattern) {
|
|
109
|
+
try {
|
|
110
|
+
const matches = execSync(`ps -eo pid,cmd | grep ${JSON.stringify(processPattern)} | grep -v grep`, {
|
|
111
|
+
encoding: 'utf-8',
|
|
112
|
+
timeout: 2000,
|
|
113
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
114
|
+
}).trim();
|
|
115
|
+
if (matches) {
|
|
116
|
+
return { active: 'active', enabled: 'unknown' };
|
|
117
|
+
}
|
|
118
|
+
} catch { /* ignore */ }
|
|
119
|
+
}
|
|
120
|
+
return { active: 'inactive', enabled: 'unknown' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeBootstrapState(continuityRoot, payload) {
|
|
125
|
+
fs.mkdirSync(continuityRoot, { recursive: true });
|
|
126
|
+
const statePath = path.join(continuityRoot, 'bootstrap-state.json');
|
|
127
|
+
fs.writeFileSync(statePath, JSON.stringify(payload, null, 2) + '\n');
|
|
128
|
+
return statePath;
|
|
129
|
+
}
|
|
130
|
+
|
|
27
131
|
// ---------------------------------------------------------------------------
|
|
28
132
|
// Hook configuration (user-overridable via delimit.yml)
|
|
29
133
|
// ---------------------------------------------------------------------------
|
|
@@ -327,8 +431,8 @@ function installClaudeHooks(tool, hookConfig) {
|
|
|
327
431
|
|
|
328
432
|
// 3. PreToolUse: security audit before deploy/publish/release commands
|
|
329
433
|
if (hookConfig.deploy_audit !== false) {
|
|
330
|
-
const
|
|
331
|
-
const existingSecurity = findClaudeHookGroup(config.hooks.PreToolUse,
|
|
434
|
+
const deployGateCmd = 'delimit-cli hook deploy-gate';
|
|
435
|
+
const existingSecurity = findClaudeHookGroup(config.hooks.PreToolUse, deployGateCmd);
|
|
332
436
|
if (!existingSecurity) {
|
|
333
437
|
config.hooks.PreToolUse.push({
|
|
334
438
|
matcher: 'Bash',
|
|
@@ -799,6 +903,113 @@ async function hookSessionStart() {
|
|
|
799
903
|
process.stdout.write(lines.join('\n') + '\n');
|
|
800
904
|
}
|
|
801
905
|
|
|
906
|
+
/**
|
|
907
|
+
* bootstrap: shared natural-language trigger handler.
|
|
908
|
+
* execute -> resume or launch governed work loop
|
|
909
|
+
* inspect -> show ledger/daemon/continuity state without executing
|
|
910
|
+
*/
|
|
911
|
+
async function hookBootstrap(mode = 'inspect', options = {}) {
|
|
912
|
+
const cwd = options.cwd || process.cwd();
|
|
913
|
+
const lines = [];
|
|
914
|
+
const silent = Boolean(options.silent);
|
|
915
|
+
const normalizedMode = mode === 'execute' ? 'execute' : 'inspect';
|
|
916
|
+
const { resolveContinuityContext } = require('./continuity-resolver');
|
|
917
|
+
const context = resolveContinuityContext({ cwd, scope: options.scope });
|
|
918
|
+
const hasPolicy = fs.existsSync(path.join(cwd, 'delimit.yml'))
|
|
919
|
+
|| fs.existsSync(path.join(cwd, '.delimit.yml'))
|
|
920
|
+
|| fs.existsSync(path.join(cwd, '.delimit', 'policies.yml'));
|
|
921
|
+
const globalLedgerDir = context.ledgerRoot;
|
|
922
|
+
const sessionDir = path.join(getDelimitHome(), 'sessions');
|
|
923
|
+
const ledgerState = buildLedgerState(globalLedgerDir);
|
|
924
|
+
const latestSession = readLatestSessionSummary(sessionDir);
|
|
925
|
+
const inboxDaemon = getServiceState('delimit-inbox.service');
|
|
926
|
+
const socialDaemon = getServiceState('delimit-social-scan.service');
|
|
927
|
+
|
|
928
|
+
lines.push('[Delimit] Bootstrap');
|
|
929
|
+
lines.push(`[Delimit] Mode: ${normalizedMode}`);
|
|
930
|
+
lines.push(`[Delimit] Repo: ${cwd}`);
|
|
931
|
+
lines.push(`[Delimit] Actor: ${context.actor}`);
|
|
932
|
+
lines.push(`[Delimit] Venture: ${context.venture}`);
|
|
933
|
+
lines.push(`[Delimit] Continuity root: ${context.continuityRoot}`);
|
|
934
|
+
lines.push(`[Delimit] Ledger scope: ${context.ledgerScope}`);
|
|
935
|
+
lines.push(hasPolicy ? '[Delimit] Governance: active' : '[Delimit] Governance: repo policy missing');
|
|
936
|
+
lines.push(fs.existsSync(globalLedgerDir) ? '[Delimit] Ledger: available' : '[Delimit] Ledger: unavailable');
|
|
937
|
+
lines.push(fs.existsSync(sessionDir) ? '[Delimit] Continuity: session history available' : '[Delimit] Continuity: no saved sessions');
|
|
938
|
+
lines.push(`[Delimit] Inbox daemon: ${inboxDaemon.active}/${inboxDaemon.enabled}`);
|
|
939
|
+
lines.push(`[Delimit] Social daemon: ${socialDaemon.active}/${socialDaemon.enabled}`);
|
|
940
|
+
|
|
941
|
+
if (latestSession) {
|
|
942
|
+
lines.push(`[Delimit] Latest session: ${latestSession.id}`);
|
|
943
|
+
if (latestSession.summary) {
|
|
944
|
+
lines.push(`[Delimit] Latest summary: ${latestSession.summary}`);
|
|
945
|
+
}
|
|
946
|
+
if (latestSession.blockers.length > 0) {
|
|
947
|
+
lines.push(`[Delimit] Blockers: ${latestSession.blockers.join('; ')}`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (ledgerState.next) {
|
|
952
|
+
const next = ledgerState.next;
|
|
953
|
+
lines.push(`[Delimit] Next open item: ${next.id} ${next.title || '(untitled)'} [${next.priority || 'P?'}]`);
|
|
954
|
+
} else {
|
|
955
|
+
lines.push('[Delimit] Next open item: none');
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (normalizedMode === 'execute') {
|
|
959
|
+
const bootstrapState = {
|
|
960
|
+
timestamp: new Date().toISOString(),
|
|
961
|
+
actor: context.actor,
|
|
962
|
+
venture: context.venture,
|
|
963
|
+
repo: cwd,
|
|
964
|
+
mode: normalizedMode,
|
|
965
|
+
nextItem: ledgerState.next ? {
|
|
966
|
+
id: ledgerState.next.id,
|
|
967
|
+
title: ledgerState.next.title || '',
|
|
968
|
+
priority: ledgerState.next.priority || '',
|
|
969
|
+
status: ledgerState.next.status || 'open',
|
|
970
|
+
} : null,
|
|
971
|
+
daemons: {
|
|
972
|
+
inbox: inboxDaemon,
|
|
973
|
+
social: socialDaemon,
|
|
974
|
+
},
|
|
975
|
+
latestSession,
|
|
976
|
+
openItemCount: ledgerState.open.length,
|
|
977
|
+
};
|
|
978
|
+
const statePath = writeBootstrapState(context.continuityRoot, bootstrapState);
|
|
979
|
+
lines.push('[Delimit] Intent: resume or launch governed persistent loop');
|
|
980
|
+
lines.push(`[Delimit] Work order saved: ${statePath}`);
|
|
981
|
+
lines.push('[Delimit] Next tools: delimit session --build');
|
|
982
|
+
} else {
|
|
983
|
+
lines.push('[Delimit] Intent: inspect current state without executing');
|
|
984
|
+
lines.push('[Delimit] Next tools: delimit session --inspect');
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const payload = {
|
|
988
|
+
mode: normalizedMode,
|
|
989
|
+
repo: cwd,
|
|
990
|
+
actor: context.actor,
|
|
991
|
+
venture: context.venture,
|
|
992
|
+
continuityRoot: context.continuityRoot,
|
|
993
|
+
ledgerRoot: context.ledgerRoot,
|
|
994
|
+
ledgerScope: context.ledgerScope,
|
|
995
|
+
hasPolicy,
|
|
996
|
+
ledgerAvailable: fs.existsSync(globalLedgerDir),
|
|
997
|
+
continuityAvailable: fs.existsSync(sessionDir),
|
|
998
|
+
daemons: {
|
|
999
|
+
inbox: inboxDaemon,
|
|
1000
|
+
social: socialDaemon,
|
|
1001
|
+
},
|
|
1002
|
+
latestSession,
|
|
1003
|
+
nextItem: ledgerState.next || null,
|
|
1004
|
+
openItemCount: ledgerState.open.length,
|
|
1005
|
+
};
|
|
1006
|
+
if (!silent) {
|
|
1007
|
+
lines.push('');
|
|
1008
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
1009
|
+
}
|
|
1010
|
+
return payload;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
802
1013
|
/**
|
|
803
1014
|
* pre-tool: Check before file edits.
|
|
804
1015
|
* If editing an OpenAPI spec, run a quick lint.
|
|
@@ -1060,6 +1271,7 @@ module.exports = {
|
|
|
1060
1271
|
removeGeminiHooks,
|
|
1061
1272
|
loadHookConfig,
|
|
1062
1273
|
hookSessionStart,
|
|
1274
|
+
hookBootstrap,
|
|
1063
1275
|
hookPreTool,
|
|
1064
1276
|
hookPreCommit,
|
|
1065
1277
|
hookDeployGate,
|
package/lib/delimit-template.js
CHANGED
|
@@ -31,6 +31,11 @@ The founder reviews and approves via email. The AI's job is to draft, send, and
|
|
|
31
31
|
- **Session end or idle**: call \`delimit_session_handoff\` to preserve context for next session
|
|
32
32
|
- If returning user, summarize what's pending. If new project, call \`delimit_scan\`.
|
|
33
33
|
|
|
34
|
+
### Natural Language Triggers
|
|
35
|
+
- \`think and build\`, \`keep building\`, \`resume building\`, \`run the swarm\`: route through native Delimit session/build behavior, equivalent to \`delimit session --build\`
|
|
36
|
+
- \`ask delimit\`, \`what's next\`, \`check the ledger\`: route through native Delimit inspect behavior, equivalent to \`delimit session --inspect\` or \`delimit ask\`
|
|
37
|
+
- Bootstrap behavior must be identical across Codex, Claude Code, Gemini, Grok, and Cursor: resolve user + venture + repo, load continuity/session state, check ledger, check daemon/swarm status, then either inspect or resume/launch the governed loop
|
|
38
|
+
|
|
34
39
|
### Code Development (debounced per edit batch)
|
|
35
40
|
- After editing UI/CSS: call \`delimit_design_validate_responsive\`
|
|
36
41
|
- After editing API specs: call \`delimit_lint\` + \`delimit_drift_check\`
|