arkgate 2.9.0 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@ All notable changes to ArkGate (`arkgate`; formerly `ark-runtime-kernel`) are do
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 2.9.1 — 2026-07-09
8
+
9
+ Field-install honesty: non-TTY start, baseline→CI sync, pin, false-green soft block, Grok defaults.
10
+ **No intentional CLI flag or JSON shape breaks.**
11
+
12
+ ### Fixed
13
+
14
+ - **Non-TTY `ark start` / `ark init`:** when stdin/stdout are not a TTY and `--yes` was
15
+ omitted, guided setup no longer throws on a null readline interface. Non-interactive
16
+ sessions use the same defaults as `--yes` (agents never hang on prompts).
17
+ - **Baseline → CI/scripts sync:** after a successful `--update-baseline`, existing
18
+ `package.json` scripts and GitHub Actions workflows that already run `ark-check` gain
19
+ `--baseline .ark-baseline.json` without a full `--force` reinstall of gate templates.
20
+ - **Grok in default agent tools:** no-signal `--install-agent-gates` now installs
21
+ claude + cursor + codex + **grok**; `GROK_BUILD` / `XAI_GROK` env also adds Grok when
22
+ other hosts are detected.
23
+
24
+ ### Added
25
+
26
+ - **`start` pins `arkgate` as a devDependency** (opt out with `--no-install`) so CI/`npx`
27
+ are not forced to rely on a stale global install.
28
+ - **False-green contract soft block:** doctor adoption gap
29
+ `contract-false-green-io-under-application` when Domain/Persistence are empty while
30
+ Application globs still cover I/O dirs (airtable/supabase/prisma/…). `ark start`
31
+ wrap-up and `/ark-autopilot` steer to `/ark-adopt` / `/ark-contract` instead of pure
32
+ ENFORCE victory.
33
+ - **`bin/lib/field-install.mjs`:** field-install helpers (baseline sync, pin, false-green)
34
+ extracted from the agent-gates surface for scannability; re-exported from `agent-gates.mjs`.
35
+
36
+ ### Changed
37
+
38
+ - **Public ROADMAP:** active backlog is **Track W** (constrained write → verified repair:
39
+ W1–W6). Finished foundation tracks live under Shipped.
40
+
7
41
  ## 2.9.0 — 2026-07-09
8
42
 
9
43
  Track P: slice isolation, vertical-slice + DDD presets, skill surface, and adoption depth.
package/bin/ark-check.mjs CHANGED
@@ -44,6 +44,7 @@ import {
44
44
  REQUIRED_GATE_FILES,
45
45
  codexPromptsDir,
46
46
  } from './lib/agent-gates.mjs';
47
+ import { syncBaselineIntoCheckSurfaces } from './lib/field-install.mjs';
47
48
  import {
48
49
  detectEnforcement,
49
50
  renderHtmlReport,
@@ -1087,8 +1088,25 @@ async function main() {
1087
1088
  console.log('Gate with: ark-check --root . --config ark.config.json --strict-config');
1088
1089
  return;
1089
1090
  }
1090
- const { fullPath, count } = writeBaseline(root, args.baseline, violations);
1091
+ const { fullPath, count } = writeBaseline(root, baselineName, violations);
1091
1092
  console.log(`Wrote ${fullPath} with ${count} frozen violation key(s).`);
1093
+ // Keep existing package.json scripts + CI workflows on the ratchet without a
1094
+ // full --force reinstall (field log: baseline after start left CI without --baseline).
1095
+ const baselineRel = path.isAbsolute(baselineName)
1096
+ ? path.relative(root, baselineName).split(path.sep).join('/')
1097
+ : String(baselineName).replace(/^\.\/+/, '');
1098
+ const sync = syncBaselineIntoCheckSurfaces(root, {
1099
+ baselineRel: baselineRel || '.ark-baseline.json',
1100
+ });
1101
+ if (sync.changed.length > 0) {
1102
+ console.log(
1103
+ `Synced --baseline into: ${sync.changed.map((c) => c.file).join(', ')}`
1104
+ );
1105
+ } else {
1106
+ console.log(
1107
+ 'No existing check scripts/workflows needed a --baseline patch (add check:architecture or re-run --install-agent-gates).'
1108
+ );
1109
+ }
1092
1110
  console.log('Commit it and gate CI with: ark-check --baseline (only NEW violations fail).');
1093
1111
  if (summary.total > 0) printViolationBreakdown(summary);
1094
1112
  return;
package/bin/ark.mjs CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  resolveArchetypePreset,
19
19
  resolveOperatingMode,
20
20
  } from './ark-shared.mjs';
