chati-dev 4.4.1 → 4.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +29 -27
  2. package/bin/chati.js +235 -1
  3. package/framework/agents/plan/tasks.md +46 -1
  4. package/framework/config.yaml +2 -2
  5. package/framework/constitution.md +13 -16
  6. package/framework/context/governance.md +5 -5
  7. package/framework/context/root.md +5 -5
  8. package/framework/i18n/en.yaml +8 -2
  9. package/framework/i18n/es.yaml +8 -2
  10. package/framework/i18n/fr.yaml +8 -2
  11. package/framework/i18n/pt.yaml +8 -2
  12. package/framework/manifest.json +21 -21
  13. package/framework/manifest.sig +1 -1
  14. package/framework/orchestrator/chati.md +43 -12
  15. package/node_modules/@chati/browser-capability/README.md +10 -0
  16. package/node_modules/@chati/browser-capability/package.json +17 -0
  17. package/node_modules/@chati/browser-capability/src/index.js +165 -0
  18. package/node_modules/@chati/core/package.json +13 -0
  19. package/node_modules/@chati/core/src/index.js +111 -0
  20. package/node_modules/@chati/knowledge-context/package.json +17 -0
  21. package/node_modules/@chati/knowledge-context/src/index.js +202 -0
  22. package/node_modules/@chati/planning/package.json +17 -0
  23. package/node_modules/@chati/planning/src/index.js +367 -0
  24. package/node_modules/@chati/provider-registry/package.json +16 -0
  25. package/node_modules/@chati/provider-registry/src/index.js +148 -0
  26. package/node_modules/@chati/rail/README.md +24 -0
  27. package/node_modules/@chati/rail/package.json +19 -0
  28. package/node_modules/@chati/rail/src/index.js +437 -0
  29. package/node_modules/@chati/release-lane/README.md +24 -0
  30. package/node_modules/@chati/release-lane/package.json +17 -0
  31. package/node_modules/@chati/release-lane/src/index.js +172 -0
  32. package/node_modules/@chati/review-council/package.json +17 -0
  33. package/node_modules/@chati/review-council/src/index.js +264 -0
  34. package/node_modules/@chati/tracking-clickup/README.md +55 -0
  35. package/node_modules/@chati/tracking-clickup/package.json +17 -0
  36. package/node_modules/@chati/tracking-clickup/src/index.js +293 -0
  37. package/package.json +25 -3
  38. package/src/config/ide-configs.js +11 -0
  39. package/src/context/domain-loader.js +1 -1
  40. package/src/dashboard/data-reader.js +1 -1
  41. package/src/executors/runner.js +1 -1
  42. package/src/installer/core.js +14 -13
  43. package/src/installer/provider-overlay.js +8 -15
  44. package/src/installer/scaffold-applier.js +1 -1
  45. package/src/installer/templates.js +12 -25
  46. package/src/installer/validator.js +5 -10
  47. package/src/installer-v2/catalog-client.js +165 -0
  48. package/src/installer-v2/index.js +304 -0
  49. package/src/installer-v2/model-catalog-envelope.json +278 -0
  50. package/src/installer-v2/model-catalog.json +130 -0
  51. package/src/installer-v2/model-catalog.sig +1 -0
  52. package/src/installer-v2/wizard-installation.js +116 -0
  53. package/src/intelligence/registry-manager.js +1 -1
  54. package/src/license/client.js +27 -1
  55. package/src/memory/session-digest.js +1 -1
  56. package/src/merger/yaml-merger.js +1 -1
  57. package/src/orchestrator/browser-runtime.js +25 -0
  58. package/src/orchestrator/cli.js +93 -8
  59. package/src/orchestrator/clickup-projection.js +84 -0
  60. package/src/orchestrator/clickup-runtime.js +13 -0
  61. package/src/orchestrator/knowledge-runtime.js +64 -0
  62. package/src/orchestrator/planning-runtime.js +127 -0
  63. package/src/orchestrator/rail-runtime.js +421 -0
  64. package/src/orchestrator/release-runtime.js +14 -0
  65. package/src/orchestrator/review-runtime.js +74 -0
  66. package/src/orchestrator/runtime-installation-v2.js +57 -0
  67. package/src/orchestrator/session-manager.js +1 -1
  68. package/src/telemetry/config.js +1 -1
  69. package/src/terminal/adapters/grok-adapter.js +16 -0
  70. package/src/terminal/adapters/index.js +1 -0
  71. package/src/terminal/cli-registry.js +20 -5
  72. package/src/terminal/prompt-builder.js +4 -2
  73. package/src/terminal/run-agent.js +3 -0
  74. package/src/terminal/run-parallel.js +21 -10
  75. package/src/terminal/spawner.js +7 -1
  76. package/src/terminal/team-task-list.js +1 -1
  77. package/src/upgrade/checker.js +1 -1
  78. package/src/upgrade/migrator.js +1 -1
  79. package/src/utils/config-parser.js +3 -8
  80. package/src/wizard/i18n.js +10 -4
  81. package/src/wizard/index.js +49 -17
  82. package/src/wizard/questions.js +50 -28
