arkgate 2.11.0 → 2.12.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 +64 -0
- package/README.md +15 -10
- package/bin/ark.mjs +52 -19
- package/bin/lib/agent-gates.mjs +68 -2094
- package/bin/lib/ci-and-commands.mjs +386 -0
- package/bin/lib/deploy-path.mjs +205 -0
- package/bin/lib/gate-files.mjs +223 -0
- package/bin/lib/hook-templates.mjs +99 -0
- package/bin/lib/install-migrate.mjs +442 -0
- package/bin/lib/mcp-adoption.mjs +423 -0
- package/bin/lib/presets.mjs +3 -0
- package/bin/lib/skill-install.mjs +259 -0
- package/bin/lib/typescript-host.mjs +88 -0
- package/bin/lib/write-path-detect.mjs +138 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/nestjs/index.js.map +1 -1
- package/dist/runtime/index.cjs +1 -1
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/docs/agent-guide.md +11 -1
- package/docs/package-surface.md +8 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/templates/skills/ark-autopilot.md +77 -45
- package/templates/skills/ark-explain.md +2 -1
- package/templates/skills/ark-explore.md +135 -34
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate file IO: package.json helpers, template writes, required gates.
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
export const __packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
9
|
+
export const __arkCheckCli = path.join(__packageRoot, 'bin', 'ark-check.mjs');
|
|
10
|
+
|
|
11
|
+
export function readJson(file) {
|
|
12
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readPackageJson(root) {
|
|
16
|
+
const file = path.join(root, 'package.json');
|
|
17
|
+
if (!fs.existsSync(file)) return null;
|
|
18
|
+
return readJson(file);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function hasCheckArchitectureScript(root) {
|
|
22
|
+
const pkg = readPackageJson(root);
|
|
23
|
+
return Boolean(pkg?.scripts?.['check:architecture']);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether package.json scripts already expose a typecheck-like command.
|
|
28
|
+
* Shared by deploy-path quality + typecheck bootstrap (single definition).
|
|
29
|
+
* @param {Record<string, unknown>|null|undefined} scripts
|
|
30
|
+
*/
|
|
31
|
+
export function packageScriptsHaveTypecheck(scripts) {
|
|
32
|
+
if (!scripts || typeof scripts !== 'object') return false;
|
|
33
|
+
return Boolean(
|
|
34
|
+
(typeof scripts.typecheck === 'string' && scripts.typecheck.trim()) ||
|
|
35
|
+
(typeof scripts['type-check'] === 'string' && scripts['type-check'].trim()) ||
|
|
36
|
+
(typeof scripts['check:types'] === 'string' && scripts['check:types'].trim()) ||
|
|
37
|
+
(typeof scripts.tsc === 'string' && /\btsc\b/.test(scripts.tsc))
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Root package (and shallow nested packages) already have a typecheck script.
|
|
43
|
+
* Does not scan CI or framework configs — only package.json scripts.
|
|
44
|
+
* @param {string} root
|
|
45
|
+
*/
|
|
46
|
+
export function treeHasTypecheckScript(root) {
|
|
47
|
+
const pkg = readPackageJson(root);
|
|
48
|
+
if (packageScriptsHaveTypecheck(pkg?.scripts)) return true;
|
|
49
|
+
try {
|
|
50
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
51
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
52
|
+
const candidates = [path.join(root, entry.name)];
|
|
53
|
+
try {
|
|
54
|
+
for (const child of fs.readdirSync(path.join(root, entry.name), { withFileTypes: true })) {
|
|
55
|
+
if (child.isDirectory() && !child.name.startsWith('.')) {
|
|
56
|
+
candidates.push(path.join(root, entry.name, child.name));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
/* ignore */
|
|
61
|
+
}
|
|
62
|
+
for (const dir of candidates) {
|
|
63
|
+
const pj = path.join(dir, 'package.json');
|
|
64
|
+
if (!fs.existsSync(pj)) continue;
|
|
65
|
+
try {
|
|
66
|
+
const nested = JSON.parse(fs.readFileSync(pj, 'utf8'));
|
|
67
|
+
if (packageScriptsHaveTypecheck(nested.scripts)) return true;
|
|
68
|
+
} catch {
|
|
69
|
+
/* ignore */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
/* ignore */
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Add a conservative `typecheck` script when the host has a TS/JS project config
|
|
81
|
+
* but no typecheck-like script yet. Never overwrites an existing script.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} root
|
|
84
|
+
* @param {{ write?: boolean }} [opts]
|
|
85
|
+
* @returns {{
|
|
86
|
+
* changed: boolean,
|
|
87
|
+
* reason: 'added' | 'already' | 'no-tsconfig' | 'no-package-json',
|
|
88
|
+
* script?: string,
|
|
89
|
+
* }}
|
|
90
|
+
*/
|
|
91
|
+
export function ensureTypecheckScript(root, opts = {}) {
|
|
92
|
+
const write = opts.write !== false;
|
|
93
|
+
const hasTsconfig =
|
|
94
|
+
fs.existsSync(path.join(root, 'tsconfig.json')) ||
|
|
95
|
+
fs.existsSync(path.join(root, 'jsconfig.json'));
|
|
96
|
+
if (!hasTsconfig) return { changed: false, reason: 'no-tsconfig' };
|
|
97
|
+
|
|
98
|
+
const pkgPath = path.join(root, 'package.json');
|
|
99
|
+
if (!fs.existsSync(pkgPath)) return { changed: false, reason: 'no-package-json' };
|
|
100
|
+
|
|
101
|
+
if (treeHasTypecheckScript(root)) {
|
|
102
|
+
return { changed: false, reason: 'already' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pkg = readPackageJson(root) || {};
|
|
106
|
+
const scripts =
|
|
107
|
+
pkg.scripts && typeof pkg.scripts === 'object' ? { ...pkg.scripts } : {};
|
|
108
|
+
const script = 'tsc --noEmit';
|
|
109
|
+
scripts.typecheck = script;
|
|
110
|
+
if (write) {
|
|
111
|
+
const next = { ...pkg, scripts };
|
|
112
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(next, null, 2)}\n`);
|
|
113
|
+
}
|
|
114
|
+
return { changed: true, reason: 'added', script };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const REQUIRED_GATE_FILES = [
|
|
118
|
+
'AGENTS.md',
|
|
119
|
+
'.mcp.json',
|
|
120
|
+
];
|
|
121
|
+
const REQUIRED_GATE_WORKFLOW = '.github/workflows/*.yml running ark-check';
|
|
122
|
+
|
|
123
|
+
export function hasArkWorkflow(root) {
|
|
124
|
+
const workflowsDir = path.join(root, '.github', 'workflows');
|
|
125
|
+
if (!fs.existsSync(workflowsDir)) return false;
|
|
126
|
+
return fs
|
|
127
|
+
.readdirSync(workflowsDir)
|
|
128
|
+
.filter((file) => /\.ya?ml$/i.test(file))
|
|
129
|
+
.some((file) => {
|
|
130
|
+
try {
|
|
131
|
+
const content = fs.readFileSync(path.join(workflowsDir, file), 'utf8');
|
|
132
|
+
return (
|
|
133
|
+
/\bark-check\b/.test(content) ||
|
|
134
|
+
/\bcheck:architecture\b/.test(content) ||
|
|
135
|
+
/\buses\s*:\s*['"]?[^'"\s#]+\/arkgate@/i.test(content)
|
|
136
|
+
);
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function missingGates(root) {
|
|
144
|
+
const missing = REQUIRED_GATE_FILES.filter(
|
|
145
|
+
(relativePath) => !fs.existsSync(path.join(root, relativePath))
|
|
146
|
+
);
|
|
147
|
+
if (!hasArkWorkflow(root)) missing.push(REQUIRED_GATE_WORKFLOW);
|
|
148
|
+
return missing;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function ensureDirForFile(file) {
|
|
152
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* True when AGENTS.md is wholly Ark-owned (header is Ark Enforcement).
|
|
157
|
+
* Project guides that merely append an Ark section must remain non-Ark so --force
|
|
158
|
+
* never wipes them.
|
|
159
|
+
*/
|
|
160
|
+
export function isArkAgentsContent(text) {
|
|
161
|
+
if (typeof text !== 'string' || !text.trim()) return false;
|
|
162
|
+
const head = text.trimStart().slice(0, 120);
|
|
163
|
+
return /^#\s*Ark(Gate)?\s+Enforcement\b/.test(head);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* True when AGENTS.md is the **library mother-repo** self-hosted guide (Identity block).
|
|
168
|
+
* Never replace with the consumer install template — even under `--force`.
|
|
169
|
+
*/
|
|
170
|
+
export function isSelfHostedLibraryAgents(text) {
|
|
171
|
+
if (typeof text !== 'string' || !text.trim()) return false;
|
|
172
|
+
return (
|
|
173
|
+
/##\s*Identity\s*[—\-–-]\s*read this first/i.test(text) ||
|
|
174
|
+
/mother\s*\/\s*canonical development repository/i.test(text) ||
|
|
175
|
+
/Git\s*\/\s*clone only/i.test(text)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function writeTemplate(root, relativePath, content, force) {
|
|
180
|
+
const fullPath = path.join(root, relativePath);
|
|
181
|
+
if (relativePath === 'AGENTS.md' && fs.existsSync(fullPath)) {
|
|
182
|
+
let existing = '';
|
|
183
|
+
try {
|
|
184
|
+
existing = fs.readFileSync(fullPath, 'utf8');
|
|
185
|
+
} catch {
|
|
186
|
+
existing = '';
|
|
187
|
+
}
|
|
188
|
+
// Library authoring tree: keep Identity + 4-layer dogfood contract forever.
|
|
189
|
+
if (existing && isSelfHostedLibraryAgents(existing)) {
|
|
190
|
+
return { relativePath, status: 'skipped-self-hosted' };
|
|
191
|
+
}
|
|
192
|
+
if (existing && !isArkAgentsContent(existing)) {
|
|
193
|
+
// Never clobber a project-owned AGENTS.md — even with --force.
|
|
194
|
+
// If Ark section not present yet, merge once; subsequent runs leave it alone.
|
|
195
|
+
const hasArkSection =
|
|
196
|
+
/#\s*Ark(Gate)?\s+Enforcement\b/.test(existing) ||
|
|
197
|
+
/ark\.config\.json is authoritative/i.test(existing);
|
|
198
|
+
if (force && isArkAgentsContent(content) && !hasArkSection) {
|
|
199
|
+
try {
|
|
200
|
+
const merged = `${existing.replace(/\s*$/, '')}\n\n---\n\n${content}`;
|
|
201
|
+
ensureDirForFile(fullPath);
|
|
202
|
+
fs.writeFileSync(fullPath, merged);
|
|
203
|
+
return { relativePath, status: 'merged' };
|
|
204
|
+
} catch {
|
|
205
|
+
return { relativePath, status: 'failed' };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { relativePath, status: 'skipped-non-ark' };
|
|
209
|
+
}
|
|
210
|
+
if (!force && isArkAgentsContent(existing)) {
|
|
211
|
+
return { relativePath, status: 'skipped' };
|
|
212
|
+
}
|
|
213
|
+
} else if (fs.existsSync(fullPath) && !force) {
|
|
214
|
+
return { relativePath, status: 'skipped' };
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
ensureDirForFile(fullPath);
|
|
218
|
+
fs.writeFileSync(fullPath, content);
|
|
219
|
+
return { relativePath, status: 'written' };
|
|
220
|
+
} catch {
|
|
221
|
+
return { relativePath, status: 'failed' };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host hook / MCP project templates for agent-gate install (Claude, Grok).
|
|
3
|
+
* Kept out of agent-gates.mjs so install orchestration stays scannable (explore gap #5).
|
|
4
|
+
*/
|
|
5
|
+
import { execCommandParts, execRunner } from '../ark-shared.mjs';
|
|
6
|
+
|
|
7
|
+
/** Preferred MCP binary name for generated hooks (package dual-bin). */
|
|
8
|
+
export const PREFERRED_MCP_BIN = 'arkgate-mcp';
|
|
9
|
+
|
|
10
|
+
export function claudeSettings(root) {
|
|
11
|
+
const runner = execRunner(root);
|
|
12
|
+
return `${JSON.stringify({
|
|
13
|
+
hooks: {
|
|
14
|
+
// Inject the contract at session start so the agent knows the architecture from
|
|
15
|
+
// the first token. Project-scoped by design; --session-context is also a silent
|
|
16
|
+
// no-op when no ark.config.json exists, so it can never leak into other projects.
|
|
17
|
+
SessionStart: [
|
|
18
|
+
{
|
|
19
|
+
hooks: [
|
|
20
|
+
{
|
|
21
|
+
type: 'command',
|
|
22
|
+
command: `${runner} ${PREFERRED_MCP_BIN} --session-context --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
PreToolUse: [
|
|
28
|
+
{
|
|
29
|
+
matcher: 'Write|Edit|MultiEdit',
|
|
30
|
+
hooks: [
|
|
31
|
+
{
|
|
32
|
+
type: 'command',
|
|
33
|
+
// W4: --hook-repair emits ARK_REPAIR_JSON / ARK_AUTOPATCH_JSON on deny
|
|
34
|
+
// (still exit 2 — never silent write). Omit --hook-repair for reject-only prose.
|
|
35
|
+
command: `${runner} ${PREFERRED_MCP_BIN} --hook --hook-repair --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
}, null, 2)}\n`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Grok Build project config: MCP registration (commit-friendly relative paths — unlike
|
|
45
|
+
// Codex's global config.toml, Grok loads .grok/config.toml from the project).
|
|
46
|
+
export function grokProjectConfig(root) {
|
|
47
|
+
const { command, args } = execCommandParts(root, PREFERRED_MCP_BIN, [
|
|
48
|
+
'--root',
|
|
49
|
+
'.',
|
|
50
|
+
'--config',
|
|
51
|
+
'ark.config.json',
|
|
52
|
+
]);
|
|
53
|
+
const argsToml = args.map((value) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`).join(', ');
|
|
54
|
+
return `# Generated by ark-check --install-agent-gates (Grok Build project scope).
|
|
55
|
+
# Restart Grok (or /mcps → refresh) after changes. Also loads repo-root .mcp.json.
|
|
56
|
+
[mcp_servers.ark]
|
|
57
|
+
command = "${command.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"
|
|
58
|
+
args = [${argsToml}]
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Grok Build hooks: same arkgate-mcp contracts as Claude. Grok sets both
|
|
63
|
+
// GROK_WORKSPACE_ROOT and CLAUDE_PROJECT_DIR (Claude-compatible alias). Prefer
|
|
64
|
+
// GROK_* with fallback so hooks still work if only one is present.
|
|
65
|
+
// Matcher keeps Claude names (Write|Edit|MultiEdit) and Grok natives
|
|
66
|
+
// (write|search_replace) — Grok aliases both directions.
|
|
67
|
+
export function grokHooks(root) {
|
|
68
|
+
const runner = execRunner(root);
|
|
69
|
+
// Nested defaults: Grok native → Claude alias → project cwd (hook cwd is the workspace).
|
|
70
|
+
const grokRoot = '${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}';
|
|
71
|
+
return `${JSON.stringify({
|
|
72
|
+
hooks: {
|
|
73
|
+
SessionStart: [
|
|
74
|
+
{
|
|
75
|
+
hooks: [
|
|
76
|
+
{
|
|
77
|
+
type: 'command',
|
|
78
|
+
timeout: 30,
|
|
79
|
+
command: `${runner} ${PREFERRED_MCP_BIN} --session-context --root "${grokRoot}" --config ark.config.json`,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
PreToolUse: [
|
|
85
|
+
{
|
|
86
|
+
matcher: 'Write|Edit|MultiEdit|write|search_replace',
|
|
87
|
+
hooks: [
|
|
88
|
+
{
|
|
89
|
+
type: 'command',
|
|
90
|
+
timeout: 30,
|
|
91
|
+
// W4: --hook-repair → structured autoPatch on deny (hard block still).
|
|
92
|
+
command: `${runner} ${PREFERRED_MCP_BIN} --hook --hook-repair --root "${grokRoot}" --config ark.config.json`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
}, null, 2)}\n`;
|
|
99
|
+
}
|