@abarcenas/ai-workflow-template 1.46.0 → 1.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/.agents/config/README.md +2 -2
  2. package/.agents/instructions/agent.instructions.md +16 -16
  3. package/.agents/instructions/context7.instructions.md +40 -0
  4. package/.agents/instructions/learned-knowledge.instructions.md +18 -0
  5. package/.agents/instructions/memory-bank.instructions.md +3 -3
  6. package/.opencode/agents/{architect.agent.md → architect.md} +5 -6
  7. package/.opencode/agents/{coder.agent.md → coder.md} +4 -8
  8. package/.opencode/agents/{deployer.agent.md → deployer.md} +4 -9
  9. package/.opencode/agents/{designer.agent.md → designer.md} +5 -6
  10. package/.opencode/agents/{e2e-tester.agent.md → e2e-tester.md} +4 -7
  11. package/.opencode/agents/{implementer.agent.md → implementer.md} +4 -8
  12. package/.opencode/agents/orchestrator/{feature-pipeline.agent.md → feature-pipeline.md} +16 -19
  13. package/.opencode/agents/orchestrator/{orchestrator.agent.md → orchestrator.md} +26 -34
  14. package/.opencode/agents/orchestrator/{tdd-orchestrator.agent.md → tdd-orchestrator.md} +26 -29
  15. package/.opencode/agents/{plan.agent.md → planner.md} +4 -9
  16. package/.opencode/agents/{researcher.agent.md → researcher.md} +5 -9
  17. package/.opencode/agents/{reviewer.agent.md → reviewer.md} +5 -8
  18. package/.opencode/agents/{scope.agent.md → scope.md} +5 -9
  19. package/.opencode/agents/{tracker.agent.md → tracker.md} +5 -9
  20. package/.opencode/agents/{unit-tester.agent.md → unit-tester.md} +4 -8
  21. package/.prettierignore +26 -0
  22. package/AGENTS.md +10 -6
  23. package/README.md +36 -12
  24. package/opencode.mcp.example.json +13 -12
  25. package/package.json +3 -1
  26. package/scripts/sync-unit.test.js +551 -12
  27. package/scripts/sync.js +296 -13
@@ -9,6 +9,7 @@
9
9
  // ═════════════════════════════════════════════════════════════════════════════
10
10
 
11
11
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
12
+ import { spawnSync } from 'child_process'
12
13
  import { createHash } from 'crypto'