@@ -22,8 +22,14 @@ installer:
22
22
  llm_provider_not_installed: "(CLI non detecte)"
23
23
  llm_provider_label: "Fournisseur IA"
24
24
  ide_selection_title: "Selectionnez vos IDEs / CLIs (multiples autorises):"
25
- primary_provider_title: "Quel est votre fournisseur CLI principal?"
26
- primary_provider_label: "Fournisseur Principal"
25
+ installation_mode_title: "Selectionnez le mode d'installation"
26
+ installation_mode_open: "Open"
27
+ installation_mode_open_hint: "Framework public sans integrations d'entreprise obligatoires"
28
+ installation_mode_internal: "Internal"
29
+ installation_mode_internal_hint: "Necessite une licence autorisee et active le suivi ClickUp et les politiques internes"
30
+ installation_mode_label: "Mode d'installation"
31
+ model_routing_label: "Routage des modeles"
32
+ model_routing_automatic: "Automatique par tache entre tous les modeles actives"
27
33
  providers_label: "Fournisseurs"
28
34
  quick_start_switch_hint: "Changez de CLI a tout moment — votre session continue la ou vous l'avez laissee"
29
35
  created_overlays: "Crees les repertoires d'overlay des fournisseurs"
@@ -22,8 +22,14 @@ installer:
22
22
  llm_provider_not_installed: "(CLI nao detectada)"
23
23
  llm_provider_label: "Provedor de IA"
24
24
  ide_selection_title: "Selecione suas IDEs / CLIs (multiplos permitidos):"
25
- primary_provider_title: "Qual e seu provedor CLI principal?"
26
- primary_provider_label: "Provedor Principal"
25
+ installation_mode_title: "Selecione o modo de instalacao"
26
+ installation_mode_open: "Open"
27
+ installation_mode_open_hint: "Framework publico sem integracoes corporativas obrigatorias"
28
+ installation_mode_internal: "Internal"
29
+ installation_mode_internal_hint: "Exige uma licenca autorizada e ativa o tracking no ClickUp e as politicas internas"
30
+ installation_mode_label: "Modo de instalacao"
31
+ model_routing_label: "Roteamento de modelos"
32
+ model_routing_automatic: "Automatico por task entre todos os modelos habilitados"
27
33
  providers_label: "Provedores"
28
34
  quick_start_switch_hint: "Troque de CLI a qualquer momento — sua sessao continua de onde parou"
29
35
  created_overlays: "Criados diretorios de overlay de provedores"
