@event4u/agent-config 8.4.0 → 8.4.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.
Files changed (126) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  4. package/dist/discovery/deprecation-report.md +1 -1
  5. package/dist/discovery/discovery-manifest.json +1 -1
  6. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  7. package/dist/discovery/discovery-manifest.summary.md +1 -1
  8. package/dist/discovery/orphan-report.md +1 -1
  9. package/dist/discovery/packs.json +1 -1
  10. package/dist/discovery/trust-report.md +1 -1
  11. package/dist/discovery/workspaces.json +1 -1
  12. package/dist/mcp/registry-manifest.json +1 -1
  13. package/package.json +4 -1
  14. package/src/agent-src/README.md +64 -0
  15. package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
  16. package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
  17. package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
  18. package/src/agent-src/templates/AGENTS.md +34 -0
  19. package/src/agent-src/templates/scripts/README.md +195 -0
  20. package/src/agent-src/templates/scripts/check_memory.ts +662 -0
  21. package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
  22. package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
  23. package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
  24. package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
  25. package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
  26. package/src/agent-src/templates/scripts/memory_report.ts +868 -0
  27. package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
  28. package/src/agent-src/templates/scripts/memory_status.ts +215 -0
  29. package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
  30. package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
  31. package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
  32. package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
  33. package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
  34. package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
  35. package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
  36. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
  37. package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
  38. package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
  39. package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
  40. package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
  41. package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
  42. package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
  43. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
  44. package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
  45. package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
  46. package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
  47. package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
  48. package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
  49. package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
  50. package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
  51. package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
  52. package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
  53. package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
  54. package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
  55. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
  56. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
  57. package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
  58. package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
  59. package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
  60. package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
  61. package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
  62. package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
  63. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
  64. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
  65. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
  66. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
  67. package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
  68. package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
  73. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
  74. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
  75. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
  76. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
  77. package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
  78. package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
  79. package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
  80. package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
  81. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
  82. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
  83. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
  84. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
  85. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
  86. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
  87. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
  88. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
  89. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
  90. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
  91. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
  92. package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
  93. package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
  94. package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
  95. package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
  96. package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
  97. package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
  98. package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
  99. package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
  100. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
  101. package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
  102. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
  103. package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
  104. package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
  105. package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
  106. package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
  107. package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
  108. package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
  109. package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
  110. package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
  111. package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
  112. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
  113. package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
  114. package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
  115. package/src/install/atomic.ts +111 -0
  116. package/src/install/conflict.ts +226 -0
  117. package/src/install/detect.ts +280 -0
  118. package/src/install/paths.ts +90 -0
  119. package/src/install/plan.ts +205 -0
  120. package/src/install/preflight.ts +210 -0
  121. package/src/install/selectedTools.ts +52 -0
  122. package/src/install/toolDetection.ts +120 -0
  123. package/src/install/txlog.ts +161 -0
  124. package/src/install/types.ts +175 -0
  125. package/src/install/wizard-plan.ts +216 -0
  126. package/src/scripts/prepack-check.mjs +108 -5
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Write-side helper: drop an engineering-memory signal.
4
+ *
5
+ * TypeScript twin of `src/agent-src/templates/scripts/memory_signal.py`
6
+ * (ADR-200, consumer-template memory). The public API and CLI contract
7
+ * mirror the Python original EXACTLY — same exported names (snake_case kept
8
+ * deliberately), same exit codes, stdout/stderr split, byte-identical
9
+ * messages, same append-only JSONL shape and rate-limit behaviour. No
10
+ * behaviour changes — latent Python bugs are replicated and flagged as
11
+ * divergence candidates in the porting report, not fixed.
12
+ *
13
+ * Shared by producers (`/bug-fix`, `/do-and-judge`, `/propose-memory`,
14
+ * incident role exit). Appends an intake line under
15
+ * `agents/memory/intake/signals-YYYY-MM.jsonl` — append-only JSONL with
16
+ * `merge=union` (see `road-to-memory-merge-safety.md`). Memory is entirely
17
+ * file-backed (no external backend).
18
+ *
19
+ * Rate limiting:
20
+ * - Per-path, per-type, within a rolling window (default 7 days).
21
+ * - Silent skip on duplicate — the producer's caller should not error,
22
+ * since over-emission is a correctness bug, not a failure mode.
23
+ *
24
+ * Usage:
25
+ * memory_signal --type historical-patterns \
26
+ * --path "app/Http/Controllers/Billing/Checkout.php" \
27
+ * --body "Null deref when currency is missing — add guard."
28
+ */
29
+
30
+ import * as crypto from 'node:crypto';
31
+ import * as fs from 'node:fs';
32
+ import * as path from 'node:path';
33
+ import process from 'node:process';
34
+ import { fileURLToPath, pathToFileURL } from 'node:url';
35
+
36
+ // Mutable so tests can repoint at a tmp tree (monkeypatch parity).
37
+ export let INTAKE_ROOT = path.join('agents', 'memory', 'intake');
38
+ export let SETTINGS_FILE = '.agent-settings.yml';
39
+
40
+ /** Test-only setters mirroring pytest monkeypatch on the module constants. */
41
+ export function _setIntakeRoot(p: string): void {
42
+ INTAKE_ROOT = p;
43
+ }
44
+ export function _setSettingsFile(p: string): void {
45
+ SETTINGS_FILE = p;
46
+ }
47
+
48
+ export const VALID_TYPES: ReadonlySet<string> = new Set([
49
+ 'historical-patterns',
50
+ 'incident-learnings',
51
+ 'ownership',
52
+ 'domain-invariants',
53
+ 'product-rules',
54
+ ]);
55
+ export const RATE_LIMIT_WINDOW_DAYS = 7;
56
+
57
+ function _isPlainObject(v: unknown): v is Record<string, unknown> {
58
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
59
+ }
60
+
61
+ function _isDir(p: string): boolean {
62
+ try {
63
+ return fs.statSync(p).isDirectory();
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ /** Mirror datetime.now(utc).isoformat(timespec="seconds"). */
70
+ export function _now_iso(): string {
71
+ return _pyIsoSeconds(new Date());
72
+ }
73
+
74
+ /** Mirror datetime.now(utc).isoformat(timespec="seconds") for a given Date. */
75
+ function _pyIsoSeconds(d: Date): string {
76
+ const yyyy = d.getUTCFullYear().toString().padStart(4, '0');
77
+ const mo = (d.getUTCMonth() + 1).toString().padStart(2, '0');
78
+ const day = d.getUTCDate().toString().padStart(2, '0');
79
+ const hh = d.getUTCHours().toString().padStart(2, '0');
80
+ const mi = d.getUTCMinutes().toString().padStart(2, '0');
81
+ const ss = d.getUTCSeconds().toString().padStart(2, '0');
82
+ return `${yyyy}-${mo}-${day}T${hh}:${mi}:${ss}+00:00`;
83
+ }
84
+
85
+ /** Short, URL-safe, stable enough for intake ids — secrets.token_hex(6). */
86
+ export function _new_id(): string {
87
+ return `sig-${crypto.randomBytes(6).toString('hex')}`;
88
+ }
89
+
90
+ /** Mirror INTAKE_ROOT / f"signals-{YYYY-MM}.jsonl" (UTC now). */
91
+ export function _monthly_file(): string {
92
+ const d = new Date();
93
+ const ym = `${d.getUTCFullYear().toString().padStart(4, '0')}-${(d.getUTCMonth() + 1)
94
+ .toString()
95
+ .padStart(2, '0')}`;
96
+ return path.join(INTAKE_ROOT, `signals-${ym}.jsonl`);
97
+ }
98
+
99
+ /** Parse an ISO-8601 timestamp the way datetime.fromisoformat does (returns ms epoch or null). */
100
+ function _parseFromIso(ts: string): number | null {
101
+ // Python's datetime.fromisoformat accepts "YYYY-MM-DDTHH:MM:SS[.ffffff][+HH:MM]".
102
+ // A plain "Z" suffix is NOT accepted by 3.10-era fromisoformat, but the
103
+ // emitter writes "+00:00" so this is sufficient. Reject malformed input.
104
+ const m =
105
+ /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?(Z|[+-]\d{2}:\d{2})?$/.exec(ts);
106
+ if (!m) {
107
+ return null;
108
+ }
109
+ const [, y, mo, d, h, mi, s, frac, tz] = m;
110
+ let ms = Date.UTC(
111
+ Number(y),
112
+ Number(mo) - 1,
113
+ Number(d),
114
+ Number(h),
115
+ Number(mi),
116
+ Number(s),
117
+ frac ? Number((frac + '000000').slice(0, 6)) / 1000 : 0,
118
+ );
119
+ if (tz && tz !== 'Z') {
120
+ const sign = tz[0] === '-' ? -1 : 1;
121
+ const oh = Number(tz.slice(1, 3));
122
+ const om = Number(tz.slice(4, 6));
123
+ ms -= sign * (oh * 60 + om) * 60 * 1000;
124
+ }
125
+ // Naive (no tz) timestamps: Python compares them as local-naive against an
126
+ // aware cutoff and would raise TypeError. The emitter always writes an
127
+ // offset, so naive input only appears in hand-crafted fixtures — treat it
128
+ // as UTC for the comparison rather than throwing (the only observable
129
+ // effect is whether the dedupe fires; tests always supply an offset).
130
+ return ms;
131
+ }
132
+
133
+ /** True if an identical (type, path, body) was written within the window. */
134
+ export function _recently_emitted(
135
+ entry_type: string,
136
+ p: string,
137
+ body: string,
138
+ window_days: number = RATE_LIMIT_WINDOW_DAYS,
139
+ ): boolean {
140
+ if (!_isDir(INTAKE_ROOT)) {
141
+ return false;
142
+ }
143
+ const cutoffMs = Date.now() - window_days * 24 * 60 * 60 * 1000;
144
+ for (const jsonl of _globSignals(INTAKE_ROOT)) {
145
+ let text: string;
146
+ try {
147
+ text = fs.readFileSync(jsonl, 'utf-8');
148
+ } catch {
149
+ continue;
150
+ }
151
+ for (const rawLine of text.split('\n')) {
152
+ const line = rawLine.trim();
153
+ if (!line) {
154
+ continue;
155
+ }
156
+ let obj: unknown;
157
+ try {
158
+ obj = JSON.parse(line);
159
+ } catch {
160
+ continue;
161
+ }
162
+ if (!_isPlainObject(obj)) {
163
+ continue;
164
+ }
165
+ if (obj['entry_type'] !== entry_type) {
166
+ continue;
167
+ }
168
+ if (obj['path'] !== p) {
169
+ continue;
170
+ }
171
+ if (obj['body'] !== body) {
172
+ continue;
173
+ }
174
+ const ts = obj['ts'];
175
+ if (typeof ts !== 'string') {
176
+ continue;
177
+ }
178
+ const emitted = _parseFromIso(ts);
179
+ if (emitted === null) {
180
+ continue;
181
+ }
182
+ if (emitted >= cutoffMs) {
183
+ return true;
184
+ }
185
+ }
186
+ }
187
+ return false;
188
+ }
189
+
190
+ /** sorted(INTAKE_ROOT.glob("signals-*.jsonl")) — absolute paths, lexically sorted. */
191
+ function _globSignals(root: string): string[] {
192
+ let names: string[];
193
+ try {
194
+ names = fs.readdirSync(root);
195
+ } catch {
196
+ return [];
197
+ }
198
+ return names
199
+ .filter((n) => n.startsWith('signals-') && n.endsWith('.jsonl'))
200
+ .sort()
201
+ .map((n) => path.join(root, n));
202
+ }
203
+
204
+ export interface SignalRecord {
205
+ [key: string]: unknown;
206
+ id: string;
207
+ ts: string;
208
+ origin: string;
209
+ entry_type: string;
210
+ path: string;
211
+ body: string;
212
+ }
213
+
214
+ /**
215
+ * Append a signal entry. Returns the written record, or null when skipped.
216
+ *
217
+ * On `present` backend, routing to the package is a no-op here today —
218
+ * the package adapter is wired in `road-to-agent-memory-integration.md`
219
+ * Phase 3. For now, the file path is the single source of truth so
220
+ * merge-safety is preserved in every mode.
221
+ */
222
+ export function emit(
223
+ entry_type: string,
224
+ p: string,
225
+ body: string,
226
+ options: {
227
+ extra?: Record<string, unknown> | null;
228
+ origin?: string;
229
+ force?: boolean;
230
+ } = {},
231
+ ): SignalRecord | null {
232
+ const extra = options.extra ?? null;
233
+ const origin = options.origin ?? 'agent';
234
+ const force = options.force ?? false;
235
+ if (!VALID_TYPES.has(entry_type)) {
236
+ throw new Error(`unknown memory type: ${entry_type}`);
237
+ }
238
+ if (!p || !body) {
239
+ throw new Error('path and body are required');
240
+ }
241
+ if (!force && _recently_emitted(entry_type, p, body)) {
242
+ return null;
243
+ }
244
+ const record: SignalRecord = {
245
+ id: _new_id(),
246
+ ts: _now_iso(),
247
+ origin,
248
+ entry_type,
249
+ path: p,
250
+ body,
251
+ };
252
+ if (extra) {
253
+ // Reserved keys stay intact; extras only fill unclaimed slots.
254
+ for (const [k, v] of Object.entries(extra)) {
255
+ if (!(k in record)) {
256
+ record[k] = v;
257
+ }
258
+ }
259
+ }
260
+ fs.mkdirSync(INTAKE_ROOT, { recursive: true });
261
+ const target = _monthly_file();
262
+ fs.appendFileSync(target, `${_jsonDumpsUnicode(record)}\n`, 'utf-8');
263
+ return record;
264
+ }
265
+
266
+ /** Mirror json.dumps(record, ensure_ascii=False) — compact ", "/": " separators, no escapes. */
267
+ function _jsonDumpsUnicode(value: unknown): string {
268
+ return _pyCompactSeparators(JSON.stringify(value));
269
+ }
270
+
271
+ function _pyCompactSeparators(json: string): string {
272
+ let result = '';
273
+ let inString = false;
274
+ let escaped = false;
275
+ for (let i = 0; i < json.length; i += 1) {
276
+ const ch = json[i] as string;
277
+ result += ch;
278
+ if (escaped) {
279
+ escaped = false;
280
+ continue;
281
+ }
282
+ if (ch === '\\') {
283
+ escaped = true;
284
+ continue;
285
+ }
286
+ if (ch === '"') {
287
+ inString = !inString;
288
+ continue;
289
+ }
290
+ if (!inString && (ch === ',' || ch === ':')) {
291
+ result += ' ';
292
+ }
293
+ }
294
+ return result;
295
+ }
296
+
297
+ interface ParsedArgs {
298
+ entry_type: string;
299
+ path: string;
300
+ body: string;
301
+ origin: string;
302
+ extra: string;
303
+ force: boolean;
304
+ }
305
+
306
+ const _PROG = 'memory_signal.py';
307
+ // argparse usage block as rendered at the default 80-column terminal width
308
+ // (COLUMNS unset). The --type choices line is too long to fit, so argparse
309
+ // wraps it onto its own line; reproduce byte-for-byte.
310
+ const _USAGE =
311
+ 'usage: memory_signal.py [-h] --type\n' +
312
+ ' {domain-invariants,historical-patterns,incident-learnings,ownership,product-rules}\n' +
313
+ ' --path PATH --body BODY [--origin ORIGIN]\n' +
314
+ ' [--extra EXTRA] [--force]\n';
315
+
316
+ /** Mirror argparse error: print usage + "<prog>: error: <msg>" to stderr, exit 2. */
317
+ function _usageError(msg: string): never {
318
+ process.stderr.write(_USAGE);
319
+ process.stderr.write(`${_PROG}: error: ${msg}\n`);
320
+ process.exit(2);
321
+ }
322
+
323
+ function _parseArgs(argv: string[]): ParsedArgs {
324
+ const args: Partial<ParsedArgs> = { origin: 'agent', extra: '', force: false };
325
+ const seen = new Set<string>();
326
+ const choices = [...VALID_TYPES].sort();
327
+ for (let i = 0; i < argv.length; i += 1) {
328
+ const a = argv[i] as string;
329
+ const takeValue = (flag: string): string => {
330
+ const v = argv[++i];
331
+ if (v === undefined) {
332
+ _usageError(`argument ${flag}: expected one argument`);
333
+ }
334
+ return v as string;
335
+ };
336
+ if (a === '--type') {
337
+ const v = takeValue('--type');
338
+ if (!choices.includes(v)) {
339
+ _usageError(
340
+ `argument --type: invalid choice: '${v}' (choose from ${choices
341
+ .map((c) => `'${c}'`)
342
+ .join(', ')})`,
343
+ );
344
+ }
345
+ args.entry_type = v;
346
+ seen.add('--type');
347
+ } else if (a === '--path') {
348
+ args.path = takeValue('--path');
349
+ seen.add('--path');
350
+ } else if (a === '--body') {
351
+ args.body = takeValue('--body');
352
+ seen.add('--body');
353
+ } else if (a === '--origin') {
354
+ args.origin = takeValue('--origin');
355
+ } else if (a === '--extra') {
356
+ args.extra = takeValue('--extra');
357
+ } else if (a === '--force') {
358
+ args.force = true;
359
+ } else if (a === '-h' || a === '--help') {
360
+ // --help is not a parity contract (per ADR-200); emit the usage block.
361
+ process.stdout.write(_USAGE);
362
+ process.exit(0);
363
+ } else {
364
+ _usageError(`unrecognized arguments: ${a}`);
365
+ }
366
+ }
367
+ const missing: string[] = [];
368
+ if (!seen.has('--type')) {
369
+ missing.push('--type');
370
+ }
371
+ if (!seen.has('--path')) {
372
+ missing.push('--path');
373
+ }
374
+ if (!seen.has('--body')) {
375
+ missing.push('--body');
376
+ }
377
+ if (missing.length > 0) {
378
+ _usageError(`the following arguments are required: ${missing.join(', ')}`);
379
+ }
380
+ return args as ParsedArgs;
381
+ }
382
+
383
+ export function main(): number {
384
+ const args = _parseArgs(process.argv.slice(2));
385
+ let extra: Record<string, unknown> = {};
386
+ if (args.extra) {
387
+ let parsed: unknown;
388
+ try {
389
+ parsed = JSON.parse(args.extra);
390
+ } catch {
391
+ process.stderr.write('error: --extra must be a JSON object\n');
392
+ return 2;
393
+ }
394
+ if (!_isPlainObject(parsed)) {
395
+ process.stderr.write('error: --extra must be a JSON object\n');
396
+ return 2;
397
+ }
398
+ extra = parsed;
399
+ }
400
+ const rec = emit(args.entry_type, args.path, args.body, {
401
+ extra,
402
+ origin: args.origin,
403
+ force: args.force,
404
+ });
405
+ if (rec === null) {
406
+ process.stdout.write(
407
+ ` ℹ️ skipped (already emitted within ` +
408
+ `${RATE_LIMIT_WINDOW_DAYS}d): ${args.entry_type} @ ${args.path}\n`,
409
+ );
410
+ return 0;
411
+ }
412
+ process.stdout.write(
413
+ ` ✅ signal emitted: id=${rec.id} type=${rec.entry_type} ` + `path=${rec.path}\n`,
414
+ );
415
+ return 0;
416
+ }
417
+
418
+ function _isCliEntry(): boolean {
419
+ if (process.argv[1] === undefined) {
420
+ return false;
421
+ }
422
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
423
+ if (import.meta.url === argvUrl) {
424
+ return true;
425
+ }
426
+ // A symlinked invocation (e.g. via an installed `.augment/` projection,
427
+ // or macOS /var → /private/var temp dirs) makes the raw URLs differ:
428
+ // import.meta.url is the resolved real path while argv[1] keeps the
429
+ // symlink path. Compare realpaths so the entry guard still fires
430
+ // (without this the CLI silently no-ops when run through a symlink).
431
+ try {
432
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
433
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
434
+ return here === argv;
435
+ } catch {
436
+ return false;
437
+ }
438
+ }
439
+
440
+ const _isMain =
441
+ _isCliEntry();
442
+ if (_isMain) {
443
+ process.exit(main());
444
+ }
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * File-backed memory status (no external backend).
4
+ *
5
+ * TypeScript twin of `src/agent-src/templates/scripts/memory_status.py`
6
+ * (ADR-200, consumer-template memory; byte-identical to the dev-side
7
+ * `src/scripts/memory_status.py`). The public API and CLI contract mirror
8
+ * the Python original EXACTLY — same exported names (snake_case kept
9
+ * deliberately), same exit codes, stdout/stderr split, byte-identical
10
+ * messages and JSON shape. No behaviour changes.
11
+ *
12
+ * The optional `@event4u/agent-memory` package was removed; retrieval
13
+ * (`scripts/memory_lookup.py`) and signal-writing (`scripts/memory_signal.py`)
14
+ * are entirely file-backed now. `status()` / `health()` report the file
15
+ * backend so the v1 retrieval-contract health envelope stays stable for
16
+ * consumers (e.g. the MCP `memory_status` tool).
17
+ *
18
+ * Usage:
19
+ * memory_status # human-readable line
20
+ * memory_status --format json # stable JSON
21
+ * memory_status --health # v1 health envelope
22
+ */
23
+
24
+ import process from 'node:process';
25
+ import * as fs from 'node:fs';
26
+ import * as path from 'node:path';
27
+ import { fileURLToPath, pathToFileURL } from 'node:url';
28
+
29
+ // Retrieval contract version served by the file-backed backend.
30
+ // Source of truth: internal/schemas/retrieval-v1.schema.json.
31
+ export const CONTRACT_VERSION = 1;
32
+ const _FILE_BACKEND_VERSION = '0.0.0-file';
33
+ const _FILE_BACKEND_FEATURES = ['file-fallback'] as const;
34
+
35
+ export interface Result {
36
+ status: string;
37
+ backend: string;
38
+ reason: string;
39
+ elapsed_ms: number;
40
+ }
41
+
42
+ /** Return the (constant) file-backend status. Never raises. */
43
+ export function status(_refresh = false): Result {
44
+ return {
45
+ status: 'file', // always "file" — no external backend
46
+ backend: 'file',
47
+ reason: 'file-backed memory (no external backend)',
48
+ elapsed_ms: 0,
49
+ };
50
+ }
51
+
52
+ /** Return a v1 retrieval-contract health envelope (file backend). */
53
+ export function health(_refresh = false): Record<string, unknown> {
54
+ return {
55
+ contract_version: CONTRACT_VERSION,
56
+ status: 'ok',
57
+ backend_version: _FILE_BACKEND_VERSION,
58
+ features: [..._FILE_BACKEND_FEATURES],
59
+ };
60
+ }
61
+
62
+ /** Mirror json.dumps(obj) — compact ", " / ": " separators, ensure_ascii. */
63
+ function _pyJsonDumps(value: unknown): string {
64
+ return _escapeNonAscii(_pyCompactSeparators(JSON.stringify(value)));
65
+ }
66
+
67
+ function _pyCompactSeparators(json: string): string {
68
+ let result = '';
69
+ let inString = false;
70
+ let escaped = false;
71
+ for (let i = 0; i < json.length; i += 1) {
72
+ const ch = json[i] as string;
73
+ result += ch;
74
+ if (escaped) {
75
+ escaped = false;
76
+ continue;
77
+ }
78
+ if (ch === '\\') {
79
+ escaped = true;
80
+ continue;
81
+ }
82
+ if (ch === '"') {
83
+ inString = !inString;
84
+ continue;
85
+ }
86
+ if (!inString && (ch === ',' || ch === ':')) {
87
+ result += ' ';
88
+ }
89
+ }
90
+ return result;
91
+ }
92
+
93
+ function _escapeNonAscii(s: string): string {
94
+ let out = '';
95
+ for (const ch of s) {
96
+ const code = ch.codePointAt(0) ?? 0;
97
+ if (code > 0x7f) {
98
+ for (let i = 0; i < ch.length; i += 1) {
99
+ out += `\\u${ch.charCodeAt(i).toString(16).padStart(4, '0')}`;
100
+ }
101
+ } else {
102
+ out += ch;
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+
108
+ interface ParsedArgs {
109
+ format: string;
110
+ refresh: boolean;
111
+ health: boolean;
112
+ }
113
+
114
+ const _PROG = 'memory_status.py';
115
+ const _USAGE = 'usage: memory_status.py [-h] [--format {text,json}] [--refresh] [--health]\n';
116
+
117
+ class _ExitError extends Error {
118
+ constructor(readonly code: number) {
119
+ super(`exit ${code}`);
120
+ }
121
+ }
122
+
123
+ /** Mirror argparse error: print usage + "<prog>: error: <msg>" to stderr, exit 2. */
124
+ function _usageError(msg: string): never {
125
+ process.stderr.write(_USAGE);
126
+ process.stderr.write(`${_PROG}: error: ${msg}\n`);
127
+ throw new _ExitError(2);
128
+ }
129
+
130
+ function _parseArgs(argv: string[]): ParsedArgs {
131
+ const args: ParsedArgs = { format: 'text', refresh: false, health: false };
132
+ const checkFormat = (v: string): string => {
133
+ if (v !== 'text' && v !== 'json') {
134
+ _usageError(`argument --format: invalid choice: '${v}' (choose from 'text', 'json')`);
135
+ }
136
+ return v;
137
+ };
138
+ for (let i = 0; i < argv.length; i += 1) {
139
+ const a = argv[i] as string;
140
+ const takeValue = (flag: string): string => {
141
+ const v = argv[++i];
142
+ if (v === undefined) {
143
+ _usageError(`argument ${flag}: expected one argument`);
144
+ }
145
+ return v as string;
146
+ };
147
+ if (a === '--format') {
148
+ args.format = checkFormat(takeValue('--format'));
149
+ } else if (a.startsWith('--format=')) {
150
+ args.format = checkFormat(a.slice('--format='.length));
151
+ } else if (a === '--refresh') {
152
+ args.refresh = true;
153
+ } else if (a === '--health') {
154
+ args.health = true;
155
+ } else if (a === '-h' || a === '--help') {
156
+ // --help is not a parity contract (per ADR-200); emit the usage block.
157
+ process.stdout.write(_USAGE);
158
+ throw new _ExitError(0);
159
+ } else {
160
+ _usageError(`unrecognized arguments: ${a}`);
161
+ }
162
+ }
163
+ return args;
164
+ }
165
+
166
+ export function main(): number {
167
+ const args = _parseArgs(process.argv.slice(2));
168
+ if (args.health) {
169
+ process.stdout.write(`${_pyJsonDumps(health())}\n`);
170
+ return 0;
171
+ }
172
+ const r = status();
173
+ if (args.format === 'json') {
174
+ process.stdout.write(`${_pyJsonDumps(r)}\n`);
175
+ } else {
176
+ process.stdout.write(` ℹ️ backend=${r.backend} status=${r.status} reason=${r.reason}\n`);
177
+ }
178
+ return 0;
179
+ }
180
+
181
+ function _isCliEntry(): boolean {
182
+ if (process.argv[1] === undefined) {
183
+ return false;
184
+ }
185
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
186
+ if (import.meta.url === argvUrl) {
187
+ return true;
188
+ }
189
+ // A symlinked invocation (e.g. via an installed `.augment/` projection,
190
+ // or macOS /var → /private/var temp dirs) makes the raw URLs differ:
191
+ // import.meta.url is the resolved real path while argv[1] keeps the
192
+ // symlink path. Compare realpaths so the entry guard still fires
193
+ // (without this the CLI silently no-ops when run through a symlink).
194
+ try {
195
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
196
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
197
+ return here === argv;
198
+ } catch {
199
+ return false;
200
+ }
201
+ }
202
+
203
+ const _isMain =
204
+ _isCliEntry();
205
+ if (_isMain) {
206
+ try {
207
+ process.exitCode = main();
208
+ } catch (e) {
209
+ if (e instanceof _ExitError) {
210
+ process.exitCode = e.code;
211
+ } else {
212
+ throw e;
213
+ }
214
+ }
215
+ }