21
+ import { pinArkgateDevDependency, FALSE_GREEN_GAP_ID } from './lib/field-install.mjs';
21
22
 
22
23
  const here = path.dirname(fileURLToPath(import.meta.url));
23
24
  const arkCheck = path.join(here, 'ark-check.mjs');
@@ -70,24 +71,32 @@ Commands:
70
71
 
71
72
  Options:
72
73
  --yes Non-interactive defaults: create config if needed, install gate templates, run strict check.
74
+ (Also the implicit default when stdin/stdout are not a TTY — agents never hang on prompts.)
73
75
  --force Allow generated files to overwrite existing files.
74
76
  --no-strict Skip the final strict ark-check run.
75
- --no-install (upgrade) Refresh gates/skills only; don't reinstall the package.
77
+ --no-install Skip adding/installing arkgate as a project devDependency (start/upgrade).
76
78
  --preset Start from a named architecture preset instead of detection.
77
79
  --archetype Application shape from templates/architecture-playbook.json (maps to the matching preset).
78
80
  Valid ids: crud-product, api-backend, frontend-surface, library-sdk, cli-utility,
79
81
  worker-pipeline, event-coordinator, integration-bridge, multi-app-workspace, prototype-spike,
80
82
  vertical-slice-product, ddd-bounded-contexts.
81
- --tools Comma-separated agents to gate (claude,cursor,codex,windsurf,cline,copilot,kiro,roo,continue,gemini).
82
- Omit to auto-detect from each tool's config dir, falling back to claude+cursor+codex.
83
+ --tools Comma-separated agents to gate (claude,cursor,codex,grok,windsurf,cline,copilot,kiro,roo,continue,gemini).
84
+ Omit to auto-detect from each tool's config dir, falling back to claude+cursor+codex+grok.
83
85
 
84
86
  Interactive mode (TTY, no --yes): asks what application shape you are building and maps it to a preset.
