@ouro.bot/friends 0.1.0-alpha.4

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 (82) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +514 -0
  3. package/changelog.json +34 -0
  4. package/dist/a2a/index.d.ts +102 -0
  5. package/dist/a2a/index.js +198 -0
  6. package/dist/agent-peer.d.ts +17 -0
  7. package/dist/agent-peer.js +57 -0
  8. package/dist/channel.d.ts +11 -0
  9. package/dist/channel.js +132 -0
  10. package/dist/consent.d.ts +34 -0
  11. package/dist/consent.js +62 -0
  12. package/dist/coordination.d.ts +100 -0
  13. package/dist/coordination.js +255 -0
  14. package/dist/file-bundle.d.ts +12 -0
  15. package/dist/file-bundle.js +23 -0
  16. package/dist/grant-store-file.d.ts +16 -0
  17. package/dist/grant-store-file.js +136 -0
  18. package/dist/grant-store.d.ts +7 -0
  19. package/dist/grant-store.js +8 -0
  20. package/dist/grants.d.ts +39 -0
  21. package/dist/grants.js +84 -0
  22. package/dist/group-context.d.ts +21 -0
  23. package/dist/group-context.js +144 -0
  24. package/dist/index.d.ts +49 -0
  25. package/dist/index.js +105 -0
  26. package/dist/link-identity.d.ts +14 -0
  27. package/dist/link-identity.js +88 -0
  28. package/dist/mcp/bin.d.ts +2 -0
  29. package/dist/mcp/bin.js +16 -0
  30. package/dist/mcp/dispatch.d.ts +14 -0
  31. package/dist/mcp/dispatch.js +432 -0
  32. package/dist/mcp/index.d.ts +6 -0
  33. package/dist/mcp/index.js +14 -0
  34. package/dist/mcp/run-main.d.ts +7 -0
  35. package/dist/mcp/run-main.js +45 -0
  36. package/dist/mcp/schemas.d.ts +10 -0
  37. package/dist/mcp/schemas.js +398 -0
  38. package/dist/mcp/server.d.ts +21 -0
  39. package/dist/mcp/server.js +194 -0
  40. package/dist/mission-share.d.ts +94 -0
  41. package/dist/mission-share.js +232 -0
  42. package/dist/mission-store-file.d.ts +18 -0
  43. package/dist/mission-store-file.js +153 -0
  44. package/dist/mission-store.d.ts +10 -0
  45. package/dist/mission-store.js +9 -0
  46. package/dist/missions.d.ts +31 -0
  47. package/dist/missions.js +98 -0
  48. package/dist/notes.d.ts +11 -0
  49. package/dist/notes.js +90 -0
  50. package/dist/observability.d.ts +27 -0
  51. package/dist/observability.js +31 -0
  52. package/dist/outcomes.d.ts +9 -0
  53. package/dist/outcomes.js +51 -0
  54. package/dist/resolver.d.ts +28 -0
  55. package/dist/resolver.js +187 -0
  56. package/dist/results.d.ts +8 -0
  57. package/dist/results.js +2 -0
  58. package/dist/room.d.ts +22 -0
  59. package/dist/room.js +40 -0
  60. package/dist/share.d.ts +106 -0
  61. package/dist/share.js +223 -0
  62. package/dist/standing.d.ts +83 -0
  63. package/dist/standing.js +111 -0
  64. package/dist/store-file.d.ts +21 -0
  65. package/dist/store-file.js +264 -0
  66. package/dist/store.d.ts +9 -0
  67. package/dist/store.js +4 -0
  68. package/dist/tokens.d.ts +8 -0
  69. package/dist/tokens.js +26 -0
  70. package/dist/trust-explanation.d.ts +16 -0
  71. package/dist/trust-explanation.js +74 -0
  72. package/dist/trust-mutation.d.ts +4 -0
  73. package/dist/trust-mutation.js +29 -0
  74. package/dist/types.d.ts +164 -0
  75. package/dist/types.js +51 -0
  76. package/dist/util/cap-string.d.ts +7 -0
  77. package/dist/util/cap-string.js +35 -0
  78. package/dist/verifier.d.ts +11 -0
  79. package/dist/verifier.js +29 -0
  80. package/dist/whoami.d.ts +7 -0
  81. package/dist/whoami.js +39 -0
  82. package/package.json +68 -0
