iranti 0.2.51 → 0.3.2

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 (163) hide show
  1. package/README.md +30 -17
  2. package/dist/scripts/api-key-create.js +1 -1
  3. package/dist/scripts/api-key-list.js +1 -1
  4. package/dist/scripts/api-key-revoke.js +1 -1
  5. package/dist/scripts/claude-code-memory-hook.js +116 -30
  6. package/dist/scripts/codex-setup.js +86 -4
  7. package/dist/scripts/iranti-cli.js +1359 -57
  8. package/dist/scripts/iranti-mcp.js +578 -75
  9. package/dist/scripts/seed.js +11 -6
  10. package/dist/scripts/setup.js +1 -1
  11. package/dist/src/api/healthChecks.d.ts +29 -0
  12. package/dist/src/api/healthChecks.d.ts.map +1 -0
  13. package/dist/src/api/healthChecks.js +72 -0
  14. package/dist/src/api/healthChecks.js.map +1 -0
  15. package/dist/src/api/middleware/validation.d.ts +22 -0
  16. package/dist/src/api/middleware/validation.d.ts.map +1 -1
  17. package/dist/src/api/middleware/validation.js +93 -3
  18. package/dist/src/api/middleware/validation.js.map +1 -1
  19. package/dist/src/api/routes/knowledge.d.ts.map +1 -1
  20. package/dist/src/api/routes/knowledge.js +53 -0
  21. package/dist/src/api/routes/knowledge.js.map +1 -1
  22. package/dist/src/api/routes/memory.d.ts.map +1 -1
  23. package/dist/src/api/routes/memory.js +73 -9
  24. package/dist/src/api/routes/memory.js.map +1 -1
  25. package/dist/src/api/server.js +38 -43
  26. package/dist/src/api/server.js.map +1 -1
  27. package/dist/src/attendant/AttendantInstance.d.ts +135 -2
  28. package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
  29. package/dist/src/attendant/AttendantInstance.js +1836 -93
  30. package/dist/src/attendant/AttendantInstance.js.map +1 -1
  31. package/dist/src/attendant/index.d.ts +1 -1
  32. package/dist/src/attendant/index.d.ts.map +1 -1
  33. package/dist/src/attendant/index.js +1 -1
  34. package/dist/src/attendant/index.js.map +1 -1
  35. package/dist/src/attendant/registry.d.ts.map +1 -1
  36. package/dist/src/attendant/registry.js +2 -0
  37. package/dist/src/attendant/registry.js.map +1 -1
  38. package/dist/src/chat/index.d.ts +23 -0
  39. package/dist/src/chat/index.d.ts.map +1 -1
  40. package/dist/src/chat/index.js +111 -22
  41. package/dist/src/chat/index.js.map +1 -1
  42. package/dist/src/generated/prisma/browser.d.ts +5 -0
  43. package/dist/src/generated/prisma/browser.d.ts.map +1 -1
  44. package/dist/src/generated/prisma/client.d.ts +5 -0
  45. package/dist/src/generated/prisma/client.d.ts.map +1 -1
  46. package/dist/src/generated/prisma/commonInputTypes.d.ts +48 -0
  47. package/dist/src/generated/prisma/commonInputTypes.d.ts.map +1 -1
  48. package/dist/src/generated/prisma/internal/class.d.ts +11 -0
  49. package/dist/src/generated/prisma/internal/class.d.ts.map +1 -1
  50. package/dist/src/generated/prisma/internal/class.js +4 -4
  51. package/dist/src/generated/prisma/internal/class.js.map +1 -1
  52. package/dist/src/generated/prisma/internal/prismaNamespace.d.ts +92 -1
  53. package/dist/src/generated/prisma/internal/prismaNamespace.d.ts.map +1 -1
  54. package/dist/src/generated/prisma/internal/prismaNamespace.js +17 -2
  55. package/dist/src/generated/prisma/internal/prismaNamespace.js.map +1 -1
  56. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.d.ts +16 -0
  57. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.d.ts.map +1 -1
  58. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.js +17 -2
  59. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.js.map +1 -1
  60. package/dist/src/generated/prisma/models/StaffEvent.d.ts +1184 -0
  61. package/dist/src/generated/prisma/models/StaffEvent.d.ts.map +1 -0
  62. package/dist/src/generated/prisma/models/StaffEvent.js +3 -0
  63. package/dist/src/generated/prisma/models/StaffEvent.js.map +1 -0
  64. package/dist/src/generated/prisma/models.d.ts +1 -0
  65. package/dist/src/generated/prisma/models.d.ts.map +1 -1
  66. package/dist/src/lib/assistantCheckpoint.d.ts +21 -0
  67. package/dist/src/lib/assistantCheckpoint.d.ts.map +1 -0
  68. package/dist/src/lib/assistantCheckpoint.js +143 -0
  69. package/dist/src/lib/assistantCheckpoint.js.map +1 -0
  70. package/dist/src/lib/autoRemember.d.ts +15 -0
  71. package/dist/src/lib/autoRemember.d.ts.map +1 -1
  72. package/dist/src/lib/autoRemember.js +433 -71
  73. package/dist/src/lib/autoRemember.js.map +1 -1
  74. package/dist/src/lib/cliHelpCatalog.d.ts.map +1 -1
  75. package/dist/src/lib/cliHelpCatalog.js +23 -11
  76. package/dist/src/lib/cliHelpCatalog.js.map +1 -1
  77. package/dist/src/lib/cliHelpRenderer.d.ts +1 -0
  78. package/dist/src/lib/cliHelpRenderer.d.ts.map +1 -1
  79. package/dist/src/lib/cliHelpRenderer.js +4 -0
  80. package/dist/src/lib/cliHelpRenderer.js.map +1 -1
  81. package/dist/src/lib/commandErrors.d.ts +5 -1
  82. package/dist/src/lib/commandErrors.d.ts.map +1 -1
  83. package/dist/src/lib/commandErrors.js +250 -17
  84. package/dist/src/lib/commandErrors.js.map +1 -1
  85. package/dist/src/lib/createFirstPartyIranti.d.ts.map +1 -1
  86. package/dist/src/lib/createFirstPartyIranti.js +1 -0
  87. package/dist/src/lib/createFirstPartyIranti.js.map +1 -1
  88. package/dist/src/lib/dbStaffEventEmitter.d.ts +2 -0
  89. package/dist/src/lib/dbStaffEventEmitter.d.ts.map +1 -1
  90. package/dist/src/lib/dbStaffEventEmitter.js +15 -0
  91. package/dist/src/lib/dbStaffEventEmitter.js.map +1 -1
  92. package/dist/src/lib/hostMemoryFormatting.d.ts +25 -0
  93. package/dist/src/lib/hostMemoryFormatting.d.ts.map +1 -0
  94. package/dist/src/lib/hostMemoryFormatting.js +55 -0
  95. package/dist/src/lib/hostMemoryFormatting.js.map +1 -0
  96. package/dist/src/lib/issueFacts.d.ts +37 -0
  97. package/dist/src/lib/issueFacts.d.ts.map +1 -0
  98. package/dist/src/lib/issueFacts.js +72 -0
  99. package/dist/src/lib/issueFacts.js.map +1 -0
  100. package/dist/src/lib/llm.d.ts +8 -0
  101. package/dist/src/lib/llm.d.ts.map +1 -1
  102. package/dist/src/lib/llm.js +33 -0
  103. package/dist/src/lib/llm.js.map +1 -1
  104. package/dist/src/lib/packageRoot.d.ts +2 -0
  105. package/dist/src/lib/packageRoot.d.ts.map +1 -0
  106. package/dist/src/lib/packageRoot.js +22 -0
  107. package/dist/src/lib/packageRoot.js.map +1 -0
  108. package/dist/src/lib/projectLearning.d.ts +21 -0
  109. package/dist/src/lib/projectLearning.d.ts.map +1 -0
  110. package/dist/src/lib/projectLearning.js +357 -0
  111. package/dist/src/lib/projectLearning.js.map +1 -0
  112. package/dist/src/lib/protocolEnforcement.d.ts +29 -0
  113. package/dist/src/lib/protocolEnforcement.d.ts.map +1 -0
  114. package/dist/src/lib/protocolEnforcement.js +124 -0
  115. package/dist/src/lib/protocolEnforcement.js.map +1 -0
  116. package/dist/src/lib/providers/claude.js +1 -1
  117. package/dist/src/lib/providers/claude.js.map +1 -1
  118. package/dist/src/lib/router.js +1 -1
  119. package/dist/src/lib/router.js.map +1 -1
  120. package/dist/src/lib/runtimeEnv.d.ts.map +1 -1
  121. package/dist/src/lib/runtimeEnv.js +8 -3
  122. package/dist/src/lib/runtimeEnv.js.map +1 -1
  123. package/dist/src/lib/scaffoldCloseout.d.ts +27 -0
  124. package/dist/src/lib/scaffoldCloseout.d.ts.map +1 -0
  125. package/dist/src/lib/scaffoldCloseout.js +139 -0
  126. package/dist/src/lib/scaffoldCloseout.js.map +1 -0
  127. package/dist/src/lib/semanticFactTags.d.ts +10 -0
  128. package/dist/src/lib/semanticFactTags.d.ts.map +1 -0
  129. package/dist/src/lib/semanticFactTags.js +166 -0
  130. package/dist/src/lib/semanticFactTags.js.map +1 -0
  131. package/dist/src/lib/sessionLedger.d.ts +94 -0
  132. package/dist/src/lib/sessionLedger.d.ts.map +1 -0
  133. package/dist/src/lib/sessionLedger.js +997 -0
  134. package/dist/src/lib/sessionLedger.js.map +1 -0
  135. package/dist/src/lib/sharedStateInvalidation.d.ts +10 -0
  136. package/dist/src/lib/sharedStateInvalidation.d.ts.map +1 -0
  137. package/dist/src/lib/sharedStateInvalidation.js +184 -0
  138. package/dist/src/lib/sharedStateInvalidation.js.map +1 -0
  139. package/dist/src/lib/staffEventsTable.d.ts +3 -0
  140. package/dist/src/lib/staffEventsTable.d.ts.map +1 -0
  141. package/dist/src/lib/staffEventsTable.js +58 -0
  142. package/dist/src/lib/staffEventsTable.js.map +1 -0
  143. package/dist/src/librarian/index.d.ts.map +1 -1
  144. package/dist/src/librarian/index.js +113 -2
  145. package/dist/src/librarian/index.js.map +1 -1
  146. package/dist/src/library/client.d.ts +6 -1
  147. package/dist/src/library/client.d.ts.map +1 -1
  148. package/dist/src/library/client.js +21 -7
  149. package/dist/src/library/client.js.map +1 -1
  150. package/dist/src/library/embeddings.d.ts +9 -1
  151. package/dist/src/library/embeddings.d.ts.map +1 -1
  152. package/dist/src/library/embeddings.js +28 -3
  153. package/dist/src/library/embeddings.js.map +1 -1
  154. package/dist/src/library/queries.d.ts.map +1 -1
  155. package/dist/src/library/queries.js +263 -46
  156. package/dist/src/library/queries.js.map +1 -1
  157. package/dist/src/sdk/index.d.ts +52 -1
  158. package/dist/src/sdk/index.d.ts.map +1 -1
  159. package/dist/src/sdk/index.js +546 -98
  160. package/dist/src/sdk/index.js.map +1 -1
  161. package/package.json +24 -3
  162. package/prisma/migrations/20260331101500_add_staff_events_ledger/migration.sql +24 -0
  163. package/prisma/schema.prisma +22 -0
