chati-dev 4.5.15 → 4.5.27

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 (124) hide show
  1. package/README.md +8 -3
  2. package/bin/chati.js +124 -66
  3. package/framework/agents/build/dev.md +5 -5
  4. package/framework/agents/deploy/devops.md +7 -7
  5. package/framework/agents/discover/brief.md +4 -4
  6. package/framework/agents/discover/brownfield-wu.md +3 -3
  7. package/framework/agents/discover/greenfield-wu.md +3 -3
  8. package/framework/agents/plan/architect.md +2 -2
  9. package/framework/agents/plan/detail.md +4 -4
  10. package/framework/agents/plan/phases.md +2 -2
  11. package/framework/agents/plan/tasks.md +2 -2
  12. package/framework/agents/plan/ux.md +2 -2
  13. package/framework/agents/quality/qa-implementation.md +11 -10
  14. package/framework/agents/quality/qa-planning.md +3 -3
  15. package/framework/agents/quality/qa-visual.md +1 -1
  16. package/framework/config.yaml +3 -3
  17. package/framework/constitution.md +18 -18
  18. package/framework/context/protocols.md +2 -2
  19. package/framework/context/quality.md +1 -1
  20. package/framework/context/root.md +4 -4
  21. package/framework/data/entity-registry.yaml +1 -1
  22. package/framework/domains/agents/orchestrator.yaml +2 -2
  23. package/framework/domains/constitution.yaml +1 -1
  24. package/framework/hooks/advance-trigger.js +4 -6
  25. package/framework/hooks/git-push-authority.js +45 -37
  26. package/framework/hooks/mode-governance.js +149 -40
  27. package/framework/hooks/model-governance.js +13 -20
  28. package/framework/hooks/prism-engine.js +74 -92
  29. package/framework/hooks/reasoning-escalator.js +23 -40
  30. package/framework/hooks/session-digest.js +12 -13
  31. package/framework/hooks/session-reader.js +224 -0
  32. package/framework/hooks/session-writer.js +195 -0
  33. package/framework/hooks/team-quality-gate.js +34 -24
  34. package/framework/i18n/en.yaml +2 -2
  35. package/framework/i18n/es.yaml +2 -2
  36. package/framework/i18n/fr.yaml +2 -2
  37. package/framework/i18n/pt.yaml +2 -2
  38. package/framework/intelligence/context-engine.md +4 -4
  39. package/framework/intelligence/memory-layer.md +1 -1
  40. package/framework/manifest.json +129 -119
  41. package/framework/manifest.sig +1 -1
  42. package/framework/orchestrator/chati-router.js +278 -24
  43. package/framework/orchestrator/chati.md +250 -68
  44. package/framework/schemas/session.schema.json +21 -1
  45. package/framework/tasks/orchestrator-deviation.md +1 -1
  46. package/framework/tasks/orchestrator-escalate.md +1 -1
  47. package/framework/tasks/orchestrator-handoff.md +6 -6
  48. package/framework/tasks/orchestrator-health.md +5 -9
  49. package/framework/tasks/orchestrator-mode-switch.md +3 -7
  50. package/framework/tasks/orchestrator-resume.md +10 -14
  51. package/framework/tasks/orchestrator-route.md +3 -3
  52. package/framework/tasks/orchestrator-spawn-terminal.md +1 -1
  53. package/framework/tasks/orchestrator-status.md +9 -9
  54. package/framework/tasks/orchestrator-suggest-mode.md +1 -1
  55. package/framework/tasks/qa-impl-consolidate.md +2 -2
  56. package/framework/tasks/qa-impl-performance-test.md +4 -4
  57. package/framework/tasks/qa-impl-regression-check.md +4 -4
  58. package/framework/tasks/qa-impl-sast-scan.md +1 -1
  59. package/framework/tasks/qa-impl-test-execute.md +1 -1
  60. package/framework/tasks/qa-impl-verdict.md +2 -2
  61. package/framework/tasks/qa-planning-consolidate.md +3 -3
  62. package/framework/tasks/qa-planning-coverage-plan.md +2 -2
  63. package/framework/tasks/qa-planning-gate-define.md +4 -4
  64. package/framework/tasks/qa-planning-risk-matrix.md +4 -4
  65. package/framework/tasks/qa-planning-test-strategy.md +2 -2
  66. package/node_modules/@chati/browser-capability/src/index.js +12 -2
  67. package/node_modules/@chati/planning/src/index.js +24 -4
  68. package/node_modules/@chati/provider-registry/src/index.js +11 -0
  69. package/node_modules/@chati/rail/src/index.js +1967 -83
  70. package/node_modules/@chati/release-lane/README.md +12 -8
  71. package/node_modules/@chati/release-lane/package.json +1 -1
  72. package/node_modules/@chati/release-lane/src/index.js +1426 -58
  73. package/node_modules/@chati/review-council/src/index.js +63 -0
  74. package/node_modules/@chati/tracking-clickup/README.md +13 -0
  75. package/node_modules/@chati/tracking-clickup/src/index.js +690 -30
  76. package/package-artifact-manifest.json +1 -0
  77. package/package-artifact-manifest.sig +1 -0
  78. package/package.json +16 -7
  79. package/scripts/verify-real-harness-e2e.js +1581 -0
  80. package/src/config/framework-adapter.js +4 -4
  81. package/src/installer/core.js +157 -56
  82. package/src/installer/manifest.js +140 -9
  83. package/src/installer/package-artifact.js +249 -0
  84. package/src/installer/templates.js +91 -23
  85. package/src/installer-v2/catalog-client.js +531 -23
  86. package/src/installer-v2/index.js +91 -34
  87. package/src/installer-v2/installation-authority.js +327 -0
  88. package/src/installer-v2/provider-executable.js +235 -0
  89. package/src/installer-v2/wizard-installation.js +1 -1
  90. package/src/intelligence/timeline.js +3 -1
  91. package/src/orchestrator/browser-runtime.js +44 -13
  92. package/src/orchestrator/cli.js +1616 -151
  93. package/src/orchestrator/clickup-projection.js +43 -1
  94. package/src/orchestrator/clickup-runtime.js +355 -39
  95. package/src/orchestrator/doctor.js +87 -4
  96. package/src/orchestrator/index.js +16 -0
  97. package/src/orchestrator/planning-runtime.js +20 -2
  98. package/src/orchestrator/rail-adjudication-evidence.js +234 -0
  99. package/src/orchestrator/rail-evidence-authority.js +147 -0
  100. package/src/orchestrator/rail-execution-evidence.js +45 -0
  101. package/src/orchestrator/rail-runtime.js +836 -56
  102. package/src/orchestrator/release-runtime.js +65 -6
  103. package/src/orchestrator/review-runtime.js +186 -41
  104. package/src/orchestrator/runtime-installation-v2.js +236 -20
  105. package/src/orchestrator/session-manager.js +1249 -52
  106. package/src/terminal/adapters/claude-adapter.js +3 -1
  107. package/src/terminal/adapters/codex-adapter.js +2 -0
  108. package/src/terminal/adapters/grok-adapter.js +7 -4
  109. package/src/terminal/handoff-parser.js +19 -1
  110. package/src/terminal/prompt-builder.js +10 -1
  111. package/src/terminal/provider-preflight.js +36 -3
  112. package/src/terminal/rail-execution-worktree.js +213 -0
  113. package/src/terminal/rail-prompts.js +169 -0
  114. package/src/terminal/rail-readonly-workspace.js +324 -0
  115. package/src/terminal/run-agent.js +434 -16
  116. package/src/terminal/run-parallel.js +5 -0
  117. package/src/terminal/run-rail-adjudication.js +323 -0
  118. package/src/terminal/run-rail-review.js +291 -0
  119. package/src/terminal/run-rail-rework.js +325 -0
  120. package/src/terminal/run-rail-task.js +380 -0
  121. package/src/terminal/run-team.js +5 -0
  122. package/src/terminal/spawner.js +1225 -77
  123. package/src/utils/schema-validator.js +5 -0
  124. package/src/wizard/index.js +3 -2
