arkgate 3.8.3 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/README.md +94 -345
- package/bin/ark-mcp-runtime.mjs +137 -11
- package/bin/lib/agent-gates.mjs +4 -0
- package/bin/lib/ci-and-commands.mjs +28 -21
- package/bin/lib/doctor-plan.mjs +37 -28
- package/bin/lib/hook-templates.mjs +13 -9
- package/bin/lib/host-support-matrix.mjs +64 -4
- package/bin/lib/install-migrate.mjs +92 -0
- package/bin/lib/managed-upgrade.mjs +2 -0
- package/bin/lib/mcp-adoption.mjs +60 -2
- package/bin/lib/post-green-path.mjs +2 -2
- package/bin/lib/skill-install.mjs +46 -2
- package/bin/lib/start-preview.mjs +13 -1
- package/bin/lib/write-path-capabilities.mjs +67 -18
- package/bin/lib/write-path-detect.mjs +11 -7
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/README.md +70 -0
- package/docs/agent-guide.md +59 -17
- package/docs/ai-gates.md +97 -11
- package/docs/develop.md +127 -0
- package/docs/enthusiast/README.md +2 -0
- package/docs/package-surface.md +3 -3
- package/docs/product-voice.md +194 -0
- package/docs/use.md +88 -0
- package/package.json +5 -1
- package/server.json +2 -2
- package/templates/hooks/opencode-ark-write-gate.mjs +85 -0
- package/templates/skills/ark-autopilot.md +20 -7
- package/templates/skills/ark-explore.md +17 -4
|
@@ -21,11 +21,15 @@ import {
|
|
|
21
21
|
} from './codex-home.mjs';
|
|
22
22
|
import {
|
|
23
23
|
PREFERRED_MCP_BIN,
|
|
24
|
+
antigravityHooks,
|
|
24
25
|
claudeSettings,
|
|
25
26
|
codexHooks,
|
|
26
27
|
codexProjectConfig,
|
|
27
28
|
grokHooks,
|
|
28
29
|
grokProjectConfig,
|
|
30
|
+
mergeAntigravityArkHook,
|
|
31
|
+
mergeOpencodeArkMcp,
|
|
32
|
+
opencodeProjectConfig,
|
|
29
33
|
} from './hook-templates.mjs';
|
|
30
34
|
import {
|
|
31
35
|
hasCheckArchitectureScript,
|
|
@@ -60,6 +64,7 @@ import {
|
|
|
60
64
|
import { detectDeployPathQuality } from './deploy-path.mjs';
|
|
61
65
|
import {
|
|
62
66
|
stripMcpServerArgs,
|
|
67
|
+
stripOpencodeMcpCommand,
|
|
63
68
|
COMMAND_GATE_TEXT_FILES,
|
|
64
69
|
COMMAND_GATE_JSON_FILES,
|
|
65
70
|
PREFERRED_CHECK_BIN,
|
|
@@ -175,6 +180,14 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
|
|
|
175
180
|
add('.grok/config.toml', grokProjectConfig(root));
|
|
176
181
|
add('.grok/hooks/ark-write-gate.json', grokHooks(root));
|
|
177
182
|
}
|
|
183
|
+
if (selectedTools.has('antigravity')) {
|
|
184
|
+
add('.agents/hooks.json', antigravityHooks(root));
|
|
185
|
+
// Still useful for Gemini CLI / legacy Gemini consumers sharing the tree.
|
|
186
|
+
add('GEMINI.md', instructionRule(root));
|
|
187
|
+
}
|
|
188
|
+
if (selectedTools.has('opencode')) {
|
|
189
|
+
add('opencode.json', opencodeProjectConfig(root), 'gate', 'json-merge');
|
|
190
|
+
}
|
|
178
191
|
if (selectedTools.has('windsurf')) add('.windsurf/rules/ark.md', instructionRule(root));
|
|
179
192
|
if (selectedTools.has('cline')) add('.clinerules/ark.md', instructionRule(root));
|
|
180
193
|
if (selectedTools.has('copilot')) {
|
|
@@ -238,6 +251,27 @@ export function runMigrateCommands(root) {
|
|
|
238
251
|
} catch {
|
|
239
252
|
continue;
|
|
240
253
|
}
|
|
254
|
+
// OpenCode: mcp.ark.command is a single argv array (type: local), not mcpServers.ark.args.
|
|
255
|
+
if (rel === 'opencode.json') {
|
|
256
|
+
const ark = json?.mcp?.ark;
|
|
257
|
+
if (!ark || typeof ark !== 'object' || !Array.isArray(ark.command)) continue;
|
|
258
|
+
const argv = ark.command.filter((entry) => typeof entry === 'string');
|
|
259
|
+
if (argv.length === 0) continue;
|
|
260
|
+
// Drop runners + any ark* bin names; keep only server flags (e.g. --root .).
|
|
261
|
+
const binArgs = stripOpencodeMcpCommand(argv);
|
|
262
|
+
const parts = execCommandParts(root, PREFERRED_MCP_BIN, binArgs);
|
|
263
|
+
const preferredArgv = [parts.command, ...parts.args];
|
|
264
|
+
if (JSON.stringify(argv) === JSON.stringify(preferredArgv)) continue;
|
|
265
|
+
json.mcp.ark = {
|
|
266
|
+
...ark,
|
|
267
|
+
type: ark.type ?? 'local',
|
|
268
|
+
command: preferredArgv,
|
|
269
|
+
enabled: ark.enabled !== false,
|
|
270
|
+
};
|
|
271
|
+
fs.writeFileSync(full, `${JSON.stringify(json, null, 2)}\n`);
|
|
272
|
+
changed.push(rel);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
241
275
|
const ark = json?.mcpServers?.ark;
|
|
242
276
|
if (!ark) continue;
|
|
243
277
|
const binArgs = stripMcpServerArgs(ark.args);
|
|
@@ -316,6 +350,14 @@ export function runInstallAgentGates(args) {
|
|
|
316
350
|
? 'no active host detected'
|
|
317
351
|
: 'default set — no agent config dirs found';
|
|
318
352
|
console.log(`Agent gates for: ${[...tools].sort().join(', ')} (${toolSource})`);
|
|
353
|
+
// Progressive disclosure (3.9.0): compact = router; skills-only = expert pack.
|
|
354
|
+
if (!args.json) {
|
|
355
|
+
const host = [...tools][0];
|
|
356
|
+
const later = host ? ` --tools ${host}` : '';
|
|
357
|
+
if (args.compact) console.log(`Profile: compact router. Expert skills later: ${arkCommand(root, 'ark-check', `--install-agent-gates --skills-only${later} --force`)}`);
|
|
358
|
+
else if (args.skillsOnly) console.log('Profile: expert skill pack only (refreshes /ark-*; leaves customized gates).');
|
|
359
|
+
else console.log('Profile: full agent gates. Compact-only onboarding: ark start.');
|
|
360
|
+
}
|
|
319
361
|
// --skills-only refreshes just the canonical /ark-* skills, which are safe to
|
|
320
362
|
// overwrite (they track the package). The gate/instruction files (AGENTS.md,
|
|
321
363
|
// settings.json, CI workflow, rules) are the ones users customize, so a plain
|
|
@@ -367,6 +409,43 @@ export function runInstallAgentGates(args) {
|
|
|
367
409
|
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
368
410
|
return writeTemplate(root, relativePath, merged, true);
|
|
369
411
|
}
|
|
412
|
+
if (relativePath === 'opencode.json') {
|
|
413
|
+
const fullPath = path.join(root, relativePath);
|
|
414
|
+
let existing = '';
|
|
415
|
+
try {
|
|
416
|
+
existing = fs.readFileSync(fullPath, 'utf8');
|
|
417
|
+
} catch {
|
|
418
|
+
// Missing project config → write the generated Ark MCP block.
|
|
419
|
+
}
|
|
420
|
+
if (!existing) {
|
|
421
|
+
return writeTemplate(root, relativePath, content, true);
|
|
422
|
+
}
|
|
423
|
+
const merged = mergeOpencodeArkMcp(existing, content);
|
|
424
|
+
if (merged == null) {
|
|
425
|
+
return { relativePath, status: 'skipped-non-ark' };
|
|
426
|
+
}
|
|
427
|
+
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
428
|
+
return writeTemplate(root, relativePath, merged, true);
|
|
429
|
+
}
|
|
430
|
+
if (relativePath === '.agents/hooks.json') {
|
|
431
|
+
const fullPath = path.join(root, relativePath);
|
|
432
|
+
let existing = '';
|
|
433
|
+
try {
|
|
434
|
+
existing = fs.readFileSync(fullPath, 'utf8');
|
|
435
|
+
} catch {
|
|
436
|
+
// Missing hooks file → write generated ark-write-gate map.
|
|
437
|
+
}
|
|
438
|
+
if (!existing) {
|
|
439
|
+
return writeTemplate(root, relativePath, content, true);
|
|
440
|
+
}
|
|
441
|
+
const merged = mergeAntigravityArkHook(existing, content);
|
|
442
|
+
if (merged == null) {
|
|
443
|
+
return { relativePath, status: 'skipped-non-ark' };
|
|
444
|
+
}
|
|
445
|
+
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
446
|
+
// Upsert ark-write-gate without requiring --force; never wipe sibling named hooks.
|
|
447
|
+
return writeTemplate(root, relativePath, merged, true);
|
|
448
|
+
}
|
|
370
449
|
return writeTemplate(
|
|
371
450
|
root,
|
|
372
451
|
relativePath,
|
|
@@ -516,6 +595,19 @@ export function runInstallAgentGates(args) {
|
|
|
516
595
|
console.log(' 3. Add the package.json alias if you want `run check:architecture`:');
|
|
517
596
|
console.log(` ${checkArchitectureScriptSnippet(root)}`);
|
|
518
597
|
}
|
|
598
|
+
if (tools.has('antigravity') && !args.compact) {
|
|
599
|
+
console.log('');
|
|
600
|
+
console.log(' Antigravity: PreToolUse deny is hard for listed write tools when hooks are trusted.');
|
|
601
|
+
console.log(' - Install path: `.agents/hooks.json` (+ GEMINI.md for legacy Gemini consumers).');
|
|
602
|
+
console.log(' - Trust project hooks in the host; pair with required CI --strict-merge.');
|
|
603
|
+
}
|
|
604
|
+
if (tools.has('opencode') && !args.compact) {
|
|
605
|
+
console.log('');
|
|
606
|
+
console.log(' OpenCode write path (honest):');
|
|
607
|
+
console.log(' - Local: advisory MCP in opencode.json (optional experimental plugin only).');
|
|
608
|
+
console.log(' - Hard merge backstop: CI --strict-merge + required status check.');
|
|
609
|
+
console.log(' - Not equivalent to Claude/Grok/Antigravity PreToolUse hard-write.');
|
|
610
|
+
}
|
|
519
611
|
if ((tools.has('codex') || args.codexHome)) {
|
|
520
612
|
console.log('');
|
|
521
613
|
if (codexMcp && codexMcp.status !== 'failed') {
|
|
@@ -110,6 +110,8 @@ const HOST_SIGNALS = {
|
|
|
110
110
|
cursor: ['.cursor/mcp.json', '.cursor/rules/ark.mdc', '.cursor/commands/ark-upgrade.md'],
|
|
111
111
|
codex: ['.codex/hooks.json', '.codex/config.toml', '.agents/skills/ark-upgrade/SKILL.md'],
|
|
112
112
|
grok: ['.grok/config.toml', '.grok/hooks/ark-write-gate.json', '.grok/skills/ark-upgrade/SKILL.md'],
|
|
113
|
+
antigravity: ['.agents/hooks.json', '.agents/skills/ark-upgrade/SKILL.md'],
|
|
114
|
+
opencode: ['opencode.json', '.opencode/skills/ark-upgrade/SKILL.md'],
|
|
113
115
|
windsurf: ['.windsurf/rules/ark.md', '.windsurf/workflows/ark-upgrade.md'],
|
|
114
116
|
cline: ['.clinerules/ark.md', '.clinerules/workflows/ark-upgrade.md'],
|
|
115
117
|
copilot: ['.github/copilot-instructions.md', '.github/prompts/ark-upgrade.prompt.md'],
|
package/bin/lib/mcp-adoption.mjs
CHANGED
|
@@ -25,8 +25,9 @@ export const COMMAND_GATE_TEXT_FILES = [
|
|
|
25
25
|
'.clinerules/ark.md', '.github/copilot-instructions.md', '.kiro/steering/ark.md',
|
|
26
26
|
'.roo/rules/ark.md', '.continue/rules/ark.md', 'GEMINI.md', 'package.json',
|
|
27
27
|
'.grok/hooks/ark-write-gate.json', '.grok/config.toml', '.codex/config.toml',
|
|
28
|
+
'.agents/hooks.json',
|
|
28
29
|
];
|
|
29
|
-
export const COMMAND_GATE_JSON_FILES = ['.mcp.json', '.cursor/mcp.json'];
|
|
30
|
+
export const COMMAND_GATE_JSON_FILES = ['.mcp.json', '.cursor/mcp.json', 'opencode.json'];
|
|
30
31
|
// Primary CLI names (product) + one-major aliases. migrate-commands must strip ALL of these
|
|
31
32
|
// before re-emitting a single preferred bin — otherwise a partial rename leaves
|
|
32
33
|
// args: ["ark-mcp", "arkgate-mcp", ...] which breaks stdio MCP hosts.
|
|
@@ -61,6 +62,34 @@ export function stripMcpServerArgs(args) {
|
|
|
61
62
|
return kept.length > 0 ? kept : ['--root', '.', '--config', 'ark.config.json'];
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* OpenCode `mcp.ark.command` is a full argv (runner + bin + flags).
|
|
67
|
+
* Strip runners and any ark* bin names so migrate can re-emit preferred command+args.
|
|
68
|
+
*/
|
|
69
|
+
export function stripOpencodeMcpCommand(command) {
|
|
70
|
+
if (!Array.isArray(command) || command.length === 0) {
|
|
71
|
+
return ['--root', '.', '--config', 'ark.config.json'];
|
|
72
|
+
}
|
|
73
|
+
const runners = new Set(['npx', 'yarn', 'pnpm', 'node', 'bun']);
|
|
74
|
+
const kept = [];
|
|
75
|
+
for (const entry of command) {
|
|
76
|
+
if (typeof entry !== 'string') continue;
|
|
77
|
+
const base = path.basename(entry.replace(/\\/g, '/'));
|
|
78
|
+
if (
|
|
79
|
+
runners.has(base) ||
|
|
80
|
+
MCP_RUNNER_ARGV.has(entry) ||
|
|
81
|
+
ARK_MCP_BINS.has(base) ||
|
|
82
|
+
ARK_MCP_BINS.has(entry) ||
|
|
83
|
+
ARK_CHECK_BINS.has(base) ||
|
|
84
|
+
ARK_CLI_BINS.has(base)
|
|
85
|
+
) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
kept.push(entry);
|
|
89
|
+
}
|
|
90
|
+
return kept.length > 0 ? kept : ['--root', '.', '--config', 'ark.config.json'];
|
|
91
|
+
}
|
|
92
|
+
|
|
64
93
|
/** True when mcpServers.ark.args list more than one Ark MCP bin (broken dual rename). */
|
|
65
94
|
export function mcpArgsHaveDuplicateBins(args) {
|
|
66
95
|
if (!Array.isArray(args)) return false;
|
|
@@ -77,6 +106,12 @@ export function brokenMcpGateFiles(root) {
|
|
|
77
106
|
} catch {
|
|
78
107
|
continue;
|
|
79
108
|
}
|
|
109
|
+
// OpenCode uses mcp.ark.command[] (single argv); Claude/Cursor use mcpServers.ark.args.
|
|
110
|
+
if (rel === 'opencode.json') {
|
|
111
|
+
const command = json?.mcp?.ark?.command;
|
|
112
|
+
if (Array.isArray(command) && mcpArgsHaveDuplicateBins(command)) bad.push(rel);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
80
115
|
const ark = json?.mcpServers?.ark;
|
|
81
116
|
if (ark && mcpArgsHaveDuplicateBins(ark.args)) bad.push(rel);
|
|
82
117
|
}
|
|
@@ -159,9 +194,32 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
159
194
|
],
|
|
160
195
|
toolsFlag: 'codex',
|
|
161
196
|
},
|
|
197
|
+
{
|
|
198
|
+
host: 'antigravity',
|
|
199
|
+
dir: '.agents',
|
|
200
|
+
skill: (n) => path.join(root, '.agents', 'skills', n, 'SKILL.md'),
|
|
201
|
+
extras: [['.agents/hooks.json', 'write-gate hook']],
|
|
202
|
+
toolsFlag: 'antigravity',
|
|
203
|
+
// Only when hooks.json is present — `.agents/skills` alone is Codex scope.
|
|
204
|
+
presentIf: () => fs.existsSync(path.join(root, '.agents', 'hooks.json')),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
host: 'opencode',
|
|
208
|
+
dir: '.opencode',
|
|
209
|
+
skill: (n) => path.join(root, '.opencode', 'skills', n, 'SKILL.md'),
|
|
210
|
+
extras: [['opencode.json', 'project MCP config']],
|
|
211
|
+
toolsFlag: 'opencode',
|
|
212
|
+
presentIf: () =>
|
|
213
|
+
fs.existsSync(path.join(root, 'opencode.json')) ||
|
|
214
|
+
fs.existsSync(path.join(root, '.opencode')),
|
|
215
|
+
},
|
|
162
216
|
];
|
|
163
217
|
for (const h of hostChecks) {
|
|
164
|
-
|
|
218
|
+
const present =
|
|
219
|
+
typeof h.presentIf === 'function'
|
|
220
|
+
? h.presentIf()
|
|
221
|
+
: fs.existsSync(path.join(root, h.dir));
|
|
222
|
+
if (!present) continue;
|
|
165
223
|
const missingSkills = skillNames.filter((n) => !fs.existsSync(h.skill(n)));
|
|
166
224
|
const missingExtras = h.extras.filter(([rel]) => !fs.existsSync(path.join(root, rel)));
|
|
167
225
|
const complete = missingSkills.length === 0 && missingExtras.length === 0;
|
|
@@ -17,11 +17,11 @@ export const POST_GREEN_PRIMARY_SKILL = '/ark-explore';
|
|
|
17
17
|
* Chained: explore shape-focus then autopilot only to apply B with user OK.
|
|
18
18
|
*/
|
|
19
19
|
export const POST_GREEN_PRIMARY_ACTION =
|
|
20
|
-
'
|
|
20
|
+
'Shape residual (design-weak): edges are clean, design is not finished. Map with /ark-explore shape-focus → dual-plan B; apply B only via /ark-autopilot with your OK. Empty plan A is not done; pattern bets are never mechanical-safe.';
|
|
21
21
|
|
|
22
22
|
/** Short label for tables / metrics. */
|
|
23
23
|
export const POST_GREEN_PRIMARY_SHORT =
|
|
24
|
-
'/ark-explore shape-focus → /ark-autopilot (apply B with OK) #
|
|
24
|
+
'/ark-explore shape-focus → /ark-autopilot (apply B with OK) # Shape residual';
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* @param {{ designWeak?: boolean } | null | undefined} designFitness
|
|
@@ -8,18 +8,28 @@ import { arkCommand } from '../ark-shared.mjs';
|
|
|
8
8
|
import { codexPromptsDir, codexSkillsDir } from './codex-home.mjs';
|
|
9
9
|
import { __packageRoot, isCompactRouterAgentsContent, readJson } from './gate-files.mjs';
|
|
10
10
|
|
|
11
|
+
/** Alias map so --tools agy installs the antigravity profile. */
|
|
12
|
+
const TOOL_ALIASES = Object.freeze({
|
|
13
|
+
agy: 'antigravity',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function normalizeToolId(value) {
|
|
17
|
+
const id = String(value).trim().toLowerCase();
|
|
18
|
+
return TOOL_ALIASES[id] ?? id;
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
export function normalizeToolsList(tools) {
|
|
12
22
|
if (tools == null) return [];
|
|
13
23
|
if (Array.isArray(tools)) {
|
|
14
24
|
return tools
|
|
15
25
|
.flatMap((t) => String(t).split(','))
|
|
16
|
-
.map(
|
|
26
|
+
.map(normalizeToolId)
|
|
17
27
|
.filter(Boolean);
|
|
18
28
|
}
|
|
19
29
|
if (typeof tools === 'string') {
|
|
20
30
|
return tools
|
|
21
31
|
.split(',')
|
|
22
|
-
.map(
|
|
32
|
+
.map(normalizeToolId)
|
|
23
33
|
.filter(Boolean);
|
|
24
34
|
}
|
|
25
35
|
return [];
|
|
@@ -77,6 +87,25 @@ export function detectActiveAgentHost(env = process.env) {
|
|
|
77
87
|
) {
|
|
78
88
|
return 'codex';
|
|
79
89
|
}
|
|
90
|
+
// Google Antigravity / agy CLI
|
|
91
|
+
if (
|
|
92
|
+
envTruthy(env.ANTIGRAVITY) ||
|
|
93
|
+
envTruthy(env.AGY) ||
|
|
94
|
+
env.ANTIGRAVITY_WORKSPACE ||
|
|
95
|
+
env.AGY_WORKSPACE ||
|
|
96
|
+
/antigravity/i.test(String(env.TERM_PROGRAM ?? ''))
|
|
97
|
+
) {
|
|
98
|
+
return 'antigravity';
|
|
99
|
+
}
|
|
100
|
+
// OpenCode
|
|
101
|
+
if (
|
|
102
|
+
envTruthy(env.OPENCODE) ||
|
|
103
|
+
env.OPENCODE_SESSION_ID ||
|
|
104
|
+
env.OPENCODE_CONFIG ||
|
|
105
|
+
envTruthy(env.OPENCODE_CLI)
|
|
106
|
+
) {
|
|
107
|
+
return 'opencode';
|
|
108
|
+
}
|
|
80
109
|
return null;
|
|
81
110
|
}
|
|
82
111
|
|
|
@@ -101,6 +130,15 @@ export function resolveTools(args) {
|
|
|
101
130
|
if (fs.existsSync(path.join(root, '.cursor'))) detected.add('cursor');
|
|
102
131
|
if (fs.existsSync(path.join(root, '.codex'))) detected.add('codex');
|
|
103
132
|
if (fs.existsSync(path.join(root, '.grok'))) detected.add('grok');
|
|
133
|
+
// Antigravity project hooks (distinct from Codex-only `.agents/skills`).
|
|
134
|
+
if (fs.existsSync(path.join(root, '.agents', 'hooks.json'))) detected.add('antigravity');
|
|
135
|
+
if (
|
|
136
|
+
fs.existsSync(path.join(root, 'opencode.json')) ||
|
|
137
|
+
fs.existsSync(path.join(root, 'opencode.jsonc')) ||
|
|
138
|
+
fs.existsSync(path.join(root, '.opencode'))
|
|
139
|
+
) {
|
|
140
|
+
detected.add('opencode');
|
|
141
|
+
}
|
|
104
142
|
if (fs.existsSync(path.join(root, '.windsurf'))) detected.add('windsurf');
|
|
105
143
|
// .clinerules can also be a single FILE (older Cline convention); only a directory
|
|
106
144
|
// can receive .clinerules/ark.md, so a file must not trigger detection.
|
|
@@ -136,6 +174,8 @@ export const KNOWN_TOOLS = [
|
|
|
136
174
|
'cursor',
|
|
137
175
|
'codex',
|
|
138
176
|
'grok',
|
|
177
|
+
'antigravity',
|
|
178
|
+
'opencode',
|
|
139
179
|
'windsurf',
|
|
140
180
|
'cline',
|
|
141
181
|
'copilot',
|
|
@@ -161,6 +201,10 @@ export const SKILL_TOOL_TARGETS = {
|
|
|
161
201
|
codex: (name) => `.agents/skills/${name}/SKILL.md`,
|
|
162
202
|
// Grok Build: project skills at .grok/skills/<name>/SKILL.md (slash-invocable).
|
|
163
203
|
grok: (name) => `.grok/skills/${name}/SKILL.md`,
|
|
204
|
+
// Antigravity loads Agent Skills from `.agents/skills` (shared path with Codex).
|
|
205
|
+
antigravity: (name) => `.agents/skills/${name}/SKILL.md`,
|
|
206
|
+
// OpenCode project skills under `.opencode/skills`.
|
|
207
|
+
opencode: (name) => `.opencode/skills/${name}/SKILL.md`,
|
|
164
208
|
windsurf: (name) => `.windsurf/workflows/${name}.md`,
|
|
165
209
|
cline: (name) => `.clinerules/workflows/${name}.md`,
|
|
166
210
|
copilot: (name) => `.github/prompts/${name}.prompt.md`,
|
|
@@ -5,7 +5,14 @@ import os from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { arkCommand, buildArchitectureRecommendation } from '../ark-shared.mjs';
|
|
7
7
|
import { compactAgentInstructions, instructionRule, mcpJson } from './ci-and-commands.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
antigravityHooks,
|
|
10
|
+
claudeSettings,
|
|
11
|
+
codexHooks,
|
|
12
|
+
grokHooks,
|
|
13
|
+
grokProjectConfig,
|
|
14
|
+
opencodeProjectConfig,
|
|
15
|
+
} from './hook-templates.mjs';
|
|
9
16
|
|
|
10
17
|
const COMPACT_HOST_TEMPLATES = {
|
|
11
18
|
claude: (root) => [
|
|
@@ -16,8 +23,13 @@ const COMPACT_HOST_TEMPLATES = {
|
|
|
16
23
|
['.grok/config.toml', grokProjectConfig(root)],
|
|
17
24
|
['.grok/hooks/ark-write-gate.json', grokHooks(root)],
|
|
18
25
|
],
|
|
26
|
+
antigravity: (root) => [
|
|
27
|
+
['.agents/hooks.json', antigravityHooks(root)],
|
|
28
|
+
['.mcp.json', mcpJson(root)],
|
|
29
|
+
],
|
|
19
30
|
cursor: (root) => [['.cursor/mcp.json', mcpJson(root)]],
|
|
20
31
|
codex: (root) => [['.codex/hooks.json', codexHooks(root)]],
|
|
32
|
+
opencode: (root) => [['opencode.json', opencodeProjectConfig(root)]],
|
|
21
33
|
windsurf: (root) => [['.windsurf/rules/ark.md', instructionRule(root)]],
|
|
22
34
|
cline: (root) => [['.clinerules/ark.md', instructionRule(root)]],
|
|
23
35
|
copilot: (root) => [['.github/copilot-instructions.md', instructionRule(root)]],
|
|
@@ -194,9 +194,11 @@ function commandArkMcpInvocation(command) {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
function requiredWriteOperations(relativePath) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
if (relativePath.startsWith('.grok/')) return ['write', 'search_replace'];
|
|
198
|
+
if (relativePath === '.agents/hooks.json' || relativePath.startsWith('.agents/')) {
|
|
199
|
+
return ['write_to_file', 'replace_file_content', 'multi_replace_file_content'];
|
|
200
|
+
}
|
|
201
|
+
return ['Write', 'Edit', 'MultiEdit'];
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
function matcherOperations(relativePath, matcher) {
|
|
@@ -248,25 +250,39 @@ function tomlArkMcpIsValid(text) {
|
|
|
248
250
|
return primary.length === 1 && tomlMcpBlockIsValid(primary[0].block);
|
|
249
251
|
}
|
|
250
252
|
|
|
253
|
+
function collectPreToolUseGroups(parsed) {
|
|
254
|
+
// Claude/Grok/Codex: { hooks: { PreToolUse: [...] } }
|
|
255
|
+
if (Array.isArray(parsed?.hooks?.PreToolUse)) return parsed.hooks.PreToolUse;
|
|
256
|
+
// Antigravity: { "named-hook": { PreToolUse: [...] }, ... }
|
|
257
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
258
|
+
const groups = [];
|
|
259
|
+
for (const value of Object.values(parsed)) {
|
|
260
|
+
if (value && typeof value === 'object' && Array.isArray(value.PreToolUse)) {
|
|
261
|
+
groups.push(...value.PreToolUse);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return groups;
|
|
265
|
+
}
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
|
|
251
269
|
function hookEvidence(root, relativePath) {
|
|
252
270
|
const text = readText(path.join(root, relativePath));
|
|
253
271
|
let hooks = [];
|
|
254
272
|
try {
|
|
255
|
-
const groups = JSON.parse(text)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
);
|
|
269
|
-
}
|
|
273
|
+
const groups = collectPreToolUseGroups(JSON.parse(text));
|
|
274
|
+
hooks = groups.flatMap((group) =>
|
|
275
|
+
Array.isArray(group?.hooks)
|
|
276
|
+
? group.hooks
|
|
277
|
+
.filter((hook) => !hook?.type || hook.type === 'command')
|
|
278
|
+
.map((hook) => ({
|
|
279
|
+
hook,
|
|
280
|
+
invocation: commandArkMcpInvocation(hook?.command),
|
|
281
|
+
operations: matcherOperations(relativePath, group.matcher),
|
|
282
|
+
}))
|
|
283
|
+
.filter((entry) => entry.invocation)
|
|
284
|
+
: []
|
|
285
|
+
);
|
|
270
286
|
} catch {
|
|
271
287
|
hooks = [];
|
|
272
288
|
}
|
|
@@ -290,6 +306,29 @@ function hookEvidence(root, relativePath) {
|
|
|
290
306
|
};
|
|
291
307
|
}
|
|
292
308
|
|
|
309
|
+
/** OpenCode local MCP: `{ mcp: { ark: { type: "local", command: [...] } } }`. */
|
|
310
|
+
function opencodeMcpIsValid(text) {
|
|
311
|
+
try {
|
|
312
|
+
const server = JSON.parse(text)?.mcp?.ark;
|
|
313
|
+
if (!server || typeof server !== 'object') return false;
|
|
314
|
+
if (server.type != null && server.type !== 'local') return false;
|
|
315
|
+
if (!Array.isArray(server.command) || server.command.length === 0) return false;
|
|
316
|
+
if (!server.command.every((value) => typeof value === 'string')) return false;
|
|
317
|
+
const [command, ...args] = server.command;
|
|
318
|
+
return mcpServerRunsArk({ command, args });
|
|
319
|
+
} catch {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function opencodeMcpEvidence(root) {
|
|
325
|
+
for (const relativePath of ['opencode.json', 'opencode.jsonc']) {
|
|
326
|
+
const text = readText(path.join(root, relativePath));
|
|
327
|
+
if (text && opencodeMcpIsValid(text)) return [relativePath];
|
|
328
|
+
}
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
|
|
293
332
|
function mcpEvidence(root, relativePath) {
|
|
294
333
|
const text = readText(path.join(root, relativePath));
|
|
295
334
|
const valid = relativePath.endsWith('.toml')
|
|
@@ -338,6 +377,7 @@ export function detectWritePathInventory(root) {
|
|
|
338
377
|
const merge = ci.failClosed ? ci.arkWorkflowFiles : [];
|
|
339
378
|
const claudeHook = hookEvidence(root, '.claude/settings.json');
|
|
340
379
|
const grokHook = hookEvidence(root, '.grok/hooks/ark-write-gate.json');
|
|
380
|
+
const antigravityHook = hookEvidence(root, '.agents/hooks.json');
|
|
341
381
|
const hosts = {
|
|
342
382
|
claude: hostRecord(
|
|
343
383
|
claudeHook.hard,
|
|
@@ -351,12 +391,21 @@ export function detectWritePathInventory(root) {
|
|
|
351
391
|
grokHook.repair,
|
|
352
392
|
merge
|
|
353
393
|
),
|
|
394
|
+
antigravity: hostRecord(
|
|
395
|
+
antigravityHook.hard,
|
|
396
|
+
// Shared project MCP (.mcp.json) is the common advisory surface; hooks own hard write.
|
|
397
|
+
mcpEvidence(root, '.mcp.json'),
|
|
398
|
+
antigravityHook.repair,
|
|
399
|
+
merge
|
|
400
|
+
),
|
|
354
401
|
cursor: hostRecord([], mcpEvidence(root, '.cursor/mcp.json'), [], merge),
|
|
355
402
|
// Codex 0.123+ emits PreToolUse for the native apply_patch handler, but some
|
|
356
403
|
// Code Mode hosts execute deferred nested writes without dispatching that
|
|
357
404
|
// project hook. Keep the installed hook as best-effort protection; do not
|
|
358
405
|
// report a hard boundary that cannot be verified for every write surface.
|
|
359
406
|
codex: hostRecord([], codexMcpEvidence(root), [], merge),
|
|
407
|
+
// OpenCode: MCP only (plugin hooks are incomplete / subagent-bypassable).
|
|
408
|
+
opencode: hostRecord([], opencodeMcpEvidence(root), [], merge),
|
|
360
409
|
};
|
|
361
410
|
|
|
362
411
|
const evidence = emptyEvidence();
|
|
@@ -11,7 +11,7 @@ import { buildWritePathCapabilityModel } from './write-path-capabilities.mjs';
|
|
|
11
11
|
|
|
12
12
|
function installToolsForHost(activeHost) {
|
|
13
13
|
return activeHost === 'unknown'
|
|
14
|
-
? 'claude,grok,cursor,codex'
|
|
14
|
+
? 'claude,grok,antigravity,cursor,codex,opencode'
|
|
15
15
|
: activeHost;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -109,22 +109,26 @@ export function detectWritePathCapabilities(root, explicitHost, attempt) {
|
|
|
109
109
|
),
|
|
110
110
|
};
|
|
111
111
|
} else if (mode === 'mcp-only') {
|
|
112
|
-
const
|
|
112
|
+
const honesty =
|
|
113
113
|
activeHost === 'codex'
|
|
114
114
|
? 'Codex local write is advisory (MCP + best-effort hooks.json — not a hard boundary; ' +
|
|
115
115
|
'not equivalent to Claude/Grok PreToolUse hard-write + repair). ' +
|
|
116
116
|
'The hard merge backstop is CI --strict-merge plus a required status check.'
|
|
117
|
-
:
|
|
118
|
-
'
|
|
117
|
+
: activeHost === 'opencode'
|
|
118
|
+
? 'OpenCode local write is advisory (MCP + optional experimental plugin — not a hard boundary; ' +
|
|
119
|
+
'not equivalent to Claude/Grok/Antigravity PreToolUse hard-write). ' +
|
|
120
|
+
'The hard merge backstop is CI --strict-merge plus a required status check.'
|
|
121
|
+
: `Active host ${activeHost} has advisory prepare-write/autoPatch tools, ` +
|
|
122
|
+
'but no hard write boundary; CI can report failure, while merge blocking requires provider policy.';
|
|
119
123
|
gap = {
|
|
120
124
|
id: 'write-path-mcp-only',
|
|
121
125
|
severity: 'info',
|
|
122
126
|
host: activeHost,
|
|
123
|
-
message:
|
|
127
|
+
message: honesty,
|
|
124
128
|
fix:
|
|
125
|
-
activeHost === 'codex'
|
|
129
|
+
activeHost === 'codex' || activeHost === 'opencode'
|
|
126
130
|
? 'Keep CI on --strict-merge and require the ark-check status on the default branch; ' +
|
|
127
|
-
`refresh
|
|
131
|
+
`refresh ${activeHost} MCP/skills with ${arkCommand(root, 'ark-check', `--install-agent-gates --tools ${activeHost}`)}`
|
|
128
132
|
: arkCommand(root, 'ark-check', `--install-agent-gates --tools ${tools}`),
|
|
129
133
|
};
|
|
130
134
|
}
|