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.
- package/CHANGELOG.md +52 -0
- package/README.md +5 -6
- package/bin/ark-mcp-runtime.mjs +3 -3
- package/bin/ark-shared.mjs +187 -16
- package/bin/ark.mjs +18 -18
- package/bin/lib/ci-and-commands.mjs +2 -2
- package/bin/lib/doctor-human.mjs +2 -0
- package/bin/lib/doctor-next-actions.mjs +35 -0
- package/bin/lib/doctor-plan.mjs +6 -8
- package/bin/lib/gate-files.mjs +22 -1
- package/bin/lib/host-support-matrix.mjs +1 -1
- package/bin/lib/html-report-depth.mjs +1 -1
- package/bin/lib/install-migrate.mjs +3 -4
- package/bin/lib/managed-upgrade.mjs +30 -5
- package/bin/lib/mcp-adoption.mjs +4 -1
- package/bin/lib/presets.mjs +14 -14
- package/bin/lib/scan-files.mjs +3 -26
- package/bin/lib/skill-install.mjs +3 -2
- package/bin/lib/start-preview.mjs +60 -5
- package/bin/lib/write-path-detect.mjs +3 -2
- package/dist/{diagnosticCatalog-BNxKdcN4.d.ts → diagnosticCatalog-D2v0ASN2.d.ts} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/runtime/index.cjs +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/docs/README.md +1 -1
- package/docs/agent-guide.md +3 -1
- package/docs/ai-gates.md +7 -3
- package/docs/develop.md +1 -2
- package/docs/use.md +1 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-place/SKILL.md +2 -0
- package/templates/hooks/opencode-ark-write-gate.mjs +2 -2
- package/templates/skills/ark-place.md +2 -0
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -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,
|
package/bin/lib/gate-files.mjs
CHANGED
|
@@ -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
|
|
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 '
|
|
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
|
-
|
|
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
|
-
//
|
|
400
|
-
|
|
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
|
|
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
|
-
|
|
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 ??
|
package/bin/lib/mcp-adoption.mjs
CHANGED
|
@@ -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: [
|
|
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
|
{
|
package/bin/lib/presets.mjs
CHANGED
|
@@ -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
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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,
|
package/bin/lib/scan-files.mjs
CHANGED
|
@@ -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
|
|
490
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
: `
|
|
145
|
-
'
|
|
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.
|
|
395
|
+
declare const version = "4.8.19";
|
|
396
396
|
|
|
397
397
|
/**
|
|
398
398
|
* AI Code Gate (basic).
|