arkgate 4.8.14 → 4.8.16
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 +172 -0
- package/README.md +17 -7
- package/bin/ark-check-runtime.mjs +17 -49
- package/bin/ark-mcp-runtime.mjs +111 -2
- package/bin/ark-shared.mjs +142 -11
- package/bin/ark.mjs +24 -33
- package/bin/lib/adr-presence.mjs +97 -0
- package/bin/lib/agent-skills-package.mjs +179 -1
- package/bin/lib/analysis-completeness.mjs +38 -2
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/architecture-scan.mjs +43 -4
- package/bin/lib/ark-run-doctor.mjs +6 -0
- package/bin/lib/ark-run-report.mjs +6 -2
- package/bin/lib/arkrules-sensors.mjs +81 -0
- package/bin/lib/baseline-key.mjs +6 -1
- package/bin/lib/check-args.mjs +52 -2
- package/bin/lib/config-contract.mjs +65 -2
- package/bin/lib/diagnostic-catalog.mjs +5 -1
- package/bin/lib/doctor-advisories.mjs +120 -10
- package/bin/lib/doctor-green-cite.mjs +139 -0
- package/bin/lib/doctor-human.mjs +115 -67
- package/bin/lib/doctor-next-actions.mjs +23 -0
- package/bin/lib/doctor-plan.mjs +35 -2
- package/bin/lib/enforcement-honesty.mjs +47 -2
- package/bin/lib/field-install.mjs +47 -7
- package/bin/lib/first-run-help.mjs +9 -1
- package/bin/lib/host-support-matrix.mjs +9 -1
- package/bin/lib/html-report-depth.mjs +2 -0
- package/bin/lib/html-report.mjs +13 -2
- package/bin/lib/import-resolve.mjs +74 -13
- package/bin/lib/improvement-compass-doctor.mjs +8 -1
- package/bin/lib/improvement-compass-map.mjs +5 -1
- package/bin/lib/invariant-coverage.mjs +121 -0
- package/bin/lib/invariant-tests-path.mjs +212 -0
- package/bin/lib/layer-description.mjs +90 -8
- package/bin/lib/mcp-hook-payload.mjs +56 -0
- package/bin/lib/no-domain-frontend.mjs +91 -0
- package/bin/lib/package-manager.mjs +8 -0
- package/bin/lib/presets.mjs +3 -4
- package/bin/lib/product-copy.mjs +15 -0
- package/bin/lib/projected-governed-coverage.mjs +114 -0
- package/bin/lib/prototype-shortcuts.mjs +224 -0
- package/bin/lib/recommend-cli.mjs +54 -0
- package/bin/lib/remediation.mjs +15 -0
- package/bin/lib/resolved-candidate-facts.mjs +83 -66
- package/bin/lib/rules-under-contract.mjs +117 -2
- package/bin/lib/snippet-analysis.mjs +14 -8
- package/bin/lib/start-preview.mjs +24 -2
- package/bin/lib/states-transitions-presence.mjs +212 -0
- package/bin/lib/status-command.mjs +2 -0
- package/bin/lib/status-transition-catalog.mjs +410 -0
- package/bin/lib/team-parliament-io.mjs +10 -0
- package/bin/lib/violations.mjs +13 -1
- package/bin/lib/write-path-capabilities.mjs +20 -5
- package/bin/lib/write-path-detect.mjs +27 -2
- package/dist/{configTypes-j7so8B4O.d.ts → configTypes-VD0qcubY.d.ts} +21 -1
- package/dist/{diagnosticCatalog-DVx_2RmF.d.ts → diagnosticCatalog-KWvGLI1U.d.ts} +24 -3
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +34 -34
- package/dist/index.d.ts +71 -10
- package/dist/index.js +34 -34
- package/dist/nestjs/index.cjs +5 -5
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +5 -5
- package/dist/runtime/index.cjs +15 -15
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +15 -15
- package/dist/{types-tGhZUiGX.d.ts → types-BSzRy2X1.d.ts} +1 -1
- package/dist/{types-Djbs3KjE.d.ts → types-D5GT5ZT8.d.ts} +3 -1
- package/docs/README.md +1 -1
- package/docs/agent-guide.md +8 -4
- package/docs/ai-gates.md +10 -3
- package/docs/brownfield-adoption.md +4 -1
- package/docs/configuration.md +66 -10
- package/docs/develop.md +4 -1
- package/docs/diagnostics.md +48 -4
- package/docs/enthusiast/how-to-agent-gates.md +5 -0
- package/docs/enthusiast/how-to-pick-shape.md +1 -1
- package/docs/package-surface.md +7 -5
- package/docs/use.md +10 -3
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +24 -3
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +97 -1
- package/templates/agent-skills/ark-autopilot/SKILL.md +12 -3
- package/templates/agent-skills/ark-coverage/SKILL.md +2 -2
- package/templates/agent-skills/ark-explain/SKILL.md +6 -3
- package/templates/agent-skills/ark-explore/SKILL.md +33 -1
- package/templates/agent-skills/ark-place/SKILL.md +42 -3
- package/templates/agent-skills/ark-runtime/SKILL.md +4 -2
- package/templates/agent-skills/ark-upgrade/SKILL.md +1 -1
- package/templates/arkrules/DomainModel.json +14 -1
- package/templates/skills/ark-adopt.md +97 -1
- package/templates/skills/ark-autopilot.md +12 -3
- package/templates/skills/ark-coverage.md +2 -2
- package/templates/skills/ark-explain.md +6 -3
- package/templates/skills/ark-explore.md +33 -1
- package/templates/skills/ark-place.md +42 -3
- package/templates/skills/ark-runtime.md +4 -2
- package/templates/skills/ark-upgrade.md +1 -1
package/bin/ark-shared.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
normalizeArkgateInstallSpec,
|
|
23
23
|
packageInstallArgv,
|
|
24
24
|
installDevHint,
|
|
25
|
+
arkPackageRecoveryCommand,
|
|
25
26
|
} from './lib/package-manager.mjs';
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -104,10 +105,85 @@ function mergeLayerPatterns(config, layerName, patterns, extras = {}) {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/** Next-flavored start captions — applied only when Next is actually detected. */
|
|
109
|
+
export const NEXT_LAYER_DESCRIPTIONS = Object.freeze({
|
|
110
|
+
ApplicationOrchestration:
|
|
111
|
+
'Use cases and services that coordinate the domain through ports. Next App Router API (`app/api/**`) and Pages API (`pages/api/**`) are orchestration shells, not UI.',
|
|
112
|
+
PresentationAdapters:
|
|
113
|
+
'Entrypoints — UI, framework app/pages dirs, controllers. Next `app/api` is Application, not this layer. Never bare lib/** (data clients are Persistence).',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/** Library / package-monorepo captions — no framework storefront. */
|
|
117
|
+
export const LIBRARY_LAYER_DESCRIPTIONS = Object.freeze({
|
|
118
|
+
ApplicationOrchestration:
|
|
119
|
+
'Use cases and the package public surface that coordinate the domain. No I/O of its own.',
|
|
120
|
+
PresentationAdapters:
|
|
121
|
+
'Optional CLIs, docs, or demo UIs — not the published library core.',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
function setLayerDescription(config, layerName, description) {
|
|
125
|
+
const layer = config.layers?.find((entry) => entry.name === layerName);
|
|
126
|
+
if (!layer || typeof description !== 'string' || description.length === 0) return;
|
|
127
|
+
layer.description = description;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** packages/* (or similar) of published libraries — no app/cli units, no web framework. */
|
|
131
|
+
export function isLibraryPackageMonorepo(signals) {
|
|
132
|
+
if (!signals || signals.nextFramework || signals.nestFramework || signals.expressLike) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const units = Array.isArray(signals.repoUnits) ? signals.repoUnits : [];
|
|
136
|
+
const productive = units.filter(
|
|
137
|
+
(unit) => unit.root !== '.' && !['docs', 'example', 'test'].includes(unit.role)
|
|
138
|
+
);
|
|
139
|
+
if (productive.length === 0) return false;
|
|
140
|
+
return (
|
|
141
|
+
productive.some((unit) => unit.role === 'library') &&
|
|
142
|
+
!productive.some((unit) => unit.role === 'application' || unit.role === 'cli')
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function applyFrameworkLayerCaptions(config, signals) {
|
|
147
|
+
const nextish = Boolean(
|
|
148
|
+
signals.nextFramework || (signals.ui && signals.toolHints?.includes('next'))
|
|
149
|
+
);
|
|
150
|
+
if (nextish) {
|
|
151
|
+
setLayerDescription(
|
|
152
|
+
config,
|
|
153
|
+
'ApplicationOrchestration',
|
|
154
|
+
NEXT_LAYER_DESCRIPTIONS.ApplicationOrchestration
|
|
155
|
+
);
|
|
156
|
+
setLayerDescription(
|
|
157
|
+
config,
|
|
158
|
+
'PresentationAdapters',
|
|
159
|
+
NEXT_LAYER_DESCRIPTIONS.PresentationAdapters
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (
|
|
164
|
+
(signals.libraryOnly && !signals.nestFramework && !signals.expressLike) ||
|
|
165
|
+
isLibraryPackageMonorepo(signals)
|
|
166
|
+
) {
|
|
167
|
+
setLayerDescription(
|
|
168
|
+
config,
|
|
169
|
+
'ApplicationOrchestration',
|
|
170
|
+
LIBRARY_LAYER_DESCRIPTIONS.ApplicationOrchestration
|
|
171
|
+
);
|
|
172
|
+
setLayerDescription(
|
|
173
|
+
config,
|
|
174
|
+
'PresentationAdapters',
|
|
175
|
+
LIBRARY_LAYER_DESCRIPTIONS.PresentationAdapters
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
107
180
|
/**
|
|
108
181
|
* Framework-aware layout overlays. Detection uses collectRepoShapeSignals (deps + filenames).
|
|
109
|
-
* Pure additive: never removes existing preset
|
|
110
|
-
* reach meaningful governed% under hexagonal/layered without a hand-written adopt pass.
|
|
182
|
+
* Pure additive for patterns: never removes existing preset globs. Goal: Nest/Next/express
|
|
183
|
+
* starters reach meaningful governed% under hexagonal/layered without a hand-written adopt pass.
|
|
184
|
+
* Layer captions are the one replace: Next-flavored copy only when Next is present;
|
|
185
|
+
* library-native copy on library-only / library-package monorepos. Generic preset
|
|
186
|
+
* captions stay when neither applies.
|
|
111
187
|
*/
|
|
112
188
|
export function applyFrameworkLayoutOverlays(config, root) {
|
|
113
189
|
if (!config || !root) return config;
|
|
@@ -427,6 +503,7 @@ export function applyFrameworkLayoutOverlays(config, root) {
|
|
|
427
503
|
: 'library';
|
|
428
504
|
}
|
|
429
505
|
|
|
506
|
+
applyFrameworkLayerCaptions(next, signals);
|
|
430
507
|
return withArkConfigMetadata(next);
|
|
431
508
|
}
|
|
432
509
|
|
|
@@ -598,6 +675,7 @@ export {
|
|
|
598
675
|
normalizeArkgateInstallSpec,
|
|
599
676
|
packageInstallArgv,
|
|
600
677
|
installDevHint,
|
|
678
|
+
arkPackageRecoveryCommand,
|
|
601
679
|
};
|
|
602
680
|
|
|
603
681
|
// FX01–FX02: registry-aware skip lives in upgrade-package-decision (injectable probe).
|
|
@@ -1082,7 +1160,9 @@ export function collectRepoShapeSignals(root) {
|
|
|
1082
1160
|
return !excludedUnitRoots.some((prefix) => rel.startsWith(prefix));
|
|
1083
1161
|
});
|
|
1084
1162
|
const discoveredFileSet = new Set(sourceFiles.map((file) => path.resolve(file)));
|
|
1085
|
-
|
|
1163
|
+
// Files under detected source roots — NOT layer-governed coverage. User-facing
|
|
1164
|
+
// "projected governed coverage" is computeCoverage (see projected-governed-coverage.mjs).
|
|
1165
|
+
const discoveredSourceCoverage = productionFiles.length === 0
|
|
1086
1166
|
? 0
|
|
1087
1167
|
: Math.round((productionFiles.filter((file) => discoveredFileSet.has(path.resolve(file))).length / productionFiles.length) * 100);
|
|
1088
1168
|
const tinyTree = sourceFileCount < 3;
|
|
@@ -1261,7 +1341,7 @@ export function collectRepoShapeSignals(root) {
|
|
|
1261
1341
|
libraryOnly,
|
|
1262
1342
|
tinyTree,
|
|
1263
1343
|
sourceFileCount,
|
|
1264
|
-
|
|
1344
|
+
discoveredSourceCoverage,
|
|
1265
1345
|
domain,
|
|
1266
1346
|
application,
|
|
1267
1347
|
domainHeavy,
|
|
@@ -1529,7 +1609,6 @@ export function scoreArchetypes(signals, playbook) {
|
|
|
1529
1609
|
confidence,
|
|
1530
1610
|
requiresConfirmation: true,
|
|
1531
1611
|
confirmationReasons: [
|
|
1532
|
-
`projected governed coverage is ${signals.projectedGovernedCoverage ?? 0}% (below 90%)`,
|
|
1533
1612
|
'no archetype received a positive score',
|
|
1534
1613
|
],
|
|
1535
1614
|
phases: fallback.phases,
|
|
@@ -1554,9 +1633,7 @@ export function scoreArchetypes(signals, playbook) {
|
|
|
1554
1633
|
confidence = Math.min(confidence, 0.28);
|
|
1555
1634
|
}
|
|
1556
1635
|
const closeRecommendations = Boolean(second && top.score - second.score <= 2);
|
|
1557
|
-
const lowProjectedCoverage = (signals.projectedGovernedCoverage ?? 0) < 90;
|
|
1558
1636
|
if (closeRecommendations) confidence = Math.min(confidence, 0.49);
|
|
1559
|
-
if (lowProjectedCoverage) confidence = Math.min(confidence, 0.49);
|
|
1560
1637
|
|
|
1561
1638
|
return {
|
|
1562
1639
|
ranked: scored,
|
|
@@ -1571,10 +1648,9 @@ export function scoreArchetypes(signals, playbook) {
|
|
|
1571
1648
|
'TypeScript/JS surface is thin or missing — treat the archetype as a weak hint. Prefer ark-check --suggest-include / --adopt-contract on the real package roots before scaffolding.',
|
|
1572
1649
|
}
|
|
1573
1650
|
: {}),
|
|
1574
|
-
requiresConfirmation: closeRecommendations || thinTs
|
|
1651
|
+
requiresConfirmation: closeRecommendations || thinTs,
|
|
1575
1652
|
confirmationReasons: [
|
|
1576
1653
|
...(closeRecommendations ? ['top recommendations are within 2 score points'] : []),
|
|
1577
|
-
...(lowProjectedCoverage ? [`projected governed coverage is ${signals.projectedGovernedCoverage}% (below 90%)`] : []),
|
|
1578
1654
|
...(thinTs ? ['the discovered source surface is thin'] : []),
|
|
1579
1655
|
],
|
|
1580
1656
|
phases: top.phases,
|
|
@@ -1631,7 +1707,7 @@ export function buildArchitectureRecommendation(root, options = {}) {
|
|
|
1631
1707
|
policyPack: policyPackId,
|
|
1632
1708
|
signals: {
|
|
1633
1709
|
sourceFileCount: signals.sourceFileCount,
|
|
1634
|
-
|
|
1710
|
+
discoveredSourceCoverage: signals.discoveredSourceCoverage,
|
|
1635
1711
|
discoveredRoots: signals.discoveredRoots,
|
|
1636
1712
|
packageUnits: signals.repoUnits.map((unit) => ({
|
|
1637
1713
|
root: unit.root,
|
|
@@ -1664,7 +1740,7 @@ export function buildArchitectureRecommendation(root, options = {}) {
|
|
|
1664
1740
|
// a thin slice and can mis-flag framework internals, so steer these to the adoption flow.
|
|
1665
1741
|
mature: signals.sourceFileCount >= MATURE_REPO_FILE_THRESHOLD,
|
|
1666
1742
|
initCommand: `${arkCommand(root, 'ark', `init --archetype ${result.archetype} --yes`)}`,
|
|
1667
|
-
firstCommand: `${arkCommand(root, 'ark', `
|
|
1743
|
+
firstCommand: `${arkCommand(root, 'ark', `start --apply --archetype ${result.archetype}`)}`,
|
|
1668
1744
|
adoptCommand: arkCommand(root, 'ark-check', '--recommend --write-plan'),
|
|
1669
1745
|
recommendCommand: arkCommand(root, 'ark-check', '--recommend'),
|
|
1670
1746
|
checkCommand: arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config'),
|
|
@@ -1705,6 +1781,49 @@ export function resolveArchetypePreset(archetypeId, playbookPath = defaultPlaybo
|
|
|
1705
1781
|
};
|
|
1706
1782
|
}
|
|
1707
1783
|
|
|
1784
|
+
/**
|
|
1785
|
+
* Preset `ark start --internal-apply` / `--init` would write. Shared so recommend
|
|
1786
|
+
* can project the same governed % start and doctor measure after that write.
|
|
1787
|
+
*/
|
|
1788
|
+
export function resolveStartInitPreset(root, rec = {}, archetype = rec.archetype) {
|
|
1789
|
+
const presetFromArchetype =
|
|
1790
|
+
archetype && isValidArchetypeId(archetype)
|
|
1791
|
+
? resolveArchetypePreset(archetype).preset
|
|
1792
|
+
: undefined;
|
|
1793
|
+
const preset = rec.preset || presetFromArchetype;
|
|
1794
|
+
const includeRoots = resolveIncludeRoots(root);
|
|
1795
|
+
const tsPackages = detectTsPackageRoots(root);
|
|
1796
|
+
const nestedTsPackages = tsPackages.filter((entry) => entry !== '.');
|
|
1797
|
+
const workspaces = detectWorkspaces(root);
|
|
1798
|
+
const looksLikeMonorepo =
|
|
1799
|
+
includeRoots.length > 0 ||
|
|
1800
|
+
nestedTsPackages.length > 0 ||
|
|
1801
|
+
workspaces.length > 0 ||
|
|
1802
|
+
fs.existsSync(path.join(root, 'rush.json')) ||
|
|
1803
|
+
fs.existsSync(path.join(root, 'pnpm-workspace.yaml')) ||
|
|
1804
|
+
fs.existsSync(path.join(root, 'lerna.json')) ||
|
|
1805
|
+
fs.existsSync(path.join(root, 'apps')) ||
|
|
1806
|
+
fs.existsSync(path.join(root, 'packages'));
|
|
1807
|
+
if (rec.preset === 'vite-vercel-spa' || preset === 'vite-vercel-spa') {
|
|
1808
|
+
return 'vite-vercel-spa';
|
|
1809
|
+
}
|
|
1810
|
+
if (looksLikeMonorepo && (rec.mature || includeRoots.length > 0 || tsPackages.length > 0)) {
|
|
1811
|
+
const useUi =
|
|
1812
|
+
rec.preset === 'feature-sliced' ||
|
|
1813
|
+
rec.archetype === 'frontend-surface' ||
|
|
1814
|
+
(nestedTsPackages.length > 0 && includeRoots.length === 0 && !rec.mature);
|
|
1815
|
+
return useUi && nestedTsPackages.length <= 3 ? 'ui-surface' : 'monorepo';
|
|
1816
|
+
}
|
|
1817
|
+
return preset ?? null;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
/** One-minute / preview footer when the coverage·confidence gate may refuse apply. */
|
|
1821
|
+
export const START_APPLY_REFUSE_FOOTER = [
|
|
1822
|
+
'If apply refuses (coverage below 50% or weak shape), that lock is deliberate.',
|
|
1823
|
+
'Lock the shape: --archetype <id> · --preset <name> · --force',
|
|
1824
|
+
'Inspect ranked shapes: arkgate-check --recommend',
|
|
1825
|
+
].join('\n');
|
|
1826
|
+
|
|
1708
1827
|
export function mapWizardChoiceToArchetype(choiceKey) {
|
|
1709
1828
|
const entry = INIT_WIZARD_CHOICES.find((c) => c.key === String(choiceKey).trim());
|
|
1710
1829
|
if (!entry) return null;
|
|
@@ -1753,6 +1872,18 @@ export function formatArchitectureRecommendationHuman(recommendation) {
|
|
|
1753
1872
|
lines.push('');
|
|
1754
1873
|
lines.push(`Archetype: ${recommendation.archetype} — ${recommendation.label}`);
|
|
1755
1874
|
lines.push(`Preset: ${recommendation.preset} (confidence ${recommendation.confidence})`);
|
|
1875
|
+
const projected =
|
|
1876
|
+
recommendation.projectedCoverage?.percent ??
|
|
1877
|
+
recommendation.signals?.projectedGovernedCoverage;
|
|
1878
|
+
if (typeof projected === 'number') {
|
|
1879
|
+
const classified = recommendation.projectedCoverage?.classifiedFiles;
|
|
1880
|
+
const total = recommendation.projectedCoverage?.totalFiles;
|
|
1881
|
+
const counts =
|
|
1882
|
+
typeof classified === 'number' && typeof total === 'number'
|
|
1883
|
+
? ` (${classified}/${total} files)`
|
|
1884
|
+
: '';
|
|
1885
|
+
lines.push(`Projected governed coverage: ${projected}%${counts}`);
|
|
1886
|
+
}
|
|
1756
1887
|
if (recommendation.requiresConfirmation) {
|
|
1757
1888
|
lines.push('⚠ Confirmation required before applying this recommendation.');
|
|
1758
1889
|
for (const reason of recommendation.confirmationReasons ?? []) lines.push(` - ${reason}`);
|
package/bin/ark.mjs
CHANGED
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
arkCommand,
|
|
9
9
|
buildArchitectureRecommendation,
|
|
10
10
|
detectPackageManager,
|
|
11
|
-
detectWorkspaces,
|
|
12
11
|
evaluateStartShapeConfidenceGate,
|
|
13
12
|
resolveIncludeRoots,
|
|
14
13
|
detectTsPackageRoots,
|
|
14
|
+
resolveStartInitPreset,
|
|
15
15
|
INIT_WIZARD_CHOICES,
|
|
16
16
|
isValidArchetypeId,
|
|
17
17
|
mapWizardChoiceToArchetype,
|
|
@@ -22,7 +22,12 @@ import {
|
|
|
22
22
|
} from './ark-shared.mjs';
|
|
23
23
|
import { pinArkgateDevDependency, FALSE_GREEN_GAP_ID } from './lib/field-install.mjs';
|
|
24
24
|
import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
applyStartPreview,
|
|
27
|
+
formatStartPackageInstallFailure,
|
|
28
|
+
planStart,
|
|
29
|
+
renderStartPreview,
|
|
30
|
+
} from './lib/start-preview.mjs';
|
|
26
31
|
import { runUpgradeCommand } from './lib/upgrade-command.mjs';
|
|
27
32
|
import { detectActiveAgentHost } from './lib/skill-install.mjs';
|
|
28
33
|
import { loadArkConfigContract } from './lib/config-contract.mjs';
|
|
@@ -489,7 +494,8 @@ async function start(args) {
|
|
|
489
494
|
const skip = shouldSkipArkgateInstall(args.root, cliVersion());
|
|
490
495
|
if (!skip.skip) {
|
|
491
496
|
const [command, commandArgs] = packageInstallArgv(args.root, `^${cliVersion()}`);
|
|
492
|
-
|
|
497
|
+
const installCommand = `${command} ${commandArgs.join(' ')}`;
|
|
498
|
+
if (!args.json) console.log(`Installing package: ${installCommand}`);
|
|
493
499
|
// Keep stdout clean for --json consumers (package managers are chatty on stdout).
|
|
494
500
|
const status = args.json
|
|
495
501
|
? (spawnSync(command, commandArgs, {
|
|
@@ -498,10 +504,9 @@ async function start(args) {
|
|
|
498
504
|
encoding: 'utf8',
|
|
499
505
|
}).status ?? 1)
|
|
500
506
|
: runCommand(command, commandArgs, args.root);
|
|
501
|
-
if (status !== 0
|
|
502
|
-
console.
|
|
503
|
-
|
|
504
|
-
);
|
|
507
|
+
if (status !== 0) {
|
|
508
|
+
console.error(formatStartPackageInstallFailure({ exitStatus: status, installCommand }));
|
|
509
|
+
return status;
|
|
505
510
|
}
|
|
506
511
|
}
|
|
507
512
|
}
|
|
@@ -568,8 +573,12 @@ async function start(args) {
|
|
|
568
573
|
if (pinned.changed) {
|
|
569
574
|
console.log(` Pinned arkgate@${pinned.version} in package.json devDependencies.`);
|
|
570
575
|
if (installStatus !== null && installStatus !== 0) {
|
|
571
|
-
|
|
572
|
-
|
|
576
|
+
const [command, commandArgs] = packageInstallArgv(root, pinned.version);
|
|
577
|
+
console.error(
|
|
578
|
+
formatStartPackageInstallFailure({
|
|
579
|
+
exitStatus: installStatus,
|
|
580
|
+
installCommand: `${command} ${commandArgs.join(' ')}`,
|
|
581
|
+
})
|
|
573
582
|
);
|
|
574
583
|
}
|
|
575
584
|
} else if (pinned.reason === 'already-present') {
|
|
@@ -586,41 +595,23 @@ async function start(args) {
|
|
|
586
595
|
const configPath = path.join(root, 'ark.config.json');
|
|
587
596
|
if (!fs.existsSync(configPath)) {
|
|
588
597
|
const initArgs = ['--root', root, '--init'];
|
|
589
|
-
const
|
|
598
|
+
const startPreset = resolveStartInitPreset(root, rec ?? {}, archetype);
|
|
590
599
|
const includeRoots = resolveIncludeRoots(root);
|
|
591
600
|
const tsPackages = detectTsPackageRoots(root);
|
|
592
601
|
const nestedTsPackages = tsPackages.filter((entry) => entry !== '.');
|
|
593
|
-
|
|
594
|
-
const looksLikeMonorepo =
|
|
595
|
-
includeRoots.length > 0 ||
|
|
596
|
-
nestedTsPackages.length > 0 ||
|
|
597
|
-
workspaces.length > 0 ||
|
|
598
|
-
fs.existsSync(path.join(root, 'rush.json')) ||
|
|
599
|
-
fs.existsSync(path.join(root, 'pnpm-workspace.yaml')) ||
|
|
600
|
-
fs.existsSync(path.join(root, 'lerna.json')) ||
|
|
601
|
-
fs.existsSync(path.join(root, 'apps')) ||
|
|
602
|
-
fs.existsSync(path.join(root, 'packages'));
|
|
603
|
-
// SPA (Vite + root api/lib) wins over monorepo heuristics (NEW-SPA-DEFAULT-LAYOUT).
|
|
604
|
-
if (rec?.preset === 'vite-vercel-spa' || preset === 'vite-vercel-spa') {
|
|
602
|
+
if (startPreset === 'vite-vercel-spa') {
|
|
605
603
|
initArgs.push('--preset', 'vite-vercel-spa');
|
|
606
604
|
console.log(' Vite/Vercel SPA layout detected — include src,api,lib; api→Application; db clients→Persistence.');
|
|
607
|
-
} else if (
|
|
608
|
-
|
|
609
|
-
// UI-heavy TS packages (Remotion/Vite) prefer ui-surface patterns when recommend says so.
|
|
610
|
-
const useUi =
|
|
611
|
-
rec?.preset === 'feature-sliced' ||
|
|
612
|
-
rec?.archetype === 'frontend-surface' ||
|
|
613
|
-
(nestedTsPackages.length > 0 && includeRoots.length === 0 && !rec?.mature);
|
|
614
|
-
initArgs.push('--preset', useUi && nestedTsPackages.length <= 3 ? 'ui-surface' : 'monorepo');
|
|
605
|
+
} else if (startPreset === 'monorepo' || startPreset === 'ui-surface') {
|
|
606
|
+
initArgs.push('--preset', startPreset);
|
|
615
607
|
const shown = includeRoots.length > 0 ? includeRoots : nestedTsPackages;
|
|
616
608
|
console.log(
|
|
617
609
|
shown.length > 0
|
|
618
610
|
? ` Multi-package / TS package layout detected — profile include: ${shown.join(', ')}.`
|
|
619
611
|
: ' Multi-package layout detected — using monorepo profile.'
|
|
620
612
|
);
|
|
621
|
-
} else if (
|
|
622
|
-
|
|
623
|
-
initArgs.push('--preset', preset);
|
|
613
|
+
} else if (startPreset) {
|
|
614
|
+
initArgs.push('--preset', startPreset);
|
|
624
615
|
}
|
|
625
616
|
const status = runArkCheck(initArgs, { cwd: root });
|
|
626
617
|
if (status !== 0) return status;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soft ADR / decision-note presence when gates are demanded.
|
|
3
|
+
* Tooling I/O. Never a gate fail. Absence is silent unless require-gates
|
|
4
|
+
* or adopted-strict (required-merge) is on.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
/** Conventional homes a human already uses. First hit wins. No config key. */
|
|
11
|
+
export const ADR_PRESENCE_HOMES = Object.freeze([
|
|
12
|
+
'docs/adr',
|
|
13
|
+
'docs/decisions',
|
|
14
|
+
'docs/adr.md',
|
|
15
|
+
'docs/decision-log.md',
|
|
16
|
+
'ADR.md',
|
|
17
|
+
'DECISIONS.md',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export const ADR_PRESENCE_ASK =
|
|
21
|
+
'Gates are required here, but there is no short decision note yet.';
|
|
22
|
+
|
|
23
|
+
export const ADR_PRESENCE_NEXT =
|
|
24
|
+
'Add a short note under docs/adr/ (or docs/decisions/) when you loosen a rule or add a real gate. Not every change.';
|
|
25
|
+
|
|
26
|
+
function isNonEmptyMarkdownFile(file) {
|
|
27
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return false;
|
|
28
|
+
try {
|
|
29
|
+
return fs.readFileSync(file, 'utf8').trim().length > 0;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function directoryHasMarkdown(dir) {
|
|
36
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return false;
|
|
37
|
+
try {
|
|
38
|
+
return fs.readdirSync(dir).some((name) => {
|
|
39
|
+
if (!name.toLowerCase().endsWith('.md')) return false;
|
|
40
|
+
return isNonEmptyMarkdownFile(path.join(dir, name));
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* First conventional decision-note home that already has a note.
|
|
49
|
+
* Empty folders and empty files do not count.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} root
|
|
52
|
+
* @returns {string | null}
|
|
53
|
+
*/
|
|
54
|
+
export function findAdrPresenceHome(root) {
|
|
55
|
+
if (typeof root !== 'string' || root.length === 0) return null;
|
|
56
|
+
for (const rel of ADR_PRESENCE_HOMES) {
|
|
57
|
+
const abs = path.join(root, rel);
|
|
58
|
+
if (rel.endsWith('.md')) {
|
|
59
|
+
if (isNonEmptyMarkdownFile(abs)) return rel;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (directoryHasMarkdown(abs)) return rel;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Soft residual when gates are demanded and no decision-note home exists.
|
|
69
|
+
* `demanded` is --require-gates / --strict / --strict-merge, or doctor
|
|
70
|
+
* adopted === 'required-merge'. Off → null (silent).
|
|
71
|
+
*
|
|
72
|
+
* @param {{ root?: string, demanded?: boolean }} [input]
|
|
73
|
+
* @returns {{ missing: true, ask: string, nextAction: string } | null}
|
|
74
|
+
*/
|
|
75
|
+
export function collectAdrPresenceResidual(input = {}) {
|
|
76
|
+
if (input.demanded !== true) return null;
|
|
77
|
+
const home = findAdrPresenceHome(input.root ?? '');
|
|
78
|
+
if (home) return null;
|
|
79
|
+
return {
|
|
80
|
+
missing: true,
|
|
81
|
+
ask: ADR_PRESENCE_ASK,
|
|
82
|
+
nextAction: ADR_PRESENCE_NEXT,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Human hint for --require-gates. Never changes exit code.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} root
|
|
90
|
+
* @param {(line: string) => void} write
|
|
91
|
+
*/
|
|
92
|
+
export function printAdrPresenceHint(root, write) {
|
|
93
|
+
const residual = collectAdrPresenceResidual({ root, demanded: true });
|
|
94
|
+
if (!residual || typeof write !== 'function') return;
|
|
95
|
+
write(residual.ask);
|
|
96
|
+
write(`Next: ${residual.nextAction}`);
|
|
97
|
+
}
|
|
@@ -51,7 +51,9 @@ export const ARK_SKILL_STUB_REDIRECTS = Object.freeze({
|
|
|
51
51
|
});
|
|
52
52
|
/**
|
|
53
53
|
* Product surface → first-class doors that exercise it.
|
|
54
|
-
* Standing check: every surface has at least one first-class door
|
|
54
|
+
* Standing check: every surface has at least one first-class door whose
|
|
55
|
+
* body mentions that surface. Table keys must match
|
|
56
|
+
* {@link ARK_SKILL_REQUIRED_SURFACES}.
|
|
55
57
|
*/
|
|
56
58
|
export const ARK_SKILL_CAPACITY = Object.freeze({
|
|
57
59
|
Layers: ['ark-adopt', 'ark-place', 'ark-explore', 'ark-autopilot', 'ark-coverage', 'ark-explain'],
|
|
@@ -62,6 +64,27 @@ export const ARK_SKILL_CAPACITY = Object.freeze({
|
|
|
62
64
|
Guiar: ['ark-explore', 'ark-autopilot', 'ark-explain', 'ark-coverage', 'ark-runtime'],
|
|
63
65
|
Ordenar: ['ark-order'],
|
|
64
66
|
});
|
|
67
|
+
/**
|
|
68
|
+
* Closed product + north-star surfaces the skill *set* must exercise.
|
|
69
|
+
* Independent of {@link ARK_SKILL_CAPACITY} so deleting a table key fails closed.
|
|
70
|
+
*/
|
|
71
|
+
export const ARK_SKILL_REQUIRED_SURFACES = Object.freeze([
|
|
72
|
+
'Layers',
|
|
73
|
+
'ArkRules',
|
|
74
|
+
'ArkRun',
|
|
75
|
+
'ArkOrder',
|
|
76
|
+
'Contener',
|
|
77
|
+
'Guiar',
|
|
78
|
+
'Ordenar',
|
|
79
|
+
]);
|
|
80
|
+
/** Named first-class door that must stay on these surfaces (no leftover substitute). */
|
|
81
|
+
export const ARK_SKILL_CAPACITY_DEDICATED_DOORS = Object.freeze({
|
|
82
|
+
ArkRun: 'ark-runtime',
|
|
83
|
+
ArkOrder: 'ark-order',
|
|
84
|
+
Ordenar: 'ark-order',
|
|
85
|
+
});
|
|
86
|
+
const NORTH_STAR_PHRASE = 'Contener · Guiar · Ordenar';
|
|
87
|
+
const FORBIDDEN_ORDER_FREEZE = 'Do not invent `/ark-order`';
|
|
65
88
|
/**
|
|
66
89
|
* Closed shipped catalog (first-class + one-release stubs).
|
|
67
90
|
* Sorted alphabetically for deterministic inventory diffs.
|
|
@@ -289,6 +312,161 @@ export function validateAgentSkillsPackage(entries) {
|
|
|
289
312
|
presentCount: names.length,
|
|
290
313
|
};
|
|
291
314
|
}
|
|
315
|
+
function pushCapacityIssue(issues, issue) {
|
|
316
|
+
issues.push(issue);
|
|
317
|
+
}
|
|
318
|
+
function skillMentionsSurface(body, surface) {
|
|
319
|
+
return body.includes(surface);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Observe skill bodies (and optional hubs) against the closed product surfaces.
|
|
323
|
+
* Inventory/layout stay on {@link validateAgentSkillsPackage}. This is the
|
|
324
|
+
* fail-closed tooth so a plane cannot silently drop out of the skill set.
|
|
325
|
+
*/
|
|
326
|
+
export function validateSkillProductCapacity(input) {
|
|
327
|
+
const issues = [];
|
|
328
|
+
const skills = input.skills ?? {};
|
|
329
|
+
const tableKeys = Object.keys(ARK_SKILL_CAPACITY);
|
|
330
|
+
for (const surface of ARK_SKILL_REQUIRED_SURFACES) {
|
|
331
|
+
if (!tableKeys.includes(surface)) {
|
|
332
|
+
pushCapacityIssue(issues, {
|
|
333
|
+
code: 'CAPACITY_SURFACE_MISSING',
|
|
334
|
+
surface,
|
|
335
|
+
message: `The capacity table dropped ${surface}. Skills must keep covering ` +
|
|
336
|
+
`Layers, ArkRules, ArkRun, ArkOrder plus Contener · Guiar · Ordenar. ` +
|
|
337
|
+
`Put ${surface} back on ARK_SKILL_CAPACITY.`,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
for (const key of tableKeys) {
|
|
342
|
+
if (!ARK_SKILL_REQUIRED_SURFACES.includes(key)) {
|
|
343
|
+
pushCapacityIssue(issues, {
|
|
344
|
+
code: 'CAPACITY_SURFACE_UNMAPPED',
|
|
345
|
+
surface: key,
|
|
346
|
+
message: `The capacity table lists ${key}, which is not a product surface. ` +
|
|
347
|
+
`Keep the table to Layers, ArkRules, ArkRun, ArkOrder and Contener · Guiar · Ordenar.`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
for (const surface of ARK_SKILL_REQUIRED_SURFACES) {
|
|
352
|
+
const doors = ARK_SKILL_CAPACITY[surface];
|
|
353
|
+
if (!doors || doors.length === 0)
|
|
354
|
+
continue;
|
|
355
|
+
for (const door of doors) {
|
|
356
|
+
if (!isFirstClassArkSkillName(door)) {
|
|
357
|
+
pushCapacityIssue(issues, {
|
|
358
|
+
code: 'CAPACITY_DOOR_NOT_FIRST_CLASS',
|
|
359
|
+
surface,
|
|
360
|
+
skillName: door,
|
|
361
|
+
message: `/${door} is listed for ${surface} but is not a first-class door. ` +
|
|
362
|
+
`Leftover names are shortcuts. Point ${surface} at a first-class skill.`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const covering = doors.filter((door) => {
|
|
367
|
+
const body = skills[door];
|
|
368
|
+
return typeof body === 'string' && skillMentionsSurface(body, surface);
|
|
369
|
+
});
|
|
370
|
+
if (covering.length === 0) {
|
|
371
|
+
const hint = surface === 'ArkOrder' || surface === 'Ordenar'
|
|
372
|
+
? ' — usually /ark-order'
|
|
373
|
+
: surface === 'ArkRun'
|
|
374
|
+
? ' — usually /ark-runtime'
|
|
375
|
+
: '';
|
|
376
|
+
pushCapacityIssue(issues, {
|
|
377
|
+
code: 'CAPACITY_BODY_GAP',
|
|
378
|
+
surface,
|
|
379
|
+
message: `Skills no longer cover ${surface}. The product has four parts ` +
|
|
380
|
+
`(Layers, ArkRules, ArkRun, ArkOrder) plus Contener · Guiar · Ordenar. ` +
|
|
381
|
+
`Put ${surface} back in a first-class skill${hint} so agents still know when to use it.`,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (const [surface, door] of Object.entries(ARK_SKILL_CAPACITY_DEDICATED_DOORS)) {
|
|
386
|
+
const doors = ARK_SKILL_CAPACITY[surface];
|
|
387
|
+
if (!doors?.includes(door) || !isFirstClassArkSkillName(door)) {
|
|
388
|
+
pushCapacityIssue(issues, {
|
|
389
|
+
code: 'CAPACITY_DEDICATED_DOOR',
|
|
390
|
+
surface,
|
|
391
|
+
skillName: door,
|
|
392
|
+
message: `/${door} is the first-class door for ${surface}. Keep it on that surface. ` +
|
|
393
|
+
`Leftover names like /ark-think are shortcuts, not a replacement.`,
|
|
394
|
+
});
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const body = skills[door];
|
|
398
|
+
if (typeof body !== 'string' || !skillMentionsSurface(body, surface)) {
|
|
399
|
+
pushCapacityIssue(issues, {
|
|
400
|
+
code: 'CAPACITY_DEDICATED_DOOR',
|
|
401
|
+
surface,
|
|
402
|
+
skillName: door,
|
|
403
|
+
message: `/${door} no longer talks about ${surface}. That door is how agents reach ` +
|
|
404
|
+
`this part of the product. Put ${surface} back in the skill body.`,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const name of ARK_FIRST_CLASS_SKILL_NAMES) {
|
|
409
|
+
const body = skills[name];
|
|
410
|
+
if (typeof body !== 'string') {
|
|
411
|
+
pushCapacityIssue(issues, {
|
|
412
|
+
code: 'CAPACITY_ROUTING_GAP',
|
|
413
|
+
skillName: name,
|
|
414
|
+
message: `/${name} is a first-class door but has no skill body in this check. ` +
|
|
415
|
+
`Ship the template so agents know when to use it.`,
|
|
416
|
+
});
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const hasNorthStar = body.includes(NORTH_STAR_PHRASE);
|
|
420
|
+
const hasWhen = /When \/ not when|\*\*When:\*\*/.test(body);
|
|
421
|
+
const hasNotWhen = /Not when|Do \*\*not\*\* use|Prefer instead/.test(body);
|
|
422
|
+
const hasHandoff = /Handoff|hand off|`\/ark-/i.test(body);
|
|
423
|
+
if (!hasNorthStar || !hasWhen || !hasNotWhen || !hasHandoff) {
|
|
424
|
+
pushCapacityIssue(issues, {
|
|
425
|
+
code: 'CAPACITY_ROUTING_GAP',
|
|
426
|
+
skillName: name,
|
|
427
|
+
message: `/${name} is missing when / not when / handoff or Contener · Guiar · Ordenar. ` +
|
|
428
|
+
`First-class skills must say when to use them and which sibling to call next.`,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
if (body.includes(FORBIDDEN_ORDER_FREEZE)) {
|
|
432
|
+
pushCapacityIssue(issues, {
|
|
433
|
+
code: 'CAPACITY_ROUTING_GAP',
|
|
434
|
+
skillName: name,
|
|
435
|
+
message: `/${name} still says not to invent /ark-order. That freeze is outdated — ` +
|
|
436
|
+
`/ark-order is a first-class door.`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const hubs = input.hubs ?? {};
|
|
441
|
+
for (const [hub, text] of Object.entries(hubs)) {
|
|
442
|
+
const body = String(text ?? '');
|
|
443
|
+
if (!body.includes('/ark-order')) {
|
|
444
|
+
pushCapacityIssue(issues, {
|
|
445
|
+
code: 'CAPACITY_HUB_GAP',
|
|
446
|
+
hub,
|
|
447
|
+
message: `${hub} no longer names /ark-order. Host instructions must stay at 100% of ` +
|
|
448
|
+
`the product. Add /ark-order back, or you left the order plane behind.`,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
if (!body.includes(NORTH_STAR_PHRASE)) {
|
|
452
|
+
pushCapacityIssue(issues, {
|
|
453
|
+
code: 'CAPACITY_HUB_GAP',
|
|
454
|
+
hub,
|
|
455
|
+
message: `${hub} no longer says Contener · Guiar · Ordenar. Keep that filter in the ` +
|
|
456
|
+
`living docs so agents pick the right sibling.`,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
if (body.includes(FORBIDDEN_ORDER_FREEZE)) {
|
|
460
|
+
pushCapacityIssue(issues, {
|
|
461
|
+
code: 'CAPACITY_HUB_GAP',
|
|
462
|
+
hub,
|
|
463
|
+
message: `${hub} still says not to invent /ark-order. That freeze is outdated — ` +
|
|
464
|
+
`/ark-order is a first-class door.`,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return { ok: issues.length === 0, issues };
|
|
469
|
+
}
|
|
292
470
|
/**
|
|
293
471
|
* Visible package stamp at the start of Agent Skills `description`.
|
|
294
472
|
* Hosts show `description` in the picker; `arkVersion:` in YAML is invisible there.
|