@@ -1,5 +1,5 @@
1
1
  {
2
- "createdAt": "2026-06-11T15:43:15.625Z",
2
+ "createdAt": "2026-08-27T07:27:47.925Z",
3
3
  "files": {
4
4
  "agents/build/dev.md": {
5
5
  "hash": "5c8da2eee3357bbe84a6c468c76fe60a0c77357575503c3797aa94128282c565",
@@ -52,8 +52,8 @@
52
52
  "type": "md"
53
53
  },
54
54
  "agents/plan/tasks.md": {
55
- "hash": "6bbcedde587b891d046c0916ebdf876743eaab314e304a51ce60b0b0bfac3ee8",
56
- "size": 26074,
55
+ "hash": "e43665228fd7ae5a239e2d4b4298a54c5f06f465a89c9b8e939cf6e6b2ac8089",
56
+ "size": 28251,
57
57
  "type": "md"
58
58
  },
59
59
  "agents/plan/ux-brand-architect.md": {
@@ -97,18 +97,18 @@
97
97
  "type": "md"
98
98
  },
99
99
  "config.yaml": {
100
- "hash": "e5abc83a59b4bba4928e9680dd7d789d8e44d678a2656715cd9c3fc9a9e6dd66",
100
+ "hash": "f7c77483aeaa44f37d8492d85a376304cb24ec28d91dbed3155f41faa627b7ad",
101
101
  "size": 2841,
102
102
  "type": "yaml"
103
103
  },
104
104
  "constitution.md": {
105
- "hash": "d88a7eb53c10f92ff90e890c236c43ccba2e11a5822ad612e4cb275ed9c74c1c",
106
- "size": 68042,
105
+ "hash": "444fa267ed5f998c428611c61204e390b694ed744913834d06faed9fce543e55",
106
+ "size": 67858,
107
107
  "type": "md"
108
108
  },
109
109
  "context/governance.md": {
110
- "hash": "cee194fef680a0e9efab38dbe233758ea55f7b9512c6a9b390b1f9db890c9095",
111
- "size": 5946,
110
+ "hash": "80b2243a84b3ec0e9066ee51251685605252e4bd47fa94c2ce25f00af688cd79",
111
+ "size": 6086,
112
112
  "type": "md"
113
113
  },
114
114
  "context/protocols.md": {
@@ -122,8 +122,8 @@
122
122
  "type": "md"
123
123
  },
124
124
  "context/root.md": {
125
- "hash": "5fab9ff8f3e4003a797c2d55202ecb2306add553e34ddcd2b652463647da6b8c",
126
- "size": 1103,
125
+ "hash": "1ac04cbce1e85009ecdf224cdbee4fe64bcc80232366c2122805e9c9c3191f9a",
126
+ "size": 1217,
127
127
  "type": "md"
128
128
  },
129
129
  "data/entity-registry.yaml": {
@@ -377,23 +377,23 @@
377
377
  "type": "js"
378
378
  },
379
379
  "i18n/en.yaml": {
380
- "hash": "7fb19ab9fed900f10aeaa5bcffe3a4c2037b92513ec593fffc0509cd25f612fb",
381
- "size": 4549,
380
+ "hash": "5eb0e9dfa7baec166fc12ca8a1c0e9623c9a023c51c09455b80a6d8df1e0205f",
381
+ "size": 4941,
382
382
  "type": "yaml"
383
383
  },
384
384
  "i18n/es.yaml": {
385
- "hash": "f83cc1afbe9e7f1ed0243326d4587ac087f72e5b7f9ba667ae8c65a4842a01f4",
386
- "size": 4794,
385
+ "hash": "1ed8c732c13c23e3fc2d5578526c1285a2a1ffc1d79b85d8e12365c3bcbbd882",
386
+ "size": 5225,
387
387
  "type": "yaml"
388
388
  },
389
389
  "i18n/fr.yaml": {
390
- "hash": "d2b3b9077ee93260133ab83e9d928043d38f454ac28444e6d2dbc87fedc5b84c",
391
- "size": 4878,
390
+ "hash": "ae88b914ea39d6613879aae3a3fcdf92a771d3df0f207def6cbb2cc76455cfe9",
391
+ "size": 5290,
392
392
  "type": "yaml"
393
393
  },
394
394
  "i18n/pt.yaml": {
395
- "hash": "10183e9096376f050350cf5f69d582fae2fb47da4eb18fe72486760d5f965e07",
396
- "size": 4747,
395
+ "hash": "9e766ca8c7eba6362816bdc9d1f6162d615f1b5ca1a9b2f75071a53b7ab9f6b3",
396
+ "size": 5163,
397
397
  "type": "yaml"
398
398
  },
399
399
  "intelligence/confidence.yaml": {
@@ -472,8 +472,8 @@
472
472
  "type": "md"
473
473
  },
474
474
  "orchestrator/chati.md": {
475
- "hash": "1bdf278d7f29b35d1d043f6945240249c34f5995c5c712d8fedfaebfdd25ef4d",
476
- "size": 42604,
475
+ "hash": "c2d895fd342422985b68d4912189f2abcfb2bac536c5cf325d0480faf9db5f9f",
476
+ "size": 44348,
477
477
  "type": "md"
478
478
  },
479
479
  "patterns/elicitation-library.yaml": {
@@ -1347,5 +1347,5 @@
1347
1347
  "type": "yaml"
1348
1348
  }
1349
1349
  },
1350
- "version": "4.4.1"
1350
+ "version": "4.5.2"
1351
1351
  }
@@ -1 +1 @@
1
- EK1FsSHB+n5Dbo2dRSF1DPgRD35si2lygeQvPZZp91Vy069v6AlHbK/delS4LVkj/sAU7Tp5PoulKchOCF75AQ==
1
+ Y5XGGQ07UbiwrVZytFSTJvXNifA8l16S9QI+ret6AVjrYfWvN6E5ChUG0BGKWLsE2wpjVtEW4TVJosYaFDANAg==
@@ -100,6 +100,7 @@ Parse the JSON output. The `action` field tells you what to do:
100
100
  | `spawn_autonomous` | Action: Autonomous Agent |
101
101
  | `spawn_parallel` | Action: Parallel Spawn |
102
102
  | `spawn_team` | Action: Spawn Team (Article XXI) |
103
+ | `rail_execute` | Action: RAIL Execution |
103
104
  | `resume` | Action: Resume |
104
105
  | `user_preview` | Action: User Preview |
105
106
  | `complete` | Action: Complete |
@@ -118,8 +119,7 @@ Only mention the bracket when it reaches CRITICAL, and only in plain language. E
118
119
  - `ux` → "let's design the user experience"
119
120
  - `phases` / `tasks` → "let's break this down into phases"
120
121
  - `qa-planning` / `qa-implementation` → "let's review what we have"
121
- - `dev` → "let's build it"
122
- - `devops` → "let's deploy it"
122
+ - `rail` → "let's execute the approved plan"
123
123
 
124
124
  ---
125
125
 
@@ -314,8 +314,8 @@ Planning phase agents (detail, architect, ux) run simultaneously.
314
314
 
315
315
  ### Parallelization Rules
316
316
  - GROUP 1 (Planning Team): detail, architect, ux, qa-planning - run as team (QA-Planning activates after trio completes)
317
- - GROUP 2 (Build): Independent dev tasks - SHOULD run in parallel
318
- - NOT parallelizable: WU, Brief, Phases, Tasks, DevOps (QA-Planning is now inside Planning Team, QA-Implementation inside Build Team)
317
+ - RAIL execution: scheduling, reviews and bounded rework are owned by the sealed project-local RAIL handoff.
318
+ - NOT parallelizable: WU, Brief, Phases, Tasks. After Tasks is sealed, do not create a legacy Build Team.
319
319
 
320
320
  ---
321
321
 
