iranti 0.3.0 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +48 -44
  2. package/dist/scripts/claude-code-memory-hook.js +42 -153
  3. package/dist/scripts/codex-setup.js +1 -1
  4. package/dist/scripts/iranti-cli.js +161 -17
  5. package/dist/scripts/iranti-mcp.js +80 -8
  6. package/dist/scripts/seed.js +1 -1
  7. package/dist/src/api/middleware/validation.d.ts.map +1 -1
  8. package/dist/src/api/middleware/validation.js +13 -1
  9. package/dist/src/api/middleware/validation.js.map +1 -1
  10. package/dist/src/api/routes/knowledge.d.ts.map +1 -1
  11. package/dist/src/api/routes/knowledge.js +3 -0
  12. package/dist/src/api/routes/knowledge.js.map +1 -1
  13. package/dist/src/api/routes/memory.d.ts.map +1 -1
  14. package/dist/src/api/routes/memory.js +3 -0
  15. package/dist/src/api/routes/memory.js.map +1 -1
  16. package/dist/src/api/server.js +1 -1
  17. package/dist/src/attendant/AttendantInstance.d.ts +44 -1
  18. package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
  19. package/dist/src/attendant/AttendantInstance.js +475 -41
  20. package/dist/src/attendant/AttendantInstance.js.map +1 -1
  21. package/dist/src/attendant/index.d.ts +1 -1
  22. package/dist/src/attendant/index.d.ts.map +1 -1
  23. package/dist/src/attendant/index.js.map +1 -1
  24. package/dist/src/chat/index.d.ts +2 -0
  25. package/dist/src/chat/index.d.ts.map +1 -1
  26. package/dist/src/chat/index.js +56 -22
  27. package/dist/src/chat/index.js.map +1 -1
  28. package/dist/src/lib/assistantCheckpoint.d.ts +21 -0
  29. package/dist/src/lib/assistantCheckpoint.d.ts.map +1 -0
  30. package/dist/src/lib/assistantCheckpoint.js +143 -0
  31. package/dist/src/lib/assistantCheckpoint.js.map +1 -0
  32. package/dist/src/lib/cliHelpCatalog.d.ts.map +1 -1
  33. package/dist/src/lib/cliHelpCatalog.js +6 -5
  34. package/dist/src/lib/cliHelpCatalog.js.map +1 -1
  35. package/dist/src/lib/hostMemoryFormatting.d.ts +25 -0
  36. package/dist/src/lib/hostMemoryFormatting.d.ts.map +1 -0
  37. package/dist/src/lib/hostMemoryFormatting.js +56 -0
  38. package/dist/src/lib/hostMemoryFormatting.js.map +1 -0
  39. package/dist/src/lib/llm.d.ts.map +1 -1
  40. package/dist/src/lib/llm.js +3 -1
  41. package/dist/src/lib/llm.js.map +1 -1
  42. package/dist/src/lib/projectLearning.d.ts +21 -0
  43. package/dist/src/lib/projectLearning.d.ts.map +1 -0
  44. package/dist/src/lib/projectLearning.js +357 -0
  45. package/dist/src/lib/projectLearning.js.map +1 -0
  46. package/dist/src/lib/protocolEnforcement.d.ts +3 -1
  47. package/dist/src/lib/protocolEnforcement.d.ts.map +1 -1
  48. package/dist/src/lib/protocolEnforcement.js +28 -2
  49. package/dist/src/lib/protocolEnforcement.js.map +1 -1
  50. package/dist/src/lib/sessionLedger.d.ts +18 -0
  51. package/dist/src/lib/sessionLedger.d.ts.map +1 -1
  52. package/dist/src/lib/sessionLedger.js +78 -0
  53. package/dist/src/lib/sessionLedger.js.map +1 -1
  54. package/dist/src/librarian/index.d.ts.map +1 -1
  55. package/dist/src/librarian/index.js +51 -0
  56. package/dist/src/librarian/index.js.map +1 -1
  57. package/dist/src/library/client.d.ts.map +1 -1
  58. package/dist/src/library/client.js +0 -1
  59. package/dist/src/library/client.js.map +1 -1
  60. package/dist/src/sdk/index.d.ts +2 -0
  61. package/dist/src/sdk/index.d.ts.map +1 -1
  62. package/dist/src/sdk/index.js +39 -2
  63. package/dist/src/sdk/index.js.map +1 -1
  64. package/package.json +9 -5
