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.
- package/README.md +8 -3
- package/bin/chati.js +124 -66
- package/framework/agents/build/dev.md +5 -5
- package/framework/agents/deploy/devops.md +7 -7
- package/framework/agents/discover/brief.md +4 -4
- package/framework/agents/discover/brownfield-wu.md +3 -3
- package/framework/agents/discover/greenfield-wu.md +3 -3
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +4 -4
- package/framework/agents/plan/phases.md +2 -2
- package/framework/agents/plan/tasks.md +2 -2
- package/framework/agents/plan/ux.md +2 -2
- package/framework/agents/quality/qa-implementation.md +11 -10
- package/framework/agents/quality/qa-planning.md +3 -3
- package/framework/agents/quality/qa-visual.md +1 -1
- package/framework/config.yaml +3 -3
- package/framework/constitution.md +18 -18
- package/framework/context/protocols.md +1 -1
- package/framework/context/quality.md +1 -1
- package/framework/context/root.md +4 -4
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/domains/agents/orchestrator.yaml +2 -2
- package/framework/domains/constitution.yaml +1 -1
- package/framework/hooks/advance-trigger.js +4 -6
- package/framework/hooks/git-push-authority.js +45 -37
- package/framework/hooks/mode-governance.js +149 -40
- package/framework/hooks/model-governance.js +13 -20
- package/framework/hooks/prism-engine.js +74 -92
- package/framework/hooks/reasoning-escalator.js +23 -40
- package/framework/hooks/session-digest.js +12 -13
- package/framework/hooks/session-reader.js +224 -0
- package/framework/hooks/session-writer.js +195 -0
- package/framework/hooks/team-quality-gate.js +34 -24
- package/framework/i18n/en.yaml +2 -2
- package/framework/i18n/es.yaml +2 -2
- package/framework/i18n/fr.yaml +2 -2
- package/framework/i18n/pt.yaml +2 -2
- package/framework/intelligence/context-engine.md +4 -4
- package/framework/intelligence/memory-layer.md +1 -1
- package/framework/manifest.json +129 -119
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +252 -25
- package/framework/orchestrator/chati.md +238 -70
- package/framework/schemas/session.schema.json +1 -1
- package/framework/tasks/orchestrator-deviation.md +1 -1
- package/framework/tasks/orchestrator-escalate.md +1 -1
- package/framework/tasks/orchestrator-handoff.md +6 -6
- package/framework/tasks/orchestrator-health.md +5 -9
- package/framework/tasks/orchestrator-mode-switch.md +3 -7
- package/framework/tasks/orchestrator-resume.md +10 -14
- package/framework/tasks/orchestrator-route.md +3 -3
- package/framework/tasks/orchestrator-spawn-terminal.md +1 -1
- package/framework/tasks/orchestrator-status.md +9 -9
- package/framework/tasks/orchestrator-suggest-mode.md +1 -1
- package/framework/tasks/qa-impl-consolidate.md +2 -2
- package/framework/tasks/qa-impl-performance-test.md +4 -4
- package/framework/tasks/qa-impl-regression-check.md +4 -4
- package/framework/tasks/qa-impl-sast-scan.md +1 -1
- package/framework/tasks/qa-impl-test-execute.md +1 -1
- package/framework/tasks/qa-impl-verdict.md +2 -2
- package/framework/tasks/qa-planning-consolidate.md +3 -3
- package/framework/tasks/qa-planning-coverage-plan.md +2 -2
- package/framework/tasks/qa-planning-gate-define.md +4 -4
- package/framework/tasks/qa-planning-risk-matrix.md +4 -4
- package/framework/tasks/qa-planning-test-strategy.md +2 -2
- package/node_modules/@chati/browser-capability/src/index.js +12 -2
- package/node_modules/@chati/planning/src/index.js +24 -4
- package/node_modules/@chati/provider-registry/src/index.js +11 -0
- package/node_modules/@chati/rail/src/index.js +1967 -83
- package/node_modules/@chati/release-lane/README.md +12 -8
- package/node_modules/@chati/release-lane/package.json +1 -1
- package/node_modules/@chati/release-lane/src/index.js +1426 -58
- package/node_modules/@chati/review-council/src/index.js +63 -0
- package/node_modules/@chati/tracking-clickup/README.md +13 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +690 -30
- package/package-artifact-manifest.json +1 -0
- package/package-artifact-manifest.sig +1 -0
- package/package.json +16 -7
- package/scripts/verify-real-harness-e2e.js +1581 -0
- package/src/config/framework-adapter.js +4 -4
- package/src/installer/core.js +148 -53
- package/src/installer/manifest.js +140 -9
- package/src/installer/package-artifact.js +249 -0
- package/src/installer/templates.js +65 -20
- package/src/installer-v2/catalog-client.js +531 -23
- package/src/installer-v2/index.js +91 -34
- package/src/installer-v2/installation-authority.js +327 -0
- package/src/installer-v2/provider-executable.js +235 -0
- package/src/installer-v2/wizard-installation.js +1 -1
- package/src/orchestrator/browser-runtime.js +44 -13
- package/src/orchestrator/cli.js +1545 -174
- package/src/orchestrator/clickup-projection.js +43 -1
- package/src/orchestrator/clickup-runtime.js +355 -39
- package/src/orchestrator/doctor.js +16 -3
- package/src/orchestrator/index.js +16 -0
- package/src/orchestrator/planning-runtime.js +20 -2
- package/src/orchestrator/rail-adjudication-evidence.js +234 -0
- package/src/orchestrator/rail-evidence-authority.js +147 -0
- package/src/orchestrator/rail-execution-evidence.js +45 -0
- package/src/orchestrator/rail-runtime.js +836 -56
- package/src/orchestrator/release-runtime.js +65 -6
- package/src/orchestrator/review-runtime.js +186 -41
- package/src/orchestrator/runtime-installation-v2.js +236 -20
- package/src/orchestrator/session-manager.js +1167 -53
- package/src/terminal/adapters/claude-adapter.js +3 -1
- package/src/terminal/adapters/codex-adapter.js +2 -0
- package/src/terminal/adapters/grok-adapter.js +7 -4
- package/src/terminal/handoff-parser.js +19 -1
- package/src/terminal/prompt-builder.js +9 -1
- package/src/terminal/provider-preflight.js +36 -3
- package/src/terminal/rail-execution-worktree.js +213 -0
- package/src/terminal/rail-prompts.js +169 -0
- package/src/terminal/rail-readonly-workspace.js +324 -0
- package/src/terminal/run-agent.js +384 -24
- package/src/terminal/run-parallel.js +5 -0
- package/src/terminal/run-rail-adjudication.js +323 -0
- package/src/terminal/run-rail-review.js +291 -0
- package/src/terminal/run-rail-rework.js +325 -0
- package/src/terminal/run-rail-task.js +380 -0
- package/src/terminal/run-team.js +5 -0
- package/src/terminal/spawner.js +1225 -77
- package/src/wizard/index.js +3 -2
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
* @returns {{ command: string, args: string[], stdinPrompt: string|null }}
|
|
19
19
|
*/
|
|
20
20
|
export function buildCommand(config, provider) {
|
|
21
|
-
const
|
|
21
|
+
const readOnly = Array.isArray(config.writeScope) && config.writeScope.length === 0;
|
|
22
|
+
const args = provider.baseArgs.filter((argument) => !(readOnly && argument === '--dangerously-skip-permissions'));
|
|
23
|
+
if (readOnly) args.push('--permission-mode', 'plan', '--no-session-persistence');
|
|
22
24
|
let effectiveModel = null;
|
|
23
25
|
|
|
24
26
|
if (config.model) {
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export function buildCommand(config, provider) {
|
|
16
16
|
const args = [...provider.baseArgs];
|
|
17
|
+
const readOnly = Array.isArray(config.writeScope) && config.writeScope.length === 0;
|
|
18
|
+
if (readOnly) args.push('-s', 'read-only', '--ephemeral');
|
|
17
19
|
let effectiveModel = null;
|
|
18
20
|
|
|
19
21
|
if (config.model) {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Grok Build CLI adapter.
|
|
3
3
|
*
|
|
4
|
-
* Grok
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Grok accepts a prompt file for headless execution. The spawner replaces the
|
|
5
|
+
* private-file marker with a descriptor-bound path, so prompt material never
|
|
6
|
+
* enters process arguments. This adapter does not silently select a model.
|
|
7
7
|
*/
|
|
8
8
|
export function buildCommand(config, provider) {
|
|
9
9
|
const args = [...provider.baseArgs];
|
|
10
|
+
const readOnly = Array.isArray(config.writeScope) && config.writeScope.length === 0;
|
|
11
|
+
if (readOnly) args.push('--permission-mode', 'plan', '--no-subagents', '--disable-web-search');
|
|
10
12
|
let effectiveModel = null;
|
|
11
13
|
if (config.model) {
|
|
12
14
|
effectiveModel = provider.modelMap[config.model] || config.model;
|
|
@@ -15,11 +17,12 @@ export function buildCommand(config, provider) {
|
|
|
15
17
|
if (config.reasoningConfiguration) {
|
|
16
18
|
args.push('--reasoning-effort', config.reasoningConfiguration);
|
|
17
19
|
}
|
|
18
|
-
args.push('-
|
|
20
|
+
args.push('--prompt-file', '<chati-private-prompt-file>');
|
|
19
21
|
return {
|
|
20
22
|
command: provider.command,
|
|
21
23
|
args,
|
|
22
24
|
stdinPrompt: null,
|
|
25
|
+
promptFile: config.prompt || '',
|
|
23
26
|
effectiveModel,
|
|
24
27
|
effectiveReasoningConfiguration: config.reasoningConfiguration || null,
|
|
25
28
|
};
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { validateSchema, HANDOFF_SCHEMA } from '../utils/schema-validator.js';
|
|
12
|
+
import * as yaml from 'js-yaml';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Valid status values for handoff blocks.
|
|
@@ -81,9 +82,16 @@ export function validateHandoff(handoff) {
|
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
const missingInputQuestion = handoff.status === 'needs_input'
|
|
86
|
+
&& (typeof handoff.needs_input_question !== 'string' || !handoff.needs_input_question.trim());
|
|
87
|
+
if (missingInputQuestion) {
|
|
88
|
+
warnings.push('needs_input status requires one non-empty needs_input_question');
|
|
89
|
+
}
|
|
90
|
+
|
|
84
91
|
// A handoff is valid if there are no errors (warnings are OK)
|
|
85
92
|
const hasErrors = schemaResult.errors.length > 0 ||
|
|
86
|
-
(handoff.status && !VALID_STATUSES.includes(handoff.status))
|
|
93
|
+
(handoff.status && !VALID_STATUSES.includes(handoff.status)) ||
|
|
94
|
+
missingInputQuestion;
|
|
87
95
|
|
|
88
96
|
return { valid: !hasErrors, warnings };
|
|
89
97
|
}
|
|
@@ -117,6 +125,14 @@ function parseHandoffFields(content) {
|
|
|
117
125
|
};
|
|
118
126
|
|
|
119
127
|
const lines = content.split('\n');
|
|
128
|
+
let yamlQuestion;
|
|
129
|
+
try {
|
|
130
|
+
const document = yaml.load(content, { schema: yaml.JSON_SCHEMA });
|
|
131
|
+
if (document && typeof document === 'object' && !Array.isArray(document)
|
|
132
|
+
&& typeof document.needs_input_question === 'string') {
|
|
133
|
+
yamlQuestion = document.needs_input_question;
|
|
134
|
+
}
|
|
135
|
+
} catch { /* retain the permissive legacy field parser for malformed blocks */ }
|
|
120
136
|
let currentKey = null;
|
|
121
137
|
let currentType = null; // 'list' | 'map'
|
|
122
138
|
|
|
@@ -199,5 +215,7 @@ function parseHandoffFields(content) {
|
|
|
199
215
|
}
|
|
200
216
|
}
|
|
201
217
|
|
|
218
|
+
if (yamlQuestion !== undefined) result.needs_input_question = yamlQuestion === '' ? null : yamlQuestion;
|
|
219
|
+
|
|
202
220
|
return result;
|
|
203
221
|
}
|
|
@@ -19,6 +19,7 @@ import { resolveProviderForAgent } from './cli-registry.js';
|
|
|
19
19
|
import { buildCompactGotchasSummary } from '../memory/gotchas-injector.js';
|
|
20
20
|
import { estimateTokens } from './cost-tracker.js';
|
|
21
21
|
import { resolveContextLimit } from '../utils/provider-limits.js';
|
|
22
|
+
import { V2_PLANNING_AGENTS } from '../orchestrator/runtime-installation-v2.js';
|
|
22
23
|
|
|
23
24
|
// Import AGENT_MODELS from model-governance (safe — named export,
|
|
24
25
|
// does not trigger main() which is guarded by fileURLToPath check).
|
|
@@ -131,7 +132,7 @@ export function buildToolMeshSection(agent) {
|
|
|
131
132
|
* @property {string} projectDir - Project root (absolute path)
|
|
132
133
|
* @property {string} [previousAgent] - Agent that produced the handoff
|
|
133
134
|
* @property {string} [workflow] - Active workflow name
|
|
134
|
-
* @property {object} [sessionState] -
|
|
135
|
+
* @property {object} [sessionState] - Canonical session fields loaded by the session manager
|
|
135
136
|
* @property {string[]} [writeScope] - Override write scope
|
|
136
137
|
* @property {string} [additionalContext] - Extra context (e.g. user answer to needs_input)
|
|
137
138
|
* @property {string} [provider] - Active CLI provider (for overlay resolution)
|
|
@@ -153,6 +154,12 @@ export function buildAgentPrompt(config) {
|
|
|
153
154
|
throw new Error('buildAgentPrompt requires config.projectDir');
|
|
154
155
|
}
|
|
155
156
|
const runtimeV2 = existsSync(join(config.projectDir, '.chati', 'v2', 'installation.json'));
|
|
157
|
+
if (runtimeV2 && !V2_PLANNING_AGENTS.includes(config.agent)) {
|
|
158
|
+
throw Object.assign(
|
|
159
|
+
new Error(`Runtime v2 does not expose the legacy ${config.agent} agent; implementation and release must use RAIL`),
|
|
160
|
+
{ code: 'V2_RAIL_EXECUTION_REQUIRED' },
|
|
161
|
+
);
|
|
162
|
+
}
|
|
156
163
|
if (runtimeV2 && (!config.provider || !config.model)) {
|
|
157
164
|
throw new Error('Runtime v2 requires an exact routed provider and model binding');
|
|
158
165
|
}
|
|
@@ -247,6 +254,7 @@ function buildSpawnAuthoritySection(config) {
|
|
|
247
254
|
|
|
248
255
|
This process was launched by the active Chati.dev orchestrator for agent \`${config.agent}\`.
|
|
249
256
|
Execute the assigned work now. Do not ask the user to activate Chati.dev, switch CLI, authenticate a provider, or restart the session.
|
|
257
|
+
The canonical session context in this prompt is the only session state you may consume. Never read, parse, write, reconstruct, or edit \`.chati/session.yaml\` directly. Return state changes in the structured handoff so the parent router and session manager can persist them.
|
|
250
258
|
If essential user information is missing, return status \`needs_input\` with exactly one concrete question in \`needs_input_question\`.
|
|
251
259
|
Never claim that this spawned session is paused. The parent orchestrator owns user interaction and will relay your structured handoff.`;
|
|
252
260
|
}
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* execution. These checks never mutate provider configuration.
|
|
4
4
|
*/
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { closeSync } from 'node:fs';
|
|
6
7
|
import { getProvider } from './cli-registry.js';
|
|
7
8
|
import { cleanParentEnv } from './spawner.js';
|
|
9
|
+
import { openPinnedProviderExecutable } from '../installer-v2/provider-executable.js';
|
|
8
10
|
|
|
9
11
|
const CHECKS = {
|
|
10
12
|
claude: { args: ['auth', 'status'], validate: validateClaude },
|
|
@@ -23,20 +25,52 @@ export function checkProviderReadiness(providerName, options = {}) {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const check = CHECKS[providerName] || { args: ['--version'], validate: validateExit };
|
|
28
|
+
let pinnedExecutable;
|
|
29
|
+
let command = provider.command;
|
|
30
|
+
let descriptorStdio;
|
|
31
|
+
try {
|
|
32
|
+
if (options.executableBinding) {
|
|
33
|
+
pinnedExecutable = openPinnedProviderExecutable(options.executableBinding, {
|
|
34
|
+
harnessId: providerName,
|
|
35
|
+
projectDir: options.workingDir,
|
|
36
|
+
});
|
|
37
|
+
// A readiness probe must target the exact binding selected by the signed
|
|
38
|
+
// installation. On Linux map the still-open descriptor to a fixed child
|
|
39
|
+
// FD and execute through it. A direct /proc/self/fd/<parent-fd> path is
|
|
40
|
+
// not sufficient because Node closes arbitrary parent descriptors in
|
|
41
|
+
// the child before exec. Other platforms retain the validated pathname.
|
|
42
|
+
if (process.platform === 'linux') {
|
|
43
|
+
command = '/proc/self/fd/3';
|
|
44
|
+
descriptorStdio = ['ignore', 'pipe', 'pipe', pinnedExecutable.descriptor];
|
|
45
|
+
} else {
|
|
46
|
+
command = pinnedExecutable.binding.executable_path;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
ready: false,
|
|
52
|
+
code: error?.code || 'PROVIDER_EXECUTABLE_BINDING_INVALID',
|
|
53
|
+
provider: providerName,
|
|
54
|
+
detail: error.message,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
26
57
|
let result;
|
|
27
58
|
try {
|
|
28
|
-
result = runner(
|
|
59
|
+
result = runner(command, check.args, {
|
|
29
60
|
cwd: options.workingDir || process.cwd(),
|
|
30
61
|
env: cleanParentEnv(process.env),
|
|
31
62
|
encoding: 'utf8',
|
|
32
63
|
timeout: options.timeout || 30_000,
|
|
64
|
+
...(descriptorStdio ? { stdio: descriptorStdio } : {}),
|
|
33
65
|
});
|
|
34
66
|
} catch (error) {
|
|
35
67
|
return { ready: false, code: 'PROVIDER_PREFLIGHT_FAILED', provider: providerName, detail: error.message };
|
|
68
|
+
} finally {
|
|
69
|
+
if (pinnedExecutable) closeSync(pinnedExecutable.descriptor);
|
|
36
70
|
}
|
|
37
71
|
|
|
38
72
|
if (result?.error?.code === 'ENOENT') {
|
|
39
|
-
return { ready: false, code: 'PROVIDER_CLI_MISSING', provider: providerName, detail: `${
|
|
73
|
+
return { ready: false, code: 'PROVIDER_CLI_MISSING', provider: providerName, detail: `${command} is not installed` };
|
|
40
74
|
}
|
|
41
75
|
return check.validate(result, providerName);
|
|
42
76
|
}
|
|
@@ -75,4 +109,3 @@ function validateGrok(result, provider) {
|
|
|
75
109
|
// that the CLI can load the trusted project and its effective config.
|
|
76
110
|
return validateExit(result, provider);
|
|
77
111
|
}
|
|
78
|
-
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
4
|
+
import { sha256 } from '@chati/core';
|
|
5
|
+
|
|
6
|
+
const MAX_GIT_OUTPUT_BYTES = 128 * 1024 * 1024;
|
|
7
|
+
const GIT_TIMEOUT_MS = 30_000;
|
|
8
|
+
const CONTROL_PLANE_EXCLUDES = Object.freeze([
|
|
9
|
+
':(exclude).chati',
|
|
10
|
+
':(exclude).chati/**',
|
|
11
|
+
':(exclude).chati.dev',
|
|
12
|
+
':(exclude).chati.dev/**',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function fail(code, message, details = {}) {
|
|
16
|
+
const error = new Error(message);
|
|
17
|
+
error.code = code;
|
|
18
|
+
error.details = details;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function trustedGitEnvironment() {
|
|
23
|
+
const environment = { ...process.env };
|
|
24
|
+
for (const key of Object.keys(environment)) {
|
|
25
|
+
if (key.startsWith('GIT_')) delete environment[key];
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
...environment,
|
|
29
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
30
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
31
|
+
GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null',
|
|
32
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function git(projectDir, args, { encoding = 'utf8', timeout = GIT_TIMEOUT_MS } = {}) {
|
|
37
|
+
try {
|
|
38
|
+
return execFileSync('git', [
|
|
39
|
+
'-c', 'core.fsmonitor=false',
|
|
40
|
+
'-c', 'core.hooksPath=/dev/null',
|
|
41
|
+
'-C', projectDir,
|
|
42
|
+
...args,
|
|
43
|
+
], {
|
|
44
|
+
encoding,
|
|
45
|
+
timeout,
|
|
46
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
47
|
+
env: trustedGitEnvironment(),
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return fail('RAIL_EXECUTION_GIT_STATE_UNAVAILABLE', 'cannot prove the Git state of the authorized execution worktree', {
|
|
51
|
+
cause: error.message,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function directoryIdentity(path) {
|
|
57
|
+
const stat = lstatSync(path);
|
|
58
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
59
|
+
fail('RAIL_EXECUTION_REPOSITORY_INVALID', 'execution Git storage must resolve to a real directory');
|
|
60
|
+
}
|
|
61
|
+
return Object.freeze({
|
|
62
|
+
path: realpathSync(path),
|
|
63
|
+
dev: stat.dev,
|
|
64
|
+
ino: stat.ino,
|
|
65
|
+
uid: stat.uid,
|
|
66
|
+
gid: stat.gid,
|
|
67
|
+
mode: stat.mode,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function repositoryIdentity(projectDir) {
|
|
72
|
+
const root = realpathSync(resolve(projectDir));
|
|
73
|
+
const topLevel = realpathSync(git(root, ['rev-parse', '--show-toplevel']).trim());
|
|
74
|
+
if (topLevel !== root) {
|
|
75
|
+
fail('RAIL_EXECUTION_REPOSITORY_INVALID', 'authorized execution must run at the exact Git worktree root');
|
|
76
|
+
}
|
|
77
|
+
const gitEntryPath = resolve(root, '.git');
|
|
78
|
+
const gitEntryStat = lstatSync(gitEntryPath);
|
|
79
|
+
let gitEntry;
|
|
80
|
+
if (gitEntryStat.isDirectory() && !gitEntryStat.isSymbolicLink()) {
|
|
81
|
+
gitEntry = Object.freeze({
|
|
82
|
+
type: 'directory', dev: gitEntryStat.dev, ino: gitEntryStat.ino,
|
|
83
|
+
uid: gitEntryStat.uid, gid: gitEntryStat.gid, mode: gitEntryStat.mode,
|
|
84
|
+
});
|
|
85
|
+
} else if (gitEntryStat.isFile() && !gitEntryStat.isSymbolicLink() && gitEntryStat.size <= 4096) {
|
|
86
|
+
gitEntry = Object.freeze({
|
|
87
|
+
type: 'file', dev: gitEntryStat.dev, ino: gitEntryStat.ino,
|
|
88
|
+
uid: gitEntryStat.uid, gid: gitEntryStat.gid, mode: gitEntryStat.mode,
|
|
89
|
+
digest: sha256(readFileSync(gitEntryPath)),
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
fail('RAIL_EXECUTION_REPOSITORY_INVALID', 'execution .git entry must be a real directory or bounded regular worktree pointer');
|
|
93
|
+
}
|
|
94
|
+
const absoluteGitDir = git(root, ['rev-parse', '--absolute-git-dir']).trim();
|
|
95
|
+
const commonGitDirRaw = git(root, ['rev-parse', '--git-common-dir']).trim();
|
|
96
|
+
const commonGitDir = isAbsolute(commonGitDirRaw) ? commonGitDirRaw : resolve(root, commonGitDirRaw);
|
|
97
|
+
const rootStat = lstatSync(root);
|
|
98
|
+
return Object.freeze({
|
|
99
|
+
root: Object.freeze({
|
|
100
|
+
path: root, dev: rootStat.dev, ino: rootStat.ino,
|
|
101
|
+
uid: rootStat.uid, gid: rootStat.gid, mode: rootStat.mode,
|
|
102
|
+
}),
|
|
103
|
+
git_entry: gitEntry,
|
|
104
|
+
git_dir: directoryIdentity(absoluteGitDir),
|
|
105
|
+
common_git_dir: directoryIdentity(commonGitDir),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function worktreeStatus(projectDir) {
|
|
110
|
+
return git(projectDir, [
|
|
111
|
+
'status', '--porcelain=v1', '--untracked-files=all', '--', '.', ...CONTROL_PLANE_EXCLUDES,
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function preexistingCommits(projectDir) {
|
|
116
|
+
const output = git(projectDir, [
|
|
117
|
+
'cat-file', '--batch-all-objects', '--batch-check=%(objectname) %(objecttype)',
|
|
118
|
+
]);
|
|
119
|
+
const commits = output.split('\n').filter(Boolean).flatMap((line) => {
|
|
120
|
+
const [objectId, type, extra] = line.trim().split(/\s+/);
|
|
121
|
+
if (extra !== undefined || !/^[a-f0-9]{40,64}$/.test(objectId || '') || !type) {
|
|
122
|
+
fail('RAIL_EXECUTION_GIT_STATE_UNAVAILABLE', 'Git returned a malformed object inventory');
|
|
123
|
+
}
|
|
124
|
+
return type === 'commit' ? [objectId] : [];
|
|
125
|
+
});
|
|
126
|
+
return Object.freeze([...new Set(commits)].sort());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function captureRailExecutionWorktree({ projectDir, baseCommitRef } = {}) {
|
|
130
|
+
if (!/^[a-f0-9]{40,64}$/.test(baseCommitRef || '')) {
|
|
131
|
+
fail('RAIL_EXECUTION_BASE_COMMIT_REQUIRED', 'authorized execution requires one full lowercase base commit id');
|
|
132
|
+
}
|
|
133
|
+
const root = realpathSync(resolve(projectDir));
|
|
134
|
+
const repository = repositoryIdentity(root);
|
|
135
|
+
const head = git(root, ['rev-parse', '--verify', 'HEAD^{commit}']).trim();
|
|
136
|
+
if (head !== baseCommitRef) {
|
|
137
|
+
fail('RAIL_EXECUTION_BASE_HEAD_MISMATCH', 'execution worktree HEAD must equal the exact authorized base commit', {
|
|
138
|
+
expected: baseCommitRef,
|
|
139
|
+
actual: head,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const status = worktreeStatus(root);
|
|
143
|
+
if (status !== '') {
|
|
144
|
+
fail('RAIL_EXECUTION_WORKTREE_DIRTY', 'execution worktree must be clean outside the protected CHATI control plane');
|
|
145
|
+
}
|
|
146
|
+
const commits = preexistingCommits(root);
|
|
147
|
+
if (!commits.includes(baseCommitRef)) {
|
|
148
|
+
fail('RAIL_EXECUTION_BASE_COMMIT_REQUIRED', 'authorized base commit is absent from the execution repository');
|
|
149
|
+
}
|
|
150
|
+
return Object.freeze({
|
|
151
|
+
schema_version: 1,
|
|
152
|
+
project_dir: root,
|
|
153
|
+
base_commit_ref: baseCommitRef,
|
|
154
|
+
launch_head_ref: head,
|
|
155
|
+
repository_identity: repository,
|
|
156
|
+
repository_identity_digest: sha256(repository),
|
|
157
|
+
preexisting_commits: commits,
|
|
158
|
+
preexisting_commits_digest: sha256(commits),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function assertRailExecutionWorktreeBaselineUnchanged(baseline) {
|
|
163
|
+
const current = captureRailExecutionWorktree({
|
|
164
|
+
projectDir: baseline?.project_dir,
|
|
165
|
+
baseCommitRef: baseline?.base_commit_ref,
|
|
166
|
+
});
|
|
167
|
+
if (sha256(current) !== sha256(baseline)) {
|
|
168
|
+
fail('RAIL_EXECUTION_BASELINE_CHANGED', 'execution Git state changed between authority capture and provider launch');
|
|
169
|
+
}
|
|
170
|
+
return current;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function verifyRailExecutionWorktreeResult({ baseline, resultCommitRef } = {}) {
|
|
174
|
+
if (!baseline || !/^[a-f0-9]{40,64}$/.test(resultCommitRef || '')) {
|
|
175
|
+
fail('RAIL_EXECUTION_OUTPUT_INVALID', 'execution worktree proof requires one full lowercase result commit id');
|
|
176
|
+
}
|
|
177
|
+
const repository = repositoryIdentity(baseline.project_dir);
|
|
178
|
+
if (sha256(repository) !== baseline.repository_identity_digest) {
|
|
179
|
+
fail('RAIL_EXECUTION_REPOSITORY_CHANGED', 'execution replaced or redirected its Git repository storage');
|
|
180
|
+
}
|
|
181
|
+
if (baseline.preexisting_commits.includes(resultCommitRef)) {
|
|
182
|
+
fail('RAIL_EXECUTION_RESULT_PREEXISTED', 'execution may not claim a commit that existed before its authorized launch');
|
|
183
|
+
}
|
|
184
|
+
const head = git(baseline.project_dir, ['rev-parse', '--verify', 'HEAD^{commit}']).trim();
|
|
185
|
+
if (head !== resultCommitRef) {
|
|
186
|
+
fail('RAIL_EXECUTION_RESULT_HEAD_MISMATCH', 'reported result commit must be the exact final execution worktree HEAD', {
|
|
187
|
+
reported: resultCommitRef,
|
|
188
|
+
actual: head,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
git(baseline.project_dir, ['merge-base', '--is-ancestor', baseline.base_commit_ref, resultCommitRef]);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (error?.code === 'RAIL_EXECUTION_GIT_STATE_UNAVAILABLE') {
|
|
195
|
+
fail('RAIL_EXECUTION_RESULT_NOT_DESCENDANT', 'final execution HEAD must descend from the authorized base commit');
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
if (worktreeStatus(baseline.project_dir) !== '') {
|
|
200
|
+
fail('RAIL_EXECUTION_WORKTREE_DIRTY', 'successful execution must commit every change outside the protected CHATI control plane');
|
|
201
|
+
}
|
|
202
|
+
return Object.freeze({
|
|
203
|
+
schema_version: 1,
|
|
204
|
+
base_commit_ref: baseline.base_commit_ref,
|
|
205
|
+
launch_head_ref: baseline.launch_head_ref,
|
|
206
|
+
result_commit_ref: resultCommitRef,
|
|
207
|
+
result_head_ref: head,
|
|
208
|
+
repository_identity_digest: baseline.repository_identity_digest,
|
|
209
|
+
preexisting_commits_digest: baseline.preexisting_commits_digest,
|
|
210
|
+
result_preexisted: false,
|
|
211
|
+
worktree_clean: true,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
const RAIL_LANGUAGE_CONTRACTS = Object.freeze({
|
|
4
|
+
en: 'Visible-language contract: write all user-visible prose in English. Keep required machine-readable JSON keys, tags, identifiers and code unchanged.',
|
|
5
|
+
pt: 'Visible-language contract: write all user-visible prose in Brazilian Portuguese (pt-BR). Keep required machine-readable JSON keys, tags, identifiers and code unchanged.',
|
|
6
|
+
es: 'Visible-language contract: write all user-visible prose in Spanish. Keep required machine-readable JSON keys, tags, identifiers and code unchanged.',
|
|
7
|
+
fr: 'Visible-language contract: write all user-visible prose in French. Keep required machine-readable JSON keys, tags, identifiers and code unchanged.',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export function railLanguageContract(language = 'en') {
|
|
11
|
+
const contract = RAIL_LANGUAGE_CONTRACTS[language];
|
|
12
|
+
if (!contract) {
|
|
13
|
+
throw Object.assign(new Error(`Unsupported canonical RAIL language: ${language}`), { code: 'RAIL_SESSION_LANGUAGE_INVALID' });
|
|
14
|
+
}
|
|
15
|
+
return contract;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildRailTaskPrompt({ projectDir, stored, task, invocation, browserManifest = null, language = invocation?.language ?? 'en' }) {
|
|
19
|
+
return [
|
|
20
|
+
'You are the execution operator for one immutable Chati.dev RAIL task.',
|
|
21
|
+
`Project root: ${resolve(projectDir)}`,
|
|
22
|
+
`Sealed handoff: ${stored.path}`,
|
|
23
|
+
`Task id: ${task.task_id}`,
|
|
24
|
+
'',
|
|
25
|
+
'The provider, model and reasoning configuration were selected by the signed installation and are immutable for this attempt.',
|
|
26
|
+
`Provider binding: ${invocation.provider_id}/${invocation.harness_id}`,
|
|
27
|
+
`Model pin: ${invocation.model_pin.model_id}`,
|
|
28
|
+
`Reasoning: ${invocation.model_pin.reasoning_configuration}`,
|
|
29
|
+
`Launch nonce: ${invocation.launch_nonce}`,
|
|
30
|
+
railLanguageContract(language),
|
|
31
|
+
'',
|
|
32
|
+
'Execute only the task below. Read the referenced project artifacts before editing. Do not expand scope, deploy, publish, or substitute another model.',
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
task_id: task.task_id,
|
|
35
|
+
dependency_ids: task.dependency_ids,
|
|
36
|
+
execution_scope_ref: task.execution_scope_ref,
|
|
37
|
+
acceptance_criteria_refs: task.acceptance_criteria_refs,
|
|
38
|
+
routing_constraints_ref: task.routing_constraints_ref,
|
|
39
|
+
requires_browser: task.requires_browser === true,
|
|
40
|
+
browser_policy: task.browser_policy ?? null,
|
|
41
|
+
source_artifacts: stored.handoff.source_artifacts,
|
|
42
|
+
task_graph_ref: stored.handoff.task_graph_ref,
|
|
43
|
+
browser_delegation_manifest: browserManifest,
|
|
44
|
+
}, null, 2),
|
|
45
|
+
'',
|
|
46
|
+
`Sealed base commit: ${stored.handoff.seal.git_commit_ref}`,
|
|
47
|
+
'When finished, commit the complete task result as a new descendant of the sealed base commit. Then return exactly one machine-readable result envelope:',
|
|
48
|
+
'<CHATI_RAIL_EXECUTION>',
|
|
49
|
+
`{"result_commit_ref":"FULL_LOWERCASE_GIT_OBJECT_ID","launch_nonce":"${invocation.launch_nonce}"}`,
|
|
50
|
+
'</CHATI_RAIL_EXECUTION>',
|
|
51
|
+
'Outside the envelope you may report files changed, checks executed, remaining risks and evidence references. RAIL review and completion are separate canonical steps.',
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildRailReviewPrompt({ projectDir, context, invocation, reviewSubject, round }) {
|
|
56
|
+
return [
|
|
57
|
+
'You are an independent clean-context reviewer for one immutable Chati.dev RAIL attempt.',
|
|
58
|
+
`Project root: ${resolve(projectDir)}`,
|
|
59
|
+
`Handoff id: ${context.handoff.handoff_id}`,
|
|
60
|
+
`Task id: ${context.task.task_id}`,
|
|
61
|
+
`Attempt id: ${context.attempt.attempt_id}`,
|
|
62
|
+
`Review round: ${round}`,
|
|
63
|
+
`Sealed base commit: ${reviewSubject.base_commit_ref}`,
|
|
64
|
+
`Immutable result commit under review: ${reviewSubject.result_commit_ref}`,
|
|
65
|
+
...(reviewSubject.workspace_digest ? [`Immutable review workspace digest: ${reviewSubject.workspace_digest}`] : []),
|
|
66
|
+
`Builder: ${context.task.execution_binding.provider_id}/${context.task.execution_binding.model_id}`,
|
|
67
|
+
`Reviewer pin: ${invocation.provider_id}/${invocation.model_pin.model_id}/${invocation.model_pin.reasoning_configuration}`,
|
|
68
|
+
`Launch nonce: ${invocation.launch_nonce}`,
|
|
69
|
+
railLanguageContract(invocation?.language ?? context?.language ?? 'en'),
|
|
70
|
+
'',
|
|
71
|
+
'Read the changed files and referenced artifacts yourself. The complete immutable delta is CHATI_RAIL_REVIEW.diff, including files omitted by Git archive metadata. Validate scope, acceptance criteria, architecture, tests, regressions and security. Do not edit files.',
|
|
72
|
+
'Use evidence-backed findings only. A valid issue uses disposition "accepted"; a disproven suspicion uses "rejected". If any accepted finding exists, verdict must be "rework".',
|
|
73
|
+
'Return exactly one JSON object between the tags below and no other tagged envelope:',
|
|
74
|
+
'<CHATI_RAIL_REVIEW>',
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
findings: [{ finding_id: 'finding-unique', severity: 'critical|high|medium|low', location_ref: 'file://path:line', evidence_refs: ['evidence://specific'], disposition: 'accepted|rejected|unresolved', disposition_reason_ref: 'evidence://reason', root_cause_taxonomy: 'implementation|specification|architecture|security|test' }],
|
|
77
|
+
verdict: 'accept|rework|required-human-decision|blocked',
|
|
78
|
+
impasse: { kind: 'none|technical', evidence_ref: 'required only for technical' },
|
|
79
|
+
authority_reason: 'scope|security|privacy|cost|external-action only when verdict is required-human-decision',
|
|
80
|
+
authority_reason_ref: 'evidence ref only when required-human-decision',
|
|
81
|
+
}, null, 2),
|
|
82
|
+
'</CHATI_RAIL_REVIEW>',
|
|
83
|
+
'',
|
|
84
|
+
'Canonical task material:',
|
|
85
|
+
JSON.stringify({
|
|
86
|
+
execution_scope_ref: context.task.execution_scope_ref,
|
|
87
|
+
acceptance_criteria_refs: context.task.acceptance_criteria_refs,
|
|
88
|
+
routing_constraints_ref: context.task.routing_constraints_ref,
|
|
89
|
+
source_artifacts: context.handoff.source_artifacts,
|
|
90
|
+
task_graph_ref: context.handoff.task_graph_ref,
|
|
91
|
+
}, null, 2),
|
|
92
|
+
].join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function buildRailReworkPrompt({ projectDir, context, sourceReview, sourceAdjudication = null, executionHistory, baseCommitRef, invocation }) {
|
|
96
|
+
return [
|
|
97
|
+
'You are correcting one immutable Chati.dev RAIL attempt after an independent review.',
|
|
98
|
+
`Project root: ${resolve(projectDir)}`,
|
|
99
|
+
`Handoff id: ${context.handoff.handoff_id}`,
|
|
100
|
+
`Task id: ${context.task.task_id}`,
|
|
101
|
+
`Attempt id: ${context.attempt.attempt_id}`,
|
|
102
|
+
`Launch nonce: ${invocation.launch_nonce}`,
|
|
103
|
+
railLanguageContract(invocation?.language ?? context?.language ?? 'en'),
|
|
104
|
+
'',
|
|
105
|
+
'Use the exact original provider, model and reasoning binding. Address every accepted finding without expanding scope, deploying or publishing.',
|
|
106
|
+
'Read the referenced files and evidence yourself. Preserve unrelated user changes. Run the checks needed to prove each correction.',
|
|
107
|
+
'',
|
|
108
|
+
'Canonical accepted review:',
|
|
109
|
+
JSON.stringify(sourceReview, null, 2),
|
|
110
|
+
...(sourceAdjudication ? [
|
|
111
|
+
'',
|
|
112
|
+
'Canonical adjudication and selected-council evidence:',
|
|
113
|
+
JSON.stringify(sourceAdjudication, null, 2),
|
|
114
|
+
] : []),
|
|
115
|
+
'',
|
|
116
|
+
'Prior immutable execution evidence:',
|
|
117
|
+
JSON.stringify(executionHistory, null, 2),
|
|
118
|
+
'',
|
|
119
|
+
'Sealed task material:',
|
|
120
|
+
JSON.stringify({
|
|
121
|
+
execution_scope_ref: context.task.execution_scope_ref,
|
|
122
|
+
acceptance_criteria_refs: context.task.acceptance_criteria_refs,
|
|
123
|
+
routing_constraints_ref: context.task.routing_constraints_ref,
|
|
124
|
+
source_artifacts: context.handoff.source_artifacts,
|
|
125
|
+
task_graph_ref: context.handoff.task_graph_ref,
|
|
126
|
+
}, null, 2),
|
|
127
|
+
'',
|
|
128
|
+
`Bound rework base commit: ${baseCommitRef}`,
|
|
129
|
+
'When finished, commit the complete corrected result as a new descendant of that bound base. Then return exactly one machine-readable result envelope:',
|
|
130
|
+
'<CHATI_RAIL_EXECUTION>',
|
|
131
|
+
`{"result_commit_ref":"FULL_LOWERCASE_GIT_OBJECT_ID","launch_nonce":"${invocation.launch_nonce}"}`,
|
|
132
|
+
'</CHATI_RAIL_EXECUTION>',
|
|
133
|
+
'Outside the envelope you may report corrected findings, files changed, checks executed, remaining risks and evidence references. A new independent review is mandatory.',
|
|
134
|
+
].join('\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function buildRailAdjudicationPrompt({ projectDir, context, plan, invocation, reviews, executions, priorDecisions, round, reviewSubject = null }) {
|
|
138
|
+
return [
|
|
139
|
+
'You are one selected top-tier adjudicator for a Chati.dev RAIL technical impasse.',
|
|
140
|
+
`Project root: ${resolve(projectDir)}`,
|
|
141
|
+
`Handoff id: ${context.handoff.handoff_id}`,
|
|
142
|
+
`Task id: ${context.task.task_id}`,
|
|
143
|
+
`Attempt id: ${context.attempt.attempt_id}`,
|
|
144
|
+
`Adjudication round: ${round}`,
|
|
145
|
+
...(reviewSubject?.result_commit_ref ? [`Immutable result commit under adjudication: ${reviewSubject.result_commit_ref}`] : []),
|
|
146
|
+
...(reviewSubject?.workspace_digest ? [`Immutable adjudication workspace digest: ${reviewSubject.workspace_digest}`] : []),
|
|
147
|
+
`Exact model pin: ${invocation.provider_id}/${invocation.model_pin.model_id}/${invocation.model_pin.reasoning_configuration}`,
|
|
148
|
+
`Launch nonce: ${invocation.launch_nonce}`,
|
|
149
|
+
railLanguageContract(invocation?.language ?? context?.language ?? 'en'),
|
|
150
|
+
'',
|
|
151
|
+
'Read the project evidence yourself. Do not edit files, deploy, publish or widen scope.',
|
|
152
|
+
'Choose only: bounded-correction for a technically local correction, handoff-revision when sealed planning must change, or blocked-authority only for scope, security, privacy, cost or external-action authority.',
|
|
153
|
+
priorDecisions?.length ? 'The previous independent decisions disagreed. Reconcile them using evidence, not majority or vendor preference.' : '',
|
|
154
|
+
'Return exactly one JSON object inside the required tags:',
|
|
155
|
+
'<CHATI_RAIL_ADJUDICATION>',
|
|
156
|
+
JSON.stringify({
|
|
157
|
+
outcome: 'bounded-correction|handoff-revision|blocked-authority',
|
|
158
|
+
decision_ref: 'evidence://adjudicator/decision',
|
|
159
|
+
correction_ref: 'evidence://correction (bounded-correction only)',
|
|
160
|
+
handoff_revision_ref: 'evidence://handoff-revision (handoff-revision only)',
|
|
161
|
+
authority_reason: 'scope|security|privacy|cost|external-action (blocked-authority only)',
|
|
162
|
+
authority_reason_ref: 'evidence://authority (blocked-authority only)',
|
|
163
|
+
}, null, 2),
|
|
164
|
+
'</CHATI_RAIL_ADJUDICATION>',
|
|
165
|
+
'',
|
|
166
|
+
'Canonical review decision and evidence:',
|
|
167
|
+
JSON.stringify({ plan, reviews, executions, prior_decisions: priorDecisions || [] }, null, 2),
|
|
168
|
+
].filter(Boolean).join('\n');
|
|
169
|
+
}
|