@@ -0,0 +1,249 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { createHash, createPublicKey, verify } from 'node:crypto';
3
+ import {
4
+ closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readFileSync, readSync,
5
+ } from 'node:fs';
6
+ import { join, resolve } from 'node:path';
7
+
8
+ // This verifier is the package's trust root, including while npm runs
9
+ // postpack after it has removed bundled @chati dependencies. Keep the tiny
10
+ // canonical JSON implementation local instead of making the trust check depend
11
+ // on an optional, mutable package-resolution path. It intentionally matches
12
+ // @chati/core's canonical JSON contract: sorted object keys, array order
13
+ // retained, finite JSON-compatible values only.
14
+ function canonicalize(value, path = '$') {
15
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
16
+ if (typeof value === 'number') {
17
+ if (!Number.isFinite(value)) throw new TypeError(`${path} must be finite`);
18
+ return value;
19
+ }
20
+ if (Array.isArray(value)) return value.map((entry, index) => canonicalize(entry, `${path}[${index}]`));
21
+ if (typeof value === 'object') {
22
+ const output = {};
23
+ for (const key of Object.keys(value).sort()) {
24
+ if (value[key] === undefined) throw new TypeError(`${path}.${key} is undefined`);
25
+ output[key] = canonicalize(value[key], `${path}.${key}`);
26
+ }
27
+ return output;
28
+ }
29
+ throw new TypeError(`${path} is not JSON-compatible`);
30
+ }
31
+
32
+ function canonicalSerialize(value) {
33
+ return JSON.stringify(canonicalize(value));
34
+ }
35
+
36
+ export const PACKAGE_ARTIFACT_MANIFEST = 'package-artifact-manifest.json';
37
+ export const PACKAGE_ARTIFACT_SIGNATURE = 'package-artifact-manifest.sig';
38
+ const EXCLUDED_ARTIFACT_FILES = new Set([PACKAGE_ARTIFACT_MANIFEST, PACKAGE_ARTIFACT_SIGNATURE]);
39
+
40
+ /** npm 10/11 emit an array, while npm 12 keys the report by package name. */
41
+ export function parseNpmPackDryRunReport(output) {
42
+ const parsed = JSON.parse(output);
43
+ if (Array.isArray(parsed)) return parsed[0] ?? null;
44
+ if (!parsed || typeof parsed !== 'object') return null;
45
+ const reports = Object.values(parsed);
46
+ return reports.length === 1 ? reports[0] : null;
47
+ }
48
+
49
+ /**
50
+ * npm emits POSIX tar paths. Treat its report as untrusted input anyway: a
51
+ * malformed path must never escape the package root while an artifact is
52
+ * signed or verified.
53
+ */
54
+ export function isCanonicalPackageArtifactPath(path) {
55
+ return typeof path === 'string'
56
+ && path.length > 0
57
+ && !path.startsWith('/')
58
+ && !path.includes(':')
59
+ && !path.includes('\0')
60
+ && !path.includes('\\')
61
+ && path.split('/').every((segment) => segment.length > 0 && segment !== '.' && segment !== '..');
62
+ }
63
+
64
+ function listPublishArtifactEntries(packageRoot) {
65
+ // This helper can run while npm is executing a workspace lifecycle script.
66
+ // Do not let that parent workspace selection redirect the independent,
67
+ // temporary package probe below.
68
+ // npm exposes every effective config option to lifecycle scripts as an
69
+ // npm_config_* environment variable. Clearing only the workspace-shaped
70
+ // options still leaves inherited userconfig, prefix, and future workspace
71
+ // aliases able to redirect this nested npm invocation. The probe supplies
72
+ // every configuration it requires explicitly below.
73
+ const npmEnvironment = Object.fromEntries(
74
+ Object.entries(process.env).filter(([key]) => !key.toLowerCase().startsWith('npm_config_')),
75
+ );
76
+ // Clearing environment variables is not sufficient: npm falls back to the
77
+ // user's on-disk .npmrc, which can re-enable a workspace selection. Point
78
+ // the user layer at the platform null device to make the temporary probe
79
+ // independent of the calling account and CI runner.
80
+ const emptyNpmConfigPath = process.platform === 'win32' ? 'NUL' : '/dev/null';
81
+ const output = execFileSync(
82
+ process.platform === 'win32' ? 'npm.cmd' : 'npm',
83
+ // Keep this an independent single-package probe. In particular, do not
84
+ // pair --no-workspaces with an inherited --workspace: npm rejects that
85
+ // combination instead of resolving the package root.
86
+ [
87
+ '--prefix', packageRoot,
88
+ '--userconfig', emptyNpmConfigPath,
89
+ 'pack', '--dry-run', '--ignore-scripts', '--json',
90
+ ],
91
+ {
92
+ cwd: packageRoot,
93
+ encoding: 'utf8',
94
+ env: npmEnvironment,
95
+ stdio: ['ignore', 'pipe', 'pipe'],
96
+ },
97
+ );
98
+ const report = parseNpmPackDryRunReport(output);
99
+ if (!Array.isArray(report?.files) || report.files.length === 0) {
100
+ throw new Error('npm pack dry-run returned no publishable files');
101
+ }
102
+ return Object.freeze(report.files
103
+ .filter(({ path }) => !EXCLUDED_ARTIFACT_FILES.has(path))
104
+ .map(({ path, mode }) => {
105
+ if (!isCanonicalPackageArtifactPath(path)) {
106
+ throw new Error(`npm pack dry-run returned an unsafe artifact path: ${String(path)}`);
107
+ }
108
+ if (!Number.isInteger(mode) || mode < 0 || mode > 0o777) {
109
+ throw new Error(`npm pack dry-run returned an invalid file mode: ${path}`);
110
+ }
111
+ return Object.freeze({ path, mode });
112
+ })
113
+ .sort((left, right) => left.path.localeCompare(right.path)));
114
+ }
115
+
116
+ export function listPublishArtifactFiles(packageRoot) {
117
+ return Object.freeze(listPublishArtifactEntries(packageRoot).map(({ path }) => path));
118
+ }
119
+
120
+ function sameFileSnapshot(left, right) {
121
+ return Boolean(left && right
122
+ && left.dev === right.dev
123
+ && left.ino === right.ino
124
+ && left.mode === right.mode
125
+ && left.uid === right.uid
126
+ && left.gid === right.gid
127
+ && left.nlink === right.nlink
128
+ && left.size === right.size
129
+ && left.mtimeMs === right.mtimeMs
130
+ && left.ctimeMs === right.ctimeMs);
131
+ }
132
+
133
+ function hashPinnedRegularFile(path, expected) {
134
+ let descriptor;
135
+ try {
136
+ descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
137
+ const opened = fstatSync(descriptor);
138
+ if (!opened.isFile() || opened.nlink !== 1 || !sameFileSnapshot(expected, opened)) {
139
+ throw new Error('publish artifact entry changed before it could be hashed');
140
+ }
141
+ const hash = createHash('sha256');
142
+ const buffer = Buffer.allocUnsafe(64 * 1024);
143
+ for (;;) {
144
+ const bytes = readSync(descriptor, buffer, 0, buffer.length, null);
145
+ if (bytes === 0) break;
146
+ hash.update(buffer.subarray(0, bytes));
147
+ }
148
+ const finished = fstatSync(descriptor);
149
+ if (!sameFileSnapshot(opened, finished)) {
150
+ throw new Error('publish artifact entry changed while it was hashed');
151
+ }
152
+ return hash.digest('hex');
153
+ } finally {
154
+ if (descriptor !== undefined) closeSync(descriptor);
155
+ }
156
+ }
157
+
158
+ export function buildPackageArtifactManifest(packageRoot, { createdAt = new Date().toISOString() } = {}) {
159
+ const root = resolve(packageRoot);
160
+ const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
161
+ const files = {};
162
+ for (const { path: relativePath, mode } of listPublishArtifactEntries(root)) {
163
+ const absolute = join(root, relativePath);
164
+ const stat = lstatSync(absolute);
165
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) {
166
+ throw new Error(`publish artifact entry must be a single-link regular file: ${relativePath}`);
167
+ }
168
+ files[relativePath] = Object.freeze({
169
+ // npm normalizes file modes in the tarball. Signing the on-disk source
170
+ // mode would reject a clean install when its umask differs, even though
171
+ // the exact packaged entry is unchanged. The npm report is the canonical
172
+ // publish mode while lstat above still rejects links and unsafe entries.
173
+ hash: hashPinnedRegularFile(absolute, stat), size: stat.size, mode,
174
+ });
175
+ }
176
+ return Object.freeze(canonicalize({
177
+ schema_version: 1,
178
+ artifact_type: 'chati-npm-publish-artifact',
179
+ package_name: packageJson.name,
180
+ version: packageJson.version,
181
+ created_at: createdAt,
182
+ files,
183
+ }));
184
+ }
185
+
186
+ export function serializePackageArtifactManifest(manifest) {
187
+ return canonicalSerialize(manifest);
188
+ }
189
+
190
+ export function contentOnlyPackageArtifactManifest(manifest) {
191
+ const content = { ...manifest };
192
+ delete content.created_at;
193
+ return content;
194
+ }
195
+
196
+ /**
197
+ * The package's embedded public key is the only default trust root. The
198
+ * signing private key must never influence runtime or publish verification.
199
+ */
200
+ export function externalTrustedPublicKey(packageRoot) {
201
+ try {
202
+ return createPublicKey(readFileSync(join(resolve(packageRoot), 'src', 'installer', 'signing-public-key.pem'), 'utf8'));
203
+ } catch {
204
+ return null;
205
+ }
206
+ }
207
+
208
+ export function verifyPackageArtifactManifest(manifest, signature, publicKey) {
209
+ if (!publicKey) return Object.freeze({ valid: false, reason: 'external-trust-root-required' });
210
+ try {
211
+ if (manifest?.schema_version !== 1
212
+ || manifest?.artifact_type !== 'chati-npm-publish-artifact'
213
+ || !manifest?.files || typeof manifest.files !== 'object' || Array.isArray(manifest.files)) {
214
+ return Object.freeze({ valid: false, reason: 'invalid-artifact-manifest' });
215
+ }
216
+ const valid = verify(
217
+ null,
218
+ Buffer.from(serializePackageArtifactManifest(manifest)),
219
+ publicKey,
220
+ Buffer.from(String(signature).trim(), 'base64'),
221
+ );
222
+ return Object.freeze({ valid, reason: valid ? null : 'invalid-signature' });
223
+ } catch (error) {
224
+ return Object.freeze({ valid: false, reason: error.message });
225
+ }
226
+ }
227
+
228
+ /** Verifies the installed package's exact publishable file set before use. */
229
+ export function verifyCurrentPackageArtifact(packageRoot) {
230
+ const root = resolve(packageRoot);
231
+ const manifestPath = join(root, PACKAGE_ARTIFACT_MANIFEST);
232
+ const signaturePath = join(root, PACKAGE_ARTIFACT_SIGNATURE);
233
+ if (!existsSync(manifestPath) || !existsSync(signaturePath)) {
234
+ return Object.freeze({ valid: false, reason: 'artifact-manifest-or-signature-missing' });
235
+ }
236
+ try {
237
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
238
+ const signature = readFileSync(signaturePath, 'utf8').trim();
239
+ const signatureResult = verifyPackageArtifactManifest(manifest, signature, externalTrustedPublicKey(root));
240
+ if (!signatureResult.valid) return signatureResult;
241
+ const current = buildPackageArtifactManifest(root, { createdAt: manifest.created_at });
242
+ if (serializePackageArtifactManifest(manifest.files) !== serializePackageArtifactManifest(current.files)) {
243
+ return Object.freeze({ valid: false, reason: 'artifact-manifest-stale' });
244
+ }
245
+ return Object.freeze({ valid: true, reason: 'ok', fileCount: Object.keys(current.files).length });
246
+ } catch (error) {
247
+ return Object.freeze({ valid: false, reason: error.message });
248
+ }
249
+ }
@@ -55,6 +55,10 @@ export function generateSessionYaml(config) {
55
55
  scaffold_applied: [],
56
56
  active_model: null,
57
57
  active_provider: null,
58
+ active_harness: null,
59
+ session_status: 'inactive',
60
+ current_execution_binding: null,
61
+ pending_interaction: null,
58
62
  context_tokens_used: 0,
59
63
  context_window_tokens: null,
60
64
  context_last_bracket: null,
@@ -119,6 +123,13 @@ export function generateConfigYaml(config) {
119
123
  codex: { enabled: providers.includes('codex') || selectedIDEs.includes('codex-cli') },
120
124
  grok: { enabled: providers.includes('grok') || selectedIDEs.includes('grok-cli') },
121
125
  },
126
+ integrations: {
127
+ clickup: {
128
+ required: config.installationMode === 'internal',
129
+ connected_harnesses: config.clickupPreflight?.connected_providers || [],
130
+ durable_projection_queue: '.chati/v2/tracking/pending-projections',
131
+ },
132
+ },
122
133
  };
