@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.
- package/.agents/config/README.md +2 -2
- package/.agents/instructions/agent.instructions.md +16 -16
- package/.agents/instructions/context7.instructions.md +40 -0
- package/.agents/instructions/learned-knowledge.instructions.md +18 -0
- package/.agents/instructions/memory-bank.instructions.md +3 -3
- package/.opencode/agents/{architect.agent.md → architect.md} +5 -6
- package/.opencode/agents/{coder.agent.md → coder.md} +4 -8
- package/.opencode/agents/{deployer.agent.md → deployer.md} +4 -9
- package/.opencode/agents/{designer.agent.md → designer.md} +5 -6
- package/.opencode/agents/{e2e-tester.agent.md → e2e-tester.md} +4 -7
- package/.opencode/agents/{implementer.agent.md → implementer.md} +4 -8
- package/.opencode/agents/orchestrator/{feature-pipeline.agent.md → feature-pipeline.md} +16 -19
- package/.opencode/agents/orchestrator/{orchestrator.agent.md → orchestrator.md} +26 -34
- package/.opencode/agents/orchestrator/{tdd-orchestrator.agent.md → tdd-orchestrator.md} +26 -29
- package/.opencode/agents/{plan.agent.md → planner.md} +4 -9
- package/.opencode/agents/{researcher.agent.md → researcher.md} +5 -9
- package/.opencode/agents/{reviewer.agent.md → reviewer.md} +5 -8
- package/.opencode/agents/{scope.agent.md → scope.md} +5 -9
- package/.opencode/agents/{tracker.agent.md → tracker.md} +5 -9
- package/.opencode/agents/{unit-tester.agent.md → unit-tester.md} +4 -8
- package/.prettierignore +26 -0
- package/AGENTS.md +10 -6
- package/README.md +36 -12
- package/opencode.mcp.example.json +13 -12
- package/package.json +3 -1
- package/scripts/sync-unit.test.js +551 -12
- 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('
|
|
601
|
+
it('never resolves project-owned files as prune entries', () => {
|
|
592
602
|
const ctx = context()
|
|
593
603
|
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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(
|
|
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['
|
|
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('
|
|
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
|
-
|
|
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 = {
|