@@ -0,0 +1,94 @@
1
+ import type { MissionStore } from "./mission-store";
2
+ import type { FriendStore } from "./store";
3
+ import type { GrantStore } from "./grant-store";
4
+ import type { AgentAttribution, MissionRecord, RelationshipOutcome, TrustLevel } from "./types";
5
+ import type { ConsentPolicy } from "./consent";
6
+ import type { AgentVerifier } from "./verifier";
7
+ /** A learning as carried on the wire: its value plus who FIRST asserted it
8
+ * (`originallyAssertedBy`), so the consumer can attribute it without laundering
9
+ * an imported fact into first-party. The mission analogue of `SharedNote`. */
10
+ export interface SharedLearning {
11
+ key: string;
12
+ value: string;
13
+ originallyAssertedBy?: AgentAttribution;
14
+ }
15
+ /** The cross-agent mission-share envelope. Names the subject by JOIN KEY
16
+ * (`missionKey`) + title only — NEVER a local UUID. A SIBLING of
17
+ * `ProfileShareEnvelope` (Fork A), not a widening. */
18
+ export interface MissionShareEnvelope {
19
+ /** The mission, named by its join key — `missionKey` + a human title. */
20
+ subject: {
21
+ missionKey: string;
22
+ title: string;
23
+ };
24
+ /** The agent that produced this envelope (its join-key agentId). */
25
+ fromAgentId: string;
26
+ scope: "mission" | "outcomes";
27
+ /** Scope-filtered relationship outcomes (present for the `outcomes` scope). */
28
+ outcomes?: RelationshipOutcome[];
29
+ /** Scope-filtered shareable learnings (present for the `mission` scope). */
30
+ learnings?: SharedLearning[];
31
+ /** Opaque, verifier-specific proof slot. The TOFU verifier ignores it. */
32
+ proof?: string;
33
+ issuedAt: string;
34
+ }
35
+ export interface PrepareMissionShareInput {
36
+ /** The LOCAL mission to share, by its local UUID id (resolved via the store). */
37
+ missionId: string;
38
+ /** The recipient agent's join-key agentId. */
39
+ toAgentId: string;
40
+ scope: "mission" | "outcomes";
41
+ /** This agent's own join-key agentId — the original asserter of first-party
42
+ * learnings (so a shared first-party learning is attributed to self). */
43
+ selfAgentId: string;
44
+ /** Optional proof to stamp on the envelope (for a non-TOFU recipient verifier). */
45
+ proof?: string;
46
+ }
47
+ export type PrepareMissionShareStatus = "not_found" | "no_consent" | "no_recipient";
48
+ export type PrepareMissionShareResult = {
49
+ ok: true;
50
+ envelope: MissionShareEnvelope;
51
+ } | {
52
+ ok: false;
53
+ status: PrepareMissionShareStatus;
54
+ };
55
+ /**
56
+ * Producer half of the mission ledger. Consent-gated (subject = the mission's
57
+ * `missionKey`), scope-filtered, provenance-preserving. Names the mission by its
58
+ * join key, never the local UUID. The recipient's trust — read off this agent's
59
+ * own friend record for `toAgentId` — is the authorization input the policy uses.
60
+ */
61
+ export declare function prepareMissionShare(missions: MissionStore, store: FriendStore, grants: GrantStore, input: PrepareMissionShareInput, consent?: ConsentPolicy): Promise<PrepareMissionShareResult>;
62
+ export interface ImportMissionShareInput {
63
+ envelope: MissionShareEnvelope;
64
+ /** The agent the envelope arrived from (its join-key agentId). */
65
+ fromAgentId: string;
66
+ /** This agent's resolved trust in the source agent — the cap on acceptance. */
67
+ trustOfSource: TrustLevel;
68
+ }
69
+ export type ImportMissionShareStatus = "imported" | "seeded" | "no_mission" | "untrusted_source" | "untrusted_introduction";
70
+ export type ImportMissionShareResult = {
71
+ ok: true;
72
+ status: "imported" | "seeded";
73
+ record: MissionRecord;
74
+ } | {
75
+ ok: false;
76
+ status: "no_mission" | "untrusted_source" | "untrusted_introduction";
77
+ };
78
+ export interface ImportMissionShareOptions {
79
+ /** Authentication seam. Defaults to TOFU. Authorization (trust) is still
80
+ * applied regardless of what the verifier says. */
81
+ verifier?: AgentVerifier;
82
+ /** Minimum trust a source must hold for its facts to be accepted at all.
83
+ * Default `acquaintance`: a stranger source is refused. */
84
+ minTrustToAccept?: TrustLevel;
85
+ }
86
+ /**
87
+ * Consumer half of the mission ledger — the non-clobbering merge. Resolves the
88
+ * mission by `findByMissionKey`; lands imported learnings in the
89
+ * `importedLearnings` namespace WITHOUT touching first-party `learnings`;
90
+ * append-merges + dedupes imported outcomes; NEVER recomputes status /
91
+ * participants; the source agent's trust caps acceptance; seeds an unknown
92
+ * mission only when a friend/family peer introduces it.
93
+ */
94
+ export declare function importMissionShare(missions: MissionStore, input: ImportMissionShareInput, options?: ImportMissionShareOptions): Promise<ImportMissionShareResult>;
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareMissionShare = prepareMissionShare;
4
+ exports.importMissionShare = importMissionShare;
5
+ // prepareMissionShare (producer) + importMissionShare (consumer) — the shared
6
+ // mission ledger (brick 3). Structural twins of share.ts's profile producer /
7
+ // consumer, re-aimed from a PERSON at a MISSION.
8
+ //
9
+ // Two DIFFERENT agents agreeing they did the SAME mission (by `missionKey`) AND
10
+ // sharing what they collectively learned — WITH CONSENT, without first-party
11
+ // learnings being clobbered. Store-only + transport-agnostic: prepareMissionShare
12
+ // returns an envelope, importMissionShare consumes one; the WIRE is the caller's
13
+ // job. Pure — the only node builtin is `node:crypto`, mirroring share.ts.
14
+ //
15
+ // Through-line invariants (every one is tested in mission-share.test.ts):
16
+ // - the mission is named by its JOIN KEY (`missionKey`), NEVER the local UUID;
17
+ // - the share is consent-gated (subject = the missionKey) and scope-filtered;
18
+ // - imported learnings NEVER touch first-party `learnings` (a separate
19
+ // `importedLearnings` namespace) — first-party always wins, structurally;
20
+ // - the source agent's trust CAPS acceptance (a stranger peer is refused);
21
+ // - `status` / `participants` are NEVER recomputed from an import (non-transitive);
22
+ // - imported outcomes are append-merged, stamped `origin:imported`, and deduped
23
+ // by (missionId, timestamp, assertedBy.agentId) — same peer idempotent,
24
+ // different peers coexist;
25
+ // - an unknown mission may be SEEDED only by a friend/family introducing peer.
26
+ const node_crypto_1 = require("node:crypto");
27
+ const observability_1 = require("./observability");
28
+ const consent_1 = require("./consent");
29
+ const verifier_1 = require("./verifier");
30
+ const share_1 = require("./share");
31
+ /** Build the scope-filtered shared learnings: only `shareable` learnings, each
32
+ * attributed to its original asserter (self for a first-party learning, the
33
+ * recorded original asserter for a relayed import). Reuses `originalAsserterOf`
34
+ * from share.ts — single-sourced, no duplicate coverage surface. */
35
+ function buildSharedLearnings(record, selfAgentId) {
36
+ return Object.entries(record.learnings)
37
+ .filter(([, learning]) => learning.shareable === true)
38
+ .map(([key, learning]) => ({
39
+ key,
40
+ value: learning.value,
41
+ originallyAssertedBy: (0, share_1.originalAsserterOf)(learning, selfAgentId),
42
+ }));
43
+ }
44
+ /**
45
+ * Producer half of the mission ledger. Consent-gated (subject = the mission's
46
+ * `missionKey`), scope-filtered, provenance-preserving. Names the mission by its
47
+ * join key, never the local UUID. The recipient's trust — read off this agent's
48
+ * own friend record for `toAgentId` — is the authorization input the policy uses.
49
+ */
50
+ async function prepareMissionShare(missions, store, grants, input, consent = consent_1.DEFAULT_CONSENT_POLICY) {
51
+ const record = await missions.get(input.missionId);
52
+ if (!record) {
53
+ return { ok: false, status: "not_found" };
54
+ }
55
+ // The recipient's trust is read from this agent's own knowledge of it (the
56
+ // a2a-agent friend record for toAgentId). An unknown recipient defaults to
57
+ // stranger — never trusted by default.
58
+ const recipientRecord = await store.findByExternalId("a2a-agent", input.toAgentId);
59
+ const recipientTrust = recipientRecord?.trustLevel ?? "stranger";
60
+ const recipient = { agentId: input.toAgentId, trustLevel: recipientTrust };
61
+ // The subject is the mission, keyed by its missionKey (a mission is just
62
+ // another grant subject under the Fork-D opaque subject key).
63
+ const consented = await consent.consents({
64
+ subjectKey: record.missionKey,
65
+ recipient,
66
+ scope: input.scope,
67
+ grants,
68
+ });
69
+ if (!consented) {
70
+ return { ok: false, status: "no_consent" };
71
+ }
72
+ const now = new Date().toISOString();
73
+ const envelope = {
74
+ subject: { missionKey: record.missionKey, title: record.title },
75
+ fromAgentId: input.selfAgentId,
76
+ scope: input.scope,
77
+ issuedAt: now,
78
+ ...(input.proof !== undefined ? { proof: input.proof } : {}),
79
+ ...(input.scope === "outcomes"
80
+ ? { outcomes: record.outcomes }
81
+ : { learnings: buildSharedLearnings(record, input.selfAgentId) }),
82
+ };
83
+ (0, observability_1.emitNervesEvent)({
84
+ component: "friends",
85
+ event: "friends.mission_share_prepared",
86
+ message: "prepared mission share envelope",
87
+ meta: { scope: input.scope, toAgentId: input.toAgentId, consentPolicy: consent.name },
88
+ });
89
+ return { ok: true, envelope };
90
+ }
91
+ // ── Consumer ──
92
+ /** Trust levels a peer must hold to INTRODUCE a previously-unknown mission. A
93
+ * friend/family peer may seed a new mission; a stranger / acquaintance peer may
94
+ * not (mirrors the person path's SEEDING_TRUST). */
95
+ const SEEDING_TRUST = new Set(["family", "friend"]);
96
+ const TRUST_RANK = { family: 4, friend: 3, acquaintance: 2, stranger: 1 };
97
+ function importedLearningFrom(learning, fromAgentId, now) {
98
+ return {
99
+ value: learning.value,
100
+ importedAt: now,
101
+ assertedBy: { agentId: fromAgentId },
102
+ ...(learning.originallyAssertedBy ? { originallyAssertedBy: learning.originallyAssertedBy } : {}),
103
+ };
104
+ }
105
+ /** Merge the envelope's learnings into the record's `importedLearnings` namespace
106
+ * under the source agentId. First-party `learnings` are NOT passed in and are
107
+ * physically untouched. Within the namespace, the newest import wins per key. */
108
+ function mergeImportedLearnings(record, learnings, fromAgentId, now) {
109
+ const existing = record.importedLearnings ?? {};
110
+ const forAgent = { ...(existing[fromAgentId] ?? {}) };
111
+ for (const learning of learnings) {
112
+ forAgent[learning.key] = importedLearningFrom(learning, fromAgentId, now);
113
+ }
114
+ return { ...existing, [fromAgentId]: forAgent };
115
+ }
116
+ /** Append the envelope's outcomes to the record's outcomes, stamping each with
117
+ * `origin:imported` + the source attribution + importedAt, and DEDUPING by
118
+ * (missionId, timestamp, assertedBy.agentId): a row whose identity already exists
119
+ * is skipped (same peer idempotent); a row from a different peer with the same
120
+ * (missionId, timestamp) coexists. The existing rows' `assertedBy?.agentId` is
121
+ * read defensively — a first-party outcome may carry no provenance, and must
122
+ * never be spuriously matched. This outcome-merge is genuinely NEW logic (the
123
+ * person path's import never merged outcomes). */
124
+ function mergeImportedOutcomes(existing, incoming, fromAgentId, now) {
125
+ const identityOf = (o, assertedAgentId) => JSON.stringify([o.missionId, o.timestamp, assertedAgentId]);
126
+ const seen = new Set(existing.map((o) => identityOf(o, o.provenance?.assertedBy?.agentId)));
127
+ const merged = [...existing];
128
+ for (const o of incoming) {
129
+ const identity = identityOf(o, fromAgentId);
130
+ if (seen.has(identity))
131
+ continue;
132
+ seen.add(identity);
133
+ merged.push({
134
+ missionId: o.missionId,
135
+ result: o.result,
136
+ timestamp: o.timestamp,
137
+ ...(o.note ? { note: o.note } : {}),
138
+ provenance: { origin: "imported", assertedBy: { agentId: fromAgentId }, importedAt: now },
139
+ });
140
+ }
141
+ return merged;
142
+ }
143
+ /** Create a freshly-seeded mission for a previously-unknown key. Always
144
+ * `status:"active"`, empty first-party `learnings`, carrying the subject's join
145
+ * key + title. */
146
+ function seedMission(envelope, now) {
147
+ return {
148
+ id: (0, node_crypto_1.randomUUID)(),
149
+ missionKey: envelope.subject.missionKey,
150
+ title: envelope.subject.title,
151
+ status: "active",
152
+ participants: [],
153
+ outcomes: [],
154
+ learnings: {},
155
+ importedLearnings: {},
156
+ createdAt: now,
157
+ updatedAt: now,
158
+ schemaVersion: 1,
159
+ };
160
+ }
161
+ /** Apply an envelope's payload to a mission record WITHOUT recomputing its
162
+ * `status` or `participants` (non-transitive) and WITHOUT touching first-party
163
+ * `learnings`. Only `importedLearnings`, `outcomes`, and `updatedAt` change. */
164
+ function applyEnvelopeToMission(record, envelope, fromAgentId, now) {
165
+ const importedLearnings = envelope.learnings && envelope.learnings.length > 0
166
+ ? mergeImportedLearnings(record, envelope.learnings, fromAgentId, now)
167
+ : record.importedLearnings;
168
+ const outcomes = envelope.outcomes && envelope.outcomes.length > 0
169
+ ? mergeImportedOutcomes(record.outcomes, envelope.outcomes, fromAgentId, now)
170
+ : record.outcomes;
171
+ return {
172
+ ...record,
173
+ // status / participants are intentionally NOT recomputed — an import must
174
+ // never flip the mission's status (the non-transitive invariant).
175
+ ...(importedLearnings ? { importedLearnings } : {}),
176
+ outcomes,
177
+ updatedAt: now,
178
+ };
179
+ }
180
+ /**
181
+ * Consumer half of the mission ledger — the non-clobbering merge. Resolves the
182
+ * mission by `findByMissionKey`; lands imported learnings in the
183
+ * `importedLearnings` namespace WITHOUT touching first-party `learnings`;
184
+ * append-merges + dedupes imported outcomes; NEVER recomputes status /
185
+ * participants; the source agent's trust caps acceptance; seeds an unknown
186
+ * mission only when a friend/family peer introduces it.
187
+ */
188
+ async function importMissionShare(missions, input, options = {}) {
189
+ const verifier = options.verifier ?? verifier_1.DEFAULT_AGENT_VERIFIER;
190
+ const minTrust = options.minTrustToAccept ?? "acquaintance";
191
+ // Authentication (caller's seam) AND authorization (trust ladder) must BOTH
192
+ // pass. The verifier authenticates the wire; the trust cap is the package's own
193
+ // gate and applies regardless of what the verifier returns.
194
+ const authenticated = verifier.verify(input.fromAgentId, input.envelope.proof);
195
+ const trustedEnough = TRUST_RANK[input.trustOfSource] >= TRUST_RANK[minTrust];
196
+ if (!authenticated || !trustedEnough) {
197
+ (0, observability_1.emitNervesEvent)({
198
+ component: "friends",
199
+ event: "friends.mission_share_refused",
200
+ message: "refused mission share from untrusted source",
201
+ meta: { fromAgentId: input.fromAgentId, trustOfSource: input.trustOfSource, authenticated },
202
+ });
203
+ return { ok: false, status: "untrusted_source" };
204
+ }
205
+ const now = new Date().toISOString();
206
+ const existing = await missions.findByMissionKey(input.envelope.subject.missionKey);
207
+ if (!existing) {
208
+ // Unknown mission. Only a friend/family peer may seed a new one.
209
+ if (!SEEDING_TRUST.has(input.trustOfSource)) {
210
+ return { ok: false, status: "untrusted_introduction" };
211
+ }
212
+ const seeded = seedMission(input.envelope, now);
213
+ const withPayload = applyEnvelopeToMission(seeded, input.envelope, input.fromAgentId, now);
214
+ await missions.put(withPayload.id, withPayload);
215
+ (0, observability_1.emitNervesEvent)({
216
+ component: "friends",
217
+ event: "friends.mission_share_seeded",
218
+ message: "seeded new mission from mission share",
219
+ meta: { missionId: withPayload.id, fromAgentId: input.fromAgentId },
220
+ });
221
+ return { ok: true, status: "seeded", record: withPayload };
222
+ }
223
+ const updated = applyEnvelopeToMission(existing, input.envelope, input.fromAgentId, now);
224
+ await missions.put(updated.id, updated);
225
+ (0, observability_1.emitNervesEvent)({
226
+ component: "friends",
227
+ event: "friends.mission_share_imported",
228
+ message: "imported mission share into existing mission",
229
+ meta: { missionId: updated.id, fromAgentId: input.fromAgentId, scope: input.envelope.scope },
230
+ });
231
+ return { ok: true, status: "imported", record: updated };
232
+ }
@@ -0,0 +1,18 @@
1
+ import type { MissionStore } from "./mission-store";
2
+ import type { MissionRecord } from "./types";
3
+ /** The sibling missions directory for a given friends directory:
4
+ * `<friendsDir>/_missions`. The collection lives UNDER the friends dir (a
5
+ * reserved `_`-prefixed subdir) so a single `--dir` still points the whole
6
+ * substrate at one place. */
7
+ export declare function missionsDirFor(friendsDir: string): string;
8
+ export declare class FileMissionStore implements MissionStore {
9
+ private readonly missionsPath;
10
+ constructor(missionsPath: string);
11
+ get(id: string): Promise<MissionRecord | null>;
12
+ put(id: string, mission: MissionRecord): Promise<void>;
13
+ delete(id: string): Promise<void>;
14
+ findByMissionKey(missionKey: string): Promise<MissionRecord | null>;
15
+ listAll(): Promise<MissionRecord[]>;
16
+ private normalize;
17
+ private readJson;
18
+ }
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ // FileMissionStore — filesystem adapter for MissionStore (brick 3).
3
+ // Stores each MissionRecord as one JSON file in a sibling `_missions/` collection
4
+ // next to the friends directory. Mirrors FileGrantStore's structure (mkdir on
5
+ // construct, one file per record, guarded reads, a `normalize` for round-trip
6
+ // discipline) so the three stores feel uniform.
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.FileMissionStore = void 0;
42
+ exports.missionsDirFor = missionsDirFor;
43
+ const fs = __importStar(require("fs"));
44
+ const fsPromises = __importStar(require("fs/promises"));
45
+ const path = __importStar(require("path"));
46
+ const observability_1 = require("./observability");
47
+ const MISSION_STATUSES = new Set([
48
+ "active",
49
+ "succeeded",
50
+ "partial",
51
+ "failed",
52
+ "abandoned",
53
+ ]);
54
+ /** The sibling missions directory for a given friends directory:
55
+ * `<friendsDir>/_missions`. The collection lives UNDER the friends dir (a
56
+ * reserved `_`-prefixed subdir) so a single `--dir` still points the whole
57
+ * substrate at one place. */
58
+ function missionsDirFor(friendsDir) {
59
+ return path.join(friendsDir, "_missions");
60
+ }
61
+ class FileMissionStore {
62
+ missionsPath;
63
+ constructor(missionsPath) {
64
+ this.missionsPath = missionsPath;
65
+ fs.mkdirSync(missionsPath, { recursive: true });
66
+ (0, observability_1.emitNervesEvent)({
67
+ component: "friends",
68
+ event: "friends.mission_store_init",
69
+ message: "file mission store initialized",
70
+ meta: {},
71
+ });
72
+ }
73
+ async get(id) {
74
+ const mission = await this.readJson(path.join(this.missionsPath, `${id}.json`));
75
+ return mission ? this.normalize(mission) : null;
76
+ }
77
+ async put(id, mission) {
78
+ await fsPromises.writeFile(path.join(this.missionsPath, `${id}.json`), JSON.stringify(this.normalize(mission), null, 2), "utf-8");
79
+ }
80
+ async delete(id) {
81
+ try {
82
+ await fsPromises.unlink(path.join(this.missionsPath, `${id}.json`));
83
+ }
84
+ catch (err) {
85
+ if (err?.code === "ENOENT")
86
+ return;
87
+ throw err;
88
+ }
89
+ }
90
+ async findByMissionKey(missionKey) {
91
+ const all = await this.listAll();
92
+ return all.find((m) => m.missionKey === missionKey) ?? null;
93
+ }
94
+ async listAll() {
95
+ let entries;
96
+ try {
97
+ entries = await fsPromises.readdir(this.missionsPath);
98
+ }
99
+ catch {
100
+ /* v8 ignore next -- defensive: dir is mkdir'd in the constructor, so readdir
101
+ only throws if it's deleted mid-run; unreachable through the API @preserve */
102
+ return [];
103
+ }
104
+ const missions = [];
105
+ for (const entry of entries) {
106
+ if (!entry.endsWith(".json"))
107
+ continue;
108
+ const raw = await this.readJson(path.join(this.missionsPath, entry));
109
+ if (!raw)
110
+ continue;
111
+ missions.push(this.normalize(raw));
112
+ }
113
+ return missions;
114
+ }
115
+ normalize(raw) {
116
+ return {
117
+ id: raw.id,
118
+ missionKey: raw.missionKey,
119
+ title: typeof raw.title === "string" ? raw.title : raw.missionKey,
120
+ status: typeof raw.status === "string" && MISSION_STATUSES.has(raw.status) ? raw.status : "active",
121
+ participants: Array.isArray(raw.participants) ? raw.participants : [],
122
+ outcomes: Array.isArray(raw.outcomes) ? raw.outcomes : [],
123
+ learnings: raw.learnings && typeof raw.learnings === "object" ? raw.learnings : {},
124
+ ...(raw.importedLearnings && typeof raw.importedLearnings === "object" ? { importedLearnings: raw.importedLearnings } : {}),
125
+ // The coordination sub-object (brick 5) passes through like importedLearnings:
126
+ // present iff the record carries one, so a legacy mission with no coordination
127
+ // round-trips unchanged (absent ⇒ unclaimed).
128
+ ...(raw.coordination && typeof raw.coordination === "object" ? { coordination: raw.coordination } : {}),
129
+ createdAt: typeof raw.createdAt === "string" ? raw.createdAt : new Date().toISOString(),
130
+ updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : new Date().toISOString(),
131
+ schemaVersion: typeof raw.schemaVersion === "number" ? raw.schemaVersion : 1,
132
+ };
133
+ }
134
+ async readJson(filePath) {
135
+ try {
136
+ const raw = await fsPromises.readFile(filePath, "utf-8");
137
+ try {
138
+ const parsed = JSON.parse(raw);
139
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
140
+ return null;
141
+ }
142
+ return parsed;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ }
152
+ }
153
+ exports.FileMissionStore = FileMissionStore;
@@ -0,0 +1,10 @@
1
+ import type { MissionRecord } from "./types";
2
+ export interface MissionStore {
3
+ get(id: string): Promise<MissionRecord | null>;
4
+ put(id: string, mission: MissionRecord): Promise<void>;
5
+ delete(id: string): Promise<void>;
6
+ /** Resolve a mission by its cross-agent join key (the name on the wire), not
7
+ * the local UUID. Returns null when no mission carries that key. */
8
+ findByMissionKey(missionKey: string): Promise<MissionRecord | null>;
9
+ listAll(): Promise<MissionRecord[]>;
10
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ // Mission store abstraction (brick 3).
3
+ // MissionRecord state persists through MissionStore — a sibling to FriendStore /
4
+ // GrantStore, mirroring their shape. A mission is many-to-many with peers and has
5
+ // its own identity/lifecycle, so it lives in its own collection rather than on a
6
+ // friend record. Adds `findByMissionKey` (the cross-agent join-key lookup the
7
+ // consumer resolves on) over the get/put/delete/listAll quartet. No mission
8
+ // module imports `fs` directly except the FileMissionStore adapter.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,31 @@
1
+ import type { MissionStore } from "./mission-store";
2
+ import type { AgentAttribution, MissionRecord, RelationshipOutcome } from "./types";
3
+ export interface RecordMissionInput {
4
+ missionKey: string;
5
+ /** Used only when CREATING; ignored on upsert of an existing mission. */
6
+ title?: string;
7
+ /** When provided, sets the mission's status (first-party). */
8
+ status?: MissionRecord["status"];
9
+ /** Merged into the mission's participants, deduped by agentId. */
10
+ participants?: AgentAttribution[];
11
+ /** Appended to the first-party `learnings` map (NEVER `importedLearnings`). */
12
+ learnings?: Array<{
13
+ key: string;
14
+ value: string;
15
+ shareable?: boolean;
16
+ }>;
17
+ /** Appended to the mission's outcomes as first-party rows (no `origin:imported`). */
18
+ outcomes?: Array<{
19
+ missionId: string;
20
+ result: RelationshipOutcome["result"];
21
+ note?: string;
22
+ }>;
23
+ }
24
+ /**
25
+ * Upsert a mission by its `missionKey`. Creates a fresh record (new local UUID,
26
+ * `status:"active"`, empty namespaces) when the key is unknown; otherwise
27
+ * resolves the existing record and applies the input. First-party learnings are
28
+ * this agent's own — they NEVER touch `importedLearnings`. Returns the persisted
29
+ * record.
30
+ */
31
+ export declare function recordMission(missions: MissionStore, input: RecordMissionInput): Promise<MissionRecord>;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.recordMission = recordMission;
4
+ // recordMission — the first-party mission writer (brick 3).
5
+ //
6
+ // Upserts a MissionRecord by its cross-agent `missionKey`: creates one with a
7
+ // fresh local UUID when the key is unknown, else resolves the existing record via
8
+ // `findByMissionKey` and applies the input. First-party learnings land in
9
+ // `learnings` (this agent's own knowledge — NEVER in `importedLearnings`);
10
+ // participants merge deduped by agentId; outcomes append; status updates when
11
+ // provided. The companion to the UNTOUCHED `recordRelationshipOutcome` in
12
+ // `outcomes.ts` (that writer owns a friend's denormalized outcome index; this one
13
+ // owns the mission record). Pure store ops — no fs / net / env; the only node
14
+ // builtin is `node:crypto` (randomUUID), mirroring share.ts.
15
+ const node_crypto_1 = require("node:crypto");
16
+ const observability_1 = require("./observability");
17
+ /** Merge the input participants into the existing list, deduped by agentId. A
18
+ * participant whose agentId already appears is skipped (idempotent). */
19
+ function mergeParticipants(existing, incoming) {
20
+ const seen = new Set(existing.map((p) => p.agentId));
21
+ const merged = [...existing];
22
+ for (const p of incoming) {
23
+ if (!seen.has(p.agentId)) {
24
+ merged.push(p);
25
+ seen.add(p.agentId);
26
+ }
27
+ }
28
+ return merged;
29
+ }
30
+ /** Apply the input's first-party learnings onto the existing learnings map,
31
+ * stamping each with `savedAt` + first-party provenance. */
32
+ function applyLearnings(existing, incoming, now) {
33
+ const learnings = { ...existing };
34
+ for (const l of incoming) {
35
+ learnings[l.key] = {
36
+ value: l.value,
37
+ savedAt: now,
38
+ shareable: l.shareable ?? false,
39
+ provenance: { origin: "first_party" },
40
+ };
41
+ }
42
+ return learnings;
43
+ }
44
+ /** Append the input's first-party outcomes, stamping each with a timestamp. */
45
+ function applyOutcomes(existing, incoming, now) {
46
+ const outcomes = [...existing];
47
+ for (const o of incoming) {
48
+ outcomes.push({
49
+ missionId: o.missionId,
50
+ result: o.result,
51
+ timestamp: now,
52
+ ...(o.note ? { note: o.note } : {}),
53
+ });
54
+ }
55
+ return outcomes;
56
+ }
57
+ /**
58
+ * Upsert a mission by its `missionKey`. Creates a fresh record (new local UUID,
59
+ * `status:"active"`, empty namespaces) when the key is unknown; otherwise
60
+ * resolves the existing record and applies the input. First-party learnings are
61
+ * this agent's own — they NEVER touch `importedLearnings`. Returns the persisted
62
+ * record.
63
+ */
64
+ async function recordMission(missions, input) {
65
+ const now = new Date().toISOString();
66
+ const found = await missions.findByMissionKey(input.missionKey);
67
+ const base = found ?? {
68
+ id: (0, node_crypto_1.randomUUID)(),
69
+ missionKey: input.missionKey,
70
+ title: input.title ?? input.missionKey,
71
+ status: input.status ?? "active",
72
+ participants: [],
73
+ outcomes: [],
74
+ learnings: {},
75
+ importedLearnings: {},
76
+ createdAt: now,
77
+ updatedAt: now,
78
+ schemaVersion: 1,
79
+ };
80
+ const updated = {
81
+ ...base,
82
+ // `status` is set from the input when provided (first-party). On create it is
83
+ // already baked into `base`; this re-applies it on an upsert.
84
+ status: input.status ?? base.status,
85
+ participants: input.participants ? mergeParticipants(base.participants, input.participants) : base.participants,
86
+ outcomes: input.outcomes ? applyOutcomes(base.outcomes, input.outcomes, now) : base.outcomes,
87
+ learnings: input.learnings ? applyLearnings(base.learnings, input.learnings, now) : base.learnings,
88
+ updatedAt: now,
89
+ };
90
+ await missions.put(updated.id, updated);
91
+ (0, observability_1.emitNervesEvent)({
92
+ component: "friends",
93
+ event: "friends.mission_recorded",
94
+ message: "recorded mission",
95
+ meta: { missionKey: input.missionKey, created: found === null, status: updated.status },
96
+ });
97
+ return updated;
98
+ }
@@ -0,0 +1,11 @@
1
+ import type { FriendStore } from "./store";
2
+ import type { NoteProvenance } from "./types";
3
+ import type { FriendOpResult } from "./results";
4
+ export interface ApplyFriendNoteInput {
5
+ type: "name" | "tool_preference" | "note";
6
+ key?: string;
7
+ content: string;
8
+ override?: boolean;
9
+ provenance?: NoteProvenance;
10
+ }
11
+ export declare function applyFriendNote(store: FriendStore, friendId: string, input: ApplyFriendNoteInput): Promise<FriendOpResult>;