@fyso/awareness-framework 0.1.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/src/cli.js ADDED
@@ -0,0 +1,1330 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const VALID_STATES = new Set(['started', 'in-progress', 'paused', 'blocked', 'waiting', 'done', 'in-review', 'ready']);
8
+ const DEFAULT_STATE = 'in-progress';
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const repoRoot = path.resolve(__dirname, '..');
11
+
12
+ export function runCli(argv, options = {}) {
13
+ const ctx = {
14
+ env: options.env || process.env,
15
+ stdout: options.stdout || process.stdout,
16
+ stderr: options.stderr || process.stderr,
17
+ };
18
+
19
+ try {
20
+ const parsed = parseArgs(argv);
21
+ const [command, subcommand] = parsed.positionals;
22
+
23
+ if (!command || command === 'help' || parsed.opts.help) {
24
+ printHelp(ctx);
25
+ return 0;
26
+ }
27
+
28
+ switch (command) {
29
+ case 'init':
30
+ return initCommand(ctx, parsed.opts);
31
+ case 'status':
32
+ return statusCommand(ctx, parsed.opts);
33
+ case 'refresh':
34
+ return statusCommand(ctx, parsed.opts);
35
+ case 'check':
36
+ return checkCommand(ctx, parsed.opts);
37
+ case 'focus':
38
+ return focusCommand(ctx, parsed.opts);
39
+ case 'log':
40
+ return logCommand(ctx, parsed.opts);
41
+ case 'handoff':
42
+ return handoffCommand(ctx, parsed.opts);
43
+ case 'evaluate':
44
+ return evaluateCommand(ctx, parsed.opts);
45
+ case 'hook':
46
+ return hookCommand(ctx, subcommand, parsed.opts);
47
+ case 'schedule':
48
+ return scheduleCommand(ctx, subcommand, parsed.opts);
49
+ case 'personality':
50
+ return personalityCommand(ctx, subcommand, parsed.opts);
51
+ case 'user':
52
+ return userCommand(ctx, subcommand, parsed.opts);
53
+ default:
54
+ err(ctx, `Unknown command: ${command}`);
55
+ err(ctx, 'Run `awareness help` for usage.');
56
+ return 1;
57
+ }
58
+ } catch (error) {
59
+ err(ctx, error.message);
60
+ return 1;
61
+ }
62
+ }
63
+
64
+ function parseArgs(argv) {
65
+ const opts = {};
66
+ const positionals = [];
67
+
68
+ for (let index = 0; index < argv.length; index += 1) {
69
+ const token = argv[index];
70
+ if (!token.startsWith('--')) {
71
+ positionals.push(token);
72
+ continue;
73
+ }
74
+
75
+ const equalsIndex = token.indexOf('=');
76
+ if (equalsIndex !== -1) {
77
+ opts[toCamel(token.slice(2, equalsIndex))] = token.slice(equalsIndex + 1);
78
+ continue;
79
+ }
80
+
81
+ const key = toCamel(token.slice(2));
82
+ const next = argv[index + 1];
83
+ if (next && !next.startsWith('--')) {
84
+ opts[key] = next;
85
+ index += 1;
86
+ } else {
87
+ opts[key] = true;
88
+ }
89
+ }
90
+
91
+ return { opts, positionals };
92
+ }
93
+
94
+ function toCamel(value) {
95
+ return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
96
+ }
97
+
98
+ function printHelp(ctx) {
99
+ out(ctx, `Awareness Framework CLI
100
+
101
+ Usage:
102
+ awareness init [--home PATH]
103
+ awareness init --wrappers [--home PATH] [--user-home PATH] [--config-home PATH] [--overwrite-wrappers]
104
+ awareness status [--home PATH]
105
+ awareness refresh [--home PATH]
106
+ awareness check [--home PATH] [--strict]
107
+ awareness focus --task ID --summary TEXT --repo TEXT --branch TEXT --next TEXT [--state STATE] [--home PATH]
108
+ awareness log --task ID --summary TEXT --changes TEXT [--context TEXT] [--state STATE] [--evidence TEXT] [--next TEXT] [--home PATH]
109
+ awareness handoff [--home PATH]
110
+ awareness evaluate [--home PATH] [--force] [--print]
111
+ awareness hook run --event EVENT [--tool TOOL] [--quiet] [--home PATH]
112
+ awareness hook install --tool codex|claude|opencode|all [--command CMD] [--home PATH] [--user-home PATH] [--config-home PATH] [--overwrite]
113
+ awareness schedule run --cadence hourly|daily [--home PATH]
114
+ awareness schedule install --cadence hourly|daily|all [--command CMD] [--load] [--home PATH] [--user-home PATH]
115
+ awareness personality show [--home PATH]
116
+ awareness personality note --text TEXT [--evidence TEXT] [--home PATH]
117
+ awareness personality adopt --text TEXT [--evidence TEXT] [--home PATH]
118
+ awareness user show --user ID [--channel NAME] [--home PATH]
119
+ awareness user note --user ID --kind nickname|question|topic|preference|fact|note --text TEXT [--evidence TEXT] [--channel NAME] [--home PATH]
120
+
121
+ Scope options:
122
+ --home PATH Exact/base private state folder. Default: ~/.agents
123
+ --agent-folder PATH Alias for the base private state folder.
124
+ --channel NAME Store state under <folder>/channels/<safe-name>.
125
+ --user ID Select a user memory file for user commands.
126
+
127
+ The CLI maintains private files under ~/.agents by default. It does not post to Jira, GitHub, or any external system.`);
128
+ }
129
+
130
+ function initCommand(ctx, opts) {
131
+ const home = agentsHome(ctx, opts);
132
+ const userHome = userHomePath(ctx, opts);
133
+ const configHome = configHomePath(ctx, opts, userHome);
134
+ const today = todayParts(ctx);
135
+ const created = [];
136
+ const existing = [];
137
+ const overwritten = [];
138
+
139
+ for (const dir of ['awareness', 'worklog', 'memory', 'memory/users', 'evaluations', 'runtime']) {
140
+ ensureDir(path.join(home, dir));
141
+ }
142
+
143
+ writeIfMissing(path.join(home, 'AGENTS.md'), readTemplate('agent-instructions.md'), created, existing);
144
+ writeIfMissing(path.join(home, 'awareness', 'current.md'), initialAwareness(today), created, existing);
145
+ writeIfMissing(path.join(home, 'worklog', `${today.date}.md`), dailyWorklog(today.date), created, existing);
146
+ writeIfMissing(path.join(home, 'memory', 'personality.md'), readTemplate('personality.md'), created, existing);
147
+ writeIfMissing(path.join(home, 'memory', 'preferences.md'), privateMemorySeed('Preferences'), created, existing);
148
+ writeIfMissing(path.join(home, 'memory', 'patterns.md'), privateMemorySeed('Patterns'), created, existing);
149
+ writeIfMissing(path.join(home, 'memory', 'long-term.md'), readTemplate('memory-long-term.md'), created, existing);
150
+
151
+ if (opts.wrappers) {
152
+ writeWrappers({
153
+ canonicalPath: path.join(home, 'AGENTS.md'),
154
+ userHome,
155
+ configHome,
156
+ overwrite: Boolean(opts.overwriteWrappers),
157
+ created,
158
+ existing,
159
+ overwritten,
160
+ });
161
+ }
162
+
163
+ out(ctx, `Initialized awareness home: ${home}`);
164
+ out(ctx, `Created: ${created.length ? created.map((file) => displayPath(home, file)).join(', ') : 'none'}`);
165
+ out(ctx, `Existing: ${existing.length ? existing.map((file) => displayPath(home, file)).join(', ') : 'none'}`);
166
+ if (opts.wrappers) {
167
+ out(ctx, `Wrappers: ${wrapperSummary({ userHome, configHome })}`);
168
+ out(ctx, `Overwritten: ${overwritten.length ? overwritten.join(', ') : 'none'}`);
169
+ }
170
+ return 0;
171
+ }
172
+
173
+ function statusCommand(ctx, opts) {
174
+ const home = agentsHome(ctx, opts);
175
+ const currentPath = awarenessPath(home);
176
+ if (!fs.existsSync(currentPath)) {
177
+ err(ctx, `Missing awareness board: ${currentPath}`);
178
+ return 1;
179
+ }
180
+
181
+ const content = fs.readFileSync(currentPath, 'utf8');
182
+ const focus = extractSection(content, 'Current Focus');
183
+ out(ctx, `Awareness home: ${home}`);
184
+ out(ctx, '');
185
+ out(ctx, 'Current Focus');
186
+ out(ctx, focus.trim() || '(empty)');
187
+
188
+ const warnings = collectWarnings(home, todayParts(ctx));
189
+ out(ctx, '');
190
+ out(ctx, warnings.length ? `Warnings: ${warnings.length}` : 'Warnings: none');
191
+ for (const warning of warnings) {
192
+ out(ctx, `- ${warning}`);
193
+ }
194
+ return warnings.length ? 1 : 0;
195
+ }
196
+
197
+ function checkCommand(ctx, opts) {
198
+ const home = agentsHome(ctx, opts);
199
+ const warnings = collectWarnings(home, todayParts(ctx));
200
+
201
+ if (!warnings.length) {
202
+ out(ctx, `OK: awareness state is maintainable (${home})`);
203
+ return 0;
204
+ }
205
+
206
+ out(ctx, `Found ${warnings.length} awareness issue(s):`);
207
+ for (const warning of warnings) {
208
+ out(ctx, `- ${warning}`);
209
+ }
210
+
211
+ return opts.strict ? 1 : 0;
212
+ }
213
+
214
+ function focusCommand(ctx, opts) {
215
+ const home = agentsHome(ctx, opts);
216
+ const task = required(opts, 'task');
217
+ const summary = required(opts, 'summary');
218
+ const next = required(opts, 'next');
219
+ const state = normalizeState(opts.state || DEFAULT_STATE);
220
+ const repo = opts.repo || 'Unspecified';
221
+ const branch = opts.branch || 'Unspecified';
222
+ const timestamp = formatTimestamp(todayParts(ctx));
223
+
224
+ ensurePrivateState(home, ctx);
225
+ const currentPath = awarenessPath(home);
226
+ const original = fs.readFileSync(currentPath, 'utf8');
227
+ const focusBlock = [
228
+ `- Task: ${task}`,
229
+ `- Summary: ${summary}`,
230
+ `- Repository: ${repo}`,
231
+ `- Branch: ${branch}`,
232
+ `- State: ${state}`,
233
+ `- Next: ${next}`,
234
+ ].join('\n');
235
+
236
+ let updated = replaceMetadata(original, 'Updated', timestamp);
237
+ updated = replaceSection(updated, 'Current Focus', `${focusBlock}\n`);
238
+ updated = upsertActiveTask(updated, {
239
+ task,
240
+ summary,
241
+ repo,
242
+ branch,
243
+ state,
244
+ next,
245
+ timestamp,
246
+ }, home);
247
+ fs.writeFileSync(currentPath, updated);
248
+
249
+ appendWorklog(home, todayParts(ctx), {
250
+ task,
251
+ summary: `Focus switched: ${summary}`,
252
+ context: `${repo} / ${branch}`,
253
+ state,
254
+ changes: `Current focus set to ${task}: ${summary}.`,
255
+ evidence: currentPath,
256
+ next,
257
+ });
258
+
259
+ out(ctx, `Current focus updated: ${task} - ${summary}`);
260
+ return 0;
261
+ }
262
+
263
+ function logCommand(ctx, opts) {
264
+ const home = agentsHome(ctx, opts);
265
+ const today = todayParts(ctx);
266
+ const state = normalizeState(opts.state || DEFAULT_STATE);
267
+ const task = opts.task || 'Unassigned';
268
+ const summary = required(opts, 'summary');
269
+ const changes = required(opts, 'changes');
270
+
271
+ ensurePrivateState(home, ctx);
272
+ appendWorklog(home, today, {
273
+ task,
274
+ summary,
275
+ context: opts.context || currentContext(home),
276
+ state,
277
+ changes,
278
+ evidence: opts.evidence || 'Not specified',
279
+ next: opts.next || '',
280
+ });
281
+
282
+ out(ctx, `Worklog entry appended: ${task} - ${summary}`);
283
+ return 0;
284
+ }
285
+
286
+ function handoffCommand(ctx, opts) {
287
+ const home = agentsHome(ctx, opts);
288
+ const currentPath = awarenessPath(home);
289
+ if (!fs.existsSync(currentPath)) {
290
+ err(ctx, `Missing awareness board: ${currentPath}`);
291
+ return 1;
292
+ }
293
+
294
+ const content = fs.readFileSync(currentPath, 'utf8');
295
+ out(ctx, 'Handoff Snapshot');
296
+ out(ctx, '');
297
+ out(ctx, 'Current Focus');
298
+ out(ctx, extractSection(content, 'Current Focus').trim() || '(empty)');
299
+ out(ctx, '');
300
+ out(ctx, 'Blocked Tasks');
301
+ out(ctx, extractSection(content, 'Blocked Tasks').trim() || '- None.');
302
+ out(ctx, '');
303
+ out(ctx, 'End-of-Day Candidates');
304
+ out(ctx, extractSection(content, 'End-of-Day Candidates').trim() || '- None.');
305
+
306
+ const warnings = collectWarnings(home, todayParts(ctx));
307
+ if (warnings.length) {
308
+ out(ctx, '');
309
+ out(ctx, 'Warnings');
310
+ for (const warning of warnings) {
311
+ out(ctx, `- ${warning}`);
312
+ }
313
+ }
314
+
315
+ return warnings.length ? 1 : 0;
316
+ }
317
+
318
+ function evaluateCommand(ctx, opts) {
319
+ const home = agentsHome(ctx, opts);
320
+ ensurePrivateState(home, ctx);
321
+ const today = todayParts(ctx);
322
+ const evaluationPath = path.join(home, 'evaluations', `${today.date}.md`);
323
+ const content = buildEvaluation(home, today);
324
+
325
+ if (opts.print) {
326
+ out(ctx, content.trimEnd());
327
+ return 0;
328
+ }
329
+
330
+ if (fs.existsSync(evaluationPath) && !opts.force) {
331
+ err(ctx, `Evaluation already exists: ${evaluationPath}`);
332
+ err(ctx, 'Use --force to overwrite or --print to preview.');
333
+ return 1;
334
+ }
335
+
336
+ ensureDir(path.dirname(evaluationPath));
337
+ fs.writeFileSync(evaluationPath, content);
338
+ out(ctx, `Evaluation written: ${evaluationPath}`);
339
+ return 0;
340
+ }
341
+
342
+ function personalityCommand(ctx, subcommand, opts) {
343
+ const home = agentsHome(ctx, opts);
344
+ ensurePrivateState(home, ctx);
345
+ const file = personalityPath(home);
346
+
347
+ switch (subcommand) {
348
+ case 'show':
349
+ case undefined:
350
+ out(ctx, fs.readFileSync(file, 'utf8').trimEnd());
351
+ return 0;
352
+ case 'note':
353
+ return personalityAppend(ctx, file, opts, 'Candidate observation');
354
+ case 'adopt':
355
+ return personalityAppend(ctx, file, opts, 'Accepted trait');
356
+ default:
357
+ err(ctx, `Unknown personality command: ${subcommand}`);
358
+ err(ctx, 'Use: show, note, or adopt.');
359
+ return 1;
360
+ }
361
+ }
362
+
363
+ function userCommand(ctx, subcommand, opts) {
364
+ const home = agentsHome(ctx, opts);
365
+ ensurePrivateState(home, ctx);
366
+ const user = selectedUser(ctx, opts);
367
+ const userSlug = safeScopeSlug(user, 'user');
368
+ const file = userMemoryPath(home, userSlug);
369
+
370
+ switch (subcommand) {
371
+ case 'show':
372
+ case undefined:
373
+ if (!fs.existsSync(file)) {
374
+ err(ctx, `Missing user memory: ${file}`);
375
+ return 1;
376
+ }
377
+ out(ctx, fs.readFileSync(file, 'utf8').trimEnd());
378
+ return 0;
379
+ case 'note':
380
+ return userMemoryAppend(ctx, file, user, opts);
381
+ default:
382
+ err(ctx, `Unknown user command: ${subcommand}`);
383
+ err(ctx, 'Use: show or note.');
384
+ return 1;
385
+ }
386
+ }
387
+
388
+ function userMemoryAppend(ctx, file, user, opts) {
389
+ const text = required(opts, 'text');
390
+ const kind = opts.kind || 'note';
391
+ const evidence = opts.evidence || 'Not specified';
392
+ const section = userMemorySection(kind);
393
+ const today = todayParts(ctx);
394
+ const timestamp = formatTimestamp(today);
395
+
396
+ ensureDir(path.dirname(file));
397
+ if (!fs.existsSync(file)) {
398
+ fs.writeFileSync(file, initialUserMemory(user, timestamp));
399
+ }
400
+
401
+ let content = fs.readFileSync(file, 'utf8');
402
+ content = replaceMetadata(content, 'Updated', timestamp);
403
+ content = appendToSection(content, section, `- ${today.date} ${today.time}: ${text} (evidence: ${evidence})\n`);
404
+ fs.writeFileSync(file, content);
405
+ out(ctx, `User memory updated: ${file}`);
406
+ return 0;
407
+ }
408
+
409
+ function userMemorySection(kind) {
410
+ const sections = {
411
+ nickname: 'Nicknames',
412
+ question: 'Questions',
413
+ topic: 'Topics',
414
+ preference: 'Preferences',
415
+ fact: 'Facts',
416
+ note: 'Notes',
417
+ };
418
+ if (!sections[kind]) {
419
+ throw new Error(`Invalid user memory kind: ${kind}. Valid kinds: ${Object.keys(sections).join(', ')}`);
420
+ }
421
+ return sections[kind];
422
+ }
423
+
424
+ function hookCommand(ctx, subcommand, opts) {
425
+ switch (subcommand) {
426
+ case 'run':
427
+ return hookRunCommand(ctx, opts);
428
+ case 'install':
429
+ return hookInstallCommand(ctx, opts);
430
+ default:
431
+ err(ctx, `Unknown hook command: ${subcommand || '(missing)'}`);
432
+ err(ctx, 'Use: awareness hook run or awareness hook install.');
433
+ return 1;
434
+ }
435
+ }
436
+
437
+ function hookRunCommand(ctx, opts) {
438
+ const home = agentsHome(ctx, opts);
439
+ const today = todayParts(ctx);
440
+ const event = required(opts, 'event');
441
+ const tool = opts.tool || 'unknown';
442
+
443
+ ensurePrivateState(home, ctx);
444
+ const warnings = collectWarnings(home, today);
445
+ const file = appendRuntimeEvent(home, today, 'hooks', {
446
+ source: 'hook',
447
+ tool,
448
+ event,
449
+ warnings: warnings.length,
450
+ });
451
+
452
+ if (!opts.quiet) {
453
+ out(ctx, `Hook recorded: ${tool} ${event}`);
454
+ out(ctx, `Runtime log: ${file}`);
455
+ out(ctx, warnings.length ? `Warnings: ${warnings.length}` : 'Warnings: none');
456
+ }
457
+
458
+ return 0;
459
+ }
460
+
461
+ function hookInstallCommand(ctx, opts) {
462
+ const tool = opts.tool || 'all';
463
+ const userHome = userHomePath(ctx, opts);
464
+ const configHome = configHomePath(ctx, opts, userHome);
465
+ const command = opts.command || ctx.env.AWARENESS_COMMAND || 'awareness';
466
+ const home = shouldPinAwarenessHome(ctx, opts) ? agentsHome(ctx, opts) : null;
467
+ const installed = [];
468
+ const existing = [];
469
+
470
+ for (const target of expandTargets(tool, ['codex', 'claude', 'opencode'])) {
471
+ if (target === 'codex') installed.push(installCodexHooks(userHome, command, home));
472
+ if (target === 'claude') installed.push(installClaudeHooks(userHome, command, home));
473
+ if (target === 'opencode') {
474
+ const result = installOpenCodePlugin(configHome, command, home, Boolean(opts.overwrite));
475
+ if (result.status === 'existing') existing.push(result.file);
476
+ else installed.push(result.file);
477
+ }
478
+ }
479
+
480
+ out(ctx, `Hook install target: ${tool}`);
481
+ out(ctx, `Installed or updated: ${installed.length ? installed.join(', ') : 'none'}`);
482
+ if (existing.length) out(ctx, `Existing custom files left untouched: ${existing.join(', ')}`);
483
+ if (!path.isAbsolute(command)) {
484
+ out(ctx, `Warning: command is not absolute (${command}). Use --command "$(command -v awareness)" for launchd or restricted PATH environments.`);
485
+ }
486
+ return 0;
487
+ }
488
+
489
+ function scheduleCommand(ctx, subcommand, opts) {
490
+ switch (subcommand) {
491
+ case 'run':
492
+ return scheduleRunCommand(ctx, opts);
493
+ case 'install':
494
+ return scheduleInstallCommand(ctx, opts);
495
+ default:
496
+ err(ctx, `Unknown schedule command: ${subcommand || '(missing)'}`);
497
+ err(ctx, 'Use: awareness schedule run or awareness schedule install.');
498
+ return 1;
499
+ }
500
+ }
501
+
502
+ function scheduleRunCommand(ctx, opts) {
503
+ const home = agentsHome(ctx, opts);
504
+ const today = todayParts(ctx);
505
+ const cadence = required(opts, 'cadence');
506
+ if (!['hourly', 'daily'].includes(cadence)) throw new Error(`Invalid cadence: ${cadence}. Valid cadences: hourly, daily`);
507
+
508
+ ensurePrivateState(home, ctx);
509
+ const warnings = collectWarnings(home, today);
510
+ const eventFile = appendRuntimeEvent(home, today, 'schedule', {
511
+ source: 'schedule',
512
+ cadence,
513
+ warnings: warnings.length,
514
+ });
515
+
516
+ let evaluation = null;
517
+ if (cadence === 'daily') {
518
+ evaluation = writeEvaluationIfMissing(home, today);
519
+ }
520
+
521
+ out(ctx, `Schedule run complete: ${cadence}`);
522
+ out(ctx, `Runtime log: ${eventFile}`);
523
+ if (evaluation) out(ctx, `Evaluation: ${evaluation.status} (${evaluation.file})`);
524
+ out(ctx, warnings.length ? `Warnings: ${warnings.length}` : 'Warnings: none');
525
+ return 0;
526
+ }
527
+
528
+ function scheduleInstallCommand(ctx, opts) {
529
+ const cadence = required(opts, 'cadence');
530
+ const cadences = expandTargets(cadence, ['hourly', 'daily']);
531
+ const userHome = userHomePath(ctx, opts);
532
+ const command = opts.command || ctx.env.AWARENESS_COMMAND || 'awareness';
533
+ const home = shouldPinAwarenessHome(ctx, opts) ? agentsHome(ctx, opts) : path.join(userHome, '.agents');
534
+ const labelScope = scopeLabel(ctx, opts);
535
+ const launchAgentDir = path.join(userHome, 'Library', 'LaunchAgents');
536
+ const launchdLogDir = path.join(home, 'runtime', 'launchd');
537
+ const written = [];
538
+ const loaded = [];
539
+
540
+ ensureDir(launchAgentDir);
541
+ ensureDir(launchdLogDir);
542
+
543
+ for (const target of cadences) {
544
+ const label = labelScope ? `dev.fyso.awareness.${labelScope}.${target}` : `dev.fyso.awareness.${target}`;
545
+ const file = path.join(launchAgentDir, `${label}.plist`);
546
+ const args = [command, 'schedule', 'run', '--cadence', target];
547
+ if (home) args.push('--home', home);
548
+ const interval = target === 'hourly' ? 3600 : 86400;
549
+ fs.writeFileSync(file, launchAgentPlist({
550
+ label,
551
+ args,
552
+ interval,
553
+ stdoutPath: path.join(launchdLogDir, `${target}.out.log`),
554
+ stderrPath: path.join(launchdLogDir, `${target}.err.log`),
555
+ }));
556
+ written.push(file);
557
+
558
+ if (opts.load) {
559
+ const result = loadLaunchAgent(file, label);
560
+ if (result.status !== 0) {
561
+ throw new Error(`launchctl failed for ${file}: ${result.stderr || result.stdout || `exit ${result.status}`}`);
562
+ }
563
+ loaded.push(file);
564
+ }
565
+ }
566
+
567
+ out(ctx, `Schedule install target: ${cadence}`);
568
+ out(ctx, `LaunchAgents written: ${written.join(', ')}`);
569
+ out(ctx, `Loaded: ${loaded.length ? loaded.join(', ') : 'not requested'}`);
570
+ if (!path.isAbsolute(command)) {
571
+ out(ctx, `Warning: command is not absolute (${command}). launchd may not resolve shell PATH; prefer --command "$(command -v awareness)".`);
572
+ }
573
+ return 0;
574
+ }
575
+
576
+ function appendRuntimeEvent(home, today, category, record) {
577
+ const dir = path.join(home, 'runtime', category);
578
+ ensureDir(dir);
579
+ const file = path.join(dir, `${today.date}.jsonl`);
580
+ fs.appendFileSync(file, `${JSON.stringify({
581
+ timestamp: formatTimestamp(today),
582
+ ...record,
583
+ })}\n`);
584
+ return file;
585
+ }
586
+
587
+ function writeEvaluationIfMissing(home, today) {
588
+ const evaluationPath = path.join(home, 'evaluations', `${today.date}.md`);
589
+ if (fs.existsSync(evaluationPath)) {
590
+ return { file: evaluationPath, status: 'already exists' };
591
+ }
592
+
593
+ ensureDir(path.dirname(evaluationPath));
594
+ fs.writeFileSync(evaluationPath, buildEvaluation(home, today));
595
+ return { file: evaluationPath, status: 'written' };
596
+ }
597
+
598
+ function installCodexHooks(userHome, command, home) {
599
+ const file = path.join(userHome, '.codex', 'hooks.json');
600
+ const data = readJsonObject(file);
601
+ data.hooks ||= {};
602
+ addCommandHook(data, 'SessionStart', hookShellCommand(command, 'codex', 'session-start', home), 'startup|resume|clear|compact', 'Refreshing awareness');
603
+ addCommandHook(data, 'Stop', hookShellCommand(command, 'codex', 'stop', home), null, 'Recording awareness stop');
604
+ addCommandHook(data, 'PreCompact', hookShellCommand(command, 'codex', 'pre-compact', home), 'manual|auto', 'Recording pre-compact state');
605
+ addCommandHook(data, 'PostCompact', hookShellCommand(command, 'codex', 'post-compact', home), 'manual|auto', 'Recording post-compact state');
606
+ writeJsonObject(file, data);
607
+ return file;
608
+ }
609
+
610
+ function installClaudeHooks(userHome, command, home) {
611
+ const file = path.join(userHome, '.claude', 'settings.json');
612
+ const data = readJsonObject(file);
613
+ data.hooks ||= {};
614
+ addCommandHook(data, 'SessionStart', hookShellCommand(command, 'claude', 'session-start', home), 'startup|resume|clear|compact', 'Refreshing awareness');
615
+ addCommandHook(data, 'Stop', hookShellCommand(command, 'claude', 'stop', home), null, 'Recording awareness stop');
616
+ addCommandHook(data, 'SessionEnd', hookShellCommand(command, 'claude', 'session-end', home), 'clear|resume|logout|prompt_input_exit|bypass_permissions_disabled|other', 'Recording session end');
617
+ addCommandHook(data, 'PreCompact', hookShellCommand(command, 'claude', 'pre-compact', home), 'manual|auto', 'Recording pre-compact state');
618
+ addCommandHook(data, 'PostCompact', hookShellCommand(command, 'claude', 'post-compact', home), 'manual|auto', 'Recording post-compact state');
619
+ writeJsonObject(file, data);
620
+ return file;
621
+ }
622
+
623
+ function installOpenCodePlugin(configHome, command, home, overwrite) {
624
+ const file = path.join(configHome, 'opencode', 'plugins', 'awareness-framework.js');
625
+ const marker = 'Awareness Framework generated plugin';
626
+ if (fs.existsSync(file)) {
627
+ const existing = fs.readFileSync(file, 'utf8');
628
+ if (!existing.includes(marker) && !overwrite) {
629
+ return { file, status: 'existing' };
630
+ }
631
+ }
632
+
633
+ ensureDir(path.dirname(file));
634
+ fs.writeFileSync(file, openCodePluginContent({ command, home, marker }));
635
+ return { file, status: 'written' };
636
+ }
637
+
638
+ function addCommandHook(data, event, command, matcher, statusMessage) {
639
+ data.hooks[event] ||= [];
640
+ const alreadyExists = data.hooks[event].some((group) => Array.isArray(group.hooks)
641
+ && group.hooks.some((hook) => hook.type === 'command' && hook.command === command));
642
+ if (alreadyExists) return;
643
+
644
+ const group = {
645
+ hooks: [
646
+ {
647
+ type: 'command',
648
+ command,
649
+ timeout: 30,
650
+ },
651
+ ],
652
+ };
653
+ if (matcher) group.matcher = matcher;
654
+ if (statusMessage) group.hooks[0].statusMessage = statusMessage;
655
+ data.hooks[event].push(group);
656
+ }
657
+
658
+ function hookShellCommand(command, tool, event, home) {
659
+ const parts = [command, 'hook', 'run', '--tool', tool, '--event', event, '--quiet'];
660
+ if (home) parts.push('--home', home);
661
+ return parts.map(shellQuote).join(' ');
662
+ }
663
+
664
+ function openCodePluginContent({ command, home, marker }) {
665
+ const homeArgs = home ? ['--home', home] : [];
666
+ return `// ${marker}
667
+ // Local private state is under ~/.agents by default. Do not commit runtime output.
668
+
669
+ const awarenessCommand = ${JSON.stringify(command)};
670
+ const homeArgs = ${JSON.stringify(homeArgs)};
671
+
672
+ async function record(event) {
673
+ try {
674
+ const proc = Bun.spawn([
675
+ awarenessCommand,
676
+ "hook",
677
+ "run",
678
+ "--tool",
679
+ "opencode",
680
+ "--event",
681
+ event,
682
+ "--quiet",
683
+ ...homeArgs,
684
+ ], {
685
+ stdout: "ignore",
686
+ stderr: "ignore",
687
+ });
688
+ await proc.exited;
689
+ } catch {
690
+ // Hook failures must not block OpenCode sessions.
691
+ }
692
+ }
693
+
694
+ export const AwarenessFramework = async () => ({
695
+ event: async ({ event }) => {
696
+ const type = event?.type;
697
+ if (type === "session.created" || type === "session.idle" || type === "session.compacted" || type === "session.error") {
698
+ await record(type);
699
+ }
700
+ },
701
+ "experimental.session.compacting": async (_input, output) => {
702
+ await record("experimental.session.compacting");
703
+ if (Array.isArray(output.context)) {
704
+ output.context.push("Awareness Framework: after compaction, refresh private state with awareness refresh when task state may have changed.");
705
+ }
706
+ },
707
+ });
708
+ `;
709
+ }
710
+
711
+ function launchAgentPlist({ label, args, interval, stdoutPath, stderrPath }) {
712
+ const argItems = args.map((arg) => ` <string>${escapeXml(arg)}</string>`).join('\n');
713
+ return `<?xml version="1.0" encoding="UTF-8"?>
714
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
715
+ <plist version="1.0">
716
+ <dict>
717
+ <key>Label</key>
718
+ <string>${escapeXml(label)}</string>
719
+ <key>ProgramArguments</key>
720
+ <array>
721
+ ${argItems}
722
+ </array>
723
+ <key>StartInterval</key>
724
+ <integer>${interval}</integer>
725
+ <key>StandardOutPath</key>
726
+ <string>${escapeXml(stdoutPath)}</string>
727
+ <key>StandardErrorPath</key>
728
+ <string>${escapeXml(stderrPath)}</string>
729
+ </dict>
730
+ </plist>
731
+ `;
732
+ }
733
+
734
+ function loadLaunchAgent(file, label) {
735
+ const uid = typeof process.getuid === 'function' ? process.getuid() : null;
736
+ const target = uid === null ? 'gui/0' : `gui/${uid}`;
737
+ spawnSync('launchctl', ['bootout', target, file], { encoding: 'utf8' });
738
+ const bootstrap = spawnSync('launchctl', ['bootstrap', target, file], { encoding: 'utf8' });
739
+ if (bootstrap.status === 0) return bootstrap;
740
+ const output = `${bootstrap.stderr || ''}${bootstrap.stdout || ''}`;
741
+ if (/already loaded/i.test(output)) {
742
+ return spawnSync('launchctl', ['kickstart', '-k', `${target}/${label}`], { encoding: 'utf8' });
743
+ }
744
+ return bootstrap;
745
+ }
746
+
747
+ function personalityAppend(ctx, file, opts, kind) {
748
+ const text = required(opts, 'text');
749
+ const evidence = opts.evidence || 'Not specified';
750
+ const today = todayParts(ctx);
751
+ const timestamp = formatTimestamp(today);
752
+ let content = fs.readFileSync(file, 'utf8');
753
+ content = replaceMetadata(content, 'Updated', timestamp);
754
+
755
+ if (kind === 'Accepted trait') {
756
+ content = appendToSection(content, 'Accepted Traits', `- ${today.date}: ${text} (evidence: ${evidence})\n`);
757
+ } else {
758
+ const entry = [
759
+ `### ${today.date} ${today.time} - Candidate`,
760
+ '',
761
+ `- Observation: ${text}`,
762
+ `- Evidence: ${evidence}`,
763
+ '- Status: candidate',
764
+ '',
765
+ ].join('\n');
766
+ content = appendToSection(content, 'Candidate Observations', entry);
767
+ }
768
+
769
+ fs.writeFileSync(file, content);
770
+ out(ctx, `${kind} recorded in ${file}`);
771
+ return 0;
772
+ }
773
+
774
+ function collectWarnings(home, today) {
775
+ const warnings = [];
776
+ const currentPath = awarenessPath(home);
777
+ const worklogPath = path.join(home, 'worklog', `${today.date}.md`);
778
+
779
+ if (!fs.existsSync(path.join(home, 'AGENTS.md'))) warnings.push('Missing AGENTS.md private protocol.');
780
+ if (!fs.existsSync(currentPath)) warnings.push('Missing awareness/current.md.');
781
+ if (!fs.existsSync(worklogPath)) warnings.push(`Missing daily worklog for ${today.date}.`);
782
+
783
+ if (fs.existsSync(currentPath)) {
784
+ const current = fs.readFileSync(currentPath, 'utf8');
785
+ const focus = extractSection(current, 'Current Focus');
786
+ if (!focus.trim()) warnings.push('Current Focus is empty.');
787
+ if (!/- Task:\s+\S+/.test(focus)) warnings.push('Current Focus is missing Task.');
788
+ if (!/- Next:\s+(?!The next concrete action)\S+/.test(focus)) warnings.push('Current Focus is missing a concrete Next action.');
789
+ if (/YYYY-MM-DD|branch-name/.test(current)) warnings.push('Awareness board still contains template placeholders.');
790
+
791
+ const active = extractSection(current, 'Active Tasks');
792
+ const taskBlocks = active
793
+ .split(/\n(?=### )/)
794
+ .filter((block) => block.trim().startsWith('### '));
795
+ for (const block of taskBlocks) {
796
+ const title = block.split('\n')[0].replace(/^### /, '').trim();
797
+ if (!/- Next:\s*\n\s+-\s+\S+/.test(block) && !/- Next:\s+\S+/.test(block)) warnings.push(`Active task lacks Next: ${title}`);
798
+ if (!/- Evidence:\s*\n\s+-\s+\S+/.test(block) && !/- Evidence:\s+\S+/.test(block)) warnings.push(`Active task lacks Evidence: ${title}`);
799
+ }
800
+ }
801
+
802
+ if (fs.existsSync(worklogPath)) {
803
+ const worklog = fs.readFileSync(worklogPath, 'utf8');
804
+ const entries = parseWorklogEntries(worklog);
805
+ if (!entries.length) warnings.push('Daily worklog has no entries.');
806
+ if (entries.some((entry) => entry.task === 'Unassigned')) warnings.push('Daily worklog has Unassigned entries to reconcile.');
807
+ if (entries.some((entry) => !entry.hasEvidence)) warnings.push('Some worklog entries may be missing Evidence.');
808
+ }
809
+
810
+ return warnings;
811
+ }
812
+
813
+ function buildEvaluation(home, today) {
814
+ const currentPath = awarenessPath(home);
815
+ const worklogPath = path.join(home, 'worklog', `${today.date}.md`);
816
+ const current = fs.existsSync(currentPath) ? fs.readFileSync(currentPath, 'utf8') : '';
817
+ const worklog = fs.existsSync(worklogPath) ? fs.readFileSync(worklogPath, 'utf8') : '';
818
+ const warnings = collectWarnings(home, today);
819
+ const entries = parseWorklogEntries(worklog);
820
+ const assignedEntries = entries.filter((entry) => entry.task && entry.task !== 'Unassigned').length;
821
+ const freshness = current.includes(today.date) ? 2 : current ? 1 : 0;
822
+ const traceability = !entries.length ? 0 : assignedEntries / entries.length >= 0.8 ? 2 : 1;
823
+ const handoff = /- Next:\s+(?!The next concrete action)\S+/.test(extractSection(current, 'Current Focus')) ? 2 : current ? 1 : 0;
824
+ const noise = current.split('\n').length <= 180 && !/YYYY-MM-DD|branch-name/.test(current) ? 2 : 1;
825
+ const reporting = extractSection(current, 'End-of-Day Candidates').trim() ? 2 : entries.length ? 1 : 0;
826
+
827
+ return `# Awareness Evaluation - ${today.date}
828
+
829
+ ## Score
830
+
831
+ | Dimension | Score | Evidence |
832
+ |-----------|-------|----------|
833
+ | Freshness | ${freshness} | ${freshness === 2 ? 'Awareness file references today.' : 'Awareness freshness should be checked.'} |
834
+ | Traceability | ${traceability} | ${assignedEntries}/${entries.length} worklog entries have explicit task IDs. |
835
+ | Handoff quality | ${handoff} | ${handoff === 2 ? 'Current Focus has a concrete Next action.' : 'Next action needs clarification.'} |
836
+ | Noise control | ${noise} | Awareness board has ${current.split('\n').length} lines. |
837
+ | Reporting readiness | ${reporting} | End-of-day candidates ${reporting === 2 ? 'are present' : 'need attention'}. |
838
+
839
+ ## Warnings
840
+
841
+ ${warnings.length ? warnings.map((warning) => `- ${warning}`).join('\n') : '- None.'}
842
+
843
+ ## Proposed Changes
844
+
845
+ - No framework change proposed automatically.
846
+ - Review repeated warnings before proposing a methodology PR.
847
+ `;
848
+ }
849
+
850
+ function appendWorklog(home, today, entry) {
851
+ const worklogPath = path.join(home, 'worklog', `${today.date}.md`);
852
+ ensureDir(path.dirname(worklogPath));
853
+ if (!fs.existsSync(worklogPath)) {
854
+ fs.writeFileSync(worklogPath, dailyWorklog(today.date));
855
+ }
856
+
857
+ const nextLine = entry.next ? `- Next: ${entry.next}\n` : '';
858
+ const markdown = `
859
+ ### ${today.time} - ${entry.task} - ${entry.summary}
860
+
861
+ - Context: ${entry.context || 'Not specified'}
862
+ - State: ${entry.state}
863
+ - Changes: ${entry.changes}
864
+ - Evidence: ${entry.evidence || 'Not specified'}
865
+ ${nextLine}`;
866
+
867
+ fs.appendFileSync(worklogPath, markdown);
868
+ }
869
+
870
+ function parseWorklogEntries(worklog) {
871
+ const headingRegex = /^#{2,3} \d{2}:\d{2} - .+$/gm;
872
+ const headings = [...worklog.matchAll(headingRegex)];
873
+ return headings.map((heading, index) => {
874
+ const start = heading.index;
875
+ const end = index + 1 < headings.length ? headings[index + 1].index : worklog.length;
876
+ const block = worklog.slice(start, end);
877
+ const headingText = heading[0];
878
+ const headingParts = headingText.replace(/^#{2,3} \d{2}:\d{2} - /, '').split(' - ');
879
+ const headingTask = headingParts.length > 1 && isTaskId(headingParts[0]) ? headingParts[0] : null;
880
+ const jiraTask = block.match(/^- Jira:\s+(.+)$/m)?.[1]?.trim();
881
+ return {
882
+ block,
883
+ task: headingTask || jiraTask || null,
884
+ hasEvidence: /- Evidence:\s+\S/.test(block) || /- Evidence:\s*\n\s+-\s+\S/.test(block),
885
+ };
886
+ });
887
+ }
888
+
889
+ function isTaskId(value) {
890
+ return value === 'Unassigned' || /^[A-Z][A-Z0-9]+-\d+$/.test(value);
891
+ }
892
+
893
+ function upsertActiveTask(content, task, home) {
894
+ const active = extractSection(content, 'Active Tasks');
895
+ const heading = `${task.task} - ${task.summary}`;
896
+ const taskPattern = new RegExp(`### ${escapeRegExp(task.task)}(?: - .*?)?\\n[\\s\\S]*?(?=\\n### |\\n## |$)`);
897
+ const block = [
898
+ `### ${heading}`,
899
+ '',
900
+ `- State: ${task.state}`,
901
+ `- Last update: ${task.timestamp}`,
902
+ `- Repository: ${task.repo}`,
903
+ `- Branch: ${task.branch}`,
904
+ '- Done:',
905
+ ' - Focus updated.',
906
+ '- Next:',
907
+ ` - ${task.next}`,
908
+ '- Blockers:',
909
+ ' - None.',
910
+ '- Evidence:',
911
+ ` - ${awarenessPath(home)}`,
912
+ '',
913
+ ].join('\n');
914
+
915
+ const newActive = taskPattern.test(active)
916
+ ? active.replace(taskPattern, block.trimEnd())
917
+ : `${active.replace(/^- None\.\s*/m, '').trimEnd()}\n\n${block}`.trimStart();
918
+
919
+ return replaceSection(content, 'Active Tasks', `${newActive.trimEnd()}\n`);
920
+ }
921
+
922
+ function ensurePrivateState(home, ctx) {
923
+ const today = todayParts(ctx);
924
+ ensureDir(home);
925
+ ensureDir(path.join(home, 'awareness'));
926
+ ensureDir(path.join(home, 'worklog'));
927
+ ensureDir(path.join(home, 'memory'));
928
+ ensureDir(path.join(home, 'memory', 'users'));
929
+ ensureDir(path.join(home, 'evaluations'));
930
+ ensureDir(path.join(home, 'runtime'));
931
+ if (!fs.existsSync(path.join(home, 'AGENTS.md'))) fs.writeFileSync(path.join(home, 'AGENTS.md'), readTemplate('agent-instructions.md'));
932
+ if (!fs.existsSync(awarenessPath(home))) fs.writeFileSync(awarenessPath(home), initialAwareness(today));
933
+ if (!fs.existsSync(path.join(home, 'worklog', `${today.date}.md`))) fs.writeFileSync(path.join(home, 'worklog', `${today.date}.md`), dailyWorklog(today.date));
934
+ if (!fs.existsSync(personalityPath(home))) fs.writeFileSync(personalityPath(home), readTemplate('personality.md'));
935
+ if (!fs.existsSync(path.join(home, 'memory', 'long-term.md'))) fs.writeFileSync(path.join(home, 'memory', 'long-term.md'), readTemplate('memory-long-term.md'));
936
+ }
937
+
938
+ function replaceSection(content, section, body) {
939
+ const heading = `## ${section}`;
940
+ const pattern = new RegExp(`(^|\\n)(## ${escapeRegExp(section)}\\n\\n?)([\\s\\S]*?)(?=\\n## |$)`);
941
+ if (pattern.test(content)) {
942
+ return content.replace(pattern, `$1$2${body.trimEnd()}\n`);
943
+ }
944
+
945
+ return `${content.trimEnd()}\n\n${heading}\n\n${body.trimEnd()}\n`;
946
+ }
947
+
948
+ function appendToSection(content, section, addition) {
949
+ const current = extractSection(content, section);
950
+ const cleaned = current.replace(/^- None yet\.\n?/m, '').trimEnd();
951
+ return replaceSection(content, section, `${cleaned ? `${cleaned}\n\n` : ''}${addition.trimEnd()}\n`);
952
+ }
953
+
954
+ function extractSection(content, section) {
955
+ const pattern = new RegExp(`(?:^|\\n)## ${escapeRegExp(section)}\\n\\n?([\\s\\S]*?)(?=\\n## |$)`);
956
+ const match = content.match(pattern);
957
+ return match ? match[1] : '';
958
+ }
959
+
960
+ function replaceMetadata(content, key, value) {
961
+ const pattern = new RegExp(`^- ${escapeRegExp(key)}:.*$`, 'm');
962
+ if (pattern.test(content)) {
963
+ return content.replace(pattern, `- ${key}: ${value}`);
964
+ }
965
+ return content.replace(/^# .+$/m, (heading) => `${heading}\n\n- ${key}: ${value}`);
966
+ }
967
+
968
+ function currentContext(home) {
969
+ const file = awarenessPath(home);
970
+ if (!fs.existsSync(file)) return 'Not specified';
971
+ const focus = extractSection(fs.readFileSync(file, 'utf8'), 'Current Focus');
972
+ const repo = focus.match(/^- Repository:\s+(.+)$/m)?.[1] || 'Not specified';
973
+ const branch = focus.match(/^- Branch:\s+(.+)$/m)?.[1] || 'Not specified';
974
+ return `${repo} / ${branch}`;
975
+ }
976
+
977
+ function readTemplate(name) {
978
+ const file = path.join(repoRoot, 'templates', name);
979
+ return fs.readFileSync(file, 'utf8');
980
+ }
981
+
982
+ function writeIfMissing(file, content, created, existing) {
983
+ ensureDir(path.dirname(file));
984
+ if (fs.existsSync(file)) {
985
+ existing.push(file);
986
+ return;
987
+ }
988
+ fs.writeFileSync(file, content);
989
+ created.push(file);
990
+ }
991
+
992
+ function writeWrappers({ canonicalPath, userHome, configHome, overwrite, created, existing, overwritten }) {
993
+ const wrappers = [
994
+ {
995
+ file: path.join(userHome, '.codex', 'AGENTS.md'),
996
+ tool: 'Codex',
997
+ },
998
+ {
999
+ file: path.join(userHome, '.claude', 'CLAUDE.md'),
1000
+ tool: 'Claude Code',
1001
+ },
1002
+ {
1003
+ file: path.join(configHome, 'opencode', 'AGENTS.md'),
1004
+ tool: 'OpenCode',
1005
+ },
1006
+ {
1007
+ file: path.join(userHome, '.pi', 'agent', 'AGENTS.md'),
1008
+ tool: 'Pi',
1009
+ },
1010
+ ];
1011
+
1012
+ for (const wrapper of wrappers) {
1013
+ writeWrapper(wrapper.file, wrapperContent(wrapper.tool, canonicalPath), overwrite, created, existing, overwritten);
1014
+ }
1015
+ }
1016
+
1017
+ function writeWrapper(file, content, overwrite, created, existing, overwritten) {
1018
+ ensureDir(path.dirname(file));
1019
+ if (fs.existsSync(file)) {
1020
+ if (!overwrite) {
1021
+ existing.push(file);
1022
+ return;
1023
+ }
1024
+ fs.writeFileSync(file, content);
1025
+ overwritten.push(file);
1026
+ return;
1027
+ }
1028
+
1029
+ fs.writeFileSync(file, content);
1030
+ created.push(file);
1031
+ }
1032
+
1033
+ function wrapperContent(tool, canonicalPath) {
1034
+ return `# ${tool} Agent Instructions
1035
+
1036
+ Read and follow the canonical private protocol at:
1037
+
1038
+ @${canonicalPath}
1039
+
1040
+ If this CLI does not expand @ imports automatically, open that file explicitly before starting work.
1041
+
1042
+ At session start, prefer \`awareness status\` or \`awareness check\` when the Awareness CLI is available. Use \`awareness refresh\` when parallel work may have changed state, and \`awareness handoff\` before returning control.
1043
+ `;
1044
+ }
1045
+
1046
+ function wrapperSummary({ userHome, configHome }) {
1047
+ return [
1048
+ path.join(userHome, '.codex', 'AGENTS.md'),
1049
+ path.join(userHome, '.claude', 'CLAUDE.md'),
1050
+ path.join(configHome, 'opencode', 'AGENTS.md'),
1051
+ path.join(userHome, '.pi', 'agent', 'AGENTS.md'),
1052
+ ].join(', ');
1053
+ }
1054
+
1055
+ function displayPath(base, file) {
1056
+ const relative = path.relative(base, file);
1057
+ return relative && !relative.startsWith('..') && !path.isAbsolute(relative) ? relative : file;
1058
+ }
1059
+
1060
+ function dailyWorklog(date) {
1061
+ return `# Daily Worklog - ${date}
1062
+
1063
+ ## Entries
1064
+ `;
1065
+ }
1066
+
1067
+ function initialAwareness(today) {
1068
+ return `# Agent Awareness
1069
+
1070
+ - Updated: ${formatTimestamp(today)}
1071
+ - Operator: Unassigned
1072
+ - Scope: Local private state; do not commit
1073
+
1074
+ ## Current Focus
1075
+
1076
+ - Task: Unassigned
1077
+ - Summary: No current focus set
1078
+ - Repository: Unspecified
1079
+ - Branch: Unspecified
1080
+ - State: waiting
1081
+ - Next: Run awareness focus to set the current task
1082
+
1083
+ ## Active Tasks
1084
+
1085
+ - None.
1086
+
1087
+ ## Blocked Tasks
1088
+
1089
+ - None.
1090
+
1091
+ ## Waiting On User
1092
+
1093
+ - None.
1094
+
1095
+ ## Parking Lot
1096
+
1097
+ - None.
1098
+
1099
+ ## End-of-Day Candidates
1100
+
1101
+ - None.
1102
+ `;
1103
+ }
1104
+
1105
+ function privateMemorySeed(title) {
1106
+ return `# ${title}
1107
+
1108
+ - Updated: never
1109
+ - Scope: Local private state; do not commit
1110
+
1111
+ ## Entries
1112
+
1113
+ - None yet.
1114
+ `;
1115
+ }
1116
+
1117
+ function initialUserMemory(user, timestamp) {
1118
+ return `# User Memory - ${user}
1119
+
1120
+ - Updated: ${timestamp}
1121
+ - Scope: Local private user memory; do not commit
1122
+ - User: ${user}
1123
+
1124
+ ## Nicknames
1125
+
1126
+ - None yet.
1127
+
1128
+ ## Questions
1129
+
1130
+ - None yet.
1131
+
1132
+ ## Topics
1133
+
1134
+ - None yet.
1135
+
1136
+ ## Preferences
1137
+
1138
+ - None yet.
1139
+
1140
+ ## Facts
1141
+
1142
+ - None yet.
1143
+
1144
+ ## Notes
1145
+
1146
+ - None yet.
1147
+ `;
1148
+ }
1149
+
1150
+ function normalizeState(state) {
1151
+ if (!VALID_STATES.has(state)) {
1152
+ throw new Error(`Invalid state: ${state}. Valid states: ${[...VALID_STATES].join(', ')}`);
1153
+ }
1154
+ return state;
1155
+ }
1156
+
1157
+ function expandTargets(value, allowed) {
1158
+ if (value === 'all') return allowed;
1159
+ const targets = String(value).split(',').map((item) => item.trim()).filter(Boolean);
1160
+ if (!targets.length) throw new Error(`Missing target. Valid targets: ${allowed.join(', ')}, all`);
1161
+ for (const target of targets) {
1162
+ if (!allowed.includes(target)) {
1163
+ throw new Error(`Invalid target: ${target}. Valid targets: ${allowed.join(', ')}, all`);
1164
+ }
1165
+ }
1166
+ return targets;
1167
+ }
1168
+
1169
+ function required(opts, key) {
1170
+ if (!opts[key] || opts[key] === true) {
1171
+ throw new Error(`Missing required option: --${key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`);
1172
+ }
1173
+ return opts[key];
1174
+ }
1175
+
1176
+ function agentsHome(ctx, opts) {
1177
+ const raw = opts.home
1178
+ || opts.agentFolder
1179
+ || ctx.env.AGENTS_HOME
1180
+ || ctx.env.AWARENESS_AGENT_FOLDER
1181
+ || path.join(os.homedir(), '.agents');
1182
+ return scopedHome(path.resolve(expandHome(raw)), awarenessScope(ctx, opts));
1183
+ }
1184
+
1185
+ function userHomePath(ctx, opts) {
1186
+ const raw = opts.userHome || ctx.env.AWARENESS_USER_HOME || os.homedir();
1187
+ return path.resolve(expandHome(raw));
1188
+ }
1189
+
1190
+ function configHomePath(ctx, opts, userHome) {
1191
+ const raw = opts.configHome || ctx.env.XDG_CONFIG_HOME || path.join(userHome, '.config');
1192
+ return path.resolve(expandHome(raw));
1193
+ }
1194
+
1195
+ function shouldPinAwarenessHome(ctx, opts) {
1196
+ return Boolean(
1197
+ opts.home
1198
+ || opts.agentFolder
1199
+ || ctx.env.AGENTS_HOME
1200
+ || ctx.env.AWARENESS_AGENT_FOLDER
1201
+ || awarenessScope(ctx, opts).channel,
1202
+ );
1203
+ }
1204
+
1205
+ function awarenessScope(ctx, opts) {
1206
+ const channel = opts.channel || ctx.env.AWARENESS_CHANNEL || '';
1207
+ return {
1208
+ channel: channel ? safeScopeSlug(channel, 'channel') : '',
1209
+ };
1210
+ }
1211
+
1212
+ function selectedUser(ctx, opts) {
1213
+ const user = opts.user || ctx.env.AWARENESS_USER || '';
1214
+ if (!user || user === true) {
1215
+ throw new Error('Missing required option: --user');
1216
+ }
1217
+ return user;
1218
+ }
1219
+
1220
+ function scopedHome(base, scope) {
1221
+ let home = base;
1222
+ if (scope.channel) home = path.join(home, 'channels', scope.channel);
1223
+ return home;
1224
+ }
1225
+
1226
+ function scopeLabel(ctx, opts) {
1227
+ const scope = awarenessScope(ctx, opts);
1228
+ return scope.channel;
1229
+ }
1230
+
1231
+ function safeScopeSlug(value, kind) {
1232
+ const slug = String(value)
1233
+ .trim()
1234
+ .replace(/^#|^@/, '')
1235
+ .toLowerCase()
1236
+ .replace(/[^a-z0-9._-]+/g, '-')
1237
+ .replace(/^-+|-+$/g, '')
1238
+ .slice(0, 96);
1239
+ if (!slug) throw new Error(`Invalid ${kind} scope: ${value}`);
1240
+ return slug;
1241
+ }
1242
+
1243
+ function expandHome(value) {
1244
+ if (value === '~') return os.homedir();
1245
+ if (value.startsWith('~/')) return path.join(os.homedir(), value.slice(2));
1246
+ return value;
1247
+ }
1248
+
1249
+ function awarenessPath(home) {
1250
+ return path.join(home, 'awareness', 'current.md');
1251
+ }
1252
+
1253
+ function personalityPath(home) {
1254
+ return path.join(home, 'memory', 'personality.md');
1255
+ }
1256
+
1257
+ function userMemoryPath(home, userSlug) {
1258
+ return path.join(home, 'memory', 'users', `${userSlug}.md`);
1259
+ }
1260
+
1261
+ function ensureDir(dir) {
1262
+ fs.mkdirSync(dir, { recursive: true });
1263
+ }
1264
+
1265
+ function readJsonObject(file) {
1266
+ if (!fs.existsSync(file)) return {};
1267
+ try {
1268
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
1269
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1270
+ throw new Error('root value must be an object');
1271
+ }
1272
+ return parsed;
1273
+ } catch (error) {
1274
+ throw new Error(`Could not parse JSON file ${file}: ${error.message}`);
1275
+ }
1276
+ }
1277
+
1278
+ function writeJsonObject(file, data) {
1279
+ ensureDir(path.dirname(file));
1280
+ fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
1281
+ }
1282
+
1283
+ function todayParts(ctx) {
1284
+ const now = ctx.env.AWARENESS_NOW ? new Date(ctx.env.AWARENESS_NOW) : new Date();
1285
+ if (Number.isNaN(now.getTime())) throw new Error(`Invalid AWARENESS_NOW value: ${ctx.env.AWARENESS_NOW}`);
1286
+ const year = now.getFullYear();
1287
+ const month = String(now.getMonth() + 1).padStart(2, '0');
1288
+ const day = String(now.getDate()).padStart(2, '0');
1289
+ const hour = String(now.getHours()).padStart(2, '0');
1290
+ const minute = String(now.getMinutes()).padStart(2, '0');
1291
+ const offset = -now.getTimezoneOffset();
1292
+ const sign = offset >= 0 ? '+' : '-';
1293
+ const offsetHour = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
1294
+ const offsetMinute = String(Math.abs(offset) % 60).padStart(2, '0');
1295
+ return {
1296
+ date: `${year}-${month}-${day}`,
1297
+ time: `${hour}:${minute}`,
1298
+ timezone: `${sign}${offsetHour}:${offsetMinute}`,
1299
+ };
1300
+ }
1301
+
1302
+ function formatTimestamp(parts) {
1303
+ return `${parts.date} ${parts.time} ${parts.timezone}`;
1304
+ }
1305
+
1306
+ function escapeRegExp(value) {
1307
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1308
+ }
1309
+
1310
+ function shellQuote(value) {
1311
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
1312
+ return `'${value.replaceAll("'", "'\\''")}'`;
1313
+ }
1314
+
1315
+ function escapeXml(value) {
1316
+ return String(value)
1317
+ .replaceAll('&', '&amp;')
1318
+ .replaceAll('<', '&lt;')
1319
+ .replaceAll('>', '&gt;')
1320
+ .replaceAll('"', '&quot;')
1321
+ .replaceAll("'", '&apos;');
1322
+ }
1323
+
1324
+ function out(ctx, message) {
1325
+ ctx.stdout.write(`${message}\n`);
1326
+ }
1327
+
1328
+ function err(ctx, message) {
1329
+ ctx.stderr.write(`${message}\n`);
1330
+ }