@event4u/agent-config 8.6.0 → 8.8.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +81 -0
- package/CONTRIBUTING.md +25 -0
- package/README.md +7 -4
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +1 -1
- package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +28 -16
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +15 -0
- package/dist/agent-src/rules/active-remediation.md +92 -0
- package/dist/agent-src/rules/broken-access-control.md +106 -0
- package/dist/agent-src/rules/communication-through-line.md +42 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +8 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +18 -1
- package/dist/agent-src/rules/prefer-enums-over-literals.md +65 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +82 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +105 -0
- package/dist/agent-src/skills/ai-code-blindspots/evals/triggers.json +52 -0
- package/dist/agent-src/skills/authz-review/SKILL.md +21 -4
- package/dist/agent-src/skills/dependency-upgrade/SKILL.md +20 -0
- package/dist/agent-src/skills/frontend-render-security/SKILL.md +104 -0
- package/dist/agent-src/skills/frontend-render-security/evals/triggers.json +52 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +7 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +4 -0
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +100 -0
- package/dist/agent-src/skills/supply-chain-intake/evals/triggers.json +52 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +43 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +24 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +196 -16
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +13 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +12 -4
- package/dist/install/emit_host_rules_cli.js +185 -0
- package/dist/install/emit_host_rules_cli.js.map +1 -0
- package/dist/install/install.mjs +11 -2
- package/dist/install/plan.js +4 -1
- package/dist/install/plan.js.map +1 -1
- package/dist/install/rule_scope.js +109 -0
- package/dist/install/rule_scope.js.map +1 -0
- package/dist/install/rule_scope_cli.js +77 -0
- package/dist/install/rule_scope_cli.js.map +1 -0
- package/dist/install/wizard-plan.js +14 -1
- package/dist/install/wizard-plan.js.map +1 -1
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +963 -0
- package/dist/scripts/_lib/agent_settings.js.map +1 -0
- package/dist/scripts/_lib/agent_src.js +607 -0
- package/dist/scripts/_lib/agent_src.js.map +1 -0
- package/dist/scripts/_lib/claude_settings_hooks.js +209 -0
- package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -0
- package/dist/scripts/_lib/model_tier.js +64 -0
- package/dist/scripts/_lib/model_tier.js.map +1 -0
- package/dist/scripts/_lib/script_output.js +140 -0
- package/dist/scripts/_lib/script_output.js.map +1 -0
- package/dist/scripts/_lib/user_global_paths.js +297 -0
- package/dist/scripts/_lib/user_global_paths.js.map +1 -0
- package/dist/scripts/condense.js +2181 -0
- package/dist/scripts/condense.js.map +1 -0
- package/dist/server/routes/install.js +18 -2
- package/dist/server/routes/install.js.map +1 -1
- package/dist/server/schemas/settings.js +4 -1
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +30 -2
- package/docs/architecture.md +2 -2
- package/docs/catalog.md +14 -0
- package/docs/contracts/mcp-tool-inventory.md +31 -30
- package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +6 -0
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +2 -2
- package/docs/decisions/ADR-117-subagents-auto-default-on.md +88 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/experiences/content_creator.md +2 -0
- package/docs/experiences/finance.md +2 -0
- package/docs/experiences/founder.md +2 -0
- package/docs/experiences/ops.md +2 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/memory-access.md +12 -0
- package/docs/proof.md +45 -3
- package/docs/release-runbook.md +138 -0
- package/docs/second-brain-scope.md +73 -0
- package/docs/succession.md +80 -0
- package/package.json +2 -2
- package/src/config/agent-settings.template.yml +29 -8
- package/src/install/emit_host_rules_cli.ts +190 -0
- package/src/install/plan.ts +11 -1
- package/src/install/rule_scope.ts +120 -0
- package/src/install/rule_scope_cli.ts +79 -0
- package/src/install/wizard-plan.ts +22 -1
- package/src/scripts/_lib/orchestration_gate.ts +12 -8
- package/src/scripts/_lib/orchestration_savings.ts +127 -0
- package/src/scripts/build_proof.ts +38 -0
- package/src/scripts/chat_history.ts +79 -7
- package/src/scripts/check_trigger_eval_presence.ts +171 -0
- package/src/scripts/domain_soundness_status.ts +236 -0
- package/src/scripts/generate_index.ts +14 -0
- package/src/scripts/hot_context_hook.ts +47 -2
- package/src/scripts/install.sh +118 -4
- package/src/scripts/lint_behavioural_eval_freshness.ts +140 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +22 -3
- package/src/scripts/mcp_server/tools.ts +159 -1
- package/src/scripts/memory_lookup.ts +184 -6
- package/src/scripts/memory_replay.ts +285 -0
- package/src/scripts/orchestration_savings_report.ts +100 -0
- package/src/scripts/schemas/domain-truth.schema.json +94 -0
- package/src/scripts/schemas/evals.schema.json +113 -0
- package/src/scripts/second_brain_score.ts +186 -0
- package/src/scripts/session_memory_index.ts +94 -0
- package/src/scripts/skill_eval_coverage.ts +318 -0
- package/src/scripts/trigger_eval_grandfather.json +226 -0
- package/src/server/schemas/settings.ts +11 -2
|
@@ -50,14 +50,18 @@
|
|
|
50
50
|
"last": {"type": "integer", "description": "Return only the most recent N entries, after other filters apply.", "minimum": 1},
|
|
51
51
|
"session": {"type": "string", "description": "Filter to a single 16-char session id."},
|
|
52
52
|
"entry_type": {"type": "string", "description": "Filter by the `t` tag (e.g. note, decision, phase)."},
|
|
53
|
-
"path": {"type": "string", "description": "Optional history-file path override; defaults to the standard chat-history location under the project root."}
|
|
53
|
+
"path": {"type": "string", "description": "Optional history-file path override; defaults to the standard chat-history location under the project root."},
|
|
54
|
+
"around": {"type": "integer", "minimum": 0, "description": "Timeline anchor: return the entries around this ref (from a detail:\"index\" row) instead of the filtered list — depth_before/depth_after neighbours plus the anchor. Refs are within-file ordinals; rotation invalidates them."},
|
|
55
|
+
"depth_before": {"type": "integer", "minimum": 0, "default": 3, "description": "Neighbours before the anchor (with `around`). Defaults to 3."},
|
|
56
|
+
"depth_after": {"type": "integer", "minimum": 0, "default": 3, "description": "Neighbours after the anchor (with `around`). Defaults to 3."},
|
|
57
|
+
"detail": {"type": "string", "enum": ["index", "full"], "default": "full", "description": "'index' returns compact rows (ref, t tag, ~100-char preview, tokens_estimate) instead of full entries — scan first, then re-read the refs you need via `around` or a narrowed filter. 'full' (default) returns complete entries."}
|
|
54
58
|
},
|
|
55
59
|
"additionalProperties": false
|
|
56
60
|
}
|
|
57
61
|
},
|
|
58
62
|
{
|
|
59
63
|
"name": "memory_lookup",
|
|
60
|
-
"description": "Retrieve engineering-memory entries for one or more memory types, optionally narrowed to specific anchor paths. Use before editing a security-sensitive or historically buggy file to surface prior incidents, ownership, and patterns tied to it. Reads `agents/memory/<type>/*.yml` plus the `agents/memory/intake/*.jsonl` signal log. Read-only. Returns the v1 retrieval envelope: a `status` field plus per-type `slices` carrying the matched entries.",
|
|
64
|
+
"description": "Retrieve engineering-memory entries for one or more memory types, optionally narrowed to specific anchor paths. Use before editing a security-sensitive or historically buggy file to surface prior incidents, ownership, and patterns tied to it. WORKFLOW: call with detail:\"index\" FIRST — each row carries id, title and tokens_estimate (the cost of fetching it) — then fetch full bodies via memory_get ONLY for the ids you will actually use, batching multiple ids into one call. Reads `agents/memory/<type>/*.yml` plus the `agents/memory/intake/*.jsonl` signal log. Read-only. Returns the v1 retrieval envelope: a `status` field plus per-type `slices` carrying the matched entries.",
|
|
61
65
|
"side_effect": "ro",
|
|
62
66
|
"implemented_on": ["stdio"],
|
|
63
67
|
"input_schema": {
|
|
@@ -65,12 +69,27 @@
|
|
|
65
69
|
"properties": {
|
|
66
70
|
"types": {"type": "array", "items": {"type": "string"}, "minItems": 1, "description": "Memory types to scan, e.g. `historical-patterns`, `incident-learnings`, `ownership`. At least one required."},
|
|
67
71
|
"keys": {"type": "array", "items": {"type": "string"}, "description": "Optional anchor paths or globs to match entries against (e.g. a file you are about to edit)."},
|
|
68
|
-
"limit": {"type": "integer", "minimum": 1, "default": 5, "description": "Maximum entries to return per type. Defaults to 5."}
|
|
72
|
+
"limit": {"type": "integer", "minimum": 1, "default": 5, "description": "Maximum entries to return per type. Defaults to 5."},
|
|
73
|
+
"detail": {"type": "string", "enum": ["index", "full"], "default": "full", "description": "'index' returns compact priced rows (id, title, tokens_estimate) instead of full bodies — call this first, then memory_get the ids you need. 'full' (default) returns complete entries."}
|
|
69
74
|
},
|
|
70
75
|
"required": ["types"],
|
|
71
76
|
"additionalProperties": false
|
|
72
77
|
}
|
|
73
78
|
},
|
|
79
|
+
{
|
|
80
|
+
"name": "memory_get",
|
|
81
|
+
"description": "Batch-fetch FULL memory entries by id — the second half of the index-first retrieval workflow. Call memory_lookup with detail:\"index\" first, pick the ids whose title/tokens_estimate justify the fetch, then fetch them here in ONE batched call. Unknown ids are reported per-id (ids[<id>]=\"unknown\"), never failing the batch. Read-only.",
|
|
82
|
+
"side_effect": "ro",
|
|
83
|
+
"implemented_on": ["stdio"],
|
|
84
|
+
"input_schema": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"properties": {
|
|
87
|
+
"ids": {"type": "array", "items": {"type": "string"}, "minItems": 1, "description": "Entry ids to fetch (from a detail:\"index\" lookup)."}
|
|
88
|
+
},
|
|
89
|
+
"required": ["ids"],
|
|
90
|
+
"additionalProperties": false
|
|
91
|
+
}
|
|
92
|
+
},
|
|
74
93
|
{
|
|
75
94
|
"name": "memory_signal",
|
|
76
95
|
"description": "Record an engineering-memory signal — a short, anchored observation such as a recurring bug pattern or an ownership note — to the monthly intake log `agents/memory/intake/signals-YYYY-MM.jsonl`. Use to capture a learning tied to a specific file so future `memory_lookup` calls surface it. Appends to the filesystem and is rate-limited per `(type, path)` within a rolling window. Returns the recorded signal.",
|
|
@@ -38,7 +38,10 @@ import {
|
|
|
38
38
|
SCHEMA_VERSION as _CH_SCHEMA_VERSION,
|
|
39
39
|
append as _chAppendImpl,
|
|
40
40
|
init as _chInitImpl,
|
|
41
|
+
history_index_row as _chHistoryIndexRow,
|
|
41
42
|
read_entries as _chReadEntriesImpl,
|
|
43
|
+
read_entries_with_refs as _chReadEntriesWithRefs,
|
|
44
|
+
slice_around as _chSliceAround,
|
|
42
45
|
read_header as _chReadHeaderImpl,
|
|
43
46
|
} from '../chat_history.js';
|
|
44
47
|
import {
|
|
@@ -364,10 +367,63 @@ async function _chatHistoryReadHandler(
|
|
|
364
367
|
throw new Error("'entry_type' must be a string when provided");
|
|
365
368
|
}
|
|
366
369
|
|
|
370
|
+
const around = args.around;
|
|
371
|
+
if (
|
|
372
|
+
around !== undefined &&
|
|
373
|
+
around !== null &&
|
|
374
|
+
(typeof around !== 'number' || !Number.isInteger(around) || around < 0)
|
|
375
|
+
) {
|
|
376
|
+
throw new Error("'around' must be a non-negative integer when provided");
|
|
377
|
+
}
|
|
378
|
+
const depthBefore = args.depth_before ?? 3;
|
|
379
|
+
const depthAfter = args.depth_after ?? 3;
|
|
380
|
+
for (const [name, v] of [
|
|
381
|
+
['depth_before', depthBefore],
|
|
382
|
+
['depth_after', depthAfter],
|
|
383
|
+
] as const) {
|
|
384
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 0) {
|
|
385
|
+
throw new Error(`'${name}' must be a non-negative integer`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const detail = args.detail ?? 'full';
|
|
389
|
+
if (detail !== 'full' && detail !== 'index') {
|
|
390
|
+
throw new Error("'detail' must be 'index' or 'full'");
|
|
391
|
+
}
|
|
392
|
+
|
|
367
393
|
if (!fs.existsSync(target)) {
|
|
368
394
|
return { path: target, entries: [], count: 0 };
|
|
369
395
|
}
|
|
370
396
|
|
|
397
|
+
// Timeline anchor path (road-to-memory-retrieval-economy Phase 3):
|
|
398
|
+
// refs index the FULL chronological list, so `around` ignores the
|
|
399
|
+
// session/entry_type/last filters by contract.
|
|
400
|
+
if (around !== undefined && around !== null) {
|
|
401
|
+
const pairs = _chSliceAround(
|
|
402
|
+
target,
|
|
403
|
+
around as number,
|
|
404
|
+
depthBefore as number,
|
|
405
|
+
depthAfter as number,
|
|
406
|
+
);
|
|
407
|
+
const entries =
|
|
408
|
+
detail === 'index'
|
|
409
|
+
? pairs.map((p) => _chHistoryIndexRow(p))
|
|
410
|
+
: pairs.map((p) => ({ ref: p.ref, ...p.entry }));
|
|
411
|
+
return { path: target, entries, count: entries.length, anchor: around };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (detail === 'index') {
|
|
415
|
+
let pairs = _chReadEntriesWithRefs({
|
|
416
|
+
path: target,
|
|
417
|
+
last: (last ?? null) as number | null,
|
|
418
|
+
session: (session ?? null) as string | null,
|
|
419
|
+
});
|
|
420
|
+
if (entryType) {
|
|
421
|
+
pairs = pairs.filter((p) => p.entry.t === entryType);
|
|
422
|
+
}
|
|
423
|
+
const entries = pairs.map((p) => _chHistoryIndexRow(p));
|
|
424
|
+
return { path: target, entries, count: entries.length };
|
|
425
|
+
}
|
|
426
|
+
|
|
371
427
|
let entries = _chReadEntries(
|
|
372
428
|
target,
|
|
373
429
|
(last ?? null) as number | null,
|
|
@@ -416,6 +472,11 @@ async function _memoryLookupHandler(
|
|
|
416
472
|
// Mirror the Python `os.chdir(consumer_root)` scoping: memory_lookup
|
|
417
473
|
// resolves `agents/memory` relative to CWD, so the consumer root is
|
|
418
474
|
// applied via a chdir window that is always restored.
|
|
475
|
+
const detailRaw = args.detail ?? 'full';
|
|
476
|
+
if (detailRaw !== 'full' && detailRaw !== 'index') {
|
|
477
|
+
throw new Error("'detail' must be 'index' or 'full'");
|
|
478
|
+
}
|
|
479
|
+
|
|
419
480
|
const prevCwd = process.cwd();
|
|
420
481
|
let envelope: Record<string, unknown>;
|
|
421
482
|
try {
|
|
@@ -424,6 +485,7 @@ async function _memoryLookupHandler(
|
|
|
424
485
|
[...(types as string[])],
|
|
425
486
|
[...(keys as string[])],
|
|
426
487
|
limitRaw,
|
|
488
|
+
{ detail: detailRaw },
|
|
427
489
|
);
|
|
428
490
|
} finally {
|
|
429
491
|
process.chdir(prevCwd);
|
|
@@ -431,6 +493,30 @@ async function _memoryLookupHandler(
|
|
|
431
493
|
return envelope;
|
|
432
494
|
}
|
|
433
495
|
|
|
496
|
+
/**
|
|
497
|
+
* Batch full-entry fetch by ID (road-to-memory-retrieval-economy Phase 1).
|
|
498
|
+
* The second half of the index→fetch economy: `memory_lookup` with
|
|
499
|
+
* `detail: 'index'` returns priced rows; this fetches ONLY the bodies the
|
|
500
|
+
* caller actually needs. Unknown IDs degrade per-ID, never the batch.
|
|
501
|
+
*/
|
|
502
|
+
async function _memoryGetHandler(
|
|
503
|
+
args: Record<string, unknown>,
|
|
504
|
+
consumerRoot: string,
|
|
505
|
+
): Promise<Record<string, unknown>> {
|
|
506
|
+
const memoryLookup = await import('../memory_lookup.js');
|
|
507
|
+
const ids = args.ids;
|
|
508
|
+
if (!Array.isArray(ids) || ids.length === 0 || !ids.every((i) => typeof i === 'string')) {
|
|
509
|
+
throw new Error("'ids' must be a non-empty list of strings");
|
|
510
|
+
}
|
|
511
|
+
const prevCwd = process.cwd();
|
|
512
|
+
try {
|
|
513
|
+
process.chdir(consumerRoot);
|
|
514
|
+
return memoryLookup.memory_get_v1([...(ids as string[])]);
|
|
515
|
+
} finally {
|
|
516
|
+
process.chdir(prevCwd);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
434
520
|
/** Phase 3 L2 — surface `scripts/memory_status.status()` as JSON. */
|
|
435
521
|
async function _memoryStatusHandler(
|
|
436
522
|
_args: Record<string, unknown>,
|
|
@@ -1225,6 +1311,40 @@ export const ALLOWLIST: Record<string, BuiltinTool> = {
|
|
|
1225
1311
|
'the standard chat-history location under the ' +
|
|
1226
1312
|
'project root.',
|
|
1227
1313
|
},
|
|
1314
|
+
around: {
|
|
1315
|
+
type: 'integer',
|
|
1316
|
+
minimum: 0,
|
|
1317
|
+
description:
|
|
1318
|
+
'Timeline anchor: return the entries around this ' +
|
|
1319
|
+
'ref (from a detail:"index" row) instead of the ' +
|
|
1320
|
+
'filtered list — depth_before/depth_after ' +
|
|
1321
|
+
'neighbours plus the anchor. Refs are within-file ' +
|
|
1322
|
+
'ordinals; rotation invalidates them.',
|
|
1323
|
+
},
|
|
1324
|
+
depth_before: {
|
|
1325
|
+
type: 'integer',
|
|
1326
|
+
minimum: 0,
|
|
1327
|
+
default: 3,
|
|
1328
|
+
description: 'Neighbours before the anchor (with `around`). Defaults to 3.',
|
|
1329
|
+
},
|
|
1330
|
+
depth_after: {
|
|
1331
|
+
type: 'integer',
|
|
1332
|
+
minimum: 0,
|
|
1333
|
+
default: 3,
|
|
1334
|
+
description: 'Neighbours after the anchor (with `around`). Defaults to 3.',
|
|
1335
|
+
},
|
|
1336
|
+
detail: {
|
|
1337
|
+
type: 'string',
|
|
1338
|
+
enum: ['index', 'full'],
|
|
1339
|
+
default: 'full',
|
|
1340
|
+
description:
|
|
1341
|
+
"'index' returns compact rows (ref, t tag, ~100-char " +
|
|
1342
|
+
'preview, tokens_estimate) instead of full entries — ' +
|
|
1343
|
+
'scan first, then re-read the refs you need via ' +
|
|
1344
|
+
'`around` or a narrowed filter. History entries are ' +
|
|
1345
|
+
"large, so index mode pays off here. 'full' " +
|
|
1346
|
+
'(default) returns complete entries.',
|
|
1347
|
+
},
|
|
1228
1348
|
},
|
|
1229
1349
|
additionalProperties: false,
|
|
1230
1350
|
},
|
|
@@ -1237,7 +1357,11 @@ export const ALLOWLIST: Record<string, BuiltinTool> = {
|
|
|
1237
1357
|
'types, optionally narrowed to specific anchor paths. Use ' +
|
|
1238
1358
|
'before editing a security-sensitive or historically buggy ' +
|
|
1239
1359
|
'file to surface prior incidents, ownership, and patterns ' +
|
|
1240
|
-
'tied to it.
|
|
1360
|
+
'tied to it. WORKFLOW: call with detail:"index" FIRST — each ' +
|
|
1361
|
+
'row carries id, title and tokens_estimate (the cost of ' +
|
|
1362
|
+
'fetching it) — then fetch full bodies via memory_get ONLY ' +
|
|
1363
|
+
'for the ids you will actually use, batching multiple ids ' +
|
|
1364
|
+
'into one call. Reads `agents/memory/<type>/*.yml` plus the ' +
|
|
1241
1365
|
'`agents/memory/intake/*.jsonl` signal log. Read-only. ' +
|
|
1242
1366
|
'Returns the v1 retrieval envelope: a `status` field plus ' +
|
|
1243
1367
|
'per-type `slices` carrying the matched entries.',
|
|
@@ -1266,12 +1390,46 @@ export const ALLOWLIST: Record<string, BuiltinTool> = {
|
|
|
1266
1390
|
default: 5,
|
|
1267
1391
|
description: 'Maximum entries to return per type. Defaults to 5.',
|
|
1268
1392
|
},
|
|
1393
|
+
detail: {
|
|
1394
|
+
type: 'string',
|
|
1395
|
+
enum: ['index', 'full'],
|
|
1396
|
+
default: 'full',
|
|
1397
|
+
description:
|
|
1398
|
+
"'index' returns compact priced rows (id, title, " +
|
|
1399
|
+
"tokens_estimate) instead of full bodies — call " +
|
|
1400
|
+
"this first, then memory_get the ids you need. " +
|
|
1401
|
+
"'full' (default) returns complete entries.",
|
|
1402
|
+
},
|
|
1269
1403
|
},
|
|
1270
1404
|
required: ['types'],
|
|
1271
1405
|
additionalProperties: false,
|
|
1272
1406
|
},
|
|
1273
1407
|
handler: _memoryLookupHandler,
|
|
1274
1408
|
},
|
|
1409
|
+
memory_get: {
|
|
1410
|
+
name: 'memory_get',
|
|
1411
|
+
description:
|
|
1412
|
+
'Batch-fetch FULL memory entries by id — the second half of ' +
|
|
1413
|
+
'the index-first retrieval workflow. Call memory_lookup with ' +
|
|
1414
|
+
'detail:"index" first, pick the ids whose title/tokens_estimate ' +
|
|
1415
|
+
'justify the fetch, then fetch them here in ONE batched call. ' +
|
|
1416
|
+
'Unknown ids are reported per-id (ids[<id>]="unknown"), never ' +
|
|
1417
|
+
'failing the batch. Read-only.',
|
|
1418
|
+
input_schema: {
|
|
1419
|
+
type: 'object',
|
|
1420
|
+
properties: {
|
|
1421
|
+
ids: {
|
|
1422
|
+
type: 'array',
|
|
1423
|
+
items: { type: 'string' },
|
|
1424
|
+
minItems: 1,
|
|
1425
|
+
description: 'Entry ids to fetch (from a detail:"index" lookup).',
|
|
1426
|
+
},
|
|
1427
|
+
},
|
|
1428
|
+
required: ['ids'],
|
|
1429
|
+
additionalProperties: false,
|
|
1430
|
+
},
|
|
1431
|
+
handler: _memoryGetHandler,
|
|
1432
|
+
},
|
|
1275
1433
|
memory_status: {
|
|
1276
1434
|
name: 'memory_status',
|
|
1277
1435
|
description:
|
|
@@ -32,6 +32,7 @@ import * as fs from 'node:fs';
|
|
|
32
32
|
import * as path from 'node:path';
|
|
33
33
|
import process from 'node:process';
|
|
34
34
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
|
+
import { createRequire } from 'node:module';
|
|
35
36
|
import YAML, { parseDocument } from 'yaml';
|
|
36
37
|
|
|
37
38
|
|
|
@@ -578,6 +579,77 @@ function _isStale(entry: Record<string, unknown>, today: Date): boolean {
|
|
|
578
579
|
* active, non-stale entries flow through `_iter_curated_entries` in the
|
|
579
580
|
* normal path.
|
|
580
581
|
*/
|
|
582
|
+
/**
|
|
583
|
+
* Deterministic entry ID (road-to-memory-retrieval-economy Phase 1).
|
|
584
|
+
*
|
|
585
|
+
* Precedence: the entry's own `id` field when present and non-empty;
|
|
586
|
+
* otherwise `<type>:<file-basename>:<ordinal>` where `ordinal` is the
|
|
587
|
+
* entry's 0-based position within its source file (iteration order is
|
|
588
|
+
* sorted and therefore stable across re-runs on an unchanged tree).
|
|
589
|
+
* `memory_get_v1` derives ids with the SAME function, so lookup and batch
|
|
590
|
+
* fetch can never disagree.
|
|
591
|
+
*/
|
|
592
|
+
export function _entry_id(
|
|
593
|
+
mtype: string,
|
|
594
|
+
file: string,
|
|
595
|
+
entry: Record<string, unknown>,
|
|
596
|
+
ordinals: Map<string, number>,
|
|
597
|
+
): string {
|
|
598
|
+
const ordinal = ordinals.get(file) ?? 0;
|
|
599
|
+
ordinals.set(file, ordinal + 1);
|
|
600
|
+
const own = entry['id'];
|
|
601
|
+
if (typeof own === 'string' && own.trim() !== '') {
|
|
602
|
+
return own;
|
|
603
|
+
}
|
|
604
|
+
if (own !== undefined && own !== null && String(own).trim() !== '') {
|
|
605
|
+
return String(own);
|
|
606
|
+
}
|
|
607
|
+
return `${mtype}:${path.basename(file)}:${ordinal}`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Human-scannable title for an index row: title > key > path > first body line > id. */
|
|
611
|
+
export function _entry_title(entry: Record<string, unknown>, id: string): string {
|
|
612
|
+
for (const field of ['title', 'key', 'path']) {
|
|
613
|
+
const v = entry[field];
|
|
614
|
+
if (typeof v === 'string' && v.trim() !== '') {
|
|
615
|
+
return v.trim().slice(0, 120);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const body = entry['body'];
|
|
619
|
+
if (typeof body === 'string' && body.trim() !== '') {
|
|
620
|
+
const first = body.trim().split('\n')[0] as string;
|
|
621
|
+
return first.trim().slice(0, 120);
|
|
622
|
+
}
|
|
623
|
+
return id;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Lazy tokenizer handle — the index mode is the ONLY consumer; the default
|
|
627
|
+
// full path must not pay the tiktoken load (hot MCP import path).
|
|
628
|
+
let _tokenizer: { gpt_tokens: (t: string) => { tokens: number }; TIKTOKEN_AVAILABLE: boolean } | null | undefined;
|
|
629
|
+
function _lazy_tokenizer(): typeof _tokenizer {
|
|
630
|
+
if (_tokenizer === undefined) {
|
|
631
|
+
try {
|
|
632
|
+
const req = createRequire(import.meta.url);
|
|
633
|
+
_tokenizer = req('./_lib/token_count.js');
|
|
634
|
+
} catch {
|
|
635
|
+
_tokenizer = null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return _tokenizer;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Real-tokenizer estimate of what fetching this entry's full envelope row costs. */
|
|
642
|
+
export function _entry_tokens_estimate(envelope_entry: Record<string, unknown>): number {
|
|
643
|
+
const serialized = JSON.stringify(envelope_entry);
|
|
644
|
+
const tok = _lazy_tokenizer();
|
|
645
|
+
if (tok !== null && tok !== undefined && tok.TIKTOKEN_AVAILABLE) {
|
|
646
|
+
return tok.gpt_tokens(serialized).tokens;
|
|
647
|
+
}
|
|
648
|
+
// Proxy fallback — a UI hint only; measurements go through the replay
|
|
649
|
+
// rig which records tokenizer provenance (D2).
|
|
650
|
+
return Math.ceil(serialized.length / 4);
|
|
651
|
+
}
|
|
652
|
+
|
|
581
653
|
function _retrieve_internal(types: string[], keys: string[], limit: number, today: Date): [Hit[], SkippedEntry[]] {
|
|
582
654
|
const repo_hits: Hit[] = [];
|
|
583
655
|
const skipped: SkippedEntry[] = [];
|
|
@@ -616,20 +688,24 @@ function _retrieve_internal(types: string[], keys: string[], limit: number, toda
|
|
|
616
688
|
}
|
|
617
689
|
// Curated entries: status filtering happens inside _iter_curated_entries.
|
|
618
690
|
// We additionally filter stale entries here and track them in skipped.
|
|
691
|
+
const _cur_ordinals = new Map<string, number>();
|
|
619
692
|
for (const [p, entry] of _iter_curated_entries(mtype)) {
|
|
620
|
-
|
|
693
|
+
// ID derivation runs for EVERY yielded entry (before any
|
|
694
|
+
// filtering) so ordinals stay consistent across all consumers
|
|
695
|
+
// of the same iterator (retrieve vs memory_get_v1).
|
|
696
|
+
const eid = _entry_id(mtype, String(p), entry, _cur_ordinals);
|
|
621
697
|
if (_isStale(entry, today)) {
|
|
622
698
|
const lv = 'last_validated' in entry ? _pyStrDateLike(entry['last_validated']) : 'unknown';
|
|
623
699
|
const rad = 'review_after_days' in entry ? _pyStr(entry['review_after_days']) : '?';
|
|
624
700
|
skipped.push({
|
|
625
|
-
id:
|
|
701
|
+
id: eid,
|
|
626
702
|
type: mtype,
|
|
627
703
|
reason: 'stale',
|
|
628
704
|
details: `last_validated=${lv}, review_after_days=${rad}`,
|
|
629
705
|
});
|
|
630
706
|
continue;
|
|
631
707
|
}
|
|
632
|
-
repo_hits.push(new Hit(
|
|
708
|
+
repo_hits.push(new Hit(eid, mtype, 'curated', String(p), _score(entry, keys), entry));
|
|
633
709
|
}
|
|
634
710
|
// Capture superseded/deprecated entries in skipped (for retrieve_with_meta).
|
|
635
711
|
for (const [p, entry] of _iter_curated_entries_all(mtype)) {
|
|
@@ -762,7 +838,19 @@ const _KNOWN_TYPES: ReadonlySet<string> = new Set([...CURATED_TYPES, KNOWLEDGE_T
|
|
|
762
838
|
* types are reported as `status: unknown_type` for that slice only, rather
|
|
763
839
|
* than failing the whole call. All entries are file-backed (`source: "repo"`).
|
|
764
840
|
*/
|
|
765
|
-
export function retrieve_v1(
|
|
841
|
+
export function retrieve_v1(
|
|
842
|
+
types: string[],
|
|
843
|
+
keys: string[],
|
|
844
|
+
limit = 20,
|
|
845
|
+
options: { detail?: 'index' | 'full' } = {},
|
|
846
|
+
): Record<string, unknown> {
|
|
847
|
+
// ADDITIVE detail parameter (road-to-memory-retrieval-economy Phase 1,
|
|
848
|
+
// D1): default 'full' keeps the published v1 envelope byte-identical.
|
|
849
|
+
// 'index' replaces each entry's `body` with a compact row —
|
|
850
|
+
// `title` + `tokens_estimate` (the cost `memory_get` would incur) — so
|
|
851
|
+
// a host can rank before fetching. The default flip to 'index' is a
|
|
852
|
+
// separate, falsification-gated human decision (Phase 1b).
|
|
853
|
+
const detail = options.detail ?? 'full';
|
|
766
854
|
const known = types.filter((t) => _KNOWN_TYPES.has(t));
|
|
767
855
|
const unknown = types.filter((t) => !_KNOWN_TYPES.has(t));
|
|
768
856
|
|
|
@@ -785,7 +873,22 @@ export function retrieve_v1(types: string[], keys: string[], limit = 20): Record
|
|
|
785
873
|
if (h.type in slice_counts) {
|
|
786
874
|
slice_counts[h.type] = (slice_counts[h.type] as number) + 1;
|
|
787
875
|
}
|
|
788
|
-
|
|
876
|
+
if (detail === 'index') {
|
|
877
|
+
entries.push({
|
|
878
|
+
id: h.id,
|
|
879
|
+
type: h.type,
|
|
880
|
+
source: 'repo',
|
|
881
|
+
confidence: new PyFloat(_round4(h.score)),
|
|
882
|
+
title: _entry_title(h.entry, h.id),
|
|
883
|
+
tokens_estimate: _entry_tokens_estimate(envelope_entry),
|
|
884
|
+
// Knowledge chunks carry their pinned flag in the index row
|
|
885
|
+
// (roadmap Phase 4) — pinned entries rank higher, so the
|
|
886
|
+
// caller should see why before fetching.
|
|
887
|
+
...(h.type === KNOWLEDGE_TYPE ? { pinned: Boolean(h.entry['pinned'] ?? false) } : {}),
|
|
888
|
+
});
|
|
889
|
+
} else {
|
|
890
|
+
entries.push(envelope_entry);
|
|
891
|
+
}
|
|
789
892
|
}
|
|
790
893
|
|
|
791
894
|
const slices: Record<string, Record<string, unknown>> = {};
|
|
@@ -819,6 +922,73 @@ export function retrieve_v1(types: string[], keys: string[], limit = 20): Record
|
|
|
819
922
|
return envelope;
|
|
820
923
|
}
|
|
821
924
|
|
|
925
|
+
/**
|
|
926
|
+
* Batch full-entry fetch by ID — the second half of the two-phase retrieval
|
|
927
|
+
* economy (road-to-memory-retrieval-economy Phase 1). Scans the same
|
|
928
|
+
* iterators as {@link retrieve} with the same ID derivation
|
|
929
|
+
* ({@link _entry_id}), so an ID returned by `detail: 'index'` always
|
|
930
|
+
* resolves here. Unknown IDs are reported per-ID (`ids[<id>] = 'unknown'`)
|
|
931
|
+
* rather than failing the batch — matching batch-fetch semantics.
|
|
932
|
+
*
|
|
933
|
+
* Envelope: `{contract_version, status, entries, ids}` where `entries`
|
|
934
|
+
* carry FULL bodies (shape identical to `retrieve_v1` full entries minus
|
|
935
|
+
* `confidence` — an explicit fetch has no relevance score) and `status` is
|
|
936
|
+
* `ok` (all found) / `partial` (some) / `error` (none).
|
|
937
|
+
*/
|
|
938
|
+
export function memory_get_v1(ids: string[]): Record<string, unknown> {
|
|
939
|
+
const wanted = new Set(ids);
|
|
940
|
+
const found = new Map<string, Record<string, unknown>>();
|
|
941
|
+
|
|
942
|
+
const record = (id: string, mtype: string, entry: Record<string, unknown>): void => {
|
|
943
|
+
if (wanted.has(id) && !found.has(id)) {
|
|
944
|
+
found.set(id, {
|
|
945
|
+
id,
|
|
946
|
+
type: mtype,
|
|
947
|
+
source: 'repo',
|
|
948
|
+
body: _isPlainObject(entry) ? { ...entry } : {},
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
for (const mtype of CURATED_TYPES) {
|
|
954
|
+
const ordinals = new Map<string, number>();
|
|
955
|
+
for (const [p, entry] of _iter_curated_entries(mtype)) {
|
|
956
|
+
record(_entry_id(mtype, String(p), entry, ordinals), mtype, entry);
|
|
957
|
+
}
|
|
958
|
+
for (const [p, entry] of _iter_intake_entries(mtype)) {
|
|
959
|
+
void p;
|
|
960
|
+
const own = entry['id'];
|
|
961
|
+
if (typeof own === 'string' && own.trim() !== '') {
|
|
962
|
+
record(own, mtype, entry);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
for (const [p, entry] of _iter_knowledge_entries()) {
|
|
967
|
+
void p;
|
|
968
|
+
record(String(entry['id'] ?? ''), KNOWLEDGE_TYPE, entry);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const entries: Record<string, unknown>[] = [];
|
|
972
|
+
const idStatus: Record<string, string> = {};
|
|
973
|
+
for (const id of ids) {
|
|
974
|
+
const e = found.get(id);
|
|
975
|
+
if (e !== undefined) {
|
|
976
|
+
entries.push(e);
|
|
977
|
+
idStatus[id] = 'ok';
|
|
978
|
+
} else {
|
|
979
|
+
idStatus[id] = 'unknown';
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const okCount = entries.length;
|
|
983
|
+
const status = okCount === ids.length ? 'ok' : okCount === 0 ? 'error' : 'partial';
|
|
984
|
+
return {
|
|
985
|
+
contract_version: _CONTRACT_VERSION,
|
|
986
|
+
status,
|
|
987
|
+
entries,
|
|
988
|
+
ids: idStatus,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
822
992
|
// ---------------------------------------------------------------------------
|
|
823
993
|
// CLI
|
|
824
994
|
// ---------------------------------------------------------------------------
|
|
@@ -829,6 +999,7 @@ interface ParsedArgs {
|
|
|
829
999
|
limit: number;
|
|
830
1000
|
format: 'text' | 'json';
|
|
831
1001
|
envelope: 'legacy' | 'v1';
|
|
1002
|
+
detail: 'index' | 'full';
|
|
832
1003
|
}
|
|
833
1004
|
|
|
834
1005
|
function _parseArgs(argv: string[]): ParsedArgs {
|
|
@@ -838,6 +1009,7 @@ function _parseArgs(argv: string[]): ParsedArgs {
|
|
|
838
1009
|
limit: 5,
|
|
839
1010
|
format: 'text',
|
|
840
1011
|
envelope: 'legacy',
|
|
1012
|
+
detail: 'full',
|
|
841
1013
|
};
|
|
842
1014
|
for (let i = 0; i < argv.length; i += 1) {
|
|
843
1015
|
const a = argv[i] as string;
|
|
@@ -863,6 +1035,12 @@ function _parseArgs(argv: string[]): ParsedArgs {
|
|
|
863
1035
|
args.envelope = _checkChoice(a.slice('--envelope='.length), ['legacy', 'v1'], '--envelope') as
|
|
864
1036
|
| 'legacy'
|
|
865
1037
|
| 'v1';
|
|
1038
|
+
} else if (a === '--detail') {
|
|
1039
|
+
args.detail = _checkChoice(argv[++i], ['index', 'full'], '--detail') as 'index' | 'full';
|
|
1040
|
+
} else if (a.startsWith('--detail=')) {
|
|
1041
|
+
args.detail = _checkChoice(a.slice('--detail='.length), ['index', 'full'], '--detail') as
|
|
1042
|
+
| 'index'
|
|
1043
|
+
| 'full';
|
|
866
1044
|
} else if (a === '-h' || a === '--help') {
|
|
867
1045
|
_printUsage();
|
|
868
1046
|
process.exit(0);
|
|
@@ -915,7 +1093,7 @@ export function main(): number {
|
|
|
915
1093
|
return 2;
|
|
916
1094
|
}
|
|
917
1095
|
if (args.envelope === 'v1') {
|
|
918
|
-
const envelope = retrieve_v1(types, args.key, args.limit);
|
|
1096
|
+
const envelope = retrieve_v1(types, args.key, args.limit, { detail: args.detail });
|
|
919
1097
|
process.stdout.write(`${pyJsonDumps(envelope, 2)}\n`);
|
|
920
1098
|
return 0;
|
|
921
1099
|
}
|