package/README.md CHANGED
@@ -9,11 +9,11 @@
9
9
 
10
10
  Iranti gives agents persistent, identity-based memory. Facts written by one agent are retrievable by any other agent through exact entity+key lookup. Iranti also supports hybrid search (lexical + vector) when exact keys are unknown. Memory persists across sessions and survives context window limits.
11
11
 
12
- **Repo version:** `0.2.52`
13
- **Latest published release:** [`v0.2.51`](https://github.com/nfemmanuel/iranti/releases/tag/v0.2.51)
12
+ **Repo version:** `0.3.0`
14
13
  Published packages:
15
- - `iranti@0.2.51`
16
- - `@iranti/sdk@0.2.51`
14
+ - npm `iranti@0.3.0`
15
+ - npm `@iranti/sdk@0.3.0`
16
+ - PyPI `iranti==0.3.0`
17
17
 
18
18
  ---
19
19
 
@@ -53,27 +53,29 @@ Vector databases answer "what's similar to X?" Iranti answers "what do we know a
53
53
 
54
54
  ## Benchmark Summary
55
55
 
56
- Iranti now has current rerun evidence across exact retrieval, process continuity, upgrade durability, relationships, bounded conflict handling, and bounded recovery behavior. The current picture is stronger and narrower than the early validation story: exact, durable, shared memory is validated; broad semantic-memory and autonomous-recovery claims still need explicit limits.
56
+ Iranti now has current rerun evidence across exact retrieval, process continuity, upgrade durability, relationships, bounded conflict handling, and bounded recovery behavior. The current picture is stronger and narrower than the early validation story: exact, durable, shared memory is validated; broad semantic-memory and autonomous-recovery claims still need explicit limits.
57
57
 
58
58
  ### Confirmed Strengths
59
59
 
60
- - **Exact lookup (`iranti_query`)**: The current installed-local `0.2.52` rerun still scored `10/10` on the canonical B1 exact-retrieval arm.
61
- - **Persistence across sessions and processes**: Facts survive context-window loss and genuine process boundaries, with current reruns and validation passes covering process-isolated reads plus cross-process invalidation.
62
- - **Upgrade durability**: A fresh continuity chain now passes across public npm `0.2.49 -> 0.2.50 -> 0.2.51` plus a final installed-local `0.2.52` hop.
60
+ - **Exact lookup (`iranti_query`)**: The prepublish candidate for the current `0.3.0` line still scored `10/10` on the canonical B1 exact-retrieval arm.
61
+ - **Persistence across sessions and processes**: Facts survive context-window loss and genuine process boundaries, with current reruns and validation passes covering process-isolated reads plus cross-process invalidation.
62
+ - **Upgrade durability**: A fresh continuity chain now passes across public npm `0.2.49 -> 0.2.50 -> 0.2.51` plus a final prepublish candidate hop on the `0.3.0` line.
63
63
  - **Conflict handling**: Reliable when confidence differentials are large and explicit.
64
- - **Relationship traversal and multi-agent coordination**: Relationship writes plus one-hop/deep traversal work, and agents can share memory across genuine subprocess boundaries with zero shared conversational context.
64
+ - **Relationship traversal and multi-agent coordination**: Relationship writes plus one-hop/deep traversal work, and agents can share memory across genuine subprocess boundaries with zero shared conversational context.
65
65
  - **Provenance on writes**: Write-side attribution through stored source metadata is working and benchmark-confirmed.
66
- - **Ingest**: Prose extraction is accurate on clean entities in the bounded rerun surfaces already documented.
67
- - **Observe with hints / explicit recovery**: `iranti_observe` with hints and explicit query-based recovery are meaningfully useful today.
68
-
69
- Recent fixes since the last rerun:
70
- - **Search now bridges semantic hits across relationships for filtered retrieval.** A strong incident or issue hit can now propagate back onto the owning filtered entity over up to two hops, so filtered project search no longer depends on direct lexical overlap alone.
71
- - **`iranti_attend` now safe-defaults parse failures toward memory on non-greeting turns.** Terse prompts such as `help`, `why?`, or `how?` no longer silently fall through to `classification_parse_failed_default_false`.
72
- - **Hybrid search now indexes entity addresses directly.** Addressed queries such as `project/<id> status` no longer rely on value-summary overlap alone.
73
-
74
- ### Current Limits
75
-
66
+ - **Ingest**: Prose extraction is accurate on clean entities in the bounded rerun surfaces already documented.
67
+ - **Observe with hints / explicit recovery**: `iranti_observe` with hints and explicit query-based recovery are meaningfully useful today.
68
+ - **Protocol enforcement turn-gate**: Strict turn-cycle enforcement (handshake → attend → discover → post-response) is validated by focused route and MCP regression tests. In `strict` mode, KB discovery routes return HTTP 428 with a structured violation payload before an agent that skipped attend can silently read stale memory. Injected facts now include a `lastUpdated` timestamp so the calling agent can judge freshness at the point of injection.
69
+
70
+ Recent fixes since the last rerun:
71
+ - **Search now bridges semantic hits across relationships for filtered retrieval.** A strong incident or issue hit can now propagate back onto the owning filtered entity over up to two hops, so filtered project search no longer depends on direct lexical overlap alone.
72
+ - **`iranti_attend` now safe-defaults parse failures toward memory on non-greeting turns.** Terse prompts such as `help`, `why?`, or `how?` no longer silently fall through to `classification_parse_failed_default_false`.
73
+ - **Hybrid search now indexes entity addresses directly.** Addressed queries such as `project/<id> status` no longer rely on value-summary overlap alone.
74
+
75
+ ### Current Limits
76
+
76
77
  - **Upgrade durability is now test-covered.** The `v0.2.21` upgrade reinitialized the instance under test — a one-time operator incident, not a systemic design flaw. All Prisma migrations are additive (no DROP or TRUNCATE); Staff Namespace seeding is guarded by `isSeeded()`; `seed-codebase.ts` upserts to `codebase/*` entities only. A KB preservation test in `tests/runtime-lifecycle/run_setup_upgrade_tests.ts` now confirms user KB data written before a `setup` re-run survives intact.
78
+ - **Project learning is now only a bounded bind-time snapshot.** When you bind a project, Iranti derives a stable `IRANTI_CODEBASE_ENTITY` and writes a small snapshot from authoritative files such as `package.json`, `README.md`, `tsconfig.json`, `pyproject.toml`, `prisma/schema.prisma`, and common source directories. It does not yet crawl the repo continuously or build autonomous whole-project understanding.
77
79
  - **Relationship and provenance reflection surfaces remain partially permission-gated in benchmark sessions.** The rerun did not prove `iranti_relate`, `iranti_related`, `iranti_related_deep`, or `iranti_who_knows` end-to-end under the benchmark session policy.
78
80
 
79
81
  ### Practical Position
@@ -81,9 +83,10 @@ Recent fixes since the last rerun:
81
83
  Iranti is strongest today as **structured memory infrastructure for multi-agent systems**:
82
84
  - exact entity/key lookup
83
85
  - durable shared memory
84
- - provenance-aware writes
86
+ - provenance-aware writes with source attribution and freshness timestamps
85
87
  - conflict-aware storage
86
88
  - session-aware recovery
89
+ - protocol-enforced turn discipline for trustworthy injection (opt-in strict mode)
87
90
 
88
91
  It should not yet be described as a fully general semantic-memory, semantic-search, or autonomous-memory-injection system.
89
92
 
@@ -468,6 +471,7 @@ iranti project init . --instance local --agent-id chatbot_main
468
471
  ```
469
472
 
470
473
  This writes `.env.iranti` in the project with the correct `IRANTI_URL`, `IRANTI_API_KEY`, and default agent identity.
474
+ It also writes a derived `IRANTI_CODEBASE_ENTITY` and a bounded initial project snapshot for that bound repo.
471
475
 
472
476
  Later changes use the same surface:
473
477
 
@@ -608,19 +612,19 @@ for item in matches:
608
612
  print(item["entity"], item["key"], item["score"])
609
613
  ```
610
614
 
611
- ### Context Persistence (attend)
612
-
613
- `handshake()` establishes the session contract. Discovery surfaces such as `query`, `search`, `get_related`, and `who_knows` are now fail-closed until the host has run `handshake()` for the session and `attend()` for the current turn. Starting a new `attend(phase="pre-response")` turn without first closing the previous one with `attend(phase="post-response")` is now blocked as a protocol violation, and `inspectSession()` / `listSessions()` expose structured lifecycle compliance state (`healthy`, `degraded`, `non_compliant`) so hosts and operators can see when breadcrumb discipline is slipping.
614
-
615
- ```python
616
- # Before each LLM call, let Attendant decide if memory is needed
617
- result = client.attend(
618
- agent_id="research_agent_001",
619
- latest_message="What's Jane Smith's current affiliation?",
620
- current_context="User: What's Jane Smith's current affiliation?\nAssistant: Let me check...",
621
- max_facts=5,
622
- phase="pre-response"
623
- )
615
+ ### Context Persistence (attend)
616
+
617
+ `handshake()` establishes the session contract. Discovery surfaces such as `query`, `search`, `get_related`, and `who_knows` are now fail-closed until the host has run `handshake()` for the session and `attend()` for the current turn. Starting a new `attend(phase="pre-response")` turn without first closing the previous one with `attend(phase="post-response")` is now blocked as a protocol violation, and `inspectSession()` / `listSessions()` expose structured lifecycle compliance state (`healthy`, `degraded`, `non_compliant`) so hosts and operators can see when breadcrumb discipline is slipping.
618
+
619
+ ```python
620
+ # Before each LLM call, let Attendant decide if memory is needed
621
+ result = client.attend(
622
+ agent_id="research_agent_001",
623
+ latest_message="What's Jane Smith's current affiliation?",
624
+ current_context="User: What's Jane Smith's current affiliation?\nAssistant: Let me check...",
625
+ max_facts=5,
626
+ phase="pre-response"
627
+ )
624
628
 
625
629
  if result["shouldInject"]:
626
630
  for fact in result['facts']:
@@ -731,11 +735,11 @@ middleware.after_receive(
731
735
  )
732
736
  ```
733
737
 
734
- **How it works**:
735
- 1. `before_send()` calls `attend()` with conversation context
736
- 2. Forgotten facts are prepended as `[MEMORY: ...]`
737
- 3. `after_receive()` extracts new facts and saves them (best-effort)
738
- 4. If the host skips `handshake()` or `attend()`, discovery calls block instead of returning warn-only metadata
738
+ **How it works**:
739
+ 1. `before_send()` calls `attend()` with conversation context
740
+ 2. Forgotten facts are prepended as `[MEMORY: ...]`
741
+ 3. `after_receive()` extracts new facts and saves them (best-effort)
742
+ 4. If the host skips `handshake()` or `attend()`, discovery calls block instead of returning warn-only metadata
739
743
 
740
744
  **Note**: Browser extensions are blocked by ChatGPT and Claude's Content Security Policy. Use API-based middleware instead.
741
745
 
@@ -762,10 +766,10 @@ Express server on port 3001 with endpoints:
762
766
  - `POST /kb/write` - Write atomic fact
763
767
  - `POST /kb/ingest` - Ingest raw text for one entity, auto-chunk into facts with per-fact confidence and per-fact write outcomes
764
768
  - `GET /kb/query/:entityType/:entityId/:key` - Query specific fact
765
- - `GET /kb/query/:entityType/:entityId` - Query all facts for entity
766
- - `GET /kb/search` - Hybrid search across facts
767
- - `POST /memory/attend` - Decide whether to inject memory for this turn and unlock one discovery step for the current turn
768
- - `POST /memory/observe` - Context persistence (inject missing facts)
769
+ - `GET /kb/query/:entityType/:entityId` - Query all facts for entity
770
+ - `GET /kb/search` - Hybrid search across facts
771
+ - `POST /memory/attend` - Decide whether to inject memory for this turn and unlock one discovery step for the current turn
772
+ - `POST /memory/observe` - Context persistence (inject missing facts)
769
773
  - `POST /memory/handshake` - Working memory brief for agent session
770
774
  - `GET /memory/ledger` - Read structured session ledger events from `staff_events`
771
775
  - `GET /memory/sessions` - List persisted operator-visible session checkpoints across agents, with optional operator filters/sorting
@@ -774,8 +778,8 @@ Express server on port 3001 with endpoints:
774
778
  - `GET /kb/related/:entityType/:entityId` - Get related entities
775
779
  - `POST /agents/register` - Register agent in registry
776
780
 
777
- All endpoints require `X-Iranti-Key` header for authentication.
778
- Discovery endpoints return `428 Precondition Required` when a host skips the required session `handshake` or current-turn `attend`.
781
+ All endpoints require `X-Iranti-Key` header for authentication.
782
+ Discovery endpoints return `428 Precondition Required` when a host skips the required session `handshake` or current-turn `attend`.
779
783
 
780
784
  ---
781
785
 
@@ -11,6 +11,8 @@ const runtimeEnv_1 = require("../src/lib/runtimeEnv");
11
11
  const staffEventRegistry_1 = require("../src/lib/staffEventRegistry");
12
12
  const client_1 = require("../src/library/client");
13
13
  const autoRemember_1 = require("../src/lib/autoRemember");
14
+ const assistantCheckpoint_1 = require("../src/lib/assistantCheckpoint");
15
+ const hostMemoryFormatting_1 = require("../src/lib/hostMemoryFormatting");
14
16
  const MEMORY_NEED_POSITIVE_PATTERNS = [
15
17
  /\bwhat(?:'s| is| was)?\s+my\b/i,
16
18
  /\bdo you remember\b/i,
@@ -41,7 +43,7 @@ function printHelp() {
41
43
  '',
42
44
  'Reads Claude Code hook JSON from stdin and returns hookSpecificOutput.additionalContext on stdout.',
43
45
  'This helper retrieves working memory; durable KB writes still require explicit iranti_write/ingest calls.',
44
- 'Set IRANTI_AUTO_REMEMBER=true to auto-save narrow personal facts to IRANTI_PERSONAL_MEMORY_ENTITY/user/main, project summaries to IRANTI_MEMORY_ENTITY, and shared checkpoint breadcrumbs for resumable work.',
46
+ 'Set IRANTI_AUTO_REMEMBER=true to auto-save prompt-side personal/project facts. Assistant-response continuity facts and shared checkpoints are captured on Stop regardless.',
45
47
  ].join('\n'));
46
48
  }
47
49
  function parseArgs(argv) {
@@ -229,19 +231,29 @@ function getMaxFacts() {
229
231
  return Math.min(12, Math.trunc(raw));
230
232
  }
231
233
  function formatSessionContext(facts, cwd) {
232
- const limited = facts.slice(0, getMaxFacts());
234
+ const limited = (0, hostMemoryFormatting_1.assignStructuredFactIds)(facts.slice(0, getMaxFacts()).map((fact) => ({
235
+ ...fact,
236
+ entityKey: `${fact.entity}/${fact.key}`,
237
+ })));
233
238
  const lines = [
234
239
  '[Iranti Session Memory]',
235
240
  `Project: ${path_1.default.basename(cwd)}`,
236
241
  'REQUIRED: Call mcp__iranti__iranti_handshake before responding to the first user message.',
237
- 'REQUIRED: Call mcp__iranti__iranti_attend before every subsequent turn.',
242
+ 'REQUIRED: Call mcp__iranti__iranti_attend(phase=\'pre-response\') before every reply and before factual discovery.',
243
+ 'REQUIRED: After every response, call mcp__iranti__iranti_attend(phase=\'post-response\').',
244
+ 'REQUIRED: Prefer injected Iranti facts before re-inferring project state.',
245
+ 'REQUIRED: Call mcp__iranti__iranti_write after every file edit, confirmed finding, system state discovery, and subagent completion — write what changed, why, and what it means.',
238
246
  ];
239
- if (limited.length > 0) {
240
- lines.push('Relevant memory:');
241
- for (const fact of limited) {
242
- lines.push(`- ${fact.entity}/${fact.key}: ${fact.summary}`);
243
- }
244
- }
247
+ const block = (0, hostMemoryFormatting_1.formatStructuredFactBlock)(limited, {
248
+ title: 'Iranti Session Facts',
249
+ introLines: [
250
+ 'Use these loaded facts as the starting working-memory frame for this session.',
251
+ 'Prefer them before re-inferring project state.',
252
+ 'Fact IDs are stable only within this block.',
253
+ ],
254
+ });
255
+ if (block)
256
+ lines.push(block);
245
257
  return lines.join('\n');
246
258
  }
247
259
  function formatPreCompactContext() {
@@ -270,154 +282,22 @@ function extractSelfMemoryQueryKey(prompt) {
270
282
  function formatPromptContext(facts, prompt) {
271
283
  if (facts.length === 0)
272
284
  return '';
273
- const lines = ['[Iranti Retrieved Memory]'];
285
+ const structuredFacts = (0, hostMemoryFormatting_1.assignStructuredFactIds)(facts.map((fact) => ({
286
+ ...fact,
287
+ entityKey: `${fact.entity}/${fact.key}`,
288
+ })));
289
+ const lines = [];
274
290
  const targetKey = prompt ? extractSelfMemoryQueryKey(prompt) : null;
275
291
  if (targetKey) {
276
- const answerCandidate = facts.find((fact) => (0, autoRemember_1.canonicalizeMemoryKey)(fact.key) === targetKey);
292
+ const answerCandidate = structuredFacts.find((fact) => (0, autoRemember_1.canonicalizeMemoryKey)(fact.entityKey.split('/').slice(2).join('/')) === targetKey);
277
293
  if (answerCandidate) {
278
- lines.push(`Direct answer: ${answerCandidate.summary}.`);
279
- lines.push('Use the direct answer above when it fully answers the user question.');
294
+ lines.push(`[Iranti Direct Answer]`);
295
+ lines.push(`Use ${answerCandidate.factId} directly if it fully answers the user question: ${answerCandidate.summary}.`);
280
296
  }
281
297
  }
282
- for (const fact of facts) {
283
- lines.push(`- ${fact.entity}/${fact.key}: ${fact.summary}`);
284
- }
298
+ lines.push((0, hostMemoryFormatting_1.formatStructuredFactBlock)(structuredFacts, { title: 'Iranti Retrieved Memory' }));
285
299
  return lines.join('\n');
286
300
  }
287
- function readTextField(value, preferredKey) {
288
- if (typeof value !== 'object' || value === null)
289
- return undefined;
290
- const record = value;
291
- const raw = record[preferredKey];
292
- return typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
293
- }
294
- function readItems(value) {
295
- if (typeof value !== 'object' || value === null)
296
- return [];
297
- const record = value;
298
- const items = Array.isArray(record.items) ? record.items : [];
299
- return items.map((item) => String(item ?? '').trim()).filter(Boolean);
300
- }
301
- function readFileChangeOutputs(value) {
302
- if (typeof value !== 'object' || value === null)
303
- return [];
304
- const record = value;
305
- const items = Array.isArray(record.items) ? record.items : [];
306
- return items.map((item) => {
307
- if (typeof item !== 'object' || item === null)
308
- return '';
309
- const change = item;
310
- const action = String(change.action ?? 'updated').trim();
311
- const targetPath = String(change.path ?? '').trim();
312
- const toPath = String(change.toPath ?? '').trim();
313
- if (!targetPath)
314
- return '';
315
- return toPath ? `${action} ${targetPath} -> ${toPath}` : `${action} ${targetPath}`;
316
- }).filter(Boolean);
317
- }
318
- function readCheckpointActions(value) {
319
- if (typeof value !== 'object' || value === null)
320
- return [];
321
- const record = value;
322
- const items = Array.isArray(record.items) ? record.items : [];
323
- return items
324
- .map((item) => {
325
- if (typeof item !== 'object' || item === null)
326
- return null;
327
- const action = item;
328
- const summary = String(action.summary ?? '').trim();
329
- if (!summary)
330
- return null;
331
- const kind = String(action.kind ?? 'action').trim() || 'action';
332
- return {
333
- kind,
334
- summary,
335
- ...(typeof action.status === 'string' && action.status.trim() ? { status: action.status.trim() } : {}),
336
- ...(typeof action.target === 'string' && action.target.trim() ? { target: action.target.trim() } : {}),
337
- ...(typeof action.detail === 'string' && action.detail.trim() ? { detail: action.detail.trim() } : {}),
338
- };
339
- })
340
- .filter((item) => Boolean(item));
341
- }
342
- function readActionOutputs(value) {
343
- if (typeof value !== 'object' || value === null)
344
- return [];
345
- const record = value;
346
- const items = Array.isArray(record.items) ? record.items : [];
347
- return items.map((item) => {
348
- if (typeof item !== 'object' || item === null)
349
- return '';
350
- const action = item;
351
- const kind = String(action.kind ?? 'action').trim() || 'action';
352
- const summary = String(action.summary ?? '').trim();
353
- const status = String(action.status ?? '').trim();
354
- if (!summary)
355
- return '';
356
- return status ? `[${status}] ${kind}: ${summary}` : `${kind}: ${summary}`;
357
- }).filter(Boolean);
358
- }
359
- function extractHookCheckpointPayload(response) {
360
- const facts = (0, autoRemember_1.extractExplicitAssistantMemory)(response).filter((fact) => fact.scope === 'project');
361
- if (facts.length === 0) {
362
- return null;
363
- }
364
- const checkpoint = {};
365
- const outputs = [];
366
- for (const fact of facts) {
367
- if (fact.key === 'current_step') {
368
- checkpoint.currentStep = readTextField(fact.value, 'text');
369
- continue;
370
- }
371
- if (fact.key === 'next_step') {
372
- checkpoint.nextStep = readTextField(fact.value, 'instruction') ?? readTextField(fact.value, 'text');
373
- continue;
374
- }
375
- if (fact.key === 'open_risks') {
376
- checkpoint.openRisks = readItems(fact.value);
377
- continue;
378
- }
379
- if (fact.key === 'important_artifacts') {
380
- outputs.push(...readItems(fact.value));
381
- continue;
382
- }
383
- if (fact.key === 'recent_file_changes') {
384
- const fileChanges = typeof fact.value === 'object' && fact.value !== null && Array.isArray(fact.value.items)
385
- ? fact.value.items
386
- .filter((item) => item && typeof item === 'object')
387
- .map((item) => ({
388
- action: String(item.action ?? 'updated').trim() || 'updated',
389
- path: String(item.path ?? '').trim(),
390
- ...(typeof item.toPath === 'string' && item.toPath.trim() ? { toPath: String(item.toPath).trim() } : {}),
391
- ...(typeof item.purpose === 'string' && item.purpose.trim() ? { purpose: String(item.purpose).trim() } : {}),
392
- }))
393
- .filter((item) => item.path)
394
- : [];
395
- if (fileChanges.length > 0) {
396
- checkpoint.fileChanges = [...(checkpoint.fileChanges ?? []), ...fileChanges];
397
- }
398
- outputs.push(...readFileChangeOutputs(fact.value));
399
- continue;
400
- }
401
- if (fact.key === 'recent_actions') {
402
- const actions = readCheckpointActions(fact.value);
403
- if (actions.length > 0) {
404
- checkpoint.actions = [...(checkpoint.actions ?? []), ...actions];
405
- }
406
- outputs.push(...readActionOutputs(fact.value));
407
- }
408
- }
409
- if (outputs.length > 0) {
410
- checkpoint.recentOutputs = outputs;
411
- }
412
- return checkpoint.currentStep
413
- || checkpoint.nextStep
414
- || (checkpoint.openRisks && checkpoint.openRisks.length > 0)
415
- || (checkpoint.recentOutputs && checkpoint.recentOutputs.length > 0)
416
- || (checkpoint.actions && checkpoint.actions.length > 0)
417
- || (checkpoint.fileChanges && checkpoint.fileChanges.length > 0)
418
- ? checkpoint
419
- : null;
420
- }
421
301
  function emitHookContext(event, additionalContext) {
422
302
  const payload = {
423
303
  hookSpecificOutput: {
@@ -523,8 +403,8 @@ async function buildHookAdditionalContext(options) {
523
403
  }
524
404
  if (event === 'Stop') {
525
405
  const response = getLastAssistantMessage(payload);
526
- if (response && (0, autoRemember_1.isAutoRememberEnabled)()) {
527
- await (0, autoRemember_1.autoRememberAssistantFacts)({
406
+ if (response) {
407
+ await (0, autoRemember_1.rememberAssistantResponseFacts)({
528
408
  iranti,
529
409
  response,
530
410
  agent,
@@ -534,7 +414,7 @@ async function buildHookAdditionalContext(options) {
534
414
  host: 'claude_code',
535
415
  },
536
416
  });
537
- const checkpoint = extractHookCheckpointPayload(response);
417
+ const checkpoint = (0, assistantCheckpoint_1.extractAssistantCheckpointPayload)(response);
538
418
  const projectEntity = (0, autoRemember_1.getProjectMemoryEntity)();
539
419
  if (checkpoint && projectEntity && typeof iranti.checkpoint === 'function') {
540
420
  await iranti.checkpoint({
@@ -547,6 +427,14 @@ async function buildHookAdditionalContext(options) {
547
427
  },
548
428
  });
549
429
  }
430
+ await iranti.attend({
431
+ agent,
432
+ latestMessage: response,
433
+ currentContext: response,
434
+ entityHints,
435
+ maxFacts: getMaxFacts(),
436
+ phase: 'post-response',
437
+ });
550
438
  }
551
439
  return '';
552
440
  }
@@ -575,6 +463,7 @@ async function buildHookAdditionalContext(options) {
575
463
  currentContext: buildCurrentContext(payload, prompt),
576
464
  entityHints,
577
465
  maxFacts: getMaxFacts(),
466
+ phase: 'pre-response',
578
467
  });
579
468
  const facts = attend.facts.map((fact) => ({
580
469
  entity: fact.entityKey.split('/').slice(0, 2).join('/'),
@@ -225,7 +225,7 @@ function buildCodexAgentsBlock() {
225
225
  '- Call `mcp__iranti__iranti_checkpoint` at natural pauses, before stepping away from long work, when interrupted, and when completing a useful slice.',
226
226
  '- When useful actions happen, record them in the checkpoint `actions` field so later sessions can see important commands, tests, searches, validations, and decisions without rerunning them blindly.',
227
227
  '- Do not treat durable writes as a substitute for checkpoints. A checkpoint not written means the next session has to reconstruct state.',
228
- '- Under-logged runs are non-compliant for this repo. When applicable, leave structured breadcrumbs for what you found, what worked, what failed, what changed, and what happens next instead of only a broad summary.',
228
+ '- Under-logged runs are non-compliant for this repo. When applicable, call iranti_write with what you found, what worked, what failed, what changed, and what happens next not a broad summary, but specific durable facts.',
229
229
  '',
230
230
  '## Host setup check',
231
231
  '- If this block was missing at session start, rerun `iranti codex-setup` from the bound project root.',