package/README.md CHANGED
@@ -9,10 +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
- **Latest release:** [`v0.2.49`](https://github.com/nfemmanuel/iranti/releases/tag/v0.2.49)
12
+ **Repo version:** `0.3.0`
13
13
  Published packages:
14
- - `iranti@0.2.49`
15
- - `@iranti/sdk@0.2.49`
14
+ - npm `iranti@0.3.0`
15
+ - npm `@iranti/sdk@0.3.0`
16
+ - PyPI `iranti==0.3.0`
16
17
 
17
18
  ---
18
19
 
@@ -52,25 +53,29 @@ Vector databases answer "what's similar to X?" Iranti answers "what do we know a
52
53
 
53
54
  ## Benchmark Summary
54
55
 
55
- Iranti has now been rerun against a broader benchmark program covering 11 active capability tracks in `v0.2.21`. The current picture is stronger and narrower than the early validation story: exact, durable, shared memory is benchmark-backed; broad semantic-memory and autonomous-memory claims still need tighter boundaries.
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
57
 
57
58
  ### Confirmed Strengths
58
59
 
59
- - **Exact lookup (`iranti_query`)**: Retrieval remains exact and durable across genuine session and process breaks. At tested scale (`N=1938`, about `107k` tokens), the measured advantage is efficiency, not accuracy: Iranti answered `10/10` with zero haystack tokens while the baseline also answered `10/10` after reading the full document.
60
- - **Persistence across sessions**: Facts survive context-window loss and genuine process boundaries. `iranti_query` remained `8/8` across isolated session breaks in the rerun.
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.
61
63
  - **Conflict handling**: Reliable when confidence differentials are large and explicit.
62
- - **Multi-agent coordination**: 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.
63
65
  - **Provenance on writes**: Write-side attribution through stored source metadata is working and benchmark-confirmed.
64
- - **Ingest**: Prose extraction is accurate on clean entities in `v0.2.21`. Reliability under conflict-heavy transactional conditions should still be treated as a separate, narrower claim.
65
- - **Observe with hints**: `iranti_observe` recovers facts reliably when given the right entity hint, with higher-confidence facts returned first.
66
- - **Session recovery**: Interrupted-session recovery now performs substantially better than baseline.
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.
67
74
 
68
75
  ### Current Limits
69
76
 
70
- - **Search is lexical-first today, not semantic multi-hop retrieval.** In the current rerun, hop-value discovery was `0/4`; bare entity-token lookup worked, but `vectorScore` stayed `0` across results.
71
- - **`iranti_attend` is not yet a reliable autonomous classifier.** Natural-language attend classification still falls back to `classification_parse_failed_default_false`; `forceInject` works as an operator bypass, not as proof of autonomous injection.
72
- - **Observe performs better with explicit entity hints than with cold-start discovery.**
73
- - **Upgrade durability should be scoped carefully.** The `v0.2.21` upgrade procedure reinitialized the instance under test; do not assume KB data survives upgrades without an explicit preservation or migration path.
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.
74
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.
75
80
 
76
81
  ### Practical Position
@@ -78,9 +83,10 @@ Iranti has now been rerun against a broader benchmark program covering 11 active
78
83
  Iranti is strongest today as **structured memory infrastructure for multi-agent systems**:
79
84
  - exact entity/key lookup
80
85
  - durable shared memory
81
- - provenance-aware writes
86
+ - provenance-aware writes with source attribution and freshness timestamps
82
87
  - conflict-aware storage
83
88
  - session-aware recovery
89
+ - protocol-enforced turn discipline for trustworthy injection (opt-in strict mode)
84
90
 
85
91
  It should not yet be described as a fully general semantic-memory, semantic-search, or autonomous-memory-injection system.
86
92
 
@@ -465,6 +471,7 @@ iranti project init . --instance local --agent-id chatbot_main
465
471
  ```
466
472
 
467
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.
468
475
 
469
476
  Later changes use the same surface:
470
477
 
@@ -607,13 +614,16 @@ for item in matches:
607
614
 
608
615
  ### Context Persistence (attend)
609
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
+
610
619
  ```python
611
620
  # Before each LLM call, let Attendant decide if memory is needed
612
621
  result = client.attend(
613
622
  agent_id="research_agent_001",
614
623
  latest_message="What's Jane Smith's current affiliation?",
615
624
  current_context="User: What's Jane Smith's current affiliation?\nAssistant: Let me check...",
616
- max_facts=5
625
+ max_facts=5,
626
+ phase="pre-response"
617
627
  )
618
628
 
619
629
  if result["shouldInject"]:
@@ -729,6 +739,7 @@ middleware.after_receive(
729
739
  1. `before_send()` calls `attend()` with conversation context
730
740
  2. Forgotten facts are prepended as `[MEMORY: ...]`
731
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
732
743
 
733
744
  **Note**: Browser extensions are blocked by ChatGPT and Claude's Content Security Policy. Use API-based middleware instead.
734
745
 
@@ -757,9 +768,10 @@ Express server on port 3001 with endpoints:
757
768
  - `GET /kb/query/:entityType/:entityId/:key` - Query specific fact
758
769
  - `GET /kb/query/:entityType/:entityId` - Query all facts for entity
759
770
  - `GET /kb/search` - Hybrid search across facts
760
- - `POST /memory/attend` - Decide whether to inject memory for this turn
771
+ - `POST /memory/attend` - Decide whether to inject memory for this turn and unlock one discovery step for the current turn
761
772
  - `POST /memory/observe` - Context persistence (inject missing facts)
762
773
  - `POST /memory/handshake` - Working memory brief for agent session
774
+ - `GET /memory/ledger` - Read structured session ledger events from `staff_events`
763
775
  - `GET /memory/sessions` - List persisted operator-visible session checkpoints across agents, with optional operator filters/sorting
764
776
  - `GET /memory/session/:agentId` - Inspect the current persisted session checkpoint/recovery state
765
777
  - `POST /kb/relate` - Create entity relationship
@@ -767,6 +779,7 @@ Express server on port 3001 with endpoints:
767
779
  - `POST /agents/register` - Register agent in registry
768
780
 
769
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`.
770
783
 
771
784
  ---
772
785
 
@@ -27,7 +27,7 @@ async function main() {
27
27
  console.error('DATABASE_URL is required.');
28
28
  process.exit(1);
29
29
  }
30
- (0, client_1.initDb)(dbUrl);
30
+ (0, client_1.initDb)(dbUrl, { applicationName: 'iranti:script:api_key_create' });
31
31
  const created = await (0, apiKeys_1.createOrRotateApiKey)({
32
32
  keyId,
33
33
  owner,
@@ -9,7 +9,7 @@ async function main() {
9
9
  console.error('DATABASE_URL is required.');
10
10
  process.exit(1);
11
11
  }
12
- (0, client_1.initDb)(dbUrl);
12
+ (0, client_1.initDb)(dbUrl, { applicationName: 'iranti:script:api_key_list' });
13
13
  const keys = await (0, apiKeys_1.listApiKeys)();
14
14
  if (keys.length === 0) {
15
15
  console.log('No registry API keys found.');
@@ -20,7 +20,7 @@ async function main() {
20
20
  console.error('DATABASE_URL is required.');
21
21
  process.exit(1);
22
22
  }
23
- (0, client_1.initDb)(dbUrl);
23
+ (0, client_1.initDb)(dbUrl, { applicationName: 'iranti:script:api_key_revoke' });
24
24
  const revoked = await (0, apiKeys_1.revokeApiKey)(keyId);
25
25
  if (!revoked) {
26
26
  console.error(`API key not found: ${keyId}`);
@@ -9,7 +9,10 @@ const path_1 = __importDefault(require("path"));
9
9
  const createFirstPartyIranti_1 = require("../src/lib/createFirstPartyIranti");
10
10
  const runtimeEnv_1 = require("../src/lib/runtimeEnv");
11
11
  const staffEventRegistry_1 = require("../src/lib/staffEventRegistry");
12
+ const client_1 = require("../src/library/client");
12
13
  const autoRemember_1 = require("../src/lib/autoRemember");
14
+ const assistantCheckpoint_1 = require("../src/lib/assistantCheckpoint");
15
+ const hostMemoryFormatting_1 = require("../src/lib/hostMemoryFormatting");
13
16
  const MEMORY_NEED_POSITIVE_PATTERNS = [
14
17
  /\bwhat(?:'s| is| was)?\s+my\b/i,
15
18
  /\bdo you remember\b/i,
@@ -40,7 +43,7 @@ function printHelp() {
40
43
  '',
41
44
  'Reads Claude Code hook JSON from stdin and returns hookSpecificOutput.additionalContext on stdout.',
42
45
  'This helper retrieves working memory; durable KB writes still require explicit iranti_write/ingest calls.',
43
- 'Set IRANTI_AUTO_REMEMBER=true to auto-save narrow personal facts to IRANTI_PERSONAL_MEMORY_ENTITY/user/main and project summaries to IRANTI_MEMORY_ENTITY.',
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.',
44
47
  ].join('\n'));
45
48
  }
46
49
  function parseArgs(argv) {
@@ -228,19 +231,39 @@ function getMaxFacts() {
228
231
  return Math.min(12, Math.trunc(raw));
229
232
  }
230
233
  function formatSessionContext(facts, cwd) {
231
- 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
+ })));
232
238
  const lines = [
233
239
  '[Iranti Session Memory]',
234
240
  `Project: ${path_1.default.basename(cwd)}`,
241
+ 'REQUIRED: Call mcp__iranti__iranti_handshake before responding to the first user message.',
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.',
235
245
  ];
236
- if (limited.length > 0) {
237
- lines.push('Relevant memory:');
238
- for (const fact of limited) {
239
- lines.push(`- ${fact.entity}/${fact.key}: ${fact.summary}`);
240
- }
241
- }
246
+ const block = (0, hostMemoryFormatting_1.formatStructuredFactBlock)(limited, {
247
+ title: 'Iranti Session Facts',
248
+ introLines: [
249
+ 'Use these loaded facts as the starting working-memory frame for this session.',
250
+ 'Prefer them before re-inferring project state.',
251
+ 'Fact IDs are stable only within this block.',
252
+ ],
253
+ });
254
+ if (block)
255
+ lines.push(block);
242
256
  return lines.join('\n');
243
257
  }
258
+ function formatPreCompactContext() {
259
+ return [
260
+ '[Iranti Post-Compaction Recovery]',
261
+ 'Context was just compacted. BEFORE responding to the next user message:',
262
+ '1. Call mcp__iranti__iranti_handshake with the current task to restore working memory.',
263
+ '2. Do not respond until the handshake completes.',
264
+ 'REQUIRED: Call mcp__iranti__iranti_attend before every subsequent turn after that.',
265
+ ].join('\n');
266
+ }
244
267
  function extractSelfMemoryQueryKey(prompt) {
245
268
  const normalized = prompt.trim();
246
269
  if (!normalized)
@@ -258,18 +281,20 @@ function extractSelfMemoryQueryKey(prompt) {
258
281
  function formatPromptContext(facts, prompt) {
259
282
  if (facts.length === 0)
260
283
  return '';
261
- const lines = ['[Iranti Retrieved Memory]'];
284
+ const structuredFacts = (0, hostMemoryFormatting_1.assignStructuredFactIds)(facts.map((fact) => ({
285
+ ...fact,
286
+ entityKey: `${fact.entity}/${fact.key}`,
287
+ })));
288
+ const lines = [];
262
289
  const targetKey = prompt ? extractSelfMemoryQueryKey(prompt) : null;
263
290
  if (targetKey) {
264
- const answerCandidate = facts.find((fact) => (0, autoRemember_1.canonicalizeMemoryKey)(fact.key) === targetKey);
291
+ const answerCandidate = structuredFacts.find((fact) => (0, autoRemember_1.canonicalizeMemoryKey)(fact.entityKey.split('/').slice(2).join('/')) === targetKey);
265
292
  if (answerCandidate) {
266
- lines.push(`Direct answer: ${answerCandidate.summary}.`);
267
- lines.push('Use the direct answer above when it fully answers the user question.');
293
+ lines.push(`[Iranti Direct Answer]`);
294
+ lines.push(`Use ${answerCandidate.factId} directly if it fully answers the user question: ${answerCandidate.summary}.`);
268
295
  }
269
296
  }
270
- for (const fact of facts) {
271
- lines.push(`- ${fact.entity}/${fact.key}: ${fact.summary}`);
272
- }
297
+ lines.push((0, hostMemoryFormatting_1.formatStructuredFactBlock)(structuredFacts, { title: 'Iranti Retrieved Memory' }));
273
298
  return lines.join('\n');
274
299
  }
275
300
  function emitHookContext(event, additionalContext) {
@@ -285,6 +310,10 @@ function shouldFetchMemory(prompt) {
285
310
  const normalized = prompt.trim();
286
311
  if (!normalized)
287
312
  return false;
313
+ const explicitFacts = (0, autoRemember_1.extractExplicitPromptMemory)(normalized);
314
+ if (explicitFacts.some((fact) => fact.scope === 'project')) {
315
+ return false;
316
+ }
288
317
  if (MEMORY_NEED_NEGATIVE_PATTERNS.some((pattern) => pattern.test(normalized))) {
289
318
  return false;
290
319
  }
@@ -294,7 +323,12 @@ function shouldFetchMemory(prompt) {
294
323
  if (/\b(my|our|we)\b/i.test(normalized)) {
295
324
  return true;
296
325
  }
297
- return normalized.includes('/');
326
+ if (normalized.includes('/')) {
327
+ return true;
328
+ }
329
+ // Any substantive prompt in a project context should trigger memory fetch.
330
+ // Short acks and filler are already caught by MEMORY_NEED_NEGATIVE_PATTERNS.
331
+ return normalized.length > 20;
298
332
  }
299
333
  function dedupeFacts(facts) {
300
334
  const byKey = new Map();
@@ -351,6 +385,9 @@ async function buildHookAdditionalContext(options) {
351
385
  const cwd = getCwd(payload);
352
386
  const agent = await ensureHookAgent(iranti, payload);
353
387
  const entityHints = getEntityHints(payload);
388
+ if (event === 'PreCompact') {
389
+ return formatPreCompactContext();
390
+ }
354
391
  if (event === 'SessionStart') {
355
392
  const brief = await iranti.handshake({
356
393
  agent,
@@ -365,12 +402,37 @@ async function buildHookAdditionalContext(options) {
365
402
  }
366
403
  if (event === 'Stop') {
367
404
  const response = getLastAssistantMessage(payload);
368
- if (response && (0, autoRemember_1.isAutoRememberEnabled)()) {
369
- await (0, autoRemember_1.autoRememberAssistantFacts)({
405
+ if (response) {
406
+ await (0, autoRemember_1.rememberAssistantResponseFacts)({
370
407
  iranti,
371
408
  response,
372
409
  agent,
373
410
  source: 'ClaudeCodeHookStop',
411
+ ledgerContext: {
412
+ source: 'claude_hook',
413
+ host: 'claude_code',
414
+ },
415
+ });
416
+ const checkpoint = (0, assistantCheckpoint_1.extractAssistantCheckpointPayload)(response);
417
+ const projectEntity = (0, autoRemember_1.getProjectMemoryEntity)();
418
+ if (checkpoint && projectEntity && typeof iranti.checkpoint === 'function') {
419
+ await iranti.checkpoint({
420
+ agent,
421
+ task: buildSessionTask(payload),
422
+ recentMessages: getRecentMessages(payload),
423
+ checkpoint: {
424
+ ...checkpoint,
425
+ entityTargets: [projectEntity],
426
+ },
427
+ });
428
+ }
429
+ await iranti.attend({
430
+ agent,
431
+ latestMessage: response,
432
+ currentContext: response,
433
+ entityHints,
434
+ maxFacts: getMaxFacts(),
435
+ phase: 'post-response',
374
436
  });
375
437
  }
376
438
  return '';
@@ -385,6 +447,10 @@ async function buildHookAdditionalContext(options) {
385
447
  prompt,
386
448
  agent,
387
449
  source: 'ClaudeCodeHook',
450
+ ledgerContext: {
451
+ source: 'claude_hook',
452
+ host: 'claude_code',
453
+ },
388
454
  });
389
455
  }
390
456
  if (!shouldFetchMemory(prompt)) {
@@ -396,6 +462,7 @@ async function buildHookAdditionalContext(options) {
396
462
  currentContext: buildCurrentContext(payload, prompt),
397
463
  entityHints,
398
464
  maxFacts: getMaxFacts(),
465
+ phase: 'pre-response',
399
466
  });
400
467
  const facts = attend.facts.map((fact) => ({
401
468
  entity: fact.entityKey.split('/').slice(0, 2).join('/'),
@@ -413,8 +480,8 @@ async function main() {
413
480
  }
414
481
  const args = parseArgs(process.argv.slice(2));
415
482
  const event = args.event;
416
- if (event !== 'SessionStart' && event !== 'UserPromptSubmit' && event !== 'Stop') {
417
- throw new Error('--event must be SessionStart, UserPromptSubmit, or Stop');
483
+ if (event !== 'SessionStart' && event !== 'UserPromptSubmit' && event !== 'Stop' && event !== 'PreCompact') {
484
+ throw new Error('--event must be SessionStart, UserPromptSubmit, Stop, or PreCompact');
418
485
  }
419
486
  const payload = parsePayload(await readStdin());
420
487
  (0, runtimeEnv_1.loadRuntimeEnv)({
@@ -426,24 +493,43 @@ async function main() {
426
493
  const iranti = (0, createFirstPartyIranti_1.createFirstPartyIranti)({
427
494
  connectionString: requireConnectionString(),
428
495
  llmProvider: process.env.LLM_PROVIDER,
496
+ sessionLedgerSource: 'claude_hook',
497
+ sessionLedgerHost: 'claude_code',
429
498
  });
430
- const context = await buildHookAdditionalContext({
431
- iranti,
432
- event,
433
- payload,
434
- });
435
- if (!context) {
436
- await (0, staffEventRegistry_1.flushStaffEventEmitter)();
437
- process.exit(0);
499
+ let context = '';
500
+ try {
501
+ context = await buildHookAdditionalContext({
502
+ iranti,
503
+ event,
504
+ payload,
505
+ });
506
+ if (context) {
507
+ emitHookContext(event, context);
508
+ }
509
+ }
510
+ finally {
511
+ await (0, staffEventRegistry_1.flushStaffEventEmitter)().catch(() => undefined);
512
+ await (0, client_1.disconnectDb)().catch(() => undefined);
438
513
  }
439
- emitHookContext(event, context);
440
- await (0, staffEventRegistry_1.flushStaffEventEmitter)();
441
514
  process.exit(0);
442
515
  }
443
516
  if (require.main === module) {
444
517
  main().catch(async (error) => {
445
518
  try {
519
+ (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
520
+ staffComponent: 'Attendant',
521
+ actionType: 'host_failure',
522
+ agentId: process.env.IRANTI_CLAUDE_AGENT_ID?.trim() || 'claude_code',
523
+ source: 'claude_hook',
524
+ level: 'audit',
525
+ reason: error instanceof Error ? error.message : String(error),
526
+ metadata: {
527
+ host: 'claude_code',
528
+ operation: 'hook_execution',
529
+ },
530
+ });
446
531
  await (0, staffEventRegistry_1.flushStaffEventEmitter)();
532
+ await (0, client_1.disconnectDb)().catch(() => undefined);
447
533
  }
448
534
  catch { }
449
535
  console.error('[claude-code-memory-hook] fatal:', error instanceof Error ? error.message : String(error));
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const node_fs_1 = __importDefault(require("node:fs"));
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
8
  const commandInvocation_1 = require("../src/lib/commandInvocation");
9
+ const scaffoldCloseout_1 = require("../src/lib/scaffoldCloseout");
9
10
  function parseArgs(argv) {
10
11
  const options = {
11
12
  name: 'iranti',
@@ -82,7 +83,7 @@ function printHelp() {
82
83
  ' - Use --no-workspace-file only if you explicitly want global registration without project-local MCP file updates.',
83
84
  ' - Does not store DATABASE_URL in Codex config; iranti-mcp loads project/instance env at runtime.',
84
85
  ' - Replaces any existing MCP entry with the same name.',
85
- ' - Expected host pattern: call iranti_handshake at session start (or on the first user turn if startup hooks are unavailable), then call iranti_attend before each reply.',
86
+ ' - Expected host pattern: run iranti_handshake at session start, run iranti_attend before each reply and before/after knowledge discovery, checkpoint at natural pauses or interrupted work, and write/checkpoint confirmed findings.',
86
87
  ].join('\n'));
87
88
  }
88
89
  function run(command, args, cwd) {
@@ -164,6 +165,7 @@ function makeWorkspaceMcpServer(options, projectEnv) {
164
165
  IRANTI_PROJECT_ENV: projectEnv,
165
166
  IRANTI_MCP_DEFAULT_AGENT: options.agent,
166
167
  IRANTI_MCP_DEFAULT_SOURCE: options.source,
168
+ IRANTI_MCP_HOST: 'codex_cli',
167
169
  };
168
170
  if (options.provider) {
169
171
  env.LLM_PROVIDER = options.provider;
@@ -179,6 +181,7 @@ function makeVsCodeWorkspaceMcpServer(options, projectEnv) {
179
181
  const env = {
180
182
  IRANTI_MCP_DEFAULT_AGENT: options.agent,
181
183
  IRANTI_MCP_DEFAULT_SOURCE: options.source,
184
+ IRANTI_MCP_HOST: 'codex_vscode',
182
185
  };
183
186
  if (options.provider) {
184
187
  env.LLM_PROVIDER = options.provider;
@@ -197,6 +200,59 @@ function makeVsCodeWorkspaceMcpServer(options, projectEnv) {
197
200
  env,
198
201
  };
199
202
  }
203
+ function buildCodexAgentsBlock() {
204
+ return [
205
+ '<!-- iranti-rules -->',
206
+ '# Iranti MCP Protocol',
207
+ '',
208
+ 'IMPORTANT: These rules override Codex default behavior for this repo.',
209
+ '',
210
+ '## Session start',
211
+ '- Call `mcp__iranti__iranti_handshake` with the active task before responding to the first user message.',
212
+ '- If startup hooks are unavailable, do this on the first safe user turn instead of skipping it.',
213
+ '',
214
+ '## Discovery and reply ordering',
215
+ '- In short: call `mcp__iranti__iranti_attend` before each reply and before/after knowledge discovery.',
216
+ '1. Call `mcp__iranti__iranti_attend` before drafting each user-facing reply.',
217
+ '2. Call `mcp__iranti__iranti_attend` before using memory discovery tools such as search, query, or related reads.',
218
+ '3. Call `mcp__iranti__iranti_attend` again after discovery when the findings may change what should be injected, written, or checkpointed.',
219
+ '',
220
+ '## Confirmed findings',
221
+ '- Call `mcp__iranti__iranti_write` after confirmed durable findings such as decisions, blockers, next steps, stable constraints, validated environment details, what worked, what failed, and what remains risky.',
222
+ '- If the work is still in progress, pair those writes with `mcp__iranti__iranti_checkpoint` so another session can resume cleanly.',
223
+ '',
224
+ '## Checkpoint discipline',
225
+ '- Call `mcp__iranti__iranti_checkpoint` at natural pauses, before stepping away from long work, when interrupted, and when completing a useful slice.',
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
+ '- 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.',
229
+ '',
230
+ '## Host setup check',
231
+ '- If this block was missing at session start, rerun `iranti codex-setup` from the bound project root.',
232
+ '<!-- /iranti-rules -->',
233
+ '',
234
+ ].join('\n');
235
+ }
236
+ function writeWorkspaceAgentsFile(projectEnv) {
237
+ const projectPath = node_path_1.default.dirname(projectEnv);
238
+ const agentsFile = node_path_1.default.join(projectPath, 'AGENTS.md');
239
+ const irantiBlock = buildCodexAgentsBlock();
240
+ if (!node_fs_1.default.existsSync(agentsFile)) {
241
+ node_fs_1.default.writeFileSync(agentsFile, irantiBlock, 'utf8');
242
+ return { filePath: agentsFile, status: 'created' };
243
+ }
244
+ const existing = node_fs_1.default.readFileSync(agentsFile, 'utf8');
245
+ if (!existing.includes('<!-- iranti-rules -->')) {
246
+ node_fs_1.default.writeFileSync(agentsFile, `${existing.trimEnd()}\n\n${irantiBlock}`, 'utf8');
247
+ return { filePath: agentsFile, status: 'updated' };
248
+ }
249
+ const replaced = existing.replace(/<!-- iranti-rules -->[\s\S]*?<!-- \/iranti-rules -->/, irantiBlock.trim());
250
+ if (replaced === existing) {
251
+ return { filePath: agentsFile, status: 'unchanged' };
252
+ }
253
+ node_fs_1.default.writeFileSync(agentsFile, replaced, 'utf8');
254
+ return { filePath: agentsFile, status: 'updated' };
255
+ }
200
256
  function writeWorkspaceMcpFile(projectEnv, options) {
201
257
  const projectPath = node_path_1.default.dirname(projectEnv);
202
258
  const mcpFile = node_path_1.default.join(projectPath, '.mcp.json');
@@ -292,7 +348,7 @@ function ensureCodexInstalled(repoRoot) {
292
348
  throw new Error('Codex CLI is not installed or not on PATH. Install Codex first, confirm `codex --version` works, then rerun `iranti codex-setup`.');
293
349
  }
294
350
  }
295
- function main() {
351
+ async function main() {
296
352
  const options = parseArgs(process.argv.slice(2));
297
353
  const repoRoot = findPackageRoot(__dirname);
298
354
  const mcpScript = node_path_1.default.join(repoRoot, 'dist', 'scripts', 'iranti-mcp.js');
@@ -313,6 +369,8 @@ function main() {
313
369
  `IRANTI_MCP_DEFAULT_AGENT=${options.agent}`,
314
370
  '--env',
315
371
  `IRANTI_MCP_DEFAULT_SOURCE=${options.source}`,
372
+ '--env',
373
+ 'IRANTI_MCP_HOST=codex_cli',
316
374
  ];
317
375
  const projectEnv = resolveProjectEnv(options);
318
376
  if (projectEnv) {
@@ -335,13 +393,19 @@ function main() {
335
393
  ? {
336
394
  mcp: writeWorkspaceMcpFile(workspaceProjectEnv, options),
337
395
  vscode: writeWorkspaceVsCodeMcpFile(workspaceProjectEnv, options),
396
+ agents: writeWorkspaceAgentsFile(workspaceProjectEnv),
338
397
  }
339
398
  : null;
340
399
  const registered = run('codex', ['mcp', 'get', options.name], repoRoot);
341
400
  console.log(registered);
342
401
  console.log('');
343
402
  console.log('Codex is now configured to use Iranti through MCP.');
344
- console.log('Recommended MCP host pattern: run iranti_handshake at session start (or on the first user turn if no startup hook exists), then run iranti_attend before each reply.');
403
+ console.log('Required host pattern:');
404
+ console.log(' 1. Run iranti_handshake at session start (or on the first safe user turn if startup hooks are unavailable).');
405
+ console.log(' 2. Run iranti_attend before each reply and before/after knowledge discovery.');
406
+ console.log(' 3. Run iranti_checkpoint at natural pauses, during interrupted work, and when completing a useful slice.');
407
+ console.log(' 4. Include key commands, tests, validations, and decisions in checkpoint actions when they matter to later recovery.');
408
+ console.log(' 5. Run iranti_write for confirmed durable findings, and pair ongoing work with iranti_checkpoint.');
345
409
  if (useInstalled) {
346
410
  console.log('Registration target: installed CLI (`iranti mcp`)');
347
411
  if (projectEnv) {
@@ -365,13 +429,31 @@ function main() {
365
429
  }
366
430
  if (options.writeWorkspaceFile) {
367
431
  if (workspaceFilesResult) {
432
+ const boundProjectEnv = workspaceProjectEnv;
368
433
  console.log(`Workspace .mcp.json: ${workspaceFilesResult.mcp.status} (${workspaceFilesResult.mcp.filePath})`);
369
434
  console.log(`Workspace .vscode/mcp.json: ${workspaceFilesResult.vscode.status} (${workspaceFilesResult.vscode.filePath})`);
435
+ console.log(`Workspace AGENTS.md: ${workspaceFilesResult.agents.status} (${workspaceFilesResult.agents.filePath})`);
436
+ const closeout = await (0, scaffoldCloseout_1.writeProjectScaffoldCloseout)({
437
+ tool: 'codex',
438
+ projectPath: node_path_1.default.dirname(boundProjectEnv),
439
+ projectEnvFile: boundProjectEnv,
440
+ files: [
441
+ { path: workspaceFilesResult.mcp.filePath, status: workspaceFilesResult.mcp.status },
442
+ { path: workspaceFilesResult.vscode.filePath, status: workspaceFilesResult.vscode.status },
443
+ { path: workspaceFilesResult.agents.filePath, status: workspaceFilesResult.agents.status },
444
+ ],
445
+ agentId: options.agent || 'codex_code',
446
+ });
447
+ console.log(`Shared memory closeout: ${closeout.status} (${closeout.detail})`);
370
448
  }
371
449
  else {
372
450
  console.log('Workspace MCP files: unchanged (no project binding found from the current working directory)');
451
+ console.log('Shared memory closeout: skipped (no bound workspace project was found)');
373
452
  }
374
453
  }
375
454
  }
376
- main();
455
+ main().catch((error) => {
456
+ console.error(error instanceof Error ? error.message : String(error));
457
+ process.exit(1);
458
+ });
377
459
  //# sourceMappingURL=codex-setup.js.map