@open-mercato/cli 0.6.7-develop.6685.1.77c0a5591b → 0.6.7-develop.6686.1.bdda5ed397
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/.turbo/turbo-build.log +2 -3
- package/build.mjs +21 -5
- package/dist/agentic/claude-code/hooks/entity-migration-check.ts +32 -88
- package/dist/agentic/codex/enforcement-rules.md +2 -16
- package/dist/agentic/cursor/hooks/entity-migration-check.mjs +19 -50
- package/dist/agentic/cursor/rules/entity-guard.mdc +9 -21
- package/dist/agentic/cursor/rules/generated-guard.mdc +3 -5
- package/dist/agentic/cursor/rules/open-mercato.mdc +3 -29
- package/dist/agentic/guides/ai-workflows.md +76 -0
- package/dist/agentic/guides/architecture.md +107 -0
- package/dist/agentic/guides/backend-ui.md +80 -0
- package/dist/agentic/guides/contracts.md +92 -0
- package/dist/agentic/guides/extensions.md +104 -0
- package/dist/agentic/guides/integrations.md +81 -0
- package/dist/agentic/guides/module-facts.json +217 -81
- package/dist/agentic/guides/module-system.md +12 -127
- package/dist/agentic/guides/modules/ai_assistant.md +1 -1
- package/dist/agentic/guides/modules/api_docs.md +1 -1
- package/dist/agentic/guides/modules/api_keys.md +1 -1
- package/dist/agentic/guides/modules/attachments.md +2 -2
- package/dist/agentic/guides/modules/audit_logs.md +2 -2
- package/dist/agentic/guides/modules/auth.md +1 -1
- package/dist/agentic/guides/modules/business_rules.md +1 -1
- package/dist/agentic/guides/modules/catalog.md +5 -5
- package/dist/agentic/guides/modules/channel_gmail.md +1 -1
- package/dist/agentic/guides/modules/channel_imap.md +1 -1
- package/dist/agentic/guides/modules/checkout.md +1 -1
- package/dist/agentic/guides/modules/communication_channels.md +1 -1
- package/dist/agentic/guides/modules/configs.md +1 -1
- package/dist/agentic/guides/modules/content.md +1 -1
- package/dist/agentic/guides/modules/currencies.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts.md +1 -1
- package/dist/agentic/guides/modules/customers.md +2 -2
- package/dist/agentic/guides/modules/dashboards.md +1 -1
- package/dist/agentic/guides/modules/data_sync.md +1 -1
- package/dist/agentic/guides/modules/dictionaries.md +1 -1
- package/dist/agentic/guides/modules/directory.md +1 -1
- package/dist/agentic/guides/modules/entities.md +2 -2
- package/dist/agentic/guides/modules/events.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
- package/dist/agentic/guides/modules/generators.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops.md +1 -1
- package/dist/agentic/guides/modules/integrations.md +1 -1
- package/dist/agentic/guides/modules/messages.md +1 -1
- package/dist/agentic/guides/modules/notifications.md +1 -1
- package/dist/agentic/guides/modules/onboarding.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways.md +1 -1
- package/dist/agentic/guides/modules/perspectives.md +1 -1
- package/dist/agentic/guides/modules/planner.md +1 -1
- package/dist/agentic/guides/modules/portal.md +1 -1
- package/dist/agentic/guides/modules/progress.md +1 -1
- package/dist/agentic/guides/modules/query_index.md +2 -2
- package/dist/agentic/guides/modules/record_locks.md +1 -1
- package/dist/agentic/guides/modules/resources.md +1 -1
- package/dist/agentic/guides/modules/sales.md +17 -17
- package/dist/agentic/guides/modules/scheduler.md +1 -1
- package/dist/agentic/guides/modules/search.md +1 -1
- package/dist/agentic/guides/modules/security.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
- package/dist/agentic/guides/modules/sso.md +1 -1
- package/dist/agentic/guides/modules/staff.md +2 -2
- package/dist/agentic/guides/modules/storage_s3.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
- package/dist/agentic/guides/modules/sync_excel.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
- package/dist/agentic/guides/modules/translations.md +1 -1
- package/dist/agentic/guides/modules/webhooks.md +2 -2
- package/dist/agentic/guides/modules/wms.md +2 -2
- package/dist/agentic/guides/modules/workflows.md +1 -1
- package/dist/agentic/guides/testing-debugging.md +74 -0
- package/dist/agentic/guides/upstream/AGENTS.md +297 -0
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +303 -0
- package/dist/agentic/guides/upstream/manifest.json +8 -0
- package/dist/agentic/shared/AGENTS.md.template +123 -311
- package/dist/agentic/shared/ai/agentic.config.json +1 -0
- package/dist/agentic/shared/ai/harness/README.md +48 -0
- package/dist/agentic/shared/ai/harness/RELEASE.md +57 -0
- package/dist/agentic/shared/ai/harness/cases.json +13487 -0
- package/dist/agentic/shared/ai/harness/cases.schema.json +132 -0
- package/dist/agentic/shared/ai/harness/fixtures/index.json +49 -0
- package/dist/agentic/shared/ai/harness/fixtures/seeds.json +162 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +20 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +43 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +110 -0
- package/dist/agentic/shared/ai/harness/release-matrix.json +133 -0
- package/dist/agentic/shared/ai/harness/release-result.schema.json +219 -0
- package/dist/agentic/shared/ai/harness/result.schema.json +76 -0
- package/dist/agentic/shared/ai/harness/routing-response.schema.json +12 -0
- package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +73 -0
- package/dist/agentic/shared/ai/harness/validators.json +78 -0
- package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +1768 -0
- package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +1096 -0
- package/dist/agentic/shared/ai/review-checklist.md +36 -0
- package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +4 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +20 -241
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +16 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +53 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +13 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +11 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +30 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +12 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +13 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +14 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +28 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +12 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +22 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +12 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +19 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +16 -601
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +19 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +13 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +14 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +10 -0
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +14 -310
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +17 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +28 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +89 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +16 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +17 -0
- package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +25 -0
- package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +11 -0
- package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +13 -0
- package/dist/agentic/shared/ai/skills/om-help/SKILL.md +15 -97
- package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +17 -0
- package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +20 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +15 -156
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +12 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +24 -708
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +16 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +14 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +15 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +12 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +20 -820
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +19 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +107 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +12 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +83 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +21 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +11 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +22 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +13 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +19 -843
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +18 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +25 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +13 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +38 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +11 -0
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +15 -63
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +15 -0
- package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +16 -467
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +16 -0
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +14 -0
- package/dist/agentic/shared/ai/skills/tiers.json +137 -20
- package/dist/agentic/shared/ai/skills/tiers.schema.json +52 -40
- package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +174 -0
- package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +2783 -0
- package/dist/agentic/shared/scripts/execution-sandbox.mjs +329 -0
- package/dist/agentic/shared/scripts/framework-context.mjs +894 -0
- package/dist/agentic/shared/scripts/install-skills.mjs +1029 -0
- package/dist/agentic/shared/scripts/install-skills.sh +4 -694
- package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +222 -0
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1761 -0
- package/dist/lib/__integration__/TC-INT-008.spec.js +87 -8
- package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
- package/dist/lib/agentic-init.js +13 -3
- package/dist/lib/agentic-init.js.map +2 -2
- package/dist/lib/agentic-setup.js +418 -186
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generators/module-facts-discovery.js +41 -7
- package/dist/lib/generators/module-facts-discovery.js.map +2 -2
- package/dist/lib/generators/module-facts.js +65 -17
- package/dist/lib/generators/module-facts.js.map +2 -2
- package/dist/lib/generators/module-registry.js +5 -4
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/resolver.js +2 -0
- package/dist/lib/resolver.js.map +2 -2
- package/package.json +5 -5
- package/src/lib/__integration__/TC-INT-008.spec.ts +103 -8
- package/src/lib/__tests__/agentic-init.test.ts +41 -5
- package/src/lib/__tests__/agentic-setup.ownership.test.ts +380 -0
- package/src/lib/agentic-init.ts +15 -3
- package/src/lib/agentic-setup.ts +526 -224
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +6 -0
- package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -0
- package/src/lib/generators/__tests__/module-facts.discovery.test.ts +54 -10
- package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
- package/src/lib/generators/__tests__/module-subset.test.ts +18 -0
- package/src/lib/generators/module-facts-discovery.ts +58 -7
- package/src/lib/generators/module-facts.ts +84 -20
- package/src/lib/generators/module-registry.ts +8 -4
- package/src/lib/resolver.ts +3 -0
- package/dist/agentic/guides/cache.md +0 -45
- package/dist/agentic/guides/core.md +0 -453
- package/dist/agentic/guides/events.md +0 -117
- package/dist/agentic/guides/queue.md +0 -53
- package/dist/agentic/guides/search.md +0 -115
- package/dist/agentic/guides/shared.md +0 -108
- package/dist/agentic/guides/ui.md +0 -347
- package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +0 -27
- package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +0 -27
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +0 -260
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +0 -146
- package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +0 -84
- package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +0 -193
- package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +0 -34
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +0 -762
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +0 -50
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +0 -97
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +0 -271
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +0 -101
package/src/lib/agentic-setup.ts
CHANGED
|
@@ -6,16 +6,35 @@
|
|
|
6
6
|
* This module reads those files at runtime — no embedded string constants.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
chmodSync,
|
|
11
|
+
copyFileSync,
|
|
12
|
+
existsSync,
|
|
13
|
+
lstatSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
mkdtempSync,
|
|
16
|
+
readFileSync,
|
|
17
|
+
readdirSync,
|
|
18
|
+
realpathSync,
|
|
19
|
+
renameSync,
|
|
20
|
+
rmSync,
|
|
21
|
+
statSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from 'node:fs'
|
|
24
|
+
import { createHash } from 'node:crypto'
|
|
10
25
|
import { spawnSync } from 'node:child_process'
|
|
11
|
-
import {
|
|
26
|
+
import { tmpdir } from 'node:os'
|
|
27
|
+
import { join, dirname, basename, relative, resolve, sep } from 'node:path'
|
|
12
28
|
import { fileURLToPath } from 'node:url'
|
|
13
29
|
import { Project, SyntaxKind } from 'ts-morph'
|
|
14
30
|
|
|
15
|
-
const
|
|
16
|
-
// In the built output (dist/lib/agentic-setup.js),
|
|
31
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url))
|
|
32
|
+
// In the built output (dist/lib/agentic-setup.js), moduleDir is dist/lib/.
|
|
17
33
|
// agentic/ is copied to dist/agentic/ by build.mjs.
|
|
18
|
-
const
|
|
34
|
+
const bundledAgenticDir = join(moduleDir, '..', 'agentic')
|
|
35
|
+
const AGENTIC_DIR = existsSync(bundledAgenticDir)
|
|
36
|
+
? bundledAgenticDir
|
|
37
|
+
: join(moduleDir, '..', '..', '..', 'create-app', 'agentic')
|
|
19
38
|
const GUIDES_DIR = join(AGENTIC_DIR, 'guides')
|
|
20
39
|
|
|
21
40
|
type AskFn = (question: string) => Promise<string>
|
|
@@ -23,6 +42,7 @@ type AskFn = (question: string) => Promise<string>
|
|
|
23
42
|
interface AgenticSetupOptions {
|
|
24
43
|
tool?: string
|
|
25
44
|
force?: boolean
|
|
45
|
+
updateHarness?: boolean
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
interface AgenticConfig {
|
|
@@ -30,6 +50,19 @@ interface AgenticConfig {
|
|
|
30
50
|
targetDir: string
|
|
31
51
|
}
|
|
32
52
|
|
|
53
|
+
interface HarnessManifestFile {
|
|
54
|
+
path: string
|
|
55
|
+
sha256: string
|
|
56
|
+
source: string
|
|
57
|
+
userEditable: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface HarnessManifest {
|
|
61
|
+
version: number
|
|
62
|
+
generator: string
|
|
63
|
+
files: HarnessManifestFile[]
|
|
64
|
+
}
|
|
65
|
+
|
|
33
66
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
34
67
|
|
|
35
68
|
function resolvePlaceholders(content: string, config: AgenticConfig): string {
|
|
@@ -59,14 +92,14 @@ function copyFile(srcDir: string, srcRelative: string, destPath: string): void {
|
|
|
59
92
|
// AST-parse the static `enabledModules` array literal in the app's src/modules.ts
|
|
60
93
|
// and collect each entry's `id`. Only the static literal is read (conditional
|
|
61
94
|
// .push()/spread entries are intentionally not seen — see spec D6).
|
|
62
|
-
function readEnabledModuleIds(modulesPath: string): string[] {
|
|
63
|
-
if (!existsSync(modulesPath)) return []
|
|
95
|
+
function readEnabledModuleIds(modulesPath: string): { parsed: boolean; ids: string[] } {
|
|
96
|
+
if (!existsSync(modulesPath)) return { parsed: false, ids: [] }
|
|
64
97
|
try {
|
|
65
98
|
const project = new Project({ useInMemoryFileSystem: true })
|
|
66
99
|
const sourceFile = project.createSourceFile('modules.ts', readFileSync(modulesPath, 'utf-8'))
|
|
67
100
|
const declaration = sourceFile.getVariableDeclaration('enabledModules')
|
|
68
101
|
const arrayLiteral = declaration?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)
|
|
69
|
-
if (!arrayLiteral) return []
|
|
102
|
+
if (!arrayLiteral) return { parsed: false, ids: [] }
|
|
70
103
|
const ids: string[] = []
|
|
71
104
|
for (const element of arrayLiteral.getElements()) {
|
|
72
105
|
const objectLiteral = element.asKind(SyntaxKind.ObjectLiteralExpression)
|
|
@@ -75,9 +108,9 @@ function readEnabledModuleIds(modulesPath: string): string[] {
|
|
|
75
108
|
const idValue = idProperty?.getInitializerIfKind(SyntaxKind.StringLiteral)?.getLiteralValue()
|
|
76
109
|
if (idValue) ids.push(idValue)
|
|
77
110
|
}
|
|
78
|
-
return ids
|
|
111
|
+
return { parsed: true, ids }
|
|
79
112
|
} catch {
|
|
80
|
-
return []
|
|
113
|
+
return { parsed: false, ids: [] }
|
|
81
114
|
}
|
|
82
115
|
}
|
|
83
116
|
|
|
@@ -92,48 +125,23 @@ function selectModuleFactSheets(targetDir: string, modulesSubdir: string): strin
|
|
|
92
125
|
.map((file) => file.replace(/\.md$/, ''))
|
|
93
126
|
: []
|
|
94
127
|
if (available.length === 0) return []
|
|
95
|
-
const
|
|
128
|
+
const parsed = readEnabledModuleIds(join(targetDir, 'src', 'modules.ts'))
|
|
129
|
+
if (!parsed.parsed) return available
|
|
130
|
+
const enabled = new Set(parsed.ids)
|
|
96
131
|
const selected = available.filter((moduleId) => enabled.has(moduleId))
|
|
97
|
-
return selected
|
|
132
|
+
return selected
|
|
98
133
|
}
|
|
99
134
|
|
|
100
135
|
const MODULE_GUIDES_START = '<!-- om:module-guides:start -->'
|
|
101
136
|
const MODULE_GUIDES_END = '<!-- om:module-guides:end -->'
|
|
102
137
|
|
|
103
|
-
|
|
104
|
-
// build.mjs from the generator's extraction of each module's own `metadata`). The
|
|
105
|
-
// label falls back description → title → generic, so the CLI never re-declares
|
|
106
|
-
// specific module names or descriptions. A missing/malformed sidecar degrades to an
|
|
107
|
-
// empty map (generic labels), never a throw.
|
|
108
|
-
function readModuleGuideLabels(guidesDir: string): Record<string, string> {
|
|
109
|
-
const factsPath = join(guidesDir, 'module-facts.json')
|
|
110
|
-
if (!existsSync(factsPath)) return {}
|
|
111
|
-
try {
|
|
112
|
-
const parsed = JSON.parse(readFileSync(factsPath, 'utf-8')) as Record<
|
|
113
|
-
string,
|
|
114
|
-
{ description?: unknown; title?: unknown }
|
|
115
|
-
>
|
|
116
|
-
const labels: Record<string, string> = {}
|
|
117
|
-
for (const [moduleId, entry] of Object.entries(parsed)) {
|
|
118
|
-
const label =
|
|
119
|
-
(entry && typeof entry.description === 'string' && entry.description) ||
|
|
120
|
-
(entry && typeof entry.title === 'string' && entry.title) ||
|
|
121
|
-
''
|
|
122
|
-
if (label) labels[moduleId] = label
|
|
123
|
-
}
|
|
124
|
-
return labels
|
|
125
|
-
} catch {
|
|
126
|
-
return {}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function renderModuleGuidesBlock(selected: string[], labels: Record<string, string>): string {
|
|
138
|
+
export function renderModuleGuidesBlock(selected: string[]): string {
|
|
131
139
|
if (selected.length === 0) return '_No module fact-sheets are bundled for this app._'
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
return [
|
|
141
|
+
`Enabled module facts: ${selected.map((moduleId) => `\`${moduleId}\``).join(',')}.`,
|
|
142
|
+
'',
|
|
143
|
+
'Load `.ai/guides/modules/<id>.md` only when `<id>` is explicitly named or is the targeted installed module/host; never preload all module facts.',
|
|
144
|
+
].join('\n')
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
// Regenerate the marker-delimited Module-Specific Guides block in the written
|
|
@@ -142,7 +150,6 @@ function renderModuleGuidesBlock(selected: string[], labels: Record<string, stri
|
|
|
142
150
|
function injectModuleGuides(
|
|
143
151
|
agentsMdPath: string,
|
|
144
152
|
selected: string[],
|
|
145
|
-
labels: Record<string, string> = {},
|
|
146
153
|
): void {
|
|
147
154
|
if (!existsSync(agentsMdPath)) return
|
|
148
155
|
const content = readFileSync(agentsMdPath, 'utf-8')
|
|
@@ -156,188 +163,424 @@ function injectModuleGuides(
|
|
|
156
163
|
}
|
|
157
164
|
const before = content.slice(0, startIndex + MODULE_GUIDES_START.length)
|
|
158
165
|
const after = content.slice(endIndex)
|
|
159
|
-
const next = `${before}\n${renderModuleGuidesBlock(selected
|
|
166
|
+
const next = `${before}\n${renderModuleGuidesBlock(selected)}\n${after}`
|
|
160
167
|
if (next !== content) writeFileSync(agentsMdPath, next)
|
|
161
168
|
}
|
|
162
169
|
|
|
163
|
-
|
|
170
|
+
const TEXT_EXTENSIONS = new Set(['.cjs', '.json', '.md', '.mdc', '.mjs', '.sh', '.ts', '.txt'])
|
|
164
171
|
|
|
165
|
-
function
|
|
166
|
-
const
|
|
167
|
-
|
|
172
|
+
function isTextAsset(path: string): boolean {
|
|
173
|
+
const dot = path.lastIndexOf('.')
|
|
174
|
+
return dot === -1 || TEXT_EXTENSIONS.has(path.slice(dot))
|
|
175
|
+
}
|
|
168
176
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
177
|
+
function listFiles(root: string): string[] {
|
|
178
|
+
if (!existsSync(root)) return []
|
|
179
|
+
const files: string[] = []
|
|
180
|
+
for (const entry of readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
181
|
+
const absolute = join(root, entry.name)
|
|
182
|
+
if (entry.isDirectory()) files.push(...listFiles(absolute))
|
|
183
|
+
else if (entry.isFile()) files.push(absolute)
|
|
184
|
+
}
|
|
185
|
+
return files
|
|
186
|
+
}
|
|
172
187
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// Skills owned by the external open-mercato/skills collection (code review, spec
|
|
184
|
-
// writing, integration tests, prepare-issue, the auto-* PR family) are NOT copied
|
|
185
|
-
// here; the user installs them with `yarn install-skills` (npx skills add). Only
|
|
186
|
-
// standalone-only + kept-local skills and repo-local override folders are copied.
|
|
187
|
-
copyFile(srcDir, 'ai/skills/tiers.json', join(targetDir, '.ai', 'skills', 'tiers.json'))
|
|
188
|
-
copyFile(srcDir, 'ai/skills/tiers.schema.json', join(targetDir, '.ai', 'skills', 'tiers.schema.json'))
|
|
189
|
-
copyFile(srcDir, 'ai/agentic.config.json', join(targetDir, '.ai', 'agentic.config.json'))
|
|
190
|
-
copyFile(srcDir, 'ai/trackers/github.md', join(targetDir, '.ai', 'trackers', 'github.md'))
|
|
191
|
-
copyFile(srcDir, 'scripts/install-skills.sh', join(targetDir, 'scripts', 'install-skills.sh'))
|
|
192
|
-
|
|
193
|
-
copyFile(
|
|
194
|
-
srcDir,
|
|
195
|
-
'ai/skills/om-backend-ui-design/SKILL.md',
|
|
196
|
-
join(targetDir, '.ai', 'skills', 'om-backend-ui-design', 'SKILL.md'),
|
|
197
|
-
)
|
|
198
|
-
copyFile(
|
|
199
|
-
srcDir,
|
|
200
|
-
'ai/skills/om-backend-ui-design/references/ui-components.md',
|
|
201
|
-
join(targetDir, '.ai', 'skills', 'om-backend-ui-design', 'references', 'ui-components.md'),
|
|
202
|
-
)
|
|
203
|
-
copyFile(srcDir, 'ai/skills/om-integration-builder/SKILL.md', join(targetDir, '.ai', 'skills', 'om-integration-builder', 'SKILL.md'))
|
|
204
|
-
if (existsSync(join(srcDir, 'ai', 'skills', 'om-integration-builder', 'STANDALONE.md'))) {
|
|
205
|
-
copyFile(
|
|
206
|
-
srcDir,
|
|
207
|
-
'ai/skills/om-integration-builder/STANDALONE.md',
|
|
208
|
-
join(targetDir, '.ai', 'skills', 'om-integration-builder', 'STANDALONE.md'),
|
|
209
|
-
)
|
|
188
|
+
function copyTree(sourceRoot: string, destinationRoot: string, config: AgenticConfig): void {
|
|
189
|
+
for (const sourcePath of listFiles(sourceRoot)) {
|
|
190
|
+
const destinationPath = join(destinationRoot, relative(sourceRoot, sourcePath))
|
|
191
|
+
ensureDir(destinationPath)
|
|
192
|
+
if (isTextAsset(sourcePath)) {
|
|
193
|
+
writeFileSync(destinationPath, resolvePlaceholders(readFileSync(sourcePath, 'utf8'), config))
|
|
194
|
+
} else {
|
|
195
|
+
copyFileSync(sourcePath, destinationPath)
|
|
196
|
+
}
|
|
197
|
+
if (process.platform !== 'win32') chmodSync(destinationPath, statSync(sourcePath).mode & 0o777)
|
|
210
198
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function hashFile(path: string): string {
|
|
202
|
+
return createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function canonicalHarnessRoot(root: string): string {
|
|
206
|
+
const resolved = resolve(root)
|
|
207
|
+
if (!existsSync(resolved)) {
|
|
208
|
+
throw new Error(`Harness root must be an existing directory: ${resolved}`)
|
|
209
|
+
}
|
|
210
|
+
const canonical = realpathSync(resolved)
|
|
211
|
+
const entry = lstatSync(canonical, { throwIfNoEntry: false })
|
|
212
|
+
if (!entry || !entry.isDirectory()) {
|
|
213
|
+
throw new Error(`Harness root must be an existing directory: ${resolved}`)
|
|
222
214
|
}
|
|
215
|
+
return canonical
|
|
216
|
+
}
|
|
223
217
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Validate a managed path without following any link below the canonical app
|
|
220
|
+
* root. A lexical containment check alone is insufficient: `app/.ai -> /tmp`
|
|
221
|
+
* would otherwise make an apparently safe update write outside the app.
|
|
222
|
+
*/
|
|
223
|
+
function assertManagedPath(root: string, path: string, options: { leaf?: 'file' | 'any' } = {}): void {
|
|
224
|
+
const absolutePath = resolve(path)
|
|
225
|
+
const normalizedRelative = relative(root, absolutePath)
|
|
226
|
+
if (
|
|
227
|
+
normalizedRelative === '..' ||
|
|
228
|
+
normalizedRelative.startsWith(`..${sep}`)
|
|
229
|
+
) {
|
|
230
|
+
throw new Error(`Harness-managed path escapes its root: ${absolutePath}`)
|
|
231
|
+
}
|
|
232
|
+
if (normalizedRelative === '') return
|
|
233
|
+
|
|
234
|
+
const components = normalizedRelative.split(sep).filter(Boolean)
|
|
235
|
+
let current = root
|
|
236
|
+
for (let index = 0; index < components.length; index += 1) {
|
|
237
|
+
current = join(current, components[index])
|
|
238
|
+
const entry = lstatSync(current, { throwIfNoEntry: false })
|
|
239
|
+
if (!entry) return
|
|
240
|
+
if (entry.isSymbolicLink()) {
|
|
241
|
+
throw new Error(`Harness-managed path contains a symbolic-link component: ${current}`)
|
|
242
|
+
}
|
|
243
|
+
const isLeaf = index === components.length - 1
|
|
244
|
+
if (!isLeaf && !entry.isDirectory()) {
|
|
245
|
+
throw new Error(`Harness-managed path component is not a directory: ${current}`)
|
|
246
|
+
}
|
|
247
|
+
if (isLeaf && options.leaf === 'file' && !entry.isFile()) {
|
|
248
|
+
throw new Error(`Harness-managed file is not a regular file: ${current}`)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
234
252
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
253
|
+
function ensureManagedParent(root: string, filePath: string): void {
|
|
254
|
+
const parent = dirname(filePath)
|
|
255
|
+
const normalizedRelative = relative(root, parent)
|
|
256
|
+
if (normalizedRelative === '..' || normalizedRelative.startsWith(`..${sep}`)) {
|
|
257
|
+
throw new Error(`Harness-managed path escapes its root: ${filePath}`)
|
|
258
|
+
}
|
|
259
|
+
let current = root
|
|
260
|
+
for (const component of normalizedRelative.split(sep).filter(Boolean)) {
|
|
261
|
+
assertManagedPath(root, current)
|
|
262
|
+
current = join(current, component)
|
|
263
|
+
const entry = lstatSync(current, { throwIfNoEntry: false })
|
|
264
|
+
if (!entry) mkdirSync(current)
|
|
265
|
+
const created = lstatSync(current, { throwIfNoEntry: false })
|
|
266
|
+
if (!created || created.isSymbolicLink() || !created.isDirectory()) {
|
|
267
|
+
throw new Error(`Harness-managed path component is not a real directory: ${current}`)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
250
271
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
'
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
272
|
+
function harnessGeneratorId(): string {
|
|
273
|
+
try {
|
|
274
|
+
const upstream = JSON.parse(readFileSync(join(GUIDES_DIR, 'upstream', 'manifest.json'), 'utf8')) as {
|
|
275
|
+
generator?: unknown
|
|
276
|
+
}
|
|
277
|
+
const generator = typeof upstream.generator === 'string' ? upstream.generator : ''
|
|
278
|
+
const version = generator.slice(generator.lastIndexOf('@') + 1)
|
|
279
|
+
if (version && version !== generator) return `open-mercato-agentic@${version}`
|
|
280
|
+
} catch {
|
|
281
|
+
// Source-mode tests may not have built upstream snapshots yet.
|
|
282
|
+
}
|
|
283
|
+
return 'open-mercato-agentic@unknown'
|
|
284
|
+
}
|
|
261
285
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
'ai
|
|
265
|
-
|
|
266
|
-
|
|
286
|
+
function externalSkillNames(targetDir: string): Set<string> {
|
|
287
|
+
try {
|
|
288
|
+
const tiers = JSON.parse(readFileSync(join(targetDir, '.ai', 'skills', 'tiers.json'), 'utf8')) as {
|
|
289
|
+
external?: { skills?: unknown }
|
|
290
|
+
}
|
|
291
|
+
return new Set(Array.isArray(tiers.external?.skills) ? tiers.external.skills.filter((name): name is string => typeof name === 'string') : [])
|
|
292
|
+
} catch {
|
|
293
|
+
return new Set()
|
|
294
|
+
}
|
|
295
|
+
}
|
|
267
296
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
'
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
297
|
+
function atomicCopyFile(sourcePath: string, destinationPath: string, managedRoot?: string): void {
|
|
298
|
+
if (managedRoot) {
|
|
299
|
+
assertManagedPath(managedRoot, destinationPath, { leaf: 'file' })
|
|
300
|
+
ensureManagedParent(managedRoot, destinationPath)
|
|
301
|
+
} else {
|
|
302
|
+
ensureDir(destinationPath)
|
|
303
|
+
}
|
|
304
|
+
const temporaryPath = `${destinationPath}.tmp-${process.pid}-${Date.now()}`
|
|
305
|
+
try {
|
|
306
|
+
if (managedRoot) assertManagedPath(managedRoot, temporaryPath, { leaf: 'file' })
|
|
307
|
+
copyFileSync(sourcePath, temporaryPath)
|
|
308
|
+
if (process.platform !== 'win32') chmodSync(temporaryPath, statSync(sourcePath).mode & 0o777)
|
|
309
|
+
if (managedRoot) {
|
|
310
|
+
assertManagedPath(managedRoot, dirname(destinationPath))
|
|
311
|
+
assertManagedPath(managedRoot, destinationPath, { leaf: 'file' })
|
|
312
|
+
assertManagedPath(managedRoot, temporaryPath, { leaf: 'file' })
|
|
313
|
+
}
|
|
314
|
+
renameSync(temporaryPath, destinationPath)
|
|
315
|
+
} finally {
|
|
316
|
+
if (!managedRoot || !lstatSync(temporaryPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
317
|
+
rmSync(temporaryPath, { force: true })
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
278
321
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
'
|
|
282
|
-
|
|
283
|
-
|
|
322
|
+
function atomicWriteFile(destinationPath: string, content: string, managedRoot?: string): void {
|
|
323
|
+
if (managedRoot) {
|
|
324
|
+
assertManagedPath(managedRoot, destinationPath, { leaf: 'file' })
|
|
325
|
+
ensureManagedParent(managedRoot, destinationPath)
|
|
326
|
+
} else {
|
|
327
|
+
ensureDir(destinationPath)
|
|
328
|
+
}
|
|
329
|
+
const temporaryPath = `${destinationPath}.tmp-${process.pid}-${Date.now()}`
|
|
330
|
+
try {
|
|
331
|
+
if (managedRoot) assertManagedPath(managedRoot, temporaryPath, { leaf: 'file' })
|
|
332
|
+
writeFileSync(temporaryPath, content)
|
|
333
|
+
if (managedRoot) {
|
|
334
|
+
assertManagedPath(managedRoot, dirname(destinationPath))
|
|
335
|
+
assertManagedPath(managedRoot, destinationPath, { leaf: 'file' })
|
|
336
|
+
assertManagedPath(managedRoot, temporaryPath, { leaf: 'file' })
|
|
337
|
+
}
|
|
338
|
+
renameSync(temporaryPath, destinationPath)
|
|
339
|
+
} finally {
|
|
340
|
+
if (!managedRoot || !lstatSync(temporaryPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
341
|
+
rmSync(temporaryPath, { force: true })
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
284
345
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
346
|
+
function resolveManifestPath(root: string, relativePath: string): string | null {
|
|
347
|
+
if (!relativePath || relativePath.includes('\0')) return null
|
|
348
|
+
const absolutePath = resolve(root, relativePath)
|
|
349
|
+
const normalizedRelative = relative(root, absolutePath)
|
|
350
|
+
if (
|
|
351
|
+
normalizedRelative === '' ||
|
|
352
|
+
normalizedRelative === '..' ||
|
|
353
|
+
normalizedRelative.startsWith(`..${sep}`)
|
|
354
|
+
) {
|
|
355
|
+
return null
|
|
356
|
+
}
|
|
357
|
+
return absolutePath
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function normalizeManifestRelativePath(path: string): string {
|
|
361
|
+
return path.replace(/\\/g, '/')
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function readHarnessManifest(manifestPath: string): HarnessManifest | null {
|
|
365
|
+
if (!existsSync(manifestPath)) return null
|
|
366
|
+
try {
|
|
367
|
+
const parsed = JSON.parse(readFileSync(manifestPath, 'utf8')) as Partial<HarnessManifest>
|
|
368
|
+
if (
|
|
369
|
+
typeof parsed.version !== 'number' ||
|
|
370
|
+
typeof parsed.generator !== 'string' ||
|
|
371
|
+
!Array.isArray(parsed.files)
|
|
372
|
+
) {
|
|
373
|
+
return null
|
|
374
|
+
}
|
|
375
|
+
const files: HarnessManifestFile[] = []
|
|
376
|
+
for (const entry of parsed.files) {
|
|
377
|
+
if (
|
|
378
|
+
!entry ||
|
|
379
|
+
typeof entry.path !== 'string' ||
|
|
380
|
+
typeof entry.sha256 !== 'string' ||
|
|
381
|
+
!/^[a-f0-9]{64}$/.test(entry.sha256) ||
|
|
382
|
+
typeof entry.source !== 'string' ||
|
|
383
|
+
typeof entry.userEditable !== 'boolean'
|
|
384
|
+
) {
|
|
385
|
+
return null
|
|
386
|
+
}
|
|
387
|
+
files.push({ ...entry, path: normalizeManifestRelativePath(entry.path) })
|
|
388
|
+
}
|
|
389
|
+
return { version: parsed.version, generator: parsed.generator, files }
|
|
390
|
+
} catch {
|
|
391
|
+
return null
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function targetPathsForTree(sourceRoot: string, destinationRoot: string): string[] {
|
|
396
|
+
return listFiles(sourceRoot).map((sourcePath) => join(destinationRoot, relative(sourceRoot, sourcePath)))
|
|
397
|
+
}
|
|
300
398
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
399
|
+
function finalizeHarnessManifest(config: AgenticConfig, selectedTools: string[]): void {
|
|
400
|
+
const { targetDir } = config
|
|
401
|
+
const srcDir = join(AGENTIC_DIR, 'shared')
|
|
402
|
+
const selectedModules = selectModuleFactSheets(targetDir, join(GUIDES_DIR, 'modules'))
|
|
403
|
+
const paths = new Set<string>([
|
|
404
|
+
join(targetDir, 'AGENTS.md'),
|
|
405
|
+
...targetPathsForTree(join(srcDir, 'ai'), join(targetDir, '.ai')),
|
|
406
|
+
...targetPathsForTree(join(srcDir, 'scripts'), join(targetDir, 'scripts')),
|
|
407
|
+
])
|
|
408
|
+
for (const file of readdirSync(GUIDES_DIR)) {
|
|
409
|
+
if (file.endsWith('.md') || file === 'module-facts.json') paths.add(join(targetDir, '.ai', 'guides', file))
|
|
410
|
+
}
|
|
411
|
+
for (const file of listFiles(join(GUIDES_DIR, 'upstream'))) {
|
|
412
|
+
paths.add(join(targetDir, '.ai', 'guides', 'upstream', relative(join(GUIDES_DIR, 'upstream'), file)))
|
|
413
|
+
}
|
|
414
|
+
for (const moduleId of selectedModules) paths.add(join(targetDir, '.ai', 'guides', 'modules', `${moduleId}.md`))
|
|
415
|
+
if (selectedTools.includes('claude-code')) {
|
|
416
|
+
paths.add(join(targetDir, 'CLAUDE.md'))
|
|
417
|
+
paths.add(join(targetDir, '.claude', 'settings.json'))
|
|
418
|
+
paths.add(join(targetDir, '.claude', 'hooks', 'entity-migration-check.ts'))
|
|
419
|
+
paths.add(join(targetDir, '.mcp.json.example'))
|
|
420
|
+
}
|
|
421
|
+
if (selectedTools.includes('codex')) paths.add(join(targetDir, '.codex', 'mcp.json.example'))
|
|
422
|
+
if (selectedTools.includes('cursor')) {
|
|
423
|
+
for (const file of listFiles(join(AGENTIC_DIR, 'cursor'))) {
|
|
424
|
+
const rel = relative(join(AGENTIC_DIR, 'cursor'), file)
|
|
425
|
+
paths.add(join(targetDir, '.cursor', rel))
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const manifestPath = join(targetDir, '.ai', 'harness', 'manifest.json')
|
|
429
|
+
const externalSkills = externalSkillNames(targetDir)
|
|
430
|
+
paths.delete(manifestPath)
|
|
431
|
+
const files = [...paths]
|
|
432
|
+
.filter((path) => existsSync(path))
|
|
433
|
+
.sort((left, right) => left.localeCompare(right))
|
|
434
|
+
.map((path) => {
|
|
435
|
+
const relativePath = normalizeManifestRelativePath(relative(targetDir, path))
|
|
436
|
+
const skillName = relativePath.match(/^\.ai\/skills\/([^/]+)\//)?.[1]
|
|
437
|
+
return {
|
|
438
|
+
path: relativePath,
|
|
439
|
+
sha256: hashFile(path),
|
|
440
|
+
source: skillName ? (externalSkills.has(skillName) ? 'external-override' : 'local-skill') : 'generated',
|
|
441
|
+
userEditable: relativePath === 'AGENTS.md' || relativePath === '.ai/agentic.config.json' || relativePath === '.ai/lessons.md',
|
|
442
|
+
}
|
|
443
|
+
})
|
|
444
|
+
atomicWriteFile(
|
|
445
|
+
manifestPath,
|
|
446
|
+
`${JSON.stringify({ version: 1, generator: harnessGeneratorId(), files }, null, 2)}\n`,
|
|
305
447
|
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Publish a fully generated harness candidate using the previous ownership
|
|
452
|
+
* manifest as the only source of ownership. Modified owned files and exact-path
|
|
453
|
+
* collisions with unknown files stay untouched; their candidate is written next
|
|
454
|
+
* to them as `<path>.incoming` for explicit review.
|
|
455
|
+
*/
|
|
456
|
+
export type HarnessUpdateConflict = { path: string; candidate: string | null }
|
|
457
|
+
|
|
458
|
+
function preserveIncomingCandidate(
|
|
459
|
+
targetRoot: string,
|
|
460
|
+
destinationPath: string,
|
|
461
|
+
sourcePath: string,
|
|
462
|
+
relativePath: string,
|
|
463
|
+
): string {
|
|
464
|
+
for (let suffix = 1; ; suffix += 1) {
|
|
465
|
+
const label = suffix === 1 ? '.incoming' : `.incoming.${suffix}`
|
|
466
|
+
const candidatePath = `${destinationPath}${label}`
|
|
467
|
+
assertManagedPath(targetRoot, candidatePath, { leaf: 'file' })
|
|
468
|
+
if (!existsSync(candidatePath)) {
|
|
469
|
+
atomicCopyFile(sourcePath, candidatePath, targetRoot)
|
|
470
|
+
return `${relativePath}${label}`
|
|
471
|
+
}
|
|
472
|
+
if (hashFile(candidatePath) === hashFile(sourcePath)) return `${relativePath}${label}`
|
|
473
|
+
}
|
|
474
|
+
}
|
|
306
475
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
'
|
|
321
|
-
|
|
322
|
-
|
|
476
|
+
export function applyHarnessUpdate(
|
|
477
|
+
targetDir: string,
|
|
478
|
+
stagingDir: string,
|
|
479
|
+
options: { force?: boolean } = {},
|
|
480
|
+
): HarnessUpdateConflict[] {
|
|
481
|
+
const targetRoot = canonicalHarnessRoot(targetDir)
|
|
482
|
+
const stagingRoot = canonicalHarnessRoot(stagingDir)
|
|
483
|
+
const targetManifestPath = join(targetRoot, '.ai', 'harness', 'manifest.json')
|
|
484
|
+
const stagingManifestPath = join(stagingRoot, '.ai', 'harness', 'manifest.json')
|
|
485
|
+
assertManagedPath(targetRoot, targetManifestPath, { leaf: 'file' })
|
|
486
|
+
assertManagedPath(stagingRoot, stagingManifestPath, { leaf: 'file' })
|
|
487
|
+
const candidateManifest = readHarnessManifest(stagingManifestPath)
|
|
488
|
+
if (!candidateManifest) {
|
|
489
|
+
throw new Error('Generated harness candidate has a missing or invalid ownership manifest.')
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const previousManifest = readHarnessManifest(targetManifestPath)
|
|
493
|
+
const previousFiles = new Map(previousManifest?.files.map((entry) => [entry.path, entry]) ?? [])
|
|
494
|
+
const candidatePaths = new Set(candidateManifest.files.map((entry) => entry.path))
|
|
495
|
+
const candidates: Array<{
|
|
496
|
+
entry: HarnessManifestFile
|
|
497
|
+
sourcePath: string
|
|
498
|
+
destinationPath: string
|
|
499
|
+
}> = []
|
|
500
|
+
|
|
501
|
+
// Validate the complete candidate before writing anything into the app.
|
|
502
|
+
for (const entry of candidateManifest.files) {
|
|
503
|
+
const sourcePath = resolveManifestPath(stagingRoot, entry.path)
|
|
504
|
+
const destinationPath = resolveManifestPath(targetRoot, entry.path)
|
|
505
|
+
if (sourcePath) assertManagedPath(stagingRoot, sourcePath, { leaf: 'file' })
|
|
506
|
+
if (destinationPath) assertManagedPath(targetRoot, destinationPath, { leaf: 'file' })
|
|
507
|
+
if (!sourcePath || !destinationPath || !existsSync(sourcePath) || hashFile(sourcePath) !== entry.sha256) {
|
|
508
|
+
throw new Error(`Generated harness candidate is invalid for ${JSON.stringify(entry.path)}.`)
|
|
509
|
+
}
|
|
510
|
+
candidates.push({ entry, sourcePath, destinationPath })
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const conflicts: HarnessUpdateConflict[] = []
|
|
514
|
+
for (const { entry, sourcePath, destinationPath } of candidates) {
|
|
515
|
+
if (options.force) {
|
|
516
|
+
atomicCopyFile(sourcePath, destinationPath, targetRoot)
|
|
323
517
|
continue
|
|
324
518
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
519
|
+
if (!existsSync(destinationPath)) {
|
|
520
|
+
atomicCopyFile(sourcePath, destinationPath, targetRoot)
|
|
521
|
+
continue
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const currentHash = hashFile(destinationPath)
|
|
525
|
+
const previousEntry = previousFiles.get(entry.path)
|
|
526
|
+
if (currentHash === entry.sha256 || (previousEntry && currentHash === previousEntry.sha256)) {
|
|
527
|
+
atomicCopyFile(sourcePath, destinationPath, targetRoot)
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const candidate = preserveIncomingCandidate(targetRoot, destinationPath, sourcePath, entry.path)
|
|
532
|
+
conflicts.push({ path: entry.path, candidate })
|
|
330
533
|
}
|
|
331
534
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
535
|
+
const retainedRetiredEntries: HarnessManifestFile[] = []
|
|
536
|
+
for (const previousEntry of previousManifest?.files ?? []) {
|
|
537
|
+
if (candidatePaths.has(previousEntry.path)) continue
|
|
538
|
+
const destinationPath = resolveManifestPath(targetRoot, previousEntry.path)
|
|
539
|
+
if (!destinationPath || !existsSync(destinationPath)) continue
|
|
540
|
+
assertManagedPath(targetRoot, destinationPath, { leaf: 'file' })
|
|
541
|
+
let unchanged = false
|
|
542
|
+
try {
|
|
543
|
+
unchanged = hashFile(destinationPath) === previousEntry.sha256
|
|
544
|
+
} catch {
|
|
545
|
+
// A user may replace a generated file with another filesystem object.
|
|
546
|
+
// Preserve it and retain the ownership tombstone for a later review.
|
|
547
|
+
}
|
|
548
|
+
if (unchanged) {
|
|
549
|
+
assertManagedPath(targetRoot, dirname(destinationPath))
|
|
550
|
+
assertManagedPath(targetRoot, destinationPath, { leaf: 'file' })
|
|
551
|
+
rmSync(destinationPath, { force: true })
|
|
552
|
+
} else {
|
|
553
|
+
retainedRetiredEntries.push(previousEntry)
|
|
554
|
+
conflicts.push({ path: previousEntry.path, candidate: null })
|
|
555
|
+
}
|
|
556
|
+
}
|
|
337
557
|
|
|
338
|
-
|
|
558
|
+
// The candidate hashes deliberately remain in the manifest for conflicts. If
|
|
559
|
+
// the user accepts an .incoming file, a later update can recognize it as an
|
|
560
|
+
// unmodified owned file. Until then the hash mismatch keeps preserving it.
|
|
561
|
+
const finalManifest: HarnessManifest = {
|
|
562
|
+
...candidateManifest,
|
|
563
|
+
files: [...candidateManifest.files, ...retainedRetiredEntries],
|
|
564
|
+
}
|
|
565
|
+
atomicWriteFile(targetManifestPath, `${JSON.stringify(finalManifest, null, 2)}\n`, targetRoot)
|
|
566
|
+
return conflicts
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ─── Generators ──────────────────────────────────────────────────────────
|
|
339
570
|
|
|
340
|
-
|
|
571
|
+
function generateShared(config: AgenticConfig): void {
|
|
572
|
+
const { targetDir } = config
|
|
573
|
+
const srcDir = join(AGENTIC_DIR, 'shared')
|
|
574
|
+
|
|
575
|
+
// Resolve which per-module fact-sheets this app gets (enabled ∩ bundled allowlist).
|
|
576
|
+
const selectedModules = selectModuleFactSheets(targetDir, join(GUIDES_DIR, 'modules'))
|
|
577
|
+
|
|
578
|
+
// One recursive mapping mirrors create-app's shared emitter.
|
|
579
|
+
writeTemplate(srcDir, 'AGENTS.md.template', join(targetDir, 'AGENTS.md'), config)
|
|
580
|
+
copyTree(join(srcDir, 'ai'), join(targetDir, '.ai'), config)
|
|
581
|
+
copyTree(join(srcDir, 'scripts'), join(targetDir, 'scripts'), config)
|
|
582
|
+
|
|
583
|
+
// Routed conceptual guides are copied wholesale (framework-wide). Per-module
|
|
341
584
|
// fact-sheets (.ai/guides/modules/<module>.md) are filtered to the app's enabled
|
|
342
585
|
// module set; the combined module-facts.json sidecar is copied as-is.
|
|
343
586
|
if (existsSync(GUIDES_DIR)) {
|
|
@@ -357,6 +600,9 @@ function generateShared(config: AgenticConfig): void {
|
|
|
357
600
|
copyFileSync(moduleFactsPath, destPath)
|
|
358
601
|
}
|
|
359
602
|
|
|
603
|
+
|
|
604
|
+
copyTree(join(GUIDES_DIR, 'upstream'), join(guidesDestDir, 'upstream'), config)
|
|
605
|
+
|
|
360
606
|
const modulesSubdir = join(GUIDES_DIR, 'modules')
|
|
361
607
|
for (const moduleId of selectedModules) {
|
|
362
608
|
const destPath = join(guidesDestDir, 'modules', `${moduleId}.md`)
|
|
@@ -364,6 +610,8 @@ function generateShared(config: AgenticConfig): void {
|
|
|
364
610
|
copyFileSync(join(modulesSubdir, `${moduleId}.md`), destPath)
|
|
365
611
|
}
|
|
366
612
|
}
|
|
613
|
+
|
|
614
|
+
injectModuleGuides(join(targetDir, 'AGENTS.md'), selectedModules)
|
|
367
615
|
}
|
|
368
616
|
|
|
369
617
|
function generateClaudeCode(config: AgenticConfig): void {
|
|
@@ -375,8 +623,7 @@ function generateClaudeCode(config: AgenticConfig): void {
|
|
|
375
623
|
copyFile(srcDir, 'hooks/entity-migration-check.ts', join(targetDir, '.claude', 'hooks', 'entity-migration-check.ts'))
|
|
376
624
|
copyFile(srcDir, 'mcp.json.example', join(targetDir, '.mcp.json.example'))
|
|
377
625
|
|
|
378
|
-
//
|
|
379
|
-
ensureSkillsLink(join(targetDir, '.claude', 'skills'), join('..', '.ai', 'skills'))
|
|
626
|
+
// The installer exclusively owns Claude's per-skill compatibility links.
|
|
380
627
|
}
|
|
381
628
|
|
|
382
629
|
function generateCodex(config: AgenticConfig): void {
|
|
@@ -428,17 +675,6 @@ function generateCursor(config: AgenticConfig): void {
|
|
|
428
675
|
// which scripts/install-skills.sh populates.
|
|
429
676
|
}
|
|
430
677
|
|
|
431
|
-
function ensureSkillsLink(linkPath: string, target: string): void {
|
|
432
|
-
ensureDir(linkPath)
|
|
433
|
-
if (existsSync(linkPath) && !lstatSync(linkPath).isSymbolicLink()) {
|
|
434
|
-
return
|
|
435
|
-
}
|
|
436
|
-
if (lstatSync(linkPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
437
|
-
unlinkSync(linkPath)
|
|
438
|
-
}
|
|
439
|
-
symlinkSync(target, linkPath)
|
|
440
|
-
}
|
|
441
|
-
|
|
442
678
|
// ─── Wizard ──────────────────────────────────────────────────────────────
|
|
443
679
|
|
|
444
680
|
const TOOLS = [
|
|
@@ -451,6 +687,35 @@ const TOOLS = [
|
|
|
451
687
|
|
|
452
688
|
const SELECTABLE = TOOLS.filter((t) => t.id !== 'multiple' && t.id !== 'skip')
|
|
453
689
|
|
|
690
|
+
function persistedAgentSelection(targetDir: string): string[] | null {
|
|
691
|
+
const selectableIds = SELECTABLE.map((tool) => tool.id)
|
|
692
|
+
const tiersPath = join(targetDir, '.ai', 'skills', 'tiers.json')
|
|
693
|
+
try {
|
|
694
|
+
const tiers = JSON.parse(readFileSync(tiersPath, 'utf8')) as { agents?: { ignore?: unknown } }
|
|
695
|
+
if (Array.isArray(tiers.agents?.ignore)
|
|
696
|
+
&& tiers.agents.ignore.every((id): id is string => typeof id === 'string')) {
|
|
697
|
+
const ignored = new Set(tiers.agents.ignore)
|
|
698
|
+
return selectableIds.filter((id) => !ignored.has(id))
|
|
699
|
+
}
|
|
700
|
+
} catch {
|
|
701
|
+
// Fall through to the ownership manifest and existing generated assets.
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const manifest = readHarnessManifest(join(targetDir, '.ai', 'harness', 'manifest.json'))
|
|
705
|
+
const owned = new Set(manifest?.files.map((entry) => entry.path) ?? [])
|
|
706
|
+
const selected = SELECTABLE.filter((tool) => {
|
|
707
|
+
if (tool.id === 'claude-code') {
|
|
708
|
+
return owned.has('CLAUDE.md') || owned.has('.claude/settings.json')
|
|
709
|
+
|| existsSync(join(targetDir, 'CLAUDE.md')) || existsSync(join(targetDir, '.claude', 'settings.json'))
|
|
710
|
+
}
|
|
711
|
+
if (tool.id === 'codex') {
|
|
712
|
+
return owned.has('.codex/mcp.json.example') || existsSync(join(targetDir, '.codex', 'mcp.json.example'))
|
|
713
|
+
}
|
|
714
|
+
return owned.has('.cursor/hooks.json') || existsSync(join(targetDir, '.cursor', 'hooks.json'))
|
|
715
|
+
}).map((tool) => tool.id)
|
|
716
|
+
return selected.length > 0 ? selected : null
|
|
717
|
+
}
|
|
718
|
+
|
|
454
719
|
async function promptSelection(ask: AskFn): Promise<string[]> {
|
|
455
720
|
console.log('')
|
|
456
721
|
console.log('🤖 Agentic workflow setup')
|
|
@@ -497,6 +762,8 @@ export async function runAgenticSetup(
|
|
|
497
762
|
|
|
498
763
|
if (options?.tool) {
|
|
499
764
|
selectedIds = options.tool.split(',').map((t) => t.trim())
|
|
765
|
+
} else if (options?.updateHarness) {
|
|
766
|
+
selectedIds = persistedAgentSelection(targetDir) ?? await promptSelection(ask)
|
|
500
767
|
} else {
|
|
501
768
|
selectedIds = await promptSelection(ask)
|
|
502
769
|
}
|
|
@@ -513,12 +780,37 @@ export async function runAgenticSetup(
|
|
|
513
780
|
targetDir,
|
|
514
781
|
}
|
|
515
782
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
783
|
+
const stagingDir = mkdtempSync(join(tmpdir(), 'open-mercato-harness-'))
|
|
784
|
+
try {
|
|
785
|
+
const modulesSourcePath = join(targetDir, 'src', 'modules.ts')
|
|
786
|
+
const modulesCandidatePath = join(stagingDir, 'src', 'modules.ts')
|
|
787
|
+
if (existsSync(modulesSourcePath)) {
|
|
788
|
+
ensureDir(modulesCandidatePath)
|
|
789
|
+
copyFileSync(modulesSourcePath, modulesCandidatePath)
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const stagingConfig: AgenticConfig = {
|
|
793
|
+
projectName: config.projectName,
|
|
794
|
+
targetDir: stagingDir,
|
|
795
|
+
}
|
|
796
|
+
generateHarness(stagingConfig, selectedIds)
|
|
797
|
+
const conflicts = applyHarnessUpdate(targetDir, stagingDir, {
|
|
798
|
+
force: options?.force,
|
|
799
|
+
})
|
|
800
|
+
if (conflicts.length > 0) {
|
|
801
|
+
console.warn('')
|
|
802
|
+
console.warn(' ⚠ Preserved locally modified harness files:')
|
|
803
|
+
for (const conflict of conflicts) {
|
|
804
|
+
const detail = conflict.candidate
|
|
805
|
+
? `candidate: ${conflict.candidate}`
|
|
806
|
+
: 'retired asset kept because it has local changes'
|
|
807
|
+
console.warn(` • ${conflict.path} (${detail})`)
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
} finally {
|
|
811
|
+
rmSync(stagingDir, { recursive: true, force: true })
|
|
812
|
+
}
|
|
520
813
|
|
|
521
|
-
persistAgentSelection(targetDir, selectedIds)
|
|
522
814
|
installSkills(targetDir)
|
|
523
815
|
|
|
524
816
|
console.log('')
|
|
@@ -540,6 +832,16 @@ export async function runAgenticSetup(
|
|
|
540
832
|
console.log('')
|
|
541
833
|
}
|
|
542
834
|
|
|
835
|
+
function generateHarness(config: AgenticConfig, selectedIds: string[]): void {
|
|
836
|
+
generateShared(config)
|
|
837
|
+
if (selectedIds.includes('claude-code')) generateClaudeCode(config)
|
|
838
|
+
if (selectedIds.includes('codex')) generateCodex(config)
|
|
839
|
+
if (selectedIds.includes('cursor')) generateCursor(config)
|
|
840
|
+
|
|
841
|
+
persistAgentSelection(config.targetDir, selectedIds)
|
|
842
|
+
finalizeHarnessManifest(config, selectedIds)
|
|
843
|
+
}
|
|
844
|
+
|
|
543
845
|
/**
|
|
544
846
|
* Persist the agent selection so later `yarn install-skills` runs keep honoring
|
|
545
847
|
* it: agents the user did not pick go into `agents.ignore` in tiers.json and
|
|
@@ -559,11 +861,11 @@ function persistAgentSelection(targetDir: string, selectedIds: string[]): void {
|
|
|
559
861
|
}
|
|
560
862
|
|
|
561
863
|
function installSkills(targetDir: string): void {
|
|
562
|
-
const installScript = join(targetDir, 'scripts', 'install-skills.
|
|
864
|
+
const installScript = join(targetDir, 'scripts', 'install-skills.mjs')
|
|
563
865
|
if (!existsSync(installScript)) return
|
|
564
866
|
console.log('')
|
|
565
867
|
console.log(' Installing agent skills (local tiers + external open-mercato/skills subset)...')
|
|
566
|
-
const result = spawnSync(
|
|
868
|
+
const result = spawnSync(process.execPath, [installScript], { cwd: targetDir, stdio: 'inherit' })
|
|
567
869
|
if (result.error || result.status !== 0) {
|
|
568
870
|
console.log(' ⚠ Skill installation did not complete; run `yarn install-skills` inside the app when online.')
|
|
569
871
|
}
|