arkgate 2.1.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 +1249 -0
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/SECURITY.md +39 -0
- package/bin/ark-check.mjs +5204 -0
- package/bin/ark-mcp.mjs +898 -0
- package/bin/ark-shared.mjs +1520 -0
- package/bin/ark.mjs +491 -0
- package/dist/eslint/index.cjs +222 -0
- package/dist/eslint/index.cjs.map +1 -0
- package/dist/eslint/index.d.cts +42 -0
- package/dist/eslint/index.d.ts +40 -0
- package/dist/eslint/index.js +193 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/index.cjs +3080 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +577 -0
- package/dist/index.d.ts +577 -0
- package/dist/index.js +2998 -0
- package/dist/index.js.map +1 -0
- package/dist/nestjs/index.cjs +2332 -0
- package/dist/nestjs/index.cjs.map +1 -0
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +22 -0
- package/dist/nestjs/index.js +2308 -0
- package/dist/nestjs/index.js.map +1 -0
- package/dist/types-DpdVN7Lm.d.cts +1023 -0
- package/dist/types-DpdVN7Lm.d.ts +1023 -0
- package/docs/agent-guide.md +490 -0
- package/docs/ai-gates.md +337 -0
- package/docs/ark-check-example.json +87 -0
- package/docs/assets/ark-write-gate.svg +28 -0
- package/docs/brownfield-adoption.md +87 -0
- package/docs/demos/01-write-gate-self-correction.md +74 -0
- package/docs/demos/02-brownfield-baseline-adoption.md +71 -0
- package/docs/demos/03-copilot-autopilot.md +83 -0
- package/docs/enthusiast/README.md +62 -0
- package/docs/enthusiast/explanation-application-shape.md +29 -0
- package/docs/enthusiast/how-to-agent-gates.md +36 -0
- package/docs/enthusiast/how-to-gallery-starter.md +27 -0
- package/docs/enthusiast/how-to-pick-shape.md +45 -0
- package/docs/enthusiast/how-to-policy-pack.md +37 -0
- package/docs/enthusiast/reference-archetypes.md +36 -0
- package/docs/enthusiast/reference-commands.md +50 -0
- package/docs/enthusiast/tutorial-first-project.md +86 -0
- package/docs/production-hardening.md +59 -0
- package/package.json +125 -0
- package/server.json +39 -0
- package/templates/architecture-playbook.json +339 -0
- package/templates/policy-packs/enthusiast-feature-sliced.json +20 -0
- package/templates/policy-packs/enthusiast-hexagonal.json +18 -0
- package/templates/policy-packs/enthusiast-layered.json +18 -0
- package/templates/policy-packs/enthusiast-monorepo.json +18 -0
- package/templates/skills/ark-adopt.md +103 -0
- package/templates/skills/ark-architect.md +90 -0
- package/templates/skills/ark-autopilot.md +95 -0
- package/templates/skills/ark-contract.md +98 -0
- package/templates/skills/ark-coverage.md +96 -0
- package/templates/skills/ark-explain.md +78 -0
- package/templates/skills/ark-fix.md +96 -0
- package/templates/skills/ark-loop.md +69 -0
- package/templates/skills/ark-place.md +68 -0
- package/templates/skills/ark-runtime.md +62 -0
- package/templates/skills/ark-upgrade.md +109 -0
package/bin/ark.mjs
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import readline from 'node:readline/promises';
|
|
8
|
+
import {
|
|
9
|
+
arkCommand,
|
|
10
|
+
buildArchitectureRecommendation,
|
|
11
|
+
detectPackageManager,
|
|
12
|
+
detectWorkspaces,
|
|
13
|
+
INIT_WIZARD_CHOICES,
|
|
14
|
+
isValidArchetypeId,
|
|
15
|
+
mapWizardChoiceToArchetype,
|
|
16
|
+
resolveArchetypePreset,
|
|
17
|
+
resolveOperatingMode,
|
|
18
|
+
} from './ark-shared.mjs';
|
|
19
|
+
|
|
20
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const arkCheck = path.join(here, 'ark-check.mjs');
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
const args = {
|
|
25
|
+
command: undefined,
|
|
26
|
+
root: process.cwd(),
|
|
27
|
+
yes: false,
|
|
28
|
+
force: false,
|
|
29
|
+
strict: true,
|
|
30
|
+
install: true,
|
|
31
|
+
help: false,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Scan from the first user token (index 2) so a leading flag like `ark --help` is
|
|
35
|
+
// recognized: the command is the first NON-dash argument, not blindly argv[2].
|
|
36
|
+
for (let i = 2; i < argv.length; i += 1) {
|
|
37
|
+
const arg = argv[i];
|
|
38
|
+
if (arg === '--root') args.root = path.resolve(argv[++i]);
|
|
39
|
+
else if (arg === '--yes' || arg === '-y') args.yes = true;
|
|
40
|
+
else if (arg === '--force') args.force = true;
|
|
41
|
+
else if (arg === '--no-strict') args.strict = false;
|
|
42
|
+
else if (arg === '--no-install') args.install = false;
|
|
43
|
+
else if (arg === '--preset') args.preset = argv[++i];
|
|
44
|
+
else if (arg === '--archetype') args.archetype = argv[++i];
|
|
45
|
+
else if (arg === '--tools') args.tools = argv[++i];
|
|
46
|
+
else if (arg === '--help' || arg === '-h' || arg === 'help') args.help = true;
|
|
47
|
+
else if (!arg.startsWith('-') && args.command === undefined) args.command = arg;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return args;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function usage() {
|
|
54
|
+
return `Usage:
|
|
55
|
+
ark start [--root <project>] [--yes]
|
|
56
|
+
ark init [--root <project>] [--preset hexagonal|layered|feature-sliced|monorepo]
|
|
57
|
+
[--archetype <playbook-id>] [--tools <list>] [--yes] [--force] [--no-strict]
|
|
58
|
+
ark upgrade [--root <project>] [--no-install] [--no-strict]
|
|
59
|
+
|
|
60
|
+
Commands:
|
|
61
|
+
start New here? The guided setup. Looks at your project, suggests a shape in
|
|
62
|
+
plain language, sets up the guardrails, and shows a plan — no code changed.
|
|
63
|
+
init Configure Ark project enforcement with explicit prompts.
|
|
64
|
+
upgrade One command to update Ark: bump the package to @latest, refresh gate
|
|
65
|
+
templates + /ark-* skills (and Codex home prompts), migrate command
|
|
66
|
+
runners to this project's package manager, then run the strict check.
|
|
67
|
+
(alias: ark update)
|
|
68
|
+
|
|
69
|
+
Options:
|
|
70
|
+
--yes Non-interactive defaults: create config if needed, install gate templates, run strict check.
|
|
71
|
+
--force Allow generated files to overwrite existing files.
|
|
72
|
+
--no-strict Skip the final strict ark-check run.
|
|
73
|
+
--no-install (upgrade) Refresh gates/skills only; don't reinstall the package.
|
|
74
|
+
--preset Start from a named architecture preset instead of detection.
|
|
75
|
+
--archetype Application shape from templates/architecture-playbook.json (maps to the matching preset).
|
|
76
|
+
Valid ids: crud-product, api-backend, frontend-surface, library-sdk, cli-utility,
|
|
77
|
+
worker-pipeline, event-coordinator, integration-bridge, multi-app-workspace, prototype-spike.
|
|
78
|
+
--tools Comma-separated agents to gate (claude,cursor,codex,windsurf,cline,copilot,kiro,roo,continue,gemini).
|
|
79
|
+
Omit to auto-detect from each tool's config dir, falling back to claude+cursor+codex.
|
|
80
|
+
|
|
81
|
+
Interactive mode (TTY, no --yes): asks what application shape you are building and maps it to a preset.
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The package-manager command that adds arkgate@latest as a dev dependency.
|
|
86
|
+
function packageInstallArgv(root) {
|
|
87
|
+
const spec = 'arkgate@latest';
|
|
88
|
+
const pm = detectPackageManager(root);
|
|
89
|
+
if (pm === 'pnpm') return ['pnpm', ['add', '-D', spec]];
|
|
90
|
+
if (pm === 'yarn') return ['yarn', ['add', '-D', spec]];
|
|
91
|
+
return ['npm', ['install', '-D', spec]];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runCommand(command, commandArgs, cwd) {
|
|
95
|
+
const result = spawnSync(command, commandArgs, { cwd, stdio: 'inherit', encoding: 'utf8' });
|
|
96
|
+
return result.status ?? 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// `ark upgrade`: the one command that replaces the "install @latest && install-agent-gates
|
|
100
|
+
// --skills-only --force && ... --codex-home --force && ... --migrate-commands && check" chain.
|
|
101
|
+
// Each step reruns ark-check as a fresh process, so the refresh runs from the freshly-installed
|
|
102
|
+
// version, not this (now-older) process.
|
|
103
|
+
async function upgrade(args) {
|
|
104
|
+
const root = args.root;
|
|
105
|
+
console.log('Ark upgrade — updating the package, gates, skills, and command runners.');
|
|
106
|
+
|
|
107
|
+
if (args.install) {
|
|
108
|
+
const [command, commandArgs] = packageInstallArgv(root);
|
|
109
|
+
console.log(`\n1/4 Updating the package: ${command} ${commandArgs.join(' ')}`);
|
|
110
|
+
const status = runCommand(command, commandArgs, root);
|
|
111
|
+
if (status !== 0) {
|
|
112
|
+
console.error(
|
|
113
|
+
`\nPackage update failed (exit ${status}). Fix the install and re-run, or use ` +
|
|
114
|
+
'`ark upgrade --no-install` to refresh gates/skills against the installed version.'
|
|
115
|
+
);
|
|
116
|
+
return status;
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
console.log('\n1/4 Skipping package install (--no-install).');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log('\n2/4 Refreshing agent gates + /ark-* skills…');
|
|
123
|
+
let status = runArkCheck(['--root', root, '--install-agent-gates'], { cwd: root });
|
|
124
|
+
if (status !== 0) return status;
|
|
125
|
+
|
|
126
|
+
// Codex loads slash-command prompts from ~/.codex/prompts, not the repo — refresh those too
|
|
127
|
+
// when a Codex home exists, so nothing is left stale. Non-fatal: a permission error there
|
|
128
|
+
// (e.g. a sandbox) shouldn't fail the whole upgrade.
|
|
129
|
+
if (fs.existsSync(path.join(os.homedir(), '.codex'))) {
|
|
130
|
+
console.log('\n Refreshing Codex home prompts (~/.codex)…');
|
|
131
|
+
runArkCheck(
|
|
132
|
+
['--root', root, '--install-agent-gates', '--skills-only', '--codex-home', '--force'],
|
|
133
|
+
{ cwd: root }
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log('\n3/4 Migrating command runners to this project’s package manager…');
|
|
138
|
+
status = runArkCheck(['--root', root, '--install-agent-gates', '--migrate-commands'], { cwd: root });
|
|
139
|
+
if (status !== 0) return status;
|
|
140
|
+
|
|
141
|
+
if (!args.strict) {
|
|
142
|
+
console.log('\n4/4 Skipping the strict check (--no-strict). Upgrade complete.');
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
console.log('\n4/4 Verifying architecture…');
|
|
146
|
+
return runArkCheck(
|
|
147
|
+
['--root', root, '--config', 'ark.config.json', '--strict-config'],
|
|
148
|
+
{ cwd: root }
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function runArkCheck(args, options = {}) {
|
|
153
|
+
const result = spawnSync(process.execPath, [arkCheck, ...args], {
|
|
154
|
+
cwd: options.cwd,
|
|
155
|
+
stdio: options.stdio ?? 'inherit',
|
|
156
|
+
encoding: 'utf8',
|
|
157
|
+
});
|
|
158
|
+
return result.status ?? 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isInteractiveTty() {
|
|
162
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function askYesNo(rl, question, defaultYes = true) {
|
|
166
|
+
const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
|
|
167
|
+
const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
|
|
168
|
+
if (!answer) return defaultYes;
|
|
169
|
+
return answer === 'y' || answer === 'yes';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function resolveArchetypeInteractive(rl, root) {
|
|
173
|
+
console.log('');
|
|
174
|
+
console.log('What are you building? (application shape — not a framework name)');
|
|
175
|
+
for (const choice of INIT_WIZARD_CHOICES) {
|
|
176
|
+
console.log(` ${choice.key}. ${choice.label}`);
|
|
177
|
+
}
|
|
178
|
+
const answer = (await rl.question('Choose 1–8 [8]: ')).trim() || '8';
|
|
179
|
+
const mapped = mapWizardChoiceToArchetype(answer);
|
|
180
|
+
if (!mapped) {
|
|
181
|
+
console.log('Unrecognized choice — analyzing the repo instead.');
|
|
182
|
+
return resolveArchetypeFromRecommend(root);
|
|
183
|
+
}
|
|
184
|
+
if (mapped === 'auto') {
|
|
185
|
+
return resolveArchetypeFromRecommend(root);
|
|
186
|
+
}
|
|
187
|
+
return mapped;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function resolveArchetypeFromRecommend(root) {
|
|
191
|
+
const rec = buildArchitectureRecommendation(root);
|
|
192
|
+
console.log(`Suggested shape: ${rec.archetype} — ${rec.label} (confidence ${rec.confidence})`);
|
|
193
|
+
return rec.archetype;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resolveInitPreset(args) {
|
|
197
|
+
if (args.preset) return { preset: args.preset, archetype: args.archetype };
|
|
198
|
+
if (args.archetype) {
|
|
199
|
+
if (!isValidArchetypeId(args.archetype)) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Unknown archetype "${args.archetype}". Run ark-check --recommend to see a suggested shape.`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const resolved = resolveArchetypePreset(args.archetype);
|
|
205
|
+
return { preset: resolved.preset, archetype: resolved.archetype, label: resolved.label };
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function init(args) {
|
|
211
|
+
const root = args.root;
|
|
212
|
+
const configPath = path.join(root, 'ark.config.json');
|
|
213
|
+
const interactive = !args.yes && isInteractiveTty();
|
|
214
|
+
const rl = interactive
|
|
215
|
+
? readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
216
|
+
: null;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
let archetype = args.archetype;
|
|
220
|
+
let preset = args.preset;
|
|
221
|
+
|
|
222
|
+
if (interactive && !preset && !archetype) {
|
|
223
|
+
archetype = await resolveArchetypeInteractive(rl, root);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!preset && archetype) {
|
|
227
|
+
const resolved = resolveArchetypePreset(archetype);
|
|
228
|
+
preset = resolved.preset;
|
|
229
|
+
console.log(`Using archetype ${archetype} → preset ${preset} (${resolved.label})`);
|
|
230
|
+
} else if (!preset && !interactive && !args.yes) {
|
|
231
|
+
// non-TTY without --yes/--preset/--archetype: fall back to detection init
|
|
232
|
+
} else if (!preset && args.yes && !archetype) {
|
|
233
|
+
const rec = buildArchitectureRecommendation(root);
|
|
234
|
+
preset = rec.preset;
|
|
235
|
+
archetype = rec.archetype;
|
|
236
|
+
console.log(`Auto-selected archetype ${archetype} → preset ${preset}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let shouldInit = !fs.existsSync(configPath);
|
|
240
|
+
if (fs.existsSync(configPath)) {
|
|
241
|
+
shouldInit = args.force
|
|
242
|
+
? true
|
|
243
|
+
: args.yes
|
|
244
|
+
? false
|
|
245
|
+
: await askYesNo(rl, 'ark.config.json already exists. Regenerate it?', false);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (shouldInit) {
|
|
249
|
+
const initArgs = ['--root', root, '--init'];
|
|
250
|
+
if (preset) initArgs.push('--preset', preset);
|
|
251
|
+
if (args.force) initArgs.push('--force');
|
|
252
|
+
const status = runArkCheck(initArgs, { cwd: root });
|
|
253
|
+
if (status !== 0) return status;
|
|
254
|
+
} else {
|
|
255
|
+
console.log('Skipped ark.config.json generation.');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const installGates = args.yes || (await askYesNo(rl, 'Configure agent and CI gate templates?', true));
|
|
259
|
+
if (installGates) {
|
|
260
|
+
const gateArgs = ['--root', root, '--install-agent-gates'];
|
|
261
|
+
if (args.tools) gateArgs.push('--tools', args.tools);
|
|
262
|
+
if (args.force) gateArgs.push('--force');
|
|
263
|
+
const status = runArkCheck(gateArgs, { cwd: root });
|
|
264
|
+
if (status !== 0) return status;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const runStrict =
|
|
268
|
+
args.strict && (args.yes || (await askYesNo(rl, 'Run strict architecture check now?', true)));
|
|
269
|
+
if (runStrict) {
|
|
270
|
+
return runArkCheck(
|
|
271
|
+
['--root', root, '--config', 'ark.config.json', '--strict-config'],
|
|
272
|
+
{ cwd: root }
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log(
|
|
277
|
+
`Ark init complete. Run \`${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config')}\` before merging.`
|
|
278
|
+
);
|
|
279
|
+
if (archetype) {
|
|
280
|
+
console.log(`Shape: ${archetype}. Plan: ${arkCommand(root, 'ark-check', '--recommend')}`);
|
|
281
|
+
}
|
|
282
|
+
return 0;
|
|
283
|
+
} finally {
|
|
284
|
+
rl?.close();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// `ark start` — the guided entry point (co-pilot Phase G). One command takes a newcomer from
|
|
289
|
+
// "I have a project" to "governed, with a plan" in plain language, without knowing any skill
|
|
290
|
+
// names: look at the code → suggest a shape → set up the guardrails → show the plan. It only
|
|
291
|
+
// orchestrates existing steps (recommend → init → --plan) and frames each in outcome terms.
|
|
292
|
+
async function start(args) {
|
|
293
|
+
const root = args.root;
|
|
294
|
+
const interactive = !args.yes && isInteractiveTty();
|
|
295
|
+
const rl = interactive
|
|
296
|
+
? readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
297
|
+
: null;
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
console.log("Let's set up Ark for your project.");
|
|
301
|
+
console.log(
|
|
302
|
+
"I'll look at your code, suggest a shape, set up the guardrails, and show you a plan."
|
|
303
|
+
);
|
|
304
|
+
console.log('Nothing in your code is changed — this only adds Ark configuration.');
|
|
305
|
+
|
|
306
|
+
// 1) Look at the project.
|
|
307
|
+
let rec;
|
|
308
|
+
try {
|
|
309
|
+
rec = buildArchitectureRecommendation(root);
|
|
310
|
+
} catch {
|
|
311
|
+
rec = undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 2) Suggest a shape, in plain language, and confirm.
|
|
315
|
+
let archetype = rec?.archetype;
|
|
316
|
+
if (rec) {
|
|
317
|
+
console.log('');
|
|
318
|
+
console.log(`Your project looks like: ${rec.label}.`);
|
|
319
|
+
if (rec.analogy) console.log(`In plain terms — ${rec.analogy}`);
|
|
320
|
+
if (rec.mature) {
|
|
321
|
+
console.log('');
|
|
322
|
+
console.log(
|
|
323
|
+
`This is an established codebase (${rec.signals?.sourceFileCount} files), so Ark will ADOPT it:`
|
|
324
|
+
);
|
|
325
|
+
console.log('match the contract to how your code is already organized, and flag only genuine issues.');
|
|
326
|
+
}
|
|
327
|
+
const proceed = args.yes || (await askYesNo(rl, '\nSet Ark up for this shape?', true));
|
|
328
|
+
if (!proceed) {
|
|
329
|
+
archetype = interactive ? await resolveArchetypeInteractive(rl, root) : rec.archetype;
|
|
330
|
+
}
|
|
331
|
+
} else if (interactive) {
|
|
332
|
+
archetype = await resolveArchetypeInteractive(rl, root);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 3) Set up config + gates. Greenfield → the shape's preset; an established repo → detection,
|
|
336
|
+
// so the contract anchors to the directories you already have instead of aspirational globs.
|
|
337
|
+
console.log('');
|
|
338
|
+
console.log('Setting up Ark…');
|
|
339
|
+
const configPath = path.join(root, 'ark.config.json');
|
|
340
|
+
if (!fs.existsSync(configPath)) {
|
|
341
|
+
const initArgs = ['--root', root, '--init'];
|
|
342
|
+
const preset = archetype ? resolveArchetypePreset(archetype).preset : undefined;
|
|
343
|
+
const workspaces = detectWorkspaces(root);
|
|
344
|
+
const looksLikeMonorepo =
|
|
345
|
+
workspaces.length > 0 ||
|
|
346
|
+
fs.existsSync(path.join(root, 'apps')) ||
|
|
347
|
+
fs.existsSync(path.join(root, 'packages'));
|
|
348
|
+
// Mature multi-package trees must NOT get a thin src/** starter (0 files → false green).
|
|
349
|
+
// Prefer the monorepo preset so include roots match apps/packages/tooling.
|
|
350
|
+
if (looksLikeMonorepo && (rec?.mature || workspaces.length > 0)) {
|
|
351
|
+
initArgs.push('--preset', 'monorepo');
|
|
352
|
+
console.log(' Multi-package layout detected — using monorepo profile.');
|
|
353
|
+
} else if (!rec?.mature && preset) {
|
|
354
|
+
initArgs.push('--preset', preset);
|
|
355
|
+
}
|
|
356
|
+
const status = runArkCheck(initArgs, { cwd: root });
|
|
357
|
+
if (status !== 0) return status;
|
|
358
|
+
} else {
|
|
359
|
+
console.log(' Found an existing ark.config.json — keeping it.');
|
|
360
|
+
}
|
|
361
|
+
runArkCheck(['--root', root, '--install-agent-gates'], { cwd: root });
|
|
362
|
+
|
|
363
|
+
// 4) Show the plan: what's safe to auto-fix vs what needs a decision.
|
|
364
|
+
console.log('');
|
|
365
|
+
console.log('Your architecture plan:');
|
|
366
|
+
runArkCheck(['--root', root, '--config', 'ark.config.json', '--plan'], { cwd: root });
|
|
367
|
+
|
|
368
|
+
// Capture plan JSON for an honest wrap-up (governed% + goal.met) without re-printing.
|
|
369
|
+
const planCapture = spawnSync(
|
|
370
|
+
process.execPath,
|
|
371
|
+
[arkCheck, '--root', root, '--config', 'ark.config.json', '--plan', '--json'],
|
|
372
|
+
{ cwd: root, encoding: 'utf8' }
|
|
373
|
+
);
|
|
374
|
+
let planOk = true;
|
|
375
|
+
let governedPercent = null;
|
|
376
|
+
let mode = 'enforce'; // suggest | adapt | enforce
|
|
377
|
+
try {
|
|
378
|
+
const parsed = JSON.parse(planCapture.stdout || '{}');
|
|
379
|
+
planOk = parsed.ok === true && parsed.plan?.goal?.met === true;
|
|
380
|
+
governedPercent = parsed.plan?.goal?.governedPercent ?? null;
|
|
381
|
+
const totalFiles = parsed.plan?.goal?.totalFiles ?? null;
|
|
382
|
+
mode = resolveOperatingMode({
|
|
383
|
+
governedPercent:
|
|
384
|
+
totalFiles === 0 ? 0 : governedPercent,
|
|
385
|
+
planMet: parsed.plan?.goal?.met === true,
|
|
386
|
+
mature: Boolean(rec?.mature),
|
|
387
|
+
totalFiles,
|
|
388
|
+
});
|
|
389
|
+
// Fresh greenfield with good coverage but no real tree yet → suggest, not enforce theatre.
|
|
390
|
+
if (mode === 'enforce' && rec && !rec.mature && (governedPercent ?? 0) < 80) {
|
|
391
|
+
mode = 'suggest';
|
|
392
|
+
}
|
|
393
|
+
// Empty scope from plan JSON is always adapt.
|
|
394
|
+
if (parsed.plan?.goal?.emptyScope || totalFiles === 0) {
|
|
395
|
+
mode = 'adapt';
|
|
396
|
+
planOk = false;
|
|
397
|
+
}
|
|
398
|
+
} catch {
|
|
399
|
+
// If capture fails, stay conservative: don't claim full enforcement.
|
|
400
|
+
mode = 'adapt';
|
|
401
|
+
planOk = false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 5) Plain-language wrap-up — three operating modes, one contract.
|
|
405
|
+
// suggest = greenfield shape proposal; adapt = match real layout / raise coverage;
|
|
406
|
+
// enforce = contract actually governs code and gates stay on.
|
|
407
|
+
console.log('');
|
|
408
|
+
if (mode === 'enforce' && planOk) {
|
|
409
|
+
console.log('Done — Ark is in ENFORCE mode: your contract governs the code and the gates stay on.');
|
|
410
|
+
console.log('What happens now:');
|
|
411
|
+
console.log(' • Every edit is checked (in CI and, if wired, at write time).');
|
|
412
|
+
console.log(' • Carry out remaining plan steps with /ark-autopilot (safe auto + your approvals).');
|
|
413
|
+
} else if (mode === 'suggest') {
|
|
414
|
+
console.log('Done — Ark is in SUGGEST mode: a starting shape is installed; enforcement grows as you add layers.');
|
|
415
|
+
console.log('What happens now:');
|
|
416
|
+
console.log(' • Gates are on for whatever the contract already matches.');
|
|
417
|
+
console.log(' • Expand coverage as you create real layer folders (see the plan above).');
|
|
418
|
+
if (governedPercent != null) {
|
|
419
|
+
console.log(` • Right now Ark governs ~${governedPercent}% of in-scope files — low is normal on a fresh scaffold.`);
|
|
420
|
+
}
|
|
421
|
+
console.log(' • When you want the agent to drive the plan: /ark-autopilot');
|
|
422
|
+
} else {
|
|
423
|
+
console.log('Done — Ark is in ADAPT mode: config is in place, but the contract still needs to match your real layout.');
|
|
424
|
+
console.log('What happens now:');
|
|
425
|
+
if (governedPercent != null) {
|
|
426
|
+
console.log(
|
|
427
|
+
` • Governed coverage is ~${governedPercent}% — a "clean" plan with low coverage checks almost nothing.`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
console.log(` • See what is unmatched: ${arkCommand(root, 'ark-check', '--coverage')}`);
|
|
431
|
+
console.log(' • On a mature repo, prefer /ark-adopt over forcing a starter preset.');
|
|
432
|
+
console.log(' • Drive fixes with /ark-autopilot once the contract matches; until then ENFORCE is not honest.');
|
|
433
|
+
}
|
|
434
|
+
console.log(` • Re-run the plan anytime: ${arkCommand(root, 'ark-check', '--plan')}`);
|
|
435
|
+
console.log(` • Full project check: ${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config')}`);
|
|
436
|
+
console.log(` • Update Ark later: ${arkCommand(root, 'ark', 'upgrade')}`);
|
|
437
|
+
|
|
438
|
+
// 6) First architecture report — freezes an origin snapshot under .ark/reports/
|
|
439
|
+
// so later --report runs can show evolution. Idempotent: origin is written only once.
|
|
440
|
+
console.log('');
|
|
441
|
+
console.log('Capturing architecture report (origin snapshot on first run)…');
|
|
442
|
+
runArkCheck(
|
|
443
|
+
['--root', root, '--config', 'ark.config.json', '--report', 'ark-report.html'],
|
|
444
|
+
{ cwd: root }
|
|
445
|
+
);
|
|
446
|
+
return 0;
|
|
447
|
+
} finally {
|
|
448
|
+
rl?.close();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function main() {
|
|
453
|
+
const args = parseArgs(process.argv);
|
|
454
|
+
if (args.help || !args.command) {
|
|
455
|
+
console.log(usage());
|
|
456
|
+
return 0;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (args.command === 'start') {
|
|
460
|
+
try {
|
|
461
|
+
return await start(args);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
464
|
+
return 2;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (args.command === 'init') {
|
|
469
|
+
try {
|
|
470
|
+
return await init(args);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
473
|
+
return 2;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (args.command === 'upgrade' || args.command === 'update') {
|
|
478
|
+
try {
|
|
479
|
+
return await upgrade(args);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
482
|
+
return 2;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
console.error(`Unknown command: ${args.command}`);
|
|
487
|
+
console.error(usage());
|
|
488
|
+
return 2;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
process.exitCode = await main();
|