arkgate 4.8.18 → 4.8.19

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +5 -6
  3. package/bin/ark-mcp-runtime.mjs +3 -3
  4. package/bin/ark-shared.mjs +187 -16
  5. package/bin/ark.mjs +18 -18
  6. package/bin/lib/ci-and-commands.mjs +2 -2
  7. package/bin/lib/doctor-human.mjs +2 -0
  8. package/bin/lib/doctor-next-actions.mjs +35 -0
  9. package/bin/lib/doctor-plan.mjs +6 -8
  10. package/bin/lib/gate-files.mjs +22 -1
  11. package/bin/lib/host-support-matrix.mjs +1 -1
  12. package/bin/lib/html-report-depth.mjs +1 -1
  13. package/bin/lib/install-migrate.mjs +3 -4
  14. package/bin/lib/managed-upgrade.mjs +30 -5
  15. package/bin/lib/mcp-adoption.mjs +4 -1
  16. package/bin/lib/presets.mjs +14 -14
  17. package/bin/lib/scan-files.mjs +3 -26
  18. package/bin/lib/skill-install.mjs +3 -2
  19. package/bin/lib/start-preview.mjs +60 -5
  20. package/bin/lib/write-path-detect.mjs +3 -2
  21. package/dist/{diagnosticCatalog-BNxKdcN4.d.ts → diagnosticCatalog-D2v0ASN2.d.ts} +1 -1
  22. package/dist/index.cjs +1 -1
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +1 -1
  25. package/dist/nestjs/index.cjs +1 -1
  26. package/dist/nestjs/index.js +1 -1
  27. package/dist/runtime/index.cjs +1 -1
  28. package/dist/runtime/index.d.ts +1 -1
  29. package/dist/runtime/index.js +1 -1
  30. package/docs/README.md +1 -1
  31. package/docs/agent-guide.md +3 -1
  32. package/docs/ai-gates.md +7 -3
  33. package/docs/develop.md +1 -2
  34. package/docs/use.md +1 -2
  35. package/package.json +1 -1
  36. package/server.json +2 -2
  37. package/templates/agent-skills/README.md +1 -1
  38. package/templates/agent-skills/ark-place/SKILL.md +2 -0
  39. package/templates/hooks/opencode-ark-write-gate.mjs +2 -2
  40. package/templates/skills/ark-place.md +2 -0
@@ -13,7 +13,7 @@ import * as arkShared from '../ark-shared.mjs';
13
13
  import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
14
14
  import { describePackageVersionDualTruth } from './field-install.mjs';
15
15
  import { detectAgentHomeGaps } from './agent-homes.mjs';
16
- import { collectDoctorNextActions } from './doctor-next-actions.mjs';
16
+ import { collectDoctorNextActions, preferredDoctorPrimaryNextAction } from './doctor-next-actions.mjs';
17
17
  import { printDoctorCompactHuman, printDoctorDetailsHuman } from './doctor-human.mjs';
18
18
  import { collectLayerOwnerResidual, layerGuidanceLine, placementDescriptionFields } from './layer-description.mjs';
19
19
  import { collectAdrPresenceResidual, printAdrPresenceHint } from './adr-presence.mjs';
@@ -653,6 +653,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
653
653
  const statusTransitionCatalog = collectStatusTransitionCatalogResidual({ root, config, files, statesTransitions });
654
654
  const noDomainFrontend = collectNoDomainFrontendResidual({ config, coverage: cov, designSmells });
655
655
  const { invariantCoverageRoots, invariantTestsPath } = collectInvariantCoverageResiduals({ adopted: isAdopted(adopted) || options.requireGates === true, coverage: config?.coverage, config, root });
656
+ const selfHost = packageVersionTruth?.selfHost === true || packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST';
656
657
  const { coverageHonesty, baselineHonesty, writePathHonesty, productHonesty } =
