@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
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import {
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
statSync,
|
|
10
|
+
symlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs'
|
|
13
|
+
import { tmpdir } from 'node:os'
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
import { applyHarnessUpdate, runAgenticSetup } from '../agentic-setup'
|
|
17
|
+
|
|
18
|
+
type ManifestEntry = {
|
|
19
|
+
path: string
|
|
20
|
+
sha256: string
|
|
21
|
+
source: string
|
|
22
|
+
userEditable: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function write(path: string, content: string): void {
|
|
26
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
27
|
+
writeFileSync(path, content)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hash(content: string): string {
|
|
31
|
+
return createHash('sha256').update(content).digest('hex')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function entry(path: string, content: string): ManifestEntry {
|
|
35
|
+
return {
|
|
36
|
+
path,
|
|
37
|
+
sha256: hash(content),
|
|
38
|
+
source: 'generated',
|
|
39
|
+
userEditable: false,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeManifest(root: string, files: ManifestEntry[]): string {
|
|
44
|
+
const path = join(root, '.ai', 'harness', 'manifest.json')
|
|
45
|
+
write(
|
|
46
|
+
path,
|
|
47
|
+
`${JSON.stringify({ version: 1, generator: 'open-mercato-agentic', files }, null, 2)}\n`,
|
|
48
|
+
)
|
|
49
|
+
return path
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('applyHarnessUpdate', () => {
|
|
53
|
+
let tempRoot: string
|
|
54
|
+
let targetDir: string
|
|
55
|
+
let stagingDir: string
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
tempRoot = mkdtempSync(join(tmpdir(), 'agentic-ownership-'))
|
|
59
|
+
targetDir = join(tempRoot, 'app')
|
|
60
|
+
stagingDir = join(tempRoot, 'candidate')
|
|
61
|
+
mkdirSync(targetDir, { recursive: true })
|
|
62
|
+
mkdirSync(stagingDir, { recursive: true })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
rmSync(tempRoot, { recursive: true, force: true })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('updates unchanged owned files while preserving modified and unknown files', () => {
|
|
70
|
+
const oldUnchanged = 'old generated content\n'
|
|
71
|
+
const oldExpectedModified = 'old generated editable content\n'
|
|
72
|
+
const userModified = 'user customization\n'
|
|
73
|
+
const unknownUserContent = 'private local skill\n'
|
|
74
|
+
const oldMissing = 'previously generated then deleted\n'
|
|
75
|
+
|
|
76
|
+
write(join(targetDir, '.ai', 'owned-unchanged.md'), oldUnchanged)
|
|
77
|
+
write(join(targetDir, '.ai', 'owned-modified.md'), userModified)
|
|
78
|
+
write(join(targetDir, '.ai', 'unknown-collision.md'), unknownUserContent)
|
|
79
|
+
write(join(targetDir, '.ai', 'skills', 'custom-local', 'SKILL.md'), 'keep me\n')
|
|
80
|
+
writeManifest(targetDir, [
|
|
81
|
+
entry('.ai/owned-unchanged.md', oldUnchanged),
|
|
82
|
+
entry('.ai/owned-modified.md', oldExpectedModified),
|
|
83
|
+
entry('.ai/missing-owned.md', oldMissing),
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
const nextFiles = new Map<string, string>([
|
|
87
|
+
['.ai/owned-unchanged.md', 'new generated content\n'],
|
|
88
|
+
['.ai/owned-modified.md', 'new generated editable content\n'],
|
|
89
|
+
['.ai/unknown-collision.md', 'new generated collision\n'],
|
|
90
|
+
['.ai/missing-owned.md', 'recreated generated content\n'],
|
|
91
|
+
])
|
|
92
|
+
for (const [relativePath, content] of nextFiles) {
|
|
93
|
+
write(join(stagingDir, relativePath), content)
|
|
94
|
+
}
|
|
95
|
+
writeManifest(
|
|
96
|
+
stagingDir,
|
|
97
|
+
[...nextFiles].map(([relativePath, content]) => entry(relativePath, content)),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const conflicts = applyHarnessUpdate(targetDir, stagingDir)
|
|
101
|
+
|
|
102
|
+
expect(conflicts).toEqual([
|
|
103
|
+
{ path: '.ai/owned-modified.md', candidate: '.ai/owned-modified.md.incoming' },
|
|
104
|
+
{ path: '.ai/unknown-collision.md', candidate: '.ai/unknown-collision.md.incoming' },
|
|
105
|
+
])
|
|
106
|
+
expect(readFileSync(join(targetDir, '.ai', 'owned-unchanged.md'), 'utf8')).toBe(
|
|
107
|
+
nextFiles.get('.ai/owned-unchanged.md'),
|
|
108
|
+
)
|
|
109
|
+
expect(readFileSync(join(targetDir, '.ai', 'owned-modified.md'), 'utf8')).toBe(userModified)
|
|
110
|
+
expect(readFileSync(join(targetDir, '.ai', 'owned-modified.md.incoming'), 'utf8')).toBe(
|
|
111
|
+
nextFiles.get('.ai/owned-modified.md'),
|
|
112
|
+
)
|
|
113
|
+
expect(readFileSync(join(targetDir, '.ai', 'unknown-collision.md'), 'utf8')).toBe(
|
|
114
|
+
unknownUserContent,
|
|
115
|
+
)
|
|
116
|
+
expect(readFileSync(join(targetDir, '.ai', 'unknown-collision.md.incoming'), 'utf8')).toBe(
|
|
117
|
+
nextFiles.get('.ai/unknown-collision.md'),
|
|
118
|
+
)
|
|
119
|
+
expect(readFileSync(join(targetDir, '.ai', 'missing-owned.md'), 'utf8')).toBe(
|
|
120
|
+
nextFiles.get('.ai/missing-owned.md'),
|
|
121
|
+
)
|
|
122
|
+
expect(readFileSync(join(targetDir, '.ai', 'skills', 'custom-local', 'SKILL.md'), 'utf8')).toBe(
|
|
123
|
+
'keep me\n',
|
|
124
|
+
)
|
|
125
|
+
expect(readFileSync(join(targetDir, '.ai', 'harness', 'manifest.json'), 'utf8')).toBe(
|
|
126
|
+
readFileSync(join(stagingDir, '.ai', 'harness', 'manifest.json'), 'utf8'),
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('never overwrites a user-edited incoming candidate on a later update', () => {
|
|
131
|
+
const userFile = 'user-owned harness\n'
|
|
132
|
+
const firstCandidate = 'first generated candidate\n'
|
|
133
|
+
const secondCandidate = 'second generated candidate\n'
|
|
134
|
+
write(join(targetDir, '.ai', 'owned.md'), userFile)
|
|
135
|
+
writeManifest(targetDir, [entry('.ai/owned.md', 'original generated file\n')])
|
|
136
|
+
write(join(stagingDir, '.ai', 'owned.md'), firstCandidate)
|
|
137
|
+
writeManifest(stagingDir, [entry('.ai/owned.md', firstCandidate)])
|
|
138
|
+
|
|
139
|
+
expect(applyHarnessUpdate(targetDir, stagingDir)).toEqual([
|
|
140
|
+
{ path: '.ai/owned.md', candidate: '.ai/owned.md.incoming' },
|
|
141
|
+
])
|
|
142
|
+
write(join(targetDir, '.ai', 'owned.md.incoming'), 'user edited the candidate\n')
|
|
143
|
+
write(join(stagingDir, '.ai', 'owned.md'), secondCandidate)
|
|
144
|
+
writeManifest(stagingDir, [entry('.ai/owned.md', secondCandidate)])
|
|
145
|
+
|
|
146
|
+
expect(applyHarnessUpdate(targetDir, stagingDir)).toEqual([
|
|
147
|
+
{ path: '.ai/owned.md', candidate: '.ai/owned.md.incoming.2' },
|
|
148
|
+
])
|
|
149
|
+
expect(readFileSync(join(targetDir, '.ai', 'owned.md'), 'utf8')).toBe(userFile)
|
|
150
|
+
expect(readFileSync(join(targetDir, '.ai', 'owned.md.incoming'), 'utf8')).toBe('user edited the candidate\n')
|
|
151
|
+
expect(readFileSync(join(targetDir, '.ai', 'owned.md.incoming.2'), 'utf8')).toBe(secondCandidate)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('removes retired owned files but preserves modified retired assets with ownership', () => {
|
|
155
|
+
const unchanged = '.claude/generated.json'
|
|
156
|
+
const modified = 'CLAUDE.md'
|
|
157
|
+
write(join(targetDir, unchanged), 'old generated setting\n')
|
|
158
|
+
write(join(targetDir, modified), 'locally edited instructions\n')
|
|
159
|
+
writeManifest(targetDir, [
|
|
160
|
+
entry(unchanged, 'old generated setting\n'),
|
|
161
|
+
entry(modified, 'old generated instructions\n'),
|
|
162
|
+
])
|
|
163
|
+
writeManifest(stagingDir, [])
|
|
164
|
+
|
|
165
|
+
expect(applyHarnessUpdate(targetDir, stagingDir)).toEqual([
|
|
166
|
+
{ path: modified, candidate: null },
|
|
167
|
+
])
|
|
168
|
+
expect(existsSync(join(targetDir, unchanged))).toBe(false)
|
|
169
|
+
expect(readFileSync(join(targetDir, modified), 'utf8')).toBe('locally edited instructions\n')
|
|
170
|
+
const manifest = JSON.parse(readFileSync(join(targetDir, '.ai', 'harness', 'manifest.json'), 'utf8')) as {
|
|
171
|
+
files: ManifestEntry[]
|
|
172
|
+
}
|
|
173
|
+
expect(manifest.files.map((item) => item.path)).toEqual([modified])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('leaves the prior manifest and app files untouched when candidate validation fails', () => {
|
|
177
|
+
write(join(targetDir, '.ai', 'owned.md'), 'old\n')
|
|
178
|
+
const oldManifestPath = writeManifest(targetDir, [entry('.ai/owned.md', 'old\n')])
|
|
179
|
+
const oldManifest = readFileSync(oldManifestPath, 'utf8')
|
|
180
|
+
writeManifest(stagingDir, [entry('.ai/missing-candidate.md', 'not written\n')])
|
|
181
|
+
|
|
182
|
+
expect(() => applyHarnessUpdate(targetDir, stagingDir)).toThrow(
|
|
183
|
+
'Generated harness candidate is invalid for ".ai/missing-candidate.md".',
|
|
184
|
+
)
|
|
185
|
+
expect(readFileSync(join(targetDir, '.ai', 'owned.md'), 'utf8')).toBe('old\n')
|
|
186
|
+
expect(readFileSync(oldManifestPath, 'utf8')).toBe(oldManifest)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('rejects manifest paths that escape the app root before publishing', () => {
|
|
190
|
+
const oldManifestPath = writeManifest(targetDir, [])
|
|
191
|
+
const oldManifest = readFileSync(oldManifestPath, 'utf8')
|
|
192
|
+
writeManifest(stagingDir, [entry('../escape.md', 'escape\n')])
|
|
193
|
+
|
|
194
|
+
expect(() => applyHarnessUpdate(targetDir, stagingDir)).toThrow(
|
|
195
|
+
'Generated harness candidate is invalid for "../escape.md".',
|
|
196
|
+
)
|
|
197
|
+
expect(readFileSync(oldManifestPath, 'utf8')).toBe(oldManifest)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('rejects a symlinked managed ancestor without writing outside the app', () => {
|
|
201
|
+
if (process.platform === 'win32') return
|
|
202
|
+
const outsideDir = join(tempRoot, 'outside')
|
|
203
|
+
mkdirSync(join(outsideDir, 'harness'), { recursive: true })
|
|
204
|
+
write(join(outsideDir, 'sentinel.txt'), 'outside sentinel\n')
|
|
205
|
+
write(
|
|
206
|
+
join(outsideDir, 'harness', 'manifest.json'),
|
|
207
|
+
`${JSON.stringify({ version: 1, generator: 'outside', files: [] }, null, 2)}\n`,
|
|
208
|
+
)
|
|
209
|
+
const nextContent = 'must stay inside the app\n'
|
|
210
|
+
write(join(stagingDir, '.ai', 'owned.md'), nextContent)
|
|
211
|
+
writeManifest(stagingDir, [entry('.ai/owned.md', nextContent)])
|
|
212
|
+
symlinkSync(outsideDir, join(targetDir, '.ai'), 'dir')
|
|
213
|
+
|
|
214
|
+
expect(() => applyHarnessUpdate(targetDir, stagingDir)).toThrow(
|
|
215
|
+
'Harness-managed path contains a symbolic-link component',
|
|
216
|
+
)
|
|
217
|
+
expect(readFileSync(join(outsideDir, 'sentinel.txt'), 'utf8')).toBe('outside sentinel\n')
|
|
218
|
+
expect(existsSync(join(outsideDir, 'owned.md'))).toBe(false)
|
|
219
|
+
expect(readdirSync(outsideDir).sort()).toEqual(['harness', 'sentinel.txt'])
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('runAgenticSetup ownership modes', () => {
|
|
224
|
+
let appDir: string
|
|
225
|
+
let previousSkipExternal: string | undefined
|
|
226
|
+
|
|
227
|
+
beforeEach(() => {
|
|
228
|
+
appDir = mkdtempSync(join(tmpdir(), 'agentic-setup-app-'))
|
|
229
|
+
write(
|
|
230
|
+
join(appDir, 'src', 'modules.ts'),
|
|
231
|
+
"export const enabledModules: Array<{ id: string }> = []\n",
|
|
232
|
+
)
|
|
233
|
+
previousSkipExternal = process.env.OM_SKIP_EXTERNAL_SKILLS
|
|
234
|
+
process.env.OM_SKIP_EXTERNAL_SKILLS = '1'
|
|
235
|
+
jest.spyOn(console, 'log').mockImplementation()
|
|
236
|
+
jest.spyOn(console, 'warn').mockImplementation()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
afterEach(() => {
|
|
240
|
+
if (previousSkipExternal === undefined) delete process.env.OM_SKIP_EXTERNAL_SKILLS
|
|
241
|
+
else process.env.OM_SKIP_EXTERNAL_SKILLS = previousSkipExternal
|
|
242
|
+
jest.restoreAllMocks()
|
|
243
|
+
rmSync(appDir, { recursive: true, force: true })
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('preserves local edits, recreates missing owned files, and reports incoming candidates', async () => {
|
|
247
|
+
const ask = async () => ''
|
|
248
|
+
await runAgenticSetup(appDir, ask, { tool: 'codex' })
|
|
249
|
+
const originalGuide = readFileSync(join(appDir, '.ai', 'guides', 'architecture.md'), 'utf8')
|
|
250
|
+
write(join(appDir, 'AGENTS.md'), '# My local harness rules\n')
|
|
251
|
+
write(join(appDir, '.ai', 'skills', 'my-private-skill', 'SKILL.md'), '# Private\n')
|
|
252
|
+
rmSync(join(appDir, '.ai', 'guides', 'architecture.md'))
|
|
253
|
+
|
|
254
|
+
await runAgenticSetup(appDir, ask, { tool: 'codex', updateHarness: true })
|
|
255
|
+
|
|
256
|
+
expect(readFileSync(join(appDir, 'AGENTS.md'), 'utf8')).toBe('# My local harness rules\n')
|
|
257
|
+
expect(readFileSync(join(appDir, 'AGENTS.md.incoming'), 'utf8')).toContain(
|
|
258
|
+
'<!-- CODEX_ENFORCEMENT_RULES_START -->',
|
|
259
|
+
)
|
|
260
|
+
expect(readFileSync(join(appDir, '.ai', 'guides', 'architecture.md'), 'utf8')).toBe(originalGuide)
|
|
261
|
+
expect(readFileSync(join(appDir, '.ai', 'skills', 'my-private-skill', 'SKILL.md'), 'utf8')).toBe(
|
|
262
|
+
'# Private\n',
|
|
263
|
+
)
|
|
264
|
+
expect(existsSync(join(appDir, '.ai', 'harness', 'manifest.json'))).toBe(true)
|
|
265
|
+
if (process.platform !== 'win32') {
|
|
266
|
+
expect(statSync(join(appDir, 'scripts', 'install-skills.sh')).mode & 0o111).not.toBe(0)
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('keeps force semantics by replacing exact generated targets', async () => {
|
|
271
|
+
const ask = async () => ''
|
|
272
|
+
await runAgenticSetup(appDir, ask, { tool: 'codex' })
|
|
273
|
+
write(join(appDir, 'AGENTS.md'), '# My local harness rules\n')
|
|
274
|
+
|
|
275
|
+
await runAgenticSetup(appDir, ask, { tool: 'codex', force: true })
|
|
276
|
+
|
|
277
|
+
expect(readFileSync(join(appDir, 'AGENTS.md'), 'utf8')).toContain(
|
|
278
|
+
'<!-- CODEX_ENFORCEMENT_RULES_START -->',
|
|
279
|
+
)
|
|
280
|
+
expect(existsSync(join(appDir, 'AGENTS.md.incoming'))).toBe(false)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it.each([
|
|
284
|
+
['top-level .ai', '.ai'],
|
|
285
|
+
['top-level scripts', 'scripts'],
|
|
286
|
+
['nested guides', join('.ai', 'guides')],
|
|
287
|
+
])('rejects a symlinked %s destination on initial setup before writing outside the app', async (_label, linkedRelative) => {
|
|
288
|
+
if (process.platform === 'win32') return
|
|
289
|
+
const targetDir = join(appDir, linkedRelative.replaceAll('/', '-'))
|
|
290
|
+
const outsideDir = join(appDir, `outside-initial-${linkedRelative.replaceAll('/', '-')}`)
|
|
291
|
+
write(
|
|
292
|
+
join(targetDir, 'src', 'modules.ts'),
|
|
293
|
+
"export const enabledModules: Array<{ id: string }> = []\n",
|
|
294
|
+
)
|
|
295
|
+
write(join(outsideDir, 'sentinel.txt'), 'outside sentinel\n')
|
|
296
|
+
const linkedPath = join(targetDir, linkedRelative)
|
|
297
|
+
mkdirSync(dirname(linkedPath), { recursive: true })
|
|
298
|
+
symlinkSync(outsideDir, linkedPath, 'dir')
|
|
299
|
+
|
|
300
|
+
await expect(runAgenticSetup(targetDir, async () => '', { tool: 'codex' })).rejects.toThrow(
|
|
301
|
+
'Harness-managed path contains a symbolic-link component',
|
|
302
|
+
)
|
|
303
|
+
expect(readFileSync(join(outsideDir, 'sentinel.txt'), 'utf8')).toBe('outside sentinel\n')
|
|
304
|
+
expect(readdirSync(outsideDir)).toEqual(['sentinel.txt'])
|
|
305
|
+
expect(existsSync(join(targetDir, 'AGENTS.md'))).toBe(false)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it.each([
|
|
309
|
+
['top-level .ai', '.ai'],
|
|
310
|
+
['top-level scripts', 'scripts'],
|
|
311
|
+
['nested guides', join('.ai', 'guides')],
|
|
312
|
+
])('rejects a symlinked %s destination on forced setup before writing outside the app', async (_label, linkedRelative) => {
|
|
313
|
+
if (process.platform === 'win32') return
|
|
314
|
+
const targetDir = join(appDir, `force-${linkedRelative.replaceAll('/', '-')}`)
|
|
315
|
+
const outsideDir = join(appDir, `outside-force-${linkedRelative.replaceAll('/', '-')}`)
|
|
316
|
+
write(
|
|
317
|
+
join(targetDir, 'src', 'modules.ts'),
|
|
318
|
+
"export const enabledModules: Array<{ id: string }> = []\n",
|
|
319
|
+
)
|
|
320
|
+
await runAgenticSetup(targetDir, async () => '', { tool: 'codex' })
|
|
321
|
+
const originalAgents = readFileSync(join(targetDir, 'AGENTS.md'), 'utf8')
|
|
322
|
+
write(join(outsideDir, 'sentinel.txt'), 'outside sentinel\n')
|
|
323
|
+
const linkedPath = join(targetDir, linkedRelative)
|
|
324
|
+
rmSync(linkedPath, { recursive: true, force: true })
|
|
325
|
+
mkdirSync(dirname(linkedPath), { recursive: true })
|
|
326
|
+
symlinkSync(outsideDir, linkedPath, 'dir')
|
|
327
|
+
|
|
328
|
+
await expect(
|
|
329
|
+
runAgenticSetup(targetDir, async () => '', { tool: 'codex', force: true }),
|
|
330
|
+
).rejects.toThrow('Harness-managed path contains a symbolic-link component')
|
|
331
|
+
expect(readFileSync(join(outsideDir, 'sentinel.txt'), 'utf8')).toBe('outside sentinel\n')
|
|
332
|
+
expect(readdirSync(outsideDir)).toEqual(['sentinel.txt'])
|
|
333
|
+
expect(readFileSync(join(targetDir, 'AGENTS.md'), 'utf8')).toBe(originalAgents)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('updates the harness without crashing when src/modules.ts is missing', async () => {
|
|
337
|
+
rmSync(join(appDir, 'src', 'modules.ts'))
|
|
338
|
+
|
|
339
|
+
await expect(runAgenticSetup(appDir, async () => '', {
|
|
340
|
+
tool: 'codex',
|
|
341
|
+
updateHarness: true,
|
|
342
|
+
})).resolves.toBeUndefined()
|
|
343
|
+
|
|
344
|
+
expect(existsSync(join(appDir, 'src', 'modules.ts'))).toBe(false)
|
|
345
|
+
expect(existsSync(join(appDir, '.ai', 'harness', 'manifest.json'))).toBe(true)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('removes unmodified tool-specific assets when switching tools', async () => {
|
|
349
|
+
const ask = async () => ''
|
|
350
|
+
await runAgenticSetup(appDir, ask, { tool: 'claude-code' })
|
|
351
|
+
expect(existsSync(join(appDir, 'CLAUDE.md'))).toBe(true)
|
|
352
|
+
expect(existsSync(join(appDir, '.claude', 'settings.json'))).toBe(true)
|
|
353
|
+
|
|
354
|
+
await runAgenticSetup(appDir, ask, { tool: 'codex', updateHarness: true })
|
|
355
|
+
|
|
356
|
+
expect(existsSync(join(appDir, 'CLAUDE.md'))).toBe(false)
|
|
357
|
+
expect(existsSync(join(appDir, '.claude', 'settings.json'))).toBe(false)
|
|
358
|
+
expect(existsSync(join(appDir, '.codex', 'mcp.json.example'))).toBe(true)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('preserves the persisted tool selection on a bare harness update without prompting', async () => {
|
|
362
|
+
const initialAsk = async () => ''
|
|
363
|
+
await runAgenticSetup(appDir, initialAsk, { tool: 'claude-code,codex' })
|
|
364
|
+
const ask = jest.fn(async () => {
|
|
365
|
+
throw new Error('bare update must not prompt for a new tool selection')
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
await runAgenticSetup(appDir, ask, { updateHarness: true })
|
|
369
|
+
|
|
370
|
+
expect(ask).not.toHaveBeenCalled()
|
|
371
|
+
expect(existsSync(join(appDir, 'CLAUDE.md'))).toBe(true)
|
|
372
|
+
expect(existsSync(join(appDir, '.claude', 'settings.json'))).toBe(true)
|
|
373
|
+
expect(existsSync(join(appDir, '.codex', 'mcp.json.example'))).toBe(true)
|
|
374
|
+
expect(existsSync(join(appDir, '.cursor', 'hooks.json'))).toBe(false)
|
|
375
|
+
const tiers = JSON.parse(readFileSync(join(appDir, '.ai', 'skills', 'tiers.json'), 'utf8')) as {
|
|
376
|
+
agents?: { ignore?: string[] }
|
|
377
|
+
}
|
|
378
|
+
expect(tiers.agents?.ignore).toEqual(['cursor'])
|
|
379
|
+
})
|
|
380
|
+
})
|
package/src/lib/agentic-init.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { createInterface } from 'node:readline'
|
|
|
5
5
|
interface AgenticInitOptions {
|
|
6
6
|
tool?: string
|
|
7
7
|
force?: boolean
|
|
8
|
+
updateHarness?: boolean
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
const TOOL_EXISTING_FILES: Record<string, string[]> = {
|
|
@@ -27,6 +28,8 @@ function parseArgs(args: string[]): AgenticInitOptions {
|
|
|
27
28
|
const arg = args[i]
|
|
28
29
|
if (arg === '--force' || arg === '-f') {
|
|
29
30
|
options.force = true
|
|
31
|
+
} else if (arg === '--update-harness') {
|
|
32
|
+
options.updateHarness = true
|
|
30
33
|
} else if (arg.startsWith('--tool=')) {
|
|
31
34
|
options.tool = arg.slice('--tool='.length)
|
|
32
35
|
} else if (arg === '--tool') {
|
|
@@ -59,6 +62,11 @@ export async function runAgenticInit(args: string[]): Promise<number> {
|
|
|
59
62
|
const targetDir = resolve('.')
|
|
60
63
|
const options = parseArgs(args)
|
|
61
64
|
|
|
65
|
+
if (options.force && options.updateHarness) {
|
|
66
|
+
console.error('❌ --force and --update-harness are mutually exclusive')
|
|
67
|
+
return 1
|
|
68
|
+
}
|
|
69
|
+
|
|
62
70
|
// Validate this is an Open Mercato app directory
|
|
63
71
|
if (!existsSync(join(targetDir, 'src', 'modules.ts'))) {
|
|
64
72
|
console.error('❌ Not an Open Mercato app directory (src/modules.ts not found)')
|
|
@@ -66,7 +74,7 @@ export async function runAgenticInit(args: string[]): Promise<number> {
|
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
// Check if agentic files already exist and warn unless --force
|
|
69
|
-
if (!options.force) {
|
|
77
|
+
if (!options.force && !options.updateHarness) {
|
|
70
78
|
const existingFiles = resolveRelevantAgenticFiles(options.tool)
|
|
71
79
|
.filter((relativePath) => existsSync(join(targetDir, relativePath)))
|
|
72
80
|
|
|
@@ -77,7 +85,7 @@ export async function runAgenticInit(args: string[]): Promise<number> {
|
|
|
77
85
|
console.log(` • ${f}`)
|
|
78
86
|
}
|
|
79
87
|
console.log('')
|
|
80
|
-
console.log('Run with --force to
|
|
88
|
+
console.log('Run with --update-harness to preserve local edits, or --force to replace generated files.')
|
|
81
89
|
console.log('')
|
|
82
90
|
return 0
|
|
83
91
|
}
|
|
@@ -92,7 +100,11 @@ export async function runAgenticInit(args: string[]): Promise<number> {
|
|
|
92
100
|
const ask = (q: string) => new Promise<string>((res) => rl.question(q, (a) => res(a.trim())))
|
|
93
101
|
|
|
94
102
|
try {
|
|
95
|
-
await runAgenticSetup(targetDir, ask, {
|
|
103
|
+
await runAgenticSetup(targetDir, ask, {
|
|
104
|
+
tool: options.tool,
|
|
105
|
+
force: options.force,
|
|
106
|
+
updateHarness: options.updateHarness,
|
|
107
|
+
})
|
|
96
108
|
} finally {
|
|
97
109
|
rl.close()
|
|
98
110
|
}
|