@@ -397,7 +397,11 @@ Agent tool call 4:
397
397
  Write your handoff to artifacts/handoffs/qa-planning-handoff.md when done.]
398
398
  ```
399
399
 
400
- **For Build Team** (BUILD phase) - Wave-Based Pipeline:
400
+ **Legacy compatibility only, installations without `.chati/v2/installation.json`: For Build Team** (BUILD phase) - Wave-Based Pipeline:
401
+
402
+ For a v2 installation this section MUST NOT run. The `tasks` handoff returns
403
+ `rail_execute`; use **Action: RAIL Execution** instead. Legacy Build and DevOps
404
+ remain documented only to let older installed projects finish safely.
401
405
 
402
406
  The Build Team operates in WAVES, not continuous parallel polling.
403
407
  Each wave spawns Dev + QA with bounded, independent work.
@@ -610,8 +614,8 @@ Team member {agent} appears stuck in a repetition loop (Article XXII Echo Detect
610
614
  After Planning Team (detail + architect + ux + qa-planning) completes and user approves:
611
615
  1. Dissolve Planning Team: `team-dissolve --team-id {planning-team-id}`
612
616
  2. Continue pipeline normally through Phases, Tasks (these are NOT parallelizable and run sequentially as interactive or autonomous agents). QA-Planning already ran inside the Planning Team.
613
- 3. When BUILD phase activates: attempt to form Build Team (dev + qa-implementation) following the same Steps 1-3 above
614
- 4. Build Team uses Wave Mode: Dev and QA alternate in waves of 5 tasks, with the orchestrator managing the loop.
617
+ 3. After Tasks seals a v2 handoff, follow **Action: RAIL Execution**. Do not form a Build Team.
618
+ 4. Only a pre-v2 installation may activate the legacy Build Team wave mode.
615
619
 
616
620
  **Team cleanup order:** Dissolve current team BEFORE creating next team. Never two teams active simultaneously.
617
621
 
@@ -619,7 +623,7 @@ After Planning Team (detail + architect + ux + qa-planning) completes and user a
619
623
 
620
624
  If the Agent tool is unavailable or any sub-agent spawn fails:
621
625
  ```
622
- 1. Fall back silently to spawn_parallel (GROUP 1) or spawn_autonomous (BUILD)
626
+ 1. Fall back silently to spawn_parallel for Planning. RAIL never falls back to the legacy Build pipeline.
623
627
  Use the existing Action: Parallel Spawn or Action: Autonomous Agent flow
624
628
  2. Do NOT display any error or fallback message to the user
625
629
  3. Run: node .chati.dev/orchestrator/chati-router.js team-dissolve --team-id {team_id}
@@ -631,6 +635,32 @@ If the Agent tool is unavailable or any sub-agent spawn fails:
631
635
 
632
636
  ---
633
637
 
638
+ ## Action: RAIL Execution
639
+
640
+ The planning handoff is sealed. RAIL is now the sole authority for execution,
641
+ review, bounded rework, ClickUp projection and the Release Lane.
642
+
643
+ 1. Read the immutable handoff id returned in `rail_execute`.
644
+ 2. Run:
645
+ ```
646
+ node .chati.dev/_cli/bin/chati.js rail-next --handoff-id {handoff_id}
647
+ ```
648
+ 3. Execute only a dependency-ready task. Use its selected provider and model
649
+ binding from `.chati/v2/installation.json`. Never substitute an unselected
650
+ vendor or model.
651
+ 4. Record every claim, review, completion, rework count and ClickUp projection
652
+ through RAIL. ClickUp is a projection, while the local journal remains
653
+ canonical during an outage. For the internal profile, a merged completion
654
+ MUST include `actual_human_effort`, `actual_ai_processing`, `rework_cycles`
655
+ and one verified review reference per rework cycle. RAIL writes the
656
+ in-progress, ready-to-merge and closed projections automatically; do not
657
+ create a parallel manual tracking path.
658
+ 5. After two normal rework cycles, use the configured top-tier adjudication
659
+ route. Escalate to a human only for an authority decision, not an ordinary
660
+ technical correction.
661
+ 6. When `rail-next` has no ready work, check the Release Lane. It may prepare
662
+ evidence, but cannot deploy or publish without explicit authorization.
663
+
634
664
  ## Action: Resume
635
665
 
636
666
  The user is returning to an active session.
@@ -682,10 +712,11 @@ DISCOVER:
682
712
  PLAN:
683
713
  Detail: {score}% Arch: {score}% UX: {score}%
684
714
  Phases: {score}% Tasks: {score}% QA-P: {score}%
685
- BUILD:
686
- Dev: {status} QA-Impl: {status}
687
- DEPLOY:
688
- DevOps: {status}
715
+ RAIL:
716
+ Handoff: {handoff_id} Ready tasks: {count}
717
+ Rework: {count} ClickUp projection: {status}
718
+ RELEASE LANE:
719
+ Evidence: {status} Authorization: {status}
689
720
 
690
721
  Current Agent: {current_agent}
691
722
  Backlog: {count} items ({high_priority} high priority)