13
14
  import {
14
15
  chmodSync,
@@ -21,7 +22,7 @@ import {
21
22
  symlinkSync,
22
23
  writeFileSync,
23
24
  } from 'fs'
24
- import { dirname, join } from 'path'
25
+ import { dirname, join, resolve } from 'path'
25
26
  import { tmpdir } from 'os'
26
27
  import { fileURLToPath } from 'url'
27
28
 
@@ -35,13 +36,22 @@ import {
35
36
  isRegularFile,
36
37
  isUntouched,
37
38
  memoryBankStubs,
39
+ missingPrettierIgnorePatterns,
38
40
  parseArgs,
41
+ parsePrettierIgnoreRules,
42
+ projectOwnedPaths,
43
+ projectOwnedStubs,
39
44
  readManifest,
45
+ requiredPrettierIgnorePatterns,
40
46
  resolvePruneEntry,
47
+ rootFiles,
41
48
  runSync,
49
+ scriptsToSync,
42
50
  writeManifest,
43
51
  } from './sync.js'
44
52
 
53
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
54
+
45
55
  const tempDirs = []
46
56
 
47
57
  function createTempDir() {
@@ -588,22 +598,32 @@ describe('resolvePruneEntry', () => {
588
598
  )
589
599
  })
590
600
 
591
- it('keeps opencode.json alive via the example fallback source', () => {
601
+ it('never resolves project-owned files as prune entries', () => {
592
602
  const ctx = context()
593
603
 
594
- const entry = resolvePruneEntry('__root__/opencode.json', ctx)
595
-
596
- expect(entry.packagePaths).toEqual([
597
- join(ctx.packageRoot, 'opencode.json'),
598
- join(ctx.packageRoot, 'opencode.mcp.example.json'),
599
- ])
604
+ for (const key of [
605
+ '__root__/AGENTS.project.md',
606
+ '__root__/opencode.json',
607
+ '__root__/.prettierignore',
608
+ '__root__/.agents-sync-ignore',
609
+ 'instructions/learned-knowledge.instructions.md',
610
+ '__project__/AGENTS.project.md',
611
+ '__project__/.prettierignore',
612
+ ]) {
613
+ expect(resolvePruneEntry(key, ctx), key).toBeNull()
614
+ }
600
615
  })
601
616
 
602
617
  it.each([
603
618
  ['unknown internal namespace', '__future__/thing.md'],
619
+ ['scaffold-only project namespace', '__project__/AGENTS.project.md'],
604
620
  ['scaffold-only memory-bank namespace', '__memory-bank__/projectbrief.md'],
605
621
  ['scaffold-only instructions namespace', '__agents-instructions__/x.md'],
606
622
  ['excluded learned-knowledge file', 'instructions/learned-knowledge.instructions.md'],
623
+ ['project-owned root AGENTS.project.md', '__root__/AGENTS.project.md'],
624
+ ['project-owned root opencode.json', '__root__/opencode.json'],
625
+ ['project-owned root .prettierignore', '__root__/.prettierignore'],
626
+ ['project-owned root .agents-sync-ignore', '__root__/.agents-sync-ignore'],
607
627
  ['excluded opencode node_modules path', '__opencode__/node_modules/dep.js'],
608
628
  ['excluded opencode package.json', '__opencode__/package.json'],
609
629
  ['empty opencode key', '__opencode__/'],
@@ -664,7 +684,7 @@ describe('runSync copy passes', () => {
664
684
  expect(result.exitCode).toBe(0)
665
685
  expect(result.copied).toBe(9)
666
686
  expect(result.added).toBe(9)
667
- expect(result.scaffolded).toBe(8)
687
+ expect(result.scaffolded).toBe(10)
668
688
  expect(result.skipped).toBe(0)
669
689
  expect(errorSpy).not.toHaveBeenCalled()
670
690
 
@@ -757,12 +777,21 @@ describe('runSync copy passes', () => {
757
777
  expect(manifest.files['__root__/AGENTS.md']).toBe(
758
778
  sha256(ROOT_FILE_CONTENTS['AGENTS.md']),
759
779
  )
760
- expect(manifest.files['__root__/opencode.json']).toBe(sha256(MCP_EXAMPLE))
780
+ expect(manifest.files['__project__/opencode.json']).toBe(sha256(MCP_EXAMPLE))
781
+ expect(manifest.files['__root__/opencode.json']).toBeUndefined()
761
782
  expect(
762
783
  manifest.files['__agents-instructions__/learned-knowledge.instructions.md'],
763
784
  ).toBe(
764
785
  sha256(agentInstructionsStubs['learned-knowledge.instructions.md']),
765
786
  )
787
+ expect(manifest.files['__project__/AGENTS.project.md']).toBe(
788
+ sha256(projectOwnedStubs['AGENTS.project.md']),
789
+ )
790
+ expect(manifest.files['__project__/.prettierignore']).toBe(
791
+ sha256(projectOwnedStubs['.prettierignore']),
792
+ )
793
+ expect(manifest.files['__root__/AGENTS.project.md']).toBeUndefined()
794
+ expect(manifest.files['__root__/.prettierignore']).toBeUndefined()
766
795
  for (const fileName of Object.keys(memoryBankStubs)) {
767
796
  expect(manifest.files[`__memory-bank__/${fileName}`]).toBeDefined()
768
797
  }
@@ -866,7 +895,7 @@ describe('runSync copy passes', () => {
866
895
  expect(outputOf(logSpy)).toContain('Mode: force.')
867
896
  })
868
897
 
869
- it('prefers a package-shipped opencode.json over the example file', () => {
898
+ it('never adopts a package-shipped opencode.json: the example scaffold wins', () => {
870
899
  const packageRoot = createPackage({
871
900
  agents: {},
872
901
  root: { 'opencode.json': '{ "mcp": { "from": "package" } }\n' },
@@ -876,8 +905,10 @@ describe('runSync copy passes', () => {
876
905
 
877
906
  runSync({ packageRoot, consumerRoot })
878
907
 
908
+ // opencode.json is project-owned, so the root copy pass skips it and the
909
+ // create-if-absent scaffold from the shipped example is what lands.
879
910
  expect(readFileSync(join(consumerRoot, 'opencode.json'), 'utf8')).toBe(
880
- '{ "mcp": { "from": "package" } }\n',
911
+ MCP_EXAMPLE,
881
912
  )
882
913
  })
883
914
 
@@ -982,6 +1013,514 @@ describe('runSync copy passes', () => {
982
1013
  })
983
1014
  })
984
1015
 
1016
+ // ── Project-owned files ──────────────────────────────────────────────────────
1017
+
1018
+ const PROJECT_OWNED_LOCAL_CONTENT = {
1019
+ 'AGENTS.project.md': '# Local project rules\n\n- never overwrite this',
1020
+ 'opencode.json': '{ "local": true }\n',
1021
+ '.prettierignore': 'my-build/\n',
1022
+ '.agents-sync-ignore': '# local ignore rules\n',
1023
+ '.agents/instructions/learned-knowledge.instructions.md': '# Local knowledge\n',
1024
+ }
1025
+
1026
+ const LEGACY_PRETTIER_IGNORE =
1027
+ '/dist\n/coverage\n/.nx/cache\n/.nx/workspace-data\n'
1028
+
1029
+ const PRETTIER_IGNORE_WARNING =
1030
+ '[ai-workflow-template] Existing .prettierignore does not protect managed files; merge the patterns from node_modules/@abarcenas/ai-workflow-template/.prettierignore'
1031
+
1032
+ const COMPLETE_PRETTIER_IGNORE = [
1033
+ '.agents/',
1034
+ '.opencode/',
1035
+ 'scripts/',
1036
+ 'AGENTS.md',
1037
+ 'skills-lock.json',
1038
+ ].join('\n')
1039
+
1040
+ // B1 coverage matrix: parent-directory coverage, negation holes, strict
1041
+ // descendants, comments, inline literals, normalisation and ignore-all forms.
1042
+ const PRETTIER_IGNORE_MATRIX = [
1043
+ ['exact shipped content', projectOwnedStubs['.prettierignore'], false],
1044
+ ['parent directories cover descendants', COMPLETE_PRETTIER_IGNORE, false],
1045
+ [
1046
+ 'shipped without AGENTS.md',
1047
+ projectOwnedStubs['.prettierignore'].replace('\nAGENTS.md\n', '\n'),
1048
+ true,
1049
+ ],
1050
+ [
1051
+ 'negation hole below .agents/',
1052
+ [
1053
+ '.agents/',
1054
+ '!.agents/skills/**',
1055
+ '.opencode/',
1056
+ 'scripts/',
1057
+ 'AGENTS.md',
1058
+ 'skills-lock.json',
1059
+ ].join('\n'),
1060
+ true,
1061
+ ],
1062
+ [
1063
+ 'later positive restores a negated subtree',
1064
+ [
1065
+ '.agents/',
1066
+ '!.agents/skills/**',
1067
+ '.agents/skills/',
1068
+ '.opencode/',
1069
+ 'scripts/',
1070
+ 'AGENTS.md',
1071
+ 'skills-lock.json',
1072
+ ].join('\n'),
1073
+ false,
1074
+ ],
1075
+ [
1076
+ 'AGENTS.md/** does not cover the file',
1077
+ COMPLETE_PRETTIER_IGNORE.replace('AGENTS.md', 'AGENTS.md/**'),
1078
+ true,
1079
+ ],
1080
+ [
1081
+ '.agents without a trailing slash',
1082
+ COMPLETE_PRETTIER_IGNORE.replace('.agents/', '.agents'),
1083
+ false,
1084
+ ],
1085
+ [
1086
+ 'commented-out .agents/',
1087
+ COMPLETE_PRETTIER_IGNORE.replace('.agents/', '# .agents/'),
1088
+ true,
1089
+ ],
1090
+ [
1091
+ 'leading **/ wildcard',
1092
+ COMPLETE_PRETTIER_IGNORE.replace('AGENTS.md', '**/AGENTS.md'),
1093
+ false,
1094
+ ],
1095
+ [
1096
+ 'inline hash is literal',
1097
+ COMPLETE_PRETTIER_IGNORE.replace('AGENTS.md', 'AGENTS.md # managed'),
1098
+ true,
1099
+ ],
1100
+ [
1101
+ 'CRLF and trailing spaces',
1102
+ COMPLETE_PRETTIER_IGNORE.split('\n')
1103
+ .map((line) => `${line} `)
1104
+ .join('\r\n'),
1105
+ false,
1106
+ ],
1107
+ [
1108
+ 'leading-slash patterns (including /dist)',
1109
+ ['/dist', '/.agents/', '/.opencode/', '/scripts/', '/AGENTS.md', '/skills-lock.json'].join(
1110
+ '\n',
1111
+ ),
1112
+ false,
1113
+ ],
1114
+ ['* ignores everything', '*', false],
1115
+ ['** ignores everything', '**', false],
1116
+ ]
1117
+
1118
+ function createProjectOwnedFixture() {
1119
+ // The package ships competing copies at the project-owned paths. None may win.
1120
+ const packageRoot = createPackage({
1121
+ agents: {
1122
+ 'instructions/learned-knowledge.instructions.md': '# Package knowledge\n',
1123
+ },
1124
+ root: {
1125
+ 'AGENTS.project.md': '# Package rules\n',
1126
+ 'opencode.json': '{ "package": true }\n',
1127
+ '.prettierignore': 'package-build/\n',
1128
+ '.agents-sync-ignore': '# package ignore\n',
1129
+ },
1130
+ mcpExample: MCP_EXAMPLE,
1131
+ })
1132
+ const consumerRoot = createTempDir()
1133
+
1134
+ for (const [relativePath, contents] of Object.entries(
1135
+ PROJECT_OWNED_LOCAL_CONTENT,
1136
+ )) {
1137
+ writeFixtureFile(consumerRoot, relativePath, contents)
1138
+ }
1139
+
1140
+ return { packageRoot, consumerRoot }
1141
+ }
1142
+
1143
+ describe('project-owned files', () => {
1144
+ it('scaffolds AGENTS.project.md and .prettierignore create-if-absent', () => {
1145
+ const { packageRoot, consumerRoot } = createCopyFixture()
1146
+
1147
+ const result = runSync({ packageRoot, consumerRoot })
1148
+
1149
+ expect(result.scaffolded).toBe(10)
1150
+ expect(readFileSync(join(consumerRoot, 'AGENTS.project.md'), 'utf8')).toBe(
1151
+ projectOwnedStubs['AGENTS.project.md'],
1152
+ )
1153
+ expect(readFileSync(join(consumerRoot, '.prettierignore'), 'utf8')).toBe(
1154
+ projectOwnedStubs['.prettierignore'],
1155
+ )
1156
+
1157
+ const manifest = readConsumerManifest(consumerRoot)
1158
+ expect(manifest.files['__project__/AGENTS.project.md']).toBe(
1159
+ sha256(projectOwnedStubs['AGENTS.project.md']),
1160
+ )
1161
+ expect(manifest.files['__project__/.prettierignore']).toBe(
1162
+ sha256(projectOwnedStubs['.prettierignore']),
1163
+ )
1164
+ // Ownership is reported through __project__, never through a sync channel.
1165
+ expect(manifest.files['__root__/AGENTS.project.md']).toBeUndefined()
1166
+ expect(manifest.files['__root__/.prettierignore']).toBeUndefined()
1167
+ })
1168
+
1169
+ it('does not overwrite existing project-owned files, even with --force', () => {
1170
+ const { packageRoot, consumerRoot } = createProjectOwnedFixture()
1171
+
1172
+ const result = runSync({
1173
+ packageRoot,
1174
+ consumerRoot,
1175
+ argv: ['--force'],
1176
+ env: {},
1177
+ })
1178
+
1179
+ expect(result.skipped).toBe(0)
1180
+ for (const [relativePath, contents] of Object.entries(
1181
+ PROJECT_OWNED_LOCAL_CONTENT,
1182
+ )) {
1183
+ expect(
1184
+ readFileSync(join(consumerRoot, relativePath), 'utf8'),
1185
+ relativePath,
1186
+ ).toBe(contents)
1187
+ }
1188
+
1189
+ const manifest = readConsumerManifest(consumerRoot)
1190
+ for (const key of [
1191
+ '__root__/AGENTS.project.md',
1192
+ '__root__/opencode.json',
1193
+ '__root__/.prettierignore',
1194
+ '__root__/.agents-sync-ignore',
1195
+ '__project__/AGENTS.project.md',
1196
+ '__project__/.prettierignore',
1197
+ '__project__/opencode.json',
1198
+ ]) {
1199
+ expect(manifest.files[key], key).toBeUndefined()
1200
+ }
1201
+ })
1202
+
1203
+ it('preserves a local AGENTS.project.md edit byte-for-byte across a sync', () => {
1204
+ const { packageRoot, consumerRoot } = createCopyFixture()
1205
+
1206
+ runSync({ packageRoot, consumerRoot })
1207
+ const localEdit = '# Project rules\n\n- local edit, no trailing newline'
1208
+ writeFixtureFile(consumerRoot, 'AGENTS.project.md', localEdit)
1209
+
1210
+ const result = runSync({
1211
+ packageRoot,
1212
+ consumerRoot,
1213
+ argv: ['--force'],
1214
+ env: {},
1215
+ })
1216
+
1217
+ expect(result.skipped).toBe(0)
1218
+ expect(readFileSync(join(consumerRoot, 'AGENTS.project.md'), 'utf8')).toBe(
1219
+ localEdit,
1220
+ )
1221
+ })
1222
+
1223
+ it('never clobbers an existing consumer .prettierignore', () => {
1224
+ const { packageRoot, consumerRoot } = createCopyFixture()
1225
+ const localIgnore = 'coverage/\nplaywright-report/\n'
1226
+ writeFixtureFile(consumerRoot, '.prettierignore', localIgnore)
1227
+
1228
+ runSync({ packageRoot, consumerRoot })
1229
+ runSync({ packageRoot, consumerRoot, argv: ['--force'], env: {} })
1230
+
1231
+ expect(readFileSync(join(consumerRoot, '.prettierignore'), 'utf8')).toBe(
1232
+ localIgnore,
1233
+ )
1234
+ expect(
1235
+ readConsumerManifest(consumerRoot).files['__project__/.prettierignore'],
1236
+ ).toBeUndefined()
1237
+ })
1238
+
1239
+ it('warns once when an existing .prettierignore does not protect managed files', () => {
1240
+ const { packageRoot, consumerRoot } = createCopyFixture()
1241
+ writeFixtureFile(consumerRoot, '.prettierignore', LEGACY_PRETTIER_IGNORE)
1242
+
1243
+ const result = runSync({ packageRoot, consumerRoot })
1244
+
1245
+ expect(result.scaffolded).toBe(9)
1246
+ expect(warnSpy).toHaveBeenCalledTimes(1)
1247
+ expect(outputOf(warnSpy)).toBe(PRETTIER_IGNORE_WARNING)
1248
+ // Warning only: the consumer file is byte-for-byte unchanged.
1249
+ expect(readFileSync(join(consumerRoot, '.prettierignore'), 'utf8')).toBe(
1250
+ LEGACY_PRETTIER_IGNORE,
1251
+ )
1252
+ })
1253
+
1254
+ it.skipIf(process.getuid?.() === 0)(
1255
+ 'skips the advisory when .prettierignore cannot be read',
1256
+ () => {
1257
+ const { packageRoot, consumerRoot } = createCopyFixture()
1258
+ const ignorePath = writeFixtureFile(
1259
+ consumerRoot,
1260
+ '.prettierignore',
1261
+ LEGACY_PRETTIER_IGNORE,
1262
+ )
1263
+ chmodSync(ignorePath, 0o000)
1264
+
1265
+ try {
1266
+ const result = runSync({ packageRoot, consumerRoot })
1267
+
1268
+ expect(result.exitCode).toBe(0)
1269
+ expect(warnSpy).not.toHaveBeenCalled()
1270
+ expect(existsSync(join(consumerRoot, '.agents-sync-manifest.json'))).toBe(
1271
+ true,
1272
+ )
1273
+ } finally {
1274
+ chmodSync(ignorePath, 0o644)
1275
+ }
1276
+ },
1277
+ )
1278
+
1279
+ it('does not warn when an existing .prettierignore covers managed files', () => {
1280
+ const { packageRoot, consumerRoot } = createCopyFixture()
1281
+ writeFixtureFile(
1282
+ consumerRoot,
1283
+ '.prettierignore',
1284
+ projectOwnedStubs['.prettierignore'],
1285
+ )
1286
+
1287
+ runSync({ packageRoot, consumerRoot })
1288
+
1289
+ expect(warnSpy).not.toHaveBeenCalled()
1290
+ })
1291
+
1292
+ it('does not warn when .prettierignore was just scaffolded', () => {
1293
+ const { packageRoot, consumerRoot } = createCopyFixture()
1294
+
1295
+ runSync({ packageRoot, consumerRoot })
1296
+
1297
+ expect(warnSpy).not.toHaveBeenCalled()
1298
+ expect(readFileSync(join(consumerRoot, '.prettierignore'), 'utf8')).toBe(
1299
+ projectOwnedStubs['.prettierignore'],
1300
+ )
1301
+ })
1302
+
1303
+ // Pins current behaviour: scaffolding is not gated by --dry-run. Update this
1304
+ // test when scaffolding becomes dry-run aware.
1305
+ it('documents that --dry-run still scaffolds missing project-owned files', () => {
1306
+ const { packageRoot, consumerRoot } = createCopyFixture()
1307
+
1308
+ const result = runSync({
1309
+ packageRoot,
1310
+ consumerRoot,
1311
+ argv: ['--dry-run'],
1312
+ env: {},
1313
+ })
1314
+
1315
+ expect(result.scaffolded).toBe(10)
1316
+ expect(readFileSync(join(consumerRoot, 'AGENTS.project.md'), 'utf8')).toBe(
1317
+ projectOwnedStubs['AGENTS.project.md'],
1318
+ )
1319
+ expect(readFileSync(join(consumerRoot, '.prettierignore'), 'utf8')).toBe(
1320
+ projectOwnedStubs['.prettierignore'],
1321
+ )
1322
+ expect(existsSync(join(consumerRoot, '.agents-sync-manifest.json'))).toBe(
1323
+ false,
1324
+ )
1325
+ })
1326
+
1327
+ it('does not refresh __project__ hashes after a consumer edit', () => {
1328
+ const { packageRoot, consumerRoot } = createCopyFixture()
1329
+
1330
+ runSync({ packageRoot, consumerRoot })
1331
+ const originalHash = readConsumerManifest(consumerRoot).files[
1332
+ '__project__/AGENTS.project.md'
1333
+ ]
1334
+ writeFixtureFile(consumerRoot, 'AGENTS.project.md', '# edited locally\n')
1335
+
1336
+ runSync({ packageRoot, consumerRoot, argv: ['--force'], env: {} })
1337
+
1338
+ expect(
1339
+ readConsumerManifest(consumerRoot).files['__project__/AGENTS.project.md'],
1340
+ ).toBe(originalHash)
1341
+ })
1342
+
1343
+ it('does not throw when a project-owned scaffold path is a directory', () => {
1344
+ const { packageRoot, consumerRoot } = createCopyFixture()
1345
+ mkdirSync(join(consumerRoot, 'AGENTS.project.md'), { recursive: true })
1346
+ mkdirSync(join(consumerRoot, '.prettierignore'), { recursive: true })
1347
+
1348
+ const result = runSync({ packageRoot, consumerRoot })
1349
+
1350
+ expect(result.exitCode).toBe(0)
1351
+ expect(existsSync(join(consumerRoot, '.agents-sync-manifest.json'))).toBe(
1352
+ true,
1353
+ )
1354
+ expect(warnSpy).not.toHaveBeenCalled()
1355
+ })
1356
+
1357
+ it('never prunes any project-owned path, including with --prune', () => {
1358
+ const packageRoot = createPackage({ agents: {} })
1359
+ const consumerRoot = createTempDir()
1360
+
1361
+ for (const [relativePath, contents] of Object.entries(
1362
+ PROJECT_OWNED_LOCAL_CONTENT,
1363
+ )) {
1364
+ writeFixtureFile(consumerRoot, relativePath, contents)
1365
+ }
1366
+
1367
+ const trackedKeys = {
1368
+ '__root__/AGENTS.project.md': 'AGENTS.project.md',
1369
+ '__root__/opencode.json': 'opencode.json',
1370
+ '__root__/.prettierignore': '.prettierignore',
1371
+ '__root__/.agents-sync-ignore': '.agents-sync-ignore',
1372
+ 'instructions/learned-knowledge.instructions.md':
1373
+ '.agents/instructions/learned-knowledge.instructions.md',
1374
+ '__project__/AGENTS.project.md': 'AGENTS.project.md',
1375
+ '__project__/.prettierignore': '.prettierignore',
1376
+ }
1377
+ writeManifest(
1378
+ join(consumerRoot, '.agents-sync-manifest.json'),
1379
+ {
1380
+ version: 1,
1381
+ files: Object.fromEntries(
1382
+ Object.entries(trackedKeys).map(([key, relativePath]) => [
1383
+ key,
1384
+ sha256(PROJECT_OWNED_LOCAL_CONTENT[relativePath]),
1385
+ ]),
1386
+ ),
1387
+ },
1388
+ )
1389
+
1390
+ const result = runSync({
1391
+ packageRoot,
1392
+ consumerRoot,
1393
+ argv: ['--prune', '--force'],
1394
+ env: {},
1395
+ })
1396
+
1397
+ expect(result.pruned).toBe(0)
1398
+ for (const [relativePath, contents] of Object.entries(
1399
+ PROJECT_OWNED_LOCAL_CONTENT,
1400
+ )) {
1401
+ expect(
1402
+ readFileSync(join(consumerRoot, relativePath), 'utf8'),
1403
+ relativePath,
1404
+ ).toBe(contents)
1405
+ }
1406
+
1407
+ const manifest = readConsumerManifest(consumerRoot)
1408
+ for (const key of Object.keys(trackedKeys)) {
1409
+ expect(manifest.files, key).toHaveProperty([key])
1410
+ }
1411
+ })
1412
+
1413
+ it('keeps the shipped .prettierignore covering every managed root file and script', () => {
1414
+ const shipped = readFileSync(join(repoRoot, '.prettierignore'), 'utf8')
1415
+
1416
+ expect(shipped).toBe(projectOwnedStubs['.prettierignore'])
1417
+ expect(missingPrettierIgnorePatterns(shipped)).toEqual([])
1418
+
1419
+ for (const rootFile of rootFiles) {
1420
+ expect(
1421
+ missingPrettierIgnorePatterns(shipped, [rootFile]),
1422
+ rootFile,
1423
+ ).toEqual([])
1424
+ }
1425
+ for (const script of scriptsToSync) {
1426
+ const pattern = `scripts/${script}`
1427
+ expect(
1428
+ missingPrettierIgnorePatterns(shipped, [pattern]),
1429
+ pattern,
1430
+ ).toEqual([])
1431
+ }
1432
+ })
1433
+
1434
+ it.each(PRETTIER_IGNORE_MATRIX)(
1435
+ 'detects coverage correctly: %s',
1436
+ (_label, contents, expectWarning) => {
1437
+ const { packageRoot, consumerRoot } = createCopyFixture()
1438
+ writeFixtureFile(consumerRoot, '.prettierignore', contents)
1439
+
1440
+ runSync({ packageRoot, consumerRoot })
1441
+
1442
+ expect(
1443
+ missingPrettierIgnorePatterns(contents).length > 0,
1444
+ 'detector result',
1445
+ ).toBe(expectWarning)
1446
+ if (expectWarning) {
1447
+ expect(warnSpy).toHaveBeenCalledTimes(1)
1448
+ expect(outputOf(warnSpy)).toBe(PRETTIER_IGNORE_WARNING)
1449
+ } else {
1450
+ expect(warnSpy).not.toHaveBeenCalled()
1451
+ }
1452
+ },
1453
+ )
1454
+
1455
+ it('parses gitignore comments, negations and escaped markers', () => {
1456
+ expect(
1457
+ parsePrettierIgnoreRules(
1458
+ '# comment\n!AGENTS.md\n\\#literal\n\\!literal\nAGENTS.md # inline\n',
1459
+ ),
1460
+ ).toEqual([
1461
+ { negated: true, pattern: 'AGENTS.md' },
1462
+ { negated: false, pattern: '#literal' },
1463
+ { negated: false, pattern: '!literal' },
1464
+ { negated: false, pattern: 'AGENTS.md # inline' },
1465
+ ])
1466
+ })
1467
+
1468
+ it('ships an opencode example that loads AGENTS.project.md', () => {
1469
+ const example = JSON.parse(
1470
+ readFileSync(join(repoRoot, 'opencode.mcp.example.json'), 'utf8'),
1471
+ )
1472
+
1473
+ expect(example.instructions).toEqual(['AGENTS.project.md'])
1474
+ })
1475
+
1476
+ it('scaffolds opencode.json with the project instructions entry', () => {
1477
+ const example = readFileSync(
1478
+ join(repoRoot, 'opencode.mcp.example.json'),
1479
+ 'utf8',
1480
+ )
1481
+ const packageRoot = createPackage({ agents: {}, mcpExample: example })
1482
+ const consumerRoot = createTempDir()
1483
+
1484
+ runSync({ packageRoot, consumerRoot })
1485
+
1486
+ const config = JSON.parse(
1487
+ readFileSync(join(consumerRoot, 'opencode.json'), 'utf8'),
1488
+ )
1489
+ expect(config.instructions).toEqual(['AGENTS.project.md'])
1490
+ })
1491
+
1492
+ it('does not ship project-owned files in the npm package', () => {
1493
+ const packageManifest = JSON.parse(
1494
+ readFileSync(join(repoRoot, 'package.json'), 'utf8'),
1495
+ )
1496
+ expect(packageManifest.files).not.toContain('AGENTS.project.md')
1497
+ expect(packageManifest.files).not.toContain('opencode.json')
1498
+ expect(packageManifest.files).toContain('.prettierignore')
1499
+
1500
+ const packed = spawnSync('npm', ['pack', '--dry-run', '--json'], {
1501
+ cwd: repoRoot,
1502
+ encoding: 'utf8',
1503
+ })
1504
+ expect(packed.status).toBe(0)
1505
+ const files = JSON.parse(packed.stdout)[0].files.map((file) => file.path)
1506
+ expect(files).toContain('.prettierignore')
1507
+ expect(files).not.toContain('AGENTS.project.md')
1508
+ expect(files).not.toContain('opencode.json')
1509
+ })
1510
+
1511
+ it('reserves every documented project-owned path', () => {
1512
+ for (const relativePath of [
1513
+ 'AGENTS.project.md',
1514
+ 'opencode.json',
1515
+ '.prettierignore',
1516
+ '.agents-sync-ignore',
1517
+ 'instructions/learned-knowledge.instructions.md',
1518
+ ]) {
1519
+ expect(projectOwnedPaths.has(relativePath), relativePath).toBe(true)
1520
+ }
1521
+ })
1522
+ })
1523
+
985
1524
  // ── Prune decisions ──────────────────────────────────────────────────────────
986
1525
 
987
1526
  const PRUNE_CONTENT = {