aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
126 lines • 4.7 kB
JavaScript
/**
* Activity-log auto-append hook
*
* Post-command hook that automatically appends an activity-log entry
* after qualifying CLI commands succeed. Closes the discipline gap
* called out in the activity-log rule:
*
* "Subagents silently create and delete artifacts with no record
* visible to the parent orchestrator"
*
* Failures are non-blocking: if the entry can't be written, we log to
* stderr and let the primary command's success stand.
*
* @design @.aiwg/architecture/storage-design.md
* @issue #934
* @issue #978
*/
import { formatEntry } from '../../../activity-log/types.js';
import { resolveStorage } from '../../../storage/index.js';
const LOG_PATH = 'activity.log';
/**
* Map of CLI command id → operation token. Commands not in this map
* skip the hook (most reads, queries, scaffolding helpers, etc.).
*/
const COMMAND_OPERATION_MAP = {
// Framework deployment
use: 'deploy',
refresh: 'deploy',
remove: 'delete',
// Extension scaffolding
'add-agent': 'create',
'add-command': 'create',
'add-skill': 'create',
'add-template': 'create',
'add-behavior': 'create',
// Validation
'validate-metadata': 'lint',
// Index updates
index: 'update', // Note: 'aiwg index build' is the primary mutating subcommand
// Ops ecosystem
ops: 'create', // crude — `aiwg ops init` creates; `push`/`adopt` also mutate
};
/**
* Build a one-line summary from the command + args. Bounded to ≤120
* chars per the activity-log rule.
*/
function summarize(command, args) {
const argString = args.join(' ').trim();
const base = argString.length > 0 ? `aiwg ${command} ${argString}` : `aiwg ${command}`;
if (base.length <= 120)
return base;
return base.slice(0, 117) + '...';
}
/**
* Returns true when AIWG_SKIP_ACTIVITY_LOG is set to a truthy value.
* Mirrors the gate in src/activity-log/cli.ts (#975).
*/
function isActivityLogSkipped() {
const raw = process.env['AIWG_SKIP_ACTIVITY_LOG'];
if (!raw)
return false;
const v = raw.toLowerCase().trim();
if (v === '0' || v === 'false' || v === '')
return false;
return true;
}
export const activityLogPostCommandHook = {
id: 'aiwg:activity-log-auto-append',
event: 'post-command',
priority: 100,
filter: {
// Only fire for the commands we know how to summarize. The map above
// is the source of truth.
commands: Object.keys(COMMAND_OPERATION_MAP),
},
async execute(ctx) {
// Skip on env-var opt-out
if (isActivityLogSkipped()) {
return { action: 'continue' };
}
// Skip if the command failed (post-command runs even on failure for
// some hook flows; we never want to log a failure as a successful op)
const exitCode = ctx.data?.['exitCode'];
if (typeof exitCode === 'number' && exitCode !== 0) {
return { action: 'continue' };
}
const operation = COMMAND_OPERATION_MAP[ctx.command];
if (!operation) {
// Filter should prevent this, but defensive
return { action: 'continue' };
}
try {
const adapter = await resolveStorage('activity_log');
const line = formatEntry({
timestamp: new Date(),
operation,
summary: summarize(ctx.command, ctx.args),
}) + '\n';
// Prefer atomic append (#976) when available
if (typeof adapter.append === 'function') {
const existing = (await adapter.read(LOG_PATH)) ?? '';
if (existing.length > 0 && !existing.endsWith('\n')) {
await adapter.append(LOG_PATH, '\n');
}
await adapter.append(LOG_PATH, line);
}
else {
const existing = (await adapter.read(LOG_PATH)) ?? '';
const trailing = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
await adapter.write(LOG_PATH, existing + trailing + line);
}
}
catch (err) {
// Non-blocking — log to stderr and let the primary command's
// success stand. Per the activity-log rule's "Rule 4: Non-Blocking".
const msg = err instanceof Error ? err.message : String(err);
process.stderr.write(`activity-log auto-append failed (non-fatal): ${msg}\n`);
}
return { action: 'continue' };
},
};
/**
* The list of commands the hook fires for. Exported for tests.
*/
export const HOOKED_COMMANDS = Object.keys(COMMAND_OPERATION_MAP);
//# sourceMappingURL=activity-log-hook.js.map