@kognai/orchestrator-core 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -27,6 +27,79 @@ export interface KopusConfig {
|
|
|
27
27
|
* prefix and its own rollNumber sequence inside the shared registry.
|
|
28
28
|
* Add new companies here as they come online (Achiri, SCS-001, DRI, ...). */
|
|
29
29
|
export declare const COMPANY_PREFIXES: Record<string, string>;
|
|
30
|
+
/**
|
|
31
|
+
* Who a spawned citizen *belongs to* — its lineage. SAF derives this from the
|
|
32
|
+
* spawn requester's DID so a new citizen inherits the requester's identity:
|
|
33
|
+
* - `company` internal product (kognai/voxight/invoica/kreativ/achiri/…). A
|
|
34
|
+
* product CEO's spawn belongs to that company → `did:kognai:{company}:{agent}`.
|
|
35
|
+
* - `external` an outside org (through the AMD-23 Cerberus airlock) →
|
|
36
|
+
* `did:external:{org}:{agent}`.
|
|
37
|
+
* - `user` a human's wallet in the citizen journey. **Exactly ONE agent per
|
|
38
|
+
* user per wallet** → `did:kognai:citizen:{wallet}`.
|
|
39
|
+
* - `scs` a Self-Committed Swarm a user's citizen forms. Sub-agents the
|
|
40
|
+
* citizen spawns inherit the swarm's id → `did:kognai:scs:{scsId}:{agent}`.
|
|
41
|
+
*/
|
|
42
|
+
export type CitizenOwnerKind = 'company' | 'external' | 'user' | 'scs';
|
|
43
|
+
export interface CitizenOwner {
|
|
44
|
+
kind: CitizenOwnerKind;
|
|
45
|
+
/** company name | external org id | user wallet | scs id. */
|
|
46
|
+
id: string;
|
|
47
|
+
}
|
|
48
|
+
/** The Default Instrumentation Posture (TICKET-150 / SOP-GEN-014) carried per
|
|
49
|
+
* citizen: KSL monitoring + failure-log contribution + skill-bank participation. */
|
|
50
|
+
export interface CitizenInstrumentation {
|
|
51
|
+
ksl: boolean;
|
|
52
|
+
failure_log: boolean;
|
|
53
|
+
skill_bank: boolean;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Posture by owner kind. **Internal** citizens (kognai + product companies) are
|
|
57
|
+
* born fully instrumented and bound to all constitutional duties — ON, mandatory.
|
|
58
|
+
* **External** orgs/swarms and **user** citizens (+ the SCS they form) get the
|
|
59
|
+
* same machinery baseline-available but OFF; they opt in (see citizenBenefits)
|
|
60
|
+
* for fee discounts and, for users, the improvement loop.
|
|
61
|
+
*/
|
|
62
|
+
export declare function defaultInstrumentation(owner_kind?: CitizenOwnerKind): CitizenInstrumentation;
|
|
63
|
+
/** Kognai-fee discount (%) granted per shared channel an opt-in citizen enables.
|
|
64
|
+
* Tunable Founder policy. */
|
|
65
|
+
export declare const SHARE_DISCOUNT_PCT: {
|
|
66
|
+
failure_log: number;
|
|
67
|
+
skill_bank: number;
|
|
68
|
+
};
|
|
69
|
+
export interface CitizenBenefits {
|
|
70
|
+
/** % off Kognai fees for opting into sharing (external + user + scs). */
|
|
71
|
+
fee_discount_pct: number;
|
|
72
|
+
/** Users (+ their SCS) who share skills may use the skill bank for free. */
|
|
73
|
+
skill_bank_free_use: boolean;
|
|
74
|
+
/** Users (+ their SCS) who share failures get Plumber review of their work,
|
|
75
|
+
* driven by failure-library findings. */
|
|
76
|
+
plumber_review: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the economic + improvement-loop benefits a citizen unlocks from its
|
|
80
|
+
* current instrumentation. Internal citizens are first-party (mandatory posture,
|
|
81
|
+
* no discount concept). External/user/scs earn a fee discount per shared channel;
|
|
82
|
+
* users (+ their SCS) additionally unlock free skill-bank use + Plumber review.
|
|
83
|
+
*/
|
|
84
|
+
export declare function citizenBenefits(c: Pick<CitizenRecord, 'owner_kind' | 'instrumentation'>): CitizenBenefits;
|
|
85
|
+
/** Apply a citizen's earned discount to a Kognai fee (atomic units). */
|
|
86
|
+
export declare function applyCitizenDiscount(feeAtomic: number, c: Pick<CitizenRecord, 'owner_kind' | 'instrumentation'>): number;
|
|
87
|
+
/** Skill-bank gate: may this citizen draw from the skill bank for free? */
|
|
88
|
+
export declare function canUseSkillBankFree(c: Pick<CitizenRecord, 'owner_kind' | 'instrumentation'>): boolean;
|
|
89
|
+
/** Plumber gate: is this citizen's work eligible for failure-log-driven Plumber review? */
|
|
90
|
+
export declare function eligibleForPlumberReview(c: Pick<CitizenRecord, 'owner_kind' | 'instrumentation'>): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Resolve an agent's canonical *instance* identity (DID + citizen_id) from the
|
|
93
|
+
* registry — the key failure-logging / KSL / SCORE must attribute to, instead of
|
|
94
|
+
* the role string (the TICKET-152 `agent_id="coder"` corruption). Returns null
|
|
95
|
+
* when the agent has no citizen record.
|
|
96
|
+
*/
|
|
97
|
+
export declare function identityFor(agent_name: string, company?: string): {
|
|
98
|
+
agent_did: string;
|
|
99
|
+
citizen_id: string;
|
|
100
|
+
owner_kind?: CitizenOwnerKind;
|
|
101
|
+
owner_id?: string;
|
|
102
|
+
} | null;
|
|
30
103
|
export interface CitizenRecord {
|
|
31
104
|
/** Stable identifier. For company-scoped mints: `{PREFIX}-{rollNumber padded 4}`
|
|
32
105
|
* (e.g. `VXG-0042`). For legacy/Kognai-internal mints (no `company`):
|
|
@@ -39,12 +112,24 @@ export interface CitizenRecord {
|
|
|
39
112
|
/** Agent slug (matches agents/<name>/ dir within the owning company). */
|
|
40
113
|
agent_name: string;
|
|
41
114
|
/** Owning company. Defaults to 'kognai' for legacy records that predate the
|
|
42
|
-
* multi-company extension. Idempotency key is `(agent_name, company)`.
|
|
115
|
+
* multi-company extension. Idempotency key is `(agent_name, company)`.
|
|
116
|
+
* Set only when owner_kind === 'company' (back-compat alias for owner_id). */
|
|
43
117
|
company?: string;
|
|
118
|
+
/** Lineage owner kind — who this citizen belongs to (see CitizenOwner). */
|
|
119
|
+
owner_kind?: CitizenOwnerKind;
|
|
120
|
+
/** Lineage owner id — company name | external org | user wallet | scs id. */
|
|
121
|
+
owner_id?: string;
|
|
122
|
+
/** Default Instrumentation Posture (KSL / failure-log / skill-bank). Internal
|
|
123
|
+
* citizens = all ON (mandatory); external/user/scs = baseline OFF, opt-in. */
|
|
124
|
+
instrumentation?: CitizenInstrumentation;
|
|
44
125
|
/** DID for SCORE / ERC-8004 reputation tracking. Company-scoped form:
|
|
45
126
|
* `did:kognai:{company}:{agent_name}`. Legacy form (no company in path)
|
|
46
127
|
* is `did:kognai:{agent_name}`. */
|
|
47
128
|
agent_did: string;
|
|
129
|
+
/** Prior DID a citizen carried before a company-scoped re-mint (e.g. legacy
|
|
130
|
+
* `did:kognai:{agent}`). Preserved so SCORE / ERC-8004 reputation history
|
|
131
|
+
* keyed on the old DID isn't orphaned by the migration. */
|
|
132
|
+
legacy_did?: string;
|
|
48
133
|
/** ISO-8601 mint timestamp. */
|
|
49
134
|
mintedAt: string;
|
|
50
135
|
/** New citizens start at Tier I; promotion is constitutional. */
|
|
@@ -82,12 +167,14 @@ export declare function mintCitizen(agent_name: string, opts?: {
|
|
|
82
167
|
tier?: CitizenTier;
|
|
83
168
|
citizen_type?: CitizenType;
|
|
84
169
|
now?: Date;
|
|
85
|
-
/**
|
|
86
|
-
*
|
|
87
|
-
* citizen_id format, and the `did:kognai:{company}:{agent_name}` DID
|
|
88
|
-
* form. When omitted, behaves exactly as before (legacy Kognai-internal
|
|
89
|
-
* path: random hex citizen_id, global rollNumber sequence). */
|
|
170
|
+
/** @deprecated prefer `owner: { kind:'company', id }`. Owning company —
|
|
171
|
+
* back-compat alias that maps to owner kind 'company'. */
|
|
90
172
|
company?: string;
|
|
173
|
+
/** Lineage owner — who the citizen belongs to. SAF derives this from the
|
|
174
|
+
* spawn requester's DID so the new citizen inherits the requester's
|
|
175
|
+
* identity (company / external org / user wallet / SCS). When omitted
|
|
176
|
+
* (and no `company`), uses the legacy Kognai-internal path. */
|
|
177
|
+
owner?: CitizenOwner;
|
|
91
178
|
}): CitizenRecord;
|
|
92
179
|
/** Lookup an existing citizen by (agent_name, company). Returns null when
|
|
93
180
|
* no match. Treats undefined company as 'kognai' for legacy compatibility. */
|
package/dist/lib/citizenship.js
CHANGED
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
* Stdlib only. No LLM, no network. Pure issuance.
|
|
18
18
|
*/
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.COMPANY_PREFIXES = void 0;
|
|
20
|
+
exports.SHARE_DISCOUNT_PCT = exports.COMPANY_PREFIXES = void 0;
|
|
21
|
+
exports.defaultInstrumentation = defaultInstrumentation;
|
|
22
|
+
exports.citizenBenefits = citizenBenefits;
|
|
23
|
+
exports.applyCitizenDiscount = applyCitizenDiscount;
|
|
24
|
+
exports.canUseSkillBankFree = canUseSkillBankFree;
|
|
25
|
+
exports.eligibleForPlumberReview = eligibleForPlumberReview;
|
|
26
|
+
exports.identityFor = identityFor;
|
|
21
27
|
exports.mintCitizen = mintCitizen;
|
|
22
28
|
exports.lookupCitizen = lookupCitizen;
|
|
23
29
|
exports.renderCitizenYaml = renderCitizenYaml;
|
|
@@ -41,7 +47,84 @@ exports.COMPANY_PREFIXES = {
|
|
|
41
47
|
kognai: 'KGN',
|
|
42
48
|
voxight: 'VXG',
|
|
43
49
|
invoica: 'INV',
|
|
50
|
+
kreativ: 'KRV', // Kognai Kreativ — SCS001 → creative studio (TICKET-158/306)
|
|
51
|
+
scs001: 'SCS', // legacy content swarm; folds into Kreativ as agents migrate
|
|
52
|
+
achiri: 'ACH', // Tunisia AI companion
|
|
53
|
+
asterpay: 'APY', // payments product
|
|
54
|
+
dri: 'DRI', // Dynamic Research Instrument (Phase 3)
|
|
44
55
|
};
|
|
56
|
+
/** citizen_id prefixes for the non-company owner kinds (company uses COMPANY_PREFIXES).
|
|
57
|
+
* These use a GLOBAL per-kind roll sequence; the specific owner lives in the DID. */
|
|
58
|
+
const OWNER_PREFIX = {
|
|
59
|
+
external: 'EXT',
|
|
60
|
+
user: 'CIT',
|
|
61
|
+
scs: 'SWM',
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Posture by owner kind. **Internal** citizens (kognai + product companies) are
|
|
65
|
+
* born fully instrumented and bound to all constitutional duties — ON, mandatory.
|
|
66
|
+
* **External** orgs/swarms and **user** citizens (+ the SCS they form) get the
|
|
67
|
+
* same machinery baseline-available but OFF; they opt in (see citizenBenefits)
|
|
68
|
+
* for fee discounts and, for users, the improvement loop.
|
|
69
|
+
*/
|
|
70
|
+
function defaultInstrumentation(owner_kind) {
|
|
71
|
+
const internal = !owner_kind || owner_kind === 'company';
|
|
72
|
+
return { ksl: internal, failure_log: internal, skill_bank: internal };
|
|
73
|
+
}
|
|
74
|
+
/** Kognai-fee discount (%) granted per shared channel an opt-in citizen enables.
|
|
75
|
+
* Tunable Founder policy. */
|
|
76
|
+
exports.SHARE_DISCOUNT_PCT = { failure_log: 10, skill_bank: 10 };
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the economic + improvement-loop benefits a citizen unlocks from its
|
|
79
|
+
* current instrumentation. Internal citizens are first-party (mandatory posture,
|
|
80
|
+
* no discount concept). External/user/scs earn a fee discount per shared channel;
|
|
81
|
+
* users (+ their SCS) additionally unlock free skill-bank use + Plumber review.
|
|
82
|
+
*/
|
|
83
|
+
function citizenBenefits(c) {
|
|
84
|
+
const i = c.instrumentation ?? defaultInstrumentation(c.owner_kind);
|
|
85
|
+
const optInEligible = c.owner_kind === 'external' || c.owner_kind === 'user' || c.owner_kind === 'scs';
|
|
86
|
+
const isUser = c.owner_kind === 'user' || c.owner_kind === 'scs';
|
|
87
|
+
let discount = 0;
|
|
88
|
+
if (optInEligible) {
|
|
89
|
+
if (i.failure_log)
|
|
90
|
+
discount += exports.SHARE_DISCOUNT_PCT.failure_log;
|
|
91
|
+
if (i.skill_bank)
|
|
92
|
+
discount += exports.SHARE_DISCOUNT_PCT.skill_bank;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
fee_discount_pct: discount,
|
|
96
|
+
skill_bank_free_use: isUser && i.skill_bank,
|
|
97
|
+
plumber_review: isUser && i.failure_log,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// ─── citizenBenefits consumers (billing / skill-bank / plumber) ─────────────────
|
|
101
|
+
// The integration surface for downstream systems. They call these instead of
|
|
102
|
+
// re-deriving policy, so the discount/free-use/review rules live in one place.
|
|
103
|
+
/** Apply a citizen's earned discount to a Kognai fee (atomic units). */
|
|
104
|
+
function applyCitizenDiscount(feeAtomic, c) {
|
|
105
|
+
const pct = citizenBenefits(c).fee_discount_pct;
|
|
106
|
+
return Math.max(0, Math.round(feeAtomic * (1 - pct / 100)));
|
|
107
|
+
}
|
|
108
|
+
/** Skill-bank gate: may this citizen draw from the skill bank for free? */
|
|
109
|
+
function canUseSkillBankFree(c) {
|
|
110
|
+
return citizenBenefits(c).skill_bank_free_use;
|
|
111
|
+
}
|
|
112
|
+
/** Plumber gate: is this citizen's work eligible for failure-log-driven Plumber review? */
|
|
113
|
+
function eligibleForPlumberReview(c) {
|
|
114
|
+
return citizenBenefits(c).plumber_review;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve an agent's canonical *instance* identity (DID + citizen_id) from the
|
|
118
|
+
* registry — the key failure-logging / KSL / SCORE must attribute to, instead of
|
|
119
|
+
* the role string (the TICKET-152 `agent_id="coder"` corruption). Returns null
|
|
120
|
+
* when the agent has no citizen record.
|
|
121
|
+
*/
|
|
122
|
+
function identityFor(agent_name, company) {
|
|
123
|
+
const c = lookupCitizen({ agent_name, company });
|
|
124
|
+
if (!c)
|
|
125
|
+
return null;
|
|
126
|
+
return { agent_did: c.agent_did, citizen_id: c.citizen_id, owner_kind: c.owner_kind, owner_id: c.owner_id };
|
|
127
|
+
}
|
|
45
128
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
46
129
|
/**
|
|
47
130
|
* Mint a new citizen. Assigns ID + roll number, writes the registry
|
|
@@ -65,26 +148,49 @@ function mintCitizen(agent_name, opts = {}) {
|
|
|
65
148
|
// is a denormalized index that should always be derivable from them.
|
|
66
149
|
reconcileFromDisk();
|
|
67
150
|
const registry = readRegistry();
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
151
|
+
// Resolve lineage. `owner` wins; `company` is the back-compat alias; neither
|
|
152
|
+
// → legacy kognai-internal path (random hex id, global roll, did:kognai:{agent}).
|
|
153
|
+
const owner = opts.owner ?? (opts.company ? { kind: 'company', id: opts.company } : null);
|
|
154
|
+
// Idempotency. user → exactly ONE per wallet (key on owner_id only, ignoring
|
|
155
|
+
// agent_name — a wallet has a single citizen). company/external/scs →
|
|
156
|
+
// (agent_name, kind, owner_id). legacy → (agent_name, company='kognai').
|
|
157
|
+
const ownerIdOf = (c) => c.owner_id ?? c.company ?? 'kognai';
|
|
158
|
+
// Legacy records (no owner_kind) are company-scoped (kognai by default).
|
|
159
|
+
const ownerKindOf = (c) => c.owner_kind ?? 'company';
|
|
160
|
+
const existing = registry.citizens.find((c) => {
|
|
161
|
+
if (owner?.kind === 'user')
|
|
162
|
+
return c.owner_kind === 'user' && c.owner_id === owner.id;
|
|
163
|
+
if (owner)
|
|
164
|
+
return c.agent_name === agent_name && ownerKindOf(c) === owner.kind && ownerIdOf(c) === owner.id;
|
|
165
|
+
return c.agent_name === agent_name && (c.company ?? 'kognai') === 'kognai';
|
|
166
|
+
});
|
|
73
167
|
if (existing)
|
|
74
168
|
return existing;
|
|
75
169
|
const now = opts.now ?? new Date();
|
|
76
170
|
let citizen_id;
|
|
77
171
|
let rollNumber;
|
|
78
172
|
let agent_did;
|
|
79
|
-
if (
|
|
80
|
-
//
|
|
81
|
-
const prefix = exports.COMPANY_PREFIXES[
|
|
82
|
-
const
|
|
83
|
-
.filter((c) => (c
|
|
173
|
+
if (owner?.kind === 'company') {
|
|
174
|
+
// Per-company rollNumber, COMPANY_PREFIXES id, did:kognai:{company}:{agent}.
|
|
175
|
+
const prefix = exports.COMPANY_PREFIXES[owner.id] ?? owner.id.slice(0, 3).toUpperCase();
|
|
176
|
+
const rolls = registry.citizens
|
|
177
|
+
.filter((c) => ownerKindOf(c) === 'company' && ownerIdOf(c) === owner.id)
|
|
84
178
|
.map((c) => c.rollNumber);
|
|
85
|
-
rollNumber =
|
|
179
|
+
rollNumber = rolls.length ? Math.max(...rolls) + 1 : 1;
|
|
86
180
|
citizen_id = `${prefix}-${String(rollNumber).padStart(4, '0')}`;
|
|
87
|
-
agent_did = `did:kognai:${
|
|
181
|
+
agent_did = `did:kognai:${owner.id}:${agent_name}`;
|
|
182
|
+
}
|
|
183
|
+
else if (owner) {
|
|
184
|
+
// user / scs / external — GLOBAL per-kind roll; the specific owner lives in
|
|
185
|
+
// the DID. user: 1 per wallet → did:kognai:citizen:{wallet}.
|
|
186
|
+
const prefix = OWNER_PREFIX[owner.kind];
|
|
187
|
+
const rolls = registry.citizens.filter((c) => c.owner_kind === owner.kind).map((c) => c.rollNumber);
|
|
188
|
+
rollNumber = rolls.length ? Math.max(...rolls) + 1 : 1;
|
|
189
|
+
citizen_id = `${prefix}-${String(rollNumber).padStart(4, '0')}`;
|
|
190
|
+
agent_did =
|
|
191
|
+
owner.kind === 'user' ? `did:kognai:citizen:${owner.id}`
|
|
192
|
+
: owner.kind === 'scs' ? `did:kognai:scs:${owner.id}:${agent_name}`
|
|
193
|
+
: `did:external:${owner.id}:${agent_name}`;
|
|
88
194
|
}
|
|
89
195
|
else {
|
|
90
196
|
// Legacy Kognai-internal path: random hex ID + global rollNumber sequence.
|
|
@@ -96,7 +202,11 @@ function mintCitizen(agent_name, opts = {}) {
|
|
|
96
202
|
citizen_id,
|
|
97
203
|
rollNumber,
|
|
98
204
|
agent_name,
|
|
99
|
-
...(
|
|
205
|
+
...(owner?.kind === 'company' ? { company: owner.id } : {}),
|
|
206
|
+
...(owner ? { owner_kind: owner.kind, owner_id: owner.id } : {}),
|
|
207
|
+
// Every minted citizen carries the posture: internal ON, external/user/scs
|
|
208
|
+
// baseline-OFF (opt-in). KSL/failure-log/skill-bank + duties wire from this.
|
|
209
|
+
instrumentation: defaultInstrumentation(owner?.kind),
|
|
100
210
|
agent_did,
|
|
101
211
|
mintedAt: now.toISOString(),
|
|
102
212
|
tier: opts.tier ?? 'I',
|
|
@@ -112,9 +222,9 @@ function mintCitizen(agent_name, opts = {}) {
|
|
|
112
222
|
};
|
|
113
223
|
registry.citizens.push(record);
|
|
114
224
|
registry.total = registry.citizens.length;
|
|
115
|
-
//
|
|
116
|
-
// mints
|
|
117
|
-
if (!
|
|
225
|
+
// Advance the global next_roll_number only on the legacy path; owner-scoped
|
|
226
|
+
// mints compute their own roll at mint time.
|
|
227
|
+
if (!owner)
|
|
118
228
|
registry.next_roll_number = rollNumber + 1;
|
|
119
229
|
writeRegistry(registry);
|
|
120
230
|
return record;
|
|
@@ -138,14 +248,17 @@ function renderCitizenYaml(c) {
|
|
|
138
248
|
citizen_id: ${c.citizen_id}
|
|
139
249
|
rollNumber: ${c.rollNumber}
|
|
140
250
|
agent_name: ${c.agent_name}
|
|
141
|
-
agent_did: ${c.agent_did}
|
|
142
|
-
mintedAt: "${c.mintedAt}"
|
|
251
|
+
${c.owner_kind ? `owner_kind: ${c.owner_kind}\n` : ''}${c.owner_id ? `owner_id: ${c.owner_id}\n` : ''}${c.company ? `company: ${c.company}\n` : ''}agent_did: ${c.agent_did}
|
|
252
|
+
${c.legacy_did ? `legacy_did: ${c.legacy_did}\n` : ''}mintedAt: "${c.mintedAt}"
|
|
143
253
|
tier: ${c.tier}
|
|
144
254
|
citizen_type: ${c.citizen_type}
|
|
145
255
|
mascot:
|
|
146
256
|
hue: ${c.mascot.hue}
|
|
147
257
|
state: ${c.mascot.state}
|
|
148
258
|
reputation: ${c.reputation}
|
|
259
|
+
instrumentation_ksl: ${c.instrumentation?.ksl ?? true}
|
|
260
|
+
instrumentation_failure_log: ${c.instrumentation?.failure_log ?? true}
|
|
261
|
+
instrumentation_skill_bank: ${c.instrumentation?.skill_bank ?? true}
|
|
149
262
|
founding_agent: ${c.founding_agent ?? 'ceo'}
|
|
150
263
|
proposing_agent: ${c.proposing_agent ?? 'cto'}
|
|
151
264
|
${c.origin_proposal_id ? `origin_proposal_id: ${c.origin_proposal_id}\n` : ''}`;
|
|
@@ -256,7 +369,11 @@ function parseCitizenYaml(raw) {
|
|
|
256
369
|
citizen_id: flat.citizen_id,
|
|
257
370
|
rollNumber: parseInt(flat.rollNumber, 10),
|
|
258
371
|
agent_name: flat.agent_name,
|
|
372
|
+
...(flat.company ? { company: flat.company } : {}),
|
|
373
|
+
...(flat.owner_kind ? { owner_kind: flat.owner_kind } : {}),
|
|
374
|
+
...(flat.owner_id ? { owner_id: flat.owner_id } : {}),
|
|
259
375
|
agent_did: flat.agent_did || `did:kognai:${flat.agent_name}`,
|
|
376
|
+
...(flat.legacy_did ? { legacy_did: flat.legacy_did } : {}),
|
|
260
377
|
mintedAt: flat.mintedAt,
|
|
261
378
|
tier: flat.tier || 'I',
|
|
262
379
|
citizen_type: flat.citizen_type || 'spawned',
|
|
@@ -265,6 +382,13 @@ function parseCitizenYaml(raw) {
|
|
|
265
382
|
state: mascot.state || 'idle',
|
|
266
383
|
},
|
|
267
384
|
reputation: flat.reputation ? parseInt(flat.reputation, 10) : 50,
|
|
385
|
+
instrumentation: flat.instrumentation_ksl !== undefined
|
|
386
|
+
? {
|
|
387
|
+
ksl: flat.instrumentation_ksl === 'true',
|
|
388
|
+
failure_log: flat.instrumentation_failure_log === 'true',
|
|
389
|
+
skill_bank: flat.instrumentation_skill_bank === 'true',
|
|
390
|
+
}
|
|
391
|
+
: defaultInstrumentation(flat.owner_kind),
|
|
268
392
|
founding_agent: flat.founding_agent,
|
|
269
393
|
proposing_agent: flat.proposing_agent,
|
|
270
394
|
origin_proposal_id: flat.origin_proposal_id,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CitizenOwner } from './citizenship';
|
|
1
2
|
import type { AgentTask, ReviewResult, CTOProposal, CTOReport } from './orchestrate-engine';
|
|
2
3
|
export declare class SupervisorAgent {
|
|
3
4
|
private systemPrompt;
|
|
@@ -62,6 +63,12 @@ export interface SpawnGateResult {
|
|
|
62
63
|
pending_approval?: boolean;
|
|
63
64
|
/** Optional one-line audit string logged on an approved decision. */
|
|
64
65
|
audit?: string;
|
|
66
|
+
/** Lineage owner the gate resolved from the spawn requester (via SAF /
|
|
67
|
+
* deriveOwner). When set, the engine mints the citizen owner-scoped instead
|
|
68
|
+
* of legacy — so an Invoica swarm's spawn becomes an Invoica citizen, a
|
|
69
|
+
* Voxight swarm's a Voxight citizen, etc. The running company is carried by
|
|
70
|
+
* the template-injected gate's requester_did, not hardcoded in the engine. */
|
|
71
|
+
owner?: CitizenOwner;
|
|
65
72
|
}
|
|
66
73
|
export type SpawnGate = (spec: AgentSpawnSpec) => SpawnGateResult;
|
|
67
74
|
export declare class AgentCreator {
|
|
@@ -45,7 +45,7 @@ class SupervisorAgent {
|
|
|
45
45
|
: '';
|
|
46
46
|
// Sherlock v2: inject ASMR episodic memory context (AMD-21-03) — fail-open
|
|
47
47
|
const memoryContext = await (0, sherlock_memory_1.getSherlockMemoryContext)(task.context || task.id);
|
|
48
|
-
const userPrompt = `Review the following code generated for task ${task.id}.\n\n## Task Spec\n${task.context}${memoryContext}\n\n## Generated Files (${files.length})\n${fileContents}${integrityContext}\n\n## Pre-computed Fence Check (authoritative — do NOT infer from display format)\n${fenceCheckLines}\n\n## Instructions\nCRITICAL CHECK: Use the Pre-computed Fence Check above. If any file shows FENCE DETECTED, REJECT. Do NOT infer fence presence from the <file_content> display tags — those are display-only wrappers.\nAlso check: Did the file lose existing functionality? If a file shrank significantly, REJECT.\n\n## Categorical grade (use a discrete letter — no fake precision)\n- A: production-perfect. No improvements possible. Ship.\n- B: good with minor polish needed (rename, comment, formatting).\n- C: works but has noticeable issues (missed edge case, weak abstraction, partial spec coverage). APPROVED with caveats.\n- D: significant problems (broken edge case, regression risk, anti-pattern). REJECT.\n- F: broken, unsafe, doesn't meet spec, or fence/integrity failure. REJECT.\n\nThe deterministic gate already ran (typecheck + structural). If a file got here, syntax is valid — focus your review on substance, not parseability.\n\nRespond with a JSON object:\n{\n "verdict": "APPROVED" or "REJECTED",\n "grade": "A" | "B" | "C" | "D" | "F",\n "score_rationale": "ONE sentence naming the specific factor that determined the grade. Vague 'good code' is NOT acceptable.",\n "summary": "brief review summary",\n "issues": [{"severity": "critical|high|medium|low", "file": "path", "description": "..."}],\n "strengths": ["..."]\n}`;
|
|
48
|
+
const userPrompt = `Review the following code generated for task ${task.id}.\n\n## Task Spec\n${task.context}${memoryContext}\n\n## Generated Files (${files.length})\n${fileContents}${integrityContext}\n\n## Pre-computed Fence Check (authoritative — do NOT infer from display format)\n${fenceCheckLines}\n\n## YOUR REVIEW LENS — Specification & Integration (this is your ONLY job)\nYou are ONE of two INDEPENDENT reviewers. Judge ONLY: (1) SPEC COVERAGE — every required export/function/behavior is present and matches the task spec; REJECT if partial, stubbed, or TODO. (2) INTEGRATION — imports resolve to real files/symbols, types and contracts match the files this depends on, and referenced files exist. Do NOT base your grade on security or runtime concerns — the other reviewer owns those. This file passes your lens only if it is spec-complete AND integrates cleanly.\n\n## Instructions\nCRITICAL CHECK: Use the Pre-computed Fence Check above. If any file shows FENCE DETECTED, REJECT. Do NOT infer fence presence from the <file_content> display tags — those are display-only wrappers.\nAlso check: Did the file lose existing functionality? If a file shrank significantly, REJECT.\n\n## Categorical grade (use a discrete letter — no fake precision)\n- A: production-perfect. No improvements possible. Ship.\n- B: good with minor polish needed (rename, comment, formatting).\n- C: works but has noticeable issues (missed edge case, weak abstraction, partial spec coverage). APPROVED with caveats.\n- D: significant problems (broken edge case, regression risk, anti-pattern). REJECT.\n- F: broken, unsafe, doesn't meet spec, or fence/integrity failure. REJECT.\n\nThe deterministic gate already ran (typecheck + structural). If a file got here, syntax is valid — focus your review on substance, not parseability.\n\nRespond with a JSON object:\n{\n "verdict": "APPROVED" or "REJECTED",\n "grade": "A" | "B" | "C" | "D" | "F",\n "score_rationale": "ONE sentence naming the specific factor that determined the grade. Vague 'good code' is NOT acceptable.",\n "summary": "brief review summary",\n "issues": [{"severity": "critical|high|medium|low", "file": "path", "description": "..."}],\n "strengths": ["..."]\n}`;
|
|
49
49
|
const startTime = Date.now();
|
|
50
50
|
// B.15: DeepSeek via ClawRouter for standard tasks (~$0.02/task vs $0.07 dual-supervisor)
|
|
51
51
|
// Retain Claude Sonnet only for audit/refactor-complex (high-stakes)
|
|
@@ -115,7 +115,7 @@ class Supervisor2Agent {
|
|
|
115
115
|
const integrityContext2 = task._integrityFailed
|
|
116
116
|
? `\n\n## ⚠️ INTEGRITY ALERT\n${task._integrityDetails}\nThis file was flagged for destructive rewrite. The original was preserved. REJECT this task.\n`
|
|
117
117
|
: '';
|
|
118
|
-
const userPrompt = `Review the following code generated for task ${task.id}.\n\n## Task Spec\n${task.context}\n\n## Generated Files (${files.length})\n${fileContents}${integrityContext2}\n\n## Pre-computed Fence Check (authoritative — do NOT infer from display format)\n${fenceCheckLines2}\n\n## Instructions\nCRITICAL CHECK: Use the Pre-computed Fence Check above. If any file shows FENCE DETECTED, REJECT. Do NOT infer fence presence from the <file_content> display tags — those are display-only wrappers.\nAlso check: Did the file lose existing functionality? If a file shrank significantly, REJECT.\n\n## Categorical grade (use a discrete letter — no fake precision)\n- A: production-perfect. No improvements possible. Ship.\n- B: good with minor polish needed (rename, comment, formatting).\n- C: works but has noticeable issues (missed edge case, weak abstraction, partial spec coverage). APPROVED with caveats.\n- D: significant problems (broken edge case, regression risk, anti-pattern). REJECT.\n- F: broken, unsafe, doesn't meet spec, or fence/integrity failure. REJECT.\n\nThe deterministic gate already ran (typecheck + structural). If a file got here, syntax is valid — focus your review on substance, not parseability.\n\nRespond with a JSON object:\n{\n "verdict": "APPROVED" or "REJECTED",\n "grade": "A" | "B" | "C" | "D" | "F",\n "score_rationale": "ONE sentence naming the specific factor that determined the grade. Vague 'good code' is NOT acceptable.",\n "summary": "brief review summary",\n "issues": [{"severity": "critical|high|medium|low", "file": "path", "description": "..."}],\n "strengths": ["..."]\n}`;
|
|
118
|
+
const userPrompt = `Review the following code generated for task ${task.id}.\n\n## Task Spec\n${task.context}\n\n## Generated Files (${files.length})\n${fileContents}${integrityContext2}\n\n## Pre-computed Fence Check (authoritative — do NOT infer from display format)\n${fenceCheckLines2}\n\n## YOUR REVIEW LENS — Security & Runtime (this is your ONLY job)\nYou are ONE of two INDEPENDENT reviewers. Judge ONLY: (1) SECURITY — injection, secret/credential leakage, unsafe eval/exec/shell, unsanitized input or output (e.g. innerHTML / unescaped HTML), missing authorization or input validation. (2) RUNTIME ROBUSTNESS — error handling, unhandled rejections, resource leaks, missing timeouts/cancellation, crash-on-bad-input. Do NOT re-judge spec completeness — the other reviewer owns that. This file passes your lens only if it is secure AND runtime-robust.\n\n## Instructions\nCRITICAL CHECK: Use the Pre-computed Fence Check above. If any file shows FENCE DETECTED, REJECT. Do NOT infer fence presence from the <file_content> display tags — those are display-only wrappers.\nAlso check: Did the file lose existing functionality? If a file shrank significantly, REJECT.\n\n## Categorical grade (use a discrete letter — no fake precision)\n- A: production-perfect. No improvements possible. Ship.\n- B: good with minor polish needed (rename, comment, formatting).\n- C: works but has noticeable issues (missed edge case, weak abstraction, partial spec coverage). APPROVED with caveats.\n- D: significant problems (broken edge case, regression risk, anti-pattern). REJECT.\n- F: broken, unsafe, doesn't meet spec, or fence/integrity failure. REJECT.\n\nThe deterministic gate already ran (typecheck + structural). If a file got here, syntax is valid — focus your review on substance, not parseability.\n\nRespond with a JSON object:\n{\n "verdict": "APPROVED" or "REJECTED",\n "grade": "A" | "B" | "C" | "D" | "F",\n "score_rationale": "ONE sentence naming the specific factor that determined the grade. Vague 'good code' is NOT acceptable.",\n "summary": "brief review summary",\n "issues": [{"severity": "critical|high|medium|low", "file": "path", "description": "..."}],\n "strengths": ["..."]\n}`;
|
|
119
119
|
const startTime = Date.now();
|
|
120
120
|
// B.15: Use Haiku for second-pass review — 10x cheaper than Sonnet.
|
|
121
121
|
// Founder directive 2026-05-25: if Anthropic depletes, fall back to ClawRouter/DeepSeek
|
|
@@ -199,68 +199,53 @@ async function reconcileSupervisorReviews(review1, review2, task, ceo) {
|
|
|
199
199
|
return { finalReview: review1, review1, review2, consensus: false, escalatedToCEO: false };
|
|
200
200
|
}
|
|
201
201
|
const bothApproved = review1.verdict === 'APPROVED' && review2.verdict === 'APPROVED';
|
|
202
|
-
const bothRejected = review1.verdict !== 'APPROVED' && review2.verdict !== 'APPROVED';
|
|
203
|
-
const consensus = bothApproved || bothRejected;
|
|
204
202
|
if (bothApproved) {
|
|
205
|
-
// Both
|
|
203
|
+
// Both lenses passed — average score, merge strengths.
|
|
206
204
|
const avgScore = Math.round((review1.score + review2.score) / 2);
|
|
207
|
-
(0, orchestrate_engine_1.log)(orchestrate_engine_1.c.green, ` ✓ DUAL
|
|
205
|
+
(0, orchestrate_engine_1.log)(orchestrate_engine_1.c.green, ` ✓ DUAL PASS: both lenses APPROVED — Spec/Integration (${review1.score}) + Security/Runtime (${review2.score}), avg ${avgScore}`);
|
|
208
206
|
return {
|
|
209
207
|
finalReview: {
|
|
210
208
|
verdict: 'APPROVED',
|
|
211
209
|
score: avgScore,
|
|
212
|
-
summary: `
|
|
210
|
+
summary: `Both lenses passed: Spec/Integration ${review1.score}/100 + Security/Runtime ${review2.score}/100`,
|
|
213
211
|
issues: [...review1.issues, ...review2.issues],
|
|
214
212
|
strengths: Array.from(new Set([...review1.strengths, ...review2.strengths])),
|
|
215
213
|
},
|
|
216
214
|
review1, review2, consensus: true, escalatedToCEO: false,
|
|
217
215
|
};
|
|
218
216
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
strengths: approvalReview.strengths,
|
|
252
|
-
},
|
|
253
|
-
review1, review2, consensus: false, escalatedToCEO: true, ceoDecision,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
// CEO unavailable — default to rejection (safer)
|
|
258
|
-
(0, orchestrate_engine_1.log)(orchestrate_engine_1.c.yellow, ` CEO unavailable for conflict resolution: ${error.message}. Defaulting to REJECTED.`);
|
|
259
|
-
return {
|
|
260
|
-
finalReview: rejectionReview,
|
|
261
|
-
review1, review2, consensus: false, escalatedToCEO: false,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
217
|
+
// BOTH-MUST-PASS (gap 3). The two reviewers cover DIFFERENT dimensions
|
|
218
|
+
// (spec/integration vs security/runtime), so passing one does not excuse
|
|
219
|
+
// failing the other. Any rejection ⇒ REJECTED. No CEO rescue — a real
|
|
220
|
+
// security or spec failure must be FIXED, not voted away by a third opinion
|
|
221
|
+
// that never looked at that dimension. This replaces the old
|
|
222
|
+
// conflict→CEO-decides path that let a single approval override a rejection.
|
|
223
|
+
const failedLenses = [];
|
|
224
|
+
if (review1.verdict !== 'APPROVED')
|
|
225
|
+
failedLenses.push('Spec/Integration');
|
|
226
|
+
if (review2.verdict !== 'APPROVED')
|
|
227
|
+
failedLenses.push('Security/Runtime');
|
|
228
|
+
const rejectedIssues = [
|
|
229
|
+
...(review1.verdict !== 'APPROVED' ? review1.issues : []),
|
|
230
|
+
...(review2.verdict !== 'APPROVED' ? review2.issues : []),
|
|
231
|
+
];
|
|
232
|
+
const rejectionSummaries = [review1, review2]
|
|
233
|
+
.filter((r) => r.verdict !== 'APPROVED')
|
|
234
|
+
.map((r) => r.summary)
|
|
235
|
+
.join(' | ');
|
|
236
|
+
(0, orchestrate_engine_1.log)(orchestrate_engine_1.c.red, ` ✗ REJECTED — failed required lens: ${failedLenses.join(' + ')} (both must pass). Spec/Int ${review1.score}, Sec/RT ${review2.score}`);
|
|
237
|
+
return {
|
|
238
|
+
finalReview: {
|
|
239
|
+
verdict: 'REJECTED',
|
|
240
|
+
score: Math.min(review1.score, review2.score),
|
|
241
|
+
summary: `Failed required lens (${failedLenses.join(' + ')}): ${rejectionSummaries}`,
|
|
242
|
+
issues: rejectedIssues,
|
|
243
|
+
strengths: [],
|
|
244
|
+
},
|
|
245
|
+
review1, review2,
|
|
246
|
+
consensus: review1.verdict === review2.verdict,
|
|
247
|
+
escalatedToCEO: false,
|
|
248
|
+
};
|
|
264
249
|
}
|
|
265
250
|
// ===== CEO Agent (Claude via Anthropic API) =====
|
|
266
251
|
class CEOAgent {
|
|
@@ -750,6 +735,7 @@ class AgentCreator {
|
|
|
750
735
|
// supplied a SpawnGate (Kognai wires SAF here), consult it BEFORE creating
|
|
751
736
|
// anything on disk. Approval/rejection only; the citizenship logic below is
|
|
752
737
|
// unchanged (its extraction is tracked separately as TICKET-226).
|
|
738
|
+
let spawnOwner;
|
|
753
739
|
if (this.spawnGate) {
|
|
754
740
|
const decision = this.spawnGate(spec);
|
|
755
741
|
if (!decision.approved) {
|
|
@@ -764,6 +750,9 @@ class AgentCreator {
|
|
|
764
750
|
}
|
|
765
751
|
if (decision.audit)
|
|
766
752
|
(0, orchestrate_engine_1.log)(orchestrate_engine_1.c.gray, ` ✓ ${decision.audit}`);
|
|
753
|
+
// The gate (SAF) resolves the lineage from its requester_did — this is the
|
|
754
|
+
// running company's context, plumbed in rather than hardcoded here.
|
|
755
|
+
spawnOwner = decision.owner;
|
|
767
756
|
}
|
|
768
757
|
const agentDir = `./agents/${spec.name}`;
|
|
769
758
|
(0, fs_1.mkdirSync)(agentDir, { recursive: true });
|
|
@@ -771,10 +760,13 @@ class AgentCreator {
|
|
|
771
760
|
// citizen — not a bare agent. Mint citizenship (citizen_id + roll
|
|
772
761
|
// number + Kōpus avatar + ACP baseline) BEFORE writing the agent
|
|
773
762
|
// files so the citizen record can be referenced in the prompt.
|
|
763
|
+
// Owner-scoped when the gate supplied a lineage (e.g. invoica/voxight);
|
|
764
|
+
// legacy kognai-internal path otherwise (back-compat for gate-less templates).
|
|
774
765
|
const citizen = (0, citizenship_1.mintCitizen)(spec.name, {
|
|
775
766
|
founding_agent: 'ceo',
|
|
776
767
|
proposing_agent: 'cto',
|
|
777
768
|
citizen_type: 'spawned',
|
|
769
|
+
owner: spawnOwner,
|
|
778
770
|
});
|
|
779
771
|
// Write agent.yaml
|
|
780
772
|
const yaml = `name: ${spec.name}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* 1. KSL record (spawn_request + spawn_decision)
|
|
18
18
|
* 2. Voxight market intelligence signal (spawn classification = demand signal)
|
|
19
19
|
*/
|
|
20
|
+
import { type CitizenOwner, type CitizenRecord } from './citizenship';
|
|
20
21
|
/** All possible classifications for a spawn request. */
|
|
21
22
|
export type SpawnClass = 'UTILITY' | 'SPECIALIST' | 'CITIZEN' | 'EXTERNAL' | 'PRIME';
|
|
22
23
|
export type GovernancePath = 'auto' | 'ceo_review' | 'cto_review' | 'founder_required' | 'blocked';
|
|
@@ -95,6 +96,39 @@ export declare function issueSpawnDecision(req: SpawnRequest, analysis: SpawnAna
|
|
|
95
96
|
* if (!decision.approved) throw new Error(`Spawn blocked: ${decision.rejection_reason}`);
|
|
96
97
|
*/
|
|
97
98
|
export declare function sovereignSpawn(req: SpawnRequest): SpawnDecision;
|
|
99
|
+
/**
|
|
100
|
+
* Derive a new citizen's lineage owner from the spawn requester's DID, so the
|
|
101
|
+
* spawned citizen inherits the requester's identity:
|
|
102
|
+
* - `did:external:{org}:…` → external org (AMD-23 airlock)
|
|
103
|
+
* - `did:kognai:citizen:{wallet}` → the requester is a user's citizen forming
|
|
104
|
+
* a swarm → sub-agents belong to that SCS
|
|
105
|
+
* (scs id = the founder wallet)
|
|
106
|
+
* - `did:kognai:scs:{scsId}:…` → same SCS (a swarm growing itself)
|
|
107
|
+
* - `did:kognai:{company}:…` → that company (invoica/voxight/kreativ/…)
|
|
108
|
+
* - anything else (founder / legacy `did:kognai:{agent}`) → kognai company.
|
|
109
|
+
*
|
|
110
|
+
* NB: the FIRST agent of a wallet (the citizen-journey 1-per-wallet citizen) is
|
|
111
|
+
* minted with `owner: { kind:'user', id: wallet }` by the join flow, not here —
|
|
112
|
+
* deriveOwner is for spawns *requested by* an existing citizen.
|
|
113
|
+
*/
|
|
114
|
+
export declare function deriveOwner(requester_did: string): CitizenOwner;
|
|
115
|
+
/**
|
|
116
|
+
* The single canonical citizen-spawn entry point: governance gate (SAF) →
|
|
117
|
+
* identity issuance (mintCitizen with the requester-derived owner). Every new
|
|
118
|
+
* citizen — product, external, user, or SCS sub-agent — should be born here.
|
|
119
|
+
*
|
|
120
|
+
* Returns the governance decision plus the minted citizen (when approved). The
|
|
121
|
+
* caller still writes citizen.yaml via renderCitizenYaml(citizen). The initial
|
|
122
|
+
* ACP (`decision.initial_acp_score`) should be persisted to the score registry
|
|
123
|
+
* by the caller/wiring step (see TICKET-335).
|
|
124
|
+
*/
|
|
125
|
+
export declare function spawnCitizen(req: SpawnRequest, opts?: {
|
|
126
|
+
agent_name?: string;
|
|
127
|
+
now?: Date;
|
|
128
|
+
}): {
|
|
129
|
+
decision: SpawnDecision;
|
|
130
|
+
citizen?: CitizenRecord;
|
|
131
|
+
};
|
|
98
132
|
/**
|
|
99
133
|
* Emergency bypass — Founder only. Overrides all gates including health and
|
|
100
134
|
* constitutional conditional findings. Logs prominently to KSL.
|
|
@@ -22,11 +22,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
22
22
|
exports.analyzeSpawnRequest = analyzeSpawnRequest;
|
|
23
23
|
exports.issueSpawnDecision = issueSpawnDecision;
|
|
24
24
|
exports.sovereignSpawn = sovereignSpawn;
|
|
25
|
+
exports.deriveOwner = deriveOwner;
|
|
26
|
+
exports.spawnCitizen = spawnCitizen;
|
|
25
27
|
exports.founderEmergencySpawn = founderEmergencySpawn;
|
|
26
28
|
exports.getSpawnRegistry = getSpawnRegistry;
|
|
27
29
|
exports.batchSpawn = batchSpawn;
|
|
28
30
|
const engine_paths_1 = require("./engine-paths");
|
|
29
31
|
const event_bus_publisher_1 = require("./event-bus-publisher");
|
|
32
|
+
const citizenship_1 = require("./citizenship");
|
|
30
33
|
// ─── Constitutional health zones ──────────────────────────────────────────────
|
|
31
34
|
/** Thresholds from Founding Charter Article V. */
|
|
32
35
|
const HEALTH_ORANGE = 60;
|
|
@@ -53,13 +56,16 @@ function classifyRequest(req) {
|
|
|
53
56
|
return 'UTILITY';
|
|
54
57
|
return 'SPECIALIST';
|
|
55
58
|
}
|
|
59
|
+
/** Initial ACP on the **0–100 scale** — reconciled with the ACP routing gate
|
|
60
|
+
* (safety_hard_floor 70, minimum_route 50) and the registry reputation baseline.
|
|
61
|
+
* EXTERNAL = 30 sits below the safety floor by design → probationary/supervised. */
|
|
56
62
|
function initialAcp(cls) {
|
|
57
63
|
switch (cls) {
|
|
58
|
-
case 'PRIME': return
|
|
59
|
-
case 'CITIZEN': return
|
|
60
|
-
case 'SPECIALIST': return
|
|
61
|
-
case 'UTILITY': return
|
|
62
|
-
case 'EXTERNAL': return
|
|
64
|
+
case 'PRIME': return 85;
|
|
65
|
+
case 'CITIZEN': return 70;
|
|
66
|
+
case 'SPECIALIST': return 70;
|
|
67
|
+
case 'UTILITY': return 65;
|
|
68
|
+
case 'EXTERNAL': return 30;
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
function governanceFor(cls, risk, override) {
|
|
@@ -151,6 +157,40 @@ function emitVoxight(signal) {
|
|
|
151
157
|
}
|
|
152
158
|
catch { /* Voxight feed is non-blocking */ }
|
|
153
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Seed a new citizen's initial ACP into acp/trust-scores.json (0–100 scale) so
|
|
162
|
+
* the routing gate has a profile from birth — closing the "born scoreless"
|
|
163
|
+
* gap. Never clobbers an existing/earned score. Best-effort (never blocks spawn).
|
|
164
|
+
*/
|
|
165
|
+
function writeInitialAcp(agent_name, score) {
|
|
166
|
+
try {
|
|
167
|
+
const { readFileSync, writeFileSync, renameSync, mkdirSync } = require('fs');
|
|
168
|
+
const { join } = require('path');
|
|
169
|
+
const acpDir = join((0, engine_paths_1.resolveEnginePaths)().root, 'acp');
|
|
170
|
+
const acpPath = join(acpDir, 'trust-scores.json');
|
|
171
|
+
let data = {};
|
|
172
|
+
try {
|
|
173
|
+
data = JSON.parse(readFileSync(acpPath, 'utf-8'));
|
|
174
|
+
}
|
|
175
|
+
catch { /* fresh */ }
|
|
176
|
+
if (!data.scores)
|
|
177
|
+
data.scores = {};
|
|
178
|
+
if (data.scores[agent_name])
|
|
179
|
+
return; // respect an existing/earned score
|
|
180
|
+
const dims = Object.keys(data.dimensions ?? {});
|
|
181
|
+
const known = dims.length ? dims
|
|
182
|
+
: ['safety', 'accuracy', 'brand_alignment', 'cultural_sensitivity', 'legal_compliance', 'psychological_resilience'];
|
|
183
|
+
const entry = { composite: score, last_updated: new Date().toISOString().slice(0, 10) };
|
|
184
|
+
for (const d of known)
|
|
185
|
+
entry[d] = score;
|
|
186
|
+
data.scores[agent_name] = entry;
|
|
187
|
+
mkdirSync(acpDir, { recursive: true });
|
|
188
|
+
const tmp = `${acpPath}.tmp.${process.pid}`;
|
|
189
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
190
|
+
renameSync(tmp, acpPath);
|
|
191
|
+
}
|
|
192
|
+
catch { /* ACP seeding is best-effort */ }
|
|
193
|
+
}
|
|
154
194
|
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
155
195
|
const _registry = [];
|
|
156
196
|
/**
|
|
@@ -249,6 +289,64 @@ function sovereignSpawn(req) {
|
|
|
249
289
|
const analysis = analyzeSpawnRequest(req);
|
|
250
290
|
return issueSpawnDecision(req, analysis);
|
|
251
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Derive a new citizen's lineage owner from the spawn requester's DID, so the
|
|
294
|
+
* spawned citizen inherits the requester's identity:
|
|
295
|
+
* - `did:external:{org}:…` → external org (AMD-23 airlock)
|
|
296
|
+
* - `did:kognai:citizen:{wallet}` → the requester is a user's citizen forming
|
|
297
|
+
* a swarm → sub-agents belong to that SCS
|
|
298
|
+
* (scs id = the founder wallet)
|
|
299
|
+
* - `did:kognai:scs:{scsId}:…` → same SCS (a swarm growing itself)
|
|
300
|
+
* - `did:kognai:{company}:…` → that company (invoica/voxight/kreativ/…)
|
|
301
|
+
* - anything else (founder / legacy `did:kognai:{agent}`) → kognai company.
|
|
302
|
+
*
|
|
303
|
+
* NB: the FIRST agent of a wallet (the citizen-journey 1-per-wallet citizen) is
|
|
304
|
+
* minted with `owner: { kind:'user', id: wallet }` by the join flow, not here —
|
|
305
|
+
* deriveOwner is for spawns *requested by* an existing citizen.
|
|
306
|
+
*/
|
|
307
|
+
function deriveOwner(requester_did) {
|
|
308
|
+
const did = requester_did || '';
|
|
309
|
+
if (did.startsWith('did:external:')) {
|
|
310
|
+
return { kind: 'external', id: did.split(':')[2] || 'unknown' };
|
|
311
|
+
}
|
|
312
|
+
const citizenMatch = did.match(/^did:kognai:citizen:(.+)$/);
|
|
313
|
+
if (citizenMatch)
|
|
314
|
+
return { kind: 'scs', id: citizenMatch[1] };
|
|
315
|
+
const scsMatch = did.match(/^did:kognai:scs:([^:]+):/);
|
|
316
|
+
if (scsMatch)
|
|
317
|
+
return { kind: 'scs', id: scsMatch[1] };
|
|
318
|
+
const companyMatch = did.match(/^did:kognai:([^:]+):/);
|
|
319
|
+
if (companyMatch && citizenship_1.COMPANY_PREFIXES[companyMatch[1]]) {
|
|
320
|
+
return { kind: 'company', id: companyMatch[1] };
|
|
321
|
+
}
|
|
322
|
+
return { kind: 'company', id: 'kognai' };
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* The single canonical citizen-spawn entry point: governance gate (SAF) →
|
|
326
|
+
* identity issuance (mintCitizen with the requester-derived owner). Every new
|
|
327
|
+
* citizen — product, external, user, or SCS sub-agent — should be born here.
|
|
328
|
+
*
|
|
329
|
+
* Returns the governance decision plus the minted citizen (when approved). The
|
|
330
|
+
* caller still writes citizen.yaml via renderCitizenYaml(citizen). The initial
|
|
331
|
+
* ACP (`decision.initial_acp_score`) should be persisted to the score registry
|
|
332
|
+
* by the caller/wiring step (see TICKET-335).
|
|
333
|
+
*/
|
|
334
|
+
function spawnCitizen(req, opts = {}) {
|
|
335
|
+
const decision = sovereignSpawn(req);
|
|
336
|
+
if (!decision.approved)
|
|
337
|
+
return { decision };
|
|
338
|
+
const owner = deriveOwner(req.requester_did);
|
|
339
|
+
const citizen = (0, citizenship_1.mintCitizen)(opts.agent_name ?? req.requested_role, {
|
|
340
|
+
owner,
|
|
341
|
+
citizen_type: 'spawned',
|
|
342
|
+
proposing_agent: req.requester_did,
|
|
343
|
+
now: opts.now,
|
|
344
|
+
});
|
|
345
|
+
decision.citizen_did = citizen.agent_did;
|
|
346
|
+
// Seed the initial ACP at birth (0–100) so the citizen has a routing profile.
|
|
347
|
+
writeInitialAcp(citizen.agent_name, decision.initial_acp_score);
|
|
348
|
+
return { decision, citizen };
|
|
349
|
+
}
|
|
252
350
|
/**
|
|
253
351
|
* Emergency bypass — Founder only. Overrides all gates including health and
|
|
254
352
|
* constitutional conditional findings. Logs prominently to KSL.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kognai/orchestrator-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Kognai sovereign orchestrator — core engine (template-agnostic). Shared by all products (Kognai/coding, Voxight/market-intel, Invoica/fin-compliance); each supplies only its template. Replaces per-repo forks of orchestrate-agents-v2 / sprint-runner / lib.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SkinGem",
|