chati-dev 2.1.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/framework/agents/build/dev.md +1 -0
- package/framework/agents/deploy/devops.md +1 -0
- package/framework/agents/discover/brief.md +1 -0
- package/framework/agents/discover/brownfield-wu.md +1 -0
- package/framework/agents/discover/greenfield-wu.md +1 -0
- package/framework/agents/plan/architect.md +1 -0
- package/framework/agents/plan/detail.md +1 -0
- package/framework/agents/plan/phases.md +1 -0
- package/framework/agents/plan/tasks.md +1 -0
- package/framework/agents/plan/ux.md +1 -0
- package/framework/agents/quality/qa-implementation.md +1 -0
- package/framework/agents/quality/qa-planning.md +1 -0
- package/framework/config.yaml +21 -2
- package/framework/constitution.md +66 -1
- package/framework/domains/agents/brownfield-wu.yaml +4 -0
- package/framework/domains/agents/dev.yaml +4 -0
- package/framework/domains/agents/orchestrator.yaml +8 -0
- package/framework/domains/constitution.yaml +28 -0
- package/framework/domains/global.yaml +20 -0
- package/framework/hooks/model-governance.js +17 -15
- package/framework/intelligence/context-engine.md +29 -0
- package/framework/intelligence/memory-layer.md +17 -0
- package/framework/orchestrator/chati.md +94 -14
- package/framework/schemas/config.schema.json +44 -0
- package/framework/schemas/session.schema.json +27 -0
- package/package.json +5 -1
- package/src/autonomy/build-loop.js +194 -0
- package/src/autonomy/build-state.js +269 -0
- package/src/autonomy/execution-profile.js +151 -0
- package/src/config/context-file-generator.js +209 -0
- package/src/gates/g2-qa-planning.js +4 -2
- package/src/gates/g4-qa-implementation.js +5 -2
- package/src/gates/gate-base.js +33 -1
- package/src/health/engine.js +250 -0
- package/src/intelligence/file-tracker.js +117 -0
- package/src/intelligence/timeline.js +144 -0
- package/src/memory/gotchas-auto-capture.js +253 -0
- package/src/terminal/adapters/claude-adapter.js +43 -0
- package/src/terminal/adapters/codex-adapter.js +41 -0
- package/src/terminal/adapters/copilot-adapter.js +38 -0
- package/src/terminal/adapters/gemini-adapter.js +42 -0
- package/src/terminal/adapters/index.js +8 -0
- package/src/terminal/cli-registry.js +218 -0
- package/src/terminal/prompt-builder.js +18 -15
- package/src/terminal/spawner.js +19 -9
- package/src/terminal/wave-analyzer.js +143 -0
- package/framework/manifest.json +0 -5
- package/framework/manifest.sig +0 -1
- /package/assets/{logo - co/314/201pia.png" → logo - c/303/263pia.png"} +0 -0
- /package/assets/{logo - co/314/201pia.svg" → logo - c/303/263pia.svg"} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chati-dev",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "AI-Powered Multi-Agent Orchestration System — Structured vibe coding for Full Stack Development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"multi-agent",
|
|
39
39
|
"cli",
|
|
40
40
|
"claude",
|
|
41
|
+
"gemini",
|
|
42
|
+
"codex",
|
|
43
|
+
"copilot",
|
|
44
|
+
"multi-llm",
|
|
41
45
|
"orchestration",
|
|
42
46
|
"fullstack"
|
|
43
47
|
],
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Autonomous build loop (Ralph Wiggum v2).
|
|
3
|
+
*
|
|
4
|
+
* Executes tasks autonomously with checkpoint-based state management,
|
|
5
|
+
* retry logic, and quality gate integration.
|
|
6
|
+
*
|
|
7
|
+
* Named "Ralph Wiggum" internally — the autonomous execution mode
|
|
8
|
+
* that iterates until all tasks are complete or escalation is needed.
|
|
9
|
+
*
|
|
10
|
+
* Constitution Article XVII — Execution Mode Governance.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
createBuildState,
|
|
15
|
+
loadBuildState,
|
|
16
|
+
saveBuildState,
|
|
17
|
+
startBuild,
|
|
18
|
+
completeBuild,
|
|
19
|
+
failBuild,
|
|
20
|
+
updateCheckpoint,
|
|
21
|
+
getNextPendingTask,
|
|
22
|
+
isTaskExhausted,
|
|
23
|
+
isTimedOut,
|
|
24
|
+
getProgress,
|
|
25
|
+
CheckpointStatus,
|
|
26
|
+
BuildStatus,
|
|
27
|
+
} from './build-state.js';
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Build Loop
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {object} BuildLoopConfig
|
|
35
|
+
* @property {string} projectDir - Project root directory
|
|
36
|
+
* @property {string[]} taskIds - Task IDs to execute
|
|
37
|
+
* @property {function(string): Promise<{success: boolean, output: string}>} executor - Task execution function
|
|
38
|
+
* @property {function(object): void} [onProgress] - Progress callback
|
|
39
|
+
* @property {boolean} [resume=false] - Whether to resume from existing state
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {object} BuildLoopResult
|
|
44
|
+
* @property {string} status - Final build status
|
|
45
|
+
* @property {number} completed - Tasks completed
|
|
46
|
+
* @property {number} failed - Tasks failed
|
|
47
|
+
* @property {number} totalAttempts - Total execution attempts
|
|
48
|
+
* @property {string} duration - Human-readable duration
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run the autonomous build loop.
|
|
53
|
+
*
|
|
54
|
+
* Loop logic:
|
|
55
|
+
* 1. Load or create build state
|
|
56
|
+
* 2. Get next pending task
|
|
57
|
+
* 3. Execute task
|
|
58
|
+
* 4. Save checkpoint
|
|
59
|
+
* 5. If task failed and not exhausted, retry
|
|
60
|
+
* 6. If task exhausted, mark as failed and continue
|
|
61
|
+
* 7. Repeat until all tasks complete or global timeout
|
|
62
|
+
*
|
|
63
|
+
* @param {BuildLoopConfig} config
|
|
64
|
+
* @returns {Promise<BuildLoopResult>}
|
|
65
|
+
*/
|
|
66
|
+
export async function runBuildLoop(config) {
|
|
67
|
+
const { projectDir, taskIds, executor, onProgress, resume = false } = config;
|
|
68
|
+
|
|
69
|
+
// Load or create state
|
|
70
|
+
let state = resume ? loadBuildState(projectDir) : null;
|
|
71
|
+
|
|
72
|
+
if (!state || state.status === BuildStatus.COMPLETED || state.status === BuildStatus.ABANDONED) {
|
|
73
|
+
state = createBuildState(taskIds);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
state = startBuild(state);
|
|
77
|
+
saveBuildState(projectDir, state);
|
|
78
|
+
|
|
79
|
+
const startTime = Date.now();
|
|
80
|
+
|
|
81
|
+
// Main loop
|
|
82
|
+
while (true) {
|
|
83
|
+
// Check global timeout
|
|
84
|
+
if (isTimedOut(state)) {
|
|
85
|
+
state = failBuild(state, 'Global timeout exceeded');
|
|
86
|
+
saveBuildState(projectDir, state);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Get next task
|
|
91
|
+
const checkpoint = getNextPendingTask(state);
|
|
92
|
+
if (!checkpoint) {
|
|
93
|
+
// All tasks processed
|
|
94
|
+
const hasFailures = state.checkpoints.some((c) => c.status === CheckpointStatus.FAILED);
|
|
95
|
+
if (hasFailures) {
|
|
96
|
+
state = failBuild(state, 'Some tasks failed');
|
|
97
|
+
} else {
|
|
98
|
+
state = completeBuild(state);
|
|
99
|
+
}
|
|
100
|
+
saveBuildState(projectDir, state);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if task is exhausted
|
|
105
|
+
if (isTaskExhausted(checkpoint)) {
|
|
106
|
+
state = updateCheckpoint(state, checkpoint.taskId, {
|
|
107
|
+
status: CheckpointStatus.FAILED,
|
|
108
|
+
error: `Exceeded max iterations (${checkpoint.attempts})`,
|
|
109
|
+
});
|
|
110
|
+
saveBuildState(projectDir, state);
|
|
111
|
+
|
|
112
|
+
if (onProgress) {
|
|
113
|
+
onProgress({ type: 'task_exhausted', taskId: checkpoint.taskId, attempts: checkpoint.attempts });
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Mark task as in progress
|
|
119
|
+
state = updateCheckpoint(state, checkpoint.taskId, {
|
|
120
|
+
status: CheckpointStatus.IN_PROGRESS,
|
|
121
|
+
attempts: checkpoint.attempts + 1,
|
|
122
|
+
lastAttempt: new Date().toISOString(),
|
|
123
|
+
});
|
|
124
|
+
saveBuildState(projectDir, state);
|
|
125
|
+
|
|
126
|
+
if (onProgress) {
|
|
127
|
+
const progress = getProgress(state);
|
|
128
|
+
onProgress({ type: 'task_started', taskId: checkpoint.taskId, attempt: checkpoint.attempts + 1, progress });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Execute task
|
|
132
|
+
try {
|
|
133
|
+
const result = await executor(checkpoint.taskId);
|
|
134
|
+
|
|
135
|
+
if (result.success) {
|
|
136
|
+
state = updateCheckpoint(state, checkpoint.taskId, {
|
|
137
|
+
status: CheckpointStatus.COMPLETED,
|
|
138
|
+
output: result.output?.slice(0, 1000) || 'Completed',
|
|
139
|
+
error: null,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (onProgress) {
|
|
143
|
+
onProgress({ type: 'task_completed', taskId: checkpoint.taskId });
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
state = updateCheckpoint(state, checkpoint.taskId, {
|
|
147
|
+
status: CheckpointStatus.IN_PROGRESS, // Will retry
|
|
148
|
+
error: result.output?.slice(0, 500) || 'Task failed',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (onProgress) {
|
|
152
|
+
onProgress({ type: 'task_failed', taskId: checkpoint.taskId, attempt: checkpoint.attempts + 1, error: result.output });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
state = updateCheckpoint(state, checkpoint.taskId, {
|
|
157
|
+
status: CheckpointStatus.IN_PROGRESS, // Will retry
|
|
158
|
+
error: err.message?.slice(0, 500) || 'Execution error',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
saveBuildState(projectDir, state);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const duration = Date.now() - startTime;
|
|
166
|
+
const progress = getProgress(state);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
status: state.status,
|
|
170
|
+
completed: progress.completed,
|
|
171
|
+
failed: progress.failed,
|
|
172
|
+
totalAttempts: state.totalAttempts,
|
|
173
|
+
duration: `${Math.round(duration / 1000)}s`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get current build loop status without executing.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} projectDir
|
|
181
|
+
* @returns {object|null}
|
|
182
|
+
*/
|
|
183
|
+
export function getBuildStatus(projectDir) {
|
|
184
|
+
const state = loadBuildState(projectDir);
|
|
185
|
+
if (!state) return null;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
sessionId: state.sessionId,
|
|
189
|
+
status: state.status,
|
|
190
|
+
progress: getProgress(state),
|
|
191
|
+
startedAt: state.startedAt,
|
|
192
|
+
lastCheckpoint: state.lastCheckpoint,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Build state manager with checkpoint-based persistence.
|
|
3
|
+
*
|
|
4
|
+
* Manages the state of autonomous build execution, including
|
|
5
|
+
* checkpoints saved after each task completion, abandoned build
|
|
6
|
+
* detection, and attempt logging.
|
|
7
|
+
*
|
|
8
|
+
* Constitution Article XVII — Execution Mode Governance.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Constants
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** Maximum iterations per individual task before giving up */
|
|
19
|
+
export const MAX_ITERATIONS_PER_TASK = 10;
|
|
20
|
+
|
|
21
|
+
/** Global timeout for an autonomous build session (30 minutes) */
|
|
22
|
+
export const GLOBAL_TIMEOUT_MS = 30 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
/** Time after which a build is considered abandoned (1 hour) */
|
|
25
|
+
export const ABANDONED_THRESHOLD_MS = 60 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build status enum.
|
|
29
|
+
* @enum {string}
|
|
30
|
+
*/
|
|
31
|
+
export const BuildStatus = {
|
|
32
|
+
PENDING: 'pending',
|
|
33
|
+
IN_PROGRESS: 'in_progress',
|
|
34
|
+
PAUSED: 'paused',
|
|
35
|
+
ABANDONED: 'abandoned',
|
|
36
|
+
FAILED: 'failed',
|
|
37
|
+
COMPLETED: 'completed',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Task checkpoint status.
|
|
42
|
+
* @enum {string}
|
|
43
|
+
*/
|
|
44
|
+
export const CheckpointStatus = {
|
|
45
|
+
PENDING: 'pending',
|
|
46
|
+
IN_PROGRESS: 'in_progress',
|
|
47
|
+
COMPLETED: 'completed',
|
|
48
|
+
FAILED: 'failed',
|
|
49
|
+
SKIPPED: 'skipped',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// State Schema
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {object} TaskCheckpoint
|
|
58
|
+
* @property {string} taskId
|
|
59
|
+
* @property {string} status - CheckpointStatus
|
|
60
|
+
* @property {number} attempts - Number of execution attempts
|
|
61
|
+
* @property {string|null} lastAttempt - ISO timestamp of last attempt
|
|
62
|
+
* @property {string|null} output - Last output summary
|
|
63
|
+
* @property {string|null} error - Last error message (if failed)
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {object} BuildState
|
|
68
|
+
* @property {string} sessionId - Build session identifier
|
|
69
|
+
* @property {string} status - BuildStatus
|
|
70
|
+
* @property {TaskCheckpoint[]} checkpoints - Per-task checkpoints
|
|
71
|
+
* @property {string} startedAt - ISO timestamp
|
|
72
|
+
* @property {string|null} lastCheckpoint - ISO timestamp of last checkpoint
|
|
73
|
+
* @property {string|null} completedAt - ISO timestamp of completion
|
|
74
|
+
* @property {number} totalAttempts - Total execution attempts across all tasks
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
const BUILD_STATE_FILE = '.chati/build-state.json';
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// State Management
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Initialize a new build state.
|
|
85
|
+
*
|
|
86
|
+
* @param {string[]} taskIds - Array of task IDs to execute
|
|
87
|
+
* @returns {BuildState}
|
|
88
|
+
*/
|
|
89
|
+
export function createBuildState(taskIds) {
|
|
90
|
+
return {
|
|
91
|
+
sessionId: `build-${Date.now()}`,
|
|
92
|
+
status: BuildStatus.PENDING,
|
|
93
|
+
checkpoints: taskIds.map((taskId) => ({
|
|
94
|
+
taskId,
|
|
95
|
+
status: CheckpointStatus.PENDING,
|
|
96
|
+
attempts: 0,
|
|
97
|
+
lastAttempt: null,
|
|
98
|
+
output: null,
|
|
99
|
+
error: null,
|
|
100
|
+
})),
|
|
101
|
+
startedAt: new Date().toISOString(),
|
|
102
|
+
lastCheckpoint: null,
|
|
103
|
+
completedAt: null,
|
|
104
|
+
totalAttempts: 0,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Load build state from disk.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} projectDir
|
|
112
|
+
* @returns {BuildState|null}
|
|
113
|
+
*/
|
|
114
|
+
export function loadBuildState(projectDir) {
|
|
115
|
+
const statePath = join(projectDir, BUILD_STATE_FILE);
|
|
116
|
+
if (!existsSync(statePath)) return null;
|
|
117
|
+
try {
|
|
118
|
+
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
119
|
+
|
|
120
|
+
// Check for abandoned builds
|
|
121
|
+
if (state.status === BuildStatus.IN_PROGRESS && state.lastCheckpoint) {
|
|
122
|
+
const elapsed = Date.now() - new Date(state.lastCheckpoint).getTime();
|
|
123
|
+
if (elapsed > ABANDONED_THRESHOLD_MS) {
|
|
124
|
+
state.status = BuildStatus.ABANDONED;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return state;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Save build state to disk.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} projectDir
|
|
138
|
+
* @param {BuildState} state
|
|
139
|
+
*/
|
|
140
|
+
export function saveBuildState(projectDir, state) {
|
|
141
|
+
const statePath = join(projectDir, BUILD_STATE_FILE);
|
|
142
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
143
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Update a task checkpoint.
|
|
148
|
+
*
|
|
149
|
+
* @param {BuildState} state
|
|
150
|
+
* @param {string} taskId
|
|
151
|
+
* @param {Partial<TaskCheckpoint>} update
|
|
152
|
+
* @returns {BuildState}
|
|
153
|
+
*/
|
|
154
|
+
export function updateCheckpoint(state, taskId, update) {
|
|
155
|
+
const checkpoint = state.checkpoints.find((c) => c.taskId === taskId);
|
|
156
|
+
if (!checkpoint) {
|
|
157
|
+
throw new Error(`Task "${taskId}" not found in build state`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Object.assign(checkpoint, update);
|
|
161
|
+
state.lastCheckpoint = new Date().toISOString();
|
|
162
|
+
|
|
163
|
+
if (update.attempts !== undefined) {
|
|
164
|
+
state.totalAttempts = state.checkpoints.reduce((sum, c) => sum + c.attempts, 0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return state;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Mark the build as started.
|
|
172
|
+
*
|
|
173
|
+
* @param {BuildState} state
|
|
174
|
+
* @returns {BuildState}
|
|
175
|
+
*/
|
|
176
|
+
export function startBuild(state) {
|
|
177
|
+
state.status = BuildStatus.IN_PROGRESS;
|
|
178
|
+
state.startedAt = new Date().toISOString();
|
|
179
|
+
return state;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Mark the build as completed.
|
|
184
|
+
*
|
|
185
|
+
* @param {BuildState} state
|
|
186
|
+
* @returns {BuildState}
|
|
187
|
+
*/
|
|
188
|
+
export function completeBuild(state) {
|
|
189
|
+
state.status = BuildStatus.COMPLETED;
|
|
190
|
+
state.completedAt = new Date().toISOString();
|
|
191
|
+
return state;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Mark the build as failed.
|
|
196
|
+
*
|
|
197
|
+
* @param {BuildState} state
|
|
198
|
+
* @param {string} [reason]
|
|
199
|
+
* @returns {BuildState}
|
|
200
|
+
*/
|
|
201
|
+
export function failBuild(state, reason) {
|
|
202
|
+
state.status = BuildStatus.FAILED;
|
|
203
|
+
state.completedAt = new Date().toISOString();
|
|
204
|
+
return state;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get the next pending task checkpoint.
|
|
209
|
+
*
|
|
210
|
+
* @param {BuildState} state
|
|
211
|
+
* @returns {TaskCheckpoint|null}
|
|
212
|
+
*/
|
|
213
|
+
export function getNextPendingTask(state) {
|
|
214
|
+
return state.checkpoints.find(
|
|
215
|
+
(c) => c.status === CheckpointStatus.PENDING || c.status === CheckpointStatus.IN_PROGRESS,
|
|
216
|
+
) || null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Check if a task has exceeded max iterations.
|
|
221
|
+
*
|
|
222
|
+
* @param {TaskCheckpoint} checkpoint
|
|
223
|
+
* @returns {boolean}
|
|
224
|
+
*/
|
|
225
|
+
export function isTaskExhausted(checkpoint) {
|
|
226
|
+
return checkpoint.attempts >= MAX_ITERATIONS_PER_TASK;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if the global timeout has been exceeded.
|
|
231
|
+
*
|
|
232
|
+
* @param {BuildState} state
|
|
233
|
+
* @returns {boolean}
|
|
234
|
+
*/
|
|
235
|
+
export function isTimedOut(state) {
|
|
236
|
+
const elapsed = Date.now() - new Date(state.startedAt).getTime();
|
|
237
|
+
return elapsed > GLOBAL_TIMEOUT_MS;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get build progress summary.
|
|
242
|
+
*
|
|
243
|
+
* @param {BuildState} state
|
|
244
|
+
* @returns {{ total: number, completed: number, failed: number, pending: number, progress: number }}
|
|
245
|
+
*/
|
|
246
|
+
export function getProgress(state) {
|
|
247
|
+
const total = state.checkpoints.length;
|
|
248
|
+
const completed = state.checkpoints.filter((c) => c.status === CheckpointStatus.COMPLETED).length;
|
|
249
|
+
const failed = state.checkpoints.filter((c) => c.status === CheckpointStatus.FAILED).length;
|
|
250
|
+
const pending = state.checkpoints.filter(
|
|
251
|
+
(c) => c.status === CheckpointStatus.PENDING || c.status === CheckpointStatus.IN_PROGRESS,
|
|
252
|
+
).length;
|
|
253
|
+
const progress = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
254
|
+
|
|
255
|
+
return { total, completed, failed, pending, progress };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Clear build state (after completion or manual reset).
|
|
260
|
+
*
|
|
261
|
+
* @param {string} projectDir
|
|
262
|
+
*/
|
|
263
|
+
export function clearBuildState(projectDir) {
|
|
264
|
+
const statePath = join(projectDir, BUILD_STATE_FILE);
|
|
265
|
+
if (existsSync(statePath)) {
|
|
266
|
+
const { unlinkSync } = require('fs');
|
|
267
|
+
unlinkSync(statePath);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Execution profile manager.
|
|
3
|
+
*
|
|
4
|
+
* Manages 3 execution profiles that govern confirmation requirements:
|
|
5
|
+
* - explore: read-only, no writes
|
|
6
|
+
* - guided: confirm before writes (default)
|
|
7
|
+
* - autonomous: full autonomy with quality gates
|
|
8
|
+
*
|
|
9
|
+
* Profiles are orthogonal to mode governance (Article XI).
|
|
10
|
+
* Modes control WHERE; profiles control WHETHER.
|
|
11
|
+
*
|
|
12
|
+
* Constitution Article XVIII — Execution Profile Governance.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Profile Definitions
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Execution profile enum.
|
|
24
|
+
* @enum {string}
|
|
25
|
+
*/
|
|
26
|
+
export const Profile = {
|
|
27
|
+
/** Read-only. Agents analyze and suggest but perform no writes. */
|
|
28
|
+
EXPLORE: 'explore',
|
|
29
|
+
/** Default. Agents propose actions and wait for user confirmation. */
|
|
30
|
+
GUIDED: 'guided',
|
|
31
|
+
/** Full autonomy. Agents execute without confirmation when gates pass. */
|
|
32
|
+
AUTONOMOUS: 'autonomous',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Minimum gate score required to activate autonomous profile */
|
|
36
|
+
export const AUTONOMOUS_GATE_THRESHOLD = 90;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Operations that ALWAYS require confirmation regardless of profile.
|
|
40
|
+
* Constitution Article XVIII, point 4.
|
|
41
|
+
*/
|
|
42
|
+
export const ALWAYS_CONFIRM_OPERATIONS = [
|
|
43
|
+
'file_delete',
|
|
44
|
+
'database_drop',
|
|
45
|
+
'force_push',
|
|
46
|
+
'deploy_production',
|
|
47
|
+
'deviation_activate',
|
|
48
|
+
'backward_transition',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Profile Management
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the current execution profile from session state.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} projectDir
|
|
59
|
+
* @returns {string} Current profile (defaults to 'guided')
|
|
60
|
+
*/
|
|
61
|
+
export function getCurrentProfile(projectDir) {
|
|
62
|
+
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
63
|
+
if (!existsSync(sessionPath)) return Profile.GUIDED;
|
|
64
|
+
|
|
65
|
+
const raw = readFileSync(sessionPath, 'utf-8');
|
|
66
|
+
const match = raw.match(/^\s*execution_profile:\s*(.+)$/m);
|
|
67
|
+
const profile = match ? match[1].trim().replace(/^["']|["']$/g, '') : Profile.GUIDED;
|
|
68
|
+
|
|
69
|
+
return Object.values(Profile).includes(profile) ? profile : Profile.GUIDED;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if a write operation is allowed under the current profile.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} profile - Current execution profile
|
|
76
|
+
* @param {string} operation - Operation type (e.g., 'file_write', 'file_delete')
|
|
77
|
+
* @returns {{ allowed: boolean, reason: string }}
|
|
78
|
+
*/
|
|
79
|
+
export function isWriteAllowed(profile, operation) {
|
|
80
|
+
// Explore profile blocks ALL writes
|
|
81
|
+
if (profile === Profile.EXPLORE) {
|
|
82
|
+
return { allowed: false, reason: '[Article XVIII] Explore profile: read-only mode. No writes permitted.' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Always-confirm operations need user approval regardless of profile
|
|
86
|
+
if (ALWAYS_CONFIRM_OPERATIONS.includes(operation)) {
|
|
87
|
+
return { allowed: false, reason: `[Article XVIII] Operation "${operation}" always requires user confirmation.` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Guided profile: allowed but requires confirmation (caller handles confirmation)
|
|
91
|
+
if (profile === Profile.GUIDED) {
|
|
92
|
+
return { allowed: true, reason: 'Guided profile: confirmation required.' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Autonomous profile: allowed without confirmation
|
|
96
|
+
if (profile === Profile.AUTONOMOUS) {
|
|
97
|
+
return { allowed: true, reason: 'Autonomous profile: proceeding without confirmation.' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { allowed: true, reason: 'Default: allowed.' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate whether autonomous profile can be activated.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} projectDir
|
|
107
|
+
* @returns {{ canActivate: boolean, reason: string, currentScore: number }}
|
|
108
|
+
*/
|
|
109
|
+
export function canActivateAutonomous(projectDir) {
|
|
110
|
+
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
111
|
+
if (!existsSync(sessionPath)) {
|
|
112
|
+
return { canActivate: false, reason: 'No active session', currentScore: 0 };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const raw = readFileSync(sessionPath, 'utf-8');
|
|
116
|
+
|
|
117
|
+
// Look for most recent QA gate score
|
|
118
|
+
const scoreMatches = raw.match(/score:\s*([\d.]+)/g);
|
|
119
|
+
if (!scoreMatches || scoreMatches.length === 0) {
|
|
120
|
+
return { canActivate: false, reason: 'No gate scores found. Run QA gates first.', currentScore: 0 };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const scores = scoreMatches.map((m) => parseFloat(m.replace('score:', '').trim()));
|
|
124
|
+
const latestScore = scores[scores.length - 1];
|
|
125
|
+
|
|
126
|
+
if (latestScore < AUTONOMOUS_GATE_THRESHOLD) {
|
|
127
|
+
return {
|
|
128
|
+
canActivate: false,
|
|
129
|
+
reason: `Gate score ${latestScore}% < required ${AUTONOMOUS_GATE_THRESHOLD}%`,
|
|
130
|
+
currentScore: latestScore,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { canActivate: true, reason: 'Gate scores meet threshold', currentScore: latestScore };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if autonomous profile should be downgraded due to quality drop.
|
|
139
|
+
*
|
|
140
|
+
* @param {number} currentScore - Current quality score
|
|
141
|
+
* @returns {{ shouldDowngrade: boolean, reason: string }}
|
|
142
|
+
*/
|
|
143
|
+
export function shouldDowngradeAutonomous(currentScore) {
|
|
144
|
+
if (currentScore < AUTONOMOUS_GATE_THRESHOLD) {
|
|
145
|
+
return {
|
|
146
|
+
shouldDowngrade: true,
|
|
147
|
+
reason: `Quality dropped to ${currentScore}% (< ${AUTONOMOUS_GATE_THRESHOLD}%). Auto-downgrading to guided profile.`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { shouldDowngrade: false, reason: '' };
|
|
151
|
+
}
|