@ghl-ai/aw 0.1.73-beta.1 → 0.1.73-beta.3

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/commands/init.mjs CHANGED
@@ -77,6 +77,19 @@ function writeHookManifestBestEffort(manifest, context) {
77
77
  }
78
78
  }
79
79
 
80
+ function installAwUsageHooksBestEffort({ silent = false } = {}) {
81
+ ensureTelemetryConfig();
82
+
83
+ try {
84
+ if (!isDefaultRoutingEnabled(HOME, process.env)) return null;
85
+ const result = installAwUsageHooks();
86
+ return formatAwUsageHooksInstallReport(result);
87
+ } catch (e) {
88
+ if (!silent) fmt.note(`aw-usage hooks install: ${e.message}`, 'Telemetry');
89
+ return null;
90
+ }
91
+ }
92
+
80
93
  function formatIntegrationStatusSummary(statuses, installedNow = []) {
81
94
  if (!statuses || statuses.length === 0) return null;
82
95
 
@@ -506,6 +519,8 @@ export async function initCommand(args) {
506
519
  if (cwd !== HOME) installLocalCommitHook(cwd);
507
520
  if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
508
521
 
522
+ const awUsageHooksReport = installAwUsageHooksBestEffort({ silent });
523
+
509
524
  // Write hook manifest after all hook installation is complete
510
525
  writeHookManifestBestEffort({ eccVersion: AW_ECC_TAG, awVersion: VERSION });
511
526
 
@@ -533,6 +548,7 @@ export async function initCommand(args) {
533
548
  '',
534
549
  ` ${chalk.green('✓')} Registry synced`,
535
550
  ` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`,
551
+ awUsageHooksReport ? ` ${chalk.green('✓')} ${awUsageHooksReport}` : null,
536
552
  removedLegacyStartupFiles.length > 0
537
553
  ? ` ${chalk.green('✓')} Removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
538
554
  : null,
@@ -680,25 +696,12 @@ export async function initCommand(args) {
680
696
  ];
681
697
  if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
682
698
 
683
- // Ensure telemetry config exists (generates machine_id on first run)
684
- ensureTelemetryConfig();
685
-
686
699
  // Install bundled aw-usage producer hooks into ~/.claude.
687
700
  // Copies the scripts + lib files and non-destructively merges 5 hook phases
688
701
  // (SessionStart, UserPromptSubmit, PostToolUse, PostToolUseFailure, Stop)
689
702
  // into ~/.claude/settings.json. Idempotent. After this, every Claude Code
690
703
  // session emits usage_events to the live telemetry API automatically.
691
- let awUsageHooksReport = null;
692
- try {
693
- if (isDefaultRoutingEnabled(HOME, process.env)) {
694
- const result = installAwUsageHooks();
695
- awUsageHooksReport = formatAwUsageHooksInstallReport(result);
696
- }
697
- } catch (e) {
698
- // Non-fatal — telemetry is observational. Surface the error in silent mode
699
- // logs but don't block the install.
700
- if (!silent) fmt.note(`aw-usage hooks install: ${e.message}`, 'Telemetry');
701
- }
704
+ const awUsageHooksReport = installAwUsageHooksBestEffort({ silent });
702
705
 
703
706
  // Write hook manifest after all hook installation is complete, including
704
707
  // bundled usage hooks, so `aw nuke` can prune AW-managed settings entries.
@@ -11,15 +11,23 @@
11
11
 
12
12
  'use strict';
13
13
 
14
- const { buildEvent, sendAsync, isDisabled } = require('../lib/aw-usage-telemetry');
14
+ const {
15
+ buildEvent,
16
+ sendAsync,
17
+ isDisabled,
18
+ findRecentSdlcSessionForProject,
19
+ } = require('../lib/aw-usage-telemetry');
15
20
 
16
21
  function buildCommitCreatedEvent({ commitHash = 'unknown', branch = 'unknown', cwd = process.cwd() } = {}) {
17
- // Git hooks have no harness session context, but cwd lets buildEvent derive
18
- // project_hash so the dashboard can correlate commits back to /aw:* sessions.
19
- const event = buildEvent({ cwd }, 'commit_created', {
22
+ const linkedSession = findRecentSdlcSessionForProject(cwd);
23
+ const event = buildEvent({
24
+ cwd,
25
+ ...(linkedSession?.session_id ? { session_id: linkedSession.session_id } : {}),
26
+ }, 'commit_created', {
20
27
  commit_hash: commitHash,
21
28
  commit_sha: commitHash,
22
29
  branch,
30
+ ...(linkedSession?.session_id ? { linked_session_source: 'project_recent_sdlc_session' } : {}),
23
31
  });
24
32
 
25
33
  // Override harness to 'git' since this fires from a git hook, not a harness.
@@ -109,7 +109,9 @@ function processPromptSubmitInput(input, deps = {}) {
109
109
 
110
110
  if (slash) {
111
111
  persistSkill(getSessionId(input), input?.turn_id || null, slash);
112
- persistSlashCmd(getSessionId(input), slash);
112
+ persistSlashCmd(getSessionId(input), slash, {
113
+ cwd: input?.cwd || (input?.workspace_roots && input.workspace_roots[0]) || null,
114
+ });
113
115
  events.push({
114
116
  eventType: 'skill_invoked',
115
117
  payload: {
@@ -21,6 +21,7 @@ const SENDER_SCRIPT = path.join(__dirname, '..', 'hooks', 'aw-usage-telemetry-se
21
21
  const AW_HOME = path.join(os.homedir(), '.aw');
22
22
  const CONFIG_PATH = path.join(AW_HOME, 'telemetry', 'config.json');
23
23
  const SESSION_DIR = path.join(AW_HOME, 'telemetry', 'sessions');
24
+ const SESSION_PROJECT_DIR = path.join(SESSION_DIR, 'by-project');
24
25
  const DEDUPE_DIR = path.join(os.tmpdir(), 'aw-usage-telemetry-dedupe');
25
26
 
26
27
  // ── Git config cache (once per process) ──────────────────────────────
@@ -157,6 +158,16 @@ function computeProjectHash(cwd) {
157
158
  return crypto.createHash('sha256').update(cwd).digest('hex').slice(0, 16);
158
159
  }
159
160
 
161
+ function normalizeProjectHash(value) {
162
+ return typeof value === 'string' && /^[a-f0-9]{16}$/i.test(value) ? value.toLowerCase() : null;
163
+ }
164
+
165
+ function resolveProjectHash(context) {
166
+ if (!context || typeof context !== 'object') return null;
167
+ return normalizeProjectHash(context.project_hash || context.projectHash)
168
+ || computeProjectHash(context.cwd);
169
+ }
170
+
160
171
  // ── Session file cleanup ─────────────────────────────────────────────
161
172
  // Prune session files older than SESSION_MAX_AGE_MS to prevent unbounded growth.
162
173
  // Called once per session start — best-effort, never blocks.
@@ -241,21 +252,40 @@ function readSessionSkill(sessionId, turnId) {
241
252
  // to the originating /aw:* invocation. Separate from `last_skill` because
242
253
  // the source is the prompt, not the tool, and the lifetime is the whole
243
254
  // session (not just one turn).
244
- function persistSessionSlashCommand(sessionId, slashCommand) {
255
+ function writeProjectSessionIndex(projectHash, sessionId, slashCommand) {
256
+ if (!projectHash || !sessionId || !slashCommand?.is_sdlc_stage) return;
257
+ try {
258
+ fs.mkdirSync(SESSION_PROJECT_DIR, { recursive: true });
259
+ fs.writeFileSync(path.join(SESSION_PROJECT_DIR, `${projectHash}.json`), JSON.stringify({
260
+ project_hash: projectHash,
261
+ session_id: sessionId,
262
+ command_namespace: slashCommand.command_namespace || null,
263
+ command_name: slashCommand.command_name,
264
+ is_sdlc_stage: Boolean(slashCommand.is_sdlc_stage),
265
+ updated_at: new Date().toISOString(),
266
+ }));
267
+ } catch { /* ignore */ }
268
+ }
269
+
270
+ function persistSessionSlashCommand(sessionId, slashCommand, context = {}) {
245
271
  if (!sessionId || !slashCommand?.command_name) return;
246
272
  try {
247
273
  fs.mkdirSync(SESSION_DIR, { recursive: true });
248
274
  const state = readSessionState(sessionId);
275
+ const projectHash = resolveProjectHash(context) || state.project_hash || null;
249
276
  fs.writeFileSync(path.join(SESSION_DIR, sessionId + '.json'), JSON.stringify({
250
277
  ...state,
278
+ ...(projectHash ? { project_hash: projectHash } : {}),
251
279
  last_slash_command: {
252
280
  command_namespace: slashCommand.command_namespace || null,
253
281
  command_name: slashCommand.command_name,
254
282
  command_args: slashCommand.command_args || '',
255
283
  is_sdlc_stage: Boolean(slashCommand.is_sdlc_stage),
284
+ ...(projectHash ? { project_hash: projectHash } : {}),
256
285
  updated_at: new Date().toISOString(),
257
286
  },
258
287
  }));
288
+ writeProjectSessionIndex(projectHash, sessionId, slashCommand);
259
289
  } catch { /* ignore */ }
260
290
  }
261
291
 
@@ -265,6 +295,59 @@ function readSessionLastSlashCommand(sessionId) {
265
295
  return cmd;
266
296
  }
267
297
 
298
+ function parseUpdatedAt(value) {
299
+ const millis = Date.parse(value || '');
300
+ return Number.isFinite(millis) ? millis : 0;
301
+ }
302
+
303
+ function isRecentSdlcSessionCandidate(candidate, projectHash, maxAgeMs) {
304
+ if (!candidate?.session_id) return false;
305
+ if (!candidate?.is_sdlc_stage) return false;
306
+ if (candidate.project_hash !== projectHash) return false;
307
+ const updatedAt = parseUpdatedAt(candidate.updated_at);
308
+ if (!updatedAt) return false;
309
+ return Date.now() - updatedAt <= maxAgeMs;
310
+ }
311
+
312
+ function readProjectSessionIndex(projectHash, maxAgeMs) {
313
+ try {
314
+ const candidate = JSON.parse(fs.readFileSync(path.join(SESSION_PROJECT_DIR, `${projectHash}.json`), 'utf8'));
315
+ return isRecentSdlcSessionCandidate(candidate, projectHash, maxAgeMs) ? candidate : null;
316
+ } catch {
317
+ return null;
318
+ }
319
+ }
320
+
321
+ function findRecentSdlcSessionForProject(cwd, maxAgeMs = SESSION_MAX_AGE_MS) {
322
+ const projectHash = computeProjectHash(cwd);
323
+ if (!projectHash) return null;
324
+
325
+ const indexed = readProjectSessionIndex(projectHash, maxAgeMs);
326
+ if (indexed) return indexed;
327
+
328
+ let best = null;
329
+ try {
330
+ const entries = fs.readdirSync(SESSION_DIR);
331
+ for (const entry of entries) {
332
+ if (!entry.endsWith('.json')) continue;
333
+ const sessionId = entry.slice(0, -'.json'.length);
334
+ const state = readSessionState(sessionId);
335
+ const cmd = state.last_slash_command;
336
+ const candidate = {
337
+ session_id: sessionId,
338
+ project_hash: cmd?.project_hash || state.project_hash || null,
339
+ is_sdlc_stage: Boolean(cmd?.is_sdlc_stage),
340
+ updated_at: cmd?.updated_at || state.updated_at || null,
341
+ };
342
+ if (!isRecentSdlcSessionCandidate(candidate, projectHash, maxAgeMs)) continue;
343
+ if (!best || parseUpdatedAt(candidate.updated_at) > parseUpdatedAt(best.updated_at)) {
344
+ best = candidate;
345
+ }
346
+ }
347
+ } catch { /* ignore */ }
348
+ return best;
349
+ }
350
+
268
351
  // ── Short-TTL dedupe guards ──────────────────────────────────────────
269
352
 
270
353
  function normalizeDedupePart(value) {
@@ -503,6 +586,7 @@ module.exports = {
503
586
  readSessionSkill,
504
587
  persistSessionSlashCommand,
505
588
  readSessionLastSlashCommand,
589
+ findRecentSdlcSessionForProject,
506
590
  readLastAssistantFromTranscript,
507
591
  resolvePromptText,
508
592
  getAwCliVersionDetails,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.73-beta.1",
3
+ "version": "0.1.73-beta.3",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {