chati-dev 4.5.16 → 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 (122) 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 +1 -1
  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 +252 -25
  43. package/framework/orchestrator/chati.md +238 -70
  44. package/framework/schemas/session.schema.json +1 -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 +148 -53
  82. package/src/installer/manifest.js +140 -9
  83. package/src/installer/package-artifact.js +249 -0
  84. package/src/installer/templates.js +65 -20
  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/orchestrator/browser-runtime.js +44 -13
  91. package/src/orchestrator/cli.js +1545 -174
  92. package/src/orchestrator/clickup-projection.js +43 -1
  93. package/src/orchestrator/clickup-runtime.js +355 -39
  94. package/src/orchestrator/doctor.js +16 -3
  95. package/src/orchestrator/index.js +16 -0
  96. package/src/orchestrator/planning-runtime.js +20 -2
  97. package/src/orchestrator/rail-adjudication-evidence.js +234 -0
  98. package/src/orchestrator/rail-evidence-authority.js +147 -0
  99. package/src/orchestrator/rail-execution-evidence.js +45 -0
  100. package/src/orchestrator/rail-runtime.js +836 -56
  101. package/src/orchestrator/release-runtime.js +65 -6
  102. package/src/orchestrator/review-runtime.js +186 -41
  103. package/src/orchestrator/runtime-installation-v2.js +236 -20
  104. package/src/orchestrator/session-manager.js +1167 -53
  105. package/src/terminal/adapters/claude-adapter.js +3 -1
  106. package/src/terminal/adapters/codex-adapter.js +2 -0
  107. package/src/terminal/adapters/grok-adapter.js +7 -4
  108. package/src/terminal/handoff-parser.js +19 -1
  109. package/src/terminal/prompt-builder.js +9 -1
  110. package/src/terminal/provider-preflight.js +36 -3
  111. package/src/terminal/rail-execution-worktree.js +213 -0
  112. package/src/terminal/rail-prompts.js +169 -0
  113. package/src/terminal/rail-readonly-workspace.js +324 -0
  114. package/src/terminal/run-agent.js +384 -24
  115. package/src/terminal/run-parallel.js +5 -0
  116. package/src/terminal/run-rail-adjudication.js +323 -0
  117. package/src/terminal/run-rail-review.js +291 -0
  118. package/src/terminal/run-rail-rework.js +325 -0
  119. package/src/terminal/run-rail-task.js +380 -0
  120. package/src/terminal/run-team.js +5 -0
  121. package/src/terminal/spawner.js +1225 -77
  122. 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
+ }
@@ -191,7 +191,7 @@ export function generateProjectContext(config) {
191
191
  - **Language**: ${language}
192
192
 
193
193
  ## Chati.dev
194
- 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
195
195
  capability snapshot. No primary harness is configured.
196
196
  <!-- chati-context:end -->
197
197
  `;
@@ -253,8 +253,16 @@ Parse the JSON output before responding:
253
253
  - If \`ok\` is \`true\`, use \`session.language\` for every visible response,
254
254
  preface, progress message, question and error, starting with the first response.
255
255
  - Follow the returned \`action\` in the adapted orchestrator.
256
- - When the action includes \`pipeline.spawn_command\`, execute that exact command.
257
- 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.
258
266
  - A router-provided spawn command starts an external provider CLI. Execute it
259
267
  with the active harness's native process permissions, preserving the exact
260
268
  command and working directory. If the harness sandboxes OS credentials,
@@ -275,11 +283,11 @@ overrides any global setting.
275
283
  | Value | Language |
276
284
  |-------|----------|
277
285
  | \`en\` | English |
278
- | \`pt\` | Portugues |
279
- | \`es\` | Espanol |
280
- | \`fr\` | Francais |
286
+ | \`pt\` | Português |
287
+ | \`es\` | Español |
288
+ | \`fr\` | Français |
281
289
 
282
- 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.
283
291
 
284
292
  ## Load
285
293
 
@@ -291,7 +299,7 @@ Read and execute the full orchestrator at \`${orchPath}\`.
291
299
  **NEVER create or reference CLAUDE.md, CLAUDE.local.md, or .claude/ directories.**
292
300
 
293
301
  **Context to pass:**
294
- - \`.chati/session.yaml\` (session state, includes language)
302
+ - the validated \`session\` object already returned by the router
295
303
  - \`AGENTS.md\` (project context)
296
304
  - \`artifacts/handoffs/\` (latest handoff)
297
305
  - \`.chati.dev/config.yaml\` (version info)
@@ -307,10 +315,14 @@ export function generateGeminiRouter(options = {}) {
307
315
  const orchPath = options.orchestratorPath || '.chati.dev/orchestrator/chati.md';
308
316
  return `description = "Activate Chati.dev orchestrator"
309
317
  prompt = """
310
- CRITICAL Language Override:
311
- Read .chati/session.yaml field "language" BEFORE anything else.
312
- Every visible response, including the first response, MUST be in this language (en, pt, es, fr).
313
- 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.
314
326
 
315
327
  The orchestrator and all agent files are pre-configured for Gemini CLI.
316
328
  No translation needed; follow all instructions as written.
@@ -319,7 +331,7 @@ Read and execute the full orchestrator at ${orchPath}.
319
331
  NEVER create or reference CLAUDE.md, CLAUDE.local.md, or .claude/ directories.
320
332
 
321
333
  Context to load:
322
- - .chati/session.yaml (session state, includes language)
334
+ - the validated session object already returned by the router
323
335
  - GEMINI.md (project context)
324
336
  - artifacts/handoffs/ (latest handoff)
325
337
  - .chati.dev/config.yaml (version info)
@@ -345,15 +357,28 @@ node .chati.dev/orchestrator/chati-router.js --harness grok
345
357
  \`\`\`
346
358
 
347
359
  Parse the JSON output. It is the source of truth for session state, task
348
- routing and provider authentication. Use the returned \`session.language\` for
349
- every visible response, preface, progress message, question and error,
350
- starting with the first response. If absent, default to English.
351
-
352
- The orchestrator and agent files are pre-configured for Grok CLI. Read and
353
- execute the full orchestrator at \`${orchPath}\`.
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}\`.
354
379
 
355
380
  Only after the router succeeds, load this context:
356
- - \`.chati/session.yaml\`
381
+ - the validated \`session\` object already returned by the router
357
382
  - \`AGENTS.md\`
358
383
  - \`.grok/session-lock.md\`
359
384
  - \`artifacts/handoffs/\`
@@ -365,6 +390,26 @@ User input: $ARGUMENTS
365
390
  `;
366
391
  }
367
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
+
368
413
  /**
369
414
  * Generate CLAUDE.local.md content (runtime state — auto-gitignored, never committed)
370
415
  */