657
658
  computeDoctorEnforcementHonesty({
658
659
  governedPercent: cov.governed.percent,
@@ -681,13 +682,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
681
682
  : rulesUnderContract?.active === true || arkRun?.active === true || doctorAdvisories.arkOrder?.active === true
682
683
  ? { active: true, extraMergeTeeth: arkRun?.extraMergeTeeth === true || doctorAdvisories.arkOrder?.extraMergeTeeth === true }
683
684
  : null,
684
- primaryNextAction:
685
- adopted === 'not-adopted' ? NOT_ADOPTED_NEXT_ACTION : postGreenPath?.action ?? dualTruthNext,
685
+ primaryNextAction: preferredDoctorPrimaryNextAction({ adopted, packageInstalled, selfHost, packageVersionTruth, postGreenPath, dualTruthNext }),
686
686
  operatingMode,
687
687
  packageInstalled,
688
- selfHost:
689
- packageVersionTruth?.selfHost === true ||
690
- packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
688
+ selfHost,
691
689
  nativeFailClosed: writePath.nativeFailClosed,
692
690
  nativeFailClosedPolicy: writePath.nativeFailClosedPolicy,
693
691
  adopted,
@@ -752,7 +750,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
752
750
  adoptionStance: adopted,
753
751
  ...(adopted === 'not-adopted'
754
752
  ? {
755
- primaryNextAction: NOT_ADOPTED_NEXT_ACTION,
753
+ primaryNextAction: productHonesty.primaryNextAction || NOT_ADOPTED_NEXT_ACTION,
756
754
  ...(postGreenPath ? DESIGN_WEAK_HONESTY_FLAGS : {}),
757
755
  }
758
756
  : postGreenPath
@@ -908,7 +906,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
908
906
  coverageHonesty,
909
907
  cov,
910
908
  packageVersionTruth,
911
- dualTruthNext,
909
+ dualTruthNext, packageInstalled, selfHost,
912
910
  activeCount,
913
911
  writePath,
914
912
  gatesMissing,
@@ -152,9 +152,30 @@ export function isCompactRouterAgentsContent(text) {
152
152
  return typeof text === 'string' && COMPACT_ROUTER.test(text);
153
153
  }
154
154
 
155
+ function hasCursorFailClosedHook(root) {
156
+ try {
157
+ const parsed = readJson(path.join(root, '.cursor', 'hooks.json'));
158
+ const entries = Array.isArray(parsed?.hooks?.preToolUse) ? parsed.hooks.preToolUse : [];
159
+ return entries.some(
160
+ (entry) =>
161
+ entry &&
162
+ typeof entry === 'object' &&
163
+ entry.failClosed === true &&
164
+ typeof entry.command === 'string' &&
165
+ /(?:^|\s)--hook(?:\s|$)/.test(entry.command)
166
+ );
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+
172
+ function hasCursorCompactRegistration(root) {
173
+ return hasArkMcpRegistration(root, '.cursor/mcp.json') && hasCursorFailClosedHook(root);
174
+ }
175
+
155
176
  function hasCompactHostRegistration(root, host) {
156
177
  if (host === 'none') return hasArkMcpRegistration(root);
157
- if (host === 'cursor') return hasArkMcpRegistration(root, '.cursor/mcp.json');
178
+ if (host === 'cursor') return hasCursorCompactRegistration(root);
158
179
  if (host === 'codex') return hasCodexCompactRegistration(root);
159
180
  const files = COMPACT_HOST_FILES[host];
160
181
  return Boolean(files) && files.every((relativePath) => fs.existsSync(path.join(root, relativePath)));
@@ -197,7 +197,7 @@ Codex hard write covers only a complete local \`apply_patch\`; Cursor covers onl
197
197
  installed + trusted, while shell/direct
198
198
  filesystem writes, hosted or specialized opt-out paths, and human edits still rely on CI.
199
199
 
200
- 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 **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run \`arkgate-check --doctor\` (or \`ark-check --doctor\`) for the evidence actually detected in the current repository.`;
200
+ 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. **Write-gate order:** install and trust the host pre-hook first. MCP prepare is fallback when that path is missing or fail-open. 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 **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run \`arkgate-check --doctor\` (or \`ark-check --doctor\`) for the evidence actually detected in the current repository.`;
201
201
  }
202
202
 
203
203
  /**
@@ -224,7 +224,7 @@ export function writePathModeHint(mode) {
224
224
  case 'reject-only':
225
225
  return 'Hard write boundary without repair payload; edits can be blocked without guided re-entry.';
226
226
  case 'mcp-only':
227
- return 'Advisory MCP only — prepare-write/autoPatch available, no hard PreToolUse for this host.';
227
+ return 'Fell back to MCP prepare — advisory prepare-write/autoPatch; no hard PreToolUse for this host.';
228
228
  case 'none':
229
229
  return 'No hard write boundary or advisory MCP for the active host (or host is unknown in this process).';
230
230
  default:
@@ -200,10 +200,9 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
200
200
  })
201
201
  );
202
202
  if (selectedTools.has('cursor')) {
203
- add('.cursor/mcp.json', mcpJson(root));
204
- // whole-file for managed upgrade/manifest (json-merge is install-time only via
205
- // mergeCursorArkHook below — same pattern as Antigravity hooks).
203
+ // Write-gate order: pre-hook first, MCP prepare as fallback + identity.
206
204
  add('.cursor/hooks.json', cursorHooks(root));
205
+ add('.cursor/mcp.json', mcpJson(root));
207
206
  if (!compact) add('.cursor/rules/ark.mdc', cursorRule(root));
208
207
  }
209
208
  if (selectedTools.has('claude')) add('.claude/settings.json', claudeSettings(root));
@@ -213,8 +212,8 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
213
212
  if (!compact) add('docs/ark-codex-config.toml', codexTomlSnippet(root));
214
213
  }
215
214
  if (selectedTools.has('grok')) {
216
- add('.grok/config.toml', grokProjectConfig(root));
217
215
  add('.grok/hooks/ark-write-gate.json', grokHooks(root));
216
+ add('.grok/config.toml', grokProjectConfig(root));
218
217
  }
219
218
  if (selectedTools.has('antigravity')) {
220
219
  add('.agents/hooks.json', antigravityHooks(root));
@@ -17,6 +17,7 @@ import {
17
17
  arkPackageVersion,
18
18
  detectActiveAgentHost,
19
19
  normalizeToolsList,
20
+ planSkillInstall,
20
21
  skillContentIdentity,
21
22
  } from './skill-install.mjs';
22
23
 
@@ -396,8 +397,11 @@ function summaryFor(assets, manifestChanged) {
396
397
  for (const asset of assets) states[asset.state] = (states[asset.state] ?? 0) + 1;
397
398
  const applying = assets.filter((asset) => asset.willApply);
398
399
  const wouldWrite = applying.length;
399
- // Public-summary compatibility: stamp-only writes are no longer scheduled.
400
- const metadataRefresh = 0;
400
+ // Same-body stamp drift (arkVersion / description prefix) stays state=current
401
+ // and is scheduled via planSkillInstall stamp-refresh — not a second pipeline.
402
+ const metadataRefresh = applying.filter(
403
+ (asset) => asset.state === 'current' && asset.reason === 'stamp-refresh'
404
+ ).length;
401
405
  const customizedPreserved = assets.filter((asset) => asset.state === 'customized').length;
402
406
  const fileChanges = applying.length;
403
407
  return {
@@ -477,9 +481,23 @@ export function planManagedUpgrade(root, options = {}) {
477
481
  refreshSkills &&
478
482
  catalogAsset.kind === 'skill' &&
479
483
  classified.state === 'customized';
484
+ // HS01 / #284: identity-current skill whose arkVersion / description prefix
485
+ // lagged the package — reuse planSkillInstall stamp-refresh, do not reclassify.
486
+ const stampRefresh =
487
+ !unparsedScope &&
488
+ catalogAsset.kind === 'skill' &&
489
+ classified.state === 'current' &&
490
+ currentScoped != null &&
491
+ planSkillInstall({
492
+ existingContent: currentScoped,
493
+ targetContent: desiredScoped,
494
+ packageVersion: catalog.version ?? arkPackageVersion(),
495
+ scope: 'repo',
496
+ }).reason === 'stamp-refresh';
480
497
  const canApply =
481
498
  classified.state === 'stale' ||
482
499
  skillRefresh ||
500
+ stampRefresh ||
483
501
  (classified.state === 'missing' && (!recorded || accepted)) ||
484
502
  (classified.state === 'conflicted' && accepted);
485
503
  const blocked = classified.requiresConsent && !accepted && !skillRefresh;
@@ -490,7 +508,11 @@ export function planManagedUpgrade(root, options = {}) {
490
508
  kind: catalogAsset.kind,
491
509
  scope: catalogAsset.scope,
492
510
  ...classified,
493
- ...(unparsedScope ? { reason: 'unparsed managed TOML scope preserved' } : {}),
511
+ ...(unparsedScope
512
+ ? { reason: 'unparsed managed TOML scope preserved' }
513
+ : stampRefresh
514
+ ? { reason: 'stamp-refresh' }
515
+ : {}),
494
516
  action: canApply ? (currentScoped == null ? 'create' : 'update') : 'none',
495
517
  willApply: canApply,
496
518
  blocked,
@@ -970,8 +992,11 @@ export function renderManagedUpgrade(plan, options = {}) {
970
992
  const customizedPreserved = summary.customizedPreserved ?? summary.states?.customized ?? 0;
971
993
  const blocked = summary.blocked ?? 0;
972
994
  console.log(
973
- `Managed assets: ${managedAssets}; would write: ${wouldWrite}; ` +
974
- `customized preserved: ${customizedPreserved}; blocked conflicts/deletions: ${blocked}.`
995
+ `Managed assets: ${managedAssets}; would write: ${wouldWrite}` +
996
+ (summary.metadataRefresh
997
+ ? `; metadata refresh: ${summary.metadataRefresh}`
998
+ : '') +
999
+ `; customized preserved: ${customizedPreserved}; blocked conflicts/deletions: ${blocked}.`
975
1000
  );
976
1001
  const honesty =
977
1002
  plan.selfService ??
@@ -191,7 +191,10 @@ export function collectAdoptionGaps(root, config, coverage) {
191
191
  host: 'cursor',
192
192
  dir: '.cursor',
193
193
  skill: (n) => path.join(root, '.cursor', 'commands', `${n}.md`),
194
- extras: [['.cursor/mcp.json', 'MCP config']],
194
+ extras: [
195
+ ['.cursor/hooks.json', 'write-gate hook'],
196
+ ['.cursor/mcp.json', 'MCP config (fallback)'],
197
+ ],
195
198
  toolsFlag: 'cursor',
196
199
  },
197
200
  {
@@ -10,6 +10,7 @@ import {
10
10
  DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
11
11
  discoverRepoUnits,
12
12
  DEFAULT_INTENT_PREFIXES,
13
+ monorepoRoleSourceGlobs,
13
14
  resolveIncludeRoots,
14
15
  } from '../ark-shared.mjs';
15
16
  import { withArkConfigMetadata } from './config-contract.mjs';
@@ -630,20 +631,18 @@ export const ARCHITECTURE_PRESETS = {
630
631
  // Turborepo: apps/ + packages/; Nx enterprise: apps/ + libs/ (+ packages/).
631
632
  if (include.length === 0) include = ['packages', 'apps', 'libs'];
632
633
  // Library Domain bags stay under packages/ or libs/ — never api/** or client/**.
633
- const roleGlobs = (roles, domainScoped) =>
634
- !root
635
- ? []
636
- : units
637
- .filter((unit) => roles.includes(unit.role) && !(domainScoped && unit.root === '.'))
638
- .flatMap((unit) =>
639
- (unit.sourceRoots ?? []).map((sourceRoot) => {
640
- const base = unit.root === '.' ? sourceRoot : sourceRoot === '.' ? unit.root : `${unit.root}/${sourceRoot}`;
641
- return base === '.' ? null : `${base}/**`;
642
- })
643
- )
644
- .filter((pattern) => pattern && (!domainScoped || /^(packages|libs)\//.test(pattern)));
645
- const librarySourcePatterns = roleGlobs(['library'], true);
646
- const applicationSourcePatterns = roleGlobs(['application', 'cli'], false);
634
+ const librarySourcePatterns = monorepoRoleSourceGlobs(units, ['library'], {
635
+ root,
636
+ domainScoped: true,
637
+ });
638
+ const applicationSourcePatterns = monorepoRoleSourceGlobs(units, ['application', 'cli'], {
639
+ root,
640
+ uiMode: 'not-ui',
641
+ });
642
+ const uiApplicationSourcePatterns = monorepoRoleSourceGlobs(units, ['application', 'cli'], {
643
+ root,
644
+ uiMode: 'ui',
645
+ });
647
646
  return presetWithOverlays(
648
647
  {
649
648
  include,
@@ -689,6 +688,7 @@ export const ARCHITECTURE_PRESETS = {
689
688
  '**/http/**',
690
689
  '**/routes/**',
691
690
  '**/hooks/**',
691
+ ...uiApplicationSourcePatterns,
692
692
  // No bare **/lib/** — NEW-ADOPT-LIB-AS-PRESENTATION / NEW-APP-VACUUM-LIB.
693
693
  ],
694
694
  optional: true,
@@ -4,7 +4,9 @@
4
4
  */
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import { isScanExcludedRelative } from '../ark-shared.mjs';
7
+ import { isScanExcludedRelative, isSkippedSourceDir } from '../ark-shared.mjs';
8
+
9
+ export { isSkippedSourceDir };
8
10
 
9
11
  export const SOURCE_FILE_NAME = /\.[cm]?[tj]sx?$/;
10
12
 
@@ -18,31 +20,6 @@ export function isGovernableSourceFile(name) {
18
20
  return SOURCE_FILE_NAME.test(name) && !name.endsWith('.d.ts') && !TEST_FILE_NAME.test(name);
19
21
  }
20
22
 
21
- export function isSkippedSourceDir(name) {
22
- return (
23
- name === 'node_modules' ||
24
- name === 'dist' ||
25
- name === 'coverage' ||
26
- name === 'bench' ||
27
- name === 'benches' ||
28
- name === 'benchmark' ||
29
- name === 'benchmarks' ||
30
- name === 'docs' ||
31
- name === 'documentation' ||
32
- name === 'example' ||
33
- name === 'examples' ||
34
- name === 'fixture' ||
35
- name === 'fixtures' ||
36
- name === 'playground' ||
37
- name === '__tests__' ||
38
- name === '__mocks__' ||
39
- name === 'e2e' ||
40
- // Top-level style Nest/Jest folders (not "testing" helpers inside src)
41
- name === 'test' ||
42
- name === 'tests'
43
- );
44
- }
45
-
46
23
  function isInsideRoot(root, target) {
47
24
  const rel = path.relative(root, target);
48
25
  return rel === '' || (!rel.startsWith(`..${path.sep}`) && rel !== '..' && !path.isAbsolute(rel));
@@ -486,8 +486,9 @@ export function skillContentMatchesTemplate(installedContent, templateContent) {
486
486
  * Repo catalogs belong to that repo's installed package, so an explicit --force
487
487
  * may move them in either direction. Codex home is shared by every repo on the
488
488
  * machine: a package older than the installed home stamp must never win, even
489
- * under --force. In both scopes a version-stamp-only difference is a no-op; the
490
- * skill body is the capability contract.
489
+ * under --force. In both scopes a version-stamp-only difference is
490
+ * `stamp-refresh` (write without --force) so the picker shows this package;
491
+ * the skill body is the capability contract.
491
492
  *
492
493
  * @param {{
493
494
  * existingContent?: string|null,
@@ -40,8 +40,8 @@ const COMPACT_HOST_TEMPLATES = {
40
40
  ['.mcp.json', mcpJson(root)],
41
41
  ],
42
42
  cursor: (root) => [
43
- ['.cursor/mcp.json', mcpJson(root)],
44
43
  ['.cursor/hooks.json', cursorHooks(root)],
44
+ ['.cursor/mcp.json', mcpJson(root)],
45
45
  ],
46
46
  codex: (root) => [
47
47
  ['.codex/hooks.json', codexHooks(root)],
@@ -83,6 +83,17 @@ function digest(content) {
83
83
  return `sha256:${crypto.createHash('sha256').update(content ?? Buffer.alloc(0)).digest('hex')}`;
84
84
  }
85
85
 
86
+ function writeGateOrderGuarantee(tools) {
87
+ const host = typeof tools === 'string' ? tools.trim().toLowerCase() : '';
88
+ if (host === 'opencode') {
89
+ return 'Write gate: MCP prepare only (this host has no hard pre-hook). Required CI is the merge line.';
90
+ }
91
+ if (['claude', 'grok', 'antigravity', 'cursor', 'codex'].includes(host)) {
92
+ return 'Write gate: host pre-hook first; MCP prepare is fallback if that hook is missing or fail-open.';
93
+ }
94
+ return 'Write gate: host pre-hook first when the host has one; MCP prepare is fallback. Required CI is the merge line.';
95
+ }
96
+
86
97
  function change(pathname, before, after) {
87
98
  return {
88
99
  path: pathname,
@@ -207,18 +218,57 @@ function commands(root, args, helpers) {
207
218
  return result;
208
219
  }
209
220
 
221
+ /**
222
+ * Plain one-liner for pnpm minimumReleaseAge / ERR_PNPM_NO_MATURE_MATCHING_VERSION.
223
+ * Host dumps stay out of the recovery — stranger needs one next step.
224
+ */
225
+ export function explainPnpmMaturityBlock(hostOutput) {
226
+ const text = String(hostOutput ?? '');
227
+ if (!/ERR_PNPM_NO_MATURE_MATCHING_VERSION|minimumReleaseAge/i.test(text)) return null;
228
+ const days = text.match(/(\d+)\s*days?/i)?.[1];
229
+ const window = days ? `${days} days` : 'a cooling-off window';
230
+ return `This repo blocks packages younger than ${window} — pin an older release or exclude arkgate temporarily.`;
231
+ }
232
+
233
+ /** Pipe the package manager so recovery can hide maturity internals. */
234
+ export function runStartPackageInstall(command, commandArgs, cwd) {
235
+ const result = spawnSync(command, commandArgs, {
236
+ cwd,
237
+ stdio: ['ignore', 'pipe', 'pipe'],
238
+ encoding: 'utf8',
239
+ });
240
+ return {
241
+ status: result.status ?? 1,
242
+ stdout: result.stdout ?? '',
243
+ stderr: result.stderr ?? '',
244
+ };
245
+ }
246
+
210
247
  /**
211
248
  * Red next-step when post-apply package install fails.
212
249
  * Host files may already be written; local bins may be missing.
213
250
  */
214
- export function formatStartPackageInstallFailure({ exitStatus, installCommand }) {
251
+ export function formatStartPackageInstallFailure({ exitStatus, installCommand, hostOutput }) {
215
252
  const doctor = arkPackageRecoveryCommand('arkgate-check', '--doctor');
216
- return [
253
+ const maturity = explainPnpmMaturityBlock(hostOutput);
254
+ const lines = [];
255
+ if (maturity) {
256
+ lines.push(maturity);
257
+ } else if (hostOutput && String(hostOutput).trim()) {
258
+ const tail = String(hostOutput)
259
+ .split('\n')
260
+ .map((line) => line.trimEnd())
261
+ .filter((line) => line.trim())
262
+ .slice(-6);
263
+ if (tail.length) lines.push(...tail);
264
+ }
265
+ lines.push(
217
266
  `Package install failed (exit ${exitStatus}). Setup files are written; the local command is not installed yet.`,
218
267
  ` ${installCommand}`,
219
268
  ` ${doctor}`,
220
- '`arkgate-check` is a command in the arkgate package — not its own npm package.',
221
- ].join('\n');
269
+ '`arkgate-check` is a command in the arkgate package — not its own npm package.'
270
+ );
271
+ return lines.join('\n');
222
272
  }
223
273
 
224
274
  /**
@@ -263,6 +313,10 @@ export function renderStartPreview(preview, options = {}) {
263
313
  if (preview.runtimeActivation) {
264
314
  console.log('Host: Codex is configured but not verified yet. Restart the host, then confirm this project.');
265
315
  }
316
+ const writeGateOrder = (preview.hostGuarantees || []).find((line) =>
317
+ String(line).startsWith('Write gate:')
318
+ );
319
+ if (writeGateOrder) console.log(writeGateOrder);
266
320
  if (preview.unresolvedDecisions.length > 0) {
267
321
  console.log('Unresolved decisions:');
268
322
  for (const decision of preview.unresolvedDecisions) console.log(` ${decision}`);
@@ -503,6 +557,7 @@ export async function planStart(args, helpers) {
503
557
  commands: commands(root, args, helpers),
504
558
  hostGuarantees: [
505
559
  args.requireWriteHook ? `Hard-write hook verified for ${args.requireWriteHook}` : 'shared CI merge gate will be installed',
560
+ writeGateOrderGuarantee(args.tools),
506
561
  'preview phase performs no writes in the target project',
507
562
  'apply writes the exact bytes identified by each afterHash',
508
563
  ...(codexSelected
@@ -141,8 +141,9 @@ export function detectWritePathCapabilities(root, explicitHost, attempt) {
141
141
  ? 'OpenCode local write is advisory (MCP + optional experimental plugin — not a hard boundary; ' +
142
142
  'not equivalent to Claude/Grok/Antigravity PreToolUse hard-write). ' +
143
143
  'The hard merge backstop is a required GitHub status context running --strict-merge.'
144
- : `Active host ${activeHost} has advisory prepare-write/autoPatch tools, ` +
145
- 'but no hard write boundary; CI can report failure, while merge blocking requires provider policy.';
144
+ : `Fell back to MCP prepare: active host ${activeHost} has no hard pre-hook. ` +
145
+ 'Advisory prepare-write/autoPatch is the local write surface. ' +
146
+ 'Next: install and trust the host write hook, or keep required CI as the merge line.';
146
147
  gap = {
147
148
  id: 'write-path-mcp-only',
148
149
  severity: 'info',
@@ -392,7 +392,7 @@ declare function createAdapterResult(input: {
392
392
  }): CurrentAdapterResult;
393
393
 
394
394
  /** ArkGate library version — single source of truth. */
395
- declare const version = "4.8.18";
395
+ declare const version = "4.8.19";
396
396
 
397
397
  /**
398
398
  * AI Code Gate (basic).