@ghl-ai/aw 0.1.93 → 0.2.1
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/aw-usage-wiring.mjs +387 -0
- package/c4/detect.mjs +4 -2
- package/codex.mjs +74 -19
- package/commands/c4.mjs +8 -6
- package/commands/init.mjs +89 -28
- package/ecc.mjs +3 -3
- package/hook-cleanup.mjs +4 -3
- package/hooks/aw-usage/hooks/aw-usage-commit-created.js +21 -0
- package/hooks/aw-usage/hooks/aw-usage-commit-trailer.js +124 -0
- package/hooks/aw-usage/hooks/aw-usage-post-tool-use.js +159 -33
- package/hooks/aw-usage/hooks/aw-usage-prompt-submit.js +39 -74
- package/hooks/aw-usage/hooks/aw-usage-telemetry-send.js +67 -7
- package/hooks/aw-usage/lib/aw-usage-commands.js +123 -0
- package/hooks/aw-usage/lib/aw-usage-envelope.js +121 -0
- package/hooks/aw-usage/lib/aw-usage-queue.js +168 -0
- package/hooks/aw-usage/lib/aw-usage-skill-source.js +229 -0
- package/hooks/aw-usage/lib/aw-usage-stage-events.js +179 -0
- package/hooks/aw-usage/lib/aw-usage-telemetry.js +260 -14
- package/hooks/aw-usage/package.json +1 -1
- package/hooks/codex-home.mjs +8 -6
- package/hooks.mjs +21 -34
- package/install-aw-usage-hooks.mjs +65 -215
- package/package.json +2 -1
- package/startup.mjs +54 -155
|
@@ -16,6 +16,8 @@ const fs = require('fs');
|
|
|
16
16
|
const os = require('os');
|
|
17
17
|
const { spawn, execSync } = require('child_process');
|
|
18
18
|
const crypto = require('crypto');
|
|
19
|
+
const { newEventId, readHookVersion, nextSeq, withFileLock } = require('./aw-usage-envelope');
|
|
20
|
+
const { commandKey, BARE_SLASH_COMMANDS } = require('./aw-usage-commands');
|
|
19
21
|
|
|
20
22
|
const SENDER_SCRIPT = path.join(__dirname, '..', 'hooks', 'aw-usage-telemetry-send.js');
|
|
21
23
|
const AW_HOME = path.join(os.homedir(), '.aw');
|
|
@@ -169,24 +171,31 @@ function resolveProjectHash(context) {
|
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
// ── Session file cleanup ─────────────────────────────────────────────
|
|
172
|
-
// Prune session files
|
|
173
|
-
// Called once per session start — best-effort, never blocks.
|
|
174
|
+
// Prune session files idle longer than SESSION_FILE_RETENTION_MS to prevent
|
|
175
|
+
// unbounded growth. Called once per session start — best-effort, never blocks.
|
|
174
176
|
|
|
175
|
-
const SESSION_MAX_AGE_MS = 72 * 60 * 60 * 1000; // 72 hours
|
|
177
|
+
const SESSION_MAX_AGE_MS = 72 * 60 * 60 * 1000; // 72 hours: v1 commit link only
|
|
178
|
+
// Long enough that a session's aw_skills survive until its last commit.
|
|
179
|
+
const SESSION_FILE_RETENTION_MS = 5 * 24 * 60 * 60 * 1000;
|
|
176
180
|
|
|
177
181
|
function pruneStaleSessionFiles() {
|
|
182
|
+
let entries;
|
|
178
183
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
entries = fs.readdirSync(SESSION_DIR);
|
|
185
|
+
} catch {
|
|
186
|
+
return; // No sessions folder yet.
|
|
187
|
+
}
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
// .tmp: left by a crash between an atomic write and its rename.
|
|
191
|
+
if (!entry.endsWith('.json') && !entry.endsWith('.seq') && !entry.endsWith('.tmp')) continue;
|
|
192
|
+
try {
|
|
183
193
|
const filePath = path.join(SESSION_DIR, entry);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
194
|
+
if (now - fs.statSync(filePath).mtimeMs > SESSION_FILE_RETENTION_MS) fs.unlinkSync(filePath);
|
|
195
|
+
} catch {
|
|
196
|
+
// Gone meanwhile, or not a plain file: skip it and keep pruning the rest.
|
|
188
197
|
}
|
|
189
|
-
}
|
|
198
|
+
}
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
// ── Session model persistence ────────────────────────────────────────
|
|
@@ -211,6 +220,8 @@ function readSessionModel(sessionId) {
|
|
|
211
220
|
|
|
212
221
|
function readSessionState(sessionId) {
|
|
213
222
|
if (!sessionId) return {};
|
|
223
|
+
// Keep the session's AW-skills file alive with the session, main file or not.
|
|
224
|
+
try { const now = new Date(); fs.utimesSync(awSkillsPath(sessionId), now, now); } catch { /* none recorded yet */ }
|
|
214
225
|
try {
|
|
215
226
|
const filePath = path.join(SESSION_DIR, sessionId + '.json');
|
|
216
227
|
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -289,6 +300,156 @@ function persistSessionSlashCommand(sessionId, slashCommand, context = {}) {
|
|
|
289
300
|
} catch { /* ignore */ }
|
|
290
301
|
}
|
|
291
302
|
|
|
303
|
+
// last_prompt and its `matched` list are written under the session lock and
|
|
304
|
+
// replaced atomically (tmp file + rename), so a tool hook never reads half a
|
|
305
|
+
// file or overwrites a newer prompt.
|
|
306
|
+
const SESSION_LOCK_WAIT_MS = 200;
|
|
307
|
+
|
|
308
|
+
function sessionLockPath(sessionId) {
|
|
309
|
+
return path.join(SESSION_DIR, sessionId + '.json.lock');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function writeSessionStateAtomic(sessionId, state) {
|
|
313
|
+
const filePath = path.join(SESSION_DIR, sessionId + '.json');
|
|
314
|
+
const tmpPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
315
|
+
fs.writeFileSync(tmpPath, JSON.stringify(state));
|
|
316
|
+
fs.renameSync(tmpPath, filePath);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// last_prompt: the commands typed in the most recent prompt (empty for a
|
|
320
|
+
// plain prompt). `matched` lists command keys already paired with a skill
|
|
321
|
+
// load, so only the first matching load gets follows_typed_command.
|
|
322
|
+
function persistSessionPrompt(sessionId, { turn_id: turnId = null, commands = [] } = {}) {
|
|
323
|
+
if (!sessionId || sessionId === 'unknown') return;
|
|
324
|
+
const write = () => {
|
|
325
|
+
const state = readSessionState(sessionId);
|
|
326
|
+
writeSessionStateAtomic(sessionId, {
|
|
327
|
+
...state,
|
|
328
|
+
last_prompt: {
|
|
329
|
+
turn_id: turnId || null,
|
|
330
|
+
commands: commands.map(({ raw_name, command_form, namespace }) => ({ raw_name, command_form, namespace })),
|
|
331
|
+
matched: [],
|
|
332
|
+
at: new Date().toISOString(),
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
try {
|
|
337
|
+
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
338
|
+
// A new prompt must land even when a tool hook holds the lock too long.
|
|
339
|
+
if (!withFileLock(sessionLockPath(sessionId), write, { waitMs: SESSION_LOCK_WAIT_MS }).ok) write();
|
|
340
|
+
} catch { /* ignore: best effort, like the other session records */ }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const TYPED_COMMAND_WINDOW_MS = 10 * 60 * 1000;
|
|
344
|
+
|
|
345
|
+
// aw_skills: the AW skills and stages this session ran, as the harness reported
|
|
346
|
+
// them. The prepare-commit-msg hook writes them as the AW-Skills trailer; the
|
|
347
|
+
// server decides which count as SDLC. Only where a skill came from is checked
|
|
348
|
+
// here, so non-AW skill names never reach a commit.
|
|
349
|
+
const AW_SKILLS_MAX = 20;
|
|
350
|
+
const SAFE_AW_SKILL = /^[a-z0-9:_/$.-]{1,60}$/;
|
|
351
|
+
// The AW router in any form: using-aw-skills, aw:/platform-core:using-aw-skills,
|
|
352
|
+
// aw-using-aw-skills, platform-core-using-aw-skills (registry link folders).
|
|
353
|
+
const ROUTER_SKILL = /(?:^|[:/-])using-aw-skills$/;
|
|
354
|
+
|
|
355
|
+
// Typed forms other than slash_ns only match `aw-` names (aw-usage-commands.js);
|
|
356
|
+
// slash_ns also carries other plugins' namespaces (`/caveman`, `/codex:rescue`).
|
|
357
|
+
function isAwTypedCommand(name, commandForm) {
|
|
358
|
+
return commandForm !== 'slash_ns' || name.startsWith('aw:') || BARE_SLASH_COMMANDS.has(name);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** The aw_skills entry for an event, or null when it is not AW evidence. */
|
|
362
|
+
function awSkillEntry(eventType, payload) {
|
|
363
|
+
let entry = null;
|
|
364
|
+
if (eventType === 'sdlc_stage_started' || eventType === 'sdlc_stage_completed') {
|
|
365
|
+
entry = typeof payload?.stage === 'string' ? `stage:${payload.stage.toLowerCase()}` : null;
|
|
366
|
+
} else if (eventType === 'skill_invoked' && typeof payload?.skill_name === 'string') {
|
|
367
|
+
const name = payload.skill_name.toLowerCase();
|
|
368
|
+
const isAw = payload.source === 'user_prompt' ? isAwTypedCommand(name, payload.command_form) : payload.skill_origin === 'aw';
|
|
369
|
+
entry = isAw ? name : null;
|
|
370
|
+
}
|
|
371
|
+
if (!entry || !SAFE_AW_SKILL.test(entry)) return null;
|
|
372
|
+
return ROUTER_SKILL.test(entry) ? null : entry;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Own file per session: the older session writers (model, last skill) rewrite
|
|
376
|
+
// <session>.json without the lock, so they could wipe or half-write aw_skills.
|
|
377
|
+
function awSkillsPath(sessionId) {
|
|
378
|
+
return path.join(SESSION_DIR, sessionId + '.aw.json');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function readAwSkillsState(sessionId) {
|
|
382
|
+
try {
|
|
383
|
+
const state = JSON.parse(fs.readFileSync(awSkillsPath(sessionId), 'utf8'));
|
|
384
|
+
return state && typeof state === 'object' ? state : {};
|
|
385
|
+
} catch {
|
|
386
|
+
// Missing or unreadable: no AW skills recorded for this session.
|
|
387
|
+
return {};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function recordSessionAwSkill(sessionId, entry, { now = Date.now() } = {}) {
|
|
392
|
+
if (!sessionId || sessionId === 'unknown') return;
|
|
393
|
+
const write = () => {
|
|
394
|
+
const state = readAwSkillsState(sessionId);
|
|
395
|
+
const skills = Array.isArray(state.aw_skills) ? state.aw_skills : [];
|
|
396
|
+
const filePath = awSkillsPath(sessionId);
|
|
397
|
+
const tmpPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
398
|
+
fs.writeFileSync(tmpPath, JSON.stringify({
|
|
399
|
+
aw_skills: skills.includes(entry) || skills.length >= AW_SKILLS_MAX ? skills : [...skills, entry],
|
|
400
|
+
aw_used_at: new Date(now).toISOString(),
|
|
401
|
+
}));
|
|
402
|
+
fs.renameSync(tmpPath, filePath);
|
|
403
|
+
};
|
|
404
|
+
try {
|
|
405
|
+
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
406
|
+
if (!withFileLock(sessionLockPath(sessionId), write, { waitMs: SESSION_LOCK_WAIT_MS }).ok) write();
|
|
407
|
+
} catch {
|
|
408
|
+
// Not writable: commits from this session carry no AW trailer.
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** The AW skills the session ran, safe for a trailer; [] when unknown. */
|
|
413
|
+
function readSessionAwSkills(sessionId) {
|
|
414
|
+
if (!sessionId) return [];
|
|
415
|
+
const skills = readAwSkillsState(sessionId).aw_skills;
|
|
416
|
+
return Array.isArray(skills) ? skills.filter(s => typeof s === 'string' && SAFE_AW_SKILL.test(s)) : [];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function isCurrentPrompt(prompt, turnId, now) {
|
|
420
|
+
if (prompt.turn_id && turnId) return prompt.turn_id === turnId;
|
|
421
|
+
return now - parseUpdatedAt(prompt.at) <= TYPED_COMMAND_WINDOW_MS;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// B1 follows_typed_command: true once per typed command, for the first skill
|
|
425
|
+
// load whose key matches it in the same prompt (same turn, or within 10
|
|
426
|
+
// minutes when the IDE gives no turn id). The pairing is recorded in
|
|
427
|
+
// last_prompt.matched under a lock so two tool hooks cannot both claim it.
|
|
428
|
+
function claimTypedCommandMatch(sessionId, turnId, skillName, { now = Date.now() } = {}) {
|
|
429
|
+
const key = commandKey(skillName);
|
|
430
|
+
if (!sessionId || !key) return false;
|
|
431
|
+
try {
|
|
432
|
+
const result = withFileLock(sessionLockPath(sessionId), () => {
|
|
433
|
+
const state = readSessionState(sessionId);
|
|
434
|
+
const prompt = state.last_prompt;
|
|
435
|
+
if (!prompt || !Array.isArray(prompt.commands) || !isCurrentPrompt(prompt, turnId, now)) return false;
|
|
436
|
+
const matched = Array.isArray(prompt.matched) ? prompt.matched : [];
|
|
437
|
+
if (matched.includes(key) || !prompt.commands.some(command => commandKey(command.raw_name) === key)) return false;
|
|
438
|
+
writeSessionStateAtomic(sessionId, { ...state, last_prompt: { ...prompt, matched: [...matched, key] } });
|
|
439
|
+
return true;
|
|
440
|
+
}, { waitMs: SESSION_LOCK_WAIT_MS });
|
|
441
|
+
return result.ok && result.value === true;
|
|
442
|
+
} catch {
|
|
443
|
+
// Session file not writable: the field is omitted for this event.
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function readSessionLastPrompt(sessionId) {
|
|
449
|
+
const prompt = readSessionState(sessionId)?.last_prompt;
|
|
450
|
+
return prompt && Array.isArray(prompt.commands) ? prompt : null;
|
|
451
|
+
}
|
|
452
|
+
|
|
292
453
|
function readSessionLastSlashCommand(sessionId) {
|
|
293
454
|
const cmd = readSessionState(sessionId)?.last_slash_command;
|
|
294
455
|
if (!cmd?.command_name) return null;
|
|
@@ -348,6 +509,59 @@ function findRecentSdlcSessionForProject(cwd, maxAgeMs = SESSION_MAX_AGE_MS) {
|
|
|
348
509
|
return best;
|
|
349
510
|
}
|
|
350
511
|
|
|
512
|
+
// ── Session activity index (B9) ──────────────────────────────────────
|
|
513
|
+
// sessions/by-project/<project_hash>.activity.json lists the IDE sessions
|
|
514
|
+
// active in a project in the last 2 hours. Every event except commit_created
|
|
515
|
+
// touches it; the commit hook reads it to name the IDE a commit came from.
|
|
516
|
+
// The v1 by-project/<hash>.json SDLC index is separate and unchanged.
|
|
517
|
+
|
|
518
|
+
const ACTIVITY_WINDOW_MS = 2 * 60 * 60 * 1000;
|
|
519
|
+
|
|
520
|
+
function activityIndexPath(projectHash) {
|
|
521
|
+
return path.join(SESSION_PROJECT_DIR, `${projectHash}.activity.json`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function readActivityIndex(projectHash) {
|
|
525
|
+
try {
|
|
526
|
+
const sessions = JSON.parse(fs.readFileSync(activityIndexPath(projectHash), 'utf8'))?.sessions;
|
|
527
|
+
return sessions && typeof sessions === 'object' && !Array.isArray(sessions) ? sessions : {};
|
|
528
|
+
} catch {
|
|
529
|
+
// No index yet, or unreadable: no active sessions known.
|
|
530
|
+
return {};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function recentEntries(sessions, now) {
|
|
535
|
+
return Object.entries(sessions).filter(([, entry]) => now - parseUpdatedAt(entry?.last_active_at) <= ACTIVITY_WINDOW_MS);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function touchSessionActivity(projectHash, sessionId, harness, { now = Date.now() } = {}) {
|
|
539
|
+
if (!projectHash || !sessionId || sessionId === 'unknown') return;
|
|
540
|
+
const filePath = activityIndexPath(projectHash);
|
|
541
|
+
try {
|
|
542
|
+
fs.mkdirSync(SESSION_PROJECT_DIR, { recursive: true });
|
|
543
|
+
withFileLock(`${filePath}.lock`, () => {
|
|
544
|
+
const sessions = Object.fromEntries(recentEntries(readActivityIndex(projectHash), now));
|
|
545
|
+
sessions[sessionId] = { harness, last_active_at: new Date(now).toISOString() };
|
|
546
|
+
const tmpPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
547
|
+
fs.writeFileSync(tmpPath, JSON.stringify({ sessions }));
|
|
548
|
+
fs.renameSync(tmpPath, filePath);
|
|
549
|
+
});
|
|
550
|
+
} catch {
|
|
551
|
+
// Index not writable: commits from this project carry no origin fields.
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Most recent session active in the project in the last 2 hours, or null. */
|
|
556
|
+
function findRecentActiveSessionForProject(projectHash, { now = Date.now() } = {}) {
|
|
557
|
+
if (!projectHash) return null;
|
|
558
|
+
const recent = recentEntries(readActivityIndex(projectHash), now)
|
|
559
|
+
.sort(([, a], [, b]) => parseUpdatedAt(b.last_active_at) - parseUpdatedAt(a.last_active_at));
|
|
560
|
+
if (recent.length === 0) return null;
|
|
561
|
+
const [sessionId, entry] = recent[0];
|
|
562
|
+
return { session_id: sessionId, harness: typeof entry.harness === 'string' ? entry.harness : 'unknown', ambiguous: recent.length > 1 };
|
|
563
|
+
}
|
|
564
|
+
|
|
351
565
|
// ── Short-TTL dedupe guards ──────────────────────────────────────────
|
|
352
566
|
|
|
353
567
|
function normalizeDedupePart(value) {
|
|
@@ -512,6 +726,14 @@ function readLastAssistantFromTranscript(transcriptPath) {
|
|
|
512
726
|
|
|
513
727
|
// ── buildEvent ───────────────────────────────────────────────────────
|
|
514
728
|
|
|
729
|
+
const SCHEMA_VERSION = 2;
|
|
730
|
+
let _hookVersion;
|
|
731
|
+
|
|
732
|
+
function getHookVersion() {
|
|
733
|
+
if (_hookVersion === undefined) _hookVersion = readHookVersion();
|
|
734
|
+
return _hookVersion;
|
|
735
|
+
}
|
|
736
|
+
|
|
515
737
|
function buildEvent(hookInput, eventType, payload) {
|
|
516
738
|
const input = hookInput || {};
|
|
517
739
|
const cfg = loadConfig();
|
|
@@ -540,6 +762,19 @@ function buildEvent(hookInput, eventType, payload) {
|
|
|
540
762
|
|| (input.workspace_roots && input.workspace_roots[0])
|
|
541
763
|
|| null;
|
|
542
764
|
|
|
765
|
+
// A8: the email the IDE itself is signed in with, when the IDE provides one.
|
|
766
|
+
// Cursor passes it in the hook input and to hook scripts; Claude Code and Codex do not.
|
|
767
|
+
const rawAccountEmail = input.user_email || process.env.CURSOR_USER_EMAIL;
|
|
768
|
+
const accountEmail = typeof rawAccountEmail === 'string' && rawAccountEmail ? rawAccountEmail.slice(0, 200) : null;
|
|
769
|
+
|
|
770
|
+
// v2 envelope (spec 4.2). seq is omitted when it cannot be taken safely.
|
|
771
|
+
const hookVersion = getHookVersion();
|
|
772
|
+
const seq = nextSeq(sessionId);
|
|
773
|
+
const projectHash = computeProjectHash(cwd);
|
|
774
|
+
if (eventType !== 'commit_created') touchSessionActivity(projectHash, sessionId, harness);
|
|
775
|
+
const awSkill = awSkillEntry(eventType, payload);
|
|
776
|
+
if (awSkill) recordSessionAwSkill(sessionId, awSkill);
|
|
777
|
+
|
|
543
778
|
return {
|
|
544
779
|
session_id: sessionId,
|
|
545
780
|
generation_id: generationId,
|
|
@@ -548,13 +783,17 @@ function buildEvent(hookInput, eventType, payload) {
|
|
|
548
783
|
machine_id: cfg.machine_id || 'unknown',
|
|
549
784
|
github_user: git.user || input.user_email || null,
|
|
550
785
|
github_email: git.email || input.user_email || null,
|
|
551
|
-
project_hash:
|
|
786
|
+
project_hash: projectHash,
|
|
552
787
|
aw_version: awCli.version,
|
|
553
788
|
aw_cli_version: awCli.version,
|
|
554
789
|
aw_cli_version_source: awCli.source,
|
|
555
790
|
event: eventType,
|
|
556
791
|
client_ts: new Date().toISOString(),
|
|
557
|
-
|
|
792
|
+
...(accountEmail ? { account_email: accountEmail } : {}),
|
|
793
|
+
event_id: newEventId(),
|
|
794
|
+
schema_version: SCHEMA_VERSION,
|
|
795
|
+
...(hookVersion ? { hook_version: hookVersion } : {}),
|
|
796
|
+
payload: { ...(payload || {}), ...(seq ? { seq } : {}) },
|
|
558
797
|
};
|
|
559
798
|
}
|
|
560
799
|
|
|
@@ -587,8 +826,15 @@ module.exports = {
|
|
|
587
826
|
persistSessionSkill,
|
|
588
827
|
readSessionSkill,
|
|
589
828
|
persistSessionSlashCommand,
|
|
829
|
+
persistSessionPrompt,
|
|
830
|
+
claimTypedCommandMatch,
|
|
831
|
+
readSessionLastPrompt,
|
|
590
832
|
readSessionLastSlashCommand,
|
|
833
|
+
computeProjectHash,
|
|
591
834
|
findRecentSdlcSessionForProject,
|
|
835
|
+
findRecentActiveSessionForProject,
|
|
836
|
+
readSessionAwSkills,
|
|
837
|
+
touchSessionActivity,
|
|
592
838
|
readLastAssistantFromTranscript,
|
|
593
839
|
resolvePromptText,
|
|
594
840
|
getAwCliVersionDetails,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "commonjs",
|
|
3
|
-
"_comment": "Override parent libs/aw/package.json's \"type\": \"module\". The aw-usage hook scripts use require() because
|
|
3
|
+
"_comment": "Override parent libs/aw/package.json's \"type\": \"module\". The aw-usage hook scripts use require() because Claude Code, Cursor, Codex and git run them with `node <path>`. aw copies this file with the scripts to ~/.aw/aw-usage/ so they stay CommonJS there too."
|
|
4
4
|
}
|
package/hooks/codex-home.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
|
|
3
|
+
import { awUsageShellScriptPath } from '../aw-usage-wiring.mjs';
|
|
3
4
|
import { getSupportedHarnessPhaseEntries } from '../hook-manifest.mjs';
|
|
4
5
|
import {
|
|
5
6
|
buildRegistryDelegatingPhaseScriptWithTelemetry,
|
|
@@ -32,7 +33,7 @@ const CODEX_HOME_PHASE_BLUEPRINTS = {
|
|
|
32
33
|
'$HOME/.aw/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh',
|
|
33
34
|
],
|
|
34
35
|
warningMessage: 'WARNING: AW using-aw-skills hook not found in ~/.aw_registry. Run aw init or aw pull platform.',
|
|
35
|
-
telemetryHookPath: '
|
|
36
|
+
telemetryHookPath: awUsageShellScriptPath('aw-usage-session-start.js'),
|
|
36
37
|
harnessEnv: 'codex',
|
|
37
38
|
});
|
|
38
39
|
},
|
|
@@ -60,7 +61,7 @@ set -euo pipefail
|
|
|
60
61
|
# Capture stdin so we can feed it to both telemetry and the reminder delegate.
|
|
61
62
|
STDIN=$(cat)
|
|
62
63
|
|
|
63
|
-
TELEMETRY_HOOK="$
|
|
64
|
+
TELEMETRY_HOOK="${awUsageShellScriptPath('aw-usage-prompt-submit.js')}"
|
|
64
65
|
if [[ -f "$TELEMETRY_HOOK" ]] && command -v node >/dev/null 2>&1; then
|
|
65
66
|
printf '%s' "$STDIN" | AW_HARNESS=codex node "$TELEMETRY_HOOK" >/dev/null 2>&1 || true
|
|
66
67
|
fi
|
|
@@ -116,9 +117,10 @@ ${this.scriptMarker}
|
|
|
116
117
|
set -euo pipefail
|
|
117
118
|
|
|
118
119
|
# AW PostToolUse phase for Codex — usage telemetry.
|
|
119
|
-
# Codex
|
|
120
|
-
#
|
|
121
|
-
|
|
120
|
+
# The interactive Codex app sends PostToolUse for Bash commands and for
|
|
121
|
+
# apply_patch (codex exec may send none); telemetry derives tool_error, shell
|
|
122
|
+
# skill reads and stage documents from the payload.
|
|
123
|
+
TELEMETRY_HOOK="${awUsageShellScriptPath('aw-usage-post-tool-use.js')}"
|
|
122
124
|
if [[ -f "$TELEMETRY_HOOK" ]]; then
|
|
123
125
|
exec node "$TELEMETRY_HOOK"
|
|
124
126
|
fi
|
|
@@ -148,7 +150,7 @@ set -euo pipefail
|
|
|
148
150
|
# Capture stdin before invoking non-blocking sidecars.
|
|
149
151
|
STDIN=$(cat)
|
|
150
152
|
|
|
151
|
-
TELEMETRY_HOOK="$
|
|
153
|
+
TELEMETRY_HOOK="${awUsageShellScriptPath('aw-usage-stop.js')}"
|
|
152
154
|
if [[ -f "$TELEMETRY_HOOK" ]] && command -v node >/dev/null 2>&1; then
|
|
153
155
|
printf '%s' "$STDIN" | AW_HARNESS=codex node "$TELEMETRY_HOOK" >/dev/null 2>&1 || true
|
|
154
156
|
fi
|
package/hooks.mjs
CHANGED
|
@@ -10,6 +10,18 @@ import { join } from 'node:path';
|
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import * as fmt from './fmt.mjs';
|
|
12
12
|
import { AW_CO_AUTHOR } from './constants.mjs';
|
|
13
|
+
import { awUsageShellScriptPath } from './aw-usage-wiring.mjs';
|
|
14
|
+
|
|
15
|
+
const COMMIT_TELEMETRY_HOOK = awUsageShellScriptPath('aw-usage-commit-created.js');
|
|
16
|
+
const COMMIT_TRAILER_HOOK = awUsageShellScriptPath('aw-usage-commit-trailer.js');
|
|
17
|
+
|
|
18
|
+
// Adds Agent-Harness, and AW-Skills + AW co-author + AW-Version for sessions
|
|
19
|
+
// that ran an AW skill or stage (see aw-usage-commit-trailer.js). Never fails the commit.
|
|
20
|
+
const ADD_COMMIT_TRAILERS = `\
|
|
21
|
+
TRAILER_HOOK="${COMMIT_TRAILER_HOOK}"
|
|
22
|
+
if command -v node >/dev/null 2>&1 && [ -f "$TRAILER_HOOK" ]; then
|
|
23
|
+
node "$TRAILER_HOOK" "$1" "${AW_CO_AUTHOR}" >/dev/null 2>&1 || true
|
|
24
|
+
fi`;
|
|
13
25
|
|
|
14
26
|
const HOME = homedir();
|
|
15
27
|
const HOOKS_DIR = join(HOME, '.aw', 'hooks');
|
|
@@ -97,10 +109,7 @@ fi
|
|
|
97
109
|
|
|
98
110
|
# Fire commit_created telemetry if commit has AW co-author trailer
|
|
99
111
|
if git log -1 --format='%b' HEAD 2>/dev/null | grep -qF "Co-Authored-By: AW"; then
|
|
100
|
-
TELEMETRY_HOOK="$
|
|
101
|
-
if [ ! -f "$TELEMETRY_HOOK" ]; then
|
|
102
|
-
TELEMETRY_HOOK="$HOME/.aw-ecc/scripts/hooks/aw-usage-commit-created.js"
|
|
103
|
-
fi
|
|
112
|
+
TELEMETRY_HOOK="${COMMIT_TELEMETRY_HOOK}"
|
|
104
113
|
if command -v node >/dev/null 2>&1 && [ -f "$TELEMETRY_HOOK" ]; then
|
|
105
114
|
COMMIT_HASH="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
106
115
|
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
|
@@ -127,24 +136,15 @@ fi
|
|
|
127
136
|
exit 0
|
|
128
137
|
`;
|
|
129
138
|
|
|
130
|
-
// prepare-commit-msg: synchronous hook —
|
|
139
|
+
// prepare-commit-msg: synchronous hook — adds attribution trailers to commits
|
|
131
140
|
// in AW-linked repos. Skips merge commits and respects AW_NO_COAUTHOR=1 opt-out.
|
|
141
|
+
// Trailers the agent wrote (Claude, Cursor, Codex) are kept.
|
|
132
142
|
const PREPARE_COMMIT_MSG = makeDispatcher('prepare-commit-msg', `\
|
|
133
|
-
#
|
|
143
|
+
# Add attribution trailers unless it's a merge commit or opt-out is set
|
|
134
144
|
if [ "$2" != "merge" ] && [ "$AW_NO_COAUTHOR" != "1" ]; then
|
|
135
|
-
# Only
|
|
145
|
+
# Only AW-linked repos (check for .aw/ symlink or worktree)
|
|
136
146
|
if [ -L ".aw" ] || [ -f ".aw/.git" ]; then
|
|
137
|
-
|
|
138
|
-
# Replace Claude/Cursor co-author trailers with AW
|
|
139
|
-
sed -i.bak '/^Co-Authored-By:.*Claude/d' "$1" 2>/dev/null || true
|
|
140
|
-
sed -i.bak '/^Co-Authored-By:.*noreply@anthropic\\.com/d' "$1" 2>/dev/null || true
|
|
141
|
-
sed -i.bak '/^Co-Authored-By:.*Cursor/d' "$1" 2>/dev/null || true
|
|
142
|
-
rm -f "$1.bak"
|
|
143
|
-
# Only append if not already present (idempotent)
|
|
144
|
-
if ! grep -qF "$TRAILER" "$1"; then
|
|
145
|
-
echo "" >> "$1"
|
|
146
|
-
echo "$TRAILER" >> "$1"
|
|
147
|
-
fi
|
|
147
|
+
${ADD_COMMIT_TRAILERS}
|
|
148
148
|
fi
|
|
149
149
|
fi`);
|
|
150
150
|
|
|
@@ -253,19 +253,9 @@ case "$(pwd)" in /tmp/aw-*|/var/folders/*/aw-*|*/.aw|*/.aw/*) exit 0 ;; esac
|
|
|
253
253
|
# Opt-out escape hatch
|
|
254
254
|
[ "$AW_NO_COAUTHOR" = "1" ] && exit 0
|
|
255
255
|
|
|
256
|
-
# Only
|
|
256
|
+
# Only AW-linked repos (check for .aw/ symlink or worktree)
|
|
257
257
|
if [ -L ".aw" ] || [ -f ".aw/.git" ]; then
|
|
258
|
-
|
|
259
|
-
# Replace Claude/Cursor co-author trailers with AW
|
|
260
|
-
sed -i.bak '/^Co-Authored-By:.*Claude/d' "$1" 2>/dev/null || true
|
|
261
|
-
sed -i.bak '/^Co-Authored-By:.*noreply@anthropic\\.com/d' "$1" 2>/dev/null || true
|
|
262
|
-
sed -i.bak '/^Co-Authored-By:.*Cursor/d' "$1" 2>/dev/null || true
|
|
263
|
-
rm -f "$1.bak"
|
|
264
|
-
# Only append if not already present (idempotent)
|
|
265
|
-
if ! grep -qF "$TRAILER" "$1"; then
|
|
266
|
-
echo "" >> "$1"
|
|
267
|
-
echo "$TRAILER" >> "$1"
|
|
268
|
-
fi
|
|
258
|
+
${ADD_COMMIT_TRAILERS}
|
|
269
259
|
fi
|
|
270
260
|
|
|
271
261
|
exit 0
|
|
@@ -281,10 +271,7 @@ case "$(pwd)" in /tmp/aw-*|/var/folders/*/aw-*|*/.aw|*/.aw/*) exit 0 ;; esac
|
|
|
281
271
|
|
|
282
272
|
# Fire commit_created telemetry if commit has AW co-author trailer
|
|
283
273
|
if git log -1 --format='%b' HEAD 2>/dev/null | grep -qF "Co-Authored-By: AW"; then
|
|
284
|
-
TELEMETRY_HOOK="$
|
|
285
|
-
if [ ! -f "$TELEMETRY_HOOK" ]; then
|
|
286
|
-
TELEMETRY_HOOK="$HOME/.aw-ecc/scripts/hooks/aw-usage-commit-created.js"
|
|
287
|
-
fi
|
|
274
|
+
TELEMETRY_HOOK="${COMMIT_TELEMETRY_HOOK}"
|
|
288
275
|
if command -v node >/dev/null 2>&1 && [ -f "$TELEMETRY_HOOK" ]; then
|
|
289
276
|
COMMIT_HASH="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
290
277
|
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|