87
+ Non-interactive (no TTY): uses the same defaults as --yes — never calls readline on a null interface.
85
88
  `;
86
89
  }
87
90
 
88
- // The package-manager command that adds arkgate@latest as a dev dependency.
89
- function packageInstallArgv(root) {
90
- const spec = 'arkgate@latest';
91
+ // The package-manager command that adds arkgate as a dev dependency.
92
+ // Prefer an explicit version/range when pin already chose one (avoid pin=^2.9.0 then
93
+ // `npm i arkgate@latest` rewriting package.json to a different range).
94
+ function packageInstallArgv(root, versionSpec) {
95
+ const range =
96
+ typeof versionSpec === 'string' && versionSpec.trim()
97
+ ? versionSpec.trim()
98
+ : 'latest';
99
+ const spec = range.startsWith('arkgate@') ? range : `arkgate@${range}`;
91
100
  const pm = detectPackageManager(root);
92
101
  if (pm === 'pnpm') return ['pnpm', ['add', '-D', spec]];
93
102
  if (pm === 'yarn') return ['yarn', ['add', '-D', spec]];
@@ -166,13 +175,49 @@ function isInteractiveTty() {
166
175
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
167
176
  }
168
177
 
178
+ /**
179
+ * True when prompts should be skipped and guided defaults applied.
180
+ * Agents typically have no TTY — never call readline on a null interface.
181
+ */
182
+ export function shouldUseNonInteractiveDefaults(args, tty = isInteractiveTty()) {
183
+ return Boolean(args?.yes || !tty);
184
+ }
185
+
169
186
  async function askYesNo(rl, question, defaultYes = true) {
187
+ if (!rl) {
188
+ // Defensive: non-TTY callers must not reach here; return the default rather than throw.
189
+ return defaultYes;
190
+ }
170
191
  const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
171
192
  const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
172
193
  if (!answer) return defaultYes;
173
194
  return answer === 'y' || answer === 'yes';
174
195
  }
175
196
 
197
+ /**
198
+ * Pin arkgate in package.json (and optionally run the package manager).
199
+ * start calls this so CI/`npx` is not forced to rely on a stale global install.
200
+ *
201
+ * @param {string} root
202
+ * @param {{ install?: boolean, runPackageManager?: boolean }} [opts]
203
+ */
204
+ export function ensureProjectArkgateDependency(root, opts = {}) {
205
+ const install = opts.install !== false;
206
+ const runPm = opts.runPackageManager === true;
207
+ if (!install) {
208
+ return { pinned: { changed: false, reason: 'skipped-no-install' }, installStatus: null };
209
+ }
210
+ const pinned = pinArkgateDevDependency(root);
211
+ let installStatus = null;
212
+ // Only run the package manager after a successful pin change — avoid surprise
213
+ // network on every start when arkgate is already listed.
214
+ if (runPm && pinned.changed) {
215
+ const [command, commandArgs] = packageInstallArgv(root, pinned.version);
216
+ installStatus = runCommand(command, commandArgs, root);
217
+ }
218
+ return { pinned, installStatus };
219
+ }
220
+
176
221
  async function resolveArchetypeInteractive(rl, root) {
177
222
  console.log('');
178
223
  console.log('What are you building? (application shape — not a framework name)');
@@ -214,12 +259,19 @@ function resolveInitPreset(args) {
214
259
  async function init(args) {
215
260
  const root = args.root;
216
261
  const configPath = path.join(root, 'ark.config.json');
217
- const interactive = !args.yes && isInteractiveTty();
262
+ const nonInteractive = shouldUseNonInteractiveDefaults(args);
263
+ const interactive = !nonInteractive;
218
264
  const rl = interactive
219
265
  ? readline.createInterface({ input: process.stdin, output: process.stdout })
220
266
  : null;
221
267
 
222
268
  try {
269
+ if (nonInteractive && !args.yes) {
270
+ console.log(
271
+ 'Non-interactive session (no TTY) — using guided defaults (same as --yes). Pass flags to override.'
272
+ );
273
+ }
274
+
223
275
  let archetype = args.archetype;
224
276
  let preset = args.preset;
225
277
 
@@ -231,9 +283,8 @@ async function init(args) {
231
283
  const resolved = resolveArchetypePreset(archetype);
232
284
  preset = resolved.preset;
233
285
  console.log(`Using archetype ${archetype} → preset ${preset} (${resolved.label})`);
234
- } else if (!preset && !interactive && !args.yes) {
235
- // non-TTY without --yes/--preset/--archetype: fall back to detection init
236
- } else if (!preset && args.yes && !archetype) {
286
+ } else if (!preset && nonInteractive && !archetype) {
287
+ // non-TTY / --yes without explicit shape: recommend → preset
237
288
  const rec = buildArchitectureRecommendation(root);
238
289
  preset = rec.preset;
239
290
  archetype = rec.archetype;
@@ -244,7 +295,7 @@ async function init(args) {
244
295
  if (fs.existsSync(configPath)) {
245
296
  shouldInit = args.force
246
297
  ? true
247
- : args.yes
298
+ : nonInteractive
248
299
  ? false
249
300
  : await askYesNo(rl, 'ark.config.json already exists. Regenerate it?', false);
250
301
  }
@@ -259,7 +310,8 @@ async function init(args) {
259
310
  console.log('Skipped ark.config.json generation.');
260
311
  }
261
312
 
262
- const installGates = args.yes || (await askYesNo(rl, 'Configure agent and CI gate templates?', true));
313
+ const installGates =
314
+ nonInteractive || (await askYesNo(rl, 'Configure agent and CI gate templates?', true));
263
315
  if (installGates) {
264
316
  const gateArgs = ['--root', root, '--install-agent-gates'];
265
317
  if (args.tools) gateArgs.push('--tools', args.tools);
@@ -269,7 +321,8 @@ async function init(args) {
269
321
  }
270
322
 
271
323
  const runStrict =
272
- args.strict && (args.yes || (await askYesNo(rl, 'Run strict architecture check now?', true)));
324
+ args.strict &&
325
+ (nonInteractive || (await askYesNo(rl, 'Run strict architecture check now?', true)));
273
326
  if (runStrict) {
274
327
  return runArkCheck(
275
328
  ['--root', root, '--config', 'ark.config.json', '--strict-config'],
@@ -299,7 +352,8 @@ async function init(args) {
299
352
  // orchestrates existing steps (recommend → init → --plan) and frames each in outcome terms.
300
353
  async function start(args) {
301
354
  const root = args.root;
302
- const interactive = !args.yes && isInteractiveTty();
355
+ const nonInteractive = shouldUseNonInteractiveDefaults(args);
356
+ const interactive = !nonInteractive;
303
357
  const rl = interactive
304
358
  ? readline.createInterface({ input: process.stdin, output: process.stdout })
305
359
  : null;
@@ -310,6 +364,11 @@ async function start(args) {
310
364
  "I'll look at your code, suggest a shape, set up the guardrails, and show you a plan."
311
365
  );
312
366
  console.log('Nothing in your code is changed — this only adds Ark configuration.');
367
+ if (nonInteractive && !args.yes) {
368
+ console.log(
369
+ 'Non-interactive session (no TTY) — using guided defaults (same as --yes). Pass flags to override.'
370
+ );
371
+ }
313
372
 
314
373
  // 1) Look at the project.
315
374
  let rec;
@@ -332,7 +391,8 @@ async function start(args) {
332
391
  );
333
392
  console.log('match the contract to how your code is already organized, and flag only genuine issues.');
334
393
  }
335
- const proceed = args.yes || (await askYesNo(rl, '\nSet Ark up for this shape?', true));
394
+ const proceed =
395
+ nonInteractive || (await askYesNo(rl, '\nSet Ark up for this shape?', true));
336
396
  if (!proceed) {
337
397
  archetype = interactive ? await resolveArchetypeInteractive(rl, root) : rec.archetype;
338
398
  }
@@ -340,6 +400,26 @@ async function start(args) {
340
400
  archetype = await resolveArchetypeInteractive(rl, root);
341
401
  }
342
402
 
403
+ // 2b) Pin arkgate as a project devDependency so CI/npx do not depend on a stale global.
404
+ if (args.install !== false && fs.existsSync(path.join(root, 'package.json'))) {
405
+ const { pinned, installStatus } = ensureProjectArkgateDependency(root, {
406
+ install: true,
407
+ runPackageManager: true,
408
+ });
409
+ if (pinned.changed) {
410
+ console.log(` Pinned arkgate@${pinned.version} in package.json devDependencies.`);
411
+ if (installStatus !== null && installStatus !== 0) {
412
+ console.log(
413
+ ` Package manager install exited ${installStatus} — package.json is still pinned; run install when online.`
414
+ );
415
+ }
416
+ } else if (pinned.reason === 'already-present') {
417
+ console.log(` arkgate already in package.json (${pinned.version}).`);
418
+ }
419
+ } else if (args.install === false) {
420
+ console.log(' Skipping arkgate package pin (--no-install).');
421
+ }
422
+
343
423
  // 3) Set up config + gates. Greenfield → the shape's preset; an established repo → detection,
344
424
  // so the contract anchors to the directories you already have instead of aspirational globs.
345
425
  console.log('');
@@ -473,8 +553,28 @@ async function start(args) {
473
553
 
474
554
  // 5) Plain-language wrap-up — one next step, status light only.
475
555
  // Modes are detected (Suggest/Adapt/Enforce), not user-picked settings.
556
+ // Soft-block false-green using the same doctor adoption gap (no second detector).
557
+ let falseGreenGap = null;
558
+ try {
559
+ const doc = JSON.parse(doctorCapture.stdout || '{}');
560
+ falseGreenGap = (doc.doctor?.adoption?.gaps ?? []).find(
561
+ (g) => g?.id === FALSE_GREEN_GAP_ID
562
+ );
563
+ } catch {
564
+ falseGreenGap = null;
565
+ }
566
+ if (falseGreenGap && mode === 'enforce') {
567
+ mode = 'adapt';
568
+ planOk = false;
569
+ }
570
+
476
571
  console.log('');
477
- if (mode === 'enforce' && planOk) {
572
+ if (falseGreenGap) {
573
+ console.log('Done — status: ADAPT (contract may be a false green — do not stop at a clean plan).');
574
+ console.log('What happens now:');
575
+ console.log(` • ${falseGreenGap.message}`);
576
+ console.log(` • Next: ${falseGreenGap.fix}`);
577
+ } else if (mode === 'enforce' && planOk) {
478
578
  console.log('Done — status: ENFORCE (gates can honestly protect you).');
479
579
  console.log('What happens now:');
480
580
  console.log(' • Every edit is checked (in CI and, if wired, at write time).');
@@ -495,11 +595,16 @@ async function start(args) {
495
595
  }
496
596
  console.log('');
497
597
  console.log('Next (the only flow you need):');
498
- console.log(' 1. In your agent: /ark-autopilot');
499
- console.log(' origin report, adoption, plan, safe fixes, leave gates on.');
598
+ if (falseGreenGap) {
599
+ console.log(' 1. In your agent: /ark-adopt (or /ark-contract) fix the contract first');
600
+ console.log(' → reclassify I/O dirs out of Application; then /ark-autopilot for residual debt.');
601
+ } else {
602
+ console.log(' 1. In your agent: /ark-autopilot');
603
+ console.log(' → origin report, adoption, plan, safe fixes, leave gates on.');
604
+ }
500
605
  console.log(` 2. Status anytime: ${arkCommand(root, 'ark-check', '--doctor')}`);
501
606
  console.log(` 3. After edits: ${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config')}`);
502
- if (mode === 'adapt' && planOk) {
607
+ if (mode === 'adapt' && planOk && !falseGreenGap) {
503
608
  console.log(
504
609
  ` 4. When green but cores still optional: ${arkCommand(root, 'ark-check', '--ratchet-cores')} → honest ENFORCE`
505
610
  );
@@ -560,4 +665,10 @@ async function main() {
560
665
  return 2;
561
666
  }
562
667
 
563
- process.exitCode = await main();
668
+ // Only run when executed as the CLI entry (not when imported by unit tests).
669
+ const isMain =
670
+ Boolean(process.argv[1]) &&
671
+ path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
672
+ if (isMain) {
673
+ process.exitCode = await main();
674
+ }
@@ -23,6 +23,7 @@ import {
23
23
  applyFrameworkLayoutOverlays,
24
24
  } from '../ark-shared.mjs';
25
25
  import { CORE_LAYER_NAMES } from './core-ratchet.mjs';
26
+ import { falseGreenAdoptionGap } from './field-install.mjs';
26
27
  import {
27
28
  assessCodexHomeMcp,
28
29
  codexArkBlockHasPreferredBin,
@@ -365,6 +366,18 @@ export function checkArgsForRoot(root, { requireGates = false } = {}) {
365
366
  return `--root . --config ark.config.json --strict-config${baselineFlag}${gatesFlag}`;
366
367
  }
367
368
 
369
+ // Field-install helpers live in field-install.mjs (keep agent-gates scannable).
370
+ // Re-export for callers that already import from this module.
371
+ export {
372
+ ensureBaselineFlagInCheckCommand,
373
+ syncBaselineIntoCheckSurfaces,
374
+ pinArkgateDevDependency,
375
+ IO_DIR_SEGMENTS,
376
+ detectContractFalseGreenRisk,
377
+ FALSE_GREEN_GAP_ID,
378
+ falseGreenAdoptionGap,
379
+ } from './field-install.mjs';
380
+
368
381
  export function packageManager(root) {
369
382
  // CI always require-gates; baseline follows checkArgsForRoot.
370
383
  const checkArgs = checkArgsForRoot(root, { requireGates: true });
@@ -769,10 +782,20 @@ export function resolveTools(args) {
769
782
  if (fs.existsSync(path.join(root, '.gemini'))) detected.add('gemini');
770
783
  // copilot has no reliable directory signal (.github exists in most repos),
771
784
  // so it is explicit-only via --tools.
772
- // No signal at all: fall back to writing the primary tools' templates so a fresh
773
- // project still gets a complete, reviewable starter set.
785
+ // Host signals: Grok Build / xAI agents often have no project `.grok/` yet but
786
+ // set an env marker (or run with GROK_*). Include Grok so skills install there.
787
+ if (
788
+ process.env.GROK_BUILD === '1' ||
789
+ process.env.GROK_BUILD === 'true' ||
790
+ process.env.XAI_GROK === '1' ||
791
+ process.env.XAI_GROK === 'true'
792
+ ) {
793
+ detected.add('grok');
794
+ }
795
+ // No signal at all: fall back to a complete starter set including Grok (field
796
+ // log: default claude+cursor+codex silently omitted Grok skills for Grok hosts).
774
797
  if (detected.size === 0) {
775
- return { tools: new Set(['claude', 'cursor', 'codex']), source: 'default' };
798
+ return { tools: new Set(['claude', 'cursor', 'codex', 'grok']), source: 'default' };
776
799
  }
777
800
  return { tools: detected, source: 'detected' };
778
801
  }
@@ -1539,6 +1562,16 @@ export function collectAdoptionGaps(root, config, coverage) {
1539
1562
  }
1540
1563
  }
1541
1564
 
1565
+ // --- False-green contract (field-install detector; doctor skillGaps already cover missing skills) ---
1566
+ let contractFalseGreen = null;
1567
+ if (!isProducer && config) {
1568
+ const gap = falseGreenAdoptionGap(root, config, coverage);
1569
+ if (gap) {
1570
+ contractFalseGreen = { risk: true, message: gap.message, fix: gap.fix };
1571
+ gaps.push(gap);
1572
+ }
1573
+ }
1574
+
1542
1575
  return {
1543
1576
  gaps,
1544
1577
  hosts,
@@ -1549,6 +1582,7 @@ export function collectAdoptionGaps(root, config, coverage) {
1549
1582
  baseline,
1550
1583
  layerBalance,
1551
1584
  deployPath,
1585
+ contractFalseGreen,
1552
1586
  };
1553
1587
  }
1554
1588