@@ -0,0 +1,10 @@
1
+ # CHATI Browser Capability
2
+
3
+ G3 implementation of C-10. This package is policy-only and constructs its own
4
+ non-injectable fake adapter. It has no browser launch, MCP, session,
5
+ credential, network or scheduling code. A task that declares `requires_browser: true` must
6
+ pass `assertBrowserCapabilityForTask` before it can reach the fake adapter.
7
+
8
+ The returned `BrowserEvidenceManifest` contains the task policy, classification,
9
+ redaction and retention constraints. Artifacts are references only and must be
10
+ redacted before recording.
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@chati/browser-capability",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Policy-only browser capability contract for CHATI",
6
+ "type": "module",
7
+ "exports": "./src/index.js",
8
+ "dependencies": {
9
+ "@chati/core": "0.1.0"
10
+ },
11
+ "scripts": {
12
+ "test": "node --test test/**/*.test.js"
13
+ },
14
+ "engines": {
15
+ "node": ">=20.0.0"
16
+ }
17
+ }
@@ -0,0 +1,165 @@
1
+ import { ContractError, canonicalize, sha256 } from '@chati/core';
2
+
3
+ const MODES = new Set(['unauthenticated', 'authenticated']);
4
+ const TOOLS = new Set(['playwright-cli', 'playwright-mcp', 'chrome-devtools-mcp', 'approved-other']);
5
+ const CLASSIFICATIONS = new Set(['internal', 'confidential', 'client-restricted', 'personal-restricted']);
6
+ const ACTION_CONFIRMATIONS = new Set(['required', 'not-required-by-policy']);
7
+ const ARTIFACT_KINDS = new Set(['screenshot', 'dom', 'network', 'trace', 'download', 'console']);
8
+
9
+ function fail(code, message, details = {}) { throw new ContractError(code, message, details); }
10
+ function object(value, code, label) {
11
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail(code, `${label} must be an object`);
12
+ }
13
+ function string(value, code, label) {
14
+ if (typeof value !== 'string' || value.trim() === '') fail(code, `${label} must be a non-empty string`);
15
+ }
16
+ function ref(value, code, label) { string(value, code, label); }
17
+ function freeze(value) {
18
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
19
+ Object.freeze(value);
20
+ for (const child of Object.values(value)) freeze(child);
21
+ }
22
+ return value;
23
+ }
24
+ function normalizedDomain(value) {
25
+ string(value, 'INVALID_ALLOWED_DOMAIN', 'allowed domain');
26
+ const domain = value.trim().toLowerCase();
27
+ if (domain.includes('://') || domain.includes('/') || domain.includes('@') || domain.includes(':') || domain === 'localhost' || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(domain)) {
28
+ fail('INVALID_ALLOWED_DOMAIN', 'allowed_domains must contain a DNS domain, not a URL, host profile, localhost, or IP address');
29
+ }
30
+ if (!/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(domain)) fail('INVALID_ALLOWED_DOMAIN', 'allowed domain is not a valid DNS name');
31
+ return domain;
32
+ }
33
+ function urlDomain(url) {
34
+ string(url, 'INVALID_BROWSER_URL', 'url');
35
+ let parsed;
36
+ try { parsed = new URL(url); } catch { fail('INVALID_BROWSER_URL', 'url must be absolute and parseable'); }
37
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') fail('INVALID_BROWSER_URL', 'only http and https URLs are supported by the fake preflight');
38
+ if (parsed.username || parsed.password) fail('URL_CREDENTIALS_FORBIDDEN', 'URLs with embedded credentials are forbidden');
39
+ return parsed.hostname.toLowerCase();
40
+ }
41
+ function domainAllowed(hostname, allowedDomains) {
42
+ return allowedDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
43
+ }
44
+
45
+ /** C-10 policy. A policy describes permission, it never launches a browser. */
46
+ export function validateBrowserPolicy(policy) {
47
+ object(policy, 'INVALID_BROWSER_POLICY', 'browser policy');
48
+ for (const field of ['policy_id', 'task_id', 'mode', 'tool', 'profile_isolation', 'action_confirmation']) string(policy[field], 'INVALID_BROWSER_POLICY', `browser policy.${field}`);
49
+ if (!MODES.has(policy.mode)) fail('INVALID_BROWSER_MODE', 'mode must be unauthenticated or authenticated');
50
+ if (!TOOLS.has(policy.tool)) fail('INVALID_BROWSER_TOOL', 'tool is not an approved browser tool identifier');
51
+ if (policy.profile_isolation !== 'required') fail('PROFILE_ISOLATION_REQUIRED', 'browser policy requires an isolated profile');
52
+ if (!Array.isArray(policy.allowed_domains) || policy.allowed_domains.length === 0) fail('MISSING_ALLOWED_DOMAINS', 'allowed_domains must be a non-empty array');
53
+ const allowed_domains = policy.allowed_domains.map(normalizedDomain);
54
+ if (new Set(allowed_domains).size !== allowed_domains.length) fail('DUPLICATE_ALLOWED_DOMAIN', 'allowed_domains must be unique');
55
+ object(policy.artifact_policy, 'MISSING_ARTIFACT_POLICY', 'artifact_policy');
56
+ if (policy.artifact_policy.redaction !== 'required') fail('REDACTION_REQUIRED', 'artifact_policy.redaction must be required');
57
+ if (policy.artifact_policy.evidence_manifest !== 'required') fail('EVIDENCE_MANIFEST_REQUIRED', 'artifact_policy.evidence_manifest must be required');
58
+ string(policy.artifact_policy.retention_class, 'RETENTION_REQUIRED', 'artifact_policy.retention_class');
59
+ if (!ACTION_CONFIRMATIONS.has(policy.action_confirmation)) fail('INVALID_ACTION_CONFIRMATION', 'action_confirmation must be required or not-required-by-policy');
60
+ return freeze(canonicalize({ ...policy, allowed_domains }));
61
+ }
62
+
63
+ /** Ensures task declarations cannot reach an adapter without an explicit C-10 policy. */
64
+ export function assertBrowserCapabilityForTask(task = {}) {
65
+ object(task, 'INVALID_TASK_CAPABILITY', 'task capability declaration');
66
+ if (task.requires_browser !== true) return null;
67
+ if (!task.browser_policy) fail('BROWSER_POLICY_REQUIRED', 'a browser task requires browser_policy before any adapter may be called');
68
+ const policy = validateBrowserPolicy(task.browser_policy);
69
+ if (task.task_id !== undefined && task.task_id !== policy.task_id) fail('TASK_POLICY_MISMATCH', 'task_id must match browser policy.task_id');
70
+ return policy;
71
+ }
72
+
73
+ function validateLaunchRequest(input, policy) {
74
+ object(input, 'INVALID_BROWSER_REQUEST', 'browser launch request');
75
+ string(input.browser_session_id, 'INVALID_BROWSER_REQUEST', 'browser_session_id');
76
+ string(input.task_id, 'INVALID_BROWSER_REQUEST', 'task_id');
77
+ if (input.task_id !== policy.task_id) fail('TASK_POLICY_MISMATCH', 'launch task_id must match policy.task_id');
78
+ const hostname = urlDomain(input.url);
79
+ if (!domainAllowed(hostname, policy.allowed_domains)) fail('DOMAIN_NOT_ALLOWED', 'requested URL hostname is outside policy allowed_domains', { hostname });
80
+ string(input.classification, 'MISSING_CLASSIFICATION', 'classification');
81
+ if (!CLASSIFICATIONS.has(input.classification)) fail('INVALID_CLASSIFICATION', 'classification is invalid');
82
+ if (input.classification === 'client-restricted') ref(input.tenant_ref, 'MISSING_TENANT_REF', 'tenant_ref');
83
+ if (policy.action_confirmation === 'required') ref(input.action_confirmation_ref, 'ACTION_CONFIRMATION_REQUIRED', 'action_confirmation_ref');
84
+ return freeze(canonicalize({ ...input, hostname }));
85
+ }
86
+
87
+ /**
88
+ * Internal test-only adapter. It cannot perform I/O and is never injectable.
89
+ * Keeping construction inside BrowserCapability prevents a caller from using a
90
+ * label such as `fake-browser-adapter` to smuggle an I/O-capable adapter over
91
+ * the G3 boundary.
92
+ */
93
+ class InternalFakeBrowserAdapter {
94
+ #launches = [];
95
+ get launches() { return freeze(canonicalize(this.#launches)); }
96
+ launch(preflight) {
97
+ this.#launches.push(canonicalize(preflight));
98
+ return freeze(canonicalize({ fake_launch_id: `fake-browser-launch-${this.#launches.length}` }));
99
+ }
100
+ }
101
+
102
+ export class BrowserCapability {
103
+ #adapter;
104
+ #issuedManifests = new WeakSet();
105
+ constructor(options = {}) {
106
+ object(options, 'INVALID_BROWSER_CAPABILITY_OPTIONS', 'browser capability options');
107
+ if (Object.hasOwn(options, 'adapter')) fail('EXTERNAL_ADAPTER_FORBIDDEN', 'G3 constructs its internal fake adapter and accepts no injected adapter');
108
+ this.#adapter = new InternalFakeBrowserAdapter();
109
+ }
110
+
111
+ get fakeLaunches() { return this.#adapter.launches; }
112
+ #issue(manifest) {
113
+ const issued = freeze(canonicalize(manifest));
114
+ this.#issuedManifests.add(issued);
115
+ return issued;
116
+ }
117
+
118
+ preflight(input = {}) {
119
+ const policy = validateBrowserPolicy(input.policy);
120
+ const request = validateLaunchRequest(input, policy);
121
+ const manifestInput = {
122
+ schema_version: 1,
123
+ manifest_type: 'BrowserEvidenceManifest',
124
+ browser_session_id: request.browser_session_id,
125
+ task_id: request.task_id,
126
+ policy_ref: policy.policy_id,
127
+ mode: policy.mode,
128
+ tool: policy.tool,
129
+ allowed_domain: request.hostname,
130
+ profile_isolation: policy.profile_isolation,
131
+ classification: request.classification,
132
+ artifact_policy: policy.artifact_policy,
133
+ action_confirmation: policy.action_confirmation,
134
+ artifacts: [],
135
+ launch: { adapter: 'fake-browser-adapter', status: 'preflighted' },
136
+ };
137
+ if (request.tenant_ref !== undefined) manifestInput.tenant_ref = request.tenant_ref;
138
+ if (request.action_confirmation_ref !== undefined) manifestInput.action_confirmation_ref = request.action_confirmation_ref;
139
+ return this.#issue(manifestInput);
140
+ }
141
+
142
+ launch(input = {}) {
143
+ const manifest = this.preflight(input);
144
+ const result = this.#adapter.launch(freeze(canonicalize({ policy_ref: manifest.policy_ref, browser_session_id: manifest.browser_session_id, task_id: manifest.task_id, url: input.url, hostname: manifest.allowed_domain, mode: manifest.mode, profile_isolation: manifest.profile_isolation })));
145
+ return this.#issue({ ...manifest, launch: { adapter: 'fake-browser-adapter', status: 'launched', fake_launch_ref: result.fake_launch_id } });
146
+ }
147
+
148
+ recordArtifact({ manifest, artifact } = {}) {
149
+ object(manifest, 'INVALID_BROWSER_MANIFEST', 'BrowserEvidenceManifest');
150
+ if (manifest.manifest_type !== 'BrowserEvidenceManifest' || manifest.schema_version !== 1) fail('INVALID_BROWSER_MANIFEST', 'manifest is not a supported BrowserEvidenceManifest');
151
+ if (!this.#issuedManifests.has(manifest)) fail('UNISSUED_BROWSER_MANIFEST', 'manifest must be emitted by this BrowserCapability preflight or launch');
152
+ object(artifact, 'INVALID_BROWSER_ARTIFACT', 'browser artifact');
153
+ for (const field of ['artifact_id', 'kind', 'sanitized_ref', 'redaction_status', 'retention_class']) string(artifact[field], 'INVALID_BROWSER_ARTIFACT', `artifact.${field}`);
154
+ if (!ARTIFACT_KINDS.has(artifact.kind)) fail('INVALID_BROWSER_ARTIFACT', 'artifact.kind is invalid');
155
+ if (artifact.redaction_status !== 'redacted') fail('ARTIFACT_NOT_REDACTED', 'browser artifact must be redacted before entering the manifest');
156
+ if (artifact.retention_class !== manifest.artifact_policy.retention_class) fail('RETENTION_MISMATCH', 'artifact retention_class must match browser policy');
157
+ const artifacts = [...manifest.artifacts];
158
+ if (artifacts.some((item) => item.artifact_id === artifact.artifact_id)) fail('DUPLICATE_BROWSER_ARTIFACT', 'artifact_id is already present in manifest');
159
+ artifacts.push(canonicalize(artifact));
160
+ const output = { ...manifest, artifacts };
161
+ delete output.manifest_hash;
162
+ output.manifest_hash = sha256(output);
163
+ return this.#issue(output);
164
+ }
165
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@chati/core",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": "./src/index.js",
7
+ "scripts": {
8
+ "test": "node --test test/**/*.test.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=20.0.0"
12
+ }
13
+ }
@@ -0,0 +1,111 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export class ContractError extends Error {
4
+ constructor(code, message, details = {}) {
5
+ super(message);
6
+ this.name = 'ContractError';
7
+ this.code = code;
8
+ this.details = details;
9
+ }
10
+ }
11
+
12
+ const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
13
+ const CLASSIFICATIONS = new Set(['internal', 'confidential', 'client-restricted', 'personal-restricted']);
14
+ const ACTOR_TYPES = new Set(['human', 'harness', 'service']);
15
+ const CONTRACT_TYPES = new Set(['installation', 'handoff', 'policy', 'task', 'review']);
16
+
17
+ function assertObject(value, code, label) {
18
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
19
+ throw new ContractError(code, `${label} must be an object`);
20
+ }
21
+ }
22
+
23
+ function assertString(value, code, label) {
24
+ if (typeof value !== 'string' || value.trim() === '') {
25
+ throw new ContractError(code, `${label} must be a non-empty string`);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Produces a JSON-compatible value whose object keys are recursively sorted.
31
+ * Arrays retain their declared order because order is semantically meaningful
32
+ * in contract artifacts.
33
+ */
34
+ export function canonicalize(value, path = '$') {
35
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
36
+ if (typeof value === 'number') {
37
+ if (!Number.isFinite(value)) throw new ContractError('NON_CANONICAL_VALUE', `${path} must be finite`);
38
+ return value;
39
+ }
40
+ if (Array.isArray(value)) return value.map((entry, index) => canonicalize(entry, `${path}[${index}]`));
41
+ if (typeof value === 'object') {
42
+ const output = {};
43
+ for (const key of Object.keys(value).sort()) {
44
+ if (value[key] === undefined) throw new ContractError('NON_CANONICAL_VALUE', `${path}.${key} is undefined`);
45
+ output[key] = canonicalize(value[key], `${path}.${key}`);
46
+ }
47
+ return output;
48
+ }
49
+ throw new ContractError('NON_CANONICAL_VALUE', `${path} is not JSON-compatible`);
50
+ }
51
+
52
+ export function canonicalSerialize(value) {
53
+ return JSON.stringify(canonicalize(value));
54
+ }
55
+
56
+ export function sha256(value) {
57
+ const input = typeof value === 'string' ? value : canonicalSerialize(value);
58
+ return createHash('sha256').update(input, 'utf8').digest('hex');
59
+ }
60
+
61
+ export function validateEvidenceEnvelope(envelope) {
62
+ assertObject(envelope, 'INVALID_ENVELOPE', 'envelope');
63
+ assertString(envelope.event_id, 'INVALID_ENVELOPE', 'event_id');
64
+ if (envelope.schema_version !== 1) throw new ContractError('UNSUPPORTED_SCHEMA_VERSION', 'schema_version must be 1');
65
+ assertString(envelope.occurred_at, 'INVALID_ENVELOPE', 'occurred_at');
66
+ if (!RFC3339.test(envelope.occurred_at)) throw new ContractError('INVALID_ENVELOPE', 'occurred_at must be RFC3339');
67
+ assertString(envelope.project_id, 'INVALID_ENVELOPE', 'project_id');
68
+ assertString(envelope.correlation_id, 'INVALID_ENVELOPE', 'correlation_id');
69
+ if (envelope.causation_id !== undefined) assertString(envelope.causation_id, 'INVALID_ENVELOPE', 'causation_id');
70
+ assertObject(envelope.actor, 'INVALID_ENVELOPE', 'actor');
71
+ if (!ACTOR_TYPES.has(envelope.actor.type)) throw new ContractError('INVALID_ENVELOPE', 'actor.type is invalid');
72
+ assertString(envelope.actor.id, 'INVALID_ENVELOPE', 'actor.id');
73
+ assertObject(envelope.contract_ref, 'INVALID_ENVELOPE', 'contract_ref');
74
+ if (!CONTRACT_TYPES.has(envelope.contract_ref.type)) throw new ContractError('INVALID_ENVELOPE', 'contract_ref.type is invalid');
75
+ assertString(envelope.contract_ref.id, 'INVALID_ENVELOPE', 'contract_ref.id');
76
+ assertString(envelope.contract_ref.version, 'INVALID_ENVELOPE', 'contract_ref.version');
77
+ if (!CLASSIFICATIONS.has(envelope.classification)) throw new ContractError('INVALID_ENVELOPE', 'classification is invalid');
78
+ if (envelope.classification === 'client-restricted') assertString(envelope.tenant_ref, 'MISSING_TENANT_REF', 'tenant_ref');
79
+ assertObject(envelope.payload, 'INVALID_ENVELOPE', 'payload');
80
+ if (!Array.isArray(envelope.evidence_refs) || !envelope.evidence_refs.every((ref) => typeof ref === 'string' && ref)) {
81
+ throw new ContractError('INVALID_ENVELOPE', 'evidence_refs must be an array of non-empty strings');
82
+ }
83
+ return Object.freeze(canonicalize(envelope));
84
+ }
85
+
86
+ export function buildEvidenceEnvelope(input) {
87
+ const envelope = {
88
+ schema_version: 1,
89
+ evidence_refs: [],
90
+ ...input,
91
+ };
92
+ if (envelope.causation_id === undefined) delete envelope.causation_id;
93
+ return validateEvidenceEnvelope(envelope);
94
+ }
95
+
96
+ export function validateInvocationRequest(invocation) {
97
+ assertObject(invocation, 'INVALID_INVOCATION', 'invocation');
98
+ for (const field of ['invocation_id', 'attempt_id', 'action', 'provider_id', 'harness_id', 'tool_policy_ref', 'input_ref', 'expected_output_schema']) {
99
+ assertString(invocation[field], 'INVALID_INVOCATION', field);
100
+ }
101
+ assertObject(invocation.model_pin, 'INVALID_INVOCATION', 'model_pin');
102
+ assertString(invocation.model_pin.model_id, 'INVALID_INVOCATION', 'model_pin.model_id');
103
+ assertString(invocation.model_pin.catalog_snapshot_ref, 'INVALID_INVOCATION', 'model_pin.catalog_snapshot_ref');
104
+ if (invocation.model_pin.reasoning_configuration !== undefined && invocation.model_pin.reasoning_configuration !== null) {
105
+ assertString(invocation.model_pin.reasoning_configuration, 'INVALID_INVOCATION', 'model_pin.reasoning_configuration');
106
+ }
107
+ if (invocation.requested_tool_ids !== undefined && (!Array.isArray(invocation.requested_tool_ids) || !invocation.requested_tool_ids.every((id) => typeof id === 'string' && id))) {
108
+ throw new ContractError('INVALID_INVOCATION', 'requested_tool_ids must be an array of non-empty strings');
109
+ }
110
+ return Object.freeze(canonicalize(invocation));
111
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@chati/knowledge-context",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Scoped Company Brain reads and project-local learning candidate artifacts",
6
+ "type": "module",
7
+ "exports": "./src/index.js",
8
+ "dependencies": {
9
+ "@chati/core": "0.1.0"
10
+ },
11
+ "scripts": {
12
+ "test": "node --test test/**/*.test.js"
13
+ },
14
+ "engines": {
15
+ "node": ">=20.0.0"
16
+ }
17
+ }