@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
- /** Owning company. When provided, the mint uses company-scoped roll
86
- * numbers (separate sequence per company), the `{PREFIX}-{rollNum padded}`
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. */
@@ -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
- // Idempotency: lookup by (agent_name, company). Treat undefined company as
69
- // 'kognai' so existing records (which predate the company field) match
70
- // mints that explicitly target the kognai company.
71
- const companyKey = opts.company; // undefined for legacy path
72
- const existing = registry.citizens.find((c) => c.agent_name === agent_name && (c.company ?? 'kognai') === (companyKey ?? 'kognai'));
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 (companyKey) {
80
- // Company-scoped path: per-company rollNumber, prefixed ID, scoped DID.
81
- const prefix = exports.COMPANY_PREFIXES[companyKey] ?? companyKey.slice(0, 3).toUpperCase();
82
- const companyRolls = registry.citizens
83
- .filter((c) => (c.company ?? 'kognai') === companyKey)
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 = companyRolls.length > 0 ? Math.max(...companyRolls) + 1 : 1;
179
+ rollNumber = rolls.length ? Math.max(...rolls) + 1 : 1;
86
180
  citizen_id = `${prefix}-${String(rollNumber).padStart(4, '0')}`;
87
- agent_did = `did:kognai:${companyKey}:${agent_name}`;
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
- ...(companyKey ? { company: companyKey } : {}),
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
- // Only advance the global next_roll_number on legacy mints — company-scoped
116
- // mints have their own per-company counter computed at mint time.
117
- if (!companyKey)
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 approvetake the average score, merge strengths
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 CONSENSUS: Both supervisors APPROVED (Sup1: ${review1.score}, Sup2: ${review2.score}, avg: ${avgScore})`);
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: `Dual-approved: Sup1 (${review1.score}/100) + Sup2 (${review2.score}/100)`,
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
- if (bothRejected) {
220
- // Both reject merge issues, take lower score
221
- const minScore = Math.min(review1.score, review2.score);
222
- (0, orchestrate_engine_1.log)(orchestrate_engine_1.c.red, ` ✗ DUAL CONSENSUS: Both supervisors REJECTED (Sup1: ${review1.score}, Sup2: ${review2.score})`);
223
- return {
224
- finalReview: {
225
- verdict: 'REJECTED',
226
- score: minScore,
227
- summary: `Dual-rejected: Sup1 (${review1.score}/100) + Sup2 (${review2.score}/100). ${review1.summary} | ${review2.summary}`,
228
- issues: [...review1.issues, ...review2.issues],
229
- strengths: [],
230
- },
231
- review1, review2, consensus: true, escalatedToCEO: false,
232
- };
233
- }
234
- // CONFLICT one approved, one rejected → escalate to CEO
235
- const approver = review1.verdict === 'APPROVED' ? 'Sup1' : 'Sup2';
236
- const rejecter = review1.verdict === 'APPROVED' ? 'Sup2' : 'Sup1';
237
- const approvalReview = review1.verdict === 'APPROVED' ? review1 : review2;
238
- const rejectionReview = review1.verdict === 'APPROVED' ? review2 : review1;
239
- (0, orchestrate_engine_1.log)(orchestrate_engine_1.c.yellow, ` ⚡ SUPERVISOR CONFLICT on ${task.id}: ${approver} APPROVED (${approvalReview.score}), ${rejecter} REJECTED (${rejectionReview.score})`);
240
- (0, orchestrate_engine_1.log)(orchestrate_engine_1.c.magenta, ` → Escalating to CEO for final decision...`);
241
- try {
242
- const ceoDecision = await ceo.resolveReviewConflict(task, approvalReview, rejectionReview, approver, rejecter);
243
- const ceoApproves = ceoDecision.toLowerCase().includes('approve');
244
- (0, orchestrate_engine_1.log)(ceoApproves ? orchestrate_engine_1.c.green : orchestrate_engine_1.c.red, ` CEO DECISION: ${ceoApproves ? 'APPROVED' : 'REJECTED'} — ${ceoDecision.substring(0, 200)}`);
245
- return {
246
- finalReview: {
247
- verdict: ceoApproves ? 'APPROVED' : 'REJECTED',
248
- score: ceoApproves ? approvalReview.score : rejectionReview.score,
249
- summary: `CEO resolved conflict (${approver} approved, ${rejecter} rejected): ${ceoDecision.substring(0, 300)}`,
250
- issues: rejectionReview.issues,
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 0.85;
59
- case 'CITIZEN': return 0.70;
60
- case 'SPECIALIST': return 0.70;
61
- case 'UTILITY': return 0.65;
62
- case 'EXTERNAL': return 0.30;
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.1.4",
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",