@thebackstoryis/engineering-with-ai 0.3.1-beta.0 → 0.3.1
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/Docs/context-management-and-token-efficiency.md +1 -1
- package/Docs/maintainers/contributing.md +8 -0
- package/Docs/operations/dashboard-configuration.md +22 -0
- package/Docs/operations/installation-updating-and-entitlements.md +40 -2
- package/Docs/reference/capabilities-and-project-layout.md +11 -6
- package/Docs/reference/cli-and-configuration.md +32 -0
- package/README.md +5 -10
- package/config/autonomy.schema.json +76 -11
- package/config/project.schema.json +29 -1
- package/package.json +3 -2
- package/public/autonomy.js +10 -4
- package/public/coding-providers.js +72 -0
- package/public/grok-credentials.js +42 -0
- package/public/index.html +36 -1
- package/public/styles.css +25 -0
- package/skills-src/ewai-context/SKILL.md +1 -0
- package/skills-src/ewai-deliver/SKILL.md +3 -1
- package/skills-src/ewai-pipeline/SKILL.md +1 -0
- package/src/autonomy.mjs +1 -1
- package/src/cli.mjs +69 -9
- package/src/coding-providers.mjs +81 -0
- package/src/companion.mjs +19 -4
- package/src/config-document.mjs +49 -0
- package/src/dashboard-preferences.mjs +7 -48
- package/src/delivery.mjs +8 -2
- package/src/discovery.mjs +5 -4
- package/src/grok-credentials.mjs +95 -0
- package/src/install.mjs +10 -4
- package/src/project.mjs +10 -2
- package/src/runtime/afk-conductor.mjs +84 -18
- package/src/runtime/autonomy-supervisor.mjs +23 -5
- package/src/runtime/autonomy-workers.mjs +11 -2
- package/src/runtime/dashboard-actions.mjs +1 -1
- package/src/runtime/dashboard-server.mjs +29 -0
- package/src/runtime/grok-provider.mjs +269 -0
- package/src/runtime/mcp-config.mjs +34 -2
- package/src/runtime/mcp-server.mjs +6 -6
- package/src/runtime/provider-adapters.mjs +207 -31
- package/src/validation-config.mjs +18 -7
- package/templates/SPECS/pipeline.yaml +3 -0
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {dirname,relative,resolve,isAbsolute} from 'node:path';
|
|
3
|
-
import {createHash,randomUUID} from 'node:crypto';
|
|
4
|
-
import YAML from 'yaml';
|
|
5
|
-
import {projectPaths} from './paths.mjs';
|
|
1
|
+
import {readProjectDocument,editProjectDocument} from './config-document.mjs';
|
|
6
2
|
|
|
7
3
|
export const DASHBOARD_VIEWS=Object.freeze([
|
|
8
4
|
['portfolio','Portfolio','Compare work and decisions across several projects.','Across projects and teams'],
|
|
@@ -27,54 +23,17 @@ export function normaliseDashboardPreferences(value={}){
|
|
|
27
23
|
return {sidebar_collapsed:value.sidebar_collapsed??false,optional_views:Object.fromEntries(DASHBOARD_VIEWS.map(v=>[v.id,optional[v.id]??false]))};
|
|
28
24
|
}
|
|
29
25
|
export function premiumPersonasActive(premium){return premium?.access==='available'&&premium?.installed===true&&premium?.verified===true;}
|
|
30
|
-
const
|
|
31
|
-
function
|
|
32
|
-
const root=realpathSync(paths.projectRoot),parent=dirname(paths.configPath),rel=relative(root,realpathSync(parent));
|
|
33
|
-
if(rel.startsWith('..')||isAbsolute(rel))fail('Dashboard preferences require a safe project configuration path.',403);
|
|
34
|
-
let cursor=parent;
|
|
35
|
-
while(cursor!==paths.projectRoot){if(lstatSync(cursor).isSymbolicLink())fail('Dashboard preferences cannot use a symbolic configuration directory.',403);const next=dirname(cursor);if(next===cursor)fail('Dashboard preferences require a safe project configuration path.',403);cursor=next;}
|
|
36
|
-
const stat=lstatSync(paths.configPath);
|
|
37
|
-
if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1)fail('Dashboard preferences require a regular, non-symbolic configuration file.',403);
|
|
38
|
-
if(stat.size>2*1024*1024)fail('The project configuration is too large for dashboard preference editing.',413);
|
|
39
|
-
return stat;
|
|
40
|
-
}
|
|
41
|
-
function snapshot(paths){
|
|
42
|
-
const stat=safePath(paths);let fd;
|
|
43
|
-
try{
|
|
44
|
-
fd=openSync(paths.configPath,constants.O_RDONLY|constants.O_NOFOLLOW);
|
|
45
|
-
const opened=fstatSync(fd);if(opened.ino!==stat.ino||opened.dev!==stat.dev)fail('Project configuration changed. Reload the settings and try again.',409);
|
|
46
|
-
const bytes=readFileSync(fd);if(bytes.length>2*1024*1024)fail('The project configuration is too large for dashboard preference editing.',413);
|
|
47
|
-
const document=YAML.parseDocument(bytes.toString('utf8'));
|
|
48
|
-
if(document.errors.length)fail('The project configuration contains invalid YAML. Fix it before editing dashboard preferences.',422);
|
|
49
|
-
let config;try{config=document.toJS({maxAliasCount:100});}catch{fail('The project configuration cannot be safely read.',422);}
|
|
50
|
-
if(config?.schema!=='ewai.project/v1'||config?.specs?.root!==paths.specsRelative)fail('The project configuration and SPECS locator must agree before editing dashboard preferences.',422);
|
|
51
|
-
return {document,preferences:normaliseDashboardPreferences(config.dashboard),digest:digest(bytes),mode:stat.mode&0o777};
|
|
52
|
-
}finally{if(fd!==undefined)closeSync(fd);}
|
|
53
|
-
}
|
|
54
|
-
function guarded(operation){try{return operation();}catch(error){if(error.dashboardPreferenceError)throw error;fail('Dashboard preferences could not be read or saved. Check the project configuration and file permissions.',409);}}
|
|
26
|
+
function snapshot(projectRoot){const state=readProjectDocument(projectRoot);return {...state,preferences:normaliseDashboardPreferences(state.config.dashboard)};}
|
|
27
|
+
function guarded(operation){try{return operation();}catch(error){if(error.dashboardPreferenceError||error.configurationError)throw error;fail('Dashboard preferences could not be read or saved. Check project configuration and file permissions.',409);}}
|
|
55
28
|
function projection(state){return {schema:'ewai.dashboard-preferences/v1',digest:state.digest,preferences:state.preferences,views:DASHBOARD_VIEWS};}
|
|
56
|
-
export function readDashboardPreferences(projectRoot){return guarded(()=>projection(snapshot(
|
|
29
|
+
export function readDashboardPreferences(projectRoot){return guarded(()=>projection(snapshot(projectRoot)));}
|
|
57
30
|
export function saveDashboardPreferences(projectRoot,input){
|
|
58
31
|
return guarded(()=>{
|
|
59
32
|
keys(input,['confirmed','expectedDigest','preferences']);
|
|
60
33
|
if(input.confirmed!==true)fail('Confirm the dashboard preference changes before saving.');
|
|
61
|
-
if(!/^sha256:[a-f0-9]{64}$/.test(String(input.expectedDigest??'')))fail('Reload dashboard preferences before saving.');
|
|
62
34
|
if(!record(input.preferences))fail('Provide the dashboard preferences to save.');
|
|
63
|
-
const preferences=normaliseDashboardPreferences(input.preferences)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
try{lockFd=openSync(lock,'wx',0o600);}catch{fail('Dashboard preferences are being edited. Retry after that save finishes.',409);}
|
|
67
|
-
const before=snapshot(paths);if(before.digest!==input.expectedDigest)fail('Project configuration changed. Reload the settings and try again.',409);
|
|
68
|
-
before.document.set('dashboard',preferences);const content=before.document.toString();
|
|
69
|
-
temporary=paths.configPath+'.dashboard-'+randomUUID()+'.tmp';tempFd=openSync(temporary,'wx',before.mode);
|
|
70
|
-
writeFileSync(tempFd,content,'utf8');fsyncSync(tempFd);closeSync(tempFd);tempFd=undefined;
|
|
71
|
-
if(snapshot(paths).digest!==before.digest)fail('Project configuration changed. Reload the settings and try again.',409);
|
|
72
|
-
safePath(paths);renameSync(temporary,paths.configPath);temporary=undefined;
|
|
73
|
-
return projection(snapshot(paths));
|
|
74
|
-
}finally{
|
|
75
|
-
if(tempFd!==undefined)closeSync(tempFd);
|
|
76
|
-
if(temporary)unlinkSync(temporary);
|
|
77
|
-
if(lockFd!==undefined){closeSync(lockFd);unlinkSync(lock);}
|
|
78
|
-
}
|
|
35
|
+
const preferences=normaliseDashboardPreferences(input.preferences);
|
|
36
|
+
const state=editProjectDocument(projectRoot,input.expectedDigest,document=>document.set('dashboard',preferences));
|
|
37
|
+
return projection({...state,preferences:normaliseDashboardPreferences(state.config.dashboard)});
|
|
79
38
|
});
|
|
80
39
|
}
|
package/src/delivery.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { dirname, relative, resolve } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import YAML from 'yaml';
|
|
13
|
+
import {codingPolicyDigest,assertCodingPolicyFresh,assertModelPolicy} from './coding-providers.mjs';
|
|
13
14
|
import { updateIntentDeliveryState, validateIntentStateCopies } from './intents.mjs';
|
|
14
15
|
import { loadProjectConfig } from './project.mjs';
|
|
15
16
|
import { readRuntimeIntent } from './runtime/intents.mjs';
|
|
@@ -38,7 +39,7 @@ import { validatePhaseArtefacts } from './delivery-artifacts.mjs';
|
|
|
38
39
|
import {
|
|
39
40
|
canonicalValidationProvider,
|
|
40
41
|
effectiveValidationConfig,
|
|
41
|
-
|
|
42
|
+
SUPPORTED_VALIDATION_PROVIDERS,
|
|
42
43
|
validationCheckpointForPhase,
|
|
43
44
|
} from './validation-config.mjs';
|
|
44
45
|
import {
|
|
@@ -431,7 +432,7 @@ function normaliseDeliveryValidation(paths, state) {
|
|
|
431
432
|
const legacyProviders = [...new Set(
|
|
432
433
|
(Array.isArray(state.providers) ? state.providers : [])
|
|
433
434
|
.map(canonicalValidationProvider)
|
|
434
|
-
.filter((provider) =>
|
|
435
|
+
.filter((provider) => SUPPORTED_VALIDATION_PROVIDERS.includes(provider)),
|
|
435
436
|
)];
|
|
436
437
|
|
|
437
438
|
for (const [checkpointName, checkpoint] of Object.entries(validation.checkpoints)) {
|
|
@@ -638,6 +639,10 @@ function recordOwnedExternalValidationCycle(projectRoot, slug, phaseId, input) {
|
|
|
638
639
|
const phase = findTrackedPhase(state, phaseId);
|
|
639
640
|
if (phase.status !== 'running') throw new Error(`Phase ${phaseId} is not running.`);
|
|
640
641
|
if (!phase.validation) throw new Error(`Phase ${phaseId} is not an external-validation checkpoint.`);
|
|
642
|
+
if(state.validation?.codingPolicyDigest){
|
|
643
|
+
const {config}=loadProjectConfig(projectRoot);
|
|
644
|
+
assertCodingPolicyFresh(state.validation.codingPolicyDigest,codingPolicyDigest(config.coding_providers));
|
|
645
|
+
}
|
|
641
646
|
|
|
642
647
|
const provider = String(input.provider ?? '').trim().toLowerCase();
|
|
643
648
|
if (!phase.validation.providers.includes(provider)) {
|
|
@@ -646,6 +651,7 @@ function recordOwnedExternalValidationCycle(projectRoot, slug, phaseId, input) {
|
|
|
646
651
|
);
|
|
647
652
|
}
|
|
648
653
|
const outcome = String(input.outcome ?? '').trim().toLowerCase();
|
|
654
|
+
assertModelPolicy(provider,state.validation?.codingPolicy);
|
|
649
655
|
if (!['pass', 'issues'].includes(outcome)) {
|
|
650
656
|
throw new Error('Validation cycle outcome must be pass or issues.');
|
|
651
657
|
}
|
package/src/discovery.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { loadProjectConfig } from './project.mjs';
|
|
|
20
20
|
import {
|
|
21
21
|
VALIDATION_CHECKPOINTS,
|
|
22
22
|
VALIDATION_PROVIDERS,
|
|
23
|
+
SUPPORTED_VALIDATION_PROVIDERS,
|
|
23
24
|
normaliseValidationConfig,
|
|
24
25
|
} from './validation-config.mjs';
|
|
25
26
|
import { publishLifecycleEventSafely } from './runtime/lifecycle-hooks.mjs';
|
|
@@ -218,7 +219,7 @@ function normaliseOrganisationBlueprint(value) {
|
|
|
218
219
|
|
|
219
220
|
function discoveryValidation(raw, configured) {
|
|
220
221
|
const rawProviders = raw.providers ?? Object.fromEntries(
|
|
221
|
-
|
|
222
|
+
SUPPORTED_VALIDATION_PROVIDERS
|
|
222
223
|
.filter((provider) => raw[provider] !== undefined)
|
|
223
224
|
.map((provider) => [provider, raw[provider]]),
|
|
224
225
|
);
|
|
@@ -852,7 +853,7 @@ function renderDiscoveryEvidence(answers, detected, standards, compliance, compl
|
|
|
852
853
|
['AI features', answers.assurance.aiFeatures],
|
|
853
854
|
['Accessibility obligations', answers.assurance.accessibility],
|
|
854
855
|
['Availability', answers.assurance.availability],
|
|
855
|
-
...
|
|
856
|
+
...Object.keys(answers.validation.external.providers).map((provider) => [
|
|
856
857
|
`${provider[0].toUpperCase()}${provider.slice(1)} external validation`,
|
|
857
858
|
`${answers.validation.external.providers[provider].state}; ${
|
|
858
859
|
answers.validation.external.providers[provider].enabled ? 'enabled' : 'disabled'
|
|
@@ -879,7 +880,7 @@ function renderProjectConstraints(answers, completedAt) {
|
|
|
879
880
|
}
|
|
880
881
|
|
|
881
882
|
function renderApproach(answers, completedAt) {
|
|
882
|
-
const providerLines =
|
|
883
|
+
const providerLines = Object.keys(answers.validation.external.providers).map((provider) => {
|
|
883
884
|
const setting = answers.validation.external.providers[provider];
|
|
884
885
|
return `- **${provider[0].toUpperCase()}${provider.slice(1)}:** ${setting.state}; ${setting.enabled ? 'enabled' : 'disabled'}`;
|
|
885
886
|
}).join('\n');
|
|
@@ -1250,7 +1251,7 @@ export async function runDiscoveryInterview(projectRoot, io = {}) {
|
|
|
1250
1251
|
const availability = await askEnum(rl, 'Availability expectation', [...AVAILABILITY_LEVELS], 'standard');
|
|
1251
1252
|
const configuredProviders = config.validation?.external?.providers ?? {};
|
|
1252
1253
|
const validationProviders = {};
|
|
1253
|
-
for (const provider of VALIDATION_PROVIDERS) {
|
|
1254
|
+
for (const provider of [...VALIDATION_PROVIDERS,...(configuredProviders.grok?['grok']:[])]) {
|
|
1254
1255
|
const label = `${provider[0].toUpperCase()}${provider.slice(1)}`;
|
|
1255
1256
|
validationProviders[provider] = await askEnum(
|
|
1256
1257
|
rl,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {constants,openSync,closeSync,fstatSync,lstatSync,realpathSync,readFileSync,mkdirSync,writeFileSync,renameSync,unlinkSync,rmSync} from 'node:fs';
|
|
2
|
+
import {homedir} from 'node:os';
|
|
3
|
+
import {resolve,relative,isAbsolute} from 'node:path';
|
|
4
|
+
import {randomUUID,createHash} from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const messages={
|
|
7
|
+
'grok-credential-invalid':'Provide a valid xAI API key using the private setup form or hidden terminal prompt.',
|
|
8
|
+
'grok-credential-input':'Use the supported credential action and confirm the change.',
|
|
9
|
+
'grok-credential-unsafe-storage':'Private credential storage is unsafe or unavailable. Repair its permissions or use XAI_API_KEY in the launching environment.',
|
|
10
|
+
'grok-credential-invalid-storage':'The saved credential could not be read safely. Repair the private credential file before continuing.',
|
|
11
|
+
'grok-credential-stale':'The saved credential changed. Refresh its status and try again.',
|
|
12
|
+
'grok-credential-busy':'Another credential change is running. Retry after it finishes.',
|
|
13
|
+
'grok-credential-missing':'No Grok key is configured. Use private credential setup or XAI_API_KEY in the launching environment.',
|
|
14
|
+
'grok-key-rejected':'The key was not accepted. Check the key and its API permissions, then try again. The previous saved key was retained.',
|
|
15
|
+
'grok-check-rate-limited':'xAI limited the authentication check. Wait before trying again. The previous saved key was retained.',
|
|
16
|
+
'grok-check-unavailable':'The authentication check could not complete. Check your connection and try again. The previous saved key was retained.',
|
|
17
|
+
};
|
|
18
|
+
function fail(code,statusCode=400){const error=new Error(messages[code]??messages['grok-credential-input']);error.code=code;error.statusCode=statusCode;throw error;}
|
|
19
|
+
function key(value){if(typeof value!=='string'||value.length>512||!/^xai-[A-Za-z0-9_-]{16,508}$/.test(value.trim()))fail('grok-credential-invalid');return value.trim();}
|
|
20
|
+
function request(input,fields){if(!input||typeof input!=='object'||Array.isArray(input)||Object.keys(input).some(k=>!fields.includes(k))||input.confirmed!==true||typeof input.expectedRevision!=='string'||input.expectedRevision.length>80)fail('grok-credential-input');}
|
|
21
|
+
function stat(path){try{return lstatSync(path);}catch(error){if(error.code==='ENOENT')return null;fail('grok-credential-unsafe-storage');}}
|
|
22
|
+
function owned(s){return s&&!s.isSymbolicLink()&&(process.getuid===undefined||s.uid===process.getuid());}
|
|
23
|
+
function paths(options={},create=false){
|
|
24
|
+
const declaredHome=resolve(options.home??homedir());
|
|
25
|
+
const root=stat(declaredHome);if(!owned(root)||!root.isDirectory()||(root.mode&0o022))fail('grok-credential-unsafe-storage');
|
|
26
|
+
let home,project;try{home=realpathSync(declaredHome);if(options.projectRoot)project=realpathSync(resolve(options.projectRoot));}catch{fail('grok-credential-unsafe-storage');}
|
|
27
|
+
const folder=resolve(home,'.ewai/credentials'),file=resolve(folder,'grok.json');
|
|
28
|
+
if(project){const rel=relative(project,file);if(rel===''||(!isAbsolute(rel)&&rel!=='..'&&!rel.startsWith('../')))fail('grok-credential-unsafe-storage');}
|
|
29
|
+
const canonicalRoot=stat(home);if(!owned(canonicalRoot)||canonicalRoot.ino!==root.ino||canonicalRoot.dev!==root.dev)fail('grok-credential-unsafe-storage');
|
|
30
|
+
for(const path of [resolve(home,'.ewai'),folder]){
|
|
31
|
+
let s=stat(path);if(!s&&create){try{mkdirSync(path,{mode:0o700});}catch(error){if(error.code!=='EEXIST')fail('grok-credential-unsafe-storage');}s=stat(path);}
|
|
32
|
+
if(s&&(!owned(s)||!s.isDirectory()||(s.mode&(path===folder?0o077:0o022))))fail('grok-credential-unsafe-storage');
|
|
33
|
+
}
|
|
34
|
+
return {folder,file,location:JSON.stringify([home,root.dev,root.ino])};
|
|
35
|
+
}
|
|
36
|
+
function stored(options={}){
|
|
37
|
+
const {file,location}=paths(options),before=stat(file);if(!before)return {value:null,signature:'missing',location};
|
|
38
|
+
if(process.platform==='win32'||!owned(before)||!before.isFile()||before.nlink!==1||(before.mode&0o077)||before.size>4096)fail('grok-credential-unsafe-storage');
|
|
39
|
+
let fd;try{
|
|
40
|
+
fd=openSync(file,constants.O_RDONLY|constants.O_NOFOLLOW);const bytes=readFileSync(fd),after=fstatSync(fd),current=stat(file);
|
|
41
|
+
if(bytes.length>4096||!current||before.ino!==after.ino||before.dev!==after.dev||before.mtimeMs!==after.mtimeMs||after.ino!==current.ino||after.mtimeMs!==current.mtimeMs)fail('grok-credential-unsafe-storage');
|
|
42
|
+
let value;try{value=JSON.parse(bytes.toString('utf8'));}catch{fail('grok-credential-invalid-storage');}
|
|
43
|
+
if(value?.schema!=='ewai.grok-credential/v1'||typeof value.revision!=='string'||!/^[a-f0-9-]{36}$/.test(value.revision)||Object.keys(value).some(k=>!['schema','revision','apiKey'].includes(k)))fail('grok-credential-invalid-storage');
|
|
44
|
+
try{key(value.apiKey);}catch{fail('grok-credential-invalid-storage');}
|
|
45
|
+
return {value,location,signature:JSON.stringify([after.dev,after.ino,after.mtimeMs,after.ctimeMs,createHash('sha256').update(bytes).digest('hex')])};
|
|
46
|
+
}catch(error){if(error.code?.startsWith('grok-'))throw error;fail('grok-credential-unsafe-storage');}finally{if(fd!==undefined)closeSync(fd);}
|
|
47
|
+
}
|
|
48
|
+
function environment(options){const env=options.env??process.env;return env.XAI_API_KEY?key(env.XAI_API_KEY):null;}
|
|
49
|
+
export function grokCredentialStatus(options={}){
|
|
50
|
+
const {value}=stored(options),override=environment(options);
|
|
51
|
+
return {schema:'ewai.grok-credential-status/v1',provider:'grok',configured:Boolean(override||value),saved:Boolean(value),source:override?'environment':value?'saved':'missing',revision:value?.revision??'missing',storage:'owner-only-local-file',encrypted:false,storageSupported:process.platform!=='win32',authority:'credentials-only'};
|
|
52
|
+
}
|
|
53
|
+
export function resolveGrokApiKey(options={}){const override=environment(options);return override??stored(options).value?.apiKey??null;}
|
|
54
|
+
function predecessor(input,options,expected){const current=stored(options);if((current.value?.revision??'missing')!==input.expectedRevision||(expected&&(current.signature!==expected.signature||current.location!==expected.location)))fail('grok-credential-stale',409);return current;}
|
|
55
|
+
function mutation(options,operation){
|
|
56
|
+
if(process.platform==='win32')fail('grok-credential-unsafe-storage');
|
|
57
|
+
const {folder}=paths(options,true),lock=resolve(folder,'.grok.lock');
|
|
58
|
+
try{mkdirSync(lock,{mode:0o700});}catch(error){fail(error.code==='EEXIST'?'grok-credential-busy':'grok-credential-unsafe-storage',409);}
|
|
59
|
+
const identity=lstatSync(lock);
|
|
60
|
+
try{return operation();}finally{const current=stat(lock);if(owned(current)&¤t.isDirectory()&¤t.ino===identity.ino)rmSync(lock,{recursive:true});}
|
|
61
|
+
}
|
|
62
|
+
async function authenticate(apiKey,options={}){
|
|
63
|
+
const controller=new AbortController();let timer;
|
|
64
|
+
try{
|
|
65
|
+
const timeout=Math.min(10000,Math.max(10,options.timeoutMs??10000));
|
|
66
|
+
const response=await Promise.race([(options.fetchImpl??fetch)('https://api.x.ai/v1/models',{method:'GET',headers:{authorization:'Bearer '+apiKey},redirect:'manual',signal:controller.signal}),new Promise((_,reject)=>{timer=setTimeout(()=>{controller.abort();reject(Error('deadline'));},timeout);})]);
|
|
67
|
+
// Discard provider bodies, including errors. They are not safe evidence.
|
|
68
|
+
if(response.body)void response.body.cancel().catch(()=>{});
|
|
69
|
+
if(response.status===200)return {status:'verified',code:'grok-authentication-verified',message:'xAI authentication check passed. No output was generated; credit and coding readiness remain unverified.'};
|
|
70
|
+
const code=[401,403].includes(response.status)?'grok-key-rejected':response.status===429?'grok-check-rate-limited':'grok-check-unavailable';return {status:'failed',code,message:messages[code]};
|
|
71
|
+
}catch{return {status:'failed',code:'grok-check-unavailable',message:messages['grok-check-unavailable']};}
|
|
72
|
+
finally{clearTimeout(timer);controller.abort();}
|
|
73
|
+
}
|
|
74
|
+
export async function checkGrokConnection(options={}){
|
|
75
|
+
const apiKey=resolveGrokApiKey(options);
|
|
76
|
+
if(!apiKey)return {...grokCredentialStatus(options),status:'failed',code:'grok-credential-missing',message:messages['grok-credential-missing']};
|
|
77
|
+
return {...grokCredentialStatus(options),...await authenticate(apiKey,options)};
|
|
78
|
+
}
|
|
79
|
+
export async function configureGrokCredentials(input,options={}){
|
|
80
|
+
request(input,['confirmed','expectedRevision','apiKey']);const apiKey=key(input.apiKey);environment(options);const previous=predecessor(input,options);
|
|
81
|
+
const check=await authenticate(apiKey,options);if(check.status!=='verified')fail(check.code);
|
|
82
|
+
mutation(options,()=>{
|
|
83
|
+
predecessor(input,options,previous);const {file}=paths(options,true),temporary=file+'.'+randomUUID()+'.tmp';
|
|
84
|
+
try{writeFileSync(temporary,JSON.stringify({schema:'ewai.grok-credential/v1',revision:randomUUID(),apiKey})+'\n',{flag:'wx',mode:0o600});paths(options);predecessor(input,options,previous);renameSync(temporary,file);}
|
|
85
|
+
catch(error){if(error.code?.startsWith('grok-'))throw error;fail('grok-credential-unsafe-storage');}
|
|
86
|
+
finally{if(stat(temporary))unlinkSync(temporary);}
|
|
87
|
+
});
|
|
88
|
+
return {...grokCredentialStatus(options),check,message:'Grok key checked and saved for your account on this computer. No work started.'};
|
|
89
|
+
}
|
|
90
|
+
export function removeGrokCredentials(input,options={}){
|
|
91
|
+
request(input,['confirmed','expectedRevision']);environment(options);const previous=predecessor(input,options);
|
|
92
|
+
mutation(options,()=>{const current=predecessor(input,options,previous);if(current.value)unlinkSync(paths(options).file);});
|
|
93
|
+
const status=grokCredentialStatus(options);return {...status,message:status.source==='environment'?'Saved key removed. XAI_API_KEY still supplies the active credential.':'Saved Grok key removed. No work started.'};
|
|
94
|
+
}
|
|
95
|
+
export function safeGrokCredentialError(error){const code=Object.hasOwn(messages,error?.code)?error.code:'grok-credential-input';return {error:messages[code],code};}
|
package/src/install.mjs
CHANGED
|
@@ -31,11 +31,12 @@ function availableClaudeAgents() {
|
|
|
31
31
|
.sort();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function selectedHosts(host, home) {
|
|
34
|
+
function selectedHosts(host, home, grokHome) {
|
|
35
35
|
if (host !== 'auto') return [host];
|
|
36
36
|
const hosts = [];
|
|
37
37
|
if (existsSync(resolve(home, '.codex'))) hosts.push('codex');
|
|
38
38
|
if (existsSync(resolve(home, '.claude'))) hosts.push('claude');
|
|
39
|
+
if (existsSync(grokHome)) hosts.push('grok');
|
|
39
40
|
if (
|
|
40
41
|
existsSync(resolve(home, '.gemini/antigravity-cli'))
|
|
41
42
|
|| existsSync(resolve(home, '.gemini/config'))
|
|
@@ -43,13 +44,15 @@ function selectedHosts(host, home) {
|
|
|
43
44
|
return hosts.length ? hosts : ['codex'];
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function skillDestinationRoot({ scope, host, home, projectRoot }) {
|
|
47
|
+
function skillDestinationRoot({ scope, host, home, projectRoot, grokHome }) {
|
|
47
48
|
if (scope === 'project') {
|
|
48
49
|
if (!projectRoot) throw new Error('Project installation requires projectRoot');
|
|
50
|
+
if (host === 'grok') return resolve(projectRoot, '.grok/skills');
|
|
49
51
|
return host === 'claude'
|
|
50
52
|
? resolve(projectRoot, '.claude/skills')
|
|
51
53
|
: resolve(projectRoot, '.agents/skills');
|
|
52
54
|
}
|
|
55
|
+
if (host === 'grok') return resolve(grokHome, 'skills');
|
|
53
56
|
return host === 'claude'
|
|
54
57
|
? resolve(home, '.claude/skills')
|
|
55
58
|
: host === 'antigravity'
|
|
@@ -103,9 +106,11 @@ export function install(options = {}) {
|
|
|
103
106
|
if (!['global', 'project'].includes(scope)) throw new Error(`Unsupported install scope: ${scope}`);
|
|
104
107
|
if (!['copy', 'link'].includes(mode)) throw new Error(`Unsupported install mode: ${mode}`);
|
|
105
108
|
|
|
106
|
-
|
|
109
|
+
// Explicit fixture homes never inherit the real machine's GROK_HOME.
|
|
110
|
+
const grokHome=resolve(options.grokHome ?? (options.home===undefined ? (options.env??process.env).GROK_HOME : undefined) ?? resolve(home,'.grok'));
|
|
111
|
+
const hosts = selectedHosts(options.host ?? 'auto', home, grokHome);
|
|
107
112
|
for (const host of hosts) {
|
|
108
|
-
if (!['codex', 'claude', 'antigravity'].includes(host)) throw new Error(`Unsupported agent host: ${host}`);
|
|
113
|
+
if (!['codex', 'claude', 'antigravity', 'grok'].includes(host)) throw new Error(`Unsupported agent host: ${host}`);
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
const results = [];
|
|
@@ -115,6 +120,7 @@ export function install(options = {}) {
|
|
|
115
120
|
scope,
|
|
116
121
|
host,
|
|
117
122
|
home,
|
|
123
|
+
grokHome,
|
|
118
124
|
projectRoot: options.projectRoot ? resolve(options.projectRoot) : ''
|
|
119
125
|
});
|
|
120
126
|
mkdirSync(destinationRoot, { recursive: true });
|
package/src/project.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { basename, dirname, extname, relative, resolve } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import YAML from 'yaml';
|
|
6
6
|
import {normaliseDashboardPreferences} from './dashboard-preferences.mjs';
|
|
7
|
+
import {normaliseCodingProviders} from './coding-providers.mjs';
|
|
7
8
|
import { projectPaths } from './paths.mjs';
|
|
8
9
|
import { premiumPersonaLearningAction } from './companion-opening.mjs';
|
|
9
10
|
import { initializeRuntime } from './runtime/database.mjs';
|
|
@@ -360,11 +361,11 @@ export function configureValidationCheckpoint(projectRoot, checkpoint, update) {
|
|
|
360
361
|
}
|
|
361
362
|
|
|
362
363
|
export function externalValidationStatus(config, orchestrator = 'manual') {
|
|
363
|
-
const effective =
|
|
364
|
+
const effective = validationStatus(config, orchestrator);
|
|
364
365
|
const selected = new Set(
|
|
365
366
|
Object.values(effective.checkpoints).flatMap((checkpoint) => checkpoint.validators),
|
|
366
367
|
);
|
|
367
|
-
return
|
|
368
|
+
return Object.keys(effective.providers).map((validator) => ({
|
|
368
369
|
validator,
|
|
369
370
|
state: effective.providers[validator].state,
|
|
370
371
|
enabled: effective.providers[validator].enabled,
|
|
@@ -374,6 +375,12 @@ export function externalValidationStatus(config, orchestrator = 'manual') {
|
|
|
374
375
|
}
|
|
375
376
|
|
|
376
377
|
export function validationStatus(config, orchestrator = 'manual') {
|
|
378
|
+
// Settings inspection has no coding-agent identity. Resolve independent
|
|
379
|
+
// roles only for an actual delivery host; phase dispatch still uses the
|
|
380
|
+
// strict effective configuration and cannot claim independence from manual.
|
|
381
|
+
if(orchestrator==='manual'&&config.coding_providers){
|
|
382
|
+
return {...effectiveValidationConfig(config.validation,orchestrator),roleResolution:'requires-orchestrator'};
|
|
383
|
+
}
|
|
377
384
|
return effectiveValidationConfig(config, orchestrator);
|
|
378
385
|
}
|
|
379
386
|
|
|
@@ -406,6 +413,7 @@ export function loadProjectConfig(projectRoot) {
|
|
|
406
413
|
}
|
|
407
414
|
config.validation = normaliseValidationConfig(config.validation);
|
|
408
415
|
config.dashboard = normaliseDashboardPreferences(config.dashboard);
|
|
416
|
+
if(config.coding_providers!==undefined)config.coding_providers=normaliseCodingProviders(config.coding_providers);
|
|
409
417
|
return { config, paths };
|
|
410
418
|
}
|
|
411
419
|
|
|
@@ -18,9 +18,12 @@ import {
|
|
|
18
18
|
} from './execution-leases.mjs';
|
|
19
19
|
import { addActivityEvent } from './work.mjs';
|
|
20
20
|
import {
|
|
21
|
-
|
|
21
|
+
SUPPORTED_AFK_PROVIDERS, buildProviderInvocation, commandAvailable, invokeProvider, providerCommand, captureProviderIdentity, prepareGrokBuildProvider,
|
|
22
22
|
} from './provider-adapters.mjs';
|
|
23
23
|
import { prepareContextPack } from './context-assembly.mjs';
|
|
24
|
+
import {readCodingPolicy, resolveCodingProviders, assertCodingPolicyFresh, assertModelPolicy} from '../coding-providers.mjs';
|
|
25
|
+
import {readAutonomyPolicy} from '../autonomy.mjs';
|
|
26
|
+
import {prepareGrokTaskEvidence,assertGrokTaskEvidenceFresh} from './grok-provider.mjs';
|
|
24
27
|
|
|
25
28
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
26
29
|
const workerPath = resolve(moduleDir, '../afk-worker.mjs');
|
|
@@ -101,9 +104,10 @@ function event(projectRoot, run, summary, details = '', eventType = 'progress',
|
|
|
101
104
|
|
|
102
105
|
function configuredProviders(config) {
|
|
103
106
|
const status = validationStatus(config, 'manual');
|
|
104
|
-
return
|
|
107
|
+
return SUPPORTED_AFK_PROVIDERS.filter((provider) => {
|
|
105
108
|
const setting = status.providers[provider];
|
|
106
|
-
return setting
|
|
109
|
+
return setting?.state === 'available' && setting.enabled && commandAvailable(providerCommand(provider))
|
|
110
|
+
&& (provider!=='grok'||prepareGrokBuildProvider('implementation').status==='requires-conformance');
|
|
107
111
|
});
|
|
108
112
|
}
|
|
109
113
|
|
|
@@ -283,15 +287,35 @@ export function preflightAfkRun(projectRoot, slug, options = {}) {
|
|
|
283
287
|
if (!ready.length && !(graphCheck?.completedTasks?.length === graph?.tasks?.length)) failures.push('No AFK task is ready to execute.');
|
|
284
288
|
|
|
285
289
|
const available = configuredProviders(config);
|
|
290
|
+
const coding=readCodingPolicy(root);
|
|
291
|
+
let approved=available;
|
|
292
|
+
if(options.autonomy&&coding.policy){const policy=readAutonomyPolicy(root);if(policy.status!=='current'||policy.grant.digest!==options.autonomy.grantDigest)failures.push('The approved autonomy grant changed.');
|
|
293
|
+
approved=available.filter(p=>policy.grant?.scope.providers.includes(p));}
|
|
286
294
|
const requested = String(options.provider ?? 'auto');
|
|
287
|
-
|
|
295
|
+
let providers = requested === 'auto'
|
|
288
296
|
? available
|
|
289
297
|
: requested.split(',').map((value) => value.trim()).filter(Boolean);
|
|
290
298
|
for (const provider of providers) {
|
|
291
|
-
if (!
|
|
299
|
+
if (!SUPPORTED_AFK_PROVIDERS.includes(provider)) failures.push(`Unknown AFK provider: ${provider}.`);
|
|
292
300
|
else if (!available.includes(provider)) failures.push(`${provider} is not both enabled in SPECS/pipeline.yaml and installed in this terminal.`);
|
|
293
301
|
}
|
|
294
|
-
|
|
302
|
+
let roles=null,providerIdentities=[],implementationProviders=[];
|
|
303
|
+
if(coding.policy){try{
|
|
304
|
+
if(requested!=='auto'&&coding.policy.primary!=='existing'&&coding.policy.primary!=='auto'
|
|
305
|
+
&& !providers.includes(coding.policy.primary))throw new Error('The requested provider conflicts with the saved primary. Recommended: update provider settings or use --provider auto.');
|
|
306
|
+
providers=providers.filter(p=>coding.policy.pool.includes(p)&&approved.includes(p));
|
|
307
|
+
const first=resolveCodingProviders({...coding.policy,secondary:'off',tertiary:'off'},
|
|
308
|
+
{available,allowedProviders:approved,orchestrator:providers[0]});
|
|
309
|
+
roles=resolveCodingProviders(coding.policy,{available,allowedProviders:approved,orchestrator:first.primary??providers[0]});
|
|
310
|
+
implementationProviders=coding.policy.primary==='existing'?providers.filter(orchestrator=>{
|
|
311
|
+
try{resolveCodingProviders(coding.policy,{available,allowedProviders:approved,orchestrator});return true;}catch{return false;}
|
|
312
|
+
}):[roles.primary];
|
|
313
|
+
if(!implementationProviders.length)throw new Error('No eligible coding agent can satisfy the independent review roles. Recommended: choose a primary and distinct reviewers.');
|
|
314
|
+
providers=[...new Set([...implementationProviders,...roles.reviewers])];
|
|
315
|
+
if(providers.some(p=>!approved.includes(p)))throw new Error('The selected provider is outside the approved pool.');
|
|
316
|
+
for(const provider of providers){assertModelPolicy(provider,coding.policy);providerIdentities.push(captureProviderIdentity(provider));}
|
|
317
|
+
}catch(error){failures.push(error.message);}}
|
|
318
|
+
if (!providers.length) failures.push('No configured and installed coding CLI is available. Recommended: enable and install a provider, then repeat preflight.');
|
|
295
319
|
const requestedParallel = Number(options.maxParallel ?? graph?.parallelism?.max_parallel_tasks ?? 1);
|
|
296
320
|
if (!Number.isInteger(requestedParallel) || requestedParallel < 1) failures.push('maxParallel must be a positive integer.');
|
|
297
321
|
const maxParallel = Math.max(1, Math.min(
|
|
@@ -308,6 +332,7 @@ export function preflightAfkRun(projectRoot, slug, options = {}) {
|
|
|
308
332
|
parentBranch: graph?.parent_branch ?? '',
|
|
309
333
|
topology,
|
|
310
334
|
providers,
|
|
335
|
+
...(coding.policy?{codingPolicy:coding.policy,codingPolicyDigest:coding.policyDigest,codingRoles:roles,implementationProviders,providerIdentities}:{}),
|
|
311
336
|
readyTasks: ready,
|
|
312
337
|
maxParallel,
|
|
313
338
|
graphCheck,
|
|
@@ -365,6 +390,8 @@ export function startAfkRun(projectRoot, slug, options = {}) {
|
|
|
365
390
|
status: 'starting',
|
|
366
391
|
desiredState: 'running',
|
|
367
392
|
providers: preflight.providers,
|
|
393
|
+
...(preflight.codingPolicy?{codingPolicy:preflight.codingPolicy,codingPolicyDigest:preflight.codingPolicyDigest,
|
|
394
|
+
codingRoles:preflight.codingRoles,implementationProviders:preflight.implementationProviders,providerIdentities:preflight.providerIdentities}:{}),
|
|
368
395
|
maxParallel: preflight.maxParallel,
|
|
369
396
|
timeoutMs: Number(options.timeoutMs ?? 45 * 60 * 1000),
|
|
370
397
|
createdAt: now(),
|
|
@@ -436,10 +463,12 @@ export function resumeAfkRun(projectRoot, runId, options = {}) {
|
|
|
436
463
|
|| options.autonomy?.runId !== run.autonomy.runId || options.autonomy?.grantDigest !== run.autonomy.grantDigest
|
|
437
464
|
|| typeof options.dependencies?.authority !== 'function')) throw new Error('Autonomous AFK resume requires the same bounded foreground authority context.');
|
|
438
465
|
if (processAlive(run.pid)) throw new Error('AFK conductor is already running.');
|
|
466
|
+
assertRunProviderFresh(paths.projectRoot,run);
|
|
439
467
|
const preflight = preflightAfkRun(paths.projectRoot, run.slug, {
|
|
440
468
|
provider: run.providers.join(','),
|
|
441
469
|
maxParallel: run.maxParallel,
|
|
442
470
|
ignoreRunId: run.id,
|
|
471
|
+
autonomy: options.autonomy,
|
|
443
472
|
});
|
|
444
473
|
if (preflight.status !== 'ready') throw new Error(`AFK recovery preflight blocked: ${preflight.failures.join(' ')}`);
|
|
445
474
|
if (run.autonomy) options.dependencies.authority({ stage: 'prepare', mode: 'command' });
|
|
@@ -479,9 +508,10 @@ export function prepareAfkContext(projectRoot, task, repository, options = {}) {
|
|
|
479
508
|
const review = options.mode === 'review';
|
|
480
509
|
const implementationCommit = String(options.implementationCommit ?? '').trim();
|
|
481
510
|
const taskContent = JSON.stringify(task);
|
|
482
|
-
|
|
511
|
+
let rules = review
|
|
483
512
|
? `TESTS_FIRST Read tests before implementation. Read cited standards from ${specsRoot}. Review implementation commit ${implementationCommit}. Check the diff, exact task contract, write_set, desired outcome, stop conditions, test evidence, correctness, standards, security and scope. Do not edit files. End with exactly one line: VERDICT: PASS or VERDICT: FAIL.`
|
|
484
513
|
: `You are an EWAI Build worker in an isolated Git worktree. Read standards from ${specsRoot} and tests before implementation. Repository ${repository.name} (${repository.role}). Work only inside write_set. Run only allowed_commands. Stop on every stop_condition. Make the smallest code and test change. Do not create SPECS records, commit, merge, change delivery state or update phase status. AUTHORITY_NONE. Finish with files changed and commands run.`;
|
|
514
|
+
if(options.evidenceCandidates?.length)rules += ' For this isolated Grok worker, read the complete source-standard and recorded-check sections supplied in this context. Host paths and Git history are inaccessible. Use the supplied exact commit diff for review. Do not execute commands: the conductor runs approved checks and owns commits. Read tests from the declared file snapshot before implementation.';
|
|
485
515
|
const ruleMarkers = review
|
|
486
516
|
? ['TESTS_FIRST', implementationCommit, 'VERDICT: PASS or VERDICT: FAIL', 'Do not edit files']
|
|
487
517
|
: ['AUTHORITY_NONE', 'Work only inside write_set', 'Do not create SPECS records', 'Do not create SPECS records, commit, merge, change delivery state or update phase status'];
|
|
@@ -513,19 +543,20 @@ export function prepareAfkContext(projectRoot, task, repository, options = {}) {
|
|
|
513
543
|
requiredMarkers: ruleMarkers.filter(Boolean),
|
|
514
544
|
selectionReason: review ? 'Tests-first review and exact verdict are mandatory.' : 'Write, command and delivery authority boundaries are mandatory.',
|
|
515
545
|
},
|
|
546
|
+
...(options.evidenceCandidates ?? []),
|
|
516
547
|
],
|
|
517
548
|
});
|
|
518
549
|
}
|
|
519
550
|
|
|
520
|
-
function taskPrompt(projectRoot, task, repository) {
|
|
521
|
-
const pack = prepareAfkContext(projectRoot, task, repository, { mode: 'implementation' });
|
|
551
|
+
function taskPrompt(projectRoot, task, repository, evidence) {
|
|
552
|
+
const pack = prepareAfkContext(projectRoot, task, repository, { mode: 'implementation',evidenceCandidates:evidence?.candidates });
|
|
522
553
|
if (pack.status !== 'ready') throw new Error(`AFK Build context is not ready: ${pack.reason}.`);
|
|
523
554
|
return pack.modelContext;
|
|
524
555
|
}
|
|
525
556
|
|
|
526
|
-
function reviewPrompt(projectRoot, task, implementationCommit) {
|
|
557
|
+
function reviewPrompt(projectRoot, task, implementationCommit, evidence) {
|
|
527
558
|
const pack = prepareAfkContext(projectRoot, task, { name: task.repo, role: 'review' }, {
|
|
528
|
-
mode: 'review', implementationCommit, repositoryRevision: implementationCommit,
|
|
559
|
+
mode: 'review', implementationCommit, repositoryRevision: implementationCommit,evidenceCandidates:evidence?.candidates,
|
|
529
560
|
});
|
|
530
561
|
if (pack.status !== 'ready') throw new Error(`AFK review context is not ready: ${pack.reason}.`);
|
|
531
562
|
return pack.modelContext;
|
|
@@ -619,6 +650,15 @@ function taskState(run, task, update = {}) {
|
|
|
619
650
|
run.tasks[task.id] = { ...(run.tasks[task.id] ?? {}), id: task.id, ...update, updatedAt: now() };
|
|
620
651
|
}
|
|
621
652
|
|
|
653
|
+
function assertRunProviderFresh(projectRoot,run) {
|
|
654
|
+
if(!run.codingPolicyDigest)return;
|
|
655
|
+
assertCodingPolicyFresh(run.codingPolicyDigest,readCodingPolicy(projectRoot).policyDigest);
|
|
656
|
+
for(const identity of run.providerIdentities??[]){
|
|
657
|
+
if(captureProviderIdentity(identity.provider).identityDigest!==identity.identityDigest)
|
|
658
|
+
throw new Error('Provider identity changed. Recheck capabilities before continuing.');
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
622
662
|
async function invokeControlled(projectRoot, run, task, invocation, invoke = invokeProvider) {
|
|
623
663
|
const paths = pathsFor(projectRoot, run.id), controller = new AbortController();
|
|
624
664
|
const key = controlKey(projectRoot, run.id), controls = runningControls.get(key) ?? new Set();
|
|
@@ -628,6 +668,7 @@ async function invokeControlled(projectRoot, run, task, invocation, invoke = inv
|
|
|
628
668
|
try {
|
|
629
669
|
if (requireRun(projectRoot, run.id).run.desiredState === 'cancelled') controller.abort();
|
|
630
670
|
authorityChecks.get(run)?.({ stage: 'check', mode: invocation.mode ?? 'command' });
|
|
671
|
+
if(run.codingPolicyDigest)assertCodingPolicyFresh(run.codingPolicyDigest,readCodingPolicy(projectRoot).policyDigest);
|
|
631
672
|
}
|
|
632
673
|
catch (error) { authorityError = error; controller.abort(); }
|
|
633
674
|
};
|
|
@@ -636,12 +677,18 @@ async function invokeControlled(projectRoot, run, task, invocation, invoke = inv
|
|
|
636
677
|
guard();
|
|
637
678
|
if (authorityError) throw authorityError;
|
|
638
679
|
authorityChecks.get(run)?.({ stage: 'dispatch', mode: invocation.mode ?? 'command' });
|
|
680
|
+
const identity=(run.providerIdentities??[]).find(value=>value.provider===invocation.provider);
|
|
681
|
+
const evidence=invocation.grokEvidence;
|
|
682
|
+
const checkIdentity=()=>{if(identity&&captureProviderIdentity(invocation.provider).identityDigest!==identity.identityDigest)throw new Error('Provider identity changed. Recheck capabilities before continuing.');
|
|
683
|
+
if(invocation.provider==='grok')assertGrokTaskEvidenceFresh(evidence,invocation.mode);};
|
|
684
|
+
checkIdentity();
|
|
639
685
|
// Persist uncertainty before the launch. A dead conductor must not leave
|
|
640
686
|
// an old true flag that lets resume abandon a still-running worker's lease.
|
|
641
687
|
run.executionStopped = false;
|
|
642
688
|
saveRun(paths, run);
|
|
643
689
|
timer = setInterval(guard, 50);
|
|
644
|
-
const result = await invoke(invocation, { signal: controller.signal,
|
|
690
|
+
const result = await invoke(invocation, { signal: controller.signal, beforeAccept(){guard();if(authorityError)throw authorityError;
|
|
691
|
+
authorityChecks.get(run)?.({stage:'accept',mode:invocation.mode??'command'});checkIdentity();}, onStart(pid) {
|
|
645
692
|
taskState(run, task, { pid });
|
|
646
693
|
run.activeWorkers = [...(run.activeWorkers ?? []).filter(worker => worker.taskId !== task.id), { taskId: task.id, pid }];
|
|
647
694
|
saveRun(paths, run);
|
|
@@ -650,6 +697,7 @@ async function invokeControlled(projectRoot, run, task, invocation, invoke = inv
|
|
|
650
697
|
if (result.executionStopped === true) run.activeWorkers = (run.activeWorkers ?? []).filter(worker => worker.taskId !== task.id);
|
|
651
698
|
else run.unconfirmedExecution = true;
|
|
652
699
|
if (authorityError) throw authorityError;
|
|
700
|
+
checkIdentity();
|
|
653
701
|
if (run.unconfirmedExecution) throw new Error('AFK execution is not confirmed stopped; recovery is required.');
|
|
654
702
|
run.executionStopped = (run.activeWorkers ?? []).length === 0;
|
|
655
703
|
saveRun(paths, run);
|
|
@@ -791,12 +839,15 @@ async function implementTask(projectRoot, run, task, provider, dependencies = {}
|
|
|
791
839
|
taskState(run, task, { status: 'implementing' });
|
|
792
840
|
saveRun(paths, run);
|
|
793
841
|
event(projectRoot, run, `Agent implementing ${task.id}.`, `${provider} · ${task.name}`);
|
|
842
|
+
const implementationEvidence=provider==='grok'?prepareGrokTaskEvidence(projectRoot,worktree,task,{mode:'implementation',commands:[red]}):null;
|
|
794
843
|
const invocation = buildProviderInvocation(provider, {
|
|
795
844
|
cwd: worktree,
|
|
796
|
-
prompt: taskPrompt(projectRoot, task, repository),
|
|
845
|
+
prompt: taskPrompt(projectRoot, task, repository,implementationEvidence),
|
|
846
|
+
grokEvidence:implementationEvidence,
|
|
797
847
|
task,
|
|
798
848
|
timeoutMs: run.timeoutMs,
|
|
799
849
|
mode: 'implementation',
|
|
850
|
+
codingPolicy:run.codingPolicy,policyDigest:run.codingPolicyDigest,policyRoot:projectRoot,
|
|
800
851
|
});
|
|
801
852
|
invocation.logPath = resolve(logRoot, 'implementation.log');
|
|
802
853
|
const result = await invokeControlled(projectRoot, run, task, invocation, invoke);
|
|
@@ -840,16 +891,25 @@ async function implementTask(projectRoot, run, task, provider, dependencies = {}
|
|
|
840
891
|
git(worktree, ['commit', '-m', `feat(${run.slug}): implement ${task.id}`]);
|
|
841
892
|
const implementationCommit = git(worktree, ['rev-parse', 'HEAD']);
|
|
842
893
|
|
|
843
|
-
const
|
|
894
|
+
const resolvedReviewers=run.codingPolicy?resolveCodingProviders(run.codingPolicy,{available:run.providers,allowedProviders:run.providers,orchestrator:provider}).reviewers:null;
|
|
895
|
+
const reviewers = resolvedReviewers?.length?resolvedReviewers:[run.providers.find((candidate) => candidate !== provider) ?? provider];
|
|
896
|
+
let review,reviewer;
|
|
897
|
+
const reviewResults=[];
|
|
898
|
+
for(const selectedReviewer of reviewers){
|
|
899
|
+
reviewer=selectedReviewer;
|
|
900
|
+
const reviewEvidence=reviewer==='grok'?prepareGrokTaskEvidence(projectRoot,worktree,task,{mode:'review',implementationCommit,commands:[red,green,refactor]}):null;
|
|
844
901
|
const reviewInvocation = buildProviderInvocation(reviewer, {
|
|
845
902
|
cwd: worktree,
|
|
846
|
-
prompt: reviewPrompt(projectRoot, task, implementationCommit),
|
|
903
|
+
prompt: reviewPrompt(projectRoot, task, implementationCommit,reviewEvidence),
|
|
904
|
+
grokEvidence:reviewEvidence,
|
|
847
905
|
task,
|
|
848
906
|
timeoutMs: run.timeoutMs,
|
|
849
907
|
mode: 'review',
|
|
908
|
+
codingPolicy:run.codingPolicy,policyDigest:run.codingPolicyDigest,policyRoot:projectRoot,
|
|
850
909
|
});
|
|
851
910
|
reviewInvocation.logPath = resolve(logRoot, 'review.log');
|
|
852
|
-
|
|
911
|
+
reviewInvocation.logPath = resolve(logRoot, `review-${reviewer}.log`);
|
|
912
|
+
review = await invokeControlled(projectRoot, run, task, reviewInvocation, invoke);
|
|
853
913
|
if (requireRun(projectRoot, run.id).run.desiredState === 'cancelled') {
|
|
854
914
|
throw new Error('AFK run was cancelled during fresh-context review.');
|
|
855
915
|
}
|
|
@@ -860,12 +920,15 @@ async function implementTask(projectRoot, run, task, provider, dependencies = {}
|
|
|
860
920
|
if (git(worktree, ['rev-parse', 'HEAD']) !== implementationCommit || gitDirty(worktree)) {
|
|
861
921
|
throw new Error('Fresh-context review changed the inspected implementation.');
|
|
862
922
|
}
|
|
923
|
+
reviewResults.push({reviewer,output:review.output,logPath:review.logPath});
|
|
924
|
+
}
|
|
925
|
+
if(reviewResults.length>1)review={...review,output:reviewResults.map(value=>`${value.reviewer}:\n${value.output}`).join('\n')};
|
|
863
926
|
heartbeatExecutionLease(projectRoot, lease.id, {
|
|
864
927
|
token: lease.token,
|
|
865
928
|
durationMs: Math.min(86_400_000, run.timeoutMs + 10 * 60 * 1000),
|
|
866
929
|
});
|
|
867
930
|
const branchHead = git(worktree, ['rev-parse', 'HEAD']);
|
|
868
|
-
const reviewerIdentity =
|
|
931
|
+
const reviewerIdentity = reviewers.map(reviewer=>`${reviewer}/${reviewer === provider ? 'fresh-session' : 'independent-cli'}`).join(',');
|
|
869
932
|
taskState(run, task, { status: 'ready-to-integrate', implementationCommit, branchHead, reviewer: reviewerIdentity });
|
|
870
933
|
saveRun(paths, run);
|
|
871
934
|
return {
|
|
@@ -890,6 +953,7 @@ async function implementTask(projectRoot, run, task, provider, dependencies = {}
|
|
|
890
953
|
}
|
|
891
954
|
|
|
892
955
|
async function integrateTask(projectRoot, run, result) {
|
|
956
|
+
assertRunProviderFresh(projectRoot,run);
|
|
893
957
|
authorityChecks.get(run)?.({ stage: 'integrate', mode: 'command' });
|
|
894
958
|
const { task, lease } = result;
|
|
895
959
|
const repository = result.repository;
|
|
@@ -1027,6 +1091,7 @@ export async function executeAfkRun(projectRoot, runId, dependencies = {}) {
|
|
|
1027
1091
|
try {
|
|
1028
1092
|
if (run.autonomy && !authorityChecks.has(run)) throw new Error('Autonomous AFK run requires its original live authority context.');
|
|
1029
1093
|
while (run.desiredState === 'running') {
|
|
1094
|
+
assertRunProviderFresh(paths.projectRoot,run);
|
|
1030
1095
|
const graph = readGraph(paths.projectRoot, run.slug);
|
|
1031
1096
|
const check = validateTaskGraph(deliveryPaths(paths.projectRoot, run.slug).deliveryRoot, { slug: run.slug });
|
|
1032
1097
|
if (check.status !== 'pass') throw new Error(`Task graph became invalid: ${check.errors.map((item) => item.message).join('; ')}`);
|
|
@@ -1043,7 +1108,8 @@ export async function executeAfkRun(projectRoot, runId, dependencies = {}) {
|
|
|
1043
1108
|
const waveDefinition = graph.afk_waves.find((candidate) => Number(candidate.wave) === Number(firstReady.parallel_wave));
|
|
1044
1109
|
const wave = executableIds.map((id) => graph.tasks.find((task) => task.id === id));
|
|
1045
1110
|
const results = await Promise.all(wave.map((task, index) => (
|
|
1046
|
-
implementTask(paths.projectRoot, run, task,
|
|
1111
|
+
implementTask(paths.projectRoot, run, task,
|
|
1112
|
+
(run.implementationProviders??run.providers)[index % (run.implementationProviders??run.providers).length], dependencies)
|
|
1047
1113
|
)));
|
|
1048
1114
|
const control = requireRun(paths.projectRoot, run.id).run;
|
|
1049
1115
|
run.desiredState = control.desiredState;
|