@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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const element=id=>document.getElementById(id),form=element('grokCredentialForm'),field=element('grokCredentialKey');
|
|
2
|
+
let saved=null,busy=false;
|
|
3
|
+
function controls(){
|
|
4
|
+
field.disabled=busy||!saved||!saved.storageSupported;
|
|
5
|
+
element('grokCredentialSave').disabled=field.disabled;
|
|
6
|
+
element('grokCredentialCancel').disabled=busy;
|
|
7
|
+
element('grokCredentialCheck').disabled=busy||!saved?.configured;
|
|
8
|
+
element('grokCredentialRemove').disabled=busy||!saved?.saved;
|
|
9
|
+
element('grokCredentialRefresh').disabled=busy;
|
|
10
|
+
}
|
|
11
|
+
function adopt(data){
|
|
12
|
+
saved=data;
|
|
13
|
+
element('grokCredentialStatus').textContent=data.source==='environment'?'Using XAI_API_KEY from the launching environment. '+(data.saved?'A saved key is also available.':'No saved key.'):
|
|
14
|
+
data.source==='saved'?'Saved Grok key available for this account.':'No Grok key configured. Recommended: check and save a key below.';
|
|
15
|
+
if(data.storageSupported===false)element('grokCredentialStatus').textContent+=' Local key storage is unavailable on this platform; use the launching environment.';
|
|
16
|
+
controls();
|
|
17
|
+
}
|
|
18
|
+
async function request(action='',body){
|
|
19
|
+
const response=await fetch('/api/coding-providers/grok/credentials'+(action?'/'+action:''),{method:action?'POST':'GET',headers:action?{'content-type':'application/json'}:undefined,body:action?JSON.stringify(body):undefined});
|
|
20
|
+
let data;try{data=await response.json();}catch{throw Error('Credential setup could not be reached. Recheck the local dashboard connection.');}
|
|
21
|
+
if(!response.ok)throw Error(data.error??'Credential setup failed. Refresh status and try again.');return data;
|
|
22
|
+
}
|
|
23
|
+
async function load(){
|
|
24
|
+
busy=true;controls();
|
|
25
|
+
try{adopt(await request());}catch{saved=null;element('grokCredentialNotice').textContent='Credential status could not be read safely. Repair private storage or use XAI_API_KEY in the launching environment, then refresh.';}
|
|
26
|
+
finally{busy=false;controls();}
|
|
27
|
+
}
|
|
28
|
+
async function action(name){
|
|
29
|
+
if(busy||!saved)return;
|
|
30
|
+
const body={confirmed:true,...(name==='check'?{}:{expectedRevision:saved.revision}),...(name==='configure'?{apiKey:field.value}:{})};
|
|
31
|
+
field.value='';busy=true;controls();element('grokCredentialNotice').textContent=name==='remove'?'Removing saved key…':'Checking xAI authentication without generated output…';
|
|
32
|
+
try{const result=await request(name,body);adopt(result);element('grokCredentialNotice').textContent=result.message;}
|
|
33
|
+
catch(error){element('grokCredentialNotice').textContent=error.message+' Refresh status before trying again.';}
|
|
34
|
+
finally{body.apiKey=undefined;field.value='';busy=false;controls();}
|
|
35
|
+
}
|
|
36
|
+
form.addEventListener('submit',event=>{event.preventDefault();void action('configure');});
|
|
37
|
+
element('grokCredentialCancel').addEventListener('click',()=>{field.value='';element('grokCredentialNotice').textContent='Cancelled. Saved key retained.';});
|
|
38
|
+
element('grokCredentialCheck').addEventListener('click',()=>void action('check'));
|
|
39
|
+
element('grokCredentialRemove').addEventListener('click',()=>void action('remove'));
|
|
40
|
+
element('grokCredentialRefresh').addEventListener('click',()=>{field.value='';element('grokCredentialNotice').textContent='';void load();});
|
|
41
|
+
window.addEventListener('pagehide',()=>{field.value='';});
|
|
42
|
+
controls();void load();
|
package/public/index.html
CHANGED
|
@@ -53,13 +53,46 @@
|
|
|
53
53
|
<header class="configuration-heading"><h2>Make this dashboard work for you.</h2><p>Keep the everyday tools. Add the extra views when your project needs them.</p></header>
|
|
54
54
|
<form id="dashboardPreferencesForm"><div id="dashboardPreferenceGroups"></div><div class="configuration-save"><button id="saveDashboardPreferences" type="submit" class="primary-action" disabled>Save changes</button><button id="cancelDashboardPreferences" type="button" class="quiet-button">Cancel</button><output id="dashboardPreferenceStatus" aria-live="polite"></output></div></form>
|
|
55
55
|
<p class="configuration-assurance">These settings only change what’s shown. Your standards, required checks and approvals still apply. Turning on a view won’t run a scan, connect a team service or install anything.</p>
|
|
56
|
+
<section class="configuration-group coding-providers" aria-labelledby="codingProvidersTitle">
|
|
57
|
+
<header><div><h3 id="codingProvidersTitle">Coding providers</h3><p>Choose who codes and who reviews. Recommended: let each CLI choose its own model.</p></div></header>
|
|
58
|
+
<form id="codingProvidersForm" class="coding-providers-body">
|
|
59
|
+
<div class="coding-provider-notice"><strong>Models are chosen by your CLI</strong><p>Existing host selection and required reviews continue until you save a new provider policy. Saving does not start work.</p></div>
|
|
60
|
+
<div id="codingProviderRoles" class="coding-provider-grid"></div>
|
|
61
|
+
<fieldset><legend>Eligible provider pool</legend><div id="codingProviderPool" class="coding-provider-pool"></div><p id="codingProviderAvailability"></p></fieldset>
|
|
62
|
+
<details><summary>Optional model suggestions and restrictions</summary><p>Leave these blank for native CLI selection. Suggestions are advisory. A permitted-model list stops execution when enforcement is unsupported. This does not cap tokens or spending.</p>
|
|
63
|
+
<label>Provider to configure<select id="codingModelProvider"></select></label>
|
|
64
|
+
<div class="coding-model-grid"><label>Suggested model ID (optional)<input id="codingModelSuggestion" maxlength="128" autocomplete="off"><small>Guidance without a forced model override.</small></label><label>Permitted model IDs (optional)<textarea id="codingModelPermitted" rows="3" autocomplete="off"></textarea><small>One exact provider-specific ID per line. No wildcards or inferred ranking.</small></label></div>
|
|
65
|
+
<p id="codingModelStatus" role="status"></p>
|
|
66
|
+
</details>
|
|
67
|
+
<div id="codingProviderPreview" class="coding-provider-preview" aria-live="polite"></div>
|
|
68
|
+
<div class="coding-provider-actions"><button id="codingProviderSave" type="submit" class="primary-action" disabled>Save provider settings</button><button id="codingProviderCancel" type="button" class="quiet-button" disabled>Cancel changes</button><button id="codingProviderDefaults" type="button" class="quiet-button" disabled>Restore existing behaviour</button><button id="codingProviderAuto" type="button" class="quiet-button" disabled>Choose automatically</button></div>
|
|
69
|
+
<output id="codingProviderStatus" role="status" aria-live="polite">Loading provider settings…</output>
|
|
70
|
+
<div id="codingProviderConflict" hidden><p id="codingProviderLatest"></p><button id="codingProviderCompare" type="button" class="quiet-button">Compare latest settings</button><button id="codingProviderRebase" type="button" class="quiet-button" hidden>Keep my draft against these settings</button></div>
|
|
71
|
+
<p>Automatic selection uses installed, enabled providers inside the approved delivery pool. Required checks, independent review and human approvals still apply. Grok unattended modes require separate conformance.</p>
|
|
72
|
+
</form>
|
|
73
|
+
</section>
|
|
74
|
+
<section class="configuration-group grok-credentials" aria-labelledby="grokCredentialsTitle">
|
|
75
|
+
<header><div><h3 id="grokCredentialsTitle">Grok Build credentials</h3><p>Use your xAI API key for automated coding and review. Interactive Grok Build keeps its own login.</p></div></header>
|
|
76
|
+
<div class="grok-credentials-body">
|
|
77
|
+
<p id="grokCredentialStatus" role="status">Checking credential status…</p>
|
|
78
|
+
<form id="grokCredentialForm">
|
|
79
|
+
<label for="grokCredentialKey">xAI API key</label>
|
|
80
|
+
<input id="grokCredentialKey" type="password" autocomplete="off" spellcheck="false" autocapitalize="off" maxlength="512" required aria-describedby="grokCredentialHelp" disabled>
|
|
81
|
+
<p id="grokCredentialHelp">Saved for your account on this computer, across EWAI projects, outside the project in an owner-only local file. The stored key is not encrypted. XAI_API_KEY overrides it.</p>
|
|
82
|
+
<div class="coding-provider-actions"><button id="grokCredentialSave" type="submit" class="primary-action" disabled>Check and save key</button><button id="grokCredentialCancel" type="button" class="quiet-button" disabled>Cancel key entry</button></div>
|
|
83
|
+
</form>
|
|
84
|
+
<div class="coding-provider-actions"><button id="grokCredentialCheck" type="button" class="quiet-button" disabled>Check connection</button><button id="grokCredentialRemove" type="button" class="quiet-button" disabled>Remove saved key</button><button id="grokCredentialRefresh" type="button" class="quiet-button">Refresh status</button></div>
|
|
85
|
+
<p>Checking authenticates with xAI without generating output. It does not prove available credit or coding readiness. Saving a key does not start work.</p>
|
|
86
|
+
<output id="grokCredentialNotice" role="status" aria-live="polite"></output>
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
56
89
|
<section class="configuration-group autonomy-configuration" aria-labelledby="autonomyConfigurationTitle">
|
|
57
90
|
<header><div><h3 id="autonomyConfigurationTitle">Delegated delivery</h3><p>Choose an exact pool and limits. EWAI can progress only permitted work between your decisions.</p></div><strong id="autonomyMode" class="autonomy-mode">Checking…</strong></header>
|
|
58
91
|
<div class="autonomy-configuration-body">
|
|
59
92
|
<fieldset class="autonomy-pool-field"><legend>Exact intent pool</legend><p>Only the intents you select can enter this grant.</p><div id="autonomyPool" class="autonomy-pool" aria-live="polite">Loading project intents…</div></fieldset>
|
|
60
93
|
<fieldset class="autonomy-actions-field"><legend>Permitted progress</legend><button type="button" role="checkbox" aria-checked="true" name="autonomyAction" value="begin-harness">Begin a ready intent</button><button type="button" role="checkbox" aria-checked="false" name="autonomyAction" value="prepare-phase">Prepare a governed phase</button><button type="button" role="checkbox" aria-checked="false" name="autonomyAction" value="afk-build">Continue approved Build work</button></fieldset>
|
|
61
94
|
<div class="autonomy-limits">
|
|
62
|
-
<label>Provider<select id="autonomyProvider"><option value="codex">Codex</option><option value="claude">Claude</option><option value="antigravity">Antigravity</option></select></label>
|
|
95
|
+
<label>Provider<select id="autonomyProvider"><option value="codex">Codex</option><option value="claude">Claude</option><option value="antigravity">Antigravity</option><option value="grok">Grok Build</option><option value="auto">Automatic from saved provider pool</option></select></label>
|
|
63
96
|
<label>New grant expires after, hours<input id="autonomyExpiryHours" type="number" min="1" max="24" value="2" inputmode="numeric"></label>
|
|
64
97
|
<label>Run limit, minutes<input id="autonomyRuntimeMinutes" type="number" min="1" max="1440" value="30" inputmode="numeric"></label>
|
|
65
98
|
<label>Operation limit, seconds<input id="autonomyOperationSeconds" type="number" min="1" max="3600" value="900" inputmode="numeric"></label>
|
|
@@ -811,5 +844,7 @@
|
|
|
811
844
|
</form>
|
|
812
845
|
</dialog>
|
|
813
846
|
<script src="/app.js" type="module"></script>
|
|
847
|
+
<script src="/coding-providers.js" type="module"></script>
|
|
848
|
+
<script src="/grok-credentials.js" type="module"></script>
|
|
814
849
|
</body>
|
|
815
850
|
</html>
|
package/public/styles.css
CHANGED
|
@@ -2736,6 +2736,27 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-
|
|
|
2736
2736
|
.autonomy-candidate p, .autonomy-run p, .autonomy-empty p { margin: 10px 0; line-height: 1.5; }
|
|
2737
2737
|
.autonomy-question-context { padding: 10px 12px; border-left: 3px solid var(--amber); background: var(--amber-soft); color: var(--ink); overflow-wrap: anywhere; }
|
|
2738
2738
|
.autonomy-reasons { color: var(--amber); font-weight: 700; }
|
|
2739
|
+
.coding-providers-body { padding: 24px; display: grid; gap: 20px; }
|
|
2740
|
+
.coding-provider-notice, .coding-provider-preview { padding: 16px; border-left: 3px solid var(--green); background: var(--surface-2); }
|
|
2741
|
+
.coding-providers p { overflow-wrap: anywhere; }
|
|
2742
|
+
.coding-provider-notice p { margin-bottom: 0; }
|
|
2743
|
+
.coding-provider-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
|
|
2744
|
+
.coding-providers label { display: grid; gap: 8px; font-weight: 700; min-width: 0; }
|
|
2745
|
+
.coding-providers small { font-weight: 400; color: var(--muted); }
|
|
2746
|
+
.coding-providers input, .coding-providers select, .coding-providers textarea { width: 100%; min-width: 0; min-height: 44px; padding: 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); font: inherit; }
|
|
2747
|
+
.coding-providers textarea { resize: vertical; }
|
|
2748
|
+
.coding-provider-pool { display: flex; flex-wrap: wrap; gap: 20px; }
|
|
2749
|
+
.coding-provider-pool label { display: flex; align-items: center; min-height: 44px; }
|
|
2750
|
+
.coding-provider-pool input { width: 18px; min-height: 18px; }
|
|
2751
|
+
.coding-provider-pool button { min-height: 44px; padding: 8px 12px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); font: inherit; cursor: pointer; }
|
|
2752
|
+
.coding-provider-pool button::before { content: '□'; margin-right: 8px; }
|
|
2753
|
+
.coding-provider-pool button[aria-checked=true]::before { content: '✓'; color: var(--green); }
|
|
2754
|
+
.coding-model-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
|
|
2755
|
+
.coding-providers summary { cursor: pointer; min-height: 44px; font-weight: 700; }
|
|
2756
|
+
.coding-provider-actions { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
2757
|
+
.coding-providers output { display: block; overflow-wrap: anywhere; }
|
|
2758
|
+
.coding-providers :focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }
|
|
2759
|
+
@media (max-width: 720px) { .coding-providers-body { padding: 18px; } .coding-provider-grid, .coding-model-grid { grid-template-columns: minmax(0, 1fr); } .coding-provider-actions button { flex-grow: 1; } }
|
|
2739
2760
|
.autonomy-candidate .autonomy-card-actions, .autonomy-run .autonomy-card-actions { margin-top: 12px; }
|
|
2740
2761
|
.autonomy-intent-detail { margin-top: 18px; padding: 16px; border-left: 4px solid var(--green); background: var(--surface-2); }
|
|
2741
2762
|
.autonomy-intent-detail h3 { margin: 0 0 7px; }
|
|
@@ -2755,3 +2776,7 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-
|
|
|
2755
2776
|
.autonomy-button-row button { width: 100%; }
|
|
2756
2777
|
.autonomy-dialog .action-sheet > footer { flex-wrap: wrap; }
|
|
2757
2778
|
}
|
|
2779
|
+
.grok-credentials-body { padding: 18px; display: grid; gap: 14px; }
|
|
2780
|
+
.grok-credentials-body form { display: grid; gap: 12px; }
|
|
2781
|
+
.grok-credentials-body input { width: 100%; box-sizing: border-box; min-height: 40px; border: 1px solid var(--line, #cfd8d5); border-radius: 6px; padding: 8px 10px; }
|
|
2782
|
+
.grok-credentials-body p { margin: 0; }
|
|
@@ -49,6 +49,7 @@ Personas are advisory lenses. They are not user evidence, specialist validation,
|
|
|
49
49
|
- The loopback dashboard receives only a body-free safe manifest.
|
|
50
50
|
- Never accept a browser-supplied project root, file path, provider command, executable, or credential.
|
|
51
51
|
- Keep estimated input usage clearly separate from optional provider-reported usage.
|
|
52
|
+
- Preserve captured coding-provider policy and the actual coding-agent identity when preparing task/review context. Model suggestions are advisory; never replace native model choice or bypass an unsupported permitted-model boundary to save tokens. Concision must retain decisions, supporting guidance, recommended actions and mandatory evidence.
|
|
52
53
|
- Context preparation cannot approve Build or Manual QA, certify quality, accept security risk, deploy, or release.
|
|
53
54
|
|
|
54
55
|
## Verify engineering performance
|
|
@@ -19,7 +19,9 @@ Preserve all fourteen stages, the UI Design and FitCheck adjuncts, shelf/resume
|
|
|
19
19
|
|
|
20
20
|
## Start and resume
|
|
21
21
|
|
|
22
|
-
For new delivery, call `ewai_delivery_begin` (or `ewai delivery begin`) for an existing project-owned intent and identify the current host as `tool`/`--tool` (`claude`, `codex`, or `antigravity`). Antigravity is invoked through the `agy` CLI. That value defines the orchestrator exclusion boundary. For continuing work, call `ewai_delivery_continue` first. Never reconstruct the next phase from chat memory.
|
|
22
|
+
For new delivery, call `ewai_delivery_begin` (or `ewai delivery begin`) for an existing project-owned intent and identify the current host as `tool`/`--tool` (`claude`, `codex`, `grok`, or `antigravity`). Antigravity is invoked through the `agy` CLI. That value defines the orchestrator exclusion boundary. For continuing work, call `ewai_delivery_continue` first. Never reconstruct the next phase from chat memory.
|
|
23
|
+
|
|
24
|
+
Optional coding-provider settings select the primary agent and distinct secondary/tertiary reviewers. Native model choice is the default; suggestions are advisory. Stop before invocation when permitted-model enforcement is unsupported. Automatic selection must intersect saved pools, configured capabilities and approved grants; settings never grant Build or Manual QA authority. Grok coding, review and proposal modes require their own verified native identity/conformance and private environment authentication. Keep an incomplete or unconfirmed mode visible and recommend a concrete recovery action.
|
|
23
25
|
|
|
24
26
|
Before substantial repository navigation or truth claims, call `ewai_index_status`; refresh with `ewai_index_refresh` when the index is missing or stale. Use `ewai_index_search` and `ewai_index_graph` for navigation and blast-radius evidence, then verify material claims against source files.
|
|
25
27
|
|
|
@@ -124,6 +124,7 @@ Do not relocate or overwrite an existing SPECS contract through init. Relocation
|
|
|
124
124
|
- Require human approval before Build when `approvals.build` is `required`.
|
|
125
125
|
- Read the full `validation` contract before scheduling review. Do not infer availability from global machine state, and do not use the active Claude, Codex, or Antigravity orchestrator as its own independent validator.
|
|
126
126
|
- Honour each checkpoint's selected providers, maximum cycles, breadth, depth, and output limit. Standards compliance remains mandatory even when no independent external validator is configured.
|
|
127
|
+
- Use the shared `ewai providers` policy for optional Claude Code, Codex, Grok Build and Antigravity coding/review roles. Keep native model choice by default; suggestions are advisory and unsupported permitted-model restrictions stop dispatch. Automatic selection remains inside configured capability and approved grant pools. Distinguish installed CLI, authenticated account and verified execution mode.
|
|
127
128
|
- Never write project outputs into the global EWAI installation.
|
|
128
129
|
- Never carry repository paths or project state from one project into another.
|
|
129
130
|
|
package/src/autonomy.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { autonomyError, autonomyGuard, autonomyDigest, autonomyPaths, autonomyFi
|
|
|
5
5
|
writeAutonomyRecord, withAutonomyLock, readAutonomySnapshot } from './runtime/autonomy-workspace.mjs';
|
|
6
6
|
|
|
7
7
|
export const AUTONOMY_ACTIONS = Object.freeze(['begin-harness', 'prepare-phase', 'afk-build']);
|
|
8
|
-
export const AUTONOMY_PROVIDERS = Object.freeze(['codex', 'claude', 'antigravity']);
|
|
8
|
+
export const AUTONOMY_PROVIDERS = Object.freeze(['codex', 'claude', 'antigravity', 'grok']);
|
|
9
9
|
export const AUTONOMY_HUMAN_EXCLUSIONS = Object.freeze(['approve-build', 'select-prototype', 'manual-qa', 'accept-risk', 'policy-exception', 'destructive-action', 'deploy', 'release']);
|
|
10
10
|
const intentPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*\/[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
11
11
|
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
package/src/cli.mjs
CHANGED
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
} from './design-systems.mjs';
|
|
33
33
|
import { installDesignSystemCandidate, validateDesignSystemCandidate } from './design-system-authoring.mjs';
|
|
34
34
|
import { applyDesignSystem } from './design-system-application.mjs';
|
|
35
|
+
import {readCodingProviders,saveCodingProviders} from './coding-providers.mjs';
|
|
36
|
+
import {grokCredentialStatus,configureGrokCredentials,checkGrokConnection,removeGrokCredentials} from './grok-credentials.mjs';
|
|
35
37
|
import {
|
|
36
38
|
createPersona,
|
|
37
39
|
indexPersonas,
|
|
@@ -493,8 +495,13 @@ function usage() {
|
|
|
493
495
|
return `EWAI Pipeline
|
|
494
496
|
|
|
495
497
|
Commands:
|
|
496
|
-
ewai install [--project PATH] [--host auto|codex|claude|antigravity] [--mode copy|link]
|
|
497
|
-
ewai
|
|
498
|
+
ewai install [--project PATH] [--host auto|codex|claude|grok|antigravity] [--mode copy|link]
|
|
499
|
+
ewai providers show|set|auto|defaults [--project PATH] [--json]
|
|
500
|
+
ewai providers set [--primary PROVIDER|auto|existing] [--secondary PROVIDER|auto|off|existing] [--tertiary PROVIDER|auto|off|existing] [--pool LIST] [--input FILE] [--expected-digest DIGEST]
|
|
501
|
+
ewai providers model PROVIDER [--suggest MODEL_ID] [--allow-model MODEL_ID] [--clear-suggestion] [--clear-restriction]
|
|
502
|
+
ewai providers credentials grok status|configure|check|remove [--project PATH] [--json]
|
|
503
|
+
ewai providers credentials grok remove --yes [--expected-revision REVISION]
|
|
504
|
+
ewai init [--project PATH] [--name NAME] [--specs PATH] [--init-specs-repo] [--claude] [--codex] [--grok] [--antigravity] [--force]
|
|
498
505
|
ewai checkin [--project PATH]
|
|
499
506
|
ewai dashboard [--project PATH]
|
|
500
507
|
ewai dashboard preferences [--project PATH] [--json]
|
|
@@ -502,7 +509,7 @@ Commands:
|
|
|
502
509
|
ewai autonomy preview [--intent DOMAIN/SLUG --action ACTION --provider PROVIDER --expires-at ISO --max-runtime-ms N --max-operation-ms N --max-attempts N --record] [--project PATH] [--json]
|
|
503
510
|
ewai autonomy approve --expected-digest DIGEST --approved-by NAME --yes [--project PATH] [--json]
|
|
504
511
|
ewai autonomy revoke --expected-digest DIGEST --revoked-by NAME --yes [--project PATH] [--json]
|
|
505
|
-
ewai autonomy run|service --expected-digest DIGEST --provider PROVIDER --yes [--project PATH] [--json]
|
|
512
|
+
ewai autonomy run|service --expected-digest DIGEST --provider PROVIDER|auto --yes [--project PATH] [--json]
|
|
506
513
|
ewai autonomy pause|resume|cancel|recover --run ID --expected-revision N --yes [--project PATH] [--json]
|
|
507
514
|
ewai autonomy control --action pause|resume|cancel|revoke|recover --run ID --expected-revision N --yes [--revoked-by NAME] [--project PATH] [--json]
|
|
508
515
|
ewai autonomy answer --input PROJECT_RELATIVE_JSON --yes [--project PATH] [--json]
|
|
@@ -569,8 +576,8 @@ Commands:
|
|
|
569
576
|
ewai archaeology prepare-review BUNDLE [--project PATH] [--force]
|
|
570
577
|
ewai archaeology curate BUNDLE [--project PATH] --yes [--approved-by NAME]
|
|
571
578
|
ewai archaeology validate-completion BUNDLE [--project PATH]
|
|
572
|
-
ewai validation list [--project PATH] [--orchestrator manual|claude|codex|antigravity]
|
|
573
|
-
ewai validation set claude|codex|antigravity available|unavailable [--enabled|--disabled] [--project PATH]
|
|
579
|
+
ewai validation list [--project PATH] [--orchestrator manual|claude|codex|grok|antigravity]
|
|
580
|
+
ewai validation set claude|codex|grok|antigravity available|unavailable [--enabled|--disabled] [--project PATH]
|
|
574
581
|
ewai validation checkpoint implementation-plan|test-plan|code [--cycles N] [--validators auto|PROVIDER,...] [--breadth targeted|change-set|capability|system] [--depth issues-only|issues-and-fixes|analysis-and-recommendations] [--output small|medium|large] [--enabled|--disabled] [--project PATH]
|
|
575
582
|
ewai security status|providers|adapters|runs|findings|dispositions [--project PATH]
|
|
576
583
|
ewai security adapter-validate FOLDER [--project PATH]
|
|
@@ -613,13 +620,13 @@ Commands:
|
|
|
613
620
|
ewai delivery phase-complete SLUG PHASE [--artefact PATH] [--project PATH]
|
|
614
621
|
ewai delivery ratify-amendments SLUG --yes --approved-by NAME --scope TEXT [--project PATH]
|
|
615
622
|
ewai delivery evidence-amendment SLUG [--yes --approved-by NAME --reason TEXT --expected-state-digest DIGEST] [--project PATH]
|
|
616
|
-
ewai delivery validation-cycle SLUG PHASE --provider claude|codex|antigravity --outcome pass|issues [--response PATH] [--fix PATH] [--notes TEXT] [--project PATH]
|
|
623
|
+
ewai delivery validation-cycle SLUG PHASE --provider claude|codex|grok|antigravity --outcome pass|issues [--response PATH] [--fix PATH] [--notes TEXT] [--project PATH]
|
|
617
624
|
ewai delivery approve-build SLUG --yes --approved-by NAME [--scope TEXT] [--project PATH]
|
|
618
625
|
ewai delivery approve-manual-qa SLUG --yes --approved-by NAME --evidence PATH [--notes TEXT] [--project PATH]
|
|
619
626
|
ewai delivery runs [SLUG] [--project PATH]
|
|
620
627
|
ewai delivery runs --mark-stale [--hours N] [--project PATH]
|
|
621
|
-
ewai afk preflight SLUG [--provider auto|claude|codex|antigravity] [--parallel N] [--project PATH]
|
|
622
|
-
ewai afk start SLUG [--provider auto|claude|codex|antigravity] [--parallel N] [--timeout-minutes N] [--project PATH]
|
|
628
|
+
ewai afk preflight SLUG [--provider auto|claude|codex|grok|antigravity] [--parallel N] [--project PATH]
|
|
629
|
+
ewai afk start SLUG [--provider auto|claude|codex|grok|antigravity] [--parallel N] [--timeout-minutes N] [--project PATH]
|
|
623
630
|
ewai afk status [RUN_ID] [--project PATH]
|
|
624
631
|
ewai afk pause RUN_ID [--project PATH]
|
|
625
632
|
ewai afk resume RUN_ID [--project PATH]
|
|
@@ -836,7 +843,7 @@ export async function run(args) {
|
|
|
836
843
|
name: option(args, '--name'),
|
|
837
844
|
specsRoot: option(args, '--specs') || undefined,
|
|
838
845
|
initSpecsRepository: has(args, '--init-specs-repo'),
|
|
839
|
-
validators: ['claude', 'codex', 'antigravity'].filter((validator) => has(args, `--${validator}`)),
|
|
846
|
+
validators: ['claude', 'codex', 'grok', 'antigravity'].filter((validator) => has(args, `--${validator}`)),
|
|
840
847
|
force: has(args, '--force')
|
|
841
848
|
});
|
|
842
849
|
print(json ? result : formatInitResult(result), json);
|
|
@@ -902,6 +909,59 @@ export async function run(args) {
|
|
|
902
909
|
print(json ? result : JSON.stringify(result, null, 2), json); return;
|
|
903
910
|
}
|
|
904
911
|
|
|
912
|
+
if(command==='providers'&&subcommand==='credentials'){
|
|
913
|
+
const action=args[3];
|
|
914
|
+
if(args[2]!=='grok'||!['status','configure','check','remove'].includes(action))throw new Error('Choose providers credentials grok status, configure, check or remove.');
|
|
915
|
+
const allowedValues=['--project',...(action==='remove'?['--expected-revision']:[])],allowedFlags=['--json',...(action==='remove'?['--yes']:[])];
|
|
916
|
+
for(let i=4;i<args.length;i++){
|
|
917
|
+
if(allowedValues.includes(args[i])){if(!args[i+1]||args[i+1].startsWith('--'))throw new Error('A credential option requires a value.');i++;}
|
|
918
|
+
else if(!allowedFlags.includes(args[i]))throw new Error('Unsupported credential option. Never put API keys in command arguments.');
|
|
919
|
+
}
|
|
920
|
+
const credentialOptions={projectRoot:selectedProject(args)};
|
|
921
|
+
let result;
|
|
922
|
+
if(action==='status')result=grokCredentialStatus(credentialOptions);
|
|
923
|
+
if(action==='check')result=await checkGrokConnection(credentialOptions);
|
|
924
|
+
if(action==='remove')result=removeGrokCredentials({confirmed:has(args,'--yes'),expectedRevision:option(args,'--expected-revision',grokCredentialStatus(credentialOptions).revision)},credentialOptions);
|
|
925
|
+
if(action==='configure'){
|
|
926
|
+
if(!process.stdin.isTTY||!process.stdout.isTTY)throw new Error('Grok setup requires an interactive terminal for hidden input. Use the local dashboard password form instead.');
|
|
927
|
+
const previous=grokCredentialStatus(credentialOptions),muted=new Writable({write(_chunk,_encoding,callback){callback();}});muted.isTTY=true;muted.columns=process.stdout.columns;
|
|
928
|
+
const prompt=createInterface({input:process.stdin,output:muted,terminal:true,historySize:0});let apiKey;
|
|
929
|
+
try{
|
|
930
|
+
process.stdout.write('This checks xAI authentication without generated output and saves the key for your account on this computer, outside the project, in an owner-only local file (not encrypted). XAI_API_KEY overrides the saved key. Cancel with Ctrl+C.\nxAI API key (hidden): ');
|
|
931
|
+
apiKey=await prompt.question('');process.stdout.write('\n');
|
|
932
|
+
result=await configureGrokCredentials({confirmed:true,expectedRevision:previous.revision,apiKey},credentialOptions);
|
|
933
|
+
}finally{apiKey=undefined;prompt.close();}
|
|
934
|
+
}
|
|
935
|
+
print(json?result:result.message??('Grok credential: '+result.source+'. Saved key: '+(result.saved?'available':'absent')+'.'),json);
|
|
936
|
+
if(result.status==='failed')process.exitCode=1;return;
|
|
937
|
+
}
|
|
938
|
+
if(command==='providers'){
|
|
939
|
+
if(!['show','set','auto','defaults','model'].includes(subcommand))throw new Error('Choose providers show, set, auto, model or defaults.');
|
|
940
|
+
const valued=new Set(['--project','--expected-digest',...(subcommand==='set'?['--primary','--secondary','--tertiary','--pool','--input']:[]),...(subcommand==='auto'?['--pool']:[]),...(subcommand==='model'?['--suggest','--allow-model']:[])]);
|
|
941
|
+
const switches=new Set(['--json',...(subcommand==='model'?['--clear-suggestion','--clear-restriction']:[])]),seen=new Set();
|
|
942
|
+
for(let i=subcommand==='model'?3:2;i<args.length;i++){const arg=args[i];if(valued.has(arg)){if(!args[i+1]||args[i+1].startsWith('--'))throw new Error(arg+' requires a value.');if(seen.has(arg)&&arg!=='--allow-model')throw new Error('Do not repeat '+arg);seen.add(arg);i++;}else if(!switches.has(arg))throw new Error('Unsupported provider argument: '+arg);}
|
|
943
|
+
const project=selectedProject(args),current=readCodingProviders(project);
|
|
944
|
+
if(subcommand==='show'){print(json?current:JSON.stringify(current,null,2),json);return;}
|
|
945
|
+
let policy=structuredClone(current.policy??{});
|
|
946
|
+
if(subcommand==='defaults')policy=null;
|
|
947
|
+
if(subcommand==='set'){
|
|
948
|
+
const input=option(args,'--input');if(input&&['--primary','--secondary','--tertiary','--pool'].some(flag=>has(args,flag)))throw new Error('Use a complete input policy or individual setting flags.');
|
|
949
|
+
if(input)policy=readProjectJsonInput(project,input,'Provider policy');
|
|
950
|
+
else{if(!['--primary','--secondary','--tertiary','--pool'].some(flag=>has(args,flag)))throw new Error('Choose provider settings to change.');for(const role of ['primary','secondary','tertiary'])if(has(args,'--'+role))policy[role]=option(args,'--'+role);if(has(args,'--pool'))policy.pool=option(args,'--pool').split(',').map(x=>x.trim());}
|
|
951
|
+
}
|
|
952
|
+
if(subcommand==='auto'){policy.primary='auto';policy.secondary='auto';policy.tertiary='auto';if(has(args,'--pool'))policy.pool=option(args,'--pool').split(',').map(x=>x.trim());}
|
|
953
|
+
if(subcommand==='model'){
|
|
954
|
+
const provider=args[2];if(!['claude','codex','grok','antigravity'].includes(provider))throw new Error('Choose a supported model provider.');
|
|
955
|
+
if(has(args,'--suggest')&&has(args,'--clear-suggestion')||has(args,'--allow-model')&&has(args,'--clear-restriction'))throw new Error('Choose one change for each model setting.');
|
|
956
|
+
if(!['--suggest','--allow-model','--clear-suggestion','--clear-restriction'].some(flag=>has(args,flag)))throw new Error('Choose a model suggestion or restriction to change.');
|
|
957
|
+
policy.models??={};const model=policy.models[provider]??{};
|
|
958
|
+
if(has(args,'--clear-suggestion'))delete model.suggestion;if(has(args,'--clear-restriction'))delete model.permitted_models;
|
|
959
|
+
if(has(args,'--suggest'))model.suggestion=option(args,'--suggest');if(has(args,'--allow-model'))model.permitted_models=options(args,'--allow-model');policy.models[provider]=model;
|
|
960
|
+
}
|
|
961
|
+
const saved=saveCodingProviders(project,{confirmed:true,expectedDigest:option(args,'--expected-digest',current.digest),policy});
|
|
962
|
+
print(json?saved:'Provider settings saved. Models remain native by default; required checks and approvals still apply.',json);return;
|
|
963
|
+
}
|
|
964
|
+
|
|
905
965
|
if (command === 'dashboard' && ['preferences','configure'].includes(subcommand)) {
|
|
906
966
|
const valueFlags=['--project','--enable','--disable','--expected-digest'];
|
|
907
967
|
const booleanFlags=['--json','--yes','--collapse','--expand'];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {spawnSync} from 'node:child_process';
|
|
3
|
+
import {readProjectDocument,editProjectDocument} from './config-document.mjs';
|
|
4
|
+
|
|
5
|
+
export const CODING_PROVIDERS=Object.freeze(['claude','codex','grok','antigravity']);
|
|
6
|
+
export const CODING_PROVIDER_LABELS=Object.freeze({claude:'Claude Code',codex:'Codex',grok:'Grok Build',antigravity:'Antigravity'});
|
|
7
|
+
const object=v=>v!==null&&typeof v==='object'&&!Array.isArray(v);
|
|
8
|
+
function fail(message,statusCode=400,code){const error=new Error(message);error.statusCode=statusCode;error.codingProviderError=true;if(code)error.code=code;throw error;}
|
|
9
|
+
function closed(value,keys,label){if(!object(value)||Object.keys(value).some(k=>!keys.includes(k)))fail(label+' contains unsupported fields.');}
|
|
10
|
+
function provider(value){if(!CODING_PROVIDERS.includes(value))fail('Choose Claude Code, Codex, Grok Build or Antigravity.');return value;}
|
|
11
|
+
function modelId(value){if(typeof value!=='string'||!/^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,127}$/.test(value))fail('Use an exact, bounded model ID without spaces or wildcards.');return value;}
|
|
12
|
+
export function normaliseCodingProviders(value){
|
|
13
|
+
if(value===undefined)return null;
|
|
14
|
+
closed(value,['pool','primary','secondary','tertiary','models'],'Provider policy');
|
|
15
|
+
const pool=value.pool??[...CODING_PROVIDERS];
|
|
16
|
+
if(!Array.isArray(pool)||!pool.length||pool.length>4||new Set(pool).size!==pool.length)fail('Choose a nonempty pool of distinct providers.');pool.forEach(provider);
|
|
17
|
+
const roles={};
|
|
18
|
+
for(const role of ['primary','secondary','tertiary']){
|
|
19
|
+
const choice=value[role]??'existing',allowed=['existing','auto',...CODING_PROVIDERS,...(role==='primary'?[]:['off'])];
|
|
20
|
+
if(!allowed.includes(choice))fail('Choose a supported '+role+' provider.');
|
|
21
|
+
if(CODING_PROVIDERS.includes(choice)&&!pool.includes(choice))fail('Include the selected '+role+' provider in the eligible pool.');roles[role]=choice;
|
|
22
|
+
}
|
|
23
|
+
const raw=value.models??{};closed(raw,CODING_PROVIDERS,'Model policy');const models={};
|
|
24
|
+
for(const p of CODING_PROVIDERS){if(raw[p]===undefined)continue;closed(raw[p],['suggestion','permitted_models'],'Model policy');const m={};
|
|
25
|
+
if(raw[p].suggestion!==undefined)m.suggestion=modelId(raw[p].suggestion);
|
|
26
|
+
if(raw[p].permitted_models!==undefined){const ids=raw[p].permitted_models;if(!Array.isArray(ids)||!ids.length||ids.length>64||new Set(ids).size!==ids.length)fail('Provide a nonempty list of distinct permitted model IDs.');m.permitted_models=ids.map(modelId);}
|
|
27
|
+
if(m.suggestion&&m.permitted_models&&!m.permitted_models.includes(m.suggestion))fail('The suggestion must be in the permitted model list.');
|
|
28
|
+
if(Object.keys(m).length)models[p]=m;
|
|
29
|
+
}
|
|
30
|
+
return {pool:[...pool],...roles,models};
|
|
31
|
+
}
|
|
32
|
+
export function codingPolicyDigest(value){return 'sha256:'+createHash('sha256').update(JSON.stringify(value===null?null:normaliseCodingProviders(value))).digest('hex');}
|
|
33
|
+
export function readCodingPolicy(root){const value=readProjectDocument(root).config.coding_providers;const policy=value===undefined?null:normaliseCodingProviders(value);return {policy,policyDigest:codingPolicyDigest(policy)};}
|
|
34
|
+
export function assertCodingPolicyFresh(expected,actual){if(!/^sha256:[a-f0-9]{64}$/.test(String(expected))||expected!==actual)fail('Provider policy changed. Recheck the saved settings and approved scope before continuing.',409);}
|
|
35
|
+
export function assertModelPolicy(selected,raw){
|
|
36
|
+
provider(selected);const policy=raw==null?null:normaliseCodingProviders(raw),model=policy?.models[selected];
|
|
37
|
+
if(model?.permitted_models)fail(CODING_PROVIDER_LABELS[selected]+' cannot enforce this permitted-model list through a verified EWAI adapter. Execution stopped. Recommended: clear the restriction to use native model selection, or use an adapter with verified enforcement.',409,'coding-provider-model-restriction-unsupported');
|
|
38
|
+
return {mode:'native',...(model?.suggestion?{suggestion:model.suggestion}: {})};
|
|
39
|
+
}
|
|
40
|
+
export function modelGuidance(selected,policy){const m=assertModelPolicy(selected,policy);return m.suggestion?'\nAdvisory model suggestion: '+m.suggestion+'. Let the CLI choose the appropriate model.':'';}
|
|
41
|
+
export function resolveCodingProviders(raw,options={}){
|
|
42
|
+
const policy=raw===null?null:normaliseCodingProviders(raw),orchestrator=options.orchestrator;
|
|
43
|
+
if(!policy)return {legacy:true,primary:null,reviewers:options.reviewers??[],policyDigest:codingPolicyDigest(null)};
|
|
44
|
+
const available=options.available??[],permitted=options.allowedProviders??CODING_PROVIDERS;
|
|
45
|
+
const eligible=policy.pool.filter(p=>available.includes(p)&&permitted.includes(p));
|
|
46
|
+
const automaticEligible=eligible.filter(p=>{try{assertModelPolicy(p,policy);return true;}catch{return false;}});
|
|
47
|
+
if(policy.primary==='auto'&&!automaticEligible.length&&eligible.length)assertModelPolicy(eligible[0],policy);
|
|
48
|
+
const primary=policy.primary==='existing'?(CODING_PROVIDERS.includes(orchestrator)?orchestrator:null):policy.primary==='auto'?automaticEligible[0]:policy.primary;
|
|
49
|
+
if(policy.primary!=='existing'&&(!primary||!eligible.includes(primary)))fail('No eligible primary provider. Recommended: enable an available provider inside the approved pool.',409);
|
|
50
|
+
const actual=orchestrator??primary;
|
|
51
|
+
const inherited=(options.reviewers??[]).filter(p=>p!==actual);
|
|
52
|
+
const reviewPool=eligible.filter(p=>p!==actual&&(options.reviewers===undefined||inherited.includes(p)));
|
|
53
|
+
const selected=[];
|
|
54
|
+
for(const role of ['secondary','tertiary']){
|
|
55
|
+
const choice=policy[role];if(choice==='off'||choice==='existing')continue;
|
|
56
|
+
if(!CODING_PROVIDERS.includes(actual))fail('Independent review requires the actual coding agent identity. Recommended: begin delivery with a supported orchestrator.',409);
|
|
57
|
+
const candidate=choice==='auto'?reviewPool.find(p=>automaticEligible.includes(p)&&!selected.includes(p)):choice;
|
|
58
|
+
if(choice==='auto'&&!candidate){const unsupported=reviewPool.find(p=>!selected.includes(p));if(unsupported)assertModelPolicy(unsupported,policy);}
|
|
59
|
+
if(candidate===actual)fail('The reviewer must be independent of the actual coding agent. Choose another provider.',409);
|
|
60
|
+
if(selected.includes(candidate))fail('Secondary and tertiary reviewers must use distinct providers. Choose another provider.',409);
|
|
61
|
+
if(!candidate||!reviewPool.includes(candidate))fail('No eligible '+role+' reviewer. Recommended: enable another independent provider in the checkpoint and approved pool.',409);
|
|
62
|
+
selected.push(candidate);
|
|
63
|
+
}
|
|
64
|
+
if(policy.secondary==='existing'||policy.tertiary==='existing')for(const p of reviewPool)if(!selected.includes(p))selected.push(p);
|
|
65
|
+
if(selected.length<Number(options.requiredReviewers??0))fail('Required independent review capacity is unavailable. Keep the required checkpoint and enable enough distinct reviewers.',409);
|
|
66
|
+
if(policy.primary!=='existing')assertModelPolicy(primary,policy);
|
|
67
|
+
for(const p of selected)assertModelPolicy(p,policy);
|
|
68
|
+
return {legacy:false,primary,reviewers:selected,policyDigest:codingPolicyDigest(policy)};
|
|
69
|
+
}
|
|
70
|
+
export function codingProviderCapabilities(options={}){
|
|
71
|
+
const probe=options.probe??(p=>{const r=spawnSync(p==='antigravity'?'agy':p,['--version'],{stdio:'pipe',encoding:'utf8',timeout:3000,env:options.env??process.env});return {available:!r.error&&r.status===0};});
|
|
72
|
+
return CODING_PROVIDERS.map(p=>{const value=probe(p),available=typeof value==='boolean'?value:value?.available===true;
|
|
73
|
+
return {provider:p,label:CODING_PROVIDER_LABELS[p],available,interactive:available,modelSelection:'native',modelRestrictions:'unsupported',unattended:p==='grok'?'requires-conformance':'existing-adapter',recommendation:available?'Use native model selection. Verify required execution capability before running.':'Install or enable this CLI, then recheck availability.'};});
|
|
74
|
+
}
|
|
75
|
+
function projection(state,options={}){const policy=state.config.coding_providers===undefined?null:normaliseCodingProviders(state.config.coding_providers),capabilities=codingProviderCapabilities(options);return {schema:'ewai.coding-providers/v1',digest:state.digest,policy,policyDigest:codingPolicyDigest(policy),capabilities,defaults:{modelSelection:'native',providerSelection:'existing',reviews:'existing checkpoint policy'},authority:'settings-only'};}
|
|
76
|
+
export function readCodingProviders(root,options={}){return projection(readProjectDocument(root),options);}
|
|
77
|
+
export function saveCodingProviders(root,input,options={}){
|
|
78
|
+
closed(input,['confirmed','expectedDigest','policy'],'Provider settings request');if(input.confirmed!==true)fail('Confirm provider setting changes before saving.');if(!Object.hasOwn(input,'policy'))fail('Provide a complete policy, or null to restore existing behaviour.');
|
|
79
|
+
const policy=input.policy===null?null:normaliseCodingProviders(input.policy);
|
|
80
|
+
const state=editProjectDocument(root,input.expectedDigest,document=>{if(policy===null)document.delete('coding_providers');else document.set('coding_providers',policy);});return projection(state,options);
|
|
81
|
+
}
|
package/src/companion.mjs
CHANGED
|
@@ -6,16 +6,19 @@ import { install } from './install.mjs';
|
|
|
6
6
|
import { detectExistingCodebase } from './project.mjs';
|
|
7
7
|
import { projectPaths, resolveProjectRoot } from './paths.mjs';
|
|
8
8
|
import { dashboardHandoffStatus } from './runtime/dashboard-handoffs.mjs';
|
|
9
|
+
import {readCodingProviders,resolveCodingProviders,modelGuidance} from './coding-providers.mjs';
|
|
9
10
|
|
|
10
|
-
const HOSTS = ['claude', 'codex', 'antigravity'];
|
|
11
|
+
const HOSTS = ['claude', 'codex', 'antigravity', 'grok'];
|
|
11
12
|
const HOST_LABELS = {
|
|
12
13
|
claude: 'Claude Code',
|
|
13
14
|
codex: 'Codex',
|
|
15
|
+
grok: 'Grok Build',
|
|
14
16
|
antigravity: 'Google Antigravity'
|
|
15
17
|
};
|
|
16
18
|
const HOST_COMMANDS = {
|
|
17
19
|
claude: 'claude',
|
|
18
20
|
codex: 'codex',
|
|
21
|
+
grok: 'grok',
|
|
19
22
|
antigravity: 'agy',
|
|
20
23
|
};
|
|
21
24
|
|
|
@@ -102,6 +105,7 @@ export function buildCompanionPrompt(state) {
|
|
|
102
105
|
export function hostInvocation(host, prompt) {
|
|
103
106
|
if (host === 'claude') return { command: 'claude', args: [prompt] };
|
|
104
107
|
if (host === 'codex') return { command: 'codex', args: [prompt] };
|
|
108
|
+
if (host === 'grok') return { command: 'grok', args: [prompt] };
|
|
105
109
|
if (host === 'antigravity') return { command: 'agy', args: ['--prompt-interactive', prompt] };
|
|
106
110
|
throw new Error(`Unsupported EWAI agent host: ${host}`);
|
|
107
111
|
}
|
|
@@ -209,7 +213,10 @@ export async function runCompanion(options = {}) {
|
|
|
209
213
|
const errorOutput = options.errorOutput ?? process.stderr;
|
|
210
214
|
const env = options.env ?? process.env;
|
|
211
215
|
const state = projectState(options.cwd ?? process.cwd());
|
|
212
|
-
|
|
216
|
+
let policy=null;
|
|
217
|
+
if(state.initialized)try{policy=readCodingProviders(state.projectRoot,{probe:()=>false}).policy;}catch(error){errorOutput.write(error.message+'\n');return 1;}
|
|
218
|
+
const policyPrimary=policy&&policy.primary!=='existing'&&policy.primary!=='auto'?policy.primary:undefined;
|
|
219
|
+
const preferredValue = String(options.preferredHost ?? env.EWAI_HOST ?? policyPrimary ?? '').toLowerCase();
|
|
213
220
|
const preferred = preferredValue === 'agy' ? 'antigravity' : preferredValue;
|
|
214
221
|
const preferredStatuses = HOSTS.includes(preferred)
|
|
215
222
|
? detectHostStatuses({ env, probe: options.probeHost, hosts: [preferred] }) : [];
|
|
@@ -229,15 +236,23 @@ export async function runCompanion(options = {}) {
|
|
|
229
236
|
}
|
|
230
237
|
|
|
231
238
|
let host = selectAvailableHost(available, preferred);
|
|
239
|
+
if(policy&&policy.primary!=='existing'&&!options.preferredHost&&!env.EWAI_HOST){
|
|
240
|
+
try{host=resolveCodingProviders({...policy,secondary:'off',tertiary:'off'},{available}).primary;}
|
|
241
|
+
catch(error){errorOutput.write(error.message+'\n');return 1;}
|
|
242
|
+
}
|
|
232
243
|
if (!host && available.length > 1) {
|
|
233
244
|
if (!options.verbose) output.write(`${formatHostAvailability(statuses)}\n`);
|
|
234
245
|
host = await askForHost(available, { input, output });
|
|
235
246
|
}
|
|
236
247
|
if (!host) {
|
|
237
|
-
errorOutput.write(`EWAI found ${available.map((item) => HOST_LABELS[item]).join(', ')}, but cannot choose interactively in this session. Set EWAI_HOST to claude, codex, or antigravity.\n`);
|
|
248
|
+
errorOutput.write(`EWAI found ${available.map((item) => HOST_LABELS[item]).join(', ')}, but cannot choose interactively in this session. Set EWAI_HOST to claude, codex, grok, or antigravity.\n`);
|
|
238
249
|
return 1;
|
|
239
250
|
}
|
|
240
251
|
|
|
252
|
+
let guidance='';try{
|
|
253
|
+
if(policy&&!policy.pool.includes(host))throw new Error('The selected CLI is outside the provider pool. Choose an eligible host or explicitly update the provider settings.');
|
|
254
|
+
guidance=modelGuidance(host,policy);
|
|
255
|
+
}catch(error){errorOutput.write(error.message+'\n');return 1;}
|
|
241
256
|
const prepareHost = options.prepareHost ?? ((selectedHost) => refreshHostSkills(selectedHost));
|
|
242
257
|
try {
|
|
243
258
|
await prepareHost(host);
|
|
@@ -249,7 +264,7 @@ export async function runCompanion(options = {}) {
|
|
|
249
264
|
|
|
250
265
|
output.write(`EWAI · ${basename(state.projectRoot)} → ${HOST_LABELS[host]}\n`);
|
|
251
266
|
const orchestratedState = { ...state, orchestrator: host, startup: companionStartupPlan(state) };
|
|
252
|
-
const invocation = hostInvocation(host, buildCompanionPrompt(orchestratedState));
|
|
267
|
+
const invocation = hostInvocation(host, buildCompanionPrompt(orchestratedState)+guidance);
|
|
253
268
|
const hostEnv = { ...env, EWAI_ORCHESTRATOR: host };
|
|
254
269
|
const launch = options.spawnHost ?? ((value) => spawnInteractive(value, { cwd: state.projectRoot, env: hostEnv }));
|
|
255
270
|
return launch(invocation, orchestratedState);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {constants,openSync,closeSync,fstatSync,lstatSync,readFileSync,writeFileSync,renameSync,unlinkSync,realpathSync,fsyncSync,existsSync} from 'node:fs';
|
|
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';
|
|
6
|
+
|
|
7
|
+
function fail(message,statusCode=409){const error=new Error(message);error.statusCode=statusCode;error.configurationError=true;throw error;}
|
|
8
|
+
function safePath(paths){
|
|
9
|
+
const root=realpathSync(paths.projectRoot),parent=dirname(paths.configPath),rel=relative(root,realpathSync(parent));
|
|
10
|
+
if(rel.startsWith('..')||isAbsolute(rel))fail('Settings require a safe project configuration path.',403);
|
|
11
|
+
let cursor=parent;
|
|
12
|
+
while(cursor!==paths.projectRoot){if(lstatSync(cursor).isSymbolicLink())fail('Settings cannot use a symbolic configuration directory.',403);const next=dirname(cursor);if(next===cursor)fail('Settings require a safe project configuration path.',403);cursor=next;}
|
|
13
|
+
const stat=lstatSync(paths.configPath);
|
|
14
|
+
if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1)fail('Settings require a regular, non-symbolic configuration file.',403);
|
|
15
|
+
if(stat.size>2*1024*1024)fail('The project configuration is too large for settings editing.',413);
|
|
16
|
+
return stat;
|
|
17
|
+
}
|
|
18
|
+
export function readProjectDocument(projectRoot){
|
|
19
|
+
const paths=projectPaths(projectRoot);let fd;
|
|
20
|
+
try{
|
|
21
|
+
const stat=safePath(paths);fd=openSync(paths.configPath,constants.O_RDONLY|constants.O_NOFOLLOW);
|
|
22
|
+
const opened=fstatSync(fd);if(opened.ino!==stat.ino||opened.dev!==stat.dev)fail('Project configuration changed. Reload the settings and try again.');
|
|
23
|
+
const bytes=readFileSync(fd);if(bytes.length>2*1024*1024)fail('The project configuration is too large for settings editing.',413);
|
|
24
|
+
const document=YAML.parseDocument(bytes.toString('utf8'));
|
|
25
|
+
if(document.errors.length)fail('Project configuration contains invalid YAML. Fix it before editing settings.',422);
|
|
26
|
+
let config;try{config=document.toJS({maxAliasCount:100});}catch{fail('The project configuration cannot be safely read.',422);}
|
|
27
|
+
if(config?.schema!=='ewai.project/v1'||config?.specs?.root!==paths.specsRelative)fail('Project configuration and SPECS locator must agree before editing settings.',422);
|
|
28
|
+
return {paths,document,config,digest:'sha256:'+createHash('sha256').update(bytes).digest('hex'),mode:stat.mode&0o777};
|
|
29
|
+
}catch(error){if(error.configurationError)throw error;fail('Project settings could not be read. Check configuration and file permissions.');}
|
|
30
|
+
finally{if(fd!==undefined)closeSync(fd);}
|
|
31
|
+
}
|
|
32
|
+
export function editProjectDocument(projectRoot,expectedDigest,apply){
|
|
33
|
+
if(!/^sha256:[a-f0-9]{64}$/.test(String(expectedDigest??'')))fail('Reload settings before saving.',400);
|
|
34
|
+
const paths=projectPaths(projectRoot),lock=paths.configPath+'.configuration.lock';let lockFd,tempFd,temporary;
|
|
35
|
+
try{
|
|
36
|
+
safePath(paths);
|
|
37
|
+
if(existsSync(paths.configPath+'.dashboard-preferences.lock'))fail('Settings are being edited. Retry after that save finishes.');
|
|
38
|
+
try{lockFd=openSync(lock,'wx',0o600);}catch{fail('Settings are being edited. Retry after that save finishes.');}
|
|
39
|
+
const before=readProjectDocument(projectRoot);if(before.digest!==expectedDigest)fail('Project configuration changed. Reload the settings and try again.');
|
|
40
|
+
apply(before.document,before.config);
|
|
41
|
+
const content=before.document.toString();
|
|
42
|
+
temporary=paths.configPath+'.settings-'+randomUUID()+'.tmp';tempFd=openSync(temporary,'wx',before.mode);
|
|
43
|
+
writeFileSync(tempFd,content,'utf8');fsyncSync(tempFd);closeSync(tempFd);tempFd=undefined;
|
|
44
|
+
if(readProjectDocument(projectRoot).digest!==before.digest)fail('Project configuration changed. Reload the settings and try again.');
|
|
45
|
+
safePath(paths);renameSync(temporary,paths.configPath);temporary=undefined;
|
|
46
|
+
return readProjectDocument(projectRoot);
|
|
47
|
+
}catch(error){if(error.configurationError||error.codingProviderError||error.dashboardPreferenceError)throw error;fail('Project settings could not be saved. Check configuration and file permissions.');}
|
|
48
|
+
finally{if(tempFd!==undefined)closeSync(tempFd);if(temporary)unlinkSync(temporary);if(lockFd!==undefined){closeSync(lockFd);unlinkSync(lock);}}
|
|
49
|
+
}
|