arkgate 3.8.2 → 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 +91 -0
- package/README.md +94 -345
- package/bin/ark-mcp-runtime.mjs +137 -11
- package/bin/ark-shared.mjs +89 -2
- package/bin/ark.mjs +41 -19
- 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 +96 -1
- package/bin/lib/managed-upgrade.mjs +30 -1
- 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 +23 -7
- package/bin/lib/upgrade-command.mjs +57 -15
- 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
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
mergePostGreenTopActions,
|
|
37
37
|
isDoctorHealthyNothingToDo,
|
|
38
38
|
} from './post-green-path.mjs';
|
|
39
|
+
import { doctorWritePathHonestyMessage } from './host-support-matrix.mjs';
|
|
39
40
|
import {
|
|
40
41
|
computePureLayerOptInNudge,
|
|
41
42
|
loadGoldenPattern,
|
|
@@ -578,14 +579,20 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
578
579
|
console.log('');
|
|
579
580
|
console.log(color.bold('Operating mode'));
|
|
580
581
|
// Modes are detected states, not user-picked settings. Plain-language "what you do next".
|
|
581
|
-
|
|
582
|
+
// Never paint green (ok) under design residual — edges clean ≠ design done (product-voice).
|
|
583
|
+
const modeMark =
|
|
584
|
+
mode === 'enforce' && !designFitness.designWeak
|
|
585
|
+
? ok
|
|
586
|
+
: warn;
|
|
587
|
+
// Status lights are detected states, not user-picked settings (see docs/product-voice.md).
|
|
588
|
+
// modeTitle alone names the light — bodies must not re-prefix Suggest/Adapt/Enforce.
|
|
582
589
|
const modeHelp = {
|
|
583
590
|
suggest:
|
|
584
|
-
'
|
|
591
|
+
'thin or new tree; the contract is not yet the control plane. You do not pick this light. Next: ark start (preview), then ark start --apply; re-check with --doctor.',
|
|
585
592
|
adapt:
|
|
586
|
-
'
|
|
593
|
+
'contract and tree still disagree, or debt is open. Write path does not fully protect you yet. You do not pick this light. Next: do doctor top action #1 (often /ark-adopt, /ark-contract, or /ark-autopilot).',
|
|
587
594
|
enforce:
|
|
588
|
-
'
|
|
595
|
+
'honest coverage and clean checked edges. You arrived here; you never turn Enforce on. Next: keep the host write path and CI check; only NEW violations should fail.',
|
|
589
596
|
};
|
|
590
597
|
const modeTitle =
|
|
591
598
|
mode === 'enforce' && designFitness.designWeak
|
|
@@ -595,7 +602,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
595
602
|
modeMark,
|
|
596
603
|
`${modeTitle} — ${
|
|
597
604
|
designFitness.designWeak
|
|
598
|
-
? '
|
|
605
|
+
? 'checked edges are honest; design smells remain. Green is not elegant design. You do not pick this light. Next: one Shape door — /ark-explore shape-focus → dual-plan B; apply B only with /ark-autopilot and your OK. Empty plan A is not done.'
|
|
599
606
|
: modeHelp[mode]
|
|
600
607
|
}`
|
|
601
608
|
);
|
|
@@ -692,34 +699,25 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
692
699
|
if (showNewHere) {
|
|
693
700
|
console.log('');
|
|
694
701
|
console.log(color.bold('New here?'));
|
|
702
|
+
// Suggest residual: start → doctor only (not a competing recommend/architect curriculum).
|
|
703
|
+
line(ok, `Primary path: ${arkCommand(root, 'ark', 'start')} (preview) → ${arkCommand(root, 'ark', 'start --apply')} → re-run --doctor`);
|
|
695
704
|
if (recommendation) {
|
|
696
|
-
line(warn, `
|
|
697
|
-
if (recommendation.galleryStarter) {
|
|
698
|
-
line(ok, `Gallery starter: ${recommendation.galleryStarter}`);
|
|
699
|
-
}
|
|
705
|
+
line(warn, `Sensor shape hint (not a second curriculum): ${recommendation.archetype} — ${recommendation.label} (preset ${recommendation.preset})`);
|
|
706
|
+
if (recommendation.galleryStarter) line(ok, `Gallery starter (optional): ${recommendation.galleryStarter}`);
|
|
700
707
|
if (recommendation.policyPack) {
|
|
701
|
-
line(ok, `Policy pack: ${arkCommand(root, 'ark-check', `--apply-policy-pack ${recommendation.policyPack}`)}`);
|
|
708
|
+
line(ok, `Policy pack (optional expert): ${arkCommand(root, 'ark-check', `--apply-policy-pack ${recommendation.policyPack}`)}`);
|
|
702
709
|
}
|
|
703
710
|
if (recommendation.signals?.nestFramework) {
|
|
704
|
-
line(
|
|
705
|
-
ok,
|
|
706
|
-
'Nest modular monolith → prefer hexagonal (or ddd-bounded-contexts if you have src/contexts/*)'
|
|
707
|
-
);
|
|
711
|
+
line(ok, 'Nest modular monolith → prefer hexagonal (or ddd-bounded-contexts if you have src/contexts/*)');
|
|
708
712
|
}
|
|
709
713
|
if (recommendation.signals?.monorepoTooling?.length) {
|
|
710
|
-
line(
|
|
711
|
-
ok,
|
|
712
|
-
`Monorepo tooling (${recommendation.signals.monorepoTooling.join(', ')}) → preset monorepo (apps/packages/libs)`
|
|
713
|
-
);
|
|
714
|
+
line(ok, `Monorepo tooling (${recommendation.signals.monorepoTooling.join(', ')}) → preset monorepo (apps/packages/libs)`);
|
|
714
715
|
}
|
|
715
716
|
} else {
|
|
716
|
-
line(warn, 'Low governed coverage or fresh config —
|
|
717
|
-
}
|
|
718
|
-
line(ok, `See the plan: ${arkCommand(root, 'ark-check', '--recommend')}`);
|
|
719
|
-
if (recommendation?.archetype) {
|
|
720
|
-
line(ok, `Quick setup: ${arkCommand(root, 'ark', `init --archetype ${recommendation.archetype} --yes`)}`);
|
|
717
|
+
line(warn, 'Low governed coverage or fresh config — finish start, then re-run doctor before adding layers of code.');
|
|
721
718
|
}
|
|
722
|
-
|
|
719
|
+
line(ok, `Optional sensor detail: ${arkCommand(root, 'ark-check', '--recommend')}`);
|
|
720
|
+
actions.unshift('finish ark start (preview + --apply), then re-run --doctor');
|
|
723
721
|
}
|
|
724
722
|
|
|
725
723
|
console.log('');
|
|
@@ -731,8 +729,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
731
729
|
warn,
|
|
732
730
|
'No active violations — coverage is still thin, so green is not yet honest enforcement'
|
|
733
731
|
);
|
|
732
|
+
} else if (designFitness.designWeak) {
|
|
733
|
+
line(warn, 'None on checked edges — edges match the contract; design residual remains (ENFORCE · design-weak). Not healthy finished.');
|
|
734
734
|
} else {
|
|
735
|
-
line(ok, 'None — the code matches the contract');
|
|
735
|
+
line(ok, 'None — the code matches the contract on checked edges');
|
|
736
736
|
}
|
|
737
737
|
} else {
|
|
738
738
|
const typeNote = summary.typeOnlyCount > 0 ? ` (${summary.valueCount} value · ${summary.typeOnlyCount} type-only)` : '';
|
|
@@ -770,6 +770,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
770
770
|
line(' ', `Active host: ${writePath.activeHost}`);
|
|
771
771
|
line(' ', `Supported profile: ${writePath.supportSummary}`);
|
|
772
772
|
line(wpMark, `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`);
|
|
773
|
+
const honestyLine = doctorWritePathHonestyMessage(writePath.activeHost, capabilities['hard-write']);
|
|
774
|
+
if (honestyLine) line(warn, honestyLine);
|
|
773
775
|
if (writePath.sessionNote) {
|
|
774
776
|
line(warn, writePath.sessionNote);
|
|
775
777
|
}
|
|
@@ -932,18 +934,25 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
932
934
|
const uniqueActions = mergePostGreenTopActions(actions, postGreenPath);
|
|
933
935
|
if (isDoctorHealthyNothingToDo(designFitness, uniqueActions)) {
|
|
934
936
|
console.log(color.green('✔ Healthy — nothing to do.'));
|
|
937
|
+
console.log(color.dim(' Contract edges and design residual are clear. Keep write path + CI.'));
|
|
935
938
|
} else {
|
|
936
939
|
if (designFitness.designWeak && uniqueActions.length === 0 && postGreenPath) {
|
|
937
940
|
uniqueActions.push(postGreenPath.action);
|
|
938
941
|
}
|
|
939
|
-
console.log(color.bold(`
|
|
940
|
-
|
|
942
|
+
console.log(color.bold(`Primary next action`));
|
|
943
|
+
console.log(` 1. ${uniqueActions[0]}`);
|
|
944
|
+
if (uniqueActions.length > 1) {
|
|
945
|
+
console.log(color.bold(`Also (${uniqueActions.length - 1}):`));
|
|
946
|
+
uniqueActions.slice(1).forEach((action, index) => console.log(` ${index + 2}. ${action}`));
|
|
947
|
+
}
|
|
941
948
|
if (postGreenPath) {
|
|
942
949
|
console.log(
|
|
943
950
|
color.dim(
|
|
944
|
-
'
|
|
951
|
+
' Shape residual is the primary door under ENFORCE · design-weak — do not skill-shop explore vs coverage vs think.'
|
|
945
952
|
)
|
|
946
953
|
);
|
|
954
|
+
} else {
|
|
955
|
+
console.log(color.dim(' Doctor is the control plane: do #1 first, then re-run --doctor.'));
|
|
947
956
|
}
|
|
948
957
|
}
|
|
949
958
|
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// Generated from hook-templates.source.mjs — run npm run generate:packaged-tooling.
|
|
2
|
-
import{execCommandParts as i,execRunner as
|
|
3
|
-
`}function
|
|
4
|
-
`}function
|
|
2
|
+
import{execCommandParts as i,execRunner as s}from"../ark-shared.mjs";const c="arkgate-mcp";function l(e){const r=s(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${r} ${c} --session-context --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit",hooks:[{type:"command",command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`}]}]}},null,2)}
|
|
3
|
+
`}function p(e){const r=s(e),o="${CODEX_PROJECT_DIR:-${PWD:-.}}";return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${c} --session-context --root "${o}" --config ark.config.json`}]}],PreToolUse:[{matcher:"ApplyPatch|apply_patch|Write|Edit|MultiEdit",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root "${o}" --config ark.config.json`}]}]}},null,2)}
|
|
4
|
+
`}function g(e){const{command:r,args:o}=i(e,c,["--root",".","--config","ark.config.json"]),t=a=>a.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),n=o.map(a=>`"${t(a)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
|
|
5
5
|
# Restart Codex after changes; MCP servers are loaded when the project session starts.
|
|
6
6
|
[mcp_servers.ark]
|
|
7
|
-
command = "${t(
|
|
8
|
-
args = [${
|
|
9
|
-
`}function
|
|
7
|
+
command = "${t(r)}"
|
|
8
|
+
args = [${n}]
|
|
9
|
+
`}function f(e){const{command:r,args:o}=i(e,c,["--root",".","--config","ark.config.json"]),t=o.map(n=>`"${n.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
|
|
10
10
|
# Restart Grok (or /mcps \u2192 refresh) after changes. Also loads repo-root .mcp.json.
|
|
11
11
|
[mcp_servers.ark]
|
|
12
|
-
command = "${
|
|
12
|
+
command = "${r.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"
|
|
13
13
|
args = [${t}]
|
|
14
|
-
`}function
|
|
15
|
-
`}
|
|
14
|
+
`}function u(e){const r=s(e),o="${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}";return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${c} --session-context --root "${o}" --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit|write|search_replace",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root "${o}" --config ark.config.json`}]}]}},null,2)}
|
|
15
|
+
`}function k(e){const r=s(e);return`${JSON.stringify({"ark-write-gate":{PreToolUse:[{matcher:"write_to_file|replace_file_content|multi_replace_file_content",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root "\${PWD:-.}" --config ark.config.json`}]}]}},null,2)}
|
|
16
|
+
`}function d(e){const{command:r,args:o}=i(e,c,["--root",".","--config","ark.config.json"]);return`${JSON.stringify({$schema:"https://opencode.ai/config.json",mcp:{ark:{type:"local",command:[r,...o],enabled:!0}}},null,2)}
|
|
17
|
+
`}function $(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!t||typeof t!="object"||Array.isArray(t))return null;const n=t["ark-write-gate"];if(!n||typeof n!="object")return null;const a={...o,"ark-write-gate":n};return`${JSON.stringify(a,null,2)}
|
|
18
|
+
`}function h(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o))return null;const n={...o};!n.$schema&&t.$schema&&(n.$schema=t.$schema);const a=o.mcp&&typeof o.mcp=="object"&&!Array.isArray(o.mcp)?{...o.mcp}:{};return a.ark=t.mcp.ark,n.mcp=a,`${JSON.stringify(n,null,2)}
|
|
19
|
+
`}export{c as PREFERRED_MCP_BIN,k as antigravityHooks,l as claudeSettings,p as codexHooks,g as codexProjectConfig,u as grokHooks,f as grokProjectConfig,$ as mergeAntigravityArkHook,h as mergeOpencodeArkMcp,d as opencodeProjectConfig};
|
|
@@ -38,6 +38,16 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
|
|
|
38
38
|
true,
|
|
39
39
|
true
|
|
40
40
|
),
|
|
41
|
+
// Google Antigravity: official PreToolUse deny is a hard block. Claim hard only when
|
|
42
|
+
// installed + trusted and the listed write tools are covered by the adapter.
|
|
43
|
+
antigravity: hostProfile(
|
|
44
|
+
'Google Antigravity',
|
|
45
|
+
'.agents/hooks.json',
|
|
46
|
+
'PreToolUse `write_to_file` / `replace_file_content` / `multi_replace_file_content`',
|
|
47
|
+
['write_to_file', 'replace_file_content', 'multi_replace_file_content'],
|
|
48
|
+
true,
|
|
49
|
+
true
|
|
50
|
+
),
|
|
41
51
|
cursor: hostProfile('Cursor', null, null, [], false, false),
|
|
42
52
|
codex: hostProfile(
|
|
43
53
|
'OpenAI Codex',
|
|
@@ -47,6 +57,16 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
|
|
|
47
57
|
false,
|
|
48
58
|
false
|
|
49
59
|
),
|
|
60
|
+
// OpenCode: first-class MCP + permissions; plugin tool.execute.before is incomplete
|
|
61
|
+
// (subagent holes). Never claim hard write.
|
|
62
|
+
opencode: hostProfile(
|
|
63
|
+
'OpenCode',
|
|
64
|
+
null,
|
|
65
|
+
'Advisory MCP + optional experimental plugin (`tool.execute.before`); not a hard boundary',
|
|
66
|
+
[],
|
|
67
|
+
false,
|
|
68
|
+
false
|
|
69
|
+
),
|
|
50
70
|
});
|
|
51
71
|
|
|
52
72
|
export const HOST_SUPPORT_HOSTS = Object.freeze(Object.keys(HOST_SUPPORT_MATRIX));
|
|
@@ -70,18 +90,58 @@ export function renderHostSupportMatrixMarkdown() {
|
|
|
70
90
|
const rows = HOST_SUPPORT_HOSTS.map((host) => {
|
|
71
91
|
const profile = HOST_SUPPORT_MATRIX[host];
|
|
72
92
|
const capabilities = profile.capabilities;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
// Fail-closed honesty: Cursor/Codex/OpenCode never claim hard write; CI is required-status.
|
|
94
|
+
// hookSurface already includes "PreToolUse …" — do not prefix PreToolUse again.
|
|
95
|
+
let local;
|
|
96
|
+
if (capabilities['hard-write']) {
|
|
97
|
+
local = `**Hard** block for listed ops (${profile.hookSurface}) when installed + trusted`;
|
|
98
|
+
} else if (host === 'codex') {
|
|
99
|
+
local =
|
|
100
|
+
'**Advisory / best-effort** at write (not equivalent to Claude/Grok hard block)';
|
|
101
|
+
} else if (host === 'opencode') {
|
|
102
|
+
local =
|
|
103
|
+
'**Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary)';
|
|
104
|
+
} else {
|
|
105
|
+
local = '**Advisory only** at write (no hard hook)';
|
|
106
|
+
}
|
|
76
107
|
const repair = capabilities['repair-payload']
|
|
77
108
|
? 'Emitted on hook deny; host must re-inject'
|
|
78
109
|
: 'No hard-boundary payload';
|
|
79
|
-
|
|
110
|
+
const merge = capabilities['hard-write']
|
|
111
|
+
? '**Required status** = hard merge boundary (`arkgate-check --strict-merge`)'
|
|
112
|
+
: '**Required status** = hard merge boundary (same CI)';
|
|
113
|
+
return `| ${profile.label} | ${local} | Advisory; the agent must call it | ${merge} | ${repair} |`;
|
|
80
114
|
}).join('\n');
|
|
81
115
|
|
|
82
116
|
return `| Host | Local write boundary | MCP validation | CI / merge path | Repair payload |
|
|
83
117
|
|------|----------------------|----------------|-----------------|----------------|
|
|
84
118
|
${rows}
|
|
85
119
|
|
|
120
|
+
**Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
|
|
121
|
+
merge check — not “CI file present.” Cursor/Codex/OpenCode never get a fake hard write claim.
|
|
122
|
+
|
|
86
123
|
This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair payloads never write code silently: the host must re-inject the candidate and ArkGate revalidates it. Run \`arkgate-check --doctor\` for the evidence actually detected in the current repository.`;
|
|
87
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Doctor human one-liner for active-host write honesty (fail-closed).
|
|
128
|
+
* @returns {string|null}
|
|
129
|
+
*/
|
|
130
|
+
export function doctorWritePathHonestyMessage(activeHost, hardWriteActive) {
|
|
131
|
+
const host = typeof activeHost === 'string' ? activeHost.trim().toLowerCase() : '';
|
|
132
|
+
if (host === 'cursor') {
|
|
133
|
+
return 'Cursor: write path is advisory (MCP/rules; no hard PreToolUse). Required CI status (arkgate-check --strict-merge) is the hard merge boundary.';
|
|
134
|
+
}
|
|
135
|
+
if (host === 'codex') {
|
|
136
|
+
return 'Codex: write path is advisory / best-effort at write (not Claude/Grok hard). Required CI status (arkgate-check --strict-merge) is the hard merge boundary.';
|
|
137
|
+
}
|
|
138
|
+
if (host === 'opencode') {
|
|
139
|
+
return 'OpenCode: write path is advisory / best-effort (MCP + optional plugin; not Claude/Grok/Antigravity hard). Required CI status (arkgate-check --strict-merge) is the hard merge boundary.';
|
|
140
|
+
}
|
|
141
|
+
if ((host === 'claude' || host === 'grok' || host === 'antigravity') && !hardWriteActive) {
|
|
142
|
+
const label =
|
|
143
|
+
host === 'claude' ? 'Claude' : host === 'grok' ? 'Grok' : 'Antigravity';
|
|
144
|
+
return `${label}: hard PreToolUse is supported for listed ops when installed + trusted; without runtime-observed hook evidence, hard is unverified. Required CI remains the merge hard boundary.`;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
@@ -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,
|
|
@@ -149,7 +154,10 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
|
|
|
149
154
|
'AGENTS.md',
|
|
150
155
|
compact ? compactAgentInstructions(root, compactHost) : agentInstructions(root)
|
|
151
156
|
);
|
|
152
|
-
|
|
157
|
+
// Always write project MCP registration — compact hosts other than Claude still need
|
|
158
|
+
// ark://manifest for agents (field: compact grok start left doctor reporting Missing .mcp.json
|
|
159
|
+
// when AGENTS lost compact markers or hosts were mixed).
|
|
160
|
+
add('.mcp.json', mcpJson(root));
|
|
153
161
|
const deploy = detectDeployPathQuality(root);
|
|
154
162
|
add(
|
|
155
163
|
'.github/workflows/ark-check.yml',
|
|
@@ -172,6 +180,14 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
|
|
|
172
180
|
add('.grok/config.toml', grokProjectConfig(root));
|
|
173
181
|
add('.grok/hooks/ark-write-gate.json', grokHooks(root));
|
|
174
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
|
+
}
|
|
175
191
|
if (selectedTools.has('windsurf')) add('.windsurf/rules/ark.md', instructionRule(root));
|
|
176
192
|
if (selectedTools.has('cline')) add('.clinerules/ark.md', instructionRule(root));
|
|
177
193
|
if (selectedTools.has('copilot')) {
|
|
@@ -235,6 +251,27 @@ export function runMigrateCommands(root) {
|
|
|
235
251
|
} catch {
|
|
236
252
|
continue;
|
|
237
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
|
+
}
|
|
238
275
|
const ark = json?.mcpServers?.ark;
|
|
239
276
|
if (!ark) continue;
|
|
240
277
|
const binArgs = stripMcpServerArgs(ark.args);
|
|
@@ -313,6 +350,14 @@ export function runInstallAgentGates(args) {
|
|
|
313
350
|
? 'no active host detected'
|
|
314
351
|
: 'default set — no agent config dirs found';
|
|
315
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
|
+
}
|
|
316
361
|
// --skills-only refreshes just the canonical /ark-* skills, which are safe to
|
|
317
362
|
// overwrite (they track the package). The gate/instruction files (AGENTS.md,
|
|
318
363
|
// settings.json, CI workflow, rules) are the ones users customize, so a plain
|
|
@@ -364,6 +409,43 @@ export function runInstallAgentGates(args) {
|
|
|
364
409
|
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
365
410
|
return writeTemplate(root, relativePath, merged, true);
|
|
366
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
|
+
}
|
|
367
449
|
return writeTemplate(
|
|
368
450
|
root,
|
|
369
451
|
relativePath,
|
|
@@ -513,6 +595,19 @@ export function runInstallAgentGates(args) {
|
|
|
513
595
|
console.log(' 3. Add the package.json alias if you want `run check:architecture`:');
|
|
514
596
|
console.log(` ${checkArchitectureScriptSnippet(root)}`);
|
|
515
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
|
+
}
|
|
516
611
|
if ((tools.has('codex') || args.codexHome)) {
|
|
517
612
|
console.log('');
|
|
518
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'],
|
|
@@ -513,7 +515,20 @@ export function applyManagedUpgrade(root, plan, expectedPlanDigest) {
|
|
|
513
515
|
const resolvedRoot = path.resolve(root);
|
|
514
516
|
if (resolvedRoot !== plan.root) throw new Error('managed upgrade plan root mismatch');
|
|
515
517
|
if (plan.summary.blocked > 0) return publicPlan(plan, { blocked: true });
|
|
518
|
+
const wouldWrite = plan.summary.wouldWrite ?? 0;
|
|
519
|
+
const metadataRefresh = plan.summary.metadataRefresh ?? 0;
|
|
520
|
+
// Content already matches: unbound --apply is a no-op (exit success), not a digest error.
|
|
521
|
+
// Optional stamp-only refresh still requires the preview's exact --plan-digest.
|
|
516
522
|
if (!expectedPlanDigest || expectedPlanDigest !== plan.planDigest) {
|
|
523
|
+
if (wouldWrite === 0 && (plan.summary.blocked ?? 0) === 0 && !expectedPlanDigest) {
|
|
524
|
+
return publicPlan(plan, {
|
|
525
|
+
readOnly: true,
|
|
526
|
+
applied: false,
|
|
527
|
+
blocked: false,
|
|
528
|
+
nothingToApply: true,
|
|
529
|
+
optionalStampRefresh: metadataRefresh,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
517
532
|
throw new Error('managed upgrade plan digest mismatch; run a new preview and use its exact nextCommand');
|
|
518
533
|
}
|
|
519
534
|
|
|
@@ -637,7 +652,21 @@ export function renderManagedUpgrade(plan, options = {}) {
|
|
|
637
652
|
'.'
|
|
638
653
|
);
|
|
639
654
|
if (plan.applied) {
|
|
640
|
-
|
|
655
|
+
// Distinguish content writes from optional stamp/metadata bookkeeping.
|
|
656
|
+
if (wouldWrite === 0 && metadataRefresh > 0) {
|
|
657
|
+
console.log(
|
|
658
|
+
`Refreshed ${metadataRefresh} version stamp(s)` +
|
|
659
|
+
(summary.manifestChanged ? ' and managed manifest' : '') +
|
|
660
|
+
' (no content body changes).'
|
|
661
|
+
);
|
|
662
|
+
} else {
|
|
663
|
+
console.log(
|
|
664
|
+
`Applied ${wouldWrite} content write(s)` +
|
|
665
|
+
(metadataRefresh > 0 ? `, ${metadataRefresh} stamp refresh(es)` : '') +
|
|
666
|
+
(summary.manifestChanged ? ', managed manifest' : '') +
|
|
667
|
+
'.'
|
|
668
|
+
);
|
|
669
|
+
}
|
|
641
670
|
return;
|
|
642
671
|
}
|
|
643
672
|
// Content already matches package templates — do not urge --apply as the primary next step.
|
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
|