123
134
 
124
135
  // Feature flags (Intelligence Upgrade + Article XXI Agent Teams)
@@ -180,7 +191,7 @@ export function generateProjectContext(config) {
180
191
  - **Language**: ${language}
181
192
 
182
193
  ## Chati.dev
183
- Provider and model routing is resolved per task from the signed installation
194
+ Provider and model routing is resolved per task from the integrity-checked installation
184
195
  capability snapshot. No primary harness is configured.
185
196
  <!-- chati-context:end -->
186
197
  `;
@@ -228,7 +239,7 @@ description: >
228
239
  Use the shell in the current project directory and run:
229
240
 
230
241
  \`\`\`bash
231
- node .chati.dev/orchestrator/chati-router.js
242
+ node .chati.dev/orchestrator/chati-router.js --harness codex
232
243
  \`\`\`
233
244
 
234
245
  Do not infer provider authentication, session state, model availability, or the
@@ -239,10 +250,19 @@ routed spawn command returns a provider authentication error.
239
250
  Parse the JSON output before responding:
240
251
 
241
252
  - If \`ok\` is \`false\`, report the exact \`error\` and relevant detail, then stop.
242
- - If \`ok\` is \`true\`, use \`session.language\` for every response.
253
+ - If \`ok\` is \`true\`, use \`session.language\` for every visible response,
254
+ preface, progress message, question and error, starting with the first response.
243
255
  - Follow the returned \`action\` in the adapted orchestrator.
244
- - When the action includes \`pipeline.spawn_command\`, execute that exact command.
245
- Never replace it with an inferred provider check or a manually constructed command.
256
+ - Execute \`pipeline.spawn_command\` only when the returned action explicitly
257
+ dispatches work, such as \`spawn_routed_interactive\`. Never execute a command
258
+ merely because a command-shaped field is present.
259
+ - For \`resume_pending_interaction\`, do not execute any provider command. Show
260
+ exactly \`pipeline.question\`, with no preface, summary or paraphrase. Only
261
+ after the user answers, write \`{"response":"<exact user response>"}\` to
262
+ \`pipeline.response_file\` using the native file-write tool, never a shell
263
+ command. Then execute \`pipeline.deferred_spawn_command\` exactly once and
264
+ unchanged. Never execute a command merely because a command-shaped field is
265
+ present.
246
266
  - A router-provided spawn command starts an external provider CLI. Execute it
247
267
  with the active harness's native process permissions, preserving the exact
248
268
  command and working directory. If the harness sandboxes OS credentials,
@@ -256,17 +276,18 @@ Parse the JSON output before responding:
256
276
 
257
277
  ## CRITICAL: Language Override
258
278
 
259
- Read \`.chati/session.yaml\` field \`language\` BEFORE anything else.
260
- ALL responses MUST be in this language. This overrides any global setting.
279
+ Use the canonical \`session.language\` returned by the router. ALL visible
280
+ responses MUST use this language from the first response onward. This
281
+ overrides any global setting.
261
282
 
262
283
  | Value | Language |
263
284
  |-------|----------|
264
285
  | \`en\` | English |
265
- | \`pt\` | Portugues |
266
- | \`es\` | Espanol |
267
- | \`fr\` | Francais |
286
+ | \`pt\` | Português |
287
+ | \`es\` | Español |
288
+ | \`fr\` | Français |
268
289
 
269
- If session.yaml does not exist or has no language field, default to English.
290
+ If the router returns no session language, default to English.
270
291
 
271
292
  ## Load
272
293
 
@@ -278,7 +299,7 @@ Read and execute the full orchestrator at \`${orchPath}\`.
278
299
  **NEVER create or reference CLAUDE.md, CLAUDE.local.md, or .claude/ directories.**
279
300
 
280
301
  **Context to pass:**
281
- - \`.chati/session.yaml\` (session state, includes language)
302
+ - the validated \`session\` object already returned by the router
282
303
  - \`AGENTS.md\` (project context)
283
304
  - \`artifacts/handoffs/\` (latest handoff)
284
305
  - \`.chati.dev/config.yaml\` (version info)
@@ -294,10 +315,14 @@ export function generateGeminiRouter(options = {}) {
294
315
  const orchPath = options.orchestratorPath || '.chati.dev/orchestrator/chati.md';
295
316
  return `description = "Activate Chati.dev orchestrator"
296
317
  prompt = """
297
- CRITICAL Language Override:
298
- Read .chati/session.yaml field "language" BEFORE anything else.
299
- ALL responses MUST be in this language (en, pt, es, fr).
300
- If session.yaml does not exist or has no language field, default to English.
318
+ STEP 0: Run the deterministic router before reading mutable state or responding:
319
+
320
+ node .chati.dev/orchestrator/chati-router.js --harness gemini
321
+
322
+ Parse its JSON output. It is the source of truth for validated session state,
323
+ task routing and provider authentication. If ok is false, report the exact
324
+ error and stop. If ok is true, use session.language for every visible response,
325
+ including the first one. If the router returns no language, default to English.
301
326
 
302
327
  The orchestrator and all agent files are pre-configured for Gemini CLI.
303
328
  No translation needed; follow all instructions as written.
@@ -306,7 +331,7 @@ Read and execute the full orchestrator at ${orchPath}.
306
331
  NEVER create or reference CLAUDE.md, CLAUDE.local.md, or .claude/ directories.
307
332
 
308
333
  Context to load:
309
- - .chati/session.yaml (session state, includes language)
334
+ - the validated session object already returned by the router
310
335
  - GEMINI.md (project context)
311
336
  - artifacts/handoffs/ (latest handoff)
312
337
  - .chati.dev/config.yaml (version info)
@@ -323,14 +348,37 @@ export function generateGrokRouter(options = {}) {
323
348
  const orchPath = options.orchestratorPath || '.chati.dev/orchestrator/chati.md';
324
349
  return `# Chati.dev Orchestrator
325
350
 
326
- Read \`.chati/session.yaml\` and use its \`language\` for every response.
327
- If the field is absent, default to English.
351
+ ## STEP 0: Run the deterministic router first
328
352
 
329
- The orchestrator and agent files are pre-configured for Grok CLI. Read and
330
- execute the full orchestrator at \`${orchPath}\`.
353
+ Before reading mutable session files or responding, run:
331
354
 
332
- Load this context:
333
- - \`.chati/session.yaml\`
355
+ \`\`\`bash
356
+ node .chati.dev/orchestrator/chati-router.js --harness grok
357
+ \`\`\`
358
+
359
+ Parse the JSON output. It is the source of truth for session state, task
360
+ routing and provider authentication.
361
+
362
+ - If the top-level \`ok\` field is \`false\`, report the exact \`error\` and
363
+ relevant detail, then stop. Do not load the orchestrator, recover mutable
364
+ state or run another command.
365
+ - Only if \`ok\` is \`true\`, use the returned \`session.language\` for every
366
+ visible response, preface, progress message, question and error, starting
367
+ with the first response. If absent, default to English.
368
+
369
+ For \`resume_pending_interaction\`, do not execute any provider command. Show
370
+ exactly \`pipeline.question\`, with no preface, summary or paraphrase. Only
371
+ after the user answers, write \`{"response":"<exact user response>"}\` to
372
+ \`pipeline.response_file\` using the native file-write tool, never a shell
373
+ command. Then execute \`pipeline.deferred_spawn_command\` exactly once and
374
+ unchanged. Never execute a command merely because a command-shaped field is
375
+ present.
376
+
377
+ Only if \`ok\` is \`true\`, read and execute the full pre-configured Grok CLI
378
+ orchestrator at \`${orchPath}\`.
379
+
380
+ Only after the router succeeds, load this context:
381
+ - the validated \`session\` object already returned by the router
334
382
  - \`AGENTS.md\`
335
383
  - \`.grok/session-lock.md\`
336
384
  - \`artifacts/handoffs/\`
@@ -342,6 +390,26 @@ User input: $ARGUMENTS
342
390
  `;
343
391
  }
344
392
 
393
+ /**
394
+ * Generate project-scoped Grok rules that keep the deterministic router as
395
+ * the only CHATI entry boundary. Grok loads .grok/rules as project rules.
396
+ */
397
+ export function generateGrokGovernanceRules() {
398
+ return `# Chati.dev Grok Governance
399
+
400
+ For every CHATI.DEV entry or continuation, run this boundary before reading
401
+ mutable CHATI state or taking an action:
402
+
403
+ \`node .chati.dev/orchestrator/chati-router.js --harness grok\`
404
+
405
+ If the returned \`ok\` value is false, report the exact error and stop. Never
406
+ bypass the license check, provider-integrity check, session authority, RAIL
407
+ journal, review council, or human release authorization. Execute RAIL actions
408
+ only through commands returned by the deterministic router. Treat \`AGENTS.md\`
409
+ as the project instruction authority and never create or modify \`CLAUDE.md\`.
410
+ `;
411
+ }
412
+
345
413
  /**
346
414
  * Generate CLAUDE.local.md content (runtime state — auto-gitignored, never committed)
347
415
  */